Add Dispatch_V0.1.1
This commit is contained in:
280
Dispatch_V0.1.1/gui/components/button.py
Normal file
280
Dispatch_V0.1.1/gui/components/button.py
Normal file
@@ -0,0 +1,280 @@
|
||||
# -*- 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().
|
||||
Reference in New Issue
Block a user