Coverage for openhcs/textual_tui/widgets/step_parameter_editor.py: 0.0%
158 statements
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
« prev ^ index » next coverage.py v7.10.3, created at 2025-08-14 05:57 +0000
1"""Step parameter editor widget - port of step_parameter_editor.py to Textual."""
3import logging
4from typing import Any
5from pathlib import Path
6from textual.containers import Container, Horizontal, Vertical, ScrollableContainer
7from textual.widgets import Button, Static
8from textual.app import ComposeResult
9from textual.message import Message
11from openhcs.core.steps.function_step import FunctionStep
12from openhcs.core.steps.abstract import AbstractStep
13from .shared.parameter_form_manager import ParameterFormManager
14from .shared.signature_analyzer import SignatureAnalyzer
16logger = logging.getLogger(__name__)
19class StepParameterEditorWidget(ScrollableContainer):
20 """
21 Step parameter editor using dynamic form generation.
22 Builds forms based on FunctionStep constructor signature.
23 """
25 class StepParameterChanged(Message):
26 """Message to indicate a parameter has changed."""
27 pass
29 def __init__(self, step: FunctionStep):
30 super().__init__()
31 self.step = step
33 # Create parameter form manager using shared components
34 # Analyze AbstractStep to get all inherited parameters including materialization_config
35 # Auto-detection correctly identifies constructors and includes all parameters
36 param_info = SignatureAnalyzer.analyze(AbstractStep.__init__)
38 # Get current parameter values from step instance
39 parameters = {}
40 parameter_types = {}
41 param_defaults = {}
43 for name, info in param_info.items():
44 # All AbstractStep parameters are relevant for editing
45 current_value = getattr(self.step, name, info.default_value)
46 parameters[name] = current_value
47 parameter_types[name] = info.param_type
48 param_defaults[name] = info.default_value
50 # Configure form manager for step editing with pipeline context
51 from openhcs.core.config import GlobalPipelineConfig
52 self.form_manager = ParameterFormManager(
53 parameters, parameter_types, "step", param_info,
54 global_config_type=GlobalPipelineConfig,
55 placeholder_prefix="Pipeline default"
56 )
57 self.param_defaults = param_defaults
59 def compose(self) -> ComposeResult:
60 """Compose the step parameter form dynamically."""
61 yield Static("[bold]Step Parameters[/bold]")
63 # Build form using shared form manager
64 yield from self._build_step_parameter_form()
66 # Action buttons
67 with Horizontal():
68 yield Button("Load .step", id="load_step_btn", compact=True)
69 yield Button("Save .step As", id="save_as_btn", compact=True)
71 def _build_step_parameter_form(self) -> ComposeResult:
72 """Generate form widgets using shared ParameterFormManager."""
73 try:
74 # Use shared form manager to build form
75 yield from self.form_manager.build_form()
76 except Exception as e:
77 yield Static(f"[red]Error building step parameter form: {e}[/red]")
79 def on_input_changed(self, event) -> None:
80 """Handle input changes from shared components."""
81 if event.input.id.startswith("step_"):
82 param_name = event.input.id.split("_", 1)[1]
83 if self.form_manager:
84 self.form_manager.update_parameter(param_name, event.value)
85 final_value = self.form_manager.parameters[param_name]
86 self._handle_parameter_change(param_name, final_value)
88 def on_checkbox_changed(self, event) -> None:
89 """Handle checkbox changes from shared components."""
90 if not event.checkbox.id.startswith("step_") or not self.form_manager:
91 return
93 checkbox_id = event.checkbox.id
94 if checkbox_id.endswith("_enabled"):
95 # Optional dataclass checkbox
96 param_name = checkbox_id.replace("step_", "").replace("_enabled", "")
97 self.form_manager.handle_optional_checkbox_change(param_name, event.value)
98 else:
99 # Regular checkbox
100 param_name = checkbox_id.split("_", 1)[1]
101 self.form_manager.update_parameter(param_name, event.value)
103 final_value = self.form_manager.parameters[param_name]
104 self._handle_parameter_change(param_name, final_value)
106 def on_radio_set_changed(self, event) -> None:
107 """Handle RadioSet changes from shared components."""
108 if event.radio_set.id.startswith("step_"):
109 param_name = event.radio_set.id.split("_", 1)[1]
110 if event.pressed and event.pressed.id:
111 enum_value = event.pressed.id[5:] # Remove "enum_" prefix
112 if self.form_manager:
113 self.form_manager.update_parameter(param_name, enum_value)
114 final_value = self.form_manager.parameters[param_name]
115 self._handle_parameter_change(param_name, final_value)
117 def on_button_pressed(self, event: Button.Pressed) -> None:
118 """Handle button presses."""
119 if event.button.id == "load_step_btn":
120 self.run_worker(self._load_step())
121 elif event.button.id == "save_as_btn":
122 self.run_worker(self._save_step_as())
123 elif event.button.id.startswith("reset_step_"):
124 # Individual parameter reset
125 param_name = event.button.id.split("_", 2)[2]
126 self._reset_parameter(param_name)
128 def _handle_parameter_change(self, param_name: str, value: Any) -> None:
129 """Update step parameter and emit change message."""
130 try:
131 # Convert value to appropriate type
132 if param_name == 'force_disk_output':
133 value = bool(value)
134 elif param_name in ('input_dir', 'output_dir') and value:
135 value = Path(value)
137 # Update step attribute
138 setattr(self.step, param_name, value)
139 logger.debug(f"Updated step parameter {param_name}={value}")
140 self.post_message(self.StepParameterChanged())
141 except Exception as e:
142 logger.error(f"Error updating step parameter {param_name}: {e}")
144 def _reset_parameter(self, param_name: str) -> None:
145 """Reset a specific parameter to its default value."""
146 if not self.form_manager or param_name not in self.param_defaults:
147 return
149 try:
150 # Use form manager to reset parameter
151 default_value = self.param_defaults[param_name]
152 self.form_manager.reset_parameter(param_name, default_value)
154 # Update step instance and notify parent
155 self._handle_parameter_change(param_name, default_value)
157 # Refresh the UI to show the reset value
158 self._refresh_form_widgets()
160 logger.debug(f"Reset step parameter {param_name} to default: {default_value}")
161 except Exception as e:
162 logger.error(f"Error resetting step parameter {param_name}: {e}")
164 def _refresh_form_widgets(self) -> None:
165 """Refresh form widgets to show current parameter values."""
166 try:
167 current_values = self.form_manager.get_current_values()
169 # Update each widget with current value
170 for param_name, value in current_values.items():
171 widget_id = f"step_{param_name}"
172 try:
173 widget = self.query_one(f"#{widget_id}")
175 # Update widget based on type
176 if hasattr(widget, 'value'):
177 # Convert enum to string for display
178 display_value = value.value if hasattr(value, 'value') else value
179 widget.value = display_value
180 elif hasattr(widget, 'pressed'):
181 # RadioSet - find and press the correct radio button
182 if hasattr(value, 'value'):
183 target_id = f"enum_{value.value}"
184 for radio in widget.query("RadioButton"):
185 if radio.id == target_id:
186 radio.value = True
187 break
188 except Exception as widget_error:
189 logger.debug(f"Could not update widget {widget_id}: {widget_error}")
191 except Exception as e:
192 logger.warning(f"Failed to refresh form widgets: {e}")
194 async def _load_step(self) -> None:
195 """Load step configuration from file."""
196 from openhcs.textual_tui.windows import open_file_browser_window, BrowserMode
197 from openhcs.textual_tui.services.file_browser_service import SelectionMode
198 from openhcs.constants.constants import Backend
200 def handle_result(result):
201 if result and isinstance(result, Path):
202 self._load_step_from_file(result)
204 # Launch file browser window for .step files
205 from openhcs.core.path_cache import get_cached_browser_path, PathCacheKey
207 await open_file_browser_window(
208 app=self.app,
209 file_manager=self.app.filemanager,
210 initial_path=get_cached_browser_path(PathCacheKey.STEP_SETTINGS),
211 backend=Backend.DISK,
212 title="Load Step Settings (.step)",
213 mode=BrowserMode.LOAD,
214 selection_mode=SelectionMode.FILES_ONLY,
215 filter_extensions=['.step'],
216 cache_key=PathCacheKey.STEP_SETTINGS,
217 on_result_callback=handle_result,
218 caller_id="step_parameter_editor"
219 )
221 async def _save_step_as(self) -> None:
222 """Save step configuration to file."""
223 from openhcs.textual_tui.windows import open_file_browser_window, BrowserMode
224 from openhcs.textual_tui.services.file_browser_service import SelectionMode
225 from openhcs.constants.constants import Backend
227 def handle_result(result):
228 if result and isinstance(result, Path):
229 self._save_step_to_file(result)
231 # Launch file browser window for saving .step files
232 from openhcs.core.path_cache import get_cached_browser_path, PathCacheKey
234 await open_file_browser_window(
235 app=self.app,
236 file_manager=self.app.filemanager,
237 initial_path=get_cached_browser_path(PathCacheKey.STEP_SETTINGS),
238 backend=Backend.DISK,
239 title="Save Step Settings (.step)",
240 mode=BrowserMode.SAVE,
241 selection_mode=SelectionMode.FILES_ONLY,
242 filter_extensions=['.step'],
243 default_filename="step_settings.step",
244 cache_key=PathCacheKey.STEP_SETTINGS,
245 on_result_callback=handle_result,
246 caller_id="step_parameter_editor"
247 )
249 def _load_step_from_file(self, file_path: Path) -> None:
250 """Load step parameters from .step file."""
251 import dill as pickle
252 try:
253 with open(file_path, 'rb') as f:
254 step_data = pickle.load(f)
256 # Update both the step object and form manager
257 for param_name, value in step_data.items():
258 if param_name in self.form_manager.parameters:
259 # Update form manager first
260 self.form_manager.update_parameter(param_name, value)
261 # Then update step object and emit change message
262 self._handle_parameter_change(param_name, value)
264 # Refresh the UI to show loaded values
265 self._refresh_form_widgets()
267 logger.debug(f"Loaded {len(step_data)} parameters from {file_path.name}")
268 except Exception as e:
269 logger.error(f"Failed to load step: {e}")
271 def _save_step_to_file(self, file_path: Path) -> None:
272 """Save step parameters to .step file."""
273 import pickle
274 try:
275 step_data = self.form_manager.get_current_values()
276 with open(file_path, 'wb') as f:
277 pickle.dump(step_data, f)
278 except Exception as e:
279 logger.error(f"Failed to save step: {e}")