Clean up a few things in project classes
This commit is contained in:
@@ -31,7 +31,7 @@ from typing import TYPE_CHECKING, Iterator
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from functools import partial
|
from functools import partial
|
||||||
|
|
||||||
from PyQt5.QtCore import QCoreApplication, QObject, pyqtSignal
|
from PyQt5.QtCore import QCoreApplication
|
||||||
|
|
||||||
from novelwriter import CONFIG, SHARED, __version__, __hexversion__
|
from novelwriter import CONFIG, SHARED, __version__, __hexversion__
|
||||||
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
|
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
|
||||||
@@ -55,13 +55,9 @@ if TYPE_CHECKING: # pragma: no cover
|
|||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
|
||||||
class NWProject(QObject):
|
class NWProject:
|
||||||
|
|
||||||
statusChanged = pyqtSignal(bool)
|
def __init__(self) -> None:
|
||||||
statusMessage = pyqtSignal(str)
|
|
||||||
|
|
||||||
def __init__(self, parent: QObject | None = None) -> None:
|
|
||||||
super().__init__(parent=parent)
|
|
||||||
|
|
||||||
# Core Elements
|
# Core Elements
|
||||||
self._options = OptionState(self) # Project-specific GUI options
|
self._options = OptionState(self) # Project-specific GUI options
|
||||||
@@ -206,7 +202,7 @@ class NWProject(QObject):
|
|||||||
|
|
||||||
def trashFolder(self) -> str:
|
def trashFolder(self) -> str:
|
||||||
"""Add the special trash root folder to the project."""
|
"""Add the special trash root folder to the project."""
|
||||||
trashHandle = self._tree.trashRoot()
|
trashHandle = self._tree.trashRoot
|
||||||
if trashHandle is None:
|
if trashHandle is None:
|
||||||
label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
|
label = trConst(nwLabels.CLASS_NAME[nwItemClass.TRASH])
|
||||||
return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH)
|
return self._tree.create(label, None, nwItemType.ROOT, nwItemClass.TRASH)
|
||||||
@@ -331,7 +327,7 @@ class NWProject(QObject):
|
|||||||
self.setProjectChanged(False)
|
self.setProjectChanged(False)
|
||||||
self._valid = True
|
self._valid = True
|
||||||
|
|
||||||
self.statusMessage.emit(self.tr("Opened Project: {0}").format(self._data.name))
|
SHARED.newStatusMessage(self.tr("Opened Project: {0}").format(self._data.name))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -381,7 +377,7 @@ class NWProject(QObject):
|
|||||||
)
|
)
|
||||||
|
|
||||||
self._storage.writeLockFile()
|
self._storage.writeLockFile()
|
||||||
self.statusMessage.emit(self.tr("Saved Project: {0}").format(self._data.name))
|
SHARED.newStatusMessage(self.tr("Saved Project: {0}").format(self._data.name))
|
||||||
self.setProjectChanged(False)
|
self.setProjectChanged(False)
|
||||||
|
|
||||||
return True
|
return True
|
||||||
@@ -403,7 +399,7 @@ class NWProject(QObject):
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
logger.info("Backing up project")
|
logger.info("Backing up project")
|
||||||
self.statusMessage.emit(self.tr("Backing up project ..."))
|
SHARED.newStatusMessage(self.tr("Backing up project ..."))
|
||||||
|
|
||||||
if not self._data.name:
|
if not self._data.name:
|
||||||
SHARED.error(self.tr(
|
SHARED.error(self.tr(
|
||||||
@@ -434,7 +430,7 @@ class NWProject(QObject):
|
|||||||
SHARED.error(self.tr("Could not write backup archive."))
|
SHARED.error(self.tr("Could not write backup archive."))
|
||||||
return False
|
return False
|
||||||
|
|
||||||
self.statusMessage.emit(self.tr("Project backed up to '{0}'").format(str(archName)))
|
SHARED.newStatusMessage(self.tr("Project backed up to '{0}'").format(str(archName)))
|
||||||
|
|
||||||
return True
|
return True
|
||||||
|
|
||||||
@@ -488,7 +484,7 @@ class NWProject(QObject):
|
|||||||
"""
|
"""
|
||||||
if isinstance(status, bool):
|
if isinstance(status, bool):
|
||||||
self._changed = status
|
self._changed = status
|
||||||
self.statusChanged.emit(self._changed)
|
SHARED.setGlobalProjectState(self._changed)
|
||||||
return self._changed
|
return self._changed
|
||||||
|
|
||||||
##
|
##
|
||||||
|
|||||||
+14
-11
@@ -40,6 +40,8 @@ if TYPE_CHECKING: # pragma: no cover
|
|||||||
|
|
||||||
logger = logging.getLogger(__name__)
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
MAX_DEPTH = 1000 # Cap of tree traversing for loops (recursion limit)
|
||||||
|
|
||||||
|
|
||||||
class NWTree:
|
class NWTree:
|
||||||
"""Core: Project Tree Data Class
|
"""Core: Project Tree Data Class
|
||||||
@@ -59,7 +61,7 @@ class NWTree:
|
|||||||
also used for file names.
|
also used for file names.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
MAX_DEPTH = 1000 # Cap of tree traversing for loops
|
__slots__ = ("_project", "_tree", "_order", "_roots", "_trash", "_changed")
|
||||||
|
|
||||||
def __init__(self, project: NWProject) -> None:
|
def __init__(self, project: NWProject) -> None:
|
||||||
|
|
||||||
@@ -74,6 +76,15 @@ class NWTree:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
##
|
||||||
|
# Properties
|
||||||
|
##
|
||||||
|
|
||||||
|
@property
|
||||||
|
def trashRoot(self) -> str | None:
|
||||||
|
"""Return the handle of the trash folder, or None."""
|
||||||
|
return self._trash
|
||||||
|
|
||||||
##
|
##
|
||||||
# Class Methods
|
# Class Methods
|
||||||
##
|
##
|
||||||
@@ -320,7 +331,7 @@ class NWTree:
|
|||||||
return False
|
return False
|
||||||
|
|
||||||
iItem = tItem
|
iItem = tItem
|
||||||
for _ in range(self.MAX_DEPTH):
|
for _ in range(MAX_DEPTH):
|
||||||
if iItem.itemParent is None:
|
if iItem.itemParent is None:
|
||||||
tItem.setRoot(iItem.itemHandle)
|
tItem.setRoot(iItem.itemHandle)
|
||||||
tItem.setClassDefaults(iItem.itemClass)
|
tItem.setClassDefaults(iItem.itemClass)
|
||||||
@@ -349,7 +360,7 @@ class NWTree:
|
|||||||
tItem = self.__getitem__(tHandle)
|
tItem = self.__getitem__(tHandle)
|
||||||
if tItem is not None:
|
if tItem is not None:
|
||||||
tTree.append(tHandle)
|
tTree.append(tHandle)
|
||||||
for _ in range(self.MAX_DEPTH):
|
for _ in range(MAX_DEPTH):
|
||||||
if tItem.itemParent is None:
|
if tItem.itemParent is None:
|
||||||
return tTree
|
return tTree
|
||||||
else:
|
else:
|
||||||
@@ -400,14 +411,6 @@ class NWTree:
|
|||||||
return True
|
return True
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def trashRoot(self) -> str | None:
|
|
||||||
"""Returns the handle of the trash folder, or None if there
|
|
||||||
isn't one.
|
|
||||||
"""
|
|
||||||
if self._trash:
|
|
||||||
return self._trash
|
|
||||||
return None
|
|
||||||
|
|
||||||
def findRoot(self, itemClass: nwItemClass | None) -> str | None:
|
def findRoot(self, itemClass: nwItemClass | None) -> str | None:
|
||||||
"""Find the first root item for a given class."""
|
"""Find the first root item for a given class."""
|
||||||
for aRoot in self._roots:
|
for aRoot in self._roots:
|
||||||
|
|||||||
@@ -798,7 +798,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
logger.error("There is no item to delete")
|
logger.error("There is no item to delete")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
trashHandle = SHARED.project.tree.trashRoot()
|
trashHandle = SHARED.project.tree.trashRoot
|
||||||
if tHandle == trashHandle:
|
if tHandle == trashHandle:
|
||||||
logger.error("Cannot delete the Trash folder")
|
logger.error("Cannot delete the Trash folder")
|
||||||
return False
|
return False
|
||||||
@@ -823,7 +823,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
logger.error("No project open")
|
logger.error("No project open")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
trashHandle = SHARED.project.tree.trashRoot()
|
trashHandle = SHARED.project.tree.trashRoot
|
||||||
|
|
||||||
logger.debug("Emptying Trash folder")
|
logger.debug("Emptying Trash folder")
|
||||||
if trashHandle is None:
|
if trashHandle is None:
|
||||||
@@ -1201,7 +1201,7 @@ class GuiProjectTree(QTreeWidget):
|
|||||||
# Trash Folder
|
# Trash Folder
|
||||||
# ============
|
# ============
|
||||||
|
|
||||||
trashHandle = SHARED.project.tree.trashRoot()
|
trashHandle = SHARED.project.tree.trashRoot
|
||||||
if tItem.itemHandle == trashHandle and trashHandle is not None:
|
if tItem.itemHandle == trashHandle and trashHandle is not None:
|
||||||
# The trash folder only has one option
|
# The trash folder only has one option
|
||||||
aEmptyTrash = ctxMenu.addAction(self.tr("Empty Trash"))
|
aEmptyTrash = ctxMenu.addAction(self.tr("Empty Trash"))
|
||||||
|
|||||||
@@ -590,8 +590,8 @@ class GuiMain(QMainWindow):
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
def openNextDocument(self, tHandle: str, wrapAround: bool = False) -> bool:
|
def openNextDocument(self, tHandle: str, wrapAround: bool = False) -> bool:
|
||||||
"""Opens the next document in the project tree, following the
|
"""Open the next document in the project tree, following the
|
||||||
document with the given handle. Stops when reaching the end.
|
document with the given handle. Stop when reaching the end.
|
||||||
"""
|
"""
|
||||||
if not SHARED.hasProject:
|
if not SHARED.hasProject:
|
||||||
logger.error("No project open")
|
logger.error("No project open")
|
||||||
|
|||||||
+21
-27
@@ -29,7 +29,7 @@ from time import time
|
|||||||
from typing import TYPE_CHECKING
|
from typing import TYPE_CHECKING
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from PyQt5.QtCore import QObject, pyqtSignal, pyqtSlot
|
from PyQt5.QtCore import QObject, pyqtSignal
|
||||||
from PyQt5.QtWidgets import QMessageBox, QWidget
|
from PyQt5.QtWidgets import QMessageBox, QWidget
|
||||||
|
|
||||||
if TYPE_CHECKING: # pragma: no cover
|
if TYPE_CHECKING: # pragma: no cover
|
||||||
@@ -84,7 +84,7 @@ class SharedData(QObject):
|
|||||||
|
|
||||||
@property
|
@property
|
||||||
def hasProject(self) -> bool:
|
def hasProject(self) -> bool:
|
||||||
"""Return True of the project instance is populated."""
|
"""Return True if the project instance is populated."""
|
||||||
return self.project.isValid
|
return self.project.isValid
|
||||||
|
|
||||||
@property
|
@property
|
||||||
@@ -107,9 +107,9 @@ class SharedData(QObject):
|
|||||||
##
|
##
|
||||||
|
|
||||||
def initSharedData(self, gui: GuiMain, theme: GuiTheme) -> None:
|
def initSharedData(self, gui: GuiMain, theme: GuiTheme) -> None:
|
||||||
"""Initialise the UserData instance. This must be called as soon
|
"""Initialise the SharedData instance. This must be called as
|
||||||
as the Main GUI is created to ensure the SHARED singleton has the
|
soon as the Main GUI is created to ensure the SHARED singleton
|
||||||
properties needed for operation.
|
has the properties needed for operation.
|
||||||
"""
|
"""
|
||||||
self._gui = gui
|
self._gui = gui
|
||||||
self._theme = theme
|
self._theme = theme
|
||||||
@@ -159,6 +159,20 @@ class SharedData(QObject):
|
|||||||
self._idleRefTime = currTime
|
self._idleRefTime = currTime
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def newStatusMessage(self, message: str) -> None:
|
||||||
|
"""Request a new status message. This is a callable function for
|
||||||
|
core classes that cannot emit signals on their own.
|
||||||
|
"""
|
||||||
|
self.projectStatusMessage.emit(message)
|
||||||
|
return
|
||||||
|
|
||||||
|
def setGlobalProjectState(self, state: bool) -> None:
|
||||||
|
"""Change the global project status. This is a callable function
|
||||||
|
for core classes that cannot emit signals on their own.
|
||||||
|
"""
|
||||||
|
self.projectStatusChanged.emit(state)
|
||||||
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
# Alert Boxes
|
# Alert Boxes
|
||||||
##
|
##
|
||||||
@@ -201,22 +215,6 @@ class SharedData(QObject):
|
|||||||
self._alert.exec_()
|
self._alert.exec_()
|
||||||
return self._alert.result() == QMessageBox.Yes
|
return self._alert.result() == QMessageBox.Yes
|
||||||
|
|
||||||
##
|
|
||||||
# Internal Slots
|
|
||||||
##
|
|
||||||
|
|
||||||
@pyqtSlot(bool)
|
|
||||||
def _emitProjectStatusChange(self, state: bool) -> None:
|
|
||||||
"""Forward the project status slot."""
|
|
||||||
self.projectStatusChanged.emit(state)
|
|
||||||
return
|
|
||||||
|
|
||||||
@pyqtSlot(str)
|
|
||||||
def _emitProjectStatusMeesage(self, message: str) -> None:
|
|
||||||
"""Forward the project message slot."""
|
|
||||||
self.projectStatusMessage.emit(message)
|
|
||||||
return
|
|
||||||
|
|
||||||
##
|
##
|
||||||
# Internal Functions
|
# Internal Functions
|
||||||
##
|
##
|
||||||
@@ -225,12 +223,8 @@ class SharedData(QObject):
|
|||||||
"""Create a new project instance."""
|
"""Create a new project instance."""
|
||||||
from novelwriter.core.project import NWProject
|
from novelwriter.core.project import NWProject
|
||||||
if isinstance(self._project, NWProject):
|
if isinstance(self._project, NWProject):
|
||||||
self._project.statusChanged.disconnect()
|
del self._project
|
||||||
self._project.statusMessage.disconnect()
|
self._project = NWProject()
|
||||||
self._project.deleteLater()
|
|
||||||
self._project = NWProject(self)
|
|
||||||
self._project.statusChanged.connect(self._emitProjectStatusChange)
|
|
||||||
self._project.statusMessage.connect(self._emitProjectStatusMeesage)
|
|
||||||
return
|
return
|
||||||
|
|
||||||
def _resetIdleTimer(self) -> None:
|
def _resetIdleTimer(self) -> None:
|
||||||
|
|||||||
@@ -119,7 +119,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
|||||||
assert bool(theTree) is False
|
assert bool(theTree) is False
|
||||||
|
|
||||||
# Check for archive and trash folders
|
# Check for archive and trash folders
|
||||||
assert theTree.trashRoot() is None
|
assert theTree.trashRoot is None
|
||||||
|
|
||||||
aHandles = []
|
aHandles = []
|
||||||
for nwItem in mockItems:
|
for nwItem in mockItems:
|
||||||
@@ -146,7 +146,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
|||||||
# ============
|
# ============
|
||||||
|
|
||||||
# Check that we have the correct archive and trash folders
|
# Check that we have the correct archive and trash folders
|
||||||
assert theTree.trashRoot() == "a000000000003"
|
assert theTree.trashRoot == "a000000000003"
|
||||||
assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002"
|
assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002"
|
||||||
assert theTree.isTrash("a000000000003") is True
|
assert theTree.isTrash("a000000000003") is True
|
||||||
|
|
||||||
@@ -261,7 +261,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
|
|||||||
del theTree["a000000000003"]
|
del theTree["a000000000003"]
|
||||||
assert len(theTree) == len(mockItems) - 3
|
assert len(theTree) == len(mockItems) - 3
|
||||||
assert "a000000000003" not in theTree
|
assert "a000000000003" not in theTree
|
||||||
assert theTree.trashRoot() is None
|
assert theTree.trashRoot is None
|
||||||
|
|
||||||
# END Test testCoreTree_BuildTree
|
# END Test testCoreTree_BuildTree
|
||||||
|
|
||||||
@@ -365,7 +365,7 @@ def testCoreTree_CheckConsistency(caplog: pytest.LogCaptureFixture, mockGUI, fnc
|
|||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreTree_Methods(mockGUI, mockItems):
|
def testCoreTree_Methods(monkeypatch, mockGUI, mockItems):
|
||||||
"""Test various class methods."""
|
"""Test various class methods."""
|
||||||
theProject = NWProject()
|
theProject = NWProject()
|
||||||
theTree = NWTree(theProject)
|
theTree = NWTree(theProject)
|
||||||
@@ -389,11 +389,10 @@ def testCoreTree_Methods(mockGUI, mockItems):
|
|||||||
assert theTree.updateItemData("b000000000001") is True
|
assert theTree.updateItemData("b000000000001") is True
|
||||||
|
|
||||||
# Update item data, root is unreachable
|
# Update item data, root is unreachable
|
||||||
maxDepth = theTree.MAX_DEPTH
|
with monkeypatch.context() as mp:
|
||||||
theTree.MAX_DEPTH = 0 # type: ignore
|
mp.setattr("novelwriter.core.tree.MAX_DEPTH", 0)
|
||||||
with pytest.raises(RecursionError):
|
with pytest.raises(RecursionError):
|
||||||
theTree.updateItemData("b000000000001")
|
theTree.updateItemData("b000000000001")
|
||||||
theTree.MAX_DEPTH = maxDepth
|
|
||||||
|
|
||||||
# Check type
|
# Check type
|
||||||
assert theTree.checkType("blabla", nwItemType.FILE) is False
|
assert theTree.checkType("blabla", nwItemType.FILE) is False
|
||||||
@@ -424,11 +423,10 @@ def testCoreTree_Methods(mockGUI, mockItems):
|
|||||||
]
|
]
|
||||||
|
|
||||||
# Cause recursion error
|
# Cause recursion error
|
||||||
maxDepth = theTree.MAX_DEPTH
|
with monkeypatch.context() as mp:
|
||||||
theTree.MAX_DEPTH = 0 # type: ignore
|
mp.setattr("novelwriter.core.tree.MAX_DEPTH", 0)
|
||||||
with pytest.raises(RecursionError):
|
with pytest.raises(RecursionError):
|
||||||
theTree.getItemPath("c000000000001")
|
theTree.getItemPath("c000000000001")
|
||||||
theTree.MAX_DEPTH = maxDepth
|
|
||||||
|
|
||||||
# Break the folder parent handle
|
# Break the folder parent handle
|
||||||
theTree["b000000000001"]._parent = "stuff" # type: ignore
|
theTree["b000000000001"]._parent = "stuff" # type: ignore
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
|||||||
assert len(SHARED.project.tree) == 0
|
assert len(SHARED.project.tree) == 0
|
||||||
assert len(SHARED.project.tree._order) == 0
|
assert len(SHARED.project.tree._order) == 0
|
||||||
assert len(SHARED.project.tree._roots) == 0
|
assert len(SHARED.project.tree._roots) == 0
|
||||||
assert SHARED.project.tree.trashRoot() is None
|
assert SHARED.project.tree.trashRoot is None
|
||||||
assert SHARED.project.data.name == ""
|
assert SHARED.project.data.name == ""
|
||||||
assert SHARED.project.data.title == ""
|
assert SHARED.project.data.title == ""
|
||||||
assert SHARED.project.data.author == ""
|
assert SHARED.project.data.author == ""
|
||||||
@@ -225,7 +225,7 @@ def testGuiMain_Editing(qtbot, monkeypatch, nwGUI, projPath, tstPaths, mockRnd):
|
|||||||
assert len(SHARED.project.tree) == 8
|
assert len(SHARED.project.tree) == 8
|
||||||
assert len(SHARED.project.tree._order) == 8
|
assert len(SHARED.project.tree._order) == 8
|
||||||
assert len(SHARED.project.tree._roots) == 4
|
assert len(SHARED.project.tree._roots) == 4
|
||||||
assert SHARED.project.tree.trashRoot() is None
|
assert SHARED.project.tree.trashRoot is None
|
||||||
assert SHARED.project.data.name == "New Project"
|
assert SHARED.project.data.name == "New Project"
|
||||||
assert SHARED.project.data.title == "New Novel"
|
assert SHARED.project.data.title == "New Novel"
|
||||||
assert SHARED.project.data.author == "Jane Doe"
|
assert SHARED.project.data.author == "Jane Doe"
|
||||||
|
|||||||
@@ -348,7 +348,7 @@ def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPat
|
|||||||
C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
|
C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
|
||||||
"0000000000010"
|
"0000000000010"
|
||||||
]
|
]
|
||||||
trashHandle = SHARED.project.tree.trashRoot()
|
trashHandle = SHARED.project.tree.trashRoot
|
||||||
assert projTree.getTreeFromHandle(trashHandle) == [
|
assert projTree.getTreeFromHandle(trashHandle) == [
|
||||||
trashHandle, "0000000000012", "0000000000011"
|
trashHandle, "0000000000012", "0000000000011"
|
||||||
]
|
]
|
||||||
@@ -541,7 +541,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
|||||||
projTree.setExpandedFromHandle(None, True)
|
projTree.setExpandedFromHandle(None, True)
|
||||||
|
|
||||||
projTree._addTrashRoot()
|
projTree._addTrashRoot()
|
||||||
hTrashRoot = SHARED.project.tree.trashRoot()
|
hTrashRoot = SHARED.project.tree.trashRoot
|
||||||
|
|
||||||
projTree.setSelectedHandle(C.hCharRoot)
|
projTree.setSelectedHandle(C.hCharRoot)
|
||||||
projTree.newTreeItem(nwItemType.FILE)
|
projTree.newTreeItem(nwItemType.FILE)
|
||||||
|
|||||||
Reference in New Issue
Block a user