Add file template feature (#1688)

This commit is contained in:
Veronica Berglyd Olsen
2024-02-08 23:17:18 +01:00
committed by GitHub
27 changed files with 505 additions and 115 deletions
@@ -40,6 +40,7 @@ cls_none = typ_cancel.svg
cls_novel = typ_book.svg
cls_object = typ_key.svg
cls_plot = typ_puzzle.svg
cls_template = typ_document-add-col.svg
cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg
cls_world = typ_location.svg
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="m5.8898 3.4449c-0.67467 1e-7 -1.2225 0.5478-1.2225 1.2225v14.665c0 0.67466 0.5478 1.2225 1.2225 1.2225h12.22c0.67466 0 1.2225-0.5478 1.2225-1.2225v-11h-1.8326c-1.6854 0-3.0551-1.3696-3.0551-3.0551v-1.8326zm9.7775 0.50488v1.3277c0 1.0108 0.82184 1.8326 1.8326 1.8326h1.3277z" fill="#e5e5e5" stroke-linecap="round" stroke-width=".55"/>
<path d="m5.8898 1c-2.0216 0-3.6674 1.6458-3.6674 3.6674v14.665c0 2.0216 1.6458 3.6674 3.6674 3.6674h12.22c2.0216 0 3.6674-1.6458 3.6674-3.6674v-12.222c0-0.32511-0.12901-0.63512-0.35879-0.86367l-4.8877-4.8877c-0.22856-0.22978-0.53856-0.35879-0.86367-0.35879zm0 2.4449h8.5551v1.8326c0 1.6854 1.3696 3.0551 3.0551 3.0551h1.8326v11c0 0.67466-0.5478 1.2225-1.2225 1.2225h-12.22c-0.67467 0-1.2225-0.5478-1.2225-1.2225v-14.665c0-0.67467 0.5478-1.2225 1.2225-1.2225zm9.7775 0.50488 3.1604 3.1604h-1.3277c-1.0108 0-1.8326-0.82184-1.8326-1.8326z" fill="#aeaeae" stroke-width="1.2222"/>
<path d="m12 6c-0.82934 0-1.5 0.67081-1.5 1.5003v3.0007h-3c-0.82934 0-1.5 0.67081-1.5 1.5003 0 0.82952 0.67066 1.5003 1.5 1.5003h3v3.0007c0 0.82952 0.67066 1.4977 1.5 1.4977 0.82934 0 1.5-0.66817 1.5-1.4977v-3.0007h3c0.82934 0 1.5-0.67081 1.5-1.5003 1e-6 -0.82952-0.67066-1.5003-1.5-1.5003h-3v-3.0007c0-0.82952-0.67066-1.5003-1.5-1.5003z" fill="#69c" stroke-width="1.4999"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

@@ -40,6 +40,7 @@ cls_none = typ_cancel.svg
cls_novel = typ_book.svg
cls_object = typ_key.svg
cls_plot = typ_puzzle.svg
cls_template = typ_document-add-col.svg
cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg
cls_world = typ_location.svg
@@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="24" height="24" version="1.2" viewBox="0 0 24 24" xmlns="http://www.w3.org/2000/svg">
<g>
<path d="m5.8898 3.4449c-0.67467 1e-7 -1.2225 0.5478-1.2225 1.2225v14.665c0 0.67466 0.5478 1.2225 1.2225 1.2225h12.22c0.67466 0 1.2225-0.5478 1.2225-1.2225v-11h-1.8326c-1.6854 0-3.0551-1.3696-3.0551-3.0551v-1.8326zm9.7775 0.50488v1.3277c0 1.0108 0.82184 1.8326 1.8326 1.8326h1.3277z" fill="#ededed" stroke-linecap="round" stroke-width=".55"/>
<path d="m5.8898 1c-2.0216 0-3.6674 1.6458-3.6674 3.6674v14.665c0 2.0216 1.6458 3.6674 3.6674 3.6674h12.22c2.0216 0 3.6674-1.6458 3.6674-3.6674v-12.222c0-0.32511-0.12901-0.63512-0.35879-0.86367l-4.8877-4.8877c-0.22856-0.22978-0.53856-0.35879-0.86367-0.35879zm0 2.4449h8.5551v1.8326c0 1.6854 1.3696 3.0551 3.0551 3.0551h1.8326v11c0 0.67466-0.5478 1.2225-1.2225 1.2225h-12.22c-0.67467 0-1.2225-0.5478-1.2225-1.2225v-14.665c0-0.67467 0.5478-1.2225 1.2225-1.2225zm9.7775 0.50488 3.1604 3.1604h-1.3277c-1.0108 0-1.8326-0.82184-1.8326-1.8326z" fill="#333" stroke-width="1.2222"/>
<path d="m12 6c-0.82934 0-1.5 0.67081-1.5 1.5003v3.0007h-3c-0.82934 0-1.5 0.67081-1.5 1.5003 0 0.82952 0.67066 1.5003 1.5 1.5003h3v3.0007c0 0.82952 0.67066 1.4977 1.5 1.4977 0.82934 0 1.5-0.66817 1.5-1.4977v-3.0007h3c0.82934 0 1.5-0.67081 1.5-1.5003 1e-6 -0.82952-0.67066-1.5003-1.5-1.5003h-3v-3.0007c0-0.82952-0.67066-1.5003-1.5-1.5003z" fill="#4271ae" stroke-width="1.4999"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

+2
View File
@@ -186,6 +186,7 @@ class nwLabels:
nwItemClass.ENTITY: QT_TRANSLATE_NOOP("Constant", "Entities"),
nwItemClass.CUSTOM: QT_TRANSLATE_NOOP("Constant", "Custom"),
nwItemClass.ARCHIVE: QT_TRANSLATE_NOOP("Constant", "Archive"),
nwItemClass.TEMPLATE: QT_TRANSLATE_NOOP("Constant", "Templates"),
nwItemClass.TRASH: QT_TRANSLATE_NOOP("Constant", "Trash"),
}
CLASS_ICON = {
@@ -199,6 +200,7 @@ class nwLabels:
nwItemClass.ENTITY: "cls_entity",
nwItemClass.CUSTOM: "cls_custom",
nwItemClass.ARCHIVE: "cls_archive",
nwItemClass.TEMPLATE: "cls_template",
nwItemClass.TRASH: "cls_trash",
}
LAYOUT_NAME = {
+3 -2
View File
@@ -485,8 +485,9 @@ class NWIndex:
# For a tag, only the first value is accepted, the rest are ignored
if tBits[0] == nwKeyWords.TAG_KEY and nBits > 1:
if tBits[1] in self._tagsIndex:
isGood[1] = self._tagsIndex.tagHandle(tBits[1]) == tHandle
check, _ = self.parseValue(tBits[1])
if check in self._tagsIndex:
isGood[1] = self._tagsIndex.tagHandle(check) == tHandle
else:
isGood[1] = True
return isGood
+21 -3
View File
@@ -334,15 +334,33 @@ class NWItem:
def isNovelLike(self) -> bool:
"""Check if the item is of a novel-like class."""
return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE)
return self._class in (
nwItemClass.NOVEL,
nwItemClass.ARCHIVE,
nwItemClass.TEMPLATE,
)
def isTemplateFile(self) -> bool:
"""Check if the item is a template file."""
return self._type == nwItemType.FILE and self._class == nwItemClass.TEMPLATE
def documentAllowed(self) -> bool:
"""Check if the item is allowed to be of document layout."""
return self._class in (nwItemClass.NOVEL, nwItemClass.ARCHIVE, nwItemClass.TRASH)
return self._class in (
nwItemClass.NOVEL,
nwItemClass.ARCHIVE,
nwItemClass.TEMPLATE,
nwItemClass.TRASH,
)
def isInactiveClass(self) -> bool:
"""Check if the item is in an inactive class."""
return self._class in (nwItemClass.NO_CLASS, nwItemClass.ARCHIVE, nwItemClass.TRASH)
return self._class in (
nwItemClass.NO_CLASS,
nwItemClass.ARCHIVE,
nwItemClass.TEMPLATE,
nwItemClass.TRASH,
)
def isRootType(self) -> bool:
"""Check if item is a root item."""
+30 -7
View File
@@ -175,24 +175,47 @@ class NWProject:
will not run if the file exists and is not empty.
"""
tItem = self._tree[tHandle]
if tItem is None:
return False
if not tItem.isFileType():
if not (tItem and tItem.isFileType()):
return False
newDoc = self._storage.getDocument(tHandle)
if (newDoc.readDocument() or "").strip():
return False
hshText = "#"*minmax(hLevel, 1, 4)
newText = f"{hshText} {tItem.itemName}\n\n{text}"
indent = "#"*minmax(hLevel, 1, 4)
text = f"{indent} {tItem.itemName}\n\n{text}"
if tItem.isNovelLike() and isDocument:
tItem.setLayout(nwItemLayout.DOCUMENT)
else:
tItem.setLayout(nwItemLayout.NOTE)
newDoc.writeDocument(newText)
self._index.scanText(tHandle, newText)
newDoc.writeDocument(text)
self._index.scanText(tHandle, text)
return True
def copyFileContent(self, tHandle: str, sHandle: str) -> bool:
"""Copy content to a new document after it is created. This
will not run if the file exists and is not empty.
"""
tItem = self._tree[tHandle]
if not (tItem and tItem.isFileType()):
return False
sItem = self._tree[sHandle]
if not (sItem and sItem.isFileType()):
return False
newDoc = self._storage.getDocument(tHandle)
if (newDoc.readDocument() or "").strip():
return False
logger.debug("Populating '%s' with text from '%s'", tHandle, sHandle)
text = self._storage.getDocument(sHandle).readDocument() or ""
newDoc.writeDocument(text)
sItem.setLayout(tItem.itemLayout)
self._index.scanText(tHandle, text)
return True
+2 -1
View File
@@ -106,7 +106,8 @@ class ProjectXMLReader:
as attributes. The id attribute was also added to the project.
Rev 1: Drops the titleFormat node from settings. 2.1 Beta 1.
Rev 2: Drops the title node from project. 2.3 Beta 1.
Rev 2: Drops the title node from project and adds the TEMPLATE
class for items. 2.3 Beta 1.
"""
def __init__(self, path: str | Path) -> None:
+1 -1
View File
@@ -48,7 +48,7 @@ class GuiEditLabel(QDialog):
mSp = CONFIG.pxInt(12)
# Item Label
self.labelValue = QLineEdit()
self.labelValue = QLineEdit(self)
self.labelValue.setMinimumWidth(mVd)
self.labelValue.setMaxLength(200)
self.labelValue.setText(text)
+3 -3
View File
@@ -361,7 +361,7 @@ class _StatusPage(NFixedPage):
self.dnButton.clicked.connect(lambda: self._moveItem(1))
# Edit Form
self.editName = QLineEdit()
self.editName = QLineEdit(self)
self.editName.setMaxLength(40)
self.editName.setPlaceholderText(self.tr("Select item to edit"))
self.editName.setEnabled(False)
@@ -621,12 +621,12 @@ class _ReplacePage(NFixedPage):
self.delButton.clicked.connect(self._delEntry)
# Edit Form
self.editKey = QLineEdit()
self.editKey = QLineEdit(self)
self.editKey.setPlaceholderText(self.tr("Select item to edit"))
self.editKey.setEnabled(False)
self.editKey.setMaxLength(40)
self.editValue = QLineEdit()
self.editValue = QLineEdit(self)
self.editValue.setEnabled(False)
self.editValue.setMaxLength(80)
+1 -1
View File
@@ -75,7 +75,7 @@ class GuiWordList(QDialog):
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.listBox.setSortingEnabled(True)
self.newEntry = QLineEdit()
self.newEntry = QLineEdit(self)
self.addButton = QPushButton(SHARED.theme.getIcon("add"), "")
self.addButton.clicked.connect(self._doAdd)
+2 -1
View File
@@ -47,7 +47,8 @@ class nwItemClass(Enum):
ENTITY = 7
CUSTOM = 8
ARCHIVE = 9
TRASH = 10
TEMPLATE = 10
TRASH = 11
# END Enum nwItemClass
+8 -9
View File
@@ -60,8 +60,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
logger.debug("Create: GuiDocHighlighter")
self._tItem = None
self._tHandle = None
self._isInactive = False
self._spellCheck = False
self._spellErr = QTextCharFormat()
@@ -238,11 +238,10 @@ class GuiDocHighlighter(QSyntaxHighlighter):
def setHandle(self, tHandle: str) -> None:
"""Set the handle of the currently highlighted document."""
self._tHandle = tHandle
self._tItem = SHARED.project.tree[tHandle]
logger.debug(
"Syntax highlighter %s for item '%s'",
"enabled" if self._tItem else "disabled", tHandle
self._isInactive = (
item.isInactiveClass() if (item := SHARED.project.tree[tHandle]) else False
)
logger.debug("Syntax highlighter enabled for item '%s'", tHandle)
return
##
@@ -286,16 +285,16 @@ class GuiDocHighlighter(QSyntaxHighlighter):
for n, bit in enumerate(bits):
xPos = pos[n]
xLen = len(bit)
if not isGood[n]:
self.setFormat(xPos, xLen, self._hStyles["codeinval"])
elif n == 0:
if n == 0 and isGood[n]:
self.setFormat(xPos, xLen, self._hStyles["keyword"])
else:
elif isGood[n] and not self._isInactive:
one, two = index.parseValue(bit)
self.setFormat(xPos, len(one), self._hStyles["value"])
if two:
yPos = xPos + len(bit) - len(two)
self.setFormat(yPos, len(two), self._hStyles["optional"])
elif not self._isInactive:
self.setFormat(xPos, xLen, self._hStyles["codeinval"])
# We never want to run the spell checker on keyword/values,
# so we force a return here
+146 -43
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,9 @@ 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)
self.projBar.newDocumentFromTemplate.connect(self.createFileFromTemplate)
# Function Mappings
self.emptyTrash = self.projTree.emptyTrash
@@ -157,7 +161,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 +170,7 @@ class GuiProjectView(QWidget):
def openProjectTasks(self) -> None:
"""Run open project tasks."""
self.projBar.buildQuickLinkMenu()
self.projBar.buildQuickLinksMenu()
self.projBar.setEnabled(True)
return
@@ -212,10 +216,17 @@ class GuiProjectView(QWidget):
@pyqtSlot(str)
def updateItemValues(self, tHandle: str) -> None:
"""Update tree item"""
"""Update tree item."""
self.projTree.setTreeItemValues(tHandle)
return
@pyqtSlot(str)
def createFileFromTemplate(self, tHandle: str) -> None:
"""Create a new document from a template."""
logger.debug("Template selected: '%s'", tHandle)
self.projTree.newTreeItem(nwItemType.FILE, copyDoc=tHandle)
return
@pyqtSlot(str, int, int, int)
def updateCounts(self, tHandle: str, cCount: int, wCount: int, pCount: int) -> None:
"""Slot for updating the word count of a specific item."""
@@ -225,8 +236,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 +245,8 @@ class GuiProjectView(QWidget):
class GuiProjectToolBar(QWidget):
newDocumentFromTemplate = pyqtSignal(str)
def __init__(self, projView: GuiProjectView) -> None:
super().__init__(parent=projView)
@@ -303,6 +316,11 @@ class GuiProjectToolBar(QWidget):
lambda: self.projTree.newTreeItem(nwItemType.FOLDER)
)
self.mTemplates = _UpdatableMenu(self.mAdd)
self.mTemplates.setActionsVisible(False)
self.mTemplates.menuItemTriggered.connect(lambda h: self.newDocumentFromTemplate.emit(h))
self.mAdd.addMenu(self.mTemplates)
self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"]))
self._buildRootMenu()
@@ -383,7 +401,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 +409,48 @@ 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, NWItem, QIcon)
def treeItemRefreshed(self, tHandle: str, nwItem: NWItem, icon: QIcon) -> None:
"""Process change in tree items to update menu content."""
if nwItem.isTemplateFile() and nwItem.isActive:
self.mTemplates.addUpdate(tHandle, nwItem.itemName, 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
##
# Internal Functions
##
@@ -421,7 +466,6 @@ class GuiProjectToolBar(QWidget):
self.mAddRoot.clear()
addClass(nwItemClass.NOVEL)
addClass(nwItemClass.ARCHIVE)
self.mAddRoot.addSeparator()
addClass(nwItemClass.PLOT)
addClass(nwItemClass.CHARACTER)
@@ -430,26 +474,12 @@ class GuiProjectToolBar(QWidget):
addClass(nwItemClass.OBJECT)
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
@@ -464,6 +494,8 @@ class GuiProjectTree(QTreeWidget):
D_HANDLE = Qt.ItemDataRole.UserRole
D_WORDS = Qt.ItemDataRole.UserRole + 1
itemRefreshed = pyqtSignal(str, NWItem, QIcon)
def __init__(self, projView: GuiProjectView) -> None:
super().__init__(parent=projView)
@@ -587,7 +619,7 @@ class GuiProjectTree(QTreeWidget):
return False
def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None,
hLevel: int = 1, isNote: bool = False) -> bool:
hLevel: int = 1, isNote: bool = False, copyDoc: str | None = None) -> bool:
"""Add new item to the tree, with a given itemType (and
itemClass if Root), and attach it to the selected handle. Also
make sure the item is added in a place it can be added, and that
@@ -627,7 +659,10 @@ class GuiProjectTree(QTreeWidget):
# Set default label and determine if new item is to be added
# as child or sibling to the selected item
if itemType == nwItemType.FILE:
if isNote:
if copyDoc and (cItem := SHARED.project.tree[copyDoc]):
newLabel = cItem.itemName
asChild = sIsParent and pItem.isDocumentLayout()
elif isNote:
newLabel = self.tr("New Note")
asChild = sIsParent
elif hLevel == 2:
@@ -675,7 +710,9 @@ class GuiProjectTree(QTreeWidget):
return True
# Handle new file creation
if itemType == nwItemType.FILE and hLevel > 0:
if itemType == nwItemType.FILE and copyDoc:
SHARED.project.copyFileContent(tHandle, copyDoc)
elif itemType == nwItemType.FILE and hLevel > 0:
SHARED.project.writeNewFile(tHandle, hLevel, not isNote)
# Add the new item to the project tree
@@ -770,8 +807,7 @@ class GuiProjectTree(QTreeWidget):
def renameTreeItem(self, tHandle: str, name: str = "") -> None:
"""Open a dialog to edit the label of an item."""
tItem = SHARED.project.tree[tHandle]
if tItem:
if tItem := SHARED.project.tree[tHandle]:
newLabel, dlgOk = GuiEditLabel.getLabel(self, text=name or tItem.itemName)
if dlgOk:
tItem.setName(newLabel)
@@ -1029,6 +1065,9 @@ class GuiProjectTree(QTreeWidget):
trFont.setUnderline(hLevel == "H1")
trItem.setFont(self.C_NAME, trFont)
# Emit Refresh Signal
self.itemRefreshed.emit(tHandle, nwItem, itemIcon)
return
def propagateCount(self, tHandle: str, newCount: int, countChildren: bool = False) -> None:
@@ -1358,8 +1397,7 @@ class GuiProjectTree(QTreeWidget):
not including) a given item.
"""
if isinstance(trItem, QTreeWidgetItem):
chCount = trItem.childCount()
for i in range(chCount):
for i in range(trItem.childCount()):
chItem = trItem.child(i)
chItem.setExpanded(isExpanded)
self._recursiveSetExpanded(chItem, isExpanded)
@@ -1633,6 +1671,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(False)
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
@@ -441,7 +441,7 @@ class GuiIcons:
# Class Icons
"cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none", "cls_novel",
"cls_object", "cls_plot", "cls_timeline", "cls_trash", "cls_world",
"cls_object", "cls_plot", "cls_template", "cls_timeline", "cls_trash", "cls_world",
# Search Icons
"search_cancel", "search_case", "search_loop", "search_preserve", "search_project",
+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()
+3 -3
View File
@@ -99,7 +99,7 @@ class GuiWelcome(QDialog):
self.tabNew.cancelNewProject.connect(self._showOpenProjectPage)
self.tabNew.openProjectRequest.connect(self._openProjectPath)
self.mainStack = QStackedWidget()
self.mainStack = QStackedWidget(self)
self.mainStack.addWidget(self.tabOpen)
self.mainStack.addWidget(self.tabNew)
@@ -524,13 +524,13 @@ class _NewProjectForm(QWidget):
# ================
# Project Name
self.projName = QLineEdit()
self.projName = QLineEdit(self)
self.projName.setMaxLength(200)
self.projName.setPlaceholderText(self.tr("Required"))
self.projName.textChanged.connect(self._updateProjPath)
# Author(s)
self.projAuthor = QLineEdit()
self.projAuthor = QLineEdit(self)
self.projAuthor.setMaxLength(200)
self.projAuthor.setPlaceholderText(self.tr("Optional"))
+12
View File
@@ -0,0 +1,12 @@
%%~name: Chapter
%%~path: f4ed1ae756a1f/2a60782759c6f
%%~kind: TEMPLATE/DOCUMENT
%%~hash: da17bc023f9fcf9d739f19ce4b49a4f95dd7acc2
%%~date: 2024-02-04 12:13:08/2024-02-08 21:45:26
## Chapter
@pov: Name
%Synopsis: A brief note about the chapter content.
Text
+12
View File
@@ -0,0 +1,12 @@
%%~name: Scene
%%~path: 09f22ae8369f7/5aec885635c85
%%~kind: TEMPLATE/DOCUMENT
%%~hash: 05c9750b00020f8cf9a7bc7362bf19681022cc03
%%~date: 2024-02-02 10:58:47/2024-02-02 10:59:32
### Scene
@pov: Name
%Synopsis: A brief note about the scene content.
Text
+12
View File
@@ -0,0 +1,12 @@
%%~name: Character Note
%%~path: 09f22ae8369f7/5ee8aebcdebc9
%%~kind: TEMPLATE/NOTE
%%~hash: 6230bb6f796e708a72f4c29dc79d63dc90a47ba5
%%~date: 2024-02-02 10:54:52/2024-02-02 10:56:05
# Character Name
@tag: Name
## Motivation
What motivates the character?
+21 -5
View File
@@ -1,6 +1,6 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.3a3" hexVersion="0x020300a3" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-29 17:42:31">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1660" autoCount="256" editTime="81969">
<novelWriterXML appVersion="2.3a4" hexVersion="0x020300a4" fileVersion="1.5" fileRevision="2" timeStamp="2024-02-08 21:45:35">
<project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1680" autoCount="263" editTime="82745">
<name>Sample Project</name>
<author>Jane Smith</author>
</project>
@@ -20,7 +20,7 @@
<entry key="C">D</entry>
</autoReplace>
<status>
<entry key="sf12341" count="4" 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="27" novelWords="987" notesWords="409">
<content items="31" novelWords="991" 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,7 +136,23 @@
<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="98acd8c76c93a" parent="None" root="98acd8c76c93a" order="5" type="ROOT" class="TRASH">
<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="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="78" />
<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="11" wordCount="2" paraCount="1" cursorPos="81" />
<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>
</item>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?>
<novelWriterXML appVersion="2.3a1" hexVersion="0x020300a1" fileVersion="1.5" fileRevision="2" timeStamp="2024-01-26 22:50:57">
<novelWriterXML appVersion="2.3a4" hexVersion="0x020300a4" fileVersion="1.5" fileRevision="2" timeStamp="2024-02-08 21:34:07">
<project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0">
<name>New Project</name>
<author>Jane Doe</author>
@@ -22,13 +22,13 @@
<entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
</status>
<importance>
<entry key="i000004" count="4" red="100" green="100" blue="100">New</entry>
<entry key="i000004" count="5" red="100" green="100" blue="100">New</entry>
<entry key="i000005" count="0" red="200" green="50" blue="0">Minor</entry>
<entry key="i000006" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
</importance>
</settings>
<content items="11" novelWords="10" notesWords="3">
<content items="12" novelWords="10" notesWords="6">
<item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="no" />
<name status="s000000" import="i000004">Novel</name>
@@ -73,5 +73,9 @@
<meta expanded="no" heading="H1" charCount="11" wordCount="3" paraCount="1" cursorPos="0" />
<name status="s000000" import="i000004" active="yes">Jane</name>
</item>
<item handle="0000000000013" parent="000000000000a" root="000000000000a" order="0" type="FILE" class="CHARACTER" layout="NOTE">
<meta expanded="no" heading="H1" charCount="11" wordCount="3" paraCount="1" cursorPos="0" />
<name status="s000000" import="i000004" active="yes">John</name>
</item>
</content>
</novelWriterXML>
+4
View File
@@ -294,6 +294,10 @@ def testCoreIndex_CheckThese(mockGUI, fncPath, mockRnd):
assert index.parseValue("Jane | Jane Smith") == ("Jane", "Jane Smith")
assert index.parseValue("Jane | Jane Smith") == ("Jane", "Jane Smith")
# Duplicates with Display Name
assert index.checkThese(["@tag", "Jane | Jane Doe"], cHandle) == [True, True]
assert index.checkThese(["@tag", "Jane | Jane Smith"], nHandle) == [True, False]
project.closeProject()
# END Test testCoreIndex_CheckThese
+19
View File
@@ -365,6 +365,7 @@ def testCoreItem_ClassSetter(mockGUI):
"""
project = NWProject()
item = NWItem(project, "0000000000000")
item.setType(nwItemType.FILE)
# Class
item.setClass(None)
@@ -377,66 +378,84 @@ def testCoreItem_ClassSetter(mockGUI):
assert item.isNovelLike() is False
assert item.documentAllowed() is False
assert item.isInactiveClass() is True
assert item.isTemplateFile() is False
item.setClass("NOVEL")
assert item.itemClass == nwItemClass.NOVEL
assert item.isNovelLike() is True
assert item.documentAllowed() is True
assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("PLOT")
assert item.itemClass == nwItemClass.PLOT
assert item.isNovelLike() is False
assert item.documentAllowed() is False
assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("CHARACTER")
assert item.itemClass == nwItemClass.CHARACTER
assert item.isNovelLike() is False
assert item.documentAllowed() is False
assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("WORLD")
assert item.itemClass == nwItemClass.WORLD
assert item.isNovelLike() is False
assert item.documentAllowed() is False
assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("TIMELINE")
assert item.itemClass == nwItemClass.TIMELINE
assert item.isNovelLike() is False
assert item.documentAllowed() is False
assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("OBJECT")
assert item.itemClass == nwItemClass.OBJECT
assert item.isNovelLike() is False
assert item.documentAllowed() is False
assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("ENTITY")
assert item.itemClass == nwItemClass.ENTITY
assert item.isNovelLike() is False
assert item.documentAllowed() is False
assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("CUSTOM")
assert item.itemClass == nwItemClass.CUSTOM
assert item.isNovelLike() is False
assert item.documentAllowed() is False
assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("ARCHIVE")
assert item.itemClass == nwItemClass.ARCHIVE
assert item.isNovelLike() is True
assert item.documentAllowed() is True
assert item.isInactiveClass() is True
assert item.isTemplateFile() is False
item.setClass("TEMPLATE")
assert item.itemClass == nwItemClass.TEMPLATE
assert item.isNovelLike() is True
assert item.documentAllowed() is True
assert item.isInactiveClass() is True
assert item.isTemplateFile() is True
item.setClass("TRASH")
assert item.itemClass == nwItemClass.TRASH
assert item.isNovelLike() is False
assert item.documentAllowed() is True
assert item.isInactiveClass() is True
assert item.isTemplateFile() is False
# Alternative
item.setClass(nwItemClass.NOVEL)
+62 -25
View File
@@ -102,34 +102,61 @@ def testCoreProject_NewFileFolder(monkeypatch, fncPath, tstPaths, mockGUI, mockR
mockRnd.reset()
buildTestProject(project, fncPath)
aHandle = "0000000000010"
bHandle = "0000000000011"
cHandle = "0000000000012"
dHandle = "0000000000013"
xHandle = "1234567890abc"
# Invalid call
assert project.newFolder("New Folder", "1234567890abc") is None
assert project.newFile("New File", "1234567890abc") is None
assert project.newFolder("New Folder", xHandle) is None
assert project.newFile("New File", xHandle) is None
# Add files properly
assert project.newFolder("Stuff", C.hNovelRoot) == "0000000000010"
assert project.newFile("Hello", "0000000000010") == "0000000000011"
assert project.newFile("Jane", C.hCharRoot) == "0000000000012"
assert project.newFolder("Stuff", C.hNovelRoot) == aHandle
assert project.newFile("Hello", aHandle) == bHandle
assert project.newFile("Jane", C.hCharRoot) == cHandle
assert project.newFile("John", C.hCharRoot) == dHandle
assert "0000000000010" in project.tree
assert "0000000000011" in project.tree
assert "0000000000012" in project.tree
assert aHandle in project.tree
assert bHandle in project.tree
assert cHandle in project.tree
assert dHandle in project.tree
# Write to file, failed
assert project.writeNewFile("blabla", 1, True) is False # Not a handle
assert project.writeNewFile("0000000000010", 1, True) is False # Not a file
assert project.writeNewFile(aHandle, 1, True) is False # Not a file
assert project.writeNewFile(C.hTitlePage, 1, True) is False # Already has content
# Write to file, success
assert project.writeNewFile("0000000000011", 2, True) is True
assert project.storage.getDocument("0000000000011").readDocument() == "## Hello\n\n"
assert project.writeNewFile(bHandle, 2, True) is True
assert project.storage.getDocument(bHandle).readDocument() == "## Hello\n\n"
# Write to file with additional text, success
assert project.writeNewFile("0000000000012", 1, False, "Hi Jane\n\n") is True
assert project.storage.getDocument("0000000000012").readDocument() == (
assert project.writeNewFile(cHandle, 1, False, "Hi Jane\n\n") is True
assert project.storage.getDocument(cHandle).readDocument() == (
"# Jane\n\nHi Jane\n\n"
)
# Copy file content, invalid target
assert project.copyFileContent(xHandle, cHandle) is False # Unknown handle
assert project.copyFileContent(aHandle, cHandle) is False # Target is a folder
# Copy file content, invalid source
assert project.copyFileContent(dHandle, xHandle) is False # Unknown handle
assert project.copyFileContent(dHandle, aHandle) is False # Source is a folder
# Copy file content, target not empty
assert project.copyFileContent(bHandle, cHandle) is False
# Copy file content, success
assert project.copyFileContent(dHandle, cHandle) is True
cText = project.storage.getDocument(cHandle).readDocument()
dText = project.storage.getDocument(dHandle).readDocument()
assert dText == cText
# Save, close and check
assert project.projChanged is True
assert project.saveProject() is True
@@ -141,23 +168,27 @@ def testCoreProject_NewFileFolder(monkeypatch, fncPath, tstPaths, mockGUI, mockR
# Delete new file, but block access
with monkeypatch.context() as mp:
mp.setattr("pathlib.Path.unlink", causeOSError)
assert project.removeItem("0000000000011") is False
assert "0000000000011" in project.tree
assert project.removeItem(bHandle) is False
assert bHandle in project.tree
# Delete new files and folders
assert (fncPath / "content" / "0000000000012.nwd").exists()
assert (fncPath / "content" / "0000000000011.nwd").exists()
assert (fncPath / "content" / f"{dHandle}.nwd").exists()
assert (fncPath / "content" / f"{cHandle}.nwd").exists()
assert (fncPath / "content" / f"{bHandle}.nwd").exists()
assert project.removeItem("0000000000012") is True
assert project.removeItem("0000000000011") is True
assert project.removeItem("0000000000010") is True
assert project.removeItem(dHandle) is True
assert project.removeItem(cHandle) is True
assert project.removeItem(bHandle) is True
assert project.removeItem(aHandle) is True
assert not (fncPath / "content" / "0000000000012.nwd").exists()
assert not (fncPath / "content" / "0000000000011.nwd").exists()
assert not (fncPath / "content" / f"{dHandle}.nwd").exists()
assert not (fncPath / "content" / f"{cHandle}.nwd").exists()
assert not (fncPath / "content" / f"{bHandle}.nwd").exists()
assert "0000000000010" not in project.tree
assert "0000000000011" not in project.tree
assert "0000000000012" not in project.tree
assert aHandle not in project.tree
assert bHandle not in project.tree
assert cHandle not in project.tree
assert dHandle not in project.tree
project.closeProject()
@@ -177,6 +208,8 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
caplog.clear()
assert project.openProject(fooBar) is False
assert "Not a known project file format." in caplog.text
assert project.isValid is False
assert project.lockStatus is None
# No project file
caplog.clear()
@@ -204,10 +237,14 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
# Open again should fail on lock file
assert project.openProject(fncPath) is False
assert project.state == NWProjectState.LOCKED
assert project.isValid is True
assert project.lockStatus is not None
# Force re-open
assert project.openProject(fncPath, clearLock=True) is True
assert project.state == NWProjectState.READY
assert project.isValid is True
assert project.lockStatus is None
# Fail getting xml reader
with monkeypatch.context() as mp:
+114 -6
View File
@@ -115,15 +115,28 @@ def testGuiProjTree_NewItems(qtbot, caplog, monkeypatch, nwGUI, projPath, mockRn
assert nwGUI.docEditor.getText() == "### New Scene\n\n"
assert projTree._getItemWordCount("0000000000014") == 2
# Add a new scene with the content copied from the previous
assert nwGUI.openDocument("0000000000014")
nwGUI.docEditor.setPlainText("### New Scene\n\nWith Stuff\n\n")
nwGUI.saveDocument()
projView.setSelectedHandle("0000000000014")
assert projTree.newTreeItem(nwItemType.FILE, copyDoc="0000000000014") is True
assert project.tree["0000000000015"].itemParent == "0000000000011" # type: ignore
assert project.tree["0000000000015"].itemRoot == C.hNovelRoot # type: ignore
assert project.tree["0000000000015"].itemClass == nwItemClass.NOVEL # type: ignore
assert nwGUI.openDocument("0000000000015")
assert nwGUI.docEditor.getText() == "### New Scene\n\nWith Stuff\n\n"
assert projTree._getItemWordCount("0000000000015") == 4
# Add a new file to the characters folder
projView.setSelectedHandle(C.hCharRoot)
assert projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True
assert project.tree["0000000000015"].itemParent == C.hCharRoot # type: ignore
assert project.tree["0000000000015"].itemRoot == C.hCharRoot # type: ignore
assert project.tree["0000000000015"].itemClass == nwItemClass.CHARACTER # type: ignore
assert nwGUI.openDocument("0000000000015")
assert project.tree["0000000000016"].itemParent == C.hCharRoot # type: ignore
assert project.tree["0000000000016"].itemRoot == C.hCharRoot # type: ignore
assert project.tree["0000000000016"].itemClass == nwItemClass.CHARACTER # type: ignore
assert nwGUI.openDocument("0000000000016")
assert nwGUI.docEditor.getText() == "# New Note\n\n"
assert projTree._getItemWordCount("0000000000015") == 2
assert projTree._getItemWordCount("0000000000016") == 2
# Make sure the sibling folder bug trap works
projView.setSelectedHandle("0000000000013")
@@ -1112,7 +1125,7 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd)
@pytest.mark.gui
def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the building of the project tree context menu. All this does
is test that the menu builds. It doesn't open the actual menu,
is test that the menu builds. It doesn't open the actual menu.
"""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
@@ -1392,3 +1405,98 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# qtbot.stop()
# END Test testGuiProjTree_ContextMenu
@pytest.mark.gui
def testGuiProjTree_Templates(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the templates feature of the project tree."""
monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
# Create a project
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
nwGUI.switchFocus(nwWidget.TREE)
nwGUI.show()
project = SHARED.project
projView = nwGUI.projView
projTree = projView.projTree
projBar = projView.projBar
# Handles for new objects
hTemplatesRoot = "0000000000010"
hSceneTemplate = "0000000000011"
hNoteTemplate = "0000000000012"
hNewScene = "0000000000013"
hNewCharacter = "0000000000014"
# Add template folder
projTree.newTreeItem(nwItemType.ROOT, nwItemClass.TEMPLATE)
nwTemplateRoot = project.tree[hTemplatesRoot]
assert nwTemplateRoot is not None
assert nwTemplateRoot.itemName == "Templates"
# Add a scene template
projTree.setSelectedHandle(hTemplatesRoot)
projTree.newTreeItem(nwItemType.FILE, hLevel=3, isNote=False)
nwSceneTemplate = project.tree[hSceneTemplate]
assert nwSceneTemplate is not None
assert nwSceneTemplate.itemName == "New Scene"
assert projBar.mTemplates.actions()[0].text() == "New Scene"
# Rename the scene template
with qtbot.waitSignal(projTree.itemRefreshed, timeout=1000) as signal:
projTree.renameTreeItem(hSceneTemplate, name="Scene")
assert signal.args[0] == hSceneTemplate
assert signal.args[1].itemName == "Scene"
assert projBar.mTemplates.actions()[0].text() == "Scene"
# Add a note template
projTree.setSelectedHandle(hTemplatesRoot)
projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True)
nwNoteTemplate = project.tree[hNoteTemplate]
assert nwNoteTemplate is not None
assert nwNoteTemplate.itemName == "New Note"
assert projBar.mTemplates.actions()[1].text() == "New Note"
# Rename the note template
with qtbot.waitSignal(projTree.itemRefreshed, timeout=1000) as signal:
projTree.renameTreeItem(hNoteTemplate, name="Note")
assert signal.args[0] == hNoteTemplate
assert signal.args[1].itemName == "Note"
assert projBar.mTemplates.actions()[1].text() == "Note"
# Add new content to template files
(projPath / "content" / f"{hSceneTemplate}.nwd").write_text("### Scene\n\n@pov: Jane\n\n")
(projPath / "content" / f"{hNoteTemplate}.nwd").write_text("# Jane\n\n@tag: Jane\n\n")
# Add a new scene using the template
projTree.setSelectedHandle(C.hSceneDoc, doScroll=True)
projBar.mTemplates.actions()[0].trigger()
nwNewScene = project.tree[hNewScene]
assert nwNewScene is not None
assert nwNewScene.itemName == "Scene"
assert project.storage.getDocument(hNewScene).readDocument() == "### Scene\n\n@pov: Jane\n\n"
# Add a new note using the template
projTree.setSelectedHandle(C.hCharRoot, doScroll=True)
projBar.mTemplates.actions()[1].trigger()
nwNewCharacter = project.tree[hNewCharacter]
assert nwNewCharacter is not None
assert nwNewCharacter.itemName == "Note"
assert project.storage.getDocument(hNewCharacter).readDocument() == "# Jane\n\n@tag: Jane\n\n"
# Remove the templates
with qtbot.waitSignal(projTree.itemRefreshed, timeout=1000) as signal:
assert projBar.mTemplates.menuAction().isVisible() is True
assert len(projBar.mTemplates.actions()) == 2
projTree.moveItemToTrash(hNoteTemplate)
assert len(projBar.mTemplates.actions()) == 1
projTree.moveItemToTrash(hSceneTemplate)
assert len(projBar.mTemplates.actions()) == 0
assert projBar.mTemplates.menuAction().isVisible() is False
# qtbot.stop()
# END Test testGuiProjTree_Templates