Add Dispatch_V0.1.1

This commit is contained in:
2026-04-29 08:18:54 +04:00
commit a7ede6ded4
404 changed files with 39167 additions and 0 deletions

View 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