mirror of
https://github.com/283375/arcaea-offline-pyside-ui.git
synced 2026-02-28 00:21:09 +00:00
wip: theme system
- Add theme id - WIP theme cache key - Force scheme (light/dark) for dynamic theme - でびるんちゃんかわいい
This commit is contained in:
@ -4,17 +4,17 @@ from typing import overload
|
||||
|
||||
import structlog
|
||||
from PySide6.QtCore import Property, QObject, QResource, Qt, Signal
|
||||
from PySide6.QtGui import QColor, QGuiApplication, QPalette
|
||||
from PySide6.QtGui import QGuiApplication, QPalette
|
||||
|
||||
from .material3 import Material3DynamicThemeImpl, Material3ThemeImpl
|
||||
from .qml import ThemeQmlExposer
|
||||
from .shared import ThemeImpl, ThemeInfo, _TCustomPalette, _TScheme
|
||||
from .shared import ThemeImpl, ThemeInfo, TThemeInfoCacheKey, _TCustomPalette, _TScheme
|
||||
|
||||
QML_IMPORT_NAME = "internal.ui.theme"
|
||||
QML_IMPORT_MAJOR_VERSION = 1
|
||||
QML_IMPORT_MINOR_VERSION = 0
|
||||
|
||||
_THEME_CACHES: dict[ThemeInfo, ThemeImpl] = {}
|
||||
_THEME_CACHES: dict[TThemeInfoCacheKey, ThemeImpl] = {}
|
||||
|
||||
logger: structlog.stdlib.BoundLogger = structlog.get_logger()
|
||||
|
||||
@ -27,30 +27,18 @@ class ThemeManager(QObject):
|
||||
super().__init__(parent)
|
||||
|
||||
self._qPalette = QPalette()
|
||||
self._customPalette: _TCustomPalette = {
|
||||
"primary": QColor.fromString("#616161"),
|
||||
"success": QColor.fromString("#616161"),
|
||||
"error": QColor.fromString("#616161"),
|
||||
}
|
||||
self._customPalette: _TCustomPalette = ThemeImpl.DEFAULT_CUSTOM_PALETTE
|
||||
|
||||
self._lastThemeInfo = ThemeInfo(
|
||||
series="material3",
|
||||
name="default",
|
||||
scheme=self.getCurrentScheme(),
|
||||
)
|
||||
self._lastThemeInfo = ThemeImpl().info
|
||||
self._qmlExposer = ThemeQmlExposer(themeImpl=ThemeImpl())
|
||||
|
||||
self._cacheMaterial3Theme(themeName="default", scheme="light")
|
||||
self._cacheMaterial3Theme(themeName="default", scheme="dark")
|
||||
self._cacheMaterial3Theme(themeName="tempest", scheme="light")
|
||||
self._cacheMaterial3Theme(themeName="tempest", scheme="dark")
|
||||
self._cacheMaterial3DynamicTheme(themeId="default")
|
||||
self._cacheMaterial3DynamicTheme(themeId="tempest")
|
||||
|
||||
self._cacheMaterial3DynamicTheme(themeName="default", scheme="light")
|
||||
self._cacheMaterial3DynamicTheme(themeName="default", scheme="dark")
|
||||
self._cacheMaterial3DynamicTheme(themeName="tempest", scheme="light")
|
||||
self._cacheMaterial3DynamicTheme(themeName="tempest", scheme="dark")
|
||||
self._cacheMaterial3DynamicTheme(themeId="devilun")
|
||||
self._cacheMaterial3DynamicTheme(themeId="kupya")
|
||||
|
||||
self.setTheme("material3-dynamic", "default")
|
||||
self.setTheme("material3-dynamic", "devilun")
|
||||
|
||||
def getCurrentScheme(self) -> _TScheme:
|
||||
qApp: QGuiApplication = QGuiApplication.instance() # pyright: ignore[reportAssignmentType]
|
||||
@ -60,52 +48,47 @@ class ThemeManager(QObject):
|
||||
else "light"
|
||||
)
|
||||
|
||||
def getMaterial3Theme(self, themeName: str, scheme: _TScheme) -> Material3ThemeImpl:
|
||||
themeDataResource = QResource(f":/themes/{themeName}.json")
|
||||
if not themeDataResource.isValid():
|
||||
raise ValueError(f"Material3 theme {themeName!r} not found")
|
||||
def _loadQResourceJson(self, resourcePath: str):
|
||||
resource = QResource(resourcePath)
|
||||
if not resource.isValid():
|
||||
raise ValueError(f"Resource {resourcePath!r} invalid")
|
||||
|
||||
themeData = json.loads(
|
||||
themeDataResource.uncompressedData().data().decode("utf-8")
|
||||
)
|
||||
return json.loads(resource.uncompressedData().data().decode("utf-8"))
|
||||
|
||||
def getMaterial3Theme(self, themeId: str, scheme: _TScheme) -> Material3ThemeImpl:
|
||||
themeData = self._loadQResourceJson(f":/themes/m3_{themeId}.json")
|
||||
return Material3ThemeImpl(themeData=themeData, scheme=scheme)
|
||||
|
||||
def getMaterial3DynamicTheme(
|
||||
self, themeName: str, scheme: _TScheme
|
||||
self, themeId: str, scheme: _TScheme
|
||||
) -> Material3DynamicThemeImpl:
|
||||
themeDataResource = QResource(f":/themes/{themeName}.json")
|
||||
if not themeDataResource.isValid():
|
||||
raise ValueError(f"Material3 theme {themeName!r} not found")
|
||||
|
||||
themeData = json.loads(
|
||||
themeDataResource.uncompressedData().data().decode("utf-8")
|
||||
)
|
||||
|
||||
return Material3DynamicThemeImpl(
|
||||
sourceColorHex=themeData["seed"],
|
||||
scheme=scheme,
|
||||
name=themeName,
|
||||
)
|
||||
themeData = self._loadQResourceJson(f":/themes/m3-dynamic_{themeId}.json")
|
||||
return Material3DynamicThemeImpl(themeData=themeData, scheme=scheme)
|
||||
|
||||
def _cacheTheme(self, *, themeImpl: ThemeImpl):
|
||||
_THEME_CACHES[themeImpl.info] = themeImpl
|
||||
_THEME_CACHES[themeImpl.info.cacheKey()] = themeImpl
|
||||
logger.debug("Theme %r cached", themeImpl.info)
|
||||
|
||||
def _getCachedTheme(self, *, themeInfo: ThemeInfo):
|
||||
cachedTheme = _THEME_CACHES.get(themeInfo)
|
||||
def _getCachedTheme(self, *, key: TThemeInfoCacheKey):
|
||||
cachedTheme = _THEME_CACHES.get(key)
|
||||
if cachedTheme is None:
|
||||
raise KeyError(f"Theme {themeInfo!r} not cached")
|
||||
raise KeyError(f"Theme {key!r} not cached")
|
||||
return cachedTheme
|
||||
|
||||
def _cacheMaterial3Theme(self, *, themeName: str, scheme: _TScheme):
|
||||
def _cacheMaterial3Theme(self, *, themeId: str):
|
||||
self._cacheTheme(
|
||||
themeImpl=self.getMaterial3Theme(themeName=themeName, scheme=scheme),
|
||||
themeImpl=self.getMaterial3Theme(themeId=themeId, scheme="light"),
|
||||
)
|
||||
self._cacheTheme(
|
||||
themeImpl=self.getMaterial3Theme(themeId=themeId, scheme="dark"),
|
||||
)
|
||||
|
||||
def _cacheMaterial3DynamicTheme(self, *, themeName: str, scheme: _TScheme):
|
||||
def _cacheMaterial3DynamicTheme(self, *, themeId: str):
|
||||
self._cacheTheme(
|
||||
themeImpl=self.getMaterial3DynamicTheme(themeName=themeName, scheme=scheme)
|
||||
themeImpl=self.getMaterial3DynamicTheme(themeId=themeId, scheme="light")
|
||||
)
|
||||
self._cacheTheme(
|
||||
themeImpl=self.getMaterial3DynamicTheme(themeId=themeId, scheme="dark")
|
||||
)
|
||||
|
||||
@overload
|
||||
@ -113,29 +96,29 @@ class ThemeManager(QObject):
|
||||
|
||||
@overload
|
||||
def setTheme(
|
||||
self, themeSeries: str, themeName: str, scheme: _TScheme | None = None, /
|
||||
self, themeSeries: str, themeId: str, scheme: _TScheme | None = None, /
|
||||
): ...
|
||||
|
||||
def setTheme(self, *args, **kwargs):
|
||||
if "themeInfo" in kwargs:
|
||||
themeInfo = kwargs["themeInfo"]
|
||||
cacheKey = kwargs["themeInfo"].cacheKey()
|
||||
elif 2 <= len(args) <= 3:
|
||||
themeSeries = args[0]
|
||||
themeName = args[1]
|
||||
themeId = args[1]
|
||||
schemeArg = args[2] if len(args) > 2 else None
|
||||
scheme = schemeArg or self.getCurrentScheme()
|
||||
|
||||
themeInfo = ThemeInfo(series=themeSeries, name=themeName, scheme=scheme)
|
||||
cacheKey: TThemeInfoCacheKey = (themeSeries, themeId, scheme)
|
||||
else:
|
||||
raise TypeError("Invalid setTheme() call")
|
||||
|
||||
logger.debug("Preparing to set theme %r", themeInfo)
|
||||
logger.debug("Preparing to set theme %r", cacheKey)
|
||||
|
||||
cachedTheme = self._getCachedTheme(themeInfo=themeInfo)
|
||||
cachedTheme = self._getCachedTheme(key=cacheKey)
|
||||
|
||||
self._qPalette = cachedTheme.qPalette
|
||||
self._customPalette = cachedTheme.customPalette
|
||||
self._lastThemeInfo = themeInfo
|
||||
self._lastThemeInfo = cachedTheme.info
|
||||
self._qmlExposer.themeImpl = cachedTheme
|
||||
|
||||
self.themeChanged.emit()
|
||||
|
||||
@ -1,5 +1,3 @@
|
||||
from typing import TypedDict
|
||||
|
||||
from materialyoucolor.blend import Blend
|
||||
from materialyoucolor.dynamiccolor.contrast_curve import ContrastCurve
|
||||
from materialyoucolor.dynamiccolor.dynamic_color import DynamicColor, FromPaletteOptions
|
||||
@ -10,48 +8,12 @@ from materialyoucolor.scheme.scheme_tonal_spot import SchemeTonalSpot
|
||||
from PySide6.QtGui import QColor, QPalette
|
||||
|
||||
from .shared import ThemeImpl, ThemeInfo, _TCustomPalette, _TScheme
|
||||
|
||||
|
||||
class _M3ThemeDataExtendedColorItem(TypedDict):
|
||||
name: str
|
||||
color: str
|
||||
description: str
|
||||
harmonized: bool
|
||||
|
||||
|
||||
_M3ThemeDataSchemes = TypedDict(
|
||||
"_M3ThemeDataSchemes",
|
||||
{
|
||||
"light": dict[str, str],
|
||||
"light-medium-contrast": dict[str, str],
|
||||
"light-high-contrast": dict[str, str],
|
||||
"dark": dict[str, str],
|
||||
"dark-medium-contrast": dict[str, str],
|
||||
"dark-high-contrast": dict[str, str],
|
||||
},
|
||||
from .types import (
|
||||
TMaterial3DynamicThemeData,
|
||||
TMaterial3ThemeData,
|
||||
TMaterial3ThemeDataExtendedColorItem,
|
||||
)
|
||||
|
||||
_M3ThemeDataPalettes = TypedDict(
|
||||
"_M3ThemeDataPalettes",
|
||||
{
|
||||
"primary": dict[str, str],
|
||||
"secondary": dict[str, str],
|
||||
"tertiary": dict[str, str],
|
||||
"neutral": dict[str, str],
|
||||
"neutral-variant": dict[str, str],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class _M3ThemeData(TypedDict):
|
||||
name: str
|
||||
description: str
|
||||
seed: str
|
||||
coreColors: dict[str, str]
|
||||
extendedColors: list[_M3ThemeDataExtendedColorItem]
|
||||
schemes: _M3ThemeDataSchemes
|
||||
palettes: _M3ThemeDataPalettes
|
||||
|
||||
|
||||
def _hexToHct(hexColor: str) -> Hct:
|
||||
pureHexPart = hexColor[1:] if hexColor.startswith("#") else hexColor
|
||||
@ -87,7 +49,7 @@ class Material3ThemeImpl(ThemeImpl):
|
||||
QPalette.ColorRole.LinkVisited: "tertiaryContainer",
|
||||
}
|
||||
|
||||
def __init__(self, *, themeData: _M3ThemeData, scheme: _TScheme):
|
||||
def __init__(self, *, themeData: TMaterial3ThemeData, scheme: _TScheme):
|
||||
self.themeData = themeData
|
||||
self.scheme: _TScheme = scheme
|
||||
|
||||
@ -96,7 +58,7 @@ class Material3ThemeImpl(ThemeImpl):
|
||||
|
||||
def _findExtendedColor(
|
||||
self, colorName: str
|
||||
) -> _M3ThemeDataExtendedColorItem | None:
|
||||
) -> TMaterial3ThemeDataExtendedColorItem | None:
|
||||
return next(
|
||||
(it for it in self.themeData["extendedColors"] if it["name"] == colorName),
|
||||
None,
|
||||
@ -106,6 +68,7 @@ class Material3ThemeImpl(ThemeImpl):
|
||||
def info(self):
|
||||
return ThemeInfo(
|
||||
series="material3",
|
||||
id=self.themeData["id"],
|
||||
name=self.themeData["name"],
|
||||
scheme=self.scheme,
|
||||
)
|
||||
@ -137,6 +100,8 @@ class Material3ThemeImpl(ThemeImpl):
|
||||
"primary": _hctToQColor(primaryHct),
|
||||
"success": _hctToQColor(successHarmonizedHct),
|
||||
"error": QColor.fromString(self.themeData["schemes"][self.scheme]["error"]),
|
||||
"toolTipBase": self.qPalette.color(QPalette.ColorRole.ToolTipBase),
|
||||
"toolTipText": self.qPalette.color(QPalette.ColorRole.ToolTipText),
|
||||
}
|
||||
|
||||
|
||||
@ -169,20 +134,45 @@ class Material3DynamicThemeImpl(ThemeImpl):
|
||||
"success": "#00c555",
|
||||
}
|
||||
|
||||
def __init__(self, sourceColorHex: str, scheme: _TScheme, *, name: str):
|
||||
def __init__(self, themeData: TMaterial3DynamicThemeData, scheme: _TScheme):
|
||||
force_scheme = themeData["options"].get("forceScheme")
|
||||
if force_scheme:
|
||||
is_dark = force_scheme == "dark"
|
||||
else:
|
||||
is_dark = scheme == "dark"
|
||||
|
||||
# TODO: more elegant way?
|
||||
self.preferredScheme: _TScheme = scheme # for theme caching
|
||||
self.actualScheme = "dark" if is_dark else "light"
|
||||
|
||||
self.themeId = themeData["id"]
|
||||
self.themeName = themeData["name"]
|
||||
|
||||
self.material3Scheme = SchemeTonalSpot(
|
||||
_hexToHct(sourceColorHex),
|
||||
is_dark=scheme == "dark",
|
||||
_hexToHct(themeData["colors"]["primary"]),
|
||||
is_dark=is_dark,
|
||||
contrast_level=0.0,
|
||||
)
|
||||
self.name = name
|
||||
|
||||
secondary_color = themeData["colors"].get("secondary")
|
||||
if secondary_color:
|
||||
self.material3Scheme.secondary_palette = TonalPalette.from_hct(
|
||||
_hexToHct(secondary_color)
|
||||
)
|
||||
|
||||
tertiary_color = themeData["colors"].get("tertiary")
|
||||
if tertiary_color:
|
||||
self.material3Scheme.tertiary_palette = TonalPalette.from_hct(
|
||||
_hexToHct(tertiary_color)
|
||||
)
|
||||
|
||||
@property
|
||||
def info(self):
|
||||
return ThemeInfo(
|
||||
series="material3-dynamic",
|
||||
name=self.name,
|
||||
scheme="dark" if self.material3Scheme.is_dark else "light",
|
||||
id=self.themeId,
|
||||
name=self.themeName,
|
||||
scheme=self.preferredScheme,
|
||||
)
|
||||
|
||||
@property
|
||||
@ -242,4 +232,6 @@ class Material3DynamicThemeImpl(ThemeImpl):
|
||||
extendedPalettes["success"].get_hct(self.material3Scheme)
|
||||
),
|
||||
"error": _hctToQColor(errorHct),
|
||||
"toolTipBase": self.qPalette.color(QPalette.ColorRole.ToolTipBase),
|
||||
"toolTipText": self.qPalette.color(QPalette.ColorRole.ToolTipText),
|
||||
}
|
||||
|
||||
@ -35,3 +35,11 @@ class ThemeQmlExposer(QObject):
|
||||
@Property(QColor, notify=themeChanged)
|
||||
def error(self):
|
||||
return self._themeImpl.customPalette["error"]
|
||||
|
||||
@Property(QColor, notify=themeChanged)
|
||||
def toolTipBase(self):
|
||||
return self._themeImpl.customPalette["toolTipBase"]
|
||||
|
||||
@Property(QColor, notify=themeChanged)
|
||||
def toolTipText(self):
|
||||
return self._themeImpl.customPalette["toolTipText"]
|
||||
|
||||
@ -9,30 +9,43 @@ class _TCustomPalette(TypedDict):
|
||||
success: QColor
|
||||
error: QColor
|
||||
|
||||
toolTipBase: QColor
|
||||
toolTipText: QColor
|
||||
|
||||
|
||||
_TScheme = Literal["light", "dark"]
|
||||
|
||||
TThemeInfoCacheKey = tuple[str, str, _TScheme]
|
||||
|
||||
|
||||
@dataclass
|
||||
class ThemeInfo:
|
||||
series: str
|
||||
id: str
|
||||
name: str
|
||||
scheme: _TScheme
|
||||
|
||||
def __hash__(self) -> int:
|
||||
return hash((self.series, self.name, self.scheme))
|
||||
def cacheKey(self) -> TThemeInfoCacheKey:
|
||||
return (self.series, self.id, self.scheme)
|
||||
|
||||
|
||||
class ThemeImpl:
|
||||
DEFAULT_CUSTOM_PALETTE = {
|
||||
DEFAULT_CUSTOM_PALETTE: _TCustomPalette = {
|
||||
"primary": QColor.fromString("#616161"),
|
||||
"success": QColor.fromString("#616161"),
|
||||
"error": QColor.fromString("#616161"),
|
||||
"toolTipBase": QColor.fromString("#616161"),
|
||||
"toolTipText": QColor.fromString("#616161"),
|
||||
}
|
||||
|
||||
@property
|
||||
def info(self) -> ThemeInfo:
|
||||
return ThemeInfo(series="placeholder", name="placeholder", scheme="dark")
|
||||
return ThemeInfo(
|
||||
series="placeholder",
|
||||
id="placeholder",
|
||||
name="placeholder",
|
||||
scheme="dark",
|
||||
)
|
||||
|
||||
@property
|
||||
def qPalette(self) -> QPalette:
|
||||
|
||||
72
ui/theme/types.py
Normal file
72
ui/theme/types.py
Normal file
@ -0,0 +1,72 @@
|
||||
from typing import TypedDict
|
||||
|
||||
from .shared import _TScheme
|
||||
|
||||
# region material3
|
||||
|
||||
|
||||
class TMaterial3ThemeDataExtendedColorItem(TypedDict):
|
||||
name: str
|
||||
color: str
|
||||
description: str
|
||||
harmonized: bool
|
||||
|
||||
|
||||
TMaterial3ThemeDataSchemes = TypedDict(
|
||||
"TMaterial3ThemeDataSchemes",
|
||||
{
|
||||
"light": dict[str, str],
|
||||
"light-medium-contrast": dict[str, str],
|
||||
"light-high-contrast": dict[str, str],
|
||||
"dark": dict[str, str],
|
||||
"dark-medium-contrast": dict[str, str],
|
||||
"dark-high-contrast": dict[str, str],
|
||||
},
|
||||
)
|
||||
|
||||
TMaterial3ThemeDataPalettes = TypedDict(
|
||||
"TMaterial3ThemeDataPalettes",
|
||||
{
|
||||
"primary": dict[str, str],
|
||||
"secondary": dict[str, str],
|
||||
"tertiary": dict[str, str],
|
||||
"neutral": dict[str, str],
|
||||
"neutral-variant": dict[str, str],
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
class TMaterial3ThemeData(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
description: str
|
||||
seed: str
|
||||
coreColors: dict[str, str]
|
||||
extendedColors: list[TMaterial3ThemeDataExtendedColorItem]
|
||||
schemes: TMaterial3ThemeDataSchemes
|
||||
palettes: TMaterial3ThemeDataPalettes
|
||||
|
||||
|
||||
# endregion
|
||||
|
||||
# region material3-dynamic
|
||||
|
||||
|
||||
class TMaterial3DynamicThemeDataColors(TypedDict):
|
||||
primary: str
|
||||
secondary: str | None
|
||||
tertiary: str | None
|
||||
|
||||
|
||||
class TMaterial3DynamicThemeDataOptions(TypedDict):
|
||||
forceScheme: _TScheme | None
|
||||
|
||||
|
||||
class TMaterial3DynamicThemeData(TypedDict):
|
||||
id: str
|
||||
name: str
|
||||
colors: TMaterial3DynamicThemeDataColors
|
||||
options: TMaterial3DynamicThemeDataOptions
|
||||
|
||||
|
||||
# endregion
|
||||
Reference in New Issue
Block a user