Add Dispatch_V0.1.1
This commit is contained in:
235
Dispatch_V0.1.1/gui/components/_tree_state_management.py
Normal file
235
Dispatch_V0.1.1/gui/components/_tree_state_management.py
Normal file
@@ -0,0 +1,235 @@
|
||||
# -*- 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
|
||||
Reference in New Issue
Block a user