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

1""" 

2Step Parameter Editor Widget for PyQt6 GUI. 

3 

4Mirrors the Textual TUI StepParameterEditorWidget with type hint-based form generation. 

5Handles FunctionStep parameter editing with nested dataclass support. 

6""" 

7 

8import logging 

9from typing import Any, Optional 

10from pathlib import Path 

11 

12from PyQt6.QtWidgets import ( 

13 QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, 

14 QScrollArea, QFrame 

15) 

16from PyQt6.QtCore import Qt, pyqtSignal 

17 

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 

23 

24logger = logging.getLogger(__name__) 

25 

26 

27class StepParameterEditorWidget(QScrollArea): 

28 """ 

29 Step parameter editor using dynamic form generation. 

30  

31 Mirrors Textual TUI implementation - builds forms based on FunctionStep  

32 constructor signature with nested dataclass support. 

33 """ 

34 

35 # Signals 

36 step_parameter_changed = pyqtSignal() 

37 

38 def __init__(self, step: FunctionStep, service_adapter=None, color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

39 super().__init__(parent) 

40 

41 # Initialize color scheme 

42 self.color_scheme = color_scheme or PyQt6ColorScheme() 

43 

44 self.step = step 

45 self.service_adapter = service_adapter 

46 

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__) 

51 

52 # Get current parameter values from step instance 

53 parameters = {} 

54 parameter_types = {} 

55 param_defaults = {} 

56 

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 

63 

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 

73 

74 self.setup_ui() 

75 self.setup_connections() 

76 

77 logger.debug(f"Step parameter editor initialized for step: {getattr(step, 'name', 'Unknown')}") 

78 

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) 

84 

85 # Main content widget 

86 content_widget = QWidget() 

87 layout = QVBoxLayout(content_widget) 

88 layout.setContentsMargins(10, 10, 10, 10) 

89 layout.setSpacing(15) 

90 

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) 

95 

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 """) 

107 

108 form_layout = QVBoxLayout(form_frame) 

109 

110 # Add parameter form manager 

111 form_layout.addWidget(self.form_manager) 

112 

113 layout.addWidget(form_frame) 

114 

115 # Action buttons (mirrors Textual TUI) 

116 button_layout = QHBoxLayout() 

117 

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) 

123 

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) 

129 

130 button_layout.addStretch() 

131 layout.addLayout(button_layout) 

132 

133 layout.addStretch() 

134 

135 self.setWidget(content_widget) 

136 

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 """ 

155 

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) 

160 

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) 

167 

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() 

172 

173 except Exception as e: 

174 logger.error(f"Error updating step parameter {param_name}: {e}") 

175 

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 

181 

182 from openhcs.core.path_cache import PathCacheKey 

183 

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 ) 

190 

191 if file_path: 

192 self._load_step_settings_from_file(file_path) 

193 

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 

199 

200 from openhcs.core.path_cache import PathCacheKey 

201 

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 ) 

208 

209 if file_path: 

210 self._save_step_settings_to_file(file_path) 

211 

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") 

217 

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}") 

222 

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") 

228 

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}") 

233 

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) 

239 

240 # Update form manager 

241 self.form_manager.update_parameter(param_name, default_value) 

242 

243 self.step_parameter_changed.emit() 

244 logger.debug(f"Reset parameter {param_name} to default: {default_value}") 

245 

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) 

251 

252 self.step_parameter_changed.emit() 

253 logger.debug("Reset all step parameters to defaults") 

254 

255 def get_current_step(self) -> FunctionStep: 

256 """Get the current step with all parameter values.""" 

257 return self.step 

258 

259 def update_step(self, step: FunctionStep): 

260 """Update the step and refresh the form.""" 

261 self.step = step 

262 

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) 

267 

268 logger.debug(f"Updated step parameter editor for step: {getattr(step, 'name', 'Unknown')}")