Add a custom templates menu to project tree add items

This commit is contained in:
Veronica Berglyd Olsen
2024-02-08 00:06:01 +01:00
parent bbb23b4bc9
commit fe8a112328
6 changed files with 158 additions and 60 deletions
+134 -46
View File
@@ -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:
+1 -1
View File
@@ -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()
+7
View File
@@ -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 -2
View File
@@ -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 -1
View File
@@ -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
View File
@@ -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>