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

236 lines
9.5 KiB
Python
Raw Permalink 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/_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