Complete GUI theme colour handling to match Qt's own
This commit is contained in:
@@ -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
@@ -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)
|
||||||
|
|||||||
@@ -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)))
|
||||||
|
|||||||
@@ -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
|
||||||
# ========================
|
# ========================
|
||||||
|
|||||||
Reference in New Issue
Block a user