# -*- 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, )