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}{h2}>\n")
elif tType == self.T_HEAD1:
- tHead = tText.replace(r"\\", "
")
+ tHead = tText.replace(nwHeadFmt.BR, "
")
tmpResult.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}{h1}>\n")
elif tType == self.T_HEAD2:
- tHead = tText.replace(r"\\", "
")
+ tHead = tText.replace(nwHeadFmt.BR, "
")
tmpResult.append(f"<{h2}{hStyle}>{aNm}{tHead}{h2}>\n")
elif tType == self.T_HEAD3:
- tHead = tText.replace(r"\\", "
")
+ tHead = tText.replace(nwHeadFmt.BR, "
")
tmpResult.append(f"<{h3}{hStyle}>{aNm}{tHead}{h3}>\n")
elif tType == self.T_HEAD4:
- tHead = tText.replace(r"\\", "
")
+ tHead = tText.replace(nwHeadFmt.BR, "
")
tmpResult.append(f"<{h4}{hStyle}>{aNm}{tHead}{h4}>\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()