Coverage for openhcs/textual_tui/services/visual_programming_dialog_service.py: 0.0%
58 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +0000
« prev ^ index » next coverage.py v7.11.0, created at 2025-11-04 02:09 +0000
1"""
2Visual Programming Dialog Service for OpenHCS TUI.
4Handles creation and management of visual programming dialogs using DualEditorPane.
5Separates dialog concerns from pipeline management.
6"""
7import asyncio
8import logging
9from typing import Any, List, Optional
11from prompt_toolkit.widgets import Dialog
13from openhcs.core.pipeline import Pipeline
14from openhcs.core.steps.function_step import FunctionStep
15# DualEditorPane injected via constructor to break circular dependency
16# Global error handling will catch all exceptions automatically
18logger = logging.getLogger(__name__)
21class VisualProgrammingDialogService:
22 """
23 Service for managing visual programming dialogs.
25 Handles dialog creation, lifecycle, and integration with DualEditorPane.
26 Keeps dialog concerns separate from pipeline management.
27 Uses dependency injection to avoid circular imports.
28 """
30 def __init__(self, state: Any, context: Any, dual_editor_pane_class: type):
31 """
32 Initialize the visual programming dialog service.
34 Args:
35 state: TUI state object
36 context: Processing context
37 dual_editor_pane_class: DualEditorPane class injected to break circular dependency
38 """
39 self.state = state
40 self.context = context
41 self.dual_editor_pane_class = dual_editor_pane_class
43 # Dialog state management
44 self.current_dialog = None
45 self.current_dialog_future = None
47 async def show_add_step_dialog(self, target_pipelines: List[Pipeline]) -> Optional[FunctionStep]:
48 """
49 Show visual programming dialog for adding a new step.
51 Args:
52 target_pipelines: List of pipelines to add the step to
54 Returns:
55 Created FunctionStep if successful, None if cancelled
56 """
57 # Create empty FunctionStep for new step creation - BACKEND API COMPLIANT
58 empty_step = FunctionStep(
59 func=None, # Required positional parameter
60 name="New Step",
61 # Let variable_components use FunctionStep's default [VariableComponents.SITE]
62 group_by=""
63 )
65 # Create DualEditorPane for visual programming with dialog callbacks
66 dual_editor = self.dual_editor_pane_class(
67 state=self.state,
68 func_step=empty_step,
69 on_save=lambda step: self._handle_save_and_close(step),
70 on_cancel=lambda: self._handle_cancel_and_close()
71 )
73 # Create and show dialog
74 result = await self._show_dialog(
75 title="Visual Programming - Add Step",
76 dual_editor=dual_editor,
77 ok_handler=lambda: self._handle_add_step_ok(dual_editor)
78 )
80 return result
82 async def show_edit_step_dialog(self, target_step: FunctionStep) -> Optional[FunctionStep]:
83 """
84 Show visual programming dialog for editing an existing step.
86 Args:
87 target_step: The step to edit
89 Returns:
90 Edited FunctionStep if successful, None if cancelled
91 """
92 # Create DualEditorPane for visual programming with existing step and dialog callbacks
93 dual_editor = self.dual_editor_pane_class(
94 state=self.state,
95 func_step=target_step,
96 on_save=lambda step: self._handle_save_and_close(step),
97 on_cancel=lambda: self._handle_cancel_and_close()
98 )
100 # Create and show dialog
101 result = await self._show_dialog(
102 title="Visual Programming - Edit Step",
103 dual_editor=dual_editor,
104 ok_handler=lambda: self._handle_edit_step_ok(dual_editor)
105 )
107 return result
109 async def _show_dialog(self, title: str, dual_editor: Any, ok_handler) -> Optional[FunctionStep]:
110 """
111 Show a visual programming dialog with the given DualEditorPane.
113 Args:
114 title: Dialog title
115 dual_editor: The DualEditorPane instance
116 ok_handler: Handler for OK button
118 Returns:
119 Result from OK handler or None if cancelled
120 """
121 # Create dialog result future
122 self.current_dialog_future = asyncio.Future()
124 # Create dialog WITHOUT buttons - DualEditorPane handles its own save/cancel
125 # Set minimum width to accommodate Function Pattern Editor button row
126 from prompt_toolkit.layout.dimension import Dimension
128 # Calculate minimum width for Function Pattern Editor buttons:
129 # "Function Pattern Editor" (title) + "Add Function" + "Load .func" + "Save .func As" + "Edit in Vim"
130 # ≈ 23 + 15 + 12 + 15 + 12 + padding ≈ 85 characters
131 min_dialog_width = 85
133 dialog = Dialog(
134 title=title,
135 body=dual_editor.container,
136 buttons=[], # No buttons - DualEditorPane has its own Save/Cancel
137 width=Dimension(min=min_dialog_width), # Ensure minimum width for button row
138 modal=True
139 )
141 # Store dialog reference
142 self.current_dialog = dialog
144 # Show dialog (async)
145 await self.state.show_dialog(dialog, result_future=self.current_dialog_future)
147 # Wait for dialog completion
148 result = await self.current_dialog_future
150 # Cleanup
151 self.current_dialog = None
152 self.current_dialog_future = None
154 return result
156 def _handle_ok(self, ok_handler):
157 """Handle OK button click."""
158 try:
159 result = ok_handler()
160 if self.current_dialog_future and not self.current_dialog_future.done():
161 self.current_dialog_future.set_result(result)
162 except Exception as e:
163 logger.error(f"Error in OK handler: {e}")
164 if self.current_dialog_future and not self.current_dialog_future.done():
165 self.current_dialog_future.set_result(None)
167 def _handle_cancel(self):
168 """Handle Cancel button click."""
169 if self.current_dialog_future and not self.current_dialog_future.done():
170 self.current_dialog_future.set_result(None)
172 def _handle_save_and_close(self, step: FunctionStep):
173 """Handle save from DualEditorPane and close dialog."""
174 if self.current_dialog_future and not self.current_dialog_future.done():
175 self.current_dialog_future.set_result(step)
177 def _handle_cancel_and_close(self):
178 """Handle cancel from DualEditorPane and close dialog."""
179 if self.current_dialog_future and not self.current_dialog_future.done():
180 self.current_dialog_future.set_result(None)
182 def _handle_add_step_ok(self, dual_editor: Any) -> Optional[FunctionStep]:
183 """Handle OK button for add step dialog."""
184 created_step = dual_editor.get_created_step()
185 return created_step
187 def _handle_edit_step_ok(self, dual_editor: Any) -> Optional[FunctionStep]:
188 """Handle OK button for edit step dialog."""
189 edited_step = dual_editor.get_created_step()
190 return edited_step