Remove max folder depth restriction, and simplify adding folders and files in the tree
This commit is contained in:
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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
@@ -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"/>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user