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 | |
15 | #include "platform/units.h" |
16 | |
17 | KirigamiWheelEvent::KirigamiWheelEvent(QObject *parent) |
18 | : QObject(parent) |
19 | { |
20 | } |
21 | |
22 | KirigamiWheelEvent::~KirigamiWheelEvent() |
23 | { |
24 | } |
25 | |
26 | void KirigamiWheelEvent::initializeFromEvent(QWheelEvent *event) |
27 | { |
28 | m_x = event->position().x(); |
29 | m_y = event->position().y(); |
30 | m_angleDelta = event->angleDelta(); |
31 | m_pixelDelta = event->pixelDelta(); |
32 | m_buttons = event->buttons(); |
33 | m_modifiers = event->modifiers(); |
34 | m_accepted = false; |
35 | m_inverted = event->inverted(); |
36 | } |
37 | |
38 | qreal KirigamiWheelEvent::x() const |
39 | { |
40 | return m_x; |
41 | } |
42 | |
43 | qreal KirigamiWheelEvent::y() const |
44 | { |
45 | return m_y; |
46 | } |
47 | |
48 | QPointF KirigamiWheelEvent::angleDelta() const |
49 | { |
50 | return m_angleDelta; |
51 | } |
52 | |
53 | QPointF KirigamiWheelEvent::pixelDelta() const |
54 | { |
55 | return m_pixelDelta; |
56 | } |
57 | |
58 | int KirigamiWheelEvent::buttons() const |
59 | { |
60 | return m_buttons; |
61 | } |
62 | |
63 | int KirigamiWheelEvent::modifiers() const |
64 | { |
65 | return m_modifiers; |
66 | } |
67 | |
68 | bool KirigamiWheelEvent::inverted() const |
69 | { |
70 | return m_inverted; |
71 | } |
72 | |
73 | bool KirigamiWheelEvent::isAccepted() |
74 | { |
75 | return m_accepted; |
76 | } |
77 | |
78 | void KirigamiWheelEvent::setAccepted(bool accepted) |
79 | { |
80 | m_accepted = accepted; |
81 | } |
82 | |
83 | /////////////////////////////// |
84 | |
85 | WheelFilterItem::WheelFilterItem(QQuickItem *parent) |
86 | : QQuickItem(parent) |
87 | { |
88 | setEnabled(false); |
89 | } |
90 | |
91 | /////////////////////////////// |
92 | |
93 | WheelHandler::WheelHandler(QObject *parent) |
94 | : QObject(parent) |
95 | , m_filterItem(new WheelFilterItem(nullptr)) |
96 | { |
97 | m_filterItem->installEventFilter(filterObj: this); |
98 | |
99 | m_wheelScrollingTimer.setSingleShot(true); |
100 | m_wheelScrollingTimer.setInterval(m_wheelScrollingDuration); |
101 | m_wheelScrollingTimer.callOnTimeout(args: [this]() { |
102 | setScrolling(false); |
103 | }); |
104 | |
105 | m_yScrollAnimation.setEasingCurve(QEasingCurve::OutCubic); |
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 | |
120 | WheelHandler::~WheelHandler() |
121 | { |
122 | delete m_filterItem; |
123 | } |
124 | |
125 | QQuickItem *WheelHandler::target() const |
126 | { |
127 | return m_flickable; |
128 | } |
129 | |
130 | void 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_yScrollAnimation.targetObject()) { |
150 | m_yScrollAnimation.stop(); |
151 | } |
152 | m_yScrollAnimation.setTargetObject(target); |
153 | |
154 | if (target) { |
155 | target->installEventFilter(filterObj: this); |
156 | |
157 | // Stack WheelFilterItem over the Flickable's scrollable content |
158 | m_filterItem->stackAfter(target->property(name: "contentItem" ).value<QQuickItem *>()); |
159 | // Make it fill the Flickable |
160 | m_filterItem->setWidth(target->width()); |
161 | m_filterItem->setHeight(target->height()); |
162 | connect(sender: target, signal: &QQuickItem::widthChanged, context: m_filterItem, slot: [this, target]() { |
163 | m_filterItem->setWidth(target->width()); |
164 | }); |
165 | connect(sender: target, signal: &QQuickItem::heightChanged, context: m_filterItem, slot: [this, target]() { |
166 | m_filterItem->setHeight(target->height()); |
167 | }); |
168 | } |
169 | |
170 | _k_rebindScrollBars(); |
171 | |
172 | Q_EMIT targetChanged(); |
173 | } |
174 | |
175 | void WheelHandler::_k_rebindScrollBars() |
176 | { |
177 | struct ScrollBarAttached { |
178 | QObject *attached = nullptr; |
179 | QQuickItem *vertical = nullptr; |
180 | QQuickItem *horizontal = nullptr; |
181 | }; |
182 | |
183 | ScrollBarAttached attachedToFlickable; |
184 | ScrollBarAttached attachedToScrollView; |
185 | |
186 | if (m_flickable) { |
187 | // Get ScrollBars so that we can filter them too, even if they're not |
188 | // in the bounds of the Flickable |
189 | const auto flickableChildren = m_flickable->children(); |
190 | for (const auto child : flickableChildren) { |
191 | if (child->inherits(classname: "QQuickScrollBarAttached" )) { |
192 | attachedToFlickable.attached = child; |
193 | attachedToFlickable.vertical = child->property(name: "vertical" ).value<QQuickItem *>(); |
194 | attachedToFlickable.horizontal = child->property(name: "horizontal" ).value<QQuickItem *>(); |
195 | break; |
196 | } |
197 | } |
198 | |
199 | // Check ScrollView if there are no scrollbars attached to the Flickable. |
200 | // We need to check if the parent inherits QQuickScrollView in case the |
201 | // parent is another Flickable that already has a Kirigami WheelHandler. |
202 | auto flickableParent = m_flickable->parentItem(); |
203 | if (flickableParent && flickableParent->inherits(classname: "QQuickScrollView" )) { |
204 | const auto siblings = flickableParent->children(); |
205 | for (const auto child : siblings) { |
206 | if (child->inherits(classname: "QQuickScrollBarAttached" )) { |
207 | attachedToScrollView.attached = child; |
208 | attachedToScrollView.vertical = child->property(name: "vertical" ).value<QQuickItem *>(); |
209 | attachedToScrollView.horizontal = child->property(name: "horizontal" ).value<QQuickItem *>(); |
210 | break; |
211 | } |
212 | } |
213 | } |
214 | } |
215 | |
216 | // Dilemma: ScrollBars can be attached to both ScrollView and Flickable, |
217 | // but only one of them should be shown anyway. Let's prefer Flickable. |
218 | |
219 | struct ChosenScrollBar { |
220 | QObject *attached = nullptr; |
221 | QQuickItem *scrollBar = nullptr; |
222 | }; |
223 | |
224 | ChosenScrollBar vertical; |
225 | if (attachedToFlickable.vertical) { |
226 | vertical.attached = attachedToFlickable.attached; |
227 | vertical.scrollBar = attachedToFlickable.vertical; |
228 | } else if (attachedToScrollView.vertical) { |
229 | vertical.attached = attachedToScrollView.attached; |
230 | vertical.scrollBar = attachedToScrollView.vertical; |
231 | } |
232 | |
233 | ChosenScrollBar horizontal; |
234 | if (attachedToFlickable.horizontal) { |
235 | horizontal.attached = attachedToFlickable.attached; |
236 | horizontal.scrollBar = attachedToFlickable.horizontal; |
237 | } else if (attachedToScrollView.horizontal) { |
238 | horizontal.attached = attachedToScrollView.attached; |
239 | horizontal.scrollBar = attachedToScrollView.horizontal; |
240 | } |
241 | |
242 | // Flickable may get re-parented to or out of a ScrollView, so we need to |
243 | // redo the discovery process. This is especially important for |
244 | // Kirigami.ScrollablePage component. |
245 | if (m_flickable) { |
246 | if (attachedToFlickable.horizontal && attachedToFlickable.vertical) { |
247 | // But if both scrollbars are already those from the preferred |
248 | // Flickable, there's no need for rediscovery. |
249 | disconnect(sender: m_flickable, signal: &QQuickItem::parentChanged, receiver: this, slot: &WheelHandler::_k_rebindScrollBars); |
250 | } else { |
251 | connect(sender: m_flickable, signal: &QQuickItem::parentChanged, context: this, slot: &WheelHandler::_k_rebindScrollBars, type: Qt::UniqueConnection); |
252 | } |
253 | } |
254 | |
255 | if (m_verticalScrollBar != vertical.scrollBar) { |
256 | if (m_verticalScrollBar) { |
257 | m_verticalScrollBar->removeEventFilter(obj: this); |
258 | disconnect(m_verticalChangedConnection); |
259 | } |
260 | m_verticalScrollBar = vertical.scrollBar; |
261 | if (vertical.scrollBar) { |
262 | vertical.scrollBar->installEventFilter(filterObj: this); |
263 | m_verticalChangedConnection = connect(sender: vertical.attached, SIGNAL(verticalChanged()), receiver: this, SLOT(_k_rebindScrollBars())); |
264 | } |
265 | } |
266 | |
267 | if (m_horizontalScrollBar != horizontal.scrollBar) { |
268 | if (m_horizontalScrollBar) { |
269 | m_horizontalScrollBar->removeEventFilter(obj: this); |
270 | disconnect(m_horizontalChangedConnection); |
271 | } |
272 | m_horizontalScrollBar = horizontal.scrollBar; |
273 | if (horizontal.scrollBar) { |
274 | horizontal.scrollBar->installEventFilter(filterObj: this); |
275 | m_horizontalChangedConnection = connect(sender: horizontal.attached, SIGNAL(horizontalChanged()), receiver: this, SLOT(_k_rebindScrollBars())); |
276 | } |
277 | } |
278 | } |
279 | |
280 | qreal WheelHandler::verticalStepSize() const |
281 | { |
282 | return m_verticalStepSize; |
283 | } |
284 | |
285 | void WheelHandler::setVerticalStepSize(qreal stepSize) |
286 | { |
287 | m_explicitVStepSize = true; |
288 | if (qFuzzyCompare(p1: m_verticalStepSize, p2: stepSize)) { |
289 | return; |
290 | } |
291 | // Mimic the behavior of QQuickScrollBar when stepSize is 0 |
292 | if (qFuzzyIsNull(d: stepSize)) { |
293 | resetVerticalStepSize(); |
294 | return; |
295 | } |
296 | m_verticalStepSize = stepSize; |
297 | Q_EMIT verticalStepSizeChanged(); |
298 | } |
299 | |
300 | void WheelHandler::resetVerticalStepSize() |
301 | { |
302 | m_explicitVStepSize = false; |
303 | if (qFuzzyCompare(p1: m_verticalStepSize, p2: m_defaultPixelStepSize)) { |
304 | return; |
305 | } |
306 | m_verticalStepSize = m_defaultPixelStepSize; |
307 | Q_EMIT verticalStepSizeChanged(); |
308 | } |
309 | |
310 | qreal WheelHandler::horizontalStepSize() const |
311 | { |
312 | return m_horizontalStepSize; |
313 | } |
314 | |
315 | void WheelHandler::setHorizontalStepSize(qreal stepSize) |
316 | { |
317 | m_explicitHStepSize = true; |
318 | if (qFuzzyCompare(p1: m_horizontalStepSize, p2: stepSize)) { |
319 | return; |
320 | } |
321 | // Mimic the behavior of QQuickScrollBar when stepSize is 0 |
322 | if (qFuzzyIsNull(d: stepSize)) { |
323 | resetHorizontalStepSize(); |
324 | return; |
325 | } |
326 | m_horizontalStepSize = stepSize; |
327 | Q_EMIT horizontalStepSizeChanged(); |
328 | } |
329 | |
330 | void WheelHandler::resetHorizontalStepSize() |
331 | { |
332 | m_explicitHStepSize = false; |
333 | if (qFuzzyCompare(p1: m_horizontalStepSize, p2: m_defaultPixelStepSize)) { |
334 | return; |
335 | } |
336 | m_horizontalStepSize = m_defaultPixelStepSize; |
337 | Q_EMIT horizontalStepSizeChanged(); |
338 | } |
339 | |
340 | Qt::KeyboardModifiers WheelHandler::pageScrollModifiers() const |
341 | { |
342 | return m_pageScrollModifiers; |
343 | } |
344 | |
345 | void WheelHandler::setPageScrollModifiers(Qt::KeyboardModifiers modifiers) |
346 | { |
347 | if (m_pageScrollModifiers == modifiers) { |
348 | return; |
349 | } |
350 | m_pageScrollModifiers = modifiers; |
351 | Q_EMIT pageScrollModifiersChanged(); |
352 | } |
353 | |
354 | void WheelHandler::resetPageScrollModifiers() |
355 | { |
356 | setPageScrollModifiers(m_defaultPageScrollModifiers); |
357 | } |
358 | |
359 | bool WheelHandler::filterMouseEvents() const |
360 | { |
361 | return m_filterMouseEvents; |
362 | } |
363 | |
364 | void WheelHandler::setFilterMouseEvents(bool enabled) |
365 | { |
366 | if (m_filterMouseEvents == enabled) { |
367 | return; |
368 | } |
369 | m_filterMouseEvents = enabled; |
370 | Q_EMIT filterMouseEventsChanged(); |
371 | } |
372 | |
373 | bool WheelHandler::keyNavigationEnabled() const |
374 | { |
375 | return m_keyNavigationEnabled; |
376 | } |
377 | |
378 | void WheelHandler::setKeyNavigationEnabled(bool enabled) |
379 | { |
380 | if (m_keyNavigationEnabled == enabled) { |
381 | return; |
382 | } |
383 | m_keyNavigationEnabled = enabled; |
384 | Q_EMIT keyNavigationEnabledChanged(); |
385 | } |
386 | |
387 | void WheelHandler::classBegin() |
388 | { |
389 | // Initializes smooth scrolling |
390 | m_engine = qmlEngine(this); |
391 | auto units = m_engine->singletonInstance<Kirigami::Platform::Units *>(uri: "org.kde.kirigami.platform" , typeName: "Units" ); |
392 | m_yScrollAnimation.setDuration(units->longDuration()); |
393 | connect(sender: units, signal: &Kirigami::Platform::Units::longDurationChanged, context: this, slot: [this] { |
394 | m_yScrollAnimation.setDuration(static_cast<Kirigami::Platform::Units *>(sender())->longDuration()); |
395 | }); |
396 | } |
397 | |
398 | void WheelHandler::componentComplete() |
399 | { |
400 | } |
401 | |
402 | void WheelHandler::setScrolling(bool scrolling) |
403 | { |
404 | if (m_wheelScrolling == scrolling) { |
405 | if (m_wheelScrolling) { |
406 | m_wheelScrollingTimer.start(); |
407 | } |
408 | return; |
409 | } |
410 | m_wheelScrolling = scrolling; |
411 | m_filterItem->setEnabled(m_wheelScrolling); |
412 | } |
413 | |
414 | bool WheelHandler::scrollFlickable(QPointF pixelDelta, QPointF angleDelta, Qt::KeyboardModifiers modifiers) |
415 | { |
416 | if (!m_flickable || (pixelDelta.isNull() && angleDelta.isNull())) { |
417 | return false; |
418 | } |
419 | |
420 | const qreal width = m_flickable->width(); |
421 | const qreal height = m_flickable->height(); |
422 | const qreal contentWidth = m_flickable->property(name: "contentWidth" ).toReal(); |
423 | const qreal contentHeight = m_flickable->property(name: "contentHeight" ).toReal(); |
424 | const qreal contentX = m_flickable->property(name: "contentX" ).toReal(); |
425 | const qreal contentY = m_flickable->property(name: "contentY" ).toReal(); |
426 | const qreal topMargin = m_flickable->property(name: "topMargin" ).toReal(); |
427 | const qreal bottomMargin = m_flickable->property(name: "bottomMargin" ).toReal(); |
428 | const qreal leftMargin = m_flickable->property(name: "leftMargin" ).toReal(); |
429 | const qreal rightMargin = m_flickable->property(name: "rightMargin" ).toReal(); |
430 | const qreal originX = m_flickable->property(name: "originX" ).toReal(); |
431 | const qreal originY = m_flickable->property(name: "originY" ).toReal(); |
432 | const qreal pageWidth = width - leftMargin - rightMargin; |
433 | const qreal pageHeight = height - topMargin - bottomMargin; |
434 | const auto window = m_flickable->window(); |
435 | const qreal devicePixelRatio = window != nullptr ? window->devicePixelRatio() : qGuiApp->devicePixelRatio(); |
436 | |
437 | // HACK: Only transpose deltas when not using xcb in order to not conflict with xcb's own delta transposing |
438 | if (modifiers & m_defaultHorizontalScrollModifiers && qGuiApp->platformName() != QLatin1String("xcb" )) { |
439 | angleDelta = angleDelta.transposed(); |
440 | pixelDelta = pixelDelta.transposed(); |
441 | } |
442 | |
443 | const qreal xTicks = angleDelta.x() / 120; |
444 | const qreal yTicks = angleDelta.y() / 120; |
445 | qreal xChange; |
446 | qreal yChange; |
447 | bool scrolled = false; |
448 | |
449 | // Scroll X |
450 | if (contentWidth > pageWidth) { |
451 | // Use page size with pageScrollModifiers. Matches QScrollBar, which uses QAbstractSlider behavior. |
452 | if (modifiers & m_pageScrollModifiers) { |
453 | xChange = qBound(min: -pageWidth, val: xTicks * pageWidth, max: pageWidth); |
454 | } else if (pixelDelta.x() != 0) { |
455 | xChange = pixelDelta.x(); |
456 | } else { |
457 | xChange = xTicks * m_horizontalStepSize; |
458 | } |
459 | |
460 | // contentX and contentY use reversed signs from what x and y would normally use, so flip the signs |
461 | |
462 | qreal minXExtent = leftMargin - originX; |
463 | qreal maxXExtent = width - (contentWidth + rightMargin + originX); |
464 | |
465 | qreal newContentX = qBound(min: -minXExtent, val: contentX - xChange, max: -maxXExtent); |
466 | // Flickable::pixelAligned rounds the position, so round to mimic that behavior. |
467 | // Rounding prevents fractional positioning from causing text to be |
468 | // clipped off on the top and bottom. |
469 | // Multiply by devicePixelRatio before rounding and divide by devicePixelRatio |
470 | // after to make position match pixels on the screen more closely. |
471 | newContentX = std::round(x: newContentX * devicePixelRatio) / devicePixelRatio; |
472 | if (contentX != newContentX) { |
473 | scrolled = true; |
474 | m_flickable->setProperty(name: "contentX" , value: newContentX); |
475 | } |
476 | } |
477 | |
478 | // Scroll Y |
479 | if (contentHeight > pageHeight) { |
480 | if (modifiers & m_pageScrollModifiers) { |
481 | yChange = qBound(min: -pageHeight, val: yTicks * pageHeight, max: pageHeight); |
482 | } else if (pixelDelta.y() != 0) { |
483 | yChange = pixelDelta.y(); |
484 | } else { |
485 | yChange = yTicks * m_verticalStepSize; |
486 | } |
487 | |
488 | // contentX and contentY use reversed signs from what x and y would normally use, so flip the signs |
489 | |
490 | qreal minYExtent = topMargin - originY; |
491 | qreal maxYExtent = height - (contentHeight + bottomMargin + originY); |
492 | |
493 | qreal newContentY; |
494 | if (m_yScrollAnimation.state() == QPropertyAnimation::Running) { |
495 | m_yScrollAnimation.stop(); |
496 | newContentY = std::clamp(val: m_yScrollAnimation.endValue().toReal() + -yChange, lo: -minYExtent, hi: -maxYExtent); |
497 | } else { |
498 | newContentY = std::clamp(val: contentY - yChange, lo: -minYExtent, hi: -maxYExtent); |
499 | } |
500 | |
501 | // Flickable::pixelAligned rounds the position, so round to mimic that behavior. |
502 | // Rounding prevents fractional positioning from causing text to be |
503 | // clipped off on the top and bottom. |
504 | // Multiply by devicePixelRatio before rounding and divide by devicePixelRatio |
505 | // after to make position match pixels on the screen more closely. |
506 | newContentY = std::round(x: newContentY * devicePixelRatio) / devicePixelRatio; |
507 | if (contentY != newContentY) { |
508 | scrolled = true; |
509 | if (m_wasTouched || !m_engine) { |
510 | m_flickable->setProperty(name: "contentY" , value: newContentY); |
511 | } else { |
512 | m_yScrollAnimation.setEndValue(newContentY); |
513 | m_yScrollAnimation.start(policy: QAbstractAnimation::KeepWhenStopped); |
514 | } |
515 | } |
516 | } |
517 | |
518 | return scrolled; |
519 | } |
520 | |
521 | bool WheelHandler::scrollUp(qreal stepSize) |
522 | { |
523 | if (qFuzzyIsNull(d: stepSize)) { |
524 | return false; |
525 | } else if (stepSize < 0) { |
526 | stepSize = m_verticalStepSize; |
527 | } |
528 | // contentY uses reversed sign |
529 | return scrollFlickable(pixelDelta: QPointF(0, stepSize)); |
530 | } |
531 | |
532 | bool WheelHandler::scrollDown(qreal stepSize) |
533 | { |
534 | if (qFuzzyIsNull(d: stepSize)) { |
535 | return false; |
536 | } else if (stepSize < 0) { |
537 | stepSize = m_verticalStepSize; |
538 | } |
539 | // contentY uses reversed sign |
540 | return scrollFlickable(pixelDelta: QPointF(0, -stepSize)); |
541 | } |
542 | |
543 | bool WheelHandler::scrollLeft(qreal stepSize) |
544 | { |
545 | if (qFuzzyIsNull(d: stepSize)) { |
546 | return false; |
547 | } else if (stepSize < 0) { |
548 | stepSize = m_horizontalStepSize; |
549 | } |
550 | // contentX uses reversed sign |
551 | return scrollFlickable(pixelDelta: QPoint(stepSize, 0)); |
552 | } |
553 | |
554 | bool WheelHandler::scrollRight(qreal stepSize) |
555 | { |
556 | if (qFuzzyIsNull(d: stepSize)) { |
557 | return false; |
558 | } else if (stepSize < 0) { |
559 | stepSize = m_horizontalStepSize; |
560 | } |
561 | // contentX uses reversed sign |
562 | return scrollFlickable(pixelDelta: QPoint(-stepSize, 0)); |
563 | } |
564 | |
565 | bool WheelHandler::eventFilter(QObject *watched, QEvent *event) |
566 | { |
567 | auto item = qobject_cast<QQuickItem *>(o: watched); |
568 | if (!item || !item->isEnabled()) { |
569 | return false; |
570 | } |
571 | |
572 | qreal contentWidth = 0; |
573 | qreal contentHeight = 0; |
574 | qreal pageWidth = 0; |
575 | qreal pageHeight = 0; |
576 | if (m_flickable) { |
577 | contentWidth = m_flickable->property(name: "contentWidth" ).toReal(); |
578 | contentHeight = m_flickable->property(name: "contentHeight" ).toReal(); |
579 | pageWidth = m_flickable->width() - m_flickable->property(name: "leftMargin" ).toReal() - m_flickable->property(name: "rightMargin" ).toReal(); |
580 | pageHeight = m_flickable->height() - m_flickable->property(name: "topMargin" ).toReal() - m_flickable->property(name: "bottomMargin" ).toReal(); |
581 | } |
582 | |
583 | // The code handling touch, mouse and hover events is mostly copied/adapted from QQuickScrollView::childMouseEventFilter() |
584 | switch (event->type()) { |
585 | case QEvent::Wheel: { |
586 | // QQuickScrollBar::interactive handling Matches behavior in QQuickScrollView::eventFilter() |
587 | if (m_filterMouseEvents) { |
588 | if (m_verticalScrollBar) { |
589 | m_verticalScrollBar->setProperty(name: "interactive" , value: true); |
590 | } |
591 | if (m_horizontalScrollBar) { |
592 | m_horizontalScrollBar->setProperty(name: "interactive" , value: true); |
593 | } |
594 | } |
595 | QWheelEvent *wheelEvent = static_cast<QWheelEvent *>(event); |
596 | |
597 | // Can't use wheelEvent->deviceType() to determine device type since on Wayland mouse is always regarded as touchpad |
598 | // https://invent.kde.org/qt/qt/qtwayland/-/blob/e695a39519a7629c1549275a148cfb9ab99a07a9/src/client/qwaylandinputdevice.cpp#L445 |
599 | // and we can only expect a touchpad never generates the same angle delta as a mouse |
600 | |
601 | // mouse wheel can also generate angle delta like 240, 360 and so on when scrolling very fast |
602 | // only checking wheelEvent->angleDelta().y() because we only animate for contentY |
603 | m_wasTouched = (std::abs(x: wheelEvent->angleDelta().y()) != 0 && std::abs(x: wheelEvent->angleDelta().y()) % 120 != 0); |
604 | // NOTE: On X11 with libinput, pixelDelta is identical to angleDelta when using a mouse that shouldn't use pixelDelta. |
605 | // If faulty pixelDelta, reset pixelDelta to (0,0). |
606 | if (wheelEvent->pixelDelta() == wheelEvent->angleDelta()) { |
607 | // In order to change any of the data, we have to create a whole new QWheelEvent from its constructor. |
608 | QWheelEvent newWheelEvent(wheelEvent->position(), |
609 | wheelEvent->globalPosition(), |
610 | QPoint(0, 0), // pixelDelta |
611 | wheelEvent->angleDelta(), |
612 | wheelEvent->buttons(), |
613 | wheelEvent->modifiers(), |
614 | wheelEvent->phase(), |
615 | wheelEvent->inverted(), |
616 | wheelEvent->source()); |
617 | m_kirigamiWheelEvent.initializeFromEvent(event: &newWheelEvent); |
618 | } else { |
619 | m_kirigamiWheelEvent.initializeFromEvent(event: wheelEvent); |
620 | } |
621 | |
622 | Q_EMIT wheel(wheel: &m_kirigamiWheelEvent); |
623 | |
624 | if (m_kirigamiWheelEvent.isAccepted()) { |
625 | return true; |
626 | } |
627 | |
628 | bool scrolled = false; |
629 | if (m_scrollFlickableTarget || (contentHeight <= pageHeight && contentWidth <= pageWidth)) { |
630 | // Don't use pixelDelta from the event unless angleDelta is not available |
631 | // because scrolling by pixelDelta is too slow on Wayland with libinput. |
632 | QPointF pixelDelta = m_kirigamiWheelEvent.angleDelta().isNull() ? m_kirigamiWheelEvent.pixelDelta() : QPoint(0, 0); |
633 | scrolled = scrollFlickable(pixelDelta, angleDelta: m_kirigamiWheelEvent.angleDelta(), modifiers: Qt::KeyboardModifiers(m_kirigamiWheelEvent.modifiers())); |
634 | } |
635 | setScrolling(scrolled); |
636 | |
637 | // NOTE: Wheel events created by touchpad gestures with pixel deltas will cause scrolling to jump back |
638 | // to where scrolling started unless the event is always accepted before it reaches the Flickable. |
639 | bool flickableWillUseGestureScrolling = !(wheelEvent->source() == Qt::MouseEventNotSynthesized || wheelEvent->pixelDelta().isNull()); |
640 | return scrolled || m_blockTargetWheel || flickableWillUseGestureScrolling; |
641 | } |
642 | |
643 | case QEvent::TouchBegin: { |
644 | m_wasTouched = true; |
645 | if (!m_filterMouseEvents) { |
646 | break; |
647 | } |
648 | if (m_verticalScrollBar) { |
649 | m_verticalScrollBar->setProperty(name: "interactive" , value: false); |
650 | } |
651 | if (m_horizontalScrollBar) { |
652 | m_horizontalScrollBar->setProperty(name: "interactive" , value: false); |
653 | } |
654 | break; |
655 | } |
656 | |
657 | case QEvent::TouchEnd: { |
658 | m_wasTouched = false; |
659 | break; |
660 | } |
661 | |
662 | case QEvent::MouseButtonPress: { |
663 | // NOTE: Flickable does not handle touch events, only synthesized mouse events |
664 | m_wasTouched = static_cast<QMouseEvent *>(event)->source() != Qt::MouseEventNotSynthesized; |
665 | if (!m_filterMouseEvents) { |
666 | break; |
667 | } |
668 | if (!m_wasTouched) { |
669 | if (m_verticalScrollBar) { |
670 | m_verticalScrollBar->setProperty(name: "interactive" , value: true); |
671 | } |
672 | if (m_horizontalScrollBar) { |
673 | m_horizontalScrollBar->setProperty(name: "interactive" , value: true); |
674 | } |
675 | break; |
676 | } |
677 | return !m_wasTouched && item == m_flickable; |
678 | } |
679 | |
680 | case QEvent::MouseMove: |
681 | case QEvent::MouseButtonRelease: { |
682 | setScrolling(false); |
683 | if (!m_filterMouseEvents) { |
684 | break; |
685 | } |
686 | if (static_cast<QMouseEvent *>(event)->source() == Qt::MouseEventNotSynthesized && item == m_flickable) { |
687 | return true; |
688 | } |
689 | break; |
690 | } |
691 | |
692 | case QEvent::HoverEnter: |
693 | case QEvent::HoverMove: { |
694 | if (!m_filterMouseEvents) { |
695 | break; |
696 | } |
697 | if (m_wasTouched && (item == m_verticalScrollBar || item == m_horizontalScrollBar)) { |
698 | if (m_verticalScrollBar) { |
699 | m_verticalScrollBar->setProperty(name: "interactive" , value: true); |
700 | } |
701 | if (m_horizontalScrollBar) { |
702 | m_horizontalScrollBar->setProperty(name: "interactive" , value: true); |
703 | } |
704 | } |
705 | break; |
706 | } |
707 | |
708 | case QEvent::KeyPress: { |
709 | if (!m_keyNavigationEnabled) { |
710 | break; |
711 | } |
712 | QKeyEvent *keyEvent = static_cast<QKeyEvent *>(event); |
713 | bool horizontalScroll = keyEvent->modifiers() & m_defaultHorizontalScrollModifiers; |
714 | switch (keyEvent->key()) { |
715 | case Qt::Key_Up: |
716 | return scrollUp(); |
717 | case Qt::Key_Down: |
718 | return scrollDown(); |
719 | case Qt::Key_Left: |
720 | return scrollLeft(); |
721 | case Qt::Key_Right: |
722 | return scrollRight(); |
723 | case Qt::Key_PageUp: |
724 | return horizontalScroll ? scrollLeft(stepSize: pageWidth) : scrollUp(stepSize: pageHeight); |
725 | case Qt::Key_PageDown: |
726 | return horizontalScroll ? scrollRight(stepSize: pageWidth) : scrollDown(stepSize: pageHeight); |
727 | case Qt::Key_Home: |
728 | return horizontalScroll ? scrollLeft(stepSize: contentWidth) : scrollUp(stepSize: contentHeight); |
729 | case Qt::Key_End: |
730 | return horizontalScroll ? scrollRight(stepSize: contentWidth) : scrollDown(stepSize: contentHeight); |
731 | default: |
732 | break; |
733 | } |
734 | break; |
735 | } |
736 | |
737 | default: |
738 | break; |
739 | } |
740 | |
741 | return false; |
742 | } |
743 | |
744 | #include "moc_wheelhandler.cpp" |
745 | |