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_novel = typ_book.svg
cls_object = typ_key.svg cls_object = typ_key.svg
cls_plot = typ_puzzle.svg cls_plot = typ_puzzle.svg
cls_template = typ_document-add-col.svg
cls_timeline = typ_calendar.svg cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg cls_trash = typ_trash.svg
cls_world = typ_location.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_novel = typ_book.svg
cls_object = typ_key.svg cls_object = typ_key.svg
cls_plot = typ_puzzle.svg cls_plot = typ_puzzle.svg
cls_template = typ_document-add-col.svg
cls_timeline = typ_calendar.svg cls_timeline = typ_calendar.svg
cls_trash = typ_trash.svg cls_trash = typ_trash.svg
cls_world = typ_location.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.ENTITY: QT_TRANSLATE_NOOP("Constant", "Entities"),
nwItemClass.CUSTOM: QT_TRANSLATE_NOOP("Constant", "Custom"), nwItemClass.CUSTOM: QT_TRANSLATE_NOOP("Constant", "Custom"),
nwItemClass.ARCHIVE: QT_TRANSLATE_NOOP("Constant", "Archive"), nwItemClass.ARCHIVE: QT_TRANSLATE_NOOP("Constant", "Archive"),
nwItemClass.TEMPLATE: QT_TRANSLATE_NOOP("Constant", "Templates"),
nwItemClass.TRASH: QT_TRANSLATE_NOOP("Constant", "Trash"), nwItemClass.TRASH: QT_TRANSLATE_NOOP("Constant", "Trash"),
} }
CLASS_ICON = { CLASS_ICON = {
@@ -199,6 +200,7 @@ class nwLabels:
nwItemClass.ENTITY: "cls_entity", nwItemClass.ENTITY: "cls_entity",
nwItemClass.CUSTOM: "cls_custom", nwItemClass.CUSTOM: "cls_custom",
nwItemClass.ARCHIVE: "cls_archive", nwItemClass.ARCHIVE: "cls_archive",
nwItemClass.TEMPLATE: "cls_template",
nwItemClass.TRASH: "cls_trash", nwItemClass.TRASH: "cls_trash",
} }
LAYOUT_NAME = { 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 # For a tag, only the first value is accepted, the rest are ignored
if tBits[0] == nwKeyWords.TAG_KEY and nBits > 1: if tBits[0] == nwKeyWords.TAG_KEY and nBits > 1:
if tBits[1] in self._tagsIndex: check, _ = self.parseValue(tBits[1])
isGood[1] = self._tagsIndex.tagHandle(tBits[1]) == tHandle if check in self._tagsIndex:
isGood[1] = self._tagsIndex.tagHandle(check) == tHandle
else: else:
isGood[1] = True isGood[1] = True
return isGood return isGood
+21 -3
View File
@@ -334,15 +334,33 @@ class NWItem:
def isNovelLike(self) -> bool: def isNovelLike(self) -> bool:
"""Check if the item is of a novel-like class.""" """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: def documentAllowed(self) -> bool:
"""Check if the item is allowed to be of document layout.""" """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: def isInactiveClass(self) -> bool:
"""Check if the item is in an inactive class.""" """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: def isRootType(self) -> bool:
"""Check if item is a root item.""" """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. will not run if the file exists and is not empty.
""" """
tItem = self._tree[tHandle] tItem = self._tree[tHandle]
if tItem is None: if not (tItem and tItem.isFileType()):
return False
if not tItem.isFileType():
return False return False
newDoc = self._storage.getDocument(tHandle) newDoc = self._storage.getDocument(tHandle)
if (newDoc.readDocument() or "").strip(): if (newDoc.readDocument() or "").strip():
return False return False
hshText = "#"*minmax(hLevel, 1, 4) indent = "#"*minmax(hLevel, 1, 4)
newText = f"{hshText} {tItem.itemName}\n\n{text}" text = f"{indent} {tItem.itemName}\n\n{text}"
if tItem.isNovelLike() and isDocument: if tItem.isNovelLike() and isDocument:
tItem.setLayout(nwItemLayout.DOCUMENT) tItem.setLayout(nwItemLayout.DOCUMENT)
else: else:
tItem.setLayout(nwItemLayout.NOTE) tItem.setLayout(nwItemLayout.NOTE)
newDoc.writeDocument(newText) newDoc.writeDocument(text)
self._index.scanText(tHandle, newText) 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 return True
+2 -1
View File
@@ -106,7 +106,8 @@ class ProjectXMLReader:
as attributes. The id attribute was also added to the project. as attributes. The id attribute was also added to the project.
Rev 1: Drops the titleFormat node from settings. 2.1 Beta 1. 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: def __init__(self, path: str | Path) -> None:
+1 -1
View File
@@ -48,7 +48,7 @@ class GuiEditLabel(QDialog):
mSp = CONFIG.pxInt(12) mSp = CONFIG.pxInt(12)
# Item Label # Item Label
self.labelValue = QLineEdit() self.labelValue = QLineEdit(self)
self.labelValue.setMinimumWidth(mVd) self.labelValue.setMinimumWidth(mVd)
self.labelValue.setMaxLength(200) self.labelValue.setMaxLength(200)
self.labelValue.setText(text) self.labelValue.setText(text)
+3 -3
View File
@@ -361,7 +361,7 @@ class _StatusPage(NFixedPage):
self.dnButton.clicked.connect(lambda: self._moveItem(1)) self.dnButton.clicked.connect(lambda: self._moveItem(1))
# Edit Form # Edit Form
self.editName = QLineEdit() self.editName = QLineEdit(self)
self.editName.setMaxLength(40) self.editName.setMaxLength(40)
self.editName.setPlaceholderText(self.tr("Select item to edit")) self.editName.setPlaceholderText(self.tr("Select item to edit"))
self.editName.setEnabled(False) self.editName.setEnabled(False)
@@ -621,12 +621,12 @@ class _ReplacePage(NFixedPage):
self.delButton.clicked.connect(self._delEntry) self.delButton.clicked.connect(self._delEntry)
# Edit Form # Edit Form
self.editKey = QLineEdit() self.editKey = QLineEdit(self)
self.editKey.setPlaceholderText(self.tr("Select item to edit")) self.editKey.setPlaceholderText(self.tr("Select item to edit"))
self.editKey.setEnabled(False) self.editKey.setEnabled(False)
self.editKey.setMaxLength(40) self.editKey.setMaxLength(40)
self.editValue = QLineEdit() self.editValue = QLineEdit(self)
self.editValue.setEnabled(False) self.editValue.setEnabled(False)
self.editValue.setMaxLength(80) self.editValue.setMaxLength(80)
+1 -1
View File
@@ -75,7 +75,7 @@ class GuiWordList(QDialog):
self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop) self.listBox.setDragDropMode(QAbstractItemView.NoDragDrop)
self.listBox.setSortingEnabled(True) self.listBox.setSortingEnabled(True)
self.newEntry = QLineEdit() self.newEntry = QLineEdit(self)
self.addButton = QPushButton(SHARED.theme.getIcon("add"), "") self.addButton = QPushButton(SHARED.theme.getIcon("add"), "")
self.addButton.clicked.connect(self._doAdd) self.addButton.clicked.connect(self._doAdd)
+2 -1
View File
@@ -47,7 +47,8 @@ class nwItemClass(Enum):
ENTITY = 7 ENTITY = 7
CUSTOM = 8 CUSTOM = 8
ARCHIVE = 9 ARCHIVE = 9
TRASH = 10 TEMPLATE = 10
TRASH = 11
# END Enum nwItemClass # END Enum nwItemClass
+8 -9
View File
@@ -60,8 +60,8 @@ class GuiDocHighlighter(QSyntaxHighlighter):
logger.debug("Create: GuiDocHighlighter") logger.debug("Create: GuiDocHighlighter")
self._tItem = None
self._tHandle = None self._tHandle = None
self._isInactive = False
self._spellCheck = False self._spellCheck = False
self._spellErr = QTextCharFormat() self._spellErr = QTextCharFormat()
@@ -238,11 +238,10 @@ class GuiDocHighlighter(QSyntaxHighlighter):
def setHandle(self, tHandle: str) -> None: def setHandle(self, tHandle: str) -> None:
"""Set the handle of the currently highlighted document.""" """Set the handle of the currently highlighted document."""
self._tHandle = tHandle self._tHandle = tHandle
self._tItem = SHARED.project.tree[tHandle] self._isInactive = (
logger.debug( item.isInactiveClass() if (item := SHARED.project.tree[tHandle]) else False
"Syntax highlighter %s for item '%s'",
"enabled" if self._tItem else "disabled", tHandle
) )
logger.debug("Syntax highlighter enabled for item '%s'", tHandle)
return return
## ##
@@ -286,16 +285,16 @@ class GuiDocHighlighter(QSyntaxHighlighter):
for n, bit in enumerate(bits): for n, bit in enumerate(bits):
xPos = pos[n] xPos = pos[n]
xLen = len(bit) xLen = len(bit)
if not isGood[n]: if n == 0 and isGood[n]:
self.setFormat(xPos, xLen, self._hStyles["codeinval"])
elif n == 0:
self.setFormat(xPos, xLen, self._hStyles["keyword"]) self.setFormat(xPos, xLen, self._hStyles["keyword"])
else: elif isGood[n] and not self._isInactive:
one, two = index.parseValue(bit) one, two = index.parseValue(bit)
self.setFormat(xPos, len(one), self._hStyles["value"]) self.setFormat(xPos, len(one), self._hStyles["value"])
if two: if two:
yPos = xPos + len(bit) - len(two) yPos = xPos + len(bit) - len(two)
self.setFormat(yPos, len(two), self._hStyles["optional"]) 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, # We never want to run the spell checker on keyword/values,
# so we force a return here # so we force a return here
+146 -43
View File
@@ -32,12 +32,14 @@ from enum import Enum
from time import time from time import time
from typing import TYPE_CHECKING 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.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QAbstractItemView, QDialog, QFrame, QHBoxLayout, QHeaderView, QLabel, QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView,
QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget, QTreeWidgetItem, QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget,
QVBoxLayout, QWidget QTreeWidgetItem, QVBoxLayout, QWidget
) )
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
@@ -131,7 +133,9 @@ class GuiProjectView(QWidget):
self.keyContext.activated.connect(lambda: self.projTree.openContextOnSelected()) self.keyContext.activated.connect(lambda: self.projTree.openContextOnSelected())
# Signals # 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 # Function Mappings
self.emptyTrash = self.projTree.emptyTrash self.emptyTrash = self.projTree.emptyTrash
@@ -157,7 +161,7 @@ class GuiProjectView(QWidget):
self.projTree.initSettings() self.projTree.initSettings()
return return
def clearProjectView(self) -> None: def closeProjectTasks(self) -> None:
"""Clear project-related GUI content.""" """Clear project-related GUI content."""
self.projBar.clearContent() self.projBar.clearContent()
self.projBar.setEnabled(False) self.projBar.setEnabled(False)
@@ -166,7 +170,7 @@ class GuiProjectView(QWidget):
def openProjectTasks(self) -> None: def openProjectTasks(self) -> None:
"""Run open project tasks.""" """Run open project tasks."""
self.projBar.buildQuickLinkMenu() self.projBar.buildQuickLinksMenu()
self.projBar.setEnabled(True) self.projBar.setEnabled(True)
return return
@@ -212,10 +216,17 @@ class GuiProjectView(QWidget):
@pyqtSlot(str) @pyqtSlot(str)
def updateItemValues(self, tHandle: str) -> None: def updateItemValues(self, tHandle: str) -> None:
"""Update tree item""" """Update tree item."""
self.projTree.setTreeItemValues(tHandle) self.projTree.setTreeItemValues(tHandle)
return 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) @pyqtSlot(str, int, int, int)
def updateCounts(self, tHandle: str, cCount: int, wCount: int, pCount: int) -> None: def updateCounts(self, tHandle: str, cCount: int, wCount: int, pCount: int) -> None:
"""Slot for updating the word count of a specific item.""" """Slot for updating the word count of a specific item."""
@@ -225,8 +236,8 @@ class GuiProjectView(QWidget):
@pyqtSlot(str) @pyqtSlot(str)
def updateRootItem(self, tHandle: str) -> None: def updateRootItem(self, tHandle: str) -> None:
"""If any root item changes, rebuild the quick link menu.""" """Process root item changes."""
self.projBar.buildQuickLinkMenu() self.projBar.buildQuickLinksMenu()
return return
# END Class GuiProjectView # END Class GuiProjectView
@@ -234,6 +245,8 @@ class GuiProjectView(QWidget):
class GuiProjectToolBar(QWidget): class GuiProjectToolBar(QWidget):
newDocumentFromTemplate = pyqtSignal(str)
def __init__(self, projView: GuiProjectView) -> None: def __init__(self, projView: GuiProjectView) -> None:
super().__init__(parent=projView) super().__init__(parent=projView)
@@ -303,6 +316,11 @@ class GuiProjectToolBar(QWidget):
lambda: self.projTree.newTreeItem(nwItemType.FOLDER) 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.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"]))
self._buildRootMenu() self._buildRootMenu()
@@ -383,7 +401,7 @@ class GuiProjectToolBar(QWidget):
self.tbAdd.setIcon(SHARED.theme.getIcon("add")) self.tbAdd.setIcon(SHARED.theme.getIcon("add"))
self.tbMore.setIcon(SHARED.theme.getIcon("menu")) self.tbMore.setIcon(SHARED.theme.getIcon("menu"))
self.buildQuickLinkMenu() self.buildQuickLinksMenu()
self._buildRootMenu() self._buildRootMenu()
return return
@@ -391,21 +409,48 @@ class GuiProjectToolBar(QWidget):
def clearContent(self) -> None: def clearContent(self) -> None:
"""Clear dynamic content on the tool bar.""" """Clear dynamic content on the tool bar."""
self.mQuick.clear() self.mQuick.clear()
self.mTemplates.clearMenu()
return return
def buildQuickLinkMenu(self) -> None: def buildQuickLinksMenu(self) -> None:
"""Build the quick link menu.""" """Build the quick link menu."""
logger.debug("Rebuilding quick links menu") logger.debug("Rebuilding quick links menu")
self.mQuick.clear() self.mQuick.clear()
for n, (tHandle, nwItem) in enumerate(SHARED.project.tree.iterRoots(None)): for tHandle, nwItem in SHARED.project.tree.iterRoots(None):
aRoot = self.mQuick.addAction(nwItem.itemName) action = self.mQuick.addAction(nwItem.itemName)
aRoot.setData(tHandle) action.setData(tHandle)
aRoot.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass])) action.setIcon(SHARED.theme.getIcon(nwLabels.CLASS_ICON[nwItem.itemClass]))
aRoot.triggered.connect( action.triggered.connect(
lambda n, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True) lambda _, tHandle=tHandle: self.projView.setSelectedHandle(tHandle, doScroll=True)
) )
return 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 # Internal Functions
## ##
@@ -421,7 +466,6 @@ class GuiProjectToolBar(QWidget):
self.mAddRoot.clear() self.mAddRoot.clear()
addClass(nwItemClass.NOVEL) addClass(nwItemClass.NOVEL)
addClass(nwItemClass.ARCHIVE)
self.mAddRoot.addSeparator() self.mAddRoot.addSeparator()
addClass(nwItemClass.PLOT) addClass(nwItemClass.PLOT)
addClass(nwItemClass.CHARACTER) addClass(nwItemClass.CHARACTER)
@@ -430,26 +474,12 @@ class GuiProjectToolBar(QWidget):
addClass(nwItemClass.OBJECT) addClass(nwItemClass.OBJECT)
addClass(nwItemClass.ENTITY) addClass(nwItemClass.ENTITY)
addClass(nwItemClass.CUSTOM) addClass(nwItemClass.CUSTOM)
self.mAddRoot.addSeparator()
addClass(nwItemClass.ARCHIVE)
addClass(nwItemClass.TEMPLATE)
return 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 # END Class GuiProjectToolBar
@@ -464,6 +494,8 @@ class GuiProjectTree(QTreeWidget):
D_HANDLE = Qt.ItemDataRole.UserRole D_HANDLE = Qt.ItemDataRole.UserRole
D_WORDS = Qt.ItemDataRole.UserRole + 1 D_WORDS = Qt.ItemDataRole.UserRole + 1
itemRefreshed = pyqtSignal(str, NWItem, QIcon)
def __init__(self, projView: GuiProjectView) -> None: def __init__(self, projView: GuiProjectView) -> None:
super().__init__(parent=projView) super().__init__(parent=projView)
@@ -587,7 +619,7 @@ class GuiProjectTree(QTreeWidget):
return False return False
def newTreeItem(self, itemType: nwItemType, itemClass: nwItemClass | None = None, 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 """Add new item to the tree, with a given itemType (and
itemClass if Root), and attach it to the selected handle. Also 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 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 # Set default label and determine if new item is to be added
# as child or sibling to the selected item # as child or sibling to the selected item
if itemType == nwItemType.FILE: 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") newLabel = self.tr("New Note")
asChild = sIsParent asChild = sIsParent
elif hLevel == 2: elif hLevel == 2:
@@ -675,7 +710,9 @@ class GuiProjectTree(QTreeWidget):
return True return True
# Handle new file creation # 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) SHARED.project.writeNewFile(tHandle, hLevel, not isNote)
# Add the new item to the project tree # Add the new item to the project tree
@@ -770,8 +807,7 @@ class GuiProjectTree(QTreeWidget):
def renameTreeItem(self, tHandle: str, name: str = "") -> None: def renameTreeItem(self, tHandle: str, name: str = "") -> None:
"""Open a dialog to edit the label of an item.""" """Open a dialog to edit the label of an item."""
tItem = SHARED.project.tree[tHandle] if tItem := SHARED.project.tree[tHandle]:
if tItem:
newLabel, dlgOk = GuiEditLabel.getLabel(self, text=name or tItem.itemName) newLabel, dlgOk = GuiEditLabel.getLabel(self, text=name or tItem.itemName)
if dlgOk: if dlgOk:
tItem.setName(newLabel) tItem.setName(newLabel)
@@ -1029,6 +1065,9 @@ class GuiProjectTree(QTreeWidget):
trFont.setUnderline(hLevel == "H1") trFont.setUnderline(hLevel == "H1")
trItem.setFont(self.C_NAME, trFont) trItem.setFont(self.C_NAME, trFont)
# Emit Refresh Signal
self.itemRefreshed.emit(tHandle, nwItem, itemIcon)
return return
def propagateCount(self, tHandle: str, newCount: int, countChildren: bool = False) -> None: def propagateCount(self, tHandle: str, newCount: int, countChildren: bool = False) -> None:
@@ -1358,8 +1397,7 @@ class GuiProjectTree(QTreeWidget):
not including) a given item. not including) a given item.
""" """
if isinstance(trItem, QTreeWidgetItem): if isinstance(trItem, QTreeWidgetItem):
chCount = trItem.childCount() for i in range(trItem.childCount()):
for i in range(chCount):
chItem = trItem.child(i) chItem = trItem.child(i)
chItem.setExpanded(isExpanded) chItem.setExpanded(isExpanded)
self._recursiveSetExpanded(chItem, isExpanded) self._recursiveSetExpanded(chItem, isExpanded)
@@ -1633,6 +1671,71 @@ class GuiProjectTree(QTreeWidget):
# END Class GuiProjectTree # 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): class _TreeContextMenu(QMenu):
def __init__(self, projTree: GuiProjectTree, nwItem: NWItem) -> None: def __init__(self, projTree: GuiProjectTree, nwItem: NWItem) -> None:
+1 -1
View File
@@ -441,7 +441,7 @@ class GuiIcons:
# Class Icons # Class Icons
"cls_archive", "cls_character", "cls_custom", "cls_entity", "cls_none", "cls_novel", "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 Icons
"search_cancel", "search_case", "search_loop", "search_preserve", "search_project", "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.docViewerPanel.closeProjectTasks()
self.outlineView.closeProjectTasks() self.outlineView.closeProjectTasks()
self.novelView.closeProjectTasks() self.novelView.closeProjectTasks()
self.projView.clearProjectView() self.projView.closeProjectTasks()
self.itemDetails.clearDetails() self.itemDetails.clearDetails()
self.mainStatus.clearStatus() self.mainStatus.clearStatus()
+3 -3
View File
@@ -99,7 +99,7 @@ class GuiWelcome(QDialog):
self.tabNew.cancelNewProject.connect(self._showOpenProjectPage) self.tabNew.cancelNewProject.connect(self._showOpenProjectPage)
self.tabNew.openProjectRequest.connect(self._openProjectPath) self.tabNew.openProjectRequest.connect(self._openProjectPath)
self.mainStack = QStackedWidget() self.mainStack = QStackedWidget(self)
self.mainStack.addWidget(self.tabOpen) self.mainStack.addWidget(self.tabOpen)
self.mainStack.addWidget(self.tabNew) self.mainStack.addWidget(self.tabNew)
@@ -524,13 +524,13 @@ class _NewProjectForm(QWidget):
# ================ # ================
# Project Name # Project Name
self.projName = QLineEdit() self.projName = QLineEdit(self)
self.projName.setMaxLength(200) self.projName.setMaxLength(200)
self.projName.setPlaceholderText(self.tr("Required")) self.projName.setPlaceholderText(self.tr("Required"))
self.projName.textChanged.connect(self._updateProjPath) self.projName.textChanged.connect(self._updateProjPath)
# Author(s) # Author(s)
self.projAuthor = QLineEdit() self.projAuthor = QLineEdit(self)
self.projAuthor.setMaxLength(200) self.projAuthor.setMaxLength(200)
self.projAuthor.setPlaceholderText(self.tr("Optional")) 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'?> <?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"> <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="1660" autoCount="256" editTime="81969"> <project id="e2be99af-f9bf-4403-857a-c3d1ac25abea" saveCount="1680" autoCount="263" editTime="82745">
<name>Sample Project</name> <name>Sample Project</name>
<author>Jane Smith</author> <author>Jane Smith</author>
</project> </project>
@@ -20,7 +20,7 @@
<entry key="C">D</entry> <entry key="C">D</entry>
</autoReplace> </autoReplace>
<status> <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="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="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> <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> <entry key="i56be10" count="1" red="117" green="0" blue="175">Main</entry>
</importance> </importance>
</settings> </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"> <item handle="7031beac91f75" parent="None" root="7031beac91f75" order="0" type="ROOT" class="NOVEL">
<meta expanded="yes" /> <meta expanded="yes" />
<name status="sc24b8f" import="ia857f0">Novel</name> <name status="sc24b8f" import="ia857f0">Novel</name>
@@ -136,7 +136,23 @@
<meta expanded="no" heading="H3" charCount="232" wordCount="42" paraCount="1" cursorPos="239" /> <meta expanded="no" heading="H3" charCount="232" wordCount="42" paraCount="1" cursorPos="239" />
<name status="s90e6c9" import="ia857f0" active="yes">Old File</name> <name status="s90e6c9" import="ia857f0" active="yes">Old File</name>
</item> </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" /> <meta expanded="yes" />
<name status="sf12341" import="ia857f0">Trash</name> <name status="sf12341" import="ia857f0">Trash</name>
</item> </item>
@@ -1,5 +1,5 @@
<?xml version='1.0' encoding='utf-8'?> <?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"> <project id="d0f3fe10-c6e6-4310-8bfd-181eb4224eed" saveCount="1" autoCount="1" editTime="0">
<name>New Project</name> <name>New Project</name>
<author>Jane Doe</author> <author>Jane Doe</author>
@@ -22,13 +22,13 @@
<entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry> <entry key="s000003" count="0" red="50" green="200" blue="0">Finished</entry>
</status> </status>
<importance> <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="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="i000006" count="0" red="200" green="150" blue="0">Major</entry>
<entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry> <entry key="i000007" count="0" red="50" green="200" blue="0">Main</entry>
</importance> </importance>
</settings> </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"> <item handle="0000000000008" parent="None" root="0000000000008" order="0" type="ROOT" class="NOVEL">
<meta expanded="no" /> <meta expanded="no" />
<name status="s000000" import="i000004">Novel</name> <name status="s000000" import="i000004">Novel</name>
@@ -73,5 +73,9 @@
<meta expanded="no" heading="H1" charCount="11" wordCount="3" paraCount="1" cursorPos="0" /> <meta expanded="no" heading="H1" charCount="11" wordCount="3" paraCount="1" cursorPos="0" />
<name status="s000000" import="i000004" active="yes">Jane</name> <name status="s000000" import="i000004" active="yes">Jane</name>
</item> </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> </content>
</novelWriterXML> </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")
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() project.closeProject()
# END Test testCoreIndex_CheckThese # END Test testCoreIndex_CheckThese
+19
View File
@@ -365,6 +365,7 @@ def testCoreItem_ClassSetter(mockGUI):
""" """
project = NWProject() project = NWProject()
item = NWItem(project, "0000000000000") item = NWItem(project, "0000000000000")
item.setType(nwItemType.FILE)
# Class # Class
item.setClass(None) item.setClass(None)
@@ -377,66 +378,84 @@ def testCoreItem_ClassSetter(mockGUI):
assert item.isNovelLike() is False assert item.isNovelLike() is False
assert item.documentAllowed() is False assert item.documentAllowed() is False
assert item.isInactiveClass() is True assert item.isInactiveClass() is True
assert item.isTemplateFile() is False
item.setClass("NOVEL") item.setClass("NOVEL")
assert item.itemClass == nwItemClass.NOVEL assert item.itemClass == nwItemClass.NOVEL
assert item.isNovelLike() is True assert item.isNovelLike() is True
assert item.documentAllowed() is True assert item.documentAllowed() is True
assert item.isInactiveClass() is False assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("PLOT") item.setClass("PLOT")
assert item.itemClass == nwItemClass.PLOT assert item.itemClass == nwItemClass.PLOT
assert item.isNovelLike() is False assert item.isNovelLike() is False
assert item.documentAllowed() is False assert item.documentAllowed() is False
assert item.isInactiveClass() is False assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("CHARACTER") item.setClass("CHARACTER")
assert item.itemClass == nwItemClass.CHARACTER assert item.itemClass == nwItemClass.CHARACTER
assert item.isNovelLike() is False assert item.isNovelLike() is False
assert item.documentAllowed() is False assert item.documentAllowed() is False
assert item.isInactiveClass() is False assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("WORLD") item.setClass("WORLD")
assert item.itemClass == nwItemClass.WORLD assert item.itemClass == nwItemClass.WORLD
assert item.isNovelLike() is False assert item.isNovelLike() is False
assert item.documentAllowed() is False assert item.documentAllowed() is False
assert item.isInactiveClass() is False assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("TIMELINE") item.setClass("TIMELINE")
assert item.itemClass == nwItemClass.TIMELINE assert item.itemClass == nwItemClass.TIMELINE
assert item.isNovelLike() is False assert item.isNovelLike() is False
assert item.documentAllowed() is False assert item.documentAllowed() is False
assert item.isInactiveClass() is False assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("OBJECT") item.setClass("OBJECT")
assert item.itemClass == nwItemClass.OBJECT assert item.itemClass == nwItemClass.OBJECT
assert item.isNovelLike() is False assert item.isNovelLike() is False
assert item.documentAllowed() is False assert item.documentAllowed() is False
assert item.isInactiveClass() is False assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("ENTITY") item.setClass("ENTITY")
assert item.itemClass == nwItemClass.ENTITY assert item.itemClass == nwItemClass.ENTITY
assert item.isNovelLike() is False assert item.isNovelLike() is False
assert item.documentAllowed() is False assert item.documentAllowed() is False
assert item.isInactiveClass() is False assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("CUSTOM") item.setClass("CUSTOM")
assert item.itemClass == nwItemClass.CUSTOM assert item.itemClass == nwItemClass.CUSTOM
assert item.isNovelLike() is False assert item.isNovelLike() is False
assert item.documentAllowed() is False assert item.documentAllowed() is False
assert item.isInactiveClass() is False assert item.isInactiveClass() is False
assert item.isTemplateFile() is False
item.setClass("ARCHIVE") item.setClass("ARCHIVE")
assert item.itemClass == nwItemClass.ARCHIVE assert item.itemClass == nwItemClass.ARCHIVE
assert item.isNovelLike() is True assert item.isNovelLike() is True
assert item.documentAllowed() is True assert item.documentAllowed() is True
assert item.isInactiveClass() 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") item.setClass("TRASH")
assert item.itemClass == nwItemClass.TRASH assert item.itemClass == nwItemClass.TRASH
assert item.isNovelLike() is False assert item.isNovelLike() is False
assert item.documentAllowed() is True assert item.documentAllowed() is True
assert item.isInactiveClass() is True assert item.isInactiveClass() is True
assert item.isTemplateFile() is False
# Alternative # Alternative
item.setClass(nwItemClass.NOVEL) item.setClass(nwItemClass.NOVEL)
+62 -25
View File
@@ -102,34 +102,61 @@ def testCoreProject_NewFileFolder(monkeypatch, fncPath, tstPaths, mockGUI, mockR
mockRnd.reset() mockRnd.reset()
buildTestProject(project, fncPath) buildTestProject(project, fncPath)
aHandle = "0000000000010"
bHandle = "0000000000011"
cHandle = "0000000000012"
dHandle = "0000000000013"
xHandle = "1234567890abc"
# Invalid call # Invalid call
assert project.newFolder("New Folder", "1234567890abc") is None assert project.newFolder("New Folder", xHandle) is None
assert project.newFile("New File", "1234567890abc") is None assert project.newFile("New File", xHandle) is None
# Add files properly # Add files properly
assert project.newFolder("Stuff", C.hNovelRoot) == "0000000000010" assert project.newFolder("Stuff", C.hNovelRoot) == aHandle
assert project.newFile("Hello", "0000000000010") == "0000000000011" assert project.newFile("Hello", aHandle) == bHandle
assert project.newFile("Jane", C.hCharRoot) == "0000000000012" assert project.newFile("Jane", C.hCharRoot) == cHandle
assert project.newFile("John", C.hCharRoot) == dHandle
assert "0000000000010" in project.tree assert aHandle in project.tree
assert "0000000000011" in project.tree assert bHandle in project.tree
assert "0000000000012" in project.tree assert cHandle in project.tree
assert dHandle in project.tree
# Write to file, failed # Write to file, failed
assert project.writeNewFile("blabla", 1, True) is False # Not a handle 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 assert project.writeNewFile(C.hTitlePage, 1, True) is False # Already has content
# Write to file, success # Write to file, success
assert project.writeNewFile("0000000000011", 2, True) is True assert project.writeNewFile(bHandle, 2, True) is True
assert project.storage.getDocument("0000000000011").readDocument() == "## Hello\n\n" assert project.storage.getDocument(bHandle).readDocument() == "## Hello\n\n"
# Write to file with additional text, success # Write to file with additional text, success
assert project.writeNewFile("0000000000012", 1, False, "Hi Jane\n\n") is True assert project.writeNewFile(cHandle, 1, False, "Hi Jane\n\n") is True
assert project.storage.getDocument("0000000000012").readDocument() == ( assert project.storage.getDocument(cHandle).readDocument() == (
"# Jane\n\nHi Jane\n\n" "# 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 # Save, close and check
assert project.projChanged is True assert project.projChanged is True
assert project.saveProject() 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 # Delete new file, but block access
with monkeypatch.context() as mp: with monkeypatch.context() as mp:
mp.setattr("pathlib.Path.unlink", causeOSError) mp.setattr("pathlib.Path.unlink", causeOSError)
assert project.removeItem("0000000000011") is False assert project.removeItem(bHandle) is False
assert "0000000000011" in project.tree assert bHandle in project.tree
# Delete new files and folders # Delete new files and folders
assert (fncPath / "content" / "0000000000012.nwd").exists() assert (fncPath / "content" / f"{dHandle}.nwd").exists()
assert (fncPath / "content" / "0000000000011.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(dHandle) is True
assert project.removeItem("0000000000011") is True assert project.removeItem(cHandle) is True
assert project.removeItem("0000000000010") 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" / f"{dHandle}.nwd").exists()
assert not (fncPath / "content" / "0000000000011.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 aHandle not in project.tree
assert "0000000000011" not in project.tree assert bHandle not in project.tree
assert "0000000000012" not in project.tree assert cHandle not in project.tree
assert dHandle not in project.tree
project.closeProject() project.closeProject()
@@ -177,6 +208,8 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
caplog.clear() caplog.clear()
assert project.openProject(fooBar) is False assert project.openProject(fooBar) is False
assert "Not a known project file format." in caplog.text assert "Not a known project file format." in caplog.text
assert project.isValid is False
assert project.lockStatus is None
# No project file # No project file
caplog.clear() caplog.clear()
@@ -204,10 +237,14 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd):
# Open again should fail on lock file # Open again should fail on lock file
assert project.openProject(fncPath) is False assert project.openProject(fncPath) is False
assert project.state == NWProjectState.LOCKED assert project.state == NWProjectState.LOCKED
assert project.isValid is True
assert project.lockStatus is not None
# Force re-open # Force re-open
assert project.openProject(fncPath, clearLock=True) is True assert project.openProject(fncPath, clearLock=True) is True
assert project.state == NWProjectState.READY assert project.state == NWProjectState.READY
assert project.isValid is True
assert project.lockStatus is None
# Fail getting xml reader # Fail getting xml reader
with monkeypatch.context() as mp: 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 nwGUI.docEditor.getText() == "### New Scene\n\n"
assert projTree._getItemWordCount("0000000000014") == 2 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 # Add a new file to the characters folder
projView.setSelectedHandle(C.hCharRoot) projView.setSelectedHandle(C.hCharRoot)
assert projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True assert projTree.newTreeItem(nwItemType.FILE, hLevel=1, isNote=True) is True
assert project.tree["0000000000015"].itemParent == C.hCharRoot # type: ignore assert project.tree["0000000000016"].itemParent == C.hCharRoot # type: ignore
assert project.tree["0000000000015"].itemRoot == C.hCharRoot # type: ignore assert project.tree["0000000000016"].itemRoot == C.hCharRoot # type: ignore
assert project.tree["0000000000015"].itemClass == nwItemClass.CHARACTER # type: ignore assert project.tree["0000000000016"].itemClass == nwItemClass.CHARACTER # type: ignore
assert nwGUI.openDocument("0000000000015") assert nwGUI.openDocument("0000000000016")
assert nwGUI.docEditor.getText() == "# New Note\n\n" 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 # Make sure the sibling folder bug trap works
projView.setSelectedHandle("0000000000013") projView.setSelectedHandle("0000000000013")
@@ -1112,7 +1125,7 @@ def testGuiProjTree_Other(qtbot, monkeypatch, nwGUI: GuiMain, projPath, mockRnd)
@pytest.mark.gui @pytest.mark.gui
def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
"""Test the building of the project tree context menu. All this does """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)) monkeypatch.setattr(GuiEditLabel, "getLabel", lambda *a, text: (text, True))
@@ -1392,3 +1405,98 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd):
# qtbot.stop() # qtbot.stop()
# END Test testGuiProjTree_ContextMenu # 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