Forward touchEvents to children inside QGraphicsProxyWidget

This reapplies the fix from 1ecf2212fa,
using QApplication::translateRawTouchEvent to dispatch the touch event
received by the QGraphicsProxyWidget to the relevant child widgets
under each touch point.

In addition, limit the implicit grabbing of each touch point before
sending the event to those cases where we have to: touch pads, and
if the target widget comes from a closest-widget matching. And don't
call the QTouchEvent override of QEvent::setAccepted() on QTouchEvent
instances in QGraphicsView classes, as this will override each event
point's acceptance state.

This way, we can identify which touch points have been accepted after
event delivery, any only implicitly grab those points that were.
Otherwise, touch points not accepted by a proxied widget hierarchy
will still be part of an accepted event, and be grabbed by the
viewport of the QGraphicsView. This would then lead to infinite
recursion when the QGraphicsProxyWidget passes the TouchUpdate event
on to each touch point's grabber.

Re-activate the test case, and extend it with more combinations.
Refactor touch-event recording to make it easier to test multi-touch
scenarios.

Task-number: QTBUG-45737
Fixes: QTBUG-67819
Pick-to: 6.2
Change-Id: Id5611f4feecb43b9367d9c2c71ad863b117efbcb
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
This commit is contained in:
Volker Hilsheimer 2021-08-29 23:00:48 +02:00 committed by Shawn Rutledge
parent 78dee15da4
commit dad1e14941
4 changed files with 160 additions and 81 deletions

View File

@ -922,12 +922,9 @@ bool QGraphicsProxyWidget::event(QEvent *event)
case QEvent::TouchBegin: case QEvent::TouchBegin:
case QEvent::TouchUpdate: case QEvent::TouchUpdate:
case QEvent::TouchEnd: { case QEvent::TouchEnd: {
if (event->spontaneous()) QTouchEvent *touchEvent = static_cast<QTouchEvent *>(event);
qt_sendSpontaneousEvent(d->widget, event); bool res = QApplicationPrivate::translateRawTouchEvent(d->widget, touchEvent);
else if (res & touchEvent->isAccepted())
QCoreApplication::sendEvent(d->widget, event);
if (event->isAccepted())
return true; return true;
break; break;

View File

@ -5980,7 +5980,8 @@ void QGraphicsScenePrivate::touchEventHandler(QTouchEvent *sceneTouchEvent)
break; break;
} }
} }
sceneTouchEvent->setAccepted(ignoreSceneTouchEvent); // don't override the acceptance state of the individual points
sceneTouchEvent->QInputEvent::setAccepted(ignoreSceneTouchEvent);
} }
bool QGraphicsScenePrivate::sendTouchBeginEvent(QGraphicsItem *origin, QTouchEvent *touchEvent) bool QGraphicsScenePrivate::sendTouchBeginEvent(QGraphicsItem *origin, QTouchEvent *touchEvent)
@ -6050,7 +6051,8 @@ bool QGraphicsScenePrivate::sendTouchBeginEvent(QGraphicsItem *origin, QTouchEve
break; break;
} }
touchEvent->setAccepted(eventAccepted); // don't override the acceptance state of the touch points
touchEvent->QInputEvent::setAccepted(eventAccepted);
return res; return res;
} }

View File

@ -3911,8 +3911,13 @@ void QApplicationPrivate::activateImplicitTouchGrab(QWidget *widget, QTouchEvent
if (touchEvent->type() != QEvent::TouchBegin) if (touchEvent->type() != QEvent::TouchBegin)
return; return;
for (int i = 0; i < touchEvent->pointCount(); ++i) // If the widget dispatched the event further (see QGraphicsProxyWidget), then
QMutableEventPoint::from(touchEvent->point(i)).setTarget(widget); // there might already be an implicit grabber. Don't override that.
for (int i = 0; i < touchEvent->pointCount(); ++i) {
auto &mep = QMutableEventPoint::from(touchEvent->point(i));
if (!mep.target() && mep.isAccepted())
mep.setTarget(widget);
}
// TODO setExclusiveGrabber() to be consistent with Qt Quick? // TODO setExclusiveGrabber() to be consistent with Qt Quick?
} }
@ -3946,16 +3951,21 @@ bool QApplicationPrivate::translateRawTouchEvent(QWidget *window, const QTouchEv
target = window; target = window;
} }
bool usingClosestWidget = false;
if (device->type() == QInputDevice::DeviceType::TouchScreen) { if (device->type() == QInputDevice::DeviceType::TouchScreen) {
QWidget *closestWidget = d->findClosestTouchPointTarget(device, touchPoint); QWidget *closestWidget = d->findClosestTouchPointTarget(device, touchPoint);
QWidget *widget = static_cast<QWidget *>(target.data()); QWidget *widget = static_cast<QWidget *>(target.data());
if (closestWidget if (closestWidget
&& (widget->isAncestorOf(closestWidget) || closestWidget->isAncestorOf(widget))) { && (widget->isAncestorOf(closestWidget) || closestWidget->isAncestorOf(widget))) {
target = closestWidget; target = closestWidget;
usingClosestWidget = true;
} }
} }
QMutableEventPoint::from(touchPoint).setTarget(target); // on touch pads, implicitly grab all touch points
// on touch screens, grab touch points that are redirected to the closest widget
if (device->type() == QInputDevice::DeviceType::TouchPad || usingClosestWidget)
QMutableEventPoint::from(touchPoint).setTarget(target);
} else { } else {
target = QMutableEventPoint::from(touchPoint).target(); target = QMutableEventPoint::from(touchPoint).target();
if (!target) if (!target)
@ -4020,7 +4030,9 @@ bool QApplicationPrivate::translateRawTouchEvent(QWidget *window, const QTouchEv
{ {
// if the TouchBegin handler recurses, we assume that means the event // if the TouchBegin handler recurses, we assume that means the event
// has been implicitly accepted and continue to send touch events // has been implicitly accepted and continue to send touch events
if (QApplication::sendSpontaneousEvent(widget, &touchEvent) && touchEvent.isAccepted()) { bool res = te->spontaneous() ? QApplication::sendSpontaneousEvent(widget, &touchEvent)
: QApplication::sendEvent(widget, &touchEvent);
if (res && touchEvent.isAccepted()) {
accepted = true; accepted = true;
if (!widget.isNull()) if (!widget.isNull())
widget->setAttribute(Qt::WA_WState_AcceptedTouchBeginEvent); widget->setAttribute(Qt::WA_WState_AcceptedTouchBeginEvent);
@ -4033,7 +4045,9 @@ bool QApplicationPrivate::translateRawTouchEvent(QWidget *window, const QTouchEv
|| QGestureManager::gesturePending(widget) || QGestureManager::gesturePending(widget)
#endif #endif
) { ) {
if (QApplication::sendSpontaneousEvent(widget, &touchEvent) && touchEvent.isAccepted()) bool res = te->spontaneous() ? QApplication::sendSpontaneousEvent(widget, &touchEvent)
: QApplication::sendEvent(widget, &touchEvent);
if (res && touchEvent.isAccepted())
accepted = true; accepted = true;
// widget can be deleted on TouchEnd // widget can be deleted on TouchEnd
if (touchEvent.type() == QEvent::TouchEnd && !widget.isNull()) if (touchEvent.type() == QEvent::TouchEnd && !widget.isNull())

View File

@ -34,6 +34,8 @@
#include <private/qgraphicsproxywidget_p.h> #include <private/qgraphicsproxywidget_p.h>
#include <private/qlayoutengine_p.h> // qSmartMin functions... #include <private/qlayoutengine_p.h> // qSmartMin functions...
Q_LOGGING_CATEGORY(lcTests, "qt.widgets.tests")
/* /*
Notes: Notes:
@ -3834,9 +3836,19 @@ void tst_QGraphicsProxyWidget::touchEventPropagation()
QPushButton *pushButton2 = new QPushButton("Two"); QPushButton *pushButton2 = new QPushButton("Two");
pushButton2->setObjectName("pushButton2"); pushButton2->setObjectName("pushButton2");
pushButton2->setAttribute(Qt::WA_AcceptTouchEvents, true); pushButton2->setAttribute(Qt::WA_AcceptTouchEvents, true);
TouchWidget *touchWidget1 = new TouchWidget;
touchWidget1->setObjectName("touchWidget1");
touchWidget1->setAttribute(Qt::WA_AcceptTouchEvents, true);
touchWidget1->setFixedSize(pushButton1->sizeHint());
TouchWidget *touchWidget2 = new TouchWidget;
touchWidget2->setObjectName("touchWidget2");
touchWidget2->setAttribute(Qt::WA_AcceptTouchEvents, true);
touchWidget2->setFixedSize(pushButton2->sizeHint());
QVBoxLayout *vbox = new QVBoxLayout; QVBoxLayout *vbox = new QVBoxLayout;
vbox->addWidget(pushButton1); vbox->addWidget(pushButton1);
vbox->addWidget(pushButton2); vbox->addWidget(pushButton2);
vbox->addWidget(touchWidget1);
vbox->addWidget(touchWidget2);
formWidget->setLayout(vbox); formWidget->setLayout(vbox);
QGraphicsProxyWidget *formProxy = scene.addWidget(formWidget); QGraphicsProxyWidget *formProxy = scene.addWidget(formWidget);
formProxy->setAcceptTouchEvents(true); formProxy->setAcceptTouchEvents(true);
@ -3846,6 +3858,7 @@ void tst_QGraphicsProxyWidget::touchEventPropagation()
view.setFixedSize(scene.width(), scene.height()); view.setFixedSize(scene.width(), scene.height());
view.verticalScrollBar()->setValue(0); view.verticalScrollBar()->setValue(0);
view.horizontalScrollBar()->setValue(0); view.horizontalScrollBar()->setValue(0);
view.viewport()->setObjectName("GraphicsView's Viewport");
view.show(); view.show();
QVERIFY(QTest::qWaitForWindowExposed(&view)); QVERIFY(QTest::qWaitForWindowExposed(&view));
@ -3857,13 +3870,18 @@ void tst_QGraphicsProxyWidget::touchEventPropagation()
struct TouchRecord { struct TouchRecord {
QObject *receiver; QObject *receiver;
QEvent::Type eventType; QEvent::Type eventType;
QHash<int, QPointF> positions; QPointF position;
}; };
QList<TouchRecord> records; QHash<int, QList<TouchRecord>> records;
QWidget *mousePressReceiver = nullptr;
int count() const { return records.count(); } int count(int id = 0) const { return records.value(id).count(); }
TouchRecord at(int i) const { return records.at(i); } TouchRecord at(int i, int id = 0) const { return records.value(id).at(i); }
void clear() { records.clear(); } void clear()
{
records.clear();
mousePressReceiver = nullptr;
}
protected: protected:
bool eventFilter(QObject *receiver, QEvent *event) override bool eventFilter(QObject *receiver, QEvent *event) override
{ {
@ -3874,12 +3892,14 @@ void tst_QGraphicsProxyWidget::touchEventPropagation()
case QEvent::TouchEnd: { case QEvent::TouchEnd: {
QTouchEvent *touchEvent = static_cast<QTouchEvent *>(event); QTouchEvent *touchEvent = static_cast<QTouchEvent *>(event);
// instead of detaching each QEventPoint, just store the relative positions // instead of detaching each QEventPoint, just store the relative positions
QHash<int, QPointF> positions;
for (const auto &touchPoint : touchEvent->points()) for (const auto &touchPoint : touchEvent->points())
positions[touchPoint.id()] = touchPoint.position(); records[touchPoint.id()] << TouchRecord{receiver, event->type(), touchPoint.position()};
records << TouchRecord{receiver, event->type(), positions}; qCDebug(lcTests) << "Recording" << event << receiver;
break; break;
} }
case QEvent::MouseButtonPress:
mousePressReceiver = qobject_cast<QWidget*>(receiver);
break;
default: default:
break; break;
} }
@ -3902,17 +3922,17 @@ void tst_QGraphicsProxyWidget::touchEventPropagation()
}; };
// verify that the embedded widget gets the correctly translated event // verify that the embedded widget gets the correctly translated event
{ QTest::touchEvent(&view, touchDevice).press(0, simpleCenter.toPoint());
auto sequence = QTest::touchEvent(view.viewport(), touchDevice);
sequence.press(0, simpleCenter.toPoint());
}
// window, viewport, scene, simpleProxy, simpleWidget // window, viewport, scene, simpleProxy, simpleWidget
QCOMPARE(eventSpy.count(), 5); QCOMPARE(eventSpy.count(), 5);
auto record = eventSpy.at(eventSpy.count() - 1); QCOMPARE(eventSpy.at(0).receiver, view.windowHandle());
QCOMPARE(eventSpy.at(1).receiver, view.viewport());
QCOMPARE(eventSpy.at(2).receiver, &scene);
QCOMPARE(eventSpy.at(3).receiver, simpleProxy);
auto record = eventSpy.at(4);
QCOMPARE(record.receiver, simpleWidget); QCOMPARE(record.receiver, simpleWidget);
QCOMPARE(record.eventType, QEvent::TouchBegin); QCOMPARE(record.eventType, QEvent::TouchBegin);
QCOMPARE(record.positions.count(), 1); QVERIFY(closeEnough(record.position, simpleCenter));
QVERIFY(closeEnough(record.positions[0], simpleCenter));
eventSpy.clear(); eventSpy.clear();
// verify that the layout of formWidget is how we expect it to be // verify that the layout of formWidget is how we expect it to be
@ -3921,67 +3941,113 @@ void tst_QGraphicsProxyWidget::touchEventPropagation()
QCOMPARE(formWidget->childAt(pushButton1->pos() + pb1Center), pushButton1); QCOMPARE(formWidget->childAt(pushButton1->pos() + pb1Center), pushButton1);
const QPoint pb2Center = pushButton2->rect().center(); const QPoint pb2Center = pushButton2->rect().center();
QCOMPARE(formWidget->childAt(pushButton2->pos() + pb2Center), pushButton2); QCOMPARE(formWidget->childAt(pushButton2->pos() + pb2Center), pushButton2);
const QPoint tw1Center = touchWidget1->rect().center();
QCOMPARE(formWidget->childAt(touchWidget1->pos() + tw1Center), touchWidget1);
const QPoint tw2Center = touchWidget2->rect().center();
QCOMPARE(formWidget->childAt(touchWidget2->pos() + tw2Center), touchWidget2);
// touch events are sent to the view, in view coordinates
const QPoint formProxyPox = view.mapFromScene(formProxy->pos().toPoint());
const QPoint pb1TouchPos = pushButton1->pos() + pb1Center + formProxyPox;
const QPoint pb2TouchPos = pushButton2->pos() + pb2Center + formProxyPox;
const QPoint tw1TouchPos = touchWidget1->pos() + tw1Center + formProxyPox;
const QPoint tw2TouchPos = touchWidget2->pos() + tw2Center + formProxyPox;
QSignalSpy clickedSpy(pushButton1, &QPushButton::clicked);
// Single touch point to nested widget not accepting event. // Single touch point to nested widget not accepting event.
// Event should bubble up and translate correctly. // Event should bubble up and translate correctly, TouchUpdate and TouchEnd events
{ // stop at the window since nobody accepted the TouchBegin. A mouse event will be generated.
auto sequence = QTest::touchEvent(view.viewport(), touchDevice); QTest::touchEvent(&view, touchDevice).press(0, pb1TouchPos);
sequence.press(0, pushButton1->pos() + pb1Center + formProxy->pos().toPoint()); QTest::touchEvent(&view, touchDevice).move(0, pb1TouchPos + QPoint(1, 1));
} QTest::touchEvent(&view, touchDevice).release(0, pb1TouchPos + QPoint(1, 1));
// ..., formProxy, pushButton1, formWidget // ..., formProxy, pushButton1, formWidget, window, window
QEXPECT_FAIL("", "Touch events are not forwarded to children - QTBUG-67819", Abort);
QCOMPARE(eventSpy.count(), 6);
record = eventSpy.at(eventSpy.count() - 2);
QCOMPARE(record.receiver, pushButton1);
QVERIFY(closeEnough(record.positions[0], pb1Center));
QCOMPARE(record.eventType, QEvent::TouchBegin);
// pushButton doesn't accept the point, so it propagates to parent
record = eventSpy.at(eventSpy.count() - 1);
QCOMPARE(record.receiver, formWidget);
QVERIFY(closeEnough(record.positions[0], pushButton1->pos() + pb1Center));
QCOMPARE(record.eventType, QEvent::TouchBegin);
eventSpy.clear();
// multi-touch to different widgets
{
auto sequence = QTest::touchEvent(view.viewport(), touchDevice);
sequence.press(0, pushButton1->pos() + pb1Center + formProxy->pos().toPoint());
sequence.press(1, pushButton2->pos() + pb2Center + formProxy->pos().toPoint());
}
// window, viewport, scene, formProxy, pushButton1, formWidget, pushButton2, formWidget
QCOMPARE(eventSpy.count(), 8); QCOMPARE(eventSpy.count(), 8);
QCOMPARE(eventSpy.at(3).receiver, formProxy); // formProxy dispatches to the right subwidget
record = eventSpy.at(4); record = eventSpy.at(4);
// the order in which the two presses are delivered is not defined QCOMPARE(record.receiver, pushButton1);
const bool pb1First = record.receiver == pushButton1; QVERIFY(closeEnough(record.position, pb1Center));
if (pb1First) { QCOMPARE(record.eventType, QEvent::TouchBegin);
QCOMPARE(record.receiver, pushButton1); // pushButton doesn't accept the point, so the TouchBegin propagates to parent
QVERIFY(closeEnough(record.positions[0], pb1Center));
} else {
QCOMPARE(record.receiver, pushButton2);
QVERIFY(closeEnough(record.positions[1], pb2Center));
}
record = eventSpy.at(5); record = eventSpy.at(5);
QCOMPARE(record.receiver, formWidget); QCOMPARE(record.receiver, formWidget);
if (pb1First) { QVERIFY(closeEnough(record.position, pushButton1->pos() + pb1Center));
QVERIFY(closeEnough(record.positions[0], pushButton1->pos() + pb1Center)); QCOMPARE(record.eventType, QEvent::TouchBegin);
} else {
QVERIFY(closeEnough(record.positions[1], pushButton2->pos() + pb2Center));
}
record = eventSpy.at(6); record = eventSpy.at(6);
if (pb1First) { QCOMPARE(record.receiver, view.windowHandle());
QCOMPARE(record.receiver, pushButton2); QCOMPARE(record.eventType, QEvent::TouchUpdate);
QVERIFY(closeEnough(record.positions[1], pb2Center));
} else {
QCOMPARE(record.receiver, pushButton1);
QVERIFY(closeEnough(record.positions[0], pb1Center));
}
record = eventSpy.at(7); record = eventSpy.at(7);
QCOMPARE(record.receiver, formWidget); QCOMPARE(record.receiver, view.windowHandle());
if (pb1First) { QCOMPARE(record.eventType, QEvent::TouchEnd);
QVERIFY(closeEnough(record.positions[1], pushButton2->pos() + pb2Center)); QCOMPARE(eventSpy.mousePressReceiver, pushButton1);
} else { QCOMPARE(clickedSpy.count(), 1);
QVERIFY(closeEnough(record.positions[0], pushButton1->pos() + pb1Center)); eventSpy.clear();
} clickedSpy.clear();
// Single touch point to nested widget accepting event.
QTest::touchEvent(&view, touchDevice).press(0, tw1TouchPos);
QTest::touchEvent(&view, touchDevice).move(0, tw1TouchPos + QPoint(5, 5));
QTest::touchEvent(&view, touchDevice).release(0, tw1TouchPos + QPoint(5, 5));
// Press: ..., formProxy, touchWidget1 (5)
// Move: window, touchWidget1 (2)
// Release: window, touchWidget1 (2)
QCOMPARE(eventSpy.count(), 9);
QCOMPARE(eventSpy.at(3).receiver, formProxy); // form proxy dispatches TouchBegin to the right widget
record = eventSpy.at(4);
QCOMPARE(record.receiver, touchWidget1);
QVERIFY(closeEnough(record.position, tw1Center));
QCOMPARE(record.eventType, QEvent::TouchBegin);
QCOMPARE(eventSpy.at(5).receiver, view.windowHandle()); // QWidgetWindow dispatches TouchUpdate
record = eventSpy.at(6);
QCOMPARE(record.receiver, touchWidget1);
QVERIFY(closeEnough(record.position, tw1Center + QPoint(5, 5)));
QCOMPARE(record.eventType, QEvent::TouchUpdate);
QCOMPARE(eventSpy.at(7).receiver, view.windowHandle()); // QWidgetWindow dispatches TouchEnd
record = eventSpy.at(8);
QCOMPARE(record.receiver, touchWidget1);
QVERIFY(closeEnough(record.position, tw1Center + QPoint(5, 5)));
QCOMPARE(record.eventType, QEvent::TouchEnd);
eventSpy.clear();
// to simplify the remaining test, install the event spy explicitly on the target widgets
qApp->removeEventFilter(&eventSpy);
formWidget->installEventFilter(&eventSpy);
pushButton1->installEventFilter(&eventSpy);
pushButton2->installEventFilter(&eventSpy);
touchWidget1->installEventFilter(&eventSpy);
touchWidget2->installEventFilter(&eventSpy);
// multi-touch to different widgets, some do and some don't accept the event
QTest::touchEvent(&view, touchDevice)
.press(0, pb1TouchPos)
.press(1, tw1TouchPos)
.press(2, pb2TouchPos)
.press(3, tw2TouchPos);
QTest::touchEvent(&view, touchDevice)
.move(0, pb1TouchPos + QPoint(1, 1))
.move(1, tw1TouchPos + QPoint(1, 1))
.move(2, pb2TouchPos + QPoint(1, 1))
.move(3, tw2TouchPos + QPoint(1, 1));
QTest::touchEvent(&view, touchDevice)
.release(0, pb1TouchPos + QPoint(1, 1))
.release(1, tw1TouchPos + QPoint(1, 1))
.release(2, pb2TouchPos + QPoint(1, 1))
.release(3, tw2TouchPos + QPoint(1, 1));
QCOMPARE(eventSpy.count(0), 2); // Begin never accepted, so move up and then stop
QCOMPARE(eventSpy.count(1), 3); // Begin accepted, so not propagated and update/end received
QCOMPARE(eventSpy.count(2), 2); // Begin never accepted
QCOMPARE(eventSpy.count(3), 3); // Begin accepted
QCOMPARE(eventSpy.at(0, 0).receiver, pushButton1);
QCOMPARE(eventSpy.at(1, 0).receiver, formWidget);
QCOMPARE(eventSpy.at(0, 1).receiver, touchWidget1);
QCOMPARE(eventSpy.at(1, 1).receiver, touchWidget1);
QCOMPARE(eventSpy.at(2, 1).receiver, touchWidget1);
QCOMPARE(eventSpy.at(0, 2).receiver, pushButton2);
QCOMPARE(eventSpy.at(1, 2).receiver, formWidget);
QCOMPARE(eventSpy.at(0, 3).receiver, touchWidget2);
QCOMPARE(eventSpy.at(1, 3).receiver, touchWidget2);
QCOMPARE(eventSpy.at(2, 3).receiver, touchWidget2);
QCOMPARE(clickedSpy.count(), 0); // multi-touch event does not synthesize a mouse event
} }
QTEST_MAIN(tst_QGraphicsProxyWidget) QTEST_MAIN(tst_QGraphicsProxyWidget)