diff --git a/novelwriter/constants.py b/novelwriter/constants.py
index f71453f3..30071606 100644
--- a/novelwriter/constants.py
+++ b/novelwriter/constants.py
@@ -25,7 +25,7 @@ along with this program. If not, see .
from PyQt5.QtCore import QCoreApplication, QT_TRANSLATE_NOOP
-from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwOutline
+from novelwriter.enum import nwItemClass, nwItemLayout, nwOutline
def trConst(tString):
@@ -42,31 +42,12 @@ 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
# END Class nwConst
-class nwLists():
- """Lists used for grouping various other constants.
- """
- # Regular user-accessible item types
- REG_TYPES = {nwItemType.ROOT, nwItemType.FOLDER, nwItemType.FILE}
-
- # Item classes where the full list of novel layouts are allowed
- CLS_NOVEL = {nwItemClass.NOVEL, nwItemClass.ARCHIVE}
-
- # Item classes which do not require items to have same class
- FREE_CLASS = {nwItemClass.ARCHIVE, nwItemClass.TRASH}
-
- # Deprecated nwItemLayout entries
- DEP_LAYOUT = ("TITLE", "PAGE", "BOOK", "PARTITION", "UNNUMBERED", "CHAPTER", "SCENE")
-
-# END Class nwLists
-
-
class nwRegEx():
FMT_EI = r"(? 0:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{ch+1:d}.{sc+1:d}")
- sHandle = self.newFile(scTitle, nwItemClass.NOVEL, pHandle)
+ sHandle = self.newFile(scTitle, pHandle)
aDoc = NWDoc(self, sHandle)
aDoc.writeDocument("### %s\n\n" % scTitle)
@@ -351,7 +338,7 @@ class NWProject():
elif numScenes > 0:
for sc in range(numScenes):
scTitle = self.tr("Scene {0}").format(f"{sc+1:d}")
- sHandle = self.newFile(scTitle, nwItemClass.NOVEL, nHandle)
+ sHandle = self.newFile(scTitle, nHandle)
aDoc = NWDoc(self, sHandle)
aDoc.writeDocument("### %s\n\n" % scTitle)
@@ -481,8 +468,9 @@ class NWProject():
# documents and one for project notes. Introduced in
# version 1.5.
# 1.4 : Introduces a more compact format for storing items. All
- # settings aside from name are now attributes. Introduced
- # in version 1.7.
+ # settings aside from name are now attributes. This format
+ # also changes the way satus and importance labels are
+ # stored and handled. Introduced in version 1.7.
if fileVersion not in ("1.0", "1.1", "1.2", "1.3", "1.4"):
self.theParent.makeAlert(self.tr(
@@ -613,7 +601,13 @@ class NWProject():
self.mainConf.updateRecentCache(self.projPath, self.projName, self.lastWCount, time())
self.mainConf.saveRecentCache()
- self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName))
+ # Check the project tree consistency
+ for tItem in self.projTree:
+ tHandle = tItem.itemHandle
+ logger.verbose("Checking item '%s'", tHandle)
+ if not self.projTree.updateItemData(tHandle):
+ logger.error("There was a problem item '%s', and it has been removed", tHandle)
+ del self.projTree[tHandle] # The file will be re-added as orphaned
self._scanProjectFolder()
self._loadProjectLocalisation()
@@ -624,6 +618,7 @@ class NWProject():
self._writeLockFile()
self.setProjectChanged(False)
+ self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName))
return True
@@ -1202,7 +1197,7 @@ class NWProject():
self.statusItems.resetCounts()
self.importItems.resetCounts()
for nwItem in self.projTree:
- if nwItem.itemClass in nwLists.CLS_NOVEL:
+ if nwItem.isNovelLike():
self.statusItems.increment(nwItem.itemStatus)
else:
self.importItems.increment(nwItem.itemImport)
@@ -1452,6 +1447,7 @@ class NWProject():
orphItem.setClass(oClass)
orphItem.setLayout(oLayout)
self.projTree.append(oHandle, oParent, orphItem)
+ self.projTree.updateItemData(orphItem.itemHandle)
if noWhere:
self.theParent.makeAlert(self.tr(
diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py
index 7dbef4c6..29532c7d 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,13 +41,15 @@ logger = logging.getLogger(__name__)
class NWTree():
+ MAX_DEPTH = 1000 # Cap of tree traversing for loops
+
def __init__(self, theProject):
self.theProject = theProject
self._projTree = {} # Holds all the items of the project
self._treeOrder = [] # The order of the tree items on the tree view
- self._treeRoots = [] # The root items of the tree
+ self._treeRoots = {} # The root items of the tree
self._trashRoot = None # The handle of the trash root folder
self._archRoot = None # The handle of the archive root folder
self._theIndex = 0 # The current iterator index
@@ -67,7 +69,7 @@ class NWTree():
"""
self._projTree = {}
self._treeOrder = []
- self._treeRoots = []
+ self._treeRoots = {}
self._trashRoot = None
self._archRoot = None
self._theIndex = 0
@@ -98,7 +100,7 @@ class NWTree():
if nwItem.itemType == nwItemType.ROOT:
logger.verbose("Item '%s' is a root item", str(tHandle))
- self._treeRoots.append(tHandle)
+ self._treeRoots[tHandle] = nwItem
if nwItem.itemClass == nwItemClass.ARCHIVE:
logger.verbose("Item '%s' is the archive folder", str(tHandle))
self._archRoot = tHandle
@@ -207,9 +209,30 @@ class NWTree():
return novelWords, noteWords
##
- # Tree Structure Methods
+ # Tree Item Methods
##
+ def updateItemData(self, tHandle):
+ """Update the root item handle of a given item. Returns True if
+ a root was found and data updated, otherwise False.
+ """
+ tItem = self.__getitem__(tHandle)
+ if tItem is None:
+ return False
+
+ iItem = tItem
+ for _ in range(self.MAX_DEPTH):
+ if iItem.itemParent is None:
+ tItem.setRoot(iItem.itemHandle)
+ tItem.setClassDefaults(iItem.itemClass)
+ return True
+ else:
+ iItem = self.__getitem__(iItem.itemParent)
+ if iItem is None:
+ 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.
"""
@@ -218,71 +241,6 @@ class NWTree():
return False
return tItem.itemType == itemType
- def trashRoot(self):
- """Returns the handle of the trash folder, or None if there
- isn't one.
- """
- if self._trashRoot:
- return self._trashRoot
- return None
-
- def isTrashRoot(self, tHandle):
- """Check if a handle is the trash folder.
- """
- if self._trashRoot is None:
- return False
- return tHandle == self._trashRoot
-
- def archiveRoot(self):
- """Returns the handle of the archive folder, or None if there
- isn't one.
- """
- if self._archRoot:
- return self._archRoot
- return None
-
- def findRoot(self, theClass):
- """Find the root item for a given class.
- Note: This returns the first item for class CUSTOM.
- """
- for aRoot in self._treeRoots:
- tItem = self.__getitem__(aRoot)
- if tItem is None:
- continue
- if theClass == tItem.itemClass:
- return tItem.itemHandle
- return None
-
- def checkRootUnique(self, theClass):
- """Checks if there already is a root entry of class 'theClass'
- in the root of the project tree. CUSTOM class is skipped as it
- is not required to be unique.
- """
- if theClass == nwItemClass.CUSTOM:
- return True
- for aRoot in self._treeRoots:
- tItem = self.__getitem__(aRoot)
- if tItem is None:
- continue
- if theClass == tItem.itemClass:
- return False
- return True
-
- def getRootItem(self, tHandle):
- """Iterate upwards in the tree until we find the item with
- parent None, the root item. We do this with a for loop with a
- maximum depth to make infinite loops impossible.
- """
- tItem = self.__getitem__(tHandle)
- if tItem is not None:
- for i in range(nwConst.MAX_DEPTH + 1):
- if tItem.itemParent is None:
- return tItem
- else:
- tHandle = tItem.itemParent
- tItem = self.__getitem__(tHandle)
- return None
-
def getItemPath(self, tHandle):
"""Iterate upwards in the tree until we find the item with
parent None, the root item, and return the list of handles.
@@ -293,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:
@@ -303,8 +261,56 @@ class NWTree():
return tTree
else:
tTree.append(tHandle)
+ else:
+ raise RecursionError("Critical internal error")
+
return tTree
+ ##
+ # Tree Root Methods
+ ##
+
+ def isRoot(self, tHandle):
+ """Check if a handle is a root item.
+ """
+ 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 trashRoot(self):
+ """Returns the handle of the trash folder, or None if there
+ isn't one.
+ """
+ if self._trashRoot:
+ return self._trashRoot
+ return None
+
+ def findRoot(self, theClass):
+ """Find the first root item for a given class.
+ """
+ for aRoot in self._treeRoots:
+ tItem = self.__getitem__(aRoot)
+ if tItem is None:
+ continue
+ if theClass == tItem.itemClass:
+ return tItem.itemHandle
+ return None
+
##
# Setters
##
@@ -420,7 +426,7 @@ class NWTree():
return
if tHandle in self._treeRoots:
- self._treeRoots.remove(tHandle)
+ del self._treeRoots[tHandle]
if tHandle == self._trashRoot:
self._trashRoot = None
if tHandle == self._archRoot:
diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py
index f7695b9a..c082dd3b 100644
--- a/novelwriter/dialogs/docmerge.py
+++ b/novelwriter/dialogs/docmerge.py
@@ -130,7 +130,7 @@ class GuiDocMerge(QDialog):
self.theParent.makeAlert(self.tr("Internal error."), nwAlert.ERROR)
return False
- nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemClass, srcItem.itemParent)
+ nHandle = self.theProject.newFile(srcItem.itemName, srcItem.itemParent)
newItem = self.theProject.projTree[nHandle]
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py
index cdf0757a..ff2cb849 100644
--- a/novelwriter/dialogs/docsplit.py
+++ b/novelwriter/dialogs/docsplit.py
@@ -33,8 +33,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.core import NWDoc
-from novelwriter.enum import nwAlert, nwItemType, nwItemClass, nwItemLayout
-from novelwriter.constants import nwConst
+from novelwriter.enum import nwAlert, nwItemType
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(
@@ -186,22 +175,16 @@ class GuiDocSplit(QDialog):
return False
# Create the folder
- fHandle = self.theProject.newFolder(
- srcItem.itemName, srcItem.itemClass, srcItem.itemParent
- )
+ fHandle = self.theProject.newFolder(srcItem.itemName, srcItem.itemParent)
self.theParent.treeView.revealNewTreeItem(fHandle)
logger.verbose("Creating folder '%s'", fHandle)
# Loop through, and create the files
for wTitle, iStart, iEnd in finalOrder:
- isNovel = srcItem.itemClass == nwItemClass.NOVEL
- itemLayout = nwItemLayout.DOCUMENT if isNovel else nwItemLayout.NOTE
-
wTitle = wTitle.lstrip("#").strip()
- nHandle = self.theProject.newFile(wTitle, srcItem.itemClass, fHandle)
+ nHandle = self.theProject.newFile(wTitle, fHandle)
newItem = self.theProject.projTree[nHandle]
- newItem.setLayout(itemLayout)
newItem.setStatus(srcItem.itemStatus)
newItem.setImport(srcItem.itemImport)
logger.verbose(
diff --git a/novelwriter/dialogs/itemeditor.py b/novelwriter/dialogs/itemeditor.py
index 84a0db14..b5faec0d 100644
--- a/novelwriter/dialogs/itemeditor.py
+++ b/novelwriter/dialogs/itemeditor.py
@@ -33,7 +33,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter.enum import nwItemLayout, nwItemType
-from novelwriter.constants import trConst, nwLists, nwLabels
+from novelwriter.constants import trConst, nwLabels
from novelwriter.gui.custom import QSwitch
logger = logging.getLogger(__name__)
@@ -74,7 +74,7 @@ class GuiItemEditor(QDialog):
# Item Status
self.editStatus = QComboBox()
self.editStatus.setMinimumWidth(mVd)
- if self.theItem.itemClass in nwLists.CLS_NOVEL:
+ if self.theItem.isNovelLike():
for key, entry in self.theProject.statusItems.items():
self.editStatus.addItem(entry["icon"], entry["name"], key)
@@ -95,7 +95,7 @@ class GuiItemEditor(QDialog):
self.editLayout.setMinimumWidth(mVd)
validLayouts = []
if self.theItem.itemType == nwItemType.FILE:
- if self.theItem.itemClass in nwLists.CLS_NOVEL:
+ if self.theItem.documentAllowed():
validLayouts.append(nwItemLayout.DOCUMENT)
validLayouts.append(nwItemLayout.NOTE)
else:
diff --git a/novelwriter/gui/mainmenu.py b/novelwriter/gui/mainmenu.py
index 4bb567d5..f69d5a56 100644
--- a/novelwriter/gui/mainmenu.py
+++ b/novelwriter/gui/mainmenu.py
@@ -71,24 +71,6 @@ class GuiMainMenu(QMenuBar):
return
- ##
- # Methods
- ##
-
- def setAvailableRoot(self):
- """Update the list of available root folders and set the ones
- that are active.
- """
- for itemClass in nwItemClass:
- if itemClass == nwItemClass.NO_CLASS:
- continue
- if itemClass == nwItemClass.TRASH:
- continue
- self.rootItems[itemClass].setVisible(
- self.theProject.projTree.checkRootUnique(itemClass)
- )
- return
-
##
# Update Menu on Settings Changed
##
@@ -215,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
@@ -277,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 7c0ec69e..23238b84 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, 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"), itemClass, pHandle)
-
+ if pItem.isNovelLike():
+ 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"), itemClass, 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
@@ -280,13 +230,9 @@ class GuiProjectTree(QTreeWidget):
if nwItem.itemType != nwItemType.FILE:
return True
- # This is a new files, so let's add some content
+ # 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:
@@ -310,6 +256,9 @@ class GuiProjectTree(QTreeWidget):
"""Reveal a newly added project item in the project tree.
"""
nwItem = self.theProject.projTree[tHandle]
+ if nwItem is None:
+ return False
+
trItem = self._addTreeItem(nwItem, nHandle)
if trItem is None:
return False
@@ -495,8 +444,7 @@ class GuiProjectTree(QTreeWidget):
logger.error("Could not delete item")
return False
- pHandle = nwItemS.itemParent
- if self.theProject.projTree.isTrashRoot(pHandle):
+ if self.theProject.projTree.isTrash(tHandle):
# If the file is in the trash folder already, as the
# user if they want to permanently delete the file.
doPermanent = False
@@ -513,13 +461,6 @@ class GuiProjectTree(QTreeWidget):
if doPermanent:
logger.debug("Permanently deleting file with handle '%s'", tHandle)
- self.propagateCount(tHandle, 0)
- tIndex = trItemP.indexOfChild(trItemS)
- trItemC = trItemP.takeChild(tIndex)
-
- if self.theParent.docEditor.docHandle() == tHandle:
- self.theParent.closeDocument()
-
delDoc = NWDoc(self.theProject, tHandle)
if not delDoc.deleteDocument():
self.theParent.makeAlert([
@@ -527,6 +468,13 @@ class GuiProjectTree(QTreeWidget):
], nwAlert.ERROR)
return False
+ self.propagateCount(tHandle, 0)
+ tIndex = trItemP.indexOfChild(trItemS)
+ trItemC = trItemP.takeChild(tIndex)
+
+ if self.theParent.docEditor.docHandle() == tHandle:
+ self.theParent.closeDocument()
+
self.theIndex.deleteHandle(tHandle)
self._deleteTreeItem(tHandle)
self._setTreeChanged(True)
@@ -540,19 +488,13 @@ class GuiProjectTree(QTreeWidget):
self.tr("Move file '{0}' to Trash?").format(nwItemS.itemName),
)
if msgYes:
- if pHandle is None:
- logger.warning("File has no parent item")
-
logger.debug("Moving file '%s' to trash", tHandle)
self.propagateCount(tHandle, 0)
- tIndex = trItemP.indexOfChild(trItemS)
+ tIndex = trItemP.indexOfChild(trItemS)
trItemC = trItemP.takeChild(tIndex)
trItemT.addChild(trItemC)
- self._updateItemParent(tHandle)
- self.propagateCount(tHandle, wCount)
-
- self.theIndex.deleteHandle(tHandle)
+ self._postItemMove(tHandle, wCount)
self._recordLastMove(trItemS, trItemP, tIndex)
self._setTreeChanged(True)
@@ -562,6 +504,7 @@ class GuiProjectTree(QTreeWidget):
if trItemP is None:
logger.error("Could not delete folder")
return False
+
tIndex = trItemP.indexOfChild(trItemS)
if trItemS.childCount() == 0:
trItemP.takeChild(tIndex)
@@ -581,7 +524,6 @@ class GuiProjectTree(QTreeWidget):
if trItemS.childCount() == 0:
self.takeTopLevelItem(tIndex)
self._deleteTreeItem(tHandle)
- self.theParent.mainMenu.setAvailableRoot()
self._setTreeChanged(True)
else:
self.theParent.makeAlert(self.tr(
@@ -634,7 +576,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
@@ -654,12 +596,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
@@ -714,9 +657,7 @@ class GuiProjectTree(QTreeWidget):
movItem = parItem.takeChild(srcIndex)
dstItem.insertChild(dstIndex, movItem)
- snItem = self.theProject.projTree[sHandle]
- dnItem = self.theProject.projTree[dHandle]
- self._postItemMove(sHandle, snItem, dnItem, wCount)
+ self._postItemMove(sHandle, wCount)
self.clearSelection()
movItem.setSelected(True)
@@ -848,29 +789,34 @@ class GuiProjectTree(QTreeWidget):
if pItem is not None:
pIndex = pItem.indexOfChild(sItem)
- wCount = int(sItem.data(self.C_COUNT, Qt.UserRole))
+ # Determine if the drag and drop is allowed:
+ # - Files can be moved anywhere
+ # - Folders can only be moved within the same root folder
+ # - Root folders cannot be moved at all
+ # - Items cannot be dropped on top of a file (moved inside)
+
isFile = snItem.itemType == nwItemType.FILE
isRoot = snItem.itemType == nwItemType.ROOT
onFile = dnItem.itemType == nwItemType.FILE
+ inSame = snItem.itemRoot == dnItem.itemRoot
- isSame = snItem.itemClass == dnItem.itemClass
- isNone = snItem.itemClass == nwItemClass.NO_CLASS
- isNote = snItem.itemLayout == nwItemLayout.NOTE
- onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile
-
- allowDrop = isSame or isNone or isNote or onFree
+ allowDrop = inSame or isFile
allowDrop &= not (self.dropIndicatorPosition() == QAbstractItemView.OnItem and onFile)
if allowDrop and not isRoot:
logger.debug("Drag'n'drop of item '%s' accepted", sHandle)
+
+ wCount = int(sItem.data(self.C_COUNT, Qt.UserRole))
self.propagateCount(sHandle, 0)
+
QTreeWidget.dropEvent(self, theEvent)
- self._postItemMove(sHandle, snItem, dnItem, wCount)
+ self._postItemMove(sHandle, wCount)
self._recordLastMove(sItem, pItem, pIndex)
else:
- theEvent.ignore()
logger.debug("Drag'n'drop of item '%s' not accepted", sHandle)
+
+ theEvent.ignore()
self.theParent.makeAlert(self.tr(
"The item cannot be moved to that location."
), nwAlert.ERROR)
@@ -881,40 +827,39 @@ class GuiProjectTree(QTreeWidget):
# Internal Functions
##
- def _postItemMove(self, sHandle, snItem, dnItem, wCount):
+ def _postItemMove(self, tHandle, wCount):
"""Run various maintenance tasks for a moved item.
"""
- isFile = snItem.itemType == nwItemType.FILE
- isSame = snItem.itemClass == dnItem.itemClass
- onFree = dnItem.itemClass in nwLists.FREE_CLASS and isFile
+ trItemS = self._getTreeItem(tHandle)
+ nwItemS = self.theProject.projTree[tHandle]
+ trItemP = trItemS.parent()
+ if trItemP is None:
+ logger.error("Failed to find new parent item of '%s'", tHandle)
+ return False
- self._updateItemParent(sHandle)
+ # Update item parent handle in the project, make sure meta data
+ # is updated accordingly, and update word count
+ pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
+ nwItemS.setParent(pHandle)
+ self.theProject.projTree.updateItemData(tHandle)
+ self.setTreeItemValues(tHandle)
+ self.propagateCount(tHandle, wCount)
- # If the item does not have the same class as the target,
- # and the target is not a free root folder, update its class
- if not (isSame or onFree):
- logger.debug(
- "Item '%s' class has been changed from '%s' to '%s'",
- sHandle, snItem.itemClass.name, dnItem.itemClass.name
- )
- snItem.setClass(dnItem.itemClass)
- self.setTreeItemValues(sHandle)
-
- self.propagateCount(sHandle, wCount)
+ logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
# The items dropped into archive or trash should be removed
# from the project index, for all other items, we rescan the
# file to ensure the index is up to date.
- if onFree:
- self.theIndex.deleteHandle(sHandle)
+ if nwItemS.isInactive():
+ self.theIndex.deleteHandle(tHandle)
else:
- self.theIndex.reIndexHandle(sHandle)
+ self.theIndex.reIndexHandle(tHandle)
# Trigger dependent updates
self._setTreeChanged(True)
- self._emitItemChange(sHandle)
+ self._emitItemChange(tHandle)
- return
+ return True
def _getTreeItem(self, tHandle):
"""Returns the QTreeWidgetItem of a given item handle.
@@ -966,7 +911,6 @@ class GuiProjectTree(QTreeWidget):
if pHandle is None:
if nwItem.itemType == nwItemType.ROOT:
self.addTopLevelItem(newItem)
- self.theParent.mainMenu.setAvailableRoot()
elif nwItem.itemType == nwItemType.TRASH:
self.addTopLevelItem(newItem)
else:
@@ -1015,25 +959,6 @@ class GuiProjectTree(QTreeWidget):
return trItem
- def _updateItemParent(self, tHandle):
- """Update the parent handle of an item so that the information
- in the project is consistent with the treeView.
- """
- trItemS = self._getTreeItem(tHandle)
- nwItemS = self.theProject.projTree[tHandle]
- trItemP = trItemS.parent()
- if trItemP is None:
- logger.error("Failed to find new parent item of '%s'", tHandle)
- return False
-
- pHandle = trItemP.data(self.C_NAME, Qt.UserRole)
- nwItemS.setParent(pHandle)
- self.setTreeItemValues(tHandle)
-
- logger.debug("The parent of item '%s' has been changed to '%s'", tHandle, pHandle)
-
- return True
-
def _setTreeChanged(self, theState):
"""Set the tree change flag, and propagate to the project.
"""
@@ -1048,7 +973,7 @@ class GuiProjectTree(QTreeWidget):
"""
if self.theProject.projTree.checkType(tHandle, nwItemType.FILE):
nwItem = self.theProject.projTree[tHandle]
- if nwItem.itemClass == nwItemClass.NOVEL:
+ if nwItem.isNovelLike():
self.novelItemChanged.emit()
else:
self.noteItemChanged.emit()
@@ -1182,7 +1107,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()
@@ -1190,7 +1115,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/novelwriter/guimain.py b/novelwriter/guimain.py
index c21fc091..fabd3867 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -55,7 +55,6 @@ from novelwriter.enum import (
nwItemType, nwItemClass, nwAlert, nwWidget, nwState
)
from novelwriter.common import getGuiItem, hexToInt
-from novelwriter.constants import nwLists
logger = logging.getLogger(__name__)
@@ -848,7 +847,7 @@ class GuiMain(QMainWindow):
tItem = self.theProject.projTree[tHandle]
if tItem is None:
return False
- if tItem.itemType not in nwLists.REG_TYPES:
+ if tItem.itemType == nwItemType.NO_TYPE:
return False
logger.verbose("Requesting change to item '%s'", tHandle)
diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py
index 7ebed572..d84a0d4c 100644
--- a/novelwriter/tools/build.py
+++ b/novelwriter/tools/build.py
@@ -780,14 +780,12 @@ class GuiBuildNovel(QDialog):
if theItem is None:
return False
- if not theItem.isExported and not ignoreFlag:
+ if not (theItem.isExported or ignoreFlag):
return False
isNone = theItem.itemType != nwItemType.FILE
isNone |= theItem.itemLayout == nwItemLayout.NO_LAYOUT
- isNone |= theItem.itemClass == nwItemClass.NO_CLASS
- isNone |= theItem.itemClass == nwItemClass.TRASH
- isNone |= theItem.itemParent == self.theProject.projTree.trashRoot()
+ isNone |= theItem.isInactive()
isNone |= theItem.itemParent is None
isNote = theItem.itemLayout == nwItemLayout.NOTE
isNovel = not isNone and not isNote
@@ -799,10 +797,6 @@ class GuiBuildNovel(QDialog):
if isNovel and not novelFiles:
return False
- rootItem = self.theProject.projTree.getRootItem(theItem.itemHandle)
- if rootItem.itemClass == nwItemClass.ARCHIVE:
- return False
-
return True
def _saveDocument(self, theFmt):
diff --git a/sample/nwProject.nwx b/sample/nwProject.nwx
index 002cea43..23660870 100644
--- a/sample/nwProject.nwx
+++ b/sample/nwProject.nwx
@@ -1,13 +1,13 @@
-
+
Sample Project
Sample Project
Jane Smith
Jay Doh
- 1303
+ 1306
199
- 65049
+ 65149
False
@@ -33,7 +33,7 @@
- New
+ New
Notes
Started
1st Draft
@@ -42,110 +42,110 @@
Finished
- None
+ None
Minor
Major
Main
- -
+
-
Novel
- -
+
-
Title Page
- -
+
-
Page
- -
+
-
Part One
- -
+
-
A Folder
- -
+
-
Chapter One
- -
+
-
Making a Scene
- -
+
-
Another Scene
- -
+
-
Interlude
- -
+
-
A Note on Structure
- -
+
-
Chapter Two
- -
+
-
We Found John!
- -
+
-
Characters
- -
+
-
Main Characters
- -
+
-
John Smith
- -
+
-
Jane Smith
- -
+
-
Locations
- -
+
-
Earth
- -
+
-
Space
- -
+
-
Mars
- -
+
-
Archive
- -
+
-
Scenes
- -
+
-
Old File
- -
+
-
Trash
- -
+
-
Delete Me!
diff --git a/tests/conftest.py b/tests/conftest.py
index 8551724b..a11db7e2 100644
--- a/tests/conftest.py
+++ b/tests/conftest.py
@@ -166,7 +166,9 @@ def nwGUI(qtbot, monkeypatch, fncDir, fncConf):
"""
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
monkeypatch.setattr("novelwriter.CONFIG", fncConf)
- nwGUI = novelwriter.main(["--testmode", "--config=%s" % fncDir, "--data=%s" % fncDir])
+ nwGUI = novelwriter.main(
+ ["--testmode", "--info", "--config=%s" % fncDir, "--data=%s" % fncDir]
+ )
qtbot.addWidget(nwGUI)
nwGUI.show()
qtbot.wait(20)
diff --git a/tests/lipsum/nwProject.nwx b/tests/lipsum/nwProject.nwx
index 8ffb64fc..d8d7a49e 100644
--- a/tests/lipsum/nwProject.nwx
+++ b/tests/lipsum/nwProject.nwx
@@ -44,87 +44,87 @@
- -
+
-
Novel
- -
+
-
Lorem Ipsum
- -
+
-
Front Matter
- -
+
-
Prologue
- -
+
-
Act One
- -
+
-
Chapter One
- -
+
-
Chapter One
- -
+
-
Scene One
- -
+
-
Scene Two
- -
+
-
Interlude
- -
+
-
Chapter Two
- -
+
-
Chapter Two
- -
+
-
Scene Three
- -
+
-
Scene Four
- -
+
-
Scene Five
- -
+
-
Characters
- -
+
-
Mr. Nobody
- -
+
-
Plot
- -
+
-
Main
- -
+
-
World
- -
+
-
Ancient Europe
diff --git a/tests/minimal/nwProject.nwx b/tests/minimal/nwProject.nwx
index 9824e2bb..6945dc08 100644
--- a/tests/minimal/nwProject.nwx
+++ b/tests/minimal/nwProject.nwx
@@ -42,35 +42,35 @@
- -
+
-
Novel
- -
+
-
Title Page
- -
+
-
New Chapter
- -
+
-
New Chapter
- -
+
-
New Scene
- -
+
-
Plot
- -
+
-
Characters
- -
+
-
World
diff --git a/tests/reference/coreProject_NewCustomA_nwProject.nwx b/tests/reference/coreProject_NewCustomA_nwProject.nwx
index 65d56307..9152e3b6 100644
--- a/tests/reference/coreProject_NewCustomA_nwProject.nwx
+++ b/tests/reference/coreProject_NewCustomA_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Test Custom
Test Novel
@@ -35,104 +35,104 @@
Finished
- New
+ New
Minor
Major
Main
- -
+
-
- Novel
+ Novel
- -
+
-
- Plot
+ Plot
- -
+
-
- Characters
+ Characters
- -
+
-
- Locations
+ Locations
- -
+
-
- Timeline
+ Timeline
- -
+
-
- Objects
+ Objects
- -
+
-
- Entities
+ Entities
- -
+
-
- Title Page
+ Title Page
- -
+
-
- Chapter 1
+ Chapter 1
- -
+
-
- Chapter 1
+ Chapter 1
- -
+
-
- Scene 1.1
+ Scene 1.1
- -
+
-
- Scene 1.2
+ Scene 1.2
- -
+
-
- Scene 1.3
+ Scene 1.3
- -
+
-
- Chapter 2
+ Chapter 2
- -
+
-
- Chapter 2
+ Chapter 2
- -
+
-
- Scene 2.1
+ Scene 2.1
- -
+
-
- Scene 2.2
+ Scene 2.2
- -
+
-
- Scene 2.3
+ Scene 2.3
- -
+
-
- Chapter 3
+ Chapter 3
- -
+
-
- Chapter 3
+ Chapter 3
- -
+
-
- Scene 3.1
+ Scene 3.1
- -
+
-
- Scene 3.2
+ Scene 3.2
- -
+
-
- Scene 3.3
+ Scene 3.3
diff --git a/tests/reference/coreProject_NewCustomB_nwProject.nwx b/tests/reference/coreProject_NewCustomB_nwProject.nwx
index baab7a17..ef29585b 100644
--- a/tests/reference/coreProject_NewCustomB_nwProject.nwx
+++ b/tests/reference/coreProject_NewCustomB_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Test Custom
Test Novel
@@ -35,68 +35,68 @@
Finished
- New
+ New
Minor
Major
Main
- -
+
-
- Novel
+ Novel
- -
+
-
- Plot
+ Plot
- -
+
-
- Characters
+ Characters
- -
+
-
- Locations
+ Locations
- -
+
-
- Timeline
+ Timeline
- -
+
-
- Objects
+ Objects
- -
+
-
- Entities
+ Entities
- -
+
-
- Title Page
+ Title Page
- -
+
-
- Scene 1
+ Scene 1
- -
+
-
- Scene 2
+ Scene 2
- -
+
-
- Scene 3
+ Scene 3
- -
+
-
- Scene 4
+ Scene 4
- -
+
-
- Scene 5
+ Scene 5
- -
+
-
- Scene 6
+ Scene 6
diff --git a/tests/reference/coreProject_NewFile_nwProject.nwx b/tests/reference/coreProject_NewFile_nwProject.nwx
index 978f5855..13fd7966 100644
--- a/tests/reference/coreProject_NewFile_nwProject.nwx
+++ b/tests/reference/coreProject_NewFile_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
@@ -33,52 +33,52 @@
Finished
- New
+ New
Minor
Major
Main
- -
+
-
Novel
- -
+
-
Plot
- -
+
-
Characters
- -
+
-
World
- -
+
-
Title Page
- -
+
-
New Chapter
- -
+
-
New Chapter
- -
+
-
New Scene
- -
+
-
- Hello
+ Hello
- -
+
-
- Jane
+ Jane
diff --git a/tests/reference/coreProject_NewMinimal_nwProject.nwx b/tests/reference/coreProject_NewMinimal_nwProject.nwx
index fc2ad735..9c58c831 100644
--- a/tests/reference/coreProject_NewMinimal_nwProject.nwx
+++ b/tests/reference/coreProject_NewMinimal_nwProject.nwx
@@ -40,35 +40,35 @@
- -
+
-
Novel
- -
+
-
Plot
- -
+
-
Characters
- -
+
-
World
- -
+
-
Title Page
- -
+
-
New Chapter
- -
+
-
New Chapter
- -
+
-
New Scene
diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx
index 94fdbeea..264cfa0f 100644
--- a/tests/reference/coreProject_NewRoot_nwProject.nwx
+++ b/tests/reference/coreProject_NewRoot_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
@@ -27,66 +27,82 @@
- New
+ New
Note
Draft
Finished
- New
+ New
Minor
Major
Main
-
- -
+
+
-
Novel
- -
+
-
Plot
- -
+
-
Characters
- -
+
-
World
- -
+
-
Title Page
- -
+
-
New Chapter
- -
+
-
New Chapter
- -
+
-
New Scene
- -
+
-
- Timeline
+ Novel
- -
+
-
- Object
+ Plot
- -
+
-
- Custom1
+ Character
- -
+
-
- Custom2
+ World
+
+ -
+
+ Timeline
+
+ -
+
+ Object
+
+ -
+
+ Custom1
+
+ -
+
+ Custom2
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 3432d06b..34f5a0ed 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
New Project
@@ -33,60 +33,60 @@
Finished
- New
+ New
Minor
Major
Main
- -
+
-
Novel
- -
+
-
Title Page
- -
+
-
New Chapter
- -
+
-
New Chapter
- -
+
-
New Scene
- -
+
-
Plot
- -
+
-
- New File
+ New Note
- -
+
-
Characters
- -
+
-
- New File
+ New Note
- -
+
-
World
- -
+
-
- New File
+ New Note
- -
+
-
- Trash
+ Trash
diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
index e6e80636..d6c8b904 100644
--- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx
@@ -33,44 +33,44 @@
Finished
- New
+ New
Minor
Major
Main
- -
+
-
- Novel
+ Novel
- -
+
-
- Title Page
+ Title Page
- -
+
-
- New Chapter
+ New Chapter
- -
+
-
- New Chapter
+ New Chapter
- -
+
-
- New Scene
+ New Scene
- -
+
-
- Plot
+ Plot
- -
+
-
- Characters
+ Characters
- -
+
-
- World
+ World
diff --git a/tests/reference/guiProjSettings_Dialog_nwProject.nwx b/tests/reference/guiProjSettings_Dialog_nwProject.nwx
index 1b3c818a..6c4cefdb 100644
--- a/tests/reference/guiProjSettings_Dialog_nwProject.nwx
+++ b/tests/reference/guiProjSettings_Dialog_nwProject.nwx
@@ -1,5 +1,5 @@
-
+
Project Name
Project Title
@@ -39,44 +39,44 @@
Final
- New
+ New
Minor
Major
Final
- -
+
-
- Novel
+ Novel
- -
+
-
- Title Page
+ Title Page
- -
+
-
- New Chapter
+ New Chapter
- -
+
-
- New Chapter
+ New Chapter
- -
+
-
- New Scene
+ New Scene
- -
+
-
- Plot
+ Plot
- -
+
-
- Characters
+ Characters
- -
+
-
- World
+ World
diff --git a/tests/test_core/test_core_document.py b/tests/test_core/test_core_document.py
index eb7794b5..881290d6 100644
--- a/tests/test_core/test_core_document.py
+++ b/tests/test_core/test_core_document.py
@@ -66,7 +66,7 @@ def testCoreDocument_LoadSave(monkeypatch, mockGUI, nwMinimal):
# Try to open a new (non-existent) file
nHandle = theProject.projTree.findRoot(nwItemClass.NOVEL)
assert nHandle is not None
- xHandle = theProject.newFile("New File", nwItemClass.NOVEL, nHandle)
+ xHandle = theProject.newFile("New File", nHandle)
theDoc = NWDoc(theProject, xHandle)
assert bool(theDoc) is True
assert repr(theDoc) == f""
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 9a7f7b88..cf7b1687 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -181,8 +181,8 @@ def testCoreIndex_CheckThese(nwMinimal, mockGUI):
assert theProject.openProject(nwMinimal) is True
theIndex = NWIndex(theProject)
- nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
- cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
+ nHandle = theProject.newFile("Hello", "a508bb932959c")
+ cHandle = theProject.newFile("Jane", "afb3043c7b2b3")
nItem = theProject.projTree[nHandle]
cItem = theProject.projTree[cHandle]
@@ -260,8 +260,8 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI):
theIndex = NWIndex(theProject)
# Some items for fail to scan tests
- dHandle = theProject.newFolder("Folder", nwItemClass.NOVEL, "a508bb932959c")
- xHandle = theProject.newFile("No Layout", nwItemClass.NOVEL, "a508bb932959c")
+ dHandle = theProject.newFolder("Folder", "a508bb932959c")
+ xHandle = theProject.newFile("No Layout", "a508bb932959c")
xItem = theProject.projTree[xHandle]
xItem.setLayout(nwItemLayout.NO_LAYOUT)
@@ -278,20 +278,24 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI):
tHandle = theProject.trashFolder()
assert theProject.projTree[tHandle] is not None
xItem.setParent(tHandle)
+ theProject.projTree.updateItemData(xItem.itemHandle)
+ assert xItem.itemRoot == tHandle
+ assert xItem.itemClass == nwItemClass.TRASH
assert theIndex.scanText(xHandle, "Hello World!") is False
# Create the archive root
aHandle = theProject.newRoot("Archive", nwItemClass.ARCHIVE)
assert theProject.projTree[aHandle] is not None
xItem.setParent(aHandle)
+ theProject.projTree.updateItemData(xItem.itemHandle)
assert theIndex.scanText(xHandle, "Hello World!") is False
# Make some usable items
- tHandle = theProject.newFile("Title", nwItemClass.NOVEL, "a508bb932959c")
- pHandle = theProject.newFile("Page", nwItemClass.NOVEL, "a508bb932959c")
- nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
- cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
- sHandle = theProject.newFile("Scene", nwItemClass.NOVEL, "a508bb932959c")
+ tHandle = theProject.newFile("Title", "a508bb932959c")
+ pHandle = theProject.newFile("Page", "a508bb932959c")
+ nHandle = theProject.newFile("Hello", "a508bb932959c")
+ cHandle = theProject.newFile("Jane", "afb3043c7b2b3")
+ sHandle = theProject.newFile("Scene", "a508bb932959c")
# Text Indexing
# =============
@@ -473,8 +477,8 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
assert theProject.openProject(nwMinimal) is True
theIndex = NWIndex(theProject)
- nHandle = theProject.newFile("Hello", nwItemClass.NOVEL, "a508bb932959c")
- cHandle = theProject.newFile("Jane", nwItemClass.CHARACTER, "afb3043c7b2b3")
+ nHandle = theProject.newFile("Hello", "a508bb932959c")
+ cHandle = theProject.newFile("Jane", "afb3043c7b2b3")
assert theIndex.getNovelData("", "") is None
assert theIndex.getNovelData("a508bb932959c", "") is None
@@ -628,9 +632,9 @@ def testCoreIndex_ExtractData(nwMinimal, mockGUI):
# Novel Stats
# ===========
- hHandle = theProject.newFile("Chapter", nwItemClass.NOVEL, "a508bb932959c")
- sHandle = theProject.newFile("Scene One", nwItemClass.NOVEL, "a508bb932959c")
- tHandle = theProject.newFile("Scene Two", nwItemClass.NOVEL, "a508bb932959c")
+ hHandle = theProject.newFile("Chapter", "a508bb932959c")
+ sHandle = theProject.newFile("Scene One", "a508bb932959c")
+ tHandle = theProject.newFile("Scene Two", "a508bb932959c")
theProject.projTree[hHandle].itemLayout == nwItemLayout.DOCUMENT
theProject.projTree[sHandle].itemLayout == nwItemLayout.DOCUMENT
diff --git a/tests/test_core/test_core_item.py b/tests/test_core/test_core_item.py
index 3adda4de..8f65bb05 100644
--- a/tests/test_core/test_core_item.py
+++ b/tests/test_core/test_core_item.py
@@ -71,6 +71,18 @@ def testCoreItem_Setters(mockGUI, constData):
theItem.setParent("0123456789abc")
assert theItem.itemParent == "0123456789abc"
+ # Root
+ theItem.setRoot(None)
+ assert theItem.itemRoot is None
+ theItem.setRoot(123)
+ assert theItem.itemRoot is None
+ theItem.setRoot("0123456789abcdef")
+ assert theItem.itemRoot is None
+ theItem.setRoot("0123456789abg")
+ assert theItem.itemRoot is None
+ theItem.setRoot("0123456789abc")
+ assert theItem.itemRoot == "0123456789abc"
+
# Order
theItem.setOrder(None)
assert theItem.itemOrder == 0
@@ -208,6 +220,7 @@ def testCoreItem_Methods(mockGUI):
# Status + Icon
# =============
+
theItem.setType("FILE")
theItem.setStatus("Note")
theItem.setImport("Minor")
@@ -217,11 +230,19 @@ def testCoreItem_Methods(mockGUI):
assert stT == "Note"
assert isinstance(stI, QIcon)
+ theItem.setImportStatus("Draft")
+ stT, stI = theItem.getImportStatus()
+ assert stT == "Draft"
+
theItem.setClass("CHARACTER")
stT, stI = theItem.getImportStatus()
assert stT == "Minor"
assert isinstance(stI, QIcon)
+ theItem.setImportStatus("Major")
+ stT, stI = theItem.getImportStatus()
+ assert stT == "Major"
+
# Representation
# ==============
@@ -263,6 +284,8 @@ def testCoreItem_TypeSetter(mockGUI):
assert theItem.itemType == nwItemType.FILE
theItem.setType("TRASH")
assert theItem.itemType == nwItemType.TRASH
+
+ # Alternative
theItem.setType(nwItemType.ROOT)
assert theItem.itemType == nwItemType.ROOT
@@ -282,28 +305,74 @@ def testCoreItem_ClassSetter(mockGUI):
assert theItem.itemClass == nwItemClass.NO_CLASS
theItem.setClass("NONSENSE")
assert theItem.itemClass == nwItemClass.NO_CLASS
+
theItem.setClass("NO_CLASS")
assert theItem.itemClass == nwItemClass.NO_CLASS
+ assert theItem.isNovelLike() is False
+ assert theItem.documentAllowed() is False
+ assert theItem.isInactive() is True
+
theItem.setClass("NOVEL")
assert theItem.itemClass == nwItemClass.NOVEL
+ assert theItem.isNovelLike() is True
+ assert theItem.documentAllowed() is True
+ assert theItem.isInactive() is False
+
theItem.setClass("PLOT")
assert theItem.itemClass == nwItemClass.PLOT
+ assert theItem.isNovelLike() is False
+ assert theItem.documentAllowed() is False
+ assert theItem.isInactive() is False
+
theItem.setClass("CHARACTER")
assert theItem.itemClass == nwItemClass.CHARACTER
+ assert theItem.isNovelLike() is False
+ assert theItem.documentAllowed() is False
+ assert theItem.isInactive() is False
+
theItem.setClass("WORLD")
assert theItem.itemClass == nwItemClass.WORLD
+ assert theItem.isNovelLike() is False
+ assert theItem.documentAllowed() is False
+ assert theItem.isInactive() is False
+
theItem.setClass("TIMELINE")
assert theItem.itemClass == nwItemClass.TIMELINE
+ assert theItem.isNovelLike() is False
+ assert theItem.documentAllowed() is False
+ assert theItem.isInactive() is False
+
theItem.setClass("OBJECT")
assert theItem.itemClass == nwItemClass.OBJECT
+ assert theItem.isNovelLike() is False
+ assert theItem.documentAllowed() is False
+ assert theItem.isInactive() is False
+
theItem.setClass("ENTITY")
assert theItem.itemClass == nwItemClass.ENTITY
+ assert theItem.isNovelLike() is False
+ assert theItem.documentAllowed() is False
+ assert theItem.isInactive() is False
+
theItem.setClass("CUSTOM")
assert theItem.itemClass == nwItemClass.CUSTOM
+ assert theItem.isNovelLike() is False
+ assert theItem.documentAllowed() is False
+ assert theItem.isInactive() is False
+
theItem.setClass("ARCHIVE")
assert theItem.itemClass == nwItemClass.ARCHIVE
+ assert theItem.isNovelLike() is True
+ assert theItem.documentAllowed() is True
+ assert theItem.isInactive() is True
+
theItem.setClass("TRASH")
assert theItem.itemClass == nwItemClass.TRASH
+ assert theItem.isNovelLike() is False
+ assert theItem.documentAllowed() is True
+ assert theItem.isInactive() is True
+
+ # Alternative
theItem.setClass(nwItemClass.NOVEL)
assert theItem.itemClass == nwItemClass.NOVEL
@@ -332,13 +401,69 @@ def testCoreItem_LayoutSetter(mockGUI):
theItem.setLayout("NOTE")
assert theItem.itemLayout == nwItemLayout.NOTE
- # Alternatives
+ # Alternative
theItem.setLayout(nwItemLayout.NOTE)
assert theItem.itemLayout == nwItemLayout.NOTE
# END Test testCoreItem_LayoutSetter
+@pytest.mark.core
+def testCoreItem_ClassDefaults(mockGUI):
+ """Test the setter for the default values.
+ """
+ theProject = NWProject(mockGUI)
+ theItem = NWItem(theProject)
+
+ # Root items should not have their class updated
+ theItem.setParent(None)
+ theItem.setClass(nwItemClass.NO_CLASS)
+ assert theItem.itemClass == nwItemClass.NO_CLASS
+
+ theItem.setClassDefaults(nwItemClass.NOVEL)
+ assert theItem.itemClass == nwItemClass.NO_CLASS
+
+ # Non-root items should have their class updated
+ theItem.setParent("0123456789abc")
+ theItem.setClass(nwItemClass.NO_CLASS)
+ assert theItem.itemClass == nwItemClass.NO_CLASS
+
+ theItem.setClassDefaults(nwItemClass.NOVEL)
+ assert theItem.itemClass == nwItemClass.NOVEL
+
+ # Non-layout items should have their layout set based on class
+ theItem.setParent("0123456789abc")
+ theItem.setClass(nwItemClass.NO_CLASS)
+ theItem.setLayout(nwItemLayout.NO_LAYOUT)
+ assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
+
+ theItem.setClassDefaults(nwItemClass.NOVEL)
+ assert theItem.itemLayout == nwItemLayout.DOCUMENT
+
+ theItem.setParent("0123456789abc")
+ theItem.setClass(nwItemClass.NO_CLASS)
+ theItem.setLayout(nwItemLayout.NO_LAYOUT)
+ assert theItem.itemLayout == nwItemLayout.NO_LAYOUT
+
+ theItem.setClassDefaults(nwItemClass.PLOT)
+ assert theItem.itemLayout == nwItemLayout.NOTE
+
+ # If documents are not allowed in that class, the layout should be changed
+ theItem.setParent("0123456789abc")
+ theItem.setClass(nwItemClass.NO_CLASS)
+ theItem.setLayout(nwItemLayout.DOCUMENT)
+ assert theItem.itemLayout == nwItemLayout.DOCUMENT
+
+ theItem.setClassDefaults(nwItemClass.PLOT)
+ assert theItem.itemLayout == nwItemLayout.NOTE
+
+ # In all cases, status and importance should no longer be None
+ assert theItem.itemStatus is not None
+ assert theItem.itemImport is not None
+
+# END Test testCoreItem_ClassDefaults
+
+
@pytest.mark.core
def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
"""Test packing and unpacking XML objects for the NWItem class.
@@ -353,6 +478,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
theItem = NWItem(theProject)
theItem.setHandle("0123456789abc")
theItem.setParent("0123456789abc")
+ theItem.setRoot("0123456789abc")
theItem.setOrder(1)
theItem.setName("A Name")
theItem.setClass("NOVEL")
@@ -370,9 +496,9 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
theItem.packXML(xContent)
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
b''
- b'- '
- b'A Name
'
+ b'- A Name
'
b''
) % bytes(constData.importKeys[3], encoding="utf8")
@@ -381,6 +507,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
assert theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "0123456789abc"
assert theItem.itemParent == "0123456789abc"
+ assert theItem.itemRoot == "0123456789abc"
assert theItem.itemOrder == 1
assert theItem.isExported is False
assert theItem.paraCount == 3
@@ -399,6 +526,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
theItem = NWItem(theProject)
theItem.setHandle("0123456789abc")
theItem.setParent("0123456789abc")
+ theItem.setRoot("0123456789abc")
theItem.setOrder(1)
theItem.setName("A Name")
theItem.setClass("NOVEL")
@@ -417,9 +545,9 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
theItem.packXML(xContent)
assert etree.tostring(xContent, pretty_print=False, encoding="utf-8") == (
b''
- b'- A Name'
- b'
'
+ b'- A Name
'
b''
) % bytes(constData.statusKeys[1], encoding="utf8")
@@ -428,6 +556,7 @@ def testCoreItem_XMLPackUnpack(mockGUI, caplog, constData):
assert theItem.unpackXML(xContent[0])
assert theItem.itemHandle == "0123456789abc"
assert theItem.itemParent == "0123456789abc"
+ assert theItem.itemRoot == "0123456789abc"
assert theItem.itemOrder == 1
assert theItem.isExpanded is True
assert theItem.isExported is True
diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py
index d4213442..8a962268 100644
--- a/tests/test_core/test_core_project.py
+++ b/tests/test_core/test_core_project.py
@@ -276,10 +276,10 @@ def testCoreProject_NewRoot(fncDir, outDir, refDir, mockGUI):
assert theProject.closeProject() is True
assert theProject.openProject(projFile) is True
- assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), type(None))
- assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), type(None))
- assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), type(None))
- assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), type(None))
+ assert isinstance(theProject.newRoot("Novel", nwItemClass.NOVEL), str)
+ assert isinstance(theProject.newRoot("Plot", nwItemClass.PLOT), str)
+ assert isinstance(theProject.newRoot("Character", nwItemClass.CHARACTER), str)
+ assert isinstance(theProject.newRoot("World", nwItemClass.WORLD), str)
assert isinstance(theProject.newRoot("Timeline", nwItemClass.TIMELINE), str)
assert isinstance(theProject.newRoot("Object", nwItemClass.OBJECT), str)
assert isinstance(theProject.newRoot("Custom1", nwItemClass.CUSTOM), str)
@@ -314,8 +314,8 @@ def testCoreProject_NewFile(fncDir, outDir, refDir, mockGUI):
assert theProject.closeProject() is True
assert theProject.openProject(projFile) is True
- assert isinstance(theProject.newFile("Hello", nwItemClass.NOVEL, "31489056e0916"), str)
- assert isinstance(theProject.newFile("Jane", nwItemClass.CHARACTER, "71ee45a3c0db9"), str)
+ assert isinstance(theProject.newFile("Hello", "31489056e0916"), str)
+ assert isinstance(theProject.newFile("Jane", "71ee45a3c0db9"), str)
assert theProject.projChanged
assert theProject.saveProject() is True
assert theProject.closeProject() is True
@@ -661,7 +661,7 @@ def testCoreProject_AccessItems(nwMinimal, mockGUI):
theProject.projTree._treeOrder.append("01234567789abc")
# Add an item with a non-existent parent
- nHandle = theProject.newFile("Test File", nwItemClass.NOVEL, "a6d311a93600a")
+ nHandle = theProject.newFile("Test File", "a6d311a93600a")
theProject.projTree[nHandle].setParent("cba9876543210")
assert theProject.projTree[nHandle].itemParent == "cba9876543210"
@@ -740,7 +740,7 @@ def testCoreProject_StatusImport(mockGUI, fncDir, constData):
# Change Importance
# =================
- fHandle = theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "73475cb40a568")
+ fHandle = theProject.newFile("Jane Doe", "73475cb40a568")
theProject.projTree[fHandle].setImport("Main")
assert theProject.projTree[fHandle].itemImport == constData.importKeys[3]
@@ -1016,9 +1016,16 @@ def testCoreProject_OrphanedFiles(mockGUI, nwLipsum):
"""
theProject = NWProject(mockGUI)
- assert theProject.openProject(nwLipsum)
+ assert theProject.openProject(nwLipsum) is True
assert theProject.projTree["636b6aa9b697b"] is None
- assert theProject.closeProject()
+
+ # Add a file with non-existent parent
+ # This file will be renoved from the project on open
+ assert theProject.newFile("Oops", "0000000000000")
+
+ # Save and close
+ assert theProject.saveProject() is True
+ assert theProject.closeProject() is True
# First Item with Meta Data
orphPath = os.path.join(nwLipsum, "content", "636b6aa9b697b.nwd")
diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py
index b9ad0473..3b35abd5 100644
--- a/tests/test_core/test_core_tree.py
+++ b/tests/test_core/test_core_tree.py
@@ -21,6 +21,7 @@ along with this program. If not, see .
import os
import pytest
+import random
from lxml import etree
from hashlib import sha256
@@ -36,6 +37,7 @@ from novelwriter.constants import nwFiles
def mockItems(mockGUI):
"""Create a list of mock items.
"""
+ random.seed(42)
theProject = NWProject(mockGUI)
itemA = NWItem(theProject)
@@ -103,7 +105,7 @@ def mockItems(mockGUI):
("a000000000002", None, itemE),
("a000000000003", None, itemF),
("a000000000004", None, itemG),
- ("b000000000002", "a000000000002", itemH),
+ ("b000000000002", "a000000000004", itemH),
]
return theItems
@@ -120,22 +122,21 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert theTree._handleSeed == 42
# Check that tree is empty (calls NWTree.__bool__)
- assert not theTree
+ assert bool(theTree) is False
# Check for archive and trash folders
assert theTree.trashRoot() is None
- assert theTree.archiveRoot() is None
- assert not theTree.isTrashRoot("a000000000003")
aHandles = []
for tHandle, pHandle, nwItem in mockItems:
aHandles.append(tHandle)
- assert theTree.append(tHandle, pHandle, nwItem)
+ assert theTree.append(tHandle, pHandle, nwItem) is True
+ assert theTree.updateItemData(tHandle) is True
- assert theTree._treeChanged
+ assert theTree._treeChanged is True
# Check that tree is not empty (calls __bool__)
- assert theTree
+ assert bool(theTree) is True
# Check the number of elements (calls __len__)
assert len(theTree) == len(mockItems)
@@ -149,8 +150,29 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
# Check that we have the correct archive and trash folders
assert theTree.trashRoot() == "a000000000003"
- assert theTree.archiveRoot() == "a000000000002"
- assert theTree.isTrashRoot("a000000000003")
+ assert theTree.findRoot(nwItemClass.ARCHIVE) == "a000000000002"
+ assert theTree.isTrash("a000000000003") is True
+ assert theTree.isRoot("a000000000002") is True
+
+ # Check the isTrash function
+ assert theTree.isTrash("0000000000000") is True # Doesn't exist
+ assert theTree.isTrash("a000000000003") is True # This the trash folder
+
+ theTree["a000000000003"].setClass(nwItemClass.NO_CLASS)
+ assert theTree.isTrash("a000000000003") is True # This is still trash
+ theTree["a000000000003"].setClass(nwItemClass.TRASH)
+
+ assert theTree.isTrash("b000000000002") is False # This is not trash
+
+ value = theTree["b000000000002"].itemParent
+ theTree["b000000000002"].setParent("a000000000003")
+ assert theTree.isTrash("b000000000002") is True # This is in trash
+ theTree["b000000000002"].setParent(value)
+
+ value = theTree["b000000000002"].itemRoot
+ theTree["b000000000002"].setRoot("a000000000003")
+ assert theTree.isTrash("b000000000002") is True # This is in trash
+ theTree["b000000000002"].setRoot(value)
# Try to add another trash folder
itemT = NWItem(theProject)
@@ -159,7 +181,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
itemT._class = nwItemClass.TRASH
itemT._expanded = False
- assert not theTree.append("1234567890abc", None, itemT)
+ assert theTree.append("1234567890abc", None, itemT) is False
assert len(theTree) == len(mockItems)
# Generate handle automatically
@@ -169,14 +191,15 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
itemT._class = nwItemClass.NOVEL
itemT._layout = nwItemLayout.DOCUMENT
- assert theTree.append(None, None, itemT)
+ assert theTree.append(None, None, itemT) is True
+ assert theTree.updateItemData(itemT.itemHandle) is True
assert len(theTree) == len(mockItems) + 1
theList = theTree.handles()
assert theList[-1] == "73475cb40a568"
# Try to add existing handle
- assert not theTree.append("73475cb40a568", None, itemT)
+ assert theTree.append("73475cb40a568", None, itemT) is False
assert len(theTree) == len(mockItems) + 1
# Delete a non-existing item
@@ -196,7 +219,6 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
del theTree["a000000000002"]
assert len(theTree) == len(mockItems) - 2
assert "a000000000002" not in theTree
- assert theTree.archiveRoot() is None
del theTree["a000000000003"]
assert len(theTree) == len(mockItems) - 3
@@ -215,31 +237,43 @@ def testCoreTree_Methods(mockGUI, mockItems):
for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem)
+ theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems)
+ # Update item data, nonsense handle
+ assert theTree.updateItemData("stuff") is False
+
+ # Update item data, invalid item parent
+ corrParent = theTree["b000000000001"].itemParent
+ theTree["b000000000001"].setParent("0000000000000")
+ assert theTree.updateItemData("b000000000001") is False
+
+ # Update item data, valid item parent
+ theTree["b000000000001"].setParent(corrParent)
+ assert theTree.updateItemData("b000000000001") is True
+
+ # Update item data, root is unreachable
+ 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
assert theTree.checkType("b000000000001", nwItemType.FILE) is False
assert theTree.checkType("c000000000001", nwItemType.FILE) is True
# Root item lookup
- theTree._treeRoots.append("stuff")
assert theTree.findRoot(nwItemClass.WORLD) is None
assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001"
assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004"
- # Check for root uniqueness
- assert theTree.checkRootUnique(nwItemClass.CUSTOM)
- assert theTree.checkRootUnique(nwItemClass.WORLD)
- assert not theTree.checkRootUnique(nwItemClass.NOVEL)
- assert not theTree.checkRootUnique(nwItemClass.CHARACTER)
-
- # Find root item of child item
- assert theTree.getRootItem("b000000000001").itemHandle == "a000000000001"
- assert theTree.getRootItem("c000000000001").itemHandle == "a000000000001"
- assert theTree.getRootItem("c000000000002").itemHandle == "a000000000001"
- assert theTree.getRootItem("stuff") is None
+ # Add a fake item to root and check that it can handle it
+ theTree._treeRoots["0000000000000"] = NWItem(theProject)
+ assert theTree.findRoot(nwItemClass.WORLD) is None
+ del theTree._treeRoots["0000000000000"]
# Get item path
assert theTree.getItemPath("stuff") == []
@@ -247,6 +281,13 @@ def testCoreTree_Methods(mockGUI, mockItems):
"c000000000001", "b000000000001", "a000000000001"
]
+ # Cause recursion error
+ maxDepth = theTree.MAX_DEPTH
+ theTree.MAX_DEPTH = 0
+ with pytest.raises(RecursionError):
+ theTree.getItemPath("c000000000001")
+ theTree.MAX_DEPTH = maxDepth
+
# Break the folder parent handle
theTree["b000000000001"]._parent = "stuff"
assert theTree.getItemPath("c000000000001") == [
@@ -371,7 +412,7 @@ def testCoreTree_Reorder(mockGUI, mockItems):
@pytest.mark.core
-def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
+def testCoreTree_XMLPackUnpack(mockGUI, mockItems, constData):
"""Test packing and unpacking the tree to and from XML.
"""
theProject = NWProject(mockGUI)
@@ -379,37 +420,47 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem)
+ theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems)
nwXML = etree.Element("novelWriterXML")
theTree.packXML(nwXML)
- assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == (
- b''
- b''
- b'- Novel
'
- b'- Act One'
- b'
'
- b'- '
- b'Chapter One
'
- b'- '
- b'Scene One
'
- b'- Outtakes
'
- b'- Trash
'
- b'- '
- b'Characters
'
- b'- Jane Doe
'
- b''
- b''
- )
+ assert etree.tostring(nwXML, pretty_print=False, encoding="utf-8") == bytes((
+ ''
+ ''
+ '- Novel
'
+ '- Act One
'
+ '- Chapter One
'
+ '- Scene One
'
+ '- Outtakes
'
+ '- Trash
'
+ '- Characters
'
+ '- Jane Doe
'
+ ''
+ ''
+ ).format(
+ s0=constData.statusKeys[0], i0=constData.importKeys[0]
+ ), encoding="utf8")
theTree.clear()
assert len(theTree) == 0
@@ -429,6 +480,7 @@ def testCoreTree_ToCFile(monkeypatch, mockGUI, mockItems, tmpDir):
for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem)
+ theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems)
theTree._treeOrder.append("stuff")
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
diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py
index eaa1d2ba..cf5c3cea 100644
--- a/tests/test_gui/test_gui_doceditor.py
+++ b/tests/test_gui/test_gui_doceditor.py
@@ -29,7 +29,7 @@ from PyQt5.QtWidgets import QAction, QMessageBox, qApp
from novelwriter.gui.doceditor import GuiDocEditor
from novelwriter.core import countWords
-from novelwriter.enum import nwDocAction, nwDocInsert, nwItemClass, nwItemLayout
+from novelwriter.enum import nwDocAction, nwDocInsert, nwItemLayout
from novelwriter.constants import nwKeyWords, nwUnicode
keyDelay = 2
@@ -1143,7 +1143,7 @@ def testGuiEditor_Tags(qtbot, monkeypatch, nwGUI, nwMinimal, ipsumText):
# Create Character
theText = "### Jane Doe\n\n@tag: Jane\n\n" + ipsumText[1] + "\n\n"
- cHandle = nwGUI.theProject.newFile("Jane Doe", nwItemClass.CHARACTER, "afb3043c7b2b3")
+ cHandle = nwGUI.theProject.newFile("Jane Doe", "afb3043c7b2b3")
assert nwGUI.openDocument(cHandle) is True
assert nwGUI.docEditor.replaceText(theText) is True
assert nwGUI.saveDocument() is True
diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py
index 445bfb9a..1f1c8c95 100644
--- a/tests/test_gui/test_gui_projtree.py
+++ b/tests/test_gui/test_gui_projtree.py
@@ -22,9 +22,6 @@ along with this program. If not, see .
import pytest
import os
-from tools import writeFile
-
-from PyQt5.QtCore import QItemSelectionModel
from PyQt5.QtWidgets import QAction, QMessageBox
from novelwriter.guimain import GuiMain
@@ -33,207 +30,406 @@ from novelwriter.enum import nwItemType, nwItemClass
@pytest.mark.gui
-def testGuiProjTree_TreeItems(qtbot, caplog, monkeypatch, nwGUI, nwMinimal):
+def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, fncDir):
"""Test adding and removing items from the project tree.
"""
# Block message box
- monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
- monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
monkeypatch.setattr(GuiMain, "editItem", lambda *a: None)
- nwGUI.theProject.projTree.setSeed(42)
nwTree = nwGUI.treeView
- ##
- # Add New Items
- ##
+ # Try to add item with no project
+ assert nwTree.newTreeItem(nwItemType.FILE) is False
- # Try to add and move item with no project
- assert not nwTree.newTreeItem(nwItemType.FILE, None)
- assert not nwTree.moveTreeItem(1)
+ # Create a project
+ nwGUI.theProject.projTree.setSeed(42)
+ prjDir = os.path.join(fncDir, "project")
+ assert nwGUI.newProject({"projPath": prjDir}) is True
- # Open a project
- assert nwGUI.openProject(nwMinimal)
+ # No itemType set
+ nwTree.clearSelection()
+ assert nwTree.newTreeItem(None) is False
+
+ # Root Items
+ # ==========
+
+ # No class set
+ assert nwTree.newTreeItem(nwItemType.ROOT) is False
+
+ # Create root item
+ assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True
+ assert "1a6562590ef19" in nwGUI.theProject.projTree
+
+ # File/Folder Items
+ # =================
# No location selected for new item
nwTree.clearSelection()
- assert not nwTree.newTreeItem(nwItemType.FILE, None)
- assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
- assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
+ caplog.clear()
+ assert nwTree.newTreeItem(nwItemType.FILE) is False
+ assert nwTree.newTreeItem(nwItemType.FOLDER) is False
+ assert "Did not find anywhere" in caplog.text
- # No itemType set or ROOT, but no class
- assert not nwTree.newTreeItem(None, None)
- assert not nwTree.newTreeItem(nwItemType.ROOT, None)
+ # Create new folder as child of Novel folder
+ nwTree.setSelectedHandle("73475cb40a568")
+ assert nwTree.newTreeItem(nwItemType.FOLDER) is True
+ assert nwGUI.theProject.projTree["031b4af5197ec"].itemParent == "73475cb40a568"
+ assert nwGUI.theProject.projTree["031b4af5197ec"].itemRoot == "73475cb40a568"
+ assert nwGUI.theProject.projTree["031b4af5197ec"].itemClass == nwItemClass.NOVEL
- # Select a location
- chItem = nwTree._getTreeItem("a6d311a93600a")
- nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
- chItem.setExpanded(True)
+ # Add a new file in the new folder
+ nwTree.setSelectedHandle("031b4af5197ec")
+ assert nwTree.newTreeItem(nwItemType.FILE) is True
+ assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemParent == "031b4af5197ec"
+ assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemRoot == "73475cb40a568"
+ assert nwGUI.theProject.projTree["41cfc0d1f2d12"].itemClass == nwItemClass.NOVEL
- # Create new item with no class set (defaults to NOVEL)
- assert nwTree.newTreeItem(nwItemType.FILE, None)
- assert nwTree.newTreeItem(nwItemType.FOLDER, None)
+ # Add a new file next to the other new file
+ nwTree.setSelectedHandle("41cfc0d1f2d12")
+ assert nwTree.newTreeItem(nwItemType.FILE) is True
+ assert nwGUI.theProject.projTree["2858dcd1057d3"].itemParent == "031b4af5197ec"
+ assert nwGUI.theProject.projTree["2858dcd1057d3"].itemRoot == "73475cb40a568"
+ assert nwGUI.theProject.projTree["2858dcd1057d3"].itemClass == nwItemClass.NOVEL
+ assert nwGUI.openDocument("2858dcd1057d3")
+ assert nwGUI.docEditor.getText() == "### New Document\n\n"
- # Check that we have the correct tree order
- assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
- ]
+ # Add a new file to the characters folder
+ nwTree.setSelectedHandle("71ee45a3c0db9")
+ assert nwTree.newTreeItem(nwItemType.FILE) is True
+ assert nwGUI.theProject.projTree["2fca346db6561"].itemParent == "71ee45a3c0db9"
+ assert nwGUI.theProject.projTree["2fca346db6561"].itemRoot == "71ee45a3c0db9"
+ assert nwGUI.theProject.projTree["2fca346db6561"].itemClass == nwItemClass.CHARACTER
+ assert nwGUI.openDocument("2fca346db6561")
+ assert nwGUI.docEditor.getText() == "# New Note\n\n"
- # Add roots
- assert not nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) # Duplicate
- assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) # Valid
+ # Make sure the sibling folder bug trap works
+ nwTree.setSelectedHandle("2858dcd1057d3")
+ nwGUI.theProject.projTree["2858dcd1057d3"].setParent(None) # This should not happen
+ caplog.clear()
+ assert nwTree.newTreeItem(nwItemType.FILE) is False
+ assert "Internal error" in caplog.text
+ nwGUI.theProject.projTree["2858dcd1057d3"].setParent("031b4af5197ec")
- # Change max depth and try to add a subfolder that is too deep
- monkeypatch.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 2)
- chItem = nwTree._getTreeItem("71ee45a3c0db9")
- nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
- assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
+ # Get the trash folder
+ nwTree._addTrashRoot()
+ trashHandle = nwGUI.theProject.trashFolder()
+ nwTree.setSelectedHandle(trashHandle)
+ assert nwTree.newTreeItem(nwItemType.FILE) is False
+ assert "Cannot add new files or folders to the Trash folder" in caplog.text
- ##
- # Move Items
- ##
+ # Other Checks
+ # ============
- nwTree.setSelectedHandle("8c659a11cd429")
+ # Also check error handling in reveal function
+ assert nwTree.revealNewTreeItem("abc") is False
- # Shift focus and try to move item
- monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
- assert not nwTree.moveTreeItem(1)
- assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
- ]
- monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
-
- # Move second item up twice (should give same result)
- nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
- assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
- ]
- nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
- assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "8c659a11cd429", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9"
- ]
-
- # Move it back down four times (last two should be the same)
- nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
- assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "f5ab3e30151e1", "8c659a11cd429", "44cb730c42048", "71ee45a3c0db9"
- ]
- nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
- assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "8c659a11cd429", "71ee45a3c0db9"
- ]
- nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
- assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
- ]
- nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
- assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
- ]
-
- # Move up twice, and undo
- nwTree._lastMove = {}
- nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
- nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
- nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger)
- assert nwTree.getTreeFromHandle("a6d311a93600a") == [
- "a6d311a93600a", "f5ab3e30151e1", "44cb730c42048", "71ee45a3c0db9", "8c659a11cd429"
- ]
-
- # Move a root item (top level items are different) twice
- nwTree.flushTreeOrder()
- assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 10
- nwTree.setSelectedHandle("9d5247ab588e0")
-
- nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
- nwTree.flushTreeOrder()
- assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
-
- nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
- nwTree.flushTreeOrder()
- assert nwGUI.theProject.projTree._treeOrder.index("9d5247ab588e0") == 11
-
- ##
- # Delete and Trash
- ##
-
- # Add some content to the new file
- nwGUI.openDocument("73475cb40a568")
- nwGUI.docEditor.setText("# Hello World\n")
- nwGUI.saveDocument()
- nwGUI.saveProject()
- assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
-
- # Delete the items we added earlier
- nwTree.clearSelection()
- assert not nwTree.emptyTrash() # No folder yet
- assert not nwTree.deleteItem(None)
- assert not nwTree.deleteItem("1111111111111")
- assert nwTree.deleteItem("73475cb40a568") # New File
- assert nwTree.deleteItem("71ee45a3c0db9") # New Folder
- assert nwTree.deleteItem("811786ad1ae74") # Custom Root
- assert "73475cb40a568" in nwGUI.theProject.projTree._treeOrder
- assert "71ee45a3c0db9" not in nwGUI.theProject.projTree._treeOrder
- assert "811786ad1ae74" not in nwGUI.theProject.projTree._treeOrder
-
- # The file is in trash, empty it
- assert os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
- assert nwTree.emptyTrash()
- assert not nwTree.emptyTrash() # Already empty
- assert not os.path.isfile(os.path.join(nwMinimal, "content", "73475cb40a568.nwd"))
- assert "73475cb40a568" not in nwGUI.theProject.projTree._treeOrder
-
- # Should not be allowed to add files and folders to Trash
- trashHandle = nwGUI.theProject.projTree.trashRoot()
- chItem = nwTree._getTreeItem(trashHandle)
- nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
- assert not nwTree.newTreeItem(nwItemType.FILE, None)
- assert not nwTree.newTreeItem(nwItemType.FOLDER, None)
-
- # Close the project
- nwGUI.closeProject()
-
- ##
- # Orphaned Files
- ##
-
- # Add an orphaned file
- orphFile = os.path.join(nwMinimal, "content", "1234567890abc.nwd")
- writeFile(orphFile, "# Hello World\n")
-
- # Open the project again
- nwGUI.openProject(nwMinimal)
-
- # Check that the orphaned file was found and added to the tree
- nwTree.flushTreeOrder()
- assert "1234567890abc" in nwGUI.theProject.projTree._treeOrder
- orItem = nwTree._getTreeItem("1234567890abc")
- assert orItem.text(nwTree.C_NAME) == "Recovered File 1"
-
- ##
- # Unexpected Error Handling
- ##
-
- # Add an item with an invalid type
- assert not nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL)
- assert "Failed to add new item" in caplog.messages[-1]
-
- # Add new file after one that has no parent handle
- chItem = nwTree._getTreeItem("44cb730c42048")
- nwTree.setCurrentItem(chItem, QItemSelectionModel.Current)
- nwTree.theProject.projTree["44cb730c42048"]._parent = None
- assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
- nwTree.clearSelection()
-
- # Add a file with no parent, and fail to find a suitable parent item
- monkeypatch.setattr("novelwriter.core.tree.NWTree.findRoot", lambda *a: None)
-
- assert not nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL)
- assert not nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL)
+ # Add an item that cannot be displayed in the tree
+ nHandle = nwGUI.theProject.newFile("Test", None)
+ assert nwTree.revealNewTreeItem(nHandle) is False
+ # Clean up
# qtbot.stopForInteraction()
nwGUI.closeProject()
-# END Test testGuiProjTree_TreeItems
+# END Test testGuiProjTree_NewItems
+
+
+@pytest.mark.gui
+def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, fncDir):
+ """Test adding and removing items from the project tree.
+ """
+ # Block message box
+ monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(GuiMain, "editItem", lambda *a: None)
+
+ nwTree = nwGUI.treeView
+
+ # Try to move item with no project
+ assert nwTree.moveTreeItem(1) is False
+
+ # Create a project
+ nwGUI.theProject.projTree.setSeed(42)
+ prjDir = os.path.join(fncDir, "project")
+ assert nwGUI.newProject({"projPath": prjDir}) is True
+
+ # Move Documents
+ # ==============
+
+ # Add some files
+ nwTree.setSelectedHandle("31489056e0916")
+ assert nwTree.newTreeItem(nwItemType.FILE) is True
+ assert nwTree.newTreeItem(nwItemType.FILE) is True
+ assert nwTree.newTreeItem(nwItemType.FILE) is True
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
+ "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
+ ]
+
+ # Move item without focus
+ monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
+ assert nwTree.moveTreeItem(1) is False
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
+ "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
+ ]
+ monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
+
+ # Move with no selections
+ nwTree.clearSelection()
+ assert nwTree.moveTreeItem(1) is False
+
+ # Move second item up twice (should give same result)
+ nwTree.setSelectedHandle("0e17daca5f3e1")
+ assert nwTree.moveTreeItem(-1) is True
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "0e17daca5f3e1", "98010bd9270f9",
+ "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
+ ]
+ assert nwTree.moveTreeItem(-1) is False
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "0e17daca5f3e1", "98010bd9270f9",
+ "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
+ ]
+
+ # Restore via menu entry
+ nwGUI.mainMenu.aMoveDown.activate(QAction.Trigger)
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
+ "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
+ ]
+
+ # Move fifth item down twice (should give same result)
+ nwTree.setSelectedHandle("031b4af5197ec")
+ assert nwTree.moveTreeItem(1) is True
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
+ "1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec",
+ ]
+ assert nwTree.moveTreeItem(1) is False
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
+ "1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec",
+ ]
+
+ # Restore via menu entry
+ nwGUI.mainMenu.aMoveUp.activate(QAction.Trigger)
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
+ "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
+ ]
+
+ # Move down again, and restore via undo
+ nwTree.setSelectedHandle("031b4af5197ec")
+ assert nwTree.moveTreeItem(1) is True
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
+ "1a6562590ef19", "41cfc0d1f2d12", "031b4af5197ec",
+ ]
+ nwGUI.mainMenu.aMoveUndo.activate(QAction.Trigger)
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
+ "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
+ ]
+
+ # Root Folder
+ # ===========
+
+ nwTree.setSelectedHandle("73475cb40a568")
+ assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0
+
+ # Move novel folder up
+ assert nwTree.moveTreeItem(-1) is False
+ nwTree.flushTreeOrder()
+ assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0
+
+ # Move novel folder down
+ assert nwTree.moveTreeItem(1) is True
+ nwTree.flushTreeOrder()
+ assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 1
+
+ # Move novel folder up again
+ assert nwTree.moveTreeItem(-1) is True
+ nwTree.flushTreeOrder()
+ assert nwGUI.theProject.projTree._treeOrder.index("73475cb40a568") == 0
+
+ # Clean up
+ # qtbot.stopForInteraction()
+ nwGUI.closeProject()
+
+# END Test testGuiProjTree_MoveItems
+
+
+@pytest.mark.gui
+def testGuiProjTree_DeleteItems(qtbot, caplog, monkeypatch, nwGUI, fncDir):
+ """Test adding and removing items from the project tree.
+ """
+ # Block message box
+ monkeypatch.setattr(QMessageBox, "warning", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "critical", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "question", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(QMessageBox, "information", lambda *a: QMessageBox.Yes)
+ monkeypatch.setattr(GuiMain, "editItem", lambda *a: None)
+
+ nwTree = nwGUI.treeView
+
+ # Try to run with no project
+ assert nwTree.emptyTrash() is False
+ assert nwTree.deleteItem() is False
+
+ # Create a project
+ nwGUI.theProject.projTree.setSeed(42)
+ prjDir = os.path.join(fncDir, "project")
+ assert nwGUI.newProject({"projPath": prjDir}) is True
+
+ # Try emptying the trash already now, when there is no trash folder
+ assert nwTree.emptyTrash() is False
+
+ # Add some files
+ nwTree.setSelectedHandle("31489056e0916")
+ assert nwTree.newTreeItem(nwItemType.FILE) is True
+ assert nwTree.newTreeItem(nwItemType.FILE) is True
+ assert nwTree.newTreeItem(nwItemType.FILE) is True
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
+ "1a6562590ef19", "031b4af5197ec", "41cfc0d1f2d12",
+ ]
+
+ # Delete File
+ # ===========
+
+ # Delete item without focus -> blocked
+ monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
+ nwTree.setSelectedHandle("41cfc0d1f2d12")
+ caplog.clear()
+ assert nwTree.deleteItem() is False
+ assert "blocked" in caplog.text
+ monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
+
+ # No selection made
+ nwTree.clearSelection()
+ caplog.clear()
+ assert nwTree.deleteItem() is False
+ assert "no item to delete" in caplog.text
+
+ # Not a valid handle
+ nwTree.clearSelection()
+ caplog.clear()
+ assert nwTree.deleteItem("0000000000000") is False
+ assert "Could not find tree item" in caplog.text
+
+ # Block adding trash folder
+ funcPointer = nwTree._addTrashRoot
+ nwTree._addTrashRoot = lambda *a: None
+ assert nwTree.deleteItem("41cfc0d1f2d12") is False
+ nwTree._addTrashRoot = funcPointer
+
+ # Delete last two documents, which also adds the trash folder
+ assert nwTree.deleteItem("41cfc0d1f2d12") is True
+ assert nwTree.deleteItem("031b4af5197ec") is True
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "98010bd9270f9", "0e17daca5f3e1",
+ "1a6562590ef19"
+ ]
+ trashHandle = nwGUI.theProject.projTree.trashRoot()
+ assert nwTree.getTreeFromHandle(trashHandle) == [
+ trashHandle, "41cfc0d1f2d12", "031b4af5197ec"
+ ]
+
+ # Delete the first file again (permanent), and ask for permission
+ # Also open the document in the editor, which should trigger a close
+ assert os.path.isfile(os.path.join(prjDir, "content", "41cfc0d1f2d12.nwd"))
+ assert "41cfc0d1f2d12" in nwGUI.theProject.projTree
+ assert nwGUI.docEditor.docHandle() is None
+ assert nwGUI.openDocument("41cfc0d1f2d12") is True
+ assert nwGUI.docEditor.docHandle() == "41cfc0d1f2d12"
+ assert nwTree.deleteItem("41cfc0d1f2d12") is True
+ assert nwGUI.docEditor.docHandle() is None
+ assert not os.path.isfile(os.path.join(prjDir, "content", "41cfc0d1f2d12.nwd"))
+ assert "41cfc0d1f2d12" not in nwGUI.theProject.projTree
+ assert nwTree.getTreeFromHandle(trashHandle) == [
+ trashHandle, "031b4af5197ec"
+ ]
+
+ # Try to delete the second document, but block the deletion
+ with monkeypatch.context() as mp:
+ mp.setattr("novelwriter.core.document.NWDoc.deleteDocument", lambda *a: False)
+ assert nwTree.deleteItem("031b4af5197ec") is False
+
+ # Delete proper, and skip asking for permission
+ assert os.path.isfile(os.path.join(prjDir, "content", "031b4af5197ec.nwd"))
+ assert "031b4af5197ec" in nwGUI.theProject.projTree
+ assert nwTree.deleteItem("031b4af5197ec", alreadyAsked=True) is True
+ assert not os.path.isfile(os.path.join(prjDir, "content", "031b4af5197ec.nwd"))
+ assert "031b4af5197ec" not in nwGUI.theProject.projTree
+ assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
+
+ # Delete Folder/Root
+ # ==================
+
+ # Deleting non-empty folders is blocked
+ assert nwTree.deleteItem("31489056e0916") is False # Folder
+ assert nwTree.deleteItem("73475cb40a568") is False # Root
+
+ # Add a folder we can delete
+ nwTree.setSelectedHandle("71ee45a3c0db9") # Character Root
+ assert nwTree.newTreeItem(nwItemType.FOLDER) is True
+ assert "2fca346db6561" in nwGUI.theProject.projTree
+
+ # Try to delete, but block parent item lookup
+ with monkeypatch.context() as mp:
+ mp.setattr("PyQt5.QtWidgets.QTreeWidgetItem.parent", lambda *a: None)
+ caplog.clear()
+ assert nwTree.deleteItem("2fca346db6561") is False
+ assert "Could not delete folder" in caplog.text
+ assert "2fca346db6561" in nwGUI.theProject.projTree
+
+ # Delete folder properly
+ assert nwTree.deleteItem("2fca346db6561") is True
+ assert "2fca346db6561" not in nwGUI.theProject.projTree
+
+ # Delete the Character root
+ assert nwTree.deleteItem("71ee45a3c0db9") is True
+ assert "71ee45a3c0db9" not in nwGUI.theProject.projTree
+
+ # Empty Trash
+ # ===========
+
+ # Try to empty trash that is already empty
+ caplog.clear()
+ assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
+ assert nwTree.emptyTrash() is False
+ assert "already empty" in caplog.text
+
+ # Move the two remaining scene documents to trash
+ assert nwTree.deleteItem("0e17daca5f3e1") is True
+ assert nwTree.deleteItem("1a6562590ef19") is True
+ assert nwTree.getTreeFromHandle("31489056e0916") == [
+ "31489056e0916", "98010bd9270f9"
+ ]
+ assert nwTree.getTreeFromHandle(trashHandle) == [
+ trashHandle, "0e17daca5f3e1", "1a6562590ef19"
+ ]
+
+ # Empty trash, but select no on question
+ with monkeypatch.context() as mp:
+ mp.setattr(QMessageBox, "question", lambda *a: QMessageBox.No)
+ assert nwTree.emptyTrash() is False
+
+ # Empty the trash proper
+ nwTree._setTreeChanged(False)
+ assert nwTree.emptyTrash() is True
+ assert nwTree.getTreeFromHandle(trashHandle) == [trashHandle]
+ assert nwTree._treeChanged is True
+
+ # Clean up
+ # qtbot.stopForInteraction()
+ nwGUI.closeProject()
+
+# END Test testGuiProjTree_DeleteItems
diff --git a/tests/test_gui/test_gui_statusbar.py b/tests/test_gui/test_gui_statusbar.py
index 9d7c4efc..66329820 100644
--- a/tests/test_gui/test_gui_statusbar.py
+++ b/tests/test_gui/test_gui_statusbar.py
@@ -25,7 +25,7 @@ import pytest
from PyQt5.QtWidgets import QMessageBox
from novelwriter.core import NWDoc
-from novelwriter.enum import nwItemClass, nwState
+from novelwriter.enum import nwState
@pytest.mark.gui
@@ -36,7 +36,7 @@ def testGuiStatusBar_Main(qtbot, monkeypatch, nwGUI, fncProj):
nwGUI.theProject.projTree.setSeed(42)
assert nwGUI.newProject({"projPath": fncProj}) is True
- cHandle = nwGUI.theProject.newFile("A Note", nwItemClass.CHARACTER, "71ee45a3c0db9")
+ cHandle = nwGUI.theProject.newFile("A Note", "71ee45a3c0db9")
newDoc = NWDoc(nwGUI.theProject, cHandle)
newDoc.writeDocument("# A Note\n\n")
nwGUI.treeView.revealNewTreeItem(cHandle)