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: File History:
Created: 2018-09-29 [0.0.1] GuiProjectTree 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] GuiProjectView
Created: 2022-06-06 [1.7b1] GuiProjectToolBar 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.QtCore import Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtGui import QIcon, QPalette from PyQt5.QtGui import QIcon, QPalette
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView, QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QInputDialog,
QInputDialog, QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget,
QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget QTreeWidgetItem, QVBoxLayout, QWidget
) )
from novelwriter.core import NWDoc from novelwriter.core import NWDoc
@@ -211,37 +210,37 @@ class GuiProjectToolBar(QWidget):
# Add Item Menu # Add Item Menu
self.mAdd = QMenu() 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.setIcon(self.theTheme.getIcon("proj_document"))
self.aAddEmpty.triggered.connect( self.aAddEmpty.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=0, isNote=False) 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.setIcon(self.theTheme.getIcon("proj_chapter"))
self.aAddChap.triggered.connect( self.aAddChap.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=2, isNote=False) 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.setIcon(self.theTheme.getIcon("proj_scene"))
self.aAddScene.triggered.connect( self.aAddScene.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False) 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.setIcon(self.theTheme.getIcon("proj_note"))
self.aAddNote.triggered.connect( self.aAddNote.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) 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.setIcon(self.theTheme.getIcon("proj_folder"))
self.aAddFolder.triggered.connect( self.aAddFolder.triggered.connect(
lambda: self.projTree.newTreeItem(nwItemType.FOLDER) 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.NOVEL)
self._addRootFolderEntry(nwItemClass.ARCHIVE) self._addRootFolderEntry(nwItemClass.ARCHIVE)
self.mAddRoot.addSeparator() self.mAddRoot.addSeparator()
@@ -268,6 +267,9 @@ class GuiProjectToolBar(QWidget):
self.aMoreUndo = self.mMore.addAction(self.tr("Undo Move")) self.aMoreUndo = self.mMore.addAction(self.tr("Undo Move"))
self.aMoreUndo.triggered.connect(lambda: self.projTree.undoLastMove()) 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 = QToolButton(self)
self.tbMore.setToolTip(self.tr("More Options")) self.tbMore.setToolTip(self.tr("More Options"))
self.tbMore.setIcon(self.theTheme.getIcon("menu")) self.tbMore.setIcon(self.theTheme.getIcon("menu"))
@@ -335,9 +337,8 @@ class GuiProjectTree(QTreeWidget):
## ##
# Context Menu # Context Menu
self.ctxMenu = GuiProjectTreeMenu(self)
self.setContextMenuPolicy(Qt.CustomContextMenu) self.setContextMenuPolicy(Qt.CustomContextMenu)
self.customContextMenuRequested.connect(self._rightClickMenu) self.customContextMenuRequested.connect(self._openContextMenu)
# Tree Settings # Tree Settings
iPx = self.theTheme.baseIconSize iPx = self.theTheme.baseIconSize
@@ -972,19 +973,106 @@ class GuiProjectTree(QTreeWidget):
return return
@pyqtSlot("QPoint") @pyqtSlot("QPoint")
def _rightClickMenu(self, clickPos): def _openContextMenu(self, clickPos):
"""The user right clicked an element in the project tree, so we """The user right clicked an element in the project tree, so we
open a context menu in-place. open a context menu in-place.
""" """
tItem = None
selItem = self.itemAt(clickPos) selItem = self.itemAt(clickPos)
if isinstance(selItem, QTreeWidgetItem): if isinstance(selItem, QTreeWidgetItem):
tHandle = selItem.data(self.C_NAME, Qt.UserRole) tHandle = selItem.data(self.C_NAME, Qt.UserRole)
self.setSelectedHandle(tHandle) # Just to be safe
tItem = self.theProject.tree[tHandle] tItem = self.theProject.tree[tHandle]
if tItem is not None:
if self.ctxMenu.filterActions(tItem): if tItem is None:
# Only open menu if any actions remain after filter logger.debug("No item found")
self.ctxMenu.exec_(self.viewport().mapToGlobal(clickPos))
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 return
@@ -1121,6 +1209,46 @@ class GuiProjectTree(QTreeWidget):
return True 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): def _scanChildren(self, theList, tItem, tIndex):
"""This is a recursive function returning all items in a tree """This is a recursive function returning all items in a tree
starting at a given QTreeWidgetItem. starting at a given QTreeWidgetItem.
@@ -1244,116 +1372,3 @@ class GuiProjectTree(QTreeWidget):
return return
# END Class GuiProjectTree # 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'?> <?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> <project>
<name>Sample Project</name> <name>Sample Project</name>
<title>Sample Project</title> <title>Sample Project</title>
<author>Jane Smith</author> <author>Jane Smith</author>
<author>Jay Doh</author> <author>Jay Doh</author>
<saveCount>1342</saveCount> <saveCount>1345</saveCount>
<autoCount>227</autoCount> <autoCount>229</autoCount>
<editTime>68090</editTime> <editTime>68286</editTime>
</project> </project>
<settings> <settings>
<doBackup>False</doBackup> <doBackup>False</doBackup>
@@ -33,13 +33,13 @@
<section></section> <section></section>
</titleFormat> </titleFormat>
<status> <status>
<entry key="sf12341" count="7" red="100" green="100" blue="100">New</entry> <entry key="sf12341" count="5" red="100" green="100" blue="100">New</entry>
<entry key="sf24ce6" count="1" red="200" green="50" blue="0">Notes</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="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="s90e6c9" count="7" 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="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="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> </status>
<importance> <importance>
<entry key="ia857f0" count="5" red="100" green="100" blue="100">None</entry> <entry key="ia857f0" count="5" red="100" green="100" blue="100">None</entry>
@@ -63,7 +63,7 @@
</item> </item>
<item handle="edca4be2fcaf8" parent="7031beac91f75" root="7031beac91f75" order="2" type="FILE" class="NOVEL" layout="DOCUMENT"> <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"/> <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>
<item handle="6a2d6d5f4f401" parent="7031beac91f75" root="7031beac91f75" order="3" type="FILE" class="NOVEL" layout="DOCUMENT"> <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"/> <meta expanded="True" charCount="95" wordCount="18" paraCount="1" cursorPos="291"/>
@@ -79,11 +79,11 @@
</item> </item>
<item handle="ba8a28a246524" parent="7031beac91f75" root="7031beac91f75" order="4" type="FILE" class="NOVEL" layout="DOCUMENT"> <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"/> <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>
<item handle="96b68994dfa3d" parent="7031beac91f75" root="7031beac91f75" order="5" type="FILE" class="NOVEL" layout="NOTE"> <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"/> <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>
<item handle="88706ddc78b1b" parent="7031beac91f75" root="7031beac91f75" order="6" type="FILE" class="NOVEL" layout="DOCUMENT"> <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"/> <meta expanded="True" charCount="139" wordCount="28" paraCount="1" cursorPos="343"/>