File indexing completed on 2025-07-13 05:03:00

0001 // SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
0002 // SPDX-FileCopyrightText: 2020 The Qt Company Ltd.
0003 // SPDX-License-Identifier: GPL-2.0-or-later
0004 
0005 // based on Flickable, but heavily simplified
0006 
0007 #include "swipearea.h"
0008 
0009 #include <QMouseEvent>
0010 #include <QObject>
0011 #include <QTabletEvent>
0012 #include <QTouchEvent>
0013 
0014 // how many pixels to move before it starts being registered as a swipe
0015 const int SWIPE_REGISTER_THRESHOLD = 10;
0016 
0017 SwipeArea::SwipeArea(QQuickItem *parent)
0018     : QQuickItem{parent}
0019 {
0020     setAcceptTouchEvents(true);
0021     setAcceptedMouseButtons(Qt::LeftButton);
0022     setFiltersChildMouseEvents(true);
0023 }
0024 
0025 SwipeArea::Mode SwipeArea::mode() const
0026 {
0027     return m_mode;
0028 }
0029 
0030 void SwipeArea::setMode(Mode mode)
0031 {
0032     m_mode = mode;
0033     Q_EMIT modeChanged();
0034 }
0035 
0036 bool SwipeArea::interactive() const
0037 {
0038     return m_interactive;
0039 }
0040 
0041 void SwipeArea::setInteractive(bool interactive)
0042 {
0043     m_interactive = interactive;
0044     Q_EMIT interactiveChanged();
0045 }
0046 
0047 bool SwipeArea::moving() const
0048 {
0049     return m_moving;
0050 }
0051 
0052 bool SwipeArea::pressed() const
0053 {
0054     return m_pressed;
0055 }
0056 
0057 void SwipeArea::setSkipSwipeThreshold(bool value)
0058 {
0059     m_skipSwipeThreshold = value;
0060 }
0061 
0062 bool SwipeArea::childMouseEventFilter(QQuickItem *item, QEvent *event)
0063 {
0064     if (!isVisible() || !isEnabled() || !m_interactive) {
0065         resetSwipe();
0066         return QQuickItem::childMouseEventFilter(item, event);
0067     }
0068 
0069     if (event->isPointerEvent() && event->type() != QEvent::UngrabMouse) {
0070         return filterPointerEvent(item, static_cast<QPointerEvent *>(event));
0071     }
0072 
0073     return QQuickItem::childMouseEventFilter(item, event);
0074 }
0075 
0076 // take exclusive grab from children
0077 bool SwipeArea::filterPointerEvent(QQuickItem *receiver, QPointerEvent *event)
0078 {
0079     // only filter mouse, touch or tablet events
0080     if (!dynamic_cast<QMouseEvent *>(event) && !dynamic_cast<QTabletEvent *>(event) && !dynamic_cast<QTouchEvent *>(event)) {
0081         return false;
0082     }
0083 
0084     const auto &firstPoint = event->points().first();
0085 
0086     if (event->pointCount() == 1 && event->exclusiveGrabber(firstPoint) == this) {
0087         // We have an exclusive grab (since we're e.g dragging), but at the same time, we have
0088         // a child with a passive grab (which is why this filter is being called). And because
0089         // of that, we end up getting the same pointer events twice; First in our own event
0090         // handlers (because of the grab), then once more in here, since we filter the child.
0091         // To avoid processing the event twice (e.g avoid calling handleReleaseEvent once more
0092         // from below), we mark the event as filtered, and simply return.
0093         event->setAccepted(true);
0094         return true;
0095     }
0096 
0097     QPointF localPos = mapFromScene(firstPoint.scenePosition());
0098     bool receiverDisabled = receiver && !receiver->isEnabled();
0099     bool receiverKeepsGrab = receiver && (receiver->keepMouseGrab() || receiver->keepTouchGrab());
0100 
0101     if ((m_stealMouse || contains(localPos)) && (!receiver || !receiverKeepsGrab || receiverDisabled)) {
0102         // clone the event, and set the first point's local position
0103         // HACK: we can't change QPointerEvent's points since it's const, so we have to pass localPos into the handlers
0104         QPointerEvent *localizedEvent = event->clone();
0105         localizedEvent->setAccepted(false);
0106 
0107         switch (firstPoint.state()) {
0108         case QEventPoint::State::Updated:
0109             handleMoveEvent(localizedEvent, localPos);
0110             break;
0111         case QEventPoint::State::Pressed:
0112             handlePressEvent(localizedEvent, localPos);
0113             break;
0114         case QEventPoint::State::Released:
0115             handleReleaseEvent(localizedEvent, localPos);
0116             break;
0117         case QEventPoint::State::Stationary:
0118         case QEventPoint::State::Unknown:
0119             break;
0120         }
0121 
0122         if ((receiver && m_stealMouse && !receiverKeepsGrab && receiver != this) || receiverDisabled) {
0123             event->setExclusiveGrabber(firstPoint, this);
0124         }
0125 
0126         bool filtered = m_stealMouse || receiverDisabled;
0127         if (filtered) {
0128             event->setAccepted(true);
0129         }
0130 
0131         return filtered;
0132     }
0133 
0134     if (firstPoint.state() == QEventPoint::State::Released || (receiverKeepsGrab && !receiverDisabled)) {
0135         // mouse released, or another item has claimed the grab
0136         resetSwipe();
0137     }
0138 
0139     return false;
0140 }
0141 
0142 void SwipeArea::mouseMoveEvent(QMouseEvent *event)
0143 {
0144     if (m_interactive) {
0145         handleMoveEvent(event, event->points().first().position());
0146         event->accept();
0147     } else {
0148         QQuickItem::mouseMoveEvent(event);
0149     }
0150 }
0151 
0152 void SwipeArea::mousePressEvent(QMouseEvent *event)
0153 {
0154     if (m_interactive) {
0155         handlePressEvent(event, event->points().first().position());
0156         event->accept();
0157     } else {
0158         QQuickItem::mousePressEvent(event);
0159     }
0160 }
0161 
0162 void SwipeArea::mouseReleaseEvent(QMouseEvent *event)
0163 {
0164     if (m_interactive) {
0165         handleReleaseEvent(event, event->points().first().position());
0166         event->accept();
0167     } else {
0168         QQuickItem::mouseReleaseEvent(event);
0169     }
0170 }
0171 
0172 void SwipeArea::mouseUngrabEvent()
0173 {
0174     QQuickItem::mouseUngrabEvent();
0175 }
0176 
0177 void SwipeArea::touchEvent(QTouchEvent *event)
0178 {
0179     bool unhandled = true;
0180     const auto &firstPoint = event->points().first();
0181 
0182     switch (firstPoint.state()) {
0183     case QEventPoint::State::Pressed:
0184         if (m_interactive) {
0185             handlePressEvent(event, firstPoint.position());
0186             event->accept();
0187             unhandled = false;
0188         }
0189         break;
0190     case QEventPoint::State::Updated:
0191         if (m_interactive) {
0192             handleMoveEvent(event, firstPoint.position());
0193             event->accept();
0194             unhandled = false;
0195         }
0196         break;
0197     case QEventPoint::State::Released:
0198         if (m_interactive) {
0199             handleReleaseEvent(event, firstPoint.position());
0200             event->accept();
0201             unhandled = false;
0202         }
0203         break;
0204     case QEventPoint::State::Stationary:
0205     case QEventPoint::State::Unknown:
0206         break;
0207     }
0208 
0209     if (unhandled) {
0210         QQuickItem::touchEvent(event);
0211     }
0212 }
0213 
0214 void SwipeArea::touchUngrabEvent()
0215 {
0216     QQuickItem::touchUngrabEvent();
0217 }
0218 
0219 void SwipeArea::setMoving(bool moving)
0220 {
0221     m_moving = moving;
0222     Q_EMIT movingChanged();
0223 }
0224 
0225 void SwipeArea::setPressed(bool pressed)
0226 {
0227     m_pressed = pressed;
0228     Q_EMIT pressedChanged();
0229 }
0230 
0231 void SwipeArea::resetSwipe()
0232 {
0233     m_skipSwipeThreshold = false;
0234     m_stealMouse = false;
0235     if (m_pressed) {
0236         setPressed(false);
0237     }
0238     if (m_moving) {
0239         setMoving(false);
0240     }
0241 }
0242 
0243 void SwipeArea::handlePressEvent(QPointerEvent *event, QPointF point)
0244 {
0245     // ignore more touch events
0246     if (m_pressed) {
0247         return;
0248     }
0249 
0250     setPressed(true);
0251     m_stealMouse = false;
0252     m_pressPos = point;
0253     m_lastPos = m_pressPos;
0254 }
0255 
0256 void SwipeArea::handleReleaseEvent(QPointerEvent *event, QPointF point)
0257 {
0258     // if we are in a swipe
0259     if (m_moving) {
0260         Q_EMIT swipeEnded();
0261     }
0262 
0263     resetSwipe();
0264 }
0265 
0266 void SwipeArea::handleMoveEvent(QPointerEvent *event, QPointF point)
0267 {
0268     if (!m_stealMouse) {
0269         if (!m_skipSwipeThreshold) {
0270             // if we haven't reached the swipe registering threshold yet, don't start the swipe
0271             if (m_mode == Mode::VerticalOnly && qAbs(point.y() - m_pressPos.y()) < SWIPE_REGISTER_THRESHOLD) {
0272                 return;
0273             } else if (m_mode == Mode::HorizontalOnly && qAbs(point.x() - m_pressPos.x()) < SWIPE_REGISTER_THRESHOLD) {
0274                 return;
0275             } else if (m_mode == Mode::BothAxis && qAbs(point.manhattanLength() - m_pressPos.manhattanLength()) < SWIPE_REGISTER_THRESHOLD) {
0276                 return;
0277             }
0278         }
0279         m_skipSwipeThreshold = false;
0280 
0281         // we now start the swipe, stealing it from children
0282 
0283         m_startPos = point;
0284         m_lastPos = point;
0285         m_stealMouse = true;
0286         setMoving(true);
0287         Q_EMIT swipeStarted(m_startPos);
0288     }
0289 
0290     const QVector2D totalDelta = QVector2D(point - m_startPos);
0291     const QVector2D delta = QVector2D(point - m_lastPos);
0292     m_lastPos = point;
0293 
0294     // ensure it's called AFTER swipeStarted()
0295     Q_EMIT swipeMove(totalDelta.x(), totalDelta.y(), delta.x(), delta.y());
0296 }