diff --git a/novelwriter/core/itemmodel.py b/novelwriter/core/itemmodel.py index 8042a3f22..25a587966 100644 --- a/novelwriter/core/itemmodel.py +++ b/novelwriter/core/itemmodel.py @@ -42,6 +42,7 @@ logger = logging.getLogger(__name__) +INV_ROOT = "invisibleRoot" C_FACTOR = 0x0100 C_LABEL_TEXT = 0x0000 | Qt.ItemDataRole.DisplayRole @@ -244,7 +245,7 @@ def _updateRelationships(self, child: ProjectNode) -> None: child.item.setParent(self._item.itemHandle) child.item.setRoot(self._item.itemRoot) child.item.setClassDefaults(self._item.itemClass) - child._flags |= Qt.ItemFlag.ItemIsDragEnabled + child._flags = NODE_FLAGS | Qt.ItemFlag.ItemIsDragEnabled else: child.item.setParent(None) child.item.setRoot(child.item.itemHandle) @@ -259,12 +260,12 @@ class ProjectModel(QAbstractItemModel): def __init__(self, tree: NWTree) -> None: super().__init__() self._tree = tree - self._root = ProjectNode(NWItem(tree._project, "invisibleRoot")) + self._root = ProjectNode(NWItem(tree._project, INV_ROOT)) self._root.item.setName("Invisible Root") logger.debug("Ready: ProjectModel") return - def __del__(self) -> None: + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: ProjectModel") return @@ -390,10 +391,6 @@ def indexFromNode(self, node: ProjectNode, column: int = 0) -> QModelIndex: """Get the index representing a node in the model.""" return self.createIndex(node.row(), column, node) - def rootIndex(self) -> QModelIndex: - """Get the index representing the root.""" - return self.createIndex(0, 0, self._root) - ## # Model Edit ## @@ -432,15 +429,6 @@ def internalMove(self, index: QModelIndex, step: int) -> None: self.endMoveRows() return - def trashSelection(self, indices: list[QModelIndex]) -> bool: - """Check if a selection of indices are all in trash or not.""" - for index in indices: - if index.isValid(): - node: ProjectNode = index.internalPointer() - if node.item.itemClass != nwItemClass.TRASH: - return False - return True - def multiMove(self, indices: list[QModelIndex], target: QModelIndex, pos: int = -1) -> None: """Move multiple items to a new location.""" if target.isValid(): @@ -458,7 +446,7 @@ def multiMove(self, indices: list[QModelIndex], target: QModelIndex, pos: int = if node.item.isRootType() is False and handle not in handles: pruned.append(node) handles.add(handle) - for node in pruned: + for node in (reversed(pruned) if pos >= 0 else pruned): if node.item.itemParent not in handles: index = self.indexFromNode(node) if temp := self.removeChild(index.parent(), index.row()): @@ -485,3 +473,12 @@ def allExpanded(self) -> list[QModelIndex]: if node._item.isExpanded: expanded.append(self.createIndex(node.row(), 0, node)) return expanded + + def trashSelection(self, indices: list[QModelIndex]) -> bool: + """Check if a selection of indices are all in trash or not.""" + for index in indices: + if index.isValid(): + node: ProjectNode = index.internalPointer() + if node.item.itemClass != nwItemClass.TRASH: + return False + return True diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py index 4e16dd53b..013a0501a 100644 --- a/novelwriter/core/project.py +++ b/novelwriter/core/project.py @@ -67,6 +67,11 @@ class NWProjectState(Enum): class NWProject: + __slots__ = ( + "_options", "_storage", "_data", "_tree", "_index", "_session", + "_langData", "_changed", "_valid", "_state", "tr", + ) + def __init__(self) -> None: # Core Elements @@ -433,7 +438,6 @@ def closeProject(self, idleTime: float = 0.0) -> None: self._tree.writeToCFile() self._session.appendSession(idleTime) self._storage.closeSession() - self._lockedBy = None return def backupProject(self, doNotify: bool) -> bool: diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py index a39b289eb..c2afe7693 100644 --- a/novelwriter/core/tree.py +++ b/novelwriter/core/tree.py @@ -66,10 +66,7 @@ class NWTree: also used for file names. """ - __slots__ = ( - "_project", "_tree", "_order", "_roots", - "_model", "_items", "_nodes", "_trash", "_changed", - ) + __slots__ = ("_project", "_model", "_items", "_nodes", "_trash") def __init__(self, project: NWProject) -> None: self._project = project diff --git a/tests/reference/coreProject_NewFileFolder_nwProject.nwx b/tests/reference/coreProject_NewFileFolder_nwProject.nwx index 40dd54eeb..a99d83336 100644 --- a/tests/reference/coreProject_NewFileFolder_nwProject.nwx +++ b/tests/reference/coreProject_NewFileFolder_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -39,7 +39,7 @@ - New Chapter + New Folder diff --git a/tests/reference/coreProject_NewRoot_nwProject.nwx b/tests/reference/coreProject_NewRoot_nwProject.nwx index e284a2db8..a83eb7317 100644 --- a/tests/reference/coreProject_NewRoot_nwProject.nwx +++ b/tests/reference/coreProject_NewRoot_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -39,7 +39,7 @@ - New Chapter + New Folder diff --git a/tests/reference/coreTools_DocDuplicator_nwProject.nwx b/tests/reference/coreTools_DocDuplicator_nwProject.nwx index 1157b376c..30c62652f 100644 --- a/tests/reference/coreTools_DocDuplicator_nwProject.nwx +++ b/tests/reference/coreTools_DocDuplicator_nwProject.nwx @@ -1,5 +1,5 @@ - + New Project Jane Doe @@ -39,7 +39,7 @@ - New Chapter + New Folder @@ -63,7 +63,7 @@ - New Chapter + New Folder @@ -83,7 +83,7 @@ - New Chapter + New Folder diff --git a/tests/reference/guiEditor_Main_Final_nwProject.nwx b/tests/reference/guiEditor_Main_Final_nwProject.nwx index e90a04781..3b05335b4 100644 --- a/tests/reference/guiEditor_Main_Final_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Final_nwProject.nwx @@ -1,6 +1,6 @@ - - + + New Project Jane Doe @@ -39,7 +39,7 @@ - New Chapter + New Folder diff --git a/tests/reference/guiEditor_Main_Initial_nwProject.nwx b/tests/reference/guiEditor_Main_Initial_nwProject.nwx index af5358516..c265fa518 100644 --- a/tests/reference/guiEditor_Main_Initial_nwProject.nwx +++ b/tests/reference/guiEditor_Main_Initial_nwProject.nwx @@ -39,7 +39,7 @@ - New Chapter + New Folder diff --git a/tests/test_core/test_core_itemmodel.py b/tests/test_core/test_core_itemmodel.py new file mode 100644 index 000000000..5d26d2eaa --- /dev/null +++ b/tests/test_core/test_core_itemmodel.py @@ -0,0 +1,623 @@ +""" +novelWriter – Item Model Tester +=============================== + +This file is a part of novelWriter +Copyright 2018–2024, Veronica Berglyd Olsen + +This program is free software: you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation, either version 3 of the License, or +(at your option) any later version. + +This program is distributed in the hope that it will be useful, but +WITHOUT ANY WARRANTY; without even the implied warranty of +MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU +General Public License for more details. + +You should have received a copy of the GNU General Public License +along with this program. If not, see . +""" +from __future__ import annotations + +import pytest + +from PyQt5.QtCore import QMimeData, QModelIndex, Qt + +from novelwriter.common import decodeMimeHandles +from novelwriter.constants import nwConst +from novelwriter.core.item import NWItem +from novelwriter.core.itemmodel import INV_ROOT, NODE_FLAGS, ProjectNode +from novelwriter.core.project import NWProject +from novelwriter.enum import nwItemLayout, nwItemType + +from tests.tools import buildTestProject + + +@pytest.mark.core +def testCoreItemModel_ProjectNode_Root(mockGUI): + """Test the project node class for the root.""" + project = NWProject() + root = ProjectNode(NWItem(project, INV_ROOT)) + + # Defaults + assert bool(root) is True + assert root.item.itemHandle == INV_ROOT + assert repr(root) == "" + assert root.children == [] + assert root.count == 0 + + # Data + assert root.row() == 0 + assert root.childCount() == 0 + assert root.parent() is None + assert root.child(0) is None + assert root.allChildren() == [] + + +@pytest.mark.core +def testCoreItemModel_ProjectNode_Children(mockGUI, mockRnd, fncPath): + """Test the project node class for children.""" + project = NWProject() + mockRnd.reset() + buildTestProject(project, fncPath) + root = project.tree.model.root + + # Check Root + assert root.childCount() == 4 + assert root.count == 9 + + # Check Children + child0 = root.child(0) + child1 = root.child(1) + child2 = root.child(2) + child3 = root.child(3) + + assert child0 is not None + assert child1 is not None + assert child2 is not None + assert child3 is not None + + assert child0.item.itemName == "Novel" + assert child1.item.itemName == "Plot" + assert child2.item.itemName == "Characters" + assert child3.item.itemName == "Locations" + + assert child0.childCount() == 2 + assert child1.childCount() == 0 + assert child2.childCount() == 0 + assert child3.childCount() == 0 + + # Check Novel Content + child00 = child0.child(0) + child01 = child0.child(1) + + assert child00 is not None + assert child01 is not None + + assert child00.item.itemName == "Title Page" + assert child01.item.itemName == "New Folder" + + child010 = child01.child(0) + child011 = child01.child(1) + + assert child010 is not None + assert child011 is not None + + assert child010.item.itemName == "New Chapter" + assert child011.item.itemName == "New Scene" + + # Check Relationships + assert child0.parent() is root + assert child1.parent() is root + assert child2.parent() is root + assert child3.parent() is root + assert child01.parent() is child0 + assert child010.parent() is child01 + assert child011.parent() is child01 + + +@pytest.mark.core +def testCoreItemModel_ProjectNode_Modify(mockGUI, mockRnd, fncPath): + """Test modifying project nodes.""" + project = NWProject() + mockRnd.reset() + buildTestProject(project, fncPath) + root = project.tree.model.root + + # Novel folder + novel = root.child(0) + assert novel is not None + assert novel.item.itemName == "Novel" + + # Chapter folder + folder = novel.child(1) + assert folder is not None + assert folder.item.itemName == "New Folder" + + # Append scene + project.tree.create("Scene 1", folder.item.itemHandle, nwItemType.FILE) + scene1 = folder.child(2) + assert scene1 is not None + assert scene1.item.itemName == "Scene 1" + + # Insert scene + project.tree.create("Scene 2", folder.item.itemHandle, nwItemType.FILE, pos=1) + scene2 = folder.child(1) + assert scene2 is not None + assert scene2.item.itemName == "Scene 2" + + # Scene 1 should now have moved + assert scene1.row() == 3 + assert [n.item.itemName for n in folder.children] == [ + "New Chapter", "Scene 2", "New Scene", "Scene 1", + ] + + # Check that defaults have been set + assert scene1.item.itemParent == folder.item.itemHandle + assert scene2.item.itemParent == folder.item.itemHandle + + assert scene1.item.itemClass == folder.item.itemClass + assert scene2.item.itemClass == folder.item.itemClass + + assert scene1.item.itemLayout == nwItemLayout.DOCUMENT + assert scene2.item.itemLayout == nwItemLayout.DOCUMENT + + # Move Scene 2, invalid position is ignored + folder.moveChild(1, -1) + scene2 = folder.child(1) + assert scene2 is not None + assert scene2.item.itemName == "Scene 2" + + # Move Scene 2, past end is ignored + folder.moveChild(1, 20) + scene2 = folder.child(1) + assert scene2 is not None + assert scene2.item.itemName == "Scene 2" + + # Move Scene 2, last position is ok + folder.moveChild(1, 3) + scene2 = folder.child(3) + assert scene2 is not None + assert scene2.item.itemName == "Scene 2" + assert [n.item.itemName for n in folder.children] == [ + "New Chapter", "New Scene", "Scene 1", "Scene 2", + ] + + # Remove original scene, invalid position + removed = folder.takeChild(-1) + assert removed is None + assert folder.childCount() == 4 + + # Remove original scene, past end + removed = folder.takeChild(20) + assert removed is None + assert folder.childCount() == 4 + + # Remove original scene, ok + removed = folder.takeChild(1) + assert removed is not None + assert removed.item.itemName == "New Scene" + assert folder.childCount() == 3 + assert [n.item.itemName for n in folder.children] == [ + "New Chapter", "Scene 1", "Scene 2", + ] + + +@pytest.mark.core +def testCoreItemModel_ProjectNode_Data(mockGUI, mockRnd, fncPath): + """Test data access from project nodes.""" + project = NWProject() + mockRnd.reset() + buildTestProject(project, fncPath) + root = project.tree.model.root + + # Novel folder + novel = root.child(0) + assert novel is not None + assert novel.item.itemName == "Novel" + + # Chapter folder + folder = novel.child(1) + assert folder is not None + assert folder.item.itemName == "New Folder" + + # Scene document + scene = folder.child(1) + assert scene is not None + assert scene.item.itemName == "New Scene" + + # Check Data + assert novel.data(0, Qt.ItemDataRole.DisplayRole) == "Novel" + assert novel.data(1, Qt.ItemDataRole.DisplayRole) == "9" + assert scene.data(0, Qt.ItemDataRole.DisplayRole) == "New Scene" + assert scene.data(1, Qt.ItemDataRole.DisplayRole) == "2" + + assert novel.data(2, Qt.ItemDataRole.ToolTipRole) == "" + assert novel.data(3, Qt.ItemDataRole.ToolTipRole) == "New" + assert scene.data(2, Qt.ItemDataRole.ToolTipRole) == "Active" + assert scene.data(3, Qt.ItemDataRole.ToolTipRole) == "New" + + # Check Flags + assert novel.flags() == NODE_FLAGS + assert scene.flags() == NODE_FLAGS | Qt.ItemFlag.ItemIsDragEnabled + + +@pytest.mark.core +def testCoreItemModel_ProjectModel_Interface(mockGUI, mockRnd, fncPath): + """Test the model interface for the project model.""" + project = NWProject() + mockRnd.reset() + buildTestProject(project, fncPath) + model = project.tree.model + + # Init + assert isinstance(model.root, ProjectNode) + assert model.root.item.itemHandle == INV_ROOT + + # Indices + rootIdx = QModelIndex() + novelIdx = model.index(0, 0, rootIdx) + folderIdx = model.index(1, 0, novelIdx) + sceneIdx = model.index(1, 0, folderIdx) + invalidIdx = model.index(-1, -1) + + assert rootIdx.isValid() is False + assert novelIdx.isValid() is True + assert folderIdx.isValid() is True + assert sceneIdx.isValid() is True + assert invalidIdx.isValid() is False + + # Columns and Rows + assert model.rowCount(rootIdx) == 4 + assert model.columnCount(rootIdx) == 4 + assert model.rowCount(novelIdx) == 2 + assert model.columnCount(novelIdx) == 4 + + # Parent of Novel + parent = model.parent(novelIdx) + assert parent.row() == 0 + assert parent.column() == 0 + assert parent.internalPointer() is model.root + + # Parent of Root + parent = model.parent(rootIdx) + assert parent.isValid() is False + + # Data and Flags + assert model.data(novelIdx, Qt.ItemDataRole.DisplayRole) == "Novel" + assert model.data(sceneIdx, Qt.ItemDataRole.DisplayRole) == "New Scene" + assert model.data(invalidIdx, Qt.ItemDataRole.DisplayRole) is None + assert model.flags(novelIdx) == NODE_FLAGS + assert model.flags(sceneIdx) == NODE_FLAGS | Qt.ItemFlag.ItemIsDragEnabled + assert model.flags(invalidIdx) == Qt.ItemFlag.NoItemFlags + + +@pytest.mark.core +def testCoreItemModel_ProjectModel_DragNDrop(mockGUI, mockRnd, fncPath): + """Test drag and drop for the project model.""" + project = NWProject() + mockRnd.reset() + buildTestProject(project, fncPath) + model = project.tree.model + + # Nodes + novel = model.root.child(0) + assert novel is not None + folder = novel.child(1) + assert folder is not None + chapter = folder.child(0) + assert chapter is not None + scene = folder.child(1) + assert scene is not None + + assert novel.item.itemName == "Novel" + assert folder.item.itemName == "New Folder" + assert chapter.item.itemName == "New Chapter" + assert scene.item.itemName == "New Scene" + + # Indices + rootIdx = QModelIndex() + novelIdx = model.index(0, 0, rootIdx) + folderIdx = model.index(1, 0, novelIdx) + chapterIdx = model.index(0, 0, folderIdx) + sceneIdx = model.index(1, 0, folderIdx) + invalidIdx = model.index(-1, -1) + + # Only move is allowed + assert model.supportedDragActions() == Qt.DropAction.MoveAction + assert model.supportedDropActions() == Qt.DropAction.MoveAction + + # Only handles are dragged and dropped + assert model.mimeTypes() == [nwConst.MIME_HANDLE] + + # Get mime data + novelMime = model.mimeData([novelIdx]) + assert decodeMimeHandles(novelMime) == [novel.item.itemHandle] + + sceneMime = model.mimeData([sceneIdx]) + assert decodeMimeHandles(sceneMime) == [scene.item.itemHandle] + + sceneChapterMime = model.mimeData([chapterIdx, sceneIdx]) + assert decodeMimeHandles(sceneChapterMime) == [ + chapter.item.itemHandle, scene.item.itemHandle, + ] + + multiMime = model.mimeData([novelIdx, folderIdx, sceneIdx, invalidIdx]) + assert decodeMimeHandles(multiMime) == [ + novel.item.itemHandle, folder.item.itemHandle, scene.item.itemHandle, + ] + + # Check that drop is possible + invalidMime = QMimeData() + invalidMime.setData("plain/text", b"foobar") + + assert model.canDropMimeData(invalidMime, Qt.DropAction.MoveAction, 0, 0, novelIdx) is False + assert model.canDropMimeData(sceneMime, Qt.DropAction.MoveAction, 0, 0, novelIdx) is True + + # Drop the scene on the novel folder + assert [n.item.itemName for n in model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", + ] + assert model.dropMimeData(invalidMime, Qt.DropAction.MoveAction, 0, 0, novelIdx) is False + assert model.dropMimeData(sceneChapterMime, Qt.DropAction.MoveAction, 0, 0, novelIdx) is True + assert [n.item.itemName for n in model.root.allChildren()] == [ + "Novel", "New Chapter", "New Scene", "Title Page", "New Folder", + "Plot", "Characters", "Locations", + ] + + +@pytest.mark.core +def testCoreItemModel_ProjectModel_Data(mockGUI, mockRnd, fncPath): + """Test data access for the project model.""" + project = NWProject() + mockRnd.reset() + buildTestProject(project, fncPath) + model = project.tree.model + + # Nodes + root = model.root + novel = root.child(0) + assert novel is not None + folder = novel.child(1) + assert folder is not None + chapter = folder.child(0) + assert chapter is not None + scene = folder.child(1) + assert scene is not None + + assert novel.item.itemName == "Novel" + assert folder.item.itemName == "New Folder" + assert chapter.item.itemName == "New Chapter" + assert scene.item.itemName == "New Scene" + + # Indices + rootIdx = QModelIndex() + novelIdx = model.index(0, 0, rootIdx) + folderIdx = model.index(1, 0, novelIdx) + chapterIdx = model.index(0, 0, folderIdx) + sceneIdx = model.index(1, 0, folderIdx) + invalidIdx = model.index(-1, -1) + + # Check Rows + assert model.row(rootIdx) == -1 + assert model.row(novelIdx) == 0 + assert model.row(folderIdx) == 1 + assert model.row(chapterIdx) == 0 + assert model.row(sceneIdx) == 1 + assert model.row(invalidIdx) == -1 + + # Check Nodes + assert model.node(rootIdx) is None + assert model.node(novelIdx) is novel + assert model.node(folderIdx) is folder + assert model.node(chapterIdx) is chapter + assert model.node(sceneIdx) is scene + assert model.node(invalidIdx) is None + + nodes = model.nodes([novelIdx, folderIdx, chapterIdx, sceneIdx]) + assert nodes[0] is novel + assert nodes[1] is folder + assert nodes[2] is chapter + assert nodes[3] is scene + + # Index from Handle + assert model.indexFromHandle(None).isValid() is False + assert model.node(model.indexFromHandle(novel.item.itemHandle)) is novel + assert model.node(model.indexFromHandle(folder.item.itemHandle)) is folder + assert model.node(model.indexFromHandle(chapter.item.itemHandle)) is chapter + assert model.node(model.indexFromHandle(scene.item.itemHandle)) is scene + + # Index from Node + assert model.indexFromHandle(None).isValid() is False + assert model.node(model.indexFromNode(novel)) is novel + assert model.node(model.indexFromNode(folder)) is folder + assert model.node(model.indexFromNode(chapter)) is chapter + assert model.node(model.indexFromNode(scene)) is scene + + +@pytest.mark.core +def testCoreItemModel_ProjectModel_Edit(qtbot, mockGUI, mockRnd, fncPath): + """Test editing the project model.""" + project = NWProject() + mockRnd.reset() + buildTestProject(project, fncPath) + model = project.tree.model + + # Nodes + root = model.root + novel = root.child(0) + assert novel is not None + title = novel.child(0) + assert title is not None + folder = novel.child(1) + assert folder is not None + chapter = folder.child(0) + assert chapter is not None + scene = folder.child(1) + assert scene is not None + + assert novel.item.itemName == "Novel" + assert title.item.itemName == "Title Page" + assert folder.item.itemName == "New Folder" + assert chapter.item.itemName == "New Chapter" + assert scene.item.itemName == "New Scene" + + # Indices + novelIdx = model.indexFromNode(novel) + titleIdx = model.indexFromNode(title) + folderIdx = model.indexFromNode(folder) + chapterIdx = model.indexFromNode(chapter) + sceneIdx = model.indexFromNode(scene) + invalidIdx = model.index(-1, -1) + + # Initial Order + assert [n.item.itemName for n in model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", + ] + + # Remove Child, invalid index + assert model.removeChild(novelIdx, -1) is None + assert model.removeChild(novelIdx, 99) is None + + # Remove Child + with qtbot.waitSignal(model.rowsAboutToBeRemoved) as signal: + child = model.removeChild(novelIdx, titleIdx.row()) + assert signal.args[0].internalPointer().item.itemName == "Novel" + assert signal.args[1] == 0 + assert signal.args[2] == 0 + assert child is not None + assert child.item.itemName == "Title Page" + assert [n.item.itemName for n in model.root.allChildren()] == [ + "Novel", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", + ] + titleIdx = model.indexFromNode(title) + folderIdx = model.indexFromNode(folder) + chapterIdx = model.indexFromNode(chapter) + sceneIdx = model.indexFromNode(scene) + + # Insert Child + with qtbot.waitSignal(model.rowsAboutToBeInserted) as signal: + model.insertChild(child, novelIdx, 1) + assert signal.args[0].internalPointer().item.itemName == "Novel" + assert signal.args[1] == 1 + assert signal.args[2] == 1 + assert [n.item.itemName for n in model.root.allChildren()] == [ + "Novel", "New Folder", "New Chapter", "New Scene", "Title Page", + "Plot", "Characters", "Locations", + ] + titleIdx = model.indexFromNode(title) + folderIdx = model.indexFromNode(folder) + chapterIdx = model.indexFromNode(chapter) + sceneIdx = model.indexFromNode(scene) + + # Move it back with internal move + with qtbot.waitSignal(model.rowsAboutToBeMoved) as signal: + model.internalMove(titleIdx, -1) + assert signal.args[0].internalPointer().item.itemName == "Novel" + assert signal.args[1] == 1 + assert signal.args[2] == 1 + assert signal.args[3].internalPointer().item.itemName == "Novel" + assert signal.args[4] == 0 + assert [n.item.itemName for n in model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", + ] + titleIdx = model.indexFromNode(title) + folderIdx = model.indexFromNode(folder) + chapterIdx = model.indexFromNode(chapter) + sceneIdx = model.indexFromNode(scene) + + # Move Multiple, with parent + # Chapter and scene selection is flipped, but they should be deselected + # because folder is also selected, so their order should not change + model.multiMove([folderIdx, sceneIdx, chapterIdx, invalidIdx], novelIdx, 0) + assert [n.item.itemName for n in model.root.allChildren()] == [ + "Novel", "New Folder", "New Chapter", "New Scene", "Title Page", + "Plot", "Characters", "Locations", + ] + assert folder.parent() is novel + assert chapter.parent() is folder + assert scene.parent() is folder + + titleIdx = model.indexFromNode(title) + folderIdx = model.indexFromNode(folder) + chapterIdx = model.indexFromNode(chapter) + sceneIdx = model.indexFromNode(scene) + + # Move Multiple, siblings, altered order + # Chapter and scene selection is flipped, and they should now be reordered, + # and no longer in the folder + model.multiMove([sceneIdx, chapterIdx, invalidIdx], novelIdx, 0) + assert [n.item.itemName for n in model.root.allChildren()] == [ + "Novel", "New Scene", "New Chapter", "New Folder", "Title Page", + "Plot", "Characters", "Locations", + ] + assert folder.parent() is novel + assert chapter.parent() is novel + assert scene.parent() is novel + + +@pytest.mark.core +def testCoreItemModel_ProjectModel_Other(qtbot, mockGUI, mockRnd, fncPath): + """Test other methods of the project model.""" + project = NWProject() + mockRnd.reset() + buildTestProject(project, fncPath) + model = project.tree.model + + # Nodes + root = model.root + novel = root.child(0) + assert novel is not None + title = novel.child(0) + assert title is not None + folder = novel.child(1) + assert folder is not None + chapter = folder.child(0) + assert chapter is not None + scene = folder.child(1) + assert scene is not None + trash = project.tree.trash + assert trash is not None + + assert novel.item.itemName == "Novel" + assert title.item.itemName == "Title Page" + assert folder.item.itemName == "New Folder" + assert chapter.item.itemName == "New Chapter" + assert scene.item.itemName == "New Scene" + + assert [n.item.itemName for n in model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", + "Plot", "Characters", "Locations", "Trash", + ] + + # Indices + chapterIdx = model.indexFromNode(chapter) + sceneIdx = model.indexFromNode(scene) + trashIdx = model.indexFromNode(trash) + + # Expanded + assert model.allExpanded() == [] + novel.item.setExpanded(True) + folder.item.setExpanded(True) + assert [model.node(i) for i in model.allExpanded()] == [novel, folder] + + # Check Trash + assert model.trashSelection([chapterIdx, sceneIdx]) is False + model.multiMove([chapterIdx, sceneIdx], trashIdx) + assert [n.item.itemName for n in model.root.allChildren()] == [ + "Novel", "Title Page", "New Folder", + "Plot", "Characters", "Locations", "Trash", "New Chapter", "New Scene", + ] + + chapterIdx = model.indexFromNode(chapter) + sceneIdx = model.indexFromNode(scene) + assert model.trashSelection([chapterIdx, sceneIdx]) is True + + # Clear + model.clear() + assert [n.item.itemName for n in model.root.allChildren()] == [] diff --git a/tests/test_gui/test_gui_doceditor.py b/tests/test_gui/test_gui_doceditor.py index 5ca249ae7..b9bdb26fa 100644 --- a/tests/test_gui/test_gui_doceditor.py +++ b/tests/test_gui/test_gui_doceditor.py @@ -82,7 +82,7 @@ def testGuiEditor_Init(qtbot, nwGUI, projPath, ipsumText, mockRnd): assert docEditor.horizontalScrollBarPolicy() == QtScrollAsNeeded assert docEditor._typConf.typPadChar == nwUnicode.U_NBSP assert docEditor.docHeader.itemTitle.text() == ( - "Novel \u203a New Chapter \u203a New Scene" + "Novel \u203a New Folder \u203a New Scene" ) assert docEditor.docHeader._docOutline == {0: "### New Scene"} diff --git a/tests/tools.py b/tests/tools.py index c48e3216e..aaa11ef60 100644 --- a/tests/tools.py +++ b/tests/tools.py @@ -188,7 +188,7 @@ def buildTestProject(obj: object, projPath: Path) -> None: project.newRoot(nwItemClass.WORLD) tdHandle = project.newFile("Title Page", nrHandle) - cfHandle = project.newFolder("New Chapter", nrHandle) or "" + cfHandle = project.newFolder("New Folder", nrHandle) or "" cdHandle = project.newFile("New Chapter", cfHandle) sdHandle = project.newFile("New Scene", cfHandle)