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 }