Coverage for openhcs/pyqt_gui/windows/config_window.py: 0.0%

314 statements  

« prev     ^ index     » next       coverage.py v7.10.3, created at 2025-08-14 05:57 +0000

1""" 

2Configuration Window for PyQt6 

3 

4Configuration editing dialog with full feature parity to Textual TUI version. 

5Uses hybrid approach: extracted business logic + clean PyQt6 UI. 

6""" 

7 

8import logging 

9import dataclasses 

10from dataclasses import fields 

11from typing import Type, Any, Callable, Optional, Dict, Protocol, Union 

12from functools import partial 

13from abc import ABC, abstractmethod 

14 

15from PyQt6.QtWidgets import ( 

16 QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, 

17 QScrollArea, QWidget, QFormLayout, QGroupBox, QFrame, 

18 QLineEdit, QSpinBox, QDoubleSpinBox, QCheckBox, QComboBox 

19) 

20from PyQt6.QtCore import Qt, pyqtSignal 

21from PyQt6.QtGui import QFont 

22 

23from openhcs.textual_tui.widgets.shared.signature_analyzer import SignatureAnalyzer 

24from openhcs.pyqt_gui.widgets.shared.parameter_form_manager import ParameterFormManager 

25from openhcs.pyqt_gui.shared.style_generator import StyleSheetGenerator 

26from openhcs.pyqt_gui.shared.color_scheme import PyQt6ColorScheme 

27 

28# Import PyQt6 help components 

29from openhcs.pyqt_gui.widgets.shared.clickable_help_components import GroupBoxWithHelp, LabelWithHelp 

30 

31logger = logging.getLogger(__name__) 

32 

33 

34# ========== FUNCTIONAL ABSTRACTIONS FOR CONFIG RESET ========== 

35 

36class FormManagerProtocol(Protocol): 

37 """Protocol defining the interface for form managers.""" 

38 def update_parameter(self, param_name: str, value: Any) -> None: ... 

39 def get_current_values(self) -> Dict[str, Any]: ... 

40 

41 

42class DataclassIntrospector: 

43 """Pure functional dataclass introspection and analysis.""" 

44 

45 @staticmethod 

46 def is_lazy_dataclass(instance: Any) -> bool: 

47 """Check if an instance is a lazy dataclass.""" 

48 return hasattr(instance, '_resolve_field_value') 

49 

50 @staticmethod 

51 def get_static_defaults(config_class: Type) -> Dict[str, Any]: 

52 """Get static default values from dataclass definition.""" 

53 return { 

54 field.name: field.default if field.default is not dataclasses.MISSING 

55 else field.default_factory() if field.default_factory is not dataclasses.MISSING 

56 else None 

57 for field in fields(config_class) 

58 } 

59 

60 @staticmethod 

61 def get_lazy_reset_values(config_class: Type) -> Dict[str, Any]: 

62 """Get reset values for lazy dataclass (all None for lazy loading).""" 

63 return {field.name: None for field in fields(config_class)} 

64 

65 @staticmethod 

66 def extract_field_values(dataclass_instance: Any) -> Dict[str, Any]: 

67 """Extract field values from a dataclass instance.""" 

68 return { 

69 field.name: getattr(dataclass_instance, field.name) 

70 for field in fields(dataclass_instance) 

71 } 

72 

73 

74class ResetStrategy(ABC): 

75 """Abstract base class for reset strategies.""" 

76 

77 @abstractmethod 

78 def generate_reset_values(self, config_class: Type, current_config: Any) -> Dict[str, Any]: 

79 """Generate the values to reset to.""" 

80 pass 

81 

82 

83class LazyAwareResetStrategy(ResetStrategy): 

84 """Strategy that respects lazy dataclass architecture.""" 

85 

86 def generate_reset_values(self, config_class: Type, current_config: Any) -> Dict[str, Any]: 

87 if DataclassIntrospector.is_lazy_dataclass(current_config): 

88 # For lazy dataclasses, we need to resolve to actual static defaults 

89 # instead of trying to create a new lazy instance with None values 

90 

91 # Get the base class that the lazy dataclass is based on 

92 base_class = self._get_base_class_from_lazy(config_class) 

93 

94 # Create a fresh instance of the base class to get static defaults 

95 static_defaults_instance = base_class() 

96 

97 # Extract the field values from the static defaults 

98 resolved_values = {} 

99 for field in fields(config_class): 

100 resolved_values[field.name] = getattr(static_defaults_instance, field.name) 

101 

102 return resolved_values 

103 else: 

104 # Regular dataclass: reset to static default values 

105 return DataclassIntrospector.get_static_defaults(config_class) 

106 

107 def _get_base_class_from_lazy(self, lazy_class: Type) -> Type: 

108 """Extract the base class from a lazy dataclass.""" 

109 # For PipelineConfig, the base class is GlobalPipelineConfig 

110 # We can determine this from the to_base_config method 

111 if hasattr(lazy_class, 'to_base_config'): 

112 # Create a dummy instance to inspect the to_base_config method 

113 dummy_instance = lazy_class() 

114 base_instance = dummy_instance.to_base_config() 

115 return type(base_instance) 

116 

117 # Fallback: assume the lazy class name pattern and import the base class 

118 from openhcs.core.config import GlobalPipelineConfig 

119 return GlobalPipelineConfig 

120 

121 

122class FormManagerUpdater: 

123 """Pure functional form manager update operations.""" 

124 

125 @staticmethod 

126 def apply_values_to_form_manager( 

127 form_manager: FormManagerProtocol, 

128 values: Dict[str, Any], 

129 modified_values_tracker: Optional[Dict[str, Any]] = None 

130 ) -> None: 

131 """Apply values to form manager and optionally track modifications.""" 

132 for param_name, value in values.items(): 

133 form_manager.update_parameter(param_name, value) 

134 if modified_values_tracker is not None: 

135 modified_values_tracker[param_name] = value 

136 

137 @staticmethod 

138 def apply_nested_reset_recursively( 

139 form_manager: Any, 

140 config_class: Type, 

141 current_config: Any 

142 ) -> None: 

143 """Apply reset values to nested form managers recursively.""" 

144 if not hasattr(form_manager, 'nested_managers'): 

145 return 

146 

147 for nested_param_name, nested_manager in form_manager.nested_managers.items(): 

148 # Get the nested dataclass type and current instance 

149 nested_field = next( 

150 (f for f in fields(config_class) if f.name == nested_param_name), 

151 None 

152 ) 

153 

154 if nested_field and dataclasses.is_dataclass(nested_field.type): 

155 nested_config_class = nested_field.type 

156 nested_current_config = getattr(current_config, nested_param_name, None) if current_config else None 

157 

158 # Generate reset values for nested dataclass with mixed state support 

159 if nested_current_config and DataclassIntrospector.is_lazy_dataclass(nested_current_config): 

160 # Lazy dataclass: support mixed states - preserve individual field lazy behavior 

161 nested_reset_values = {} 

162 for field in fields(nested_config_class): 

163 # For lazy dataclasses, always reset to None to preserve lazy behavior 

164 # This allows individual fields to maintain placeholder behavior 

165 nested_reset_values[field.name] = None 

166 else: 

167 # Regular concrete dataclass: reset to static defaults 

168 nested_reset_values = DataclassIntrospector.get_static_defaults(nested_config_class) 

169 

170 # Apply reset values to nested manager 

171 FormManagerUpdater.apply_values_to_form_manager(nested_manager, nested_reset_values) 

172 

173 # Recurse for deeper nesting 

174 FormManagerUpdater.apply_nested_reset_recursively( 

175 nested_manager, nested_config_class, nested_current_config 

176 ) 

177 else: 

178 # Fallback: reset using parameter info 

179 FormManagerUpdater._reset_manager_to_parameter_defaults(nested_manager) 

180 

181 @staticmethod 

182 def _reset_manager_to_parameter_defaults(manager: Any) -> None: 

183 """Reset a manager to its parameter defaults.""" 

184 if (hasattr(manager, 'textual_form_manager') and 

185 hasattr(manager.textual_form_manager, 'parameter_info')): 

186 default_values = { 

187 param_name: param_info.default_value 

188 for param_name, param_info in manager.textual_form_manager.parameter_info.items() 

189 } 

190 FormManagerUpdater.apply_values_to_form_manager(manager, default_values) 

191 

192 

193class ResetOperation: 

194 """Immutable reset operation that respects lazy dataclass architecture.""" 

195 

196 def __init__(self, strategy: ResetStrategy, config_class: Type, current_config: Any): 

197 self.strategy = strategy 

198 self.config_class = config_class 

199 self.current_config = current_config 

200 self._reset_values = None 

201 

202 @property 

203 def reset_values(self) -> Dict[str, Any]: 

204 """Lazy computation of reset values.""" 

205 if self._reset_values is None: 

206 self._reset_values = self.strategy.generate_reset_values( 

207 self.config_class, self.current_config 

208 ) 

209 return self._reset_values 

210 

211 def apply_to_form_manager( 

212 self, 

213 form_manager: FormManagerProtocol, 

214 modified_values_tracker: Optional[Dict[str, Any]] = None 

215 ) -> None: 

216 """Apply this reset operation to a form manager.""" 

217 # Apply top-level reset values 

218 FormManagerUpdater.apply_values_to_form_manager( 

219 form_manager, self.reset_values, modified_values_tracker 

220 ) 

221 

222 # Apply nested reset values recursively 

223 FormManagerUpdater.apply_nested_reset_recursively( 

224 form_manager, self.config_class, self.current_config 

225 ) 

226 

227 @classmethod 

228 def create_lazy_aware_reset(cls, config_class: Type, current_config: Any) -> 'ResetOperation': 

229 """Factory method for lazy-aware reset operations.""" 

230 return cls(LazyAwareResetStrategy(), config_class, current_config) 

231 

232 @classmethod 

233 def create_custom_reset(cls, strategy: ResetStrategy, config_class: Type, current_config: Any) -> 'ResetOperation': 

234 """Factory method for custom reset operations.""" 

235 return cls(strategy, config_class, current_config) 

236 

237 

238class ConfigWindow(QDialog): 

239 """ 

240 PyQt6 Configuration Window. 

241  

242 Configuration editing dialog with parameter forms and validation. 

243 Preserves all business logic from Textual version with clean PyQt6 UI. 

244 """ 

245 

246 # Signals 

247 config_saved = pyqtSignal(object) # saved config 

248 config_cancelled = pyqtSignal() 

249 

250 def __init__(self, config_class: Type, current_config: Any, 

251 on_save_callback: Optional[Callable] = None, 

252 color_scheme: Optional[PyQt6ColorScheme] = None, parent=None, 

253 is_global_config_editing: bool = False): 

254 """ 

255 Initialize the configuration window. 

256 

257 Args: 

258 config_class: Configuration class type 

259 current_config: Current configuration instance 

260 on_save_callback: Function to call when config is saved 

261 color_scheme: Color scheme for styling (optional, uses default if None) 

262 parent: Parent widget 

263 """ 

264 super().__init__(parent) 

265 

266 # Business logic state (extracted from Textual version) 

267 self.config_class = config_class 

268 self.current_config = current_config 

269 self.on_save_callback = on_save_callback 

270 

271 # Initialize color scheme and style generator 

272 self.color_scheme = color_scheme or PyQt6ColorScheme() 

273 self.style_generator = StyleSheetGenerator(self.color_scheme) 

274 

275 # Create config form using shared parameter form manager (mirrors Textual TUI) 

276 param_info = SignatureAnalyzer.analyze(config_class) 

277 

278 # Get current parameter values from config instance 

279 parameters = {} 

280 parameter_types = {} 

281 

282 logger.info("=== CONFIG WINDOW PARAMETER LOADING ===") 

283 for name, info in param_info.items(): 

284 # For lazy dataclasses, always preserve None values for consistent placeholder behavior 

285 if hasattr(current_config, '_resolve_field_value'): 

286 # This is a lazy dataclass - use object.__getattribute__ to preserve None values 

287 # This ensures ALL fields show placeholder behavior regardless of Optional status 

288 current_value = object.__getattribute__(current_config, name) if hasattr(current_config, name) else info.default_value 

289 logger.info(f"Lazy field {name}: stored={current_value}, default={info.default_value}") 

290 else: 

291 # Regular dataclass - use normal getattr 

292 current_value = getattr(current_config, name, info.default_value) 

293 logger.info(f"Regular field {name}: value={current_value}") 

294 parameters[name] = current_value 

295 parameter_types[name] = info.param_type 

296 logger.info(f"Final parameter value for {name}: {parameters[name]}") 

297 

298 # Store parameter info 

299 self.parameter_info = param_info 

300 

301 # Create parameter form manager (reuses Textual TUI logic) 

302 # Determine global config type and placeholder prefix 

303 global_config_type = config_class if is_global_config_editing else None 

304 placeholder_prefix = "Default" if is_global_config_editing else "Pipeline default" 

305 

306 self.form_manager = ParameterFormManager( 

307 parameters, parameter_types, "config", param_info, 

308 color_scheme=self.color_scheme, 

309 is_global_config_editing=is_global_config_editing, 

310 global_config_type=global_config_type, 

311 placeholder_prefix=placeholder_prefix 

312 ) 

313 

314 # Setup UI 

315 self.setup_ui() 

316 self.setup_connections() 

317 

318 logger.debug(f"Config window initialized for {config_class.__name__}") 

319 

320 def _should_use_scroll_area(self) -> bool: 

321 """Determine if scroll area should be used based on config complexity.""" 

322 # For simple dataclasses with few fields, don't use scroll area 

323 # This ensures dataclass fields show in full as requested 

324 if dataclasses.is_dataclass(self.config_class): 

325 field_count = len(dataclasses.fields(self.config_class)) 

326 # Use scroll area only for complex configs with many fields 

327 return field_count > 15 

328 

329 # For non-dataclass configs, use scroll area 

330 return True 

331 

332 def setup_ui(self): 

333 """Setup the user interface.""" 

334 self.setWindowTitle(f"Configuration - {self.config_class.__name__}") 

335 self.setModal(False) # Non-modal like plate manager and pipeline editor 

336 self.setMinimumSize(600, 400) 

337 self.resize(800, 600) 

338 

339 layout = QVBoxLayout(self) 

340 layout.setSpacing(10) 

341 

342 # Header with help functionality for dataclass 

343 header_widget = QWidget() 

344 header_layout = QHBoxLayout(header_widget) 

345 header_layout.setContentsMargins(10, 10, 10, 10) 

346 

347 header_label = QLabel(f"Configure {self.config_class.__name__}") 

348 header_label.setFont(QFont("Arial", 14, QFont.Weight.Bold)) 

349 header_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)};") 

350 header_layout.addWidget(header_label) 

351 

352 # Add help button for the dataclass itself 

353 if dataclasses.is_dataclass(self.config_class): 

354 from openhcs.pyqt_gui.widgets.shared.clickable_help_components import HelpButton 

355 help_btn = HelpButton(help_target=self.config_class, text="Help", color_scheme=self.color_scheme) 

356 help_btn.setMaximumWidth(80) 

357 header_layout.addWidget(help_btn) 

358 

359 header_layout.addStretch() 

360 layout.addWidget(header_widget) 

361 

362 # Parameter form - use scroll area only for complex configs, not simple dataclasses 

363 if self._should_use_scroll_area(): 

364 scroll_area = QScrollArea() 

365 scroll_area.setWidgetResizable(True) 

366 scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) 

367 scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 

368 scroll_area.setWidget(self.form_manager) 

369 layout.addWidget(scroll_area) 

370 else: 

371 # For simple dataclasses, show form directly without scrolling 

372 layout.addWidget(self.form_manager) 

373 

374 # Button panel 

375 button_panel = self.create_button_panel() 

376 layout.addWidget(button_panel) 

377 

378 # Apply centralized styling 

379 self.setStyleSheet(self.style_generator.generate_config_window_style()) 

380 

381 def create_parameter_form(self) -> QWidget: 

382 """ 

383 Create the parameter form using extracted business logic. 

384  

385 Returns: 

386 Widget containing parameter form 

387 """ 

388 form_widget = QWidget() 

389 main_layout = QVBoxLayout(form_widget) 

390 

391 # Group parameters by category (simplified grouping) 

392 basic_params = {} 

393 advanced_params = {} 

394 

395 for param_name, param_info in self.parameter_info.items(): 

396 # Simple categorization based on parameter name 

397 if any(keyword in param_name.lower() for keyword in ['debug', 'verbose', 'advanced', 'experimental']): 

398 advanced_params[param_name] = param_info 

399 else: 

400 basic_params[param_name] = param_info 

401 

402 # Create basic parameters group 

403 if basic_params: 

404 basic_group = self.create_parameter_group("Basic Settings", basic_params) 

405 main_layout.addWidget(basic_group) 

406 

407 # Create advanced parameters group 

408 if advanced_params: 

409 advanced_group = self.create_parameter_group("Advanced Settings", advanced_params) 

410 main_layout.addWidget(advanced_group) 

411 

412 main_layout.addStretch() 

413 return form_widget 

414 

415 def create_parameter_group(self, group_name: str, parameters: Dict) -> QGroupBox: 

416 """ 

417 Create a parameter group. 

418  

419 Args: 

420 group_name: Name of the parameter group 

421 parameters: Dictionary of parameters 

422  

423 Returns: 

424 QGroupBox containing the parameters 

425 """ 

426 group_box = QGroupBox(group_name) 

427 layout = QFormLayout(group_box) 

428 

429 for param_name, param_info in parameters.items(): 

430 # Get current value - preserve None values for lazy dataclasses 

431 if hasattr(self.current_config, '_resolve_field_value'): 

432 current_value = object.__getattribute__(self.current_config, param_name) if hasattr(self.current_config, param_name) else param_info.default_value 

433 else: 

434 current_value = getattr(self.current_config, param_name, param_info.default_value) 

435 

436 # Create parameter widget 

437 widget = self.create_parameter_widget(param_name, param_info.param_type, current_value) 

438 if widget: 

439 # Parameter label with help functionality 

440 label_text = param_name.replace('_', ' ').title() 

441 param_description = param_info.description 

442 

443 # Use LabelWithHelp for parameter help 

444 label_with_help = LabelWithHelp( 

445 text=label_text, 

446 param_name=param_name, 

447 param_description=param_description, 

448 param_type=param_info.param_type, 

449 color_scheme=self.color_scheme 

450 ) 

451 label_with_help.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_secondary)}; font-weight: normal;") 

452 

453 # Add to form 

454 layout.addRow(label_with_help, widget) 

455 self.parameter_widgets[param_name] = widget 

456 

457 return group_box 

458 

459 def create_parameter_widget(self, param_name: str, param_type: type, current_value: Any) -> Optional[QWidget]: 

460 """ 

461 Create parameter widget based on type. 

462  

463 Args: 

464 param_name: Parameter name 

465 param_type: Parameter type 

466 current_value: Current parameter value 

467  

468 Returns: 

469 Widget for parameter editing or None 

470 """ 

471 try: 

472 # Boolean parameters 

473 if param_type == bool: 

474 widget = QCheckBox() 

475 widget.setChecked(bool(current_value)) 

476 widget.toggled.connect(lambda checked: self.handle_parameter_change(param_name, checked)) 

477 return widget 

478 

479 # Integer parameters 

480 elif param_type == int: 

481 widget = QSpinBox() 

482 widget.setRange(-999999, 999999) 

483 widget.setValue(int(current_value) if current_value is not None else 0) 

484 widget.valueChanged.connect(lambda value: self.handle_parameter_change(param_name, value)) 

485 return widget 

486 

487 # Float parameters 

488 elif param_type == float: 

489 widget = QDoubleSpinBox() 

490 widget.setRange(-999999.0, 999999.0) 

491 widget.setDecimals(6) 

492 widget.setValue(float(current_value) if current_value is not None else 0.0) 

493 widget.valueChanged.connect(lambda value: self.handle_parameter_change(param_name, value)) 

494 return widget 

495 

496 # Enum parameters 

497 elif any(base.__name__ == 'Enum' for base in param_type.__bases__): 

498 widget = QComboBox() 

499 for enum_value in param_type: 

500 widget.addItem(str(enum_value.value), enum_value) 

501 

502 # Set current value 

503 if current_value is not None: 

504 for i in range(widget.count()): 

505 if widget.itemData(i) == current_value: 

506 widget.setCurrentIndex(i) 

507 break 

508 

509 widget.currentIndexChanged.connect( 

510 lambda index: self.handle_parameter_change(param_name, widget.itemData(index)) 

511 ) 

512 return widget 

513 

514 # String and other parameters 

515 else: 

516 widget = QLineEdit() 

517 widget.setText(str(current_value) if current_value is not None else "") 

518 widget.textChanged.connect(lambda text: self.handle_parameter_change(param_name, text)) 

519 return widget 

520 

521 except Exception as e: 

522 logger.warning(f"Failed to create widget for parameter {param_name}: {e}") 

523 return None 

524 

525 def create_button_panel(self) -> QWidget: 

526 """ 

527 Create the button panel. 

528  

529 Returns: 

530 Widget containing action buttons 

531 """ 

532 panel = QFrame() 

533 panel.setFrameStyle(QFrame.Shape.Box) 

534 panel.setStyleSheet(f""" 

535 QFrame {{ 

536 background-color: {self.color_scheme.to_hex(self.color_scheme.panel_bg)}; 

537 border: 1px solid {self.color_scheme.to_hex(self.color_scheme.border_color)}; 

538 border-radius: 3px; 

539 padding: 10px; 

540 }} 

541 """) 

542 

543 layout = QHBoxLayout(panel) 

544 layout.addStretch() 

545 

546 # Reset button 

547 reset_button = QPushButton("Reset to Defaults") 

548 reset_button.setMinimumWidth(120) 

549 reset_button.clicked.connect(self.reset_to_defaults) 

550 button_styles = self.style_generator.generate_config_button_styles() 

551 reset_button.setStyleSheet(button_styles["reset"]) 

552 layout.addWidget(reset_button) 

553 

554 layout.addSpacing(10) 

555 

556 # Cancel button 

557 cancel_button = QPushButton("Cancel") 

558 cancel_button.setMinimumWidth(80) 

559 cancel_button.clicked.connect(self.reject) 

560 cancel_button.setStyleSheet(button_styles["cancel"]) 

561 layout.addWidget(cancel_button) 

562 

563 # Save button 

564 save_button = QPushButton("Save") 

565 save_button.setMinimumWidth(80) 

566 save_button.clicked.connect(self.save_config) 

567 save_button.setStyleSheet(button_styles["save"]) 

568 layout.addWidget(save_button) 

569 

570 return panel 

571 

572 def setup_connections(self): 

573 """Setup signal/slot connections.""" 

574 self.config_saved.connect(self.on_config_saved) 

575 self.config_cancelled.connect(self.on_config_cancelled) 

576 

577 # Connect form manager parameter changes 

578 self.form_manager.parameter_changed.connect(self._handle_parameter_change) 

579 

580 def _handle_parameter_change(self, param_name: str, value): 

581 """Handle parameter change from form manager (mirrors Textual TUI).""" 

582 # No need to track modifications - form manager maintains state correctly 

583 pass 

584 

585 def load_current_values(self): 

586 """Load current configuration values into widgets.""" 

587 # The form manager already loads current values during initialization 

588 # This method is kept for compatibility but doesn't need to do anything 

589 # since the form manager handles widget initialization with current values 

590 pass 

591 

592 def handle_parameter_change(self, param_name: str, value: Any): 

593 """ 

594 Handle parameter value changes. 

595 

596 Args: 

597 param_name: Name of the parameter 

598 value: New parameter value 

599 """ 

600 # Form manager handles state correctly - no tracking needed 

601 pass 

602 

603 def update_widget_value(self, widget: QWidget, value: Any): 

604 """ 

605 Update widget value without triggering signals. 

606  

607 Args: 

608 widget: Widget to update 

609 value: New value 

610 """ 

611 # Temporarily block signals to avoid recursion 

612 widget.blockSignals(True) 

613 

614 try: 

615 if isinstance(widget, QCheckBox): 

616 widget.setChecked(bool(value)) 

617 elif isinstance(widget, QSpinBox): 

618 widget.setValue(int(value) if value is not None else 0) 

619 elif isinstance(widget, QDoubleSpinBox): 

620 widget.setValue(float(value) if value is not None else 0.0) 

621 elif isinstance(widget, QComboBox): 

622 for i in range(widget.count()): 

623 if widget.itemData(i) == value: 

624 widget.setCurrentIndex(i) 

625 break 

626 elif isinstance(widget, QLineEdit): 

627 widget.setText(str(value) if value is not None else "") 

628 finally: 

629 widget.blockSignals(False) 

630 

631 def reset_to_defaults(self): 

632 """Reset all parameters using individual field reset logic for consistency.""" 

633 # Use the same logic as individual reset buttons to ensure consistency 

634 # This delegates to the form manager's lazy-aware reset logic 

635 if hasattr(self.form_manager, 'reset_all_parameters'): 

636 # For form managers that support lazy-aware reset_all_parameters 

637 self.form_manager.reset_all_parameters() 

638 else: 

639 # Fallback: reset each parameter individually using the same logic as reset buttons 

640 param_info = SignatureAnalyzer.analyze(self.config_class) 

641 for param_name in param_info.keys(): 

642 if hasattr(self.form_manager, '_reset_parameter'): 

643 # Use the individual reset logic (PyQt form manager) 

644 self.form_manager._reset_parameter(param_name) 

645 elif hasattr(self.form_manager, 'reset_parameter'): 

646 # Use the individual reset logic (Textual form manager) 

647 self.form_manager.reset_parameter(param_name) 

648 

649 logger.debug("Reset all parameters using individual field reset logic") 

650 

651 def save_config(self): 

652 """Save the configuration preserving lazy behavior for unset fields.""" 

653 try: 

654 # Get current values from form manager 

655 form_values = self.form_manager.get_current_values() 

656 

657 # For lazy dataclasses, use form values directly 

658 # The form manager already maintains None vs concrete distinction correctly 

659 config_values = form_values 

660 

661 # Create new config instance 

662 new_config = self.config_class(**config_values) 

663 

664 # Emit signal and call callback 

665 self.config_saved.emit(new_config) 

666 

667 if self.on_save_callback: 

668 self.on_save_callback(new_config) 

669 

670 self.accept() 

671 

672 except Exception as e: 

673 logger.error(f"Failed to save configuration: {e}") 

674 from PyQt6.QtWidgets import QMessageBox 

675 QMessageBox.critical(self, "Save Error", f"Failed to save configuration:\n{e}") 

676 

677 def on_config_saved(self, config): 

678 """Handle config saved signal.""" 

679 logger.debug(f"Config saved: {config}") 

680 

681 def on_config_cancelled(self): 

682 """Handle config cancelled signal.""" 

683 logger.debug("Config cancelled") 

684 

685 def reject(self): 

686 """Handle dialog rejection (Cancel button).""" 

687 self.config_cancelled.emit() 

688 super().reject()