133 lines
5.2 KiB
Python
133 lines
5.2 KiB
Python
# -*- coding: utf-8 -*-
|
|
# gui/containers/_widget_style_service.py
|
|
"""Сервис локальной стилизации host-виджета без протекания style в subtree."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import re
|
|
from typing import Optional, TYPE_CHECKING
|
|
|
|
from PySide6.QtCore import Qt
|
|
|
|
from gui.styles import APP_STYLES
|
|
|
|
if TYPE_CHECKING:
|
|
from PySide6.QtWidgets import QWidget
|
|
|
|
|
|
_SELECTOR_BLOCK_RE = re.compile(r"([^{}]+)\{")
|
|
|
|
|
|
class WidgetStyleService:
|
|
"""Хранит explicit/inherited/effective style и применяет QSS только к host."""
|
|
|
|
def __init__(self, host: QWidget) -> None:
|
|
self._host = host
|
|
self._theme = "dark" if host.palette().window().color().lightness() < 128 else "light"
|
|
self._is_active = False
|
|
self._explicit_style_key: Optional[str] = None
|
|
self._explicit_active_style_key: Optional[str] = None
|
|
self._inherited_style_key: Optional[str] = None
|
|
self._last_style_role = ""
|
|
self._last_stylesheet = ""
|
|
self._last_styled_background = False
|
|
|
|
@property
|
|
def explicit_style_key(self) -> Optional[str]:
|
|
return self._explicit_style_key
|
|
|
|
@property
|
|
def inherited_style_key(self) -> Optional[str]:
|
|
return self._inherited_style_key
|
|
|
|
@property
|
|
def effective_style_key(self) -> Optional[str]:
|
|
return self._explicit_style_key or self._inherited_style_key
|
|
|
|
def has_explicit_style(self) -> bool:
|
|
return self._explicit_style_key is not None
|
|
|
|
def apply(
|
|
self,
|
|
style_key: Optional[str] = None,
|
|
active_key: Optional[str] = None,
|
|
is_active: Optional[bool] = None,
|
|
) -> bool:
|
|
previous_effective = self.effective_style_key
|
|
previous_render = self._current_render_key()
|
|
if style_key is not None:
|
|
self._explicit_style_key = style_key
|
|
self._explicit_active_style_key = style_key if active_key is None else active_key
|
|
elif active_key is not None:
|
|
self._explicit_active_style_key = active_key
|
|
if is_active is not None:
|
|
self._is_active = bool(is_active)
|
|
effective_changed = previous_effective != self.effective_style_key
|
|
if effective_changed or previous_render != self._current_render_key():
|
|
self._refresh_host()
|
|
return effective_changed
|
|
|
|
def set_inherited_style(self, style_key: Optional[str]) -> bool:
|
|
previous_effective = self.effective_style_key
|
|
previous_render = self._current_render_key()
|
|
self._inherited_style_key = style_key
|
|
effective_changed = previous_effective != self.effective_style_key
|
|
if effective_changed or previous_render != self._current_render_key():
|
|
self._refresh_host()
|
|
return effective_changed
|
|
|
|
def handle_theme_changed(self, theme: str) -> None:
|
|
theme = (theme or "").strip().lower()
|
|
if theme in {"dark", "light"} and theme != self._theme:
|
|
self._theme = theme
|
|
self._refresh_host()
|
|
|
|
def _refresh_host(self) -> None:
|
|
style_role = self.effective_style_key or ""
|
|
render_key = self._current_render_key()
|
|
resolved_key = self._resolve_theme_key(render_key)
|
|
css = APP_STYLES.get(resolved_key, "") if resolved_key else ""
|
|
stylesheet = self._scope_css_to_host(css, style_role) if css and style_role else ""
|
|
styled_background = bool(css)
|
|
if style_role != self._last_style_role:
|
|
self._host.setProperty("style_role", style_role)
|
|
self._last_style_role = style_role
|
|
if stylesheet != self._last_stylesheet:
|
|
self._host.setStyleSheet(stylesheet)
|
|
self._last_stylesheet = stylesheet
|
|
if styled_background != self._last_styled_background:
|
|
self._host.setAttribute(Qt.WidgetAttribute.WA_StyledBackground, styled_background)
|
|
self._last_styled_background = styled_background
|
|
|
|
def _resolve_theme_key(self, style_key: Optional[str]) -> Optional[str]:
|
|
if not style_key:
|
|
return None
|
|
themed_key = f"{style_key}_{self._theme.upper()}"
|
|
if themed_key in APP_STYLES:
|
|
return themed_key
|
|
return style_key if style_key in APP_STYLES else None
|
|
|
|
def _current_render_key(self) -> Optional[str]:
|
|
return self._explicit_active_style_key if self._is_active else self.effective_style_key
|
|
|
|
def _scope_css_to_host(self, css: str, style_role: str) -> str:
|
|
def replace(match: re.Match[str]) -> str:
|
|
selectors = []
|
|
for raw_selector in match.group(1).split(","):
|
|
selector = raw_selector.strip()
|
|
selectors.append(self._scope_selector(selector, style_role))
|
|
return ", ".join(selectors) + " {"
|
|
|
|
return _SELECTOR_BLOCK_RE.sub(replace, css)
|
|
|
|
@staticmethod
|
|
def _scope_selector(selector: str, style_role: str) -> str:
|
|
if not selector or any(token in selector for token in (" ", ">", "+", "~")):
|
|
return selector
|
|
if 'style_role="' in selector:
|
|
return selector
|
|
pseudo_index = selector.find(":")
|
|
base = selector if pseudo_index < 0 else selector[:pseudo_index]
|
|
suffix = "" if pseudo_index < 0 else selector[pseudo_index:]
|
|
return f'{base}[style_role="{style_role}"]{suffix}'
|