# -*- 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}'