diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index e86776ac..fc59f77f 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -31,7 +31,7 @@ from typing import TYPE_CHECKING, Iterator from pathlib import Path from functools import partial -from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal +from PyQt5.QtCore import QCoreApplication from novelwriter import CONFIG, SHARED, __version__, __hexversion__ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout @@ -55,13 +55,9 @@ if TYPE_CHECKING: # pragma: no cover logger = logging.getLogger(__name__) -class NWProject(QObject): +class NWProject: - statusChanged = pyqtSignal(bool) - statusMessage = pyqtSignal(str) - - def __init__(self, parent: QObject | None = None) -> None: - super().__init__(parent=parent) + def __init__(self) -> None: # Core Elements self._options = OptionState(self) # Project-specific GUI options @@ -206,7 +202,7 @@ class NWProject(QObject): def trashFolder(self) -> str: """Add the special trash root folder to the project.""" - trashHandle = self._tree.trashRoot() + trashHandle = self._tree.trashRoot if trashHandle is None: label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH]) return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH) @@ -331,7 +327,7 @@ class NWProject(QObject): self.setProjectChanged(False) self._valid = True - self.statusMessage.emit(self.tr("Opened Project: {0}").format(self._data.name)) + SHARED.newStatusMessage(self.tr("Opened Project: {0}").format(self._data.name)) return True @@ -381,7 +377,7 @@ class NWProject(QObject): ) self._storage.writeLockFile() - self.statusMessage.emit(self.tr("Saved Project: {0}").format(self._data.name)) + SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name)) self.setProjectChanged(False) return True @@ -403,7 +399,7 @@ class NWProject(QObject): return False logger.info("Backing up project") - self.statusMessage.emit(self.tr("Backing up project ...")) + SHARED.newStatusMessage(self.tr("Backing up project ...")) if not self._data.name: SHARED.error(self.tr( @@ -434,7 +430,7 @@ class NWProject(QObject): SHARED.error(self.tr("Could not write backup archive.")) return False - self.statusMessage.emit(self.tr("Project backed up to '{0}'").format(str(archName))) + SHARED.newStatusMessage(self.tr("Project backed up to '{0}'").format(str(archName))) return True @@ -488,7 +484,7 @@ class NWProject(QObject): """ if isinstance(status, bool): self._changed = status - self.statusChanged.emit(self._changed) + SHARED.setGlobalProjectState(self._changed) return self._changed ## diff --git a/novelwriter/core/spellcheck.py b/novelwriter/core/spellcheck.py index 0ebfe654..e2a022e9 100644 --- a/novelwriter/core/spellcheck.py +++ b/novelwriter/core/spellcheck.py @@ -29,6 +29,8 @@ import logging from typing import TYPE_CHECKING, Iterator from pathlib import Path +from PyQt5.QtCore import QLocale + from novelwriter.error import logException from novelwriter.constants import nwFiles @@ -47,11 +49,15 @@ class NWSpellEnchant: def __init__(self, project: NWProject) -> None: self._project = project - self._dictObj = FakeEnchant() + self._enchant = FakeEnchant() self._userDict = UserDictionary(project) self._language = 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 ## @@ -72,7 +78,7 @@ class NWSpellEnchant: crash. Note that enchant will allow loading an empty string as a tag, but this will fail later on. See issue #1096. """ - self._dictObj = FakeEnchant() + self._enchant = FakeEnchant() self._broker = None self._language = None @@ -81,7 +87,7 @@ class NWSpellEnchant: if language and enchant.dict_exists(language): self._broker = enchant.Broker() - self._dictObj = self._broker.request_dict(language) + self._enchant = self._broker.request_dict(language) self._language = language logger.debug("Enchant spell checking for language '%s' loaded", language) else: @@ -90,12 +96,12 @@ class NWSpellEnchant: except Exception: logger.error("Failed to load enchant spell checking for language '%s'", language) - if self._dictObj is None: - self._dictObj = FakeEnchant() + if self._enchant is None: + self._enchant = FakeEnchant() else: self._userDict.load() - for pWord in self._userDict: - self._dictObj.add_to_session(pWord) + for word in self._userDict: + self._enchant.add_to_session(word) return @@ -106,14 +112,14 @@ class NWSpellEnchant: def checkWord(self, word: str) -> bool: """Wrapper function for pyenchant.""" try: - return bool(self._dictObj.check(word)) + return bool(self._enchant.check(word)) except Exception: return True def suggestWords(self, word: str) -> list[str]: """Wrapper function for pyenchant.""" try: - return self._dictObj.suggest(word) + return self._enchant.suggest(word) except Exception: return [] @@ -123,7 +129,7 @@ class NWSpellEnchant: if not word: return False try: - self._dictObj.add_to_session(word) + self._enchant.add_to_session(word) except Exception: return False @@ -134,30 +140,26 @@ class NWSpellEnchant: return added def listDictionaries(self) -> list[tuple[str, str]]: - """Wrapper function for pyenchant.""" - retList = [] + """List available dictionaries.""" + lang = [] try: import enchant - for spTag, spProvider in enchant.list_dicts(): - retList.append((spTag, spProvider.name)) + tags = [x for x, _ in enchant.list_dicts()] + lang = [(x, f"{QLocale(x).nativeLanguageName().title()} [{x}]") for x in set(tags)] except Exception: logger.error("Failed to list languages for enchant spell checking") - - return retList + return sorted(lang, key=lambda x: x[1]) def describeDict(self) -> tuple[str, str]: - """Return the tag and provider of the currently loaded - dictionary. - """ + """Describe the currently loaded dictionary.""" try: - tag = self._dictObj.tag - name = self._dictObj.provider.name # type: ignore + tag = self._enchant.tag + name = self._enchant.provider.name # type: ignore except Exception: logger.error("Failed to extract information about the dictionary") logException() tag = "" name = "" - return tag, name # END Class NWSpellEnchant @@ -192,7 +194,6 @@ class UserDictionary: def __init__(self, project: NWProject) -> None: self._project = project self._words = set() - self._path = None return def __contains__(self, word: str) -> bool: @@ -212,13 +213,14 @@ class UserDictionary: def load(self) -> None: """Load the user's dictionary.""" - self._path = self._project.storage.getMetaFile(nwFiles.DICT_FILE) 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: - 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) self._words = set(data.get("novelWriter.userDict", [])) + logger.info("Loaded: %s", nwFiles.DICT_FILE) except Exception: logger.error("Failed to load user dictionary") logException() @@ -226,17 +228,15 @@ class UserDictionary: def save(self) -> None: """Save the user's dictionary.""" - if self._path is None: - self._path = self._project.storage.getMetaFile(nwFiles.DICT_FILE) - if not isinstance(self._path, Path): - return - try: - with open(self._path, mode="w", encoding="utf-8") as fObj: - data = {"novelWriter.userDict": list(self._words)} - json.dump(data, fObj, indent=2) - except Exception: - logger.error("Failed to save user dictionary") - logException() + wordList = self._project.storage.getMetaFile(nwFiles.DICT_FILE) + if isinstance(wordList, Path): + try: + with open(wordList, mode="w", encoding="utf-8") as fObj: + data = {"novelWriter.userDict": list(self._words)} + json.dump(data, fObj, indent=2) + except Exception: + logger.error("Failed to save user dictionary") + logException() return # END Class UserDictionary diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index fef2759d..b0a79822 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -40,6 +40,8 @@ if TYPE_CHECKING: # pragma: no cover logger = logging.getLogger(__name__) +MAX_DEPTH = 1000 # Cap of tree traversing for loops (recursion limit) + class NWTree: """Core: Project Tree Data Class @@ -59,7 +61,7 @@ class NWTree: also used for file names. """ - MAX_DEPTH = 1000 # Cap of tree traversing for loops + __slots__ = ("_project", "_tree", "_order", "_roots", "_trash", "_changed") def __init__(self, project: NWProject) -> None: @@ -74,6 +76,15 @@ class NWTree: return + ## + # Properties + ## + + @property + def trashRoot(self) -> str | None: + """Return the handle of the trash folder, or None.""" + return self._trash + ## # Class Methods ## @@ -320,7 +331,7 @@ class NWTree: return False iItem = tItem - for _ in range(self.MAX_DEPTH): + for _ in range(MAX_DEPTH): if iItem.itemParent is None: tItem.setRoot(iItem.itemHandle) tItem.setClassDefaults(iItem.itemClass) @@ -349,7 +360,7 @@ class NWTree: tItem = self.__getitem__(tHandle) if tItem is not None: tTree.append(tHandle) - for _ in range(self.MAX_DEPTH): + for _ in range(MAX_DEPTH): if tItem.itemParent is None: return tTree else: @@ -400,14 +411,6 @@ class NWTree: return True return False - def trashRoot(self) -> str | None: - """Returns the handle of the trash folder, or None if there - isn't one. - """ - if self._trash: - return self._trash - return None - def findRoot(self, itemClass: nwItemClass | None) -> str | None: """Find the first root item for a given class.""" for aRoot in self._roots: diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 21bcb95d..72120448 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -26,7 +26,7 @@ from __future__ import annotations import logging from PyQt5.QtGui import QFont -from PyQt5.QtCore import Qt, QLocale +from PyQt5.QtCore import Qt from PyQt5.QtWidgets import ( QDialog, QWidget, QComboBox, QSpinBox, QPushButton, QDialogButtonBox, QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox @@ -49,8 +49,6 @@ class GuiPreferences(NPagedDialog): logger.debug("Create: GuiPreferences") self.setObjectName("GuiPreferences") - self.mainGui = mainGui - self.setWindowTitle(self.tr("Preferences")) self.tabGeneral = GuiPreferencesGeneral(self) @@ -645,8 +643,6 @@ class GuiPreferencesEditor(QWidget): def __init__(self, prefsGui): super().__init__(parent=prefsGui) - self.mainGui = prefsGui.mainGui - # The Form self.mainForm = NConfigLayout() self.mainForm.setHelpTextStyle(SHARED.theme.helpText) @@ -662,12 +658,9 @@ class GuiPreferencesEditor(QWidget): self.spellLanguage = QComboBox(self) self.spellLanguage.setMaximumWidth(mW) - langAvail = self.mainGui.docEditor.spEnchant.listDictionaries() - if CONFIG.hasEnchant and langAvail: - for spTag, spProv in langAvail: - qLocal = QLocale(spTag) - spLang = qLocal.nativeLanguageName().title() - self.spellLanguage.addItem("%s [%s]" % (spLang, spProv), spTag) + if CONFIG.hasEnchant: + for tag, language in SHARED.spelling.listDictionaries(): + self.spellLanguage.addItem(language, tag) else: self.spellLanguage.addItem(self.tr("None"), "") self.spellLanguage.setEnabled(False) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index dac33288..ac79a986 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -28,7 +28,7 @@ import logging from typing import TYPE_CHECKING from PyQt5.QtGui import QIcon, QPixmap, QColor -from PyQt5.QtCore import Qt, QLocale, pyqtSlot +from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import ( QColorDialog, QComboBox, QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget @@ -89,9 +89,6 @@ class GuiProjectSettings(NPagedDialog): self.buttonBox.rejected.connect(self._doClose) self.addControls(self.buttonBox) - # Flags - self._spellChanged = False - # Focus Tab self._focusTab(focusTab) @@ -103,10 +100,6 @@ class GuiProjectSettings(NPagedDialog): logger.debug("Delete: GuiProjectSettings") return - @property - def spellChanged(self): - return self._spellChanged - ## # Slots ## @@ -125,9 +118,7 @@ class GuiProjectSettings(NPagedDialog): project.data.setTitle(bookTitle) project.data.setAuthor(bookAuthor) project.data.setDoBackup(doBackup) - - # Remember this as updating spell dictionary can be expensive - self._spellChanged = project.data.setSpellLang(spellLang) + project.data.setSpellLang(spellLang) if self.tabStatus.colChanged: newList, delList = self.tabStatus.getNewList() @@ -199,8 +190,6 @@ class GuiProjectEditMain(QWidget): def __init__(self, projGui): super().__init__(parent=projGui) - self.mainGui = projGui.mainGui - # The Form self.mainForm = NConfigLayout() self.mainForm.setHelpTextStyle(SHARED.theme.helpText) @@ -244,11 +233,9 @@ class GuiProjectEditMain(QWidget): self.spellLang = QComboBox(self) self.spellLang.setMaximumWidth(xW) self.spellLang.addItem(self.tr("Default"), "None") - langAvail = self.mainGui.docEditor.spEnchant.listDictionaries() - for spTag, spProv in langAvail: - qLocal = QLocale(spTag) - spLang = qLocal.nativeLanguageName().title() - self.spellLang.addItem("%s [%s]" % (spLang, spProv), spTag) + if CONFIG.hasEnchant: + for tag, language in SHARED.spelling.listDictionaries(): + self.spellLang.addItem(language, tag) self.mainForm.addRow( self.tr("Spell check language"), diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index cbe7f260..d4204a36 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -54,7 +54,6 @@ from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass from novelwriter.common import minmax, transferCase from novelwriter.constants import nwConst, nwKeyWords, nwUnicode from novelwriter.core.index import countWords -from novelwriter.core.spellcheck import NWSpellEnchant from novelwriter.gui.dochighlight import GuiDocHighlighter if TYPE_CHECKING: # pragma: no cover @@ -74,7 +73,6 @@ class GuiDocEditor(QTextEdit): statusMessage = pyqtSignal(str) docCountsChanged = pyqtSignal(str, int, int, int) editedStatusChanged = pyqtSignal(bool) - spellDictionaryChanged = pyqtSignal(str, str) loadDocumentTagRequest = pyqtSignal(str, Enum) novelStructureChanged = pyqtSignal() novelItemMetaChanged = pyqtSignal(str) @@ -133,8 +131,7 @@ class GuiDocEditor(QTextEdit): self.docSearch = GuiDocEditSearch(self) # Syntax - self.spEnchant = NWSpellEnchant(SHARED.project) - self.highLight = GuiDocHighlighter(qDoc, self.spEnchant) + self.highLight = GuiDocHighlighter(qDoc) # Context Menu self.setContextMenuPolicy(Qt.CustomContextMenu) @@ -303,7 +300,7 @@ class GuiDocEditor(QTextEdit): self._typPadAfter = CONFIG.fmtPadAfter # Reload spell check and dictionaries - self.setDictionaries() + SHARED.updateSpellCheckLanguage() # Set font textFont = QFont() @@ -399,7 +396,7 @@ class GuiDocEditor(QTextEdit): self._checkDocSize(docSize) spTemp = self.highLight.spellCheck if self._bigDoc: - self.highLight.spellCheck = False + self.highLight.setSpellCheck(False) bfTime = time() self._allowAutoReplace(False) @@ -420,7 +417,7 @@ class GuiDocEditor(QTextEdit): self.docHeader.setTitleFromHandle(self._docHandle) self.docFooter.setHandle(self._docHandle) self.updateDocMargins() - self.highLight.spellCheck = spTemp + self.highLight.setSpellCheck(spTemp) if tLine is None and self._nwItem is not None: # For large documents, we queue the repositioning until the @@ -693,55 +690,37 @@ class GuiDocEditor(QTextEdit): # Spell Checking ## - def setDictionaries(self): - """Set the spell checker dictionary language, and emit the - dictionary changed signal. - """ - if SHARED.project.data.spellLang is None: - theLang = CONFIG.spellLanguage - else: - theLang = SHARED.project.data.spellLang - - self.spEnchant.setLanguage(theLang) - _, theProvider = self.spEnchant.describeDict() - - self.spellDictionaryChanged.emit(str(theLang), str(theProvider)) - if not self._bigDoc: - self.spellCheckDocument() - - return True - - def toggleSpellCheck(self, theMode): + def toggleSpellCheck(self, state: bool | None) -> None: """This is the main spell check setting function, and this one should call all other setSpellCheck functions in other classes. If the spell check mode (theMode) is not defined (None), then toggle the current status saved in this class. """ - if theMode is None: - theMode = not self._spellCheck + if state is None: + state = not self._spellCheck if not CONFIG.hasEnchant: - if theMode: + if state: SHARED.info(self.tr( "Spell checking requires the package PyEnchant. " "It does not appear to be installed." )) - theMode = False + state = False - if self.spEnchant.spellLanguage is None: - theMode = False + if SHARED.spelling.spellLanguage is None: + state = False - self._spellCheck = theMode - self.mainGui.mainMenu.setSpellCheck(theMode) - SHARED.project.data.setSpellCheck(theMode) - self.highLight.setSpellCheck(theMode) - if not self._bigDoc or theMode is False: + self._spellCheck = state + self.mainGui.mainMenu.setSpellCheck(state) + SHARED.project.data.setSpellCheck(state) + self.highLight.setSpellCheck(state) + if not self._bigDoc or state is False: # We don't run the spell checker automatically on big docs 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: """Rerun the highlighter to update spell checking status of the @@ -1193,14 +1172,14 @@ class GuiDocEditor(QTextEdit): if spellCheck: logger.debug("Looking up '%s' in the dictionary", theWord) - spellCheck &= not self.spEnchant.checkWord(theWord) + spellCheck &= not SHARED.spelling.checkWord(theWord) if spellCheck: mnuContext.addSeparator() mnuHead = QAction(self.tr("Spelling Suggestion(s)"), mnuContext) mnuContext.addAction(mnuHead) - theSuggest = self.spEnchant.suggestWords(theWord)[:15] + theSuggest = SHARED.spelling.suggestWords(theWord)[:15] if len(theSuggest) > 0: for aWord in theSuggest: mnuWord = QAction("%s %s" % (nwUnicode.U_ENDASH, aWord), mnuContext) @@ -1245,7 +1224,7 @@ class GuiDocEditor(QTextEdit): """ theWord = theCursor.selectedText().strip().strip(self._nonWord) logger.debug("Added '%s' to project dictionary", theWord) - self.spEnchant.addWord(theWord) + SHARED.spelling.addWord(theWord) self.highLight.rehighlightBlock(theCursor.block()) return diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index 9c9c1079..d265ade8 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -29,7 +29,7 @@ from time import time from PyQt5.QtCore import Qt, QRegularExpression from PyQt5.QtGui import ( - QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush + QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush, QTextDocument ) from novelwriter import CONFIG, SHARED @@ -46,31 +46,32 @@ class GuiDocHighlighter(QSyntaxHighlighter): BLOCK_META = 2 BLOCK_TITLE = 4 - def __init__(self, theDoc, spEnchant): - super().__init__(theDoc) + def __init__(self, document: QTextDocument) -> None: + super().__init__(document) logger.debug("Create: GuiDocHighlighter") - self.theDoc = theDoc - self.spEnchant = spEnchant - self.theHandle = None - self.spellCheck = False - self.spellRx = None - self.hRules = [] - self.hStyles = {} + self._tHandle = None + self._spellCheck = False + self._spellRx = QRegularExpression() - self.colHead = QColor(0, 0, 0) - self.colHeadH = QColor(0, 0, 0) - self.colEmph = QColor(0, 0, 0) - self.colDialN = QColor(0, 0, 0) - self.colDialD = QColor(0, 0, 0) - self.colDialS = QColor(0, 0, 0) - self.colHidden = QColor(0, 0, 0) - self.colKey = QColor(0, 0, 0) - self.colVal = QColor(0, 0, 0) - self.colSpell = QColor(0, 0, 0) - self.colError = QColor(0, 0, 0) - self.colRepTag = QColor(0, 0, 0) + self._hRules: list[tuple[str, dict]] = [] + self._hStyles: dict[str, QTextCharFormat] = {} + + self._colHead = QColor(0, 0, 0) + self._colHeadH = QColor(0, 0, 0) + self._colEmph = QColor(0, 0, 0) + self._colDialN = QColor(0, 0, 0) + self._colDialD = QColor(0, 0, 0) + self._colDialS = QColor(0, 0, 0) + self._colHidden = QColor(0, 0, 0) + self._colKey = QColor(0, 0, 0) + self._colVal = QColor(0, 0, 0) + self._colSpell = QColor(0, 0, 0) + self._colError = QColor(0, 0, 0) + self._colRepTag = QColor(0, 0, 0) + self._colMod = QColor(0, 0, 0) + self._colBreak = QColor(0, 0, 0) self.initHighlighter() @@ -78,71 +79,76 @@ class GuiDocHighlighter(QSyntaxHighlighter): return - def initHighlighter(self): + @property + def spellCheck(self) -> bool: + """Check if spell checking is enabled.""" + return self._spellCheck + + def initHighlighter(self) -> None: """Initialise the syntax highlighter, setting all the colour rules and building the RegExes. """ logger.debug("Setting up highlighting rules") - self.colHead = QColor(*SHARED.theme.colHead) - self.colHeadH = QColor(*SHARED.theme.colHeadH) - self.colDialN = QColor(*SHARED.theme.colDialN) - self.colDialD = QColor(*SHARED.theme.colDialD) - self.colDialS = QColor(*SHARED.theme.colDialS) - self.colHidden = QColor(*SHARED.theme.colHidden) - self.colKey = QColor(*SHARED.theme.colKey) - self.colVal = QColor(*SHARED.theme.colVal) - self.colSpell = QColor(*SHARED.theme.colSpell) - self.colError = QColor(*SHARED.theme.colError) - self.colRepTag = QColor(*SHARED.theme.colRepTag) - self.colMod = QColor(*SHARED.theme.colMod) - self.colBreak = QColor(*SHARED.theme.colEmph) - self.colBreak.setAlpha(64) + self._colHead = QColor(*SHARED.theme.colHead) + self._colHeadH = QColor(*SHARED.theme.colHeadH) + self._colDialN = QColor(*SHARED.theme.colDialN) + self._colDialD = QColor(*SHARED.theme.colDialD) + self._colDialS = QColor(*SHARED.theme.colDialS) + self._colHidden = QColor(*SHARED.theme.colHidden) + self._colKey = QColor(*SHARED.theme.colKey) + self._colVal = QColor(*SHARED.theme.colVal) + self._colSpell = QColor(*SHARED.theme.colSpell) + self._colError = QColor(*SHARED.theme.colError) + self._colRepTag = QColor(*SHARED.theme.colRepTag) + self._colMod = QColor(*SHARED.theme.colMod) + self._colBreak = QColor(*SHARED.theme.colEmph) + self._colBreak.setAlpha(64) - self.colEmph = None + self._colEmph = None if CONFIG.highlightEmph: - self.colEmph = QColor(*SHARED.theme.colEmph) + self._colEmph = QColor(*SHARED.theme.colEmph) - self.hStyles = { - "header1": self._makeFormat(self.colHead, "bold", 1.8), - "header2": self._makeFormat(self.colHead, "bold", 1.6), - "header3": self._makeFormat(self.colHead, "bold", 1.4), - "header4": self._makeFormat(self.colHead, "bold", 1.2), - "header1h": self._makeFormat(self.colHeadH, "bold", 1.8), - "header2h": self._makeFormat(self.colHeadH, "bold", 1.6), - "header3h": self._makeFormat(self.colHeadH, "bold", 1.4), - "header4h": self._makeFormat(self.colHeadH, "bold", 1.2), - "bold": self._makeFormat(self.colEmph, "bold"), - "italic": self._makeFormat(self.colEmph, "italic"), - "strike": self._makeFormat(self.colHidden, "strike"), - "mspaces": self._makeFormat(self.colError, "errline"), - "nobreak": self._makeFormat(self.colBreak, "background"), - "dialogue1": self._makeFormat(self.colDialN), - "dialogue2": self._makeFormat(self.colDialD), - "dialogue3": self._makeFormat(self.colDialS), - "replace": self._makeFormat(self.colRepTag), - "hidden": self._makeFormat(self.colHidden), - "keyword": self._makeFormat(self.colKey), - "modifier": self._makeFormat(self.colMod), - "value": self._makeFormat(self.colVal, "underline"), - "codevalue": self._makeFormat(self.colVal), + self._hStyles = { + "header1": self._makeFormat(self._colHead, "bold", 1.8), + "header2": self._makeFormat(self._colHead, "bold", 1.6), + "header3": self._makeFormat(self._colHead, "bold", 1.4), + "header4": self._makeFormat(self._colHead, "bold", 1.2), + "header1h": self._makeFormat(self._colHeadH, "bold", 1.8), + "header2h": self._makeFormat(self._colHeadH, "bold", 1.6), + "header3h": self._makeFormat(self._colHeadH, "bold", 1.4), + "header4h": self._makeFormat(self._colHeadH, "bold", 1.2), + "bold": self._makeFormat(self._colEmph, "bold"), + "italic": self._makeFormat(self._colEmph, "italic"), + "strike": self._makeFormat(self._colHidden, "strike"), + "mspaces": self._makeFormat(self._colError, "errline"), + "nobreak": self._makeFormat(self._colBreak, "background"), + "dialogue1": self._makeFormat(self._colDialN), + "dialogue2": self._makeFormat(self._colDialD), + "dialogue3": self._makeFormat(self._colDialS), + "replace": self._makeFormat(self._colRepTag), + "hidden": self._makeFormat(self._colHidden), + "keyword": self._makeFormat(self._colKey), + "modifier": self._makeFormat(self._colMod), + "value": self._makeFormat(self._colVal, "underline"), + "codevalue": self._makeFormat(self._colVal), "codeinval": self._makeFormat(None, "errline"), } - self.hRules = [] + self._hRules = [] # Multiple or Trailing Spaces if CONFIG.showMultiSpaces: - self.hRules.append(( + self._hRules.append(( r"[ ]{2,}|[ ]*$", { - 0: self.hStyles["mspaces"], + 0: self._hStyles["mspaces"], } )) # Non-Breaking Spaces - self.hRules.append(( - "[%s%s]+" % (nwUnicode.U_NBSP, nwUnicode.U_THNBSP), { - 0: self.hStyles["nobreak"], + self._hRules.append(( + f"[{nwUnicode.U_NBSP}{nwUnicode.U_THNBSP}]+", { + 0: self._hStyles["nobreak"], } )) @@ -155,68 +161,68 @@ class GuiDocHighlighter(QSyntaxHighlighter): # Straight Quotes if not (fmtDblO == fmtDblC == "\""): - self.hRules.append(( + self._hRules.append(( "(\\B\")(.*?)(\"\\B)", { - 0: self.hStyles["dialogue1"], + 0: self._hStyles["dialogue1"], } )) # Double Quotes dblEnd = "|$" if CONFIG.allowOpenDQuote else "" - self.hRules.append(( + self._hRules.append(( f"(\\B{fmtDblO})(.*?)({fmtDblC}\\B{dblEnd})", { - 0: self.hStyles["dialogue2"], + 0: self._hStyles["dialogue2"], } )) # Single Quotes sngEnd = "|$" if CONFIG.allowOpenSQuote else "" - self.hRules.append(( + self._hRules.append(( f"(\\B{fmtSngO})(.*?)({fmtSngC}\\B{sngEnd})", { - 0: self.hStyles["dialogue3"], + 0: self._hStyles["dialogue3"], } )) # Markdown Syntax - self.hRules.append(( + self._hRules.append(( nwRegEx.FMT_EI, { - 1: self.hStyles["hidden"], - 2: self.hStyles["italic"], - 3: self.hStyles["hidden"], + 1: self._hStyles["hidden"], + 2: self._hStyles["italic"], + 3: self._hStyles["hidden"], } )) - self.hRules.append(( + self._hRules.append(( nwRegEx.FMT_EB, { - 1: self.hStyles["hidden"], - 2: self.hStyles["bold"], - 3: self.hStyles["hidden"], + 1: self._hStyles["hidden"], + 2: self._hStyles["bold"], + 3: self._hStyles["hidden"], } )) - self.hRules.append(( + self._hRules.append(( nwRegEx.FMT_ST, { - 1: self.hStyles["hidden"], - 2: self.hStyles["strike"], - 3: self.hStyles["hidden"], + 1: self._hStyles["hidden"], + 2: self._hStyles["strike"], + 3: self._hStyles["hidden"], } )) # Alignment Tags - self.hRules.append(( + self._hRules.append(( r"(^>{1,2}|<{1,2}$)", { - 1: self.hStyles["hidden"], + 1: self._hStyles["hidden"], } )) # Auto-Replace Tags - self.hRules.append(( + self._hRules.append(( r"<(\S+?)>", { - 0: self.hStyles["replace"], + 0: self._hStyles["replace"], } )) # Build a QRegExp for each highlight pattern self.rxRules = [] - for regEx, regRules in self.hRules: + for regEx, regRules in self._hRules: hReg = QRegularExpression(regEx) hReg.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption) self.rxRules.append((hReg, regRules)) @@ -225,68 +231,65 @@ class GuiDocHighlighter(QSyntaxHighlighter): # Include additional characters that the highlighter should # consider to be word separators uCode = nwUnicode.U_ENDASH + nwUnicode.U_EMDASH - self.spellRx = QRegularExpression(r"\b[^\s\-\+\/" + uCode + r"]+\b") - self.spellRx.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption) + self._spellRx = QRegularExpression(r"\b[^\s\-\+\/" + uCode + r"]+\b") + self._spellRx.setPatternOptions(QRegularExpression.UseUnicodePropertiesOption) - return True + return ## # Setters ## - def setSpellCheck(self, theMode): - """Enable/disable the real time spell checker. - """ - self.spellCheck = theMode - return True + def setSpellCheck(self, state: bool) -> None: + """Enable/disable the real time spell checker.""" + self._spellCheck = state + return - def setHandle(self, theHandle): - """Set the handle of the currently highlighted document. This is - needed for the index lookup for validating tags and references. - """ - self.theHandle = theHandle - return True + def setHandle(self, tHandle: str) -> None: + """Set the handle of the currently highlighted document.""" + self._tHandle = tHandle + return ## # Methods ## - def rehighlightByType(self, theType): + def rehighlightByType(self, cType: int) -> None: """Loop through all blocks and re-highlight those of a given content type. """ qDoc = self.document() nBlocks = qDoc.blockCount() - bfTime = time() + tStart = time() for i in range(nBlocks): theBlock = qDoc.findBlockByNumber(i) - if theBlock.userState() & theType > 0: + if theBlock.userState() & cType > 0: self.rehighlightBlock(theBlock) - afTime = time() - logger.debug( - "Document highlighted in %.3f ms" % (1000*(afTime-bfTime)) - ) + logger.debug("Document highlighted in %.3f ms" % (1000*(time() - tStart))) return ## # Highlight Block ## - def highlightBlock(self, theText): + def highlightBlock(self, text: str) -> None: """Highlight a single block. Prefer to check first character for all formats that are defined by their initial characters. This is significantly faster than running the regex checks used for text paragraphs. """ self.setCurrentBlockState(self.BLOCK_NONE) - if self.theHandle is None or not theText: + if self._tHandle is None or not text: return - if theText.startswith("@"): # Keywords and commands + if text.startswith("@"): # Keywords and commands self.setCurrentBlockState(self.BLOCK_META) pIndex = SHARED.project.index - tItem = SHARED.project.tree[self.theHandle] - isValid, theBits, thePos = pIndex.scanThis(theText) + tItem = SHARED.project.tree[self._tHandle] + if tItem is None: + return + + isValid, theBits, thePos = pIndex.scanThis(text) isGood = pIndex.checkThese(theBits, tItem) if isValid: for n, theBit in enumerate(theBits): @@ -294,12 +297,12 @@ class GuiDocHighlighter(QSyntaxHighlighter): xLen = len(theBit) if isGood[n]: if n == 0: - self.setFormat(xPos, xLen, self.hStyles["keyword"]) + self.setFormat(xPos, xLen, self._hStyles["keyword"]) else: - self.setFormat(xPos, xLen, self.hStyles["value"]) + self.setFormat(xPos, xLen, self._hStyles["value"]) else: kwFmt = self.format(xPos) - kwFmt.setUnderlineColor(self.colError) + kwFmt.setUnderlineColor(self._colError) kwFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) self.setFormat(xPos, xLen, kwFmt) @@ -307,69 +310,67 @@ class GuiDocHighlighter(QSyntaxHighlighter): # so we force a return here return - elif theText.startswith(("# ", "#! ", "## ", "##! ", "### ", "#### ")): + elif text.startswith(("# ", "#! ", "## ", "##! ", "### ", "#### ")): self.setCurrentBlockState(self.BLOCK_TITLE) - if theText.startswith("# "): # Header 1 - self.setFormat(0, 1, self.hStyles["header1h"]) - self.setFormat(1, len(theText), self.hStyles["header1"]) + if text.startswith("# "): # Header 1 + self.setFormat(0, 1, self._hStyles["header1h"]) + self.setFormat(1, len(text), self._hStyles["header1"]) - elif theText.startswith("## "): # Header 2 - self.setFormat(0, 2, self.hStyles["header2h"]) - self.setFormat(2, len(theText), self.hStyles["header2"]) + elif text.startswith("## "): # Header 2 + self.setFormat(0, 2, self._hStyles["header2h"]) + self.setFormat(2, len(text), self._hStyles["header2"]) - elif theText.startswith("### "): # Header 3 - self.setFormat(0, 3, self.hStyles["header3h"]) - self.setFormat(3, len(theText), self.hStyles["header3"]) + elif text.startswith("### "): # Header 3 + self.setFormat(0, 3, self._hStyles["header3h"]) + self.setFormat(3, len(text), self._hStyles["header3"]) - elif theText.startswith("#### "): # Header 4 - self.setFormat(0, 4, self.hStyles["header4h"]) - self.setFormat(4, len(theText), self.hStyles["header4"]) + elif text.startswith("#### "): # Header 4 + self.setFormat(0, 4, self._hStyles["header4h"]) + self.setFormat(4, len(text), self._hStyles["header4"]) - if theText.startswith("#! "): # Title - self.setFormat(0, 2, self.hStyles["header1h"]) - self.setFormat(2, len(theText), self.hStyles["header1"]) + if text.startswith("#! "): # Title + self.setFormat(0, 2, self._hStyles["header1h"]) + self.setFormat(2, len(text), self._hStyles["header1"]) - elif theText.startswith("##! "): # Unnumbered - self.setFormat(0, 3, self.hStyles["header2h"]) - self.setFormat(3, len(theText), self.hStyles["header2"]) + elif text.startswith("##! "): # Unnumbered + self.setFormat(0, 3, self._hStyles["header2h"]) + self.setFormat(3, len(text), self._hStyles["header2"]) - elif theText.startswith("%"): # Comments + elif text.startswith("%"): # Comments self.setCurrentBlockState(self.BLOCK_TEXT) - toCheck = theText[1:].lstrip() + toCheck = text[1:].lstrip() synTag = toCheck[:9].lower() - tLen = len(theText) + tLen = len(text) cLen = len(toCheck) cOff = tLen - cLen if synTag == "synopsis:": - self.setFormat(0, cOff+9, self.hStyles["modifier"]) - self.setFormat(cOff+9, tLen, self.hStyles["hidden"]) + self.setFormat(0, cOff+9, self._hStyles["modifier"]) + self.setFormat(cOff+9, tLen, self._hStyles["hidden"]) else: - self.setFormat(0, tLen, self.hStyles["hidden"]) + self.setFormat(0, tLen, self._hStyles["hidden"]) else: # Text Paragraph - if theText.startswith("["): # Special Command - sText = theText.rstrip() + if text.startswith("["): # Special Command + sText = text.rstrip() if sText in ("[NEWPAGE]", "[NEW PAGE]", "[VSPACE]"): - self.setFormat(0, len(theText), self.hStyles["keyword"]) + self.setFormat(0, len(text), self._hStyles["keyword"]) return elif sText.startswith("[VSPACE:") and sText.endswith("]"): tLen = len(sText) tVal = checkInt(sText[8:-1], 0) - self.setFormat(0, 8, self.hStyles["keyword"]) - if tVal > 0: - self.setFormat(8, tLen-9, self.hStyles["codevalue"]) - else: - self.setFormat(8, tLen-9, self.hStyles["codeinval"]) - self.setFormat(tLen-1, tLen, self.hStyles["keyword"]) + cVal = "codevalue" if tVal > 0 else "codeinval" + self.setFormat(0, 8, self._hStyles["keyword"]) + self.setFormat(8, tLen-9, self._hStyles[cVal]) + self.setFormat(tLen-1, tLen, self._hStyles["keyword"]) return # Regular text self.setCurrentBlockState(self.BLOCK_TEXT) for rX, xFmt in self.rxRules: - rxItt = rX.globalMatch(theText, 0) + rxItt = rX.globalMatch(text, 0) while rxItt.hasNext(): rxMatch = rxItt.next() for xM in xFmt: @@ -377,24 +378,24 @@ class GuiDocHighlighter(QSyntaxHighlighter): xLen = rxMatch.capturedLength(xM) for x in range(xPos, xPos+xLen): spFmt = self.format(x) - if spFmt != self.hStyles["hidden"]: + if spFmt != self._hStyles["hidden"]: spFmt.merge(xFmt[xM]) self.setFormat(x, 1, spFmt) - if not self.spellCheck: + if not self._spellCheck: return - rxSpell = self.spellRx.globalMatch(theText.replace("_", " "), 0) + rxSpell = self._spellRx.globalMatch(text.replace("_", " "), 0) while rxSpell.hasNext(): 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(): continue xPos = rxMatch.capturedStart(0) xLen = rxMatch.capturedLength(0) for x in range(xPos, xPos+xLen): spFmt = self.format(x) - spFmt.setUnderlineColor(self.colSpell) + spFmt.setUnderlineColor(self._colSpell) spFmt.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) self.setFormat(x, 1, spFmt) @@ -404,33 +405,35 @@ class GuiDocHighlighter(QSyntaxHighlighter): # Internal Functions ## - def _makeFormat(self, fmtCol=None, fmtStyle=None, fmtSize=None): + def _makeFormat(self, color: QColor | None = None, style: str | None = None, + size: float | None = None) -> QTextCharFormat: """Generate a valid character format to be applied to the text that is to be highlighted. """ - theFormat = QTextCharFormat() + charFormat = QTextCharFormat() - if fmtCol is not None: - theFormat.setForeground(fmtCol) + if color is not None: + charFormat.setForeground(color) - if fmtStyle is not None: - if "bold" in fmtStyle: - theFormat.setFontWeight(QFont.Bold) - if "italic" in fmtStyle: - theFormat.setFontItalic(True) - if "strike" in fmtStyle: - theFormat.setFontStrikeOut(True) - if "errline" in fmtStyle: - theFormat.setUnderlineColor(self.colError) - theFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) - if "underline" in fmtStyle: - theFormat.setFontUnderline(True) - if "background" in fmtStyle: - theFormat.setBackground(QBrush(fmtCol, Qt.SolidPattern)) + if style is not None: + styles = style.split(",") + if "bold" in styles: + charFormat.setFontWeight(QFont.Bold) + if "italic" in styles: + charFormat.setFontItalic(True) + if "strike" in styles: + charFormat.setFontStrikeOut(True) + if "errline" in styles: + charFormat.setUnderlineColor(self._colError) + charFormat.setUnderlineStyle(QTextCharFormat.SpellCheckUnderline) + if "underline" in styles: + charFormat.setFontUnderline(True) + if "background" in styles and color is not None: + charFormat.setBackground(QBrush(color, Qt.SolidPattern)) - if fmtSize is not None: - theFormat.setFontPointSize(int(round(fmtSize*CONFIG.textSize))) + if size is not None: + charFormat.setFontPointSize(int(round(size*CONFIG.textSize))) - return theFormat + return charFormat -# END Class DocHighlighter +# END Class GuiDocHighlighter diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index b12f655f..55813a71 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -25,11 +25,12 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING from pathlib import Path from urllib.parse import urljoin from urllib.request import pathname2url -from PyQt5.QtCore import QUrl +from PyQt5.QtCore import QUrl, pyqtSlot from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction @@ -37,6 +38,9 @@ from novelwriter import CONFIG, SHARED from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) @@ -46,7 +50,7 @@ class GuiMainMenu(QMenuBar): add them from this class. """ - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiMainMenu") @@ -77,46 +81,52 @@ class GuiMainMenu(QMenuBar): # Update Menu on Settings Changed ## - def setSpellCheck(self, theMode): - """Forward spell check check state to its action. - """ - self.aSpellCheck.setChecked(theMode) + def setSpellCheck(self, state: bool) -> None: + """Forward spell check check state to its action.""" + self.aSpellCheck.setChecked(state) return ## - # Slots + # Private Slots ## - def _toggleSpellCheck(self): + @pyqtSlot() + def _toggleSpellCheck(self) -> None: """Toggle spell checking. The active status of the spell check flag is handled by the document editor class, so we make no decision, just pass a None to the function and let it decide. """ self.mainGui.docEditor.toggleSpellCheck(None) - return True + return - def _openWebsite(self, theUrl): - """Open a URL in the system's default browser. - """ - QDesktopServices.openUrl(QUrl(theUrl)) - return True + @pyqtSlot(str) + def _openWebsite(self, url: str) -> None: + """Open a URL in the system's default browser.""" + QDesktopServices.openUrl(QUrl(url)) + return - def _openUserManualFile(self): - """Open the documentation in PDF format. - """ + @pyqtSlot() + def _openUserManualFile(self) -> None: + """Open the documentation in PDF format.""" if isinstance(CONFIG.pdfDocs, Path): QDesktopServices.openUrl( QUrl(urljoin("file:", pathname2url(str(CONFIG.pdfDocs)))) ) return + @pyqtSlot(str) + def _changeSpelling(self, language: str) -> None: + """Change the spell check language.""" + SHARED.project.data.setSpellLang(language) + SHARED.updateSpellCheckLanguage() + return + ## - # Menu Builders + # Internal Functions ## - def _buildProjectMenu(self): - """Assemble the Project menu. - """ + def _buildProjectMenu(self) -> None: + """Assemble the Project menu.""" # Project self.projMenu = self.addMenu(self.tr("&Project")) @@ -190,9 +200,8 @@ class GuiMainMenu(QMenuBar): return - def _buildDocumentMenu(self): - """Assemble the Document menu. - """ + def _buildDocumentMenu(self) -> None: + """Assemble the Document menu.""" # Document self.docuMenu = self.addMenu(self.tr("&Document")) @@ -245,9 +254,8 @@ class GuiMainMenu(QMenuBar): return - def _buildEditMenu(self): - """Assemble the Edit menu. - """ + def _buildEditMenu(self) -> None: + """Assemble the Edit menu.""" # Edit self.editMenu = self.addMenu(self.tr("&Edit")) @@ -301,9 +309,8 @@ class GuiMainMenu(QMenuBar): return - def _buildViewMenu(self): - """Assemble the View menu. - """ + def _buildViewMenu(self) -> None: + """Assemble the View menu.""" # View self.viewMenu = self.addMenu(self.tr("&View")) @@ -375,9 +382,8 @@ class GuiMainMenu(QMenuBar): return - def _buildInsertMenu(self): - """Assemble the Insert menu. - """ + def _buildInsertMenu(self) -> None: + """Assemble the Insert menu.""" # Insert self.insMenu = self.addMenu(self.tr("&Insert")) @@ -589,9 +595,8 @@ class GuiMainMenu(QMenuBar): return - def _buildFormatMenu(self): - """Assemble the Format menu. - """ + def _buildFormatMenu(self) -> None: + """Assemble the Format menu.""" # Format self.fmtMenu = self.addMenu(self.tr("&Format")) @@ -739,9 +744,8 @@ class GuiMainMenu(QMenuBar): return - def _buildSearchMenu(self): - """Assemble the Search menu. - """ + def _buildSearchMenu(self) -> None: + """Assemble the Search menu.""" # Search self.srcMenu = self.addMenu(self.tr("&Search")) @@ -753,28 +757,21 @@ class GuiMainMenu(QMenuBar): # Search > Replace self.aReplace = QAction(self.tr("Replace"), self) - if CONFIG.osDarwin: - self.aReplace.setShortcut("Ctrl+=") - else: - self.aReplace.setShortcut("Ctrl+H") + self.aReplace.setShortcut("Ctrl+=" if CONFIG.osDarwin else "Ctrl+H") self.aReplace.triggered.connect(lambda: self.mainGui.docEditor.beginReplace()) self.srcMenu.addAction(self.aReplace) # Search > Find Next self.aFindNext = QAction(self.tr("Find Next"), self) - if CONFIG.osDarwin: - self.aFindNext.setShortcuts(["Ctrl+G", "F3"]) - else: - self.aFindNext.setShortcuts(["F3", "Ctrl+G"]) + self.aFindNext.setShortcuts(["Ctrl+G", "F3"] if CONFIG.osDarwin else ["F3", "Ctrl+G"]) self.aFindNext.triggered.connect(lambda: self.mainGui.docEditor.findNext()) self.srcMenu.addAction(self.aFindNext) # Search > Find Prev self.aFindPrev = QAction(self.tr("Find Previous"), self) - if CONFIG.osDarwin: - self.aFindPrev.setShortcuts(["Ctrl+Shift+G", "Shift+F3"]) - else: - self.aFindPrev.setShortcuts(["Shift+F3", "Ctrl+Shift+G"]) + self.aFindPrev.setShortcuts( + ["Ctrl+Shift+G", "Shift+F3"] if CONFIG.osDarwin else ["Shift+F3", "Ctrl+Shift+G"] + ) self.aFindPrev.triggered.connect(lambda: self.mainGui.docEditor.findNext(goBack=True)) self.srcMenu.addAction(self.aFindPrev) @@ -786,9 +783,8 @@ class GuiMainMenu(QMenuBar): return - def _buildToolsMenu(self): - """Assemble the Tools menu. - """ + def _buildToolsMenu(self) -> None: + """Assemble the Tools menu.""" # Tools self.toolsMenu = self.addMenu(self.tr("&Tools")) @@ -800,6 +796,15 @@ class GuiMainMenu(QMenuBar): self.aSpellCheck.setShortcut("Ctrl+F7") self.toolsMenu.addAction(self.aSpellCheck) + self.mSelectLanguage = self.toolsMenu.addMenu(self.tr("Spell Check Language")) + languages = SHARED.spelling.listDictionaries() + languages.insert(0, ("None", self.tr("Default"))) + for n, (tag, language) in enumerate(languages): + aSpell = QAction(self.mSelectLanguage) + aSpell.setText(language) + aSpell.triggered.connect(lambda n, tag=tag: self._changeSpelling(tag)) + self.mSelectLanguage.addAction(aSpell) + # Tools > Re-Run Spell Check self.aReRunSpell = QAction(self.tr("Re-Run Spell Check"), self) self.aReRunSpell.setShortcut("F7") @@ -849,9 +854,8 @@ class GuiMainMenu(QMenuBar): return - def _buildHelpMenu(self): - """Assemble the Help menu. - """ + def _buildHelpMenu(self) -> None: + """Assemble the Help menu.""" # Help self.helpMenu = self.addMenu(self.tr("&Help")) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 1905bdec..e753a828 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -798,7 +798,7 @@ class GuiProjectTree(QTreeWidget): logger.error("There is no item to delete") return False - trashHandle = SHARED.project.tree.trashRoot() + trashHandle = SHARED.project.tree.trashRoot if tHandle == trashHandle: logger.error("Cannot delete the Trash folder") return False @@ -823,7 +823,7 @@ class GuiProjectTree(QTreeWidget): logger.error("No project open") return False - trashHandle = SHARED.project.tree.trashRoot() + trashHandle = SHARED.project.tree.trashRoot logger.debug("Emptying Trash folder") if trashHandle is None: @@ -1201,7 +1201,7 @@ class GuiProjectTree(QTreeWidget): # Trash Folder # ============ - trashHandle = SHARED.project.tree.trashRoot() + trashHandle = SHARED.project.tree.trashRoot if tItem.itemHandle == trashHandle and trashHandle is not None: # The trash folder only has one option aEmptyTrash = ctxMenu.addAction(self.tr("Empty Trash")) diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 597124de..cb5ca85a 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -3,8 +3,7 @@ novelWriter – GUI Main Window Status Bar ======================================== File History: -Created: 2019-04-20 [0.0.1] GuiMainStatus -Created: 2020-05-17 [0.5.1] StatusLED +Created: 2019-04-20 [0.0.1] This file is a part of novelWriter Copyright 2018–2023, Veronica Berglyd Olsen @@ -121,7 +120,7 @@ class GuiMainStatus(QStatusBar): def clearStatus(self) -> None: """Reset all widgets on the status bar to default values.""" self.setRefTime(-1.0) - self.setLanguage(None, "") + self.setLanguage(*SHARED.spelling.describeDict()) self.setProjectStats(0, 0) self.setProjectStatus(StatusLED.S_NONE) self.setDocumentStatus(StatusLED.S_NONE) @@ -208,13 +207,8 @@ class GuiMainStatus(QStatusBar): self.langText.setText(self.tr("None")) self.langText.setToolTip("") else: - qLocal = QLocale(language) - spLang = qLocal.nativeLanguageName().title() - self.langText.setText(spLang) - if provider: - self.langText.setToolTip("%s (%s)" % (language, provider)) - else: - self.langText.setToolTip(language) + self.langText.setText(QLocale(language).nativeLanguageName().title()) + self.langText.setToolTip(f"{language} ({provider})" if provider else language) return @pyqtSlot(bool) diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index d606b8d3..96806648 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -234,6 +234,7 @@ class GuiMain(QMainWindow): SHARED.projectStatusChanged.connect(self.mainStatus.updateProjectStatus) SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage) + SHARED.spellLanguageChanged.connect(self.mainStatus.setLanguage) self.viewsBar.viewChangeRequested.connect(self._changeView) @@ -251,7 +252,6 @@ class GuiMain(QMainWindow): self.novelView.selectedItemChanged.connect(self.itemDetails.updateViewBox) self.novelView.openDocumentRequest.connect(self._openDocument) - self.docEditor.spellDictionaryChanged.connect(self.mainStatus.setLanguage) self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus) self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts) self.docEditor.docCountsChanged.connect(self.projView.updateCounts) @@ -428,7 +428,6 @@ class GuiMain(QMainWindow): SHARED.closeProject() - self.docEditor.setDictionaries() self._updateWindowTitle() self._changeView(nwView.PROJECT) @@ -489,7 +488,6 @@ class GuiMain(QMainWindow): # Update GUI self._updateWindowTitle(SHARED.project.data.name) self.rebuildTrees() - self.docEditor.setDictionaries() self.docEditor.toggleSpellCheck(SHARED.project.data.spellCheck) self.mainStatus.setRefTime(SHARED.project.projOpened) self.projView.openProjectTasks() @@ -590,8 +588,8 @@ class GuiMain(QMainWindow): return True def openNextDocument(self, tHandle: str, wrapAround: bool = False) -> bool: - """Opens the next document in the project tree, following the - document with the given handle. Stops when reaching the end. + """Open the next document in the project tree, following the + document with the given handle. Stop when reaching the end. """ if not SHARED.hasProject: logger.error("No project open") @@ -907,8 +905,7 @@ class GuiMain(QMainWindow): if dlgProj.result() == QDialog.Accepted: logger.debug("Applying new project settings") - if dlgProj.spellChanged: - self.docEditor.setDictionaries() + SHARED.updateSpellCheckLanguage() self.itemDetails.refreshDetails() self._updateWindowTitle(SHARED.project.data.name) @@ -982,7 +979,7 @@ class GuiMain(QMainWindow): if dlgWords.result() == QDialog.Accepted: logger.debug("Reloading word list") - self.docEditor.setDictionaries() + SHARED.updateSpellCheckLanguage(reload=True) return True diff --git a/novelwriter/shared.py b/novelwriter/shared.py index eea5fe26..9fc91c5e 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -29,9 +29,11 @@ from time import time from typing import TYPE_CHECKING from pathlib import Path -from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot +from PyQt5.QtCore import QObject, pyqtSignal from PyQt5.QtWidgets import QMessageBox, QWidget +from novelwriter.core.spellcheck import NWSpellEnchant + if TYPE_CHECKING: # pragma: no cover from novelwriter.guimain import GuiMain from novelwriter.gui.theme import GuiTheme @@ -43,24 +45,30 @@ logger = logging.getLogger(__name__) class SharedData(QObject): __slots__ = ( - "_gui", "_theme", "_project", "_lockedBy", "_alert", + "_gui", "_theme", "_project", "_spelling", "_lockedBy", "_alert", "_idleTime", "_idleRefTime", ) projectStatusChanged = pyqtSignal(bool) projectStatusMessage = pyqtSignal(str) + spellLanguageChanged = pyqtSignal(str, str) def __init__(self) -> None: super().__init__() self._gui = None self._theme = None self._project = None + self._spelling = None self._lockedBy = None self._alert = None self._idleTime = 0.0 self._idleRefTime = time() return + ## + # Properties + ## + @property def mainGui(self) -> GuiMain: """Return the Main GUI instance.""" @@ -82,9 +90,16 @@ class SharedData(QObject): raise Exception("SharedData class not fully initialised") 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 def hasProject(self) -> bool: - """Return True of the project instance is populated.""" + """Return True if the project instance is populated.""" return self.project.isValid @property @@ -107,9 +122,9 @@ class SharedData(QObject): ## def initSharedData(self, gui: GuiMain, theme: GuiTheme) -> None: - """Initialise the UserData instance. This must be called as soon - as the Main GUI is created to ensure the SHARED singleton has the - properties needed for operation. + """Initialise the SharedData instance. This must be called as + soon as the Main GUI is created to ensure the SHARED singleton + has the properties needed for operation. """ self._gui = gui self._theme = theme @@ -130,6 +145,7 @@ class SharedData(QObject): self._lockedBy = self.project.lockStatus self._resetProject() + self.updateSpellCheckLanguage(reload=True) self._resetIdleTimer() return status @@ -148,6 +164,16 @@ class SharedData(QObject): self._resetIdleTimer() return + def updateSpellCheckLanguage(self, reload: bool = False) -> None: + """Update the active spell check langauge from settings.""" + from novelwriter import CONFIG + language = self.project.data.spellLang or CONFIG.spellLanguage + if language != self.spelling.spellLanguage or reload: + self.spelling.setLanguage(language) + _, provider = self.spelling.describeDict() + self.spellLanguageChanged.emit(language, provider) + return + def updateIdleTime(self, currTime: float, userIdle: bool) -> None: """Update the idle time record. If the userIdle flag is True, the user idle counter is updated with the time difference since @@ -159,6 +185,20 @@ class SharedData(QObject): self._idleRefTime = currTime return + def newStatusMessage(self, message: str) -> None: + """Request a new status message. This is a callable function for + core classes that cannot emit signals on their own. + """ + self.projectStatusMessage.emit(message) + return + + def setGlobalProjectState(self, state: bool) -> None: + """Change the global project status. This is a callable function + for core classes that cannot emit signals on their own. + """ + self.projectStatusChanged.emit(state) + return + ## # Alert Boxes ## @@ -201,36 +241,19 @@ class SharedData(QObject): self._alert.exec_() return self._alert.result() == QMessageBox.Yes - ## - # Internal Slots - ## - - @pyqtSlot(bool) - def _emitProjectStatusChange(self, state: bool) -> None: - """Forward the project status slot.""" - self.projectStatusChanged.emit(state) - return - - @pyqtSlot(str) - def _emitProjectStatusMeesage(self, message: str) -> None: - """Forward the project message slot.""" - self.projectStatusMessage.emit(message) - return - ## # Internal Functions ## def _resetProject(self) -> None: - """Create a new project instance.""" + """Create a new project and spell checking instance.""" from novelwriter.core.project import NWProject if isinstance(self._project, NWProject): - self._project.statusChanged.disconnect() - self._project.statusMessage.disconnect() - self._project.deleteLater() - self._project = NWProject(self) - self._project.statusChanged.connect(self._emitProjectStatusChange) - self._project.statusMessage.connect(self._emitProjectStatusMeesage) + del self._project + del self._spelling + self._project = NWProject() + self._spelling = NWSpellEnchant(self._project) + self.updateSpellCheckLanguage() return def _resetIdleTimer(self) -> None: diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py index 80f80954..bbf706e0 100644 --- a/tests/test_base/test_base_shared.py +++ b/tests/test_base/test_base_shared.py @@ -42,6 +42,8 @@ def testBaseSharedData_Init(): shared.theme with pytest.raises(Exception): shared.project + with pytest.raises(Exception): + shared.spelling # Create some mock objects mockGui = MockGuiMain() @@ -113,7 +115,7 @@ def testBaseSharedData_Projects(fncPath, caplog: pytest.LogCaptureFixture): project.openProject(fncPath) # First open with our independent project instance assert shared.hasProject is False assert shared.projectLock is None - assert shared.openProject(fncPath) is False # Then with out shared instance + assert shared.openProject(fncPath) is False # Then with our shared instance assert shared.hasProject is False assert isinstance(shared.projectLock, list) diff --git a/tests/test_core/test_core_spellcheck.py b/tests/test_core/test_core_spellcheck.py index 6b161bca..d20269b5 100644 --- a/tests/test_core/test_core_spellcheck.py +++ b/tests/test_core/test_core_spellcheck.py @@ -58,17 +58,14 @@ def testCoreSpell_UserDictionary(monkeypatch, mockGUI, fncPath): assert sorted(userDict) == ["bar", "foo"] # Save the file, but fail - assert userDict._path is None with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) userDict.save() - # There should be no file, but the file path should now be cached - assert userDict._path == dictFile + # There should be no file assert not dictFile.exists() # Break the path check - userDict._path = None with monkeypatch.context() as mp: mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None) userDict.save() @@ -85,23 +82,19 @@ def testCoreSpell_UserDictionary(monkeypatch, mockGUI, fncPath): assert sorted(userDict) == [] # Load the file, but fail - userDict._path = None with monkeypatch.context() as mp: mp.setattr("builtins.open", causeOSError) userDict.load() - # Path is now set, but no words - assert userDict._path == dictFile + # No words loaded assert sorted(userDict) == [] # Break the path check - userDict._path = None with monkeypatch.context() as mp: mp.setattr("novelwriter.core.storage.NWStorage.getMetaFile", lambda *a: None) userDict.load() - # Path is now None, and no words - assert userDict._path is None + # No words loaded assert sorted(userDict) == [] # Load the words again, properly @@ -122,18 +115,18 @@ def testCoreSpell_FakeEnchant(monkeypatch, mockGUI, fncPath): mp.setitem(sys.modules, "enchant", None) spChk = NWSpellEnchant(project) spChk.setLanguage("en_US") - assert isinstance(spChk._dictObj, FakeEnchant) + assert isinstance(spChk._enchant, FakeEnchant) # Request a non-existent dictionary spChk = NWSpellEnchant(project) spChk.setLanguage("whatchamajig") - assert isinstance(spChk._dictObj, FakeEnchant) + assert isinstance(spChk._enchant, FakeEnchant) # Request an empty language string # See issue https://github.com/vkbo/novelWriter/issues/1096 spChk = NWSpellEnchant(project) spChk.setLanguage("") - assert isinstance(spChk._dictObj, FakeEnchant) + assert isinstance(spChk._enchant, FakeEnchant) # FakeEnchant should handle requests fkChk = FakeEnchant() @@ -164,14 +157,14 @@ def testCoreSpell_Enchant(monkeypatch, mockGUI, fncPath): assert spChk.spellLanguage is None # 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.suggestWords("word") == [] assert spChk.addWord("word") is True # Set the dict to None, and check enchant error handling spChk = NWSpellEnchant(project) - spChk._dictObj = None # type: ignore + spChk._enchant = None # type: ignore assert spChk.checkWord("word") is True assert spChk.suggestWords("word") == [] assert spChk.addWord("word") is False @@ -182,7 +175,7 @@ def testCoreSpell_Enchant(monkeypatch, mockGUI, fncPath): spChk = NWSpellEnchant(project) 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.listDictionaries() != [] assert spChk.describeDict() != ("", "") @@ -194,6 +187,6 @@ def testCoreSpell_Enchant(monkeypatch, mockGUI, fncPath): with monkeypatch.context() as mp: mp.setattr("enchant.Broker.request_dict", lambda *a: None) spChk.setLanguage("en_US") - assert isinstance(spChk._dictObj, FakeEnchant) + assert isinstance(spChk._enchant, FakeEnchant) # END Test testCoreSpell_Enchant diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 83abd8cb..0b36897b 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -119,7 +119,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): assert bool(theTree) is False # Check for archive and trash folders - assert theTree.trashRoot() is None + assert theTree.trashRoot is None aHandles = [] for nwItem in mockItems: @@ -146,7 +146,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): # ============ # Check that we have the correct archive and trash folders - assert theTree.trashRoot() == "a000000000003" + assert theTree.trashRoot == "a000000000003" assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002" assert theTree.isTrash("a000000000003") is True @@ -261,7 +261,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): del theTree["a000000000003"] assert len(theTree) == len(mockItems) - 3 assert "a000000000003" not in theTree - assert theTree.trashRoot() is None + assert theTree.trashRoot is None # END Test testCoreTree_BuildTree @@ -365,7 +365,7 @@ def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fnc @pytest.mark.core -def testCoreTree_Methods(mockGUI, mockItems): +def testCoreTree_Methods(monkeypatch, mockGUI, mockItems): """Test various class methods.""" theProject = NWProject() theTree = NWTree(theProject) @@ -389,11 +389,10 @@ def testCoreTree_Methods(mockGUI, mockItems): assert theTree.updateItemData("b000000000001") is True # Update item data, root is unreachable - maxDepth = theTree.MAX_DEPTH - theTree.MAX_DEPTH = 0 # type: ignore - with pytest.raises(RecursionError): - theTree.updateItemData("b000000000001") - theTree.MAX_DEPTH = maxDepth + with monkeypatch.context() as mp: + mp.setattr("novelwriter.core.tree.MAX_DEPTH", 0) + with pytest.raises(RecursionError): + theTree.updateItemData("b000000000001") # Check type assert theTree.checkType("blabla", nwItemType.FILE) is False @@ -424,11 +423,10 @@ def testCoreTree_Methods(mockGUI, mockItems): ] # Cause recursion error - maxDepth = theTree.MAX_DEPTH - theTree.MAX_DEPTH = 0 # type: ignore - with pytest.raises(RecursionError): - theTree.getItemPath("c000000000001") - theTree.MAX_DEPTH = maxDepth + with monkeypatch.context() as mp: + mp.setattr("novelwriter.core.tree.MAX_DEPTH", 0) + with pytest.raises(RecursionError): + theTree.getItemPath("c000000000001") # Break the folder parent handle theTree["b000000000001"]._parent = "stuff" # type: ignore diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index da1eb274..14712795 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -30,7 +30,7 @@ from PyQt5.QtWidgets import ( QDialogButtonBox, QDialog, QAction, QFileDialog, QFontDialog ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.dialogs.preferences import GuiPreferences @@ -42,7 +42,7 @@ def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths): """Test the preferences dialog.""" monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None) monkeypatch.setattr(GuiPreferences, "result", lambda *a: QDialog.Accepted) - monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")]) + monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) with monkeypatch.context() as mp: mp.setattr(GuiPreferences, "updateTheme", lambda *a: True) diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index bafdadc7..f56f01bf 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -43,7 +43,6 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): # Block the GUI blocking thread monkeypatch.setattr(GuiProjectSettings, "exec_", lambda *a: None) monkeypatch.setattr(GuiProjectSettings, "result", lambda *a: QDialog.Accepted) - monkeypatch.setattr(GuiProjectSettings, "spellChanged", lambda *a: True) # Check that we cannot open when there is no project nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) @@ -84,10 +83,8 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): @pytest.mark.gui def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): - """Test the main tab of the project settings dialog. - """ - # Mock components - monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")]) + """Test the main tab of the project settings dialog.""" + monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) # Create new project buildTestProject(nwGUI, projPath) @@ -130,7 +127,6 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockR assert tabMain.editName.text() == "Project Name" assert tabMain.editTitle.text() == "Project Title" assert tabMain.editAuthor.text() == "Jane Doe" - assert projSettings.spellChanged is False projSettings._doSave() assert theProject.data.name == "Project Name" @@ -150,9 +146,7 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat dialog. """ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - - # Mock components - monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")]) + monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) # Create new project mockRnd.reset() @@ -348,12 +342,9 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat @pytest.mark.gui 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)) - - # Mock components - monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")]) + monkeypatch.setattr(SHARED._spelling, "listDictionaries", lambda: [("en", "English [en]")]) # Create new project mockRnd.reset() diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index fd906971..e3025931 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -205,7 +205,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): assert len(SHARED.project.tree) == 0 assert len(SHARED.project.tree._order) == 0 assert len(SHARED.project.tree._roots) == 0 - assert SHARED.project.tree.trashRoot() is None + assert SHARED.project.tree.trashRoot is None assert SHARED.project.data.name == "" assert SHARED.project.data.title == "" assert SHARED.project.data.author == "" @@ -225,7 +225,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): assert len(SHARED.project.tree) == 8 assert len(SHARED.project.tree._order) == 8 assert len(SHARED.project.tree._roots) == 4 - assert SHARED.project.tree.trashRoot() is None + assert SHARED.project.tree.trashRoot is None assert SHARED.project.data.name == "New Project" assert SHARED.project.data.title == "New Novel" assert SHARED.project.data.author == "Jane Doe" @@ -242,7 +242,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): assert nwGUI.projView.projTree._getTreeItem(C.hSceneDoc) is not None nwGUI.mainMenu.aSpellCheck.setChecked(True) - assert nwGUI.mainMenu._toggleSpellCheck() + nwGUI.mainMenu._toggleSpellCheck() # Change some settings CONFIG.hideHScroll = True diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index bfb57a39..71b35df8 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -348,7 +348,7 @@ def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPat C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010" ] - trashHandle = SHARED.project.tree.trashRoot() + trashHandle = SHARED.project.tree.trashRoot assert projTree.getTreeFromHandle(trashHandle) == [ trashHandle, "0000000000012", "0000000000011" ] @@ -541,7 +541,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): projTree.setExpandedFromHandle(None, True) projTree._addTrashRoot() - hTrashRoot = SHARED.project.tree.trashRoot() + hTrashRoot = SHARED.project.tree.trashRoot projTree.setSelectedHandle(C.hCharRoot) projTree.newTreeItem(nwItemType.FILE)