Merge branch 'testing' into dev

This commit is contained in:
Veronica K. B. Olsen
2021-02-12 22:07:46 +01:00
10 changed files with 404 additions and 22 deletions
+2 -2
View File
@@ -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
+4
View File
@@ -104,6 +104,10 @@ class OptionState():
"countFrom",
"clearDouble",
},
"GuiWordList": {
"winWidth",
"winHeight",
}
}
return
+21 -12
View File
@@ -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()
@@ -238,7 +246,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 +258,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:
logger.error("Failed to load spell check word list for language %s" % theLang)
nw.logException()
@@ -268,8 +278,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 +288,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 +301,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 +317,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
+2
View File
@@ -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",
]
+9 -4
View File
@@ -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()
+213
View File
@@ -0,0 +1,213 @@
# -*- 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 20182021, 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 <https://www.gnu.org/licenses/>.
"""
import nw
import logging
import os
from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import (
QDialog, QDialogButtonBox, QVBoxLayout, QHBoxLayout, QListWidget,
QAbstractItemView, QPushButton, QLineEdit, QLabel
)
from nw.constants import nwFiles, nwAlert
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.theTheme = theParent.theTheme
self.theProject = theProject
self.optState = theProject.optState
self.setWindowTitle("Project Word List")
mS = self.mainConf.pxInt(250)
wW = self.mainConf.pxInt(320)
wH = self.mainConf.pxInt(340)
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))
)
# Main Widgets
# ============
self.headLabel = QLabel("<b>Project Word List</b>")
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)
# Assemble
# ========
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)
self._loadWordList()
logger.debug("GuiWordList initialisation complete")
return
##
# 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:
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 True
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
+17 -1
View File
@@ -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")
self.docEditor.setDictionaries()
return
def showWritingStatsDialog(self):
"""Open the session log dialog.
"""
+2 -2
View File
@@ -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 == {"e_word", "f_word", "g_word", "a_word", "b_word", "c_word"}
# Check words
assert spChk.checkWord("a_word")
+1 -1
View File
@@ -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)
+133
View File
@@ -0,0 +1,133 @@
# -*- coding: utf-8 -*-
"""
novelWriter Other Dialog Classes Tester
=========================================
This file is a part of novelWriter
Copyright 20182021, 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 <https://www.gnu.org/licenses/>.
"""
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