Add Dispatch_V0.1.1

This commit is contained in:
2026-04-29 08:18:54 +04:00
commit a7ede6ded4
404 changed files with 39167 additions and 0 deletions

View File

@@ -0,0 +1,333 @@
# -*- 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 в конструктор — запрещено правилами.
# Примеры использования — в блоке комментариев внизу файла.