Complete GUI theme colour handling to match Qt's own

This commit is contained in:
Veronica Berglyd Olsen
2025-01-12 17:30:46 +01:00
parent 60130f067b
commit 029ac58a75
4 changed files with 103 additions and 73 deletions
+1 -2
View File
@@ -25,7 +25,7 @@ from __future__ import annotations
import logging import logging
from PyQt6.QtGui import QCloseEvent, QColor from PyQt6.QtGui import QCloseEvent
from PyQt6.QtWidgets import ( from PyQt6.QtWidgets import (
QDialogButtonBox, QHBoxLayout, QLabel, QTextBrowser, QVBoxLayout, QWidget QDialogButtonBox, QHBoxLayout, QLabel, QTextBrowser, QVBoxLayout, QWidget
) )
@@ -58,7 +58,6 @@ class GuiAbout(NDialog):
# Logo and Banner # Logo and Banner
self.nwImage = SHARED.theme.loadDecoration("nw-text", h=nwH) self.nwImage = SHARED.theme.loadDecoration("nw-text", h=nwH)
self.bgColor = QColor(255, 255, 255) if SHARED.theme.isLightTheme else QColor(54, 54, 54)
self.nwLogo = QLabel(self) self.nwLogo = QLabel(self)
self.nwLogo.setPixmap(SHARED.theme.getPixmap("novelwriter", (nwPx, nwPx))) self.nwLogo.setPixmap(SHARED.theme.getPixmap("novelwriter", (nwPx, nwPx)))
+99 -65
View File
@@ -71,7 +71,6 @@ class GuiTheme:
self.themeUrl = "" self.themeUrl = ""
self.themeLicense = "" self.themeLicense = ""
self.themeLicenseUrl = "" self.themeLicenseUrl = ""
self.isLightTheme = True
# GUI # GUI
self.statNone = QColor(0, 0, 0) self.statNone = QColor(0, 0, 0)
@@ -80,6 +79,7 @@ class GuiTheme:
self.helpText = QColor(0, 0, 0) self.helpText = QColor(0, 0, 0)
self.fadedText = QColor(0, 0, 0) self.fadedText = QColor(0, 0, 0)
self.errorText = QColor(255, 0, 0) self.errorText = QColor(255, 0, 0)
self.isDarkTheme = False
# Loaded Syntax Settings # Loaded Syntax Settings
# ====================== # ======================
@@ -226,9 +226,7 @@ class GuiTheme:
return False return False
# Reset Palette # Reset Palette
self._guiPalette = QApplication.style().standardPalette() self._resetTheme()
self._resetGuiColors()
self.iconCache.clear()
# Main # Main
sec = "Main" sec = "Main"
@@ -294,44 +292,60 @@ class GuiTheme:
self.statSaved = self._parseColour(parser, sec, "statussaved") self.statSaved = self._parseColour(parser, sec, "statussaved")
# Update Dependant Colours # Update Dependant Colours
backCol = self._guiPalette.window().color() # Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp
textCol = self._guiPalette.windowText().color() text = self._guiPalette.text().color()
window = self._guiPalette.window().color()
highlight = self._guiPalette.highlight().color()
isDark = text.lightnessF() > window.lightnessF()
# Calculate Based on Qt Fusion QtColActive = QPalette.ColorGroup.Active
light = backCol.lighter(150) QtColInactive = QPalette.ColorGroup.Inactive
mid = backCol.darker(130) QtColDisabled = QPalette.ColorGroup.Disabled
midLight = mid.lighter(110)
dark = backCol.darker(150)
shadow = dark.darker(135)
self._guiPalette.setColor(QPalette.ColorRole.Light, light) light = window.lighter(150)
self._guiPalette.setColor(QPalette.ColorRole.Mid, mid) mid = window.darker(130)
self._guiPalette.setColor(QPalette.ColorRole.Midlight, midLight) midLight = mid.lighter(110)
self._guiPalette.setColor(QPalette.ColorRole.Dark, dark) dark = window.darker(150)
self._guiPalette.setColor(QPalette.ColorRole.Shadow, shadow) shadow = dark.darker(135)
darkOff = dark.darker(150)
shadowOff = shadow.darker(150)
# Calculate Help Text grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140)
backLNess = backCol.lightnessF() dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
textLNess = textCol.lightnessF()
self.isLightTheme = backLNess > textLNess placeholder = text
if self.helpText == QColor(0, 0, 0): placeholder.setAlpha(128)
if self.isLightTheme:
helpLCol = textLNess + 0.35*(backLNess - textLNess) self._guiPalette.setBrush(QPalette.ColorRole.Light, light)
else: self._guiPalette.setBrush(QPalette.ColorRole.Mid, mid)
helpLCol = backLNess + 0.65*(textLNess - backLNess) self._guiPalette.setBrush(QPalette.ColorRole.Midlight, midLight)
self.helpText = QColor.fromHsl(0, 0, int(255*helpLCol)) self._guiPalette.setBrush(QPalette.ColorRole.Dark, dark)
logger.debug( self._guiPalette.setBrush(QPalette.ColorRole.Shadow, shadow)
"Computed help text colour: rgb(%d, %d, %d)",
self.helpText.red(), self.helpText.green(), self.helpText.blue() self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Text, dimmed)
) self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.WindowText, dimmed)
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.ButtonText, dimmed)
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Base, window)
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Dark, darkOff)
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Shadow, shadowOff)
self._guiPalette.setBrush(QPalette.ColorRole.PlaceholderText, placeholder)
self._guiPalette.setBrush(QtColActive, QPalette.ColorRole.Highlight, highlight)
self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Highlight, highlight)
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Highlight, grey)
if CONFIG.verQtValue >= 0x060600:
self._guiPalette.setBrush(QtColActive, QPalette.ColorRole.Accent, highlight)
self._guiPalette.setBrush(QtColInactive, QPalette.ColorRole.Accent, highlight)
self._guiPalette.setBrush(QtColDisabled, QPalette.ColorRole.Accent, grey)
# Load icons after theme is parsed # Load icons after theme is parsed
self.iconCache.loadTheme(CONFIG.iconTheme) self.iconCache.loadTheme(CONFIG.iconTheme)
# Apply styles # Finalise
self.isDarkTheme = isDark
QApplication.setPalette(self._guiPalette) QApplication.setPalette(self._guiPalette)
# Reset stylesheets so that they are regenerated
self._buildStyleSheets(self._guiPalette) self._buildStyleSheets(self._guiPalette)
return True return True
@@ -437,14 +451,55 @@ class GuiTheme:
# Internal Functions # Internal Functions
## ##
def _resetGuiColors(self) -> None: def _resetTheme(self) -> None:
"""Reset GUI colours to default values.""" """Reset GUI colours to default values."""
self.statNone = QColor(120, 120, 120) palette = QPalette()
self.statUnsaved = QColor(200, 15, 39)
self.statSaved = QColor(2, 133, 37) text = palette.color(QPalette.ColorRole.Text)
self.helpText = QColor(0, 0, 0) window = palette.color(QPalette.ColorRole.Window)
self.fadedText = QColor(128, 128, 128) isDark = text.lightnessF() > window.lightnessF()
self.errorText = QColor(255, 0, 0)
# Reset GUI Palette
faded = QColor(128, 128, 128)
dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190)
grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140)
red = QColor(242, 119, 122) if isDark else QColor(240, 40, 41)
orange = QColor(249, 145, 57) if isDark else QColor(245, 135, 31)
yellow = QColor(255, 204, 102) if isDark else QColor(234, 183, 0)
green = QColor(153, 204, 153) if isDark else QColor(113, 140, 0)
aqua = QColor(102, 204, 204) if isDark else QColor(62, 153, 159)
blue = QColor(102, 153, 204) if isDark else QColor(66, 113, 174)
purple = QColor(204, 153, 204) if isDark else QColor(137, 89, 168)
self.statNone = grey
self.statUnsaved = red
self.statSaved = green
self.helpText = dimmed
self.fadedText = faded
self.errorText = red
self._guiPalette = palette
# Reset Icons
icons = self.iconCache
icons.clear()
icons.setIconColor("default", text)
icons.setIconColor("faded", faded)
icons.setIconColor("red", red)
icons.setIconColor("orange", orange)
icons.setIconColor("yellow", yellow)
icons.setIconColor("green", green)
icons.setIconColor("aqua", aqua)
icons.setIconColor("blue", blue)
icons.setIconColor("purple", purple)
icons.setIconColor("root", blue)
icons.setIconColor("folder", yellow)
icons.setIconColor("file", text)
icons.setIconColor("title", green)
icons.setIconColor("chapter", red)
icons.setIconColor("scene", blue)
icons.setIconColor("note", yellow)
return return
def _listConf(self, targetDict: dict, checkDir: Path) -> bool: def _listConf(self, targetDict: dict, checkDir: Path) -> bool:
@@ -466,7 +521,7 @@ class GuiTheme:
self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole
) -> None: ) -> None:
"""Set a palette colour value from a config string.""" """Set a palette colour value from a config string."""
self._guiPalette.setColor(value, self._parseColour(parser, section, name)) self._guiPalette.setBrush(value, self._parseColour(parser, section, name))
return return
def _buildStyleSheets(self, palette: QPalette) -> None: def _buildStyleSheets(self, palette: QPalette) -> None:
@@ -550,29 +605,8 @@ class GuiIcons:
def clear(self) -> None: def clear(self) -> None:
"""Clear the icon cache.""" """Clear the icon cache."""
text = QApplication.palette().windowText().color()
default = text.name(QColor.NameFormat.HexRgb).encode("utf-8")
faded = self.mainTheme.fadedText.name(QColor.NameFormat.HexRgb).encode("utf-8")
self._svgData = {} self._svgData = {}
self._svgColours = { self._svgColours = {}
"default": default,
"faded": faded,
"red": b"#ff0000",
"orange": b"#ff7f00",
"yellow": b"#ffff00",
"green": b"#00ff00",
"aqua": b"#00ffff",
"blue": b"#0000ff",
"purple": b"#ff00ff",
"root": b"#0000ff",
"folder": b"#ffff00",
"file": default,
"title": b"#00ff00",
"chapter": b"#ff0000",
"scene": b"#0000ff",
"note": b"#ffff00",
}
self._qIcons = {} self._qIcons = {}
self._headerDec = [] self._headerDec = []
self._headerDecNarrow = [] self._headerDecNarrow = []
@@ -640,7 +674,7 @@ class GuiIcons:
map or the icon map. This function always returns a QPixmap. map or the icon map. This function always returns a QPixmap.
""" """
if name in self.IMAGE_MAP: if name in self.IMAGE_MAP:
idx = 0 if self.mainTheme.isLightTheme else 1 idx = int(self.mainTheme.isDarkTheme)
imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx] imgPath = CONFIG.assetPath("images") / self.IMAGE_MAP[name][idx]
else: else:
logger.error("Decoration with name '%s' does not exist", name) logger.error("Decoration with name '%s' does not exist", name)
+1 -1
View File
@@ -85,7 +85,7 @@ class GuiWelcome(NDialog):
self.bgImage = SHARED.theme.loadDecoration("welcome") self.bgImage = SHARED.theme.loadDecoration("welcome")
self.nwImage = SHARED.theme.loadDecoration("nw-text", h=hD) self.nwImage = SHARED.theme.loadDecoration("nw-text", h=hD)
self.bgColor = QColor(255, 255, 255) if SHARED.theme.isLightTheme else QColor(54, 54, 54) self.bgColor = QColor(54, 54, 54) if SHARED.theme.isDarkTheme else QColor(255, 255, 255)
self.nwLogo = QLabel(self) self.nwLogo = QLabel(self)
self.nwLogo.setPixmap(SHARED.theme.getPixmap("novelwriter", (hF, hF))) self.nwLogo.setPixmap(SHARED.theme.getPixmap("novelwriter", (hF, hF)))
+2 -5
View File
@@ -166,16 +166,13 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths):
"[Palette]\n" "[Palette]\n"
"window = 0, 0, 0\n" "window = 0, 0, 0\n"
"windowtext = 255, 255, 255\n" "windowtext = 255, 255, 255\n"
"\n"
"[GUI]\n"
"helptext = 0, 0, 0\n"
) )
mainTheme._availThemes["test"] = mockTheme mainTheme._availThemes["test"] = mockTheme
CONFIG.guiTheme = "test" CONFIG.guiTheme = "test"
assert mainTheme.loadTheme() is True assert mainTheme.loadTheme() is True
assert mainTheme.isLightTheme is False assert mainTheme.isDarkTheme is False
assert mainTheme.helpText.getRgb() == (165, 165, 165, 255) assert mainTheme.helpText.getRgb() == (190, 190, 190, 255)
# Load Default Light Theme # Load Default Light Theme
# ======================== # ========================