qtdeclarative/tests/auto/quick/pointerhandlers/qquicktaphandler/tst_qquicktaphandler.cpp

1088 lines
49 KiB
C++
Raw Normal View History

// Copyright (C) 2017 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include <QtTest/QtTest>
#include <QtGui/qstylehints.h>
#include <QtQuick/qquickview.h>
#include <QtQuick/qquickitem.h>
#include <QtQuick/private/qquickpointerhandler_p.h>
#include <QtQuick/private/qquicktaphandler_p.h>
#include <qpa/qwindowsysteminterface.h>
#include <private/qquickwindow_p.h>
#include <QtQml/qqmlengine.h>
#include <QtQml/qqmlproperty.h>
#include <QtQuickTestUtils/private/qmlutils_p.h>
#include <QtQuickTestUtils/private/viewtestutils_p.h>
Q_LOGGING_CATEGORY(lcPointerTests, "qt.quick.pointer.tests")
class tst_TapHandler : public QQmlDataTest
{
Q_OBJECT
public:
tst_TapHandler()
: QQmlDataTest(QT_QMLTEST_DATADIR)
{}
private slots:
void initTestCase() override;
void touchGesturePolicyDragThreshold();
void mouseGesturePolicyDragThreshold();
void touchMouseGesturePolicyDragThreshold();
void touchGesturePolicyWithinBounds();
void mouseGesturePolicyWithinBounds();
void touchGesturePolicyReleaseWithinBounds();
void mouseGesturePolicyReleaseWithinBounds();
Add TapHandler.gesturePolicy: DragWithinBounds enum value; examples On a touchscreen, right-clicking is not directly possible; so sometimes a long-press gesture is used as a substitute. The next thing a UI designer would want would then be a way of showing feedback that a long-press is in progress, rather than simply waiting for the long-press to occur and then surprising the user with some instant action. For example, a menu might begin to open as the user holds down the touchpoint; but before the long-press gesture is complete, the user can simply release, to cancel the gesture and close the menu. The timeHeld property could drive the animation, to avoid needing a separate animation type; in fact the reason timeHeld exists is to make it easy to emulate this sort of touch-press animation, like one that occurs on touchscreens since Windows 7. But after the menu is open, the user would probably expect to be able to drag the finger to a menu item and release, to select the menu item. For such a purpose, the existing gesture policies weren't very useful: each of them resets the timeHeld property if the user drags beyond the drag threshold; so if the user expects to drag and release over a menu item, then the timeHeld property cannot drive the menu-opening animation, because the menu would disappear as soon as the user drags a little. So it makes more sense to have a gesturePolicy that acts like WithinBounds, but also applies the same policy to the timeHeld property and the longPressed signal. We don't care about the drag threshold: if the user is holding down a finger, it's considered to be a long-press-in-progress, regardless of how far it has moved since press (as long as it stays within the parent's bounds). An example of such a menu is added. The menu must have TapHandler as its root object, because it reacts to press-and-drag within some larger item, larger than the menu itself. For example such a menu could be used in a canvas-like application (drawing, diagramming, dragging things like photos or file icons, or something like that): dragging items on the canvas is possible, but long-pressing anywhere will open a context menu. But in this example so far, only the menu is implemented. It's a pie menu, because those are particularly touch-friendly; but perhaps for the mouse, a conventional context menu would be used. [ChangeLog][QtQuick][Event Handlers] TapHandler now has one more gesturePolicy value: DragWithinBounds; it is similar to WithinBounds, except that timeHeld is not reset during dragging, and the longPressed signal can be emitted regardless of the drag threshold. This is useful for implementing press-drag-release components such as menus, while using timeHeld to directly drive an "opening" animation. Change-Id: I298f8b1ad8f8d7d3c241ef4fdd68e7ec8d8b5bdd Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
2021-10-07 20:27:32 +00:00
void gesturePolicyDragWithinBounds_data();
void gesturePolicyDragWithinBounds();
void touchMultiTap();
void mouseMultiTap_data();
void mouseMultiTap();
void mouseMultiTapLeftRight_data();
void mouseMultiTapLeftRight();
void singleTapDoubleTap_data();
void singleTapDoubleTap();
void touchLongPress();
void mouseLongPress();
void buttonsMultiTouch();
void componentUserBehavioralOverride();
void rightLongPressIgnoreWheel();
void negativeZStackingOrder();
void nonTopLevelParentWindow();
QQuickItem: ignore double-clicks by default; remove allowDoubleClick Because Qt has a pattern that events arrive pre-accepted, most event-handling functions in QQuickItem call ignore(), and it's up to subclasses to override those functions to allow the event to remain accepted, if they choose to handle it. So it was odd that QQuickItem::mouseDoubleClickEvent() did not call ignore(). Pointer handlers don't handle MouseButtonDblClick events, so QQuickDeliveryAgent does not send those events to handlers. Since 0e3adb65b0e9c44fa6e202630ff57c907ecf0820 though, we disallowed delivery of double-click events to Items after any handler has already accepted the single point in a mouse event. This caused some inconsistencies; in fact the allowDoubleClick variable was getting thrashed a lot, making it hard to reason about the logic. Items that contained handlers behaved differently than items that did not. One scenario being fixed here was absurd: a parent Rectangle (which never handles pointer events on its own) got an implicit grab in deliverMatchingPointsToItem() and thus stole the grab from a handler (!), during delivery of a MouseButtonDblClick (!!), just because the event happened to remain accepted, even though no item or handler reacted to it directly. The Rectangle needs to ignore() the event to avoid that, just as all Items now do by default. Then it turns out that we don't need a stateful allowDoubleClick anymore: the logic is more consistent without it. Items can handle double-clicks, but they don't by default, as with any other pointer event. Pointer handlers don't handle MouseButtonDblClick because they detect double-clicks in their own way, and that's enforced by simply not sending those events to handlers. Passive grabs should be retained regardless of the interloper MouseButtonDblClick event: items that handle it cannot cancel a handler's passive grab. They can steal a handler's exclusive grab, but that should be prevented in other ways, such as ignoring the event so that there is no accidental implicit grab. Reverts 0e3adb65b0e9c44fa6e202630ff57c907ecf0820. DeliveryAgent no longer calls clearPassiveGrabbers() directly as QQuickWindow did then; and it also no longer delivers MouseButtonDblClick the same as a press event. QSinglePointEvent::isBeginEvent() returns false in that case, so deliverPressOrReleaseEvent() is not called. A couple of existing tests now need to avoid generating double-clicks, but they were not trying to test that anyway. New tests are added (test coverage of double-clicks has been unfortunately sparse so far). Pick-to: 6.3 Fixes: QTBUG-102625 Change-Id: If74baff68ffc46b8b403d37f4e10ddf6b159d40c Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Doris Verria <doris.verria@qt.io> Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
2022-05-30 21:04:22 +00:00
void nestedDoubleTap_data();
void nestedDoubleTap();
private:
void createView(QScopedPointer<QQuickView> &window, const char *fileName,
QWindow *parent = nullptr);
QPointingDevice *touchDevice = QTest::createTouchDevice(); // TODO const after fixing QTBUG-107864
void mouseEvent(QEvent::Type type, Qt::MouseButton button, const QPoint &point,
QWindow *targetWindow, QWindow *mapToWindow);
};
void tst_TapHandler::createView(QScopedPointer<QQuickView> &window, const char *fileName,
QWindow *parent)
{
window.reset(new QQuickView(parent));
if (parent) {
parent->show();
QVERIFY(QTest::qWaitForWindowActive(parent));
}
window->setSource(testFileUrl(fileName));
QTRY_COMPARE(window->status(), QQuickView::Ready);
QQuickViewTestUtils::centerOnScreen(window.data());
QQuickViewTestUtils::moveMouseAway(window.data());
window->show();
QVERIFY(QTest::qWaitForWindowActive(window.data()));
QVERIFY(window->rootObject() != nullptr);
}
void tst_TapHandler::mouseEvent(QEvent::Type type, Qt::MouseButton button, const QPoint &point,
QWindow *targetWindow, QWindow *mapToWindow)
{
QVERIFY(targetWindow);
QVERIFY(mapToWindow);
auto buttons = button;
if (type == QEvent::MouseButtonRelease) {
buttons = Qt::NoButton;
}
QMouseEvent me(type, point, mapToWindow->mapToGlobal(point), button, buttons,
Qt::KeyboardModifiers(), QPointingDevice::primaryPointingDevice());
QVERIFY(qApp->notify(targetWindow, &me));
}
void tst_TapHandler::initTestCase()
{
// This test assumes that we don't get synthesized mouse events from QGuiApplication
qApp->setAttribute(Qt::AA_SynthesizeMouseForUnhandledTouchEvents, false);
QQmlDataTest::initTestCase();
}
void tst_TapHandler::touchGesturePolicyDragThreshold()
{
const int dragThreshold = QGuiApplication::styleHints()->startDragDistance();
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *buttonDragThreshold = window->rootObject()->findChild<QQuickItem*>("DragThreshold");
QVERIFY(buttonDragThreshold);
QQuickTapHandler *tapHandler = buttonDragThreshold->findChild<QQuickTapHandler*>();
QVERIFY(tapHandler);
QSignalSpy dragThresholdTappedSpy(buttonDragThreshold, SIGNAL(tapped()));
// DragThreshold button stays pressed while touchpoint stays within dragThreshold, emits tapped on release
QPoint p1 = buttonDragThreshold->mapToScene(QPointF(20, 20)).toPoint();
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonDragThreshold->property("pressed").toBool());
p1 += QPoint(dragThreshold, 0);
QTest::touchEvent(window, touchDevice).move(1, p1, window);
QQuickTouchUtils::flush(window);
QVERIFY(buttonDragThreshold->property("pressed").toBool());
QTest::touchEvent(window, touchDevice).release(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!buttonDragThreshold->property("pressed").toBool());
QCOMPARE(dragThresholdTappedSpy.size(), 1);
QCOMPARE(buttonDragThreshold->property("tappedPosition").toPoint(), p1);
QCOMPARE(tapHandler->point().position(), QPointF());
// DragThreshold button is no longer pressed if touchpoint goes beyond dragThreshold
dragThresholdTappedSpy.clear();
p1 = buttonDragThreshold->mapToScene(QPointF(20, 20)).toPoint();
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonDragThreshold->property("pressed").toBool());
p1 += QPoint(dragThreshold, 0);
QTest::touchEvent(window, touchDevice).move(1, p1, window);
QQuickTouchUtils::flush(window);
QVERIFY(buttonDragThreshold->property("pressed").toBool());
p1 += QPoint(1, 0);
QTest::touchEvent(window, touchDevice).move(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!buttonDragThreshold->property("pressed").toBool());
QTest::touchEvent(window, touchDevice).release(1, p1, window);
QQuickTouchUtils::flush(window);
QVERIFY(!buttonDragThreshold->property("pressed").toBool());
QCOMPARE(dragThresholdTappedSpy.size(), 0);
}
void tst_TapHandler::mouseGesturePolicyDragThreshold()
{
const int dragThreshold = QGuiApplication::styleHints()->startDragDistance();
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *buttonDragThreshold = window->rootObject()->findChild<QQuickItem*>("DragThreshold");
QVERIFY(buttonDragThreshold);
QQuickTapHandler *tapHandler = buttonDragThreshold->findChild<QQuickTapHandler*>();
QVERIFY(tapHandler);
QSignalSpy dragThresholdTappedSpy(buttonDragThreshold, SIGNAL(tapped()));
// DragThreshold button stays pressed while mouse stays within dragThreshold, emits tapped on release
QPoint p1 = buttonDragThreshold->mapToScene(QPointF(20, 20)).toPoint();
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(buttonDragThreshold->property("pressed").toBool());
p1 += QPoint(dragThreshold, 0);
QTest::mouseMove(window, p1);
QVERIFY(buttonDragThreshold->property("pressed").toBool());
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(!buttonDragThreshold->property("pressed").toBool());
QTRY_COMPARE(dragThresholdTappedSpy.size(), 1);
QCOMPARE(buttonDragThreshold->property("tappedPosition").toPoint(), p1);
QCOMPARE(tapHandler->point().position(), QPointF());
// DragThreshold button is no longer pressed if mouse goes beyond dragThreshold
dragThresholdTappedSpy.clear();
p1 = buttonDragThreshold->mapToScene(QPointF(20, 20)).toPoint();
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(buttonDragThreshold->property("pressed").toBool());
p1 += QPoint(dragThreshold, 0);
QTest::mouseMove(window, p1);
QVERIFY(buttonDragThreshold->property("pressed").toBool());
p1 += QPoint(1, 0);
QTest::mouseMove(window, p1);
QTRY_VERIFY(!buttonDragThreshold->property("pressed").toBool());
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1);
QVERIFY(!buttonDragThreshold->property("pressed").toBool());
QCOMPARE(dragThresholdTappedSpy.size(), 0);
}
void tst_TapHandler::touchMouseGesturePolicyDragThreshold()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *buttonDragThreshold = window->rootObject()->findChild<QQuickItem*>("DragThreshold");
QVERIFY(buttonDragThreshold);
QSignalSpy tappedSpy(buttonDragThreshold, SIGNAL(tapped()));
QSignalSpy canceledSpy(buttonDragThreshold, SIGNAL(canceled()));
// Press mouse, drag it outside the button, release
QPoint p1 = buttonDragThreshold->mapToScene(QPointF(20, 20)).toPoint();
QPoint p2 = p1 + QPoint(int(buttonDragThreshold->height()), 0);
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(buttonDragThreshold->property("pressed").toBool());
QTest::mouseMove(window, p2);
QTRY_COMPARE(canceledSpy.size(), 1);
QCOMPARE(tappedSpy.size(), 0);
QCOMPARE(buttonDragThreshold->property("pressed").toBool(), false);
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p2);
// Press and release touch, verify that it still works (QTBUG-71466)
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonDragThreshold->property("pressed").toBool());
QTest::touchEvent(window, touchDevice).release(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!buttonDragThreshold->property("pressed").toBool());
QCOMPARE(tappedSpy.size(), 1);
// Press touch, drag it outside the button, release
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonDragThreshold->property("pressed").toBool());
QTest::touchEvent(window, touchDevice).move(1, p2, window);
QQuickTouchUtils::flush(window);
QTRY_COMPARE(buttonDragThreshold->property("pressed").toBool(), false);
QTest::touchEvent(window, touchDevice).release(1, p2, window);
QQuickTouchUtils::flush(window);
QTRY_COMPARE(canceledSpy.size(), 2);
QCOMPARE(tappedSpy.size(), 1); // didn't increase
QCOMPARE(buttonDragThreshold->property("pressed").toBool(), false);
// Press and release mouse, verify that it still works
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(buttonDragThreshold->property("pressed").toBool());
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_COMPARE(tappedSpy.size(), 2);
QCOMPARE(canceledSpy.size(), 2); // didn't increase
QCOMPARE(buttonDragThreshold->property("pressed").toBool(), false);
}
void tst_TapHandler::touchGesturePolicyWithinBounds()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *buttonWithinBounds = window->rootObject()->findChild<QQuickItem*>("WithinBounds");
QVERIFY(buttonWithinBounds);
QSignalSpy withinBoundsTappedSpy(buttonWithinBounds, SIGNAL(tapped()));
// WithinBounds button stays pressed while touchpoint stays within bounds, emits tapped on release
QPoint p1 = buttonWithinBounds->mapToScene(QPointF(20, 20)).toPoint();
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonWithinBounds->property("pressed").toBool());
p1 += QPoint(50, 0);
QTest::touchEvent(window, touchDevice).move(1, p1, window);
QQuickTouchUtils::flush(window);
QVERIFY(buttonWithinBounds->property("pressed").toBool());
QTest::touchEvent(window, touchDevice).release(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!buttonWithinBounds->property("pressed").toBool());
QCOMPARE(withinBoundsTappedSpy.size(), 1);
// WithinBounds button is no longer pressed if touchpoint leaves bounds
withinBoundsTappedSpy.clear();
p1 = buttonWithinBounds->mapToScene(QPointF(20, 20)).toPoint();
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonWithinBounds->property("pressed").toBool());
p1 += QPoint(0, 100);
QTest::touchEvent(window, touchDevice).move(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!buttonWithinBounds->property("pressed").toBool());
QTest::touchEvent(window, touchDevice).release(1, p1, window);
QQuickTouchUtils::flush(window);
QVERIFY(!buttonWithinBounds->property("pressed").toBool());
QCOMPARE(withinBoundsTappedSpy.size(), 0);
}
void tst_TapHandler::mouseGesturePolicyWithinBounds()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *buttonWithinBounds = window->rootObject()->findChild<QQuickItem*>("WithinBounds");
QVERIFY(buttonWithinBounds);
QSignalSpy withinBoundsTappedSpy(buttonWithinBounds, SIGNAL(tapped()));
// WithinBounds button stays pressed while touchpoint stays within bounds, emits tapped on release
QPoint p1 = buttonWithinBounds->mapToScene(QPointF(20, 20)).toPoint();
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(buttonWithinBounds->property("pressed").toBool());
p1 += QPoint(50, 0);
QTest::mouseMove(window, p1);
QVERIFY(buttonWithinBounds->property("pressed").toBool());
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(!buttonWithinBounds->property("pressed").toBool());
QCOMPARE(withinBoundsTappedSpy.size(), 1);
// WithinBounds button is no longer pressed if touchpoint leaves bounds
withinBoundsTappedSpy.clear();
p1 = buttonWithinBounds->mapToScene(QPointF(20, 20)).toPoint();
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(buttonWithinBounds->property("pressed").toBool());
p1 += QPoint(0, 100);
QTest::mouseMove(window, p1);
QTRY_VERIFY(!buttonWithinBounds->property("pressed").toBool());
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1);
QVERIFY(!buttonWithinBounds->property("pressed").toBool());
QCOMPARE(withinBoundsTappedSpy.size(), 0);
}
void tst_TapHandler::touchGesturePolicyReleaseWithinBounds()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *buttonReleaseWithinBounds = window->rootObject()->findChild<QQuickItem*>("ReleaseWithinBounds");
QVERIFY(buttonReleaseWithinBounds);
QSignalSpy releaseWithinBoundsTappedSpy(buttonReleaseWithinBounds, SIGNAL(tapped()));
// ReleaseWithinBounds button stays pressed while touchpoint wanders anywhere,
// then if it comes back within bounds, emits tapped on release
QPoint p1 = buttonReleaseWithinBounds->mapToScene(QPointF(20, 20)).toPoint();
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
p1 += QPoint(50, 0);
QTest::touchEvent(window, touchDevice).move(1, p1, window);
QQuickTouchUtils::flush(window);
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
p1 += QPoint(250, 100);
QTest::touchEvent(window, touchDevice).move(1, p1, window);
QQuickTouchUtils::flush(window);
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
p1 = buttonReleaseWithinBounds->mapToScene(QPointF(25, 15)).toPoint();
QTest::touchEvent(window, touchDevice).move(1, p1, window);
QQuickTouchUtils::flush(window);
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
QTest::touchEvent(window, touchDevice).release(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!buttonReleaseWithinBounds->property("pressed").toBool());
QCOMPARE(releaseWithinBoundsTappedSpy.size(), 1);
// ReleaseWithinBounds button does not emit tapped if released out of bounds
releaseWithinBoundsTappedSpy.clear();
p1 = buttonReleaseWithinBounds->mapToScene(QPointF(20, 20)).toPoint();
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
p1 += QPoint(0, 100);
QTest::touchEvent(window, touchDevice).move(1, p1, window);
QQuickTouchUtils::flush(window);
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
QTest::touchEvent(window, touchDevice).release(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!buttonReleaseWithinBounds->property("pressed").toBool());
QCOMPARE(releaseWithinBoundsTappedSpy.size(), 0);
}
void tst_TapHandler::mouseGesturePolicyReleaseWithinBounds()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *buttonReleaseWithinBounds = window->rootObject()->findChild<QQuickItem*>("ReleaseWithinBounds");
QVERIFY(buttonReleaseWithinBounds);
QSignalSpy releaseWithinBoundsTappedSpy(buttonReleaseWithinBounds, SIGNAL(tapped()));
// ReleaseWithinBounds button stays pressed while touchpoint wanders anywhere,
// then if it comes back within bounds, emits tapped on release
QPoint p1 = buttonReleaseWithinBounds->mapToScene(QPointF(20, 20)).toPoint();
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
p1 += QPoint(50, 0);
QTest::mouseMove(window, p1);
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
p1 += QPoint(250, 100);
QTest::mouseMove(window, p1);
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
p1 = buttonReleaseWithinBounds->mapToScene(QPointF(25, 15)).toPoint();
QTest::mouseMove(window, p1);
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(!buttonReleaseWithinBounds->property("pressed").toBool());
QCOMPARE(releaseWithinBoundsTappedSpy.size(), 1);
// ReleaseWithinBounds button does not emit tapped if released out of bounds
releaseWithinBoundsTappedSpy.clear();
p1 = buttonReleaseWithinBounds->mapToScene(QPointF(20, 20)).toPoint();
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
p1 += QPoint(0, 100);
QTest::mouseMove(window, p1);
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(!buttonReleaseWithinBounds->property("pressed").toBool());
QCOMPARE(releaseWithinBoundsTappedSpy.size(), 0);
}
Add TapHandler.gesturePolicy: DragWithinBounds enum value; examples On a touchscreen, right-clicking is not directly possible; so sometimes a long-press gesture is used as a substitute. The next thing a UI designer would want would then be a way of showing feedback that a long-press is in progress, rather than simply waiting for the long-press to occur and then surprising the user with some instant action. For example, a menu might begin to open as the user holds down the touchpoint; but before the long-press gesture is complete, the user can simply release, to cancel the gesture and close the menu. The timeHeld property could drive the animation, to avoid needing a separate animation type; in fact the reason timeHeld exists is to make it easy to emulate this sort of touch-press animation, like one that occurs on touchscreens since Windows 7. But after the menu is open, the user would probably expect to be able to drag the finger to a menu item and release, to select the menu item. For such a purpose, the existing gesture policies weren't very useful: each of them resets the timeHeld property if the user drags beyond the drag threshold; so if the user expects to drag and release over a menu item, then the timeHeld property cannot drive the menu-opening animation, because the menu would disappear as soon as the user drags a little. So it makes more sense to have a gesturePolicy that acts like WithinBounds, but also applies the same policy to the timeHeld property and the longPressed signal. We don't care about the drag threshold: if the user is holding down a finger, it's considered to be a long-press-in-progress, regardless of how far it has moved since press (as long as it stays within the parent's bounds). An example of such a menu is added. The menu must have TapHandler as its root object, because it reacts to press-and-drag within some larger item, larger than the menu itself. For example such a menu could be used in a canvas-like application (drawing, diagramming, dragging things like photos or file icons, or something like that): dragging items on the canvas is possible, but long-pressing anywhere will open a context menu. But in this example so far, only the menu is implemented. It's a pie menu, because those are particularly touch-friendly; but perhaps for the mouse, a conventional context menu would be used. [ChangeLog][QtQuick][Event Handlers] TapHandler now has one more gesturePolicy value: DragWithinBounds; it is similar to WithinBounds, except that timeHeld is not reset during dragging, and the longPressed signal can be emitted regardless of the drag threshold. This is useful for implementing press-drag-release components such as menus, while using timeHeld to directly drive an "opening" animation. Change-Id: I298f8b1ad8f8d7d3c241ef4fdd68e7ec8d8b5bdd Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
2021-10-07 20:27:32 +00:00
void tst_TapHandler::gesturePolicyDragWithinBounds_data()
{
QTest::addColumn<const QPointingDevice *>("device");
Add TapHandler.gesturePolicy: DragWithinBounds enum value; examples On a touchscreen, right-clicking is not directly possible; so sometimes a long-press gesture is used as a substitute. The next thing a UI designer would want would then be a way of showing feedback that a long-press is in progress, rather than simply waiting for the long-press to occur and then surprising the user with some instant action. For example, a menu might begin to open as the user holds down the touchpoint; but before the long-press gesture is complete, the user can simply release, to cancel the gesture and close the menu. The timeHeld property could drive the animation, to avoid needing a separate animation type; in fact the reason timeHeld exists is to make it easy to emulate this sort of touch-press animation, like one that occurs on touchscreens since Windows 7. But after the menu is open, the user would probably expect to be able to drag the finger to a menu item and release, to select the menu item. For such a purpose, the existing gesture policies weren't very useful: each of them resets the timeHeld property if the user drags beyond the drag threshold; so if the user expects to drag and release over a menu item, then the timeHeld property cannot drive the menu-opening animation, because the menu would disappear as soon as the user drags a little. So it makes more sense to have a gesturePolicy that acts like WithinBounds, but also applies the same policy to the timeHeld property and the longPressed signal. We don't care about the drag threshold: if the user is holding down a finger, it's considered to be a long-press-in-progress, regardless of how far it has moved since press (as long as it stays within the parent's bounds). An example of such a menu is added. The menu must have TapHandler as its root object, because it reacts to press-and-drag within some larger item, larger than the menu itself. For example such a menu could be used in a canvas-like application (drawing, diagramming, dragging things like photos or file icons, or something like that): dragging items on the canvas is possible, but long-pressing anywhere will open a context menu. But in this example so far, only the menu is implemented. It's a pie menu, because those are particularly touch-friendly; but perhaps for the mouse, a conventional context menu would be used. [ChangeLog][QtQuick][Event Handlers] TapHandler now has one more gesturePolicy value: DragWithinBounds; it is similar to WithinBounds, except that timeHeld is not reset during dragging, and the longPressed signal can be emitted regardless of the drag threshold. This is useful for implementing press-drag-release components such as menus, while using timeHeld to directly drive an "opening" animation. Change-Id: I298f8b1ad8f8d7d3c241ef4fdd68e7ec8d8b5bdd Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
2021-10-07 20:27:32 +00:00
QTest::addColumn<QPoint>("dragStart");
QTest::addColumn<QPoint>("dragDistance");
QTest::addColumn<QString>("expectedFeedback");
const QPointingDevice *constTouchDevice = touchDevice;
QTest::newRow("mouse: click") << QPointingDevice::primaryPointingDevice() << QPoint(200, 200) << QPoint(0, 0) << "middle";
QTest::newRow("touch: tap") << constTouchDevice << QPoint(200, 200) << QPoint(0, 0) << "middle";
QTest::newRow("mouse: drag up") << QPointingDevice::primaryPointingDevice() << QPoint(200, 200) << QPoint(0, -20) << "top";
QTest::newRow("touch: drag up") << constTouchDevice << QPoint(200, 200) << QPoint(0, -20) << "top";
QTest::newRow("mouse: drag out to cancel") << QPointingDevice::primaryPointingDevice() << QPoint(435, 200) << QPoint(10, 0) << "canceled";
QTest::newRow("touch: drag out to cancel") << constTouchDevice << QPoint(435, 200) << QPoint(10, 0) << "canceled";
Add TapHandler.gesturePolicy: DragWithinBounds enum value; examples On a touchscreen, right-clicking is not directly possible; so sometimes a long-press gesture is used as a substitute. The next thing a UI designer would want would then be a way of showing feedback that a long-press is in progress, rather than simply waiting for the long-press to occur and then surprising the user with some instant action. For example, a menu might begin to open as the user holds down the touchpoint; but before the long-press gesture is complete, the user can simply release, to cancel the gesture and close the menu. The timeHeld property could drive the animation, to avoid needing a separate animation type; in fact the reason timeHeld exists is to make it easy to emulate this sort of touch-press animation, like one that occurs on touchscreens since Windows 7. But after the menu is open, the user would probably expect to be able to drag the finger to a menu item and release, to select the menu item. For such a purpose, the existing gesture policies weren't very useful: each of them resets the timeHeld property if the user drags beyond the drag threshold; so if the user expects to drag and release over a menu item, then the timeHeld property cannot drive the menu-opening animation, because the menu would disappear as soon as the user drags a little. So it makes more sense to have a gesturePolicy that acts like WithinBounds, but also applies the same policy to the timeHeld property and the longPressed signal. We don't care about the drag threshold: if the user is holding down a finger, it's considered to be a long-press-in-progress, regardless of how far it has moved since press (as long as it stays within the parent's bounds). An example of such a menu is added. The menu must have TapHandler as its root object, because it reacts to press-and-drag within some larger item, larger than the menu itself. For example such a menu could be used in a canvas-like application (drawing, diagramming, dragging things like photos or file icons, or something like that): dragging items on the canvas is possible, but long-pressing anywhere will open a context menu. But in this example so far, only the menu is implemented. It's a pie menu, because those are particularly touch-friendly; but perhaps for the mouse, a conventional context menu would be used. [ChangeLog][QtQuick][Event Handlers] TapHandler now has one more gesturePolicy value: DragWithinBounds; it is similar to WithinBounds, except that timeHeld is not reset during dragging, and the longPressed signal can be emitted regardless of the drag threshold. This is useful for implementing press-drag-release components such as menus, while using timeHeld to directly drive an "opening" animation. Change-Id: I298f8b1ad8f8d7d3c241ef4fdd68e7ec8d8b5bdd Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
2021-10-07 20:27:32 +00:00
}
void tst_TapHandler::gesturePolicyDragWithinBounds()
{
QFETCH(const QPointingDevice *, device);
Add TapHandler.gesturePolicy: DragWithinBounds enum value; examples On a touchscreen, right-clicking is not directly possible; so sometimes a long-press gesture is used as a substitute. The next thing a UI designer would want would then be a way of showing feedback that a long-press is in progress, rather than simply waiting for the long-press to occur and then surprising the user with some instant action. For example, a menu might begin to open as the user holds down the touchpoint; but before the long-press gesture is complete, the user can simply release, to cancel the gesture and close the menu. The timeHeld property could drive the animation, to avoid needing a separate animation type; in fact the reason timeHeld exists is to make it easy to emulate this sort of touch-press animation, like one that occurs on touchscreens since Windows 7. But after the menu is open, the user would probably expect to be able to drag the finger to a menu item and release, to select the menu item. For such a purpose, the existing gesture policies weren't very useful: each of them resets the timeHeld property if the user drags beyond the drag threshold; so if the user expects to drag and release over a menu item, then the timeHeld property cannot drive the menu-opening animation, because the menu would disappear as soon as the user drags a little. So it makes more sense to have a gesturePolicy that acts like WithinBounds, but also applies the same policy to the timeHeld property and the longPressed signal. We don't care about the drag threshold: if the user is holding down a finger, it's considered to be a long-press-in-progress, regardless of how far it has moved since press (as long as it stays within the parent's bounds). An example of such a menu is added. The menu must have TapHandler as its root object, because it reacts to press-and-drag within some larger item, larger than the menu itself. For example such a menu could be used in a canvas-like application (drawing, diagramming, dragging things like photos or file icons, or something like that): dragging items on the canvas is possible, but long-pressing anywhere will open a context menu. But in this example so far, only the menu is implemented. It's a pie menu, because those are particularly touch-friendly; but perhaps for the mouse, a conventional context menu would be used. [ChangeLog][QtQuick][Event Handlers] TapHandler now has one more gesturePolicy value: DragWithinBounds; it is similar to WithinBounds, except that timeHeld is not reset during dragging, and the longPressed signal can be emitted regardless of the drag threshold. This is useful for implementing press-drag-release components such as menus, while using timeHeld to directly drive an "opening" animation. Change-Id: I298f8b1ad8f8d7d3c241ef4fdd68e7ec8d8b5bdd Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
2021-10-07 20:27:32 +00:00
QFETCH(QPoint, dragStart);
QFETCH(QPoint, dragDistance);
QFETCH(QString, expectedFeedback);
const bool expectedCanceled = expectedFeedback == "canceled";
QQuickView window;
QVERIFY(QQuickTest::showView(window, testFileUrl("dragReleaseMenu.qml")));
QQuickTapHandler *tapHandler = window.rootObject()->findChild<QQuickTapHandler*>();
QVERIFY(tapHandler);
QSignalSpy canceledSpy(tapHandler, &QQuickTapHandler::canceled);
QQuickTest::pointerPress(device, &window, 0, dragStart);
QTRY_VERIFY(tapHandler->isPressed());
QQuickTest::pointerMove(device, &window, 0, dragStart + dragDistance);
if (expectedCanceled)
QTRY_COMPARE(tapHandler->timeHeld(), -1);
else
QTRY_VERIFY(tapHandler->timeHeld() > 0.1);
QQuickTest::pointerRelease(device, &window, 0, dragStart + dragDistance);
Add TapHandler.gesturePolicy: DragWithinBounds enum value; examples On a touchscreen, right-clicking is not directly possible; so sometimes a long-press gesture is used as a substitute. The next thing a UI designer would want would then be a way of showing feedback that a long-press is in progress, rather than simply waiting for the long-press to occur and then surprising the user with some instant action. For example, a menu might begin to open as the user holds down the touchpoint; but before the long-press gesture is complete, the user can simply release, to cancel the gesture and close the menu. The timeHeld property could drive the animation, to avoid needing a separate animation type; in fact the reason timeHeld exists is to make it easy to emulate this sort of touch-press animation, like one that occurs on touchscreens since Windows 7. But after the menu is open, the user would probably expect to be able to drag the finger to a menu item and release, to select the menu item. For such a purpose, the existing gesture policies weren't very useful: each of them resets the timeHeld property if the user drags beyond the drag threshold; so if the user expects to drag and release over a menu item, then the timeHeld property cannot drive the menu-opening animation, because the menu would disappear as soon as the user drags a little. So it makes more sense to have a gesturePolicy that acts like WithinBounds, but also applies the same policy to the timeHeld property and the longPressed signal. We don't care about the drag threshold: if the user is holding down a finger, it's considered to be a long-press-in-progress, regardless of how far it has moved since press (as long as it stays within the parent's bounds). An example of such a menu is added. The menu must have TapHandler as its root object, because it reacts to press-and-drag within some larger item, larger than the menu itself. For example such a menu could be used in a canvas-like application (drawing, diagramming, dragging things like photos or file icons, or something like that): dragging items on the canvas is possible, but long-pressing anywhere will open a context menu. But in this example so far, only the menu is implemented. It's a pie menu, because those are particularly touch-friendly; but perhaps for the mouse, a conventional context menu would be used. [ChangeLog][QtQuick][Event Handlers] TapHandler now has one more gesturePolicy value: DragWithinBounds; it is similar to WithinBounds, except that timeHeld is not reset during dragging, and the longPressed signal can be emitted regardless of the drag threshold. This is useful for implementing press-drag-release components such as menus, while using timeHeld to directly drive an "opening" animation. Change-Id: I298f8b1ad8f8d7d3c241ef4fdd68e7ec8d8b5bdd Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
2021-10-07 20:27:32 +00:00
QCOMPARE(window.rootObject()->property("feedbackText"), expectedFeedback);
if (expectedCanceled)
QCOMPARE(canceledSpy.size(), 1);
Add TapHandler.gesturePolicy: DragWithinBounds enum value; examples On a touchscreen, right-clicking is not directly possible; so sometimes a long-press gesture is used as a substitute. The next thing a UI designer would want would then be a way of showing feedback that a long-press is in progress, rather than simply waiting for the long-press to occur and then surprising the user with some instant action. For example, a menu might begin to open as the user holds down the touchpoint; but before the long-press gesture is complete, the user can simply release, to cancel the gesture and close the menu. The timeHeld property could drive the animation, to avoid needing a separate animation type; in fact the reason timeHeld exists is to make it easy to emulate this sort of touch-press animation, like one that occurs on touchscreens since Windows 7. But after the menu is open, the user would probably expect to be able to drag the finger to a menu item and release, to select the menu item. For such a purpose, the existing gesture policies weren't very useful: each of them resets the timeHeld property if the user drags beyond the drag threshold; so if the user expects to drag and release over a menu item, then the timeHeld property cannot drive the menu-opening animation, because the menu would disappear as soon as the user drags a little. So it makes more sense to have a gesturePolicy that acts like WithinBounds, but also applies the same policy to the timeHeld property and the longPressed signal. We don't care about the drag threshold: if the user is holding down a finger, it's considered to be a long-press-in-progress, regardless of how far it has moved since press (as long as it stays within the parent's bounds). An example of such a menu is added. The menu must have TapHandler as its root object, because it reacts to press-and-drag within some larger item, larger than the menu itself. For example such a menu could be used in a canvas-like application (drawing, diagramming, dragging things like photos or file icons, or something like that): dragging items on the canvas is possible, but long-pressing anywhere will open a context menu. But in this example so far, only the menu is implemented. It's a pie menu, because those are particularly touch-friendly; but perhaps for the mouse, a conventional context menu would be used. [ChangeLog][QtQuick][Event Handlers] TapHandler now has one more gesturePolicy value: DragWithinBounds; it is similar to WithinBounds, except that timeHeld is not reset during dragging, and the longPressed signal can be emitted regardless of the drag threshold. This is useful for implementing press-drag-release components such as menus, while using timeHeld to directly drive an "opening" animation. Change-Id: I298f8b1ad8f8d7d3c241ef4fdd68e7ec8d8b5bdd Reviewed-by: Mitch Curtis <mitch.curtis@qt.io>
2021-10-07 20:27:32 +00:00
}
void tst_TapHandler::touchMultiTap()
{
const int dragThreshold = QGuiApplication::styleHints()->startDragDistance();
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *button = window->rootObject()->findChild<QQuickItem*>("DragThreshold");
QVERIFY(button);
QSignalSpy tappedSpy(button, SIGNAL(tapped()));
// Tap once
QPoint p1 = button->mapToScene(QPointF(2, 2)).toPoint();
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(button->property("pressed").toBool());
QTest::touchEvent(window, touchDevice).release(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!button->property("pressed").toBool());
QCOMPARE(tappedSpy.size(), 1);
// Tap again in exactly the same place (not likely with touch in the real world)
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(button->property("pressed").toBool());
QTest::touchEvent(window, touchDevice).release(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!button->property("pressed").toBool());
QCOMPARE(tappedSpy.size(), 2);
// Tap a third time, nearby
p1 += QPoint(dragThreshold, dragThreshold);
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(button->property("pressed").toBool());
QTest::touchEvent(window, touchDevice).release(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!button->property("pressed").toBool());
QCOMPARE(tappedSpy.size(), 3);
// Tap a fourth time, drifting farther away
p1 += QPoint(dragThreshold, dragThreshold);
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(button->property("pressed").toBool());
QTest::touchEvent(window, touchDevice).release(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!button->property("pressed").toBool());
QCOMPARE(tappedSpy.size(), 4);
}
void tst_TapHandler::mouseMultiTap_data()
{
QTest::addColumn<QQuickTapHandler::ExclusiveSignals>("exclusiveSignals");
QTest::addColumn<int>("expectedSingleTaps");
QTest::addColumn<int>("expectedSingleTapsAfterMovingAway");
QTest::addColumn<int>("expectedSingleTapsAfterWaiting");
QTest::addColumn<int>("expectedDoubleTaps");
QTest::newRow("NotExclusive") << QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::NotExclusive)
<< 1 << 2 << 3 << 1;
QTest::newRow("SingleTap") << QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::SingleTap)
<< 1 << 2 << 3 << 0;
QTest::newRow("DoubleTap") << QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::DoubleTap)
<< 0 << 0 << 0 << 1;
QTest::newRow("SingleTap|DoubleTap") << QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::SingleTap | QQuickTapHandler::DoubleTap)
<< 0 << 0 << 0 << 0;
}
void tst_TapHandler::mouseMultiTap()
{
QFETCH(QQuickTapHandler::ExclusiveSignals, exclusiveSignals);
QFETCH(int, expectedSingleTaps);
QFETCH(int, expectedSingleTapsAfterMovingAway);
QFETCH(int, expectedSingleTapsAfterWaiting);
QFETCH(int, expectedDoubleTaps);
const int dragThreshold = QGuiApplication::styleHints()->startDragDistance();
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *button = window->rootObject()->findChild<QQuickItem*>("DragThreshold");
QVERIFY(button);
QQuickTapHandler *tapHandler = button->findChild<QQuickTapHandler*>();
QVERIFY(tapHandler);
tapHandler->setExclusiveSignals(exclusiveSignals);
QSignalSpy tappedSpy(button, SIGNAL(tapped()));
QSignalSpy singleTapSpy(tapHandler, &QQuickTapHandler::singleTapped);
QSignalSpy doubleTapSpy(tapHandler, &QQuickTapHandler::doubleTapped);
// Click once
QPoint p1 = button->mapToScene(QPointF(2, 2)).toPoint();
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1, 10);
QTRY_VERIFY(button->property("pressed").toBool());
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1, 10);
QTRY_VERIFY(!button->property("pressed").toBool());
QCOMPARE(tappedSpy.size(), 1);
// If exclusiveSignals == SingleTap | DoubleTap:
// This would be a single-click if we waited longer than the double-click interval,
// but it's too early for the signal at this moment; and we're going to click again.
// If exclusiveSignals == DoubleTap: singleTapped() won't happen.
// Otherwise: we got singleTapped() immediately.
QCOMPARE(singleTapSpy.size(), expectedSingleTaps);
QCOMPARE(tapHandler->timeHeld(), -1);
// Click again in exactly the same place
QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, p1, 10);
QCOMPARE(tappedSpy.size(), 2);
QCOMPARE(singleTapSpy.size(), expectedSingleTaps);
QCOMPARE(doubleTapSpy.size(), expectedDoubleTaps);
// Click a third time, nearby: that'll be a triple-click
p1 += QPoint(1, 1);
QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, p1, 10);
QCOMPARE(tappedSpy.size(), 3);
QCOMPARE(singleTapSpy.size(), expectedSingleTaps);
QCOMPARE(doubleTapSpy.size(), expectedDoubleTaps);
QCOMPARE(tapHandler->tapCount(), 3);
// Click a fourth time, drifting farther away: treated as a separate click, regardless of timing
p1 += QPoint(dragThreshold, dragThreshold);
QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, p1); // default delay to prevent double-click
QCOMPARE(tappedSpy.size(), 4);
QCOMPARE(tapHandler->tapCount(), 1);
QTRY_COMPARE(singleTapSpy.size(), expectedSingleTapsAfterMovingAway);
// Click a fifth time later on at the same place: treated as a separate click
QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, p1);
QCOMPARE(tappedSpy.size(), 5);
QCOMPARE(tapHandler->tapCount(), 1);
QCOMPARE(singleTapSpy.size(), expectedSingleTapsAfterWaiting);
}
void tst_TapHandler::mouseMultiTapLeftRight_data()
{
QTest::addColumn<QQuickTapHandler::ExclusiveSignals>("exclusiveSignals");
QTest::addColumn<int>("expectedSingleTaps");
QTest::addColumn<int>("expectedDoubleTaps");
QTest::addColumn<int>("expectedTabCount2");
QTest::addColumn<int>("expectedTabCount3");
QTest::newRow("NotExclusive") << QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::NotExclusive)
<< 3 << 0 << 1 << 1;
QTest::newRow("SingleTap") << QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::SingleTap)
<< 3 << 0 << 1 << 1;
QTest::newRow("DoubleTap") << QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::DoubleTap)
<< 0 << 0 << 1 << 1;
QTest::newRow("SingleTap|DoubleTap") << QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::SingleTap | QQuickTapHandler::DoubleTap)
<< 0 << 0 << 1 << 1;
}
void tst_TapHandler::mouseMultiTapLeftRight() //QTBUG-111557
{
QFETCH(QQuickTapHandler::ExclusiveSignals, exclusiveSignals);
QFETCH(int, expectedSingleTaps);
QFETCH(int, expectedDoubleTaps);
QFETCH(int, expectedTabCount2);
QFETCH(int, expectedTabCount3);
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *button = window->rootObject()->findChild<QQuickItem*>("DragThreshold");
QVERIFY(button);
QQuickTapHandler *tapHandler = button->findChild<QQuickTapHandler*>();
QVERIFY(tapHandler);
tapHandler->setExclusiveSignals(exclusiveSignals);
tapHandler->setAcceptedButtons(Qt::LeftButton | Qt::RightButton);
QSignalSpy tappedSpy(button, SIGNAL(tapped()));
QSignalSpy singleTapSpy(tapHandler, &QQuickTapHandler::singleTapped);
QSignalSpy doubleTapSpy(tapHandler, &QQuickTapHandler::doubleTapped);
// Click once with the left button
QPoint p1 = button->mapToScene(QPointF(2, 2)).toPoint();
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1, 10);
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1, 10);
// Click again with the right button -> should reset tabCount()
QTest::mousePress(window, Qt::RightButton, Qt::NoModifier, p1, 10);
QTest::mouseRelease(window, Qt::RightButton, Qt::NoModifier, p1, 10);
QCOMPARE(tapHandler->tapCount(), expectedTabCount2);
// Click again with the left button -> should reset tabCount()
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1, 10);
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1, 10);
QCOMPARE(tapHandler->tapCount(), expectedTabCount3);
QCOMPARE(singleTapSpy.size(), expectedSingleTaps);
QCOMPARE(doubleTapSpy.size(), expectedDoubleTaps);
}
void tst_TapHandler::singleTapDoubleTap_data()
{
QTest::addColumn<QPointingDevice::DeviceType>("deviceType");
QTest::addColumn<QQuickTapHandler::ExclusiveSignals>("exclusiveSignals");
QTest::addColumn<int>("expectedEndingSingleTapCount");
QTest::addColumn<int>("expectedDoubleTapCount");
QTest::newRow("mouse:NotExclusive")
<< QPointingDevice::DeviceType::Mouse
<< QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::NotExclusive)
<< 1 << 1;
QTest::newRow("mouse:SingleTap")
<< QPointingDevice::DeviceType::Mouse
<< QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::SingleTap)
<< 1 << 0;
QTest::newRow("mouse:DoubleTap")
<< QPointingDevice::DeviceType::Mouse
<< QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::DoubleTap)
<< 0 << 1;
QTest::newRow("mouse:SingleTap|DoubleTap")
<< QPointingDevice::DeviceType::Mouse
<< QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::SingleTap | QQuickTapHandler::DoubleTap)
<< 0 << 1;
QTest::newRow("touch:NotExclusive")
<< QPointingDevice::DeviceType::TouchScreen
<< QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::NotExclusive)
<< 1 << 1;
QTest::newRow("touch:SingleTap")
<< QPointingDevice::DeviceType::TouchScreen
<< QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::SingleTap)
<< 1 << 0;
QTest::newRow("touch:DoubleTap")
<< QPointingDevice::DeviceType::TouchScreen
<< QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::DoubleTap)
<< 0 << 1;
QTest::newRow("touch:SingleTap|DoubleTap")
<< QPointingDevice::DeviceType::TouchScreen
<< QQuickTapHandler::ExclusiveSignals(QQuickTapHandler::SingleTap | QQuickTapHandler::DoubleTap)
<< 0 << 1;
}
void tst_TapHandler::singleTapDoubleTap()
{
QFETCH(QPointingDevice::DeviceType, deviceType);
QFETCH(QQuickTapHandler::ExclusiveSignals, exclusiveSignals);
QFETCH(int, expectedEndingSingleTapCount);
QFETCH(int, expectedDoubleTapCount);
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *button = window->rootObject()->findChild<QQuickItem*>("DragThreshold");
QVERIFY(button);
QQuickTapHandler *tapHandler = button->findChild<QQuickTapHandler*>();
QVERIFY(tapHandler);
tapHandler->setExclusiveSignals(exclusiveSignals);
QSignalSpy tappedSpy(tapHandler, &QQuickTapHandler::tapped);
QSignalSpy singleTapSpy(tapHandler, &QQuickTapHandler::singleTapped);
QSignalSpy doubleTapSpy(tapHandler, &QQuickTapHandler::doubleTapped);
auto tap = [window, tapHandler, deviceType, this](const QPoint &p1) {
switch (static_cast<QPointingDevice::DeviceType>(deviceType)) {
case QPointingDevice::DeviceType::Mouse:
QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, p1, 10);
break;
case QPointingDevice::DeviceType::TouchScreen:
QTest::touchEvent(window, touchDevice).press(0, p1, window);
QTRY_VERIFY(tapHandler->isPressed());
QTest::touchEvent(window, touchDevice).release(0, p1, window);
break;
default:
break;
}
};
// tap once
const QPoint p1 = button->mapToScene(QPointF(2, 2)).toPoint();
tap(p1);
QCOMPARE(tappedSpy.size(), 1);
QCOMPARE(doubleTapSpy.size(), 0);
// tap again immediately afterwards
tap(p1);
QTRY_COMPARE(doubleTapSpy.size(), expectedDoubleTapCount);
QCOMPARE(tappedSpy.size(), 2);
QCOMPARE(singleTapSpy.size(), expectedEndingSingleTapCount);
}
void tst_TapHandler::touchLongPress()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *button = window->rootObject()->findChild<QQuickItem*>("DragThreshold");
QVERIFY(button);
QQuickTapHandler *tapHandler = button->findChild<QQuickTapHandler*>("DragThreshold");
QVERIFY(tapHandler);
QSignalSpy tappedSpy(button, SIGNAL(tapped()));
QSignalSpy longPressThresholdChangedSpy(tapHandler, SIGNAL(longPressThresholdChanged()));
QSignalSpy timeHeldSpy(tapHandler, SIGNAL(timeHeldChanged()));
QSignalSpy longPressedSpy(tapHandler, SIGNAL(longPressed()));
// Reduce the threshold so that we can get a long press quickly
tapHandler->setLongPressThreshold(0.5);
QCOMPARE(longPressThresholdChangedSpy.size(), 1);
// Press and hold
QPoint p1 = button->mapToScene(button->clipRect().center()).toPoint();
QTest::touchEvent(window, touchDevice).press(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(button->property("pressed").toBool());
QTRY_COMPARE(longPressedSpy.size(), 1);
timeHeldSpy.wait(); // the longer we hold it, the more this will occur
qDebug() << "held" << tapHandler->timeHeld() << "secs; timeHeld updated" << timeHeldSpy.size() << "times";
QVERIFY(timeHeldSpy.size() > 0);
QVERIFY(tapHandler->timeHeld() > 0.4); // Should be > 0.5 but slow CI and timer granularity can interfere
// Release and verify that tapped was not emitted
QTest::touchEvent(window, touchDevice).release(1, p1, window);
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!button->property("pressed").toBool());
QCOMPARE(tappedSpy.size(), 0);
}
void tst_TapHandler::mouseLongPress()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *button = window->rootObject()->findChild<QQuickItem*>("DragThreshold");
QVERIFY(button);
QQuickTapHandler *tapHandler = button->findChild<QQuickTapHandler*>("DragThreshold");
QVERIFY(tapHandler);
QSignalSpy tappedSpy(button, SIGNAL(tapped()));
QSignalSpy longPressThresholdChangedSpy(tapHandler, SIGNAL(longPressThresholdChanged()));
QSignalSpy timeHeldSpy(tapHandler, SIGNAL(timeHeldChanged()));
QSignalSpy longPressedSpy(tapHandler, SIGNAL(longPressed()));
// Reduce the threshold so that we can get a long press quickly
tapHandler->setLongPressThreshold(0.5);
QCOMPARE(longPressThresholdChangedSpy.size(), 1);
// Press and hold
QPoint p1 = button->mapToScene(button->clipRect().center()).toPoint();
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_VERIFY(button->property("pressed").toBool());
QTRY_COMPARE(longPressedSpy.size(), 1);
timeHeldSpy.wait(); // the longer we hold it, the more this will occur
qDebug() << "held" << tapHandler->timeHeld() << "secs; timeHeld updated" << timeHeldSpy.size() << "times";
QVERIFY(timeHeldSpy.size() > 0);
QVERIFY(tapHandler->timeHeld() > 0.4); // Should be > 0.5 but slow CI and timer granularity can interfere
// Release and verify that tapped was not emitted
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1, 500);
QTRY_VERIFY(!button->property("pressed").toBool());
QCOMPARE(tappedSpy.size(), 0);
}
void tst_TapHandler::buttonsMultiTouch()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QQuickItem *buttonDragThreshold = window->rootObject()->findChild<QQuickItem*>("DragThreshold");
QVERIFY(buttonDragThreshold);
QSignalSpy dragThresholdTappedSpy(buttonDragThreshold, SIGNAL(tapped()));
QQuickItem *buttonWithinBounds = window->rootObject()->findChild<QQuickItem*>("WithinBounds");
QVERIFY(buttonWithinBounds);
QSignalSpy withinBoundsTappedSpy(buttonWithinBounds, SIGNAL(tapped()));
QQuickItem *buttonReleaseWithinBounds = window->rootObject()->findChild<QQuickItem*>("ReleaseWithinBounds");
QVERIFY(buttonReleaseWithinBounds);
QSignalSpy releaseWithinBoundsTappedSpy(buttonReleaseWithinBounds, SIGNAL(tapped()));
QTest::QTouchEventSequence touchSeq = QTest::touchEvent(window, touchDevice, false);
// can press multiple buttons at the same time
QPoint p1 = buttonDragThreshold->mapToScene(QPointF(20, 20)).toPoint();
touchSeq.press(1, p1, window).commit();
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonDragThreshold->property("pressed").toBool());
QPoint p2 = buttonWithinBounds->mapToScene(QPointF(20, 20)).toPoint();
touchSeq.stationary(1).press(2, p2, window).commit();
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonWithinBounds->property("pressed").toBool());
QVERIFY(buttonWithinBounds->property("active").toBool());
QPoint p3 = buttonReleaseWithinBounds->mapToScene(QPointF(20, 20)).toPoint();
touchSeq.stationary(1).stationary(2).press(3, p3, window).commit();
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
QVERIFY(buttonReleaseWithinBounds->property("active").toBool());
QVERIFY(buttonWithinBounds->property("pressed").toBool());
QVERIFY(buttonWithinBounds->property("active").toBool());
QVERIFY(buttonDragThreshold->property("pressed").toBool());
// combinations of small touchpoint movements and stationary points should not cause state changes
p1 += QPoint(2, 0);
p2 += QPoint(3, 0);
touchSeq.move(1, p1).move(2, p2).stationary(3).commit();
QVERIFY(buttonDragThreshold->property("pressed").toBool());
QVERIFY(buttonWithinBounds->property("pressed").toBool());
QVERIFY(buttonWithinBounds->property("active").toBool());
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
QVERIFY(buttonReleaseWithinBounds->property("active").toBool());
p3 += QPoint(4, 0);
touchSeq.stationary(1).stationary(2).move(3, p3).commit();
QVERIFY(buttonDragThreshold->property("pressed").toBool());
QVERIFY(buttonWithinBounds->property("pressed").toBool());
QVERIFY(buttonWithinBounds->property("active").toBool());
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
QVERIFY(buttonReleaseWithinBounds->property("active").toBool());
// can release top button and press again: others stay pressed the whole time
touchSeq.stationary(2).stationary(3).release(1, p1, window).commit();
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!buttonDragThreshold->property("pressed").toBool());
QCOMPARE(dragThresholdTappedSpy.size(), 1);
QVERIFY(buttonWithinBounds->property("pressed").toBool());
QCOMPARE(withinBoundsTappedSpy.size(), 0);
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
QCOMPARE(releaseWithinBoundsTappedSpy.size(), 0);
touchSeq.stationary(2).stationary(3).press(1, p1, window).commit();
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonDragThreshold->property("pressed").toBool());
QVERIFY(buttonWithinBounds->property("pressed").toBool());
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
// can release middle button and press again: others stay pressed the whole time
touchSeq.stationary(1).stationary(3).release(2, p2, window).commit();
QQuickTouchUtils::flush(window);
QTRY_VERIFY(!buttonWithinBounds->property("pressed").toBool());
QCOMPARE(withinBoundsTappedSpy.size(), 1);
QVERIFY(buttonDragThreshold->property("pressed").toBool());
QCOMPARE(dragThresholdTappedSpy.size(), 1);
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
QCOMPARE(releaseWithinBoundsTappedSpy.size(), 0);
touchSeq.stationary(1).stationary(3).press(2, p2, window).commit();
QQuickTouchUtils::flush(window);
QVERIFY(buttonDragThreshold->property("pressed").toBool());
QVERIFY(buttonWithinBounds->property("pressed").toBool());
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
// can release bottom button and press again: others stay pressed the whole time
touchSeq.stationary(1).stationary(2).release(3, p3, window).commit();
QQuickTouchUtils::flush(window);
QCOMPARE(releaseWithinBoundsTappedSpy.size(), 1);
QVERIFY(buttonWithinBounds->property("pressed").toBool());
QCOMPARE(withinBoundsTappedSpy.size(), 1);
QVERIFY(!buttonReleaseWithinBounds->property("pressed").toBool());
QCOMPARE(dragThresholdTappedSpy.size(), 1);
touchSeq.stationary(1).stationary(2).press(3, p3, window).commit();
QQuickTouchUtils::flush(window);
QTRY_VERIFY(buttonDragThreshold->property("pressed").toBool());
QVERIFY(buttonWithinBounds->property("pressed").toBool());
QVERIFY(buttonReleaseWithinBounds->property("pressed").toBool());
}
void tst_TapHandler::componentUserBehavioralOverride()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttonOverrideHandler.qml");
QQuickView * window = windowPtr.data();
QQuickItem *button = window->rootObject()->findChild<QQuickItem*>("Overridden");
QVERIFY(button);
QQuickTapHandler *innerTapHandler = button->findChild<QQuickTapHandler*>("Overridden");
QVERIFY(innerTapHandler);
QQuickTapHandler *userTapHandler = button->findChild<QQuickTapHandler*>("override");
QVERIFY(userTapHandler);
QSignalSpy tappedSpy(button, SIGNAL(tapped()));
QSignalSpy innerGrabChangedSpy(innerTapHandler, SIGNAL(grabChanged(QPointingDevice::GrabTransition, QEventPoint)));
QSignalSpy userGrabChangedSpy(userTapHandler, SIGNAL(grabChanged(QPointingDevice::GrabTransition, QEventPoint)));
QSignalSpy innerPressedChangedSpy(innerTapHandler, SIGNAL(pressedChanged()));
QSignalSpy userPressedChangedSpy(userTapHandler, SIGNAL(pressedChanged()));
// Press
QPoint p1 = button->mapToScene(button->clipRect().center()).toPoint();
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_COMPARE(userPressedChangedSpy.size(), 1);
QCOMPARE(innerPressedChangedSpy.size(), 0);
QCOMPARE(innerGrabChangedSpy.size(), 0);
QCOMPARE(userGrabChangedSpy.size(), 1);
// Release
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1);
QTRY_COMPARE(userPressedChangedSpy.size(), 2);
QCOMPARE(innerPressedChangedSpy.size(), 0);
QCOMPARE(tappedSpy.size(), 1); // only because the override handler makes that happen
QCOMPARE(innerGrabChangedSpy.size(), 0);
QCOMPARE(userGrabChangedSpy.size(), 2);
}
void tst_TapHandler::rightLongPressIgnoreWheel()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "rightTapHandler.qml");
QQuickView * window = windowPtr.data();
QQuickTapHandler *tap = window->rootObject()->findChild<QQuickTapHandler*>();
QVERIFY(tap);
QSignalSpy tappedSpy(tap, &QQuickTapHandler::tapped);
QSignalSpy longPressedSpy(tap, &QQuickTapHandler::longPressed);
QPoint p1(100, 100);
// Mouse wheel with ScrollBegin phase (because as soon as two fingers are touching
// the trackpad, it will send such an event: QTBUG-71955)
{
QWheelEvent wheelEvent(p1, p1, QPoint(0, 0), QPoint(0, 0),
Qt::NoButton, Qt::NoModifier, Qt::ScrollBegin, false, Qt::MouseEventNotSynthesized);
QGuiApplication::sendEvent(window, &wheelEvent);
}
// Press
QTest::mousePress(window, Qt::RightButton, Qt::NoModifier, p1);
QTRY_COMPARE(tap->isPressed(), true);
// Mouse wheel ScrollEnd phase
QWheelEvent wheelEvent(p1, p1, QPoint(0, 0), QPoint(0, 0),
Qt::NoButton, Qt::NoModifier, Qt::ScrollEnd, false, Qt::MouseEventNotSynthesized);
QGuiApplication::sendEvent(window, &wheelEvent);
QTRY_COMPARE(longPressedSpy.size(), 1);
QCOMPARE(tap->isPressed(), true);
QCOMPARE(tappedSpy.size(), 0);
// Release
QTest::mouseRelease(window, Qt::RightButton, Qt::NoModifier, p1, 500);
QTRY_COMPARE(tap->isPressed(), false);
QCOMPARE(tappedSpy.size(), 0);
}
void tst_TapHandler::negativeZStackingOrder() // QTBUG-83114
{
QScopedPointer<QQuickView> windowPtr;
QQuickItem: ignore double-clicks by default; remove allowDoubleClick Because Qt has a pattern that events arrive pre-accepted, most event-handling functions in QQuickItem call ignore(), and it's up to subclasses to override those functions to allow the event to remain accepted, if they choose to handle it. So it was odd that QQuickItem::mouseDoubleClickEvent() did not call ignore(). Pointer handlers don't handle MouseButtonDblClick events, so QQuickDeliveryAgent does not send those events to handlers. Since 0e3adb65b0e9c44fa6e202630ff57c907ecf0820 though, we disallowed delivery of double-click events to Items after any handler has already accepted the single point in a mouse event. This caused some inconsistencies; in fact the allowDoubleClick variable was getting thrashed a lot, making it hard to reason about the logic. Items that contained handlers behaved differently than items that did not. One scenario being fixed here was absurd: a parent Rectangle (which never handles pointer events on its own) got an implicit grab in deliverMatchingPointsToItem() and thus stole the grab from a handler (!), during delivery of a MouseButtonDblClick (!!), just because the event happened to remain accepted, even though no item or handler reacted to it directly. The Rectangle needs to ignore() the event to avoid that, just as all Items now do by default. Then it turns out that we don't need a stateful allowDoubleClick anymore: the logic is more consistent without it. Items can handle double-clicks, but they don't by default, as with any other pointer event. Pointer handlers don't handle MouseButtonDblClick because they detect double-clicks in their own way, and that's enforced by simply not sending those events to handlers. Passive grabs should be retained regardless of the interloper MouseButtonDblClick event: items that handle it cannot cancel a handler's passive grab. They can steal a handler's exclusive grab, but that should be prevented in other ways, such as ignoring the event so that there is no accidental implicit grab. Reverts 0e3adb65b0e9c44fa6e202630ff57c907ecf0820. DeliveryAgent no longer calls clearPassiveGrabbers() directly as QQuickWindow did then; and it also no longer delivers MouseButtonDblClick the same as a press event. QSinglePointEvent::isBeginEvent() returns false in that case, so deliverPressOrReleaseEvent() is not called. A couple of existing tests now need to avoid generating double-clicks, but they were not trying to test that anyway. New tests are added (test coverage of double-clicks has been unfortunately sparse so far). Pick-to: 6.3 Fixes: QTBUG-102625 Change-Id: If74baff68ffc46b8b403d37f4e10ddf6b159d40c Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Doris Verria <doris.verria@qt.io> Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
2022-05-30 21:04:22 +00:00
createView(windowPtr, "nested.qml");
QQuickView *window = windowPtr.data();
QQuickItem *root = window->rootObject();
QQuickTapHandler *parentTapHandler = window->rootObject()->findChild<QQuickTapHandler*>("parentTapHandler");
QVERIFY(parentTapHandler != nullptr);
QSignalSpy clickSpyParent(parentTapHandler, &QQuickTapHandler::tapped);
QQuickTapHandler *childTapHandler = window->rootObject()->findChild<QQuickTapHandler*>("childTapHandler");
QVERIFY(childTapHandler != nullptr);
QSignalSpy clickSpyChild(childTapHandler, &QQuickTapHandler::tapped);
QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, QPoint(150, 100));
QCOMPARE(clickSpyChild.size(), 1);
QCOMPARE(clickSpyParent.size(), 1);
auto order = root->property("taps").toList();
QVERIFY(order.at(0) == "childTapHandler");
QVERIFY(order.at(1) == "parentTapHandler");
// Now change stacking order and try again.
childTapHandler->parentItem()->setZ(-1);
root->setProperty("taps", QVariantList());
QTest::mouseClick(window, Qt::LeftButton, Qt::NoModifier, QPoint(150, 100));
QCOMPARE(clickSpyChild.size(), 2);
QCOMPARE(clickSpyParent.size(), 2);
order = root->property("taps").toList();
QVERIFY(order.at(0) == "parentTapHandler");
QVERIFY(order.at(1) == "childTapHandler");
}
void tst_TapHandler::nonTopLevelParentWindow() // QTBUG-91716
{
QScopedPointer<QQuickWindow> parentWindowPtr(new QQuickWindow);
auto parentWindow = parentWindowPtr.get();
parentWindow->setGeometry(400, 400, 250, 250);
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "simpleTapHandler.qml", parentWindow);
auto window = windowPtr.get();
window->setGeometry(10, 10, 100, 100);
QQuickItem *root = window->rootObject();
auto p1 = QPoint(20, 20);
mouseEvent(QEvent::MouseButtonPress, Qt::LeftButton, p1, window, parentWindow);
mouseEvent(QEvent::MouseButtonRelease, Qt::LeftButton, p1, window, parentWindow);
QCOMPARE(root->property("tapCount").toInt(), 1);
QTest::touchEvent(window, touchDevice).press(0, p1, parentWindow).commit();
QTest::touchEvent(window, touchDevice).release(0, p1, parentWindow).commit();
QCOMPARE(root->property("tapCount").toInt(), 2);
}
QQuickItem: ignore double-clicks by default; remove allowDoubleClick Because Qt has a pattern that events arrive pre-accepted, most event-handling functions in QQuickItem call ignore(), and it's up to subclasses to override those functions to allow the event to remain accepted, if they choose to handle it. So it was odd that QQuickItem::mouseDoubleClickEvent() did not call ignore(). Pointer handlers don't handle MouseButtonDblClick events, so QQuickDeliveryAgent does not send those events to handlers. Since 0e3adb65b0e9c44fa6e202630ff57c907ecf0820 though, we disallowed delivery of double-click events to Items after any handler has already accepted the single point in a mouse event. This caused some inconsistencies; in fact the allowDoubleClick variable was getting thrashed a lot, making it hard to reason about the logic. Items that contained handlers behaved differently than items that did not. One scenario being fixed here was absurd: a parent Rectangle (which never handles pointer events on its own) got an implicit grab in deliverMatchingPointsToItem() and thus stole the grab from a handler (!), during delivery of a MouseButtonDblClick (!!), just because the event happened to remain accepted, even though no item or handler reacted to it directly. The Rectangle needs to ignore() the event to avoid that, just as all Items now do by default. Then it turns out that we don't need a stateful allowDoubleClick anymore: the logic is more consistent without it. Items can handle double-clicks, but they don't by default, as with any other pointer event. Pointer handlers don't handle MouseButtonDblClick because they detect double-clicks in their own way, and that's enforced by simply not sending those events to handlers. Passive grabs should be retained regardless of the interloper MouseButtonDblClick event: items that handle it cannot cancel a handler's passive grab. They can steal a handler's exclusive grab, but that should be prevented in other ways, such as ignoring the event so that there is no accidental implicit grab. Reverts 0e3adb65b0e9c44fa6e202630ff57c907ecf0820. DeliveryAgent no longer calls clearPassiveGrabbers() directly as QQuickWindow did then; and it also no longer delivers MouseButtonDblClick the same as a press event. QSinglePointEvent::isBeginEvent() returns false in that case, so deliverPressOrReleaseEvent() is not called. A couple of existing tests now need to avoid generating double-clicks, but they were not trying to test that anyway. New tests are added (test coverage of double-clicks has been unfortunately sparse so far). Pick-to: 6.3 Fixes: QTBUG-102625 Change-Id: If74baff68ffc46b8b403d37f4e10ddf6b159d40c Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Doris Verria <doris.verria@qt.io> Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
2022-05-30 21:04:22 +00:00
void tst_TapHandler::nestedDoubleTap_data()
{
QTest::addColumn<QQuickTapHandler::GesturePolicy>("childGesturePolicy");
QTest::newRow("DragThreshold") << QQuickTapHandler::GesturePolicy::DragThreshold;
QTest::newRow("WithinBounds") << QQuickTapHandler::GesturePolicy::WithinBounds;
QTest::newRow("ReleaseWithinBounds") << QQuickTapHandler::GesturePolicy::ReleaseWithinBounds;
QTest::newRow("DragWithinBounds") << QQuickTapHandler::GesturePolicy::DragWithinBounds;
}
void tst_TapHandler::nestedDoubleTap() // QTBUG-102625
{
QFETCH(QQuickTapHandler::GesturePolicy, childGesturePolicy);
QQuickView window;
QVERIFY(QQuickTest::showView(window, testFileUrl("nested.qml")));
QQuickItem *root = window.rootObject();
QQuickTapHandler *parentTapHandler = root->findChild<QQuickTapHandler*>("parentTapHandler");
QVERIFY(parentTapHandler);
QSignalSpy parentSpy(parentTapHandler, &QQuickTapHandler::doubleTapped);
QQuickTapHandler *childTapHandler = root->findChild<QQuickTapHandler*>("childTapHandler");
QVERIFY(childTapHandler);
QSignalSpy childSpy(childTapHandler, &QQuickTapHandler::doubleTapped);
childTapHandler->setGesturePolicy(childGesturePolicy);
QTest::mouseDClick(&window, Qt::LeftButton, Qt::NoModifier, QPoint(150, 100));
QCOMPARE(childSpy.size(), 1);
QQuickItem: ignore double-clicks by default; remove allowDoubleClick Because Qt has a pattern that events arrive pre-accepted, most event-handling functions in QQuickItem call ignore(), and it's up to subclasses to override those functions to allow the event to remain accepted, if they choose to handle it. So it was odd that QQuickItem::mouseDoubleClickEvent() did not call ignore(). Pointer handlers don't handle MouseButtonDblClick events, so QQuickDeliveryAgent does not send those events to handlers. Since 0e3adb65b0e9c44fa6e202630ff57c907ecf0820 though, we disallowed delivery of double-click events to Items after any handler has already accepted the single point in a mouse event. This caused some inconsistencies; in fact the allowDoubleClick variable was getting thrashed a lot, making it hard to reason about the logic. Items that contained handlers behaved differently than items that did not. One scenario being fixed here was absurd: a parent Rectangle (which never handles pointer events on its own) got an implicit grab in deliverMatchingPointsToItem() and thus stole the grab from a handler (!), during delivery of a MouseButtonDblClick (!!), just because the event happened to remain accepted, even though no item or handler reacted to it directly. The Rectangle needs to ignore() the event to avoid that, just as all Items now do by default. Then it turns out that we don't need a stateful allowDoubleClick anymore: the logic is more consistent without it. Items can handle double-clicks, but they don't by default, as with any other pointer event. Pointer handlers don't handle MouseButtonDblClick because they detect double-clicks in their own way, and that's enforced by simply not sending those events to handlers. Passive grabs should be retained regardless of the interloper MouseButtonDblClick event: items that handle it cannot cancel a handler's passive grab. They can steal a handler's exclusive grab, but that should be prevented in other ways, such as ignoring the event so that there is no accidental implicit grab. Reverts 0e3adb65b0e9c44fa6e202630ff57c907ecf0820. DeliveryAgent no longer calls clearPassiveGrabbers() directly as QQuickWindow did then; and it also no longer delivers MouseButtonDblClick the same as a press event. QSinglePointEvent::isBeginEvent() returns false in that case, so deliverPressOrReleaseEvent() is not called. A couple of existing tests now need to avoid generating double-clicks, but they were not trying to test that anyway. New tests are added (test coverage of double-clicks has been unfortunately sparse so far). Pick-to: 6.3 Fixes: QTBUG-102625 Change-Id: If74baff68ffc46b8b403d37f4e10ddf6b159d40c Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Doris Verria <doris.verria@qt.io> Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
2022-05-30 21:04:22 +00:00
// If the child gets by with a passive grab, both handlers see tap and double-tap.
// If the child takes an exclusive grab and stops event propagation, the parent doesn't see them.
QCOMPARE(parentSpy.size(),
QQuickItem: ignore double-clicks by default; remove allowDoubleClick Because Qt has a pattern that events arrive pre-accepted, most event-handling functions in QQuickItem call ignore(), and it's up to subclasses to override those functions to allow the event to remain accepted, if they choose to handle it. So it was odd that QQuickItem::mouseDoubleClickEvent() did not call ignore(). Pointer handlers don't handle MouseButtonDblClick events, so QQuickDeliveryAgent does not send those events to handlers. Since 0e3adb65b0e9c44fa6e202630ff57c907ecf0820 though, we disallowed delivery of double-click events to Items after any handler has already accepted the single point in a mouse event. This caused some inconsistencies; in fact the allowDoubleClick variable was getting thrashed a lot, making it hard to reason about the logic. Items that contained handlers behaved differently than items that did not. One scenario being fixed here was absurd: a parent Rectangle (which never handles pointer events on its own) got an implicit grab in deliverMatchingPointsToItem() and thus stole the grab from a handler (!), during delivery of a MouseButtonDblClick (!!), just because the event happened to remain accepted, even though no item or handler reacted to it directly. The Rectangle needs to ignore() the event to avoid that, just as all Items now do by default. Then it turns out that we don't need a stateful allowDoubleClick anymore: the logic is more consistent without it. Items can handle double-clicks, but they don't by default, as with any other pointer event. Pointer handlers don't handle MouseButtonDblClick because they detect double-clicks in their own way, and that's enforced by simply not sending those events to handlers. Passive grabs should be retained regardless of the interloper MouseButtonDblClick event: items that handle it cannot cancel a handler's passive grab. They can steal a handler's exclusive grab, but that should be prevented in other ways, such as ignoring the event so that there is no accidental implicit grab. Reverts 0e3adb65b0e9c44fa6e202630ff57c907ecf0820. DeliveryAgent no longer calls clearPassiveGrabbers() directly as QQuickWindow did then; and it also no longer delivers MouseButtonDblClick the same as a press event. QSinglePointEvent::isBeginEvent() returns false in that case, so deliverPressOrReleaseEvent() is not called. A couple of existing tests now need to avoid generating double-clicks, but they were not trying to test that anyway. New tests are added (test coverage of double-clicks has been unfortunately sparse so far). Pick-to: 6.3 Fixes: QTBUG-102625 Change-Id: If74baff68ffc46b8b403d37f4e10ddf6b159d40c Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Doris Verria <doris.verria@qt.io> Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
2022-05-30 21:04:22 +00:00
childGesturePolicy == QQuickTapHandler::GesturePolicy::DragThreshold ? 1 : 0);
QCOMPARE(root->property("taps").toList().size(),
QQuickItem: ignore double-clicks by default; remove allowDoubleClick Because Qt has a pattern that events arrive pre-accepted, most event-handling functions in QQuickItem call ignore(), and it's up to subclasses to override those functions to allow the event to remain accepted, if they choose to handle it. So it was odd that QQuickItem::mouseDoubleClickEvent() did not call ignore(). Pointer handlers don't handle MouseButtonDblClick events, so QQuickDeliveryAgent does not send those events to handlers. Since 0e3adb65b0e9c44fa6e202630ff57c907ecf0820 though, we disallowed delivery of double-click events to Items after any handler has already accepted the single point in a mouse event. This caused some inconsistencies; in fact the allowDoubleClick variable was getting thrashed a lot, making it hard to reason about the logic. Items that contained handlers behaved differently than items that did not. One scenario being fixed here was absurd: a parent Rectangle (which never handles pointer events on its own) got an implicit grab in deliverMatchingPointsToItem() and thus stole the grab from a handler (!), during delivery of a MouseButtonDblClick (!!), just because the event happened to remain accepted, even though no item or handler reacted to it directly. The Rectangle needs to ignore() the event to avoid that, just as all Items now do by default. Then it turns out that we don't need a stateful allowDoubleClick anymore: the logic is more consistent without it. Items can handle double-clicks, but they don't by default, as with any other pointer event. Pointer handlers don't handle MouseButtonDblClick because they detect double-clicks in their own way, and that's enforced by simply not sending those events to handlers. Passive grabs should be retained regardless of the interloper MouseButtonDblClick event: items that handle it cannot cancel a handler's passive grab. They can steal a handler's exclusive grab, but that should be prevented in other ways, such as ignoring the event so that there is no accidental implicit grab. Reverts 0e3adb65b0e9c44fa6e202630ff57c907ecf0820. DeliveryAgent no longer calls clearPassiveGrabbers() directly as QQuickWindow did then; and it also no longer delivers MouseButtonDblClick the same as a press event. QSinglePointEvent::isBeginEvent() returns false in that case, so deliverPressOrReleaseEvent() is not called. A couple of existing tests now need to avoid generating double-clicks, but they were not trying to test that anyway. New tests are added (test coverage of double-clicks has been unfortunately sparse so far). Pick-to: 6.3 Fixes: QTBUG-102625 Change-Id: If74baff68ffc46b8b403d37f4e10ddf6b159d40c Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org> Reviewed-by: Doris Verria <doris.verria@qt.io> Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
2022-05-30 21:04:22 +00:00
childGesturePolicy == QQuickTapHandler::GesturePolicy::DragThreshold ? 4 : 2);
}
QTEST_MAIN(tst_TapHandler)
#include "tst_qquicktaphandler.moc"