Remove re-usage of project class (#1504)_

This commit is contained in:
Veronica Berglyd Olsen
2023-08-23 19:56:58 +01:00
committed by GitHub
80 changed files with 2032 additions and 1810 deletions
+2
View File
@@ -31,6 +31,7 @@ from PyQt5.QtWidgets import QApplication, QErrorMessage
from novelwriter.error import exceptionHandler, logException from novelwriter.error import exceptionHandler, logException
from novelwriter.config import Config from novelwriter.config import Config
from novelwriter.shared import SharedData
## ##
# Version Scheme # Version Scheme
@@ -74,6 +75,7 @@ logger = logging.getLogger(__name__)
# Global config singleton # Global config singleton
CONFIG = Config() CONFIG = Config()
SHARED = SharedData()
def main(sysArgs: list | None = None): def main(sysArgs: list | None = None):
+1 -2
View File
@@ -512,8 +512,7 @@ def sha256sum(path: str | Path) -> str | None:
# =============================================================================================== # # =============================================================================================== #
def getGuiItem(objName: str) -> QWidget | 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(): for qWidget in qApp.topLevelWidgets():
if qWidget.objectName() == objName: if qWidget.objectName() == objName:
return qWidget return qWidget
+3 -18
View File
@@ -3,7 +3,8 @@ novelWriter Config Class
========================== ==========================
File History: 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 This file is a part of novelWriter
Copyright 20182023, Veronica Berglyd Olsen Copyright 20182023, Veronica Berglyd Olsen
@@ -28,7 +29,6 @@ import json
import logging import logging
from time import time from time import time
from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from PyQt5.QtGui import QFontDatabase 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.common import NWConfigParser, checkInt, checkPath, formatTimeStamp
from novelwriter.constants import nwFiles, nwUnicode from novelwriter.constants import nwFiles, nwUnicode
if TYPE_CHECKING: # pragma: no cover
from novelwriter.gui.theme import GuiTheme
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -96,7 +93,6 @@ class Config:
# User Settings # User Settings
# ============= # =============
self._themeObj = None
self._recentObj = RecentProjects(self) self._recentObj = RecentProjects(self)
# General GUI Settings # General GUI Settings
@@ -244,12 +240,6 @@ class Config:
def recentProjects(self) -> RecentProjects: def recentProjects(self) -> RecentProjects:
return self._recentObj 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 @property
def mainWinSize(self) -> list[int]: def mainWinSize(self) -> list[int]:
return [int(x*self.guiScale) for x in self._mainWinSize] return [int(x*self.guiScale) for x in self._mainWinSize]
@@ -297,11 +287,6 @@ class Config:
# Setters # Setters
## ##
def setThemeInstance(self, theme: GuiTheme) -> None:
"""Set the applications theme instance."""
self._themeObj = theme
return
def setMainWinSize(self, width: int, height: int) -> None: def setMainWinSize(self, width: int, height: int) -> None:
"""Set the size of the main window, but only if the change is """Set the size of the main window, but only if the change is
larger than 5 pixels. The OS window manager will sometimes larger than 5 pixels. The OS window manager will sometimes
@@ -499,7 +484,7 @@ class Config:
self._recentObj.loadCache() self._recentObj.loadCache()
self._checkOptionalPackages() self._checkOptionalPackages()
logger.debug("Config initialisation complete") logger.debug("Config instance initialised")
return return
+4 -7
View File
@@ -25,7 +25,7 @@ from __future__ import annotations
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP 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: def trConst(text: str) -> str:
@@ -52,6 +52,9 @@ class nwConst:
URL_HELP = "https://github.com/vkbo/novelWriter/discussions" URL_HELP = "https://github.com/vkbo/novelWriter/discussions"
URL_RELEASE = "https://github.com/vkbo/novelWriter/releases/latest" URL_RELEASE = "https://github.com/vkbo/novelWriter/releases/latest"
# Gui Settings
STATUS_MSG_TIMEOUT = 15000 # milliseconds
# END Class nwConst # END Class nwConst
@@ -162,12 +165,6 @@ class nwLabels:
nwItemLayout.DOCUMENT: QT_TRANSLATE_NOOP("Constant", "Novel Document"), nwItemLayout.DOCUMENT: QT_TRANSLATE_NOOP("Constant", "Novel Document"),
nwItemLayout.NOTE: QT_TRANSLATE_NOOP("Constant", "Project Note"), 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 = { ITEM_DESCRIPTION = {
"none": QT_TRANSLATE_NOOP("Constant", "None"), "none": QT_TRANSLATE_NOOP("Constant", "None"),
"root": QT_TRANSLATE_NOOP("Constant", "Root Folder"), "root": QT_TRANSLATE_NOOP("Constant", "Root Folder"),
+8 -13
View File
@@ -28,21 +28,17 @@ from __future__ import annotations
import shutil import shutil
import logging import logging
from typing import TYPE_CHECKING, Iterable from typing import Iterable
from functools import partial from functools import partial
from PyQt5.QtCore import QCoreApplication from PyQt5.QtCore import QCoreApplication
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwAlert
from novelwriter.common import minmax, simplified from novelwriter.common import minmax, simplified
from novelwriter.constants import nwItemClass from novelwriter.constants import nwItemClass
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -312,8 +308,7 @@ class ProjectBuilder:
parameter provided by the New Project Wizard. parameter provided by the New Project Wizard.
""" """
def __init__(self, mainGui: GuiMain) -> None: def __init__(self) -> None:
self.mainGui = mainGui
self.tr = partial(QCoreApplication.translate, "NWProject") self.tr = partial(QCoreApplication.translate, "NWProject")
return return
@@ -344,7 +339,7 @@ class ProjectBuilder:
logger.error("No project path set for the new project") logger.error("No project path set for the new project")
return False return False
project = NWProject(self.mainGui) project = NWProject()
if not project.storage.openProjectInPlace(projPath, newProject=True): if not project.storage.openProjectInPlace(projPath, newProject=True):
return False return False
@@ -478,17 +473,17 @@ class ProjectBuilder:
try: try:
shutil.unpack_archive(pkgSample, projPath) shutil.unpack_archive(pkgSample, projPath)
except Exception as exc: except Exception as exc:
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr(
"Failed to create a new example project." "Failed to create a new example project."
), level=nwAlert.ERROR, exception=exc) ), exc=exc)
return False return False
else: else:
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr(
"Failed to create a new example project. " "Failed to create a new example project. "
"Could not find the necessary files. " "Could not find the necessary files. "
"They seem to be missing from this installation." "They seem to be missing from this installation."
), level=nwAlert.ERROR) ))
return False return False
return True return True
+1 -1
View File
@@ -533,7 +533,7 @@ class NWIndex:
def getTableOfContents( def getTableOfContents(
self, rHandle: str, maxDepth: int, skipExcl: bool = True 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.""" """Generate a table of contents up to a maximum depth."""
tOrder = [] tOrder = []
tData = {} tData = {}
+85 -119
View File
@@ -33,8 +33,8 @@ from functools import partial
from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal
from novelwriter import CONFIG, __version__, __hexversion__ from novelwriter import CONFIG, SHARED, __version__, __hexversion__
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.constants import trConst, nwLabels from novelwriter.constants import trConst, nwLabels
from novelwriter.core.tree import NWTree from novelwriter.core.tree import NWTree
@@ -49,7 +49,6 @@ from novelwriter.common import (
) )
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
from novelwriter.core.status import NWStatus from novelwriter.core.status import NWStatus
@@ -58,13 +57,11 @@ logger = logging.getLogger(__name__)
class NWProject(QObject): class NWProject(QObject):
projectStatusChanged = pyqtSignal(bool) statusChanged = pyqtSignal(bool)
statusMessage = pyqtSignal(str)
def __init__(self, mainGui: GuiMain) -> None: def __init__(self, parent: QObject | None = None) -> None:
super().__init__(parent=mainGui) super().__init__(parent=parent)
# Internal
self.mainGui = mainGui
# Core Elements # Core Elements
self._options = OptionState(self) # Project-specific GUI options self._options = OptionState(self) # Project-specific GUI options
@@ -75,13 +72,20 @@ class NWProject(QObject):
self._session = NWSessionLog(self) # The session record self._session = NWSessionLog(self) # The session record
# Project Status # Project Status
self._langData = {} # Localisation data self._langData = {} # Localisation data
self._projChanged = False # The project has unsaved changes self._lockedBy = None # Data on which computer has the project open
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 # Internal Mapping
self.tr = partial(QCoreApplication.translate, "NWProject") self.tr = partial(QCoreApplication.translate, "NWProject")
logger.debug("Ready: NWProject")
return
def __del__(self): # pragma: no cover
logger.debug("Delete: NWProject")
return return
## ##
@@ -118,7 +122,24 @@ class NWProject(QObject):
@property @property
def projChanged(self) -> bool: 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 # Item Methods
@@ -139,7 +160,7 @@ class NWProject(QObject):
"""Add a new file with a given label and parent item.""" """Add a new file with a given label and parent item."""
return self._tree.create(label, parent, nwItemType.FILE) 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 """Write content to a new document after it is created. This
will not run if the file exists and is not empty. will not run if the file exists and is not empty.
""" """
@@ -154,7 +175,7 @@ class NWProject(QObject):
return False return False
hshText = "#"*minmax(hLevel, 1, 4) 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: if tItem.isNovelLike() and isDocument:
tItem.setLayout(nwItemLayout.DOCUMENT) tItem.setLayout(nwItemLayout.DOCUMENT)
else: else:
@@ -172,9 +193,9 @@ class NWProject(QObject):
if self._tree.checkType(tHandle, nwItemType.FILE): if self._tree.checkType(tHandle, nwItemType.FILE):
delDoc = self._storage.getDocument(tHandle) delDoc = self._storage.getDocument(tHandle)
if not delDoc.deleteDocument(): if not delDoc.deleteDocument():
self.mainGui.makeAlert( SHARED.error(
self.tr("Could not delete document file."), self.tr("Could not delete document file."),
info=delDoc.getError(), level=nwAlert.ERROR info=delDoc.getError()
) )
return False return False
@@ -195,45 +216,21 @@ class NWProject(QObject):
# Project Methods # Project Methods
## ##
def clearProject(self) -> None: def openProject(self, projPath: str | Path, clearLock: bool = False) -> bool:
"""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:
"""Open the project file provided. If it doesn't exist, assume """Open the project file provided. If it doesn't exist, assume
it is a folder and look for the file within it. If successful, 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 parse the XML of the file and populate the project variables and
build the tree of project items. build the tree of project items.
""" """
self.clearProject()
logger.info("Opening project: %s", projPath) logger.info("Opening project: %s", projPath)
if not self._storage.openProjectInPlace(projPath): if not self._storage.openProjectInPlace(projPath):
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr("Could not open project with path: {0}").format(projPath))
"Could not open project with path: {0}"
).format(projPath), level=nwAlert.ERROR)
return False return False
# Project Lock # Project Lock
# ============ # ============
if overrideLock: if clearLock:
self._storage.clearLockFile() self._storage.clearLockFile()
lockStatus = self._storage.readLockFile() lockStatus = self._storage.readLockFile()
@@ -243,7 +240,6 @@ class NWProject(QObject):
else: else:
logger.error("Project is locked, so not opening") logger.error("Project is locked, so not opening")
self._lockedBy = lockStatus self._lockedBy = lockStatus
self.clearProject()
return False return False
else: else:
logger.debug("Project is not locked") logger.debug("Project is not locked")
@@ -253,7 +249,6 @@ class NWProject(QObject):
xmlReader = self._storage.getXmlReader() xmlReader = self._storage.getXmlReader()
if not isinstance(xmlReader, ProjectXMLReader): if not isinstance(xmlReader, ProjectXMLReader):
self.clearProject()
return False return False
self._data = NWProjectData(self) self._data = NWProjectData(self)
@@ -264,49 +259,43 @@ class NWProject(QObject):
if not xmlParsed: if not xmlParsed:
if xmlReader.state == XMLReadState.NOT_NWX_FILE: 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." "Project file does not appear to be a novelWriterXML file."
), level=nwAlert.ERROR) ))
elif xmlReader.state == XMLReadState.UNKNOWN_VERSION: elif xmlReader.state == XMLReadState.UNKNOWN_VERSION:
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr(
"Unknown or unsupported novelWriter project file format. " "Unknown or unsupported novelWriter project file format. "
"The project cannot be opened by this version of novelWriter. " "The project cannot be opened by this version of novelWriter. "
"The file was saved with novelWriter version {0}." "The file was saved with novelWriter version {0}."
).format(appVersion), level=nwAlert.ERROR) ).format(appVersion))
else: else:
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr("Failed to parse project xml."))
"Failed to parse project xml."
), level=nwAlert.ERROR)
self.clearProject()
return False return False
# Check Legacy Upgrade # Check Legacy Upgrade
# ==================== # ====================
if xmlReader.state == XMLReadState.WAS_LEGACY: 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. " "The file format of your project is about to be updated. "
"If you proceed, older versions of novelWriter will no " "If you proceed, older versions of novelWriter will no "
"longer be able to open this project. Continue?" "longer be able to open this project. Continue?"
)) ))
if not msgYes: if not msgYes:
self.clearProject()
return False return False
# Check novelWriter Version # Check novelWriter Version
# ========================= # =========================
if xmlReader.hexVersion > hexToInt(__hexversion__): 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 " "This project was saved by a newer version of "
"novelWriter, version {0}. This is version {1}. If you " "novelWriter, version {0}. This is version {1}. If you "
"continue to open the project, some attributes and " "continue to open the project, some attributes and "
"settings may not be preserved, but the overall project " "settings may not be preserved, but the overall project "
"should be fine. Continue opening the project?" "should be fine. Continue opening the project?"
).format(appVersion, __version__)) ).format(appVersion, __version__), warn=True)
if not msgYes: if not msgYes:
self.clearProject()
return False return False
# Extract Data # Extract Data
@@ -317,17 +306,19 @@ class NWProject(QObject):
self._loadProjectLocalisation() self._loadProjectLocalisation()
# Update recent projects # Update recent projects
CONFIG.recentProjects.update( storePath = self._storage.storagePath
self._storage.storagePath, self._data.name, sum(self._data.initCounts), time() if storePath:
) CONFIG.recentProjects.update(
storePath, self._data.name, sum(self._data.initCounts), time()
)
# Check the project tree consistency # Check the project tree consistency
# This also handles any orphaned files found # This also handles any orphaned files found
orphans, recovered = self._tree.checkConsistency(self.tr("Recovered")) orphans, recovered = self._tree.checkConsistency(self.tr("Recovered"))
if orphans > 0: 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." "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() self._index.loadIndex()
if xmlReader.state == XMLReadState.WAS_LEGACY: if xmlReader.state == XMLReadState.WAS_LEGACY:
@@ -338,7 +329,9 @@ class NWProject(QObject):
self._session.startSession() self._session.startSession()
self._storage.writeLockFile() self._storage.writeLockFile()
self.setProjectChanged(False) 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 return True
@@ -349,9 +342,7 @@ class NWProject(QObject):
file. file.
""" """
if not self._storage.isOpen(): if not self._storage.isOpen():
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr("There is no project open."))
"There is no project open."
), level=nwAlert.ERROR)
return False return False
saveTime = time() saveTime = time()
@@ -374,9 +365,7 @@ class NWProject(QObject):
editTime = self._data.editTime + max(round(saveTime - self._session.start), 0) editTime = self._data.editTime + max(round(saveTime - self._session.start), 0)
content = self._tree.pack() content = self._tree.pack()
if not xmlWriter.write(self._data, content, saveTime, editTime): if not xmlWriter.write(self._data, content, saveTime, editTime):
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr("Failed to save project."), exc=xmlWriter.error)
"Failed to save project."
), level=nwAlert.ERROR, exception=xmlWriter.error)
return False return False
# Save other project data # Save other project data
@@ -385,24 +374,25 @@ class NWProject(QObject):
self._storage.runPostSaveTasks(autoSave=autoSave) self._storage.runPostSaveTasks(autoSave=autoSave)
# Update recent projects # Update recent projects
CONFIG.recentProjects.update( storePath = self._storage.storagePath
self._storage.storagePath, self._data.name, sum(self._data.currCounts), saveTime if storePath:
) CONFIG.recentProjects.update(
storePath, self._data.name, sum(self._data.currCounts), saveTime
)
self._storage.writeLockFile() 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) self.setProjectChanged(False)
return True return True
def closeProject(self, idleTime: float = 0.0) -> None: def closeProject(self, idleTime: float = 0.0) -> None:
"""Close the current project and clear all meta data.""" """Close the project."""
logger.info("Closing project") logger.info("Closing project")
self._options.saveSettings() self._options.saveSettings()
self._tree.writeToCFile() self._tree.writeToCFile()
self._session.appendSession(idleTime) self._session.appendSession(idleTime)
self._storage.closeSession() self._storage.closeSession()
self.clearProject()
self._lockedBy = None self._lockedBy = None
return return
@@ -413,13 +403,13 @@ class NWProject(QObject):
return False return False
logger.info("Backing up project") 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: if not self._data.name:
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr(
"Cannot backup project because no project name is set. " "Cannot backup project because no project name is set. "
"Please set a Project Name in Project Settings." "Please set a Project Name in Project Settings."
), level=nwAlert.ERROR) ))
return False return False
cleanName = makeFileNameSafe(self._data.name) cleanName = makeFileNameSafe(self._data.name)
@@ -428,9 +418,7 @@ class NWProject(QObject):
try: try:
baseDir.mkdir(exist_ok=True, parents=True) baseDir.mkdir(exist_ok=True, parents=True)
except Exception as exc: except Exception as exc:
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr("Could not create backup folder."), exc=exc)
"Could not create backup folder."
), level=nwAlert.ERROR, exception=exc)
return False return False
timeStamp = formatTimeStamp(time(), fileSafe=True) timeStamp = formatTimeStamp(time(), fileSafe=True)
@@ -438,19 +426,15 @@ class NWProject(QObject):
if self._storage.zipIt(archName, compression=2): if self._storage.zipIt(archName, compression=2):
size = formatInt(archName.stat().st_size) size = formatInt(archName.stat().st_size)
if doNotify: if doNotify:
self.mainGui.makeAlert( SHARED.info(
self.tr("Created a backup of your project of size {0}B.").format(size), self.tr("Created a backup of your project of size {0}B.").format(size),
info=self.tr("Path: {0}").format(str(backupPath)) info=self.tr("Path: {0}").format(str(backupPath))
) )
else: else:
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr("Could not write backup archive."))
"Could not write backup archive."
), level=nwAlert.ERROR)
return False return False
self.mainGui.setStatus(self.tr( self.statusMessage.emit(self.tr("Project backed up to '{0}'").format(str(archName)))
"Project backed up to '{0}'"
).format(str(archName)))
return True return True
@@ -503,27 +487,15 @@ class NWProject(QObject):
information to the GUI statusbar. information to the GUI statusbar.
""" """
if isinstance(status, bool): if isinstance(status, bool):
self._projChanged = status self._changed = status
self.projectStatusChanged.emit(self._projChanged) self.statusChanged.emit(self._changed)
return self._projChanged return self._changed
## ##
# Getters # Class Methods
## ##
def getLockStatus(self) -> list | None: def iterProjectItems(self) -> Iterator[NWItem]:
"""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]:
"""This function ensures that the item tree loaded is sent to """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 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, 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 order has been altered, or a file is orphaned, this function is
capable of handling it. capable of handling it.
""" """
sentItems = [] sentItems = set()
iterItems = self._tree.handles() iterItems = self._tree.handles()
n = 0 n = 0
nMax = min(len(iterItems), 10000) nMax = min(len(iterItems), 10000)
@@ -540,17 +512,15 @@ class NWProject(QObject):
tItem = self._tree[tHandle] tItem = self._tree[tHandle]
n += 1 n += 1
if tItem is None: if tItem is None:
# Technically a bug since treeOrder is built from the # Technically a bug
# same data as _projTree
continue continue
elif tItem.itemParent is None: elif tItem.itemParent is None:
# Item is a root, or already been identified as an # Item is a root, or already been identified as orphaned
# orphaned item sentItems.add(tHandle)
sentItems.append(tHandle)
yield tItem yield tItem
elif tItem.itemParent in sentItems: elif tItem.itemParent in sentItems:
# Item's parent has been sent, so all is fine # Item's parent has been sent, so all is fine
sentItems.append(tHandle) sentItems.add(tHandle)
yield tItem yield tItem
elif tItem.itemParent in iterItems: elif tItem.itemParent in iterItems:
# Item's parent exists, but hasn't been sent yet, so add # Item's parent exists, but hasn't been sent yet, so add
@@ -566,10 +536,6 @@ class NWProject(QObject):
yield tItem yield tItem
return return
##
# Class Methods
##
def updateWordCounts(self) -> None: def updateWordCounts(self) -> None:
"""Update the total word count values.""" """Update the total word count values."""
novel, notes = self._tree.sumWords() novel, notes = self._tree.sumWords()
+9 -9
View File
@@ -213,15 +213,15 @@ class UserDictionary:
def load(self) -> None: def load(self) -> None:
"""Load the user's dictionary.""" """Load the user's dictionary."""
self._path = self._project.storage.getMetaFile(nwFiles.DICT_FILE) self._path = self._project.storage.getMetaFile(nwFiles.DICT_FILE)
if not isinstance(self._path, Path): self._words = set()
return if isinstance(self._path, Path) and self._path.is_file():
try: try:
with open(self._path, mode="r", encoding="utf-8") as fObj: with open(self._path, mode="r", encoding="utf-8") as fObj:
data = json.load(fObj) data = json.load(fObj)
self._words = set(data.get("novelWriter.userDict", [])) self._words = set(data.get("novelWriter.userDict", []))
except Exception: except Exception:
logger.error("Failed to load user dictionary") logger.error("Failed to load user dictionary")
logException() logException()
return return
def save(self) -> None: def save(self) -> None:
+6 -6
View File
@@ -129,7 +129,7 @@ class NWStatus:
def name(self, key: str | None) -> str: def name(self, key: str | None) -> str:
"""Return the name associated with a given key.""" """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"] return self._store[key]["name"]
elif self._default is not None: elif self._default is not None:
return self._store[self._default]["name"] return self._store[self._default]["name"]
@@ -137,7 +137,7 @@ class NWStatus:
def cols(self, key: str | None) -> tuple[int, int, int]: def cols(self, key: str | None) -> tuple[int, int, int]:
"""Return the colours associated with a given key.""" """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"] return self._store[key]["cols"]
elif self._default is not None: elif self._default is not None:
return self._store[self._default]["cols"] return self._store[self._default]["cols"]
@@ -145,7 +145,7 @@ class NWStatus:
def count(self, key: str | None) -> int: def count(self, key: str | None) -> int:
"""Return the count associated with a given key.""" """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"] return self._store[key]["count"]
elif self._default is not None: elif self._default is not None:
return self._store[self._default]["count"] return self._store[self._default]["count"]
@@ -153,7 +153,7 @@ class NWStatus:
def icon(self, key: str | None) -> QIcon: def icon(self, key: str | None) -> QIcon:
"""Return the icon associated with a given key.""" """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"] return self._store[key]["icon"]
elif self._default is not None: elif self._default is not None:
return self._store[self._default]["icon"] return self._store[self._default]["icon"]
@@ -186,9 +186,9 @@ class NWStatus:
self._store[key]["count"] = 0 self._store[key]["count"] = 0
return return
def increment(self, key: str) -> None: def increment(self, key: str | None) -> None:
"""Increment the counter for a given entry.""" """Increment the counter for a given entry."""
if key in self._store: if key and key in self._store:
self._store[key]["count"] += 1 self._store[key]["count"] += 1
return return
+7 -9
View File
@@ -36,7 +36,7 @@ from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression 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.common import formatTimeStamp, numberToRoman, checkInt
from novelwriter.constants import nwConst, nwHeadFmt, nwRegEx, nwUnicode from novelwriter.constants import nwConst, nwHeadFmt, nwRegEx, nwUnicode
from novelwriter.core.project import NWProject from novelwriter.core.project import NWProject
@@ -315,10 +315,8 @@ class Tokenizer(ABC):
def addRootHeading(self, tHandle: str) -> bool: def addRootHeading(self, tHandle: str) -> bool:
"""Add a heading at the start of a new root folder.""" """Add a heading at the start of a new root folder."""
if not self._project.tree.checkType(tHandle, nwItemType.ROOT): tItem = self._project.tree[tHandle]
return False if not tItem or not tItem.isRootType():
theItem = self._project.tree[tHandle]
if not theItem:
return False return False
if self._isFirst: if self._isFirst:
@@ -327,14 +325,14 @@ class Tokenizer(ABC):
else: else:
textAlign = self.A_PBB | self.A_CENTRE textAlign = self.A_PBB | self.A_CENTRE
locNotes = self._localLookup("Notes") trNotes = self._localLookup("Notes")
theTitle = f"{locNotes}: {theItem.itemName}" title = f"{trNotes}: {tItem.itemName}"
self._tokens = [] self._tokens = []
self._tokens.append(( self._tokens.append((
self.T_TITLE, 0, theTitle, None, textAlign self.T_TITLE, 0, title, None, textAlign
)) ))
if self._keepMarkdown: if self._keepMarkdown:
self._allMarkdown.append(f"# {theTitle}\n\n") self._allMarkdown.append(f"# {title}\n\n")
return True return True
+58 -63
View File
@@ -65,13 +65,12 @@ class NWTree:
self._project = project self._project = project
self._projTree: dict[str, NWItem] = {} # Holds all the items of the project self._tree: 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._order: list[str] = [] # The order of the tree items in the tree view
self._treeRoots: dict[str, NWItem] = {} # The root items of the tree self._roots: dict[str, NWItem] = {} # The root items of the tree
self._trashRoot = None # The handle of the trash root folder self._trash = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder self._changed = False # True if tree structure has changed
self._treeChanged = False # True if tree structure has changed
return return
@@ -81,17 +80,16 @@ class NWTree:
def clear(self) -> None: def clear(self) -> None:
"""Clear the item tree entirely.""" """Clear the item tree entirely."""
self._projTree = {} self._tree = {}
self._treeOrder = [] self._order = []
self._treeRoots = {} self._roots = {}
self._trashRoot = None self._trash = None
self._archRoot = None self._changed = False
self._treeChanged = False
return return
def handles(self) -> list[str]: def handles(self) -> list[str]:
"""Returns a copy of the list of all the active handles.""" """Returns a copy of the list of all the active handles."""
return self._treeOrder.copy() return self._order.copy()
@overload # pragma: no cover @overload # pragma: no cover
def create(self, label: str, parent: None, itemType: Literal[nwItemType.ROOT], 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 is returned. For root elements, this cannot occur.
""" """
parent = None if itemType == nwItemType.ROOT else parent 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() tHandle = self._makeHandle()
newItem = NWItem(self._project, tHandle) newItem = NWItem(self._project, tHandle)
newItem.setName(label) newItem.setName(label)
@@ -130,7 +128,7 @@ class NWTree:
logger.warning("Invalid item handle '%s' detected, skipping", tHandle) logger.warning("Invalid item handle '%s' detected, skipping", tHandle)
return False return False
if tHandle in self._projTree: if tHandle in self._tree:
logger.warning("Duplicate handle '%s' detected, skipping", tHandle) logger.warning("Duplicate handle '%s' detected, skipping", tHandle)
return False return False
@@ -138,20 +136,17 @@ class NWTree:
if nwItem.isRootType(): if nwItem.isRootType():
logger.debug("Item '%s' is a root item", str(tHandle)) logger.debug("Item '%s' is a root item", str(tHandle))
self._treeRoots[tHandle] = nwItem self._roots[tHandle] = nwItem
if nwItem.itemClass == nwItemClass.ARCHIVE: if nwItem.itemClass == nwItemClass.TRASH:
logger.debug("Item '%s' is the archive folder", str(tHandle)) if self._trash is None:
self._archRoot = tHandle
elif nwItem.itemClass == nwItemClass.TRASH:
if self._trashRoot is None:
logger.debug("Item '%s' is the trash folder", str(tHandle)) logger.debug("Item '%s' is the trash folder", str(tHandle))
self._trashRoot = tHandle self._trash = tHandle
else: else:
logger.error("Only one trash folder allowed") logger.error("Only one trash folder allowed")
return False return False
self._projTree[tHandle] = nwItem self._tree[tHandle] = nwItem
self._treeOrder.append(tHandle) self._order.append(tHandle)
self._setTreeChanged(True) self._setTreeChanged(True)
return True return True
@@ -171,7 +166,7 @@ class NWTree:
items. In the order defined by the _treeOrder list. items. In the order defined by the _treeOrder list.
""" """
tree = [] tree = []
for tHandle in self._treeOrder: for tHandle in self._order:
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem: if tItem:
tree.append(tItem.pack()) tree.append(tItem.pack())
@@ -199,7 +194,7 @@ class NWTree:
""" """
storage = self._project.storage storage = self._project.storage
files = set(storage.scanContent()) files = set(storage.scanContent())
for tHandle in self._treeOrder: for tHandle in self._order:
if self.updateItemData(tHandle): if self.updateItemData(tHandle):
logger.debug("Checking item '%s' ... OK", tHandle) logger.debug("Checking item '%s' ... OK", tHandle)
files.discard(tHandle) # Remove it from the record files.discard(tHandle) # Remove it from the record
@@ -220,7 +215,7 @@ class NWTree:
oName, oParent, oClass, oLayout = aDoc.getMeta() oName, oParent, oClass, oLayout = aDoc.getMeta()
oName = oName or cHandle 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 oClass = oClass or nwItemClass.NOVEL
oLayout = oLayout or nwItemLayout.NOTE oLayout = oLayout or nwItemLayout.NOTE
@@ -229,8 +224,10 @@ class NWTree:
oParent = self.findRoot(oClass) oParent = self.findRoot(oClass)
if oParent is None: # Otherwise, add to the Novel root if oParent is None: # Otherwise, add to the Novel root
oParent = self.findRoot(nwItemClass.NOVEL) oParent = self.findRoot(nwItemClass.NOVEL)
if oParent is None: # If not, give up if oParent is None: # If not, create a new novel folder
continue 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 # Create a new item
newItem = NWItem(self._project, cHandle) newItem = NWItem(self._project, cHandle)
@@ -256,7 +253,7 @@ class NWTree:
tocList = [] tocList = []
tocLen = 0 tocLen = 0
for tHandle in self._treeOrder: for tHandle in self._order:
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is None: if tItem is None:
continue continue
@@ -298,7 +295,7 @@ class NWTree:
"""Loop over all entries and add up the word counts.""" """Loop over all entries and add up the word counts."""
noteWords = 0 noteWords = 0
novelWords = 0 novelWords = 0
for tHandle in self._treeOrder: for tHandle in self._order:
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is None: if tItem is None:
continue continue
@@ -374,13 +371,13 @@ class NWTree:
def rootClasses(self) -> set[nwItemClass]: def rootClasses(self) -> set[nwItemClass]:
"""Return a set of all root classes in use by the project.""" """Return a set of all root classes in use by the project."""
rootClasses = set() rootClasses = set()
for nwItem in self._treeRoots.values(): for nwItem in self._roots.values():
rootClasses.add(nwItem.itemClass) rootClasses.add(nwItem.itemClass)
return rootClasses return rootClasses
def iterRoots(self, itemClass: nwItemClass | None) -> Iterator[tuple[str, NWItem]]: def iterRoots(self, itemClass: nwItemClass | None) -> Iterator[tuple[str, NWItem]]:
"""Iterate over all root items of a given class in order.""" """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) nwItem = self.__getitem__(tHandle)
if isinstance(nwItem, NWItem) and nwItem.isRootType(): if isinstance(nwItem, NWItem) and nwItem.isRootType():
if itemClass is None or nwItem.itemClass == itemClass: if itemClass is None or nwItem.itemClass == itemClass:
@@ -394,12 +391,12 @@ class NWTree:
return True return True
if tItem.itemClass == nwItemClass.TRASH: if tItem.itemClass == nwItemClass.TRASH:
return True return True
if self._trashRoot is not None: if self._trash is not None:
if tHandle == self._trashRoot: if tHandle == self._trash:
return True return True
elif tItem.itemParent == self._trashRoot: elif tItem.itemParent == self._trash:
return True return True
elif tItem.itemRoot == self._trashRoot: elif tItem.itemRoot == self._trash:
return True return True
return False return False
@@ -407,13 +404,13 @@ class NWTree:
"""Returns the handle of the trash folder, or None if there """Returns the handle of the trash folder, or None if there
isn't one. isn't one.
""" """
if self._trashRoot: if self._trash:
return self._trashRoot return self._trash
return None return None
def findRoot(self, itemClass: nwItemClass | None) -> str | None: def findRoot(self, itemClass: nwItemClass | None) -> str | None:
"""Find the first root item for a given class.""" """Find the first root item for a given class."""
for aRoot in self._treeRoots: for aRoot in self._roots:
tItem = self.__getitem__(aRoot) tItem = self.__getitem__(aRoot)
if tItem is None: if tItem is None:
continue continue
@@ -427,18 +424,18 @@ class NWTree:
def setOrder(self, newOrder: list[str]) -> None: def setOrder(self, newOrder: list[str]) -> None:
"""Reorders the tree based on a list of items.""" """Reorders the tree based on a list of items."""
tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._projTree] tmpOrder = [tHandle for tHandle in newOrder if tHandle in self._tree]
if not (len(tmpOrder) == len(newOrder) == len(self._treeOrder)): if not (len(tmpOrder) == len(newOrder) == len(self._order)):
# Something is wrong, so let's debug it # Something is wrong, so let's debug it
for tHandle in newOrder: 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) 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: if tHandle not in tmpOrder:
logger.warning("Handle '%s' in old tree order is not in new order", tHandle) logger.warning("Handle '%s' in old tree order is not in new order", tHandle)
# Save the temp list # Save the temp list
self._treeOrder = tmpOrder self._order = tmpOrder
self._setTreeChanged(True) self._setTreeChanged(True)
logger.debug("Project tree order updated") logger.debug("Project tree order updated")
@@ -450,36 +447,34 @@ class NWTree:
def __len__(self) -> int: def __len__(self) -> int:
"""The number of items in the project.""" """The number of items in the project."""
return len(self._treeOrder) return len(self._order)
def __bool__(self) -> bool: def __bool__(self) -> bool:
"""True if there are any items in the project.""" """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: def __getitem__(self, tHandle: str | None) -> NWItem | None:
"""Return a project item based on its handle. Returns None if """Return a project item based on its handle. Returns None if
the handle doesn't exist in the project. the handle doesn't exist in the project.
""" """
if tHandle and tHandle in self._projTree: if tHandle and tHandle in self._tree:
return self._projTree[tHandle] return self._tree[tHandle]
logger.error("No tree item with handle '%s'", str(tHandle)) logger.error("No tree item with handle '%s'", str(tHandle))
return None return None
def __delitem__(self, tHandle: str) -> None: def __delitem__(self, tHandle: str) -> None:
"""Remove an item from the internal lists and dictionaries.""" """Remove an item from the internal lists and dictionaries."""
if tHandle in self._treeOrder and tHandle in self._projTree: if tHandle in self._order and tHandle in self._tree:
self._treeOrder.remove(tHandle) self._order.remove(tHandle)
del self._projTree[tHandle] del self._tree[tHandle]
else: else:
logger.warning("Failed to delete item '%s': item not found", tHandle) logger.warning("Failed to delete item '%s': item not found", tHandle)
return return
if tHandle in self._treeRoots: if tHandle in self._roots:
del self._treeRoots[tHandle] del self._roots[tHandle]
if tHandle == self._trashRoot: if tHandle == self._trash:
self._trashRoot = None self._trash = None
if tHandle == self._archRoot:
self._archRoot = None
self._setTreeChanged(True) self._setTreeChanged(True)
@@ -487,12 +482,12 @@ class NWTree:
def __contains__(self, tHandle: str) -> bool: def __contains__(self, tHandle: str) -> bool:
"""Checks if a handle exists in the tree.""" """Checks if a handle exists in the tree."""
return tHandle in self._treeOrder return tHandle in self._order
def __iter__(self) -> Iterator[NWItem]: def __iter__(self) -> Iterator[NWItem]:
"""Iterate through project items.""" """Iterate through project items."""
for tHandle in self._treeOrder: for tHandle in self._order:
tItem = self._projTree.get(tHandle) tItem = self._tree.get(tHandle)
if isinstance(tItem, NWItem): if isinstance(tItem, NWItem):
yield tItem yield tItem
return return
@@ -505,7 +500,7 @@ class NWTree:
"""Set the changed flag to theState, and if being set to True, """Set the changed flag to theState, and if being set to True,
propagate that state change to the parent NWProject class. propagate that state change to the parent NWProject class.
""" """
self._treeChanged = state self._changed = state
if state: if state:
self._project.setProjectChanged(True) self._project.setProjectChanged(True)
return return
@@ -516,7 +511,7 @@ class NWTree:
""" """
logger.debug("Generating new handle") logger.debug("Generating new handle")
handle = f"{random.getrandbits(52):013x}" handle = f"{random.getrandbits(52):013x}"
if handle in self._projTree: if handle in self._tree:
logger.warning("Duplicate handle encountered! Retrying ...") logger.warning("Duplicate handle encountered! Retrying ...")
handle = self._makeHandle() handle = self._makeHandle()
+8 -8
View File
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
QTextBrowser, QVBoxLayout, QWidget QTextBrowser, QVBoxLayout, QWidget
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.common import readTextFile from novelwriter.common import readTextFile
from novelwriter.constants import nwConst from novelwriter.constants import nwConst
@@ -60,7 +60,7 @@ class GuiAbout(QDialog):
nPx = CONFIG.pxInt(96) nPx = CONFIG.pxInt(96)
self.nwIcon = QLabel() self.nwIcon = QLabel()
self.nwIcon.setPixmap(CONFIG.theme.getPixmap("novelwriter", (nPx, nPx))) self.nwIcon.setPixmap(SHARED.theme.getPixmap("novelwriter", (nPx, nPx)))
self.lblName = QLabel("<b>novelWriter</b>") self.lblName = QLabel("<b>novelWriter</b>")
self.lblVers = QLabel(f"v{novelwriter.__version__}") self.lblVers = QLabel(f"v{novelwriter.__version__}")
self.lblDate = QLabel(datetime.strptime(novelwriter.__date__, "%Y-%m-%d").strftime("%x")) self.lblDate = QLabel(datetime.strptime(novelwriter.__date__, "%Y-%m-%d").strftime("%x"))
@@ -228,12 +228,12 @@ class GuiAbout(QDialog):
" color: rgb({kColR},{kColG},{kColB});" " color: rgb({kColR},{kColG},{kColB});"
"}}\n" "}}\n"
).format( ).format(
hColR=CONFIG.theme.colHead[0], hColR=SHARED.theme.colHead[0],
hColG=CONFIG.theme.colHead[1], hColG=SHARED.theme.colHead[1],
hColB=CONFIG.theme.colHead[2], hColB=SHARED.theme.colHead[2],
kColR=CONFIG.theme.colKey[0], kColR=SHARED.theme.colKey[0],
kColG=CONFIG.theme.colKey[1], kColG=SHARED.theme.colKey[1],
kColB=CONFIG.theme.colKey[2], kColB=SHARED.theme.colKey[2],
) )
self.pageAbout.document().setDefaultStyleSheet(styleSheet) self.pageAbout.document().setDefaultStyleSheet(styleSheet)
self.pageNotes.document().setDefaultStyleSheet(styleSheet) self.pageNotes.document().setDefaultStyleSheet(styleSheet)
+9 -12
View File
@@ -29,10 +29,10 @@ import logging
from PyQt5.QtCore import Qt, QSize from PyQt5.QtCore import Qt, QSize
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QDialog, QDialogButtonBox, QGridLayout, QLabel, 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.switch import NSwitch
from novelwriter.extensions.configlayout import NHelpLabel from novelwriter.extensions.configlayout import NHelpLabel
@@ -43,24 +43,21 @@ class GuiDocMerge(QDialog):
D_HANDLE = Qt.ItemDataRole.UserRole D_HANDLE = Qt.ItemDataRole.UserRole
def __init__(self, mainGui, sHandle, itemList): def __init__(self, parent: QWidget, sHandle: str, itemList: list[str]) -> None:
super().__init__(parent=mainGui) super().__init__(parent=parent)
logger.debug("Create: GuiDocMerge") logger.debug("Create: GuiDocMerge")
self.setObjectName("GuiDocMerge") self.setObjectName("GuiDocMerge")
self.setWindowTitle(self.tr("Merge Documents"))
self.mainGui = mainGui
self._data = {} self._data = {}
self.setWindowTitle(self.tr("Merge Documents"))
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge"))) self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Documents to Merge")))
self.helpLabel = NHelpLabel(self.tr( self.helpLabel = NHelpLabel(self.tr(
"Drag and drop items to change the order, or uncheck to exclude." "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) hSp = CONFIG.pxInt(12)
vSp = CONFIG.pxInt(8) vSp = CONFIG.pxInt(8)
bSp = CONFIG.pxInt(12) bSp = CONFIG.pxInt(12)
@@ -155,11 +152,11 @@ class GuiDocMerge(QDialog):
self.listBox.clear() self.listBox.clear()
for tHandle in itemList: for tHandle in itemList:
nwItem = self.mainGui.project.tree[tHandle] nwItem = SHARED.project.tree[tHandle]
if nwItem is None or not nwItem.isFileType(): if nwItem is None or not nwItem.isFileType():
continue continue
itemIcon = CONFIG.theme.getItemIcon( itemIcon = SHARED.theme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading
) )
+9 -11
View File
@@ -32,7 +32,7 @@ from PyQt5.QtWidgets import (
QListWidgetItem, QDialogButtonBox, QLabel, QGridLayout QListWidgetItem, QDialogButtonBox, QLabel, QGridLayout
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.configlayout import NHelpLabel from novelwriter.extensions.configlayout import NHelpLabel
@@ -45,14 +45,12 @@ class GuiDocSplit(QDialog):
LEVEL_ROLE = Qt.ItemDataRole.UserRole + 1 LEVEL_ROLE = Qt.ItemDataRole.UserRole + 1
LABEL_ROLE = Qt.ItemDataRole.UserRole + 2 LABEL_ROLE = Qt.ItemDataRole.UserRole + 2
def __init__(self, mainGui, sHandle): def __init__(self, parent, sHandle):
super().__init__(parent=mainGui) super().__init__(parent=parent)
logger.debug("Create: GuiDocSplit") logger.debug("Create: GuiDocSplit")
self.setObjectName("GuiDocSplit") self.setObjectName("GuiDocSplit")
self.mainGui = mainGui
self._data = {} self._data = {}
self._text = [] self._text = []
@@ -61,16 +59,16 @@ class GuiDocSplit(QDialog):
self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers"))) self.headLabel = QLabel("<b>{0}</b>".format(self.tr("Document Headers")))
self.helpLabel = NHelpLabel( self.helpLabel = NHelpLabel(
self.tr("Select the maximum level to split into files."), self.tr("Select the maximum level to split into files."),
CONFIG.theme.helpText SHARED.theme.helpText
) )
# Values # Values
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
hSp = CONFIG.pxInt(12) hSp = CONFIG.pxInt(12)
vSp = CONFIG.pxInt(8) vSp = CONFIG.pxInt(8)
bSp = CONFIG.pxInt(12) bSp = CONFIG.pxInt(12)
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
spLevel = pOptions.getInt("GuiDocSplit", "spLevel", 3) spLevel = pOptions.getInt("GuiDocSplit", "spLevel", 3)
intoFolder = pOptions.getBool("GuiDocSplit", "intoFolder", True) intoFolder = pOptions.getBool("GuiDocSplit", "intoFolder", True)
docHierarchy = pOptions.getBool("GuiDocSplit", "docHierarchy", True) docHierarchy = pOptions.getBool("GuiDocSplit", "docHierarchy", True)
@@ -169,7 +167,7 @@ class GuiDocSplit(QDialog):
self._data["docHierarchy"] = docHierarchy self._data["docHierarchy"] = docHierarchy
self._data["moveToTrash"] = moveToTrash self._data["moveToTrash"] = moveToTrash
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
pOptions.setValue("GuiDocSplit", "spLevel", spLevel) pOptions.setValue("GuiDocSplit", "spLevel", spLevel)
pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder) pOptions.setValue("GuiDocSplit", "intoFolder", intoFolder)
pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy) pOptions.setValue("GuiDocSplit", "docHierarchy", docHierarchy)
@@ -199,13 +197,13 @@ class GuiDocSplit(QDialog):
self.listBox.clear() self.listBox.clear()
nwItem = self.mainGui.project.tree[sHandle] nwItem = SHARED.project.tree[sHandle]
if nwItem is None or not nwItem.isFileType(): if nwItem is None or not nwItem.isFileType():
return return
spLevel = self.splitLevel.currentData() spLevel = self.splitLevel.currentData()
if not self._text: if not self._text:
inDoc = self.mainGui.project.storage.getDocument(sHandle) inDoc = SHARED.project.storage.getDocument(sHandle)
self._text = (inDoc.readDocument() or "").splitlines() self._text = (inDoc.readDocument() or "").splitlines()
for lineNo, aLine in enumerate(self._text): for lineNo, aLine in enumerate(self._text):
+19 -23
View File
@@ -32,7 +32,7 @@ from PyQt5.QtWidgets import (
QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox QLineEdit, QFileDialog, QFontDialog, QDoubleSpinBox
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.dialogs.quotes import GuiQuoteSelect
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.pageddialog import NPagedDialog from novelwriter.extensions.pageddialog import NPagedDialog
@@ -163,7 +163,7 @@ class GuiPreferencesGeneral(QWidget):
# The Form # The Form
self.mainForm = NConfigLayout() self.mainForm = NConfigLayout()
self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
self.setLayout(self.mainForm) self.setLayout(self.mainForm)
# Look and Feel # Look and Feel
@@ -190,7 +190,7 @@ class GuiPreferencesGeneral(QWidget):
# Select Theme # Select Theme
self.guiTheme = QComboBox() self.guiTheme = QComboBox()
self.guiTheme.setMinimumWidth(minWidth) self.guiTheme.setMinimumWidth(minWidth)
self.theThemes = CONFIG.theme.listThemes() self.theThemes = SHARED.theme.listThemes()
for themeDir, themeName in self.theThemes: for themeDir, themeName in self.theThemes:
self.guiTheme.addItem(themeName, themeDir) self.guiTheme.addItem(themeName, themeDir)
themeIdx = self.guiTheme.findData(CONFIG.guiTheme) themeIdx = self.guiTheme.findData(CONFIG.guiTheme)
@@ -206,7 +206,7 @@ class GuiPreferencesGeneral(QWidget):
# Editor Theme # Editor Theme
self.guiSyntax = QComboBox() self.guiSyntax = QComboBox()
self.guiSyntax.setMinimumWidth(CONFIG.pxInt(200)) self.guiSyntax.setMinimumWidth(CONFIG.pxInt(200))
self.theSyntaxes = CONFIG.theme.listSyntax() self.theSyntaxes = SHARED.theme.listSyntax()
for syntaxFile, syntaxName in self.theSyntaxes: for syntaxFile, syntaxName in self.theSyntaxes:
self.guiSyntax.addItem(syntaxName, syntaxFile) self.guiSyntax.addItem(syntaxName, syntaxFile)
syntaxIdx = self.guiSyntax.findData(CONFIG.guiSyntax) syntaxIdx = self.guiSyntax.findData(CONFIG.guiSyntax)
@@ -225,7 +225,7 @@ class GuiPreferencesGeneral(QWidget):
self.guiFont.setFixedWidth(CONFIG.pxInt(162)) self.guiFont.setFixedWidth(CONFIG.pxInt(162))
self.guiFont.setText(CONFIG.guiFont) self.guiFont.setText(CONFIG.guiFont)
self.fontButton = QPushButton("...") 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.fontButton.clicked.connect(self._selectFont)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Font family"), self.tr("Font family"),
@@ -341,7 +341,7 @@ class GuiPreferencesProjects(QWidget):
# The Form # The Form
self.mainForm = NConfigLayout() self.mainForm = NConfigLayout()
self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
self.setLayout(self.mainForm) self.setLayout(self.mainForm)
# Automatic Save # Automatic Save
@@ -493,7 +493,7 @@ class GuiPreferencesDocuments(QWidget):
# The Form # The Form
self.mainForm = NConfigLayout() self.mainForm = NConfigLayout()
self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
self.setLayout(self.mainForm) self.setLayout(self.mainForm)
# Text Style # Text Style
@@ -506,7 +506,7 @@ class GuiPreferencesDocuments(QWidget):
self.textFont.setFixedWidth(CONFIG.pxInt(162)) self.textFont.setFixedWidth(CONFIG.pxInt(162))
self.textFont.setText(CONFIG.textFont) self.textFont.setText(CONFIG.textFont)
self.fontButton = QPushButton("...") 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.fontButton.clicked.connect(self._selectFont)
self.mainForm.addRow( self.mainForm.addRow(
self.tr("Font family"), self.tr("Font family"),
@@ -649,7 +649,7 @@ class GuiPreferencesEditor(QWidget):
# The Form # The Form
self.mainForm = NConfigLayout() self.mainForm = NConfigLayout()
self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
self.setLayout(self.mainForm) self.setLayout(self.mainForm)
mW = CONFIG.pxInt(250) mW = CONFIG.pxInt(250)
@@ -663,17 +663,13 @@ class GuiPreferencesEditor(QWidget):
self.spellLanguage.setMaximumWidth(mW) self.spellLanguage.setMaximumWidth(mW)
langAvail = self.mainGui.docEditor.spEnchant.listDictionaries() langAvail = self.mainGui.docEditor.spEnchant.listDictionaries()
if CONFIG.hasEnchant: if CONFIG.hasEnchant and langAvail:
if langAvail: for spTag, spProv in langAvail:
for spTag, spProv in langAvail: qLocal = QLocale(spTag)
qLocal = QLocale(spTag) spLang = qLocal.nativeLanguageName().title()
spLang = qLocal.nativeLanguageName().title() self.spellLanguage.addItem("%s [%s]" % (spLang, spProv), spTag)
self.spellLanguage.addItem("%s [%s]" % (spLang, spProv), spTag)
else:
self.spellLanguage.addItem(self.tr("None"), "")
self.spellLanguage.setEnabled(False)
else: else:
self.spellLanguage.addItem(self.tr("Not installed"), "") self.spellLanguage.addItem(self.tr("None"), "")
self.spellLanguage.setEnabled(False) self.spellLanguage.setEnabled(False)
spellIdx = self.spellLanguage.findData(CONFIG.spellLanguage) spellIdx = self.spellLanguage.findData(CONFIG.spellLanguage)
@@ -819,7 +815,7 @@ class GuiPreferencesSyntax(QWidget):
# The Form # The Form
self.mainForm = NConfigLayout() self.mainForm = NConfigLayout()
self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
self.setLayout(self.mainForm) self.setLayout(self.mainForm)
# Quotes & Dialogue # Quotes & Dialogue
@@ -921,7 +917,7 @@ class GuiPreferencesAutomation(QWidget):
# The Form # The Form
self.mainForm = NConfigLayout() self.mainForm = NConfigLayout()
self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
self.setLayout(self.mainForm) self.setLayout(self.mainForm)
# Automatic Features # Automatic Features
@@ -1072,7 +1068,7 @@ class GuiPreferencesQuotes(QWidget):
# The Form # The Form
self.mainForm = NConfigLayout() self.mainForm = NConfigLayout()
self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
self.setLayout(self.mainForm) self.setLayout(self.mainForm)
# Quotation Style # Quotation Style
@@ -1080,7 +1076,7 @@ class GuiPreferencesQuotes(QWidget):
self.mainForm.addGroupLabel(self.tr("Quotation Style")) self.mainForm.addGroupLabel(self.tr("Quotation Style"))
qWidth = CONFIG.pxInt(40) qWidth = CONFIG.pxInt(40)
bWidth = int(2.5*CONFIG.theme.getTextWidth("...")) bWidth = int(2.5*SHARED.theme.getTextWidth("..."))
self.quoteSym = {} self.quoteSym = {}
# Single Quote Style # Single Quote Style
+28 -34
View File
@@ -33,7 +33,7 @@ from PyQt5.QtWidgets import (
QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget QLineEdit, QSpinBox, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.common import formatTime, numberToRoman from novelwriter.common import formatTime, numberToRoman
from novelwriter.constants import nwUnicode from novelwriter.constants import nwUnicode
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
@@ -45,19 +45,17 @@ logger = logging.getLogger(__name__)
class GuiProjectDetails(NPagedDialog): class GuiProjectDetails(NPagedDialog):
def __init__(self, mainGui): def __init__(self, parent):
super().__init__(parent=mainGui) super().__init__(parent=parent)
logger.debug("Create: GuiProjectDetails") logger.debug("Create: GuiProjectDetails")
self.setObjectName("GuiProjectDetails") self.setObjectName("GuiProjectDetails")
self.mainGui = mainGui
self.setWindowTitle(self.tr("Project Details")) self.setWindowTitle(self.tr("Project Details"))
wW = CONFIG.pxInt(600) wW = CONFIG.pxInt(600)
wH = CONFIG.pxInt(400) wH = CONFIG.pxInt(400)
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
self.setMinimumWidth(wW) self.setMinimumWidth(wW)
self.setMinimumHeight(wH) self.setMinimumHeight(wH)
@@ -66,8 +64,8 @@ class GuiProjectDetails(NPagedDialog):
CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "winHeight", wH)) CONFIG.pxInt(pOptions.getInt("GuiProjectDetails", "winHeight", wH))
) )
self.tabMain = GuiProjectDetailsMain(self.mainGui) self.tabMain = GuiProjectDetailsMain(self)
self.tabContents = GuiProjectDetailsContents(self.mainGui) self.tabContents = GuiProjectDetailsContents(self)
self.addTab(self.tabMain, self.tr("Overview")) self.addTab(self.tabMain, self.tr("Overview"))
self.addTab(self.tabContents, self.tr("Contents")) self.addTab(self.tabContents, self.tr("Contents"))
@@ -124,7 +122,7 @@ class GuiProjectDetails(NPagedDialog):
countFrom = self.tabContents.poValue.value() countFrom = self.tabContents.poValue.value()
clearDouble = self.tabContents.dblValue.isChecked() clearDouble = self.tabContents.dblValue.isChecked()
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
pOptions.setValue("GuiProjectDetails", "winWidth", winWidth) pOptions.setValue("GuiProjectDetails", "winWidth", winWidth)
pOptions.setValue("GuiProjectDetails", "winHeight", winHeight) pOptions.setValue("GuiProjectDetails", "winHeight", winHeight)
pOptions.setValue("GuiProjectDetails", "widthCol0", widthCol0) pOptions.setValue("GuiProjectDetails", "widthCol0", widthCol0)
@@ -143,13 +141,11 @@ class GuiProjectDetails(NPagedDialog):
class GuiProjectDetailsMain(QWidget): class GuiProjectDetailsMain(QWidget):
def __init__(self, mainGui): def __init__(self, parent):
super().__init__(parent=mainGui) super().__init__(parent=parent)
self.mainGui = mainGui fPx = SHARED.theme.fontPixelSize
fPt = SHARED.theme.fontPointSize
fPx = CONFIG.theme.fontPixelSize
fPt = CONFIG.theme.fontPointSize
vPx = CONFIG.pxInt(4) vPx = CONFIG.pxInt(4)
hPx = CONFIG.pxInt(12) hPx = CONFIG.pxInt(12)
@@ -241,14 +237,13 @@ class GuiProjectDetailsMain(QWidget):
return return
def updateValues(self): def updateValues(self) -> None:
"""Set all the values. """Set all the values."""
""" project = SHARED.project
project = self.mainGui.project
pIndex = project.index pIndex = project.index
hCounts = pIndex.getNovelTitleCounts() hCounts = pIndex.getNovelTitleCounts()
nwCount = pIndex.getNovelWordCount() nwCount = pIndex.getNovelWordCount()
edTime = project.getCurrentEditTime() edTime = project.currentEditTime
self.bookTitle.setText(project.data.title or project.data.name) self.bookTitle.setText(project.data.title or project.data.name)
self.projName.setText(self.tr("Project: {0}").format(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_PAGE = 3
C_PROG = 4 C_PROG = 4
def __init__(self, mainGui): def __init__(self, parent):
super().__init__(parent=mainGui) super().__init__(parent=parent)
self.mainGui = mainGui
# Internal # Internal
self._theToC = [] self._theToC = []
self._currentRoot = None self._currentRoot = None
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
hPx = CONFIG.pxInt(12) hPx = CONFIG.pxInt(12)
vPx = CONFIG.pxInt(4) vPx = CONFIG.pxInt(4)
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
# Header # Header
# ====== # ======
self.tocLabel = QLabel("<b>%s</b>" % self.tr("Table of Contents")) self.tocLabel = QLabel("<b>%s</b>" % self.tr("Table of Contents"))
self.novelValue = NovelSelector(self, self.mainGui) self.novelValue = NovelSelector(self)
self.novelValue.setMinimumWidth(CONFIG.pxInt(200)) self.novelValue.setMinimumWidth(CONFIG.pxInt(200))
self.novelValue.novelSelectionChanged.connect(self._novelValueChanged) self.novelValue.novelSelectionChanged.connect(self._novelValueChanged)
@@ -320,10 +313,11 @@ class GuiProjectDetailsContents(QWidget):
]) ])
treeHeadItem = self.tocTree.headerItem() treeHeadItem = self.tocTree.headerItem()
treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight) if treeHeadItem:
treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignRight) treeHeadItem.setTextAlignment(self.C_WORDS, Qt.AlignRight)
treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignRight) treeHeadItem.setTextAlignment(self.C_PAGES, Qt.AlignRight)
treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignRight) treeHeadItem.setTextAlignment(self.C_PAGE, Qt.AlignRight)
treeHeadItem.setTextAlignment(self.C_PROG, Qt.AlignRight)
treeHeader = self.tocTree.header() treeHeader = self.tocTree.header()
treeHeader.setStretchLastSection(True) treeHeader.setStretchLastSection(True)
@@ -347,7 +341,7 @@ class GuiProjectDetailsContents(QWidget):
wordsPerPage = pOptions.getInt("GuiProjectDetails", "wordsPerPage", 350) wordsPerPage = pOptions.getInt("GuiProjectDetails", "wordsPerPage", 350)
countFrom = pOptions.getInt("GuiProjectDetails", "countFrom", 1) countFrom = pOptions.getInt("GuiProjectDetails", "countFrom", 1)
clearDouble = pOptions.getInt("GuiProjectDetails", "clearDouble", True) clearDouble = pOptions.getBool("GuiProjectDetails", "clearDouble", True)
wordsHelp = ( wordsHelp = (
self.tr("Typical word count for a 5 by 8 inch book page with 11 pt font is 350.") 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. """Extract the information from the project index.
""" """
logger.debug("Populating ToC from handle '%s'", rootHandle) 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)) self._theToC.append(("", 0, self.tr("END"), 0))
return return
@@ -496,7 +490,7 @@ class GuiProjectDetailsContents(QWidget):
progPage = f"{cPage:n}" progPage = f"{cPage:n}"
progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%" progText = f"{pgProg:.1f}{nwUnicode.U_THSP}%"
hDec = CONFIG.theme.getHeaderDecoration(tLevel) hDec = SHARED.theme.getHeaderDecoration(tLevel)
if tTitle.strip() == "": if tTitle.strip() == "":
tTitle = self.tr("Untitled") tTitle = self.tr("Untitled")
+12 -9
View File
@@ -25,6 +25,7 @@ from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from datetime import datetime from datetime import datetime
@@ -36,10 +37,13 @@ from PyQt5.QtWidgets import (
QFileDialog, QLineEdit QFileDialog, QLineEdit
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.common import formatInt from novelwriter.common import formatInt
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -55,19 +59,18 @@ class GuiProjectLoad(QDialog):
D_PATH = Qt.ItemDataRole.UserRole D_PATH = Qt.ItemDataRole.UserRole
def __init__(self, mainGui): def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
logger.debug("Create: GuiProjectLoad") logger.debug("Create: GuiProjectLoad")
self.setObjectName("GuiProjectLoad") self.setObjectName("GuiProjectLoad")
self.mainGui = mainGui
self.openState = self.NONE_STATE self.openState = self.NONE_STATE
self.openPath = None self.openPath = None
sPx = CONFIG.pxInt(16) sPx = CONFIG.pxInt(16)
nPx = CONFIG.pxInt(96) nPx = CONFIG.pxInt(96)
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
self.outerBox = QVBoxLayout() self.outerBox = QVBoxLayout()
self.innerBox = QHBoxLayout() self.innerBox = QHBoxLayout()
@@ -79,7 +82,7 @@ class GuiProjectLoad(QDialog):
self.setMinimumHeight(CONFIG.pxInt(400)) self.setMinimumHeight(CONFIG.pxInt(400))
self.nwIcon = QLabel() 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.innerBox.addWidget(self.nwIcon, 0, Qt.AlignTop)
self.projectForm = QGridLayout() self.projectForm = QGridLayout()
@@ -110,7 +113,7 @@ class GuiProjectLoad(QDialog):
self.selPath.setReadOnly(True) self.selPath.setReadOnly(True)
self.browseButton = QPushButton("...") 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.browseButton.clicked.connect(self._doBrowse)
self.projectForm.addWidget(self.lblRecent, 0, 0, 1, 3) self.projectForm.addWidget(self.lblRecent, 0, 0, 1, 3)
@@ -225,7 +228,7 @@ class GuiProjectLoad(QDialog):
selList = self.listBox.selectedItems() selList = self.listBox.selectedItems()
if selList: if selList:
projName = selList[0].text(self.C_NAME) 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? " "Remove '{0}' from the recent projects list? "
"The project files will not be deleted." "The project files will not be deleted."
).format(projName)) ).format(projName))
@@ -268,7 +271,7 @@ class GuiProjectLoad(QDialog):
self.listBox.clear() self.listBox.clear()
dataList = CONFIG.recentProjects.listEntries() dataList = CONFIG.recentProjects.listEntries()
sortList = sorted(dataList, key=lambda x: x[3], reverse=True) 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: for path, title, words, time in sortList:
newItem = QTreeWidgetItem([""]*4) newItem = QTreeWidgetItem([""]*4)
newItem.setIcon(self.C_NAME, nwxIcon) 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_NAME, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter) newItem.setTextAlignment(self.C_COUNT, Qt.AlignRight | Qt.AlignVCenter)
newItem.setTextAlignment(self.C_TIME, 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.addTopLevelItem(newItem)
self.listBox.setCurrentItem(self.listBox.topLevelItem(0)) self.listBox.setCurrentItem(self.listBox.topLevelItem(0))
+20 -26
View File
@@ -34,8 +34,7 @@ from PyQt5.QtWidgets import (
QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget QPushButton, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwAlert
from novelwriter.common import simplified from novelwriter.common import simplified
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
from novelwriter.extensions.pageddialog import NPagedDialog from novelwriter.extensions.pageddialog import NPagedDialog
@@ -61,12 +60,12 @@ class GuiProjectSettings(NPagedDialog):
self.setObjectName("GuiProjectSettings") self.setObjectName("GuiProjectSettings")
self.mainGui = mainGui self.mainGui = mainGui
self.mainGui.project.countStatus() SHARED.project.countStatus()
self.setWindowTitle(self.tr("Project Settings")) self.setWindowTitle(self.tr("Project Settings"))
wW = CONFIG.pxInt(570) wW = CONFIG.pxInt(570)
wH = CONFIG.pxInt(375) wH = CONFIG.pxInt(375)
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
self.setMinimumWidth(wW) self.setMinimumWidth(wW)
self.setMinimumHeight(wH) self.setMinimumHeight(wH)
@@ -115,7 +114,7 @@ class GuiProjectSettings(NPagedDialog):
def _doSave(self): def _doSave(self):
"""Save settings and close dialog. """Save settings and close dialog.
""" """
project = self.mainGui.project project = SHARED.project
projName = self.tabMain.editName.text() projName = self.tabMain.editName.text()
bookTitle = self.tabMain.editTitle.text() bookTitle = self.tabMain.editTitle.text()
bookAuthor = self.tabMain.editAuthor.text() bookAuthor = self.tabMain.editAuthor.text()
@@ -183,7 +182,7 @@ class GuiProjectSettings(NPagedDialog):
statusColW = CONFIG.rpxInt(self.tabStatus.listBox.columnWidth(0)) statusColW = CONFIG.rpxInt(self.tabStatus.listBox.columnWidth(0))
importColW = CONFIG.rpxInt(self.tabImport.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", "winWidth", winWidth)
pOptions.setValue("GuiProjectSettings", "winHeight", winHeight) pOptions.setValue("GuiProjectSettings", "winHeight", winHeight)
pOptions.setValue("GuiProjectSettings", "replaceColW", replaceColW) pOptions.setValue("GuiProjectSettings", "replaceColW", replaceColW)
@@ -204,13 +203,13 @@ class GuiProjectEditMain(QWidget):
# The Form # The Form
self.mainForm = NConfigLayout() self.mainForm = NConfigLayout()
self.mainForm.setHelpTextStyle(CONFIG.theme.helpText) self.mainForm.setHelpTextStyle(SHARED.theme.helpText)
self.setLayout(self.mainForm) self.setLayout(self.mainForm)
self.mainForm.addGroupLabel(self.tr("Project Settings")) self.mainForm.addGroupLabel(self.tr("Project Settings"))
xW = CONFIG.pxInt(250) xW = CONFIG.pxInt(250)
pData = self.mainGui.project.data pData = SHARED.project.data
self.editName = QLineEdit() self.editName = QLineEdit()
self.editName.setMaxLength(200) self.editName.setMaxLength(200)
@@ -288,26 +287,24 @@ class GuiProjectEditStatus(QWidget):
def __init__(self, projGui, isStatus): def __init__(self, projGui, isStatus):
super().__init__(parent=projGui) super().__init__(parent=projGui)
self.mainGui = projGui.mainGui
if isStatus: if isStatus:
self.theStatus = self.mainGui.project.data.itemStatus self.theStatus = SHARED.project.data.itemStatus
pageLabel = self.tr("Novel File Status Levels") pageLabel = self.tr("Novel File Status Levels")
colSetting = "statusColW" colSetting = "statusColW"
else: else:
self.theStatus = self.mainGui.project.data.itemImport self.theStatus = SHARED.project.data.itemImport
pageLabel = self.tr("Note File Importance Levels") pageLabel = self.tr("Note File Importance Levels")
colSetting = "importColW" colSetting = "importColW"
wCol0 = CONFIG.pxInt( wCol0 = CONFIG.pxInt(
self.mainGui.project.options.getInt("GuiProjectSettings", colSetting, 130) SHARED.project.options.getInt("GuiProjectSettings", colSetting, 130)
) )
self.colDeleted = [] self.colDeleted = []
self.colChanged = False self.colChanged = False
self.selColour = QColor(100, 100, 100) self.selColour = QColor(100, 100, 100)
self.iPx = CONFIG.theme.baseIconSize self.iPx = SHARED.theme.baseIconSize
# The List # The List
# ======== # ========
@@ -326,16 +323,16 @@ class GuiProjectEditStatus(QWidget):
# List Controls # List Controls
# ============= # =============
self.addButton = QPushButton(CONFIG.theme.getIcon("add"), "") self.addButton = QPushButton(SHARED.theme.getIcon("add"), "")
self.addButton.clicked.connect(self._newItem) 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.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.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)) self.dnButton.clicked.connect(lambda: self._moveItem(1))
# Edit Form # Edit Form
@@ -441,9 +438,7 @@ class GuiProjectEditStatus(QWidget):
if isinstance(selItem, QTreeWidgetItem): if isinstance(selItem, QTreeWidgetItem):
iRow = self.listBox.indexOfTopLevelItem(selItem) iRow = self.listBox.indexOfTopLevelItem(selItem)
if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0: if selItem.data(self.COL_LABEL, self.NUM_ROLE) > 0:
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr("Cannot delete a status item that is in use."))
"Cannot delete a status item that is in use."
), level=nwAlert.ERROR)
else: else:
self.listBox.takeTopLevelItem(iRow) self.listBox.takeTopLevelItem(iRow)
self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE)) self.colDeleted.append(selItem.data(self.COL_LABEL, self.KEY_ROLE))
@@ -574,11 +569,10 @@ class GuiProjectEditReplace(QWidget):
def __init__(self, projGui): def __init__(self, projGui):
super().__init__(parent=projGui) super().__init__(parent=projGui)
self.mainGui = projGui.mainGui
self.arChanged = False self.arChanged = False
wCol0 = CONFIG.pxInt( 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") 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.setColumnWidth(self.COL_KEY, wCol0)
self.listBox.setIndentation(0) 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]) newItem = QTreeWidgetItem(["<%s>" % aKey, aVal])
self.listBox.addTopLevelItem(newItem) self.listBox.addTopLevelItem(newItem)
@@ -604,10 +598,10 @@ class GuiProjectEditReplace(QWidget):
# List Controls # List Controls
# ============= # =============
self.addButton = QPushButton(CONFIG.theme.getIcon("add"), "") self.addButton = QPushButton(SHARED.theme.getIcon("add"), "")
self.addButton.clicked.connect(self._addEntry) 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) self.delButton.clicked.connect(self._delEntry)
# Edit Form # Edit Form
+2 -2
View File
@@ -35,7 +35,7 @@ from PyQt5.QtWidgets import (
qApp, QDialog, QHBoxLayout, QVBoxLayout, QDialogButtonBox, QLabel 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.common import logException
from novelwriter.constants import nwConst from novelwriter.constants import nwConst
@@ -58,7 +58,7 @@ class GuiUpdates(QDialog):
# Left Box # Left Box
self.nwIcon = QLabel() 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 = QVBoxLayout()
self.leftBox.addWidget(self.nwIcon) self.leftBox.addWidget(self.nwIcon)
+10 -15
View File
@@ -33,8 +33,7 @@ from PyQt5.QtWidgets import (
QLineEdit, QListWidget, QListWidgetItem, QPushButton, QVBoxLayout QLineEdit, QListWidget, QListWidgetItem, QPushButton, QVBoxLayout
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwAlert
from novelwriter.core.spellcheck import UserDictionary from novelwriter.core.spellcheck import UserDictionary
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
@@ -52,12 +51,10 @@ class GuiWordList(QDialog):
self.setObjectName("GuiWordList") self.setObjectName("GuiWordList")
self.setWindowTitle(self.tr("Project Word List")) self.setWindowTitle(self.tr("Project Word List"))
self.mainGui = mainGui
mS = CONFIG.pxInt(250) mS = CONFIG.pxInt(250)
wW = CONFIG.pxInt(320) wW = CONFIG.pxInt(320)
wH = CONFIG.pxInt(340) wH = CONFIG.pxInt(340)
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
self.setMinimumWidth(mS) self.setMinimumWidth(mS)
self.setMinimumHeight(mS) self.setMinimumHeight(mS)
@@ -77,10 +74,10 @@ class GuiWordList(QDialog):
self.newEntry = QLineEdit() self.newEntry = QLineEdit()
self.addButton = QPushButton(CONFIG.theme.getIcon("add"), "") self.addButton = QPushButton(SHARED.theme.getIcon("add"), "")
self.addButton.clicked.connect(self._doAdd) 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.delButton.clicked.connect(self._doDelete)
self.editBox = QHBoxLayout() self.editBox = QHBoxLayout()
@@ -123,15 +120,13 @@ class GuiWordList(QDialog):
"""Add a new word to the word list.""" """Add a new word to the word list."""
word = self.newEntry.text().strip() word = self.newEntry.text().strip()
if word == "": if word == "":
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr("Cannot add a blank word."))
"Cannot add a blank word."
), level=nwAlert.ERROR)
return return
if self.listBox.findItems(word, Qt.MatchExactly): 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." "The word '{0}' is already in the word list."
).format(word), level=nwAlert.ERROR) ).format(word))
return return
self.listBox.addItem(word) self.listBox.addItem(word)
@@ -149,7 +144,7 @@ class GuiWordList(QDialog):
def _doSave(self): def _doSave(self):
"""Save the new word list and close.""" """Save the new word list and close."""
self._saveGuiSettings() self._saveGuiSettings()
userDict = UserDictionary(self.mainGui.project) userDict = UserDictionary(SHARED.project)
for i in range(self.listBox.count()): for i in range(self.listBox.count()):
item = self.listBox.item(i) item = self.listBox.item(i)
if isinstance(item, QListWidgetItem): if isinstance(item, QListWidgetItem):
@@ -172,7 +167,7 @@ class GuiWordList(QDialog):
def _loadWordList(self): def _loadWordList(self):
"""Load the project's word list, if it exists.""" """Load the project's word list, if it exists."""
userDict = UserDictionary(self.mainGui.project) userDict = UserDictionary(SHARED.project)
userDict.load() userDict.load()
self.listBox.clear() self.listBox.clear()
for word in userDict: for word in userDict:
@@ -185,7 +180,7 @@ class GuiWordList(QDialog):
winWidth = CONFIG.rpxInt(self.width()) winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height()) winHeight = CONFIG.rpxInt(self.height())
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
pOptions.setValue("GuiWordList", "winWidth", winWidth) pOptions.setValue("GuiWordList", "winWidth", winWidth)
pOptions.setValue("GuiWordList", "winHeight", winHeight) pOptions.setValue("GuiWordList", "winHeight", winHeight)
-10
View File
@@ -119,16 +119,6 @@ class nwDocInsert(Enum):
# END Enum nwDocInsert # END Enum nwDocInsert
class nwAlert(Enum):
INFO = 0
WARN = 1
ERROR = 2
ASK = 3
# END Enum nwAlert
class nwView(Enum): class nwView(Enum):
EDITOR = 0 EDITOR = 0
+4 -10
View File
@@ -25,18 +25,13 @@ from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING
from PyQt5.QtCore import pyqtSignal, pyqtSlot from PyQt5.QtCore import pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import QComboBox, QWidget from PyQt5.QtWidgets import QComboBox, QWidget
from novelwriter import CONFIG from novelwriter import SHARED
from novelwriter.enum import nwItemClass from novelwriter.enum import nwItemClass
from novelwriter.constants import nwLabels from novelwriter.constants import nwLabels
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -44,9 +39,8 @@ class NovelSelector(QComboBox):
novelSelectionChanged = pyqtSignal(str) novelSelectionChanged = pyqtSignal(str)
def __init__(self, parent: QWidget, mainGui: GuiMain) -> None: def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
self._mainGui = mainGui
self._blockSignal = False self._blockSignal = False
self._firstHandle = None self._firstHandle = None
self.currentIndexChanged.connect(self._indexChanged) self.currentIndexChanged.connect(self._indexChanged)
@@ -86,9 +80,9 @@ class NovelSelector(QComboBox):
self._firstHandle = None self._firstHandle = None
self.clear() self.clear()
icon = CONFIG.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL]) icon = SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItemClass.NOVEL])
handle = self.currentData() 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: if prefix:
name = prefix.format(nwItem.itemName) name = prefix.format(nwItem.itemName)
self.addItem(name, tHandle) self.addItem(name, tHandle)
+104 -108
View File
@@ -49,8 +49,8 @@ from PyQt5.QtWidgets import (
QPushButton, QShortcut, QTextEdit, QToolBar, QToolButton, QWidget QPushButton, QShortcut, QTextEdit, QToolBar, QToolButton, QWidget
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwAlert, nwDocAction, nwDocInsert, nwDocMode, nwItemClass from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass
from novelwriter.common import minmax, transferCase from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwConst, nwKeyWords, nwUnicode from novelwriter.constants import nwConst, nwKeyWords, nwUnicode
from novelwriter.core.index import countWords from novelwriter.core.index import countWords
@@ -71,9 +71,10 @@ class GuiDocEditor(QTextEdit):
) )
# Custom Signals # Custom Signals
spellDictionaryChanged = pyqtSignal(str, str) statusMessage = pyqtSignal(str)
docEditedStatusChanged = pyqtSignal(bool)
docCountsChanged = pyqtSignal(str, int, int, int) docCountsChanged = pyqtSignal(str, int, int, int)
editedStatusChanged = pyqtSignal(bool)
spellDictionaryChanged = pyqtSignal(str, str)
loadDocumentTagRequest = pyqtSignal(str, Enum) loadDocumentTagRequest = pyqtSignal(str, Enum)
novelStructureChanged = pyqtSignal() novelStructureChanged = pyqtSignal()
novelItemMetaChanged = pyqtSignal(str) novelItemMetaChanged = pyqtSignal(str)
@@ -101,7 +102,7 @@ class GuiDocEditor(QTextEdit):
self._wordCount = 0 # Word count self._wordCount = 0 # Word count
self._paraCount = 0 # Paragraph count self._paraCount = 0 # Paragraph count
self._lastEdit = 0 # Time stamp of last edit 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._lastFind = None # Position of the last found search word
self._bigDoc = False # Flag for very large document size self._bigDoc = False # Flag for very large document size
self._doReplace = False # Switch to temporarily disable auto-replace self._doReplace = False # Switch to temporarily disable auto-replace
@@ -132,8 +133,8 @@ class GuiDocEditor(QTextEdit):
self.docSearch = GuiDocEditSearch(self) self.docSearch = GuiDocEditSearch(self)
# Syntax # Syntax
self.spEnchant = NWSpellEnchant(self.mainGui.project) self.spEnchant = NWSpellEnchant(SHARED.project)
self.highLight = GuiDocHighlighter(qDoc, self.mainGui, self.spEnchant) self.highLight = GuiDocHighlighter(qDoc, self.spEnchant)
# Context Menu # Context Menu
self.setContextMenuPolicy(Qt.CustomContextMenu) self.setContextMenuPolicy(Qt.CustomContextMenu)
@@ -188,6 +189,34 @@ class GuiDocEditor(QTextEdit):
return 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): def clearEditor(self):
"""Clear the current document and reset all document-related """Clear the current document and reset all document-related
flags and counters. flags and counters.
@@ -203,7 +232,7 @@ class GuiDocEditor(QTextEdit):
self._wordCount = 0 self._wordCount = 0
self._paraCount = 0 self._paraCount = 0
self._lastEdit = 0 self._lastEdit = 0
self._lastActive = 0 self._lastActive = 0.0
self._lastFind = None self._lastFind = None
self._bigDoc = False self._bigDoc = False
self._doReplace = False self._doReplace = False
@@ -227,14 +256,14 @@ class GuiDocEditor(QTextEdit):
"""Update the syntax highlighting theme. """Update the syntax highlighting theme.
""" """
mainPalette = self.palette() mainPalette = self.palette()
mainPalette.setColor(QPalette.Window, QColor(*CONFIG.theme.colBack)) mainPalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
mainPalette.setColor(QPalette.Base, QColor(*CONFIG.theme.colBack)) mainPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack))
mainPalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) mainPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
self.setPalette(mainPalette) self.setPalette(mainPalette)
docPalette = self.viewport().palette() docPalette = self.viewport().palette()
docPalette.setColor(QPalette.Base, QColor(*CONFIG.theme.colBack)) docPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack))
docPalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) docPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
self.viewport().setPalette(docPalette) self.viewport().setPalette(docPalette)
self.docHeader.matchColours() self.docHeader.matchColours()
@@ -340,7 +369,7 @@ class GuiDocEditor(QTextEdit):
document is new (empty string), we set up the editor for editing document is new (empty string), we set up the editor for editing
the file. the file.
""" """
self._nwDocument = self.mainGui.project.storage.getDocument(tHandle) self._nwDocument = SHARED.project.storage.getDocument(tHandle)
self._nwItem = self._nwDocument.getCurrentItem() self._nwItem = self._nwDocument.getCurrentItem()
theDoc = self._nwDocument.readDocument() theDoc = self._nwDocument.readDocument()
@@ -351,14 +380,14 @@ class GuiDocEditor(QTextEdit):
docSize = len(theDoc) docSize = len(theDoc)
if docSize > nwConst.MAX_DOCSIZE: 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 you are trying to open is too big. "
"The document size is {0} MB. " "The document size is {0} MB. "
"The maximum size allowed is {1} MB." "The maximum size allowed is {1} MB."
).format( ).format(
f"{docSize/1.0e6:.2f}", f"{docSize/1.0e6:.2f}",
f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
), level=nwAlert.ERROR) ))
self.clearEditor() self.clearEditor()
return False return False
@@ -426,9 +455,7 @@ class GuiDocEditor(QTextEdit):
# Update the status bar # Update the status bar
if self._nwItem is not None: if self._nwItem is not None:
self.mainGui.setStatus( self.statusMessage.emit(self.tr("Opened Document: {0}").format(self._nwItem.itemName))
self.tr("Opened Document: {0}").format(self._nwItem.itemName)
)
return True return True
@@ -451,14 +478,14 @@ class GuiDocEditor(QTextEdit):
""" """
docSize = len(theText) docSize = len(theText)
if docSize > nwConst.MAX_DOCSIZE: 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 you are trying to add is too big. "
"The text size is {0} MB. " "The text size is {0} MB. "
"The maximum size allowed is {1} MB." "The maximum size allowed is {1} MB."
).format( ).format(
f"{docSize/1.0e6:.2f}", f"{docSize/1.0e6:.2f}",
f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
), level=nwAlert.ERROR) ))
return False return False
qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
@@ -497,7 +524,7 @@ class GuiDocEditor(QTextEdit):
if not self._nwDocument.writeDocument(docText): if not self._nwDocument.writeDocument(docText):
saveOk = False saveOk = False
if self._nwDocument._currHash != self._nwDocument._prevHash: 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 " "This document has been changed outside of novelWriter "
"while it was open. Overwrite the file on disk?" "while it was open. Overwrite the file on disk?"
)) ))
@@ -505,10 +532,9 @@ class GuiDocEditor(QTextEdit):
saveOk = self._nwDocument.writeDocument(docText, forceWrite=True) saveOk = self._nwDocument.writeDocument(docText, forceWrite=True)
if not saveOk: if not saveOk:
self.mainGui.makeAlert( SHARED.error(
self.tr("Could not save document."), self.tr("Could not save document."),
info=self._nwDocument.getError(), info=self._nwDocument.getError()
level=nwAlert.ERROR
) )
return False return False
@@ -516,10 +542,10 @@ class GuiDocEditor(QTextEdit):
self.setDocumentChanged(False) self.setDocumentChanged(False)
oldHeader = self._nwItem.mainHeading oldHeader = self._nwItem.mainHeading
oldCount = self.mainGui.project.index.getHandleHeaderCount(tHandle) oldCount = SHARED.project.index.getHandleHeaderCount(tHandle)
self.mainGui.project.index.scanText(tHandle, docText) SHARED.project.index.scanText(tHandle, docText)
newHeader = self._nwItem.mainHeading newHeader = self._nwItem.mainHeading
newCount = self.mainGui.project.index.getHandleHeaderCount(tHandle) newCount = SHARED.project.index.getHandleHeaderCount(tHandle)
if self._nwItem.itemClass == nwItemClass.NOVEL: if self._nwItem.itemClass == nwItemClass.NOVEL:
if oldCount == newCount: if oldCount == newCount:
@@ -534,9 +560,7 @@ class GuiDocEditor(QTextEdit):
self.docFooter.updateInfo() self.docFooter.updateInfo()
# Update the status bar # Update the status bar
self.mainGui.setStatus( self.statusMessage.emit(self.tr("Saved Document: {0}").format(self._nwItem.itemName))
self.tr("Saved Document: {0}").format(self._nwItem.itemName)
)
return True return True
@@ -580,31 +604,6 @@ class GuiDocEditor(QTextEdit):
return 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 # Getters
## ##
@@ -636,7 +635,7 @@ class GuiDocEditor(QTextEdit):
document change signal. document change signal.
""" """
self._docChanged = bValue self._docChanged = bValue
self.docEditedStatusChanged.emit(self._docChanged) self.editedStatusChanged.emit(self._docChanged)
return self._docChanged return self._docChanged
def setCursorPosition(self, position): def setCursorPosition(self, position):
@@ -698,10 +697,10 @@ class GuiDocEditor(QTextEdit):
"""Set the spell checker dictionary language, and emit the """Set the spell checker dictionary language, and emit the
dictionary changed signal. dictionary changed signal.
""" """
if self.mainGui.project.data.spellLang is None: if SHARED.project.data.spellLang is None:
theLang = CONFIG.spellLanguage theLang = CONFIG.spellLanguage
else: else:
theLang = self.mainGui.project.data.spellLang theLang = SHARED.project.data.spellLang
self.spEnchant.setLanguage(theLang) self.spEnchant.setLanguage(theLang)
_, theProvider = self.spEnchant.describeDict() _, theProvider = self.spEnchant.describeDict()
@@ -723,7 +722,7 @@ class GuiDocEditor(QTextEdit):
if not CONFIG.hasEnchant: if not CONFIG.hasEnchant:
if theMode: if theMode:
self.mainGui.makeAlert(self.tr( SHARED.info(self.tr(
"Spell checking requires the package PyEnchant. " "Spell checking requires the package PyEnchant. "
"It does not appear to be installed." "It does not appear to be installed."
)) ))
@@ -734,7 +733,7 @@ class GuiDocEditor(QTextEdit):
self._spellCheck = theMode self._spellCheck = theMode
self.mainGui.mainMenu.setSpellCheck(theMode) self.mainGui.mainMenu.setSpellCheck(theMode)
self.mainGui.project.data.setSpellCheck(theMode) SHARED.project.data.setSpellCheck(theMode)
self.highLight.setSpellCheck(theMode) self.highLight.setSpellCheck(theMode)
if not self._bigDoc or theMode is False: if not self._bigDoc or theMode is False:
# We don't run the spell checker automatically on big docs # We don't run the spell checker automatically on big docs
@@ -744,7 +743,7 @@ class GuiDocEditor(QTextEdit):
return True return True
def spellCheckDocument(self): def spellCheckDocument(self) -> None:
"""Rerun the highlighter to update spell checking status of the """Rerun the highlighter to update spell checking status of the
currently loaded text. The fastest way to do this, at least as 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 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() self.highLight.rehighlight()
qApp.restoreOverrideCursor() qApp.restoreOverrideCursor()
logger.debug("Document highlighted in %.3f ms", 1000*(time() - start)) logger.debug("Document highlighted in %.3f ms", 1000*(time() - start))
self.mainGui.mainStatus.setStatus(self.tr("Spell check complete")) self.statusMessage.emit(self.tr("Spell check complete"))
return
return True
## ##
# General Class Methods # General Class Methods
@@ -868,7 +866,7 @@ class GuiDocEditor(QTextEdit):
if self._nwDocument is None: if self._nwDocument is None:
logger.error("No document open") logger.error("No document open")
return False return False
self.mainGui.makeAlert( SHARED.info(
self.tr("The currently open file is saved in:"), self.tr("The currently open file is saved in:"),
info=self._nwDocument.getFileLocation() info=self._nwDocument.getFileLocation()
) )
@@ -1104,12 +1102,12 @@ class GuiDocEditor(QTextEdit):
self._lastFind = None self._lastFind = None
if self.document().characterCount() > nwConst.MAX_DOCSIZE: 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 document has grown too big and you cannot add more text to it. "
"The maximum size of a single novelWriter document is {0} MB." "The maximum size of a single novelWriter document is {0} MB."
).format( ).format(
f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}" f"{nwConst.MAX_DOCSIZE/1.0e6:.2f}"
), level=nwAlert.ERROR) ))
self.undo() self.undo()
return return
@@ -1666,9 +1664,7 @@ class GuiDocEditor(QTextEdit):
""" """
theCursor = self.textCursor() theCursor = self.textCursor()
if not theCursor.hasSelection(): if not theCursor.hasSelection():
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr("Please select some text before calling replace quotes."))
"Please select some text before calling replace quotes."
), level=nwAlert.ERROR)
return False return False
posS = theCursor.selectionStart() posS = theCursor.selectionStart()
@@ -1916,7 +1912,7 @@ class GuiDocEditor(QTextEdit):
if theText.startswith("@"): if theText.startswith("@"):
isGood, tBits, tPos = self.mainGui.project.index.scanThis(theText) isGood, tBits, tPos = SHARED.project.index.scanThis(theText)
if not isGood: if not isGood:
return False return False
@@ -2233,9 +2229,9 @@ class GuiDocEditSearch(QFrame):
self.doMatchCap = CONFIG.searchMatchCap self.doMatchCap = CONFIG.searchMatchCap
mPx = CONFIG.pxInt(6) mPx = CONFIG.pxInt(6)
tPx = int(0.8*CONFIG.theme.fontPixelSize) tPx = int(0.8*SHARED.theme.fontPixelSize)
self.boxFont = CONFIG.theme.guiFont self.boxFont = SHARED.theme.guiFont
self.boxFont.setPointSizeF(0.9*CONFIG.theme.fontPointSize) self.boxFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
self.setAutoFillBackground(True) self.setAutoFillBackground(True)
@@ -2268,7 +2264,7 @@ class GuiDocEditSearch(QFrame):
self.resultLabel = QLabel("?/?") self.resultLabel = QLabel("?/?")
self.resultLabel.setFont(self.boxFont) 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 = QAction(self.tr("Case Sensitive"), self)
self.toggleCase.setCheckable(True) self.toggleCase.setCheckable(True)
@@ -2374,15 +2370,15 @@ class GuiDocEditSearch(QFrame):
self.replaceBox.setPalette(qPalette) self.replaceBox.setPalette(qPalette)
# Set icons # Set icons
self.toggleCase.setIcon(CONFIG.theme.getIcon("search_case")) self.toggleCase.setIcon(SHARED.theme.getIcon("search_case"))
self.toggleWord.setIcon(CONFIG.theme.getIcon("search_word")) self.toggleWord.setIcon(SHARED.theme.getIcon("search_word"))
self.toggleRegEx.setIcon(CONFIG.theme.getIcon("search_regex")) self.toggleRegEx.setIcon(SHARED.theme.getIcon("search_regex"))
self.toggleLoop.setIcon(CONFIG.theme.getIcon("search_loop")) self.toggleLoop.setIcon(SHARED.theme.getIcon("search_loop"))
self.toggleProject.setIcon(CONFIG.theme.getIcon("search_project")) self.toggleProject.setIcon(SHARED.theme.getIcon("search_project"))
self.toggleMatchCap.setIcon(CONFIG.theme.getIcon("search_preserve")) self.toggleMatchCap.setIcon(SHARED.theme.getIcon("search_preserve"))
self.cancelSearch.setIcon(CONFIG.theme.getIcon("search_cancel")) self.cancelSearch.setIcon(SHARED.theme.getIcon("search_cancel"))
self.searchButton.setIcon(CONFIG.theme.getIcon("search")) self.searchButton.setIcon(SHARED.theme.getIcon("search"))
self.replaceButton.setIcon(CONFIG.theme.getIcon("search_replace")) self.replaceButton.setIcon(SHARED.theme.getIcon("search_replace"))
# Set stylesheets # Set stylesheets
self.searchOpt.setStyleSheet("QToolBar {padding: 0;}") self.searchOpt.setStyleSheet("QToolBar {padding: 0;}")
@@ -2474,7 +2470,7 @@ class GuiDocEditSearch(QFrame):
""" """
currRes = "?" if currRes is None else currRes currRes = "?" if currRes is None else currRes
resCount = "?" if resCount is None else "1000+" if resCount > 1000 else resCount 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.setText(f"{currRes}/{resCount}")
self.resultLabel.setMinimumWidth(minWidth) self.resultLabel.setMinimumWidth(minWidth)
self.adjustSize() self.adjustSize()
@@ -2639,7 +2635,7 @@ class GuiDocEditHeader(QWidget):
self._docHandle = None self._docHandle = None
fPx = int(0.9*CONFIG.theme.fontPixelSize) fPx = int(0.9*SHARED.theme.fontPixelSize)
hSp = CONFIG.pxInt(6) hSp = CONFIG.pxInt(6)
# Main Widget Settings # Main Widget Settings
@@ -2656,7 +2652,7 @@ class GuiDocEditHeader(QWidget):
self.theTitle.setFixedHeight(fPx) self.theTitle.setFixedHeight(fPx)
lblFont = self.theTitle.font() lblFont = self.theTitle.font()
lblFont.setPointSizeF(0.9*CONFIG.theme.fontPointSize) lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
self.theTitle.setFont(lblFont) self.theTitle.setFont(lblFont)
# Buttons # Buttons
@@ -2726,15 +2722,15 @@ class GuiDocEditHeader(QWidget):
def updateTheme(self): def updateTheme(self):
"""Update theme elements. """Update theme elements.
""" """
self.editButton.setIcon(CONFIG.theme.getIcon("edit")) self.editButton.setIcon(SHARED.theme.getIcon("edit"))
self.searchButton.setIcon(CONFIG.theme.getIcon("search")) self.searchButton.setIcon(SHARED.theme.getIcon("search"))
self.minmaxButton.setIcon(CONFIG.theme.getIcon("maximise")) self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
self.closeButton.setIcon(CONFIG.theme.getIcon("close")) self.closeButton.setIcon(SHARED.theme.getIcon("close"))
buttonStyle = ( buttonStyle = (
"QToolButton {{border: none; background: transparent;}} " "QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
).format(*CONFIG.theme.colText) ).format(*SHARED.theme.colText)
self.editButton.setStyleSheet(buttonStyle) self.editButton.setStyleSheet(buttonStyle)
self.searchButton.setStyleSheet(buttonStyle) self.searchButton.setStyleSheet(buttonStyle)
@@ -2750,9 +2746,9 @@ class GuiDocEditHeader(QWidget):
theme rather than the main GUI. theme rather than the main GUI.
""" """
thePalette = QPalette() thePalette = QPalette()
thePalette.setColor(QPalette.Window, QColor(*CONFIG.theme.colBack)) thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*CONFIG.theme.colText)) thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
thePalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
self.setPalette(thePalette) self.setPalette(thePalette)
self.theTitle.setPalette(thePalette) self.theTitle.setPalette(thePalette)
@@ -2772,7 +2768,7 @@ class GuiDocEditHeader(QWidget):
self.minmaxButton.setVisible(False) self.minmaxButton.setVisible(False)
return True return True
pTree = self.mainGui.project.tree pTree = SHARED.project.tree
if CONFIG.showFullPath: if CONFIG.showFullPath:
tTitle = [] tTitle = []
tTree = pTree.getItemPath(tHandle) tTree = pTree.getItemPath(tHandle)
@@ -2801,9 +2797,9 @@ class GuiDocEditHeader(QWidget):
toggleFocusMode function and should not be activated directly. toggleFocusMode function and should not be activated directly.
""" """
if self.mainGui.isFocusMode: if self.mainGui.isFocusMode:
self.minmaxButton.setIcon(CONFIG.theme.getIcon("minimise")) self.minmaxButton.setIcon(SHARED.theme.getIcon("minimise"))
else: else:
self.minmaxButton.setIcon(CONFIG.theme.getIcon("maximise")) self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
return return
## ##
@@ -2876,13 +2872,13 @@ class GuiDocEditFooter(QWidget):
self._docSelection = False self._docSelection = False
self.sPx = int(round(0.9*CONFIG.theme.baseIconSize)) self.sPx = int(round(0.9*SHARED.theme.baseIconSize))
fPx = int(0.9*CONFIG.theme.fontPixelSize) fPx = int(0.9*SHARED.theme.fontPixelSize)
bSp = CONFIG.pxInt(4) bSp = CONFIG.pxInt(4)
hSp = CONFIG.pxInt(6) hSp = CONFIG.pxInt(6)
lblFont = self.font() lblFont = self.font()
lblFont.setPointSizeF(0.9*CONFIG.theme.fontPointSize) lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
# Main Widget Settings # Main Widget Settings
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
@@ -2969,8 +2965,8 @@ class GuiDocEditFooter(QWidget):
def updateTheme(self): def updateTheme(self):
"""Update theme elements. """Update theme elements.
""" """
self.linesIcon.setPixmap(CONFIG.theme.getPixmap("status_lines", (self.sPx, self.sPx))) self.linesIcon.setPixmap(SHARED.theme.getPixmap("status_lines", (self.sPx, self.sPx)))
self.wordsIcon.setPixmap(CONFIG.theme.getPixmap("status_stats", (self.sPx, self.sPx))) self.wordsIcon.setPixmap(SHARED.theme.getPixmap("status_stats", (self.sPx, self.sPx)))
self.matchColours() self.matchColours()
@@ -2981,9 +2977,9 @@ class GuiDocEditFooter(QWidget):
theme rather than the main GUI. theme rather than the main GUI.
""" """
thePalette = QPalette() thePalette = QPalette()
thePalette.setColor(QPalette.Window, QColor(*CONFIG.theme.colBack)) thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*CONFIG.theme.colText)) thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
thePalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
self.setPalette(thePalette) self.setPalette(thePalette)
self.statusText.setPalette(thePalette) self.statusText.setPalette(thePalette)
@@ -3000,7 +2996,7 @@ class GuiDocEditFooter(QWidget):
logger.debug("No handle set, so clearing the editor footer") logger.debug("No handle set, so clearing the editor footer")
self._theItem = None self._theItem = None
else: else:
self._theItem = self.mainGui.project.tree[self._docHandle] self._theItem = SHARED.project.tree[self._docHandle]
self.setHasSelection(False) self.setHasSelection(False)
self.updateInfo() self.updateInfo()
+18 -19
View File
@@ -32,7 +32,7 @@ from PyQt5.QtGui import (
QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush QColor, QTextCharFormat, QFont, QSyntaxHighlighter, QBrush
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.common import checkInt from novelwriter.common import checkInt
from novelwriter.constants import nwRegEx, nwUnicode from novelwriter.constants import nwRegEx, nwUnicode
@@ -46,14 +46,13 @@ class GuiDocHighlighter(QSyntaxHighlighter):
BLOCK_META = 2 BLOCK_META = 2
BLOCK_TITLE = 4 BLOCK_TITLE = 4
def __init__(self, theDoc, mainGui, spEnchant): def __init__(self, theDoc, spEnchant):
super().__init__(theDoc) super().__init__(theDoc)
logger.debug("Create: GuiDocHighlighter") logger.debug("Create: GuiDocHighlighter")
self.theDoc = theDoc self.theDoc = theDoc
self.spEnchant = spEnchant self.spEnchant = spEnchant
self.mainGui = mainGui
self.theHandle = None self.theHandle = None
self.spellCheck = False self.spellCheck = False
self.spellRx = None self.spellRx = None
@@ -85,24 +84,24 @@ class GuiDocHighlighter(QSyntaxHighlighter):
""" """
logger.debug("Setting up highlighting rules") logger.debug("Setting up highlighting rules")
self.colHead = QColor(*CONFIG.theme.colHead) self.colHead = QColor(*SHARED.theme.colHead)
self.colHeadH = QColor(*CONFIG.theme.colHeadH) self.colHeadH = QColor(*SHARED.theme.colHeadH)
self.colDialN = QColor(*CONFIG.theme.colDialN) self.colDialN = QColor(*SHARED.theme.colDialN)
self.colDialD = QColor(*CONFIG.theme.colDialD) self.colDialD = QColor(*SHARED.theme.colDialD)
self.colDialS = QColor(*CONFIG.theme.colDialS) self.colDialS = QColor(*SHARED.theme.colDialS)
self.colHidden = QColor(*CONFIG.theme.colHidden) self.colHidden = QColor(*SHARED.theme.colHidden)
self.colKey = QColor(*CONFIG.theme.colKey) self.colKey = QColor(*SHARED.theme.colKey)
self.colVal = QColor(*CONFIG.theme.colVal) self.colVal = QColor(*SHARED.theme.colVal)
self.colSpell = QColor(*CONFIG.theme.colSpell) self.colSpell = QColor(*SHARED.theme.colSpell)
self.colError = QColor(*CONFIG.theme.colError) self.colError = QColor(*SHARED.theme.colError)
self.colRepTag = QColor(*CONFIG.theme.colRepTag) self.colRepTag = QColor(*SHARED.theme.colRepTag)
self.colMod = QColor(*CONFIG.theme.colMod) self.colMod = QColor(*SHARED.theme.colMod)
self.colBreak = QColor(*CONFIG.theme.colEmph) self.colBreak = QColor(*SHARED.theme.colEmph)
self.colBreak.setAlpha(64) self.colBreak.setAlpha(64)
self.colEmph = None self.colEmph = None
if CONFIG.highlightEmph: if CONFIG.highlightEmph:
self.colEmph = QColor(*CONFIG.theme.colEmph) self.colEmph = QColor(*SHARED.theme.colEmph)
self.hStyles = { self.hStyles = {
"header1": self._makeFormat(self.colHead, "bold", 1.8), "header1": self._makeFormat(self.colHead, "bold", 1.8),
@@ -285,8 +284,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
if theText.startswith("@"): # Keywords and commands if theText.startswith("@"): # Keywords and commands
self.setCurrentBlockState(self.BLOCK_META) self.setCurrentBlockState(self.BLOCK_META)
pIndex = self.mainGui.project.index pIndex = SHARED.project.index
tItem = self.mainGui.project.tree[self.theHandle] tItem = SHARED.project.tree[self.theHandle]
isValid, theBits, thePos = pIndex.scanThis(theText) isValid, theBits, thePos = pIndex.scanThis(theText)
isGood = pIndex.checkThese(theBits, tItem) isGood = pIndex.checkThese(theBits, tItem)
if isValid: if isValid:
+159 -171
View File
@@ -30,22 +30,27 @@ from __future__ import annotations
import logging import logging
from enum import Enum 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 ( from PyQt5.QtGui import (
QTextOption, QFont, QPalette, QColor, QTextCursor, QIcon, QCursor QColor, QCursor, QFont, QIcon, QMouseEvent, QPalette, QResizeEvent,
QTextCursor, QTextOption
) )
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
qApp, QTextBrowser, QWidget, QScrollArea, QLabel, QHBoxLayout, QToolButton, QAction, qApp, QFrame, QHBoxLayout, QLabel, QMenu, QScrollArea,
QAction, QMenu, QFrame QTextBrowser, QToolButton, QWidget
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemType, nwDocAction, nwDocMode from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.constants import nwUnicode from novelwriter.constants import nwUnicode
from novelwriter.core.tohtml import ToHtml from novelwriter.core.tohtml import ToHtml
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -53,7 +58,7 @@ class GuiDocViewer(QTextBrowser):
loadDocumentTagRequest = pyqtSignal(str, Enum) loadDocumentTagRequest = pyqtSignal(str, Enum)
def __init__(self, mainGui): def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
logger.debug("Create: GuiDocViewer") logger.debug("Create: GuiDocViewer")
@@ -90,25 +95,43 @@ class GuiDocViewer(QTextBrowser):
return 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.clear()
self.setSearchPaths([""]) self.setSearchPaths([""])
self._docHandle = None self._docHandle = None
self.docHeader.setTitleFromHandle(self._docHandle) self.docHeader.setTitleFromHandle(self._docHandle)
return True return
def updateTheme(self): def updateTheme(self) -> None:
"""Update theme elements. """Update theme elements."""
"""
self.docHeader.updateTheme() self.docHeader.updateTheme()
self.docFooter.updateTheme() self.docFooter.updateTheme()
return return
def initViewer(self): def initViewer(self) -> None:
"""Set editor settings from main config. """Set editor settings from main config."""
"""
self._makeStyleSheet() self._makeStyleSheet()
# Set Font # Set Font
@@ -119,14 +142,14 @@ class GuiDocViewer(QTextBrowser):
# Set the widget colours to match syntax theme # Set the widget colours to match syntax theme
mainPalette = self.palette() mainPalette = self.palette()
mainPalette.setColor(QPalette.Window, QColor(*CONFIG.theme.colBack)) mainPalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
mainPalette.setColor(QPalette.Base, QColor(*CONFIG.theme.colBack)) mainPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack))
mainPalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) mainPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
self.setPalette(mainPalette) self.setPalette(mainPalette)
docPalette = self.viewport().palette() docPalette = self.viewport().palette()
docPalette.setColor(QPalette.Base, QColor(*CONFIG.theme.colBack)) docPalette.setColor(QPalette.Base, QColor(*SHARED.theme.colBack))
docPalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) docPalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
self.viewport().setPalette(docPalette) self.viewport().setPalette(docPalette)
self.docHeader.matchColours() self.docHeader.matchColours()
@@ -157,12 +180,11 @@ class GuiDocViewer(QTextBrowser):
if self._docHandle is not None: if self._docHandle is not None:
self.reloadText() self.reloadText()
return True return
def loadText(self, tHandle, updateHistory=True): def loadText(self, tHandle: str, updateHistory: bool = True) -> bool:
"""Load text into the viewer from an item handle. """Load text into the viewer from an item handle."""
""" if not SHARED.project.tree.checkType(tHandle, nwItemType.FILE):
if not self.mainGui.project.tree.checkType(tHandle, nwItemType.FILE):
logger.warning("Item not found") logger.warning("Item not found")
return False return False
@@ -170,7 +192,7 @@ class GuiDocViewer(QTextBrowser):
qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
sPos = self.verticalScrollBar().value() sPos = self.verticalScrollBar().value()
aDoc = ToHtml(self.mainGui.project) aDoc = ToHtml(SHARED.project)
aDoc.setPreview(CONFIG.viewComments, CONFIG.viewSynopsis) aDoc.setPreview(CONFIG.viewComments, CONFIG.viewSynopsis)
aDoc.setLinkHeaders(True) aDoc.setLinkHeaders(True)
@@ -210,7 +232,7 @@ class GuiDocViewer(QTextBrowser):
self.verticalScrollBar().setValue(sPos) self.verticalScrollBar().setValue(sPos)
self._docHandle = tHandle self._docHandle = tHandle
self.mainGui.project.data.setLastHandle(tHandle, "viewer") SHARED.project.data.setLastHandle(tHandle, "viewer")
self.docHeader.setTitleFromHandle(self._docHandle) self.docHeader.setTitleFromHandle(self._docHandle)
self.updateDocMargins() self.updateDocMargins()
@@ -224,23 +246,20 @@ class GuiDocViewer(QTextBrowser):
return True return True
def reloadText(self): def reloadText(self) -> None:
"""Reload the text in the current document. """Reload the text in the current document."""
""" if self._docHandle:
self.loadText(self._docHandle, updateHistory=False) self.loadText(self._docHandle, updateHistory=False)
return return
def redrawText(self): def redrawText(self) -> None:
"""Redraw the text by marking the document content as "dirty". """Redraw the text by marking the content as "dirty"."""
"""
self.document().markContentsDirty(0, self.document().characterCount()) self.document().markContentsDirty(0, self.document().characterCount())
self.updateDocMargins() self.updateDocMargins()
return return
def docAction(self, theAction): def docAction(self, theAction: nwDocAction) -> bool:
"""Wrapper function for various document actions on the current """Process document actions on the current document."""
document.
"""
logger.debug("Requesting action: '%s'", theAction.name) logger.debug("Requesting action: '%s'", theAction.name)
if self._docHandle is None: if self._docHandle is None:
logger.error("No document open") logger.error("No document open")
@@ -258,9 +277,8 @@ class GuiDocViewer(QTextBrowser):
return False return False
return True return True
def navigateTo(self, tAnchor): def navigateTo(self, tAnchor: str) -> bool:
"""Go to a specific #link in the document. """Go to a specific #link in the document."""
"""
if not isinstance(tAnchor, str): if not isinstance(tAnchor, str):
return False return False
if tAnchor.startswith("#"): if tAnchor.startswith("#"):
@@ -268,27 +286,13 @@ class GuiDocViewer(QTextBrowser):
self.setSource(QUrl(tAnchor)) self.setSource(QUrl(tAnchor))
return True return True
def navBackward(self): def clearNavHistory(self) -> None:
"""Navigate backwards in the document view history. """Clear the navigation 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.
"""
self.docHistory.clear() self.docHistory.clear()
return return
def updateDocMargins(self): def updateDocMargins(self) -> None:
"""Automatically adjust the margins so the text is centred. """Automatically adjust the margins so the text is centred."""
"""
wW = self.width() wW = self.width()
wH = self.height() wH = self.height()
cM = CONFIG.getTextMargin() cM = CONFIG.getTextMargin()
@@ -320,41 +324,20 @@ class GuiDocViewer(QTextBrowser):
# Setters # Setters
## ##
def setScrollPosition(self, thePos): def setScrollPosition(self, pos: int) -> None:
"""Set the scrollbar position. """Set the scrollbar position."""
"""
vBar = self.verticalScrollBar() vBar = self.verticalScrollBar()
if vBar.isVisible(): if vBar.isVisible():
vBar.setValue(thePos) vBar.setValue(pos)
return 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 # Public Slots
## ##
@pyqtSlot(str) @pyqtSlot(str)
def updateDocInfo(self, tHandle): def updateDocInfo(self, tHandle: str) -> None:
"""Called when an item label is changed to check if the document """Update the header titlebar if needed."""
title bar needs updating,
"""
if tHandle == self._docHandle: if tHandle == self._docHandle:
self.docHeader.setTitleFromHandle(self._docHandle) self.docHeader.setTitleFromHandle(self._docHandle)
self.updateDocMargins() self.updateDocMargins()
@@ -364,11 +347,22 @@ class GuiDocViewer(QTextBrowser):
# Private Slots # 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") @pyqtSlot("QUrl")
def _linkClicked(self, theURL): def _linkClicked(self, url: QUrl) -> None:
"""Process a clicked link internally in the document. """Process a clicked link internally in the document."""
""" theLink = url.url()
theLink = theURL.url()
logger.debug("Clicked link: '%s'", theLink) logger.debug("Clicked link: '%s'", theLink)
if len(theLink) > 0: if len(theLink) > 0:
theBits = theLink.split("=") theBits = theLink.split("=")
@@ -377,9 +371,8 @@ class GuiDocViewer(QTextBrowser):
return return
@pyqtSlot("QPoint") @pyqtSlot("QPoint")
def _openContextMenu(self, thePos): def _openContextMenu(self, point: QPoint) -> None:
"""Triggered by right click to open the context menu. """Open context menu at location."""
"""
userCursor = self.textCursor() userCursor = self.textCursor()
userSelection = userCursor.hasSelection() userSelection = userCursor.hasSelection()
@@ -404,18 +397,18 @@ class GuiDocViewer(QTextBrowser):
mnuSelWord = QAction(self.tr("Select Word"), mnuContext) mnuSelWord = QAction(self.tr("Select Word"), mnuContext)
mnuSelWord.triggered.connect( mnuSelWord.triggered.connect(
lambda: self._makePosSelection(QTextCursor.WordUnderCursor, thePos) lambda: self._makePosSelection(QTextCursor.WordUnderCursor, point)
) )
mnuContext.addAction(mnuSelWord) mnuContext.addAction(mnuSelWord)
mnuSelPara = QAction(self.tr("Select Paragraph"), mnuContext) mnuSelPara = QAction(self.tr("Select Paragraph"), mnuContext)
mnuSelPara.triggered.connect( mnuSelPara.triggered.connect(
lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, thePos) lambda: self._makePosSelection(QTextCursor.BlockUnderCursor, point)
) )
mnuContext.addAction(mnuSelPara) mnuContext.addAction(mnuSelPara)
# Open the context menu # Open the context menu
mnuContext.exec_(self.viewport().mapToGlobal(thePos)) mnuContext.exec_(self.viewport().mapToGlobal(point))
return return
@@ -423,37 +416,33 @@ class GuiDocViewer(QTextBrowser):
# Events # Events
## ##
def resizeEvent(self, theEvent): def resizeEvent(self, event: QResizeEvent) -> None:
"""If the text editor is resized, we must make sure the document """Update document margins when widget is resized."""
has its margins adjusted according to user preferences.
"""
self.updateDocMargins() self.updateDocMargins()
super().resizeEvent(theEvent) super().resizeEvent(event)
return return
def mouseReleaseEvent(self, theEvent): def mouseReleaseEvent(self, event: QMouseEvent) -> None:
"""Capture mouse click events on the document. """Capture mouse click events on the document."""
""" if event.button() == Qt.BackButton:
if theEvent.button() == Qt.BackButton:
self.navBackward() self.navBackward()
elif theEvent.button() == Qt.ForwardButton: elif event.button() == Qt.ForwardButton:
self.navForward() self.navForward()
else: else:
super().mouseReleaseEvent(theEvent) super().mouseReleaseEvent(event)
return return
## ##
# Internal Functions # Internal Functions
## ##
def _makeSelection(self, selMode): def _makeSelection(self, selType: QTextCursor.SelectionType) -> None:
"""Wrapper function to select text based on a selection mode. """Handle select of text based on a selection mode."""
"""
theCursor = self.textCursor() theCursor = self.textCursor()
theCursor.clearSelection() theCursor.clearSelection()
theCursor.select(selMode) theCursor.select(selType)
if selMode == QTextCursor.BlockUnderCursor: if selType == QTextCursor.BlockUnderCursor:
# This selection mode also selects the preceding paragraph # This selection mode also selects the preceding paragraph
# separator, which we want to avoid. # separator, which we want to avoid.
posS = theCursor.selectionStart() posS = theCursor.selectionStart()
@@ -467,19 +456,18 @@ class GuiDocViewer(QTextBrowser):
return return
def _makePosSelection(self, selMode, thePos): def _makePosSelection(self, selType: QTextCursor.SelectionType, pos: QPoint) -> None:
"""Wrapper function to select text based on selection mode, but """Handle text selection at a given location."""
first move cursor to given position. theCursor = self.cursorForPosition(pos)
"""
theCursor = self.cursorForPosition(thePos)
self.setTextCursor(theCursor) self.setTextCursor(theCursor)
self._makeSelection(selMode) self._makeSelection(selType)
return return
def _makeStyleSheet(self): def _makeStyleSheet(self) -> None:
"""Generate an appropriate style sheet for the document viewer, """Generate an appropriate style sheet for the document viewer,
based on the current syntax highlighter theme, based on the current syntax highlighter theme,
""" """
pTheme = SHARED.theme
styleSheet = ( styleSheet = (
"body {{" "body {{"
" color: rgb({tColR}, {tColG}, {tColB});" " color: rgb({tColR}, {tColG}, {tColB});"
@@ -506,31 +494,31 @@ class GuiDocViewer(QTextBrowser):
" text-align: center;" " text-align: center;"
"}}\n" "}}\n"
).format( ).format(
tColR=CONFIG.theme.colText[0], tColR=pTheme.colText[0],
tColG=CONFIG.theme.colText[1], tColG=pTheme.colText[1],
tColB=CONFIG.theme.colText[2], tColB=pTheme.colText[2],
hColR=CONFIG.theme.colHead[0], hColR=pTheme.colHead[0],
hColG=CONFIG.theme.colHead[1], hColG=pTheme.colHead[1],
hColB=CONFIG.theme.colHead[2], hColB=pTheme.colHead[2],
aColR=CONFIG.theme.colVal[0], aColR=pTheme.colVal[0],
aColG=CONFIG.theme.colVal[1], aColG=pTheme.colVal[1],
aColB=CONFIG.theme.colVal[2], aColB=pTheme.colVal[2],
eColR=CONFIG.theme.colEmph[0], eColR=pTheme.colEmph[0],
eColG=CONFIG.theme.colEmph[1], eColG=pTheme.colEmph[1],
eColB=CONFIG.theme.colEmph[2], eColB=pTheme.colEmph[2],
kColR=CONFIG.theme.colKey[0], kColR=pTheme.colKey[0],
kColG=CONFIG.theme.colKey[1], kColG=pTheme.colKey[1],
kColB=CONFIG.theme.colKey[2], kColB=pTheme.colKey[2],
cColR=CONFIG.theme.colHidden[0], cColR=pTheme.colHidden[0],
cColG=CONFIG.theme.colHidden[1], cColG=pTheme.colHidden[1],
cColB=CONFIG.theme.colHidden[2], cColB=pTheme.colHidden[2],
mColR=CONFIG.theme.colMod[0], mColR=pTheme.colMod[0],
mColG=CONFIG.theme.colMod[1], mColG=pTheme.colMod[1],
mColB=CONFIG.theme.colMod[2], mColB=pTheme.colMod[2],
) )
self.document().setDefaultStyleSheet(styleSheet) self.document().setDefaultStyleSheet(styleSheet)
return True return
# END Class GuiDocViewer # END Class GuiDocViewer
@@ -628,7 +616,7 @@ class GuiDocViewHistory:
"""Update the scrollbar position of the previous entry. """Update the scrollbar position of the previous entry.
""" """
if self._prevPos >= 0 and self._prevPos < len(self._posHistory): 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 return
def _updateNavButtons(self): def _updateNavButtons(self):
@@ -685,7 +673,7 @@ class GuiDocViewHeader(QWidget):
# Internal Variables # Internal Variables
self._docHandle = None self._docHandle = None
fPx = int(0.9*CONFIG.theme.fontPixelSize) fPx = int(0.9*SHARED.theme.fontPixelSize)
hSp = CONFIG.pxInt(6) hSp = CONFIG.pxInt(6)
# Main Widget Settings # Main Widget Settings
@@ -702,7 +690,7 @@ class GuiDocViewHeader(QWidget):
self.theTitle.setFixedHeight(fPx) self.theTitle.setFixedHeight(fPx)
lblFont = self.theTitle.font() lblFont = self.theTitle.font()
lblFont.setPointSizeF(0.9*CONFIG.theme.fontPointSize) lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
self.theTitle.setFont(lblFont) self.theTitle.setFont(lblFont)
# Buttons # Buttons
@@ -773,15 +761,15 @@ class GuiDocViewHeader(QWidget):
def updateTheme(self): def updateTheme(self):
"""Update theme elements. """Update theme elements.
""" """
self.backButton.setIcon(CONFIG.theme.getIcon("backward")) self.backButton.setIcon(SHARED.theme.getIcon("backward"))
self.forwardButton.setIcon(CONFIG.theme.getIcon("forward")) self.forwardButton.setIcon(SHARED.theme.getIcon("forward"))
self.refreshButton.setIcon(CONFIG.theme.getIcon("refresh")) self.refreshButton.setIcon(SHARED.theme.getIcon("refresh"))
self.closeButton.setIcon(CONFIG.theme.getIcon("close")) self.closeButton.setIcon(SHARED.theme.getIcon("close"))
buttonStyle = ( buttonStyle = (
"QToolButton {{border: none; background: transparent;}} " "QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
).format(*CONFIG.theme.colText) ).format(*SHARED.theme.colText)
self.backButton.setStyleSheet(buttonStyle) self.backButton.setStyleSheet(buttonStyle)
self.forwardButton.setStyleSheet(buttonStyle) self.forwardButton.setStyleSheet(buttonStyle)
@@ -797,9 +785,9 @@ class GuiDocViewHeader(QWidget):
theme rather than the main GUI. theme rather than the main GUI.
""" """
thePalette = QPalette() thePalette = QPalette()
thePalette.setColor(QPalette.Window, QColor(*CONFIG.theme.colBack)) thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*CONFIG.theme.colText)) thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
thePalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
self.setPalette(thePalette) self.setPalette(thePalette)
self.theTitle.setPalette(thePalette) self.theTitle.setPalette(thePalette)
@@ -819,7 +807,7 @@ class GuiDocViewHeader(QWidget):
self.refreshButton.setVisible(False) self.refreshButton.setVisible(False)
return True return True
pTree = self.mainGui.project.tree pTree = SHARED.project.tree
if CONFIG.showFullPath: if CONFIG.showFullPath:
tTitle = [] tTitle = []
tTree = pTree.getItemPath(tHandle) tTree = pTree.getItemPath(tHandle)
@@ -864,7 +852,7 @@ class GuiDocViewHeader(QWidget):
def _refreshDocument(self): def _refreshDocument(self):
"""Reload the content of the document. """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.mainGui.saveDocument()
self.docViewer.reloadText() self.docViewer.reloadText()
return return
@@ -902,7 +890,7 @@ class GuiDocViewFooter(QWidget):
# Internal Variables # Internal Variables
self._docHandle = None self._docHandle = None
fPx = int(0.9*CONFIG.theme.fontPixelSize) fPx = int(0.9*SHARED.theme.fontPixelSize)
bSp = CONFIG.pxInt(2) bSp = CONFIG.pxInt(2)
hSp = CONFIG.pxInt(8) hSp = CONFIG.pxInt(8)
@@ -987,7 +975,7 @@ class GuiDocViewFooter(QWidget):
self.lblSynopsis.setAlignment(Qt.AlignLeft | Qt.AlignTop) self.lblSynopsis.setAlignment(Qt.AlignLeft | Qt.AlignTop)
lblFont = self.font() lblFont = self.font()
lblFont.setPointSizeF(0.9*CONFIG.theme.fontPointSize) lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
self.lblRefs.setFont(lblFont) self.lblRefs.setFont(lblFont)
self.lblSticky.setFont(lblFont) self.lblSticky.setFont(lblFont)
self.lblComments.setFont(lblFont) self.lblComments.setFont(lblFont)
@@ -1032,21 +1020,21 @@ class GuiDocViewFooter(QWidget):
""" """
# Icons # Icons
fPx = int(0.9*CONFIG.theme.fontPixelSize) fPx = int(0.9*SHARED.theme.fontPixelSize)
stickyOn = CONFIG.theme.getPixmap("sticky-on", (fPx, fPx)) stickyOn = SHARED.theme.getPixmap("sticky-on", (fPx, fPx))
stickyOff = CONFIG.theme.getPixmap("sticky-off", (fPx, fPx)) stickyOff = SHARED.theme.getPixmap("sticky-off", (fPx, fPx))
stickyIcon = QIcon() stickyIcon = QIcon()
stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On) stickyIcon.addPixmap(stickyOn, QIcon.Normal, QIcon.On)
stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off) stickyIcon.addPixmap(stickyOff, QIcon.Normal, QIcon.Off)
bulletOn = CONFIG.theme.getPixmap("bullet-on", (fPx, fPx)) bulletOn = SHARED.theme.getPixmap("bullet-on", (fPx, fPx))
bulletOff = CONFIG.theme.getPixmap("bullet-off", (fPx, fPx)) bulletOff = SHARED.theme.getPixmap("bullet-off", (fPx, fPx))
bulletIcon = QIcon() bulletIcon = QIcon()
bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On) bulletIcon.addPixmap(bulletOn, QIcon.Normal, QIcon.On)
bulletIcon.addPixmap(bulletOff, QIcon.Normal, QIcon.Off) 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.stickyRefs.setIcon(stickyIcon)
self.showComments.setIcon(bulletIcon) self.showComments.setIcon(bulletIcon)
self.showSynopsis.setIcon(bulletIcon) self.showSynopsis.setIcon(bulletIcon)
@@ -1056,7 +1044,7 @@ class GuiDocViewFooter(QWidget):
buttonStyle = ( buttonStyle = (
"QToolButton {{border: none; background: transparent;}} " "QToolButton {{border: none; background: transparent;}} "
"QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}" "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
).format(*CONFIG.theme.colText) ).format(*SHARED.theme.colText)
self.showHide.setStyleSheet(buttonStyle) self.showHide.setStyleSheet(buttonStyle)
self.stickyRefs.setStyleSheet(buttonStyle) self.stickyRefs.setStyleSheet(buttonStyle)
@@ -1072,9 +1060,9 @@ class GuiDocViewFooter(QWidget):
theme rather than the main GUI. theme rather than the main GUI.
""" """
thePalette = QPalette() thePalette = QPalette()
thePalette.setColor(QPalette.Window, QColor(*CONFIG.theme.colBack)) thePalette.setColor(QPalette.Window, QColor(*SHARED.theme.colBack))
thePalette.setColor(QPalette.WindowText, QColor(*CONFIG.theme.colText)) thePalette.setColor(QPalette.WindowText, QColor(*SHARED.theme.colText))
thePalette.setColor(QPalette.Text, QColor(*CONFIG.theme.colText)) thePalette.setColor(QPalette.Text, QColor(*SHARED.theme.colText))
self.setPalette(thePalette) self.setPalette(thePalette)
self.lblRefs.setPalette(thePalette) self.lblRefs.setPalette(thePalette)
@@ -1102,8 +1090,8 @@ class GuiDocViewFooter(QWidget):
""" """
logger.debug("Reference sticky is %s", str(theState)) logger.debug("Reference sticky is %s", str(theState))
self.docViewer.stickyRef = theState self.docViewer.stickyRef = theState
if not theState and self.docViewer.docHandle() is not None: if not theState and self.docViewer.docHandle is not None:
self.viewMeta.refreshReferences(self.docViewer.docHandle()) self.viewMeta.refreshReferences(self.docViewer.docHandle)
return return
@pyqtSlot(bool) @pyqtSlot(bool)
@@ -1145,7 +1133,7 @@ class GuiDocViewDetails(QScrollArea):
self.refList.setScaledContents(True) self.refList.setScaledContents(True)
self.refList.linkActivated.connect(self._linkClicked) 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 # Assemble
self.outerWidget = QWidget() self.outerWidget = QWidget()
@@ -1172,10 +1160,10 @@ class GuiDocViewDetails(QScrollArea):
if self.mainGui.docViewer.stickyRef: if self.mainGui.docViewer.stickyRef:
return return
theRefs = self.mainGui.project.index.getBackReferenceList(tHandle) theRefs = SHARED.project.index.getBackReferenceList(tHandle)
theList = [] theList = []
for tHandle in theRefs: for tHandle in theRefs:
tItem = self.mainGui.project.tree[tHandle] tItem = SHARED.project.tree[tHandle]
if tItem is not None: if tItem is not None:
theList.append("<a href='%s#%s' %s>%s</a>" % ( theList.append("<a href='%s#%s' %s>%s</a>" % (
tHandle, theRefs[tHandle], self.linkStyle, tItem.itemName tHandle, theRefs[tHandle], self.linkStyle, tItem.itemName
+38 -44
View File
@@ -25,25 +25,28 @@ from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING
from PyQt5.QtGui import QFont
from PyQt5.QtCore import Qt, pyqtSlot from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtGui import QFont, QPixmap
from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel from PyQt5.QtWidgets import QWidget, QGridLayout, QLabel
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.constants import trConst, nwLabels from novelwriter.constants import trConst, nwLabels
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiItemDetails(QWidget): class GuiItemDetails(QWidget):
def __init__(self, mainGui): def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
logger.debug("Create: GuiItemDetails") logger.debug("Create: GuiItemDetails")
self.mainGui = mainGui
# Internal Variables # Internal Variables
self._itemHandle = None self._itemHandle = None
@@ -51,7 +54,7 @@ class GuiItemDetails(QWidget):
hSp = CONFIG.pxInt(6) hSp = CONFIG.pxInt(6)
vSp = CONFIG.pxInt(1) vSp = CONFIG.pxInt(1)
mPx = CONFIG.pxInt(6) mPx = CONFIG.pxInt(6)
fPt = CONFIG.theme.fontPointSize fPt = SHARED.theme.fontPointSize
fntLabel = QFont() fntLabel = QFont()
fntLabel.setBold(True) fntLabel.setBold(True)
@@ -176,8 +179,8 @@ class GuiItemDetails(QWidget):
self.updateTheme() self.updateTheme()
# Make sure the columns for flags and counts don't resize too often # Make sure the columns for flags and counts don't resize too often
flagWidth = CONFIG.theme.getTextWidth("Mm", fntValue) flagWidth = SHARED.theme.getTextWidth("Mm", fntValue)
countWidth = CONFIG.theme.getTextWidth("99,999", fntValue) countWidth = SHARED.theme.getTextWidth("99,999", fntValue)
self.mainBox.setColumnMinimumWidth(1, flagWidth) self.mainBox.setColumnMinimumWidth(1, flagWidth)
self.mainBox.setColumnMinimumWidth(4, countWidth) self.mainBox.setColumnMinimumWidth(4, countWidth)
@@ -189,35 +192,28 @@ class GuiItemDetails(QWidget):
# Class Methods # Class Methods
## ##
def clearDetails(self): def clearDetails(self) -> None:
"""Clear all the data values. """Clear all the data values."""
"""
self._itemHandle = None self._itemHandle = None
self.labelIcon.clear()
self.labelIcon.setPixmap(QPixmap(1, 1)) self.labelData.clear()
self.statusIcon.setPixmap(QPixmap(1, 1)) self.statusIcon.clear()
self.classIcon.setText("") self.statusData.clear()
self.usageIcon.setText("") self.classIcon.clear()
self.classData.clear()
self.labelData.setText("") self.usageIcon.clear()
self.statusData.setText("") self.usageData.clear()
self.classData.setText("") self.cCountData.clear()
self.usageData.setText("") self.wCountData.clear()
self.pCountData.clear()
self.cCountData.setText("")
self.wCountData.setText("")
self.pCountData.setText("")
return return
def refreshDetails(self): def refreshDetails(self) -> None:
"""Reload the content of the details panel. """Reload the content of the details panel."""
"""
self.updateViewBox(self._itemHandle) self.updateViewBox(self._itemHandle)
def updateTheme(self): def updateTheme(self) -> None:
"""Update theme elements. """Update theme elements."""
"""
self.updateViewBox(self._itemHandle) self.updateViewBox(self._itemHandle)
return return
@@ -226,20 +222,19 @@ class GuiItemDetails(QWidget):
## ##
@pyqtSlot(str) @pyqtSlot(str)
def updateViewBox(self, tHandle): def updateViewBox(self, tHandle: str) -> None:
"""Populate the details box from a given handle. """Populate the details box from a given handle."""
"""
if tHandle is None: if tHandle is None:
self.clearDetails() self.clearDetails()
return return
nwItem = self.mainGui.project.tree[tHandle] nwItem = SHARED.project.tree[tHandle]
if nwItem is None: if nwItem is None:
self.clearDetails() self.clearDetails()
return return
self._itemHandle = tHandle self._itemHandle = tHandle
iPx = int(round(0.8*CONFIG.theme.baseIconSize)) iPx = int(round(0.8*SHARED.theme.baseIconSize))
# Label # Label
# ===== # =====
@@ -250,11 +245,11 @@ class GuiItemDetails(QWidget):
if nwItem.isFileType(): if nwItem.isFileType():
if nwItem.isActive: if nwItem.isActive:
self.labelIcon.setPixmap(CONFIG.theme.getPixmap("checked", (iPx, iPx))) self.labelIcon.setPixmap(SHARED.theme.getPixmap("checked", (iPx, iPx)))
else: else:
self.labelIcon.setPixmap(CONFIG.theme.getPixmap("unchecked", (iPx, iPx))) self.labelIcon.setPixmap(SHARED.theme.getPixmap("unchecked", (iPx, iPx)))
else: else:
self.labelIcon.setPixmap(CONFIG.theme.getPixmap("noncheckable", (iPx, iPx))) self.labelIcon.setPixmap(SHARED.theme.getPixmap("noncheckable", (iPx, iPx)))
self.labelData.setText(theLabel) self.labelData.setText(theLabel)
@@ -268,14 +263,14 @@ class GuiItemDetails(QWidget):
# Class # 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.classIcon.setPixmap(classIcon.pixmap(iPx, iPx))
self.classData.setText(trConst(nwLabels.CLASS_NAME[nwItem.itemClass])) self.classData.setText(trConst(nwLabels.CLASS_NAME[nwItem.itemClass]))
# Layout # Layout
# ====== # ======
usageIcon = CONFIG.theme.getItemIcon( usageIcon = SHARED.theme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, nwItem.mainHeading
) )
self.usageIcon.setPixmap(usageIcon.pixmap(iPx, iPx)) self.usageIcon.setPixmap(usageIcon.pixmap(iPx, iPx))
@@ -296,7 +291,7 @@ class GuiItemDetails(QWidget):
return return
@pyqtSlot(str, int, int, int) @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 """Update the counts if the handle is the same as the one we're
already showing. Otherwise, do nothing. already showing. Otherwise, do nothing.
""" """
@@ -304,7 +299,6 @@ class GuiItemDetails(QWidget):
self.cCountData.setText(f"{cC:n}") self.cCountData.setText(f"{cC:n}")
self.wCountData.setText(f"{wC:n}") self.wCountData.setText(f"{wC:n}")
self.pCountData.setText(f"{pC:n}") self.pCountData.setText(f"{pC:n}")
return return
# END Class GuiItemDetails # END Class GuiItemDetails
+5 -5
View File
@@ -33,7 +33,7 @@ from PyQt5.QtCore import QUrl
from PyQt5.QtGui import QDesktopServices from PyQt5.QtGui import QDesktopServices
from PyQt5.QtWidgets import QMenuBar, QAction from PyQt5.QtWidgets import QMenuBar, QAction
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget from novelwriter.enum import nwDocAction, nwDocInsert, nwWidget
from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode from novelwriter.constants import nwConst, trConst, nwKeyWords, nwLabels, nwUnicode
@@ -349,13 +349,13 @@ class GuiMainMenu(QMenuBar):
# View > Go Backward # View > Go Backward
self.aViewPrev = QAction(self.tr("Navigate Backward"), self) self.aViewPrev = QAction(self.tr("Navigate Backward"), self)
self.aViewPrev.setShortcut("Alt+Left") 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) self.viewMenu.addAction(self.aViewPrev)
# View > Go Forward # View > Go Forward
self.aViewNext = QAction(self.tr("Navigate Forward"), self) self.aViewNext = QAction(self.tr("Navigate Forward"), self)
self.aViewNext.setShortcut("Alt+Right") 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) self.viewMenu.addAction(self.aViewNext)
# View > Separator # View > Separator
@@ -795,7 +795,7 @@ class GuiMainMenu(QMenuBar):
# Tools > Check Spelling # Tools > Check Spelling
self.aSpellCheck = QAction(self.tr("Check Spelling"), self) self.aSpellCheck = QAction(self.tr("Check Spelling"), self)
self.aSpellCheck.setCheckable(True) 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.triggered.connect(self._toggleSpellCheck) # triggered, not toggled!
self.aSpellCheck.setShortcut("Ctrl+F7") self.aSpellCheck.setShortcut("Ctrl+F7")
self.toolsMenu.addAction(self.aSpellCheck) self.toolsMenu.addAction(self.aSpellCheck)
@@ -825,7 +825,7 @@ class GuiMainMenu(QMenuBar):
# Tools > Backup Project # Tools > Backup Project
self.aBackupProject = QAction(self.tr("Backup Project"), self) 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) self.toolsMenu.addAction(self.aBackupProject)
# Tools > Build Manuscript # Tools > Build Manuscript
+30 -31
View File
@@ -38,7 +38,7 @@ from PyQt5.QtWidgets import (
QTreeWidgetItem, QVBoxLayout, QWidget QTreeWidgetItem, QVBoxLayout, QWidget
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocMode, nwItemClass, nwOutline from novelwriter.enum import nwDocMode, nwItemClass, nwOutline
from novelwriter.common import minmax from novelwriter.common import minmax
from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst
@@ -106,7 +106,7 @@ class GuiNovelView(QWidget):
self.novelTree.initSettings() self.novelTree.initSettings()
return return
def clearProject(self): def clearNovelView(self):
"""Clear project-related GUI content. """Clear project-related GUI content.
""" """
self.novelTree.clearContent() self.novelTree.clearContent()
@@ -117,21 +117,20 @@ class GuiNovelView(QWidget):
def openProjectTasks(self): def openProjectTasks(self):
"""Run open project tasks. """Run open project tasks.
""" """
lastNovel = self.mainGui.project.data.getLastHandle("novelTree") lastNovel = SHARED.project.data.getLastHandle("novelTree")
if lastNovel not in self.mainGui.project.tree: if lastNovel not in SHARED.project.tree:
lastNovel = self.mainGui.project.tree.findRoot(nwItemClass.NOVEL) lastNovel = SHARED.project.tree.findRoot(nwItemClass.NOVEL)
logger.debug("Setting novel tree to root item '%s'", lastNovel) 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 "GuiNovelView", "lastCol", NovelTreeColumn, NovelTreeColumn.HIDDEN
) )
lastColSize = self.mainGui.project.options.getInt( lastColSize = SHARED.project.options.getInt(
"GuiNovelView", "lastColSize", 25 "GuiNovelView", "lastColSize", 25
) )
self.clearProject() self.clearNovelView()
self.novelBar.buildNovelRootMenu() self.novelBar.buildNovelRootMenu()
self.novelBar.setLastColType(lastCol, doRefresh=False) self.novelBar.setLastColType(lastCol, doRefresh=False)
self.novelBar.setCurrentRoot(lastNovel) self.novelBar.setCurrentRoot(lastNovel)
@@ -142,13 +141,13 @@ class GuiNovelView(QWidget):
return return
def closeProjectTasks(self): def closeProjectTasks(self):
"""Run closing project tasks. """Run closing project tasks."""
"""
lastColType = self.novelTree.lastColType lastColType = self.novelTree.lastColType
lastColSize = self.novelTree.lastColSize lastColSize = self.novelTree.lastColSize
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
pOptions.setValue("GuiNovelView", "lastCol", lastColType) pOptions.setValue("GuiNovelView", "lastCol", lastColType)
pOptions.setValue("GuiNovelView", "lastColSize", lastColSize) pOptions.setValue("GuiNovelView", "lastColSize", lastColSize)
self.clearNovelView()
return return
def setTreeFocus(self): def setTreeFocus(self):
@@ -170,7 +169,7 @@ class GuiNovelView(QWidget):
def refreshTree(self): def refreshTree(self):
"""Refresh the current tree. """Refresh the current tree.
""" """
self.novelTree.refreshTree(rootHandle=self.mainGui.project.data.getLastHandle("novelTree")) self.novelTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("novelTree"))
return return
@pyqtSlot(str) @pyqtSlot(str)
@@ -201,7 +200,7 @@ class GuiNovelToolBar(QWidget):
self.novelView = novelView self.novelView = novelView
self.mainGui = novelView.mainGui self.mainGui = novelView.mainGui
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
mPx = CONFIG.pxInt(2) mPx = CONFIG.pxInt(2)
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
@@ -211,7 +210,7 @@ class GuiNovelToolBar(QWidget):
selFont = self.font() selFont = self.font()
selFont.setWeight(QFont.Bold) selFont.setWeight(QFont.Bold)
self.novelPrefix = self.tr("Outline of {0}") self.novelPrefix = self.tr("Outline of {0}")
self.novelValue = NovelSelector(self, self.mainGui) self.novelValue = NovelSelector(self)
self.novelValue.setFont(selFont) self.novelValue.setFont(selFont)
self.novelValue.setMinimumWidth(CONFIG.pxInt(150)) self.novelValue.setMinimumWidth(CONFIG.pxInt(150))
self.novelValue.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding) self.novelValue.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
@@ -274,9 +273,9 @@ class GuiNovelToolBar(QWidget):
"""Update theme elements. """Update theme elements.
""" """
# Icons # Icons
self.tbNovel.setIcon(CONFIG.theme.getIcon("cls_novel")) self.tbNovel.setIcon(SHARED.theme.getIcon("cls_novel"))
self.tbRefresh.setIcon(CONFIG.theme.getIcon("refresh")) self.tbRefresh.setIcon(SHARED.theme.getIcon("refresh"))
self.tbMore.setIcon(CONFIG.theme.getIcon("menu")) self.tbMore.setIcon(SHARED.theme.getIcon("menu"))
qPalette = self.palette() qPalette = self.palette()
qPalette.setBrush(QPalette.Window, qPalette.base()) qPalette.setBrush(QPalette.Window, qPalette.base())
@@ -345,7 +344,7 @@ class GuiNovelToolBar(QWidget):
def _refreshNovelTree(self): def _refreshNovelTree(self):
"""Rebuild the current tree. """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) self.novelView.novelTree.refreshTree(rootHandle=rootHandle, overRide=True)
return return
@@ -415,7 +414,7 @@ class GuiNovelTree(QTreeWidget):
# Build GUI # Build GUI
# ========= # =========
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
cMg = CONFIG.pxInt(6) cMg = CONFIG.pxInt(6)
self.setIconSize(QSize(iPx, iPx)) self.setIconSize(QSize(iPx, iPx))
@@ -481,8 +480,8 @@ class GuiNovelTree(QTreeWidget):
def updateTheme(self): def updateTheme(self):
"""Update theme elements. """Update theme elements.
""" """
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
self._pMore = CONFIG.theme.loadDecoration("deco_doc_more", pxH=iPx) self._pMore = SHARED.theme.loadDecoration("deco_doc_more", pxH=iPx)
return return
## ##
@@ -514,10 +513,10 @@ class GuiNovelTree(QTreeWidget):
""" """
logger.debug("Requesting refresh of the novel tree") logger.debug("Requesting refresh of the novel tree")
if rootHandle is None: 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) 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): if not (treeChanged or indexChanged or overRide):
logger.debug("No changes have been made to the novel index") logger.debug("No changes have been made to the novel index")
return return
@@ -528,7 +527,7 @@ class GuiNovelTree(QTreeWidget):
titleKey = selItem[0].data(self.C_DATA, self.D_KEY) titleKey = selItem[0].data(self.C_DATA, self.D_KEY)
self._populateTree(rootHandle) 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: if titleKey is not None and titleKey in self._treeMap:
self._treeMap[titleKey].setSelected(True) self._treeMap[titleKey].setSelected(True)
@@ -538,7 +537,7 @@ class GuiNovelTree(QTreeWidget):
def refreshHandle(self, tHandle): def refreshHandle(self, tHandle):
"""Refresh the data for a given handle. """Refresh the data for a given handle.
""" """
idxData = self.mainGui.project.index.getItemData(tHandle) idxData = SHARED.project.index.getItemData(tHandle)
if idxData is None: if idxData is None:
return return
@@ -575,7 +574,7 @@ class GuiNovelTree(QTreeWidget):
self._lastCol = colType self._lastCol = colType
self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN) self.setColumnHidden(self.C_EXTRA, colType == NovelTreeColumn.HIDDEN)
if doRefresh: if doRefresh:
lastNovel = self.mainGui.project.data.getLastHandle("novelTree") lastNovel = SHARED.project.data.getLastHandle("novelTree")
self.refreshTree(rootHandle=lastNovel, overRide=True) self.refreshTree(rootHandle=lastNovel, overRide=True)
return return
@@ -707,7 +706,7 @@ class GuiNovelTree(QTreeWidget):
tStart = time() tStart = time()
logger.debug("Building novel tree for root item '%s'", rootHandle) 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: for tKey, tHandle, sTitle, novIdx in novStruct:
if novIdx.level == "H0": if novIdx.level == "H0":
continue continue
@@ -733,7 +732,7 @@ class GuiNovelTree(QTreeWidget):
"""Set the tree item values from the index entry. """Set the tree item values from the index entry.
""" """
iLevel = nwHeaders.H_LEVEL.get(idxItem.level, 0) 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.setData(self.C_TITLE, Qt.DecorationRole, hDec)
trItem.setText(self.C_TITLE, idxItem.title) trItem.setText(self.C_TITLE, idxItem.title)
@@ -759,7 +758,7 @@ class GuiNovelTree(QTreeWidget):
refData = [] refData = []
refName = "" refName = ""
theRefs = self.mainGui.project.index.getReferences(tHandle, sTitle) theRefs = SHARED.project.index.getReferences(tHandle, sTitle)
if self._lastCol == NovelTreeColumn.POV: if self._lastCol == NovelTreeColumn.POV:
refData = theRefs[nwKeyWords.POV_KEY] refData = theRefs[nwKeyWords.POV_KEY]
refName = self._povLabel refName = self._povLabel
@@ -783,7 +782,7 @@ class GuiNovelTree(QTreeWidget):
""" """
logger.debug("Generating meta data tooltip for '%s:%s'", tHandle, sTitle) 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) novIdx = pIndex.getItemHeader(tHandle, sTitle)
refTags = pIndex.getReferences(tHandle, sTitle) refTags = pIndex.getReferences(tHandle, sTitle)
+31 -36
View File
@@ -41,7 +41,7 @@ from PyQt5.QtWidgets import (
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import ( from novelwriter.enum import (
nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline nwDocMode, nwItemClass, nwItemLayout, nwItemType, nwOutline
) )
@@ -62,8 +62,6 @@ class GuiOutlineView(QWidget):
def __init__(self, mainGui): def __init__(self, mainGui):
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
self.mainGui = mainGui
# Build GUI # Build GUI
self.outlineTree = GuiOutlineTree(self) self.outlineTree = GuiOutlineTree(self)
self.outlineData = GuiOutlineDetails(self) self.outlineData = GuiOutlineDetails(self)
@@ -117,10 +115,10 @@ class GuiOutlineView(QWidget):
def refreshTree(self): def refreshTree(self):
"""Refresh the current tree. """Refresh the current tree.
""" """
self.outlineTree.refreshTree(rootHandle=self.mainGui.project.data.getLastHandle("outline")) self.outlineTree.refreshTree(rootHandle=SHARED.project.data.getLastHandle("outline"))
return return
def clearProject(self): def clearOutline(self):
"""Clear project-related GUI content. """Clear project-related GUI content.
""" """
self.outlineData.clearDetails() self.outlineData.clearDetails()
@@ -130,13 +128,13 @@ class GuiOutlineView(QWidget):
def openProjectTasks(self): def openProjectTasks(self):
"""Run open project tasks. """Run open project tasks.
""" """
lastOutline = self.mainGui.project.data.getLastHandle("outline") lastOutline = SHARED.project.data.getLastHandle("outline")
if not (lastOutline in self.mainGui.project.tree or lastOutline is None): if not (lastOutline in SHARED.project.tree or lastOutline is None):
lastOutline = self.mainGui.project.tree.findRoot(nwItemClass.NOVEL) lastOutline = SHARED.project.tree.findRoot(nwItemClass.NOVEL)
logger.debug("Setting outline tree to root item '%s'", lastOutline) logger.debug("Setting outline tree to root item '%s'", lastOutline)
self.clearProject() self.clearOutline()
self.outlineBar.populateNovelList() self.outlineBar.populateNovelList()
self.outlineBar.setCurrentRoot(lastOutline) self.outlineBar.setCurrentRoot(lastOutline)
self.outlineBar.setEnabled(True) self.outlineBar.setEnabled(True)
@@ -146,6 +144,7 @@ class GuiOutlineView(QWidget):
def closeProjectTasks(self): def closeProjectTasks(self):
self.outlineTree.closeProjectTasks() self.outlineTree.closeProjectTasks()
self.outlineData.updateClasses() self.outlineData.updateClasses()
self.clearOutline()
return return
def splitSizes(self): def splitSizes(self):
@@ -214,8 +213,6 @@ class GuiOutlineToolBar(QToolBar):
logger.debug("Create: GuiOutlineToolBar") logger.debug("Create: GuiOutlineToolBar")
self.mainGui = theOutline.mainGui
iPx = CONFIG.pxInt(22) iPx = CONFIG.pxInt(22)
mPx = CONFIG.pxInt(12) mPx = CONFIG.pxInt(12)
@@ -230,7 +227,7 @@ class GuiOutlineToolBar(QToolBar):
self.novelLabel = QLabel(self.tr("Outline of")) self.novelLabel = QLabel(self.tr("Outline of"))
self.novelLabel.setContentsMargins(0, 0, mPx, 0) 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.setMinimumWidth(CONFIG.pxInt(200))
self.novelValue.novelSelectionChanged.connect(self._novelValueChanged) self.novelValue.novelSelectionChanged.connect(self._novelValueChanged)
@@ -272,8 +269,8 @@ class GuiOutlineToolBar(QToolBar):
self.setStyleSheet("QToolBar {border: 0px;}") self.setStyleSheet("QToolBar {border: 0px;}")
self.novelValue.updateList(includeAll=True) self.novelValue.updateList(includeAll=True)
self.aRefresh.setIcon(CONFIG.theme.getIcon("refresh")) self.aRefresh.setIcon(SHARED.theme.getIcon("refresh"))
self.tbColumns.setIcon(CONFIG.theme.getIcon("menu")) self.tbColumns.setIcon(SHARED.theme.getIcon("menu"))
return return
@@ -370,7 +367,6 @@ class GuiOutlineTree(QTreeWidget):
logger.debug("Create: GuiOutlineTree") logger.debug("Create: GuiOutlineTree")
self.outlineView = outlineView self.outlineView = outlineView
self.mainGui = outlineView.mainGui
self.setUniformRowHeights(True) self.setUniformRowHeights(True)
self.setFrameStyle(QFrame.NoFrame) self.setFrameStyle(QFrame.NoFrame)
@@ -381,7 +377,7 @@ class GuiOutlineTree(QTreeWidget):
self.itemDoubleClicked.connect(self._treeDoubleClick) self.itemDoubleClicked.connect(self._treeDoubleClick)
self.itemSelectionChanged.connect(self._itemSelected) self.itemSelectionChanged.connect(self._itemSelected)
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
self.setIconSize(QSize(iPx, iPx)) self.setIconSize(QSize(iPx, iPx))
self.setIndentation(0) self.setIndentation(0)
@@ -398,11 +394,11 @@ class GuiOutlineTree(QTreeWidget):
self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()] self._hFonts = [self.font(), fH1, fH2, self.font(), self.font()]
self._dIcon = { self._dIcon = {
"H0": CONFIG.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H0"), "H0": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H0"),
"H1": CONFIG.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H1"), "H1": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H1"),
"H2": CONFIG.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H2"), "H2": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H2"),
"H3": CONFIG.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H3"), "H3": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H3"),
"H4": CONFIG.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H4"), "H4": SHARED.theme.getItemIcon(nwItemType.FILE, None, nwItemLayout.DOCUMENT, "H4"),
} }
# Internals # Internals
@@ -488,13 +484,13 @@ class GuiOutlineTree(QTreeWidget):
# If the novel index or novel tree has changed since the tree # If the novel index or novel tree has changed since the tree
# was last built, we rebuild the tree from the updated index. # 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): if not (novelChanged or indexChanged or overRide):
logger.debug("No changes have been made to the novel index") logger.debug("No changes have been made to the novel index")
return return
self._populateTree(rootHandle) self._populateTree(rootHandle)
self.mainGui.project.data.setLastHandle(rootHandle or None, "outline") SHARED.project.data.setLastHandle(rootHandle or None, "outline")
return return
@@ -574,7 +570,7 @@ class GuiOutlineTree(QTreeWidget):
""" """
# Load whatever we saved last time, regardless of wether it # Load whatever we saved last time, regardless of wether it
# contains the correct names or number of columns. # contains the correct names or number of columns.
colState = self.mainGui.project.options.getValue("GuiOutline", "columnState", {}) colState = SHARED.project.options.getValue("GuiOutline", "columnState", {})
tmpOrder = [] tmpOrder = []
tmpHidden = {} tmpHidden = {}
@@ -625,7 +621,7 @@ class GuiOutlineTree(QTreeWidget):
logHidden, orgWidth if logHidden and logWidth == 0 else logWidth logHidden, orgWidth if logHidden and logWidth == 0 else logWidth
] ]
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
pOptions.setValue("GuiOutline", "columnState", colState) pOptions.setValue("GuiOutline", "columnState", colState)
pOptions.saveSettings() pOptions.saveSettings()
@@ -661,7 +657,7 @@ class GuiOutlineTree(QTreeWidget):
headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight) headItem.setTextAlignment(self._colIdx[nwOutline.WCOUNT], Qt.AlignRight)
headItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], 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: for _, tHandle, sTitle, novIdx in novStruct:
iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0) iLevel = nwHeaders.H_LEVEL.get(novIdx.level, 0)
@@ -669,8 +665,8 @@ class GuiOutlineTree(QTreeWidget):
continue continue
trItem = QTreeWidgetItem() trItem = QTreeWidgetItem()
nwItem = self.mainGui.project.tree[tHandle] nwItem = SHARED.project.tree[tHandle]
hDec = CONFIG.theme.getHeaderDecoration(iLevel) hDec = SHARED.theme.getHeaderDecoration(iLevel)
trItem.setData(self._colIdx[nwOutline.TITLE], Qt.DecorationRole, hDec) trItem.setData(self._colIdx[nwOutline.TITLE], Qt.DecorationRole, hDec)
trItem.setText(self._colIdx[nwOutline.TITLE], novIdx.title) 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.WCOUNT], Qt.AlignRight)
trItem.setTextAlignment(self._colIdx[nwOutline.PCOUNT], 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.POV], ", ".join(refs[nwKeyWords.POV_KEY]))
trItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(refs[nwKeyWords.FOCUS_KEY])) trItem.setText(self._colIdx[nwOutline.FOCUS], ", ".join(refs[nwKeyWords.FOCUS_KEY]))
trItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(refs[nwKeyWords.CHAR_KEY])) trItem.setText(self._colIdx[nwOutline.CHAR], ", ".join(refs[nwKeyWords.CHAR_KEY]))
@@ -770,12 +766,11 @@ class GuiOutlineDetails(QScrollArea):
logger.debug("Create: GuiOutlineDetails") logger.debug("Create: GuiOutlineDetails")
self.theOutline = theOutline self.theOutline = theOutline
self.mainGui = theOutline.mainGui
# Sizes # Sizes
minTitle = 30*CONFIG.theme.textNWidth minTitle = 30*SHARED.theme.textNWidth
maxTitle = 40*CONFIG.theme.textNWidth maxTitle = 40*SHARED.theme.textNWidth
wCount = CONFIG.theme.getTextWidth("999,999") wCount = SHARED.theme.getTextWidth("999,999")
hSpace = int(CONFIG.pxInt(10)) hSpace = int(CONFIG.pxInt(10))
vSpace = int(CONFIG.pxInt(4)) vSpace = int(CONFIG.pxInt(4))
@@ -1005,8 +1000,8 @@ class GuiOutlineDetails(QScrollArea):
"""Update the content of the tree with the given handle and line """Update the content of the tree with the given handle and line
number pointing to a header. number pointing to a header.
""" """
pIndex = self.mainGui.project.index pIndex = SHARED.project.index
nwItem = self.mainGui.project.tree[tHandle] nwItem = SHARED.project.tree[tHandle]
novIdx = pIndex.getItemHeader(tHandle, sTitle) novIdx = pIndex.getItemHeader(tHandle, sTitle)
theRefs = pIndex.getReferences(tHandle, sTitle) theRefs = pIndex.getReferences(tHandle, sTitle)
if nwItem is None or novIdx is None: if nwItem is None or novIdx is None:
@@ -1049,7 +1044,7 @@ class GuiOutlineDetails(QScrollArea):
def updateClasses(self): def updateClasses(self):
"""Update the visibility status of class details. """Update the visibility status of class details.
""" """
usedClasses = self.mainGui.project.tree.rootClasses() usedClasses = SHARED.project.tree.rootClasses()
pltVisible = nwItemClass.PLOT in usedClasses pltVisible = nwItemClass.PLOT in usedClasses
timVisible = nwItemClass.TIMELINE in usedClasses timVisible = nwItemClass.TIMELINE in usedClasses
+97 -107
View File
@@ -39,7 +39,7 @@ from PyQt5.QtWidgets import (
QVBoxLayout, QWidget QVBoxLayout, QWidget
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.common import minmax from novelwriter.common import minmax
from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels
from novelwriter.core.item import NWItem 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.editlabel import GuiEditLabel
from novelwriter.dialogs.projsettings import GuiProjectSettings from novelwriter.dialogs.projsettings import GuiProjectSettings
from novelwriter.enum import ( from novelwriter.enum import (
nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwAlert, nwWidget nwDocMode, nwItemType, nwItemClass, nwItemLayout, nwWidget
) )
if TYPE_CHECKING: # pragma: no cover if TYPE_CHECKING: # pragma: no cover
@@ -165,7 +165,7 @@ class GuiProjectView(QWidget):
self.projTree.initSettings() self.projTree.initSettings()
return return
def clearProject(self) -> None: def clearProjectView(self) -> None:
"""Clear project-related GUI content.""" """Clear project-related GUI content."""
self.projBar.clearContent() self.projBar.clearContent()
self.projBar.setEnabled(False) self.projBar.setEnabled(False)
@@ -236,7 +236,7 @@ class GuiProjectToolBar(QWidget):
self.projTree = projView.projTree self.projTree = projView.projTree
self.mainGui = projView.mainGui self.mainGui = projView.mainGui
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
mPx = CONFIG.pxInt(2) mPx = CONFIG.pxInt(2)
self.setContentsMargins(0, 0, 0, 0) self.setContentsMargins(0, 0, 0, 0)
@@ -367,16 +367,16 @@ class GuiProjectToolBar(QWidget):
self.tbAdd.setStyleSheet(buttonStyle) self.tbAdd.setStyleSheet(buttonStyle)
self.tbMore.setStyleSheet(buttonStyle) self.tbMore.setStyleSheet(buttonStyle)
self.tbQuick.setIcon(CONFIG.theme.getIcon("bookmark")) self.tbQuick.setIcon(SHARED.theme.getIcon("bookmark"))
self.tbMoveU.setIcon(CONFIG.theme.getIcon("up")) self.tbMoveU.setIcon(SHARED.theme.getIcon("up"))
self.tbMoveD.setIcon(CONFIG.theme.getIcon("down")) self.tbMoveD.setIcon(SHARED.theme.getIcon("down"))
self.aAddEmpty.setIcon(CONFIG.theme.getIcon("proj_document")) self.aAddEmpty.setIcon(SHARED.theme.getIcon("proj_document"))
self.aAddChap.setIcon(CONFIG.theme.getIcon("proj_chapter")) self.aAddChap.setIcon(SHARED.theme.getIcon("proj_chapter"))
self.aAddScene.setIcon(CONFIG.theme.getIcon("proj_scene")) self.aAddScene.setIcon(SHARED.theme.getIcon("proj_scene"))
self.aAddNote.setIcon(CONFIG.theme.getIcon("proj_note")) self.aAddNote.setIcon(SHARED.theme.getIcon("proj_note"))
self.aAddFolder.setIcon(CONFIG.theme.getIcon("proj_folder")) self.aAddFolder.setIcon(SHARED.theme.getIcon("proj_folder"))
self.tbAdd.setIcon(CONFIG.theme.getIcon("add")) self.tbAdd.setIcon(SHARED.theme.getIcon("add"))
self.tbMore.setIcon(CONFIG.theme.getIcon("menu")) self.tbMore.setIcon(SHARED.theme.getIcon("menu"))
self.buildQuickLinkMenu() self.buildQuickLinkMenu()
self._buildRootMenu() self._buildRootMenu()
@@ -392,10 +392,10 @@ class GuiProjectToolBar(QWidget):
"""Build the quick link menu.""" """Build the quick link menu."""
logger.debug("Rebuilding quick links menu") logger.debug("Rebuilding quick links menu")
self.mQuick.clear() 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 = self.mQuick.addAction(nwItem.itemName)
aRoot.setData(tHandle) 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( aRoot.triggered.connect(
lambda n, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True) lambda n, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True)
) )
@@ -409,7 +409,7 @@ class GuiProjectToolBar(QWidget):
"""Build the rood folder menu.""" """Build the rood folder menu."""
def addClass(itemClass): def addClass(itemClass):
aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[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)) aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass))
self.mAddRoot.addAction(aNew) self.mAddRoot.addAction(aNew)
return return
@@ -438,7 +438,7 @@ class GuiProjectToolBar(QWidget):
documents. They should only be visible if novel documents can documents. They should only be visible if novel documents can
actually be added. actually be added.
""" """
nwItem = self.mainGui.project.tree[tHandle] nwItem = SHARED.project.tree[tHandle]
allowDoc = isinstance(nwItem, NWItem) and nwItem.documentAllowed() allowDoc = isinstance(nwItem, NWItem) and nwItem.documentAllowed()
self.aAddEmpty.setVisible(allowDoc) self.aAddEmpty.setVisible(allowDoc)
self.aAddChap.setVisible(allowDoc) self.aAddChap.setVisible(allowDoc)
@@ -480,7 +480,7 @@ class GuiProjectTree(QTreeWidget):
self.customContextMenuRequested.connect(self._openContextMenu) self.customContextMenuRequested.connect(self._openContextMenu)
# Tree Settings # Tree Settings
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
cMg = CONFIG.pxInt(6) cMg = CONFIG.pxInt(6)
self.setIconSize(QSize(iPx, iPx)) 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 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. 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") logger.error("No project open")
return False return False
@@ -572,19 +572,17 @@ class GuiProjectTree(QTreeWidget):
if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass): if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
tHandle = self.mainGui.project.newRoot(itemClass) tHandle = SHARED.project.newRoot(itemClass)
sHandle = self.getSelectedHandle() 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 nHandle = pItem.itemRoot if pItem else None
elif itemType in (nwItemType.FILE, nwItemType.FOLDER): elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
sHandle = self.getSelectedHandle() 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: if sHandle is None or pItem is None:
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr("Did not find anywhere to add the file or folder!"))
"Did not find anywhere to add the file or folder!"
), level=nwAlert.ERROR)
return False return False
# Collect some information about the selected item # Collect some information about the selected item
@@ -592,10 +590,8 @@ class GuiProjectTree(QTreeWidget):
sLevel = nwHeaders.H_LEVEL.get(pItem.mainHeading, 0) sLevel = nwHeaders.H_LEVEL.get(pItem.mainHeading, 0)
sIsParent = False if qItem is None else qItem.childCount() > 0 sIsParent = False if qItem is None else qItem.childCount() > 0
if self.mainGui.project.tree.isTrash(sHandle): if SHARED.project.tree.isTrash(sHandle):
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr("Cannot add new files or folders to the Trash folder."))
"Cannot add new files or folders to the Trash folder."
), level=nwAlert.ERROR)
return False return False
# Set default label and determine if new item is to be added # Set default label and determine if new item is to be added
@@ -635,9 +631,9 @@ class GuiProjectTree(QTreeWidget):
# Add the file or folder # Add the file or folder
if itemType == nwItemType.FILE: if itemType == nwItemType.FILE:
tHandle = self.mainGui.project.newFile(newLabel, sHandle) tHandle = SHARED.project.newFile(newLabel, sHandle)
else: else:
tHandle = self.mainGui.project.newFolder(newLabel, sHandle) tHandle = SHARED.project.newFolder(newLabel, sHandle)
else: else:
logger.error("Failed to add new item") logger.error("Failed to add new item")
@@ -650,7 +646,7 @@ class GuiProjectTree(QTreeWidget):
# Handle new file creation # Handle new file creation
if itemType == nwItemType.FILE and hLevel > 0: 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 # Add the new item to the project tree
self.revealNewTreeItem(tHandle, nHandle=nHandle, wordCount=True) self.revealNewTreeItem(tHandle, nHandle=nHandle, wordCount=True)
@@ -661,7 +657,7 @@ class GuiProjectTree(QTreeWidget):
def revealNewTreeItem(self, tHandle: str | None, nHandle: str | None = None, def revealNewTreeItem(self, tHandle: str | None, nHandle: str | None = None,
wordCount: bool = False) -> bool: wordCount: bool = False) -> bool:
"""Reveal a newly added project item in the project tree.""" """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: if tHandle is None or nwItem is None:
return False return False
@@ -670,7 +666,7 @@ class GuiProjectTree(QTreeWidget):
return False return False
if nwItem.isFileType() and wordCount: 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.propagateCount(tHandle, wC)
self.projView.wordCountsChanged.emit() self.projView.wordCountsChanged.emit()
@@ -745,7 +741,7 @@ class GuiProjectTree(QTreeWidget):
def renameTreeItem(self, tHandle: str) -> bool: def renameTreeItem(self, tHandle: str) -> bool:
"""Open a dialog to edit the label of an item.""" """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: if tItem is None:
return False return False
@@ -769,7 +765,7 @@ class GuiProjectTree(QTreeWidget):
if isinstance(item, QTreeWidgetItem): if isinstance(item, QTreeWidgetItem):
theList = self._scanChildren(theList, item, i) theList = self._scanChildren(theList, item, i)
logger.debug("Saving project tree item order") logger.debug("Saving project tree item order")
self.mainGui.project.setTreeOrder(theList) SHARED.project.setTreeOrder(theList)
return return
def getTreeFromHandle(self, tHandle: str) -> list[str]: 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 can be called on any item, and will check whether to attempt a
permanent deletion or moving the item to Trash. permanent deletion or moving the item to Trash.
""" """
if not self.mainGui.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
@@ -802,16 +798,16 @@ class GuiProjectTree(QTreeWidget):
logger.error("There is no item to delete") logger.error("There is no item to delete")
return False return False
trashHandle = self.mainGui.project.tree.trashRoot() trashHandle = SHARED.project.tree.trashRoot()
if tHandle == trashHandle: if tHandle == trashHandle:
logger.error("Cannot delete the Trash folder") logger.error("Cannot delete the Trash folder")
return False return False
nwItem = self.mainGui.project.tree[tHandle] nwItem = SHARED.project.tree[tHandle]
if nwItem is None: if nwItem is None:
return False 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) status = self.permDeleteItem(tHandle)
else: else:
status = self.moveItemToTrash(tHandle) status = self.moveItemToTrash(tHandle)
@@ -823,17 +819,15 @@ class GuiProjectTree(QTreeWidget):
function only asks for confirmation once, and calls the regular function only asks for confirmation once, and calls the regular
deleteItem function for each document in the Trash folder. deleteItem function for each document in the Trash folder.
""" """
if not self.mainGui.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
trashHandle = self.mainGui.project.tree.trashRoot() trashHandle = SHARED.project.tree.trashRoot()
logger.debug("Emptying Trash folder") logger.debug("Emptying Trash folder")
if trashHandle is None: if trashHandle is None:
self.mainGui.makeAlert(self.tr( SHARED.info(self.tr("There is currently no Trash folder in this project."))
"There is currently no Trash folder in this project."
))
return False return False
theTrash = self.getTreeFromHandle(trashHandle) theTrash = self.getTreeFromHandle(trashHandle)
@@ -842,12 +836,10 @@ class GuiProjectTree(QTreeWidget):
nTrash = len(theTrash) nTrash = len(theTrash)
if nTrash == 0: if nTrash == 0:
self.mainGui.makeAlert(self.tr( SHARED.info(self.tr("The Trash folder is already empty."))
"The Trash folder is already empty."
))
return False return False
msgYes = self.mainGui.askQuestion( msgYes = SHARED.question(
self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash) self.tr("Permanently delete {0} file(s) from Trash?").format(nTrash)
) )
if not msgYes: if not msgYes:
@@ -870,13 +862,13 @@ class GuiProjectTree(QTreeWidget):
so such a request is cancelled. so such a request is cancelled.
""" """
trItemS = self._getTreeItem(tHandle) trItemS = self._getTreeItem(tHandle)
nwItemS = self.mainGui.project.tree[tHandle] nwItemS = SHARED.project.tree[tHandle]
if trItemS is None or nwItemS is None: if trItemS is None or nwItemS is None:
logger.error("Could not find tree item for deletion") logger.error("Could not find tree item for deletion")
return False return False
if self.mainGui.project.tree.isTrash(tHandle): if SHARED.project.tree.isTrash(tHandle):
logger.error("Item is already in the Trash folder") logger.error("Item is already in the Trash folder")
return False return False
@@ -893,8 +885,8 @@ class GuiProjectTree(QTreeWidget):
return False return False
if askFirst: if askFirst:
msgYes = self.mainGui.askQuestion( msgYes = SHARED.question(
self.tr("Move '{0}' to Trash?").format(nwItemS.itemName), self.tr("Move '{0}' to Trash?").format(nwItemS.itemName)
) )
if not msgYes: if not msgYes:
logger.info("Action cancelled by user") logger.info("Action cancelled by user")
@@ -920,7 +912,7 @@ class GuiProjectTree(QTreeWidget):
Root items are handled a little different than other items. Root items are handled a little different than other items.
""" """
trItemS = self._getTreeItem(tHandle) trItemS = self._getTreeItem(tHandle)
nwItemS = self.mainGui.project.tree[tHandle] nwItemS = SHARED.project.tree[tHandle]
if trItemS is None or nwItemS is None: if trItemS is None or nwItemS is None:
logger.error("Could not find tree item for deletion") logger.error("Could not find tree item for deletion")
return False return False
@@ -928,16 +920,14 @@ class GuiProjectTree(QTreeWidget):
if nwItemS.isRootType(): if nwItemS.isRootType():
# Only an empty ROOT folder can be deleted # Only an empty ROOT folder can be deleted
if trItemS.childCount() > 0: if trItemS.childCount() > 0:
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr("Root folders can only be deleted when they are empty."))
"Root folders can only be deleted when they are empty."
), level=nwAlert.ERROR)
return False return False
logger.debug("Permanently deleting root folder '%s'", tHandle) logger.debug("Permanently deleting root folder '%s'", tHandle)
tIndex = self.indexOfTopLevelItem(trItemS) tIndex = self.indexOfTopLevelItem(trItemS)
self.takeTopLevelItem(tIndex) self.takeTopLevelItem(tIndex)
self.mainGui.project.removeItem(tHandle) SHARED.project.removeItem(tHandle)
self._treeMap.pop(tHandle, None) self._treeMap.pop(tHandle, None)
self._alertTreeChange(tHandle, flush=True) self._alertTreeChange(tHandle, flush=True)
@@ -948,7 +938,7 @@ class GuiProjectTree(QTreeWidget):
else: else:
if askFirst: if askFirst:
msgYes = self.mainGui.askQuestion( msgYes = SHARED.question(
self.tr("Permanently delete '{0}'?").format(nwItemS.itemName) self.tr("Permanently delete '{0}'?").format(nwItemS.itemName)
) )
if not msgYes: if not msgYes:
@@ -964,9 +954,9 @@ class GuiProjectTree(QTreeWidget):
trItemP.takeChild(tIndex) trItemP.takeChild(tIndex)
for dHandle in reversed(self.getTreeFromHandle(tHandle)): for dHandle in reversed(self.getTreeFromHandle(tHandle)):
if self.mainGui.docEditor.docHandle() == dHandle: if self.mainGui.docEditor.docHandle == dHandle:
self.mainGui.closeDocument() self.mainGui.closeDocument()
self.mainGui.project.removeItem(dHandle) SHARED.project.removeItem(dHandle)
self._treeMap.pop(dHandle, None) self._treeMap.pop(dHandle, None)
self._alertTreeChange(tHandle, flush=flush) self._alertTreeChange(tHandle, flush=flush)
@@ -984,13 +974,13 @@ class GuiProjectTree(QTreeWidget):
already coming from the project tree. already coming from the project tree.
""" """
trItem = self._getTreeItem(tHandle) trItem = self._getTreeItem(tHandle)
nwItem = self.mainGui.project.tree[tHandle] nwItem = SHARED.project.tree[tHandle]
if trItem is None or nwItem is None: if trItem is None or nwItem is None:
return return
itemStatus, statusIcon = nwItem.getImportStatus(incIcon=True) itemStatus, statusIcon = nwItem.getImportStatus(incIcon=True)
hLevel = nwItem.mainHeading hLevel = nwItem.mainHeading
itemIcon = CONFIG.theme.getItemIcon( itemIcon = SHARED.theme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
) )
@@ -1006,7 +996,7 @@ class GuiProjectTree(QTreeWidget):
else: else:
iconName = "noncheckable" 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(): if CONFIG.emphLabels and nwItem.isDocumentLayout():
trFont = trItem.font(self.C_NAME) trFont = trItem.font(self.C_NAME)
@@ -1046,10 +1036,10 @@ class GuiProjectTree(QTreeWidget):
pHandle = pItem.data(self.C_DATA, self.D_HANDLE) pHandle = pItem.data(self.C_DATA, self.D_HANDLE)
if pHandle: 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 # A file has an internal word count we need to account
# for, but a folder always has 0 words on its own. # 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) self.propagateCount(pHandle, pCount, countChildren=False)
@@ -1064,7 +1054,7 @@ class GuiProjectTree(QTreeWidget):
logger.debug("Building the project tree ...") logger.debug("Building the project tree ...")
self.clearTree() self.clearTree()
count = 0 count = 0
for nwItem in self.mainGui.project.getProjectItems(): for nwItem in SHARED.project.iterProjectItems():
count += 1 count += 1
self._addTreeItem(nwItem) self._addTreeItem(nwItem)
if count > 0: if count > 0:
@@ -1177,7 +1167,7 @@ class GuiProjectTree(QTreeWidget):
if tHandle is None: if tHandle is None:
return return
tItem = self.mainGui.project.tree[tHandle] tItem = SHARED.project.tree[tHandle]
if tItem is None: if tItem is None:
return return
@@ -1199,7 +1189,7 @@ class GuiProjectTree(QTreeWidget):
selItem = self.itemAt(clickPos) selItem = self.itemAt(clickPos)
if isinstance(selItem, QTreeWidgetItem): if isinstance(selItem, QTreeWidgetItem):
tHandle = selItem.data(self.C_DATA, self.D_HANDLE) tHandle = selItem.data(self.C_DATA, self.D_HANDLE)
tItem = self.mainGui.project.tree[tHandle] tItem = SHARED.project.tree[tHandle]
hasChild = selItem.childCount() > 0 hasChild = selItem.childCount() > 0
if tItem is None or tHandle is None: if tItem is None or tHandle is None:
@@ -1211,7 +1201,7 @@ class GuiProjectTree(QTreeWidget):
# Trash Folder # Trash Folder
# ============ # ============
trashHandle = self.mainGui.project.tree.trashRoot() trashHandle = SHARED.project.tree.trashRoot()
if tItem.itemHandle == trashHandle and trashHandle is not None: if tItem.itemHandle == trashHandle and trashHandle is not None:
# The trash folder only has one option # The trash folder only has one option
aEmptyTrash = ctxMenu.addAction(self.tr("Empty Trash")) aEmptyTrash = ctxMenu.addAction(self.tr("Empty Trash"))
@@ -1250,7 +1240,7 @@ class GuiProjectTree(QTreeWidget):
checkMark = f" ({nwUnicode.U_CHECK})" checkMark = f" ({nwUnicode.U_CHECK})"
if tItem.isNovelLike(): if tItem.isNovelLike():
mStatus = ctxMenu.addMenu(self.tr("Set Status to ...")) 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 "") entryName = entry["name"] + (checkMark if tItem.itemStatus == key else "")
aStatus = mStatus.addAction(entry["icon"], entryName) aStatus = mStatus.addAction(entry["icon"], entryName)
aStatus.triggered.connect( aStatus.triggered.connect(
@@ -1263,7 +1253,7 @@ class GuiProjectTree(QTreeWidget):
) )
else: else:
mImport = ctxMenu.addMenu(self.tr("Set Importance to ...")) 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 "") entryName = entry["name"] + (checkMark if tItem.itemImport == key else "")
aImport = mImport.addAction(entry["icon"], entryName) aImport = mImport.addAction(entry["icon"], entryName)
aImport.triggered.connect( aImport.triggered.connect(
@@ -1375,7 +1365,7 @@ class GuiProjectTree(QTreeWidget):
return return
tHandle = selItem.data(self.C_DATA, self.D_HANDLE) tHandle = selItem.data(self.C_DATA, self.D_HANDLE)
tItem = self.mainGui.project.tree[tHandle] tItem = SHARED.project.tree[tHandle]
if tItem is None: if tItem is None:
return return
@@ -1419,7 +1409,7 @@ class GuiProjectTree(QTreeWidget):
def _postItemMove(self, tHandle: str, wCount: int) -> bool: def _postItemMove(self, tHandle: str, wCount: int) -> bool:
"""Run various maintenance tasks for a moved item.""" """Run various maintenance tasks for a moved item."""
trItemS = self._getTreeItem(tHandle) trItemS = self._getTreeItem(tHandle)
nwItemS = self.mainGui.project.tree[tHandle] nwItemS = SHARED.project.tree[tHandle]
trItemP = trItemS.parent() if trItemS else None trItemP = trItemS.parent() if trItemS else None
if trItemP is None or nwItemS is None: if trItemP is None or nwItemS is None:
logger.error("Failed to find new parent item of '%s'", tHandle) 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)) logger.debug("A total of %d item(s) were moved", len(mHandles))
for mHandle in mHandles: for mHandle in mHandles:
logger.debug("Updating item '%s'", mHandle) logger.debug("Updating item '%s'", mHandle)
self.mainGui.project.tree.updateItemData(mHandle) SHARED.project.tree.updateItemData(mHandle)
# Update the index # Update the index
if nwItemS.isInactiveClass(): if nwItemS.isInactiveClass():
self.mainGui.project.index.deleteHandle(mHandle) SHARED.project.index.deleteHandle(mHandle)
else: else:
self.mainGui.project.index.reIndexHandle(mHandle) SHARED.project.index.reIndexHandle(mHandle)
self.setTreeItemValues(mHandle) self.setTreeItemValues(mHandle)
@@ -1462,7 +1452,7 @@ class GuiProjectTree(QTreeWidget):
def _toggleItemActive(self, tHandle: str) -> None: def _toggleItemActive(self, tHandle: str) -> None:
"""Toggle the active status of an item.""" """Toggle the active status of an item."""
tItem = self.mainGui.project.tree[tHandle] tItem = SHARED.project.tree[tHandle]
if tItem is not None: if tItem is not None:
tItem.setActive(not tItem.isActive) tItem.setActive(not tItem.isActive)
self.setTreeItemValues(tItem.itemHandle) self.setTreeItemValues(tItem.itemHandle)
@@ -1483,7 +1473,7 @@ class GuiProjectTree(QTreeWidget):
def _changeItemStatus(self, tHandle: str, tStatus: str) -> None: def _changeItemStatus(self, tHandle: str, tStatus: str) -> None:
"""Set a new status value of an item.""" """Set a new status value of an item."""
tItem = self.mainGui.project.tree[tHandle] tItem = SHARED.project.tree[tHandle]
if tItem is not None: if tItem is not None:
tItem.setStatus(tStatus) tItem.setStatus(tStatus)
self.setTreeItemValues(tItem.itemHandle) self.setTreeItemValues(tItem.itemHandle)
@@ -1492,7 +1482,7 @@ class GuiProjectTree(QTreeWidget):
def _changeItemImport(self, tHandle: str, tImport: str) -> None: def _changeItemImport(self, tHandle: str, tImport: str) -> None:
"""Set a new importance value of an item.""" """Set a new importance value of an item."""
tItem = self.mainGui.project.tree[tHandle] tItem = SHARED.project.tree[tHandle]
if tItem is not None: if tItem is not None:
tItem.setImport(tImport) tItem.setImport(tImport)
self.setTreeItemValues(tItem.itemHandle) self.setTreeItemValues(tItem.itemHandle)
@@ -1501,7 +1491,7 @@ class GuiProjectTree(QTreeWidget):
def _changeItemLayout(self, tHandle: str, itemLayout: nwItemLayout) -> None: def _changeItemLayout(self, tHandle: str, itemLayout: nwItemLayout) -> None:
"""Set a new item layout value of an item.""" """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 tItem is not None:
if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed(): if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed():
tItem.setLayout(nwItemLayout.DOCUMENT) tItem.setLayout(nwItemLayout.DOCUMENT)
@@ -1515,9 +1505,9 @@ class GuiProjectTree(QTreeWidget):
def _covertFolderToFile(self, tHandle: str, itemLayout: nwItemLayout) -> None: def _covertFolderToFile(self, tHandle: str, itemLayout: nwItemLayout) -> None:
"""Convert a folder to a note or document.""" """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(): 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}? " "Do you want to convert the folder to a {0}? "
"This action cannot be reversed." "This action cannot be reversed."
).format(trConst(nwLabels.LAYOUT_NAME[itemLayout]))) ).format(trConst(nwLabels.LAYOUT_NAME[itemLayout])))
@@ -1540,7 +1530,7 @@ class GuiProjectTree(QTreeWidget):
logger.info("Request to merge items under handle '%s'", tHandle) logger.info("Request to merge items under handle '%s'", tHandle)
itemList = self.getTreeFromHandle(tHandle) itemList = self.getTreeFromHandle(tHandle)
tItem = self.mainGui.project.tree[tHandle] tItem = SHARED.project.tree[tHandle]
if tItem is None: if tItem is None:
return False return False
@@ -1559,14 +1549,14 @@ class GuiProjectTree(QTreeWidget):
mrgData = dlgMerge.getData() mrgData = dlgMerge.getData()
mrgList = mrgData.get("finalItems", []) mrgList = mrgData.get("finalItems", [])
if not mrgList: if not mrgList:
self.mainGui.makeAlert(self.tr("No documents selected for merging.")) SHARED.info(self.tr("No documents selected for merging."))
return False return False
# Save the open document first, in case it's part of merge # Save the open document first, in case it's part of merge
self.mainGui.saveDocument() self.mainGui.saveDocument()
# Create merge object, and append docs # Create merge object, and append docs
docMerger = DocMerger(self.mainGui.project) docMerger = DocMerger(SHARED.project)
mLabel = self.tr("Merged") mLabel = self.tr("Merged")
if newFile: if newFile:
@@ -1582,13 +1572,13 @@ class GuiProjectTree(QTreeWidget):
docMerger.appendText(sHandle, True, mLabel) docMerger.appendText(sHandle, True, mLabel)
if not docMerger.writeTargetDoc(): if not docMerger.writeTargetDoc():
self.mainGui.makeAlert( SHARED.error(
self.tr("Could not write document content."), self.tr("Could not write document content."),
info=docMerger.getError(), level=nwAlert.ERROR info=docMerger.getError()
) )
return False return False
self.mainGui.project.index.reIndexHandle(mHandle) SHARED.project.index.reIndexHandle(mHandle)
if newFile: if newFile:
self.revealNewTreeItem(mHandle, nHandle=tHandle, wordCount=True) self.revealNewTreeItem(mHandle, nHandle=tHandle, wordCount=True)
@@ -1613,7 +1603,7 @@ class GuiProjectTree(QTreeWidget):
"""Split a document into multiple documents.""" """Split a document into multiple documents."""
logger.info("Request to split items with handle '%s'", tHandle) 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: if tItem is None:
return False return False
@@ -1632,7 +1622,7 @@ class GuiProjectTree(QTreeWidget):
intoFolder = splitData.get("intoFolder", False) intoFolder = splitData.get("intoFolder", False)
docHierarchy = splitData.get("docHierarchy", False) docHierarchy = splitData.get("docHierarchy", False)
docSplit = DocSplitter(self.mainGui.project, tHandle) docSplit = DocSplitter(SHARED.project, tHandle)
if intoFolder: if intoFolder:
fHandle = docSplit.newParentFolder(tItem.itemParent, tItem.itemName) fHandle = docSplit.newParentFolder(tItem.itemParent, tItem.itemName)
self.revealNewTreeItem(fHandle, nHandle=tHandle) self.revealNewTreeItem(fHandle, nHandle=tHandle)
@@ -1642,13 +1632,13 @@ class GuiProjectTree(QTreeWidget):
docSplit.splitDocument(headerList, splitText) docSplit.splitDocument(headerList, splitText)
for writeOk, dHandle, nHandle in docSplit.writeDocuments(docHierarchy): 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.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True)
self._alertTreeChange(dHandle, flush=False) self._alertTreeChange(dHandle, flush=False)
if not writeOk: if not writeOk:
self.mainGui.makeAlert( SHARED.error(
self.tr("Could not write document content."), self.tr("Could not write document content."),
info=docSplit.getError(), level=nwAlert.ERROR info=docSplit.getError()
) )
if splitData.get("moveToTrash", False): if splitData.get("moveToTrash", False):
@@ -1673,19 +1663,19 @@ class GuiProjectTree(QTreeWidget):
else: else:
question = self.tr("Do you want to duplicate this item and all child items?") 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 return False
docDup = DocDuplicator(self.mainGui.project) docDup = DocDuplicator(SHARED.project)
dupCount = 0 dupCount = 0
for dHandle, nHandle in docDup.duplicate(itemTree): 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.revealNewTreeItem(dHandle, nHandle=nHandle, wordCount=True)
self._alertTreeChange(dHandle, flush=False) self._alertTreeChange(dHandle, flush=False)
dupCount += 1 dupCount += 1
if dupCount != nItems: 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() self.saveTreeOrder()
@@ -1699,7 +1689,7 @@ class GuiProjectTree(QTreeWidget):
cCount = tItem.childCount() cCount = tItem.childCount()
# Update tree-related meta data # Update tree-related meta data
nwItem = self.mainGui.project.tree[tHandle] nwItem = SHARED.project.tree[tHandle]
if nwItem is not None: if nwItem is not None:
nwItem.setExpanded(tItem.isExpanded() and cCount > 0) nwItem.setExpanded(tItem.isExpanded() and cCount > 0)
nwItem.setOrder(tIndex) nwItem.setOrder(tIndex)
@@ -1742,9 +1732,9 @@ class GuiProjectTree(QTreeWidget):
elif pHandle and pHandle in self._treeMap: elif pHandle and pHandle in self._treeMap:
pItem = self._treeMap[pHandle] pItem = self._treeMap[pHandle]
else: else:
self.mainGui.makeAlert(self.tr( SHARED.error(self.tr(
"There is nowhere to add item with name '{0}'." "There is nowhere to add item with name '{0}'."
).format(nwItem.itemName), level=nwAlert.ERROR) ).format(nwItem.itemName))
return None return None
byIndex = -1 byIndex = -1
@@ -1766,13 +1756,13 @@ class GuiProjectTree(QTreeWidget):
"""Adds the trash root folder if it doesn't already exist in the """Adds the trash root folder if it doesn't already exist in the
project tree. project tree.
""" """
trashHandle = self.mainGui.project.trashFolder() trashHandle = SHARED.project.trashFolder()
if trashHandle is None: if trashHandle is None:
return None return None
trItem = self._getTreeItem(trashHandle) trItem = self._getTreeItem(trashHandle)
if trItem is None: if trItem is None:
trItem = self._addTreeItem(self.mainGui.project.tree[trashHandle]) trItem = self._addTreeItem(SHARED.project.tree[trashHandle])
if trItem is not None: if trItem is not None:
trItem.setExpanded(True) trItem.setExpanded(True)
self._alertTreeChange(trashHandle, flush=True) self._alertTreeChange(trashHandle, flush=True)
@@ -1785,14 +1775,14 @@ class GuiProjectTree(QTreeWidget):
deleted. deleted.
""" """
self._timeChanged = time() self._timeChanged = time()
self.mainGui.project.setProjectChanged(True) SHARED.project.setProjectChanged(True)
if flush: if flush:
self.saveTreeOrder() 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 return
tItem = self.mainGui.project.tree[tHandle] tItem = SHARED.project.tree[tHandle]
if tItem and tItem.isRootType(): if tItem and tItem.isRootType():
self.projView.rootFolderChanged.emit(tHandle) self.projView.rootFolderChanged.emit(tHandle)
+10 -10
View File
@@ -30,7 +30,7 @@ from PyQt5.QtWidgets import (
QToolBar, QWidget, QSizePolicy, QAction, QMenu, QToolButton QToolBar, QWidget, QSizePolicy, QAction, QMenu, QToolButton
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwView from novelwriter.enum import nwView
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -51,8 +51,8 @@ class GuiSideBar(QToolBar):
iPx = CONFIG.pxInt(22) iPx = CONFIG.pxInt(22)
mPx = CONFIG.pxInt(60) mPx = CONFIG.pxInt(60)
lblFont = CONFIG.theme.guiFont lblFont = SHARED.theme.guiFont
lblFont.setPointSizeF(0.65*CONFIG.theme.fontPointSize) lblFont.setPointSizeF(0.65*SHARED.theme.fontPointSize)
self.setMovable(False) self.setMovable(False)
self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon) self.setToolButtonStyle(Qt.ToolButtonTextUnderIcon)
@@ -130,13 +130,13 @@ class GuiSideBar(QToolBar):
""" """
self.setStyleSheet("QToolBar {border: 0px;}") self.setStyleSheet("QToolBar {border: 0px;}")
self.aProject.setIcon(CONFIG.theme.getIcon("view_editor")) self.aProject.setIcon(SHARED.theme.getIcon("view_editor"))
self.aNovel.setIcon(CONFIG.theme.getIcon("view_novel")) self.aNovel.setIcon(SHARED.theme.getIcon("view_novel"))
self.aOutline.setIcon(CONFIG.theme.getIcon("view_outline")) self.aOutline.setIcon(SHARED.theme.getIcon("view_outline"))
self.aBuild.setIcon(CONFIG.theme.getIcon("view_build")) self.aBuild.setIcon(SHARED.theme.getIcon("view_build"))
self.aDetails.setIcon(CONFIG.theme.getIcon("proj_details")) self.aDetails.setIcon(SHARED.theme.getIcon("proj_details"))
self.aStats.setIcon(CONFIG.theme.getIcon("proj_stats")) self.aStats.setIcon(SHARED.theme.getIcon("proj_stats"))
self.tbSettings.setIcon(CONFIG.theme.getIcon("settings")) self.tbSettings.setIcon(SHARED.theme.getIcon("settings"))
return return
+68 -83
View File
@@ -27,34 +27,38 @@ from __future__ import annotations
import logging import logging
from time import time from time import time
from typing import TYPE_CHECKING, Literal
from PyQt5.QtCore import pyqtSlot, QLocale from PyQt5.QtCore import pyqtSlot, QLocale
from PyQt5.QtGui import QColor from PyQt5.QtGui import QColor
from PyQt5.QtWidgets import qApp, QStatusBar, QLabel from PyQt5.QtWidgets import qApp, QStatusBar, QLabel
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.common import formatTime from novelwriter.common import formatTime
from novelwriter.constants import nwConst
from novelwriter.extensions.statusled import StatusLED from novelwriter.extensions.statusled import StatusLED
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
class GuiMainStatus(QStatusBar): class GuiMainStatus(QStatusBar):
def __init__(self, mainGui): def __init__(self, mainGui: GuiMain) -> None:
super().__init__(parent=mainGui) super().__init__(parent=mainGui)
logger.debug("Create: GuiMainStatus") logger.debug("Create: GuiMainStatus")
self.mainGui = mainGui self._refTime = -1.0
self.refTime = None self._userIdle = False
self.userIdle = False
colNone = QColor(*CONFIG.theme.statNone) colNone = QColor(*SHARED.theme.statNone)
colSaved = QColor(*CONFIG.theme.statSaved) colSaved = QColor(*SHARED.theme.statSaved)
colUnsaved = QColor(*CONFIG.theme.statUnsaved) colUnsaved = QColor(*SHARED.theme.statUnsaved)
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
# Permanent Widgets # Permanent Widgets
# ================= # =================
@@ -98,7 +102,7 @@ class GuiMainStatus(QStatusBar):
self.timeIcon = QLabel() self.timeIcon = QLabel()
self.timeText = QLabel("") self.timeText = QLabel("")
self.timeText.setToolTip(self.tr("Session Time")) 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.timeIcon.setContentsMargins(0, 0, 0, 0)
self.timeText.setContentsMargins(0, 0, 0, 0) self.timeText.setContentsMargins(0, 0, 0, 0)
self.addPermanentWidget(self.timeIcon) self.addPermanentWidget(self.timeIcon)
@@ -114,80 +118,59 @@ class GuiMainStatus(QStatusBar):
return return
def clearStatus(self): def clearStatus(self) -> None:
"""Reset all widgets on the status bar to default values. """Reset all widgets on the status bar to default values."""
""" self.setRefTime(-1.0)
self.setRefTime(None)
self.setLanguage(None, "") self.setLanguage(None, "")
self.setProjectStats(0, 0) self.setProjectStats(0, 0)
self.setProjectStatus(StatusLED.S_NONE) self.setProjectStatus(StatusLED.S_NONE)
self.setDocumentStatus(StatusLED.S_NONE) self.setDocumentStatus(StatusLED.S_NONE)
self.updateTime() self.updateTime()
return True return
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))
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) self.timeIcon.setPixmap(self.timePixmap)
return return
## ##
# Setters # Setters
## ##
def setRefTime(self, theTime): def setRefTime(self, refTime: float) -> None:
"""Set the reference time for the status bar clock. """Set the reference time for the status bar clock."""
""" self._refTime = refTime
self.refTime = theTime
return return
def setStatus(self, theMessage, timeOut=20.0): def setProjectStatus(self, state: Literal[0, 1, 2]) -> None:
"""Set the status bar message to display for 'timeOut' seconds. """Set the project status colour icon."""
""" self.projIcon.setState(state)
self.showMessage(theMessage, int(timeOut*1000))
qApp.processEvents()
return return
def setProjectStatus(self, theState): def setDocumentStatus(self, state: Literal[0, 1, 2]) -> None:
"""Set the project status colour icon. """Set the document status colour icon."""
""" self.docIcon.setState(state)
self.projIcon.setState(theState)
return return
def setDocumentStatus(self, theState): def setUserIdle(self, idle: bool) -> None:
"""Set the document status colour icon. """Change the idle status icon."""
"""
self.docIcon.setState(theState)
return
def setUserIdle(self, userIdle):
"""Change the idle status icon.
"""
if not CONFIG.stopWhenIdle: if not CONFIG.stopWhenIdle:
userIdle = False idle = False
if self._userIdle != idle:
if self.userIdle != userIdle: if idle:
if userIdle:
self.timeIcon.setPixmap(self.idlePixmap) self.timeIcon.setPixmap(self.idlePixmap)
else: else:
self.timeIcon.setPixmap(self.timePixmap) self.timeIcon.setPixmap(self.timePixmap)
self._userIdle = idle
self.userIdle = userIdle
return return
def setProjectStats(self, pWC, sWC): def setProjectStats(self, pWC: int, sWC: int) -> None:
"""Update the current project statistics. """Update the current project statistics."""
"""
self.statsText.setText(self.tr("Words: {0} ({1})").format(f"{pWC:n}", f"{sWC:+n}")) self.statsText.setText(self.tr("Words: {0} ({1})").format(f"{pWC:n}", f"{sWC:+n}"))
if CONFIG.incNotesWCount: if CONFIG.incNotesWCount:
self.statsText.setToolTip(self.tr("Project word count (session change)")) 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)")) self.statsText.setToolTip(self.tr("Novel word count (session change)"))
return return
def updateTime(self, idleTime=0.0): def updateTime(self, idleTime: float = 0.0) -> None:
"""Update the session clock. """Update the session clock."""
""" if self._refTime < 0.0:
if self.refTime is None:
self.timeText.setText("00:00:00") self.timeText.setText("00:00:00")
else: else:
if CONFIG.stopWhenIdle: if CONFIG.stopWhenIdle:
sessTime = round(time() - self.refTime - idleTime) sessTime = round(time() - self._refTime - idleTime)
else: else:
sessTime = round(time() - self.refTime) sessTime = round(time() - self._refTime)
self.timeText.setText(formatTime(sessTime)) self.timeText.setText(formatTime(sessTime))
return 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) @pyqtSlot(str, str)
def setLanguage(self, theLanguage, theProvider): def setLanguage(self, language: str, provider: str) -> None:
"""Set the language code for the spell checker. """Set the language code for the spell checker."""
""" if language == "None":
if theLanguage == "None":
self.langText.setText(self.tr("None")) self.langText.setText(self.tr("None"))
self.langText.setToolTip("") self.langText.setToolTip("")
else: else:
qLocal = QLocale(theLanguage) qLocal = QLocale(language)
spLang = qLocal.nativeLanguageName().title() spLang = qLocal.nativeLanguageName().title()
self.langText.setText(spLang) self.langText.setText(spLang)
if theProvider: if provider:
self.langText.setToolTip("%s (%s)" % (theLanguage, theProvider)) self.langText.setToolTip("%s (%s)" % (language, provider))
else: else:
self.langText.setToolTip(theLanguage) self.langText.setToolTip(language)
return return
@pyqtSlot(bool) @pyqtSlot(bool)
def doUpdateProjectStatus(self, isChanged): def updateProjectStatus(self, status: bool) -> None:
"""Slot for updating the project status. """Update the project status."""
""" self.setProjectStatus(StatusLED.S_BAD if status else StatusLED.S_GOOD)
self.setProjectStatus(StatusLED.S_BAD if isChanged else StatusLED.S_GOOD)
return return
@pyqtSlot(bool) @pyqtSlot(bool)
def doUpdateDocumentStatus(self, isChanged): def updateDocumentStatus(self, status: bool) -> None:
"""Slot for updating the document status. """Update the document status."""
""" self.setDocumentStatus(StatusLED.S_BAD if status else StatusLED.S_GOOD)
self.setDocumentStatus(StatusLED.S_BAD if isChanged else StatusLED.S_GOOD)
return return
# END Class GuiMainStatus # END Class GuiMainStatus
+118 -219
View File
@@ -31,13 +31,13 @@ from pathlib import Path
from datetime import datetime from datetime import datetime
from PyQt5.QtCore import Qt, QTimer, QThreadPool, pyqtSlot 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 ( from PyQt5.QtWidgets import (
qApp, QDialog, QFileDialog, QMainWindow, QMessageBox, QShortcut, QSplitter, qApp, QDialog, QFileDialog, QMainWindow, QMessageBox, QShortcut, QSplitter,
QStackedWidget, QVBoxLayout, QWidget QStackedWidget, QVBoxLayout, QWidget
) )
from novelwriter import CONFIG, __hexversion__ from novelwriter import CONFIG, SHARED, __hexversion__
from novelwriter.gui.theme import GuiTheme from novelwriter.gui.theme import GuiTheme
from novelwriter.gui.sidebar import GuiSideBar from novelwriter.gui.sidebar import GuiSideBar
from novelwriter.gui.outline import GuiOutlineView 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.manuscript import GuiManuscript
from novelwriter.tools.projwizard import GuiProjectWizard from novelwriter.tools.projwizard import GuiProjectWizard
from novelwriter.tools.writingstats import GuiWritingStats from novelwriter.tools.writingstats import GuiWritingStats
from novelwriter.core.project import NWProject
from novelwriter.core.coretools import ProjectBuilder from novelwriter.core.coretools import ProjectBuilder
from novelwriter.enum import ( 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.common import getGuiItem, hexToInt
from novelwriter.constants import nwFiles, nwLabels, trConst from novelwriter.constants import nwFiles
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -112,15 +111,11 @@ class GuiMain(QMainWindow):
# Core Classes # Core Classes
# ============ # ============
# Core Classes # Initialise UserData Instance
CONFIG.setThemeInstance(GuiTheme()) SHARED.initSharedData(self, GuiTheme())
self._project = NWProject(self)
# Core Settings # Core Settings
self.hasProject = False
self.isFocusMode = False self.isFocusMode = False
self.idleRefTime = time()
self.idleTime = 0.0
# Prepare Main Window # Prepare Main Window
self.resize(*CONFIG.mainWinSize) self.resize(*CONFIG.mainWinSize)
@@ -135,7 +130,6 @@ class GuiMain(QMainWindow):
# ============= # =============
# Sizes # Sizes
iPx = CONFIG.theme.fontPixelSize
mPx = CONFIG.pxInt(4) mPx = CONFIG.pxInt(4)
hWd = CONFIG.pxInt(4) hWd = CONFIG.pxInt(4)
@@ -238,7 +232,8 @@ class GuiMain(QMainWindow):
# Connect Signals # 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) self.viewsBar.viewChangeRequested.connect(self._changeView)
@@ -257,12 +252,13 @@ class GuiMain(QMainWindow):
self.novelView.openDocumentRequest.connect(self._openDocument) self.novelView.openDocumentRequest.connect(self._openDocument)
self.docEditor.spellDictionaryChanged.connect(self.mainStatus.setLanguage) 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.itemDetails.updateCounts)
self.docEditor.docCountsChanged.connect(self.projView.updateCounts) self.docEditor.docCountsChanged.connect(self.projView.updateCounts)
self.docEditor.loadDocumentTagRequest.connect(self._followTag) self.docEditor.loadDocumentTagRequest.connect(self._followTag)
self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree) self.docEditor.novelStructureChanged.connect(self.novelView.refreshTree)
self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta) self.docEditor.novelItemMetaChanged.connect(self.novelView.updateNovelItemMeta)
self.docEditor.statusMessage.connect(self.mainStatus.setStatusMessage)
self.docViewer.loadDocumentTagRequest.connect(self._followTag) self.docViewer.loadDocumentTagRequest.connect(self._followTag)
@@ -301,18 +297,6 @@ class GuiMain(QMainWindow):
keyEscape.setKey(QKeySequence(Qt.Key_Escape)) keyEscape.setKey(QKeySequence(Qt.Key_Escape))
keyEscape.activated.connect(self._keyPressEscape) 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 # Check that config loaded fine
self.reportConfErr() self.reportConfErr()
@@ -328,33 +312,14 @@ class GuiMain(QMainWindow):
logger.debug("Ready: GUI") logger.debug("Ready: GUI")
if __hexversion__[-2] == "a" and logger.getEffectiveLevel() > logging.DEBUG: 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. " "You are running an untested development version of novelWriter. "
"Please be careful when working on a live project " "Please be careful when working on a live project "
"and make sure you take regular backups." "and make sure you take regular backups."
), level=nwAlert.WARN) ))
logger.info("novelWriter is ready ...") logger.info("novelWriter is ready ...")
self.setStatus(self.tr("novelWriter is ready ...")) self.mainStatus.setStatusMessage(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()
return return
@@ -365,14 +330,12 @@ class GuiMain(QMainWindow):
return return
def postLaunchTasks(self, cmdOpen: str | None) -> None: def postLaunchTasks(self, cmdOpen: str | None) -> None:
"""This function is called after the main window is created to """Process tasks after the main window has been created."""
determine what to open or show after initialisation.
"""
if cmdOpen: if cmdOpen:
logger.info("Command line path: %s", cmdOpen) logger.info("Command line path: %s", cmdOpen)
self.openProject(cmdOpen) self.openProject(cmdOpen)
if not self.hasProject: if not SHARED.hasProject:
self.showProjectLoadDialog() self.showProjectLoadDialog()
# Determine whether release notes need to be shown or not # Determine whether release notes need to be shown or not
@@ -382,26 +345,17 @@ class GuiMain(QMainWindow):
return return
##
# Properties
##
@property
def project(self) -> NWProject:
"""The project instance."""
return self._project
## ##
# Project Actions # Project Actions
## ##
def newProject(self, projData: dict | None = None) -> bool: def newProject(self, projData: dict | None = None) -> bool:
"""Create a new project via the new project wizard.""" """Create a new project via the new project wizard."""
if self.hasProject: if SHARED.hasProject:
if not self.closeProject(): if not self.closeProject():
self.makeAlert(self.tr( SHARED.error(self.tr(
"Cannot create a new project when another project is open." "Cannot create a new project when another project is open."
), level=nwAlert.ERROR) ))
return False return False
if projData is None: if projData is None:
@@ -416,14 +370,14 @@ class GuiMain(QMainWindow):
return False return False
if (Path(projPath) / nwFiles.PROJ_FILE).is_file(): if (Path(projPath) / nwFiles.PROJ_FILE).is_file():
self.makeAlert(self.tr( SHARED.error(self.tr(
"A project already exists in that location. " "A project already exists in that location. "
"Please choose another folder." "Please choose another folder."
), level=nwAlert.ERROR) ))
return False return False
logger.info("Creating new project") logger.info("Creating new project")
nwProject = ProjectBuilder(self) nwProject = ProjectBuilder()
if nwProject.buildProject(projData): if nwProject.buildProject(projData):
self.openProject(projPath) self.openProject(projPath)
else: else:
@@ -436,45 +390,46 @@ class GuiMain(QMainWindow):
close application event so the user doesn't get prompted twice close application event so the user doesn't get prompted twice
to confirm. to confirm.
""" """
if not self.hasProject: if not SHARED.hasProject:
# There is no project loaded, everything OK # There is no project loaded, everything OK
return True return True
if not isYes: if not isYes:
msgYes = self.askQuestion("%s<br>%s" % ( msgYes = SHARED.question("%s<br>%s" % (
self.tr("Close the current project?"), self.tr("Close the current project?"),
self.tr("Changes are saved automatically.") self.tr("Changes are saved automatically.")
)) ))
if not msgYes: if not msgYes:
return False return False
if self.docEditor.docChanged(): if self.docEditor.docChanged:
self.saveDocument() self.saveDocument()
saveOK = self.saveProject() saveOK = self.saveProject()
doBackup = False doBackup = False
if self._project.data.doBackup and CONFIG.backupOnClose: if SHARED.project.data.doBackup and CONFIG.backupOnClose:
doBackup = True doBackup = True
if CONFIG.askBeforeBackup: if CONFIG.askBeforeBackup:
msgYes = self.askQuestion(self.tr("Backup the current project?")) doBackup = SHARED.question(self.tr("Backup the current project?"))
if not msgYes:
doBackup = False
if doBackup: if doBackup:
self._project.backupProject(False) SHARED.project.backupProject(False)
if saveOK: if saveOK:
self.closeDocument() self.closeDocument()
self.docViewer.clearNavHistory() self.docViewer.clearNavHistory()
self.closeDocViewer(byUser=False)
self.outlineView.closeProjectTasks() self.outlineView.closeProjectTasks()
self.novelView.closeProjectTasks() self.novelView.closeProjectTasks()
self.projView.clearProjectView()
self.itemDetails.clearDetails()
self.mainStatus.clearStatus()
self._project.closeProject(self.idleTime) SHARED.closeProject()
self.idleRefTime = time()
self.idleTime = 0.0
self.clearGUI() self.docEditor.setDictionaries()
self.hasProject = False self._updateWindowTitle()
self._changeView(nwView.PROJECT) self._changeView(nwView.PROJECT)
return saveOK return saveOK
@@ -493,9 +448,10 @@ class GuiMain(QMainWindow):
self._changeView(nwView.PROJECT) self._changeView(nwView.PROJECT)
# Try to open the project # Try to open the project
if not self._project.openProject(projFile): tStart = time()
if not SHARED.openProject(projFile):
# The project open failed. # The project open failed.
lockStatus = self._project.getLockStatus() lockStatus = SHARED.projectLock
if lockStatus is None: if lockStatus is None:
# The project is not locked, so failed for some other # The project is not locked, so failed for some other
# reason handled by the project class. # reason handled by the project class.
@@ -524,23 +480,18 @@ class GuiMain(QMainWindow):
except Exception: except Exception:
lockDetails = "" lockDetails = ""
if self.askQuestion(lockText, info=lockInfo, details=lockDetails, level=nwAlert.WARN): if SHARED.question(lockText, info=lockInfo, details=lockDetails, warn=True):
if not self._project.openProject(projFile, overrideLock=True): if not SHARED.openProject(projFile, clearLock=True):
return False return False
else: else:
return False return False
# Project is loaded
self.hasProject = True
self.idleRefTime = time()
self.idleTime = 0.0
# Update GUI # Update GUI
self._updateWindowTitle(self._project.data.name) self._updateWindowTitle(SHARED.project.data.name)
self.rebuildTrees() self.rebuildTrees()
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.docEditor.toggleSpellCheck(self._project.data.spellCheck) self.docEditor.toggleSpellCheck(SHARED.project.data.spellCheck)
self.mainStatus.setRefTime(self._project.projOpened) self.mainStatus.setRefTime(SHARED.project.projOpened)
self.projView.openProjectTasks() self.projView.openProjectTasks()
self.novelView.openProjectTasks() self.novelView.openProjectTasks()
self.outlineView.openProjectTasks() self.outlineView.openProjectTasks()
@@ -548,9 +499,9 @@ class GuiMain(QMainWindow):
# Restore previously open documents, if any # Restore previously open documents, if any
# If none was recorded, open the first document found # 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: if lastEdited is None:
for nwItem in self._project.tree: for nwItem in SHARED.project.tree:
if nwItem and nwItem.isFileType(): if nwItem and nwItem.isFileType():
lastEdited = nwItem.itemHandle lastEdited = nwItem.itemHandle
break break
@@ -558,32 +509,31 @@ class GuiMain(QMainWindow):
if lastEdited is not None: if lastEdited is not None:
self.openDocument(lastEdited, doScroll=True) self.openDocument(lastEdited, doScroll=True)
lastViewed = self._project.data.getLastHandle("viewer") lastViewed = SHARED.project.data.getLastHandle("viewer")
if lastViewed is not None: if lastViewed is not None:
self.viewDocument(lastViewed) self.viewDocument(lastViewed)
# Check if we need to rebuild the index # Check if we need to rebuild the index
if self._project.index.indexBroken: if SHARED.project.index.indexBroken:
self.makeAlert(self.tr("The project index is outdated or broken. Rebuilding index.")) SHARED.info(self.tr("The project index is outdated or broken. Rebuilding index."))
self.rebuildIndex() self.rebuildIndex()
# Make sure the changed status is set to false on things opened # Make sure the changed status is set to false on things opened
qApp.processEvents() qApp.processEvents()
self.docEditor.setDocumentChanged(False) 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 return True
def saveProject(self, autoSave: bool = False) -> bool: def saveProject(self, autoSave: bool = False) -> bool:
"""Save the current project.""" """Save the current project."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
self.projView.saveProjectTasks() self.projView.saveProjectTasks()
self._project.saveProject(autoSave=autoSave) return SHARED.saveProject(autoSave=autoSave)
return True
## ##
# Document Actions # Document Actions
@@ -591,7 +541,7 @@ class GuiMain(QMainWindow):
def closeDocument(self, beforeOpen: bool = False) -> bool: def closeDocument(self, beforeOpen: bool = False) -> bool:
"""Close the document and clear the editor and title field.""" """Close the document and clear the editor and title field."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
@@ -600,7 +550,7 @@ class GuiMain(QMainWindow):
self.toggleFocusMode() self.toggleFocusMode()
self.docEditor.saveCursorPosition() self.docEditor.saveCursorPosition()
if self.docEditor.docChanged(): if self.docEditor.docChanged:
self.saveDocument() self.saveDocument()
self.docEditor.clearEditor() self.docEditor.clearEditor()
if not beforeOpen: if not beforeOpen:
@@ -611,16 +561,16 @@ class GuiMain(QMainWindow):
def openDocument(self, tHandle: str | None, tLine: int | None = None, def openDocument(self, tHandle: str | None, tLine: int | None = None,
changeFocus: bool = True, doScroll: bool = False) -> bool: changeFocus: bool = True, doScroll: bool = False) -> bool:
"""Open a specific document, optionally at a given line.""" """Open a specific document, optionally at a given line."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False 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) logger.debug("Requested item '%s' is not a document", tHandle)
return False return False
self._changeView(nwView.EDITOR) self._changeView(nwView.EDITOR)
cHandle = self.docEditor.docHandle() cHandle = self.docEditor.docHandle
if cHandle == tHandle: if cHandle == tHandle:
self.docEditor.setCursorLine(tLine) self.docEditor.setCursorLine(tLine)
if changeFocus: if changeFocus:
@@ -629,7 +579,7 @@ class GuiMain(QMainWindow):
self.closeDocument(beforeOpen=True) self.closeDocument(beforeOpen=True)
if self.docEditor.loadText(tHandle, tLine): 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.projView.setSelectedHandle(tHandle, doScroll=doScroll)
self.novelView.setActiveHandle(tHandle) self.novelView.setActiveHandle(tHandle)
if changeFocus: if changeFocus:
@@ -643,14 +593,14 @@ class GuiMain(QMainWindow):
"""Opens the next document in the project tree, following the """Opens the next document in the project tree, following the
document with the given handle. Stops when reaching the end. document with the given handle. Stops when reaching the end.
""" """
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
nHandle = None # The next handle after tHandle nHandle = None # The next handle after tHandle
fHandle = None # The first file handle we encounter fHandle = None # The first file handle we encounter
foundIt = False # We've found tHandle, pick the next we see 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(): if not tItem.isFileType():
continue continue
if fHandle is None: if fHandle is None:
@@ -672,7 +622,7 @@ class GuiMain(QMainWindow):
def saveDocument(self) -> bool: def saveDocument(self) -> bool:
"""Save the current documents.""" """Save the current documents."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
self.docEditor.saveText() self.docEditor.saveText()
@@ -680,7 +630,7 @@ class GuiMain(QMainWindow):
def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool: def viewDocument(self, tHandle: str | None = None, sTitle: str | None = None) -> bool:
"""Load a document for viewing in the view panel.""" """Load a document for viewing in the view panel."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
@@ -688,7 +638,7 @@ class GuiMain(QMainWindow):
logger.debug("Viewing document, but no handle provided") logger.debug("Viewing document, but no handle provided")
if self.docEditor.hasFocus(): if self.docEditor.hasFocus():
tHandle = self.docEditor.docHandle() tHandle = self.docEditor.docHandle
if tHandle is not None: if tHandle is not None:
self.saveDocument() self.saveDocument()
@@ -696,7 +646,7 @@ class GuiMain(QMainWindow):
tHandle = self.projView.getSelectedHandle() tHandle = self.projView.getSelectedHandle()
if tHandle is None: if tHandle is None:
tHandle = self._project.data.getLastHandle("viewer") tHandle = SHARED.project.data.getLastHandle("viewer")
if tHandle is None: if tHandle is None:
logger.debug("No document to view, giving up") 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 """Import the text contained in an out-of-project text file, and
insert the text into the currently open document. insert the text into the currently open document.
""" """
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
@@ -751,19 +701,19 @@ class GuiMain(QMainWindow):
theText = inFile.read() theText = inFile.read()
CONFIG.setLastPath(loadFile) CONFIG.setLastPath(loadFile)
except Exception as exc: except Exception as exc:
self.makeAlert(self.tr( SHARED.error(self.tr(
"Could not read file. The file must be an existing text file." "Could not read file. The file must be an existing text file."
), level=nwAlert.ERROR, exception=exc) ), exc=exc)
return False return False
if self.docEditor.docHandle() is None: if self.docEditor.docHandle is None:
self.makeAlert(self.tr( SHARED.error(self.tr(
"Please open a document to import the text file into." "Please open a document to import the text file into."
), level=nwAlert.ERROR) ))
return False return False
if not self.docEditor.isEmpty(): if not self.docEditor.isEmpty:
msgYes = self.askQuestion(self.tr( msgYes = SHARED.question(self.tr(
"Importing the file will overwrite the current content of " "Importing the file will overwrite the current content of "
"the document. Do you want to proceed?" "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. active. It is not checked that the item is actually a document.
That should be handled by the openDocument function. That should be handled by the openDocument function.
""" """
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
@@ -815,7 +765,7 @@ class GuiMain(QMainWindow):
return False return False
if tHandle is not None and sTitle is not None: 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: if hItem is not None:
tLine = hItem.line tLine = hItem.line
@@ -826,12 +776,12 @@ class GuiMain(QMainWindow):
def editItemLabel(self, tHandle: str | None = None) -> bool: def editItemLabel(self, tHandle: str | None = None) -> bool:
"""Open the edit item dialog.""" """Open the edit item dialog."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
if tHandle is None and (self.docEditor.anyFocus() or self.isFocusMode): if tHandle is None and (self.docEditor.anyFocus() or self.isFocusMode):
tHandle = self.docEditor.docHandle() tHandle = self.docEditor.docHandle
self.projView.renameTreeItem(tHandle) self.projView.renameTreeItem(tHandle)
return True return True
@@ -843,7 +793,7 @@ class GuiMain(QMainWindow):
def rebuildIndex(self, beQuiet: bool = False) -> bool: def rebuildIndex(self, beQuiet: bool = False) -> bool:
"""Rebuild the entire index.""" """Rebuild the entire index."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
@@ -852,12 +802,12 @@ class GuiMain(QMainWindow):
tStart = time() tStart = time()
self.projView.saveProjectTasks() self.projView.saveProjectTasks()
self._project.index.rebuildIndex() SHARED.project.index.rebuildIndex()
self.projView.populateTree() self.projView.populateTree()
self.novelView.refreshTree() self.novelView.refreshTree()
tEnd = time() tEnd = time()
self.setStatus( self.mainStatus.setStatusMessage(
self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}") self.tr("Indexing completed in {0} ms").format(f"{(tEnd - tStart)*1000.0:.1f}")
) )
self.docEditor.updateTagHighLighting() self.docEditor.updateTagHighLighting()
@@ -865,7 +815,7 @@ class GuiMain(QMainWindow):
qApp.restoreOverrideCursor() qApp.restoreOverrideCursor()
if not beQuiet: 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 return True
@@ -911,7 +861,7 @@ class GuiMain(QMainWindow):
self.saveDocument() self.saveDocument()
if dlgConf.needsRestart: if dlgConf.needsRestart:
self.makeAlert(self.tr( SHARED.info(self.tr(
"Some changes will not be applied until novelWriter has been restarted." "Some changes will not be applied until novelWriter has been restarted."
)) ))
@@ -921,7 +871,7 @@ class GuiMain(QMainWindow):
if dlgConf.updateTheme: if dlgConf.updateTheme:
# We are doing this manually instead of connecting to # We are doing this manually instead of connecting to
# qApp.paletteChanged since the processing order matters # qApp.paletteChanged since the processing order matters
CONFIG.theme.loadTheme() SHARED.theme.loadTheme()
self.docEditor.updateTheme() self.docEditor.updateTheme()
self.docViewer.updateTheme() self.docViewer.updateTheme()
self.viewsBar.updateTheme() self.viewsBar.updateTheme()
@@ -932,7 +882,7 @@ class GuiMain(QMainWindow):
self.mainStatus.updateTheme() self.mainStatus.updateTheme()
if dlgConf.updateSyntax: if dlgConf.updateSyntax:
CONFIG.theme.loadSyntax() SHARED.theme.loadSyntax()
self.docEditor.updateSyntaxColours() self.docEditor.updateSyntaxColours()
self.docEditor.initEditor() self.docEditor.initEditor()
@@ -948,7 +898,7 @@ class GuiMain(QMainWindow):
@pyqtSlot(int) @pyqtSlot(int)
def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.TAB_MAIN) -> bool: def showProjectSettingsDialog(self, focusTab: int = GuiProjectSettings.TAB_MAIN) -> bool:
"""Open the project settings dialog.""" """Open the project settings dialog."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
@@ -960,13 +910,13 @@ class GuiMain(QMainWindow):
if dlgProj.spellChanged: if dlgProj.spellChanged:
self.docEditor.setDictionaries() self.docEditor.setDictionaries()
self.itemDetails.refreshDetails() self.itemDetails.refreshDetails()
self._updateWindowTitle(self._project.data.name) self._updateWindowTitle(SHARED.project.data.name)
return True return True
def showProjectDetailsDialog(self) -> bool: def showProjectDetailsDialog(self) -> bool:
"""Open the project details dialog.""" """Open the project details dialog."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
@@ -985,7 +935,7 @@ class GuiMain(QMainWindow):
@pyqtSlot() @pyqtSlot()
def showBuildManuscriptDialog(self) -> bool: def showBuildManuscriptDialog(self) -> bool:
"""Open the build manuscript dialog.""" """Open the build manuscript dialog."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
@@ -1005,7 +955,7 @@ class GuiMain(QMainWindow):
def showLoremIpsumDialog(self) -> bool: def showLoremIpsumDialog(self) -> bool:
"""Open the insert lorem ipsum text dialog.""" """Open the insert lorem ipsum text dialog."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
@@ -1023,7 +973,7 @@ class GuiMain(QMainWindow):
def showProjectWordListDialog(self) -> bool: def showProjectWordListDialog(self) -> bool:
"""Open the project word list dialog.""" """Open the project word list dialog."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
@@ -1038,7 +988,7 @@ class GuiMain(QMainWindow):
def showWritingStatsDialog(self) -> bool: def showWritingStatsDialog(self) -> bool:
"""Open the session stats dialog.""" """Open the session stats dialog."""
if not self.hasProject: if not SHARED.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
@@ -1094,56 +1044,13 @@ class GuiMain(QMainWindow):
return 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}<br>{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: def reportConfErr(self) -> bool:
"""Checks if the Config module has any errors to report, and let """Checks if the Config module has any errors to report, and let
the user know if this is the case. The Config module caches the user know if this is the case. The Config module caches
errors since it is initialised before the GUI itself. errors since it is initialised before the GUI itself.
""" """
if CONFIG.hasError: if CONFIG.hasError:
self.makeAlert(CONFIG.errorText(), level=nwAlert.ERROR) SHARED.error(CONFIG.errorText())
return True return True
return False return False
@@ -1153,8 +1060,8 @@ class GuiMain(QMainWindow):
def closeMain(self) -> bool: def closeMain(self) -> bool:
"""Save everything, and close novelWriter.""" """Save everything, and close novelWriter."""
if self.hasProject: if SHARED.hasProject:
msgYes = self.askQuestion("%s<br>%s" % ( msgYes = SHARED.question("%s<br>%s" % (
self.tr("Do you want to exit novelWriter?"), self.tr("Do you want to exit novelWriter?"),
self.tr("Changes are saved automatically.") self.tr("Changes are saved automatically.")
)) ))
@@ -1174,7 +1081,7 @@ class GuiMain(QMainWindow):
# Ignore window size if in full screen mode # Ignore window size if in full screen mode
CONFIG.setMainWinSize(self.width(), self.height()) CONFIG.setMainWinSize(self.width(), self.height())
if self.hasProject: if SHARED.hasProject:
self.closeProject(True) self.closeProject(True)
CONFIG.saveConfig() CONFIG.saveConfig()
@@ -1206,7 +1113,7 @@ class GuiMain(QMainWindow):
def closeDocEditor(self) -> None: def closeDocEditor(self) -> None:
"""Close the document editor. This does not hide the editor.""" """Close the document editor. This does not hide the editor."""
self.closeDocument() self.closeDocument()
self._project.data.setLastHandle(None, "editor") SHARED.project.data.setLastHandle(None, "editor")
return return
def closeDocViewer(self, byUser: bool = True) -> bool: def closeDocViewer(self, byUser: bool = True) -> bool:
@@ -1214,7 +1121,7 @@ class GuiMain(QMainWindow):
self.docViewer.clearViewer() self.docViewer.clearViewer()
if byUser: if byUser:
# Only reset the last handle if the user called this # 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 # Hide the panel
bPos = self.splitMain.sizes() bPos = self.splitMain.sizes()
@@ -1227,7 +1134,7 @@ class GuiMain(QMainWindow):
"""Handle toggle focus mode. The Main GUI Focus Mode hides tree, """Handle toggle focus mode. The Main GUI Focus Mode hides tree,
view, statusbar and menu. 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") logger.error("No document open, so not activating Focus Mode")
return False return False
@@ -1250,7 +1157,7 @@ class GuiMain(QMainWindow):
if self.splitView.isVisible(): if self.splitView.isVisible():
self.splitView.setVisible(False) self.splitView.setVisible(False)
elif self.docViewer.docHandle() is not None: elif self.docViewer.docHandle is not None:
self.splitView.setVisible(True) self.splitView.setVisible(True)
return True return True
@@ -1400,15 +1307,15 @@ class GuiMain(QMainWindow):
"""Handle the index lookup of a tag and display an alert if the """Handle the index lookup of a tag and display an alert if the
tag cannot be found. tag cannot be found.
""" """
tHandle, sTitle = self._project.index.getTagSource(tag) tHandle, sTitle = SHARED.project.index.getTagSource(tag)
if tHandle is None: if tHandle is None:
self.makeAlert(self.tr( SHARED.error(self.tr(
"Could not find the reference for tag '{0}'. It either doesn't " "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 " "exist, or the index is out of date. The index can be updated "
"from the Tools menu, or by pressing {1}." "from the Tools menu, or by pressing {1}."
).format( ).format(
tag, "F9" tag, "F9"
), level=nwAlert.ERROR) ))
return None, None return None, None
return tHandle, sTitle return tHandle, sTitle
@@ -1447,7 +1354,7 @@ class GuiMain(QMainWindow):
if tHandle is not None: if tHandle is not None:
if mode == nwDocMode.EDIT: if mode == nwDocMode.EDIT:
tLine = None tLine = None
hItem = self._project.index.getItemHeader(tHandle, sTitle) hItem = SHARED.project.index.getItemHeader(tHandle, sTitle)
if hItem is not None: if hItem is not None:
tLine = hItem.line tLine = hItem.line
self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus) self.openDocument(tHandle, tLine=tLine, changeFocus=setFocus)
@@ -1478,30 +1385,22 @@ class GuiMain(QMainWindow):
@pyqtSlot() @pyqtSlot()
def _timeTick(self) -> None: def _timeTick(self) -> None:
"""Process time tick of the main timer.""" """Process time tick of the main timer."""
if not self.hasProject: if not SHARED.hasProject:
return return
currTime = time() currTime = time()
editIdle = currTime - self.docEditor.lastActive() > CONFIG.userIdleTime editIdle = currTime - self.docEditor.lastActive > CONFIG.userIdleTime
userIdle = qApp.applicationState() != Qt.ApplicationActive userIdle = qApp.applicationState() != Qt.ApplicationActive
self.mainStatus.setUserIdle(editIdle or userIdle)
if editIdle or userIdle: SHARED.updateIdleTime(currTime, editIdle or userIdle)
self.idleTime += currTime - self.idleRefTime self.mainStatus.updateTime(idleTime=SHARED.projectIdleTime)
self.mainStatus.setUserIdle(True)
else:
self.mainStatus.setUserIdle(False)
self.idleRefTime = currTime
self.mainStatus.updateTime(idleTime=self.idleTime)
return return
@pyqtSlot() @pyqtSlot()
def _autoSaveProject(self) -> None: def _autoSaveProject(self) -> None:
"""Autosave of the project. This is a timer-activated slot.""" """Autosave of the project. This is a timer-activated slot."""
doSave = self.hasProject doSave = SHARED.hasProject
doSave &= self._project.projChanged doSave &= SHARED.project.projChanged
doSave &= self._project.storage.isOpen() doSave &= SHARED.project.storage.isOpen()
if doSave: if doSave:
logger.debug("Autosaving project") logger.debug("Autosaving project")
self.saveProject(autoSave=True) self.saveProject(autoSave=True)
@@ -1510,7 +1409,7 @@ class GuiMain(QMainWindow):
@pyqtSlot() @pyqtSlot()
def _autoSaveDocument(self) -> None: def _autoSaveDocument(self) -> None:
"""Autosave of the document. This is a timer-activated slot.""" """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") logger.debug("Autosaving document")
self.saveDocument() self.saveDocument()
return return
@@ -1518,17 +1417,17 @@ class GuiMain(QMainWindow):
@pyqtSlot() @pyqtSlot()
def _updateStatusWordCount(self) -> None: def _updateStatusWordCount(self) -> None:
"""Update the word count on the status bar.""" """Update the word count on the status bar."""
if not self.hasProject: if not SHARED.hasProject:
self.mainStatus.setProjectStats(0, 0) self.mainStatus.setProjectStats(0, 0)
self._project.updateWordCounts() SHARED.project.updateWordCounts()
if CONFIG.incNotesWCount: if CONFIG.incNotesWCount:
iTotal = sum(self._project.data.initCounts) iTotal = sum(SHARED.project.data.initCounts)
cTotal = sum(self._project.data.currCounts) cTotal = sum(SHARED.project.data.currCounts)
self.mainStatus.setProjectStats(cTotal, cTotal - iTotal) self.mainStatus.setProjectStats(cTotal, cTotal - iTotal)
else: else:
iNovel, _ = self._project.data.initCounts iNovel, _ = SHARED.project.data.initCounts
cNovel, _ = self._project.data.currCounts cNovel, _ = SHARED.project.data.currCounts
self.mainStatus.setProjectStats(cNovel, cNovel - iNovel) self.mainStatus.setProjectStats(cNovel, cNovel - iNovel)
return return
@@ -1554,7 +1453,7 @@ class GuiMain(QMainWindow):
def _mainStackChanged(self, index: int) -> None: def _mainStackChanged(self, index: int) -> None:
"""Process main window tab change.""" """Process main window tab change."""
if index == self.idxOutlineView: if index == self.idxOutlineView:
if self.hasProject: if SHARED.hasProject:
self.outlineView.refreshTree() self.outlineView.refreshTree()
return return
+300
View File
@@ -0,0 +1,300 @@
"""
novelWriter Shared Data Class
===============================
File History:
Created: 2023-08-10 [2.1b2]
This file is a part of novelWriter
Copyright 20182023, 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 <https://www.gnu.org/licenses/>.
"""
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"<b>{type(exception).__name__}</b>: {str(exception)}"
self.setInformativeText(f"{info}<br>{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
+2 -2
View File
@@ -32,7 +32,7 @@ from PyQt5.QtWidgets import (
QSpinBox QSpinBox
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.common import readTextFile from novelwriter.common import readTextFile
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
@@ -60,7 +60,7 @@ class GuiLipsum(QDialog):
nPx = CONFIG.pxInt(64) nPx = CONFIG.pxInt(64)
vSp = CONFIG.pxInt(4) vSp = CONFIG.pxInt(4)
self.docIcon = QLabel() 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 = QVBoxLayout()
self.leftBox.setSpacing(vSp) self.leftBox.setSpacing(vSp)
+17 -23
View File
@@ -25,7 +25,6 @@ from __future__ import annotations
import logging import logging
from typing import TYPE_CHECKING
from pathlib import Path from pathlib import Path
from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
@@ -35,8 +34,8 @@ from PyQt5.QtWidgets import (
QPushButton, QSplitter, QVBoxLayout, QWidget QPushButton, QSplitter, QVBoxLayout, QWidget
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwAlert, nwBuildFmt from novelwriter.enum import nwBuildFmt
from novelwriter.common import makeFileNameSafe from novelwriter.common import makeFileNameSafe
from novelwriter.constants import nwLabels from novelwriter.constants import nwLabels
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
@@ -44,9 +43,6 @@ from novelwriter.core.docbuild import NWBuildDocument
from novelwriter.core.buildsettings import BuildSettings from novelwriter.core.buildsettings import BuildSettings
from novelwriter.extensions.simpleprogress import NProgressSimple from novelwriter.extensions.simpleprogress import NProgressSimple
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -59,14 +55,12 @@ class GuiManuscriptBuild(QDialog):
D_KEY = Qt.ItemDataRole.UserRole 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) super().__init__(parent=parent)
logger.debug("Create: GuiManuscriptBuild") logger.debug("Create: GuiManuscriptBuild")
self.setObjectName("GuiManuscriptBuild") self.setObjectName("GuiManuscriptBuild")
self.mainGui = mainGui
self._parent = parent self._parent = parent
self._build = build self._build = build
@@ -74,14 +68,14 @@ class GuiManuscriptBuild(QDialog):
self.setMinimumWidth(CONFIG.pxInt(500)) self.setMinimumWidth(CONFIG.pxInt(500))
self.setMinimumHeight(CONFIG.pxInt(300)) self.setMinimumHeight(CONFIG.pxInt(300))
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
sp4 = CONFIG.pxInt(4) sp4 = CONFIG.pxInt(4)
sp8 = CONFIG.pxInt(8) sp8 = CONFIG.pxInt(8)
sp16 = CONFIG.pxInt(16) sp16 = CONFIG.pxInt(16)
wWin = CONFIG.pxInt(620) wWin = CONFIG.pxInt(620)
hWin = CONFIG.pxInt(360) hWin = CONFIG.pxInt(360)
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
self.resize( self.resize(
CONFIG.pxInt(pOptions.getInt("GuiManuscriptBuild", "winWidth", wWin)), CONFIG.pxInt(pOptions.getInt("GuiManuscriptBuild", "winWidth", wWin)),
CONFIG.pxInt(pOptions.getInt("GuiManuscriptBuild", "winHeight", hWin)) CONFIG.pxInt(pOptions.getInt("GuiManuscriptBuild", "winHeight", hWin))
@@ -146,7 +140,7 @@ class GuiManuscriptBuild(QDialog):
# Build Path # Build Path
self.lblPath = QLabel(self.tr("Path")) self.lblPath = QLabel(self.tr("Path"))
self.buildPath = QLineEdit(self) self.buildPath = QLineEdit(self)
self.btnBrowse = QPushButton(CONFIG.theme.getIcon("browse"), "") self.btnBrowse = QPushButton(SHARED.theme.getIcon("browse"), "")
self.pathBox = QHBoxLayout() self.pathBox = QHBoxLayout()
self.pathBox.addWidget(self.buildPath) self.pathBox.addWidget(self.buildPath)
@@ -156,7 +150,7 @@ class GuiManuscriptBuild(QDialog):
# Build Name # Build Name
self.lblName = QLabel(self.tr("File Name")) self.lblName = QLabel(self.tr("File Name"))
self.buildName = QLineEdit(self) 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.btnReset.setToolTip(self.tr("Reset file name to default"))
self.nameBox = QHBoxLayout() self.nameBox = QHBoxLayout()
@@ -181,7 +175,7 @@ class GuiManuscriptBuild(QDialog):
self.buildBox.setVerticalSpacing(sp4) self.buildBox.setVerticalSpacing(sp4)
# Dialog Buttons # 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 = QDialogButtonBox(QDialogButtonBox.Close)
self.dlgButtons.addButton(self.btnBuild, QDialogButtonBox.ActionRole) self.dlgButtons.addButton(self.btnBuild, QDialogButtonBox.ActionRole)
@@ -279,7 +273,7 @@ class GuiManuscriptBuild(QDialog):
@pyqtSlot() @pyqtSlot()
def _doResetBuildName(self): def _doResetBuildName(self):
"""Generate a default build name.""" """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.buildName.setText(bName)
self._build.setLastBuildName(bName) self._build.setLastBuildName(bName)
return return
@@ -308,19 +302,19 @@ class GuiManuscriptBuild(QDialog):
self.buildProgress.setValue(0) self.buildProgress.setValue(0)
bPath = Path(self.buildPath.text()) bPath = Path(self.buildPath.text())
if not bPath.is_dir(): 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 return False
bExt = nwLabels.BUILD_EXT[bFormat] bExt = nwLabels.BUILD_EXT[bFormat]
buildPath = (bPath / makeFileNameSafe(bName)).with_suffix(bExt) buildPath = (bPath / makeFileNameSafe(bName)).with_suffix(bExt)
if buildPath.exists(): if buildPath.exists():
if not self.mainGui.askQuestion( if not SHARED.question(
self.tr("The file already exists. Do you want to overwrite it?") self.tr("The file already exists. Do you want to overwrite it?")
): ):
return False return False
docBuild = NWBuildDocument(self.mainGui.project, self._build) docBuild = NWBuildDocument(SHARED.project, self._build)
docBuild.queueAll() docBuild.queueAll()
self.buildProgress.setMaximum(len(docBuild)) self.buildProgress.setMaximum(len(docBuild))
@@ -353,7 +347,7 @@ class GuiManuscriptBuild(QDialog):
fmtWidth = CONFIG.rpxInt(mainSplit[0]) fmtWidth = CONFIG.rpxInt(mainSplit[0])
sumWidth = CONFIG.rpxInt(mainSplit[1]) sumWidth = CONFIG.rpxInt(mainSplit[1])
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
pOptions.setValue("GuiManuscriptBuild", "winWidth", winWidth) pOptions.setValue("GuiManuscriptBuild", "winWidth", winWidth)
pOptions.setValue("GuiManuscriptBuild", "winHeight", winHeight) pOptions.setValue("GuiManuscriptBuild", "winHeight", winHeight)
pOptions.setValue("GuiManuscriptBuild", "fmtWidth", fmtWidth) pOptions.setValue("GuiManuscriptBuild", "fmtWidth", fmtWidth)
@@ -365,9 +359,9 @@ class GuiManuscriptBuild(QDialog):
def _populateContentList(self): def _populateContentList(self):
"""Build the content list.""" """Build the content list."""
rootMap = {} rootMap = {}
filtered = self._build.buildItemFilter(self.mainGui.project) filtered = self._build.buildItemFilter(SHARED.project)
self.listContent.clear() self.listContent.clear()
for nwItem in self.mainGui.project.tree: for nwItem in SHARED.project.tree:
tHandle = nwItem.itemHandle tHandle = nwItem.itemHandle
rHandle = nwItem.itemRoot rHandle = nwItem.itemRoot
@@ -376,11 +370,11 @@ class GuiManuscriptBuild(QDialog):
if filtered.get(tHandle, (False, 0))[0]: if filtered.get(tHandle, (False, 0))[0]:
if rHandle not in rootMap: if rHandle not in rootMap:
rItem = self.mainGui.project.tree[rHandle] rItem = SHARED.project.tree[rHandle]
if isinstance(rItem, NWItem): if isinstance(rItem, NWItem):
rootMap[rHandle] = rItem.itemName rootMap[rHandle] = rItem.itemName
itemIcon = CONFIG.theme.getItemIcon( itemIcon = SHARED.theme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemType, nwItem.itemClass,
nwItem.itemLayout, nwItem.mainHeading nwItem.itemLayout, nwItem.mainHeading
) )
+20 -22
View File
@@ -38,7 +38,7 @@ from PyQt5.QtWidgets import (
) )
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkInt, fuzzyTime from novelwriter.common import checkInt, fuzzyTime
from novelwriter.core.tohtml import ToHtml from novelwriter.core.tohtml import ToHtml
@@ -74,18 +74,18 @@ class GuiManuscript(QDialog):
self.mainGui = mainGui self.mainGui = mainGui
self._builds = BuildCollection(self.mainGui.project) self._builds = BuildCollection(SHARED.project)
self._buildMap: dict[str, QListWidgetItem] = {} self._buildMap: dict[str, QListWidgetItem] = {}
self.setWindowTitle(self.tr("Build Manuscript")) self.setWindowTitle(self.tr("Build Manuscript"))
self.setMinimumWidth(CONFIG.pxInt(600)) self.setMinimumWidth(CONFIG.pxInt(600))
self.setMinimumHeight(CONFIG.pxInt(500)) self.setMinimumHeight(CONFIG.pxInt(500))
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
wWin = CONFIG.pxInt(900) wWin = CONFIG.pxInt(900)
hWin = CONFIG.pxInt(600) hWin = CONFIG.pxInt(600)
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
self.resize( self.resize(
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "winWidth", wWin)), CONFIG.pxInt(pOptions.getInt("GuiManuscript", "winWidth", wWin)),
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "winHeight", hWin)) CONFIG.pxInt(pOptions.getInt("GuiManuscript", "winHeight", hWin))
@@ -105,21 +105,21 @@ class GuiManuscript(QDialog):
).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue()) ).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
self.tbAdd = QToolButton(self) 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.setIconSize(QSize(iPx, iPx))
self.tbAdd.setToolTip(self.tr("Add New Build")) self.tbAdd.setToolTip(self.tr("Add New Build"))
self.tbAdd.setStyleSheet(buttonStyle) self.tbAdd.setStyleSheet(buttonStyle)
self.tbAdd.clicked.connect(self._createNewBuild) self.tbAdd.clicked.connect(self._createNewBuild)
self.tbDel = QToolButton(self) 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.setIconSize(QSize(iPx, iPx))
self.tbDel.setToolTip(self.tr("Delete Selected Build")) self.tbDel.setToolTip(self.tr("Delete Selected Build"))
self.tbDel.setStyleSheet(buttonStyle) self.tbDel.setStyleSheet(buttonStyle)
self.tbDel.clicked.connect(self._deleteSelectedBuild) self.tbDel.clicked.connect(self._deleteSelectedBuild)
self.tbEdit = QToolButton(self) 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.setIconSize(QSize(iPx, iPx))
self.tbEdit.setToolTip(self.tr("Edit Selected Build")) self.tbEdit.setToolTip(self.tr("Edit Selected Build"))
self.tbEdit.setStyleSheet(buttonStyle) self.tbEdit.setStyleSheet(buttonStyle)
@@ -163,7 +163,7 @@ class GuiManuscript(QDialog):
# Assemble GUI # Assemble GUI
# ============ # ============
self.docPreview = _PreviewWidget(self.mainGui) self.docPreview = _PreviewWidget(self)
self.controlBox = QVBoxLayout() self.controlBox = QVBoxLayout()
self.controlBox.addLayout(self.listToolBox, 0) self.controlBox.addLayout(self.listToolBox, 0)
@@ -210,7 +210,7 @@ class GuiManuscript(QDialog):
self._updateBuildsList() self._updateBuildsList()
logger.debug("Loading build cache") 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(): if cache.is_file():
try: try:
with open(cache, mode="r", encoding="utf-8") as fObj: with open(cache, mode="r", encoding="utf-8") as fObj:
@@ -268,7 +268,7 @@ class GuiManuscript(QDialog):
"""Delete the currently selected build settings entry.""" """Delete the currently selected build settings entry."""
build = self._getSelectedBuild() build = self._getSelectedBuild()
if build is not None: 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._builds.removeBuild(build.buildID)
self._updateBuildsList() self._updateBuildsList()
return return
@@ -289,7 +289,7 @@ class GuiManuscript(QDialog):
if build is None: if build is None:
return return
docBuild = NWBuildDocument(self.mainGui.project, build) docBuild = NWBuildDocument(SHARED.project, build)
docBuild.queueAll() docBuild.queueAll()
self.docPreview.beginNewBuild(len(docBuild)) self.docPreview.beginNewBuild(len(docBuild))
@@ -309,7 +309,7 @@ class GuiManuscript(QDialog):
self._updatePreview(result, build) self._updatePreview(result, build)
logger.debug("Saving build cache") 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: try:
with open(cache, mode="w+", encoding="utf-8") as outFile: with open(cache, mode="w+", encoding="utf-8") as outFile:
outFile.write(json.dumps(result, indent=2)) outFile.write(json.dumps(result, indent=2))
@@ -325,7 +325,7 @@ class GuiManuscript(QDialog):
"""Open the build dialog and build the manuscript.""" """Open the build dialog and build the manuscript."""
build = self._getSelectedBuild() build = self._getSelectedBuild()
if isinstance(build, BuildSettings): if isinstance(build, BuildSettings):
dlgBuild = GuiManuscriptBuild(self, self.mainGui, build) dlgBuild = GuiManuscriptBuild(self, build)
dlgBuild.exec_() dlgBuild.exec_()
# After the build is done, save build settings changes # After the build is done, save build settings changes
@@ -390,7 +390,7 @@ class GuiManuscript(QDialog):
optsWidth = CONFIG.rpxInt(mainSplit[0]) optsWidth = CONFIG.rpxInt(mainSplit[0])
viewWidth = CONFIG.rpxInt(mainSplit[1]) viewWidth = CONFIG.rpxInt(mainSplit[1])
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
pOptions.setValue("GuiManuscript", "winWidth", winWidth) pOptions.setValue("GuiManuscript", "winWidth", winWidth)
pOptions.setValue("GuiManuscript", "winHeight", winHeight) pOptions.setValue("GuiManuscript", "winHeight", winHeight)
pOptions.setValue("GuiManuscript", "optsWidth", optsWidth) pOptions.setValue("GuiManuscript", "optsWidth", optsWidth)
@@ -426,7 +426,7 @@ class GuiManuscript(QDialog):
for key, name in self._builds.builds(): for key, name in self._builds.builds():
bItem = QListWidgetItem() bItem = QListWidgetItem()
bItem.setText(name) bItem.setText(name)
bItem.setIcon(CONFIG.theme.getIcon("export")) bItem.setIcon(SHARED.theme.getIcon("export"))
bItem.setData(self.D_KEY, key) bItem.setData(self.D_KEY, key)
self.buildList.addItem(bItem) self.buildList.addItem(bItem)
self._buildMap[key] = bItem self._buildMap[key] = bItem
@@ -446,10 +446,8 @@ class GuiManuscript(QDialog):
class _PreviewWidget(QTextBrowser): class _PreviewWidget(QTextBrowser):
def __init__(self, mainGui: GuiMain): def __init__(self, parent: QWidget):
super().__init__(parent=mainGui) super().__init__(parent=parent)
self.mainGui = mainGui
self._docTime = 0 self._docTime = 0
self._buildName = "" self._buildName = ""
@@ -460,7 +458,7 @@ class _PreviewWidget(QTextBrowser):
dPalette.setColor(QPalette.Text, QColor(0, 0, 0)) dPalette.setColor(QPalette.Text, QColor(0, 0, 0))
self.setPalette(dPalette) self.setPalette(dPalette)
self.setMinimumWidth(40*CONFIG.theme.textNWidth) self.setMinimumWidth(40*SHARED.theme.textNWidth)
self.setTextFont(CONFIG.textFont, CONFIG.textSize) self.setTextFont(CONFIG.textFont, CONFIG.textSize)
self.setTabStopDistance(CONFIG.getTabWidth()) self.setTabStopDistance(CONFIG.getTabWidth())
self.setOpenExternalLinks(False) self.setOpenExternalLinks(False)
@@ -478,7 +476,7 @@ class _PreviewWidget(QTextBrowser):
aPalette.setColor(QPalette.Foreground, aPalette.toolTipText().color()) aPalette.setColor(QPalette.Foreground, aPalette.toolTipText().color())
aFont = self.font() aFont = self.font()
aFont.setPointSizeF(0.9*CONFIG.theme.fontPointSize) aFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
self.ageLabel = QLabel("", self) self.ageLabel = QLabel("", self)
self.ageLabel.setIndent(0) self.ageLabel.setIndent(0)
@@ -486,7 +484,7 @@ class _PreviewWidget(QTextBrowser):
self.ageLabel.setPalette(aPalette) self.ageLabel.setPalette(aPalette)
self.ageLabel.setAutoFillBackground(True) self.ageLabel.setAutoFillBackground(True)
self.ageLabel.setAlignment(Qt.AlignCenter) self.ageLabel.setAlignment(Qt.AlignCenter)
self.ageLabel.setFixedHeight(int(2.1*CONFIG.theme.fontPixelSize)) self.ageLabel.setFixedHeight(int(2.1*SHARED.theme.fontPixelSize))
# Progress # Progress
self.buildProgress = NProgressCircle(self, CONFIG.pxInt(160), CONFIG.pxInt(16)) self.buildProgress = NProgressCircle(self, CONFIG.pxInt(160), CONFIG.pxInt(16))
+34 -41
View File
@@ -39,7 +39,7 @@ from PyQt5.QtWidgets import (
QWidget QWidget
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.constants import nwHeadFmt, nwLabels, trConst from novelwriter.constants import nwHeadFmt, nwLabels, trConst
from novelwriter.core.buildsettings import BuildSettings, FilterMode from novelwriter.core.buildsettings import BuildSettings, FilterMode
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
@@ -76,8 +76,6 @@ class GuiBuildSettings(QDialog):
if CONFIG.osDarwin: if CONFIG.osDarwin:
self.setWindowFlag(Qt.WindowType.Tool) self.setWindowFlag(Qt.WindowType.Tool)
self.mainGui = mainGui
self._build = build self._build = build
self.setWindowTitle(self.tr("Manuscript Build Settings")) self.setWindowTitle(self.tr("Manuscript Build Settings"))
@@ -88,7 +86,7 @@ class GuiBuildSettings(QDialog):
wWin = CONFIG.pxInt(750) wWin = CONFIG.pxInt(750)
hWin = CONFIG.pxInt(550) hWin = CONFIG.pxInt(550)
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
self.resize( self.resize(
CONFIG.pxInt(pOptions.getInt("GuiBuildSettings", "winWidth", wWin)), CONFIG.pxInt(pOptions.getInt("GuiBuildSettings", "winWidth", wWin)),
CONFIG.pxInt(pOptions.getInt("GuiBuildSettings", "winHeight", hWin)) CONFIG.pxInt(pOptions.getInt("GuiBuildSettings", "winHeight", hWin))
@@ -100,7 +98,7 @@ class GuiBuildSettings(QDialog):
self.optSideBar = NPagedSideBar(self) self.optSideBar = NPagedSideBar(self)
self.optSideBar.setMinimumWidth(mPx) self.optSideBar.setMinimumWidth(mPx)
self.optSideBar.setMaximumWidth(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.addLabel(self.tr("Options"))
self.optSideBar.addButton(self.tr("Selection"), self.OPT_FILTERS) self.optSideBar.addButton(self.tr("Selection"), self.OPT_FILTERS)
@@ -245,7 +243,7 @@ class GuiBuildSettings(QDialog):
whether the user wants to save them. whether the user wants to save them.
""" """
if self._build.changed: 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) "Do you want to save your changes to '{0}'?".format(self._build.name)
)) ))
if response: if response:
@@ -262,7 +260,7 @@ class GuiBuildSettings(QDialog):
treeWidth, filterWidth = self.optTabSelect.mainSplitSizes() treeWidth, filterWidth = self.optTabSelect.mainSplitSizes()
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
pOptions.setValue("GuiBuildSettings", "winWidth", winWidth) pOptions.setValue("GuiBuildSettings", "winWidth", winWidth)
pOptions.setValue("GuiBuildSettings", "winHeight", winHeight) pOptions.setValue("GuiBuildSettings", "winHeight", winHeight)
pOptions.setValue("GuiBuildSettings", "treeWidth", treeWidth) pOptions.setValue("GuiBuildSettings", "treeWidth", treeWidth)
@@ -303,16 +301,14 @@ class _FilterTab(QWidget):
def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None:
super().__init__(parent=buildMain) super().__init__(parent=buildMain)
self.mainGui = buildMain.mainGui
self._treeMap: dict[str, QTreeWidgetItem] = {} self._treeMap: dict[str, QTreeWidgetItem] = {}
self._build = build self._build = build
self._statusFlags: dict[int, QIcon] = { self._statusFlags: dict[int, QIcon] = {
self.F_NONE: QIcon(), self.F_NONE: QIcon(),
self.F_FILTERED: CONFIG.theme.getIcon("build_filtered"), self.F_FILTERED: SHARED.theme.getIcon("build_filtered"),
self.F_INCLUDED: CONFIG.theme.getIcon("build_included"), self.F_INCLUDED: SHARED.theme.getIcon("build_included"),
self.F_EXCLUDED: CONFIG.theme.getIcon("build_excluded"), self.F_EXCLUDED: SHARED.theme.getIcon("build_excluded"),
} }
self._trIncluded = self.tr("Included in manuscript") self._trIncluded = self.tr("Included in manuscript")
@@ -322,7 +318,7 @@ class _FilterTab(QWidget):
# ============ # ============
# Tree Settings # Tree Settings
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
cMg = CONFIG.pxInt(6) cMg = CONFIG.pxInt(6)
# Tree Widget # Tree Widget
@@ -360,7 +356,7 @@ class _FilterTab(QWidget):
self.resetButton = QToolButton(self) self.resetButton = QToolButton(self)
self.resetButton.setToolTip(self.tr("Reset to default")) 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.resetButton.clicked.connect(lambda: self._setSelectedMode(self.F_FILTERED))
self.modeBox = QHBoxLayout() self.modeBox = QHBoxLayout()
@@ -379,7 +375,7 @@ class _FilterTab(QWidget):
# Assemble GUI # Assemble GUI
# ============ # ============
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
self.selectionBox = QVBoxLayout() self.selectionBox = QVBoxLayout()
self.selectionBox.addWidget(self.optTree) self.selectionBox.addWidget(self.optTree)
@@ -445,7 +441,7 @@ class _FilterTab(QWidget):
logger.debug("Building project tree") logger.debug("Building project tree")
self._treeMap = {} self._treeMap = {}
self.optTree.clear() self.optTree.clear()
for nwItem in self.mainGui.project.getProjectItems(): for nwItem in SHARED.project.iterProjectItems():
tHandle = nwItem.itemHandle tHandle = nwItem.itemHandle
pHandle = nwItem.itemParent pHandle = nwItem.itemParent
@@ -461,7 +457,7 @@ class _FilterTab(QWidget):
continue continue
hLevel = nwItem.mainHeading hLevel = nwItem.mainHeading
itemIcon = CONFIG.theme.getItemIcon( itemIcon = SHARED.theme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel nwItem.itemType, nwItem.itemClass, nwItem.itemLayout, hLevel
) )
@@ -475,7 +471,7 @@ class _FilterTab(QWidget):
trItem.setText(self.C_NAME, nwItem.itemName) trItem.setText(self.C_NAME, nwItem.itemName)
trItem.setData(self.C_DATA, self.D_HANDLE, tHandle) trItem.setData(self.C_DATA, self.D_HANDLE, tHandle)
trItem.setData(self.C_DATA, self.D_FILE, isFile) 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) trItem.setTextAlignment(self.C_NAME, Qt.AlignLeft)
@@ -499,19 +495,19 @@ class _FilterTab(QWidget):
self.filterOpt.clear() self.filterOpt.clear()
self.filterOpt.addLabel(self._build.getLabel("filter")) self.filterOpt.addLabel(self._build.getLabel("filter"))
self.filterOpt.addItem( self.filterOpt.addItem(
CONFIG.theme.getIcon("proj_scene"), SHARED.theme.getIcon("proj_scene"),
self._build.getLabel("filter.includeNovel"), self._build.getLabel("filter.includeNovel"),
"doc:filter.includeNovel", "doc:filter.includeNovel",
default=self._build.getBool("filter.includeNovel") default=self._build.getBool("filter.includeNovel")
) )
self.filterOpt.addItem( self.filterOpt.addItem(
CONFIG.theme.getIcon("proj_note"), SHARED.theme.getIcon("proj_note"),
self._build.getLabel("filter.includeNotes"), self._build.getLabel("filter.includeNotes"),
"doc:filter.includeNotes", "doc:filter.includeNotes",
default=self._build.getBool("filter.includeNotes") default=self._build.getBool("filter.includeNotes")
) )
self.filterOpt.addItem( self.filterOpt.addItem(
CONFIG.theme.getIcon("unchecked"), SHARED.theme.getIcon("unchecked"),
self._build.getLabel("filter.includeInactive"), self._build.getLabel("filter.includeInactive"),
"doc:filter.includeInactive", "doc:filter.includeInactive",
default=self._build.getBool("filter.includeInactive") default=self._build.getBool("filter.includeInactive")
@@ -521,9 +517,9 @@ class _FilterTab(QWidget):
# Root Classes # Root Classes
self.filterOpt.addLabel(self.tr("Select Root Folders")) 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(): if not nwItem.isInactiveClass():
itemIcon = CONFIG.theme.getItemIcon( itemIcon = SHARED.theme.getItemIcon(
nwItem.itemType, nwItem.itemClass, nwItem.itemLayout nwItem.itemType, nwItem.itemClass, nwItem.itemLayout
) )
self.filterOpt.addItem( self.filterOpt.addItem(
@@ -557,7 +553,7 @@ class _FilterTab(QWidget):
def _setTreeItemMode(self) -> None: def _setTreeItemMode(self) -> None:
"""Update the filtered mode icon on all items.""" """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(): for tHandle, item in self._treeMap.items():
allow, mode = filtered.get(tHandle, (False, FilterMode.UNKNOWN)) allow, mode = filtered.get(tHandle, (False, FilterMode.UNKNOWN))
if mode == FilterMode.INCLUDED: if mode == FilterMode.INCLUDED:
@@ -597,12 +593,10 @@ class _HeadingsTab(QWidget):
def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None:
super().__init__(parent=buildMain) super().__init__(parent=buildMain)
self.mainGui = buildMain.mainGui
self._build = build self._build = build
self._editing = 0 self._editing = 0
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
vSp = CONFIG.pxInt(12) vSp = CONFIG.pxInt(12)
bSp = CONFIG.pxInt(6) bSp = CONFIG.pxInt(6)
@@ -616,7 +610,7 @@ class _HeadingsTab(QWidget):
self.fmtTitle = QLineEdit("") self.fmtTitle = QLineEdit("")
self.fmtTitle.setReadOnly(True) self.fmtTitle.setReadOnly(True)
self.btnTitle = QToolButton() 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)) self.btnTitle.clicked.connect(lambda: self._editHeading(self.EDIT_TITLE))
wrapTitle = QHBoxLayout() wrapTitle = QHBoxLayout()
@@ -632,7 +626,7 @@ class _HeadingsTab(QWidget):
self.fmtChapter = QLineEdit("") self.fmtChapter = QLineEdit("")
self.fmtChapter.setReadOnly(True) self.fmtChapter.setReadOnly(True)
self.btnChapter = QToolButton() 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)) self.btnChapter.clicked.connect(lambda: self._editHeading(self.EDIT_CHAPTER))
wrapChapter = QHBoxLayout() wrapChapter = QHBoxLayout()
@@ -648,7 +642,7 @@ class _HeadingsTab(QWidget):
self.fmtUnnumbered = QLineEdit("") self.fmtUnnumbered = QLineEdit("")
self.fmtUnnumbered.setReadOnly(True) self.fmtUnnumbered.setReadOnly(True)
self.btnUnnumbered = QToolButton() 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)) self.btnUnnumbered.clicked.connect(lambda: self._editHeading(self.EDIT_UNNUM))
wrapUnnumbered = QHBoxLayout() wrapUnnumbered = QHBoxLayout()
@@ -665,7 +659,7 @@ class _HeadingsTab(QWidget):
self.fmtScene = QLineEdit("") self.fmtScene = QLineEdit("")
self.fmtScene.setReadOnly(True) self.fmtScene.setReadOnly(True)
self.btnScene = QToolButton() 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.btnScene.clicked.connect(lambda: self._editHeading(self.EDIT_SCENE))
self.hdeScene = QLabel(self.tr("Hide")) self.hdeScene = QLabel(self.tr("Hide"))
self.hdeScene.setToolTip(sceneHideTip) self.hdeScene.setToolTip(sceneHideTip)
@@ -692,7 +686,7 @@ class _HeadingsTab(QWidget):
self.fmtSection = QLineEdit("") self.fmtSection = QLineEdit("")
self.fmtSection.setReadOnly(True) self.fmtSection.setReadOnly(True)
self.btnSection = QToolButton() 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.btnSection.clicked.connect(lambda: self._editHeading(self.EDIT_SECTION))
self.hdeSection = QLabel(self.tr("Hide")) self.hdeSection = QLabel(self.tr("Hide"))
self.hdeSection.setToolTip(sectionHideTip) self.hdeSection.setToolTip(sectionHideTip)
@@ -868,9 +862,9 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter):
def __init__(self, document: QTextDocument) -> None: def __init__(self, document: QTextDocument) -> None:
super().__init__(document) super().__init__(document)
self._fmtSymbol = QTextCharFormat() self._fmtSymbol = QTextCharFormat()
self._fmtSymbol.setForeground(QColor(*CONFIG.theme.colHead)) self._fmtSymbol.setForeground(QColor(*SHARED.theme.colHead))
self._fmtFormat = QTextCharFormat() self._fmtFormat = QTextCharFormat()
self._fmtFormat.setForeground(QColor(*CONFIG.theme.colEmph)) self._fmtFormat.setForeground(QColor(*SHARED.theme.colEmph))
return return
def highlightBlock(self, text: str) -> None: def highlightBlock(self, text: str) -> None:
@@ -896,7 +890,7 @@ class _ContentTab(QWidget):
self._build = build self._build = build
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
# Left Form # Left Form
# ========= # =========
@@ -964,14 +958,13 @@ class _FormatTab(QWidget):
super().__init__(parent=buildMain) super().__init__(parent=buildMain)
self.buildMain = buildMain self.buildMain = buildMain
self.mainGui = buildMain.mainGui
self._build = build self._build = build
self._unitScale = 1.0 self._unitScale = 1.0
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
spW = 6*CONFIG.theme.textNWidth spW = 6*SHARED.theme.textNWidth
dbW = 8*CONFIG.theme.textNWidth dbW = 8*SHARED.theme.textNWidth
# Text Format Form # Text Format Form
# ================ # ================
@@ -992,7 +985,7 @@ class _FormatTab(QWidget):
self.textFont = QLineEdit() self.textFont = QLineEdit()
self.textFont.setReadOnly(True) self.textFont.setReadOnly(True)
self.btnTextFont = QPushButton("...") 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.btnTextFont.clicked.connect(self._selectFont)
self.formFormat.addRow( self.formFormat.addRow(
self._build.getLabel("format.textFont"), self.textFont, button=self.btnTextFont self._build.getLabel("format.textFont"), self.textFont, button=self.btnTextFont
@@ -1278,7 +1271,7 @@ class _OutputTab(QWidget):
self._build = build self._build = build
iPx = CONFIG.theme.baseIconSize iPx = SHARED.theme.baseIconSize
# Left Form # Left Form
# ========= # =========
+4 -4
View File
@@ -32,7 +32,7 @@ from PyQt5.QtWidgets import (
QPushButton, QRadioButton, QSpinBox, QVBoxLayout, QWizard, QWizardPage QPushButton, QRadioButton, QSpinBox, QVBoxLayout, QWizard, QWizardPage
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.common import makeFileNameSafe from novelwriter.common import makeFileNameSafe
from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switch import NSwitch
@@ -55,7 +55,7 @@ class GuiProjectWizard(QWizard):
self.mainGui = mainGui self.mainGui = mainGui
self.sideImage = CONFIG.theme.loadDecoration( self.sideImage = SHARED.theme.loadDecoration(
"wiz-back", None, CONFIG.pxInt(370) "wiz-back", None, CONFIG.pxInt(370)
) )
self.setWizardStyle(QWizard.ModernStyle) self.setWizardStyle(QWizard.ModernStyle)
@@ -104,7 +104,7 @@ class ProjWizardIntroPage(QWizardPage):
"Peter Mitterhofer", "CC BY-SA 4.0" "Peter Mitterhofer", "CC BY-SA 4.0"
)) ))
lblFont = self.imgCredit.font() lblFont = self.imgCredit.font()
lblFont.setPointSizeF(0.6*CONFIG.theme.fontPointSize) lblFont.setPointSizeF(0.6*SHARED.theme.fontPointSize)
self.imgCredit.setFont(lblFont) self.imgCredit.setFont(lblFont)
xW = CONFIG.pxInt(300) xW = CONFIG.pxInt(300)
@@ -172,7 +172,7 @@ class ProjWizardFolderPage(QWizardPage):
self.projPath.setPlaceholderText(self.tr("Required")) self.projPath.setPlaceholderText(self.tr("Required"))
self.browseButton = QPushButton("...") 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.browseButton.clicked.connect(self._doBrowse)
self.errLabel = QLabel("") self.errLabel = QLabel("")
+20 -23
View File
@@ -36,8 +36,7 @@ from PyQt5.QtWidgets import (
QLabel, QGroupBox, QMenu, QAction, QFileDialog, QSpinBox, QHBoxLayout QLabel, QGroupBox, QMenu, QAction, QFileDialog, QSpinBox, QHBoxLayout
) )
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwAlert
from novelwriter.error import formatException from novelwriter.error import formatException
from novelwriter.common import formatTime, checkInt, checkIntTuple, minmax from novelwriter.common import formatTime, checkInt, checkIntTuple, minmax
from novelwriter.constants import nwConst from novelwriter.constants import nwConst
@@ -72,14 +71,12 @@ class GuiWritingStats(QDialog):
if CONFIG.osDarwin: if CONFIG.osDarwin:
self.setWindowFlag(Qt.WindowType.Tool) self.setWindowFlag(Qt.WindowType.Tool)
self.mainGui = mainGui
self.logData = [] self.logData = []
self.filterData = [] self.filterData = []
self.timeFilter = 0.0 self.timeFilter = 0.0
self.wordOffset = 0 self.wordOffset = 0
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
self.setWindowTitle(self.tr("Writing Statistics")) self.setWindowTitle(self.tr("Writing Statistics"))
self.setMinimumWidth(CONFIG.pxInt(420)) self.setMinimumWidth(CONFIG.pxInt(420))
@@ -132,7 +129,7 @@ class GuiWritingStats(QDialog):
self.listBox.setSortingEnabled(True) self.listBox.setSortingEnabled(True)
# Word Bar # 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.barWidth = CONFIG.pxInt(200)
self.barImage = QPixmap(self.barHeight, self.barHeight) self.barImage = QPixmap(self.barHeight, self.barHeight)
self.barImage.fill(self.palette().highlight().color()) self.barImage.fill(self.palette().highlight().color())
@@ -143,27 +140,27 @@ class GuiWritingStats(QDialog):
self.infoBox.setLayout(self.infoForm) self.infoBox.setLayout(self.infoForm)
self.labelTotal = QLabel(formatTime(0)) 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.labelTotal.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.labelIdleT = QLabel(formatTime(0)) 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.labelIdleT.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.labelFilter = QLabel(formatTime(0)) 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.labelFilter.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.novelWords = QLabel("0") self.novelWords = QLabel("0")
self.novelWords.setFont(CONFIG.theme.guiFontFixed) self.novelWords.setFont(SHARED.theme.guiFontFixed)
self.novelWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.novelWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.notesWords = QLabel("0") self.notesWords = QLabel("0")
self.notesWords.setFont(CONFIG.theme.guiFontFixed) self.notesWords.setFont(SHARED.theme.guiFontFixed)
self.notesWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.notesWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
self.totalWords = QLabel("0") self.totalWords = QLabel("0")
self.totalWords.setFont(CONFIG.theme.guiFontFixed) self.totalWords.setFont(SHARED.theme.guiFontFixed)
self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight) self.totalWords.setAlignment(Qt.AlignVCenter | Qt.AlignRight)
lblTTime = QLabel(self.tr("Total Time:")) lblTTime = QLabel(self.tr("Total Time:"))
@@ -190,7 +187,7 @@ class GuiWritingStats(QDialog):
self.infoForm.setRowStretch(6, 1) self.infoForm.setRowStretch(6, 1)
# Filter Options # Filter Options
sPx = CONFIG.theme.baseIconSize sPx = SHARED.theme.baseIconSize
self.filterBox = QGroupBox(self.tr("Filters"), self) self.filterBox = QGroupBox(self.tr("Filters"), self)
self.filterForm = QGridLayout(self) self.filterForm = QGridLayout(self)
@@ -333,7 +330,7 @@ class GuiWritingStats(QDialog):
showIdleTime = self.showIdleTime.isChecked() showIdleTime = self.showIdleTime.isChecked()
histMax = self.histMax.value() histMax = self.histMax.value()
pOptions = self.mainGui.project.options pOptions = SHARED.project.options
pOptions.setValue("GuiWritingStats", "winWidth", winWidth) pOptions.setValue("GuiWritingStats", "winWidth", winWidth)
pOptions.setValue("GuiWritingStats", "winHeight", winHeight) pOptions.setValue("GuiWritingStats", "winHeight", winHeight)
pOptions.setValue("GuiWritingStats", "widthCol0", widthCol0) pOptions.setValue("GuiWritingStats", "widthCol0", widthCol0)
@@ -413,14 +410,14 @@ class GuiWritingStats(QDialog):
# Report to user # Report to user
if wSuccess: if wSuccess:
self.mainGui.makeAlert( SHARED.info(
self.tr("{0} file successfully written to:").format(textFmt), self.tr("{0} file successfully written to:").format(textFmt),
info=savePath info=savePath
) )
else: else:
self.mainGui.makeAlert( SHARED.error(
self.tr("Failed to write {0} file.").format(textFmt), self.tr("Failed to write {0} file.").format(textFmt),
info=errMsg, level=nwAlert.ERROR info=errMsg
) )
return wSuccess return wSuccess
@@ -441,7 +438,7 @@ class GuiWritingStats(QDialog):
ttTime = 0 ttTime = 0
ttIdle = 0 ttIdle = 0
for record in self.mainGui.project.session.iterRecords(): for record in SHARED.project.session.iterRecords():
rType = record.get("type") rType = record.get("type")
if rType == "initial": if rType == "initial":
self.wordOffset = checkInt(record.get("offset"), 0) 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_COUNT, Qt.AlignRight)
newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter) newItem.setTextAlignment(self.C_BAR, Qt.AlignLeft | Qt.AlignVCenter)
newItem.setFont(self.C_TIME, CONFIG.theme.guiFontFixed) newItem.setFont(self.C_TIME, SHARED.theme.guiFontFixed)
newItem.setFont(self.C_LENGTH, CONFIG.theme.guiFontFixed) newItem.setFont(self.C_LENGTH, SHARED.theme.guiFontFixed)
newItem.setFont(self.C_COUNT, CONFIG.theme.guiFontFixed) newItem.setFont(self.C_COUNT, SHARED.theme.guiFontFixed)
if showIdleTime: if showIdleTime:
newItem.setFont(self.C_IDLE, CONFIG.theme.guiFontFixed) newItem.setFont(self.C_IDLE, SHARED.theme.guiFontFixed)
else: else:
newItem.setFont(self.C_IDLE, CONFIG.theme.guiFont) newItem.setFont(self.C_IDLE, SHARED.theme.guiFont)
self.listBox.addTopLevelItem(newItem) self.listBox.addTopLevelItem(newItem)
self.timeFilter += sDiff self.timeFilter += sDiff
+13 -5
View File
@@ -22,17 +22,18 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import sys import sys
import pytest import pytest
import shutil import shutil
import logging
from pathlib import Path from pathlib import Path
from mocked import MockGuiMain
from tools import cleanProject from tools import cleanProject
from mocked import MockGuiMain, MockTheme
from PyQt5.QtWidgets import QMessageBox from PyQt5.QtWidgets import QMessageBox
sys.path.insert(1, str(Path(__file__).parent.parent.absolute())) 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 _TST_ROOT = Path(__file__).parent
_TMP_ROOT = _TST_ROOT / "temp" _TMP_ROOT = _TST_ROOT / "temp"
@@ -62,6 +63,7 @@ def resetConfigVars():
@pytest.fixture(scope="session", autouse=True) @pytest.fixture(scope="session", autouse=True)
def sessionFixture(): def sessionFixture():
"""A session wide fixture to set up the test environment.""" """A session wide fixture to set up the test environment."""
logging.root.setLevel(logging.INFO)
if _TMP_ROOT.exists(): if _TMP_ROOT.exists():
shutil.rmtree(_TMP_ROOT) shutil.rmtree(_TMP_ROOT)
_TMP_ROOT.mkdir() _TMP_ROOT.mkdir()
@@ -81,6 +83,7 @@ def functionFixture(qtbot):
CONFIG.__init__() CONFIG.__init__()
CONFIG.initConfig(confPath=_TMP_CONF, dataPath=_TMP_CONF) CONFIG.initConfig(confPath=_TMP_CONF, dataPath=_TMP_CONF)
resetConfigVars() resetConfigVars()
logging.getLogger("novelwriter").setLevel(logging.INFO)
return return
@@ -136,10 +139,15 @@ def projPath(fncPath):
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def mockGUI(): def mockGUI(qtbot, monkeypatch):
"""Create a mock instance of novelWriter's main GUI class.""" """Create a mock instance of novelWriter's main GUI class."""
theGui = MockGuiMain() monkeypatch.setattr(QMessageBox, "exec_", lambda *a: None)
return theGui 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") @pytest.fixture(scope="function")
+16 -38
View File
@@ -19,49 +19,25 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from PyQt5.QtCore import QObject from PyQt5.QtGui import QPixmap
from PyQt5.QtWidgets import QWidget
# =========================================================================== # # =========================================================================== #
# Mock GUI # Mock GUI
# =========================================================================== # # =========================================================================== #
class MockGuiMain(QObject): class MockGuiMain(QWidget):
def __init__(self): def __init__(self):
super().__init__() super().__init__()
self._project = None
self.hasProject = True
self.mainStatus = MockStatusBar() self.mainStatus = MockStatusBar()
self.projPath = "" self.projPath = ""
# Test Variables
self.askResponse = True
self.lastAlert = ""
self.lastQuestion = ""
return return
@property
def project(self):
return self._project
def postLaunchTasks(self, cmdOpen): def postLaunchTasks(self, cmdOpen):
return 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): def setStatus(self, theMessage):
return return
@@ -78,16 +54,6 @@ class MockGuiMain(QObject):
def close(self): def close(self):
return "close" return "close"
# Test Functions
def undo(self):
self.askResponse = True
return
def clear(self):
self.lastAlert = ""
return
# END Class MockGuiMain # END Class MockGuiMain
@@ -99,12 +65,24 @@ class MockStatusBar:
def setStatus(self, theText): def setStatus(self, theText):
return return
def doUpdateProjectStatus(self, theStatus): def updateProjectStatus(self, theStatus):
return return
# END Class MockStatusBar # END Class MockStatusBar
class MockTheme:
def __init__(self):
self.baseIconSize = 10
return
def getPixmap(self, *a):
return QPixmap()
# END Class MockTheme
class MockApp: class MockApp:
def __init__(self): def __init__(self):
+122 -94
View File
@@ -24,6 +24,7 @@ import pytest
import hashlib import hashlib
from pathlib import Path from pathlib import Path
from xml.etree import ElementTree as ET
from tools import writeFile from tools import writeFile
from mocked import causeOSError from mocked import causeOSError
@@ -35,12 +36,12 @@ from novelwriter.common import (
formatTimeStamp, fuzzyTime, getGuiItem, hexToInt, isHandle, isItemClass, formatTimeStamp, fuzzyTime, getGuiItem, hexToInt, isHandle, isItemClass,
isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax, isItemLayout, isItemType, isTitleTag, jsonEncode, makeFileNameSafe, minmax,
numberToRoman, NWConfigParser, readTextFile, sha256sum, simplified, numberToRoman, NWConfigParser, readTextFile, sha256sum, simplified,
transferCase, yesNo transferCase, xmlIndent, yesNo
) )
@pytest.mark.base @pytest.mark.base
def testBaseCommon_CheckStringNone(): def testBaseCommon_checkStringNone():
"""Test the checkStringNone function.""" """Test the checkStringNone function."""
assert checkStringNone("Stuff", "NotNone") == "Stuff" assert checkStringNone("Stuff", "NotNone") == "Stuff"
assert checkStringNone("None", "NotNone") is None assert checkStringNone("None", "NotNone") is None
@@ -49,11 +50,11 @@ def testBaseCommon_CheckStringNone():
assert checkStringNone(1.0, "NotNone") == "NotNone" assert checkStringNone(1.0, "NotNone") == "NotNone"
assert checkStringNone(True, "NotNone") == "NotNone" assert checkStringNone(True, "NotNone") == "NotNone"
# END Test testBaseCommon_CheckStringNone # END Test testBaseCommon_checkStringNone
@pytest.mark.base @pytest.mark.base
def testBaseCommon_CheckString(): def testBaseCommon_checkString():
"""Test the checkString function. Anything that is a string should """Test the checkString function. Anything that is a string should
be returned, otherwise it returns the default. be returned, otherwise it returns the default.
""" """
@@ -64,11 +65,11 @@ def testBaseCommon_CheckString():
assert checkString(1.0, "default") == "default" assert checkString(1.0, "default") == "default"
assert checkString(True, "default") == "default" assert checkString(True, "default") == "default"
# END Test testBaseCommon_CheckString # END Test testBaseCommon_checkString
@pytest.mark.base @pytest.mark.base
def testBaseCommon_CheckInt(): def testBaseCommon_checkInt():
"""Test the checkInt function. Anything that can be converted to an """Test the checkInt function. Anything that can be converted to an
integer should be returned, otherwise it returns the default. integer should be returned, otherwise it returns the default.
""" """
@@ -80,11 +81,11 @@ def testBaseCommon_CheckInt():
assert checkInt("1", 3) == 1 assert checkInt("1", 3) == 1
assert checkInt("1.0", 3) == 3 assert checkInt("1.0", 3) == 3
# END Test testBaseCommon_CheckInt # END Test testBaseCommon_checkInt
@pytest.mark.base @pytest.mark.base
def testBaseCommon_CheckFloat(): def testBaseCommon_checkFloat():
"""Test the checkFloat function. Anything that can be converted to an """Test the checkFloat function. Anything that can be converted to an
integer should be returned, otherwise it returns the default. 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", 3.0) == 1.0
assert checkFloat("1.0", 3.0) == 1.0 assert checkFloat("1.0", 3.0) == 1.0
# END Test testBaseCommon_CheckInt # END Test testBaseCommon_checkFloat
@pytest.mark.base @pytest.mark.base
def testBaseCommon_CheckBool(): def testBaseCommon_checkBool():
"""Test the checkBool function. Any bool, string version of Python """Test the checkBool function. Any bool, string version of Python
bool, or integer 1 or 0, are returned as bool. Otherwise, the bool, or integer 1 or 0, are returned as bool. Otherwise, the
default is returned. default is returned.
@@ -145,11 +146,11 @@ def testBaseCommon_CheckBool():
assert checkBool(2.0, True) is True assert checkBool(2.0, True) is True
assert checkBool(2.0, False) is False assert checkBool(2.0, False) is False
# END Test testBaseCommon_CheckBool # END Test testBaseCommon_checkBool
@pytest.mark.base @pytest.mark.base
def testBaseCommon_CheckHandle(): def testBaseCommon_checkHandle():
"""Test the checkHandle function.""" """Test the checkHandle function."""
assert checkHandle("None", 1, True) is None assert checkHandle("None", 1, True) is None
assert checkHandle("None", 1, False) == 1 assert checkHandle("None", 1, False) == 1
@@ -158,36 +159,36 @@ def testBaseCommon_CheckHandle():
assert checkHandle("47666c91c7ccf", None, False) == "47666c91c7ccf" assert checkHandle("47666c91c7ccf", None, False) == "47666c91c7ccf"
assert checkHandle("h7666c91c7ccf", None, False) is None assert checkHandle("h7666c91c7ccf", None, False) is None
# END Test testBaseCommon_CheckHandle # END Test testBaseCommon_checkHandle
@pytest.mark.base @pytest.mark.base
def testBaseCommon_CheckUuid(): def testBaseCommon_checkUuid():
"""Test the checkUuid function.""" """Test the checkUuid function."""
testUuid = "e2be99af-f9bf-4403-857a-c3d1ac25abea" testUuid = "e2be99af-f9bf-4403-857a-c3d1ac25abea"
assert checkUuid("", None) is None assert checkUuid("", None) is None # type: ignore
assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abe", None) is None assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abe", None) is None # type: ignore
assert checkUuid("e2be99af-f9bf-qq03-857a-c3d1ac25abea", None) is None assert checkUuid("e2be99af-f9bf-qq03-857a-c3d1ac25abea", None) is None # type: ignore
assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abeaa", None) is None assert checkUuid("e2be99af-f9bf-4403-857a-c3d1ac25abeaa", None) is None # type: ignore
assert checkUuid(testUuid, None) == testUuid assert checkUuid(testUuid, None) == testUuid # type: ignore
# END Test testBaseCommon_CheckUuid # END Test testBaseCommon_checkUuid
@pytest.mark.base @pytest.mark.base
def testBaseCommon_CheckPath(): def testBaseCommon_checkPath():
"""Test the checkPath function.""" """Test the checkPath function."""
assert checkPath(Path("test"), None) == Path("test") assert checkPath(Path("test"), None) == Path("test") # type: ignore
assert checkPath("test", None) == Path("test") assert checkPath("test", None) == Path("test") # type: ignore
assert checkPath(None, None) is None assert checkPath(None, None) is None # type: ignore
assert checkPath("", None) is None assert checkPath("", None) is None # type: ignore
assert checkPath(" ", None) is None assert checkPath(" ", None) is None # type: ignore
# END Test testBaseCommon_CheckPath # END Test testBaseCommon_checkPath
@pytest.mark.base @pytest.mark.base
def testBaseCommon_IsHandle(): def testBaseCommon_isHandle():
"""Test the isHandle function.""" """Test the isHandle function."""
assert isHandle("47666c91c7ccf") is True assert isHandle("47666c91c7ccf") is True
assert isHandle("47666C91C7CCF") is False assert isHandle("47666C91C7CCF") is False
@@ -196,12 +197,12 @@ def testBaseCommon_IsHandle():
assert isHandle(None) is False assert isHandle(None) is False
assert isHandle("STUFF") is False assert isHandle("STUFF") is False
# END Test testBaseCommon_IsHandle # END Test testBaseCommon_isHandle
@pytest.mark.base @pytest.mark.base
def testBaseCommon_IsTitleTag(): def testBaseCommon_isTitleTag():
"""Test the isItemClass function.""" """Test the isTitleTag function."""
assert isTitleTag("T1234") is True assert isTitleTag("T1234") is True
assert isTitleTag("t1234") is False assert isTitleTag("t1234") is False
@@ -213,11 +214,11 @@ def testBaseCommon_IsTitleTag():
assert isTitleTag(None) is False assert isTitleTag(None) is False
assert isTitleTag("STUFF") is False assert isTitleTag("STUFF") is False
# END Test testBaseCommon_IsTitleTag # END Test testBaseCommon_isTitleTag
@pytest.mark.base @pytest.mark.base
def testBaseCommon_IsItemClass(): def testBaseCommon_isItemClass():
"""Test the isItemClass function.""" """Test the isItemClass function."""
assert isItemClass("NO_CLASS") is True assert isItemClass("NO_CLASS") is True
assert isItemClass("NOVEL") is True assert isItemClass("NOVEL") is True
@@ -233,14 +234,14 @@ def testBaseCommon_IsItemClass():
# Invalid # Invalid
assert isItemClass("None") is False assert isItemClass("None") is False
assert isItemClass(None) is False assert isItemClass(None) is False # type: ignore
assert isItemClass("STUFF") is False assert isItemClass("STUFF") is False
# END Test testBaseCommon_IsItemClass # END Test testBaseCommon_isItemClass
@pytest.mark.base @pytest.mark.base
def testBaseCommon_IsItemType(): def testBaseCommon_isItemType():
"""Test the isItemType function.""" """Test the isItemType function."""
assert isItemType("NO_TYPE") is True assert isItemType("NO_TYPE") is True
assert isItemType("ROOT") is True assert isItemType("ROOT") is True
@@ -252,14 +253,14 @@ def testBaseCommon_IsItemType():
# Invalid # Invalid
assert isItemType("None") is False assert isItemType("None") is False
assert isItemType(None) is False assert isItemType(None) is False # type: ignore
assert isItemType("STUFF") is False assert isItemType("STUFF") is False
# END Test testBaseCommon_IsItemType # END Test testBaseCommon_isItemType
@pytest.mark.base @pytest.mark.base
def testBaseCommon_IsItemLayout(): def testBaseCommon_isItemLayout():
"""Test the isItemLayout function.""" """Test the isItemLayout function."""
assert isItemLayout("NO_LAYOUT") is True assert isItemLayout("NO_LAYOUT") is True
assert isItemLayout("DOCUMENT") is True assert isItemLayout("DOCUMENT") is True
@@ -276,14 +277,14 @@ def testBaseCommon_IsItemLayout():
# Invalid # Invalid
assert isItemLayout("None") is False assert isItemLayout("None") is False
assert isItemLayout(None) is False assert isItemLayout(None) is False # type: ignore
assert isItemLayout("STUFF") is False assert isItemLayout("STUFF") is False
# END Test testBaseCommon_IsItemLayout # END Test testBaseCommon_isItemLayout
@pytest.mark.base @pytest.mark.base
def testBaseCommon_HexToInt(): def testBaseCommon_hexToInt():
"""Test the hexToInt function.""" """Test the hexToInt function."""
assert hexToInt(1) == 0 assert hexToInt(1) == 0
assert hexToInt("1") == 1 assert hexToInt("1") == 1
@@ -292,42 +293,42 @@ def testBaseCommon_HexToInt():
assert hexToInt("0xffffq") == 0 assert hexToInt("0xffffq") == 0
assert hexToInt("0xffffq", 12) == 12 assert hexToInt("0xffffq", 12) == 12
# END Test testBaseCommon_HexToInt # END Test testBaseCommon_hexToInt
@pytest.mark.base @pytest.mark.base
def testBaseCommon_MinMax(): def testBaseCommon_minmax():
"""Test the minmax function.""" """Test the minmax function."""
for i in range(-5, 15): for i in range(-5, 15):
assert 0 <= minmax(i, 0, 10) <= 10 assert 0 <= minmax(i, 0, 10) <= 10
# END Test testBaseCommon_MinMax # END Test testBaseCommon_minmax
@pytest.mark.base @pytest.mark.base
def testBaseCommon_CheckIntTuple(): def testBaseCommon_checkIntTuple():
"""Test the checkIntTuple function.""" """Test the checkIntTuple function."""
assert checkIntTuple(0, (0, 1, 2), 3) == 0 assert checkIntTuple(0, (0, 1, 2), 3) == 0
assert checkIntTuple(5, (0, 1, 2), 3) == 3 assert checkIntTuple(5, (0, 1, 2), 3) == 3
# END Test testBaseCommon_CheckIntTuple # END Test testBaseCommon_checkIntTuple
@pytest.mark.base @pytest.mark.base
def testBaseCommon_FormatTimeStamp(): def testBaseCommon_formatTimeStamp():
"""Test the formatTimeStamp function.""" """Test the formatTimeStamp function."""
tTime = time.mktime(time.gmtime(0)) tTime = time.mktime(time.gmtime(0))
assert formatTimeStamp(tTime, False) == "1970-01-01 00:00:00" assert formatTimeStamp(tTime, False) == "1970-01-01 00:00:00"
assert formatTimeStamp(tTime, True) == "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 @pytest.mark.base
def testBaseCommon_FormatTime(): def testBaseCommon_formatTime():
"""Test the formatTime function.""" """Test the formatTime function."""
assert formatTime("1") == "ERROR" assert formatTime("1") == "ERROR" # type: ignore
assert formatTime(1.0) == "ERROR" assert formatTime(1.0) == "ERROR" # type: ignore
assert formatTime(1) == "00:00:01" assert formatTime(1) == "00:00:01"
assert formatTime(59) == "00:00:59" assert formatTime(59) == "00:00:59"
assert formatTime(60) == "00:01:00" assert formatTime(60) == "00:01:00"
@@ -342,21 +343,21 @@ def testBaseCommon_FormatTime():
assert formatTime(86400) == "1-00:00:00" assert formatTime(86400) == "1-00:00:00"
assert formatTime(360000) == "4-04:00:00" assert formatTime(360000) == "4-04:00:00"
# END Test testBaseCommon_FormatTime # END Test testBaseCommon_formatTime
@pytest.mark.base @pytest.mark.base
def testBaseCommon_Simplified(): def testBaseCommon_simplified():
"""Test the simplified function.""" """Test the simplified function."""
assert simplified("Hello World") == "Hello World" assert simplified("Hello World") == "Hello World"
assert simplified(" Hello World ") == "Hello World" assert simplified(" Hello World ") == "Hello World"
assert simplified("\tHello\n\r\tWorld") == "Hello World" assert simplified("\tHello\n\r\tWorld") == "Hello World"
# END Test testBaseCommon_Simplified # END Test testBaseCommon_simplified
@pytest.mark.base @pytest.mark.base
def testBaseCommon_YesNo(): def testBaseCommon_yesNo():
"""Test the yesNo function.""" """Test the yesNo function."""
# Bool # Bool
assert yesNo(True) == "yes" assert yesNo(True) == "yes"
@@ -366,8 +367,8 @@ def testBaseCommon_YesNo():
assert yesNo(None) == "no" assert yesNo(None) == "no"
# String # String
assert yesNo("foo") == "yes" assert yesNo("foo") == "yes" # type: ignore
assert yesNo("") == "no" assert yesNo("") == "no" # type: ignore
# Integer # Integer
assert yesNo(0) == "no" assert yesNo(0) == "no"
@@ -375,15 +376,15 @@ def testBaseCommon_YesNo():
assert yesNo(2) == "yes" assert yesNo(2) == "yes"
# Float # Float
assert yesNo(0.0) == "no" assert yesNo(0.0) == "no" # type: ignore
assert yesNo(1.0) == "yes" assert yesNo(1.0) == "yes" # type: ignore
assert yesNo(2.0) == "yes" assert yesNo(2.0) == "yes" # type: ignore
# END Test testBaseCommon_YesNo # END Test testBaseCommon_yesNo
@pytest.mark.base @pytest.mark.base
def testBaseCommon_FormatInt(): def testBaseCommon_formatInt():
"""Test the formatInt function.""" """Test the formatInt function."""
# Normal Cases # Normal Cases
assert formatInt(1) == "1" assert formatInt(1) == "1"
@@ -398,29 +399,29 @@ def testBaseCommon_FormatInt():
assert formatInt(1234567890) == "1.23\u2009G" assert formatInt(1234567890) == "1.23\u2009G"
# Exceptions # Exceptions
assert formatInt(12.3) == "ERR" assert formatInt(12.3) == "ERR" # type: ignore
assert formatInt(None) == "ERR" assert formatInt(None) == "ERR" # type: ignore
assert formatInt("42") == "ERR" assert formatInt("42") == "ERR" # type: ignore
# END Test testBaseCommon_FormatInt # END Test testBaseCommon_formatInt
@pytest.mark.base @pytest.mark.base
def testBaseCommon_TransferCase(): def testBaseCommon_transferCase():
"""Test the transferCase function.""" """Test the transferCase function."""
assert transferCase(1, "TaRgEt") == "TaRgEt" assert transferCase(1, "TaRgEt") == "TaRgEt" # type: ignore
assert transferCase("source", 1) == 1 assert transferCase("source", 1) == 1 # type: ignore
assert transferCase("", "TaRgEt") == "TaRgEt" assert transferCase("", "TaRgEt") == "TaRgEt"
assert transferCase("source", "") == "" assert transferCase("source", "") == ""
assert transferCase("Source", "target") == "Target" assert transferCase("Source", "target") == "Target"
assert transferCase("SOURCE", "target") == "TARGET" 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 @pytest.mark.base
def testBaseCommon_FuzzyTime(): def testBaseCommon_fuzzyTime():
"""Test the fuzzyTime function.""" """Test the fuzzyTime function."""
assert fuzzyTime(-1) == "in the future" assert fuzzyTime(-1) == "in the future"
assert fuzzyTime(0) == "just now" assert fuzzyTime(0) == "just now"
@@ -451,13 +452,13 @@ def testBaseCommon_FuzzyTime():
assert fuzzyTime(47336399) == "a year ago" assert fuzzyTime(47336399) == "a year ago"
assert fuzzyTime(47336400) == "2 years ago" assert fuzzyTime(47336400) == "2 years ago"
# END Test testBaseCommon_FuzzyTime # END Test testBaseCommon_fuzzyTime
@pytest.mark.core @pytest.mark.core
def testBaseCommon_RomanNumbers(): def testBaseCommon_numberToRoman():
"""Test conversion of integers to Roman numbers.""" """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(0, False) == "OOR"
assert numberToRoman(1, False) == "I" assert numberToRoman(1, False) == "I"
assert numberToRoman(2, False) == "II" assert numberToRoman(2, False) == "II"
@@ -478,14 +479,14 @@ def testBaseCommon_RomanNumbers():
assert numberToRoman(2010, False) == "MMX" assert numberToRoman(2010, False) == "MMX"
assert numberToRoman(999, True) == "cmxcix" assert numberToRoman(999, True) == "cmxcix"
# END Test testBaseCommon_RomanNumbers # END Test testBaseCommon_numberToRoman
@pytest.mark.base @pytest.mark.base
def testBaseCommon_JsonEncode(): def testBaseCommon_jsonEncode():
"""Test the jsonEncode function.""" """Test the jsonEncode function."""
# Wrong type # Wrong type
assert jsonEncode(None) == "[]" assert jsonEncode(None) == "[]" # type: ignore
# Correct types # Correct types
assert jsonEncode([1, 2]) == "[\n 1,\n 2\n]" 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 @pytest.mark.base
def testBaseCommon_ReadTextFile(monkeypatch, fncPath, ipsumText): def testBaseCommon_xmlIndent():
"""Test the xmlIndent function."""
xRoot = ET.fromstring(
"<xml>"
"<group>"
"<item>foo</item>"
"</group>"
"</xml>"
)
xmlIndent(ET.ElementTree(xRoot))
assert ET.tostring(xRoot) == (
b"<xml>\n"
b" <group>\n"
b" <item>foo</item>\n"
b" </group>\n"
b"</xml>\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.""" """Test the readTextFile function."""
testText = "\n\n".join(ipsumText) + "\n" testText = "\n\n".join(ipsumText) + "\n"
testFile = fncPath / "ipsum.txt" testFile = fncPath / "ipsum.txt"
@@ -578,11 +606,11 @@ def testBaseCommon_ReadTextFile(monkeypatch, fncPath, ipsumText):
mp.setattr("pathlib.Path.read_text", causeOSError) mp.setattr("pathlib.Path.read_text", causeOSError)
assert readTextFile(testFile) == "" assert readTextFile(testFile) == ""
# END Test testBaseCommon_ReadTextFile # END Test testBaseCommon_readTextFile
@pytest.mark.base @pytest.mark.base
def testBaseCommon_MakeFileNameSafe(): def testBaseCommon_makeFileNameSafe():
"""Test the makeFileNameSafe function.""" """Test the makeFileNameSafe function."""
assert makeFileNameSafe(" aaaa ") == "aaaa" assert makeFileNameSafe(" aaaa ") == "aaaa"
assert makeFileNameSafe("aaaa,bbbb") == "aaaabbbb" assert makeFileNameSafe("aaaa,bbbb") == "aaaabbbb"
@@ -591,11 +619,11 @@ def testBaseCommon_MakeFileNameSafe():
assert makeFileNameSafe("æøå") == "æøå" assert makeFileNameSafe("æøå") == "æøå"
assert makeFileNameSafe("Stuff œfi2⁵") == "Stuff œfi25" assert makeFileNameSafe("Stuff œfi2⁵") == "Stuff œfi25"
# END Test testBaseCommon_MakeFileNameSafe # END Test testBaseCommon_makeFileNameSafe
@pytest.mark.base @pytest.mark.base
def testBaseCommon_Sha256Sum(monkeypatch, fncPath, ipsumText): def testBaseCommon_sha256sum(monkeypatch, fncPath, ipsumText):
"""Test the sha256sum function.""" """Test the sha256sum function."""
longText = 50*(" ".join(ipsumText) + " ") longText = 50*(" ".join(ipsumText) + " ")
shortText = "This is a short file" shortText = "This is a short file"
@@ -630,16 +658,16 @@ def testBaseCommon_Sha256Sum(monkeypatch, fncPath, ipsumText):
assert sha256sum(shortFile) is None assert sha256sum(shortFile) is None
assert sha256sum(noneFile) is None assert sha256sum(noneFile) is None
# END Test testBaseCommon_Sha256Sum # END Test testBaseCommon_sha256sum
@pytest.mark.base @pytest.mark.base
def testBaseCommon_GetGuiItem(nwGUI): def testBaseCommon_getGuiItem(nwGUI):
"""Check the GUI item function.""" """Check the GUI item function."""
assert getGuiItem("gibberish") is None assert getGuiItem("gibberish") is None
assert isinstance(getGuiItem("GuiMain"), GuiMain) assert isinstance(getGuiItem("GuiMain"), GuiMain)
# END Test testBaseCommon_GetGuiItem # END Test testBaseCommon_getGuiItem
@pytest.mark.base @pytest.mark.base
@@ -675,14 +703,14 @@ def testBaseCommon_NWConfigParser(fncPath):
assert cfgParser.rdStr("main", "blabla", "stuff") == "stuff" assert cfgParser.rdStr("main", "blabla", "stuff") == "stuff"
# Read Boolean # Read Boolean
assert cfgParser.rdBool("main", "boolopt1", None) is True assert cfgParser.rdBool("main", "boolopt1", None) is True # type: ignore
assert cfgParser.rdBool("main", "boolopt2", None) is True assert cfgParser.rdBool("main", "boolopt2", None) is True # type: ignore
assert cfgParser.rdBool("main", "boolopt3", None) is True assert cfgParser.rdBool("main", "boolopt3", None) is True # type: ignore
assert cfgParser.rdBool("main", "boolopt4", None) is False assert cfgParser.rdBool("main", "boolopt4", None) is False # type: ignore
assert cfgParser.rdBool("main", "intopt1", None) is None assert cfgParser.rdBool("main", "intopt1", None) is None # type: ignore
assert cfgParser.rdBool("nope", "boolopt1", None) is None assert cfgParser.rdBool("nope", "boolopt1", None) is None # type: ignore
assert cfgParser.rdBool("main", "blabla", None) is None assert cfgParser.rdBool("main", "blabla", None) is None # type: ignore
# Read Integer # Read Integer
assert cfgParser.rdInt("main", "intopt1", 13) == 42 assert cfgParser.rdInt("main", "intopt1", 13) == 42
+3 -6
View File
@@ -30,8 +30,7 @@ from novelwriter import CONFIG, main, logger
@pytest.mark.base @pytest.mark.base
def testBaseInit_Launch(caplog, monkeypatch, fncPath): def testBaseInit_Launch(caplog, monkeypatch, fncPath):
"""Check launching the main GUI. """Check launching the main GUI."""
"""
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
# TestMode Launch # TestMode Launch
@@ -80,8 +79,7 @@ def testBaseInit_Launch(caplog, monkeypatch, fncPath):
@pytest.mark.base @pytest.mark.base
def testBaseInit_Options(monkeypatch, fncPath): 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("novelwriter.guimain.GuiMain", MockGuiMain)
monkeypatch.setattr(sys, "argv", [ monkeypatch.setattr(sys, "argv", [
"novelWriter.py", "--testmode", f"--config={fncPath}", f"--data={fncPath}" "novelWriter.py", "--testmode", f"--config={fncPath}", f"--data={fncPath}"
@@ -146,8 +144,7 @@ def testBaseInit_Options(monkeypatch, fncPath):
@pytest.mark.base @pytest.mark.base
def testBaseInit_Imports(caplog, monkeypatch, fncPath): def testBaseInit_Imports(caplog, monkeypatch, fncPath):
"""Check import error handling. """Check import error handling."""
"""
monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain) monkeypatch.setattr("novelwriter.guimain.GuiMain", MockGuiMain)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *a: None) monkeypatch.setattr("PyQt5.QtWidgets.QApplication.__init__", lambda *a: None)
monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *a: 0) monkeypatch.setattr("PyQt5.QtWidgets.QApplication.exec_", lambda *a: 0)
+187
View File
@@ -0,0 +1,187 @@
"""
novelWriter SharedData Class Tester
=====================================
This file is a part of novelWriter
Copyright 20182023, 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 <https://www.gnu.org/licenses/>.
"""
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<br><b>Exception</b>: 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
+2 -2
View File
@@ -220,7 +220,7 @@ def testCoreBuildSettings_BuildValues():
@pytest.mark.core @pytest.mark.core
def testCoreBuildSettings_Filters(mockGUI, fncPath: Path, mockRnd): def testCoreBuildSettings_Filters(mockGUI, fncPath: Path, mockRnd):
"""Test filters for project items.""" """Test filters for project items."""
project = NWProject(mockGUI) project = NWProject()
buildTestProject(project, fncPath) buildTestProject(project, fncPath)
build = BuildSettings() build = BuildSettings()
@@ -368,7 +368,7 @@ def testCoreBuildSettings_Filters(mockGUI, fncPath: Path, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreBuildSettings_Collection(monkeypatch, mockGUI, fncPath: Path, mockRnd): def testCoreBuildSettings_Collection(monkeypatch, mockGUI, fncPath: Path, mockRnd):
"""Test the collections class for builds.""" """Test the collections class for builds."""
project = NWProject(mockGUI) project = NWProject()
buildTestProject(project, fncPath) buildTestProject(project, fncPath)
buildsFile = project.storage.getMetaFile(nwFiles.BUILDS_FILE) buildsFile = project.storage.getMetaFile(nwFiles.BUILDS_FILE)
assert isinstance(buildsFile, Path) assert isinstance(buildsFile, Path)
+13 -13
View File
@@ -38,7 +38,7 @@ from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter, Pr
@pytest.mark.core @pytest.mark.core
def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ipsumText): def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ipsumText):
"""Test the DocMerger utility.""" """Test the DocMerger utility."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
@@ -126,7 +126,7 @@ def testCoreTools_DocMerger(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd, ip
@pytest.mark.core @pytest.mark.core
def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText): def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText):
"""Test the DocSplitter utility.""" """Test the DocSplitter utility."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
@@ -265,7 +265,7 @@ def testCoreTools_DocSplitter(monkeypatch, mockGUI, fncPath, mockRnd, ipsumText)
@pytest.mark.core @pytest.mark.core
def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd): def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd):
"""Test the DocDuplicator utility.""" """Test the DocDuplicator utility."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
@@ -290,7 +290,7 @@ def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd):
assert list(dup.duplicate([C.hSceneDoc])) == [ assert list(dup.duplicate([C.hSceneDoc])) == [
("0000000000010", C.hSceneDoc), # The Scene ("0000000000010", C.hSceneDoc), # The Scene
] ]
assert theProject.tree._treeOrder == [ assert theProject.tree._order == [
C.hNovelRoot, C.hPlotRoot, C.hCharRoot, C.hWorldRoot, C.hNovelRoot, C.hPlotRoot, C.hCharRoot, C.hWorldRoot,
C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000010",
@@ -311,7 +311,7 @@ def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd):
("0000000000012", None), # The Chapter ("0000000000012", None), # The Chapter
("0000000000013", None), # The Scene ("0000000000013", None), # The Scene
] ]
assert theProject.tree._treeOrder == [ assert theProject.tree._order == [
C.hNovelRoot, C.hPlotRoot, C.hCharRoot, C.hWorldRoot, C.hNovelRoot, C.hPlotRoot, C.hCharRoot, C.hWorldRoot,
C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000010",
@@ -342,7 +342,7 @@ def testCoreTools_DocDuplicator(mockGUI, fncPath, tstPaths, mockRnd):
("0000000000017", None), # The Chapter ("0000000000017", None), # The Chapter
("0000000000018", None), # The Scene ("0000000000018", None), # The Scene
] ]
assert theProject.tree._treeOrder == [ assert theProject.tree._order == [
C.hNovelRoot, C.hPlotRoot, C.hCharRoot, C.hWorldRoot, C.hNovelRoot, C.hPlotRoot, C.hCharRoot, C.hWorldRoot,
C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000010",
@@ -410,7 +410,7 @@ def testCoreTools_NewMinimal(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd):
testFile = tstPaths.outDir / "coreTools_NewMinimal_nwProject.nwx" testFile = tstPaths.outDir / "coreTools_NewMinimal_nwProject.nwx"
compFile = tstPaths.refDir / "coreTools_NewMinimal_nwProject.nwx" compFile = tstPaths.refDir / "coreTools_NewMinimal_nwProject.nwx"
projBuild = ProjectBuilder(mockGUI) projBuild = ProjectBuilder()
# Setting no data should fail # Setting no data should fail
assert projBuild.buildProject({}) is False assert projBuild.buildProject({}) is False
@@ -432,7 +432,7 @@ def testCoreTools_NewMinimal(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd):
@pytest.mark.core @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. """Create a new project from a project wizard dictionary.
Custom type with chapters and scenes. Custom type with chapters and scenes.
""" """
@@ -460,7 +460,7 @@ def testCoreTools_NewCustomA(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd):
"numScenes": 3, "numScenes": 3,
} }
projBuild = ProjectBuilder(mockGUI) projBuild = ProjectBuilder()
assert projBuild.buildProject(projData) is True assert projBuild.buildProject(projData) is True
copyfile(projFile, testFile) copyfile(projFile, testFile)
@@ -470,7 +470,7 @@ def testCoreTools_NewCustomA(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd):
@pytest.mark.core @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. """Create a new project from a project wizard dictionary.
Custom type without chapters, but with scenes. Custom type without chapters, but with scenes.
""" """
@@ -498,7 +498,7 @@ def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd):
"numScenes": 6, "numScenes": 6,
} }
projBuild = ProjectBuilder(mockGUI) projBuild = ProjectBuilder()
assert projBuild.buildProject(projData) is True assert projBuild.buildProject(projData) is True
copyfile(projFile, testFile) copyfile(projFile, testFile)
@@ -508,7 +508,7 @@ def testCoreTools_NewCustomB(monkeypatch, fncPath, tstPaths, mockGUI, mockRnd):
@pytest.mark.core @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 """Check that we can create a new project can be created from the
provided sample project via a zip file. provided sample project via a zip file.
""" """
@@ -522,7 +522,7 @@ def testCoreTools_NewSample(monkeypatch, fncPath, tstPaths, mockGUI):
"popCustom": False, "popCustom": False,
} }
projBuild = ProjectBuilder(mockGUI) projBuild = ProjectBuilder()
# No path set # No path set
assert projBuild.buildProject({"popSample": True}) is False assert projBuild.buildProject({"popSample": True}) is False
+8 -8
View File
@@ -76,7 +76,7 @@ BUILD_CONF = {
@pytest.mark.core @pytest.mark.core
def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
"""Test building an open document manuscript.""" """Test building an open document manuscript."""
project = NWProject(mockGUI) project = NWProject()
project.openProject(prjLipsum) project.openProject(prjLipsum)
build = BuildSettings() build = BuildSettings()
@@ -180,7 +180,7 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
@pytest.mark.core @pytest.mark.core
def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
"""Test building an HTML manuscript.""" """Test building an HTML manuscript."""
project = NWProject(mockGUI) project = NWProject()
project.openProject(prjLipsum) project.openProject(prjLipsum)
build = BuildSettings() build = BuildSettings()
@@ -250,7 +250,7 @@ def testCoreDocBuild_HTML(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
@pytest.mark.core @pytest.mark.core
def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
"""Test building an Markdown manuscript.""" """Test building an Markdown manuscript."""
project = NWProject(mockGUI) project = NWProject()
project.openProject(prjLipsum) project.openProject(prjLipsum)
build = BuildSettings() build = BuildSettings()
@@ -320,7 +320,7 @@ def testCoreDocBuild_Markdown(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths
@pytest.mark.core @pytest.mark.core
def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths): def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
"""Test building a NWD manuscript.""" """Test building a NWD manuscript."""
project = NWProject(mockGUI) project = NWProject()
project.openProject(prjLipsum) project.openProject(prjLipsum)
build = BuildSettings() build = BuildSettings()
@@ -390,7 +390,7 @@ def testCoreDocBuild_NWD(monkeypatch, mockGUI, prjLipsum, fncPath, tstPaths):
@pytest.mark.core @pytest.mark.core
def testCoreDocBuild_Custom(mockGUI, fncPath: Path): def testCoreDocBuild_Custom(mockGUI, fncPath: Path):
"""Test custom builds and some error handling.""" """Test custom builds and some error handling."""
project = NWProject(mockGUI) project = NWProject()
buildTestProject(project, fncPath) buildTestProject(project, fncPath)
build = BuildSettings() build = BuildSettings()
@@ -421,8 +421,8 @@ def testCoreDocBuild_Custom(mockGUI, fncPath: Path):
# Add an invalid item to the project # Add an invalid item to the project
nHandle = "0123456789def" nHandle = "0123456789def"
project.tree._treeOrder.append(nHandle) project.tree._order.append(nHandle)
project.tree._projTree[nHandle] = None # type: ignore project.tree._tree[nHandle] = None # type: ignore
docBuild.queueAll() docBuild.queueAll()
assert len(docBuild) == 8 assert len(docBuild) == 8
@@ -455,7 +455,7 @@ def testCoreDocBuild_Custom(mockGUI, fncPath: Path):
@pytest.mark.core @pytest.mark.core
def testCoreDocBuild_IterBuild(mockGUI, fncPath: Path, mockRnd): def testCoreDocBuild_IterBuild(mockGUI, fncPath: Path, mockRnd):
"""Test iter build wrapper.""" """Test iter build wrapper."""
project = NWProject(mockGUI) project = NWProject()
buildTestProject(project, fncPath) buildTestProject(project, fncPath)
build = BuildSettings() build = BuildSettings()
build.unpack(BUILD_CONF) build.unpack(BUILD_CONF)
+2 -2
View File
@@ -32,7 +32,7 @@ from novelwriter.core.document import NWDocument
@pytest.mark.core @pytest.mark.core
def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd): def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
"""Test loading and saving a document with the NWDocument class.""" """Test loading and saving a document with the NWDocument class."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
@@ -173,7 +173,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, fncPath, mockRnd):
def testCoreDocument_Methods(mockGUI, fncPath, mockRnd): def testCoreDocument_Methods(mockGUI, fncPath, mockRnd):
"""Test other methods of the NWDocument class. """Test other methods of the NWDocument class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
+9 -9
View File
@@ -42,7 +42,7 @@ def testCoreIndex_LoadSave(monkeypatch, prjLipsum, mockGUI, tstPaths):
testFile = tstPaths.outDir / "coreIndex_LoadSave_tagsIndex.json" testFile = tstPaths.outDir / "coreIndex_LoadSave_tagsIndex.json"
compFile = tstPaths.refDir / "coreIndex_LoadSave_tagsIndex.json" compFile = tstPaths.refDir / "coreIndex_LoadSave_tagsIndex.json"
theProject = NWProject(mockGUI) theProject = NWProject()
assert theProject.openProject(prjLipsum) assert theProject.openProject(prjLipsum)
theIndex = NWIndex(theProject) theIndex = NWIndex(theProject)
@@ -155,7 +155,7 @@ def testCoreIndex_LoadSave(monkeypatch, prjLipsum, mockGUI, tstPaths):
@pytest.mark.core @pytest.mark.core
def testCoreIndex_ScanThis(mockGUI): def testCoreIndex_ScanThis(mockGUI):
"""Test the tag scanner function scanThis.""" """Test the tag scanner function scanThis."""
theProject = NWProject(mockGUI) theProject = NWProject()
theIndex = theProject.index theIndex = theProject.index
isValid, theBits, thePos = theIndex.scanThis("tag: this, and this") isValid, theBits, thePos = theIndex.scanThis("tag: this, and this")
@@ -204,7 +204,7 @@ def testCoreIndex_ScanThis(mockGUI):
def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd): def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd):
"""Test the tag checker function checkThese. """Test the tag checker function checkThese.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
theIndex = theProject.index theIndex = theProject.index
@@ -281,7 +281,7 @@ def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd): def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
"""Check the index text scanner.""" """Check the index text scanner."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
theIndex = theProject.index theIndex = theProject.index
@@ -502,7 +502,7 @@ def testCoreIndex_ScanText(mockGUI, fncPath, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd): def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
"""Check the index data extraction functions.""" """Check the index data extraction functions."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) 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 # 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)] == [ assert [(h, t) for h, t, _ in theIndex._itemIndex.iterNovelStructure(skipExcl=False)] == [
(C.hTitlePage, "T0001"), (C.hTitlePage, "T0001"),
(C.hChapterDoc, "T0001"), (C.hChapterDoc, "T0001"),
@@ -738,7 +738,7 @@ def testCoreIndex_ExtractData(mockGUI, fncPath, mockRnd):
(sHandle, "T0001"), (sHandle, "T0001"),
(tHandle, "T0001"), (tHandle, "T0001"),
] ]
theProject.tree._treeOrder.remove("0000000000000") theProject.tree._order.remove("0000000000000")
# Extract stats # Extract stats
assert theIndex.getNovelWordCount(skipExcl=False) == 43 assert theIndex.getNovelWordCount(skipExcl=False) == 43
@@ -941,7 +941,7 @@ def testCoreIndex_TagsIndex():
@pytest.mark.core @pytest.mark.core
def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd): def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
"""Check the ItemIndex class.""" """Check the ItemIndex class."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
theProject.index.clearIndex() theProject.index.clearIndex()
@@ -1077,7 +1077,7 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
assert nStruct[0][0] == uHandle assert nStruct[0][0] == uHandle
# Inject garbage into tree # Inject garbage into tree
theProject.tree._treeOrder.append("stuff") theProject.tree._order.append("stuff")
nStruct = list(itemIndex.iterNovelStructure()) nStruct = list(itemIndex.iterNovelStructure())
assert len(nStruct) == 4 assert len(nStruct) == 4
assert nStruct[0][0] == nHandle assert nStruct[0][0] == nHandle
+7 -7
View File
@@ -34,7 +34,7 @@ from novelwriter.core.project import NWProject
@pytest.mark.core @pytest.mark.core
def testCoreItem_Setters(mockGUI, mockRnd, fncPath): def testCoreItem_Setters(mockGUI, mockRnd, fncPath):
"""Test all the simple setters for the NWItem class.""" """Test all the simple setters for the NWItem class."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
theItem = NWItem(theProject, "0000000000000") theItem = NWItem(theProject, "0000000000000")
@@ -185,7 +185,7 @@ def testCoreItem_Setters(mockGUI, mockRnd, fncPath):
@pytest.mark.core @pytest.mark.core
def testCoreItem_Methods(mockGUI, mockRnd, fncPath): def testCoreItem_Methods(mockGUI, mockRnd, fncPath):
"""Test the simple methods of the NWItem class.""" """Test the simple methods of the NWItem class."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
theItem = NWItem(theProject, "0000000000000") theItem = NWItem(theProject, "0000000000000")
@@ -333,7 +333,7 @@ def testCoreItem_TypeSetter(mockGUI):
"""Test the setter for all the nwItemType values for the NWItem """Test the setter for all the nwItemType values for the NWItem
class. class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
theItem = NWItem(theProject, "0000000000000") theItem = NWItem(theProject, "0000000000000")
# Type # Type
@@ -362,7 +362,7 @@ def testCoreItem_ClassSetter(mockGUI):
"""Test the setter for all the nwItemClass values for the NWItem """Test the setter for all the nwItemClass values for the NWItem
class. class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
theItem = NWItem(theProject, "0000000000000") theItem = NWItem(theProject, "0000000000000")
# Class # Class
@@ -449,7 +449,7 @@ def testCoreItem_LayoutSetter(mockGUI):
"""Test the setter for all the nwItemLayout values for the NWItem """Test the setter for all the nwItemLayout values for the NWItem
class. class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
theItem = NWItem(theProject, "0000000000000") theItem = NWItem(theProject, "0000000000000")
# Faulty Layouts # Faulty Layouts
@@ -477,7 +477,7 @@ def testCoreItem_LayoutSetter(mockGUI):
def testCoreItem_ClassDefaults(mockGUI): def testCoreItem_ClassDefaults(mockGUI):
"""Test the setter for the default values. """Test the setter for the default values.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
theItem = NWItem(theProject, "0000000000000") theItem = NWItem(theProject, "0000000000000")
# Root items should not have their class updated # Root items should not have their class updated
@@ -532,7 +532,7 @@ def testCoreItem_ClassDefaults(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd): def testCoreItem_PackUnpack(mockGUI, caplog, mockRnd):
"""Test packing and unpacking entries for the NWItem class.""" """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.itemStatus.write(None, "New", (100, 100, 100))
theProject.data.itemImport.write(None, "New", (100, 100, 100)) theProject.data.itemImport.write(None, "New", (100, 100, 100))
+2 -2
View File
@@ -33,7 +33,7 @@ from novelwriter.gui.noveltree import NovelTreeColumn
@pytest.mark.core @pytest.mark.core
def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath): def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath):
"""Test loading and saving from the OptionState class.""" """Test loading and saving from the OptionState class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theOpts = OptionState(theProject) theOpts = OptionState(theProject)
metaDir = fncPath / "meta" metaDir = fncPath / "meta"
@@ -106,7 +106,7 @@ def testCoreOptions_LoadSave(monkeypatch, mockGUI, fncPath):
@pytest.mark.core @pytest.mark.core
def testCoreOptions_SetGet(mockGUI): def testCoreOptions_SetGet(mockGUI):
"""Test setting and getting values from the OptionState class.""" """Test setting and getting values from the OptionState class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theOpts = OptionState(theProject) theOpts = OptionState(theProject)
nwColHidden = NovelTreeColumn.HIDDEN nwColHidden = NovelTreeColumn.HIDDEN
+30 -26
View File
@@ -19,6 +19,7 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>. along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
from PyQt5.QtWidgets import QMessageBox
import pytest import pytest
from shutil import copyfile from shutil import copyfile
@@ -27,7 +28,7 @@ from zipfile import ZipFile
from mocked import causeOSError from mocked import causeOSError
from tools import C, cmpFiles, buildTestProject, XML_IGNORE from tools import C, cmpFiles, buildTestProject, XML_IGNORE
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemClass from novelwriter.enum import nwItemClass
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.tree import NWTree from novelwriter.core.tree import NWTree
@@ -44,7 +45,7 @@ def testCoreProject_NewRoot(fncPath, tstPaths, mockGUI, mockRnd):
testFile = tstPaths.outDir / "coreProject_NewRoot_nwProject.nwx" testFile = tstPaths.outDir / "coreProject_NewRoot_nwProject.nwx"
compFile = tstPaths.refDir / "coreProject_NewRoot_nwProject.nwx" compFile = tstPaths.refDir / "coreProject_NewRoot_nwProject.nwx"
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
@@ -94,7 +95,7 @@ def testCoreProject_NewFileFolder(monkeypatch, fncPath, tstPaths, mockGUI, mockR
testFile = tstPaths.outDir / "coreProject_NewFileFolder_nwProject.nwx" testFile = tstPaths.outDir / "coreProject_NewFileFolder_nwProject.nwx"
compFile = tstPaths.refDir / "coreProject_NewFileFolder_nwProject.nwx" compFile = tstPaths.refDir / "coreProject_NewFileFolder_nwProject.nwx"
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
@@ -163,7 +164,7 @@ def testCoreProject_NewFileFolder(monkeypatch, fncPath, tstPaths, mockGUI, mockR
@pytest.mark.core @pytest.mark.core
def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
"""Test opening a project.""" """Test opening a project."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
@@ -176,7 +177,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK
assert theProject.storage.writeLockFile() is True assert theProject.storage.writeLockFile() is True
assert theProject.openProject(fncPath) is False 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) # Fail to read lockfile (which still opens the project)
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@@ -189,9 +190,9 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
# Force open with lockfile # Force open with lockfile
theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK theProject.storage._lockFilePath = fncPath / nwFiles.PROJ_LOCK
assert theProject.storage.writeLockFile() is True assert theProject.storage.writeLockFile() is True
assert theProject.openProject(fncPath, overrideLock=True) is True assert theProject.openProject(fncPath, clearLock=True) is True
theProject.closeProject() theProject.closeProject()
assert theProject.getLockStatus() is None assert theProject.lockStatus is None
# Fail getting xml reader # Fail getting xml reader
with monkeypatch.context() as mp: 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, "read", lambda *a: False)
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.NOT_NWX_FILE)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.NOT_NWX_FILE))
assert theProject.openProject(fncPath) is False 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 # Unknown project file version
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "read", lambda *a: False) mp.setattr(ProjectXMLReader, "read", lambda *a: False)
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.UNKNOWN_VERSION)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.UNKNOWN_VERSION))
assert theProject.openProject(fncPath) is False 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 # Other parse error
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "read", lambda *a: False) mp.setattr(ProjectXMLReader, "read", lambda *a: False)
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.CANNOT_PARSE)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.CANNOT_PARSE))
assert theProject.openProject(fncPath) is False 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 # Won't convert legacy file
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY)) 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 theProject.openProject(fncPath) is False
assert "The file format of your project is about to be" in mockGUI.lastQuestion lastMsg = SHARED.alert.logMessage if SHARED.alert else ""
mockGUI.askResponse = True assert "The file format of your project is about to be" in lastMsg
# Won't open project from newer version # Won't open project from newer version
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999)) 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 theProject.openProject(fncPath) is False
assert "This project was saved by a newer version" in mockGUI.lastQuestion lastMsg = SHARED.alert.logMessage if SHARED.alert else ""
mockGUI.askResponse = True assert "This project was saved by a newer version" in lastMsg
# Fail checking items should still pass # Fail checking items should still pass
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
@@ -246,10 +250,10 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY)) mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY))
mp.setattr("novelwriter.core.index.NWIndex.loadIndex", lambda *a: True) mp.setattr("novelwriter.core.index.NWIndex.loadIndex", lambda *a: True)
mockGUI.askResponse = True
theProject.index._indexBroken = True theProject.index._indexBroken = True
assert theProject.openProject(fncPath) is 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 assert theProject.index._indexBroken is False
theProject.closeProject() theProject.closeProject()
@@ -260,7 +264,7 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncPath): def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncPath):
"""Test saving a project.""" """Test saving a project."""
theProject = NWProject(mockGUI) theProject = NWProject()
# Nothing to save # Nothing to save
assert theProject.saveProject() is False assert theProject.saveProject() is False
@@ -289,7 +293,7 @@ def testCoreProject_Save(monkeypatch, mockGUI, mockRnd, fncPath):
@pytest.mark.core @pytest.mark.core
def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd): def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
"""Test helper functions for the project folder.""" """Test helper functions for the project folder."""
theProject = NWProject(mockGUI) theProject = NWProject()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
# Storage Objects # Storage Objects
@@ -323,7 +327,7 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
assert theProject.tree.handles() == newOrder assert theProject.tree.handles() == newOrder
# Add a non-existing item # 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 # Add an item with a non-existent parent
nHandle = theProject.newFile("Test File", C.hChapterDir) nHandle = theProject.newFile("Test File", C.hChapterDir)
@@ -331,7 +335,7 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
assert theProject.tree[nHandle].itemParent == "cba9876543210" assert theProject.tree[nHandle].itemParent == "cba9876543210"
retOrder = [] retOrder = []
for tItem in theProject.getProjectItems(): for tItem in theProject.iterProjectItems():
retOrder.append(tItem.itemHandle) retOrder.append(tItem.itemHandle)
assert retOrder == [ assert retOrder == [
@@ -353,7 +357,7 @@ def testCoreProject_AccessItems(mockGUI, fncPath, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd): def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd):
"""Test the status and importance flag handling.""" """Test the status and importance flag handling."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
@@ -462,7 +466,7 @@ def testCoreProject_StatusImport(mockGUI, fncPath, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd): def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
"""Test other project class methods and functions.""" """Test other project class methods and functions."""
theProject = NWProject(mockGUI) theProject = NWProject()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
# Project Name # Project Name
@@ -482,7 +486,7 @@ def testCoreProject_Methods(monkeypatch, mockGUI, fncPath, mockRnd):
theProject._session._start = 1600000000 theProject._session._start = 1600000000
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.project.time", lambda: 1600005600) mp.setattr("novelwriter.core.project.time", lambda: 1600005600)
assert theProject.getCurrentEditTime() == 6834 assert theProject.currentEditTime == 6834
# Trash folder # Trash folder
# Should create on first call, and just returned on later calls # 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 backup file and checks that the project XML file is identical to
the original file. the original file.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
# No Project # No Project
assert theProject.backupProject(doNotify=False) is False assert theProject.backupProject(doNotify=False) is False
+1 -1
View File
@@ -35,7 +35,7 @@ from novelwriter.core.sessions import NWSessionLog
@pytest.mark.core @pytest.mark.core
def testCoreSessions_Main(monkeypatch, mockGUI, fncPath): def testCoreSessions_Main(monkeypatch, mockGUI, fncPath):
"""Test log file handling of the NWSessionLog class.""" """Test log file handling of the NWSessionLog class."""
project = NWProject(mockGUI) project = NWProject()
buildTestProject(project, fncPath) buildTestProject(project, fncPath)
logFile = project.storage.getMetaFile(nwFiles.SESS_FILE) logFile = project.storage.getMetaFile(nwFiles.SESS_FILE)
+3 -3
View File
@@ -36,7 +36,7 @@ from novelwriter.core.spellcheck import FakeEnchant, NWSpellEnchant, UserDiction
@pytest.mark.core @pytest.mark.core
def testCoreSpell_UserDictionary(monkeypatch, mockGUI, fncPath): def testCoreSpell_UserDictionary(monkeypatch, mockGUI, fncPath):
"""Test the UserDictionary class.""" """Test the UserDictionary class."""
project = NWProject(mockGUI) project = NWProject()
buildTestProject(project, fncPath) buildTestProject(project, fncPath)
# Check that there is no file before we start # Check that there is no file before we start
@@ -114,7 +114,7 @@ def testCoreSpell_UserDictionary(monkeypatch, mockGUI, fncPath):
@pytest.mark.core @pytest.mark.core
def testCoreSpell_FakeEnchant(monkeypatch, mockGUI, fncPath): def testCoreSpell_FakeEnchant(monkeypatch, mockGUI, fncPath):
"""Test the FakeEnchant spell checker fallback.""" """Test the FakeEnchant spell checker fallback."""
project = NWProject(mockGUI) project = NWProject()
buildTestProject(project, fncPath) buildTestProject(project, fncPath)
# Make package import fail # Make package import fail
@@ -149,7 +149,7 @@ def testCoreSpell_FakeEnchant(monkeypatch, mockGUI, fncPath):
@pytest.mark.core @pytest.mark.core
def testCoreSpell_Enchant(monkeypatch, mockGUI, fncPath): def testCoreSpell_Enchant(monkeypatch, mockGUI, fncPath):
"""Test the pyenchant spell checker.""" """Test the pyenchant spell checker."""
project = NWProject(mockGUI) project = NWProject()
buildTestProject(project, fncPath) buildTestProject(project, fncPath)
# Break the enchant package, and check error handling # Break the enchant package, and check error handling
+3 -3
View File
@@ -43,7 +43,7 @@ class MockProject:
@pytest.mark.core @pytest.mark.core
def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd): def testCoreStorage_OpenProjectInPlace(mockGUI, fncPath, mockRnd):
"""Test opening a project in a folder.""" """Test opening a project in a folder."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
theProject.closeProject() theProject.closeProject()
@@ -180,7 +180,7 @@ def testCoreStorage_ZipIt(monkeypatch, mockGUI, fncPath, tstPaths, mockRnd):
"""Test making a zip archive of a project.""" """Test making a zip archive of a project."""
zipFile = tstPaths.tmpDir / "project.zip" zipFile = tstPaths.tmpDir / "project.zip"
theProject = NWProject(mockGUI) theProject = NWProject()
storage = theProject.storage storage = theProject.storage
assert storage.zipIt(zipFile) is False assert storage.zipIt(zipFile) is False
@@ -365,7 +365,7 @@ def testCoreStorage_DeprecatedFiles(monkeypatch, fncPath):
@pytest.mark.core @pytest.mark.core
def testCoreStorage_OldFormatConvert(monkeypatch, mockGUI, fncPath): def testCoreStorage_OldFormatConvert(monkeypatch, mockGUI, fncPath):
"""Test cleanup of deprecated files that needs to be converted.""" """Test cleanup of deprecated files that needs to be converted."""
project = NWProject(mockGUI) project = NWProject()
buildTestProject(project, fncPath) buildTestProject(project, fncPath)
legacy = _LegacyStorage(project) legacy = _LegacyStorage(project)
+6 -6
View File
@@ -31,7 +31,7 @@ from novelwriter.core.project import NWProject
def testCoreToHtml_ConvertFormat(mockGUI): def testCoreToHtml_ConvertFormat(mockGUI):
"""Test the tokenizer and converter chain using the ToHtml class. """Test the tokenizer and converter chain using the ToHtml class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
theHtml = ToHtml(theProject) theHtml = ToHtml(theProject)
# Novel Files Headers # Novel Files Headers
@@ -233,7 +233,7 @@ def testCoreToHtml_ConvertFormat(mockGUI):
def testCoreToHtml_ConvertDirect(mockGUI): def testCoreToHtml_ConvertDirect(mockGUI):
"""Test the converter directly using the ToHtml class. """Test the converter directly using the ToHtml class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
theHtml = ToHtml(theProject) theHtml = ToHtml(theProject)
theHtml._isNovel = True theHtml._isNovel = True
@@ -380,7 +380,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
def testCoreToHtml_SpecialCases(mockGUI): def testCoreToHtml_SpecialCases(mockGUI):
"""Test some special cases that have caused errors in the past. """Test some special cases that have caused errors in the past.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
theHtml = ToHtml(theProject) theHtml = ToHtml(theProject)
theHtml._isNovel = True theHtml._isNovel = True
@@ -454,7 +454,7 @@ def testCoreToHtml_SpecialCases(mockGUI):
def testCoreToHtml_Complex(mockGUI, fncPath): def testCoreToHtml_Complex(mockGUI, fncPath):
"""Test the save method of the ToHtml class. """Test the save method of the ToHtml class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
theHtml = ToHtml(theProject) theHtml = ToHtml(theProject)
theHtml._isNovel = True theHtml._isNovel = True
@@ -549,7 +549,7 @@ def testCoreToHtml_Complex(mockGUI, fncPath):
def testCoreToHtml_Methods(mockGUI): def testCoreToHtml_Methods(mockGUI):
"""Test all the other methods of the ToHtml class. """Test all the other methods of the ToHtml class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
theHtml = ToHtml(theProject) theHtml = ToHtml(theProject)
theHtml.setKeepMarkdown(True) theHtml.setKeepMarkdown(True)
@@ -609,7 +609,7 @@ def testCoreToHtml_Methods(mockGUI):
def testCoreToHtml_Format(mockGUI): def testCoreToHtml_Format(mockGUI):
"""Test all the formatters for the ToHtml class. """Test all the formatters for the ToHtml class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
theHtml = ToHtml(theProject) theHtml = ToHtml(theProject)
# Export Mode # Export Mode
+8 -8
View File
@@ -36,7 +36,7 @@ class BareTokenizer(Tokenizer):
@pytest.mark.core @pytest.mark.core
def testCoreToken_Setters(mockGUI): def testCoreToken_Setters(mockGUI):
"""Test all the setters for the Tokenizer class.""" """Test all the setters for the Tokenizer class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theToken = BareTokenizer(theProject) theToken = BareTokenizer(theProject)
# Verify defaults # Verify defaults
@@ -133,7 +133,7 @@ def testCoreToken_Setters(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath): def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath):
"""Test handling files and text in the Tokenizer class.""" """Test handling files and text in the Tokenizer class."""
theProject = NWProject(mockGUI) theProject = NWProject()
mockRnd.reset() mockRnd.reset()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
@@ -231,7 +231,7 @@ def testCoreToken_StripEscape():
@pytest.mark.core @pytest.mark.core
def testCoreToken_HeaderFormat(mockGUI): def testCoreToken_HeaderFormat(mockGUI):
"""Test the tokenization of header formats in the Tokenizer class.""" """Test the tokenization of header formats in the Tokenizer class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theToken = BareTokenizer(theProject) theToken = BareTokenizer(theProject)
theToken.setKeepMarkdown(True) theToken.setKeepMarkdown(True)
@@ -434,7 +434,7 @@ def testCoreToken_HeaderFormat(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToken_MetaFormat(mockGUI): def testCoreToken_MetaFormat(mockGUI):
"""Test the tokenization of meta formats in the Tokenizer class.""" """Test the tokenization of meta formats in the Tokenizer class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theToken = BareTokenizer(theProject) theToken = BareTokenizer(theProject)
theToken.setKeepMarkdown(True) theToken.setKeepMarkdown(True)
@@ -502,7 +502,7 @@ def testCoreToken_MetaFormat(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToken_MarginFormat(mockGUI): def testCoreToken_MarginFormat(mockGUI):
"""Test the tokenization of margin formats in the Tokenizer class.""" """Test the tokenization of margin formats in the Tokenizer class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theToken = BareTokenizer(theProject) theToken = BareTokenizer(theProject)
theToken.setKeepMarkdown(True) theToken.setKeepMarkdown(True)
@@ -556,7 +556,7 @@ def testCoreToken_MarginFormat(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToken_TextFormat(mockGUI): def testCoreToken_TextFormat(mockGUI):
"""Test the tokenization of text formats in the Tokenizer class.""" """Test the tokenization of text formats in the Tokenizer class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theToken = BareTokenizer(theProject) theToken = BareTokenizer(theProject)
theToken.setKeepMarkdown(True) theToken.setKeepMarkdown(True)
@@ -677,7 +677,7 @@ def testCoreToken_TextFormat(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToken_SpecialFormat(mockGUI): def testCoreToken_SpecialFormat(mockGUI):
"""Test the tokenization of special formats in the Tokenizer class.""" """Test the tokenization of special formats in the Tokenizer class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theToken = BareTokenizer(theProject) theToken = BareTokenizer(theProject)
theToken._isNovel = True theToken._isNovel = True
@@ -879,7 +879,7 @@ def testCoreToken_SpecialFormat(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToken_ProcessHeaders(mockGUI): def testCoreToken_ProcessHeaders(mockGUI):
"""Test the header and page parser of the Tokenizer class.""" """Test the header and page parser of the Tokenizer class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theProject.data.setLanguage("en") theProject.data.setLanguage("en")
theProject._loadProjectLocalisation() theProject._loadProjectLocalisation()
theToken = BareTokenizer(theProject) theToken = BareTokenizer(theProject)
+4 -4
View File
@@ -32,7 +32,7 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
"""Test the tokenizer and converter chain using the ToMarkdown """Test the tokenizer and converter chain using the ToMarkdown
class. class.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
theMD = ToMarkdown(theProject) theMD = ToMarkdown(theProject)
# Headers # Headers
@@ -159,7 +159,7 @@ def testCoreToMarkdown_ConvertFormat(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToMarkdown_ConvertDirect(mockGUI): def testCoreToMarkdown_ConvertDirect(mockGUI):
"""Test the converter directly using the ToMarkdown class.""" """Test the converter directly using the ToMarkdown class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theMD = ToMarkdown(theProject) theMD = ToMarkdown(theProject)
theMD._isNovel = True theMD._isNovel = True
@@ -209,7 +209,7 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToMarkdown_Complex(mockGUI, fncPath): def testCoreToMarkdown_Complex(mockGUI, fncPath):
"""Test the save method of the ToMarkdown class.""" """Test the save method of the ToMarkdown class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theMD = ToMarkdown(theProject) theMD = ToMarkdown(theProject)
theMD._isNovel = True theMD._isNovel = True
@@ -261,7 +261,7 @@ def testCoreToMarkdown_Complex(mockGUI, fncPath):
@pytest.mark.core @pytest.mark.core
def testCoreToMarkdown_Format(mockGUI): def testCoreToMarkdown_Format(mockGUI):
"""Test all the formatters for the ToMarkdown class.""" """Test all the formatters for the ToMarkdown class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theMD = ToMarkdown(theProject) theMD = ToMarkdown(theProject)
assert theMD._formatKeywords("", theMD.A_NONE) == "" assert theMD._formatKeywords("", theMD.A_NONE) == ""
+7 -7
View File
@@ -53,7 +53,7 @@ def xmlToText(xElem):
@pytest.mark.core @pytest.mark.core
def testCoreToOdt_Init(mockGUI): def testCoreToOdt_Init(mockGUI):
"""Test initialisation of the ODT document.""" """Test initialisation of the ODT document."""
theProject = NWProject(mockGUI) theProject = NWProject()
# Flat Doc # Flat Doc
# ======== # ========
@@ -108,7 +108,7 @@ def testCoreToOdt_Init(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToOdt_TextFormatting(mockGUI): def testCoreToOdt_TextFormatting(mockGUI):
"""Test formatting of paragraphs.""" """Test formatting of paragraphs."""
theProject = NWProject(mockGUI) theProject = NWProject()
theDoc = ToOdt(theProject, isFlat=True) theDoc = ToOdt(theProject, isFlat=True)
theDoc.initDocument() theDoc.initDocument()
@@ -242,7 +242,7 @@ def testCoreToOdt_TextFormatting(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToOdt_Convert(mockGUI): def testCoreToOdt_Convert(mockGUI):
"""Test the converter of the ToOdt class.""" """Test the converter of the ToOdt class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theDoc = ToOdt(theProject, isFlat=True) theDoc = ToOdt(theProject, isFlat=True)
theDoc._isNovel = True theDoc._isNovel = True
@@ -573,7 +573,7 @@ def testCoreToOdt_ConvertDirect(mockGUI):
"""Test the converter directly using the ToOdt class to reach some """Test the converter directly using the ToOdt class to reach some
otherwise hard to reach conditions. otherwise hard to reach conditions.
""" """
theProject = NWProject(mockGUI) theProject = NWProject()
theDoc = ToOdt(theProject, isFlat=True) theDoc = ToOdt(theProject, isFlat=True)
theDoc._isNovel = True theDoc._isNovel = True
@@ -626,7 +626,7 @@ def testCoreToOdt_ConvertDirect(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreToOdt_SaveFlat(mockGUI, fncPath, tstPaths): def testCoreToOdt_SaveFlat(mockGUI, fncPath, tstPaths):
"""Test the document save functions.""" """Test the document save functions."""
theProject = NWProject(mockGUI) theProject = NWProject()
theProject.data.setAuthor("Jane Smith") theProject.data.setAuthor("Jane Smith")
theProject.data.setName("Test Project") theProject.data.setName("Test Project")
theProject.data.setSaveCount(1234) theProject.data.setSaveCount(1234)
@@ -668,7 +668,7 @@ def testCoreToOdt_SaveFlat(mockGUI, fncPath, tstPaths):
@pytest.mark.core @pytest.mark.core
def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths): def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths):
"""Test the document save functions.""" """Test the document save functions."""
theProject = NWProject(mockGUI) theProject = NWProject()
theProject.data.setAuthor("Jane Smith") theProject.data.setAuthor("Jane Smith")
theProject.data.setName("Test Project") theProject.data.setName("Test Project")
theProject.data.setSaveCount(1234) theProject.data.setSaveCount(1234)
@@ -745,7 +745,7 @@ def testCoreToOdt_SaveFull(mockGUI, fncPath, tstPaths):
@pytest.mark.core @pytest.mark.core
def testCoreToOdt_Format(mockGUI): def testCoreToOdt_Format(mockGUI):
"""Test the formatters for the ToOdt class.""" """Test the formatters for the ToOdt class."""
theProject = NWProject(mockGUI) theProject = NWProject()
theDoc = ToOdt(theProject, isFlat=True) theDoc = ToOdt(theProject, isFlat=True)
assert theDoc._formatSynopsis("synopsis text") == ( assert theDoc._formatSynopsis("synopsis text") == (
+23 -21
View File
@@ -39,7 +39,7 @@ from novelwriter.core.project import NWProject
@pytest.fixture(scope="function") @pytest.fixture(scope="function")
def mockItems(mockGUI, mockRnd): def mockItems(mockGUI, mockRnd):
"""Create a list of mock items.""" """Create a list of mock items."""
theProject = NWProject(mockGUI) theProject = NWProject()
itemA = NWItem(theProject, "a000000000001") itemA = NWItem(theProject, "a000000000001")
itemA._name = "Novel" itemA._name = "Novel"
@@ -112,7 +112,7 @@ def mockItems(mockGUI, mockRnd):
@pytest.mark.core @pytest.mark.core
def testCoreTree_BuildTree(mockGUI, mockItems): def testCoreTree_BuildTree(mockGUI, mockItems):
"""Test building a project tree from a list of items.""" """Test building a project tree from a list of items."""
theProject = NWProject(mockGUI) theProject = NWProject()
theTree = NWTree(theProject) theTree = NWTree(theProject)
# Check that tree is empty (calls NWTree.__bool__) # 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.append(nwItem) is True
assert theTree.updateItemData(nwItem.itemHandle) 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__) # Check that tree is not empty (calls __bool__)
assert bool(theTree) is True assert bool(theTree) is True
@@ -269,7 +269,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
@pytest.mark.core @pytest.mark.core
def testCoreTree_PackUnpack(mockGUI, mockItems): def testCoreTree_PackUnpack(mockGUI, mockItems):
"""Test packing and unpacking data.""" """Test packing and unpacking data."""
theProject = NWProject(mockGUI) theProject = NWProject()
theTree = NWTree(theProject) theTree = NWTree(theProject)
aHandles = [] aHandles = []
@@ -298,7 +298,7 @@ def testCoreTree_PackUnpack(mockGUI, mockItems):
@pytest.mark.core @pytest.mark.core
def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fncPath, mockRnd): def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fncPath, mockRnd):
"""Check the project consistency.""" """Check the project consistency."""
theProject = NWProject(mockGUI) theProject = NWProject()
buildTestProject(theProject, fncPath) buildTestProject(theProject, fncPath)
# By default, all is well # By default, all is well
@@ -354,10 +354,12 @@ def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fnc
assert itemX.itemClass == nwItemClass.NOVEL assert itemX.itemClass == nwItemClass.NOVEL
assert itemX.itemName == "[Recovered] Stuff" 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() theProject.tree.clear()
assert theProject.tree.checkConsistency("Recovered") == (4, 0) assert theProject.tree.checkConsistency("Recovered") == (4, 4)
assert len(theProject.tree) == 0 assert len(theProject.tree) == 5
nHandle = theProject.tree.findRoot(nwItemClass.NOVEL)
assert theProject.tree[nHandle].itemName == "Recovered" # type: ignore
# END Test testCoreTree_CheckConsistency # END Test testCoreTree_CheckConsistency
@@ -365,7 +367,7 @@ def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fnc
@pytest.mark.core @pytest.mark.core
def testCoreTree_Methods(mockGUI, mockItems): def testCoreTree_Methods(mockGUI, mockItems):
"""Test various class methods.""" """Test various class methods."""
theProject = NWProject(mockGUI) theProject = NWProject()
theTree = NWTree(theProject) theTree = NWTree(theProject)
for nwItem in mockItems: for nwItem in mockItems:
@@ -411,9 +413,9 @@ def testCoreTree_Methods(mockGUI, mockItems):
assert roots[3][0] == "a000000000004" assert roots[3][0] == "a000000000004"
# Add a fake item to root and check that it can handle it # 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 assert theTree.findRoot(nwItemClass.WORLD) is None
del theTree._treeRoots["0000000000000"] del theTree._roots["0000000000000"]
# Get item path # Get item path
assert theTree.getItemPath("stuff") == [] assert theTree.getItemPath("stuff") == []
@@ -446,7 +448,7 @@ def testCoreTree_Methods(mockGUI, mockItems):
def testCoreTree_MakeHandles(mockGUI): def testCoreTree_MakeHandles(mockGUI):
"""Test generating item handles.""" """Test generating item handles."""
random.seed(42) random.seed(42)
theProject = NWProject(mockGUI) theProject = NWProject()
theTree = NWTree(theProject) theTree = NWTree(theProject)
handles = ["1c803a3b1799d", "bdd6406671ad1", "3eb1346685257", "23b8c392456de"] handles = ["1c803a3b1799d", "bdd6406671ad1", "3eb1346685257", "23b8c392456de"]
@@ -454,13 +456,13 @@ def testCoreTree_MakeHandles(mockGUI):
random.seed(42) random.seed(42)
tHandle = theTree._makeHandle() tHandle = theTree._makeHandle()
assert tHandle == handles[0] 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 # 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() tHandle = theTree._makeHandle()
assert tHandle == handles[2] 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 # Reset the seed to force collissions, which should still end up
# returning the next handle in the sequence # returning the next handle in the sequence
@@ -474,14 +476,14 @@ def testCoreTree_MakeHandles(mockGUI):
@pytest.mark.core @pytest.mark.core
def testCoreTree_Stats(mockGUI, mockItems): def testCoreTree_Stats(mockGUI, mockItems):
"""Test project stats methods.""" """Test project stats methods."""
theProject = NWProject(mockGUI) theProject = NWProject()
theTree = NWTree(theProject) theTree = NWTree(theProject)
for nwItem in mockItems: for nwItem in mockItems:
theTree.append(nwItem) theTree.append(nwItem)
assert len(theTree) == len(mockItems) assert len(theTree) == len(mockItems)
theTree._treeOrder.append("stuff") theTree._order.append("stuff")
# Count Words # Count Words
novelWords, noteWords = theTree.sumWords() novelWords, noteWords = theTree.sumWords()
@@ -494,7 +496,7 @@ def testCoreTree_Stats(mockGUI, mockItems):
@pytest.mark.core @pytest.mark.core
def testCoreTree_Reorder(caplog, mockGUI, mockItems): def testCoreTree_Reorder(caplog, mockGUI, mockItems):
"""Test changing tree order.""" """Test changing tree order."""
theProject = NWProject(mockGUI) theProject = NWProject()
theTree = NWTree(theProject) theTree = NWTree(theProject)
aHandle = [] 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 assert "Handle 'stuff' in new tree order is not in old order" in caplog.text
caplog.clear() caplog.clear()
theTree._treeOrder.append("stuff") theTree._order.append("stuff")
theTree.setOrder(bHandle) theTree.setOrder(bHandle)
assert theTree.handles() == bHandle assert theTree.handles() == bHandle
assert "Handle 'stuff' in old tree order is not in new order" in caplog.text 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 @pytest.mark.core
def testCoreTree_ToCFile(monkeypatch, fncPath, mockGUI, mockItems): def testCoreTree_ToCFile(monkeypatch, fncPath, mockGUI, mockItems):
"""Test writing the ToC.txt file.""" """Test writing the ToC.txt file."""
theProject = NWProject(mockGUI) theProject = NWProject()
theTree = NWTree(theProject) theTree = NWTree(theProject)
for nwItem in mockItems: for nwItem in mockItems:
@@ -537,7 +539,7 @@ def testCoreTree_ToCFile(monkeypatch, fncPath, mockGUI, mockItems):
theTree.updateItemData(nwItem.itemHandle) theTree.updateItemData(nwItem.itemHandle)
assert len(theTree) == len(mockItems) assert len(theTree) == len(mockItems)
theTree._treeOrder.append("stuff") theTree._order.append("stuff")
def mockIsFile(fileName): def mockIsFile(fileName):
"""Return True for items that are files in novelWriter and """Return True for items that are files in novelWriter and
+2 -1
View File
@@ -23,6 +23,7 @@ import pytest
from tools import C, buildTestProject from tools import C, buildTestProject
from novelwriter import SHARED
from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
@@ -35,7 +36,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Create a new project # Create a new project
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
theProject = nwGUI.project theProject = SHARED.project
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
docText = ( docText = (
+1 -2
View File
@@ -39,8 +39,7 @@ KEY_DELAY = 1
@pytest.mark.gui @pytest.mark.gui
def testDlgPreferences_Main(qtbot, monkeypatch, nwGUI, tstPaths): 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, "exec_", lambda *a: None)
monkeypatch.setattr(GuiPreferences, "result", lambda *a: QDialog.Accepted) monkeypatch.setattr(GuiPreferences, "result", lambda *a: QDialog.Accepted)
monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")]) monkeypatch.setattr(nwGUI.docEditor.spEnchant, "listDictionaries", lambda: [("en", "none")])
+2 -1
View File
@@ -25,6 +25,7 @@ from tools import getGuiItem
from PyQt5.QtWidgets import QAction from PyQt5.QtWidgets import QAction
from novelwriter import SHARED
from novelwriter.dialogs.projdetails import GuiProjectDetails 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.wordCountVal.text() == f"{3000:n}"
assert projDet.tabMain.chapCountVal.text() == f"{3:n}" assert projDet.tabMain.chapCountVal.text() == f"{3:n}"
assert projDet.tabMain.sceneCountVal.text() == f"{5: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) assert projDet.tabMain.projPathVal.text() == str(prjLipsum)
+6 -6
View File
@@ -27,7 +27,7 @@ from PyQt5.QtGui import QColor
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QDialog, QAction, QColorDialog from PyQt5.QtWidgets import QDialog, QAction, QColorDialog
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemType from novelwriter.enum import nwItemType
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.dialogs.projsettings import GuiProjectSettings from novelwriter.dialogs.projsettings import GuiProjectSettings
@@ -50,8 +50,8 @@ def testDlgProjSettings_Dialog(qtbot, monkeypatch, nwGUI):
assert getGuiItem("GuiProjectSettings") is None assert getGuiItem("GuiProjectSettings") is None
# Pretend we have a project # Pretend we have a project
nwGUI.hasProject = True SHARED.project._valid = True
nwGUI.project.data.setSpellLang("en") SHARED.project.data.setSpellLang("en")
# Get the dialog object # Get the dialog object
nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger) nwGUI.mainMenu.aProjectSettings.activate(QAction.Trigger)
@@ -95,7 +95,7 @@ def testDlgProjSettings_Main(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockR
CONFIG.setBackupPath(fncPath) CONFIG.setBackupPath(fncPath)
# Set some values # Set some values
theProject = nwGUI.project theProject = SHARED.project
theProject.data.setSpellLang("en") theProject.data.setSpellLang("en")
theProject.data.setAuthor("Jane Smith") theProject.data.setAuthor("Jane Smith")
theProject.data.setAutoReplace({"A": "B", "C": "D"}) theProject.data.setAutoReplace({"A": "B", "C": "D"})
@@ -160,7 +160,7 @@ def testDlgProjSettings_StatusImport(qtbot, monkeypatch, nwGUI, fncPath, projPat
CONFIG.setBackupPath(fncPath) CONFIG.setBackupPath(fncPath)
# Set some values # Set some values
theProject = nwGUI.project theProject = SHARED.project
theProject.tree[C.hTitlePage].setStatus(C.sFinished) theProject.tree[C.hTitlePage].setStatus(C.sFinished)
theProject.tree[C.hChapterDoc].setStatus(C.sDraft) theProject.tree[C.hChapterDoc].setStatus(C.sDraft)
theProject.tree[C.hSceneDoc].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) CONFIG.setBackupPath(fncPath)
# Set some values # Set some values
theProject = nwGUI.project theProject = SHARED.project
theProject.data.setAutoReplace({ theProject.data.setAutoReplace({
"A": "B", "C": "D" "A": "B", "C": "D"
}) })
+2 -1
View File
@@ -26,6 +26,7 @@ from PyQt5.QtWidgets import QDialog, QAction
from tools import buildTestProject, getGuiItem from tools import buildTestProject, getGuiItem
from novelwriter import SHARED
from novelwriter.core.spellcheck import UserDictionary from novelwriter.core.spellcheck import UserDictionary
from novelwriter.dialogs.wordlist import GuiWordList from novelwriter.dialogs.wordlist import GuiWordList
@@ -55,7 +56,7 @@ def testDlgWordList_Dialog(qtbot, monkeypatch, nwGUI, projPath):
assert wList.listBox.count() == 0 assert wList.listBox.count() == 0
# Add words # Add words
userDict = UserDictionary(nwGUI.project) userDict = UserDictionary(SHARED.project)
userDict.add("word_a") userDict.add("word_a")
userDict.add("word_c") userDict.add("word_c")
userDict.add("word_g") userDict.add("word_g")
+21 -21
View File
@@ -28,7 +28,7 @@ from PyQt5.QtCore import Qt
from PyQt5.QtGui import QTextBlock, QTextCursor, QTextOption from PyQt5.QtGui import QTextBlock, QTextCursor, QTextOption
from PyQt5.QtWidgets import QAction, qApp from PyQt5.QtWidgets import QAction, qApp
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout
from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.core.index import countWords 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 assert "Could not save document." in caplog.text
# Change header level # 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:]) nwGUI.docEditor.replaceText(longText[1:])
assert nwGUI.docEditor.saveText() is True 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 # Regular save
assert nwGUI.docEditor.saveText() is True 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" assert nwGUI.docEditor.getText() == "### New Scene\n\nSome\ntext.\nMore\u00a0text.\n"
# Check Propertoes # Check Propertoes
assert nwGUI.docEditor.docChanged() is True assert nwGUI.docEditor.docChanged is True
assert nwGUI.docEditor.docHandle() == C.hSceneDoc assert nwGUI.docEditor.docHandle == C.hSceneDoc
assert nwGUI.docEditor.lastActive() > 0.0 assert nwGUI.docEditor.lastActive > 0.0
assert nwGUI.docEditor.isEmpty() is False assert nwGUI.docEditor.isEmpty is False
# Cursor Position # Cursor Position
assert nwGUI.docEditor.setCursorPosition(None) is False assert nwGUI.docEditor.setCursorPosition(None) is False
assert nwGUI.docEditor.setCursorPosition(10) is True assert nwGUI.docEditor.setCursorPosition(10) is True
assert nwGUI.docEditor.getCursorPosition() == 10 assert nwGUI.docEditor.getCursorPosition() == 10
assert nwGUI.project.tree[C.hSceneDoc].cursorPos != 10 assert SHARED.project.tree[C.hSceneDoc].cursorPos != 10
nwGUI.docEditor.saveCursorPosition() 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(None) is False
assert nwGUI.docEditor.setCursorLine(3) is True assert nwGUI.docEditor.setCursorLine(3) is True
@@ -213,7 +213,7 @@ def testGuiEditor_MetaData(qtbot, nwGUI, projPath, mockRnd):
# Document Changed Signal # Document Changed Signal
nwGUI.docEditor._docChanged = False 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) nwGUI.docEditor.setDocumentChanged(True)
assert nwGUI.docEditor._docChanged is True assert nwGUI.docEditor._docChanged is True
@@ -1067,7 +1067,7 @@ def testGuiEditor_Tags(qtbot, nwGUI, projPath, ipsumText, mockRnd):
# Create Character # Create Character
theText = "### Jane Doe\n\n@tag: Jane\n\n" + ipsumText[1] + "\n\n" 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.openDocument(cHandle) is True
assert nwGUI.docEditor.replaceText(theText) is True assert nwGUI.docEditor.replaceText(theText) is True
assert nwGUI.saveDocument() 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)" assert nwGUI.docEditor.docFooter.wordsText.text() == "Words: 0 (+0)"
# Open a document and populate it # Open a document and populate it
nwGUI.project.tree[C.hSceneDoc]._initCount = 0 # Clear item's count SHARED.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]._wordCount = 0 # Clear item's count
assert nwGUI.openDocument(C.hSceneDoc) is True assert nwGUI.openDocument(C.hSceneDoc) is True
theText = "\n\n".join(ipsumText) theText = "\n\n".join(ipsumText)
@@ -1170,9 +1170,9 @@ def testGuiEditor_WordCounters(qtbot, monkeypatch, nwGUI, projPath, ipsumText, m
nwGUI.docEditor.wCounterDoc.run() nwGUI.docEditor.wCounterDoc.run()
# nwGUI.docEditor._updateDocCounts(cC, wC, pC) # nwGUI.docEditor._updateDocCounts(cC, wC, pC)
assert nwGUI.project.tree[C.hSceneDoc]._charCount == cC assert SHARED.project.tree[C.hSceneDoc]._charCount == cC
assert nwGUI.project.tree[C.hSceneDoc]._wordCount == wC assert SHARED.project.tree[C.hSceneDoc]._wordCount == wC
assert nwGUI.project.tree[C.hSceneDoc]._paraCount == pC assert SHARED.project.tree[C.hSceneDoc]._paraCount == pC
assert nwGUI.docEditor.docFooter.wordsText.text() == f"Words: {wC} (+{wC})" assert nwGUI.docEditor.docFooter.wordsText.text() == f"Words: {wC} (+{wC})"
# Select all text # Select all text
@@ -1361,7 +1361,7 @@ def testGuiEditor_Search(qtbot, monkeypatch, nwGUI, prjLipsum):
# Next match # Next match
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) 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) nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert abs(nwGUI.docEditor.getCursorPosition() - 620) < 3 assert abs(nwGUI.docEditor.getCursorPosition() - 620) < 3
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) 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.doNextFile is True
assert nwGUI.docEditor.docSearch.setSearchText("abcdef") assert nwGUI.docEditor.docSearch.setSearchText("abcdef")
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert nwGUI.docEditor.docHandle() != "2426c6f0ca922" assert nwGUI.docEditor.docHandle != "2426c6f0ca922"
assert nwGUI.docEditor.docHandle() == "04468803b92e1" assert nwGUI.docEditor.docHandle == "04468803b92e1"
nwGUI.mainMenu.aFindNext.activate(QAction.Trigger) nwGUI.mainMenu.aFindNext.activate(QAction.Trigger)
assert nwGUI.docEditor.docHandle() != "04468803b92e1" assert nwGUI.docEditor.docHandle != "04468803b92e1"
assert nwGUI.docEditor.docHandle() == "7a992350f3eb6" assert nwGUI.docEditor.docHandle == "7a992350f3eb6"
# Toggle Replace # Toggle Replace
nwGUI.docEditor.beginReplace() nwGUI.docEditor.beginReplace()
+10 -10
View File
@@ -27,7 +27,7 @@ from PyQt5.QtCore import Qt, QUrl
from PyQt5.QtGui import QTextCursor from PyQt5.QtGui import QTextCursor
from PyQt5.QtWidgets import qApp, QAction from PyQt5.QtWidgets import qApp, QAction
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction from novelwriter.enum import nwDocAction
from novelwriter.core.tohtml import ToHtml from novelwriter.core.tohtml import ToHtml
@@ -40,8 +40,8 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
# Rebuild the index # Rebuild the index
nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger) nwGUI.mainMenu.aRebuildIndex.activate(QAction.Trigger)
assert nwGUI.project.index._tagsIndex._tags != {} assert SHARED.project.index._tagsIndex._tags != {}
assert nwGUI.project.index._itemIndex._items != {} assert SHARED.project.index._itemIndex._items != {}
# Select a document in the project tree # Select a document in the project tree
nwGUI.projView.setSelectedHandle("88243afbe5ed8") nwGUI.projView.setSelectedHandle("88243afbe5ed8")
@@ -50,7 +50,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
theItem = nwGUI.projView.projTree._getTreeItem("88243afbe5ed8") theItem = nwGUI.projView.projTree._getTreeItem("88243afbe5ed8")
theRect = nwGUI.projView.projTree.visualItemRect(theItem) theRect = nwGUI.projView.projTree.visualItemRect(theItem)
qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=theRect.center()) qtbot.mouseClick(nwGUI.projView.projTree.viewport(), Qt.MidButton, pos=theRect.center())
assert nwGUI.docViewer.docHandle() == "88243afbe5ed8" assert nwGUI.docViewer.docHandle == "88243afbe5ed8"
# Reload the text # Reload the text
origText = nwGUI.docViewer.toPlainText() origText = nwGUI.docViewer.toPlainText()
@@ -97,7 +97,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
# Close document # Close document
nwGUI.docViewer.docHeader._closeDocument() nwGUI.docViewer.docHeader._closeDocument()
assert nwGUI.docViewer.docHandle() is None assert nwGUI.docViewer.docHandle is None
# Action on no document # Action on no document
assert nwGUI.docViewer.docAction(nwDocAction.COPY) is False assert nwGUI.docViewer.docAction(nwDocAction.COPY) is False
@@ -114,21 +114,21 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
theRect = nwGUI.docViewer.cursorRect() theRect = nwGUI.docViewer.cursorRect()
# qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100) # qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.LeftButton, pos=theRect.center(), delay=100)
nwGUI.docViewer._linkClicked(QUrl("#char=Bod")) nwGUI.docViewer._linkClicked(QUrl("#char=Bod"))
assert nwGUI.docViewer.docHandle() == "4c4f28287af27" assert nwGUI.docViewer.docHandle == "4c4f28287af27"
# Click mouse nav buttons # Click mouse nav buttons
qtbot.mouseClick(nwGUI.docViewer.viewport(), Qt.BackButton, pos=theRect.center(), delay=100) 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) 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 # Scroll bar default on empty document
nwGUI.docViewer.clear() nwGUI.docViewer.clear()
assert nwGUI.docViewer.getScrollPosition() == 0 assert nwGUI.docViewer.scrollPosition == 0
nwGUI.docViewer.reloadText() nwGUI.docViewer.reloadText()
# Change document title # Change document title
nwItem = nwGUI.project.tree["4c4f28287af27"] nwItem = SHARED.project.tree["4c4f28287af27"]
nwItem.setName("Test Title") nwItem.setName("Test Title")
assert nwItem.itemName == "Test Title" assert nwItem.itemName == "Test Title"
nwGUI.docViewer.updateDocInfo("4c4f28287af27") nwGUI.docViewer.updateDocInfo("4c4f28287af27")
+29 -29
View File
@@ -30,7 +30,7 @@ from tools import (
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QDialog, QMessageBox, QInputDialog 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.enum import nwItemType, nwView, nwWidget
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.outline import GuiOutlineView
@@ -104,18 +104,18 @@ def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, prjLipsum):
@pytest.mark.gui @pytest.mark.gui
def testGuiMain_NewProject(monkeypatch, nwGUI, projPath): def testGuiMain_NewProject(monkeypatch, nwGUI, projPath):
"""Test creating a new project. """Test creating a new project."""
""" # Open wizard, but return no data
# No data
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(GuiProjectWizard, "exec_", lambda *a: None) mp.setattr(GuiProjectWizard, "exec_", lambda *a: None)
assert nwGUI.newProject(projData=None) is False assert nwGUI.newProject(projData=None) is False
# Close project # Close project
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
nwGUI.hasProject = True SHARED.project._valid = True
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert nwGUI.newProject(projData={"projPath": projPath}) is False assert nwGUI.newProject(projData={"projPath": projPath}) is False
SHARED.project._valid = False
# No project path # No project path
assert nwGUI.newProject(projData={}) is False assert nwGUI.newProject(projData={}) is False
@@ -153,10 +153,10 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
nwGUI.projStack.setCurrentIndex(0) nwGUI.projStack.setCurrentIndex(0)
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(GuiProjectTree, "hasFocus", lambda *a: True) 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.projView.projTree._getTreeItem(sHandle).setSelected(True)
nwGUI._keyPressReturn() nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.docEditor.docHandle == sHandle
assert nwGUI.closeDocument() is True assert nwGUI.closeDocument() is True
# Novel Tree has focus # Novel Tree has focus
@@ -164,11 +164,11 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
nwGUI.novelView.novelTree.refreshTree(rootHandle=None, overRide=True) nwGUI.novelView.novelTree.refreshTree(rootHandle=None, overRide=True)
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(GuiNovelView, "treeHasFocus", lambda *a: True) 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) selItem = nwGUI.novelView.novelTree.topLevelItem(2)
nwGUI.novelView.novelTree.setCurrentItem(selItem) nwGUI.novelView.novelTree.setCurrentItem(selItem)
nwGUI._keyPressReturn() nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.docEditor.docHandle == sHandle
assert nwGUI.closeDocument() is True assert nwGUI.closeDocument() is True
# Project Outline has focus # Project Outline has focus
@@ -176,11 +176,11 @@ def testGuiMain_ProjectTreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
nwGUI.switchFocus(nwWidget.OUTLINE) nwGUI.switchFocus(nwWidget.OUTLINE)
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(GuiOutlineView, "treeHasFocus", lambda *a: True) 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) selItem = nwGUI.outlineView.outlineTree.topLevelItem(2)
nwGUI.outlineView.outlineTree.setCurrentItem(selItem) nwGUI.outlineView.outlineTree.setCurrentItem(selItem)
nwGUI._keyPressReturn() nwGUI._keyPressReturn()
assert nwGUI.docEditor.docHandle() == sHandle assert nwGUI.docEditor.docHandle == sHandle
assert nwGUI.closeDocument() is True assert nwGUI.closeDocument() is True
# qtbot.stop() # qtbot.stop()
@@ -202,14 +202,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
assert nwGUI.saveProject() assert nwGUI.saveProject()
assert nwGUI.closeProject() assert nwGUI.closeProject()
assert len(nwGUI.project.tree) == 0 assert len(SHARED.project.tree) == 0
assert len(nwGUI.project.tree._treeOrder) == 0 assert len(SHARED.project.tree._order) == 0
assert len(nwGUI.project.tree._treeRoots) == 0 assert len(SHARED.project.tree._roots) == 0
assert nwGUI.project.tree.trashRoot() is None assert SHARED.project.tree.trashRoot() is None
assert nwGUI.project.data.name == "" assert SHARED.project.data.name == ""
assert nwGUI.project.data.title == "" assert SHARED.project.data.title == ""
assert nwGUI.project.data.author == "" assert SHARED.project.data.author == ""
assert nwGUI.project.data.spellCheck is False assert SHARED.project.data.spellCheck is False
# Check the files # Check the files
projFile = projPath / "nwProject.nwx" projFile = projPath / "nwProject.nwx"
@@ -222,14 +222,14 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
assert nwGUI.openProject(projPath) assert nwGUI.openProject(projPath)
# Check that we loaded the data # Check that we loaded the data
assert len(nwGUI.project.tree) == 8 assert len(SHARED.project.tree) == 8
assert len(nwGUI.project.tree._treeOrder) == 8 assert len(SHARED.project.tree._order) == 8
assert len(nwGUI.project.tree._treeRoots) == 4 assert len(SHARED.project.tree._roots) == 4
assert nwGUI.project.tree.trashRoot() is None assert SHARED.project.tree.trashRoot() is None
assert nwGUI.project.data.name == "New Project" assert SHARED.project.data.name == "New Project"
assert nwGUI.project.data.title == "New Novel" assert SHARED.project.data.title == "New Novel"
assert nwGUI.project.data.author == "Jane Doe" assert SHARED.project.data.author == "Jane Doe"
assert nwGUI.project.data.spellCheck is False assert SHARED.project.data.spellCheck is False
# Check that tree items have been created # Check that tree items have been created
assert nwGUI.projView.projTree._getTreeItem(C.hNovelRoot) is not None 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() nwGUI.docEditor.wCounterDoc.run()
# Save the document # Save the document
assert nwGUI.docEditor.docChanged() assert nwGUI.docEditor.docChanged
assert nwGUI.saveDocument() assert nwGUI.saveDocument()
assert not nwGUI.docEditor.docChanged() assert not nwGUI.docEditor.docChanged
nwGUI.rebuildIndex() nwGUI.rebuildIndex()
# Open and view the edited document # Open and view the edited document
+10 -21
View File
@@ -27,7 +27,7 @@ from PyQt5.QtWidgets import QAction, QFileDialog, QMessageBox
from tools import C, writeFile, buildTestProject from tools import C, writeFile, buildTestProject
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwDocAction, nwDocInsert from novelwriter.enum import nwDocAction, nwDocInsert
from novelwriter.constants import nwKeyWords, nwUnicode from novelwriter.constants import nwKeyWords, nwUnicode
from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.doceditor import GuiDocEditor
@@ -206,7 +206,7 @@ def testGuiMenu_EditFormat(qtbot, monkeypatch, nwGUI, prjLipsum):
# Clear the Text # Clear the Text
nwGUI.docEditor.clear() nwGUI.docEditor.clear()
assert nwGUI.docEditor.isEmpty() assert nwGUI.docEditor.isEmpty
# Alignment & Indent # Alignment & Indent
# ================== # ==================
@@ -403,17 +403,17 @@ def testGuiMenu_ContextMenus(qtbot, nwGUI, prjLipsum):
# Navigation History # Navigation History
assert nwGUI.viewDocument("04468803b92e1") assert nwGUI.viewDocument("04468803b92e1")
assert nwGUI.docViewer.docHandle() == "04468803b92e1" assert nwGUI.docViewer.docHandle == "04468803b92e1"
assert nwGUI.docViewer.docHeader.backButton.isEnabled() assert nwGUI.docViewer.docHeader.backButton.isEnabled()
assert not nwGUI.docViewer.docHeader.forwardButton.isEnabled() assert not nwGUI.docViewer.docHeader.forwardButton.isEnabled()
qtbot.mouseClick(nwGUI.docViewer.docHeader.backButton, Qt.LeftButton) 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 not nwGUI.docViewer.docHeader.backButton.isEnabled()
assert nwGUI.docViewer.docHeader.forwardButton.isEnabled() assert nwGUI.docViewer.docHeader.forwardButton.isEnabled()
qtbot.mouseClick(nwGUI.docViewer.docHeader.forwardButton, Qt.LeftButton) 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 nwGUI.docViewer.docHeader.backButton.isEnabled()
assert not nwGUI.docViewer.docHeader.forwardButton.isEnabled() assert not nwGUI.docViewer.docHeader.forwardButton.isEnabled()
@@ -438,10 +438,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
nwGUI.docEditor.clear() nwGUI.docEditor.clear()
assert nwGUI.docEditor.insertText(nwDocInsert.NO_INSERT) is False 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.insertText(None) is False
assert nwGUI.docEditor.isEmpty() assert nwGUI.docEditor.isEmpty
# qtbot.stop() # qtbot.stop()
@@ -653,21 +653,10 @@ def testGuiMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd):
# Reveal File Location # 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) nwGUI.mainMenu.aFileDetails.activate(QAction.Trigger)
path = str(projPath / "content" / "000000000000f.nwd")
theBits = theMessage.split("|") logMsg = SHARED.alert.logMessage if SHARED.alert else ""
assert len(theBits) == 2 assert logMsg == f"The currently open file is saved in: {path}"
assert theBits[0] == "The currently open file is saved in:"
assert theBits[1] == str(projPath / "content" / "000000000000f.nwd")
# qtbot.stop() # qtbot.stop()
+8 -8
View File
@@ -29,7 +29,7 @@ from PyQt5.QtGui import QFocusEvent
from PyQt5.QtCore import Qt, QEvent from PyQt5.QtCore import Qt, QEvent
from PyQt5.QtWidgets import QInputDialog, QToolTip from PyQt5.QtWidgets import QInputDialog, QToolTip
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwWidget, nwItemType from novelwriter.enum import nwWidget, nwItemType
from novelwriter.gui.noveltree import NovelTreeColumn from novelwriter.gui.noveltree import NovelTreeColumn
from novelwriter.dialogs.editlabel import GuiEditLabel 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._getTreeItem(C.hCharRoot).setSelected(True)
nwGUI.projView.projTree.newTreeItem(nwItemType.FILE) nwGUI.projView.projTree.newTreeItem(nwItemType.FILE)
contentPath = nwGUI.project.storage.contentPath contentPath = SHARED.project.storage.contentPath
assert isinstance(contentPath, Path) assert isinstance(contentPath, Path)
(contentPath / "0000000000010.nwd").write_text( (contentPath / "0000000000010.nwd").write_text(
@@ -118,26 +118,26 @@ def testGuiNovelTree_TreeItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Double-click item # Double-click item
scItem.setSelected(True) scItem.setSelected(True)
assert scItem.isSelected() assert scItem.isSelected()
assert nwGUI.docEditor.docHandle() is None assert nwGUI.docEditor.docHandle is None
novelTree._treeDoubleClick(scItem, 0) novelTree._treeDoubleClick(scItem, 0)
assert nwGUI.docEditor.docHandle() == C.hSceneDoc assert nwGUI.docEditor.docHandle == C.hSceneDoc
# Open item with middle mouse button # Open item with middle mouse button
scItem.setSelected(True) scItem.setSelected(True)
assert scItem.isSelected() 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) 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) scRect = novelTree.visualItemRect(scItem)
oldData = scItem.data(novelTree.C_TITLE, novelTree.D_HANDLE) oldData = scItem.data(novelTree.C_TITLE, novelTree.D_HANDLE)
scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, None) scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, None)
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) 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) scItem.setData(novelTree.C_TITLE, novelTree.D_HANDLE, oldData)
qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10) qtbot.mouseClick(vPort, Qt.MiddleButton, pos=scRect.center(), delay=10)
assert nwGUI.docViewer.docHandle() == C.hSceneDoc assert nwGUI.docViewer.docHandle == C.hSceneDoc
# Last Column # Last Column
# =========== # ===========
+6 -6
View File
@@ -27,7 +27,7 @@ from tools import buildTestProject, writeFile
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QWidget, QAction from PyQt5.QtWidgets import QWidget, QAction
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemClass, nwOutline, nwView from novelwriter.enum import nwItemClass, nwOutline, nwView
@@ -71,7 +71,7 @@ def testGuiOutline_Main(qtbot, monkeypatch, nwGUI, projPath):
# Option State # Option State
# ============ # ============
pOptions = nwGUI.project.options pOptions = SHARED.project.options
colNames = [h.name for h in nwOutline] colNames = [h.name for h in nwOutline]
colItems = [h for h in nwOutline] colItems = [h for h in nwOutline]
colWidth = {h: outlineTree.DEF_WIDTH[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 assert outlineBar.novelValue.itemData(2) == "" # All novels
# Add a second novel folder # Add a second novel folder
newHandle = nwGUI.project.newRoot(nwItemClass.NOVEL) newHandle = SHARED.project.newRoot(nwItemClass.NOVEL)
nwGUI.projView.projTree.revealNewTreeItem(newHandle) nwGUI.projView.projTree.revealNewTreeItem(newHandle)
# Check new values in dropdown list # Check new values in dropdown list
@@ -198,7 +198,7 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum):
("Section 4", 4), ("Section 4", 4),
] ]
for dTitle, hLevel in docList: for dTitle, hLevel in docList:
aHandle = nwGUI.project.newFile(dTitle, newHandle) aHandle = SHARED.project.newFile(dTitle, newHandle)
hHash = "#"*hLevel hHash = "#"*hLevel
writeFile(prjLipsum / "content" / f"{aHandle}.nwd", f"{hHash} {dTitle}\n\n") writeFile(prjLipsum / "content" / f"{aHandle}.nwd", f"{hHash} {dTitle}\n\n")
nwGUI.projView.projTree.revealNewTreeItem(aHandle) nwGUI.projView.projTree.revealNewTreeItem(aHandle)
@@ -246,7 +246,7 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum):
# Click POV Link # Click POV Link
assert outlineData.povKeyValue.text() == "<a href='Bod'>Bod</a>" assert outlineData.povKeyValue.text() == "<a href='Bod'>Bod</a>"
outlineView._tagClicked("Bod") outlineView._tagClicked("Bod")
assert nwGUI.docViewer.docHandle() == "4c4f28287af27" assert nwGUI.docViewer.docHandle == "4c4f28287af27"
# Scene One, Section Two # Scene One, Section Two
selItem = outlineTree.topLevelItem(5) selItem = outlineTree.topLevelItem(5)
@@ -262,7 +262,7 @@ def testGuiOutline_Content(qtbot, nwGUI, prjLipsum):
assert outlineData.itemValue.text() == "Finished" assert outlineData.itemValue.text() == "Finished"
outlineTree._treeDoubleClick(selItem, 0) outlineTree._treeDoubleClick(selItem, 0)
assert nwGUI.docEditor.docHandle() == "88243afbe5ed8" assert nwGUI.docEditor.docHandle == "88243afbe5ed8"
# qtbot.stop() # qtbot.stop()
+40 -40
View File
@@ -30,7 +30,7 @@ from mocked import causeOSError
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QMessageBox, QMenu, QTreeWidgetItem, QDialog 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.enum import nwItemLayout, nwItemType, nwItemClass
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView
@@ -46,7 +46,7 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn
projView = nwGUI.projView projView = nwGUI.projView
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
theProject = nwGUI.project theProject = SHARED.project
# Try to add item with no project # Try to add item with no project
assert projView.projTree.newTreeItem(nwItemType.FILE) is False assert projView.projTree.newTreeItem(nwItemType.FILE) is False
@@ -260,19 +260,19 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# =========== # ===========
projView.setSelectedHandle(C.hNovelRoot) 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 # Move novel folder up
assert projTree.moveTreeItem(-1) is False 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 # Move novel folder down
assert projTree.moveTreeItem(1) is True 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 # Move novel folder up again
assert projTree.moveTreeItem(-1) is True 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 # Clean up
# qtbot.stop() # qtbot.stop()
@@ -348,7 +348,7 @@ def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPat
C.hChapterDir, C.hChapterDoc, C.hSceneDoc, C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010" "0000000000010"
] ]
trashHandle = nwGUI.project.tree.trashRoot() trashHandle = SHARED.project.tree.trashRoot()
assert projTree.getTreeFromHandle(trashHandle) == [ assert projTree.getTreeFromHandle(trashHandle) == [
trashHandle, "0000000000012", "0000000000011" trashHandle, "0000000000012", "0000000000011"
] ]
@@ -368,7 +368,7 @@ def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, projPath,
"""Test moving items to Trash.""" """Test moving items to Trash."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
theProject = nwGUI.project theProject = SHARED.project
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
# Create a project # Create a project
@@ -420,7 +420,7 @@ def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, pro
"""Test permanently deleting items.""" """Test permanently deleting items."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
theProject = nwGUI.project theProject = SHARED.project
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
# Create a project # 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 # Deleting file is OK, and if it is open, it should close
assert nwGUI.openDocument(C.hTitlePage) is True 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 projTree.permDeleteItem(C.hTitlePage) is True
assert C.hTitlePage not in theProject.tree 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 # Deleting folder + files recursively is ok
assert projTree.permDeleteItem(C.hChapterDir) is True assert projTree.permDeleteItem(C.hChapterDir) is True
@@ -471,7 +471,7 @@ def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mock
"""Test emptying Trash.""" """Test emptying Trash."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True)) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
theProject = nwGUI.project theProject = SHARED.project
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
# No project open # No project open
@@ -541,16 +541,16 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
projTree.setExpandedFromHandle(None, True) projTree.setExpandedFromHandle(None, True)
projTree._addTrashRoot() projTree._addTrashRoot()
hTrashRoot = nwGUI.project.tree.trashRoot() hTrashRoot = SHARED.project.tree.trashRoot()
projTree.setSelectedHandle(C.hCharRoot) projTree.setSelectedHandle(C.hCharRoot)
projTree.newTreeItem(nwItemType.FILE) projTree.newTreeItem(nwItemType.FILE)
projTree.setSelectedHandle(C.hNovelRoot) projTree.setSelectedHandle(C.hNovelRoot)
projTree.newTreeItem(nwItemType.FILE, isNote=True) projTree.newTreeItem(nwItemType.FILE, isNote=True)
nwGUI.project.newFile("SubNote", hNovelNote) SHARED.project.newFile("SubNote", hNovelNote)
projTree.revealNewTreeItem(hSubNote) projTree.revealNewTreeItem(hSubNote)
assert nwGUI.project.tree[hSubNote].itemParent == hNovelNote assert SHARED.project.tree[hSubNote].itemParent == hNovelNote
def itemPos(tHandle): def itemPos(tHandle):
return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center() return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center()
@@ -578,7 +578,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Direct Edit Functions # Direct Edit Functions
# ===================== # =====================
# Trigger the dedicated functions the menu entries connect to # Trigger the dedicated functions the menu entries connect to
nwItem = nwGUI.project.tree[hNovelNote] nwItem = SHARED.project.tree[hNovelNote]
# Toggle active flag # Toggle active flag
assert nwItem.isActive is True assert nwItem.isActive is True
@@ -619,17 +619,17 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT) projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT)
assert nwGUI.project.tree[hNewFolderOne].isFolderType() assert SHARED.project.tree[hNewFolderOne].isFolderType()
# Convert the first folder to a document # Convert the first folder to a document
projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT) projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT)
assert nwGUI.project.tree[hNewFolderOne].isFileType() assert SHARED.project.tree[hNewFolderOne].isFileType()
assert nwGUI.project.tree[hNewFolderOne].isDocumentLayout() assert SHARED.project.tree[hNewFolderOne].isDocumentLayout()
# Convert the second folder to a note # Convert the second folder to a note
projTree._covertFolderToFile(hNewFolderTwo, nwItemLayout.NOTE) projTree._covertFolderToFile(hNewFolderTwo, nwItemLayout.NOTE)
assert nwGUI.project.tree[hNewFolderTwo].isFileType() assert SHARED.project.tree[hNewFolderTwo].isFileType()
assert nwGUI.project.tree[hNewFolderTwo].isNoteLayout() assert SHARED.project.tree[hNewFolderTwo].isNoteLayout()
# qtbot.stop() # qtbot.stop()
@@ -649,7 +649,7 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd,
# Create a project # Create a project
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
theProject = nwGUI.project theProject = SHARED.project
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
mergedDoc1 = "0000000000014" mergedDoc1 = "0000000000014"
@@ -751,7 +751,7 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd,
# Create a project # Create a project
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
theProject = nwGUI.project theProject = SHARED.project
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
docText = ( docText = (
@@ -852,7 +852,7 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mock
"""Test the duplicate items function.""" """Test the duplicate items function."""
# Create a project # Create a project
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
assert len(nwGUI.project.tree) == 8 assert len(SHARED.project.tree) == 8
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
projTree._getTreeItem(C.hNovelRoot).setExpanded(True) # type: ignore projTree._getTreeItem(C.hNovelRoot).setExpanded(True) # type: ignore
@@ -860,28 +860,28 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mock
# Nothing to do # Nothing to do
assert projTree._duplicateFromHandle(C.hInvalid) is False 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 # Duplicate title page, but select no
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No) mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert projTree._duplicateFromHandle(C.hTitlePage) is False assert projTree._duplicateFromHandle(C.hTitlePage) is False
assert len(nwGUI.project.tree) == 8 assert len(SHARED.project.tree) == 8
# Duplicate title page # Duplicate title page
assert projTree._duplicateFromHandle(C.hTitlePage) is True assert projTree._duplicateFromHandle(C.hTitlePage) is True
assert len(nwGUI.project.tree) == 9 assert len(SHARED.project.tree) == 9
# Duplicate folder # Duplicate folder
assert projTree._duplicateFromHandle(C.hChapterDir) is True assert projTree._duplicateFromHandle(C.hChapterDir) is True
assert len(nwGUI.project.tree) == 12 assert len(SHARED.project.tree) == 12
# Duplicate novel root # Duplicate novel root
assert projTree._duplicateFromHandle(C.hNovelRoot) is True 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 # 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, C.hNovelRoot, C.hTitlePage, "0000000000010", C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000011", "0000000000012", "0000000000013", "0000000000014", "0000000000015", "0000000000011", "0000000000012", "0000000000013", "0000000000014", "0000000000015",
"0000000000016", "0000000000017", "0000000000018", "0000000000019", "000000000001a", "0000000000016", "0000000000017", "0000000000018", "0000000000019", "000000000001a",
@@ -889,7 +889,7 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mock
] ]
# Make the duplicator stop early # Make the duplicator stop early
content = nwGUI.project.storage.contentPath content = SHARED.project.storage.contentPath
assert isinstance(content, Path) assert isinstance(content, Path)
(content / "000000000001e.nwd").touch() (content / "000000000001e.nwd").touch()
assert (content / "000000000001e.nwd").exists() 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 # Should only create the folder, and skip the two files because the
# next handle is already a file # next handle is already a file
assert projTree._duplicateFromHandle(C.hChapterDir) is True assert projTree._duplicateFromHandle(C.hChapterDir) is True
assert len(nwGUI.project.tree) == 22 assert len(SHARED.project.tree) == 22
# qtbot.stop() # qtbot.stop()
@@ -938,13 +938,13 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd)
assert projTree.revealNewTreeItem(C.hInvalid) is False assert projTree.revealNewTreeItem(C.hInvalid) is False
# Try to add an orphaned file to the tree # Try to add an orphaned file to the tree
nHandle = nwGUI.project.newFile("Test", C.hNovelRoot) nHandle = SHARED.project.newFile("Test", C.hNovelRoot)
nwGUI.project.tree[nHandle].setParent(None) # type: ignore SHARED.project.tree[nHandle].setParent(None) # type: ignore
assert projTree.revealNewTreeItem(nHandle) is False assert projTree.revealNewTreeItem(nHandle) is False
# Try to add an item with unknown parent to the tree # Try to add an item with unknown parent to the tree
nHandle = nwGUI.project.newFile("Test", C.hNovelRoot) nHandle = SHARED.project.newFile("Test", C.hNovelRoot)
nwGUI.project.tree[nHandle].setParent(C.hInvalid) # type: ignore SHARED.project.tree[nHandle].setParent(C.hInvalid) # type: ignore
assert projTree.revealNewTreeItem(nHandle) is False assert projTree.revealNewTreeItem(nHandle) is False
# Method: undoLastMove # Method: undoLastMove
@@ -969,25 +969,25 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd)
# Try to open a file with nothings selected # Try to open a file with nothings selected
projTree.clearSelection() projTree.clearSelection()
projTree._treeDoubleClick(QTreeWidgetItem(), 0) projTree._treeDoubleClick(QTreeWidgetItem(), 0)
assert nwGUI.docEditor.docHandle() is None assert nwGUI.docEditor.docHandle is None
# When the item cannot be found # When the item cannot be found
projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None) mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None)
projTree._treeDoubleClick(QTreeWidgetItem(), 0) projTree._treeDoubleClick(QTreeWidgetItem(), 0)
assert nwGUI.docEditor.docHandle() is None assert nwGUI.docEditor.docHandle is None
# Successfully open a file # Successfully open a file
projTree._treeDoubleClick(projTree._getTreeItem(C.hTitlePage), 0) 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 projTree._getTreeItem(C.hTitlePage).setSelected(False) # type: ignore
# A non-file item should be expanded instead # A non-file item should be expanded instead
projTree._getTreeItem(C.hNovelRoot).setExpanded(False) # type: ignore projTree._getTreeItem(C.hNovelRoot).setExpanded(False) # type: ignore
projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore
projTree._treeDoubleClick(projTree._getTreeItem(C.hNovelRoot), 1) 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 assert projTree._getTreeItem(C.hNovelRoot).isExpanded() is True # type: ignore
# Navigate the Tree # Navigate the Tree
+8 -9
View File
@@ -24,17 +24,16 @@ import pytest
from tools import C, buildTestProject from tools import C, buildTestProject
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.extensions.statusled import StatusLED from novelwriter.extensions.statusled import StatusLED
@pytest.mark.gui @pytest.mark.gui
def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd): 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) buildTestProject(nwGUI, projPath)
cHandle = nwGUI.project.newFile("A Note", C.hCharRoot) cHandle = SHARED.project.newFile("A Note", C.hCharRoot)
newDoc = nwGUI.project.storage.getDocument(cHandle) newDoc = SHARED.project.storage.getDocument(cHandle)
newDoc.writeDocument("# A Note\n\n") newDoc.writeDocument("# A Note\n\n")
nwGUI.projView.projTree.revealNewTreeItem(cHandle) nwGUI.projView.projTree.revealNewTreeItem(cHandle)
nwGUI.rebuildIndex(beQuiet=True) nwGUI.rebuildIndex(beQuiet=True)
@@ -42,7 +41,7 @@ def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd):
# Reference Time # Reference Time
refTime = time.time() refTime = time.time()
nwGUI.mainStatus.setRefTime(refTime) nwGUI.mainStatus.setRefTime(refTime)
assert nwGUI.mainStatus.refTime == refTime assert nwGUI.mainStatus._refTime == refTime
# Project Status # Project Status
nwGUI.mainStatus.setProjectStatus(StatusLED.S_NONE) nwGUI.mainStatus.setProjectStatus(StatusLED.S_NONE)
@@ -64,18 +63,18 @@ def testGuiStatusBar_Main(qtbot, nwGUI, projPath, mockRnd):
CONFIG.stopWhenIdle = False CONFIG.stopWhenIdle = False
nwGUI.mainStatus.setUserIdle(True) nwGUI.mainStatus.setUserIdle(True)
nwGUI.mainStatus.updateTime() nwGUI.mainStatus.updateTime()
assert nwGUI.mainStatus.userIdle is False assert nwGUI.mainStatus._userIdle is False
assert nwGUI.mainStatus.timeText.text() == "00:00:00" assert nwGUI.mainStatus.timeText.text() == "00:00:00"
CONFIG.stopWhenIdle = True CONFIG.stopWhenIdle = True
nwGUI.mainStatus.setUserIdle(True) nwGUI.mainStatus.setUserIdle(True)
nwGUI.mainStatus.updateTime(5) nwGUI.mainStatus.updateTime(5)
assert nwGUI.mainStatus.userIdle is True assert nwGUI.mainStatus._userIdle is True
assert nwGUI.mainStatus.timeText.text() != "00:00:00" assert nwGUI.mainStatus.timeText.text() != "00:00:00"
nwGUI.mainStatus.setUserIdle(False) nwGUI.mainStatus.setUserIdle(False)
nwGUI.mainStatus.updateTime(5) nwGUI.mainStatus.updateTime(5)
assert nwGUI.mainStatus.userIdle is False assert nwGUI.mainStatus._userIdle is False
assert nwGUI.mainStatus.timeText.text() != "00:00:00" assert nwGUI.mainStatus.timeText.text() != "00:00:00"
# Language # Language
+5 -5
View File
@@ -30,7 +30,7 @@ from tools import writeFile
from PyQt5.QtGui import QIcon, QPalette, QPixmap from PyQt5.QtGui import QIcon, QPalette, QPixmap
from PyQt5.QtWidgets import QApplication from PyQt5.QtWidgets import QApplication
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.constants import nwLabels from novelwriter.constants import nwLabels
@@ -38,7 +38,7 @@ from novelwriter.constants import nwLabels
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_Main(qtbot, nwGUI, tstPaths): def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
"""Test the theme class init.""" """Test the theme class init."""
mainTheme = CONFIG.theme mainTheme = SHARED.theme
# Methods # Methods
# ======= # =======
@@ -121,7 +121,7 @@ def testGuiTheme_Main(qtbot, nwGUI, tstPaths):
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI): def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI):
"""Test the theme part of the class.""" """Test the theme part of the class."""
mainTheme = CONFIG.theme mainTheme = SHARED.theme
# List Themes # List Themes
# =========== # ===========
@@ -199,7 +199,7 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI):
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI): def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
"""Test the syntax part of the class.""" """Test the syntax part of the class."""
mainTheme = CONFIG.theme mainTheme = SHARED.theme
# List Themes # List Themes
# =========== # ===========
@@ -264,7 +264,7 @@ def testGuiTheme_Syntax(qtbot, monkeypatch, nwGUI):
@pytest.mark.gui @pytest.mark.gui
def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, tstPaths): def testGuiTheme_Icons(qtbot, caplog, monkeypatch, nwGUI, tstPaths):
"""Test the icon cache class.""" """Test the icon cache class."""
iconCache = CONFIG.theme.iconCache iconCache = SHARED.theme.iconCache
# Load Theme # Load Theme
# ========== # ==========
+2 -2
View File
@@ -45,7 +45,7 @@ def testManuscriptBuild_Main(
build = BuildSettings() build = BuildSettings()
build.setLastPath(fncPath) build.setLastPath(fncPath)
manus = GuiManuscriptBuild(nwGUI, nwGUI, build) manus = GuiManuscriptBuild(nwGUI, build)
manus.show() manus.show()
# Check initial values # Check initial values
@@ -101,7 +101,7 @@ def testManuscriptBuild_Main(
# Error Handling # Error Handling
# ============== # ==============
manus = GuiManuscriptBuild(nwGUI, nwGUI, build) manus = GuiManuscriptBuild(nwGUI, build)
manus.show() manus.show()
# Name, path and format should be remembered # Name, path and format should be remembered
+3 -3
View File
@@ -32,7 +32,7 @@ from PyQt5.QtCore import Qt, pyqtSlot
from PyQt5.QtWidgets import QDialogButtonBox from PyQt5.QtWidgets import QDialogButtonBox
from PyQt5.QtPrintSupport import QPrintPreviewDialog from PyQt5.QtPrintSupport import QPrintPreviewDialog
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
from novelwriter.core.buildsettings import BuildSettings from novelwriter.core.buildsettings import BuildSettings
from novelwriter.tools.manuscript import GuiManuscript 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.""" """Test the init/main functionality of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
nwGUI.openProject(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* * *" allText = "New Novel\nBy Jane Doe\nA Chapter\n\t\tHi\n* * *"
manus = GuiManuscript(nwGUI) manus = GuiManuscript(nwGUI)
@@ -159,7 +159,7 @@ def testManuscript_Features(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath:
manus.show() manus.show()
manus.loadContent() 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) manus.buildList.setCurrentRow(0)
build = manus._getSelectedBuild() build = manus._getSelectedBuild()
assert isinstance(build, BuildSettings) assert isinstance(build, BuildSettings)
+16 -16
View File
@@ -21,16 +21,16 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import pytest import pytest
from tools import C, buildTestProject
from pathlib import Path from pathlib import Path
from pytestqt.qtbot import QtBot from pytestqt.qtbot import QtBot
from tools import C, buildTestProject
from PyQt5.QtGui import QFont from PyQt5.QtGui import QFont
from PyQt5.QtCore import pyqtSlot from PyQt5.QtCore import pyqtSlot
from PyQt5.QtWidgets import QDialogButtonBox, QFontDialog from PyQt5.QtWidgets import QDialogButtonBox, QFontDialog
from novelwriter import CONFIG from novelwriter import CONFIG, SHARED
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
from novelwriter.constants import nwHeadFmt from novelwriter.constants import nwHeadFmt
from novelwriter.core.buildsettings import BuildSettings, FilterMode from novelwriter.core.buildsettings import BuildSettings, FilterMode
@@ -128,11 +128,11 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR
"worldRoot": 9, "worldRoot": 9,
} }
hPlotDoc = nwGUI.project.newFile("Main Plot", C.hPlotRoot) hPlotDoc = SHARED.project.newFile("Main Plot", C.hPlotRoot)
hCharDoc = nwGUI.project.newFile("Jane Doe", C.hCharRoot) hCharDoc = SHARED.project.newFile("Jane Doe", C.hCharRoot)
nwGUI.projView.projTree.revealNewTreeItem(hPlotDoc) nwGUI.projView.projTree.revealNewTreeItem(hPlotDoc)
nwGUI.projView.projTree.revealNewTreeItem(hCharDoc) 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 # Create the dialog and populate it
bSettings = GuiBuildSettings(nwGUI, build) bSettings = GuiBuildSettings(nwGUI, build)
@@ -167,7 +167,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR
# Switch off novel docs # Switch off novel docs
filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(False) filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(False)
assert build.buildItemFilter(nwGUI.project) == { assert build.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED), C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED), C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED), C.hChapterDir: (False, FilterMode.SKIPPED),
@@ -182,7 +182,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR
# Switch on note docs # Switch on note docs
filterTab.filterOpt._widgets[switchMap["incNotes"]].setChecked(True) filterTab.filterOpt._widgets[switchMap["incNotes"]].setChecked(True)
assert build.buildItemFilter(nwGUI.project) == { assert build.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED), C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED), C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED), C.hChapterDir: (False, FilterMode.SKIPPED),
@@ -197,7 +197,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR
# Switch on inactive docs # Switch on inactive docs
filterTab.filterOpt._widgets[switchMap["incInactive"]].setChecked(True) filterTab.filterOpt._widgets[switchMap["incInactive"]].setChecked(True)
assert build.buildItemFilter(nwGUI.project) == { assert build.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED), C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED), C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED), 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.hChapterDoc].setSelected(True)
filterTab._treeMap[C.hSceneDoc].setSelected(True) filterTab._treeMap[C.hSceneDoc].setSelected(True)
filterTab.includedButton.click() filterTab.includedButton.click()
assert build.buildItemFilter(nwGUI.project) == { assert build.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED), C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED), C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED), 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[hPlotDoc].setSelected(True) # type: ignore
filterTab._treeMap[hCharDoc].setSelected(True) # type: ignore filterTab._treeMap[hCharDoc].setSelected(True) # type: ignore
filterTab.excludedButton.click() filterTab.excludedButton.click()
assert build.buildItemFilter(nwGUI.project) == { assert build.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED), C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (False, FilterMode.FILTERED), C.hTitlePage: (False, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED), C.hChapterDir: (False, FilterMode.SKIPPED),
@@ -247,7 +247,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR
# Switch on novel docs # Switch on novel docs
filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(True) filterTab.filterOpt._widgets[switchMap["incNovel"]].setChecked(True)
assert build.buildItemFilter(nwGUI.project) == { assert build.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED), C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED), # Now enabled C.hTitlePage: (True, FilterMode.FILTERED), # Now enabled
C.hChapterDir: (False, FilterMode.SKIPPED), C.hChapterDir: (False, FilterMode.SKIPPED),
@@ -264,7 +264,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR
filterTab.optTree.clearSelection() filterTab.optTree.clearSelection()
filterTab._treeMap[C.hNovelRoot].setSelected(True) filterTab._treeMap[C.hNovelRoot].setSelected(True)
filterTab.resetButton.click() filterTab.resetButton.click()
assert build.buildItemFilter(nwGUI.project) == { assert build.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED), C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED), C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED), 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[hPlotDoc].setSelected(True) # type: ignore
filterTab._treeMap[hCharDoc].setSelected(True) # type: ignore filterTab._treeMap[hCharDoc].setSelected(True) # type: ignore
filterTab.resetButton.click() filterTab.resetButton.click()
assert build.buildItemFilter(nwGUI.project) == { assert build.buildItemFilter(SHARED.project) == {
C.hNovelRoot: (False, FilterMode.SKIPPED), C.hNovelRoot: (False, FilterMode.SKIPPED),
C.hTitlePage: (True, FilterMode.FILTERED), C.hTitlePage: (True, FilterMode.FILTERED),
C.hChapterDir: (False, FilterMode.SKIPPED), 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.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
C.hPlotRoot, hPlotDoc, C.hCharRoot, hCharDoc, C.hPlotRoot, hPlotDoc, C.hCharRoot, hCharDoc,
] ]
nwGUI.project.tree[hCharDoc].setRoot(None) # type: ignore SHARED.project.tree[hCharDoc].setRoot(None) # type: ignore
nwGUI.project.tree[hPlotDoc].setParent(None) # type: ignore SHARED.project.tree[hPlotDoc].setParent(None) # type: ignore
filterTab._populateTree() filterTab._populateTree()
assert list(filterTab._treeMap.keys()) == [ assert list(filterTab._treeMap.keys()) == [
C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
+9 -6
View File
@@ -20,15 +20,17 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
""" """
import json import json
from pathlib import Path
import pytest import pytest
from pathlib import Path
from tools import getGuiItem, buildTestProject from tools import getGuiItem, buildTestProject
from mocked import causeOSError from mocked import causeOSError
from PyQt5.QtCore import Qt from PyQt5.QtCore import Qt
from PyQt5.QtWidgets import QAction, QFileDialog from PyQt5.QtWidgets import QAction, QFileDialog
from novelwriter import SHARED
from novelwriter.constants import nwFiles from novelwriter.constants import nwFiles
from novelwriter.tools.writingstats import GuiWritingStats from novelwriter.tools.writingstats import GuiWritingStats
@@ -39,7 +41,7 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, projPath, tstPaths):
""" """
# Create a project to work on # Create a project to work on
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
project = nwGUI.project project = SHARED.project
qtbot.wait(100) qtbot.wait(100)
assert nwGUI.saveProject() assert nwGUI.saveProject()
@@ -377,13 +379,14 @@ def testToolWritingStats_Main(qtbot, monkeypatch, nwGUI, projPath, tstPaths):
# IOError # IOError
# ======= # =======
monkeypatch.setattr("builtins.open", causeOSError) with monkeypatch.context() as mp:
assert not sessLog._loadLogFile() mp.setattr("builtins.open", causeOSError)
assert not sessLog._saveData(sessLog.FMT_CSV) assert not sessLog._loadLogFile()
assert not sessLog._saveData(sessLog.FMT_CSV)
# qtbot.stop() # qtbot.stop()
sessLog._doClose() sessLog._doClose()
assert nwGUI.closeProject() assert nwGUI.closeProject() is True
# END Test testToolWritingStats_Main # END Test testToolWritingStats_Main
+3 -3
View File
@@ -165,10 +165,10 @@ def buildTestProject(obj, projPath):
nwGUI = None nwGUI = None
project = obj project = obj
else: else:
from novelwriter import SHARED
nwGUI = obj nwGUI = obj
project = obj.project project = SHARED.project
project.clearProject()
project.storage.openProjectInPlace(projPath) project.storage.openProjectInPlace(projPath)
project.setDefaultStatusImport() project.setDefaultStatusImport()
@@ -204,9 +204,9 @@ def buildTestProject(obj, projPath):
project.session.startSession() project.session.startSession()
project.setProjectChanged(True) project.setProjectChanged(True)
project.saveProject(autoSave=True) project.saveProject(autoSave=True)
project._valid = True
if nwGUI is not None: if nwGUI is not None:
nwGUI.hasProject = True
nwGUI.rebuildTrees() nwGUI.rebuildTrees()
return return