components: Introduce SwipeArea component

This commit is contained in:
Devin Lin 2023-09-29 22:56:49 -07:00
parent 58faa0362c
commit 2d37ef0771
4 changed files with 362 additions and 0 deletions

View file

@ -8,6 +8,7 @@ set(mobileshellplugin_SRCS
mobileshellplugin.cpp
shellutil.cpp
components/direction.cpp
components/swipearea.cpp
notifications/notificationthumbnailer.cpp
notifications/notificationfilemenu.cpp
)

View file

@ -0,0 +1,268 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <devin@kde.org>
// 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 <QMouseEvent>
#include <QObject>
#include <QTabletEvent>
#include <QTouchEvent>
// 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<QPointerEvent *>(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<QMouseEvent *>(event) && !dynamic_cast<QTabletEvent *>(event) && !dynamic_cast<QTouchEvent *>(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());
}

View file

@ -0,0 +1,91 @@
// SPDX-FileCopyrightText: 2023 Devin Lin <espidev@gmail.com>
// SPDX-License-Identifier: LGPL-2.0-or-later
#pragma once
#include <QMouseEvent>
#include <QPointF>
#include <QPointerEvent>
#include <QQmlListProperty>
#include <QQuickItem>
#include <QTouchEvent>
/**
* @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 <devin@kde.org>
*/
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)

View file

@ -10,6 +10,7 @@
#include <QQuickItem>
#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<Direction>(uri, 1, 0, "Direction");
qmlRegisterType<SwipeArea>(uri, 1, 0, "SwipeArea");
// notifications
qmlRegisterType<NotificationThumbnailer>(uri, 1, 0, "NotificationThumbnailer");