Files
2026-04-29 08:18:54 +04:00

281 lines
12 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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().