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

245 lines
9.1 KiB
Python
Raw 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/components/topology_tree_widget.py
"""
Виджет дерева топологии склада с ленивой загрузкой.
"""
from __future__ import annotations
from typing import Optional, Dict, Any, Callable
from dataclasses import dataclass
from PySide6.QtCore import Qt, Signal, QTimer
from PySide6.QtWidgets import QTreeWidget, QTreeWidgetItem, QAbstractItemView, QHeaderView, QSizePolicy
from PySide6.QtGui import QFont
from gui.containers.s_container import SContainer
from gui.styles import APP_STYLES
from gui.theme_bus import theme_bus
from gui.components._tree_node_building import TreeNodeBuilder
from gui.components._tree_state_management import TreeStateManager
@dataclass
class TreeNodeData:
"""Данные узла дерева."""
node_type: str # типы узлов: 'site', 'facility', 'zone', 'rack', 'shelf', 'cell', 'volume'
node_id: str # UUID или другой идентификатор
display_data: Dict[str, Any] # Данные для отображения
has_children: bool = False
children_loaded: bool = False
raw_data: Optional[Dict[str, Any]] = None # Полные данные узла
class TopologyTreeWidget(SContainer):
"""Виджет дерева топологии склада с ленивой загрузкой."""
# Сигналы
nodeSelected = Signal(str, str, dict) # тип, ид, данные отображения
nodeDoubleClicked = Signal(str, str, dict) # тип, ид, данные отображения
dataLoadError = Signal(str) # сообщение об ошибке
# Карта отображения атрибутов для каждого типа узла (только ключи)
DISPLAY_ATTRIBUTES = {
'site': ['code', 'site_city'],
'facility': ['facility_type'],
'zone': ['code', 'name'],
'rack': ['code', 'name'],
'shelf': ['code', 'name', 'params', 'size'],
'cell': ['r', 'c', 'z'],
'volume': ['code', 'volume_mode', 'max_w', 'max_h', 'max_d']
}
def __init__(
self,
data_loader: Callable[[str, Optional[str]], list[TreeNodeData]],
width_percent: int | None = None,
height_percent: int | None = None,
margin: int | tuple[int, int, int, int] = 0,
style: str = "TREE_WIDGET_TOP_FLAT",
parent=None
):
super().__init__(
width_percent=width_percent,
height_percent=height_percent,
margin=margin,
parent=parent,
style=style,
)
self.data_loader = data_loader
self._loading_nodes = set()
self._tree = None
self._tree_style_key = style
# Сервисы (композиция вместо mixin)
self._node_builder = TreeNodeBuilder(self)
self._state_mgr = TreeStateManager(self)
self._tree = QTreeWidget()
self._tree.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Expanding)
super().add_widget(self._tree)
self._apply_tree_style()
self._setup_ui()
self._connect_signals()
theme_bus.theme_changed.connect(self._apply_tree_style)
QTimer.singleShot(0, self._node_builder.load_root_nodes)
# -- Настройка интерфейса -------------------------------------------------------------
def _setup_ui(self) -> None:
"""Настройка внешнего вида и поведения виджета."""
self._tree.setColumnCount(2)
self._tree.setHeaderHidden(True)
header = self._tree.header()
header.setStretchLastSection(True)
header.setSectionResizeMode(0, QHeaderView.ResizeToContents)
header.setSectionResizeMode(1, QHeaderView.Stretch)
self._tree.setSelectionMode(QAbstractItemView.SingleSelection)
self._tree.setSelectionBehavior(QAbstractItemView.SelectRows)
self._tree.setAnimated(True)
self._tree.setUniformRowHeights(True)
self._tree.setExpandsOnDoubleClick(False)
self._tree.setItemsExpandable(True)
font = QFont()
font.setPointSize(10)
self._tree.setFont(font)
def _apply_tree_style(self, *_args) -> None:
"""Явно применяем стиль к QTreeWidget."""
if not self._tree_style_key:
return
if self._tree is None:
return
self._tree.setStyleSheet(APP_STYLES.get(self._tree_style_key, ""))
def _connect_signals(self) -> None:
"""Подключение внутренних сигналов."""
self._tree.itemClicked.connect(self._on_item_clicked)
self._tree.itemDoubleClicked.connect(self._on_item_double_clicked)
self._tree.itemExpanded.connect(self._on_item_expanded)
self._tree.itemCollapsed.connect(self._on_item_collapsed)
# -- Обработчики событий -------------------------------------------------------
def _on_item_clicked(self, item: QTreeWidgetItem, column: int) -> None:
"""Обработчик одинарного клика на элементе дерева."""
item_data = item.data(0, Qt.UserRole)
if not item_data:
return
if item_data.get('is_stub') or item_data.get('is_error'):
return
self.nodeSelected.emit(item_data['type'], item_data['id'], item_data['display_data'])
def _on_item_double_clicked(self, item: QTreeWidgetItem, column: int) -> None:
"""Обработчик двойного клика на элементе дерева."""
item_data = item.data(0, Qt.UserRole)
if not item_data:
return
if item_data.get('is_stub') or item_data.get('is_error'):
return
self.nodeDoubleClicked.emit(item_data['type'], item_data['id'], item_data['display_data'])
def _on_item_expanded(self, item: QTreeWidgetItem) -> None:
"""Обработчик раскрытия элемента дерева."""
item_data = item.data(0, Qt.UserRole)
if not item_data:
return
if item_data.get('has_children') and not item_data.get('children_loaded'):
self._node_builder.load_children(item)
self.resizeColumnToContents(0)
def _on_item_collapsed(self, item: QTreeWidgetItem) -> None:
"""Обработчик сворачивания элемента дерева."""
item_data = item.data(0, Qt.UserRole)
if not item_data:
return
# -- Методы-адаптеры QWidget ----------------------------------------------
def __getattr__(self, name: str):
if name == "_tree":
return None
tree = self.__dict__.get("_tree")
if tree is None:
return super().__getattribute__(name)
return getattr(tree, name)
def viewport(self):
if self._tree is None:
return None
return self._tree.viewport()
def setMouseTracking(self, enabled: bool) -> None:
super().setMouseTracking(enabled)
if self._tree is not None:
self._tree.setMouseTracking(enabled)
viewport = self._tree.viewport()
if viewport is not None:
viewport.setMouseTracking(enabled)
def installEventFilter(self, filter_obj) -> None:
super().installEventFilter(filter_obj)
if self._tree is not None:
self._tree.installEventFilter(filter_obj)
viewport = self._tree.viewport()
if viewport is not None:
viewport.installEventFilter(filter_obj)
def set_enabled(self, enabled: bool) -> None:
self.setEnabled(enabled)
def set_min_width(self, width: int) -> None:
self.setMinimumWidth(width)
def set_min_height(self, height: int) -> None:
self.setMinimumHeight(height)
def set_max_width(self, width: int) -> None:
self.setMaximumWidth(width)
def set_max_height(self, height: int) -> None:
self.setMaximumHeight(height)
def set_fixed_size(self, width: int, height: int) -> None:
self.setMinimumSize(width, height)
self.setMaximumSize(width, height)
def set_tooltip(self, text: str) -> None:
self.setToolTip(text)
def set_size_policy(self, horizontal, vertical) -> None:
self.setSizePolicy(horizontal, vertical)
# -- Делегирование к сервисам (публичный API) ----------------------------
def reload_tree(self, preserve_state: bool = True) -> None:
self._state_mgr.reload_tree(preserve_state)
def clear_tree(self) -> None:
self._state_mgr.clear_tree()
def refresh_node(self, node_type: str, node_id: str) -> None:
self._state_mgr.refresh_node(node_type, node_id)
def select_node(
self,
node_type: str,
node_id: str,
*,
emit_selected: bool = False,
allow_load: bool = True,
expand_parents: bool = True,
) -> bool:
return self._state_mgr.select_node(
node_type, node_id,
emit_selected=emit_selected,
allow_load=allow_load,
expand_parents=expand_parents,
)