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 <noreply@anthropic.com>
This commit is contained in:
2026-08-07 17:08:31 +02:00
co-authored by Claude Fable 5
parent 3d58ae59cc
commit a55be79500
2 changed files with 87 additions and 4 deletions
+5 -4
View File
@@ -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
+82
View File
@@ -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")