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

source code of kirigami/src/wheelhandler.cpp