Files
novelWriter/tests/test_gui/test_gui_projtree.py
T
2024-03-03 20:37:44 +01:00

1503 lines
56 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
novelWriter Main GUI Project Tree Class Tester
================================================
This file is a part of novelWriter
Copyright 20182024, 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 <https://www.gnu.org/licenses/>.
"""
from __future__ import annotations
import pytest
from pathlib import Path
from novelwriter.core.project import NWProject
from tools import C, buildTestProject
from mocked import causeOSError
from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QMouseEvent
from PyQt5.QtCore import QEvent, QMimeData, QPoint, QTimer, Qt
from PyQt5.QtWidgets import QMessageBox, QMenu, QTreeWidget, QTreeWidgetItem, QDialog
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemLayout, nwItemType, nwItemClass, nwWidget
from novelwriter.guimain import GuiMain
from novelwriter.core.item import NWItem
from novelwriter.gui.projtree import GuiProjectTree, GuiProjectView, _TreeContextMenu
from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel
@pytest.mark.gui
def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
"""Test adding and removing items from the project tree."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
projView = nwGUI.projView
projTree = nwGUI.projView.projTree
project = SHARED.project
# Try to add item with no project
assert projTree.newTreeItem(nwItemType.FILE) is False
# Create a project
buildTestProject(nwGUI, projPath)
# No itemType set
projTree.clearSelection()
assert projTree.newTreeItem(None) is False
# Root Items
# ==========
# No class set
assert projTree.newTreeItem(nwItemType.ROOT) is False
# Create root item
assert projTree.newTreeItem(nwItemType.ROOT, nwItemClass.WORLD) is True
assert "0000000000010" in project.tree
# File/Folder Items
# =================
# No location selected for new item
projTree.clearSelection()
caplog.clear()
assert projTree.newTreeItem(nwItemType.FILE) is False
assert projTree.newTreeItem(nwItemType.FOLDER) is False
assert "Did not find anywhere" 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
# 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
# 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")
assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
assert projTree._getItemWordCount("0000000000013") == 2
# 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")
assert nwGUI.docEditor.getText() == "### New Scene\n\n"
assert projTree._getItemWordCount("0000000000014") == 2
# Add a new scene with the content copied from the previous
assert nwGUI.openDocument("0000000000014")
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")
assert nwGUI.docEditor.getText() == "### New Scene\n\nWith Stuff\n\n"
assert projTree._getItemWordCount("0000000000015") == 4
# 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")
assert nwGUI.docEditor.getText() == "# New Note\n\n"
assert projTree._getItemWordCount("0000000000016") == 2
# 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
# 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
# Get the trash folder
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()
projView.setSelectedHandle(trashHandle)
assert projTree.newTreeItem(nwItemType.FILE) is False
assert "Cannot add new files or folders to the Trash folder" in caplog.text
# Rename Item
# ===========
# 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
# Clean up
# qtbot.stop()
nwGUI.closeProject()
# END Test testGuiProjTree_NewItems
@pytest.mark.gui
def testGuiProjTree_MoveItems(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test adding and removing items from the project tree."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
projView = nwGUI.projView
projTree = nwGUI.projView.projTree
# Try to move item with no project
assert projTree.moveTreeItem(1) is False
# 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",
]
# Move with no selections
projTree.clearSelection()
assert projTree.moveTreeItem(1) is False
# 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",
]
# Restore
assert projTree.moveTreeItem(1) is True
assert projTree.getTreeFromHandle(C.hChapterDir) == [
C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000011", "0000000000012",
]
# 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",
]
# Restore
assert projTree.moveTreeItem(-1) is True
assert projTree.getTreeFromHandle(C.hChapterDir) == [
C.hChapterDir, C.hChapterDoc, C.hSceneDoc,
"0000000000010", "0000000000011", "0000000000012",
]
# 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",
]
# Root Folder
# ===========
projView.setSelectedHandle(C.hNovelRoot)
assert SHARED.project.tree._order.index(C.hNovelRoot) == 0
# Move novel folder up
assert projTree.moveTreeItem(-1) is False
assert SHARED.project.tree._order.index(C.hNovelRoot) == 0
# Move novel folder down
assert projTree.moveTreeItem(1) is True
assert SHARED.project.tree._order.index(C.hNovelRoot) == 1
# Move novel folder up again
assert projTree.moveTreeItem(-1) is True
assert SHARED.project.tree._order.index(C.hNovelRoot) == 0
# Clean up
# qtbot.stop()
nwGUI.closeProject()
# END Test testGuiProjTree_MoveItems
@pytest.mark.gui
def testGuiProjTree_RequestDeleteItem(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
"""Test external requests for removing items from project tree."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
projView = nwGUI.projView
projTree = nwGUI.projView.projTree
# Try to run with no project
assert projView.requestDeleteItem() is False
# Create a project
buildTestProject(nwGUI, projPath)
# Try emptying the trash already now, when there is no trash folder
assert projView.emptyTrash() is False
# 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",
]
# 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)
# 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
# ===================
assert projView.requestDeleteItem(C.hNovelRoot) is False # Novel Root is blocked
assert projView.requestDeleteItem(C.hCharRoot) is True # Character Root
# Delete File
# ===========
# Block adding trash folder
funcPointer = projTree._addTrashRoot
projTree._addTrashRoot = lambda *a: None
assert projView.requestDeleteItem("0000000000012") is False
projTree._addTrashRoot = funcPointer
# 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"
]
# Try to delete the trash folder
caplog.clear()
assert projView.requestDeleteItem("0000000000013") is False
assert "Cannot delete the Trash folder" in caplog.text
nwGUI.closeProject()
# END Test testGuiProjTree_RequestDeleteItem
@pytest.mark.gui
def testGuiProjTree_MoveItemToTrash(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRnd):
"""Test moving items to Trash."""
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.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
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert projTree.moveItemToTrash(C.hTitlePage) is False
assert project.tree.isTrash(C.hTitlePage) is False
# Move a document to Trash
assert projTree.moveItemToTrash(C.hTitlePage) is True
assert project.tree.isTrash(C.hTitlePage) is True
# 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()
# END Test testGuiProjTree_MoveItemToTrash
@pytest.mark.gui
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
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.No)
assert projTree.permDeleteItem(C.hTitlePage) is False
assert C.hTitlePage in project.tree
# 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
# 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
nwGUI.closeProject()
# END Test testGuiProjTree_PermanentlyDeleteItem
@pytest.mark.gui
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
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.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
# 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
# Running Empty Trash again is cancelled due to empty folder
assert projTree.emptyTrash() is False
nwGUI.closeProject()
# END Test testGuiProjTree_EmptyTrash
@pytest.mark.gui
def testGuiProjTree_MergeDocuments(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText):
"""Test the merge document function."""
mergeData = {}
monkeypatch.setattr(GuiDocMerge, "__init__", lambda *a: None)
monkeypatch.setattr(GuiDocMerge, "exec_", lambda *a: None)
monkeypatch.setattr(GuiDocMerge, "result", lambda *a: QDialog.Accepted)
monkeypatch.setattr(GuiDocMerge, "getData", lambda *a: mergeData)
buildTestProject(nwGUI, projPath)
project = SHARED.project
projTree = nwGUI.projView.projTree
mergedDoc1 = "0000000000014"
# 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
docText1 = "\n\n".join(ipsumText[0:2]) + "\n\n"
docText2 = "\n\n".join(ipsumText[1:3]) + "\n\n"
docText3 = "\n\n".join(ipsumText[2:4]) + "\n\n"
docText4 = "\n\n".join(ipsumText[3:5]) + "\n\n"
lenText1 = len(docText1)
lenText2 = len(docText2)
lenText3 = len(docText3)
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
projTree.revealNewTreeItem(hChapter1)
projTree.revealNewTreeItem(hSceneOne11)
projTree.revealNewTreeItem(hSceneOne12)
projTree.revealNewTreeItem(hSceneOne13)
# Invalid file handle
assert projTree._mergeDocuments(C.hInvalid, False) is False
# Cannot merge root item
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
# Merge to New Doc
# ================
# Set merge job for new documents
mergeData["finalItems"] = [hChapter1, hSceneOne11, hSceneOne12, hSceneOne13]
mergeData["moveToTrash"] = False
# User cancels merge
with monkeypatch.context() as mp:
mp.setattr(GuiDocMerge, "result", lambda *a: QDialog.Rejected)
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
# Merge to Existing Doc
# =====================
# Set merge job for parent document
mergeData["finalItems"] = [hSceneOne11, hSceneOne12, hSceneOne13]
mergeData["moveToTrash"] = False
# Merging to a folder is not allowed
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
# 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
assert project.tree.isTrash(hSceneOne11) # type: ignore
assert project.tree.isTrash(hSceneOne12) # type: ignore
assert project.tree.isTrash(hSceneOne13) # type: ignore
# qtbot.stop()
# END Test testGuiProjTree_MergeDocuments
@pytest.mark.gui
def testGuiProjTree_SplitDocument(qtbot, monkeypatch, nwGUI, projPath, mockRnd, ipsumText):
"""Test the split document function."""
splitData = {}
splitText = []
monkeypatch.setattr(GuiDocSplit, "__init__", lambda *a: None)
monkeypatch.setattr(GuiDocSplit, "exec_", lambda *a: None)
monkeypatch.setattr(GuiDocSplit, "result", lambda *a: QDialog.Accepted)
monkeypatch.setattr(GuiDocSplit, "getData", lambda *a: (splitData, splitText))
# Create a project
buildTestProject(nwGUI, projPath)
project = SHARED.project
projTree = nwGUI.projView.projTree
docText = (
"Text\n\n"
"##! Prologue\n\nText\n\n"
"## Chapter One\n\nText\n\n"
"### Scene One\n\nText\n\n"
"### Scene Two\n\nText\n\n"
"## Chapter Two\n\nText\n\n"
"### Scene Three\n\nText\n\n"
"### Scene Four\n\nText\n\n"
"#! New Title\n\nText\n\n"
"## New Chapter\n\nText\n\n"
"### New Scene\n\nText\n\n"
"#### New Section\n\nText\n\n"
)
hSplitDoc = project.newFile("Split Doc", C.hNovelRoot)
project.writeNewFile(hSplitDoc, 1, True, docText) # type: ignore
projTree.revealNewTreeItem(hSplitDoc, nHandle=C.hNovelRoot, wordCount=True)
docText = f"# Split Doc\n\n{docText}"
splitData["headerList"] = [
(0, 1, "Split Doc"),
(4, 2, "Prologue"),
(8, 2, "Chapter One"),
(12, 3, "Scene One"),
(16, 3, "Scene Two"),
(20, 2, "Chapter Two"),
(24, 3, "Scene Three"),
(28, 3, "Scene Four"),
(32, 1, "New Title"),
(36, 2, "New Chapter"),
(40, 3, "New Scene"),
(44, 4, "New Section"),
]
fstSet = [
"0000000000011", "0000000000012", "0000000000013", "0000000000014",
"0000000000015", "0000000000016", "0000000000017", "0000000000018",
"0000000000019", "000000000001a", "000000000001b", "000000000001c",
]
sndSet = [
"000000000001d", "000000000001e", "000000000001f", "0000000000020",
"0000000000021", "0000000000022", "0000000000023", "0000000000024",
"0000000000025", "0000000000026", "0000000000027", "0000000000028",
]
trdSet = [
"000000000002a", "000000000002b", "000000000002c", "000000000002d",
"000000000002e", "000000000002f", "0000000000030", "0000000000031",
"0000000000032", "0000000000033", "0000000000034", "0000000000035",
]
# Try to split an invalid document and a non-document
assert projTree._splitDocument(C.hInvalid) is False
assert projTree._splitDocument(C.hNovelRoot) is False
# Split into same root folder
splitData["intoFolder"] = False
# Writing fails
with monkeypatch.context() as mp:
mp.setattr("builtins.open", causeOSError)
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
for tHandle in sndSet:
assert tHandle in project.tree
assert (projPath / "content" / f"{tHandle}.nwd").is_file()
# Add to a folder and move source to trash
splitData["intoFolder"] = True
splitData["moveToTrash"] = 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
# Cancelled by user
with monkeypatch.context() as mp:
mp.setattr(GuiDocSplit, "result", lambda *a: QDialog.Rejected)
assert projTree._splitDocument(hSplitDoc) is False
# qtbot.stop()
# END Test testGuiProjTree_SplitDocument
@pytest.mark.gui
def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd):
"""Test the duplicate items function."""
# Create a project
buildTestProject(nwGUI, projPath)
assert len(SHARED.project.tree) == 8
projTree = nwGUI.projView.projTree
projTree._getTreeItem(C.hNovelRoot).setExpanded(True) # type: ignore
projTree._getTreeItem(C.hChapterDir).setExpanded(True) # type: ignore
# Nothing to do
assert projTree._duplicateFromHandle(C.hInvalid) is False
assert len(SHARED.project.tree) == 8
# Duplicate title page, but select no
with monkeypatch.context() as mp:
mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.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,
]
# Make the duplicator stop early
content = SHARED.project.storage.contentPath
assert isinstance(content, Path)
(content / "000000000001e.nwd").touch()
assert (content / "000000000001e.nwd").exists()
# 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
# qtbot.stop()
# END Test testGuiProjTree_Duplicate
@pytest.mark.gui
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._getTreeItem(C.hChapterDir).setExpanded(True) # type: ignore
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 = Qt.MouseButton.LeftButton
modifier = Qt.KeyboardModifier.NoModifier
# 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()
# END Test testGuiProjTree_AutoScroll
@pytest.mark.gui
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 = Qt.MouseButton.LeftButton
modifier = Qt.KeyboardModifier.NoModifier
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()
# END Test testGuiProjTree_DragAndDrop
@pytest.mark.gui
def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, 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
# Method: initSettings
# ====================
# Test that the scrollbar setting works
CONFIG.hideVScroll = True
CONFIG.hideHScroll = True
projView.initSettings()
assert projTree.verticalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
assert projTree.horizontalScrollBarPolicy() == Qt.ScrollBarAlwaysOff
CONFIG.hideVScroll = False
CONFIG.hideHScroll = False
projView.initSettings()
assert projTree.verticalScrollBarPolicy() == Qt.ScrollBarAsNeeded
assert projTree.horizontalScrollBarPolicy() == Qt.ScrollBarAsNeeded
# 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._treeDoubleClick(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._treeDoubleClick(QTreeWidgetItem(), 0)
assert nwGUI.docEditor.docHandle is None
# Successfully open a file
projTree._treeDoubleClick(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._treeDoubleClick(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 = 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
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()
# 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 == [
"Create New ...", "Rename", "Set Status to ...", "Expand All",
"Collapse All", "Duplicate", "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 == [
"Create New ...", "Rename", "Set Status to ...", "Transform ...", "Expand All",
"Collapse All", "Duplicate", "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", "Create New ...", "Rename", "Rename to Heading",
"Toggle Active", "Set Status to ...", "Transform ...", "Expand All", "Collapse All",
"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"
]
# 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", "Create New ...", "Rename", "Rename to Heading",
"Toggle Active", "Set Importance to ...", "Transform ...", "Duplicate", "Move to Trash",
]
assert getTransformSubMenu(ctxMenu) == [
"Split Document by Headings",
]
# 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", "Create New ...", "Rename", "Rename to Heading",
"Toggle Active", "Set Status to ...", "Transform ...", "Duplicate", "Move to Trash",
]
assert getTransformSubMenu(ctxMenu) == [
"Convert to Novel Document", "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])
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
@pytest.mark.gui
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(nwWidget.TREE)
nwGUI.show()
project = SHARED.project
projView = nwGUI.projView
projTree = projView.projTree
projBar = projView.projBar
# Handles for new objects
hTemplatesRoot = "0000000000010"
hSceneTemplate = "0000000000011"
hNoteTemplate = "0000000000012"
hNewScene = "0000000000013"
hNewCharacter = "0000000000014"
# Add template folder
projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE)
nwTemplateRoot = project.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]
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"
# Add a note template
projTree.setSelectedHandle(hTemplatesRoot)
projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
nwNoteTemplate = project.tree[hNoteTemplate]
assert nwNoteTemplate is not None
assert nwNoteTemplate.itemName == "New Note"
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"
# Add new content to template files
(projPath / "content" / f"{hSceneTemplate}.nwd").write_text("### Scene\n\n@pov: Jane\n\n")
(projPath / "content" / f"{hNoteTemplate}.nwd").write_text("# Jane\n\n@tag: Jane\n\n")
# Add a new scene using the template
projTree.setSelectedHandle(C.hSceneDoc, doScroll=True)
projBar.mTemplates.actions()[0].trigger()
nwNewScene = project.tree[hNewScene]
assert nwNewScene is not None
assert nwNewScene.itemName == "Scene"
assert project.storage.getDocument(hNewScene).readDocument() == "### Scene\n\n@pov: Jane\n\n"
# Add a new note using the template
projTree.setSelectedHandle(C.hCharRoot, doScroll=True)
projBar.mTemplates.actions()[1].trigger()
nwNewCharacter = project.tree[hNewCharacter]
assert nwNewCharacter is not None
assert nwNewCharacter.itemName == "Note"
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
# qtbot.stop()
# END Test testGuiProjTree_Templates