Add build details widget to manuscript tool

This commit is contained in:
Veronica Berglyd Olsen
2023-09-01 16:55:23 +02:00
parent 746bd4bf2c
commit ecebd69cda
4 changed files with 267 additions and 33 deletions
+50 -5
View File
@@ -319,7 +319,6 @@ class BuildSettings:
value = min(max(value, definition[2]), definition[3]) value = min(max(value, definition[2]), definition[3])
self._changed = value != self._settings[key] self._changed = value != self._settings[key]
self._settings[key] = value self._settings[key] = value
logger.debug(f"Build Setting '{key}' set to: {value}")
return True return True
## ##
@@ -457,6 +456,8 @@ class BuildCollection:
def __init__(self, project: NWProject) -> None: def __init__(self, project: NWProject) -> None:
self._project = project self._project = project
self._builds = {} self._builds = {}
self._lastBuild = ""
self._defaultBuild = ""
self._loadCollection() self._loadCollection()
return return
@@ -465,7 +466,21 @@ class BuildCollection:
return len(self._builds) 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: def getBuild(self, buildID: str) -> BuildSettings | None:
@@ -476,6 +491,24 @@ class BuildCollection:
build.unpack(self._builds[buildID]) build.unpack(self._builds[buildID])
return build 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: def setBuild(self, build: BuildSettings) -> None:
"""Set build settings data in the collection.""" """Set build settings data in the collection."""
if isinstance(build, BuildSettings): if isinstance(build, BuildSettings):
@@ -484,6 +517,10 @@ class BuildCollection:
self._saveCollection() self._saveCollection()
return return
##
# Methods
##
def removeBuild(self, buildID: str) -> None: def removeBuild(self, buildID: str) -> None:
"""Remove the a build from the collection.""" """Remove the a build from the collection."""
self._builds.pop(buildID, None) self._builds.pop(buildID, None)
@@ -527,7 +564,11 @@ class BuildCollection:
return False return False
for key, entry in builds.items(): 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 self._builds[key] = entry
return True return True
@@ -540,9 +581,13 @@ class BuildCollection:
logger.debug("Saving builds file") logger.debug("Saving builds file")
try: 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: 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: except Exception:
logger.error("Failed to save builds file") logger.error("Failed to save builds file")
logException() logException()
+4 -3
View File
@@ -58,13 +58,14 @@ VALID_MAP = {
"GuiWordList": {"winWidth", "winHeight"}, "GuiWordList": {"winWidth", "winHeight"},
"GuiNovelView": {"lastCol", "lastColSize"}, "GuiNovelView": {"lastCol", "lastColSize"},
"GuiBuildSettings": { "GuiBuildSettings": {
"winWidth", "winHeight", "treeWidth", "filterWidth" "winWidth", "winHeight", "treeWidth", "filterWidth",
}, },
"GuiManuscript": { "GuiManuscript": {
"winWidth", "winHeight", "optsWidth", "viewWidth" "winWidth", "winHeight", "optsWidth", "viewWidth", "listHeight",
"detailsHeight", "detailsWidth", "detailsExpanded",
}, },
"GuiManuscriptBuild": { "GuiManuscriptBuild": {
"winWidth", "winHeight", "fmtWidth", "sumWidth" "winWidth", "winHeight", "fmtWidth", "sumWidth",
}, },
} }
+4 -4
View File
@@ -36,6 +36,7 @@ from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression from PyQt5.QtCore import QCoreApplication, QRegularExpression
from novelwriter import SHARED
from novelwriter.enum import nwItemLayout from novelwriter.enum import nwItemLayout
from novelwriter.common import formatTimeStamp, numberToRoman, checkInt from novelwriter.common import formatTimeStamp, numberToRoman, checkInt
from novelwriter.constants import nwConst, nwHeadFmt, nwRegEx, nwUnicode from novelwriter.constants import nwConst, nwHeadFmt, nwRegEx, nwUnicode
@@ -147,7 +148,7 @@ class Tokenizer(ABC):
self._linkHeaders = False # Add an anchor before headers self._linkHeaders = False # Add an anchor before headers
# Instance Variables # Instance Variables
self._hFormatter = HeadingFormatter(self._project) self._hFormatter = HeadingFormatter()
self._firstScene = False # Flag to indicate that the first scene of the chapter self._firstScene = False # Flag to indicate that the first scene of the chapter
# This File # This File
@@ -771,8 +772,7 @@ class Tokenizer(ABC):
class HeadingFormatter: class HeadingFormatter:
def __init__(self, project: NWProject) -> None: def __init__(self) -> None:
self._project = project
self._chCount = 0 self._chCount = 0
self._scChCount = 0 self._scChCount = 0
self._scAbsCount = 0 self._scAbsCount = 0
@@ -801,7 +801,7 @@ class HeadingFormatter:
hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scChCount)) hFormat = hFormat.replace(nwHeadFmt.SC_NUM, str(self._scChCount))
hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._scAbsCount)) hFormat = hFormat.replace(nwHeadFmt.SC_ABS, str(self._scAbsCount))
if nwHeadFmt.CH_WORD in hFormat: 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) hFormat = hFormat.replace(nwHeadFmt.CH_WORD, chWord)
if nwHeadFmt.CH_ROML in hFormat: if nwHeadFmt.CH_ROML in hFormat:
chRom = numberToRoman(self._chCount, toLower=True) chRom = numberToRoman(self._chCount, toLower=True)
+209 -21
View File
@@ -33,12 +33,14 @@ from datetime import datetime
from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent
from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
from PyQt5.QtWidgets import ( from PyQt5.QtWidgets import (
QDialog, QGridLayout, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, QDialog, QGridLayout, QHBoxLayout, QLabel, QListWidget, QListWidgetItem,
QSplitter, QTextBrowser, QToolButton, QVBoxLayout, QWidget, qApp QPushButton, QSplitter, QTextBrowser, QToolButton, QTreeWidget,
QTreeWidgetItem, QVBoxLayout, QWidget, qApp
) )
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
from novelwriter import CONFIG, SHARED from novelwriter import CONFIG, SHARED
from novelwriter.core.tokenizer import HeadingFormatter
from novelwriter.error import logException from novelwriter.error import logException
from novelwriter.common import checkInt, fuzzyTime from novelwriter.common import checkInt, fuzzyTime
from novelwriter.core.tohtml import ToHtml from novelwriter.core.tohtml import ToHtml
@@ -127,10 +129,6 @@ class GuiManuscript(QDialog):
self.lblBuilds = QLabel("<b>{0}</b>".format(self.tr("Builds"))) self.lblBuilds = QLabel("<b>{0}</b>".format(self.tr("Builds")))
self.buildList = QListWidget()
self.buildList.setIconSize(QSize(iPx, iPx))
self.buildList.doubleClicked.connect(self._editSelectedBuild)
self.listToolBox = QHBoxLayout() self.listToolBox = QHBoxLayout()
self.listToolBox.addWidget(self.lblBuilds) self.listToolBox.addWidget(self.lblBuilds)
self.listToolBox.addStretch(1) self.listToolBox.addStretch(1)
@@ -139,6 +137,27 @@ class GuiManuscript(QDialog):
self.listToolBox.addWidget(self.tbEdit) self.listToolBox.addWidget(self.tbEdit)
self.listToolBox.setSpacing(0) 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 # Process Controls
# ================ # ================
@@ -167,7 +186,7 @@ class GuiManuscript(QDialog):
self.controlBox = QVBoxLayout() self.controlBox = QVBoxLayout()
self.controlBox.addLayout(self.listToolBox, 0) 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.addLayout(self.processBox, 0)
self.controlBox.setContentsMargins(0, 0, 0, 0) self.controlBox.setContentsMargins(0, 0, 0, 0)
@@ -206,8 +225,13 @@ class GuiManuscript(QDialog):
build = BuildSettings() build = BuildSettings()
build.setName(self.tr("My Manuscript")) build.setName(self.tr("My Manuscript"))
self._builds.setBuild(build) self._builds.setBuild(build)
selected = build.buildID
else:
selected = self._builds.lastBuild
self._updateBuildsList() self._updateBuildsList()
if selected in self._buildMap:
self.buildList.setCurrentItem(self._buildMap[selected])
logger.debug("Loading build cache") logger.debug("Loading build cache")
cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json" cache = CONFIG.dataPath("cache") / f"build_{SHARED.project.data.uuid}.json"
@@ -263,6 +287,14 @@ class GuiManuscript(QDialog):
self._openSettingsDialog(build) self._openSettingsDialog(build)
return 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() @pyqtSlot()
def _deleteSelectedBuild(self): def _deleteSelectedBuild(self):
"""Delete the currently selected build settings entry.""" """Delete the currently selected build settings entry."""
@@ -278,6 +310,9 @@ class GuiManuscript(QDialog):
"""Process new build settings from the settings dialog.""" """Process new build settings from the settings dialog."""
self._builds.setBuild(build) self._builds.setBuild(build)
self._updateBuildItem(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 return
@pyqtSlot() @pyqtSlot()
@@ -363,8 +398,6 @@ class GuiManuscript(QDialog):
self.docPreview.setJustify( self.docPreview.setJustify(
build.getBool("format.justifyText") build.getBool("format.justifyText")
) )
if build.buildID and build.buildID in self._buildMap:
self._buildMap[build.buildID].setSelected(True)
return return
def _getSelectedBuild(self) -> BuildSettings | None: def _getSelectedBuild(self) -> BuildSettings | None:
@@ -383,6 +416,10 @@ class GuiManuscript(QDialog):
"""Save the user GUI settings.""" """Save the user GUI settings."""
logger.debug("Saving GuiManuscript 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()) winWidth = CONFIG.rpxInt(self.width())
winHeight = CONFIG.rpxInt(self.height()) winHeight = CONFIG.rpxInt(self.height())
@@ -390,11 +427,21 @@ class GuiManuscript(QDialog):
optsWidth = CONFIG.rpxInt(mainSplit[0]) optsWidth = CONFIG.rpxInt(mainSplit[0])
viewWidth = CONFIG.rpxInt(mainSplit[1]) 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 = SHARED.project.options
pOptions.setValue("GuiManuscript", "winWidth", winWidth) pOptions.setValue("GuiManuscript", "winWidth", winWidth)
pOptions.setValue("GuiManuscript", "winHeight", winHeight) pOptions.setValue("GuiManuscript", "winHeight", winHeight)
pOptions.setValue("GuiManuscript", "optsWidth", optsWidth) pOptions.setValue("GuiManuscript", "optsWidth", optsWidth)
pOptions.setValue("GuiManuscript", "viewWidth", viewWidth) 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() pOptions.saveSettings()
return return
@@ -444,9 +491,150 @@ class GuiManuscript(QDialog):
# END Class GuiManuscript # 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): class _PreviewWidget(QTextBrowser):
def __init__(self, parent: QWidget): def __init__(self, parent: QWidget) -> None:
super().__init__(parent=parent) super().__init__(parent=parent)
self._docTime = 0 self._docTime = 0
@@ -512,13 +700,13 @@ class _PreviewWidget(QTextBrowser):
# Setters # Setters
## ##
def setBuildName(self, name: str): def setBuildName(self, name: str) -> None:
"""Set the build name for the document label.""" """Set the build name for the document label."""
self._buildName = name self._buildName = name
self._updateBuildAge() self._updateBuildAge()
return return
def setJustify(self, state: bool): def setJustify(self, state: bool) -> None:
"""Enable/disable the justify text option.""" """Enable/disable the justify text option."""
pOptions = self.document().defaultTextOption() pOptions = self.document().defaultTextOption()
if state: if state:
@@ -528,7 +716,7 @@ class _PreviewWidget(QTextBrowser):
self.document().setDefaultTextOption(pOptions) self.document().setDefaultTextOption(pOptions)
return return
def setTextFont(self, family: str, size: int): def setTextFont(self, family: str, size: int) -> None:
"""Set the text font properties.""" """Set the text font properties."""
if family: if family:
font = QFont() font = QFont()
@@ -541,7 +729,7 @@ class _PreviewWidget(QTextBrowser):
# Methods # Methods
## ##
def beginNewBuild(self, length: int): def beginNewBuild(self, length: int) -> None:
"""Clear the document and show the progress bar.""" """Clear the document and show the progress bar."""
self.buildProgress.setMaximum(length) self.buildProgress.setMaximum(length)
self.buildProgress.setValue(0) self.buildProgress.setValue(0)
@@ -551,13 +739,13 @@ class _PreviewWidget(QTextBrowser):
self.clear() self.clear()
return return
def buildStep(self, value: int): def buildStep(self, value: int) -> None:
"""Update the progress bar value.""" """Update the progress bar value."""
self.buildProgress.setValue(value) self.buildProgress.setValue(value)
qApp.processEvents() qApp.processEvents()
return return
def setContent(self, data: dict): def setContent(self, data: dict) -> None:
"""Set the content of the preview widget.""" """Set the content of the preview widget."""
sPos = self.verticalScrollBar().value() sPos = self.verticalScrollBar().value()
qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
@@ -602,7 +790,7 @@ class _PreviewWidget(QTextBrowser):
# Events # Events
## ##
def resizeEvent(self, event: QResizeEvent): def resizeEvent(self, event: QResizeEvent) -> None:
"""Capture resize and update the document margins.""" """Capture resize and update the document margins."""
super().resizeEvent(event) super().resizeEvent(event)
self._updateDocMargins() self._updateDocMargins()
@@ -613,7 +801,7 @@ class _PreviewWidget(QTextBrowser):
## ##
@pyqtSlot("QPrinter*") @pyqtSlot("QPrinter*")
def printPreview(self, printer: QPrinter): def printPreview(self, printer: QPrinter) -> None:
"""Connect the print preview painter to the document viewer.""" """Connect the print preview painter to the document viewer."""
qApp.setOverrideCursor(QCursor(Qt.WaitCursor)) qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
printer.setOrientation(QPrinter.Portrait) printer.setOrientation(QPrinter.Portrait)
@@ -626,7 +814,7 @@ class _PreviewWidget(QTextBrowser):
## ##
@pyqtSlot() @pyqtSlot()
def _updateBuildAge(self): def _updateBuildAge(self) -> None:
"""Update the build time and the fuzzy age.""" """Update the build time and the fuzzy age."""
if self._docTime > 0: if self._docTime > 0:
strBuildTime = "%s (%s)" % ( strBuildTime = "%s (%s)" % (
@@ -642,7 +830,7 @@ class _PreviewWidget(QTextBrowser):
return return
@pyqtSlot() @pyqtSlot()
def _hideProgress(self): def _hideProgress(self) -> None:
"""Clean up the build progress bar.""" """Clean up the build progress bar."""
self.buildProgress.setVisible(False) self.buildProgress.setVisible(False)
return return
@@ -651,7 +839,7 @@ class _PreviewWidget(QTextBrowser):
# Internal Functions # Internal Functions
## ##
def _updateDocMargins(self): def _updateDocMargins(self) -> None:
"""Automatically adjust the header to fill the top of the """Automatically adjust the header to fill the top of the
document within the viewport. document within the viewport.
""" """