File indexing completed on 2024-05-05 07:57:44

0001 /* SPDX-FileCopyrightText: 2019 Marco Martin <mart@kde.org>
0002  * SPDX-FileCopyrightText: 2021 Noah Davis <noahadvs@gmail.com>
0003  * SPDX-License-Identifier: LGPL-2.0-or-later
0004  */
0005 
0006 #pragma once
0007 
0008 #include <QGuiApplication>
0009 #include <QObject>
0010 #include <QPoint>
0011 #include <QPropertyAnimation>
0012 #include <QQmlParserStatus>
0013 #include <QQuickItem>
0014 #include <QStyleHints>
0015 #include <QTimer>
0016 
0017 class QWheelEvent;
0018 class QQmlEngine;
0019 class WheelHandler;
0020 
0021 /**
0022  * Describes the mouse wheel event
0023  */
0024 class KirigamiWheelEvent : public QObject
0025 {
0026     Q_OBJECT
0027     QML_ELEMENT
0028     QML_UNCREATABLE("")
0029 
0030     /**
0031      * x: real
0032      *
0033      * X coordinate of the mouse pointer
0034      */
0035     Q_PROPERTY(qreal x READ x CONSTANT FINAL)
0036 
0037     /**
0038      * y: real
0039      *
0040      * Y coordinate of the mouse pointer
0041      */
0042     Q_PROPERTY(qreal y READ y CONSTANT FINAL)
0043 
0044     /**
0045      * angleDelta: point
0046      *
0047      * The distance the wheel is rotated in degrees.
0048      * The x and y coordinates indicate the horizontal and vertical wheels respectively.
0049      * A positive value indicates it was rotated up/right, negative, bottom/left
0050      * This value is more likely to be set in traditional mice.
0051      */
0052     Q_PROPERTY(QPointF angleDelta READ angleDelta CONSTANT FINAL)
0053 
0054     /**
0055      * pixelDelta: point
0056      *
0057      * provides the delta in screen pixels available on high resolution trackpads
0058      */
0059     Q_PROPERTY(QPointF pixelDelta READ pixelDelta CONSTANT FINAL)
0060 
0061     /**
0062      * buttons: int
0063      *
0064      * it contains an OR combination of the buttons that were pressed during the wheel, they can be:
0065      * Qt.LeftButton, Qt.MiddleButton, Qt.RightButton
0066      */
0067     Q_PROPERTY(int buttons READ buttons CONSTANT FINAL)
0068 
0069     /**
0070      * modifiers: int
0071      *
0072      * Keyboard mobifiers that were pressed during the wheel event, such as:
0073      * Qt.NoModifier (default, no modifiers)
0074      * Qt.ControlModifier
0075      * Qt.ShiftModifier
0076      * ...
0077      */
0078     Q_PROPERTY(int modifiers READ modifiers CONSTANT FINAL)
0079 
0080     /**
0081      * inverted: bool
0082      *
0083      * Whether the delta values are inverted
0084      * On some platformsthe returned delta are inverted, so positive values would mean bottom/left
0085      */
0086     Q_PROPERTY(bool inverted READ inverted CONSTANT FINAL)
0087 
0088     /**
0089      * accepted: bool
0090      *
0091      * If set, the event shouldn't be managed anymore,
0092      * for instance it can be used to block the handler to manage the scroll of a view on some scenarios
0093      * @code
0094      * // This handler handles automatically the scroll of
0095      * // flickableItem, unless Ctrl is pressed, in this case the
0096      * // app has custom code to handle Ctrl+wheel zooming
0097      * Kirigami.WheelHandler {
0098      *   target: flickableItem
0099      *   blockTargetWheel: true
0100      *   scrollFlickableTarget: true
0101      *   onWheel: {
0102      *        if (wheel.modifiers & Qt.ControlModifier) {
0103      *            wheel.accepted = true;
0104      *            // Handle scaling of the view
0105      *       }
0106      *   }
0107      * }
0108      * @endcode
0109      *
0110      */
0111     Q_PROPERTY(bool accepted READ isAccepted WRITE setAccepted FINAL)
0112 
0113 public:
0114     KirigamiWheelEvent(QObject *parent = nullptr);
0115     ~KirigamiWheelEvent() override;
0116 
0117     void initializeFromEvent(QWheelEvent *event);
0118 
0119     qreal x() const;
0120     qreal y() const;
0121     QPointF angleDelta() const;
0122     QPointF pixelDelta() const;
0123     int buttons() const;
0124     int modifiers() const;
0125     bool inverted() const;
0126     bool isAccepted();
0127     void setAccepted(bool accepted);
0128 
0129 private:
0130     qreal m_x = 0;
0131     qreal m_y = 0;
0132     QPointF m_angleDelta;
0133     QPointF m_pixelDelta;
0134     Qt::MouseButtons m_buttons = Qt::NoButton;
0135     Qt::KeyboardModifiers m_modifiers = Qt::NoModifier;
0136     bool m_inverted = false;
0137     bool m_accepted = false;
0138 };
0139 
0140 class WheelFilterItem : public QQuickItem
0141 {
0142     Q_OBJECT
0143 public:
0144     WheelFilterItem(QQuickItem *parent = nullptr);
0145 };
0146 
0147 /**
0148  * @brief Handles scrolling for a Flickable and 2 attached ScrollBars.
0149  *
0150  * WheelHandler filters events from a Flickable, a vertical ScrollBar and a horizontal ScrollBar.
0151  * Wheel and KeyPress events (when `keyNavigationEnabled` is true) are used to scroll the Flickable.
0152  * When `filterMouseEvents` is true, WheelHandler blocks mouse button input from reaching the Flickable
0153  * and sets the `interactive` property of the scrollbars to false when touch input is used.
0154  *
0155  * Wheel event handling behavior:
0156  *
0157  * - 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
0158  * default scroll speed should be consistent with Qt Widgets.
0159  * - When using angle delta, scroll using the step increments defined by `verticalStepSize` and `horizontalStepSize`.
0160  * - When one of the keyboard modifiers in `pageScrollModifiers` is used, scroll by pages.
0161  * - When using a device that doesn't use 120 angle delta unit increments such as a touchpad, the `verticalStepSize`, `horizontalStepSize` and page increments
0162  * (if using page scrolling) will be multiplied by `angle delta / 120` to keep scrolling smooth.
0163  * - 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
0164  * events to scroll, if possible. This prevents controls inside the Flickable's contentItem that allow scrolling to change the value (e.g., Sliders, SpinBoxes)
0165  * from conflicting with scrolling the page.
0166  *
0167  * Common usage with a Flickable:
0168  *
0169  * @include wheelhandler/FlickableUsage.qml
0170  *
0171  * Common usage inside of a ScrollView template:
0172  *
0173  * @include wheelhandler/ScrollViewUsage.qml
0174  *
0175  */
0176 class WheelHandler : public QObject, public QQmlParserStatus
0177 {
0178     Q_OBJECT
0179     Q_INTERFACES(QQmlParserStatus)
0180     QML_ELEMENT
0181 
0182     /**
0183      * @brief This property holds the Qt Quick Flickable that the WheelHandler will control.
0184      */
0185     Q_PROPERTY(QQuickItem *target READ target WRITE setTarget NOTIFY targetChanged FINAL)
0186 
0187     /**
0188      * @brief This property holds the vertical step size.
0189      *
0190      * The default value is equivalent to `20 * Qt.styleHints.wheelScrollLines`. This is consistent with the default increment for QScrollArea.
0191      *
0192      * @sa horizontalStepSize
0193      *
0194      * @since KDE Frameworks 5.89
0195      */
0196     Q_PROPERTY(qreal verticalStepSize READ verticalStepSize WRITE setVerticalStepSize RESET resetVerticalStepSize NOTIFY verticalStepSizeChanged FINAL)
0197 
0198     /**
0199      * @brief This property holds the horizontal step size.
0200      *
0201      * The default value is equivalent to `20 * Qt.styleHints.wheelScrollLines`. This is consistent with the default increment for QScrollArea.
0202      *
0203      * @sa verticalStepSize
0204      *
0205      * @since KDE Frameworks 5.89
0206      */
0207     Q_PROPERTY(
0208         qreal horizontalStepSize READ horizontalStepSize WRITE setHorizontalStepSize RESET resetHorizontalStepSize NOTIFY horizontalStepSizeChanged FINAL)
0209 
0210     /**
0211      * @brief This property holds the keyboard modifiers that will be used to start page scrolling.
0212      *
0213      * The default value is equivalent to `Qt.ControlModifier | Qt.ShiftModifier`. This matches QScrollBar, which uses QAbstractSlider behavior.
0214      *
0215      * @since KDE Frameworks 5.89
0216      */
0217     Q_PROPERTY(Qt::KeyboardModifiers pageScrollModifiers READ pageScrollModifiers WRITE setPageScrollModifiers RESET resetPageScrollModifiers NOTIFY
0218                    pageScrollModifiersChanged FINAL)
0219 
0220     /**
0221      * @brief This property holds whether the WheelHandler filters mouse events like a Qt Quick Controls ScrollView would.
0222      *
0223      * Touch events are allowed to flick the view and they make the scrollbars not interactive.
0224      *
0225      * Mouse events are not allowed to flick the view and they make the scrollbars interactive.
0226      *
0227      * Hover events on the scrollbars and wheel events on anything also make the scrollbars interactive when this property is set to true.
0228      *
0229      * The default value is `false`.
0230      *
0231      * @since KDE Frameworks 5.89
0232      */
0233     Q_PROPERTY(bool filterMouseEvents READ filterMouseEvents WRITE setFilterMouseEvents NOTIFY filterMouseEventsChanged FINAL)
0234 
0235     /**
0236      * @brief This property holds whether the WheelHandler handles keyboard scrolling.
0237      *
0238      * - Left arrow scrolls a step to the left.
0239      * - Right arrow scrolls a step to the right.
0240      * - Up arrow scrolls a step upwards.
0241      * - Down arrow scrolls a step downwards.
0242      * - PageUp scrolls to the previous page.
0243      * - PageDown scrolls to the next page.
0244      * - Home scrolls to the beginning.
0245      * - End scrolls to the end.
0246      * - When Alt is held, scroll horizontally when using PageUp, PageDown, Home or End.
0247      *
0248      * The default value is `false`.
0249      *
0250      * @since KDE Frameworks 5.89
0251      */
0252     Q_PROPERTY(bool keyNavigationEnabled READ keyNavigationEnabled WRITE setKeyNavigationEnabled NOTIFY keyNavigationEnabledChanged FINAL)
0253 
0254     /**
0255      * @brief This property holds whether the WheelHandler blocks all wheel events from reaching the Flickable.
0256      *
0257      * When this property is false, scrolling the Flickable with WheelHandler will only block an event from reaching the Flickable if the Flickable is actually
0258      * scrolled by WheelHandler.
0259      *
0260      * 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
0261      * to jump back to where scrolling started unless the events are always accepted before they reach the Flickable.
0262      *
0263      * The default value is true.
0264      */
0265     Q_PROPERTY(bool blockTargetWheel MEMBER m_blockTargetWheel NOTIFY blockTargetWheelChanged FINAL)
0266 
0267     /**
0268      * @brief This property holds whether the WheelHandler can use wheel events to scroll the Flickable.
0269      *
0270      * The default value is true.
0271      */
0272     Q_PROPERTY(bool scrollFlickableTarget MEMBER m_scrollFlickableTarget NOTIFY scrollFlickableTargetChanged FINAL)
0273 
0274 public:
0275     explicit WheelHandler(QObject *parent = nullptr);
0276     ~WheelHandler() override;
0277 
0278     QQuickItem *target() const;
0279     void setTarget(QQuickItem *target);
0280 
0281     qreal verticalStepSize() const;
0282     void setVerticalStepSize(qreal stepSize);
0283     void resetVerticalStepSize();
0284 
0285     qreal horizontalStepSize() const;
0286     void setHorizontalStepSize(qreal stepSize);
0287     void resetHorizontalStepSize();
0288 
0289     Qt::KeyboardModifiers pageScrollModifiers() const;
0290     void setPageScrollModifiers(Qt::KeyboardModifiers modifiers);
0291     void resetPageScrollModifiers();
0292 
0293     bool filterMouseEvents() const;
0294     void setFilterMouseEvents(bool enabled);
0295 
0296     bool keyNavigationEnabled() const;
0297     void setKeyNavigationEnabled(bool enabled);
0298 
0299     /**
0300      * Scroll up one step. If the stepSize parameter is less than 0, the verticalStepSize will be used.
0301      *
0302      * returns true if the contentItem was moved.
0303      *
0304      * @since KDE Frameworks 5.89
0305      */
0306     Q_INVOKABLE bool scrollUp(qreal stepSize = -1);
0307 
0308     /**
0309      * Scroll down one step. If the stepSize parameter is less than 0, the verticalStepSize will be used.
0310      *
0311      * returns true if the contentItem was moved.
0312      *
0313      * @since KDE Frameworks 5.89
0314      */
0315     Q_INVOKABLE bool scrollDown(qreal stepSize = -1);
0316 
0317     /**
0318      * Scroll left one step. If the stepSize parameter is less than 0, the horizontalStepSize will be used.
0319      *
0320      * returns true if the contentItem was moved.
0321      *
0322      * @since KDE Frameworks 5.89
0323      */
0324     Q_INVOKABLE bool scrollLeft(qreal stepSize = -1);
0325 
0326     /**
0327      * Scroll right one step. If the stepSize parameter is less than 0, the horizontalStepSize will be used.
0328      *
0329      * returns true if the contentItem was moved.
0330      *
0331      * @since KDE Frameworks 5.89
0332      */
0333     Q_INVOKABLE bool scrollRight(qreal stepSize = -1);
0334 
0335 Q_SIGNALS:
0336     void targetChanged();
0337     void verticalStepSizeChanged();
0338     void horizontalStepSizeChanged();
0339     void pageScrollModifiersChanged();
0340     void filterMouseEventsChanged();
0341     void keyNavigationEnabledChanged();
0342     void blockTargetWheelChanged();
0343     void scrollFlickableTargetChanged();
0344 
0345     /**
0346      * @brief This signal is emitted when a wheel event reaches the event filter, just before scrolling is handled.
0347      *
0348      * Accepting the wheel event in the `onWheel` signal handler prevents scrolling from happening.
0349      */
0350     void wheel(KirigamiWheelEvent *wheel);
0351 
0352 protected:
0353     bool eventFilter(QObject *watched, QEvent *event) override;
0354 
0355 private Q_SLOTS:
0356     void _k_rebindScrollBars();
0357 
0358 private:
0359     void classBegin() override;
0360     void componentComplete() override;
0361 
0362     void setScrolling(bool scrolling);
0363     bool scrollFlickable(QPointF pixelDelta, QPointF angleDelta = {}, Qt::KeyboardModifiers modifiers = Qt::NoModifier);
0364 
0365     QPointer<QQuickItem> m_flickable;
0366     QPointer<QQuickItem> m_verticalScrollBar;
0367     QPointer<QQuickItem> m_horizontalScrollBar;
0368     QMetaObject::Connection m_verticalChangedConnection;
0369     QMetaObject::Connection m_horizontalChangedConnection;
0370     QPointer<QQuickItem> m_filterItem;
0371     // Matches QScrollArea and QTextEdit
0372     qreal m_defaultPixelStepSize = 20 * QGuiApplication::styleHints()->wheelScrollLines();
0373     qreal m_verticalStepSize = m_defaultPixelStepSize;
0374     qreal m_horizontalStepSize = m_defaultPixelStepSize;
0375     bool m_explicitVStepSize = false;
0376     bool m_explicitHStepSize = false;
0377     bool m_wheelScrolling = false;
0378     constexpr static qreal m_wheelScrollingDuration = 400;
0379     bool m_filterMouseEvents = false;
0380     bool m_keyNavigationEnabled = false;
0381     bool m_blockTargetWheel = true;
0382     bool m_scrollFlickableTarget = true;
0383     // Same as QXcbWindow.
0384     constexpr static Qt::KeyboardModifiers m_defaultHorizontalScrollModifiers = Qt::AltModifier;
0385     // Same as QScrollBar/QAbstractSlider.
0386     constexpr static Qt::KeyboardModifiers m_defaultPageScrollModifiers = Qt::ControlModifier | Qt::ShiftModifier;
0387     Qt::KeyboardModifiers m_pageScrollModifiers = m_defaultPageScrollModifiers;
0388     QTimer m_wheelScrollingTimer;
0389     KirigamiWheelEvent m_kirigamiWheelEvent;
0390 
0391     // Smooth scrolling
0392     QQmlEngine *m_engine = nullptr;
0393     QPropertyAnimation m_yScrollAnimation{nullptr, "contentY"};
0394     bool m_wasTouched = false;
0395 };