Re-implement the tree context menu

This commit is contained in:
Veronica Berglyd Olsen
2022-06-11 13:44:45 +02:00
parent 77746f712f
commit f0bef18b63
2 changed files with 158 additions and 143 deletions
+146 -131
View File
@@ -5,7 +5,6 @@ GUI classes for the main window project tree
File History:
Created: 2018-09-29 [0.0.1] GuiProjectTree
Created: 2020-06-04 [0.7] GuiProjectTreeMenu
Created: 2022-06-06 [1.7b1] GuiProjectView
Created: 2022-06-06 [1.7b1] GuiProjectToolBar
@@ -35,9 +34,9 @@ from time import time
from PyQt5.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon, QPalette
from PyQt5.QtWidgets import (
QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView,
QInputDialog, QLabel, QMenu, QShortcut, QSizePolicy, QToolButton,
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget
QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QInputDialog,
QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget
)
from novelwriter.core import NWDoc
@@ -211,37 +210,37 @@ class GuiProjectToolBar(QWidget):
# Add Item Menu
self.mAdd = QMenu()
self.aAddEmpty = self.mAdd.addAction(self.tr("Plain Document"))
self.aAddEmpty = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["document"]))
self.aAddEmpty.setIcon(self.theTheme.getIcon("proj_document"))
self.aAddEmpty.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False)
)
self.aAddChap = self.mAdd.addAction(self.tr("Chapter Document"))
self.aAddChap = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h2"]))
self.aAddChap.setIcon(self.theTheme.getIcon("proj_chapter"))
self.aAddChap.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False)
)
self.aAddScene = self.mAdd.addAction(self.tr("Scene Document"))
self.aAddScene = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["doc_h3"]))
self.aAddScene.setIcon(self.theTheme.getIcon("proj_scene"))
self.aAddScene.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False)
)
self.aAddNote = self.mAdd.addAction(self.tr("Project Note"))
self.aAddNote = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["note"]))
self.aAddNote.setIcon(self.theTheme.getIcon("proj_note"))
self.aAddNote.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
)
self.aAddFolder = self.mAdd.addAction(self.tr("Folder"))
self.aAddFolder = self.mAdd.addAction(trConst(nwLabels.ITEM_DESCRIPTION["folder"]))
self.aAddFolder.setIcon(self.theTheme.getIcon("proj_folder"))
self.aAddFolder.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FOLDER)
)
self.mAddRoot = self.mAdd.addMenu(self.tr("Root Folder"))
self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"]))
self._addRootFolderEntry(nwItemClass.NOVEL)
self._addRootFolderEntry(nwItemClass.ARCHIVE)
self.mAddRoot.addSeparator()
@@ -268,6 +267,9 @@ class GuiProjectToolBar(QWidget):
self.aMoreUndo = self.mMore.addAction(self.tr("Undo Move"))
self.aMoreUndo.triggered.connect(lambda: self.projTree.undoLastMove())
self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash())
self.tbMore = QToolButton(self)
self.tbMore.setToolTip(self.tr("More Options"))
self.tbMore.setIcon(self.theTheme.getIcon("menu"))
@@ -335,9 +337,8 @@ class GuiProjectTree(QTreeWidget):
##
# Context Menu
self.ctxMenu = GuiProjectTreeMenu(self)
self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._rightClickMenu)
self.customContextMenuRequested.connect(self._openContextMenu)
# Tree Settings
iPx = self.theTheme.baseIconSize
@@ -972,19 +973,106 @@ class GuiProjectTree(QTreeWidget):
return
@pyqtSlot("QPoint")
def _rightClickMenu(self, clickPos):
def _openContextMenu(self, clickPos):
"""The user right clicked an element in the project tree, so we
open a context menu in-place.
"""
tItem = None
selItem = self.itemAt(clickPos)
if isinstance(selItem, QTreeWidgetItem):
tHandle = selItem.data(self.C_NAME, Qt.UserRole)
self.setSelectedHandle(tHandle) # Just to be safe
tItem = self.theProject.tree[tHandle]
if tItem is not None:
if self.ctxMenu.filterActions(tItem):
# Only open menu if any actions remain after filter
self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
if tItem is None:
logger.debug("No item found")
ctxMenu = QMenu()
# Trash Folder
# ============
trashHandle = self.theProject.tree.trashRoot()
if tItem.itemHandle == trashHandle and trashHandle is not None:
# The trash folder only has one option
ctxMenu.addAction(
self.tr("Empty Trash"), lambda: self.emptyTrash()
)
ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
return
# Document Actions
# ================
isFile = tItem.itemType == nwItemType.FILE
if isFile:
ctxMenu.addAction(
self.tr("Open Document"),
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.EDIT)
)
ctxMenu.addAction(
self.tr("View Document"),
lambda: self.projView.openDocumentRequest.emit(tHandle, nwDocMode.VIEW)
)
ctxMenu.addSeparator()
# Edit Item Settings
# ==================
if isFile:
ctxMenu.addAction(
self.tr("Toggle Exported"), lambda: self._toggleItemExported(tHandle)
)
if tItem.isNovelLike():
mStatus = ctxMenu.addMenu(self.tr("Change Status"))
for n, (key, entry) in enumerate(self.theProject.statusItems.items()):
aStatus = mStatus.addAction(entry["icon"], entry["name"])
aStatus.triggered.connect(
lambda n, key=key: self._changeItemStatus(tHandle, key)
)
else:
mImport = ctxMenu.addMenu(self.tr("Change Importance"))
for n, (key, entry) in enumerate(self.theProject.importItems.items()):
aImport = mImport.addAction(entry["icon"], entry["name"])
aImport.triggered.connect(
lambda n, key=key: self._changeItemImport(tHandle, key)
)
if isFile and tItem.documentAllowed():
if tItem.itemLayout == nwItemLayout.NOTE:
ctxMenu.addAction(
self.tr("Change to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.DOCUMENT])
),
lambda: self._changeItemLayout(tHandle, nwItemLayout.DOCUMENT)
)
else:
ctxMenu.addAction(
self.tr("Change to {0}").format(
trConst(nwLabels.LAYOUT_NAME[nwItemLayout.NOTE])
),
lambda: self._changeItemLayout(tHandle, nwItemLayout.NOTE)
)
ctxMenu.addSeparator()
# Major Item Actions
# ==================
ctxMenu.addAction(
self.tr("Edit Item Settings"), lambda: self.editTreeItem(tHandle)
)
if tItem.itemClass == nwItemClass.TRASH or tItem.itemType == nwItemType.ROOT:
ctxMenu.addAction(
self.tr("Delete Permanently"), lambda: self.deleteItem(tHandle)
)
else:
ctxMenu.addAction(
self.tr("Move to Trash"), lambda: self.deleteItem(tHandle)
)
ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
return
@@ -1121,6 +1209,46 @@ class GuiProjectTree(QTreeWidget):
return True
def _toggleItemExported(self, tHandle):
"""Toggle the exported status of an item.
"""
tItem = self.theProject.tree[tHandle]
if tItem is not None:
tItem.setExported(not tItem.isExported)
self.setTreeItemValues(tItem.itemHandle)
return
def _changeItemStatus(self, tHandle, tStatus):
"""Set a new status value of an item.
"""
tItem = self.theProject.tree[tHandle]
if tItem is not None:
tItem.setStatus(tStatus)
self.setTreeItemValues(tItem.itemHandle)
return
def _changeItemImport(self, tHandle, tImport):
"""Set a new importance value of an item.
"""
tItem = self.theProject.tree[tHandle]
if tItem is not None:
tItem.setImport(tImport)
self.setTreeItemValues(tItem.itemHandle)
return
def _changeItemLayout(self, tHandle, itemLayout):
"""Set a new item layout value of an item.
"""
tItem = self.theProject.tree[tHandle]
if tItem is not None:
if itemLayout == nwItemLayout.DOCUMENT and tItem.documentAllowed():
tItem.setLayout(nwItemLayout.DOCUMENT)
self.setTreeItemValues(tItem.itemHandle)
elif itemLayout == nwItemLayout.NOTE:
tItem.setLayout(nwItemLayout.NOTE)
self.setTreeItemValues(tItem.itemHandle)
return
def _scanChildren(self, theList, tItem, tIndex):
"""This is a recursive function returning all items in a tree
starting at a given QTreeWidgetItem.
@@ -1244,116 +1372,3 @@ class GuiProjectTree(QTreeWidget):
return
# END Class GuiProjectTree
class GuiProjectTreeMenu(QMenu):
def __init__(self, theTree):
QMenu.__init__(self, theTree)
self.theTree = theTree
self.theItem = None
self.editItem = QAction(self.tr("Edit Project Item"), self)
self.editItem.triggered.connect(self._doEditItem)
self.addAction(self.editItem)
self.openItem = QAction(self.tr("Open Document"), self)
self.openItem.triggered.connect(self._doOpenItem)
self.addAction(self.openItem)
self.viewItem = QAction(self.tr("View Document"), self)
self.viewItem.triggered.connect(self._doViewItem)
self.addAction(self.viewItem)
self.toggleExp = QAction(self.tr("Toggle Included Flag"), self)
self.toggleExp.triggered.connect(self._doToggleExported)
self.addAction(self.toggleExp)
self.deleteItem = QAction(self.tr("Delete Item"), self)
self.deleteItem.triggered.connect(self._doDeleteItem)
self.addAction(self.deleteItem)
self.emptyTrash = QAction(self.tr("Empty Trash"), self)
self.emptyTrash.triggered.connect(self._doEmptyTrash)
self.addAction(self.emptyTrash)
return
def filterActions(self, theItem):
"""Filter the menu entries available based on the properties of
the item the menu was activated on.
"""
self.theItem = theItem
if theItem is None:
logger.error("Failed to extract information to build tree context menu")
return False
trashHandle = self.theTree.theProject.tree.trashRoot()
isTrash = theItem.itemHandle == trashHandle and trashHandle is not None
isFile = theItem.itemType == nwItemType.FILE
self.editItem.setVisible(not isTrash)
self.openItem.setVisible(isFile)
self.viewItem.setVisible(isFile)
self.toggleExp.setVisible(isFile)
self.deleteItem.setVisible(not isTrash)
self.emptyTrash.setVisible(isTrash)
return True
##
# Slots
##
@pyqtSlot()
def _doOpenItem(self):
"""Forward the open document call to the main GUI window.
"""
if self.theItem is not None:
self.theTree.theParent.openDocument(self.theItem.itemHandle, doScroll=False)
return
@pyqtSlot()
def _doViewItem(self):
"""Forward the view document call to the main GUI window.
"""
if self.theItem is not None:
self.theTree.theParent.viewDocument(self.theItem.itemHandle)
return
@pyqtSlot()
def _doEditItem(self):
"""Forward the edit item call to the main GUI window.
"""
if self.theItem is not None:
self.theTree.theParent.editItem()
return
@pyqtSlot()
def _doToggleExported(self):
"""Flip the isExported flag of the current item.
"""
if self.theItem is not None:
self.theItem.setExported(not self.theItem.isExported)
self.theTree.setTreeItemValues(self.theItem.itemHandle)
return
@pyqtSlot()
def _doDeleteItem(self):
"""Forward the delete item call to the project tree.
"""
if self.theItem is not None:
self.theTree.deleteItem()
return
@pyqtSlot()
def _doEmptyTrash(self):
"""Forward the empty trash call to the project tree.
"""
self.theTree.emptyTrash()
return
# END Class GuiProjectTreeMenu
+12 -12
View File
@@ -1,13 +1,13 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-06-06 22:02:03">
<novelWriterXML appVersion="1.7-beta1" hexVersion="0x010700b1" fileVersion="1.4" timeStamp="2022-06-11 13:36:01">
<project>
<name>Sample Project</name>
<title>Sample Project</title>
<author>Jane Smith</author>
<author>Jay Doh</author>
<saveCount>1342</saveCount>
<autoCount>227</autoCount>
<editTime>68090</editTime>
<saveCount>1345</saveCount>
<autoCount>229</autoCount>
<editTime>68286</editTime>
</project>
<settings>
<doBackup>False</doBackup>
@@ -33,13 +33,13 @@
<section></section>
</titleFormat>
<status>
<entry key="sf12341" count="7" red="100" green="100" blue="100">New</entry>
<entry key="sf24ce6" count="1" red="200" green="50" blue="0">Notes</entry>
<entry key="sf12341" count="5" red="100" green="100" blue="100">New</entry>
<entry key="sf24ce6" count="2" red="200" green="50" blue="0">Notes</entry>
<entry key="sc24b8f" count="2" red="182" green="60" blue="0">Started</entry>
<entry key="s90e6c9" count="6" red="193" green="129" blue="0">1st Draft</entry>
<entry key="sd51c5b" count="1" red="193" green="129" blue="0">2nd Draft</entry>
<entry key="s90e6c9" count="7" red="193" green="129" blue="0">1st Draft</entry>
<entry key="sd51c5b" count="0" red="193" green="129" blue="0">2nd Draft</entry>
<entry key="s8ae72a" count="0" red="193" green="129" blue="0">3rd Draft</entry>
<entry key="s78ea90" count="0" red="58" green="180" blue="58">Finished</entry>
<entry key="s78ea90" count="1" red="58" green="180" blue="58">Finished</entry>
</status>
<importance>
<entry key="ia857f0" count="5" red="100" green="100" blue="100">None</entry>
@@ -63,7 +63,7 @@
</item>
<item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="26" wordCount="6" paraCount="1" cursorPos="36"/>
<name status="sf12341" import="ia857f0" exported="True">Part One</name>
<name status="s90e6c9" import="ia857f0" exported="True">Part One</name>
</item>
<item handle="6a2d6d5f4f401" parent="7031beac91f75" root="7031beac91f75" order="3" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="True" charCount="95" wordCount="18" paraCount="1" cursorPos="291"/>
@@ -79,11 +79,11 @@
</item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="False" charCount="617" wordCount="101" paraCount="3" cursorPos="310"/>
<name status="sf12341" import="ia857f0" exported="True">Interlude</name>
<name status="s78ea90" import="ia857f0" exported="True">Interlude</name>
</item>
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE">
<meta expanded="False" charCount="1909" wordCount="346" paraCount="7" cursorPos="0"/>
<name status="sd51c5b" import="ia857f0" exported="False">A Note on Structure</name>
<name status="sf24ce6" import="ia857f0" exported="False">A Note on Structure</name>
</item>
<item handle="88706ddc78b1b" parent="7031beac91f75" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT">
<meta expanded="True" charCount="139" wordCount="28" paraCount="1" cursorPos="343"/>