Files
Dispatch/Dispatch_V0.1.1/gui/components/radio_button.py
2026-04-29 08:18:54 +04:00

250 lines
9.8 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/radio_button.py
"""Обёртка над QRadioButton с централизованными стилями."""
from PySide6.QtWidgets import QRadioButton, QSizePolicy, QButtonGroup
from PySide6.QtCore import Slot
from gui.styles import APP_STYLES
from gui.theme_bus import theme_bus
from gui.containers.s_container import SContainer
class RadioButton(SContainer):
"""Кастомный QRadioButton в SContainer с поддержкой выравнивания."""
def __init__(
self,
text: str = "",
width_percent: int | None = None,
height_percent: int | None = None,
margin: int | tuple[int, int, int, int] = 0,
style: str = "RADIO_BUTTON",
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._theme = "dark"
self._is_active = False
self._base_style_key = style
self._style_key_normal = None
self._style_key_active = None
self._button = QRadioButton(text)
self._button.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Preferred)
super().add_widget(self._button)
if active_style is not None:
self._style_key_normal = style
self._style_key_active = active_style
if is_active is not None:
self._is_active = bool(is_active)
self.style()
theme_bus.theme_changed.connect(self.set_theme)
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._base_style_key = style_key
if active_key is not None:
self._style_key_normal = style_key
self._style_key_active = active_key
else:
self._style_key_normal = None
self._style_key_active = None
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
base_key = self._base_style_key
key = base_key
if self._theme == "light":
if self._is_active and f"{base_key}_LIGHT_ACTIVE" in APP_STYLES:
key = f"{base_key}_LIGHT_ACTIVE"
elif f"{base_key}_LIGHT" in APP_STYLES:
key = f"{base_key}_LIGHT"
else:
if self._is_active and f"{base_key}_DARK_ACTIVE" in APP_STYLES:
key = f"{base_key}_DARK_ACTIVE"
elif f"{base_key}_DARK" in APP_STYLES:
key = f"{base_key}_DARK"
self._button.setStyleSheet(APP_STYLES.get(key, ""))
@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()
def set_text(self, text: str) -> None:
self._button.setText(text)
def get_text(self) -> str:
return self._button.text()
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_tooltip(self, text: str) -> None:
self._button.setToolTip(text)
def set_size_policy(self, horizontal, vertical) -> None:
self._button.setSizePolicy(horizontal, vertical)
super().setSizePolicy(horizontal, vertical)
def set_alignment(self, alignment) -> None:
"""Установить выравнивание внутренней кнопки в контейнере."""
self.set_widget_alignment(self._button, alignment)
def set_margins(self, margin: int | tuple[int, int, int, int]) -> None:
"""Установить отступы контейнера."""
if isinstance(margin, (list, tuple)) and len(margin) == 4:
self.layout().setContentsMargins(*margin)
else:
self.layout().setContentsMargins(margin, margin, margin, margin)
def add_to_group(self, group: QButtonGroup) -> None:
"""Добавить кнопку в QButtonGroup, не раскрывая внутренний виджет."""
group.addButton(self._button)
@property
def toggled(self):
return self._button.toggled
@property
def clicked(self):
return self._button.clicked
def add_widget(self, widget, alignment=None):
raise NotImplementedError("RadioButton может содержать только одну кнопку")
# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
#
# 1) Назначение модуля:
# Обёртка над QRadioButton в SContainer с поддержкой APP_STYLES,
# темизации и интеграции с QButtonGroup через метод add_to_group().
#
# 2) Зависимости модуля:
# Импорты: QRadioButton, QSizePolicy, QButtonGroup (PySide6.QtWidgets),
# Slot (PySide6.QtCore),
# APP_STYLES (gui.styles),
# theme_bus (gui.theme_bus),
# SContainer (gui.containers.s_container)
# Хост-класс / базовый класс: SContainer
# Внешние библиотеки: PySide6 (обязательна)
#
# 3) Экспорт:
# Класс RadioButton — публичный виджет радиокнопки.
# Методы: style(), set_theme(), set_text(), get_text(),
# set_checked(), is_checked(), set_enabled(),
# set_alignment(), set_margins(), add_to_group(QButtonGroup),
# set_min/max_width/height(), set_fixed_size(),
# set_tooltip(), set_size_policy().
# Свойства: toggled, clicked.
#
# 4) Состояние (поля):
# _theme: str — текущая тема
# _is_active: bool — признак активного состояния
# _base_style_key: str — базовый ключ стиля ("RADIO_BUTTON")
# _style_key_normal: str|None — явный нормальный стиль
# _style_key_active: str|None — явный активный стиль
# _button: QRadioButton — внутренний виджет
#
# 5) Последовательность действий и вызовов:
# __init__(text, ...)
# -> super().__init__(...)
# -> QRadioButton(text) -> setSizePolicy(Preferred)
# -> super().add_widget(_button)
# -> style() -> theme_bus.theme_changed.connect(set_theme)
# add_to_group(group: QButtonGroup)
# -> group.addButton(self._button) — инкапсуляция внутреннего виджета
# toggled / clicked (properties)
# -> делегируют к _button.toggled / _button.clicked
#
# 6) Побочные эффекты:
# - Устанавливает stylesheet на QRadioButton.
# - Подключается к theme_bus.theme_changed.
# - add_to_group() модифицирует внешний QButtonGroup.
#
# 7) Границы ответственности:
# Модуль НЕ управляет взаимоисключающим выбором самостоятельно (это
# задача RadioGroup/QButtonGroup). add_widget() заблокирован.
#
# 8) Обработка ошибок:
# add_widget() бросает NotImplementedError.
# set_theme() молча игнорирует невалидные значения.
#
# 9) Инварианты и контракты:
# - Контейнер содержит ровно один QRadioButton.
# - _theme ∈ {"dark", "light"}.
#
# 10) Правило сопровождения:
# Для группировки — использовать RadioGroup.add_button(this).
# Не вызывать group.addButton(_button) напрямую — использовать
# add_to_group() для сохранения инкапсуляции.