aboutsummaryrefslogtreecommitdiffstats
path: root/src/quick/handlers/qquicktaphandler.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/quick/handlers/qquicktaphandler.cpp')
-rw-r--r--src/quick/handlers/qquicktaphandler.cpp267
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 {