236 lines
9.5 KiB
Python
236 lines
9.5 KiB
Python
# -*- 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
|