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