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)