From 89a7c3e5cb4914441e607b0f48ddc43fdb4c8ba6 Mon Sep 17 00:00:00 2001 From: Veronica Berglyd Olsen <1619840+vkbo@users.noreply.github.com> Date: Mon, 2 Jun 2025 23:08:01 +0200 Subject: [PATCH] Add dark/light toggle button to sidebar --- novelwriter/constants.py | 13 ++++++++- novelwriter/gui/sidebar.py | 43 ++++++++++++++++++++++++++--- novelwriter/gui/theme.py | 33 ++++++++++++----------- novelwriter/guimain.py | 55 ++++++++++++++++++++++++++------------ 4 files changed, 107 insertions(+), 37 deletions(-) diff --git a/novelwriter/constants.py b/novelwriter/constants.py index c20b9c47..336c6318 100644 --- a/novelwriter/constants.py +++ b/novelwriter/constants.py @@ -28,7 +28,8 @@ from typing import Final from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication from novelwriter.enum import ( - nwBuildFmt, nwComment, nwItemClass, nwItemLayout, nwOutline, nwStatusShape + nwBuildFmt, nwComment, nwItemClass, nwItemLayout, nwOutline, nwStatusShape, + nwTheme ) @@ -453,6 +454,16 @@ class nwLabels: "blue": QT_TRANSLATE_NOOP("Constant", "Blue"), "purple": QT_TRANSLATE_NOOP("Constant", "Purple"), } + THEME_MODE_ICON: Final[dict[nwTheme, str]] = { + nwTheme.AUTO: "theme_auto", + nwTheme.LIGHT: "theme_light", + nwTheme.DARK: "theme_dark", + } + THEME_MODE_LABEL: Final[dict[nwTheme, str]] = { + nwTheme.AUTO: QT_TRANSLATE_NOOP("Constant", "System Theme"), + nwTheme.LIGHT: QT_TRANSLATE_NOOP("Constant", "Light Theme"), + nwTheme.DARK: QT_TRANSLATE_NOOP("Constant", "Dark Theme"), + } class nwHeadFmt: diff --git a/novelwriter/gui/sidebar.py b/novelwriter/gui/sidebar.py index f784623f..a4dd3c9c 100644 --- a/novelwriter/gui/sidebar.py +++ b/novelwriter/gui/sidebar.py @@ -27,12 +27,13 @@ import logging from typing import TYPE_CHECKING -from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal +from PyQt6.QtCore import QEvent, QPoint, QSize, pyqtSignal, pyqtSlot from PyQt6.QtWidgets import QMenu, QVBoxLayout, QWidget -from novelwriter import SHARED +from novelwriter import CONFIG, SHARED from novelwriter.common import qtLambda -from novelwriter.enum import nwView +from novelwriter.constants import nwLabels, trConst +from novelwriter.enum import nwTheme, nwView from novelwriter.extensions.eventfilters import StatusTipFilter from novelwriter.extensions.modified import NIconToolButton from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON @@ -77,6 +78,10 @@ class GuiSideBar(QWidget): self.tbOutline.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View"))) self.tbOutline.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.OUTLINE)) + self.tbTheme = NIconToolButton(self, iSz) + self.tbTheme.setToolTip(self.tr("Switch Colour Theme")) + self.tbTheme.clicked.connect(self._cycleColurTheme) + self.tbBuild = NIconToolButton(self, iSz) self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript"))) self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) @@ -109,6 +114,7 @@ class GuiSideBar(QWidget): self.outerBox.addWidget(self.tbOutline) self.outerBox.addWidget(self.tbBuild) self.outerBox.addStretch(1) + self.outerBox.addWidget(self.tbTheme) self.outerBox.addWidget(self.tbDetails) self.outerBox.addWidget(self.tbStats) self.outerBox.addWidget(self.tbSettings) @@ -131,6 +137,7 @@ class GuiSideBar(QWidget): self.tbSearch.setStyleSheet(buttonStyle) self.tbOutline.setStyleSheet(buttonStyle) self.tbBuild.setStyleSheet(buttonStyle) + self.tbTheme.setStyleSheet(buttonStyle) self.tbDetails.setStyleSheet(buttonStyle) self.tbStats.setStyleSheet(buttonStyle) self.tbSettings.setStyleSheet(buttonStyle) @@ -144,6 +151,36 @@ class GuiSideBar(QWidget): self.tbStats.setThemeIcon("sb_stats") self.tbSettings.setThemeIcon("settings") + self._setThemeModeIcon() + + return + + ## + # Private Slots + ## + + @pyqtSlot() + def _cycleColurTheme(self) -> None: + """Go to nex colour theme.""" + match CONFIG.themeMode: + case nwTheme.AUTO: + CONFIG.themeMode = nwTheme.LIGHT + case nwTheme.LIGHT: + CONFIG.themeMode = nwTheme.DARK + case nwTheme.DARK: + CONFIG.themeMode = nwTheme.AUTO + self.mainGui.checkThemeUpdate() + self._setThemeModeIcon() + return + + ## + # Internal Functions + ## + + def _setThemeModeIcon(self) -> None: + """Set the theme button icon.""" + self.tbTheme.setThemeIcon(nwLabels.THEME_MODE_ICON[CONFIG.themeMode]) + self.tbTheme.setToolTip(trConst(nwLabels.THEME_MODE_LABEL[CONFIG.themeMode])) return diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index 4a53f453..43a6ffe3 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -100,22 +100,22 @@ class GuiTheme: """ __slots__ = ( - "_availSyntax", "_availThemes", "_darkThemes", "_guiPalette", "_lightThemes", "_qColors", - "_styleSheets", "_svgColors", "_syntaxList", "_themeList", "baseButtonHeight", - "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", "fadedText", - "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", + "_availSyntax", "_availThemes", "_currentTheme", "_darkThemes", "_guiPalette", + "_lightThemes", "_qColors", "_styleSheets", "_svgColors", "_syntaxList", "_themeList", + "baseButtonHeight", "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", + "fadedText", "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", "getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getToggleIcon", "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText", - "iconCache", "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", "textNWidth", + "iconCache", "isDarkTheme", "syntaxTheme", "textNHeight", "textNWidth", "themeMeta", ) def __init__(self) -> None: - self.iconCache = GuiIcons(self) - - # GUI Theme + # Theme Objects self.themeMeta = ThemeMeta() + self.iconCache = GuiIcons(self) + self.syntaxTheme = SyntaxColors() self.isDarkTheme = False # Special Text Colours @@ -123,11 +123,8 @@ class GuiTheme: self.fadedText = QColor(0, 0, 0) self.errorText = QColor(255, 0, 0) - # Syntax Theme - self.syntaxMeta = ThemeMeta() - self.syntaxTheme = SyntaxColors() - # Load Themes + self._currentTheme = "" self._guiPalette = QPalette() self._themeList: list[T_ThemeEntry] = [] self._availThemes: dict[str, Path] = {} @@ -275,6 +272,10 @@ class GuiTheme: theme = DEF_GUI_LIGHT CONFIG.lightTheme = DEF_GUI_LIGHT + if theme == self._currentTheme: + logger.info("Theme '%s' is already loaded", theme) + return False + if not (file := self._availThemes.get(theme)): logger.error("Could not load GUI theme") return False @@ -387,7 +388,6 @@ class GuiTheme: text = self._guiPalette.text().color() window = self._guiPalette.window().color() highlight = self._guiPalette.highlight().color() - isDark = text.lightnessF() > window.lightnessF() QtColActive = QPalette.ColorGroup.Active QtColInactive = QPalette.ColorGroup.Inactive @@ -407,8 +407,8 @@ class GuiTheme: darkOff = dark.darker(150) shadowOff = ref.darker(150) - grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140) - dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) + grey = QColor(120, 120, 120) if darkMode else QColor(140, 140, 140) + dimmed = QColor(130, 130, 130) if darkMode else QColor(190, 190, 190) placeholder = QColor(text) placeholder.setAlpha(128) @@ -453,10 +453,11 @@ class GuiTheme: self.iconCache.loadTheme(CONFIG.iconTheme) # Finalise - self.isDarkTheme = isDark + self.isDarkTheme = darkMode QApplication.setPalette(self._guiPalette) self._buildStyleSheets(self._guiPalette) + self._currentTheme = theme CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}") return True diff --git a/novelwriter/guimain.py b/novelwriter/guimain.py index 601540f0..6af7b3c7 100644 --- a/novelwriter/guimain.py +++ b/novelwriter/guimain.py @@ -30,7 +30,7 @@ from datetime import datetime from pathlib import Path from time import time -from PyQt6.QtCore import Qt, QTimer, pyqtSlot +from PyQt6.QtCore import QEvent, Qt, QTimer, pyqtSlot from PyQt6.QtGui import QCloseEvent, QCursor, QIcon, QShortcut from PyQt6.QtWidgets import ( QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, @@ -902,10 +902,46 @@ class GuiMain(QMainWindow): return not self.splitView.isVisible() + def checkThemeUpdate(self) -> None: + """Load theme if mode changed.""" + if SHARED.theme.loadTheme(): + self.refreshThemeColors(syntax=True) + self.docEditor.initEditor() + self.docViewer.initViewer() + return + + def refreshThemeColors(self, syntax: bool) -> None: + """Refresh the GUI theme.""" + SHARED.theme.loadTheme() + self.setPalette(QApplication.palette()) + self.docEditor.updateTheme() + self.docViewer.updateTheme() + self.docViewerPanel.updateTheme() + self.sideBar.updateTheme() + self.projView.updateTheme() + self.novelView.updateTheme() + self.projSearch.updateTheme() + self.outlineView.updateTheme() + self.itemDetails.updateTheme() + self.mainStatus.updateTheme() + SHARED.project.tree.refreshAllItems() + + if syntax: + self.docEditor.updateSyntaxColors() + + return + ## # Events ## + def changeEvent(self, event: QEvent) -> None: + """Capture application change events.""" + if int(event.type()) == 210: + # ThemeChange + self.checkThemeUpdate() + return + def closeEvent(self, event: QCloseEvent) -> None: """Capture the closing event of the GUI and call the close function to handle all the close process steps. @@ -1054,22 +1090,7 @@ class GuiMain(QMainWindow): self.novelView.refreshCurrentTree() if theme: - SHARED.theme.loadTheme() - self.setPalette(QApplication.palette()) - self.docEditor.updateTheme() - self.docViewer.updateTheme() - self.docViewerPanel.updateTheme() - self.sideBar.updateTheme() - self.projView.updateTheme() - self.novelView.updateTheme() - self.projSearch.updateTheme() - self.outlineView.updateTheme() - self.itemDetails.updateTheme() - self.mainStatus.updateTheme() - SHARED.project.tree.refreshAllItems() - - if syntax: - self.docEditor.updateSyntaxColors() + self.refreshThemeColors(syntax=syntax) self.docEditor.initEditor() self.docViewer.initViewer()