Clean up project tree class

This commit is contained in:
Veronica Berglyd Olsen
2024-10-31 17:30:49 +01:00
parent b529d5b125
commit 7f49979bc9
4 changed files with 134 additions and 137 deletions
+2 -9
View File
@@ -484,16 +484,9 @@ class GuiNovelTree(QTreeWidget):
if rootHandle is None:
rootHandle = SHARED.project.tree.findRoot(nwItemClass.NOVEL)
treeChanged = SHARED.mainGui.projView.changedSince(self._lastBuild)
indexChanged = SHARED.project.index.rootChangedSince(rootHandle, self._lastBuild)
if not (treeChanged or indexChanged or overRide):
logger.debug("No changes have been made to the novel index")
return
selItem = self.selectedItems()
titleKey = None
if selItem:
titleKey = selItem[0].data(self.C_DATA, self.D_KEY)
if selItems := self.selectedItems():
titleKey = selItems[0].data(self.C_DATA, self.D_KEY)
self._populateTree(rootHandle)
SHARED.project.data.setLastHandle(rootHandle, "novelTree")
+115 -119
View File
@@ -29,7 +29,6 @@ from __future__ import annotations
import logging
from enum import Enum
from time import time
from PyQt5.QtCore import QPoint, Qt, QTimer, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette
@@ -39,7 +38,7 @@ from PyQt5.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
from novelwriter.common import minmax
from novelwriter.common import minmax, qtLambda
from novelwriter.constants import nwLabels, nwStyles, nwUnicode, trConst
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
from novelwriter.core.item import NWItem
@@ -97,37 +96,37 @@ class GuiProjectView(QWidget):
self.keyMoveUp = QShortcut(self.projTree)
self.keyMoveUp.setKey("Ctrl+Up")
self.keyMoveUp.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyMoveUp.activated.connect(lambda: self.projTree.moveTreeItem(-1))
self.keyMoveUp.activated.connect(qtLambda(self.projTree.moveTreeItem, -1))
self.keyMoveDn = QShortcut(self.projTree)
self.keyMoveDn.setKey("Ctrl+Down")
self.keyMoveDn.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyMoveDn.activated.connect(lambda: self.projTree.moveTreeItem(1))
self.keyMoveDn.activated.connect(qtLambda(self.projTree.moveTreeItem, 1))
self.keyGoPrev = QShortcut(self.projTree)
self.keyGoPrev.setKey("Alt+Up")
self.keyGoPrev.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyGoPrev.activated.connect(lambda: self.projTree.moveToNextItem(-1))
self.keyGoPrev.activated.connect(qtLambda(self.projTree.moveToNextItem, -1))
self.keyGoNext = QShortcut(self.projTree)
self.keyGoNext.setKey("Alt+Down")
self.keyGoNext.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyGoNext.activated.connect(lambda: self.projTree.moveToNextItem(1))
self.keyGoNext.activated.connect(qtLambda(self.projTree.moveToNextItem, 1))
self.keyGoUp = QShortcut(self.projTree)
self.keyGoUp.setKey("Alt+Left")
self.keyGoUp.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyGoUp.activated.connect(lambda: self.projTree.moveToLevel(-1))
self.keyGoUp.activated.connect(qtLambda(self.projTree.moveToLevel, -1))
self.keyGoDown = QShortcut(self.projTree)
self.keyGoDown.setKey("Alt+Right")
self.keyGoDown.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyGoDown.activated.connect(lambda: self.projTree.moveToLevel(1))
self.keyGoDown.activated.connect(qtLambda(self.projTree.moveToLevel, 1))
self.keyContext = QShortcut(self.projTree)
self.keyContext.setKey("Ctrl+.")
self.keyContext.setContext(Qt.ShortcutContext.WidgetShortcut)
self.keyContext.activated.connect(lambda: self.projTree.openContextOnSelected())
self.keyContext.activated.connect(self.projTree.openContextMenu)
# Signals
self.selectedItemChanged.connect(self.projBar.treeSelectionChanged)
@@ -138,7 +137,6 @@ class GuiProjectView(QWidget):
self.emptyTrash = self.projTree.emptyTrash
self.requestDeleteItem = self.projTree.requestDeleteItem
self.getSelectedHandle = self.projTree.getSelectedHandle
self.changedSince = self.projTree.changedSince
return
@@ -292,38 +290,38 @@ class GuiProjectToolBar(QWidget):
# Move Buttons
self.tbMoveU = NIconToolButton(self, iSz)
self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up"))
self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1))
self.tbMoveU.clicked.connect(qtLambda(self.projTree.moveTreeItem, -1))
self.tbMoveD = NIconToolButton(self, iSz)
self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down"))
self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1))
self.tbMoveD.clicked.connect(qtLambda(self.projTree.moveTreeItem, 1))
# Add Item Menu
self.mAdd = QMenu(self)
self.aAddEmpty = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["document"]))
self.aAddEmpty.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False)
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=0, isNote=False)
)
self.aAddChap = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h2"]))
self.aAddChap.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False)
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=2, isNote=False)
)
self.aAddScene = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h3"]))
self.aAddScene.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False)
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=3, isNote=False)
)
self.aAddNote = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["note"]))
self.aAddNote.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
qtLambda(self.projTree.newTreeItem, nwItemType.FILE, hLevel=1, isNote=True)
)
self.aAddFolder = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["folder"]))
self.aAddFolder.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FOLDER)
qtLambda(self.projTree.newTreeItem, nwItemType.FOLDER)
)
self.mTemplates = _UpdatableMenu(self.mAdd)
@@ -343,13 +341,17 @@ class GuiProjectToolBar(QWidget):
self.mMore = QMenu(self)
self.aExpand = self.mMore.addAction(self.tr("Expand All"))
self.aExpand.triggered.connect(lambda: self.projTree.setExpandedFromHandle(None, True))
self.aExpand.triggered.connect(
qtLambda(self.projTree.setExpandedFromHandle, None, True)
)
self.aCollapse = self.mMore.addAction(self.tr("Collapse All"))
self.aCollapse.triggered.connect(lambda: self.projTree.setExpandedFromHandle(None, False))
self.aCollapse.triggered.connect(
qtLambda(self.projTree.setExpandedFromHandle, None, False)
)
self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash())
self.aEmptyTrash.triggered.connect(qtLambda(self.projTree.emptyTrash))
self.tbMore = NIconToolButton(self, iSz)
self.tbMore.setToolTip(self.tr("More Options"))
@@ -422,7 +424,7 @@ class GuiProjectToolBar(QWidget):
action.setData(tHandle)
action.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]))
action.triggered.connect(
lambda _, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True)
qtLambda(self.projView.setSelectedHandle, tHandle, doScroll=True)
)
return
@@ -461,7 +463,9 @@ class GuiProjectToolBar(QWidget):
def addClass(itemClass: nwItemClass) -> None:
aNew = self.mAddRoot.addAction(trConst(nwLabels.CLASS_NAME[itemClass]))
aNew.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[itemClass]))
aNew.triggered.connect(lambda: self.projTree.newTreeItem(nwItemType.ROOT, itemClass))
aNew.triggered.connect(
qtLambda(self.projTree.newTreeItem, nwItemType.ROOT, itemClass)
)
self.mAddRoot.addAction(aNew)
return
@@ -504,7 +508,6 @@ class GuiProjectTree(QTreeWidget):
# Internal Variables
self._treeMap: dict[str, QTreeWidgetItem] = {}
self._timeChanged = 0.0
self._popAlert = None
self._actHandle = None
@@ -518,7 +521,7 @@ class GuiProjectTree(QTreeWidget):
# Context Menu
self.setContextMenuPolicy(Qt.ContextMenuPolicy.CustomContextMenu)
self.customContextMenuRequested.connect(self._openContextMenu)
self.customContextMenuRequested.connect(self.openContextMenu)
# Tree Settings
iPx = SHARED.theme.baseIconHeight
@@ -602,7 +605,6 @@ class GuiProjectTree(QTreeWidget):
"""Clear the GUI content and the related map."""
self.clear()
self._treeMap = {}
self._timeChanged = 0.0
return
def createNewNote(self, tag: str, itemClass: nwItemClass) -> None:
@@ -1121,12 +1123,10 @@ class GuiProjectTree(QTreeWidget):
"""
logger.debug("Building the project tree ...")
self.clearTree()
count = 0
for nwItem in SHARED.project.iterProjectItems():
count += 1
self._addTreeItem(nwItem)
if count > 0:
logger.info("%d item(s) added to the project tree", count)
self.setActiveHandle(self._actHandle)
logger.info("%d item(s) added to the project tree", len(self._treeMap))
return
def getSelectedHandle(self) -> str | None:
@@ -1137,19 +1137,13 @@ class GuiProjectTree(QTreeWidget):
return items[0].data(self.C_DATA, self.D_HANDLE)
return None
def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> bool:
def setSelectedHandle(self, tHandle: str | None, doScroll: bool = False) -> None:
"""Set a specific handle as the selected item."""
tItem = self._getTreeItem(tHandle)
if tItem is None:
return False
if tHandle in self._treeMap:
self.setCurrentItem(self._treeMap[tHandle])
if (indexes := self.selectedIndexes()) and doScroll:
self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
return True
if (indexes := self.selectedIndexes()) and doScroll:
self.scrollTo(indexes[0], QAbstractItemView.ScrollHint.PositionAtCenter)
return
def setActiveHandle(self, tHandle: str | None) -> None:
"""Highlight the rows associated with a given handle."""
@@ -1169,19 +1163,52 @@ class GuiProjectTree(QTreeWidget):
status for all child items. If tHandle is None, it affects the
entire tree.
"""
trItem = self._getTreeItem(tHandle) or self.invisibleRootItem()
self._recursiveSetExpanded(trItem, isExpanded)
item = self._getTreeItem(tHandle) or self.invisibleRootItem()
self._recursiveSetExpanded(item, isExpanded)
return
def openContextOnSelected(self) -> bool:
"""Open the context menu on the current selected item."""
if items := self.selectedItems():
return self._openContextMenu(self.visualItemRect(items[0]).center())
return False
##
# Public Slots
##
def changedSince(self, checkTime: float) -> bool:
"""Check if the tree has changed since a given time."""
return self._timeChanged > checkTime
@pyqtSlot()
@pyqtSlot("QPoint")
def openContextMenu(self, clickPos: QPoint | None = None) -> None:
"""The user right clicked an element in the project tree, so we
open a context menu in-place.
"""
if clickPos is None and (items := self.selectedItems()):
clickPos = self.visualItemRect(items[0]).center()
if clickPos is not None:
tItem = None
tHandle = None
hasChild = False
sItem = self.itemAt(clickPos)
sItems = self.selectedItems()
if isinstance(sItem, QTreeWidgetItem):
tHandle = sItem.data(self.C_DATA, self.D_HANDLE)
tItem = SHARED.project.tree[tHandle]
hasChild = sItem.childCount() > 0
if tItem is None or tHandle is None:
logger.debug("No item found")
return
ctxMenu = _TreeContextMenu(self, tItem)
trashHandle = SHARED.project.tree.trashRoot
if trashHandle and tHandle == trashHandle:
ctxMenu.buildTrashMenu()
elif len(sItems) > 1:
handles = [str(x.data(self.C_DATA, self.D_HANDLE)) for x in sItems]
ctxMenu.buildMultiSelectMenu(handles)
else:
ctxMenu.buildSingleSelectMenu(hasChild)
ctxMenu.exec(self.viewport().mapToGlobal(clickPos))
ctxMenu.deleteLater()
return
##
# Private Slots
@@ -1224,40 +1251,6 @@ class GuiProjectTree(QTreeWidget):
return
@pyqtSlot("QPoint")
def _openContextMenu(self, clickPos: QPoint) -> bool:
"""The user right clicked an element in the project tree, so we
open a context menu in-place.
"""
tItem = None
tHandle = None
hasChild = False
sItem = self.itemAt(clickPos)
sItems = self.selectedItems()
if isinstance(sItem, QTreeWidgetItem):
tHandle = sItem.data(self.C_DATA, self.D_HANDLE)
tItem = SHARED.project.tree[tHandle]
hasChild = sItem.childCount() > 0
if tItem is None or tHandle is None:
logger.debug("No item found")
return False
ctxMenu = _TreeContextMenu(self, tItem)
trashHandle = SHARED.project.tree.trashRoot
if trashHandle and tHandle == trashHandle:
ctxMenu.buildTrashMenu()
elif len(sItems) > 1:
handles = [str(x.data(self.C_DATA, self.D_HANDLE)) for x in sItems]
ctxMenu.buildMultiSelectMenu(handles)
else:
ctxMenu.buildSingleSelectMenu(hasChild)
ctxMenu.exec(self.viewport().mapToGlobal(clickPos))
ctxMenu.deleteLater()
return True
@pyqtSlot()
def _doAutoScroll(self) -> None:
"""Scroll one item up or down based on direction value."""
@@ -1658,7 +1651,6 @@ class GuiProjectTree(QTreeWidget):
signals. A flush is only needed if an item is moved, created or
deleted.
"""
self._timeChanged = time()
SHARED.project.setProjectChanged(True)
if flush:
self.saveTreeOrder()
@@ -1785,7 +1777,7 @@ class _TreeContextMenu(QMenu):
# Edit Item Settings
action = self.addAction(self.tr("Rename"))
action.triggered.connect(lambda: self.projTree.renameTreeItem(self._handle))
action.triggered.connect(qtLambda(self.projTree.renameTreeItem, self._handle))
if isFile:
self._itemHeader()
self._itemActive(False)
@@ -1821,13 +1813,15 @@ class _TreeContextMenu(QMenu):
def _docActions(self) -> None:
"""Add document actions."""
action = self.addAction(self.tr("Open Document"))
action.triggered.connect(
lambda: self.projView.openDocumentRequest.emit(self._handle, nwDocMode.EDIT, "", True)
)
action.triggered.connect(qtLambda(
self.projView.openDocumentRequest.emit,
self._handle, nwDocMode.EDIT, "", True
))
action = self.addAction(self.tr("View Document"))
action.triggered.connect(
lambda: self.projView.openDocumentRequest.emit(self._handle, nwDocMode.VIEW, "", False)
)
action.triggered.connect(qtLambda(
self.projView.openDocumentRequest.emit,
self._handle, nwDocMode.VIEW, "", False
))
return
def _itemCreation(self) -> None:
@@ -1846,7 +1840,7 @@ class _TreeContextMenu(QMenu):
if hItem := SHARED.project.index.getItemHeading(self._handle, "T0001"):
action = self.addAction(self.tr("Rename to Heading"))
action.triggered.connect(
lambda: self.projTree.renameTreeItem(self._handle, hItem.title)
qtLambda(self.projTree.renameTreeItem, self._handle, hItem.title)
)
return
@@ -1855,9 +1849,9 @@ class _TreeContextMenu(QMenu):
if multi:
mSub = self.addMenu(self.tr("Set Active to ..."))
aOne = mSub.addAction(SHARED.theme.getIcon("checked"), self.projTree.trActive)
aOne.triggered.connect(lambda: self._iterItemActive(True))
aOne.triggered.connect(qtLambda(self._iterItemActive, True))
aTwo = mSub.addAction(SHARED.theme.getIcon("unchecked"), self.projTree.trInactive)
aTwo.triggered.connect(lambda: self._iterItemActive(False))
aTwo.triggered.connect(qtLambda(self._iterItemActive, False))
else:
action = self.addAction(self.tr("Toggle Active"))
action.triggered.connect(self._toggleItemActive)
@@ -1868,37 +1862,39 @@ class _TreeContextMenu(QMenu):
if self._item.isNovelLike():
menu = self.addMenu(self.tr("Set Status to ..."))
current = self._item.itemStatus
for n, (key, entry) in enumerate(SHARED.project.data.itemStatus.iterItems()):
for key, entry in SHARED.project.data.itemStatus.iterItems():
name = entry.name
if not multi and current == key:
name += f" ({nwUnicode.U_CHECK})"
action = menu.addAction(entry.icon, name)
if multi:
action.triggered.connect(lambda n, key=key: self._iterSetItemStatus(key))
action.triggered.connect(qtLambda(self._iterSetItemStatus, key))
else:
action.triggered.connect(lambda n, key=key: self._changeItemStatus(key))
action.triggered.connect(qtLambda(self._changeItemStatus, key))
menu.addSeparator()
action = menu.addAction(self.tr("Manage Labels ..."))
action.triggered.connect(
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_STATUS)
)
action.triggered.connect(qtLambda(
self.projView.projectSettingsRequest.emit,
GuiProjectSettings.PAGE_STATUS
))
else:
menu = self.addMenu(self.tr("Set Importance to ..."))
current = self._item.itemImport
for n, (key, entry) in enumerate(SHARED.project.data.itemImport.iterItems()):
for key, entry in SHARED.project.data.itemImport.iterItems():
name = entry.name
if not multi and current == key:
name += f" ({nwUnicode.U_CHECK})"
action = menu.addAction(entry.icon, name)
if multi:
action.triggered.connect(lambda n, key=key: self._iterSetItemImport(key))
action.triggered.connect(qtLambda(self._iterSetItemImport, key))
else:
action.triggered.connect(lambda n, key=key: self._changeItemImport(key))
action.triggered.connect(qtLambda(self._changeItemImport, key))
menu.addSeparator()
action = menu.addAction(self.tr("Manage Labels ..."))
action.triggered.connect(
lambda: self.projView.projectSettingsRequest.emit(GuiProjectSettings.PAGE_IMPORT)
)
action.triggered.connect(qtLambda(
self.projView.projectSettingsRequest.emit,
GuiProjectSettings.PAGE_IMPORT
))
return
def _itemTransform(self, isFile: bool, isFolder: bool, hasChild: bool) -> None:
@@ -1917,33 +1913,33 @@ class _TreeContextMenu(QMenu):
if isNoteFile and self._item.documentAllowed():
action = menu.addAction(self.tr("Convert to {0}").format(trDoc))
action.triggered.connect(lambda: self._changeItemLayout(loDoc))
action.triggered.connect(qtLambda(self._changeItemLayout, loDoc))
if isDocFile:
action = menu.addAction(self.tr("Convert to {0}").format(trNote))
action.triggered.connect(lambda: self._changeItemLayout(loNote))
action.triggered.connect(qtLambda(self._changeItemLayout, loNote))
if isFolder and self._item.documentAllowed():
action = menu.addAction(self.tr("Convert to {0}").format(trDoc))
action.triggered.connect(lambda: self._covertFolderToFile(loDoc))
action.triggered.connect(qtLambda(self._covertFolderToFile, loDoc))
if isFolder:
action = menu.addAction(self.tr("Convert to {0}").format(trNote))
action.triggered.connect(lambda: self._covertFolderToFile(loNote))
action.triggered.connect(qtLambda(self._covertFolderToFile, loNote))
if hasChild and isFile:
action = menu.addAction(self.tr("Merge Child Items into Self"))
action.triggered.connect(lambda: tree._mergeDocuments(tHandle, False))
action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, False))
action = menu.addAction(self.tr("Merge Child Items into New"))
action.triggered.connect(lambda: tree._mergeDocuments(tHandle, True))
action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, True))
if hasChild and isFolder:
action = menu.addAction(self.tr("Merge Documents in Folder"))
action.triggered.connect(lambda: tree._mergeDocuments(tHandle, True))
action.triggered.connect(qtLambda(tree._mergeDocuments, tHandle, True))
if isFile:
action = menu.addAction(self.tr("Split Document by Headings"))
action.triggered.connect(lambda: tree._splitDocument(tHandle))
action.triggered.connect(qtLambda(tree._splitDocument, tHandle))
return
@@ -1953,19 +1949,19 @@ class _TreeContextMenu(QMenu):
tHandle = self._handle
if hasChild:
action = self.addAction(self.tr("Expand All"))
action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, True))
action.triggered.connect(qtLambda(tree.setExpandedFromHandle, tHandle, True))
action = self.addAction(self.tr("Collapse All"))
action.triggered.connect(lambda: tree.setExpandedFromHandle(tHandle, False))
action.triggered.connect(qtLambda(tree.setExpandedFromHandle, tHandle, False))
action = self.addAction(self.tr("Duplicate"))
action.triggered.connect(lambda: tree._duplicateFromHandle(tHandle))
action.triggered.connect(qtLambda(tree._duplicateFromHandle, tHandle))
if self._item.itemClass == nwItemClass.TRASH or isRoot or (isFolder and not hasChild):
action = self.addAction(self.tr("Delete Permanently"))
action.triggered.connect(lambda: tree.permDeleteItem(tHandle))
action.triggered.connect(qtLambda(tree.permDeleteItem, tHandle))
else:
action = self.addAction(self.tr("Move to Trash"))
action.triggered.connect(lambda: tree.moveItemToTrash(tHandle))
action.triggered.connect(qtLambda(tree.moveItemToTrash, tHandle))
return
+1 -1
View File
@@ -134,7 +134,7 @@ def testGuiViewer_Main(qtbot, monkeypatch, nwGUI, prjLipsum):
assert docViewer.docAction(nwDocAction.COPY) is False
# Open again via menu
assert nwGUI.projView.projTree.setSelectedHandle("88243afbe5ed8")
nwGUI.projView.projTree.setSelectedHandle("88243afbe5ed8")
nwGUI.mainMenu.aViewDoc.activate(QAction.ActionEvent.Trigger)
# Open context menu
+16 -8
View File
@@ -21,6 +21,7 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
from __future__ import annotations
from pathlib import Path
from unittest.mock import MagicMock
import pytest
@@ -1146,30 +1147,37 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
def itemPos(tHandle):
return projTree.visualItemRect(projTree._getTreeItem(tHandle)).center()
# Pop the menu
# Pop the menu in various positions and check for success
with monkeypatch.context() as mp:
mp.setattr(QMenu, "exec", lambda *a: None)
mockMenu = MagicMock()
mp.setattr(QMenu, "exec", mockMenu)
projTree.clearSelection()
# No item under menu
assert projTree._openContextMenu(projTree.viewport().rect().bottomRight()) is False
projTree.openContextMenu(projTree.viewport().rect().bottomRight())
assert mockMenu.call_count == 0
# Open Trash Menu
assert projTree._openContextMenu(itemPos(hTrashRoot)) is True
projTree.openContextMenu(itemPos(hTrashRoot))
assert mockMenu.call_count == 1
# Open Single Select Menu
assert projTree._openContextMenu(itemPos(C.hNovelRoot)) is True
projTree.openContextMenu(itemPos(C.hNovelRoot))
assert mockMenu.call_count == 2
# Open Multi-Select Menu
projTree._getTreeItem(hNovelNote).setSelected(True)
projTree._getTreeItem(hSubNote).setSelected(True)
assert projTree._openContextMenu(itemPos(hCharNote)) is True
projTree.openContextMenu(itemPos(hCharNote))
assert mockMenu.call_count == 3
# Check the keyboard shortcut handler as well
projTree.setSelectedHandle(C.hNovelRoot)
assert projTree.openContextOnSelected() is True
projTree.openContextMenu(None)
assert mockMenu.call_count == 4
projTree.clearSelection()
assert projTree.openContextOnSelected() is False
projTree.openContextMenu(None)
assert mockMenu.call_count == 4
# Menu Builders
# =============