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()