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 | |
17 | class QWheelEvent; |
18 | class QQmlEngine; |
19 | class WheelHandler; |
20 | |
21 | /** |
22 | * Describes the mouse wheel event |
23 | */ |
24 | class 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 | |
113 | public: |
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 | |
129 | private: |
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 | |
140 | class WheelFilterItem : public QQuickItem |
141 | { |
142 | Q_OBJECT |
143 | public: |
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 | */ |
176 | class 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 | |
274 | public: |
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 | |
335 | Q_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 | |
352 | protected: |
353 | bool eventFilter(QObject *watched, QEvent *event) override; |
354 | |
355 | private Q_SLOTS: |
356 | void _k_rebindScrollBars(); |
357 | |
358 | private: |
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 | |