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.
Files changed (37) hide show
  1. haybale_graph_editor/__init__.py +59 -0
  2. haybale_graph_editor/editors/__init__.py +0 -0
  3. haybale_graph_editor/editors/graph_canvas/__init__.py +16 -0
  4. haybale_graph_editor/editors/graph_canvas/connection_info_popup.py +134 -0
  5. haybale_graph_editor/editors/graph_canvas/event_handlers.py +46 -0
  6. haybale_graph_editor/editors/graph_canvas/graph_canvas_manager.py +217 -0
  7. haybale_graph_editor/editors/graph_canvas/handlers/__init__.py +11 -0
  8. haybale_graph_editor/editors/graph_canvas/handlers/context_menu.py +543 -0
  9. haybale_graph_editor/editors/graph_canvas/handlers/context_menu_actions.py +58 -0
  10. haybale_graph_editor/editors/graph_canvas/handlers/interaction.py +56 -0
  11. haybale_graph_editor/editors/graph_canvas/handlers/selection.py +147 -0
  12. haybale_graph_editor/editors/graph_canvas/handlers/visual_layer.py +465 -0
  13. haybale_graph_editor/editors/graph_canvas/node_menu_builder.py +389 -0
  14. haybale_graph_editor/editors/graph_canvas/ui_edge.py +205 -0
  15. haybale_graph_editor/editors/graph_canvas/ui_node.py +300 -0
  16. haybale_graph_editor/editors/graph_editor.py +515 -0
  17. haybale_graph_editor/editors/node_status.py +66 -0
  18. haybale_graph_editor/focuses.py +70 -0
  19. haybale_graph_editor/panels/context_menu/create_node_panel.py +99 -0
  20. haybale_graph_editor/panels/context_menu/edge_actions.py +54 -0
  21. haybale_graph_editor/panels/context_menu/node_actions.py +171 -0
  22. haybale_graph_editor/panels/context_menu/node_errors.py +87 -0
  23. haybale_graph_editor/panels/context_menu/port_info.py +53 -0
  24. haybale_graph_editor/panels/context_menu/selection_actions.py +80 -0
  25. haybale_graph_editor/panels/edge_panels.py +283 -0
  26. haybale_graph_editor/panels/graph_info_panel.py +52 -0
  27. haybale_graph_editor/panels/node_ports_panel.py +80 -0
  28. haybale_graph_editor/panels/node_props_panel.py +83 -0
  29. haybale_graph_editor/panels/node_settings.py +71 -0
  30. haybale_graph_editor/protocols.py +54 -0
  31. haybale_graph_editor/state/__init__.py +0 -0
  32. haybale_graph_editor/state/edit_state.py +57 -0
  33. haybale_graph_editor/state/graph_app_state.py +69 -0
  34. haybale_graph_editor-0.0.2.dist-info/METADATA +8 -0
  35. haybale_graph_editor-0.0.2.dist-info/RECORD +37 -0
  36. haybale_graph_editor-0.0.2.dist-info/WHEEL +4 -0
  37. 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
+ """