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

198 lines
9.6 KiB
Python
Raw 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/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 параметр.