1/* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
2 * SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
3 * SPDX-License-Identifier: LGPL-2.0-or-later
4 */
5
6#pragma once
7
8#include <QGuiApplication>
9#include <QObject>
10#include <QPoint>
11#include <QPropertyAnimation>
12#include <QQmlParserStatus>
13#include <QQueue>
14#include <QQuickItem>
15#include <QStyleHints>
16#include <QTimer>
17
18#include "platform/settings.h"
19#include "platform/units.h"
20
21class QWheelEvent;
22class QQmlEngine;
23class WheelHandler;
24
25/*!
26 * \qmltype WheelEvent
27 * \inqmlmodule org.kde.kirigami
28 *
29 * \brief Describes the mouse wheel event.
30 */
31class KirigamiWheelEvent : public QObject
32{
33 Q_OBJECT
34 QML_NAMED_ELEMENT(WheelEvent)
35 QML_UNCREATABLE("")
36
37 /*!
38 * \qmlproperty double WheelEvent::x
39 *
40 * X coordinate of the mouse pointer.
41 */
42 Q_PROPERTY(qreal x READ x CONSTANT FINAL)
43
44 /*!
45 * \qmlproperty double WheelEvent::y
46 *
47 * Y coordinate of the mouse pointer.
48 */
49 Q_PROPERTY(qreal y READ y CONSTANT FINAL)
50
51 /*!
52 * \qmlproperty point WheelEvent::angleDelta
53 *
54 * The distance the wheel is rotated in degrees.
55 * The x and y coordinates indicate the horizontal and vertical wheels respectively.
56 * A positive value indicates it was rotated up/right, negative, bottom/left
57 * This value is more likely to be set in traditional mice.
58 */
59 Q_PROPERTY(QPointF angleDelta READ angleDelta CONSTANT FINAL)
60
61 /*!
62 * \qmlproperty point WheelEvent::pixelDelta
63 *
64 * Provides the delta in screen pixels available on high resolution trackpads.
65 */
66 Q_PROPERTY(QPointF pixelDelta READ pixelDelta CONSTANT FINAL)
67
68 /*!
69 * \qmlproperty int WheelEvent::buttons
70 *
71 * It contains an OR combination of the buttons that were pressed during the wheel, they can be:
72 * \list
73 * \li Qt.LeftButton
74 * \li Qt.MiddleButton
75 * \li Qt.RightButton
76 * \endlist
77 */
78 Q_PROPERTY(int buttons READ buttons CONSTANT FINAL)
79
80 /*!
81 * \qmlproperty int WheelEvent::modifiers
82 *
83 * Keyboard modifiers that were pressed during the wheel event, such as:
84 * Qt.NoModifier (default, no modifiers)
85 * Qt.ControlModifier
86 * Qt.ShiftModifier
87 * ...
88 */
89 Q_PROPERTY(int modifiers READ modifiers CONSTANT FINAL)
90
91 /*!
92 * \qmlproperty bool WheelEvent::inverted
93 *
94 * Whether the delta values are inverted
95 * On some platformsthe returned delta are inverted, so positive values would mean bottom/left
96 */
97 Q_PROPERTY(bool inverted READ inverted CONSTANT FINAL)
98
99 /*!
100 * \qmlproperty bool WheelEvent::accepted
101 *
102 * If set, the event shouldn't be managed anymore,
103 * for instance it can be used to block the handler to manage the scroll of a view on some scenarios.
104 * \code
105 * // This handler handles automatically the scroll of
106 * // flickableItem, unless Ctrl is pressed, in this case the
107 * // app has custom code to handle Ctrl+wheel zooming
108 * Kirigami.WheelHandler {
109 * target: flickableItem
110 * blockTargetWheel: true
111 * scrollFlickableTarget: true
112 * onWheel: {
113 * if (wheel.modifiers & Qt.ControlModifier) {
114 * wheel.accepted = true;
115 * // Handle scaling of the view
116 * }
117 * }
118 * }
119 * \endcode
120 *
121 */
122 Q_PROPERTY(bool accepted READ isAccepted WRITE setAccepted FINAL)
123
124public:
125 KirigamiWheelEvent(QObject *parent = nullptr);
126 ~KirigamiWheelEvent() override;
127
128 void initializeFromEvent(QWheelEvent *event);
129
130 qreal x() const;
131 qreal y() const;
132 QPointF angleDelta() const;
133 QPointF pixelDelta() const;
134 int buttons() const;
135 int modifiers() const;
136 bool inverted() const;
137 bool isAccepted();
138 void setAccepted(bool accepted);
139
140private:
141 qreal m_x = 0;
142 qreal m_y = 0;
143 QPointF m_angleDelta;
144 QPointF m_pixelDelta;
145 Qt::MouseButtons m_buttons = Qt::NoButton;
146 Qt::KeyboardModifiers m_modifiers = Qt::NoModifier;
147 bool m_inverted = false;
148 bool m_accepted = false;
149};
150
151class WheelFilterItem : public QQuickItem
152{
153 Q_OBJECT
154public:
155 WheelFilterItem(QQuickItem *parent = nullptr);
156};
157
158/*!
159 * \qmltype WheelHandler
160 * \inqmlmodule org.kde.kirigami
161 *
162 * \brief Handles scrolling for a Flickable and 2 attached ScrollBars.
163 *
164 * WheelHandler filters events from a Flickable, a vertical ScrollBar and a horizontal ScrollBar.
165 * Wheel and KeyPress events (when keyNavigationEnabled is true) are used to scroll the Flickable.
166 * When filterMouseEvents is true, WheelHandler blocks mouse button input from reaching the Flickable
167 * and sets the interactive property of the scrollbars to false when touch input is used.
168 *
169 * Wheel event handling behavior:
170 * \list
171 * \li Pixel delta is ignored unless angle delta is not available because pixel delta scrolling is too slow. Qt Widgets doesn't use pixel delta either, so the
172 * default scroll speed should be consistent with Qt Widgets.
173 * \li When using angle delta, scroll using the step increments defined by verticalStepSize and horizontalStepSize.
174 * \li When one of the keyboard modifiers in pageScrollModifiers is used, scroll by pages.
175 * \li When using a device that doesn't use 120 angle delta unit increments such as a touchpad, the verticalStepSize, horizontalStepSize and page increments
176 * (if using page scrolling) will be multiplied by \c{angle delta / 120} to keep scrolling smooth.
177 * \li If scrolling has happened in the last 400ms, use an internal QQuickItem stacked over the Flickable's contentItem to catch wheel events and use those
178 * wheel events to scroll, if possible. This prevents controls inside the Flickable's contentItem that allow scrolling to change the value (e.g., Sliders,
179 * SpinBoxes) from conflicting with scrolling the page. \endlist
180 *
181 * Common usage with a Flickable:
182 *
183 * \quotefile wheelhandler/FlickableUsage.qml
184 *
185 * Common usage inside of a ScrollView template:
186 *
187 * \quotefile wheelhandler/ScrollViewUsage.qml
188 *
189 */
190class WheelHandler : public QObject, public QQmlParserStatus
191{
192 Q_OBJECT
193 Q_INTERFACES(QQmlParserStatus)
194 QML_ELEMENT
195
196 /*!
197 * \qmlproperty Item WheelHandler::target
198 *
199 * \brief This property holds the Qt Quick Flickable that the WheelHandler will control.
200 */
201 Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged FINAL)
202
203 /*!
204 * \qmlproperty double WheelHandler::verticalStepSize
205 *
206 * \brief This property holds the vertical step size.
207 *
208 * The default value is equivalent to \c{20 * Qt.styleHints.wheelScrollLines}. This is consistent with the default increment for QScrollArea.
209 *
210 * \sa horizontalStepSize
211 *
212 * \since 5.89
213 */
214 Q_PROPERTY(qreal verticalStepSize READ verticalStepSize WRITE setVerticalStepSize RESET resetVerticalStepSize NOTIFY verticalStepSizeChanged FINAL)
215
216 /*!
217 * \qmlproperty double WheelHandler::horizontalStepSize
218 *
219 * \brief This property holds the horizontal step size.
220 *
221 * The default value is equivalent to \c{20 * Qt.styleHints.wheelScrollLines}. This is consistent with the default increment for QScrollArea.
222 *
223 * \sa verticalStepSize
224 *
225 * \since 5.89
226 */
227 Q_PROPERTY(
228 qreal horizontalStepSize READ horizontalStepSize WRITE setHorizontalStepSize RESET resetHorizontalStepSize NOTIFY horizontalStepSizeChanged FINAL)
229
230 /*!
231 * \qmlproperty int WheelHandler::pageScrollModifiers
232 *
233 * \brief This property holds the keyboard modifiers that will be used to start page scrolling.
234 *
235 * The default value is equivalent to \c{Qt.ControlModifier | Qt.ShiftModifier}. This matches QScrollBar, which uses QAbstractSlider behavior.
236 *
237 * \since 5.89
238 */
239 Q_PROPERTY(Qt::KeyboardModifiers pageScrollModifiers READ pageScrollModifiers WRITE setPageScrollModifiers RESET resetPageScrollModifiers NOTIFY
240 pageScrollModifiersChanged FINAL)
241
242 /*!
243 * \qmlproperty bool WheelHandler::filterMouseEvents
244 *
245 * \brief This property holds whether the WheelHandler filters mouse events like a Qt Quick Controls ScrollView would.
246 *
247 * Touch events are allowed to flick the view and they make the scrollbars not interactive.
248 *
249 * Mouse events are not allowed to flick the view and they make the scrollbars interactive.
250 *
251 * Hover events on the scrollbars and wheel events on anything also make the scrollbars interactive when this property is set to true.
252 *
253 * The default value is \c false.
254 *
255 * \since 5.89
256 */
257 Q_PROPERTY(bool filterMouseEvents READ filterMouseEvents WRITE setFilterMouseEvents NOTIFY filterMouseEventsChanged FINAL)
258
259 /*!
260 * \qmlproperty bool WheelHandler::keyNavigationEnabled
261 *
262 * \brief This property holds whether the WheelHandler handles keyboard scrolling.
263 *
264 * \list
265 * \li Left arrow scrolls a step to the left.
266 * \li Right arrow scrolls a step to the right.
267 * \li Up arrow scrolls a step upwards.
268 * \li Down arrow scrolls a step downwards.
269 * \li PageUp scrolls to the previous page.
270 * \li PageDown scrolls to the next page.
271 * \li Home scrolls to the beginning.
272 * \li End scrolls to the end.
273 * \li When Alt is held, scroll horizontally when using PageUp, PageDown, Home or End.
274 * \endlist
275 *
276 * The default value is \c false.
277 *
278 * \since 5.89
279 */
280 Q_PROPERTY(bool keyNavigationEnabled READ keyNavigationEnabled WRITE setKeyNavigationEnabled NOTIFY keyNavigationEnabledChanged FINAL)
281
282 /*!
283 * \qmlproperty bool WheelHandler::blockTargetWheel
284 *
285 * \brief This property holds whether the WheelHandler blocks all wheel events from reaching the Flickable.
286 *
287 * When this property is false, scrolling the Flickable with WheelHandler will only block an event from reaching the Flickable if the Flickable is actually
288 * scrolled by WheelHandler.
289 *
290 * \note Wheel events created by touchpad gestures with pixel deltas will always be accepted no matter what. This is because they will cause the Flickable
291 * to jump back to where scrolling started unless the events are always accepted before they reach the Flickable.
292 *
293 * The default value is true.
294 */
295 Q_PROPERTY(bool blockTargetWheel MEMBER m_blockTargetWheel NOTIFY blockTargetWheelChanged FINAL)
296
297 /*!
298 * \qmlproperty bool WheelHandler::scrollFlickableTarget
299 *
300 * \brief This property holds whether the WheelHandler can use wheel events to scroll the Flickable.
301 *
302 * The default value is true.
303 */
304 Q_PROPERTY(bool scrollFlickableTarget MEMBER m_scrollFlickableTarget NOTIFY scrollFlickableTargetChanged FINAL)
305
306public:
307 explicit WheelHandler(QObject *parent = nullptr);
308 ~WheelHandler() override;
309
310 QQuickItem *target() const;
311 void setTarget(QQuickItem *target);
312
313 qreal verticalStepSize() const;
314 void setVerticalStepSize(qreal stepSize);
315 void resetVerticalStepSize();
316
317 qreal horizontalStepSize() const;
318 void setHorizontalStepSize(qreal stepSize);
319 void resetHorizontalStepSize();
320
321 Qt::KeyboardModifiers pageScrollModifiers() const;
322 void setPageScrollModifiers(Qt::KeyboardModifiers modifiers);
323 void resetPageScrollModifiers();
324
325 bool filterMouseEvents() const;
326 void setFilterMouseEvents(bool enabled);
327
328 bool keyNavigationEnabled() const;
329 void setKeyNavigationEnabled(bool enabled);
330
331 /*!
332 * \qmlmethod bool WheelHandler::scrollUp(double stepSize = -1)
333 *
334 * Scroll up one step. If the stepSize parameter is less than 0, the verticalStepSize will be used.
335 *
336 * returns true if the contentItem was moved.
337 *
338 * \since 5.89
339 */
340 Q_INVOKABLE bool scrollUp(qreal stepSize = -1);
341
342 /*!
343 * \qmlmethod bool WheelHandler::scrollDown(double stepSize = -1)
344 *
345 * Scroll down one step. If the stepSize parameter is less than 0, the verticalStepSize will be used.
346 *
347 * returns true if the contentItem was moved.
348 *
349 * \since 5.89
350 */
351 Q_INVOKABLE bool scrollDown(qreal stepSize = -1);
352
353 /*!
354 * \qmlmethod bool WheelHandler::scrollLeft(double stepSize = -1)
355 *
356 * Scroll left one step. If the \a stepSize is less than 0, the horizontalStepSize will be used.
357 *
358 * returns true if the contentItem was moved.
359 *
360 * \since 5.89
361 */
362 Q_INVOKABLE bool scrollLeft(qreal stepSize = -1);
363
364 /*!
365 * \qmlmethod bool WheelHandler::scrollRight(double stepSize = -1)
366 *
367 * Scroll right one step. If the stepSize parameter is less than 0, the horizontalStepSize will be used.
368 *
369 * returns true if the contentItem was moved.
370 *
371 * \since 5.89
372 */
373 Q_INVOKABLE bool scrollRight(qreal stepSize = -1);
374
375Q_SIGNALS:
376 void targetChanged();
377 void verticalStepSizeChanged();
378 void horizontalStepSizeChanged();
379 void pageScrollModifiersChanged();
380 void filterMouseEventsChanged();
381 void keyNavigationEnabledChanged();
382 void blockTargetWheelChanged();
383 void scrollFlickableTargetChanged();
384
385 /*!
386 * \qmlsignal WheelHandler::wheel(WheelEvent wheel)
387 *
388 * \brief This signal is emitted when a wheel event reaches the event filter, just before scrolling is handled.
389 *
390 * Accepting the wheel event in the \c onWheel signal handler prevents scrolling from happening.
391 */
392 void wheel(KirigamiWheelEvent *wheel);
393
394protected:
395 bool eventFilter(QObject *watched, QEvent *event) override;
396
397private Q_SLOTS:
398 void _k_rebindScrollBars();
399
400private:
401 void classBegin() override;
402 void componentComplete() override;
403
404 void setScrolling(bool scrolling);
405 void startInertiaScrolling();
406 bool scrollFlickable(QPointF pixelDelta, QPointF angleDelta = {}, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
407
408 Kirigami::Platform::Units *m_units = nullptr;
409 Kirigami::Platform::Settings *m_settings = nullptr;
410 QPointer<QQuickItem> m_flickable;
411 QPointer<QQuickItem> m_verticalScrollBar;
412 QPointer<QQuickItem> m_horizontalScrollBar;
413 QPointer<QQuickItem> m_scrollView;
414 QMetaObject::Connection m_verticalChangedConnection;
415 QMetaObject::Connection m_horizontalChangedConnection;
416 QPointer<QQuickItem> m_filterItem;
417 // Matches QScrollArea and QTextEdit
418 qreal m_defaultPixelStepSize = 20 * QGuiApplication::styleHints()->wheelScrollLines();
419 qreal m_verticalStepSize = m_defaultPixelStepSize;
420 qreal m_horizontalStepSize = m_defaultPixelStepSize;
421 bool m_explicitVStepSize = false;
422 bool m_explicitHStepSize = false;
423 bool m_wheelScrolling = false;
424 constexpr static qreal m_wheelScrollingDuration = 400;
425 bool m_filterMouseEvents = false;
426 bool m_keyNavigationEnabled = false;
427 bool m_blockTargetWheel = true;
428 bool m_scrollFlickableTarget = true;
429 // Same as QXcbWindow.
430 constexpr static Qt::KeyboardModifiers m_defaultHorizontalScrollModifiers = Qt::AltModifier;
431 // Same as QScrollBar/QAbstractSlider.
432 constexpr static Qt::KeyboardModifiers m_defaultPageScrollModifiers = Qt::ControlModifier | Qt::ShiftModifier;
433 Qt::KeyboardModifiers m_pageScrollModifiers = m_defaultPageScrollModifiers;
434 QTimer m_wheelScrollingTimer;
435 KirigamiWheelEvent m_kirigamiWheelEvent;
436 QQueue<QPoint> m_wheelEvents;
437 QQueue<uint64_t> m_timestamps;
438
439 // Smooth scrolling
440 QQmlEngine *m_engine = nullptr;
441 QPropertyAnimation m_xScrollAnimation{nullptr, "contentX"};
442 QPropertyAnimation m_yScrollAnimation{nullptr, "contentY"};
443 QPropertyAnimation m_xInertiaScrollAnimation{nullptr, "contentX"};
444 QPropertyAnimation m_yInertiaScrollAnimation{nullptr, "contentY"};
445 bool m_wasTouched = false;
446};
447

source code of kirigami/src/wheelhandler.h