diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py
index 161bbf6f..a6880525 100644
--- a/novelwriter/core/buildsettings.py
+++ b/novelwriter/core/buildsettings.py
@@ -23,12 +23,14 @@ You should have received a copy of the GNU General Public License
along with this program. If not, see .
"""
+import uuid
import logging
from enum import Enum
from PyQt5.QtCore import QT_TRANSLATE_NOOP
+from novelwriter.common import checkUuid, isHandle
from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject
@@ -115,29 +117,72 @@ class FilterMode(Enum):
class BuildSettings:
def __init__(self):
- self._skiproot = set()
+ self._name = ""
+ self._uuid = ""
+ self._skipRoot = set()
self._excluded = set()
self._included = set()
self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()}
+ self._changed = False
return
- def isFiltered(self, tHandle):
- return tHandle not in self._included and tHandle not in self._excluded
+ ##
+ # Properties
+ ##
- def isIncluded(self, tHandle):
- return tHandle in self._included
+ @property
+ def name(self):
+ return self._name
- def isExcluded(self, tHandle):
- return tHandle in self._excluded
+ @property
+ def buildID(self):
+ return self._uuid
- def isRootAllowed(self, tHandle):
- return tHandle not in self._skiproot
+ @property
+ def changed(self):
+ return self._changed
+
+ ##
+ # Getters
+ ##
+
+ @staticmethod
+ def getLabel(key):
+ """Extract the label for a specific item.
+ """
+ return SETTINGS_LABELS.get(key, "ERROR")
+
+ def getValue(self, key):
+ """Get the value for a specific item, or return the default.
+ """
+ return self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None)[1]))
+
+ ##
+ # Setters
+ ##
+
+ def setName(self, name):
+ """Set the build setting display name.
+ """
+ self._name = str(name)
+ return
+
+ def setBuildID(self, value):
+ """Set a UUID build ID.
+ """
+ value = checkUuid(value, "")
+ if not value:
+ self._uuid = str(uuid.uuid4())
+ elif value != self._uuid:
+ self._uuid = value
+ return
def setFiltered(self, tHandle):
"""Set an item as filtered.
"""
self._excluded.discard(tHandle)
self._included.discard(tHandle)
+ self._changed = True
return
def setIncluded(self, tHandle):
@@ -145,6 +190,7 @@ class BuildSettings:
"""
self._excluded.discard(tHandle)
self._included.add(tHandle)
+ self._changed = True
return
def setExcluded(self, tHandle):
@@ -152,15 +198,18 @@ class BuildSettings:
"""
self._excluded.add(tHandle)
self._included.discard(tHandle)
+ self._changed = True
return
def setSkipRoot(self, tHandle, state):
"""Set a specific root folder as skipped or not.
"""
if state is True:
- self._skiproot.discard(tHandle)
+ self._skipRoot.discard(tHandle)
+ self._changed = True
elif state is False:
- self._skiproot.add(tHandle)
+ self._skipRoot.add(tHandle)
+ self._changed = True
return
def setValue(self, key, value):
@@ -174,20 +223,27 @@ class BuildSettings:
if len(definition) == 4:
value = min(max(value, definition[2]), definition[3])
self._settings[key] = value
+ self._changed = True
logger.debug(f"Build Setting '{key}' set to: {value}")
return True
- def getLabel(self, key):
- """Extract the label for a specific item.
- """
- return SETTINGS_LABELS.get(key, "ERROR")
+ ##
+ # Methods
+ ##
- def getValue(self, key):
- """Get the value for a specific item, or return the default.
- """
- return self._settings.get(key, SETTINGS_TEMPLATE.get(key, (None, None)[1]))
+ def isFiltered(self, tHandle):
+ return tHandle not in self._included and tHandle not in self._excluded
- def checkItemFilter(self, project):
+ def isIncluded(self, tHandle):
+ return tHandle in self._included
+
+ def isExcluded(self, tHandle):
+ return tHandle in self._excluded
+
+ def isRootAllowed(self, tHandle):
+ return tHandle not in self._skipRoot
+
+ def buildItemFilter(self, project):
"""Return a dictionary of item handles with filter decissions
applied.
"""
@@ -204,7 +260,7 @@ class BuildSettings:
if not isinstance(item, NWItem):
result[tHandle] = (False, FilterMode.UNKNOWN)
continue
- if item.isInactiveClass() or (item.itemRoot in self._skiproot):
+ if item.isInactiveClass() or (item.itemRoot in self._skipRoot):
result[tHandle] = (False, FilterMode.SKIPPED)
continue
if not item.isFileType():
@@ -230,4 +286,49 @@ class BuildSettings:
return result
+ def resetChangedState(self):
+ """This must be called when the changes to this class has been
+ safely saved to file or passed on.
+ """
+ self._changed = False
+ return
+
+ def pack(self):
+ """Pack all content into a JSON compatible dictionary.
+ """
+ return {
+ "name": self._name,
+ "uuid": self._uuid,
+ "settings": self._settings.copy(),
+ "included": list(self._included),
+ "excluded": list(self._excluded),
+ "skipRoot": list(self._skipRoot),
+ }
+
+ def unpack(self, data):
+ """Unpack a dictionary and populate the class.
+ """
+ included = data.get("included", [])
+ excluded = data.get("excluded", [])
+ skipRoot = data.get("skipRoot", [])
+ settings = data.get("settings", {})
+
+ self.setName(data.get("name", ""))
+ self.setBuildID(data.get("uuid", ""))
+ if isinstance(included, list):
+ self._included = set([h for h in included if isHandle(h)])
+ if isinstance(excluded, list):
+ self._excluded = set([h for h in excluded if isHandle(h)])
+ if isinstance(skipRoot, list):
+ self._skipRoot = set([h for h in skipRoot if isHandle(h)])
+
+ self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()}
+ if isinstance(settings, dict):
+ for key, value in settings.items():
+ self.setValue(key, value)
+
+ self._changed = False
+
+ return
+
# END Class BuildSettings
diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py
index dc871a2d..b55ba46b 100644
--- a/novelwriter/tools/manusbuild.py
+++ b/novelwriter/tools/manusbuild.py
@@ -26,7 +26,13 @@ along with this program. If not, see .
import logging
import novelwriter
-from PyQt5.QtWidgets import QDialog, QGridLayout, QSplitter, QTextBrowser, QVBoxLayout, QWidget
+from PyQt5.QtWidgets import (
+ QDialog, QGridLayout, QPushButton, QSplitter, QTextBrowser, QVBoxLayout,
+ QWidget, qApp
+)
+
+from novelwriter.common import getGuiItem
+from novelwriter.tools.manussettings import GuiBuildSettings
logger = logging.getLogger(__name__)
@@ -57,7 +63,11 @@ class GuiBuildManuscript(QDialog):
# Controls
# ========
+ self.btnNew = QPushButton(self.tr("New Build"))
+ self.btnNew.clicked.connect(self._createNewBuild)
+
self.optsGrid = QGridLayout()
+ self.optsGrid.addWidget(self.btnNew, 0, 0)
self.manPreview = GuiManuscriptPreview(self)
@@ -98,6 +108,27 @@ class GuiBuildManuscript(QDialog):
event.accept()
return
+ ##
+ # Private Slots
+ ##
+
+ def _createNewBuild(self):
+ """Open the build settings dialog for a new build.
+ """
+ dlgSettings = getGuiItem("GuiBuildSettings")
+ if dlgSettings is None:
+ dlgSettings = GuiBuildSettings(self)
+ assert isinstance(dlgSettings, GuiBuildSettings)
+
+ dlgSettings.setModal(False)
+ dlgSettings.show()
+ dlgSettings.raise_()
+ qApp.processEvents()
+
+ dlgSettings.loadContent({"name": self.tr("My Manuscript")})
+
+ return
+
##
# Internal Functions
##
diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py
index 65d517ce..3207d2fc 100644
--- a/novelwriter/tools/manussettings.py
+++ b/novelwriter/tools/manussettings.py
@@ -60,11 +60,7 @@ class GuiBuildSettings(QDialog):
self.mainTheme = mainGui.mainTheme
self.theProject = mainGui.theProject
- self.buildOpts = {
- "name": self.tr("Default Settings"),
- "settings": BuildSettings(),
- "filter": {},
- }
+ self._build = None
self.setWindowTitle(self.tr("Manuscript Build Settings"))
self.setMinimumWidth(self.mainConf.pxInt(700))
@@ -136,11 +132,17 @@ class GuiBuildSettings(QDialog):
return
- def loadContent(self):
- """Populate the tool widgets.
+ def loadContent(self, data):
+ """Populate the child widgets.
"""
- self.optTabSelect.populateTree()
- self.optTabSelect.populateFilters()
+ if isinstance(self._build, BuildSettings) and self._build.changed:
+ logger.error("There are unsaved changes in this GUI")
+ return
+
+ self._build = BuildSettings()
+ self._build.unpack(data)
+ self.optTabSelect.loadContent(self._build)
+
return
##
@@ -226,9 +228,9 @@ class GuiBuildFilterTab(QWidget):
self.mainGui = buildMain.mainGui
self.mainTheme = buildMain.mainGui.mainTheme
self.theProject = buildMain.mainGui.theProject
- self.buildOpts = buildMain.buildOpts
self._treeMap = {}
+ self._build = None
self._statusFlags = {
self.F_NONE: ("", QIcon()),
@@ -319,6 +321,14 @@ class GuiBuildFilterTab(QWidget):
return
+ def loadContent(self, build):
+ """Populate the widgets.
+ """
+ self._build = build
+ self._populateTree()
+ self._populateFilters()
+ return
+
def mainSplitSizes(self):
"""Extract the sizes of the main splitter.
"""
@@ -327,13 +337,37 @@ class GuiBuildFilterTab(QWidget):
return 0, 0
return sizes[0], sizes[1]
- def populateTree(self):
+ ##
+ # Slots
+ ##
+
+ @pyqtSlot(str, bool)
+ def _applyFilterSwitch(self, key, state):
+ """A filter switch has been toggled, so update the settings.
+ """
+ if not isinstance(self._build, BuildSettings):
+ return
+ if key.startswith("doc:"):
+ self._build.setValue(key[4:], state)
+ self._setTreeItemMode()
+ elif key.startswith("root:"):
+ self._build.setSkipRoot(key[5:], state)
+ self._populateTree()
+ return
+
+ ##
+ # Internal Functions
+ ##
+
+ def _populateTree(self):
"""Build the tree of project items.
"""
+ if not isinstance(self._build, BuildSettings):
+ return
+
logger.debug("Building project tree")
self._treeMap = {}
self.optTree.clear()
- bSettings = self.buildOpts["settings"]
for nwItem in self.theProject.getProjectItems():
tHandle = nwItem.itemHandle
@@ -342,7 +376,7 @@ class GuiBuildFilterTab(QWidget):
isFile = nwItem.isFileType()
isActive = nwItem.isActive
- if nwItem.isInactiveClass() or not bSettings.isRootAllowed(rHandle):
+ if nwItem.isInactiveClass() or not self._build.isRootAllowed(rHandle):
continue
hLevel = nwItem.mainHeading
@@ -385,30 +419,31 @@ class GuiBuildFilterTab(QWidget):
return
- def populateFilters(self):
+ def _populateFilters(self):
"""Populate the filter options switches.
"""
self.filterOpt.clear()
- bSettings = self.buildOpts["settings"]
+ if not isinstance(self._build, BuildSettings):
+ return
- self.filterOpt.addLabel(bSettings.getLabel("filter"))
+ self.filterOpt.addLabel(self._build.getLabel("filter"))
self.filterOpt.addItem(
self.mainTheme.getIcon("proj_scene"),
- bSettings.getLabel("filter.includeNovel"),
+ self._build.getLabel("filter.includeNovel"),
"doc:filter.includeNovel",
- default=bSettings.getValue("filter.includeNovel") or False
+ default=self._build.getValue("filter.includeNovel") or False
)
self.filterOpt.addItem(
self.mainTheme.getIcon("proj_note"),
- bSettings.getLabel("filter.includeNotes"),
+ self._build.getLabel("filter.includeNotes"),
"doc:filter.includeNotes",
- default=bSettings.getValue("filter.includeNotes") or False
+ default=self._build.getValue("filter.includeNotes") or False
)
self.filterOpt.addItem(
self.mainTheme.getIcon("unchecked"),
- bSettings.getLabel("filter.includeInactive"),
+ self._build.getLabel("filter.includeInactive"),
"doc:filter.includeInactive",
- default=bSettings.getValue("filter.includeInactive") or False
+ default=self._build.getValue("filter.includeInactive") or False
)
self.filterOpt.addSeparator()
@@ -424,30 +459,11 @@ class GuiBuildFilterTab(QWidget):
return
- ##
- # Slots
- ##
-
- @pyqtSlot(str, bool)
- def _applyFilterSwitch(self, key, state):
- """A filter switch has been toggled, so update the settings.
- """
- if key.startswith("doc:"):
- self.buildOpts["settings"].setValue(key[4:], state)
- self._setTreeItemMode()
- elif key.startswith("root:"):
- self.buildOpts["settings"].setSkipRoot(key[5:], state)
- self.populateTree()
- return
-
- ##
- # Internal Functions
- ##
-
def _setSelectedMode(self, mode):
"""Set the mode for the selected items.
"""
- bSettings = self.buildOpts["settings"]
+ if not isinstance(self._build, BuildSettings):
+ return
for item in self.optTree.selectedItems():
if not isinstance(item, QTreeWidgetItem):
@@ -457,11 +473,11 @@ class GuiBuildFilterTab(QWidget):
isFile = item.data(self.C_DATA, self.D_FILE)
if isFile:
if mode == self.F_FILTERED:
- bSettings.setFiltered(tHandle)
+ self._build.setFiltered(tHandle)
elif mode == self.F_INCLUDED:
- bSettings.setIncluded(tHandle)
+ self._build.setIncluded(tHandle)
elif mode == self.F_EXCLUDED:
- bSettings.setExcluded(tHandle)
+ self._build.setExcluded(tHandle)
self._setTreeItemMode()
@@ -470,7 +486,10 @@ class GuiBuildFilterTab(QWidget):
def _setTreeItemMode(self):
"""Update the filtered mode icon on all items.
"""
- filtered = self.buildOpts["settings"].checkItemFilter(self.theProject)
+ if not isinstance(self._build, BuildSettings):
+ return
+
+ filtered = self._build.buildItemFilter(self.theProject)
for tHandle, item in self._treeMap.items():
allow, mode = filtered.get(tHandle, (False, FilterMode.UNKNOWN))
if mode == FilterMode.INCLUDED:
@@ -495,12 +514,10 @@ class GuiBuildHeadingsTab(QWidget):
self.mainGui = buildMain.mainGui
self.mainTheme = buildMain.mainGui.mainTheme
self.theProject = buildMain.mainGui.theProject
- self.buildOpts = buildMain.buildOpts
iPx = self.mainTheme.baseIconSize
vSp = self.mainConf.pxInt(12)
bSp = self.mainConf.pxInt(6)
- bSettings = self.buildOpts["settings"]
# Format Boxes
# ============
@@ -508,7 +525,7 @@ class GuiBuildHeadingsTab(QWidget):
self.formatBox.setHorizontalSpacing(vSp)
# Title Heading
- self.lblTitle = QLabel(bSettings.getLabel("headings.fmtTitle"))
+ self.lblTitle = QLabel(BuildSettings.getLabel("headings.fmtTitle"))
self.fmtTitle = QLineEdit("")
self.fmtTitle.setEnabled(False)
self.btnTitle = QToolButton()
@@ -523,7 +540,7 @@ class GuiBuildHeadingsTab(QWidget):
self.formatBox.addLayout(wrapTitle, 0, 1, Qt.AlignLeft)
# Chapter Heading
- self.lblChapter = QLabel(bSettings.getLabel("headings.fmtChapter"))
+ self.lblChapter = QLabel(BuildSettings.getLabel("headings.fmtChapter"))
self.fmtChapter = QLineEdit("")
self.fmtChapter.setEnabled(False)
self.btnChapter = QToolButton()
@@ -538,7 +555,7 @@ class GuiBuildHeadingsTab(QWidget):
self.formatBox.addLayout(wrapChapter, 1, 1, Qt.AlignLeft)
# Unnumbered Chapter Heading
- self.lblUnChapter = QLabel(bSettings.getLabel("headings.fmtUnnumbered"))
+ self.lblUnChapter = QLabel(BuildSettings.getLabel("headings.fmtUnnumbered"))
self.fmtUnChapter = QLineEdit("")
self.fmtUnChapter.setEnabled(False)
self.btnUnChapter = QToolButton()
@@ -553,7 +570,7 @@ class GuiBuildHeadingsTab(QWidget):
self.formatBox.addLayout(wrapUnChapter, 2, 1, Qt.AlignLeft)
# Scene Heading
- self.lblScene = QLabel(bSettings.getLabel("headings.fmtScene"))
+ self.lblScene = QLabel(BuildSettings.getLabel("headings.fmtScene"))
self.fmtScene = QLineEdit("")
self.fmtScene.setEnabled(False)
self.btnScene = QToolButton()
@@ -575,7 +592,7 @@ class GuiBuildHeadingsTab(QWidget):
self.formatBox.addLayout(wrapSceneHide, 3, 2, Qt.AlignLeft)
# Section Heading
- self.lblSection = QLabel(bSettings.getLabel("headings.fmtSection"))
+ self.lblSection = QLabel(BuildSettings.getLabel("headings.fmtSection"))
self.fmtSection = QLineEdit("")
self.fmtSection.setEnabled(False)
self.btnSection = QToolButton()