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 ##