From 3a2e2ff47863ef70e17da19332eb8f340d682a47 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 20 Mar 2024 17:26:36 +0100
Subject: [PATCH 1/9] Add consistent styling of tool buttons and tab widgets
---
novelwriter/extensions/modified.py | 16 +++++-
novelwriter/gui/doceditor.py | 67 ++++++++----------------
novelwriter/gui/docviewer.py | 82 +++++++++---------------------
novelwriter/gui/docviewerpanel.py | 28 ++--------
novelwriter/gui/noveltree.py | 28 ++++------
novelwriter/gui/projtree.py | 42 ++++++---------
novelwriter/gui/sidebar.py | 39 +++++---------
novelwriter/gui/theme.py | 47 +++++++++++++++++
novelwriter/tools/manuscript.py | 54 +++++++++-----------
9 files changed, 174 insertions(+), 229 deletions(-)
diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py
index 39416b23..15b61db0 100644
--- a/novelwriter/extensions/modified.py
+++ b/novelwriter/extensions/modified.py
@@ -25,9 +25,9 @@ along with this program. If not, see .
"""
from __future__ import annotations
-from PyQt5.QtCore import Qt
+from PyQt5.QtCore import QSize, Qt
from PyQt5.QtGui import QWheelEvent
-from PyQt5.QtWidgets import QComboBox, QDoubleSpinBox, QSpinBox, QWidget
+from PyQt5.QtWidgets import QComboBox, QDoubleSpinBox, QSpinBox, QToolButton, QWidget
class NComboBox(QComboBox):
@@ -85,3 +85,15 @@ class NDoubleSpinBox(QDoubleSpinBox):
return
# END Class NDoubleSpinBox
+
+
+class NIconToolButton(QToolButton):
+
+ def __init__(self, parent: QWidget, iconSize: int) -> None:
+ super().__init__(parent=parent)
+ self.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+ self.setIconSize(QSize(iconSize, iconSize))
+ self.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
+ return
+
+# END Class NIconToolButton
diff --git a/novelwriter/gui/doceditor.py b/novelwriter/gui/doceditor.py
index a3051647..8cd72429 100644
--- a/novelwriter/gui/doceditor.py
+++ b/novelwriter/gui/doceditor.py
@@ -53,15 +53,17 @@ from PyQt5.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
-from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
from novelwriter.common import minmax, transferCase
from novelwriter.constants import nwKeyWords, nwShortcode, nwUnicode
-from novelwriter.tools.lipsum import GuiLipsum
from novelwriter.core.document import NWDocument
-from novelwriter.text.counting import standardCounter
+from novelwriter.enum import nwDocAction, nwDocInsert, nwDocMode, nwItemClass, nwTrinary
+from novelwriter.extensions.eventfilters import WheelEventFilter
+from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.dochighlight import BLOCK_META, BLOCK_TITLE
from novelwriter.gui.editordocument import GuiTextDocument
-from novelwriter.extensions.eventfilters import WheelEventFilter
+from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
+from novelwriter.text.counting import standardCounter
+from novelwriter.tools.lipsum import GuiLipsum
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
@@ -2805,10 +2807,8 @@ class GuiDocEditHeader(QWidget):
self._docHandle = None
self._docOutline: dict[int, str] = {}
- fPx = int(0.9*SHARED.theme.fontPixelSize)
- mPx = CONFIG.pxInt(8)
- hSp = CONFIG.pxInt(6)
- iconSize = QSize(fPx, fPx)
+ iPx = SHARED.theme.baseIconSize
+ mPx = CONFIG.pxInt(4)
# Main Widget Settings
self.setAutoFillBackground(True)
@@ -2820,7 +2820,7 @@ class GuiDocEditHeader(QWidget):
self.itemTitle.setContentsMargins(0, 0, 0, 0)
self.itemTitle.setAutoFillBackground(True)
self.itemTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
- self.itemTitle.setFixedHeight(fPx)
+ self.itemTitle.setFixedHeight(iPx)
lblFont = self.itemTitle.font()
lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
@@ -2830,63 +2830,44 @@ class GuiDocEditHeader(QWidget):
self.outlineMenu = QMenu(self)
# Buttons
- self.tbButton = QToolButton(self)
- self.tbButton.setContentsMargins(0, 0, 0, 0)
- self.tbButton.setIconSize(iconSize)
- self.tbButton.setFixedSize(fPx, fPx)
- self.tbButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+ self.tbButton = NIconToolButton(self, iPx)
self.tbButton.setVisible(False)
self.tbButton.setToolTip(self.tr("Toggle Tool Bar"))
self.tbButton.clicked.connect(lambda: self.toggleToolBarRequest.emit())
- self.outlineButton = QToolButton(self)
- self.outlineButton.setContentsMargins(0, 0, 0, 0)
- self.outlineButton.setIconSize(iconSize)
- self.outlineButton.setFixedSize(fPx, fPx)
- self.outlineButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+ self.outlineButton = NIconToolButton(self, iPx)
self.outlineButton.setVisible(False)
self.outlineButton.setToolTip(self.tr("Outline"))
self.outlineButton.setMenu(self.outlineMenu)
- self.outlineButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
- self.searchButton = QToolButton(self)
- self.searchButton.setContentsMargins(0, 0, 0, 0)
- self.searchButton.setIconSize(iconSize)
- self.searchButton.setFixedSize(fPx, fPx)
- self.searchButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+ self.searchButton = NIconToolButton(self, iPx)
self.searchButton.setVisible(False)
self.searchButton.setToolTip(self.tr("Search"))
self.searchButton.clicked.connect(self.docEditor.toggleSearch)
- self.minmaxButton = QToolButton(self)
- self.minmaxButton.setContentsMargins(0, 0, 0, 0)
- self.minmaxButton.setIconSize(iconSize)
- self.minmaxButton.setFixedSize(fPx, fPx)
- self.minmaxButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+ self.minmaxButton = NIconToolButton(self, iPx)
self.minmaxButton.setVisible(False)
self.minmaxButton.setToolTip(self.tr("Toggle Focus Mode"))
self.minmaxButton.clicked.connect(lambda: self.docEditor.toggleFocusModeRequest.emit())
- self.closeButton = QToolButton(self)
- self.closeButton.setContentsMargins(0, 0, 0, 0)
- self.closeButton.setIconSize(iconSize)
- self.closeButton.setFixedSize(fPx, fPx)
- self.closeButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+ self.closeButton = NIconToolButton(self, iPx)
self.closeButton.setVisible(False)
self.closeButton.setToolTip(self.tr("Close"))
self.closeButton.clicked.connect(self._closeDocument)
# Assemble Layout
self.outerBox = QHBoxLayout()
- self.outerBox.setSpacing(hSp)
self.outerBox.addWidget(self.tbButton, 0)
self.outerBox.addWidget(self.outlineButton, 0)
self.outerBox.addWidget(self.searchButton, 0)
+ self.outerBox.addSpacing(mPx)
self.outerBox.addWidget(self.itemTitle, 1)
- self.outerBox.addSpacing(fPx + hSp)
+ self.outerBox.addSpacing(mPx)
+ self.outerBox.addSpacing(iPx)
self.outerBox.addWidget(self.minmaxButton, 0)
self.outerBox.addWidget(self.closeButton, 0)
self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
+ self.outerBox.setSpacing(0)
self.setLayout(self.outerBox)
@@ -2896,7 +2877,7 @@ class GuiDocEditHeader(QWidget):
# Fix Margins and Size
# This is needed for high DPI systems. See issue #499.
self.setContentsMargins(0, 0, 0, 0)
- self.setMinimumHeight(fPx + 2*mPx)
+ self.setMinimumHeight(iPx + 2*mPx)
self.updateTheme()
@@ -2944,15 +2925,9 @@ class GuiDocEditHeader(QWidget):
self.minmaxButton.setIcon(SHARED.theme.getIcon("maximise"))
self.closeButton.setIcon(SHARED.theme.getIcon("close"))
- colText = SHARED.theme.colText
- buttonStyle = (
- "QToolButton {{border: none; background: transparent;}} "
- "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
- ).format(colText.red(), colText.green(), colText.blue())
- buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
-
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
self.tbButton.setStyleSheet(buttonStyle)
- self.outlineButton.setStyleSheet(buttonStyleMenu)
+ self.outlineButton.setStyleSheet(buttonStyle)
self.searchButton.setStyleSheet(buttonStyle)
self.minmaxButton.setStyleSheet(buttonStyle)
self.closeButton.setStyleSheet(buttonStyle)
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index d3352720..891af3e6 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -44,6 +44,8 @@ from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
from novelwriter.error import logException
from novelwriter.constants import nwHeaders, nwUnicode
+from novelwriter.extensions.modified import NIconToolButton
+from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
from novelwriter.core.tohtml import ToHtml
from novelwriter.extensions.eventfilters import WheelEventFilter
@@ -630,10 +632,8 @@ class GuiDocViewHeader(QWidget):
self._docHandle = None
self._docOutline: dict[int, tuple[str, int]] = {}
- fPx = int(0.9*SHARED.theme.fontPixelSize)
- hSp = CONFIG.pxInt(6)
- mPx = CONFIG.pxInt(8)
- iconSize = QSize(fPx, fPx)
+ iPx = SHARED.theme.baseIconSize
+ mPx = CONFIG.pxInt(4)
# Main Widget Settings
self.setAutoFillBackground(True)
@@ -646,7 +646,7 @@ class GuiDocViewHeader(QWidget):
self.itemTitle.setContentsMargins(0, 0, 0, 0)
self.itemTitle.setAutoFillBackground(True)
self.itemTitle.setAlignment(Qt.AlignmentFlag.AlignHCenter | Qt.AlignmentFlag.AlignTop)
- self.itemTitle.setFixedHeight(fPx)
+ self.itemTitle.setFixedHeight(iPx)
lblFont = self.itemTitle.font()
lblFont.setPointSizeF(0.9*SHARED.theme.fontPointSize)
@@ -656,62 +656,43 @@ class GuiDocViewHeader(QWidget):
self.outlineMenu = QMenu(self)
# Buttons
- self.outlineButton = QToolButton(self)
- self.outlineButton.setContentsMargins(0, 0, 0, 0)
- self.outlineButton.setIconSize(iconSize)
- self.outlineButton.setFixedSize(fPx, fPx)
- self.outlineButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+ self.outlineButton = NIconToolButton(self, iPx)
self.outlineButton.setVisible(False)
self.outlineButton.setToolTip(self.tr("Outline"))
self.outlineButton.setMenu(self.outlineMenu)
- self.outlineButton.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
- self.backButton = QToolButton(self)
- self.backButton.setContentsMargins(0, 0, 0, 0)
- self.backButton.setIconSize(iconSize)
- self.backButton.setFixedSize(fPx, fPx)
- self.backButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+ self.backButton = NIconToolButton(self, iPx)
self.backButton.setVisible(False)
self.backButton.setToolTip(self.tr("Go Backward"))
self.backButton.clicked.connect(self.docViewer.navBackward)
- self.forwardButton = QToolButton(self)
- self.forwardButton.setContentsMargins(0, 0, 0, 0)
- self.forwardButton.setIconSize(iconSize)
- self.forwardButton.setFixedSize(fPx, fPx)
- self.forwardButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+ self.forwardButton = NIconToolButton(self, iPx)
self.forwardButton.setVisible(False)
self.forwardButton.setToolTip(self.tr("Go Forward"))
self.forwardButton.clicked.connect(self.docViewer.navForward)
- self.refreshButton = QToolButton(self)
- self.refreshButton.setContentsMargins(0, 0, 0, 0)
- self.refreshButton.setIconSize(iconSize)
- self.refreshButton.setFixedSize(fPx, fPx)
- self.refreshButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+ self.refreshButton = NIconToolButton(self, iPx)
self.refreshButton.setVisible(False)
self.refreshButton.setToolTip(self.tr("Reload"))
self.refreshButton.clicked.connect(self._refreshDocument)
- self.closeButton = QToolButton(self)
- self.closeButton.setContentsMargins(0, 0, 0, 0)
- self.closeButton.setIconSize(iconSize)
- self.closeButton.setFixedSize(fPx, fPx)
- self.closeButton.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
+ self.closeButton = NIconToolButton(self, iPx)
self.closeButton.setVisible(False)
self.closeButton.setToolTip(self.tr("Close"))
self.closeButton.clicked.connect(self._closeDocument)
# Assemble Layout
self.outerBox = QHBoxLayout()
- self.outerBox.setSpacing(hSp)
self.outerBox.addWidget(self.outlineButton, 0)
self.outerBox.addWidget(self.backButton, 0)
self.outerBox.addWidget(self.forwardButton, 0)
+ self.outerBox.addSpacing(mPx)
self.outerBox.addWidget(self.itemTitle, 1)
- self.outerBox.addSpacing(fPx + hSp)
+ self.outerBox.addSpacing(mPx)
+ self.outerBox.addSpacing(iPx)
self.outerBox.addWidget(self.refreshButton, 0)
self.outerBox.addWidget(self.closeButton, 0)
+ self.outerBox.setSpacing(0)
self.setLayout(self.outerBox)
@@ -719,7 +700,7 @@ class GuiDocViewHeader(QWidget):
# This is needed for high DPI systems. See issue #499.
self.setContentsMargins(0, 0, 0, 0)
self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
- self.setMinimumHeight(fPx + 2*mPx)
+ self.setMinimumHeight(iPx + 2*mPx)
# Fix the Colours
self.updateTheme()
@@ -773,14 +754,8 @@ class GuiDocViewHeader(QWidget):
self.refreshButton.setIcon(SHARED.theme.getIcon("refresh"))
self.closeButton.setIcon(SHARED.theme.getIcon("close"))
- colText = SHARED.theme.colText
- buttonStyle = (
- "QToolButton {{border: none; background: transparent;}} "
- "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
- ).format(colText.red(), colText.green(), colText.blue())
- buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
-
- self.outlineButton.setStyleSheet(buttonStyleMenu)
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
+ self.outlineButton.setStyleSheet(buttonStyle)
self.backButton.setStyleSheet(buttonStyle)
self.forwardButton.setStyleSheet(buttonStyle)
self.refreshButton.setStyleSheet(buttonStyle)
@@ -878,17 +853,16 @@ class GuiDocViewFooter(QWidget):
# Internal Variables
self._docHandle = None
- fPx = int(0.9*SHARED.theme.fontPixelSize)
+ iPx = SHARED.theme.baseIconSize
hSp = CONFIG.pxInt(4)
+ mPx = CONFIG.pxInt(4)
# Main Widget Settings
self.setContentsMargins(0, 0, 0, 0)
self.setAutoFillBackground(True)
# Show/Hide Details
- self.showHide = QToolButton(self)
- self.showHide.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
- self.showHide.setIconSize(QSize(fPx, fPx))
+ self.showHide = NIconToolButton(self, iPx)
self.showHide.clicked.connect(lambda: self.docViewer.togglePanelVisibility.emit())
self.showHide.setToolTip(self.tr("Show/Hide Viewer Panel"))
@@ -898,7 +872,7 @@ class GuiDocViewFooter(QWidget):
self.showComments.setCheckable(True)
self.showComments.setChecked(CONFIG.viewComments)
self.showComments.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
- self.showComments.setIconSize(QSize(fPx, fPx))
+ self.showComments.setIconSize(QSize(iPx, iPx))
self.showComments.toggled.connect(self._doToggleComments)
self.showComments.setToolTip(self.tr("Show Comments"))
@@ -908,7 +882,7 @@ class GuiDocViewFooter(QWidget):
self.showSynopsis.setCheckable(True)
self.showSynopsis.setChecked(CONFIG.viewSynopsis)
self.showSynopsis.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonTextBesideIcon)
- self.showSynopsis.setIconSize(QSize(fPx, fPx))
+ self.showSynopsis.setIconSize(QSize(iPx, iPx))
self.showSynopsis.toggled.connect(self._doToggleSynopsis)
self.showSynopsis.setToolTip(self.tr("Show Synopsis Comments"))
@@ -928,10 +902,9 @@ class GuiDocViewFooter(QWidget):
# Fix Margins and Size
# This is needed for high DPI systems. See issue #499.
- cM = CONFIG.pxInt(8)
self.setContentsMargins(0, 0, 0, 0)
- self.outerBox.setContentsMargins(cM, cM, cM, cM)
- self.setMinimumHeight(fPx + 2*cM)
+ self.outerBox.setContentsMargins(mPx, mPx, mPx, mPx)
+ self.setMinimumHeight(iPx + 2*mPx)
# Fix the Colours
self.updateTheme()
@@ -954,12 +927,7 @@ class GuiDocViewFooter(QWidget):
self.showComments.setIcon(bulletIcon)
self.showSynopsis.setIcon(bulletIcon)
- colText = SHARED.theme.colText
- buttonStyle = (
- "QToolButton {{border: none; background: transparent;}} "
- "QToolButton:hover {{border: none; background: rgba({0}, {1}, {2}, 0.2);}}"
- ).format(colText.red(), colText.green(), colText.blue())
-
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
self.showHide.setStyleSheet(buttonStyle)
self.showComments.setStyleSheet(buttonStyle)
self.showSynopsis.setStyleSheet(buttonStyle)
diff --git a/novelwriter/gui/docviewerpanel.py b/novelwriter/gui/docviewerpanel.py
index a5a5695e..a58d7f02 100644
--- a/novelwriter/gui/docviewerpanel.py
+++ b/novelwriter/gui/docviewerpanel.py
@@ -34,10 +34,11 @@ from PyQt5.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
-from novelwriter.enum import nwDocMode, nwItemClass
from novelwriter.common import checkInt
from novelwriter.constants import nwHeaders, nwLabels, nwLists, trConst
from novelwriter.core.index import IndexHeading, IndexItem
+from novelwriter.enum import nwDocMode, nwItemClass
+from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
logger = logging.getLogger(__name__)
@@ -99,35 +100,14 @@ class GuiDocViewerPanel(QWidget):
def updateTheme(self, updateTabs: bool = True) -> None:
"""Update theme elements."""
- qPalette = self.palette()
- mPx = CONFIG.pxInt(2)
- vPx = CONFIG.pxInt(4)
- hPx = CONFIG.pxInt(8)
- hCol = qPalette.highlight().color()
- fCol = qPalette.text().color()
-
- buttonStyle = (
- "QToolButton {{padding: {0}px; margin: 0 0 {1}px 0; border: none; "
- "background: transparent;}} "
- "QToolButton:hover {{border: none; background: rgba({2}, {3}, {4}, 0.2);}} "
- "QToolButton::menu-indicator {{image: none;}} "
- ).format(mPx, mPx, fCol.red(), fCol.green(), fCol.blue())
self.optsButton.setIcon(SHARED.theme.getIcon("menu"))
- self.optsButton.setStyleSheet(buttonStyle)
-
- styleSheet = (
- "QTabWidget::pane {{border: 0;}} "
- "QTabWidget QTabBar::tab {{border: 0; padding: {0}px {1}px;}} "
- "QTabWidget QTabBar::tab:selected {{color: rgb({2}, {3}, {4});}} "
- ).format(vPx, hPx, hCol.red(), hCol.green(), hCol.blue())
- self.mainTabs.setStyleSheet(styleSheet)
+ self.optsButton.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON))
+ self.mainTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS))
self.updateHandle(self._lastHandle)
-
if updateTabs:
self.tabBackRefs.updateTheme()
for tab in self.kwTabs.values():
tab.updateTheme()
-
return
def openProjectTasks(self) -> None:
diff --git a/novelwriter/gui/noveltree.py b/novelwriter/gui/noveltree.py
index f2c62ddb..95a6e25f 100644
--- a/novelwriter/gui/noveltree.py
+++ b/novelwriter/gui/noveltree.py
@@ -35,16 +35,18 @@ from PyQt5.QtGui import QFocusEvent, QFont, QMouseEvent, QPalette, QResizeEvent
from PyQt5.QtCore import QModelIndex, QPoint, Qt, QSize, pyqtSlot, pyqtSignal
from PyQt5.QtWidgets import (
QAbstractItemView, QActionGroup, QFrame, QHBoxLayout, QHeaderView,
- QInputDialog, QMenu, QSizePolicy, QToolButton, QToolTip, QTreeWidget,
- QTreeWidgetItem, QVBoxLayout, QWidget
+ QInputDialog, QMenu, QSizePolicy, QToolTip, QTreeWidget, QTreeWidgetItem,
+ QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED
-from novelwriter.enum import nwDocMode, nwItemClass, nwOutline
from novelwriter.common import minmax
from novelwriter.constants import nwHeaders, nwKeyWords, nwLabels, trConst
from novelwriter.core.index import IndexHeading
+from novelwriter.enum import nwDocMode, nwItemClass, nwOutline
+from novelwriter.extensions.modified import NIconToolButton
from novelwriter.extensions.novelselector import NovelSelector
+from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
@@ -215,15 +217,13 @@ class GuiNovelToolBar(QWidget):
self.novelValue.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
self.novelValue.novelSelectionChanged.connect(self.setCurrentRoot)
- self.tbNovel = QToolButton(self)
+ self.tbNovel = NIconToolButton(self, iPx)
self.tbNovel.setToolTip(self.tr("Novel Root"))
- self.tbNovel.setIconSize(QSize(iPx, iPx))
self.tbNovel.clicked.connect(self.novelValue.showPopup)
# Refresh Button
- self.tbRefresh = QToolButton(self)
+ self.tbRefresh = NIconToolButton(self, iPx)
self.tbRefresh.setToolTip(self.tr("Refresh"))
- self.tbRefresh.setIconSize(QSize(iPx, iPx))
self.tbRefresh.clicked.connect(self._refreshNovelTree)
# More Options Menu
@@ -241,11 +241,9 @@ class GuiNovelToolBar(QWidget):
self.aLastColSize = self.mLastCol.addAction(self.tr("Column Size"))
self.aLastColSize.triggered.connect(self._selectLastColumnSize)
- self.tbMore = QToolButton(self)
+ self.tbMore = NIconToolButton(self, iPx)
self.tbMore.setToolTip(self.tr("More Options"))
- self.tbMore.setIconSize(QSize(iPx, iPx))
self.tbMore.setMenu(self.mMore)
- self.tbMore.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
# Assemble
self.outerBox = QHBoxLayout()
@@ -280,16 +278,10 @@ class GuiNovelToolBar(QWidget):
self.setPalette(qPalette)
# StyleSheets
- fadeCol = qPalette.text().color()
- buttonStyle = (
- "QToolButton {{padding: {0}px; border: none; background: transparent;}} "
- "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
- ).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
- buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
-
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
self.tbNovel.setStyleSheet(buttonStyle)
self.tbRefresh.setStyleSheet(buttonStyle)
- self.tbMore.setStyleSheet(buttonStyleMenu)
+ self.tbMore.setStyleSheet(buttonStyle)
self.novelValue.setStyleSheet(
"QComboBox {border-style: none; padding-left: 0;} "
diff --git a/novelwriter/gui/projtree.py b/novelwriter/gui/projtree.py
index 674f9da2..7299aff0 100644
--- a/novelwriter/gui/projtree.py
+++ b/novelwriter/gui/projtree.py
@@ -38,20 +38,22 @@ from PyQt5.QtGui import (
from PyQt5.QtCore import QPoint, QTimer, Qt, QSize, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractItemView, QAction, QDialog, QFrame, QHBoxLayout, QHeaderView,
- QLabel, QMenu, QShortcut, QSizePolicy, QToolButton, QTreeWidget,
- QTreeWidgetItem, QVBoxLayout, QWidget
+ QLabel, QMenu, QShortcut, QSizePolicy, QTreeWidget, QTreeWidgetItem,
+ QVBoxLayout, QWidget
)
from novelwriter import CONFIG, SHARED
-from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout
from novelwriter.common import minmax
from novelwriter.constants import nwHeaders, nwUnicode, trConst, nwLabels
-from novelwriter.core.item import NWItem
from novelwriter.core.coretools import DocDuplicator, DocMerger, DocSplitter
+from novelwriter.core.item import NWItem
from novelwriter.dialogs.docmerge import GuiDocMerge
from novelwriter.dialogs.docsplit import GuiDocSplit
from novelwriter.dialogs.editlabel import GuiEditLabel
from novelwriter.dialogs.projectsettings import GuiProjectSettings
+from novelwriter.enum import nwDocMode, nwItemType, nwItemClass, nwItemLayout
+from novelwriter.extensions.modified import NIconToolButton
+from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
@@ -275,22 +277,18 @@ class GuiProjectToolBar(QWidget):
# Quick Links
self.mQuick = QMenu(self)
- self.tbQuick = QToolButton(self)
+ self.tbQuick = NIconToolButton(self, iPx)
self.tbQuick.setToolTip("%s [Ctrl+L]" % self.tr("Quick Links"))
self.tbQuick.setShortcut("Ctrl+L")
- self.tbQuick.setIconSize(QSize(iPx, iPx))
self.tbQuick.setMenu(self.mQuick)
- self.tbQuick.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
# Move Buttons
- self.tbMoveU = QToolButton(self)
+ self.tbMoveU = NIconToolButton(self, iPx)
self.tbMoveU.setToolTip("%s [Ctrl+Up]" % self.tr("Move Up"))
- self.tbMoveU.setIconSize(QSize(iPx, iPx))
self.tbMoveU.clicked.connect(lambda: self.projTree.moveTreeItem(-1))
- self.tbMoveD = QToolButton(self)
+ self.tbMoveD = NIconToolButton(self, iPx)
self.tbMoveD.setToolTip("%s [Ctrl+Down]" % self.tr("Move Down"))
- self.tbMoveD.setIconSize(QSize(iPx, iPx))
self.tbMoveD.clicked.connect(lambda: self.projTree.moveTreeItem(1))
# Add Item Menu
@@ -329,12 +327,10 @@ class GuiProjectToolBar(QWidget):
self.mAddRoot = self.mAdd.addMenu(trConst(nwLabels.ITEM_DESCRIPTION["root"]))
self._buildRootMenu()
- self.tbAdd = QToolButton(self)
+ self.tbAdd = NIconToolButton(self, iPx)
self.tbAdd.setToolTip("%s [Ctrl+N]" % self.tr("Add Item"))
self.tbAdd.setShortcut("Ctrl+N")
- self.tbAdd.setIconSize(QSize(iPx, iPx))
self.tbAdd.setMenu(self.mAdd)
- self.tbAdd.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
# More Options Menu
self.mMore = QMenu(self)
@@ -348,11 +344,9 @@ class GuiProjectToolBar(QWidget):
self.aEmptyTrash = self.mMore.addAction(self.tr("Empty Trash"))
self.aEmptyTrash.triggered.connect(lambda: self.projTree.emptyTrash())
- self.tbMore = QToolButton(self)
+ self.tbMore = NIconToolButton(self, iPx)
self.tbMore.setToolTip(self.tr("More Options"))
- self.tbMore.setIconSize(QSize(iPx, iPx))
self.tbMore.setMenu(self.mMore)
- self.tbMore.setPopupMode(QToolButton.ToolButtonPopupMode.InstantPopup)
# Assemble
self.outerBox = QHBoxLayout()
@@ -382,18 +376,12 @@ class GuiProjectToolBar(QWidget):
qPalette.setBrush(QPalette.ColorRole.Window, qPalette.base())
self.setPalette(qPalette)
- fadeCol = qPalette.text().color()
- buttonStyle = (
- "QToolButton {{padding: {0}px; border: none; background: transparent;}} "
- "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
- ).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
- buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
-
- self.tbQuick.setStyleSheet(buttonStyleMenu)
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
+ self.tbQuick.setStyleSheet(buttonStyle)
self.tbMoveU.setStyleSheet(buttonStyle)
self.tbMoveD.setStyleSheet(buttonStyle)
- self.tbAdd.setStyleSheet(buttonStyleMenu)
- self.tbMore.setStyleSheet(buttonStyleMenu)
+ self.tbAdd.setStyleSheet(buttonStyle)
+ self.tbMore.setStyleSheet(buttonStyle)
self.tbQuick.setIcon(SHARED.theme.getIcon("bookmark"))
self.tbMoveU.setIcon(SHARED.theme.getIcon("up"))
diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py
index d425ccf8..9497444d 100644
--- a/novelwriter/gui/sidebar.py
+++ b/novelwriter/gui/sidebar.py
@@ -28,12 +28,14 @@ import logging
from typing import TYPE_CHECKING
from PyQt5.QtGui import QPalette
-from PyQt5.QtCore import QEvent, QPoint, Qt, QSize, pyqtSignal
-from PyQt5.QtWidgets import QMenu, QToolButton, QVBoxLayout, QWidget
+from PyQt5.QtCore import QEvent, QPoint, pyqtSignal
+from PyQt5.QtWidgets import QMenu, QVBoxLayout, QWidget
from novelwriter import CONFIG, SHARED
from novelwriter.enum import nwView
from novelwriter.extensions.eventfilters import StatusTipFilter
+from novelwriter.extensions.modified import NIconToolButton
+from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
@@ -53,46 +55,37 @@ class GuiSideBar(QWidget):
self.mainGui = mainGui
iPx = CONFIG.pxInt(24)
- iconSize = QSize(iPx, iPx)
self.setContentsMargins(0, 0, 0, 0)
self.installEventFilter(StatusTipFilter(mainGui))
# Buttons
- self.tbProject = QToolButton(self)
+ self.tbProject = NIconToolButton(self, iPx)
self.tbProject.setToolTip("{0} [Ctrl+T]".format(self.tr("Project Tree View")))
- self.tbProject.setIconSize(iconSize)
self.tbProject.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.PROJECT))
- self.tbNovel = QToolButton(self)
+ self.tbNovel = NIconToolButton(self, iPx)
self.tbNovel.setToolTip("{0} [Ctrl+T]".format(self.tr("Novel Tree View")))
- self.tbNovel.setIconSize(iconSize)
self.tbNovel.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.NOVEL))
- self.tbOutline = QToolButton(self)
+ self.tbOutline = NIconToolButton(self, iPx)
self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
- self.tbOutline.setIconSize(iconSize)
self.tbOutline.clicked.connect(lambda: self.viewChangeRequested.emit(nwView.OUTLINE))
- self.tbBuild = QToolButton(self)
+ self.tbBuild = NIconToolButton(self, iPx)
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
- self.tbBuild.setIconSize(iconSize)
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
- self.tbDetails = QToolButton(self)
+ self.tbDetails = NIconToolButton(self, iPx)
self.tbDetails.setToolTip("{0} [Shift+F6]".format(self.tr("Novel Details")))
- self.tbDetails.setIconSize(iconSize)
self.tbDetails.clicked.connect(self.mainGui.showNovelDetailsDialog)
- self.tbStats = QToolButton(self)
+ self.tbStats = NIconToolButton(self, iPx)
self.tbStats.setToolTip("{0} [F6]".format(self.tr("Writing Statistics")))
- self.tbStats.setIconSize(iconSize)
self.tbStats.clicked.connect(self.mainGui.showWritingStatsDialog)
# Settings Menu
- self.tbSettings = QToolButton(self)
+ self.tbSettings = NIconToolButton(self, iPx)
self.tbSettings.setToolTip(self.tr("Settings"))
- self.tbSettings.setIconSize(iconSize)
- self.tbSettings.setToolButtonStyle(Qt.ToolButtonStyle.ToolButtonIconOnly)
self.mSettings = _PopRightMenu(self.tbSettings)
self.mSettings.addAction(self.mainGui.mainMenu.aEditWordList)
@@ -101,7 +94,6 @@ class GuiSideBar(QWidget):
self.mSettings.addAction(self.mainGui.mainMenu.aPreferences)
self.tbSettings.setMenu(self.mSettings)
- self.tbSettings.setPopupMode(QToolButton.InstantPopup)
# Assemble
self.outerBox = QVBoxLayout()
@@ -129,12 +121,7 @@ class GuiSideBar(QWidget):
qPalette.setBrush(QPalette.Window, qPalette.base())
self.setPalette(qPalette)
- fadeCol = qPalette.text().color()
- buttonStyle = (
- "QToolButton {{padding: {0}px; border: none; background: transparent;}} "
- "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
- ).format(CONFIG.pxInt(6), fadeCol.red(), fadeCol.green(), fadeCol.blue())
- buttonStyleMenu = f"{buttonStyle} QToolButton::menu-indicator {{image: none;}}"
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_BIG_TOOLBUTTON)
self.tbProject.setIcon(SHARED.theme.getIcon("view_editor"))
self.tbProject.setStyleSheet(buttonStyle)
@@ -155,7 +142,7 @@ class GuiSideBar(QWidget):
self.tbStats.setStyleSheet(buttonStyle)
self.tbSettings.setIcon(SHARED.theme.getIcon("settings"))
- self.tbSettings.setStyleSheet(buttonStyleMenu)
+ self.tbSettings.setStyleSheet(buttonStyle)
return
diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py
index 548ed327..8ffe396e 100644
--- a/novelwriter/gui/theme.py
+++ b/novelwriter/gui/theme.py
@@ -43,6 +43,10 @@ from novelwriter.constants import nwLabels
logger = logging.getLogger(__name__)
+STYLES_FLAT_TABS = "flatTabWidget"
+STYLES_MIN_TOOLBUTTON = "minimalToolButton"
+STYLES_BIG_TOOLBUTTON = "bigToolButton"
+
# =============================================================================================== #
# Gui Theme Class
@@ -120,6 +124,7 @@ class GuiTheme:
self._syntaxList: list[tuple[str, str]] = []
self._availThemes: dict[str, Path] = {}
self._availSyntax: dict[str, Path] = {}
+ self._styleSheets: dict[str, str] = {}
self._listConf(self._availSyntax, CONFIG.assetPath("syntax"))
self._listConf(self._availThemes, CONFIG.assetPath("themes"))
@@ -272,6 +277,9 @@ class GuiTheme:
# Apply Styles
qApp.setPalette(self._guiPalette)
+ # Reset stylesheets so that they are regenerated
+ self._buildStyleSheets(self._guiPalette)
+
return True
def loadSyntax(self) -> bool:
@@ -366,6 +374,10 @@ class GuiTheme:
return self._syntaxList
+ def getStyleSheet(self, name: str) -> str:
+ """Load a standard style sheet."""
+ return self._styleSheets.get(name, "")
+
##
# Internal Functions
##
@@ -412,6 +424,41 @@ class GuiTheme:
self._guiPalette.setColor(value, self._parseColour(parser, section, name))
return
+ def _buildStyleSheets(self, palette: QPalette) -> None:
+ """Build default style sheets."""
+ self._styleSheets = {}
+
+ aPx = CONFIG.pxInt(2)
+ bPx = CONFIG.pxInt(4)
+ cPx = CONFIG.pxInt(6)
+ dPx = CONFIG.pxInt(8)
+
+ tCol = palette.text().color()
+ hCol = palette.highlight().color()
+
+ # Flat Tab Widget and Tab Bar:
+ self._styleSheets[STYLES_FLAT_TABS] = (
+ "QTabWidget::pane {{border: 0;}} "
+ "QTabWidget QTabBar::tab {{border: 0; padding: {0}px {1}px;}} "
+ "QTabWidget QTabBar::tab:selected {{color: rgb({2}, {3}, {4});}} "
+ ).format(bPx, dPx, hCol.red(), hCol.green(), hCol.blue())
+
+ # Minimal Tool Button
+ self._styleSheets[STYLES_MIN_TOOLBUTTON] = (
+ "QToolButton {{padding: {0}px; margin: 0; border: none; background: transparent;}} "
+ "QToolButton:hover {{border: none; background: rgba({1}, {2}, {3}, 0.2);}} "
+ "QToolButton::menu-indicator {{image: none;}} "
+ ).format(aPx, tCol.red(), tCol.green(), tCol.blue())
+
+ # Big Tool Button
+ self._styleSheets[STYLES_BIG_TOOLBUTTON] = (
+ "QToolButton {{padding: {0}px; margin: 0; border: none; background: transparent;}} "
+ "QToolButton:hover {{border: none; background: rgba({1}, {2}, {3}, 0.2);}} "
+ "QToolButton::menu-indicator {{image: none;}} "
+ ).format(cPx, tCol.red(), tCol.green(), tCol.blue())
+
+ return
+
# End Class GuiTheme
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 0e89b3c6..316c2541 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -35,21 +35,23 @@ from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractItemView, QDialog, QFormLayout, QGridLayout, QHBoxLayout, QLabel,
QListWidget, QListWidgetItem, QPushButton, QSizePolicy, QSplitter,
- QStackedWidget, QTextBrowser, QToolButton, QTreeWidget, QTreeWidgetItem,
+ QStackedWidget, QTabWidget, QTextBrowser, QTreeWidget, QTreeWidgetItem,
QVBoxLayout, QWidget, qApp
)
from PyQt5.QtPrintSupport import QPrintPreviewDialog, QPrinter
from novelwriter import CONFIG, SHARED
-from novelwriter.error import logException
from novelwriter.common import checkInt, fuzzyTime
-from novelwriter.core.tohtml import ToHtml
-from novelwriter.core.docbuild import NWBuildDocument
-from novelwriter.core.tokenizer import HeadingFormatter
from novelwriter.core.buildsettings import BuildCollection, BuildSettings
+from novelwriter.core.docbuild import NWBuildDocument
+from novelwriter.core.tohtml import ToHtml
+from novelwriter.core.tokenizer import HeadingFormatter
+from novelwriter.error import logException
+from novelwriter.extensions.circularprogress import NProgressCircle
+from novelwriter.extensions.modified import NIconToolButton
+from novelwriter.gui.theme import STYLES_FLAT_TABS, STYLES_MIN_TOOLBUTTON
from novelwriter.tools.manusbuild import GuiManuscriptBuild
from novelwriter.tools.manussettings import GuiBuildSettings
-from novelwriter.extensions.circularprogress import NProgressCircle
if TYPE_CHECKING: # pragma: no cover
from novelwriter.guimain import GuiMain
@@ -101,29 +103,22 @@ class GuiManuscript(QDialog):
qPalette.setBrush(QPalette.Window, qPalette.base())
self.setPalette(qPalette)
- fadeCol = qPalette.text().color()
- buttonStyle = (
- "QToolButton {{padding: {0}px; border: none; background: transparent;}} "
- "QToolButton:hover {{border: none; background: rgba({1},{2},{3},0.2);}}"
- ).format(CONFIG.pxInt(2), fadeCol.red(), fadeCol.green(), fadeCol.blue())
+ buttonStyle = SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON)
- self.tbAdd = QToolButton(self)
+ self.tbAdd = NIconToolButton(self, iPx)
self.tbAdd.setIcon(SHARED.theme.getIcon("add"))
- self.tbAdd.setIconSize(QSize(iPx, iPx))
self.tbAdd.setToolTip(self.tr("Add New Build"))
self.tbAdd.setStyleSheet(buttonStyle)
self.tbAdd.clicked.connect(self._createNewBuild)
- self.tbDel = QToolButton(self)
+ self.tbDel = NIconToolButton(self, iPx)
self.tbDel.setIcon(SHARED.theme.getIcon("remove"))
- self.tbDel.setIconSize(QSize(iPx, iPx))
self.tbDel.setToolTip(self.tr("Delete Selected Build"))
self.tbDel.setStyleSheet(buttonStyle)
self.tbDel.clicked.connect(self._deleteSelectedBuild)
- self.tbEdit = QToolButton(self)
+ self.tbEdit = NIconToolButton(self, iPx)
self.tbEdit.setIcon(SHARED.theme.getIcon("edit"))
- self.tbEdit.setIconSize(QSize(iPx, iPx))
self.tbEdit.setToolTip(self.tr("Edit Selected Build"))
self.tbEdit.setStyleSheet(buttonStyle)
self.tbEdit.clicked.connect(self._editSelectedBuild)
@@ -141,21 +136,28 @@ class GuiManuscript(QDialog):
# Builds
# ======
- self.buildList = QListWidget()
+ self.buildList = QListWidget(self)
self.buildList.setIconSize(QSize(iPx, iPx))
self.buildList.doubleClicked.connect(self._editSelectedBuild)
self.buildList.currentItemChanged.connect(self._updateBuildDetails)
self.buildList.setSelectionMode(QAbstractItemView.SingleSelection)
self.buildList.setDragDropMode(QAbstractItemView.InternalMove)
+ # Details Tabs
+ # ============
+
self.buildDetails = _DetailsWidget(self)
self.buildDetails.setColumnWidth(
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsWidth", 100)),
)
+ self.detailsTabs = QTabWidget(self)
+ self.detailsTabs.addTab(self.buildDetails, self.tr("Build"))
+ self.detailsTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS))
+
self.buildSplit = QSplitter(Qt.Orientation.Vertical, self)
self.buildSplit.addWidget(self.buildList)
- self.buildSplit.addWidget(self.buildDetails)
+ self.buildSplit.addWidget(self.detailsTabs)
self.buildSplit.setSizes([
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "listHeight", 50)),
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsHeight", 50)),
@@ -882,19 +884,13 @@ class _StatsWidget(QWidget):
self.minWidget = QWidget(self)
self.maxWidget = QWidget(self)
- fPx = int(0.6*SHARED.theme.fontPixelSize)
- toggleIcon = SHARED.theme.getToggleIcon("unfold", (fPx, fPx))
+ iPx = int(0.6*SHARED.theme.baseIconSize)
+ toggleIcon = SHARED.theme.getToggleIcon("unfold", (iPx, iPx))
- fadeCol = self.palette().text().color()
- buttonStyle = (
- "QToolButton {{padding: 0; border: none; background: transparent;}} "
- "QToolButton:hover {{border: none; background: rgba({0},{1},{2},0.2);}}"
- ).format(fadeCol.red(), fadeCol.green(), fadeCol.blue())
-
- self.toggleButton = QToolButton(self)
+ self.toggleButton = NIconToolButton(self, iPx)
self.toggleButton.setCheckable(True)
self.toggleButton.setIcon(toggleIcon)
- self.toggleButton.setStyleSheet(buttonStyle)
+ self.toggleButton.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_MIN_TOOLBUTTON))
self.toggleButton.toggled.connect(self._toggleView)
self._buildMinimal()
From ace1df733668055bc93d22dac67eccc476def209 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 20 Mar 2024 17:27:17 +0100
Subject: [PATCH 2/9] Increase size of some small button icons
---
novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg | 2 +-
novelwriter/assets/icons/typicons_dark/typ_refresh.svg | 2 +-
novelwriter/assets/icons/typicons_dark/typ_times.svg | 2 +-
novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg | 2 +-
novelwriter/assets/icons/typicons_light/typ_refresh.svg | 2 +-
novelwriter/assets/icons/typicons_light/typ_times.svg | 2 +-
6 files changed, 6 insertions(+), 6 deletions(-)
diff --git a/novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg b/novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg
index 19f19130..09b422a8 100644
--- a/novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg
+++ b/novelwriter/assets/icons/typicons_dark/typ_refresh-flipped.svg
@@ -1,4 +1,4 @@
diff --git a/novelwriter/assets/icons/typicons_dark/typ_refresh.svg b/novelwriter/assets/icons/typicons_dark/typ_refresh.svg
index 99466ce7..6a2c5548 100644
--- a/novelwriter/assets/icons/typicons_dark/typ_refresh.svg
+++ b/novelwriter/assets/icons/typicons_dark/typ_refresh.svg
@@ -1,4 +1,4 @@
diff --git a/novelwriter/assets/icons/typicons_dark/typ_times.svg b/novelwriter/assets/icons/typicons_dark/typ_times.svg
index dbd18f8d..4d086668 100644
--- a/novelwriter/assets/icons/typicons_dark/typ_times.svg
+++ b/novelwriter/assets/icons/typicons_dark/typ_times.svg
@@ -1,4 +1,4 @@
diff --git a/novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg b/novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg
index 0987b6a5..f59e7a83 100644
--- a/novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg
+++ b/novelwriter/assets/icons/typicons_light/typ_refresh-flipped.svg
@@ -1,4 +1,4 @@
diff --git a/novelwriter/assets/icons/typicons_light/typ_refresh.svg b/novelwriter/assets/icons/typicons_light/typ_refresh.svg
index 5020f2a0..78eb2622 100644
--- a/novelwriter/assets/icons/typicons_light/typ_refresh.svg
+++ b/novelwriter/assets/icons/typicons_light/typ_refresh.svg
@@ -1,4 +1,4 @@
diff --git a/novelwriter/assets/icons/typicons_light/typ_times.svg b/novelwriter/assets/icons/typicons_light/typ_times.svg
index a1cbf8f5..dc905456 100644
--- a/novelwriter/assets/icons/typicons_light/typ_times.svg
+++ b/novelwriter/assets/icons/typicons_light/typ_times.svg
@@ -1,4 +1,4 @@
From bcadc40998eb4c217f1ff4594406c80d8f50b28a Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 20 Mar 2024 19:37:55 +0100
Subject: [PATCH 3/9] Make preview mode in HTML build also usable by Manuscript
tool
---
novelwriter/core/docbuild.py | 1 +
novelwriter/core/tohtml.py | 15 ++++-----------
novelwriter/gui/docviewer.py | 5 ++++-
tests/test_core/test_core_tohtml.py | 14 ++------------
4 files changed, 11 insertions(+), 24 deletions(-)
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index fec7aa97..534cdd6c 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -170,6 +170,7 @@ class NWBuildDocument:
makeObj = ToHtml(self._project)
filtered = self._setupBuild(makeObj)
+ makeObj.setPreview(self._preview)
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index 58b7d806..da2507bd 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -74,15 +74,9 @@ class ToHtml(Tokenizer):
# Setters
##
- def setPreview(self, doComments: bool, doSynopsis: bool) -> None:
- """If we're using this class to generate markdown preview, we
- need to make a few changes to formatting, which is managed by
- these flags.
- """
- self._genMode = self.M_PREVIEW
- self._doKeywords = True
- self._doComments = doComments
- self._doSynopsis = doSynopsis
+ def setPreview(self, state: bool) -> None:
+ """Set to preview generator mode."""
+ self._genMode = self.M_PREVIEW if state else self.M_EXPORT
return
def setStyles(self, cssStyles: bool) -> None:
@@ -302,8 +296,7 @@ class ToHtml(Tokenizer):
lines.append(tTemp)
self._result = "".join(lines)
- if self._genMode != self.M_PREVIEW:
- self._fullHTML.append(self._result)
+ self._fullHTML.append(self._result)
return
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 891af3e6..701edafd 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -197,7 +197,10 @@ class GuiDocViewer(QTextBrowser):
sPos = self.verticalScrollBar().value()
aDoc = ToHtml(SHARED.project)
- aDoc.setPreview(CONFIG.viewComments, CONFIG.viewSynopsis)
+ aDoc.setPreview(True)
+ aDoc.setKeywords(True)
+ aDoc.setComments(CONFIG.viewComments)
+ aDoc.setSynopsis(CONFIG.viewSynopsis)
aDoc.setLinkHeadings(True)
# Be extra careful here to prevent crashes when first opening a
diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py
index beb837e0..9f656ca4 100644
--- a/tests/test_core/test_core_tohtml.py
+++ b/tests/test_core/test_core_tohtml.py
@@ -247,7 +247,7 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
# Preview Mode
# ============
- html.setPreview(True, True)
+ html.setPreview(True)
# Text (HTML4)
html._text = "Some **nested bold and _italic_ and ~~strikethrough~~ text** here\n"
@@ -606,16 +606,6 @@ def testCoreToHtml_Methods(mockGUI):
"
Text with <brackets> & short–dash, long—dash …
\n"
)
- # With Preview
- html.setPreview(True, True)
- html._text = docText
- html.doPreProcessing()
- html.tokenizeText()
- html.doConvert()
- assert html.allMarkdown[-1] == (
- "Text with & short–dash, long—dash …\n\n"
- )
-
# Result Size
assert html.getFullResultSize() == 147
@@ -669,7 +659,7 @@ def testCoreToHtml_Format(mockGUI):
# Preview Mode
# ============
- html.setPreview(True, True)
+ html.setPreview(True)
assert html._formatSynopsis("synopsis text", True) == (
"\n"
From d42074f8ea50721980f87c75a55a40ae90b8f591 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 20 Mar 2024 20:43:11 +0100
Subject: [PATCH 4/9] Extend HTML anchor to include handle
---
novelwriter/core/docbuild.py | 1 +
novelwriter/core/tohtml.py | 21 +++++++++++++++++++--
novelwriter/core/tokenizer.py | 4 ++--
novelwriter/gui/docviewer.py | 5 +++--
novelwriter/guimain.py | 2 +-
tests/test_core/test_core_tokenizer.py | 4 ++--
6 files changed, 28 insertions(+), 9 deletions(-)
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index 534cdd6c..ce5afdd7 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -171,6 +171,7 @@ class NWBuildDocument:
filtered = self._setupBuild(makeObj)
makeObj.setPreview(self._preview)
+ makeObj.setLinkHeadings(self._preview)
for i, tHandle in enumerate(self._queue):
self._error = None
if filtered.get(tHandle, (False, 0))[0]:
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index da2507bd..ed791223 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -55,6 +55,7 @@ class ToHtml(Tokenizer):
self._genMode = self.M_EXPORT
self._cssStyles = True
self._fullHTML: list[str] = []
+ self._navMap: dict[str, str] = {}
# Internals
self._trMap = {}
@@ -70,6 +71,10 @@ class ToHtml(Tokenizer):
def fullHTML(self) -> list[str]:
return self._fullHTML
+ @property
+ def navigationMap(self) -> dict[str, str]:
+ return self._navMap
+
##
# Setters
##
@@ -227,9 +232,11 @@ class ToHtml(Tokenizer):
else:
hStyle = ""
- if self._linkHeaders:
- aNm = f""
+ if self._linkHeadings and self._nwItem:
+ tHH = f"{self._nwItem.itemHandle}:T{nHead:04d}"
+ aNm = f""
else:
+ tHH = ""
aNm = ""
# Process Text Type
@@ -249,22 +256,32 @@ class ToHtml(Tokenizer):
elif tType == self.T_TITLE:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"{aNm}{tHead}
\n")
+ if tHH:
+ self._navMap[tHH] = f"TT {tHead}"
elif tType == self.T_HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}{h1}>\n")
+ if tHH:
+ self._navMap[tHH] = f"H1 {tHead}"
elif tType == self.T_HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"<{h2}{hStyle}>{aNm}{tHead}{h2}>\n")
+ if tHH:
+ self._navMap[tHH] = f"H2 {tHead}"
elif tType == self.T_HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"<{h3}{hStyle}>{aNm}{tHead}{h3}>\n")
+ if tHH:
+ self._navMap[tHH] = f"H3 {tHead}"
elif tType == self.T_HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"<{h4}{hStyle}>{aNm}{tHead}{h4}>\n")
+ if tHH:
+ self._navMap[tHH] = f"H4 {tHead}"
elif tType == self.T_SEP:
lines.append(f"{tText}
\n")
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index ac44471c..15de37be 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -165,7 +165,7 @@ class Tokenizer(ABC):
self._hideHScene = False # Do not include hard scene headings
self._hideSection = False # Do not include section headings
- self._linkHeaders = False # Add an anchor before headings
+ self._linkHeadings = False # Add an anchor before headings
self._titleStyle = self.A_CENTRE | self.A_PBB
self._chapterStyle = self.A_PBB
@@ -353,7 +353,7 @@ class Tokenizer(ABC):
def setLinkHeadings(self, state: bool) -> None:
"""Enable or disable adding an anchor before headings."""
- self._linkHeaders = state
+ self._linkHeadings = state
return
def setBodyText(self, state: bool) -> None:
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index 701edafd..ac123ab9 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -732,7 +732,8 @@ class GuiDocViewHeader(QWidget):
def setOutline(self, data: dict[int, tuple[str, int]]) -> None:
"""Set the document outline dataset."""
- if data != self._docOutline:
+ tHandle = self._docHandle
+ if data != self._docOutline and tHandle:
self.outlineMenu.clear()
entries = []
minLevel = 5
@@ -744,7 +745,7 @@ class GuiDocViewHeader(QWidget):
indent = " "*(level - minLevel)
action = self.outlineMenu.addAction(f"{indent}{text}")
action.triggered.connect(
- lambda _, title=title: self.docViewer.navigateTo(f"#{title}")
+ lambda _, title=title: self.docViewer.navigateTo(f"#{tHandle}:{title}")
)
self._docOutline = data
return
diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py
index baf881c7..21db48b6 100644
--- a/novelwriter/guimain.py
+++ b/novelwriter/guimain.py
@@ -653,7 +653,7 @@ class GuiMain(QMainWindow):
self.docEditor.ensureCursorVisibleNoCentre()
if sTitle:
- self.docViewer.navigateTo(f"#{sTitle}")
+ self.docViewer.navigateTo(f"#{tHandle}:{sTitle}")
return True
diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py
index b3b69774..30558c9c 100644
--- a/tests/test_core/test_core_tokenizer.py
+++ b/tests/test_core/test_core_tokenizer.py
@@ -68,7 +68,7 @@ def testCoreToken_Setters(mockGUI):
assert tokens._hideScene is False
assert tokens._hideHScene is False
assert tokens._hideSection is False
- assert tokens._linkHeaders is False
+ assert tokens._linkHeadings is False
assert tokens._doBodyText is True
assert tokens._doSynopsis is False
assert tokens._doComments is False
@@ -124,7 +124,7 @@ def testCoreToken_Setters(mockGUI):
assert tokens._hideScene is True
assert tokens._hideHScene is True
assert tokens._hideSection is True
- assert tokens._linkHeaders is True
+ assert tokens._linkHeadings is True
assert tokens._doBodyText is False
assert tokens._doSynopsis is True
assert tokens._doComments is True
From 46c8bedd66e1277a133a14c8b158b6a7a435a26d Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 20 Mar 2024 20:43:31 +0100
Subject: [PATCH 5/9] Add outline table to Manuscript tool
---
novelwriter/tools/manuscript.py | 113 +++++++++++++++++++++++++++++---
1 file changed, 104 insertions(+), 9 deletions(-)
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 316c2541..b213acb2 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -31,7 +31,7 @@ from typing import TYPE_CHECKING
from datetime import datetime
from PyQt5.QtGui import QCloseEvent, QColor, QCursor, QFont, QPalette, QResizeEvent
-from PyQt5.QtCore import QSize, QTimer, Qt, pyqtSlot
+from PyQt5.QtCore import QSize, QTimer, QUrl, Qt, pyqtSignal, pyqtSlot
from PyQt5.QtWidgets import (
QAbstractItemView, QDialog, QFormLayout, QGridLayout, QHBoxLayout, QLabel,
QListWidget, QListWidgetItem, QPushButton, QSizePolicy, QSplitter,
@@ -58,6 +58,14 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__)
+OUTLINE_MAP = {
+ "TT": 0,
+ "H1": 1,
+ "H2": 2,
+ "H3": 3,
+ "H4": 4,
+}
+
class GuiManuscript(QDialog):
"""GUI Tools: Manuscript Tool
@@ -151,8 +159,11 @@ class GuiManuscript(QDialog):
CONFIG.pxInt(pOptions.getInt("GuiManuscript", "detailsWidth", 100)),
)
+ self.buildOutline = _OutlineWidget(self)
+
self.detailsTabs = QTabWidget(self)
self.detailsTabs.addTab(self.buildDetails, self.tr("Build"))
+ self.detailsTabs.addTab(self.buildOutline, self.tr("Outline"))
self.detailsTabs.setStyleSheet(SHARED.theme.getStyleSheet(STYLES_FLAT_TABS))
self.buildSplit = QSplitter(Qt.Orientation.Vertical, self)
@@ -225,6 +236,9 @@ class GuiManuscript(QDialog):
self.setLayout(self.outerBox)
self.setSizeGripEnabled(True)
+ # Signals
+ self.buildOutline.outlineEntryClicked.connect(self.docPreview.navigateTo)
+
logger.debug("Ready: GuiManuscript")
return
@@ -354,6 +368,7 @@ class GuiManuscript(QDialog):
"uuid": build.buildID,
"time": int(time()),
"stats": buildObj.textStats,
+ "outline": buildObj.navigationMap,
"styles": buildObj.getStyleSheet(),
"html": buildObj.fullHTML,
}
@@ -410,6 +425,7 @@ class GuiManuscript(QDialog):
build.getBool("format.justifyText")
)
self.docStats.updateStats(data.get("stats", {}))
+ self.buildOutline.updateOutline(data.get("outline", {}))
return
def _getSelectedBuild(self) -> BuildSettings | None:
@@ -519,7 +535,7 @@ class _DetailsWidget(QWidget):
self.listView = QTreeWidget(self)
self.listView.setHeaderLabels([self.tr("Setting"), self.tr("Value")])
self.listView.setIndentation(SHARED.theme.baseIconSize)
- self.listView.setSelectionMode(QAbstractItemView.NoSelection)
+ self.listView.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
# Assemble
self.outerBox = QVBoxLayout()
@@ -651,6 +667,80 @@ class _DetailsWidget(QWidget):
# END Class _DetailsWidget
+class _OutlineWidget(QWidget):
+
+ D_LINE = Qt.ItemDataRole.UserRole
+
+ outlineEntryClicked = pyqtSignal(str)
+
+ def __init__(self, parent: QWidget) -> None:
+ super().__init__(parent=parent)
+
+ self._outline = {}
+
+ # Tree Widget
+ self.listView = QTreeWidget(self)
+ self.listView.setHeaderHidden(True)
+ self.listView.setSelectionMode(QAbstractItemView.SelectionMode.NoSelection)
+ self.listView.itemClicked.connect(self._onItemClick)
+
+ # Assemble
+ self.outerBox = QVBoxLayout()
+ self.outerBox.addWidget(self.listView)
+ self.outerBox.setContentsMargins(0, 0, 0, 0)
+ self.setLayout(self.outerBox)
+
+ return
+
+ def updateOutline(self, data: dict[str, str]) -> None:
+ """Update the outline."""
+ if isinstance(data, dict) and data != self._outline:
+ self.listView.clear()
+
+ tFont = self.font()
+ tFont.setBold(True)
+ tBrush = self.palette().highlight()
+
+ hFont = self.font()
+ hFont.setBold(True)
+ hFont.setUnderline(True)
+
+ root = self.listView.invisibleRootItem()
+ parent = root
+ for anchor, text in data.items():
+ level = OUTLINE_MAP.get(text[:2], -1)
+ text = text[3:]
+ if 0 <= level < 4:
+ item = QTreeWidgetItem([text])
+ item.setData(0, self.D_LINE, anchor)
+ if level == 0:
+ item.setFont(0, tFont)
+ item.setForeground(0, tBrush)
+ elif level == 1:
+ item.setFont(0, hFont)
+
+ if level < 3:
+ root.addChild(item)
+ parent = item
+ elif parent:
+ parent.addChild(item)
+
+ self._outline = data
+
+ return
+
+ ##
+ # Private Slots
+ ##
+
+ def _onItemClick(self, item: QTreeWidgetItem) -> None:
+ """Process tree item click."""
+ self.outlineEntryClicked.emit(str(item.data(0, self.D_LINE)))
+ return
+
+# END Class _OutlineWidget
+
+
class _PreviewWidget(QTextBrowser):
def __init__(self, parent: QWidget) -> None:
@@ -690,7 +780,7 @@ class _PreviewWidget(QTextBrowser):
self.ageLabel.setFont(aFont)
self.ageLabel.setPalette(aPalette)
self.ageLabel.setAutoFillBackground(True)
- self.ageLabel.setAlignment(Qt.AlignCenter)
+ self.ageLabel.setAlignment(Qt.AlignmentFlag.AlignCenter)
self.ageLabel.setFixedHeight(int(2.1*SHARED.theme.fontPixelSize))
# Progress
@@ -708,7 +798,7 @@ class _PreviewWidget(QTextBrowser):
self._updateBuildAge()
# Age Timer
- self.ageTimer = QTimer()
+ self.ageTimer = QTimer(self)
self.ageTimer.setInterval(10)
self.ageTimer.timeout.connect(self._updateBuildAge)
self.ageTimer.start()
@@ -729,9 +819,9 @@ class _PreviewWidget(QTextBrowser):
"""Enable/disable the justify text option."""
pOptions = self.document().defaultTextOption()
if state:
- pOptions.setAlignment(Qt.AlignJustify)
+ pOptions.setAlignment(Qt.AlignmentFlag.AlignJustify)
else:
- pOptions.setAlignment(Qt.AlignAbsolute)
+ pOptions.setAlignment(Qt.AlignmentFlag.AlignAbsolute)
self.document().setDefaultTextOption(pOptions)
return
@@ -767,7 +857,7 @@ class _PreviewWidget(QTextBrowser):
def setContent(self, data: dict) -> None:
"""Set the content of the preview widget."""
sPos = self.verticalScrollBar().value()
- qApp.setOverrideCursor(QCursor(Qt.WaitCursor))
+ qApp.setOverrideCursor(QCursor(Qt.CursorShape.WaitCursor))
self.buildProgress.setCentreText(self.tr("Processing ..."))
qApp.processEvents()
@@ -777,8 +867,6 @@ class _PreviewWidget(QTextBrowser):
html = "".join(data.get("html", []))
html = html.replace("\t", "!!tab!!")
- html = html.replace("", "")
- html = html.replace("", "")
self.setHtml(html)
qApp.processEvents()
while self.find("!!tab!!"):
@@ -823,6 +911,13 @@ class _PreviewWidget(QTextBrowser):
qApp.restoreOverrideCursor()
return
+ @pyqtSlot(str)
+ def navigateTo(self, anchor: str) -> None:
+ """Go to a specific #link in the document."""
+ logger.debug("Moving to anchor '#%s'", anchor)
+ self.setSource(QUrl(f"#{anchor}"))
+ return
+
##
# Private Slots
##
From 144ba66d9d7f55946d29b1e1c531dfe78588f3bd Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Wed, 20 Mar 2024 23:44:25 +0100
Subject: [PATCH 6/9] Remove redundant variables in tokenizer
---
novelwriter/core/tohtml.py | 14 ++++----
novelwriter/core/tokenizer.py | 31 +++++++----------
novelwriter/tools/manuscript.py | 3 ++
tests/test_core/test_core_tohtml.py | 22 ++++++------
tests/test_core/test_core_tokenizer.py | 47 +++-----------------------
tests/test_core/test_core_tomd.py | 3 --
6 files changed, 38 insertions(+), 82 deletions(-)
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index ed791223..ad548aa6 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -232,8 +232,8 @@ class ToHtml(Tokenizer):
else:
hStyle = ""
- if self._linkHeadings and self._nwItem:
- tHH = f"{self._nwItem.itemHandle}:T{nHead:04d}"
+ if self._linkHeadings and self._handle:
+ tHH = f"{self._handle}:T{nHead:04d}"
aNm = f""
else:
tHH = ""
@@ -257,31 +257,31 @@ class ToHtml(Tokenizer):
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"{aNm}{tHead}
\n")
if tHH:
- self._navMap[tHH] = f"TT {tHead}"
+ self._navMap[tHH] = f"TT:{tHead}"
elif tType == self.T_HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}{h1}>\n")
if tHH:
- self._navMap[tHH] = f"H1 {tHead}"
+ self._navMap[tHH] = f"H1:{tHead}"
elif tType == self.T_HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"<{h2}{hStyle}>{aNm}{tHead}{h2}>\n")
if tHH:
- self._navMap[tHH] = f"H2 {tHead}"
+ self._navMap[tHH] = f"H2:{tHead}"
elif tType == self.T_HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"<{h3}{hStyle}>{aNm}{tHead}{h3}>\n")
if tHH:
- self._navMap[tHH] = f"H3 {tHead}"
+ self._navMap[tHH] = f"H3:{tHead}"
elif tType == self.T_HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"<{h4}{hStyle}>{aNm}{tHead}{h4}>\n")
if tHH:
- self._navMap[tHH] = f"H4 {tHead}"
+ self._navMap[tHH] = f"H4:{tHead}"
elif tType == self.T_SEP:
lines.append(f"{tText}
\n")
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index 15de37be..dd30cfc6 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -118,7 +118,7 @@ class Tokenizer(ABC):
# Data Variables
self._text = "" # The raw text to be tokenized
- self._nwItem = None # The NWItem currently being processed
+ self._handle = None # The item handle currently being processed
self._result = "" # The result of the last document
self._counts = {} # Counter data
@@ -176,9 +176,7 @@ class Tokenizer(ABC):
self._noSep = True # Flag to indicate that we don't want a scene separator
# This File
- self._isNone = False # Document has unknown layout
self._isNovel = False # Document is a novel document
- self._isNote = False # Document is a project note
self._isFirst = True # Document is the first in a set
# Error Handling
@@ -417,24 +415,21 @@ class Tokenizer(ABC):
return True
- def setText(self, tHandle: str, text: str | None = None) -> bool:
+ def setText(self, tHandle: str, text: str | None = None) -> None:
"""Set the text for the tokenizer from a handle. If text is not
- set, load it from the file.
+ set, its is loaded from the file.
"""
- self._nwItem = self._project.tree[tHandle]
- if self._nwItem is None:
- return False
+ self._text = ""
+ self._handle = None
+ if nwItem := self._project.tree[tHandle]:
+ if text is None:
+ text = self._project.storage.getDocument(tHandle).readDocument() or ""
- if text is None:
- text = self._project.storage.getDocument(tHandle).readDocument() or ""
+ self._text = text
+ self._handle = tHandle
+ self._isNovel = nwItem.itemLayout == nwItemLayout.DOCUMENT
- self._text = text
-
- self._isNone = self._nwItem.itemLayout == nwItemLayout.NO_LAYOUT
- self._isNovel = self._nwItem.itemLayout == nwItemLayout.DOCUMENT
- self._isNote = self._nwItem.itemLayout == nwItemLayout.NOTE
-
- return True
+ return
def doPreProcessing(self) -> None:
"""Run trough the various replace dictionaries."""
@@ -470,7 +465,7 @@ class Tokenizer(ABC):
"""
self._tokens = []
if self._isNovel:
- self._hFormatter.setHandle(self._nwItem.itemHandle if self._nwItem else None)
+ self._hFormatter.setHandle(self._handle)
nHead = 0
breakNext = False
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index b213acb2..906b37d5 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -707,6 +707,7 @@ class _OutlineWidget(QWidget):
root = self.listView.invisibleRootItem()
parent = root
+ indent = False
for anchor, text in data.items():
level = OUTLINE_MAP.get(text[:2], -1)
text = text[3:]
@@ -724,7 +725,9 @@ class _OutlineWidget(QWidget):
parent = item
elif parent:
parent.addChild(item)
+ indent = True
+ self.listView.setIndentation(SHARED.theme.baseIconSize if indent else CONFIG.pxInt(4))
self._outline = data
return
diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py
index 9f656ca4..30aefd0a 100644
--- a/tests/test_core/test_core_tohtml.py
+++ b/tests/test_core/test_core_tohtml.py
@@ -38,7 +38,6 @@ def testCoreToHtml_ConvertHeaders(mockGUI):
# ===================
html._isNovel = True
- html._isNote = False
html._isFirst = True
# Header 1
@@ -87,33 +86,33 @@ def testCoreToHtml_ConvertHeaders(mockGUI):
# ==================
html._isNovel = False
- html._isNote = True
html._isFirst = True
+ html._handle = "0000000000000"
html.setLinkHeadings(True)
# Header 1
html._text = "# Heading One\n"
html.tokenizeText()
html.doConvert()
- assert html.result == "Heading One
\n"
+ assert html.result == "Heading One
\n"
# Header 2
html._text = "## Heading Two\n"
html.tokenizeText()
html.doConvert()
- assert html.result == "Heading Two
\n"
+ assert html.result == "Heading Two
\n"
# Header 3
html._text = "### Heading Three\n"
html.tokenizeText()
html.doConvert()
- assert html.result == "Heading Three
\n"
+ assert html.result == "Heading Three
\n"
# Header 4
html._text = "#### Heading Four\n"
html.tokenizeText()
html.doConvert()
- assert html.result == "Heading Four
\n"
+ assert html.result == "Heading Four
\n"
# Title
html._text = "#! Heading One\n"
@@ -121,14 +120,14 @@ def testCoreToHtml_ConvertHeaders(mockGUI):
html.doConvert()
assert html.result == (
""
- "Heading One
\n"
+ "Heading One\n"
)
# Unnumbered
html._text = "##! Heading Two\n"
html.tokenizeText()
html.doConvert()
- assert html.result == "Heading Two
\n"
+ assert html.result == "Heading Two
\n"
# END Test testCoreToHtml_ConvertHeaders
@@ -140,7 +139,6 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
html = ToHtml(project)
html._isNovel = True
- html._isNote = False
html._isFirst = True
# Paragraphs
@@ -269,7 +267,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
html = ToHtml(project)
html._isNovel = True
- html._isNote = False
+ html._handle = "0000000000000"
html.setLinkHeadings(True)
# Special Titles
@@ -283,7 +281,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
html.doConvert()
assert html.result == (
""
- "A Title
\n"
+ "A Title\n"
)
# Unnumbered
@@ -294,7 +292,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
html.doConvert()
assert html.result == (
""
- "Prologue
\n"
+ "Prologue\n"
)
# Separators
diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py
index 30558c9c..68ecfdf7 100644
--- a/tests/test_core/test_core_tokenizer.py
+++ b/tests/test_core/test_core_tokenizer.py
@@ -199,16 +199,16 @@ def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath):
)
# Set Text
- assert tokens.setText("stuff") is False
- assert tokens.setText(C.hSceneDoc) is True
+ tokens.setText("stuff")
+ assert tokens._text == ""
+
+ tokens.setText(C.hSceneDoc)
assert tokens._text == docText
- assert tokens.setText(C.hSceneDoc, docText) is True
+ tokens.setText(C.hSceneDoc, docText)
assert tokens._text == docText
- assert tokens._isNone is False
assert tokens._isNovel is True
- assert tokens._isNote is False
# Pre Processing
tokens.doPreProcessing()
@@ -259,7 +259,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File
tokens._isNovel = True
- tokens._isNote = False
tokens._isFirst = True
tokens._text = "#! Novel Title\n"
@@ -272,7 +271,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File
tokens._isNovel = False
- tokens._isNote = True
tokens._isFirst = True
tokens._text = "#! Note Title\n"
@@ -288,7 +286,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File
tokens._isNovel = True
- tokens._isNote = False
tokens._isFirst = True
tokens._text = "# Novel Title\n"
@@ -301,7 +298,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File
tokens._isNovel = False
- tokens._isNote = True
tokens._isFirst = True
tokens._text = "# Note Title\n"
@@ -317,7 +313,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File
tokens._isNovel = True
- tokens._isNote = False
tokens._noBreak = False
tokens._text = "## Chapter One\n"
@@ -330,7 +325,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File
tokens._isNovel = False
- tokens._isNote = True
tokens._text = "## Heading 2\n"
tokens.tokenizeText()
@@ -345,7 +339,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File
tokens._isNovel = True
- tokens._isNote = False
tokens._text = "### Scene One\n"
tokens.tokenizeText()
@@ -357,7 +350,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File
tokens._isNovel = False
- tokens._isNote = True
tokens._text = "### Heading 3\n"
tokens.tokenizeText()
@@ -372,7 +364,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File
tokens._isNovel = True
- tokens._isNote = False
tokens._text = "#### A Section\n"
tokens.tokenizeText()
@@ -384,7 +375,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File
tokens._isNovel = False
- tokens._isNote = True
tokens._text = "#### Heading 4\n"
tokens.tokenizeText()
@@ -399,7 +389,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File
tokens._isNovel = True
- tokens._isNote = False
tokens._isFirst = False
tokens._text = "#! Title\n"
@@ -412,7 +401,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File
tokens._isNovel = False
- tokens._isNote = True
tokens._isFirst = False
tokens._text = "#! Title\n"
@@ -428,7 +416,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File
tokens._isNovel = True
- tokens._isNote = False
tokens._text = "##! Prologue\n"
tokens.tokenizeText()
@@ -440,7 +427,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Note File
tokens._isNovel = False
- tokens._isNote = True
tokens._text = "##! Prologue\n"
tokens.tokenizeText()
@@ -479,7 +465,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Novel Docs
tokens._isNovel = True
- tokens._isNote = False
# First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_NONE
@@ -499,7 +484,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Note Docs
tokens._isNovel = False
- tokens._isNote = True
# First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_NONE
@@ -530,7 +514,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Novel Docs
tokens._isNovel = True
- tokens._isNote = False
# First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_CENTRE
@@ -550,7 +533,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Note Docs
tokens._isNovel = False
- tokens._isNote = True
# First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_NONE
@@ -581,7 +563,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Novel Docs
tokens._isNovel = True
- tokens._isNote = False
# First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_PBB
@@ -601,7 +582,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Note Docs
tokens._isNovel = False
- tokens._isNote = True
# First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_NONE
@@ -632,7 +612,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Novel Docs
tokens._isNovel = True
- tokens._isNote = False
# First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_CENTRE | Tokenizer.A_PBB
@@ -652,7 +631,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Note Docs
tokens._isNovel = False
- tokens._isNote = True
# First Document is False
assert processStyle("# Title\n", False) == Tokenizer.A_NONE
@@ -673,7 +651,6 @@ def testCoreToken_HeaderStyle(mockGUI):
# Check Separation
# ================
tokens._isNovel = True
- tokens._isNote = False
# Title Styles
tokens.setTitleStyle(True, True)
@@ -1310,8 +1287,6 @@ def testCoreToken_ProcessHeaders(mockGUI):
# Story Files
##
- tokens._isNone = False
- tokens._isNote = False
tokens._isNovel = True
# Titles
@@ -1530,8 +1505,6 @@ def testCoreToken_CountStats(mockGUI, ipsumText):
project.data.setLanguage("en")
project._loadProjectLocalisation()
tokens = BareTokenizer(project)
- tokens._isNone = False
- tokens._isNote = False
tokens._isNovel = True
# Short Text
@@ -1740,8 +1713,6 @@ def testCoreToken_SceneSeparators(mockGUI):
project.data.setLanguage("en")
project._loadProjectLocalisation()
md = ToMarkdown(project)
- md._isNone = False
- md._isNote = False
md._isNovel = True
# Separator Handling, Titles
@@ -1858,8 +1829,6 @@ def testCoreToken_SceneSeparators(mockGUI):
# Requires a fresh builder class
md = ToMarkdown(project)
md.setExtendedMarkdown()
- md._isNone = False
- md._isNote = False
md._isNovel = True
md._text = (
@@ -1919,8 +1888,6 @@ def testCoreToken_HeaderVisibility(mockGUI):
# Novel Files
# ===========
- md._isNone = False
- md._isNote = False
md._isNovel = True
# Show All
@@ -1973,8 +1940,6 @@ def testCoreToken_HeaderVisibility(mockGUI):
# Note Files
# ==========
- md._isNone = False
- md._isNote = True
md._isNovel = False
# Hide All
@@ -2015,8 +1980,6 @@ def testCoreToken_CounterHandling(mockGUI):
project.data.setLanguage("en")
project._loadProjectLocalisation()
md = ToMarkdown(project)
- md._isNone = False
- md._isNote = False
md._isNovel = True
# Counter Handling, Novel Titles
diff --git a/tests/test_core/test_core_tomd.py b/tests/test_core/test_core_tomd.py
index c67caf63..2192517f 100644
--- a/tests/test_core/test_core_tomd.py
+++ b/tests/test_core/test_core_tomd.py
@@ -35,7 +35,6 @@ def testCoreToMarkdown_ConvertHeaders(mockGUI):
toMD = ToMarkdown(project)
toMD._isNovel = True
- toMD._isNote = False
toMD._isFirst = True
# Header 1
@@ -84,7 +83,6 @@ def testCoreToMarkdown_ConvertParagraphs(mockGUI):
toMD = ToMarkdown(project)
toMD._isNovel = True
- toMD._isNote = False
toMD._isFirst = True
# Text for Extended Markdown
@@ -200,7 +198,6 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
toMD = ToMarkdown(project)
toMD._isNovel = True
- toMD._isNote = False
# Special Titles
# ==============
From 5befd244e68c0a8e7cbc6fbb80c9ce54c88a93e3 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 21 Mar 2024 16:42:58 +0100
Subject: [PATCH 7/9] Move outline generation to Tokenizer class instead
---
novelwriter/core/docbuild.py | 22 +++++++--
novelwriter/core/index.py | 8 ++--
novelwriter/core/project.py | 4 +-
novelwriter/core/sessions.py | 4 +-
novelwriter/core/status.py | 8 ++--
novelwriter/core/tohtml.py | 23 ++-------
novelwriter/core/tokenizer.py | 79 +++++++++++++++++++++----------
novelwriter/core/tree.py | 4 +-
novelwriter/gui/docviewer.py | 10 ++--
novelwriter/gui/editordocument.py | 4 +-
novelwriter/tools/manuscript.py | 30 +++++-------
11 files changed, 109 insertions(+), 87 deletions(-)
diff --git a/novelwriter/core/docbuild.py b/novelwriter/core/docbuild.py
index ce5afdd7..3c1d6951 100644
--- a/novelwriter/core/docbuild.py
+++ b/novelwriter/core/docbuild.py
@@ -52,7 +52,10 @@ class NWBuildDocument:
manuscript, based on a build definition object (BuildSettings).
"""
- __slots__ = ("_project", "_build", "_queue", "_error", "_cache", "_count", "_preview")
+ __slots__ = (
+ "_project", "_build", "_queue", "_error", "_cache", "_count",
+ "_outline", "_preview"
+ )
def __init__(self, project: NWProject, build: BuildSettings) -> None:
self._project = project
@@ -61,6 +64,7 @@ class NWBuildDocument:
self._error = None
self._cache = None
self._count = False
+ self._outline = False
self._preview = False
return
@@ -86,13 +90,21 @@ class NWBuildDocument:
##
def setCountEnabled(self, state: bool) -> None:
- """Turn on/off stats counting for builds."""
+ """Turn on/off stats for builds."""
self._count = state
return
+ def setBuildOutline(self, state: bool) -> None:
+ """Turn on/off outline for builds."""
+ self._outline = state
+ return
+
def setPreviewMode(self, state: bool) -> None:
- """Set the preview mode of the build. Implies count mode."""
+ """Set the preview mode of the build. This also enables stats
+ count and outline mode.
+ """
self._preview = state
+ self._outline = state
self._count = state
return
@@ -364,12 +376,16 @@ class NWBuildDocument:
bldObj.doConvert()
if self._count:
bldObj.countStats()
+ if self._outline:
+ bldObj.buildOutline()
elif tItem.isFileType():
bldObj.setText(tHandle)
bldObj.doPreProcessing()
bldObj.tokenizeText()
if self._count:
bldObj.countStats()
+ if self._outline:
+ bldObj.buildOutline()
if convert:
bldObj.doConvert()
else:
diff --git a/novelwriter/core/index.py b/novelwriter/core/index.py
index 66e2130b..7160ea03 100644
--- a/novelwriter/core/index.py
+++ b/novelwriter/core/index.py
@@ -33,7 +33,7 @@ import logging
from time import time
from typing import TYPE_CHECKING
from pathlib import Path
-from collections.abc import Generator, ItemsView, Iterable
+from collections.abc import ItemsView, Iterable
from novelwriter import SHARED
from novelwriter.enum import nwComment, nwItemClass, nwItemType, nwItemLayout
@@ -523,7 +523,7 @@ class NWIndex:
return tItem[sTitle]
return None
- def iterItemHeadings(self, tHandle: str) -> Generator[str, IndexHeading]:
+ def iterItemHeadings(self, tHandle: str) -> Iterable[tuple[str, IndexHeading]]:
"""Get all headings for a specific item."""
if tItem := self._itemIndex[tHandle]:
yield from tItem.items()
@@ -531,7 +531,7 @@ class NWIndex:
def novelStructure(
self, rootHandle: str | None = None, activeOnly: bool = True
- ) -> Generator[tuple[str, str, str, IndexHeading]]:
+ ) -> Iterable[tuple[str, str, str, IndexHeading]]:
"""Iterate over all titles in the novel, in the correct order as
they appear in the tree view and in the respective document
files, but skipping all note files.
@@ -673,7 +673,7 @@ class NWIndex:
def getTagsData(
self, activeOnly: bool = True
- ) -> Generator[tuple[str, str, str, IndexItem | None, IndexHeading | None]]:
+ ) -> Iterable[tuple[str, str, str, IndexItem | None, IndexHeading | None]]:
"""Return all known tags."""
for tag, data in self._tagsIndex.items():
iItem = self._itemIndex[data.get("handle")]
diff --git a/novelwriter/core/project.py b/novelwriter/core/project.py
index b69c29bc..80aee83f 100644
--- a/novelwriter/core/project.py
+++ b/novelwriter/core/project.py
@@ -31,7 +31,7 @@ from time import time
from typing import TYPE_CHECKING
from pathlib import Path
from functools import partial
-from collections.abc import Generator
+from collections.abc import Iterable
from PyQt5.QtCore import QCoreApplication
@@ -517,7 +517,7 @@ class NWProject:
# Class Methods
##
- def iterProjectItems(self) -> Generator[NWItem]:
+ def iterProjectItems(self) -> Iterable[NWItem]:
"""This function ensures that the item tree loaded is sent to
the GUI tree view in such a way that the tree can be built. That
is, the parent item must be sent before its child. In principle,
diff --git a/novelwriter/core/sessions.py b/novelwriter/core/sessions.py
index 5356c6d4..2f79ad20 100644
--- a/novelwriter/core/sessions.py
+++ b/novelwriter/core/sessions.py
@@ -29,7 +29,7 @@ import logging
from time import time
from typing import TYPE_CHECKING
from pathlib import Path
-from collections.abc import Generator
+from collections.abc import Iterable
from novelwriter.error import logException
from novelwriter.common import formatTimeStamp
@@ -110,7 +110,7 @@ class NWSessionLog:
return True
- def iterRecords(self) -> Generator[dict]:
+ def iterRecords(self) -> Iterable[dict]:
"""Iterate through all records in the log."""
sessFile = self._project.storage.getMetaFile(nwFiles.SESS_FILE)
if isinstance(sessFile, Path) and sessFile.is_file():
diff --git a/novelwriter/core/status.py b/novelwriter/core/status.py
index ad8db0df..4b6911f2 100644
--- a/novelwriter/core/status.py
+++ b/novelwriter/core/status.py
@@ -28,10 +28,10 @@ import random
import logging
from typing import TYPE_CHECKING, Literal
-from collections.abc import Generator, ItemsView, Iterator, KeysView, ValuesView
+from collections.abc import ItemsView, Iterable, Iterator, KeysView, ValuesView
from PyQt5.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QColor
-from PyQt5.QtCore import QRectF, Qt
+from PyQt5.QtCore import QRectF
from novelwriter import CONFIG
from novelwriter.common import minmax, simplified
@@ -193,7 +193,7 @@ class NWStatus:
self._store[key]["count"] += 1
return
- def pack(self) -> Generator[tuple[str, dict]]:
+ def pack(self) -> Iterable[tuple[str, dict]]:
"""Pack the status entries into a dictionary."""
for key, data in self._store.items():
yield (data["name"], {
@@ -248,7 +248,7 @@ class NWStatus:
def _createIcon(self, red: int, green: int, blue: int) -> QIcon:
"""Generate an icon for a status label."""
pixmap = QPixmap(self._iPX, self._iPX)
- pixmap.fill(Qt.transparent)
+ pixmap.fill(QColor(0, 0, 0, 0))
painter = QPainter(pixmap)
painter.setRenderHint(QPainter.Antialiasing)
diff --git a/novelwriter/core/tohtml.py b/novelwriter/core/tohtml.py
index ad548aa6..72539c70 100644
--- a/novelwriter/core/tohtml.py
+++ b/novelwriter/core/tohtml.py
@@ -55,7 +55,6 @@ class ToHtml(Tokenizer):
self._genMode = self.M_EXPORT
self._cssStyles = True
self._fullHTML: list[str] = []
- self._navMap: dict[str, str] = {}
# Internals
self._trMap = {}
@@ -71,10 +70,6 @@ class ToHtml(Tokenizer):
def fullHTML(self) -> list[str]:
return self._fullHTML
- @property
- def navigationMap(self) -> dict[str, str]:
- return self._navMap
-
##
# Setters
##
@@ -174,6 +169,8 @@ class ToHtml(Tokenizer):
pStyle = None
lines = []
+ tHandle = self._handle
+
for tType, nHead, tText, tFormat, tStyle in self._tokens:
# Replace < and > with HTML entities
@@ -232,11 +229,9 @@ class ToHtml(Tokenizer):
else:
hStyle = ""
- if self._linkHeadings and self._handle:
- tHH = f"{self._handle}:T{nHead:04d}"
- aNm = f""
+ if self._linkHeadings and tHandle:
+ aNm = f""
else:
- tHH = ""
aNm = ""
# Process Text Type
@@ -256,32 +251,22 @@ class ToHtml(Tokenizer):
elif tType == self.T_TITLE:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"{aNm}{tHead}
\n")
- if tHH:
- self._navMap[tHH] = f"TT:{tHead}"
elif tType == self.T_HEAD1:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"<{h1}{h1Cl}{hStyle}>{aNm}{tHead}{h1}>\n")
- if tHH:
- self._navMap[tHH] = f"H1:{tHead}"
elif tType == self.T_HEAD2:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"<{h2}{hStyle}>{aNm}{tHead}{h2}>\n")
- if tHH:
- self._navMap[tHH] = f"H2:{tHead}"
elif tType == self.T_HEAD3:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"<{h3}{hStyle}>{aNm}{tHead}{h3}>\n")
- if tHH:
- self._navMap[tHH] = f"H3:{tHead}"
elif tType == self.T_HEAD4:
tHead = tText.replace(nwHeadFmt.BR, "
")
lines.append(f"<{h4}{hStyle}>{aNm}{tHead}{h4}>\n")
- if tHH:
- self._navMap[tHH] = f"H4:{tHead}"
elif tType == self.T_SEP:
lines.append(f"{tText}
\n")
diff --git a/novelwriter/core/tokenizer.py b/novelwriter/core/tokenizer.py
index dd30cfc6..9125ba01 100644
--- a/novelwriter/core/tokenizer.py
+++ b/novelwriter/core/tokenizer.py
@@ -35,13 +35,13 @@ from functools import partial
from PyQt5.QtCore import QCoreApplication, QRegularExpression
-from novelwriter.enum import nwComment, nwItemLayout
from novelwriter.common import formatTimeStamp, numberToRoman, checkInt
from novelwriter.constants import (
nwHeadFmt, nwKeyWords, nwLabels, nwRegEx, nwShortcode, nwUnicode, trConst
)
from novelwriter.core.index import processComment
from novelwriter.core.project import NWProject
+from novelwriter.enum import nwComment, nwItemLayout
logger = logging.getLogger(__name__)
@@ -120,13 +120,14 @@ class Tokenizer(ABC):
self._text = "" # The raw text to be tokenized
self._handle = None # The item handle currently being processed
self._result = "" # The result of the last document
- self._counts = {} # Counter data
self._keepMarkdown = False # Whether to keep the markdown text
self._allMarkdown = [] # The result novelWriter markdown of all documents
- # Processed Tokens
+ # Processed Tokens and Meta Data
self._tokens: list[tuple[int, int, str, list[tuple[int, int]], int]] = []
+ self._counts: dict[str, int] = {}
+ self._outline: dict[str, str] = {}
# User Settings
self._textFont = "Serif" # Output text font
@@ -226,6 +227,11 @@ class Tokenizer(ABC):
"""The collected stats about the text."""
return self._counts
+ @property
+ def textOutline(self) -> dict[str, str]:
+ """The generated outline of the text."""
+ return self._outline
+
@property
def errData(self) -> list[str]:
"""The error data."""
@@ -392,43 +398,42 @@ class Tokenizer(ABC):
def doConvert(self) -> None:
raise NotImplementedError
- def addRootHeading(self, tHandle: str) -> bool:
+ def addRootHeading(self, tHandle: str) -> None:
"""Add a heading at the start of a new root folder."""
- tItem = self._project.tree[tHandle]
- if not tItem or not tItem.isRootType():
- return False
+ self._text = ""
+ self._handle = None
- if self._isFirst:
- textAlign = self.A_CENTRE
- self._isFirst = False
- else:
- textAlign = self.A_PBB | self.A_CENTRE
+ if (tItem := self._project.tree[tHandle]) and tItem.isRootType():
+ self._handle = tHandle
+ if self._isFirst:
+ textAlign = self.A_CENTRE
+ self._isFirst = False
+ else:
+ textAlign = self.A_PBB | self.A_CENTRE
- trNotes = self._localLookup("Notes")
- title = f"{trNotes}: {tItem.itemName}"
- self._tokens = []
- self._tokens.append((
- self.T_TITLE, 0, title, [], textAlign
- ))
- if self._keepMarkdown:
- self._allMarkdown.append(f"# {title}\n\n")
+ trNotes = self._localLookup("Notes")
+ title = f"{trNotes}: {tItem.itemName}"
+ self._tokens = []
+ self._tokens.append((
+ self.T_TITLE, 1, title, [], textAlign
+ ))
+ if self._keepMarkdown:
+ self._allMarkdown.append(f"#! {title}\n\n")
- return True
+ return
def setText(self, tHandle: str, text: str | None = None) -> None:
"""Set the text for the tokenizer from a handle. If text is not
- set, its is loaded from the file.
+ set, it's is loaded from the file.
"""
self._text = ""
self._handle = None
if nwItem := self._project.tree[tHandle]:
if text is None:
text = self._project.storage.getDocument(tHandle).readDocument() or ""
-
self._text = text
self._handle = tHandle
self._isNovel = nwItem.itemLayout == nwItemLayout.DOCUMENT
-
return
def doPreProcessing(self) -> None:
@@ -798,7 +803,29 @@ class Tokenizer(ABC):
return
- def countStats(self) -> dict[str, int]:
+ def buildOutline(self) -> None:
+ """Build an outline of the text up to level 3 headings."""
+ tHandle = self._handle or ""
+ isNovel = self._isNovel
+ for tType, nHead, tText, _, _ in self._tokens:
+ if tType == self.T_TITLE:
+ prefix = "TT"
+ elif tType == self.T_HEAD1:
+ prefix = "PT" if isNovel else "H1"
+ elif tType == self.T_HEAD2:
+ prefix = "CH" if isNovel else "H2"
+ elif tType == self.T_HEAD3:
+ prefix = "SC" if isNovel else "H3"
+ else:
+ continue
+
+ key = f"{tHandle}:T{nHead:04d}"
+ text = tText.replace(nwHeadFmt.BR, " ").replace("&", "&")
+ self._outline[key] = f"{prefix}|{text}"
+
+ return
+
+ def countStats(self) -> None:
"""Count stats on the tokenized text."""
titleCount = self._counts.get("titleCount", 0)
paragraphCount = self._counts.get("paragraphCount", 0)
@@ -905,7 +932,7 @@ class Tokenizer(ABC):
self._counts["textWordChars"] = textWordChars
self._counts["titleWordChars"] = titleWordChars
- return {}
+ return
def saveRawMarkdown(self, path: str | Path) -> None:
"""Save the raw text to a plain text file."""
diff --git a/novelwriter/core/tree.py b/novelwriter/core/tree.py
index 4dea1a95..f206fdb2 100644
--- a/novelwriter/core/tree.py
+++ b/novelwriter/core/tree.py
@@ -28,7 +28,7 @@ import logging
from typing import TYPE_CHECKING, Literal, overload
from pathlib import Path
-from collections.abc import Generator, Iterator
+from collections.abc import Iterable, Iterator
from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType
from novelwriter.error import logException
@@ -387,7 +387,7 @@ class NWTree:
rootClasses.add(nwItem.itemClass)
return rootClasses
- def iterRoots(self, itemClass: nwItemClass | None) -> Generator[tuple[str, NWItem]]:
+ def iterRoots(self, itemClass: nwItemClass | None) -> Iterable[tuple[str, NWItem]]:
"""Iterate over all root items of a given class in order."""
for tHandle in self._order:
nwItem = self.__getitem__(tHandle)
diff --git a/novelwriter/gui/docviewer.py b/novelwriter/gui/docviewer.py
index ac123ab9..36b9d80d 100644
--- a/novelwriter/gui/docviewer.py
+++ b/novelwriter/gui/docviewer.py
@@ -41,13 +41,13 @@ from PyQt5.QtWidgets import (
)
from novelwriter import CONFIG, SHARED
+from novelwriter.constants import nwHeaders, nwUnicode
+from novelwriter.core.tohtml import ToHtml
from novelwriter.enum import nwItemType, nwDocAction, nwDocMode
from novelwriter.error import logException
-from novelwriter.constants import nwHeaders, nwUnicode
+from novelwriter.extensions.eventfilters import WheelEventFilter
from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_MIN_TOOLBUTTON
-from novelwriter.core.tohtml import ToHtml
-from novelwriter.extensions.eventfilters import WheelEventFilter
logger = logging.getLogger(__name__)
@@ -633,7 +633,7 @@ class GuiDocViewHeader(QWidget):
# Internal Variables
self._docHandle = None
- self._docOutline: dict[int, tuple[str, int]] = {}
+ self._docOutline: dict[str, tuple[str, int]] = {}
iPx = SHARED.theme.baseIconSize
mPx = CONFIG.pxInt(4)
@@ -730,7 +730,7 @@ class GuiDocViewHeader(QWidget):
self.refreshButton.setVisible(False)
return
- def setOutline(self, data: dict[int, tuple[str, int]]) -> None:
+ def setOutline(self, data: dict[str, tuple[str, int]]) -> None:
"""Set the document outline dataset."""
tHandle = self._docHandle
if data != self._docOutline and tHandle:
diff --git a/novelwriter/gui/editordocument.py b/novelwriter/gui/editordocument.py
index 421e89a0..04babe3f 100644
--- a/novelwriter/gui/editordocument.py
+++ b/novelwriter/gui/editordocument.py
@@ -23,10 +23,10 @@ along with this program. If not, see .
"""
from __future__ import annotations
-from collections.abc import Generator
import logging
from time import time
+from collections.abc import Iterable
from PyQt5.QtGui import QTextBlock, QTextCursor, QTextDocument
from PyQt5.QtCore import QObject, pyqtSlot
@@ -114,7 +114,7 @@ class GuiTextDocument(QTextDocument):
return word, cPos, cLen, SHARED.spelling.suggestWords(word)
return "", -1, -1, []
- def iterBlockByType(self, cType: int, maxCount: int = 1000) -> Generator[QTextBlock]:
+ def iterBlockByType(self, cType: int, maxCount: int = 1000) -> Iterable[QTextBlock]:
"""Iterate over all text blocks of a given type."""
count = 0
for i in range(self.blockCount()):
diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py
index 906b37d5..d9d7342e 100644
--- a/novelwriter/tools/manuscript.py
+++ b/novelwriter/tools/manuscript.py
@@ -58,14 +58,6 @@ if TYPE_CHECKING: # pragma: no cover
logger = logging.getLogger(__name__)
-OUTLINE_MAP = {
- "TT": 0,
- "H1": 1,
- "H2": 2,
- "H3": 3,
- "H4": 4,
-}
-
class GuiManuscript(QDialog):
"""GUI Tools: Manuscript Tool
@@ -368,7 +360,7 @@ class GuiManuscript(QDialog):
"uuid": build.buildID,
"time": int(time()),
"stats": buildObj.textStats,
- "outline": buildObj.navigationMap,
+ "outline": buildObj.textOutline,
"styles": buildObj.getStyleSheet(),
"html": buildObj.fullHTML,
}
@@ -708,22 +700,24 @@ class _OutlineWidget(QWidget):
root = self.listView.invisibleRootItem()
parent = root
indent = False
- for anchor, text in data.items():
- level = OUTLINE_MAP.get(text[:2], -1)
- text = text[3:]
- if 0 <= level < 4:
+ for anchor, entry in data.items():
+ prefix, _, text = entry.partition("|")
+ if prefix in ("TT", "PT", "CH", "SC", "H1", "H2"):
item = QTreeWidgetItem([text])
item.setData(0, self.D_LINE, anchor)
- if level == 0:
+ if prefix == "TT":
item.setFont(0, tFont)
item.setForeground(0, tBrush)
- elif level == 1:
+ root.addChild(item)
+ parent = root
+ elif prefix == "PT":
item.setFont(0, hFont)
-
- if level < 3:
+ root.addChild(item)
+ parent = root
+ elif prefix in ("CH", "H1"):
root.addChild(item)
parent = item
- elif parent:
+ elif prefix in ("SC", "H2"):
parent.addChild(item)
indent = True
From c0b1d0ad3d95a15d35c53aa2fd87fa55a54cf851 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 21 Mar 2024 16:43:36 +0100
Subject: [PATCH 8/9] Fix tests
---
.../mBuildDocBuild_NWD_Lorem_Ipsum.json | 10 +++---
.../mBuildDocBuild_NWD_Lorem_Ipsum.txt | 6 ++--
tests/test_core/test_core_docbuild.py | 4 +--
tests/test_core/test_core_index.py | 1 +
tests/test_core/test_core_tohtml.py | 34 +++++++++----------
tests/test_core/test_core_tokenizer.py | 30 ++++++++--------
tests/test_core/test_core_tomd.py | 12 +++----
tests/test_core/test_core_toodt.py | 4 +--
8 files changed, 50 insertions(+), 51 deletions(-)
diff --git a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json
index 0a5189be..b727138a 100644
--- a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json
+++ b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.json
@@ -2,8 +2,8 @@
"meta": {
"projectName": "Lorem Ipsum",
"novelAuthor": "lipsum.com",
- "buildTime": 1706648873,
- "buildTimeStr": "2024-01-30 22:07:53"
+ "buildTime": 1711014280,
+ "buildTimeStr": "2024-03-21 10:44:40"
},
"text": {
"nwd": [
@@ -169,7 +169,7 @@
"Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo."
],
[
- "# Notes: Characters"
+ "#! Notes: Characters"
],
[
"# Nobody Owens",
@@ -184,7 +184,7 @@
"Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque."
],
[
- "# Notes: Plot"
+ "#! Notes: Plot"
],
[
"# Main Plot",
@@ -196,7 +196,7 @@
"Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur."
],
[
- "# Notes: World"
+ "#! Notes: World"
],
[
"# Ancient Europe",
diff --git a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.txt b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.txt
index 139d0aec..db3ab0f3 100644
--- a/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.txt
+++ b/tests/reference/mBuildDocBuild_NWD_Lorem_Ipsum.txt
@@ -147,7 +147,7 @@ Pellentesque rhoncus pharetra eros, non mollis nisi pretium non. Mauris accumsan
Integer egestas maximus leo eu facilisis. Nunc rhoncus dignissim lectus eu lacinia. Praesent lacinia urna porttitor aliquam condimentum. Nulla eu eros dictum, dictum nunc vitae, sagittis nibh. Integer ante neque, consequat nec sollicitudin id, consectetur vitae dolor. Nullam volutpat sem orci, quis viverra magna auctor a. Suspendisse potenti. Maecenas commodo sed neque pellentesque vehicula. Sed luctus nisl risus, elementum semper purus interdum vel. Ut pulvinar, massa sit amet venenatis placerat, nunc lacus hendrerit odio, non aliquet nunc risus eu lectus. Maecenas feugiat semper ligula, id lobortis sem porta eu. Integer posuere elit magna, at mollis eros bibendum et. Ut imperdiet purus vel nulla aliquam maximus. Morbi sodales purus tellus, a rhoncus sem rutrum sit amet. Quisque risus sem, laoreet nec convallis nec, rutrum vitae justo.
-# Notes: Characters
+#! Notes: Characters
# Nobody Owens
@@ -160,7 +160,7 @@ Suspendisse faucibus est auctor orci mollis luctus. Praesent quis sodales neque.
Nullam scelerisque velit et tortor congue vestibulum a at nisi. Vivamus sodales ut turpis a convallis. In dignissim nibh at luctus sodales. Etiam sit amet rhoncus massa. Phasellus ligula magna, sollicitudin non imperdiet sit amet, volutpat vel magna. Nunc vestibulum tempor lectus, sit amet porta nunc hendrerit in. Curabitur non odio sit amet massa tincidunt facilisis. Integer et luctus nunc, eget euismod leo. Praesent faucibus metus sed purus convallis scelerisque. Fusce viverra lorem et placerat malesuada. In at elit malesuada, ullamcorper risus vitae, sodales dolor. Donec quis elementum lectus. Quisque eu eros at dui imperdiet euismod ut id neque.
-# Notes: Plot
+#! Notes: Plot
# Main Plot
@@ -170,7 +170,7 @@ Suspendisse vulputate malesuada pellentesque. Aenean sollicitudin cursus mi, vit
Mauris ut erat eu lorem malesuada egestas vel vel urna. Maecenas ac semper quam. Maecenas aliquet metus non interdum mattis. Proin consectetur molestie ligula. Aliquam sollicitudin pulvinar urna a pellentesque. Suspendisse ultrices, est mattis scelerisque porta, nisi nisi laoreet nisl, non condimentum quam ante a velit. Proin scelerisque justo augue, nec laoreet ligula egestas at. Etiam enim quam, ultrices non accumsan hendrerit, elementum vel ligula. Vestibulum ante ipsum primis in faucibus orci luctus et ultrices posuere cubilia curae; Pellentesque habitant morbi tristique senectus et netus et malesuada fames ac turpis egestas. Nam efficitur odio libero, in vestibulum arcu aliquam at. Cras non vehicula augue. Integer lobortis, est vitae aliquam facilisis, metus ligula aliquet eros, at porttitor sem tortor eget massa. Aliquam varius scelerisque neque sed gravida. Aenean eleifend lorem id ante elementum sollicitudin. Proin commodo massa a quam volutpat, mollis fermentum turpis efficitur.
-# Notes: World
+#! Notes: World
# Ancient Europe
diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py
index e6ca622a..466716cf 100644
--- a/tests/test_core/test_core_docbuild.py
+++ b/tests/test_core/test_core_docbuild.py
@@ -575,10 +575,10 @@ def testCoreDocBuild_IterBuild(mockGUI, fncPath: Path, mockRnd):
"By Jane Doe\n\n"
"## New Chapter\n\n\n"
"### New Scene\n\n\n"
- "# Notes: Plot\n\n"
+ "#! Notes: Plot\n\n"
"# Main Plot\n"
"**Text**\n\n"
- "# Notes: Characters\n\n"
+ "#! Notes: Characters\n\n"
"# Jane Doe\n"
"~~Text~~\n\n"
)
diff --git a/tests/test_core/test_core_index.py b/tests/test_core/test_core_index.py
index 13ba42dd..f10ed45a 100644
--- a/tests/test_core/test_core_index.py
+++ b/tests/test_core/test_core_index.py
@@ -1194,6 +1194,7 @@ def testCoreIndex_ItemIndex(mockGUI, fncPath, mockRnd):
# Add a second novel
mHandle = project.newRoot(nwItemClass.NOVEL)
uHandle = project.newFile("Title Page", mHandle)
+ assert uHandle is not None
itemIndex.add(uHandle, project.tree[uHandle]) # type: ignore
itemIndex.addItemHeading(uHandle, "T0001", "H1", "Novel 2") # type: ignore
assert uHandle in itemIndex
diff --git a/tests/test_core/test_core_tohtml.py b/tests/test_core/test_core_tohtml.py
index 30aefd0a..0152607d 100644
--- a/tests/test_core/test_core_tohtml.py
+++ b/tests/test_core/test_core_tohtml.py
@@ -227,7 +227,6 @@ def testCoreToHtml_ConvertParagraphs(mockGUI):
# Multiple Keywords
html._isFirst = False
- html._noBreak = False
html.setKeywords(True)
html._text = "## Chapter\n\n@pov: Bod\n@plot: Main\n@location: Europe\n\n"
html.tokenizeText()
@@ -275,8 +274,8 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Title
html._tokens = [
- (html.T_TITLE, 1, "A Title", None, html.A_PBB | html.A_CENTRE),
- (html.T_EMPTY, 1, "", None, html.A_NONE),
+ (html.T_TITLE, 1, "A Title", [], html.A_PBB | html.A_CENTRE),
+ (html.T_EMPTY, 1, "", [], html.A_NONE),
]
html.doConvert()
assert html.result == (
@@ -286,8 +285,8 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Unnumbered
html._tokens = [
- (html.T_HEAD2, 1, "Prologue", None, html.A_PBB),
- (html.T_EMPTY, 1, "", None, html.A_NONE),
+ (html.T_HEAD2, 1, "Prologue", [], html.A_PBB),
+ (html.T_EMPTY, 1, "", [], html.A_NONE),
]
html.doConvert()
assert html.result == (
@@ -300,16 +299,16 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Separator
html._tokens = [
- (html.T_SEP, 1, "* * *", None, html.A_CENTRE),
- (html.T_EMPTY, 1, "", None, html.A_NONE),
+ (html.T_SEP, 1, "* * *", [], html.A_CENTRE),
+ (html.T_EMPTY, 1, "", [], html.A_NONE),
]
html.doConvert()
assert html.result == "* * *
\n"
# Skip
html._tokens = [
- (html.T_SKIP, 1, "", None, html.A_NONE),
- (html.T_EMPTY, 1, "", None, html.A_NONE),
+ (html.T_SKIP, 1, "", [], html.A_NONE),
+ (html.T_EMPTY, 1, "", [], html.A_NONE),
]
html.doConvert()
assert html.result == "
\n"
@@ -322,7 +321,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Align Left
html.setStyles(False)
html._tokens = [
- (html.T_HEAD1, 1, "A Title", None, html.A_LEFT),
+ (html.T_HEAD1, 1, "A Title", [], html.A_LEFT),
]
html.doConvert()
assert html.result == (
@@ -333,7 +332,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Align Left
html._tokens = [
- (html.T_HEAD1, 1, "A Title", None, html.A_LEFT),
+ (html.T_HEAD1, 1, "A Title", [], html.A_LEFT),
]
html.doConvert()
assert html.result == (
@@ -342,7 +341,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Align Right
html._tokens = [
- (html.T_HEAD1, 1, "A Title", None, html.A_RIGHT),
+ (html.T_HEAD1, 1, "A Title", [], html.A_RIGHT),
]
html.doConvert()
assert html.result == (
@@ -351,7 +350,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Align Centre
html._tokens = [
- (html.T_HEAD1, 1, "A Title", None, html.A_CENTRE),
+ (html.T_HEAD1, 1, "A Title", [], html.A_CENTRE),
]
html.doConvert()
assert html.result == (
@@ -360,7 +359,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Align Justify
html._tokens = [
- (html.T_HEAD1, 1, "A Title", None, html.A_JUSTIFY),
+ (html.T_HEAD1, 1, "A Title", [], html.A_JUSTIFY),
]
html.doConvert()
assert html.result == (
@@ -372,7 +371,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Page Break Always
html._tokens = [
- (html.T_HEAD1, 1, "A Title", None, html.A_PBB | html.A_PBA),
+ (html.T_HEAD1, 1, "A Title", [], html.A_PBB | html.A_PBA),
]
html.doConvert()
assert html.result == (
@@ -386,7 +385,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Indent Left
html._tokens = [
(html.T_TEXT, 1, "Some text ...", [], html.A_IND_L),
- (html.T_EMPTY, 2, "", None, html.A_NONE),
+ (html.T_EMPTY, 2, "", [], html.A_NONE),
]
html.doConvert()
assert html.result == (
@@ -396,7 +395,7 @@ def testCoreToHtml_ConvertDirect(mockGUI):
# Indent Right
html._tokens = [
(html.T_TEXT, 1, "Some text ...", [], html.A_IND_R),
- (html.T_EMPTY, 2, "", None, html.A_NONE),
+ (html.T_EMPTY, 2, "", [], html.A_NONE),
]
html.doConvert()
assert html.result == (
@@ -459,7 +458,6 @@ def testCoreToHtml_SpecialCases(mockGUI):
)
html._isFirst = False
- html._noBreak = False
html._text = "## Heading <1>\n"
html.tokenizeText()
html.doConvert()
diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py
index 68ecfdf7..a35fa2e0 100644
--- a/tests/test_core/test_core_tokenizer.py
+++ b/tests/test_core/test_core_tokenizer.py
@@ -33,7 +33,7 @@ from novelwriter.core.tokenizer import HeadingFormatter, Tokenizer, stripEscape
class BareTokenizer(Tokenizer):
def doConvert(self):
- super().doConvert()
+ super().doConvert() # type: ignore (deliberate check)
@pytest.mark.core
@@ -181,21 +181,23 @@ def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath):
assert project.saveProject()
# Root Heading
- assert tokens.addRootHeading("stuff") is False
- assert tokens.addRootHeading(C.hSceneDoc) is False
+ assert len(tokens._tokens) == 0
+ tokens.addRootHeading("stuff")
+ tokens.addRootHeading(C.hSceneDoc)
+ assert len(tokens._tokens) == 0
# First Page
- assert tokens.addRootHeading(C.hPlotRoot) is True
- assert tokens.allMarkdown[-1] == "# Notes: Plot\n\n"
+ tokens.addRootHeading(C.hPlotRoot)
+ assert tokens.allMarkdown[-1] == "#! Notes: Plot\n\n"
assert tokens._tokens[-1] == (
- Tokenizer.T_TITLE, 0, "Notes: Plot", [], Tokenizer.A_CENTRE
+ Tokenizer.T_TITLE, 1, "Notes: Plot", [], Tokenizer.A_CENTRE
)
# Not First Page
- assert tokens.addRootHeading(C.hPlotRoot) is True
- assert tokens.allMarkdown[-1] == "# Notes: Plot\n\n"
+ tokens.addRootHeading(C.hPlotRoot)
+ assert tokens.allMarkdown[-1] == "#! Notes: Plot\n\n"
assert tokens._tokens[-1] == (
- Tokenizer.T_TITLE, 0, "Notes: Plot", [], Tokenizer.A_CENTRE | Tokenizer.A_PBB
+ Tokenizer.T_TITLE, 1, "Notes: Plot", [], Tokenizer.A_CENTRE | Tokenizer.A_PBB
)
# Set Text
@@ -218,14 +220,14 @@ def testCoreToken_TextOps(monkeypatch, mockGUI, mockRnd, fncPath):
savePath = fncPath / "dump.nwd"
tokens.saveRawMarkdown(savePath)
assert readFile(savePath) == (
- "# Notes: Plot\n\n"
- "# Notes: Plot\n\n"
+ "#! Notes: Plot\n\n"
+ "#! Notes: Plot\n\n"
)
tokens.saveRawMarkdownJSON(savePath)
assert json.loads(readFile(savePath))["text"] == {
"nwd": [
- ["# Notes: Plot"],
- ["# Notes: Plot"]
+ ["#! Notes: Plot"],
+ ["#! Notes: Plot"]
]
}
@@ -313,7 +315,6 @@ def testCoreToken_HeaderFormat(mockGUI):
# Story File
tokens._isNovel = True
- tokens._noBreak = False
tokens._text = "## Chapter One\n"
tokens.tokenizeText()
@@ -448,7 +449,6 @@ def testCoreToken_HeaderStyle(mockGUI):
def processStyle(text: str, first: bool) -> int:
tokens._text = text
tokens._isFirst = first
- tokens._noBreak = first
tokens.tokenizeText()
return tokens._tokens[0][4]
diff --git a/tests/test_core/test_core_tomd.py b/tests/test_core/test_core_tomd.py
index 2192517f..4767ebd4 100644
--- a/tests/test_core/test_core_tomd.py
+++ b/tests/test_core/test_core_tomd.py
@@ -204,8 +204,8 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
# Title
toMD._tokens = [
- (toMD.T_TITLE, 1, "A Title", None, toMD.A_PBB | toMD.A_CENTRE),
- (toMD.T_EMPTY, 1, "", None, toMD.A_NONE),
+ (toMD.T_TITLE, 1, "A Title", [], toMD.A_PBB | toMD.A_CENTRE),
+ (toMD.T_EMPTY, 1, "", [], toMD.A_NONE),
]
toMD.doConvert()
assert toMD.result == "# A Title\n\n"
@@ -215,16 +215,16 @@ def testCoreToMarkdown_ConvertDirect(mockGUI):
# Separator
toMD._tokens = [
- (toMD.T_SEP, 1, "* * *", None, toMD.A_CENTRE),
- (toMD.T_EMPTY, 1, "", None, toMD.A_NONE),
+ (toMD.T_SEP, 1, "* * *", [], toMD.A_CENTRE),
+ (toMD.T_EMPTY, 1, "", [], toMD.A_NONE),
]
toMD.doConvert()
assert toMD.result == "* * *\n\n"
# Skip
toMD._tokens = [
- (toMD.T_SKIP, 1, "", None, toMD.A_NONE),
- (toMD.T_EMPTY, 1, "", None, toMD.A_NONE),
+ (toMD.T_SKIP, 1, "", [], toMD.A_NONE),
+ (toMD.T_EMPTY, 1, "", [], toMD.A_NONE),
]
toMD.doConvert()
assert toMD.result == "\n\n"
diff --git a/tests/test_core/test_core_toodt.py b/tests/test_core/test_core_toodt.py
index 89cdf8c4..3ecd1491 100644
--- a/tests/test_core/test_core_toodt.py
+++ b/tests/test_core/test_core_toodt.py
@@ -652,7 +652,7 @@ def testCoreToOdt_ConvertDirect(mockGUI):
doc = ToOdt(project, isFlat=True)
doc._tokens = [
(doc.T_TEXT, 1, "This is a paragraph", [], doc.A_JUSTIFY),
- (doc.T_EMPTY, 1, "", None, doc.A_NONE),
+ (doc.T_EMPTY, 1, "", [], doc.A_NONE),
]
doc.initDocument()
doc.doConvert()
@@ -673,7 +673,7 @@ def testCoreToOdt_ConvertDirect(mockGUI):
doc = ToOdt(project, isFlat=True)
doc._tokens = [
(doc.T_TEXT, 1, "This is a paragraph", [], doc.A_PBA),
- (doc.T_EMPTY, 1, "", None, doc.A_NONE),
+ (doc.T_EMPTY, 1, "", [], doc.A_NONE),
]
doc.initDocument()
doc.doConvert()
From 991e4fd3e2e805539a806f49fe7a182c73dd37d9 Mon Sep 17 00:00:00 2001
From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com>
Date: Thu, 21 Mar 2024 18:44:15 +0100
Subject: [PATCH 9/9] Add test coverage
---
tests/test_core/test_core_docbuild.py | 4 ++
tests/test_core/test_core_tokenizer.py | 67 +++++++++++++++++++++
tests/test_tools/test_tools_manuscript.py | 71 +++++++++++++++++++++--
3 files changed, 136 insertions(+), 6 deletions(-)
diff --git a/tests/test_core/test_core_docbuild.py b/tests/test_core/test_core_docbuild.py
index 466716cf..634d6e92 100644
--- a/tests/test_core/test_core_docbuild.py
+++ b/tests/test_core/test_core_docbuild.py
@@ -86,8 +86,12 @@ def testCoreDocBuild_OpenDocument(monkeypatch, mockGUI, prjLipsum, fncPath, tstP
docBuild = NWBuildDocument(project, build)
docBuild.setCountEnabled(True)
+ docBuild.setBuildOutline(True)
docBuild.queueAll()
+ assert docBuild._count is True
+ assert docBuild._outline is True
+
assert len(docBuild) == 21
# Check FODT Build
diff --git a/tests/test_core/test_core_tokenizer.py b/tests/test_core/test_core_tokenizer.py
index a35fa2e0..beba03f0 100644
--- a/tests/test_core/test_core_tokenizer.py
+++ b/tests/test_core/test_core_tokenizer.py
@@ -1498,6 +1498,73 @@ def testCoreToken_ProcessHeaders(mockGUI):
# END Test testCoreToken_ProcessHeaders
+@pytest.mark.core
+def testCoreToken_BuildOutline(mockGUI, ipsumText):
+ """Test stats counter of the Tokenizer class."""
+ project = NWProject()
+ project.data.setLanguage("en")
+ project._loadProjectLocalisation()
+ tokens = BareTokenizer(project)
+
+ # Novel
+ tokens._isNovel = True
+ tokens._handle = "0000000000000"
+ tokens._text = (
+ "#! My Novel\n\n"
+ "# Part One\n\n"
+ "## Chapter One\n\n"
+ "### Scene One\n\n"
+ "Text\n\n"
+ "### Scene Two\n\n"
+ "Text\n\n"
+ "## Chapter Two\n\n"
+ "### Scene Three\n\n"
+ "Text\n\n"
+ "###! Scene Four\n\n"
+ "Text\n\n"
+ "#### Section\n\n"
+ "Text\n\n"
+ )
+ tokens.tokenizeText()
+ tokens.buildOutline()
+
+ # Note
+ tokens._isNovel = False
+ tokens._handle = "0000000000001"
+ tokens._text = (
+ "#! My Notes\n\n"
+ "# Header 1\n\n"
+ "Text\n\n"
+ "## Header 2\n\n"
+ "Text\n\n"
+ "### Header 3\n\n"
+ "Text\n\n"
+ "#### Header 4\n\n"
+ "Text\n\n"
+ )
+ tokens.tokenizeText()
+ tokens.buildOutline()
+
+ # Check Outline
+ assert tokens.textOutline == {
+ "0000000000000:T0001": "TT|My Novel",
+ "0000000000000:T0002": "PT|Part One",
+ "0000000000000:T0003": "CH|Chapter One",
+ "0000000000000:T0004": "SC|Scene One",
+ "0000000000000:T0005": "SC|Scene Two",
+ "0000000000000:T0006": "CH|Chapter Two",
+ "0000000000000:T0007": "SC|Scene Three",
+ "0000000000000:T0008": "SC|Scene Four",
+ "0000000000001:T0001": "TT|My Notes",
+ "0000000000001:T0002": "H1|Header 1",
+ "0000000000001:T0003": "H2|Header 2",
+ "0000000000001:T0004": "H3|Header 3",
+ }
+
+
+# END Test testCoreToken_BuildOutline
+
+
@pytest.mark.core
def testCoreToken_CountStats(mockGUI, ipsumText):
"""Test stats counter of the Tokenizer class."""
diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py
index b6c5df66..635fb174 100644
--- a/tests/test_tools/test_tools_manuscript.py
+++ b/tests/test_tools/test_tools_manuscript.py
@@ -26,11 +26,12 @@ import pytest
from pathlib import Path
from pytestqt.qtbot import QtBot
+from novelwriter.constants import nwHeadFmt
from tools import C, buildTestProject
from mocked import causeOSError
from PyQt5.QtCore import Qt, pyqtSlot
-from PyQt5.QtWidgets import QAction, QDialogButtonBox
+from PyQt5.QtWidgets import QAction, QDialogButtonBox, QListWidgetItem
from PyQt5.QtPrintSupport import QPrintPreviewDialog
from novelwriter import CONFIG, SHARED
@@ -152,11 +153,31 @@ def testManuscript_Builds(qtbot: QtBot, nwGUI: GuiMain, projPath: Path):
@pytest.mark.gui
-def testManuscript_Features(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath: Path):
+def testManuscript_Features(monkeypatch, qtbot, nwGUI, projPath, mockRnd):
"""Test other features of the GuiManuscript dialog."""
buildTestProject(nwGUI, projPath)
nwGUI.openProject(projPath)
+ nwGUI.openDocument(C.hTitlePage)
+ nwGUI.docEditor.setPlainText(
+ "#! My Novel\n\n"
+ "# Part One\n\n"
+ "## Chapter One\n\n"
+ "### Scene One\n\n"
+ "Text\n\n"
+ "### Scene Two\n\n"
+ "Text\n\n"
+ "## Chapter Two\n\n"
+ "### Scene Three\n\n"
+ "Text\n\n"
+ "###! Scene Four\n\n"
+ "Text\n\n"
+ "#### Section\n\n"
+ "Text\n\n"
+ )
+ nwGUI.saveDocument()
+ # qtbot.stop()
+
manus = GuiManuscript(nwGUI)
manus.show()
manus.loadContent()
@@ -185,6 +206,14 @@ def testManuscript_Features(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath:
manus.btnPreview.click()
assert cacheFile.exists() is False
+ first = manus.buildList.item(0)
+ assert isinstance(first, QListWidgetItem)
+ build = manus._builds.getBuild(first.data(GuiManuscript.D_KEY))
+ assert isinstance(build, BuildSettings)
+ build.setValue("headings.fmtScene", nwHeadFmt.TITLE)
+ build.setValue("headings.fmtHardScene", nwHeadFmt.TITLE)
+ manus._builds.setBuild(build)
+
# Preview again, and allow cache file to be created
manus.buildList.setCurrentRow(0)
with qtbot.waitSignal(manus.docPreview.document().contentsChanged):
@@ -192,15 +221,45 @@ def testManuscript_Features(monkeypatch, qtbot: QtBot, nwGUI: GuiMain, projPath:
assert manus.docPreview.toPlainText().strip() != ""
assert cacheFile.exists() is True
+ # Check Outline
+ assert manus.buildOutline._outline == {
+ "000000000000c:T0001": "TT|My Novel",
+ "000000000000c:T0002": "PT|Part One",
+ "000000000000c:T0003": "CH|Chapter One",
+ "000000000000c:T0004": "SC|Scene One",
+ "000000000000c:T0005": "SC|Scene Two",
+ "000000000000c:T0006": "CH|Chapter Two",
+ "000000000000c:T0007": "SC|Scene Three",
+ "000000000000c:T0008": "SC|Scene Four",
+ "000000000000e:T0001": "CH|New Chapter",
+ "000000000000f:T0001": "SC|New Scene",
+ }
+ listView = manus.buildOutline.listView
+ assert listView.topLevelItemCount() == 5
+ keyRole = manus.buildOutline.D_LINE
+
+ assert (item := listView.topLevelItem(0)) and item.data(0, keyRole) == "000000000000c:T0001"
+ assert (item := listView.topLevelItem(1)) and item.data(0, keyRole) == "000000000000c:T0002"
+ assert (item := listView.topLevelItem(2)) and item.data(0, keyRole) == "000000000000c:T0003"
+ assert (item := listView.topLevelItem(3)) and item.data(0, keyRole) == "000000000000c:T0006"
+ assert (item := listView.topLevelItem(4)) and item.data(0, keyRole) == "000000000000e:T0001"
+
+ # Click Outline
+ item = listView.topLevelItem(4)
+ assert item is not None
+ with qtbot.waitSignal(manus.buildOutline.outlineEntryClicked) as signal:
+ manus.buildOutline._onItemClick(item)
+ assert signal.args == ["000000000000e:T0001"]
+
# Check Preview Stats
assert manus.docStats.mainStack.currentWidget() == manus.docStats.minWidget
- assert manus.docStats.minWordCount.text() == "7"
- assert manus.docStats.minCharCount.text() == "31"
+ assert manus.docStats.minWordCount.text() == "25"
+ assert manus.docStats.minCharCount.text() == "117"
manus.docStats.toggleButton.toggle()
assert manus.docStats.mainStack.currentWidget() == manus.docStats.maxWidget
- assert manus.docStats.maxTotalWords.text() == "7"
- assert manus.docStats.maxTotalChars.text() == "31"
+ assert manus.docStats.maxTotalWords.text() == "25"
+ assert manus.docStats.maxTotalChars.text() == "117"
# Toggle justify
assert manus.docPreview.document().defaultTextOption().alignment() == Qt.AlignAbsolute