From 6e64ede90b8b1f9dc35a2994cafb14c43db730d6 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Feb 2021 17:01:46 +0100 Subject: [PATCH 1/8] Add a starting widget for the word liste ditor and connect it to the GUI --- nw/core/options.py | 4 ++ nw/gui/__init__.py | 2 + nw/gui/mainmenu.py | 13 +++-- nw/gui/wordlist.py | 142 +++++++++++++++++++++++++++++++++++++++++++++ nw/guimain.py | 18 +++++- 5 files changed, 174 insertions(+), 5 deletions(-) create mode 100644 nw/gui/wordlist.py diff --git a/nw/core/options.py b/nw/core/options.py index 52a30af5..51fba3b6 100644 --- a/nw/core/options.py +++ b/nw/core/options.py @@ -104,6 +104,10 @@ class OptionState(): "countFrom", "clearDouble", }, + "GuiWordList": { + "winWidth", + "winHeight", + } } return diff --git a/nw/gui/__init__.py b/nw/gui/__init__.py index aa7455b5..8744348f 100644 --- a/nw/gui/__init__.py +++ b/nw/gui/__init__.py @@ -20,6 +20,7 @@ from nw.gui.projtree import GuiProjectTree from nw.gui.projwizard import GuiProjectWizard from nw.gui.statusbar import GuiMainStatus from nw.gui.theme import GuiTheme +from nw.gui.wordlist import GuiWordList from nw.gui.writingstats import GuiWritingStats __all__ = [ @@ -44,5 +45,6 @@ __all__ = [ "GuiProjectTree", "GuiProjectWizard", "GuiTheme", + "GuiWordList", "GuiWritingStats", ] diff --git a/nw/gui/mainmenu.py b/nw/gui/mainmenu.py index 4cf4960a..6d6267a9 100644 --- a/nw/gui/mainmenu.py +++ b/nw/gui/mainmenu.py @@ -910,23 +910,28 @@ class GuiMainMenu(QMenuBar): # Tools self.toolsMenu = self.addMenu("&Tools") - # Tools > Toggle Spell Check + # Tools > Check Spelling self.aSpellCheck = QAction("Check Spelling", self) self.aSpellCheck.setStatusTip("Toggle check spelling") self.aSpellCheck.setCheckable(True) self.aSpellCheck.setChecked(self.theProject.spellCheck) - # Here we must used triggered, not toggled, to avoid recursion - self.aSpellCheck.triggered.connect(self._toggleSpellCheck) + self.aSpellCheck.triggered.connect(self._toggleSpellCheck) # triggered, not toggled! self.aSpellCheck.setShortcut("Ctrl+F7") self.toolsMenu.addAction(self.aSpellCheck) - # Tools > Update Spell Check + # Tools > Re-Run Spell Check self.aReRunSpell = QAction("Re-Run Spell Check", self) self.aReRunSpell.setStatusTip("Run the spell checker on current document") self.aReRunSpell.setShortcut("F7") self.aReRunSpell.triggered.connect(lambda: self.theParent.docEditor.spellCheckDocument()) self.toolsMenu.addAction(self.aReRunSpell) + # Tools > Project Word List + self.aEditWordList = QAction("Project Word List", self) + self.aEditWordList.setStatusTip("Edit the project's word list") + self.aEditWordList.triggered.connect(lambda: self.theParent.showProjectWordListDialog()) + self.toolsMenu.addAction(self.aEditWordList) + # Tools > Separator self.toolsMenu.addSeparator() diff --git a/nw/gui/wordlist.py b/nw/gui/wordlist.py new file mode 100644 index 00000000..fcf8682f --- /dev/null +++ b/nw/gui/wordlist.py @@ -0,0 +1,142 @@ +# -*- coding: utf-8 -*- +""" +novelWriter – GUI User Wordlist +=============================== +Class holding the user's wordlist dialog + +File History: +Created: 2021-02-12 [1.2b1] + +This file is a part of novelWriter +Copyright 2018–2021, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import nw +import logging +import os + +from PyQt5.QtWidgets import ( + QDialog, QDialogButtonBox, QVBoxLayout, QListWidget, QAbstractItemView +) + +from nw.constants import nwFiles + +logger = logging.getLogger(__name__) + +class GuiWordList(QDialog): + + def __init__(self, theParent, theProject): + QDialog.__init__(self, theParent) + + logger.debug("Initialising GuiWordList ...") + self.setObjectName("GuiWordList") + + self.mainConf = nw.CONFIG + self.theParent = theParent + self.theProject = theProject + self.optState = theProject.optState + + self.setWindowTitle("Project Word List") + + wW = self.mainConf.pxInt(250) + wH = self.mainConf.pxInt(300) + + self.setMinimumWidth(wW) + self.setMinimumHeight(wH) + self.resize( + self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winWidth", wW)), + self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winHeight", wH)) + ) + + # Main Widgets + # ============ + + self.listBox = QListWidget() + self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop) + self.listBox.setSortingEnabled(True) + + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close) + self.buttonBox.accepted.connect(self._doSave) + self.buttonBox.rejected.connect(self._doClose) + + # Assemble + # ======== + + self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.listBox, 1) + self.outerBox.addWidget(self.buttonBox, 0) + + self.setLayout(self.outerBox) + + self._loadWordList() + + logger.debug("GuiWordList initialisation complete") + + return + + ## + # Slots + ## + + def _doSave(self): + """Save the new word list and close. + """ + self._saveGuiSettings() + self.accept() + return + + def _doClose(self): + """Close without saving the word list. + """ + self._saveGuiSettings() + self.reject() + return + + ## + # Internal Functions + ## + + def _loadWordList(self): + """Load the project's word list, if it exists. + """ + self.listBox.clear() + + wordList = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT) + if not os.path.isfile(wordList): + logger.debug("No project dictionary file found") + return False + + with open(wordList, mode="r", encoding="utf8") as inFile: + for inLine in inFile: + theWord = inLine.strip() + if len(theWord) == 0: + continue + self.listBox.addItem(theWord) + + return True + + def _saveGuiSettings(self): + """Save GUI settings. + """ + winWidth = self.mainConf.rpxInt(self.width()) + winHeight = self.mainConf.rpxInt(self.height()) + + self.optState.setValue("GuiWordList", "winWidth", winWidth) + self.optState.setValue("GuiWordList", "winHeight", winHeight) + + return + +# END Class GuiWordList diff --git a/nw/guimain.py b/nw/guimain.py index cb2756cc..268862b3 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -43,7 +43,7 @@ from nw.gui import ( GuiDocViewDetails, GuiDocViewer, GuiItemDetails, GuiItemEditor, GuiMainMenu, GuiMainStatus, GuiNovelTree, GuiOutline, GuiOutlineDetails, GuiPreferences, GuiProjectDetails, GuiProjectLoad, GuiProjectSettings, - GuiProjectTree, GuiProjectWizard, GuiTheme, GuiWritingStats + GuiProjectTree, GuiProjectWizard, GuiTheme, GuiWordList, GuiWritingStats ) from nw.core import NWProject, NWDoc, NWIndex from nw.constants import nwItemType, nwItemClass, nwAlert, nwLists @@ -1017,6 +1017,22 @@ class GuiMain(QMainWindow): return + def showProjectWordListDialog(self): + """Open the project word list dialog. + """ + if not self.hasProject: + logger.error("No project open") + return + + dlgWords = GuiWordList(self, self.theProject) + dlgWords.exec_() + + if dlgWords.result() == QDialog.Accepted: + logger.debug("Reloading word list") + # ToDo + + return + def showWritingStatsDialog(self): """Open the session log dialog. """ From b115d4f02dbafa8b8830c4ac3016462e93096d93 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Feb 2021 19:40:10 +0100 Subject: [PATCH 2/8] Finish the word list dialog and make it possible to reload the user dictionary with pyenchant --- nw/core/spellcheck.py | 12 ++++++- nw/gui/wordlist.py | 82 +++++++++++++++++++++++++++++++++++++++---- nw/guimain.py | 2 +- 3 files changed, 88 insertions(+), 8 deletions(-) diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index 8dd33bbe..e221f8b5 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -123,6 +123,7 @@ class NWSpellCheck(): if len(theLine) > 0 and theLine not in self.projDict: self.projDict.append(theLine) logger.debug("Project word list contains %d words" % len(self.projDict)) + except Exception: logger.error("Failed to load project word list") nw.logException() @@ -141,6 +142,7 @@ class NWSpellEnchant(NWSpellCheck): def __init__(self): NWSpellCheck.__init__(self) logger.debug("Enchant spell checking activated") + self.theBroker = None return def setLanguage(self, theLang, projectDict=None): @@ -150,9 +152,15 @@ class NWSpellEnchant(NWSpellCheck): """ try: import enchant - self.theDict = enchant.Dict(theLang) + if self.theBroker is not None: + logger.verbose("Deleting old pyenchant Broker") + del self.theBroker + + self.theBroker = enchant.Broker() + self.theDict = self.theBroker.request_dict(theLang) self.spellLanguage = theLang logger.debug("Enchant spell checking for language %s loaded" % theLang) + except Exception: logger.error("Failed to load enchant spell checking for language %s" % theLang) self.theDict = NWSpellEnchantDummy() @@ -258,9 +266,11 @@ class NWSpellSimple(NWSpellCheck): if len(theLine) == 0 or theLine.startswith("#"): continue self.WORDS.append(theLine.strip().lower()) + logger.debug("Spell check word list for language %s loaded" % theLang) logger.debug("Word list contains %d words" % len(self.WORDS)) self.spellLanguage = theLang + except Exception: logger.error("Failed to load spell check word list for language %s" % theLang) nw.logException() diff --git a/nw/gui/wordlist.py b/nw/gui/wordlist.py index fcf8682f..5ce377cc 100644 --- a/nw/gui/wordlist.py +++ b/nw/gui/wordlist.py @@ -28,11 +28,13 @@ import nw import logging import os +from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( - QDialog, QDialogButtonBox, QVBoxLayout, QListWidget, QAbstractItemView + QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget, + QAbstractItemView, QPushButton, QLineEdit, QLabel ) -from nw.constants import nwFiles +from nw.constants import nwFiles, nwAlert logger = logging.getLogger(__name__) @@ -46,16 +48,18 @@ class GuiWordList(QDialog): self.mainConf = nw.CONFIG self.theParent = theParent + self.theTheme = theParent.theTheme self.theProject = theProject self.optState = theProject.optState self.setWindowTitle("Project Word List") - wW = self.mainConf.pxInt(250) - wH = self.mainConf.pxInt(300) + mS = self.mainConf.pxInt(250) + wW = self.mainConf.pxInt(320) + wH = self.mainConf.pxInt(340) - self.setMinimumWidth(wW) - self.setMinimumHeight(wH) + self.setMinimumWidth(mS) + self.setMinimumHeight(mS) self.resize( self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winWidth", wW)), self.mainConf.pxInt(self.optState.getInt("GuiWordList", "winHeight", wH)) @@ -64,10 +68,27 @@ class GuiWordList(QDialog): # Main Widgets # ============ + self.headLabel = QLabel("Project Word List") + self.listBox = QListWidget() self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop) self.listBox.setSortingEnabled(True) + self.newEntry = QLineEdit() + + self.addButton = QPushButton(self.theTheme.getIcon("add"), "") + self.addButton.setToolTip("Add new entry") + self.addButton.clicked.connect(self._doAdd) + + self.delButton = QPushButton(self.theTheme.getIcon("remove"), "") + self.delButton.setToolTip("Delete selected entry") + self.delButton.clicked.connect(self._doDelete) + + self.editBox = QHBoxLayout() + self.editBox.addWidget(self.newEntry, 1) + self.editBox.addWidget(self.addButton, 0) + self.editBox.addWidget(self.delButton, 0) + self.buttonBox = QDialogButtonBox(QDialogButtonBox.Save | QDialogButtonBox.Close) self.buttonBox.accepted.connect(self._doSave) self.buttonBox.rejected.connect(self._doClose) @@ -76,7 +97,11 @@ class GuiWordList(QDialog): # ======== self.outerBox = QVBoxLayout() + self.outerBox.addWidget(self.headLabel) + self.outerBox.addSpacing(self.mainConf.pxInt(8)) self.outerBox.addWidget(self.listBox, 1) + self.outerBox.addLayout(self.editBox, 0) + self.outerBox.addSpacing(self.mainConf.pxInt(12)) self.outerBox.addWidget(self.buttonBox, 0) self.setLayout(self.outerBox) @@ -91,11 +116,56 @@ class GuiWordList(QDialog): # Slots ## + def _doAdd(self): + """Add a new word to the word list. + """ + newWord = self.newEntry.text().strip() + if newWord == "": + self.theParent.makeAlert("Cannot add a blank word.", nwAlert.ERROR) + return False + + if self.listBox.findItems(newWord, Qt.MatchExactly): + self.theParent.makeAlert( + "The word '%s' is already in the word list." % newWord, nwAlert.ERROR + ) + return False + + self.listBox.addItem(newWord) + self.newEntry.setText("") + + return True + + def _doDelete(self): + """Delete the selected item. + """ + selItem = self.listBox.selectedItems() + if selItem: + self.listBox.takeItem(self.listBox.row(selItem[0])) + return + def _doSave(self): """Save the new word list and close. """ self._saveGuiSettings() + + dctFile = os.path.join(self.theProject.projMeta, nwFiles.PROJ_DICT) + tmpFile = dctFile + "~" + + try: + with open(tmpFile, mode="w", encoding="utf8") as outFile: + for i in range(self.listBox.count()): + outFile.write(self.listBox.item(i).text() + "\n") + + except Exception as e: + logger.error("Could not save new word list") + logger.error(str(e)) + self.reject() + + if os.path.isfile(dctFile): + os.unlink(dctFile) + os.rename(tmpFile, dctFile) self.accept() + return def _doClose(self): diff --git a/nw/guimain.py b/nw/guimain.py index 268862b3..6cb68cb3 100644 --- a/nw/guimain.py +++ b/nw/guimain.py @@ -1029,7 +1029,7 @@ class GuiMain(QMainWindow): if dlgWords.result() == QDialog.Accepted: logger.debug("Reloading word list") - # ToDo + self.docEditor.setDictionaries() return From a3b46581b0d1daff44109da93cf24e18f87a25ad Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Feb 2021 20:15:28 +0100 Subject: [PATCH 3/8] Make the simple spell checker use a set instead of a list --- nw/core/spellcheck.py | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index 8b3807fa..caa8156b 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -238,7 +238,7 @@ class NWSpellSimple(NWSpellCheck): when no other is available. This method is fairly slow compared to other implementations. """ - WORDS = [] + theWords = set() def __init__(self): NWSpellCheck.__init__(self) @@ -250,17 +250,19 @@ class NWSpellSimple(NWSpellCheck): """Load a dictionary as a list from the app assets folder. """ self.theLang = theLang - self.WORDS = [] + self.theWords = set() dictFile = os.path.join(self.mainConf.dictPath, theLang+".dict") try: with open(dictFile, mode="r", encoding="utf-8") as wordsFile: for theLine in wordsFile: if len(theLine) == 0 or theLine.startswith("#"): continue - self.WORDS.append(theLine.strip().lower()) - logger.debug("Spell check word list for language %s loaded" % theLang) - logger.debug("Word list contains %d words" % len(self.WORDS)) + self.theWords.add(theLine.strip().lower()) + + logger.debug("Spell check dictionary for language %s loaded" % theLang) + logger.debug("Dictionary contains %d words" % len(self.theWords)) self.spellLanguage = theLang + except Exception as e: logger.error("Failed to load spell check word list for language %s" % theLang) logger.error(str(e)) @@ -268,8 +270,7 @@ class NWSpellSimple(NWSpellCheck): self._readProjectDictionary(projectDict) for pWord in self.projDict: - if pWord not in self.WORDS: - self.WORDS.append(pWord) + self.theWords.add(pWord) return @@ -279,7 +280,7 @@ class NWSpellSimple(NWSpellCheck): word by the syntax highlighter. """ theWord = theWord.replace(self.mainConf.fmtApostrophe, "'").lower() - return theWord in self.WORDS + return theWord in self.theWords def suggestWords(self, theWord): """Get suggestions for correct word from difflib, and make sure @@ -292,7 +293,7 @@ class NWSpellSimple(NWSpellCheck): if len(theWord) == 0: return [] - theMatches = difflib.get_close_matches(theWord.lower(), self.WORDS, n=10, cutoff=0.75) + theMatches = difflib.get_close_matches(theWord.lower(), self.theWords, n=10, cutoff=0.75) theOptions = [] for aWord in theMatches: if len(aWord) == 0: @@ -308,8 +309,8 @@ class NWSpellSimple(NWSpellCheck): """Wrapper for the internal project dictionary feature. """ newWord = newWord.strip().lower() - if newWord not in self.WORDS: - self.WORDS.append(newWord) + if newWord not in self.theWords: + self.theWords.add(newWord) NWSpellCheck.addWord(self, newWord) return From ec1c664ed68411998be2c0c8ef8d0c256cb3b1c1 Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Feb 2021 20:15:38 +0100 Subject: [PATCH 4/8] Fix test --- tests/test_core/test_core_spell.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/test_core/test_core_spell.py b/tests/test_core/test_core_spell.py index 4a9bde15..90ef0fff 100644 --- a/tests/test_core/test_core_spell.py +++ b/tests/test_core/test_core_spell.py @@ -139,13 +139,13 @@ def testCoreSpell_Simple(monkeypatch, tmpDir, tmpConf): monkeypatch.setattr("builtins.open", causeOSError) spChk.setLanguage("en", wList) assert spChk.spellLanguage is None - assert spChk.WORDS == spChk.projDict + assert spChk.theWords == set(spChk.projDict) monkeypatch.undo() # Load dictionary properly spChk.setLanguage("en", wList) assert spChk.projDict == ["a_word", "b_word", "c_word"] - assert spChk.WORDS == ["e_word", "f_word", "g_word", "a_word", "b_word", "c_word"] + assert spChk.theWords == set(["e_word", "f_word", "g_word", "a_word", "b_word", "c_word"]) # Check words assert spChk.checkWord("a_word") From d08322c1a37bb58fe8e0aab913ac3c98cb9a486b Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Feb 2021 20:18:32 +0100 Subject: [PATCH 5/8] No point making a list to convert to a set --- tests/test_core/test_core_spell.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_core/test_core_spell.py b/tests/test_core/test_core_spell.py index 90ef0fff..46ee1937 100644 --- a/tests/test_core/test_core_spell.py +++ b/tests/test_core/test_core_spell.py @@ -145,7 +145,7 @@ def testCoreSpell_Simple(monkeypatch, tmpDir, tmpConf): # Load dictionary properly spChk.setLanguage("en", wList) assert spChk.projDict == ["a_word", "b_word", "c_word"] - assert spChk.theWords == set(["e_word", "f_word", "g_word", "a_word", "b_word", "c_word"]) + assert spChk.theWords == {"e_word", "f_word", "g_word", "a_word", "b_word", "c_word"} # Check words assert spChk.checkWord("a_word") From ff1d625dc8eef15cc1ffac792717be8710c7740e Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Feb 2021 20:26:37 +0100 Subject: [PATCH 6/8] Fix logging of exception --- nw/gui/wordlist.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/nw/gui/wordlist.py b/nw/gui/wordlist.py index 5ce377cc..0d3e3072 100644 --- a/nw/gui/wordlist.py +++ b/nw/gui/wordlist.py @@ -156,9 +156,9 @@ class GuiWordList(QDialog): for i in range(self.listBox.count()): outFile.write(self.listBox.item(i).text() + "\n") - except Exception as e: + except Exception: logger.error("Could not save new word list") - logger.error(str(e)) + nw.logException() self.reject() if os.path.isfile(dctFile): From 11e3573ebd0c777494e1fc80db01b97a3ae698d3 Mon Sep 17 00:00:00 2001 From: app4soft Date: Fri, 12 Feb 2021 22:00:09 +0200 Subject: [PATCH 7/8] Fix installation guide links order --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 488d39be..a884f462 100644 --- a/README.md +++ b/README.md @@ -121,8 +121,8 @@ to quickly navigate between the documents while editing. # Installing and Running -For install instructions, please check the [documentation](https://novelwriter.readthedocs.io/) in -the [Getting Started](https://novelwriter.readthedocs.io/en/latest/int_started.html) section. +For install instructions, please check the [Getting Started](https://novelwriter.readthedocs.io/en/latest/int_started.html) section in +the [documentation](https://novelwriter.readthedocs.io/). ## TLDR Instructions From fca5c7708971d47a513b266764b5f593abd2ce4a Mon Sep 17 00:00:00 2001 From: "Veronica K. B. Olsen" <1619840+vkbo@users.noreply.github.com> Date: Fri, 12 Feb 2021 22:00:22 +0100 Subject: [PATCH 8/8] Add GuiWordList test --- nw/core/spellcheck.py | 2 +- nw/gui/wordlist.py | 3 +- tests/test_gui/test_gui_dialogs.py | 2 +- tests/test_gui/test_gui_wordlist.py | 133 ++++++++++++++++++++++++++++ 4 files changed, 137 insertions(+), 3 deletions(-) create mode 100644 tests/test_gui/test_gui_wordlist.py diff --git a/nw/core/spellcheck.py b/nw/core/spellcheck.py index 79c2accc..9d22afcd 100644 --- a/nw/core/spellcheck.py +++ b/nw/core/spellcheck.py @@ -153,7 +153,7 @@ class NWSpellEnchant(NWSpellCheck): try: import enchant if self.theBroker is not None: - logger.verbose("Deleting old pyenchant Broker") + logger.verbose("Deleting old pyenchant broker") del self.theBroker self.theBroker = enchant.Broker() diff --git a/nw/gui/wordlist.py b/nw/gui/wordlist.py index 0d3e3072..f5b6fe80 100644 --- a/nw/gui/wordlist.py +++ b/nw/gui/wordlist.py @@ -160,13 +160,14 @@ class GuiWordList(QDialog): logger.error("Could not save new word list") nw.logException() self.reject() + return False if os.path.isfile(dctFile): os.unlink(dctFile) os.rename(tmpFile, dctFile) self.accept() - return + return True def _doClose(self): """Close without saving the word list. diff --git a/tests/test_gui/test_gui_dialogs.py b/tests/test_gui/test_gui_dialogs.py index 243434d3..f4f6f27d 100644 --- a/tests/test_gui/test_gui_dialogs.py +++ b/tests/test_gui/test_gui_dialogs.py @@ -61,7 +61,7 @@ def testGuiDialogs_Quotes(qtbot, monkeypatch, nwGUI, nwMinimal): # END Test testDialogs_Quotes @pytest.mark.gui -def testGuiDialogs_Other(qtbot, monkeypatch, nwGUI, nwMinimal, tmpDir): +def testGuiDialogs_Other(qtbot, monkeypatch, nwGUI, tmpDir): """Various other dialog tests. """ monkeypatch.setattr(QFileDialog, "getExistingDirectory", lambda *args, **kwargs: tmpDir) diff --git a/tests/test_gui/test_gui_wordlist.py b/tests/test_gui/test_gui_wordlist.py new file mode 100644 index 00000000..fdbc1725 --- /dev/null +++ b/tests/test_gui/test_gui_wordlist.py @@ -0,0 +1,133 @@ +# -*- coding: utf-8 -*- +""" +novelWriter – Other Dialog Classes Tester +========================================= + +This file is a part of novelWriter +Copyright 2018–2021, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import os + +from PyQt5.QtCore import Qt +from PyQt5.QtWidgets import QDialog, QMessageBox, QAction + +from tools import writeFile, readFile, getGuiItem +from dummy import causeOSError + +from nw.gui.wordlist import GuiWordList +from nw.constants import nwFiles + +keyDelay = 2 +typeDelay = 1 +stepDelay = 20 + +def testGuiWordList_Dialog(qtbot, monkeypatch, nwGUI, nwMinimal, tmpDir): + """test the word list editor. + """ + monkeypatch.setattr(QMessageBox, "question", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *args: QMessageBox.Yes) + monkeypatch.setattr(GuiWordList, "exec_", lambda *args: None) + monkeypatch.setattr(GuiWordList, "result", lambda *args: QDialog.Accepted) + monkeypatch.setattr(GuiWordList, "accept", lambda *args: None) + + # Open project + nwGUI.openProject(nwMinimal) + qtbot.wait(stepDelay) + dictFile = os.path.join(nwMinimal, "meta", nwFiles.PROJ_DICT) + + # Load the dialog + nwGUI.mainMenu.aEditWordList.activate(QAction.Trigger) + qtbot.waitUntil(lambda: getGuiItem("GuiWordList") is not None, timeout=1000) + + wList = getGuiItem("GuiWordList") + assert isinstance(wList, GuiWordList) + wList.show() + qtbot.wait(stepDelay) + + # List should be blank + assert wList.listBox.count() == 0 + + # Add words + writeFile(dictFile, ( + "word_a\n" + "word_c\n" + "word_g\n" + " \n" # Should be ignored + "word_f\n" + "word_b\n" + )) + qtbot.wait(stepDelay) + assert wList._loadWordList() + + # Check that the content was loaded + assert wList.listBox.item(0).text() == "word_a" + assert wList.listBox.item(1).text() == "word_b" + assert wList.listBox.item(2).text() == "word_c" + assert wList.listBox.item(3).text() == "word_f" + assert wList.listBox.item(4).text() == "word_g" + + # Add a blank word + wList.newEntry.setText(" ") + assert not wList._doAdd() + + # Add an existing word + wList.newEntry.setText("word_c") + assert not wList._doAdd() + + # Add a new word + wList.newEntry.setText("word_d") + assert wList._doAdd() + + # Check that the content now + assert wList.listBox.item(0).text() == "word_a" + assert wList.listBox.item(1).text() == "word_b" + assert wList.listBox.item(2).text() == "word_c" + assert wList.listBox.item(3).text() == "word_d" + assert wList.listBox.item(4).text() == "word_f" + assert wList.listBox.item(5).text() == "word_g" + + # Delete a word + wList.newEntry.setText("delete_me") + assert wList._doAdd() + assert wList.listBox.item(0).text() == "delete_me" + + delItem = wList.listBox.findItems("delete_me", Qt.MatchExactly)[0] + assert delItem.text() == "delete_me" + delItem.setSelected(True) + wList._doDelete() + assert wList.listBox.findItems("delete_me", Qt.MatchExactly) == [] + assert wList.listBox.item(0).text() == "word_a" + + # Save files + assert wList._doSave() + assert readFile(dictFile) == ( + "word_a\n" + "word_b\n" + "word_c\n" + "word_d\n" + "word_f\n" + "word_g\n" + ) + + # Save again and make it fail + monkeypatch.setattr("builtins.open", causeOSError) + assert not wList._doSave() + + # qtbot.stopForInteraction() + wList._doClose() + +# END Test testGuiWordList_Dialog