diff options
| author | Shawn Rutledge <shawn.rutledge@qt.io> | 2022-08-22 14:24:33 +0200 |
|---|---|---|
| committer | Shawn Rutledge <shawn.rutledge@qt.io> | 2022-10-23 18:41:56 +0200 |
| commit | 25ab62cce578d50ad7310ffe42b49a5921a8a8c7 (patch) | |
| tree | 9850151f85aa9af963c6c4f9fb232fd5a3ed61e2 | |
| parent | b6d6d15ab80d3ab4b078535b597d854a707160c5 (diff) | |
TapHandler: don't cancel on release
The previous fix for QTBUG-94012 was in conflict with the idea that
TapHandler could be used to augment behavior on other event-handling
Items: it would emit canceled rather than tapped whenever there was a
rival exclusive grabber, unless that grabber was an event-filtering
item. The bug I was trying to fix was in the Qt Quick 3D dynamictexture
example: if you click on a TextEdit in a subscene to set the cursor
position, a TapHandler on the entire View3D should not react, because
something in the subscene was already reacting. The event accepted flag
also does not stop propagation in other subscenes. This bug should
rather be fixed by changing gesturePolicy so that the outer TapHandler
does try to take the exclusive grab, and then it is stolen by TextEdit
because TapHandler's default grabPermissions include
ApprovesTakeOverByAnything.
Handlers in general are useful to augment existing behavior of Items.
At least gesturePolicy and grabPermisions are available to fine-tune the
stealing of the exclusive grab; but when TapHandler is using a stealthy
passive grab (which it does with the default gesturePolicy), we don't
want to prevent it from reacting.
This reverts the TapHandler changes from commit
d342b8f77658537c4fd3318982d2ae7a964b6426 but keeps the autotest, which
is now improved by checking multiple gesturePolicy values and checking
for the canceled signal if the policy specifies an exclusive grab.
Also call QEventPoint::setAccepted(false) if the gesturePolicy is
DragThreshold, because if the TapHandler is getting by with a passive
grab, it should not interfere with delivery to other items. This makes
touch behavior more consistent with mouse behavior.
Fixes: QTBUG-99887
Fixes: QTBUG-105609
Task-number: QTBUG-94012
Change-Id: Ib8f80d4e6523b01ef204a69f1f30f010bee6eb8f
Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
(cherry picked from commit 0f85202699e020f254c5f382787896acc2efd060)
Reviewed-by: Shawn Rutledge <shawn.rutledge@qt.io>
4 files changed, 107 insertions, 17 deletions
diff --git a/src/quick/handlers/qquicktaphandler.cpp b/src/quick/handlers/qquicktaphandler.cpp index 0ec7c0ab65..492105b8c4 100644 --- a/src/quick/handlers/qquicktaphandler.cpp +++ b/src/quick/handlers/qquicktaphandler.cpp @@ -117,24 +117,14 @@ bool QQuickTapHandler::wantsEventPoint(const QPointerEvent *event, const QEventP void QQuickTapHandler::handleEventPoint(QPointerEvent *event, QEventPoint &point) { + const bool isTouch = QQuickDeliveryAgentPrivate::isTouchEvent(event); switch (point.state()) { case QEventPoint::Pressed: setPressed(true, false, event, point); break; case QEventPoint::Released: { - // If the point has an exclusive grabber Item, then if it got the grab by filtering (like Flickable does), - // it's OK for DragHandler to react in spite of that. But in other cases, if an exclusive grab - // still exists at the time of release, TapHandler should not react, because it would be redundant: - // some other item is already reacting, i.e. acting as if it has been clicked or tapped. - // So in that case we cancel the pressed state and do not emit tapped(). - bool nonFilteringExclusiveGrabber = false; - if (auto g = qmlobject_cast<QQuickItem *>(event->exclusiveGrabber(point))) { - if (!g->filtersChildMouseEvents()) - nonFilteringExclusiveGrabber = true; - } - if (QQuickDeliveryAgentPrivate::isTouchEvent(event) || - (static_cast<const QSinglePointEvent *>(event)->buttons() & acceptedButtons()) == Qt::NoButton) - setPressed(false, nonFilteringExclusiveGrabber, event, point); + if (isTouch || (static_cast<const QSinglePointEvent *>(event)->buttons() & acceptedButtons()) == Qt::NoButton) + setPressed(false, false, event, point); break; } default: @@ -142,6 +132,11 @@ void QQuickTapHandler::handleEventPoint(QPointerEvent *event, QEventPoint &point } QQuickSinglePointHandler::handleEventPoint(event, point); + + // If TapHandler only needs a passive grab, it should not block other items and handlers from reacting. + // If the point is accepted, QQuickItemPrivate::localizedTouchEvent() would skip it. + if (isTouch && m_gesturePolicy == DragThreshold) + point.setAccepted(false); } /*! diff --git a/tests/auto/quick/qquickdeliveryagent/tst_qquickdeliveryagent.cpp b/tests/auto/quick/qquickdeliveryagent/tst_qquickdeliveryagent.cpp index ab462fce72..aaa7fd8545 100644 --- a/tests/auto/quick/qquickdeliveryagent/tst_qquickdeliveryagent.cpp +++ b/tests/auto/quick/qquickdeliveryagent/tst_qquickdeliveryagent.cpp @@ -131,6 +131,7 @@ public: private slots: void passiveGrabberOrder(); void passiveGrabberItems(); + void tapHandlerDoesntOverrideSubsceneGrabber_data(); void tapHandlerDoesntOverrideSubsceneGrabber(); void undoDelegationWhenSubsceneFocusCleared(); void touchCompression(); @@ -322,8 +323,25 @@ void tst_qquickdeliveryagent::passiveGrabberItems() QCOMPARE(pressedPoint.exclusiveGrabber, nullptr); } +void tst_qquickdeliveryagent::tapHandlerDoesntOverrideSubsceneGrabber_data() +{ + QTest::addColumn<QQuickTapHandler::GesturePolicy>("gesturePolicy"); + QTest::addColumn<int>("expectedTaps"); + QTest::addColumn<int>("expectedCancels"); + // TapHandler gets passive grab => "stealth" tap, regardless of other Items + QTest::newRow("DragThreshold") << QQuickTapHandler::DragThreshold << 1 << 0; + // TapHandler gets exclusive grab => it's cancelled when the TextEdit takes the grab + QTest::newRow("WithinBounds") << QQuickTapHandler::WithinBounds << 0 << 2; // 2 because of QTBUG-105865 + QTest::newRow("ReleaseWithinBounds") << QQuickTapHandler::ReleaseWithinBounds << 0 << 2; + QTest::newRow("DragWithinBounds") << QQuickTapHandler::DragWithinBounds << 0 << 2; +} + void tst_qquickdeliveryagent::tapHandlerDoesntOverrideSubsceneGrabber() // QTBUG-94012 { + QFETCH(QQuickTapHandler::GesturePolicy, gesturePolicy); + QFETCH(int, expectedTaps); + QFETCH(int, expectedCancels); + QQuickView window; #ifdef DISABLE_HOVER_IN_IRRELEVANT_TESTS QQuickWindowPrivate::get(&window)->deliveryAgentPrivate()->frameSynchronousHoverEnabled = false; @@ -340,19 +358,22 @@ void tst_qquickdeliveryagent::tapHandlerDoesntOverrideSubsceneGrabber() // QTBUG // add a TapHandler to it QQuickTapHandler tapHandler(&subscene); + tapHandler.setGesturePolicy(gesturePolicy); QSignalSpy clickSpy(&tapHandler, &QQuickTapHandler::tapped); + QSignalSpy cancelSpy(&tapHandler, &QQuickTapHandler::canceled); window.show(); QVERIFY(QTest::qWaitForWindowExposed(&window)); int cursorPos = textEdit->property("cursorPosition").toInt(); // Click on the middle of the subscene to the right (texture cloned from the left). - // TapHandler takes a passive grab on press; TextEdit takes the exclusive grab; - // and TapHandler does not emit tapped, because of the non-filtering exclusive grabber. + // TapHandler takes whichever type of grab on press; TextEdit takes the exclusive grab; + // TapHandler either gets tapped if it has passive grab, or gets its exclusive grab cancelled. QTest::mouseClick(&window, Qt::LeftButton, Qt::NoModifier, clickPos); qCDebug(lcTests) << "clicking subscene TextEdit set cursorPos to" << cursorPos; - QVERIFY(textEdit->property("cursorPosition").toInt() > cursorPos); - QCOMPARE(clickSpy.count(), 0); // doesn't tap + QVERIFY(textEdit->property("cursorPosition").toInt() > cursorPos); // TextEdit reacts regardless + QCOMPARE(clickSpy.count(), expectedTaps); + QCOMPARE(cancelSpy.count(), expectedCancels); } void tst_qquickdeliveryagent::undoDelegationWhenSubsceneFocusCleared() // QTBUG-105192 diff --git a/tests/auto/quickcontrols2/pointerhandlers/data/tapHandlerButton.qml b/tests/auto/quickcontrols2/pointerhandlers/data/tapHandlerButton.qml new file mode 100644 index 0000000000..74e0166d0e --- /dev/null +++ b/tests/auto/quickcontrols2/pointerhandlers/data/tapHandlerButton.qml @@ -0,0 +1,12 @@ +import QtQuick +import QtQuick.Controls + +Rectangle { + width: 150; height: 150 + color: th.pressed ? "lightsteelblue" : "beige" + Button { + text: pressed ? "pressed" : "" + width: 150 // workaround for QTBUG-104954 + TapHandler { id: th } + } +} diff --git a/tests/auto/quickcontrols2/pointerhandlers/tst_pointerhandlers.cpp b/tests/auto/quickcontrols2/pointerhandlers/tst_pointerhandlers.cpp index ba1fbd2dd1..a51d667ebc 100644 --- a/tests/auto/quickcontrols2/pointerhandlers/tst_pointerhandlers.cpp +++ b/tests/auto/quickcontrols2/pointerhandlers/tst_pointerhandlers.cpp @@ -6,16 +6,22 @@ #include <QtQuick/qquickview.h> #include <QtQuick/private/qquickmousearea_p.h> +#include <QtQuick/private/qquicktaphandler_p.h> #include <QtQml/qqmlengine.h> #include <QtQml/qqmlcontext.h> #include <QtQuickTemplates2/private/qquickbutton_p.h> +#include <QtGui/qguiapplication.h> +#include <QtGui/private/qpointingdevice_p.h> + #include <QtQuickTestUtils/private/qmlutils_p.h> #include <QtQuickTestUtils/private/viewtestutils_p.h> #include <QtQuickTestUtils/private/visualtestutils_p.h> +Q_LOGGING_CATEGORY(lcPointerTests, "qt.quick.pointer.tests") + using namespace QQuickViewTestUtils; using namespace QQuickVisualTestUtils; @@ -28,6 +34,11 @@ public: private slots: void hover_controlInsideControl(); void hover_controlAndMouseArea(); + void buttonTapHandler_data(); + void buttonTapHandler(); + +private: + QScopedPointer<QPointingDevice> touchscreen = QScopedPointer<QPointingDevice>(QTest::createTouchDevice()); }; tst_pointerhandlers::tst_pointerhandlers() @@ -154,6 +165,57 @@ void tst_pointerhandlers::hover_controlAndMouseArea() QCOMPARE(innerMouseArea->hovered(), false); } +void tst_pointerhandlers::buttonTapHandler_data() +{ + QTest::addColumn<QPointingDevice::DeviceType>("deviceType"); + QTest::addColumn<Qt::MouseButton>("mouseButton"); + + QTest::newRow("left mouse") << QPointingDevice::DeviceType::Mouse << Qt::LeftButton; + QTest::newRow("right mouse") << QPointingDevice::DeviceType::Mouse << Qt::RightButton; + QTest::newRow("touch") << QPointingDevice::DeviceType::TouchScreen << Qt::NoButton; +} + +void tst_pointerhandlers::buttonTapHandler() // QTBUG-105609 +{ + QFETCH(QPointingDevice::DeviceType, deviceType); + QFETCH(Qt::MouseButton, mouseButton); + + QQuickView window; + QVERIFY(QQuickTest::showView(window, testFileUrl("tapHandlerButton.qml"))); + + QPointer<QQuickTapHandler> handler = window.rootObject()->findChild<QQuickTapHandler*>(); + QVERIFY(handler); + handler->setAcceptedButtons(mouseButton); + QQuickItem *target = handler->target(); + QVERIFY(target); + QSignalSpy tappedSpy(handler, &QQuickTapHandler::tapped); + QSignalSpy clickedSpy(target, SIGNAL(clicked())); // avoid #include for this signal + + const QPoint pos(10, 10); + switch (static_cast<QPointingDevice::DeviceType>(deviceType)) { + case QPointingDevice::DeviceType::Mouse: + // click it + QTest::mouseClick(&window, mouseButton, Qt::NoModifier, pos); + QTRY_COMPARE(clickedSpy.count(), 1); // perhaps Button should not react to right-click, but it does + QCOMPARE(tappedSpy.count(), 1); + break; + + case QPointingDevice::DeviceType::TouchScreen: { + // tap it + QTest::QTouchEventSequence touch = QTest::touchEvent(&window, touchscreen.data()); + touch.press(0, pos, &window).commit(); + QTRY_COMPARE(target->property("pressed").toBool(), true); + touch.release(0, pos, &window).commit(); + QTRY_COMPARE(clickedSpy.count(), 1); + QCOMPARE(tappedSpy.count(), 1); + break; + } + default: + break; + } + QCOMPARE(handler->isPressed(), false); +} + QTEST_MAIN(tst_pointerhandlers) #include "tst_pointerhandlers.moc" |
