1/*
2 * SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
3 *
4 * SPDX-License-Identifier: LGPL-2.0-or-later
5 */
6
7#include "wheelhandler.h"
8#include "settings.h"
9
10#include <QQmlEngine>
11#include <QQuickItem>
12#include <QQuickWindow>
13#include <QWheelEvent>
14
15KirigamiWheelEvent::KirigamiWheelEvent(QObject *parent)
16 : QObject(parent)
17{
18}
19
20KirigamiWheelEvent::~KirigamiWheelEvent()
21{
22}
23
24void KirigamiWheelEvent::initializeFromEvent(QWheelEvent *event)
25{
26 m_x = event->position().x();
27 m_y = event->position().y();
28 m_angleDelta = event->angleDelta();
29 m_pixelDelta = event->pixelDelta();
30 m_buttons = event->buttons();
31 m_modifiers = event->modifiers();
32 m_accepted = false;
33 m_inverted = event->inverted();
34}
35
36qreal KirigamiWheelEvent::x() const
37{
38 return m_x;
39}
40
41qreal KirigamiWheelEvent::y() const
42{
43 return m_y;
44}
45
46QPointF KirigamiWheelEvent::angleDelta() const
47{
48 return m_angleDelta;
49}
50
51QPointF KirigamiWheelEvent::pixelDelta() const
52{
53 return m_pixelDelta;
54}
55
56int KirigamiWheelEvent::buttons() const
57{
58 return m_buttons;
59}
60
61int KirigamiWheelEvent::modifiers() const
62{
63 return m_modifiers;
64}
65
66bool KirigamiWheelEvent::inverted() const
67{
68 return m_inverted;
69}
70
71bool KirigamiWheelEvent::isAccepted()
72{
73 return m_accepted;
74}
75
76void KirigamiWheelEvent::setAccepted(bool accepted)
77{
78 m_accepted = accepted;
79}
80
81///////////////////////////////
82
83WheelFilterItem::WheelFilterItem(QQuickItem *parent)
84 : QQuickItem(parent)
85{
86 setEnabled(false);
87}
88
89///////////////////////////////
90
91WheelHandler::WheelHandler(QObject *parent)
92 : QObject(parent)
93 , m_filterItem(new WheelFilterItem(nullptr))
94{
95 m_filterItem->installEventFilter(filterObj: this);
96
97 m_wheelScrollingTimer.setSingleShot(true);
98 m_wheelScrollingTimer.setInterval(m_wheelScrollingDuration);
99 m_wheelScrollingTimer.callOnTimeout(args: [this]() {
100 setScrolling(false);
101 });
102
103 m_xScrollAnimation.setEasingCurve(QEasingCurve::OutCubic);
104 m_yScrollAnimation.setEasingCurve(QEasingCurve::OutCubic);
105 m_xInertiaScrollAnimation.setEasingCurve(QEasingCurve::OutQuad);
106 m_yInertiaScrollAnimation.setEasingCurve(QEasingCurve::OutQuad);
107
108 connect(sender: QGuiApplication::styleHints(), signal: &QStyleHints::wheelScrollLinesChanged, context: this, slot: [this](int scrollLines) {
109 m_defaultPixelStepSize = 20 * scrollLines;
110 if (!m_explicitVStepSize && m_verticalStepSize != m_defaultPixelStepSize) {
111 m_verticalStepSize = m_defaultPixelStepSize;
112 Q_EMIT verticalStepSizeChanged();
113 }
114 if (!m_explicitHStepSize && m_horizontalStepSize != m_defaultPixelStepSize) {
115 m_horizontalStepSize = m_defaultPixelStepSize;
116 Q_EMIT horizontalStepSizeChanged();
117 }
118 });
119}
120
121WheelHandler::~WheelHandler()
122{
123 delete m_filterItem;
124}
125
126QQuickItem *WheelHandler::target() const
127{
128 return m_flickable;
129}
130
131void WheelHandler::setTarget(QQuickItem *target)
132{
133 if (m_flickable == target) {
134 return;
135 }
136
137 if (target && !target->inherits(classname: "QQuickFlickable")) {
138 qmlWarning(me: this) << "target must be a QQuickFlickable";
139 return;
140 }
141
142 if (m_flickable) {
143 m_flickable->removeEventFilter(obj: this);
144 disconnect(sender: m_flickable, signal: nullptr, receiver: m_filterItem, member: nullptr);
145 disconnect(sender: m_flickable, signal: &QQuickItem::parentChanged, receiver: this, slot: &WheelHandler::_k_rebindScrollBars);
146 }
147
148 m_flickable = target;
149 m_filterItem->setParentItem(target);
150 if (m_xScrollAnimation.targetObject()) {
151 m_xScrollAnimation.stop();
152 }
153 m_xScrollAnimation.setTargetObject(target);
154 if (m_yScrollAnimation.targetObject()) {
155 m_yScrollAnimation.stop();
156 }
157 m_yScrollAnimation.setTargetObject(target);
158 if (m_yInertiaScrollAnimation.targetObject()) {
159 m_yInertiaScrollAnimation.stop();
160 }
161 m_yInertiaScrollAnimation.setTargetObject(target);
162 if (m_xInertiaScrollAnimation.targetObject()) {
163 m_xInertiaScrollAnimation.stop();
164 }
165 m_xInertiaScrollAnimation.setTargetObject(target);
166
167 if (target) {
168 target->installEventFilter(filterObj: this);
169
170 // Stack WheelFilterItem over the Flickable's scrollable content
171 m_filterItem->stackAfter(target->property(name: "contentItem").value<QQuickItem *>());
172 // Make it fill the Flickable
173 m_filterItem->setWidth(target->width());
174 m_filterItem->setHeight(target->height());
175 connect(sender: target, signal: &QQuickItem::widthChanged, context: m_filterItem, slot: [this, target]() {
176 m_filterItem->setWidth(target->width());
177 });
178 connect(sender: target, signal: &QQuickItem::heightChanged, context: m_filterItem, slot: [this, target]() {
179 m_filterItem->setHeight(target->height());
180 });
181 }
182
183 _k_rebindScrollBars();
184
185 Q_EMIT targetChanged();
186}
187
188void WheelHandler::_k_rebindScrollBars()
189{
190 struct ScrollBarAttached {
191 QObject *attached = nullptr;
192 QQuickItem *vertical = nullptr;
193 QQuickItem *horizontal = nullptr;
194 };
195
196 ScrollBarAttached attachedToFlickable;
197 ScrollBarAttached attachedToScrollView;
198
199 if (m_flickable) {
200 // Get ScrollBars so that we can filter them too, even if they're not
201 // in the bounds of the Flickable
202 const auto flickableChildren = m_flickable->children();
203 for (const auto child : flickableChildren) {
204 if (child->inherits(classname: "QQuickScrollBarAttached")) {
205 attachedToFlickable.attached = child;
206 attachedToFlickable.vertical = child->property(name: "vertical").value<QQuickItem *>();
207 attachedToFlickable.horizontal = child->property(name: "horizontal").value<QQuickItem *>();
208 break;
209 }
210 }
211
212 // Check ScrollView if there are no scrollbars attached to the Flickable.
213 // We need to check if the parent inherits QQuickScrollView in case the
214 // parent is another Flickable that already has a Kirigami WheelHandler.
215 auto flickableParent = m_flickable->parentItem();
216 if (m_scrollView && m_scrollView != flickableParent) {
217 m_scrollView->removeEventFilter(obj: this);
218 }
219 if (flickableParent && flickableParent->inherits(classname: "QQuickScrollView")) {
220 if (m_scrollView != flickableParent) {
221 m_scrollView = flickableParent;
222 m_scrollView->installEventFilter(filterObj: this);
223 }
224 const auto siblings = m_scrollView->children();
225 for (const auto child : siblings) {
226 if (child->inherits(classname: "QQuickScrollBarAttached")) {
227 attachedToScrollView.attached = child;
228 attachedToScrollView.vertical = child->property(name: "vertical").value<QQuickItem *>();
229 attachedToScrollView.horizontal = child->property(name: "horizontal").value<QQuickItem *>();
230 break;
231 }
232 }
233 }
234 }
235
236 // Dilemma: ScrollBars can be attached to both ScrollView and Flickable,
237 // but only one of them should be shown anyway. Let's prefer Flickable.
238
239 struct ChosenScrollBar {
240 QObject *attached = nullptr;
241 QQuickItem *scrollBar = nullptr;
242 };
243
244 ChosenScrollBar vertical;
245 if (attachedToFlickable.vertical) {
246 vertical.attached = attachedToFlickable.attached;
247 vertical.scrollBar = attachedToFlickable.vertical;
248 } else if (attachedToScrollView.vertical) {
249 vertical.attached = attachedToScrollView.attached;
250 vertical.scrollBar = attachedToScrollView.vertical;
251 }
252
253 ChosenScrollBar horizontal;
254 if (attachedToFlickable.horizontal) {
255 horizontal.attached = attachedToFlickable.attached;
256 horizontal.scrollBar = attachedToFlickable.horizontal;
257 } else if (attachedToScrollView.horizontal) {
258 horizontal.attached = attachedToScrollView.attached;
259 horizontal.scrollBar = attachedToScrollView.horizontal;
260 }
261
262 // Flickable may get re-parented to or out of a ScrollView, so we need to
263 // redo the discovery process. This is especially important for
264 // Kirigami.ScrollablePage component.
265 if (m_flickable) {
266 if (attachedToFlickable.horizontal && attachedToFlickable.vertical) {
267 // But if both scrollbars are already those from the preferred
268 // Flickable, there's no need for rediscovery.
269 disconnect(sender: m_flickable, signal: &QQuickItem::parentChanged, receiver: this, slot: &WheelHandler::_k_rebindScrollBars);
270 } else {
271 connect(sender: m_flickable, signal: &QQuickItem::parentChanged, context: this, slot: &WheelHandler::_k_rebindScrollBars, type: Qt::UniqueConnection);
272 }
273 }
274
275 if (m_verticalScrollBar != vertical.scrollBar) {
276 if (m_verticalScrollBar) {
277 m_verticalScrollBar->removeEventFilter(obj: this);
278 disconnect(m_verticalChangedConnection);
279 }
280 m_verticalScrollBar = vertical.scrollBar;
281 if (vertical.scrollBar) {
282 vertical.scrollBar->installEventFilter(filterObj: this);
283 m_verticalChangedConnection = connect(sender: vertical.attached, SIGNAL(verticalChanged()), receiver: this, SLOT(_k_rebindScrollBars()));
284 }
285 }
286
287 if (m_horizontalScrollBar != horizontal.scrollBar) {
288 if (m_horizontalScrollBar) {
289 m_horizontalScrollBar->removeEventFilter(obj: this);
290 disconnect(m_horizontalChangedConnection);
291 }
292 m_horizontalScrollBar = horizontal.scrollBar;
293 if (horizontal.scrollBar) {
294 horizontal.scrollBar->installEventFilter(filterObj: this);
295 m_horizontalChangedConnection = connect(sender: horizontal.attached, SIGNAL(horizontalChanged()), receiver: this, SLOT(_k_rebindScrollBars()));
296 }
297 }
298}
299
300qreal WheelHandler::verticalStepSize() const
301{
302 return m_verticalStepSize;
303}
304
305void WheelHandler::setVerticalStepSize(qreal stepSize)
306{
307 m_explicitVStepSize = true;
308 if (qFuzzyCompare(p1: m_verticalStepSize, p2: stepSize)) {
309 return;
310 }
311 // Mimic the behavior of QQuickScrollBar when stepSize is 0
312 if (qFuzzyIsNull(d: stepSize)) {
313 resetVerticalStepSize();
314 return;
315 }
316 m_verticalStepSize = stepSize;
317 Q_EMIT verticalStepSizeChanged();
318}
319
320void WheelHandler::resetVerticalStepSize()
321{
322 m_explicitVStepSize = false;
323 if (qFuzzyCompare(p1: m_verticalStepSize, p2: m_defaultPixelStepSize)) {
324 return;
325 }
326 m_verticalStepSize = m_defaultPixelStepSize;
327 Q_EMIT verticalStepSizeChanged();
328}
329
330qreal WheelHandler::horizontalStepSize() const
331{
332 return m_horizontalStepSize;
333}
334
335void WheelHandler::setHorizontalStepSize(qreal stepSize)
336{
337 m_explicitHStepSize = true;
338 if (qFuzzyCompare(p1: m_horizontalStepSize, p2: stepSize)) {
339 return;
340 }
341 // Mimic the behavior of QQuickScrollBar when stepSize is 0
342 if (qFuzzyIsNull(d: stepSize)) {
343 resetHorizontalStepSize();
344 return;
345 }
346 m_horizontalStepSize = stepSize;
347 Q_EMIT horizontalStepSizeChanged();
348}
349
350void WheelHandler::resetHorizontalStepSize()
351{
352 m_explicitHStepSize = false;
353 if (qFuzzyCompare(p1: m_horizontalStepSize, p2: m_defaultPixelStepSize)) {
354 return;
355 }
356 m_horizontalStepSize = m_defaultPixelStepSize;
357 Q_EMIT horizontalStepSizeChanged();
358}
359
360Qt::KeyboardModifiers WheelHandler::pageScrollModifiers() const
361{
362 return m_pageScrollModifiers;
363}
364
365void WheelHandler::setPageScrollModifiers(Qt::KeyboardModifiers modifiers)
366{
367 if (m_pageScrollModifiers == modifiers) {
368 return;
369 }
370 m_pageScrollModifiers = modifiers;
371 Q_EMIT pageScrollModifiersChanged();
372}
373
374void WheelHandler::resetPageScrollModifiers()
375{
376 setPageScrollModifiers(m_defaultPageScrollModifiers);
377}
378
379bool WheelHandler::filterMouseEvents() const
380{
381 return m_filterMouseEvents;
382}
383
384void WheelHandler::setFilterMouseEvents(bool enabled)
385{
386 if (m_filterMouseEvents == enabled) {
387 return;
388 }
389 m_filterMouseEvents = enabled;
390 Q_EMIT filterMouseEventsChanged();
391}
392
393bool WheelHandler::keyNavigationEnabled() const
394{
395 return m_keyNavigationEnabled;
396}
397
398void WheelHandler::setKeyNavigationEnabled(bool enabled)
399{
400 if (m_keyNavigationEnabled == enabled) {
401 return;
402 }
403 m_keyNavigationEnabled = enabled;
404 Q_EMIT keyNavigationEnabledChanged();
405}
406
407void WheelHandler::classBegin()
408{
409 // Initializes smooth scrolling
410 m_engine = qmlEngine(this);
411 m_units = m_engine->singletonInstance<Kirigami::Platform::Units *>(uri: "org.kde.kirigami.platform", typeName: "Units");
412 m_settings = m_engine->singletonInstance<Kirigami::Platform::Settings *>(uri: "org.kde.kirigami.platform", typeName: "Settings");
413}
414
415void WheelHandler::componentComplete()
416{
417}
418
419void WheelHandler::setScrolling(bool scrolling)
420{
421 if (m_wheelScrolling == scrolling) {
422 if (m_wheelScrolling) {
423 m_wheelScrollingTimer.start();
424 }
425 return;
426 }
427 m_wheelScrolling = scrolling;
428 m_filterItem->setEnabled(m_wheelScrolling);
429}
430
431void WheelHandler::startInertiaScrolling()
432{
433 const qreal width = m_flickable->width();
434 const qreal height = m_flickable->height();
435 const qreal contentWidth = m_flickable->property(name: "contentWidth").toReal();
436 const qreal contentHeight = m_flickable->property(name: "contentHeight").toReal();
437 const qreal topMargin = m_flickable->property(name: "topMargin").toReal();
438 const qreal bottomMargin = m_flickable->property(name: "bottomMargin").toReal();
439 const qreal leftMargin = m_flickable->property(name: "leftMargin").toReal();
440 const qreal rightMargin = m_flickable->property(name: "rightMargin").toReal();
441 const qreal originX = m_flickable->property(name: "originX").toReal();
442 const qreal originY = m_flickable->property(name: "originY").toReal();
443 const qreal contentX = m_flickable->property(name: "contentX").toReal();
444 const qreal contentY = m_flickable->property(name: "contentY").toReal();
445
446 QPointF minExtent = QPointF(leftMargin, topMargin) - QPointF(originX, originY);
447 QPointF maxExtent = QPointF(width, height) - (QPointF(contentWidth, contentHeight) + QPointF(rightMargin, bottomMargin) + QPointF(originX, originY));
448
449 QPointF totalDelta(0, 0);
450 for (const QPoint delta : m_wheelEvents) {
451 totalDelta += delta;
452 }
453 const uint64_t elapsed = std::max<uint64_t>(a: m_timestamps.last() - m_timestamps.first(), b: 1);
454
455 // The inertia is more natural if we multiply
456 // the actual scrolling speed by some factor,
457 // chosen manually here to be 2.5. Otherwise, the
458 // scrolling will appear to be too slow.
459 const qreal speedFactor = 2.5;
460
461 // We get the velocity in px/s by calculating
462 // displacement / elapsed time; we multiply by
463 // 1000 since the elapsed time is in ms.
464 QPointF vel = -totalDelta * 1000 / elapsed * speedFactor;
465 QPointF startValue = QPointF(contentX, contentY);
466
467 // We decelerate at 4000px/s^2, chosen by manual test
468 // to be natural.
469 const qreal deceleration = 4000 * speedFactor;
470
471 // We use constant deceleration formulas to find:
472 // time = |velocity / deceleration|
473 // distance_traveled = time * velocity / 2
474 QPointF time = QPointF(qAbs(t: vel.x() / deceleration), qAbs(t: vel.y() / deceleration));
475 QPointF endValue = QPointF(startValue.x() + time.x() * vel.x() / 2, startValue.y() + time.y() * vel.y() / 2);
476
477 // We bound the end value so that we don't animate
478 // beyond the scrollable amount.
479 QPointF boundedEndValue =
480 QPointF(std::max(a: std::min(a: endValue.x(), b: -maxExtent.x()), b: -minExtent.x()), std::max(a: std::min(a: endValue.y(), b: -maxExtent.y()), b: -minExtent.y()));
481
482 // If we did bound the end value, we check how much
483 // (from 0 to 1) of the animation is actually played,
484 // and we adjust the time required for it accordingly.
485 QPointF progressFactor = QPointF((boundedEndValue.x() - startValue.x()) / (endValue.x() - startValue.x()),
486 (boundedEndValue.y() - startValue.y()) / (endValue.y() - startValue.y()));
487 // The formula here is:
488 // partial_time = complete_time * (1 - sqrt(1 - partial_progress_factor)),
489 // with partial_progress_factor being between 0 and 1.
490 // It can be obtained by inverting the OutQad easing formula,
491 // which is f(t) = t(2 - t).
492 // We also convert back from seconds to milliseconds.
493 QPointF realTime = QPointF(time.x() * (1 - std::sqrt(x: 1 - progressFactor.x())), time.y() * (1 - std::sqrt(x: 1 - progressFactor.y()))) * 1000;
494 m_wheelEvents.clear();
495 m_timestamps.clear();
496
497 m_xScrollAnimation.stop();
498 m_yScrollAnimation.stop();
499 if (realTime.x() > 0) {
500 m_xInertiaScrollAnimation.setStartValue(startValue.x());
501 m_xInertiaScrollAnimation.setEndValue(boundedEndValue.x());
502 m_xInertiaScrollAnimation.setDuration(realTime.x());
503 m_xInertiaScrollAnimation.start(policy: QAbstractAnimation::KeepWhenStopped);
504 }
505 if (realTime.y() > 0) {
506 m_yInertiaScrollAnimation.setStartValue(startValue.y());
507 m_yInertiaScrollAnimation.setEndValue(boundedEndValue.y());
508 m_yInertiaScrollAnimation.setDuration(realTime.y());
509 m_yInertiaScrollAnimation.start(policy: QAbstractAnimation::KeepWhenStopped);
510 }
511}
512
513bool WheelHandler::scrollFlickable(QPointF pixelDelta, QPointF angleDelta, Qt::KeyboardModifiers modifiers)
514{
515 if (!m_flickable || (pixelDelta.isNull() && angleDelta.isNull())) {
516 return false;
517 }
518
519 const qreal width = m_flickable->width();
520 const qreal height = m_flickable->height();
521 const qreal contentWidth = m_flickable->property(name: "contentWidth").toReal();
522 const qreal contentHeight = m_flickable->property(name: "contentHeight").toReal();
523 const qreal contentX = m_flickable->property(name: "contentX").toReal();
524 const qreal contentY = m_flickable->property(name: "contentY").toReal();
525 const qreal topMargin = m_flickable->property(name: "topMargin").toReal();
526 const qreal bottomMargin = m_flickable->property(name: "bottomMargin").toReal();
527 const qreal leftMargin = m_flickable->property(name: "leftMargin").toReal();
528 const qreal rightMargin = m_flickable->property(name: "rightMargin").toReal();
529 const qreal originX = m_flickable->property(name: "originX").toReal();
530 const qreal originY = m_flickable->property(name: "originY").toReal();
531 const qreal pageWidth = width - leftMargin - rightMargin;
532 const qreal pageHeight = height - topMargin - bottomMargin;
533 const auto window = m_flickable->window();
534 const auto screen = window ? window->screen() : nullptr;
535 const qreal devicePixelRatio = window != nullptr ? window->devicePixelRatio() : qGuiApp->devicePixelRatio();
536 const qreal refreshRate = screen ? screen->refreshRate() : 0;
537
538 // HACK: Only transpose deltas when not using xcb in order to not conflict with xcb's own delta transposing
539 if (modifiers & m_defaultHorizontalScrollModifiers && qGuiApp->platformName() != QLatin1String("xcb")) {
540 angleDelta = angleDelta.transposed();
541 pixelDelta = pixelDelta.transposed();
542 }
543
544 const qreal xTicks = angleDelta.x() / 120;
545 const qreal yTicks = angleDelta.y() / 120;
546 bool scrolled = false;
547
548 auto getChange = [pageScrollModifiers = modifiers & m_pageScrollModifiers](qreal ticks, qreal pixelDelta, qreal stepSize, qreal pageSize) {
549 // Use page size with pageScrollModifiers. Matches QScrollBar, which uses QAbstractSlider behavior.
550 if (pageScrollModifiers) {
551 return qBound(min: -pageSize, val: ticks * pageSize, max: pageSize);
552 } else if (pixelDelta != 0) {
553 return pixelDelta;
554 } else {
555 return ticks * stepSize;
556 }
557 };
558
559 auto getPosition = [devicePixelRatio](qreal size,
560 qreal contentSize,
561 qreal contentPos,
562 qreal originPos,
563 qreal pageSize,
564 qreal leadingMargin,
565 qreal trailingMargin,
566 qreal change,
567 const QPropertyAnimation &animation) {
568 if (contentSize <= pageSize) {
569 return contentPos;
570 }
571
572 // contentX and contentY use reversed signs from what x and y would normally use, so flip the signs
573
574 qreal minExtent = leadingMargin - originPos;
575 qreal maxExtent = size - (contentSize + trailingMargin + originPos);
576 qreal newContentPos = (animation.state() == QPropertyAnimation::Running ? animation.endValue().toReal() : contentPos) - change;
577 // bound the values without asserts
578 newContentPos = std::max(a: -minExtent, b: std::min(a: newContentPos, b: -maxExtent));
579
580 // Flickable::pixelAligned rounds the position, so round to mimic that behavior.
581 // Rounding prevents fractional positioning from causing text to be
582 // clipped off on the top and bottom.
583 // Multiply by devicePixelRatio before rounding and divide by devicePixelRatio
584 // after to make position match pixels on the screen more closely.
585 return std::round(x: newContentPos * devicePixelRatio) / devicePixelRatio;
586 };
587
588 auto setPosition = [this, devicePixelRatio, refreshRate](qreal oldPos, qreal newPos, qreal stepSize, const char *property, QPropertyAnimation &animation) {
589 animation.stop();
590 if (oldPos == newPos) {
591 return false;
592 }
593 if (!m_settings->smoothScroll() || !m_engine || refreshRate <= 0) {
594 animation.setDuration(0);
595 m_flickable->setProperty(name: property, value: newPos);
596 return true;
597 }
598
599 // Can't use wheelEvent->deviceType() to determine device type since
600 // on Wayland mouse is always regarded as touchpad:
601 // https://invent.kde.org/qt/qt/qtwayland/-/blob/e695a39519a7629c1549275a148cfb9ab99a07a9/src/client/qwaylandinputdevice.cpp#L445
602 // Mouse wheel can generate angle delta like 240, 360 and so on when
603 // scrolling very fast on some mice such as the Logitech M150.
604 // Mice with hi-res mouse wheels such as the Logitech MX Master 3 can
605 // generate angle deltas as small as 16.
606 // On X11, trackpads can also generate very fine angle deltas.
607
608 // Duration is based on the duration and movement for 120 angle delta.
609 // Shorten duration for smaller movements, limit duration for big movements.
610 // We don't want fine deltas to feel extra slow and fast scrolling should still feel fast.
611 // Minimum 3 frames for a 60hz display if delta > 2 physical pixels
612 // (start already rendered -> 1/3 rendered -> 2/3 rendered -> end rendered).
613 // Skip animation if <= 2 real frames for low refresh rate screens.
614 // Otherwise, we don't scale the duration based on refresh rate or
615 // device pixel ratio to avoid making the animation unexpectedly
616 // longer or shorter on different screens.
617
618 qreal absPixelDelta = std::abs(x: newPos - oldPos);
619 int duration = absPixelDelta * devicePixelRatio > 2 //
620 ? std::max(a: qCeil(v: 1000.0 / 60.0 * 3), b: std::min(a: qRound(d: absPixelDelta * m_units->longDuration() / stepSize), b: m_units->longDuration()))
621 : 0;
622 animation.setDuration(duration <= qCeil(v: 1000.0 / refreshRate * 2) ? 0 : duration);
623 if (animation.duration() > 0) {
624 animation.setStartValue(oldPos);
625 animation.setEndValue(newPos);
626 animation.start(policy: QAbstractAnimation::KeepWhenStopped);
627 } else {
628 m_flickable->setProperty(name: property, value: newPos);
629 }
630 return true;
631 };
632
633 qreal xChange = getChange(xTicks, pixelDelta.x(), m_horizontalStepSize, pageWidth);
634 qreal newContentX = getPosition(width, contentWidth, contentX, originX, pageWidth, leftMargin, rightMargin, xChange, m_xScrollAnimation);
635
636 qreal yChange = getChange(yTicks, pixelDelta.y(), m_verticalStepSize, pageHeight);
637 qreal newContentY = getPosition(height, contentHeight, contentY, originY, pageHeight, topMargin, bottomMargin, yChange, m_yScrollAnimation);
638
639 // Don't use `||` because we need the position to be set for contentX and contentY.
640 scrolled |= setPosition(contentX, newContentX, m_horizontalStepSize, "contentX", m_xScrollAnimation);
641 scrolled |= setPosition(contentY, newContentY, m_verticalStepSize, "contentY", m_yScrollAnimation);
642
643 return scrolled;
644}
645
646bool WheelHandler::scrollUp(qreal stepSize)
647{
648 if (qFuzzyIsNull(d: stepSize)) {
649 return false;
650 } else if (stepSize < 0) {
651 stepSize = m_verticalStepSize;
652 }
653 // contentY uses reversed sign
654 return scrollFlickable(pixelDelta: QPointF(0, stepSize));
655}
656
657bool WheelHandler::scrollDown(qreal stepSize)
658{
659 if (qFuzzyIsNull(d: stepSize)) {
660 return false;
661 } else if (stepSize < 0) {
662 stepSize = m_verticalStepSize;
663 }
664 // contentY uses reversed sign
665 return scrollFlickable(pixelDelta: QPointF(0, -stepSize));
666}
667
668bool WheelHandler::scrollLeft(qreal stepSize)
669{
670 if (qFuzzyIsNull(d: stepSize)) {
671 return false;
672 } else if (stepSize < 0) {
673 stepSize = m_horizontalStepSize;
674 }
675 // contentX uses reversed sign
676 return scrollFlickable(pixelDelta: QPoint(stepSize, 0));
677}
678
679bool WheelHandler::scrollRight(qreal stepSize)
680{
681 if (qFuzzyIsNull(d: stepSize)) {
682 return false;
683 } else if (stepSize < 0) {
684 stepSize = m_horizontalStepSize;
685 }
686 // contentX uses reversed sign
687 return scrollFlickable(pixelDelta: QPoint(-stepSize, 0));
688}
689
690bool WheelHandler::eventFilter(QObject *watched, QEvent *event)
691{
692 auto item = qobject_cast<QQuickItem *>(o: watched);
693 if (!item || !item->isEnabled()) {
694 return false;
695 }
696
697 // We only process keyboard events for QQuickScrollView.
698 const auto eventType = event->type();
699 if (item == m_scrollView && eventType != QEvent::KeyPress && eventType != QEvent::KeyRelease) {
700 return false;
701 }
702
703 qreal contentWidth = 0;
704 qreal contentHeight = 0;
705 qreal pageWidth = 0;
706 qreal pageHeight = 0;
707 if (m_flickable) {
708 contentWidth = m_flickable->property(name: "contentWidth").toReal();
709 contentHeight = m_flickable->property(name: "contentHeight").toReal();
710 pageWidth = m_flickable->width() - m_flickable->property(name: "leftMargin").toReal() - m_flickable->property(name: "rightMargin").toReal();
711 pageHeight = m_flickable->height() - m_flickable->property(name: "topMargin").toReal() - m_flickable->property(name: "bottomMargin").toReal();
712 }
713
714 // The code handling touch, mouse and hover events is mostly copied/adapted from QQuickScrollView::childMouseEventFilter()
715 switch (eventType) {
716 case QEvent::Wheel: {
717 // QQuickScrollBar::interactive handling Matches behavior in QQuickScrollView::eventFilter()
718 if (m_filterMouseEvents) {
719 if (m_verticalScrollBar) {
720 m_verticalScrollBar->setProperty(name: "interactive", value: true);
721 }
722 if (m_horizontalScrollBar) {
723 m_horizontalScrollBar->setProperty(name: "interactive", value: true);
724 }
725 }
726 QWheelEvent *wheelEvent = static_cast<QWheelEvent *>(event);
727
728 // NOTE: On X11 with libinput, pixelDelta is identical to angleDelta when using a mouse that shouldn't use pixelDelta.
729 // If faulty pixelDelta, reset pixelDelta to (0,0).
730 if (wheelEvent->pixelDelta() == wheelEvent->angleDelta()) {
731 // In order to change any of the data, we have to create a whole new QWheelEvent from its constructor.
732 QWheelEvent newWheelEvent(wheelEvent->position(),
733 wheelEvent->globalPosition(),
734 QPoint(0, 0), // pixelDelta
735 wheelEvent->angleDelta(),
736 wheelEvent->buttons(),
737 wheelEvent->modifiers(),
738 wheelEvent->phase(),
739 wheelEvent->inverted(),
740 wheelEvent->source());
741 m_kirigamiWheelEvent.initializeFromEvent(event: &newWheelEvent);
742 } else {
743 m_kirigamiWheelEvent.initializeFromEvent(event: wheelEvent);
744 }
745
746 Q_EMIT wheel(wheel: &m_kirigamiWheelEvent);
747
748 if (m_wheelEvents.count() > 6) {
749 m_wheelEvents.dequeue();
750 m_timestamps.dequeue();
751 }
752 if (m_wheelEvents.count() > 2 && wheelEvent->isEndEvent()) {
753 startInertiaScrolling();
754 } else {
755 m_wheelEvents.enqueue(t: wheelEvent->pixelDelta());
756 m_timestamps.enqueue(t: wheelEvent->timestamp());
757 }
758
759 if (m_kirigamiWheelEvent.isAccepted()) {
760 return true;
761 }
762
763 bool scrolled = false;
764 if (m_scrollFlickableTarget || (contentHeight <= pageHeight && contentWidth <= pageWidth)) {
765 // Don't use pixelDelta from the event unless angleDelta is not available
766 // because scrolling by pixelDelta is too slow on Wayland with libinput.
767 QPointF pixelDelta = m_kirigamiWheelEvent.angleDelta().isNull() ? m_kirigamiWheelEvent.pixelDelta() : QPoint(0, 0);
768 scrolled = scrollFlickable(pixelDelta, angleDelta: m_kirigamiWheelEvent.angleDelta(), modifiers: Qt::KeyboardModifiers(m_kirigamiWheelEvent.modifiers()));
769 }
770 setScrolling(scrolled);
771
772 // NOTE: Wheel events created by touchpad gestures with pixel deltas will cause scrolling to jump back
773 // to where scrolling started unless the event is always accepted before it reaches the Flickable.
774 bool flickableWillUseGestureScrolling = !(wheelEvent->source() == Qt::MouseEventNotSynthesized || wheelEvent->pixelDelta().isNull());
775 return scrolled || m_blockTargetWheel || flickableWillUseGestureScrolling;
776 }
777
778 case QEvent::TouchBegin: {
779 m_wasTouched = true;
780 if (!m_filterMouseEvents) {
781 break;
782 }
783 if (m_verticalScrollBar) {
784 m_verticalScrollBar->setProperty(name: "interactive", value: false);
785 }
786 if (m_horizontalScrollBar) {
787 m_horizontalScrollBar->setProperty(name: "interactive", value: false);
788 }
789 break;
790 }
791
792 case QEvent::TouchEnd: {
793 m_wasTouched = false;
794 break;
795 }
796
797 case QEvent::MouseButtonPress: {
798 // NOTE: Flickable does not handle touch events, only synthesized mouse events
799 m_wasTouched = static_cast<QMouseEvent *>(event)->source() != Qt::MouseEventNotSynthesized;
800 if (!m_filterMouseEvents) {
801 break;
802 }
803 if (!m_wasTouched) {
804 if (m_verticalScrollBar) {
805 m_verticalScrollBar->setProperty(name: "interactive", value: true);
806 }
807 if (m_horizontalScrollBar) {
808 m_horizontalScrollBar->setProperty(name: "interactive", value: true);
809 }
810 break;
811 }
812 return !m_wasTouched && item == m_flickable;
813 }
814
815 case QEvent::MouseMove:
816 case QEvent::MouseButtonRelease: {
817 setScrolling(false);
818 if (!m_filterMouseEvents) {
819 break;
820 }
821 if (static_cast<QMouseEvent *>(event)->source() == Qt::MouseEventNotSynthesized && item == m_flickable) {
822 return true;
823 }
824 break;
825 }
826
827 case QEvent::HoverEnter:
828 case QEvent::HoverMove: {
829 if (!m_filterMouseEvents) {
830 break;
831 }
832 if (m_wasTouched && (item == m_verticalScrollBar || item == m_horizontalScrollBar)) {
833 if (m_verticalScrollBar) {
834 m_verticalScrollBar->setProperty(name: "interactive", value: true);
835 }
836 if (m_horizontalScrollBar) {
837 m_horizontalScrollBar->setProperty(name: "interactive", value: true);
838 }
839 }
840 break;
841 }
842
843 case QEvent::KeyPress: {
844 if (!m_keyNavigationEnabled) {
845 break;
846 }
847 QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event);
848 bool horizontalScroll = keyEvent->modifiers() & m_defaultHorizontalScrollModifiers;
849 switch (keyEvent->key()) {
850 case Qt::Key_Up:
851 return scrollUp();
852 case Qt::Key_Down:
853 return scrollDown();
854 case Qt::Key_Left:
855 return scrollLeft();
856 case Qt::Key_Right:
857 return scrollRight();
858 case Qt::Key_PageUp:
859 return horizontalScroll ? scrollLeft(stepSize: pageWidth) : scrollUp(stepSize: pageHeight);
860 case Qt::Key_PageDown:
861 return horizontalScroll ? scrollRight(stepSize: pageWidth) : scrollDown(stepSize: pageHeight);
862 case Qt::Key_Home:
863 return horizontalScroll ? scrollLeft(stepSize: contentWidth) : scrollUp(stepSize: contentHeight);
864 case Qt::Key_End:
865 return horizontalScroll ? scrollRight(stepSize: contentWidth) : scrollDown(stepSize: contentHeight);
866 default:
867 break;
868 }
869 break;
870 }
871
872 default:
873 break;
874 }
875
876 return false;
877}
878
879#include "moc_wheelhandler.cpp"
880

source code of kirigami/src/wheelhandler.cpp