Add Dispatch_V0.1.1
This commit is contained in:
132
Dispatch_V0.1.1/gui/containers/_widget_style_service.py
Normal file
132
Dispatch_V0.1.1/gui/containers/_widget_style_service.py
Normal file
@@ -0,0 +1,132 @@
|
||||
# -*- 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}'
|
||||
Reference in New Issue
Block a user