From 3fdfe7b7efdcaf5cef83755e535b7b9f9d704797 Mon Sep 17 00:00:00 2001 From: Jonathan Thomas Date: Tue, 12 May 2026 16:46:55 -0500 Subject: [PATCH] Allow mouse wheel tilt (left/right) to scroll timeline - Restored horizontal timeline scrolling via mouse-wheel tilt by handling angleDelta().x() / pixelDelta().x() in wheelEvent() and routing that input through the existing deferred horizontal scroll accumulator/timer path. This allows left/right wheel tilt to scroll without requiring Shift. - Kept existing Shift + wheel behavior intact (still mapped to horizontal scrolling using Y delta). - Extended timeline helper wheel-event stubs to support X-axis deltas, enabling regression tests for tilt input in unit tests. - Added a regression test that verifies horizontal scroll occurs from tilt-wheel input with no modifier keys. --- src/tests/test_timeline_helpers.py | 36 ++++++++++++++++--- .../views/timeline_backend/qwidget/base.py | 19 +++++++++- 2 files changed, 49 insertions(+), 6 deletions(-) diff --git a/src/tests/test_timeline_helpers.py b/src/tests/test_timeline_helpers.py index 1af847f9e..cf0b7aad3 100644 --- a/src/tests/test_timeline_helpers.py +++ b/src/tests/test_timeline_helpers.py @@ -546,10 +546,14 @@ class TimelineHelperTests(unittest.TestCase): return self.active class DeltaStub: - def __init__(self, y=0.0, is_null=False): + def __init__(self, x=0.0, y=0.0, is_null=False): + self._x = float(x) self._y = float(y) self._is_null = bool(is_null) + def x(self): + return self._x + def y(self): return self._y @@ -576,13 +580,13 @@ class TimelineHelperTests(unittest.TestCase): self.accepted = True class WheelEventStub: - def __init__(self, delta, modifiers=Qt.ControlModifier, pixel_delta=None): + def __init__(self, delta, modifiers=Qt.ControlModifier, pixel_delta=None, x_delta=0.0): self._modifiers = modifiers - self._angle_delta = DeltaStub(delta, is_null=False) + self._angle_delta = DeltaStub(x=x_delta, y=delta, is_null=False) if pixel_delta is None: - self._pixel_delta = DeltaStub(0.0, is_null=True) + self._pixel_delta = DeltaStub(is_null=True) else: - self._pixel_delta = DeltaStub(pixel_delta, is_null=False) + self._pixel_delta = DeltaStub(y=pixel_delta, is_null=False) self.accepted = False self.ignored = False @@ -2869,6 +2873,28 @@ class TimelineHelperTests(unittest.TestCase): self.assertAlmostEqual(helper.timeline_scrolled[0][1], 0.64) self.assertEqual(helper.timeline_scrolled[0][2:], [400.0, 100.0]) + def test_qwidget_tilt_wheel_scrolls_horizontally_without_shift(self): + helper, _event_cls, wheel_event_cls = self.make_qwidget_ctrl_zoom_helper() + event = wheel_event_cls(0.0, modifiers=Qt.NoModifier, x_delta=-120.0) + app = types.SimpleNamespace( + window=types.SimpleNamespace( + TimelineScrolled=types.SimpleNamespace( + emit=lambda positions: helper.timeline_scrolled.append(list(positions)) + ) + ) + ) + + with patch.object(self.qwidget_base_module, "get_app", return_value=app): + self.qwidget_base_module.TimelineWidgetBase.wheelEvent(helper, event) + helper._hscroll_timer.active = False + self.qwidget_base_module.TimelineWidgetBase._flush_pending_horizontal_scroll(helper) + + self.assertTrue(event.accepted) + self.assertFalse(event.ignored) + self.assertEqual(helper._hscroll_timer.started, 1) + self.assertAlmostEqual(helper.scrollbar_position[0], 0.24) + self.assertAlmostEqual(helper.scrollbar_position[1], 0.64) + def test_qwidget_set_zoom_factor_keeps_playhead_at_existing_viewport_ratio(self): helper = types.SimpleNamespace() helper.zoom_factor = 10.0 diff --git a/src/windows/views/timeline_backend/qwidget/base.py b/src/windows/views/timeline_backend/qwidget/base.py index 960a809b8..a49de8dc3 100644 --- a/src/windows/views/timeline_backend/qwidget/base.py +++ b/src/windows/views/timeline_backend/qwidget/base.py @@ -2032,9 +2032,26 @@ class TimelineWidgetBase(QWidget): event.accept() return + horizontal_delta = 0.0 + pixel_delta = event.pixelDelta() + angle_delta = event.angleDelta() + if not pixel_delta.isNull(): + horizontal_delta = pixel_delta.x() + if not horizontal_delta: + horizontal_delta = angle_delta.x() + + if horizontal_delta and self.scrollbar_position[3] > 0 and self.scrollbar_position[2] > self.scrollbar_position[3]: + delta = -horizontal_delta / 120.0 + self._pending_hscroll_delta += delta + if not self._hscroll_timer.isActive(): + # Process accumulated wheel events once the event queue is flushed + self._hscroll_timer.start(0) + event.accept() + return + if event.modifiers() & Qt.ShiftModifier: if self.scrollbar_position[3] > 0 and self.scrollbar_position[2] > self.scrollbar_position[3]: - delta = -event.angleDelta().y() / 120.0 + delta = -angle_delta.y() / 120.0 if delta: self._pending_hscroll_delta += delta if not self._hscroll_timer.isActive():