Add dark/light toggle button to sidebar

This commit is contained in:
Veronica Berglyd Olsen
2025-06-02 23:08:01 +02:00
parent 10475f32fb
commit 89a7c3e5cb
4 changed files with 107 additions and 37 deletions
+12 -1
View File
@@ -28,7 +28,8 @@ from typing import Final
from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication from PyQt6.QtCore import QT_TRANSLATE_NOOP, QCoreApplication
from novelwriter.enum import ( 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"), "blue": QT_TRANSLATE_NOOP("Constant", "Blue"),
"purple": QT_TRANSLATE_NOOP("Constant", "Purple"), "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: class nwHeadFmt:
+40 -3
View File
@@ -27,12 +27,13 @@ import logging
from typing import TYPE_CHECKING 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 PyQt6.QtWidgets import QMenu, QVBoxLayout, QWidget
from novelwriter import SHARED from novelwriter import CONFIG, SHARED
from novelwriter.common import qtLambda 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.eventfilters import StatusTipFilter
from novelwriter.extensions.modified import NIconToolButton from novelwriter.extensions.modified import NIconToolButton
from novelwriter.gui.theme import STYLES_BIG_TOOLBUTTON 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.setToolTip("{0} [Ctrl+Shift+T]".format(self.tr("Novel Outline View")))
self.tbOutline.clicked.connect(qtLambda(self.requestViewChange.emit, nwView.OUTLINE)) 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 = NIconToolButton(self, iSz)
self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript"))) self.tbBuild.setToolTip("{0} [F5]".format(self.tr("Build Manuscript")))
self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog) self.tbBuild.clicked.connect(self.mainGui.showBuildManuscriptDialog)
@@ -109,6 +114,7 @@ class GuiSideBar(QWidget):
self.outerBox.addWidget(self.tbOutline) self.outerBox.addWidget(self.tbOutline)
self.outerBox.addWidget(self.tbBuild) self.outerBox.addWidget(self.tbBuild)
self.outerBox.addStretch(1) self.outerBox.addStretch(1)
self.outerBox.addWidget(self.tbTheme)
self.outerBox.addWidget(self.tbDetails) self.outerBox.addWidget(self.tbDetails)
self.outerBox.addWidget(self.tbStats) self.outerBox.addWidget(self.tbStats)
self.outerBox.addWidget(self.tbSettings) self.outerBox.addWidget(self.tbSettings)
@@ -131,6 +137,7 @@ class GuiSideBar(QWidget):
self.tbSearch.setStyleSheet(buttonStyle) self.tbSearch.setStyleSheet(buttonStyle)
self.tbOutline.setStyleSheet(buttonStyle) self.tbOutline.setStyleSheet(buttonStyle)
self.tbBuild.setStyleSheet(buttonStyle) self.tbBuild.setStyleSheet(buttonStyle)
self.tbTheme.setStyleSheet(buttonStyle)
self.tbDetails.setStyleSheet(buttonStyle) self.tbDetails.setStyleSheet(buttonStyle)
self.tbStats.setStyleSheet(buttonStyle) self.tbStats.setStyleSheet(buttonStyle)
self.tbSettings.setStyleSheet(buttonStyle) self.tbSettings.setStyleSheet(buttonStyle)
@@ -144,6 +151,36 @@ class GuiSideBar(QWidget):
self.tbStats.setThemeIcon("sb_stats") self.tbStats.setThemeIcon("sb_stats")
self.tbSettings.setThemeIcon("settings") 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 return
+17 -16
View File
@@ -100,22 +100,22 @@ class GuiTheme:
""" """
__slots__ = ( __slots__ = (
"_availSyntax", "_availThemes", "_darkThemes", "_guiPalette", "_lightThemes", "_qColors", "_availSyntax", "_availThemes", "_currentTheme", "_darkThemes", "_guiPalette",
"_styleSheets", "_svgColors", "_syntaxList", "_themeList", "baseButtonHeight", "_lightThemes", "_qColors", "_styleSheets", "_svgColors", "_syntaxList", "_themeList",
"baseIconHeight", "baseIconSize", "buttonIconSize", "errorText", "fadedText", "baseButtonHeight", "baseIconHeight", "baseIconSize", "buttonIconSize", "errorText",
"fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration", "fadedText", "fontPixelSize", "fontPointSize", "getDecoration", "getHeaderDecoration",
"getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getToggleIcon", "getHeaderDecorationNarrow", "getIcon", "getItemIcon", "getPixmap", "getToggleIcon",
"guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText", "guiFont", "guiFontB", "guiFontBU", "guiFontFixed", "guiFontSmall", "helpText",
"iconCache", "isDarkTheme", "syntaxMeta", "syntaxTheme", "textNHeight", "textNWidth", "iconCache", "isDarkTheme", "syntaxTheme", "textNHeight", "textNWidth",
"themeMeta", "themeMeta",
) )
def __init__(self) -> None: def __init__(self) -> None:
self.iconCache = GuiIcons(self) # Theme Objects
# GUI Theme
self.themeMeta = ThemeMeta() self.themeMeta = ThemeMeta()
self.iconCache = GuiIcons(self)
self.syntaxTheme = SyntaxColors()
self.isDarkTheme = False self.isDarkTheme = False
# Special Text Colours # Special Text Colours
@@ -123,11 +123,8 @@ class GuiTheme:
self.fadedText = QColor(0, 0, 0) self.fadedText = QColor(0, 0, 0)
self.errorText = QColor(255, 0, 0) self.errorText = QColor(255, 0, 0)
# Syntax Theme
self.syntaxMeta = ThemeMeta()
self.syntaxTheme = SyntaxColors()
# Load Themes # Load Themes
self._currentTheme = ""
self._guiPalette = QPalette() self._guiPalette = QPalette()
self._themeList: list[T_ThemeEntry] = [] self._themeList: list[T_ThemeEntry] = []
self._availThemes: dict[str, Path] = {} self._availThemes: dict[str, Path] = {}
@@ -275,6 +272,10 @@ class GuiTheme:
theme = DEF_GUI_LIGHT theme = DEF_GUI_LIGHT
CONFIG.lightTheme = 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)): if not (file := self._availThemes.get(theme)):
logger.error("Could not load GUI theme") logger.error("Could not load GUI theme")
return False return False
@@ -387,7 +388,6 @@ class GuiTheme:
text = self._guiPalette.text().color() text = self._guiPalette.text().color()
window = self._guiPalette.window().color() window = self._guiPalette.window().color()
highlight = self._guiPalette.highlight().color() highlight = self._guiPalette.highlight().color()
isDark = text.lightnessF() > window.lightnessF()
QtColActive = QPalette.ColorGroup.Active QtColActive = QPalette.ColorGroup.Active
QtColInactive = QPalette.ColorGroup.Inactive QtColInactive = QPalette.ColorGroup.Inactive
@@ -407,8 +407,8 @@ class GuiTheme:
darkOff = dark.darker(150) darkOff = dark.darker(150)
shadowOff = ref.darker(150) shadowOff = ref.darker(150)
grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140) grey = QColor(120, 120, 120) if darkMode else QColor(140, 140, 140)
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) dimmed = QColor(130, 130, 130) if darkMode else QColor(190, 190, 190)
placeholder = QColor(text) placeholder = QColor(text)
placeholder.setAlpha(128) placeholder.setAlpha(128)
@@ -453,10 +453,11 @@ class GuiTheme:
self.iconCache.loadTheme(CONFIG.iconTheme) self.iconCache.loadTheme(CONFIG.iconTheme)
# Finalise # Finalise
self.isDarkTheme = isDark self.isDarkTheme = darkMode
QApplication.setPalette(self._guiPalette) QApplication.setPalette(self._guiPalette)
self._buildStyleSheets(self._guiPalette) self._buildStyleSheets(self._guiPalette)
self._currentTheme = theme
CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}") CONFIG.splashMessage(f"Loaded GUI theme: {meta.name}")
return True return True
+38 -17
View File
@@ -30,7 +30,7 @@ from datetime import datetime
from pathlib import Path from pathlib import Path
from time import time 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.QtGui import QCloseEvent, QCursor, QIcon, QShortcut
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox, QApplication, QFileDialog, QHBoxLayout, QMainWindow, QMessageBox,
@@ -902,10 +902,46 @@ class GuiMain(QMainWindow):
return not self.splitView.isVisible() 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 # 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: def closeEvent(self, event: QCloseEvent) -> None:
"""Capture the closing event of the GUI and call the close """Capture the closing event of the GUI and call the close
function to handle all the close process steps. function to handle all the close process steps.
@@ -1054,22 +1090,7 @@ class GuiMain(QMainWindow):
self.novelView.refreshCurrentTree() self.novelView.refreshCurrentTree()
if theme: if theme:
SHARED.theme.loadTheme() self.refreshThemeColors(syntax=syntax)
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.docEditor.initEditor() self.docEditor.initEditor()
self.docViewer.initViewer() self.docViewer.initViewer()