Implement support for passive grabbers with mouse/touch events

Send mouse and touch events through passive grabbers.

With support for passive grabbers, the drawer can now make itself a
passive grabber for the mouse when it is pressed inside the margin
area. It will then see the update events that are delivered to the
exclusive grabber, and can try to grab the mouse when the e.g. move
has progressed enough. Then the earlier grabber gets an ungrab event
that allows it to cancel the action.

Done-with: Doris Verria <doris.verria@qt.io>
Fixes: QTBUG-59141
Change-Id: Ie8e79c2ec2aa1f5a00056f23856bd0bed19af2d6
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
This commit is contained in:
Volker Hilsheimer 2022-05-06 17:46:37 +02:00 committed by Shawn Rutledge
parent 6d2d4914f8
commit 375e400390
5 changed files with 241 additions and 29 deletions

View File

@ -912,29 +912,44 @@ void QQuickDeliveryAgentPrivate::deliverToPassiveGrabbers(const QVector<QPointer
QQuickPointerHandlerPrivate::deviceDeliveryTargets(pointerEvent->device());
QVarLengthArray<QPair<QQuickItem *, bool>, 4> sendFilteredPointerEventResult;
hasFiltered.clear();
for (auto o : passiveGrabbers) {
QQuickPointerHandler *handler = qobject_cast<QQuickPointerHandler *>(o);
for (QObject *grabberObject : passiveGrabbers) {
// a null pointer in passiveGrabbers is unlikely, unless the grabbing handler was deleted dynamically
if (Q_LIKELY(handler) && !eventDeliveryTargets.contains(handler)) {
bool alreadyFiltered = false;
QQuickItem *par = handler->parentItem();
if (Q_UNLIKELY(!grabberObject))
continue;
// a passiveGrabber might be an item or a handler
if (QQuickPointerHandler *handler = qobject_cast<QQuickPointerHandler *>(grabberObject)) {
if (handler && !eventDeliveryTargets.contains(handler)) {
bool alreadyFiltered = false;
QQuickItem *par = handler->parentItem();
// see if we already have sent a filter event to the parent
auto it = std::find_if(sendFilteredPointerEventResult.begin(), sendFilteredPointerEventResult.end(),
[par](const QPair<QQuickItem *, bool> &pair) { return pair.first == par; });
if (it != sendFilteredPointerEventResult.end()) {
// Yes, the event was already filtered to that parent, do not call it again but use
// the result of the previous call to determine if we should call the handler.
alreadyFiltered = it->second;
} else if (par) {
alreadyFiltered = sendFilteredPointerEvent(pointerEvent, par);
sendFilteredPointerEventResult << qMakePair(par, alreadyFiltered);
// see if we already have sent a filter event to the parent
auto it = std::find_if(sendFilteredPointerEventResult.begin(), sendFilteredPointerEventResult.end(),
[par](const QPair<QQuickItem *, bool> &pair) { return pair.first == par; });
if (it != sendFilteredPointerEventResult.end()) {
// Yes, the event was sent to that parent for filtering: do not call it again, but use
// the result of the previous call to determine whether we should call the handler.
alreadyFiltered = it->second;
} else if (par) {
alreadyFiltered = sendFilteredPointerEvent(pointerEvent, par);
sendFilteredPointerEventResult << qMakePair(par, alreadyFiltered);
}
if (!alreadyFiltered) {
if (par)
localizePointerEvent(pointerEvent, par);
handler->handlePointerEvent(pointerEvent);
}
}
if (!alreadyFiltered) {
if (par)
localizePointerEvent(pointerEvent, par);
handler->handlePointerEvent(pointerEvent);
} else if (QQuickItem *grabberItem = static_cast<QQuickItem *>(grabberObject)) {
// don't steal the grab if input should remain with the exclusive grabber only
if (QQuickItem *excGrabber = static_cast<QQuickItem *>(pointerEvent->exclusiveGrabber(pointerEvent->point(0)))) {
if ((isMouseEvent(pointerEvent) && excGrabber->keepMouseGrab())
|| (isTouchEvent(pointerEvent) && excGrabber->keepTouchGrab())) {
return;
}
}
localizePointerEvent(pointerEvent, grabberItem);
QCoreApplication::sendEvent(grabberItem, pointerEvent);
pointerEvent->accept();
}
}
}
@ -1184,6 +1199,21 @@ bool QQuickDeliveryAgentPrivate::deliverSinglePointEventUntilAccepted(QPointerEv
QVector<QQuickItem *> targetItems = pointerTargets(rootItem, event, point, false, false);
point.setAccepted(false);
// Let passive grabbers see the event. This must be done before we deliver the
// event to the target and to handlers that might stop event propagation.
// Passive grabbers cannot stop event delivery.
for (const auto &passiveGrabber : event->passiveGrabbers(point)) {
if (auto *grabberItem = qobject_cast<QQuickItem *>(passiveGrabber)) {
if (targetItems.contains(grabberItem))
continue;
localizePointerEvent(event, grabberItem);
QCoreApplication::sendEvent(grabberItem, event);
}
}
// Maintain the invariant that items receive input events in accepted state.
// A passive grabber might have explicitly ignored the event.
event->accept();
for (QQuickItem *item : targetItems) {
QQuickItemPrivate *itemPrivate = QQuickItemPrivate::get(item);
localizePointerEvent(event, item);

View File

@ -302,29 +302,36 @@ static bool isWithinDragMargin(const QQuickDrawer *drawer, const QPointF &pos)
bool QQuickDrawerPrivate::startDrag(QEvent *event)
{
Q_Q(QQuickDrawer);
delayedEnterTransition = false;
if (!window || !interactive || dragMargin < 0.0 || qFuzzyIsNull(dragMargin))
return false;
switch (event->type()) {
case QEvent::MouseButtonPress:
if (isWithinDragMargin(q, static_cast<QMouseEvent *>(event)->scenePosition())) {
prepareEnterTransition();
reposition();
return handleMouseEvent(window->contentItem(), static_cast<QMouseEvent *>(event));
if (QMouseEvent *mouseEvent = static_cast<QMouseEvent *>(event); isWithinDragMargin(q, mouseEvent->scenePosition())) {
// watch future events and grab the mouse once it has moved
// sufficiently fast or far (in grabMouse).
delayedEnterTransition = true;
mouseEvent->addPassiveGrabber(mouseEvent->point(0), popupItem);
handleMouseEvent(window->contentItem(), mouseEvent);
return false;
}
break;
#if QT_CONFIG(quicktemplates2_multitouch)
case QEvent::TouchBegin:
case QEvent::TouchUpdate:
for (const QTouchEvent::TouchPoint &point : static_cast<QTouchEvent *>(event)->points()) {
case QEvent::TouchUpdate: {
auto *touchEvent = static_cast<QTouchEvent *>(event);
for (const QTouchEvent::TouchPoint &point : touchEvent->points()) {
if (point.state() == QEventPoint::Pressed && isWithinDragMargin(q, point.scenePosition())) {
prepareEnterTransition();
reposition();
return handleTouchEvent(window->contentItem(), static_cast<QTouchEvent *>(event));
delayedEnterTransition = true;
touchEvent->addPassiveGrabber(point, popupItem);
handleTouchEvent(window->contentItem(), touchEvent);
return false;
}
}
break;
}
#endif
default:
@ -372,6 +379,12 @@ bool QQuickDrawerPrivate::grabMouse(QQuickItem *item, QMouseEvent *event)
}
if (overThreshold) {
if (delayedEnterTransition) {
prepareEnterTransition();
reposition();
delayedEnterTransition = false;
}
popupItem->grabMouse();
popupItem->setKeepMouseGrab(true);
offset = offsetAt(movePoint);
@ -418,7 +431,12 @@ bool QQuickDrawerPrivate::grabTouch(QQuickItem *item, QTouchEvent *event)
}
if (overThreshold) {
popupItem->grabTouchPoints(QList<int>() << touchId);
if (delayedEnterTransition) {
prepareEnterTransition();
reposition();
delayedEnterTransition = false;
}
event->setExclusiveGrabber(point, popupItem);
popupItem->setKeepTouchGrab(true);
offset = offsetAt(movePoint);
}

View File

@ -97,6 +97,7 @@ public:
qreal position = 0;
qreal dragMargin = 0;
QQuickVelocityCalculator velocityCalculator;
bool delayedEnterTransition = false;
};
QT_END_NAMESPACE

View File

@ -0,0 +1,34 @@
import QtQuick
import Test
Item {
width: 320
height: 240
property alias exclusiveGrabber: exGrabber
property alias passiveGrabber: psGrabber
ExclusiveGrabber {
id: exGrabber
width: parent.width
height: 30
color: "blue"
anchors.verticalCenter: parent.verticalCenter
Text {
anchors.centerIn: parent
text: "Exclusive Grabber"
}
}
PassiveGrabber {
id: psGrabber
width: parent.width * 0.3
height: parent.height
color: "yellow"
opacity: 0.5
Text {
anchors.centerIn: parent
text: "Passive Grabber"
rotation: 90
}
}
}

View File

@ -34,6 +34,7 @@
#include <QtQuick/QQuickItem>
#include <QtQuick/QQuickView>
#include <QtQuick/QQuickWindow>
#include <QtQuick/private/qquickrectangle_p.h>
#include <QtQuick/private/qquickflickable_p.h>
#include <QtQuick/private/qquickpointhandler_p.h>
#include <QtQuick/private/qquickshadereffectsource_p.h>
@ -153,6 +154,7 @@ public:
private slots:
void passiveGrabberOrder();
void passiveGrabberItems();
void tapHandlerDoesntOverrideSubsceneGrabber();
void touchCompression();
void hoverPropagation_nested_data();
@ -216,6 +218,133 @@ void tst_qquickdeliveryagent::passiveGrabberOrder()
QCOMPARE(spy.senders.last(), rootTap);
}
class PassiveGrabberItem : public QQuickRectangle
{
public:
PassiveGrabberItem(QQuickItem *parent = nullptr) : QQuickRectangle(parent) {
setAcceptedMouseButtons(Qt::LeftButton);
}
void mousePressEvent(QMouseEvent *event) override {
qCDebug(lcTests) << "Passive grabber pressed";
lastPressed = true;
event->addPassiveGrabber(event->point(0), this);
event->ignore();
}
void mouseMoveEvent(QMouseEvent *event) override {
qCDebug(lcTests) << "Mouse move handled by passive grabber";
const QPointF pos = event->scenePosition();
const int threshold = 20;
bool overThreshold = pos.x() >= threshold;
if (overThreshold) {
event->setExclusiveGrabber(event->point(0), this);
this->setKeepMouseGrab(true);
event->accept();
} else {
event->ignore();
}
}
void mouseReleaseEvent(QMouseEvent *event) override {
qCDebug(lcTests) << "Passive grabber released";
lastPressed = false;
event->ignore();
}
bool lastPressed = false;
};
class ExclusiveGrabberItem : public QQuickRectangle
{
public:
ExclusiveGrabberItem(QQuickItem *parent = nullptr) : QQuickRectangle(parent) {
setAcceptedMouseButtons(Qt::LeftButton);
}
void mousePressEvent(QMouseEvent *event) override {
qCDebug(lcTests) << "Exclusive grabber pressed";
lastPressed = true;
event->accept();
}
void mouseMoveEvent(QMouseEvent *event) override {
event->accept();
}
void mouseReleaseEvent(QMouseEvent *event) override {
qCDebug(lcTests) << "Exclusive grabber released";
lastPressed = false;
event->accept();
}
void mouseUngrabEvent() override {
qCDebug(lcTests) << "Exclusive grab ended";
ungrabbed = true;
}
bool lastPressed = false;
bool ungrabbed = false;
};
void tst_qquickdeliveryagent::passiveGrabberItems()
{
QQuickView view;
QQmlComponent component(view.engine());
qmlRegisterType<PassiveGrabberItem>("Test", 1, 0, "PassiveGrabber");
qmlRegisterType<ExclusiveGrabberItem>("Test", 1, 0, "ExclusiveGrabber");
component.loadUrl(testFileUrl("passiveGrabberItem.qml"));
view.setContent(QUrl(), &component, component.create());
QQuickItem *root = qobject_cast<QQuickItem*>(view.rootObject());
QVERIFY(root);
ExclusiveGrabberItem *exclusiveGrabber = root->property("exclusiveGrabber").value<ExclusiveGrabberItem*>();
PassiveGrabberItem *passiveGrabber = root->property("passiveGrabber").value<PassiveGrabberItem *>();
QVERIFY(exclusiveGrabber);
QVERIFY(passiveGrabber);
view.show();
QVERIFY(QTest::qWaitForWindowActive(&view));
QTest::mousePress(&view, Qt::LeftButton, Qt::NoModifier, QPoint(exclusiveGrabber->x() + 1, exclusiveGrabber->y() + 1));
auto devPriv = QPointingDevicePrivate::get(QPointingDevice::primaryPointingDevice());
const auto &persistentPoint = devPriv->activePoints.values().first();
QTRY_COMPARE(persistentPoint.passiveGrabbers.count(), 1);
QCOMPARE(persistentPoint.passiveGrabbers.first(), passiveGrabber);
QCOMPARE(persistentPoint.exclusiveGrabber, exclusiveGrabber);
QVERIFY(exclusiveGrabber->lastPressed);
QVERIFY(passiveGrabber->lastPressed);
// Mouse move bigger than threshold -> passive grabber becomes exclusive grabber
QTest::mouseMove(&view);
QTRY_COMPARE(persistentPoint.exclusiveGrabber, passiveGrabber);
QVERIFY(exclusiveGrabber->ungrabbed);
QTest::mouseRelease(&view, Qt::LeftButton);
// Only the passive grabber got the release event
// since it became the exclusive grabber on mouseMove
QTRY_VERIFY(!passiveGrabber->lastPressed);
QVERIFY(exclusiveGrabber->lastPressed);
QCOMPARE(persistentPoint.passiveGrabbers.count(), 0);
QCOMPARE(persistentPoint.exclusiveGrabber, nullptr);
exclusiveGrabber->lastPressed = false;
exclusiveGrabber->ungrabbed = false;
passiveGrabber->lastPressed = false;
QTest::mousePress(&view, Qt::LeftButton, Qt::NoModifier, QPoint(exclusiveGrabber->x() + 1, exclusiveGrabber->y() + 1));
const auto &pressedPoint = devPriv->activePoints.values().first();
QTRY_COMPARE(pressedPoint.passiveGrabbers.count(), 1);
QCOMPARE(pressedPoint.passiveGrabbers.first(), passiveGrabber);
QCOMPARE(pressedPoint.exclusiveGrabber, exclusiveGrabber);
QVERIFY(exclusiveGrabber->lastPressed);
QVERIFY(passiveGrabber->lastPressed);
// Mouse move smaller than threshold -> grab remains with the exclusive grabber
QTest::mouseMove(&view, QPoint(exclusiveGrabber->x(), exclusiveGrabber->y()));
QTRY_COMPARE(pressedPoint.exclusiveGrabber, exclusiveGrabber);
QTest::mouseRelease(&view, Qt::LeftButton, Qt::NoModifier, QPoint(exclusiveGrabber->x(), exclusiveGrabber->y()));
// Both the passive and the exclusive grabber get the mouseRelease event
QTRY_VERIFY(!passiveGrabber->lastPressed);
QVERIFY(!exclusiveGrabber->lastPressed);
QCOMPARE(pressedPoint.passiveGrabbers.count(), 0);
QCOMPARE(pressedPoint.exclusiveGrabber, nullptr);
}
void tst_qquickdeliveryagent::tapHandlerDoesntOverrideSubsceneGrabber() // QTBUG-94012
{
QQuickView window;