From 05aa656e7541735d4d33cc20b5d32f5cced2a4b3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 14 Feb 2022 22:18:16 +0100 Subject: [PATCH 01/13] Remove checks for unique root --- novelwriter/core/project.py | 7 +------ novelwriter/core/tree.py | 15 --------------- novelwriter/gui/mainmenu.py | 18 ------------------ novelwriter/gui/projtree.py | 2 -- 4 files changed, 1 insertion(+), 41 deletions(-) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 26d2c098..284efbec 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -121,13 +121,8 @@ class NWProject(): ## def newRoot(self, rootName, rootClass): - """Add a new root item. These items are unique, except for item class - CUSTOM, and always have parent handle set to None. + """Add a new root item. """ - if not self.projTree.checkRootUnique(rootClass): - self.theParent.makeAlert(self.tr("Duplicate root item detected."), nwAlert.ERROR) - return None - newItem = NWItem(self) newItem.setName(rootName) newItem.setType(nwItemType.ROOT) diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 7dbef4c6..f429873b 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -253,21 +253,6 @@ class NWTree(): return tItem.itemHandle return None - def checkRootUnique(self, theClass): - """Checks if there already is a root entry of class 'theClass' - in the root of the project tree. CUSTOM class is skipped as it - is not required to be unique. - """ - if theClass == nwItemClass.CUSTOM: - return True - for aRoot in self._treeRoots: - tItem = self.__getitem__(aRoot) - if tItem is None: - continue - if theClass == tItem.itemClass: - return False - return True - def getRootItem(self, tHandle): """Iterate upwards in the tree until we find the item with parent None, the root item. We do this with a for loop with a diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 5f91eccc..58578fc7 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -71,24 +71,6 @@ class GuiMainMenu(QMenuBar): return - ## - # Methods - ## - - def setAvailableRoot(self): - """Update the list of available root folders and set the ones - that are active. - """ - for itemClass in nwItemClass: - if itemClass == nwItemClass.NO_CLASS: - continue - if itemClass == nwItemClass.TRASH: - continue - self.rootItems[itemClass].setVisible( - self.theProject.projTree.checkRootUnique(itemClass) - ) - return - ## # Update Menu on Settings Changed ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 9f55cbe4..a1cd9081 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -581,7 +581,6 @@ class GuiProjectTree(QTreeWidget): if trItemS.childCount() == 0: self.takeTopLevelItem(tIndex) self._deleteTreeItem(tHandle) - self.theParent.mainMenu.setAvailableRoot() self._setTreeChanged(True) else: self.theParent.makeAlert(self.tr( @@ -973,7 +972,6 @@ class GuiProjectTree(QTreeWidget): if pHandle is None: if nwItem.itemType == nwItemType.ROOT: self.addTopLevelItem(newItem) - self.theParent.mainMenu.setAvailableRoot() elif nwItem.itemType == nwItemType.TRASH: self.addTopLevelItem(newItem) else: From 3fdea6e57a9689028cb91ceae2bec73cc9065bce Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 14 Feb 2022 22:18:38 +0100 Subject: [PATCH 02/13] Fix tests --- .../coreProject_NewRoot_nwProject.nwx | 38 ++++++++++-- tests/test_core/test_core_project.py | 8 +-- tests/test_core/test_core_tree.py | 6 -- tests/test_gui/test_gui_projtree.py | 61 ++++++++++--------- 4 files changed, 68 insertions(+), 45 deletions(-) diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 58344c12..9d74dc3c 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -39,7 +39,7 @@ Main - + Novel ROOT @@ -112,27 +112,55 @@ 0 + Novel + ROOT + NOVEL + New + False + + + Plot + ROOT + PLOT + New + False + + + Character + ROOT + CHARACTER + New + False + + + World + ROOT + WORLD + New + False + + Timeline ROOT TIMELINE New False - + Object ROOT OBJECT New False - + Custom1 ROOT CUSTOM New False - + Custom2 ROOT CUSTOM diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 7ae461eb..2f9d40e3 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -275,10 +275,10 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI): assert theProject.closeProject() is True assert theProject.openProject(projFile) is True - assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None)) - assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), type(None)) - assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), type(None)) - assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), type(None)) + assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), str) + assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), str) + assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), str) + assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), str) assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str) assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str) assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), str) diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index dc49c41a..51cb6d2a 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -229,12 +229,6 @@ def testCoreTree_Methods(mockGUI, mockItems): assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001" assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004" - # Check for root uniqueness - assert theTree.checkRootUnique(nwItemClass.CUSTOM) - assert theTree.checkRootUnique(nwItemClass.WORLD) - assert not theTree.checkRootUnique(nwItemClass.NOVEL) - assert not theTree.checkRootUnique(nwItemClass.CHARACTER) - # Find root item of child item assert theTree.getRootItem("b000000000001").itemHandle == "a000000000001" assert theTree.getRootItem("c000000000001").itemHandle == "a000000000001" diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 445bfb9a..9d43481c 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -51,21 +51,22 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): ## # Try to add and move item with no project - assert not nwTree.newTreeItem(nwItemType.FILE, None) - assert not nwTree.moveTreeItem(1) + assert nwTree.newTreeItem(nwItemType.FILE, None) is False + assert nwTree.moveTreeItem(1) is False # Open a project - assert nwGUI.openProject(nwMinimal) + assert nwGUI.openProject(nwMinimal) is True # No location selected for new item nwTree.clearSelection() - assert not nwTree.newTreeItem(nwItemType.FILE, None) - assert not nwTree.newTreeItem(nwItemType.FOLDER, None) - assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) + assert nwTree.newTreeItem(nwItemType.FILE, None) is False + assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False + assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is True # No itemType set or ROOT, but no class - assert not nwTree.newTreeItem(None, None) - assert not nwTree.newTreeItem(nwItemType.ROOT, None) + nwTree.clearSelection() + assert nwTree.newTreeItem(None, None) is False + assert nwTree.newTreeItem(nwItemType.ROOT, None) is False # Select a location chItem = nwTree._getTreeItem("a6d311a93600a") @@ -73,23 +74,23 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): chItem.setExpanded(True) # Create new item with no class set (defaults to NOVEL) - assert nwTree.newTreeItem(nwItemType.FILE, None) - assert nwTree.newTreeItem(nwItemType.FOLDER, None) + assert nwTree.newTreeItem(nwItemType.FILE, None) is True + assert nwTree.newTreeItem(nwItemType.FOLDER, None) is True # Check that we have the correct tree order assert nwTree.getTreeFromHandle("a6d311a93600a") == [ "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9" ] - # Add roots - assert not nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) # Duplicate - assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) # Valid + # Add more roots + assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True # Duplicate + assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) is True # Unique # Change max depth and try to add a subfolder that is too deep monkeypatch.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 2) chItem = nwTree._getTreeItem("71ee45a3c0db9") nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) - assert not nwTree.newTreeItem(nwItemType.FOLDER, None) + assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False ## # Move Items @@ -99,7 +100,7 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): # Shift focus and try to move item monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) - assert not nwTree.moveTreeItem(1) + assert nwTree.moveTreeItem(1) is False assert nwTree.getTreeFromHandle("a6d311a93600a") == [ "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9" ] @@ -153,7 +154,7 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) nwTree.flushTreeOrder() - assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11 + assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 12 ## # Delete and Trash @@ -168,20 +169,20 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): # Delete the items we added earlier nwTree.clearSelection() - assert not nwTree.emptyTrash() # No folder yet - assert not nwTree.deleteItem(None) - assert not nwTree.deleteItem("1111111111111") - assert nwTree.deleteItem("73475cb40a568") # New File - assert nwTree.deleteItem("71ee45a3c0db9") # New Folder - assert nwTree.deleteItem("811786ad1ae74") # Custom Root + assert nwTree.emptyTrash() is False # No folder yet + assert nwTree.deleteItem(None) is False + assert nwTree.deleteItem("1111111111111") is False + assert nwTree.deleteItem("73475cb40a568") is True # New File + assert nwTree.deleteItem("71ee45a3c0db9") is True # New Folder + assert nwTree.deleteItem("811786ad1ae74") is True # Custom Root assert "73475cb40a568" in nwGUI.theProject.projTree._treeOrder assert "71ee45a3c0db9" not in nwGUI.theProject.projTree._treeOrder assert "811786ad1ae74" not in nwGUI.theProject.projTree._treeOrder # The file is in trash, empty it assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd")) - assert nwTree.emptyTrash() - assert not nwTree.emptyTrash() # Already empty + assert nwTree.emptyTrash() is True + assert nwTree.emptyTrash() is False # Already empty assert not os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd")) assert "73475cb40a568" not in nwGUI.theProject.projTree._treeOrder @@ -189,8 +190,8 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): trashHandle = nwGUI.theProject.projTree.trashRoot() chItem = nwTree._getTreeItem(trashHandle) nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) - assert not nwTree.newTreeItem(nwItemType.FILE, None) - assert not nwTree.newTreeItem(nwItemType.FOLDER, None) + assert nwTree.newTreeItem(nwItemType.FILE, None) is False + assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False # Close the project nwGUI.closeProject() @@ -217,21 +218,21 @@ def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): ## # Add an item with an invalid type - assert not nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL) + assert nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL) is False assert "Failed to add new item" in caplog.messages[-1] # Add new file after one that has no parent handle chItem = nwTree._getTreeItem("44cb730c42048") nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) nwTree.theProject.projTree["44cb730c42048"]._parent = None - assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) + assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is False nwTree.clearSelection() # Add a file with no parent, and fail to find a suitable parent item monkeypatch.setattr("novelwriter.core.tree.NWTree.findRoot", lambda *a: None) - assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) - assert not nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL) + assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is False + assert nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL) is False # qtbot.stopForInteraction() nwGUI.closeProject() From f9ddcd2f8a6fa66759c84f0f4550242709f880d3 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 2 Apr 2022 19:13:54 +0200 Subject: [PATCH 03/13] Restructure the record of root items in the tree class --- novelwriter/core/index.py | 4 +- novelwriter/core/project.py | 12 +++++ novelwriter/core/tree.py | 44 ++++++++++++------- novelwriter/tools/build.py | 3 +- .../coreProject_NewRoot_nwProject.nwx | 28 +++++++++--- tests/test_core/test_core_index.py | 1 + tests/test_core/test_core_tree.py | 7 --- 7 files changed, 66 insertions(+), 33 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index b05fa952..b9b1e5f9 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -208,8 +208,6 @@ class NWIndex(): text. """ theItem = self.theProject.projTree[tHandle] - theRoot = self.theProject.projTree.getRootItem(tHandle) - if theItem is None: logger.info("Not indexing unknown item '%s'", tHandle) return False @@ -232,7 +230,7 @@ class NWIndex(): if self.theProject.projTree.isTrashRoot(theItem.itemParent): logger.debug("Not indexing trash item '%s'", tHandle) return False - if theRoot.itemClass == nwItemClass.ARCHIVE: + if self.theProject.projTree.getItemClass(tHandle) == nwItemClass.ARCHIVE: logger.debug("Not indexing archived item '%s'", tHandle) return False diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index a3c16de6..47397e5c 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -608,6 +608,7 @@ class NWProject(): self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName)) self._scanProjectFolder() + self._checkProjectTree() self._loadProjectLocalisation() self.updateWordCounts() @@ -1340,6 +1341,17 @@ class NWProject(): xEntry.text = aValue return + def _checkProjectTree(self): + """Check the project tree and make sure all items have sensible + values. + """ + for tItem in self.projTree: + tHandle = tItem.itemHandle + logger.verbose("Checking item '%s'", tHandle) + if tItem.itemRoot is None: + self.projTree.updateItemRoot(tHandle) + logger.warning("Corrected the root setting of item '%s'", tHandle) + def _scanProjectFolder(self): """Scan the project folder and check that the files in it are also in the project XML file. If they aren't, import them as diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index f429873b..3316f993 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -47,7 +47,7 @@ class NWTree(): self._projTree = {} # Holds all the items of the project self._treeOrder = [] # The order of the tree items on the tree view - self._treeRoots = [] # The root items of the tree + self._treeRoots = {} # 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._theIndex = 0 # The current iterator index @@ -67,7 +67,7 @@ class NWTree(): """ self._projTree = {} self._treeOrder = [] - self._treeRoots = [] + self._treeRoots = {} self._trashRoot = None self._archRoot = None self._theIndex = 0 @@ -98,7 +98,7 @@ class NWTree(): if nwItem.itemType == nwItemType.ROOT: logger.verbose("Item '%s' is a root item", str(tHandle)) - self._treeRoots.append(tHandle) + self._treeRoots[tHandle] = nwItem if nwItem.itemClass == nwItemClass.ARCHIVE: logger.verbose("Item '%s' is the archive folder", str(tHandle)) self._archRoot = tHandle @@ -253,20 +253,34 @@ class NWTree(): return tItem.itemHandle return None - def getRootItem(self, tHandle): - """Iterate upwards in the tree until we find the item with - parent None, the root item. We do this with a for loop with a - maximum depth to make infinite loops impossible. + def isRoot(self, tHandle): + """Check if a handle is a root item. + """ + return tHandle in self._treeRoots + + def updateItemRoot(self, tHandle): + """Update the root item handle of a given item. + """ + tItem = self.__getitem__(tHandle) + iItem = tItem + if iItem is not None: + for _ in range(nwConst.MAX_DEPTH + 1): + if iItem.itemParent is None: + tItem.setRoot(iItem.itemHandle) + return iItem.itemHandle + else: + tHandle = iItem.itemParent + iItem = self.__getitem__(tHandle) + return None + + def getItemClass(self, tHandle): + """Return the class of a given item. """ tItem = self.__getitem__(tHandle) if tItem is not None: - for i in range(nwConst.MAX_DEPTH + 1): - if tItem.itemParent is None: - return tItem - else: - tHandle = tItem.itemParent - tItem = self.__getitem__(tHandle) - return None + if tItem.itemRoot in self._treeRoots: + return self._treeRoots[tItem.itemRoot].itemClass + return nwItemClass.NO_CLASS def getItemPath(self, tHandle): """Iterate upwards in the tree until we find the item with @@ -405,7 +419,7 @@ class NWTree(): return if tHandle in self._treeRoots: - self._treeRoots.remove(tHandle) + del self._treeRoots[tHandle] if tHandle == self._trashRoot: self._trashRoot = None if tHandle == self._archRoot: diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 7ebed572..0bb593a6 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -799,8 +799,7 @@ class GuiBuildNovel(QDialog): if isNovel and not novelFiles: return False - rootItem = self.theProject.projTree.getRootItem(theItem.itemHandle) - if rootItem.itemClass == nwItemClass.ARCHIVE: + if self.theProject.projTree.getItemClass(theItem.itemHandle) == nwItemClass.ARCHIVE: return False return True diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index cf55336e..b69de2b3 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -39,7 +39,7 @@ Main - + Novel @@ -72,19 +72,35 @@ New Scene - + + + Novel + + + + Plot + + + + Character + + + + World + + Timeline - + Object - + Custom1 - + Custom2 diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 9a7f7b88..5e5d3eb9 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -284,6 +284,7 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): aHandle = theProject.newRoot("Archive", nwItemClass.ARCHIVE) assert theProject.projTree[aHandle] is not None xItem.setParent(aHandle) + xItem.setRoot(aHandle) assert theIndex.scanText(xHandle, "Hello World!") is False # Make some usable items diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 66d76459..a86993a3 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -224,17 +224,10 @@ def testCoreTree_Methods(mockGUI, mockItems): assert theTree.checkType("c000000000001", nwItemType.FILE) is True # Root item lookup - theTree._treeRoots.append("stuff") assert theTree.findRoot(nwItemClass.WORLD) is None assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001" assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004" - # Find root item of child item - assert theTree.getRootItem("b000000000001").itemHandle == "a000000000001" - assert theTree.getRootItem("c000000000001").itemHandle == "a000000000001" - assert theTree.getRootItem("c000000000002").itemHandle == "a000000000001" - assert theTree.getRootItem("stuff") is None - # Get item path assert theTree.getItemPath("stuff") == [] assert theTree.getItemPath("c000000000001") == [ From 37970bd4461054a08216c8d9ca2151881972f23c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 2 Apr 2022 19:43:26 +0200 Subject: [PATCH 04/13] Add root information to project file --- novelwriter/core/item.py | 18 +++++++ novelwriter/core/tree.py | 1 + tests/lipsum/nwProject.nwx | 48 +++++++++---------- tests/minimal/nwProject.nwx | 22 ++++----- .../coreProject_NewCustomA_nwProject.nwx | 48 +++++++++---------- .../coreProject_NewCustomB_nwProject.nwx | 30 ++++++------ .../coreProject_NewFile_nwProject.nwx | 22 ++++----- .../coreProject_NewMinimal_nwProject.nwx | 18 +++---- .../coreProject_NewRoot_nwProject.nwx | 34 ++++++------- .../guiEditor_Main_Final_nwProject.nwx | 26 +++++----- .../guiEditor_Main_Initial_nwProject.nwx | 18 +++---- .../guiProjSettings_Dialog_nwProject.nwx | 18 +++---- tests/test_core/test_core_item.py | 29 +++++++++-- tests/test_core/test_core_tree.py | 41 ++++++++-------- 14 files changed, 207 insertions(+), 166 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 67dd836c..73dfdd6b 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -45,6 +45,7 @@ class NWItem(): self._name = "" self._handle = None self._parent = None + self._root = None self._order = 0 self._type = nwItemType.NO_TYPE self._class = nwItemClass.NO_CLASS @@ -84,6 +85,10 @@ class NWItem(): def itemParent(self): return self._parent + @property + def itemRoot(self): + return self._root + @property def itemOrder(self): return self._order @@ -142,6 +147,7 @@ class NWItem(): itemAttrib = {} itemAttrib["handle"] = str(self._handle) itemAttrib["parent"] = str(self._parent) + itemAttrib["root"] = str(self._root) itemAttrib["order"] = str(self._order) itemAttrib["type"] = str(self._type.name) itemAttrib["class"] = str(self._class.name) @@ -182,6 +188,7 @@ class NWItem(): return False self.setParent(xItem.attrib.get("parent", None)) + self.setRoot(xItem.attrib.get("root", None)) self.setOrder(xItem.attrib.get("order", 0)) self.setType(xItem.attrib.get("type", nwItemType.NO_TYPE)) self.setClass(xItem.attrib.get("class", nwItemClass.NO_CLASS)) @@ -301,6 +308,17 @@ class NWItem(): self._parent = None return + def setRoot(self, theRoot): + """Set the root handle, and ensure it is valid. + """ + if theRoot is None: + self._root = None + elif isHandle(theRoot): + self._root = theRoot + else: + self._root = None + return + def setOrder(self, theOrder): """Set the item order, and ensure that it is valid. This value is purely a meta value, and not actually used by novelWriter at diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 3316f993..b146c5a1 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -113,6 +113,7 @@ class NWTree(): self._projTree[tHandle] = nwItem self._treeOrder.append(tHandle) + self.updateItemRoot(tHandle) self._setTreeChanged(True) return True diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx index c6eab805..457d8672 100644 --- a/tests/lipsum/nwProject.nwx +++ b/tests/lipsum/nwProject.nwx @@ -1,12 +1,12 @@ - + Lorem Ipsum Lorem Ipsum lipsum.com - 21 + 23 24 - 1847 + 1857 False @@ -44,87 +44,87 @@ - + Novel - + Lorem Ipsum - + Front Matter - + Prologue - + Act One - + Chapter One - + Chapter One - + Scene One - + Scene Two - + Interlude - + Chapter Two - + Chapter Two - + Scene Three - + Scene Four - + Scene Five - + Characters - + Mr. Nobody - + Plot - + Main - + World - + Ancient Europe diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx index 991d8b68..708d449f 100644 --- a/tests/minimal/nwProject.nwx +++ b/tests/minimal/nwProject.nwx @@ -1,13 +1,13 @@ - + Test Minimal Minimal Jane Doe John Doh - 12 + 13 2 - 129 + 134 True @@ -42,35 +42,35 @@ - + Novel - + Title Page - + New Chapter - + New Chapter - + New Scene - + Plot - + Characters - + World diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index 4b8a67bf..92517d59 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -42,95 +42,95 @@ - + Novel - + Plot - + Characters - + Locations - + Timeline - + Objects - + Entities - + Title Page - + Chapter 1 - + Chapter 1 - + Scene 1.1 - + Scene 1.2 - + Scene 1.3 - + Chapter 2 - + Chapter 2 - + Scene 2.1 - + Scene 2.2 - + Scene 2.3 - + Chapter 3 - + Chapter 3 - + Scene 3.1 - + Scene 3.2 - + Scene 3.3 diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 3f55663e..2614990c 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -42,59 +42,59 @@ - + Novel - + Plot - + Characters - + Locations - + Timeline - + Objects - + Entities - + Title Page - + Scene 1 - + Scene 2 - + Scene 3 - + Scene 4 - + Scene 5 - + Scene 6 diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFile_nwProject.nwx index d623ee2d..a0c16221 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFile_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,43 +40,43 @@ - + Novel - + Plot - + Characters - + World - + Title Page - + New Chapter - + New Chapter - + New Scene - + Hello - + Jane diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx index 6becb112..4581e521 100644 --- a/tests/reference/coreProject_NewMinimal_nwProject.nwx +++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,35 +40,35 @@ - + Novel - + Plot - + Characters - + World - + Title Page - + New Chapter - + New Chapter - + New Scene diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index b69de2b3..25b47036 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,67 +40,67 @@ - + Novel - + Plot - + Characters - + World - + Title Page - + New Chapter - + New Chapter - + New Scene - + Novel - + Plot - + Character - + World - + Timeline - + Object - + Custom1 - + Custom2 diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 4dd43b9a..a33a6e6a 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,51 +40,51 @@ - + Novel - + Title Page - + New Chapter - + New Chapter - + New Scene - + Plot - + New File - + Characters - + New File - + World - + New File - + Trash diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index f1b17740..5fbd17bb 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -40,35 +40,35 @@ - + Novel - + Title Page - + New Chapter - + New Chapter - + New Scene - + Plot - + Characters - + World diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index b88e7f17..e34a844b 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -46,35 +46,35 @@ - + Novel - + Title Page - + New Chapter - + New Chapter - + New Scene - + Plot - + Characters - + World diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index 368592f1..05c1d077 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -65,6 +65,18 @@ def testCoreItem_Setters(mockGUI): theItem.setParent("0123456789abc") assert theItem.itemParent == "0123456789abc" + # Root + theItem.setRoot(None) + assert theItem.itemRoot is None + theItem.setRoot(123) + assert theItem.itemRoot is None + theItem.setRoot("0123456789abcdef") + assert theItem.itemRoot is None + theItem.setRoot("0123456789abg") + assert theItem.itemRoot is None + theItem.setRoot("0123456789abc") + assert theItem.itemRoot == "0123456789abc" + # Order theItem.setOrder(None) assert theItem.itemOrder == 0 @@ -326,6 +338,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): theItem = NWItem(theProject) theItem.setHandle("0123456789abc") theItem.setParent("0123456789abc") + theItem.setRoot("0123456789abc") theItem.setOrder(1) theItem.setName("A Name") theItem.setClass("NOVEL") @@ -342,9 +355,11 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): xContent = etree.SubElement(nwXML, "content") theItem.packXML(xContent) assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( - b'A Name' + b'' + b'A Name' + b'' ) # Unpack @@ -368,6 +383,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): theItem = NWItem(theProject) theItem.setHandle("0123456789abc") theItem.setParent("0123456789abc") + theItem.setRoot("0123456789abc") theItem.setOrder(1) theItem.setName("A Name") theItem.setClass("NOVEL") @@ -385,8 +401,11 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog): xContent = etree.SubElement(nwXML, "content") theItem.packXML(xContent) assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == ( - b'A Name' + b'' + b'A Name' + b'' + b'' ) # Unpack diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index a86993a3..6216a1b1 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -374,25 +374,28 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == ( b'' b'' - b'' - b'Novel' - b'Act One' - b'' - b'Chapter One' - b'' - b'Scene One' - b'' - b'Outtakes' - b'' - b'Trash' - b'' - b'Characters' - b'Jane Doe' + b'Novel' + b'' + b'Act One' + b'Chapter One' + b'' + b'Scene One' + b'' + b'Outtakes' + b'Trash' + b'Characters' + b'Jane Doe' b'' b'' ) From bdde6a20802d5a1b0298a0fc6597beba390b75af Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 3 Apr 2022 17:45:17 +0200 Subject: [PATCH 05/13] Clean up handling of root and class info for items --- novelwriter/core/index.py | 4 +- novelwriter/core/project.py | 28 +++--- novelwriter/core/tree.py | 131 ++++++++++++++--------------- novelwriter/gui/projtree.py | 2 +- novelwriter/tools/build.py | 6 +- tests/test_core/test_core_index.py | 3 +- tests/test_core/test_core_tree.py | 47 ++++++++--- 7 files changed, 121 insertions(+), 100 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index b9b1e5f9..b7332478 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -227,10 +227,10 @@ class NWIndex(): if theItem.itemParent is None: logger.info("Not indexing orphaned item '%s'", tHandle) return False - if self.theProject.projTree.isTrashRoot(theItem.itemParent): + if theItem.itemClass == nwItemClass.TRASH: logger.debug("Not indexing trash item '%s'", tHandle) return False - if self.theProject.projTree.getItemClass(tHandle) == nwItemClass.ARCHIVE: + if theItem.itemClass == nwItemClass.ARCHIVE: logger.debug("Not indexing archived item '%s'", tHandle) return False diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 47397e5c..913e159a 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -53,7 +53,7 @@ logger = logging.getLogger(__name__) class NWProject(): - FILE_VERSION = "1.4" + FILE_VERSION = "1.4" # The current project file format version def __init__(self, theParent): @@ -129,6 +129,7 @@ class NWProject(): newItem.setClass(rootClass) newItem.setStatus(0) self.projTree.append(None, None, newItem) + self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle def newFolder(self, folderName, folderClass, pHandle): @@ -140,6 +141,7 @@ class NWProject(): newItem.setClass(folderClass) newItem.setStatus(0) self.projTree.append(None, pHandle, newItem) + self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle def newFile(self, fileName, fileClass, pHandle): @@ -156,6 +158,7 @@ class NWProject(): newItem.setClass(fileClass) newItem.setStatus(0) self.projTree.append(None, pHandle, newItem) + self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle def trashFolder(self): @@ -168,6 +171,7 @@ class NWProject(): newItem.setType(nwItemType.TRASH) newItem.setClass(nwItemClass.TRASH) self.projTree.append(None, None, newItem) + self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle return trashHandle @@ -605,10 +609,15 @@ class NWProject(): self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time()) self.mainConf.saveRecentCache() - self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName)) + # Check the project tree consistency + for tItem in self.projTree: + tHandle = tItem.itemHandle + logger.verbose("Checking item '%s'", tHandle) + if not self.projTree.updateItemData(tHandle): + logger.error("There was a problem item '%s', and it has been removed", tHandle) + del self.projTree[tHandle] # The file will be re-added as orphaned self._scanProjectFolder() - self._checkProjectTree() self._loadProjectLocalisation() self.updateWordCounts() @@ -617,6 +626,7 @@ class NWProject(): self._writeLockFile() self.setProjectChanged(False) + self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName)) return True @@ -1341,17 +1351,6 @@ class NWProject(): xEntry.text = aValue return - def _checkProjectTree(self): - """Check the project tree and make sure all items have sensible - values. - """ - for tItem in self.projTree: - tHandle = tItem.itemHandle - logger.verbose("Checking item '%s'", tHandle) - if tItem.itemRoot is None: - self.projTree.updateItemRoot(tHandle) - logger.warning("Corrected the root setting of item '%s'", tHandle) - def _scanProjectFolder(self): """Scan the project folder and check that the files in it are also in the project XML file. If they aren't, import them as @@ -1441,6 +1440,7 @@ class NWProject(): orphItem.setClass(oClass) orphItem.setLayout(oLayout) self.projTree.append(oHandle, oParent, orphItem) + self.projTree.updateItemData(orphItem.itemHandle) if noWhere: self.theParent.makeAlert(self.tr( diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index b146c5a1..ddef88b7 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -113,7 +113,6 @@ class NWTree(): self._projTree[tHandle] = nwItem self._treeOrder.append(tHandle) - self.updateItemRoot(tHandle) self._setTreeChanged(True) return True @@ -208,9 +207,30 @@ class NWTree(): return novelWords, noteWords ## - # Tree Structure Methods + # Tree Item Methods ## + def updateItemData(self, tHandle): + """Update the root item handle of a given item. Returns True if + a root was found and data updated, otherwise False. + """ + tItem = self.__getitem__(tHandle) + if tItem is None: + return False + + iItem = tItem + for _ in range(nwConst.MAX_DEPTH + 1): + if iItem.itemParent is None: + tItem.setRoot(iItem.itemHandle) + tItem.setClass(iItem.itemClass) + return True + else: + iItem = self.__getitem__(iItem.itemParent) + if iItem is None: + return False + + return False + def checkType(self, tHandle, itemType): """Return true of item exists and is of the specified item type. """ @@ -219,70 +239,6 @@ class NWTree(): return False return tItem.itemType == itemType - def trashRoot(self): - """Returns the handle of the trash folder, or None if there - isn't one. - """ - if self._trashRoot: - return self._trashRoot - return None - - def isTrashRoot(self, tHandle): - """Check if a handle is the trash folder. - """ - if self._trashRoot is None: - return False - return tHandle == self._trashRoot - - def archiveRoot(self): - """Returns the handle of the archive folder, or None if there - isn't one. - """ - if self._archRoot: - return self._archRoot - return None - - def findRoot(self, theClass): - """Find the root item for a given class. - Note: This returns the first item for class CUSTOM. - """ - for aRoot in self._treeRoots: - tItem = self.__getitem__(aRoot) - if tItem is None: - continue - if theClass == tItem.itemClass: - return tItem.itemHandle - return None - - def isRoot(self, tHandle): - """Check if a handle is a root item. - """ - return tHandle in self._treeRoots - - def updateItemRoot(self, tHandle): - """Update the root item handle of a given item. - """ - tItem = self.__getitem__(tHandle) - iItem = tItem - if iItem is not None: - for _ in range(nwConst.MAX_DEPTH + 1): - if iItem.itemParent is None: - tItem.setRoot(iItem.itemHandle) - return iItem.itemHandle - else: - tHandle = iItem.itemParent - iItem = self.__getitem__(tHandle) - return None - - def getItemClass(self, tHandle): - """Return the class of a given item. - """ - tItem = self.__getitem__(tHandle) - if tItem is not None: - if tItem.itemRoot in self._treeRoots: - return self._treeRoots[tItem.itemRoot].itemClass - return nwItemClass.NO_CLASS - def getItemPath(self, tHandle): """Iterate upwards in the tree until we find the item with parent None, the root item, and return the list of handles. @@ -305,6 +261,49 @@ class NWTree(): tTree.append(tHandle) return tTree + ## + # Tree Root Methods + ## + + def isRoot(self, tHandle): + """Check if a handle is a root item. + """ + return tHandle in self._treeRoots + + def isTrashRoot(self, tHandle): + """Check if a handle is the trash folder. + """ + if self._trashRoot is None: + return False + return tHandle == self._trashRoot + + def trashRoot(self): + """Returns the handle of the trash folder, or None if there + isn't one. + """ + if self._trashRoot: + return self._trashRoot + return None + + def archiveRoot(self): + """Returns the handle of the archive folder, or None if there + isn't one. + """ + if self._archRoot: + return self._archRoot + return None + + def findRoot(self, theClass): + """Find the first root item for a given class. + """ + for aRoot in self._treeRoots: + tItem = self.__getitem__(aRoot) + if tItem is None: + continue + if theClass == tItem.itemClass: + return tItem.itemHandle + return None + ## # Setters ## diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index a1cd9081..18b2b9f5 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -280,7 +280,7 @@ class GuiProjectTree(QTreeWidget): if nwItem.itemType != nwItemType.FILE: return True - # This is a new files, so let's add some content + # This is a new file, so let's add some content newDoc = NWDoc(self.theProject, tHandle) curTxt = newDoc.readDocument() if curTxt is None: diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index 0bb593a6..feb1662d 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -780,12 +780,13 @@ class GuiBuildNovel(QDialog): if theItem is None: return False - if not theItem.isExported and not ignoreFlag: + if not (theItem.isExported or ignoreFlag): return False isNone = theItem.itemType != nwItemType.FILE isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT isNone |= theItem.itemClass == nwItemClass.NO_CLASS + isNone |= theItem.itemClass == nwItemClass.ARCHIVE isNone |= theItem.itemClass == nwItemClass.TRASH isNone |= theItem.itemParent == self.theProject.projTree.trashRoot() isNone |= theItem.itemParent is None @@ -799,9 +800,6 @@ class GuiBuildNovel(QDialog): if isNovel and not novelFiles: return False - if self.theProject.projTree.getItemClass(theItem.itemHandle) == nwItemClass.ARCHIVE: - return False - return True def _saveDocument(self, theFmt): diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 5e5d3eb9..8070cbf2 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -278,13 +278,14 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): tHandle = theProject.trashFolder() assert theProject.projTree[tHandle] is not None xItem.setParent(tHandle) + theProject.projTree.updateItemData(xItem.itemHandle) assert theIndex.scanText(xHandle, "Hello World!") is False # Create the archive root aHandle = theProject.newRoot("Archive", nwItemClass.ARCHIVE) assert theProject.projTree[aHandle] is not None xItem.setParent(aHandle) - xItem.setRoot(aHandle) + theProject.projTree.updateItemData(xItem.itemHandle) assert theIndex.scanText(xHandle, "Hello World!") is False # Make some usable items diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 6216a1b1..1e3e7d94 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -103,7 +103,7 @@ def mockItems(mockGUI): ("a000000000002", None, itemE), ("a000000000003", None, itemF), ("a000000000004", None, itemG), - ("b000000000002", "a000000000002", itemH), + ("b000000000002", "a000000000004", itemH), ] return theItems @@ -120,22 +120,23 @@ def testCoreTree_BuildTree(mockGUI, mockItems): assert theTree._handleSeed == 42 # Check that tree is empty (calls NWTree.__bool__) - assert not theTree + assert bool(theTree) is False # Check for archive and trash folders assert theTree.trashRoot() is None assert theTree.archiveRoot() is None - assert not theTree.isTrashRoot("a000000000003") + assert theTree.isTrashRoot("a000000000003") is False aHandles = [] for tHandle, pHandle, nwItem in mockItems: aHandles.append(tHandle) - assert theTree.append(tHandle, pHandle, nwItem) + assert theTree.append(tHandle, pHandle, nwItem) is True + assert theTree.updateItemData(tHandle) is True - assert theTree._treeChanged + assert theTree._treeChanged is True # Check that tree is not empty (calls __bool__) - assert theTree + assert bool(theTree) is True # Check the number of elements (calls __len__) assert len(theTree) == len(mockItems) @@ -151,6 +152,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): assert theTree.trashRoot() == "a000000000003" assert theTree.archiveRoot() == "a000000000002" assert theTree.isTrashRoot("a000000000003") + assert theTree.isRoot("a000000000002") # Try to add another trash folder itemT = NWItem(theProject) @@ -169,14 +171,15 @@ def testCoreTree_BuildTree(mockGUI, mockItems): itemT._class = nwItemClass.NOVEL itemT._layout = nwItemLayout.DOCUMENT - assert theTree.append(None, None, itemT) + assert theTree.append(None, None, itemT) is True + assert theTree.updateItemData(itemT.itemHandle) is True assert len(theTree) == len(mockItems) + 1 theList = theTree.handles() assert theList[-1] == "73475cb40a568" # Try to add existing handle - assert not theTree.append("73475cb40a568", None, itemT) + assert theTree.append("73475cb40a568", None, itemT) is False assert len(theTree) == len(mockItems) + 1 # Delete a non-existing item @@ -207,7 +210,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): @pytest.mark.core -def testCoreTree_Methods(mockGUI, mockItems): +def testCoreTree_Methods(monkeypatch, mockGUI, mockItems): """Test various class methods. """ theProject = NWProject(mockGUI) @@ -215,9 +218,27 @@ def testCoreTree_Methods(mockGUI, mockItems): for tHandle, pHandle, nwItem in mockItems: theTree.append(tHandle, pHandle, nwItem) + theTree.updateItemData(tHandle) assert len(theTree) == len(mockItems) + # Update item data, nonsense handle + assert theTree.updateItemData("stuff") is False + + # Update item data, invalid item parent + corrParent = theTree["b000000000001"].itemParent + theTree["b000000000001"].setParent("0000000000000") + assert theTree.updateItemData("b000000000001") is False + + # Update item data, valid item parent + theTree["b000000000001"].setParent(corrParent) + assert theTree.updateItemData("b000000000001") is True + + # Update item data, root is unreachable + with monkeypatch.context() as mp: + mp.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 0) + assert theTree.updateItemData("b000000000001") is False + # Chech type assert theTree.checkType("blabla", nwItemType.FILE) is False assert theTree.checkType("b000000000001", nwItemType.FILE) is False @@ -366,6 +387,7 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): for tHandle, pHandle, nwItem in mockItems: theTree.append(tHandle, pHandle, nwItem) + theTree.updateItemData(tHandle) assert len(theTree) == len(mockItems) @@ -377,8 +399,8 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): b'Novel' b'' - b'Act One' + b'type="FOLDER" class="NOVEL">Act One' + b'' b'Chapter One' @@ -393,7 +415,7 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): b'class="TRASH">Trash' b'Characters' - b'Jane Doe' b'' @@ -418,6 +440,7 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir): for tHandle, pHandle, nwItem in mockItems: theTree.append(tHandle, pHandle, nwItem) + theTree.updateItemData(tHandle) assert len(theTree) == len(mockItems) theTree._treeOrder.append("stuff") From 61f5425662ccf5b7c3ac80c76127fa0f49d7e236 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 20:55:54 +0200 Subject: [PATCH 06/13] Update new item functions in project class --- novelwriter/core/item.py | 25 ++++++++++++++++--- novelwriter/core/project.py | 43 ++++++++++++--------------------- novelwriter/core/tree.py | 2 +- novelwriter/dialogs/docmerge.py | 2 +- novelwriter/dialogs/docsplit.py | 12 +++------ novelwriter/gui/projtree.py | 4 +-- 6 files changed, 45 insertions(+), 43 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 1edd93b8..af189597 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -294,14 +294,33 @@ class NWItem(): stIcon = self.theProject.importItems.icon(self._import) return stName, stIcon - def setImportStatus(self, theLabel): + def setImportStatus(self, value): """Update the importance or status value based on class. This is a wrapper setter for setStatus and setImport. """ if self._class in nwLists.CLS_NOVEL: - self.setStatus(theLabel) + self.setStatus(value) else: - self.setImport(theLabel) + self.setImport(value) + return + + def setClassDefaults(self, itemClass): + """Set the default values based on the item's class and the + project settings. + """ + self.setClass(itemClass) + + if self._class in nwLists.CLS_NOVEL: + self._layout = nwItemLayout.DOCUMENT + else: + self._layout = nwItemLayout.NOTE + + if self._status is None: + self.setStatus("New") # This forces a default value lookup + + if self._import is None: + self.setImport("New") # This forces a default value lookup + return ## diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 0d71b81f..28aba590 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -127,36 +127,26 @@ class NWProject(): newItem.setName(rootName) newItem.setType(nwItemType.ROOT) newItem.setClass(rootClass) - newItem.setStatus(0) self.projTree.append(None, None, newItem) self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle - def newFolder(self, folderName, folderClass, pHandle): - """Add a new folder with a given name and class and parent item. + def newFolder(self, folderName, pHandle): + """Add a new folder with a given name and parent item. """ newItem = NWItem(self) newItem.setName(folderName) newItem.setType(nwItemType.FOLDER) - newItem.setClass(folderClass) - newItem.setStatus(0) self.projTree.append(None, pHandle, newItem) self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle - def newFile(self, fileName, fileClass, pHandle): - """Add a new file with a given name and class, and set a layout - based on the class. DOCUMENT for NOVEL, otherwise NOTE. + def newFile(self, fileName, pHandle): + """Add a new file with a given name and parent item. """ newItem = NWItem(self) newItem.setName(fileName) newItem.setType(nwItemType.FILE) - if fileClass == nwItemClass.NOVEL: - newItem.setLayout(nwItemLayout.DOCUMENT) - else: - newItem.setLayout(nwItemLayout.NOTE) - newItem.setClass(fileClass) - newItem.setStatus(0) self.projTree.append(None, pHandle, newItem) self.projTree.updateItemData(newItem.itemHandle) return newItem.itemHandle @@ -286,10 +276,10 @@ class NWProject(): xHandle[2] = self.newRoot(self.tr("Plot"), nwItemClass.PLOT) xHandle[3] = self.newRoot(self.tr("Characters"), nwItemClass.CHARACTER) xHandle[4] = self.newRoot(self.tr("World"), nwItemClass.WORLD) - xHandle[5] = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, xHandle[1]) - xHandle[6] = self.newFolder(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[1]) - xHandle[7] = self.newFile(self.tr("New Chapter"), nwItemClass.NOVEL, xHandle[6]) - xHandle[8] = self.newFile(self.tr("New Scene"), nwItemClass.NOVEL, xHandle[6]) + xHandle[5] = self.newFile(self.tr("Title Page"), xHandle[1]) + xHandle[6] = self.newFolder(self.tr("New Chapter"), xHandle[1]) + xHandle[7] = self.newFile(self.tr("New Chapter"), xHandle[6]) + xHandle[8] = self.newFile(self.tr("New Scene"), xHandle[6]) aDoc = NWDoc(self, xHandle[5]) aDoc.writeDocument(titlePage) @@ -312,8 +302,7 @@ class NWProject(): self.newRoot(trConst(nwLabels.CLASS_NAME[newRoot]), newRoot) # Create a title page - tHandle = self.newFile(self.tr("Title Page"), nwItemClass.NOVEL, nHandle) - self.projTree.setFileItemLayout(tHandle, nwItemLayout.DOCUMENT) + tHandle = self.newFile(self.tr("Title Page"), nHandle) aDoc = NWDoc(self, tHandle) aDoc.writeDocument(titlePage) @@ -329,10 +318,9 @@ class NWProject(): chTitle = self.tr("Chapter {0}").format(f"{ch+1:d}") pHandle = nHandle if chFolders: - pHandle = self.newFolder(chTitle, nwItemClass.NOVEL, nHandle) + pHandle = self.newFolder(chTitle, nHandle) - cHandle = self.newFile(chTitle, nwItemClass.NOVEL, pHandle) - self.projTree.setFileItemLayout(cHandle, nwItemLayout.DOCUMENT) + cHandle = self.newFile(chTitle, pHandle) aDoc = NWDoc(self, cHandle) aDoc.writeDocument("## %s\n\n" % chTitle) @@ -341,7 +329,7 @@ class NWProject(): if numScenes > 0: for sc in range(numScenes): scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}") - sHandle = self.newFile(scTitle, nwItemClass.NOVEL, pHandle) + sHandle = self.newFile(scTitle, pHandle) aDoc = NWDoc(self, sHandle) aDoc.writeDocument("### %s\n\n" % scTitle) @@ -350,7 +338,7 @@ class NWProject(): elif numScenes > 0: for sc in range(numScenes): scTitle = self.tr("Scene {0}").format(f"{sc+1:d}") - sHandle = self.newFile(scTitle, nwItemClass.NOVEL, nHandle) + sHandle = self.newFile(scTitle, nHandle) aDoc = NWDoc(self, sHandle) aDoc.writeDocument("### %s\n\n" % scTitle) @@ -480,8 +468,9 @@ class NWProject(): # documents and one for project notes. Introduced in # version 1.5. # 1.4 : Introduces a more compact format for storing items. All - # settings aside from name are now attributes. Introduced - # in version 1.7. + # settings aside from name are now attributes. This format + # also changes the way satus and importance labels are + # stored and handled. Introduced in version 1.7. if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"): self.theParent.makeAlert(self.tr( diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index ddef88b7..e1b68220 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -222,7 +222,7 @@ class NWTree(): for _ in range(nwConst.MAX_DEPTH + 1): if iItem.itemParent is None: tItem.setRoot(iItem.itemHandle) - tItem.setClass(iItem.itemClass) + tItem.setClassDefaults(iItem.itemClass) return True else: iItem = self.__getitem__(iItem.itemParent) diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index f7695b9a..c082dd3b 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -130,7 +130,7 @@ class GuiDocMerge(QDialog): self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR) return False - nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent) + nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent) newItem = self.theProject.projTree[nHandle] newItem.setStatus(srcItem.itemStatus) newItem.setImport(srcItem.itemImport) diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index cdf0757a..b650671d 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -33,7 +33,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.core import NWDoc -from novelwriter.enum import nwAlert, nwItemType, nwItemClass, nwItemLayout +from novelwriter.enum import nwAlert, nwItemType from novelwriter.constants import nwConst from novelwriter.gui.custom import QHelpLabel @@ -186,22 +186,16 @@ class GuiDocSplit(QDialog): return False # Create the folder - fHandle = self.theProject.newFolder( - srcItem.itemName, srcItem.itemClass, srcItem.itemParent - ) + fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent) self.theParent.treeView.revealNewTreeItem(fHandle) logger.verbose("Creating folder '%s'", fHandle) # Loop through, and create the files for wTitle, iStart, iEnd in finalOrder: - isNovel = srcItem.itemClass == nwItemClass.NOVEL - itemLayout = nwItemLayout.DOCUMENT if isNovel else nwItemLayout.NOTE - wTitle = wTitle.lstrip("#").strip() - nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle) + nHandle = self.theProject.newFile(wTitle, fHandle) newItem = self.theProject.projTree[nHandle] - newItem.setLayout(itemLayout) newItem.setStatus(srcItem.itemStatus) newItem.setImport(srcItem.itemImport) logger.verbose( diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 01a8f707..584a281c 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -250,7 +250,7 @@ class GuiProjectTree(QTreeWidget): # If we're still here, add the file or folder if itemType == nwItemType.FILE: - tHandle = self.theProject.newFile(self.tr("New File"), itemClass, pHandle) + tHandle = self.theProject.newFile(self.tr("New File"), pHandle) elif itemType == nwItemType.FOLDER: if len(parTree) >= nwConst.MAX_DEPTH - 1: @@ -261,7 +261,7 @@ class GuiProjectTree(QTreeWidget): "Maximum folder depth has been reached." ), nwAlert.ERROR) return False - tHandle = self.theProject.newFolder(self.tr("New Folder"), itemClass, pHandle) + tHandle = self.theProject.newFolder(self.tr("New Folder"), pHandle) else: logger.error("Failed to add new item") From ebee9ff791b5bc385ebace92176f8b32c5b17ca2 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 16 Apr 2022 20:56:52 +0200 Subject: [PATCH 07/13] Update tests --- .../coreProject_NewCustomA_nwProject.nwx | 50 +++++++++---------- .../coreProject_NewCustomB_nwProject.nwx | 32 ++++++------ .../coreProject_NewFile_nwProject.nwx | 8 +-- .../coreProject_NewRoot_nwProject.nwx | 20 ++++---- .../guiEditor_Main_Final_nwProject.nwx | 12 ++--- .../guiEditor_Main_Initial_nwProject.nwx | 18 +++---- .../guiProjSettings_Dialog_nwProject.nwx | 20 ++++---- tests/test_core/test_core_document.py | 2 +- tests/test_core/test_core_index.py | 30 +++++------ tests/test_core/test_core_project.py | 10 ++-- tests/test_core/test_core_tree.py | 26 +++++----- tests/test_gui/test_gui_doceditor.py | 4 +- tests/test_gui/test_gui_statusbar.py | 4 +- 13 files changed, 119 insertions(+), 117 deletions(-) diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx index 2651909c..9152e3b6 100644 --- a/tests/reference/coreProject_NewCustomA_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -35,7 +35,7 @@ Finished - New + New Minor Major Main @@ -44,95 +44,95 @@ - Novel + Novel - Plot + Plot - Characters + Characters - Locations + Locations - Timeline + Timeline - Objects + Objects - Entities + Entities - Title Page + Title Page - Chapter 1 + Chapter 1 - Chapter 1 + Chapter 1 - Scene 1.1 + Scene 1.1 - Scene 1.2 + Scene 1.2 - Scene 1.3 + Scene 1.3 - Chapter 2 + Chapter 2 - Chapter 2 + Chapter 2 - Scene 2.1 + Scene 2.1 - Scene 2.2 + Scene 2.2 - Scene 2.3 + Scene 2.3 - Chapter 3 + Chapter 3 - Chapter 3 + Chapter 3 - Scene 3.1 + Scene 3.1 - Scene 3.2 + Scene 3.2 - Scene 3.3 + Scene 3.3 diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx index 7ca8751d..ef29585b 100644 --- a/tests/reference/coreProject_NewCustomB_nwProject.nwx +++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx @@ -1,5 +1,5 @@ - + Test Custom Test Novel @@ -35,7 +35,7 @@ Finished - New + New Minor Major Main @@ -44,59 +44,59 @@ - Novel + Novel - Plot + Plot - Characters + Characters - Locations + Locations - Timeline + Timeline - Objects + Objects - Entities + Entities - Title Page + Title Page - Scene 1 + Scene 1 - Scene 2 + Scene 2 - Scene 3 + Scene 3 - Scene 4 + Scene 4 - Scene 5 + Scene 5 - Scene 6 + Scene 6 diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFile_nwProject.nwx index a1c33b58..13fd7966 100644 --- a/tests/reference/coreProject_NewFile_nwProject.nwx +++ b/tests/reference/coreProject_NewFile_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -33,7 +33,7 @@ Finished - New + New Minor Major Main @@ -74,11 +74,11 @@ - Hello + Hello - Jane + Jane diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index 4274814c..264cfa0f 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -33,7 +33,7 @@ Finished - New + New Minor Major Main @@ -74,35 +74,35 @@ - Novel + Novel - Plot + Plot - Character + Character - World + World - Timeline + Timeline - Object + Object - Custom1 + Custom1 - Custom2 + Custom2 diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 84515134..64ed4b4f 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -33,7 +33,7 @@ Finished - New + New Minor Major Main @@ -66,7 +66,7 @@ - New File + New File @@ -74,7 +74,7 @@ - New File + New File @@ -82,11 +82,11 @@ - New File + New File - Trash + Trash diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index 8ed179b3..d6c8b904 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -33,7 +33,7 @@ Finished - New + New Minor Major Main @@ -42,35 +42,35 @@ - Novel + Novel - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Plot + Plot - Characters + Characters - World + World diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx index b74533d4..6c4cefdb 100644 --- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx +++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx @@ -1,5 +1,5 @@ - + Project Name Project Title @@ -39,7 +39,7 @@ Final - New + New Minor Major Final @@ -48,35 +48,35 @@ - Novel + Novel - Title Page + Title Page - New Chapter + New Chapter - New Chapter + New Chapter - New Scene + New Scene - Plot + Plot - Characters + Characters - World + World diff --git a/tests/test_core/test_core_document.py b/tests/test_core/test_core_document.py index eb7794b5..881290d6 100644 --- a/tests/test_core/test_core_document.py +++ b/tests/test_core/test_core_document.py @@ -66,7 +66,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal): # Try to open a new (non-existent) file nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL) assert nHandle is not None - xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle) + xHandle = theProject.newFile("New File", nHandle) theDoc = NWDoc(theProject, xHandle) assert bool(theDoc) is True assert repr(theDoc) == f"" diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py index 8070cbf2..cf7b1687 100644 --- a/tests/test_core/test_core_index.py +++ b/tests/test_core/test_core_index.py @@ -181,8 +181,8 @@ def testCoreIndex_CheckThese(nwMinimal, mockGUI): assert theProject.openProject(nwMinimal) is True theIndex = NWIndex(theProject) - nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") - cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") + nHandle = theProject.newFile("Hello", "a508bb932959c") + cHandle = theProject.newFile("Jane", "afb3043c7b2b3") nItem = theProject.projTree[nHandle] cItem = theProject.projTree[cHandle] @@ -260,8 +260,8 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): theIndex = NWIndex(theProject) # Some items for fail to scan tests - dHandle = theProject.newFolder("Folder", nwItemClass.NOVEL, "a508bb932959c") - xHandle = theProject.newFile("No Layout", nwItemClass.NOVEL, "a508bb932959c") + dHandle = theProject.newFolder("Folder", "a508bb932959c") + xHandle = theProject.newFile("No Layout", "a508bb932959c") xItem = theProject.projTree[xHandle] xItem.setLayout(nwItemLayout.NO_LAYOUT) @@ -279,6 +279,8 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): assert theProject.projTree[tHandle] is not None xItem.setParent(tHandle) theProject.projTree.updateItemData(xItem.itemHandle) + assert xItem.itemRoot == tHandle + assert xItem.itemClass == nwItemClass.TRASH assert theIndex.scanText(xHandle, "Hello World!") is False # Create the archive root @@ -289,11 +291,11 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI): assert theIndex.scanText(xHandle, "Hello World!") is False # Make some usable items - tHandle = theProject.newFile("Title", nwItemClass.NOVEL, "a508bb932959c") - pHandle = theProject.newFile("Page", nwItemClass.NOVEL, "a508bb932959c") - nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") - cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") - sHandle = theProject.newFile("Scene", nwItemClass.NOVEL, "a508bb932959c") + tHandle = theProject.newFile("Title", "a508bb932959c") + pHandle = theProject.newFile("Page", "a508bb932959c") + nHandle = theProject.newFile("Hello", "a508bb932959c") + cHandle = theProject.newFile("Jane", "afb3043c7b2b3") + sHandle = theProject.newFile("Scene", "a508bb932959c") # Text Indexing # ============= @@ -475,8 +477,8 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): assert theProject.openProject(nwMinimal) is True theIndex = NWIndex(theProject) - nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c") - cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3") + nHandle = theProject.newFile("Hello", "a508bb932959c") + cHandle = theProject.newFile("Jane", "afb3043c7b2b3") assert theIndex.getNovelData("", "") is None assert theIndex.getNovelData("a508bb932959c", "") is None @@ -630,9 +632,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI): # Novel Stats # =========== - hHandle = theProject.newFile("Chapter", nwItemClass.NOVEL, "a508bb932959c") - sHandle = theProject.newFile("Scene One", nwItemClass.NOVEL, "a508bb932959c") - tHandle = theProject.newFile("Scene Two", nwItemClass.NOVEL, "a508bb932959c") + hHandle = theProject.newFile("Chapter", "a508bb932959c") + sHandle = theProject.newFile("Scene One", "a508bb932959c") + tHandle = theProject.newFile("Scene Two", "a508bb932959c") theProject.projTree[hHandle].itemLayout == nwItemLayout.DOCUMENT theProject.projTree[sHandle].itemLayout == nwItemLayout.DOCUMENT diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 30cb7f33..3d5b37f4 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -314,8 +314,8 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI): assert theProject.closeProject() is True assert theProject.openProject(projFile) is True - assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str) - assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str) + assert isinstance(theProject.newFile("Hello", "31489056e0916"), str) + assert isinstance(theProject.newFile("Jane", "71ee45a3c0db9"), str) assert theProject.projChanged assert theProject.saveProject() is True assert theProject.closeProject() is True @@ -661,7 +661,7 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI): theProject.projTree._treeOrder.append("01234567789abc") # Add an item with a non-existent parent - nHandle = theProject.newFile("Test File", nwItemClass.NOVEL, "a6d311a93600a") + nHandle = theProject.newFile("Test File", "a6d311a93600a") theProject.projTree[nHandle].setParent("cba9876543210") assert theProject.projTree[nHandle].itemParent == "cba9876543210" @@ -740,7 +740,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, constData): # Change Importance # ================= - fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "73475cb40a568") + fHandle = theProject.newFile("Jane Doe", "73475cb40a568") theProject.projTree[fHandle].setImport("Main") assert theProject.projTree[fHandle].itemImport == constData.importKeys[3] @@ -1069,7 +1069,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): assert oItem.itemParent == "b3643d0f92e32" assert oItem.itemClass == nwItemClass.NOVEL assert oItem.itemType == nwItemType.FILE - assert oItem.itemLayout == nwItemLayout.NOTE + assert oItem.itemLayout == nwItemLayout.DOCUMENT assert theProject.saveProject(nwLipsum) assert theProject.closeProject() diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 3fd07a43..343c4609 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -397,31 +397,31 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): b'' b'' b'Novel' - b'' + b'class="NOVEL">Novel' b'Act One' + b'type="FOLDER" class="NOVEL">Act One' b'Chapter One' b'Scene One' b'Outtakes' + b'class="ARCHIVE">Outtakes' b'Trash' - b'' + b'class="TRASH">Trash' b'Characters' + b'class="CHARACTER">Characters' b'Jane Doe' b'' b'' diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index eaa1d2ba..cf5c3cea 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -29,7 +29,7 @@ from PyQt5.QtWidgets import QAction, QMessageBox, qApp from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.core import countWords -from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout +from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout from novelwriter.constants import nwKeyWords, nwUnicode keyDelay = 2 @@ -1143,7 +1143,7 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText): # Create Character theText = "### Jane Doe\n\n@tag: Jane\n\n" + ipsumText[1] + "\n\n" - cHandle = nwGUI.theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3") + cHandle = nwGUI.theProject.newFile("Jane Doe", "afb3043c7b2b3") assert nwGUI.openDocument(cHandle) is True assert nwGUI.docEditor.replaceText(theText) is True assert nwGUI.saveDocument() is True diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py index 9d7c4efc..66329820 100644 --- a/tests/test_gui/test_gui_statusbar.py +++ b/tests/test_gui/test_gui_statusbar.py @@ -25,7 +25,7 @@ import pytest from PyQt5.QtWidgets import QMessageBox from novelwriter.core import NWDoc -from novelwriter.enum import nwItemClass, nwState +from novelwriter.enum import nwState @pytest.mark.gui @@ -36,7 +36,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj): nwGUI.theProject.projTree.setSeed(42) assert nwGUI.newProject({"projPath": fncProj}) is True - cHandle = nwGUI.theProject.newFile("A Note", nwItemClass.CHARACTER, "71ee45a3c0db9") + cHandle = nwGUI.theProject.newFile("A Note", "71ee45a3c0db9") newDoc = NWDoc(nwGUI.theProject, cHandle) newDoc.writeDocument("# A Note\n\n") nwGUI.treeView.revealNewTreeItem(cHandle) From 4cfbbf4c680afd205a308182791ae7564b567b9e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Apr 2022 14:44:11 +0200 Subject: [PATCH 08/13] Remove max folder depth restriction, and simplify adding folders and files in the tree --- novelwriter/constants.py | 1 - novelwriter/core/item.py | 4 +- novelwriter/core/tree.py | 32 ++++- novelwriter/dialogs/docsplit.py | 11 -- novelwriter/gui/mainmenu.py | 4 +- novelwriter/gui/projtree.py | 127 +++++------------- .../guiEditor_Main_Final_031b4af5197ec.nwd | 2 +- .../guiEditor_Main_Final_1a6562590ef19.nwd | 2 +- .../guiEditor_Main_Final_41cfc0d1f2d12.nwd | 2 +- .../guiEditor_Main_Final_nwProject.nwx | 8 +- tests/test_core/test_core_tree.py | 10 +- tests/test_dialogs/test_dlg_docmerge.py | 6 +- tests/test_dialogs/test_dlg_docsplit.py | 8 +- tests/test_dialogs/test_dlg_itemeditor.py | 2 +- 14 files changed, 87 insertions(+), 132 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index f71453f3..796d0145 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -42,7 +42,6 @@ class nwConst(): FMT_DSTAMP = "%Y-%m-%d" # Date only format # Various Hard Limits - MAX_DEPTH = 30 # Maximum folder depth of a project MAX_DOCSIZE = 5000000 # Maxium size of a single document MAX_BUILDSIZE = 10000000 # Maxium size of a project build diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index af189597..893b3965 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -308,7 +308,9 @@ class NWItem(): """Set the default values based on the item's class and the project settings. """ - self.setClass(itemClass) + if self._parent is not None: + # Only update for child items + self.setClass(itemClass) if self._class in nwLists.CLS_NOVEL: self._layout = nwItemLayout.DOCUMENT diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index e1b68220..2983f88a 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -33,7 +33,7 @@ from hashlib import sha256 from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.error import logException from novelwriter.common import checkHandle -from novelwriter.constants import nwConst, nwFiles +from novelwriter.constants import nwFiles from novelwriter.core.item import NWItem logger = logging.getLogger(__name__) @@ -41,6 +41,8 @@ logger = logging.getLogger(__name__) class NWTree(): + MAX_DEPTH = 1000 # Cap of tree traversing for loops + def __init__(self, theProject): self.theProject = theProject @@ -219,7 +221,7 @@ class NWTree(): return False iItem = tItem - for _ in range(nwConst.MAX_DEPTH + 1): + for _ in range(self.MAX_DEPTH): if iItem.itemParent is None: tItem.setRoot(iItem.itemHandle) tItem.setClassDefaults(iItem.itemClass) @@ -228,8 +230,8 @@ class NWTree(): iItem = self.__getitem__(iItem.itemParent) if iItem is None: return False - - return False + else: + raise RecursionError("Critical internal error") def checkType(self, tHandle, itemType): """Return true of item exists and is of the specified item type. @@ -249,7 +251,7 @@ class NWTree(): tItem = self.__getitem__(tHandle) if tItem is not None: tTree.append(tHandle) - for _ in range(nwConst.MAX_DEPTH + 1): + for _ in range(self.MAX_DEPTH): if tItem.itemParent is None: return tTree else: @@ -259,6 +261,9 @@ class NWTree(): return tTree else: tTree.append(tHandle) + else: + raise RecursionError("Critical internal error") + return tTree ## @@ -270,6 +275,23 @@ class NWTree(): """ return tHandle in self._treeRoots + def isTrash(self, tHandle): + """Check if an item is in or is the trash folder. + """ + tItem = self.__getitem__(tHandle) + if tItem is None: + return True + if tItem.itemClass == nwItemClass.TRASH: + return True + if self._trashRoot is not None: + if tHandle == self._trashRoot: + return True + elif tItem.itemParent == self._trashRoot: + return True + elif tItem.itemRoot == self._trashRoot: + return True + return False + def isTrashRoot(self, tHandle): """Check if a handle is the trash folder. """ diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index b650671d..ff2cb849 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -34,7 +34,6 @@ from PyQt5.QtWidgets import ( from novelwriter.core import NWDoc from novelwriter.enum import nwAlert, nwItemType -from novelwriter.constants import nwConst from novelwriter.gui.custom import QHelpLabel logger = logging.getLogger(__name__) @@ -160,16 +159,6 @@ class GuiDocSplit(QDialog): ), nwAlert.ERROR) return False - # Check that another folder can be created - parTree = self.theProject.projTree.getItemPath(srcItem.itemParent) - if len(parTree) >= nwConst.MAX_DEPTH - 1: - self.theParent.makeAlert(self.tr( - "Cannot add new folder for the document split. " - "Maximum folder depth has been reached. " - "Please move the file to another level in the project tree." - ), nwAlert.ERROR) - return False - msgYes = self.theParent.askQuestion( self.tr("Split Document"), "{0}

{1}".format( diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py index 0b7efa60..f69d5a56 100644 --- a/novelwriter/gui/mainmenu.py +++ b/novelwriter/gui/mainmenu.py @@ -197,7 +197,7 @@ class GuiMainMenu(QMenuBar): # Project > New Folder self.aCreateFolder = QAction(self.tr("Create Folder"), self) self.aCreateFolder.setShortcut("Ctrl+Shift+N") - self.aCreateFolder.triggered.connect(lambda: self._newTreeItem(nwItemType.FOLDER, None)) + self.aCreateFolder.triggered.connect(lambda: self._newTreeItem(nwItemType.FOLDER)) self.projMenu.addAction(self.aCreateFolder) # Project > Separator @@ -259,7 +259,7 @@ class GuiMainMenu(QMenuBar): # Document > New self.aNewDoc = QAction(self.tr("New Document"), self) self.aNewDoc.setShortcut("Ctrl+N") - self.aNewDoc.triggered.connect(lambda: self._newTreeItem(nwItemType.FILE, None)) + self.aNewDoc.triggered.connect(lambda: self._newTreeItem(nwItemType.FILE)) self.docuMenu.addAction(self.aNewDoc) # Document > Open diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 584a281c..0f25f68c 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -37,7 +37,7 @@ from PyQt5.QtWidgets import ( from novelwriter.core import NWDoc from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.constants import nwConst, trConst, nwLists, nwLabels +from novelwriter.constants import trConst, nwLists, nwLabels logger = logging.getLogger(__name__) @@ -161,114 +161,64 @@ class GuiProjectTree(QTreeWidget): self._timeChanged = 0 return - def newTreeItem(self, itemType, itemClass): - """Add new item to the tree, with a given itemType and - itemClass, and attach it to the selected handle. Also make sure - the item is added in a place it can be added, and that other + def newTreeItem(self, itemType, itemClass=None): + """Add new item to the tree, with a given itemType (and + itemClass if Root), and attach it to the selected handle. Also 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. """ - pHandle = self.getSelectedHandle() - nHandle = None - if not self.theParent.hasProject: logger.error("No project open") return False - if not isinstance(itemType, nwItemType): - # This would indicate an internal bug - logger.error("No itemType provided") - return False + nHandle = None + tHandle = None - # The item needs to be assigned an item class, so one must be - # provided, or it must be possible to extract it from the parent - # item of the new item. - if itemClass is None and pHandle is not None: - pItem = self.theProject.projTree[pHandle] - if pItem is not None: - itemClass = pItem.itemClass + if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass): - # If class is still not set, alert the user and exit - if itemClass is None: - if itemType == nwItemType.FILE: - self.theParent.makeAlert(self.tr( - "Please select a valid location in the tree to add the document." - ), nwAlert.ERROR) - else: - self.theParent.makeAlert(self.tr( - "Please select a valid location in the tree to add the folder." - ), nwAlert.ERROR) - return False - - # Everything is fine, we have what we need, so we proceed - logger.verbose( - "Adding new item of type '%s' and class '%s' to handle '%s'", - itemType.name, itemClass.name, str(pHandle) - ) - - if itemType == nwItemType.ROOT: tHandle = self.theProject.newRoot( trConst(nwLabels.CLASS_NAME[itemClass]), itemClass ) - if tHandle is None: - logger.error("No root item added") - return False - else: - # If no parent has been selected, make the new file under - # the root NOVEL item. - if pHandle is None: - pHandle = self.theProject.projTree.findRoot(nwItemClass.NOVEL) + elif itemType in (nwItemType.FILE, nwItemType.FOLDER): - # If still nothing, give up - if pHandle is None: + sHandle = self.getSelectedHandle() + if sHandle is None or sHandle not in self.theProject.projTree: self.theParent.makeAlert(self.tr( "Did not find anywhere to add the file or folder!" ), nwAlert.ERROR) return False - # Now check if the selected item is a file, in which case - # the new file will be a sibling - pItem = self.theProject.projTree[pHandle] + # If the selected item is a file, the new item will be a sibling + pItem = self.theProject.projTree[sHandle] if pItem.itemType == nwItemType.FILE: - nHandle = pHandle - pHandle = pItem.itemParent + nHandle = sHandle + sHandle = pItem.itemParent + if sHandle is None: + logger.error("Internal error") # Bug + return False - # If we again have no home, give up - if pHandle is None: - self.theParent.makeAlert(self.tr( - "Did not find anywhere to add the file or folder!" - ), nwAlert.ERROR) - return False - - if self.theProject.projTree.isTrashRoot(pHandle): + if self.theProject.projTree.isTrash(sHandle): self.theParent.makeAlert(self.tr( "Cannot add new files or folders to the Trash folder." ), nwAlert.ERROR) return False - parTree = self.theProject.projTree.getItemPath(pHandle) - - # If we're still here, add the file or folder + # Add the file or folder if itemType == nwItemType.FILE: - tHandle = self.theProject.newFile(self.tr("New File"), pHandle) - + if pItem.itemClass in nwLists.CLS_NOVEL: + tHandle = self.theProject.newFile(self.tr("New Document"), sHandle) + else: + tHandle = self.theProject.newFile(self.tr("New Note"), sHandle) elif itemType == nwItemType.FOLDER: - if len(parTree) >= nwConst.MAX_DEPTH - 1: - # Folders cannot be deeper than MAX_DEPTH - 1, leaving room - # for one more level of files. - self.theParent.makeAlert(self.tr( - "Cannot add new folder to this item. " - "Maximum folder depth has been reached." - ), nwAlert.ERROR) - return False - tHandle = self.theProject.newFolder(self.tr("New Folder"), pHandle) + tHandle = self.theProject.newFolder(self.tr("New Folder"), sHandle) - else: - logger.error("Failed to add new item") - return False + else: + logger.error("Failed to add new item") + return False - # If there is no handle set, return here - if tHandle is None: + # If there is no handle set, return here. This is a bug + if tHandle is None: # pragma: no cover return True # Add the new item to the tree @@ -282,11 +232,7 @@ class GuiProjectTree(QTreeWidget): # This is a new file, so let's add some content newDoc = NWDoc(self.theProject, tHandle) - curTxt = newDoc.readDocument() - if curTxt is None: - curTxt = "" - - if curTxt == "": + if not newDoc.readDocument(): if nwItem.itemLayout == nwItemLayout.DOCUMENT: newText = f"### {nwItem.itemName}\n\n" else: @@ -633,7 +579,7 @@ class GuiProjectTree(QTreeWidget): return - def propagateCount(self, tHandle, theCount, nDepth=0): + def propagateCount(self, tHandle, theCount): """Recursive function setting the word count for a given item, and propagating that count upwards in the tree until reaching a root item. This function is more efficient than recalculating @@ -653,12 +599,13 @@ class GuiProjectTree(QTreeWidget): return pCount = 0 + pHandle = None for i in range(pItem.childCount()): pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole)) pHandle = pItem.data(self.C_NAME, Qt.UserRole) - if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "": - self.propagateCount(pHandle, pCount, nDepth+1) + if pHandle: + self.propagateCount(pHandle, pCount) return @@ -1180,7 +1127,7 @@ class GuiProjectTreeMenu(QMenu): """Forward the new file call to the project tree. """ if self.theItem is not None: - self.theTree.newTreeItem(nwItemType.FILE, None) + self.theTree.newTreeItem(nwItemType.FILE) return @pyqtSlot() @@ -1188,7 +1135,7 @@ class GuiProjectTreeMenu(QMenu): """Forward the new folder call to the project tree. """ if self.theItem is not None: - self.theTree.newTreeItem(nwItemType.FOLDER, None) + self.theTree.newTreeItem(nwItemType.FOLDER) return @pyqtSlot() diff --git a/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd b/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd index acb36501..6492390b 100644 --- a/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd +++ b/tests/reference/guiEditor_Main_Final_031b4af5197ec.nwd @@ -1,4 +1,4 @@ -%%~name: New File +%%~name: New Note %%~path: 44cb730c42048/031b4af5197ec %%~kind: PLOT/NOTE # Main Plot diff --git a/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd b/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd index 9a3ca0a9..1da5a713 100644 --- a/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd +++ b/tests/reference/guiEditor_Main_Final_1a6562590ef19.nwd @@ -1,4 +1,4 @@ -%%~name: New File +%%~name: New Note %%~path: 71ee45a3c0db9/1a6562590ef19 %%~kind: CHARACTER/NOTE # Jane Doe diff --git a/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd b/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd index 8e8cb037..14a58a49 100644 --- a/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd +++ b/tests/reference/guiEditor_Main_Final_41cfc0d1f2d12.nwd @@ -1,4 +1,4 @@ -%%~name: New File +%%~name: New Note %%~path: 811786ad1ae74/41cfc0d1f2d12 %%~kind: WORLD/NOTE # Main Location diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index 64ed4b4f..34f5a0ed 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project @@ -66,7 +66,7 @@
- New File + New Note @@ -74,7 +74,7 @@ - New File + New Note @@ -82,7 +82,7 @@ - New File + New Note diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 343c4609..6cfa9cf0 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -210,7 +210,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): @pytest.mark.core -def testCoreTree_Methods(monkeypatch, mockGUI, mockItems): +def testCoreTree_Methods(mockGUI, mockItems): """Test various class methods. """ theProject = NWProject(mockGUI) @@ -235,9 +235,11 @@ def testCoreTree_Methods(monkeypatch, mockGUI, mockItems): assert theTree.updateItemData("b000000000001") is True # Update item data, root is unreachable - with monkeypatch.context() as mp: - mp.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 0) - assert theTree.updateItemData("b000000000001") is False + maxDepth = theTree.MAX_DEPTH + theTree.MAX_DEPTH = 0 + with pytest.raises(RecursionError): + theTree.updateItemData("b000000000001") + theTree.MAX_DEPTH = maxDepth # Chech type assert theTree.checkType("blabla", nwItemType.FILE) is False diff --git a/tests/test_dialogs/test_dlg_docmerge.py b/tests/test_dialogs/test_dlg_docmerge.py index 6a3825b8..a4b21f45 100644 --- a/tests/test_dialogs/test_dlg_docmerge.py +++ b/tests/test_dialogs/test_dlg_docmerge.py @@ -58,9 +58,9 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj): nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.clearSelection() nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.newTreeItem(nwItemType.FILE) + nwGUI.treeView.newTreeItem(nwItemType.FILE) + nwGUI.treeView.newTreeItem(nwItemType.FILE) assert nwGUI.saveProject() is True assert nwGUI.closeProject() is True diff --git a/tests/test_dialogs/test_dlg_docsplit.py b/tests/test_dialogs/test_dlg_docsplit.py index d8d4bbac..66f8868a 100644 --- a/tests/test_dialogs/test_dlg_docsplit.py +++ b/tests/test_dialogs/test_dlg_docsplit.py @@ -62,7 +62,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj): nwGUI.switchFocus(nwWidget.TREE) nwGUI.treeView.clearSelection() nwGUI.treeView._getTreeItem(hNovelRoot).setSelected(True) - nwGUI.treeView.newTreeItem(nwItemType.FILE, None) + nwGUI.treeView.newTreeItem(nwItemType.FILE) assert nwGUI.saveProject() is True assert nwGUI.closeProject() is True @@ -230,12 +230,6 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj): mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) assert nwSplit._doSplit() is False - # Block folder creation by returning that the folder has a depth - # of 50 items in the tree - with monkeypatch.context() as mp: - mp.setattr(NWTree, "getItemPath", lambda *a: [""]*50) - assert nwSplit._doSplit() is False - # Clear the list nwSplit.listBox.clear() assert nwSplit._doSplit() is False diff --git a/tests/test_dialogs/test_dlg_itemeditor.py b/tests/test_dialogs/test_dlg_itemeditor.py index 10d72549..127d2e74 100644 --- a/tests/test_dialogs/test_dlg_itemeditor.py +++ b/tests/test_dialogs/test_dlg_itemeditor.py @@ -175,7 +175,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, constData): itemEdit.show() # Check Existing Settings - assert itemEdit.editName.text() == "New File" + assert itemEdit.editName.text() == "New Note" assert itemEdit.editStatus.currentData() == constData.importKeys[0] assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE assert itemEdit.editExport.isChecked() is True From d158de5d8265c0de1f6bc8e88d0faefdb957bcf6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Apr 2022 17:00:43 +0200 Subject: [PATCH 09/13] Update test coverage --- novelwriter/gui/projtree.py | 25 +- tests/conftest.py | 4 +- tests/test_core/test_core_project.py | 11 +- tests/test_core/test_core_tree.py | 110 ++++-- tests/test_gui/test_gui_projtree.py | 557 ++++++++++++++++++--------- 5 files changed, 474 insertions(+), 233 deletions(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 0f25f68c..5fdf7fcc 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -256,6 +256,9 @@ class GuiProjectTree(QTreeWidget): """Reveal a newly added project item in the project tree. """ nwItem = self.theProject.projTree[tHandle] + if nwItem is None: + return False + trItem = self._addTreeItem(nwItem, nHandle) if trItem is None: return False @@ -459,13 +462,6 @@ class GuiProjectTree(QTreeWidget): if doPermanent: logger.debug("Permanently deleting file with handle '%s'", tHandle) - self.propagateCount(tHandle, 0) - tIndex = trItemP.indexOfChild(trItemS) - trItemC = trItemP.takeChild(tIndex) - - if self.theParent.docEditor.docHandle() == tHandle: - self.theParent.closeDocument() - delDoc = NWDoc(self.theProject, tHandle) if not delDoc.deleteDocument(): self.theParent.makeAlert([ @@ -473,6 +469,13 @@ class GuiProjectTree(QTreeWidget): ], nwAlert.ERROR) return False + self.propagateCount(tHandle, 0) + tIndex = trItemP.indexOfChild(trItemS) + trItemC = trItemP.takeChild(tIndex) + + if self.theParent.docEditor.docHandle() == tHandle: + self.theParent.closeDocument() + self.theIndex.deleteHandle(tHandle) self._deleteTreeItem(tHandle) self._setTreeChanged(True) @@ -486,13 +489,10 @@ class GuiProjectTree(QTreeWidget): self.tr("Move file '{0}' to Trash?").format(nwItemS.itemName), ) if msgYes: - if pHandle is None: - logger.warning("File has no parent item") - logger.debug("Moving file '%s' to trash", tHandle) self.propagateCount(tHandle, 0) - tIndex = trItemP.indexOfChild(trItemS) + tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) trItemT.addChild(trItemC) self._updateItemParent(tHandle) @@ -508,6 +508,7 @@ class GuiProjectTree(QTreeWidget): if trItemP is None: logger.error("Could not delete folder") return False + tIndex = trItemP.indexOfChild(trItemS) if trItemS.childCount() == 0: trItemP.takeChild(tIndex) @@ -993,7 +994,7 @@ class GuiProjectTree(QTreeWidget): """ if self.theProject.projTree.checkType(tHandle, nwItemType.FILE): nwItem = self.theProject.projTree[tHandle] - if nwItem.itemClass == nwItemClass.NOVEL: + if nwItem.itemClass in nwLists.CLS_NOVEL: self.novelItemChanged.emit() else: self.noteItemChanged.emit() diff --git a/tests/conftest.py b/tests/conftest.py index 8551724b..a11db7e2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -166,7 +166,9 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf): """ monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) monkeypatch.setattr("novelwriter.CONFIG", fncConf) - nwGUI = novelwriter.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % fncDir]) + nwGUI = novelwriter.main( + ["--testmode", "--info", "--config=%s" % fncDir, "--data=%s" % fncDir] + ) qtbot.addWidget(nwGUI) nwGUI.show() qtbot.wait(20) diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 3d5b37f4..b584679d 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -1016,9 +1016,16 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): """ theProject = NWProject(mockGUI) - assert theProject.openProject(nwLipsum) + assert theProject.openProject(nwLipsum) is True assert theProject.projTree["636b6aa9b697b"] is None - assert theProject.closeProject() + + # Add a file with non-existent parent + # This file will be renoved from the project on open + assert theProject.newFile("Oops", "0000000000000") + + # Save and close + assert theProject.saveProject() is True + assert theProject.closeProject() is True # First Item with Meta Data orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd") diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 6cfa9cf0..3e8aeeca 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -21,6 +21,7 @@ along with this program. If not, see . import os import pytest +import random from lxml import etree from hashlib import sha256 @@ -36,6 +37,7 @@ from novelwriter.constants import nwFiles def mockItems(mockGUI): """Create a list of mock items. """ + random.seed(42) theProject = NWProject(mockGUI) itemA = NWItem(theProject) @@ -151,8 +153,28 @@ def testCoreTree_BuildTree(mockGUI, mockItems): # Check that we have the correct archive and trash folders assert theTree.trashRoot() == "a000000000003" assert theTree.archiveRoot() == "a000000000002" - assert theTree.isTrashRoot("a000000000003") - assert theTree.isRoot("a000000000002") + assert theTree.isTrashRoot("a000000000003") is True + assert theTree.isRoot("a000000000002") is True + + # Check the isTrash function + assert theTree.isTrash("0000000000000") is True # Doesn't exist + assert theTree.isTrash("a000000000003") is True # This the trash folder + + theTree["a000000000003"].setClass(nwItemClass.NO_CLASS) + assert theTree.isTrash("a000000000003") is True # This is still trash + theTree["a000000000003"].setClass(nwItemClass.TRASH) + + assert theTree.isTrash("b000000000002") is False # This is not trash + + value = theTree["b000000000002"].itemParent + theTree["b000000000002"].setParent("a000000000003") + assert theTree.isTrash("b000000000002") is True # This is in trash + theTree["b000000000002"].setParent(value) + + value = theTree["b000000000002"].itemRoot + theTree["b000000000002"].setRoot("a000000000003") + assert theTree.isTrash("b000000000002") is True # This is in trash + theTree["b000000000002"].setRoot(value) # Try to add another trash folder itemT = NWItem(theProject) @@ -161,7 +183,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems): itemT._class = nwItemClass.TRASH itemT._expanded = False - assert not theTree.append("1234567890abc", None, itemT) + assert theTree.append("1234567890abc", None, itemT) is False assert len(theTree) == len(mockItems) # Generate handle automatically @@ -251,12 +273,24 @@ def testCoreTree_Methods(mockGUI, mockItems): assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001" assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004" + # Add a fake item to root and check that it can handle it + theTree._treeRoots["0000000000000"] = NWItem(theProject) + assert theTree.findRoot(nwItemClass.WORLD) is None + del theTree._treeRoots["0000000000000"] + # Get item path assert theTree.getItemPath("stuff") == [] assert theTree.getItemPath("c000000000001") == [ "c000000000001", "b000000000001", "a000000000001" ] + # Cause recursion error + maxDepth = theTree.MAX_DEPTH + theTree.MAX_DEPTH = 0 + with pytest.raises(RecursionError): + theTree.getItemPath("c000000000001") + theTree.MAX_DEPTH = maxDepth + # Break the folder parent handle theTree["b000000000001"]._parent = "stuff" assert theTree.getItemPath("c000000000001") == [ @@ -381,7 +415,7 @@ def testCoreTree_Reorder(mockGUI, mockItems): @pytest.mark.core -def testCoreTree_XMLPackUnpack(mockGUI, mockItems): +def testCoreTree_XMLPackUnpack(mockGUI, mockItems, constData): """Test packing and unpacking the tree to and from XML. """ theProject = NWProject(mockGUI) @@ -395,39 +429,41 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems): nwXML = etree.Element("novelWriterXML") theTree.packXML(nwXML) - assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == ( - b'' - b'' - b'Novel' - b'Act One' - b'Chapter One' - b'Scene One' - b'Outtakes' - b'Trash' - b'Characters' - b'Jane Doe' - b'' - b'' - ) + assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == bytes(( + '' + '' + 'Novel' + 'Act One' + 'Chapter One' + 'Scene One' + 'Outtakes' + 'Trash' + 'Characters' + 'Jane Doe' + '' + '' + ).format( + s0=constData.statusKeys[0], i0=constData.importKeys[0] + ), encoding="utf8") theTree.clear() assert len(theTree) == 0 diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 9d43481c..1f1c8c95 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -22,9 +22,6 @@ along with this program. If not, see . import pytest import os -from tools import writeFile - -from PyQt5.QtCore import QItemSelectionModel from PyQt5.QtWidgets import QAction, QMessageBox from novelwriter.guimain import GuiMain @@ -33,208 +30,406 @@ from novelwriter.enum import nwItemType, nwItemClass @pytest.mark.gui -def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal): +def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): """Test adding and removing items from the project tree. """ # Block message box - monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) - monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) - nwGUI.theProject.projTree.setSeed(42) nwTree = nwGUI.treeView - ## - # Add New Items - ## + # Try to add item with no project + assert nwTree.newTreeItem(nwItemType.FILE) is False - # Try to add and move item with no project - assert nwTree.newTreeItem(nwItemType.FILE, None) is False - assert nwTree.moveTreeItem(1) is False + # Create a project + nwGUI.theProject.projTree.setSeed(42) + prjDir = os.path.join(fncDir, "project") + assert nwGUI.newProject({"projPath": prjDir}) is True - # Open a project - assert nwGUI.openProject(nwMinimal) is True + # No itemType set + nwTree.clearSelection() + assert nwTree.newTreeItem(None) is False + + # Root Items + # ========== + + # No class set + assert nwTree.newTreeItem(nwItemType.ROOT) is False + + # Create root item + assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True + assert "1a6562590ef19" in nwGUI.theProject.projTree + + # File/Folder Items + # ================= # No location selected for new item nwTree.clearSelection() - assert nwTree.newTreeItem(nwItemType.FILE, None) is False - assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False - assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is True + caplog.clear() + assert nwTree.newTreeItem(nwItemType.FILE) is False + assert nwTree.newTreeItem(nwItemType.FOLDER) is False + assert "Did not find anywhere" in caplog.text - # No itemType set or ROOT, but no class - nwTree.clearSelection() - assert nwTree.newTreeItem(None, None) is False - assert nwTree.newTreeItem(nwItemType.ROOT, None) is False + # Create new folder as child of Novel folder + nwTree.setSelectedHandle("73475cb40a568") + assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert nwGUI.theProject.projTree["031b4af5197ec"].itemParent == "73475cb40a568" + assert nwGUI.theProject.projTree["031b4af5197ec"].itemRoot == "73475cb40a568" + assert nwGUI.theProject.projTree["031b4af5197ec"].itemClass == nwItemClass.NOVEL - # Select a location - chItem = nwTree._getTreeItem("a6d311a93600a") - nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) - chItem.setExpanded(True) + # Add a new file in the new folder + nwTree.setSelectedHandle("031b4af5197ec") + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemParent == "031b4af5197ec" + assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemRoot == "73475cb40a568" + assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemClass == nwItemClass.NOVEL - # Create new item with no class set (defaults to NOVEL) - assert nwTree.newTreeItem(nwItemType.FILE, None) is True - assert nwTree.newTreeItem(nwItemType.FOLDER, None) is True + # Add a new file next to the other new file + nwTree.setSelectedHandle("41cfc0d1f2d12") + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwGUI.theProject.projTree["2858dcd1057d3"].itemParent == "031b4af5197ec" + assert nwGUI.theProject.projTree["2858dcd1057d3"].itemRoot == "73475cb40a568" + assert nwGUI.theProject.projTree["2858dcd1057d3"].itemClass == nwItemClass.NOVEL + assert nwGUI.openDocument("2858dcd1057d3") + assert nwGUI.docEditor.getText() == "### New Document\n\n" - # Check that we have the correct tree order - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9" - ] + # Add a new file to the characters folder + nwTree.setSelectedHandle("71ee45a3c0db9") + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwGUI.theProject.projTree["2fca346db6561"].itemParent == "71ee45a3c0db9" + assert nwGUI.theProject.projTree["2fca346db6561"].itemRoot == "71ee45a3c0db9" + assert nwGUI.theProject.projTree["2fca346db6561"].itemClass == nwItemClass.CHARACTER + assert nwGUI.openDocument("2fca346db6561") + assert nwGUI.docEditor.getText() == "# New Note\n\n" - # Add more roots - assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True # Duplicate - assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) is True # Unique + # Make sure the sibling folder bug trap works + nwTree.setSelectedHandle("2858dcd1057d3") + nwGUI.theProject.projTree["2858dcd1057d3"].setParent(None) # This should not happen + caplog.clear() + assert nwTree.newTreeItem(nwItemType.FILE) is False + assert "Internal error" in caplog.text + nwGUI.theProject.projTree["2858dcd1057d3"].setParent("031b4af5197ec") - # Change max depth and try to add a subfolder that is too deep - monkeypatch.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 2) - chItem = nwTree._getTreeItem("71ee45a3c0db9") - nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) - assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False + # Get the trash folder + nwTree._addTrashRoot() + trashHandle = nwGUI.theProject.trashFolder() + nwTree.setSelectedHandle(trashHandle) + assert nwTree.newTreeItem(nwItemType.FILE) is False + assert "Cannot add new files or folders to the Trash folder" in caplog.text - ## - # Move Items - ## + # Other Checks + # ============ - nwTree.setSelectedHandle("8c659a11cd429") + # Also check error handling in reveal function + assert nwTree.revealNewTreeItem("abc") is False - # Shift focus and try to move item - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) - assert nwTree.moveTreeItem(1) is False - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9" - ] - monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) - - # Move second item up twice (should give same result) - nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9" - ] - nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9" - ] - - # Move it back down four times (last two should be the same) - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9" - ] - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "8c659a11cd429", "71ee45a3c0db9" - ] - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429" - ] - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429" - ] - - # Move up twice, and undo - nwTree._lastMove = {} - nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) - nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) - nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger) - assert nwTree.getTreeFromHandle("a6d311a93600a") == [ - "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429" - ] - - # Move a root item (top level items are different) twice - nwTree.flushTreeOrder() - assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10 - nwTree.setSelectedHandle("9d5247ab588e0") - - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) - nwTree.flushTreeOrder() - assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11 - - nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) - nwTree.flushTreeOrder() - assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 12 - - ## - # Delete and Trash - ## - - # Add some content to the new file - nwGUI.openDocument("73475cb40a568") - nwGUI.docEditor.setText("# Hello World\n") - nwGUI.saveDocument() - nwGUI.saveProject() - assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd")) - - # Delete the items we added earlier - nwTree.clearSelection() - assert nwTree.emptyTrash() is False # No folder yet - assert nwTree.deleteItem(None) is False - assert nwTree.deleteItem("1111111111111") is False - assert nwTree.deleteItem("73475cb40a568") is True # New File - assert nwTree.deleteItem("71ee45a3c0db9") is True # New Folder - assert nwTree.deleteItem("811786ad1ae74") is True # Custom Root - assert "73475cb40a568" in nwGUI.theProject.projTree._treeOrder - assert "71ee45a3c0db9" not in nwGUI.theProject.projTree._treeOrder - assert "811786ad1ae74" not in nwGUI.theProject.projTree._treeOrder - - # The file is in trash, empty it - assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd")) - assert nwTree.emptyTrash() is True - assert nwTree.emptyTrash() is False # Already empty - assert not os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd")) - assert "73475cb40a568" not in nwGUI.theProject.projTree._treeOrder - - # Should not be allowed to add files and folders to Trash - trashHandle = nwGUI.theProject.projTree.trashRoot() - chItem = nwTree._getTreeItem(trashHandle) - nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) - assert nwTree.newTreeItem(nwItemType.FILE, None) is False - assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False - - # Close the project - nwGUI.closeProject() - - ## - # Orphaned Files - ## - - # Add an orphaned file - orphFile = os.path.join(nwMinimal, "content", "1234567890abc.nwd") - writeFile(orphFile, "# Hello World\n") - - # Open the project again - nwGUI.openProject(nwMinimal) - - # Check that the orphaned file was found and added to the tree - nwTree.flushTreeOrder() - assert "1234567890abc" in nwGUI.theProject.projTree._treeOrder - orItem = nwTree._getTreeItem("1234567890abc") - assert orItem.text(nwTree.C_NAME) == "Recovered File 1" - - ## - # Unexpected Error Handling - ## - - # Add an item with an invalid type - assert nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL) is False - assert "Failed to add new item" in caplog.messages[-1] - - # Add new file after one that has no parent handle - chItem = nwTree._getTreeItem("44cb730c42048") - nwTree.setCurrentItem(chItem, QItemSelectionModel.Current) - nwTree.theProject.projTree["44cb730c42048"]._parent = None - assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is False - nwTree.clearSelection() - - # Add a file with no parent, and fail to find a suitable parent item - monkeypatch.setattr("novelwriter.core.tree.NWTree.findRoot", lambda *a: None) - - assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is False - assert nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL) is False + # Add an item that cannot be displayed in the tree + nHandle = nwGUI.theProject.newFile("Test", None) + assert nwTree.revealNewTreeItem(nHandle) is False + # Clean up # qtbot.stopForInteraction() nwGUI.closeProject() -# END Test testGuiProjTree_TreeItems +# END Test testGuiProjTree_NewItems + + +@pytest.mark.gui +def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir): + """Test adding and removing items from the project tree. + """ + # Block message box + monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + + nwTree = nwGUI.treeView + + # Try to move item with no project + assert nwTree.moveTreeItem(1) is False + + # Create a project + nwGUI.theProject.projTree.setSeed(42) + prjDir = os.path.join(fncDir, "project") + assert nwGUI.newProject({"projPath": prjDir}) is True + + # Move Documents + # ============== + + # Add some files + nwTree.setSelectedHandle("31489056e0916") + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + + # Move item without focus + monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) + assert nwTree.moveTreeItem(1) is False + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) + + # Move with no selections + nwTree.clearSelection() + assert nwTree.moveTreeItem(1) is False + + # Move second item up twice (should give same result) + nwTree.setSelectedHandle("0e17daca5f3e1") + assert nwTree.moveTreeItem(-1) is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "0e17daca5f3e1", "98010bd9270f9", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + assert nwTree.moveTreeItem(-1) is False + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "0e17daca5f3e1", "98010bd9270f9", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + + # Restore via menu entry + nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + + # Move fifth item down twice (should give same result) + nwTree.setSelectedHandle("031b4af5197ec") + assert nwTree.moveTreeItem(1) is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec", + ] + assert nwTree.moveTreeItem(1) is False + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec", + ] + + # Restore via menu entry + nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + + # Move down again, and restore via undo + nwTree.setSelectedHandle("031b4af5197ec") + assert nwTree.moveTreeItem(1) is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec", + ] + nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger) + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + + # Root Folder + # =========== + + nwTree.setSelectedHandle("73475cb40a568") + assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0 + + # Move novel folder up + assert nwTree.moveTreeItem(-1) is False + nwTree.flushTreeOrder() + assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0 + + # Move novel folder down + assert nwTree.moveTreeItem(1) is True + nwTree.flushTreeOrder() + assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 1 + + # Move novel folder up again + assert nwTree.moveTreeItem(-1) is True + nwTree.flushTreeOrder() + assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0 + + # Clean up + # qtbot.stopForInteraction() + nwGUI.closeProject() + +# END Test testGuiProjTree_MoveItems + + +@pytest.mark.gui +def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir): + """Test adding and removing items from the project tree. + """ + # Block message box + monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes) + monkeypatch.setattr(GuiMain, "editItem", lambda *a: None) + + nwTree = nwGUI.treeView + + # Try to run with no project + assert nwTree.emptyTrash() is False + assert nwTree.deleteItem() is False + + # Create a project + nwGUI.theProject.projTree.setSeed(42) + prjDir = os.path.join(fncDir, "project") + assert nwGUI.newProject({"projPath": prjDir}) is True + + # Try emptying the trash already now, when there is no trash folder + assert nwTree.emptyTrash() is False + + # Add some files + nwTree.setSelectedHandle("31489056e0916") + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.newTreeItem(nwItemType.FILE) is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12", + ] + + # Delete File + # =========== + + # Delete item without focus -> blocked + monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False) + nwTree.setSelectedHandle("41cfc0d1f2d12") + caplog.clear() + assert nwTree.deleteItem() is False + assert "blocked" in caplog.text + monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True) + + # No selection made + nwTree.clearSelection() + caplog.clear() + assert nwTree.deleteItem() is False + assert "no item to delete" in caplog.text + + # Not a valid handle + nwTree.clearSelection() + caplog.clear() + assert nwTree.deleteItem("0000000000000") is False + assert "Could not find tree item" in caplog.text + + # Block adding trash folder + funcPointer = nwTree._addTrashRoot + nwTree._addTrashRoot = lambda *a: None + assert nwTree.deleteItem("41cfc0d1f2d12") is False + nwTree._addTrashRoot = funcPointer + + # Delete last two documents, which also adds the trash folder + assert nwTree.deleteItem("41cfc0d1f2d12") is True + assert nwTree.deleteItem("031b4af5197ec") is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9", "0e17daca5f3e1", + "1a6562590ef19" + ] + trashHandle = nwGUI.theProject.projTree.trashRoot() + assert nwTree.getTreeFromHandle(trashHandle) == [ + trashHandle, "41cfc0d1f2d12", "031b4af5197ec" + ] + + # Delete the first file again (permanent), and ask for permission + # Also open the document in the editor, which should trigger a close + assert os.path.isfile(os.path.join(prjDir, "content", "41cfc0d1f2d12.nwd")) + assert "41cfc0d1f2d12" in nwGUI.theProject.projTree + assert nwGUI.docEditor.docHandle() is None + assert nwGUI.openDocument("41cfc0d1f2d12") is True + assert nwGUI.docEditor.docHandle() == "41cfc0d1f2d12" + assert nwTree.deleteItem("41cfc0d1f2d12") is True + assert nwGUI.docEditor.docHandle() is None + assert not os.path.isfile(os.path.join(prjDir, "content", "41cfc0d1f2d12.nwd")) + assert "41cfc0d1f2d12" not in nwGUI.theProject.projTree + assert nwTree.getTreeFromHandle(trashHandle) == [ + trashHandle, "031b4af5197ec" + ] + + # Try to delete the second document, but block the deletion + with monkeypatch.context() as mp: + mp.setattr("novelwriter.core.document.NWDoc.deleteDocument", lambda *a: False) + assert nwTree.deleteItem("031b4af5197ec") is False + + # Delete proper, and skip asking for permission + assert os.path.isfile(os.path.join(prjDir, "content", "031b4af5197ec.nwd")) + assert "031b4af5197ec" in nwGUI.theProject.projTree + assert nwTree.deleteItem("031b4af5197ec", alreadyAsked=True) is True + assert not os.path.isfile(os.path.join(prjDir, "content", "031b4af5197ec.nwd")) + assert "031b4af5197ec" not in nwGUI.theProject.projTree + assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] + + # Delete Folder/Root + # ================== + + # Deleting non-empty folders is blocked + assert nwTree.deleteItem("31489056e0916") is False # Folder + assert nwTree.deleteItem("73475cb40a568") is False # Root + + # Add a folder we can delete + nwTree.setSelectedHandle("71ee45a3c0db9") # Character Root + assert nwTree.newTreeItem(nwItemType.FOLDER) is True + assert "2fca346db6561" in nwGUI.theProject.projTree + + # Try to delete, but block parent item lookup + with monkeypatch.context() as mp: + mp.setattr("PyQt5.QtWidgets.QTreeWidgetItem.parent", lambda *a: None) + caplog.clear() + assert nwTree.deleteItem("2fca346db6561") is False + assert "Could not delete folder" in caplog.text + assert "2fca346db6561" in nwGUI.theProject.projTree + + # Delete folder properly + assert nwTree.deleteItem("2fca346db6561") is True + assert "2fca346db6561" not in nwGUI.theProject.projTree + + # Delete the Character root + assert nwTree.deleteItem("71ee45a3c0db9") is True + assert "71ee45a3c0db9" not in nwGUI.theProject.projTree + + # Empty Trash + # =========== + + # Try to empty trash that is already empty + caplog.clear() + assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] + assert nwTree.emptyTrash() is False + assert "already empty" in caplog.text + + # Move the two remaining scene documents to trash + assert nwTree.deleteItem("0e17daca5f3e1") is True + assert nwTree.deleteItem("1a6562590ef19") is True + assert nwTree.getTreeFromHandle("31489056e0916") == [ + "31489056e0916", "98010bd9270f9" + ] + assert nwTree.getTreeFromHandle(trashHandle) == [ + trashHandle, "0e17daca5f3e1", "1a6562590ef19" + ] + + # Empty trash, but select no on question + with monkeypatch.context() as mp: + mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No) + assert nwTree.emptyTrash() is False + + # Empty the trash proper + nwTree._setTreeChanged(False) + assert nwTree.emptyTrash() is True + assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle] + assert nwTree._treeChanged is True + + # Clean up + # qtbot.stopForInteraction() + nwGUI.closeProject() + +# END Test testGuiProjTree_DeleteItems From 50db7fcddbc49f36930fea4afa7a8a3794778f7f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Apr 2022 17:49:47 +0200 Subject: [PATCH 10/13] Simplify updating an item after it has been moved --- novelwriter/constants.py | 11 +---- novelwriter/core/item.py | 38 ++++++++++++--- novelwriter/core/project.py | 4 +- novelwriter/dialogs/itemeditor.py | 6 +-- novelwriter/gui/projtree.py | 73 +++++++++------------------- novelwriter/guimain.py | 3 +- tests/test_core/test_core_project.py | 2 +- 7 files changed, 62 insertions(+), 75 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 796d0145..dbdc3664 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -25,7 +25,7 @@ along with this program. If not, see . from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP -from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwOutline +from novelwriter.enum import nwItemClass, nwItemLayout, nwOutline def trConst(tString): @@ -51,18 +51,9 @@ class nwConst(): class nwLists(): """Lists used for grouping various other constants. """ - # Regular user-accessible item types - REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE} - - # Item classes where the full list of novel layouts are allowed - CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE} - # Item classes which do not require items to have same class FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH} - # Deprecated nwItemLayout entries - DEP_LAYOUT = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE") - # END Class nwLists diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 893b3965..3a741f62 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -31,10 +31,13 @@ from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.common import ( checkInt, isHandle, isItemClass, isItemLayout, isItemType, simplified ) -from novelwriter.constants import nwLabels, nwLists, trConst +from novelwriter.constants import nwLabels, trConst logger = logging.getLogger(__name__) +# Deprecated layout labels +DEP_LAYOUTS = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE") + class NWItem(): @@ -282,11 +285,27 @@ class NWItem(): return trConst(nwLabels.ITEM_DESCRIPTION.get(descKey, "")) + def isNovelLike(self): + """Returns true if the item is of a novel-like class. + """ + return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE) + + def documentAllowed(self): + """Returns true if the item is allowed to be of document layout. + """ + return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE, nwItemClass.TRASH) + + def isInactive(self): + """Returns true if the item is in the inactive parts of the + project. + """ + return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH) + def getImportStatus(self): """Return the relevant importance or status label and icon for the current item based on its class. """ - if self._class in nwLists.CLS_NOVEL: + if self.isNovelLike(): stName = self.theProject.statusItems.name(self._status) stIcon = self.theProject.statusItems.icon(self._status) else: @@ -298,7 +317,7 @@ class NWItem(): """Update the importance or status value based on class. This is a wrapper setter for setStatus and setImport. """ - if self._class in nwLists.CLS_NOVEL: + if self.isNovelLike(): self.setStatus(value) else: self.setImport(value) @@ -312,9 +331,14 @@ class NWItem(): # Only update for child items self.setClass(itemClass) - if self._class in nwLists.CLS_NOVEL: - self._layout = nwItemLayout.DOCUMENT - else: + if self._layout == nwItemLayout.NO_LAYOUT: + # If no layout is set, pick one + if self.isNovelLike(): + self._layout = nwItemLayout.DOCUMENT + else: + self._layout = nwItemLayout.NOTE + elif not self.documentAllowed(): + # Change layout to note if it is not in an allowed folder self._layout = nwItemLayout.NOTE if self._status is None: @@ -411,7 +435,7 @@ class NWItem(): self._layout = itemLayout elif isItemLayout(itemLayout): self._layout = nwItemLayout[itemLayout] - elif itemLayout in nwLists.DEP_LAYOUT: + elif itemLayout in DEP_LAYOUTS: self._layout = nwItemLayout.DOCUMENT else: logger.error("Unrecognised item layout '%s'", itemLayout) diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 28aba590..0d24d74c 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -46,7 +46,7 @@ from novelwriter.common import ( checkString, checkBool, checkInt, isHandle, formatTimeStamp, makeFileNameSafe, hexToInt, simplified ) -from novelwriter.constants import nwLists, trConst, nwFiles, nwLabels +from novelwriter.constants import trConst, nwFiles, nwLabels logger = logging.getLogger(__name__) @@ -1197,7 +1197,7 @@ class NWProject(): self.statusItems.resetCounts() self.importItems.resetCounts() for nwItem in self.projTree: - if nwItem.itemClass in nwLists.CLS_NOVEL: + if nwItem.isNovelLike(): self.statusItems.increment(nwItem.itemStatus) else: self.importItems.increment(nwItem.itemImport) diff --git a/novelwriter/dialogs/itemeditor.py b/novelwriter/dialogs/itemeditor.py index 84a0db14..b5faec0d 100644 --- a/novelwriter/dialogs/itemeditor.py +++ b/novelwriter/dialogs/itemeditor.py @@ -33,7 +33,7 @@ from PyQt5.QtWidgets import ( ) from novelwriter.enum import nwItemLayout, nwItemType -from novelwriter.constants import trConst, nwLists, nwLabels +from novelwriter.constants import trConst, nwLabels from novelwriter.gui.custom import QSwitch logger = logging.getLogger(__name__) @@ -74,7 +74,7 @@ class GuiItemEditor(QDialog): # Item Status self.editStatus = QComboBox() self.editStatus.setMinimumWidth(mVd) - if self.theItem.itemClass in nwLists.CLS_NOVEL: + if self.theItem.isNovelLike(): for key, entry in self.theProject.statusItems.items(): self.editStatus.addItem(entry["icon"], entry["name"], key) @@ -95,7 +95,7 @@ class GuiItemEditor(QDialog): self.editLayout.setMinimumWidth(mVd) validLayouts = [] if self.theItem.itemType == nwItemType.FILE: - if self.theItem.itemClass in nwLists.CLS_NOVEL: + if self.theItem.documentAllowed(): validLayouts.append(nwItemLayout.DOCUMENT) validLayouts.append(nwItemLayout.NOTE) else: diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 5fdf7fcc..c75a0f58 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -206,7 +206,7 @@ class GuiProjectTree(QTreeWidget): # Add the file or folder if itemType == nwItemType.FILE: - if pItem.itemClass in nwLists.CLS_NOVEL: + if pItem.isNovelLike(): tHandle = self.theProject.newFile(self.tr("New Document"), sHandle) else: tHandle = self.theProject.newFile(self.tr("New Note"), sHandle) @@ -495,10 +495,7 @@ class GuiProjectTree(QTreeWidget): tIndex = trItemP.indexOfChild(trItemS) trItemC = trItemP.takeChild(tIndex) trItemT.addChild(trItemC) - self._updateItemParent(tHandle) - self.propagateCount(tHandle, wCount) - - self.theIndex.deleteHandle(tHandle) + self._postItemMove(tHandle, wCount) self._recordLastMove(trItemS, trItemP, tIndex) self._setTreeChanged(True) @@ -661,9 +658,7 @@ class GuiProjectTree(QTreeWidget): movItem = parItem.takeChild(srcIndex) dstItem.insertChild(dstIndex, movItem) - snItem = self.theProject.projTree[sHandle] - dnItem = self.theProject.projTree[dHandle] - self._postItemMove(sHandle, snItem, dnItem, wCount) + self._postItemMove(sHandle, wCount) self.clearSelection() movItem.setSelected(True) @@ -812,7 +807,7 @@ class GuiProjectTree(QTreeWidget): logger.debug("Drag'n'drop of item '%s' accepted", sHandle) self.propagateCount(sHandle, 0) QTreeWidget.dropEvent(self, theEvent) - self._postItemMove(sHandle, snItem, dnItem, wCount) + self._postItemMove(sHandle, wCount) self._recordLastMove(sItem, pItem, pIndex) else: @@ -828,40 +823,37 @@ class GuiProjectTree(QTreeWidget): # Internal Functions ## - def _postItemMove(self, sHandle, snItem, dnItem, wCount): + def _postItemMove(self, tHandle, wCount): """Run various maintenance tasks for a moved item. """ - isFile = snItem.itemType == nwItemType.FILE - isSame = snItem.itemClass == dnItem.itemClass - onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile + trItemS = self._getTreeItem(tHandle) + nwItemS = self.theProject.projTree[tHandle] + trItemP = trItemS.parent() + if trItemP is None: + logger.error("Failed to find new parent item of '%s'", tHandle) + return False - self._updateItemParent(sHandle) + pHandle = trItemP.data(self.C_NAME, Qt.UserRole) + nwItemS.setParent(pHandle) + self.theProject.projTree.updateItemData(tHandle) + self.setTreeItemValues(tHandle) + self.propagateCount(tHandle, wCount) - # If the item does not have the same class as the target, - # and the target is not a free root folder, update its class - if not (isSame or onFree): - logger.debug( - "Item '%s' class has been changed from '%s' to '%s'", - sHandle, snItem.itemClass.name, dnItem.itemClass.name - ) - snItem.setClass(dnItem.itemClass) - self.setTreeItemValues(sHandle) - - self.propagateCount(sHandle, wCount) + logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle) # The items dropped into archive or trash should be removed # from the project index, for all other items, we rescan the # file to ensure the index is up to date. - if onFree: - self.theIndex.deleteHandle(sHandle) + if nwItemS.isInactive(): + self.theIndex.deleteHandle(tHandle) else: - self.theIndex.reIndexHandle(sHandle) + self.theIndex.reIndexHandle(tHandle) # Trigger dependent updates self._setTreeChanged(True) - self._emitItemChange(sHandle) + self._emitItemChange(tHandle) - return + return True def _getTreeItem(self, tHandle): """Returns the QTreeWidgetItem of a given item handle. @@ -961,25 +953,6 @@ class GuiProjectTree(QTreeWidget): return trItem - def _updateItemParent(self, tHandle): - """Update the parent handle of an item so that the information - in the project is consistent with the treeView. - """ - trItemS = self._getTreeItem(tHandle) - nwItemS = self.theProject.projTree[tHandle] - trItemP = trItemS.parent() - if trItemP is None: - logger.error("Failed to find new parent item of '%s'", tHandle) - return False - - pHandle = trItemP.data(self.C_NAME, Qt.UserRole) - nwItemS.setParent(pHandle) - self.setTreeItemValues(tHandle) - - logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle) - - return True - def _setTreeChanged(self, theState): """Set the tree change flag, and propagate to the project. """ @@ -994,7 +967,7 @@ class GuiProjectTree(QTreeWidget): """ if self.theProject.projTree.checkType(tHandle, nwItemType.FILE): nwItem = self.theProject.projTree[tHandle] - if nwItem.itemClass in nwLists.CLS_NOVEL: + if nwItem.isNovelLike(): self.novelItemChanged.emit() else: self.noteItemChanged.emit() diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index c21fc091..fabd3867 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -55,7 +55,6 @@ from novelwriter.enum import ( nwItemType, nwItemClass, nwAlert, nwWidget, nwState ) from novelwriter.common import getGuiItem, hexToInt -from novelwriter.constants import nwLists logger = logging.getLogger(__name__) @@ -848,7 +847,7 @@ class GuiMain(QMainWindow): tItem = self.theProject.projTree[tHandle] if tItem is None: return False - if tItem.itemType not in nwLists.REG_TYPES: + if tItem.itemType == nwItemType.NO_TYPE: return False logger.verbose("Requesting change to item '%s'", tHandle) diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index b584679d..8a962268 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -1076,7 +1076,7 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum): assert oItem.itemParent == "b3643d0f92e32" assert oItem.itemClass == nwItemClass.NOVEL assert oItem.itemType == nwItemType.FILE - assert oItem.itemLayout == nwItemLayout.DOCUMENT + assert oItem.itemLayout == nwItemLayout.NOTE assert theProject.saveProject(nwLipsum) assert theProject.closeProject() From 3f4356467ef9f9ec580091eaaff9d4430e07b736 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Apr 2022 18:32:22 +0200 Subject: [PATCH 11/13] Relax moving restrictions in project tree, and improve test coverage of NWItem class --- novelwriter/core/item.py | 9 ++- novelwriter/gui/projtree.py | 18 ++--- tests/test_core/test_core_item.py | 115 +++++++++++++++++++++++++++++- 3 files changed, 130 insertions(+), 12 deletions(-) diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 3a741f62..0178c512 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -259,7 +259,7 @@ class NWItem(): return ## - # Methods + # Lookup Methods ## def describeMe(self, hLevel=None): @@ -296,8 +296,7 @@ class NWItem(): return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE, nwItemClass.TRASH) def isInactive(self): - """Returns true if the item is in the inactive parts of the - project. + """Returns true if the item is in an inactive class. """ return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH) @@ -313,6 +312,10 @@ class NWItem(): stIcon = self.theProject.importItems.icon(self._import) return stName, stIcon + ## + # Special Setters + ## + def setImportStatus(self, value): """Update the importance or status value based on class. This is a wrapper setter for setStatus and setImport. diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index c75a0f58..baefcfea 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -37,7 +37,7 @@ from PyQt5.QtWidgets import ( from novelwriter.core import NWDoc from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert -from novelwriter.constants import trConst, nwLists, nwLabels +from novelwriter.constants import trConst, nwLabels logger = logging.getLogger(__name__) @@ -790,21 +790,23 @@ class GuiProjectTree(QTreeWidget): if pItem is not None: pIndex = pItem.indexOfChild(sItem) - wCount = int(sItem.data(self.C_COUNT, Qt.UserRole)) + # Determine if the drag and drop is allowed: + # - Files can be moved anywhere + # - Folders can only be moved within the same root folder + # - Root folders cannot be moved at all + # - Items cannot be dropped on top of a file (moved inside) + isFile = snItem.itemType == nwItemType.FILE isRoot = snItem.itemType == nwItemType.ROOT onFile = dnItem.itemType == nwItemType.FILE + inSame = snItem.itemRoot == dnItem.itemRoot - isSame = snItem.itemClass == dnItem.itemClass - isNone = snItem.itemClass == nwItemClass.NO_CLASS - isNote = snItem.itemLayout == nwItemLayout.NOTE - onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile - - allowDrop = isSame or isNone or isNote or onFree + allowDrop = inSame or isFile allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile) if allowDrop and not isRoot: logger.debug("Drag'n'drop of item '%s' accepted", sHandle) + wCount = int(sItem.data(self.C_COUNT, Qt.UserRole)) self.propagateCount(sHandle, 0) QTreeWidget.dropEvent(self, theEvent) self._postItemMove(sHandle, wCount) diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py index b86fe968..8f65bb05 100644 --- a/tests/test_core/test_core_item.py +++ b/tests/test_core/test_core_item.py @@ -220,6 +220,7 @@ def testCoreItem_Methods(mockGUI): # Status + Icon # ============= + theItem.setType("FILE") theItem.setStatus("Note") theItem.setImport("Minor") @@ -229,11 +230,19 @@ def testCoreItem_Methods(mockGUI): assert stT == "Note" assert isinstance(stI, QIcon) + theItem.setImportStatus("Draft") + stT, stI = theItem.getImportStatus() + assert stT == "Draft" + theItem.setClass("CHARACTER") stT, stI = theItem.getImportStatus() assert stT == "Minor" assert isinstance(stI, QIcon) + theItem.setImportStatus("Major") + stT, stI = theItem.getImportStatus() + assert stT == "Major" + # Representation # ============== @@ -275,6 +284,8 @@ def testCoreItem_TypeSetter(mockGUI): assert theItem.itemType == nwItemType.FILE theItem.setType("TRASH") assert theItem.itemType == nwItemType.TRASH + + # Alternative theItem.setType(nwItemType.ROOT) assert theItem.itemType == nwItemType.ROOT @@ -294,28 +305,74 @@ def testCoreItem_ClassSetter(mockGUI): assert theItem.itemClass == nwItemClass.NO_CLASS theItem.setClass("NONSENSE") assert theItem.itemClass == nwItemClass.NO_CLASS + theItem.setClass("NO_CLASS") assert theItem.itemClass == nwItemClass.NO_CLASS + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is True + theItem.setClass("NOVEL") assert theItem.itemClass == nwItemClass.NOVEL + assert theItem.isNovelLike() is True + assert theItem.documentAllowed() is True + assert theItem.isInactive() is False + theItem.setClass("PLOT") assert theItem.itemClass == nwItemClass.PLOT + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("CHARACTER") assert theItem.itemClass == nwItemClass.CHARACTER + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("WORLD") assert theItem.itemClass == nwItemClass.WORLD + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("TIMELINE") assert theItem.itemClass == nwItemClass.TIMELINE + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("OBJECT") assert theItem.itemClass == nwItemClass.OBJECT + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("ENTITY") assert theItem.itemClass == nwItemClass.ENTITY + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("CUSTOM") assert theItem.itemClass == nwItemClass.CUSTOM + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is False + assert theItem.isInactive() is False + theItem.setClass("ARCHIVE") assert theItem.itemClass == nwItemClass.ARCHIVE + assert theItem.isNovelLike() is True + assert theItem.documentAllowed() is True + assert theItem.isInactive() is True + theItem.setClass("TRASH") assert theItem.itemClass == nwItemClass.TRASH + assert theItem.isNovelLike() is False + assert theItem.documentAllowed() is True + assert theItem.isInactive() is True + + # Alternative theItem.setClass(nwItemClass.NOVEL) assert theItem.itemClass == nwItemClass.NOVEL @@ -344,13 +401,69 @@ def testCoreItem_LayoutSetter(mockGUI): theItem.setLayout("NOTE") assert theItem.itemLayout == nwItemLayout.NOTE - # Alternatives + # Alternative theItem.setLayout(nwItemLayout.NOTE) assert theItem.itemLayout == nwItemLayout.NOTE # END Test testCoreItem_LayoutSetter +@pytest.mark.core +def testCoreItem_ClassDefaults(mockGUI): + """Test the setter for the default values. + """ + theProject = NWProject(mockGUI) + theItem = NWItem(theProject) + + # Root items should not have their class updated + theItem.setParent(None) + theItem.setClass(nwItemClass.NO_CLASS) + assert theItem.itemClass == nwItemClass.NO_CLASS + + theItem.setClassDefaults(nwItemClass.NOVEL) + assert theItem.itemClass == nwItemClass.NO_CLASS + + # Non-root items should have their class updated + theItem.setParent("0123456789abc") + theItem.setClass(nwItemClass.NO_CLASS) + assert theItem.itemClass == nwItemClass.NO_CLASS + + theItem.setClassDefaults(nwItemClass.NOVEL) + assert theItem.itemClass == nwItemClass.NOVEL + + # Non-layout items should have their layout set based on class + theItem.setParent("0123456789abc") + theItem.setClass(nwItemClass.NO_CLASS) + theItem.setLayout(nwItemLayout.NO_LAYOUT) + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + + theItem.setClassDefaults(nwItemClass.NOVEL) + assert theItem.itemLayout == nwItemLayout.DOCUMENT + + theItem.setParent("0123456789abc") + theItem.setClass(nwItemClass.NO_CLASS) + theItem.setLayout(nwItemLayout.NO_LAYOUT) + assert theItem.itemLayout == nwItemLayout.NO_LAYOUT + + theItem.setClassDefaults(nwItemClass.PLOT) + assert theItem.itemLayout == nwItemLayout.NOTE + + # If documents are not allowed in that class, the layout should be changed + theItem.setParent("0123456789abc") + theItem.setClass(nwItemClass.NO_CLASS) + theItem.setLayout(nwItemLayout.DOCUMENT) + assert theItem.itemLayout == nwItemLayout.DOCUMENT + + theItem.setClassDefaults(nwItemClass.PLOT) + assert theItem.itemLayout == nwItemLayout.NOTE + + # In all cases, status and importance should no longer be None + assert theItem.itemStatus is not None + assert theItem.itemImport is not None + +# END Test testCoreItem_ClassDefaults + + @pytest.mark.core def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData): """Test packing and unpacking XML objects for the NWItem class. From 4eec2926cb6d8acc49657ab436b6e6430c77a2ad Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 17 Apr 2022 18:38:25 +0200 Subject: [PATCH 12/13] Clean up unused code and add more comments --- novelwriter/constants.py | 9 --------- novelwriter/gui/projtree.py | 7 ++++++- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index dbdc3664..30071606 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -48,15 +48,6 @@ class nwConst(): # END Class nwConst -class nwLists(): - """Lists used for grouping various other constants. - """ - # Item classes which do not require items to have same class - FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH} - -# END Class nwLists - - class nwRegEx(): FMT_EI = r"(? Date: Sun, 17 Apr 2022 19:15:00 +0200 Subject: [PATCH 13/13] Make better use of the new NWItem functions and do some cleanup --- novelwriter/core/index.py | 9 ++-- novelwriter/core/item.py | 71 +++++++++++++++---------------- novelwriter/core/tree.py | 15 ------- novelwriter/gui/projtree.py | 3 +- novelwriter/tools/build.py | 5 +-- sample/nwProject.nwx | 60 +++++++++++++------------- tests/test_core/test_core_tree.py | 7 +-- 7 files changed, 71 insertions(+), 99 deletions(-) diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py index 45ae9acc..9b8dd47c 100644 --- a/novelwriter/core/index.py +++ b/novelwriter/core/index.py @@ -30,7 +30,7 @@ import logging from time import time -from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout +from novelwriter.enum import nwItemType, nwItemLayout from novelwriter.error import logException from novelwriter.constants import nwFiles, nwKeyWords, nwUnicode from novelwriter.core.document import NWDoc @@ -227,11 +227,8 @@ class NWIndex(): if theItem.itemParent is None: logger.info("Not indexing orphaned item '%s'", tHandle) return False - if theItem.itemClass == nwItemClass.TRASH: - logger.debug("Not indexing trash item '%s'", tHandle) - return False - if theItem.itemClass == nwItemClass.ARCHIVE: - logger.debug("Not indexing archived item '%s'", tHandle) + if theItem.isInactive(): + logger.debug("Not indexing inactive item '%s'", tHandle) return False itemClass = theItem.itemClass diff --git a/novelwriter/core/item.py b/novelwriter/core/item.py index 0178c512..f6d93b36 100644 --- a/novelwriter/core/item.py +++ b/novelwriter/core/item.py @@ -35,9 +35,6 @@ from novelwriter.constants import nwLabels, trConst logger = logging.getLogger(__name__) -# Deprecated layout labels -DEP_LAYOUTS = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE") - class NWItem(): @@ -365,33 +362,33 @@ class NWItem(): self._name = "" return - def setHandle(self, tHandle): + def setHandle(self, handle): """Set the item handle, and ensure it is valid. """ - if isHandle(tHandle): - self._handle = tHandle + if isHandle(handle): + self._handle = handle else: self._handle = None return - def setParent(self, pHandle): + def setParent(self, handle): """Set the parent handle, and ensure it is valid. """ - if pHandle is None: + if handle is None: self._parent = None - elif isHandle(pHandle): - self._parent = pHandle + elif isHandle(handle): + self._parent = handle else: self._parent = None return - def setRoot(self, rHandle): + def setRoot(self, handle): """Set the root handle, and ensure it is valid. """ - if rHandle is None: + if handle is None: self._root = None - elif isHandle(rHandle): - self._root = rHandle + elif isHandle(handle): + self._root = handle else: self._root = None return @@ -404,59 +401,59 @@ class NWItem(): self._order = checkInt(order, 0) return - def setType(self, itemType): + def setType(self, value): """Set the item type from either a proper nwItemType, or set it from a string representing an nwItemType. """ - if isinstance(itemType, nwItemType): - self._type = itemType - elif isItemType(itemType): - self._type = nwItemType[itemType] + if isinstance(value, nwItemType): + self._type = value + elif isItemType(value): + self._type = nwItemType[value] else: - logger.error("Unrecognised item type '%s'", itemType) + logger.error("Unrecognised item type '%s'", value) self._type = nwItemType.NO_TYPE return - def setClass(self, itemClass): + def setClass(self, value): """Set the item class from either a proper nwItemClass, or set it from a string representing an nwItemClass. """ - if isinstance(itemClass, nwItemClass): - self._class = itemClass - elif isItemClass(itemClass): - self._class = nwItemClass[itemClass] + if isinstance(value, nwItemClass): + self._class = value + elif isItemClass(value): + self._class = nwItemClass[value] else: - logger.error("Unrecognised item class '%s'", itemClass) + logger.error("Unrecognised item class '%s'", value) self._class = nwItemClass.NO_CLASS return - def setLayout(self, itemLayout): + def setLayout(self, value): """Set the item layout from either a proper nwItemLayout, or set it from a string representing an nwItemLayout. """ - if isinstance(itemLayout, nwItemLayout): - self._layout = itemLayout - elif isItemLayout(itemLayout): - self._layout = nwItemLayout[itemLayout] - elif itemLayout in DEP_LAYOUTS: + if isinstance(value, nwItemLayout): + self._layout = value + elif isItemLayout(value): + self._layout = nwItemLayout[value] + elif value in ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE"): self._layout = nwItemLayout.DOCUMENT else: - logger.error("Unrecognised item layout '%s'", itemLayout) + logger.error("Unrecognised item layout '%s'", value) self._layout = nwItemLayout.NO_LAYOUT return - def setStatus(self, itemStatus): + def setStatus(self, value): """Set the item status by looking it up in the valid status items of the current project. """ - self._status = self.theProject.statusItems.check(itemStatus) + self._status = self.theProject.statusItems.check(value) return - def setImport(self, itemImport): + def setImport(self, value): """Set the item importance by looking it up in the valid import items of the current project. """ - self._import = self.theProject.importItems.check(itemImport) + self._import = self.theProject.importItems.check(value) return def setExpanded(self, state): diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index 2983f88a..29532c7d 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -292,13 +292,6 @@ class NWTree(): return True return False - def isTrashRoot(self, tHandle): - """Check if a handle is the trash folder. - """ - if self._trashRoot is None: - return False - return tHandle == self._trashRoot - def trashRoot(self): """Returns the handle of the trash folder, or None if there isn't one. @@ -307,14 +300,6 @@ class NWTree(): return self._trashRoot return None - def archiveRoot(self): - """Returns the handle of the archive folder, or None if there - isn't one. - """ - if self._archRoot: - return self._archRoot - return None - def findRoot(self, theClass): """Find the first root item for a given class. """ diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index cce1c91b..23238b84 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -444,8 +444,7 @@ class GuiProjectTree(QTreeWidget): logger.error("Could not delete item") return False - pHandle = nwItemS.itemParent - if self.theProject.projTree.isTrashRoot(pHandle): + if self.theProject.projTree.isTrash(tHandle): # If the file is in the trash folder already, as the # user if they want to permanently delete the file. doPermanent = False diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py index feb1662d..d84a0d4c 100644 --- a/novelwriter/tools/build.py +++ b/novelwriter/tools/build.py @@ -785,10 +785,7 @@ class GuiBuildNovel(QDialog): isNone = theItem.itemType != nwItemType.FILE isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT - isNone |= theItem.itemClass == nwItemClass.NO_CLASS - isNone |= theItem.itemClass == nwItemClass.ARCHIVE - isNone |= theItem.itemClass == nwItemClass.TRASH - isNone |= theItem.itemParent == self.theProject.projTree.trashRoot() + isNone |= theItem.isInactive() isNone |= theItem.itemParent is None isNote = theItem.itemLayout == nwItemLayout.NOTE isNovel = not isNone and not isNote diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx index 002cea43..23660870 100644 --- a/sample/nwProject.nwx +++ b/sample/nwProject.nwx @@ -1,13 +1,13 @@ - + Sample Project Sample Project Jane Smith Jay Doh - 1303 + 1306 199 - 65049 + 65149 False @@ -33,7 +33,7 @@
- New + New Notes Started 1st Draft @@ -42,110 +42,110 @@ Finished - None + None Minor Major Main
- + Novel - + Title Page - + Page - + Part One - + A Folder - + Chapter One - + Making a Scene - + Another Scene - + Interlude - + A Note on Structure - + Chapter Two - + We Found John! - + Characters - + Main Characters - + John Smith - + Jane Smith - + Locations - + Earth - + Space - + Mars - + Archive - + Scenes - + Old File - + Trash - + Delete Me! diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py index 3e8aeeca..3b35abd5 100644 --- a/tests/test_core/test_core_tree.py +++ b/tests/test_core/test_core_tree.py @@ -126,8 +126,6 @@ def testCoreTree_BuildTree(mockGUI, mockItems): # Check for archive and trash folders assert theTree.trashRoot() is None - assert theTree.archiveRoot() is None - assert theTree.isTrashRoot("a000000000003") is False aHandles = [] for tHandle, pHandle, nwItem in mockItems: @@ -152,8 +150,8 @@ def testCoreTree_BuildTree(mockGUI, mockItems): # Check that we have the correct archive and trash folders assert theTree.trashRoot() == "a000000000003" - assert theTree.archiveRoot() == "a000000000002" - assert theTree.isTrashRoot("a000000000003") is True + assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002" + assert theTree.isTrash("a000000000003") is True assert theTree.isRoot("a000000000002") is True # Check the isTrash function @@ -221,7 +219,6 @@ def testCoreTree_BuildTree(mockGUI, mockItems): del theTree["a000000000002"] assert len(theTree) == len(mockItems) - 2 assert "a000000000002" not in theTree - assert theTree.archiveRoot() is None del theTree["a000000000003"] assert len(theTree) == len(mockItems) - 3