Coverage for openhcs/pyqt_gui/widgets/step_parameter_editor.py: 0.0%
129 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"""
2Step Parameter Editor Widget for PyQt6 GUI.
4Mirrors the Textual TUI StepParameterEditorWidget with type hint-based form generation.
5Handles FunctionStep parameter editing with nested dataclass support.
6"""
8import logging
9from typing import Any, Optional
10from pathlib import Path
12from PyQt6.QtWidgets import (
13 QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel,
14 QScrollArea, QFrame
15)
16from PyQt6.QtCore import Qt, pyqtSignal
18from openhcs.core.steps.function_step import FunctionStep
19from openhcs.textual_tui.widgets.shared.signature_analyzer import SignatureAnalyzer
20from openhcs.pyqt_gui.widgets.shared.parameter_form_manager import ParameterFormManager
21from openhcs.pyqt_gui.shared.color_scheme import PyQt6ColorScheme
22from openhcs.pyqt_gui.shared.color_scheme import PyQt6ColorScheme
24logger = logging.getLogger(__name__)
27class StepParameterEditorWidget(QScrollArea):
28 """
29 Step parameter editor using dynamic form generation.
31 Mirrors Textual TUI implementation - builds forms based on FunctionStep
32 constructor signature with nested dataclass support.
33 """
35 # Signals
36 step_parameter_changed = pyqtSignal()
38 def __init__(self, step: FunctionStep, service_adapter=None, color_scheme: Optional[PyQt6ColorScheme] = None, parent=None):
39 super().__init__(parent)
41 # Initialize color scheme
42 self.color_scheme = color_scheme or PyQt6ColorScheme()
44 self.step = step
45 self.service_adapter = service_adapter
47 # Analyze AbstractStep signature to get all inherited parameters (mirrors Textual TUI)
48 from openhcs.core.steps.abstract import AbstractStep
49 # Auto-detection correctly identifies constructors and includes all parameters
50 param_info = SignatureAnalyzer.analyze(AbstractStep.__init__)
52 # Get current parameter values from step instance
53 parameters = {}
54 parameter_types = {}
55 param_defaults = {}
57 for name, info in param_info.items():
58 # All AbstractStep parameters are relevant for editing
59 current_value = getattr(self.step, name, info.default_value)
60 parameters[name] = current_value
61 parameter_types[name] = info.param_type
62 param_defaults[name] = info.default_value
64 # Create parameter form manager (reuses Textual TUI logic)
65 from openhcs.core.config import GlobalPipelineConfig
66 self.form_manager = ParameterFormManager(
67 parameters, parameter_types, "step", param_info,
68 color_scheme=self.color_scheme,
69 global_config_type=GlobalPipelineConfig,
70 placeholder_prefix="Pipeline default"
71 )
72 self.param_defaults = param_defaults
74 self.setup_ui()
75 self.setup_connections()
77 logger.debug(f"Step parameter editor initialized for step: {getattr(step, 'name', 'Unknown')}")
79 def setup_ui(self):
80 """Setup the user interface."""
81 self.setWidgetResizable(True)
82 self.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
83 self.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded)
85 # Main content widget
86 content_widget = QWidget()
87 layout = QVBoxLayout(content_widget)
88 layout.setContentsMargins(10, 10, 10, 10)
89 layout.setSpacing(15)
91 # Header
92 header_label = QLabel("Step Parameters")
93 header_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)}; font-weight: bold; font-size: 14px;")
94 layout.addWidget(header_label)
96 # Parameter form (using shared form manager)
97 form_frame = QFrame()
98 form_frame.setFrameStyle(QFrame.Shape.Box)
99 form_frame.setStyleSheet(f"""
100 QFrame {{
101 background-color: {self.color_scheme.to_hex(self.color_scheme.panel_bg)};
102 border: 1px solid {self.color_scheme.to_hex(self.color_scheme.border_color)};
103 border-radius: 5px;
104 padding: 10px;
105 }}
106 """)
108 form_layout = QVBoxLayout(form_frame)
110 # Add parameter form manager
111 form_layout.addWidget(self.form_manager)
113 layout.addWidget(form_frame)
115 # Action buttons (mirrors Textual TUI)
116 button_layout = QHBoxLayout()
118 load_btn = QPushButton("Load .step")
119 load_btn.setMaximumWidth(100)
120 load_btn.setStyleSheet(self._get_button_style())
121 load_btn.clicked.connect(self.load_step_settings)
122 button_layout.addWidget(load_btn)
124 save_btn = QPushButton("Save .step As")
125 save_btn.setMaximumWidth(120)
126 save_btn.setStyleSheet(self._get_button_style())
127 save_btn.clicked.connect(self.save_step_settings)
128 button_layout.addWidget(save_btn)
130 button_layout.addStretch()
131 layout.addLayout(button_layout)
133 layout.addStretch()
135 self.setWidget(content_widget)
137 def _get_button_style(self) -> str:
138 """Get consistent button styling."""
139 return """
140 QPushButton {
141 background-color: {self.color_scheme.to_hex(self.color_scheme.input_bg)};
142 color: white;
143 border: 1px solid {self.color_scheme.to_hex(self.color_scheme.border_light)};
144 border-radius: 3px;
145 padding: 6px 12px;
146 font-size: 11px;
147 }
148 QPushButton:hover {
149 background-color: {self.color_scheme.to_hex(self.color_scheme.button_hover_bg)};
150 }
151 QPushButton:pressed {
152 background-color: {self.color_scheme.to_hex(self.color_scheme.button_pressed_bg)};
153 }
154 """
156 def setup_connections(self):
157 """Setup signal/slot connections."""
158 # Connect form manager parameter changes
159 self.form_manager.parameter_changed.connect(self._handle_parameter_change)
161 def _handle_parameter_change(self, param_name: str, value: Any):
162 """Handle parameter change from form manager (mirrors Textual TUI)."""
163 try:
164 # Get the properly converted value from the form manager
165 # The form manager handles all type conversions including List[Enum]
166 final_value = self.form_manager.get_current_values().get(param_name, value)
168 # Update step attribute
169 setattr(self.step, param_name, final_value)
170 logger.debug(f"Updated step parameter {param_name}={final_value}")
171 self.step_parameter_changed.emit()
173 except Exception as e:
174 logger.error(f"Error updating step parameter {param_name}: {e}")
176 def load_step_settings(self):
177 """Load step settings from .step file (mirrors Textual TUI)."""
178 if not self.service_adapter:
179 logger.warning("No service adapter available for file dialog")
180 return
182 from openhcs.core.path_cache import PathCacheKey
184 file_path = self.service_adapter.show_cached_file_dialog(
185 cache_key=PathCacheKey.STEP_SETTINGS,
186 title="Load Step Settings (.step)",
187 file_filter="Step Files (*.step);;All Files (*)",
188 mode="open"
189 )
191 if file_path:
192 self._load_step_settings_from_file(file_path)
194 def save_step_settings(self):
195 """Save step settings to .step file (mirrors Textual TUI)."""
196 if not self.service_adapter:
197 logger.warning("No service adapter available for file dialog")
198 return
200 from openhcs.core.path_cache import PathCacheKey
202 file_path = self.service_adapter.show_cached_file_dialog(
203 cache_key=PathCacheKey.STEP_SETTINGS,
204 title="Save Step Settings (.step)",
205 file_filter="Step Files (*.step);;All Files (*)",
206 mode="save"
207 )
209 if file_path:
210 self._save_step_settings_to_file(file_path)
212 def _load_step_settings_from_file(self, file_path: Path):
213 """Load step settings from file."""
214 try:
215 # TODO: Implement step settings loading
216 logger.debug(f"Load step settings from {file_path} - TODO: implement")
218 except Exception as e:
219 logger.error(f"Failed to load step settings from {file_path}: {e}")
220 if self.service_adapter:
221 self.service_adapter.show_error_dialog(f"Failed to load step settings: {e}")
223 def _save_step_settings_to_file(self, file_path: Path):
224 """Save step settings to file."""
225 try:
226 # TODO: Implement step settings saving
227 logger.debug(f"Save step settings to {file_path} - TODO: implement")
229 except Exception as e:
230 logger.error(f"Failed to save step settings to {file_path}: {e}")
231 if self.service_adapter:
232 self.service_adapter.show_error_dialog(f"Failed to save step settings: {e}")
234 def reset_parameter(self, param_name: str):
235 """Reset parameter to default value."""
236 if param_name in self.param_defaults:
237 default_value = self.param_defaults[param_name]
238 setattr(self.step, param_name, default_value)
240 # Update form manager
241 self.form_manager.update_parameter(param_name, default_value)
243 self.step_parameter_changed.emit()
244 logger.debug(f"Reset parameter {param_name} to default: {default_value}")
246 def reset_all_parameters(self):
247 """Reset all parameters to default values."""
248 for param_name, default_value in self.param_defaults.items():
249 setattr(self.step, param_name, default_value)
250 self.form_manager.update_parameter(param_name, default_value)
252 self.step_parameter_changed.emit()
253 logger.debug("Reset all step parameters to defaults")
255 def get_current_step(self) -> FunctionStep:
256 """Get the current step with all parameter values."""
257 return self.step
259 def update_step(self, step: FunctionStep):
260 """Update the step and refresh the form."""
261 self.step = step
263 # Update form manager with new values
264 for param_name in self.form_manager.parameters.keys():
265 current_value = getattr(self.step, param_name, None)
266 self.form_manager.update_parameter(param_name, current_value)
268 logger.debug(f"Updated step parameter editor for step: {getattr(step, 'name', 'Unknown')}")