diff --git a/examples/quick/pointerhandlers/components/Button.qml b/examples/quick/pointerhandlers/components/Button.qml index 879e24f5f8..4943f9cb94 100644 --- a/examples/quick/pointerhandlers/components/Button.qml +++ b/examples/quick/pointerhandlers/components/Button.qml @@ -11,6 +11,7 @@ Rectangle { property alias hovered: hoverHandler.hovered property alias gesturePolicy: tap.gesturePolicy property alias margin: tap.margin + property alias exclusiveSignals: tap.exclusiveSignals signal tapped implicitHeight: Math.max(Screen.pixelDensity * 7, label.implicitHeight * 2) @@ -29,7 +30,7 @@ Rectangle { id: tap margin: 10 // the user can tap a little beyond the edges objectName: label.text + " Tap" - onTapped: { + onSingleTapped: { tapFlash.start() root.tapped() } diff --git a/examples/quick/pointerhandlers/multibuttons.qml b/examples/quick/pointerhandlers/multibuttons.qml index 422065cc42..29657f10ea 100644 --- a/examples/quick/pointerhandlers/multibuttons.qml +++ b/examples/quick/pointerhandlers/multibuttons.qml @@ -30,6 +30,7 @@ Item { text: "Launch Missile" Layout.fillWidth: true gesturePolicy: TapHandler.ReleaseWithinBounds + exclusiveSignals: TapHandler.SingleTap onTapped: missileEmitter.burst(1) Text { anchors { top: parent.bottom; horizontalCenter: parent.horizontalCenter } diff --git a/src/quick/handlers/qquicktaphandler.cpp b/src/quick/handlers/qquicktaphandler.cpp index 0ec7c0ab65..bb59af1f81 100644 --- a/src/quick/handlers/qquicktaphandler.cpp +++ b/src/quick/handlers/qquicktaphandler.cpp @@ -179,6 +179,14 @@ void QQuickTapHandler::timerEvent(QTimerEvent *event) m_longPressTimer.stop(); qCDebug(lcTapHandler) << objectName() << "longPressed"; emit longPressed(); + } else if (event->timerId() == m_doubleTapTimer.timerId()) { + m_doubleTapTimer.stop(); + qCDebug(lcTapHandler) << objectName() << "double-tap timer expired; taps:" << m_tapCount; + Q_ASSERT(m_exclusiveSignals == (SingleTap | DoubleTap)); + if (m_tapCount == 1) + emit singleTapped(m_singleTapReleasedPoint, m_singleTapReleasedButton); + else if (m_tapCount == 2) + emit doubleTapped(m_singleTapReleasedPoint, m_singleTapReleasedButton); } } @@ -247,6 +255,38 @@ void QQuickTapHandler::setGesturePolicy(QQuickTapHandler::GesturePolicy gestureP emit gesturePolicyChanged(); } +/*! + \qmlproperty enumeration QtQuick::TapHandler::exclusiveSignals + \since 6.5 + + Determines the exclusivity of the singleTapped() and doubleTapped() signals. + + \value NotExclusive (the default) singleTapped() and doubleTapped() are + emitted immediately when the user taps once or twice, respectively. + + \value SingleTap singleTapped() is emitted immediately when the user taps + once, and doubleTapped() is never emitted. + + \value DoubleTap doubleTapped() is emitted immediately when the user taps + twice, and singleTapped() is never emitted. + + \value (SingleTap | DoubleTap) Both signals are delayed until + QStyleHints::mouseDoubleClickInterval(), such that either singleTapped() + or doubleTapped() can be emitted, but not both. But if 3 or more taps + occur within \c mouseDoubleClickInterval, neither signal is emitted. + + \note The remaining signals such as tapped() and tapCountChanged() are + always emitted immediately, regardless of this property. +*/ +void QQuickTapHandler::setExclusiveSignals(QQuickTapHandler::ExclusiveSignals exc) +{ + if (m_exclusiveSignals == exc) + return; + + m_exclusiveSignals = exc; + emit exclusiveSignalsChanged(); +} + /*! \qmlproperty bool QtQuick::TapHandler::pressed \readonly @@ -270,6 +310,16 @@ void QQuickTapHandler::setPressed(bool press, bool cancel, QPointerEvent *event, } else { m_longPressTimer.stop(); m_holdTimer.invalidate(); + if (m_exclusiveSignals == (SingleTap | DoubleTap)) { + if (m_tapCount == 0) { + m_singleTapReleasedPoint = point; + m_singleTapReleasedButton = event->isSinglePointEvent() ? static_cast(event)->button() : Qt::NoButton; + qCDebug(lcTapHandler) << objectName() << "waiting to emit singleTapped:" << qApp->styleHints()->mouseDoubleClickInterval() << "ms"; + m_doubleTapTimer.start(qApp->styleHints()->mouseDoubleClickInterval(), this); + } else if (m_doubleTapTimer.isActive()) { + qCDebug(lcTapHandler) << objectName() << "tap" << (m_tapCount + 1) << "after" << event->timestamp() / 1000.0 - m_lastTapTimestamp << "sec"; + } + } } if (press) { // on press, grab before emitting changed signals @@ -281,22 +331,25 @@ void QQuickTapHandler::setPressed(bool press, bool cancel, QPointerEvent *event, if (!cancel && !press && parentContains(point)) { if (point.timeHeld() < longPressThreshold()) { // Assuming here that pointerEvent()->timestamp() is in ms. - qreal ts = event->timestamp() / 1000.0; - if (ts - m_lastTapTimestamp < m_multiTapInterval && - QVector2D(point.scenePosition() - m_lastTapPos).lengthSquared() < + const qreal ts = event->timestamp() / 1000.0; + const qreal interval = ts - m_lastTapTimestamp; + const auto distanceSquared = QVector2D(point.scenePosition() - m_lastTapPos).lengthSquared(); + if (interval < m_multiTapInterval && distanceSquared < (event->device()->type() == QInputDevice::DeviceType::Mouse ? m_mouseMultiClickDistanceSquared : m_touchMultiTapDistanceSquared)) ++m_tapCount; else m_tapCount = 1; - qCDebug(lcTapHandler) << objectName() << "tapped" << m_tapCount << "times"; + qCDebug(lcTapHandler) << objectName() << "tapped" << m_tapCount << "times; interval since last:" << interval + << "sec; distance since last:" << qSqrt(distanceSquared); auto button = event->isSinglePointEvent() ? static_cast(event)->button() : Qt::NoButton; emit tapped(point, button); emit tapCountChanged(); - if (m_tapCount == 1) + if (m_tapCount == 1 && !m_exclusiveSignals.testFlag(DoubleTap)) emit singleTapped(point, button); - else if (m_tapCount == 2) + else if (m_tapCount == 2 && !m_exclusiveSignals.testFlag(SingleTap)) { emit doubleTapped(point, button); + } m_lastTapTimestamp = ts; m_lastTapPos = point.scenePosition(); } else { diff --git a/src/quick/handlers/qquicktaphandler_p.h b/src/quick/handlers/qquicktaphandler_p.h index dd9b09f5c8..feb3ab70ce 100644 --- a/src/quick/handlers/qquicktaphandler_p.h +++ b/src/quick/handlers/qquicktaphandler_p.h @@ -30,6 +30,7 @@ class Q_QUICK_PRIVATE_EXPORT QQuickTapHandler : public QQuickSinglePointHandler Q_PROPERTY(qreal timeHeld READ timeHeld NOTIFY timeHeldChanged) Q_PROPERTY(qreal longPressThreshold READ longPressThreshold WRITE setLongPressThreshold NOTIFY longPressThresholdChanged) Q_PROPERTY(GesturePolicy gesturePolicy READ gesturePolicy WRITE setGesturePolicy NOTIFY gesturePolicyChanged) + Q_PROPERTY(QQuickTapHandler::ExclusiveSignals exclusiveSignals READ exclusiveSignals WRITE setExclusiveSignals NOTIFY exclusiveSignalsChanged REVISION(6, 5)) QML_NAMED_ELEMENT(TapHandler) QML_ADDED_IN_VERSION(2, 12) @@ -43,6 +44,14 @@ public: }; Q_ENUM(GesturePolicy) + enum ExclusiveSignal { + NotExclusive = 0, + SingleTap = 1 << 1, + DoubleTap = 1 << 2 + }; + Q_DECLARE_FLAGS(ExclusiveSignals, ExclusiveSignal) + Q_FLAG(ExclusiveSignal) + explicit QQuickTapHandler(QQuickItem *parent = nullptr); bool isPressed() const { return m_pressed; } @@ -56,12 +65,16 @@ public: GesturePolicy gesturePolicy() const { return m_gesturePolicy; } void setGesturePolicy(GesturePolicy gesturePolicy); + QQuickTapHandler::ExclusiveSignals exclusiveSignals() const { return m_exclusiveSignals; } + void setExclusiveSignals(QQuickTapHandler::ExclusiveSignals newexclusiveSignals); + Q_SIGNALS: void pressedChanged(); void tapCountChanged(); void timeHeldChanged(); void longPressThresholdChanged(); void gesturePolicyChanged(); + Q_REVISION(6, 5) void exclusiveSignalsChanged(); // the second argument (Qt::MouseButton) was added in 6.2: avoid name clashes with IDs by not naming it for now void tapped(QEventPoint eventPoint, Qt::MouseButton /* button */); void singleTapped(QEventPoint eventPoint, Qt::MouseButton /* button */); @@ -86,9 +99,13 @@ private: qreal m_lastTapTimestamp = 0; QElapsedTimer m_holdTimer; QBasicTimer m_longPressTimer; + QBasicTimer m_doubleTapTimer; + QEventPoint m_singleTapReleasedPoint; + Qt::MouseButton m_singleTapReleasedButton; int m_tapCount = 0; int m_longPressThreshold = -1; GesturePolicy m_gesturePolicy = GesturePolicy::DragThreshold; + ExclusiveSignals m_exclusiveSignals = NotExclusive; bool m_pressed = false; static qreal m_multiTapInterval; diff --git a/tests/auto/quick/pointerhandlers/qquicktaphandler/data/Button.qml b/tests/auto/quick/pointerhandlers/qquicktaphandler/data/Button.qml index d06aedf2d7..fa58d76e4a 100644 --- a/tests/auto/quick/pointerhandlers/qquicktaphandler/data/Button.qml +++ b/tests/auto/quick/pointerhandlers/qquicktaphandler/data/Button.qml @@ -26,7 +26,10 @@ Rectangle { id: tap objectName: label.text longPressThreshold: 100 // CI can be insanely slow, so don't demand a timely release to generate onTapped + onSingleTapped: console.log("Single tap") + onDoubleTapped: console.log("Double tap") onTapped: { + console.log("Tapped") tapFlash.start() root.tappedPosition = point.scenePosition root.tapped() diff --git a/tests/auto/quick/pointerhandlers/qquicktaphandler/tst_qquicktaphandler.cpp b/tests/auto/quick/pointerhandlers/qquicktaphandler/tst_qquicktaphandler.cpp index 638ce43def..c53b583e59 100644 --- a/tests/auto/quick/pointerhandlers/qquicktaphandler/tst_qquicktaphandler.cpp +++ b/tests/auto/quick/pointerhandlers/qquicktaphandler/tst_qquicktaphandler.cpp @@ -42,7 +42,10 @@ private slots: void gesturePolicyDragWithinBounds_data(); void gesturePolicyDragWithinBounds(); void touchMultiTap(); + void mouseMultiTap_data(); void mouseMultiTap(); + void singleTapDoubleTap_data(); + void singleTapDoubleTap(); void touchLongPress(); void mouseLongPress(); void buttonsMultiTouch(); @@ -523,8 +526,32 @@ void tst_TapHandler::touchMultiTap() QCOMPARE(tappedSpy.size(), 4); } +void tst_TapHandler::mouseMultiTap_data() +{ + QTest::addColumn("exclusiveSignals"); + QTest::addColumn("expectedSingleTaps"); + QTest::addColumn("expectedSingleTapsAfterMovingAway"); + QTest::addColumn("expectedSingleTapsAfterWaiting"); + QTest::addColumn("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 windowPtr; createView(windowPtr, "buttons.qml"); @@ -532,38 +559,143 @@ void tst_TapHandler::mouseMultiTap() QQuickItem *button = window->rootObject()->findChild("DragThreshold"); QVERIFY(button); + QQuickTapHandler *tapHandler = button->findChild(); + QVERIFY(tapHandler); + tapHandler->setExclusiveSignals(exclusiveSignals); QSignalSpy tappedSpy(button, SIGNAL(tapped())); + QSignalSpy singleTapSpy(tapHandler, &QQuickTapHandler::singleTapped); + QSignalSpy doubleTapSpy(tapHandler, &QQuickTapHandler::doubleTapped); - // Tap once + // Click once QPoint p1 = button->mapToScene(QPointF(2, 2)).toPoint(); - QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1); + QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1, 10); QTRY_VERIFY(button->property("pressed").toBool()); - QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1); + 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); - // Tap again in exactly the same place (not likely with touch in the real world) - QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1); - QTRY_VERIFY(button->property("pressed").toBool()); - QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1); - QTRY_VERIFY(!button->property("pressed").toBool()); + // 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); - // Tap a third time, nearby - p1 += QPoint(dragThreshold, dragThreshold); - QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1); - QTRY_VERIFY(button->property("pressed").toBool()); - QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1); - QTRY_VERIFY(!button->property("pressed").toBool()); + // 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); - // Tap a fourth time, drifting farther away + // Click a fourth time, drifting farther away: treated as a separate click, regardless of timing p1 += QPoint(dragThreshold, dragThreshold); - QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1); - QTRY_VERIFY(button->property("pressed").toBool()); - QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1); - QTRY_VERIFY(!button->property("pressed").toBool()); + 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::singleTapDoubleTap_data() +{ + QTest::addColumn("deviceType"); + QTest::addColumn("exclusiveSignals"); + QTest::addColumn("expectedEndingSingleTapCount"); + QTest::addColumn("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 windowPtr; + createView(windowPtr, "buttons.qml"); + QQuickView * window = windowPtr.data(); + + QQuickItem *button = window->rootObject()->findChild("DragThreshold"); + QVERIFY(button); + QQuickTapHandler *tapHandler = button->findChild(); + 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(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()