diff --git a/novelwriter/dialogs/about.py b/novelwriter/dialogs/about.py index eaa91a8c..efc61388 100644 --- a/novelwriter/dialogs/about.py +++ b/novelwriter/dialogs/about.py @@ -25,7 +25,7 @@ from __future__ import annotations import logging -from PyQt6.QtGui import QCloseEvent, QColor +from PyQt6.QtGui import QCloseEvent from PyQt6.QtWidgets import ( QDialogButtonBox, QHBoxLayout, QLabel, QTextBrowser, QVBoxLayout, QWidget ) @@ -58,7 +58,6 @@ class GuiAbout(NDialog): # Logo and Banner 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.setPixmap(SHARED.theme.getPixmap("novelwriter", (nwPx, nwPx))) diff --git a/novelwriter/gui/theme.py b/novelwriter/gui/theme.py index c0a01c19..5e568253 100644 --- a/novelwriter/gui/theme.py +++ b/novelwriter/gui/theme.py @@ -71,7 +71,6 @@ class GuiTheme: self.themeUrl = "" self.themeLicense = "" self.themeLicenseUrl = "" - self.isLightTheme = True # GUI self.statNone = QColor(0, 0, 0) @@ -80,6 +79,7 @@ class GuiTheme: self.helpText = QColor(0, 0, 0) self.fadedText = QColor(0, 0, 0) self.errorText = QColor(255, 0, 0) + self.isDarkTheme = False # Loaded Syntax Settings # ====================== @@ -226,9 +226,7 @@ class GuiTheme: return False # Reset Palette - self._guiPalette = QApplication.style().standardPalette() - self._resetGuiColors() - self.iconCache.clear() + self._resetTheme() # Main sec = "Main" @@ -294,44 +292,60 @@ class GuiTheme: self.statSaved = self._parseColour(parser, sec, "statussaved") # Update Dependant Colours - backCol = self._guiPalette.window().color() - textCol = self._guiPalette.windowText().color() + # Based on: https://github.com/qt/qtbase/blob/dev/src/gui/kernel/qplatformtheme.cpp + 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 - light = backCol.lighter(150) - mid = backCol.darker(130) - midLight = mid.lighter(110) - dark = backCol.darker(150) - shadow = dark.darker(135) + QtColActive = QPalette.ColorGroup.Active + QtColInactive = QPalette.ColorGroup.Inactive + QtColDisabled = QPalette.ColorGroup.Disabled - self._guiPalette.setColor(QPalette.ColorRole.Light, light) - self._guiPalette.setColor(QPalette.ColorRole.Mid, mid) - self._guiPalette.setColor(QPalette.ColorRole.Midlight, midLight) - self._guiPalette.setColor(QPalette.ColorRole.Dark, dark) - self._guiPalette.setColor(QPalette.ColorRole.Shadow, shadow) + light = window.lighter(150) + mid = window.darker(130) + midLight = mid.lighter(110) + dark = window.darker(150) + shadow = dark.darker(135) + darkOff = dark.darker(150) + shadowOff = shadow.darker(150) - # Calculate Help Text - backLNess = backCol.lightnessF() - textLNess = textCol.lightnessF() - self.isLightTheme = backLNess > textLNess - if self.helpText == QColor(0, 0, 0): - if self.isLightTheme: - helpLCol = textLNess + 0.35*(backLNess - textLNess) - else: - helpLCol = backLNess + 0.65*(textLNess - backLNess) - self.helpText = QColor.fromHsl(0, 0, int(255*helpLCol)) - logger.debug( - "Computed help text colour: rgb(%d, %d, %d)", - self.helpText.red(), self.helpText.green(), self.helpText.blue() - ) + grey = QColor(120, 120, 120) if isDark else QColor(140, 140, 140) + dimmed = QColor(130, 130, 130) if isDark else QColor(190, 190, 190) + + placeholder = text + placeholder.setAlpha(128) + + self._guiPalette.setBrush(QPalette.ColorRole.Light, light) + self._guiPalette.setBrush(QPalette.ColorRole.Mid, mid) + self._guiPalette.setBrush(QPalette.ColorRole.Midlight, midLight) + self._guiPalette.setBrush(QPalette.ColorRole.Dark, dark) + self._guiPalette.setBrush(QPalette.ColorRole.Shadow, shadow) + + 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 self.iconCache.loadTheme(CONFIG.iconTheme) - # Apply styles + # Finalise + self.isDarkTheme = isDark QApplication.setPalette(self._guiPalette) - - # Reset stylesheets so that they are regenerated self._buildStyleSheets(self._guiPalette) return True @@ -437,14 +451,55 @@ class GuiTheme: # Internal Functions ## - def _resetGuiColors(self) -> None: + def _resetTheme(self) -> None: """Reset GUI colours to default values.""" - self.statNone = QColor(120, 120, 120) - self.statUnsaved = QColor(200, 15, 39) - self.statSaved = QColor(2, 133, 37) - self.helpText = QColor(0, 0, 0) - self.fadedText = QColor(128, 128, 128) - self.errorText = QColor(255, 0, 0) + palette = QPalette() + + text = palette.color(QPalette.ColorRole.Text) + window = palette.color(QPalette.ColorRole.Window) + isDark = text.lightnessF() > window.lightnessF() + + # 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 def _listConf(self, targetDict: dict, checkDir: Path) -> bool: @@ -466,7 +521,7 @@ class GuiTheme: self, parser: NWConfigParser, section: str, name: str, value: QPalette.ColorRole ) -> None: """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 def _buildStyleSheets(self, palette: QPalette) -> None: @@ -550,29 +605,8 @@ class GuiIcons: def clear(self) -> None: """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._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._svgColours = {} self._qIcons = {} self._headerDec = [] self._headerDecNarrow = [] @@ -640,7 +674,7 @@ class GuiIcons: map or the icon map. This function always returns a QPixmap. """ 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] else: logger.error("Decoration with name '%s' does not exist", name) diff --git a/novelwriter/tools/welcome.py b/novelwriter/tools/welcome.py index 7dc2298e..88126797 100644 --- a/novelwriter/tools/welcome.py +++ b/novelwriter/tools/welcome.py @@ -85,7 +85,7 @@ class GuiWelcome(NDialog): self.bgImage = SHARED.theme.loadDecoration("welcome") 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.setPixmap(SHARED.theme.getPixmap("novelwriter", (hF, hF))) diff --git a/tests/test_gui/test_gui_theme.py b/tests/test_gui/test_gui_theme.py index 29c96720..a3f3495b 100644 --- a/tests/test_gui/test_gui_theme.py +++ b/tests/test_gui/test_gui_theme.py @@ -166,16 +166,13 @@ def testGuiTheme_Theme(qtbot, monkeypatch, nwGUI, tstPaths): "[Palette]\n" "window = 0, 0, 0\n" "windowtext = 255, 255, 255\n" - "\n" - "[GUI]\n" - "helptext = 0, 0, 0\n" ) mainTheme._availThemes["test"] = mockTheme CONFIG.guiTheme = "test" assert mainTheme.loadTheme() is True - assert mainTheme.isLightTheme is False - assert mainTheme.helpText.getRgb() == (165, 165, 165, 255) + assert mainTheme.isDarkTheme is False + assert mainTheme.helpText.getRgb() == (190, 190, 190, 255) # Load Default Light Theme # ========================