# -*- coding: utf-8 -*- # gui/components/_tree_state_management.py """Сервис управления состоянием дерева: поиск, раскрытие, перезагрузка (композиция).""" from __future__ import annotations from typing import TYPE_CHECKING, Optional from PySide6.QtCore import Qt from PySide6.QtWidgets import QTreeWidgetItem, QAbstractItemView from error_logger import log_exception if TYPE_CHECKING: from gui.components.topology_tree_widget import TopologyTreeWidget class TreeStateManager: """Поиск узлов, снимок/восстановление раскрытия, перезагрузка дерева.""" def __init__(self, host: "TopologyTreeWidget") -> None: self._host = host def refresh_node(self, node_type: str, node_id: str) -> None: """Обновление данных узла и его детей.""" found_item = self._find_item_by_data(node_type, node_id) if found_item: while found_item.childCount() > 0: found_item.removeChild(found_item.child(0)) item_data = found_item.data(0, Qt.UserRole) if item_data: item_data['children_loaded'] = False found_item.setData(0, Qt.UserRole, item_data) if item_data.get('has_children'): stub = QTreeWidgetItem(["Загрузка..."]) stub.setData(0, Qt.UserRole, {"is_stub": True}) found_item.addChild(stub) self._host._tree.resizeColumnToContents(0) def _find_item_by_data(self, node_type: str, node_id: str) -> Optional[QTreeWidgetItem]: """Поиск элемента дерева по типу и ID.""" def search(item: QTreeWidgetItem) -> Optional[QTreeWidgetItem]: d = item.data(0, Qt.UserRole) if d and d.get('type') == node_type and d.get('id') == node_id: return item for i in range(item.childCount()): result = search(item.child(i)) if result: return result return None for i in range(self._host._tree.topLevelItemCount()): result = search(self._host._tree.topLevelItem(i)) if result: return result return None def _find_or_load_item_by_data( self, node_type: str, node_id: str, ) -> Optional[QTreeWidgetItem]: """Найти узел, лениво подгружая и раскрывая ветки.""" node_type = str(node_type or "") node_id = str(node_id or "") if not node_type or not node_id: return None found = self._find_item_by_data(node_type, node_id) if found is not None: return found def search(item: QTreeWidgetItem) -> Optional[QTreeWidgetItem]: d = item.data(0, Qt.UserRole) or {} if d.get("is_stub") or d.get("is_error"): return None ct = str(d.get("type") or "") ci = str(d.get("id") or "") if ct == node_type and ci == node_id: return item if ct == node_type and ci != node_id: return None if bool(d.get("has_children")) and not bool(d.get("children_loaded")): try: self._host._node_builder.load_children(item) except Exception as e: log_exception(__name__, "_find_or_load.load_children", e) for idx in range(item.childCount()): result = search(item.child(idx)) if result is not None: try: item.setExpanded(True) except Exception as e: log_exception(__name__, "_find_or_load.setExpanded", e) return result return None for idx in range(self._host._tree.topLevelItemCount()): result = search(self._host._tree.topLevelItem(idx)) if result is not None: return result return None @staticmethod def _expand_item_parents(item: QTreeWidgetItem) -> None: """Раскрыть цепочку родителей до корня.""" parent = item.parent() while parent is not None: parent.setExpanded(True) parent = parent.parent() def clear_tree(self) -> None: """Полная очистка дерева.""" self._host._tree.clear() self._host._loading_nodes.clear() @staticmethod def _item_key(item: QTreeWidgetItem) -> tuple[str, str] | None: """Уникальный ключ узла: (type, id).""" data = item.data(0, Qt.UserRole) or {} nt = str(data.get("type") or "") ni = str(data.get("id") or "") if not nt or not ni: return None if data.get("is_stub") or data.get("is_error"): return None return nt, ni def _collect_expanded_paths(self) -> list[list[tuple[str, str]]]: """Снять снимок раскрытых узлов как пути от корня.""" paths: list[list[tuple[str, str]]] = [] def walk(item: QTreeWidgetItem, path: list[tuple[str, str]]) -> None: key = self._item_key(item) if key is None: return path = [*path, key] if item.isExpanded(): paths.append(path) for idx in range(item.childCount()): walk(item.child(idx), path) for idx in range(self._host._tree.topLevelItemCount()): walk(self._host._tree.topLevelItem(idx), []) return paths def _find_child_by_key(self, parent: QTreeWidgetItem, key: tuple[str, str]) -> Optional[QTreeWidgetItem]: for idx in range(parent.childCount()): child = parent.child(idx) if self._item_key(child) == key: return child return None def _find_root_by_key(self, key: tuple[str, str]) -> Optional[QTreeWidgetItem]: for idx in range(self._host._tree.topLevelItemCount()): item = self._host._tree.topLevelItem(idx) if self._item_key(item) == key: return item return None def _expand_path(self, path: list[tuple[str, str]]) -> None: """Раскрыть путь root->... с подзагрузкой ленивых детей.""" if not path: return current = self._find_root_by_key(path[0]) if current is None: return current.setExpanded(True) for key in path[1:]: data = current.data(0, Qt.UserRole) or {} if data.get("has_children") and not data.get("children_loaded"): self._host._node_builder.load_children(current) child = self._find_child_by_key(current, key) if child is None: return child.setExpanded(True) current = child def _restore_tree_state( self, expanded_paths: list[list[tuple[str, str]]], selected_key: tuple[str, str] | None, ) -> None: """Восстановить раскрытие и выбранный элемент после reload.""" for path in expanded_paths: self._expand_path(path) if selected_key: sel = self._find_item_by_data(selected_key[0], selected_key[1]) if sel is not None: self._host._tree.setCurrentItem(sel) self._host._tree.scrollToItem( sel, QAbstractItemView.ScrollHint.PositionAtCenter, ) def reload_tree(self, preserve_state: bool = True) -> None: """Полная перезагрузка дерева с опциональным сохранением состояния.""" expanded_paths: list[list[tuple[str, str]]] = [] selected_key: tuple[str, str] | None = None if preserve_state: expanded_paths = self._collect_expanded_paths() current = self._host._tree.currentItem() if current is not None: selected_key = self._item_key(current) self.clear_tree() self._host._node_builder.load_root_nodes() if preserve_state: self._restore_tree_state(expanded_paths, selected_key) self._host._tree.resizeColumnToContents(0) def select_node( self, node_type: str, node_id: str, *, emit_selected: bool = False, allow_load: bool = True, expand_parents: bool = True, ) -> bool: """Программно выделить узел дерева. Возвращает True при успехе.""" rt = str(node_type or "") ri = str(node_id or "") if allow_load: item = self._find_or_load_item_by_data(rt, ri) else: item = self._find_item_by_data(rt, ri) if item is None: return False if expand_parents: self._expand_item_parents(item) self._host._tree.setCurrentItem(item) self._host._tree.scrollToItem( item, QAbstractItemView.ScrollHint.PositionAtCenter, ) if emit_selected: d = item.data(0, Qt.UserRole) or {} if not d.get("is_stub") and not d.get("is_error"): self._host.nodeSelected.emit( str(d.get("type") or ""), str(d.get("id") or ""), dict(d.get("display_data") or {}), ) return True