From 096686457d1a171ef4b05b95ba7ae196a4f9205c Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 25 Oct 2025 20:47:32 +0200 Subject: [PATCH 01/10] Add a standard button generator --- novelwriter/enum.py | 15 +++++++++++++ novelwriter/extensions/modified.py | 34 +++++++++++++++++++++++++++-- novelwriter/gui/theme.py | 35 +++++++++++++++++++++++++----- novelwriter/tools/welcome.py | 32 ++++++++------------------- 4 files changed, 85 insertions(+), 31 deletions(-) diff --git a/novelwriter/enum.py b/novelwriter/enum.py index 7ea60294..1bb85120 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -247,3 +247,18 @@ class nwStatusShape(Enum): BLOCK_2 = 17 BLOCK_3 = 18 BLOCK_4 = 19 + + +class nwStandardButton(Enum): + """Enum: Standard Dialog Buttons.""" + + OK = 0 + CANCEL = 1 + YES = 2 + NO = 3 + OPEN = 4 + CLOSE = 5 + BROWSE = 6 + LIST = 7 + NEW = 8 + CREATE = 9 diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index bb256ff1..7e245d21 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -31,8 +31,8 @@ from typing import TYPE_CHECKING from PyQt6.QtCore import QModelIndex, QSize, Qt, pyqtSignal, pyqtSlot from PyQt6.QtWidgets import ( - QApplication, QComboBox, QDialog, QDoubleSpinBox, QLabel, QSpinBox, - QToolButton, QTreeView, QWidget + QApplication, QComboBox, QDialog, QDoubleSpinBox, QLabel, QPushButton, + QSpinBox, QToolButton, QTreeView, QWidget ) from novelwriter import CONFIG, SHARED @@ -199,6 +199,36 @@ class NDoubleSpinBox(QDoubleSpinBox): event.ignore() +class NPushButton(QPushButton): + """Custom: Modified QPushButton. + + A quicker way to create a push button using the app theme. + """ + + def __init__( + self, parent: QWidget, text: str, iconSize: QSize, + icon: str | None = None, color: str | None = None + ) -> None: + super().__init__(parent=parent) + self.setText(text) + self.setIconSize(iconSize) + self._icon = icon + self._color = color + if icon: + self.refreshIcon() + + def setThemeIcon(self, icon: str, color: str | None = None) -> None: + """Set an icon from the current theme.""" + self._icon = icon + self._color = color + self.refreshIcon() + + def refreshIcon(self) -> None: + """Reload the theme icon.""" + if self._icon: + self.setIcon(SHARED.theme.getIcon(self._icon, self._color)) + + class NIconToolButton(QToolButton): """Custom: Modified QToolButton. diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index f3a026f1..9b3c10d4 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -31,19 +31,20 @@ from dataclasses import dataclass from math import ceil from typing import TYPE_CHECKING, Final -from PyQt6.QtCore import QSize, Qt +from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication, QSize, Qt from PyQt6.QtGui import ( QColor, QFont, QFontDatabase, QFontMetrics, QGuiApplication, QIcon, QPainter, QPainterPath, QPalette, QPixmap ) -from PyQt6.QtWidgets import QApplication +from PyQt6.QtWidgets import QApplication, QWidget from novelwriter import CONFIG from novelwriter.common import checkInt, minmax from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS, DEF_TREECOL from novelwriter.constants import nwLabels -from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwTheme +from novelwriter.enum import nwItemClass, nwItemLayout, nwItemType, nwStandardButton, nwTheme from novelwriter.error import logException +from novelwriter.extensions.modified import NPushButton from novelwriter.types import QtBlack, QtHexArgb, QtPaintAntiAlias, QtTransparent if TYPE_CHECKING: @@ -55,6 +56,19 @@ STYLES_FLAT_TABS = "flatTabWidget" STYLES_MIN_TOOLBUTTON = "minimalToolButton" STYLES_BIG_TOOLBUTTON = "bigToolButton" +STANDARD_BUTTONS = { + nwStandardButton.OK: (QT_TRANSLATE_NOOP("Button", "OK"), "bullet-on", "blue"), + nwStandardButton.CANCEL: (QT_TRANSLATE_NOOP("Button", "Cancel"), "cancel", "red"), + nwStandardButton.YES: (QT_TRANSLATE_NOOP("Button", "Yes"), "bullet-on", "green"), + nwStandardButton.NO: (QT_TRANSLATE_NOOP("Button", "No"), "bullet-on", "red"), + nwStandardButton.OPEN: (QT_TRANSLATE_NOOP("Button", "Open"), "open", "blue"), + nwStandardButton.CLOSE: (QT_TRANSLATE_NOOP("Button", "Close"), "close", "default"), + nwStandardButton.BROWSE: (QT_TRANSLATE_NOOP("Button", "Browse"), "browse", "yellow"), + nwStandardButton.LIST: (QT_TRANSLATE_NOOP("Button", "List"), "list", "blue"), + nwStandardButton.NEW: (QT_TRANSLATE_NOOP("Button", "New"), "add", "green"), + nwStandardButton.CREATE: (QT_TRANSLATE_NOOP("Button", "Create"), "star", "yellow"), +} + @dataclass class ThemeEntry: @@ -120,9 +134,9 @@ class GuiTheme: "_qColors", "_styleSheets", "_svgColors", "_syntaxList", "accentCol", "baseButtonHeight", "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", "fadedText", "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", - "getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getToggleIcon", - "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText", - "iconCache", "isDarkTheme", "syntaxTheme", "textNHeight", "textNWidth", + "getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getStandardButton", + "getToggleIcon", "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", + "helpText", "iconCache", "isDarkTheme", "syntaxTheme", "textNHeight", "textNWidth", ) def __init__(self) -> None: @@ -153,6 +167,7 @@ class GuiTheme: self.getItemIcon = self.iconCache.getItemIcon self.getToggleIcon = self.iconCache.getToggleIcon self.getDecoration = self.iconCache.getDecoration + self.getStandardButton = self.iconCache.getStandardButton self.getHeaderDecoration = self.iconCache.getHeaderDecoration self.getHeaderDecorationNarrow = self.iconCache.getHeaderDecorationNarrow @@ -808,6 +823,14 @@ class GuiIcons: w, h = size return self.getIcon(name, color, w, h).pixmap(w, h, QIcon.Mode.Normal) + def getStandardButton(self, button: nwStandardButton, parent: QWidget) -> NPushButton: + """Return a standard button with icon and text.""" + text, icon, color = STANDARD_BUTTONS.get(button, ("", "", "")) + return NPushButton( + parent, QCoreApplication.translate("Button", text), + self._theme.buttonIconSize, icon, color + ) + def getDecoration(self, name: str, w: int | None = None, h: int | None = None) -> QPixmap: """Load graphical decoration element based on the decoration map or the icon map. This function always returns a QPixmap. diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index 69d2e090..041a0031 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -35,15 +35,15 @@ from PyQt6.QtCore import ( from PyQt6.QtGui import QAction, QCloseEvent, QFont, QPainter, QPaintEvent, QPen, QShortcut from PyQt6.QtWidgets import ( QApplication, QFileDialog, QFormLayout, QHBoxLayout, QLabel, QLineEdit, - QListView, QMenu, QPushButton, QScrollArea, QStackedWidget, - QStyledItemDelegate, QStyleOptionViewItem, QVBoxLayout, QWidget + QListView, QMenu, QScrollArea, QStackedWidget, QStyledItemDelegate, + QStyleOptionViewItem, QVBoxLayout, QWidget ) from novelwriter import CONFIG, SHARED from novelwriter.common import formatInt, makeFileNameSafe, qtAddAction, qtLambda from novelwriter.constants import nwFiles from novelwriter.core.coretools import ProjectBuilder -from novelwriter.enum import nwItemClass +from novelwriter.enum import nwItemClass, nwStandardButton from novelwriter.extensions.configlayout import NWrappedWidgetBox from novelwriter.extensions.modified import NDialog, NIconToolButton, NSpinBox from novelwriter.extensions.switch import NSwitch @@ -75,8 +75,6 @@ class GuiWelcome(NDialog): self.setMinimumHeight(450) self.resize(*CONFIG.welcomeWinSize) - btnIconSize = SHARED.theme.buttonIconSize - # Elements # ======== @@ -104,34 +102,22 @@ class GuiWelcome(NDialog): # Buttons # ======= - self.btnList = QPushButton(self.tr("List"), self) - self.btnList.setIcon(SHARED.theme.getIcon("list", "blue")) - self.btnList.setIconSize(btnIconSize) + self.btnList = SHARED.theme.getStandardButton(nwStandardButton.LIST, self) self.btnList.clicked.connect(self._showOpenProjectPage) - self.btnNew = QPushButton(self.tr("New"), self) - self.btnNew.setIcon(SHARED.theme.getIcon("add", "green")) - self.btnNew.setIconSize(btnIconSize) + self.btnNew = SHARED.theme.getStandardButton(nwStandardButton.NEW, self) self.btnNew.clicked.connect(self._showNewProjectPage) - self.btnBrowse = QPushButton(self.tr("Browse"), self) - self.btnBrowse.setIcon(SHARED.theme.getIcon("browse", "yellow")) - self.btnBrowse.setIconSize(btnIconSize) + self.btnBrowse = SHARED.theme.getStandardButton(nwStandardButton.BROWSE, self) self.btnBrowse.clicked.connect(self._browseForProject) - self.btnCancel = QPushButton(self.tr("Cancel"), self) - self.btnCancel.setIcon(SHARED.theme.getIcon("cancel", "red")) - self.btnCancel.setIconSize(btnIconSize) + self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self) self.btnCancel.clicked.connect(qtLambda(self.close)) - self.btnCreate = QPushButton(self.tr("Create"), self) - self.btnCreate.setIcon(SHARED.theme.getIcon("star", "yellow")) - self.btnCreate.setIconSize(btnIconSize) + self.btnCreate = SHARED.theme.getStandardButton(nwStandardButton.CREATE, self) self.btnCreate.clicked.connect(self.tabNew.createNewProject) - self.btnOpen = QPushButton(self.tr("Open"), self) - self.btnOpen.setIcon(SHARED.theme.getIcon("open", "blue")) - self.btnOpen.setIconSize(btnIconSize) + self.btnOpen = SHARED.theme.getStandardButton(nwStandardButton.OPEN, self) self.btnOpen.clicked.connect(self._openSelectedItem) self.btnBox = QHBoxLayout() From bb1d90213b400526a5760fcc7a3e5b8954189c71 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 25 Oct 2025 21:38:30 +0200 Subject: [PATCH 02/10] Update standard dialog boxes --- novelwriter/enum.py | 1 + novelwriter/gui/theme.py | 21 +++++++++++---------- novelwriter/shared.py | 35 ++++++++++++++++++++++++++++++----- 3 files changed, 42 insertions(+), 15 deletions(-) diff --git a/novelwriter/enum.py b/novelwriter/enum.py index 1bb85120..4db5cdce 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -262,3 +262,4 @@ class nwStandardButton(Enum): LIST = 7 NEW = 8 CREATE = 9 + RESET = 10 diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 9b3c10d4..441c9cd1 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -57,16 +57,17 @@ STYLES_MIN_TOOLBUTTON = "minimalToolButton" STYLES_BIG_TOOLBUTTON = "bigToolButton" STANDARD_BUTTONS = { - nwStandardButton.OK: (QT_TRANSLATE_NOOP("Button", "OK"), "bullet-on", "blue"), - nwStandardButton.CANCEL: (QT_TRANSLATE_NOOP("Button", "Cancel"), "cancel", "red"), - nwStandardButton.YES: (QT_TRANSLATE_NOOP("Button", "Yes"), "bullet-on", "green"), - nwStandardButton.NO: (QT_TRANSLATE_NOOP("Button", "No"), "bullet-on", "red"), - nwStandardButton.OPEN: (QT_TRANSLATE_NOOP("Button", "Open"), "open", "blue"), - nwStandardButton.CLOSE: (QT_TRANSLATE_NOOP("Button", "Close"), "close", "default"), - nwStandardButton.BROWSE: (QT_TRANSLATE_NOOP("Button", "Browse"), "browse", "yellow"), - nwStandardButton.LIST: (QT_TRANSLATE_NOOP("Button", "List"), "list", "blue"), - nwStandardButton.NEW: (QT_TRANSLATE_NOOP("Button", "New"), "add", "green"), - nwStandardButton.CREATE: (QT_TRANSLATE_NOOP("Button", "Create"), "star", "yellow"), + nwStandardButton.OK: (QT_TRANSLATE_NOOP("Button", "OK"), "btn_ok", "blue"), + nwStandardButton.CANCEL: (QT_TRANSLATE_NOOP("Button", "Cancel"), "btn_cancel", "red"), + nwStandardButton.YES: (QT_TRANSLATE_NOOP("Button", "Yes"), "btn_yes", "green"), + nwStandardButton.NO: (QT_TRANSLATE_NOOP("Button", "No"), "btn_no", "red"), + nwStandardButton.OPEN: (QT_TRANSLATE_NOOP("Button", "Open"), "btn_open", "blue"), + nwStandardButton.CLOSE: (QT_TRANSLATE_NOOP("Button", "Close"), "btn_close", "red"), + nwStandardButton.BROWSE: (QT_TRANSLATE_NOOP("Button", "Browse"), "btn_browse", "yellow"), + nwStandardButton.LIST: (QT_TRANSLATE_NOOP("Button", "List"), "btn_list", "blue"), + nwStandardButton.NEW: (QT_TRANSLATE_NOOP("Button", "New"), "btn_new", "green"), + nwStandardButton.CREATE: (QT_TRANSLATE_NOOP("Button", "Create"), "btn_create", "yellow"), + nwStandardButton.RESET: (QT_TRANSLATE_NOOP("Button", "Reset"), "btn_reset", "green"), } diff --git a/novelwriter/shared.py b/novelwriter/shared.py index 7c971341..be439b48 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -34,12 +34,12 @@ from typing import TYPE_CHECKING, TypeVar from PyQt6.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot from PyQt6.QtGui import QDesktopServices, QFont, QScreen -from PyQt6.QtWidgets import QApplication, QFileDialog, QFontDialog, QMessageBox, QWidget +from PyQt6.QtWidgets import QApplication, QDialog, QFileDialog, QFontDialog, QMessageBox, QWidget from novelwriter.common import formatFileFilter from novelwriter.constants import nwFiles from novelwriter.core.spellcheck import NWSpellEnchant -from novelwriter.enum import nwChange, nwItemClass +from novelwriter.enum import nwChange, nwItemClass, nwStandardButton if TYPE_CHECKING: from collections.abc import Callable @@ -422,7 +422,7 @@ class SharedData(QObject): alert.setAlertType(_GuiAlert.WARN if warn else _GuiAlert.ASK, True) self._lastAlert = alert.logMessage alert.exec() - return alert.result() == QMessageBox.StandardButton.Yes + return alert.finalState ## # Internal Functions @@ -469,6 +469,7 @@ class _GuiAlert(QMessageBox): super().__init__(parent=parent) self._theme = theme self._message = "" + self._state = False logger.debug("Ready: _GuiAlert") def __del__(self) -> None: # pragma: no cover @@ -478,6 +479,10 @@ class _GuiAlert(QMessageBox): def logMessage(self) -> str: return self._message + @property + def finalState(self) -> bool: + return self._state + def setMessage(self, text: str, info: str, details: str) -> None: """Set the alert box message.""" self._message = " ".join(filter(None, [text, info, details])) @@ -496,9 +501,17 @@ class _GuiAlert(QMessageBox): Yes/No buttons or just an Ok button. """ if isYesNo: - self.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + self._btnYes = self._theme.getStandardButton(nwStandardButton.YES, self) + self._btnYes.clicked.connect(self._onAccept) + self._btnNo = self._theme.getStandardButton(nwStandardButton.NO, self) + self._btnNo.clicked.connect(self._onReject) + self.addButton(self._btnYes, QMessageBox.ButtonRole.YesRole) + self.addButton(self._btnNo, QMessageBox.ButtonRole.NoRole) else: - self.setStandardButtons(QMessageBox.StandardButton.Ok) + self._btnOk = self._theme.getStandardButton(nwStandardButton.OK, self) + self._btnOk.clicked.connect(self._onAccept) + self.addButton(self._btnOk, QMessageBox.ButtonRole.AcceptRole) + pSz = 2*self._theme.baseIconHeight if level == self.INFO: self.setIconPixmap(self._theme.getPixmap("alert_info", (pSz, pSz), "blue")) @@ -512,3 +525,15 @@ class _GuiAlert(QMessageBox): elif level == self.ASK: self.setIconPixmap(self._theme.getPixmap("alert_question", (pSz, pSz), "blue")) self.setWindowTitle(self.tr("Question")) + + @pyqtSlot() + def _onAccept(self) -> None: + """Process accepted state.""" + self._state = True + self.close() + + @pyqtSlot() + def _onReject(self) -> None: + """Process rejected state.""" + self._state = False + self.setResult(QDialog.DialogCode.Rejected) From 1938969a63be1afdd28cc81ae8f174bd927b9248 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 25 Oct 2025 21:42:39 +0200 Subject: [PATCH 03/10] Update icon themes --- .../assets/icons/material_filled_normal.icons | 13 +++++++++++-- .../assets/icons/material_filled_thin.icons | 13 +++++++++++-- .../assets/icons/material_rounded_normal.icons | 13 +++++++++++-- .../assets/icons/material_rounded_thin.icons | 13 +++++++++++-- .../assets/icons/material_sharp_normal.icons | 13 +++++++++++-- .../assets/icons/material_sharp_thin.icons | 13 +++++++++++-- tests/files/all_icons.json | 14 ++++++++++++-- utils/icon_themes.py | 16 ++++++++++++++-- utils/icon_themes/font_awesome.json | 18 ++++++++++++++---- utils/icon_themes/material_symbols.json | 14 ++++++++++++-- utils/icon_themes/remix.json | 14 ++++++++++++-- 11 files changed, 130 insertions(+), 24 deletions(-) diff --git a/novelwriter/assets/icons/material_filled_normal.icons b/novelwriter/assets/icons/material_filled_normal.icons index 36b8536f..1cd8cfb1 100644 --- a/novelwriter/assets/icons/material_filled_normal.icons +++ b/novelwriter/assets/icons/material_filled_normal.icons @@ -59,6 +59,17 @@ icon:sb_stats = icon:theme_dark = icon:theme_auto = +icon:btn_ok = +icon:btn_cancel = +icon:btn_yes = +icon:btn_no = +icon:btn_open = +icon:btn_close = +icon:btn_browse = +icon:btn_list = +icon:btn_new = +icon:btn_create = +icon:btn_reset = icon:add = icon:bookmarks = icon:browse = @@ -94,7 +105,6 @@ icon:minimise = icon:more_vertical = icon:noncheckable = -icon:open = icon:panel = icon:pin = icon:project_copy = @@ -103,7 +113,6 @@ icon:refresh = icon:revert = icon:settings = -icon:star = icon:stats = icon:text = icon:timer_off = diff --git a/novelwriter/assets/icons/material_filled_thin.icons b/novelwriter/assets/icons/material_filled_thin.icons index a405ea3e..2d399f22 100644 --- a/novelwriter/assets/icons/material_filled_thin.icons +++ b/novelwriter/assets/icons/material_filled_thin.icons @@ -59,6 +59,17 @@ icon:sb_stats = icon:theme_dark = icon:theme_auto = +icon:btn_ok = +icon:btn_cancel = +icon:btn_yes = +icon:btn_no = +icon:btn_open = +icon:btn_close = +icon:btn_browse = +icon:btn_list = +icon:btn_new = +icon:btn_create = +icon:btn_reset = icon:add = icon:bookmarks = icon:browse = @@ -94,7 +105,6 @@ icon:minimise = icon:more_vertical = icon:noncheckable = -icon:open = icon:panel = icon:pin = icon:project_copy = @@ -103,7 +113,6 @@ icon:refresh = icon:revert = icon:settings = -icon:star = icon:stats = icon:text = icon:timer_off = diff --git a/novelwriter/assets/icons/material_rounded_normal.icons b/novelwriter/assets/icons/material_rounded_normal.icons index 9c22a001..044bdd5d 100644 --- a/novelwriter/assets/icons/material_rounded_normal.icons +++ b/novelwriter/assets/icons/material_rounded_normal.icons @@ -59,6 +59,17 @@ icon:sb_stats = icon:theme_dark = icon:theme_auto = +icon:btn_ok = +icon:btn_cancel = +icon:btn_yes = +icon:btn_no = +icon:btn_open = +icon:btn_close = +icon:btn_browse = +icon:btn_list = +icon:btn_new = +icon:btn_create = +icon:btn_reset = icon:add = icon:bookmarks = icon:browse = @@ -94,7 +105,6 @@ icon:minimise = icon:more_vertical = icon:noncheckable = -icon:open = icon:panel = icon:pin = icon:project_copy = @@ -103,7 +113,6 @@ icon:refresh = icon:revert = icon:settings = -icon:star = icon:stats = icon:text = icon:timer_off = diff --git a/novelwriter/assets/icons/material_rounded_thin.icons b/novelwriter/assets/icons/material_rounded_thin.icons index 1002fcbf..22834e45 100644 --- a/novelwriter/assets/icons/material_rounded_thin.icons +++ b/novelwriter/assets/icons/material_rounded_thin.icons @@ -59,6 +59,17 @@ icon:sb_stats = icon:theme_dark = icon:theme_auto = +icon:btn_ok = +icon:btn_cancel = +icon:btn_yes = +icon:btn_no = +icon:btn_open = +icon:btn_close = +icon:btn_browse = +icon:btn_list = +icon:btn_new = +icon:btn_create = +icon:btn_reset = icon:add = icon:bookmarks = icon:browse = @@ -94,7 +105,6 @@ icon:minimise = icon:more_vertical = icon:noncheckable = -icon:open = icon:panel = icon:pin = icon:project_copy = @@ -103,7 +113,6 @@ icon:refresh = icon:revert = icon:settings = -icon:star = icon:stats = icon:text = icon:timer_off = diff --git a/novelwriter/assets/icons/material_sharp_normal.icons b/novelwriter/assets/icons/material_sharp_normal.icons index b8b8c0e6..13c68127 100644 --- a/novelwriter/assets/icons/material_sharp_normal.icons +++ b/novelwriter/assets/icons/material_sharp_normal.icons @@ -59,6 +59,17 @@ icon:sb_stats = icon:theme_dark = icon:theme_auto = +icon:btn_ok = +icon:btn_cancel = +icon:btn_yes = +icon:btn_no = +icon:btn_open = +icon:btn_close = +icon:btn_browse = +icon:btn_list = +icon:btn_new = +icon:btn_create = +icon:btn_reset = icon:add = icon:bookmarks = icon:browse = @@ -94,7 +105,6 @@ icon:minimise = icon:more_vertical = icon:noncheckable = -icon:open = icon:panel = icon:pin = icon:project_copy = @@ -103,7 +113,6 @@ icon:refresh = icon:revert = icon:settings = -icon:star = icon:stats = icon:text = icon:timer_off = diff --git a/novelwriter/assets/icons/material_sharp_thin.icons b/novelwriter/assets/icons/material_sharp_thin.icons index afe8528d..c8bb13a8 100644 --- a/novelwriter/assets/icons/material_sharp_thin.icons +++ b/novelwriter/assets/icons/material_sharp_thin.icons @@ -59,6 +59,17 @@ icon:sb_stats = icon:theme_dark = icon:theme_auto = +icon:btn_ok = +icon:btn_cancel = +icon:btn_yes = +icon:btn_no = +icon:btn_open = +icon:btn_close = +icon:btn_browse = +icon:btn_list = +icon:btn_new = +icon:btn_create = +icon:btn_reset = icon:add = icon:bookmarks = icon:browse = @@ -94,7 +105,6 @@ icon:minimise = icon:more_vertical = icon:noncheckable = -icon:open = icon:panel = icon:pin = icon:project_copy = @@ -103,7 +113,6 @@ icon:refresh = icon:revert = icon:settings = -icon:star = icon:stats = icon:text = icon:timer_off = diff --git a/tests/files/all_icons.json b/tests/files/all_icons.json index 3d85f818..41797f20 100644 --- a/tests/files/all_icons.json +++ b/tests/files/all_icons.json @@ -60,6 +60,18 @@ "theme_dark", "theme_auto", + "btn_ok", + "btn_cancel", + "btn_yes", + "btn_no", + "btn_open", + "btn_close", + "btn_browse", + "btn_list", + "btn_new", + "btn_create", + "btn_reset", + "add", "bookmarks", "browse", @@ -95,7 +107,6 @@ "more_arrow", "more_vertical", "noncheckable", - "open", "panel", "pin", "project_copy", @@ -104,7 +115,6 @@ "remove", "revert", "settings", - "star", "stats", "text", "timer_off", diff --git a/utils/icon_themes.py b/utils/icon_themes.py index cb906b01..e5c2268f 100644 --- a/utils/icon_themes.py +++ b/utils/icon_themes.py @@ -107,6 +107,18 @@ ICONS = [ "theme_dark", "theme_auto", + "btn_ok", + "btn_cancel", + "btn_yes", + "btn_no", + "btn_open", + "btn_close", + "btn_browse", + "btn_list", + "btn_new", + "btn_create", + "btn_reset", + "add", "bookmarks", "browse", @@ -142,7 +154,6 @@ ICONS = [ "more_arrow", "more_vertical", "noncheckable", - "open", "panel", "pin", "project_copy", @@ -151,7 +162,6 @@ ICONS = [ "remove", "revert", "settings", - "star", "stats", "text", "timer_off", @@ -288,6 +298,8 @@ def processFontAwesome(workDir: Path, iconsDir: Path, jobs: dict) -> None: viewbox = [int(x) for x in svg.get("viewBox", "").split()] viewbox = [viewbox[2]//2 - 256, 0, 512, 512] svg.set("viewBox", " ".join(str(x) for x in viewbox)) + for elem in svg.iter(): + elem.attrib.pop("fill", None) icons[key] = svg else: print(f"Not Found: {icon}.svg") diff --git a/utils/icon_themes/font_awesome.json b/utils/icon_themes/font_awesome.json index d98dc9e2..1769409e 100644 --- a/utils/icon_themes/font_awesome.json +++ b/utils/icon_themes/font_awesome.json @@ -60,6 +60,18 @@ "theme_dark": "moon", "theme_auto": "circle-half-stroke", + "btn_ok": "circle-check", + "btn_cancel": "ban", + "btn_yes": "circle-check", + "btn_no": "circle-xmark", + "btn_open": "file-arrow-up", + "btn_close": "circle-xmark", + "btn_browse": "folder-open", + "btn_list": "list", + "btn_new": "plus", + "btn_create": "star", + "btn_reset": "rotate-left", + "add": "plus", "bookmarks": "bookmark", "browse": "folder-open", @@ -95,16 +107,14 @@ "more_arrow": "caret-right", "more_vertical": "ellipsis-vertical", "noncheckable": "square-minus", - "open": "file-arrow-up", "panel": "table-list", "pin": "thumbtack", "project_copy": "copy", "quote": "quote-right", - "refresh": "arrow-rotate-right", + "refresh": "rotate-right", "remove": "minus", - "revert": "arrow-rotate-left", + "revert": "rotate-left", "settings": "gear", - "star": "star", "stats": "chart-line", "text": "file-lines", "timer_off": "pause", diff --git a/utils/icon_themes/material_symbols.json b/utils/icon_themes/material_symbols.json index 61c1975a..3d1adc47 100644 --- a/utils/icon_themes/material_symbols.json +++ b/utils/icon_themes/material_symbols.json @@ -60,6 +60,18 @@ "theme_dark": "dark_mode", "theme_auto": "contrast", + "btn_ok": "check_circle", + "btn_cancel": "cancel", + "btn_yes": "check_circle", + "btn_no": "do_not_disturb_on", + "btn_open": "open_in_new", + "btn_close": "close", + "btn_browse": "folder_open", + "btn_list": "format_list_bulleted", + "btn_new": "new_window", + "btn_create": "star", + "btn_reset": "undo", + "add": "add", "bookmarks": "bookmarks", "browse": "folder_open", @@ -95,7 +107,6 @@ "more_arrow": "arrow_right", "more_vertical": "more_vert", "noncheckable": "indeterminate_check_box", - "open": "open_in_new", "panel": "dock_to_bottom", "pin": "keep", "project_copy": "folder_copy", @@ -104,7 +115,6 @@ "remove": "remove", "revert": "settings_backup_restore", "settings": "settings", - "star": "star", "stats": "stacked_line_chart", "text": "subject", "timer_off": "timer_off", diff --git a/utils/icon_themes/remix.json b/utils/icon_themes/remix.json index 0ac173ec..da78a84f 100644 --- a/utils/icon_themes/remix.json +++ b/utils/icon_themes/remix.json @@ -60,6 +60,18 @@ "theme_dark": "moon", "theme_auto": "contrast", + "btn_ok": "checkbox-circle", + "btn_cancel": "indeterminate-circle", + "btn_yes": "checkbox-circle", + "btn_no": "close-circle", + "btn_open": "file-upload", + "btn_close": "close-circle", + "btn_browse": "folder-2", + "btn_list": "list-unordered", + "btn_new": "add", + "btn_create": "star-fill", + "btn_reset": "reset-left", + "add": "add", "bookmarks": "bookmark", "browse": "folder-2", @@ -95,7 +107,6 @@ "more_arrow": "arrow-right-s-fill", "more_vertical": "more-2-fill", "noncheckable": "checkbox-indeterminate", - "open": "file-upload", "panel": "layout-bottom", "pin": "pushpin", "project_copy": "file-copy-2", @@ -104,7 +115,6 @@ "remove": "subtract", "revert": "reset-left", "settings": "settings-2", - "star": "star-fill", "stats": "line-chart", "text": "file-text", "timer_off": "zzz", From 2d67cb7dcc7df98ee2856dabce186bb8da4b79b4 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 25 Oct 2025 22:37:19 +0200 Subject: [PATCH 04/10] Update tests and test coverage --- novelwriter/extensions/modified.py | 6 -- novelwriter/shared.py | 4 +- run_tests.py | 5 +- tests/conftest.py | 8 ++- tests/mocked.py | 6 ++ tests/test_base/test_base_shared.py | 70 +++++++++++++++++++++-- tests/test_core/test_core_project.py | 7 +-- tests/test_gui/test_gui_guimain.py | 9 +-- tests/test_gui/test_gui_i18n.py | 4 +- tests/test_gui/test_gui_mainmenu.py | 5 +- tests/test_gui/test_gui_projtree.py | 13 +++-- tests/test_tools/test_tools_manusbuild.py | 5 +- 12 files changed, 105 insertions(+), 37 deletions(-) diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index 7e245d21..b802a4d9 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -217,12 +217,6 @@ class NPushButton(QPushButton): if icon: self.refreshIcon() - def setThemeIcon(self, icon: str, color: str | None = None) -> None: - """Set an icon from the current theme.""" - self._icon = icon - self._color = color - self.refreshIcon() - def refreshIcon(self) -> None: """Reload the theme icon.""" if self._icon: diff --git a/novelwriter/shared.py b/novelwriter/shared.py index be439b48..b76ad165 100644 --- a/novelwriter/shared.py +++ b/novelwriter/shared.py @@ -34,7 +34,7 @@ from typing import TYPE_CHECKING, TypeVar from PyQt6.QtCore import QObject, QRunnable, QThreadPool, QTimer, QUrl, pyqtSignal, pyqtSlot from PyQt6.QtGui import QDesktopServices, QFont, QScreen -from PyQt6.QtWidgets import QApplication, QDialog, QFileDialog, QFontDialog, QMessageBox, QWidget +from PyQt6.QtWidgets import QApplication, QFileDialog, QFontDialog, QMessageBox, QWidget from novelwriter.common import formatFileFilter from novelwriter.constants import nwFiles @@ -536,4 +536,4 @@ class _GuiAlert(QMessageBox): def _onReject(self) -> None: """Process rejected state.""" self._state = False - self.setResult(QDialog.DialogCode.Rejected) + self.close() diff --git a/run_tests.py b/run_tests.py index 18e1eeae..6f593127 100755 --- a/run_tests.py +++ b/run_tests.py @@ -25,7 +25,10 @@ if __name__ == "__main__": env["QT_SCALE_FACTOR"] = "1.0" if args.r or args.t or args.u: - cmd = ["coverage", "run", "-m"] + cmd = ["coverage", "run"] + if args.lf or args.sw: + cmd += ["--append"] + cmd += ["-m"] else: cmd = [sys.executable, "-m"] diff --git a/tests/conftest.py b/tests/conftest.py index 765755bf..f8e3a5eb 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -157,9 +157,11 @@ def projPath(fncPath): def mockGUI(qtbot, monkeypatch): """Create a mock instance of novelWriter's main GUI class.""" from novelwriter.gui.theme import GuiTheme + from novelwriter.shared import _GuiAlert monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) - monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) + monkeypatch.setattr(_GuiAlert, "exec", lambda *a: None) + monkeypatch.setattr(_GuiAlert, "finalState", True) gui = MockGuiMain() theme = GuiTheme() monkeypatch.setattr(SHARED, "_gui", gui) @@ -182,9 +184,11 @@ def nwGUI(qtbot, monkeypatch, functionFixture): """Create an instance of the novelWriter GUI.""" from novelwriter.gui.theme import GuiTheme from novelwriter.guimain import GuiMain + from novelwriter.shared import _GuiAlert monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) - monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) + monkeypatch.setattr(_GuiAlert, "exec", lambda *a: None) + monkeypatch.setattr(_GuiAlert, "finalState", True) CONFIG.loadConfig() SHARED.initTheme(GuiTheme()) diff --git a/tests/mocked.py b/tests/mocked.py index f86d5be0..92b56abe 100644 --- a/tests/mocked.py +++ b/tests/mocked.py @@ -22,9 +22,12 @@ from __future__ import annotations from unittest.mock import MagicMock +from PyQt6.QtCore import QSize from PyQt6.QtGui import QFont, QIcon, QPixmap from PyQt6.QtWidgets import QWidget +from novelwriter.extensions.modified import NPushButton + class MockGuiMain(QWidget): @@ -72,6 +75,9 @@ class MockTheme: def getHeaderDecoration(self, *a) -> QPixmap: return QPixmap() + def getStandardButton(self, *a) -> NPushButton: + return NPushButton(None, "", QSize(1, 1)) # type: ignore + def getIcon(self, *a) -> QIcon: return QIcon() diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py index 64cc306b..a020a694 100644 --- a/tests/test_base/test_base_shared.py +++ b/tests/test_base/test_base_shared.py @@ -26,10 +26,10 @@ import pytest from PyQt6.QtCore import QUrl from PyQt6.QtGui import QDesktopServices -from PyQt6.QtWidgets import QFileDialog, QMessageBox, QWidget +from PyQt6.QtWidgets import QFileDialog, QWidget from novelwriter.core.project import NWProject -from novelwriter.shared import SharedData +from novelwriter.shared import SharedData, _GuiAlert from tests.mocked import MockGuiMain, MockTheme from tests.tools import buildTestProject @@ -143,10 +143,10 @@ def testBaseSharedData_Projects(monkeypatch, caplog, fncPath): @pytest.mark.base -def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog): +def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog, mockGUI): """Test SharedData class alert helper functions.""" - monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) - monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) + monkeypatch.setattr(_GuiAlert, "exec", lambda *a: None) + monkeypatch.setattr(_GuiAlert, "finalState", True) shared = SharedData() @@ -188,3 +188,63 @@ def testBaseSharedData_Alerts(qtbot, monkeypatch, caplog): # Question box assert shared.question("Why?") is True assert shared.lastAlert == "Why?" + + +@pytest.mark.base +def testBaseSharedData_GuiAlert(): + """Test the _GuiAlert class.""" + alert = _GuiAlert(None, MockTheme()) # type: ignore + + # Default states + assert alert.logMessage == "" + assert alert.finalState is False + + # Populate message + text = "one" + info = "two" + details = "three" + alert.setMessage(text, info, details) + assert alert.logMessage == f"{text} {info} {details}" + assert alert.text() == text + assert alert.informativeText() == info + assert alert.detailedText() == details + + # Populate exception + exc = Exception("oops") + alert.setException(exc) + assert alert.logMessage == f"{text} {info} {details}" + assert alert.informativeText() == f"{info}
Exception: {exc!s}" + + # Alert: Info + alert.setAlertType(_GuiAlert.INFO, False) + assert hasattr(alert, "_btnOk") + assert alert.windowTitle() == "Information" + alert._btnOk.click() + assert alert.finalState is True + alert._state = False + + # Alert: Warning + alert.setAlertType(_GuiAlert.WARN, False) + assert hasattr(alert, "_btnOk") + assert alert.windowTitle() == "Warning" + alert._btnOk.click() + assert alert.finalState is True + alert._state = False + + # Alert: Error + alert.setAlertType(_GuiAlert.ERROR, False) + assert hasattr(alert, "_btnOk") + assert alert.windowTitle() == "Error" + alert._btnOk.click() + assert alert.finalState is True + alert._state = False + + # Alert: Question + alert.setAlertType(_GuiAlert.ASK, True) + assert hasattr(alert, "_btnYes") + assert hasattr(alert, "_btnNo") + assert alert.windowTitle() == "Question" + alert._btnYes.click() + assert alert.finalState is True + alert._btnNo.click() + assert alert.finalState is False diff --git a/tests/test_core/test_core_project.py b/tests/test_core/test_core_project.py index cdb202c1..799cd423 100644 --- a/tests/test_core/test_core_project.py +++ b/tests/test_core/test_core_project.py @@ -25,13 +25,12 @@ from zipfile import ZipFile import pytest -from PyQt6.QtWidgets import QMessageBox - from novelwriter import CONFIG, SHARED from novelwriter.constants import nwFiles from novelwriter.core.project import NWProject, NWProjectState from novelwriter.core.projectxml import ProjectXMLReader, ProjectXMLWriter, XMLReadState from novelwriter.enum import nwItemClass +from novelwriter.shared import _GuiAlert from tests.mocked import causeOSError from tests.tools import XML_IGNORE, C, buildTestProject, cmpFiles @@ -274,14 +273,14 @@ def testCoreProject_Open(monkeypatch, caplog, mockGUI, fncPath, mockRnd): # Won't convert legacy file with monkeypatch.context() as mp: mp.setattr(ProjectXMLReader, "state", property(lambda *a: XMLReadState.WAS_LEGACY)) - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert project.openProject(fncPath, clearLock=True) is False assert "The file format of your project is about to be" in SHARED.lastAlert # Won't open project from newer version with monkeypatch.context() as mp: mp.setattr(ProjectXMLReader, "hexVersion", property(lambda *a: 0x99999999)) - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert project.openProject(fncPath, clearLock=True) is False assert "This project was saved by a newer version" in SHARED.lastAlert diff --git a/tests/test_gui/test_gui_guimain.py b/tests/test_gui/test_gui_guimain.py index 0e15b71e..4cfa3a91 100644 --- a/tests/test_gui/test_gui_guimain.py +++ b/tests/test_gui/test_gui_guimain.py @@ -30,7 +30,7 @@ import pytest from PyQt6.QtCore import Qt from PyQt6.QtGui import QPalette -from PyQt6.QtWidgets import QInputDialog, QMessageBox +from PyQt6.QtWidgets import QInputDialog from novelwriter import CONFIG, SHARED, __hexversion__ from novelwriter.common import jsonEncode @@ -42,6 +42,7 @@ from novelwriter.gui.doceditor import GuiDocEditor from novelwriter.gui.noveltree import GuiNovelView from novelwriter.gui.outline import GuiOutlineView from novelwriter.gui.projtree import GuiProjectTree +from novelwriter.shared import _GuiAlert from novelwriter.tools.welcome import GuiWelcome from novelwriter.types import QtModCtrl, QtModShift @@ -104,7 +105,7 @@ def testGuiMain_Launch(qtbot, monkeypatch, nwGUI, projPath): # Check that closes can be blocked with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert nwGUI.openProject(projPath) is True assert nwGUI.closeMain() is False nwGUI.closeProject() @@ -841,7 +842,7 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd) # Block closing assert SHARED.hasProject is True with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert nwGUI.openProject(projPath) is False assert SHARED.hasProject is True @@ -854,7 +855,7 @@ def testGuiMain_OpenClose(qtbot, monkeypatch, nwGUI, projPath, fncPath, mockRnd) shutil.copyfile(lockBack, lockPath) with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert nwGUI.openProject(projPath) is False assert nwGUI.openProject(projPath) is True diff --git a/tests/test_gui/test_gui_i18n.py b/tests/test_gui/test_gui_i18n.py index 915d83a4..e482d85f 100644 --- a/tests/test_gui/test_gui_i18n.py +++ b/tests/test_gui/test_gui_i18n.py @@ -24,7 +24,7 @@ import sys import pytest -from PyQt6.QtWidgets import QApplication, QDialog, QMessageBox +from PyQt6.QtWidgets import QApplication, QDialog from novelwriter import CONFIG, SHARED from novelwriter.dialogs.about import GuiAbout @@ -49,8 +49,6 @@ LANG_DATA = CONFIG.listLanguages(CONFIG.LANG_NW) def testGuiI18n_Localisation(qtbot, monkeypatch, language, nwGUI, projPath): """Test loading the gui with a specific language.""" monkeypatch.setattr(QDialog, "exec", lambda *a: None) - monkeypatch.setattr(QMessageBox, "exec", lambda *a: None) - monkeypatch.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.Yes) # Set the test language CONFIG.guiLocale = language diff --git a/tests/test_gui/test_gui_mainmenu.py b/tests/test_gui/test_gui_mainmenu.py index 0d6344bc..e599e9f9 100644 --- a/tests/test_gui/test_gui_mainmenu.py +++ b/tests/test_gui/test_gui_mainmenu.py @@ -25,12 +25,13 @@ from unittest.mock import MagicMock import pytest from PyQt6.QtGui import QAction, QDesktopServices, QTextBlock -from PyQt6.QtWidgets import QFileDialog, QMessageBox +from PyQt6.QtWidgets import QFileDialog from novelwriter import CONFIG, SHARED from novelwriter.constants import nwKeyWords, nwShortcode, nwStats, nwUnicode from novelwriter.enum import nwDocAction, nwDocInsert from novelwriter.gui.doceditor import GuiDocEditor +from novelwriter.shared import _GuiAlert from novelwriter.types import QtKeepAnchor, QtMoveRight, QtSelectWord from tests.tools import C, buildTestProject, writeFile @@ -575,7 +576,7 @@ def testGuiMainMenu_Insert(qtbot, monkeypatch, nwGUI, fncPath, projPath, mockRnd # The document isn't empty, so the message box should pop with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a, **k: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert not nwGUI.importDocument() assert docEditor.getText() == "Bar" diff --git a/tests/test_gui/test_gui_projtree.py b/tests/test_gui/test_gui_projtree.py index f6430f55..bdd50ad0 100644 --- a/tests/test_gui/test_gui_projtree.py +++ b/tests/test_gui/test_gui_projtree.py @@ -26,7 +26,7 @@ import pytest from PyQt6.QtCore import QEvent, QItemSelectionModel, QModelIndex, QPointF from PyQt6.QtGui import QMouseEvent -from PyQt6.QtWidgets import QMenu, QMessageBox +from PyQt6.QtWidgets import QMenu from novelwriter import CONFIG, SHARED from novelwriter.dialogs.docmerge import GuiDocMerge @@ -34,6 +34,7 @@ from novelwriter.dialogs.docsplit import GuiDocSplit from novelwriter.dialogs.editlabel import GuiEditLabel from novelwriter.enum import nwDocMode, nwItemClass, nwItemLayout, nwItemType from novelwriter.gui.projtree import _TreeContextMenu +from novelwriter.shared import _GuiAlert from novelwriter.types import ( QtAccepted, QtModNone, QtMouseLeft, QtMouseMiddle, QtRejected, QtScrollAlwaysOff, QtScrollAsNeeded @@ -627,7 +628,7 @@ def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, m # User can cancel move to trash with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) projTree.processDeleteRequest(hScenes, askFirst=True) assert [n.item.itemName for n in tree.model.root.allChildren()] == [ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", @@ -645,7 +646,7 @@ def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, m # User can block permanent deletion with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) projTree.processDeleteRequest(hScenes[0:2], askFirst=True) assert [n.item.itemName for n in tree.model.root.allChildren()] == [ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", @@ -677,7 +678,7 @@ def testGuiProjTree_DeleteRequest(qtbot, caplog, monkeypatch, nwGUI, projPath, m # Trash can be completely emptied, but user can block it with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) projTree.emptyTrash() assert [n.item.itemName for n in tree.model.root.allChildren()] == [ "Novel", "Title Page", "Chapter Folder", "Plot", "Characters", "Trash", @@ -995,7 +996,7 @@ def testGuiProjTree_Duplicate(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Duplicate title page, but select no with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QtRejected) + mp.setattr(_GuiAlert, "finalState", False) projTree.duplicateFromHandle(C.hTitlePage) assert [n.item.itemName for n in tree.model.root.allChildren()] == [ "Novel", "Title Page", "New Folder", "New Chapter", "New Scene", @@ -1302,7 +1303,7 @@ def testGuiProjTree_ContextMenu(qtbot, monkeypatch, nwGUI, projPath, mockRnd): # Click no on the dialog with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QtRejected) + mp.setattr(_GuiAlert, "finalState", False) ctxMenu._convertFolderToFile(nwItemLayout.DOCUMENT) assert nodeOne.item.isFolderType() diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py index 91924120..35ddb7fa 100644 --- a/tests/test_tools/test_tools_manusbuild.py +++ b/tests/test_tools/test_tools_manusbuild.py @@ -26,13 +26,14 @@ import pytest from PyQt6.QtCore import QUrl from PyQt6.QtGui import QDesktopServices -from PyQt6.QtWidgets import QFileDialog, QListWidgetItem, QMessageBox +from PyQt6.QtWidgets import QFileDialog, QListWidgetItem from pytestqt.qtbot import QtBot from novelwriter.constants import nwLabels from novelwriter.core.buildsettings import BuildSettings from novelwriter.enum import nwBuildFmt from novelwriter.guimain import GuiMain +from novelwriter.shared import _GuiAlert from novelwriter.tools.manusbuild import GuiManuscriptBuild from novelwriter.types import QtDialogClose @@ -134,7 +135,7 @@ def testToolManuscriptBuild_Main( manus.buildPath.setText(str(fncPath)) manus.buildName.setText("TestBuild") with monkeypatch.context() as mp: - mp.setattr(QMessageBox, "result", lambda *a: QMessageBox.StandardButton.No) + mp.setattr(_GuiAlert, "finalState", False) assert manus._runBuild() is False # Test that the open button works From 1b0c33984585640982e17c0837452b71c8677082 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 25 Oct 2025 23:31:47 +0200 Subject: [PATCH 05/10] Add more icons --- .../assets/icons/material_filled_normal.icons | 8 +++++- .../assets/icons/material_filled_thin.icons | 8 +++++- .../icons/material_rounded_normal.icons | 8 +++++- .../assets/icons/material_rounded_thin.icons | 8 +++++- .../assets/icons/material_sharp_normal.icons | 8 +++++- .../assets/icons/material_sharp_thin.icons | 8 +++++- novelwriter/enum.py | 28 +++++++++++-------- tests/files/all_icons.json | 6 ++++ utils/icon_themes.py | 6 ++++ utils/icon_themes/font_awesome.json | 6 ++++ utils/icon_themes/material_symbols.json | 8 +++++- utils/icon_themes/remix.json | 6 ++++ 12 files changed, 90 insertions(+), 18 deletions(-) diff --git a/novelwriter/assets/icons/material_filled_normal.icons b/novelwriter/assets/icons/material_filled_normal.icons index 1cd8cfb1..7b58f1a4 100644 --- a/novelwriter/assets/icons/material_filled_normal.icons +++ b/novelwriter/assets/icons/material_filled_normal.icons @@ -64,12 +64,18 @@ icon:btn_cancel = icon:btn_no = icon:btn_open = -icon:btn_close = +icon:btn_close = +icon:btn_save = icon:btn_browse = icon:btn_list = icon:btn_new = icon:btn_create = icon:btn_reset = +icon:btn_insert = +icon:btn_apply = +icon:btn_build = +icon:btn_print = +icon:btn_preview = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_filled_thin.icons b/novelwriter/assets/icons/material_filled_thin.icons index 2d399f22..0bf9758b 100644 --- a/novelwriter/assets/icons/material_filled_thin.icons +++ b/novelwriter/assets/icons/material_filled_thin.icons @@ -64,12 +64,18 @@ icon:btn_cancel = icon:btn_no = icon:btn_open = -icon:btn_close = +icon:btn_close = +icon:btn_save = icon:btn_browse = icon:btn_list = icon:btn_new = icon:btn_create = icon:btn_reset = +icon:btn_insert = +icon:btn_apply = +icon:btn_build = +icon:btn_print = +icon:btn_preview = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_rounded_normal.icons b/novelwriter/assets/icons/material_rounded_normal.icons index 044bdd5d..0ff44cbd 100644 --- a/novelwriter/assets/icons/material_rounded_normal.icons +++ b/novelwriter/assets/icons/material_rounded_normal.icons @@ -64,12 +64,18 @@ icon:btn_cancel = icon:btn_no = icon:btn_open = -icon:btn_close = +icon:btn_close = +icon:btn_save = icon:btn_browse = icon:btn_list = icon:btn_new = icon:btn_create = icon:btn_reset = +icon:btn_insert = +icon:btn_apply = +icon:btn_build = +icon:btn_print = +icon:btn_preview = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_rounded_thin.icons b/novelwriter/assets/icons/material_rounded_thin.icons index 22834e45..b5606512 100644 --- a/novelwriter/assets/icons/material_rounded_thin.icons +++ b/novelwriter/assets/icons/material_rounded_thin.icons @@ -64,12 +64,18 @@ icon:btn_cancel = icon:btn_no = icon:btn_open = -icon:btn_close = +icon:btn_close = +icon:btn_save = icon:btn_browse = icon:btn_list = icon:btn_new = icon:btn_create = icon:btn_reset = +icon:btn_insert = +icon:btn_apply = +icon:btn_build = +icon:btn_print = +icon:btn_preview = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_sharp_normal.icons b/novelwriter/assets/icons/material_sharp_normal.icons index 13c68127..a9b6ff25 100644 --- a/novelwriter/assets/icons/material_sharp_normal.icons +++ b/novelwriter/assets/icons/material_sharp_normal.icons @@ -64,12 +64,18 @@ icon:btn_cancel = icon:btn_no = icon:btn_open = -icon:btn_close = +icon:btn_close = +icon:btn_save = icon:btn_browse = icon:btn_list = icon:btn_new = icon:btn_create = icon:btn_reset = +icon:btn_insert = +icon:btn_apply = +icon:btn_build = +icon:btn_print = +icon:btn_preview = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/assets/icons/material_sharp_thin.icons b/novelwriter/assets/icons/material_sharp_thin.icons index c8bb13a8..11e04deb 100644 --- a/novelwriter/assets/icons/material_sharp_thin.icons +++ b/novelwriter/assets/icons/material_sharp_thin.icons @@ -64,12 +64,18 @@ icon:btn_cancel = icon:btn_no = icon:btn_open = -icon:btn_close = +icon:btn_close = +icon:btn_save = icon:btn_browse = icon:btn_list = icon:btn_new = icon:btn_create = icon:btn_reset = +icon:btn_insert = +icon:btn_apply = +icon:btn_build = +icon:btn_print = +icon:btn_preview = icon:add = icon:bookmarks = icon:browse = diff --git a/novelwriter/enum.py b/novelwriter/enum.py index 4db5cdce..0b88f8ab 100644 --- a/novelwriter/enum.py +++ b/novelwriter/enum.py @@ -252,14 +252,20 @@ class nwStatusShape(Enum): class nwStandardButton(Enum): """Enum: Standard Dialog Buttons.""" - OK = 0 - CANCEL = 1 - YES = 2 - NO = 3 - OPEN = 4 - CLOSE = 5 - BROWSE = 6 - LIST = 7 - NEW = 8 - CREATE = 9 - RESET = 10 + OK = 0 + CANCEL = 1 + YES = 2 + NO = 3 + OPEN = 4 + CLOSE = 5 + SAVE = 6 + BROWSE = 7 + LIST = 8 + NEW = 9 + CREATE = 10 + RESET = 11 + INSERT = 12 + APPLY = 13 + BUILD = 14 + PRINT = 15 + PREVIEW = 16 diff --git a/tests/files/all_icons.json b/tests/files/all_icons.json index 41797f20..0579a3df 100644 --- a/tests/files/all_icons.json +++ b/tests/files/all_icons.json @@ -66,11 +66,17 @@ "btn_no", "btn_open", "btn_close", + "btn_save", "btn_browse", "btn_list", "btn_new", "btn_create", "btn_reset", + "btn_insert", + "btn_apply", + "btn_build", + "btn_print", + "btn_preview", "add", "bookmarks", diff --git a/utils/icon_themes.py b/utils/icon_themes.py index e5c2268f..cd133dbc 100644 --- a/utils/icon_themes.py +++ b/utils/icon_themes.py @@ -113,11 +113,17 @@ ICONS = [ "btn_no", "btn_open", "btn_close", + "btn_save", "btn_browse", "btn_list", "btn_new", "btn_create", "btn_reset", + "btn_insert", + "btn_apply", + "btn_build", + "btn_print", + "btn_preview", "add", "bookmarks", diff --git a/utils/icon_themes/font_awesome.json b/utils/icon_themes/font_awesome.json index 1769409e..82f295fc 100644 --- a/utils/icon_themes/font_awesome.json +++ b/utils/icon_themes/font_awesome.json @@ -66,11 +66,17 @@ "btn_no": "circle-xmark", "btn_open": "file-arrow-up", "btn_close": "circle-xmark", + "btn_save": "floppy-disk", "btn_browse": "folder-open", "btn_list": "list", "btn_new": "plus", "btn_create": "star", "btn_reset": "rotate-left", + "btn_insert": "i-cursor", + "btn_apply": "square-check", + "btn_build": "up-right-from-square", + "btn_print": "print", + "btn_preview": "eye", "add": "plus", "bookmarks": "bookmark", diff --git a/utils/icon_themes/material_symbols.json b/utils/icon_themes/material_symbols.json index 3d1adc47..0727a21c 100644 --- a/utils/icon_themes/material_symbols.json +++ b/utils/icon_themes/material_symbols.json @@ -65,12 +65,18 @@ "btn_yes": "check_circle", "btn_no": "do_not_disturb_on", "btn_open": "open_in_new", - "btn_close": "close", + "btn_close": "cancel", + "btn_save": "file_save", "btn_browse": "folder_open", "btn_list": "format_list_bulleted", "btn_new": "new_window", "btn_create": "star", "btn_reset": "undo", + "btn_insert": "insert_text", + "btn_apply": "check_box", + "btn_build": "export_notes", + "btn_print": "print", + "btn_preview": "preview", "add": "add", "bookmarks": "bookmarks", diff --git a/utils/icon_themes/remix.json b/utils/icon_themes/remix.json index da78a84f..9e4bd207 100644 --- a/utils/icon_themes/remix.json +++ b/utils/icon_themes/remix.json @@ -66,11 +66,17 @@ "btn_no": "close-circle", "btn_open": "file-upload", "btn_close": "close-circle", + "btn_save": "save-3", "btn_browse": "folder-2", "btn_list": "list-unordered", "btn_new": "add", "btn_create": "star-fill", "btn_reset": "reset-left", + "btn_insert": "add-box", + "btn_apply": "checkbox", + "btn_build": "stack", + "btn_print": "printer", + "btn_preview": "eye", "add": "add", "bookmarks": "bookmark", From c9c2b354554dd1265e30222a57ca4f757cc6d544 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 25 Oct 2025 23:38:19 +0200 Subject: [PATCH 06/10] Update dialog buttons on all dialogs and tools --- novelwriter/dialogs/about.py | 10 +++++-- novelwriter/dialogs/docmerge.py | 23 ++++++++++------ novelwriter/dialogs/docsplit.py | 17 ++++++++---- novelwriter/dialogs/editlabel.py | 18 ++++++++---- novelwriter/dialogs/preferences.py | 17 ++++++++---- novelwriter/dialogs/projectsettings.py | 21 ++++++++------ novelwriter/dialogs/quotes.py | 21 ++++++++------ novelwriter/dialogs/wordlist.py | 16 +++++++---- novelwriter/gui/theme.py | 28 +++++++++++-------- novelwriter/tools/dictionaries.py | 12 +++++--- novelwriter/tools/lipsum.py | 23 ++++++++-------- novelwriter/tools/manusbuild.py | 38 +++++++++++--------------- novelwriter/tools/manuscript.py | 15 +++++----- novelwriter/tools/manussettings.py | 21 +++++++++----- novelwriter/tools/noveldetails.py | 12 +++++--- novelwriter/tools/writingstats.py | 31 ++++++++++----------- 16 files changed, 193 insertions(+), 130 deletions(-) diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index 59d4251f..429930cd 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -33,10 +33,11 @@ from PyQt6.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import readTextFile +from novelwriter.enum import nwStandardButton from novelwriter.extensions.configlayout import NColorLabel from novelwriter.extensions.modified import NDialog from novelwriter.extensions.versioninfo import VersionInfoWidget -from novelwriter.types import QtAlignRightTop, QtDialogClose, QtHexArgb +from novelwriter.types import QtAlignRightTop, QtHexArgb if TYPE_CHECKING: from PyQt6.QtGui import QCloseEvent @@ -82,8 +83,11 @@ class GuiAbout(NDialog): self.txtCredits.setViewportMargins(0, 8, 8, 0) # Buttons - self.btnBox = QDialogButtonBox(QtDialogClose, self) - self.btnBox.rejected.connect(self.reject) + self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self) + self.btnClose.clicked.connect(self.reject) + + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnClose, QDialogButtonBox.ButtonRole.RejectRole) # Assemble self.innerBox = QVBoxLayout() diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 2c392374..0c8bdfa2 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -33,10 +33,11 @@ from PyQt6.QtWidgets import ( ) from novelwriter import SHARED +from novelwriter.enum import nwStandardButton from novelwriter.extensions.configlayout import NColorLabel from novelwriter.extensions.modified import NDialog from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk, QtDialogReset, QtUserRole +from novelwriter.types import QtAccepted, QtUserRole logger = logging.getLogger(__name__) @@ -85,13 +86,19 @@ class GuiDocMerge(NDialog): self.optBox.setColumnStretch(2, 1) # Buttons - self.buttonBox = QDialogButtonBox(QtDialogOk | QtDialogCancel, self) - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) + self.btnOk = SHARED.theme.getStandardButton(nwStandardButton.OK, self) + self.btnOk.clicked.connect(self.accept) - self.resetButton = self.buttonBox.addButton(QtDialogReset) - if self.resetButton: - self.resetButton.clicked.connect(self._resetList) + self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self) + self.btnCancel.clicked.connect(self.reject) + + self.btnReset = SHARED.theme.getStandardButton(nwStandardButton.RESET, self) + self.btnReset.clicked.connect(self._resetList) + + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnOk, QDialogButtonBox.ButtonRole.AcceptRole) + self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.addButton(self.btnReset, QDialogButtonBox.ButtonRole.ResetRole) # Assemble self.outerBox = QVBoxLayout() @@ -103,7 +110,7 @@ class GuiDocMerge(NDialog): self.outerBox.addSpacing(8) self.outerBox.addLayout(self.optBox) self.outerBox.addSpacing(12) - self.outerBox.addWidget(self.buttonBox) + self.outerBox.addWidget(self.btnBox) self.setLayout(self.outerBox) # Load Content diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 5850aff3..7355409e 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -33,10 +33,11 @@ from PyQt6.QtWidgets import ( ) from novelwriter import SHARED +from novelwriter.enum import nwStandardButton from novelwriter.extensions.configlayout import NColorLabel from novelwriter.extensions.modified import NComboBox, NDialog from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk, QtUserRole +from novelwriter.types import QtAccepted, QtUserRole logger = logging.getLogger(__name__) @@ -117,9 +118,15 @@ class GuiDocSplit(NDialog): self.optBox.setColumnStretch(3, 1) # Buttons - self.buttonBox = QDialogButtonBox(QtDialogOk | QtDialogCancel, self) - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) + self.btnOk = SHARED.theme.getStandardButton(nwStandardButton.OK, self) + self.btnOk.clicked.connect(self.accept) + + self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self) + self.btnCancel.clicked.connect(self.reject) + + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnOk, QDialogButtonBox.ButtonRole.AcceptRole) + self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) # Assemble self.outerBox = QVBoxLayout() @@ -132,7 +139,7 @@ class GuiDocSplit(NDialog): self.outerBox.addSpacing(8) self.outerBox.addLayout(self.optBox) self.outerBox.addSpacing(12) - self.outerBox.addWidget(self.buttonBox) + self.outerBox.addWidget(self.btnBox) self.setLayout(self.outerBox) # Load Content diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py index b8b4bae4..4d56e07e 100644 --- a/novelwriter/dialogs/editlabel.py +++ b/novelwriter/dialogs/editlabel.py @@ -27,8 +27,10 @@ import logging from PyQt6.QtWidgets import QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QVBoxLayout, QWidget +from novelwriter import SHARED +from novelwriter.enum import nwStandardButton from novelwriter.extensions.modified import NDialog -from novelwriter.types import QtAccepted, QtDialogCancel, QtDialogOk +from novelwriter.types import QtAccepted logger = logging.getLogger(__name__) @@ -54,9 +56,15 @@ class GuiEditLabel(NDialog): self.lblValue.setBuddy(self.lblValue) # Buttons - self.buttonBox = QDialogButtonBox(QtDialogOk | QtDialogCancel, self) - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) + self.btnOk = SHARED.theme.getStandardButton(nwStandardButton.OK, self) + self.btnOk.clicked.connect(self.accept) + + self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self) + self.btnCancel.clicked.connect(self.reject) + + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnOk, QDialogButtonBox.ButtonRole.AcceptRole) + self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) # Assemble self.innerBox = QHBoxLayout() @@ -67,7 +75,7 @@ class GuiEditLabel(NDialog): self.outerBox = QVBoxLayout() self.outerBox.setSpacing(12) self.outerBox.addLayout(self.innerBox, 1) - self.outerBox.addWidget(self.buttonBox, 0) + self.outerBox.addWidget(self.btnBox, 0) self.setLayout(self.outerBox) diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 758f7908..7062f4ad 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -38,13 +38,14 @@ from novelwriter.common import compact, describeFont, processDialogSymbols, uniq from novelwriter.config import DEF_GUI_DARK, DEF_GUI_LIGHT, DEF_ICONS, DEF_TREECOL from novelwriter.constants import nwLabels, nwQuotes, nwUnicode, trConst from novelwriter.dialogs.quotes import GuiQuoteSelect +from novelwriter.enum import nwStandardButton from novelwriter.extensions.configlayout import NColorLabel, NScrollableForm from novelwriter.extensions.modified import ( NComboBox, NDialog, NDoubleSpinBox, NIconToolButton, NSpinBox ) from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtAlignCenter, QtDialogCancel, QtDialogSave +from novelwriter.types import QtAlignCenter logger = logging.getLogger(__name__) @@ -89,9 +90,15 @@ class GuiPreferences(NDialog): self.mainForm.setHelpTextStyle(SHARED.theme.helpText) # Buttons - self.buttonBox = QDialogButtonBox(QtDialogSave | QtDialogCancel, self) - self.buttonBox.accepted.connect(self._doSave) - self.buttonBox.rejected.connect(self.reject) + self.btnSave = SHARED.theme.getStandardButton(nwStandardButton.SAVE, self) + self.btnSave.clicked.connect(self._doSave) + + self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self) + self.btnCancel.clicked.connect(self.reject) + + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnSave, QDialogButtonBox.ButtonRole.AcceptRole) + self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) # Assemble self.searchBox = QHBoxLayout() @@ -107,7 +114,7 @@ class GuiPreferences(NDialog): self.outerBox = QVBoxLayout() self.outerBox.addLayout(self.searchBox) self.outerBox.addLayout(self.mainBox) - self.outerBox.addWidget(self.buttonBox) + self.outerBox.addWidget(self.btnBox) self.outerBox.setSpacing(8) self.setLayout(self.outerBox) diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index 5e16d52e..a6f090cf 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -41,15 +41,12 @@ from novelwriter import CONFIG, SHARED from novelwriter.common import formatFileFilter, qtAddAction, qtLambda, simplified from novelwriter.constants import nwLabels, trConst from novelwriter.core.status import CUSTOM_COL, NWStatus, StatusEntry -from novelwriter.enum import nwStatusShape +from novelwriter.enum import nwStandardButton, nwStatusShape from novelwriter.extensions.configlayout import NColorLabel, NFixedPage, NScrollableForm from novelwriter.extensions.modified import NComboBox, NDialog, NIconToolButton from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch -from novelwriter.types import ( - QtDialogCancel, QtDialogSave, QtSizeMinimum, QtSizeMinimumExpanding, - QtUserRole -) +from novelwriter.types import QtSizeMinimum, QtSizeMinimumExpanding, QtUserRole logger = logging.getLogger(__name__) @@ -95,9 +92,15 @@ class GuiProjectSettings(NDialog): self.sidebar.buttonClicked.connect(self._sidebarClicked) # Buttons - self.buttonBox = QDialogButtonBox(QtDialogSave | QtDialogCancel, self) - self.buttonBox.accepted.connect(self._doSave) - self.buttonBox.rejected.connect(self.reject) + self.btnSave = SHARED.theme.getStandardButton(nwStandardButton.SAVE, self) + self.btnSave.clicked.connect(self._doSave) + + self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self) + self.btnCancel.clicked.connect(self.reject) + + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnSave, QDialogButtonBox.ButtonRole.AcceptRole) + self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) # Content SHARED.project.countStatus() @@ -126,7 +129,7 @@ class GuiProjectSettings(NDialog): self.outerBox = QVBoxLayout() self.outerBox.addLayout(self.topBox) self.outerBox.addLayout(self.mainBox) - self.outerBox.addWidget(self.buttonBox) + self.outerBox.addWidget(self.btnBox) self.outerBox.setSpacing(8) self.setLayout(self.outerBox) diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py index 6030ecfe..2a813d39 100644 --- a/novelwriter/dialogs/quotes.py +++ b/novelwriter/dialogs/quotes.py @@ -32,12 +32,11 @@ from PyQt6.QtWidgets import ( QListWidgetItem, QVBoxLayout, QWidget ) +from novelwriter import SHARED from novelwriter.constants import nwQuotes, trConst +from novelwriter.enum import nwStandardButton from novelwriter.extensions.modified import NDialog -from novelwriter.types import ( - QtAccepted, QtAlignCenter, QtAlignTop, QtDialogCancel, QtDialogOk, - QtUserRole -) +from novelwriter.types import QtAccepted, QtAlignCenter, QtAlignTop, QtUserRole logger = logging.getLogger(__name__) @@ -91,9 +90,15 @@ class GuiQuoteSelect(NDialog): self.listBox.setMinimumHeight(150) # Buttons - self.buttonBox = QDialogButtonBox(QtDialogOk | QtDialogCancel, self) - self.buttonBox.accepted.connect(self.accept) - self.buttonBox.rejected.connect(self.reject) + self.btnOk = SHARED.theme.getStandardButton(nwStandardButton.OK, self) + self.btnOk.clicked.connect(self.accept) + + self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self) + self.btnCancel.clicked.connect(self.reject) + + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnOk, QDialogButtonBox.ButtonRole.AcceptRole) + self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) # Assemble self.labelBox.addWidget(self.previewLabel, 0, QtAlignTop) @@ -103,7 +108,7 @@ class GuiQuoteSelect(NDialog): self.innerBox.addWidget(self.listBox) self.outerBox.addLayout(self.innerBox) - self.outerBox.addWidget(self.buttonBox) + self.outerBox.addWidget(self.btnBox) self.setLayout(self.outerBox) diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index 19e2da9d..c1db00ee 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -37,9 +37,9 @@ from PyQt6.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import formatFileFilter from novelwriter.core.spellcheck import UserDictionary +from novelwriter.enum import nwStandardButton from novelwriter.extensions.configlayout import NColorLabel from novelwriter.extensions.modified import NDialog, NIconToolButton -from novelwriter.types import QtDialogClose, QtDialogSave if TYPE_CHECKING: from PyQt6.QtGui import QCloseEvent @@ -110,9 +110,15 @@ class GuiWordList(NDialog): self.editBox.addWidget(self.delButton, 0) # Buttons - self.buttonBox = QDialogButtonBox(QtDialogSave | QtDialogClose, self) - self.buttonBox.accepted.connect(self._doSave) - self.buttonBox.rejected.connect(self.reject) + self.btnSave = SHARED.theme.getStandardButton(nwStandardButton.SAVE, self) + self.btnSave.clicked.connect(self._doSave) + + self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self) + self.btnClose.clicked.connect(self.reject) + + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnSave, QDialogButtonBox.ButtonRole.AcceptRole) + self.btnBox.addButton(self.btnClose, QDialogButtonBox.ButtonRole.RejectRole) # Assemble self.outerBox = QVBoxLayout() @@ -120,7 +126,7 @@ class GuiWordList(NDialog): self.outerBox.addWidget(self.listBox, 1) self.outerBox.addLayout(self.editBox, 0) self.outerBox.addSpacing(12) - self.outerBox.addWidget(self.buttonBox, 0) + self.outerBox.addWidget(self.btnBox, 0) self.outerBox.setSpacing(4) self.setLayout(self.outerBox) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 441c9cd1..6293a9f6 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -57,17 +57,23 @@ STYLES_MIN_TOOLBUTTON = "minimalToolButton" STYLES_BIG_TOOLBUTTON = "bigToolButton" STANDARD_BUTTONS = { - nwStandardButton.OK: (QT_TRANSLATE_NOOP("Button", "OK"), "btn_ok", "blue"), - nwStandardButton.CANCEL: (QT_TRANSLATE_NOOP("Button", "Cancel"), "btn_cancel", "red"), - nwStandardButton.YES: (QT_TRANSLATE_NOOP("Button", "Yes"), "btn_yes", "green"), - nwStandardButton.NO: (QT_TRANSLATE_NOOP("Button", "No"), "btn_no", "red"), - nwStandardButton.OPEN: (QT_TRANSLATE_NOOP("Button", "Open"), "btn_open", "blue"), - nwStandardButton.CLOSE: (QT_TRANSLATE_NOOP("Button", "Close"), "btn_close", "red"), - nwStandardButton.BROWSE: (QT_TRANSLATE_NOOP("Button", "Browse"), "btn_browse", "yellow"), - nwStandardButton.LIST: (QT_TRANSLATE_NOOP("Button", "List"), "btn_list", "blue"), - nwStandardButton.NEW: (QT_TRANSLATE_NOOP("Button", "New"), "btn_new", "green"), - nwStandardButton.CREATE: (QT_TRANSLATE_NOOP("Button", "Create"), "btn_create", "yellow"), - nwStandardButton.RESET: (QT_TRANSLATE_NOOP("Button", "Reset"), "btn_reset", "green"), + nwStandardButton.OK: (QT_TRANSLATE_NOOP("Button", "OK"), "btn_ok", "blue"), + nwStandardButton.CANCEL: (QT_TRANSLATE_NOOP("Button", "Cancel"), "btn_cancel", "red"), + nwStandardButton.YES: (QT_TRANSLATE_NOOP("Button", "&Yes"), "btn_yes", "green"), + nwStandardButton.NO: (QT_TRANSLATE_NOOP("Button", "&No"), "btn_no", "red"), + nwStandardButton.OPEN: (QT_TRANSLATE_NOOP("Button", "Open"), "btn_open", "blue"), + nwStandardButton.CLOSE: (QT_TRANSLATE_NOOP("Button", "Close"), "btn_close", "faded"), + nwStandardButton.SAVE: (QT_TRANSLATE_NOOP("Button", "Save"), "btn_save", "blue"), + nwStandardButton.BROWSE: (QT_TRANSLATE_NOOP("Button", "Browse"), "btn_browse", "yellow"), + nwStandardButton.LIST: (QT_TRANSLATE_NOOP("Button", "List"), "btn_list", "blue"), + nwStandardButton.NEW: (QT_TRANSLATE_NOOP("Button", "New"), "btn_new", "green"), + nwStandardButton.CREATE: (QT_TRANSLATE_NOOP("Button", "Create"), "btn_create", "yellow"), + nwStandardButton.RESET: (QT_TRANSLATE_NOOP("Button", "Reset"), "btn_reset", "green"), + nwStandardButton.INSERT: (QT_TRANSLATE_NOOP("Button", "Insert"), "btn_insert", "blue"), + nwStandardButton.APPLY: (QT_TRANSLATE_NOOP("Button", "Apply"), "btn_apply", "blue"), + nwStandardButton.BUILD: (QT_TRANSLATE_NOOP("Button", "Build"), "btn_build", "blue"), + nwStandardButton.PRINT: (QT_TRANSLATE_NOOP("Button", "Print"), "btn_print", "blue"), + nwStandardButton.PREVIEW: (QT_TRANSLATE_NOOP("Button", "Preview"), "btn_preview", "blue"), } diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py index 5cc97021..06f26a95 100644 --- a/novelwriter/tools/dictionaries.py +++ b/novelwriter/tools/dictionaries.py @@ -37,9 +37,10 @@ from PyQt6.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import formatFileFilter, formatInt, getFileSize, openExternalPath +from novelwriter.enum import nwStandardButton from novelwriter.error import formatException from novelwriter.extensions.modified import NIconToolButton, NNonBlockingDialog -from novelwriter.types import QtDialogClose, QtHexArgb +from novelwriter.types import QtHexArgb logger = logging.getLogger(__name__) @@ -110,8 +111,11 @@ class GuiDictionaries(NNonBlockingDialog): self.infoBox.setFrameStyle(QFrame.Shape.NoFrame) # Buttons - self.buttonBox = QDialogButtonBox(QtDialogClose, self) - self.buttonBox.rejected.connect(self.reject) + self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self) + self.btnClose.clicked.connect(self.reject) + + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnClose, QDialogButtonBox.ButtonRole.AcceptRole) # Assemble self.outerBox = QVBoxLayout() @@ -123,7 +127,7 @@ class GuiDictionaries(NNonBlockingDialog): self.outerBox.addLayout(self.inBox, 0) self.outerBox.addWidget(self.infoBox, 1) self.outerBox.addSpacing(8) - self.outerBox.addWidget(self.buttonBox, 0) + self.outerBox.addWidget(self.btnBox, 0) self.setLayout(self.outerBox) diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index d28fb845..49b793a5 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -34,9 +34,10 @@ from PyQt6.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import readTextFile +from novelwriter.enum import nwStandardButton from novelwriter.extensions.modified import NDialog from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtAlignLeft, QtAlignRight, QtDialogClose, QtRoleAction +from novelwriter.types import QtAlignLeft, QtAlignRight logger = logging.getLogger(__name__) @@ -91,22 +92,22 @@ class GuiLipsum(NDialog): self.innerBox.addLayout(self.formBox) # Buttons - self.buttonBox = QDialogButtonBox(self) - self.buttonBox.rejected.connect(self.reject) + self.btnInsert = SHARED.theme.getStandardButton(nwStandardButton.INSERT, self) + self.btnInsert.clicked.connect(self._doInsert) + self.btnInsert.setAutoDefault(False) - self.btnClose = self.buttonBox.addButton(QtDialogClose) - if self.btnClose: - self.btnClose.setAutoDefault(False) + self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self) + self.btnClose.clicked.connect(self.reject) + self.btnClose.setAutoDefault(False) - self.btnInsert = self.buttonBox.addButton(self.tr("Insert"), QtRoleAction) - if self.btnInsert: - self.btnInsert.clicked.connect(self._doInsert) - self.btnInsert.setAutoDefault(False) + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnInsert, QDialogButtonBox.ButtonRole.ApplyRole) + self.btnBox.addButton(self.btnClose, QDialogButtonBox.ButtonRole.RejectRole) # Assemble self.outerBox = QVBoxLayout() self.outerBox.addLayout(self.innerBox) - self.outerBox.addWidget(self.buttonBox) + self.outerBox.addWidget(self.btnBox) self.outerBox.setSpacing(16) self.setLayout(self.outerBox) diff --git a/novelwriter/tools/manusbuild.py b/novelwriter/tools/manusbuild.py index 19070b14..c77898d7 100644 --- a/novelwriter/tools/manusbuild.py +++ b/novelwriter/tools/manusbuild.py @@ -32,7 +32,7 @@ from PyQt6.QtCore import QTimer, pyqtSlot from PyQt6.QtWidgets import ( QAbstractButton, QAbstractItemView, QDialogButtonBox, QFileDialog, QGridLayout, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, - QPushButton, QSplitter, QVBoxLayout, QWidget + QSplitter, QVBoxLayout, QWidget ) from novelwriter import SHARED @@ -40,10 +40,10 @@ from novelwriter.common import makeFileNameSafe, openExternalPath from novelwriter.constants import nwLabels from novelwriter.core.docbuild import NWBuildDocument from novelwriter.core.item import NWItem -from novelwriter.enum import nwBuildFmt -from novelwriter.extensions.modified import NDialog, NIconToolButton +from novelwriter.enum import nwBuildFmt, nwStandardButton +from novelwriter.extensions.modified import NDialog, NIconToolButton, NPushButton from novelwriter.extensions.progressbars import NProgressSimple -from novelwriter.types import QtAlignCenter, QtDialogClose, QtRoleAction, QtRoleReject, QtUserRole +from novelwriter.types import QtAlignCenter, QtRoleAction, QtRoleReject, QtUserRole if TYPE_CHECKING: from PyQt6.QtGui import QCloseEvent @@ -178,25 +178,19 @@ class GuiManuscriptBuild(NDialog): self.buildBox.setVerticalSpacing(4) # Dialog Buttons - self.buttonBox = QDialogButtonBox(self) - - self.btnOpen = QPushButton( - SHARED.theme.getIcon("browse", "yellow"), self.tr("Open Folder"), self - ) - self.btnOpen.setIconSize(bSz) + self.btnOpen = NPushButton(self, self.tr("Open Folder"), bSz, "browse", "yellow") self.btnOpen.setAutoDefault(False) - self.buttonBox.addButton(self.btnOpen, QtRoleAction) - self.btnBuild = QPushButton( - SHARED.theme.getIcon("sb_build", "blue"), self.tr("&Build"), self - ) - self.btnBuild.setIconSize(bSz) + self.btnBuild = SHARED.theme.getStandardButton(nwStandardButton.BUILD, self) self.btnBuild.setAutoDefault(True) - self.buttonBox.addButton(self.btnBuild, QtRoleAction) - self.btnClose = self.buttonBox.addButton(QtDialogClose) - if self.btnClose: - self.btnClose.setAutoDefault(False) + self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self) + self.btnClose.setAutoDefault(False) + + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnOpen, QtRoleAction) + self.btnBox.addButton(self.btnBuild, QtRoleAction) + self.btnBox.addButton(self.btnClose, QtRoleReject) # Assemble GUI # ============ @@ -223,7 +217,7 @@ class GuiManuscriptBuild(NDialog): self.outerBox.addSpacing(4) self.outerBox.addLayout(self.buildBox, 0) self.outerBox.addSpacing(16) - self.outerBox.addWidget(self.buttonBox, 0) + self.outerBox.addWidget(self.btnBox, 0) self.outerBox.setSpacing(0) self.setLayout(self.outerBox) @@ -239,7 +233,7 @@ class GuiManuscriptBuild(NDialog): # Signals self.btnReset.clicked.connect(self._doResetBuildName) self.btnBrowse.clicked.connect(self._doSelectPath) - self.buttonBox.clicked.connect(self._dialogButtonClicked) + self.btnBox.clicked.connect(self._dialogButtonClicked) self.listFormats.itemSelectionChanged.connect(self._resetProgress) logger.debug("Ready: GuiManuscriptBuild") @@ -266,7 +260,7 @@ class GuiManuscriptBuild(NDialog): @pyqtSlot("QAbstractButton*") def _dialogButtonClicked(self, button: QAbstractButton) -> None: """Handle button clicks from the dialog button box.""" - role = self.buttonBox.buttonRole(button) + role = self.btnBox.buttonRole(button) if role == QtRoleAction: if button == self.btnBuild: self._runBuild() diff --git a/novelwriter/tools/manuscript.py b/novelwriter/tools/manuscript.py index 8c458db3..79a93787 100644 --- a/novelwriter/tools/manuscript.py +++ b/novelwriter/tools/manuscript.py @@ -36,9 +36,9 @@ from PyQt6.QtGui import ( from PyQt6.QtPrintSupport import QPrinter, QPrintPreviewDialog from PyQt6.QtWidgets import ( QAbstractItemView, QApplication, QFormLayout, QGridLayout, QHBoxLayout, - QLabel, QListWidget, QListWidgetItem, QPushButton, QSplitter, - QStackedWidget, QTabWidget, QTextBrowser, QTreeWidget, QTreeWidgetItem, - QVBoxLayout, QWidget + QLabel, QListWidget, QListWidgetItem, QSplitter, QStackedWidget, + QTabWidget, QTextBrowser, QTreeWidget, QTreeWidgetItem, QVBoxLayout, + QWidget ) from novelwriter import CONFIG, SHARED @@ -46,6 +46,7 @@ from novelwriter.common import fuzzyTime, qtLambda from novelwriter.constants import nwHeadFmt, nwLabels, nwStats, nwUnicode, trStats from novelwriter.core.buildsettings import BuildCollection, BuildSettings from novelwriter.core.docbuild import NWBuildDocument +from novelwriter.enum import nwStandardButton from novelwriter.extensions.modified import NIconToggleButton, NIconToolButton, NToolDialog from novelwriter.extensions.progressbars import NProgressCircle from novelwriter.extensions.switch import NSwitch @@ -171,16 +172,16 @@ class GuiManuscript(NToolDialog): # Process Controls # ================ - self.btnPreview = QPushButton(self.tr("Preview"), self) + self.btnPreview = SHARED.theme.getStandardButton(nwStandardButton.PREVIEW, self) self.btnPreview.clicked.connect(self._generatePreview) - self.btnPrint = QPushButton(self.tr("Print"), self) + self.btnPrint = SHARED.theme.getStandardButton(nwStandardButton.PRINT, self) self.btnPrint.clicked.connect(self._printDocument) - self.btnBuild = QPushButton(self.tr("Build"), self) + self.btnBuild = SHARED.theme.getStandardButton(nwStandardButton.BUILD, self) self.btnBuild.clicked.connect(self._buildManuscript) - self.btnClose = QPushButton(self.tr("Close"), self) + self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self) self.btnClose.clicked.connect(qtLambda(self.close)) self.processBox = QGridLayout() diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index e8bf94b6..795c7eed 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -40,6 +40,7 @@ from novelwriter import CONFIG, SHARED from novelwriter.common import describeFont, fontMatcher, qtAddAction, qtLambda from novelwriter.constants import nwHeadFmt, nwKeyWords, nwLabels, nwUnicode, trConst from novelwriter.core.buildsettings import BuildSettings, FilterMode +from novelwriter.enum import nwStandardButton from novelwriter.extensions.configlayout import ( NColorLabel, NFixedPage, NScrollableForm, NScrollablePage ) @@ -50,9 +51,8 @@ from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch from novelwriter.extensions.switchbox import NSwitchBox from novelwriter.types import ( - QtAlignCenter, QtAlignLeft, QtDialogApply, QtDialogClose, QtDialogSave, - QtHeaderFixed, QtHeaderStretch, QtRoleAccept, QtRoleApply, QtRoleReject, - QtUserRole + QtAlignCenter, QtAlignLeft, QtHeaderFixed, QtHeaderStretch, QtRoleAccept, + QtRoleApply, QtRoleReject, QtUserRole ) if TYPE_CHECKING: @@ -125,8 +125,15 @@ class GuiBuildSettings(NToolDialog): self.toolStack.addWidget(self.optTabFormatting) # Buttons - self.buttonBox = QDialogButtonBox(QtDialogApply | QtDialogSave | QtDialogClose, self) - self.buttonBox.clicked.connect(self._dialogButtonClicked) + self.btnApply = SHARED.theme.getStandardButton(nwStandardButton.APPLY, self) + self.btnSave = SHARED.theme.getStandardButton(nwStandardButton.SAVE, self) + self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self) + + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnApply, QDialogButtonBox.ButtonRole.ApplyRole) + self.btnBox.addButton(self.btnSave, QDialogButtonBox.ButtonRole.AcceptRole) + self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.clicked.connect(self._dialogButtonClicked) # Assemble self.topBox = QHBoxLayout() @@ -143,7 +150,7 @@ class GuiBuildSettings(NToolDialog): self.outerBox = QVBoxLayout() self.outerBox.addLayout(self.topBox) self.outerBox.addLayout(self.mainBox) - self.outerBox.addWidget(self.buttonBox) + self.outerBox.addWidget(self.btnBox) self.outerBox.setSpacing(12) self.setLayout(self.outerBox) @@ -205,7 +212,7 @@ class GuiBuildSettings(NToolDialog): @pyqtSlot("QAbstractButton*") def _dialogButtonClicked(self, button: QAbstractButton) -> None: """Handle button clicks from the dialog button box.""" - role = self.buttonBox.buttonRole(button) + role = self.btnBox.buttonRole(button) if role == QtRoleApply: self._applyChanges() self._emitBuildData() diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py index 7447b553..bfec8938 100644 --- a/novelwriter/tools/noveldetails.py +++ b/novelwriter/tools/noveldetails.py @@ -38,12 +38,13 @@ from PyQt6.QtWidgets import ( from novelwriter import SHARED from novelwriter.common import formatTime, numberToRoman from novelwriter.constants import nwUnicode +from novelwriter.enum import nwStandardButton from novelwriter.extensions.configlayout import NColorLabel, NFixedPage, NScrollablePage from novelwriter.extensions.modified import NNonBlockingDialog from novelwriter.extensions.novelselector import NovelSelector from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtAlignRight, QtDecoration, QtDialogClose +from novelwriter.types import QtAlignRight, QtDecoration if TYPE_CHECKING: from PyQt6.QtGui import QCloseEvent @@ -102,8 +103,11 @@ class GuiNovelDetails(NNonBlockingDialog): self.mainStack.addWidget(self.contentsPage) # Buttons - self.buttonBox = QDialogButtonBox(QtDialogClose, self) - self.buttonBox.rejected.connect(self.reject) + self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self) + self.btnClose.clicked.connect(self.reject) + + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnClose, QDialogButtonBox.ButtonRole.RejectRole) # Assemble self.topBox = QHBoxLayout() @@ -119,7 +123,7 @@ class GuiNovelDetails(NNonBlockingDialog): self.outerBox = QVBoxLayout() self.outerBox.addLayout(self.topBox) self.outerBox.addLayout(self.mainBox) - self.outerBox.addWidget(self.buttonBox) + self.outerBox.addWidget(self.btnBox) self.outerBox.setSpacing(8) self.setLayout(self.outerBox) diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index 4494da8f..b6440e5c 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -39,13 +39,11 @@ from PyQt6.QtWidgets import ( from novelwriter import CONFIG, SHARED from novelwriter.common import checkInt, checkIntTuple, formatTime, minmax, qtLambda from novelwriter.constants import nwConst +from novelwriter.enum import nwStandardButton from novelwriter.error import formatException -from novelwriter.extensions.modified import NToolDialog +from novelwriter.extensions.modified import NPushButton, NToolDialog from novelwriter.extensions.switch import NSwitch -from novelwriter.types import ( - QtAlignLeftMiddle, QtAlignRight, QtAlignRightMiddle, QtDecoration, - QtDialogClose, QtRoleAction -) +from novelwriter.types import QtAlignLeftMiddle, QtAlignRight, QtAlignRightMiddle, QtDecoration if TYPE_CHECKING: from novelwriter.guimain import GuiMain @@ -182,6 +180,7 @@ class GuiWritingStats(NToolDialog): # Filter Options iPx = SHARED.theme.baseIconHeight + bSz = SHARED.theme.buttonIconSize self.filterForm = QGridLayout(self) self.filterForm.setRowStretch(6, 1) @@ -276,6 +275,10 @@ class GuiWritingStats(NToolDialog): self.optsBox.addWidget(self.histMax, 0) # Buttons + self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self) + self.btnClose.clicked.connect(self._doClose) + self.btnClose.setAutoDefault(False) + self.saveJSON = QAction(self.tr("JSON Data File (.json)"), self) self.saveJSON.triggered.connect(qtLambda(self._saveData, self.FMT_JSON)) @@ -286,17 +289,13 @@ class GuiWritingStats(NToolDialog): self.saveMenu.addAction(self.saveJSON) self.saveMenu.addAction(self.saveCSV) - self.buttonBox = QDialogButtonBox(self) - self.buttonBox.rejected.connect(self._doClose) + self.btnSave = NPushButton(self, self.tr("Save As"), bSz, "btn_save", "blue") + self.btnSave.setAutoDefault(False) + self.btnSave.setMenu(self.saveMenu) - self.btnClose = self.buttonBox.addButton(QtDialogClose) - if self.btnClose: - self.btnClose.setAutoDefault(False) - - self.btnSave = self.buttonBox.addButton(self.tr("Save As"), QtRoleAction) - if self.btnSave: - self.btnSave.setAutoDefault(False) - self.btnSave.setMenu(self.saveMenu) + self.btnBox = QDialogButtonBox(self) + self.btnBox.addButton(self.btnClose, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.addButton(self.btnSave, QDialogButtonBox.ButtonRole.ActionRole) # Assemble self.outerBox = QGridLayout() @@ -304,7 +303,7 @@ class GuiWritingStats(NToolDialog): self.outerBox.addLayout(self.optsBox, 1, 0, 1, 2) self.outerBox.addWidget(self.infoBox, 2, 0) self.outerBox.addWidget(self.filterBox, 2, 1) - self.outerBox.addWidget(self.buttonBox, 3, 0, 1, 2) + self.outerBox.addWidget(self.btnBox, 3, 0, 1, 2) self.outerBox.setRowStretch(0, 1) self.setLayout(self.outerBox) From ce939a40534b72819628e2063d8bcf8c7810f4c6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 25 Oct 2025 23:52:56 +0200 Subject: [PATCH 07/10] Clean up button types --- novelwriter/dialogs/about.py | 4 ++-- novelwriter/dialogs/docmerge.py | 8 ++++---- novelwriter/dialogs/docsplit.py | 6 +++--- novelwriter/dialogs/editlabel.py | 6 +++--- novelwriter/dialogs/preferences.py | 6 +++--- novelwriter/dialogs/projectsettings.py | 9 ++++++--- novelwriter/dialogs/quotes.py | 9 ++++++--- novelwriter/dialogs/wordlist.py | 5 +++-- novelwriter/tools/dictionaries.py | 4 ++-- novelwriter/tools/lipsum.py | 6 +++--- novelwriter/tools/manussettings.py | 8 ++++---- novelwriter/tools/noveldetails.py | 4 ++-- novelwriter/tools/writingstats.py | 9 ++++++--- novelwriter/types.py | 8 +------- 14 files changed, 48 insertions(+), 44 deletions(-) diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index 429930cd..080b3b21 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -37,7 +37,7 @@ from novelwriter.enum import nwStandardButton from novelwriter.extensions.configlayout import NColorLabel from novelwriter.extensions.modified import NDialog from novelwriter.extensions.versioninfo import VersionInfoWidget -from novelwriter.types import QtAlignRightTop, QtHexArgb +from novelwriter.types import QtAlignRightTop, QtHexArgb, QtRoleReject if TYPE_CHECKING: from PyQt6.QtGui import QCloseEvent @@ -87,7 +87,7 @@ class GuiAbout(NDialog): self.btnClose.clicked.connect(self.reject) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnClose, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.addButton(self.btnClose, QtRoleReject) # Assemble self.innerBox = QVBoxLayout() diff --git a/novelwriter/dialogs/docmerge.py b/novelwriter/dialogs/docmerge.py index 0c8bdfa2..9f3d6184 100644 --- a/novelwriter/dialogs/docmerge.py +++ b/novelwriter/dialogs/docmerge.py @@ -37,7 +37,7 @@ from novelwriter.enum import nwStandardButton from novelwriter.extensions.configlayout import NColorLabel from novelwriter.extensions.modified import NDialog from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtAccepted, QtUserRole +from novelwriter.types import QtAccepted, QtRoleAccept, QtRoleReject, QtRoleReset, QtUserRole logger = logging.getLogger(__name__) @@ -96,9 +96,9 @@ class GuiDocMerge(NDialog): self.btnReset.clicked.connect(self._resetList) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnOk, QDialogButtonBox.ButtonRole.AcceptRole) - self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) - self.btnBox.addButton(self.btnReset, QDialogButtonBox.ButtonRole.ResetRole) + self.btnBox.addButton(self.btnOk, QtRoleAccept) + self.btnBox.addButton(self.btnCancel, QtRoleReject) + self.btnBox.addButton(self.btnReset, QtRoleReset) # Assemble self.outerBox = QVBoxLayout() diff --git a/novelwriter/dialogs/docsplit.py b/novelwriter/dialogs/docsplit.py index 7355409e..059a1678 100644 --- a/novelwriter/dialogs/docsplit.py +++ b/novelwriter/dialogs/docsplit.py @@ -37,7 +37,7 @@ from novelwriter.enum import nwStandardButton from novelwriter.extensions.configlayout import NColorLabel from novelwriter.extensions.modified import NComboBox, NDialog from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtAccepted, QtUserRole +from novelwriter.types import QtAccepted, QtRoleAccept, QtRoleReject, QtUserRole logger = logging.getLogger(__name__) @@ -125,8 +125,8 @@ class GuiDocSplit(NDialog): self.btnCancel.clicked.connect(self.reject) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnOk, QDialogButtonBox.ButtonRole.AcceptRole) - self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.addButton(self.btnOk, QtRoleAccept) + self.btnBox.addButton(self.btnCancel, QtRoleReject) # Assemble self.outerBox = QVBoxLayout() diff --git a/novelwriter/dialogs/editlabel.py b/novelwriter/dialogs/editlabel.py index 4d56e07e..4e998aa8 100644 --- a/novelwriter/dialogs/editlabel.py +++ b/novelwriter/dialogs/editlabel.py @@ -30,7 +30,7 @@ from PyQt6.QtWidgets import QDialogButtonBox, QHBoxLayout, QLabel, QLineEdit, QV from novelwriter import SHARED from novelwriter.enum import nwStandardButton from novelwriter.extensions.modified import NDialog -from novelwriter.types import QtAccepted +from novelwriter.types import QtAccepted, QtRoleAccept, QtRoleReject logger = logging.getLogger(__name__) @@ -63,8 +63,8 @@ class GuiEditLabel(NDialog): self.btnCancel.clicked.connect(self.reject) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnOk, QDialogButtonBox.ButtonRole.AcceptRole) - self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.addButton(self.btnOk, QtRoleAccept) + self.btnBox.addButton(self.btnCancel, QtRoleReject) # Assemble self.innerBox = QHBoxLayout() diff --git a/novelwriter/dialogs/preferences.py b/novelwriter/dialogs/preferences.py index 7062f4ad..71cfca38 100644 --- a/novelwriter/dialogs/preferences.py +++ b/novelwriter/dialogs/preferences.py @@ -45,7 +45,7 @@ from novelwriter.extensions.modified import ( ) from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtAlignCenter +from novelwriter.types import QtAlignCenter, QtRoleAccept, QtRoleReject logger = logging.getLogger(__name__) @@ -97,8 +97,8 @@ class GuiPreferences(NDialog): self.btnCancel.clicked.connect(self.reject) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnSave, QDialogButtonBox.ButtonRole.AcceptRole) - self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.addButton(self.btnSave, QtRoleAccept) + self.btnBox.addButton(self.btnCancel, QtRoleReject) # Assemble self.searchBox = QHBoxLayout() diff --git a/novelwriter/dialogs/projectsettings.py b/novelwriter/dialogs/projectsettings.py index a6f090cf..dcb7f8de 100644 --- a/novelwriter/dialogs/projectsettings.py +++ b/novelwriter/dialogs/projectsettings.py @@ -46,7 +46,10 @@ from novelwriter.extensions.configlayout import NColorLabel, NFixedPage, NScroll from novelwriter.extensions.modified import NComboBox, NDialog, NIconToolButton from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtSizeMinimum, QtSizeMinimumExpanding, QtUserRole +from novelwriter.types import ( + QtRoleAccept, QtRoleReject, QtSizeMinimum, QtSizeMinimumExpanding, + QtUserRole +) logger = logging.getLogger(__name__) @@ -99,8 +102,8 @@ class GuiProjectSettings(NDialog): self.btnCancel.clicked.connect(self.reject) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnSave, QDialogButtonBox.ButtonRole.AcceptRole) - self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.addButton(self.btnSave, QtRoleAccept) + self.btnBox.addButton(self.btnCancel, QtRoleReject) # Content SHARED.project.countStatus() diff --git a/novelwriter/dialogs/quotes.py b/novelwriter/dialogs/quotes.py index 2a813d39..2e625ba0 100644 --- a/novelwriter/dialogs/quotes.py +++ b/novelwriter/dialogs/quotes.py @@ -36,7 +36,10 @@ from novelwriter import SHARED from novelwriter.constants import nwQuotes, trConst from novelwriter.enum import nwStandardButton from novelwriter.extensions.modified import NDialog -from novelwriter.types import QtAccepted, QtAlignCenter, QtAlignTop, QtUserRole +from novelwriter.types import ( + QtAccepted, QtAlignCenter, QtAlignTop, QtRoleAccept, QtRoleReject, + QtUserRole +) logger = logging.getLogger(__name__) @@ -97,8 +100,8 @@ class GuiQuoteSelect(NDialog): self.btnCancel.clicked.connect(self.reject) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnOk, QDialogButtonBox.ButtonRole.AcceptRole) - self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.addButton(self.btnOk, QtRoleAccept) + self.btnBox.addButton(self.btnCancel, QtRoleReject) # Assemble self.labelBox.addWidget(self.previewLabel, 0, QtAlignTop) diff --git a/novelwriter/dialogs/wordlist.py b/novelwriter/dialogs/wordlist.py index c1db00ee..1a05e187 100644 --- a/novelwriter/dialogs/wordlist.py +++ b/novelwriter/dialogs/wordlist.py @@ -40,6 +40,7 @@ from novelwriter.core.spellcheck import UserDictionary from novelwriter.enum import nwStandardButton from novelwriter.extensions.configlayout import NColorLabel from novelwriter.extensions.modified import NDialog, NIconToolButton +from novelwriter.types import QtRoleAccept, QtRoleReject if TYPE_CHECKING: from PyQt6.QtGui import QCloseEvent @@ -117,8 +118,8 @@ class GuiWordList(NDialog): self.btnClose.clicked.connect(self.reject) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnSave, QDialogButtonBox.ButtonRole.AcceptRole) - self.btnBox.addButton(self.btnClose, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.addButton(self.btnSave, QtRoleAccept) + self.btnBox.addButton(self.btnClose, QtRoleReject) # Assemble self.outerBox = QVBoxLayout() diff --git a/novelwriter/tools/dictionaries.py b/novelwriter/tools/dictionaries.py index 06f26a95..cf90d65a 100644 --- a/novelwriter/tools/dictionaries.py +++ b/novelwriter/tools/dictionaries.py @@ -40,7 +40,7 @@ from novelwriter.common import formatFileFilter, formatInt, getFileSize, openExt from novelwriter.enum import nwStandardButton from novelwriter.error import formatException from novelwriter.extensions.modified import NIconToolButton, NNonBlockingDialog -from novelwriter.types import QtHexArgb +from novelwriter.types import QtHexArgb, QtRoleReject logger = logging.getLogger(__name__) @@ -115,7 +115,7 @@ class GuiDictionaries(NNonBlockingDialog): self.btnClose.clicked.connect(self.reject) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnClose, QDialogButtonBox.ButtonRole.AcceptRole) + self.btnBox.addButton(self.btnClose, QtRoleReject) # Assemble self.outerBox = QVBoxLayout() diff --git a/novelwriter/tools/lipsum.py b/novelwriter/tools/lipsum.py index 49b793a5..a970fe93 100644 --- a/novelwriter/tools/lipsum.py +++ b/novelwriter/tools/lipsum.py @@ -37,7 +37,7 @@ from novelwriter.common import readTextFile from novelwriter.enum import nwStandardButton from novelwriter.extensions.modified import NDialog from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtAlignLeft, QtAlignRight +from novelwriter.types import QtAlignLeft, QtAlignRight, QtRoleApply, QtRoleReject logger = logging.getLogger(__name__) @@ -101,8 +101,8 @@ class GuiLipsum(NDialog): self.btnClose.setAutoDefault(False) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnInsert, QDialogButtonBox.ButtonRole.ApplyRole) - self.btnBox.addButton(self.btnClose, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.addButton(self.btnInsert, QtRoleApply) + self.btnBox.addButton(self.btnClose, QtRoleReject) # Assemble self.outerBox = QVBoxLayout() diff --git a/novelwriter/tools/manussettings.py b/novelwriter/tools/manussettings.py index 795c7eed..407eff5e 100644 --- a/novelwriter/tools/manussettings.py +++ b/novelwriter/tools/manussettings.py @@ -127,12 +127,12 @@ class GuiBuildSettings(NToolDialog): # Buttons self.btnApply = SHARED.theme.getStandardButton(nwStandardButton.APPLY, self) self.btnSave = SHARED.theme.getStandardButton(nwStandardButton.SAVE, self) - self.btnCancel = SHARED.theme.getStandardButton(nwStandardButton.CANCEL, self) + self.btnClose = SHARED.theme.getStandardButton(nwStandardButton.CLOSE, self) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnApply, QDialogButtonBox.ButtonRole.ApplyRole) - self.btnBox.addButton(self.btnSave, QDialogButtonBox.ButtonRole.AcceptRole) - self.btnBox.addButton(self.btnCancel, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.addButton(self.btnApply, QtRoleApply) + self.btnBox.addButton(self.btnSave, QtRoleAccept) + self.btnBox.addButton(self.btnClose, QtRoleReject) self.btnBox.clicked.connect(self._dialogButtonClicked) # Assemble diff --git a/novelwriter/tools/noveldetails.py b/novelwriter/tools/noveldetails.py index bfec8938..c0314e9b 100644 --- a/novelwriter/tools/noveldetails.py +++ b/novelwriter/tools/noveldetails.py @@ -44,7 +44,7 @@ from novelwriter.extensions.modified import NNonBlockingDialog from novelwriter.extensions.novelselector import NovelSelector from novelwriter.extensions.pagedsidebar import NPagedSideBar from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtAlignRight, QtDecoration +from novelwriter.types import QtAlignRight, QtDecoration, QtRoleReject if TYPE_CHECKING: from PyQt6.QtGui import QCloseEvent @@ -107,7 +107,7 @@ class GuiNovelDetails(NNonBlockingDialog): self.btnClose.clicked.connect(self.reject) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnClose, QDialogButtonBox.ButtonRole.RejectRole) + self.btnBox.addButton(self.btnClose, QtRoleReject) # Assemble self.topBox = QHBoxLayout() diff --git a/novelwriter/tools/writingstats.py b/novelwriter/tools/writingstats.py index b6440e5c..dcf3ce40 100644 --- a/novelwriter/tools/writingstats.py +++ b/novelwriter/tools/writingstats.py @@ -43,7 +43,10 @@ from novelwriter.enum import nwStandardButton from novelwriter.error import formatException from novelwriter.extensions.modified import NPushButton, NToolDialog from novelwriter.extensions.switch import NSwitch -from novelwriter.types import QtAlignLeftMiddle, QtAlignRight, QtAlignRightMiddle, QtDecoration +from novelwriter.types import ( + QtAlignLeftMiddle, QtAlignRight, QtAlignRightMiddle, QtDecoration, + QtRoleAction, QtRoleReject +) if TYPE_CHECKING: from novelwriter.guimain import GuiMain @@ -294,8 +297,8 @@ class GuiWritingStats(NToolDialog): self.btnSave.setMenu(self.saveMenu) self.btnBox = QDialogButtonBox(self) - self.btnBox.addButton(self.btnClose, QDialogButtonBox.ButtonRole.RejectRole) - self.btnBox.addButton(self.btnSave, QDialogButtonBox.ButtonRole.ActionRole) + self.btnBox.addButton(self.btnSave, QtRoleAction) + self.btnBox.addButton(self.btnClose, QtRoleReject) # Assemble self.outerBox = QGridLayout() diff --git a/novelwriter/types.py b/novelwriter/types.py index f972e04f..52a18351 100644 --- a/novelwriter/types.py +++ b/novelwriter/types.py @@ -93,17 +93,11 @@ QtMouseMiddle = Qt.MouseButton.MiddleButton QtAccepted = QDialog.DialogCode.Accepted QtRejected = QDialog.DialogCode.Rejected -QtDialogApply = QDialogButtonBox.StandardButton.Apply -QtDialogCancel = QDialogButtonBox.StandardButton.Cancel -QtDialogClose = QDialogButtonBox.StandardButton.Close -QtDialogOk = QDialogButtonBox.StandardButton.Ok -QtDialogReset = QDialogButtonBox.StandardButton.Reset -QtDialogSave = QDialogButtonBox.StandardButton.Save - QtRoleAccept = QDialogButtonBox.ButtonRole.AcceptRole QtRoleAction = QDialogButtonBox.ButtonRole.ActionRole QtRoleApply = QDialogButtonBox.ButtonRole.ApplyRole QtRoleReject = QDialogButtonBox.ButtonRole.RejectRole +QtRoleReset = QDialogButtonBox.ButtonRole.ResetRole # Cursor Types From 5f34e924d185b4f670fdb3963d9033d35a4c9cab Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 25 Oct 2025 23:53:18 +0200 Subject: [PATCH 08/10] Fix broken tests --- tests/test_dialogs/test_dlg_preferences.py | 14 ++----- tests/test_tools/test_tools_manusbuild.py | 9 +---- tests/test_tools/test_tools_manuscript.py | 9 +---- tests/test_tools/test_tools_manussettings.py | 41 +++++--------------- 4 files changed, 18 insertions(+), 55 deletions(-) diff --git a/tests/test_dialogs/test_dlg_preferences.py b/tests/test_dialogs/test_dlg_preferences.py index dd749aa5..f9509528 100644 --- a/tests/test_dialogs/test_dlg_preferences.py +++ b/tests/test_dialogs/test_dlg_preferences.py @@ -32,7 +32,7 @@ from novelwriter.constants import nwUnicode from novelwriter.dialogs.preferences import GuiPreferences from novelwriter.dialogs.quotes import GuiQuoteSelect from novelwriter.gui.theme import ThemeEntry -from novelwriter.types import QtDialogCancel, QtDialogSave, QtModNone +from novelwriter.types import QtModNone KEY_DELAY = 1 @@ -118,16 +118,12 @@ def testDlgPreferences_Actions(qtbot, monkeypatch, nwGUI): # Check Save Button prefs.show() with qtbot.waitSignal(prefs.newPreferencesReady) as signal: - button = prefs.buttonBox.button(QtDialogSave) - assert button is not None - button.click() + prefs.btnSave.click() assert len(signal.args) == 4 # Check Close Button prefs.show() - button = prefs.buttonBox.button(QtDialogCancel) - assert button is not None - button.click() + prefs.btnCancel.click() assert prefs.isHidden() is True # Close Using Escape Key @@ -342,9 +338,7 @@ def testDlgPreferences_Settings(qtbot, monkeypatch, nwGUI, fncPath, tstPaths): with monkeypatch.context() as mp: mp.setattr(QFontDatabase, "families", lambda *a: ["TestFont"]) with qtbot.waitSignal(prefs.newPreferencesReady) as signal: - button = prefs.buttonBox.button(QtDialogSave) - assert button is not None - button.click() + prefs.btnSave.click() assert signal.args == [True, True, True, True] # Check Settings diff --git a/tests/test_tools/test_tools_manusbuild.py b/tests/test_tools/test_tools_manusbuild.py index 35ddb7fa..39875483 100644 --- a/tests/test_tools/test_tools_manusbuild.py +++ b/tests/test_tools/test_tools_manusbuild.py @@ -35,7 +35,6 @@ from novelwriter.enum import nwBuildFmt from novelwriter.guimain import GuiMain from novelwriter.shared import _GuiAlert from novelwriter.tools.manusbuild import GuiManuscriptBuild -from novelwriter.types import QtDialogClose from tests.tools import buildTestProject @@ -95,9 +94,7 @@ def testToolManuscriptBuild_Main( assert (fncPath / "TestBuild").with_suffix(nwLabels.BUILD_EXT[fmt]).exists() lastFmt = fmt - button = manus.buttonBox.button(QtDialogClose) - assert button is not None - manus._dialogButtonClicked(button) + manus._dialogButtonClicked(manus.btnClose) manus.deleteLater() assert build.lastBuildName == "TestBuild" @@ -151,7 +148,5 @@ def testToolManuscriptBuild_Main( assert lastUrl.startswith("file://") # Finish - button = manus.buttonBox.button(QtDialogClose) - assert button is not None - manus._dialogButtonClicked(button) + manus._dialogButtonClicked(manus.btnClose) # qtbot.stop() diff --git a/tests/test_tools/test_tools_manuscript.py b/tests/test_tools/test_tools_manuscript.py index 0e498ea1..0d74dbff 100644 --- a/tests/test_tools/test_tools_manuscript.py +++ b/tests/test_tools/test_tools_manuscript.py @@ -37,7 +37,6 @@ from novelwriter.core.buildsettings import BuildSettings from novelwriter.tools.manusbuild import GuiManuscriptBuild from novelwriter.tools.manuscript import GuiManuscript from novelwriter.tools.manussettings import GuiBuildSettings -from novelwriter.types import QtDialogApply, QtDialogSave from tests.tools import C, buildTestProject @@ -115,9 +114,7 @@ def testToolManuscript_Builds(qtbot, nwGUI, projPath): with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000): bSettings.newSettingsReady.connect(_testNewSettingsReady) - button = bSettings.buttonBox.button(QtDialogSave) - assert button is not None - button.click() + bSettings.btnSave.click() assert isinstance(build, BuildSettings) assert build.name == "Test Build" @@ -136,9 +133,7 @@ def testToolManuscript_Builds(qtbot, nwGUI, projPath): with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000): bSettings.newSettingsReady.connect(_testNewSettingsReady) - button = bSettings.buttonBox.button(QtDialogApply) - assert button is not None - button.click() # Should leave the dialog open + bSettings.btnApply.click() # Should leave the dialog open assert isinstance(build, BuildSettings) assert build.name == "Test Build" diff --git a/tests/test_tools/test_tools_manussettings.py b/tests/test_tools/test_tools_manussettings.py index b5ce7880..853fe6cb 100644 --- a/tests/test_tools/test_tools_manussettings.py +++ b/tests/test_tools/test_tools_manussettings.py @@ -33,7 +33,6 @@ from novelwriter.core.buildsettings import BuildSettings, FilterMode from novelwriter.tools.manussettings import ( GuiBuildSettings, _FilterTab, _FormattingTab, _HeadingsTab ) -from novelwriter.types import QtDialogApply, QtDialogClose, QtDialogSave from tests.tools import C, buildTestProject @@ -78,9 +77,7 @@ def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd): # Capture Apply button with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000): bSettings.newSettingsReady.connect(_testNewSettingsReady) - button = bSettings.buttonBox.button(QtDialogApply) - assert button is not None - bSettings._dialogButtonClicked(button) + bSettings._dialogButtonClicked(bSettings.btnApply) assert triggered @@ -89,9 +86,7 @@ def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd): with qtbot.waitSignal(bSettings.newSettingsReady, timeout=5000): bSettings.newSettingsReady.connect(_testNewSettingsReady) - button = bSettings.buttonBox.button(QtDialogSave) - assert button is not None - bSettings._dialogButtonClicked(button) + bSettings._dialogButtonClicked(bSettings.btnSave) assert triggered @@ -109,9 +104,7 @@ def testToolBuildSettings_Init(qtbot, nwGUI, projPath, mockRnd): assert triggered # Finish - button = bSettings.buttonBox.button(QtDialogClose) - assert button is not None - bSettings._dialogButtonClicked(button) + bSettings._dialogButtonClicked(bSettings.btnClose) # qtbot.stop() @@ -326,9 +319,7 @@ def testToolBuildSettings_Filter(qtbot, nwGUI, projPath, mockRnd): ] # Finish - button = bSettings.buttonBox.button(QtDialogClose) - assert button is not None - bSettings._dialogButtonClicked(button) + bSettings._dialogButtonClicked(bSettings.btnClose) # qtbot.stop() @@ -505,9 +496,7 @@ def testToolBuildSettings_Headings(qtbot, nwGUI): assert sBuild.getBool("headings.hideSection") is True # Finish - button = bSettings.buttonBox.button(QtDialogClose) - assert button is not None - bSettings._dialogButtonClicked(button) + bSettings._dialogButtonClicked(bSettings.btnClose) # qtbot.stop() @@ -579,9 +568,7 @@ def testToolBuildSettings_FormatTextContent(qtbot, nwGUI): assert sBuild.getBool("text.addNoteHeadings") is True # Finish - button = bSettings.buttonBox.button(QtDialogClose) - assert button is not None - bSettings._dialogButtonClicked(button) + bSettings._dialogButtonClicked(bSettings.btnClose) # qtbot.stop() @@ -657,9 +644,7 @@ def testToolBuildSettings_FormatTextFormat(monkeypatch, qtbot, nwGUI): assert fmtTab._textFont == font # Finish - button = bSettings.buttonBox.button(QtDialogClose) - assert button is not None - bSettings._dialogButtonClicked(button) + bSettings._dialogButtonClicked(bSettings.btnClose) # qtbot.stop() @@ -703,9 +688,7 @@ def testToolBuildSettings_FormatFirstLineIndent(monkeypatch, qtbot, nwGUI): assert sBuild.getBool("format.indentFirstPar") is True # Finish - button = bSettings.buttonBox.button(QtDialogClose) - assert button is not None - bSettings._dialogButtonClicked(button) + bSettings._dialogButtonClicked(bSettings.btnClose) # qtbot.stop() @@ -761,9 +744,7 @@ def testToolBuildSettings_FormatPageLayout(monkeypatch, qtbot, nwGUI): assert fmtTab.rightMargin.value() == 1.5 # Finish - button = bSettings.buttonBox.button(QtDialogClose) - assert button is not None - bSettings._dialogButtonClicked(button) + bSettings._dialogButtonClicked(bSettings.btnClose) # qtbot.stop() @@ -832,7 +813,5 @@ def testToolBuildSettings_FormatOutput(qtbot, nwGUI): assert fmtTab.odtPageHeader.text() == nwHeadFmt.DOC_AUTO # Finish - button = bSettings.buttonBox.button(QtDialogClose) - assert button is not None - bSettings._dialogButtonClicked(button) + bSettings._dialogButtonClicked(bSettings.btnClose) # qtbot.stop() From af36a3b1bf51a3bd809ae3c72dad433523397802 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sat, 25 Oct 2025 23:55:29 +0200 Subject: [PATCH 09/10] Remove extra qtbase translations for buttons --- i18n/README.md | 17 ----------------- i18n/qtbase.py | 49 ------------------------------------------------- utils/assets.py | 1 - 3 files changed, 67 deletions(-) delete mode 100644 i18n/qtbase.py diff --git a/i18n/README.md b/i18n/README.md index b2df46f0..6b8ce27f 100644 --- a/i18n/README.md +++ b/i18n/README.md @@ -114,23 +114,6 @@ You can now test the translation in novelWriter. The Preferences dialog should l language, so go ahead and select it. -### Missing QtBase Translations - -The default Qt dialogs also have translations, for instance for standard buttons like "Yes", "No", -"Ok", "Cancel", etc. Generally, these translation files are installed with the Qt libraries on your -system, and novelWriter will collect those translations from there. However, these translations are -missing for many languages. - -As a starting point, there is no need to translate any entries in the `.ts` files that are under -elements starting with the letter "Q", like "QPlatformTheme", "QWizard", etc. If these turn up in -English in novelWriter after activating a translation, it means they are probably missing in the Qt -library, and you may also need to translate these. - -These additional translation entries are generated from a file named `i18n/qtbase.py`, which is not -a file that novelWriter uses. It is there only to generate these additional entries for the `.ts` -files. - - ## Project Localisation Projects can have a different language setting than the GUI itself. The files with format diff --git a/i18n/qtbase.py b/i18n/qtbase.py deleted file mode 100644 index e40279cd..00000000 --- a/i18n/qtbase.py +++ /dev/null @@ -1,49 +0,0 @@ -""" -Qt Base Translation File -======================== - -This file causes Qt Linguist to generate translation entries for the Qt -elements that need translation in novelWriter for those languages who do -not yet have a qtbase_xx.qm file shipped with Qt. - -If a qtbase_xx.qm file already exists, do not add a translation for the -entries generated from this file. -""" # noqa - -from PyQt6.QtCore import QT_TRANSLATE_NOOP - -# QDialogButtonBox -# ================ - -QT_TRANSLATE_NOOP("QDialogButtonBox", "OK") - -# QGnomeTheme -# =========== - -QT_TRANSLATE_NOOP("QGnomeTheme", "&OK") -QT_TRANSLATE_NOOP("QGnomeTheme", "&Save") -QT_TRANSLATE_NOOP("QGnomeTheme", "&Cancel") -QT_TRANSLATE_NOOP("QGnomeTheme", "&Close") -QT_TRANSLATE_NOOP("QGnomeTheme", "Close without Saving") - -# QPlatformTheme -# ============== - -QT_TRANSLATE_NOOP("QPlatformTheme", "OK") -QT_TRANSLATE_NOOP("QPlatformTheme", "Save") -QT_TRANSLATE_NOOP("QPlatformTheme", "Save All") -QT_TRANSLATE_NOOP("QPlatformTheme", "Open") -QT_TRANSLATE_NOOP("QPlatformTheme", "&Yes") -QT_TRANSLATE_NOOP("QPlatformTheme", "Yes to &All") -QT_TRANSLATE_NOOP("QPlatformTheme", "&No") -QT_TRANSLATE_NOOP("QPlatformTheme", "N&o to All") -QT_TRANSLATE_NOOP("QPlatformTheme", "Abort") -QT_TRANSLATE_NOOP("QPlatformTheme", "Retry") -QT_TRANSLATE_NOOP("QPlatformTheme", "Ignore") -QT_TRANSLATE_NOOP("QPlatformTheme", "Close") -QT_TRANSLATE_NOOP("QPlatformTheme", "Cancel") -QT_TRANSLATE_NOOP("QPlatformTheme", "Discard") -QT_TRANSLATE_NOOP("QPlatformTheme", "Help") -QT_TRANSLATE_NOOP("QPlatformTheme", "Apply") -QT_TRANSLATE_NOOP("QPlatformTheme", "Reset") -QT_TRANSLATE_NOOP("QPlatformTheme", "Restore Defaults") diff --git a/utils/assets.py b/utils/assets.py index 7f0222cd..5822cc96 100644 --- a/utils/assets.py +++ b/utils/assets.py @@ -111,7 +111,6 @@ def updateTranslationSources(args: argparse.Namespace) -> None: print("") sources = list((ROOT_DIR / "novelwriter").glob("**/*.py")) - sources.insert(0, ROOT_DIR / "i18n" / "qtbase.py") for source in sources: print(source.relative_to(ROOT_DIR)) From a02254f5d5cdd8e67bae1167df13282074652198 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Sun, 26 Oct 2025 00:12:37 +0200 Subject: [PATCH 10/10] Make minor improvements to the code and fix test on MacOS --- novelwriter/extensions/modified.py | 7 +++---- tests/test_base/test_base_shared.py | 14 ++++++++++---- 2 files changed, 13 insertions(+), 8 deletions(-) diff --git a/novelwriter/extensions/modified.py b/novelwriter/extensions/modified.py index b802a4d9..ee1be5e7 100644 --- a/novelwriter/extensions/modified.py +++ b/novelwriter/extensions/modified.py @@ -210,12 +210,11 @@ class NPushButton(QPushButton): icon: str | None = None, color: str | None = None ) -> None: super().__init__(parent=parent) - self.setText(text) - self.setIconSize(iconSize) self._icon = icon self._color = color - if icon: - self.refreshIcon() + self.setText(text) + self.setIconSize(iconSize) + self.refreshIcon() def refreshIcon(self) -> None: """Reload the theme icon.""" diff --git a/tests/test_base/test_base_shared.py b/tests/test_base/test_base_shared.py index a020a694..0a37b624 100644 --- a/tests/test_base/test_base_shared.py +++ b/tests/test_base/test_base_shared.py @@ -20,6 +20,8 @@ along with this program. If not, see . """ # noqa from __future__ import annotations +import sys + from unittest.mock import MagicMock import pytest @@ -218,7 +220,8 @@ def testBaseSharedData_GuiAlert(): # Alert: Info alert.setAlertType(_GuiAlert.INFO, False) assert hasattr(alert, "_btnOk") - assert alert.windowTitle() == "Information" + if sys.platform != "darwin": # Not set on MacOS + assert alert.windowTitle() == "Information" alert._btnOk.click() assert alert.finalState is True alert._state = False @@ -226,7 +229,8 @@ def testBaseSharedData_GuiAlert(): # Alert: Warning alert.setAlertType(_GuiAlert.WARN, False) assert hasattr(alert, "_btnOk") - assert alert.windowTitle() == "Warning" + if sys.platform != "darwin": # Not set on MacOS + assert alert.windowTitle() == "Warning" alert._btnOk.click() assert alert.finalState is True alert._state = False @@ -234,7 +238,8 @@ def testBaseSharedData_GuiAlert(): # Alert: Error alert.setAlertType(_GuiAlert.ERROR, False) assert hasattr(alert, "_btnOk") - assert alert.windowTitle() == "Error" + if sys.platform != "darwin": # Not set on MacOS + assert alert.windowTitle() == "Error" alert._btnOk.click() assert alert.finalState is True alert._state = False @@ -243,7 +248,8 @@ def testBaseSharedData_GuiAlert(): alert.setAlertType(_GuiAlert.ASK, True) assert hasattr(alert, "_btnYes") assert hasattr(alert, "_btnNo") - assert alert.windowTitle() == "Question" + if sys.platform != "darwin": # Not set on MacOS + assert alert.windowTitle() == "Question" alert._btnYes.click() assert alert.finalState is True alert._btnNo.click()