# -*- coding: utf-8 -*- # gui/components/button.py from PySide6.QtWidgets import QPushButton, QSizePolicy from PySide6.QtCore import Slot, QSize from PySide6.QtGui import QIcon from gui.theme_bus import theme_bus from gui.containers.s_container import SContainer # Импортируем кастомный контейнер from gui.styles import APP_STYLES class Button(SContainer): """Навигационная кнопка на основе кастомного контейнера SContainer.""" def __init__(self, text: str, index: int = 0, **kwargs): # Извлекаем параметры для передачи в SContainer width_percent = kwargs.get("width_percent", None) height_percent = kwargs.get("height_percent", None) margin = kwargs.get("margin", [0, 2, 0, 2]) style = kwargs.get("style", None) active_style = kwargs.get("active_style", None) is_active = kwargs.get("is_active", None) content_fit = kwargs.get("content_fit", True) parent = kwargs.get("parent", None) # Вызываем конструктор SContainer с параметрами 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 = QPushButton(text) self._button.setProperty("widget_index", index) # Добавляем кнопку в layout контейнера super().add_widget(self._button) # Настраиваем кнопку для заполнения всего доступного пространства self._button.setSizePolicy( QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding ) # Флаг для отслеживания первого обновления self._initial_update_done = False 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._theme = "dark" if self.palette().window().color().lightness() < 128 else "light" self.style() theme_bus.theme_changed.connect(self.set_theme) # Иконка (опционально): путь к PNG + размер иконки внутри кнопки. # Размер иконки — это свойство QPushButton (QSize), а не layout-геометрия; # правило 6.7 (запрет fixed-size) распространяется на разметку, не на иконки. icon_path = kwargs.get("icon_path", None) icon_size = kwargs.get("icon_size", 16) if icon_path: self._button.setIcon(QIcon(str(icon_path))) self._button.setIconSize(QSize(int(icon_size), int(icon_size))) def style( self, style_key: str | None = None, active_key: str | None = None, is_active: bool | 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: active_key = self._style_key_active or self._style_key_normal key = active_key if self._is_active else self._style_key_normal themed = f"{key}_{self._theme.upper()}" if themed in APP_STYLES: key = themed 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): """Внешний слот: принимает 'dark' или 'light'.""" theme = (theme or "").strip().lower() if theme not in ("dark", "light"): return # игнорируем ошибочные значения if self._theme == theme: return self._theme = theme self.style() # Делегируем clicked сигнал и другие методы внутренней кнопке @property def clicked(self): return self._button.clicked @property def toggled(self): return self._button.toggled def click(self): self._button.click() def set_text(self, text: str): self._button.setText(text) def get_text(self) -> str: return self._button.text() def set_tooltip(self, text: str): self._button.setToolTip(text) def get_tooltip(self) -> str: return self._button.toolTip() def set_checkable(self, checkable: bool): self._button.setCheckable(checkable) def set_checked(self, checked: bool): self._button.setChecked(checked) def is_checked(self) -> bool: return self._button.isChecked() def set_enabled(self, enabled: bool): 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_font(self, font): self._button.setFont(font) def set_property(self, name: str, value): super().setProperty(name, value) self._button.setProperty(name, value) def set_size_policy(self, horizontal, vertical) -> None: self._button.setSizePolicy(horizontal, vertical) super().setSizePolicy(horizontal, vertical) # Переопределяем add_widget, чтобы предотвратить добавление других виджетов def add_widget(self, widget, alignment=None): raise NotImplementedError("Button может содержать только одну кнопку") # --------------------------------------------------------------------------- # Module workflow notes # --------------------------------------------------------------------------- # # 1) Назначение модуля: # Навигационная/функциональная кнопка, реализованная как запечатанный # контейнер SContainer с одной внутренней QPushButton, поддерживающая # централизованные стили APP_STYLES и автоматическое переключение # тем (dark/light) через theme_bus. # # 2) Зависимости модуля: # Импорты: QPushButton, QSizePolicy (PySide6.QtWidgets), # Slot (PySide6.QtCore), # theme_bus (gui.theme_bus), # SContainer (gui.containers.s_container), # APP_STYLES (gui.styles) # Хост-класс / базовый класс: SContainer # Внешние библиотеки: PySide6 (обязательна) # # 3) Экспорт: # Класс Button — публичный виджет-кнопка. # Основные методы: style(), set_theme(), set_text(), get_text(), # set_tooltip(), set_checkable(), set_checked(), is_checked(), # set_enabled(), set_min_width/height(), set_max_width/height(), # set_fixed_size(), set_font(), set_property(), set_size_policy(), # click(). # Свойства: clicked, toggled (делегируют к внутренней QPushButton). # # 4) Состояние (поля): # index: int — числовой индекс кнопки (для идентификации в группе) # _theme: str — текущая тема ("dark" | "light") # _is_active: bool — признак активного состояния # _style_key_normal: str|None — ключ стиля нормального состояния # _style_key_active: str|None — ключ стиля активного состояния # _button: QPushButton — внутренний виджет кнопки # _initial_update_done: bool — флаг первого обновления # # 5) Последовательность действий и вызовов: # __init__(text, index, **kwargs) # -> super().__init__(...) — инициализация SContainer # -> QPushButton(text) — создание внутренней кнопки # -> super().add_widget(_button) — добавление в layout контейнера # -> style() — первичное применение стиля из APP_STYLES # -> theme_bus.theme_changed.connect(set_theme) # style(style_key?, active_key?, is_active?) # -> выбор ключа на основе _is_active + _theme # -> _button.setStyleSheet(APP_STYLES[key]) # set_theme(theme: str) # -> _theme = theme -> style() — перерисовка стиля # clicked / toggled (properties) # -> делегируют к _button.clicked / _button.toggled # # 6) Побочные эффекты: # - Устанавливает stylesheet на внутреннюю QPushButton. # - Подключается к глобальному сигналу theme_bus.theme_changed при создании. # # 7) Границы ответственности: # Модуль НЕ управляет layout хоста, НЕ хранит бизнес-логику, # НЕ регистрирует обработчики кликов (это делает потребитель). # add_widget() заблокирован — кнопка содержит только QPushButton. # # 8) Обработка ошибок: # add_widget() бросает NotImplementedError при попытке добавить # дополнительный виджет. set_theme() молча игнорирует невалидные # значения темы. # # 9) Инварианты и контракты: # - Контейнер всегда содержит ровно одну QPushButton. # - _theme ∈ {"dark", "light"}. # - Если _style_key_normal задан, стиль определяется им; иначе — # используется стандартная пара STANDARD_BUTTON_*_THEME(_ACTIVE). # # 10) Правило сопровождения: # При добавлении новой темы — расширить ветку в style(). # Не добавлять дочерние виджеты внутрь Button. # Новые делегирующие методы дублировать на _button и super().