Modify logic so build settings are edited separately
This commit is contained in:
@@ -23,12 +23,14 @@ You should have received a copy of the GNU General Public License
|
||||
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
"""
|
||||
|
||||
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
|
||||
|
||||
@@ -26,7 +26,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||
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
|
||||
##
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user