# -*- coding: utf-8 -*- # gui/containers/s_container.py """Универсальный контейнер с процентным масштабированием по обеим осям. Orientation ("v" | "h") определяет направление стэкирования потомков. По умолчанию "v" — вертикальный стэк (самый частый паттерн). При width_percent=None ось X — Expanding. При height_percent=None ось Y — Expanding. Таким образом: SContainer() → растягивается по обеим осям SContainer(width_percent=30) → ≡ VContainer (фикс. ширина, свободная высота) SContainer(height_percent=20) → ≡ HContainer (фикс. высота, свободная ширина) SContainer(width_percent=30, height_percent=50) → обе оси фиксированы """ from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QLayout, QWidget from .percent_sized_widget import PercentSizedWidget from .content_host import ContentHost class SContainer(PercentSizedWidget): """Универсальный контейнер с процентным масштабированием.""" def __init__( self, width_percent: int | float | None = None, height_percent: int | float | None = None, margin: int | tuple[int, int, int, int] = 0, spacing: int = 0, orientation: str = "v", content_width_percent: int | None = None, content_height_percent: int | None = None, content_width: int | None = None, content_height: int | None = None, content_fit: bool = True, content_driven: bool = False, parent: QWidget | None = None, style: str | None = None, active_style: str | None = None, is_active: bool | None = None, ): super().__init__(width_percent, height_percent, parent, content_driven=content_driven) self._init_stylable() self._auto_add_children = True # Внешний layout — единственный, без промежуточного QGridLayout. if orientation == "h": self._layout: QLayout = QHBoxLayout(self) else: self._layout: QLayout = QVBoxLayout(self) self._layout.setSpacing(0) if isinstance(margin, (list, tuple)) and len(margin) == 4: self._layout.setContentsMargins(*margin) else: self._layout.setContentsMargins(margin, margin, margin, margin) self._content_host = ContentHost( width_percent=content_width_percent, height_percent=content_height_percent, orientation=orientation, margin=0, spacing=spacing, parent=self, ) self._content_host.set_content_fit(content_fit) if content_width is not None or content_height is not None: w = content_width if content_width is not None else self._content_host.sizeHint().width() h = content_height if content_height is not None else self._content_host.sizeHint().height() self._content_host.set_fixed_size(w, h) self._layout.addWidget(self._content_host) if style is not None or active_style is not None or is_active is not None: self._apply_style(style_key=style, active_key=active_style, is_active=is_active) # ── публичный API ── def add_widget(self, widget: QWidget, alignment=None) -> None: """Добавляет виджет в layout контейнера.""" self._content_host.add_widget(widget) def insert_widget(self, index: int, widget: QWidget) -> None: """Вставляет виджет в layout контента по индексу.""" self._content_host.insert_widget(index, widget) def remove_widget(self, widget: QWidget) -> None: """Удаляет виджет из layout контента.""" self._content_host.remove_widget(widget) def add_widget_with_stretch(self, widget: QWidget, stretch: int, alignment=None) -> None: self._content_host.add_widget_with_stretch(widget, stretch) def add_stretch(self, stretch: int = 1) -> None: self._content_host.add_stretch(stretch) def invalidate_layout(self) -> None: self._content_host.get_layout().invalidate() def get_layout(self) -> QLayout: return self._layout 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 set_spacing(self, spacing: int) -> None: self._content_host.get_layout().setSpacing(spacing) def set_alignment(self, alignment: str) -> None: raise NotImplementedError("Qt alignment for containers is disabled; use content springs.") def set_widget_alignment(self, widget: QWidget, alignment: str) -> None: raise NotImplementedError("Qt alignment for containers is disabled; use content springs.") def get_available_size_for_content(self) -> tuple[int, int]: """Полезная внутренняя область (без учёта margin).""" margins = self._layout.contentsMargins() w = self.width() - margins.left() - margins.right() h = self.height() - margins.top() - margins.bottom() return max(0, w), max(0, h) # --------------------------------------------------------------------------- # Module workflow notes # --------------------------------------------------------------------------- # # 1) Назначение модуля: # Универсальный контейнер с процентным масштабированием по обеим осям. # Ориентация ("v"|"h") определяет направление стэкирования потомков. # Является базовым классом для VContainer и HContainer. # Эквивалентности: SContainer(w%=30) ≡ VContainer(w%=30), # SContainer(h%=20) ≡ HContainer(h%=20). # # 2) Зависимости модуля: # Импорты: QVBoxLayout, QHBoxLayout, QLayout, QWidget (PySide6) # Хост/базовый класс: StylableMixin + PercentSizedWidget (MRO) # Внутренние: ContentHost (content_host.py), StylableMixin (stylable_mixin.py) # Внешние библиотеки: PySide6 # # 3) Экспорт: # Класс SContainer — универсальный контейнер. # Методы: add_widget(), add_widget_with_stretch(), add_stretch(), # invalidate_layout(), get_layout(), set_margins(), # set_spacing(), set_alignment(), set_widget_alignment(), # get_available_size_for_content() # # 4) Состояние (поля): # _layout : QVBoxLayout|QHBoxLayout — внешний layout. # _content_host : ContentHost — промежуточный хост для потомков. # _auto_add_children : bool = True — дочерние виджеты авто-добавляются. # # 5) Последовательность действий и вызовов: # __init__(params) -> super().__init__(w%, h%, parent) # -> _init_stylable() -> создание QVBoxLayout/QHBoxLayout по orientation # -> setSpacing(0) на _layout -> setContentsMargins(margin) # -> создание ContentHost(orientation, spacing) # -> _content_host.set_content_fit(content_fit) # -> set_fixed_size(content_width, content_height) если заданы # -> _layout.addWidget(_content_host) # -> _apply_style() если style задан # add_widget(w) -> _content_host.add_widget(w) # set_spacing(s) -> _content_host.get_layout().setSpacing(s) # # 6) Побочные эффекты: # _auto_add_children = True — дочерние PercentSizedWidget авто-добавляются. # set_alignment() и set_widget_alignment() бросают NotImplementedError. # _apply_style() устанавливает stylesheet. # Подписка на theme_bus через StylableMixin. # # 7) Границы ответственности: # НЕ поддерживает spring-based alignment (в отличие от GridContainer). # НЕ содержит сетку — только линейный layout. # НЕ управляет scroll — это ScrollContainer. # # 8) Обработка ошибок: # set_alignment() → NotImplementedError. # set_widget_alignment() → NotImplementedError. # Для обоих: «Qt alignment for containers is disabled; use content springs.» # # 9) Инварианты и контракты: # - orientation ∈ {"v", "h"}, иначе умолчание — вертикальный. # - При w%=None → ось X = Expanding; при h%=None → ось Y = Expanding. # - spacing внешнего _layout всегда 0 (spacing применяется к ContentHost). # - alignment параметр в __init__ — deprecated, игнорируется. # # 10) Правило сопровождения: # Новая логика должна быть совместима с VContainer/HContainer — # они наследуют SContainer. Не вводить логику, специфичную только # для одной ориентации. Spacing: внешний layout = 0, внутренний # (ContentHost) = spacing параметр.