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] 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