# -*- coding: utf-8 -*- # gui/components/toggle_button.py """Компонент кнопки-переключателя на основе SContainer.""" from PySide6.QtWidgets import QToolButton, QSizePolicy from PySide6.QtCore import Slot from gui.theme_bus import theme_bus from gui.containers.s_container import SContainer from gui.styles import APP_STYLES class ToggleButton(SContainer): """Кнопка-переключатель на основе SContainer со стилями, зависящими от темы.""" def __init__( self, text: str, index: int = 0, width_percent: int | None = None, height_percent: int | None = None, margin: int | tuple[int, int, int, int] = (0, 2, 0, 2), style: str | None = None, active_style: str | None = None, is_active: bool | None = None, content_fit: bool = True, parent=None, ): super().__init__( width_percent=width_percent, height_percent=height_percent, margin=margin, style=style, active_style=active_style, is_active=is_active, content_fit=content_fit, parent=parent, ) self.index = index self._theme = "dark" self._is_active = False self._style_key_normal = None self._style_key_active = None self._button = QToolButton() self._button.setText(text) self._button.setProperty("widget_index", index) self._button.setCheckable(True) self._button.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding, ) super().add_widget(self._button) self._button.toggled.connect(self._on_toggled) if style is not None: self._style_key_normal = style self._style_key_active = active_style or style if is_active is not None: self._is_active = bool(is_active) self.style() theme_bus.theme_changed.connect(self.set_theme) def _on_toggled(self, checked: bool) -> None: self.style(is_active=checked) def style( self, style_key: str | None = None, active_key: str | None = None, is_active: bool | None = None, ) -> None: """Короткий метод применения стиля. Можно задать ключи и активность явно.""" if style_key is not None: self._style_key_normal = style_key self._style_key_active = active_key or style_key if is_active is not None: self._is_active = bool(is_active) if self._style_key_normal is not None: normal_key = self._style_key_normal active_key = self._style_key_active or self._style_key_normal if self._theme == "light": themed_normal = f"{normal_key}_LIGHT" themed_active = f"{active_key}_LIGHT" if themed_normal in APP_STYLES: normal_key = themed_normal if themed_active in APP_STYLES: active_key = themed_active key = active_key if self._is_active else normal_key self._button.setStyleSheet(APP_STYLES.get(key, "")) return if self._theme == "light": if self._is_active and "STANDARD_BUTTON_LIGHT_THEME_ACTIVE" in APP_STYLES: self._button.setStyleSheet(APP_STYLES["STANDARD_BUTTON_LIGHT_THEME_ACTIVE"]) else: self._button.setStyleSheet(APP_STYLES["STANDARD_BUTTON_LIGHT_THEME"]) return if self._is_active: self._button.setStyleSheet(APP_STYLES["STANDARD_BUTTON_DARK_THEME_ACTIVE"]) else: self._button.setStyleSheet(APP_STYLES["STANDARD_BUTTON_DARK_THEME"]) @Slot(str) def set_theme(self, theme: str) -> None: """Внешний слот: принимает 'dark' или 'light'.""" theme = (theme or "").strip().lower() if theme not in ("dark", "light"): return if self._theme == theme: return self._theme = theme self.style() @property def clicked(self): return self._button.clicked @property def toggled(self): return self._button.toggled def click(self) -> None: self._button.click() def set_text(self, text: str) -> None: self._button.setText(text) def get_text(self) -> str: return self._button.text() def set_tooltip(self, text: str) -> None: self._button.setToolTip(text) def get_tooltip(self) -> str: return self._button.toolTip() def set_checkable(self, checkable: bool) -> None: self._button.setCheckable(checkable) def set_checked(self, checked: bool) -> None: self._button.setChecked(checked) def is_checked(self) -> bool: return self._button.isChecked() def set_enabled(self, enabled: bool) -> None: self._button.setEnabled(enabled) super().setEnabled(enabled) def set_min_width(self, width: int) -> None: self._button.setMinimumWidth(width) super().setMinimumWidth(width) def set_min_height(self, height: int) -> None: self._button.setMinimumHeight(height) super().setMinimumHeight(height) def set_max_width(self, width: int) -> None: self._button.setMaximumWidth(width) super().setMaximumWidth(width) def set_max_height(self, height: int) -> None: self._button.setMaximumHeight(height) super().setMaximumHeight(height) def set_fixed_size(self, width: int, height: int) -> None: self._button.setMinimumSize(width, height) self._button.setMaximumSize(width, height) super().setMinimumSize(width, height) super().setMaximumSize(width, height) def set_size_policy(self, horizontal, vertical) -> None: self._button.setSizePolicy(horizontal, vertical) super().setSizePolicy(horizontal, vertical) def add_widget(self, widget, alignment=None): raise NotImplementedError("ToggleButton может содержать только одну кнопку") # --------------------------------------------------------------------------- # Module workflow notes # --------------------------------------------------------------------------- # # 1) Назначение модуля: # Кнопка-переключатель (toggle) на основе QToolButton в SContainer, # автоматически переключающая стиль при toggled и поддерживающая # темизацию через theme_bus. # # 2) Зависимости модуля: # Импорты: QToolButton, QSizePolicy (PySide6.QtWidgets), # Slot (PySide6.QtCore), # theme_bus (gui.theme_bus), # SContainer (gui.containers.s_container), # APP_STYLES (gui.styles) # Хост-класс / базовый класс: SContainer # Внешние библиотеки: PySide6 (обязательна) # # 3) Экспорт: # Класс ToggleButton — публичный виджет-переключатель. # Методы: style(), set_theme(), click(), set_text(), get_text(), # set_tooltip(), get_tooltip(), set_checkable(), set_checked(), # is_checked(), set_enabled(), set_min/max_width/height(), # set_fixed_size(), set_size_policy(). # Свойства: clicked, toggled. # # 4) Состояние (поля): # index: int — числовой индекс # _theme: str — текущая тема # _is_active: bool — признак активного состояния # _style_key_normal: str|None — ключ нормального стиля # _style_key_active: str|None — ключ активного стиля # _button: QToolButton — внутренний виджет (checkable) # # 5) Последовательность действий и вызовов: # __init__(text, index, ...) # -> super().__init__(...) # -> QToolButton() -> setText, setCheckable(True), setSizePolicy # -> super().add_widget(_button) # -> _button.toggled.connect(_on_toggled) — автосмена стиля # -> style() -> theme_bus.theme_changed.connect(set_theme) # _on_toggled(checked: bool) # -> style(is_active=checked) — переключение стиля при нажатии # style(style_key?, active_key?, is_active?) # -> если _style_key_normal задан: # -> проверить themed-варианты (_LIGHT) в APP_STYLES # -> выбрать active или normal ключ # -> иначе: STANDARD_BUTTON_*_THEME(_ACTIVE) # -> _button.setStyleSheet(APP_STYLES[key]) # # 6) Побочные эффекты: # - Устанавливает stylesheet на QToolButton. # - Подключается к theme_bus.theme_changed. # - При toggle — автоматически меняет стиль. # # 7) Границы ответственности: # Модуль НЕ хранит бизнес-логику переключения. # НЕ группирует кнопки (для этого — RadioGroup/QButtonGroup). # add_widget() заблокирован. # # 8) Обработка ошибок: # add_widget() бросает NotImplementedError. # set_theme() молча игнорирует невалидные значения. # # 9) Инварианты и контракты: # - _button всегда checkable. # - _is_active синхронизирован с checked-состоянием через _on_toggled. # - _theme ∈ {"dark", "light"}. # # 10) Правило сопровождения: # Отличие от Button: использует QToolButton (checkable по умолчанию), # автоматически переключает стиль при toggled. Не путать с TabButton # (специализирован для TabWidget).