334 lines
16 KiB
Python
334 lines
16 KiB
Python
# -*- 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 в конструктор — запрещено правилами.
|
||
# Примеры использования — в блоке комментариев внизу файла.
|