diff --git a/components/mobileshell/CMakeLists.txt b/components/mobileshell/CMakeLists.txt index cba025ae..965cc1f1 100644 --- a/components/mobileshell/CMakeLists.txt +++ b/components/mobileshell/CMakeLists.txt @@ -8,6 +8,7 @@ set(mobileshellplugin_SRCS mobileshellplugin.cpp shellutil.cpp components/direction.cpp + components/swipearea.cpp notifications/notificationthumbnailer.cpp notifications/notificationfilemenu.cpp ) diff --git a/components/mobileshell/components/swipearea.cpp b/components/mobileshell/components/swipearea.cpp new file mode 100644 index 00000000..c59c5aa9 --- /dev/null +++ b/components/mobileshell/components/swipearea.cpp @@ -0,0 +1,268 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-FileCopyrightText: 2020 The Qt Company Ltd. +// SPDX-License-Identifier: GPL-2.0-or-later + +// based on Flickable, but heavily simplified + +#include "swipearea.h" + +#include +#include +#include +#include + +// how many pixels to move before it starts being registered as a swipe +const int SWIPE_REGISTER_THRESHOLD = 10; + +SwipeArea::SwipeArea(QQuickItem *parent) + : QQuickItem{parent} +{ + setAcceptTouchEvents(true); + setAcceptedMouseButtons(Qt::LeftButton); + setFiltersChildMouseEvents(true); +} + +bool SwipeArea::interactive() +{ + return m_interactive; +} + +bool SwipeArea::moving() +{ + return m_moving; +} + +bool SwipeArea::pressed() +{ + return m_pressed; +} + +bool SwipeArea::childMouseEventFilter(QQuickItem *item, QEvent *event) +{ + if (!isVisible() || !isEnabled() || !m_interactive) { + resetSwipe(); + return QQuickItem::childMouseEventFilter(item, event); + } + + if (event->isPointerEvent() && event->type() != QEvent::UngrabMouse) { + return filterPointerEvent(item, static_cast(event)); + } + + return QQuickItem::childMouseEventFilter(item, event); +} + +// take exclusive grab from children +bool SwipeArea::filterPointerEvent(QQuickItem *receiver, QPointerEvent *event) +{ + // only filter mouse, touch or tablet events + if (!dynamic_cast(event) && !dynamic_cast(event) && !dynamic_cast(event)) { + return false; + } + + const auto &firstPoint = event->points().first(); + + if (event->pointCount() == 1 && event->exclusiveGrabber(firstPoint) == this) { + // We have an exclusive grab (since we're e.g dragging), but at the same time, we have + // a child with a passive grab (which is why this filter is being called). And because + // of that, we end up getting the same pointer events twice; First in our own event + // handlers (because of the grab), then once more in here, since we filter the child. + // To avoid processing the event twice (e.g avoid calling handleReleaseEvent once more + // from below), we mark the event as filtered, and simply return. + event->setAccepted(true); + return true; + } + + QPointF localPos = mapFromScene(firstPoint.scenePosition()); + bool receiverDisabled = receiver && !receiver->isEnabled(); + bool receiverKeepsGrab = receiver && (receiver->keepMouseGrab() || receiver->keepTouchGrab()); + + if ((m_stealMouse || contains(localPos)) && (!receiver || !receiverKeepsGrab || receiverDisabled)) { + // clone the event, and set the first point's local position + // HACK: we can't change QPointerEvent's points since it's const, so we have to pass localPos into the handlers + QPointerEvent *localizedEvent = event->clone(); + localizedEvent->setAccepted(false); + + switch (firstPoint.state()) { + case QEventPoint::State::Updated: + handleMoveEvent(localizedEvent, localPos); + break; + case QEventPoint::State::Pressed: + handlePressEvent(localizedEvent, localPos); + break; + case QEventPoint::State::Released: + handleReleaseEvent(localizedEvent, localPos); + break; + case QEventPoint::State::Stationary: + case QEventPoint::State::Unknown: + break; + } + + if ((receiver && m_stealMouse && !receiverKeepsGrab && receiver != this) || receiverDisabled) { + event->setExclusiveGrabber(firstPoint, this); + } + + bool filtered = m_stealMouse || receiverDisabled; + if (filtered) { + event->setAccepted(true); + } + + return filtered; + } + + if (firstPoint.state() == QEventPoint::State::Released || (receiverKeepsGrab && !receiverDisabled)) { + // mouse released, or another item has claimed the grab + resetSwipe(); + } + + return false; +} + +void SwipeArea::mouseMoveEvent(QMouseEvent *event) +{ + if (m_interactive) { + handleMoveEvent(event, event->points().first().position()); + event->accept(); + } else { + QQuickItem::mouseMoveEvent(event); + } +} + +void SwipeArea::mousePressEvent(QMouseEvent *event) +{ + if (m_interactive) { + handlePressEvent(event, event->points().first().position()); + event->accept(); + } else { + QQuickItem::mousePressEvent(event); + } +} + +void SwipeArea::mouseReleaseEvent(QMouseEvent *event) +{ + if (m_interactive) { + handleReleaseEvent(event, event->points().first().position()); + event->accept(); + } else { + QQuickItem::mouseReleaseEvent(event); + } +} + +void SwipeArea::mouseUngrabEvent() +{ + QQuickItem::mouseUngrabEvent(); +} + +void SwipeArea::touchEvent(QTouchEvent *event) +{ + bool unhandled = true; + const auto &firstPoint = event->points().first(); + + switch (firstPoint.state()) { + case QEventPoint::State::Pressed: + if (m_interactive) { + handlePressEvent(event, firstPoint.position()); + event->accept(); + unhandled = false; + } + break; + case QEventPoint::State::Updated: + if (m_interactive) { + handleMoveEvent(event, firstPoint.position()); + event->accept(); + unhandled = false; + } + break; + case QEventPoint::State::Released: + if (m_interactive) { + handleReleaseEvent(event, firstPoint.position()); + event->accept(); + unhandled = false; + } + break; + case QEventPoint::State::Stationary: + case QEventPoint::State::Unknown: + break; + } + + if (unhandled) { + QQuickItem::touchEvent(event); + } +} + +void SwipeArea::touchUngrabEvent() +{ + QQuickItem::touchUngrabEvent(); +} + +void SwipeArea::setInteractive(bool interactive) +{ + m_interactive = interactive; + Q_EMIT interactiveChanged(); +} + +void SwipeArea::setMoving(bool moving) +{ + m_moving = moving; + Q_EMIT movingChanged(); +} + +void SwipeArea::setPressed(bool pressed) +{ + m_pressed = pressed; + Q_EMIT pressedChanged(); +} + +void SwipeArea::resetSwipe() +{ + m_stealMouse = false; + if (m_pressed) { + setPressed(false); + } + if (m_moving) { + setMoving(false); + } +} + +void SwipeArea::handlePressEvent(QPointerEvent *event, QPointF point) +{ + // ignore more touch events + if (m_pressed) { + return; + } + + setPressed(true); + m_stealMouse = false; + m_pressPos = point; + m_lastPos = m_pressPos; +} + +void SwipeArea::handleReleaseEvent(QPointerEvent *event, QPointF point) +{ + resetSwipe(); + Q_EMIT swipeEnded(); +} + +void SwipeArea::handleMoveEvent(QPointerEvent *event, QPointF point) +{ + const QVector2D totalDelta = QVector2D(point - m_startPos); + const QVector2D delta = QVector2D(point - m_lastPos); + + m_lastPos = point; + + if (!m_stealMouse) { + // if we haven't reached the swipe registering threshold yet, don't start the swipe + if (qAbs(point.manhattanLength() - m_pressPos.manhattanLength()) < SWIPE_REGISTER_THRESHOLD) { + return; + } + + // we now start the swipe, stealing it from children + + m_startPos = point; + m_stealMouse = true; + setMoving(true); + Q_EMIT swipeStarted(m_startPos); + } + + // ensure it's called AFTER swipeStarted() + Q_EMIT swipeMove(totalDelta.x(), totalDelta.y(), delta.x(), delta.y()); +} diff --git a/components/mobileshell/components/swipearea.h b/components/mobileshell/components/swipearea.h new file mode 100644 index 00000000..16674179 --- /dev/null +++ b/components/mobileshell/components/swipearea.h @@ -0,0 +1,91 @@ +// SPDX-FileCopyrightText: 2023 Devin Lin +// SPDX-License-Identifier: LGPL-2.0-or-later + +#pragma once + +#include +#include +#include +#include +#include +#include + +/** + * @short A component that provides access to swipes over its children, similar to Flickable. + * However, it does not do any of the positioning Flickable does, and so it + * can be used to build custom components with specialized swiping needs (ex. panels) + * + * TODO: New fingers that come in should steal from the old finger + * + * @author Devin Lin + */ +class SwipeArea : public QQuickItem +{ + Q_OBJECT + Q_PROPERTY(bool interactive READ interactive NOTIFY interactiveChanged) + Q_PROPERTY(bool moving READ moving NOTIFY movingChanged) + Q_PROPERTY(bool pressed READ pressed NOTIFY pressedChanged) + + QML_NAMED_ELEMENT(SwipeArea) + +public: + SwipeArea(QQuickItem *parent = nullptr); + + bool interactive(); + bool moving(); + bool pressed(); + +Q_SIGNALS: + void interactiveChanged(); + void movingChanged(); + void pressedChanged(); + + void swipeEnded(); + void swipeStarted(QPointF point); + + // deltaX, deltaY - amount moved since last swipeMove() + // totalDeltaX, totalDeltaY - amount move since startedSwipe() + void swipeMove(qreal totalDeltaX, qreal totalDeltaY, qreal deltaX, qreal deltaY); + +protected: + bool childMouseEventFilter(QQuickItem *item, QEvent *event) override; + void mouseMoveEvent(QMouseEvent *event) override; + void mousePressEvent(QMouseEvent *event) override; + void mouseReleaseEvent(QMouseEvent *event) override; + void mouseUngrabEvent() override; + void touchEvent(QTouchEvent *event) override; + void touchUngrabEvent() override; + +private: + void setInteractive(bool interactive); + void setMoving(bool moving); + void setPressed(bool pressed); + + bool filterPointerEvent(QQuickItem *receiver, QPointerEvent *event); + + void handlePressEvent(QPointerEvent *event, QPointF point); + void handleReleaseEvent(QPointerEvent *event, QPointF point); + void handleMoveEvent(QPointerEvent *event, QPointF point); + + void resetSwipe(); + + bool m_interactive = true; + bool m_pressed = false; + + // whether we have started a flick + bool m_moving = false; + + // whether on this current flick, we want to steal the mouse/touch event from children + bool m_stealMouse = false; + + // the point where the user pressed down on at the start of the interaction + QPointF m_pressPos; + + // the point where the swipe actually started being registered (can be some distance from the pressed position) + QPointF m_startPos; + + // the previous point where interaction was at + QPointF m_lastPos; +}; + +QML_DECLARE_TYPE(SwipeArea) diff --git a/components/mobileshell/mobileshellplugin.cpp b/components/mobileshell/mobileshellplugin.cpp index 45fc9225..86068584 100644 --- a/components/mobileshell/mobileshellplugin.cpp +++ b/components/mobileshell/mobileshellplugin.cpp @@ -10,6 +10,7 @@ #include #include "components/direction.h" +#include "components/swipearea.h" #include "notifications/notificationfilemenu.h" #include "notifications/notificationthumbnailer.h" @@ -31,6 +32,7 @@ void MobileShellPlugin::registerTypes(const char *uri) // components qmlRegisterType(uri, 1, 0, "Direction"); + qmlRegisterType(uri, 1, 0, "SwipeArea"); // notifications qmlRegisterType(uri, 1, 0, "NotificationThumbnailer");