192 lines
9.0 KiB
Python
192 lines
9.0 KiB
Python
# -*- coding: utf-8 -*-
|
||
# gui/containers/scroll_container.py
|
||
"""Прокручиваемый контейнер с процентным sizing и контейнерным API проекта."""
|
||
|
||
from __future__ import annotations
|
||
|
||
from PySide6.QtCore import Qt
|
||
from PySide6.QtWidgets import QFrame, QLayout, QScrollArea, QVBoxLayout, QWidget
|
||
|
||
from .content_host import ContentHost
|
||
from .percent_sized_widget import PercentSizedWidget
|
||
|
||
|
||
class ScrollContainer(PercentSizedWidget):
|
||
"""Контейнер-обёртка над QScrollArea с поддержкой percent sizing."""
|
||
|
||
def __init__(
|
||
self,
|
||
width_percent: int | float | None = None,
|
||
height_percent: int | float | None = None,
|
||
margin: int | tuple[int, int, int, int] = 0,
|
||
content_margins: int | tuple[int, int, int, int] = 0,
|
||
spacing: int = 0,
|
||
orientation: str = "v",
|
||
widget_resizable: bool = True,
|
||
vertical_scroll_bar_policy: str | Qt.ScrollBarPolicy = "as_needed",
|
||
horizontal_scroll_bar_policy: str | Qt.ScrollBarPolicy = "always_off",
|
||
content_fit: 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)
|
||
self._init_stylable()
|
||
self._auto_add_children = True
|
||
|
||
self._scroll_area = QScrollArea(self)
|
||
self._scroll_area.setWidgetResizable(bool(widget_resizable))
|
||
self._scroll_area.setFrameShape(QFrame.Shape.NoFrame)
|
||
|
||
self._content_host = ContentHost(
|
||
orientation=orientation,
|
||
margin=content_margins,
|
||
spacing=spacing,
|
||
parent=self,
|
||
)
|
||
self._content_host.set_content_fit(bool(content_fit))
|
||
self._content_host.get_layout().setSizeConstraint(QLayout.SizeConstraint.SetMinAndMaxSize)
|
||
self._scroll_area.setWidget(self._content_host)
|
||
|
||
self._layout: QVBoxLayout = QVBoxLayout(self)
|
||
self._layout.setSpacing(0)
|
||
self.set_margins(margin)
|
||
self._layout.addWidget(self._scroll_area)
|
||
|
||
self.set_vertical_scroll_bar_policy(vertical_scroll_bar_policy)
|
||
self.set_horizontal_scroll_bar_policy(horizontal_scroll_bar_policy)
|
||
|
||
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)
|
||
|
||
@staticmethod
|
||
def _normalize_scroll_policy(policy: str | Qt.ScrollBarPolicy) -> Qt.ScrollBarPolicy:
|
||
if isinstance(policy, Qt.ScrollBarPolicy):
|
||
return policy
|
||
token = str(policy or "").strip().lower()
|
||
mapping = {
|
||
"as_needed": Qt.ScrollBarPolicy.ScrollBarAsNeeded,
|
||
"always_off": Qt.ScrollBarPolicy.ScrollBarAlwaysOff,
|
||
"always_on": Qt.ScrollBarPolicy.ScrollBarAlwaysOn,
|
||
}
|
||
if token not in mapping:
|
||
raise ValueError(
|
||
"Unknown scroll policy. Allowed: 'as_needed', 'always_off', 'always_on'."
|
||
)
|
||
return mapping[token]
|
||
|
||
def add_widget(self, widget: QWidget, alignment=None) -> None:
|
||
self._content_host.add_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_content_margins(self, margin: int | tuple[int, int, int, int]) -> None:
|
||
layout = self._content_host.get_layout()
|
||
if isinstance(margin, (list, tuple)) and len(margin) == 4:
|
||
layout.setContentsMargins(*margin)
|
||
else:
|
||
layout.setContentsMargins(margin, margin, margin, margin)
|
||
|
||
def set_spacing(self, spacing: int) -> None:
|
||
self._content_host.get_layout().setSpacing(spacing)
|
||
|
||
def set_widget_resizable(self, enabled: bool) -> None:
|
||
self._scroll_area.setWidgetResizable(bool(enabled))
|
||
|
||
def set_vertical_scroll_bar_policy(self, policy: str | Qt.ScrollBarPolicy) -> None:
|
||
self._scroll_area.setVerticalScrollBarPolicy(self._normalize_scroll_policy(policy))
|
||
|
||
def set_horizontal_scroll_bar_policy(self, policy: str | Qt.ScrollBarPolicy) -> None:
|
||
self._scroll_area.setHorizontalScrollBarPolicy(self._normalize_scroll_policy(policy))
|
||
|
||
@property
|
||
def scroll_area(self) -> QScrollArea:
|
||
return self._scroll_area
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Module workflow notes
|
||
# ---------------------------------------------------------------------------
|
||
#
|
||
# 1) Назначение модуля:
|
||
# Прокручиваемый контейнер — обёртка над QScrollArea с поддержкой
|
||
# процентного sizing и контейнерного API проекта (add_widget, стилизация,
|
||
# тема). Используется для длинных списков, форм и панелей, которые
|
||
# не помещаются в видимую область.
|
||
#
|
||
# 2) Зависимости модуля:
|
||
# Импорты: Qt, QFrame, QLayout, QScrollArea, QVBoxLayout, QWidget (PySide6)
|
||
# Хост/базовый класс: StylableMixin + PercentSizedWidget (MRO)
|
||
# Внутренние: ContentHost (content_host.py), StylableMixin (stylable_mixin.py)
|
||
# Внешние библиотеки: PySide6
|
||
#
|
||
# 3) Экспорт:
|
||
# Класс ScrollContainer — прокручиваемый контейнер.
|
||
# Методы: add_widget(), add_widget_with_stretch(), add_stretch(),
|
||
# invalidate_layout(), get_layout(), set_margins(),
|
||
# set_content_margins(), set_spacing(), set_widget_resizable(),
|
||
# set_vertical_scroll_bar_policy(), set_horizontal_scroll_bar_policy()
|
||
# Свойство: scroll_area (доступ к QScrollArea).
|
||
#
|
||
# 4) Состояние (поля):
|
||
# _scroll_area : QScrollArea — Qt scroll area (NoFrame).
|
||
# _content_host : ContentHost — внутренний хост с layout для потомков.
|
||
# _layout : QVBoxLayout — внешний layout самого контейнера.
|
||
# _auto_add_children: bool = True — потомки авто-добавляются.
|
||
#
|
||
# 5) Последовательность действий и вызовов:
|
||
# __init__(params) -> super().__init__(w%, h%, parent)
|
||
# -> _init_stylable() -> создание QScrollArea (NoFrame)
|
||
# -> создание ContentHost(orientation, content_margins, spacing)
|
||
# -> _content_host.set_content_fit(content_fit)
|
||
# -> _content_host.get_layout().setSizeConstraint(SetMinAndMaxSize)
|
||
# -> _scroll_area.setWidget(_content_host)
|
||
# -> создание _layout (QVBoxLayout) -> _layout.addWidget(_scroll_area)
|
||
# -> set_vertical/horizontal_scroll_bar_policy()
|
||
# -> _apply_style() если style задан
|
||
# add_widget(w) -> _content_host.add_widget(w)
|
||
#
|
||
# 6) Побочные эффекты:
|
||
# ContentHost помещается внутрь QScrollArea как scrollable widget.
|
||
# SizeConstraint = SetMinAndMaxSize — контролирует поведение scroll.
|
||
# _auto_add_children = True — дочерние PercentSizedWidget авто-добавляются.
|
||
# _apply_style() устанавливает stylesheet на self.
|
||
#
|
||
# 7) Границы ответственности:
|
||
# НЕ управляет содержимым скролла — это делает ContentHost.
|
||
# НЕ реализует собственный scroll — делегирует QScrollArea.
|
||
# НЕ применяет alignment/springs.
|
||
#
|
||
# 8) Обработка ошибок:
|
||
# _normalize_scroll_policy: ValueError при невалидной строке политики.
|
||
# Допустимые значения: "as_needed", "always_off", "always_on".
|
||
#
|
||
# 9) Инварианты и контракты:
|
||
# - scroll_bar_policy ∈ {"as_needed", "always_off", "always_on"} или
|
||
# Qt.ScrollBarPolicy enum.
|
||
# - По умолчанию vertical = as_needed, horizontal = always_off.
|
||
# - content_fit по умолчанию False (в отличие от других контейнеров).
|
||
# - widget_resizable по умолчанию True.
|
||
#
|
||
# 10) Правило сопровождения:
|
||
# При изменении scroll-политик проверять комбинацию с content_fit
|
||
# и widget_resizable — они взаимозависимы. SizeConstraint
|
||
# (SetMinAndMaxSize) критичен для правильного поведения scroll.
|