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

0001 /*
0002     SPDX-FileCopyrightText: 2021 David Edmundson <davidedmundson@kde.org>
0003     SPDX-FileCopyrightText: 2022 Derek Christ <christ.derek@gmail.com>
0004     SPDX-FileCopyrightText: 2022 Bharadwaj Raju <bharadwaj.raju777@protonmail.com>
0005 
0006     SPDX-License-Identifier: LGPL-2.0-or-later
0007 */
0008 
0009 #include "trianglemousefilter.h"
0010 
0011 #include <QPolygonF>
0012 #include <cmath>
0013 
0014 TriangleMouseFilter::TriangleMouseFilter(QQuickItem *parent)
0015     : QQuickItem(parent)
0016     , m_edgeLine()
0017     , m_active(true)
0018     , m_blockFirstEnter(false)
0019 {
0020     setFiltersChildMouseEvents(true);
0021 
0022     m_resetTimer.setSingleShot(true);
0023     connect(&m_resetTimer, &QTimer::timeout, this, [this]() {
0024         m_lastCursorPosition.reset();
0025         m_lastTimestamp.reset();
0026 
0027         update();
0028 
0029         if (m_interceptedHoverItem) {
0030             resendHoverEvents(m_interceptedHoverItem.interceptedHoverEnterPosition.value());
0031         }
0032 
0033         m_interceptionPos.reset();
0034     });
0035 };
0036 
0037 bool TriangleMouseFilter::childMouseEventFilter(QQuickItem *item, QEvent *event)
0038 {
0039     update();
0040     if (!m_active) {
0041         // Even if inactive, we still need to record the current item so when active becomes true after the child item is hovered, the filter can still work
0042         // correctly
0043         switch (event->type()) {
0044         case QEvent::HoverEnter:
0045             m_interceptedHoverItem = item;
0046             break;
0047         case QEvent::HoverLeave:
0048             m_interceptedHoverItem = nullptr;
0049             break;
0050         default:
0051             break;
0052         }
0053         return false;
0054     }
0055 
0056     switch (event->type()) {
0057     case QEvent::HoverLeave:
0058         if (!m_interceptedHoverItem /* reset in resendHoverEvent */) {
0059             return false;
0060         } else if (m_interceptedHoverItem == item || m_resetTimer.isActive() /* The previous item hasn't received a HoverLeave event */) {
0061             m_interceptedHoverItem = nullptr;
0062             return false;
0063         }
0064         return true;
0065     case QEvent::HoverEnter:
0066     case QEvent::HoverMove: {
0067         QHoverEvent &he = *static_cast<QHoverEvent *>(event);
0068 
0069         const QPointF position = item->mapToItem(this, he.posF());
0070 
0071         // This clause means that we block focus when first entering a given position
0072         // in the case of kickoff it's so that we can move the mouse from the bottom tabbar to the side view
0073         bool firstEnter = m_blockFirstEnter && event->type() == QEvent::HoverEnter && !m_interceptionPos;
0074 
0075         if (event->type() == QEvent::HoverMove && m_interceptedHoverItem == item && m_lastCursorPosition.has_value() && m_lastTimestamp.has_value()
0076             && !firstEnter) {
0077             // If no movement was registered, filter event in any case
0078             if (position == m_lastCursorPosition) {
0079                 return true;
0080             }
0081 
0082             const QPointF deltaPosition = position - m_lastCursorPosition.value();
0083             m_lastCursorPosition = position;
0084             const auto deltaTime = he.timestamp() - m_lastTimestamp.value();
0085             m_lastTimestamp = he.timestamp();
0086 
0087             // As a first metric, we check the direction in which the cursor has been moved
0088             bool directionMetric = false;
0089             switch (m_edge) {
0090             case Qt::RightEdge:
0091                 directionMetric = deltaPosition.x() < -JITTER_THRESHOLD;
0092                 break;
0093             case Qt::TopEdge:
0094                 directionMetric = deltaPosition.y() > JITTER_THRESHOLD;
0095                 break;
0096             case Qt::LeftEdge:
0097                 directionMetric = deltaPosition.x() > JITTER_THRESHOLD;
0098                 break;
0099             case Qt::BottomEdge:
0100                 directionMetric = deltaPosition.y() < -JITTER_THRESHOLD;
0101                 break;
0102             }
0103             if (directionMetric) {
0104                 resendHoverEvents(position);
0105                 return true;
0106             }
0107 
0108             // As a second metric, we use the velocity of the cursor to disable the filter
0109             if (deltaTime != 0 && he.timestamp() != 0) {
0110                 const double velocity = std::pow(deltaPosition.x(), 2) + std::pow(deltaPosition.y(), 2) / deltaTime;
0111                 if (velocity < VELOCITY_THRESHOLD) {
0112                     resendHoverEvents(position);
0113                     return true;
0114                 }
0115             }
0116         }
0117 
0118         // Finally, we check if the cursor movement was inside the filtered region
0119         if (firstEnter || filterContains(position)) {
0120             if (firstEnter) {
0121                 // In case of a firstEnter, set the interceptionPos but not the interceptedHoverEnterPosition
0122                 // so that the timer does not reselect the intercepted item
0123                 m_interceptedHoverItem = item;
0124                 m_interceptionPos = position;
0125             } else if (event->type() == QEvent::HoverEnter) {
0126                 m_interceptedHoverItem = item;
0127                 m_interceptedHoverItem.interceptedHoverEnterPosition = position;
0128             }
0129 
0130             m_lastCursorPosition = position;
0131             m_lastTimestamp = he.timestamp();
0132 
0133             if (m_filterTimeout > 0) {
0134                 m_resetTimer.start(m_filterTimeout);
0135             }
0136             return true;
0137         } else {
0138             // Pass event through
0139             m_interceptionPos = position;
0140 
0141             if (he.type() == QEvent::HoverMove && m_interceptedHoverItem == item) {
0142                 resendHoverEvents(position);
0143             }
0144             return false;
0145         }
0146     }
0147     default:
0148         return false;
0149     }
0150 }
0151 
0152 void TriangleMouseFilter::resendHoverEvents(const QPointF &cursorPosition)
0153 {
0154     // If we are no longer inhibiting events and have previously intercepted a hover enter
0155     // we manually send the hover enter to that item
0156     if (m_interceptionPos) {
0157         const auto targetPosition = mapToItem(m_interceptedHoverItem.item, m_interceptionPos.value());
0158         QHoverEvent e(QEvent::HoverEnter, targetPosition, targetPosition);
0159         qApp->sendEvent(m_interceptedHoverItem.item, &e);
0160     }
0161 
0162     if (m_interceptionPos != cursorPosition) {
0163         const auto targetPosition = mapToItem(m_interceptedHoverItem.item, cursorPosition);
0164         QHoverEvent e(QEvent::HoverMove, targetPosition, targetPosition);
0165         qApp->sendEvent(m_interceptedHoverItem.item, &e);
0166     }
0167 
0168     m_interceptedHoverItem = nullptr;
0169 }
0170 
0171 bool TriangleMouseFilter::filterContains(const QPointF &p) const
0172 {
0173     if (!m_interceptionPos) {
0174         return false;
0175     }
0176 
0177     // QPolygonF.contains returns false if we're on the edge, so we pad our main item
0178     const QRectF shape = (m_edgeLine.size() == 4) ? QRect(m_edgeLine[0] - 1, m_edgeLine[1] - 1, width() + m_edgeLine[2] + 1, height() + m_edgeLine[3] + 1)
0179                                                   : QRect(-1, -1, width() + 1, height() + 1);
0180 
0181     QPolygonF poly;
0182 
0183     // We use some jitter protection by extending our triangle out slight past the mouse position in the opposite direction of the edge;
0184     switch (m_edge) {
0185     case Qt::RightEdge:
0186         poly << m_interceptionPos.value() + QPointF(-JITTER_THRESHOLD, 0) << shape.topRight() << shape.bottomRight();
0187         break;
0188     case Qt::TopEdge:
0189         poly << m_interceptionPos.value() + QPointF(0, -JITTER_THRESHOLD) << shape.topLeft() << shape.topRight();
0190         break;
0191     case Qt::LeftEdge:
0192         poly << m_interceptionPos.value() + QPointF(JITTER_THRESHOLD, 0) << shape.topLeft() << shape.bottomLeft();
0193         break;
0194     case Qt::BottomEdge:
0195         poly << m_interceptionPos.value() + QPointF(0, JITTER_THRESHOLD) << shape.bottomLeft() << shape.bottomRight();
0196     }
0197 
0198     bool firstCheck = poly.containsPoint(p, Qt::OddEvenFill);
0199     poly.replace(0, m_secondaryPoint);
0200     bool secondCheck = m_secondaryPoint != QPointF(0, 0) && poly.containsPoint(p, Qt::OddEvenFill);
0201     return (firstCheck || secondCheck);
0202 }