haybale-graph-editor 0.0.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- haybale_graph_editor/__init__.py +59 -0
- haybale_graph_editor/editors/__init__.py +0 -0
- haybale_graph_editor/editors/graph_canvas/__init__.py +16 -0
- haybale_graph_editor/editors/graph_canvas/connection_info_popup.py +134 -0
- haybale_graph_editor/editors/graph_canvas/event_handlers.py +46 -0
- haybale_graph_editor/editors/graph_canvas/graph_canvas_manager.py +217 -0
- haybale_graph_editor/editors/graph_canvas/handlers/__init__.py +11 -0
- haybale_graph_editor/editors/graph_canvas/handlers/context_menu.py +543 -0
- haybale_graph_editor/editors/graph_canvas/handlers/context_menu_actions.py +58 -0
- haybale_graph_editor/editors/graph_canvas/handlers/interaction.py +56 -0
- haybale_graph_editor/editors/graph_canvas/handlers/selection.py +147 -0
- haybale_graph_editor/editors/graph_canvas/handlers/visual_layer.py +465 -0
- haybale_graph_editor/editors/graph_canvas/node_menu_builder.py +389 -0
- haybale_graph_editor/editors/graph_canvas/ui_edge.py +205 -0
- haybale_graph_editor/editors/graph_canvas/ui_node.py +300 -0
- haybale_graph_editor/editors/graph_editor.py +515 -0
- haybale_graph_editor/editors/node_status.py +66 -0
- haybale_graph_editor/focuses.py +70 -0
- haybale_graph_editor/panels/context_menu/create_node_panel.py +99 -0
- haybale_graph_editor/panels/context_menu/edge_actions.py +54 -0
- haybale_graph_editor/panels/context_menu/node_actions.py +171 -0
- haybale_graph_editor/panels/context_menu/node_errors.py +87 -0
- haybale_graph_editor/panels/context_menu/port_info.py +53 -0
- haybale_graph_editor/panels/context_menu/selection_actions.py +80 -0
- haybale_graph_editor/panels/edge_panels.py +283 -0
- haybale_graph_editor/panels/graph_info_panel.py +52 -0
- haybale_graph_editor/panels/node_ports_panel.py +80 -0
- haybale_graph_editor/panels/node_props_panel.py +83 -0
- haybale_graph_editor/panels/node_settings.py +71 -0
- haybale_graph_editor/protocols.py +54 -0
- haybale_graph_editor/state/__init__.py +0 -0
- haybale_graph_editor/state/edit_state.py +57 -0
- haybale_graph_editor/state/graph_app_state.py +69 -0
- haybale_graph_editor-0.0.2.dist-info/METADATA +8 -0
- haybale_graph_editor-0.0.2.dist-info/RECORD +37 -0
- haybale_graph_editor-0.0.2.dist-info/WHEEL +4 -0
- haybale_graph_editor-0.0.2.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""haybale-graph-editor: graph editor library for Haywire.
|
|
2
|
+
|
|
3
|
+
Provides the GraphContainer protocol, GraphAppState registry, and
|
|
4
|
+
GraphEditor surface. Decoupled from any specific graph source — source
|
|
5
|
+
libraries register their containers, this library renders them.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
from importlib.metadata import version as _pkg_version
|
|
9
|
+
from pathlib import Path
|
|
10
|
+
|
|
11
|
+
from haywire.core.library.base import BaseLibrary
|
|
12
|
+
from haywire.core.library.decorator import library
|
|
13
|
+
from haywire.core.state import LibraryStateRegistry
|
|
14
|
+
from haywire.ui.editor.registry import EditorTypeRegistry
|
|
15
|
+
|
|
16
|
+
# Public API re-exports
|
|
17
|
+
from haybale_graph_editor.protocols import GraphContainer
|
|
18
|
+
from haybale_graph_editor.state.graph_app_state import GraphAppState
|
|
19
|
+
from haywire.ui.panel.registry import PanelRegistry
|
|
20
|
+
|
|
21
|
+
__all__ = ["GraphContainer", "GraphAppState", "Library"]
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
@library(
|
|
25
|
+
label="Graph Editor",
|
|
26
|
+
id="graph_editor",
|
|
27
|
+
version=_pkg_version("haybale-graph-editor"),
|
|
28
|
+
description="Visual graph editor library — host-agnostic",
|
|
29
|
+
url="",
|
|
30
|
+
help_url="",
|
|
31
|
+
author="",
|
|
32
|
+
author_url="",
|
|
33
|
+
dependencies=[],
|
|
34
|
+
tags=["graph-editor"],
|
|
35
|
+
file_watcher=True,
|
|
36
|
+
)
|
|
37
|
+
class Library(BaseLibrary):
|
|
38
|
+
"""Graph Editor library."""
|
|
39
|
+
|
|
40
|
+
def register_components(self):
|
|
41
|
+
base_path = Path(__file__).parent
|
|
42
|
+
|
|
43
|
+
self.add_folder_to_registry(
|
|
44
|
+
folder_path=str(base_path / "state"),
|
|
45
|
+
registry_cls=LibraryStateRegistry,
|
|
46
|
+
)
|
|
47
|
+
|
|
48
|
+
self.add_folder_to_registry(
|
|
49
|
+
folder_path=str(base_path / "panels"),
|
|
50
|
+
registry_cls=PanelRegistry,
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
self.add_folder_to_registry(
|
|
54
|
+
folder_path=str(base_path / "editors"),
|
|
55
|
+
registry_cls=EditorTypeRegistry,
|
|
56
|
+
)
|
|
57
|
+
|
|
58
|
+
def validate(self) -> bool:
|
|
59
|
+
return True
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
from .connection_info_popup import EdgeInfoPopup
|
|
2
|
+
from .graph_canvas_manager import GraphCanvasManager
|
|
3
|
+
from .node_menu_builder import NodeMenuBuilder
|
|
4
|
+
from .ui_edge import EdgeVisualState
|
|
5
|
+
from .ui_edge import UIEdge
|
|
6
|
+
from .ui_node import UINode
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"EdgeInfoPopup",
|
|
10
|
+
"EdgeVisualState",
|
|
11
|
+
"GraphCanvasManager",
|
|
12
|
+
"GraphEventMetadata",
|
|
13
|
+
"NodeMenuBuilder",
|
|
14
|
+
"UIEdge",
|
|
15
|
+
"UINode",
|
|
16
|
+
]
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
"""
|
|
2
|
+
ConnectionInfoPopup - Detailed connection information display component
|
|
3
|
+
|
|
4
|
+
This component provides a dedicated popup for inspecting edge/connection details including:
|
|
5
|
+
- Connection path (nodes and ports)
|
|
6
|
+
- Validation status
|
|
7
|
+
- Error details with full error rendering
|
|
8
|
+
- Warning messages
|
|
9
|
+
- Adapter chain visualization and testing
|
|
10
|
+
- Execution statistics
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from nicegui import ui
|
|
14
|
+
from typing import Optional
|
|
15
|
+
|
|
16
|
+
from haywire.core.edge.edge import Edge
|
|
17
|
+
from haywire.core.errors.haywire_exception import HaywireException
|
|
18
|
+
from haywire.core.edge.edge_wrapper import EdgeWrapperState
|
|
19
|
+
|
|
20
|
+
from haywire.ui import elements as hui
|
|
21
|
+
from haywire.ui.errors.error_info import error_render_detail
|
|
22
|
+
|
|
23
|
+
from haywire.ui.components.popup import Popup
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EdgeInfoPopup:
|
|
27
|
+
"""Dedicated popup for displaying detailed connection/edge information."""
|
|
28
|
+
|
|
29
|
+
def __init__(self):
|
|
30
|
+
self._info_popup: Optional[Popup] = None
|
|
31
|
+
|
|
32
|
+
def show(self, x: float, y: float, edge_id: str, edge: Edge, state: EdgeWrapperState):
|
|
33
|
+
"""Show detailed connection information in a dedicated popup."""
|
|
34
|
+
# Close any existing popup first
|
|
35
|
+
self.close()
|
|
36
|
+
|
|
37
|
+
# Create a larger popup for detailed information
|
|
38
|
+
popup = Popup.create_context_menu(
|
|
39
|
+
"Connection Details", x + 10, y + 10, width="400px", clamp_to_viewport=False
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
with popup:
|
|
43
|
+
with ui.column().classes("w-full gap-2 p-2"):
|
|
44
|
+
# Header with connection status
|
|
45
|
+
with ui.row().classes("w-full"):
|
|
46
|
+
ui.label(f"{edge.edge_type}").classes("text-xs hw-text-muted ml-2 mt-2")
|
|
47
|
+
is_valid = state.is_valid()
|
|
48
|
+
status_icon = "✓" if is_valid else "✗"
|
|
49
|
+
status_color = "hw-text-success" if is_valid else "hw-text-danger"
|
|
50
|
+
status_text = "Valid" if is_valid else "Invalid"
|
|
51
|
+
ui.label(f"{status_icon} {status_text}").classes(f"text-sm font-bold {status_color}")
|
|
52
|
+
|
|
53
|
+
ui.separator().classes("my-2")
|
|
54
|
+
|
|
55
|
+
# Error Section (if present, expandable, default open)
|
|
56
|
+
error = state.get_error()
|
|
57
|
+
if error and isinstance(error, HaywireException):
|
|
58
|
+
with hui.expansion_section("Error Details"):
|
|
59
|
+
with (
|
|
60
|
+
ui.card()
|
|
61
|
+
.classes("w-full p-3")
|
|
62
|
+
.style("background: var(--hw-danger-bg); border: 1px solid var(--hw-danger);")
|
|
63
|
+
):
|
|
64
|
+
ui.label(f"Category: {error.category}").classes("text-xs hw-text-danger ml-2")
|
|
65
|
+
# Render the error detail with button to show full details
|
|
66
|
+
error_render_detail(error)
|
|
67
|
+
|
|
68
|
+
# Warning Section (if present, expandable, default open)
|
|
69
|
+
if state.has_warning():
|
|
70
|
+
with hui.expansion_section("Warning"):
|
|
71
|
+
with (
|
|
72
|
+
ui.card()
|
|
73
|
+
.classes("w-full p-3")
|
|
74
|
+
.style("background: var(--hw-bg-surface); border: 1px solid var(--hw-border);")
|
|
75
|
+
):
|
|
76
|
+
with ui.column():
|
|
77
|
+
for warning in state.warnings:
|
|
78
|
+
ui.label(f"⚠ {warning}").classes("text-xs hw-text-warning ml-2")
|
|
79
|
+
|
|
80
|
+
# Adapter Chain Section (if available, expandable, default closed)
|
|
81
|
+
if edge.chain_adapter_keys:
|
|
82
|
+
with hui.expansion_section("Adapter Chain", default_open=False):
|
|
83
|
+
with (
|
|
84
|
+
ui.card()
|
|
85
|
+
.classes("w-full p-3")
|
|
86
|
+
.style("background: var(--hw-bg-surface); border: 1px solid var(--hw-border);")
|
|
87
|
+
):
|
|
88
|
+
# Display each adapter in the chain
|
|
89
|
+
for i, adapter_key in enumerate(edge.chain_adapter_keys, 1):
|
|
90
|
+
ui.label(f"{i}. {adapter_key}").classes("text-xs hw-text-accent ml-2")
|
|
91
|
+
|
|
92
|
+
if state.execution_count > 0:
|
|
93
|
+
# Execution Statistics Section (expandable, default closed)
|
|
94
|
+
with hui.expansion_section("Execution Statistics", default_open=False):
|
|
95
|
+
exec_count = state.execution_count
|
|
96
|
+
ui.label(f"Execution Count: {exec_count}").classes("text-xs hw-text-muted ml-2")
|
|
97
|
+
|
|
98
|
+
avg_time = state.average_execution_time_us
|
|
99
|
+
if avg_time > 0:
|
|
100
|
+
ui.label(f"Average Time: {avg_time:.1f} μs").classes(
|
|
101
|
+
"text-xs hw-text-muted ml-2"
|
|
102
|
+
)
|
|
103
|
+
else:
|
|
104
|
+
ui.label("Average Time: Not measured").classes("text-xs hw-text-dim ml-2")
|
|
105
|
+
|
|
106
|
+
ui.label(f"Tested value: {state.example_test_value}").classes(
|
|
107
|
+
"text-xs hw-text-muted ml-2"
|
|
108
|
+
)
|
|
109
|
+
ui.label(f"Tested result: {state.example_test_result}").classes(
|
|
110
|
+
"text-xs hw-text-muted ml-2"
|
|
111
|
+
)
|
|
112
|
+
|
|
113
|
+
# Connection Path Section (Expandable, default closed)
|
|
114
|
+
with hui.expansion_section("Connection Path", default_open=False):
|
|
115
|
+
ui.label("Connection Path").classes("font-semibold text-sm")
|
|
116
|
+
ui.label(f"{edge.source_node_id} [{edge.outlet_port_id}]").classes("text-xs opacity-70")
|
|
117
|
+
ui.label("↓").classes("text-xs opacity-50 ml-2")
|
|
118
|
+
ui.label(f"{edge.sink_node_id} [{edge.inlet_port_id}]").classes("text-xs opacity-70")
|
|
119
|
+
|
|
120
|
+
# Close button
|
|
121
|
+
ui.separator().classes("my-2")
|
|
122
|
+
btn_close = ui.button("Close", on_click=lambda: self.close())
|
|
123
|
+
btn_close.props("flat")
|
|
124
|
+
btn_close.classes("w-full text-sm py-2")
|
|
125
|
+
|
|
126
|
+
popup.open()
|
|
127
|
+
self._info_popup = popup
|
|
128
|
+
|
|
129
|
+
def close(self):
|
|
130
|
+
"""Close the connection info popup."""
|
|
131
|
+
if self._info_popup:
|
|
132
|
+
self._info_popup.close()
|
|
133
|
+
self._info_popup.delete()
|
|
134
|
+
self._info_popup = None
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Event handler registration system for the enhanced graph canvas event system.
|
|
3
|
+
|
|
4
|
+
This module provides:
|
|
5
|
+
- Decorator for registering event handlers
|
|
6
|
+
- Type-safe handler registration
|
|
7
|
+
- Automatic handler discovery
|
|
8
|
+
- build_event_handler_map: scan multiple handler sources into a dispatch map
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from typing import Callable, Dict, List, Type
|
|
12
|
+
from haywire.ui.components.graph.event_definitions import BaseGraphEvent
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def handles_event(*event_classes: Type[BaseGraphEvent]):
|
|
16
|
+
"""Decorator to register methods as handlers for specific event classes"""
|
|
17
|
+
|
|
18
|
+
def decorator(func: Callable):
|
|
19
|
+
setattr(func, "_handles_event_classes", event_classes)
|
|
20
|
+
return func
|
|
21
|
+
|
|
22
|
+
return decorator
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def build_event_handler_map(sources: List[object]) -> Dict[str, Callable]:
|
|
26
|
+
"""
|
|
27
|
+
Build an event-type → handler mapping by scanning a list of handler sources.
|
|
28
|
+
|
|
29
|
+
For each source object, scans all methods for the ``_handles_event_classes``
|
|
30
|
+
attribute set by the ``@handles_event`` decorator. When two sources register
|
|
31
|
+
the same event type the later source wins.
|
|
32
|
+
|
|
33
|
+
Args:
|
|
34
|
+
sources: Objects whose methods should be scanned for ``@handles_event``.
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
Dict mapping event_type strings to bound handler methods.
|
|
38
|
+
"""
|
|
39
|
+
handlers: Dict[str, Callable] = {}
|
|
40
|
+
for source in sources:
|
|
41
|
+
for method_name in dir(source):
|
|
42
|
+
method = getattr(source, method_name)
|
|
43
|
+
if hasattr(method, "_handles_event_classes"):
|
|
44
|
+
for event_class in method._handles_event_classes:
|
|
45
|
+
handlers[event_class.event_type] = method
|
|
46
|
+
return handlers
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
"""
|
|
2
|
+
GraphCanvasManager — facade that wires handler objects to the Vue canvas.
|
|
3
|
+
|
|
4
|
+
Public API is unchanged; all event-handling and visual-management logic has
|
|
5
|
+
been moved into focused handler classes under handlers/.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import logging
|
|
9
|
+
import traceback
|
|
10
|
+
from typing import Callable, Dict, Set
|
|
11
|
+
from nicegui import ui
|
|
12
|
+
|
|
13
|
+
from haywire.core.graph.editor import Editor
|
|
14
|
+
|
|
15
|
+
from haywire.ui.components.zoom.pan import ZoomPanContainer
|
|
16
|
+
from haywire.ui.components.graph.canvas import GraphCanvasVue
|
|
17
|
+
from haywire.ui.components.graph.event_definitions import BaseGraphEvent, GRAPH_EVENT_REGISTRY
|
|
18
|
+
from haywire.core.session.session import Session
|
|
19
|
+
|
|
20
|
+
from ...state.edit_state import EditState
|
|
21
|
+
from .event_handlers import build_event_handler_map
|
|
22
|
+
from .handlers.interaction import InteractionHandlers
|
|
23
|
+
from .handlers.selection import SelectionHandlers
|
|
24
|
+
from .handlers.visual_layer import VisualLayerHandlers
|
|
25
|
+
from .handlers.context_menu import ContextMenuHandlers, SessionContextMenuProvider
|
|
26
|
+
|
|
27
|
+
logger = logging.getLogger(__name__)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
class GraphCanvasManager:
|
|
31
|
+
"""
|
|
32
|
+
Facade that wires canvas event handlers and the Vue component together.
|
|
33
|
+
|
|
34
|
+
Responsibilities kept here:
|
|
35
|
+
- Canvas/zoom/menu construction (_setup_canvas)
|
|
36
|
+
- Building and connecting handler objects
|
|
37
|
+
- Event dispatch (build_event_handler_map + _handle_canvas_event)
|
|
38
|
+
- Public facade methods (sync_with_graph, cleanup, …)
|
|
39
|
+
|
|
40
|
+
All domain logic lives in the handler sub-objects:
|
|
41
|
+
- visual_layer → VisualLayerHandlers
|
|
42
|
+
- selection → SelectionHandlers
|
|
43
|
+
- interactions → InteractionHandlers
|
|
44
|
+
- context_menus → ContextMenuHandlers
|
|
45
|
+
"""
|
|
46
|
+
|
|
47
|
+
def __init__(self, editor: Editor, skin_factory, node_factory, panel_registry, session: "Session"):
|
|
48
|
+
self.editor = editor
|
|
49
|
+
self.skin_factory = skin_factory
|
|
50
|
+
self.node_factory = node_factory
|
|
51
|
+
self._panel_registry = panel_registry
|
|
52
|
+
self._session = session
|
|
53
|
+
self.session_id = session.session_id[:8]
|
|
54
|
+
|
|
55
|
+
self.graph = editor.graph
|
|
56
|
+
|
|
57
|
+
# Vue component references built in _setup_canvas
|
|
58
|
+
self.zoom_container, self.canvas_vue = self._setup_canvas()
|
|
59
|
+
|
|
60
|
+
# Build handler objects
|
|
61
|
+
self.visual_layer = VisualLayerHandlers(
|
|
62
|
+
graph=self.graph,
|
|
63
|
+
editor=self.editor,
|
|
64
|
+
skin_factory=self.skin_factory,
|
|
65
|
+
canvas_vue=self.canvas_vue,
|
|
66
|
+
context=self._session.context,
|
|
67
|
+
)
|
|
68
|
+
self.selection = SelectionHandlers(
|
|
69
|
+
graph=self.graph,
|
|
70
|
+
editor=self.editor,
|
|
71
|
+
session_id=self.session_id,
|
|
72
|
+
session=self._session,
|
|
73
|
+
)
|
|
74
|
+
self.interactions = InteractionHandlers(editor=self.editor)
|
|
75
|
+
|
|
76
|
+
context_menu_provider = SessionContextMenuProvider(
|
|
77
|
+
context=self._session.context,
|
|
78
|
+
session=self._session,
|
|
79
|
+
panel_registry=self._panel_registry,
|
|
80
|
+
on_emit_event=self._handle_canvas_event,
|
|
81
|
+
on_emit_sync_event=self.canvas_vue.emit_sync_event,
|
|
82
|
+
)
|
|
83
|
+
|
|
84
|
+
self.context_menu_handlers = ContextMenuHandlers(
|
|
85
|
+
visual_layer=self.visual_layer,
|
|
86
|
+
provider=context_menu_provider,
|
|
87
|
+
)
|
|
88
|
+
|
|
89
|
+
# Build dispatch map from all handler sources
|
|
90
|
+
self._event_handlers: Dict[str, Callable] = build_event_handler_map(
|
|
91
|
+
[
|
|
92
|
+
self.visual_layer,
|
|
93
|
+
self.selection,
|
|
94
|
+
self.interactions,
|
|
95
|
+
self.context_menu_handlers,
|
|
96
|
+
]
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
self._validate_handler_coverage()
|
|
100
|
+
|
|
101
|
+
# Subscribe for incremental validation updates
|
|
102
|
+
self.graph.subscribe_to_validation(self.visual_layer.on_validated)
|
|
103
|
+
|
|
104
|
+
logger.info(f"🔧 GraphCanvasManager for {self.session_id} is setup")
|
|
105
|
+
|
|
106
|
+
# =========================================================================
|
|
107
|
+
# Setup
|
|
108
|
+
# =========================================================================
|
|
109
|
+
|
|
110
|
+
def _setup_canvas(self) -> tuple[ZoomPanContainer, GraphCanvasVue]:
|
|
111
|
+
"""Create zoom container and Vue canvas component."""
|
|
112
|
+
zoom_container = (
|
|
113
|
+
ZoomPanContainer()
|
|
114
|
+
.classes("w-full flex-grow")
|
|
115
|
+
.style("border: 2px solid var(--hw-border);")
|
|
116
|
+
.style("height: 100%;")
|
|
117
|
+
)
|
|
118
|
+
|
|
119
|
+
with zoom_container.content_container:
|
|
120
|
+
canvas_vue = GraphCanvasVue(
|
|
121
|
+
zoom_container=zoom_container,
|
|
122
|
+
on_canvas_event=self._handle_canvas_event,
|
|
123
|
+
canvas_width=self.graph.canvas_width,
|
|
124
|
+
canvas_height=self.graph.canvas_height,
|
|
125
|
+
)
|
|
126
|
+
return zoom_container, canvas_vue
|
|
127
|
+
|
|
128
|
+
def _validate_handler_coverage(self):
|
|
129
|
+
"""Warn if any user event lacks a registered handler."""
|
|
130
|
+
user_events = [
|
|
131
|
+
event_type
|
|
132
|
+
for event_type, event_class in GRAPH_EVENT_REGISTRY.items()
|
|
133
|
+
if getattr(event_class, "category", "user") == "user"
|
|
134
|
+
]
|
|
135
|
+
missing = [et for et in user_events if et not in self._event_handlers]
|
|
136
|
+
if missing:
|
|
137
|
+
logger.warning(f"⚠️ Missing handlers for events: {missing}")
|
|
138
|
+
else:
|
|
139
|
+
logger.debug(f"✅ All {len(user_events)} user events have registered handlers")
|
|
140
|
+
|
|
141
|
+
# =========================================================================
|
|
142
|
+
# Event routing
|
|
143
|
+
# =========================================================================
|
|
144
|
+
|
|
145
|
+
def _handle_canvas_event(self, event: BaseGraphEvent):
|
|
146
|
+
"""Dispatch incoming canvas events to the appropriate handler."""
|
|
147
|
+
event_type = event.event_type
|
|
148
|
+
handler = self._event_handlers.get(event_type)
|
|
149
|
+
|
|
150
|
+
if handler:
|
|
151
|
+
logger.debug(f"🔧 Calling handler for {event_type}: {handler.__name__}")
|
|
152
|
+
try:
|
|
153
|
+
handler(event)
|
|
154
|
+
except Exception as e:
|
|
155
|
+
logger.error(f"❌ Error calling handler for {event_type}: {e}")
|
|
156
|
+
ui.notify(f"Error while processing {event.description}: {e}", type="negative")
|
|
157
|
+
traceback.print_exc()
|
|
158
|
+
else:
|
|
159
|
+
logger.warning(f"No handler found for event type: {event_type}")
|
|
160
|
+
|
|
161
|
+
# =========================================================================
|
|
162
|
+
# Public facade — unchanged external API
|
|
163
|
+
# =========================================================================
|
|
164
|
+
|
|
165
|
+
def sync_with_graph(self):
|
|
166
|
+
"""Synchronise visual representation with the current graph state."""
|
|
167
|
+
self.visual_layer.sync_with_graph()
|
|
168
|
+
|
|
169
|
+
def sync_selections(self):
|
|
170
|
+
"""Emit consolidated selection sync event to Vue."""
|
|
171
|
+
self.visual_layer.sync_selections(
|
|
172
|
+
self.selection.selected_nodes,
|
|
173
|
+
self.selection.selected_edges,
|
|
174
|
+
)
|
|
175
|
+
|
|
176
|
+
def clear_all_visuals(self):
|
|
177
|
+
"""Clear all visual representations."""
|
|
178
|
+
self.visual_layer.clear_all_visuals()
|
|
179
|
+
self.selection.selected_nodes.clear()
|
|
180
|
+
self.selection.selected_edges.clear()
|
|
181
|
+
|
|
182
|
+
# Kept for callers that still reference these directly
|
|
183
|
+
@property
|
|
184
|
+
def node_panels(self) -> Dict:
|
|
185
|
+
return self.visual_layer.node_panels
|
|
186
|
+
|
|
187
|
+
@property
|
|
188
|
+
def edge_paths(self) -> Dict:
|
|
189
|
+
return self.visual_layer.edge_paths
|
|
190
|
+
|
|
191
|
+
@property
|
|
192
|
+
def selected_nodes(self) -> Set[str]:
|
|
193
|
+
return self.selection.selected_nodes
|
|
194
|
+
|
|
195
|
+
@property
|
|
196
|
+
def selected_edges(self) -> Set[str]:
|
|
197
|
+
return self.selection.selected_edges
|
|
198
|
+
|
|
199
|
+
def _has_clipboard_content(self) -> bool:
|
|
200
|
+
clipboard = self._session.context.data[EditState].clipboard
|
|
201
|
+
return clipboard is not None and len(clipboard.nodes) > 0
|
|
202
|
+
|
|
203
|
+
def cleanup(self):
|
|
204
|
+
"""Unsubscribe from graph validation and release resources."""
|
|
205
|
+
logger.info(f"🔧 Shutting down GraphCanvasManager for {self.session_id} ...")
|
|
206
|
+
|
|
207
|
+
try:
|
|
208
|
+
self.graph.unsubscribe_from_validation(self.visual_layer.on_validated)
|
|
209
|
+
except Exception as exc:
|
|
210
|
+
logger.warning(f"GraphCanvasManager: unsubscribe error: {exc}")
|
|
211
|
+
|
|
212
|
+
if self.canvas_vue:
|
|
213
|
+
self.canvas_vue.cleanup()
|
|
214
|
+
|
|
215
|
+
self.visual_layer.cleanup()
|
|
216
|
+
|
|
217
|
+
logger.info(f"🔧 GraphCanvasManager for {self.session_id} is shut down")
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Canvas event handler objects for GraphCanvasManager.
|
|
3
|
+
|
|
4
|
+
Each module provides a focused handler class that owns the state and
|
|
5
|
+
dependencies relevant to one concern:
|
|
6
|
+
|
|
7
|
+
- interaction.py — drag events, edge click
|
|
8
|
+
- selection.py — selection state, clipboard (copy/paste)
|
|
9
|
+
- visual_layer.py — node/edge visual registry, graph sync
|
|
10
|
+
- context_menu.py — context menu routing via IContextMenuProvider
|
|
11
|
+
"""
|