diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index 0f25f68c..5fdf7fcc 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -256,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 @@ -459,13 +462,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([ @@ -473,6 +469,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) @@ -486,13 +489,10 @@ 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) @@ -508,6 +508,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) @@ -993,7 +994,7 @@ class GuiProjectTree(QTreeWidget): """ if self.theProject.projTree.checkType(tHandle, nwItemType.FILE): nwItem = self.theProject.projTree[tHandle] - if nwItem.itemClass == nwItemClass.NOVEL: + if nwItem.itemClass in nwLists.CLS_NOVEL: self.novelItemChanged.emit() else: self.noteItemChanged.emit() 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/test_core/test_core_project.py b/tests/test_core/test_core_project.py index 3d5b37f4..b584679d 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -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 6cfa9cf0..3e8aeeca 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) @@ -151,8 +153,28 @@ 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.isRoot("a000000000002") + assert theTree.isTrashRoot("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) @@ -161,7 +183,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 @@ -251,12 +273,24 @@ def testCoreTree_Methods(mockGUI, mockItems): assert theTree.findRoot(nwItemClass.NOVEL) == "a000000000001" assert theTree.findRoot(nwItemClass.CHARACTER) == "a000000000004" + # 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") == [] assert theTree.getItemPath("c000000000001") == [ "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") == [ @@ -381,7 +415,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) @@ -395,39 +429,41 @@ def testCoreTree_XMLPackUnpack(mockGUI, 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'Chapter One' - b'Scene One' - b'Outtakes' - b'Trash' - 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 diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index 9d43481c..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,208 +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 nwTree.newTreeItem(nwItemType.FILE, None) is False - 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 - # Open a project - assert nwGUI.openProject(nwMinimal) is True + # 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 nwTree.newTreeItem(nwItemType.FILE, None) is False - assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False - assert nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is True + 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 - nwTree.clearSelection() - assert nwTree.newTreeItem(None, None) is False - assert nwTree.newTreeItem(nwItemType.ROOT, None) is False + # 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) is True - assert nwTree.newTreeItem(nwItemType.FOLDER, None) is True + # 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 more roots - assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True # Duplicate - assert nwTree.newTreeItem(nwItemType.ROOT, nwItemClass.CUSTOM) is True # Unique + # 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 nwTree.newTreeItem(nwItemType.FOLDER, None) is False + # 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 nwTree.moveTreeItem(1) is False - 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") == 12 - - ## - # 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 nwTree.emptyTrash() is False # No folder yet - assert nwTree.deleteItem(None) is False - assert nwTree.deleteItem("1111111111111") is False - assert nwTree.deleteItem("73475cb40a568") is True # New File - assert nwTree.deleteItem("71ee45a3c0db9") is True # New Folder - assert nwTree.deleteItem("811786ad1ae74") is True # 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() is True - assert nwTree.emptyTrash() is False # 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 nwTree.newTreeItem(nwItemType.FILE, None) is False - assert nwTree.newTreeItem(nwItemType.FOLDER, None) is False - - # 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 nwTree.newTreeItem(nwItemType.NO_TYPE, nwItemClass.NOVEL) is False - 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 nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is False - 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 nwTree.newTreeItem(nwItemType.FILE, nwItemClass.NOVEL) is False - assert nwTree.newTreeItem(nwItemType.FOLDER, nwItemClass.NOVEL) is False + # 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