Add file template feature (#1688)
This commit is contained in:
@@ -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 |
@@ -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 = {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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."""
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
|
||||
@@ -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
@@ -47,7 +47,8 @@ class nwItemClass(Enum):
|
||||
ENTITY = 7
|
||||
CUSTOM = 8
|
||||
ARCHIVE = 9
|
||||
TRASH = 10
|
||||
TEMPLATE = 10
|
||||
TRASH = 11
|
||||
|
||||
# END Enum nwItemClass
|
||||
|
||||
|
||||
@@ -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
@@ -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:
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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()
|
||||
|
||||
|
||||
@@ -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"))
|
||||
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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:
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user