File indexing completed on 2024-04-28 15:27:45

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