diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index db2895e1..acd2c921 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -25,8 +25,13 @@ along with this program. If not, see . import logging +from enum import Enum + from PyQt5.QtCore import QT_TRANSLATE_NOOP +from novelwriter.core.item import NWItem +from novelwriter.core.project import NWProject + logger = logging.getLogger(__name__) # The Settings Template @@ -35,6 +40,9 @@ logger = logging.getLogger(__name__) # (type, default, [min value, max value]) SETTINGS_TEMPLATE = { + "filter.includeNovel": (bool, True), + "filter.includeNotes": (bool, False), + "filter.includeInactive": (bool, False), "headings.fmtTitle": (str, "%title%"), "headings.fmtChapter": (str, "%title%"), "headings.fmtUnnumbered": (str, "%title%"), @@ -42,6 +50,10 @@ SETTINGS_TEMPLATE = { "headings.fmtSection": (str, "%title%"), "headings.hideScene": (bool, False), "headings.hideSection": (bool, False), + "text.includeSynopsis": (bool, False), + "text.includeComments": (bool, False), + "text.includeKeywords": (bool, False), + "text.includeBody": (bool, True), "format.buildLang": (str, "en_GB"), "format.textFont": (str, ""), "format.textSize": (str, ""), @@ -52,14 +64,12 @@ SETTINGS_TEMPLATE = { "html.addStyles": (bool, False), } -FILTER_TEMPLATE = { - "filter.includeSynopsis": (bool, False), - "filter.includeComments": (bool, False), - "filter.includeKeywords": (bool, False), - "filter.includeBody": (bool, True), -} - SETTINGS_LABELS = { + "filter": QT_TRANSLATE_NOOP("Builds", "Document Types"), + "filter.includeNovel": QT_TRANSLATE_NOOP("Builds", "Novel Documents"), + "filter.includeNotes": QT_TRANSLATE_NOOP("Builds", "Project Notes"), + "filter.includeInactive": QT_TRANSLATE_NOOP("Builds", "Inactive Documents"), + "headings": QT_TRANSLATE_NOOP("Builds", "Headings"), "headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Title Heading"), "headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Heading"), @@ -69,6 +79,12 @@ SETTINGS_LABELS = { "headings.hideScene": QT_TRANSLATE_NOOP("Builds", "Hide Scene"), "headings.hideSection": QT_TRANSLATE_NOOP("Builds", "Hide Section"), + "text": QT_TRANSLATE_NOOP("Builds", "Text Content"), + "text.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Synopsis"), + "text.includeComments": QT_TRANSLATE_NOOP("Builds", "Comments"), + "text.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Keywords"), + "text.includeBody": QT_TRANSLATE_NOOP("Builds", "Body Text"), + "format": QT_TRANSLATE_NOOP("Builds", "Text Format"), "format.buildLang": QT_TRANSLATE_NOOP("Builds", "Build Language"), "format.textFont": QT_TRANSLATE_NOOP("Builds", "Font Family"), @@ -84,27 +100,28 @@ SETTINGS_LABELS = { "html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"), } -FILTER_LABELS = { - "filter.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Synopsis"), - "filter.includeComments": QT_TRANSLATE_NOOP("Builds", "Comments"), - "filter.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Keywords"), - "filter.includeBody": QT_TRANSLATE_NOOP("Builds", "Body Text"), -} + +class FilterMode(Enum): + + UNKNOWN = 0 + FILTERED = 1 + INCLUDED = 2 + EXCLUDED = 3 + +# END Enum FilterMode class BuildSettings: def __init__(self): - - self._data = {} - self._excluded = set() self._included = set() - - self._loadTemplate() - + self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()} return + def isFiltered(self, tHandle): + return tHandle not in self._included and tHandle not in self._excluded + def isIncluded(self, tHandle): return tHandle in self._included @@ -132,13 +149,68 @@ class BuildSettings: self._included.discard(tHandle) return - ## - # Internal Functions - ## - - def _loadTemplate(self): - """Populate the data dictionary from the template. + def setValue(self, key, value): + """Set a specific value for a build setting. """ - return + if key not in SETTINGS_TEMPLATE: + return False + definition = SETTINGS_TEMPLATE[key] + if not isinstance(value, definition[0]): + return False + if len(definition) == 4: + value = min(max(value, definition[2]), definition[3]) + self._settings[key] = value + 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") + + 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 checkItemFilter(self, project): + """Return a dictionary of item handles with filter decissions + applied. + """ + result = {} + if not isinstance(project, NWProject): + return result + + incNovel = self.getValue("filter.includeNovel") or False + incNotes = self.getValue("filter.includeNotes") or False + incInactive = self.getValue("filter.includeInactive") or False + + for item in project.tree: + tHandle = item.itemHandle + if not isinstance(item, NWItem): + result[tHandle] = (False, FilterMode.UNKNOWN) + continue + if not item.isFileType(): + result[tHandle] = (False, FilterMode.FILTERED) + continue + if tHandle in self._included: + result[tHandle] = (True, FilterMode.INCLUDED) + continue + if tHandle in self._excluded: + result[tHandle] = (False, FilterMode.EXCLUDED) + continue + + isNote = item.isNoteLayout() + isNovel = item.isDocumentLayout() + isActive = item.isActive + + byActive = isActive or (not isActive and incInactive) + byLayout = (isNote and incNotes) or (isNovel and incNovel) + + isAllowed = byActive and byLayout + + result[tHandle] = (isAllowed, FilterMode.FILTERED) + + return result # END Class BuildSettings diff --git a/novelwriter/extensions/switchbox.py b/novelwriter/extensions/switchbox.py index 45d7680f..57e43f7e 100644 --- a/novelwriter/extensions/switchbox.py +++ b/novelwriter/extensions/switchbox.py @@ -45,6 +45,13 @@ class NSwitchBox(QScrollArea): self._wSwitch = 2*self._hSwitch self._sIcon = int(round(0.8*baseSize)) + self.clear() + + return + + def clear(self): + """Rebuild the content of the core widget. + """ self._content = QGridLayout() self._content.setColumnStretch(1, 1) diff --git a/novelwriter/tools/mbuild.py b/novelwriter/tools/mbuild.py index 11cabb0e..8eed17bb 100644 --- a/novelwriter/tools/mbuild.py +++ b/novelwriter/tools/mbuild.py @@ -34,7 +34,7 @@ from PyQt5.QtWidgets import ( QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget ) -from novelwriter.core.buildsettings import BuildSettings +from novelwriter.core.buildsettings import BuildSettings, FilterMode from novelwriter.extensions.switchbox import NSwitchBox from novelwriter.extensions.pagedsidebar import NPagedSideBar @@ -62,6 +62,7 @@ class GuiBuildManuscript(QDialog): self.buildOpts = { "name": self.tr("Default Settings"), "settings": BuildSettings(), + "filter": {}, } self.setWindowTitle(self.tr("Build Manuscript")) @@ -131,6 +132,7 @@ class GuiBuildManuscript(QDialog): """Populate the tool widgets. """ self.optTabSelect.populateTree() + self.optTabSelect.populateFilters() return ## @@ -168,7 +170,10 @@ class GuiBuildFilterTab(QWidget): C_STATUS = 2 D_HANDLE = Qt.UserRole - D_FILE = Qt.UserRole + 1 + D_FILTER = Qt.UserRole + 1 + D_FILE = Qt.UserRole + 2 + D_NOVEL = Qt.UserRole + 3 + D_ACTIVE = Qt.UserRole + 4 F_NONE = 0 F_FILTERED = 1 @@ -246,21 +251,7 @@ class GuiBuildFilterTab(QWidget): # Filer Options self.filterOpt = NSwitchBox(self, iPx) - - self.filterOpt.addLabel(self.tr("Document Types")) - self.filterOpt.addItem(QIcon(), "Novel Documents", "doc:novel") - self.filterOpt.addItem(QIcon(), "Project Notes", "doc:notes") - self.filterOpt.addItem(QIcon(), "Inactive Documents", "doc:inactive") - self.filterOpt.addSeparator() - - # Root Classes - self.filterOpt.addLabel(self.tr("Root Folders")) - for tHandle, nwItem in self.theProject.tree.iterRoots(None): - if not nwItem.isInactiveClass(): - itemIcon = self.mainTheme.getItemIcon( - nwItem.itemType, nwItem.itemClass, nwItem.itemLayout - ) - self.filterOpt.addItem(itemIcon, nwItem.itemName, f"root:{tHandle}") + self.filterOpt.switchToggled.connect(self._applyFilterSwitch) # Assemble self.selectionBox = QVBoxLayout() @@ -285,6 +276,8 @@ class GuiBuildFilterTab(QWidget): tHandle = nwItem.itemHandle pHandle = nwItem.itemParent isFile = nwItem.isFileType() + isNovel = nwItem.isNovelLike() + isActive = nwItem.isActive if nwItem.isInactiveClass(): logger.debug("Skipping inactive class item '%s'", tHandle) @@ -296,7 +289,7 @@ class GuiBuildFilterTab(QWidget): ) if isFile: - iconName = "checked" if nwItem.isActive else "unchecked" + iconName = "checked" if isActive else "unchecked" else: iconName = "noncheckable" @@ -304,7 +297,10 @@ class GuiBuildFilterTab(QWidget): trItem.setIcon(self.C_NAME, itemIcon) trItem.setText(self.C_NAME, nwItem.itemName) trItem.setData(self.C_DATA, self.D_HANDLE, tHandle) + trItem.setData(self.C_DATA, self.D_FILTER, False) trItem.setData(self.C_DATA, self.D_FILE, isFile) + trItem.setData(self.C_DATA, self.D_NOVEL, isNovel) + trItem.setData(self.C_DATA, self.D_ACTIVE, isActive) trItem.setIcon(self.C_ACTIVE, self.mainTheme.getIcon(iconName)) trItem.setTextAlignment(self.C_NAME, Qt.AlignLeft) @@ -330,6 +326,45 @@ class GuiBuildFilterTab(QWidget): return + def populateFilters(self): + """Populate the filter options switches. + """ + self.filterOpt.clear() + buildSettings = self.buildOpts["settings"] + + self.filterOpt.addLabel(buildSettings.getLabel("filter")) + for key in ["filter.includeNovel", "filter.includeNotes", "filter.includeInactive"]: + label = buildSettings.getLabel(key) + value = buildSettings.getValue(key) + if isinstance(value, bool): + self.filterOpt.addItem(QIcon(), label, f"doc:{key}", default=value) + + self.filterOpt.addSeparator() + + # Root Classes + self.filterOpt.addLabel(self.tr("Root Folders")) + for tHandle, nwItem in self.theProject.tree.iterRoots(None): + if not nwItem.isInactiveClass(): + itemIcon = self.mainTheme.getItemIcon( + nwItem.itemType, nwItem.itemClass, nwItem.itemLayout + ) + self.filterOpt.addItem(itemIcon, nwItem.itemName, f"root:{tHandle}", default=True) + + 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() + return + ## # Internal Functions ## @@ -360,15 +395,15 @@ class GuiBuildFilterTab(QWidget): def _setTreeItemMode(self): """Update the filtered mode icon on all items. """ - buildSettings = self.buildOpts["settings"] + filtered = self.buildOpts["settings"].checkItemFilter(self.theProject) for tHandle, item in self._treeMap.items(): - if item.data(self.C_DATA, self.D_FILE): - if buildSettings.isIncluded(tHandle): - item.setIcon(self.C_STATUS, self._statusFlags[self.F_INCLUDED][1]) - elif buildSettings.isExcluded(tHandle): - item.setIcon(self.C_STATUS, self._statusFlags[self.F_EXCLUDED][1]) - else: - item.setIcon(self.C_STATUS, self._statusFlags[self.F_FILTERED][1]) + allow, mode = filtered.get(tHandle, (False, FilterMode.UNKNOWN)) + if mode == FilterMode.INCLUDED: + item.setIcon(self.C_STATUS, self._statusFlags[self.F_INCLUDED][1]) + elif mode == FilterMode.EXCLUDED: + item.setIcon(self.C_STATUS, self._statusFlags[self.F_EXCLUDED][1]) + elif mode == FilterMode.FILTERED and allow: + item.setIcon(self.C_STATUS, self._statusFlags[self.F_FILTERED][1]) else: item.setIcon(self.C_STATUS, self._statusFlags[self.F_NONE][1])