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 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:
+40 -3
View File
@@ -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
+17 -16
View File
@@ -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
+38 -17
View File
@@ -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()