diff --git a/novelwriter/__init__.py b/novelwriter/__init__.py index 242c7275..86706068 100644 --- a/novelwriter/__init__.py +++ b/novelwriter/__init__.py @@ -31,6 +31,7 @@ from PyQt5.QtWidgets import QApplication, QErrorMessage from novelwriter.error import exceptionHandler, logException from novelwriter.config import Config +from novelwriter.shared import SharedData ## # Version Scheme @@ -74,6 +75,7 @@ logger = logging.getLogger(__name__) # Global config singleton CONFIG = Config() +SHARED = SharedData() def main(sysArgs: list | None = None): diff --git a/novelwriter/common.py b/novelwriter/common.py index 8ccf0bf0..08fd407b 100644 --- a/novelwriter/common.py +++ b/novelwriter/common.py @@ -512,8 +512,7 @@ def sha256sum(path: str | Path) -> str | None: # =============================================================================================== # def getGuiItem(objName: str) -> QWidget | None: - """Returns a QtWidget based on its objectName. - """ + """Returns a QtWidget based on its objectName.""" for qWidget in qApp.topLevelWidgets(): if qWidget.objectName() == objName: return qWidget diff --git a/novelwriter/config.py b/novelwriter/config.py index cafa3e36..d0bd7fc3 100644 --- a/novelwriter/config.py +++ b/novelwriter/config.py @@ -3,7 +3,8 @@ novelWriter – Config Class ========================== File History: -Created: 2018-09-22 [0.0.1] Config +Created: 2018-09-22 [0.0.1] Config +Created: 2022-11-09 [2.0rc2] RecentProjects This file is a part of novelWriter Copyright 2018–2023, Veronica Berglyd Olsen @@ -28,7 +29,6 @@ import json import logging from time import time -from typing import TYPE_CHECKING from pathlib import Path from PyQt5.QtGui import QFontDatabase @@ -42,9 +42,6 @@ from novelwriter.error import formatException, logException from novelwriter.common import NWConfigParser, checkInt, checkPath, formatTimeStamp from novelwriter.constants import nwFiles, nwUnicode -if TYPE_CHECKING: # pragma: no cover - from novelwriter.gui.theme import GuiTheme - logger = logging.getLogger(__name__) @@ -96,7 +93,6 @@ class Config: # User Settings # ============= - self._themeObj = None self._recentObj = RecentProjects(self) # General GUI Settings @@ -244,12 +240,6 @@ class Config: def recentProjects(self) -> RecentProjects: return self._recentObj - @property - def theme(self) -> GuiTheme: - if self._themeObj is None: - raise Exception("Cannot access GUI theme before it is initialised") - return self._themeObj - @property def mainWinSize(self) -> list[int]: return [int(x*self.guiScale) for x in self._mainWinSize] @@ -297,11 +287,6 @@ class Config: # Setters ## - def setThemeInstance(self, theme: GuiTheme) -> None: - """Set the applications theme instance.""" - self._themeObj = theme - return - def setMainWinSize(self, width: int, height: int) -> None: """Set the size of the main window, but only if the change is larger than 5 pixels. The OS window manager will sometimes @@ -499,7 +484,7 @@ class Config: self._recentObj.loadCache() self._checkOptionalPackages() - logger.debug("Config initialisation complete") + logger.debug("Config instance initialised") return diff --git a/novelwriter/constants.py b/novelwriter/constants.py index f74ce319..3e7d0bb6 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -25,7 +25,7 @@ from __future__ import annotations from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP -from novelwriter.enum import nwAlert, nwBuildFmt, nwItemClass, nwItemLayout, nwOutline +from novelwriter.enum import nwBuildFmt, nwItemClass, nwItemLayout, nwOutline def trConst(text: str) -> str: @@ -52,6 +52,9 @@ class nwConst: URL_HELP = "https://github.com/vkbo/novelWriter/discussions" URL_RELEASE = "https://github.com/vkbo/novelWriter/releases/latest" + # Gui Settings + STATUS_MSG_TIMEOUT = 15000 # milliseconds + # END Class nwConst @@ -162,12 +165,6 @@ class nwLabels: nwItemLayout.DOCUMENT: QT_TRANSLATE_NOOP("Constant", "Novel Document"), nwItemLayout.NOTE: QT_TRANSLATE_NOOP("Constant", "Project Note"), } - ALERT_NAME = { - nwAlert.INFO: QT_TRANSLATE_NOOP("Constant", "Information"), - nwAlert.WARN: QT_TRANSLATE_NOOP("Constant", "Warning"), - nwAlert.ERROR: QT_TRANSLATE_NOOP("Constant", "Error"), - nwAlert.ASK: QT_TRANSLATE_NOOP("Constant", "Question"), - } ITEM_DESCRIPTION = { "none": QT_TRANSLATE_NOOP("Constant", "None"), "root": QT_TRANSLATE_NOOP("Constant", "Root Folder"), diff --git a/novelwriter/core/coretools.py b/novelwriter/core/coretools.py index 094c8c55..017ea082 100644 --- a/novelwriter/core/coretools.py +++ b/novelwriter/core/coretools.py @@ -28,21 +28,17 @@ from __future__ import annotations import shutil import logging -from typing import TYPE_CHECKING, Iterable +from typing import Iterable from functools import partial from PyQt5.QtCore import QCoreApplication -from novelwriter import CONFIG -from novelwriter.enum import nwAlert +from novelwriter import CONFIG, SHARED from novelwriter.common import minmax, simplified from novelwriter.constants import nwItemClass from novelwriter.core.item import NWItem from novelwriter.core.project import NWProject -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) @@ -312,8 +308,7 @@ class ProjectBuilder: parameter provided by the New Project Wizard. """ - def __init__(self, mainGui: GuiMain) -> None: - self.mainGui = mainGui + def __init__(self) -> None: self.tr = partial(QCoreApplication.translate, "NWProject") return @@ -344,7 +339,7 @@ class ProjectBuilder: logger.error("No project path set for the new project") return False - project = NWProject(self.mainGui) + project = NWProject() if not project.storage.openProjectInPlace(projPath, newProject=True): return False @@ -478,17 +473,17 @@ class ProjectBuilder: try: shutil.unpack_archive(pkgSample, projPath) except Exception as exc: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "Failed to create a new example project." - ), level=nwAlert.ERROR, exception=exc) + ), exc=exc) return False else: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "Failed to create a new example project. " "Could not find the necessary files. " "They seem to be missing from this installation." - ), level=nwAlert.ERROR) + )) return False return True diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 4e815ed8..45b91487 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -533,7 +533,7 @@ class NWIndex: def getTableOfContents( self, rHandle: str, maxDepth: int, skipExcl: bool = True - ) -> list[tuple[str, str, str, int]]: + ) -> list[tuple[str, int, str, int]]: """Generate a table of contents up to a maximum depth.""" tOrder = [] tData = {} diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index c1b2ea2a..e86776ac 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -33,8 +33,8 @@ from functools import partial from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal -from novelwriter import CONFIG, __version__, __hexversion__ -from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert +from novelwriter import CONFIG, SHARED, __version__, __hexversion__ +from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.error import logException from novelwriter.constants import trConst, nwLabels from novelwriter.core.tree import NWTree @@ -49,7 +49,6 @@ from novelwriter.common import ( ) if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain from novelwriter.core.item import NWItem from novelwriter.core.status import NWStatus @@ -58,13 +57,11 @@ logger = logging.getLogger(__name__) class NWProject(QObject): - projectStatusChanged = pyqtSignal(bool) + statusChanged = pyqtSignal(bool) + statusMessage = pyqtSignal(str) - def __init__(self, mainGui: GuiMain) -> None: - super().__init__(parent=mainGui) - - # Internal - self.mainGui = mainGui + def __init__(self, parent: QObject | None = None) -> None: + super().__init__(parent=parent) # Core Elements self._options = OptionState(self) # Project-specific GUI options @@ -75,13 +72,20 @@ class NWProject(QObject): self._session = NWSessionLog(self) # The session record # Project Status - self._langData = {} # Localisation data - self._projChanged = False # The project has unsaved changes - self._lockedBy = None # Data on which computer has the project open + self._langData = {} # Localisation data + self._lockedBy = None # Data on which computer has the project open + self._changed = False # The project has unsaved changes + self._valid = False # The project was successfully loaded # Internal Mapping self.tr = partial(QCoreApplication.translate, "NWProject") + logger.debug("Ready: NWProject") + + return + + def __del__(self): # pragma: no cover + logger.debug("Delete: NWProject") return ## @@ -118,7 +122,24 @@ class NWProject(QObject): @property def projChanged(self) -> bool: - return self._projChanged + return self._changed + + @property + def isValid(self) -> bool: + """Return True if a project is loaded.""" + return self._valid + + @property + def lockStatus(self) -> list | None: + """Return the project lock information.""" + if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4: + return self._lockedBy + return None + + @property + def currentEditTime(self) -> int: + """Return total edit time, including the current session.""" + return self._data.editTime + round(time() - self._session.start) ## # Item Methods @@ -139,7 +160,7 @@ class NWProject(QObject): """Add a new file with a given label and parent item.""" return self._tree.create(label, parent, nwItemType.FILE) - def writeNewFile(self, tHandle: str, hLevel: int, isDocument: bool, addText: str = "") -> bool: + def writeNewFile(self, tHandle: str, hLevel: int, isDocument: bool, text: str = "") -> bool: """Write content to a new document after it is created. This will not run if the file exists and is not empty. """ @@ -154,7 +175,7 @@ class NWProject(QObject): return False hshText = "#"*minmax(hLevel, 1, 4) - newText = f"{hshText} {tItem.itemName}\n\n{addText}" + newText = f"{hshText} {tItem.itemName}\n\n{text}" if tItem.isNovelLike() and isDocument: tItem.setLayout(nwItemLayout.DOCUMENT) else: @@ -172,9 +193,9 @@ class NWProject(QObject): if self._tree.checkType(tHandle, nwItemType.FILE): delDoc = self._storage.getDocument(tHandle) if not delDoc.deleteDocument(): - self.mainGui.makeAlert( + SHARED.error( self.tr("Could not delete document file."), - info=delDoc.getError(), level=nwAlert.ERROR + info=delDoc.getError() ) return False @@ -195,45 +216,21 @@ class NWProject(QObject): # Project Methods ## - def clearProject(self) -> None: - """Clear the data for the current project, and set them to - default values. - - Note: Don't clear the lockedBy data here as it is needed after - this function is called. - """ - # Core Elements - self._options = OptionState(self) - self._storage.clear() - self._data = NWProjectData(self) - self._tree.clear() - self._index.clearIndex() - self._session = NWSessionLog(self) - - # Project Status - self._langData = {} - self._projChanged = False - - return - - def openProject(self, projPath: str | Path, overrideLock: bool = False) -> bool: + def openProject(self, projPath: str | Path, clearLock: bool = False) -> bool: """Open the project file provided. If it doesn't exist, assume it is a folder and look for the file within it. If successful, parse the XML of the file and populate the project variables and build the tree of project items. """ - self.clearProject() logger.info("Opening project: %s", projPath) if not self._storage.openProjectInPlace(projPath): - self.mainGui.makeAlert(self.tr( - "Could not open project with path: {0}" - ).format(projPath), level=nwAlert.ERROR) + SHARED.error(self.tr("Could not open project with path: {0}").format(projPath)) return False # Project Lock # ============ - if overrideLock: + if clearLock: self._storage.clearLockFile() lockStatus = self._storage.readLockFile() @@ -243,7 +240,6 @@ class NWProject(QObject): else: logger.error("Project is locked, so not opening") self._lockedBy = lockStatus - self.clearProject() return False else: logger.debug("Project is not locked") @@ -253,7 +249,6 @@ class NWProject(QObject): xmlReader = self._storage.getXmlReader() if not isinstance(xmlReader, ProjectXMLReader): - self.clearProject() return False self._data = NWProjectData(self) @@ -264,49 +259,43 @@ class NWProject(QObject): if not xmlParsed: if xmlReader.state == XMLReadState.NOT_NWX_FILE: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "Project file does not appear to be a novelWriterXML file." - ), level=nwAlert.ERROR) + )) elif xmlReader.state == XMLReadState.UNKNOWN_VERSION: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "Unknown or unsupported novelWriter project file format. " "The project cannot be opened by this version of novelWriter. " "The file was saved with novelWriter version {0}." - ).format(appVersion), level=nwAlert.ERROR) + ).format(appVersion)) else: - self.mainGui.makeAlert(self.tr( - "Failed to parse project xml." - ), level=nwAlert.ERROR) - - self.clearProject() + SHARED.error(self.tr("Failed to parse project xml.")) return False # Check Legacy Upgrade # ==================== if xmlReader.state == XMLReadState.WAS_LEGACY: - msgYes = self.mainGui.askQuestion(self.tr( + msgYes = SHARED.question(self.tr( "The file format of your project is about to be updated. " "If you proceed, older versions of novelWriter will no " "longer be able to open this project. Continue?" )) if not msgYes: - self.clearProject() return False # Check novelWriter Version # ========================= if xmlReader.hexVersion > hexToInt(__hexversion__): - msgYes = self.mainGui.askQuestion(self.tr( + msgYes = SHARED.question(self.tr( "This project was saved by a newer version of " "novelWriter, version {0}. This is version {1}. If you " "continue to open the project, some attributes and " "settings may not be preserved, but the overall project " "should be fine. Continue opening the project?" - ).format(appVersion, __version__)) + ).format(appVersion, __version__), warn=True) if not msgYes: - self.clearProject() return False # Extract Data @@ -317,17 +306,19 @@ class NWProject(QObject): self._loadProjectLocalisation() # Update recent projects - CONFIG.recentProjects.update( - self._storage.storagePath, self._data.name, sum(self._data.initCounts), time() - ) + storePath = self._storage.storagePath + if storePath: + CONFIG.recentProjects.update( + storePath, self._data.name, sum(self._data.initCounts), time() + ) # Check the project tree consistency # This also handles any orphaned files found orphans, recovered = self._tree.checkConsistency(self.tr("Recovered")) if orphans > 0: - self.mainGui.makeAlert(self.tr( + SHARED.warn(self.tr( "Found {0} orphaned file(s) in the project. {1} file(s) were recovered." - ).format(orphans, recovered), level=nwAlert.WARN) + ).format(orphans, recovered)) self._index.loadIndex() if xmlReader.state == XMLReadState.WAS_LEGACY: @@ -338,7 +329,9 @@ class NWProject(QObject): self._session.startSession() self._storage.writeLockFile() self.setProjectChanged(False) - self.mainGui.setStatus(self.tr("Opened Project: {0}").format(self._data.name)) + self._valid = True + + self.statusMessage.emit(self.tr("Opened Project: {0}").format(self._data.name)) return True @@ -349,9 +342,7 @@ class NWProject(QObject): file. """ if not self._storage.isOpen(): - self.mainGui.makeAlert(self.tr( - "There is no project open." - ), level=nwAlert.ERROR) + SHARED.error(self.tr("There is no project open.")) return False saveTime = time() @@ -374,9 +365,7 @@ class NWProject(QObject): editTime = self._data.editTime + max(round(saveTime - self._session.start), 0) content = self._tree.pack() if not xmlWriter.write(self._data, content, saveTime, editTime): - self.mainGui.makeAlert(self.tr( - "Failed to save project." - ), level=nwAlert.ERROR, exception=xmlWriter.error) + SHARED.error(self.tr("Failed to save project."), exc=xmlWriter.error) return False # Save other project data @@ -385,24 +374,25 @@ class NWProject(QObject): self._storage.runPostSaveTasks(autoSave=autoSave) # Update recent projects - CONFIG.recentProjects.update( - self._storage.storagePath, self._data.name, sum(self._data.currCounts), saveTime - ) + storePath = self._storage.storagePath + if storePath: + CONFIG.recentProjects.update( + storePath, self._data.name, sum(self._data.currCounts), saveTime + ) self._storage.writeLockFile() - self.mainGui.setStatus(self.tr("Saved Project: {0}").format(self._data.name)) + self.statusMessage.emit(self.tr("Saved Project: {0}").format(self._data.name)) self.setProjectChanged(False) return True def closeProject(self, idleTime: float = 0.0) -> None: - """Close the current project and clear all meta data.""" + """Close the project.""" logger.info("Closing project") self._options.saveSettings() self._tree.writeToCFile() self._session.appendSession(idleTime) self._storage.closeSession() - self.clearProject() self._lockedBy = None return @@ -413,13 +403,13 @@ class NWProject(QObject): return False logger.info("Backing up project") - self.mainGui.setStatus(self.tr("Backing up project ...")) + self.statusMessage.emit(self.tr("Backing up project ...")) if not self._data.name: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "Cannot backup project because no project name is set. " "Please set a Project Name in Project Settings." - ), level=nwAlert.ERROR) + )) return False cleanName = makeFileNameSafe(self._data.name) @@ -428,9 +418,7 @@ class NWProject(QObject): try: baseDir.mkdir(exist_ok=True, parents=True) except Exception as exc: - self.mainGui.makeAlert(self.tr( - "Could not create backup folder." - ), level=nwAlert.ERROR, exception=exc) + SHARED.error(self.tr("Could not create backup folder."), exc=exc) return False timeStamp = formatTimeStamp(time(), fileSafe=True) @@ -438,19 +426,15 @@ class NWProject(QObject): if self._storage.zipIt(archName, compression=2): size = formatInt(archName.stat().st_size) if doNotify: - self.mainGui.makeAlert( + SHARED.info( self.tr("Created a backup of your project of size {0}B.").format(size), info=self.tr("Path: {0}").format(str(backupPath)) ) else: - self.mainGui.makeAlert(self.tr( - "Could not write backup archive." - ), level=nwAlert.ERROR) + SHARED.error(self.tr("Could not write backup archive.")) return False - self.mainGui.setStatus(self.tr( - "Project backed up to '{0}'" - ).format(str(archName))) + self.statusMessage.emit(self.tr("Project backed up to '{0}'").format(str(archName))) return True @@ -503,27 +487,15 @@ class NWProject(QObject): information to the GUI statusbar. """ if isinstance(status, bool): - self._projChanged = status - self.projectStatusChanged.emit(self._projChanged) - return self._projChanged + self._changed = status + self.statusChanged.emit(self._changed) + return self._changed ## - # Getters + # Class Methods ## - def getLockStatus(self) -> list | None: - """Return the project lock information for the project.""" - if isinstance(self._lockedBy, list) and len(self._lockedBy) == 4: - return self._lockedBy - return None - - def getCurrentEditTime(self) -> int: - """Get the total project edit time, including the time spent in - the current session. - """ - return self._data.editTime + round(time() - self._session.start) - - def getProjectItems(self) -> Iterator[NWItem]: + def iterProjectItems(self) -> Iterator[NWItem]: """This function ensures that the item tree loaded is sent to the GUI tree view in such a way that the tree can be built. That is, the parent item must be sent before its child. In principle, @@ -531,7 +503,7 @@ class NWProject(QObject): order has been altered, or a file is orphaned, this function is capable of handling it. """ - sentItems = [] + sentItems = set() iterItems = self._tree.handles() n = 0 nMax = min(len(iterItems), 10000) @@ -540,17 +512,15 @@ class NWProject(QObject): tItem = self._tree[tHandle] n += 1 if tItem is None: - # Technically a bug since treeOrder is built from the - # same data as _projTree + # Technically a bug continue elif tItem.itemParent is None: - # Item is a root, or already been identified as an - # orphaned item - sentItems.append(tHandle) + # Item is a root, or already been identified as orphaned + sentItems.add(tHandle) yield tItem elif tItem.itemParent in sentItems: # Item's parent has been sent, so all is fine - sentItems.append(tHandle) + sentItems.add(tHandle) yield tItem elif tItem.itemParent in iterItems: # Item's parent exists, but hasn't been sent yet, so add @@ -566,10 +536,6 @@ class NWProject(QObject): yield tItem return - ## - # Class Methods - ## - def updateWordCounts(self) -> None: """Update the total word count values.""" novel, notes = self._tree.sumWords() diff --git a/novelwriter/core/spellcheck.py b/novelwriter/core/spellcheck.py index d7d55d91..0ebfe654 100644 --- a/novelwriter/core/spellcheck.py +++ b/novelwriter/core/spellcheck.py @@ -213,15 +213,15 @@ class UserDictionary: def load(self) -> None: """Load the user's dictionary.""" self._path = self._project.storage.getMetaFile(nwFiles.DICT_FILE) - if not isinstance(self._path, Path): - return - try: - with open(self._path, mode="r", encoding="utf-8") as fObj: - data = json.load(fObj) - self._words = set(data.get("novelWriter.userDict", [])) - except Exception: - logger.error("Failed to load user dictionary") - logException() + self._words = set() + if isinstance(self._path, Path) and self._path.is_file(): + try: + with open(self._path, mode="r", encoding="utf-8") as fObj: + data = json.load(fObj) + self._words = set(data.get("novelWriter.userDict", [])) + except Exception: + logger.error("Failed to load user dictionary") + logException() return def save(self) -> None: diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py index fad14027..80b0ab1c 100644 --- a/novelwriter/core/status.py +++ b/novelwriter/core/status.py @@ -129,7 +129,7 @@ class NWStatus: def name(self, key: str | None) -> str: """Return the name associated with a given key.""" - if key in self._store: + if key and key in self._store: return self._store[key]["name"] elif self._default is not None: return self._store[self._default]["name"] @@ -137,7 +137,7 @@ class NWStatus: def cols(self, key: str | None) -> tuple[int, int, int]: """Return the colours associated with a given key.""" - if key in self._store: + if key and key in self._store: return self._store[key]["cols"] elif self._default is not None: return self._store[self._default]["cols"] @@ -145,7 +145,7 @@ class NWStatus: def count(self, key: str | None) -> int: """Return the count associated with a given key.""" - if key in self._store: + if key and key in self._store: return self._store[key]["count"] elif self._default is not None: return self._store[self._default]["count"] @@ -153,7 +153,7 @@ class NWStatus: def icon(self, key: str | None) -> QIcon: """Return the icon associated with a given key.""" - if key in self._store: + if key and key in self._store: return self._store[key]["icon"] elif self._default is not None: return self._store[self._default]["icon"] @@ -186,9 +186,9 @@ class NWStatus: self._store[key]["count"] = 0 return - def increment(self, key: str) -> None: + def increment(self, key: str | None) -> None: """Increment the counter for a given entry.""" - if key in self._store: + if key and key in self._store: self._store[key]["count"] += 1 return diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py index f5d54d29..6fc281b3 100644 --- a/novelwriter/core/tokenizer.py +++ b/novelwriter/core/tokenizer.py @@ -36,7 +36,7 @@ from functools import partial from PyQt5.QtCore import QCoreApplication, QRegularExpression -from novelwriter.enum import nwItemLayout, nwItemType +from novelwriter.enum import nwItemLayout from novelwriter.common import formatTimeStamp, numberToRoman, checkInt from novelwriter.constants import nwConst, nwHeadFmt, nwRegEx, nwUnicode from novelwriter.core.project import NWProject @@ -315,10 +315,8 @@ class Tokenizer(ABC): def addRootHeading(self, tHandle: str) -> bool: """Add a heading at the start of a new root folder.""" - if not self._project.tree.checkType(tHandle, nwItemType.ROOT): - return False - theItem = self._project.tree[tHandle] - if not theItem: + tItem = self._project.tree[tHandle] + if not tItem or not tItem.isRootType(): return False if self._isFirst: @@ -327,14 +325,14 @@ class Tokenizer(ABC): else: textAlign = self.A_PBB | self.A_CENTRE - locNotes = self._localLookup("Notes") - theTitle = f"{locNotes}: {theItem.itemName}" + trNotes = self._localLookup("Notes") + title = f"{trNotes}: {tItem.itemName}" self._tokens = [] self._tokens.append(( - self.T_TITLE, 0, theTitle, None, textAlign + self.T_TITLE, 0, title, None, textAlign )) if self._keepMarkdown: - self._allMarkdown.append(f"# {theTitle}\n\n") + self._allMarkdown.append(f"# {title}\n\n") return True diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 31969a9d..fef2759d 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -65,13 +65,12 @@ class NWTree: self._project = project - self._projTree: dict[str, NWItem] = {} # Holds all the items of the project - self._treeOrder: list[str] = [] # The order of the tree items in the tree view - self._treeRoots: dict[str, NWItem] = {} # The root items of the tree + self._tree: dict[str, NWItem] = {} # Holds all the items of the project + self._order: list[str] = [] # The order of the tree items in the tree view + self._roots: dict[str, NWItem] = {} # The root items of the tree - self._trashRoot = None # The handle of the trash root folder - self._archRoot = None # The handle of the archive root folder - self._treeChanged = False # True if tree structure has changed + self._trash = None # The handle of the trash root folder + self._changed = False # True if tree structure has changed return @@ -81,17 +80,16 @@ class NWTree: def clear(self) -> None: """Clear the item tree entirely.""" - self._projTree = {} - self._treeOrder = [] - self._treeRoots = {} - self._trashRoot = None - self._archRoot = None - self._treeChanged = False + self._tree = {} + self._order = [] + self._roots = {} + self._trash = None + self._changed = False return def handles(self) -> list[str]: """Returns a copy of the list of all the active handles.""" - return self._treeOrder.copy() + return self._order.copy() @overload # pragma: no cover def create(self, label: str, parent: None, itemType: Literal[nwItemType.ROOT], @@ -109,7 +107,7 @@ class NWTree: parent, None is returned. For root elements, this cannot occur. """ parent = None if itemType == nwItemType.ROOT else parent - if parent is None or parent in self._treeOrder: + if parent is None or parent in self._order: tHandle = self._makeHandle() newItem = NWItem(self._project, tHandle) newItem.setName(label) @@ -130,7 +128,7 @@ class NWTree: logger.warning("Invalid item handle '%s' detected, skipping", tHandle) return False - if tHandle in self._projTree: + if tHandle in self._tree: logger.warning("Duplicate handle '%s' detected, skipping", tHandle) return False @@ -138,20 +136,17 @@ class NWTree: if nwItem.isRootType(): logger.debug("Item '%s' is a root item", str(tHandle)) - self._treeRoots[tHandle] = nwItem - if nwItem.itemClass == nwItemClass.ARCHIVE: - logger.debug("Item '%s' is the archive folder", str(tHandle)) - self._archRoot = tHandle - elif nwItem.itemClass == nwItemClass.TRASH: - if self._trashRoot is None: + self._roots[tHandle] = nwItem + if nwItem.itemClass == nwItemClass.TRASH: + if self._trash is None: logger.debug("Item '%s' is the trash folder", str(tHandle)) - self._trashRoot = tHandle + self._trash = tHandle else: logger.error("Only one trash folder allowed") return False - self._projTree[tHandle] = nwItem - self._treeOrder.append(tHandle) + self._tree[tHandle] = nwItem + self._order.append(tHandle) self._setTreeChanged(True) return True @@ -171,7 +166,7 @@ class NWTree: items. In the order defined by the _treeOrder list. """ tree = [] - for tHandle in self._treeOrder: + for tHandle in self._order: tItem = self.__getitem__(tHandle) if tItem: tree.append(tItem.pack()) @@ -199,7 +194,7 @@ class NWTree: """ storage = self._project.storage files = set(storage.scanContent()) - for tHandle in self._treeOrder: + for tHandle in self._order: if self.updateItemData(tHandle): logger.debug("Checking item '%s' ... OK", tHandle) files.discard(tHandle) # Remove it from the record @@ -220,7 +215,7 @@ class NWTree: oName, oParent, oClass, oLayout = aDoc.getMeta() oName = oName or cHandle - oParent = oParent if oParent in self._treeOrder else None + oParent = oParent if oParent in self._order else None oClass = oClass or nwItemClass.NOVEL oLayout = oLayout or nwItemLayout.NOTE @@ -229,8 +224,10 @@ class NWTree: oParent = self.findRoot(oClass) if oParent is None: # Otherwise, add to the Novel root oParent = self.findRoot(nwItemClass.NOVEL) - if oParent is None: # If not, give up - continue + if oParent is None: # If not, create a new novel folder + oParent = self.create(prefix, None, nwItemType.ROOT, nwItemClass.NOVEL) + + assert oParent is not None # Otherwise there's an issue with self.create() # Create a new item newItem = NWItem(self._project, cHandle) @@ -256,7 +253,7 @@ class NWTree: tocList = [] tocLen = 0 - for tHandle in self._treeOrder: + for tHandle in self._order: tItem = self.__getitem__(tHandle) if tItem is None: continue @@ -298,7 +295,7 @@ class NWTree: """Loop over all entries and add up the word counts.""" noteWords = 0 novelWords = 0 - for tHandle in self._treeOrder: + for tHandle in self._order: tItem = self.__getitem__(tHandle) if tItem is None: continue @@ -374,13 +371,13 @@ class NWTree: def rootClasses(self) -> set[nwItemClass]: """Return a set of all root classes in use by the project.""" rootClasses = set() - for nwItem in self._treeRoots.values(): + for nwItem in self._roots.values(): rootClasses.add(nwItem.itemClass) return rootClasses def iterRoots(self, itemClass: nwItemClass | None) -> Iterator[tuple[str, NWItem]]: """Iterate over all root items of a given class in order.""" - for tHandle in self._treeOrder: + for tHandle in self._order: nwItem = self.__getitem__(tHandle) if isinstance(nwItem, NWItem) and nwItem.isRootType(): if itemClass is None or nwItem.itemClass == itemClass: @@ -394,12 +391,12 @@ class NWTree: return True if tItem.itemClass == nwItemClass.TRASH: return True - if self._trashRoot is not None: - if tHandle == self._trashRoot: + if self._trash is not None: + if tHandle == self._trash: return True - elif tItem.itemParent == self._trashRoot: + elif tItem.itemParent == self._trash: return True - elif tItem.itemRoot == self._trashRoot: + elif tItem.itemRoot == self._trash: return True return False @@ -407,13 +404,13 @@ class NWTree: """Returns the handle of the trash folder, or None if there isn't one. """ - if self._trashRoot: - return self._trashRoot + if self._trash: + return self._trash return None def findRoot(self, itemClass: nwItemClass | None) -> str | None: """Find the first root item for a given class.""" - for aRoot in self._treeRoots: + for aRoot in self._roots: tItem = self.__getitem__(aRoot) if tItem is None: continue @@ -427,18 +424,18 @@ class NWTree: def setOrder(self, newOrder: list[str]) -> None: """Reorders the tree based on a list of items.""" - tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._projTree] - if not (len(tmpOrder) == len(newOrder) == len(self._treeOrder)): + tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._tree] + if not (len(tmpOrder) == len(newOrder) == len(self._order)): # Something is wrong, so let's debug it for tHandle in newOrder: - if tHandle not in self._projTree: + if tHandle not in self._tree: logger.error("Handle '%s' in new tree order is not in old order", tHandle) - for tHandle in self._treeOrder: + for tHandle in self._order: if tHandle not in tmpOrder: logger.warning("Handle '%s' in old tree order is not in new order", tHandle) # Save the temp list - self._treeOrder = tmpOrder + self._order = tmpOrder self._setTreeChanged(True) logger.debug("Project tree order updated") @@ -450,36 +447,34 @@ class NWTree: def __len__(self) -> int: """The number of items in the project.""" - return len(self._treeOrder) + return len(self._order) def __bool__(self) -> bool: """True if there are any items in the project.""" - return bool(self._treeOrder) + return bool(self._order) def __getitem__(self, tHandle: str | None) -> NWItem | None: """Return a project item based on its handle. Returns None if the handle doesn't exist in the project. """ - if tHandle and tHandle in self._projTree: - return self._projTree[tHandle] + if tHandle and tHandle in self._tree: + return self._tree[tHandle] logger.error("No tree item with handle '%s'", str(tHandle)) return None def __delitem__(self, tHandle: str) -> None: """Remove an item from the internal lists and dictionaries.""" - if tHandle in self._treeOrder and tHandle in self._projTree: - self._treeOrder.remove(tHandle) - del self._projTree[tHandle] + if tHandle in self._order and tHandle in self._tree: + self._order.remove(tHandle) + del self._tree[tHandle] else: logger.warning("Failed to delete item '%s': item not found", tHandle) return - if tHandle in self._treeRoots: - del self._treeRoots[tHandle] - if tHandle == self._trashRoot: - self._trashRoot = None - if tHandle == self._archRoot: - self._archRoot = None + if tHandle in self._roots: + del self._roots[tHandle] + if tHandle == self._trash: + self._trash = None self._setTreeChanged(True) @@ -487,12 +482,12 @@ class NWTree: def __contains__(self, tHandle: str) -> bool: """Checks if a handle exists in the tree.""" - return tHandle in self._treeOrder + return tHandle in self._order def __iter__(self) -> Iterator[NWItem]: """Iterate through project items.""" - for tHandle in self._treeOrder: - tItem = self._projTree.get(tHandle) + for tHandle in self._order: + tItem = self._tree.get(tHandle) if isinstance(tItem, NWItem): yield tItem return @@ -505,7 +500,7 @@ class NWTree: """Set the changed flag to theState, and if being set to True, propagate that state change to the parent NWProject class. """ - self._treeChanged = state + self._changed = state if state: self._project.setProjectChanged(True) return @@ -516,7 +511,7 @@ class NWTree: """ logger.debug("Generating new handle") handle = f"{random.getrandbits(52):013x}" - if handle in self._projTree: + if handle in self._tree: logger.warning("Duplicate handle encountered! Retrying ...") handle = self._makeHandle() diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index 7907882f..c7640ec0 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -35,7 +35,7 @@ from PyQt5.QtWidgets import ( QTextBrowser, QVBoxLayout, QWidget ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.common import readTextFile from novelwriter.constants import nwConst @@ -60,7 +60,7 @@ class GuiAbout(QDialog): nPx = CONFIG.pxInt(96) self.nwIcon = QLabel() - self.nwIcon.setPixmap(CONFIG.theme.getPixmap("novelwriter", (nPx, nPx))) + self.nwIcon.setPixmap(SHARED.theme.getPixmap("novelwriter", (nPx, nPx))) self.lblName = QLabel("novelWriter") self.lblVers = QLabel(f"v{novelwriter.__version__}") self.lblDate = QLabel(datetime.strptime(novelwriter.__date__, "%Y-%m-%d").strftime("%x")) @@ -228,12 +228,12 @@ class GuiAbout(QDialog): " color: rgb({kColR},{kColG},{kColB});" "}}\n" ).format( - hColR=CONFIG.theme.colHead[0], - hColG=CONFIG.theme.colHead[1], - hColB=CONFIG.theme.colHead[2], - kColR=CONFIG.theme.colKey[0], - kColG=CONFIG.theme.colKey[1], - kColB=CONFIG.theme.colKey[2], + hColR=SHARED.theme.colHead[0], + hColG=SHARED.theme.colHead[1], + hColB=SHARED.theme.colHead[2], + kColR=SHARED.theme.colKey[0], + kColG=SHARED.theme.colKey[1], + kColB=SHARED.theme.colKey[2], ) self.pageAbout.document().setDefaultStyleSheet(styleSheet) self.pageNotes.document().setDefaultStyleSheet(styleSheet) diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 0faf9dd9..9af0a168 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -29,10 +29,10 @@ import logging from PyQt5.QtCore import Qt, QSize from PyQt5.QtWidgets import ( QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel, - QListWidget, QListWidgetItem, QVBoxLayout, + QListWidget, QListWidgetItem, QVBoxLayout, QWidget ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.configlayout import NHelpLabel @@ -43,24 +43,21 @@ class GuiDocMerge(QDialog): D_HANDLE = Qt.ItemDataRole.UserRole - def __init__(self, mainGui, sHandle, itemList): - super().__init__(parent=mainGui) + def __init__(self, parent: QWidget, sHandle: str, itemList: list[str]) -> None: + super().__init__(parent=parent) logger.debug("Create: GuiDocMerge") self.setObjectName("GuiDocMerge") - - self.mainGui = mainGui + self.setWindowTitle(self.tr("Merge Documents")) self._data = {} - self.setWindowTitle(self.tr("Merge Documents")) - self.headLabel = QLabel("{0}".format(self.tr("Documents to Merge"))) self.helpLabel = NHelpLabel(self.tr( "Drag and drop items to change the order, or uncheck to exclude." - ), CONFIG.theme.helpText) + ), SHARED.theme.helpText) - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize hSp = CONFIG.pxInt(12) vSp = CONFIG.pxInt(8) bSp = CONFIG.pxInt(12) @@ -155,11 +152,11 @@ class GuiDocMerge(QDialog): self.listBox.clear() for tHandle in itemList: - nwItem = self.mainGui.project.tree[tHandle] + nwItem = SHARED.project.tree[tHandle] if nwItem is None or not nwItem.isFileType(): continue - itemIcon = CONFIG.theme.getItemIcon( + itemIcon = SHARED.theme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading ) diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 9542268f..26b54f59 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -32,7 +32,7 @@ from PyQt5.QtWidgets import ( QListWidgetItem, QDialogButtonBox, QLabel, QGridLayout ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.configlayout import NHelpLabel @@ -45,14 +45,12 @@ class GuiDocSplit(QDialog): LEVEL_ROLE = Qt.ItemDataRole.UserRole + 1 LABEL_ROLE = Qt.ItemDataRole.UserRole + 2 - def __init__(self, mainGui, sHandle): - super().__init__(parent=mainGui) + def __init__(self, parent, sHandle): + super().__init__(parent=parent) logger.debug("Create: GuiDocSplit") self.setObjectName("GuiDocSplit") - self.mainGui = mainGui - self._data = {} self._text = [] @@ -61,16 +59,16 @@ class GuiDocSplit(QDialog): self.headLabel = QLabel("{0}".format(self.tr("Document Headers"))) self.helpLabel = NHelpLabel( self.tr("Select the maximum level to split into files."), - CONFIG.theme.helpText + SHARED.theme.helpText ) # Values - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize hSp = CONFIG.pxInt(12) vSp = CONFIG.pxInt(8) bSp = CONFIG.pxInt(12) - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options spLevel = pOptions.getInt("GuiDocSplit", "spLevel", 3) intoFolder = pOptions.getBool("GuiDocSplit", "intoFolder", True) docHierarchy = pOptions.getBool("GuiDocSplit", "docHierarchy", True) @@ -169,7 +167,7 @@ class GuiDocSplit(QDialog): self._data["docHierarchy"] = docHierarchy self._data["moveToTrash"] = moveToTrash - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options pOptions.setValue("GuiDocSplit", "spLevel", spLevel) pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder) pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy) @@ -199,13 +197,13 @@ class GuiDocSplit(QDialog): self.listBox.clear() - nwItem = self.mainGui.project.tree[sHandle] + nwItem = SHARED.project.tree[sHandle] if nwItem is None or not nwItem.isFileType(): return spLevel = self.splitLevel.currentData() if not self._text: - inDoc = self.mainGui.project.storage.getDocument(sHandle) + inDoc = SHARED.project.storage.getDocument(sHandle) self._text = (inDoc.readDocument() or "").splitlines() for lineNo, aLine in enumerate(self._text): diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 47d35b08..21bcb95d 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -32,7 +32,7 @@ from PyQt5.QtWidgets import ( QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.pageddialog import NPagedDialog @@ -163,7 +163,7 @@ class GuiPreferencesGeneral(QWidget): # The Form self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) + self.mainForm.setHelpTextStyle(SHARED.theme.helpText) self.setLayout(self.mainForm) # Look and Feel @@ -190,7 +190,7 @@ class GuiPreferencesGeneral(QWidget): # Select Theme self.guiTheme = QComboBox() self.guiTheme.setMinimumWidth(minWidth) - self.theThemes = CONFIG.theme.listThemes() + self.theThemes = SHARED.theme.listThemes() for themeDir, themeName in self.theThemes: self.guiTheme.addItem(themeName, themeDir) themeIdx = self.guiTheme.findData(CONFIG.guiTheme) @@ -206,7 +206,7 @@ class GuiPreferencesGeneral(QWidget): # Editor Theme self.guiSyntax = QComboBox() self.guiSyntax.setMinimumWidth(CONFIG.pxInt(200)) - self.theSyntaxes = CONFIG.theme.listSyntax() + self.theSyntaxes = SHARED.theme.listSyntax() for syntaxFile, syntaxName in self.theSyntaxes: self.guiSyntax.addItem(syntaxName, syntaxFile) syntaxIdx = self.guiSyntax.findData(CONFIG.guiSyntax) @@ -225,7 +225,7 @@ class GuiPreferencesGeneral(QWidget): self.guiFont.setFixedWidth(CONFIG.pxInt(162)) self.guiFont.setText(CONFIG.guiFont) self.fontButton = QPushButton("...") - self.fontButton.setMaximumWidth(int(2.5*CONFIG.theme.getTextWidth("..."))) + self.fontButton.setMaximumWidth(int(2.5*SHARED.theme.getTextWidth("..."))) self.fontButton.clicked.connect(self._selectFont) self.mainForm.addRow( self.tr("Font family"), @@ -341,7 +341,7 @@ class GuiPreferencesProjects(QWidget): # The Form self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) + self.mainForm.setHelpTextStyle(SHARED.theme.helpText) self.setLayout(self.mainForm) # Automatic Save @@ -493,7 +493,7 @@ class GuiPreferencesDocuments(QWidget): # The Form self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) + self.mainForm.setHelpTextStyle(SHARED.theme.helpText) self.setLayout(self.mainForm) # Text Style @@ -506,7 +506,7 @@ class GuiPreferencesDocuments(QWidget): self.textFont.setFixedWidth(CONFIG.pxInt(162)) self.textFont.setText(CONFIG.textFont) self.fontButton = QPushButton("...") - self.fontButton.setMaximumWidth(int(2.5*CONFIG.theme.getTextWidth("..."))) + self.fontButton.setMaximumWidth(int(2.5*SHARED.theme.getTextWidth("..."))) self.fontButton.clicked.connect(self._selectFont) self.mainForm.addRow( self.tr("Font family"), @@ -649,7 +649,7 @@ class GuiPreferencesEditor(QWidget): # The Form self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) + self.mainForm.setHelpTextStyle(SHARED.theme.helpText) self.setLayout(self.mainForm) mW = CONFIG.pxInt(250) @@ -663,17 +663,13 @@ class GuiPreferencesEditor(QWidget): self.spellLanguage.setMaximumWidth(mW) langAvail = self.mainGui.docEditor.spEnchant.listDictionaries() - if CONFIG.hasEnchant: - if langAvail: - for spTag, spProv in langAvail: - qLocal = QLocale(spTag) - spLang = qLocal.nativeLanguageName().title() - self.spellLanguage.addItem("%s [%s]" % (spLang, spProv), spTag) - else: - self.spellLanguage.addItem(self.tr("None"), "") - self.spellLanguage.setEnabled(False) + if CONFIG.hasEnchant and langAvail: + for spTag, spProv in langAvail: + qLocal = QLocale(spTag) + spLang = qLocal.nativeLanguageName().title() + self.spellLanguage.addItem("%s [%s]" % (spLang, spProv), spTag) else: - self.spellLanguage.addItem(self.tr("Not installed"), "") + self.spellLanguage.addItem(self.tr("None"), "") self.spellLanguage.setEnabled(False) spellIdx = self.spellLanguage.findData(CONFIG.spellLanguage) @@ -819,7 +815,7 @@ class GuiPreferencesSyntax(QWidget): # The Form self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) + self.mainForm.setHelpTextStyle(SHARED.theme.helpText) self.setLayout(self.mainForm) # Quotes & Dialogue @@ -921,7 +917,7 @@ class GuiPreferencesAutomation(QWidget): # The Form self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) + self.mainForm.setHelpTextStyle(SHARED.theme.helpText) self.setLayout(self.mainForm) # Automatic Features @@ -1072,7 +1068,7 @@ class GuiPreferencesQuotes(QWidget): # The Form self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) + self.mainForm.setHelpTextStyle(SHARED.theme.helpText) self.setLayout(self.mainForm) # Quotation Style @@ -1080,7 +1076,7 @@ class GuiPreferencesQuotes(QWidget): self.mainForm.addGroupLabel(self.tr("Quotation Style")) qWidth = CONFIG.pxInt(40) - bWidth = int(2.5*CONFIG.theme.getTextWidth("...")) + bWidth = int(2.5*SHARED.theme.getTextWidth("...")) self.quoteSym = {} # Single Quote Style diff --git a/novelwriter/dialogs/projdetails.py b/novelwriter/dialogs/projdetails.py index 5544ec7d..f5415267 100644 --- a/novelwriter/dialogs/projdetails.py +++ b/novelwriter/dialogs/projdetails.py @@ -33,7 +33,7 @@ from PyQt5.QtWidgets import ( QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.common import formatTime, numberToRoman from novelwriter.constants import nwUnicode from novelwriter.extensions.switch import NSwitch @@ -45,19 +45,17 @@ logger = logging.getLogger(__name__) class GuiProjectDetails(NPagedDialog): - def __init__(self, mainGui): - super().__init__(parent=mainGui) + def __init__(self, parent): + super().__init__(parent=parent) logger.debug("Create: GuiProjectDetails") self.setObjectName("GuiProjectDetails") - self.mainGui = mainGui - self.setWindowTitle(self.tr("Project Details")) wW = CONFIG.pxInt(600) wH = CONFIG.pxInt(400) - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options self.setMinimumWidth(wW) self.setMinimumHeight(wH) @@ -66,8 +64,8 @@ class GuiProjectDetails(NPagedDialog): CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "winHeight", wH)) ) - self.tabMain = GuiProjectDetailsMain(self.mainGui) - self.tabContents = GuiProjectDetailsContents(self.mainGui) + self.tabMain = GuiProjectDetailsMain(self) + self.tabContents = GuiProjectDetailsContents(self) self.addTab(self.tabMain, self.tr("Overview")) self.addTab(self.tabContents, self.tr("Contents")) @@ -124,7 +122,7 @@ class GuiProjectDetails(NPagedDialog): countFrom = self.tabContents.poValue.value() clearDouble = self.tabContents.dblValue.isChecked() - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options pOptions.setValue("GuiProjectDetails", "winWidth", winWidth) pOptions.setValue("GuiProjectDetails", "winHeight", winHeight) pOptions.setValue("GuiProjectDetails", "widthCol0", widthCol0) @@ -143,13 +141,11 @@ class GuiProjectDetails(NPagedDialog): class GuiProjectDetailsMain(QWidget): - def __init__(self, mainGui): - super().__init__(parent=mainGui) + def __init__(self, parent): + super().__init__(parent=parent) - self.mainGui = mainGui - - fPx = CONFIG.theme.fontPixelSize - fPt = CONFIG.theme.fontPointSize + fPx = SHARED.theme.fontPixelSize + fPt = SHARED.theme.fontPointSize vPx = CONFIG.pxInt(4) hPx = CONFIG.pxInt(12) @@ -241,14 +237,13 @@ class GuiProjectDetailsMain(QWidget): return - def updateValues(self): - """Set all the values. - """ - project = self.mainGui.project + def updateValues(self) -> None: + """Set all the values.""" + project = SHARED.project pIndex = project.index hCounts = pIndex.getNovelTitleCounts() nwCount = pIndex.getNovelWordCount() - edTime = project.getCurrentEditTime() + edTime = project.currentEditTime self.bookTitle.setText(project.data.title or project.data.name) self.projName.setText(self.tr("Project: {0}").format(project.data.name)) @@ -275,26 +270,24 @@ class GuiProjectDetailsContents(QWidget): C_PAGE = 3 C_PROG = 4 - def __init__(self, mainGui): - super().__init__(parent=mainGui) - - self.mainGui = mainGui + def __init__(self, parent): + super().__init__(parent=parent) # Internal self._theToC = [] self._currentRoot = None - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize hPx = CONFIG.pxInt(12) vPx = CONFIG.pxInt(4) - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options # Header # ====== self.tocLabel = QLabel("%s" % self.tr("Table of Contents")) - self.novelValue = NovelSelector(self, self.mainGui) + self.novelValue = NovelSelector(self) self.novelValue.setMinimumWidth(CONFIG.pxInt(200)) self.novelValue.novelSelectionChanged.connect(self._novelValueChanged) @@ -320,10 +313,11 @@ class GuiProjectDetailsContents(QWidget): ]) treeHeadItem = self.tocTree.headerItem() - treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) - treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignRight) - treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignRight) - treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignRight) + if treeHeadItem: + treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) + treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignRight) + treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignRight) + treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignRight) treeHeader = self.tocTree.header() treeHeader.setStretchLastSection(True) @@ -347,7 +341,7 @@ class GuiProjectDetailsContents(QWidget): wordsPerPage = pOptions.getInt("GuiProjectDetails", "wordsPerPage", 350) countFrom = pOptions.getInt("GuiProjectDetails", "countFrom", 1) - clearDouble = pOptions.getInt("GuiProjectDetails", "clearDouble", True) + clearDouble = pOptions.getBool("GuiProjectDetails", "clearDouble", True) wordsHelp = ( self.tr("Typical word count for a 5 by 8 inch book page with 11 pt font is 350.") @@ -443,7 +437,7 @@ class GuiProjectDetailsContents(QWidget): """Extract the information from the project index. """ logger.debug("Populating ToC from handle '%s'", rootHandle) - self._theToC = self.mainGui.project.index.getTableOfContents(rootHandle, 2) + self._theToC = SHARED.project.index.getTableOfContents(rootHandle, 2) self._theToC.append(("", 0, self.tr("END"), 0)) return @@ -496,7 +490,7 @@ class GuiProjectDetailsContents(QWidget): progPage = f"{cPage:n}" progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%" - hDec = CONFIG.theme.getHeaderDecoration(tLevel) + hDec = SHARED.theme.getHeaderDecoration(tLevel) if tTitle.strip() == "": tTitle = self.tr("Untitled") diff --git a/novelwriter/dialogs/projload.py b/novelwriter/dialogs/projload.py index 09fabd4d..d15635a5 100644 --- a/novelwriter/dialogs/projload.py +++ b/novelwriter/dialogs/projload.py @@ -25,6 +25,7 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING from pathlib import Path from datetime import datetime @@ -36,10 +37,13 @@ from PyQt5.QtWidgets import ( QFileDialog, QLineEdit ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.common import formatInt from novelwriter.constants import nwFiles +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) @@ -55,19 +59,18 @@ class GuiProjectLoad(QDialog): D_PATH = Qt.ItemDataRole.UserRole - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiProjectLoad") self.setObjectName("GuiProjectLoad") - self.mainGui = mainGui self.openState = self.NONE_STATE self.openPath = None sPx = CONFIG.pxInt(16) nPx = CONFIG.pxInt(96) - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize self.outerBox = QVBoxLayout() self.innerBox = QHBoxLayout() @@ -79,7 +82,7 @@ class GuiProjectLoad(QDialog): self.setMinimumHeight(CONFIG.pxInt(400)) self.nwIcon = QLabel() - self.nwIcon.setPixmap(CONFIG.theme.getPixmap("novelwriter", (nPx, nPx))) + self.nwIcon.setPixmap(SHARED.theme.getPixmap("novelwriter", (nPx, nPx))) self.innerBox.addWidget(self.nwIcon, 0, Qt.AlignTop) self.projectForm = QGridLayout() @@ -110,7 +113,7 @@ class GuiProjectLoad(QDialog): self.selPath.setReadOnly(True) self.browseButton = QPushButton("...") - self.browseButton.setMaximumWidth(int(2.5*CONFIG.theme.getTextWidth("..."))) + self.browseButton.setMaximumWidth(int(2.5*SHARED.theme.getTextWidth("..."))) self.browseButton.clicked.connect(self._doBrowse) self.projectForm.addWidget(self.lblRecent, 0, 0, 1, 3) @@ -225,7 +228,7 @@ class GuiProjectLoad(QDialog): selList = self.listBox.selectedItems() if selList: projName = selList[0].text(self.C_NAME) - msgYes = self.mainGui.askQuestion(self.tr( + msgYes = SHARED.question(self.tr( "Remove '{0}' from the recent projects list? " "The project files will not be deleted." ).format(projName)) @@ -268,7 +271,7 @@ class GuiProjectLoad(QDialog): self.listBox.clear() dataList = CONFIG.recentProjects.listEntries() sortList = sorted(dataList, key=lambda x: x[3], reverse=True) - nwxIcon = CONFIG.theme.getIcon("proj_nwx") + nwxIcon = SHARED.theme.getIcon("proj_nwx") for path, title, words, time in sortList: newItem = QTreeWidgetItem([""]*4) newItem.setIcon(self.C_NAME, nwxIcon) @@ -279,7 +282,7 @@ class GuiProjectLoad(QDialog): newItem.setTextAlignment(self.C_NAME, Qt.AlignLeft | Qt.AlignVCenter) newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter) newItem.setTextAlignment(self.C_TIME, Qt.AlignRight | Qt.AlignVCenter) - newItem.setFont(self.C_TIME, CONFIG.theme.guiFontFixed) + newItem.setFont(self.C_TIME, SHARED.theme.guiFontFixed) self.listBox.addTopLevelItem(newItem) self.listBox.setCurrentItem(self.listBox.topLevelItem(0)) diff --git a/novelwriter/dialogs/projsettings.py b/novelwriter/dialogs/projsettings.py index 92b22454..dac33288 100644 --- a/novelwriter/dialogs/projsettings.py +++ b/novelwriter/dialogs/projsettings.py @@ -34,8 +34,7 @@ from PyQt5.QtWidgets import ( QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) -from novelwriter import CONFIG -from novelwriter.enum import nwAlert +from novelwriter import CONFIG, SHARED from novelwriter.common import simplified from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.pageddialog import NPagedDialog @@ -61,12 +60,12 @@ class GuiProjectSettings(NPagedDialog): self.setObjectName("GuiProjectSettings") self.mainGui = mainGui - self.mainGui.project.countStatus() + SHARED.project.countStatus() self.setWindowTitle(self.tr("Project Settings")) wW = CONFIG.pxInt(570) wH = CONFIG.pxInt(375) - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options self.setMinimumWidth(wW) self.setMinimumHeight(wH) @@ -115,7 +114,7 @@ class GuiProjectSettings(NPagedDialog): def _doSave(self): """Save settings and close dialog. """ - project = self.mainGui.project + project = SHARED.project projName = self.tabMain.editName.text() bookTitle = self.tabMain.editTitle.text() bookAuthor = self.tabMain.editAuthor.text() @@ -183,7 +182,7 @@ class GuiProjectSettings(NPagedDialog): statusColW = CONFIG.rpxInt(self.tabStatus.listBox.columnWidth(0)) importColW = CONFIG.rpxInt(self.tabImport.listBox.columnWidth(0)) - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options pOptions.setValue("GuiProjectSettings", "winWidth", winWidth) pOptions.setValue("GuiProjectSettings", "winHeight", winHeight) pOptions.setValue("GuiProjectSettings", "replaceColW", replaceColW) @@ -204,13 +203,13 @@ class GuiProjectEditMain(QWidget): # The Form self.mainForm = NConfigLayout() - self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) + self.mainForm.setHelpTextStyle(SHARED.theme.helpText) self.setLayout(self.mainForm) self.mainForm.addGroupLabel(self.tr("Project Settings")) xW = CONFIG.pxInt(250) - pData = self.mainGui.project.data + pData = SHARED.project.data self.editName = QLineEdit() self.editName.setMaxLength(200) @@ -288,26 +287,24 @@ class GuiProjectEditStatus(QWidget): def __init__(self, projGui, isStatus): super().__init__(parent=projGui) - self.mainGui = projGui.mainGui - if isStatus: - self.theStatus = self.mainGui.project.data.itemStatus + self.theStatus = SHARED.project.data.itemStatus pageLabel = self.tr("Novel File Status Levels") colSetting = "statusColW" else: - self.theStatus = self.mainGui.project.data.itemImport + self.theStatus = SHARED.project.data.itemImport pageLabel = self.tr("Note File Importance Levels") colSetting = "importColW" wCol0 = CONFIG.pxInt( - self.mainGui.project.options.getInt("GuiProjectSettings", colSetting, 130) + SHARED.project.options.getInt("GuiProjectSettings", colSetting, 130) ) self.colDeleted = [] self.colChanged = False self.selColour = QColor(100, 100, 100) - self.iPx = CONFIG.theme.baseIconSize + self.iPx = SHARED.theme.baseIconSize # The List # ======== @@ -326,16 +323,16 @@ class GuiProjectEditStatus(QWidget): # List Controls # ============= - self.addButton = QPushButton(CONFIG.theme.getIcon("add"), "") + self.addButton = QPushButton(SHARED.theme.getIcon("add"), "") self.addButton.clicked.connect(self._newItem) - self.delButton = QPushButton(CONFIG.theme.getIcon("remove"), "") + self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "") self.delButton.clicked.connect(self._delItem) - self.upButton = QPushButton(CONFIG.theme.getIcon("up"), "") + self.upButton = QPushButton(SHARED.theme.getIcon("up"), "") self.upButton.clicked.connect(lambda: self._moveItem(-1)) - self.dnButton = QPushButton(CONFIG.theme.getIcon("down"), "") + self.dnButton = QPushButton(SHARED.theme.getIcon("down"), "") self.dnButton.clicked.connect(lambda: self._moveItem(1)) # Edit Form @@ -441,9 +438,7 @@ class GuiProjectEditStatus(QWidget): if isinstance(selItem, QTreeWidgetItem): iRow = self.listBox.indexOfTopLevelItem(selItem) if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0: - self.mainGui.makeAlert(self.tr( - "Cannot delete a status item that is in use." - ), level=nwAlert.ERROR) + SHARED.error(self.tr("Cannot delete a status item that is in use.")) else: self.listBox.takeTopLevelItem(iRow) self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) @@ -574,11 +569,10 @@ class GuiProjectEditReplace(QWidget): def __init__(self, projGui): super().__init__(parent=projGui) - self.mainGui = projGui.mainGui self.arChanged = False wCol0 = CONFIG.pxInt( - self.mainGui.project.options.getInt("GuiProjectSettings", "replaceColW", 130) + SHARED.project.options.getInt("GuiProjectSettings", "replaceColW", 130) ) pageLabel = self.tr("Text Replace List for Preview and Export") @@ -594,7 +588,7 @@ class GuiProjectEditReplace(QWidget): self.listBox.setColumnWidth(self.COL_KEY, wCol0) self.listBox.setIndentation(0) - for aKey, aVal in self.mainGui.project.data.autoReplace.items(): + for aKey, aVal in SHARED.project.data.autoReplace.items(): newItem = QTreeWidgetItem(["<%s>" % aKey, aVal]) self.listBox.addTopLevelItem(newItem) @@ -604,10 +598,10 @@ class GuiProjectEditReplace(QWidget): # List Controls # ============= - self.addButton = QPushButton(CONFIG.theme.getIcon("add"), "") + self.addButton = QPushButton(SHARED.theme.getIcon("add"), "") self.addButton.clicked.connect(self._addEntry) - self.delButton = QPushButton(CONFIG.theme.getIcon("remove"), "") + self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "") self.delButton.clicked.connect(self._delEntry) # Edit Form diff --git a/novelwriter/dialogs/updates.py b/novelwriter/dialogs/updates.py index 9553fa8d..c39a31cc 100644 --- a/novelwriter/dialogs/updates.py +++ b/novelwriter/dialogs/updates.py @@ -35,7 +35,7 @@ from PyQt5.QtWidgets import ( qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QLabel ) -from novelwriter import CONFIG, __version__, __date__ +from novelwriter import CONFIG, SHARED, __version__, __date__ from novelwriter.common import logException from novelwriter.constants import nwConst @@ -58,7 +58,7 @@ class GuiUpdates(QDialog): # Left Box self.nwIcon = QLabel() - self.nwIcon.setPixmap(CONFIG.theme.getPixmap("novelwriter", (nPx, nPx))) + self.nwIcon.setPixmap(SHARED.theme.getPixmap("novelwriter", (nPx, nPx))) self.leftBox = QVBoxLayout() self.leftBox.addWidget(self.nwIcon) diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index ad549298..7bfb5d95 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -33,8 +33,7 @@ from PyQt5.QtWidgets import ( QLineEdit, QListWidget, QListWidgetItem, QPushButton, QVBoxLayout ) -from novelwriter import CONFIG -from novelwriter.enum import nwAlert +from novelwriter import CONFIG, SHARED from novelwriter.core.spellcheck import UserDictionary if TYPE_CHECKING: # pragma: no cover @@ -52,12 +51,10 @@ class GuiWordList(QDialog): self.setObjectName("GuiWordList") self.setWindowTitle(self.tr("Project Word List")) - self.mainGui = mainGui - mS = CONFIG.pxInt(250) wW = CONFIG.pxInt(320) wH = CONFIG.pxInt(340) - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options self.setMinimumWidth(mS) self.setMinimumHeight(mS) @@ -77,10 +74,10 @@ class GuiWordList(QDialog): self.newEntry = QLineEdit() - self.addButton = QPushButton(CONFIG.theme.getIcon("add"), "") + self.addButton = QPushButton(SHARED.theme.getIcon("add"), "") self.addButton.clicked.connect(self._doAdd) - self.delButton = QPushButton(CONFIG.theme.getIcon("remove"), "") + self.delButton = QPushButton(SHARED.theme.getIcon("remove"), "") self.delButton.clicked.connect(self._doDelete) self.editBox = QHBoxLayout() @@ -123,15 +120,13 @@ class GuiWordList(QDialog): """Add a new word to the word list.""" word = self.newEntry.text().strip() if word == "": - self.mainGui.makeAlert(self.tr( - "Cannot add a blank word." - ), level=nwAlert.ERROR) + SHARED.error(self.tr("Cannot add a blank word.")) return if self.listBox.findItems(word, Qt.MatchExactly): - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "The word '{0}' is already in the word list." - ).format(word), level=nwAlert.ERROR) + ).format(word)) return self.listBox.addItem(word) @@ -149,7 +144,7 @@ class GuiWordList(QDialog): def _doSave(self): """Save the new word list and close.""" self._saveGuiSettings() - userDict = UserDictionary(self.mainGui.project) + userDict = UserDictionary(SHARED.project) for i in range(self.listBox.count()): item = self.listBox.item(i) if isinstance(item, QListWidgetItem): @@ -172,7 +167,7 @@ class GuiWordList(QDialog): def _loadWordList(self): """Load the project's word list, if it exists.""" - userDict = UserDictionary(self.mainGui.project) + userDict = UserDictionary(SHARED.project) userDict.load() self.listBox.clear() for word in userDict: @@ -185,7 +180,7 @@ class GuiWordList(QDialog): winWidth = CONFIG.rpxInt(self.width()) winHeight = CONFIG.rpxInt(self.height()) - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options pOptions.setValue("GuiWordList", "winWidth", winWidth) pOptions.setValue("GuiWordList", "winHeight", winHeight) diff --git a/novelwriter/enum.py b/novelwriter/enum.py index 1abc29c7..36546778 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -119,16 +119,6 @@ class nwDocInsert(Enum): # END Enum nwDocInsert -class nwAlert(Enum): - - INFO = 0 - WARN = 1 - ERROR = 2 - ASK = 3 - -# END Enum nwAlert - - class nwView(Enum): EDITOR = 0 diff --git a/novelwriter/extensions/novelselector.py b/novelwriter/extensions/novelselector.py index 8bb2c4c2..c22012fc 100644 --- a/novelwriter/extensions/novelselector.py +++ b/novelwriter/extensions/novelselector.py @@ -25,18 +25,13 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING - from PyQt5.QtCore import pyqtSignal, pyqtSlot from PyQt5.QtWidgets import QComboBox, QWidget -from novelwriter import CONFIG +from novelwriter import SHARED from novelwriter.enum import nwItemClass from novelwriter.constants import nwLabels -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) @@ -44,9 +39,8 @@ class NovelSelector(QComboBox): novelSelectionChanged = pyqtSignal(str) - def __init__(self, parent: QWidget, mainGui: GuiMain) -> None: + def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) - self._mainGui = mainGui self._blockSignal = False self._firstHandle = None self.currentIndexChanged.connect(self._indexChanged) @@ -86,9 +80,9 @@ class NovelSelector(QComboBox): self._firstHandle = None self.clear() - icon = CONFIG.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) + icon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) handle = self.currentData() - for tHandle, nwItem in self._mainGui.project.tree.iterRoots(nwItemClass.NOVEL): + for tHandle, nwItem in SHARED.project.tree.iterRoots(nwItemClass.NOVEL): if prefix: name = prefix.format(nwItem.itemName) self.addItem(name, tHandle) diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py index 876172e1..cbe7f260 100644 --- a/novelwriter/gui/doceditor.py +++ b/novelwriter/gui/doceditor.py @@ -49,8 +49,8 @@ from PyQt5.QtWidgets import ( QPushButton, QShortcut, QTextEdit, QToolBar, QToolButton, QWidget ) -from novelwriter import CONFIG -from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode, nwItemClass +from novelwriter import CONFIG, SHARED +from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass from novelwriter.common import minmax, transferCase from novelwriter.constants import nwConst, nwKeyWords, nwUnicode from novelwriter.core.index import countWords @@ -71,9 +71,10 @@ class GuiDocEditor(QTextEdit): ) # Custom Signals - spellDictionaryChanged = pyqtSignal(str, str) - docEditedStatusChanged = pyqtSignal(bool) + statusMessage = pyqtSignal(str) docCountsChanged = pyqtSignal(str, int, int, int) + editedStatusChanged = pyqtSignal(bool) + spellDictionaryChanged = pyqtSignal(str, str) loadDocumentTagRequest = pyqtSignal(str, Enum) novelStructureChanged = pyqtSignal() novelItemMetaChanged = pyqtSignal(str) @@ -101,7 +102,7 @@ class GuiDocEditor(QTextEdit): self._wordCount = 0 # Word count self._paraCount = 0 # Paragraph count self._lastEdit = 0 # Time stamp of last edit - self._lastActive = 0 # Time stamp of last activity + self._lastActive = 0.0 # Time stamp of last activity self._lastFind = None # Position of the last found search word self._bigDoc = False # Flag for very large document size self._doReplace = False # Switch to temporarily disable auto-replace @@ -132,8 +133,8 @@ class GuiDocEditor(QTextEdit): self.docSearch = GuiDocEditSearch(self) # Syntax - self.spEnchant = NWSpellEnchant(self.mainGui.project) - self.highLight = GuiDocHighlighter(qDoc, self.mainGui, self.spEnchant) + self.spEnchant = NWSpellEnchant(SHARED.project) + self.highLight = GuiDocHighlighter(qDoc, self.spEnchant) # Context Menu self.setContextMenuPolicy(Qt.CustomContextMenu) @@ -188,6 +189,34 @@ class GuiDocEditor(QTextEdit): return + ## + # Properties + ## + + @property + def docChanged(self) -> bool: + """Return the changed status of the document.""" + return self._docChanged + + @property + def docHandle(self) -> str | None: + """Return the handle of the currently open document.""" + return self._docHandle + + @property + def lastActive(self) -> float: + """Return the last active timestamp for the user.""" + return self._lastActive + + @property + def isEmpty(self) -> bool: + """Check if the current document is empty.""" + return self.document().isEmpty() + + ## + # Methods + ## + def clearEditor(self): """Clear the current document and reset all document-related flags and counters. @@ -203,7 +232,7 @@ class GuiDocEditor(QTextEdit): self._wordCount = 0 self._paraCount = 0 self._lastEdit = 0 - self._lastActive = 0 + self._lastActive = 0.0 self._lastFind = None self._bigDoc = False self._doReplace = False @@ -227,14 +256,14 @@ class GuiDocEditor(QTextEdit): """Update the syntax highlighting theme. """ mainPalette = self.palette() - mainPalette.setColor(QPalette.Window, QColor(*CONFIG.theme.colBack)) - mainPalette.setColor(QPalette.Base, QColor(*CONFIG.theme.colBack)) - mainPalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) + mainPalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) + mainPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack)) + mainPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) self.setPalette(mainPalette) docPalette = self.viewport().palette() - docPalette.setColor(QPalette.Base, QColor(*CONFIG.theme.colBack)) - docPalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) + docPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack)) + docPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) self.viewport().setPalette(docPalette) self.docHeader.matchColours() @@ -340,7 +369,7 @@ class GuiDocEditor(QTextEdit): document is new (empty string), we set up the editor for editing the file. """ - self._nwDocument = self.mainGui.project.storage.getDocument(tHandle) + self._nwDocument = SHARED.project.storage.getDocument(tHandle) self._nwItem = self._nwDocument.getCurrentItem() theDoc = self._nwDocument.readDocument() @@ -351,14 +380,14 @@ class GuiDocEditor(QTextEdit): docSize = len(theDoc) if docSize > nwConst.MAX_DOCSIZE: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "The document you are trying to open is too big. " "The document size is {0} MB. " "The maximum size allowed is {1} MB." ).format( f"{docSize/1.0e6:.2f}", f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" - ), level=nwAlert.ERROR) + )) self.clearEditor() return False @@ -426,9 +455,7 @@ class GuiDocEditor(QTextEdit): # Update the status bar if self._nwItem is not None: - self.mainGui.setStatus( - self.tr("Opened Document: {0}").format(self._nwItem.itemName) - ) + self.statusMessage.emit(self.tr("Opened Document: {0}").format(self._nwItem.itemName)) return True @@ -451,14 +478,14 @@ class GuiDocEditor(QTextEdit): """ docSize = len(theText) if docSize > nwConst.MAX_DOCSIZE: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "The text you are trying to add is too big. " "The text size is {0} MB. " "The maximum size allowed is {1} MB." ).format( f"{docSize/1.0e6:.2f}", f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" - ), level=nwAlert.ERROR) + )) return False qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) @@ -497,7 +524,7 @@ class GuiDocEditor(QTextEdit): if not self._nwDocument.writeDocument(docText): saveOk = False if self._nwDocument._currHash != self._nwDocument._prevHash: - msgYes = self.mainGui.askQuestion(self.tr( + msgYes = SHARED.question(self.tr( "This document has been changed outside of novelWriter " "while it was open. Overwrite the file on disk?" )) @@ -505,10 +532,9 @@ class GuiDocEditor(QTextEdit): saveOk = self._nwDocument.writeDocument(docText, forceWrite=True) if not saveOk: - self.mainGui.makeAlert( + SHARED.error( self.tr("Could not save document."), - info=self._nwDocument.getError(), - level=nwAlert.ERROR + info=self._nwDocument.getError() ) return False @@ -516,10 +542,10 @@ class GuiDocEditor(QTextEdit): self.setDocumentChanged(False) oldHeader = self._nwItem.mainHeading - oldCount = self.mainGui.project.index.getHandleHeaderCount(tHandle) - self.mainGui.project.index.scanText(tHandle, docText) + oldCount = SHARED.project.index.getHandleHeaderCount(tHandle) + SHARED.project.index.scanText(tHandle, docText) newHeader = self._nwItem.mainHeading - newCount = self.mainGui.project.index.getHandleHeaderCount(tHandle) + newCount = SHARED.project.index.getHandleHeaderCount(tHandle) if self._nwItem.itemClass == nwItemClass.NOVEL: if oldCount == newCount: @@ -534,9 +560,7 @@ class GuiDocEditor(QTextEdit): self.docFooter.updateInfo() # Update the status bar - self.mainGui.setStatus( - self.tr("Saved Document: {0}").format(self._nwItem.itemName) - ) + self.statusMessage.emit(self.tr("Saved Document: {0}").format(self._nwItem.itemName)) return True @@ -580,31 +604,6 @@ class GuiDocEditor(QTextEdit): return - ## - # Properties - ## - - def docChanged(self): - """Return the changed status of the document in the editor. - """ - return self._docChanged - - def docHandle(self): - """Return the handle of the currently open document. Return - None if no document is open. - """ - return self._docHandle - - def lastActive(self): - """Return the last active timestamp for the user. - """ - return self._lastActive - - def isEmpty(self): - """Wrapper function to check if the current document is empty. - """ - return self.document().isEmpty() - ## # Getters ## @@ -636,7 +635,7 @@ class GuiDocEditor(QTextEdit): document change signal. """ self._docChanged = bValue - self.docEditedStatusChanged.emit(self._docChanged) + self.editedStatusChanged.emit(self._docChanged) return self._docChanged def setCursorPosition(self, position): @@ -698,10 +697,10 @@ class GuiDocEditor(QTextEdit): """Set the spell checker dictionary language, and emit the dictionary changed signal. """ - if self.mainGui.project.data.spellLang is None: + if SHARED.project.data.spellLang is None: theLang = CONFIG.spellLanguage else: - theLang = self.mainGui.project.data.spellLang + theLang = SHARED.project.data.spellLang self.spEnchant.setLanguage(theLang) _, theProvider = self.spEnchant.describeDict() @@ -723,7 +722,7 @@ class GuiDocEditor(QTextEdit): if not CONFIG.hasEnchant: if theMode: - self.mainGui.makeAlert(self.tr( + SHARED.info(self.tr( "Spell checking requires the package PyEnchant. " "It does not appear to be installed." )) @@ -734,7 +733,7 @@ class GuiDocEditor(QTextEdit): self._spellCheck = theMode self.mainGui.mainMenu.setSpellCheck(theMode) - self.mainGui.project.data.setSpellCheck(theMode) + SHARED.project.data.setSpellCheck(theMode) self.highLight.setSpellCheck(theMode) if not self._bigDoc or theMode is False: # We don't run the spell checker automatically on big docs @@ -744,7 +743,7 @@ class GuiDocEditor(QTextEdit): return True - def spellCheckDocument(self): + def spellCheckDocument(self) -> None: """Rerun the highlighter to update spell checking status of the currently loaded text. The fastest way to do this, at least as of Qt 5.13, is to clear the text and put it back. This clears @@ -760,9 +759,8 @@ class GuiDocEditor(QTextEdit): self.highLight.rehighlight() qApp.restoreOverrideCursor() logger.debug("Document highlighted in %.3f ms", 1000*(time() - start)) - self.mainGui.mainStatus.setStatus(self.tr("Spell check complete")) - - return True + self.statusMessage.emit(self.tr("Spell check complete")) + return ## # General Class Methods @@ -868,7 +866,7 @@ class GuiDocEditor(QTextEdit): if self._nwDocument is None: logger.error("No document open") return False - self.mainGui.makeAlert( + SHARED.info( self.tr("The currently open file is saved in:"), info=self._nwDocument.getFileLocation() ) @@ -1104,12 +1102,12 @@ class GuiDocEditor(QTextEdit): self._lastFind = None if self.document().characterCount() > nwConst.MAX_DOCSIZE: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "The document has grown too big and you cannot add more text to it. " "The maximum size of a single novelWriter document is {0} MB." ).format( f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" - ), level=nwAlert.ERROR) + )) self.undo() return @@ -1666,9 +1664,7 @@ class GuiDocEditor(QTextEdit): """ theCursor = self.textCursor() if not theCursor.hasSelection(): - self.mainGui.makeAlert(self.tr( - "Please select some text before calling replace quotes." - ), level=nwAlert.ERROR) + SHARED.error(self.tr("Please select some text before calling replace quotes.")) return False posS = theCursor.selectionStart() @@ -1916,7 +1912,7 @@ class GuiDocEditor(QTextEdit): if theText.startswith("@"): - isGood, tBits, tPos = self.mainGui.project.index.scanThis(theText) + isGood, tBits, tPos = SHARED.project.index.scanThis(theText) if not isGood: return False @@ -2233,9 +2229,9 @@ class GuiDocEditSearch(QFrame): self.doMatchCap = CONFIG.searchMatchCap mPx = CONFIG.pxInt(6) - tPx = int(0.8*CONFIG.theme.fontPixelSize) - self.boxFont = CONFIG.theme.guiFont - self.boxFont.setPointSizeF(0.9*CONFIG.theme.fontPointSize) + tPx = int(0.8*SHARED.theme.fontPixelSize) + self.boxFont = SHARED.theme.guiFont + self.boxFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) self.setContentsMargins(0, 0, 0, 0) self.setAutoFillBackground(True) @@ -2268,7 +2264,7 @@ class GuiDocEditSearch(QFrame): self.resultLabel = QLabel("?/?") self.resultLabel.setFont(self.boxFont) - self.resultLabel.setMinimumWidth(CONFIG.theme.getTextWidth("?/?", self.boxFont)) + self.resultLabel.setMinimumWidth(SHARED.theme.getTextWidth("?/?", self.boxFont)) self.toggleCase = QAction(self.tr("Case Sensitive"), self) self.toggleCase.setCheckable(True) @@ -2374,15 +2370,15 @@ class GuiDocEditSearch(QFrame): self.replaceBox.setPalette(qPalette) # Set icons - self.toggleCase.setIcon(CONFIG.theme.getIcon("search_case")) - self.toggleWord.setIcon(CONFIG.theme.getIcon("search_word")) - self.toggleRegEx.setIcon(CONFIG.theme.getIcon("search_regex")) - self.toggleLoop.setIcon(CONFIG.theme.getIcon("search_loop")) - self.toggleProject.setIcon(CONFIG.theme.getIcon("search_project")) - self.toggleMatchCap.setIcon(CONFIG.theme.getIcon("search_preserve")) - self.cancelSearch.setIcon(CONFIG.theme.getIcon("search_cancel")) - self.searchButton.setIcon(CONFIG.theme.getIcon("search")) - self.replaceButton.setIcon(CONFIG.theme.getIcon("search_replace")) + self.toggleCase.setIcon(SHARED.theme.getIcon("search_case")) + self.toggleWord.setIcon(SHARED.theme.getIcon("search_word")) + self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex")) + self.toggleLoop.setIcon(SHARED.theme.getIcon("search_loop")) + self.toggleProject.setIcon(SHARED.theme.getIcon("search_project")) + self.toggleMatchCap.setIcon(SHARED.theme.getIcon("search_preserve")) + self.cancelSearch.setIcon(SHARED.theme.getIcon("search_cancel")) + self.searchButton.setIcon(SHARED.theme.getIcon("search")) + self.replaceButton.setIcon(SHARED.theme.getIcon("search_replace")) # Set stylesheets self.searchOpt.setStyleSheet("QToolBar {padding: 0;}") @@ -2474,7 +2470,7 @@ class GuiDocEditSearch(QFrame): """ currRes = "?" if currRes is None else currRes resCount = "?" if resCount is None else "1000+" if resCount > 1000 else resCount - minWidth = CONFIG.theme.getTextWidth(f"{resCount}//{resCount}", self.boxFont) + minWidth = SHARED.theme.getTextWidth(f"{resCount}//{resCount}", self.boxFont) self.resultLabel.setText(f"{currRes}/{resCount}") self.resultLabel.setMinimumWidth(minWidth) self.adjustSize() @@ -2639,7 +2635,7 @@ class GuiDocEditHeader(QWidget): self._docHandle = None - fPx = int(0.9*CONFIG.theme.fontPixelSize) + fPx = int(0.9*SHARED.theme.fontPixelSize) hSp = CONFIG.pxInt(6) # Main Widget Settings @@ -2656,7 +2652,7 @@ class GuiDocEditHeader(QWidget): self.theTitle.setFixedHeight(fPx) lblFont = self.theTitle.font() - lblFont.setPointSizeF(0.9*CONFIG.theme.fontPointSize) + lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) self.theTitle.setFont(lblFont) # Buttons @@ -2726,15 +2722,15 @@ class GuiDocEditHeader(QWidget): def updateTheme(self): """Update theme elements. """ - self.editButton.setIcon(CONFIG.theme.getIcon("edit")) - self.searchButton.setIcon(CONFIG.theme.getIcon("search")) - self.minmaxButton.setIcon(CONFIG.theme.getIcon("maximise")) - self.closeButton.setIcon(CONFIG.theme.getIcon("close")) + self.editButton.setIcon(SHARED.theme.getIcon("edit")) + self.searchButton.setIcon(SHARED.theme.getIcon("search")) + self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise")) + self.closeButton.setIcon(SHARED.theme.getIcon("close")) buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(*CONFIG.theme.colText) + ).format(*SHARED.theme.colText) self.editButton.setStyleSheet(buttonStyle) self.searchButton.setStyleSheet(buttonStyle) @@ -2750,9 +2746,9 @@ class GuiDocEditHeader(QWidget): theme rather than the main GUI. """ thePalette = QPalette() - thePalette.setColor(QPalette.Window, QColor(*CONFIG.theme.colBack)) - thePalette.setColor(QPalette.WindowText, QColor(*CONFIG.theme.colText)) - thePalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) + thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) + thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText)) + thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) self.setPalette(thePalette) self.theTitle.setPalette(thePalette) @@ -2772,7 +2768,7 @@ class GuiDocEditHeader(QWidget): self.minmaxButton.setVisible(False) return True - pTree = self.mainGui.project.tree + pTree = SHARED.project.tree if CONFIG.showFullPath: tTitle = [] tTree = pTree.getItemPath(tHandle) @@ -2801,9 +2797,9 @@ class GuiDocEditHeader(QWidget): toggleFocusMode function and should not be activated directly. """ if self.mainGui.isFocusMode: - self.minmaxButton.setIcon(CONFIG.theme.getIcon("minimise")) + self.minmaxButton.setIcon(SHARED.theme.getIcon("minimise")) else: - self.minmaxButton.setIcon(CONFIG.theme.getIcon("maximise")) + self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise")) return ## @@ -2876,13 +2872,13 @@ class GuiDocEditFooter(QWidget): self._docSelection = False - self.sPx = int(round(0.9*CONFIG.theme.baseIconSize)) - fPx = int(0.9*CONFIG.theme.fontPixelSize) + self.sPx = int(round(0.9*SHARED.theme.baseIconSize)) + fPx = int(0.9*SHARED.theme.fontPixelSize) bSp = CONFIG.pxInt(4) hSp = CONFIG.pxInt(6) lblFont = self.font() - lblFont.setPointSizeF(0.9*CONFIG.theme.fontPointSize) + lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) # Main Widget Settings self.setContentsMargins(0, 0, 0, 0) @@ -2969,8 +2965,8 @@ class GuiDocEditFooter(QWidget): def updateTheme(self): """Update theme elements. """ - self.linesIcon.setPixmap(CONFIG.theme.getPixmap("status_lines", (self.sPx, self.sPx))) - self.wordsIcon.setPixmap(CONFIG.theme.getPixmap("status_stats", (self.sPx, self.sPx))) + self.linesIcon.setPixmap(SHARED.theme.getPixmap("status_lines", (self.sPx, self.sPx))) + self.wordsIcon.setPixmap(SHARED.theme.getPixmap("status_stats", (self.sPx, self.sPx))) self.matchColours() @@ -2981,9 +2977,9 @@ class GuiDocEditFooter(QWidget): theme rather than the main GUI. """ thePalette = QPalette() - thePalette.setColor(QPalette.Window, QColor(*CONFIG.theme.colBack)) - thePalette.setColor(QPalette.WindowText, QColor(*CONFIG.theme.colText)) - thePalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) + thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) + thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText)) + thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) self.setPalette(thePalette) self.statusText.setPalette(thePalette) @@ -3000,7 +2996,7 @@ class GuiDocEditFooter(QWidget): logger.debug("No handle set, so clearing the editor footer") self._theItem = None else: - self._theItem = self.mainGui.project.tree[self._docHandle] + self._theItem = SHARED.project.tree[self._docHandle] self.setHasSelection(False) self.updateInfo() diff --git a/novelwriter/gui/dochighlight.py b/novelwriter/gui/dochighlight.py index 1032c886..9c9c1079 100644 --- a/novelwriter/gui/dochighlight.py +++ b/novelwriter/gui/dochighlight.py @@ -32,7 +32,7 @@ from PyQt5.QtGui import ( QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.common import checkInt from novelwriter.constants import nwRegEx, nwUnicode @@ -46,14 +46,13 @@ class GuiDocHighlighter(QSyntaxHighlighter): BLOCK_META = 2 BLOCK_TITLE = 4 - def __init__(self, theDoc, mainGui, spEnchant): + def __init__(self, theDoc, spEnchant): super().__init__(theDoc) logger.debug("Create: GuiDocHighlighter") self.theDoc = theDoc self.spEnchant = spEnchant - self.mainGui = mainGui self.theHandle = None self.spellCheck = False self.spellRx = None @@ -85,24 +84,24 @@ class GuiDocHighlighter(QSyntaxHighlighter): """ logger.debug("Setting up highlighting rules") - self.colHead = QColor(*CONFIG.theme.colHead) - self.colHeadH = QColor(*CONFIG.theme.colHeadH) - self.colDialN = QColor(*CONFIG.theme.colDialN) - self.colDialD = QColor(*CONFIG.theme.colDialD) - self.colDialS = QColor(*CONFIG.theme.colDialS) - self.colHidden = QColor(*CONFIG.theme.colHidden) - self.colKey = QColor(*CONFIG.theme.colKey) - self.colVal = QColor(*CONFIG.theme.colVal) - self.colSpell = QColor(*CONFIG.theme.colSpell) - self.colError = QColor(*CONFIG.theme.colError) - self.colRepTag = QColor(*CONFIG.theme.colRepTag) - self.colMod = QColor(*CONFIG.theme.colMod) - self.colBreak = QColor(*CONFIG.theme.colEmph) + self.colHead = QColor(*SHARED.theme.colHead) + self.colHeadH = QColor(*SHARED.theme.colHeadH) + self.colDialN = QColor(*SHARED.theme.colDialN) + self.colDialD = QColor(*SHARED.theme.colDialD) + self.colDialS = QColor(*SHARED.theme.colDialS) + self.colHidden = QColor(*SHARED.theme.colHidden) + self.colKey = QColor(*SHARED.theme.colKey) + self.colVal = QColor(*SHARED.theme.colVal) + self.colSpell = QColor(*SHARED.theme.colSpell) + self.colError = QColor(*SHARED.theme.colError) + self.colRepTag = QColor(*SHARED.theme.colRepTag) + self.colMod = QColor(*SHARED.theme.colMod) + self.colBreak = QColor(*SHARED.theme.colEmph) self.colBreak.setAlpha(64) self.colEmph = None if CONFIG.highlightEmph: - self.colEmph = QColor(*CONFIG.theme.colEmph) + self.colEmph = QColor(*SHARED.theme.colEmph) self.hStyles = { "header1": self._makeFormat(self.colHead, "bold", 1.8), @@ -285,8 +284,8 @@ class GuiDocHighlighter(QSyntaxHighlighter): if theText.startswith("@"): # Keywords and commands self.setCurrentBlockState(self.BLOCK_META) - pIndex = self.mainGui.project.index - tItem = self.mainGui.project.tree[self.theHandle] + pIndex = SHARED.project.index + tItem = SHARED.project.tree[self.theHandle] isValid, theBits, thePos = pIndex.scanThis(theText) isGood = pIndex.checkThese(theBits, tItem) if isValid: diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py index fdb5435f..bbfb4c64 100644 --- a/novelwriter/gui/docviewer.py +++ b/novelwriter/gui/docviewer.py @@ -30,22 +30,27 @@ from __future__ import annotations import logging from enum import Enum +from typing import TYPE_CHECKING -from PyQt5.QtCore import Qt, QUrl, QSize, pyqtSlot, pyqtSignal +from PyQt5.QtCore import pyqtSignal, pyqtSlot, QPoint, QSize, Qt, QUrl from PyQt5.QtGui import ( - QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor + QColor, QCursor, QFont, QIcon, QMouseEvent, QPalette, QResizeEvent, + QTextCursor, QTextOption ) from PyQt5.QtWidgets import ( - qApp, QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton, - QAction, QMenu, QFrame + QAction, qApp, QFrame, QHBoxLayout, QLabel, QMenu, QScrollArea, + QTextBrowser, QToolButton, QWidget ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemType, nwDocAction, nwDocMode from novelwriter.error import logException from novelwriter.constants import nwUnicode from novelwriter.core.tohtml import ToHtml +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) @@ -53,7 +58,7 @@ class GuiDocViewer(QTextBrowser): loadDocumentTagRequest = pyqtSignal(str, Enum) - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiDocViewer") @@ -90,25 +95,43 @@ class GuiDocViewer(QTextBrowser): return - def clearViewer(self): - """Clear the content of the document and reset key variables. - """ + ## + # Properties + ## + + @property + def docHandle(self) -> str | None: + """Return the handle of the currently open document.""" + return self._docHandle + + @property + def scrollPosition(self) -> int: + """Return the scrollbar position.""" + vBar = self.verticalScrollBar() + if vBar.isVisible(): + return vBar.value() + return 0 + + ## + # Methods + ## + + def clearViewer(self) -> None: + """Clear the content of the document and reset key variables.""" self.clear() self.setSearchPaths([""]) self._docHandle = None self.docHeader.setTitleFromHandle(self._docHandle) - return True + return - def updateTheme(self): - """Update theme elements. - """ + def updateTheme(self) -> None: + """Update theme elements.""" self.docHeader.updateTheme() self.docFooter.updateTheme() return - def initViewer(self): - """Set editor settings from main config. - """ + def initViewer(self) -> None: + """Set editor settings from main config.""" self._makeStyleSheet() # Set Font @@ -119,14 +142,14 @@ class GuiDocViewer(QTextBrowser): # Set the widget colours to match syntax theme mainPalette = self.palette() - mainPalette.setColor(QPalette.Window, QColor(*CONFIG.theme.colBack)) - mainPalette.setColor(QPalette.Base, QColor(*CONFIG.theme.colBack)) - mainPalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) + mainPalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) + mainPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack)) + mainPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) self.setPalette(mainPalette) docPalette = self.viewport().palette() - docPalette.setColor(QPalette.Base, QColor(*CONFIG.theme.colBack)) - docPalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) + docPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack)) + docPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) self.viewport().setPalette(docPalette) self.docHeader.matchColours() @@ -157,12 +180,11 @@ class GuiDocViewer(QTextBrowser): if self._docHandle is not None: self.reloadText() - return True + return - def loadText(self, tHandle, updateHistory=True): - """Load text into the viewer from an item handle. - """ - if not self.mainGui.project.tree.checkType(tHandle, nwItemType.FILE): + def loadText(self, tHandle: str, updateHistory: bool = True) -> bool: + """Load text into the viewer from an item handle.""" + if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE): logger.warning("Item not found") return False @@ -170,7 +192,7 @@ class GuiDocViewer(QTextBrowser): qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) sPos = self.verticalScrollBar().value() - aDoc = ToHtml(self.mainGui.project) + aDoc = ToHtml(SHARED.project) aDoc.setPreview(CONFIG.viewComments, CONFIG.viewSynopsis) aDoc.setLinkHeaders(True) @@ -210,7 +232,7 @@ class GuiDocViewer(QTextBrowser): self.verticalScrollBar().setValue(sPos) self._docHandle = tHandle - self.mainGui.project.data.setLastHandle(tHandle, "viewer") + SHARED.project.data.setLastHandle(tHandle, "viewer") self.docHeader.setTitleFromHandle(self._docHandle) self.updateDocMargins() @@ -224,23 +246,20 @@ class GuiDocViewer(QTextBrowser): return True - def reloadText(self): - """Reload the text in the current document. - """ - self.loadText(self._docHandle, updateHistory=False) + def reloadText(self) -> None: + """Reload the text in the current document.""" + if self._docHandle: + self.loadText(self._docHandle, updateHistory=False) return - def redrawText(self): - """Redraw the text by marking the document content as "dirty". - """ + def redrawText(self) -> None: + """Redraw the text by marking the content as "dirty".""" self.document().markContentsDirty(0, self.document().characterCount()) self.updateDocMargins() return - def docAction(self, theAction): - """Wrapper function for various document actions on the current - document. - """ + def docAction(self, theAction: nwDocAction) -> bool: + """Process document actions on the current document.""" logger.debug("Requesting action: '%s'", theAction.name) if self._docHandle is None: logger.error("No document open") @@ -258,9 +277,8 @@ class GuiDocViewer(QTextBrowser): return False return True - def navigateTo(self, tAnchor): - """Go to a specific #link in the document. - """ + def navigateTo(self, tAnchor: str) -> bool: + """Go to a specific #link in the document.""" if not isinstance(tAnchor, str): return False if tAnchor.startswith("#"): @@ -268,27 +286,13 @@ class GuiDocViewer(QTextBrowser): self.setSource(QUrl(tAnchor)) return True - def navBackward(self): - """Navigate backwards in the document view history. - """ - self.docHistory.backward() - return - - def navForward(self): - """Navigate forwards in the document view history. - """ - self.docHistory.forward() - return - - def clearNavHistory(self): - """Clear the navigation history. - """ + def clearNavHistory(self) -> None: + """Clear the navigation history.""" self.docHistory.clear() return - def updateDocMargins(self): - """Automatically adjust the margins so the text is centred. - """ + def updateDocMargins(self) -> None: + """Automatically adjust the margins so the text is centred.""" wW = self.width() wH = self.height() cM = CONFIG.getTextMargin() @@ -320,41 +324,20 @@ class GuiDocViewer(QTextBrowser): # Setters ## - def setScrollPosition(self, thePos): - """Set the scrollbar position. - """ + def setScrollPosition(self, pos: int) -> None: + """Set the scrollbar position.""" vBar = self.verticalScrollBar() if vBar.isVisible(): - vBar.setValue(thePos) + vBar.setValue(pos) return - ## - # Getters - ## - - def docHandle(self): - """Return the handle of the currently open document. Returns - None if no document is open. - """ - return self._docHandle - - def getScrollPosition(self): - """Get the scrollbar position. Returns 0 if no scrollbar. - """ - vBar = self.verticalScrollBar() - if vBar.isVisible(): - return vBar.value() - return 0 - ## # Public Slots ## @pyqtSlot(str) - def updateDocInfo(self, tHandle): - """Called when an item label is changed to check if the document - title bar needs updating, - """ + def updateDocInfo(self, tHandle: str) -> None: + """Update the header titlebar if needed.""" if tHandle == self._docHandle: self.docHeader.setTitleFromHandle(self._docHandle) self.updateDocMargins() @@ -364,11 +347,22 @@ class GuiDocViewer(QTextBrowser): # Private Slots ## + @pyqtSlot() + def navBackward(self) -> None: + """Navigate backwards in the document view history.""" + self.docHistory.backward() + return + + @pyqtSlot() + def navForward(self) -> None: + """Navigate forwards in the document view history.""" + self.docHistory.forward() + return + @pyqtSlot("QUrl") - def _linkClicked(self, theURL): - """Process a clicked link internally in the document. - """ - theLink = theURL.url() + def _linkClicked(self, url: QUrl) -> None: + """Process a clicked link internally in the document.""" + theLink = url.url() logger.debug("Clicked link: '%s'", theLink) if len(theLink) > 0: theBits = theLink.split("=") @@ -377,9 +371,8 @@ class GuiDocViewer(QTextBrowser): return @pyqtSlot("QPoint") - def _openContextMenu(self, thePos): - """Triggered by right click to open the context menu. - """ + def _openContextMenu(self, point: QPoint) -> None: + """Open context menu at location.""" userCursor = self.textCursor() userSelection = userCursor.hasSelection() @@ -404,18 +397,18 @@ class GuiDocViewer(QTextBrowser): mnuSelWord = QAction(self.tr("Select Word"), mnuContext) mnuSelWord.triggered.connect( - lambda: self._makePosSelection(QTextCursor.WordUnderCursor, thePos) + lambda: self._makePosSelection(QTextCursor.WordUnderCursor, point) ) mnuContext.addAction(mnuSelWord) mnuSelPara = QAction(self.tr("Select Paragraph"), mnuContext) mnuSelPara.triggered.connect( - lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, thePos) + lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, point) ) mnuContext.addAction(mnuSelPara) # Open the context menu - mnuContext.exec_(self.viewport().mapToGlobal(thePos)) + mnuContext.exec_(self.viewport().mapToGlobal(point)) return @@ -423,37 +416,33 @@ class GuiDocViewer(QTextBrowser): # Events ## - def resizeEvent(self, theEvent): - """If the text editor is resized, we must make sure the document - has its margins adjusted according to user preferences. - """ + def resizeEvent(self, event: QResizeEvent) -> None: + """Update document margins when widget is resized.""" self.updateDocMargins() - super().resizeEvent(theEvent) + super().resizeEvent(event) return - def mouseReleaseEvent(self, theEvent): - """Capture mouse click events on the document. - """ - if theEvent.button() == Qt.BackButton: + def mouseReleaseEvent(self, event: QMouseEvent) -> None: + """Capture mouse click events on the document.""" + if event.button() == Qt.BackButton: self.navBackward() - elif theEvent.button() == Qt.ForwardButton: + elif event.button() == Qt.ForwardButton: self.navForward() else: - super().mouseReleaseEvent(theEvent) + super().mouseReleaseEvent(event) return ## # Internal Functions ## - def _makeSelection(self, selMode): - """Wrapper function to select text based on a selection mode. - """ + def _makeSelection(self, selType: QTextCursor.SelectionType) -> None: + """Handle select of text based on a selection mode.""" theCursor = self.textCursor() theCursor.clearSelection() - theCursor.select(selMode) + theCursor.select(selType) - if selMode == QTextCursor.BlockUnderCursor: + if selType == QTextCursor.BlockUnderCursor: # This selection mode also selects the preceding paragraph # separator, which we want to avoid. posS = theCursor.selectionStart() @@ -467,19 +456,18 @@ class GuiDocViewer(QTextBrowser): return - def _makePosSelection(self, selMode, thePos): - """Wrapper function to select text based on selection mode, but - first move cursor to given position. - """ - theCursor = self.cursorForPosition(thePos) + def _makePosSelection(self, selType: QTextCursor.SelectionType, pos: QPoint) -> None: + """Handle text selection at a given location.""" + theCursor = self.cursorForPosition(pos) self.setTextCursor(theCursor) - self._makeSelection(selMode) + self._makeSelection(selType) return - def _makeStyleSheet(self): + def _makeStyleSheet(self) -> None: """Generate an appropriate style sheet for the document viewer, based on the current syntax highlighter theme, """ + pTheme = SHARED.theme styleSheet = ( "body {{" " color: rgb({tColR}, {tColG}, {tColB});" @@ -506,31 +494,31 @@ class GuiDocViewer(QTextBrowser): " text-align: center;" "}}\n" ).format( - tColR=CONFIG.theme.colText[0], - tColG=CONFIG.theme.colText[1], - tColB=CONFIG.theme.colText[2], - hColR=CONFIG.theme.colHead[0], - hColG=CONFIG.theme.colHead[1], - hColB=CONFIG.theme.colHead[2], - aColR=CONFIG.theme.colVal[0], - aColG=CONFIG.theme.colVal[1], - aColB=CONFIG.theme.colVal[2], - eColR=CONFIG.theme.colEmph[0], - eColG=CONFIG.theme.colEmph[1], - eColB=CONFIG.theme.colEmph[2], - kColR=CONFIG.theme.colKey[0], - kColG=CONFIG.theme.colKey[1], - kColB=CONFIG.theme.colKey[2], - cColR=CONFIG.theme.colHidden[0], - cColG=CONFIG.theme.colHidden[1], - cColB=CONFIG.theme.colHidden[2], - mColR=CONFIG.theme.colMod[0], - mColG=CONFIG.theme.colMod[1], - mColB=CONFIG.theme.colMod[2], + tColR=pTheme.colText[0], + tColG=pTheme.colText[1], + tColB=pTheme.colText[2], + hColR=pTheme.colHead[0], + hColG=pTheme.colHead[1], + hColB=pTheme.colHead[2], + aColR=pTheme.colVal[0], + aColG=pTheme.colVal[1], + aColB=pTheme.colVal[2], + eColR=pTheme.colEmph[0], + eColG=pTheme.colEmph[1], + eColB=pTheme.colEmph[2], + kColR=pTheme.colKey[0], + kColG=pTheme.colKey[1], + kColB=pTheme.colKey[2], + cColR=pTheme.colHidden[0], + cColG=pTheme.colHidden[1], + cColB=pTheme.colHidden[2], + mColR=pTheme.colMod[0], + mColG=pTheme.colMod[1], + mColB=pTheme.colMod[2], ) self.document().setDefaultStyleSheet(styleSheet) - return True + return # END Class GuiDocViewer @@ -628,7 +616,7 @@ class GuiDocViewHistory: """Update the scrollbar position of the previous entry. """ if self._prevPos >= 0 and self._prevPos < len(self._posHistory): - self._posHistory[self._prevPos] = self.docViewer.getScrollPosition() + self._posHistory[self._prevPos] = self.docViewer.scrollPosition return def _updateNavButtons(self): @@ -685,7 +673,7 @@ class GuiDocViewHeader(QWidget): # Internal Variables self._docHandle = None - fPx = int(0.9*CONFIG.theme.fontPixelSize) + fPx = int(0.9*SHARED.theme.fontPixelSize) hSp = CONFIG.pxInt(6) # Main Widget Settings @@ -702,7 +690,7 @@ class GuiDocViewHeader(QWidget): self.theTitle.setFixedHeight(fPx) lblFont = self.theTitle.font() - lblFont.setPointSizeF(0.9*CONFIG.theme.fontPointSize) + lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) self.theTitle.setFont(lblFont) # Buttons @@ -773,15 +761,15 @@ class GuiDocViewHeader(QWidget): def updateTheme(self): """Update theme elements. """ - self.backButton.setIcon(CONFIG.theme.getIcon("backward")) - self.forwardButton.setIcon(CONFIG.theme.getIcon("forward")) - self.refreshButton.setIcon(CONFIG.theme.getIcon("refresh")) - self.closeButton.setIcon(CONFIG.theme.getIcon("close")) + self.backButton.setIcon(SHARED.theme.getIcon("backward")) + self.forwardButton.setIcon(SHARED.theme.getIcon("forward")) + self.refreshButton.setIcon(SHARED.theme.getIcon("refresh")) + self.closeButton.setIcon(SHARED.theme.getIcon("close")) buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(*CONFIG.theme.colText) + ).format(*SHARED.theme.colText) self.backButton.setStyleSheet(buttonStyle) self.forwardButton.setStyleSheet(buttonStyle) @@ -797,9 +785,9 @@ class GuiDocViewHeader(QWidget): theme rather than the main GUI. """ thePalette = QPalette() - thePalette.setColor(QPalette.Window, QColor(*CONFIG.theme.colBack)) - thePalette.setColor(QPalette.WindowText, QColor(*CONFIG.theme.colText)) - thePalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) + thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) + thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText)) + thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) self.setPalette(thePalette) self.theTitle.setPalette(thePalette) @@ -819,7 +807,7 @@ class GuiDocViewHeader(QWidget): self.refreshButton.setVisible(False) return True - pTree = self.mainGui.project.tree + pTree = SHARED.project.tree if CONFIG.showFullPath: tTitle = [] tTree = pTree.getItemPath(tHandle) @@ -864,7 +852,7 @@ class GuiDocViewHeader(QWidget): def _refreshDocument(self): """Reload the content of the document. """ - if self.docViewer.docHandle() == self.mainGui.docEditor.docHandle(): + if self.docViewer.docHandle == self.mainGui.docEditor.docHandle: self.mainGui.saveDocument() self.docViewer.reloadText() return @@ -902,7 +890,7 @@ class GuiDocViewFooter(QWidget): # Internal Variables self._docHandle = None - fPx = int(0.9*CONFIG.theme.fontPixelSize) + fPx = int(0.9*SHARED.theme.fontPixelSize) bSp = CONFIG.pxInt(2) hSp = CONFIG.pxInt(8) @@ -987,7 +975,7 @@ class GuiDocViewFooter(QWidget): self.lblSynopsis.setAlignment(Qt.AlignLeft | Qt.AlignTop) lblFont = self.font() - lblFont.setPointSizeF(0.9*CONFIG.theme.fontPointSize) + lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) self.lblRefs.setFont(lblFont) self.lblSticky.setFont(lblFont) self.lblComments.setFont(lblFont) @@ -1032,21 +1020,21 @@ class GuiDocViewFooter(QWidget): """ # Icons - fPx = int(0.9*CONFIG.theme.fontPixelSize) + fPx = int(0.9*SHARED.theme.fontPixelSize) - stickyOn = CONFIG.theme.getPixmap("sticky-on", (fPx, fPx)) - stickyOff = CONFIG.theme.getPixmap("sticky-off", (fPx, fPx)) + stickyOn = SHARED.theme.getPixmap("sticky-on", (fPx, fPx)) + stickyOff = SHARED.theme.getPixmap("sticky-off", (fPx, fPx)) stickyIcon = QIcon() stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On) stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off) - bulletOn = CONFIG.theme.getPixmap("bullet-on", (fPx, fPx)) - bulletOff = CONFIG.theme.getPixmap("bullet-off", (fPx, fPx)) + bulletOn = SHARED.theme.getPixmap("bullet-on", (fPx, fPx)) + bulletOff = SHARED.theme.getPixmap("bullet-off", (fPx, fPx)) bulletIcon = QIcon() bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On) bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off) - self.showHide.setIcon(CONFIG.theme.getIcon("reference")) + self.showHide.setIcon(SHARED.theme.getIcon("reference")) self.stickyRefs.setIcon(stickyIcon) self.showComments.setIcon(bulletIcon) self.showSynopsis.setIcon(bulletIcon) @@ -1056,7 +1044,7 @@ class GuiDocViewFooter(QWidget): buttonStyle = ( "QToolButton {{border: none; background: transparent;}} " "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" - ).format(*CONFIG.theme.colText) + ).format(*SHARED.theme.colText) self.showHide.setStyleSheet(buttonStyle) self.stickyRefs.setStyleSheet(buttonStyle) @@ -1072,9 +1060,9 @@ class GuiDocViewFooter(QWidget): theme rather than the main GUI. """ thePalette = QPalette() - thePalette.setColor(QPalette.Window, QColor(*CONFIG.theme.colBack)) - thePalette.setColor(QPalette.WindowText, QColor(*CONFIG.theme.colText)) - thePalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) + thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack)) + thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText)) + thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText)) self.setPalette(thePalette) self.lblRefs.setPalette(thePalette) @@ -1102,8 +1090,8 @@ class GuiDocViewFooter(QWidget): """ logger.debug("Reference sticky is %s", str(theState)) self.docViewer.stickyRef = theState - if not theState and self.docViewer.docHandle() is not None: - self.viewMeta.refreshReferences(self.docViewer.docHandle()) + if not theState and self.docViewer.docHandle is not None: + self.viewMeta.refreshReferences(self.docViewer.docHandle) return @pyqtSlot(bool) @@ -1145,7 +1133,7 @@ class GuiDocViewDetails(QScrollArea): self.refList.setScaledContents(True) self.refList.linkActivated.connect(self._linkClicked) - self.linkStyle = "style='color: rgb({0},{1},{2})'".format(*CONFIG.theme.colLink) + self.linkStyle = "style='color: rgb({0},{1},{2})'".format(*SHARED.theme.colLink) # Assemble self.outerWidget = QWidget() @@ -1172,10 +1160,10 @@ class GuiDocViewDetails(QScrollArea): if self.mainGui.docViewer.stickyRef: return - theRefs = self.mainGui.project.index.getBackReferenceList(tHandle) + theRefs = SHARED.project.index.getBackReferenceList(tHandle) theList = [] for tHandle in theRefs: - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] if tItem is not None: theList.append("%s" % ( tHandle, theRefs[tHandle], self.linkStyle, tItem.itemName diff --git a/novelwriter/gui/itemdetails.py b/novelwriter/gui/itemdetails.py index df11ce2c..bf12ec50 100644 --- a/novelwriter/gui/itemdetails.py +++ b/novelwriter/gui/itemdetails.py @@ -25,25 +25,28 @@ from __future__ import annotations import logging +from typing import TYPE_CHECKING + +from PyQt5.QtGui import QFont from PyQt5.QtCore import Qt, pyqtSlot -from PyQt5.QtGui import QFont, QPixmap from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.constants import trConst, nwLabels +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) class GuiItemDetails(QWidget): - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiItemDetails") - self.mainGui = mainGui - # Internal Variables self._itemHandle = None @@ -51,7 +54,7 @@ class GuiItemDetails(QWidget): hSp = CONFIG.pxInt(6) vSp = CONFIG.pxInt(1) mPx = CONFIG.pxInt(6) - fPt = CONFIG.theme.fontPointSize + fPt = SHARED.theme.fontPointSize fntLabel = QFont() fntLabel.setBold(True) @@ -176,8 +179,8 @@ class GuiItemDetails(QWidget): self.updateTheme() # Make sure the columns for flags and counts don't resize too often - flagWidth = CONFIG.theme.getTextWidth("Mm", fntValue) - countWidth = CONFIG.theme.getTextWidth("99,999", fntValue) + flagWidth = SHARED.theme.getTextWidth("Mm", fntValue) + countWidth = SHARED.theme.getTextWidth("99,999", fntValue) self.mainBox.setColumnMinimumWidth(1, flagWidth) self.mainBox.setColumnMinimumWidth(4, countWidth) @@ -189,35 +192,28 @@ class GuiItemDetails(QWidget): # Class Methods ## - def clearDetails(self): - """Clear all the data values. - """ + def clearDetails(self) -> None: + """Clear all the data values.""" self._itemHandle = None - - self.labelIcon.setPixmap(QPixmap(1, 1)) - self.statusIcon.setPixmap(QPixmap(1, 1)) - self.classIcon.setText("") - self.usageIcon.setText("") - - self.labelData.setText("–") - self.statusData.setText("–") - self.classData.setText("–") - self.usageData.setText("–") - - self.cCountData.setText("–") - self.wCountData.setText("–") - self.pCountData.setText("–") - + self.labelIcon.clear() + self.labelData.clear() + self.statusIcon.clear() + self.statusData.clear() + self.classIcon.clear() + self.classData.clear() + self.usageIcon.clear() + self.usageData.clear() + self.cCountData.clear() + self.wCountData.clear() + self.pCountData.clear() return - def refreshDetails(self): - """Reload the content of the details panel. - """ + def refreshDetails(self) -> None: + """Reload the content of the details panel.""" self.updateViewBox(self._itemHandle) - def updateTheme(self): - """Update theme elements. - """ + def updateTheme(self) -> None: + """Update theme elements.""" self.updateViewBox(self._itemHandle) return @@ -226,20 +222,19 @@ class GuiItemDetails(QWidget): ## @pyqtSlot(str) - def updateViewBox(self, tHandle): - """Populate the details box from a given handle. - """ + def updateViewBox(self, tHandle: str) -> None: + """Populate the details box from a given handle.""" if tHandle is None: self.clearDetails() return - nwItem = self.mainGui.project.tree[tHandle] + nwItem = SHARED.project.tree[tHandle] if nwItem is None: self.clearDetails() return self._itemHandle = tHandle - iPx = int(round(0.8*CONFIG.theme.baseIconSize)) + iPx = int(round(0.8*SHARED.theme.baseIconSize)) # Label # ===== @@ -250,11 +245,11 @@ class GuiItemDetails(QWidget): if nwItem.isFileType(): if nwItem.isActive: - self.labelIcon.setPixmap(CONFIG.theme.getPixmap("checked", (iPx, iPx))) + self.labelIcon.setPixmap(SHARED.theme.getPixmap("checked", (iPx, iPx))) else: - self.labelIcon.setPixmap(CONFIG.theme.getPixmap("unchecked", (iPx, iPx))) + self.labelIcon.setPixmap(SHARED.theme.getPixmap("unchecked", (iPx, iPx))) else: - self.labelIcon.setPixmap(CONFIG.theme.getPixmap("noncheckable", (iPx, iPx))) + self.labelIcon.setPixmap(SHARED.theme.getPixmap("noncheckable", (iPx, iPx))) self.labelData.setText(theLabel) @@ -268,14 +263,14 @@ class GuiItemDetails(QWidget): # Class # ===== - classIcon = CONFIG.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]) + classIcon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]) self.classIcon.setPixmap(classIcon.pixmap(iPx, iPx)) self.classData.setText(trConst(nwLabels.CLASS_NAME[nwItem.itemClass])) # Layout # ====== - usageIcon = CONFIG.theme.getItemIcon( + usageIcon = SHARED.theme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading ) self.usageIcon.setPixmap(usageIcon.pixmap(iPx, iPx)) @@ -296,7 +291,7 @@ class GuiItemDetails(QWidget): return @pyqtSlot(str, int, int, int) - def updateCounts(self, tHandle, cC, wC, pC): + def updateCounts(self, tHandle: str, cC: int, wC: int, pC: int) -> None: """Update the counts if the handle is the same as the one we're already showing. Otherwise, do nothing. """ @@ -304,7 +299,6 @@ class GuiItemDetails(QWidget): self.cCountData.setText(f"{cC:n}") self.wCountData.setText(f"{wC:n}") self.pCountData.setText(f"{pC:n}") - return # END Class GuiItemDetails diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index dce2ca55..b12f655f 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -33,7 +33,7 @@ from PyQt5.QtCore import QUrl from PyQt5.QtGui import QDesktopServices from PyQt5.QtWidgets import QMenuBar, QAction -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode @@ -349,13 +349,13 @@ class GuiMainMenu(QMenuBar): # View > Go Backward self.aViewPrev = QAction(self.tr("Navigate Backward"), self) self.aViewPrev.setShortcut("Alt+Left") - self.aViewPrev.triggered.connect(lambda: self.mainGui.docViewer.navBackward()) + self.aViewPrev.triggered.connect(self.mainGui.docViewer.navBackward) self.viewMenu.addAction(self.aViewPrev) # View > Go Forward self.aViewNext = QAction(self.tr("Navigate Forward"), self) self.aViewNext.setShortcut("Alt+Right") - self.aViewNext.triggered.connect(lambda: self.mainGui.docViewer.navForward()) + self.aViewNext.triggered.connect(self.mainGui.docViewer.navForward) self.viewMenu.addAction(self.aViewNext) # View > Separator @@ -795,7 +795,7 @@ class GuiMainMenu(QMenuBar): # Tools > Check Spelling self.aSpellCheck = QAction(self.tr("Check Spelling"), self) self.aSpellCheck.setCheckable(True) - self.aSpellCheck.setChecked(self.mainGui.project.data.spellCheck) + self.aSpellCheck.setChecked(SHARED.project.data.spellCheck) self.aSpellCheck.triggered.connect(self._toggleSpellCheck) # triggered, not toggled! self.aSpellCheck.setShortcut("Ctrl+F7") self.toolsMenu.addAction(self.aSpellCheck) @@ -825,7 +825,7 @@ class GuiMainMenu(QMenuBar): # Tools > Backup Project self.aBackupProject = QAction(self.tr("Backup Project"), self) - self.aBackupProject.triggered.connect(lambda: self.mainGui.project.backupProject(True)) + self.aBackupProject.triggered.connect(lambda: SHARED.project.backupProject(True)) self.toolsMenu.addAction(self.aBackupProject) # Tools > Build Manuscript diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py index 98d36fe9..4d804c40 100644 --- a/novelwriter/gui/noveltree.py +++ b/novelwriter/gui/noveltree.py @@ -38,7 +38,7 @@ from PyQt5.QtWidgets import ( QTreeWidgetItem, QVBoxLayout, QWidget ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwDocMode, nwItemClass, nwOutline from novelwriter.common import minmax from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst @@ -106,7 +106,7 @@ class GuiNovelView(QWidget): self.novelTree.initSettings() return - def clearProject(self): + def clearNovelView(self): """Clear project-related GUI content. """ self.novelTree.clearContent() @@ -117,21 +117,20 @@ class GuiNovelView(QWidget): def openProjectTasks(self): """Run open project tasks. """ - lastNovel = self.mainGui.project.data.getLastHandle("novelTree") - if lastNovel not in self.mainGui.project.tree: - lastNovel = self.mainGui.project.tree.findRoot(nwItemClass.NOVEL) + lastNovel = SHARED.project.data.getLastHandle("novelTree") + if lastNovel not in SHARED.project.tree: + lastNovel = SHARED.project.tree.findRoot(nwItemClass.NOVEL) logger.debug("Setting novel tree to root item '%s'", lastNovel) - lastCol = self.mainGui.project.options.getEnum( + lastCol = SHARED.project.options.getEnum( "GuiNovelView", "lastCol", NovelTreeColumn, NovelTreeColumn.HIDDEN ) - lastColSize = self.mainGui.project.options.getInt( + lastColSize = SHARED.project.options.getInt( "GuiNovelView", "lastColSize", 25 ) - self.clearProject() - + self.clearNovelView() self.novelBar.buildNovelRootMenu() self.novelBar.setLastColType(lastCol, doRefresh=False) self.novelBar.setCurrentRoot(lastNovel) @@ -142,13 +141,13 @@ class GuiNovelView(QWidget): return def closeProjectTasks(self): - """Run closing project tasks. - """ + """Run closing project tasks.""" lastColType = self.novelTree.lastColType lastColSize = self.novelTree.lastColSize - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options pOptions.setValue("GuiNovelView", "lastCol", lastColType) pOptions.setValue("GuiNovelView", "lastColSize", lastColSize) + self.clearNovelView() return def setTreeFocus(self): @@ -170,7 +169,7 @@ class GuiNovelView(QWidget): def refreshTree(self): """Refresh the current tree. """ - self.novelTree.refreshTree(rootHandle=self.mainGui.project.data.getLastHandle("novelTree")) + self.novelTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("novelTree")) return @pyqtSlot(str) @@ -201,7 +200,7 @@ class GuiNovelToolBar(QWidget): self.novelView = novelView self.mainGui = novelView.mainGui - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize mPx = CONFIG.pxInt(2) self.setContentsMargins(0, 0, 0, 0) @@ -211,7 +210,7 @@ class GuiNovelToolBar(QWidget): selFont = self.font() selFont.setWeight(QFont.Bold) self.novelPrefix = self.tr("Outline of {0}") - self.novelValue = NovelSelector(self, self.mainGui) + self.novelValue = NovelSelector(self) self.novelValue.setFont(selFont) self.novelValue.setMinimumWidth(CONFIG.pxInt(150)) self.novelValue.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) @@ -274,9 +273,9 @@ class GuiNovelToolBar(QWidget): """Update theme elements. """ # Icons - self.tbNovel.setIcon(CONFIG.theme.getIcon("cls_novel")) - self.tbRefresh.setIcon(CONFIG.theme.getIcon("refresh")) - self.tbMore.setIcon(CONFIG.theme.getIcon("menu")) + self.tbNovel.setIcon(SHARED.theme.getIcon("cls_novel")) + self.tbRefresh.setIcon(SHARED.theme.getIcon("refresh")) + self.tbMore.setIcon(SHARED.theme.getIcon("menu")) qPalette = self.palette() qPalette.setBrush(QPalette.Window, qPalette.base()) @@ -345,7 +344,7 @@ class GuiNovelToolBar(QWidget): def _refreshNovelTree(self): """Rebuild the current tree. """ - rootHandle = self.mainGui.project.data.getLastHandle("novelTree") + rootHandle = SHARED.project.data.getLastHandle("novelTree") self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True) return @@ -415,7 +414,7 @@ class GuiNovelTree(QTreeWidget): # Build GUI # ========= - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize cMg = CONFIG.pxInt(6) self.setIconSize(QSize(iPx, iPx)) @@ -481,8 +480,8 @@ class GuiNovelTree(QTreeWidget): def updateTheme(self): """Update theme elements. """ - iPx = CONFIG.theme.baseIconSize - self._pMore = CONFIG.theme.loadDecoration("deco_doc_more", pxH=iPx) + iPx = SHARED.theme.baseIconSize + self._pMore = SHARED.theme.loadDecoration("deco_doc_more", pxH=iPx) return ## @@ -514,10 +513,10 @@ class GuiNovelTree(QTreeWidget): """ logger.debug("Requesting refresh of the novel tree") if rootHandle is None: - rootHandle = self.mainGui.project.tree.findRoot(nwItemClass.NOVEL) + rootHandle = SHARED.project.tree.findRoot(nwItemClass.NOVEL) treeChanged = self.mainGui.projView.changedSince(self._lastBuild) - indexChanged = self.mainGui.project.index.rootChangedSince(rootHandle, self._lastBuild) + indexChanged = SHARED.project.index.rootChangedSince(rootHandle, self._lastBuild) if not (treeChanged or indexChanged or overRide): logger.debug("No changes have been made to the novel index") return @@ -528,7 +527,7 @@ class GuiNovelTree(QTreeWidget): titleKey = selItem[0].data(self.C_DATA, self.D_KEY) self._populateTree(rootHandle) - self.mainGui.project.data.setLastHandle(rootHandle, "novelTree") + SHARED.project.data.setLastHandle(rootHandle, "novelTree") if titleKey is not None and titleKey in self._treeMap: self._treeMap[titleKey].setSelected(True) @@ -538,7 +537,7 @@ class GuiNovelTree(QTreeWidget): def refreshHandle(self, tHandle): """Refresh the data for a given handle. """ - idxData = self.mainGui.project.index.getItemData(tHandle) + idxData = SHARED.project.index.getItemData(tHandle) if idxData is None: return @@ -575,7 +574,7 @@ class GuiNovelTree(QTreeWidget): self._lastCol = colType self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN) if doRefresh: - lastNovel = self.mainGui.project.data.getLastHandle("novelTree") + lastNovel = SHARED.project.data.getLastHandle("novelTree") self.refreshTree(rootHandle=lastNovel, overRide=True) return @@ -707,7 +706,7 @@ class GuiNovelTree(QTreeWidget): tStart = time() logger.debug("Building novel tree for root item '%s'", rootHandle) - novStruct = self.mainGui.project.index.novelStructure(rootHandle=rootHandle, skipExcl=True) + novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, skipExcl=True) for tKey, tHandle, sTitle, novIdx in novStruct: if novIdx.level == "H0": continue @@ -733,7 +732,7 @@ class GuiNovelTree(QTreeWidget): """Set the tree item values from the index entry. """ iLevel = nwHeaders.H_LEVEL.get(idxItem.level, 0) - hDec = CONFIG.theme.getHeaderDecoration(iLevel) + hDec = SHARED.theme.getHeaderDecoration(iLevel) trItem.setData(self.C_TITLE, Qt.DecorationRole, hDec) trItem.setText(self.C_TITLE, idxItem.title) @@ -759,7 +758,7 @@ class GuiNovelTree(QTreeWidget): refData = [] refName = "" - theRefs = self.mainGui.project.index.getReferences(tHandle, sTitle) + theRefs = SHARED.project.index.getReferences(tHandle, sTitle) if self._lastCol == NovelTreeColumn.POV: refData = theRefs[nwKeyWords.POV_KEY] refName = self._povLabel @@ -783,7 +782,7 @@ class GuiNovelTree(QTreeWidget): """ logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle) - pIndex = self.mainGui.project.index + pIndex = SHARED.project.index novIdx = pIndex.getItemHeader(tHandle, sTitle) refTags = pIndex.getReferences(tHandle, sTitle) diff --git a/novelwriter/gui/outline.py b/novelwriter/gui/outline.py index 73440f1f..4fd27176 100644 --- a/novelwriter/gui/outline.py +++ b/novelwriter/gui/outline.py @@ -41,7 +41,7 @@ from PyQt5.QtWidgets import ( QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import ( nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline ) @@ -62,8 +62,6 @@ class GuiOutlineView(QWidget): def __init__(self, mainGui): super().__init__(parent=mainGui) - self.mainGui = mainGui - # Build GUI self.outlineTree = GuiOutlineTree(self) self.outlineData = GuiOutlineDetails(self) @@ -117,10 +115,10 @@ class GuiOutlineView(QWidget): def refreshTree(self): """Refresh the current tree. """ - self.outlineTree.refreshTree(rootHandle=self.mainGui.project.data.getLastHandle("outline")) + self.outlineTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("outline")) return - def clearProject(self): + def clearOutline(self): """Clear project-related GUI content. """ self.outlineData.clearDetails() @@ -130,13 +128,13 @@ class GuiOutlineView(QWidget): def openProjectTasks(self): """Run open project tasks. """ - lastOutline = self.mainGui.project.data.getLastHandle("outline") - if not (lastOutline in self.mainGui.project.tree or lastOutline is None): - lastOutline = self.mainGui.project.tree.findRoot(nwItemClass.NOVEL) + lastOutline = SHARED.project.data.getLastHandle("outline") + if not (lastOutline in SHARED.project.tree or lastOutline is None): + lastOutline = SHARED.project.tree.findRoot(nwItemClass.NOVEL) logger.debug("Setting outline tree to root item '%s'", lastOutline) - self.clearProject() + self.clearOutline() self.outlineBar.populateNovelList() self.outlineBar.setCurrentRoot(lastOutline) self.outlineBar.setEnabled(True) @@ -146,6 +144,7 @@ class GuiOutlineView(QWidget): def closeProjectTasks(self): self.outlineTree.closeProjectTasks() self.outlineData.updateClasses() + self.clearOutline() return def splitSizes(self): @@ -214,8 +213,6 @@ class GuiOutlineToolBar(QToolBar): logger.debug("Create: GuiOutlineToolBar") - self.mainGui = theOutline.mainGui - iPx = CONFIG.pxInt(22) mPx = CONFIG.pxInt(12) @@ -230,7 +227,7 @@ class GuiOutlineToolBar(QToolBar): self.novelLabel = QLabel(self.tr("Outline of")) self.novelLabel.setContentsMargins(0, 0, mPx, 0) - self.novelValue = NovelSelector(self, self.mainGui) + self.novelValue = NovelSelector(self) self.novelValue.setMinimumWidth(CONFIG.pxInt(200)) self.novelValue.novelSelectionChanged.connect(self._novelValueChanged) @@ -272,8 +269,8 @@ class GuiOutlineToolBar(QToolBar): self.setStyleSheet("QToolBar {border: 0px;}") self.novelValue.updateList(includeAll=True) - self.aRefresh.setIcon(CONFIG.theme.getIcon("refresh")) - self.tbColumns.setIcon(CONFIG.theme.getIcon("menu")) + self.aRefresh.setIcon(SHARED.theme.getIcon("refresh")) + self.tbColumns.setIcon(SHARED.theme.getIcon("menu")) return @@ -370,7 +367,6 @@ class GuiOutlineTree(QTreeWidget): logger.debug("Create: GuiOutlineTree") self.outlineView = outlineView - self.mainGui = outlineView.mainGui self.setUniformRowHeights(True) self.setFrameStyle(QFrame.NoFrame) @@ -381,7 +377,7 @@ class GuiOutlineTree(QTreeWidget): self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemSelectionChanged.connect(self._itemSelected) - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize self.setIconSize(QSize(iPx, iPx)) self.setIndentation(0) @@ -398,11 +394,11 @@ class GuiOutlineTree(QTreeWidget): self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()] self._dIcon = { - "H0": CONFIG.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H0"), - "H1": CONFIG.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H1"), - "H2": CONFIG.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H2"), - "H3": CONFIG.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H3"), - "H4": CONFIG.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H4"), + "H0": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H0"), + "H1": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H1"), + "H2": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H2"), + "H3": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H3"), + "H4": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H4"), } # Internals @@ -488,13 +484,13 @@ class GuiOutlineTree(QTreeWidget): # If the novel index or novel tree has changed since the tree # was last built, we rebuild the tree from the updated index. - indexChanged = self.mainGui.project.index.rootChangedSince(rootHandle, self._lastBuild) + indexChanged = SHARED.project.index.rootChangedSince(rootHandle, self._lastBuild) if not (novelChanged or indexChanged or overRide): logger.debug("No changes have been made to the novel index") return self._populateTree(rootHandle) - self.mainGui.project.data.setLastHandle(rootHandle or None, "outline") + SHARED.project.data.setLastHandle(rootHandle or None, "outline") return @@ -574,7 +570,7 @@ class GuiOutlineTree(QTreeWidget): """ # Load whatever we saved last time, regardless of wether it # contains the correct names or number of columns. - colState = self.mainGui.project.options.getValue("GuiOutline", "columnState", {}) + colState = SHARED.project.options.getValue("GuiOutline", "columnState", {}) tmpOrder = [] tmpHidden = {} @@ -625,7 +621,7 @@ class GuiOutlineTree(QTreeWidget): logHidden, orgWidth if logHidden and logWidth == 0 else logWidth ] - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options pOptions.setValue("GuiOutline", "columnState", colState) pOptions.saveSettings() @@ -661,7 +657,7 @@ class GuiOutlineTree(QTreeWidget): headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight) headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight) - novStruct = self.mainGui.project.index.novelStructure(rootHandle=rootHandle, skipExcl=True) + novStruct = SHARED.project.index.novelStructure(rootHandle=rootHandle, skipExcl=True) for _, tHandle, sTitle, novIdx in novStruct: iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0) @@ -669,8 +665,8 @@ class GuiOutlineTree(QTreeWidget): continue trItem = QTreeWidgetItem() - nwItem = self.mainGui.project.tree[tHandle] - hDec = CONFIG.theme.getHeaderDecoration(iLevel) + nwItem = SHARED.project.tree[tHandle] + hDec = SHARED.theme.getHeaderDecoration(iLevel) trItem.setData(self._colIdx[nwOutline.TITLE], Qt.DecorationRole, hDec) trItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title) @@ -689,7 +685,7 @@ class GuiOutlineTree(QTreeWidget): trItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight) trItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], Qt.AlignRight) - refs = self.mainGui.project.index.getReferences(tHandle, sTitle) + refs = SHARED.project.index.getReferences(tHandle, sTitle) trItem.setText(self._colIdx[nwOutline.POV], ", ".join(refs[nwKeyWords.POV_KEY])) trItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(refs[nwKeyWords.FOCUS_KEY])) trItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(refs[nwKeyWords.CHAR_KEY])) @@ -770,12 +766,11 @@ class GuiOutlineDetails(QScrollArea): logger.debug("Create: GuiOutlineDetails") self.theOutline = theOutline - self.mainGui = theOutline.mainGui # Sizes - minTitle = 30*CONFIG.theme.textNWidth - maxTitle = 40*CONFIG.theme.textNWidth - wCount = CONFIG.theme.getTextWidth("999,999") + minTitle = 30*SHARED.theme.textNWidth + maxTitle = 40*SHARED.theme.textNWidth + wCount = SHARED.theme.getTextWidth("999,999") hSpace = int(CONFIG.pxInt(10)) vSpace = int(CONFIG.pxInt(4)) @@ -1005,8 +1000,8 @@ class GuiOutlineDetails(QScrollArea): """Update the content of the tree with the given handle and line number pointing to a header. """ - pIndex = self.mainGui.project.index - nwItem = self.mainGui.project.tree[tHandle] + pIndex = SHARED.project.index + nwItem = SHARED.project.tree[tHandle] novIdx = pIndex.getItemHeader(tHandle, sTitle) theRefs = pIndex.getReferences(tHandle, sTitle) if nwItem is None or novIdx is None: @@ -1049,7 +1044,7 @@ class GuiOutlineDetails(QScrollArea): def updateClasses(self): """Update the visibility status of class details. """ - usedClasses = self.mainGui.project.tree.rootClasses() + usedClasses = SHARED.project.tree.rootClasses() pltVisible = nwItemClass.PLOT in usedClasses timVisible = nwItemClass.TIMELINE in usedClasses diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index a4dedd46..1905bdec 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -39,7 +39,7 @@ from PyQt5.QtWidgets import ( QVBoxLayout, QWidget ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.common import minmax from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels from novelwriter.core.item import NWItem @@ -49,7 +49,7 @@ from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.projsettings import GuiProjectSettings from novelwriter.enum import ( - nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwWidget + nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwWidget ) if TYPE_CHECKING: # pragma: no cover @@ -165,7 +165,7 @@ class GuiProjectView(QWidget): self.projTree.initSettings() return - def clearProject(self) -> None: + def clearProjectView(self) -> None: """Clear project-related GUI content.""" self.projBar.clearContent() self.projBar.setEnabled(False) @@ -236,7 +236,7 @@ class GuiProjectToolBar(QWidget): self.projTree = projView.projTree self.mainGui = projView.mainGui - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize mPx = CONFIG.pxInt(2) self.setContentsMargins(0, 0, 0, 0) @@ -367,16 +367,16 @@ class GuiProjectToolBar(QWidget): self.tbAdd.setStyleSheet(buttonStyle) self.tbMore.setStyleSheet(buttonStyle) - self.tbQuick.setIcon(CONFIG.theme.getIcon("bookmark")) - self.tbMoveU.setIcon(CONFIG.theme.getIcon("up")) - self.tbMoveD.setIcon(CONFIG.theme.getIcon("down")) - self.aAddEmpty.setIcon(CONFIG.theme.getIcon("proj_document")) - self.aAddChap.setIcon(CONFIG.theme.getIcon("proj_chapter")) - self.aAddScene.setIcon(CONFIG.theme.getIcon("proj_scene")) - self.aAddNote.setIcon(CONFIG.theme.getIcon("proj_note")) - self.aAddFolder.setIcon(CONFIG.theme.getIcon("proj_folder")) - self.tbAdd.setIcon(CONFIG.theme.getIcon("add")) - self.tbMore.setIcon(CONFIG.theme.getIcon("menu")) + self.tbQuick.setIcon(SHARED.theme.getIcon("bookmark")) + self.tbMoveU.setIcon(SHARED.theme.getIcon("up")) + self.tbMoveD.setIcon(SHARED.theme.getIcon("down")) + self.aAddEmpty.setIcon(SHARED.theme.getIcon("proj_document")) + self.aAddChap.setIcon(SHARED.theme.getIcon("proj_chapter")) + self.aAddScene.setIcon(SHARED.theme.getIcon("proj_scene")) + self.aAddNote.setIcon(SHARED.theme.getIcon("proj_note")) + self.aAddFolder.setIcon(SHARED.theme.getIcon("proj_folder")) + self.tbAdd.setIcon(SHARED.theme.getIcon("add")) + self.tbMore.setIcon(SHARED.theme.getIcon("menu")) self.buildQuickLinkMenu() self._buildRootMenu() @@ -392,10 +392,10 @@ class GuiProjectToolBar(QWidget): """Build the quick link menu.""" logger.debug("Rebuilding quick links menu") self.mQuick.clear() - for n, (tHandle, nwItem) in enumerate(self.mainGui.project.tree.iterRoots(None)): + for n, (tHandle, nwItem) in enumerate(SHARED.project.tree.iterRoots(None)): aRoot = self.mQuick.addAction(nwItem.itemName) aRoot.setData(tHandle) - aRoot.setIcon(CONFIG.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass])) + aRoot.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass])) aRoot.triggered.connect( lambda n, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True) ) @@ -409,7 +409,7 @@ class GuiProjectToolBar(QWidget): """Build the rood folder menu.""" def addClass(itemClass): aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass])) - aNew.setIcon(CONFIG.theme.getIcon(nwLabels.CLASS_ICON[itemClass])) + aNew.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass])) aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass)) self.mAddRoot.addAction(aNew) return @@ -438,7 +438,7 @@ class GuiProjectToolBar(QWidget): documents. They should only be visible if novel documents can actually be added. """ - nwItem = self.mainGui.project.tree[tHandle] + nwItem = SHARED.project.tree[tHandle] allowDoc = isinstance(nwItem, NWItem) and nwItem.documentAllowed() self.aAddEmpty.setVisible(allowDoc) self.aAddChap.setVisible(allowDoc) @@ -480,7 +480,7 @@ class GuiProjectTree(QTreeWidget): self.customContextMenuRequested.connect(self._openContextMenu) # Tree Settings - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize cMg = CONFIG.pxInt(6) self.setIconSize(QSize(iPx, iPx)) @@ -563,7 +563,7 @@ class GuiProjectTree(QTreeWidget): make sure the item is added in a place it can be added, and that other meta data is set correctly to ensure a valid project tree. """ - if not self.mainGui.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -572,19 +572,17 @@ class GuiProjectTree(QTreeWidget): if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass): - tHandle = self.mainGui.project.newRoot(itemClass) + tHandle = SHARED.project.newRoot(itemClass) sHandle = self.getSelectedHandle() - pItem = self.mainGui.project.tree[sHandle] if sHandle else None + pItem = SHARED.project.tree[sHandle] if sHandle else None nHandle = pItem.itemRoot if pItem else None elif itemType in (nwItemType.FILE, nwItemType.FOLDER): sHandle = self.getSelectedHandle() - pItem = self.mainGui.project.tree[sHandle] if sHandle else None + pItem = SHARED.project.tree[sHandle] if sHandle else None if sHandle is None or pItem is None: - self.mainGui.makeAlert(self.tr( - "Did not find anywhere to add the file or folder!" - ), level=nwAlert.ERROR) + SHARED.error(self.tr("Did not find anywhere to add the file or folder!")) return False # Collect some information about the selected item @@ -592,10 +590,8 @@ class GuiProjectTree(QTreeWidget): sLevel = nwHeaders.H_LEVEL.get(pItem.mainHeading, 0) sIsParent = False if qItem is None else qItem.childCount() > 0 - if self.mainGui.project.tree.isTrash(sHandle): - self.mainGui.makeAlert(self.tr( - "Cannot add new files or folders to the Trash folder." - ), level=nwAlert.ERROR) + if SHARED.project.tree.isTrash(sHandle): + SHARED.error(self.tr("Cannot add new files or folders to the Trash folder.")) return False # Set default label and determine if new item is to be added @@ -635,9 +631,9 @@ class GuiProjectTree(QTreeWidget): # Add the file or folder if itemType == nwItemType.FILE: - tHandle = self.mainGui.project.newFile(newLabel, sHandle) + tHandle = SHARED.project.newFile(newLabel, sHandle) else: - tHandle = self.mainGui.project.newFolder(newLabel, sHandle) + tHandle = SHARED.project.newFolder(newLabel, sHandle) else: logger.error("Failed to add new item") @@ -650,7 +646,7 @@ class GuiProjectTree(QTreeWidget): # Handle new file creation if itemType == nwItemType.FILE and hLevel > 0: - self.mainGui.project.writeNewFile(tHandle, hLevel, not isNote) + SHARED.project.writeNewFile(tHandle, hLevel, not isNote) # Add the new item to the project tree self.revealNewTreeItem(tHandle, nHandle=nHandle, wordCount=True) @@ -661,7 +657,7 @@ class GuiProjectTree(QTreeWidget): def revealNewTreeItem(self, tHandle: str | None, nHandle: str | None = None, wordCount: bool = False) -> bool: """Reveal a newly added project item in the project tree.""" - nwItem = self.mainGui.project.tree[tHandle] if tHandle else None + nwItem = SHARED.project.tree[tHandle] if tHandle else None if tHandle is None or nwItem is None: return False @@ -670,7 +666,7 @@ class GuiProjectTree(QTreeWidget): return False if nwItem.isFileType() and wordCount: - wC = self.mainGui.project.index.getCounts(tHandle)[1] + wC = SHARED.project.index.getCounts(tHandle)[1] self.propagateCount(tHandle, wC) self.projView.wordCountsChanged.emit() @@ -745,7 +741,7 @@ class GuiProjectTree(QTreeWidget): def renameTreeItem(self, tHandle: str) -> bool: """Open a dialog to edit the label of an item.""" - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] if tItem is None: return False @@ -769,7 +765,7 @@ class GuiProjectTree(QTreeWidget): if isinstance(item, QTreeWidgetItem): theList = self._scanChildren(theList, item, i) logger.debug("Saving project tree item order") - self.mainGui.project.setTreeOrder(theList) + SHARED.project.setTreeOrder(theList) return def getTreeFromHandle(self, tHandle: str) -> list[str]: @@ -787,7 +783,7 @@ class GuiProjectTree(QTreeWidget): can be called on any item, and will check whether to attempt a permanent deletion or moving the item to Trash. """ - if not self.mainGui.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -802,16 +798,16 @@ class GuiProjectTree(QTreeWidget): logger.error("There is no item to delete") return False - trashHandle = self.mainGui.project.tree.trashRoot() + trashHandle = SHARED.project.tree.trashRoot() if tHandle == trashHandle: logger.error("Cannot delete the Trash folder") return False - nwItem = self.mainGui.project.tree[tHandle] + nwItem = SHARED.project.tree[tHandle] if nwItem is None: return False - if self.mainGui.project.tree.isTrash(tHandle) or nwItem.isRootType(): + if SHARED.project.tree.isTrash(tHandle) or nwItem.isRootType(): status = self.permDeleteItem(tHandle) else: status = self.moveItemToTrash(tHandle) @@ -823,17 +819,15 @@ class GuiProjectTree(QTreeWidget): function only asks for confirmation once, and calls the regular deleteItem function for each document in the Trash folder. """ - if not self.mainGui.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False - trashHandle = self.mainGui.project.tree.trashRoot() + trashHandle = SHARED.project.tree.trashRoot() logger.debug("Emptying Trash folder") if trashHandle is None: - self.mainGui.makeAlert(self.tr( - "There is currently no Trash folder in this project." - )) + SHARED.info(self.tr("There is currently no Trash folder in this project.")) return False theTrash = self.getTreeFromHandle(trashHandle) @@ -842,12 +836,10 @@ class GuiProjectTree(QTreeWidget): nTrash = len(theTrash) if nTrash == 0: - self.mainGui.makeAlert(self.tr( - "The Trash folder is already empty." - )) + SHARED.info(self.tr("The Trash folder is already empty.")) return False - msgYes = self.mainGui.askQuestion( + msgYes = SHARED.question( self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash) ) if not msgYes: @@ -870,13 +862,13 @@ class GuiProjectTree(QTreeWidget): so such a request is cancelled. """ trItemS = self._getTreeItem(tHandle) - nwItemS = self.mainGui.project.tree[tHandle] + nwItemS = SHARED.project.tree[tHandle] if trItemS is None or nwItemS is None: logger.error("Could not find tree item for deletion") return False - if self.mainGui.project.tree.isTrash(tHandle): + if SHARED.project.tree.isTrash(tHandle): logger.error("Item is already in the Trash folder") return False @@ -893,8 +885,8 @@ class GuiProjectTree(QTreeWidget): return False if askFirst: - msgYes = self.mainGui.askQuestion( - self.tr("Move '{0}' to Trash?").format(nwItemS.itemName), + msgYes = SHARED.question( + self.tr("Move '{0}' to Trash?").format(nwItemS.itemName) ) if not msgYes: logger.info("Action cancelled by user") @@ -920,7 +912,7 @@ class GuiProjectTree(QTreeWidget): Root items are handled a little different than other items. """ trItemS = self._getTreeItem(tHandle) - nwItemS = self.mainGui.project.tree[tHandle] + nwItemS = SHARED.project.tree[tHandle] if trItemS is None or nwItemS is None: logger.error("Could not find tree item for deletion") return False @@ -928,16 +920,14 @@ class GuiProjectTree(QTreeWidget): if nwItemS.isRootType(): # Only an empty ROOT folder can be deleted if trItemS.childCount() > 0: - self.mainGui.makeAlert(self.tr( - "Root folders can only be deleted when they are empty." - ), level=nwAlert.ERROR) + SHARED.error(self.tr("Root folders can only be deleted when they are empty.")) return False logger.debug("Permanently deleting root folder '%s'", tHandle) tIndex = self.indexOfTopLevelItem(trItemS) self.takeTopLevelItem(tIndex) - self.mainGui.project.removeItem(tHandle) + SHARED.project.removeItem(tHandle) self._treeMap.pop(tHandle, None) self._alertTreeChange(tHandle, flush=True) @@ -948,7 +938,7 @@ class GuiProjectTree(QTreeWidget): else: if askFirst: - msgYes = self.mainGui.askQuestion( + msgYes = SHARED.question( self.tr("Permanently delete '{0}'?").format(nwItemS.itemName) ) if not msgYes: @@ -964,9 +954,9 @@ class GuiProjectTree(QTreeWidget): trItemP.takeChild(tIndex) for dHandle in reversed(self.getTreeFromHandle(tHandle)): - if self.mainGui.docEditor.docHandle() == dHandle: + if self.mainGui.docEditor.docHandle == dHandle: self.mainGui.closeDocument() - self.mainGui.project.removeItem(dHandle) + SHARED.project.removeItem(dHandle) self._treeMap.pop(dHandle, None) self._alertTreeChange(tHandle, flush=flush) @@ -984,13 +974,13 @@ class GuiProjectTree(QTreeWidget): already coming from the project tree. """ trItem = self._getTreeItem(tHandle) - nwItem = self.mainGui.project.tree[tHandle] + nwItem = SHARED.project.tree[tHandle] if trItem is None or nwItem is None: return itemStatus, statusIcon = nwItem.getImportStatus(incIcon=True) hLevel = nwItem.mainHeading - itemIcon = CONFIG.theme.getItemIcon( + itemIcon = SHARED.theme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel ) @@ -1006,7 +996,7 @@ class GuiProjectTree(QTreeWidget): else: iconName = "noncheckable" - trItem.setIcon(self.C_ACTIVE, CONFIG.theme.getIcon(iconName)) + trItem.setIcon(self.C_ACTIVE, SHARED.theme.getIcon(iconName)) if CONFIG.emphLabels and nwItem.isDocumentLayout(): trFont = trItem.font(self.C_NAME) @@ -1046,10 +1036,10 @@ class GuiProjectTree(QTreeWidget): pHandle = pItem.data(self.C_DATA, self.D_HANDLE) if pHandle: - if self.mainGui.project.tree.checkType(pHandle, nwItemType.FILE): + if SHARED.project.tree.checkType(pHandle, nwItemType.FILE): # A file has an internal word count we need to account # for, but a folder always has 0 words on its own. - pCount += self.mainGui.project.index.getCounts(pHandle)[1] + pCount += SHARED.project.index.getCounts(pHandle)[1] self.propagateCount(pHandle, pCount, countChildren=False) @@ -1064,7 +1054,7 @@ class GuiProjectTree(QTreeWidget): logger.debug("Building the project tree ...") self.clearTree() count = 0 - for nwItem in self.mainGui.project.getProjectItems(): + for nwItem in SHARED.project.iterProjectItems(): count += 1 self._addTreeItem(nwItem) if count > 0: @@ -1177,7 +1167,7 @@ class GuiProjectTree(QTreeWidget): if tHandle is None: return - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] if tItem is None: return @@ -1199,7 +1189,7 @@ class GuiProjectTree(QTreeWidget): selItem = self.itemAt(clickPos) if isinstance(selItem, QTreeWidgetItem): tHandle = selItem.data(self.C_DATA, self.D_HANDLE) - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] hasChild = selItem.childCount() > 0 if tItem is None or tHandle is None: @@ -1211,7 +1201,7 @@ class GuiProjectTree(QTreeWidget): # Trash Folder # ============ - trashHandle = self.mainGui.project.tree.trashRoot() + trashHandle = SHARED.project.tree.trashRoot() if tItem.itemHandle == trashHandle and trashHandle is not None: # The trash folder only has one option aEmptyTrash = ctxMenu.addAction(self.tr("Empty Trash")) @@ -1250,7 +1240,7 @@ class GuiProjectTree(QTreeWidget): checkMark = f" ({nwUnicode.U_CHECK})" if tItem.isNovelLike(): mStatus = ctxMenu.addMenu(self.tr("Set Status to ...")) - for n, (key, entry) in enumerate(self.mainGui.project.data.itemStatus.items()): + for n, (key, entry) in enumerate(SHARED.project.data.itemStatus.items()): entryName = entry["name"] + (checkMark if tItem.itemStatus == key else "") aStatus = mStatus.addAction(entry["icon"], entryName) aStatus.triggered.connect( @@ -1263,7 +1253,7 @@ class GuiProjectTree(QTreeWidget): ) else: mImport = ctxMenu.addMenu(self.tr("Set Importance to ...")) - for n, (key, entry) in enumerate(self.mainGui.project.data.itemImport.items()): + for n, (key, entry) in enumerate(SHARED.project.data.itemImport.items()): entryName = entry["name"] + (checkMark if tItem.itemImport == key else "") aImport = mImport.addAction(entry["icon"], entryName) aImport.triggered.connect( @@ -1375,7 +1365,7 @@ class GuiProjectTree(QTreeWidget): return tHandle = selItem.data(self.C_DATA, self.D_HANDLE) - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] if tItem is None: return @@ -1419,7 +1409,7 @@ class GuiProjectTree(QTreeWidget): def _postItemMove(self, tHandle: str, wCount: int) -> bool: """Run various maintenance tasks for a moved item.""" trItemS = self._getTreeItem(tHandle) - nwItemS = self.mainGui.project.tree[tHandle] + nwItemS = SHARED.project.tree[tHandle] trItemP = trItemS.parent() if trItemS else None if trItemP is None or nwItemS is None: logger.error("Failed to find new parent item of '%s'", tHandle) @@ -1436,13 +1426,13 @@ class GuiProjectTree(QTreeWidget): logger.debug("A total of %d item(s) were moved", len(mHandles)) for mHandle in mHandles: logger.debug("Updating item '%s'", mHandle) - self.mainGui.project.tree.updateItemData(mHandle) + SHARED.project.tree.updateItemData(mHandle) # Update the index if nwItemS.isInactiveClass(): - self.mainGui.project.index.deleteHandle(mHandle) + SHARED.project.index.deleteHandle(mHandle) else: - self.mainGui.project.index.reIndexHandle(mHandle) + SHARED.project.index.reIndexHandle(mHandle) self.setTreeItemValues(mHandle) @@ -1462,7 +1452,7 @@ class GuiProjectTree(QTreeWidget): def _toggleItemActive(self, tHandle: str) -> None: """Toggle the active status of an item.""" - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] if tItem is not None: tItem.setActive(not tItem.isActive) self.setTreeItemValues(tItem.itemHandle) @@ -1483,7 +1473,7 @@ class GuiProjectTree(QTreeWidget): def _changeItemStatus(self, tHandle: str, tStatus: str) -> None: """Set a new status value of an item.""" - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] if tItem is not None: tItem.setStatus(tStatus) self.setTreeItemValues(tItem.itemHandle) @@ -1492,7 +1482,7 @@ class GuiProjectTree(QTreeWidget): def _changeItemImport(self, tHandle: str, tImport: str) -> None: """Set a new importance value of an item.""" - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] if tItem is not None: tItem.setImport(tImport) self.setTreeItemValues(tItem.itemHandle) @@ -1501,7 +1491,7 @@ class GuiProjectTree(QTreeWidget): def _changeItemLayout(self, tHandle: str, itemLayout: nwItemLayout) -> None: """Set a new item layout value of an item.""" - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] if tItem is not None: if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed(): tItem.setLayout(nwItemLayout.DOCUMENT) @@ -1515,9 +1505,9 @@ class GuiProjectTree(QTreeWidget): def _covertFolderToFile(self, tHandle: str, itemLayout: nwItemLayout) -> None: """Convert a folder to a note or document.""" - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] if tItem is not None and tItem.isFolderType(): - msgYes = self.mainGui.askQuestion(self.tr( + msgYes = SHARED.question(self.tr( "Do you want to convert the folder to a {0}? " "This action cannot be reversed." ).format(trConst(nwLabels.LAYOUT_NAME[itemLayout]))) @@ -1540,7 +1530,7 @@ class GuiProjectTree(QTreeWidget): logger.info("Request to merge items under handle '%s'", tHandle) itemList = self.getTreeFromHandle(tHandle) - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] if tItem is None: return False @@ -1559,14 +1549,14 @@ class GuiProjectTree(QTreeWidget): mrgData = dlgMerge.getData() mrgList = mrgData.get("finalItems", []) if not mrgList: - self.mainGui.makeAlert(self.tr("No documents selected for merging.")) + SHARED.info(self.tr("No documents selected for merging.")) return False # Save the open document first, in case it's part of merge self.mainGui.saveDocument() # Create merge object, and append docs - docMerger = DocMerger(self.mainGui.project) + docMerger = DocMerger(SHARED.project) mLabel = self.tr("Merged") if newFile: @@ -1582,13 +1572,13 @@ class GuiProjectTree(QTreeWidget): docMerger.appendText(sHandle, True, mLabel) if not docMerger.writeTargetDoc(): - self.mainGui.makeAlert( + SHARED.error( self.tr("Could not write document content."), - info=docMerger.getError(), level=nwAlert.ERROR + info=docMerger.getError() ) return False - self.mainGui.project.index.reIndexHandle(mHandle) + SHARED.project.index.reIndexHandle(mHandle) if newFile: self.revealNewTreeItem(mHandle, nHandle=tHandle, wordCount=True) @@ -1613,7 +1603,7 @@ class GuiProjectTree(QTreeWidget): """Split a document into multiple documents.""" logger.info("Request to split items with handle '%s'", tHandle) - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] if tItem is None: return False @@ -1632,7 +1622,7 @@ class GuiProjectTree(QTreeWidget): intoFolder = splitData.get("intoFolder", False) docHierarchy = splitData.get("docHierarchy", False) - docSplit = DocSplitter(self.mainGui.project, tHandle) + docSplit = DocSplitter(SHARED.project, tHandle) if intoFolder: fHandle = docSplit.newParentFolder(tItem.itemParent, tItem.itemName) self.revealNewTreeItem(fHandle, nHandle=tHandle) @@ -1642,13 +1632,13 @@ class GuiProjectTree(QTreeWidget): docSplit.splitDocument(headerList, splitText) for writeOk, dHandle, nHandle in docSplit.writeDocuments(docHierarchy): - self.mainGui.project.index.reIndexHandle(dHandle) + SHARED.project.index.reIndexHandle(dHandle) self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True) self._alertTreeChange(dHandle, flush=False) if not writeOk: - self.mainGui.makeAlert( + SHARED.error( self.tr("Could not write document content."), - info=docSplit.getError(), level=nwAlert.ERROR + info=docSplit.getError() ) if splitData.get("moveToTrash", False): @@ -1673,19 +1663,19 @@ class GuiProjectTree(QTreeWidget): else: question = self.tr("Do you want to duplicate this item and all child items?") - if not self.mainGui.askQuestion(question): + if not SHARED.question(question): return False - docDup = DocDuplicator(self.mainGui.project) + docDup = DocDuplicator(SHARED.project) dupCount = 0 for dHandle, nHandle in docDup.duplicate(itemTree): - self.mainGui.project.index.reIndexHandle(dHandle) + SHARED.project.index.reIndexHandle(dHandle) self.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True) self._alertTreeChange(dHandle, flush=False) dupCount += 1 if dupCount != nItems: - self.mainGui.makeAlert(self.tr("Could not duplicate all items."), level=nwAlert.WARN) + SHARED.warn(self.tr("Could not duplicate all items.")) self.saveTreeOrder() @@ -1699,7 +1689,7 @@ class GuiProjectTree(QTreeWidget): cCount = tItem.childCount() # Update tree-related meta data - nwItem = self.mainGui.project.tree[tHandle] + nwItem = SHARED.project.tree[tHandle] if nwItem is not None: nwItem.setExpanded(tItem.isExpanded() and cCount > 0) nwItem.setOrder(tIndex) @@ -1742,9 +1732,9 @@ class GuiProjectTree(QTreeWidget): elif pHandle and pHandle in self._treeMap: pItem = self._treeMap[pHandle] else: - self.mainGui.makeAlert(self.tr( + SHARED.error(self.tr( "There is nowhere to add item with name '{0}'." - ).format(nwItem.itemName), level=nwAlert.ERROR) + ).format(nwItem.itemName)) return None byIndex = -1 @@ -1766,13 +1756,13 @@ class GuiProjectTree(QTreeWidget): """Adds the trash root folder if it doesn't already exist in the project tree. """ - trashHandle = self.mainGui.project.trashFolder() + trashHandle = SHARED.project.trashFolder() if trashHandle is None: return None trItem = self._getTreeItem(trashHandle) if trItem is None: - trItem = self._addTreeItem(self.mainGui.project.tree[trashHandle]) + trItem = self._addTreeItem(SHARED.project.tree[trashHandle]) if trItem is not None: trItem.setExpanded(True) self._alertTreeChange(trashHandle, flush=True) @@ -1785,14 +1775,14 @@ class GuiProjectTree(QTreeWidget): deleted. """ self._timeChanged = time() - self.mainGui.project.setProjectChanged(True) + SHARED.project.setProjectChanged(True) if flush: self.saveTreeOrder() - if tHandle is None or tHandle not in self.mainGui.project.tree: + if tHandle is None or tHandle not in SHARED.project.tree: return - tItem = self.mainGui.project.tree[tHandle] + tItem = SHARED.project.tree[tHandle] if tItem and tItem.isRootType(): self.projView.rootFolderChanged.emit(tHandle) diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index 6ec9f308..266d735a 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -30,7 +30,7 @@ from PyQt5.QtWidgets import ( QToolBar, QWidget, QSizePolicy, QAction, QMenu, QToolButton ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwView logger = logging.getLogger(__name__) @@ -51,8 +51,8 @@ class GuiSideBar(QToolBar): iPx = CONFIG.pxInt(22) mPx = CONFIG.pxInt(60) - lblFont = CONFIG.theme.guiFont - lblFont.setPointSizeF(0.65*CONFIG.theme.fontPointSize) + lblFont = SHARED.theme.guiFont + lblFont.setPointSizeF(0.65*SHARED.theme.fontPointSize) self.setMovable(False) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) @@ -130,13 +130,13 @@ class GuiSideBar(QToolBar): """ self.setStyleSheet("QToolBar {border: 0px;}") - self.aProject.setIcon(CONFIG.theme.getIcon("view_editor")) - self.aNovel.setIcon(CONFIG.theme.getIcon("view_novel")) - self.aOutline.setIcon(CONFIG.theme.getIcon("view_outline")) - self.aBuild.setIcon(CONFIG.theme.getIcon("view_build")) - self.aDetails.setIcon(CONFIG.theme.getIcon("proj_details")) - self.aStats.setIcon(CONFIG.theme.getIcon("proj_stats")) - self.tbSettings.setIcon(CONFIG.theme.getIcon("settings")) + self.aProject.setIcon(SHARED.theme.getIcon("view_editor")) + self.aNovel.setIcon(SHARED.theme.getIcon("view_novel")) + self.aOutline.setIcon(SHARED.theme.getIcon("view_outline")) + self.aBuild.setIcon(SHARED.theme.getIcon("view_build")) + self.aDetails.setIcon(SHARED.theme.getIcon("proj_details")) + self.aStats.setIcon(SHARED.theme.getIcon("proj_stats")) + self.tbSettings.setIcon(SHARED.theme.getIcon("settings")) return diff --git a/novelwriter/gui/statusbar.py b/novelwriter/gui/statusbar.py index 138d1cd3..597124de 100644 --- a/novelwriter/gui/statusbar.py +++ b/novelwriter/gui/statusbar.py @@ -27,34 +27,38 @@ from __future__ import annotations import logging from time import time +from typing import TYPE_CHECKING, Literal from PyQt5.QtCore import pyqtSlot, QLocale from PyQt5.QtGui import QColor from PyQt5.QtWidgets import qApp, QStatusBar, QLabel -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.common import formatTime +from novelwriter.constants import nwConst from novelwriter.extensions.statusled import StatusLED +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + logger = logging.getLogger(__name__) class GuiMainStatus(QStatusBar): - def __init__(self, mainGui): + def __init__(self, mainGui: GuiMain) -> None: super().__init__(parent=mainGui) logger.debug("Create: GuiMainStatus") - self.mainGui = mainGui - self.refTime = None - self.userIdle = False + self._refTime = -1.0 + self._userIdle = False - colNone = QColor(*CONFIG.theme.statNone) - colSaved = QColor(*CONFIG.theme.statSaved) - colUnsaved = QColor(*CONFIG.theme.statUnsaved) + colNone = QColor(*SHARED.theme.statNone) + colSaved = QColor(*SHARED.theme.statSaved) + colUnsaved = QColor(*SHARED.theme.statUnsaved) - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize # Permanent Widgets # ================= @@ -98,7 +102,7 @@ class GuiMainStatus(QStatusBar): self.timeIcon = QLabel() self.timeText = QLabel("") self.timeText.setToolTip(self.tr("Session Time")) - self.timeText.setMinimumWidth(CONFIG.theme.getTextWidth("00:00:00:")) + self.timeText.setMinimumWidth(SHARED.theme.getTextWidth("00:00:00:")) self.timeIcon.setContentsMargins(0, 0, 0, 0) self.timeText.setContentsMargins(0, 0, 0, 0) self.addPermanentWidget(self.timeIcon) @@ -114,80 +118,59 @@ class GuiMainStatus(QStatusBar): return - def clearStatus(self): - """Reset all widgets on the status bar to default values. - """ - self.setRefTime(None) + def clearStatus(self) -> None: + """Reset all widgets on the status bar to default values.""" + self.setRefTime(-1.0) self.setLanguage(None, "") self.setProjectStats(0, 0) self.setProjectStatus(StatusLED.S_NONE) self.setDocumentStatus(StatusLED.S_NONE) self.updateTime() - return True - - def updateTheme(self): - """Update theme elements. - """ - iPx = CONFIG.theme.baseIconSize - - self.langIcon.setPixmap(CONFIG.theme.getPixmap("status_lang", (iPx, iPx))) - self.statsIcon.setPixmap(CONFIG.theme.getPixmap("status_stats", (iPx, iPx))) - - self.timePixmap = CONFIG.theme.getPixmap("status_time", (iPx, iPx)) - self.idlePixmap = CONFIG.theme.getPixmap("status_idle", (iPx, iPx)) + return + def updateTheme(self) -> None: + """Update theme elements.""" + iPx = SHARED.theme.baseIconSize + self.langIcon.setPixmap(SHARED.theme.getPixmap("status_lang", (iPx, iPx))) + self.statsIcon.setPixmap(SHARED.theme.getPixmap("status_stats", (iPx, iPx))) + self.timePixmap = SHARED.theme.getPixmap("status_time", (iPx, iPx)) + self.idlePixmap = SHARED.theme.getPixmap("status_idle", (iPx, iPx)) self.timeIcon.setPixmap(self.timePixmap) - return ## # Setters ## - def setRefTime(self, theTime): - """Set the reference time for the status bar clock. - """ - self.refTime = theTime + def setRefTime(self, refTime: float) -> None: + """Set the reference time for the status bar clock.""" + self._refTime = refTime return - def setStatus(self, theMessage, timeOut=20.0): - """Set the status bar message to display for 'timeOut' seconds. - """ - self.showMessage(theMessage, int(timeOut*1000)) - qApp.processEvents() + def setProjectStatus(self, state: Literal[0, 1, 2]) -> None: + """Set the project status colour icon.""" + self.projIcon.setState(state) return - def setProjectStatus(self, theState): - """Set the project status colour icon. - """ - self.projIcon.setState(theState) + def setDocumentStatus(self, state: Literal[0, 1, 2]) -> None: + """Set the document status colour icon.""" + self.docIcon.setState(state) return - def setDocumentStatus(self, theState): - """Set the document status colour icon. - """ - self.docIcon.setState(theState) - return - - def setUserIdle(self, userIdle): - """Change the idle status icon. - """ + def setUserIdle(self, idle: bool) -> None: + """Change the idle status icon.""" if not CONFIG.stopWhenIdle: - userIdle = False - - if self.userIdle != userIdle: - if userIdle: + idle = False + if self._userIdle != idle: + if idle: self.timeIcon.setPixmap(self.idlePixmap) else: self.timeIcon.setPixmap(self.timePixmap) - - self.userIdle = userIdle - + self._userIdle = idle return - def setProjectStats(self, pWC, sWC): - """Update the current project statistics. - """ + def setProjectStats(self, pWC: int, sWC: int) -> None: + """Update the current project statistics.""" self.statsText.setText(self.tr("Words: {0} ({1})").format(f"{pWC:n}", f"{sWC:+n}")) if CONFIG.incNotesWCount: self.statsText.setToolTip(self.tr("Project word count (session change)")) @@ -195,53 +178,55 @@ class GuiMainStatus(QStatusBar): self.statsText.setToolTip(self.tr("Novel word count (session change)")) return - def updateTime(self, idleTime=0.0): - """Update the session clock. - """ - if self.refTime is None: + def updateTime(self, idleTime: float = 0.0) -> None: + """Update the session clock.""" + if self._refTime < 0.0: self.timeText.setText("00:00:00") else: if CONFIG.stopWhenIdle: - sessTime = round(time() - self.refTime - idleTime) + sessTime = round(time() - self._refTime - idleTime) else: - sessTime = round(time() - self.refTime) + sessTime = round(time() - self._refTime) self.timeText.setText(formatTime(sessTime)) return ## - # Slots + # Public Slots ## + @pyqtSlot(str) + def setStatusMessage(self, message: str) -> None: + """Set the status bar message to display.""" + self.showMessage(message, nwConst.STATUS_MSG_TIMEOUT) + qApp.processEvents() + return + @pyqtSlot(str, str) - def setLanguage(self, theLanguage, theProvider): - """Set the language code for the spell checker. - """ - if theLanguage == "None": + def setLanguage(self, language: str, provider: str) -> None: + """Set the language code for the spell checker.""" + if language == "None": self.langText.setText(self.tr("None")) self.langText.setToolTip("") else: - qLocal = QLocale(theLanguage) + qLocal = QLocale(language) spLang = qLocal.nativeLanguageName().title() self.langText.setText(spLang) - if theProvider: - self.langText.setToolTip("%s (%s)" % (theLanguage, theProvider)) + if provider: + self.langText.setToolTip("%s (%s)" % (language, provider)) else: - self.langText.setToolTip(theLanguage) - + self.langText.setToolTip(language) return @pyqtSlot(bool) - def doUpdateProjectStatus(self, isChanged): - """Slot for updating the project status. - """ - self.setProjectStatus(StatusLED.S_BAD if isChanged else StatusLED.S_GOOD) + def updateProjectStatus(self, status: bool) -> None: + """Update the project status.""" + self.setProjectStatus(StatusLED.S_BAD if status else StatusLED.S_GOOD) return @pyqtSlot(bool) - def doUpdateDocumentStatus(self, isChanged): - """Slot for updating the document status. - """ - self.setDocumentStatus(StatusLED.S_BAD if isChanged else StatusLED.S_GOOD) + def updateDocumentStatus(self, status: bool) -> None: + """Update the document status.""" + self.setDocumentStatus(StatusLED.S_BAD if status else StatusLED.S_GOOD) return # END Class GuiMainStatus diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index ab728dfe..d606b8d3 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -31,13 +31,13 @@ from pathlib import Path from datetime import datetime from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot -from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence, QPixmap +from PyQt5.QtGui import QCloseEvent, QCursor, QIcon, QKeySequence from PyQt5.QtWidgets import ( qApp, QDialog, QFileDialog, QMainWindow, QMessageBox, QShortcut, QSplitter, QStackedWidget, QVBoxLayout, QWidget ) -from novelwriter import CONFIG, __hexversion__ +from novelwriter import CONFIG, SHARED, __hexversion__ from novelwriter.gui.theme import GuiTheme from novelwriter.gui.sidebar import GuiSideBar from novelwriter.gui.outline import GuiOutlineView @@ -59,14 +59,13 @@ from novelwriter.tools.lipsum import GuiLipsum from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.projwizard import GuiProjectWizard from novelwriter.tools.writingstats import GuiWritingStats -from novelwriter.core.project import NWProject from novelwriter.core.coretools import ProjectBuilder from novelwriter.enum import ( - nwDocAction, nwDocMode, nwItemType, nwItemClass, nwAlert, nwWidget, nwView + nwDocAction, nwDocMode, nwItemType, nwItemClass, nwWidget, nwView ) from novelwriter.common import getGuiItem, hexToInt -from novelwriter.constants import nwFiles, nwLabels, trConst +from novelwriter.constants import nwFiles logger = logging.getLogger(__name__) @@ -112,15 +111,11 @@ class GuiMain(QMainWindow): # Core Classes # ============ - # Core Classes - CONFIG.setThemeInstance(GuiTheme()) - self._project = NWProject(self) + # Initialise UserData Instance + SHARED.initSharedData(self, GuiTheme()) # Core Settings - self.hasProject = False self.isFocusMode = False - self.idleRefTime = time() - self.idleTime = 0.0 # Prepare Main Window self.resize(*CONFIG.mainWinSize) @@ -135,7 +130,6 @@ class GuiMain(QMainWindow): # ============= # Sizes - iPx = CONFIG.theme.fontPixelSize mPx = CONFIG.pxInt(4) hWd = CONFIG.pxInt(4) @@ -238,7 +232,8 @@ class GuiMain(QMainWindow): # Connect Signals # =============== - self._project.projectStatusChanged.connect(self.mainStatus.doUpdateProjectStatus) + SHARED.projectStatusChanged.connect(self.mainStatus.updateProjectStatus) + SHARED.projectStatusMessage.connect(self.mainStatus.setStatusMessage) self.viewsBar.viewChangeRequested.connect(self._changeView) @@ -257,12 +252,13 @@ class GuiMain(QMainWindow): self.novelView.openDocumentRequest.connect(self._openDocument) self.docEditor.spellDictionaryChanged.connect(self.mainStatus.setLanguage) - self.docEditor.docEditedStatusChanged.connect(self.mainStatus.doUpdateDocumentStatus) + self.docEditor.editedStatusChanged.connect(self.mainStatus.updateDocumentStatus) self.docEditor.docCountsChanged.connect(self.itemDetails.updateCounts) self.docEditor.docCountsChanged.connect(self.projView.updateCounts) self.docEditor.loadDocumentTagRequest.connect(self._followTag) self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree) self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta) + self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage) self.docViewer.loadDocumentTagRequest.connect(self._followTag) @@ -301,18 +297,6 @@ class GuiMain(QMainWindow): keyEscape.setKey(QKeySequence(Qt.Key_Escape)) keyEscape.activated.connect(self._keyPressEscape) - # Forward Functions - self.setStatus = self.mainStatus.setStatus - - # Cache Alert Pixmaps - pxSize = (2*iPx, 2*iPx) - self.alertPix: dict[nwAlert, QPixmap] = { - nwAlert.INFO: CONFIG.theme.getPixmap("alert_info", pxSize), - nwAlert.WARN: CONFIG.theme.getPixmap("alert_warn", pxSize), - nwAlert.ERROR: CONFIG.theme.getPixmap("alert_error", pxSize), - nwAlert.ASK: CONFIG.theme.getPixmap("alert_question", pxSize), - } - # Check that config loaded fine self.reportConfErr() @@ -328,33 +312,14 @@ class GuiMain(QMainWindow): logger.debug("Ready: GUI") if __hexversion__[-2] == "a" and logger.getEffectiveLevel() > logging.DEBUG: - self.makeAlert(self.tr( + SHARED.warn(self.tr( "You are running an untested development version of novelWriter. " "Please be careful when working on a live project " "and make sure you take regular backups." - ), level=nwAlert.WARN) + )) logger.info("novelWriter is ready ...") - self.setStatus(self.tr("novelWriter is ready ...")) - - return - - def clearGUI(self) -> None: - """Clear all sub-elements of the main GUI.""" - # Project Area - self.projView.clearProject() - self.novelView.clearProject() - self.itemDetails.clearDetails() - - # Work Area - self.docEditor.clearEditor() - self.docEditor.setDictionaries() - self.closeDocViewer(byUser=False) - self.outlineView.clearProject() - - # General - self.mainStatus.clearStatus() - self._updateWindowTitle() + self.mainStatus.setStatusMessage(self.tr("novelWriter is ready ...")) return @@ -365,14 +330,12 @@ class GuiMain(QMainWindow): return def postLaunchTasks(self, cmdOpen: str | None) -> None: - """This function is called after the main window is created to - determine what to open or show after initialisation. - """ + """Process tasks after the main window has been created.""" if cmdOpen: logger.info("Command line path: %s", cmdOpen) self.openProject(cmdOpen) - if not self.hasProject: + if not SHARED.hasProject: self.showProjectLoadDialog() # Determine whether release notes need to be shown or not @@ -382,26 +345,17 @@ class GuiMain(QMainWindow): return - ## - # Properties - ## - - @property - def project(self) -> NWProject: - """The project instance.""" - return self._project - ## # Project Actions ## def newProject(self, projData: dict | None = None) -> bool: """Create a new project via the new project wizard.""" - if self.hasProject: + if SHARED.hasProject: if not self.closeProject(): - self.makeAlert(self.tr( + SHARED.error(self.tr( "Cannot create a new project when another project is open." - ), level=nwAlert.ERROR) + )) return False if projData is None: @@ -416,14 +370,14 @@ class GuiMain(QMainWindow): return False if (Path(projPath) / nwFiles.PROJ_FILE).is_file(): - self.makeAlert(self.tr( + SHARED.error(self.tr( "A project already exists in that location. " "Please choose another folder." - ), level=nwAlert.ERROR) + )) return False logger.info("Creating new project") - nwProject = ProjectBuilder(self) + nwProject = ProjectBuilder() if nwProject.buildProject(projData): self.openProject(projPath) else: @@ -436,45 +390,46 @@ class GuiMain(QMainWindow): close application event so the user doesn't get prompted twice to confirm. """ - if not self.hasProject: + if not SHARED.hasProject: # There is no project loaded, everything OK return True if not isYes: - msgYes = self.askQuestion("%s
%s" % ( + msgYes = SHARED.question("%s
%s" % ( self.tr("Close the current project?"), self.tr("Changes are saved automatically.") )) if not msgYes: return False - if self.docEditor.docChanged(): + if self.docEditor.docChanged: self.saveDocument() saveOK = self.saveProject() doBackup = False - if self._project.data.doBackup and CONFIG.backupOnClose: + if SHARED.project.data.doBackup and CONFIG.backupOnClose: doBackup = True if CONFIG.askBeforeBackup: - msgYes = self.askQuestion(self.tr("Backup the current project?")) - if not msgYes: - doBackup = False + doBackup = SHARED.question(self.tr("Backup the current project?")) if doBackup: - self._project.backupProject(False) + SHARED.project.backupProject(False) if saveOK: self.closeDocument() self.docViewer.clearNavHistory() + self.closeDocViewer(byUser=False) + self.outlineView.closeProjectTasks() self.novelView.closeProjectTasks() + self.projView.clearProjectView() + self.itemDetails.clearDetails() + self.mainStatus.clearStatus() - self._project.closeProject(self.idleTime) - self.idleRefTime = time() - self.idleTime = 0.0 + SHARED.closeProject() - self.clearGUI() - self.hasProject = False + self.docEditor.setDictionaries() + self._updateWindowTitle() self._changeView(nwView.PROJECT) return saveOK @@ -493,9 +448,10 @@ class GuiMain(QMainWindow): self._changeView(nwView.PROJECT) # Try to open the project - if not self._project.openProject(projFile): + tStart = time() + if not SHARED.openProject(projFile): # The project open failed. - lockStatus = self._project.getLockStatus() + lockStatus = SHARED.projectLock if lockStatus is None: # The project is not locked, so failed for some other # reason handled by the project class. @@ -524,23 +480,18 @@ class GuiMain(QMainWindow): except Exception: lockDetails = "" - if self.askQuestion(lockText, info=lockInfo, details=lockDetails, level=nwAlert.WARN): - if not self._project.openProject(projFile, overrideLock=True): + if SHARED.question(lockText, info=lockInfo, details=lockDetails, warn=True): + if not SHARED.openProject(projFile, clearLock=True): return False else: return False - # Project is loaded - self.hasProject = True - self.idleRefTime = time() - self.idleTime = 0.0 - # Update GUI - self._updateWindowTitle(self._project.data.name) + self._updateWindowTitle(SHARED.project.data.name) self.rebuildTrees() self.docEditor.setDictionaries() - self.docEditor.toggleSpellCheck(self._project.data.spellCheck) - self.mainStatus.setRefTime(self._project.projOpened) + self.docEditor.toggleSpellCheck(SHARED.project.data.spellCheck) + self.mainStatus.setRefTime(SHARED.project.projOpened) self.projView.openProjectTasks() self.novelView.openProjectTasks() self.outlineView.openProjectTasks() @@ -548,9 +499,9 @@ class GuiMain(QMainWindow): # Restore previously open documents, if any # If none was recorded, open the first document found - lastEdited = self._project.data.getLastHandle("editor") + lastEdited = SHARED.project.data.getLastHandle("editor") if lastEdited is None: - for nwItem in self._project.tree: + for nwItem in SHARED.project.tree: if nwItem and nwItem.isFileType(): lastEdited = nwItem.itemHandle break @@ -558,32 +509,31 @@ class GuiMain(QMainWindow): if lastEdited is not None: self.openDocument(lastEdited, doScroll=True) - lastViewed = self._project.data.getLastHandle("viewer") + lastViewed = SHARED.project.data.getLastHandle("viewer") if lastViewed is not None: self.viewDocument(lastViewed) # Check if we need to rebuild the index - if self._project.index.indexBroken: - self.makeAlert(self.tr("The project index is outdated or broken. Rebuilding index.")) + if SHARED.project.index.indexBroken: + SHARED.info(self.tr("The project index is outdated or broken. Rebuilding index.")) self.rebuildIndex() # Make sure the changed status is set to false on things opened qApp.processEvents() self.docEditor.setDocumentChanged(False) - self._project.setProjectChanged(False) + SHARED.project.setProjectChanged(False) - logger.debug("Project load complete") + logger.debug("Project loaded in %.3f ms", (time() - tStart)*1000) return True def saveProject(self, autoSave: bool = False) -> bool: """Save the current project.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False self.projView.saveProjectTasks() - self._project.saveProject(autoSave=autoSave) - return True + return SHARED.saveProject(autoSave=autoSave) ## # Document Actions @@ -591,7 +541,7 @@ class GuiMain(QMainWindow): def closeDocument(self, beforeOpen: bool = False) -> bool: """Close the document and clear the editor and title field.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -600,7 +550,7 @@ class GuiMain(QMainWindow): self.toggleFocusMode() self.docEditor.saveCursorPosition() - if self.docEditor.docChanged(): + if self.docEditor.docChanged: self.saveDocument() self.docEditor.clearEditor() if not beforeOpen: @@ -611,16 +561,16 @@ class GuiMain(QMainWindow): def openDocument(self, tHandle: str | None, tLine: int | None = None, changeFocus: bool = True, doScroll: bool = False) -> bool: """Open a specific document, optionally at a given line.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False - if not tHandle or not self._project.tree.checkType(tHandle, nwItemType.FILE): + if not tHandle or not SHARED.project.tree.checkType(tHandle, nwItemType.FILE): logger.debug("Requested item '%s' is not a document", tHandle) return False self._changeView(nwView.EDITOR) - cHandle = self.docEditor.docHandle() + cHandle = self.docEditor.docHandle if cHandle == tHandle: self.docEditor.setCursorLine(tLine) if changeFocus: @@ -629,7 +579,7 @@ class GuiMain(QMainWindow): self.closeDocument(beforeOpen=True) if self.docEditor.loadText(tHandle, tLine): - self._project.data.setLastHandle(tHandle, "editor") + SHARED.project.data.setLastHandle(tHandle, "editor") self.projView.setSelectedHandle(tHandle, doScroll=doScroll) self.novelView.setActiveHandle(tHandle) if changeFocus: @@ -643,14 +593,14 @@ class GuiMain(QMainWindow): """Opens the next document in the project tree, following the document with the given handle. Stops when reaching the end. """ - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False nHandle = None # The next handle after tHandle fHandle = None # The first file handle we encounter foundIt = False # We've found tHandle, pick the next we see - for tItem in self._project.tree: + for tItem in SHARED.project.tree: if not tItem.isFileType(): continue if fHandle is None: @@ -672,7 +622,7 @@ class GuiMain(QMainWindow): def saveDocument(self) -> bool: """Save the current documents.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False self.docEditor.saveText() @@ -680,7 +630,7 @@ class GuiMain(QMainWindow): def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool: """Load a document for viewing in the view panel.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -688,7 +638,7 @@ class GuiMain(QMainWindow): logger.debug("Viewing document, but no handle provided") if self.docEditor.hasFocus(): - tHandle = self.docEditor.docHandle() + tHandle = self.docEditor.docHandle if tHandle is not None: self.saveDocument() @@ -696,7 +646,7 @@ class GuiMain(QMainWindow): tHandle = self.projView.getSelectedHandle() if tHandle is None: - tHandle = self._project.data.getLastHandle("viewer") + tHandle = SHARED.project.data.getLastHandle("viewer") if tHandle is None: logger.debug("No document to view, giving up") @@ -725,7 +675,7 @@ class GuiMain(QMainWindow): """Import the text contained in an out-of-project text file, and insert the text into the currently open document. """ - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -751,19 +701,19 @@ class GuiMain(QMainWindow): theText = inFile.read() CONFIG.setLastPath(loadFile) except Exception as exc: - self.makeAlert(self.tr( + SHARED.error(self.tr( "Could not read file. The file must be an existing text file." - ), level=nwAlert.ERROR, exception=exc) + ), exc=exc) return False - if self.docEditor.docHandle() is None: - self.makeAlert(self.tr( + if self.docEditor.docHandle is None: + SHARED.error(self.tr( "Please open a document to import the text file into." - ), level=nwAlert.ERROR) + )) return False - if not self.docEditor.isEmpty(): - msgYes = self.askQuestion(self.tr( + if not self.docEditor.isEmpty: + msgYes = SHARED.question(self.tr( "Importing the file will overwrite the current content of " "the document. Do you want to proceed?" )) @@ -797,7 +747,7 @@ class GuiMain(QMainWindow): active. It is not checked that the item is actually a document. That should be handled by the openDocument function. """ - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -815,7 +765,7 @@ class GuiMain(QMainWindow): return False if tHandle is not None and sTitle is not None: - hItem = self._project.index.getItemHeader(tHandle, sTitle) + hItem = SHARED.project.index.getItemHeader(tHandle, sTitle) if hItem is not None: tLine = hItem.line @@ -826,12 +776,12 @@ class GuiMain(QMainWindow): def editItemLabel(self, tHandle: str | None = None) -> bool: """Open the edit item dialog.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False if tHandle is None and (self.docEditor.anyFocus() or self.isFocusMode): - tHandle = self.docEditor.docHandle() + tHandle = self.docEditor.docHandle self.projView.renameTreeItem(tHandle) return True @@ -843,7 +793,7 @@ class GuiMain(QMainWindow): def rebuildIndex(self, beQuiet: bool = False) -> bool: """Rebuild the entire index.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -852,12 +802,12 @@ class GuiMain(QMainWindow): tStart = time() self.projView.saveProjectTasks() - self._project.index.rebuildIndex() + SHARED.project.index.rebuildIndex() self.projView.populateTree() self.novelView.refreshTree() tEnd = time() - self.setStatus( + self.mainStatus.setStatusMessage( self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}") ) self.docEditor.updateTagHighLighting() @@ -865,7 +815,7 @@ class GuiMain(QMainWindow): qApp.restoreOverrideCursor() if not beQuiet: - self.makeAlert(self.tr("The project index has been successfully rebuilt.")) + SHARED.info(self.tr("The project index has been successfully rebuilt.")) return True @@ -911,7 +861,7 @@ class GuiMain(QMainWindow): self.saveDocument() if dlgConf.needsRestart: - self.makeAlert(self.tr( + SHARED.info(self.tr( "Some changes will not be applied until novelWriter has been restarted." )) @@ -921,7 +871,7 @@ class GuiMain(QMainWindow): if dlgConf.updateTheme: # We are doing this manually instead of connecting to # qApp.paletteChanged since the processing order matters - CONFIG.theme.loadTheme() + SHARED.theme.loadTheme() self.docEditor.updateTheme() self.docViewer.updateTheme() self.viewsBar.updateTheme() @@ -932,7 +882,7 @@ class GuiMain(QMainWindow): self.mainStatus.updateTheme() if dlgConf.updateSyntax: - CONFIG.theme.loadSyntax() + SHARED.theme.loadSyntax() self.docEditor.updateSyntaxColours() self.docEditor.initEditor() @@ -948,7 +898,7 @@ class GuiMain(QMainWindow): @pyqtSlot(int) def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.TAB_MAIN) -> bool: """Open the project settings dialog.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -960,13 +910,13 @@ class GuiMain(QMainWindow): if dlgProj.spellChanged: self.docEditor.setDictionaries() self.itemDetails.refreshDetails() - self._updateWindowTitle(self._project.data.name) + self._updateWindowTitle(SHARED.project.data.name) return True def showProjectDetailsDialog(self) -> bool: """Open the project details dialog.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -985,7 +935,7 @@ class GuiMain(QMainWindow): @pyqtSlot() def showBuildManuscriptDialog(self) -> bool: """Open the build manuscript dialog.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -1005,7 +955,7 @@ class GuiMain(QMainWindow): def showLoremIpsumDialog(self) -> bool: """Open the insert lorem ipsum text dialog.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -1023,7 +973,7 @@ class GuiMain(QMainWindow): def showProjectWordListDialog(self) -> bool: """Open the project word list dialog.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -1038,7 +988,7 @@ class GuiMain(QMainWindow): def showWritingStatsDialog(self) -> bool: """Open the session stats dialog.""" - if not self.hasProject: + if not SHARED.hasProject: logger.error("No project open") return False @@ -1094,56 +1044,13 @@ class GuiMain(QMainWindow): return - def makeAlert(self, text: str, info: str = "", details: str = "", - level: nwAlert = nwAlert.INFO, exception: Exception | None = None) -> None: - """Alert both the user and the logger at the same time. The - message can be either a string or a list of strings. - """ - logText = " ".join(filter(None, [text, info, details])) - if level == nwAlert.INFO: - logger.info(logText, stacklevel=2) - elif level == nwAlert.WARN: - logger.warning(logText, stacklevel=2) - elif level == nwAlert.ERROR: - logger.error(logText, stacklevel=2, exc_info=exception) - - if exception is not None: - excText = f"{type(exception).__name__}: {str(exception)}" - info = f"{info}
{excText}" if info else excText - - msgBox = QMessageBox(self) - msgBox.setWindowTitle(trConst(nwLabels.ALERT_NAME[level])) - msgBox.setText(text) - msgBox.setInformativeText(info) - msgBox.setDetailedText(details) - msgBox.setStandardButtons(QMessageBox.Ok) - msgBox.setIconPixmap(self.alertPix[level]) - msgBox.adjustSize() - msgBox.exec_() - - return - - def askQuestion(self, text: str, info: str = "", details: str = "", - level: nwAlert = nwAlert.ASK) -> bool: - """Ask the user a Yes/No question, and return the answer.""" - msgBox = QMessageBox(self) - msgBox.setWindowTitle(trConst(nwLabels.ALERT_NAME[level])) - msgBox.setText(text) - msgBox.setInformativeText(info) - msgBox.setDetailedText(details) - msgBox.setStandardButtons(QMessageBox.Yes | QMessageBox.No) - msgBox.setIconPixmap(self.alertPix[level]) - msgBox.adjustSize() - msgBox.exec_() - return msgBox.result() == QMessageBox.Yes - def reportConfErr(self) -> bool: """Checks if the Config module has any errors to report, and let the user know if this is the case. The Config module caches errors since it is initialised before the GUI itself. """ if CONFIG.hasError: - self.makeAlert(CONFIG.errorText(), level=nwAlert.ERROR) + SHARED.error(CONFIG.errorText()) return True return False @@ -1153,8 +1060,8 @@ class GuiMain(QMainWindow): def closeMain(self) -> bool: """Save everything, and close novelWriter.""" - if self.hasProject: - msgYes = self.askQuestion("%s
%s" % ( + if SHARED.hasProject: + msgYes = SHARED.question("%s
%s" % ( self.tr("Do you want to exit novelWriter?"), self.tr("Changes are saved automatically.") )) @@ -1174,7 +1081,7 @@ class GuiMain(QMainWindow): # Ignore window size if in full screen mode CONFIG.setMainWinSize(self.width(), self.height()) - if self.hasProject: + if SHARED.hasProject: self.closeProject(True) CONFIG.saveConfig() @@ -1206,7 +1113,7 @@ class GuiMain(QMainWindow): def closeDocEditor(self) -> None: """Close the document editor. This does not hide the editor.""" self.closeDocument() - self._project.data.setLastHandle(None, "editor") + SHARED.project.data.setLastHandle(None, "editor") return def closeDocViewer(self, byUser: bool = True) -> bool: @@ -1214,7 +1121,7 @@ class GuiMain(QMainWindow): self.docViewer.clearViewer() if byUser: # Only reset the last handle if the user called this - self._project.data.setLastHandle(None, "viewer") + SHARED.project.data.setLastHandle(None, "viewer") # Hide the panel bPos = self.splitMain.sizes() @@ -1227,7 +1134,7 @@ class GuiMain(QMainWindow): """Handle toggle focus mode. The Main GUI Focus Mode hides tree, view, statusbar and menu. """ - if self.docEditor.docHandle() is None: + if self.docEditor.docHandle is None: logger.error("No document open, so not activating Focus Mode") return False @@ -1250,7 +1157,7 @@ class GuiMain(QMainWindow): if self.splitView.isVisible(): self.splitView.setVisible(False) - elif self.docViewer.docHandle() is not None: + elif self.docViewer.docHandle is not None: self.splitView.setVisible(True) return True @@ -1400,15 +1307,15 @@ class GuiMain(QMainWindow): """Handle the index lookup of a tag and display an alert if the tag cannot be found. """ - tHandle, sTitle = self._project.index.getTagSource(tag) + tHandle, sTitle = SHARED.project.index.getTagSource(tag) if tHandle is None: - self.makeAlert(self.tr( + SHARED.error(self.tr( "Could not find the reference for tag '{0}'. It either doesn't " "exist, or the index is out of date. The index can be updated " "from the Tools menu, or by pressing {1}." ).format( tag, "F9" - ), level=nwAlert.ERROR) + )) return None, None return tHandle, sTitle @@ -1447,7 +1354,7 @@ class GuiMain(QMainWindow): if tHandle is not None: if mode == nwDocMode.EDIT: tLine = None - hItem = self._project.index.getItemHeader(tHandle, sTitle) + hItem = SHARED.project.index.getItemHeader(tHandle, sTitle) if hItem is not None: tLine = hItem.line self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus) @@ -1478,30 +1385,22 @@ class GuiMain(QMainWindow): @pyqtSlot() def _timeTick(self) -> None: """Process time tick of the main timer.""" - if not self.hasProject: + if not SHARED.hasProject: return - currTime = time() - editIdle = currTime - self.docEditor.lastActive() > CONFIG.userIdleTime + editIdle = currTime - self.docEditor.lastActive > CONFIG.userIdleTime userIdle = qApp.applicationState() != Qt.ApplicationActive - - if editIdle or userIdle: - self.idleTime += currTime - self.idleRefTime - self.mainStatus.setUserIdle(True) - else: - self.mainStatus.setUserIdle(False) - - self.idleRefTime = currTime - self.mainStatus.updateTime(idleTime=self.idleTime) - + self.mainStatus.setUserIdle(editIdle or userIdle) + SHARED.updateIdleTime(currTime, editIdle or userIdle) + self.mainStatus.updateTime(idleTime=SHARED.projectIdleTime) return @pyqtSlot() def _autoSaveProject(self) -> None: """Autosave of the project. This is a timer-activated slot.""" - doSave = self.hasProject - doSave &= self._project.projChanged - doSave &= self._project.storage.isOpen() + doSave = SHARED.hasProject + doSave &= SHARED.project.projChanged + doSave &= SHARED.project.storage.isOpen() if doSave: logger.debug("Autosaving project") self.saveProject(autoSave=True) @@ -1510,7 +1409,7 @@ class GuiMain(QMainWindow): @pyqtSlot() def _autoSaveDocument(self) -> None: """Autosave of the document. This is a timer-activated slot.""" - if self.hasProject and self.docEditor.docChanged(): + if SHARED.hasProject and self.docEditor.docChanged: logger.debug("Autosaving document") self.saveDocument() return @@ -1518,17 +1417,17 @@ class GuiMain(QMainWindow): @pyqtSlot() def _updateStatusWordCount(self) -> None: """Update the word count on the status bar.""" - if not self.hasProject: + if not SHARED.hasProject: self.mainStatus.setProjectStats(0, 0) - self._project.updateWordCounts() + SHARED.project.updateWordCounts() if CONFIG.incNotesWCount: - iTotal = sum(self._project.data.initCounts) - cTotal = sum(self._project.data.currCounts) + iTotal = sum(SHARED.project.data.initCounts) + cTotal = sum(SHARED.project.data.currCounts) self.mainStatus.setProjectStats(cTotal, cTotal - iTotal) else: - iNovel, _ = self._project.data.initCounts - cNovel, _ = self._project.data.currCounts + iNovel, _ = SHARED.project.data.initCounts + cNovel, _ = SHARED.project.data.currCounts self.mainStatus.setProjectStats(cNovel, cNovel - iNovel) return @@ -1554,7 +1453,7 @@ class GuiMain(QMainWindow): def _mainStackChanged(self, index: int) -> None: """Process main window tab change.""" if index == self.idxOutlineView: - if self.hasProject: + if SHARED.hasProject: self.outlineView.refreshTree() return diff --git a/novelwriter/shared.py b/novelwriter/shared.py new file mode 100644 index 00000000..eea5fe26 --- /dev/null +++ b/novelwriter/shared.py @@ -0,0 +1,300 @@ +""" +novelWriter – Shared Data Class +=============================== + +File History: +Created: 2023-08-10 [2.1b2] + +This file is a part of novelWriter +Copyright 2018–2023, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from __future__ import annotations + +import logging + +from time import time +from typing import TYPE_CHECKING +from pathlib import Path + +from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot +from PyQt5.QtWidgets import QMessageBox, QWidget + +if TYPE_CHECKING: # pragma: no cover + from novelwriter.guimain import GuiMain + from novelwriter.gui.theme import GuiTheme + from novelwriter.core.project import NWProject + +logger = logging.getLogger(__name__) + + +class SharedData(QObject): + + __slots__ = ( + "_gui", "_theme", "_project", "_lockedBy", "_alert", + "_idleTime", "_idleRefTime", + ) + + projectStatusChanged = pyqtSignal(bool) + projectStatusMessage = pyqtSignal(str) + + def __init__(self) -> None: + super().__init__() + self._gui = None + self._theme = None + self._project = None + self._lockedBy = None + self._alert = None + self._idleTime = 0.0 + self._idleRefTime = time() + return + + @property + def mainGui(self) -> GuiMain: + """Return the Main GUI instance.""" + if self._gui is None: + raise Exception("SharedData class not fully initialised") + return self._gui + + @property + def theme(self) -> GuiTheme: + """Return the GUI Theme instance.""" + if self._theme is None: + raise Exception("SharedData class not fully initialised") + return self._theme + + @property + def project(self) -> NWProject: + """Return the active NWProject instance.""" + if self._project is None: + raise Exception("SharedData class not fully initialised") + return self._project + + @property + def hasProject(self) -> bool: + """Return True of the project instance is populated.""" + return self.project.isValid + + @property + def projectLock(self) -> list | None: + """Return cached lock information for the last project.""" + return self._lockedBy + + @property + def projectIdleTime(self) -> float: + """Return the session idle time.""" + return self._idleTime + + @property + def alert(self) -> _GuiAlert | None: + """Return a pointer to the last alert box.""" + return self._alert + + ## + # Methods + ## + + def initSharedData(self, gui: GuiMain, theme: GuiTheme) -> None: + """Initialise the UserData instance. This must be called as soon + as the Main GUI is created to ensure the SHARED singleton has the + properties needed for operation. + """ + self._gui = gui + self._theme = theme + self._resetProject() + logger.debug("SharedData instance initialised") + return + + def openProject(self, path: str | Path, clearLock: bool = False) -> bool: + """Open a project.""" + if self.project.isValid: + logger.error("A project is already open") + return False + + self._lockedBy = None + status = self.project.openProject(path, clearLock=clearLock) + if status is False: + # We must cache the lock status before resetting the project + self._lockedBy = self.project.lockStatus + self._resetProject() + + self._resetIdleTimer() + + return status + + def saveProject(self, autoSave: bool = False) -> bool: + """Save the current project.""" + if not self.project.isValid: + logger.error("There is no project open") + return False + return self.project.saveProject(autoSave=autoSave) + + def closeProject(self) -> None: + """Close the current project.""" + self.project.closeProject(self._idleTime) + self._resetProject() + self._resetIdleTimer() + return + + def updateIdleTime(self, currTime: float, userIdle: bool) -> None: + """Update the idle time record. If the userIdle flag is True, + the user idle counter is updated with the time difference since + the last time this function was called. Otherwise, only the + reference time is updated. + """ + if userIdle: + self._idleTime += currTime - self._idleRefTime + self._idleRefTime = currTime + return + + ## + # Alert Boxes + ## + + def info(self, text: str, info: str = "", details: str = "") -> None: + """Open an information alert box.""" + self._alert = _GuiAlert(self.mainGui, self.theme) + self._alert.setMessage(text, info, details) + self._alert.setAlertType(_GuiAlert.INFO, False) + logger.info(self._alert.logMessage, stacklevel=2) + self._alert.exec_() + return + + def warn(self, text: str, info: str = "", details: str = "") -> None: + """Open a warning alert box.""" + self._alert = _GuiAlert(self.mainGui, self.theme) + self._alert.setMessage(text, info, details) + self._alert.setAlertType(_GuiAlert.WARN, False) + logger.warning(self._alert.logMessage, stacklevel=2) + self._alert.exec_() + return + + def error(self, text: str, info: str = "", details: str = "", + exc: Exception | None = None) -> None: + """Open an error alert box.""" + self._alert = _GuiAlert(self.mainGui, self.theme) + self._alert.setMessage(text, info, details) + self._alert.setAlertType(_GuiAlert.ERROR, False) + if exc: + self._alert.setException(exc) + logger.error(self._alert.logMessage, stacklevel=2) + self._alert.exec_() + return + + def question(self, text: str, info: str = "", details: str = "", warn: bool = False) -> bool: + """Open a question box.""" + self._alert = _GuiAlert(self.mainGui, self.theme) + self._alert.setMessage(text, info, details) + self._alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True) + self._alert.exec_() + return self._alert.result() == QMessageBox.Yes + + ## + # Internal Slots + ## + + @pyqtSlot(bool) + def _emitProjectStatusChange(self, state: bool) -> None: + """Forward the project status slot.""" + self.projectStatusChanged.emit(state) + return + + @pyqtSlot(str) + def _emitProjectStatusMeesage(self, message: str) -> None: + """Forward the project message slot.""" + self.projectStatusMessage.emit(message) + return + + ## + # Internal Functions + ## + + def _resetProject(self) -> None: + """Create a new project instance.""" + from novelwriter.core.project import NWProject + if isinstance(self._project, NWProject): + self._project.statusChanged.disconnect() + self._project.statusMessage.disconnect() + self._project.deleteLater() + self._project = NWProject(self) + self._project.statusChanged.connect(self._emitProjectStatusChange) + self._project.statusMessage.connect(self._emitProjectStatusMeesage) + return + + def _resetIdleTimer(self) -> None: + """Reset the timer data for the idle timer.""" + self._idleRefTime = time() + self._idleTime = 0.0 + return + +# END Class SharedData + + +class _GuiAlert(QMessageBox): + + INFO = 0 + WARN = 1 + ERROR = 2 + ASK = 3 + + def __init__(self, parent: QWidget, theme: GuiTheme) -> None: + super().__init__(parent=parent) + self._theme = theme + self._message = "" + return + + @property + def logMessage(self) -> str: + return self._message + + def setMessage(self, text: str, info: str, details: str) -> None: + """Set the alert box message.""" + self._message = " ".join(filter(None, [text, info, details])) + self.setText(text) + self.setInformativeText(info) + self.setDetailedText(details) + return + + def setException(self, exception: Exception) -> None: + """Add exception details.""" + info = self.informativeText() + text = f"{type(exception).__name__}: {str(exception)}" + self.setInformativeText(f"{info}
{text}" if info else text) + return + + def setAlertType(self, level: int, isYesNo: bool) -> None: + """Set the type of alert and whether the dialog should have + Yes/No buttons or just an Ok button. + """ + if isYesNo: + self.setStandardButtons(QMessageBox.Yes | QMessageBox.No) + else: + self.setStandardButtons(QMessageBox.Ok) + pSz = 2*self._theme.baseIconSize + if level == self.INFO: + self.setIconPixmap(self._theme.getPixmap("alert_info", (pSz, pSz))) + self.setWindowTitle(self.tr("Information")) + elif level == self.WARN: + self.setIconPixmap(self._theme.getPixmap("alert_warn", (pSz, pSz))) + self.setWindowTitle(self.tr("Warning")) + elif level == self.ERROR: + self.setIconPixmap(self._theme.getPixmap("alert_error", (pSz, pSz))) + self.setWindowTitle(self.tr("Error")) + elif level == self.ASK: + self.setIconPixmap(self._theme.getPixmap("alert_question", (pSz, pSz))) + self.setWindowTitle(self.tr("Question")) + return + +# END Class _GuiAlert diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index d7aa9d51..d5febc2b 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -32,7 +32,7 @@ from PyQt5.QtWidgets import ( QSpinBox ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.common import readTextFile from novelwriter.extensions.switch import NSwitch @@ -60,7 +60,7 @@ class GuiLipsum(QDialog): nPx = CONFIG.pxInt(64) vSp = CONFIG.pxInt(4) self.docIcon = QLabel() - self.docIcon.setPixmap(CONFIG.theme.getPixmap("proj_document", (nPx, nPx))) + self.docIcon.setPixmap(SHARED.theme.getPixmap("proj_document", (nPx, nPx))) self.leftBox = QVBoxLayout() self.leftBox.setSpacing(vSp) diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index 2fb91269..390d626c 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -25,7 +25,6 @@ from __future__ import annotations import logging -from typing import TYPE_CHECKING from pathlib import Path from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot @@ -35,8 +34,8 @@ from PyQt5.QtWidgets import ( QPushButton, QSplitter, QVBoxLayout, QWidget ) -from novelwriter import CONFIG -from novelwriter.enum import nwAlert, nwBuildFmt +from novelwriter import CONFIG, SHARED +from novelwriter.enum import nwBuildFmt from novelwriter.common import makeFileNameSafe from novelwriter.constants import nwLabels from novelwriter.core.item import NWItem @@ -44,9 +43,6 @@ from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.buildsettings import BuildSettings from novelwriter.extensions.simpleprogress import NProgressSimple -if TYPE_CHECKING: # pragma: no cover - from novelwriter.guimain import GuiMain - logger = logging.getLogger(__name__) @@ -59,14 +55,12 @@ class GuiManuscriptBuild(QDialog): D_KEY = Qt.ItemDataRole.UserRole - def __init__(self, parent: QWidget, mainGui: GuiMain, build: BuildSettings): + def __init__(self, parent: QWidget, build: BuildSettings): super().__init__(parent=parent) logger.debug("Create: GuiManuscriptBuild") self.setObjectName("GuiManuscriptBuild") - self.mainGui = mainGui - self._parent = parent self._build = build @@ -74,14 +68,14 @@ class GuiManuscriptBuild(QDialog): self.setMinimumWidth(CONFIG.pxInt(500)) self.setMinimumHeight(CONFIG.pxInt(300)) - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize sp4 = CONFIG.pxInt(4) sp8 = CONFIG.pxInt(8) sp16 = CONFIG.pxInt(16) wWin = CONFIG.pxInt(620) hWin = CONFIG.pxInt(360) - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options self.resize( CONFIG.pxInt(pOptions.getInt("GuiManuscriptBuild", "winWidth", wWin)), CONFIG.pxInt(pOptions.getInt("GuiManuscriptBuild", "winHeight", hWin)) @@ -146,7 +140,7 @@ class GuiManuscriptBuild(QDialog): # Build Path self.lblPath = QLabel(self.tr("Path")) self.buildPath = QLineEdit(self) - self.btnBrowse = QPushButton(CONFIG.theme.getIcon("browse"), "") + self.btnBrowse = QPushButton(SHARED.theme.getIcon("browse"), "") self.pathBox = QHBoxLayout() self.pathBox.addWidget(self.buildPath) @@ -156,7 +150,7 @@ class GuiManuscriptBuild(QDialog): # Build Name self.lblName = QLabel(self.tr("File Name")) self.buildName = QLineEdit(self) - self.btnReset = QPushButton(CONFIG.theme.getIcon("revert"), "") + self.btnReset = QPushButton(SHARED.theme.getIcon("revert"), "") self.btnReset.setToolTip(self.tr("Reset file name to default")) self.nameBox = QHBoxLayout() @@ -181,7 +175,7 @@ class GuiManuscriptBuild(QDialog): self.buildBox.setVerticalSpacing(sp4) # Dialog Buttons - self.btnBuild = QPushButton(CONFIG.theme.getIcon("export"), self.tr("&Build")) + self.btnBuild = QPushButton(SHARED.theme.getIcon("export"), self.tr("&Build")) self.dlgButtons = QDialogButtonBox(QDialogButtonBox.Close) self.dlgButtons.addButton(self.btnBuild, QDialogButtonBox.ActionRole) @@ -279,7 +273,7 @@ class GuiManuscriptBuild(QDialog): @pyqtSlot() def _doResetBuildName(self): """Generate a default build name.""" - bName = f"{self.mainGui.project.data.name} - {self._build.name}" + bName = f"{SHARED.project.data.name} - {self._build.name}" self.buildName.setText(bName) self._build.setLastBuildName(bName) return @@ -308,19 +302,19 @@ class GuiManuscriptBuild(QDialog): self.buildProgress.setValue(0) bPath = Path(self.buildPath.text()) if not bPath.is_dir(): - self.mainGui.makeAlert(self.tr("Output folder does not exist."), level=nwAlert.ERROR) + SHARED.error(self.tr("Output folder does not exist.")) return False bExt = nwLabels.BUILD_EXT[bFormat] buildPath = (bPath / makeFileNameSafe(bName)).with_suffix(bExt) if buildPath.exists(): - if not self.mainGui.askQuestion( + if not SHARED.question( self.tr("The file already exists. Do you want to overwrite it?") ): return False - docBuild = NWBuildDocument(self.mainGui.project, self._build) + docBuild = NWBuildDocument(SHARED.project, self._build) docBuild.queueAll() self.buildProgress.setMaximum(len(docBuild)) @@ -353,7 +347,7 @@ class GuiManuscriptBuild(QDialog): fmtWidth = CONFIG.rpxInt(mainSplit[0]) sumWidth = CONFIG.rpxInt(mainSplit[1]) - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options pOptions.setValue("GuiManuscriptBuild", "winWidth", winWidth) pOptions.setValue("GuiManuscriptBuild", "winHeight", winHeight) pOptions.setValue("GuiManuscriptBuild", "fmtWidth", fmtWidth) @@ -365,9 +359,9 @@ class GuiManuscriptBuild(QDialog): def _populateContentList(self): """Build the content list.""" rootMap = {} - filtered = self._build.buildItemFilter(self.mainGui.project) + filtered = self._build.buildItemFilter(SHARED.project) self.listContent.clear() - for nwItem in self.mainGui.project.tree: + for nwItem in SHARED.project.tree: tHandle = nwItem.itemHandle rHandle = nwItem.itemRoot @@ -376,11 +370,11 @@ class GuiManuscriptBuild(QDialog): if filtered.get(tHandle, (False, 0))[0]: if rHandle not in rootMap: - rItem = self.mainGui.project.tree[rHandle] + rItem = SHARED.project.tree[rHandle] if isinstance(rItem, NWItem): rootMap[rHandle] = rItem.itemName - itemIcon = CONFIG.theme.getItemIcon( + itemIcon = SHARED.theme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading ) diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 8666e47d..1a1e4d1d 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -38,7 +38,7 @@ from PyQt5.QtWidgets import ( ) from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.error import logException from novelwriter.common import checkInt, fuzzyTime from novelwriter.core.tohtml import ToHtml @@ -74,18 +74,18 @@ class GuiManuscript(QDialog): self.mainGui = mainGui - self._builds = BuildCollection(self.mainGui.project) + self._builds = BuildCollection(SHARED.project) self._buildMap: dict[str, QListWidgetItem] = {} self.setWindowTitle(self.tr("Build Manuscript")) self.setMinimumWidth(CONFIG.pxInt(600)) self.setMinimumHeight(CONFIG.pxInt(500)) - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize wWin = CONFIG.pxInt(900) hWin = CONFIG.pxInt(600) - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options self.resize( CONFIG.pxInt(pOptions.getInt("GuiManuscript", "winWidth", wWin)), CONFIG.pxInt(pOptions.getInt("GuiManuscript", "winHeight", hWin)) @@ -105,21 +105,21 @@ class GuiManuscript(QDialog): ).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue()) self.tbAdd = QToolButton(self) - self.tbAdd.setIcon(CONFIG.theme.getIcon("add")) + self.tbAdd.setIcon(SHARED.theme.getIcon("add")) self.tbAdd.setIconSize(QSize(iPx, iPx)) self.tbAdd.setToolTip(self.tr("Add New Build")) self.tbAdd.setStyleSheet(buttonStyle) self.tbAdd.clicked.connect(self._createNewBuild) self.tbDel = QToolButton(self) - self.tbDel.setIcon(CONFIG.theme.getIcon("remove")) + self.tbDel.setIcon(SHARED.theme.getIcon("remove")) self.tbDel.setIconSize(QSize(iPx, iPx)) self.tbDel.setToolTip(self.tr("Delete Selected Build")) self.tbDel.setStyleSheet(buttonStyle) self.tbDel.clicked.connect(self._deleteSelectedBuild) self.tbEdit = QToolButton(self) - self.tbEdit.setIcon(CONFIG.theme.getIcon("edit")) + self.tbEdit.setIcon(SHARED.theme.getIcon("edit")) self.tbEdit.setIconSize(QSize(iPx, iPx)) self.tbEdit.setToolTip(self.tr("Edit Selected Build")) self.tbEdit.setStyleSheet(buttonStyle) @@ -163,7 +163,7 @@ class GuiManuscript(QDialog): # Assemble GUI # ============ - self.docPreview = _PreviewWidget(self.mainGui) + self.docPreview = _PreviewWidget(self) self.controlBox = QVBoxLayout() self.controlBox.addLayout(self.listToolBox, 0) @@ -210,7 +210,7 @@ class GuiManuscript(QDialog): self._updateBuildsList() logger.debug("Loading build cache") - cache = CONFIG.dataPath("cache") / f"build_{self.mainGui.project.data.uuid}.json" + cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json" if cache.is_file(): try: with open(cache, mode="r", encoding="utf-8") as fObj: @@ -268,7 +268,7 @@ class GuiManuscript(QDialog): """Delete the currently selected build settings entry.""" build = self._getSelectedBuild() if build is not None: - if self.mainGui.askQuestion(self.tr("Delete build '{0}'?".format(build.name))): + if SHARED.question(self.tr("Delete build '{0}'?".format(build.name))): self._builds.removeBuild(build.buildID) self._updateBuildsList() return @@ -289,7 +289,7 @@ class GuiManuscript(QDialog): if build is None: return - docBuild = NWBuildDocument(self.mainGui.project, build) + docBuild = NWBuildDocument(SHARED.project, build) docBuild.queueAll() self.docPreview.beginNewBuild(len(docBuild)) @@ -309,7 +309,7 @@ class GuiManuscript(QDialog): self._updatePreview(result, build) logger.debug("Saving build cache") - cache = CONFIG.dataPath("cache") / f"build_{self.mainGui.project.data.uuid}.json" + cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json" try: with open(cache, mode="w+", encoding="utf-8") as outFile: outFile.write(json.dumps(result, indent=2)) @@ -325,7 +325,7 @@ class GuiManuscript(QDialog): """Open the build dialog and build the manuscript.""" build = self._getSelectedBuild() if isinstance(build, BuildSettings): - dlgBuild = GuiManuscriptBuild(self, self.mainGui, build) + dlgBuild = GuiManuscriptBuild(self, build) dlgBuild.exec_() # After the build is done, save build settings changes @@ -390,7 +390,7 @@ class GuiManuscript(QDialog): optsWidth = CONFIG.rpxInt(mainSplit[0]) viewWidth = CONFIG.rpxInt(mainSplit[1]) - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options pOptions.setValue("GuiManuscript", "winWidth", winWidth) pOptions.setValue("GuiManuscript", "winHeight", winHeight) pOptions.setValue("GuiManuscript", "optsWidth", optsWidth) @@ -426,7 +426,7 @@ class GuiManuscript(QDialog): for key, name in self._builds.builds(): bItem = QListWidgetItem() bItem.setText(name) - bItem.setIcon(CONFIG.theme.getIcon("export")) + bItem.setIcon(SHARED.theme.getIcon("export")) bItem.setData(self.D_KEY, key) self.buildList.addItem(bItem) self._buildMap[key] = bItem @@ -446,10 +446,8 @@ class GuiManuscript(QDialog): class _PreviewWidget(QTextBrowser): - def __init__(self, mainGui: GuiMain): - super().__init__(parent=mainGui) - - self.mainGui = mainGui + def __init__(self, parent: QWidget): + super().__init__(parent=parent) self._docTime = 0 self._buildName = "" @@ -460,7 +458,7 @@ class _PreviewWidget(QTextBrowser): dPalette.setColor(QPalette.Text, QColor(0, 0, 0)) self.setPalette(dPalette) - self.setMinimumWidth(40*CONFIG.theme.textNWidth) + self.setMinimumWidth(40*SHARED.theme.textNWidth) self.setTextFont(CONFIG.textFont, CONFIG.textSize) self.setTabStopDistance(CONFIG.getTabWidth()) self.setOpenExternalLinks(False) @@ -478,7 +476,7 @@ class _PreviewWidget(QTextBrowser): aPalette.setColor(QPalette.Foreground, aPalette.toolTipText().color()) aFont = self.font() - aFont.setPointSizeF(0.9*CONFIG.theme.fontPointSize) + aFont.setPointSizeF(0.9*SHARED.theme.fontPointSize) self.ageLabel = QLabel("", self) self.ageLabel.setIndent(0) @@ -486,7 +484,7 @@ class _PreviewWidget(QTextBrowser): self.ageLabel.setPalette(aPalette) self.ageLabel.setAutoFillBackground(True) self.ageLabel.setAlignment(Qt.AlignCenter) - self.ageLabel.setFixedHeight(int(2.1*CONFIG.theme.fontPixelSize)) + self.ageLabel.setFixedHeight(int(2.1*SHARED.theme.fontPixelSize)) # Progress self.buildProgress = NProgressCircle(self, CONFIG.pxInt(160), CONFIG.pxInt(16)) diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 62eaba27..842114a9 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -39,7 +39,7 @@ from PyQt5.QtWidgets import ( QWidget ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.constants import nwHeadFmt, nwLabels, trConst from novelwriter.core.buildsettings import BuildSettings, FilterMode from novelwriter.extensions.switch import NSwitch @@ -76,8 +76,6 @@ class GuiBuildSettings(QDialog): if CONFIG.osDarwin: self.setWindowFlag(Qt.WindowType.Tool) - self.mainGui = mainGui - self._build = build self.setWindowTitle(self.tr("Manuscript Build Settings")) @@ -88,7 +86,7 @@ class GuiBuildSettings(QDialog): wWin = CONFIG.pxInt(750) hWin = CONFIG.pxInt(550) - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options self.resize( CONFIG.pxInt(pOptions.getInt("GuiBuildSettings", "winWidth", wWin)), CONFIG.pxInt(pOptions.getInt("GuiBuildSettings", "winHeight", hWin)) @@ -100,7 +98,7 @@ class GuiBuildSettings(QDialog): self.optSideBar = NPagedSideBar(self) self.optSideBar.setMinimumWidth(mPx) self.optSideBar.setMaximumWidth(mPx) - self.optSideBar.setLabelColor(CONFIG.theme.helpText) + self.optSideBar.setLabelColor(SHARED.theme.helpText) self.optSideBar.addLabel(self.tr("Options")) self.optSideBar.addButton(self.tr("Selection"), self.OPT_FILTERS) @@ -245,7 +243,7 @@ class GuiBuildSettings(QDialog): whether the user wants to save them. """ if self._build.changed: - response = self.mainGui.askQuestion(self.tr( + response = SHARED.question(self.tr( "Do you want to save your changes to '{0}'?".format(self._build.name) )) if response: @@ -262,7 +260,7 @@ class GuiBuildSettings(QDialog): treeWidth, filterWidth = self.optTabSelect.mainSplitSizes() - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options pOptions.setValue("GuiBuildSettings", "winWidth", winWidth) pOptions.setValue("GuiBuildSettings", "winHeight", winHeight) pOptions.setValue("GuiBuildSettings", "treeWidth", treeWidth) @@ -303,16 +301,14 @@ class _FilterTab(QWidget): def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: super().__init__(parent=buildMain) - self.mainGui = buildMain.mainGui - self._treeMap: dict[str, QTreeWidgetItem] = {} self._build = build self._statusFlags: dict[int, QIcon] = { self.F_NONE: QIcon(), - self.F_FILTERED: CONFIG.theme.getIcon("build_filtered"), - self.F_INCLUDED: CONFIG.theme.getIcon("build_included"), - self.F_EXCLUDED: CONFIG.theme.getIcon("build_excluded"), + self.F_FILTERED: SHARED.theme.getIcon("build_filtered"), + self.F_INCLUDED: SHARED.theme.getIcon("build_included"), + self.F_EXCLUDED: SHARED.theme.getIcon("build_excluded"), } self._trIncluded = self.tr("Included in manuscript") @@ -322,7 +318,7 @@ class _FilterTab(QWidget): # ============ # Tree Settings - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize cMg = CONFIG.pxInt(6) # Tree Widget @@ -360,7 +356,7 @@ class _FilterTab(QWidget): self.resetButton = QToolButton(self) self.resetButton.setToolTip(self.tr("Reset to default")) - self.resetButton.setIcon(CONFIG.theme.getIcon("revert")) + self.resetButton.setIcon(SHARED.theme.getIcon("revert")) self.resetButton.clicked.connect(lambda: self._setSelectedMode(self.F_FILTERED)) self.modeBox = QHBoxLayout() @@ -379,7 +375,7 @@ class _FilterTab(QWidget): # Assemble GUI # ============ - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options self.selectionBox = QVBoxLayout() self.selectionBox.addWidget(self.optTree) @@ -445,7 +441,7 @@ class _FilterTab(QWidget): logger.debug("Building project tree") self._treeMap = {} self.optTree.clear() - for nwItem in self.mainGui.project.getProjectItems(): + for nwItem in SHARED.project.iterProjectItems(): tHandle = nwItem.itemHandle pHandle = nwItem.itemParent @@ -461,7 +457,7 @@ class _FilterTab(QWidget): continue hLevel = nwItem.mainHeading - itemIcon = CONFIG.theme.getItemIcon( + itemIcon = SHARED.theme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel ) @@ -475,7 +471,7 @@ class _FilterTab(QWidget): trItem.setText(self.C_NAME, nwItem.itemName) trItem.setData(self.C_DATA, self.D_HANDLE, tHandle) trItem.setData(self.C_DATA, self.D_FILE, isFile) - trItem.setIcon(self.C_ACTIVE, CONFIG.theme.getIcon(iconName)) + trItem.setIcon(self.C_ACTIVE, SHARED.theme.getIcon(iconName)) trItem.setTextAlignment(self.C_NAME, Qt.AlignLeft) @@ -499,19 +495,19 @@ class _FilterTab(QWidget): self.filterOpt.clear() self.filterOpt.addLabel(self._build.getLabel("filter")) self.filterOpt.addItem( - CONFIG.theme.getIcon("proj_scene"), + SHARED.theme.getIcon("proj_scene"), self._build.getLabel("filter.includeNovel"), "doc:filter.includeNovel", default=self._build.getBool("filter.includeNovel") ) self.filterOpt.addItem( - CONFIG.theme.getIcon("proj_note"), + SHARED.theme.getIcon("proj_note"), self._build.getLabel("filter.includeNotes"), "doc:filter.includeNotes", default=self._build.getBool("filter.includeNotes") ) self.filterOpt.addItem( - CONFIG.theme.getIcon("unchecked"), + SHARED.theme.getIcon("unchecked"), self._build.getLabel("filter.includeInactive"), "doc:filter.includeInactive", default=self._build.getBool("filter.includeInactive") @@ -521,9 +517,9 @@ class _FilterTab(QWidget): # Root Classes self.filterOpt.addLabel(self.tr("Select Root Folders")) - for tHandle, nwItem in self.mainGui.project.tree.iterRoots(None): + for tHandle, nwItem in SHARED.project.tree.iterRoots(None): if not nwItem.isInactiveClass(): - itemIcon = CONFIG.theme.getItemIcon( + itemIcon = SHARED.theme.getItemIcon( nwItem.itemType, nwItem.itemClass, nwItem.itemLayout ) self.filterOpt.addItem( @@ -557,7 +553,7 @@ class _FilterTab(QWidget): def _setTreeItemMode(self) -> None: """Update the filtered mode icon on all items.""" - filtered = self._build.buildItemFilter(self.mainGui.project) + filtered = self._build.buildItemFilter(SHARED.project) for tHandle, item in self._treeMap.items(): allow, mode = filtered.get(tHandle, (False, FilterMode.UNKNOWN)) if mode == FilterMode.INCLUDED: @@ -597,12 +593,10 @@ class _HeadingsTab(QWidget): def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: super().__init__(parent=buildMain) - self.mainGui = buildMain.mainGui - self._build = build self._editing = 0 - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize vSp = CONFIG.pxInt(12) bSp = CONFIG.pxInt(6) @@ -616,7 +610,7 @@ class _HeadingsTab(QWidget): self.fmtTitle = QLineEdit("") self.fmtTitle.setReadOnly(True) self.btnTitle = QToolButton() - self.btnTitle.setIcon(CONFIG.theme.getIcon("edit")) + self.btnTitle.setIcon(SHARED.theme.getIcon("edit")) self.btnTitle.clicked.connect(lambda: self._editHeading(self.EDIT_TITLE)) wrapTitle = QHBoxLayout() @@ -632,7 +626,7 @@ class _HeadingsTab(QWidget): self.fmtChapter = QLineEdit("") self.fmtChapter.setReadOnly(True) self.btnChapter = QToolButton() - self.btnChapter.setIcon(CONFIG.theme.getIcon("edit")) + self.btnChapter.setIcon(SHARED.theme.getIcon("edit")) self.btnChapter.clicked.connect(lambda: self._editHeading(self.EDIT_CHAPTER)) wrapChapter = QHBoxLayout() @@ -648,7 +642,7 @@ class _HeadingsTab(QWidget): self.fmtUnnumbered = QLineEdit("") self.fmtUnnumbered.setReadOnly(True) self.btnUnnumbered = QToolButton() - self.btnUnnumbered.setIcon(CONFIG.theme.getIcon("edit")) + self.btnUnnumbered.setIcon(SHARED.theme.getIcon("edit")) self.btnUnnumbered.clicked.connect(lambda: self._editHeading(self.EDIT_UNNUM)) wrapUnnumbered = QHBoxLayout() @@ -665,7 +659,7 @@ class _HeadingsTab(QWidget): self.fmtScene = QLineEdit("") self.fmtScene.setReadOnly(True) self.btnScene = QToolButton() - self.btnScene.setIcon(CONFIG.theme.getIcon("edit")) + self.btnScene.setIcon(SHARED.theme.getIcon("edit")) self.btnScene.clicked.connect(lambda: self._editHeading(self.EDIT_SCENE)) self.hdeScene = QLabel(self.tr("Hide")) self.hdeScene.setToolTip(sceneHideTip) @@ -692,7 +686,7 @@ class _HeadingsTab(QWidget): self.fmtSection = QLineEdit("") self.fmtSection.setReadOnly(True) self.btnSection = QToolButton() - self.btnSection.setIcon(CONFIG.theme.getIcon("edit")) + self.btnSection.setIcon(SHARED.theme.getIcon("edit")) self.btnSection.clicked.connect(lambda: self._editHeading(self.EDIT_SECTION)) self.hdeSection = QLabel(self.tr("Hide")) self.hdeSection.setToolTip(sectionHideTip) @@ -868,9 +862,9 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter): def __init__(self, document: QTextDocument) -> None: super().__init__(document) self._fmtSymbol = QTextCharFormat() - self._fmtSymbol.setForeground(QColor(*CONFIG.theme.colHead)) + self._fmtSymbol.setForeground(QColor(*SHARED.theme.colHead)) self._fmtFormat = QTextCharFormat() - self._fmtFormat.setForeground(QColor(*CONFIG.theme.colEmph)) + self._fmtFormat.setForeground(QColor(*SHARED.theme.colEmph)) return def highlightBlock(self, text: str) -> None: @@ -896,7 +890,7 @@ class _ContentTab(QWidget): self._build = build - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize # Left Form # ========= @@ -964,14 +958,13 @@ class _FormatTab(QWidget): super().__init__(parent=buildMain) self.buildMain = buildMain - self.mainGui = buildMain.mainGui self._build = build self._unitScale = 1.0 - iPx = CONFIG.theme.baseIconSize - spW = 6*CONFIG.theme.textNWidth - dbW = 8*CONFIG.theme.textNWidth + iPx = SHARED.theme.baseIconSize + spW = 6*SHARED.theme.textNWidth + dbW = 8*SHARED.theme.textNWidth # Text Format Form # ================ @@ -992,7 +985,7 @@ class _FormatTab(QWidget): self.textFont = QLineEdit() self.textFont.setReadOnly(True) self.btnTextFont = QPushButton("...") - self.btnTextFont.setMaximumWidth(int(2.5*CONFIG.theme.getTextWidth("..."))) + self.btnTextFont.setMaximumWidth(int(2.5*SHARED.theme.getTextWidth("..."))) self.btnTextFont.clicked.connect(self._selectFont) self.formFormat.addRow( self._build.getLabel("format.textFont"), self.textFont, button=self.btnTextFont @@ -1278,7 +1271,7 @@ class _OutputTab(QWidget): self._build = build - iPx = CONFIG.theme.baseIconSize + iPx = SHARED.theme.baseIconSize # Left Form # ========= diff --git a/novelwriter/tools/projwizard.py b/novelwriter/tools/projwizard.py index 6564320a..ef715847 100644 --- a/novelwriter/tools/projwizard.py +++ b/novelwriter/tools/projwizard.py @@ -32,7 +32,7 @@ from PyQt5.QtWidgets import ( QPushButton, QRadioButton, QSpinBox, QVBoxLayout, QWizard, QWizardPage ) -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.common import makeFileNameSafe from novelwriter.extensions.switch import NSwitch @@ -55,7 +55,7 @@ class GuiProjectWizard(QWizard): self.mainGui = mainGui - self.sideImage = CONFIG.theme.loadDecoration( + self.sideImage = SHARED.theme.loadDecoration( "wiz-back", None, CONFIG.pxInt(370) ) self.setWizardStyle(QWizard.ModernStyle) @@ -104,7 +104,7 @@ class ProjWizardIntroPage(QWizardPage): "Peter Mitterhofer", "CC BY-SA 4.0" )) lblFont = self.imgCredit.font() - lblFont.setPointSizeF(0.6*CONFIG.theme.fontPointSize) + lblFont.setPointSizeF(0.6*SHARED.theme.fontPointSize) self.imgCredit.setFont(lblFont) xW = CONFIG.pxInt(300) @@ -172,7 +172,7 @@ class ProjWizardFolderPage(QWizardPage): self.projPath.setPlaceholderText(self.tr("Required")) self.browseButton = QPushButton("...") - self.browseButton.setMaximumWidth(int(2.5*CONFIG.theme.getTextWidth("..."))) + self.browseButton.setMaximumWidth(int(2.5*SHARED.theme.getTextWidth("..."))) self.browseButton.clicked.connect(self._doBrowse) self.errLabel = QLabel("") diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index fb995ef2..74b11da7 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -36,8 +36,7 @@ from PyQt5.QtWidgets import ( QLabel, QGroupBox, QMenu, QAction, QFileDialog, QSpinBox, QHBoxLayout ) -from novelwriter import CONFIG -from novelwriter.enum import nwAlert +from novelwriter import CONFIG, SHARED from novelwriter.error import formatException from novelwriter.common import formatTime, checkInt, checkIntTuple, minmax from novelwriter.constants import nwConst @@ -72,14 +71,12 @@ class GuiWritingStats(QDialog): if CONFIG.osDarwin: self.setWindowFlag(Qt.WindowType.Tool) - self.mainGui = mainGui - self.logData = [] self.filterData = [] self.timeFilter = 0.0 self.wordOffset = 0 - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options self.setWindowTitle(self.tr("Writing Statistics")) self.setMinimumWidth(CONFIG.pxInt(420)) @@ -132,7 +129,7 @@ class GuiWritingStats(QDialog): self.listBox.setSortingEnabled(True) # Word Bar - self.barHeight = int(round(0.5*CONFIG.theme.fontPixelSize)) + self.barHeight = int(round(0.5*SHARED.theme.fontPixelSize)) self.barWidth = CONFIG.pxInt(200) self.barImage = QPixmap(self.barHeight, self.barHeight) self.barImage.fill(self.palette().highlight().color()) @@ -143,27 +140,27 @@ class GuiWritingStats(QDialog): self.infoBox.setLayout(self.infoForm) self.labelTotal = QLabel(formatTime(0)) - self.labelTotal.setFont(CONFIG.theme.guiFontFixed) + self.labelTotal.setFont(SHARED.theme.guiFontFixed) self.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.labelIdleT = QLabel(formatTime(0)) - self.labelIdleT.setFont(CONFIG.theme.guiFontFixed) + self.labelIdleT.setFont(SHARED.theme.guiFontFixed) self.labelIdleT.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.labelFilter = QLabel(formatTime(0)) - self.labelFilter.setFont(CONFIG.theme.guiFontFixed) + self.labelFilter.setFont(SHARED.theme.guiFontFixed) self.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.novelWords = QLabel("0") - self.novelWords.setFont(CONFIG.theme.guiFontFixed) + self.novelWords.setFont(SHARED.theme.guiFontFixed) self.novelWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.notesWords = QLabel("0") - self.notesWords.setFont(CONFIG.theme.guiFontFixed) + self.notesWords.setFont(SHARED.theme.guiFontFixed) self.notesWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.totalWords = QLabel("0") - self.totalWords.setFont(CONFIG.theme.guiFontFixed) + self.totalWords.setFont(SHARED.theme.guiFontFixed) self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) lblTTime = QLabel(self.tr("Total Time:")) @@ -190,7 +187,7 @@ class GuiWritingStats(QDialog): self.infoForm.setRowStretch(6, 1) # Filter Options - sPx = CONFIG.theme.baseIconSize + sPx = SHARED.theme.baseIconSize self.filterBox = QGroupBox(self.tr("Filters"), self) self.filterForm = QGridLayout(self) @@ -333,7 +330,7 @@ class GuiWritingStats(QDialog): showIdleTime = self.showIdleTime.isChecked() histMax = self.histMax.value() - pOptions = self.mainGui.project.options + pOptions = SHARED.project.options pOptions.setValue("GuiWritingStats", "winWidth", winWidth) pOptions.setValue("GuiWritingStats", "winHeight", winHeight) pOptions.setValue("GuiWritingStats", "widthCol0", widthCol0) @@ -413,14 +410,14 @@ class GuiWritingStats(QDialog): # Report to user if wSuccess: - self.mainGui.makeAlert( + SHARED.info( self.tr("{0} file successfully written to:").format(textFmt), info=savePath ) else: - self.mainGui.makeAlert( + SHARED.error( self.tr("Failed to write {0} file.").format(textFmt), - info=errMsg, level=nwAlert.ERROR + info=errMsg ) return wSuccess @@ -441,7 +438,7 @@ class GuiWritingStats(QDialog): ttTime = 0 ttIdle = 0 - for record in self.mainGui.project.session.iterRecords(): + for record in SHARED.project.session.iterRecords(): rType = record.get("type") if rType == "initial": self.wordOffset = checkInt(record.get("offset"), 0) @@ -587,13 +584,13 @@ class GuiWritingStats(QDialog): newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight) newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter) - newItem.setFont(self.C_TIME, CONFIG.theme.guiFontFixed) - newItem.setFont(self.C_LENGTH, CONFIG.theme.guiFontFixed) - newItem.setFont(self.C_COUNT, CONFIG.theme.guiFontFixed) + newItem.setFont(self.C_TIME, SHARED.theme.guiFontFixed) + newItem.setFont(self.C_LENGTH, SHARED.theme.guiFontFixed) + newItem.setFont(self.C_COUNT, SHARED.theme.guiFontFixed) if showIdleTime: - newItem.setFont(self.C_IDLE, CONFIG.theme.guiFontFixed) + newItem.setFont(self.C_IDLE, SHARED.theme.guiFontFixed) else: - newItem.setFont(self.C_IDLE, CONFIG.theme.guiFont) + newItem.setFont(self.C_IDLE, SHARED.theme.guiFont) self.listBox.addTopLevelItem(newItem) self.timeFilter += sDiff diff --git a/tests/conftest.py b/tests/conftest.py index 42d0afb4..4d6e6e72 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -22,17 +22,18 @@ along with this program. If not, see . import sys import pytest import shutil +import logging from pathlib import Path -from mocked import MockGuiMain from tools import cleanProject +from mocked import MockGuiMain, MockTheme from PyQt5.QtWidgets import QMessageBox sys.path.insert(1, str(Path(__file__).parent.parent.absolute())) -from novelwriter import CONFIG, main # noqa: E402 +from novelwriter import CONFIG, SHARED, main # noqa: E402 _TST_ROOT = Path(__file__).parent _TMP_ROOT = _TST_ROOT / "temp" @@ -62,6 +63,7 @@ def resetConfigVars(): @pytest.fixture(scope="session", autouse=True) def sessionFixture(): """A session wide fixture to set up the test environment.""" + logging.root.setLevel(logging.INFO) if _TMP_ROOT.exists(): shutil.rmtree(_TMP_ROOT) _TMP_ROOT.mkdir() @@ -81,6 +83,7 @@ def functionFixture(qtbot): CONFIG.__init__() CONFIG.initConfig(confPath=_TMP_CONF, dataPath=_TMP_CONF) resetConfigVars() + logging.getLogger("novelwriter").setLevel(logging.INFO) return @@ -136,10 +139,15 @@ def projPath(fncPath): @pytest.fixture(scope="function") -def mockGUI(): +def mockGUI(qtbot, monkeypatch): """Create a mock instance of novelWriter's main GUI class.""" - theGui = MockGuiMain() - return theGui + monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None) + monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes) + gui = MockGuiMain() + theme = MockTheme() + monkeypatch.setattr(SHARED, "_gui", gui) + monkeypatch.setattr(SHARED, "_theme", theme) + return gui @pytest.fixture(scope="function") diff --git a/tests/mocked.py b/tests/mocked.py index cb25a51b..a22258f5 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -19,49 +19,25 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ -from PyQt5.QtCore import QObject +from PyQt5.QtGui import QPixmap +from PyQt5.QtWidgets import QWidget # =========================================================================== # # Mock GUI # =========================================================================== # -class MockGuiMain(QObject): +class MockGuiMain(QWidget): def __init__(self): super().__init__() - - self._project = None - - self.hasProject = True self.mainStatus = MockStatusBar() self.projPath = "" - - # Test Variables - self.askResponse = True - self.lastAlert = "" - self.lastQuestion = "" - return - @property - def project(self): - return self._project - def postLaunchTasks(self, cmdOpen): return - def makeAlert(self, text, info="", detals="", level=0, exception=None): - assert isinstance(text, str) - print("%s: %s" % (str(level), text)) - self.lastAlert = str(text) - return - - def askQuestion(self, text, info="", details="", level=3): - print("Question: %s" % text) - self.lastQuestion = text - return self.askResponse - def setStatus(self, theMessage): return @@ -78,16 +54,6 @@ class MockGuiMain(QObject): def close(self): return "close" - # Test Functions - - def undo(self): - self.askResponse = True - return - - def clear(self): - self.lastAlert = "" - return - # END Class MockGuiMain @@ -99,12 +65,24 @@ class MockStatusBar: def setStatus(self, theText): return - def doUpdateProjectStatus(self, theStatus): + def updateProjectStatus(self, theStatus): return # END Class MockStatusBar +class MockTheme: + + def __init__(self): + self.baseIconSize = 10 + return + + def getPixmap(self, *a): + return QPixmap() + +# END Class MockTheme + + class MockApp: def __init__(self): diff --git a/tests/test_base/test_base_common.py b/tests/test_base/test_base_common.py index 3b9c4f3a..278ce145 100644 --- a/tests/test_base/test_base_common.py +++ b/tests/test_base/test_base_common.py @@ -24,6 +24,7 @@ import pytest import hashlib from pathlib import Path +from xml.etree import ElementTree as ET from tools import writeFile from mocked import causeOSError @@ -35,12 +36,12 @@ from novelwriter.common import ( formatTimeStamp, fuzzyTime, getGuiItem, hexToInt, isHandle, isItemClass, isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax, numberToRoman, NWConfigParser, readTextFile, sha256sum, simplified, - transferCase, yesNo + transferCase, xmlIndent, yesNo ) @pytest.mark.base -def testBaseCommon_CheckStringNone(): +def testBaseCommon_checkStringNone(): """Test the checkStringNone function.""" assert checkStringNone("Stuff", "NotNone") == "Stuff" assert checkStringNone("None", "NotNone") is None @@ -49,11 +50,11 @@ def testBaseCommon_CheckStringNone(): assert checkStringNone(1.0, "NotNone") == "NotNone" assert checkStringNone(True, "NotNone") == "NotNone" -# END Test testBaseCommon_CheckStringNone +# END Test testBaseCommon_checkStringNone @pytest.mark.base -def testBaseCommon_CheckString(): +def testBaseCommon_checkString(): """Test the checkString function. Anything that is a string should be returned, otherwise it returns the default. """ @@ -64,11 +65,11 @@ def testBaseCommon_CheckString(): assert checkString(1.0, "default") == "default" assert checkString(True, "default") == "default" -# END Test testBaseCommon_CheckString +# END Test testBaseCommon_checkString @pytest.mark.base -def testBaseCommon_CheckInt(): +def testBaseCommon_checkInt(): """Test the checkInt function. Anything that can be converted to an integer should be returned, otherwise it returns the default. """ @@ -80,11 +81,11 @@ def testBaseCommon_CheckInt(): assert checkInt("1", 3) == 1 assert checkInt("1.0", 3) == 3 -# END Test testBaseCommon_CheckInt +# END Test testBaseCommon_checkInt @pytest.mark.base -def testBaseCommon_CheckFloat(): +def testBaseCommon_checkFloat(): """Test the checkFloat function. Anything that can be converted to an integer should be returned, otherwise it returns the default. """ @@ -96,11 +97,11 @@ def testBaseCommon_CheckFloat(): assert checkFloat("1", 3.0) == 1.0 assert checkFloat("1.0", 3.0) == 1.0 -# END Test testBaseCommon_CheckInt +# END Test testBaseCommon_checkFloat @pytest.mark.base -def testBaseCommon_CheckBool(): +def testBaseCommon_checkBool(): """Test the checkBool function. Any bool, string version of Python bool, or integer 1 or 0, are returned as bool. Otherwise, the default is returned. @@ -145,11 +146,11 @@ def testBaseCommon_CheckBool(): assert checkBool(2.0, True) is True assert checkBool(2.0, False) is False -# END Test testBaseCommon_CheckBool +# END Test testBaseCommon_checkBool @pytest.mark.base -def testBaseCommon_CheckHandle(): +def testBaseCommon_checkHandle(): """Test the checkHandle function.""" assert checkHandle("None", 1, True) is None assert checkHandle("None", 1, False) == 1 @@ -158,36 +159,36 @@ def testBaseCommon_CheckHandle(): assert checkHandle("47666c91c7ccf", None, False) == "47666c91c7ccf" assert checkHandle("h7666c91c7ccf", None, False) is None -# END Test testBaseCommon_CheckHandle +# END Test testBaseCommon_checkHandle @pytest.mark.base -def testBaseCommon_CheckUuid(): +def testBaseCommon_checkUuid(): """Test the checkUuid function.""" testUuid = "e2be99af-f9bf-4403-857a-c3d1ac25abea" - assert checkUuid("", None) is None - assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abe", None) is None - assert checkUuid("e2be99af-f9bf-qq03-857a-c3d1ac25abea", None) is None - assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abeaa", None) is None - assert checkUuid(testUuid, None) == testUuid + assert checkUuid("", None) is None # type: ignore + assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abe", None) is None # type: ignore + assert checkUuid("e2be99af-f9bf-qq03-857a-c3d1ac25abea", None) is None # type: ignore + assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abeaa", None) is None # type: ignore + assert checkUuid(testUuid, None) == testUuid # type: ignore -# END Test testBaseCommon_CheckUuid +# END Test testBaseCommon_checkUuid @pytest.mark.base -def testBaseCommon_CheckPath(): +def testBaseCommon_checkPath(): """Test the checkPath function.""" - assert checkPath(Path("test"), None) == Path("test") - assert checkPath("test", None) == Path("test") - assert checkPath(None, None) is None - assert checkPath("", None) is None - assert checkPath(" ", None) is None + assert checkPath(Path("test"), None) == Path("test") # type: ignore + assert checkPath("test", None) == Path("test") # type: ignore + assert checkPath(None, None) is None # type: ignore + assert checkPath("", None) is None # type: ignore + assert checkPath(" ", None) is None # type: ignore -# END Test testBaseCommon_CheckPath +# END Test testBaseCommon_checkPath @pytest.mark.base -def testBaseCommon_IsHandle(): +def testBaseCommon_isHandle(): """Test the isHandle function.""" assert isHandle("47666c91c7ccf") is True assert isHandle("47666C91C7CCF") is False @@ -196,12 +197,12 @@ def testBaseCommon_IsHandle(): assert isHandle(None) is False assert isHandle("STUFF") is False -# END Test testBaseCommon_IsHandle +# END Test testBaseCommon_isHandle @pytest.mark.base -def testBaseCommon_IsTitleTag(): - """Test the isItemClass function.""" +def testBaseCommon_isTitleTag(): + """Test the isTitleTag function.""" assert isTitleTag("T1234") is True assert isTitleTag("t1234") is False @@ -213,11 +214,11 @@ def testBaseCommon_IsTitleTag(): assert isTitleTag(None) is False assert isTitleTag("STUFF") is False -# END Test testBaseCommon_IsTitleTag +# END Test testBaseCommon_isTitleTag @pytest.mark.base -def testBaseCommon_IsItemClass(): +def testBaseCommon_isItemClass(): """Test the isItemClass function.""" assert isItemClass("NO_CLASS") is True assert isItemClass("NOVEL") is True @@ -233,14 +234,14 @@ def testBaseCommon_IsItemClass(): # Invalid assert isItemClass("None") is False - assert isItemClass(None) is False + assert isItemClass(None) is False # type: ignore assert isItemClass("STUFF") is False -# END Test testBaseCommon_IsItemClass +# END Test testBaseCommon_isItemClass @pytest.mark.base -def testBaseCommon_IsItemType(): +def testBaseCommon_isItemType(): """Test the isItemType function.""" assert isItemType("NO_TYPE") is True assert isItemType("ROOT") is True @@ -252,14 +253,14 @@ def testBaseCommon_IsItemType(): # Invalid assert isItemType("None") is False - assert isItemType(None) is False + assert isItemType(None) is False # type: ignore assert isItemType("STUFF") is False -# END Test testBaseCommon_IsItemType +# END Test testBaseCommon_isItemType @pytest.mark.base -def testBaseCommon_IsItemLayout(): +def testBaseCommon_isItemLayout(): """Test the isItemLayout function.""" assert isItemLayout("NO_LAYOUT") is True assert isItemLayout("DOCUMENT") is True @@ -276,14 +277,14 @@ def testBaseCommon_IsItemLayout(): # Invalid assert isItemLayout("None") is False - assert isItemLayout(None) is False + assert isItemLayout(None) is False # type: ignore assert isItemLayout("STUFF") is False -# END Test testBaseCommon_IsItemLayout +# END Test testBaseCommon_isItemLayout @pytest.mark.base -def testBaseCommon_HexToInt(): +def testBaseCommon_hexToInt(): """Test the hexToInt function.""" assert hexToInt(1) == 0 assert hexToInt("1") == 1 @@ -292,42 +293,42 @@ def testBaseCommon_HexToInt(): assert hexToInt("0xffffq") == 0 assert hexToInt("0xffffq", 12) == 12 -# END Test testBaseCommon_HexToInt +# END Test testBaseCommon_hexToInt @pytest.mark.base -def testBaseCommon_MinMax(): +def testBaseCommon_minmax(): """Test the minmax function.""" for i in range(-5, 15): assert 0 <= minmax(i, 0, 10) <= 10 -# END Test testBaseCommon_MinMax +# END Test testBaseCommon_minmax @pytest.mark.base -def testBaseCommon_CheckIntTuple(): +def testBaseCommon_checkIntTuple(): """Test the checkIntTuple function.""" assert checkIntTuple(0, (0, 1, 2), 3) == 0 assert checkIntTuple(5, (0, 1, 2), 3) == 3 -# END Test testBaseCommon_CheckIntTuple +# END Test testBaseCommon_checkIntTuple @pytest.mark.base -def testBaseCommon_FormatTimeStamp(): +def testBaseCommon_formatTimeStamp(): """Test the formatTimeStamp function.""" tTime = time.mktime(time.gmtime(0)) assert formatTimeStamp(tTime, False) == "1970-01-01 00:00:00" assert formatTimeStamp(tTime, True) == "1970-01-01 00.00.00" -# END Test testBaseCommon_FormatTimeStamp +# END Test testBaseCommon_formatTimeStamp @pytest.mark.base -def testBaseCommon_FormatTime(): +def testBaseCommon_formatTime(): """Test the formatTime function.""" - assert formatTime("1") == "ERROR" - assert formatTime(1.0) == "ERROR" + assert formatTime("1") == "ERROR" # type: ignore + assert formatTime(1.0) == "ERROR" # type: ignore assert formatTime(1) == "00:00:01" assert formatTime(59) == "00:00:59" assert formatTime(60) == "00:01:00" @@ -342,21 +343,21 @@ def testBaseCommon_FormatTime(): assert formatTime(86400) == "1-00:00:00" assert formatTime(360000) == "4-04:00:00" -# END Test testBaseCommon_FormatTime +# END Test testBaseCommon_formatTime @pytest.mark.base -def testBaseCommon_Simplified(): +def testBaseCommon_simplified(): """Test the simplified function.""" assert simplified("Hello World") == "Hello World" assert simplified(" Hello World ") == "Hello World" assert simplified("\tHello\n\r\tWorld") == "Hello World" -# END Test testBaseCommon_Simplified +# END Test testBaseCommon_simplified @pytest.mark.base -def testBaseCommon_YesNo(): +def testBaseCommon_yesNo(): """Test the yesNo function.""" # Bool assert yesNo(True) == "yes" @@ -366,8 +367,8 @@ def testBaseCommon_YesNo(): assert yesNo(None) == "no" # String - assert yesNo("foo") == "yes" - assert yesNo("") == "no" + assert yesNo("foo") == "yes" # type: ignore + assert yesNo("") == "no" # type: ignore # Integer assert yesNo(0) == "no" @@ -375,15 +376,15 @@ def testBaseCommon_YesNo(): assert yesNo(2) == "yes" # Float - assert yesNo(0.0) == "no" - assert yesNo(1.0) == "yes" - assert yesNo(2.0) == "yes" + assert yesNo(0.0) == "no" # type: ignore + assert yesNo(1.0) == "yes" # type: ignore + assert yesNo(2.0) == "yes" # type: ignore -# END Test testBaseCommon_YesNo +# END Test testBaseCommon_yesNo @pytest.mark.base -def testBaseCommon_FormatInt(): +def testBaseCommon_formatInt(): """Test the formatInt function.""" # Normal Cases assert formatInt(1) == "1" @@ -398,29 +399,29 @@ def testBaseCommon_FormatInt(): assert formatInt(1234567890) == "1.23\u2009G" # Exceptions - assert formatInt(12.3) == "ERR" - assert formatInt(None) == "ERR" - assert formatInt("42") == "ERR" + assert formatInt(12.3) == "ERR" # type: ignore + assert formatInt(None) == "ERR" # type: ignore + assert formatInt("42") == "ERR" # type: ignore -# END Test testBaseCommon_FormatInt +# END Test testBaseCommon_formatInt @pytest.mark.base -def testBaseCommon_TransferCase(): +def testBaseCommon_transferCase(): """Test the transferCase function.""" - assert transferCase(1, "TaRgEt") == "TaRgEt" - assert transferCase("source", 1) == 1 + assert transferCase(1, "TaRgEt") == "TaRgEt" # type: ignore + assert transferCase("source", 1) == 1 # type: ignore assert transferCase("", "TaRgEt") == "TaRgEt" assert transferCase("source", "") == "" assert transferCase("Source", "target") == "Target" assert transferCase("SOURCE", "target") == "TARGET" assert transferCase("source", "TARGET") == "target" -# END Test testBaseCommon_TransferCase +# END Test testBaseCommon_transferCase @pytest.mark.base -def testBaseCommon_FuzzyTime(): +def testBaseCommon_fuzzyTime(): """Test the fuzzyTime function.""" assert fuzzyTime(-1) == "in the future" assert fuzzyTime(0) == "just now" @@ -451,13 +452,13 @@ def testBaseCommon_FuzzyTime(): assert fuzzyTime(47336399) == "a year ago" assert fuzzyTime(47336400) == "2 years ago" -# END Test testBaseCommon_FuzzyTime +# END Test testBaseCommon_fuzzyTime @pytest.mark.core -def testBaseCommon_RomanNumbers(): +def testBaseCommon_numberToRoman(): """Test conversion of integers to Roman numbers.""" - assert numberToRoman(None, False) == "NAN" + assert numberToRoman(None, False) == "NAN" # type: ignore assert numberToRoman(0, False) == "OOR" assert numberToRoman(1, False) == "I" assert numberToRoman(2, False) == "II" @@ -478,14 +479,14 @@ def testBaseCommon_RomanNumbers(): assert numberToRoman(2010, False) == "MMX" assert numberToRoman(999, True) == "cmxcix" -# END Test testBaseCommon_RomanNumbers +# END Test testBaseCommon_numberToRoman @pytest.mark.base -def testBaseCommon_JsonEncode(): +def testBaseCommon_jsonEncode(): """Test the jsonEncode function.""" # Wrong type - assert jsonEncode(None) == "[]" + assert jsonEncode(None) == "[]" # type: ignore # Correct types assert jsonEncode([1, 2]) == "[\n 1,\n 2\n]" @@ -561,11 +562,38 @@ def testBaseCommon_JsonEncode(): '}' ) -# END Test testBaseCommon_JsonEncode +# END Test testBaseCommon_jsonEncode @pytest.mark.base -def testBaseCommon_ReadTextFile(monkeypatch, fncPath, ipsumText): +def testBaseCommon_xmlIndent(): + """Test the xmlIndent function.""" + xRoot = ET.fromstring( + "" + "" + "foo" + "" + "" + ) + xmlIndent(ET.ElementTree(xRoot)) + assert ET.tostring(xRoot) == ( + b"\n" + b" \n" + b" foo\n" + b" \n" + b"\n" + ) + + # If we send nonsense, nothing is done + data = "foobar" + xmlIndent(data) # type: ignore + assert data == "foobar" + +# END Test testBaseCommon_xmlIndent + + +@pytest.mark.base +def testBaseCommon_readTextFile(monkeypatch, fncPath, ipsumText): """Test the readTextFile function.""" testText = "\n\n".join(ipsumText) + "\n" testFile = fncPath / "ipsum.txt" @@ -578,11 +606,11 @@ def testBaseCommon_ReadTextFile(monkeypatch, fncPath, ipsumText): mp.setattr("pathlib.Path.read_text", causeOSError) assert readTextFile(testFile) == "" -# END Test testBaseCommon_ReadTextFile +# END Test testBaseCommon_readTextFile @pytest.mark.base -def testBaseCommon_MakeFileNameSafe(): +def testBaseCommon_makeFileNameSafe(): """Test the makeFileNameSafe function.""" assert makeFileNameSafe(" aaaa ") == "aaaa" assert makeFileNameSafe("aaaa,bbbb") == "aaaabbbb" @@ -591,11 +619,11 @@ def testBaseCommon_MakeFileNameSafe(): assert makeFileNameSafe("æøå") == "æøå" assert makeFileNameSafe("Stuff œfi2⁵") == "Stuff œfi25" -# END Test testBaseCommon_MakeFileNameSafe +# END Test testBaseCommon_makeFileNameSafe @pytest.mark.base -def testBaseCommon_Sha256Sum(monkeypatch, fncPath, ipsumText): +def testBaseCommon_sha256sum(monkeypatch, fncPath, ipsumText): """Test the sha256sum function.""" longText = 50*(" ".join(ipsumText) + " ") shortText = "This is a short file" @@ -630,16 +658,16 @@ def testBaseCommon_Sha256Sum(monkeypatch, fncPath, ipsumText): assert sha256sum(shortFile) is None assert sha256sum(noneFile) is None -# END Test testBaseCommon_Sha256Sum +# END Test testBaseCommon_sha256sum @pytest.mark.base -def testBaseCommon_GetGuiItem(nwGUI): +def testBaseCommon_getGuiItem(nwGUI): """Check the GUI item function.""" assert getGuiItem("gibberish") is None assert isinstance(getGuiItem("GuiMain"), GuiMain) -# END Test testBaseCommon_GetGuiItem +# END Test testBaseCommon_getGuiItem @pytest.mark.base @@ -675,14 +703,14 @@ def testBaseCommon_NWConfigParser(fncPath): assert cfgParser.rdStr("main", "blabla", "stuff") == "stuff" # Read Boolean - assert cfgParser.rdBool("main", "boolopt1", None) is True - assert cfgParser.rdBool("main", "boolopt2", None) is True - assert cfgParser.rdBool("main", "boolopt3", None) is True - assert cfgParser.rdBool("main", "boolopt4", None) is False - assert cfgParser.rdBool("main", "intopt1", None) is None + assert cfgParser.rdBool("main", "boolopt1", None) is True # type: ignore + assert cfgParser.rdBool("main", "boolopt2", None) is True # type: ignore + assert cfgParser.rdBool("main", "boolopt3", None) is True # type: ignore + assert cfgParser.rdBool("main", "boolopt4", None) is False # type: ignore + assert cfgParser.rdBool("main", "intopt1", None) is None # type: ignore - assert cfgParser.rdBool("nope", "boolopt1", None) is None - assert cfgParser.rdBool("main", "blabla", None) is None + assert cfgParser.rdBool("nope", "boolopt1", None) is None # type: ignore + assert cfgParser.rdBool("main", "blabla", None) is None # type: ignore # Read Integer assert cfgParser.rdInt("main", "intopt1", 13) == 42 diff --git a/tests/test_base/test_base_init.py b/tests/test_base/test_base_init.py index b6e09d15..cbc20c51 100644 --- a/tests/test_base/test_base_init.py +++ b/tests/test_base/test_base_init.py @@ -30,8 +30,7 @@ from novelwriter import CONFIG, main, logger @pytest.mark.base def testBaseInit_Launch(caplog, monkeypatch, fncPath): - """Check launching the main GUI. - """ + """Check launching the main GUI.""" monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) # TestMode Launch @@ -80,8 +79,7 @@ def testBaseInit_Launch(caplog, monkeypatch, fncPath): @pytest.mark.base def testBaseInit_Options(monkeypatch, fncPath): - """Test command line options for logging level. - """ + """Test command line options for logging level.""" monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) monkeypatch.setattr(sys, "argv", [ "novelWriter.py", "--testmode", f"--config={fncPath}", f"--data={fncPath}" @@ -146,8 +144,7 @@ def testBaseInit_Options(monkeypatch, fncPath): @pytest.mark.base def testBaseInit_Imports(caplog, monkeypatch, fncPath): - """Check import error handling. - """ + """Check import error handling.""" monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *a: None) monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *a: 0) diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py new file mode 100644 index 00000000..80f80954 --- /dev/null +++ b/tests/test_base/test_base_shared.py @@ -0,0 +1,187 @@ +""" +novelWriter – SharedData Class Tester +===================================== + +This file is a part of novelWriter +Copyright 2018–2023, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" + +import pytest + +from mocked import MockGuiMain, MockTheme + +from PyQt5.QtWidgets import QMessageBox + +from novelwriter.core.project import NWProject +from novelwriter.shared import SharedData, _GuiAlert +from tests.tools import buildTestProject + + +@pytest.mark.base +def testBaseSharedData_Init(): + """Test SharedData class initialisation.""" + shared = SharedData() + + # When not initialised, it should raise exceptions + with pytest.raises(Exception): + shared.mainGui + with pytest.raises(Exception): + shared.theme + with pytest.raises(Exception): + shared.project + + # Create some mock objects + mockGui = MockGuiMain() + mockTheme = MockTheme() + assert mockGui is not mockTheme + + # Properly initialise the class + shared.initSharedData(mockGui, mockTheme) # type: ignore + + assert shared.mainGui is mockGui + assert shared.theme is mockTheme + + assert isinstance(shared.project, NWProject) + assert shared.hasProject is False + assert shared.projectIdleTime == 0.0 + assert shared.projectLock is None + + assert shared.alert is None + +# END Test testBaseSharedData_Init + + +@pytest.mark.base +def testBaseSharedData_Projects(fncPath, caplog: pytest.LogCaptureFixture): + """Test SharedData handling of projects.""" + project = NWProject() + buildTestProject(project, fncPath) + project.closeProject(0.0) # Clears the lockfile + + shared = SharedData() + assert shared._project is None + + # Initialise the instance, should create an empty project + mockGui = MockGuiMain() + mockTheme = MockTheme() + shared.initSharedData(mockGui, mockTheme) # type: ignore + assert isinstance(shared.project, NWProject) + assert shared.hasProject is False + + # Load the test project + assert shared.openProject(fncPath) is True + assert shared.hasProject is True + + # We cannot open two projects + caplog.clear() + assert shared.openProject(fncPath) is False + assert caplog.messages[-1] == "A project is already open" + assert shared._idleTime == 0.0 + + # Update idle time + refTime = shared._idleRefTime + shared.updateIdleTime(refTime + 1.0, False) + shared.updateIdleTime(refTime + 2.0, True) + shared.updateIdleTime(refTime + 3.0, False) + shared.updateIdleTime(refTime + 4.0, True) + assert round(shared.projectIdleTime) == 2 + + # Save project + assert shared.saveProject() is True + + # Close project + shared.closeProject() + assert shared.hasProject is False + + # Cannot save a project after it's been closed + assert shared.saveProject() is False + + # Check locked project info + project.openProject(fncPath) # First open with our independent project instance + assert shared.hasProject is False + assert shared.projectLock is None + assert shared.openProject(fncPath) is False # Then with out shared instance + assert shared.hasProject is False + assert isinstance(shared.projectLock, list) + +# END Test testBaseSharedData_Projects + + +@pytest.mark.base +def testBaseSharedData_Alerts(monkeypatch, caplog: pytest.LogCaptureFixture): + """Test SharedData class alert helper functions.""" + monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None) + monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.Yes) + + shared = SharedData() + + mockGui = MockGuiMain() + mockTheme = MockTheme() + shared.initSharedData(mockGui, mockTheme) # type: ignore + + assert shared.alert is None + + # Info box + caplog.clear() + shared.info("Hello World", info="foo", details="bar") + assert isinstance(shared.alert, _GuiAlert) + assert shared.alert.text() == "Hello World" + assert shared.alert.informativeText() == "foo" + assert shared.alert.detailedText() == "bar" + assert caplog.text.strip().startswith("INFO") + assert caplog.text.strip().endswith("Hello World foo bar") + shared._alert = None + + # Warning box + caplog.clear() + shared.warn("Oops!", info="foo", details="bar") + assert isinstance(shared.alert, _GuiAlert) + assert shared.alert.text() == "Oops!" + assert shared.alert.informativeText() == "foo" + assert shared.alert.detailedText() == "bar" + assert caplog.text.strip().startswith("WARNING") + assert caplog.text.strip().endswith("Oops! foo bar") + shared._alert = None + + # Error box + caplog.clear() + shared.error("Oh noes!", info="foo", details="bar") + assert isinstance(shared.alert, _GuiAlert) + assert shared.alert.text() == "Oh noes!" + assert shared.alert.informativeText() == "foo" + assert shared.alert.detailedText() == "bar" + assert caplog.text.strip().startswith("ERROR") + assert caplog.text.strip().endswith("Oh noes! foo bar") + shared._alert = None + + # Error box with exception + caplog.clear() + shared.error("Oh noes!", info="foo", details="bar", exc=Exception("Boom!")) + assert isinstance(shared.alert, _GuiAlert) + assert shared.alert.text() == "Oh noes!" + assert shared.alert.informativeText() == "foo
Exception: Boom!" + assert shared.alert.detailedText() == "bar" + assert caplog.text.strip().startswith("ERROR") + assert caplog.text.strip().endswith("Oh noes! foo bar") + shared._alert = None + + # Question box + assert shared.question("Why?") is True + assert isinstance(shared.alert, _GuiAlert) + assert shared.alert.text() == "Why?" + shared._alert = None + +# END Test testBaseSharedData_Alerts diff --git a/tests/test_core/test_core_buildsettings.py b/tests/test_core/test_core_buildsettings.py index e809d6d1..505e9bef 100644 --- a/tests/test_core/test_core_buildsettings.py +++ b/tests/test_core/test_core_buildsettings.py @@ -220,7 +220,7 @@ def testCoreBuildSettings_BuildValues(): @pytest.mark.core def testCoreBuildSettings_Filters(mockGUI, fncPath: Path, mockRnd): """Test filters for project items.""" - project = NWProject(mockGUI) + project = NWProject() buildTestProject(project, fncPath) build = BuildSettings() @@ -368,7 +368,7 @@ def testCoreBuildSettings_Filters(mockGUI, fncPath: Path, mockRnd): @pytest.mark.core def testCoreBuildSettings_Collection(monkeypatch, mockGUI, fncPath: Path, mockRnd): """Test the collections class for builds.""" - project = NWProject(mockGUI) + project = NWProject() buildTestProject(project, fncPath) buildsFile = project.storage.getMetaFile(nwFiles.BUILDS_FILE) assert isinstance(buildsFile, Path) diff --git a/tests/test_core/test_core_coretools.py b/tests/test_core/test_core_coretools.py index 04aae086..c5c90023 100644 --- a/tests/test_core/test_core_coretools.py +++ b/tests/test_core/test_core_coretools.py @@ -38,7 +38,7 @@ from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter, Pr @pytest.mark.core def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ipsumText): """Test the DocMerger utility.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) @@ -126,7 +126,7 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ip @pytest.mark.core def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText): """Test the DocSplitter utility.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) @@ -265,7 +265,7 @@ def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText) @pytest.mark.core def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd): """Test the DocDuplicator utility.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) @@ -290,7 +290,7 @@ def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd): assert list(dup.duplicate([C.hSceneDoc])) == [ ("0000000000010", C.hSceneDoc), # The Scene ] - assert theProject.tree._treeOrder == [ + assert theProject.tree._order == [ C.hNovelRoot, C.hPlotRoot, C.hCharRoot, C.hWorldRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010", @@ -311,7 +311,7 @@ def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd): ("0000000000012", None), # The Chapter ("0000000000013", None), # The Scene ] - assert theProject.tree._treeOrder == [ + assert theProject.tree._order == [ C.hNovelRoot, C.hPlotRoot, C.hCharRoot, C.hWorldRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010", @@ -342,7 +342,7 @@ def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd): ("0000000000017", None), # The Chapter ("0000000000018", None), # The Scene ] - assert theProject.tree._treeOrder == [ + assert theProject.tree._order == [ C.hNovelRoot, C.hPlotRoot, C.hCharRoot, C.hWorldRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010", @@ -410,7 +410,7 @@ def testCoreTools_NewMinimal(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): testFile = tstPaths.outDir / "coreTools_NewMinimal_nwProject.nwx" compFile = tstPaths.refDir / "coreTools_NewMinimal_nwProject.nwx" - projBuild = ProjectBuilder(mockGUI) + projBuild = ProjectBuilder() # Setting no data should fail assert projBuild.buildProject({}) is False @@ -432,7 +432,7 @@ def testCoreTools_NewMinimal(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): @pytest.mark.core -def testCoreTools_NewCustomA(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): +def testCoreTools_NewCustomA(monkeypatch, fncPath, tstPaths, mockRnd): """Create a new project from a project wizard dictionary. Custom type with chapters and scenes. """ @@ -460,7 +460,7 @@ def testCoreTools_NewCustomA(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): "numScenes": 3, } - projBuild = ProjectBuilder(mockGUI) + projBuild = ProjectBuilder() assert projBuild.buildProject(projData) is True copyfile(projFile, testFile) @@ -470,7 +470,7 @@ def testCoreTools_NewCustomA(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): @pytest.mark.core -def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): +def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockRnd): """Create a new project from a project wizard dictionary. Custom type without chapters, but with scenes. """ @@ -498,7 +498,7 @@ def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): "numScenes": 6, } - projBuild = ProjectBuilder(mockGUI) + projBuild = ProjectBuilder() assert projBuild.buildProject(projData) is True copyfile(projFile, testFile) @@ -508,7 +508,7 @@ def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd): @pytest.mark.core -def testCoreTools_NewSample(monkeypatch, fncPath, tstPaths, mockGUI): +def testCoreTools_NewSample(monkeypatch, fncPath, tstPaths): """Check that we can create a new project can be created from the provided sample project via a zip file. """ @@ -522,7 +522,7 @@ def testCoreTools_NewSample(monkeypatch, fncPath, tstPaths, mockGUI): "popCustom": False, } - projBuild = ProjectBuilder(mockGUI) + projBuild = ProjectBuilder() # No path set assert projBuild.buildProject({"popSample": True}) is False diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py index 16e8941f..4c5e924a 100644 --- a/tests/test_core/test_core_docbuild.py +++ b/tests/test_core/test_core_docbuild.py @@ -76,7 +76,7 @@ BUILD_CONF = { @pytest.mark.core def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): """Test building an open document manuscript.""" - project = NWProject(mockGUI) + project = NWProject() project.openProject(prjLipsum) build = BuildSettings() @@ -180,7 +180,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP @pytest.mark.core def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): """Test building an HTML manuscript.""" - project = NWProject(mockGUI) + project = NWProject() project.openProject(prjLipsum) build = BuildSettings() @@ -250,7 +250,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): @pytest.mark.core def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): """Test building an Markdown manuscript.""" - project = NWProject(mockGUI) + project = NWProject() project.openProject(prjLipsum) build = BuildSettings() @@ -320,7 +320,7 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths @pytest.mark.core def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): """Test building a NWD manuscript.""" - project = NWProject(mockGUI) + project = NWProject() project.openProject(prjLipsum) build = BuildSettings() @@ -390,7 +390,7 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): @pytest.mark.core def testCoreDocBuild_Custom(mockGUI, fncPath: Path): """Test custom builds and some error handling.""" - project = NWProject(mockGUI) + project = NWProject() buildTestProject(project, fncPath) build = BuildSettings() @@ -421,8 +421,8 @@ def testCoreDocBuild_Custom(mockGUI, fncPath: Path): # Add an invalid item to the project nHandle = "0123456789def" - project.tree._treeOrder.append(nHandle) - project.tree._projTree[nHandle] = None # type: ignore + project.tree._order.append(nHandle) + project.tree._tree[nHandle] = None # type: ignore docBuild.queueAll() assert len(docBuild) == 8 @@ -455,7 +455,7 @@ def testCoreDocBuild_Custom(mockGUI, fncPath: Path): @pytest.mark.core def testCoreDocBuild_IterBuild(mockGUI, fncPath: Path, mockRnd): """Test iter build wrapper.""" - project = NWProject(mockGUI) + project = NWProject() buildTestProject(project, fncPath) build = BuildSettings() build.unpack(BUILD_CONF) diff --git a/tests/test_core/test_core_document.py b/tests/test_core/test_core_document.py index 9ab3a495..8cafa69a 100644 --- a/tests/test_core/test_core_document.py +++ b/tests/test_core/test_core_document.py @@ -32,7 +32,7 @@ from novelwriter.core.document import NWDocument @pytest.mark.core def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd): """Test loading and saving a document with the NWDocument class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) @@ -173,7 +173,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd): def testCoreDocument_Methods(mockGUI, fncPath, mockRnd): """Test other methods of the NWDocument class. """ - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index c3b09a43..b9e107e5 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -42,7 +42,7 @@ def testCoreIndex_LoadSave(monkeypatch, prjLipsum, mockGUI, tstPaths): testFile = tstPaths.outDir / "coreIndex_LoadSave_tagsIndex.json" compFile = tstPaths.refDir / "coreIndex_LoadSave_tagsIndex.json" - theProject = NWProject(mockGUI) + theProject = NWProject() assert theProject.openProject(prjLipsum) theIndex = NWIndex(theProject) @@ -155,7 +155,7 @@ def testCoreIndex_LoadSave(monkeypatch, prjLipsum, mockGUI, tstPaths): @pytest.mark.core def testCoreIndex_ScanThis(mockGUI): """Test the tag scanner function scanThis.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theIndex = theProject.index isValid, theBits, thePos = theIndex.scanThis("tag: this, and this") @@ -204,7 +204,7 @@ def testCoreIndex_ScanThis(mockGUI): def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd): """Test the tag checker function checkThese. """ - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) theIndex = theProject.index @@ -281,7 +281,7 @@ def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd): @pytest.mark.core def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd): """Check the index text scanner.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) theIndex = theProject.index @@ -502,7 +502,7 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd): @pytest.mark.core def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): """Check the index data extraction functions.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) @@ -727,7 +727,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): ] # Add a fake handle to the tree and check that it's ignored - theProject.tree._treeOrder.append("0000000000000") + theProject.tree._order.append("0000000000000") assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [ (C.hTitlePage, "T0001"), (C.hChapterDoc, "T0001"), @@ -738,7 +738,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): (sHandle, "T0001"), (tHandle, "T0001"), ] - theProject.tree._treeOrder.remove("0000000000000") + theProject.tree._order.remove("0000000000000") # Extract stats assert theIndex.getNovelWordCount(skipExcl=False) == 43 @@ -941,7 +941,7 @@ def testCoreIndex_TagsIndex(): @pytest.mark.core def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd): """Check the ItemIndex class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) theProject.index.clearIndex() @@ -1077,7 +1077,7 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd): assert nStruct[0][0] == uHandle # Inject garbage into tree - theProject.tree._treeOrder.append("stuff") + theProject.tree._order.append("stuff") nStruct = list(itemIndex.iterNovelStructure()) assert len(nStruct) == 4 assert nStruct[0][0] == nHandle diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index 752bfc10..2878137f 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -34,7 +34,7 @@ from novelwriter.core.project import NWProject @pytest.mark.core def testCoreItem_Setters(mockGUI, mockRnd, fncPath): """Test all the simple setters for the NWItem class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) theItem = NWItem(theProject, "0000000000000") @@ -185,7 +185,7 @@ def testCoreItem_Setters(mockGUI, mockRnd, fncPath): @pytest.mark.core def testCoreItem_Methods(mockGUI, mockRnd, fncPath): """Test the simple methods of the NWItem class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) theItem = NWItem(theProject, "0000000000000") @@ -333,7 +333,7 @@ def testCoreItem_TypeSetter(mockGUI): """Test the setter for all the nwItemType values for the NWItem class. """ - theProject = NWProject(mockGUI) + theProject = NWProject() theItem = NWItem(theProject, "0000000000000") # Type @@ -362,7 +362,7 @@ def testCoreItem_ClassSetter(mockGUI): """Test the setter for all the nwItemClass values for the NWItem class. """ - theProject = NWProject(mockGUI) + theProject = NWProject() theItem = NWItem(theProject, "0000000000000") # Class @@ -449,7 +449,7 @@ def testCoreItem_LayoutSetter(mockGUI): """Test the setter for all the nwItemLayout values for the NWItem class. """ - theProject = NWProject(mockGUI) + theProject = NWProject() theItem = NWItem(theProject, "0000000000000") # Faulty Layouts @@ -477,7 +477,7 @@ def testCoreItem_LayoutSetter(mockGUI): def testCoreItem_ClassDefaults(mockGUI): """Test the setter for the default values. """ - theProject = NWProject(mockGUI) + theProject = NWProject() theItem = NWItem(theProject, "0000000000000") # Root items should not have their class updated @@ -532,7 +532,7 @@ def testCoreItem_ClassDefaults(mockGUI): @pytest.mark.core def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd): """Test packing and unpacking entries for the NWItem class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theProject.data.itemStatus.write(None, "New", (100, 100, 100)) theProject.data.itemImport.write(None, "New", (100, 100, 100)) diff --git a/tests/test_core/test_core_options.py b/tests/test_core/test_core_options.py index dba4bdea..747a8f73 100644 --- a/tests/test_core/test_core_options.py +++ b/tests/test_core/test_core_options.py @@ -33,7 +33,7 @@ from novelwriter.gui.noveltree import NovelTreeColumn @pytest.mark.core def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath): """Test loading and saving from the OptionState class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theOpts = OptionState(theProject) metaDir = fncPath / "meta" @@ -106,7 +106,7 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath): @pytest.mark.core def testCoreOptions_SetGet(mockGUI): """Test setting and getting values from the OptionState class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theOpts = OptionState(theProject) nwColHidden = NovelTreeColumn.HIDDEN diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index b13b75c4..1cc58342 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from PyQt5.QtWidgets import QMessageBox import pytest from shutil import copyfile @@ -27,7 +28,7 @@ from zipfile import ZipFile from mocked import causeOSError from tools import C, cmpFiles, buildTestProject, XML_IGNORE -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemClass from novelwriter.constants import nwFiles from novelwriter.core.tree import NWTree @@ -44,7 +45,7 @@ def testCoreProject_NewRoot(fncPath, tstPaths, mockGUI, mockRnd): testFile = tstPaths.outDir / "coreProject_NewRoot_nwProject.nwx" compFile = tstPaths.refDir / "coreProject_NewRoot_nwProject.nwx" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) @@ -94,7 +95,7 @@ def testCoreProject_NewFileFolder(monkeypatch, fncPath, tstPaths, mockGUI, mockR testFile = tstPaths.outDir / "coreProject_NewFileFolder_nwProject.nwx" compFile = tstPaths.refDir / "coreProject_NewFileFolder_nwProject.nwx" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) @@ -163,7 +164,7 @@ def testCoreProject_NewFileFolder(monkeypatch, fncPath, tstPaths, mockGUI, mockR @pytest.mark.core def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): """Test opening a project.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) @@ -176,7 +177,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK assert theProject.storage.writeLockFile() is True assert theProject.openProject(fncPath) is False - assert isinstance(theProject.getLockStatus(), list) + assert isinstance(theProject.lockStatus, list) # Fail to read lockfile (which still opens the project) with monkeypatch.context() as mp: @@ -189,9 +190,9 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): # Force open with lockfile theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK assert theProject.storage.writeLockFile() is True - assert theProject.openProject(fncPath, overrideLock=True) is True + assert theProject.openProject(fncPath, clearLock=True) is True theProject.closeProject() - assert theProject.getLockStatus() is None + assert theProject.lockStatus is None # Fail getting xml reader with monkeypatch.context() as mp: @@ -203,37 +204,40 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): mp.setattr(ProjectXMLReader, "read", lambda *a: False) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.NOT_NWX_FILE)) assert theProject.openProject(fncPath) is False - assert "Project file does not appear" in mockGUI.lastAlert + lastMsg = SHARED.alert.logMessage if SHARED.alert else "" + assert "Project file does not appear" in lastMsg # Unknown project file version with monkeypatch.context() as mp: mp.setattr(ProjectXMLReader, "read", lambda *a: False) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.UNKNOWN_VERSION)) assert theProject.openProject(fncPath) is False - assert "Unknown or unsupported novelWriter project file" in mockGUI.lastAlert + lastMsg = SHARED.alert.logMessage if SHARED.alert else "" + assert "Unknown or unsupported novelWriter project file" in lastMsg # Other parse error with monkeypatch.context() as mp: mp.setattr(ProjectXMLReader, "read", lambda *a: False) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.CANNOT_PARSE)) assert theProject.openProject(fncPath) is False - assert "Failed to parse project xml" in mockGUI.lastAlert + lastMsg = SHARED.alert.logMessage if SHARED.alert else "" + assert "Failed to parse project xml" in lastMsg # Won't convert legacy file with monkeypatch.context() as mp: mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY)) - mockGUI.askResponse = False + mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) assert theProject.openProject(fncPath) is False - assert "The file format of your project is about to be" in mockGUI.lastQuestion - mockGUI.askResponse = True + lastMsg = SHARED.alert.logMessage if SHARED.alert else "" + assert "The file format of your project is about to be" in lastMsg # Won't open project from newer version with monkeypatch.context() as mp: mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999)) - mockGUI.askResponse = False + mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) assert theProject.openProject(fncPath) is False - assert "This project was saved by a newer version" in mockGUI.lastQuestion - mockGUI.askResponse = True + lastMsg = SHARED.alert.logMessage if SHARED.alert else "" + assert "This project was saved by a newer version" in lastMsg # Fail checking items should still pass with monkeypatch.context() as mp: @@ -246,10 +250,10 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): with monkeypatch.context() as mp: mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY)) mp.setattr("novelwriter.core.index.NWIndex.loadIndex", lambda *a: True) - mockGUI.askResponse = True theProject.index._indexBroken = True assert theProject.openProject(fncPath) is True - assert "The file format of your project is about to be" in mockGUI.lastQuestion + lastMsg = SHARED.alert.logMessage if SHARED.alert else "" + assert "The file format of your project is about to be" in lastMsg assert theProject.index._indexBroken is False theProject.closeProject() @@ -260,7 +264,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): @pytest.mark.core def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncPath): """Test saving a project.""" - theProject = NWProject(mockGUI) + theProject = NWProject() # Nothing to save assert theProject.saveProject() is False @@ -289,7 +293,7 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncPath): @pytest.mark.core def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd): """Test helper functions for the project folder.""" - theProject = NWProject(mockGUI) + theProject = NWProject() buildTestProject(theProject, fncPath) # Storage Objects @@ -323,7 +327,7 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd): assert theProject.tree.handles() == newOrder # Add a non-existing item - theProject.tree._treeOrder.append(C.hInvalid) + theProject.tree._order.append(C.hInvalid) # Add an item with a non-existent parent nHandle = theProject.newFile("Test File", C.hChapterDir) @@ -331,7 +335,7 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd): assert theProject.tree[nHandle].itemParent == "cba9876543210" retOrder = [] - for tItem in theProject.getProjectItems(): + for tItem in theProject.iterProjectItems(): retOrder.append(tItem.itemHandle) assert retOrder == [ @@ -353,7 +357,7 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd): @pytest.mark.core def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd): """Test the status and importance flag handling.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) @@ -462,7 +466,7 @@ def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd): @pytest.mark.core def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd): """Test other project class methods and functions.""" - theProject = NWProject(mockGUI) + theProject = NWProject() buildTestProject(theProject, fncPath) # Project Name @@ -482,7 +486,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd): theProject._session._start = 1600000000 with monkeypatch.context() as mp: mp.setattr("novelwriter.core.project.time", lambda: 1600005600) - assert theProject.getCurrentEditTime() == 6834 + assert theProject.currentEditTime == 6834 # Trash folder # Should create on first call, and just returned on later calls @@ -580,7 +584,7 @@ def testCoreProject_Backup(monkeypatch, mockGUI, fncPath, tstPaths): backup file and checks that the project XML file is identical to the original file. """ - theProject = NWProject(mockGUI) + theProject = NWProject() # No Project assert theProject.backupProject(doNotify=False) is False diff --git a/tests/test_core/test_core_sessions.py b/tests/test_core/test_core_sessions.py index 3f1353af..159e7469 100644 --- a/tests/test_core/test_core_sessions.py +++ b/tests/test_core/test_core_sessions.py @@ -35,7 +35,7 @@ from novelwriter.core.sessions import NWSessionLog @pytest.mark.core def testCoreSessions_Main(monkeypatch, mockGUI, fncPath): """Test log file handling of the NWSessionLog class.""" - project = NWProject(mockGUI) + project = NWProject() buildTestProject(project, fncPath) logFile = project.storage.getMetaFile(nwFiles.SESS_FILE) diff --git a/tests/test_core/test_core_spellcheck.py b/tests/test_core/test_core_spellcheck.py index 9f2e59de..6b161bca 100644 --- a/tests/test_core/test_core_spellcheck.py +++ b/tests/test_core/test_core_spellcheck.py @@ -36,7 +36,7 @@ from novelwriter.core.spellcheck import FakeEnchant, NWSpellEnchant, UserDiction @pytest.mark.core def testCoreSpell_UserDictionary(monkeypatch, mockGUI, fncPath): """Test the UserDictionary class.""" - project = NWProject(mockGUI) + project = NWProject() buildTestProject(project, fncPath) # Check that there is no file before we start @@ -114,7 +114,7 @@ def testCoreSpell_UserDictionary(monkeypatch, mockGUI, fncPath): @pytest.mark.core def testCoreSpell_FakeEnchant(monkeypatch, mockGUI, fncPath): """Test the FakeEnchant spell checker fallback.""" - project = NWProject(mockGUI) + project = NWProject() buildTestProject(project, fncPath) # Make package import fail @@ -149,7 +149,7 @@ def testCoreSpell_FakeEnchant(monkeypatch, mockGUI, fncPath): @pytest.mark.core def testCoreSpell_Enchant(monkeypatch, mockGUI, fncPath): """Test the pyenchant spell checker.""" - project = NWProject(mockGUI) + project = NWProject() buildTestProject(project, fncPath) # Break the enchant package, and check error handling diff --git a/tests/test_core/test_core_storage.py b/tests/test_core/test_core_storage.py index 77db582b..19d8a5b5 100644 --- a/tests/test_core/test_core_storage.py +++ b/tests/test_core/test_core_storage.py @@ -43,7 +43,7 @@ class MockProject: @pytest.mark.core def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd): """Test opening a project in a folder.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) theProject.closeProject() @@ -180,7 +180,7 @@ def testCoreStorage_ZipIt(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd): """Test making a zip archive of a project.""" zipFile = tstPaths.tmpDir / "project.zip" - theProject = NWProject(mockGUI) + theProject = NWProject() storage = theProject.storage assert storage.zipIt(zipFile) is False @@ -365,7 +365,7 @@ def testCoreStorage_DeprecatedFiles(monkeypatch, fncPath): @pytest.mark.core def testCoreStorage_OldFormatConvert(monkeypatch, mockGUI, fncPath): """Test cleanup of deprecated files that needs to be converted.""" - project = NWProject(mockGUI) + project = NWProject() buildTestProject(project, fncPath) legacy = _LegacyStorage(project) diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py index ebaf1860..6c7dee61 100644 --- a/tests/test_core/test_core_tohtml.py +++ b/tests/test_core/test_core_tohtml.py @@ -31,7 +31,7 @@ from novelwriter.core.project import NWProject def testCoreToHtml_ConvertFormat(mockGUI): """Test the tokenizer and converter chain using the ToHtml class. """ - theProject = NWProject(mockGUI) + theProject = NWProject() theHtml = ToHtml(theProject) # Novel Files Headers @@ -233,7 +233,7 @@ def testCoreToHtml_ConvertFormat(mockGUI): def testCoreToHtml_ConvertDirect(mockGUI): """Test the converter directly using the ToHtml class. """ - theProject = NWProject(mockGUI) + theProject = NWProject() theHtml = ToHtml(theProject) theHtml._isNovel = True @@ -380,7 +380,7 @@ def testCoreToHtml_ConvertDirect(mockGUI): def testCoreToHtml_SpecialCases(mockGUI): """Test some special cases that have caused errors in the past. """ - theProject = NWProject(mockGUI) + theProject = NWProject() theHtml = ToHtml(theProject) theHtml._isNovel = True @@ -454,7 +454,7 @@ def testCoreToHtml_SpecialCases(mockGUI): def testCoreToHtml_Complex(mockGUI, fncPath): """Test the save method of the ToHtml class. """ - theProject = NWProject(mockGUI) + theProject = NWProject() theHtml = ToHtml(theProject) theHtml._isNovel = True @@ -549,7 +549,7 @@ def testCoreToHtml_Complex(mockGUI, fncPath): def testCoreToHtml_Methods(mockGUI): """Test all the other methods of the ToHtml class. """ - theProject = NWProject(mockGUI) + theProject = NWProject() theHtml = ToHtml(theProject) theHtml.setKeepMarkdown(True) @@ -609,7 +609,7 @@ def testCoreToHtml_Methods(mockGUI): def testCoreToHtml_Format(mockGUI): """Test all the formatters for the ToHtml class. """ - theProject = NWProject(mockGUI) + theProject = NWProject() theHtml = ToHtml(theProject) # Export Mode diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py index 5214b925..d746418d 100644 --- a/tests/test_core/test_core_tokenizer.py +++ b/tests/test_core/test_core_tokenizer.py @@ -36,7 +36,7 @@ class BareTokenizer(Tokenizer): @pytest.mark.core def testCoreToken_Setters(mockGUI): """Test all the setters for the Tokenizer class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theToken = BareTokenizer(theProject) # Verify defaults @@ -133,7 +133,7 @@ def testCoreToken_Setters(mockGUI): @pytest.mark.core def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath): """Test handling files and text in the Tokenizer class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() mockRnd.reset() buildTestProject(theProject, fncPath) @@ -231,7 +231,7 @@ def testCoreToken_StripEscape(): @pytest.mark.core def testCoreToken_HeaderFormat(mockGUI): """Test the tokenization of header formats in the Tokenizer class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theToken = BareTokenizer(theProject) theToken.setKeepMarkdown(True) @@ -434,7 +434,7 @@ def testCoreToken_HeaderFormat(mockGUI): @pytest.mark.core def testCoreToken_MetaFormat(mockGUI): """Test the tokenization of meta formats in the Tokenizer class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theToken = BareTokenizer(theProject) theToken.setKeepMarkdown(True) @@ -502,7 +502,7 @@ def testCoreToken_MetaFormat(mockGUI): @pytest.mark.core def testCoreToken_MarginFormat(mockGUI): """Test the tokenization of margin formats in the Tokenizer class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theToken = BareTokenizer(theProject) theToken.setKeepMarkdown(True) @@ -556,7 +556,7 @@ def testCoreToken_MarginFormat(mockGUI): @pytest.mark.core def testCoreToken_TextFormat(mockGUI): """Test the tokenization of text formats in the Tokenizer class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theToken = BareTokenizer(theProject) theToken.setKeepMarkdown(True) @@ -677,7 +677,7 @@ def testCoreToken_TextFormat(mockGUI): @pytest.mark.core def testCoreToken_SpecialFormat(mockGUI): """Test the tokenization of special formats in the Tokenizer class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theToken = BareTokenizer(theProject) theToken._isNovel = True @@ -879,7 +879,7 @@ def testCoreToken_SpecialFormat(mockGUI): @pytest.mark.core def testCoreToken_ProcessHeaders(mockGUI): """Test the header and page parser of the Tokenizer class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theProject.data.setLanguage("en") theProject._loadProjectLocalisation() theToken = BareTokenizer(theProject) diff --git a/tests/test_core/test_core_tomd.py b/tests/test_core/test_core_tomd.py index f03ee8f5..06658a85 100644 --- a/tests/test_core/test_core_tomd.py +++ b/tests/test_core/test_core_tomd.py @@ -32,7 +32,7 @@ def testCoreToMarkdown_ConvertFormat(mockGUI): """Test the tokenizer and converter chain using the ToMarkdown class. """ - theProject = NWProject(mockGUI) + theProject = NWProject() theMD = ToMarkdown(theProject) # Headers @@ -159,7 +159,7 @@ def testCoreToMarkdown_ConvertFormat(mockGUI): @pytest.mark.core def testCoreToMarkdown_ConvertDirect(mockGUI): """Test the converter directly using the ToMarkdown class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theMD = ToMarkdown(theProject) theMD._isNovel = True @@ -209,7 +209,7 @@ def testCoreToMarkdown_ConvertDirect(mockGUI): @pytest.mark.core def testCoreToMarkdown_Complex(mockGUI, fncPath): """Test the save method of the ToMarkdown class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theMD = ToMarkdown(theProject) theMD._isNovel = True @@ -261,7 +261,7 @@ def testCoreToMarkdown_Complex(mockGUI, fncPath): @pytest.mark.core def testCoreToMarkdown_Format(mockGUI): """Test all the formatters for the ToMarkdown class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theMD = ToMarkdown(theProject) assert theMD._formatKeywords("", theMD.A_NONE) == "" diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py index 359b0935..19962185 100644 --- a/tests/test_core/test_core_toodt.py +++ b/tests/test_core/test_core_toodt.py @@ -53,7 +53,7 @@ def xmlToText(xElem): @pytest.mark.core def testCoreToOdt_Init(mockGUI): """Test initialisation of the ODT document.""" - theProject = NWProject(mockGUI) + theProject = NWProject() # Flat Doc # ======== @@ -108,7 +108,7 @@ def testCoreToOdt_Init(mockGUI): @pytest.mark.core def testCoreToOdt_TextFormatting(mockGUI): """Test formatting of paragraphs.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theDoc = ToOdt(theProject, isFlat=True) theDoc.initDocument() @@ -242,7 +242,7 @@ def testCoreToOdt_TextFormatting(mockGUI): @pytest.mark.core def testCoreToOdt_Convert(mockGUI): """Test the converter of the ToOdt class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theDoc = ToOdt(theProject, isFlat=True) theDoc._isNovel = True @@ -573,7 +573,7 @@ def testCoreToOdt_ConvertDirect(mockGUI): """Test the converter directly using the ToOdt class to reach some otherwise hard to reach conditions. """ - theProject = NWProject(mockGUI) + theProject = NWProject() theDoc = ToOdt(theProject, isFlat=True) theDoc._isNovel = True @@ -626,7 +626,7 @@ def testCoreToOdt_ConvertDirect(mockGUI): @pytest.mark.core def testCoreToOdt_SaveFlat(mockGUI, fncPath, tstPaths): """Test the document save functions.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theProject.data.setAuthor("Jane Smith") theProject.data.setName("Test Project") theProject.data.setSaveCount(1234) @@ -668,7 +668,7 @@ def testCoreToOdt_SaveFlat(mockGUI, fncPath, tstPaths): @pytest.mark.core def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths): """Test the document save functions.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theProject.data.setAuthor("Jane Smith") theProject.data.setName("Test Project") theProject.data.setSaveCount(1234) @@ -745,7 +745,7 @@ def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths): @pytest.mark.core def testCoreToOdt_Format(mockGUI): """Test the formatters for the ToOdt class.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theDoc = ToOdt(theProject, isFlat=True) assert theDoc._formatSynopsis("synopsis text") == ( diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index c2301bf7..83abd8cb 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -39,7 +39,7 @@ from novelwriter.core.project import NWProject @pytest.fixture(scope="function") def mockItems(mockGUI, mockRnd): """Create a list of mock items.""" - theProject = NWProject(mockGUI) + theProject = NWProject() itemA = NWItem(theProject, "a000000000001") itemA._name = "Novel" @@ -112,7 +112,7 @@ def mockItems(mockGUI, mockRnd): @pytest.mark.core def testCoreTree_BuildTree(mockGUI, mockItems): """Test building a project tree from a list of items.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theTree = NWTree(theProject) # Check that tree is empty (calls NWTree.__bool__) @@ -127,7 +127,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): assert theTree.append(nwItem) is True assert theTree.updateItemData(nwItem.itemHandle) is True - assert theTree._treeChanged is True + assert theTree._changed is True # Check that tree is not empty (calls __bool__) assert bool(theTree) is True @@ -269,7 +269,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): @pytest.mark.core def testCoreTree_PackUnpack(mockGUI, mockItems): """Test packing and unpacking data.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theTree = NWTree(theProject) aHandles = [] @@ -298,7 +298,7 @@ def testCoreTree_PackUnpack(mockGUI, mockItems): @pytest.mark.core def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fncPath, mockRnd): """Check the project consistency.""" - theProject = NWProject(mockGUI) + theProject = NWProject() buildTestProject(theProject, fncPath) # By default, all is well @@ -354,10 +354,12 @@ def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fnc assert itemX.itemClass == nwItemClass.NOVEL assert itemX.itemName == "[Recovered] Stuff" - # If the tree is empty, there is nowhere to add any of the 4 files + # If the tree is empty, a new root folder is created theProject.tree.clear() - assert theProject.tree.checkConsistency("Recovered") == (4, 0) - assert len(theProject.tree) == 0 + assert theProject.tree.checkConsistency("Recovered") == (4, 4) + assert len(theProject.tree) == 5 + nHandle = theProject.tree.findRoot(nwItemClass.NOVEL) + assert theProject.tree[nHandle].itemName == "Recovered" # type: ignore # END Test testCoreTree_CheckConsistency @@ -365,7 +367,7 @@ def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fnc @pytest.mark.core def testCoreTree_Methods(mockGUI, mockItems): """Test various class methods.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theTree = NWTree(theProject) for nwItem in mockItems: @@ -411,9 +413,9 @@ def testCoreTree_Methods(mockGUI, mockItems): assert roots[3][0] == "a000000000004" # Add a fake item to root and check that it can handle it - theTree._treeRoots["0000000000000"] = NWItem(theProject, "0000000000000") + theTree._roots["0000000000000"] = NWItem(theProject, "0000000000000") assert theTree.findRoot(nwItemClass.WORLD) is None - del theTree._treeRoots["0000000000000"] + del theTree._roots["0000000000000"] # Get item path assert theTree.getItemPath("stuff") == [] @@ -446,7 +448,7 @@ def testCoreTree_Methods(mockGUI, mockItems): def testCoreTree_MakeHandles(mockGUI): """Test generating item handles.""" random.seed(42) - theProject = NWProject(mockGUI) + theProject = NWProject() theTree = NWTree(theProject) handles = ["1c803a3b1799d", "bdd6406671ad1", "3eb1346685257", "23b8c392456de"] @@ -454,13 +456,13 @@ def testCoreTree_MakeHandles(mockGUI): random.seed(42) tHandle = theTree._makeHandle() assert tHandle == handles[0] - theTree._projTree[handles[0]] = None # type: ignore + theTree._tree[handles[0]] = None # type: ignore # Add the next in line to the project to force duplicate - theTree._projTree[handles[1]] = None # type: ignore + theTree._tree[handles[1]] = None # type: ignore tHandle = theTree._makeHandle() assert tHandle == handles[2] - theTree._projTree[handles[2]] = None # type: ignore + theTree._tree[handles[2]] = None # type: ignore # Reset the seed to force collissions, which should still end up # returning the next handle in the sequence @@ -474,14 +476,14 @@ def testCoreTree_MakeHandles(mockGUI): @pytest.mark.core def testCoreTree_Stats(mockGUI, mockItems): """Test project stats methods.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theTree = NWTree(theProject) for nwItem in mockItems: theTree.append(nwItem) assert len(theTree) == len(mockItems) - theTree._treeOrder.append("stuff") + theTree._order.append("stuff") # Count Words novelWords, noteWords = theTree.sumWords() @@ -494,7 +496,7 @@ def testCoreTree_Stats(mockGUI, mockItems): @pytest.mark.core def testCoreTree_Reorder(caplog, mockGUI, mockItems): """Test changing tree order.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theTree = NWTree(theProject) aHandle = [] @@ -518,7 +520,7 @@ def testCoreTree_Reorder(caplog, mockGUI, mockItems): assert "Handle 'stuff' in new tree order is not in old order" in caplog.text caplog.clear() - theTree._treeOrder.append("stuff") + theTree._order.append("stuff") theTree.setOrder(bHandle) assert theTree.handles() == bHandle assert "Handle 'stuff' in old tree order is not in new order" in caplog.text @@ -529,7 +531,7 @@ def testCoreTree_Reorder(caplog, mockGUI, mockItems): @pytest.mark.core def testCoreTree_ToCFile(monkeypatch, fncPath, mockGUI, mockItems): """Test writing the ToC.txt file.""" - theProject = NWProject(mockGUI) + theProject = NWProject() theTree = NWTree(theProject) for nwItem in mockItems: @@ -537,7 +539,7 @@ def testCoreTree_ToCFile(monkeypatch, fncPath, mockGUI, mockItems): theTree.updateItemData(nwItem.itemHandle) assert len(theTree) == len(mockItems) - theTree._treeOrder.append("stuff") + theTree._order.append("stuff") def mockIsFile(fileName): """Return True for items that are files in novelWriter and diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index fc6ceccf..0de88a72 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -23,6 +23,7 @@ import pytest from tools import C, buildTestProject +from novelwriter import SHARED from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.editlabel import GuiEditLabel @@ -35,7 +36,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Create a new project buildTestProject(nwGUI, projPath) - theProject = nwGUI.project + theProject = SHARED.project projTree = nwGUI.projView.projTree docText = ( diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index d42ba40f..da1eb274 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -39,8 +39,7 @@ KEY_DELAY = 1 @pytest.mark.gui def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths): - """Test the load project wizard. - """ + """Test the preferences dialog.""" monkeypatch.setattr(GuiPreferences, "exec_", lambda *a: None) monkeypatch.setattr(GuiPreferences, "result", lambda *a: QDialog.Accepted) monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")]) diff --git a/tests/test_dialogs/test_dlg_projdetails.py b/tests/test_dialogs/test_dlg_projdetails.py index af234423..88fd9a92 100644 --- a/tests/test_dialogs/test_dlg_projdetails.py +++ b/tests/test_dialogs/test_dlg_projdetails.py @@ -25,6 +25,7 @@ from tools import getGuiItem from PyQt5.QtWidgets import QAction +from novelwriter import SHARED from novelwriter.dialogs.projdetails import GuiProjectDetails @@ -54,7 +55,7 @@ def testDlgProjDetails_Dialog(qtbot, nwGUI, prjLipsum): assert projDet.tabMain.wordCountVal.text() == f"{3000:n}" assert projDet.tabMain.chapCountVal.text() == f"{3:n}" assert projDet.tabMain.sceneCountVal.text() == f"{5:n}" - assert projDet.tabMain.revCountVal.text() == f"{nwGUI.project.data.saveCount:n}" + assert projDet.tabMain.revCountVal.text() == f"{SHARED.project.data.saveCount:n}" assert projDet.tabMain.projPathVal.text() == str(prjLipsum) diff --git a/tests/test_dialogs/test_dlg_projsettings.py b/tests/test_dialogs/test_dlg_projsettings.py index c3137265..bafdadc7 100644 --- a/tests/test_dialogs/test_dlg_projsettings.py +++ b/tests/test_dialogs/test_dlg_projsettings.py @@ -27,7 +27,7 @@ from PyQt5.QtGui import QColor from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QDialog, QAction, QColorDialog -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemType from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.projsettings import GuiProjectSettings @@ -50,8 +50,8 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI): assert getGuiItem("GuiProjectSettings") is None # Pretend we have a project - nwGUI.hasProject = True - nwGUI.project.data.setSpellLang("en") + SHARED.project._valid = True + SHARED.project.data.setSpellLang("en") # Get the dialog object nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) @@ -95,7 +95,7 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockR CONFIG.setBackupPath(fncPath) # Set some values - theProject = nwGUI.project + theProject = SHARED.project theProject.data.setSpellLang("en") theProject.data.setAuthor("Jane Smith") theProject.data.setAutoReplace({"A": "B", "C": "D"}) @@ -160,7 +160,7 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat CONFIG.setBackupPath(fncPath) # Set some values - theProject = nwGUI.project + theProject = SHARED.project theProject.tree[C.hTitlePage].setStatus(C.sFinished) theProject.tree[C.hChapterDoc].setStatus(C.sDraft) theProject.tree[C.hSceneDoc].setStatus(C.sDraft) @@ -361,7 +361,7 @@ def testDlgProjSettings_Replace(qtbot, monkeypatch, nwGUI, fncPath, projPath, mo CONFIG.setBackupPath(fncPath) # Set some values - theProject = nwGUI.project + theProject = SHARED.project theProject.data.setAutoReplace({ "A": "B", "C": "D" }) diff --git a/tests/test_dialogs/test_dlg_wordlist.py b/tests/test_dialogs/test_dlg_wordlist.py index 35b1b1cf..6012cf8e 100644 --- a/tests/test_dialogs/test_dlg_wordlist.py +++ b/tests/test_dialogs/test_dlg_wordlist.py @@ -26,6 +26,7 @@ from PyQt5.QtWidgets import QDialog, QAction from tools import buildTestProject, getGuiItem +from novelwriter import SHARED from novelwriter.core.spellcheck import UserDictionary from novelwriter.dialogs.wordlist import GuiWordList @@ -55,7 +56,7 @@ def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, projPath): assert wList.listBox.count() == 0 # Add words - userDict = UserDictionary(nwGUI.project) + userDict = UserDictionary(SHARED.project) userDict.add("word_a") userDict.add("word_c") userDict.add("word_g") diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 8452623f..ef05fd2a 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -28,7 +28,7 @@ from PyQt5.QtCore import Qt from PyQt5.QtGui import QTextBlock, QTextCursor, QTextOption from PyQt5.QtWidgets import QAction, qApp -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.core.index import countWords @@ -163,10 +163,10 @@ def testGuiEditor_SaveText(qtbot, monkeypatch, caplog, nwGUI, projPath, ipsumTex assert "Could not save document." in caplog.text # Change header level - assert nwGUI.project.tree[C.hSceneDoc].itemLayout == nwItemLayout.DOCUMENT + assert SHARED.project.tree[C.hSceneDoc].itemLayout == nwItemLayout.DOCUMENT nwGUI.docEditor.replaceText(longText[1:]) assert nwGUI.docEditor.saveText() is True - assert nwGUI.project.tree[C.hSceneDoc].itemLayout == nwItemLayout.DOCUMENT + assert SHARED.project.tree[C.hSceneDoc].itemLayout == nwItemLayout.DOCUMENT # Regular save assert nwGUI.docEditor.saveText() is True @@ -194,18 +194,18 @@ def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd): assert nwGUI.docEditor.getText() == "### New Scene\n\nSome\ntext.\nMore\u00a0text.\n" # Check Propertoes - assert nwGUI.docEditor.docChanged() is True - assert nwGUI.docEditor.docHandle() == C.hSceneDoc - assert nwGUI.docEditor.lastActive() > 0.0 - assert nwGUI.docEditor.isEmpty() is False + assert nwGUI.docEditor.docChanged is True + assert nwGUI.docEditor.docHandle == C.hSceneDoc + assert nwGUI.docEditor.lastActive > 0.0 + assert nwGUI.docEditor.isEmpty is False # Cursor Position assert nwGUI.docEditor.setCursorPosition(None) is False assert nwGUI.docEditor.setCursorPosition(10) is True assert nwGUI.docEditor.getCursorPosition() == 10 - assert nwGUI.project.tree[C.hSceneDoc].cursorPos != 10 + assert SHARED.project.tree[C.hSceneDoc].cursorPos != 10 nwGUI.docEditor.saveCursorPosition() - assert nwGUI.project.tree[C.hSceneDoc].cursorPos == 10 + assert SHARED.project.tree[C.hSceneDoc].cursorPos == 10 assert nwGUI.docEditor.setCursorLine(None) is False assert nwGUI.docEditor.setCursorLine(3) is True @@ -213,7 +213,7 @@ def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd): # Document Changed Signal nwGUI.docEditor._docChanged = False - with qtbot.waitSignal(nwGUI.docEditor.docEditedStatusChanged, raising=True, timeout=100): + with qtbot.waitSignal(nwGUI.docEditor.editedStatusChanged, raising=True, timeout=100): nwGUI.docEditor.setDocumentChanged(True) assert nwGUI.docEditor._docChanged is True @@ -1067,7 +1067,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd): # Create Character theText = "### Jane Doe\n\n@tag: Jane\n\n" + ipsumText[1] + "\n\n" - cHandle = nwGUI.project.newFile("Jane Doe", C.hCharRoot) + cHandle = SHARED.project.newFile("Jane Doe", C.hCharRoot) assert nwGUI.openDocument(cHandle) is True assert nwGUI.docEditor.replaceText(theText) is True assert nwGUI.saveDocument() is True @@ -1145,8 +1145,8 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)" # Open a document and populate it - nwGUI.project.tree[C.hSceneDoc]._initCount = 0 # Clear item's count - nwGUI.project.tree[C.hSceneDoc]._wordCount = 0 # Clear item's count + SHARED.project.tree[C.hSceneDoc]._initCount = 0 # Clear item's count + SHARED.project.tree[C.hSceneDoc]._wordCount = 0 # Clear item's count assert nwGUI.openDocument(C.hSceneDoc) is True theText = "\n\n".join(ipsumText) @@ -1170,9 +1170,9 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m nwGUI.docEditor.wCounterDoc.run() # nwGUI.docEditor._updateDocCounts(cC, wC, pC) - assert nwGUI.project.tree[C.hSceneDoc]._charCount == cC - assert nwGUI.project.tree[C.hSceneDoc]._wordCount == wC - assert nwGUI.project.tree[C.hSceneDoc]._paraCount == pC + assert SHARED.project.tree[C.hSceneDoc]._charCount == cC + assert SHARED.project.tree[C.hSceneDoc]._wordCount == wC + assert SHARED.project.tree[C.hSceneDoc]._paraCount == pC assert nwGUI.docEditor.docFooter.wordsText.text() == f"Words: {wC} (+{wC})" # Select all text @@ -1361,7 +1361,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): # Next match nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) - assert nwGUI.docEditor.docHandle() == "2426c6f0ca922" # Next document + assert nwGUI.docEditor.docHandle == "2426c6f0ca922" # Next document nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) assert abs(nwGUI.docEditor.getCursorPosition() - 620) < 3 nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) @@ -1371,11 +1371,11 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum): assert nwGUI.docEditor.docSearch.doNextFile is True assert nwGUI.docEditor.docSearch.setSearchText("abcdef") nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) - assert nwGUI.docEditor.docHandle() != "2426c6f0ca922" - assert nwGUI.docEditor.docHandle() == "04468803b92e1" + assert nwGUI.docEditor.docHandle != "2426c6f0ca922" + assert nwGUI.docEditor.docHandle == "04468803b92e1" nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) - assert nwGUI.docEditor.docHandle() != "04468803b92e1" - assert nwGUI.docEditor.docHandle() == "7a992350f3eb6" + assert nwGUI.docEditor.docHandle != "04468803b92e1" + assert nwGUI.docEditor.docHandle == "7a992350f3eb6" # Toggle Replace nwGUI.docEditor.beginReplace() diff --git a/tests/test_gui/test_gui_docviewer.py b/tests/test_gui/test_gui_docviewer.py index 69b11039..cc8bc995 100644 --- a/tests/test_gui/test_gui_docviewer.py +++ b/tests/test_gui/test_gui_docviewer.py @@ -27,7 +27,7 @@ from PyQt5.QtCore import Qt, QUrl from PyQt5.QtGui import QTextCursor from PyQt5.QtWidgets import qApp, QAction -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwDocAction from novelwriter.core.tohtml import ToHtml @@ -40,8 +40,8 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): # Rebuild the index nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger) - assert nwGUI.project.index._tagsIndex._tags != {} - assert nwGUI.project.index._itemIndex._items != {} + assert SHARED.project.index._tagsIndex._tags != {} + assert SHARED.project.index._itemIndex._items != {} # Select a document in the project tree nwGUI.projView.setSelectedHandle("88243afbe5ed8") @@ -50,7 +50,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): theItem = nwGUI.projView.projTree._getTreeItem("88243afbe5ed8") theRect = nwGUI.projView.projTree.visualItemRect(theItem) qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=theRect.center()) - assert nwGUI.docViewer.docHandle() == "88243afbe5ed8" + assert nwGUI.docViewer.docHandle == "88243afbe5ed8" # Reload the text origText = nwGUI.docViewer.toPlainText() @@ -97,7 +97,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): # Close document nwGUI.docViewer.docHeader._closeDocument() - assert nwGUI.docViewer.docHandle() is None + assert nwGUI.docViewer.docHandle is None # Action on no document assert nwGUI.docViewer.docAction(nwDocAction.COPY) is False @@ -114,21 +114,21 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum): theRect = nwGUI.docViewer.cursorRect() # qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100) nwGUI.docViewer._linkClicked(QUrl("#char=Bod")) - assert nwGUI.docViewer.docHandle() == "4c4f28287af27" + assert nwGUI.docViewer.docHandle == "4c4f28287af27" # Click mouse nav buttons qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.BackButton, pos=theRect.center(), delay=100) - assert nwGUI.docViewer.docHandle() == "88243afbe5ed8" + assert nwGUI.docViewer.docHandle == "88243afbe5ed8" qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.ForwardButton, pos=theRect.center(), delay=100) - assert nwGUI.docViewer.docHandle() == "4c4f28287af27" + assert nwGUI.docViewer.docHandle == "4c4f28287af27" # Scroll bar default on empty document nwGUI.docViewer.clear() - assert nwGUI.docViewer.getScrollPosition() == 0 + assert nwGUI.docViewer.scrollPosition == 0 nwGUI.docViewer.reloadText() # Change document title - nwItem = nwGUI.project.tree["4c4f28287af27"] + nwItem = SHARED.project.tree["4c4f28287af27"] nwItem.setName("Test Title") assert nwItem.itemName == "Test Title" nwGUI.docViewer.updateDocInfo("4c4f28287af27") diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index c46cac96..fd906971 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -30,7 +30,7 @@ from tools import ( from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QDialog, QMessageBox, QInputDialog -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemType, nwView, nwWidget from novelwriter.constants import nwFiles from novelwriter.gui.outline import GuiOutlineView @@ -104,18 +104,18 @@ def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, prjLipsum): @pytest.mark.gui def testGuiMain_NewProject(monkeypatch, nwGUI, projPath): - """Test creating a new project. - """ - # No data + """Test creating a new project.""" + # Open wizard, but return no data with monkeypatch.context() as mp: mp.setattr(GuiProjectWizard, "exec_", lambda *a: None) assert nwGUI.newProject(projData=None) is False # Close project with monkeypatch.context() as mp: - nwGUI.hasProject = True + SHARED.project._valid = True mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) assert nwGUI.newProject(projData={"projPath": projPath}) is False + SHARED.project._valid = False # No project path assert nwGUI.newProject(projData={}) is False @@ -153,10 +153,10 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): nwGUI.projStack.setCurrentIndex(0) with monkeypatch.context() as mp: mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True) - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None nwGUI.projView.projTree._getTreeItem(sHandle).setSelected(True) nwGUI._keyPressReturn() - assert nwGUI.docEditor.docHandle() == sHandle + assert nwGUI.docEditor.docHandle == sHandle assert nwGUI.closeDocument() is True # Novel Tree has focus @@ -164,11 +164,11 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): nwGUI.novelView.novelTree.refreshTree(rootHandle=None, overRide=True) with monkeypatch.context() as mp: mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: True) - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None selItem = nwGUI.novelView.novelTree.topLevelItem(2) nwGUI.novelView.novelTree.setCurrentItem(selItem) nwGUI._keyPressReturn() - assert nwGUI.docEditor.docHandle() == sHandle + assert nwGUI.docEditor.docHandle == sHandle assert nwGUI.closeDocument() is True # Project Outline has focus @@ -176,11 +176,11 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): nwGUI.switchFocus(nwWidget.OUTLINE) with monkeypatch.context() as mp: mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True) - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None selItem = nwGUI.outlineView.outlineTree.topLevelItem(2) nwGUI.outlineView.outlineTree.setCurrentItem(selItem) nwGUI._keyPressReturn() - assert nwGUI.docEditor.docHandle() == sHandle + assert nwGUI.docEditor.docHandle == sHandle assert nwGUI.closeDocument() is True # qtbot.stop() @@ -202,14 +202,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): assert nwGUI.saveProject() assert nwGUI.closeProject() - assert len(nwGUI.project.tree) == 0 - assert len(nwGUI.project.tree._treeOrder) == 0 - assert len(nwGUI.project.tree._treeRoots) == 0 - assert nwGUI.project.tree.trashRoot() is None - assert nwGUI.project.data.name == "" - assert nwGUI.project.data.title == "" - assert nwGUI.project.data.author == "" - assert nwGUI.project.data.spellCheck is False + assert len(SHARED.project.tree) == 0 + assert len(SHARED.project.tree._order) == 0 + assert len(SHARED.project.tree._roots) == 0 + assert SHARED.project.tree.trashRoot() is None + assert SHARED.project.data.name == "" + assert SHARED.project.data.title == "" + assert SHARED.project.data.author == "" + assert SHARED.project.data.spellCheck is False # Check the files projFile = projPath / "nwProject.nwx" @@ -222,14 +222,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): assert nwGUI.openProject(projPath) # Check that we loaded the data - assert len(nwGUI.project.tree) == 8 - assert len(nwGUI.project.tree._treeOrder) == 8 - assert len(nwGUI.project.tree._treeRoots) == 4 - assert nwGUI.project.tree.trashRoot() is None - assert nwGUI.project.data.name == "New Project" - assert nwGUI.project.data.title == "New Novel" - assert nwGUI.project.data.author == "Jane Doe" - assert nwGUI.project.data.spellCheck is False + assert len(SHARED.project.tree) == 8 + assert len(SHARED.project.tree._order) == 8 + assert len(SHARED.project.tree._roots) == 4 + assert SHARED.project.tree.trashRoot() is None + assert SHARED.project.data.name == "New Project" + assert SHARED.project.data.title == "New Novel" + assert SHARED.project.data.author == "Jane Doe" + assert SHARED.project.data.spellCheck is False # Check that tree items have been created assert nwGUI.projView.projTree._getTreeItem(C.hNovelRoot) is not None @@ -506,9 +506,9 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd): nwGUI.docEditor.wCounterDoc.run() # Save the document - assert nwGUI.docEditor.docChanged() + assert nwGUI.docEditor.docChanged assert nwGUI.saveDocument() - assert not nwGUI.docEditor.docChanged() + assert not nwGUI.docEditor.docChanged nwGUI.rebuildIndex() # Open and view the edited document diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index c3cf85f7..626c71aa 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -27,7 +27,7 @@ from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox from tools import C, writeFile, buildTestProject -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwDocAction, nwDocInsert from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.gui.doceditor import GuiDocEditor @@ -206,7 +206,7 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum): # Clear the Text nwGUI.docEditor.clear() - assert nwGUI.docEditor.isEmpty() + assert nwGUI.docEditor.isEmpty # Alignment & Indent # ================== @@ -403,17 +403,17 @@ def testGuiMenu_ContextMenus(qtbot, nwGUI, prjLipsum): # Navigation History assert nwGUI.viewDocument("04468803b92e1") - assert nwGUI.docViewer.docHandle() == "04468803b92e1" + assert nwGUI.docViewer.docHandle == "04468803b92e1" assert nwGUI.docViewer.docHeader.backButton.isEnabled() assert not nwGUI.docViewer.docHeader.forwardButton.isEnabled() qtbot.mouseClick(nwGUI.docViewer.docHeader.backButton, Qt.LeftButton) - assert nwGUI.docViewer.docHandle() == "4c4f28287af27" + assert nwGUI.docViewer.docHandle == "4c4f28287af27" assert not nwGUI.docViewer.docHeader.backButton.isEnabled() assert nwGUI.docViewer.docHeader.forwardButton.isEnabled() qtbot.mouseClick(nwGUI.docViewer.docHeader.forwardButton, Qt.LeftButton) - assert nwGUI.docViewer.docHandle() == "04468803b92e1" + assert nwGUI.docViewer.docHandle == "04468803b92e1" assert nwGUI.docViewer.docHeader.backButton.isEnabled() assert not nwGUI.docViewer.docHeader.forwardButton.isEnabled() @@ -438,10 +438,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): nwGUI.docEditor.clear() assert nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) is False - assert nwGUI.docEditor.isEmpty() + assert nwGUI.docEditor.isEmpty assert nwGUI.docEditor.insertText(None) is False - assert nwGUI.docEditor.isEmpty() + assert nwGUI.docEditor.isEmpty # qtbot.stop() @@ -653,21 +653,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd): # Reveal File Location # ==================== - theMessage = "" - - def recordMsg(*args, **kwargs): - nonlocal theMessage - theMessage = "%s|%s" % (args[0], kwargs["info"]) - return None - - assert not theMessage - monkeypatch.setattr(nwGUI, "makeAlert", recordMsg) nwGUI.mainMenu.aFileDetails.activate(QAction.Trigger) - - theBits = theMessage.split("|") - assert len(theBits) == 2 - assert theBits[0] == "The currently open file is saved in:" - assert theBits[1] == str(projPath / "content" / "000000000000f.nwd") + path = str(projPath / "content" / "000000000000f.nwd") + logMsg = SHARED.alert.logMessage if SHARED.alert else "" + assert logMsg == f"The currently open file is saved in: {path}" # qtbot.stop() diff --git a/tests/test_gui/test_gui_noveltree.py b/tests/test_gui/test_gui_noveltree.py index 86675cf8..4ada522b 100644 --- a/tests/test_gui/test_gui_noveltree.py +++ b/tests/test_gui/test_gui_noveltree.py @@ -29,7 +29,7 @@ from PyQt5.QtGui import QFocusEvent from PyQt5.QtCore import Qt, QEvent from PyQt5.QtWidgets import QInputDialog, QToolTip -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwWidget, nwItemType from novelwriter.gui.noveltree import NovelTreeColumn from novelwriter.dialogs.editlabel import GuiEditLabel @@ -48,7 +48,7 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): nwGUI.projView.projTree._getTreeItem(C.hCharRoot).setSelected(True) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) - contentPath = nwGUI.project.storage.contentPath + contentPath = SHARED.project.storage.contentPath assert isinstance(contentPath, Path) (contentPath / "0000000000010.nwd").write_text( @@ -118,26 +118,26 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Double-click item scItem.setSelected(True) assert scItem.isSelected() - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None novelTree._treeDoubleClick(scItem, 0) - assert nwGUI.docEditor.docHandle() == C.hSceneDoc + assert nwGUI.docEditor.docHandle == C.hSceneDoc # Open item with middle mouse button scItem.setSelected(True) assert scItem.isSelected() - assert nwGUI.docViewer.docHandle() is None + assert nwGUI.docViewer.docHandle is None qtbot.mouseClick(vPort, Qt.MiddleButton, pos=vPort.rect().center(), delay=10) - assert nwGUI.docViewer.docHandle() is None + assert nwGUI.docViewer.docHandle is None scRect = novelTree.visualItemRect(scItem) oldData = scItem.data(novelTree.C_TITLE, novelTree.D_HANDLE) scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, None) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) - assert nwGUI.docViewer.docHandle() is None + assert nwGUI.docViewer.docHandle is None scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) - assert nwGUI.docViewer.docHandle() == C.hSceneDoc + assert nwGUI.docViewer.docHandle == C.hSceneDoc # Last Column # =========== diff --git a/tests/test_gui/test_gui_outline.py b/tests/test_gui/test_gui_outline.py index b1bb74f8..85ea4640 100644 --- a/tests/test_gui/test_gui_outline.py +++ b/tests/test_gui/test_gui_outline.py @@ -27,7 +27,7 @@ from tools import buildTestProject, writeFile from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QWidget, QAction -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemClass, nwOutline, nwView @@ -71,7 +71,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, projPath): # Option State # ============ - pOptions = nwGUI.project.options + pOptions = SHARED.project.options colNames = [h.name for h in nwOutline] colItems = [h for h in nwOutline] colWidth = {h: outlineTree.DEF_WIDTH[h] for h in nwOutline} @@ -181,7 +181,7 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum): assert outlineBar.novelValue.itemData(2) == "" # All novels # Add a second novel folder - newHandle = nwGUI.project.newRoot(nwItemClass.NOVEL) + newHandle = SHARED.project.newRoot(nwItemClass.NOVEL) nwGUI.projView.projTree.revealNewTreeItem(newHandle) # Check new values in dropdown list @@ -198,7 +198,7 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum): ("Section 4", 4), ] for dTitle, hLevel in docList: - aHandle = nwGUI.project.newFile(dTitle, newHandle) + aHandle = SHARED.project.newFile(dTitle, newHandle) hHash = "#"*hLevel writeFile(prjLipsum / "content" / f"{aHandle}.nwd", f"{hHash} {dTitle}\n\n") nwGUI.projView.projTree.revealNewTreeItem(aHandle) @@ -246,7 +246,7 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum): # Click POV Link assert outlineData.povKeyValue.text() == "Bod" outlineView._tagClicked("Bod") - assert nwGUI.docViewer.docHandle() == "4c4f28287af27" + assert nwGUI.docViewer.docHandle == "4c4f28287af27" # Scene One, Section Two selItem = outlineTree.topLevelItem(5) @@ -262,7 +262,7 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum): assert outlineData.itemValue.text() == "Finished" outlineTree._treeDoubleClick(selItem, 0) - assert nwGUI.docEditor.docHandle() == "88243afbe5ed8" + assert nwGUI.docEditor.docHandle == "88243afbe5ed8" # qtbot.stop() diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index c5428506..bfb57a39 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -30,7 +30,7 @@ from mocked import causeOSError from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QMessageBox, QMenu, QTreeWidgetItem, QDialog -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass from novelwriter.guimain import GuiMain from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView @@ -46,7 +46,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn projView = nwGUI.projView projTree = nwGUI.projView.projTree - theProject = nwGUI.project + theProject = SHARED.project # Try to add item with no project assert projView.projTree.newTreeItem(nwItemType.FILE) is False @@ -260,19 +260,19 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # =========== projView.setSelectedHandle(C.hNovelRoot) - assert nwGUI.project.tree._treeOrder.index(C.hNovelRoot) == 0 + assert SHARED.project.tree._order.index(C.hNovelRoot) == 0 # Move novel folder up assert projTree.moveTreeItem(-1) is False - assert nwGUI.project.tree._treeOrder.index(C.hNovelRoot) == 0 + assert SHARED.project.tree._order.index(C.hNovelRoot) == 0 # Move novel folder down assert projTree.moveTreeItem(1) is True - assert nwGUI.project.tree._treeOrder.index(C.hNovelRoot) == 1 + assert SHARED.project.tree._order.index(C.hNovelRoot) == 1 # Move novel folder up again assert projTree.moveTreeItem(-1) is True - assert nwGUI.project.tree._treeOrder.index(C.hNovelRoot) == 0 + assert SHARED.project.tree._order.index(C.hNovelRoot) == 0 # Clean up # qtbot.stop() @@ -348,7 +348,7 @@ def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPat C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000010" ] - trashHandle = nwGUI.project.tree.trashRoot() + trashHandle = SHARED.project.tree.trashRoot() assert projTree.getTreeFromHandle(trashHandle) == [ trashHandle, "0000000000012", "0000000000011" ] @@ -368,7 +368,7 @@ def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, """Test moving items to Trash.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - theProject = nwGUI.project + theProject = SHARED.project projTree = nwGUI.projView.projTree # Create a project @@ -420,7 +420,7 @@ def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, pro """Test permanently deleting items.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - theProject = nwGUI.project + theProject = SHARED.project projTree = nwGUI.projView.projTree # Create a project @@ -450,10 +450,10 @@ def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, pro # Deleting file is OK, and if it is open, it should close assert nwGUI.openDocument(C.hTitlePage) is True - assert nwGUI.docEditor.docHandle() == C.hTitlePage + assert nwGUI.docEditor.docHandle == C.hTitlePage assert projTree.permDeleteItem(C.hTitlePage) is True assert C.hTitlePage not in theProject.tree - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None # Deleting folder + files recursively is ok assert projTree.permDeleteItem(C.hChapterDir) is True @@ -471,7 +471,7 @@ def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mock """Test emptying Trash.""" monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) - theProject = nwGUI.project + theProject = SHARED.project projTree = nwGUI.projView.projTree # No project open @@ -541,16 +541,16 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): projTree.setExpandedFromHandle(None, True) projTree._addTrashRoot() - hTrashRoot = nwGUI.project.tree.trashRoot() + hTrashRoot = SHARED.project.tree.trashRoot() projTree.setSelectedHandle(C.hCharRoot) projTree.newTreeItem(nwItemType.FILE) projTree.setSelectedHandle(C.hNovelRoot) projTree.newTreeItem(nwItemType.FILE, isNote=True) - nwGUI.project.newFile("SubNote", hNovelNote) + SHARED.project.newFile("SubNote", hNovelNote) projTree.revealNewTreeItem(hSubNote) - assert nwGUI.project.tree[hSubNote].itemParent == hNovelNote + assert SHARED.project.tree[hSubNote].itemParent == hNovelNote def itemPos(tHandle): return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center() @@ -578,7 +578,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Direct Edit Functions # ===================== # Trigger the dedicated functions the menu entries connect to - nwItem = nwGUI.project.tree[hNovelNote] + nwItem = SHARED.project.tree[hNovelNote] # Toggle active flag assert nwItem.isActive is True @@ -619,17 +619,17 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): with monkeypatch.context() as mp: mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT) - assert nwGUI.project.tree[hNewFolderOne].isFolderType() + assert SHARED.project.tree[hNewFolderOne].isFolderType() # Convert the first folder to a document projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT) - assert nwGUI.project.tree[hNewFolderOne].isFileType() - assert nwGUI.project.tree[hNewFolderOne].isDocumentLayout() + assert SHARED.project.tree[hNewFolderOne].isFileType() + assert SHARED.project.tree[hNewFolderOne].isDocumentLayout() # Convert the second folder to a note projTree._covertFolderToFile(hNewFolderTwo, nwItemLayout.NOTE) - assert nwGUI.project.tree[hNewFolderTwo].isFileType() - assert nwGUI.project.tree[hNewFolderTwo].isNoteLayout() + assert SHARED.project.tree[hNewFolderTwo].isFileType() + assert SHARED.project.tree[hNewFolderTwo].isNoteLayout() # qtbot.stop() @@ -649,7 +649,7 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, # Create a project buildTestProject(nwGUI, projPath) - theProject = nwGUI.project + theProject = SHARED.project projTree = nwGUI.projView.projTree mergedDoc1 = "0000000000014" @@ -751,7 +751,7 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, # Create a project buildTestProject(nwGUI, projPath) - theProject = nwGUI.project + theProject = SHARED.project projTree = nwGUI.projView.projTree docText = ( @@ -852,7 +852,7 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mock """Test the duplicate items function.""" # Create a project buildTestProject(nwGUI, projPath) - assert len(nwGUI.project.tree) == 8 + assert len(SHARED.project.tree) == 8 projTree = nwGUI.projView.projTree projTree._getTreeItem(C.hNovelRoot).setExpanded(True) # type: ignore @@ -860,28 +860,28 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mock # Nothing to do assert projTree._duplicateFromHandle(C.hInvalid) is False - assert len(nwGUI.project.tree) == 8 + assert len(SHARED.project.tree) == 8 # Duplicate title page, but select no with monkeypatch.context() as mp: mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) assert projTree._duplicateFromHandle(C.hTitlePage) is False - assert len(nwGUI.project.tree) == 8 + assert len(SHARED.project.tree) == 8 # Duplicate title page assert projTree._duplicateFromHandle(C.hTitlePage) is True - assert len(nwGUI.project.tree) == 9 + assert len(SHARED.project.tree) == 9 # Duplicate folder assert projTree._duplicateFromHandle(C.hChapterDir) is True - assert len(nwGUI.project.tree) == 12 + assert len(SHARED.project.tree) == 12 # Duplicate novel root assert projTree._duplicateFromHandle(C.hNovelRoot) is True - assert len(nwGUI.project.tree) == 21 + assert len(SHARED.project.tree) == 21 # Check tree order that all items are next to eachother - assert nwGUI.project.tree._treeOrder == [ + assert SHARED.project.tree._order == [ C.hNovelRoot, C.hTitlePage, "0000000000010", C.hChapterDir, C.hChapterDoc, C.hSceneDoc, "0000000000011", "0000000000012", "0000000000013", "0000000000014", "0000000000015", "0000000000016", "0000000000017", "0000000000018", "0000000000019", "000000000001a", @@ -889,7 +889,7 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mock ] # Make the duplicator stop early - content = nwGUI.project.storage.contentPath + content = SHARED.project.storage.contentPath assert isinstance(content, Path) (content / "000000000001e.nwd").touch() assert (content / "000000000001e.nwd").exists() @@ -897,7 +897,7 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mock # Should only create the folder, and skip the two files because the # next handle is already a file assert projTree._duplicateFromHandle(C.hChapterDir) is True - assert len(nwGUI.project.tree) == 22 + assert len(SHARED.project.tree) == 22 # qtbot.stop() @@ -938,13 +938,13 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd) assert projTree.revealNewTreeItem(C.hInvalid) is False # Try to add an orphaned file to the tree - nHandle = nwGUI.project.newFile("Test", C.hNovelRoot) - nwGUI.project.tree[nHandle].setParent(None) # type: ignore + nHandle = SHARED.project.newFile("Test", C.hNovelRoot) + SHARED.project.tree[nHandle].setParent(None) # type: ignore assert projTree.revealNewTreeItem(nHandle) is False # Try to add an item with unknown parent to the tree - nHandle = nwGUI.project.newFile("Test", C.hNovelRoot) - nwGUI.project.tree[nHandle].setParent(C.hInvalid) # type: ignore + nHandle = SHARED.project.newFile("Test", C.hNovelRoot) + SHARED.project.tree[nHandle].setParent(C.hInvalid) # type: ignore assert projTree.revealNewTreeItem(nHandle) is False # Method: undoLastMove @@ -969,25 +969,25 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd) # Try to open a file with nothings selected projTree.clearSelection() projTree._treeDoubleClick(QTreeWidgetItem(), 0) - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None # When the item cannot be found projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore with monkeypatch.context() as mp: mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None) projTree._treeDoubleClick(QTreeWidgetItem(), 0) - assert nwGUI.docEditor.docHandle() is None + assert nwGUI.docEditor.docHandle is None # Successfully open a file projTree._treeDoubleClick(projTree._getTreeItem(C.hTitlePage), 0) - assert nwGUI.docEditor.docHandle() == C.hTitlePage + assert nwGUI.docEditor.docHandle == C.hTitlePage projTree._getTreeItem(C.hTitlePage).setSelected(False) # type: ignore # A non-file item should be expanded instead projTree._getTreeItem(C.hNovelRoot).setExpanded(False) # type: ignore projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore projTree._treeDoubleClick(projTree._getTreeItem(C.hNovelRoot), 1) - assert nwGUI.docEditor.docHandle() == C.hTitlePage + assert nwGUI.docEditor.docHandle == C.hTitlePage assert projTree._getTreeItem(C.hNovelRoot).isExpanded() is True # type: ignore # Navigate the Tree diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index b227fb61..2a024f8a 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -24,17 +24,16 @@ import pytest from tools import C, buildTestProject -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.extensions.statusled import StatusLED @pytest.mark.gui def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd): - """Test the the various features of the status bar. - """ + """Test the the various features of the status bar.""" buildTestProject(nwGUI, projPath) - cHandle = nwGUI.project.newFile("A Note", C.hCharRoot) - newDoc = nwGUI.project.storage.getDocument(cHandle) + cHandle = SHARED.project.newFile("A Note", C.hCharRoot) + newDoc = SHARED.project.storage.getDocument(cHandle) newDoc.writeDocument("# A Note\n\n") nwGUI.projView.projTree.revealNewTreeItem(cHandle) nwGUI.rebuildIndex(beQuiet=True) @@ -42,7 +41,7 @@ def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd): # Reference Time refTime = time.time() nwGUI.mainStatus.setRefTime(refTime) - assert nwGUI.mainStatus.refTime == refTime + assert nwGUI.mainStatus._refTime == refTime # Project Status nwGUI.mainStatus.setProjectStatus(StatusLED.S_NONE) @@ -64,18 +63,18 @@ def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd): CONFIG.stopWhenIdle = False nwGUI.mainStatus.setUserIdle(True) nwGUI.mainStatus.updateTime() - assert nwGUI.mainStatus.userIdle is False + assert nwGUI.mainStatus._userIdle is False assert nwGUI.mainStatus.timeText.text() == "00:00:00" CONFIG.stopWhenIdle = True nwGUI.mainStatus.setUserIdle(True) nwGUI.mainStatus.updateTime(5) - assert nwGUI.mainStatus.userIdle is True + assert nwGUI.mainStatus._userIdle is True assert nwGUI.mainStatus.timeText.text() != "00:00:00" nwGUI.mainStatus.setUserIdle(False) nwGUI.mainStatus.updateTime(5) - assert nwGUI.mainStatus.userIdle is False + assert nwGUI.mainStatus._userIdle is False assert nwGUI.mainStatus.timeText.text() != "00:00:00" # Language diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 2f012229..f476ca94 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -30,7 +30,7 @@ from tools import writeFile from PyQt5.QtGui import QIcon, QPalette, QPixmap from PyQt5.QtWidgets import QApplication -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.constants import nwLabels @@ -38,7 +38,7 @@ from novelwriter.constants import nwLabels @pytest.mark.gui def testGuiTheme_Main(qtbot, nwGUI, tstPaths): """Test the theme class init.""" - mainTheme = CONFIG.theme + mainTheme = SHARED.theme # Methods # ======= @@ -121,7 +121,7 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths): @pytest.mark.gui def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI): """Test the theme part of the class.""" - mainTheme = CONFIG.theme + mainTheme = SHARED.theme # List Themes # =========== @@ -199,7 +199,7 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI): @pytest.mark.gui def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): """Test the syntax part of the class.""" - mainTheme = CONFIG.theme + mainTheme = SHARED.theme # List Themes # =========== @@ -264,7 +264,7 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): @pytest.mark.gui def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, tstPaths): """Test the icon cache class.""" - iconCache = CONFIG.theme.iconCache + iconCache = SHARED.theme.iconCache # Load Theme # ========== diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py index 7e78f073..271f9b0f 100644 --- a/tests/test_tools/test_tools_manusbuild.py +++ b/tests/test_tools/test_tools_manusbuild.py @@ -45,7 +45,7 @@ def testManuscriptBuild_Main( build = BuildSettings() build.setLastPath(fncPath) - manus = GuiManuscriptBuild(nwGUI, nwGUI, build) + manus = GuiManuscriptBuild(nwGUI, build) manus.show() # Check initial values @@ -101,7 +101,7 @@ def testManuscriptBuild_Main( # Error Handling # ============== - manus = GuiManuscriptBuild(nwGUI, nwGUI, build) + manus = GuiManuscriptBuild(nwGUI, build) manus.show() # Name, path and format should be remembered diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py index 9d58554d..af8a8146 100644 --- a/tests/test_tools/test_tools_manuscript.py +++ b/tests/test_tools/test_tools_manuscript.py @@ -32,7 +32,7 @@ from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtWidgets import QDialogButtonBox from PyQt5.QtPrintSupport import QPrintPreviewDialog -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.guimain import GuiMain from novelwriter.core.buildsettings import BuildSettings from novelwriter.tools.manuscript import GuiManuscript @@ -45,7 +45,7 @@ def testManuscript_Init(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: Pat """Test the init/main functionality of the GuiManuscript dialog.""" buildTestProject(nwGUI, projPath) nwGUI.openProject(projPath) - nwGUI.project.storage.getDocument(C.hChapterDoc).writeDocument("## A Chapter\n\n\t\tHi") + SHARED.project.storage.getDocument(C.hChapterDoc).writeDocument("## A Chapter\n\n\t\tHi") allText = "New Novel\nBy Jane Doe\nA Chapter\n\t\tHi\n* * *" manus = GuiManuscript(nwGUI) @@ -159,7 +159,7 @@ def testManuscript_Features(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: manus.show() manus.loadContent() - cacheFile = CONFIG.dataPath("cache") / f"build_{nwGUI.project.data.uuid}.json" + cacheFile = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json" manus.buildList.setCurrentRow(0) build = manus._getSelectedBuild() assert isinstance(build, BuildSettings) diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py index deccca1c..1371caf0 100644 --- a/tests/test_tools/test_tools_manussettings.py +++ b/tests/test_tools/test_tools_manussettings.py @@ -21,16 +21,16 @@ along with this program. If not, see . import pytest -from tools import C, buildTestProject - from pathlib import Path from pytestqt.qtbot import QtBot +from tools import C, buildTestProject + from PyQt5.QtGui import QFont from PyQt5.QtCore import pyqtSlot from PyQt5.QtWidgets import QDialogButtonBox, QFontDialog -from novelwriter import CONFIG +from novelwriter import CONFIG, SHARED from novelwriter.guimain import GuiMain from novelwriter.constants import nwHeadFmt from novelwriter.core.buildsettings import BuildSettings, FilterMode @@ -128,11 +128,11 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR "worldRoot": 9, } - hPlotDoc = nwGUI.project.newFile("Main Plot", C.hPlotRoot) - hCharDoc = nwGUI.project.newFile("Jane Doe", C.hCharRoot) + hPlotDoc = SHARED.project.newFile("Main Plot", C.hPlotRoot) + hCharDoc = SHARED.project.newFile("Jane Doe", C.hCharRoot) nwGUI.projView.projTree.revealNewTreeItem(hPlotDoc) nwGUI.projView.projTree.revealNewTreeItem(hCharDoc) - nwGUI.project.tree[hPlotDoc].setActive(False) # type: ignore + SHARED.project.tree[hPlotDoc].setActive(False) # type: ignore # Create the dialog and populate it bSettings = GuiBuildSettings(nwGUI, build) @@ -167,7 +167,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR # Switch off novel docs filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(False) - assert build.buildItemFilter(nwGUI.project) == { + assert build.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -182,7 +182,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR # Switch on note docs filterTab.filterOpt._widgets[switchMap["incNotes"]].setChecked(True) - assert build.buildItemFilter(nwGUI.project) == { + assert build.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -197,7 +197,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR # Switch on inactive docs filterTab.filterOpt._widgets[switchMap["incInactive"]].setChecked(True) - assert build.buildItemFilter(nwGUI.project) == { + assert build.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -214,7 +214,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR filterTab._treeMap[C.hChapterDoc].setSelected(True) filterTab._treeMap[C.hSceneDoc].setSelected(True) filterTab.includedButton.click() - assert build.buildItemFilter(nwGUI.project) == { + assert build.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -232,7 +232,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR filterTab._treeMap[hPlotDoc].setSelected(True) # type: ignore filterTab._treeMap[hCharDoc].setSelected(True) # type: ignore filterTab.excludedButton.click() - assert build.buildItemFilter(nwGUI.project) == { + assert build.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (False, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -247,7 +247,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR # Switch on novel docs filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(True) - assert build.buildItemFilter(nwGUI.project) == { + assert build.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (True, FilterMode.FILTERED), # Now enabled C.hChapterDir: (False, FilterMode.SKIPPED), @@ -264,7 +264,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR filterTab.optTree.clearSelection() filterTab._treeMap[C.hNovelRoot].setSelected(True) filterTab.resetButton.click() - assert build.buildItemFilter(nwGUI.project) == { + assert build.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (True, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -284,7 +284,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR filterTab._treeMap[hPlotDoc].setSelected(True) # type: ignore filterTab._treeMap[hCharDoc].setSelected(True) # type: ignore filterTab.resetButton.click() - assert build.buildItemFilter(nwGUI.project) == { + assert build.buildItemFilter(SHARED.project) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (True, FilterMode.FILTERED), C.hChapterDir: (False, FilterMode.SKIPPED), @@ -302,8 +302,8 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, C.hPlotRoot, hPlotDoc, C.hCharRoot, hCharDoc, ] - nwGUI.project.tree[hCharDoc].setRoot(None) # type: ignore - nwGUI.project.tree[hPlotDoc].setParent(None) # type: ignore + SHARED.project.tree[hCharDoc].setRoot(None) # type: ignore + SHARED.project.tree[hPlotDoc].setParent(None) # type: ignore filterTab._populateTree() assert list(filterTab._treeMap.keys()) == [ C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, diff --git a/tests/test_tools/test_tools_writingstats.py b/tests/test_tools/test_tools_writingstats.py index 3674004c..64de8775 100644 --- a/tests/test_tools/test_tools_writingstats.py +++ b/tests/test_tools/test_tools_writingstats.py @@ -20,15 +20,17 @@ along with this program. If not, see . """ import json -from pathlib import Path import pytest +from pathlib import Path + from tools import getGuiItem, buildTestProject from mocked import causeOSError from PyQt5.QtCore import Qt from PyQt5.QtWidgets import QAction, QFileDialog +from novelwriter import SHARED from novelwriter.constants import nwFiles from novelwriter.tools.writingstats import GuiWritingStats @@ -39,7 +41,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, projPath, tstPaths): """ # Create a project to work on buildTestProject(nwGUI, projPath) - project = nwGUI.project + project = SHARED.project qtbot.wait(100) assert nwGUI.saveProject() @@ -377,13 +379,14 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, projPath, tstPaths): # IOError # ======= - monkeypatch.setattr("builtins.open", causeOSError) - assert not sessLog._loadLogFile() - assert not sessLog._saveData(sessLog.FMT_CSV) + with monkeypatch.context() as mp: + mp.setattr("builtins.open", causeOSError) + assert not sessLog._loadLogFile() + assert not sessLog._saveData(sessLog.FMT_CSV) # qtbot.stop() sessLog._doClose() - assert nwGUI.closeProject() + assert nwGUI.closeProject() is True # END Test testToolWritingStats_Main diff --git a/tests/tools.py b/tests/tools.py index f8f3f03c..6230d1a2 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -165,10 +165,10 @@ def buildTestProject(obj, projPath): nwGUI = None project = obj else: + from novelwriter import SHARED nwGUI = obj - project = obj.project + project = SHARED.project - project.clearProject() project.storage.openProjectInPlace(projPath) project.setDefaultStatusImport() @@ -204,9 +204,9 @@ def buildTestProject(obj, projPath): project.session.startSession() project.setProjectChanged(True) project.saveProject(autoSave=True) + project._valid = True if nwGUI is not None: - nwGUI.hasProject = True nwGUI.rebuildTrees() return