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

288 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2025-10-01 18:33 +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 typing import Type, Any, Callable, Optional 

11 

12from PyQt6.QtWidgets import ( 

13 QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, 

14 QScrollArea, QWidget, QFrame, QSplitter, QTreeWidget, QTreeWidgetItem, 

15 QLineEdit, QSpinBox, QDoubleSpinBox, QCheckBox, QComboBox 

16) 

17from PyQt6.QtCore import Qt, pyqtSignal 

18from PyQt6.QtGui import QFont 

19 

20# Infrastructure classes removed - functionality migrated to ParameterFormManager service layer 

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

22from openhcs.pyqt_gui.shared.style_generator import StyleSheetGenerator 

23from openhcs.pyqt_gui.shared.color_scheme import PyQt6ColorScheme 

24from openhcs.core.config import GlobalPipelineConfig 

25# ❌ REMOVED: require_config_context decorator - enhanced decorator events system handles context automatically 

26from openhcs.core.lazy_placeholder_simplified import LazyDefaultPlaceholderService 

27from openhcs.config_framework.context_manager import config_context 

28 

29 

30 

31logger = logging.getLogger(__name__) 

32 

33 

34# Infrastructure classes removed - functionality migrated to ParameterFormManager service layer 

35 

36 

37class ConfigWindow(QDialog): 

38 """ 

39 PyQt6 Configuration Window. 

40  

41 Configuration editing dialog with parameter forms and validation. 

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

43 """ 

44 

45 # Signals 

46 config_saved = pyqtSignal(object) # saved config 

47 config_cancelled = pyqtSignal() 

48 

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

50 on_save_callback: Optional[Callable] = None, 

51 color_scheme: Optional[PyQt6ColorScheme] = None, parent=None,): 

52 """ 

53 Initialize the configuration window. 

54 

55 Args: 

56 config_class: Configuration class type 

57 current_config: Current configuration instance 

58 on_save_callback: Function to call when config is saved 

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

60 parent: Parent widget 

61 orchestrator: Optional orchestrator reference for context persistence 

62 """ 

63 super().__init__(parent) 

64 

65 # Business logic state (extracted from Textual version) 

66 self.config_class = config_class 

67 self.current_config = current_config 

68 self.on_save_callback = on_save_callback 

69 

70 

71 # Initialize color scheme and style generator 

72 self.color_scheme = color_scheme or PyQt6ColorScheme() 

73 self.style_generator = StyleSheetGenerator(self.color_scheme) 

74 

75 # SIMPLIFIED: Use dual-axis resolution 

76 from openhcs.core.lazy_placeholder import LazyDefaultPlaceholderService 

77 

78 # Determine placeholder prefix based on actual instance type (not class type) 

79 is_lazy_dataclass = LazyDefaultPlaceholderService.has_lazy_resolution(type(current_config)) 

80 placeholder_prefix = "Pipeline default" if is_lazy_dataclass else "Default" 

81 

82 # SIMPLIFIED: Use ParameterFormManager with dual-axis resolution 

83 root_field_id = type(current_config).__name__ # e.g., "GlobalPipelineConfig" or "PipelineConfig" 

84 global_config_type = GlobalPipelineConfig # Always use GlobalPipelineConfig for dual-axis resolution 

85 

86 # CRITICAL FIX: Pipeline Config Editor should NOT use itself as parent context 

87 # context_obj=None means inherit from thread-local GlobalPipelineConfig only 

88 # The overlay (current form state) will be built by ParameterFormManager 

89 # This fixes the circular context bug where reset showed old values instead of global defaults 

90 

91 self.form_manager = ParameterFormManager.from_dataclass_instance( 

92 dataclass_instance=current_config, 

93 field_id=root_field_id, 

94 placeholder_prefix=placeholder_prefix, 

95 color_scheme=self.color_scheme, 

96 use_scroll_area=True, 

97 global_config_type=global_config_type, 

98 context_obj=None # Inherit from thread-local GlobalPipelineConfig only 

99 ) 

100 

101 # No config_editor needed - everything goes through form_manager 

102 self.config_editor = None 

103 

104 # Setup UI 

105 self.setup_ui() 

106 

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

108 

109 def _should_use_scroll_area(self) -> bool: 

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

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

112 # This ensures dataclass fields show in full as requested 

113 if dataclasses.is_dataclass(self.config_class): 

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

115 # Use scroll area for configs with more than 8 fields (PipelineConfig has ~12 fields) 

116 return field_count > 8 

117 

118 # For non-dataclass configs, use scroll area 

119 return True 

120 

121 def setup_ui(self): 

122 """Setup the user interface.""" 

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

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

125 self.setMinimumSize(600, 400) 

126 self.resize(800, 600) 

127 

128 layout = QVBoxLayout(self) 

129 layout.setSpacing(10) 

130 

131 # Header with help functionality for dataclass 

132 header_widget = QWidget() 

133 header_layout = QHBoxLayout(header_widget) 

134 header_layout.setContentsMargins(10, 10, 10, 10) 

135 

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

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

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

139 header_layout.addWidget(header_label) 

140 

141 # Add help button for the dataclass itself 

142 if dataclasses.is_dataclass(self.config_class): 

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

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

145 help_btn.setMaximumWidth(80) 

146 header_layout.addWidget(help_btn) 

147 

148 header_layout.addStretch() 

149 layout.addWidget(header_widget) 

150 

151 # Create splitter with tree view on left and form on right 

152 splitter = QSplitter(Qt.Orientation.Horizontal) 

153 

154 # Left panel - Inheritance hierarchy tree 

155 self.tree_widget = self._create_inheritance_tree() 

156 splitter.addWidget(self.tree_widget) 

157 

158 # Right panel - Parameter form 

159 if self._should_use_scroll_area(): 

160 self.scroll_area = QScrollArea() 

161 self.scroll_area.setWidgetResizable(True) 

162 self.scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) 

163 self.scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 

164 self.scroll_area.setWidget(self.form_manager) 

165 splitter.addWidget(self.scroll_area) 

166 else: 

167 # For simple dataclasses, show form directly without scrolling 

168 splitter.addWidget(self.form_manager) 

169 

170 # Set splitter proportions (30% tree, 70% form) 

171 splitter.setSizes([300, 700]) 

172 

173 # Add splitter with stretch factor so it expands to fill available space 

174 layout.addWidget(splitter, 1) # stretch factor = 1 

175 

176 # Button panel 

177 button_panel = self.create_button_panel() 

178 layout.addWidget(button_panel) 

179 

180 # Apply centralized styling 

181 self.setStyleSheet(self.style_generator.generate_config_window_style()) 

182 

183 def _create_inheritance_tree(self) -> QTreeWidget: 

184 """Create tree widget showing inheritance hierarchy for navigation.""" 

185 tree = QTreeWidget() 

186 tree.setHeaderLabel("Configuration Hierarchy") 

187 # Remove width restrictions to allow horizontal dragging 

188 tree.setMinimumWidth(200) 

189 

190 # Style the tree with original appearance 

191 tree.setStyleSheet(f""" 

192 QTreeWidget {{ 

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

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

195 border-radius: 3px; 

196 color: {self.color_scheme.to_hex(self.color_scheme.text_primary)}; 

197 font-size: 12px; 

198 }} 

199 QTreeWidget::item {{ 

200 padding: 4px; 

201 border-bottom: 1px solid {self.color_scheme.to_hex(self.color_scheme.border_color)}; 

202 }} 

203 QTreeWidget::item:selected {{ 

204 background-color: {self.color_scheme.to_hex(self.color_scheme.selection_bg)}; 

205 color: white; 

206 }} 

207 QTreeWidget::item:hover {{ 

208 background-color: {self.color_scheme.to_hex(self.color_scheme.hover_bg)}; 

209 }} 

210 """) 

211 

212 # Build inheritance hierarchy 

213 self._populate_inheritance_tree(tree) 

214 

215 # Connect double-click to navigation 

216 tree.itemDoubleClicked.connect(self._on_tree_item_double_clicked) 

217 

218 return tree 

219 

220 def _populate_inheritance_tree(self, tree: QTreeWidget): 

221 """Populate the inheritance tree with only dataclasses visible in the UI.""" 

222 import dataclasses 

223 

224 # Create root item for the main config class 

225 config_name = self.config_class.__name__ if self.config_class else "Configuration" 

226 root_item = QTreeWidgetItem([config_name]) 

227 root_item.setData(0, Qt.ItemDataRole.UserRole, {'type': 'dataclass', 'class': self.config_class}) 

228 tree.addTopLevelItem(root_item) 

229 

230 # Only show dataclasses that are visible in the UI (have form sections) 

231 if dataclasses.is_dataclass(self.config_class): 

232 self._add_ui_visible_dataclasses_to_tree(root_item, self.config_class) 

233 

234 # Expand the tree 

235 tree.expandAll() 

236 

237 def _add_ui_visible_dataclasses_to_tree(self, parent_item: QTreeWidgetItem, dataclass_type): 

238 """Add only dataclasses that are visible in the UI form.""" 

239 import dataclasses 

240 

241 # Get all fields from this dataclass 

242 fields = dataclasses.fields(dataclass_type) 

243 

244 for field in fields: 

245 field_name = field.name 

246 field_type = field.type 

247 

248 # Only show dataclass fields (these appear as sections in the UI) 

249 if dataclasses.is_dataclass(field_type): 

250 # Create a child item for this nested dataclass 

251 field_item = QTreeWidgetItem([f"{field_name} ({field_type.__name__})"]) 

252 field_item.setData(0, Qt.ItemDataRole.UserRole, { 

253 'type': 'dataclass', 

254 'class': field_type, 

255 'field_name': field_name 

256 }) 

257 parent_item.addChild(field_item) 

258 

259 # Show inheritance hierarchy for this dataclass 

260 self._add_inheritance_info(field_item, field_type) 

261 

262 # Recursively add nested dataclasses 

263 self._add_ui_visible_dataclasses_to_tree(field_item, field_type) 

264 

265 def _add_inheritance_info(self, parent_item: QTreeWidgetItem, dataclass_type): 

266 """Add inheritance information for a dataclass with proper hierarchy, skipping lazy classes.""" 

267 # Get direct base classes, skipping lazy versions 

268 direct_bases = [] 

269 for cls in dataclass_type.__bases__: 

270 if (cls.__name__ != 'object' and 

271 hasattr(cls, '__dataclass_fields__') and 

272 not cls.__name__.startswith('Lazy')): # Skip lazy dataclass wrappers 

273 direct_bases.append(cls) 

274 

275 # Add base classes directly as children (no "Inherits from:" label) 

276 for base_class in direct_bases: 

277 base_item = QTreeWidgetItem([base_class.__name__]) 

278 base_item.setData(0, Qt.ItemDataRole.UserRole, { 

279 'type': 'inheritance_link', 

280 'target_class': base_class 

281 }) 

282 parent_item.addChild(base_item) 

283 

284 # Recursively add inheritance for this base class 

285 self._add_inheritance_info(base_item, base_class) 

286 

287 def _on_tree_item_double_clicked(self, item: QTreeWidgetItem, column: int): 

288 """Handle tree item double-clicks for navigation.""" 

289 data = item.data(0, Qt.ItemDataRole.UserRole) 

290 if not data: 

291 return 

292 

293 item_type = data.get('type') 

294 

295 if item_type == 'dataclass': 

296 # Navigate to the dataclass section in the form 

297 field_name = data.get('field_name') 

298 if field_name: 

299 self._scroll_to_section(field_name) 

300 logger.debug(f"Navigating to section: {field_name}") 

301 else: 

302 class_obj = data.get('class') 

303 class_name = getattr(class_obj, '__name__', 'Unknown') if class_obj else 'Unknown' 

304 logger.debug(f"Double-clicked on root dataclass: {class_name}") 

305 

306 elif item_type == 'inheritance_link': 

307 # Find and navigate to the target class in the tree 

308 target_class = data.get('target_class') 

309 if target_class: 

310 self._navigate_to_class_in_tree(target_class) 

311 logger.debug(f"Navigating to inherited class: {target_class.__name__}") 

312 

313 def _navigate_to_class_in_tree(self, target_class): 

314 """Find and highlight a class in the tree.""" 

315 # Search through all items in the tree to find the target class 

316 root = self.tree_widget.invisibleRootItem() 

317 self._search_and_highlight_class(root, target_class) 

318 

319 def _search_and_highlight_class(self, parent_item, target_class): 

320 """Recursively search for and highlight a class in the tree.""" 

321 for i in range(parent_item.childCount()): 

322 child = parent_item.child(i) 

323 data = child.data(0, Qt.ItemDataRole.UserRole) 

324 

325 if data and data.get('type') == 'dataclass': 

326 if data.get('class') == target_class: 

327 # Found the target - select and scroll to it 

328 self.tree_widget.setCurrentItem(child) 

329 self.tree_widget.scrollToItem(child) 

330 return True 

331 

332 # Recursively search children 

333 if self._search_and_highlight_class(child, target_class): 

334 return True 

335 

336 return False 

337 

338 def _scroll_to_section(self, field_name: str): 

339 """Scroll to a specific section in the form.""" 

340 try: 

341 # Check if we have a scroll area 

342 if hasattr(self, 'scroll_area') and self.scroll_area: 

343 # Find the group box for this field name 

344 form_widget = self.scroll_area.widget() 

345 if form_widget: 

346 group_box = self._find_group_box_by_name(form_widget, field_name) 

347 if group_box: 

348 # Scroll to the group box with small margins for better visibility 

349 self.scroll_area.ensureWidgetVisible(group_box, 20, 20) 

350 logger.debug(f"Scrolled to section: {field_name}") 

351 return 

352 

353 # Fallback: try to scroll to form manager directly if no scroll area 

354 if hasattr(self.form_manager, 'nested_managers') and field_name in self.form_manager.nested_managers: 

355 nested_manager = self.form_manager.nested_managers[field_name] 

356 if hasattr(self, 'scroll_area') and self.scroll_area: 

357 self.scroll_area.ensureWidgetVisible(nested_manager, 20, 20) 

358 logger.debug(f"Scrolled to nested manager: {field_name}") 

359 return 

360 

361 logger.debug(f"Could not find section to scroll to: {field_name}") 

362 except Exception as e: 

363 logger.warning(f"Error scrolling to section {field_name}: {e}") 

364 

365 def _find_group_box_by_name(self, parent_widget, field_name: str): 

366 """Recursively find a group box by field name.""" 

367 from PyQt6.QtWidgets import QGroupBox 

368 

369 # Look for QGroupBox widgets with matching titles 

370 group_boxes = parent_widget.findChildren(QGroupBox) 

371 

372 for group_box in group_boxes: 

373 title = group_box.title() 

374 # Check if field name matches the title (case insensitive) 

375 if (field_name.lower() in title.lower() or 

376 title.lower().replace(' ', '_') == field_name.lower() or 

377 field_name.lower().replace('_', ' ') in title.lower()): 

378 logger.debug(f"Found matching group box: '{title}' for field '{field_name}'") 

379 return group_box 

380 

381 # Also check object names as fallback 

382 for child in parent_widget.findChildren(QWidget): 

383 if hasattr(child, 'objectName') and child.objectName(): 

384 if field_name.lower() in child.objectName().lower(): 

385 logger.debug(f"Found matching widget by object name: '{child.objectName()}' for field '{field_name}'") 

386 return child 

387 

388 logger.debug(f"No matching section found for field: {field_name}") 

389 # Debug: print all group box titles to help troubleshoot 

390 all_titles = [gb.title() for gb in group_boxes] 

391 logger.debug(f"Available group box titles: {all_titles}") 

392 

393 return None 

394 

395 

396 

397 

398 

399 

400 

401 def create_button_panel(self) -> QWidget: 

402 """ 

403 Create the button panel. 

404  

405 Returns: 

406 Widget containing action buttons 

407 """ 

408 panel = QFrame() 

409 panel.setFrameStyle(QFrame.Shape.Box) 

410 panel.setStyleSheet(f""" 

411 QFrame {{ 

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

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

414 border-radius: 3px; 

415 padding: 10px; 

416 }} 

417 """) 

418 

419 layout = QHBoxLayout(panel) 

420 layout.addStretch() 

421 

422 # Reset button 

423 reset_button = QPushButton("Reset to Defaults") 

424 reset_button.setMinimumWidth(120) 

425 reset_button.clicked.connect(self.reset_to_defaults) 

426 button_styles = self.style_generator.generate_config_button_styles() 

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

428 layout.addWidget(reset_button) 

429 

430 layout.addSpacing(10) 

431 

432 # Cancel button 

433 cancel_button = QPushButton("Cancel") 

434 cancel_button.setMinimumWidth(80) 

435 cancel_button.clicked.connect(self.reject) 

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

437 layout.addWidget(cancel_button) 

438 

439 # Save button 

440 save_button = QPushButton("Save") 

441 save_button.setMinimumWidth(80) 

442 save_button.clicked.connect(self.save_config) 

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

444 layout.addWidget(save_button) 

445 

446 return panel 

447 

448 

449 

450 

451 

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

453 """ 

454 Update widget value without triggering signals. 

455  

456 Args: 

457 widget: Widget to update 

458 value: New value 

459 """ 

460 # Temporarily block signals to avoid recursion 

461 widget.blockSignals(True) 

462 

463 try: 

464 if isinstance(widget, QCheckBox): 

465 widget.setChecked(bool(value)) 

466 elif isinstance(widget, QSpinBox): 

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

468 elif isinstance(widget, QDoubleSpinBox): 

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

470 elif isinstance(widget, QComboBox): 

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

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

473 widget.setCurrentIndex(i) 

474 break 

475 elif isinstance(widget, QLineEdit): 

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

477 finally: 

478 widget.blockSignals(False) 

479 

480 def reset_to_defaults(self): 

481 """Reset all parameters using centralized service with full sophistication.""" 

482 # Service layer now contains ALL the sophisticated logic previously in infrastructure classes 

483 # This includes nested dataclass reset, lazy awareness, and recursive traversal 

484 self.form_manager.reset_all_parameters() 

485 

486 # Refresh placeholder text to ensure UI shows correct defaults 

487 self.form_manager._refresh_all_placeholders() 

488 

489 logger.debug("Reset all parameters using enhanced ParameterFormManager service") 

490 

491 def refresh_config(self, new_config): 

492 """Refresh the config window with new configuration data. 

493 

494 This is called when the underlying configuration changes (e.g., from tier 3 edits) 

495 to keep the UI in sync with the actual data. 

496 

497 Args: 

498 new_config: New configuration instance to display 

499 """ 

500 try: 

501 # Import required services 

502 from openhcs.core.lazy_placeholder import LazyDefaultPlaceholderService 

503 

504 # Update the current config 

505 self.current_config = new_config 

506 

507 # Determine placeholder prefix based on actual instance type (same logic as __init__) 

508 is_lazy_dataclass = LazyDefaultPlaceholderService.has_lazy_resolution(type(new_config)) 

509 placeholder_prefix = "Pipeline default" if is_lazy_dataclass else "Default" 

510 

511 # SIMPLIFIED: Create new form manager with dual-axis resolution 

512 root_field_id = type(new_config).__name__ # e.g., "GlobalPipelineConfig" or "PipelineConfig" 

513 

514 # FIXED: Use the dataclass instance itself for context consistently 

515 new_form_manager = ParameterFormManager.from_dataclass_instance( 

516 dataclass_instance=new_config, 

517 field_id=root_field_id, 

518 placeholder_prefix=placeholder_prefix, 

519 color_scheme=self.color_scheme, 

520 use_scroll_area=True, 

521 global_config_type=GlobalPipelineConfig 

522 ) 

523 

524 # Find and replace the form widget in the layout 

525 # Layout structure: [0] header, [1] form/scroll_area, [2] buttons 

526 layout = self.layout() 

527 if layout.count() >= 2: 

528 # Get the form container (might be scroll area or direct form) 

529 form_container_item = layout.itemAt(1) 

530 if form_container_item: 

531 old_container = form_container_item.widget() 

532 

533 # Remove old container from layout 

534 layout.removeItem(form_container_item) 

535 

536 # Properly delete old container and its contents 

537 if old_container: 

538 old_container.deleteLater() 

539 

540 # Add new form container at the same position 

541 if self._should_use_scroll_area(): 

542 # Create new scroll area with new form 

543 from PyQt6.QtWidgets import QScrollArea 

544 from PyQt6.QtCore import Qt 

545 scroll_area = QScrollArea() 

546 scroll_area.setWidgetResizable(True) 

547 scroll_area.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) 

548 scroll_area.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) 

549 scroll_area.setWidget(new_form_manager) 

550 layout.insertWidget(1, scroll_area) 

551 else: 

552 # Add form directly 

553 layout.insertWidget(1, new_form_manager) 

554 

555 # Update the form manager reference 

556 self.form_manager = new_form_manager 

557 

558 logger.debug(f"Config window refreshed with new {type(new_config).__name__}") 

559 else: 

560 logger.error("Could not find form container in layout") 

561 else: 

562 logger.error(f"Layout has insufficient items: {layout.count()}") 

563 

564 except Exception as e: 

565 logger.error(f"Failed to refresh config window: {e}") 

566 import traceback 

567 logger.error(f"Traceback: {traceback.format_exc()}") 

568 

569 def save_config(self): 

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

571 try: 

572 if LazyDefaultPlaceholderService.has_lazy_resolution(self.config_class): 

573 # BETTER APPROACH: For lazy dataclasses, only save user-modified values 

574 # Get only values that were explicitly set by the user (non-None raw values) 

575 user_modified_values = self.form_manager.get_user_modified_values() 

576 

577 # Create fresh lazy instance with only user-modified values 

578 # This preserves lazy resolution for unmodified fields 

579 new_config = self.config_class(**user_modified_values) 

580 else: 

581 # For non-lazy dataclasses, use all current values 

582 current_values = self.form_manager.get_current_values() 

583 new_config = self.config_class(**current_values) 

584 

585 # Emit signal and call callback 

586 self.config_saved.emit(new_config) 

587 

588 if self.on_save_callback: 

589 self.on_save_callback(new_config) 

590 

591 self.accept() 

592 

593 except Exception as e: 

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

595 from PyQt6.QtWidgets import QMessageBox 

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

597 

598 

599 

600 def reject(self): 

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

602 self.config_cancelled.emit() 

603 self._cleanup_signal_connections() 

604 super().reject() 

605 

606 def accept(self): 

607 """Handle dialog acceptance (Save button).""" 

608 self._cleanup_signal_connections() 

609 super().accept() 

610 

611 def closeEvent(self, event): 

612 """Handle window close event.""" 

613 self._cleanup_signal_connections() 

614 super().closeEvent(event) 

615 

616 def _cleanup_signal_connections(self): 

617 """Clean up signal connections to prevent memory leaks.""" 

618 # The signal connection is handled by the plate manager 

619 # We just need to mark that this window is closing 

620 logger.debug("Config window closing, signal connections will be cleaned up")