104 lines
3.4 KiB
Python
104 lines
3.4 KiB
Python
from __future__ import annotations
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from PySide6.QtGui import QPalette, QColor, QGuiApplication
|
|
from PySide6.QtWidgets import QApplication
|
|
from PySide6.QtCore import QObject, Signal
|
|
|
|
|
|
class Theme(Enum):
|
|
SYSTEM = "system"
|
|
LIGHT = "light"
|
|
DARK = "dark"
|
|
ORANGE_ANCHOR = "#FFA500"
|
|
ORANGE_ANCHOR_VISITED = "#B38000"
|
|
|
|
|
|
@dataclass
|
|
class ThemeConfig:
|
|
theme: Theme = Theme.SYSTEM
|
|
|
|
|
|
class ThemeManager(QObject):
|
|
themeChanged = Signal(Theme)
|
|
|
|
def __init__(self, app: QApplication, cfg: ThemeConfig):
|
|
super().__init__()
|
|
self._app = app
|
|
self._cfg = cfg
|
|
|
|
# Follow OS if supported (Qt 6+)
|
|
hints = QGuiApplication.styleHints()
|
|
if hasattr(hints, "colorSchemeChanged"):
|
|
hints.colorSchemeChanged.connect(
|
|
lambda _: (self._cfg.theme == Theme.SYSTEM)
|
|
and self.apply(self._cfg.theme)
|
|
)
|
|
|
|
def current(self) -> Theme:
|
|
return self._cfg.theme
|
|
|
|
def set(self, theme: Theme):
|
|
self._cfg.theme = theme
|
|
self.apply(theme)
|
|
|
|
def apply(self, theme: Theme):
|
|
# Resolve "system"
|
|
if theme == Theme.SYSTEM:
|
|
hints = QGuiApplication.styleHints()
|
|
scheme = getattr(hints, "colorScheme", None)
|
|
if callable(scheme):
|
|
scheme = hints.colorScheme()
|
|
# 0=Light, 1=Dark; fall back to Light
|
|
theme = Theme.DARK if scheme == 1 else Theme.LIGHT
|
|
|
|
# Always use Fusion so palette applies consistently cross-platform
|
|
self._app.setStyle("Fusion")
|
|
|
|
if theme == Theme.DARK:
|
|
pal = self._dark_palette()
|
|
self._app.setPalette(pal)
|
|
self._app.setStyleSheet("")
|
|
else:
|
|
pal = self._light_palette()
|
|
self._app.setPalette(pal)
|
|
self._app.setStyleSheet("")
|
|
|
|
self.themeChanged.emit(theme)
|
|
|
|
# ----- Palettes -----
|
|
def _dark_palette(self) -> QPalette:
|
|
pal = QPalette()
|
|
base = QColor(35, 35, 35)
|
|
window = QColor(53, 53, 53)
|
|
text = QColor(220, 220, 220)
|
|
disabled = QColor(127, 127, 127)
|
|
focus = QColor(42, 130, 218)
|
|
|
|
pal.setColor(QPalette.Window, window)
|
|
pal.setColor(QPalette.WindowText, text)
|
|
pal.setColor(QPalette.Base, base)
|
|
pal.setColor(QPalette.AlternateBase, window)
|
|
pal.setColor(QPalette.ToolTipBase, window)
|
|
pal.setColor(QPalette.ToolTipText, text)
|
|
pal.setColor(QPalette.Text, text)
|
|
pal.setColor(QPalette.PlaceholderText, disabled)
|
|
pal.setColor(QPalette.Button, window)
|
|
pal.setColor(QPalette.ButtonText, text)
|
|
pal.setColor(QPalette.BrightText, QColor(255, 84, 84))
|
|
pal.setColor(QPalette.Highlight, focus)
|
|
pal.setColor(QPalette.HighlightedText, QColor(0, 0, 0))
|
|
pal.setColor(QPalette.Link, QColor(Theme.ORANGE_ANCHOR.value))
|
|
pal.setColor(QPalette.LinkVisited, QColor(Theme.ORANGE_ANCHOR_VISITED.value))
|
|
|
|
return pal
|
|
|
|
def _light_palette(self) -> QPalette:
|
|
# Let Qt provide its default light palette, but nudge a couple roles
|
|
pal = self._app.style().standardPalette()
|
|
pal.setColor(QPalette.Highlight, QColor(0, 120, 215))
|
|
pal.setColor(QPalette.HighlightedText, QColor(255, 255, 255))
|
|
pal.setColor(
|
|
QPalette.Link, QColor("#1a73e8")
|
|
) # Light blue for links in light mode
|
|
return pal
|