Add code to filter items in build tool based on layout and active status

This commit is contained in:
Veronica Berglyd Olsen
2023-05-07 19:21:07 +02:00
parent e7a148d20d
commit 46b4e39bf5
3 changed files with 166 additions and 52 deletions
+98 -26
View File
@@ -25,8 +25,13 @@ along with this program. If not, see <https://www.gnu.org/licenses/>.
import logging import logging
from enum import Enum
from PyQt5.QtCore import QT_TRANSLATE_NOOP from PyQt5.QtCore import QT_TRANSLATE_NOOP
from novelwriter.core.item import NWItem
from novelwriter.core.project import NWProject
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# The Settings Template # The Settings Template
@@ -35,6 +40,9 @@ logger = logging.getLogger(__name__)
# (type, default, [min value, max value]) # (type, default, [min value, max value])
SETTINGS_TEMPLATE = { SETTINGS_TEMPLATE = {
"filter.includeNovel": (bool, True),
"filter.includeNotes": (bool, False),
"filter.includeInactive": (bool, False),
"headings.fmtTitle": (str, "%title%"), "headings.fmtTitle": (str, "%title%"),
"headings.fmtChapter": (str, "%title%"), "headings.fmtChapter": (str, "%title%"),
"headings.fmtUnnumbered": (str, "%title%"), "headings.fmtUnnumbered": (str, "%title%"),
@@ -42,6 +50,10 @@ SETTINGS_TEMPLATE = {
"headings.fmtSection": (str, "%title%"), "headings.fmtSection": (str, "%title%"),
"headings.hideScene": (bool, False), "headings.hideScene": (bool, False),
"headings.hideSection": (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.buildLang": (str, "en_GB"),
"format.textFont": (str, ""), "format.textFont": (str, ""),
"format.textSize": (str, ""), "format.textSize": (str, ""),
@@ -52,14 +64,12 @@ SETTINGS_TEMPLATE = {
"html.addStyles": (bool, False), "html.addStyles": (bool, False),
} }
FILTER_TEMPLATE = {
"filter.includeSynopsis": (bool, False),
"filter.includeComments": (bool, False),
"filter.includeKeywords": (bool, False),
"filter.includeBody": (bool, True),
}
SETTINGS_LABELS = { 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": QT_TRANSLATE_NOOP("Builds", "Headings"),
"headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Title Heading"), "headings.fmtTitle": QT_TRANSLATE_NOOP("Builds", "Title Heading"),
"headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Heading"), "headings.fmtChapter": QT_TRANSLATE_NOOP("Builds", "Chapter Heading"),
@@ -69,6 +79,12 @@ SETTINGS_LABELS = {
"headings.hideScene": QT_TRANSLATE_NOOP("Builds", "Hide Scene"), "headings.hideScene": QT_TRANSLATE_NOOP("Builds", "Hide Scene"),
"headings.hideSection": QT_TRANSLATE_NOOP("Builds", "Hide Section"), "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": QT_TRANSLATE_NOOP("Builds", "Text Format"),
"format.buildLang": QT_TRANSLATE_NOOP("Builds", "Build Language"), "format.buildLang": QT_TRANSLATE_NOOP("Builds", "Build Language"),
"format.textFont": QT_TRANSLATE_NOOP("Builds", "Font Family"), "format.textFont": QT_TRANSLATE_NOOP("Builds", "Font Family"),
@@ -84,27 +100,28 @@ SETTINGS_LABELS = {
"html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"), "html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"),
} }
FILTER_LABELS = {
"filter.includeSynopsis": QT_TRANSLATE_NOOP("Builds", "Synopsis"), class FilterMode(Enum):
"filter.includeComments": QT_TRANSLATE_NOOP("Builds", "Comments"),
"filter.includeKeywords": QT_TRANSLATE_NOOP("Builds", "Keywords"), UNKNOWN = 0
"filter.includeBody": QT_TRANSLATE_NOOP("Builds", "Body Text"), FILTERED = 1
} INCLUDED = 2
EXCLUDED = 3
# END Enum FilterMode
class BuildSettings: class BuildSettings:
def __init__(self): def __init__(self):
self._data = {}
self._excluded = set() self._excluded = set()
self._included = set() self._included = set()
self._settings = {k: v[1] for k, v in SETTINGS_TEMPLATE.items()}
self._loadTemplate()
return return
def isFiltered(self, tHandle):
return tHandle not in self._included and tHandle not in self._excluded
def isIncluded(self, tHandle): def isIncluded(self, tHandle):
return tHandle in self._included return tHandle in self._included
@@ -132,13 +149,68 @@ class BuildSettings:
self._included.discard(tHandle) self._included.discard(tHandle)
return return
## def setValue(self, key, value):
# Internal Functions """Set a specific value for a build setting.
##
def _loadTemplate(self):
"""Populate the data dictionary from the template.
""" """
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 # END Class BuildSettings
+7
View File
@@ -45,6 +45,13 @@ class NSwitchBox(QScrollArea):
self._wSwitch = 2*self._hSwitch self._wSwitch = 2*self._hSwitch
self._sIcon = int(round(0.8*baseSize)) 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 = QGridLayout()
self._content.setColumnStretch(1, 1) self._content.setColumnStretch(1, 1)
+61 -26
View File
@@ -34,7 +34,7 @@ from PyQt5.QtWidgets import (
QStackedWidget, QTreeWidget, QTreeWidgetItem, QVBoxLayout, QWidget 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.switchbox import NSwitchBox
from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.pagedsidebar import NPagedSideBar
@@ -62,6 +62,7 @@ class GuiBuildManuscript(QDialog):
self.buildOpts = { self.buildOpts = {
"name": self.tr("Default Settings"), "name": self.tr("Default Settings"),
"settings": BuildSettings(), "settings": BuildSettings(),
"filter": {},
} }
self.setWindowTitle(self.tr("Build Manuscript")) self.setWindowTitle(self.tr("Build Manuscript"))
@@ -131,6 +132,7 @@ class GuiBuildManuscript(QDialog):
"""Populate the tool widgets. """Populate the tool widgets.
""" """
self.optTabSelect.populateTree() self.optTabSelect.populateTree()
self.optTabSelect.populateFilters()
return return
## ##
@@ -168,7 +170,10 @@ class GuiBuildFilterTab(QWidget):
C_STATUS = 2 C_STATUS = 2
D_HANDLE = Qt.UserRole 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_NONE = 0
F_FILTERED = 1 F_FILTERED = 1
@@ -246,21 +251,7 @@ class GuiBuildFilterTab(QWidget):
# Filer Options # Filer Options
self.filterOpt = NSwitchBox(self, iPx) self.filterOpt = NSwitchBox(self, iPx)
self.filterOpt.switchToggled.connect(self._applyFilterSwitch)
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}")
# Assemble # Assemble
self.selectionBox = QVBoxLayout() self.selectionBox = QVBoxLayout()
@@ -285,6 +276,8 @@ class GuiBuildFilterTab(QWidget):
tHandle = nwItem.itemHandle tHandle = nwItem.itemHandle
pHandle = nwItem.itemParent pHandle = nwItem.itemParent
isFile = nwItem.isFileType() isFile = nwItem.isFileType()
isNovel = nwItem.isNovelLike()
isActive = nwItem.isActive
if nwItem.isInactiveClass(): if nwItem.isInactiveClass():
logger.debug("Skipping inactive class item '%s'", tHandle) logger.debug("Skipping inactive class item '%s'", tHandle)
@@ -296,7 +289,7 @@ class GuiBuildFilterTab(QWidget):
) )
if isFile: if isFile:
iconName = "checked" if nwItem.isActive else "unchecked" iconName = "checked" if isActive else "unchecked"
else: else:
iconName = "noncheckable" iconName = "noncheckable"
@@ -304,7 +297,10 @@ class GuiBuildFilterTab(QWidget):
trItem.setIcon(self.C_NAME, itemIcon) trItem.setIcon(self.C_NAME, itemIcon)
trItem.setText(self.C_NAME, nwItem.itemName) trItem.setText(self.C_NAME, nwItem.itemName)
trItem.setData(self.C_DATA, self.D_HANDLE, tHandle) 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_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.setIcon(self.C_ACTIVE, self.mainTheme.getIcon(iconName))
trItem.setTextAlignment(self.C_NAME, Qt.AlignLeft) trItem.setTextAlignment(self.C_NAME, Qt.AlignLeft)
@@ -330,6 +326,45 @@ class GuiBuildFilterTab(QWidget):
return 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 # Internal Functions
## ##
@@ -360,15 +395,15 @@ class GuiBuildFilterTab(QWidget):
def _setTreeItemMode(self): def _setTreeItemMode(self):
"""Update the filtered mode icon on all items. """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(): for tHandle, item in self._treeMap.items():
if item.data(self.C_DATA, self.D_FILE): allow, mode = filtered.get(tHandle, (False, FilterMode.UNKNOWN))
if buildSettings.isIncluded(tHandle): if mode == FilterMode.INCLUDED:
item.setIcon(self.C_STATUS, self._statusFlags[self.F_INCLUDED][1]) item.setIcon(self.C_STATUS, self._statusFlags[self.F_INCLUDED][1])
elif buildSettings.isExcluded(tHandle): elif mode == FilterMode.EXCLUDED:
item.setIcon(self.C_STATUS, self._statusFlags[self.F_EXCLUDED][1]) item.setIcon(self.C_STATUS, self._statusFlags[self.F_EXCLUDED][1])
else: elif mode == FilterMode.FILTERED and allow:
item.setIcon(self.C_STATUS, self._statusFlags[self.F_FILTERED][1]) item.setIcon(self.C_STATUS, self._statusFlags[self.F_FILTERED][1])
else: else:
item.setIcon(self.C_STATUS, self._statusFlags[self.F_NONE][1]) item.setIcon(self.C_STATUS, self._statusFlags[self.F_NONE][1])