From c480db898ce46caca3ccd0c8d36fb70e36a56398 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Aug 2023 15:08:57 +0200 Subject: [PATCH 01/12] Double the left margin of the button text for the paged side bar --- novelwriter/extensions/pagedsidebar.py | 51 ++++++++++++++------------ 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index 7afd727f..b2bcd44f 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -1,7 +1,6 @@ """ novelWriter – Custom Widget: Paged SideBar ========================================== -A custom widget for making a sidebar for flipping through pages File History: Created: 2023-02-21 [2.1b1] NPagedSideBar @@ -24,20 +23,26 @@ General Public License for more details. You should have received a copy of the GNU General Public License along with this program. If not, see . """ +from __future__ import annotations -from PyQt5.QtGui import QColor, QPainter +from PyQt5.QtGui import QColor, QPaintEvent, QPainter from PyQt5.QtCore import QRectF, Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( - QButtonGroup, QLabel, QSizePolicy, QStyle, QStyleOptionToolButton, QToolBar, - QToolButton, QWidget + QAbstractButton, QAction, QButtonGroup, QLabel, QSizePolicy, QStyle, + QStyleOptionToolButton, QToolBar, QToolButton, QWidget ) class NPagedSideBar(QToolBar): + """Extensions: Paged Side Bar + + A side bar widget that holds buttons that mimic tabs. It is designed + to be used in combination with a QStackedWidget for options panels. + """ buttonClicked = pyqtSignal(int) - def __init__(self, parent): + def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) self._buttons = [] @@ -58,7 +63,7 @@ class NPagedSideBar(QToolBar): return - def setLabelColor(self, color): + def setLabelColor(self, color: list | QColor) -> None: """Set the text color for the labels.""" if isinstance(color, list): self._labelCol = QColor(*color) @@ -66,7 +71,7 @@ class NPagedSideBar(QToolBar): self._labelCol = color return - def addSeparator(self): + def addSeparator(self) -> None: """Add a spacer widget.""" spacer = QWidget(self) spacer.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) @@ -74,16 +79,16 @@ class NPagedSideBar(QToolBar): self.insertWidget(self._stretchAction, spacer) return - def addLabel(self, text): + def addLabel(self, text: str) -> None: """Add a new label to the toolbar.""" - label = NPagedToolLabel(self, self._labelCol) + label = _NPagedToolLabel(self, self._labelCol) label.setText(text) self.insertWidget(self._stretchAction, label) return - def addButton(self, text, buttonId=-1): + def addButton(self, text: str, buttonId: int = -1) -> QAction: """Add a new button to the toolbar.""" - button = NPagedToolButton(self) + button = _NPagedToolButton(self) button.setText(text) action = self.insertWidget(self._stretchAction, button) @@ -94,7 +99,7 @@ class NPagedSideBar(QToolBar): return action - def setSelected(self, buttonId): + def setSelected(self, buttonId: int) -> None: """Set the selected button.""" self._group.button(buttonId).setChecked(True) return @@ -104,7 +109,7 @@ class NPagedSideBar(QToolBar): ## @pyqtSlot("QAbstractButton*") - def _buttonClicked(self, button): + def _buttonClicked(self, button: QAbstractButton) -> None: """A button was clicked in the group, emit its id.""" buttonId = self._group.id(button) if buttonId != -1: @@ -114,11 +119,11 @@ class NPagedSideBar(QToolBar): # END Class NPagedSideBar -class NPagedToolButton(QToolButton): +class _NPagedToolButton(QToolButton): __slots__ = ("_bH", "_tM", "_lM", "_cR") - def __init__(self, parent): + def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) @@ -133,7 +138,7 @@ class NPagedToolButton(QToolButton): return - def paintEvent(self, event): + def paintEvent(self, event: QPaintEvent) -> None: """Overload the paint event to draw a simple, left aligned text label, with a highlight when selected and a transparent base colour when hovered. @@ -164,23 +169,23 @@ class NPagedToolButton(QToolButton): else: textCol = palette.text().color() - tW = width - 2*self._lM + tW = width - 3*self._lM tH = height - 2*self._tM paint.setPen(textCol) paint.setOpacity(1.0) - paint.drawText(QRectF(self._lM, self._tM, tW, tH), Qt.AlignLeft, self.text()) + paint.drawText(QRectF(2*self._lM, self._tM, tW, tH), Qt.AlignLeft, self.text()) return -# END Class NPagedToolButton +# END Class _NPagedToolButton -class NPagedToolLabel(QLabel): +class _NPagedToolLabel(QLabel): __slots__ = ("_bH", "_tM", "_lM", "_textCol") - def __init__(self, parent, textColor=None): + def __init__(self, parent: QWidget, textColor: QColor | None = None) -> None: super().__init__(parent=parent) self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Fixed) @@ -195,7 +200,7 @@ class NPagedToolLabel(QLabel): return - def paintEvent(self, event): + def paintEvent(self, event: QPaintEvent) -> None: """Overload the paint event to draw a simple, left aligned text label that matches the button style. """ @@ -215,4 +220,4 @@ class NPagedToolLabel(QLabel): return -# END Class NPagedToolLabel +# END Class _NPagedToolLabel From 7a7416522187aa4e5cd2689a1ed0b0aeb564a906 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Aug 2023 15:39:30 +0200 Subject: [PATCH 02/12] Improve section headings on filter tab --- novelwriter/core/buildsettings.py | 2 +- novelwriter/tools/manussettings.py | 74 +++++++++++++++--------------- 2 files changed, 38 insertions(+), 38 deletions(-) diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index a5c5cd56..c0814f87 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -84,7 +84,7 @@ SETTINGS_TEMPLATE = { } SETTINGS_LABELS = { - "filter": QT_TRANSLATE_NOOP("Builds", "Document Types"), + "filter": QT_TRANSLATE_NOOP("Builds", "Document Filters"), "filter.includeNovel": QT_TRANSLATE_NOOP("Builds", "Novel Documents"), "filter.includeNotes": QT_TRANSLATE_NOOP("Builds", "Project Notes"), "filter.includeInactive": QT_TRANSLATE_NOOP("Builds", "Inactive Documents"), diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index d2666e42..33ce7691 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -69,7 +69,7 @@ class GuiBuildSettings(QDialog): newSettingsReady = pyqtSignal(BuildSettings) - def __init__(self, parent: QWidget, mainGui: GuiMain, build: BuildSettings): + def __init__(self, parent: QWidget, mainGui: GuiMain, build: BuildSettings) -> None: super().__init__(parent=parent) logger.debug("Create: GuiBuildSettings") @@ -169,10 +169,10 @@ class GuiBuildSettings(QDialog): return - def __del__(self): # pragma: no cover + def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiBuildSettings") - def loadContent(self): + def loadContent(self) -> None: """Populate the child widgets.""" self.editBuildName.setText(self._build.name) self.optTabSelect.loadContent() @@ -187,7 +187,7 @@ class GuiBuildSettings(QDialog): ## @pyqtSlot(int) - def _stackPageSelected(self, pageId: int): + def _stackPageSelected(self, pageId: int) -> None: """Process a user request to switch page.""" if pageId == self.OPT_FILTERS: self.toolStack.setCurrentWidget(self.optTabSelect) @@ -202,7 +202,7 @@ class GuiBuildSettings(QDialog): return @pyqtSlot("QAbstractButton*") - def _dialogButtonClicked(self, button: QAbstractButton): + def _dialogButtonClicked(self, button: QAbstractButton) -> None: """Handle button clicks from the dialog button box.""" role = self.dlgButtons.buttonRole(button) if role == QDialogButtonBox.ApplyRole: @@ -218,7 +218,7 @@ class GuiBuildSettings(QDialog): # Events ## - def closeEvent(self, event: QEvent): + def closeEvent(self, event: QEvent) -> None: """Capture the user closing the window so we can save settings. """ @@ -233,7 +233,7 @@ class GuiBuildSettings(QDialog): # Internal Functions ## - def _askToSaveBuild(self): + def _askToSaveBuild(self) -> None: """Check if there are unsaved changes, and if there are, ask if it's ok to reject them. """ @@ -247,7 +247,7 @@ class GuiBuildSettings(QDialog): self._build.resetChangedState() return - def _saveSettings(self): + def _saveSettings(self) -> None: """Save the various user settings.""" logger.debug("Saving GuiBuildSettings settings") @@ -265,7 +265,7 @@ class GuiBuildSettings(QDialog): return - def _emitBuildData(self): + def _emitBuildData(self) -> None: """Assemble the build data and emit the signal.""" self._build.setName(self.editBuildName.text()) self.optTabHeadings.saveContent() @@ -294,7 +294,7 @@ class _FilterTab(QWidget): F_INCLUDED = 2 F_EXCLUDED = 3 - def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings): + def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: super().__init__(parent=buildMain) self.mainGui = buildMain.mainGui @@ -401,7 +401,7 @@ class _FilterTab(QWidget): return - def loadContent(self): + def loadContent(self) -> None: """Populate the widgets.""" self._populateTree() self._populateFilters() @@ -418,7 +418,7 @@ class _FilterTab(QWidget): ## @pyqtSlot(str, bool) - def _applyFilterSwitch(self, key: str, state: bool): + def _applyFilterSwitch(self, key: str, state: bool) -> None: """Apply filter switch and update the settings.""" if key.startswith("doc:"): self._build.setValue(key[4:], state) @@ -432,7 +432,7 @@ class _FilterTab(QWidget): # Internal Functions ## - def _populateTree(self): + def _populateTree(self) -> None: """Build the tree of project items.""" logger.debug("Building project tree") self._treeMap = {} @@ -486,7 +486,7 @@ class _FilterTab(QWidget): return - def _populateFilters(self): + def _populateFilters(self) -> None: """Populate the filter options switches.""" self.filterOpt.clear() self.filterOpt.addLabel(self._build.getLabel("filter")) @@ -512,7 +512,7 @@ class _FilterTab(QWidget): self.filterOpt.addSeparator() # Root Classes - self.filterOpt.addLabel(self.tr("Root Folders")) + self.filterOpt.addLabel(self.tr("Select Root Folders")) for tHandle, nwItem in self.theProject.tree.iterRoots(None): if not nwItem.isInactiveClass(): itemIcon = self.mainTheme.getItemIcon( @@ -525,7 +525,7 @@ class _FilterTab(QWidget): return - def _setSelectedMode(self, mode: int): + def _setSelectedMode(self, mode: int) -> None: """Set the mode for the selected items.""" for item in self.optTree.selectedItems(): if isinstance(item, QTreeWidgetItem): @@ -543,7 +543,7 @@ class _FilterTab(QWidget): return - def _setTreeItemMode(self): + def _setTreeItemMode(self) -> None: """Update the filtered mode icon on all items.""" filtered = self._build.buildItemFilter(self.theProject) for tHandle, item in self._treeMap.items(): @@ -569,7 +569,7 @@ class _HeadingsTab(QWidget): EDIT_SCENE = 4 EDIT_SECTION = 5 - def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings): + def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: super().__init__(parent=buildMain) self.mainGui = buildMain.mainGui @@ -747,7 +747,7 @@ class _HeadingsTab(QWidget): return - def loadContent(self): + def loadContent(self) -> None: """Populate the widgets.""" self.fmtTitle.setText(self._build.getStr("headings.fmtTitle")) self.fmtChapter.setText(self._build.getStr("headings.fmtChapter")) @@ -758,7 +758,7 @@ class _HeadingsTab(QWidget): self.swtSection.setChecked(self._build.getBool("headings.hideSection")) return - def saveContent(self): + def saveContent(self) -> None: """Save choices back into build object.""" self._build.setValue("headings.hideScene", self.swtScene.isChecked()) self._build.setValue("headings.hideSection", self.swtSection.isChecked()) @@ -768,7 +768,7 @@ class _HeadingsTab(QWidget): # Internal Functions ## - def _insertIntoForm(self, text: str): + def _insertIntoForm(self, text: str) -> None: """Insert formatting text from the dropdown menu.""" if self._editing > 0: cursor = self.editTextBox.textCursor() @@ -776,7 +776,7 @@ class _HeadingsTab(QWidget): self.editTextBox.setFocus() return - def _editHeading(self, heading: int): + def _editHeading(self, heading: int) -> None: """Populate the form with a specific heading format.""" self._editing = heading self.editTextBox.setEnabled(True) @@ -810,7 +810,7 @@ class _HeadingsTab(QWidget): # Private Slots ## - def _saveFormat(self): + def _saveFormat(self) -> None: """Save the format from the edit text box.""" heading = self._editing text = self.editTextBox.toPlainText().strip().replace("\n", "//") @@ -842,7 +842,7 @@ class _HeadingsTab(QWidget): class _HeadingSyntaxHighlighter(QSyntaxHighlighter): - def __init__(self, document: QTextDocument, mainTheme: GuiTheme): + def __init__(self, document: QTextDocument, mainTheme: GuiTheme) -> None: super().__init__(document) self._fmtSymbol = QTextCharFormat() self._fmtSymbol.setForeground(QColor(*mainTheme.colHead)) @@ -850,7 +850,7 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter): self._fmtFormat.setForeground(QColor(*mainTheme.colEmph)) return - def highlightBlock(self, text: str): + def highlightBlock(self, text: str) -> None: """Add syntax highlighting to the text block.""" for heading in nwHeadFmt.ALL: pos = text.find(heading) @@ -868,7 +868,7 @@ class _HeadingSyntaxHighlighter(QSyntaxHighlighter): class _ContentTab(QWidget): - def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings): + def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: super().__init__(parent=buildMain) self.mainGui = buildMain.mainGui @@ -917,7 +917,7 @@ class _ContentTab(QWidget): return - def loadContent(self): + def loadContent(self) -> None: """Populate the widgets.""" self.incSynopsis.setChecked(self._build.getBool("text.includeSynopsis")) self.incComments.setChecked(self._build.getBool("text.includeComments")) @@ -926,7 +926,7 @@ class _ContentTab(QWidget): self.addNoteHead.setChecked(self._build.getBool("text.addNoteHeadings")) return - def saveContent(self): + def saveContent(self) -> None: """Save choices back into build object.""" self._build.setValue("text.includeSynopsis", self.incSynopsis.isChecked()) self._build.setValue("text.includeComments", self.incComments.isChecked()) @@ -940,7 +940,7 @@ class _ContentTab(QWidget): class _FormatTab(QWidget): - def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings): + def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: super().__init__(parent=buildMain) self.buildMain = buildMain @@ -1085,7 +1085,7 @@ class _FormatTab(QWidget): return - def loadContent(self): + def loadContent(self) -> None: """Populate the widgets.""" langIdx = self.buildLang.findData(self._build.getStr("format.buildLang")) if langIdx != -1: @@ -1128,7 +1128,7 @@ class _FormatTab(QWidget): return - def saveContent(self): + def saveContent(self) -> None: """Save choices back into build object.""" self._build.setValue("format.buildLang", str(self.buildLang.currentData())) self._build.setValue("format.textFont", self.textFont.text()) @@ -1154,7 +1154,7 @@ class _FormatTab(QWidget): ## @pyqtSlot() - def _selectFont(self): + def _selectFont(self) -> None: """Open the QFontDialog and set a font for the font style.""" currFont = QFont() currFont.setFamily(self.textFont.text()) @@ -1166,7 +1166,7 @@ class _FormatTab(QWidget): return @pyqtSlot(int) - def _changeUnit(self, index: int): + def _changeUnit(self, index: int) -> None: """The current unit change, so recalculate sizes.""" newUnit = self.pageUnit.itemData(index) newScale = nwLabels.UNIT_SCALE.get(newUnit, 1.0) @@ -1220,7 +1220,7 @@ class _FormatTab(QWidget): return @pyqtSlot(int) - def _changePageSize(self, index: int): + def _changePageSize(self, index: int) -> None: """The page size has changed.""" self.pageWidth.setEnabled(True) self.pageHeight.setEnabled(True) @@ -1239,7 +1239,7 @@ class _FormatTab(QWidget): class _OutputTab(QWidget): - def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings): + def __init__(self, buildMain: GuiBuildSettings, build: BuildSettings) -> None: super().__init__(parent=buildMain) self.mainGui = buildMain.mainGui @@ -1282,13 +1282,13 @@ class _OutputTab(QWidget): return - def loadContent(self): + def loadContent(self) -> None: """Populate the widgets.""" self.odtAddColours.setChecked(self._build.getBool("odt.addColours")) self.htmlAddStyles.setChecked(self._build.getBool("html.addStyles")) return - def saveContent(self): + def saveContent(self) -> None: """Save choices back into build object.""" self._build.setValue("odt.addColours", self.odtAddColours.isChecked()) self._build.setValue("html.addStyles", self.htmlAddStyles.isChecked()) From 6168b45c75e6457fe54b07373b6a8305182fa49c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Aug 2023 16:36:34 +0200 Subject: [PATCH 03/12] Add a triangle arrow to the menu items on the paged side bar --- novelwriter/extensions/pagedsidebar.py | 32 ++++++++++++++++++-------- 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/novelwriter/extensions/pagedsidebar.py b/novelwriter/extensions/pagedsidebar.py index b2bcd44f..6da97974 100644 --- a/novelwriter/extensions/pagedsidebar.py +++ b/novelwriter/extensions/pagedsidebar.py @@ -25,8 +25,8 @@ along with this program. If not, see . """ from __future__ import annotations -from PyQt5.QtGui import QColor, QPaintEvent, QPainter -from PyQt5.QtCore import QRectF, Qt, pyqtSignal, pyqtSlot +from PyQt5.QtGui import QColor, QPaintEvent, QPainter, QPolygon +from PyQt5.QtCore import QPoint, QRectF, Qt, pyqtSignal, pyqtSlot from PyQt5.QtWidgets import ( QAbstractButton, QAction, QButtonGroup, QLabel, QSizePolicy, QStyle, QStyleOptionToolButton, QToolBar, QToolButton, QWidget @@ -121,7 +121,7 @@ class NPagedSideBar(QToolBar): class _NPagedToolButton(QToolButton): - __slots__ = ("_bH", "_tM", "_lM", "_cR") + __slots__ = ("_bH", "_tM", "_lM", "_cR", "_aH") def __init__(self, parent: QWidget) -> None: super().__init__(parent=parent) @@ -131,9 +131,10 @@ class _NPagedToolButton(QToolButton): fH = self.fontMetrics().height() self._bH = round(fH * 1.7) - self._tM = (self._bH - fH) // 2 - self._lM = self.style().pixelMetric(QStyle.PM_ButtonMargin) - self._cR = self._lM // 2 + self._tM = (self._bH - fH)//2 + self._lM = 3*self.style().pixelMetric(QStyle.PM_ButtonMargin)//2 + self._cR = self._lM//2 + self._aH = 2*fH//7 self.setFixedHeight(self._bH) return @@ -149,6 +150,7 @@ class _NPagedToolButton(QToolButton): paint = QPainter(self) paint.setRenderHint(QPainter.Antialiasing, True) paint.setPen(Qt.NoPen) + paint.setBrush(Qt.NoBrush) width = self.width() height = self.height() @@ -169,12 +171,22 @@ class _NPagedToolButton(QToolButton): else: textCol = palette.text().color() - tW = width - 3*self._lM + tW = width - 2*self._lM tH = height - 2*self._tM paint.setPen(textCol) paint.setOpacity(1.0) - paint.drawText(QRectF(2*self._lM, self._tM, tW, tH), Qt.AlignLeft, self.text()) + paint.drawText(QRectF(self._lM, self._tM, tW, tH), Qt.AlignLeft, self.text()) + + tC = self.height()//2 + tW = self.width() - self._aH - self._lM + if self.isChecked(): + paint.setBrush(textCol) + paint.drawPolygon(QPolygon([ + QPoint(tW, tC - self._aH), + QPoint(tW + self._aH, tC), + QPoint(tW, tC + self._aH), + ])) return @@ -192,8 +204,8 @@ class _NPagedToolLabel(QLabel): fH = self.fontMetrics().height() self._bH = round(fH * 1.7) - self._tM = (self._bH - fH) // 2 - self._lM = self.style().pixelMetric(QStyle.PM_ButtonMargin) + self._tM = (self._bH - fH)//2 + self._lM = self.style().pixelMetric(QStyle.PM_ButtonMargin)//2 self.setFixedHeight(self._bH) self._textCol = textColor or self.palette().text().color() From 18d0265515fc6557fff3c8c6259b57a940943f55 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Aug 2023 17:25:29 +0200 Subject: [PATCH 04/12] Fix possibly unbound variable in project tree --- novelwriter/gui/projtree.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py index ab29d4aa..d77797f8 100644 --- a/novelwriter/gui/projtree.py +++ b/novelwriter/gui/projtree.py @@ -1202,6 +1202,7 @@ class GuiProjectTree(QTreeWidget): open a context menu in-place. """ tItem = None + tHandle = None hasChild = False selItem = self.itemAt(clickPos) if isinstance(selItem, QTreeWidgetItem): @@ -1209,7 +1210,7 @@ class GuiProjectTree(QTreeWidget): tItem = self.theProject.tree[tHandle] hasChild = selItem.childCount() > 0 - if tItem is None: + if tItem is None or tHandle is None: logger.debug("No item found") return False From 2780e722d0ac5b3c04df7737afa914b18b710c4a Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Aug 2023 17:28:05 +0200 Subject: [PATCH 05/12] Change icon for filtered items in build settings dialog --- .../assets/icons/typicons_dark/icons.conf | 2 +- .../icons/typicons_dark/typ_arrow-forward.svg | 4 ++ .../assets/icons/typicons_dark/typ_filter.svg | 4 -- .../assets/icons/typicons_light/icons.conf | 2 +- .../typicons_light/typ_arrow-forward.svg | 4 ++ .../icons/typicons_light/typ_filter.svg | 4 -- novelwriter/tools/manussettings.py | 46 +++++++++++-------- tests/test_tools/test_tools_manussettings.py | 2 +- 8 files changed, 37 insertions(+), 31 deletions(-) create mode 100644 novelwriter/assets/icons/typicons_dark/typ_arrow-forward.svg delete mode 100644 novelwriter/assets/icons/typicons_dark/typ_filter.svg create mode 100644 novelwriter/assets/icons/typicons_light/typ_arrow-forward.svg delete mode 100644 novelwriter/assets/icons/typicons_light/typ_filter.svg diff --git a/novelwriter/assets/icons/typicons_dark/icons.conf b/novelwriter/assets/icons/typicons_dark/icons.conf index 2e62c460..8e354654 100644 --- a/novelwriter/assets/icons/typicons_dark/icons.conf +++ b/novelwriter/assets/icons/typicons_dark/icons.conf @@ -21,7 +21,7 @@ backward = typ_chevron-left.svg bookmark = typ_bookmark.svg browse = typ_folder-open.svg build_excluded = typ_cancel.svg -build_filtered = typ_filter.svg +build_filtered = typ_arrow-forward.svg build_included = typ_pin.svg bullet-off = typ_media-record-outline.svg bullet-on = typ_media-record.svg diff --git a/novelwriter/assets/icons/typicons_dark/typ_arrow-forward.svg b/novelwriter/assets/icons/typicons_dark/typ_arrow-forward.svg new file mode 100644 index 00000000..e235e9c2 --- /dev/null +++ b/novelwriter/assets/icons/typicons_dark/typ_arrow-forward.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_dark/typ_filter.svg b/novelwriter/assets/icons/typicons_dark/typ_filter.svg deleted file mode 100644 index fd3f3afc..00000000 --- a/novelwriter/assets/icons/typicons_dark/typ_filter.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/novelwriter/assets/icons/typicons_light/icons.conf b/novelwriter/assets/icons/typicons_light/icons.conf index df1d40ee..a52469ea 100644 --- a/novelwriter/assets/icons/typicons_light/icons.conf +++ b/novelwriter/assets/icons/typicons_light/icons.conf @@ -21,7 +21,7 @@ backward = typ_chevron-left.svg bookmark = typ_bookmark.svg browse = typ_folder-open.svg build_excluded = typ_cancel.svg -build_filtered = typ_filter.svg +build_filtered = typ_arrow-forward.svg build_included = typ_pin.svg bullet-off = typ_media-record-outline.svg bullet-on = typ_media-record.svg diff --git a/novelwriter/assets/icons/typicons_light/typ_arrow-forward.svg b/novelwriter/assets/icons/typicons_light/typ_arrow-forward.svg new file mode 100644 index 00000000..9d488e06 --- /dev/null +++ b/novelwriter/assets/icons/typicons_light/typ_arrow-forward.svg @@ -0,0 +1,4 @@ + + + + diff --git a/novelwriter/assets/icons/typicons_light/typ_filter.svg b/novelwriter/assets/icons/typicons_light/typ_filter.svg deleted file mode 100644 index 40000608..00000000 --- a/novelwriter/assets/icons/typicons_light/typ_filter.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 33ce7691..5cb62d9e 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -106,7 +106,7 @@ class GuiBuildSettings(QDialog): self.optSideBar.setLabelColor(self.mainTheme.helpText) self.optSideBar.addLabel(self.tr("Options")) - self.optSideBar.addButton(self.tr("Filters"), self.OPT_FILTERS) + self.optSideBar.addButton(self.tr("Selection"), self.OPT_FILTERS) self.optSideBar.addButton(self.tr("Headings"), self.OPT_HEADINGS) self.optSideBar.addButton(self.tr("Content"), self.OPT_CONTENT) self.optSideBar.addButton(self.tr("Format"), self.OPT_FORMAT) @@ -301,16 +301,19 @@ class _FilterTab(QWidget): self.mainTheme = buildMain.mainGui.mainTheme self.theProject = buildMain.mainGui.theProject - self._treeMap = {} + self._treeMap: dict[str, QTreeWidgetItem] = {} self._build = build - self._statusFlags = { - self.F_NONE: ("", QIcon()), - self.F_FILTERED: (self.tr("Filtered"), self.mainTheme.getIcon("build_filtered")), - self.F_INCLUDED: (self.tr("Included"), self.mainTheme.getIcon("build_included")), - self.F_EXCLUDED: (self.tr("Excluded"), self.mainTheme.getIcon("build_excluded")), + self._statusFlags: dict[int, QIcon] = { + self.F_NONE: QIcon(), + self.F_FILTERED: self.mainTheme.getIcon("build_filtered"), + self.F_INCLUDED: self.mainTheme.getIcon("build_included"), + self.F_EXCLUDED: self.mainTheme.getIcon("build_excluded"), } + self._trIncluded = self.tr("Included in manuscript") + self._trExcluded = self.tr("Excluded from manuscript") + # Project Tree # ============ @@ -341,25 +344,25 @@ class _FilterTab(QWidget): # Filters # ======= - self.filteredButton = QToolButton(self) - self.filteredButton.setToolTip(self._statusFlags[self.F_FILTERED][0]) - self.filteredButton.setIcon(self._statusFlags[self.F_FILTERED][1]) - self.filteredButton.clicked.connect(lambda: self._setSelectedMode(self.F_FILTERED)) + self.resetButton = QToolButton(self) + self.resetButton.setToolTip(self.tr("Reset to default")) + self.resetButton.setIcon(self.mainTheme.getIcon("revert")) + self.resetButton.clicked.connect(lambda: self._setSelectedMode(self.F_FILTERED)) self.includedButton = QToolButton(self) - self.includedButton.setToolTip(self._statusFlags[self.F_INCLUDED][0]) - self.includedButton.setIcon(self._statusFlags[self.F_INCLUDED][1]) + self.includedButton.setToolTip(self.tr("Always included")) + self.includedButton.setIcon(self._statusFlags[self.F_INCLUDED]) self.includedButton.clicked.connect(lambda: self._setSelectedMode(self.F_INCLUDED)) self.excludedButton = QToolButton(self) - self.excludedButton.setToolTip(self._statusFlags[self.F_EXCLUDED][0]) - self.excludedButton.setIcon(self._statusFlags[self.F_EXCLUDED][1]) + self.excludedButton.setToolTip(self.tr("Always excluded")) + self.excludedButton.setIcon(self._statusFlags[self.F_EXCLUDED]) self.excludedButton.clicked.connect(lambda: self._setSelectedMode(self.F_EXCLUDED)) self.modeBox = QHBoxLayout() self.modeBox.addWidget(QLabel(self.tr("Mark selection as"))) self.modeBox.addStretch(1) - self.modeBox.addWidget(self.filteredButton) + self.modeBox.addWidget(self.resetButton) self.modeBox.addWidget(self.includedButton) self.modeBox.addWidget(self.excludedButton) @@ -549,13 +552,16 @@ class _FilterTab(QWidget): for tHandle, item in self._treeMap.items(): allow, mode = filtered.get(tHandle, (False, FilterMode.UNKNOWN)) 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]) + item.setToolTip(self.C_STATUS, self._trIncluded) 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]) + item.setToolTip(self.C_STATUS, self._trExcluded) 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]) + item.setToolTip(self.C_STATUS, self._trIncluded) else: - item.setIcon(self.C_STATUS, self._statusFlags[self.F_NONE][1]) + item.setIcon(self.C_STATUS, self._statusFlags[self.F_NONE]) return # END Class _FilterTab diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py index c455b623..e4443e4b 100644 --- a/tests/test_tools/test_tools_manussettings.py +++ b/tests/test_tools/test_tools_manussettings.py @@ -266,7 +266,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR filterTab._treeMap[C.hSceneDoc].setSelected(True) filterTab._treeMap[hPlotDoc].setSelected(True) filterTab._treeMap[hCharDoc].setSelected(True) - filterTab.filteredButton.click() + filterTab.resetButton.click() assert build.buildItemFilter(nwGUI.theProject) == { C.hNovelRoot: (False, FilterMode.SKIPPED), C.hTitlePage: (True, FilterMode.FILTERED), From 72de32b1263dae89703c67f3cb7fd0b8d5fe5bcb Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Aug 2023 17:49:11 +0200 Subject: [PATCH 06/12] Add iterative filter action for single selection in build settings --- novelwriter/tools/manussettings.py | 16 ++++++++- tests/test_tools/test_tools_manussettings.py | 35 +++++++++++++++----- 2 files changed, 41 insertions(+), 10 deletions(-) diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 5cb62d9e..b072b14e 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -530,7 +530,11 @@ class _FilterTab(QWidget): def _setSelectedMode(self, mode: int) -> None: """Set the mode for the selected items.""" - for item in self.optTree.selectedItems(): + items = self.optTree.selectedItems() + if len(items) == 1 and isinstance(items[0], QTreeWidgetItem): + items = self._scanChildren(items[0], []) + + for item in items: if isinstance(item, QTreeWidgetItem): tHandle = item.data(self.C_DATA, self.D_HANDLE) isFile = item.data(self.C_DATA, self.D_FILE) @@ -564,6 +568,16 @@ class _FilterTab(QWidget): item.setIcon(self.C_STATUS, self._statusFlags[self.F_NONE]) return + def _scanChildren(self, item: QTreeWidgetItem | None, items: list) -> list[QTreeWidgetItem]: + """This is a recursive function returning all items in a tree + starting at a given QTreeWidgetItem. + """ + if isinstance(item, QTreeWidgetItem): + items.append(item) + for i in range(item.childCount()): + self._scanChildren(item.child(i), items) + return items + # END Class _FilterTab diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py index e4443e4b..1286dfd1 100644 --- a/tests/test_tools/test_tools_manussettings.py +++ b/tests/test_tools/test_tools_manussettings.py @@ -132,7 +132,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR hCharDoc = nwGUI.theProject.newFile("Jane Doe", C.hCharRoot) nwGUI.projView.projTree.revealNewTreeItem(hPlotDoc) nwGUI.projView.projTree.revealNewTreeItem(hCharDoc) - nwGUI.theProject.tree[hPlotDoc].setActive(False) + nwGUI.theProject.tree[hPlotDoc].setActive(False) # type: ignore # Create the dialog and populate it bSettings = GuiBuildSettings(nwGUI, nwGUI, build) @@ -147,7 +147,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR assert filterTab.optTree.topLevelItemCount() == 4 # The 4 root folders assert filterTab.filterOpt._index == 10 # 2 headers, 1 sep, 3 opt and 4 roots - # Untoggle note folders + # Un-toggle note folders filterTab.filterOpt._widgets[switchMap["worldRoot"]].setChecked(False) # World Root assert filterTab.optTree.topLevelItemCount() == 3 assert C.hWorldRoot in build._skipRoot @@ -229,8 +229,8 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR # Set char and plot docs to excluded filterTab.optTree.clearSelection() - filterTab._treeMap[hPlotDoc].setSelected(True) - filterTab._treeMap[hCharDoc].setSelected(True) + filterTab._treeMap[hPlotDoc].setSelected(True) # type: ignore + filterTab._treeMap[hCharDoc].setSelected(True) # type: ignore filterTab.excludedButton.click() assert build.buildItemFilter(nwGUI.theProject) == { C.hNovelRoot: (False, FilterMode.SKIPPED), @@ -260,12 +260,29 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR C.hWorldRoot: (False, FilterMode.SKIPPED), } + # Selecting only novel root should iterate through all children + filterTab.optTree.clearSelection() + filterTab._treeMap[C.hNovelRoot].setSelected(True) + filterTab.resetButton.click() + assert build.buildItemFilter(nwGUI.theProject) == { + C.hNovelRoot: (False, FilterMode.SKIPPED), + C.hTitlePage: (True, FilterMode.FILTERED), + C.hChapterDir: (False, FilterMode.SKIPPED), + C.hChapterDoc: (True, FilterMode.FILTERED), + C.hSceneDoc: (True, FilterMode.FILTERED), + C.hPlotRoot: (False, FilterMode.SKIPPED), + hPlotDoc: (False, FilterMode.EXCLUDED), + C.hCharRoot: (False, FilterMode.SKIPPED), + hCharDoc: (False, FilterMode.EXCLUDED), + C.hWorldRoot: (False, FilterMode.SKIPPED), + } + # Set everything back to filtered filterTab.optTree.clearSelection() - filterTab._treeMap[C.hChapterDoc].setSelected(True) + filterTab._treeMap[C.hNovelRoot].setSelected(True) filterTab._treeMap[C.hSceneDoc].setSelected(True) - filterTab._treeMap[hPlotDoc].setSelected(True) - filterTab._treeMap[hCharDoc].setSelected(True) + filterTab._treeMap[hPlotDoc].setSelected(True) # type: ignore + filterTab._treeMap[hCharDoc].setSelected(True) # type: ignore filterTab.resetButton.click() assert build.buildItemFilter(nwGUI.theProject) == { C.hNovelRoot: (False, FilterMode.SKIPPED), @@ -285,8 +302,8 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, C.hPlotRoot, hPlotDoc, C.hCharRoot, hCharDoc, ] - nwGUI.theProject.tree[hCharDoc].setRoot(None) # Char doc has no root handle - nwGUI.theProject.tree[hPlotDoc].setParent(None) # Plot doc has no parent handle + nwGUI.theProject.tree[hCharDoc].setRoot(None) # type: ignore + nwGUI.theProject.tree[hPlotDoc].setParent(None) # type: ignore filterTab._populateTree() assert list(filterTab._treeMap.keys()) == [ C.hNovelRoot, C.hTitlePage, C.hChapterDir, C.hChapterDoc, C.hSceneDoc, From 3f951fa61fcfc7b6fc70e05a9f0c5d09c7a7ac78 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Aug 2023 18:20:19 +0200 Subject: [PATCH 07/12] Move reset button to after included and excluded --- novelwriter/tools/manussettings.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index b072b14e..43107ff8 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -344,11 +344,6 @@ class _FilterTab(QWidget): # Filters # ======= - self.resetButton = QToolButton(self) - self.resetButton.setToolTip(self.tr("Reset to default")) - self.resetButton.setIcon(self.mainTheme.getIcon("revert")) - self.resetButton.clicked.connect(lambda: self._setSelectedMode(self.F_FILTERED)) - self.includedButton = QToolButton(self) self.includedButton.setToolTip(self.tr("Always included")) self.includedButton.setIcon(self._statusFlags[self.F_INCLUDED]) @@ -359,12 +354,18 @@ class _FilterTab(QWidget): self.excludedButton.setIcon(self._statusFlags[self.F_EXCLUDED]) self.excludedButton.clicked.connect(lambda: self._setSelectedMode(self.F_EXCLUDED)) + self.resetButton = QToolButton(self) + self.resetButton.setToolTip(self.tr("Reset to default")) + self.resetButton.setIcon(self.mainTheme.getIcon("revert")) + self.resetButton.clicked.connect(lambda: self._setSelectedMode(self.F_FILTERED)) + self.modeBox = QHBoxLayout() self.modeBox.addWidget(QLabel(self.tr("Mark selection as"))) self.modeBox.addStretch(1) - self.modeBox.addWidget(self.resetButton) self.modeBox.addWidget(self.includedButton) self.modeBox.addWidget(self.excludedButton) + self.modeBox.addWidget(self.resetButton) + self.modeBox.setSpacing(CONFIG.pxInt(4)) # Filer Options self.filterOpt = NSwitchBox(self, iPx) From 5bc391c6c25f4d14d5f6e0a47e9859673c4e4006 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Aug 2023 19:51:10 +0200 Subject: [PATCH 08/12] Add file extensions to Outpu tab headers --- novelwriter/core/buildsettings.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/novelwriter/core/buildsettings.py b/novelwriter/core/buildsettings.py index c0814f87..ff869351 100644 --- a/novelwriter/core/buildsettings.py +++ b/novelwriter/core/buildsettings.py @@ -125,10 +125,10 @@ SETTINGS_LABELS = { "format.leftMargin": QT_TRANSLATE_NOOP("Builds", "Left Margin"), "format.rightMargin": QT_TRANSLATE_NOOP("Builds", "Right Margin"), - "odt": QT_TRANSLATE_NOOP("Builds", "Open Document"), + "odt": QT_TRANSLATE_NOOP("Builds", "Open Document (.odt)"), "odt.addColours": QT_TRANSLATE_NOOP("Builds", "Add Highlight Colours"), - "html": QT_TRANSLATE_NOOP("Builds", "HTML"), + "html": QT_TRANSLATE_NOOP("Builds", "HTML (.html)"), "html.addStyles": QT_TRANSLATE_NOOP("Builds", "Add CSS Styles"), } From effe7abdec937d709021da1c6f468fea7242036e Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Aug 2023 21:49:55 +0200 Subject: [PATCH 09/12] Fix formatting of line breaks in headers --- novelwriter/constants.py | 1 + novelwriter/core/tohtml.py | 14 +++++++------- novelwriter/core/tomd.py | 14 +++++++------- novelwriter/core/toodt.py | 14 +++++++------- novelwriter/tools/manussettings.py | 4 ++-- tests/test_tools/test_tools_manussettings.py | 4 +++- 6 files changed, 27 insertions(+), 24 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index 05242c36..5a306a73 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -257,6 +257,7 @@ class nwLabels: class nwHeadFmt: + BR = "{BR}" TITLE = "{Title}" CH_NUM = "{Chapter}" CH_WORD = "{Chapter:Word}" diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py index 6c0fa4aa..20c01701 100644 --- a/novelwriter/core/tohtml.py +++ b/novelwriter/core/tohtml.py @@ -31,7 +31,7 @@ from pathlib import Path from novelwriter import CONFIG from novelwriter.common import formatTimeStamp -from novelwriter.constants import nwKeyWords, nwLabels, nwHtmlUnicode +from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwHtmlUnicode from novelwriter.core.project import NWProject from novelwriter.core.tokenizer import Tokenizer, stripEscape @@ -244,27 +244,27 @@ class ToHtml(Tokenizer): parStyle = None elif tType == self.T_TITLE: - tHead = tText.replace(r"\\", "
") + tHead = tText.replace(nwHeadFmt.BR, "
") tmpResult.append(f"

{aNm}{tHead}

\n") elif tType == self.T_UNNUM: - tHead = tText.replace(r"\\", "
") + tHead = tText.replace(nwHeadFmt.BR, "
") tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}\n") elif tType == self.T_HEAD1: - tHead = tText.replace(r"\\", "
") + tHead = tText.replace(nwHeadFmt.BR, "
") tmpResult.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}\n") elif tType == self.T_HEAD2: - tHead = tText.replace(r"\\", "
") + tHead = tText.replace(nwHeadFmt.BR, "
") tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}\n") elif tType == self.T_HEAD3: - tHead = tText.replace(r"\\", "
") + tHead = tText.replace(nwHeadFmt.BR, "
") tmpResult.append(f"<{h3}{hStyle}>{aNm}{tHead}\n") elif tType == self.T_HEAD4: - tHead = tText.replace(r"\\", "
") + tHead = tText.replace(nwHeadFmt.BR, "
") tmpResult.append(f"<{h4}{hStyle}>{aNm}{tHead}\n") elif tType == self.T_SEP: diff --git a/novelwriter/core/tomd.py b/novelwriter/core/tomd.py index 1702bc6d..e634de74 100644 --- a/novelwriter/core/tomd.py +++ b/novelwriter/core/tomd.py @@ -27,7 +27,7 @@ import logging from pathlib import Path -from novelwriter.constants import nwLabels +from novelwriter.constants import nwHeadFmt, nwLabels from novelwriter.core.project import NWProject from novelwriter.core.tokenizer import Tokenizer @@ -122,27 +122,27 @@ class ToMarkdown(Tokenizer): thisPar = [] elif tType == self.T_TITLE: - tHead = tText.replace(r"\\", "\n") + tHead = tText.replace(nwHeadFmt.BR, "\n") tmpResult.append(f"# {tHead}\n\n") elif tType == self.T_UNNUM: - tHead = tText.replace(r"\\", "\n") + tHead = tText.replace(nwHeadFmt.BR, "\n") tmpResult.append(f"## {tHead}\n\n") elif tType == self.T_HEAD1: - tHead = tText.replace(r"\\", "\n") + tHead = tText.replace(nwHeadFmt.BR, "\n") tmpResult.append(f"# {tHead}\n\n") elif tType == self.T_HEAD2: - tHead = tText.replace(r"\\", "\n") + tHead = tText.replace(nwHeadFmt.BR, "\n") tmpResult.append(f"## {tHead}\n\n") elif tType == self.T_HEAD3: - tHead = tText.replace(r"\\", "\n") + tHead = tText.replace(nwHeadFmt.BR, "\n") tmpResult.append(f"### {tHead}\n\n") elif tType == self.T_HEAD4: - tHead = tText.replace(r"\\", "\n") + tHead = tText.replace(nwHeadFmt.BR, "\n") tmpResult.append(f"#### {tHead}\n\n") elif tType == self.T_SEP: diff --git a/novelwriter/core/toodt.py b/novelwriter/core/toodt.py index 87392e52..93b8afac 100644 --- a/novelwriter/core/toodt.py +++ b/novelwriter/core/toodt.py @@ -36,7 +36,7 @@ from datetime import datetime from novelwriter import __version__ from novelwriter.common import xmlIndent -from novelwriter.constants import nwKeyWords, nwLabels +from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels from novelwriter.core.project import NWProject from novelwriter.core.tokenizer import Tokenizer, stripEscape @@ -445,27 +445,27 @@ class ToOdt(Tokenizer): parStyle = None elif tType == self.T_TITLE: - tHead = tText.replace(r"\\", "\n") + tHead = tText.replace(nwHeadFmt.BR, "\n") self._addTextPar("Title", oStyle, tHead, isHead=False) # Title must be text:p elif tType == self.T_UNNUM: - tHead = tText.replace(r"\\", "\n") + tHead = tText.replace(nwHeadFmt.BR, "\n") self._addTextPar("Heading_20_2", oStyle, tHead, isHead=True, oLevel="2") elif tType == self.T_HEAD1: - tHead = tText.replace(r"\\", "\n") + tHead = tText.replace(nwHeadFmt.BR, "\n") self._addTextPar("Heading_20_1", oStyle, tHead, isHead=True, oLevel="1") elif tType == self.T_HEAD2: - tHead = tText.replace(r"\\", "\n") + tHead = tText.replace(nwHeadFmt.BR, "\n") self._addTextPar("Heading_20_2", oStyle, tHead, isHead=True, oLevel="2") elif tType == self.T_HEAD3: - tHead = tText.replace(r"\\", "\n") + tHead = tText.replace(nwHeadFmt.BR, "\n") self._addTextPar("Heading_20_3", oStyle, tHead, isHead=True, oLevel="3") elif tType == self.T_HEAD4: - tHead = tText.replace(r"\\", "\n") + tHead = tText.replace(nwHeadFmt.BR, "\n") self._addTextPar("Heading_20_4", oStyle, tHead, isHead=True, oLevel="4") elif tType == self.T_SEP: diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 43107ff8..fe65f923 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -822,7 +822,7 @@ class _HeadingsTab(QWidget): text = "" label = self.tr("None") - self.editTextBox.setPlainText(text.replace("//", "\n")) + self.editTextBox.setPlainText(text.replace(nwHeadFmt.BR, "\n")) self.lblEditForm.setText(self.tr("Editing: {0}").format(label)) return @@ -834,7 +834,7 @@ class _HeadingsTab(QWidget): def _saveFormat(self) -> None: """Save the format from the edit text box.""" heading = self._editing - text = self.editTextBox.toPlainText().strip().replace("\n", "//") + text = self.editTextBox.toPlainText().strip().replace("\n", nwHeadFmt.BR) if heading == self.EDIT_TITLE: self.fmtTitle.setText(text) self._build.setValue("headings.fmtTitle", text) diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py index 1286dfd1..36cf6d79 100644 --- a/tests/test_tools/test_tools_manussettings.py +++ b/tests/test_tools/test_tools_manussettings.py @@ -429,7 +429,9 @@ def testBuildSettings_Headings(qtbot: QtBot, nwGUI: GuiMain): headTab.btnChapter.click() headTab.editTextBox.setPlainText(f"Chapter {nwHeadFmt.CH_NUM}\n{nwHeadFmt.TITLE}\n") headTab.btnApply.click() - assert build.getStr("headings.fmtChapter") == f"Chapter {nwHeadFmt.CH_NUM}//{nwHeadFmt.TITLE}" + assert build.getStr("headings.fmtChapter") == ( + f"Chapter {nwHeadFmt.CH_NUM}{nwHeadFmt.BR}{nwHeadFmt.TITLE}" + ) # Set all to plain title headTab.btnTitle.click() From 61836c8c042fb5d27dd4b2f1a51b0cc36f16a93f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Aug 2023 22:04:12 +0200 Subject: [PATCH 10/12] Change page size preset to Custom when values are changed instead of blocking the spin boxes --- novelwriter/tools/manussettings.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index fe65f923..0e848fd1 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -1054,10 +1054,12 @@ class _FormatTab(QWidget): self.pageWidth = QDoubleSpinBox(self) self.pageWidth.setFixedWidth(dbW) self.pageWidth.setMaximum(500.0) + self.pageWidth.valueChanged.connect(self._pageSizeValueChanged) self.pageHeight = QDoubleSpinBox(self) self.pageHeight.setFixedWidth(dbW) self.pageHeight.setMaximum(500.0) + self.pageHeight.valueChanged.connect(self._pageSizeValueChanged) self.topMargin = QDoubleSpinBox(self) self.topMargin.setFixedWidth(dbW) @@ -1206,15 +1208,19 @@ class _FormatTab(QWidget): pMax = 500.0 if isMM else 50.0 mMax = 150.0 if isMM else 15.0 + self.pageWidth.blockSignals(True) self.pageWidth.setDecimals(nDec) self.pageWidth.setSingleStep(nStep) self.pageWidth.setMaximum(pMax) self.pageWidth.setValue(pageWidth) + self.pageWidth.blockSignals(False) + self.pageHeight.blockSignals(True) self.pageHeight.setDecimals(nDec) self.pageHeight.setSingleStep(nStep) self.pageHeight.setMaximum(pMax) self.pageHeight.setValue(pageHeight) + self.pageHeight.blockSignals(False) self.topMargin.setDecimals(nDec) self.topMargin.setSingleStep(nStep) @@ -1237,22 +1243,31 @@ class _FormatTab(QWidget): self.rightMargin.setValue(rightMargin) self._unitScale = newScale + self._changePageSize(self.pageSize.currentIndex()) return @pyqtSlot(int) def _changePageSize(self, index: int) -> None: """The page size has changed.""" - self.pageWidth.setEnabled(True) - self.pageHeight.setEnabled(True) - w, h = nwLabels.PAPER_SIZE[self.pageSize.itemData(index)] if index >= 0 else (-1.0, -1.0) if w > 0.0 and h > 0.0: - self.pageWidth.setEnabled(False) - self.pageHeight.setEnabled(False) + self.pageWidth.blockSignals(True) self.pageWidth.setValue(w/self._unitScale) + self.pageWidth.blockSignals(False) + self.pageHeight.blockSignals(True) self.pageHeight.setValue(h/self._unitScale) + self.pageHeight.blockSignals(False) + return + @pyqtSlot() + def _pageSizeValueChanged(self): + """The user has changed the page size spin boxes, so we flip + the page size box to Custom. + """ + index = self.pageSize.findData("Custom") + if index >= 0: + self.pageSize.setCurrentIndex(index) return # END Class _FormatTab From 4951e4f8fedbb18b14bcc5ef469b7f819b510e77 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Aug 2023 22:27:15 +0200 Subject: [PATCH 11/12] Automatically select the first build if no builds are selected in the build tool --- novelwriter/tools/manuscript.py | 27 ++++++++++++----------- tests/test_tools/test_tools_manuscript.py | 9 ++------ 2 files changed, 16 insertions(+), 20 deletions(-) diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index e5ffa5b0..1112622f 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -330,15 +330,13 @@ class GuiManuscript(QDialog): def _buildManuscript(self): """Open the build dialog and build the manuscript.""" build = self._getSelectedBuild() - if build is None: - return + if isinstance(build, BuildSettings): + dlgBuild = GuiManuscriptBuild(self, self.mainGui, build) + dlgBuild.exec_() - dlgBuild = GuiManuscriptBuild(self, self.mainGui, build) - dlgBuild.exec_() - - # After the build is done, save build settings changes - if build.changed: - self._builds.setBuild(build) + # After the build is done, save build settings changes + if build.changed: + self._builds.setBuild(build) return @@ -376,10 +374,13 @@ class GuiManuscript(QDialog): return def _getSelectedBuild(self) -> BuildSettings | None: - """Get the currently selected build.""" - bItems = self.buildList.selectedItems() - if bItems: - build = self._builds.getBuild(bItems[0].data(self.D_KEY)) + """Get the currently selected build. If none are selected, + automatically select the first one. + """ + items = self.buildList.selectedItems() + item = items[0] if items else self.buildList.item(0) + if item: + build = self._builds.getBuild(item.data(self.D_KEY)) if isinstance(build, BuildSettings): return build return None @@ -634,7 +635,7 @@ class _PreviewWidget(QTextBrowser): ) else: strBuildTime = self.tr("Unknown") - text = "{0} {1}".format(self.tr("Built"), strBuildTime) + text = "{0}: {1}".format(self.tr("Built"), strBuildTime) if self._buildName: text = "{0}
{1}".format(self._buildName, text) self.ageLabel.setText(text) diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py index 3f2bcdb6..cb5aedb1 100644 --- a/tests/test_tools/test_tools_manuscript.py +++ b/tests/test_tools/test_tools_manuscript.py @@ -168,11 +168,12 @@ def testManuscript_Features(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: # ======== # No build selected - manus.buildList.clearSelection() + manus.buildList.clear() manus.btnPreview.click() qtbot.wait(200) # Should be enough to run the build assert manus.docPreview.toPlainText().strip() == "" assert cacheFile.exists() is False + manus._updateBuildsList() # Preview the first, but fail to save cache manus.buildList.setCurrentRow(0) @@ -207,12 +208,6 @@ def testManuscript_Features(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: with monkeypatch.context() as mp: mp.setattr("novelwriter.tools.manusbuild.GuiManuscriptBuild.exec_", lambda *a: None) - # With no selection, no dialog should be created - manus.btnBuild.click() - for obj in manus.children(): - assert not isinstance(obj, GuiManuscriptBuild) - - # With a selection, there should be one manus.buildList.setCurrentRow(0) manus.btnBuild.click() for obj in manus.children(): From 28263dcbacb47d0fb6cb6c31411ad313d94f651f Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 6 Aug 2023 23:04:59 +0200 Subject: [PATCH 12/12] Make build settings dialog a child of main gui, and ensure only one instance can be open for each build --- novelwriter/tools/manuscript.py | 18 ++++++++++++++---- novelwriter/tools/manussettings.py | 14 ++++++++++++-- tests/test_tools/test_tools_manussettings.py | 12 ++++++------ 3 files changed, 32 insertions(+), 12 deletions(-) diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 1112622f..8292ceb3 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -30,7 +30,7 @@ from time import time from typing import TYPE_CHECKING from datetime import datetime -from PyQt5.QtGui import QColor, QCursor, QFont, QPalette, QResizeEvent +from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot from PyQt5.QtWidgets import ( QDialog, QGridLayout, QHBoxLayout, QLabel, QListWidget, QListWidgetItem, QPushButton, @@ -231,13 +231,13 @@ class GuiManuscript(QDialog): # Events ## - def closeEvent(self, event): + def closeEvent(self, event: QCloseEvent): """Capture the user closing the window so we can save GUI settings. We also check that we don't have a build settings dialog open. """ self._saveSettings() - for obj in self.children(): + for obj in self.mainGui.children(): # Make sure we don't have any settings windows open if isinstance(obj, GuiBuildSettings) and obj.isVisible(): obj.close() @@ -407,13 +407,23 @@ class GuiManuscript(QDialog): def _openSettingsDialog(self, build: BuildSettings): """Open the build settings dialog.""" - dlgSettings = GuiBuildSettings(self, self.mainGui, build) + for obj in self.mainGui.children(): + # Don't open a second dialog if one exists + if isinstance(obj, GuiBuildSettings): + if obj.buildID == build.buildID: + logger.debug("Found instance of GuiBuildSettings") + obj.show() + obj.raise_() + return + + dlgSettings = GuiBuildSettings(self.mainGui, build) dlgSettings.setModal(False) dlgSettings.show() dlgSettings.raise_() qApp.processEvents() dlgSettings.loadContent() dlgSettings.newSettingsReady.connect(self._processNewSettings) + return def _updateBuildsList(self): diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 0e848fd1..a3f22d2f 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -69,8 +69,8 @@ class GuiBuildSettings(QDialog): newSettingsReady = pyqtSignal(BuildSettings) - def __init__(self, parent: QWidget, mainGui: GuiMain, build: BuildSettings) -> None: - super().__init__(parent=parent) + def __init__(self, mainGui: GuiMain, build: BuildSettings) -> None: + super().__init__(parent=mainGui) logger.debug("Create: GuiBuildSettings") self.setObjectName("GuiBuildSettings") @@ -171,6 +171,7 @@ class GuiBuildSettings(QDialog): def __del__(self) -> None: # pragma: no cover logger.debug("Delete: GuiBuildSettings") + return def loadContent(self) -> None: """Populate the child widgets.""" @@ -182,6 +183,15 @@ class GuiBuildSettings(QDialog): self.optTabOutput.loadContent() return + ## + # Properties + ## + + @property + def buildID(self) -> str: + """The build ID of the build of the dialog.""" + return self._build.buildID + ## # Private Slots ## diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py index 36cf6d79..f84b2e3d 100644 --- a/tests/test_tools/test_tools_manussettings.py +++ b/tests/test_tools/test_tools_manussettings.py @@ -47,7 +47,7 @@ def testBuildSettings_Init(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockRnd build = BuildSettings() # Create the dialog and populate it - bSettings = GuiBuildSettings(nwGUI, nwGUI, build) + bSettings = GuiBuildSettings(nwGUI, build) bSettings.show() bSettings.loadContent() @@ -135,7 +135,7 @@ def testBuildSettings_Filter(qtbot: QtBot, nwGUI: GuiMain, projPath: Path, mockR nwGUI.theProject.tree[hPlotDoc].setActive(False) # type: ignore # Create the dialog and populate it - bSettings = GuiBuildSettings(nwGUI, nwGUI, build) + bSettings = GuiBuildSettings(nwGUI, build) bSettings.show() bSettings.loadContent() @@ -337,7 +337,7 @@ def testBuildSettings_Headings(qtbot: QtBot, nwGUI: GuiMain): build.setValue("headings.hideSection", False) # Create the dialog and populate it - bSettings = GuiBuildSettings(nwGUI, nwGUI, build) + bSettings = GuiBuildSettings(nwGUI, build) bSettings.show() bSettings.loadContent() @@ -486,7 +486,7 @@ def testBuildSettings_Content(qtbot: QtBot, nwGUI: GuiMain): build.setValue("text.addNoteHeadings", False) # Create the dialog and populate it - bSettings = GuiBuildSettings(nwGUI, nwGUI, build) + bSettings = GuiBuildSettings(nwGUI, build) bSettings.show() bSettings.loadContent() @@ -553,7 +553,7 @@ def testBuildSettings_Format(monkeypatch, qtbot: QtBot, nwGUI: GuiMain): build.setValue("format.rightMargin", 15.0) # Create the dialog and populate it - bSettings = GuiBuildSettings(nwGUI, nwGUI, build) + bSettings = GuiBuildSettings(nwGUI, build) bSettings.show() bSettings.loadContent() @@ -641,7 +641,7 @@ def testBuildSettings_Output(qtbot: QtBot, nwGUI: GuiMain): build.setValue("html.addStyles", False) # Create the dialog and populate it - bSettings = GuiBuildSettings(nwGUI, nwGUI, build) + bSettings = GuiBuildSettings(nwGUI, build) bSettings.show() bSettings.loadContent()