146 lines
7.3 KiB
Python
146 lines
7.3 KiB
Python
# -*- coding: utf-8 -*-
|
||
# gui/containers/content_host.py
|
||
|
||
from PySide6.QtCore import Qt
|
||
from PySide6.QtWidgets import QVBoxLayout, QHBoxLayout, QWidget, QSizePolicy
|
||
from .percent_sized_widget import PercentSizedWidget
|
||
|
||
|
||
class ContentHost(PercentSizedWidget):
|
||
"""Внутренний хост контента с процентными размерами и базовым layout."""
|
||
|
||
def __init__(
|
||
self,
|
||
width_percent: int | None = None,
|
||
height_percent: int | None = None,
|
||
orientation: str = "v",
|
||
margin: int | tuple[int, int, int, int] = 0,
|
||
spacing: int = 0,
|
||
parent: QWidget | None = None,
|
||
):
|
||
super().__init__(width_percent, height_percent, parent)
|
||
|
||
if orientation == "h":
|
||
self._layout = QHBoxLayout(self)
|
||
else:
|
||
self._layout = QVBoxLayout(self)
|
||
|
||
self._layout.setSpacing(spacing)
|
||
|
||
if isinstance(margin, (list, tuple)) and len(margin) == 4:
|
||
self._layout.setContentsMargins(*margin)
|
||
else:
|
||
self._layout.setContentsMargins(margin, margin, margin, margin)
|
||
|
||
# Контент-хост по умолчанию растягивается внутри контейнера
|
||
self.set_content_fit(True)
|
||
|
||
def set_content_fit(self, expand: bool) -> None:
|
||
"""Управление растягиванием контента внутри контейнера."""
|
||
if expand:
|
||
self.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||
else:
|
||
self.setSizePolicy(QSizePolicy.Preferred, QSizePolicy.Preferred)
|
||
|
||
def _auto_add_to_parent(self) -> None:
|
||
"""Контент-хост добавляется вручную контейнером."""
|
||
return
|
||
|
||
def add_widget(self, widget: QWidget, alignment=None) -> None:
|
||
"""Добавляет виджет в layout контейнера."""
|
||
self._layout.addWidget(widget)
|
||
self._notify_children_layout_changed()
|
||
|
||
def insert_widget(self, index: int, widget: QWidget) -> None:
|
||
"""Вставляет виджет в layout контейнера по индексу."""
|
||
self._layout.insertWidget(index, widget)
|
||
self._notify_children_layout_changed()
|
||
|
||
def remove_widget(self, widget: QWidget) -> None:
|
||
"""Удаляет виджет из layout контейнера."""
|
||
self._layout.removeWidget(widget)
|
||
self._notify_children_layout_changed()
|
||
|
||
def add_widget_with_stretch(self, widget: QWidget, stretch: int, alignment=None) -> None:
|
||
"""Добавляет виджет с stretch в layout контейнера."""
|
||
self._layout.addWidget(widget, stretch)
|
||
self._notify_children_layout_changed()
|
||
|
||
def add_stretch(self, stretch: int = 1) -> None:
|
||
self._layout.addStretch(stretch)
|
||
|
||
def get_layout(self):
|
||
return self._layout
|
||
|
||
def _notify_children_layout_changed(self) -> None:
|
||
"""Канал «состав детей»: после изменения списка детей host'а просим
|
||
каждого percent-sized потомка перепланировать свой пересчёт.
|
||
Коалесцирование обеспечивается флагом _update_pending в каждом потомке —
|
||
несколько add_widget(...) подряд дают только один singleShot(0) на потомка.
|
||
"""
|
||
for index in range(self._layout.count()):
|
||
item = self._layout.itemAt(index)
|
||
if item is None:
|
||
continue
|
||
child_widget = item.widget()
|
||
if isinstance(child_widget, PercentSizedWidget):
|
||
child_widget.schedule_percent_update()
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Module workflow notes
|
||
# ---------------------------------------------------------------------------
|
||
#
|
||
# 1) Назначение модуля:
|
||
# Внутренний хост контента — промежуточный виджет, который размещается
|
||
# внутри контейнеров (SContainer, GridContainer, StackContainer и пр.)
|
||
# и содержит реальные дочерние виджеты. Поддерживает процентные размеры,
|
||
# вертикальную/горизонтальную ориентацию layout'а, отступы и spacing.
|
||
# Является деталью реализации контейнеров, а не частью публичного API.
|
||
#
|
||
# 2) Зависимости модуля:
|
||
# Импорты: Qt, QVBoxLayout, QHBoxLayout, QWidget, QSizePolicy (PySide6)
|
||
# Хост/базовый класс: PercentSizedWidget (percent_sized_widget.py)
|
||
# Внешние библиотеки: PySide6
|
||
#
|
||
# 3) Экспорт:
|
||
# Класс ContentHost — внутренний хост контента с layout.
|
||
# Методы: set_content_fit(), add_widget(), add_widget_with_stretch(),
|
||
# add_stretch(), get_layout()
|
||
#
|
||
# 4) Состояние (поля):
|
||
# _layout : QVBoxLayout | QHBoxLayout — layout для размещения потомков,
|
||
# выбирается по параметру orientation ("v"/"h").
|
||
#
|
||
# 5) Последовательность действий и вызовов:
|
||
# __init__(params) -> super().__init__(width_percent, height_percent)
|
||
# -> создание QVBoxLayout/QHBoxLayout
|
||
# -> setSpacing() -> setContentsMargins()
|
||
# -> set_content_fit(True) — SizePolicy = Expanding по умолчанию
|
||
# add_widget(widget) -> _layout.addWidget(widget)
|
||
# add_widget_with_stretch(widget, stretch) -> _layout.addWidget(widget, stretch)
|
||
#
|
||
# 6) Побочные эффекты:
|
||
# Устанавливает SizePolicy на self при вызове set_content_fit().
|
||
# _auto_add_to_parent() переопределён как no-op — ContentHost добавляется
|
||
# контейнером вручную, а не через механизм авто-добавления PercentSizedWidget.
|
||
#
|
||
# 7) Границы ответственности:
|
||
# НЕ управляет собственными процентами — это делает PercentSizedWidget.
|
||
# НЕ стилизуется (не наследует StylableMixin).
|
||
# НЕ дублирует логику контейнера; лишь предоставляет layout.
|
||
#
|
||
# 8) Обработка ошибок:
|
||
# Нет явной обработки; некорректные margin/spacing молча приведут
|
||
# к ошибке Qt. Если margin — не tuple(4), воспринимается как int.
|
||
#
|
||
# 9) Инварианты и контракты:
|
||
# - orientation ∈ {"v", "h"}, иначе умолчание — вертикальный.
|
||
# - margin: int или tuple(4). Иной тип вызовет ошибку setContentsMargins.
|
||
# - _auto_add_to_parent всегда возвращает None.
|
||
#
|
||
# 10) Правило сопровождения:
|
||
# Не расширять публичный интерфейс ContentHost — он должен оставаться
|
||
# тонкой обёрткой. Новые фичи (стилизация, alignment) добавлять
|
||
# в контейнеры, использующие ContentHost, а не сюда.
|