File indexing completed on 2024-05-12 05:37:15

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         if (m_interceptedHoverItem) {
0028             resendHoverEvents(m_interceptedHoverItem.interceptedHoverEnterPosition.value());
0029         }
0030 
0031         m_interceptionPos.reset();
0032     });
0033 };
0034 
0035 bool TriangleMouseFilter::childMouseEventFilter(QQuickItem *item, QEvent *event)
0036 {
0037     if (!m_active) {
0038         // 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
0039         // correctly
0040         switch (event->type()) {
0041         case QEvent::HoverEnter:
0042             m_interceptedHoverItem = item;
0043             break;
0044         case QEvent::HoverLeave:
0045             m_interceptedHoverItem = nullptr;
0046             break;
0047         default:
0048             break;
0049         }
0050         return false;
0051     }
0052 
0053     switch (event->type()) {
0054     case QEvent::HoverLeave:
0055         if (m_interceptedHoverItem == item) {
0056             m_interceptedHoverItem = nullptr;
0057         }
0058         return false;
0059     case QEvent::HoverEnter:
0060     case QEvent::HoverMove: {
0061         QHoverEvent &he = *static_cast<QHoverEvent *>(event);
0062 
0063         const QPointF position = item->mapToItem(this, he.position());
0064 
0065         // This clause means that we block focus when first entering a given position
0066         // in the case of kickoff it's so that we can move the mouse from the bottom tabbar to the side view
0067         bool firstEnter = m_blockFirstEnter && event->type() == QEvent::HoverEnter && !m_interceptionPos;
0068 
0069         if (event->type() == QEvent::HoverMove && m_interceptedHoverItem == item && m_lastCursorPosition.has_value() && m_lastTimestamp.has_value()
0070             && !firstEnter) {
0071             // If no movement was registered, filter event in any case
0072             if (position == m_lastCursorPosition) {
0073                 return true;
0074             }
0075 
0076             const QPointF deltaPosition = position - m_lastCursorPosition.value();
0077             m_lastCursorPosition = position;
0078             const auto deltaTime = he.timestamp() - m_lastTimestamp.value();
0079             m_lastTimestamp = he.timestamp();
0080 
0081             // As a first metric, we check the direction in which the cursor has been moved
0082             bool directionMetric = false;
0083             switch (m_edge) {
0084             case Qt::RightEdge:
0085                 directionMetric = deltaPosition.x() < -JITTER_THRESHOLD;
0086                 break;
0087             case Qt::TopEdge:
0088                 directionMetric = deltaPosition.y() > JITTER_THRESHOLD;
0089                 break;
0090             case Qt::LeftEdge:
0091                 directionMetric = deltaPosition.x() > JITTER_THRESHOLD;
0092                 break;
0093             case Qt::BottomEdge:
0094                 directionMetric = deltaPosition.y() < -JITTER_THRESHOLD;
0095                 break;
0096             }
0097             if (directionMetric) {
0098                 resendHoverEvents(position);
0099                 return true;
0100             }
0101 
0102             // As a second metric, we use the velocity of the cursor to disable the filter
0103             if (deltaTime != 0 && he.timestamp() != 0) {
0104                 const double velocity = std::pow(deltaPosition.x(), 2) + std::pow(deltaPosition.y(), 2) / deltaTime;
0105                 if (velocity < VELOCITY_THRESHOLD) {
0106                     resendHoverEvents(position);
0107                     return true;
0108                 }
0109             }
0110         }
0111 
0112         // Finally, we check if the cursor movement was inside the filtered region
0113         if (firstEnter || filterContains(position)) {
0114             if (firstEnter) {
0115                 // In case of a firstEnter, set the interceptionPos but not the interceptedHoverEnterPosition
0116                 // so that the timer does not reselect the intercepted item
0117                 m_interceptedHoverItem = item;
0118                 m_interceptionPos = position;
0119             } else if (event->type() == QEvent::HoverEnter) {
0120                 m_interceptedHoverItem = item;
0121                 m_interceptedHoverItem.interceptedHoverEnterPosition = position;
0122             }
0123 
0124             m_lastCursorPosition = position;
0125             m_lastTimestamp = he.timestamp();
0126 
0127             if (m_filterTimeout > 0) {
0128                 m_resetTimer.start(m_filterTimeout);
0129             }
0130             return true;
0131         } else {
0132             // Pass event through
0133             m_interceptionPos = position;
0134 
0135             if (he.type() == QEvent::HoverMove && m_interceptedHoverItem == item) {
0136                 resendHoverEvents(position);
0137             }
0138             return false;
0139         }
0140     }
0141     default:
0142         return false;
0143     }
0144 }
0145 
0146 void TriangleMouseFilter::resendHoverEvents(const QPointF &cursorPosition)
0147 {
0148     // If we are no longer inhibiting events and have previously intercepted a hover enter
0149     // we manually send the hover enter to that item
0150     if (m_interceptionPos) {
0151         const auto targetPosition = mapToItem(m_interceptedHoverItem.item, m_interceptionPos.value());
0152         QHoverEvent e(QEvent::HoverEnter, targetPosition, targetPosition);
0153         qApp->sendEvent(m_interceptedHoverItem.item, &e);
0154     }
0155 
0156     if (m_interceptionPos != cursorPosition) {
0157         const auto targetPosition = mapToItem(m_interceptedHoverItem.item, cursorPosition);
0158         QHoverEvent e(QEvent::HoverMove, targetPosition, targetPosition);
0159         qApp->sendEvent(m_interceptedHoverItem.item, &e);
0160     }
0161 
0162     m_interceptedHoverItem = nullptr;
0163 }
0164 
0165 bool TriangleMouseFilter::filterContains(const QPointF &p) const
0166 {
0167     if (!m_interceptionPos) {
0168         return false;
0169     }
0170 
0171     // QPolygonF.contains returns false if we're on the edge, so we pad our main item
0172     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)
0173                                                   : QRect(-1, -1, width() + 1, height() + 1);
0174 
0175     QPolygonF poly;
0176 
0177     // We use some jitter protection by extending our triangle out slight past the mouse position in the opposite direction of the edge;
0178     switch (m_edge) {
0179     case Qt::RightEdge:
0180         poly << m_interceptionPos.value() + QPointF(-JITTER_THRESHOLD, 0) << shape.topRight() << shape.bottomRight();
0181         break;
0182     case Qt::TopEdge:
0183         poly << m_interceptionPos.value() + QPointF(0, -JITTER_THRESHOLD) << shape.topLeft() << shape.topRight();
0184         break;
0185     case Qt::LeftEdge:
0186         poly << m_interceptionPos.value() + QPointF(JITTER_THRESHOLD, 0) << shape.topLeft() << shape.bottomLeft();
0187         break;
0188     case Qt::BottomEdge:
0189         poly << m_interceptionPos.value() + QPointF(0, JITTER_THRESHOLD) << shape.bottomLeft() << shape.bottomRight();
0190     }
0191 
0192     bool firstCheck = poly.containsPoint(p, Qt::OddEvenFill);
0193     poly.replace(0, m_secondaryPoint);
0194     bool secondCheck = m_secondaryPoint != QPointF(0, 0) && poly.containsPoint(p, Qt::OddEvenFill);
0195     return (firstCheck || secondCheck);
0196 }