From ecebd69cda6f58b8d743658c04f81d7056168da8 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 1 Sep 2023 16:55:23 +0200
Subject: [PATCH 1/6] Add build details widget to manuscript tool
---
novelwriter/core/buildsettings.py | 55 ++++++-
novelwriter/core/options.py | 7 +-
novelwriter/core/tokenizer.py | 8 +-
novelwriter/tools/manuscript.py | 230 +++++++++++++++++++++++++++---
4 files changed, 267 insertions(+), 33 deletions(-)
diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py
index 64196ed1..f506cef5 100644
--- a/novelwriter/core/buildsettings.py
+++ b/novelwriter/core/buildsettings.py
@@ -319,7 +319,6 @@ class BuildSettings:
value = min(max(value, definition[2]), definition[3])
self._changed = value != self._settings[key]
self._settings[key] = value
- logger.debug(f"Build Setting '{key}' set to: {value}")
return True
##
@@ -457,6 +456,8 @@ class BuildCollection:
def __init__(self, project: NWProject) -> None:
self._project = project
self._builds = {}
+ self._lastBuild = ""
+ self._defaultBuild = ""
self._loadCollection()
return
@@ -465,7 +466,21 @@ class BuildCollection:
return len(self._builds)
##
- # Methods
+ # Properties
+ ##
+
+ @property
+ def lastBuild(self) -> str:
+ """Return the last active build."""
+ return self._lastBuild
+
+ @property
+ def defaultBuild(self) -> str:
+ """Return the default build."""
+ return self._defaultBuild
+
+ ##
+ # Getters
##
def getBuild(self, buildID: str) -> BuildSettings | None:
@@ -476,6 +491,24 @@ class BuildCollection:
build.unpack(self._builds[buildID])
return build
+ ##
+ # Setters
+ ##
+
+ def setLastBuild(self, buildID: str) -> None:
+ """Set the last active build id."""
+ if buildID != self._lastBuild:
+ self._lastBuild = buildID
+ self._saveCollection()
+ return
+
+ def setDefaultBuild(self, buildID: str) -> None:
+ """Set the default build id."""
+ if buildID != self._defaultBuild:
+ self._defaultBuild = buildID
+ self._saveCollection()
+ return
+
def setBuild(self, build: BuildSettings) -> None:
"""Set build settings data in the collection."""
if isinstance(build, BuildSettings):
@@ -484,6 +517,10 @@ class BuildCollection:
self._saveCollection()
return
+ ##
+ # Methods
+ ##
+
def removeBuild(self, buildID: str) -> None:
"""Remove the a build from the collection."""
self._builds.pop(buildID, None)
@@ -527,7 +564,11 @@ class BuildCollection:
return False
for key, entry in builds.items():
- if isinstance(entry, dict):
+ if key == "lastBuild":
+ self._lastBuild = str(entry)
+ elif key == "defaultBuild":
+ self._defaultBuild = str(entry)
+ elif isinstance(entry, dict):
self._builds[key] = entry
return True
@@ -540,9 +581,13 @@ class BuildCollection:
logger.debug("Saving builds file")
try:
- data = {"novelWriter.builds": self._builds}
+ data = {
+ "lastBuild": self._lastBuild,
+ "defaultBuild": self._defaultBuild,
+ }
+ data.update(self._builds)
with open(buildsFile, mode="w+", encoding="utf-8") as outFile:
- outFile.write(jsonEncode(data, nmax=4))
+ outFile.write(jsonEncode({"novelWriter.builds": data}, nmax=4))
except Exception:
logger.error("Failed to save builds file")
logException()
diff --git a/novelwriter/core/options.py b/novelwriter/core/options.py
index 0cc7ac0a..0dd62f06 100644
--- a/novelwriter/core/options.py
+++ b/novelwriter/core/options.py
@@ -58,13 +58,14 @@ VALID_MAP = {
"GuiWordList": {"winWidth", "winHeight"},
"GuiNovelView": {"lastCol", "lastColSize"},
"GuiBuildSettings": {
- "winWidth", "winHeight", "treeWidth", "filterWidth"
+ "winWidth", "winHeight", "treeWidth", "filterWidth",
},
"GuiManuscript": {
- "winWidth", "winHeight", "optsWidth", "viewWidth"
+ "winWidth", "winHeight", "optsWidth", "viewWidth", "listHeight",
+ "detailsHeight", "detailsWidth", "detailsExpanded",
},
"GuiManuscriptBuild": {
- "winWidth", "winHeight", "fmtWidth", "sumWidth"
+ "winWidth", "winHeight", "fmtWidth", "sumWidth",
},
}
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index 6fc281b3..839f9f69 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -36,6 +36,7 @@ from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression
+from novelwriter import SHARED
from novelwriter.enum import nwItemLayout
from novelwriter.common import formatTimeStamp, numberToRoman, checkInt
from novelwriter.constants import nwConst, nwHeadFmt, nwRegEx, nwUnicode
@@ -147,7 +148,7 @@ class Tokenizer(ABC):
self._linkHeaders = False # Add an anchor before headers
# Instance Variables
- self._hFormatter = HeadingFormatter(self._project)
+ self._hFormatter = HeadingFormatter()
self._firstScene = False # Flag to indicate that the first scene of the chapter
# This File
@@ -771,8 +772,7 @@ class Tokenizer(ABC):
class HeadingFormatter:
- def __init__(self, project: NWProject) -> None:
- self._project = project
+ def __init__(self) -> None:
self._chCount = 0
self._scChCount = 0
self._scAbsCount = 0
@@ -801,7 +801,7 @@ class HeadingFormatter:
hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scChCount))
hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._scAbsCount))
if nwHeadFmt.CH_WORD in hFormat:
- chWord = self._project.localLookup(self._chCount)
+ chWord = SHARED.project.localLookup(self._chCount)
hFormat = hFormat.replace(nwHeadFmt.CH_WORD, chWord)
if nwHeadFmt.CH_ROML in hFormat:
chRom = numberToRoman(self._chCount, toLower=True)
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 1a1e4d1d..56919b2b 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -33,12 +33,14 @@ from datetime import datetime
from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent
from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
from PyQt5.QtWidgets import (
- QDialog, QGridLayout, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton,
- QSplitter, QTextBrowser, QToolButton, QVBoxLayout, QWidget, qApp
+ QDialog, QGridLayout, QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
+ QPushButton, QSplitter, QTextBrowser, QToolButton, QTreeWidget,
+ QTreeWidgetItem, QVBoxLayout, QWidget, qApp
)
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
from novelwriter import CONFIG, SHARED
+from novelwriter.core.tokenizer import HeadingFormatter
from novelwriter.error import logException
from novelwriter.common import checkInt, fuzzyTime
from novelwriter.core.tohtml import ToHtml
@@ -127,10 +129,6 @@ class GuiManuscript(QDialog):
self.lblBuilds = QLabel("{0}".format(self.tr("Builds")))
- self.buildList = QListWidget()
- self.buildList.setIconSize(QSize(iPx, iPx))
- self.buildList.doubleClicked.connect(self._editSelectedBuild)
-
self.listToolBox = QHBoxLayout()
self.listToolBox.addWidget(self.lblBuilds)
self.listToolBox.addStretch(1)
@@ -139,6 +137,27 @@ class GuiManuscript(QDialog):
self.listToolBox.addWidget(self.tbEdit)
self.listToolBox.setSpacing(0)
+ # Builds
+ # ======
+
+ self.buildList = QListWidget()
+ self.buildList.setIconSize(QSize(iPx, iPx))
+ self.buildList.doubleClicked.connect(self._editSelectedBuild)
+ self.buildList.currentItemChanged.connect(self._updateBuildDetails)
+
+ self.buildDetails = _DetailsWidget(self)
+ self.buildDetails.setColumnWidth(
+ CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsWidth", 100)),
+ )
+
+ self.buildSplit = QSplitter(Qt.Orientation.Vertical, self)
+ self.buildSplit.addWidget(self.buildList)
+ self.buildSplit.addWidget(self.buildDetails)
+ self.buildSplit.setSizes([
+ CONFIG.pxInt(pOptions.getInt("GuiManuscript", "listHeight", 50)),
+ CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsHeight", 50)),
+ ])
+
# Process Controls
# ================
@@ -167,7 +186,7 @@ class GuiManuscript(QDialog):
self.controlBox = QVBoxLayout()
self.controlBox.addLayout(self.listToolBox, 0)
- self.controlBox.addWidget(self.buildList, 1)
+ self.controlBox.addWidget(self.buildSplit, 1)
self.controlBox.addLayout(self.processBox, 0)
self.controlBox.setContentsMargins(0, 0, 0, 0)
@@ -206,8 +225,13 @@ class GuiManuscript(QDialog):
build = BuildSettings()
build.setName(self.tr("My Manuscript"))
self._builds.setBuild(build)
+ selected = build.buildID
+ else:
+ selected = self._builds.lastBuild
self._updateBuildsList()
+ if selected in self._buildMap:
+ self.buildList.setCurrentItem(self._buildMap[selected])
logger.debug("Loading build cache")
cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
@@ -263,6 +287,14 @@ class GuiManuscript(QDialog):
self._openSettingsDialog(build)
return
+ @pyqtSlot("QListWidgetItem*", "QListWidgetItem*")
+ def _updateBuildDetails(self, current: QListWidgetItem, previous: QListWidgetItem) -> None:
+ """Process change of build selection to update the details."""
+ build = self._builds.getBuild(current.data(self.D_KEY))
+ if build is not None:
+ self.buildDetails.updateInfo(build)
+ return
+
@pyqtSlot()
def _deleteSelectedBuild(self):
"""Delete the currently selected build settings entry."""
@@ -278,6 +310,9 @@ class GuiManuscript(QDialog):
"""Process new build settings from the settings dialog."""
self._builds.setBuild(build)
self._updateBuildItem(build)
+ current = self.buildList.currentItem()
+ if isinstance(current, QListWidgetItem) and current.data(self.D_KEY) == build.buildID:
+ self._updateBuildDetails(current, current)
return
@pyqtSlot()
@@ -363,8 +398,6 @@ class GuiManuscript(QDialog):
self.docPreview.setJustify(
build.getBool("format.justifyText")
)
- if build.buildID and build.buildID in self._buildMap:
- self._buildMap[build.buildID].setSelected(True)
return
def _getSelectedBuild(self) -> BuildSettings | None:
@@ -383,6 +416,10 @@ class GuiManuscript(QDialog):
"""Save the user GUI settings."""
logger.debug("Saving GuiManuscript settings")
+ current = self.buildList.currentItem()
+ if isinstance(current, QListWidgetItem):
+ self._builds.setLastBuild(current.data(self.D_KEY))
+
winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height())
@@ -390,11 +427,21 @@ class GuiManuscript(QDialog):
optsWidth = CONFIG.rpxInt(mainSplit[0])
viewWidth = CONFIG.rpxInt(mainSplit[1])
+ buildSplit = self.buildSplit.sizes()
+ listHeight = CONFIG.rpxInt(buildSplit[0])
+ detailsHeight = CONFIG.rpxInt(buildSplit[1])
+ detailsWidth = CONFIG.rpxInt(self.buildDetails.getColumnWidth())
+ detailsExpanded = self.buildDetails.getExpandedState()
+
pOptions = SHARED.project.options
pOptions.setValue("GuiManuscript", "winWidth", winWidth)
pOptions.setValue("GuiManuscript", "winHeight", winHeight)
pOptions.setValue("GuiManuscript", "optsWidth", optsWidth)
pOptions.setValue("GuiManuscript", "viewWidth", viewWidth)
+ pOptions.setValue("GuiManuscript", "listHeight", listHeight)
+ pOptions.setValue("GuiManuscript", "detailsHeight", detailsHeight)
+ pOptions.setValue("GuiManuscript", "detailsWidth", detailsWidth)
+ pOptions.setValue("GuiManuscript", "detailsExpanded", detailsExpanded)
pOptions.saveSettings()
return
@@ -444,9 +491,150 @@ class GuiManuscript(QDialog):
# END Class GuiManuscript
+class _DetailsWidget(QWidget):
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+
+ self._initExpanded = True
+
+ # Tree Vidget
+ self.listView = QTreeWidget(self)
+ self.listView.setHeaderLabels(["Setting", "Value"])
+ self.listView.setIndentation(SHARED.theme.baseIconSize)
+
+ # Assemble
+ self.outerBox = QVBoxLayout()
+ self.outerBox.addWidget(self.listView)
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
+ self.setLayout(self.outerBox)
+
+ return
+
+ ##
+ # Getters
+ ##
+
+ def getColumnWidth(self) -> int:
+ """Get the width of the first column."""
+ return self.listView.columnWidth(0)
+
+ def getExpandedState(self) -> list[bool]:
+ """Get the expanded state of each top level item."""
+ state = []
+ for i in range(self.listView.topLevelItemCount()):
+ item = self.listView.topLevelItem(i)
+ if isinstance(item, QTreeWidgetItem):
+ state.append(item.isExpanded())
+ return state
+
+ ##
+ # Setters
+ ##
+
+ def setColumnWidth(self, value: int) -> None:
+ """Set the width of the first column."""
+ self.listView.setColumnWidth(0, value)
+ return
+
+ def setExpandedState(self, state: list[bool]) -> None:
+ """Set the expanded state of each top level item."""
+ count = len(state)
+ for i in range(self.listView.topLevelItemCount()):
+ item = self.listView.topLevelItem(i)
+ if isinstance(item, QTreeWidgetItem) and i < count:
+ item.setExpanded(state[i])
+ return
+
+ ##
+ # Methods
+ ##
+
+ def updateInfo(self, build: BuildSettings) -> None:
+ """Load the build settings info into the table."""
+ if self._initExpanded:
+ previous = SHARED.project.options.getValue("GuiManuscript", "detailsExpanded", [])
+ expanded = [bool(s) for s in previous]
+ self._initExpanded = False
+ else:
+ expanded = self.getExpandedState()
+
+ self.listView.clear()
+
+ on = SHARED.theme.getIcon("bullet-on")
+ off = SHARED.theme.getIcon("bullet-off")
+
+ # Name
+ item = QTreeWidgetItem()
+ item.setText(0, self.tr("Name"))
+ item.setText(1, build.name)
+ self.listView.addTopLevelItem(item)
+
+ # Selection
+ item = QTreeWidgetItem()
+ item.setText(0, self.tr("Selection"))
+ item.setText(1, "")
+ self.listView.addTopLevelItem(item)
+ for tHandle, nwItem in SHARED.project.tree.iterRoots(None):
+ if not nwItem.isInactiveClass():
+ sub = QTreeWidgetItem()
+ sub.setText(0, nwItem.itemName)
+ sub.setIcon(1, on if build.isRootAllowed(tHandle) else off)
+ item.addChild(sub)
+
+ # Headings
+ hFmt = HeadingFormatter()
+ hFmt.incChapter()
+ hFmt.incScene()
+ hFmt.resetScene()
+ hFmt.incScene()
+ title = self.tr("Title")
+
+ item = QTreeWidgetItem()
+ item.setText(0, build.getLabel("headings"))
+ item.setText(1, "")
+ self.listView.addTopLevelItem(item)
+ entries = [
+ "headings.fmtTitle", "headings.fmtChapter", "headings.fmtUnnumbered",
+ "headings.fmtScene", "headings.fmtSection"
+ ]
+ for key in entries:
+ sub = QTreeWidgetItem()
+ sub.setText(0, build.getLabel(key))
+ sub.setText(1, hFmt.apply(build.getStr(key), title))
+ item.addChild(sub)
+ for key in ["headings.hideScene", "headings.hideSection"]:
+ sub = QTreeWidgetItem()
+ sub.setText(0, build.getLabel(key))
+ sub.setIcon(1, on if build.getBool(key) else off)
+ item.addChild(sub)
+
+ # Text Content
+ item = QTreeWidgetItem()
+ item.setText(0, build.getLabel("text.grpContent"))
+ item.setText(1, "")
+ self.listView.addTopLevelItem(item)
+ entries = [
+ "text.includeSynopsis", "text.includeComments",
+ "text.includeKeywords", "text.includeBodyText",
+ ]
+ for key in entries:
+ sub = QTreeWidgetItem()
+ sub.setText(0, build.getLabel(key))
+ sub.setIcon(1, on if build.getBool(key) else off)
+ item.addChild(sub)
+
+ # Restore expanded state
+ self.setExpandedState(expanded)
+
+ return
+
+# END Class _DetailsWidget
+
+
class _PreviewWidget(QTextBrowser):
- def __init__(self, parent: QWidget):
+ def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent)
self._docTime = 0
@@ -512,13 +700,13 @@ class _PreviewWidget(QTextBrowser):
# Setters
##
- def setBuildName(self, name: str):
+ def setBuildName(self, name: str) -> None:
"""Set the build name for the document label."""
self._buildName = name
self._updateBuildAge()
return
- def setJustify(self, state: bool):
+ def setJustify(self, state: bool) -> None:
"""Enable/disable the justify text option."""
pOptions = self.document().defaultTextOption()
if state:
@@ -528,7 +716,7 @@ class _PreviewWidget(QTextBrowser):
self.document().setDefaultTextOption(pOptions)
return
- def setTextFont(self, family: str, size: int):
+ def setTextFont(self, family: str, size: int) -> None:
"""Set the text font properties."""
if family:
font = QFont()
@@ -541,7 +729,7 @@ class _PreviewWidget(QTextBrowser):
# Methods
##
- def beginNewBuild(self, length: int):
+ def beginNewBuild(self, length: int) -> None:
"""Clear the document and show the progress bar."""
self.buildProgress.setMaximum(length)
self.buildProgress.setValue(0)
@@ -551,13 +739,13 @@ class _PreviewWidget(QTextBrowser):
self.clear()
return
- def buildStep(self, value: int):
+ def buildStep(self, value: int) -> None:
"""Update the progress bar value."""
self.buildProgress.setValue(value)
qApp.processEvents()
return
- def setContent(self, data: dict):
+ def setContent(self, data: dict) -> None:
"""Set the content of the preview widget."""
sPos = self.verticalScrollBar().value()
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
@@ -602,7 +790,7 @@ class _PreviewWidget(QTextBrowser):
# Events
##
- def resizeEvent(self, event: QResizeEvent):
+ def resizeEvent(self, event: QResizeEvent) -> None:
"""Capture resize and update the document margins."""
super().resizeEvent(event)
self._updateDocMargins()
@@ -613,7 +801,7 @@ class _PreviewWidget(QTextBrowser):
##
@pyqtSlot("QPrinter*")
- def printPreview(self, printer: QPrinter):
+ def printPreview(self, printer: QPrinter) -> None:
"""Connect the print preview painter to the document viewer."""
qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
printer.setOrientation(QPrinter.Portrait)
@@ -626,7 +814,7 @@ class _PreviewWidget(QTextBrowser):
##
@pyqtSlot()
- def _updateBuildAge(self):
+ def _updateBuildAge(self) -> None:
"""Update the build time and the fuzzy age."""
if self._docTime > 0:
strBuildTime = "%s (%s)" % (
@@ -642,7 +830,7 @@ class _PreviewWidget(QTextBrowser):
return
@pyqtSlot()
- def _hideProgress(self):
+ def _hideProgress(self) -> None:
"""Clean up the build progress bar."""
self.buildProgress.setVisible(False)
return
@@ -651,7 +839,7 @@ class _PreviewWidget(QTextBrowser):
# Internal Functions
##
- def _updateDocMargins(self):
+ def _updateDocMargins(self) -> None:
"""Automatically adjust the header to fill the top of the
document within the viewport.
"""
From 03c4b916821fa534b17fba457e350d36868a3136 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 1 Sep 2023 17:19:25 +0200
Subject: [PATCH 2/6] Make a few improvements to manuscript details panel
---
novelwriter/tools/manuscript.py | 15 +++++++++------
1 file changed, 9 insertions(+), 6 deletions(-)
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 56919b2b..193312f1 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -33,9 +33,9 @@ from datetime import datetime
from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent
from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
from PyQt5.QtWidgets import (
- QDialog, QGridLayout, QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
- QPushButton, QSplitter, QTextBrowser, QToolButton, QTreeWidget,
- QTreeWidgetItem, QVBoxLayout, QWidget, qApp
+ QAbstractItemView, QDialog, QGridLayout, QHBoxLayout, QLabel, QListWidget,
+ QListWidgetItem, QPushButton, QSplitter, QTextBrowser, QToolButton,
+ QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget, qApp
)
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
@@ -500,8 +500,9 @@ class _DetailsWidget(QWidget):
# Tree Vidget
self.listView = QTreeWidget(self)
- self.listView.setHeaderLabels(["Setting", "Value"])
+ self.listView.setHeaderLabels([self.tr("Setting"), self.tr("Value")])
self.listView.setIndentation(SHARED.theme.baseIconSize)
+ self.listView.setSelectionMode(QAbstractItemView.NoSelection)
# Assemble
self.outerBox = QVBoxLayout()
@@ -542,8 +543,10 @@ class _DetailsWidget(QWidget):
count = len(state)
for i in range(self.listView.topLevelItemCount()):
item = self.listView.topLevelItem(i)
- if isinstance(item, QTreeWidgetItem) and i < count:
- item.setExpanded(state[i])
+ if isinstance(item, QTreeWidgetItem):
+ item.setExpanded(
+ (state[i] if i < count else True) if item.childCount() > 0 else False
+ )
return
##
From 9c7663996c288dd297613e2339c8005b499aeaca Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 1 Sep 2023 17:34:34 +0200
Subject: [PATCH 3/6] Fix issues discovered when testing
---
novelwriter/core/project.py | 2 +-
novelwriter/core/tokenizer.py | 8 ++++----
novelwriter/tools/manuscript.py | 9 +++++----
tests/test_core/test_core_buildsettings.py | 16 +++++++++++++---
4 files changed, 23 insertions(+), 12 deletions(-)
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index fc59f77f..4cba0c82 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -600,7 +600,7 @@ class NWProject:
self._langData = json.load(inFile)
logger.debug("Loaded project language file: %s", langFile.name)
except Exception:
- logger.error("Failed to project language file")
+ logger.error("Failed to load project language file")
logException()
return False
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index 839f9f69..6fc281b3 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -36,7 +36,6 @@ from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression
-from novelwriter import SHARED
from novelwriter.enum import nwItemLayout
from novelwriter.common import formatTimeStamp, numberToRoman, checkInt
from novelwriter.constants import nwConst, nwHeadFmt, nwRegEx, nwUnicode
@@ -148,7 +147,7 @@ class Tokenizer(ABC):
self._linkHeaders = False # Add an anchor before headers
# Instance Variables
- self._hFormatter = HeadingFormatter()
+ self._hFormatter = HeadingFormatter(self._project)
self._firstScene = False # Flag to indicate that the first scene of the chapter
# This File
@@ -772,7 +771,8 @@ class Tokenizer(ABC):
class HeadingFormatter:
- def __init__(self) -> None:
+ def __init__(self, project: NWProject) -> None:
+ self._project = project
self._chCount = 0
self._scChCount = 0
self._scAbsCount = 0
@@ -801,7 +801,7 @@ class HeadingFormatter:
hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scChCount))
hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._scAbsCount))
if nwHeadFmt.CH_WORD in hFormat:
- chWord = SHARED.project.localLookup(self._chCount)
+ chWord = self._project.localLookup(self._chCount)
hFormat = hFormat.replace(nwHeadFmt.CH_WORD, chWord)
if nwHeadFmt.CH_ROML in hFormat:
chRom = numberToRoman(self._chCount, toLower=True)
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 193312f1..b4f7cb0f 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -290,9 +290,10 @@ class GuiManuscript(QDialog):
@pyqtSlot("QListWidgetItem*", "QListWidgetItem*")
def _updateBuildDetails(self, current: QListWidgetItem, previous: QListWidgetItem) -> None:
"""Process change of build selection to update the details."""
- build = self._builds.getBuild(current.data(self.D_KEY))
- if build is not None:
- self.buildDetails.updateInfo(build)
+ if isinstance(current, QListWidgetItem):
+ build = self._builds.getBuild(current.data(self.D_KEY))
+ if build is not None:
+ self.buildDetails.updateInfo(build)
return
@pyqtSlot()
@@ -586,7 +587,7 @@ class _DetailsWidget(QWidget):
item.addChild(sub)
# Headings
- hFmt = HeadingFormatter()
+ hFmt = HeadingFormatter(SHARED.project)
hFmt.incChapter()
hFmt.incScene()
hFmt.resetScene()
diff --git a/tests/test_core/test_core_buildsettings.py b/tests/test_core/test_core_buildsettings.py
index 505e9bef..ca75cc40 100644
--- a/tests/test_core/test_core_buildsettings.py
+++ b/tests/test_core/test_core_buildsettings.py
@@ -377,6 +377,8 @@ def testCoreBuildSettings_Collection(monkeypatch, mockGUI, fncPath: Path, mockRn
builds = BuildCollection(project)
assert len(builds) == 0
assert not buildsFile.exists()
+ assert builds.lastBuild == ""
+ assert builds.defaultBuild == ""
# Create a default build
buildOne = BuildSettings()
@@ -412,7 +414,9 @@ def testCoreBuildSettings_Collection(monkeypatch, mockGUI, fncPath: Path, mockRn
# Check the file content
data = json.loads(buildsFile.read_text(encoding="utf-8"))
- assert list(data["novelWriter.builds"].keys()) == [buildIDOne, buildIDTwo]
+ assert list(data["novelWriter.builds"].keys()) == [
+ "lastBuild", "defaultBuild", buildIDOne, buildIDTwo
+ ]
# Remove a build
builds.removeBuild(buildIDOne)
@@ -423,12 +427,16 @@ def testCoreBuildSettings_Collection(monkeypatch, mockGUI, fncPath: Path, mockRn
# Check the file content
data = json.loads(buildsFile.read_text(encoding="utf-8"))
- assert list(data["novelWriter.builds"].keys()) == [buildIDTwo]
+ assert list(data["novelWriter.builds"].keys()) == [
+ "lastBuild", "defaultBuild", buildIDTwo
+ ]
builds.setBuild(buildOne)
assert list(builds.builds()) == [
(buildIDTwo, "Build Two"),
(buildIDOne, "Build One"),
]
+ builds.setLastBuild(buildIDOne)
+ builds.setDefaultBuild(buildIDTwo)
# Check errors: No valid path
with monkeypatch.context() as mp:
@@ -447,7 +455,7 @@ def testCoreBuildSettings_Collection(monkeypatch, mockGUI, fncPath: Path, mockRn
buildsFile.write_text("foobar")
assert builds._loadCollection() is False
- # Check errors: Valid jason file, but list instead of object
+ # Check errors: Valid json file, but list instead of object
buildsFile.write_text("[]")
assert builds._loadCollection() is False
buildsFile.unlink()
@@ -459,5 +467,7 @@ def testCoreBuildSettings_Collection(monkeypatch, mockGUI, fncPath: Path, mockRn
(buildIDTwo, "Build Two"),
(buildIDOne, "Build One"),
]
+ assert another.lastBuild == buildIDOne
+ assert another.defaultBuild == buildIDTwo
# END Test testCoreBuildSettings_Collection
From dee1e9d71b0dfe6deecf27244c35012dbcd41ad1 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 1 Sep 2023 17:37:24 +0200
Subject: [PATCH 4/6] Re-sort imports
---
novelwriter/tools/manuscript.py | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index b4f7cb0f..d356418e 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -40,11 +40,11 @@ from PyQt5.QtWidgets import (
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
from novelwriter import CONFIG, SHARED
-from novelwriter.core.tokenizer import HeadingFormatter
from novelwriter.error import logException
from novelwriter.common import checkInt, fuzzyTime
from novelwriter.core.tohtml import ToHtml
from novelwriter.core.docbuild import NWBuildDocument
+from novelwriter.core.tokenizer import HeadingFormatter
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manussettings import GuiBuildSettings
From 206265b6d3357ccbed2b2d2575c3e75009ff0895 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 1 Sep 2023 17:39:02 +0200
Subject: [PATCH 5/6] Update base translation file
---
i18n/nw_base.ts | 60 ++++++++++++++++++++++++++++++++++++-------------
1 file changed, 44 insertions(+), 16 deletions(-)
diff --git a/i18n/nw_base.ts b/i18n/nw_base.ts
index a3df9f28..6db13f00 100644
--- a/i18n/nw_base.ts
+++ b/i18n/nw_base.ts
@@ -1986,53 +1986,53 @@
GuiManuscript
-
+
Build Manuscript
-
+
Add New Build
-
+
Delete Selected Build
-
+
Edit Selected Build
-
+
Builds
-
+
Preview
-
+
Print
-
+
Build
-
+
Close
-
-
+
+
My Manuscript
@@ -4487,6 +4487,34 @@
+
+ _DetailsWidget
+
+
+ Setting
+
+
+
+
+ Value
+
+
+
+
+ Name
+
+
+
+
+ Selection
+
+
+
+
+ Title
+
+
+
_FilterTab
@@ -4625,27 +4653,27 @@
_PreviewWidget
-
+
Press the "Build Preview" button to generate ...
-
+
Processing ...
-
+
Done
-
+
Unknown
-
+
Built
From 856e504c260e8a7c82104172d9841966dd3d0144 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Fri, 1 Sep 2023 18:46:01 +0200
Subject: [PATCH 6/6] Improve the ternary if in expand state on manuscript
dialog
---
novelwriter/tools/manuscript.py | 4 +---
1 file changed, 1 insertion(+), 3 deletions(-)
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index d356418e..b0163806 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -545,9 +545,7 @@ class _DetailsWidget(QWidget):
for i in range(self.listView.topLevelItemCount()):
item = self.listView.topLevelItem(i)
if isinstance(item, QTreeWidgetItem):
- item.setExpanded(
- (state[i] if i < count else True) if item.childCount() > 0 else False
- )
+ item.setExpanded((state[i] if i < count else True) and item.childCount() > 0)
return
##