Remove max folder depth restriction, and simplify adding folders and files in the tree

This commit is contained in:
Veronica Berglyd Olsen
2022-04-17 14:44:11 +02:00
parent ebee9ff791
commit 4cfbbf4c68
14 changed files with 87 additions and 132 deletions
-1
View File
@@ -42,7 +42,6 @@ class nwConst():
FMT_DSTAMP = "%Y-%m-%d" # Date only format FMT_DSTAMP = "%Y-%m-%d" # Date only format
# Various Hard Limits # Various Hard Limits
MAX_DEPTH = 30 # Maximum folder depth of a project
MAX_DOCSIZE = 5000000 # Maxium size of a single document MAX_DOCSIZE = 5000000 # Maxium size of a single document
MAX_BUILDSIZE = 10000000 # Maxium size of a project build MAX_BUILDSIZE = 10000000 # Maxium size of a project build
+3 -1
View File
@@ -308,7 +308,9 @@ class NWItem():
"""Set the default values based on the item's class and the """Set the default values based on the item's class and the
project settings. 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: if self._class in nwLists.CLS_NOVEL:
self._layout = nwItemLayout.DOCUMENT self._layout = nwItemLayout.DOCUMENT
+27 -5
View File
@@ -33,7 +33,7 @@ from hashlib import sha256
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkHandle from novelwriter.common import checkHandle
from novelwriter.constants import nwConst, nwFiles from novelwriter.constants import nwFiles
from novelwriter.core.item import NWItem from novelwriter.core.item import NWItem
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -41,6 +41,8 @@ logger = logging.getLogger(__name__)
class NWTree(): class NWTree():
MAX_DEPTH = 1000 # Cap of tree traversing for loops
def __init__(self, theProject): def __init__(self, theProject):
self.theProject = theProject self.theProject = theProject
@@ -219,7 +221,7 @@ class NWTree():
return False return False
iItem = tItem iItem = tItem
for _ in range(nwConst.MAX_DEPTH + 1): for _ in range(self.MAX_DEPTH):
if iItem.itemParent is None: if iItem.itemParent is None:
tItem.setRoot(iItem.itemHandle) tItem.setRoot(iItem.itemHandle)
tItem.setClassDefaults(iItem.itemClass) tItem.setClassDefaults(iItem.itemClass)
@@ -228,8 +230,8 @@ class NWTree():
iItem = self.__getitem__(iItem.itemParent) iItem = self.__getitem__(iItem.itemParent)
if iItem is None: if iItem is None:
return False return False
else:
return False raise RecursionError("Critical internal error")
def checkType(self, tHandle, itemType): def checkType(self, tHandle, itemType):
"""Return true of item exists and is of the specified item type. """Return true of item exists and is of the specified item type.
@@ -249,7 +251,7 @@ class NWTree():
tItem = self.__getitem__(tHandle) tItem = self.__getitem__(tHandle)
if tItem is not None: if tItem is not None:
tTree.append(tHandle) tTree.append(tHandle)
for _ in range(nwConst.MAX_DEPTH + 1): for _ in range(self.MAX_DEPTH):
if tItem.itemParent is None: if tItem.itemParent is None:
return tTree return tTree
else: else:
@@ -259,6 +261,9 @@ class NWTree():
return tTree return tTree
else: else:
tTree.append(tHandle) tTree.append(tHandle)
else:
raise RecursionError("Critical internal error")
return tTree return tTree
## ##
@@ -270,6 +275,23 @@ class NWTree():
""" """
return tHandle in self._treeRoots 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): def isTrashRoot(self, tHandle):
"""Check if a handle is the trash folder. """Check if a handle is the trash folder.
""" """
-11
View File
@@ -34,7 +34,6 @@ from PyQt5.QtWidgets import (
from novelwriter.core import NWDoc from novelwriter.core import NWDoc
from novelwriter.enum import nwAlert, nwItemType from novelwriter.enum import nwAlert, nwItemType
from novelwriter.constants import nwConst
from novelwriter.gui.custom import QHelpLabel from novelwriter.gui.custom import QHelpLabel
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@@ -160,16 +159,6 @@ class GuiDocSplit(QDialog):
), nwAlert.ERROR) ), nwAlert.ERROR)
return False 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( msgYes = self.theParent.askQuestion(
self.tr("Split Document"), self.tr("Split Document"),
"{0}<br><br>{1}".format( "{0}<br><br>{1}".format(
+2 -2
View File
@@ -197,7 +197,7 @@ class GuiMainMenu(QMenuBar):
# Project > New Folder # Project > New Folder
self.aCreateFolder = QAction(self.tr("Create Folder"), self) self.aCreateFolder = QAction(self.tr("Create Folder"), self)
self.aCreateFolder.setShortcut("Ctrl+Shift+N") 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) self.projMenu.addAction(self.aCreateFolder)
# Project > Separator # Project > Separator
@@ -259,7 +259,7 @@ class GuiMainMenu(QMenuBar):
# Document > New # Document > New
self.aNewDoc = QAction(self.tr("New Document"), self) self.aNewDoc = QAction(self.tr("New Document"), self)
self.aNewDoc.setShortcut("Ctrl+N") 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) self.docuMenu.addAction(self.aNewDoc)
# Document > Open # Document > Open
+37 -90
View File
@@ -37,7 +37,7 @@ from PyQt5.QtWidgets import (
from novelwriter.core import NWDoc from novelwriter.core import NWDoc
from novelwriter.enum import nwItemType, nwItemClass, nwItemLayout, nwAlert 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__) logger = logging.getLogger(__name__)
@@ -161,114 +161,64 @@ class GuiProjectTree(QTreeWidget):
self._timeChanged = 0 self._timeChanged = 0
return return
def newTreeItem(self, itemType, itemClass): def newTreeItem(self, itemType, itemClass=None):
"""Add new item to the tree, with a given itemType and """Add new item to the tree, with a given itemType (and
itemClass, and attach it to the selected handle. Also make sure itemClass if Root), and attach it to the selected handle. Also make
the item is added in a place it can be added, and that other 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. meta data is set correctly to ensure a valid project tree.
""" """
pHandle = self.getSelectedHandle()
nHandle = None
if not self.theParent.hasProject: if not self.theParent.hasProject:
logger.error("No project open") logger.error("No project open")
return False return False
if not isinstance(itemType, nwItemType): nHandle = None
# This would indicate an internal bug tHandle = None
logger.error("No itemType provided")
return False
# The item needs to be assigned an item class, so one must be if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
# 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 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( tHandle = self.theProject.newRoot(
trConst(nwLabels.CLASS_NAME[itemClass]), itemClass trConst(nwLabels.CLASS_NAME[itemClass]), itemClass
) )
if tHandle is None:
logger.error("No root item added")
return False
else: elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
# 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)
# If still nothing, give up sHandle = self.getSelectedHandle()
if pHandle is None: if sHandle is None or sHandle not in self.theProject.projTree:
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
"Did not find anywhere to add the file or folder!" "Did not find anywhere to add the file or folder!"
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
# Now check if the selected item is a file, in which case # If the selected item is a file, the new item will be a sibling
# the new file will be a sibling pItem = self.theProject.projTree[sHandle]
pItem = self.theProject.projTree[pHandle]
if pItem.itemType == nwItemType.FILE: if pItem.itemType == nwItemType.FILE:
nHandle = pHandle nHandle = sHandle
pHandle = pItem.itemParent sHandle = pItem.itemParent
if sHandle is None:
logger.error("Internal error") # Bug
return False
# If we again have no home, give up if self.theProject.projTree.isTrash(sHandle):
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):
self.theParent.makeAlert(self.tr( self.theParent.makeAlert(self.tr(
"Cannot add new files or folders to the Trash folder." "Cannot add new files or folders to the Trash folder."
), nwAlert.ERROR) ), nwAlert.ERROR)
return False return False
parTree = self.theProject.projTree.getItemPath(pHandle) # Add the file or folder
# If we're still here, add the file or folder
if itemType == nwItemType.FILE: 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: elif itemType == nwItemType.FOLDER:
if len(parTree) >= nwConst.MAX_DEPTH - 1: tHandle = self.theProject.newFolder(self.tr("New Folder"), sHandle)
# 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)
else: else:
logger.error("Failed to add new item") logger.error("Failed to add new item")
return False return False
# If there is no handle set, return here # If there is no handle set, return here. This is a bug
if tHandle is None: if tHandle is None: # pragma: no cover
return True return True
# Add the new item to the tree # 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 # This is a new file, so let's add some content
newDoc = NWDoc(self.theProject, tHandle) newDoc = NWDoc(self.theProject, tHandle)
curTxt = newDoc.readDocument() if not newDoc.readDocument():
if curTxt is None:
curTxt = ""
if curTxt == "":
if nwItem.itemLayout == nwItemLayout.DOCUMENT: if nwItem.itemLayout == nwItemLayout.DOCUMENT:
newText = f"### {nwItem.itemName}\n\n" newText = f"### {nwItem.itemName}\n\n"
else: else:
@@ -633,7 +579,7 @@ class GuiProjectTree(QTreeWidget):
return return
def propagateCount(self, tHandle, theCount, nDepth=0): def propagateCount(self, tHandle, theCount):
"""Recursive function setting the word count for a given item, """Recursive function setting the word count for a given item,
and propagating that count upwards in the tree until reaching a and propagating that count upwards in the tree until reaching a
root item. This function is more efficient than recalculating root item. This function is more efficient than recalculating
@@ -653,12 +599,13 @@ class GuiProjectTree(QTreeWidget):
return return
pCount = 0 pCount = 0
pHandle = None
for i in range(pItem.childCount()): for i in range(pItem.childCount()):
pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole)) pCount += int(pItem.child(i).data(self.C_COUNT, Qt.UserRole))
pHandle = pItem.data(self.C_NAME, Qt.UserRole) pHandle = pItem.data(self.C_NAME, Qt.UserRole)
if not nDepth > nwConst.MAX_DEPTH + 1 and pHandle != "": if pHandle:
self.propagateCount(pHandle, pCount, nDepth+1) self.propagateCount(pHandle, pCount)
return return
@@ -1180,7 +1127,7 @@ class GuiProjectTreeMenu(QMenu):
"""Forward the new file call to the project tree. """Forward the new file call to the project tree.
""" """
if self.theItem is not None: if self.theItem is not None:
self.theTree.newTreeItem(nwItemType.FILE, None) self.theTree.newTreeItem(nwItemType.FILE)
return return
@pyqtSlot() @pyqtSlot()
@@ -1188,7 +1135,7 @@ class GuiProjectTreeMenu(QMenu):
"""Forward the new folder call to the project tree. """Forward the new folder call to the project tree.
""" """
if self.theItem is not None: if self.theItem is not None:
self.theTree.newTreeItem(nwItemType.FOLDER, None) self.theTree.newTreeItem(nwItemType.FOLDER)
return return
@pyqtSlot() @pyqtSlot()
@@ -1,4 +1,4 @@
%%~name: New File %%~name: New Note
%%~path: 44cb730c42048/031b4af5197ec %%~path: 44cb730c42048/031b4af5197ec
%%~kind: PLOT/NOTE %%~kind: PLOT/NOTE
# Main Plot # Main Plot
@@ -1,4 +1,4 @@
%%~name: New File %%~name: New Note
%%~path: 71ee45a3c0db9/1a6562590ef19 %%~path: 71ee45a3c0db9/1a6562590ef19
%%~kind: CHARACTER/NOTE %%~kind: CHARACTER/NOTE
# Jane Doe # Jane Doe
@@ -1,4 +1,4 @@
%%~name: New File %%~name: New Note
%%~path: 811786ad1ae74/41cfc0d1f2d12 %%~path: 811786ad1ae74/41cfc0d1f2d12
%%~kind: WORLD/NOTE %%~kind: WORLD/NOTE
# Main Location # Main Location
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-16 20:52:26"> <novelWriterXML appVersion="1.7-alpha0" hexVersion="0x010700a0" fileVersion="1.4" timeStamp="2022-04-17 14:18:28">
<project> <project>
<name>New Project</name> <name>New Project</name>
<title></title> <title></title>
@@ -66,7 +66,7 @@
</item> </item>
<item handle="031b4af5197ec" parent="44cb730c42048" root="44cb730c42048" order="0" type="FILE" class="PLOT" layout="NOTE"> <item handle="031b4af5197ec" parent="44cb730c42048" root="44cb730c42048" order="0" type="FILE" class="PLOT" layout="NOTE">
<meta charCount="48" wordCount="10" paraCount="1" cursorPos="69"/> <meta charCount="48" wordCount="10" paraCount="1" cursorPos="69"/>
<name status="sa3b179" import="i466852" exported="True">New File</name> <name status="sa3b179" import="i466852" exported="True">New Note</name>
</item> </item>
<item handle="71ee45a3c0db9" parent="None" root="71ee45a3c0db9" order="2" type="ROOT" class="CHARACTER"> <item handle="71ee45a3c0db9" parent="None" root="71ee45a3c0db9" order="2" type="ROOT" class="CHARACTER">
<meta expanded="True"/> <meta expanded="True"/>
@@ -74,7 +74,7 @@
</item> </item>
<item handle="1a6562590ef19" parent="71ee45a3c0db9" root="71ee45a3c0db9" order="0" type="FILE" class="CHARACTER" layout="NOTE"> <item handle="1a6562590ef19" parent="71ee45a3c0db9" root="71ee45a3c0db9" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta charCount="34" wordCount="8" paraCount="1" cursorPos="51"/> <meta charCount="34" wordCount="8" paraCount="1" cursorPos="51"/>
<name status="sa3b179" import="i466852" exported="True">New File</name> <name status="sa3b179" import="i466852" exported="True">New Note</name>
</item> </item>
<item handle="811786ad1ae74" parent="None" root="811786ad1ae74" order="3" type="ROOT" class="WORLD"> <item handle="811786ad1ae74" parent="None" root="811786ad1ae74" order="3" type="ROOT" class="WORLD">
<meta expanded="True"/> <meta expanded="True"/>
@@ -82,7 +82,7 @@
</item> </item>
<item handle="41cfc0d1f2d12" parent="811786ad1ae74" root="811786ad1ae74" order="0" type="FILE" class="WORLD" layout="NOTE"> <item handle="41cfc0d1f2d12" parent="811786ad1ae74" root="811786ad1ae74" order="0" type="FILE" class="WORLD" layout="NOTE">
<meta charCount="51" wordCount="9" paraCount="1" cursorPos="68"/> <meta charCount="51" wordCount="9" paraCount="1" cursorPos="68"/>
<name status="sa3b179" import="i466852" exported="True">New File</name> <name status="sa3b179" import="i466852" exported="True">New Note</name>
</item> </item>
<item handle="2fca346db6561" parent="None" root="2fca346db6561" order="4" type="TRASH" class="TRASH"> <item handle="2fca346db6561" parent="None" root="2fca346db6561" order="4" type="TRASH" class="TRASH">
<meta expanded="True"/> <meta expanded="True"/>
+6 -4
View File
@@ -210,7 +210,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
@pytest.mark.core @pytest.mark.core
def testCoreTree_Methods(monkeypatch, mockGUI, mockItems): def testCoreTree_Methods(mockGUI, mockItems):
"""Test various class methods. """Test various class methods.
""" """
theProject = NWProject(mockGUI) theProject = NWProject(mockGUI)
@@ -235,9 +235,11 @@ def testCoreTree_Methods(monkeypatch, mockGUI, mockItems):
assert theTree.updateItemData("b000000000001") is True assert theTree.updateItemData("b000000000001") is True
# Update item data, root is unreachable # Update item data, root is unreachable
with monkeypatch.context() as mp: maxDepth = theTree.MAX_DEPTH
mp.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 0) theTree.MAX_DEPTH = 0
assert theTree.updateItemData("b000000000001") is False with pytest.raises(RecursionError):
theTree.updateItemData("b000000000001")
theTree.MAX_DEPTH = maxDepth
# Chech type # Chech type
assert theTree.checkType("blabla", nwItemType.FILE) is False assert theTree.checkType("blabla", nwItemType.FILE) is False
+3 -3
View File
@@ -58,9 +58,9 @@ def testDlgMerge_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwGUI.switchFocus(nwWidget.TREE) nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection() nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True) nwGUI.treeView._getTreeItem(hChapterDir).setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None) nwGUI.treeView.newTreeItem(nwItemType.FILE)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None) nwGUI.treeView.newTreeItem(nwItemType.FILE)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None) nwGUI.treeView.newTreeItem(nwItemType.FILE)
assert nwGUI.saveProject() is True assert nwGUI.saveProject() is True
assert nwGUI.closeProject() is True assert nwGUI.closeProject() is True
+1 -7
View File
@@ -62,7 +62,7 @@ def testDlgSplit_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwGUI.switchFocus(nwWidget.TREE) nwGUI.switchFocus(nwWidget.TREE)
nwGUI.treeView.clearSelection() nwGUI.treeView.clearSelection()
nwGUI.treeView._getTreeItem(hNovelRoot).setSelected(True) nwGUI.treeView._getTreeItem(hNovelRoot).setSelected(True)
nwGUI.treeView.newTreeItem(nwItemType.FILE, None) nwGUI.treeView.newTreeItem(nwItemType.FILE)
assert nwGUI.saveProject() is True assert nwGUI.saveProject() is True
assert nwGUI.closeProject() 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) mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
assert nwSplit._doSplit() is False 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 # Clear the list
nwSplit.listBox.clear() nwSplit.listBox.clear()
assert nwSplit._doSplit() is False assert nwSplit._doSplit() is False
+1 -1
View File
@@ -175,7 +175,7 @@ def testDlgItemEditor_Note(qtbot, monkeypatch, nwGUI, fncProj, constData):
itemEdit.show() itemEdit.show()
# Check Existing Settings # Check Existing Settings
assert itemEdit.editName.text() == "New File" assert itemEdit.editName.text() == "New Note"
assert itemEdit.editStatus.currentData() == constData.importKeys[0] assert itemEdit.editStatus.currentData() == constData.importKeys[0]
assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE assert itemEdit.editLayout.currentData() == nwItemLayout.NOTE
assert itemEdit.editExport.isChecked() is True assert itemEdit.editExport.isChecked() is True