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

241 lines
10 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/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.