Move the spell check instance to the shared class
This commit is contained in:
@@ -47,11 +47,15 @@ class NWSpellEnchant:
|
|||||||
|
|
||||||
def __init__(self, project: NWProject) -> None:
|
def __init__(self, project: NWProject) -> None:
|
||||||
self._project = project
|
self._project = project
|
||||||
self._dictObj = FakeEnchant()
|
self._enchant = FakeEnchant()
|
||||||
self._userDict = UserDictionary(project)
|
self._userDict = UserDictionary(project)
|
||||||
self._language = None
|
self._language = None
|
||||||
self._broker = None
|
self._broker = None
|
||||||
logger.debug("Enchant spell checking activated")
|
logger.debug("Ready: NWSpellEnchant")
|
||||||
|
return
|
||||||
|
|
||||||
|
def __del__(self): # pragma: no cover
|
||||||
|
logger.debug("Delete: NWSpellEnchant")
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -72,7 +76,7 @@ class NWSpellEnchant:
|
|||||||
crash. Note that enchant will allow loading an empty string as
|
crash. Note that enchant will allow loading an empty string as
|
||||||
a tag, but this will fail later on. See issue #1096.
|
a tag, but this will fail later on. See issue #1096.
|
||||||
"""
|
"""
|
||||||
self._dictObj = FakeEnchant()
|
self._enchant = FakeEnchant()
|
||||||
self._broker = None
|
self._broker = None
|
||||||
self._language = None
|
self._language = None
|
||||||
|
|
||||||
@@ -81,7 +85,7 @@ class NWSpellEnchant:
|
|||||||
|
|
||||||
if language and enchant.dict_exists(language):
|
if language and enchant.dict_exists(language):
|
||||||
self._broker = enchant.Broker()
|
self._broker = enchant.Broker()
|
||||||
self._dictObj = self._broker.request_dict(language)
|
self._enchant = self._broker.request_dict(language)
|
||||||
self._language = language
|
self._language = language
|
||||||
logger.debug("Enchant spell checking for language '%s' loaded", language)
|
logger.debug("Enchant spell checking for language '%s' loaded", language)
|
||||||
else:
|
else:
|
||||||
@@ -90,12 +94,11 @@ class NWSpellEnchant:
|
|||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Failed to load enchant spell checking for language '%s'", language)
|
logger.error("Failed to load enchant spell checking for language '%s'", language)
|
||||||
|
|
||||||
if self._dictObj is None:
|
if self._enchant is None:
|
||||||
self._dictObj = FakeEnchant()
|
self._enchant = FakeEnchant()
|
||||||
else:
|
else:
|
||||||
self._userDict.load()
|
for word in self._userDict:
|
||||||
for pWord in self._userDict:
|
self._enchant.add_to_session(word)
|
||||||
self._dictObj.add_to_session(pWord)
|
|
||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -106,14 +109,14 @@ class NWSpellEnchant:
|
|||||||
def checkWord(self, word: str) -> bool:
|
def checkWord(self, word: str) -> bool:
|
||||||
"""Wrapper function for pyenchant."""
|
"""Wrapper function for pyenchant."""
|
||||||
try:
|
try:
|
||||||
return bool(self._dictObj.check(word))
|
return bool(self._enchant.check(word))
|
||||||
except Exception:
|
except Exception:
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def suggestWords(self, word: str) -> list[str]:
|
def suggestWords(self, word: str) -> list[str]:
|
||||||
"""Wrapper function for pyenchant."""
|
"""Wrapper function for pyenchant."""
|
||||||
try:
|
try:
|
||||||
return self._dictObj.suggest(word)
|
return self._enchant.suggest(word)
|
||||||
except Exception:
|
except Exception:
|
||||||
return []
|
return []
|
||||||
|
|
||||||
@@ -123,7 +126,7 @@ class NWSpellEnchant:
|
|||||||
if not word:
|
if not word:
|
||||||
return False
|
return False
|
||||||
try:
|
try:
|
||||||
self._dictObj.add_to_session(word)
|
self._enchant.add_to_session(word)
|
||||||
except Exception:
|
except Exception:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
@@ -133,6 +136,11 @@ class NWSpellEnchant:
|
|||||||
|
|
||||||
return added
|
return added
|
||||||
|
|
||||||
|
def loadUserWordList(self) -> None:
|
||||||
|
"""Load the user word list from the project."""
|
||||||
|
self._userDict.load()
|
||||||
|
return
|
||||||
|
|
||||||
def listDictionaries(self) -> list[tuple[str, str]]:
|
def listDictionaries(self) -> list[tuple[str, str]]:
|
||||||
"""Wrapper function for pyenchant."""
|
"""Wrapper function for pyenchant."""
|
||||||
retList = []
|
retList = []
|
||||||
@@ -142,22 +150,18 @@ class NWSpellEnchant:
|
|||||||
retList.append((spTag, spProvider.name))
|
retList.append((spTag, spProvider.name))
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Failed to list languages for enchant spell checking")
|
logger.error("Failed to list languages for enchant spell checking")
|
||||||
|
|
||||||
return retList
|
return retList
|
||||||
|
|
||||||
def describeDict(self) -> tuple[str, str]:
|
def describeDict(self) -> tuple[str, str]:
|
||||||
"""Return the tag and provider of the currently loaded
|
"""Describe the currently loaded dictionary."""
|
||||||
dictionary.
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
tag = self._dictObj.tag
|
tag = self._enchant.tag
|
||||||
name = self._dictObj.provider.name # type: ignore
|
name = self._enchant.provider.name # type: ignore
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Failed to extract information about the dictionary")
|
logger.error("Failed to extract information about the dictionary")
|
||||||
logException()
|
logException()
|
||||||
tag = ""
|
tag = ""
|
||||||
name = ""
|
name = ""
|
||||||
|
|
||||||
return tag, name
|
return tag, name
|
||||||
|
|
||||||
# END Class NWSpellEnchant
|
# END Class NWSpellEnchant
|
||||||
@@ -192,7 +196,6 @@ class UserDictionary:
|
|||||||
def __init__(self, project: NWProject) -> None:
|
def __init__(self, project: NWProject) -> None:
|
||||||
self._project = project
|
self._project = project
|
||||||
self._words = set()
|
self._words = set()
|
||||||
self._path = None
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def __contains__(self, word: str) -> bool:
|
def __contains__(self, word: str) -> bool:
|
||||||
@@ -212,13 +215,14 @@ class UserDictionary:
|
|||||||
|
|
||||||
def load(self) -> None:
|
def load(self) -> None:
|
||||||
"""Load the user's dictionary."""
|
"""Load the user's dictionary."""
|
||||||
self._path = self._project.storage.getMetaFile(nwFiles.DICT_FILE)
|
|
||||||
self._words = set()
|
self._words = set()
|
||||||
if isinstance(self._path, Path) and self._path.is_file():
|
wordList = self._project.storage.getMetaFile(nwFiles.DICT_FILE)
|
||||||
|
if isinstance(wordList, Path) and wordList.is_file():
|
||||||
try:
|
try:
|
||||||
with open(self._path, mode="r", encoding="utf-8") as fObj:
|
with open(wordList, mode="r", encoding="utf-8") as fObj:
|
||||||
data = json.load(fObj)
|
data = json.load(fObj)
|
||||||
self._words = set(data.get("novelWriter.userDict", []))
|
self._words = set(data.get("novelWriter.userDict", []))
|
||||||
|
logger.info("Loaded: %s", nwFiles.DICT_FILE)
|
||||||
except Exception:
|
except Exception:
|
||||||
logger.error("Failed to load user dictionary")
|
logger.error("Failed to load user dictionary")
|
||||||
logException()
|
logException()
|
||||||
@@ -226,17 +230,15 @@ class UserDictionary:
|
|||||||
|
|
||||||
def save(self) -> None:
|
def save(self) -> None:
|
||||||
"""Save the user's dictionary."""
|
"""Save the user's dictionary."""
|
||||||
if self._path is None:
|
wordList = self._project.storage.getMetaFile(nwFiles.DICT_FILE)
|
||||||
self._path = self._project.storage.getMetaFile(nwFiles.DICT_FILE)
|
if isinstance(wordList, Path):
|
||||||
if not isinstance(self._path, Path):
|
try:
|
||||||
return
|
with open(wordList, mode="w", encoding="utf-8") as fObj:
|
||||||
try:
|
data = {"novelWriter.userDict": list(self._words)}
|
||||||
with open(self._path, mode="w", encoding="utf-8") as fObj:
|
json.dump(data, fObj, indent=2)
|
||||||
data = {"novelWriter.userDict": list(self._words)}
|
except Exception:
|
||||||
json.dump(data, fObj, indent=2)
|
logger.error("Failed to save user dictionary")
|
||||||
except Exception:
|
logException()
|
||||||
logger.error("Failed to save user dictionary")
|
|
||||||
logException()
|
|
||||||
return
|
return
|
||||||
|
|
||||||
# END Class UserDictionary
|
# END Class UserDictionary
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ class GuiPreferences(NPagedDialog):
|
|||||||
logger.debug("Create: GuiPreferences")
|
logger.debug("Create: GuiPreferences")
|
||||||
self.setObjectName("GuiPreferences")
|
self.setObjectName("GuiPreferences")
|
||||||
|
|
||||||
self.mainGui = mainGui
|
|
||||||
|
|
||||||
self.setWindowTitle(self.tr("Preferences"))
|
self.setWindowTitle(self.tr("Preferences"))
|
||||||
|
|
||||||
self.tabGeneral = GuiPreferencesGeneral(self)
|
self.tabGeneral = GuiPreferencesGeneral(self)
|
||||||
@@ -645,8 +643,6 @@ class GuiPreferencesEditor(QWidget):
|
|||||||
def __init__(self, prefsGui):
|
def __init__(self, prefsGui):
|
||||||
super().__init__(parent=prefsGui)
|
super().__init__(parent=prefsGui)
|
||||||
|
|
||||||
self.mainGui = prefsGui.mainGui
|
|
||||||
|
|
||||||
# The Form
|
# The Form
|
||||||
self.mainForm = NConfigLayout()
|
self.mainForm = NConfigLayout()
|
||||||
self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
|
self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
|
||||||
@@ -662,7 +658,7 @@ class GuiPreferencesEditor(QWidget):
|
|||||||
self.spellLanguage = QComboBox(self)
|
self.spellLanguage = QComboBox(self)
|
||||||
self.spellLanguage.setMaximumWidth(mW)
|
self.spellLanguage.setMaximumWidth(mW)
|
||||||
|
|
||||||
langAvail = self.mainGui.docEditor.spEnchant.listDictionaries()
|
langAvail = SHARED.spelling.listDictionaries()
|
||||||
if CONFIG.hasEnchant and langAvail:
|
if CONFIG.hasEnchant and langAvail:
|
||||||
for spTag, spProv in langAvail:
|
for spTag, spProv in langAvail:
|
||||||
qLocal = QLocale(spTag)
|
qLocal = QLocale(spTag)
|
||||||
|
|||||||
@@ -199,8 +199,6 @@ class GuiProjectEditMain(QWidget):
|
|||||||
def __init__(self, projGui):
|
def __init__(self, projGui):
|
||||||
super().__init__(parent=projGui)
|
super().__init__(parent=projGui)
|
||||||
|
|
||||||
self.mainGui = projGui.mainGui
|
|
||||||
|
|
||||||
# The Form
|
# The Form
|
||||||
self.mainForm = NConfigLayout()
|
self.mainForm = NConfigLayout()
|
||||||
self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
|
self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
|
||||||
@@ -244,7 +242,7 @@ class GuiProjectEditMain(QWidget):
|
|||||||
self.spellLang = QComboBox(self)
|
self.spellLang = QComboBox(self)
|
||||||
self.spellLang.setMaximumWidth(xW)
|
self.spellLang.setMaximumWidth(xW)
|
||||||
self.spellLang.addItem(self.tr("Default"), "None")
|
self.spellLang.addItem(self.tr("Default"), "None")
|
||||||
langAvail = self.mainGui.docEditor.spEnchant.listDictionaries()
|
langAvail = SHARED.spelling.listDictionaries()
|
||||||
for spTag, spProv in langAvail:
|
for spTag, spProv in langAvail:
|
||||||
qLocal = QLocale(spTag)
|
qLocal = QLocale(spTag)
|
||||||
spLang = qLocal.nativeLanguageName().title()
|
spLang = qLocal.nativeLanguageName().title()
|
||||||
|
|||||||
@@ -54,7 +54,6 @@ from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass
|
|||||||
from novelwriter.common import minmax, transferCase
|
from novelwriter.common import minmax, transferCase
|
||||||
from novelwriter.constants import nwConst, nwKeyWords, nwUnicode
|
from novelwriter.constants import nwConst, nwKeyWords, nwUnicode
|
||||||
from novelwriter.core.index import countWords
|
from novelwriter.core.index import countWords
|
||||||
from novelwriter.core.spellcheck import NWSpellEnchant
|
|
||||||
from novelwriter.gui.dochighlight import GuiDocHighlighter
|
from novelwriter.gui.dochighlight import GuiDocHighlighter
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
@@ -133,8 +132,7 @@ class GuiDocEditor(QTextEdit):
|
|||||||
self.docSearch = GuiDocEditSearch(self)
|
self.docSearch = GuiDocEditSearch(self)
|
||||||
|
|
||||||
# Syntax
|
# Syntax
|
||||||
self.spEnchant = NWSpellEnchant(SHARED.project)
|
self.highLight = GuiDocHighlighter(qDoc)
|
||||||
self.highLight = GuiDocHighlighter(qDoc, self.spEnchant)
|
|
||||||
|
|
||||||
# Context Menu
|
# Context Menu
|
||||||
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
self.setContextMenuPolicy(Qt.CustomContextMenu)
|
||||||
@@ -702,8 +700,8 @@ class GuiDocEditor(QTextEdit):
|
|||||||
else:
|
else:
|
||||||
theLang = SHARED.project.data.spellLang
|
theLang = SHARED.project.data.spellLang
|
||||||
|
|
||||||
self.spEnchant.setLanguage(theLang)
|
SHARED.spelling.setLanguage(theLang)
|
||||||
_, theProvider = self.spEnchant.describeDict()
|
_, theProvider = SHARED.spelling.describeDict()
|
||||||
|
|
||||||
self.spellDictionaryChanged.emit(str(theLang), str(theProvider))
|
self.spellDictionaryChanged.emit(str(theLang), str(theProvider))
|
||||||
if not self._bigDoc:
|
if not self._bigDoc:
|
||||||
@@ -711,37 +709,37 @@ class GuiDocEditor(QTextEdit):
|
|||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
def toggleSpellCheck(self, theMode):
|
def toggleSpellCheck(self, state: bool) -> None:
|
||||||
"""This is the main spell check setting function, and this one
|
"""This is the main spell check setting function, and this one
|
||||||
should call all other setSpellCheck functions in other classes.
|
should call all other setSpellCheck functions in other classes.
|
||||||
If the spell check mode (theMode) is not defined (None), then
|
If the spell check mode (theMode) is not defined (None), then
|
||||||
toggle the current status saved in this class.
|
toggle the current status saved in this class.
|
||||||
"""
|
"""
|
||||||
if theMode is None:
|
if state is None:
|
||||||
theMode = not self._spellCheck
|
state = not self._spellCheck
|
||||||
|
|
||||||
if not CONFIG.hasEnchant:
|
if not CONFIG.hasEnchant:
|
||||||
if theMode:
|
if state:
|
||||||
SHARED.info(self.tr(
|
SHARED.info(self.tr(
|
||||||
"Spell checking requires the package PyEnchant. "
|
"Spell checking requires the package PyEnchant. "
|
||||||
"It does not appear to be installed."
|
"It does not appear to be installed."
|
||||||
))
|
))
|
||||||
theMode = False
|
state = False
|
||||||
|
|
||||||
if self.spEnchant.spellLanguage is None:
|
if SHARED.spelling.spellLanguage is None:
|
||||||
theMode = False
|
state = False
|
||||||
|
|
||||||
self._spellCheck = theMode
|
self._spellCheck = state
|
||||||
self.mainGui.mainMenu.setSpellCheck(theMode)
|
self.mainGui.mainMenu.setSpellCheck(state)
|
||||||
SHARED.project.data.setSpellCheck(theMode)
|
SHARED.project.data.setSpellCheck(state)
|
||||||
self.highLight.setSpellCheck(theMode)
|
self.highLight.setSpellCheck(state)
|
||||||
if not self._bigDoc or theMode is False:
|
if not self._bigDoc or state is False:
|
||||||
# We don't run the spell checker automatically on big docs
|
# We don't run the spell checker automatically on big docs
|
||||||
self.spellCheckDocument()
|
self.spellCheckDocument()
|
||||||
|
|
||||||
logger.debug("Spell check is set to '%s'", str(theMode))
|
logger.debug("Spell check is set to '%s'", str(state))
|
||||||
|
|
||||||
return True
|
return
|
||||||
|
|
||||||
def spellCheckDocument(self) -> None:
|
def spellCheckDocument(self) -> None:
|
||||||
"""Rerun the highlighter to update spell checking status of the
|
"""Rerun the highlighter to update spell checking status of the
|
||||||
@@ -1193,14 +1191,14 @@ class GuiDocEditor(QTextEdit):
|
|||||||
|
|
||||||
if spellCheck:
|
if spellCheck:
|
||||||
logger.debug("Looking up '%s' in the dictionary", theWord)
|
logger.debug("Looking up '%s' in the dictionary", theWord)
|
||||||
spellCheck &= not self.spEnchant.checkWord(theWord)
|
spellCheck &= not SHARED.spelling.checkWord(theWord)
|
||||||
|
|
||||||
if spellCheck:
|
if spellCheck:
|
||||||
mnuContext.addSeparator()
|
mnuContext.addSeparator()
|
||||||
mnuHead = QAction(self.tr("Spelling Suggestion(s)"), mnuContext)
|
mnuHead = QAction(self.tr("Spelling Suggestion(s)"), mnuContext)
|
||||||
mnuContext.addAction(mnuHead)
|
mnuContext.addAction(mnuHead)
|
||||||
|
|
||||||
theSuggest = self.spEnchant.suggestWords(theWord)[:15]
|
theSuggest = SHARED.spelling.suggestWords(theWord)[:15]
|
||||||
if len(theSuggest) > 0:
|
if len(theSuggest) > 0:
|
||||||
for aWord in theSuggest:
|
for aWord in theSuggest:
|
||||||
mnuWord = QAction("%s %s" % (nwUnicode.U_ENDASH, aWord), mnuContext)
|
mnuWord = QAction("%s %s" % (nwUnicode.U_ENDASH, aWord), mnuContext)
|
||||||
@@ -1245,7 +1243,7 @@ class GuiDocEditor(QTextEdit):
|
|||||||
"""
|
"""
|
||||||
theWord = theCursor.selectedText().strip().strip(self._nonWord)
|
theWord = theCursor.selectedText().strip().strip(self._nonWord)
|
||||||
logger.debug("Added '%s' to project dictionary", theWord)
|
logger.debug("Added '%s' to project dictionary", theWord)
|
||||||
self.spEnchant.addWord(theWord)
|
SHARED.spelling.addWord(theWord)
|
||||||
self.highLight.rehighlightBlock(theCursor.block())
|
self.highLight.rehighlightBlock(theCursor.block())
|
||||||
return
|
return
|
||||||
|
|
||||||
|
|||||||
@@ -29,7 +29,7 @@ from time import time
|
|||||||
|
|
||||||
from PyQt5.QtCore import Qt, QRegularExpression
|
from PyQt5.QtCore import Qt, QRegularExpression
|
||||||
from PyQt5.QtGui import (
|
from PyQt5.QtGui import (
|
||||||
QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush
|
QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush, QTextDocument
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED
|
from novelwriter import CONFIG, SHARED
|
||||||
@@ -46,13 +46,11 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
BLOCK_META = 2
|
BLOCK_META = 2
|
||||||
BLOCK_TITLE = 4
|
BLOCK_TITLE = 4
|
||||||
|
|
||||||
def __init__(self, theDoc, spEnchant):
|
def __init__(self, document: QTextDocument) -> None:
|
||||||
super().__init__(theDoc)
|
super().__init__(document)
|
||||||
|
|
||||||
logger.debug("Create: GuiDocHighlighter")
|
logger.debug("Create: GuiDocHighlighter")
|
||||||
|
|
||||||
self.theDoc = theDoc
|
|
||||||
self.spEnchant = spEnchant
|
|
||||||
self.theHandle = None
|
self.theHandle = None
|
||||||
self.spellCheck = False
|
self.spellCheck = False
|
||||||
self.spellRx = None
|
self.spellRx = None
|
||||||
@@ -387,7 +385,7 @@ class GuiDocHighlighter(QSyntaxHighlighter):
|
|||||||
rxSpell = self.spellRx.globalMatch(theText.replace("_", " "), 0)
|
rxSpell = self.spellRx.globalMatch(theText.replace("_", " "), 0)
|
||||||
while rxSpell.hasNext():
|
while rxSpell.hasNext():
|
||||||
rxMatch = rxSpell.next()
|
rxMatch = rxSpell.next()
|
||||||
if not self.spEnchant.checkWord(rxMatch.captured(0)):
|
if not SHARED.spelling.checkWord(rxMatch.captured(0)):
|
||||||
if rxMatch.captured(0).isupper() or rxMatch.captured(0).isnumeric():
|
if rxMatch.captured(0).isupper() or rxMatch.captured(0).isnumeric():
|
||||||
continue
|
continue
|
||||||
xPos = rxMatch.capturedStart(0)
|
xPos = rxMatch.capturedStart(0)
|
||||||
|
|||||||
+16
-2
@@ -32,6 +32,8 @@ from pathlib import Path
|
|||||||
from PyQt5.QtCore import QObject, pyqtSignal
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
from PyQt5.QtWidgets import QMessageBox, QWidget
|
from PyQt5.QtWidgets import QMessageBox, QWidget
|
||||||
|
|
||||||
|
from novelwriter.core.spellcheck import NWSpellEnchant
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
from novelwriter.guimain import GuiMain
|
from novelwriter.guimain import GuiMain
|
||||||
from novelwriter.gui.theme import GuiTheme
|
from novelwriter.gui.theme import GuiTheme
|
||||||
@@ -43,18 +45,20 @@ logger = logging.getLogger(__name__)
|
|||||||
class SharedData(QObject):
|
class SharedData(QObject):
|
||||||
|
|
||||||
__slots__ = (
|
__slots__ = (
|
||||||
"_gui", "_theme", "_project", "_lockedBy", "_alert",
|
"_gui", "_theme", "_project", "_spelling", "_lockedBy", "_alert",
|
||||||
"_idleTime", "_idleRefTime",
|
"_idleTime", "_idleRefTime",
|
||||||
)
|
)
|
||||||
|
|
||||||
projectStatusChanged = pyqtSignal(bool)
|
projectStatusChanged = pyqtSignal(bool)
|
||||||
projectStatusMessage = pyqtSignal(str)
|
projectStatusMessage = pyqtSignal(str)
|
||||||
|
spellLanguageChanged = pyqtSignal(str, str)
|
||||||
|
|
||||||
def __init__(self) -> None:
|
def __init__(self) -> None:
|
||||||
super().__init__()
|
super().__init__()
|
||||||
self._gui = None
|
self._gui = None
|
||||||
self._theme = None
|
self._theme = None
|
||||||
self._project = None
|
self._project = None
|
||||||
|
self._spelling = None
|
||||||
self._lockedBy = None
|
self._lockedBy = None
|
||||||
self._alert = None
|
self._alert = None
|
||||||
self._idleTime = 0.0
|
self._idleTime = 0.0
|
||||||
@@ -82,6 +86,13 @@ class SharedData(QObject):
|
|||||||
raise Exception("SharedData class not fully initialised")
|
raise Exception("SharedData class not fully initialised")
|
||||||
return self._project
|
return self._project
|
||||||
|
|
||||||
|
@property
|
||||||
|
def spelling(self) -> NWSpellEnchant:
|
||||||
|
"""Return the active NWProject instance."""
|
||||||
|
if self._spelling is None:
|
||||||
|
raise Exception("SharedData class not fully initialised")
|
||||||
|
return self._spelling
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def hasProject(self) -> bool:
|
def hasProject(self) -> bool:
|
||||||
"""Return True if the project instance is populated."""
|
"""Return True if the project instance is populated."""
|
||||||
@@ -130,6 +141,7 @@ class SharedData(QObject):
|
|||||||
self._lockedBy = self.project.lockStatus
|
self._lockedBy = self.project.lockStatus
|
||||||
self._resetProject()
|
self._resetProject()
|
||||||
|
|
||||||
|
self.spelling.loadUserWordList()
|
||||||
self._resetIdleTimer()
|
self._resetIdleTimer()
|
||||||
|
|
||||||
return status
|
return status
|
||||||
@@ -220,11 +232,13 @@ class SharedData(QObject):
|
|||||||
##
|
##
|
||||||
|
|
||||||
def _resetProject(self) -> None:
|
def _resetProject(self) -> None:
|
||||||
"""Create a new project instance."""
|
"""Create a new project and spell checking instance."""
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
if isinstance(self._project, NWProject):
|
if isinstance(self._project, NWProject):
|
||||||
del self._project
|
del self._project
|
||||||
|
del self._spelling
|
||||||
self._project = NWProject()
|
self._project = NWProject()
|
||||||
|
self._spelling = NWSpellEnchant(self._project)
|
||||||
return
|
return
|
||||||
|
|
||||||
def _resetIdleTimer(self) -> None:
|
def _resetIdleTimer(self) -> None:
|
||||||
|
|||||||
@@ -58,17 +58,14 @@ def testCoreSpell_UserDictionary(monkeypatch, mockGUI, fncPath):
|
|||||||
assert sorted(userDict) == ["bar", "foo"]
|
assert sorted(userDict) == ["bar", "foo"]
|
||||||
|
|
||||||
# Save the file, but fail
|
# Save the file, but fail
|
||||||
assert userDict._path is None
|
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr("builtins.open", causeOSError)
|
mp.setattr("builtins.open", causeOSError)
|
||||||
userDict.save()
|
userDict.save()
|
||||||
|
|
||||||
# There should be no file, but the file path should now be cached
|
# There should be no file
|
||||||
assert userDict._path == dictFile
|
|
||||||
assert not dictFile.exists()
|
assert not dictFile.exists()
|
||||||
|
|
||||||
# Break the path check
|
# Break the path check
|
||||||
userDict._path = None
|
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
|
mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
|
||||||
userDict.save()
|
userDict.save()
|
||||||
@@ -85,23 +82,19 @@ def testCoreSpell_UserDictionary(monkeypatch, mockGUI, fncPath):
|
|||||||
assert sorted(userDict) == []
|
assert sorted(userDict) == []
|
||||||
|
|
||||||
# Load the file, but fail
|
# Load the file, but fail
|
||||||
userDict._path = None
|
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr("builtins.open", causeOSError)
|
mp.setattr("builtins.open", causeOSError)
|
||||||
userDict.load()
|
userDict.load()
|
||||||
|
|
||||||
# Path is now set, but no words
|
# No words loaded
|
||||||
assert userDict._path == dictFile
|
|
||||||
assert sorted(userDict) == []
|
assert sorted(userDict) == []
|
||||||
|
|
||||||
# Break the path check
|
# Break the path check
|
||||||
userDict._path = None
|
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
|
mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None)
|
||||||
userDict.load()
|
userDict.load()
|
||||||
|
|
||||||
# Path is now None, and no words
|
# No words loaded
|
||||||
assert userDict._path is None
|
|
||||||
assert sorted(userDict) == []
|
assert sorted(userDict) == []
|
||||||
|
|
||||||
# Load the words again, properly
|
# Load the words again, properly
|
||||||
@@ -122,18 +115,18 @@ def testCoreSpell_FakeEnchant(monkeypatch, mockGUI, fncPath):
|
|||||||
mp.setitem(sys.modules, "enchant", None)
|
mp.setitem(sys.modules, "enchant", None)
|
||||||
spChk = NWSpellEnchant(project)
|
spChk = NWSpellEnchant(project)
|
||||||
spChk.setLanguage("en_US")
|
spChk.setLanguage("en_US")
|
||||||
assert isinstance(spChk._dictObj, FakeEnchant)
|
assert isinstance(spChk._enchant, FakeEnchant)
|
||||||
|
|
||||||
# Request a non-existent dictionary
|
# Request a non-existent dictionary
|
||||||
spChk = NWSpellEnchant(project)
|
spChk = NWSpellEnchant(project)
|
||||||
spChk.setLanguage("whatchamajig")
|
spChk.setLanguage("whatchamajig")
|
||||||
assert isinstance(spChk._dictObj, FakeEnchant)
|
assert isinstance(spChk._enchant, FakeEnchant)
|
||||||
|
|
||||||
# Request an empty language string
|
# Request an empty language string
|
||||||
# See issue https://github.com/vkbo/novelWriter/issues/1096
|
# See issue https://github.com/vkbo/novelWriter/issues/1096
|
||||||
spChk = NWSpellEnchant(project)
|
spChk = NWSpellEnchant(project)
|
||||||
spChk.setLanguage("")
|
spChk.setLanguage("")
|
||||||
assert isinstance(spChk._dictObj, FakeEnchant)
|
assert isinstance(spChk._enchant, FakeEnchant)
|
||||||
|
|
||||||
# FakeEnchant should handle requests
|
# FakeEnchant should handle requests
|
||||||
fkChk = FakeEnchant()
|
fkChk = FakeEnchant()
|
||||||
@@ -164,14 +157,14 @@ def testCoreSpell_Enchant(monkeypatch, mockGUI, fncPath):
|
|||||||
assert spChk.spellLanguage is None
|
assert spChk.spellLanguage is None
|
||||||
|
|
||||||
# Check that the FakeEnchant class is actually handling this
|
# Check that the FakeEnchant class is actually handling this
|
||||||
assert isinstance(spChk._dictObj, FakeEnchant)
|
assert isinstance(spChk._enchant, FakeEnchant)
|
||||||
assert spChk.checkWord("word") is True
|
assert spChk.checkWord("word") is True
|
||||||
assert spChk.suggestWords("word") == []
|
assert spChk.suggestWords("word") == []
|
||||||
assert spChk.addWord("word") is True
|
assert spChk.addWord("word") is True
|
||||||
|
|
||||||
# Set the dict to None, and check enchant error handling
|
# Set the dict to None, and check enchant error handling
|
||||||
spChk = NWSpellEnchant(project)
|
spChk = NWSpellEnchant(project)
|
||||||
spChk._dictObj = None # type: ignore
|
spChk._enchant = None # type: ignore
|
||||||
assert spChk.checkWord("word") is True
|
assert spChk.checkWord("word") is True
|
||||||
assert spChk.suggestWords("word") == []
|
assert spChk.suggestWords("word") == []
|
||||||
assert spChk.addWord("word") is False
|
assert spChk.addWord("word") is False
|
||||||
@@ -182,7 +175,7 @@ def testCoreSpell_Enchant(monkeypatch, mockGUI, fncPath):
|
|||||||
spChk = NWSpellEnchant(project)
|
spChk = NWSpellEnchant(project)
|
||||||
spChk.setLanguage("en_US")
|
spChk.setLanguage("en_US")
|
||||||
spChk.setLanguage("en_US")
|
spChk.setLanguage("en_US")
|
||||||
assert isinstance(spChk._dictObj, enchant.Dict)
|
assert isinstance(spChk._enchant, enchant.Dict)
|
||||||
assert spChk.spellLanguage == "en_US"
|
assert spChk.spellLanguage == "en_US"
|
||||||
assert spChk.listDictionaries() != []
|
assert spChk.listDictionaries() != []
|
||||||
assert spChk.describeDict() != ("", "")
|
assert spChk.describeDict() != ("", "")
|
||||||
@@ -194,6 +187,6 @@ def testCoreSpell_Enchant(monkeypatch, mockGUI, fncPath):
|
|||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr("enchant.Broker.request_dict", lambda *a: None)
|
mp.setattr("enchant.Broker.request_dict", lambda *a: None)
|
||||||
spChk.setLanguage("en_US")
|
spChk.setLanguage("en_US")
|
||||||
assert isinstance(spChk._dictObj, FakeEnchant)
|
assert isinstance(spChk._enchant, FakeEnchant)
|
||||||
|
|
||||||
# END Test testCoreSpell_Enchant
|
# END Test testCoreSpell_Enchant
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ from PyQt5.QtWidgets import (
|
|||||||
QDialogButtonBox, QDialog, QAction, QFileDialog, QFontDialog
|
QDialogButtonBox, QDialog, QAction, QFileDialog, QFontDialog
|
||||||
)
|
)
|
||||||
|
|
||||||
from novelwriter import CONFIG
|
from novelwriter import CONFIG, SHARED
|
||||||
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
from novelwriter.dialogs.quotes import GuiQuoteSelect
|
||||||
from novelwriter.dialogs.preferences import GuiPreferences
|
from novelwriter.dialogs.preferences import GuiPreferences
|
||||||
|
|
||||||
@@ -42,7 +42,7 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths):
|
|||||||
"""Test the preferences dialog."""
|
"""Test the preferences dialog."""
|
||||||
monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None)
|
monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None)
|
||||||
monkeypatch.setattr(GuiPreferences, "result", lambda *a: QDialog.Accepted)
|
monkeypatch.setattr(GuiPreferences, "result", lambda *a: QDialog.Accepted)
|
||||||
monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")])
|
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "none")])
|
||||||
|
|
||||||
with monkeypatch.context() as mp:
|
with monkeypatch.context() as mp:
|
||||||
mp.setattr(GuiPreferences, "updateTheme", lambda *a: True)
|
mp.setattr(GuiPreferences, "updateTheme", lambda *a: True)
|
||||||
|
|||||||
@@ -84,10 +84,9 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI):
|
|||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
|
def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
|
||||||
"""Test the main tab of the project settings dialog.
|
"""Test the main tab of the project settings dialog."""
|
||||||
"""
|
|
||||||
# Mock components
|
# Mock components
|
||||||
monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")])
|
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "none")])
|
||||||
|
|
||||||
# Create new project
|
# Create new project
|
||||||
buildTestProject(nwGUI, projPath)
|
buildTestProject(nwGUI, projPath)
|
||||||
@@ -152,7 +151,7 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
|
|||||||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||||||
|
|
||||||
# Mock components
|
# Mock components
|
||||||
monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")])
|
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "none")])
|
||||||
|
|
||||||
# Create new project
|
# Create new project
|
||||||
mockRnd.reset()
|
mockRnd.reset()
|
||||||
@@ -348,12 +347,11 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
|
|||||||
|
|
||||||
@pytest.mark.gui
|
@pytest.mark.gui
|
||||||
def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
|
def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
|
||||||
"""Test the auto-replace tab of the project settings dialog.
|
"""Test the auto-replace tab of the project settings dialog."""
|
||||||
"""
|
|
||||||
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
|
||||||
|
|
||||||
# Mock components
|
# Mock components
|
||||||
monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")])
|
monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "none")])
|
||||||
|
|
||||||
# Create new project
|
# Create new project
|
||||||
mockRnd.reset()
|
mockRnd.reset()
|
||||||
|
|||||||
Reference in New Issue
Block a user