diff --git a/src/aare/gui/main_window.py b/src/aare/gui/main_window.py index 87f0621d..67fecf58 100644 --- a/src/aare/gui/main_window.py +++ b/src/aare/gui/main_window.py @@ -150,6 +150,10 @@ class ClickableCursorFilter(QObject): forbidden cursor) still win: they set it later.""" def eventFilter(self, obj, event): + # Same PySide 6.9 quirk as WheelValueGuard: an app-level filter can be + # handed a QWidgetItem instead of a QEvent during layout teardown. + if not isinstance(event, QEvent): + return False if event.type() == QEvent.Type.Polish and isinstance( obj, (QAbstractButton, QComboBox, QSlider) ): diff --git a/src/aare/gui/widgets/wheel_value_guard.py b/src/aare/gui/widgets/wheel_value_guard.py index 7697de8e..f06eb504 100644 --- a/src/aare/gui/widgets/wheel_value_guard.py +++ b/src/aare/gui/widgets/wheel_value_guard.py @@ -1,4 +1,4 @@ -from PySide6.QtCore import QEvent, QObject, Qt +from PySide6.QtCore import QObject, Qt from PySide6.QtGui import QWheelEvent from PySide6.QtWidgets import ( QAbstractScrollArea, @@ -26,7 +26,10 @@ class WheelValueGuard(QObject): GUARDED = (QAbstractSpinBox, QSlider, QDial, QComboBox, QTabBar) def eventFilter(self, obj, event): - if event.type() == QEvent.Type.Wheel and isinstance(obj, self.GUARDED): + # isinstance, not event.type(): PySide 6.9 sometimes hands an app-level + # filter a QWidgetItem instead of a QEvent while layouts are torn down, + # and .type() then raises (pytest-qt CALL ERROR on a random test). + if isinstance(event, QWheelEvent) and isinstance(obj, self.GUARDED): if event.buttons() & Qt.MouseButton.RightButton: return False # right button held: deliberate value adjustment area = obj.parentWidget() @@ -45,7 +48,9 @@ class WheelValueGuard(QObject): ) QApplication.sendEvent(area.viewport(), relayed) return True - return super().eventFilter(obj, event) + # Not super().eventFilter(): QObject's does nothing but type-checks its + # arguments, so the stray QWidgetItem would raise there instead. + return False if __name__ == "__main__": diff --git a/tests/unit/gui/test_main_window.py b/tests/unit/gui/test_main_window.py index c7f2e3ab..c6cba4c0 100644 --- a/tests/unit/gui/test_main_window.py +++ b/tests/unit/gui/test_main_window.py @@ -3,10 +3,10 @@ from unittest.mock import MagicMock, patch import pytest from PySide6.QtCore import QSettings, Qt -from PySide6.QtWidgets import QApplication, QDockWidget +from PySide6.QtWidgets import QApplication, QDockWidget, QWidget, QWidgetItem from aare.gui import styles -from aare.gui.main_window import MainWindow +from aare.gui.main_window import ClickableCursorFilter, MainWindow from aare.gui.styles import THEME_BLUEBIRD, THEME_SUNRISE, THEME_SUNSET @@ -683,3 +683,10 @@ def test_sample_camera_frame_paints_visible_views_and_acks(qtbot, mock_ui_state, # No subscriber (GUI started without a sample feed): the slot must not blow up. win.prediction_thread = None win._on_sample_camera_frame(QImage(4, 6, QImage.Format.Format_RGB888)) + + +def test_cursor_filter_ignores_a_non_event_argument(qapp): + # PySide 6.9 can hand an app-level filter a QWidgetItem instead of a + # QEvent during layout teardown; raising there fails a random test. + widget = QWidget() + assert ClickableCursorFilter().eventFilter(widget, QWidgetItem(widget)) is False diff --git a/tests/unit/gui/test_wheel_value_guard.py b/tests/unit/gui/test_wheel_value_guard.py index 68d760a9..30ae245c 100644 --- a/tests/unit/gui/test_wheel_value_guard.py +++ b/tests/unit/gui/test_wheel_value_guard.py @@ -5,7 +5,15 @@ deliberate right-button + wheel gesture.""" import pytest from PySide6.QtCore import QPoint, QPointF, Qt from PySide6.QtGui import QWheelEvent -from PySide6.QtWidgets import QApplication, QScrollArea, QSlider, QSpinBox, QVBoxLayout, QWidget +from PySide6.QtWidgets import ( + QApplication, + QScrollArea, + QSlider, + QSpinBox, + QVBoxLayout, + QWidget, + QWidgetItem, +) from aare.gui.widgets.wheel_value_guard import WheelValueGuard @@ -72,3 +80,11 @@ def test_bare_wheel_scrolls_the_enclosing_area(guard, qtbot): QApplication.sendEvent(spin, _wheel(Qt.MouseButton.NoButton)) assert spin.value() == 50 assert bar.value() != before + + +def test_a_non_event_argument_is_ignored(guard, qtbot): + # PySide 6.9 can hand an app-level filter a QWidgetItem instead of a + # QEvent during layout teardown; raising there fails a random test. + widget = QWidget() + qtbot.addWidget(widget) + assert guard.eventFilter(widget, QWidgetItem(widget)) is False