# -*- coding: utf-8 -*- """novelWriter GUI Document Editor novelWriter – GUI Document Editor =================================== Class holding the document editor File History: Created: 2018-09-29 [0.0.1] """ import logging import nw from time import time from PyQt5.QtWidgets import QWidget, QTextEdit, QVBoxLayout, QToolBar, QAction from PyQt5.QtCore import Qt, QSize, QTimer from PyQt5.QtGui import QIcon from nw.gui.dochighlight import GuiDocHighlighter from nw.gui.wordcounter import WordCounter logger = logging.getLogger(__name__) class GuiDocEditor(QWidget): def __init__(self, theParent): QWidget.__init__(self) logger.debug("Initialising DocEditor ...") self.mainConf = nw.CONFIG self.theParent = theParent self.charCount = 0 self.wordCount = 0 self.lineCount = 0 self.lastEdit = 0 self.outerBox = QVBoxLayout() self.guiEditor = QTextEdit() if self.mainConf.textFixedW: self.guiEditor.setLineWrapMode(QTextEdit.FixedPixelWidth) self.guiEditor.setLineWrapColumnOrWidth(self.mainConf.textWidth) else: mTB = self.mainConf.textMargin[0] mLR = self.mainConf.textMargin[1] self.guiEditor.setViewportMargins(mLR,mTB,mLR,mTB) self.hLight = GuiDocHighlighter(self.guiEditor.document()) self.setLayout(self.outerBox) self.editToolBar = QToolBar() self._buildTabToolBar() self.outerBox.addWidget(self.editToolBar) self.outerBox.addWidget(self.guiEditor) self.guiEditor.setMinimumWidth(400) self.guiEditor.setAcceptRichText(False) self.theDoc = self.guiEditor.document() self.theDoc.setDocumentMargin(0) self.theDoc.contentsChange.connect(self._docChange) # Set Up Word Count Thread and Timer self.wcInterval = self.mainConf.wordCountTimer self.wcTimer = QTimer() self.wcTimer.setInterval(int(self.wcInterval*1000)) self.wcTimer.timeout.connect(self._runCounter) self.wCounter = WordCounter(self) self.wCounter.finished.connect(self._updateCounts) logger.debug("DocEditor initialisation complete") return ## # Class Methods ## def setText(self, theText): self.guiEditor.setPlainText(theText) self.lastEdit = time() self._runCounter() self.wcTimer.start() return True def getText(self): theText = self.guiEditor.toPlainText() return theText def changeWidth(self): """Automatically adjust the margins so the text is centred, but only if Config.textFixedW is set to True. """ if self.mainConf.textFixedW: tW = self.guiEditor.width() sW = self.guiEditor.verticalScrollBar().width() tM = int((tW - sW - self.mainConf.textWidth)/2) mTB = self.mainConf.textMargin[0] self.guiEditor.setViewportMargins(tM,mTB,0,mTB) return ## # Document Events and Maintenance ## def _docChange(self, thePos, charsRemoved, charsAdded): self.lastEdit = time() if not self.wcTimer.isActive(): self.wcTimer.start() if self.mainConf.doReplace: self._docAutoReplace(self.theDoc.findBlock(thePos)) logger.verbose("Doc change signal took %.3f µs" % ((time()-self.lastEdit)*1e6)) return def _docAutoReplace(self, theBlock): """Autoreplace text elements based on main configuration. """ if not theBlock.isValid(): return theText = theBlock.text() theCursor = self.guiEditor.textCursor() thePos = theCursor.positionInBlock() theLen = len(theText) if theLen < 1 or thePos-1 > theLen: return theOne = theText[thePos-1:thePos] theTwo = theText[thePos-2:thePos] theThree = theText[thePos-3:thePos] if self.mainConf.doReplaceQuote and theOne == "\"": qOpen = self.mainConf.replaceQuotes[0] qClose = self.mainConf.replaceQuotes[1] nOpen = theText.count(qOpen) nClose = theText.count(qClose) if nOpen > nClose: self.guiEditor.textCursor().deletePreviousChar() self.guiEditor.textCursor().insertText(qClose) else: self.guiEditor.textCursor().deletePreviousChar() self.guiEditor.textCursor().insertText(qOpen) elif self.mainConf.doReplaceDash and theTwo == "--": self.guiEditor.textCursor().deletePreviousChar() self.guiEditor.textCursor().deletePreviousChar() self.guiEditor.textCursor().insertText("–") elif self.mainConf.doReplaceDash and theTwo == "–-": self.guiEditor.textCursor().deletePreviousChar() self.guiEditor.textCursor().deletePreviousChar() self.guiEditor.textCursor().insertText("—") elif self.mainConf.doReplaceDots and theThree == "...": self.guiEditor.textCursor().deletePreviousChar() self.guiEditor.textCursor().deletePreviousChar() self.guiEditor.textCursor().deletePreviousChar() self.guiEditor.textCursor().insertText("…") return def _runCounter(self): """Decide whether to run the word counter, or stop the timer due to inactivity. """ sinceActive = time()-self.lastEdit if sinceActive > 5*self.wcInterval: logger.verbose("Stopping word count timer due to no activity over the last %.3f seconds" % sinceActive) self.wcTimer.stop() elif self.wCounter.isRunning(): logger.verbose("Word counter thread is busy") else: logger.verbose("Starting word counter") self.wCounter.start() return def _updateCounts(self): """Slot for the word counter's finished signal """ logger.verbose("Updating word counts") self.charCount = self.wCounter.charCount self.wordCount = self.wCounter.wordCount self.theParent.statusBar.setCharCount(self.charCount) self.theParent.statusBar.setWordCount(self.wordCount) return ## # GUI Builder ## def _buildTabToolBar(self): toolBar = self.editToolBar toolBar.setToolButtonStyle(Qt.ToolButtonIconOnly) toolBar.setIconSize(QSize(16,16)) # Text > Bold tbTextBold = QAction(QIcon.fromTheme("format-text-bold"), "Bold (Ctrl+B)", toolBar) tbTextBold.setShortcut("Ctrl+B") tbTextBold.setStatusTip("Toggle Selected Text Bold") toolBar.addAction(tbTextBold) # Text > Italics tbTextItalic = QAction(QIcon.fromTheme("format-text-italic"), "Italic (Ctrl+I)", toolBar) tbTextItalic.setShortcut("Ctrl+I") tbTextItalic.setStatusTip("Toggle Selected Text Italic") toolBar.addAction(tbTextItalic) # Text > Underline tbTextUnderline = QAction(QIcon.fromTheme("format-text-underline"), "Underline (Ctrl+U)", toolBar) tbTextUnderline.setShortcut("Ctrl+U") tbTextUnderline.setStatusTip("Toggle Selected Text Underline") toolBar.addAction(tbTextUnderline) # Text > Strikethrough tbTextStrikethrough = QAction(QIcon.fromTheme("format-text-strikethrough"), "Strikethrough (Ctrl+D)", toolBar) tbTextStrikethrough.setShortcut("Ctrl+D") tbTextStrikethrough.setStatusTip("Toggle Selected Text Strikethrough") toolBar.addAction(tbTextStrikethrough) # -------------------- toolBar.addSeparator() # Edit > Cut tbEditCut = QAction(QIcon.fromTheme("edit-cut"), "Cut (Ctrl+X)", toolBar) tbEditCut.setShortcut("Ctrl+X") tbEditCut.setStatusTip("Cut Selected Text") toolBar.addAction(tbEditCut) # Edit > Copy tbEditCopy = QAction(QIcon.fromTheme("edit-copy"), "Copy (Ctrl+C)", toolBar) tbEditCopy.setShortcut("Ctrl+C") tbEditCopy.setStatusTip("Copy Selected Text") toolBar.addAction(tbEditCopy) # Edit > Paste tbEditPaste = QAction(QIcon.fromTheme("edit-paste"), "Paste (Ctrl+V)", toolBar) tbEditPaste.setShortcut("Ctrl+V") tbEditPaste.setStatusTip("Paste Text") toolBar.addAction(tbEditPaste) # -------------------- toolBar.addSeparator() # Edit > Undo tbEditUndo = QAction(QIcon.fromTheme("edit-undo"), "Undo (Ctrl+Z)", toolBar) tbEditUndo.setShortcut("Ctrl+Z") tbEditUndo.setStatusTip("Undo Last Change") toolBar.addAction(tbEditUndo) # Edit > Redo tbEditRedo = QAction(QIcon.fromTheme("edit-redo"), "Redo (Ctrl+Y)", toolBar) tbEditRedo.setShortcut("Ctrl+Y") tbEditRedo.setStatusTip("Revert Last Undo") toolBar.addAction(tbEditRedo) return # END Class GuiDocEditor