File indexing completed on 2024-05-05 04:47:01

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