diff options
Diffstat (limited to 'src/quick/handlers/qquicktaphandler.cpp')
-rw-r--r-- | src/quick/handlers/qquicktaphandler.cpp | 267 |
1 files changed, 180 insertions, 87 deletions
diff --git a/src/quick/handlers/qquicktaphandler.cpp b/src/quick/handlers/qquicktaphandler.cpp index 306eec65b8..accf307382 100644 --- a/src/quick/handlers/qquicktaphandler.cpp +++ b/src/quick/handlers/qquicktaphandler.cpp @@ -13,7 +13,7 @@ QT_BEGIN_NAMESPACE Q_LOGGING_CATEGORY(lcTapHandler, "qt.quick.handler.tap") -qreal QQuickTapHandler::m_multiTapInterval(0.0); +quint64 QQuickTapHandler::m_multiTapInterval(0); // single tap distance is the same as the drag threshold int QQuickTapHandler::m_mouseMultiClickDistanceSquared(-1); int QQuickTapHandler::m_touchMultiTapDistanceSquared(-1); @@ -50,14 +50,15 @@ int QQuickTapHandler::m_touchMultiTapDistanceSquared(-1); QStyleHints::touchDoubleTapDistance() with touch, and the time between taps must not exceed QStyleHints::mouseDoubleClickInterval(). - \sa MouseArea + \sa MouseArea, {Qt Quick Examples - Pointer Handlers} */ QQuickTapHandler::QQuickTapHandler(QQuickItem *parent) : QQuickSinglePointHandler(parent) + , m_longPressThreshold(QGuiApplication::styleHints()->mousePressAndHoldInterval()) { if (m_mouseMultiClickDistanceSquared < 0) { - m_multiTapInterval = qApp->styleHints()->mouseDoubleClickInterval() / 1000.0; + m_multiTapInterval = qApp->styleHints()->mouseDoubleClickInterval(); m_mouseMultiClickDistanceSquared = qApp->styleHints()->mouseDoubleClickDistance(); m_mouseMultiClickDistanceSquared *= m_mouseMultiClickDistanceSquared; m_touchMultiTapDistanceSquared = qApp->styleHints()->touchDoubleTapDistance(); @@ -78,6 +79,8 @@ bool QQuickTapHandler::wantsEventPoint(const QPointerEvent *event, const QEventP bool ret = false; bool overThreshold = d_func()->dragOverThreshold(point); if (overThreshold && m_gesturePolicy != DragWithinBounds) { + if (m_longPressTimer.isActive()) + qCDebug(lcTapHandler) << objectName() << "drag threshold exceeded"; m_longPressTimer.stop(); m_holdTimer.invalidate(); } @@ -87,16 +90,17 @@ bool QQuickTapHandler::wantsEventPoint(const QPointerEvent *event, const QEventP ret = parentContains(point); break; case QEventPoint::Updated: + ret = point.id() == this->point().id(); switch (m_gesturePolicy) { case DragThreshold: - ret = !overThreshold && parentContains(point); + ret = ret && !overThreshold && parentContains(point); break; case WithinBounds: case DragWithinBounds: - ret = parentContains(point); + ret = ret && parentContains(point); break; case ReleaseWithinBounds: - ret = point.id() == this->point().id(); + // no change to ret: depends only whether it's the already-tracking point ID break; } break; @@ -144,19 +148,28 @@ void QQuickTapHandler::handleEventPoint(QPointerEvent *event, QEventPoint &point /*! \qmlproperty real QtQuick::TapHandler::longPressThreshold - The time in seconds that an event point 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. + 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 + 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; @@ -165,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) @@ -175,6 +193,7 @@ void QQuickTapHandler::timerEvent(QTimerEvent *event) if (event->timerId() == m_longPressTimer.timerId()) { m_longPressTimer.stop(); qCDebug(lcTapHandler) << objectName() << "longPressed"; + m_longPressed = true; emit longPressed(); } else if (event->timerId() == m_doubleTapTimer.timerId()) { m_doubleTapTimer.stop(); @@ -199,49 +218,96 @@ void QQuickTapHandler::timerEvent(QTimerEvent *event) The \c gesturePolicy also affects grab behavior as described below. - \value TapHandler.DragThreshold - (the default value) The event point must not move significantly. - If the mouse, finger or stylus moves past the system-wide drag - threshold (QStyleHints::startDragDistance), the tap gesture is - canceled, even if the button or finger is still pressed. This policy - can be useful whenever TapHandler needs to cooperate with other - input handlers (for example \l DragHandler) or event-handling Items - (for example QtQuick Controls), because in this case TapHandler - will not take the exclusive grab, but merely a - \l {QPointerEvent::addPassiveGrabber()}{passive grab}. - - \value TapHandler.WithinBounds - If the event point leaves the bounds of the \c parent Item, the tap - gesture is canceled. The TapHandler will take the - \l {QPointerEvent::setExclusiveGrabber}{exclusive grab} on - press, but will release the grab as soon as the boundary constraint - is no longer satisfied. - - \value TapHandler.ReleaseWithinBounds - At the time of release (the mouse button is released or the finger - is lifted), if the event point is outside the bounds of the - \c parent Item, a tap gesture is not recognized. This corresponds to - typical behavior for button widgets: you can cancel a click by - dragging outside the button, and you can also change your mind by - dragging back inside the button before release. Note that it's - necessary for TapHandler to take the - \l {QPointerEvent::setExclusiveGrabber}{exclusive grab} on press - and retain it until release in order to detect this gesture. - - \value TapHandler.DragWithinBounds - On press, TapHandler takes the - \l {QPointerEvent::setExclusiveGrabber}{exclusive grab}; after that, - the event point can be dragged within the bounds of the \c parent - item, while the \l timeHeld property keeps counting, and the - \l longPressed() signal will be emitted regardless of drag distance. - However, like \c WithinBounds, if the point leaves the bounds, - the tap gesture is \l {PointerHandler::}{canceled()}, \l active() - becomes \c false, and \l timeHeld stops counting. This is suitable - for implementing press-drag-release components, such as menus, in - which a single TapHandler detects press, \c timeHeld drives an - "opening" animation, and then the user can drag to a menu item and - release, while never leaving the bounds of the parent scene containing - the menu. This value was added in Qt 6.3. + \table + \header + \li Constant + \li Description + \row + \li \c TapHandler.DragThreshold + \image pointerHandlers/tapHandlerOverlappingButtons.webp + Grab on press: \e passive + \li (the default value) The \l eventPoint must not move significantly. + If the mouse, finger or stylus moves past the system-wide drag + threshold (QStyleHints::startDragDistance), the tap gesture is + canceled, even if the device or finger is still pressed. This policy + can be useful whenever TapHandler needs to cooperate with other + input handlers (for example \l DragHandler) or event-handling Items + (for example \l {Qt Quick Controls}), because in this case TapHandler + will not take the exclusive grab, but merely a + \l {QPointerEvent::addPassiveGrabber()}{passive grab}. + That is, \c DragThreshold is especially useful to \e augment + existing behavior: it reacts to tap/click/long-press even when + another item or handler is already reacting, perhaps even in a + different layer of the UI. The following snippet shows one + TapHandler as used in one component; but if we stack up two + instances of the component, you will see the handlers in both of them + react simultaneously when a press occurs over both of them, because + the passive grab does not stop event propagation: + \quotefromfile pointerHandlers/tapHandlerOverlappingButtons.qml + \skipto Item + \printuntil component Button + \skipto TapHandler + \printuntil } + \skipuntil Text { + \skipuntil } + \printuntil Button + \printuntil Button + \printuntil } + + \row + \li \c TapHandler.WithinBounds + \image pointerHandlers/tapHandlerButtonWithinBounds.webp + Grab on press: \e exclusive + \li If the \l eventPoint leaves the bounds of the \c parent Item, the tap + gesture is canceled. The TapHandler will take the + \l {QPointerEvent::setExclusiveGrabber}{exclusive grab} on + press, but will release the grab as soon as the boundary constraint + is no longer satisfied. + \snippet pointerHandlers/tapHandlerButtonWithinBounds.qml 1 + + \row + \li \c TapHandler.ReleaseWithinBounds + \image pointerHandlers/tapHandlerButtonReleaseWithinBounds.webp + Grab on press: \e exclusive + \li At the time of release (the mouse button is released or the finger + is lifted), if the \l eventPoint is outside the bounds of the + \c parent Item, a tap gesture is not recognized. This corresponds to + typical behavior for button widgets: you can cancel a click by + dragging outside the button, and you can also change your mind by + dragging back inside the button before release. Note that it's + necessary for TapHandler to take the + \l {QPointerEvent::setExclusiveGrabber}{exclusive grab} on press + and retain it until release in order to detect this gesture. + \snippet pointerHandlers/tapHandlerButtonReleaseWithinBounds.qml 1 + + \row + \li \c TapHandler.DragWithinBounds + \image pointerHandlers/dragReleaseMenu.webp + Grab on press: \e exclusive + \li On press, TapHandler takes the + \l {QPointerEvent::setExclusiveGrabber}{exclusive grab}; after that, + the \l eventPoint can be dragged within the bounds of the \c parent + item, while the \l timeHeld property keeps counting, and the + \l longPressed() signal will be emitted regardless of drag distance. + However, like \c WithinBounds, if the point leaves the bounds, + the tap gesture is \l {PointerHandler::}{canceled()}, \l active() + becomes \c false, and \l timeHeld stops counting. This is suitable + for implementing press-drag-release components, such as menus, in + which a single TapHandler detects press, \c timeHeld drives an + "opening" animation, and then the user can drag to a menu item and + release, while never leaving the bounds of the parent scene containing + the menu. This value was added in Qt 6.3. + \snippet pointerHandlers/dragReleaseMenu.qml 1 + \endtable + + The \l {Qt Quick Examples - Pointer Handlers} demonstrates some use cases for these. + + \note If you find that TapHandler is reacting in cases that conflict with + some other behavior, the first thing you should try is to think about which + \c gesturePolicy is appropriate. If you cannot fix it by changing \c gesturePolicy, + some cases are better served by adjusting \l {PointerHandler::}{grabPermissions}, + either in this handler, or in another handler that should \e prevent TapHandler + from reacting. */ void QQuickTapHandler::setGesturePolicy(QQuickTapHandler::GesturePolicy gesturePolicy) { @@ -290,7 +356,7 @@ void QQuickTapHandler::setExclusiveSignals(QQuickTapHandler::ExclusiveSignals ex Holds true whenever the mouse or touch point is pressed, and any movement since the press is compliant with the current - \l gesturePolicy. When the event point is released or the policy is + \l gesturePolicy. When the \l eventPoint is released or the policy is violated, \e pressed will change to false. */ void QQuickTapHandler::setPressed(bool press, bool cancel, QPointerEvent *event, QEventPoint &point) @@ -302,21 +368,12 @@ 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(); m_holdTimer.invalidate(); - if (m_exclusiveSignals == (SingleTap | DoubleTap)) { - if (m_tapCount == 0) { - m_singleTapReleasedPoint = point; - m_singleTapReleasedButton = event->isSinglePointEvent() ? static_cast<QSinglePointEvent *>(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 @@ -326,33 +383,57 @@ void QQuickTapHandler::setPressed(bool press, bool cancel, QPointerEvent *event, setExclusiveGrab(event, point, press); } if (!cancel && !press && parentContains(point)) { - if (point.timeHeld() < longPressThreshold()) { + if (m_longPressed) { + qCDebug(lcTapHandler) << objectName() << "long press threshold" << longPressThreshold() << "exceeded:" << point.timeHeld(); + } else { // Assuming here that pointerEvent()->timestamp() is in ms. - const qreal ts = event->timestamp() / 1000.0; - const qreal interval = ts - m_lastTapTimestamp; + const quint64 ts = event->timestamp(); + const quint64 interval = ts - m_lastTapTimestamp; const auto distanceSquared = QVector2D(point.scenePosition() - m_lastTapPos).lengthSquared(); - if (interval < m_multiTapInterval && distanceSquared < + const auto singleTapReleasedButton = event->isSinglePointEvent() ? static_cast<QSinglePointEvent *>(event)->button() : Qt::NoButton; + if ((interval < m_multiTapInterval && distanceSquared < (event->device()->type() == QInputDevice::DeviceType::Mouse ? m_mouseMultiClickDistanceSquared : m_touchMultiTapDistanceSquared)) + && m_singleTapReleasedButton == singleTapReleasedButton) { ++m_tapCount; - else + } else { + m_singleTapReleasedButton = singleTapReleasedButton; + m_singleTapReleasedPoint = point; m_tapCount = 1; + } qCDebug(lcTapHandler) << objectName() << "tapped" << m_tapCount << "times; interval since last:" << interval << "sec; distance since last:" << qSqrt(distanceSquared); auto button = event->isSinglePointEvent() ? static_cast<QSinglePointEvent *>(event)->button() : Qt::NoButton; emit tapped(point, button); emit tapCountChanged(); - if (m_tapCount == 1 && !m_exclusiveSignals.testFlag(DoubleTap)) - emit singleTapped(point, button); - else if (m_tapCount == 2 && !m_exclusiveSignals.testFlag(SingleTap)) { - emit doubleTapped(point, button); + switch (m_exclusiveSignals) { + case NotExclusive: + if (m_tapCount == 1) + emit singleTapped(point, button); + else if (m_tapCount == 2) + emit doubleTapped(point, button); + break; + case SingleTap: + if (m_tapCount == 1) + emit singleTapped(point, button); + break; + case DoubleTap: + if (m_tapCount == 2) + emit doubleTapped(point, button); + break; + case (SingleTap | DoubleTap): + if (m_tapCount == 1) { + qCDebug(lcTapHandler) << objectName() << "waiting to emit singleTapped:" << m_multiTapInterval << "ms"; + m_doubleTapTimer.start(m_multiTapInterval, this); + } } + qCDebug(lcTapHandler) << objectName() << "tap" << m_tapCount << "after" << event->timestamp() - m_lastTapTimestamp << "ms"; + m_lastTapTimestamp = ts; m_lastTapPos = point.scenePosition(); - } else { - qCDebug(lcTapHandler) << objectName() << "tap threshold" << longPressThreshold() << "exceeded:" << point.timeHeld(); } } + m_longPressed = false; emit pressedChanged(); if (!press && m_gesturePolicy != DragThreshold) { // on release, ungrab after emitting changed signals @@ -380,13 +461,25 @@ void QQuickTapHandler::onGrabChanged(QQuickPointerHandler *grabber, QPointingDev void QQuickTapHandler::connectPreRenderSignal(bool conn) { + // disconnect pre-existing connection, if any + disconnect(m_preRenderSignalConnection); + auto par = parentItem(); - if (!par) + if (!par || !par->window()) return; - if (conn) - connect(par->window(), &QQuickWindow::beforeSynchronizing, this, &QQuickTapHandler::updateTimeHeld); - else - disconnect(par->window(), &QQuickWindow::beforeSynchronizing, this, &QQuickTapHandler::updateTimeHeld); + + /* + Note: beforeSynchronizing is emitted from the SG thread, and the + timeHeldChanged signal can be used to do arbitrary things in user QML. + + But the docs say the GUI thread is blockd, and "Therefore, it is safe + to access GUI thread thread data in a slot or lambda that is connected + with Qt::DirectConnection." We use the default AutoConnection just in case. + */ + if (conn) { + m_preRenderSignalConnection = connect(par->window(), &QQuickWindow::beforeSynchronizing, + this, &QQuickTapHandler::updateTimeHeld); + } } void QQuickTapHandler::updateTimeHeld() @@ -399,8 +492,8 @@ void QQuickTapHandler::updateTimeHeld() \readonly The number of taps which have occurred within the time and space - constraints to be considered a single gesture. For example, to detect - a triple-tap, you can write: + constraints to be considered a single gesture. The counter is reset to 1 + if the button changed. For example, to detect a triple-tap, you can write: \qml Rectangle { |