Improve placement of new documents (#2302)
This commit is contained in:
@@ -33,7 +33,7 @@ from typing import TYPE_CHECKING, Literal, overload
|
|||||||
from PyQt6.QtCore import QModelIndex
|
from PyQt6.QtCore import QModelIndex
|
||||||
|
|
||||||
from novelwriter import SHARED
|
from novelwriter import SHARED
|
||||||
from novelwriter.constants import nwFiles, nwLabels, trConst
|
from novelwriter.constants import nwFiles, nwLabels, nwStyles, trConst
|
||||||
from novelwriter.core.item import NWItem
|
from novelwriter.core.item import NWItem
|
||||||
from novelwriter.core.itemmodel import ProjectModel, ProjectNode
|
from novelwriter.core.itemmodel import ProjectModel, ProjectNode
|
||||||
from novelwriter.enum import nwChange, nwItemClass, nwItemLayout, nwItemType
|
from novelwriter.enum import nwChange, nwItemClass, nwItemLayout, nwItemType
|
||||||
@@ -262,6 +262,31 @@ class NWTree:
|
|||||||
|
|
||||||
return
|
return
|
||||||
|
|
||||||
|
def pickParent(self, sNode: ProjectNode, hLevel: int, isNote: bool) -> tuple[str | None, int]:
|
||||||
|
"""Pick an appropriate parent handle for adding a new item."""
|
||||||
|
if sNode.item.isFolderType() or sNode.item.isRootType():
|
||||||
|
# Always add as a direct child of folders
|
||||||
|
return sNode.item.itemHandle, sNode.childCount()
|
||||||
|
|
||||||
|
pNode = sNode.parent()
|
||||||
|
pLevel = nwStyles.H_LEVEL.get(pNode.item.mainHeading, 0) if pNode else 0
|
||||||
|
|
||||||
|
# Notes are treated as H0, and scenes and sections both as H3
|
||||||
|
sLevel = min(0 if isNote else nwStyles.H_LEVEL.get(sNode.item.mainHeading, 0), 3)
|
||||||
|
|
||||||
|
if pNode and pNode.item.isFileType() and pLevel >= hLevel and sLevel > hLevel:
|
||||||
|
# If the selected item is a smaller heading and the parent heading
|
||||||
|
# is equal or larger, we make it a sibling of the parent (See #2260)
|
||||||
|
return pNode.item.itemParent, pNode.row() + 1
|
||||||
|
|
||||||
|
if sNode.childCount() > 0 and (0 < sLevel < hLevel or isNote):
|
||||||
|
# If the selected item already has child nodes and has a larger
|
||||||
|
# heading or is a note, we make the new item a child
|
||||||
|
return sNode.item.itemHandle, sNode.childCount()
|
||||||
|
|
||||||
|
# The default behaviour is to make the new item a sibling
|
||||||
|
return sNode.item.itemParent, sNode.row() + 1
|
||||||
|
|
||||||
def refreshItems(self, items: list[str]) -> None:
|
def refreshItems(self, items: list[str]) -> None:
|
||||||
"""Refresh these items on the GUI. If they are an ordered range,
|
"""Refresh these items on the GUI. If they are an ordered range,
|
||||||
also set the isRange flag to True.
|
also set the isRange flag to True.
|
||||||
|
|||||||
+55
-51
@@ -275,9 +275,9 @@ class GuiProjectToolBar(QWidget):
|
|||||||
# Add Item Menu
|
# Add Item Menu
|
||||||
self.mAdd = QMenu(self)
|
self.mAdd = QMenu(self)
|
||||||
|
|
||||||
self.aAddEmpty = qtAddAction(self.mAdd, trConst(nwLabels.ITEM_DESCRIPTION["document"]))
|
self.aAddScene = qtAddAction(self.mAdd, trConst(nwLabels.ITEM_DESCRIPTION["doc_h3"]))
|
||||||
self.aAddEmpty.triggered.connect(
|
self.aAddScene.triggered.connect(
|
||||||
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=0, isNote=False)
|
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=3, isNote=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.aAddChap = qtAddAction(self.mAdd, trConst(nwLabels.ITEM_DESCRIPTION["doc_h2"]))
|
self.aAddChap = qtAddAction(self.mAdd, trConst(nwLabels.ITEM_DESCRIPTION["doc_h2"]))
|
||||||
@@ -285,9 +285,14 @@ class GuiProjectToolBar(QWidget):
|
|||||||
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=2, isNote=False)
|
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=2, isNote=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.aAddScene = qtAddAction(self.mAdd, trConst(nwLabels.ITEM_DESCRIPTION["doc_h3"]))
|
self.aAddPart = qtAddAction(self.mAdd, trConst(nwLabels.ITEM_DESCRIPTION["doc_h1"]))
|
||||||
self.aAddScene.triggered.connect(
|
self.aAddPart.triggered.connect(
|
||||||
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=3, isNote=False)
|
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=1, isNote=False)
|
||||||
|
)
|
||||||
|
|
||||||
|
self.aAddEmpty = qtAddAction(self.mAdd, trConst(nwLabels.ITEM_DESCRIPTION["document"]))
|
||||||
|
self.aAddEmpty.triggered.connect(
|
||||||
|
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=0, isNote=False)
|
||||||
)
|
)
|
||||||
|
|
||||||
self.aAddNote = qtAddAction(self.mAdd, trConst(nwLabels.ITEM_DESCRIPTION["note"]))
|
self.aAddNote = qtAddAction(self.mAdd, trConst(nwLabels.ITEM_DESCRIPTION["note"]))
|
||||||
@@ -366,9 +371,10 @@ class GuiProjectToolBar(QWidget):
|
|||||||
self.tbAdd.setThemeIcon("add", "green")
|
self.tbAdd.setThemeIcon("add", "green")
|
||||||
self.tbMore.setThemeIcon("more_vertical")
|
self.tbMore.setThemeIcon("more_vertical")
|
||||||
|
|
||||||
self.aAddEmpty.setIcon(SHARED.theme.getIcon("prj_document", "file"))
|
|
||||||
self.aAddChap.setIcon(SHARED.theme.getIcon("prj_chapter", "chapter"))
|
|
||||||
self.aAddScene.setIcon(SHARED.theme.getIcon("prj_scene", "scene"))
|
self.aAddScene.setIcon(SHARED.theme.getIcon("prj_scene", "scene"))
|
||||||
|
self.aAddChap.setIcon(SHARED.theme.getIcon("prj_chapter", "chapter"))
|
||||||
|
self.aAddPart.setIcon(SHARED.theme.getIcon("prj_title", "title"))
|
||||||
|
self.aAddEmpty.setIcon(SHARED.theme.getIcon("prj_document", "file"))
|
||||||
self.aAddNote.setIcon(SHARED.theme.getIcon("prj_note", "note"))
|
self.aAddNote.setIcon(SHARED.theme.getIcon("prj_note", "note"))
|
||||||
self.aAddFolder.setIcon(SHARED.theme.getIcon("prj_folder", "folder"))
|
self.aAddFolder.setIcon(SHARED.theme.getIcon("prj_folder", "folder"))
|
||||||
|
|
||||||
@@ -425,9 +431,10 @@ class GuiProjectToolBar(QWidget):
|
|||||||
"""
|
"""
|
||||||
nwItem = SHARED.project.tree[tHandle]
|
nwItem = SHARED.project.tree[tHandle]
|
||||||
allowDoc = isinstance(nwItem, NWItem) and nwItem.documentAllowed()
|
allowDoc = isinstance(nwItem, NWItem) and nwItem.documentAllowed()
|
||||||
self.aAddEmpty.setVisible(allowDoc)
|
|
||||||
self.aAddChap.setVisible(allowDoc)
|
|
||||||
self.aAddScene.setVisible(allowDoc)
|
self.aAddScene.setVisible(allowDoc)
|
||||||
|
self.aAddChap.setVisible(allowDoc)
|
||||||
|
self.aAddPart.setVisible(allowDoc)
|
||||||
|
self.aAddEmpty.setVisible(allowDoc)
|
||||||
return
|
return
|
||||||
|
|
||||||
##
|
##
|
||||||
@@ -503,7 +510,6 @@ class GuiProjectTree(QTreeView):
|
|||||||
self.customContextMenuRequested.connect(self.openContextMenu)
|
self.customContextMenuRequested.connect(self.openContextMenu)
|
||||||
|
|
||||||
# Connect signals
|
# Connect signals
|
||||||
self.clicked.connect(self._onSingleClick)
|
|
||||||
self.doubleClicked.connect(self._onDoubleClick)
|
self.doubleClicked.connect(self._onDoubleClick)
|
||||||
self.collapsed.connect(self._onNodeCollapsed)
|
self.collapsed.connect(self._onNodeCollapsed)
|
||||||
self.expanded.connect(self._onNodeExpanded)
|
self.expanded.connect(self._onNodeExpanded)
|
||||||
@@ -553,6 +559,9 @@ class GuiProjectTree(QTreeView):
|
|||||||
|
|
||||||
def loadModel(self) -> None:
|
def loadModel(self) -> None:
|
||||||
"""Load and prepare a new project model."""
|
"""Load and prepare a new project model."""
|
||||||
|
if selectModelOld := self.selectionModel():
|
||||||
|
selectModelOld.disconnect()
|
||||||
|
|
||||||
self.setModel(SHARED.project.tree.model)
|
self.setModel(SHARED.project.tree.model)
|
||||||
|
|
||||||
# Lock the column sizes
|
# Lock the column sizes
|
||||||
@@ -568,6 +577,9 @@ class GuiProjectTree(QTreeView):
|
|||||||
header.resizeSection(ProjectNode.C_ACTIVE, iPx + 6)
|
header.resizeSection(ProjectNode.C_ACTIVE, iPx + 6)
|
||||||
header.resizeSection(ProjectNode.C_STATUS, iPx + 6)
|
header.resizeSection(ProjectNode.C_STATUS, iPx + 6)
|
||||||
|
|
||||||
|
if selectModelNew := self.selectionModel():
|
||||||
|
selectModelNew.currentChanged.connect(self._onSelectionChange)
|
||||||
|
|
||||||
self.restoreExpandedState()
|
self.restoreExpandedState()
|
||||||
|
|
||||||
return
|
return
|
||||||
@@ -606,12 +618,12 @@ class GuiProjectTree(QTreeView):
|
|||||||
tHandle = None
|
tHandle = None
|
||||||
if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
|
if itemType == nwItemType.ROOT and isinstance(itemClass, nwItemClass):
|
||||||
|
|
||||||
pos = -1
|
sPos = -1
|
||||||
if (node := self._getNode(self.currentIndex())) and (itemRoot := node.item.itemRoot):
|
if (node := self._getNode(self.currentIndex())) and (itemRoot := node.item.itemRoot):
|
||||||
if root := SHARED.project.tree.nodes.get(itemRoot):
|
if root := SHARED.project.tree.nodes.get(itemRoot):
|
||||||
pos = root.row() + 1
|
sPos = root.row() + 1
|
||||||
|
|
||||||
tHandle = SHARED.project.newRoot(itemClass, pos)
|
tHandle = SHARED.project.newRoot(itemClass, sPos)
|
||||||
self.restoreExpandedState()
|
self.restoreExpandedState()
|
||||||
|
|
||||||
elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
|
elif itemType in (nwItemType.FILE, nwItemType.FOLDER):
|
||||||
@@ -624,52 +636,43 @@ class GuiProjectTree(QTreeView):
|
|||||||
SHARED.error(self.tr("Cannot add new files or folders to the Trash folder."))
|
SHARED.error(self.tr("Cannot add new files or folders to the Trash folder."))
|
||||||
return
|
return
|
||||||
|
|
||||||
# Collect some information about the selected item
|
# Set default label and determine where to put the new item
|
||||||
sLevel = nwStyles.H_LEVEL.get(node.item.mainHeading, 0)
|
nNote = isNote
|
||||||
sIsParent = node.childCount() > 0
|
nLevel = hLevel
|
||||||
|
|
||||||
# Set default label and determine if new item is to be added
|
|
||||||
# as child or sibling to the selected item
|
|
||||||
if itemType == nwItemType.FILE:
|
if itemType == nwItemType.FILE:
|
||||||
if copyDoc and (cItem := SHARED.project.tree[copyDoc]):
|
if copyDoc and (cItem := SHARED.project.tree[copyDoc]):
|
||||||
|
nNote = cItem.isNoteLayout()
|
||||||
|
nLevel = nwStyles.H_LEVEL.get(cItem.mainHeading, 0)
|
||||||
newLabel = cItem.itemName
|
newLabel = cItem.itemName
|
||||||
asChild = sIsParent and node.item.isDocumentLayout()
|
|
||||||
elif isNote:
|
elif isNote:
|
||||||
newLabel = self.tr("New Note")
|
newLabel = self.tr("New Note")
|
||||||
asChild = sIsParent
|
elif hLevel == 1:
|
||||||
|
newLabel = self.tr("New Part")
|
||||||
elif hLevel == 2:
|
elif hLevel == 2:
|
||||||
newLabel = self.tr("New Chapter")
|
newLabel = self.tr("New Chapter")
|
||||||
asChild = sIsParent and node.item.isDocumentLayout() and sLevel < 2
|
|
||||||
elif hLevel == 3:
|
elif hLevel == 3:
|
||||||
newLabel = self.tr("New Scene")
|
newLabel = self.tr("New Scene")
|
||||||
asChild = sIsParent and node.item.isDocumentLayout() and sLevel < 3
|
|
||||||
else:
|
else:
|
||||||
newLabel = self.tr("New Document")
|
newLabel = self.tr("New Document")
|
||||||
asChild = sIsParent and node.item.isDocumentLayout()
|
|
||||||
else:
|
else:
|
||||||
newLabel = self.tr("New Folder")
|
newLabel = self.tr("New Folder")
|
||||||
asChild = False
|
nLevel = 0
|
||||||
|
|
||||||
pos = -1
|
sHandle, sPos = SHARED.project.tree.pickParent(node, nLevel, nNote)
|
||||||
sHandle = None
|
if sHandle:
|
||||||
if not (asChild or node.item.isFolderType() or node.item.isRootType()):
|
newLabel, dlgOk = GuiEditLabel.getLabel(self, text=newLabel)
|
||||||
pos = node.row() + 1
|
if dlgOk:
|
||||||
sHandle = node.item.itemParent
|
# Add the file or folder
|
||||||
|
if itemType == nwItemType.FILE:
|
||||||
sHandle = sHandle or node.item.itemHandle
|
if tHandle := SHARED.project.newFile(newLabel, sHandle, sPos):
|
||||||
newLabel, dlgOk = GuiEditLabel.getLabel(self, text=newLabel)
|
if copyDoc:
|
||||||
if dlgOk:
|
SHARED.project.copyFileContent(tHandle, copyDoc)
|
||||||
# Add the file or folder
|
elif hLevel > 0:
|
||||||
if itemType == nwItemType.FILE:
|
SHARED.project.writeNewFile(tHandle, hLevel, not nNote)
|
||||||
if tHandle := SHARED.project.newFile(newLabel, sHandle, pos):
|
SHARED.project.index.reIndexHandle(tHandle)
|
||||||
if copyDoc:
|
SHARED.project.tree.refreshItems([tHandle])
|
||||||
SHARED.project.copyFileContent(tHandle, copyDoc)
|
else:
|
||||||
elif hLevel > 0:
|
tHandle = SHARED.project.newFolder(newLabel, sHandle, sPos)
|
||||||
SHARED.project.writeNewFile(tHandle, hLevel, not isNote)
|
|
||||||
SHARED.project.index.reIndexHandle(tHandle)
|
|
||||||
SHARED.project.tree.refreshItems([tHandle])
|
|
||||||
else:
|
|
||||||
tHandle = SHARED.project.newFolder(newLabel, sHandle, pos)
|
|
||||||
|
|
||||||
# Select the new item automatically
|
# Select the new item automatically
|
||||||
if tHandle:
|
if tHandle:
|
||||||
@@ -969,10 +972,10 @@ class GuiProjectTree(QTreeView):
|
|||||||
# Private Slots
|
# Private Slots
|
||||||
##
|
##
|
||||||
|
|
||||||
@pyqtSlot(QModelIndex)
|
@pyqtSlot(QModelIndex, QModelIndex)
|
||||||
def _onSingleClick(self, index: QModelIndex) -> None:
|
def _onSelectionChange(self, current: QModelIndex, previous: QModelIndex) -> None:
|
||||||
"""The user changed which item is selected."""
|
"""The user changed which item is selected."""
|
||||||
if node := self._getNode(index):
|
if node := self._getNode(current):
|
||||||
self.projView.selectedItemChanged.emit(node.item.itemHandle)
|
self.projView.selectedItemChanged.emit(node.item.itemHandle)
|
||||||
return
|
return
|
||||||
|
|
||||||
@@ -1198,9 +1201,10 @@ class _TreeContextMenu(QMenu):
|
|||||||
def _itemCreation(self) -> None:
|
def _itemCreation(self) -> None:
|
||||||
"""Add create item actions."""
|
"""Add create item actions."""
|
||||||
menu = qtAddMenu(self, self.tr("Create New ..."))
|
menu = qtAddMenu(self, self.tr("Create New ..."))
|
||||||
menu.addAction(self._view.projBar.aAddEmpty)
|
|
||||||
menu.addAction(self._view.projBar.aAddChap)
|
|
||||||
menu.addAction(self._view.projBar.aAddScene)
|
menu.addAction(self._view.projBar.aAddScene)
|
||||||
|
menu.addAction(self._view.projBar.aAddChap)
|
||||||
|
menu.addAction(self._view.projBar.aAddPart)
|
||||||
|
menu.addAction(self._view.projBar.aAddEmpty)
|
||||||
menu.addAction(self._view.projBar.aAddNote)
|
menu.addAction(self._view.projBar.aAddNote)
|
||||||
menu.addAction(self._view.projBar.aAddFolder)
|
menu.addAction(self._view.projBar.aAddFolder)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -211,6 +211,145 @@ def testCoreTree_ManipulateTree(mockGUI, mockItems):
|
|||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.core
|
||||||
|
def testCoreTree_PickParent(mockGUI, mockItems):
|
||||||
|
"""Check the parent item picker."""
|
||||||
|
project = NWProject()
|
||||||
|
tree = NWTree(project)
|
||||||
|
assert len(tree) == 0
|
||||||
|
|
||||||
|
# Case 1: Root and Folder
|
||||||
|
# =======================
|
||||||
|
|
||||||
|
hNovelRoot = tree.create("Novel", None, nwItemType.ROOT, nwItemClass.NOVEL)
|
||||||
|
nNovelRoot = tree.nodes[hNovelRoot]
|
||||||
|
|
||||||
|
# Add a folder
|
||||||
|
sHandle, sPos = tree.pickParent(nNovelRoot, 0, False)
|
||||||
|
assert sHandle == hNovelRoot
|
||||||
|
assert sPos == 0
|
||||||
|
hNovelFolder = tree.create("Folder", sHandle, nwItemType.FOLDER, pos=sPos)
|
||||||
|
assert hNovelFolder is not None
|
||||||
|
nNovelFolder = tree.nodes[hNovelFolder]
|
||||||
|
assert nNovelFolder.item.itemClass == nwItemClass.NOVEL
|
||||||
|
|
||||||
|
# Add a partition
|
||||||
|
sHandle, sPos = tree.pickParent(nNovelRoot, 1, False)
|
||||||
|
assert sHandle == hNovelRoot
|
||||||
|
assert sPos == 1
|
||||||
|
hPartOne = tree.create("Part One", sHandle, nwItemType.FILE, pos=sPos)
|
||||||
|
assert hPartOne is not None
|
||||||
|
nPartOne = tree.nodes[hPartOne]
|
||||||
|
assert nPartOne.item.itemClass == nwItemClass.NOVEL
|
||||||
|
nPartOne.item.setMainHeading("H1")
|
||||||
|
|
||||||
|
# Folders behave identical to root
|
||||||
|
sHandle, sPos = tree.pickParent(nNovelFolder, 0, False)
|
||||||
|
assert sHandle == hNovelFolder
|
||||||
|
assert sPos == 0
|
||||||
|
|
||||||
|
# Case 2: Documents of Same Level
|
||||||
|
# ===============================
|
||||||
|
|
||||||
|
# Add a chapter under Part One
|
||||||
|
hChapterOne = tree.create("Chapter One", hPartOne, nwItemType.FILE)
|
||||||
|
assert hChapterOne is not None
|
||||||
|
nChapterOne = tree.nodes[hChapterOne]
|
||||||
|
assert nChapterOne.item.itemParent == hPartOne
|
||||||
|
nChapterOne.item.setMainHeading("H2")
|
||||||
|
|
||||||
|
# Add a chapter next to Chapter One -> Sibling (default behaviour)
|
||||||
|
sHandle, sPos = tree.pickParent(nChapterOne, 2, False)
|
||||||
|
assert sHandle == hPartOne
|
||||||
|
assert sPos == 1
|
||||||
|
hChapterTwo = tree.create("Chapter Two", sHandle, nwItemType.FILE, pos=sPos)
|
||||||
|
assert hChapterTwo is not None
|
||||||
|
nChapterTwo = tree.nodes[hChapterTwo]
|
||||||
|
assert nChapterTwo.item.itemClass == nwItemClass.NOVEL
|
||||||
|
nChapterTwo.item.setMainHeading("H2")
|
||||||
|
|
||||||
|
# Add a new part next to Chapter Two -> Sibling to parent (second if condition)
|
||||||
|
sHandle, sPos = tree.pickParent(nChapterTwo, 1, False)
|
||||||
|
assert sHandle == hNovelRoot
|
||||||
|
assert sPos == 2
|
||||||
|
|
||||||
|
# Add a scene under Chapter Two
|
||||||
|
hSceneOne = tree.create("Scene One", hChapterOne, nwItemType.FILE)
|
||||||
|
assert hSceneOne is not None
|
||||||
|
nSceneOne = tree.nodes[hSceneOne]
|
||||||
|
assert nSceneOne.item.itemParent == hChapterOne
|
||||||
|
nSceneOne.item.setMainHeading("H3")
|
||||||
|
|
||||||
|
# Adding a part next to the scene should also jump a level up
|
||||||
|
sHandle, sPos = tree.pickParent(nSceneOne, 1, False)
|
||||||
|
assert sHandle == hPartOne
|
||||||
|
assert sPos == 1
|
||||||
|
|
||||||
|
# Case 3: Documents of Deeper Level
|
||||||
|
# =================================
|
||||||
|
|
||||||
|
# Add chapter directly to Part One with existing chapters -> Added as child (third if)
|
||||||
|
sHandle, sPos = tree.pickParent(nPartOne, 2, False)
|
||||||
|
assert sHandle == hPartOne
|
||||||
|
assert sPos == 2
|
||||||
|
|
||||||
|
# But adding a new part becomes a sibling
|
||||||
|
sHandle, sPos = tree.pickParent(nPartOne, 1, False)
|
||||||
|
assert sHandle == hNovelRoot
|
||||||
|
assert sPos == 2
|
||||||
|
|
||||||
|
# Add a page without a heading
|
||||||
|
hPage = tree.create("Page", hNovelRoot, nwItemType.FILE)
|
||||||
|
assert hPage is not None
|
||||||
|
nPage = tree.nodes[hPage]
|
||||||
|
assert nPage.item.itemParent == hNovelRoot
|
||||||
|
nPage.item.setMainHeading("H0")
|
||||||
|
|
||||||
|
# Add a scene below the page
|
||||||
|
hSceneTwo = tree.create("Scene Two", hPage, nwItemType.FILE)
|
||||||
|
assert hSceneTwo is not None
|
||||||
|
nSceneTwo = tree.nodes[hSceneTwo]
|
||||||
|
assert nSceneTwo.item.itemParent == hPage
|
||||||
|
nSceneTwo.item.setMainHeading("H3")
|
||||||
|
|
||||||
|
# A page without a heading should not add anything as a child
|
||||||
|
sHandle, sPos = tree.pickParent(nPage, 3, False)
|
||||||
|
assert sHandle == hNovelRoot
|
||||||
|
assert sPos == 3
|
||||||
|
|
||||||
|
# Case 4: Notes
|
||||||
|
# =============
|
||||||
|
|
||||||
|
hCharRoot = tree.create("Characters", None, nwItemType.ROOT, nwItemClass.CHARACTER)
|
||||||
|
nCharRoot = tree.nodes[hCharRoot]
|
||||||
|
|
||||||
|
# Add a note at root level
|
||||||
|
hNoteOne = tree.create("Note One", hCharRoot, nwItemType.FILE)
|
||||||
|
assert hNoteOne is not None
|
||||||
|
nNoteOne = tree.nodes[hNoteOne]
|
||||||
|
assert nNoteOne.item.itemClass == nwItemClass.CHARACTER
|
||||||
|
nNoteOne.item.setMainHeading("H1")
|
||||||
|
assert nCharRoot.childCount() == 1
|
||||||
|
|
||||||
|
# Adding a new note is a sibling
|
||||||
|
sHandle, sPos = tree.pickParent(nNoteOne, 1, True)
|
||||||
|
assert sHandle == hCharRoot
|
||||||
|
assert sPos == 1
|
||||||
|
|
||||||
|
# Add a child note to Note One
|
||||||
|
hNoteTwo = tree.create("Note Two", hNoteOne, nwItemType.FILE)
|
||||||
|
assert hNoteTwo is not None
|
||||||
|
nNoteTwo = tree.nodes[hNoteTwo]
|
||||||
|
assert nNoteTwo.item.itemClass == nwItemClass.CHARACTER
|
||||||
|
nNoteTwo.item.setMainHeading("H1")
|
||||||
|
assert nNoteOne.childCount() == 1
|
||||||
|
|
||||||
|
# Adding a new note now is a child
|
||||||
|
sHandle, sPos = tree.pickParent(nNoteOne, 1, True)
|
||||||
|
assert sHandle == hNoteOne
|
||||||
|
assert sPos == 1
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.core
|
@pytest.mark.core
|
||||||
def testCoreTree_ItemMethods(monkeypatch, mockGUI, mockItems):
|
def testCoreTree_ItemMethods(monkeypatch, mockGUI, mockItems):
|
||||||
"""Check the item methods of the tree."""
|
"""Check the item methods of the tree."""
|
||||||
|
|||||||
@@ -134,7 +134,7 @@ def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, moc
|
|||||||
# Add a new file in the new folder
|
# Add a new file in the new folder
|
||||||
hNewFile = "0000000000013"
|
hNewFile = "0000000000013"
|
||||||
projView.setSelectedHandle(hNewFolder, doScroll=True)
|
projView.setSelectedHandle(hNewFolder, doScroll=True)
|
||||||
projTree.newTreeItem(nwItemType.FILE)
|
projTree.newTreeItem(nwItemType.FILE, hLevel=0)
|
||||||
assert hNewFile in tree
|
assert hNewFile in tree
|
||||||
item = tree[hNewFile]
|
item = tree[hNewFile]
|
||||||
assert item is not None
|
assert item is not None
|
||||||
@@ -148,9 +148,28 @@ def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, moc
|
|||||||
"Objects", "Trash",
|
"Objects", "Trash",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add a new chapter next to the other new file
|
# Add a new partition next to the other new file
|
||||||
hNewChapter = "0000000000014"
|
hNewPart = "0000000000014"
|
||||||
projView.setSelectedHandle(hNewFile, doScroll=True)
|
projView.setSelectedHandle(hNewFile, doScroll=True)
|
||||||
|
projTree.newTreeItem(nwItemType.FILE, hLevel=1)
|
||||||
|
assert hNewPart in tree
|
||||||
|
item = tree[hNewPart]
|
||||||
|
assert item is not None
|
||||||
|
assert item.itemName == "New Part"
|
||||||
|
assert item.itemParent == hNewFolder
|
||||||
|
assert item.itemRoot == C.hNovelRoot
|
||||||
|
assert item.itemClass == nwItemClass.NOVEL
|
||||||
|
assert nwGUI.openDocument(hNewPart)
|
||||||
|
assert nwGUI.docEditor.getText() == "# New Part\n\n"
|
||||||
|
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
||||||
|
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
||||||
|
"New Folder", "New Document", "New Part", "Plot", "Characters",
|
||||||
|
"Locations", "Objects", "Trash",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Add a new chapter next to the other new file
|
||||||
|
hNewChapter = "0000000000015"
|
||||||
|
projView.setSelectedHandle(hNewPart, doScroll=True)
|
||||||
projTree.newTreeItem(nwItemType.FILE, hLevel=2)
|
projTree.newTreeItem(nwItemType.FILE, hLevel=2)
|
||||||
assert hNewChapter in tree
|
assert hNewChapter in tree
|
||||||
item = tree[hNewChapter]
|
item = tree[hNewChapter]
|
||||||
@@ -163,12 +182,12 @@ def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, moc
|
|||||||
assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
|
assert nwGUI.docEditor.getText() == "## New Chapter\n\n"
|
||||||
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
||||||
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
||||||
"New Folder", "New Document", "New Chapter", "Plot", "Characters",
|
"New Folder", "New Document", "New Part", "New Chapter", "Plot",
|
||||||
"Locations", "Objects", "Trash",
|
"Characters", "Locations", "Objects", "Trash",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add a new scene next to the other new file
|
# Add a new scene next to the other new file
|
||||||
hNewScene = "0000000000015"
|
hNewScene = "0000000000016"
|
||||||
projView.setSelectedHandle(hNewChapter, doScroll=True)
|
projView.setSelectedHandle(hNewChapter, doScroll=True)
|
||||||
projTree.newTreeItem(nwItemType.FILE, hLevel=3)
|
projTree.newTreeItem(nwItemType.FILE, hLevel=3)
|
||||||
assert hNewScene in tree
|
assert hNewScene in tree
|
||||||
@@ -182,8 +201,8 @@ def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, moc
|
|||||||
assert nwGUI.docEditor.getText() == "### New Scene\n\n"
|
assert nwGUI.docEditor.getText() == "### New Scene\n\n"
|
||||||
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
||||||
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
||||||
"New Folder", "New Document", "New Chapter", "New Scene", "Plot",
|
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
|
||||||
"Characters", "Locations", "Objects", "Trash",
|
"Plot", "Characters", "Locations", "Objects", "Trash",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add a new scene with the content copied from the previous
|
# Add a new scene with the content copied from the previous
|
||||||
@@ -191,7 +210,7 @@ def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, moc
|
|||||||
nwGUI.docEditor.setPlainText("### New Scene\n\nWith Stuff\n\n")
|
nwGUI.docEditor.setPlainText("### New Scene\n\nWith Stuff\n\n")
|
||||||
nwGUI.saveDocument()
|
nwGUI.saveDocument()
|
||||||
|
|
||||||
hNewSceneCopy = "0000000000016"
|
hNewSceneCopy = "0000000000017"
|
||||||
projView.setSelectedHandle(hNewScene, doScroll=True)
|
projView.setSelectedHandle(hNewScene, doScroll=True)
|
||||||
projTree.newTreeItem(nwItemType.FILE, copyDoc=hNewScene)
|
projTree.newTreeItem(nwItemType.FILE, copyDoc=hNewScene)
|
||||||
assert hNewSceneCopy in tree
|
assert hNewSceneCopy in tree
|
||||||
@@ -205,12 +224,12 @@ def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, moc
|
|||||||
assert nwGUI.docEditor.getText() == "### New Scene\n\nWith Stuff\n\n"
|
assert nwGUI.docEditor.getText() == "### New Scene\n\nWith Stuff\n\n"
|
||||||
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
||||||
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
||||||
"New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
|
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
|
||||||
"Plot", "Characters", "Locations", "Objects", "Trash",
|
"New Scene", "Plot", "Characters", "Locations", "Objects", "Trash",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Add a new file to the characters folder
|
# Add a new file to the characters folder
|
||||||
hNewCharacter = "0000000000017"
|
hNewCharacter = "0000000000018"
|
||||||
projView.setSelectedHandle(C.hCharRoot, doScroll=True)
|
projView.setSelectedHandle(C.hCharRoot, doScroll=True)
|
||||||
projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
|
projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
|
||||||
assert hNewCharacter in tree
|
assert hNewCharacter in tree
|
||||||
@@ -224,8 +243,9 @@ def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, moc
|
|||||||
assert nwGUI.docEditor.getText() == "# New Note\n\n"
|
assert nwGUI.docEditor.getText() == "# New Note\n\n"
|
||||||
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
||||||
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
||||||
"New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
|
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
|
||||||
"Plot", "Characters", "New Note", "Locations", "Objects", "Trash",
|
"New Scene", "Plot", "Characters", "New Note", "Locations",
|
||||||
|
"Objects", "Trash",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Cancel during creation
|
# Cancel during creation
|
||||||
@@ -235,15 +255,16 @@ def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, moc
|
|||||||
projTree.newTreeItem(nwItemType.FILE)
|
projTree.newTreeItem(nwItemType.FILE)
|
||||||
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
||||||
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
||||||
"New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
|
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
|
||||||
"Plot", "Characters", "New Note", "Locations", "Objects", "Trash",
|
"New Scene", "Plot", "Characters", "New Note", "Locations",
|
||||||
|
"Objects", "Trash",
|
||||||
]
|
]
|
||||||
|
|
||||||
# From Template
|
# From Template
|
||||||
# =============
|
# =============
|
||||||
|
|
||||||
# Create template folder
|
# Create template folder
|
||||||
hTemplateRoot = "0000000000018"
|
hTemplateRoot = "0000000000019"
|
||||||
projView.setSelectedHandle(hObjectRoot)
|
projView.setSelectedHandle(hObjectRoot)
|
||||||
projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE)
|
projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE)
|
||||||
assert hTemplateRoot in tree
|
assert hTemplateRoot in tree
|
||||||
@@ -255,13 +276,13 @@ def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, moc
|
|||||||
assert item.itemClass == nwItemClass.TEMPLATE
|
assert item.itemClass == nwItemClass.TEMPLATE
|
||||||
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
||||||
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
||||||
"New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
|
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
|
||||||
"Plot", "Characters", "New Note", "Locations", "Objects", "Templates",
|
"New Scene", "Plot", "Characters", "New Note", "Locations",
|
||||||
"Trash",
|
"Objects", "Templates", "Trash",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Create scene template
|
# Create scene template
|
||||||
hSceneTemplate = "0000000000019"
|
hSceneTemplate = "000000000001a"
|
||||||
projView.setSelectedHandle(hTemplateRoot, doScroll=True)
|
projView.setSelectedHandle(hTemplateRoot, doScroll=True)
|
||||||
projTree.newTreeItem(nwItemType.FILE, hLevel=3)
|
projTree.newTreeItem(nwItemType.FILE, hLevel=3)
|
||||||
assert hSceneTemplate in tree
|
assert hSceneTemplate in tree
|
||||||
@@ -278,13 +299,13 @@ def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, moc
|
|||||||
nwGUI.saveDocument()
|
nwGUI.saveDocument()
|
||||||
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
||||||
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
||||||
"New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
|
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
|
||||||
"Plot", "Characters", "New Note", "Locations", "Objects", "Templates",
|
"New Scene", "Plot", "Characters", "New Note", "Locations",
|
||||||
"New Scene Template", "Trash",
|
"Objects", "Templates", "New Scene Template", "Trash",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Create from template
|
# Create from template
|
||||||
hNewFromTemplate = "000000000001a"
|
hNewFromTemplate = "000000000001b"
|
||||||
projView.setSelectedHandle(hNewSceneCopy, doScroll=True)
|
projView.setSelectedHandle(hNewSceneCopy, doScroll=True)
|
||||||
projView.createFileFromTemplate(hSceneTemplate)
|
projView.createFileFromTemplate(hSceneTemplate)
|
||||||
assert hNewFromTemplate in tree
|
assert hNewFromTemplate in tree
|
||||||
@@ -298,9 +319,9 @@ def testGuiProjTree_NewTreeItem(qtbot, caplog, monkeypatch, nwGUI, projPath, moc
|
|||||||
assert nwGUI.docEditor.getText() == "### New Scene Template\n\nWith Stuff\n\n"
|
assert nwGUI.docEditor.getText() == "### New Scene Template\n\nWith Stuff\n\n"
|
||||||
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
||||||
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
||||||
"New Folder", "New Document", "New Chapter", "New Scene", "New Scene",
|
"New Folder", "New Document", "New Part", "New Chapter", "New Scene",
|
||||||
"New Scene Template", "Plot", "Characters", "New Note", "Locations",
|
"New Scene", "New Scene Template", "Plot", "Characters", "New Note",
|
||||||
"Objects", "Templates", "New Scene Template", "Trash",
|
"Locations", "Objects", "Templates", "New Scene Template", "Trash",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Rename Item
|
# Rename Item
|
||||||
@@ -489,7 +510,7 @@ def testGuiProjTree_MouseClicks(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
|||||||
|
|
||||||
# Single click emits a signal
|
# Single click emits a signal
|
||||||
with qtbot.waitSignal(projView.selectedItemChanged) as signal:
|
with qtbot.waitSignal(projView.selectedItemChanged) as signal:
|
||||||
projTree._onSingleClick(model.indexFromHandle(C.hNovelRoot))
|
projTree._onSelectionChange(model.indexFromHandle(C.hNovelRoot), QModelIndex())
|
||||||
assert signal.args[0] == C.hNovelRoot
|
assert signal.args[0] == C.hNovelRoot
|
||||||
|
|
||||||
# Double click on folder expands/collapses it
|
# Double click on folder expands/collapses it
|
||||||
@@ -1065,7 +1086,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
|||||||
# Handles for new objects
|
# Handles for new objects
|
||||||
hCharNote = "0000000000011"
|
hCharNote = "0000000000011"
|
||||||
hNovelNote = "0000000000012"
|
hNovelNote = "0000000000012"
|
||||||
hTrashDoc = "0000000000013"
|
hTrashDoc = "0000000000013"
|
||||||
hSubNote = "0000000000014"
|
hSubNote = "0000000000014"
|
||||||
hNewFolderOne = "0000000000015"
|
hNewFolderOne = "0000000000015"
|
||||||
hNewFolderTwo = "0000000000017"
|
hNewFolderTwo = "0000000000017"
|
||||||
@@ -1090,7 +1111,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
|
|||||||
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
assert [n.item.itemName for n in tree.model.root.allChildren()] == [
|
||||||
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
"Novel", "Title Page", "New Folder", "New Chapter", "New Scene",
|
||||||
"New Note", "SubNote", "Plot", "Characters", "New Note", "Locations",
|
"New Note", "SubNote", "Plot", "Characters", "New Note", "Locations",
|
||||||
"Trash", "New Document",
|
"Trash", "New Part",
|
||||||
]
|
]
|
||||||
|
|
||||||
# Pop the menu in various positions and check for success
|
# Pop the menu in various positions and check for success
|
||||||
|
|||||||
Reference in New Issue
Block a user