diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py
index c83e7907..df4d33c1 100644
--- a/novelwriter/core/itemmodel.py
+++ b/novelwriter/core/itemmodel.py
@@ -240,6 +240,14 @@ class ProjectNode:
self._refreshChildrenPos()
return
+ def setExpanded(self, state: bool) -> None:
+ """Set the node's expanded state."""
+ if state and self._children:
+ self._item.setExpanded(True)
+ else:
+ self._item.setExpanded(False)
+ return
+
##
# Internal Functions
##
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index aa3ea021..d9ca56a6 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -101,22 +101,22 @@ class GuiProjectView(QWidget):
self.keyGoPrev = QShortcut(self.projTree)
self.keyGoPrev.setKey("Alt+Up")
self.keyGoPrev.setContext(Qt.ShortcutContext.WidgetShortcut)
- self.keyGoPrev.activated.connect(self.projTree.moveSiblingUp)
+ self.keyGoPrev.activated.connect(self.projTree.goToSiblingUp)
self.keyGoNext = QShortcut(self.projTree)
self.keyGoNext.setKey("Alt+Down")
self.keyGoNext.setContext(Qt.ShortcutContext.WidgetShortcut)
- self.keyGoNext.activated.connect(self.projTree.moveSiblingDown)
+ self.keyGoNext.activated.connect(self.projTree.goToSiblingDown)
self.keyGoUp = QShortcut(self.projTree)
self.keyGoUp.setKey("Alt+Left")
self.keyGoUp.setContext(Qt.ShortcutContext.WidgetShortcut)
- self.keyGoUp.activated.connect(self.projTree.moveToParent)
+ self.keyGoUp.activated.connect(self.projTree.goToParent)
self.keyGoDown = QShortcut(self.projTree)
self.keyGoDown.setKey("Alt+Right")
self.keyGoDown.setContext(Qt.ShortcutContext.WidgetShortcut)
- self.keyGoDown.activated.connect(self.projTree.moveToFirstChild)
+ self.keyGoDown.activated.connect(self.projTree.goToFirstChild)
self.keyContext = QShortcut(self.projTree)
self.keyContext.setKey("Ctrl+.")
@@ -415,8 +415,6 @@ class GuiProjectToolBar(QWidget):
self.mTemplates.addUpdate(tHandle, item.itemName, item.getMainIcon())
elif tHandle in self.mTemplates:
self.mTemplates.remove(tHandle)
- elif change == nwChange.DELETE and tHandle in self.mTemplates:
- self.mTemplates.remove(tHandle)
return
##
@@ -680,7 +678,8 @@ class GuiProjectTree(QTreeView):
tHandle = SHARED.project.newFolder(newLabel, sHandle, pos)
# Select the new item automatically
- self.setSelectedHandle(tHandle)
+ if tHandle:
+ self.setSelectedHandle(tHandle)
return
@@ -793,6 +792,7 @@ class GuiProjectTree(QTreeView):
dHandles = docDup.duplicate(itemTree)
if len(dHandles) != len(itemTree):
SHARED.warn(self.tr("Could not duplicate all items."))
+ self.restoreExpandedState()
return
##
@@ -807,7 +807,7 @@ class GuiProjectTree(QTreeView):
super().mousePressEvent(event)
if event.button() == QtMouseLeft:
if not self.indexAt(event.pos()).isValid():
- self.selectionModel().clearCurrentIndex()
+ self._clearSelection()
elif event.button() == QtMouseMiddle:
if (node := self._getNode(self.indexAt(event.pos()))) and node.item.isFileType():
self.projView.openDocumentRequest.emit(
@@ -841,7 +841,7 @@ class GuiProjectTree(QTreeView):
return
@pyqtSlot()
- def moveSiblingUp(self) -> None:
+ def goToSiblingUp(self) -> None:
"""Skip to the previous sibling."""
if (node := self._getNode(self.currentIndex())) and (parent := node.parent()):
if (move := parent.child(node.row() - 1)) and (model := self._getModel()):
@@ -849,7 +849,7 @@ class GuiProjectTree(QTreeView):
return
@pyqtSlot()
- def moveSiblingDown(self) -> None:
+ def goToSiblingDown(self) -> None:
"""Skip to the next sibling."""
if (node := self._getNode(self.currentIndex())) and (parent := node.parent()):
if (move := parent.child(node.row() + 1)) and (model := self._getModel()):
@@ -857,7 +857,7 @@ class GuiProjectTree(QTreeView):
return
@pyqtSlot()
- def moveToParent(self) -> None:
+ def goToParent(self) -> None:
"""Move to parent item."""
if (
(model := self._getModel())
@@ -868,7 +868,7 @@ class GuiProjectTree(QTreeView):
return
@pyqtSlot()
- def moveToFirstChild(self) -> None:
+ def goToFirstChild(self) -> None:
"""Move to first child item."""
if (
(model := self._getModel())
@@ -893,7 +893,9 @@ class GuiProjectTree(QTreeView):
return
@pyqtSlot()
- def processDeleteRequest(self, handles: list[str] = [], askFirst: bool = True) -> None:
+ def processDeleteRequest(
+ self, handles: list[str] | None = None, askFirst: bool = True
+ ) -> None:
"""Move selected items to Trash."""
if handles and (model := self._getModel()):
indices = [model.indexFromHandle(handle) for handle in handles]
@@ -1002,20 +1004,26 @@ class GuiProjectTree(QTreeView):
def _onNodeCollapsed(self, index: QModelIndex) -> None:
"""Capture a node collapse, and pass it to the model."""
if node := self._getNode(index):
- node.item.setExpanded(False)
+ node.setExpanded(False)
return
@pyqtSlot(QModelIndex)
def _onNodeExpanded(self, index: QModelIndex) -> None:
"""Capture a node expand, and pass it to the model."""
if node := self._getNode(index):
- node.item.setExpanded(True)
+ node.setExpanded(True)
return
##
# Internal Functions
##
+ def _clearSelection(self) -> None:
+ """Clear the currently selected items."""
+ self.clearSelection()
+ self.selectionModel().clearCurrentIndex()
+ return
+
def _selectedRows(self) -> list[QModelIndex]:
"""Return all column 0 indexes."""
return [i for i in self.selectedIndexes() if i.column() == 0]
@@ -1162,9 +1170,8 @@ class _TreeContextMenu(QMenu):
# Process Item
if self._children:
self._expandCollapse()
- if isFile:
- action = self.addAction(self.tr("Duplicate"))
- action.triggered.connect(qtLambda(self._tree.duplicateFromHandle, self._handle))
+ action = self.addAction(self.tr("Duplicate"))
+ action.triggered.connect(qtLambda(self._tree.duplicateFromHandle, self._handle))
self._deleteOrTrash()
return
@@ -1321,9 +1328,10 @@ class _TreeContextMenu(QMenu):
def _deleteOrTrash(self) -> None:
"""Add move to Trash action."""
- if self._model.trashSelection(self._indices):
- text = self.tr("Delete Permanently")
- elif len(self._indices) == 1 and self._item.isRootType():
+ if (
+ self._model.trashSelection(self._indices)
+ or len(self._indices) == 1 and self._item.isRootType()
+ ):
text = self.tr("Delete Permanently")
else:
text = self.tr("Move to Trash")
diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py
index bca7cbaa..e5221cd2 100644
--- a/novelwriter/tools/manussettings.py
+++ b/novelwriter/tools/manussettings.py
@@ -419,8 +419,7 @@ class _FilterTab(NFixedPage):
logger.debug("Building project tree")
self._treeMap = {}
self.optTree.clear()
- for node in SHARED.project.tree.model.root.allChildren():
- nwItem = node.item
+ for nwItem in SHARED.project.tree:
tHandle = nwItem.itemHandle
pHandle = nwItem.itemParent
rHandle = nwItem.itemRoot
diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx
index 3b05335b..d3051769 100644
--- a/tests/reference/guiEditor_Main_Final_nwProject.nwx
+++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx
@@ -34,7 +34,7 @@
Novel
-
-
+
Title Page
-
@@ -42,39 +42,39 @@
New Folder
-
-
+
New Chapter
-
-
+
New Scene
-
-
+
Plot
-
-
+
New Note
-
-
+
Characters
-
-
+
New Note
-
-
+
Locations
-
-
+
New Note
-
-
+
Trash
diff --git a/tests/test_core/test_core_itemmodel.py b/tests/test_core/test_core_itemmodel.py
index 5d26d2ea..f63afa35 100644
--- a/tests/test_core/test_core_itemmodel.py
+++ b/tests/test_core/test_core_itemmodel.py
@@ -116,6 +116,16 @@ def testCoreItemModel_ProjectNode_Children(mockGUI, mockRnd, fncPath):
assert child010.parent() is child01
assert child011.parent() is child01
+ # Expand
+ child0.setExpanded(True)
+ child1.setExpanded(True)
+ child2.setExpanded(True)
+ child3.setExpanded(True)
+ assert child0.item.isExpanded is True # Only one with children
+ assert child1.item.isExpanded is False
+ assert child2.item.isExpanded is False
+ assert child3.item.isExpanded is False
+
@pytest.mark.core
def testCoreItemModel_ProjectNode_Modify(mockGUI, mockRnd, fncPath):
diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py
index 2c572383..3d5ede31 100644
--- a/tests/test_gui/test_gui_projtree.py
+++ b/tests/test_gui/test_gui_projtree.py
@@ -20,24 +20,20 @@ along with this program. If not, see .
"""
from __future__ import annotations
-from pathlib import Path
from unittest.mock import MagicMock
import pytest
-from PyQt5.QtCore import QEvent, QMimeData, QPoint, Qt, QTimer
-from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QMouseEvent
-from PyQt5.QtWidgets import QMenu, QMessageBox, QTreeWidget, QTreeWidgetItem
+from PyQt5.QtCore import QEvent, QItemSelectionModel, QModelIndex, QPoint
+from PyQt5.QtGui import QMouseEvent
+from PyQt5.QtWidgets import QMenu, QMessageBox
from novelwriter import CONFIG, SHARED
-from novelwriter.core.item import NWItem
-from novelwriter.core.project import NWProject
from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel
-from novelwriter.enum import nwFocus, nwItemClass, nwItemLayout, nwItemType
-from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView, _TreeContextMenu
-from novelwriter.guimain import GuiMain
+from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType
+from novelwriter.gui.projtree import _TreeContextMenu
from novelwriter.types import (
QtAccepted, QtModNone, QtMouseLeft, QtMouseMiddle, QtRejected,
QtScrollAlwaysOff, QtScrollAsNeeded
@@ -48,122 +44,264 @@ from tests.tools import C, buildTestProject
@pytest.mark.gui
-@pytest.mark.skip
-def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
- """Test adding and removing items from the project tree."""
+def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
+ """Test adding items to the project tree."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
projView = nwGUI.projView
- projTree = nwGUI.projView.projTree
+ projTree = projView.projTree
project = SHARED.project
+ tree = project.tree
# Try to add item with no project
- assert projTree.newTreeItem(nwItemType.FILE) is False
+ projTree.newTreeItem(nwItemType.FILE)
+ assert len(tree) == 0
# Create a project
buildTestProject(nwGUI, projPath)
-
- # No itemType set
- projTree.clearSelection()
- assert projTree.newTreeItem(None) is False
+ trash = tree.trash
+ assert trash is not None
+ assert len(tree) == 9
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
# Root Items
# ==========
# No class set
- assert projTree.newTreeItem(nwItemType.ROOT) is False
+ projTree.newTreeItem(nwItemType.ROOT)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
- # Create root item
- assert projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True
- assert "0000000000010" in project.tree
+ # Create Objects root item after Locations
+ hObjectRoot = "0000000000011"
+ projView.setSelectedHandle(C.hWorldRoot)
+ projTree.newTreeItem(nwItemType.ROOT, nwItemClass.OBJECT)
+ assert hObjectRoot in tree
+ item = tree[hObjectRoot]
+ assert item is not None
+ assert item.itemName == "Objects"
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Objects", "Trash",
+ ]
# File/Folder Items
# =================
# No location selected for new item
- projTree.clearSelection()
+ projTree._clearSelection()
+
caplog.clear()
- assert projTree.newTreeItem(nwItemType.FILE) is False
- assert projTree.newTreeItem(nwItemType.FOLDER) is False
+ projTree.newTreeItem(nwItemType.FILE)
assert "Did not find anywhere" in caplog.text
+ caplog.clear()
+ projTree.newTreeItem(nwItemType.FOLDER)
+ assert "Did not find anywhere" in caplog.text
+
+ # Try to add them to Trash
+ projView.setSelectedHandle(trash.item.itemHandle)
+
+ caplog.clear()
+ projTree.newTreeItem(nwItemType.FILE)
+ assert "Cannot add new files or folders" in caplog.text
+
+ caplog.clear()
+ projTree.newTreeItem(nwItemType.FOLDER)
+ assert "Cannot add new files or folders" in caplog.text
+
# Create new folder as child of Novel folder
- projView.setSelectedHandle(C.hNovelRoot)
- assert projTree.newTreeItem(nwItemType.FOLDER) is True
- assert project.tree["0000000000011"].itemParent == C.hNovelRoot # type: ignore
- assert project.tree["0000000000011"].itemRoot == C.hNovelRoot # type: ignore
- assert project.tree["0000000000011"].itemClass == nwItemClass.NOVEL # type: ignore
+ hNewFolder = "0000000000012"
+ projView.setSelectedHandle(C.hNovelRoot, doScroll=True)
+ projTree.newTreeItem(nwItemType.FOLDER)
+ assert hNewFolder in tree
+ item = tree[hNewFolder]
+ assert item is not None
+ assert item.itemName == "New Folder"
+ assert item.itemParent == C.hNovelRoot
+ assert item.itemRoot == C.hNovelRoot
+ assert item.itemClass == nwItemClass.NOVEL
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "New Folder", "Plot", "Characters", "Locations", "Objects", "Trash",
+ ]
# Add a new file in the new folder
- projView.setSelectedHandle("0000000000011")
- assert projTree.newTreeItem(nwItemType.FILE) is True
- assert project.tree["0000000000012"].itemParent == "0000000000011" # type: ignore
- assert project.tree["0000000000012"].itemRoot == C.hNovelRoot # type: ignore
- assert project.tree["0000000000012"].itemClass == nwItemClass.NOVEL # type: ignore
+ hNewFile = "0000000000013"
+ projView.setSelectedHandle(hNewFolder, doScroll=True)
+ projTree.newTreeItem(nwItemType.FILE)
+ assert hNewFile in tree
+ item = tree[hNewFile]
+ assert item is not None
+ assert item.itemName == "New Document"
+ assert item.itemParent == hNewFolder
+ assert item.itemRoot == C.hNovelRoot
+ assert item.itemClass == nwItemClass.NOVEL
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "New Folder", "New Document", "Plot", "Characters", "Locations",
+ "Objects", "Trash",
+ ]
# Add a new chapter next to the other new file
- projView.setSelectedHandle("0000000000012")
- assert projTree.newTreeItem(nwItemType.FILE, hLevel=2) is True
- assert project.tree["0000000000013"].itemParent == "0000000000011" # type: ignore
- assert project.tree["0000000000013"].itemRoot == C.hNovelRoot # type: ignore
- assert project.tree["0000000000013"].itemClass == nwItemClass.NOVEL # type: ignore
- assert nwGUI.openDocument("0000000000013")
+ hNewChapter = "0000000000014"
+ projView.setSelectedHandle(hNewFile, doScroll=True)
+ projTree.newTreeItem(nwItemType.FILE, hLevel=2)
+ assert hNewChapter in tree
+ item = tree[hNewChapter]
+ assert item is not None
+ assert item.itemName == "New Chapter"
+ assert item.itemParent == hNewFolder
+ assert item.itemRoot == C.hNovelRoot
+ assert item.itemClass == nwItemClass.NOVEL
+ assert nwGUI.openDocument(hNewChapter)
assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "New Folder", "New Document", "New Chapter", "Plot", "Characters",
+ "Locations", "Objects", "Trash",
+ ]
# Add a new scene next to the other new file
- projView.setSelectedHandle("0000000000012")
- assert projTree.newTreeItem(nwItemType.FILE, hLevel=3) is True
- assert project.tree["0000000000014"].itemParent == "0000000000011" # type: ignore
- assert project.tree["0000000000014"].itemRoot == C.hNovelRoot # type: ignore
- assert project.tree["0000000000014"].itemClass == nwItemClass.NOVEL # type: ignore
- assert nwGUI.openDocument("0000000000014")
+ hNewScene = "0000000000015"
+ projView.setSelectedHandle(hNewChapter, doScroll=True)
+ projTree.newTreeItem(nwItemType.FILE, hLevel=3)
+ assert hNewScene in tree
+ item = tree[hNewScene]
+ assert item is not None
+ assert item.itemName == "New Scene"
+ assert item.itemParent == hNewFolder
+ assert item.itemRoot == C.hNovelRoot
+ assert item.itemClass == nwItemClass.NOVEL
+ assert nwGUI.openDocument(hNewScene)
assert nwGUI.docEditor.getText() == "### New Scene\n\n"
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "New Folder", "New Document", "New Chapter", "New Scene", "Plot",
+ "Characters", "Locations", "Objects", "Trash",
+ ]
# Add a new scene with the content copied from the previous
- assert nwGUI.openDocument("0000000000014")
+ nwGUI.openDocument(hNewScene)
nwGUI.docEditor.setPlainText("### New Scene\n\nWith Stuff\n\n")
nwGUI.saveDocument()
- projView.setSelectedHandle("0000000000014")
- assert projTree.newTreeItem(nwItemType.FILE, copyDoc="0000000000014") is True
- assert project.tree["0000000000015"].itemParent == "0000000000011" # type: ignore
- assert project.tree["0000000000015"].itemRoot == C.hNovelRoot # type: ignore
- assert project.tree["0000000000015"].itemClass == nwItemClass.NOVEL # type: ignore
- assert nwGUI.openDocument("0000000000015")
+
+ hNewSceneCopy = "0000000000016"
+ projView.setSelectedHandle(hNewScene, doScroll=True)
+ projTree.newTreeItem(nwItemType.FILE, copyDoc=hNewScene)
+ assert hNewSceneCopy in tree
+ item = tree[hNewSceneCopy]
+ assert item is not None
+ assert item.itemName == "New Scene"
+ assert item.itemParent == hNewFolder
+ assert item.itemRoot == C.hNovelRoot
+ assert item.itemClass == nwItemClass.NOVEL
+ assert nwGUI.openDocument(hNewSceneCopy)
assert nwGUI.docEditor.getText() == "### New Scene\n\nWith Stuff\n\n"
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
+ "Plot", "Characters", "Locations", "Objects", "Trash",
+ ]
# Add a new file to the characters folder
- projView.setSelectedHandle(C.hCharRoot)
- assert projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True
- assert project.tree["0000000000016"].itemParent == C.hCharRoot # type: ignore
- assert project.tree["0000000000016"].itemRoot == C.hCharRoot # type: ignore
- assert project.tree["0000000000016"].itemClass == nwItemClass.CHARACTER # type: ignore
- assert nwGUI.openDocument("0000000000016")
+ hNewCharacter = "0000000000017"
+ projView.setSelectedHandle(C.hCharRoot, doScroll=True)
+ projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
+ assert hNewCharacter in tree
+ item = tree[hNewCharacter]
+ assert item is not None
+ assert item.itemName == "New Note"
+ assert item.itemParent == C.hCharRoot
+ assert item.itemRoot == C.hCharRoot
+ assert item.itemClass == nwItemClass.CHARACTER
+ assert nwGUI.openDocument(hNewCharacter)
assert nwGUI.docEditor.getText() == "# New Note\n\n"
-
- # Make sure the sibling folder bug trap works
- projView.setSelectedHandle("0000000000013")
- project.tree["0000000000013"].setParent(None) # This should not happen # type: ignore
- caplog.clear()
- assert projTree.newTreeItem(nwItemType.FILE) is False
- assert "Internal error" in caplog.text
- project.tree["0000000000013"].setParent("0000000000011") # type: ignore
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
+ "Plot", "Characters", "New Note", "Locations", "Objects", "Trash",
+ ]
# Cancel during creation
with monkeypatch.context() as mp:
mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("", False))
- projView.setSelectedHandle("0000000000013")
- assert projTree.newTreeItem(nwItemType.FILE) is False
+ projView.setSelectedHandle(hNewFile, doScroll=True)
+ projTree.newTreeItem(nwItemType.FILE)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
+ "Plot", "Characters", "New Note", "Locations", "Objects", "Trash",
+ ]
- # Get the trash folder
- with monkeypatch.context() as mp:
- mp.setattr(NWProject, "trashFolder", lambda *a: None)
- assert projTree._addTrashRoot() is None
+ # From Template
+ # =============
- assert isinstance(projTree._addTrashRoot(), QTreeWidgetItem)
- trashHandle = project.trashFolder()
- projView.setSelectedHandle(trashHandle)
- assert projTree.newTreeItem(nwItemType.FILE) is False
- assert "Cannot add new files or folders to the Trash folder" in caplog.text
+ # Create template folder
+ hTemplateRoot = "0000000000018"
+ projView.setSelectedHandle(hObjectRoot)
+ projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE)
+ assert hTemplateRoot in tree
+ item = tree[hTemplateRoot]
+ assert item is not None
+ assert item.itemName == "Templates"
+ assert item.itemParent is None
+ assert item.itemRoot == hTemplateRoot
+ assert item.itemClass == nwItemClass.TEMPLATE
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
+ "Plot", "Characters", "New Note", "Locations", "Objects", "Templates",
+ "Trash",
+ ]
+
+ # Create scene template
+ hSceneTemplate = "0000000000019"
+ projView.setSelectedHandle(hTemplateRoot, doScroll=True)
+ projTree.newTreeItem(nwItemType.FILE, hLevel=3)
+ assert hSceneTemplate in tree
+ item = tree[hSceneTemplate]
+ assert item is not None
+ assert item.itemName == "New Scene"
+ assert item.itemParent == hTemplateRoot
+ assert item.itemRoot == hTemplateRoot
+ assert item.itemClass == nwItemClass.TEMPLATE
+
+ item.setName("New Scene Template")
+ nwGUI.openDocument(hSceneTemplate)
+ nwGUI.docEditor.setPlainText("### New Scene Template\n\nWith Stuff\n\n")
+ nwGUI.saveDocument()
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
+ "Plot", "Characters", "New Note", "Locations", "Objects", "Templates",
+ "New Scene Template", "Trash",
+ ]
+
+ # Create from template
+ hNewFromTemplate = "000000000001a"
+ projView.setSelectedHandle(hNewSceneCopy, doScroll=True)
+ projView.createFileFromTemplate(hSceneTemplate)
+ assert hNewFromTemplate in tree
+ item = tree[hNewFromTemplate]
+ assert item is not None
+ assert item.itemName == "New Scene Template"
+ assert item.itemParent == hNewFolder
+ assert item.itemRoot == C.hNovelRoot
+ assert item.itemClass == nwItemClass.NOVEL
+ assert nwGUI.openDocument(hNewFromTemplate)
+ assert nwGUI.docEditor.getText() == "### New Scene Template\n\nWith Stuff\n\n"
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
+ "New Scene Template", "Plot", "Characters", "New Note", "Locations",
+ "Objects", "Templates", "New Scene Template", "Trash",
+ ]
# Rename Item
# ===========
@@ -171,25 +309,11 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn
# Rename plot folder
with monkeypatch.context() as mp:
mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("Stuff", True))
- projTree.renameTreeItem(C.hPlotRoot)
- assert project.tree[C.hPlotRoot].itemName == "Stuff" # type: ignore
-
- # Other Checks
- # ============
-
- # Also check error handling in reveal function
- assert projTree.revealNewTreeItem("abc") is False
-
- # Add an item that cannot be displayed in the tree
- nHandle = project.newFile("Test", None) # type: ignore
- assert projTree.revealNewTreeItem(nHandle) is False
-
- # Adding an invalid item directly to the tree should also fail
- assert projTree._addTreeItem(None) is None
-
- # Setting values for a non-existing tree item should be handled
- projTree.setTreeItemValues(None)
- projTree.setTreeItemValues(C.hInvalid) # The function used to take handles
+ projView.setSelectedHandle(C.hPlotRoot, doScroll=True)
+ projView.renameTreeItem()
+ item = tree[C.hPlotRoot]
+ assert item is not None
+ assert item.itemName == "Stuff"
# Clean up
# qtbot.stop()
@@ -197,343 +321,373 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn
@pytest.mark.gui
-@pytest.mark.skip
-def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
- """Test adding and removing items from the project tree."""
+def testGuiProjTree_SimpleOperations(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
+ """Test simple operations in the project tree like internal move,
+ change selection and expand/collapse nodes.
+ """
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
projView = nwGUI.projView
- projTree = nwGUI.projView.projTree
+ projTree = projView.projTree
+ project = SHARED.project
+ tree = project.tree
+ model = tree.model
- # Try to move item with no project
- assert projTree.moveTreeItem(1) is False
+ # The default model is empty
+ assert projView.getSelectedHandle() is None
+ assert projTree._getModel() is None
+ assert projTree._selectedRows() == []
+ assert projTree._getNode(QModelIndex()) is None
# Create a project
buildTestProject(nwGUI, projPath)
-
- # Move Documents
- # ==============
-
- # Add some files
- projView.setSelectedHandle(C.hChapterDir)
- assert projTree.newTreeItem(nwItemType.FILE) is True
- assert projTree.newTreeItem(nwItemType.FILE) is True
- assert projTree.newTreeItem(nwItemType.FILE) is True
- assert projTree.getTreeFromHandle(C.hChapterDir) == [
- C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
- "0000000000010", "0000000000011", "0000000000012",
+ trash = tree.trash
+ assert trash is not None
+ assert len(tree) == 9
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
]
- # Move with no selections
- projTree.clearSelection()
- assert projTree.moveTreeItem(1) is False
+ # Add some scenes
+ hFolder = "0000000000011"
+ hScenes = ["0000000000012", "0000000000013", "0000000000014", "0000000000015"]
+ projView.setSelectedHandle(C.hNovelRoot, doScroll=True)
+ projTree.newTreeItem(nwItemType.FOLDER)
+ assert hFolder in tree
+ item = tree[hFolder]
+ assert item is not None
+ item.setName("Chapter Folder")
- # Move second item up twice (should give same result)
- projView.setSelectedHandle(C.hSceneDoc)
- assert projTree.moveTreeItem(-1) is True
- assert projTree.getTreeFromHandle(C.hChapterDir) == [
- C.hChapterDir, C.hSceneDoc, C.hChapterDoc,
- "0000000000010", "0000000000011", "0000000000012",
- ]
- assert projTree.moveTreeItem(-1) is False
- assert projTree.getTreeFromHandle(C.hChapterDir) == [
- C.hChapterDir, C.hSceneDoc, C.hChapterDoc,
- "0000000000010", "0000000000011", "0000000000012",
+ projView.setSelectedHandle(hFolder, doScroll=True)
+ for n, hScene in enumerate(hScenes):
+ projTree.newTreeItem(nwItemType.FILE, hLevel=3)
+ assert hScene in tree
+ item = tree[hScene]
+ assert item is not None
+ item.setName(f"Scene {n+1}")
+
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
+ "Plot", "Characters", "Locations", "Trash",
]
- # Restore
- assert projTree.moveTreeItem(1) is True
- assert projTree.getTreeFromHandle(C.hChapterDir) == [
- C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
- "0000000000010", "0000000000011", "0000000000012",
+ # Expansion
+ # =========
+
+ # Default State
+ assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [
+ C.hNovelRoot, hFolder
]
- # Move fifth item down twice (should give same result)
- projView.setSelectedHandle("0000000000011")
- assert projTree.moveTreeItem(1) is True
- assert projTree.getTreeFromHandle(C.hChapterDir) == [
- C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
- "0000000000010", "0000000000012", "0000000000011",
- ]
- assert projTree.moveTreeItem(1) is False
- assert projTree.getTreeFromHandle(C.hChapterDir) == [
- C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
- "0000000000010", "0000000000012", "0000000000011",
+ # Expand Novel
+ projTree.expandFromIndex(model.indexFromHandle(C.hNovelRoot))
+ assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [
+ C.hNovelRoot, C.hChapterDir, hFolder
]
- # Restore
- assert projTree.moveTreeItem(-1) is True
- assert projTree.getTreeFromHandle(C.hChapterDir) == [
- C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
- "0000000000010", "0000000000011", "0000000000012",
+ # Collapse Novel
+ projTree.collapseFromIndex(model.indexFromHandle(C.hNovelRoot))
+ assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [
+ C.hNovelRoot
]
- # Move down again
- projView.setSelectedHandle("0000000000011")
- assert projTree.moveTreeItem(1) is True
- assert projTree.getTreeFromHandle(C.hChapterDir) == [
- C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
- "0000000000010", "0000000000012", "0000000000011",
+ # Move Up
+ # =======
+
+ # Already first
+ projView.setSelectedHandle(hScenes[0], doScroll=True)
+ projTree.moveItemUp()
+ assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [
+ "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
]
- # Root Folder
- # ===========
+ # Proper move
+ projView.setSelectedHandle(hScenes[1], doScroll=True)
+ projTree.moveItemUp()
+ assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [
+ "Chapter Folder", "Scene 2", "Scene 1", "Scene 3", "Scene 4",
+ ]
- projView.setSelectedHandle(C.hNovelRoot)
- assert SHARED.project.tree._order.index(C.hNovelRoot) == 0
+ # Move Down
+ # =========
- # Move novel folder up
- assert projTree.moveTreeItem(-1) is False
- assert SHARED.project.tree._order.index(C.hNovelRoot) == 0
+ # Already last
+ projView.setSelectedHandle(hScenes[3], doScroll=True)
+ projTree.moveItemDown()
+ assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [
+ "Chapter Folder", "Scene 2", "Scene 1", "Scene 3", "Scene 4",
+ ]
- # Move novel folder down
- assert projTree.moveTreeItem(1) is True
- assert SHARED.project.tree._order.index(C.hNovelRoot) == 1
+ # Proper move
+ projView.setSelectedHandle(hScenes[1], doScroll=True)
+ projTree.moveItemDown()
+ assert [n.item.itemName for n in tree.model.root.allChildren()][5:10] == [
+ "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
+ ]
- # Move novel folder up again
- assert projTree.moveTreeItem(-1) is True
- assert SHARED.project.tree._order.index(C.hNovelRoot) == 0
+ # Select Up
+ # =========
- # Clean up
- # qtbot.stop()
- nwGUI.closeProject()
+ # Sibling
+ projView.setSelectedHandle(hFolder, doScroll=True)
+ assert projView.getSelectedHandle() == hFolder
+ projTree.goToSiblingUp()
+ assert projView.getSelectedHandle() == C.hChapterDir
+ projTree.goToSiblingUp()
+ assert projView.getSelectedHandle() == C.hTitlePage
+ projTree.goToSiblingUp()
+ assert projView.getSelectedHandle() == C.hTitlePage
+
+ # Parent
+ projTree.goToParent()
+ assert projView.getSelectedHandle() == C.hNovelRoot
+ projTree.goToParent()
+ assert projView.getSelectedHandle() == C.hNovelRoot
+
+ # Select Down
+ # =========
+
+ # Child
+ projView.setSelectedHandle(C.hNovelRoot, doScroll=True)
+ assert projView.getSelectedHandle() == C.hNovelRoot
+ projTree.goToFirstChild()
+ assert projView.getSelectedHandle() == C.hTitlePage
+
+ # Sibling
+ projTree.goToSiblingDown()
+ assert projView.getSelectedHandle() == C.hChapterDir
+ projTree.goToSiblingDown()
+ assert projView.getSelectedHandle() == hFolder
+ projTree.goToSiblingDown()
+ assert projView.getSelectedHandle() == hFolder
@pytest.mark.gui
-@pytest.mark.skip
-def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
- """Test external requests for removing items from project tree."""
+def testGuiProjTree_MouseClicks(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
+ """Test mouse clicks in the project tree."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
projView = nwGUI.projView
- projTree = nwGUI.projView.projTree
+ projTree = projView.projTree
+ project = SHARED.project
+ tree = project.tree
+ model = tree.model
- # Try to run with no project
- assert projView.requestDeleteItem() is False
+ # The default model is empty
+ assert projView.getSelectedHandle() is None
+ assert projTree._getModel() is None
+ assert projTree._selectedRows() == []
+ assert projTree._getNode(QModelIndex()) is None
# Create a project
buildTestProject(nwGUI, projPath)
+ trash = tree.trash
+ assert trash is not None
+ assert len(tree) == 9
- # Try emptying the trash already now, when there is no trash folder
- assert projView.emptyTrash() is False
+ # Single click emits a signal
+ with qtbot.waitSignal(projView.selectedItemChanged) as signal:
+ projTree._onSingleClick(model.indexFromHandle(C.hNovelRoot))
+ assert signal.args[0] == C.hNovelRoot
- # Add some files
- projView.setSelectedHandle(C.hChapterDir)
- assert projTree.newTreeItem(nwItemType.FILE) is True
- assert projTree.newTreeItem(nwItemType.FILE) is True
- assert projTree.newTreeItem(nwItemType.FILE) is True
- assert projTree.getTreeFromHandle(C.hChapterDir) == [
- C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
- "0000000000010", "0000000000011", "0000000000012",
+ # Double click on folder expands/collapses it
+ assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == []
+ projTree._onDoubleClick(model.indexFromHandle(C.hNovelRoot))
+ assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == [
+ C.hNovelRoot
]
+ projTree._onDoubleClick(model.indexFromHandle(C.hNovelRoot))
+ assert [n.item.itemHandle for n in [model.node(i) for i in model.allExpanded()] if n] == []
- # Delete item without focus -> blocked
- monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: False)
- projView.setSelectedHandle("0000000000012")
- assert projView.requestDeleteItem() is False
- monkeypatch.setattr(GuiProjectTree, "hasFocus", lambda *a: True)
+ # Double click on file opens it
+ with qtbot.waitSignal(projView.openDocumentRequest) as signal:
+ projTree._onDoubleClick(model.indexFromHandle(C.hChapterDoc))
+ assert signal.args[0] == C.hChapterDoc
+ assert signal.args[1] == nwDocMode.EDIT
+ assert signal.args[2] == ""
+ assert signal.args[3] is True
- # No selection made
- projTree.clearSelection()
- caplog.clear()
- assert projView.requestDeleteItem() is False
- assert "no item to delete" in caplog.text
-
- # Not a valid handle
- projTree.clearSelection()
- caplog.clear()
- assert projView.requestDeleteItem("0000000000000") is False
- assert "No tree item with handle '0000000000000'" in caplog.text
-
- # Delete Root Folders
+ # Mouse Button Clicks
# ===================
- assert projView.requestDeleteItem(C.hNovelRoot) is False # Novel Root is blocked
- assert projView.requestDeleteItem(C.hCharRoot) is True # Character Root
+ eType = QEvent.Type.MouseButtonPress
+ modifier = QtModNone
- # Delete File
- # ===========
+ # Trigger the viewer
+ pos = projTree.visualRect(model.indexFromHandle(C.hChapterDoc)).center()
+ button = QtMouseMiddle
+ event = QMouseEvent(eType, pos, button, button, modifier)
+ projTree.mousePressEvent(event)
+ assert nwGUI.docViewer.docHandle == C.hChapterDoc
- # Block adding trash folder
- funcPointer = projTree._addTrashRoot
- projTree._addTrashRoot = lambda *a: None
- assert projView.requestDeleteItem("0000000000012") is False
- projTree._addTrashRoot = funcPointer
+ # Trigger the left click clear
+ pos = QPoint(5000, 5000)
+ button = QtMouseLeft
+ event = QMouseEvent(eType, pos, button, button, modifier)
- # Delete last two documents, which also adds the trash folder
- assert projView.requestDeleteItem("0000000000012") is True
- assert projView.requestDeleteItem("0000000000011") is True
- assert projTree.getTreeFromHandle(C.hChapterDir) == [
- C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
- "0000000000010"
- ]
- trashHandle = SHARED.project.tree.trashRoot
- assert projTree.getTreeFromHandle(trashHandle) == [
- trashHandle, "0000000000012", "0000000000011"
+ projTree.setSelectedHandle(C.hChapterDoc)
+ assert projView.getSelectedHandle() == C.hChapterDoc
+
+ projTree.mousePressEvent(event)
+ assert projView.getSelectedHandle() is None
+
+
+@pytest.mark.gui
+def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
+ """Test delete requests in the project tree."""
+ monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
+
+ projView = nwGUI.projView
+ projTree = projView.projTree
+ project = SHARED.project
+ tree = project.tree
+
+ # Create a project
+ buildTestProject(nwGUI, projPath)
+ trash = tree.trash
+ assert trash is not None
+ assert len(tree) == 9
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
]
- # Try to delete the trash folder
- caplog.clear()
- assert projView.requestDeleteItem("0000000000013") is False
- assert "Cannot delete the Trash folder" in caplog.text
+ # Add some scenes
+ hFolder = "0000000000011"
+ hScenes = ["0000000000012", "0000000000013", "0000000000014", "0000000000015"]
+ projView.setSelectedHandle(C.hNovelRoot, doScroll=True)
+ projTree.newTreeItem(nwItemType.FOLDER)
+ assert hFolder in tree
+ item = tree[hFolder]
+ assert item is not None
+ item.setName("Chapter Folder")
- nwGUI.closeProject()
+ projView.setSelectedHandle(hFolder, doScroll=True)
+ for n, hScene in enumerate(hScenes):
+ projTree.newTreeItem(nwItemType.FILE, hLevel=3)
+ assert hScene in tree
+ item = tree[hScene]
+ assert item is not None
+ item.setName(f"Scene {n+1}")
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
-@pytest.mark.gui
-@pytest.mark.skip
-def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
- """Test moving items to Trash."""
- monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
+ # Calling it with no selection, does nothing
+ projTree._clearSelection()
+ projTree.processDeleteRequest()
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
- project = SHARED.project
- projTree = nwGUI.projView.projTree
+ # Deleting a used root raises an error
+ projTree.processDeleteRequest([C.hNovelRoot])
+ assert SHARED.lastAlert == "Root folders can only be deleted when they are empty."
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
- # Create a project
- buildTestProject(nwGUI, projPath)
+ # Deleting a unused root is fine
+ projTree.processDeleteRequest([C.hWorldRoot])
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
+ "Plot", "Characters", "Trash",
+ ]
- # Invalid item
- caplog.clear()
- assert projTree.moveItemToTrash(C.hInvalid) is False
- assert "Could not find tree item for deletion" in caplog.text
-
- # Root folders cannot be moved to Trash
- caplog.clear()
- assert projTree.moveItemToTrash(C.hNovelRoot) is False
- assert "Root folders cannot be moved to Trash" in caplog.text
-
- # Block adding trash folder
- funcPointer = projTree._addTrashRoot
- projTree._addTrashRoot = lambda *a: None
-
- caplog.clear()
- assert projTree.moveItemToTrash(C.hTitlePage) is False
- assert project.tree.isTrash(C.hTitlePage) is False
- assert "Could not delete item" in caplog.text
-
- projTree._addTrashRoot = funcPointer
-
- # User cancels action
+ # User can cancel move to trash
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
- assert projTree.moveItemToTrash(C.hTitlePage) is False
- assert project.tree.isTrash(C.hTitlePage) is False
+ projTree.processDeleteRequest(hScenes, askFirst=True)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Chapter Folder", "Scene 1", "Scene 2", "Scene 3", "Scene 4",
+ "Plot", "Characters", "Trash",
+ ]
- # Move a document to Trash
- assert projTree.moveItemToTrash(C.hTitlePage) is True
- assert project.tree.isTrash(C.hTitlePage) is True
+ # Items not already in trash can be moved there
+ projTree.processDeleteRequest(hScenes, askFirst=True)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Chapter Folder", "Plot", "Characters", "Trash",
+ "Scene 1", "Scene 2", "Scene 3", "Scene 4",
+ ]
- # Cannot be moved again
- caplog.clear()
- assert projTree.moveItemToTrash(C.hTitlePage) is False
- assert "Item is already in the Trash folder" in caplog.text
-
- nwGUI.closeProject()
-
-
-@pytest.mark.gui
-@pytest.mark.skip
-def testGuiProjTree_PermanentlyDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
- """Test permanently deleting items."""
- monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
-
- project = SHARED.project
- projTree = nwGUI.projView.projTree
-
- # Create a project
- buildTestProject(nwGUI, projPath)
-
- # Invalid item
- caplog.clear()
- assert projTree.permDeleteItem(C.hInvalid) is False
- assert "Could not find tree item for deletion" in caplog.text
-
- # Not deleting root item in use
- caplog.clear()
- assert projTree.permDeleteItem(C.hNovelRoot) is False
- assert "Root folders can only be deleted when they are empty" in caplog.text
- assert C.hNovelRoot in project.tree
-
- # Deleting unused root item is allowed
- caplog.clear()
- assert projTree.permDeleteItem(C.hPlotRoot) is True
- assert C.hPlotRoot not in project.tree
-
- # User cancels action
+ # User can block permanent deletion
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
- assert projTree.permDeleteItem(C.hTitlePage) is False
- assert C.hTitlePage in project.tree
+ projTree.processDeleteRequest(hScenes[0:2], askFirst=True)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Chapter Folder", "Plot", "Characters", "Trash",
+ "Scene 1", "Scene 2", "Scene 3", "Scene 4",
+ ]
- # Deleting file is OK, and if it is open, it should close
- assert nwGUI.openDocument(C.hTitlePage) is True
- assert nwGUI.docEditor.docHandle == C.hTitlePage
- assert projTree.permDeleteItem(C.hTitlePage) is True
- assert C.hTitlePage not in project.tree
- assert nwGUI.docEditor.docHandle is None
+ # Items in trash can be permanently deleted
+ projTree.processDeleteRequest(hScenes[0:2], askFirst=True)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Chapter Folder", "Plot", "Characters", "Trash",
+ "Scene 3", "Scene 4",
+ ]
- # Deleting folder + files recursively is ok
- assert projTree.permDeleteItem(C.hChapterDir) is True
- assert C.hChapterDir not in project.tree
- assert C.hChapterDoc not in project.tree
- assert C.hSceneDoc not in project.tree
+ # Moving a parent item to trash, includes children
+ projTree.processDeleteRequest([C.hChapterDir], askFirst=True)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
+ "Scene 3", "Scene 4", "New Folder", "New Chapter", "New Scene",
+ ]
- nwGUI.closeProject()
+ # Permanently delete in trash is recursive
+ projTree.processDeleteRequest([C.hChapterDir], askFirst=True)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
+ "Scene 3", "Scene 4",
+ ]
-
-@pytest.mark.gui
-@pytest.mark.skip
-def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
- """Test emptying Trash."""
- monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
-
- project = SHARED.project
- projTree = nwGUI.projView.projTree
-
- # No project open
- caplog.clear()
- assert projTree.emptyTrash() is False
- assert "No project open" in caplog.text
-
- # Create a project
- buildTestProject(nwGUI, projPath)
-
- # No Trash folder
- assert projTree.emptyTrash() is False
-
- # Move some documents to Trash
- assert projTree.moveItemToTrash(C.hTitlePage) is True
- assert projTree.moveItemToTrash(C.hChapterDir) is True
-
- assert project.tree.isTrash(C.hTitlePage) is True
- assert project.tree.isTrash(C.hChapterDir) is True
- assert project.tree.isTrash(C.hChapterDoc) is True
- assert project.tree.isTrash(C.hSceneDoc) is True
-
- # User cancels
+ # Trash can be completely emptied, but user can block it
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
- assert projTree.emptyTrash() is False
- assert C.hTitlePage in project.tree
- assert C.hChapterDir in project.tree
- assert C.hChapterDoc in project.tree
- assert C.hSceneDoc in project.tree
+ projTree.emptyTrash()
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
+ "Scene 3", "Scene 4",
+ ]
- # Run again to empty all items
- assert projTree.emptyTrash() is True
- assert C.hTitlePage not in project.tree
- assert C.hChapterDir not in project.tree
- assert C.hChapterDoc not in project.tree
- assert C.hSceneDoc not in project.tree
+ # Trash can be completely emptied
+ projTree.emptyTrash()
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
+ ]
- # Running Empty Trash again is cancelled due to empty folder
- assert projTree.emptyTrash() is False
+ # Emptying empty trash pops an alert
+ projTree.emptyTrash()
+ assert SHARED.lastAlert == "The Trash folder is already empty."
- nwGUI.closeProject()
+ # Trash can be deleted if empty
+ projTree.processDeleteRequest([trash.item.itemHandle])
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "Chapter Folder", "Plot", "Characters"
+ ]
+
+ # Emptying trash when it doesn't exist, recreates it
+ projTree.emptyTrash()
+ assert SHARED.lastAlert == "The Trash folder is already empty."
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash",
+ ]
@pytest.mark.gui
-@pytest.mark.skip
def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText):
"""Test the merge document function."""
mergeData = {}
@@ -544,18 +698,33 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd,
monkeypatch.setattr(GuiDocMerge, "result", lambda *a: QtAccepted)
monkeypatch.setattr(GuiDocMerge, "data", lambda *a: mergeData)
- buildTestProject(nwGUI, projPath)
-
+ projView = nwGUI.projView
+ projTree = projView.projTree
project = SHARED.project
- projTree = nwGUI.projView.projTree
+ tree = project.tree
- mergedDoc1 = "0000000000014"
+ # Create a project
+ buildTestProject(nwGUI, projPath)
+ trash = tree.trash
+ assert trash is not None
+ assert len(tree) == 9
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
# Create File to Merge
hChapter1 = project.newFile("Chapter 1", C.hNovelRoot)
- hSceneOne11 = project.newFile("Scene 1.1", hChapter1) # type: ignore
- hSceneOne12 = project.newFile("Scene 1.2", hChapter1) # type: ignore
- hSceneOne13 = project.newFile("Scene 1.3", hChapter1) # type: ignore
+ assert hChapter1 == "0000000000011"
+
+ hSceneOne11 = project.newFile("Scene 1.1", hChapter1)
+ hSceneOne12 = project.newFile("Scene 1.2", hChapter1)
+ hSceneOne13 = project.newFile("Scene 1.3", hChapter1)
+ assert hSceneOne11 == "0000000000012"
+ assert hSceneOne12 == "0000000000013"
+ assert hSceneOne13 == "0000000000014"
+
+ mergedDoc1 = "0000000000015"
docText1 = "\n\n".join(ipsumText[0:2]) + "\n\n"
docText2 = "\n\n".join(ipsumText[1:3]) + "\n\n"
@@ -568,25 +737,26 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd,
lenText4 = len(docText4)
lenAll = lenText1 + lenText2 + lenText3 + lenText4
- project.writeNewFile(hChapter1, 2, True, docText1) # type: ignore
- project.writeNewFile(hSceneOne11, 3, True, docText2) # type: ignore
- project.writeNewFile(hSceneOne12, 3, True, docText3) # type: ignore
- project.writeNewFile(hSceneOne13, 3, True, docText4) # type: ignore
+ project.writeNewFile(hChapter1, 2, True, docText1)
+ project.writeNewFile(hSceneOne11, 3, True, docText2)
+ project.writeNewFile(hSceneOne12, 3, True, docText3)
+ project.writeNewFile(hSceneOne13, 3, True, docText4)
- projTree.revealNewTreeItem(hChapter1)
- projTree.revealNewTreeItem(hSceneOne11)
- projTree.revealNewTreeItem(hSceneOne12)
- projTree.revealNewTreeItem(hSceneOne13)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Chapter 1", "Scene 1.1", "Scene 1.2", "Scene 1.3",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
# Invalid file handle
- assert projTree._mergeDocuments(C.hInvalid, False) is False
+ assert projTree.mergeDocuments(C.hInvalid, False) is False
# Cannot merge root item
- assert projTree._mergeDocuments(C.hNovelRoot, False) is False
+ assert projTree.mergeDocuments(C.hNovelRoot, False) is False
# Merge to new file, but there is now merge data
mergeData.clear()
- assert projTree._mergeDocuments(hChapter1, True) is False
+ assert projTree.mergeDocuments(hChapter1, True) is False
# Merge to New Doc
# ================
@@ -598,11 +768,18 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd,
# User cancels merge
with monkeypatch.context() as mp:
mp.setattr(GuiDocMerge, "result", lambda *a: QtRejected)
- assert projTree._mergeDocuments(hChapter1, True) is False
+ assert projTree.mergeDocuments(hChapter1, True) is False
# The merge goes through
- assert projTree._mergeDocuments(hChapter1, True) is True
- assert len(project.storage.getDocument(mergedDoc1).readDocument()) > lenAll # type: ignore
+ assert projTree.mergeDocuments(hChapter1, True) is True
+ mergedText = project.storage.getDocument(mergedDoc1).readDocument()
+ assert mergedText is not None
+ assert len(mergedText) > lenAll
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Chapter 1", "Scene 1.1", "Scene 1.2", "Scene 1.3", "[Merged] Chapter 1",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
# Merge to Existing Doc
# =====================
@@ -612,28 +789,34 @@ def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd,
mergeData["moveToTrash"] = False
# Merging to a folder is not allowed
- assert projTree._mergeDocuments(C.hChapterDir, False) is False
+ assert projTree.mergeDocuments(C.hChapterDir, False) is False
# Block writing and check error handling
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
- assert projTree._mergeDocuments(hChapter1, False) is False
+ assert projTree.mergeDocuments(hChapter1, False) is False
# Successful merge, and move to trash
mergeData["moveToTrash"] = True
- assert len(project.storage.getDocument(hChapter1).readDocument()) < lenAll # type: ignore
- assert projTree._mergeDocuments(hChapter1, False) is True
- assert len(project.storage.getDocument(hChapter1).readDocument()) > lenAll # type: ignore
+ mergedText = project.storage.getDocument(hChapter1).readDocument()
+ assert mergedText is not None
+ assert len(mergedText) < lenAll
+ assert projTree.mergeDocuments(hChapter1, False) is True
+ mergedText = project.storage.getDocument(hChapter1).readDocument()
+ assert mergedText is not None
+ assert len(mergedText) > lenAll
- assert project.tree.isTrash(hSceneOne11) # type: ignore
- assert project.tree.isTrash(hSceneOne12) # type: ignore
- assert project.tree.isTrash(hSceneOne13) # type: ignore
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Chapter 1", "[Merged] Chapter 1",
+ "Plot", "Characters", "Locations", "Trash",
+ "Scene 1.1", "Scene 1.2", "Scene 1.3",
+ ]
# qtbot.stop()
@pytest.mark.gui
-@pytest.mark.skip
def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText):
"""Test the split document function."""
splitData = {}
@@ -645,11 +828,20 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd,
monkeypatch.setattr(GuiDocSplit, "result", lambda *a: QtAccepted)
monkeypatch.setattr(GuiDocSplit, "data", lambda *a: (splitData, splitText))
+ projView = nwGUI.projView
+ projTree = projView.projTree
+ project = SHARED.project
+ tree = project.tree
+
# Create a project
buildTestProject(nwGUI, projPath)
-
- project = SHARED.project
- projTree = nwGUI.projView.projTree
+ trash = tree.trash
+ assert trash is not None
+ assert len(tree) == 9
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
docText = (
"Text\n\n"
@@ -667,8 +859,8 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd,
)
hSplitDoc = project.newFile("Split Doc", C.hNovelRoot)
- project.writeNewFile(hSplitDoc, 1, True, docText) # type: ignore
- projTree.revealNewTreeItem(hSplitDoc, nHandle=C.hNovelRoot, wordCount=True)
+ assert hSplitDoc is not None
+ project.writeNewFile(hSplitDoc, 1, True, docText)
docText = f"# Split Doc\n\n{docText}"
splitData["headerList"] = [
@@ -687,24 +879,24 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd,
]
fstSet = [
- "0000000000011", "0000000000012", "0000000000013", "0000000000014",
- "0000000000015", "0000000000016", "0000000000017", "0000000000018",
- "0000000000019", "000000000001a", "000000000001b", "000000000001c",
+ "0000000000012", "0000000000013", "0000000000014", "0000000000015",
+ "0000000000016", "0000000000017", "0000000000018", "0000000000019",
+ "000000000001a", "000000000001b", "000000000001c", "000000000001d",
]
sndSet = [
- "000000000001d", "000000000001e", "000000000001f", "0000000000020",
- "0000000000021", "0000000000022", "0000000000023", "0000000000024",
- "0000000000025", "0000000000026", "0000000000027", "0000000000028",
+ "000000000001e", "000000000001f", "0000000000020", "0000000000021",
+ "0000000000022", "0000000000023", "0000000000024", "0000000000025",
+ "0000000000026", "0000000000027", "0000000000028", "0000000000029",
]
trdSet = [
- "000000000002a", "000000000002b", "000000000002c", "000000000002d",
- "000000000002e", "000000000002f", "0000000000030", "0000000000031",
- "0000000000032", "0000000000033", "0000000000034", "0000000000035",
+ "000000000002b", "000000000002c", "000000000002d", "000000000002e",
+ "000000000002f", "0000000000030", "0000000000031", "0000000000032",
+ "0000000000033", "0000000000034", "0000000000035", "0000000000036",
]
# Try to split an invalid document and a non-document
- assert projTree._splitDocument(C.hInvalid) is False
- assert projTree._splitDocument(C.hNovelRoot) is False
+ assert projTree.splitDocument(C.hInvalid) is False
+ assert projTree.splitDocument(C.hNovelRoot) is False
# Split into same root folder
splitData["intoFolder"] = False
@@ -712,13 +904,13 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd,
# Writing fails
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
- assert projTree._splitDocument(hSplitDoc) is True
+ assert projTree.splitDocument(hSplitDoc) is True
for tHandle in fstSet:
assert tHandle in project.tree
assert not (projPath / "content" / f"{tHandle}.nwd").is_file()
# Writing succeeds
- assert projTree._splitDocument(hSplitDoc) is True
+ assert projTree.splitDocument(hSplitDoc) is True
for tHandle in sndSet:
assert tHandle in project.tree
assert (projPath / "content" / f"{tHandle}.nwd").is_file()
@@ -726,240 +918,95 @@ def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd,
# Add to a folder and move source to trash
splitData["intoFolder"] = True
splitData["moveToTrash"] = True
- assert projTree._splitDocument(hSplitDoc) is True
+ assert projTree.splitDocument(hSplitDoc) is True
assert "0000000000029" in project.tree # The folder
for tHandle in trdSet:
assert tHandle in project.tree
assert (projPath / "content" / f"{tHandle}.nwd").is_file()
- assert project.tree.isTrash(hSplitDoc) is True # type: ignore
+ assert trash.allChildren() == [tree.nodes[hSplitDoc]]
# Cancelled by user
with monkeypatch.context() as mp:
mp.setattr(GuiDocSplit, "result", lambda *a: QtRejected)
- assert projTree._splitDocument(hSplitDoc) is False
+ assert projTree.splitDocument(hSplitDoc) is False
# qtbot.stop()
@pytest.mark.gui
-@pytest.mark.skip
-def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd):
+def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the duplicate items function."""
# Create a project
- buildTestProject(nwGUI, projPath)
- assert len(SHARED.project.tree) == 8
+ projView = nwGUI.projView
+ projTree = projView.projTree
+ project = SHARED.project
+ tree = project.tree
+
+ # Create a project
+ buildTestProject(nwGUI, projPath)
+ trash = tree.trash
+ assert trash is not None
+ assert len(tree) == 9
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
- projTree = nwGUI.projView.projTree
projTree.expandAll()
# Nothing to do
- assert projTree.duplicateFromHandle(C.hInvalid) is False
- assert len(SHARED.project.tree) == 8
+ projTree.duplicateFromHandle(C.hInvalid)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
# Duplicate title page, but select no
with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
- assert projTree.duplicateFromHandle(C.hTitlePage) is False
- assert len(SHARED.project.tree) == 8
-
- # Duplicate title page
- assert projTree.duplicateFromHandle(C.hTitlePage) is True
- assert len(SHARED.project.tree) == 9
-
- # Duplicate folder
- assert projTree.duplicateFromHandle(C.hChapterDir) is True
- assert len(SHARED.project.tree) == 12
-
- # Duplicate novel root
- assert projTree.duplicateFromHandle(C.hNovelRoot) is True
- assert len(SHARED.project.tree) == 21
-
- # Check tree order that all items are next to each other
- assert SHARED.project.tree._order == [
- C.hNovelRoot, C.hTitlePage, "0000000000010", C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
- "0000000000011", "0000000000012", "0000000000013", "0000000000014", "0000000000015",
- "0000000000016", "0000000000017", "0000000000018", "0000000000019", "000000000001a",
- "000000000001b", "000000000001c", C.hPlotRoot, C.hCharRoot, C.hWorldRoot,
+ mp.setattr(QMessageBox, "result", lambda *a: QtRejected)
+ projTree.duplicateFromHandle(C.hTitlePage)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
]
- # Make the duplicator stop early
- content = SHARED.project.storage.contentPath
- assert isinstance(content, Path)
- (content / "000000000001e.nwd").touch()
- assert (content / "000000000001e.nwd").exists()
+ # Duplicate title page
+ projTree.duplicateFromHandle(C.hTitlePage)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "Title Page", "New Folder", "New Chapter",
+ "New Scene", "Plot", "Characters", "Locations", "Trash",
+ ]
- # Should only create the folder, and skip the two files because the
- # next handle is already a file
- assert projTree.duplicateFromHandle(C.hChapterDir) is True
- assert len(SHARED.project.tree) == 22
+ # Duplicate folder
+ projTree.duplicateFromHandle(C.hChapterDir)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "Title Page", "New Folder", "New Chapter",
+ "New Scene", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
+
+ # Duplicate novel root
+ projTree.duplicateFromHandle(C.hNovelRoot)
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "Title Page", "New Folder", "New Chapter",
+ "New Scene", "New Folder", "New Chapter", "New Scene",
+ "Novel", "Title Page", "Title Page", "New Folder", "New Chapter",
+ "New Scene", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
# qtbot.stop()
@pytest.mark.gui
-@pytest.mark.skip
-def testGuiProjTree_AutoScroll(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd):
- """Test the auto scroll feature."""
- buildTestProject(nwGUI, projPath)
- projTree: GuiProjectTree = nwGUI.projView.projTree
-
- monkeypatch.setattr(GuiEditLabel, "getLabel", lambda parent, text: (text, True))
- monkeypatch.setattr(QTreeWidget, "dragMoveEvent", lambda *a, **k: None)
- monkeypatch.setattr(QTimer, "isActive", lambda *a: False)
- monkeypatch.setattr(QTimer, "start", lambda *a: None)
-
- projTree.setSelectedHandle(C.hChapterDir, True)
- projTree.expandAll()
- for i in range(100):
- projTree.newTreeItem(nwItemType.FILE, None, 3, False)
-
- projTree.setSelectedHandle("0000000000015", True)
- nwGUI.resize(500, 500)
-
- action = Qt.DropAction.MoveAction
- mime = QMimeData()
- mouse = QtMouseLeft
- modifier = QtModNone
-
- # Scroll Down
- h = projTree.height()
- projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, h-1), action, mime, mouse, modifier))
- assert projTree._scrollDirection == 1
- projTree._doAutoScroll()
- assert projTree._scrollDirection == 0
- projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, h-2), action, mime, mouse, modifier))
- assert projTree._scrollDirection == 1
- projTree._doAutoScroll()
- assert projTree._scrollDirection == 0
- projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, h-3), action, mime, mouse, modifier))
- assert projTree._scrollDirection == 1
- projTree._doAutoScroll()
- assert projTree._scrollDirection == 0
- projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, h-4), action, mime, mouse, modifier))
- assert projTree._scrollDirection == 1
- projTree._doAutoScroll()
- assert projTree._scrollDirection == 0
-
- # Scroll Up
- projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, 1), action, mime, mouse, modifier))
- assert projTree._scrollDirection == -1
- projTree._doAutoScroll()
- assert projTree._scrollDirection == 0
- projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, 2), action, mime, mouse, modifier))
- assert projTree._scrollDirection == -1
- projTree._doAutoScroll()
- assert projTree._scrollDirection == 0
- projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, 3), action, mime, mouse, modifier))
- assert projTree._scrollDirection == -1
- projTree._doAutoScroll()
- assert projTree._scrollDirection == 0
- projTree.dragMoveEvent(QDragMoveEvent(QPoint(1, 4), action, mime, mouse, modifier))
- assert projTree._scrollDirection == -1
- projTree._doAutoScroll()
- assert projTree._scrollDirection == 0
-
- # qtbot.stop()
-
-
-@pytest.mark.gui
-@pytest.mark.skip
-def testGuiProjTree_DragAndDrop(qtbot, monkeypatch, caplog, nwGUI: GuiMain, projPath, mockRnd):
- """Test the auto scroll feature."""
- buildTestProject(nwGUI, projPath)
- projTree: GuiProjectTree = nwGUI.projView.projTree
-
- monkeypatch.setattr(QTreeWidget, "dropEvent", lambda *a, **k: None)
-
- projTree.setSelectedHandle(C.hSceneDoc, True)
- projTree._getTreeItem(C.hChapterDir).setExpanded(True) # type: ignore
-
- nPos = projTree.visualItemRect(projTree._getTreeItem(C.hNovelRoot)).bottomLeft()
- action = Qt.DropAction.MoveAction
- mime = QMimeData()
- mouse = QtMouseLeft
- modifier = QtModNone
-
- projTree.saveTreeOrder()
- treeOrder = SHARED.project.tree._order
-
- # Move an item, but no selection
- event = QDropEvent(nPos, action, mime, mouse, modifier)
- projTree.dropEvent(event)
- projTree.saveTreeOrder()
- assert SHARED.project.tree._order == treeOrder
-
- # Invalid location
- caplog.clear()
- event = QDropEvent(QPoint(1000, 1000), action, mime, mouse, modifier)
- projTree.dropEvent(event)
- assert event.isAccepted() is False
- assert "Invalid drop location" in caplog.text
- projTree.saveTreeOrder()
- assert SHARED.project.tree._order == treeOrder
-
- # Root item selected
- caplog.clear()
- event = QDropEvent(nPos, action, mime, mouse, modifier)
- projTree.clearSelection()
- projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore
- projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore
- projTree.dropEvent(event)
- assert event.isAccepted() is False
- projTree.saveTreeOrder()
- assert SHARED.project.tree._order == treeOrder
-
- # Make sure illegal drag events are cancelled
- with monkeypatch.context() as mp:
- mp.setattr(QTreeWidget, "dragEnterEvent", lambda *a: None)
- mime = QMimeData()
- mime.setText("foobar")
- event = QDragEnterEvent(nPos, action, mime, mouse, modifier)
- projTree.clearSelection()
- projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore
- projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore
- projTree._getTreeItem(C.hChapterDoc).setSelected(True) # type: ignore
- assert projTree.selectedItems() == [ # Novel Root selection is cancelled automatically
- projTree._getTreeItem(C.hTitlePage), projTree._getTreeItem(C.hChapterDoc)
- ]
- projTree.dragEnterEvent(event)
- assert mime.text() == ""
- assert projTree._popAlert is not None
-
- # Pop the alert
- with monkeypatch.context() as mp:
- mp.setattr(QTreeWidget, "startDrag", lambda *a: None)
- projTree.startDrag(None) # type: ignore
- assert projTree._popAlert is None
-
- # Valid drag events are processed
- with monkeypatch.context() as mp:
- mp.setattr(QTreeWidget, "dragEnterEvent", lambda *a: None)
- mime = QMimeData()
- mime.setText("foobar")
- event = QDragEnterEvent(nPos, action, mime, mouse, modifier)
- projTree.clearSelection()
- projTree._getTreeItem(C.hChapterDoc).setSelected(True) # type: ignore
- projTree._getTreeItem(C.hSceneDoc).setSelected(True) # type: ignore
- assert projTree.selectedItems() == [ # Novel Root selection is cancelled automatically
- projTree._getTreeItem(C.hChapterDoc), projTree._getTreeItem(C.hSceneDoc)
- ]
- projTree.dragEnterEvent(event)
- assert mime.text() == "foobar"
- assert projTree._popAlert is None
-
- # qtbot.stop()
-
-
-@pytest.mark.gui
-@pytest.mark.skip
-def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd):
+def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test various parts of the project tree class not covered by
other tests.
"""
buildTestProject(nwGUI, projPath)
- projView: GuiProjectView = nwGUI.projView
- projTree: GuiProjectTree = nwGUI.projView.projTree
+ projView = nwGUI.projView
+ projTree = projView.projTree
# Method: initSettings
# ====================
@@ -977,224 +1024,100 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd)
assert projTree.verticalScrollBarPolicy() == QtScrollAsNeeded
assert projTree.horizontalScrollBarPolicy() == QtScrollAsNeeded
- # Method: revealNewTreeItem
- # =========================
-
- # Send invalid handle
- assert projTree.revealNewTreeItem(C.hInvalid) is False
-
- # Try to add an orphaned file to the tree
- nHandle = SHARED.project.newFile("Test", C.hNovelRoot)
- SHARED.project.tree[nHandle].setParent(None) # type: ignore
- assert projTree.revealNewTreeItem(nHandle) is False
-
- # Try to add an item with unknown parent to the tree
- nHandle = SHARED.project.newFile("Test", C.hNovelRoot)
- SHARED.project.tree[nHandle].setParent(C.hInvalid) # type: ignore
- assert projTree.revealNewTreeItem(nHandle) is False
-
- # Slot: _treeDoubleClick
- # ======================
-
- # Try to open a file with nothings selected
- projTree.clearSelection()
- projTree._onDoubleClick(QTreeWidgetItem(), 0)
- assert nwGUI.docEditor.docHandle is None
-
- # When the item cannot be found
- projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore
- with monkeypatch.context() as mp:
- mp.setattr("novelwriter.core.tree.NWTree.__getitem__", lambda *a: None)
- projTree._onDoubleClick(QTreeWidgetItem(), 0)
- assert nwGUI.docEditor.docHandle is None
-
- # Successfully open a file
- projTree._onDoubleClick(projTree._getTreeItem(C.hTitlePage), 0)
- assert nwGUI.docEditor.docHandle == C.hTitlePage
- projTree._getTreeItem(C.hTitlePage).setSelected(False) # type: ignore
-
- # A non-file item should be expanded instead
- projTree._getTreeItem(C.hNovelRoot).setExpanded(False) # type: ignore
- projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore
- projTree._onDoubleClick(projTree._getTreeItem(C.hNovelRoot), 1)
- assert nwGUI.docEditor.docHandle == C.hTitlePage
- assert projTree._getTreeItem(C.hNovelRoot).isExpanded() is True # type: ignore
-
- # Navigate the Tree
- # =================
-
- # Expand handles
- projTree.setExpandedFromHandle(C.hNovelRoot, True)
- projTree.setSelectedHandle(C.hSceneDoc)
- assert projTree.getSelectedHandle() == C.hSceneDoc
-
- # Move between documents in that folder
- projTree.moveToNextItem(-1)
- assert projTree.getSelectedHandle() == C.hChapterDoc
- projTree.moveToNextItem(-1) # Can't move further up
- assert projTree.getSelectedHandle() == C.hChapterDoc
- projTree.moveToNextItem(1)
- assert projTree.getSelectedHandle() == C.hSceneDoc
- projTree.moveToNextItem(1) # Can't move further down
- assert projTree.getSelectedHandle() == C.hSceneDoc
-
- # Move up/down the parent/child hierarchy
- projTree.moveToLevel(-1)
- assert projTree.getSelectedHandle() == C.hChapterDir
- projTree.moveToLevel(-1)
- assert projTree.getSelectedHandle() == C.hNovelRoot
- projTree.moveToLevel(-1) # Can't move further up
- assert projTree.getSelectedHandle() == C.hNovelRoot
- projTree.moveToLevel(1)
- assert projTree.getSelectedHandle() == C.hTitlePage
- projTree.moveToLevel(1) # Can't move further down
- assert projTree.getSelectedHandle() == C.hTitlePage
-
- # Move between roots
- projTree.setSelectedHandle(C.hNovelRoot)
- projTree.moveToNextItem(1)
- assert projTree.getSelectedHandle() == C.hPlotRoot
- projTree.moveToNextItem(1)
- assert projTree.getSelectedHandle() == C.hCharRoot
- projTree.moveToNextItem(1)
- assert projTree.getSelectedHandle() == C.hWorldRoot
- projTree.moveToNextItem(1) # Can't move further down
- assert projTree.getSelectedHandle() == C.hWorldRoot
-
- # When nothing is selected, nothing happens
- projTree.clearSelection()
- assert projTree.getSelectedHandle() is None
- projTree.moveToNextItem(-1)
- assert projTree.getSelectedHandle() is None
- projTree.moveToNextItem(1)
- assert projTree.getSelectedHandle() is None
- projTree.moveToLevel(-1)
- assert projTree.getSelectedHandle() is None
- projTree.moveToLevel(1)
- assert projTree.getSelectedHandle() is None
-
- # Mouse Button Clicks
- # ===================
-
- eType = QEvent.Type.MouseButtonPress
- pos = projTree.visualItemRect(projTree._getTreeItem(C.hChapterDoc)).center()
- button = QtMouseMiddle
- modifier = QtModNone
-
- # Trigger the viewer
- event = QMouseEvent(eType, pos, button, button, modifier)
- projTree.mousePressEvent(event)
- assert nwGUI.docViewer.docHandle == C.hChapterDoc
-
- # Trigger the left click clear
- pos = QPoint(5000, 5000)
- button = QtMouseLeft
- event = QMouseEvent(eType, pos, button, button, modifier)
- projTree.setSelectedHandle(C.hChapterDoc)
- projTree.mousePressEvent(event)
- assert projTree.selectedItems() == []
-
- # Rename Item
- # ===========
-
- with monkeypatch.context() as mp:
- mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("FooBar", True))
- projTree.clearSelection()
- assert SHARED.project.tree[C.hChapterDoc].itemName == "New Chapter" # type: ignore
- projView.renameTreeItem(C.hChapterDoc)
- assert SHARED.project.tree[C.hChapterDoc].itemName == "FooBar" # type: ignore
-
- projTree.setSelectedHandle(C.hSceneDoc)
- assert SHARED.project.tree[C.hSceneDoc].itemName == "New Scene" # type: ignore
- projView.renameTreeItem()
- assert SHARED.project.tree[C.hSceneDoc].itemName == "FooBar" # type: ignore
-
- # Check Crash Resistance
- # ======================
- projTree._postItemMove("dfghj") # This should exit cleanly
-
# qtbot.stop()
@pytest.mark.gui
-@pytest.mark.skip
def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the building of the project tree context menu. All this does
is test that the menu builds. It doesn't open the actual menu.
"""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
+ QtSelect = QItemSelectionModel.SelectionFlag.Select
+
+ # Create a project
+ projView = nwGUI.projView
+ projTree = projView.projTree
+ project = SHARED.project
+ tree = project.tree
+ model = tree.model
# Create a project
buildTestProject(nwGUI, projPath)
- nwGUI.openProject(projPath)
- nwGUI._switchFocus(nwFocus.TREE)
+ trash = tree.trash
+ assert trash is not None
+ assert len(tree) == 9
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
# Handles for new objects
hCharNote = "0000000000011"
hNovelNote = "0000000000012"
- hSubNote = "0000000000013"
- hNewFolderOne = "0000000000014"
- hNewFolderTwo = "0000000000016"
-
- projView = nwGUI.projView
- projTree = nwGUI.projView.projTree
- projTree.setExpandedFromHandle(None, True)
-
- projTree._addTrashRoot()
- hTrashRoot = SHARED.project.tree.trashRoot
+ hTrashDoc = "0000000000013"
+ hSubNote = "0000000000014"
+ hNewFolderOne = "0000000000015"
+ hNewFolderTwo = "0000000000017"
+ hTrashRoot = trash.item.itemHandle
+ projTree.expandAll()
projTree.setSelectedHandle(C.hCharRoot)
- projTree.newTreeItem(nwItemType.FILE)
+ projTree.newTreeItem(nwItemType.FILE, isNote=True)
projTree.setSelectedHandle(C.hNovelRoot)
projTree.newTreeItem(nwItemType.FILE, isNote=True)
+ projTree.setSelectedHandle(C.hNovelRoot)
+ projTree.newTreeItem(nwItemType.FILE)
+ projTree.setSelectedHandle(hTrashDoc)
+ projTree.processDeleteRequest()
- SHARED.project.newFile("SubNote", hNovelNote)
- projTree.revealNewTreeItem(hSubNote)
- assert SHARED.project.tree[hSubNote].itemParent == hNovelNote # type: ignore
+ project.newFile("SubNote", hNovelNote)
+ item = tree[hSubNote]
+ assert item is not None
+ assert item.itemName == "SubNote"
+ assert item.itemParent == hNovelNote
- def itemPos(tHandle):
- return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center()
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "New Note", "SubNote", "Plot", "Characters", "New Note", "Locations",
+ "Trash", "New Document",
+ ]
# Pop the menu in various positions and check for success
with monkeypatch.context() as mp:
mockMenu = MagicMock()
mp.setattr(QMenu, "exec", mockMenu)
- projTree.clearSelection()
+ projTree._clearSelection()
# No item under menu
projTree.openContextMenu(projTree.viewport().rect().bottomRight())
assert mockMenu.call_count == 0
# Open Trash Menu
- projTree.openContextMenu(itemPos(hTrashRoot))
+ projView.setSelectedHandle(hTrashRoot)
+ projTree.openContextMenu()
assert mockMenu.call_count == 1
# Open Single Select Menu
- projTree.openContextMenu(itemPos(C.hNovelRoot))
+ projView.setSelectedHandle(C.hNovelRoot)
+ projTree.openContextMenu()
assert mockMenu.call_count == 2
# Open Multi-Select Menu
- projTree._getTreeItem(hNovelNote).setSelected(True)
- projTree._getTreeItem(hSubNote).setSelected(True)
- projTree.openContextMenu(itemPos(hCharNote))
+ projTree.selectionModel().select(model.indexFromHandle(hNovelNote), QtSelect)
+ projTree.selectionModel().select(model.indexFromHandle(hSubNote), QtSelect)
+ projTree.openContextMenu()
assert mockMenu.call_count == 3
-
- # Check the keyboard shortcut handler as well
- projTree.setSelectedHandle(C.hNovelRoot)
- projTree.openContextMenu(None)
- assert mockMenu.call_count == 4
projTree.clearSelection()
- projTree.openContextMenu(None)
- assert mockMenu.call_count == 4
# Menu Builders
# =============
# Context Menu on Root Item
- nwItem = SHARED.project.tree[C.hNovelRoot]
- assert isinstance(nwItem, NWItem)
- ctxMenu = _TreeContextMenu(projTree, nwItem)
- ctxMenu.buildSingleSelectMenu(True)
+ node = tree.nodes[C.hNovelRoot]
+ indices = [model.indexFromHandle(C.hNovelRoot)]
+ ctxMenu = _TreeContextMenu(projTree, model, node, indices)
+ ctxMenu.buildSingleSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Create New ...", "Rename", "Set Status to ...", "Expand All",
@@ -1202,10 +1125,10 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
]
# Context Menu on Folder Item
- nwItem = SHARED.project.tree[C.hChapterDir]
- assert isinstance(nwItem, NWItem)
- ctxMenu = _TreeContextMenu(projTree, nwItem)
- ctxMenu.buildSingleSelectMenu(True)
+ node = tree.nodes[C.hChapterDir]
+ indices = [model.indexFromHandle(C.hChapterDir)]
+ ctxMenu = _TreeContextMenu(projTree, model, node, indices)
+ ctxMenu.buildSingleSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Create New ...", "Rename", "Set Status to ...", "Transform ...", "Expand All",
@@ -1219,26 +1142,24 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
return []
# Context Menu on Document File Item
- nwItem = SHARED.project.tree[C.hChapterDoc]
- assert isinstance(nwItem, NWItem)
- ctxMenu = _TreeContextMenu(projTree, nwItem)
- ctxMenu.buildSingleSelectMenu(True)
+ node = tree.nodes[C.hChapterDoc]
+ indices = [model.indexFromHandle(C.hChapterDoc)]
+ ctxMenu = _TreeContextMenu(projTree, model, node, indices)
+ ctxMenu.buildSingleSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Open Document", "View Document", "Create New ...", "Rename", "Rename to Heading",
- "Toggle Active", "Set Status to ...", "Transform ...", "Expand All", "Collapse All",
- "Duplicate", "Move to Trash",
+ "Toggle Active", "Set Status to ...", "Transform ...", "Duplicate", "Move to Trash",
]
assert getTransformSubMenu(ctxMenu) == [
- "Convert to Project Note", "Merge Child Items into Self",
- "Merge Child Items into New", "Split Document by Headings"
+ "Convert to Project Note", "Split Document by Headings"
]
# Context Menu on Note File Item in Character Folder
- nwItem = SHARED.project.tree[hCharNote]
- assert isinstance(nwItem, NWItem)
- ctxMenu = _TreeContextMenu(projTree, nwItem)
- ctxMenu.buildSingleSelectMenu(False)
+ node = tree.nodes[hCharNote]
+ indices = [model.indexFromHandle(hCharNote)]
+ ctxMenu = _TreeContextMenu(projTree, model, node, indices)
+ ctxMenu.buildSingleSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Open Document", "View Document", "Create New ...", "Rename", "Rename to Heading",
@@ -1249,34 +1170,44 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
]
# Context Menu on Note File Item in Novel Tree
- nwItem = SHARED.project.tree[hNovelNote]
- assert isinstance(nwItem, NWItem)
- ctxMenu = _TreeContextMenu(projTree, nwItem)
- ctxMenu.buildSingleSelectMenu(False)
+ node = tree.nodes[hNovelNote]
+ indices = [model.indexFromHandle(hNovelNote)]
+ ctxMenu = _TreeContextMenu(projTree, model, node, indices)
+ ctxMenu.buildSingleSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Open Document", "View Document", "Create New ...", "Rename", "Rename to Heading",
- "Toggle Active", "Set Status to ...", "Transform ...", "Duplicate", "Move to Trash",
+ "Toggle Active", "Set Status to ...", "Transform ...", "Expand All", "Collapse All",
+ "Duplicate", "Move to Trash",
]
assert getTransformSubMenu(ctxMenu) == [
- "Convert to Novel Document", "Split Document by Headings",
+ "Convert to Novel Document", "Merge Child Items into Self",
+ "Merge Child Items into New", "Split Document by Headings",
]
# Context Menu on Multiple Items, Clicked on Document
- nwItem = SHARED.project.tree[hNovelNote]
- assert isinstance(nwItem, NWItem)
- ctxMenu = _TreeContextMenu(projTree, nwItem)
- ctxMenu.buildMultiSelectMenu([hCharNote, hNovelNote, hSubNote])
+ node = tree.nodes[hNovelNote]
+ indices = [
+ model.indexFromHandle(hCharNote),
+ model.indexFromHandle(hNovelNote),
+ model.indexFromHandle(hSubNote),
+ ]
+ ctxMenu = _TreeContextMenu(projTree, model, node, indices)
+ ctxMenu.buildMultiSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Set Active to ...", "Set Status to ...", "Move to Trash",
]
# Context Menu on Multiple Items, Clicked on Note
- nwItem = SHARED.project.tree[hCharNote]
- assert isinstance(nwItem, NWItem)
- ctxMenu = _TreeContextMenu(projTree, nwItem)
- ctxMenu.buildMultiSelectMenu([hCharNote, hNovelNote, hSubNote])
+ node = tree.nodes[hCharNote]
+ indices = [
+ model.indexFromHandle(hCharNote),
+ model.indexFromHandle(hNovelNote),
+ model.indexFromHandle(hSubNote),
+ ]
+ ctxMenu = _TreeContextMenu(projTree, model, node, indices)
+ ctxMenu.buildMultiSelectMenu()
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Set Active to ...", "Set Importance to ...", "Move to Trash",
@@ -1285,189 +1216,180 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# Direct Edit Functions, Single
# =============================
- nwItem = SHARED.project.tree[hNovelNote]
- assert isinstance(nwItem, NWItem)
+ node = tree.nodes[hNovelNote]
+ indices = [model.indexFromHandle(hNovelNote)]
- # # Toggle active flag
- ctxMenu = _TreeContextMenu(projTree, nwItem)
- ctxMenu.buildSingleSelectMenu(False)
- assert nwItem.isActive is True
+ # Toggle active flag
+ ctxMenu = _TreeContextMenu(projTree, model, node, indices)
+ ctxMenu.buildSingleSelectMenu()
+ assert node.item.isActive is True
ctxMenu._toggleItemActive()
- assert nwItem.isActive is False
+ assert node.item.isActive is False
# Change item status
- assert nwItem.itemStatus == "s000000"
+ assert node.item.itemStatus == "s000000"
ctxMenu._changeItemStatus("s000001")
- assert nwItem.itemStatus == "s000001"
+ assert node.item.itemStatus == "s000001"
# Change item importance
- assert nwItem.itemImport == "i000004"
+ assert node.item.itemImport == "i000004"
ctxMenu._changeItemImport("i000005")
- assert nwItem.itemImport == "i000005"
+ assert node.item.itemImport == "i000005"
# Change item layout
- assert nwItem.itemLayout == nwItemLayout.NOTE
+ assert node.item.itemLayout == nwItemLayout.NOTE
ctxMenu._changeItemLayout(nwItemLayout.DOCUMENT)
- assert nwItem.itemLayout == nwItemLayout.DOCUMENT
+ assert node.item.itemLayout == nwItemLayout.DOCUMENT
ctxMenu._changeItemLayout(nwItemLayout.NOTE)
- assert nwItem.itemLayout == nwItemLayout.NOTE
+ assert node.item.itemLayout == nwItemLayout.NOTE
# Convert Folders to Documents
# ============================
projView.setSelectedHandle(hNovelNote)
- assert projTree.newTreeItem(nwItemType.FOLDER) is True
+ projTree.newTreeItem(nwItemType.FOLDER)
projView.setSelectedHandle(hNewFolderOne)
- assert projTree.newTreeItem(nwItemType.FILE) is True
+ projTree.newTreeItem(nwItemType.FILE)
projView.setSelectedHandle(hNovelNote)
- assert projTree.newTreeItem(nwItemType.FOLDER) is True
+ projTree.newTreeItem(nwItemType.FOLDER)
projView.setSelectedHandle(hNewFolderTwo)
- assert projTree.newTreeItem(nwItemType.FILE, isNote=True) is True
+ projTree.newTreeItem(nwItemType.FILE, isNote=True)
- nwItem = SHARED.project.tree[hNewFolderOne]
- assert isinstance(nwItem, NWItem)
- ctxMenu = _TreeContextMenu(projTree, nwItem)
- ctxMenu.buildSingleSelectMenu(False)
+ assert hNewFolderOne in tree
+ assert hNewFolderTwo in tree
+
+ nodeOne = tree.nodes[hNewFolderOne]
+ nodeTwo = tree.nodes[hNewFolderTwo]
+
+ # Select Folder One
+ indices = [model.indexFromHandle(hNewFolderOne)]
+ ctxMenu = _TreeContextMenu(projTree, model, nodeOne, indices)
+ ctxMenu.buildSingleSelectMenu()
# Click no on the dialog
with monkeypatch.context() as mp:
- mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No)
+ mp.setattr(QMessageBox, "result", lambda *a: QtRejected)
ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT)
- assert SHARED.project.tree[hNewFolderOne].isFolderType() # type: ignore
+ assert nodeOne.item.isFolderType()
# Convert the first folder to a document
+ assert nodeOne.item.isFolderType()
ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT)
- assert SHARED.project.tree[hNewFolderOne].isFileType() # type: ignore
- assert SHARED.project.tree[hNewFolderOne].isDocumentLayout() # type: ignore
+ assert nodeOne.item.isFileType()
+ assert nodeOne.item.isDocumentLayout()
- nwItem = SHARED.project.tree[hNewFolderTwo]
- assert isinstance(nwItem, NWItem)
- ctxMenu = _TreeContextMenu(projTree, nwItem)
- ctxMenu.buildSingleSelectMenu(False)
+ # Select Folder Two
+ indices = [model.indexFromHandle(hNewFolderTwo)]
+ ctxMenu = _TreeContextMenu(projTree, model, nodeTwo, indices)
+ ctxMenu.buildSingleSelectMenu()
# Convert the second folder to a note
ctxMenu._convertFolderToFile(nwItemLayout.NOTE)
- assert SHARED.project.tree[hNewFolderTwo].isFileType() # type: ignore
- assert SHARED.project.tree[hNewFolderTwo].isNoteLayout() # type: ignore
+ assert nodeTwo.item.isFileType()
+ assert nodeTwo.item.isNoteLayout()
# Direct Edit Functions, Multi
# ============================
- nwItem = SHARED.project.tree[hCharNote]
- assert isinstance(nwItem, NWItem)
- ctxMenu = _TreeContextMenu(projTree, nwItem)
- ctxMenu.buildMultiSelectMenu([hCharNote, hNovelNote, hSubNote])
+ node = tree.nodes[hCharNote]
+ indices = [
+ model.indexFromHandle(hCharNote),
+ model.indexFromHandle(hNovelNote),
+ model.indexFromHandle(hSubNote),
+ ]
+ ctxMenu = _TreeContextMenu(projTree, model, node, indices)
+ ctxMenu.buildMultiSelectMenu()
- projTree.clearSelection()
- projTree._getTreeItem(hCharNote).setSelected(True)
- projTree._getTreeItem(hNovelNote).setSelected(True)
- projTree._getTreeItem(hSubNote).setSelected(True)
+ # projTree.clearSelection()
+ # projTree._getTreeItem(hCharNote).setSelected(True)
+ # projTree._getTreeItem(hNovelNote).setSelected(True)
+ # projTree._getTreeItem(hSubNote).setSelected(True)
+
+ nodeCNote = tree.nodes[hCharNote]
+ nodeNNote = tree.nodes[hNovelNote]
+ nodeSNote = tree.nodes[hSubNote]
# Item Active
- assert SHARED.project.tree[hCharNote].isActive is True # type: ignore
- assert SHARED.project.tree[hNovelNote].isActive is False # type: ignore
- assert SHARED.project.tree[hSubNote].isActive is True # type: ignore
+ assert nodeCNote.item.isActive is True
+ assert nodeNNote.item.isActive is False
+ assert nodeSNote.item.isActive is True
ctxMenu._iterItemActive(False)
- assert SHARED.project.tree[hCharNote].isActive is False # type: ignore
- assert SHARED.project.tree[hNovelNote].isActive is False # type: ignore
- assert SHARED.project.tree[hSubNote].isActive is False # type: ignore
+ assert nodeCNote.item.isActive is False
+ assert nodeNNote.item.isActive is False
+ assert nodeSNote.item.isActive is False
ctxMenu._iterItemActive(True)
- assert SHARED.project.tree[hCharNote].isActive is True # type: ignore
- assert SHARED.project.tree[hNovelNote].isActive is True # type: ignore
- assert SHARED.project.tree[hSubNote].isActive is True # type: ignore
+ assert nodeCNote.item.isActive is True
+ assert nodeNNote.item.isActive is True
+ assert nodeSNote.item.isActive is True
# Item Status
- assert SHARED.project.tree[hCharNote].itemStatus == "s000000" # type: ignore
- assert SHARED.project.tree[hNovelNote].itemStatus == "s000001" # type: ignore
- assert SHARED.project.tree[hSubNote].itemStatus == "s000000" # type: ignore
+ assert nodeCNote.item.itemStatus == "s000000"
+ assert nodeNNote.item.itemStatus == "s000001"
+ assert nodeSNote.item.itemStatus == "s000000"
ctxMenu._iterSetItemStatus("s000003")
- assert SHARED.project.tree[hCharNote].itemStatus == "s000000" # type: ignore
- assert SHARED.project.tree[hNovelNote].itemStatus == "s000003" # type: ignore
- assert SHARED.project.tree[hSubNote].itemStatus == "s000003" # type: ignore
+ assert nodeCNote.item.itemStatus == "s000000"
+ assert nodeNNote.item.itemStatus == "s000003"
+ assert nodeSNote.item.itemStatus == "s000003"
# Item Importance
- assert SHARED.project.tree[hCharNote].itemImport == "i000004" # type: ignore
- assert SHARED.project.tree[hNovelNote].itemImport == "i000005" # type: ignore
- assert SHARED.project.tree[hSubNote].itemImport == "i000004" # type: ignore
+ assert nodeCNote.item.itemImport == "i000004"
+ assert nodeNNote.item.itemImport == "i000005"
+ assert nodeSNote.item.itemImport == "i000004"
ctxMenu._iterSetItemImport("i000007")
- assert SHARED.project.tree[hCharNote].itemImport == "i000007" # type: ignore
- assert SHARED.project.tree[hNovelNote].itemImport == "i000005" # type: ignore
- assert SHARED.project.tree[hSubNote].itemImport == "i000004" # type: ignore
-
- # Move to Trash
- assert SHARED.project.tree[hCharNote].itemRoot == C.hCharRoot # type: ignore
- assert SHARED.project.tree[hNovelNote].itemRoot == C.hNovelRoot # type: ignore
- assert SHARED.project.tree[hSubNote].itemRoot == C.hNovelRoot # type: ignore
- ctxMenu._iterMoveToTrash()
- assert SHARED.project.tree[hCharNote].itemRoot == hTrashRoot # type: ignore
- assert SHARED.project.tree[hNovelNote].itemRoot == hTrashRoot # type: ignore
- assert SHARED.project.tree[hSubNote].itemRoot == hTrashRoot # type: ignore
-
- # Permanently Delete Menu
- nwItem = SHARED.project.tree[hCharNote]
- assert isinstance(nwItem, NWItem)
- ctxMenu = _TreeContextMenu(projTree, nwItem)
- ctxMenu.buildMultiSelectMenu([hCharNote, hNovelNote, hSubNote])
- actions = [x.text() for x in ctxMenu.actions() if x.text()]
- assert actions == [
- "Set Active to ...", "Set Importance to ...", "Delete Permanently",
- ]
-
- # Permanently Delete
- ctxMenu._iterPermDelete()
- assert SHARED.project.tree[hCharNote] is None
- assert SHARED.project.tree[hNovelNote] is None
- assert SHARED.project.tree[hSubNote] is None
+ assert nodeCNote.item.itemImport == "i000007"
+ assert nodeNNote.item.itemImport == "i000005"
+ assert nodeSNote.item.itemImport == "i000004"
# qtbot.stop()
@pytest.mark.gui
-@pytest.mark.skip
def testGuiProjTree_Templates(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the templates feature of the project tree."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
# Create a project
- buildTestProject(nwGUI, projPath)
- nwGUI.openProject(projPath)
- nwGUI._switchFocus(nwFocus.TREE)
- nwGUI.show()
-
- project = SHARED.project
-
projView = nwGUI.projView
projTree = projView.projTree
projBar = projView.projBar
+ project = SHARED.project
+ tree = project.tree
+
+ # Create a project
+ buildTestProject(nwGUI, projPath)
+ trash = tree.trash
+ assert trash is not None
+ assert len(tree) == 9
+ assert [n.item.itemName for n in tree.model.root.allChildren()] == [
+ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
+ "Plot", "Characters", "Locations", "Trash",
+ ]
# Handles for new objects
- hTemplatesRoot = "0000000000010"
- hSceneTemplate = "0000000000011"
- hNoteTemplate = "0000000000012"
- hNewScene = "0000000000013"
- hNewCharacter = "0000000000014"
+ hTemplatesRoot = "0000000000011"
+ hSceneTemplate = "0000000000012"
+ hNoteTemplate = "0000000000013"
+ hNewScene = "0000000000014"
+ hNewCharacter = "0000000000015"
# Add template folder
projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE)
- nwTemplateRoot = project.tree[hTemplatesRoot]
+ nwTemplateRoot = tree[hTemplatesRoot]
assert nwTemplateRoot is not None
assert nwTemplateRoot.itemName == "Templates"
# Add a scene template
projTree.setSelectedHandle(hTemplatesRoot)
projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False)
- nwSceneTemplate = project.tree[hSceneTemplate]
+ nwSceneTemplate = tree[hSceneTemplate]
assert nwSceneTemplate is not None
assert nwSceneTemplate.itemName == "New Scene"
assert projBar.mTemplates.actions()[0].text() == "New Scene"
# Rename the scene template
- with qtbot.waitSignal(projTree.itemRefreshed, timeout=1000) as signal:
- projTree.renameTreeItem(hSceneTemplate, name="Scene")
- assert signal.args[0] == hSceneTemplate
- assert signal.args[1].itemName == "Scene"
- assert projBar.mTemplates.actions()[0].text() == "Scene"
+ projView.renameTreeItem(hSceneTemplate, name="Scene")
+ assert projBar.mTemplates.actions()[0].text() == "Scene"
# Add a note template
projTree.setSelectedHandle(hTemplatesRoot)
@@ -1478,11 +1400,8 @@ def testGuiProjTree_Templates(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
assert projBar.mTemplates.actions()[1].text() == "New Note"
# Rename the note template
- with qtbot.waitSignal(projTree.itemRefreshed, timeout=1000) as signal:
- projTree.renameTreeItem(hNoteTemplate, name="Note")
- assert signal.args[0] == hNoteTemplate
- assert signal.args[1].itemName == "Note"
- assert projBar.mTemplates.actions()[1].text() == "Note"
+ projView.renameTreeItem(hNoteTemplate, name="Note")
+ assert projBar.mTemplates.actions()[1].text() == "Note"
# Add new content to template files
(projPath / "content" / f"{hSceneTemplate}.nwd").write_text("### Scene\n\n@pov: Jane\n\n")
@@ -1505,13 +1424,14 @@ def testGuiProjTree_Templates(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
assert project.storage.getDocument(hNewCharacter).readDocument() == "# Jane\n\n@tag: Jane\n\n"
# Remove the templates
- with qtbot.waitSignal(projTree.itemRefreshed, timeout=1000) as signal:
- assert projBar.mTemplates.menuAction().isVisible() is True
- assert len(projBar.mTemplates.actions()) == 2
- projTree.moveItemToTrash(hNoteTemplate)
- assert len(projBar.mTemplates.actions()) == 1
- projTree.moveItemToTrash(hSceneTemplate)
- assert len(projBar.mTemplates.actions()) == 0
- assert projBar.mTemplates.menuAction().isVisible() is False
+ assert projBar.mTemplates.menuAction().isVisible() is True
+ assert len(projBar.mTemplates.actions()) == 2
+ assert trash.childCount() == 0
+ projTree.processDeleteRequest([hNoteTemplate])
+ assert trash.childCount() == 1
+ assert len(projBar.mTemplates.actions()) == 1
+ projTree.processDeleteRequest([hSceneTemplate])
+ assert len(projBar.mTemplates.actions()) == 0
+ assert projBar.mTemplates.menuAction().isVisible() is False
# qtbot.stop()