241 lines
10 KiB
Python
241 lines
10 KiB
Python
# -*- coding: utf-8 -*-
|
||
# gui/components/group_box.py
|
||
"""Обёртка над QGroupBox с контейнером SContainer."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from PySide6.QtCore import Slot, Qt
|
||
from PySide6.QtWidgets import QGroupBox, QVBoxLayout, QWidget, QSizePolicy
|
||
|
||
from gui.containers.s_container import SContainer
|
||
from gui.styles import APP_STYLES
|
||
from gui.theme_bus import theme_bus
|
||
|
||
|
||
class GroupBox(SContainer):
|
||
"""QGroupBox с централизованным стилем и внутренним layout по умолчанию."""
|
||
|
||
_ALIGN_MAP = {
|
||
"top": Qt.AlignmentFlag.AlignTop,
|
||
"bottom": Qt.AlignmentFlag.AlignBottom,
|
||
"left": Qt.AlignmentFlag.AlignLeft,
|
||
"right": Qt.AlignmentFlag.AlignRight,
|
||
"hcenter": Qt.AlignmentFlag.AlignHCenter,
|
||
"vcenter": Qt.AlignmentFlag.AlignVCenter,
|
||
"center": Qt.AlignmentFlag.AlignCenter,
|
||
}
|
||
|
||
def __init__(
|
||
self,
|
||
title: str = "",
|
||
width_percent: int | None = None,
|
||
height_percent: int | None = None,
|
||
margin: int | tuple[int, int, int, int] = 0,
|
||
content_margins: int | tuple[int, int, int, int] = 10,
|
||
spacing: int = 8,
|
||
alignment=None,
|
||
style: str = "GROUP_BOX",
|
||
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._group_box = QGroupBox(title)
|
||
self._group_box.setSizePolicy(QSizePolicy.Policy.Preferred, QSizePolicy.Policy.Minimum)
|
||
self._group_layout = QVBoxLayout()
|
||
if isinstance(content_margins, (list, tuple)) and len(content_margins) == 4:
|
||
self._group_layout.setContentsMargins(*content_margins)
|
||
else:
|
||
self._group_layout.setContentsMargins(content_margins, content_margins, content_margins, content_margins)
|
||
self._group_layout.setSpacing(spacing)
|
||
if alignment is not None:
|
||
self._group_layout.setAlignment(self._normalize_alignment(alignment))
|
||
self._group_box.setLayout(self._group_layout)
|
||
|
||
super().add_widget(self._group_box)
|
||
|
||
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._group_box.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._group_box.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_title(self, title: str) -> None:
|
||
self._group_box.setTitle(title)
|
||
|
||
def get_title(self) -> str:
|
||
return self._group_box.title()
|
||
|
||
def add_widget(self, widget: QWidget) -> None:
|
||
self._group_layout.addWidget(widget)
|
||
|
||
def add_stretch(self, stretch: int = 1) -> None:
|
||
self._group_layout.addStretch(stretch)
|
||
|
||
def set_margins(self, margin: int | tuple[int, int, int, int]) -> None:
|
||
if isinstance(margin, (list, tuple)) and len(margin) == 4:
|
||
self._group_layout.setContentsMargins(*margin)
|
||
else:
|
||
self._group_layout.setContentsMargins(margin, margin, margin, margin)
|
||
|
||
def set_spacing(self, spacing: int) -> None:
|
||
self._group_layout.setSpacing(spacing)
|
||
|
||
def _normalize_alignment(self, alignment: str | Qt.Alignment) -> Qt.Alignment:
|
||
"""Преобразует строковое значение alignment в Qt.Alignment."""
|
||
if isinstance(alignment, str):
|
||
key = alignment.strip().lower()
|
||
mapped = self._ALIGN_MAP.get(key)
|
||
if mapped is None:
|
||
raise ValueError(f"Unknown alignment '{key}'. Allowed: {list(self._ALIGN_MAP.keys())}")
|
||
return mapped
|
||
return alignment
|
||
|
||
def set_alignment(self, alignment: str | Qt.Alignment) -> None:
|
||
"""Устанавливает выравнивание layout (строка или Qt.Alignment)."""
|
||
self._group_layout.setAlignment(self._normalize_alignment(alignment))
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Module workflow notes
|
||
# ---------------------------------------------------------------------------
|
||
#
|
||
# 1) Назначение модуля:
|
||
# Обёртка над QGroupBox, встроенная в SContainer, с заголовком,
|
||
# внутренним QVBoxLayout для дочерних виджетов и поддержкой стилей
|
||
# APP_STYLES + theme_bus.
|
||
#
|
||
# 2) Зависимости модуля:
|
||
# Импорты: Slot, Qt (PySide6.QtCore),
|
||
# QGroupBox, QVBoxLayout, QWidget, QSizePolicy (PySide6.QtWidgets),
|
||
# SContainer (gui.containers.s_container),
|
||
# APP_STYLES (gui.styles),
|
||
# theme_bus (gui.theme_bus)
|
||
# Хост-класс / базовый класс: SContainer
|
||
# Внешние библиотеки: PySide6 (обязательна)
|
||
#
|
||
# 3) Экспорт:
|
||
# Класс GroupBox — публичный контейнерный виджет с заголовком.
|
||
# Методы: style(), set_theme(), set_title(), get_title(),
|
||
# add_widget(QWidget), add_stretch(), set_margins(),
|
||
# set_spacing(), set_alignment().
|
||
#
|
||
# 4) Состояние (поля):
|
||
# _theme: str — текущая тема
|
||
# _is_active: bool — признак активного состояния
|
||
# _base_style_key: str — базовый ключ стиля ("GROUP_BOX")
|
||
# _style_key_normal: str|None — явный нормальный стиль
|
||
# _style_key_active: str|None — явный активный стиль
|
||
# _group_box: QGroupBox — внутренний QGroupBox
|
||
# _group_layout: QVBoxLayout — layout внутри QGroupBox
|
||
# _ALIGN_MAP: dict — маппинг строковых выравниваний в Qt.Alignment
|
||
#
|
||
# 5) Последовательность действий и вызовов:
|
||
# __init__(title, content_margins, spacing, alignment, ...)
|
||
# -> super().__init__(...)
|
||
# -> QGroupBox(title) -> QVBoxLayout -> setContentsMargins, setSpacing
|
||
# -> setAlignment(если передано)
|
||
# -> _group_box.setLayout(_group_layout)
|
||
# -> super().add_widget(_group_box) — добавление QGroupBox в SContainer
|
||
# -> style() -> theme_bus.theme_changed.connect(set_theme)
|
||
# add_widget(widget) — ПЕРЕОПРЕДЕЛЁН:
|
||
# -> _group_layout.addWidget(widget) (добавляет внутрь QGroupBox, не в SContainer)
|
||
#
|
||
# 6) Побочные эффекты:
|
||
# - Устанавливает stylesheet на QGroupBox.
|
||
# - Подключается к theme_bus.theme_changed.
|
||
# - add_widget() изменяет _group_layout (не SContainer layout).
|
||
#
|
||
# 7) Границы ответственности:
|
||
# Модуль — визуальная группировка. НЕ реализует вложенные стили
|
||
# для дочерних элементов. НЕ ограничивает типы дочерних виджетов.
|
||
#
|
||
# 8) Обработка ошибок:
|
||
# _normalize_alignment() бросает ValueError при невалидной строке
|
||
# выравнивания.
|
||
#
|
||
# 9) Инварианты и контракты:
|
||
# - _group_box всегда имеет QVBoxLayout.
|
||
# - Допустимые строки выравнивания: top, bottom, left, right,
|
||
# hcenter, vcenter, center.
|
||
# - add_widget() GroupBox — НЕ запечатан, принимает любые QWidget.
|
||
#
|
||
# 10) Правило сопровождения:
|
||
# Если нужна горизонтальная компоновка внутри GroupBox —
|
||
# вкладывать HContainer в add_widget(), не менять _group_layout на QHBoxLayout.
|