Add a custom templates menu to project tree add items
This commit is contained in:
+134
-46
@@ -32,12 +32,14 @@ from enum import Enum
|
||||
from time import time
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from PyQt5.QtGui import QDragEnterEvent, QDragMoveEvent, QDropEvent, QMouseEvent, QPalette
|
||||
from PyQt5.QtGui import (
|
||||
QDragEnterEvent, QDragMoveEvent, QDropEvent, QIcon, QMouseEvent, QPalette
|
||||
)
|
||||
from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot
|
||||
from PyQt5.QtWidgets import (
|
||||
QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel,
|
||||
QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem,
|
||||
QVBoxLayout, QWidget
|
||||
QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView,
|
||||
QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget,
|
||||
QTreeWidgetItem, QVBoxLayout, QWidget
|
||||
)
|
||||
|
||||
from novelwriter import CONFIG, SHARED
|
||||
@@ -131,7 +133,8 @@ class GuiProjectView(QWidget):
|
||||
self.keyContext.activated.connect(lambda: self.projTree.openContextOnSelected())
|
||||
|
||||
# Signals
|
||||
self.selectedItemChanged.connect(self.projBar._treeSelectionChanged)
|
||||
self.selectedItemChanged.connect(self.projBar.treeSelectionChanged)
|
||||
self.projTree.itemRefreshed.connect(self.projBar.treeItemRefreshed)
|
||||
|
||||
# Function Mappings
|
||||
self.emptyTrash = self.projTree.emptyTrash
|
||||
@@ -157,7 +160,7 @@ class GuiProjectView(QWidget):
|
||||
self.projTree.initSettings()
|
||||
return
|
||||
|
||||
def clearProjectView(self) -> None:
|
||||
def closeProjectTasks(self) -> None:
|
||||
"""Clear project-related GUI content."""
|
||||
self.projBar.clearContent()
|
||||
self.projBar.setEnabled(False)
|
||||
@@ -166,7 +169,7 @@ class GuiProjectView(QWidget):
|
||||
|
||||
def openProjectTasks(self) -> None:
|
||||
"""Run open project tasks."""
|
||||
self.projBar.buildQuickLinkMenu()
|
||||
self.projBar.buildQuickLinksMenu()
|
||||
self.projBar.setEnabled(True)
|
||||
return
|
||||
|
||||
@@ -225,8 +228,8 @@ class GuiProjectView(QWidget):
|
||||
|
||||
@pyqtSlot(str)
|
||||
def updateRootItem(self, tHandle: str) -> None:
|
||||
"""If any root item changes, rebuild the quick link menu."""
|
||||
self.projBar.buildQuickLinkMenu()
|
||||
"""Process root item changes."""
|
||||
self.projBar.buildQuickLinksMenu()
|
||||
return
|
||||
|
||||
# END Class GuiProjectView
|
||||
@@ -234,6 +237,8 @@ class GuiProjectView(QWidget):
|
||||
|
||||
class GuiProjectToolBar(QWidget):
|
||||
|
||||
newDocumentFromTemplate = pyqtSignal(str)
|
||||
|
||||
def __init__(self, projView: GuiProjectView) -> None:
|
||||
super().__init__(parent=projView)
|
||||
|
||||
@@ -303,6 +308,11 @@ class GuiProjectToolBar(QWidget):
|
||||
lambda: self.projTree.newTreeItem(nwItemType.FOLDER)
|
||||
)
|
||||
|
||||
self.mTemplates = _UpdatableMenu(self.mAdd)
|
||||
self.mTemplates.setActionsVisible(False)
|
||||
self.mTemplates.menuItemTriggered.connect(self._templateSelected)
|
||||
self.mAdd.addMenu(self.mTemplates)
|
||||
|
||||
self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"]))
|
||||
self._buildRootMenu()
|
||||
|
||||
@@ -383,7 +393,7 @@ class GuiProjectToolBar(QWidget):
|
||||
self.tbAdd.setIcon(SHARED.theme.getIcon("add"))
|
||||
self.tbMore.setIcon(SHARED.theme.getIcon("menu"))
|
||||
|
||||
self.buildQuickLinkMenu()
|
||||
self.buildQuickLinksMenu()
|
||||
self._buildRootMenu()
|
||||
|
||||
return
|
||||
@@ -391,21 +401,59 @@ class GuiProjectToolBar(QWidget):
|
||||
def clearContent(self) -> None:
|
||||
"""Clear dynamic content on the tool bar."""
|
||||
self.mQuick.clear()
|
||||
self.mTemplates.clearMenu()
|
||||
return
|
||||
|
||||
def buildQuickLinkMenu(self) -> None:
|
||||
def buildQuickLinksMenu(self) -> None:
|
||||
"""Build the quick link menu."""
|
||||
logger.debug("Rebuilding quick links menu")
|
||||
self.mQuick.clear()
|
||||
for n, (tHandle, nwItem) in enumerate(SHARED.project.tree.iterRoots(None)):
|
||||
aRoot = self.mQuick.addAction(nwItem.itemName)
|
||||
aRoot.setData(tHandle)
|
||||
aRoot.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]))
|
||||
aRoot.triggered.connect(
|
||||
lambda n, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True)
|
||||
for tHandle, nwItem in SHARED.project.tree.iterRoots(None):
|
||||
action = self.mQuick.addAction(nwItem.itemName)
|
||||
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)
|
||||
)
|
||||
return
|
||||
|
||||
##
|
||||
# Public Slots
|
||||
##
|
||||
|
||||
@pyqtSlot(str, str, QIcon, bool)
|
||||
def treeItemRefreshed(self, tHandle: str, name: str, icon: QIcon, isTemplate: bool) -> None:
|
||||
"""Process change in tree items to update menu content."""
|
||||
if isTemplate:
|
||||
self.mTemplates.addUpdate(tHandle, name, icon)
|
||||
elif tHandle in self.mTemplates:
|
||||
self.mTemplates.remove(tHandle)
|
||||
return
|
||||
|
||||
@pyqtSlot(str)
|
||||
def treeSelectionChanged(self, tHandle: str) -> None:
|
||||
"""Toggle the visibility of the new item entries for novel
|
||||
documents. They should only be visible if novel documents can
|
||||
actually be added.
|
||||
"""
|
||||
nwItem = SHARED.project.tree[tHandle]
|
||||
allowDoc = isinstance(nwItem, NWItem) and nwItem.documentAllowed()
|
||||
self.aAddEmpty.setVisible(allowDoc)
|
||||
self.aAddChap.setVisible(allowDoc)
|
||||
self.aAddScene.setVisible(allowDoc)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
##
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _templateSelected(self, tHandle: str) -> None:
|
||||
"""Forward the template menu signal."""
|
||||
logger.debug("Template selected: '%s'", tHandle)
|
||||
self.newDocumentFromTemplate.emit(tHandle)
|
||||
return
|
||||
|
||||
##
|
||||
# Internal Functions
|
||||
##
|
||||
@@ -421,7 +469,6 @@ class GuiProjectToolBar(QWidget):
|
||||
|
||||
self.mAddRoot.clear()
|
||||
addClass(nwItemClass.NOVEL)
|
||||
addClass(nwItemClass.ARCHIVE)
|
||||
self.mAddRoot.addSeparator()
|
||||
addClass(nwItemClass.PLOT)
|
||||
addClass(nwItemClass.CHARACTER)
|
||||
@@ -431,27 +478,11 @@ class GuiProjectToolBar(QWidget):
|
||||
addClass(nwItemClass.ENTITY)
|
||||
addClass(nwItemClass.CUSTOM)
|
||||
self.mAddRoot.addSeparator()
|
||||
addClass(nwItemClass.ARCHIVE)
|
||||
addClass(nwItemClass.TEMPLATE)
|
||||
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
##
|
||||
|
||||
@pyqtSlot(str)
|
||||
def _treeSelectionChanged(self, tHandle: str) -> None:
|
||||
"""Toggle the visibility of the new item entries for novel
|
||||
documents. They should only be visible if novel documents can
|
||||
actually be added.
|
||||
"""
|
||||
nwItem = SHARED.project.tree[tHandle]
|
||||
allowDoc = isinstance(nwItem, NWItem) and nwItem.documentAllowed()
|
||||
self.aAddEmpty.setVisible(allowDoc)
|
||||
self.aAddChap.setVisible(allowDoc)
|
||||
self.aAddScene.setVisible(allowDoc)
|
||||
return
|
||||
|
||||
# END Class GuiProjectToolBar
|
||||
|
||||
|
||||
@@ -466,7 +497,7 @@ class GuiProjectTree(QTreeWidget):
|
||||
D_HANDLE = Qt.ItemDataRole.UserRole
|
||||
D_WORDS = Qt.ItemDataRole.UserRole + 1
|
||||
|
||||
templatesUpdated = pyqtSignal(str)
|
||||
itemRefreshed = pyqtSignal(str, str, QIcon, bool)
|
||||
|
||||
def __init__(self, projView: GuiProjectView) -> None:
|
||||
super().__init__(parent=projView)
|
||||
@@ -478,7 +509,6 @@ class GuiProjectTree(QTreeWidget):
|
||||
|
||||
# Internal Variables
|
||||
self._treeMap = {}
|
||||
self._templateItems = set()
|
||||
self._timeChanged = 0.0
|
||||
self._popAlert = None
|
||||
|
||||
@@ -1034,15 +1064,8 @@ class GuiProjectTree(QTreeWidget):
|
||||
trFont.setUnderline(hLevel == "H1")
|
||||
trItem.setFont(self.C_NAME, trFont)
|
||||
|
||||
# Templates
|
||||
if nwItem.isTemplateFile():
|
||||
self._templateItems.add(tHandle)
|
||||
logger.debug("Updated template '%s'", tHandle)
|
||||
self.templatesUpdated.emit(tHandle)
|
||||
elif tHandle in self._templateItems:
|
||||
self._templateItems.discard(tHandle)
|
||||
logger.debug("Removed template '%s'", tHandle)
|
||||
self.templatesUpdated.emit(tHandle)
|
||||
# Emit Refresh Signal
|
||||
self.itemRefreshed.emit(tHandle, nwItem.itemName, itemIcon, nwItem.isTemplateFile())
|
||||
|
||||
return
|
||||
|
||||
@@ -1647,6 +1670,71 @@ class GuiProjectTree(QTreeWidget):
|
||||
# END Class GuiProjectTree
|
||||
|
||||
|
||||
class _UpdatableMenu(QMenu):
|
||||
|
||||
menuItemTriggered = pyqtSignal(str)
|
||||
|
||||
def __init__(self, parent: QWidget) -> None:
|
||||
super().__init__(parent=parent)
|
||||
self._map: dict[str, QAction] = {}
|
||||
self.setTitle(self.tr("From Template"))
|
||||
self.triggered.connect(self._actionTriggered)
|
||||
return
|
||||
|
||||
def __contains__(self, tHandle: str) -> bool:
|
||||
"""Look up a handle in the menu."""
|
||||
return tHandle in self._map
|
||||
|
||||
##
|
||||
# Methods
|
||||
##
|
||||
|
||||
def addUpdate(self, tHandle: str, name: str, icon: QIcon) -> None:
|
||||
"""Add or update a template item."""
|
||||
if tHandle in self._map:
|
||||
action = self._map[tHandle]
|
||||
action.setText(name)
|
||||
action.setIcon(icon)
|
||||
else:
|
||||
action = QAction(icon, name, self)
|
||||
action.setData(tHandle)
|
||||
self.addAction(action)
|
||||
self._map[tHandle] = action
|
||||
self.setActionsVisible(True)
|
||||
return
|
||||
|
||||
def remove(self, tHandle: str) -> None:
|
||||
"""Remove a template item."""
|
||||
if action := self._map.pop(tHandle, None):
|
||||
self.removeAction(action)
|
||||
if not self._map:
|
||||
self.setActionsVisible(True)
|
||||
return
|
||||
|
||||
def clearMenu(self) -> None:
|
||||
"""Clear all menu content."""
|
||||
self._map.clear()
|
||||
self.clear()
|
||||
return
|
||||
|
||||
def setActionsVisible(self, value: bool) -> None:
|
||||
"""Set the visibility of root action."""
|
||||
self.menuAction().setVisible(value)
|
||||
return
|
||||
|
||||
##
|
||||
# Private Slots
|
||||
##
|
||||
|
||||
@pyqtSlot(QAction)
|
||||
def _actionTriggered(self, action: QAction) -> None:
|
||||
"""Translate the menu trigger into an item trigger."""
|
||||
self.menuItemTriggered.emit(str(action.data()))
|
||||
return
|
||||
|
||||
# END Class _UpdatableMenu
|
||||
|
||||
|
||||
class _TreeContextMenu(QMenu):
|
||||
|
||||
def __init__(self, projTree: GuiProjectTree, nwItem: NWItem) -> None:
|
||||
|
||||
@@ -400,7 +400,7 @@ class GuiMain(QMainWindow):
|
||||
self.docViewerPanel.closeProjectTasks()
|
||||
self.outlineView.closeProjectTasks()
|
||||
self.novelView.closeProjectTasks()
|
||||
self.projView.clearProjectView()
|
||||
self.projView.closeProjectTasks()
|
||||
self.itemDetails.clearDetails()
|
||||
self.mainStatus.clearStatus()
|
||||
|
||||
|
||||
@@ -0,0 +1,7 @@
|
||||
%%~name: Chapter
|
||||
%%~path: f4ed1ae756a1f/2a60782759c6f
|
||||
%%~kind: TEMPLATE/NOTE
|
||||
%%~hash: d09e26b73443ca5592973b954fb8d34b406765cc
|
||||
%%~date: 2024-02-04 12:13:08/2024-02-08 00:02:03
|
||||
## Chapter
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
%%~name: Scene
|
||||
%%~path: 09f22ae8369f7/5aec885635c85
|
||||
%%~kind: TEMPLATES/DOCUMENT
|
||||
%%~kind: TEMPLATE/DOCUMENT
|
||||
%%~hash: 05c9750b00020f8cf9a7bc7362bf19681022cc03
|
||||
%%~date: 2024-02-02 10:58:47/2024-02-02 10:59:32
|
||||
### Scene
|
||||
@@ -10,4 +10,3 @@
|
||||
%Synopsis: A brief note about the scene content.
|
||||
|
||||
Text
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
%%~name: Character Note
|
||||
%%~path: 09f22ae8369f7/5ee8aebcdebc9
|
||||
%%~kind: TEMPLATES/NOTE
|
||||
%%~kind: TEMPLATE/NOTE
|
||||
%%~hash: 6230bb6f796e708a72f4c29dc79d63dc90a47ba5
|
||||
%%~date: 2024-02-02 10:54:52/2024-02-02 10:56:05
|
||||
# Character Name
|
||||
|
||||
+14
-10
@@ -1,6 +1,6 @@
|
||||
<?xml version='1.0' encoding='utf-8'?>
|
||||
<novelWriterXML appVersion="2.3a4" hexVersion="0x020300a4" fileVersion="1.5" fileRevision="2" timeStamp="2024-02-02 11:32:35">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1668" autoCount="261" editTime="82439">
|
||||
<novelWriterXML appVersion="2.3a4" hexVersion="0x020300a4" fileVersion="1.5" fileRevision="2" timeStamp="2024-02-08 00:03:11">
|
||||
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1671" autoCount="262" editTime="82534">
|
||||
<name>Sample Project</name>
|
||||
<author>Jane Smith</author>
|
||||
</project>
|
||||
@@ -20,7 +20,7 @@
|
||||
<entry key="C">D</entry>
|
||||
</autoReplace>
|
||||
<status>
|
||||
<entry key="sf12341" count="7" red="100" green="100" blue="100">New</entry>
|
||||
<entry key="sf12341" count="8" 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="3" red="182" green="60" blue="0">Started</entry>
|
||||
<entry key="s90e6c9" count="7" red="193" green="129" blue="0">1st Draft</entry>
|
||||
@@ -35,7 +35,7 @@
|
||||
<entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
|
||||
</importance>
|
||||
</settings>
|
||||
<content items="30" novelWords="989" notesWords="416">
|
||||
<content items="31" novelWords="990" notesWords="416">
|
||||
<item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
|
||||
<meta expanded="yes" />
|
||||
<name status="sc24b8f" import="ia857f0">Novel</name>
|
||||
@@ -136,18 +136,22 @@
|
||||
<meta expanded="no" heading="H3" charCount="232" wordCount="42" paraCount="1" cursorPos="239" />
|
||||
<name status="s90e6c9" import="ia857f0" active="yes">Old File</name>
|
||||
</item>
|
||||
<item handle="09f22ae8369f7" parent="None" root="09f22ae8369f7" order="5" type="ROOT" class="TEMPLATES">
|
||||
<item handle="f4ed1ae756a1f" parent="None" root="f4ed1ae756a1f" order="5" type="ROOT" class="TEMPLATE">
|
||||
<meta expanded="yes" />
|
||||
<name status="sf12341" import="ia857f0">Templates</name>
|
||||
</item>
|
||||
<item handle="5ee8aebcdebc9" parent="09f22ae8369f7" root="09f22ae8369f7" order="0" type="FILE" class="TEMPLATES" layout="NOTE">
|
||||
<meta expanded="no" heading="H1" charCount="52" wordCount="7" paraCount="1" cursorPos="75" />
|
||||
<name status="sf12341" import="ia857f0" active="yes">Character Note</name>
|
||||
</item>
|
||||
<item handle="5aec885635c85" parent="09f22ae8369f7" root="09f22ae8369f7" order="1" type="FILE" class="TEMPLATES" layout="DOCUMENT">
|
||||
<item handle="5aec885635c85" parent="f4ed1ae756a1f" root="f4ed1ae756a1f" order="0" type="FILE" class="TEMPLATE" layout="DOCUMENT">
|
||||
<meta expanded="no" heading="H3" charCount="9" wordCount="2" paraCount="1" cursorPos="79" />
|
||||
<name status="sf12341" import="ia857f0" active="yes">Scene</name>
|
||||
</item>
|
||||
<item handle="2a60782759c6f" parent="f4ed1ae756a1f" root="f4ed1ae756a1f" order="1" type="FILE" class="TEMPLATE" layout="DOCUMENT">
|
||||
<meta expanded="no" heading="H2" charCount="7" wordCount="1" paraCount="0" cursorPos="3" />
|
||||
<name status="sf12341" import="ia857f0" active="yes">Chapter</name>
|
||||
</item>
|
||||
<item handle="5ee8aebcdebc9" parent="f4ed1ae756a1f" root="f4ed1ae756a1f" order="2" type="FILE" class="TEMPLATE" layout="NOTE">
|
||||
<meta expanded="no" heading="H1" charCount="53" wordCount="7" paraCount="1" cursorPos="75" />
|
||||
<name status="sf12341" import="ia857f0" active="yes">Character Note</name>
|
||||
</item>
|
||||
<item handle="98acd8c76c93a" parent="None" root="98acd8c76c93a" order="6" type="ROOT" class="TRASH">
|
||||
<meta expanded="yes" />
|
||||
<name status="sf12341" import="ia857f0">Trash</name>
|
||||
|
||||
Reference in New Issue
Block a user