From a55be7950025d56bb57f454b00d036c20bb04042 Mon Sep 17 00:00:00 2001 From: Dawn Date: Fri, 7 Aug 2026 17:08:31 +0200 Subject: [PATCH] feat: wheel only adjusts values while the right button is held WheelValueGuard (app-level event filter, installed by MainWindow) makes a bare wheel over spin boxes, sliders, dials, and combos scroll the enclosing scroll area instead of nudging the value - motor protection. NoWheelScrollArea stops swallowing wheel events since values are now guarded at the widget. Co-Authored-By: Claude Fable 5 --- src/aare/gui/widgets/no_wheel_scroll_area.py | 9 ++- src/aare/gui/widgets/wheel_value_guard.py | 82 ++++++++++++++++++++ 2 files changed, 87 insertions(+), 4 deletions(-) create mode 100644 src/aare/gui/widgets/wheel_value_guard.py diff --git a/src/aare/gui/widgets/no_wheel_scroll_area.py b/src/aare/gui/widgets/no_wheel_scroll_area.py index c57c15f4..1dce19d9 100644 --- a/src/aare/gui/widgets/no_wheel_scroll_area.py +++ b/src/aare/gui/widgets/no_wheel_scroll_area.py @@ -2,9 +2,10 @@ from PySide6.QtWidgets import QScrollArea class NoWheelScrollArea(QScrollArea): + """Historic name: it used to swallow the wheel entirely so scrolling the + column could not nudge a value widget. WheelValueGuard now protects the + value widgets themselves (right button + wheel to adjust), so the wheel + scrolls the column content normally again — and only ever scrolls.""" + def __init__(self, parent=None): super().__init__(parent) - - def wheelEvent(self, event): - # Override the wheelEvent and do nothing - pass diff --git a/src/aare/gui/widgets/wheel_value_guard.py b/src/aare/gui/widgets/wheel_value_guard.py new file mode 100644 index 00000000..7697de8e --- /dev/null +++ b/src/aare/gui/widgets/wheel_value_guard.py @@ -0,0 +1,82 @@ +from PySide6.QtCore import QEvent, QObject, Qt +from PySide6.QtGui import QWheelEvent +from PySide6.QtWidgets import ( + QAbstractScrollArea, + QAbstractSpinBox, + QApplication, + QComboBox, + QDial, + QSlider, + QTabBar, +) + + +class WheelValueGuard(QObject): + """App-level wheel safety for value widgets. + + The wheel only ADJUSTS a slider / spin box / dial / combo while the + RIGHT mouse button is held down — a deliberate two-hand gesture. A bare + wheel over any of them is re-aimed at the enclosing scroll area, so + scrolling a page can never nudge a value and therefore never moves a + motor. Install once with QApplication.installEventFilter. + """ + + # QTabBar: wheel switches tabs on Linux by default — same accidental-input + # hazard as a value nudge, so guard it too. + GUARDED = (QAbstractSpinBox, QSlider, QDial, QComboBox, QTabBar) + + def eventFilter(self, obj, event): + if event.type() == QEvent.Type.Wheel and isinstance(obj, self.GUARDED): + if event.buttons() & Qt.MouseButton.RightButton: + return False # right button held: deliberate value adjustment + area = obj.parentWidget() + while area is not None and not isinstance(area, QAbstractScrollArea): + area = area.parentWidget() + if area is not None: + relayed = QWheelEvent( + area.viewport().mapFromGlobal(event.globalPosition()), + event.globalPosition(), + event.pixelDelta(), + event.angleDelta(), + event.buttons(), + event.modifiers(), + event.phase(), + event.inverted(), + ) + QApplication.sendEvent(area.viewport(), relayed) + return True + return super().eventFilter(obj, event) + + +if __name__ == "__main__": + # ponytail: smallest check that fails if the guard logic breaks + import os + + os.environ.setdefault("QT_QPA_PLATFORM", "offscreen") + from PySide6.QtCore import QPoint, QPointF + + app = QApplication([]) + guard = WheelValueGuard() + app.installEventFilter(guard) + slider = QSlider(Qt.Orientation.Horizontal) + slider.setRange(0, 100) + slider.setValue(50) + slider.show() + + def wheel(buttons): + return QWheelEvent( + QPointF(5, 5), + QPointF(5, 5), + QPoint(0, 0), + QPoint(0, 120), + buttons, + Qt.KeyboardModifier.NoModifier, + Qt.ScrollPhase.NoScrollPhase, + False, + ) + + QApplication.sendEvent(slider, wheel(Qt.MouseButton.NoButton)) + assert slider.value() == 50, "bare wheel must not adjust the slider" + QApplication.sendEvent(slider, wheel(Qt.MouseButton.RightButton)) + assert slider.value() != 50, "right-button + wheel must adjust the slider" + print("gude")