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

292 statements  

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

1""" 

2Dual Editor Window for PyQt6 

3 

4Step and function editing dialog with tabbed interface. 

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

6""" 

7 

8import logging 

9from typing import Optional, Callable, Any, Dict 

10 

11from PyQt6.QtWidgets import ( 

12 QDialog, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, 

13 QTabWidget, QWidget, QFrame, QTextEdit, QScrollArea 

14) 

15from PyQt6.QtCore import Qt, pyqtSignal 

16from PyQt6.QtGui import QFont 

17 

18from openhcs.core.steps.function_step import FunctionStep 

19from openhcs.textual_tui.services.pattern_data_manager import PatternDataManager 

20 

21from openhcs.pyqt_gui.shared.color_scheme import PyQt6ColorScheme 

22logger = logging.getLogger(__name__) 

23 

24 

25class DualEditorWindow(QDialog): 

26 """ 

27 PyQt6 Dual Editor Window. 

28  

29 Step and function editing dialog with tabbed interface for comprehensive editing. 

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

31 """ 

32 

33 # Signals 

34 step_saved = pyqtSignal(object) # FunctionStep 

35 step_cancelled = pyqtSignal() 

36 changes_detected = pyqtSignal(bool) # has_changes 

37 

38 def __init__(self, step_data: Optional[FunctionStep] = None, is_new: bool = False, 

39 on_save_callback: Optional[Callable] = None, color_scheme: Optional[PyQt6ColorScheme] = None, parent=None): 

40 """ 

41 Initialize the dual editor window. 

42  

43 Args: 

44 step_data: FunctionStep to edit (None for new step) 

45 is_new: Whether this is a new step 

46 on_save_callback: Function to call when step is saved 

47 parent: Parent widget 

48 """ 

49 super().__init__(parent) 

50 

51 # Make window non-modal (like plate manager and pipeline editor) 

52 self.setModal(False) 

53 

54 # Initialize color scheme 

55 self.color_scheme = color_scheme or PyQt6ColorScheme() 

56 

57 # Business logic state (extracted from Textual version) 

58 self.is_new = is_new 

59 self.on_save_callback = on_save_callback 

60 

61 # Pattern management (extracted from Textual version) 

62 self.pattern_manager = PatternDataManager() 

63 

64 if step_data: 

65 self.editing_step = step_data 

66 else: 

67 self.editing_step = self.pattern_manager.create_new_step() 

68 

69 # Store original for change detection 

70 self.original_step = self.pattern_manager.clone_pattern(self.editing_step) 

71 

72 # Change tracking 

73 self.has_changes = False 

74 self.current_tab = "step" 

75 

76 # UI components 

77 self.tab_widget: Optional[QTabWidget] = None 

78 self.step_editor: Optional[QWidget] = None 

79 self.func_editor: Optional[QWidget] = None 

80 

81 # Setup UI 

82 self.setup_ui() 

83 self.setup_connections() 

84 

85 logger.debug(f"Dual editor window initialized (new={is_new})") 

86 

87 def setup_ui(self): 

88 """Setup the user interface.""" 

89 title = "New Step" if self.is_new else f"Edit Step: {getattr(self.editing_step, 'name', 'Unknown')}" 

90 self.setWindowTitle(title) 

91 # Keep non-modal (already set in __init__) 

92 self.setMinimumSize(800, 600) 

93 self.resize(1000, 700) 

94 

95 layout = QVBoxLayout(self) 

96 layout.setSpacing(10) 

97 

98 # Header 

99 header_label = QLabel(title) 

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

101 header_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)}; padding: 10px;") 

102 layout.addWidget(header_label) 

103 

104 # Tabbed content 

105 self.tab_widget = QTabWidget() 

106 self.tab_widget.setStyleSheet(f""" 

107 QTabWidget::pane {{ 

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

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

110 }} 

111 QTabBar::tab {{ 

112 background-color: {self.color_scheme.to_hex(self.color_scheme.input_bg)}; 

113 color: white; 

114 padding: 8px 16px; 

115 margin-right: 2px; 

116 border-top-left-radius: 4px; 

117 border-top-right-radius: 4px; 

118 }} 

119 QTabBar::tab:selected {{ 

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

121 }} 

122 QTabBar::tab:hover {{ 

123 background-color: {self.color_scheme.to_hex(self.color_scheme.button_hover_bg)}; 

124 }} 

125 """) 

126 

127 # Create tabs 

128 self.create_step_tab() 

129 self.create_function_tab() 

130 

131 layout.addWidget(self.tab_widget) 

132 

133 # Button panel 

134 button_panel = self.create_button_panel() 

135 layout.addWidget(button_panel) 

136 

137 # Set styling 

138 self.setStyleSheet(f""" 

139 QDialog {{ 

140 background-color: {self.color_scheme.to_hex(self.color_scheme.window_bg)}; 

141 color: white; 

142 }} 

143 """) 

144 

145 def create_step_tab(self): 

146 """Create the step settings tab (using dedicated widget).""" 

147 from openhcs.pyqt_gui.widgets.step_parameter_editor import StepParameterEditorWidget 

148 

149 # Create step parameter editor widget (mirrors Textual TUI) 

150 self.step_editor = StepParameterEditorWidget(self.editing_step, service_adapter=None, color_scheme=self.color_scheme) 

151 

152 # Connect parameter changes 

153 self.step_editor.step_parameter_changed.connect(self.detect_changes) 

154 

155 self.tab_widget.addTab(self.step_editor, "Step Settings") 

156 

157 def create_function_tab(self): 

158 """Create the function pattern tab (using dedicated widget).""" 

159 from openhcs.pyqt_gui.widgets.function_list_editor import FunctionListEditorWidget 

160 

161 # Convert step func to function list format 

162 initial_functions = self._convert_step_func_to_list() 

163 

164 # Create function list editor widget (mirrors Textual TUI) 

165 step_id = getattr(self.editing_step, 'name', 'unknown_step') 

166 self.func_editor = FunctionListEditorWidget( 

167 initial_functions=initial_functions, 

168 step_identifier=step_id, 

169 service_adapter=None 

170 ) 

171 

172 # Store main window reference for orchestrator access (find it through parent chain) 

173 main_window = self._find_main_window() 

174 if main_window: 

175 self.func_editor.main_window = main_window 

176 

177 # Initialize step configuration settings in function editor (mirrors Textual TUI) 

178 self.func_editor.current_group_by = self.editing_step.group_by 

179 self.func_editor.current_variable_components = self.editing_step.variable_components or [] 

180 

181 # Refresh component button to show correct text and state (mirrors Textual TUI reactive updates) 

182 self.func_editor._refresh_component_button() 

183 

184 # Connect function pattern changes 

185 self.func_editor.function_pattern_changed.connect(self._on_function_pattern_changed) 

186 

187 self.tab_widget.addTab(self.func_editor, "Function Pattern") 

188 

189 def _on_function_pattern_changed(self): 

190 """Handle function pattern changes from function editor.""" 

191 # Update step func from function editor - use current_pattern to get full pattern data 

192 current_pattern = self.func_editor.current_pattern 

193 self.editing_step.func = current_pattern 

194 self.detect_changes() 

195 logger.debug(f"Function pattern changed: {current_pattern}") 

196 

197 

198 

199 def create_button_panel(self) -> QWidget: 

200 """ 

201 Create the button panel with save/cancel actions. 

202 

203 Returns: 

204 Widget containing action buttons 

205 """ 

206 panel = QFrame() 

207 panel.setFrameStyle(QFrame.Shape.Box) 

208 panel.setStyleSheet(f""" 

209 QFrame {{ 

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

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

212 border-radius: 3px; 

213 padding: 10px; 

214 }} 

215 """) 

216 

217 layout = QHBoxLayout(panel) 

218 

219 # Changes indicator 

220 self.changes_label = QLabel("") 

221 self.changes_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.status_warning)}; font-style: italic;") 

222 layout.addWidget(self.changes_label) 

223 

224 layout.addStretch() 

225 

226 # Cancel button 

227 cancel_button = QPushButton("Cancel") 

228 cancel_button.setMinimumWidth(80) 

229 cancel_button.clicked.connect(self.cancel_edit) 

230 cancel_button.setStyleSheet(f""" 

231 QPushButton {{ 

232 background-color: {self.color_scheme.to_hex(self.color_scheme.status_error)}; 

233 color: white; 

234 border: 1px solid {self.color_scheme.to_hex(self.color_scheme.status_error)}; 

235 border-radius: 3px; 

236 padding: 8px; 

237 }} 

238 QPushButton:hover {{ 

239 background-color: {self.color_scheme.to_hex(self.color_scheme.status_error)}; 

240 }} 

241 """) 

242 layout.addWidget(cancel_button) 

243 

244 # Save button 

245 self.save_button = QPushButton("Save") 

246 self.save_button.setMinimumWidth(80) 

247 self.save_button.setEnabled(False) # Initially disabled 

248 self.save_button.clicked.connect(self.save_edit) 

249 self.save_button.setStyleSheet(f""" 

250 QPushButton {{ 

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

252 color: white; 

253 border: 1px solid {self.color_scheme.to_hex(self.color_scheme.selection_bg)}; 

254 border-radius: 3px; 

255 padding: 8px; 

256 }} 

257 QPushButton:hover {{ 

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

259 }} 

260 QPushButton:disabled {{ 

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

262 color: {self.color_scheme.to_hex(self.color_scheme.border_light)}; 

263 border: 1px solid {self.color_scheme.to_hex(self.color_scheme.separator_color)}; 

264 }} 

265 """) 

266 layout.addWidget(self.save_button) 

267 

268 return panel 

269 

270 def setup_connections(self): 

271 """Setup signal/slot connections.""" 

272 # Tab change tracking 

273 self.tab_widget.currentChanged.connect(self.on_tab_changed) 

274 

275 # Change detection 

276 self.changes_detected.connect(self.on_changes_detected) 

277 func_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_accent)}; font-weight: bold; font-size: 14px;") 

278 header_layout.addWidget(func_label) 

279 

280 header_layout.addStretch() 

281 

282 # Function management buttons (mirrors Textual TUI) 

283 add_btn = QPushButton("Add") 

284 add_btn.setMaximumWidth(60) 

285 add_btn.setStyleSheet(self._get_button_style()) 

286 add_btn.clicked.connect(self.add_function) 

287 header_layout.addWidget(add_btn) 

288 

289 load_btn = QPushButton("Load") 

290 load_btn.setMaximumWidth(60) 

291 load_btn.setStyleSheet(self._get_button_style()) 

292 load_btn.clicked.connect(self.load_function_pattern) 

293 header_layout.addWidget(load_btn) 

294 

295 save_as_btn = QPushButton("Save As") 

296 save_as_btn.setMaximumWidth(80) 

297 save_as_btn.setStyleSheet(self._get_button_style()) 

298 save_as_btn.clicked.connect(self.save_function_pattern) 

299 header_layout.addWidget(save_as_btn) 

300 

301 code_btn = QPushButton("Code") 

302 code_btn.setMaximumWidth(60) 

303 code_btn.setStyleSheet(self._get_button_style()) 

304 code_btn.clicked.connect(self.edit_function_code) 

305 header_layout.addWidget(code_btn) 

306 

307 layout.addLayout(header_layout) 

308 

309 # Function list scroll area (mirrors Textual TUI) 

310 self.function_scroll = QScrollArea() 

311 self.function_scroll.setWidgetResizable(True) 

312 self.function_scroll.setVerticalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAsNeeded) 

313 self.function_scroll.setStyleSheet(f""" 

314 QScrollArea {{ 

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

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

317 border-radius: 4px; 

318 }} 

319 """) 

320 

321 # Function list container 

322 self.function_container = QWidget() 

323 self.function_layout = QVBoxLayout(self.function_container) 

324 self.function_layout.setAlignment(Qt.AlignmentFlag.AlignTop) 

325 self.function_layout.setSpacing(8) 

326 

327 # Initialize function list from step 

328 self.function_panes = [] 

329 self._populate_function_list() 

330 

331 self.function_scroll.setWidget(self.function_container) 

332 layout.addWidget(self.function_scroll) 

333 

334 return frame 

335 

336 def _get_button_style(self) -> str: 

337 """Get consistent button styling.""" 

338 return """ 

339 QPushButton { 

340 background-color: {self.color_scheme.to_hex(self.color_scheme.input_bg)}; 

341 color: white; 

342 border: 1px solid {self.color_scheme.to_hex(self.color_scheme.border_light)}; 

343 border-radius: 3px; 

344 padding: 6px 12px; 

345 font-size: 11px; 

346 } 

347 QPushButton:hover { 

348 background-color: {self.color_scheme.to_hex(self.color_scheme.button_hover_bg)}; 

349 } 

350 QPushButton:pressed { 

351 background-color: {self.color_scheme.to_hex(self.color_scheme.button_pressed_bg)}; 

352 } 

353 """ 

354 

355 def _populate_function_list(self): 

356 """Populate function list from current step (mirrors Textual TUI).""" 

357 # Clear existing panes 

358 for pane in self.function_panes: 

359 pane.setParent(None) 

360 self.function_panes.clear() 

361 

362 # Convert step func to function list 

363 functions = self._convert_step_func_to_list() 

364 

365 if not functions: 

366 # Show empty state 

367 empty_label = QLabel("No functions defined. Click 'Add' to begin.") 

368 empty_label.setAlignment(Qt.AlignmentFlag.AlignCenter) 

369 empty_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.text_disabled)}; font-style: italic; padding: 20px;") 

370 self.function_layout.addWidget(empty_label) 

371 else: 

372 # Create function panes 

373 for i, func_item in enumerate(functions): 

374 pane = self._create_function_pane(func_item, i) 

375 self.function_panes.append(pane) 

376 self.function_layout.addWidget(pane) 

377 

378 def _convert_step_func_to_list(self): 

379 """Convert step func to initial pattern format for function list editor.""" 

380 if not hasattr(self.editing_step, 'func') or not self.editing_step.func: 

381 return [] 

382 

383 # Return the step func directly - the function list editor will handle the conversion 

384 return self.editing_step.func 

385 

386 

387 

388 def _find_main_window(self): 

389 """Find the main window through the parent chain.""" 

390 try: 

391 # Navigate up the parent chain to find OpenHCSMainWindow 

392 current = self.parent() 

393 while current: 

394 # Check if this is the main window (has floating_windows attribute) 

395 if hasattr(current, 'floating_windows') and hasattr(current, 'service_adapter'): 

396 logger.debug(f"Found main window: {type(current).__name__}") 

397 return current 

398 current = current.parent() 

399 

400 logger.warning("Could not find main window in parent chain") 

401 return None 

402 

403 except Exception as e: 

404 logger.error(f"Error finding main window: {e}") 

405 return None 

406 

407 def _get_current_plate_from_pipeline_editor(self): 

408 """Get current plate from pipeline editor (mirrors Textual TUI pattern).""" 

409 try: 

410 # Navigate up to find pipeline editor widget 

411 current = self.parent() 

412 while current: 

413 # Check if this is a pipeline editor widget 

414 if hasattr(current, 'current_plate') and hasattr(current, 'pipeline_steps'): 

415 current_plate = getattr(current, 'current_plate', None) 

416 if current_plate: 

417 logger.debug(f"Found current plate from pipeline editor: {current_plate}") 

418 return current_plate 

419 

420 # Check children for pipeline editor widget 

421 for child in current.findChildren(QWidget): 

422 if hasattr(child, 'current_plate') and hasattr(child, 'pipeline_steps'): 

423 current_plate = getattr(child, 'current_plate', None) 

424 if current_plate: 

425 logger.debug(f"Found current plate from pipeline editor child: {current_plate}") 

426 return current_plate 

427 

428 current = current.parent() 

429 

430 logger.warning("Could not find current plate from pipeline editor") 

431 return None 

432 

433 except Exception as e: 

434 logger.error(f"Error getting current plate from pipeline editor: {e}") 

435 return None 

436 

437 # Old function pane methods removed - now using dedicated FunctionListEditorWidget 

438 

439 def create_button_panel(self) -> QWidget: 

440 """ 

441 Create the button panel with save/cancel actions. 

442  

443 Returns: 

444 Widget containing action buttons 

445 """ 

446 panel = QFrame() 

447 panel.setFrameStyle(QFrame.Shape.Box) 

448 panel.setStyleSheet(f""" 

449 QFrame {{ 

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

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

452 border-radius: 3px; 

453 padding: 10px; 

454 }} 

455 """) 

456 

457 layout = QHBoxLayout(panel) 

458 

459 # Changes indicator 

460 self.changes_label = QLabel("") 

461 self.changes_label.setStyleSheet(f"color: {self.color_scheme.to_hex(self.color_scheme.status_warning)}; font-style: italic;") 

462 layout.addWidget(self.changes_label) 

463 

464 layout.addStretch() 

465 

466 # Cancel button 

467 cancel_button = QPushButton("Cancel") 

468 cancel_button.setMinimumWidth(80) 

469 cancel_button.clicked.connect(self.cancel_edit) 

470 cancel_button.setStyleSheet(f""" 

471 QPushButton {{ 

472 background-color: {self.color_scheme.to_hex(self.color_scheme.status_error)}; 

473 color: white; 

474 border: 1px solid {self.color_scheme.to_hex(self.color_scheme.status_error)}; 

475 border-radius: 3px; 

476 padding: 8px; 

477 }} 

478 QPushButton:hover {{ 

479 background-color: {self.color_scheme.to_hex(self.color_scheme.status_error)}; 

480 }} 

481 """) 

482 layout.addWidget(cancel_button) 

483 

484 # Save button 

485 self.save_button = QPushButton("Save") 

486 self.save_button.setMinimumWidth(80) 

487 self.save_button.clicked.connect(self.save_step) 

488 self.save_button.setStyleSheet(f""" 

489 QPushButton {{ 

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

491 color: white; 

492 border: 1px solid {self.color_scheme.to_hex(self.color_scheme.selection_bg)}; 

493 border-radius: 3px; 

494 padding: 8px; 

495 }} 

496 QPushButton:hover {{ 

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

498 }} 

499 """) 

500 layout.addWidget(self.save_button) 

501 

502 return panel 

503 

504 def setup_connections(self): 

505 """Setup signal/slot connections.""" 

506 self.tab_widget.currentChanged.connect(self.on_tab_changed) 

507 self.changes_detected.connect(self.on_changes_detected) 

508 

509 def get_function_info(self) -> str: 

510 """ 

511 Get function information for display. 

512  

513 Returns: 

514 Function information string 

515 """ 

516 if not self.editing_step or not hasattr(self.editing_step, 'func'): 

517 return "No function assigned" 

518 

519 func = self.editing_step.func 

520 func_name = getattr(func, '__name__', 'Unknown Function') 

521 func_module = getattr(func, '__module__', 'Unknown Module') 

522 

523 info = f"Function: {func_name}\n" 

524 info += f"Module: {func_module}\n" 

525 

526 # Add parameter info if available 

527 if hasattr(self.editing_step, 'parameters'): 

528 params = self.editing_step.parameters 

529 if params: 

530 info += f"\nParameters ({len(params)}):\n" 

531 for param_name, param_value in params.items(): 

532 info += f" {param_name}: {param_value}\n" 

533 

534 return info 

535 

536 def on_step_parameter_changed(self, param_name: str, value): 

537 """Handle step parameter changes from form manager.""" 

538 try: 

539 # Update the editing step 

540 setattr(self.editing_step, param_name, value) 

541 self.detect_changes() 

542 logger.debug(f"Step parameter changed: {param_name} = {value}") 

543 except Exception as e: 

544 logger.error(f"Failed to update step parameter {param_name}: {e}") 

545 

546 def on_tab_changed(self, index: int): 

547 """Handle tab changes.""" 

548 tab_names = ["step", "function"] 

549 if 0 <= index < len(tab_names): 

550 self.current_tab = tab_names[index] 

551 logger.debug(f"Tab changed to: {self.current_tab}") 

552 

553 def detect_changes(self): 

554 """Detect if changes have been made.""" 

555 has_changes = False 

556 

557 # Check step parameters 

558 for attr in ['name', 'variable_components', 'group_by', 'force_disk_output', 'input_dir', 'output_dir']: 

559 original_value = getattr(self.original_step, attr, None) 

560 current_value = getattr(self.editing_step, attr, None) 

561 if original_value != current_value: 

562 has_changes = True 

563 break 

564 

565 # Check function pattern 

566 if not has_changes: 

567 original_func = getattr(self.original_step, 'func', None) 

568 current_func = getattr(self.editing_step, 'func', None) 

569 # Simple comparison - could be enhanced for deep comparison 

570 has_changes = str(original_func) != str(current_func) 

571 

572 if has_changes != self.has_changes: 

573 self.has_changes = has_changes 

574 self.changes_detected.emit(has_changes) 

575 

576 def on_changes_detected(self, has_changes: bool): 

577 """Handle changes detection.""" 

578 if has_changes: 

579 self.changes_label.setText("● Unsaved changes") 

580 self.save_button.setEnabled(True) 

581 else: 

582 self.changes_label.setText("") 

583 self.save_button.setEnabled(False) 

584 

585 def save_step(self): 

586 """Save the edited step.""" 

587 try: 

588 # Validate step 

589 if not hasattr(self.editing_step, 'name') or not self.editing_step.name.strip(): 

590 from PyQt6.QtWidgets import QMessageBox 

591 QMessageBox.warning(self, "Validation Error", "Step name cannot be empty.") 

592 return 

593 

594 # Emit signals and call callback 

595 self.step_saved.emit(self.editing_step) 

596 

597 if self.on_save_callback: 

598 self.on_save_callback(self.editing_step) 

599 

600 self.accept() 

601 logger.debug(f"Step saved: {getattr(self.editing_step, 'name', 'Unknown')}") 

602 

603 except Exception as e: 

604 logger.error(f"Failed to save step: {e}") 

605 from PyQt6.QtWidgets import QMessageBox 

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

607 

608 def cancel_edit(self): 

609 """Cancel editing and close dialog.""" 

610 if self.has_changes: 

611 from PyQt6.QtWidgets import QMessageBox 

612 reply = QMessageBox.question( 

613 self, 

614 "Unsaved Changes", 

615 "You have unsaved changes. Are you sure you want to cancel?", 

616 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, 

617 QMessageBox.StandardButton.No 

618 ) 

619 

620 if reply != QMessageBox.StandardButton.Yes: 

621 return 

622 

623 self.step_cancelled.emit() 

624 self.reject() 

625 logger.debug("Step editing cancelled") 

626 

627 

628 

629 def closeEvent(self, event): 

630 """Handle dialog close event.""" 

631 if self.has_changes: 

632 from PyQt6.QtWidgets import QMessageBox 

633 reply = QMessageBox.question( 

634 self, 

635 "Unsaved Changes", 

636 "You have unsaved changes. Are you sure you want to close?", 

637 QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No, 

638 QMessageBox.StandardButton.No 

639 ) 

640 

641 if reply != QMessageBox.StandardButton.Yes: 

642 event.ignore() 

643 return 

644 

645 event.accept()