diff --git a/nw/gui/doceditor.py b/nw/gui/doceditor.py index 61303a2f..90e7072b 100644 --- a/nw/gui/doceditor.py +++ b/nw/gui/doceditor.py @@ -16,9 +16,9 @@ import enchant 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.QtWidgets import QTextEdit, QAction, QMenu, QShortcut +from PyQt5.QtGui import QTextCursor, QTextOption, QIcon, QKeySequence +from PyQt5.QtCore import Qt, QTimer from nw.gui.dochighlight import GuiDocHighlighter from nw.gui.wordcounter import WordCounter @@ -37,6 +37,7 @@ class GuiDocEditor(QTextEdit): self.mainConf = nw.CONFIG self.theParent = theParent self.docChanged = False + self.pwlFile = None # Document Variables self.charCount = 0 @@ -54,7 +55,12 @@ class GuiDocEditor(QTextEdit): # 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) + self.customContextMenuRequested.connect(self._openContextMenu) # Editor State self.hasSelection = False @@ -76,6 +82,9 @@ class GuiDocEditor(QTextEdit): 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() @@ -106,6 +115,13 @@ class GuiDocEditor(QTextEdit): 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 @@ -155,54 +171,63 @@ class GuiDocEditor(QTextEdit): 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() - 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 _openSpellContext(self): + self._openContextMenu(self.cursorRect().center()) + return + + def _openContextMenu(self, thePos): + + theCursor = self.cursorForPosition(thePos) + theCursor.select(QTextCursor.WordUnderCursor) + theWord = theCursor.selectedText() + if theWord == "": + return + if self.theDict.check(theWord): + return + + mnuSuggest = QMenu() + spIcon = QIcon.fromTheme("tools-check-spelling") + 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) + mnuSuggest.addSeparator() + 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)) + + 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 _addWord(self, theCursor): + theWord = theCursor.selectedText().strip() + 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): @@ -328,11 +353,3 @@ class GuiDocEditor(QTextEdit): 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 diff --git a/nw/gui/dochighlight.py b/nw/gui/dochighlight.py index 11d4a0d8..59923808 100644 --- a/nw/gui/dochighlight.py +++ b/nw/gui/dochighlight.py @@ -20,13 +20,13 @@ logger = logging.getLogger(__name__) 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) @@ -145,12 +145,16 @@ class GuiDocHighlighter(QSyntaxHighlighter): # 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") return + def setDict(self, theDict): + self.theDict = theDict + return + def _makeFormat(self, fmtCol=None, fmtStyle=None, fmtSize=None): theFormat = QTextCharFormat() @@ -188,7 +192,7 @@ class GuiDocHighlighter(QSyntaxHighlighter): 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)): diff --git a/nw/gui/winmain.py b/nw/gui/winmain.py index aef80173..ab12372f 100644 --- a/nw/gui/winmain.py +++ b/nw/gui/winmain.py @@ -159,8 +159,9 @@ class GuiMain(QMainWindow): 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): @@ -174,6 +175,7 @@ class GuiMain(QMainWindow): self.mainMenu.updateRecentProjects() self._setWindowTitle(self.theProject.projName) self._makeStatusIcons() + 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 11090f2f..e03d9103 100644 --- a/nw/project/project.py +++ b/nw/project/project.py @@ -39,6 +39,8 @@ class NWProject(): self.treeRoots = None self.trashRoot = None self.projPath = None + self.projMeta = None + self.projCache = None self.projFile = None self.projName = None self.bookTitle = None @@ -116,6 +118,8 @@ class NWProject(): self.treeRoots = [] self.trashRoot = None self.projPath = None + self.projMeta = None + self.projCache = None self.projFile = "nwProject.nwx" self.projName = "" self.bookTitle = "" @@ -141,6 +145,12 @@ class NWProject(): 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() @@ -201,13 +211,12 @@ class NWProject(): 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) @@ -326,6 +335,16 @@ class NWProject(): # 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: