# -*- coding: utf-8 -*- # gui/containers/grid_container.py from PySide6.QtCore import Qt from PySide6.QtWidgets import QGridLayout, QLayout, QWidget, QSizePolicy from .percent_sized_widget import PercentSizedWidget from .content_host import ContentHost class GridContainer(PercentSizedWidget): """Сеточный контейнер с размерами в процентах от родителя.""" 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] | None = None, spacing: int | None = None, horizontal_spacing: int = 0, vertical_spacing: int = 0, 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, row_percentages: list[int | float] | None = None, col_percentages: list[int | float] | None = None, row_stretches: list[int] | None = None, col_stretches: list[int] | None = None, 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._outer_layout = QGridLayout(self) self._outer_layout.setSpacing(0) applied_margins = content_margins if content_margins is not None else margin if isinstance(applied_margins, (list, tuple)) and len(applied_margins) == 4: self._outer_layout.setContentsMargins(*applied_margins) else: self._outer_layout.setContentsMargins(applied_margins, applied_margins, applied_margins, applied_margins) self._content_host = ContentHost( width_percent=content_width_percent, height_percent=content_height_percent, orientation="v", margin=0, spacing=0, 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._grid_widget = QWidget(self._content_host) self._layout = QGridLayout(self._grid_widget) if spacing is not None: self._layout.setHorizontalSpacing(spacing) self._layout.setVerticalSpacing(spacing) else: self._layout.setHorizontalSpacing(horizontal_spacing) self._layout.setVerticalSpacing(vertical_spacing) self._content_host.add_widget(self._grid_widget) self._outer_layout.addWidget(self._content_host, 0, 0) self._outer_layout.setRowStretch(0, 1) self._outer_layout.setColumnStretch(0, 1) self._row_percentages = [] self._col_percentages = [] if row_percentages is not None and col_percentages is not None: self.set_cell_percentages(row_percentages, col_percentages) if row_stretches is not None: self.set_row_stretches(row_stretches) if col_stretches is not None: self.set_col_stretches(col_stretches) 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) def set_cell_percentages(self, row_percentages: list[int | float], col_percentages: list[int | float]): """Устанавливает процентное соотношение строк и столбцов.""" if not row_percentages or not col_percentages: raise ValueError("Не указано процентное соотношение ячеек сетки") self._row_percentages = self.normalize_percentages(row_percentages) self._col_percentages = self.normalize_percentages(col_percentages) for row, percent in enumerate(self._row_percentages): self._layout.setRowStretch(row, int(percent)) for col, percent in enumerate(self._col_percentages): self._layout.setColumnStretch(col, int(percent)) def normalize_percentages(self, percentages: list[int | float]) -> list[float]: """Нормализует список процентов до суммы 100.""" total = sum(percentages) if total == 0: return [100 / len(percentages)] * len(percentages) elif total != 100: return [p / total * 100 for p in percentages] return percentages.copy() def add_widget(self, widget: QWidget, row: int, col: int, row_span: int = 1, col_span: int = 1, alignment: str | None = None) -> None: if row + row_span - 1 >= len(self._row_percentages) or col + col_span - 1 >= len(self._col_percentages): raise IndexError(f"Ячейка ({row}, {col}) с span ({row_span}, {col_span}) вне диапазона сетки") self._layout.addWidget(widget, row, col, row_span, col_span) if isinstance(widget, PercentSizedWidget): widget.schedule_percent_update() def set_spacing(self, horizontal: int, vertical: int) -> None: """ Устанавливает раздельные отступы между ячейками по горизонтали и вертикали. Args: horizontal: Отступ между колонками в пикселях. vertical: Отступ между строками в пикселях. """ self._layout.setHorizontalSpacing(horizontal) self._layout.setVerticalSpacing(vertical) def get_layout(self) -> QLayout: return self._outer_layout def set_margins(self, margin: int | tuple[int, int, int, int]) -> None: if isinstance(margin, (list, tuple)) and len(margin) == 4: self._outer_layout.setContentsMargins(*margin) else: self._outer_layout.setContentsMargins(margin, margin, margin, margin) def set_alignment(self, alignment: str) -> None: raise NotImplementedError("Qt alignment for containers is disabled; use content springs.") def set_column_minimum_width(self, col: int, width: int) -> None: """ Устанавливает минимальную ширину колонки. Args: col: Индекс колонки (начиная с 0). width: Минимальная ширина в пикселях. """ if col < 0: raise ValueError(f"Индекс колонки не может быть отрицательным: {col}") self._layout.setColumnMinimumWidth(col, width) def set_column_min_widths(self, widths: list[int]) -> None: for col, width in enumerate(widths): self.set_column_minimum_width(col, width) def set_row_stretches(self, stretches: list[int]) -> None: for row, stretch in enumerate(stretches): self._layout.setRowStretch(row, int(stretch)) def set_col_stretches(self, stretches: list[int]) -> None: for col, stretch in enumerate(stretches): self._layout.setColumnStretch(col, int(stretch)) def set_row_minimum_height(self, row: int, height: int) -> None: """ Устанавливает минимальную высоту строки. Args: row: Индекс строки (начиная с 0). height: Минимальная высота в пикселях. """ if row < 0: raise ValueError(f"Индекс строки не может быть отрицательным: {row}") self._layout.setRowMinimumHeight(row, height) def set_row_min_heights(self, heights: list[int]) -> None: for row, height in enumerate(heights): self.set_row_minimum_height(row, height) def get_available_size_for_content(self) -> tuple[int, int]: """Полезная внутренняя область (без учёта margin).""" margins = self._outer_layout.contentsMargins() w = self.width() - margins.left() - margins.right() h = self.height() - margins.top() - margins.bottom() return max(0, w), max(0, h) # ============================================================================ # Примеры использования GridContainer # ============================================================================ # # Базовый пример: форма с двумя колонками (labels и inputs) # ---------------------------------------------------------------------------- # grid = GridContainer(width_percent=100, margin=6, parent=parent_widget) # grid.set_cell_percentages(row_percentages=[1, 1, 1], col_percentages=[40, 60]) # grid.set_spacing(horizontal=12, vertical=8) # grid.set_column_minimum_width(0, 120) # # # Добавление виджетов (row, col) # grid.add_widget(label1, 0, 0) # grid.add_widget(input1, 0, 1) # grid.add_widget(label2, 1, 0) # grid.add_widget(input2, 1, 1) # grid.add_widget(label3, 2, 0) # grid.add_widget(input3, 2, 1) # # ============================================================================ # Пример: форма со stretch колонок (для пропорционального роста) # ---------------------------------------------------------------------------- # grid = GridContainer(width_percent=100, height_percent=50) # grid.set_cell_percentages(row_percentages=[1, 1], col_percentages=[1, 2]) # grid.set_spacing(horizontal=12, vertical=2) # grid.set_column_minimum_width(0, 140) # # # Первая колонка растёт в 1x, вторая в 2x # grid.add_widget(code_label, 0, 0) # grid.add_widget(code_input, 0, 1) # grid.add_widget(name_label, 1, 0) # grid.add_widget(name_input, 1, 1) # # ============================================================================ # Сложная сетка с объединением ячеек (span) # ---------------------------------------------------------------------------- # grid = GridContainer(width_percent=100, height_percent=100, spacing=10) # grid.set_cell_percentages( # row_percentages=[20, 30, 50], # col_percentages=[25, 25, 25, 25] # ) # # # Виджет занимает 2 колонки (row_span=1, col_span=2) # grid.add_widget(header_widget, 0, 0, row_span=1, col_span=4) # grid.add_widget(sidebar_widget, 1, 0, row_span=2, col_span=1) # grid.add_widget(content_widget, 1, 1, row_span=2, col_span=3) # # ============================================================================ # Важные замечания # ---------------------------------------------------------------------------- # 1. set_cell_percentages ОБЯЗАТЕЛЕН перед add_widget, иначе IndexError # 2. row_percentages и col_percentages автоматически нормализуются до 100% # Например: [1, 2, 1] → [25%, 50%, 25%] # 3. spacing применяется ко всем ячейкам; set_spacing позволяет раздельный # horizontal/vertical spacing для более тонкой настройки # 4. Minimum width/height гарантируют, что ячейка не сожмётся ниже лимита # даже при малом размере родителя # 5. Для форм с парами label-input рекомендуется: # - col_percentages=[1, 2] (label уже, input шире) # - set_column_minimum_width(0, 120-140) для label # - horizontal_spacing=12, vertical_spacing=2-8 # ============================================================================ # --------------------------------------------------------------------------- # Module workflow notes # --------------------------------------------------------------------------- # # 1) Назначение модуля: # Сеточный контейнер с размерами в процентах от родителя. Обеспечивает # размещение виджетов в строках/столбцах с процентным распределением, # поддержкой span (объединения ячеек), выравнивания через spring-based # alignment и стилизации через APP_STYLES/тему. # # 2) Зависимости модуля: # Импорты: Qt, QGridLayout, QLayout, QWidget, QSizePolicy (PySide6) # Хост/базовый класс: StylableMixin + PercentSizedWidget (MRO) # Внутренние: ContentHost (content_host.py) # Внешние библиотеки: PySide6 # # 3) Экспорт: # Класс GridContainer — сеточный контейнер. # Методы: set_cell_percentages(), normalize_percentages(), add_widget(), # set_spacing(), get_layout(), set_margins(), set_alignment(), # set_column_minimum_width(), set_column_min_widths(), # set_row_stretches(), set_col_stretches(), # set_row_minimum_height(), set_row_min_heights(), # get_available_size_for_content() # # 4) Состояние (поля): # _outer_layout : QGridLayout — внешний layout (spring-based alignment). # _content_host : ContentHost — промежуточный хост для внутренней сетки. # _grid_widget : QWidget — виджет, содержащий внутренний QGridLayout. # _layout : QGridLayout — внутренний layout для пользовательских ячеек. # _row_percentages: list[float] — нормализованные доли строк (до 100%). # _col_percentages: list[float] — нормализованные доли столбцов (до 100%). # # 5) Последовательность действий и вызовов: # __init__(params) -> super().__init__(w%, h%, parent) # -> _init_stylable() -> создание _outer_layout (QGridLayout) # -> создание ContentHost -> создание _grid_widget + _layout (QGridLayout) # -> _content_host.add_widget(_grid_widget) # -> set_cell_percentages() если row/col_percentages заданы # -> set_row_stretches() / set_col_stretches() если заданы # -> _apply_style() если style/active_style/is_active заданы # add_widget(widget, row, col, ...) -> проверка bounds -> _layout.addWidget() # set_cell_percentages(rows, cols) -> normalize -> setRowStretch/setColumnStretch # # 6) Побочные эффекты: # Мутирует stretch-значения QGridLayout при set_cell_percentages/stretches. # _apply_style() устанавливает stylesheet на self. # set_alignment() бросает NotImplementedError — запрещён. # # 7) Границы ответственности: # НЕ размещает виджеты автоматически — требует явного add_widget(row, col). # НЕ поддерживает alignment. # НЕ управляет auto_add_children (нет флага _auto_add_children). # # 8) Обработка ошибок: # set_cell_percentages: ValueError при пустых row/col_percentages. # add_widget: IndexError при выходе за пределы сетки. # set_column_minimum_width / set_row_minimum_height: ValueError при index < 0. # _parse_alignment_springs: удалён, alignment запрещён. # # 9) Инварианты и контракты: # - set_cell_percentages ОБЯЗАТЕЛЕН до add_widget, иначе IndexError. # - row/col_percentages нормализуются до суммы 100%. # - content_margins и margin — не одно и то же: content_margins приоритетнее. # # 10) Правило сопровождения: # Не менять двухуровневую компоновку (outer_layout → content_host → grid). # Не добавлять alignment в конструктор — запрещено правилами. # Примеры использования — в блоке комментариев внизу файла.