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 }