Skip to content

Commit

Permalink
Refactor handling of spell checking (#1508)
Browse files Browse the repository at this point in the history
  • Loading branch information
vkbo authored Aug 25, 2023
2 parents f339ec2 + cf2d308 commit 1b98bdf
Show file tree
Hide file tree
Showing 19 changed files with 446 additions and 483 deletions.
22 changes: 9 additions & 13 deletions novelwriter/core/project.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
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
Expand All @@ -55,13 +55,9 @@
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
Expand Down Expand Up @@ -206,7 +202,7 @@ def removeItem(self, tHandle: str) -> bool:

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)
Expand Down Expand Up @@ -331,7 +327,7 @@ def openProject(self, projPath: str | Path, clearLock: bool = False) -> bool:
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

Expand Down Expand Up @@ -381,7 +377,7 @@ def saveProject(self, autoSave: bool = False) -> bool:
)

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
Expand All @@ -403,7 +399,7 @@ def backupProject(self, doNotify: bool) -> bool:
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(
Expand Down Expand Up @@ -434,7 +430,7 @@ def backupProject(self, doNotify: bool) -> bool:
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

Expand Down Expand Up @@ -488,7 +484,7 @@ def setProjectChanged(self, status: bool) -> bool:
"""
if isinstance(status, bool):
self._changed = status
self.statusChanged.emit(self._changed)
SHARED.setGlobalProjectState(self._changed)
return self._changed

##
Expand Down
76 changes: 38 additions & 38 deletions novelwriter/core/spellcheck.py
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@
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

Expand All @@ -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

##
Expand All @@ -72,7 +78,7 @@ def setLanguage(self, language: str | None):
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

Expand All @@ -81,7 +87,7 @@ def setLanguage(self, language: str | None):

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:
Expand All @@ -90,12 +96,12 @@ def setLanguage(self, language: str | None):
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

Expand All @@ -106,14 +112,14 @@ def setLanguage(self, language: str | None):
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 []

Expand All @@ -123,7 +129,7 @@ def addWord(self, word: str) -> bool:
if not word:
return False
try:
self._dictObj.add_to_session(word)
self._enchant.add_to_session(word)
except Exception:
return False

Expand All @@ -134,30 +140,26 @@ def addWord(self, word: str) -> bool:
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
Expand Down Expand Up @@ -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:
Expand All @@ -212,31 +213,30 @@ def add(self, word: str) -> bool:

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()
return

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
25 changes: 14 additions & 11 deletions novelwriter/core/tree.py
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,8 @@

logger = logging.getLogger(__name__)

MAX_DEPTH = 1000 # Cap of tree traversing for loops (recursion limit)


class NWTree:
"""Core: Project Tree Data Class
Expand All @@ -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:

Expand All @@ -74,6 +76,15 @@ def __init__(self, project: NWProject) -> None:

return

##
# Properties
##

@property
def trashRoot(self) -> str | None:
"""Return the handle of the trash folder, or None."""
return self._trash

##
# Class Methods
##
Expand Down Expand Up @@ -320,7 +331,7 @@ def updateItemData(self, tHandle: str) -> bool:
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)
Expand Down Expand Up @@ -349,7 +360,7 @@ def getItemPath(self, tHandle: str) -> list[str]:
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:
Expand Down Expand Up @@ -400,14 +411,6 @@ def isTrash(self, tHandle: str) -> bool:
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:
Expand Down
15 changes: 4 additions & 11 deletions novelwriter/dialogs/preferences.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
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
Expand All @@ -49,8 +49,6 @@ def __init__(self, mainGui):
logger.debug("Create: GuiPreferences")
self.setObjectName("GuiPreferences")

self.mainGui = mainGui

self.setWindowTitle(self.tr("Preferences"))

self.tabGeneral = GuiPreferencesGeneral(self)
Expand Down Expand Up @@ -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)
Expand All @@ -662,12 +658,9 @@ def __init__(self, prefsGui):
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)
Expand Down
Loading

0 comments on commit 1b98bdf

Please sign in to comment.