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

source code of kirigami/src/wheelhandler.h