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,335 @@
# -*- coding: utf-8 -*-
# gui/components/tab_widget.py
from typing import Any, List, Optional, Type
from PySide6.QtCore import Signal
from PySide6.QtGui import QResizeEvent, QShowEvent
from PySide6.QtWidgets import QWidget
from gui.containers.s_container import SContainer
from gui.containers.h_container import HContainer
from gui.containers.stack_container import StackContainer
from gui.components.tab_button import TabButton
from error_logger import log_exception
class TabWidget(SContainer):
"""
Процентный TabWidget:
- Верхняя панель вкладок (HContainer) заданной высоты в %
- Контент (StackContainer + QStackedWidget) занимает оставшуюся высоту
"""
currentChanged = Signal(int)
def __init__(
self,
width_percent: Optional[int] = None,
height_percent: Optional[int] = None,
tab_bar_height_percent: int = 8,
margin: int = 0,
parent: Optional[QWidget] = None,
content_fit: bool = True,
style: Optional[str] = None,
active_style: Optional[str] = None,
is_active: Optional[bool] = None,
button_cls: Type = TabButton,
button_style_normal: Optional[str] = None,
button_style_active: Optional[str] = None,
button_width_percent: Optional[int] = None,
button_height_percent: Optional[int] = None,
button_margin: Optional[list[int]] = None,
button_inner_margin: Optional[int] = None,
tab_bar_content_fit: bool = True,
):
super().__init__(
width_percent=width_percent,
height_percent=height_percent,
orientation="v",
margin=margin,
content_fit=content_fit,
parent=parent
)
if not (1 <= tab_bar_height_percent <= 99):
raise ValueError("tab_bar_height_percent должен быть в диапазоне 1..99")
self._tab_bar_height_percent = tab_bar_height_percent
self._content_height_percent = 100 - tab_bar_height_percent
# Верхняя панель вкладок
self._tab_bar = HContainer(
height_percent=self._tab_bar_height_percent,
margin=0,
content_fit=tab_bar_content_fit,
)
# Контентная часть
self._content_container = StackContainer(
width_percent=100,
height_percent=self._content_height_percent,
margin=0
)
# Добавляем в основной VContainer
self.add_widget(self._tab_bar)
self.add_widget(self._content_container)
self._button_cls = button_cls
self._button_style_normal = button_style_normal
self._button_style_active = button_style_active
self._button_width_percent = button_width_percent
self._button_height_percent = button_height_percent
self._button_margin = button_margin
self._button_inner_margin = button_inner_margin
self._buttons: List[Any] = []
if style is not None or active_style is not None or is_active is not None:
self.style(style_key=style, active_key=active_style, is_active=is_active)
# -------------------- Публичный API --------------------
def showEvent(self, event: QShowEvent) -> None:
super().showEvent(event)
self._recompute_tab_button_widths()
def resizeEvent(self, event: QResizeEvent) -> None:
super().resizeEvent(event)
self._recompute_tab_button_widths()
def style(
self,
style_key: Optional[str] = None,
active_key: Optional[str] = None,
is_active: Optional[bool] = None,
) -> None:
"""Короткий метод применения стиля для TabWidget."""
super().style(style_key=style_key, active_key=active_key, is_active=is_active)
def add_tab(self, widget: QWidget, title: str) -> int:
"""
Добавляет вкладку и страницу.
Возвращает индекс новой вкладки.
"""
index = len(self._buttons)
btn = self._button_cls(
text=title,
index=index,
width_percent=self._button_width_percent,
height_percent=self._button_height_percent or 100,
margin=self._button_margin if self._button_margin is not None else 0,
text_left_margin=self._button_inner_margin,
style=self._button_style_normal,
active_style=self._button_style_active,
)
btn.clicked.connect(lambda checked=False, i=index: self.set_current_index(i))
self._tab_bar.add_widget(btn)
self._content_container.add_widget(widget)
self._buttons.append(btn)
self._recompute_tab_button_widths()
# Если это первая вкладка — активируем
if index == 0:
self.set_current_index(0)
return index
def set_current_index(self, index: int) -> None:
if index < 0 or index >= self.count():
return
self._content_container.set_current_index(index)
self._apply_active_style(index)
self.currentChanged.emit(index)
def current_index(self) -> int:
return self._content_container.current_index()
def widget(self, index: int) -> Optional[QWidget]:
return self._content_container.widget(index)
def count(self) -> int:
return self._content_container.count()
def remove_tab(self, index: int) -> None:
if index < 0 or index >= self.count():
return
# 1) убрать кнопку
btn = self._buttons.pop(index)
btn.setParent(None)
btn.deleteLater()
# 2) убрать страницу
w = self._content_container.widget(index)
if w is not None:
self._content_container.remove_widget(w)
if w is not None:
w.setParent(None)
# 3) переиндексация кнопок и их callback
for i, b in enumerate(self._buttons):
b.index = i
if hasattr(b, "set_property"):
b.set_property("tab_index", i)
try:
b.clicked.disconnect()
except Exception as e:
log_exception(__name__, "remove_tab.disconnect", e)
b.clicked.connect(lambda checked=False, ii=i: self.set_current_index(ii))
self._recompute_tab_button_widths()
# 4) корректировка текущей
if self.count() > 0:
self.set_current_index(min(index, self.count() - 1))
def set_tab_text(self, index: int, text: str) -> None:
if 0 <= index < len(self._buttons):
self._buttons[index].set_text(text)
def set_tab_enabled(self, index: int, enabled: bool) -> None:
if 0 <= index < len(self._buttons):
self._buttons[index].set_enabled(enabled)
def add_tab_bar_spacer(self, stretch: int = 1) -> None:
"""Добавляет растягиваемый спейсер в панель вкладок (прижимает кнопки влево)."""
self._tab_bar.add_stretch(stretch)
# -------------------- Внутренние --------------------
def _apply_active_style(self, active_index: int) -> None:
for i, b in enumerate(self._buttons):
if i == active_index:
b.style(is_active=True)
else:
b.style(is_active=False)
def _recompute_tab_button_widths(self) -> None:
"""
Делит 100% ширины между кнопками вкладок.
Важное: проценты целые, сумма строго 100.
"""
n = len(self._buttons)
if n == 0:
return
base = 100 // n
rem = 100 % n
for i, b in enumerate(self._buttons):
if self._button_width_percent is not None:
w = self._button_width_percent
else:
w = base + (1 if i < rem else 0)
h = self._button_height_percent or 100
# API PercentSizedWidget
b.set_percent_sizes(width_percent=w, height_percent=h)
# Чтобы компоновка пересчиталась сразу
self._tab_bar.invalidate_layout()
tab_bar_layout = self._tab_bar.get_layout()
if tab_bar_layout is not None:
tab_bar_layout.activate()
self._tab_bar.updateGeometry()
self.updateGeometry()
# ---------------------------------------------------------------------------
# Module workflow notes
# ---------------------------------------------------------------------------
#
# 1) Назначение модуля:
# Процентный TabWidget с верхней панелью вкладок (HContainer)
# заданной высоты в % и контентной частью (StackContainer),
# занимающей оставшуюся высоту.
#
# 2) Зависимости модуля:
# Импорты: Any, List, Optional, Type (typing),
# Signal, Qt (PySide6.QtCore),
# QResizeEvent, QShowEvent (PySide6.QtGui),
# QWidget (PySide6.QtWidgets),
# SContainer (gui.containers.s_container),
# HContainer (gui.containers.h_container),
# StackContainer (gui.containers.stack_container),
# TabButton (gui.components.tab_button)
# Хост-класс / базовый класс: SContainer (orientation="v")
# Внешние библиотеки: PySide6 (обязательна)
#
# 3) Экспорт:
# Класс TabWidget — контейнер с вкладками.
# Сигнал: currentChanged(int).
# Методы: add_tab(widget, title) -> index, set_current_index(int),
# current_index(), widget(index), count(), remove_tab(index),
# set_tab_text(), set_tab_enabled(), add_tab_bar_spacer(),
# style().
#
# 4) Состояние (поля):
# _tab_bar_height_percent: int — высота панели вкладок в %
# _content_height_percent: int — высота контента (100 - tab_bar)
# _tab_bar: HContainer — горизонтальный контейнер кнопок
# _content_container: StackContainer — стек страниц
# _button_cls: Type — класс кнопки (по умолчанию TabButton)
# _button_style_normal: str|None — нормальный стиль кнопок
# _button_style_active: str|None — активный стиль кнопок
# _button_width/height_percent: int|None — размеры кнопок
# _button_margin: list[int]|None — отступы кнопок
# _button_inner_margin: int|None — внутренний отступ текста
# _buttons: list[TabButton] — список кнопок вкладок
#
# 5) Последовательность действий и вызовов:
# __init__(tab_bar_height_percent, ...)
# -> super().__init__(orientation="v")
# -> HContainer(height_percent) — панель вкладок
# -> StackContainer(height_percent) — стек контента
# -> self.add_widget(_tab_bar) + self.add_widget(_content_container)
# add_tab(widget, title)
# -> TabButton(text, index, ...) -> clicked.connect(set_current_index)
# -> _tab_bar.add_widget(btn) -> _content_container.add_widget(widget)
# -> _recompute_tab_button_widths()
# -> если первая вкладка → set_current_index(0)
# set_current_index(index)
# -> _content_container.set_current_index(index)
# -> _apply_active_style(index) — is_active=True для текущей, False для остальных
# -> currentChanged.emit(index)
# remove_tab(index)
# -> удалить кнопку, удалить страницу, переиндексировать оставшиеся
# -> _recompute_tab_button_widths()
# _recompute_tab_button_widths()
# -> 100% / n кнопок → set_percent_sizes на каждую кнопку
# -> invalidate_layout + activate + updateGeometry
#
# 6) Побочные эффекты:
# - Модифицирует _tab_bar и _content_container при add/remove.
# - Испускает currentChanged при смене вкладки.
# - Пересчитывает ширину кнопок при showEvent/resizeEvent.
#
# 7) Границы ответственности:
# Модуль управляет переключением страниц и визуальным состоянием
# вкладок. НЕ определяет стили кнопок — делегирует TabButton.
# НЕ создаёт содержимое страниц.
#
# 8) Обработка ошибок:
# tab_bar_height_percent вне [1, 99] → ValueError.
# set_current_index() / remove_tab() — проверка диапазона индекса.
# disconnect() в remove_tab() обёрнут в try/except.
#
# 9) Инварианты и контракты:
# - tab_bar_height_percent ∈ [1, 99].
# - len(_buttons) == _content_container.count().
# - Сумма width_percent всех кнопок == 100 (при автораскладке).
# - Индексы кнопок совпадают с индексами страниц в StackContainer.
#
# 10) Правило сопровождения:
# Для кастомизации кнопок — передать button_cls в конструктор.
# Не манипулировать _tab_bar и _content_container напрямую —
# использовать add_tab() / remove_tab().