File indexing completed on 2024-05-12 17:08:58

0001 /*
0002     SPDX-FileCopyrightText: 2021 David Edmundson <davidedmundson@kde.org>
0003 
0004     SPDX-License-Identifier: LGPL-2.0-or-later
0005 */
0006 
0007 #pragma once
0008 
0009 #include <optional>
0010 
0011 #include <QGuiApplication>
0012 #include <QQuickItem>
0013 #include <QTimer>
0014 
0015 struct InterceptedQuickItemData {
0016     QPointer<QQuickItem> item;
0017     std::optional<QPointF> interceptedHoverEnterPosition; // position of intercepted enter events
0018 
0019     bool operator==(QQuickItem *otherItem) const
0020     {
0021         return otherItem == item;
0022     }
0023 
0024     void operator=(QQuickItem *otherItem)
0025     {
0026         item = otherItem;
0027         interceptedHoverEnterPosition.reset();
0028     }
0029 
0030     explicit operator bool() const
0031     {
0032         return !item.isNull() && interceptedHoverEnterPosition.has_value();
0033     }
0034 };
0035 
0036 /**
0037  * This class filters child mouse events that move from a current location towards a given edge.
0038  *
0039  * The primary use-case being where we have a list of actions that trigger on hover on one side
0040  * adjacent to a large hit area where a user will want to interact with.
0041  *
0042  * Without this filter a user moving their mouse towards the target area will trigger other hover events
0043  *
0044  * This item distinguishes mouse moves towards an edge from attempts to select another item in the combo
0045  * tree.
0046  *
0047  * See: https://bjk5.com/post/44698559168/breaking-down-amazons-mega-dropdown
0048  */
0049 class TriangleMouseFilter : public QQuickItem
0050 {
0051     Q_OBJECT
0052 
0053     /**
0054      * Whether the filter is active.  If false, all events will be passed through. True by default.
0055      */
0056     Q_PROPERTY(bool active MEMBER m_active NOTIFY activeChanged)
0057 
0058     /**
0059      * The timeout in ms after which the filter is disabled and the current item is selected
0060      * regardless.
0061      *
0062      * The default is 300
0063      * Setting a negative value disables the timeout
0064      */
0065     Q_PROPERTY(int filterTimeOut MEMBER m_filterTimeout NOTIFY filterTimoutChanged)
0066 
0067     /**
0068      * The edge that we want to filter mouse actions towards.
0069      * i.e if we have a listview on the left with a submenu on the right, the value
0070      * will be Qt.RightEdge
0071      *
0072      * RTL configurations must be handled explicitly by the caller
0073      */
0074     Q_PROPERTY(Qt::Edge edge MEMBER m_edge NOTIFY edgeChanged)
0075 
0076     /**
0077      * The line (as two points) representing the geometry of the edge we want to filter mouse actions towards,
0078      * relative to the filtered item. This property is a QVector of x1, y1, x2, y2 instead of a QLine
0079      * for ease of use from QML which does not have a line basic type. If edgeLine is not set or is set to
0080      * anything other than a vector of 4 ints, the edge of the applet will be used instead.
0081      */
0082     Q_PROPERTY(QVector<int> edgeLine MEMBER m_edgeLine NOTIFY edgeLineChanged)
0083 
0084     /**
0085      * Whether the filter will block the first hover enter event. False by default.
0086      */
0087     Q_PROPERTY(bool blockFirstEnter MEMBER m_blockFirstEnter NOTIFY blockFirstEnterChanged)
0088 
0089     /**
0090      * A secondary starting point (other than the interception point) to also check for mouse position.
0091      * Not considered if it has the value (0, 0). Default value (0, 0)
0092      */
0093     Q_PROPERTY(QPointF secondaryPoint MEMBER m_secondaryPoint NOTIFY secondaryPointChanged)
0094 
0095 public:
0096     TriangleMouseFilter(QQuickItem *parent = nullptr);
0097     ~TriangleMouseFilter() = default;
0098 
0099 Q_SIGNALS:
0100     void filterTimoutChanged();
0101     void edgeChanged();
0102     void edgeLineChanged();
0103     void activeChanged();
0104     void blockFirstEnterChanged();
0105     void secondaryPointChanged();
0106 
0107 protected:
0108     bool childMouseEventFilter(QQuickItem *item, QEvent *event) override;
0109 
0110 private:
0111     static constexpr double VELOCITY_THRESHOLD = 0.1;
0112     static constexpr int JITTER_THRESHOLD = 1;
0113 
0114     bool filterContains(const QPointF &p) const;
0115     void resendHoverEvents(const QPointF &cursorPosition);
0116 
0117     QTimer m_resetTimer;
0118 
0119     InterceptedQuickItemData m_interceptedHoverItem; // item newly entered but the enter event was intercepted
0120 
0121     std::optional<QPointF> m_lastCursorPosition;
0122     std::optional<decltype(std::declval<QHoverEvent>().timestamp())> m_lastTimestamp;
0123     std::optional<QPointF> m_interceptionPos; // point where we started intercepting
0124     Qt::Edge m_edge = Qt::RightEdge;
0125     QVector<int> m_edgeLine;
0126     int m_filterTimeout = 300;
0127     bool m_active;
0128     bool m_blockFirstEnter;
0129     bool m_usingCustomEdgeLine;
0130     QPointF m_secondaryPoint;
0131 };