Add multi selection to project tree (#1612)

This commit is contained in:
Veronica Berglyd Olsen
2023-11-22 19:00:25 +01:00
committed by GitHub
2 changed files with 837 additions and 537 deletions
File diff suppressed because it is too large Load Diff
+410 -185
View File
@@ -23,18 +23,20 @@ from __future__ import annotations
import pytest import pytest
from pathlib import Path from pathlib import Path
from novelwriter.core.project import NWProject
from tools import C, buildTestProject from tools import C, buildTestProject
from mocked import causeOSError from mocked import causeOSError
from PyQt5.QtGui import QDragMoveEvent, QDropEvent from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QMouseEvent
from PyQt5.QtCore import QMimeData, QPoint, QTimer, Qt from PyQt5.QtCore import QEvent, QMimeData, QPoint, QTimer, Qt
from PyQt5.QtWidgets import QMessageBox, QMenu, QTreeWidget, QTreeWidgetItem, QDialog from PyQt5.QtWidgets import QMessageBox, QMenu, QTreeWidget, QTreeWidgetItem, QDialog
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass, nwWidget
from novelwriter.guimain import GuiMain from novelwriter.guimain import GuiMain
from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView from novelwriter.core.item import NWItem
from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView, _TreeContextMenu
from novelwriter.dialogs.docmerge import GuiDocMerge from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.dialogs.editlabel import GuiEditLabel
@@ -50,81 +52,84 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn
project = SHARED.project project = SHARED.project
# Try to add item with no project # Try to add item with no project
assert projView.projTree.newTreeItem(nwItemType.FILE) is False assert projTree.newTreeItem(nwItemType.FILE) is False
# Create a project # Create a project
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
# No itemType set # No itemType set
projView.projTree.clearSelection() projTree.clearSelection()
assert projView.projTree.newTreeItem(None) is False assert projTree.newTreeItem(None) is False
# Root Items # Root Items
# ========== # ==========
# No class set # No class set
assert projView.projTree.newTreeItem(nwItemType.ROOT) is False assert projTree.newTreeItem(nwItemType.ROOT) is False
# Create root item # Create root item
assert projView.projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True assert projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True
assert "0000000000010" in project.tree assert "0000000000010" in project.tree
# File/Folder Items # File/Folder Items
# ================= # =================
# No location selected for new item # No location selected for new item
projView.projTree.clearSelection() projTree.clearSelection()
caplog.clear() caplog.clear()
assert projView.projTree.newTreeItem(nwItemType.FILE) is False assert projTree.newTreeItem(nwItemType.FILE) is False
assert projView.projTree.newTreeItem(nwItemType.FOLDER) is False assert projTree.newTreeItem(nwItemType.FOLDER) is False
assert "Did not find anywhere" in caplog.text assert "Did not find anywhere" in caplog.text
# Create new folder as child of Novel folder # Create new folder as child of Novel folder
projView.setSelectedHandle(C.hNovelRoot) projView.setSelectedHandle(C.hNovelRoot)
assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True assert projTree.newTreeItem(nwItemType.FOLDER) is True
assert project.tree["0000000000011"].itemParent == C.hNovelRoot # type: ignore assert project.tree["0000000000011"].itemParent == C.hNovelRoot # type: ignore
assert project.tree["0000000000011"].itemRoot == C.hNovelRoot # type: ignore assert project.tree["0000000000011"].itemRoot == C.hNovelRoot # type: ignore
assert project.tree["0000000000011"].itemClass == nwItemClass.NOVEL # type: ignore assert project.tree["0000000000011"].itemClass == nwItemClass.NOVEL # type: ignore
# Add a new file in the new folder # Add a new file in the new folder
projView.setSelectedHandle("0000000000011") projView.setSelectedHandle("0000000000011")
assert projView.projTree.newTreeItem(nwItemType.FILE) is True assert projTree.newTreeItem(nwItemType.FILE) is True
assert project.tree["0000000000012"].itemParent == "0000000000011" # type: ignore assert project.tree["0000000000012"].itemParent == "0000000000011" # type: ignore
assert project.tree["0000000000012"].itemRoot == C.hNovelRoot # type: ignore assert project.tree["0000000000012"].itemRoot == C.hNovelRoot # type: ignore
assert project.tree["0000000000012"].itemClass == nwItemClass.NOVEL # type: ignore assert project.tree["0000000000012"].itemClass == nwItemClass.NOVEL # type: ignore
# Add a new chapter next to the other new file # Add a new chapter next to the other new file
projView.setSelectedHandle("0000000000012") projView.setSelectedHandle("0000000000012")
assert projView.projTree.newTreeItem(nwItemType.FILE, hLevel=2) is True assert projTree.newTreeItem(nwItemType.FILE, hLevel=2) is True
assert project.tree["0000000000013"].itemParent == "0000000000011" # type: ignore assert project.tree["0000000000013"].itemParent == "0000000000011" # type: ignore
assert project.tree["0000000000013"].itemRoot == C.hNovelRoot # type: ignore assert project.tree["0000000000013"].itemRoot == C.hNovelRoot # type: ignore
assert project.tree["0000000000013"].itemClass == nwItemClass.NOVEL # type: ignore assert project.tree["0000000000013"].itemClass == nwItemClass.NOVEL # type: ignore
assert nwGUI.openDocument("0000000000013") assert nwGUI.openDocument("0000000000013")
assert nwGUI.docEditor.getText() == "## New Chapter\n\n" assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
assert projTree._getItemWordCount("0000000000013") == 2
# Add a new scene next to the other new file # Add a new scene next to the other new file
projView.setSelectedHandle("0000000000012") projView.setSelectedHandle("0000000000012")
assert projView.projTree.newTreeItem(nwItemType.FILE, hLevel=3) is True assert projTree.newTreeItem(nwItemType.FILE, hLevel=3) is True
assert project.tree["0000000000014"].itemParent == "0000000000011" # type: ignore assert project.tree["0000000000014"].itemParent == "0000000000011" # type: ignore
assert project.tree["0000000000014"].itemRoot == C.hNovelRoot # type: ignore assert project.tree["0000000000014"].itemRoot == C.hNovelRoot # type: ignore
assert project.tree["0000000000014"].itemClass == nwItemClass.NOVEL # type: ignore assert project.tree["0000000000014"].itemClass == nwItemClass.NOVEL # type: ignore
assert nwGUI.openDocument("0000000000014") assert nwGUI.openDocument("0000000000014")
assert nwGUI.docEditor.getText() == "### New Scene\n\n" assert nwGUI.docEditor.getText() == "### New Scene\n\n"
assert projTree._getItemWordCount("0000000000014") == 2
# Add a new file to the characters folder # Add a new file to the characters folder
projView.setSelectedHandle(C.hCharRoot) projView.setSelectedHandle(C.hCharRoot)
assert projView.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True assert projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True
assert project.tree["0000000000015"].itemParent == C.hCharRoot # type: ignore assert project.tree["0000000000015"].itemParent == C.hCharRoot # type: ignore
assert project.tree["0000000000015"].itemRoot == C.hCharRoot # type: ignore assert project.tree["0000000000015"].itemRoot == C.hCharRoot # type: ignore
assert project.tree["0000000000015"].itemClass == nwItemClass.CHARACTER # type: ignore assert project.tree["0000000000015"].itemClass == nwItemClass.CHARACTER # type: ignore
assert nwGUI.openDocument("0000000000015") assert nwGUI.openDocument("0000000000015")
assert nwGUI.docEditor.getText() == "# New Note\n\n" assert nwGUI.docEditor.getText() == "# New Note\n\n"
assert projTree._getItemWordCount("0000000000015") == 2
# Make sure the sibling folder bug trap works # Make sure the sibling folder bug trap works
projView.setSelectedHandle("0000000000013") projView.setSelectedHandle("0000000000013")
project.tree["0000000000013"].setParent(None) # This should not happen # type: ignore project.tree["0000000000013"].setParent(None) # This should not happen # type: ignore
caplog.clear() caplog.clear()
assert projView.projTree.newTreeItem(nwItemType.FILE) is False assert projTree.newTreeItem(nwItemType.FILE) is False
assert "Internal error" in caplog.text assert "Internal error" in caplog.text
project.tree["0000000000013"].setParent("0000000000011") # type: ignore project.tree["0000000000013"].setParent("0000000000011") # type: ignore
@@ -132,13 +137,17 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("", False)) mp.setattr(GuiEditLabel, "getLabel", lambda *a, **k: ("", False))
projView.setSelectedHandle("0000000000013") projView.setSelectedHandle("0000000000013")
assert projView.projTree.newTreeItem(nwItemType.FILE) is False assert projTree.newTreeItem(nwItemType.FILE) is False
# Get the trash folder # Get the trash folder
projView.projTree._addTrashRoot() with monkeypatch.context() as mp:
mp.setattr(NWProject, "trashFolder", lambda *a: None)
assert projTree._addTrashRoot() is None
assert isinstance(projTree._addTrashRoot(), QTreeWidgetItem)
trashHandle = project.trashFolder() trashHandle = project.trashFolder()
projView.setSelectedHandle(trashHandle) projView.setSelectedHandle(trashHandle)
assert projView.projTree.newTreeItem(nwItemType.FILE) is False assert projTree.newTreeItem(nwItemType.FILE) is False
assert "Cannot add new files or folders to the Trash folder" in caplog.text assert "Cannot add new files or folders to the Trash folder" in caplog.text
# Rename Item # Rename Item
@@ -157,14 +166,14 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn
# ============ # ============
# Also check error handling in reveal function # Also check error handling in reveal function
assert projView.projTree.revealNewTreeItem("abc") is False assert projTree.revealNewTreeItem("abc") is False
# Add an item that cannot be displayed in the tree # Add an item that cannot be displayed in the tree
nHandle = project.newFile("Test", None) # type: ignore nHandle = project.newFile("Test", None) # type: ignore
assert projView.projTree.revealNewTreeItem(nHandle) is False assert projTree.revealNewTreeItem(nHandle) is False
# Adding an invalid item directly to the tree should also fail # Adding an invalid item directly to the tree should also fail
assert projView.projTree._addTreeItem(None) is None assert projTree._addTreeItem(None) is None
# Clean up # Clean up
# qtbot.stop() # qtbot.stop()
@@ -182,7 +191,7 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
projTree = nwGUI.projView.projTree projTree = nwGUI.projView.projTree
# Try to move item with no project # Try to move item with no project
assert projView.projTree.moveTreeItem(1) is False assert projTree.moveTreeItem(1) is False
# Create a project # Create a project
buildTestProject(nwGUI, projPath) buildTestProject(nwGUI, projPath)
@@ -244,18 +253,13 @@ def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"0000000000010", "0000000000011", "0000000000012", "0000000000010", "0000000000011", "0000000000012",
] ]
# Move down again, and restore via undo # Move down again
projView.setSelectedHandle("0000000000011") projView.setSelectedHandle("0000000000011")
assert projTree.moveTreeItem(1) is True assert projTree.moveTreeItem(1) is True
assert projTree.getTreeFromHandle(C.hChapterDir) == [ assert projTree.getTreeFromHandle(C.hChapterDir) == [
C.hChapterDir, C.hChapterDoc, C.hSceneDoc, C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000012", "0000000000011", "0000000000010", "0000000000012", "0000000000011",
] ]
assert projTree.undoLastMove() is True
assert projTree.getTreeFromHandle(C.hChapterDir) == [
C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000011", "0000000000012",
]
# Root Folder # Root Folder
# =========== # ===========
@@ -519,125 +523,6 @@ def testGuiProjTree_EmptyTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mock
# END Test testGuiProjTree_EmptyTrash # END Test testGuiProjTree_EmptyTrash
@pytest.mark.gui
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))
monkeypatch.setattr(QMenu, "exec_", lambda *a: None)
# Create a project
buildTestProject(nwGUI, projPath)
# 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
projTree.setSelectedHandle(C.hCharRoot)
projTree.newTreeItem(nwItemType.FILE)
projTree.setSelectedHandle(C.hNovelRoot)
projTree.newTreeItem(nwItemType.FILE, isNote=True)
SHARED.project.newFile("SubNote", hNovelNote)
projTree.revealNewTreeItem(hSubNote)
assert SHARED.project.tree[hSubNote].itemParent == hNovelNote # type: ignore
def itemPos(tHandle):
return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center()
# No item under menu
assert projTree._openContextMenu(projTree.viewport().rect().bottomRight()) is False
# Generate the possible menu combinations
assert projTree._openContextMenu(itemPos(hTrashRoot)) is True
assert projTree._openContextMenu(itemPos(C.hNovelRoot)) is True
assert projTree._openContextMenu(itemPos(hNovelNote)) is True
assert projTree._openContextMenu(itemPos(C.hTitlePage)) is True
assert projTree._openContextMenu(itemPos(C.hChapterDir)) is True
assert projTree._openContextMenu(itemPos(C.hChapterDoc)) is True
assert projTree._openContextMenu(itemPos(C.hCharRoot)) is True
assert projTree._openContextMenu(itemPos(hCharNote)) is True
assert projTree._openContextMenu(itemPos(hNovelNote)) is True
# Check the keyboard shortcut handler as well
projTree.setSelectedHandle(C.hNovelRoot)
assert projTree.openContextOnSelected() is True
projTree.clearSelection()
assert projTree.openContextOnSelected() is False
# Direct Edit Functions
# =====================
# Trigger the dedicated functions the menu entries connect to
nwItem = SHARED.project.tree[hNovelNote]
assert nwItem is not None
# Toggle active flag
assert nwItem.isActive is True
projTree._toggleItemActive(hNovelNote)
assert nwItem.isActive is False
# Change item status
assert nwItem.itemStatus == "s000000"
projTree._changeItemStatus(hNovelNote, "s000001")
assert nwItem.itemStatus == "s000001"
# Change item importance
assert nwItem.itemImport == "i000004"
projTree._changeItemImport(hNovelNote, "i000005")
assert nwItem.itemImport == "i000005"
# Change item layout
assert nwItem.itemLayout == nwItemLayout.NOTE
projTree._changeItemLayout(hNovelNote, nwItemLayout.DOCUMENT)
assert nwItem.itemLayout == nwItemLayout.DOCUMENT
projTree._changeItemLayout(hNovelNote, nwItemLayout.NOTE)
assert nwItem.itemLayout == nwItemLayout.NOTE
# Convert Folders to Documents
# ============================
projView.setSelectedHandle(hNovelNote)
assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True
projView.setSelectedHandle(hNewFolderOne)
assert projView.projTree.newTreeItem(nwItemType.FILE) is True
projView.setSelectedHandle(hNovelNote)
assert projView.projTree.newTreeItem(nwItemType.FOLDER) is True
projView.setSelectedHandle(hNewFolderTwo)
assert projView.projTree.newTreeItem(nwItemType.FILE, isNote=True) is True
# Click no on the dialog
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT)
assert SHARED.project.tree[hNewFolderOne].isFolderType() # type: ignore
# Convert the first folder to a document
projTree._covertFolderToFile(hNewFolderOne, nwItemLayout.DOCUMENT)
assert SHARED.project.tree[hNewFolderOne].isFileType() # type: ignore
assert SHARED.project.tree[hNewFolderOne].isDocumentLayout() # type: ignore
# Convert the second folder to a note
projTree._covertFolderToFile(hNewFolderTwo, nwItemLayout.NOTE)
assert SHARED.project.tree[hNewFolderTwo].isFileType() # type: ignore
assert SHARED.project.tree[hNewFolderTwo].isNoteLayout() # type: ignore
# qtbot.stop()
# END Test testGuiProjTree_ContextMenu
@pytest.mark.gui @pytest.mark.gui
def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText): def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText):
"""Test the merge document function.""" """Test the merge document function."""
@@ -988,17 +873,14 @@ def testGuiProjTree_DragAndDrop(qtbot, monkeypatch, caplog, nwGUI: GuiMain, proj
mouse = Qt.MouseButton.LeftButton mouse = Qt.MouseButton.LeftButton
modifier = Qt.KeyboardModifier.NoModifier modifier = Qt.KeyboardModifier.NoModifier
# Move an item projTree.saveTreeOrder()
# The actual move is blocked, but the undo history should record treeOrder = SHARED.project.tree._order
# the event, although without an actual move implied
assert projTree._lastMove == {} # Move an item, but no selection
event = QDropEvent(nPos, action, mime, mouse, modifier) event = QDropEvent(nPos, action, mime, mouse, modifier)
projTree.dropEvent(event) projTree.dropEvent(event)
assert projTree._lastMove == { projTree.saveTreeOrder()
"item": projTree._getTreeItem(C.hSceneDoc), assert SHARED.project.tree._order == treeOrder
"parent": projTree._getTreeItem(C.hChapterDir),
"index": 1,
}
# Invalid location # Invalid location
caplog.clear() caplog.clear()
@@ -1006,23 +888,58 @@ def testGuiProjTree_DragAndDrop(qtbot, monkeypatch, caplog, nwGUI: GuiMain, proj
projTree.dropEvent(event) projTree.dropEvent(event)
assert event.isAccepted() is False assert event.isAccepted() is False
assert "Invalid drop location" in caplog.text assert "Invalid drop location" in caplog.text
projTree.saveTreeOrder()
# No item selected assert SHARED.project.tree._order == treeOrder
caplog.clear()
event = QDropEvent(nPos, action, mime, mouse, modifier)
projTree.clearSelection()
projTree.dropEvent(event)
assert event.isAccepted() is False
assert "Invalid drag and drop event" in caplog.text
# Root item selected # Root item selected
caplog.clear() caplog.clear()
event = QDropEvent(nPos, action, mime, mouse, modifier) event = QDropEvent(nPos, action, mime, mouse, modifier)
projTree.clearSelection() projTree.clearSelection()
projTree.setSelectedHandle(C.hNovelRoot, True) projTree._getTreeItem(C.hTitlePage).setSelected(True) # type: ignore
projTree._getTreeItem(C.hNovelRoot).setSelected(True) # type: ignore
projTree.dropEvent(event) projTree.dropEvent(event)
assert event.isAccepted() is False assert event.isAccepted() is False
assert "Invalid drag and drop event" in caplog.text 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() # qtbot.stop()
@@ -1070,22 +987,6 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd)
SHARED.project.tree[nHandle].setParent(C.hInvalid) # type: ignore SHARED.project.tree[nHandle].setParent(C.hInvalid) # type: ignore
assert projTree.revealNewTreeItem(nHandle) is False assert projTree.revealNewTreeItem(nHandle) is False
# Method: undoLastMove
# ====================
# Nothing to move
assert projTree.undoLastMove() is False
projTree._lastMove["item"] = QTreeWidgetItem()
projTree._lastMove["parent"] = QTreeWidgetItem()
projTree._lastMove["index"] = 0
assert projTree.undoLastMove() is False
projTree._lastMove["item"] = projTree._treeMap[C.hTitlePage]
projTree._lastMove["parent"] = QTreeWidgetItem()
projTree._lastMove["index"] = 0
assert projTree.undoLastMove() is False
# Slot: _treeDoubleClick # Slot: _treeDoubleClick
# ====================== # ======================
@@ -1166,6 +1067,330 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd)
projTree.moveToLevel(1) projTree.moveToLevel(1)
assert projTree.getSelectedHandle() is None assert projTree.getSelectedHandle() is None
# Mouse Button Clicks
# ===================
eType = QEvent.Type.MouseButtonPress
pos = projTree.visualItemRect(projTree._getTreeItem(C.hChapterDoc)).center()
button = Qt.MouseButton.MiddleButton
modifier = Qt.KeyboardModifier.NoModifier
# 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 = Qt.MouseButton.LeftButton
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
assert projView.renameTreeItem(C.hChapterDoc) is True
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
assert projView.renameTreeItem() is True
assert SHARED.project.tree[C.hSceneDoc].itemName == "FooBar" # type: ignore
# Check Crash Resistance
# ======================
projTree._postItemMove("dfghj") # This should exit cleanly
# qtbot.stop() # qtbot.stop()
# END Test testGuiProjTree_Other # END Test testGuiProjTree_Other
@pytest.mark.gui
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))
# Create a project
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
nwGUI.switchFocus(nwWidget.TREE)
# 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
projTree.setSelectedHandle(C.hCharRoot)
projTree.newTreeItem(nwItemType.FILE)
projTree.setSelectedHandle(C.hNovelRoot)
projTree.newTreeItem(nwItemType.FILE, isNote=True)
SHARED.project.newFile("SubNote", hNovelNote)
projTree.revealNewTreeItem(hSubNote)
assert SHARED.project.tree[hSubNote].itemParent == hNovelNote # type: ignore
def itemPos(tHandle):
return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center()
# Pop the menu
with monkeypatch.context() as mp:
mp.setattr(QMenu, "exec_", lambda *a: None)
projTree.clearSelection()
# No item under menu
assert projTree._openContextMenu(projTree.viewport().rect().bottomRight()) is False
# Open Trash Menu
assert projTree._openContextMenu(itemPos(hTrashRoot)) is True
# Open Single Select Menu
assert projTree._openContextMenu(itemPos(C.hNovelRoot)) is True
# Open Multi-Select Menu
projTree._getTreeItem(hNovelNote).setSelected(True)
projTree._getTreeItem(hSubNote).setSelected(True)
assert projTree._openContextMenu(itemPos(hCharNote)) is True
# Check the keyboard shortcut handler as well
projTree.setSelectedHandle(C.hNovelRoot)
assert projTree.openContextOnSelected() is True
projTree.clearSelection()
assert projTree.openContextOnSelected() is False
# Menu Builders
# =============
# Context Menu on Root Item
nwItem = SHARED.project.tree[C.hNovelRoot]
assert isinstance(nwItem, NWItem)
ctxMenu = _TreeContextMenu(projTree, nwItem)
ctxMenu.buildSingleSelectMenu(True)
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Rename", "Set Status to ...", "Expand All", "Collapse All",
"Duplicate from Here", "Delete Permanently",
]
# Context Menu on Folder Item
nwItem = SHARED.project.tree[C.hChapterDir]
assert isinstance(nwItem, NWItem)
ctxMenu = _TreeContextMenu(projTree, nwItem)
ctxMenu.buildSingleSelectMenu(True)
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Rename", "Set Status to ...", "Transform", "Expand All", "Collapse All",
"Duplicate from Here", "Move to Trash",
]
def getTransformSubMenu(menu: QMenu) -> list[str]:
for action in menu.actions():
if action.text() == "Transform":
return [x.text() for x in action.menu().actions() if x.text()]
return []
# Context Menu on Document File Item
nwItem = SHARED.project.tree[C.hChapterDoc]
assert isinstance(nwItem, NWItem)
ctxMenu = _TreeContextMenu(projTree, nwItem)
ctxMenu.buildSingleSelectMenu(True)
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Open Document", "View Document", "Rename", "Toggle Active", "Set Status to ...",
"Transform", "Expand All", "Collapse All", "Duplicate from Here", "Move to Trash",
]
assert getTransformSubMenu(ctxMenu) == [
"Convert to Project Note", "Merge Child Items into Self",
"Merge Child Items into New", "Split Document by Headers"
]
# 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)
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Open Document", "View Document", "Rename", "Toggle Active", "Set Importance to ...",
"Transform", "Duplicate Document", "Move to Trash",
]
assert getTransformSubMenu(ctxMenu) == [
"Split Document by Headers",
]
# 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)
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Open Document", "View Document", "Rename", "Toggle Active", "Set Status to ...",
"Transform", "Duplicate Document", "Move to Trash",
]
assert getTransformSubMenu(ctxMenu) == [
"Convert to Novel Document", "Split Document by Headers",
]
# 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])
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])
actions = [x.text() for x in ctxMenu.actions() if x.text()]
assert actions == [
"Set Active to ...", "Set Importance to ...", "Move to Trash",
]
# Direct Edit Functions, Single
# =============================
nwItem = SHARED.project.tree[hNovelNote]
assert isinstance(nwItem, NWItem)
# # Toggle active flag
ctxMenu = _TreeContextMenu(projTree, nwItem)
ctxMenu.buildSingleSelectMenu(False)
assert nwItem.isActive is True
ctxMenu._toggleItemActive()
assert nwItem.isActive is False
# Change item status
assert nwItem.itemStatus == "s000000"
ctxMenu._changeItemStatus("s000001")
assert nwItem.itemStatus == "s000001"
# Change item importance
assert nwItem.itemImport == "i000004"
ctxMenu._changeItemImport("i000005")
assert nwItem.itemImport == "i000005"
# Change item layout
assert nwItem.itemLayout == nwItemLayout.NOTE
ctxMenu._changeItemLayout(nwItemLayout.DOCUMENT)
assert nwItem.itemLayout == nwItemLayout.DOCUMENT
ctxMenu._changeItemLayout(nwItemLayout.NOTE)
assert nwItem.itemLayout == nwItemLayout.NOTE
# Convert Folders to Documents
# ============================
projView.setSelectedHandle(hNovelNote)
assert projTree.newTreeItem(nwItemType.FOLDER) is True
projView.setSelectedHandle(hNewFolderOne)
assert projTree.newTreeItem(nwItemType.FILE) is True
projView.setSelectedHandle(hNovelNote)
assert projTree.newTreeItem(nwItemType.FOLDER) is True
projView.setSelectedHandle(hNewFolderTwo)
assert projTree.newTreeItem(nwItemType.FILE, isNote=True) is True
nwItem = SHARED.project.tree[hNewFolderOne]
assert isinstance(nwItem, NWItem)
ctxMenu = _TreeContextMenu(projTree, nwItem)
ctxMenu.buildSingleSelectMenu(False)
# Click no on the dialog
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
ctxMenu._covertFolderToFile(nwItemLayout.DOCUMENT)
assert SHARED.project.tree[hNewFolderOne].isFolderType() # type: ignore
# Convert the first folder to a document
ctxMenu._covertFolderToFile(nwItemLayout.DOCUMENT)
assert SHARED.project.tree[hNewFolderOne].isFileType() # type: ignore
assert SHARED.project.tree[hNewFolderOne].isDocumentLayout() # type: ignore
nwItem = SHARED.project.tree[hNewFolderTwo]
assert isinstance(nwItem, NWItem)
ctxMenu = _TreeContextMenu(projTree, nwItem)
ctxMenu.buildSingleSelectMenu(False)
# Convert the second folder to a note
ctxMenu._covertFolderToFile(nwItemLayout.NOTE)
assert SHARED.project.tree[hNewFolderTwo].isFileType() # type: ignore
assert SHARED.project.tree[hNewFolderTwo].isNoteLayout() # type: ignore
# Direct Edit Functions, Multi
# ============================
nwItem = SHARED.project.tree[hCharNote]
assert isinstance(nwItem, NWItem)
ctxMenu = _TreeContextMenu(projTree, nwItem)
ctxMenu.buildMultiSelectMenu([hCharNote, hNovelNote, hSubNote])
projTree.clearSelection()
projTree._getTreeItem(hCharNote).setSelected(True)
projTree._getTreeItem(hNovelNote).setSelected(True)
projTree._getTreeItem(hSubNote).setSelected(True)
# 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
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
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
# 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
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
# 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
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
# qtbot.stop()
# END Test testGuiProjTree_ContextMenu