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