From 5adb2f9f977b4816d64b42c562d429f2834f2cc6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Fri, 10 May 2019 19:56:10 +0200 Subject: [PATCH 1/7] Remove the 'hack' to get a word under cursor position as we can just use self.cursorForPosition() --- nw/gui/doceditor.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 61303a2fa..cd03c7125 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -155,25 +155,13 @@ def keyPressEvent(self, keyEvent): QTextEdit.keyPressEvent(self, keyEvent) return - def mousePressEvent(self, theEvent): - """Capture right click events and rewrite them to left button event. This moves the cursor - to the location of the pointer. Needed to select the word under the right click. - Adapted from: https://nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check - """ - if theEvent.button() == Qt.RightButton: - theEvent = QMouseEvent( - QEvent.MouseButtonPress, theEvent.pos(), Qt.LeftButton, Qt.LeftButton, Qt.NoModifier - ) - QTextEdit.mousePressEvent(self, theEvent) - return - def contextMenuEvent(self, theEvent): """Intercept the context menu and insert spelling suggestions, if any. Uses the custom QAction class SpellAction from the same example code. Adapted from: https://nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check """ - mnuSpell = self.createStandardContextMenu() - theCursor = self.textCursor() + mnuSpell = self.createStandardContextMenu(theEvent.pos()) + theCursor = self.cursorForPosition(theEvent.pos()) theCursor.select(QTextCursor.WordUnderCursor) self.setTextCursor(theCursor) From 87c786975a0176c22e071cb1445a59a341f943dd Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Fri, 10 May 2019 20:58:05 +0200 Subject: [PATCH 2/7] Reimplemented the spell check replace word code --- nw/gui/doceditor.py | 76 ++++++++++++++++++++++----------------------- 1 file changed, 37 insertions(+), 39 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index cd03c7125..82baf8e37 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -17,8 +17,8 @@ from time import time from PyQt5.QtWidgets import QTextEdit, QAction, QMenu -from PyQt5.QtCore import Qt, QTimer, QEvent, pyqtSignal -from PyQt5.QtGui import QTextCursor, QTextOption, QMouseEvent +from PyQt5.QtCore import Qt, QTimer +from PyQt5.QtGui import QTextCursor, QTextOption, QIcon from nw.gui.dochighlight import GuiDocHighlighter from nw.gui.wordcounter import WordCounter @@ -56,6 +56,10 @@ def __init__(self, theParent): self.theDict = enchant.Dict(self.mainConf.spellLanguage) self.hLight = GuiDocHighlighter(self.theDoc, self.theDict) + # Context Menu + self.setContextMenuPolicy(Qt.CustomContextMenu) + self.customContextMenuRequested.connect(self._openContextMenu) + # Editor State self.hasSelection = False @@ -155,42 +159,44 @@ def keyPressEvent(self, keyEvent): QTextEdit.keyPressEvent(self, keyEvent) return - def contextMenuEvent(self, theEvent): - """Intercept the context menu and insert spelling suggestions, if any. - Uses the custom QAction class SpellAction from the same example code. - Adapted from: https://nachtimwald.com/2009/08/22/qplaintextedit-with-in-line-spell-check - """ - mnuSpell = self.createStandardContextMenu(theEvent.pos()) - theCursor = self.cursorForPosition(theEvent.pos()) - theCursor.select(QTextCursor.WordUnderCursor) - self.setTextCursor(theCursor) - - if self.textCursor().hasSelection(): - theText = self.textCursor().selectedText() - if not self.theDict.check(theText): - mnuSuggest = QMenu("Spelling Suggestions") - for aWord in self.theDict.suggest(theText): - action = SpellAction(aWord, mnuSuggest) - action.correct.connect(self._correctWord) - mnuSuggest.addAction(action) - if len(mnuSuggest.actions()) > 0: - theActions = mnuSpell.actions() - mnuSpell.insertSeparator(theActions[0]) - mnuSpell.insertMenu(theActions[0], mnuSuggest) - - mnuSpell.exec_(theEvent.globalPos()) - return - ## # Internal Functions ## - def _correctWord(self, word): - theCursor = self.textCursor() + def _openContextMenu(self, thePos): + + theCursor = self.cursorForPosition(thePos) + theCursor.select(QTextCursor.WordUnderCursor) + theText = theCursor.selectedText() + if theText == "": + return + + mnuSuggest = QMenu() + spIcon = QIcon.fromTheme("tools-check-spelling") + if self.theDict.check(theText): + mnuHead = QAction(spIcon,"No Suggestion", mnuSuggest) + mnuSuggest.addAction(mnuHead) + else: + mnuHead = QAction(spIcon,"Spelling Suggestion", mnuSuggest) + mnuSuggest.addAction(mnuHead) + mnuSuggest.addSeparator() + for aWord in self.theDict.suggest(theText): + mnuWord = QAction(aWord, mnuSuggest) + mnuWord.triggered.connect(lambda thePos, aWord=aWord : self._correctWord(theCursor, aWord)) + mnuSuggest.addAction(mnuWord) + + mnuSuggest.exec_(self.viewport().mapToGlobal(thePos)) + + return + + def _correctWord(self, theCursor, theWord): + xPos = theCursor.selectionStart() theCursor.beginEditBlock() theCursor.removeSelectedText() - theCursor.insertText(word) + theCursor.insertText(theWord) theCursor.endEditBlock() + theCursor.setPosition(xPos) + self.setTextCursor(theCursor) return def _docChange(self, thePos, charsRemoved, charsAdded): @@ -316,11 +322,3 @@ def _makeSelection(self, selMode): return # END Class GuiDocEditor - -class SpellAction(QAction): - correct = pyqtSignal(str) - def __init__(self, *args): - QAction.__init__(self, *args) - self.triggered.connect(lambda x: self.correct.emit(self.text())) - -# END Class SpellAction From f26cb7c2814afbffbb910750437a16501a4f7a75 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Fri, 10 May 2019 21:16:11 +0200 Subject: [PATCH 3/7] Open spell check context menu with ctrl+. shortcut --- nw/gui/doceditor.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 82baf8e37..7440e7821 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -16,9 +16,9 @@ from time import time -from PyQt5.QtWidgets import QTextEdit, QAction, QMenu +from PyQt5.QtWidgets import QTextEdit, QAction, QMenu, QShortcut +from PyQt5.QtGui import QTextCursor, QTextOption, QIcon, QKeySequence from PyQt5.QtCore import Qt, QTimer -from PyQt5.QtGui import QTextCursor, QTextOption, QIcon from nw.gui.dochighlight import GuiDocHighlighter from nw.gui.wordcounter import WordCounter @@ -80,6 +80,9 @@ def __init__(self, theParent): self.setAcceptRichText(False) self.setFontPointSize(self.mainConf.textSize) + # Custom Shortcuts + QShortcut(QKeySequence("Ctrl+."), self, context=Qt.WidgetShortcut, activated=self._openSpellContext) + # Set Up Word Count Thread and Timer self.wcInterval = self.mainConf.wordCountTimer self.wcTimer = QTimer() @@ -163,6 +166,10 @@ def keyPressEvent(self, keyEvent): # Internal Functions ## + def _openSpellContext(self): + self._openContextMenu(self.cursorRect().center()) + return + def _openContextMenu(self, thePos): theCursor = self.cursorForPosition(thePos) From d6409a5bf9c294b9e841efb1915811ea23fa0f97 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Fri, 10 May 2019 23:11:46 +0200 Subject: [PATCH 4/7] Spell checker context menu can now add words to dictionary for current project --- nw/gui/doceditor.py | 31 ++++++++++++++++++++++++++----- nw/gui/dochighlight.py | 8 ++++++-- nw/gui/winmain.py | 6 ++++-- nw/project/project.py | 33 ++++++++++++++++++++++++++------- 4 files changed, 62 insertions(+), 16 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 7440e7821..1d287e0ba 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -37,6 +37,7 @@ def __init__(self, theParent): self.mainConf = nw.CONFIG self.theParent = theParent self.docChanged = False + self.pwlFile = None # Document Variables self.charCount = 0 @@ -54,7 +55,8 @@ def __init__(self, theParent): # Core Elements self.theDoc = self.document() self.theDict = enchant.Dict(self.mainConf.spellLanguage) - self.hLight = GuiDocHighlighter(self.theDoc, self.theDict) + self.hLight = GuiDocHighlighter(self.theDoc) + self.hLight.setDict(self.theDict) # Context Menu self.setContextMenuPolicy(Qt.CustomContextMenu) @@ -113,6 +115,13 @@ def setText(self, theText): self.setDocumentChanged(False) return True + def setPwl(self, pwlFile): + if pwlFile is not None: + self.pwlFile = pwlFile + self.theDict = enchant.DictWithPWL(self.mainConf.spellLanguage,pwlFile) + self.hLight.setDict(self.theDict) + return + def getText(self): theText = self.toPlainText() return theText @@ -174,23 +183,27 @@ def _openContextMenu(self, thePos): theCursor = self.cursorForPosition(thePos) theCursor.select(QTextCursor.WordUnderCursor) - theText = theCursor.selectedText() - if theText == "": + theWord = theCursor.selectedText() + if theWord == "": return mnuSuggest = QMenu() spIcon = QIcon.fromTheme("tools-check-spelling") - if self.theDict.check(theText): + if self.theDict.check(theWord): mnuHead = QAction(spIcon,"No Suggestion", mnuSuggest) mnuSuggest.addAction(mnuHead) else: mnuHead = QAction(spIcon,"Spelling Suggestion", mnuSuggest) mnuSuggest.addAction(mnuHead) mnuSuggest.addSeparator() - for aWord in self.theDict.suggest(theText): + for aWord in self.theDict.suggest(theWord): mnuWord = QAction(aWord, mnuSuggest) mnuWord.triggered.connect(lambda thePos, aWord=aWord : self._correctWord(theCursor, aWord)) mnuSuggest.addAction(mnuWord) + mnuSuggest.addSeparator() + mnuAdd = QAction("Add Word to Dictionary", mnuSuggest) + mnuAdd.triggered.connect(lambda thePos : self._addWord(theCursor)) + mnuSuggest.addAction(mnuAdd) mnuSuggest.exec_(self.viewport().mapToGlobal(thePos)) @@ -206,6 +219,14 @@ def _correctWord(self, theCursor, theWord): self.setTextCursor(theCursor) return + def _addWord(self, theCursor): + theWord = theCursor.selectedText() + logger.info("Added '%s' to project dictionary" % theWord) + self.theDict.add_to_pwl(theWord) + self.hLight.setDict(self.theDict) + self.hLight.rehighlightBlock(theCursor.block()) + return + def _docChange(self, thePos, charsRemoved, charsAdded): self.lastEdit = time() self.setDocumentChanged(True) diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 11d4a0d82..56d76f1ea 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -20,13 +20,13 @@ class GuiDocHighlighter(QSyntaxHighlighter): - def __init__(self, theDoc, theDict): + def __init__(self, theDoc): QSyntaxHighlighter.__init__(self, theDoc) logger.debug("Initialising DocHighlighter ...") self.mainConf = nw.CONFIG self.theDoc = theDoc - self.theDict = theDict + self.theDict = None self.hRules = [] self.colHead = QColor( 0,155,200) @@ -151,6 +151,10 @@ def __init__(self, theDoc, theDict): return + def setDict(self, theDict): + self.theDict = theDict + return + def _makeFormat(self, fmtCol=None, fmtStyle=None, fmtSize=None): theFormat = QTextCharFormat() diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index a313757e8..94cc2aacf 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -147,8 +147,9 @@ def makeAlert(self, theMessage, theLevel): def newProject(self): logger.info("Creating new project") self.treeView.clearTree() - self.theProject.newProject() - self.treeView.buildTree() + if self.saveProject(): + self.theProject.newProject() + self.treeView.buildTree() return def openProject(self, projFile=None): @@ -161,6 +162,7 @@ def openProject(self, projFile=None): self.treeView.buildTree() self.mainMenu.updateRecentProjects() self._setWindowTitle(self.theProject.projName) + self.docEditor.setPwl(path.join(self.theProject.projMeta,"wordlist.txt")) return True def saveProject(self): diff --git a/nw/project/project.py b/nw/project/project.py index 1ba4685ae..812a43d2a 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -41,6 +41,8 @@ def __init__(self, theParent): self.treeRoots = None self.trashRoot = None self.projPath = None + self.projMeta = None + self.projCache = None self.projFile = None self.projName = None self.bookTitle = None @@ -120,6 +122,8 @@ def clearProject(self): self.treeRoots = [] self.trashRoot = None self.projPath = None + self.projMeta = None + self.projCache = None self.projFile = "nwProject.nwx" self.projName = "" self.bookTitle = "" @@ -149,6 +153,12 @@ def openProject(self, fileName): self.projPath = path.dirname(fileName) logger.debug("Opening project: %s" % self.projPath) + self.projMeta = path.join(self.projPath,"meta") + self.projCache = path.join(self.projPath,"cache") + + if not self._checkFolder(self.projMeta): return + if not self._checkFolder(self.projCache): return + nwXML = etree.parse(fileName) xRoot = nwXML.getroot() @@ -210,13 +220,12 @@ def saveProject(self): self.theParent.makeAlert("Project path not set, cannot save.",2) return False - if not path.isdir(self.projPath): - try: - mkdir(self.projPath) - logger.info("Created folder %s" % self.projPath) - except Exception as e: - self.theParent.makeAlert(["Could not create folder.",str(e)],2) - return False + self.projMeta = path.join(self.projPath,"meta") + self.projCache = path.join(self.projPath,"cache") + + if not self._checkFolder(self.projPath): return + if not self._checkFolder(self.projMeta): return + if not self._checkFolder(self.projCache): return logger.debug("Saving project: %s" % self.projPath) @@ -335,6 +344,16 @@ def checkRootUnique(self, theClass): # Internal Functions ## + def _checkFolder(self, thePath): + if not path.isdir(thePath): + try: + mkdir(thePath) + logger.info("Created folder %s" % thePath) + except Exception as e: + self.theParent.makeAlert(["Could not create folder.",str(e)],2) + return False + return True + def _scanProjectFolder(self): if self.projPath is None: From 00bc83543dac00beb3fbc5d1e0abd7e69f2d59f7 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Fri, 10 May 2019 23:16:15 +0200 Subject: [PATCH 5/7] Improved the suggestion menu --- nw/gui/doceditor.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 1d287e0ba..90e7072b8 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -186,17 +186,17 @@ def _openContextMenu(self, thePos): theWord = theCursor.selectedText() if theWord == "": return + if self.theDict.check(theWord): + return mnuSuggest = QMenu() spIcon = QIcon.fromTheme("tools-check-spelling") - if self.theDict.check(theWord): - mnuHead = QAction(spIcon,"No Suggestion", mnuSuggest) - mnuSuggest.addAction(mnuHead) - else: - mnuHead = QAction(spIcon,"Spelling Suggestion", mnuSuggest) - mnuSuggest.addAction(mnuHead) - mnuSuggest.addSeparator() - for aWord in self.theDict.suggest(theWord): + mnuHead = QAction(spIcon,"Spelling Suggestion", mnuSuggest) + mnuSuggest.addAction(mnuHead) + mnuSuggest.addSeparator() + theSuggest = self.theDict.suggest(theWord) + if len(theSuggest) > 0: + for aWord in theSuggest: mnuWord = QAction(aWord, mnuSuggest) mnuWord.triggered.connect(lambda thePos, aWord=aWord : self._correctWord(theCursor, aWord)) mnuSuggest.addAction(mnuWord) @@ -204,6 +204,9 @@ def _openContextMenu(self, thePos): mnuAdd = QAction("Add Word to Dictionary", mnuSuggest) mnuAdd.triggered.connect(lambda thePos : self._addWord(theCursor)) mnuSuggest.addAction(mnuAdd) + else: + mnuHead = QAction("No Suggestions", mnuSuggest) + mnuSuggest.addAction(mnuHead) mnuSuggest.exec_(self.viewport().mapToGlobal(thePos)) @@ -220,7 +223,7 @@ def _correctWord(self, theCursor, theWord): return def _addWord(self, theCursor): - theWord = theCursor.selectedText() + theWord = theCursor.selectedText().strip() logger.info("Added '%s' to project dictionary" % theWord) self.theDict.add_to_pwl(theWord) self.hLight.setDict(self.theDict) From a59f9fe8cbd5b4480ec52777fa60302a4b8b4733 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Fri, 10 May 2019 23:26:29 +0200 Subject: [PATCH 6/7] Fixed the spell check regex for italics and underscore text --- nw/gui/dochighlight.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 56d76f1ea..5efad4054 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -145,7 +145,7 @@ def __init__(self, theDoc): # Build a QRegExp for each pattern and for the spell checker self.rules = [(QRegularExpression(a),b) for (a,b) in self.hRules] - self.spellRx = QRegularExpression(r"[\w\'{:s}]+".format(self.mainConf.fmtApostrophe)) + self.spellRx = QRegularExpression(r"[\w\'{:s}&&[^_]]+".format(self.mainConf.fmtApostrophe)) logger.debug("DocHighlighter initialisation complete") From 73f1547e9f38d20f1ddec0fc4fc0138466ba399e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" Date: Sat, 11 May 2019 00:26:19 +0200 Subject: [PATCH 7/7] Using a better word splitting regex, but need to replace underscores with spaces. --- nw/gui/dochighlight.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 5efad4054..599238080 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -145,7 +145,7 @@ def __init__(self, theDoc): # Build a QRegExp for each pattern and for the spell checker self.rules = [(QRegularExpression(a),b) for (a,b) in self.hRules] - self.spellRx = QRegularExpression(r"[\w\'{:s}&&[^_]]+".format(self.mainConf.fmtApostrophe)) + self.spellRx = QRegularExpression(r"\b[^\s]+\b") logger.debug("DocHighlighter initialisation complete") @@ -192,7 +192,7 @@ def highlightBlock(self, theText): if self.theDict is None: return - rxSpell = self.spellRx.globalMatch(theText, 0) + rxSpell = self.spellRx.globalMatch(theText.replace("_"," "), 0) while rxSpell.hasNext(): rxMatch = rxSpell.next() if not self.theDict.check(rxMatch.captured(0)):