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