# -*- 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.