diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index b9b1e5f9..b7332478 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -227,10 +227,10 @@ class NWIndex():
if theItem.itemParent is None:
logger.info("Not indexing orphaned item '%s'", tHandle)
return False
- if self.theProject.projTree.isTrashRoot(theItem.itemParent):
+ if theItem.itemClass == nwItemClass.TRASH:
logger.debug("Not indexing trash item '%s'", tHandle)
return False
- if self.theProject.projTree.getItemClass(tHandle) == nwItemClass.ARCHIVE:
+ if theItem.itemClass == nwItemClass.ARCHIVE:
logger.debug("Not indexing archived item '%s'", tHandle)
return False
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index 47397e5c..913e159a 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -53,7 +53,7 @@ logger = logging.getLogger(__name__)
class NWProject():
- FILE_VERSION = "1.4"
+ FILE_VERSION = "1.4" # The current project file format version
def __init__(self, theParent):
@@ -129,6 +129,7 @@ class NWProject():
newItem.setClass(rootClass)
newItem.setStatus(0)
self.projTree.append(None, None, newItem)
+ self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def newFolder(self, folderName, folderClass, pHandle):
@@ -140,6 +141,7 @@ class NWProject():
newItem.setClass(folderClass)
newItem.setStatus(0)
self.projTree.append(None, pHandle, newItem)
+ self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def newFile(self, fileName, fileClass, pHandle):
@@ -156,6 +158,7 @@ class NWProject():
newItem.setClass(fileClass)
newItem.setStatus(0)
self.projTree.append(None, pHandle, newItem)
+ self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
def trashFolder(self):
@@ -168,6 +171,7 @@ class NWProject():
newItem.setType(nwItemType.TRASH)
newItem.setClass(nwItemClass.TRASH)
self.projTree.append(None, None, newItem)
+ self.projTree.updateItemData(newItem.itemHandle)
return newItem.itemHandle
return trashHandle
@@ -605,10 +609,15 @@ 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._checkProjectTree()
self._loadProjectLocalisation()
self.updateWordCounts()
@@ -617,6 +626,7 @@ class NWProject():
self._writeLockFile()
self.setProjectChanged(False)
+ self.theParent.setStatus(self.tr("Opened Project: {0}").format(self.projName))
return True
@@ -1341,17 +1351,6 @@ class NWProject():
xEntry.text = aValue
return
- def _checkProjectTree(self):
- """Check the project tree and make sure all items have sensible
- values.
- """
- for tItem in self.projTree:
- tHandle = tItem.itemHandle
- logger.verbose("Checking item '%s'", tHandle)
- if tItem.itemRoot is None:
- self.projTree.updateItemRoot(tHandle)
- logger.warning("Corrected the root setting of item '%s'", tHandle)
-
def _scanProjectFolder(self):
"""Scan the project folder and check that the files in it are
also in the project XML file. If they aren't, import them as
@@ -1441,6 +1440,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 b146c5a1..ddef88b7 100644
--- a/novelwriter/core/tree.py
+++ b/novelwriter/core/tree.py
@@ -113,7 +113,6 @@ class NWTree():
self._projTree[tHandle] = nwItem
self._treeOrder.append(tHandle)
- self.updateItemRoot(tHandle)
self._setTreeChanged(True)
return True
@@ -208,9 +207,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(nwConst.MAX_DEPTH + 1):
+ if iItem.itemParent is None:
+ tItem.setRoot(iItem.itemHandle)
+ tItem.setClass(iItem.itemClass)
+ return True
+ else:
+ iItem = self.__getitem__(iItem.itemParent)
+ if iItem is None:
+ return False
+
+ return False
+
def checkType(self, tHandle, itemType):
"""Return true of item exists and is of the specified item type.
"""
@@ -219,70 +239,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 isRoot(self, tHandle):
- """Check if a handle is a root item.
- """
- return tHandle in self._treeRoots
-
- def updateItemRoot(self, tHandle):
- """Update the root item handle of a given item.
- """
- tItem = self.__getitem__(tHandle)
- iItem = tItem
- if iItem is not None:
- for _ in range(nwConst.MAX_DEPTH + 1):
- if iItem.itemParent is None:
- tItem.setRoot(iItem.itemHandle)
- return iItem.itemHandle
- else:
- tHandle = iItem.itemParent
- iItem = self.__getitem__(tHandle)
- return None
-
- def getItemClass(self, tHandle):
- """Return the class of a given item.
- """
- tItem = self.__getitem__(tHandle)
- if tItem is not None:
- if tItem.itemRoot in self._treeRoots:
- return self._treeRoots[tItem.itemRoot].itemClass
- return nwItemClass.NO_CLASS
-
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.
@@ -305,6 +261,49 @@ class NWTree():
tTree.append(tHandle)
return tTree
+ ##
+ # Tree Root Methods
+ ##
+
+ def isRoot(self, tHandle):
+ """Check if a handle is a root item.
+ """
+ return tHandle in self._treeRoots
+
+ def isTrashRoot(self, tHandle):
+ """Check if a handle is the trash folder.
+ """
+ if self._trashRoot is None:
+ return False
+ return tHandle == self._trashRoot
+
+ 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 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 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
##
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index a1cd9081..18b2b9f5 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -280,7 +280,7 @@ 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:
diff --git a/novelwriter/tools/build.py b/novelwriter/tools/build.py
index 0bb593a6..feb1662d 100644
--- a/novelwriter/tools/build.py
+++ b/novelwriter/tools/build.py
@@ -780,12 +780,13 @@ 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.ARCHIVE
isNone |= theItem.itemClass == nwItemClass.TRASH
isNone |= theItem.itemParent == self.theProject.projTree.trashRoot()
isNone |= theItem.itemParent is None
@@ -799,9 +800,6 @@ class GuiBuildNovel(QDialog):
if isNovel and not novelFiles:
return False
- if self.theProject.projTree.getItemClass(theItem.itemHandle) == nwItemClass.ARCHIVE:
- return False
-
return True
def _saveDocument(self, theFmt):
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 5e5d3eb9..8070cbf2 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -278,13 +278,14 @@ def testCoreIndex_ScanText(nwMinimal, mockGUI):
tHandle = theProject.trashFolder()
assert theProject.projTree[tHandle] is not None
xItem.setParent(tHandle)
+ theProject.projTree.updateItemData(xItem.itemHandle)
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)
- xItem.setRoot(aHandle)
+ theProject.projTree.updateItemData(xItem.itemHandle)
assert theIndex.scanText(xHandle, "Hello World!") is False
# Make some usable items
diff --git a/tests/test_core/test_core_tree.py b/tests/test_core/test_core_tree.py
index 6216a1b1..1e3e7d94 100644
--- a/tests/test_core/test_core_tree.py
+++ b/tests/test_core/test_core_tree.py
@@ -103,7 +103,7 @@ def mockItems(mockGUI):
("a000000000002", None, itemE),
("a000000000003", None, itemF),
("a000000000004", None, itemG),
- ("b000000000002", "a000000000002", itemH),
+ ("b000000000002", "a000000000004", itemH),
]
return theItems
@@ -120,22 +120,23 @@ 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")
+ assert theTree.isTrashRoot("a000000000003") is False
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)
@@ -151,6 +152,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
assert theTree.trashRoot() == "a000000000003"
assert theTree.archiveRoot() == "a000000000002"
assert theTree.isTrashRoot("a000000000003")
+ assert theTree.isRoot("a000000000002")
# Try to add another trash folder
itemT = NWItem(theProject)
@@ -169,14 +171,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
@@ -207,7 +210,7 @@ def testCoreTree_BuildTree(mockGUI, mockItems):
@pytest.mark.core
-def testCoreTree_Methods(mockGUI, mockItems):
+def testCoreTree_Methods(monkeypatch, mockGUI, mockItems):
"""Test various class methods.
"""
theProject = NWProject(mockGUI)
@@ -215,9 +218,27 @@ 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
+ with monkeypatch.context() as mp:
+ mp.setattr("novelwriter.constants.nwConst.MAX_DEPTH", 0)
+ assert theTree.updateItemData("b000000000001") is False
+
# Chech type
assert theTree.checkType("blabla", nwItemType.FILE) is False
assert theTree.checkType("b000000000001", nwItemType.FILE) is False
@@ -366,6 +387,7 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
for tHandle, pHandle, nwItem in mockItems:
theTree.append(tHandle, pHandle, nwItem)
+ theTree.updateItemData(tHandle)
assert len(theTree) == len(mockItems)
@@ -377,8 +399,8 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
b'- Novel
'
b'- '
- b'Act One
'
+ b'type="FOLDER" class="NOVEL">Act One'
+ b''
b'- Chapter One'
@@ -393,7 +415,7 @@ def testCoreTree_XMLPackUnpack(mockGUI, mockItems):
b'class="TRASH">Trash
'
b'- Characters
'
- b'- Jane Doe
'
b''
@@ -418,6 +440,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")