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

192 lines
9.0 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/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.