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

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