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
# 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
+3 -1
View File
@@ -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
+27 -5
View File
@@ -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.
"""
-11
View File
@@ -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}<br><br>{1}".format(
+2 -2
View File
@@ -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
+37 -90
View File
@@ -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()
@@ -1,4 +1,4 @@
%%~name: New File
%%~name: New Note
%%~path: 44cb730c42048/031b4af5197ec
%%~kind: PLOT/NOTE
# Main Plot
@@ -1,4 +1,4 @@
%%~name: New File
%%~name: New Note
%%~path: 71ee45a3c0db9/1a6562590ef19
%%~kind: CHARACTER/NOTE
# Jane Doe
@@ -1,4 +1,4 @@
%%~name: New File
%%~name: New Note
%%~path: 811786ad1ae74/41cfc0d1f2d12
%%~kind: WORLD/NOTE
# Main Location
@@ -1,5 +1,5 @@
<?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>
<name>New Project</name>
<title></title>
@@ -66,7 +66,7 @@
</item>
<item handle="031b4af5197ec" parent="44cb730c42048" root="44cb730c42048" order="0" type="FILE" class="PLOT" layout="NOTE">
<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 handle="71ee45a3c0db9" parent="None" root="71ee45a3c0db9" order="2" type="ROOT" class="CHARACTER">
<meta expanded="True"/>
@@ -74,7 +74,7 @@
</item>
<item handle="1a6562590ef19" parent="71ee45a3c0db9" root="71ee45a3c0db9" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<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 handle="811786ad1ae74" parent="None" root="811786ad1ae74" order="3" type="ROOT" class="WORLD">
<meta expanded="True"/>
@@ -82,7 +82,7 @@
</item>
<item handle="41cfc0d1f2d12" parent="811786ad1ae74" root="811786ad1ae74" order="0" type="FILE" class="WORLD" layout="NOTE">
<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 handle="2fca346db6561" parent="None" root="2fca346db6561" order="4" type="TRASH" class="TRASH">
<meta expanded="True"/>
+6 -4
View File
@@ -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
+3 -3
View File
@@ -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
+1 -7
View File
@@ -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
+1 -1
View File
@@ -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