Disable TapHandler.longPressed signal if longPressThreshold == 0

There needs to be a way to disable the long-press feature, because it's
exclusive: if we emit longPressed(), we do not emit tapped(). But we
should also be able to accommodate slow users who pause for too long
unintentionally, or while simply observing the behavior.

Also clarify that resetting longPressThreshold reverts to the default.
Add more exhaustive test coverage, verify that longPressed
and tapped are mutually exclusive, and verify the effects of
violating the gesturePolicy.

Change longPressThreshold on LauncherList's back button so that it
always triggers, regardless whether the user pauses on it for a while.

[ChangeLog][QtQuick][Event Handlers] TapHandler.longPressThreshold
can now be set to 0 to disable its press-and-hold feature, and can be
reset to undefined to restore the platform default.

Fixes: QTBUG-119132
Task-number: QTBUG-105810
Pick-to: 6.5 6.6
Change-Id: Id5fd7e51c70fdb0cb6c4beb5615717a222aec871
Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
Reviewed-by: Richard Moe Gustavsen <richard.gustavsen@qt.io>
This commit is contained in:
Shawn Rutledge 2023-11-14 11:22:08 -07:00
parent 1b166c87d0
commit 8f6809681e
6 changed files with 213 additions and 64 deletions

View File

@ -182,6 +182,7 @@ Rectangle {
id: tapHandler
enabled: root.activePageCount > 0
gesturePolicy: TapHandler.ReleaseWithinBounds
longPressThreshold: 0
onTapped: {
pageContainer.children[pageContainer.children.length - 1].exit()
}

View File

@ -55,6 +55,7 @@ int QQuickTapHandler::m_touchMultiTapDistanceSquared(-1);
QQuickTapHandler::QQuickTapHandler(QQuickItem *parent)
: QQuickSinglePointHandler(parent)
, m_longPressThreshold(QGuiApplication::styleHints()->mousePressAndHoldInterval())
{
if (m_mouseMultiClickDistanceSquared < 0) {
m_multiTapInterval = qApp->styleHints()->mouseDoubleClickInterval();
@ -148,18 +149,27 @@ void QQuickTapHandler::handleEventPoint(QPointerEvent *event, QEventPoint &point
\qmlproperty real QtQuick::TapHandler::longPressThreshold
The time in seconds that an \l eventPoint must be pressed in order to
trigger a long press gesture and emit the \l longPressed() signal.
If the point is released before this time limit, a tap can be detected
if the \l gesturePolicy constraint is satisfied. The default value is
QStyleHints::mousePressAndHoldInterval() converted to seconds.
trigger a long press gesture and emit the \l longPressed() signal, if the
value is greater than \c 0. If the point is released before this time
limit, a tap can be detected if the \l gesturePolicy constraint is
satisfied. If \c longPressThreshold is \c 0, the timer is disabled and the
signal will not be emitted. If \c longPressThreshold is set to \c undefined,
the default value is used instead, and can be read back from this property.
The default value is QStyleHints::mousePressAndHoldInterval() converted to
seconds.
*/
qreal QQuickTapHandler::longPressThreshold() const
{
return longPressThresholdMilliseconds() / 1000.0;
return m_longPressThreshold / qreal(1000);
}
void QQuickTapHandler::setLongPressThreshold(qreal longPressThreshold)
{
if (longPressThreshold < 0) {
resetLongPressThreshold();
return;
}
int ms = qRound(longPressThreshold * 1000);
if (m_longPressThreshold == ms)
return;
@ -168,9 +178,14 @@ void QQuickTapHandler::setLongPressThreshold(qreal longPressThreshold)
emit longPressThresholdChanged();
}
int QQuickTapHandler::longPressThresholdMilliseconds() const
void QQuickTapHandler::resetLongPressThreshold()
{
return (m_longPressThreshold < 0 ? QGuiApplication::styleHints()->mousePressAndHoldInterval() : m_longPressThreshold);
int ms = QGuiApplication::styleHints()->mousePressAndHoldInterval();
if (m_longPressThreshold == ms)
return;
m_longPressThreshold = ms;
emit longPressThresholdChanged();
}
void QQuickTapHandler::timerEvent(QTimerEvent *event)
@ -353,7 +368,8 @@ void QQuickTapHandler::setPressed(bool press, bool cancel, QPointerEvent *event,
connectPreRenderSignal(press);
updateTimeHeld();
if (press) {
m_longPressTimer.start(longPressThresholdMilliseconds(), this);
if (m_longPressThreshold > 0)
m_longPressTimer.start(m_longPressThreshold, this);
m_holdTimer.start();
} else {
m_longPressTimer.stop();

View File

@ -30,7 +30,7 @@ class Q_QUICK_PRIVATE_EXPORT QQuickTapHandler : public QQuickSinglePointHandler
Q_PROPERTY(bool pressed READ isPressed NOTIFY pressedChanged FINAL)
Q_PROPERTY(int tapCount READ tapCount NOTIFY tapCountChanged FINAL)
Q_PROPERTY(qreal timeHeld READ timeHeld NOTIFY timeHeldChanged FINAL)
Q_PROPERTY(qreal longPressThreshold READ longPressThreshold WRITE setLongPressThreshold NOTIFY longPressThresholdChanged FINAL)
Q_PROPERTY(qreal longPressThreshold READ longPressThreshold WRITE setLongPressThreshold NOTIFY longPressThresholdChanged RESET resetLongPressThreshold FINAL)
Q_PROPERTY(GesturePolicy gesturePolicy READ gesturePolicy WRITE setGesturePolicy NOTIFY gesturePolicyChanged FINAL)
Q_PROPERTY(QQuickTapHandler::ExclusiveSignals exclusiveSignals READ exclusiveSignals WRITE setExclusiveSignals NOTIFY exclusiveSignalsChanged REVISION(6, 5) FINAL)
@ -63,6 +63,7 @@ public:
qreal longPressThreshold() const;
void setLongPressThreshold(qreal longPressThreshold);
void resetLongPressThreshold();
GesturePolicy gesturePolicy() const { return m_gesturePolicy; }
void setGesturePolicy(GesturePolicy gesturePolicy);
@ -92,7 +93,6 @@ protected:
private:
void setPressed(bool press, bool cancel, QPointerEvent *event, QEventPoint &point);
int longPressThresholdMilliseconds() const;
void connectPreRenderSignal(bool conn = true);
void updateTimeHeld();

View File

@ -10,13 +10,20 @@ Rectangle {
property alias pressed: tap.pressed
property bool checked: false
property alias gesturePolicy: tap.gesturePolicy
property alias longPressThreshold: tap.longPressThreshold
property point tappedPosition: Qt.point(0, 0)
property real timeHeldWhenTapped: 0
property real timeHeldWhenLongPressed: 0
signal tapped
signal canceled
width: label.implicitWidth * 1.5; height: label.implicitHeight * 2.0
border.color: "#9f9d9a"; border.width: 1; radius: height / 4; antialiasing: true
function assignUndefinedLongPressThreshold() {
tap.longPressThreshold = undefined
}
gradient: Gradient {
GradientStop { position: 0.0; color: tap.pressed ? "#b8b5b2" : "#efebe7" }
GradientStop { position: 1.0; color: "#b8b5b2" }
@ -25,14 +32,17 @@ Rectangle {
TapHandler {
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")
onTapped: (eventPoint, button) => {
console.log("Tapped", button, eventPoint)
tapFlash.start()
root.tappedPosition = point.scenePosition
root.tapped()
root.timeHeldWhenTapped = tap.timeHeld // eventPoint.timeHeld is already 0
}
onLongPressed: {
root.timeHeldWhenLongPressed = tap.timeHeld
}
onCanceled: root.canceled()
}

View File

@ -9,19 +9,25 @@ Item {
Button {
objectName: "DragThreshold"
label: "DragThreshold"
x: 10; y: 10; width: parent.width - 20; height: 40
x: 10; y: 10; width: 300; height: 40
gesturePolicy: TapHandler.DragThreshold
}
Button {
objectName: "WithinBounds"
label: "WithinBounds"
x: 10; y: 60; width: parent.width - 20; height: 40
x: 10; y: 60; width: 300; height: 40
gesturePolicy: TapHandler.WithinBounds
}
Button {
objectName: "ReleaseWithinBounds"
label: "ReleaseWithinBounds"
x: 10; y: 110; width: parent.width - 20; height: 40
x: 10; y: 110; width: 300; height: 40
gesturePolicy: TapHandler.ReleaseWithinBounds
}
Button {
objectName: "DragWithinBounds"
label: "DragWithinBounds"
x: 10; y: 160; width: 300; height: 40
gesturePolicy: TapHandler.DragWithinBounds
}
}

View File

@ -48,8 +48,8 @@ private slots:
void mouseMultiTapLeftRight();
void singleTapDoubleTap_data();
void singleTapDoubleTap();
void touchLongPress();
void mouseLongPress();
void longPress_data();
void longPress();
void buttonsMultiTouch();
void componentUserBehavioralOverride();
void rightLongPressIgnoreWheel();
@ -778,76 +778,192 @@ void tst_TapHandler::singleTapDoubleTap()
QCOMPARE(singleTapSpy.size(), expectedEndingSingleTapCount);
}
void tst_TapHandler::touchLongPress()
void tst_TapHandler::longPress_data()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QTest::addColumn<const QPointingDevice *>("device");
QTest::addColumn<QString>("buttonName");
QTest::addColumn<qreal>("longPressThreshold");
QTest::addColumn<QPoint>("releaseOffset");
QTest::addColumn<bool>("expectLongPress");
QTest::addColumn<bool>("expectTapped");
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()));
const QPointingDevice *constTouchDevice = touchDevice;
// Reduce the threshold so that we can get a long press quickly
tapHandler->setLongPressThreshold(0.5);
QCOMPARE(longPressThresholdChangedSpy.size(), 1);
// Reduce the threshold so that we can get a long press quickly (faster in CI)
const qreal longPressThreshold = 0.3;
QTest::newRow("mouse, lpt longPressThreshold: DragThreshold")
<< QPointingDevice::primaryPointingDevice() << "DragThreshold"
<< longPressThreshold << QPoint(0, 0) << true << false;
QTest::newRow("touch, lpt longPressThreshold: DragThreshold")
<< constTouchDevice << "DragThreshold" << longPressThreshold
<< QPoint(0, 0) << true << false;
QTest::newRow("mouse, lpt longPressThreshold: DragThreshold, drag")
<< QPointingDevice::primaryPointingDevice() << "DragThreshold"
<< longPressThreshold << QPoint(50, 0) << false << false;
QTest::newRow("touch, lpt longPressThreshold: DragThreshold, drag")
<< constTouchDevice << "DragThreshold"
<< longPressThreshold << QPoint(50, 0) << false << false;
// 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
QTest::newRow("mouse, lpt longPressThreshold: WithinBounds")
<< QPointingDevice::primaryPointingDevice() << "WithinBounds"
<< longPressThreshold << QPoint(0, 0) << true << false;
QTest::newRow("touch, lpt longPressThreshold: WithinBounds")
<< constTouchDevice << "WithinBounds"
<< longPressThreshold << QPoint(0, 0) << true << false;
QTest::newRow("mouse, lpt longPressThreshold: WithinBounds, drag")
<< QPointingDevice::primaryPointingDevice() << "WithinBounds"
<< longPressThreshold << QPoint(50, 0) << true << false;
QTest::newRow("touch, lpt longPressThreshold: WithinBounds, drag")
<< constTouchDevice << "WithinBounds"
<< longPressThreshold << QPoint(50, 0) << true << false;
QTest::newRow("mouse, lpt longPressThreshold: WithinBounds, drag out")
<< QPointingDevice::primaryPointingDevice() << "WithinBounds"
<< longPressThreshold << QPoint(155, 0) << false << false;
QTest::newRow("touch, lpt longPressThreshold: WithinBounds, drag out")
<< constTouchDevice << "WithinBounds"
<< longPressThreshold << QPoint(155, 0) << false << false;
// 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);
QTest::newRow("mouse, lpt longPressThreshold: ReleaseWithinBounds")
<< QPointingDevice::primaryPointingDevice() << "ReleaseWithinBounds"
<< longPressThreshold << QPoint(0, 0) << true << false;
QTest::newRow("touch, lpt longPressThreshold: ReleaseWithinBounds")
<< constTouchDevice << "ReleaseWithinBounds"
<< longPressThreshold << QPoint(0, 0) << true << false;
QTest::newRow("mouse, lpt longPressThreshold: ReleaseWithinBounds, drag")
<< QPointingDevice::primaryPointingDevice() << "ReleaseWithinBounds"
<< longPressThreshold << QPoint(50, 0) << true << false;
QTest::newRow("touch, lpt longPressThreshold: ReleaseWithinBounds, drag")
<< constTouchDevice << "ReleaseWithinBounds"
<< longPressThreshold << QPoint(50, 0) << true << false;
QTest::newRow("mouse, lpt longPressThreshold: ReleaseWithinBounds, drag out")
<< QPointingDevice::primaryPointingDevice() << "ReleaseWithinBounds"
<< longPressThreshold << QPoint(155, 0) << false << false;
QTest::newRow("touch, lpt longPressThreshold: ReleaseWithinBounds, drag out")
<< constTouchDevice << "ReleaseWithinBounds"
<< longPressThreshold << QPoint(155, 0) << false << false;
QTest::newRow("mouse, lpt longPressThreshold: DragWithinBounds")
<< QPointingDevice::primaryPointingDevice() << "DragWithinBounds"
<< longPressThreshold << QPoint(0, 0) << true << false;
QTest::newRow("touch, lpt longPressThreshold: DragWithinBounds")
<< constTouchDevice << "DragWithinBounds"
<< longPressThreshold << QPoint(0, 0) << true << false;
QTest::newRow("mouse, lpt longPressThreshold: DragWithinBounds, drag")
<< QPointingDevice::primaryPointingDevice() << "DragWithinBounds"
<< longPressThreshold << QPoint(50, 0) << true << false;
QTest::newRow("touch, lpt longPressThreshold: DragWithinBounds, drag")
<< constTouchDevice << "DragWithinBounds"
<< longPressThreshold << QPoint(50, 0) << true << false;
QTest::newRow("mouse, lpt longPressThreshold: DragWithinBounds, drag out")
<< QPointingDevice::primaryPointingDevice() << "DragWithinBounds"
<< longPressThreshold << QPoint(155, 0) << false << false;
QTest::newRow("touch, lpt longPressThreshold: DragWithinBounds, drag out")
<< constTouchDevice << "DragWithinBounds"
<< longPressThreshold << QPoint(155, 0) << false << false;
// Zero or negative threshold means long press is disabled
QTest::newRow("mouse, lpt 0: DragThreshold")
<< QPointingDevice::primaryPointingDevice() << "DragThreshold"
<< qreal(0) << QPoint(0, 0) << false << true;
QTest::newRow("mouse, lpt -1: DragThreshold")
<< QPointingDevice::primaryPointingDevice() << "DragThreshold"
<< qreal(-1) << QPoint(0, 0) << true << false;
QTest::newRow("touch, lpt 0: DragThreshold")
<< constTouchDevice << "DragThreshold"
<< qreal(0) << QPoint(0, 0) << false << true;
QTest::newRow("mouse, lpt 0: WithinBounds")
<< QPointingDevice::primaryPointingDevice() << "WithinBounds"
<< qreal(0) << QPoint(0, 0) << false << true;
QTest::newRow("mouse, lpt -1: WithinBounds")
<< QPointingDevice::primaryPointingDevice() << "WithinBounds"
<< qreal(-1) << QPoint(0, 0) << true << false;
QTest::newRow("touch, lpt 0: WithinBounds")
<< constTouchDevice << "WithinBounds"
<< qreal(0) << QPoint(0, 0) << false << true;
QTest::newRow("mouse, lpt 0: ReleaseWithinBounds")
<< QPointingDevice::primaryPointingDevice() << "ReleaseWithinBounds"
<< qreal(0) << QPoint(0, 0) << false << true;
QTest::newRow("mouse, lpt -1: ReleaseWithinBounds")
<< QPointingDevice::primaryPointingDevice() << "ReleaseWithinBounds"
<< qreal(-1) << QPoint(0, 0) << true << false;
QTest::newRow("touch, lpt 0: ReleaseWithinBounds")
<< constTouchDevice << "ReleaseWithinBounds"
<< qreal(0) << QPoint(0, 0) << false << true;
QTest::newRow("mouse, lpt 0: DragWithinBounds")
<< QPointingDevice::primaryPointingDevice() << "DragWithinBounds"
<< qreal(0) << QPoint(0, 0) << false << true;
QTest::newRow("mouse, lpt -1: DragWithinBounds")
<< QPointingDevice::primaryPointingDevice() << "DragWithinBounds"
<< qreal(-1) << QPoint(0, 0) << true << false;
QTest::newRow("touch, lpt 0: DragWithinBounds")
<< constTouchDevice << "DragWithinBounds"
<< qreal(0) << QPoint(0, 0) << false << true;
}
void tst_TapHandler::mouseLongPress()
void tst_TapHandler::longPress()
{
QScopedPointer<QQuickView> windowPtr;
createView(windowPtr, "buttons.qml");
QQuickView * window = windowPtr.data();
QFETCH(const QPointingDevice *, device);
QFETCH(QString, buttonName);
QFETCH(qreal, longPressThreshold);
QFETCH(QPoint, releaseOffset);
QFETCH(bool, expectLongPress);
QFETCH(bool, expectTapped);
QQuickItem *button = window->rootObject()->findChild<QQuickItem*>("DragThreshold");
QQuickView window;
QVERIFY(QQuickTest::showView(window, testFileUrl("buttons.qml")));
QQuickItem *button = window.rootObject()->findChild<QQuickItem*>(buttonName);
QVERIFY(button);
QQuickTapHandler *tapHandler = button->findChild<QQuickTapHandler*>("DragThreshold");
QQuickTapHandler *tapHandler = button->findChild<QQuickTapHandler*>(buttonName);
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);
const qreal defaultThreshold = tapHandler->longPressThreshold();
qsizetype changedCount = 0;
QCOMPARE_GT(defaultThreshold, 0);
tapHandler->setLongPressThreshold(longPressThreshold);
if (longPressThreshold > 0)
QCOMPARE(longPressThresholdChangedSpy.size(), ++changedCount);
QVERIFY(QMetaObject::invokeMethod(button, "assignUndefinedLongPressThreshold"));
if (longPressThreshold > 0)
QCOMPARE(longPressThresholdChangedSpy.size(), ++changedCount);
QCOMPARE(tapHandler->longPressThreshold(), defaultThreshold);
tapHandler->setLongPressThreshold(longPressThreshold);
// Press and hold
QPoint p1 = button->mapToScene(button->clipRect().center()).toPoint();
QTest::mousePress(window, Qt::LeftButton, Qt::NoModifier, p1);
QQuickTest::pointerPress(device, &window, 1, p1);
QTRY_VERIFY(button->property("pressed").toBool());
QTRY_COMPARE(longPressedSpy.size(), 1);
QTRY_COMPARE(longPressedSpy.size(), expectLongPress ? 1 : 0);
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
QCOMPARE_GT(timeHeldSpy.size(), 0);
if (expectLongPress) {
// Should be > longPressThreshold but slow CI and timer granularity can interfere
QCOMPARE_GT(tapHandler->timeHeld(), longPressThreshold - 0.1);
} else {
// Should be quite small, but event delivery is not instantaneous
QCOMPARE_LT(tapHandler->timeHeld(), 0.3);
}
// Release and verify that tapped was not emitted
QTest::mouseRelease(window, Qt::LeftButton, Qt::NoModifier, p1, 500);
// If we have an offset, we need a move between press and release for realistic simulation
if (!releaseOffset.isNull())
QQuickTest::pointerMove(device, &window, 1, p1 + releaseOffset);
// Release (optionally at an offset) and check whether tapped was emitted
QQuickTest::pointerRelease(device, &window, 1, p1 + releaseOffset);
QTRY_VERIFY(!button->property("pressed").toBool());
QCOMPARE(tappedSpy.size(), 0);
if (expectLongPress)
QCOMPARE_GT(button->property("timeHeldWhenLongPressed").toReal(), longPressThreshold - 0.1);
QCOMPARE(tapHandler->timeHeld(), -1);
QCOMPARE(tappedSpy.size(), expectTapped ? 1 : 0);
QCOMPARE(longPressedSpy.size(), expectLongPress ? 1 : 0);
}
void tst_TapHandler::buttonsMultiTouch()