|
6 | 6 | import numpy as np |
7 | 7 | from PySide6 import QtCore, QtWidgets |
8 | 8 | from PySide6.QtCore import Qt |
9 | | -from PySide6.QtWidgets import QDialog |
| 9 | +from PySide6.QtWidgets import QApplication, QDialog |
10 | 10 | from shapely.geometry import Point |
11 | 11 |
|
12 | 12 | import jabs.feature_extraction |
@@ -271,6 +271,21 @@ def id_overlay_mode(self, mode: PlayerWidget.IdentityOverlayMode) -> None: |
271 | 271 | """ |
272 | 272 | self._player_widget.id_overlay_mode = mode |
273 | 273 |
|
| 274 | + def get_open_dialogs(self) -> list[tuple[str, QtWidgets.QWidget]]: |
| 275 | + """Get a list of open dialogs managed by this widget. |
| 276 | +
|
| 277 | + Returns: |
| 278 | + List of (title, widget) tuples for all visible dialogs. |
| 279 | + """ |
| 280 | + dialogs = [] |
| 281 | + |
| 282 | + if self._training_report_dialog is not None and self._training_report_dialog.isVisible(): |
| 283 | + dialogs.append( |
| 284 | + (self._training_report_dialog.windowTitle(), self._training_report_dialog) |
| 285 | + ) |
| 286 | + |
| 287 | + return dialogs |
| 288 | + |
274 | 289 | def set_project(self, project: Project) -> None: |
275 | 290 | """set the currently opened project""" |
276 | 291 | self._project = project |
@@ -736,31 +751,86 @@ def _training_thread_complete(self, elapsed_ms) -> None: |
736 | 751 |
|
737 | 752 | # Display training report if available |
738 | 753 | if self._training_report_markdown: |
739 | | - # Close any existing training report dialog before showing new one |
740 | | - if self._training_report_dialog is not None: |
| 754 | + # Check if any JABS window has focus (main window or any child) |
| 755 | + jabs_app_has_focus = QApplication.activeWindow() is not None |
| 756 | + |
| 757 | + # If any JABS window has focus and a dialog exists, close it and create new one |
| 758 | + # This is the only reliable way to switch focus on macOS |
| 759 | + if jabs_app_has_focus and self._training_report_dialog is not None: |
| 760 | + # Save the old position and size before closing |
| 761 | + old_geometry = self._training_report_dialog.geometry() |
741 | 762 | self._training_report_dialog.close() |
742 | 763 | self._training_report_dialog = None |
743 | 764 |
|
744 | | - self._training_report_dialog = TrainingReportDialog( |
745 | | - self._training_report_markdown, |
746 | | - title=f"Training Report: {self._controls.current_behavior}", |
747 | | - parent=self, |
748 | | - ) |
749 | | - # Connect to cleanup when dialog is closed |
750 | | - self._training_report_dialog.finished.connect( |
751 | | - lambda: setattr(self, "_training_report_dialog", None) |
752 | | - ) |
| 765 | + # Create new dialog |
| 766 | + self._training_report_dialog = TrainingReportDialog( |
| 767 | + self._training_report_markdown, |
| 768 | + title=f"Training Report: {self._controls.current_behavior}", |
| 769 | + parent=self, |
| 770 | + ) |
753 | 771 |
|
754 | | - # Temporarily set stay-on-top to ensure it appears in front |
755 | | - self._training_report_dialog.setWindowFlags( |
756 | | - self._training_report_dialog.windowFlags() | Qt.WindowType.WindowStaysOnTopHint |
757 | | - ) |
758 | | - self._training_report_dialog.show() |
759 | | - self._training_report_dialog.raise_() |
760 | | - self._training_report_dialog.activateWindow() |
| 772 | + # Connect to cleanup when dialog is closed |
| 773 | + self._training_report_dialog.finished.connect( |
| 774 | + lambda: setattr(self, "_training_report_dialog", None) |
| 775 | + ) |
| 776 | + |
| 777 | + # Show with aggressive focus-stealing |
| 778 | + self._training_report_dialog.setWindowFlags( |
| 779 | + self._training_report_dialog.windowFlags() | Qt.WindowType.WindowStaysOnTopHint |
| 780 | + ) |
| 781 | + self._training_report_dialog.show() |
| 782 | + # Restore geometry AFTER show() to preserve position |
| 783 | + self._training_report_dialog.setGeometry(old_geometry) |
| 784 | + # Force processing of events to ensure window system updates |
| 785 | + QtCore.QCoreApplication.processEvents() |
| 786 | + self._training_report_dialog.raise_() |
| 787 | + self._training_report_dialog.activateWindow() |
| 788 | + # Try to force repaint/update |
| 789 | + self._training_report_dialog.repaint() |
| 790 | + QtCore.QCoreApplication.processEvents() |
| 791 | + |
| 792 | + # Remove stay-on-top flag after a brief delay |
| 793 | + QtCore.QTimer.singleShot(100, lambda: self._remove_stay_on_top_flag()) |
| 794 | + |
| 795 | + elif self._training_report_dialog is not None: |
| 796 | + # JABS doesn't have focus - just update existing dialog quietly |
| 797 | + self._training_report_dialog.update_content( |
| 798 | + self._training_report_markdown, |
| 799 | + title=f"Training Report: {self._controls.current_behavior}", |
| 800 | + ) |
| 801 | + # Ensure dialog is visible (in case it was minimized or hidden) |
| 802 | + if self._training_report_dialog.isMinimized(): |
| 803 | + self._training_report_dialog.showNormal() |
| 804 | + elif not self._training_report_dialog.isVisible(): |
| 805 | + self._training_report_dialog.show() |
| 806 | + else: |
| 807 | + # No existing dialog - create new one |
| 808 | + self._training_report_dialog = TrainingReportDialog( |
| 809 | + self._training_report_markdown, |
| 810 | + title=f"Training Report: {self._controls.current_behavior}", |
| 811 | + parent=self, |
| 812 | + ) |
| 813 | + # Connect to cleanup when dialog is closed |
| 814 | + self._training_report_dialog.finished.connect( |
| 815 | + lambda: setattr(self, "_training_report_dialog", None) |
| 816 | + ) |
761 | 817 |
|
762 | | - # Remove stay-on-top flag after a brief delay so user can manage windows normally |
763 | | - QtCore.QTimer.singleShot(100, lambda: self._remove_stay_on_top_flag()) |
| 818 | + # Show the dialog |
| 819 | + self._training_report_dialog.show() |
| 820 | + |
| 821 | + # Only bring to front and activate if JABS has focus |
| 822 | + if jabs_app_has_focus: |
| 823 | + # Temporarily set stay-on-top to ensure it appears in front |
| 824 | + self._training_report_dialog.setWindowFlags( |
| 825 | + self._training_report_dialog.windowFlags() |
| 826 | + | Qt.WindowType.WindowStaysOnTopHint |
| 827 | + ) |
| 828 | + self._training_report_dialog.show() # Need to call show() again after changing flags |
| 829 | + self._training_report_dialog.raise_() |
| 830 | + self._training_report_dialog.activateWindow() |
| 831 | + |
| 832 | + # Remove stay-on-top flag after a brief delay so user can manage windows normally |
| 833 | + QtCore.QTimer.singleShot(100, lambda: self._remove_stay_on_top_flag()) |
764 | 834 |
|
765 | 835 | self._training_report_markdown = None # Clear after displaying |
766 | 836 |
|
|
0 commit comments