1// Copyright (C) 2019 The Qt Company Ltd.
2// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
3
4#include "qquicktaphandler_p.h"
5#include "qquicksinglepointhandler_p_p.h"
6#include <QtQuick/private/qquickdeliveryagent_p_p.h>
7#include <QtQuick/qquickwindow.h>
8#include <qpa/qplatformtheme.h>
9#include <private/qguiapplication_p.h>
10#include <QtGui/qstylehints.h>
11
12QT_BEGIN_NAMESPACE
13
14Q_LOGGING_CATEGORY(lcTapHandler, "qt.quick.handler.tap")
15
16quint64 QQuickTapHandler::m_multiTapInterval(0);
17// single tap distance is the same as the drag threshold
18int QQuickTapHandler::m_mouseMultiClickDistanceSquared(-1);
19int QQuickTapHandler::m_touchMultiTapDistanceSquared(-1);
20
21/*!
22 \qmltype TapHandler
23 \instantiates QQuickTapHandler
24 \inherits SinglePointHandler
25 \inqmlmodule QtQuick
26 \ingroup qtquick-input-handlers
27 \brief Handler for taps and clicks.
28
29 TapHandler is a handler for taps on a touchscreen or clicks on a mouse.
30
31 Detection of a valid tap gesture depends on \l gesturePolicy. The default
32 value is DragThreshold, which requires the press and release to be close
33 together in both space and time. In this case, DragHandler is able to
34 function using only a passive grab, and therefore does not interfere with
35 event delivery to any other Items or Input Handlers. So the default
36 gesturePolicy is useful when you want to modify behavior of an existing
37 control or Item by adding a TapHandler with bindings and/or JavaScript
38 callbacks.
39
40 Note that buttons (such as QPushButton) are often implemented not to care
41 whether the press and release occur close together: if you press the button
42 and then change your mind, you need to drag all the way off the edge of the
43 button in order to cancel the click. For this use case, set the
44 \l gesturePolicy to \c TapHandler.ReleaseWithinBounds.
45
46 \snippet pointerHandlers/tapHandlerButton.qml 0
47
48 For multi-tap gestures (double-tap, triple-tap etc.), the distance moved
49 must not exceed QStyleHints::mouseDoubleClickDistance() with mouse and
50 QStyleHints::touchDoubleTapDistance() with touch, and the time between
51 taps must not exceed QStyleHints::mouseDoubleClickInterval().
52
53 \sa MouseArea, {Qt Quick Examples - Pointer Handlers}
54*/
55
56QQuickTapHandler::QQuickTapHandler(QQuickItem *parent)
57 : QQuickSinglePointHandler(parent)
58{
59 if (m_mouseMultiClickDistanceSquared < 0) {
60 m_multiTapInterval = qApp->styleHints()->mouseDoubleClickInterval();
61 m_mouseMultiClickDistanceSquared = qApp->styleHints()->mouseDoubleClickDistance();
62 m_mouseMultiClickDistanceSquared *= m_mouseMultiClickDistanceSquared;
63 m_touchMultiTapDistanceSquared = qApp->styleHints()->touchDoubleTapDistance();
64 m_touchMultiTapDistanceSquared *= m_touchMultiTapDistanceSquared;
65 }
66}
67
68bool QQuickTapHandler::wantsEventPoint(const QPointerEvent *event, const QEventPoint &point)
69{
70 if (!QQuickDeliveryAgentPrivate::isMouseEvent(ev: event) &&
71 !QQuickDeliveryAgentPrivate::isTouchEvent(ev: event) &&
72 !QQuickDeliveryAgentPrivate::isTabletEvent(ev: event))
73 return false;
74 // If the user has not violated any constraint, it could be a tap.
75 // Otherwise we want to give up the grab so that a competing handler
76 // (e.g. DragHandler) gets a chance to take over.
77 // Don't forget to emit released in case of a cancel.
78 bool ret = false;
79 bool overThreshold = d_func()->dragOverThreshold(point);
80 if (overThreshold && m_gesturePolicy != DragWithinBounds) {
81 m_longPressTimer.stop();
82 m_holdTimer.invalidate();
83 }
84 switch (point.state()) {
85 case QEventPoint::Pressed:
86 case QEventPoint::Released:
87 ret = parentContains(point);
88 break;
89 case QEventPoint::Updated:
90 ret = point.id() == this->point().id();
91 switch (m_gesturePolicy) {
92 case DragThreshold:
93 ret = ret && !overThreshold && parentContains(point);
94 break;
95 case WithinBounds:
96 case DragWithinBounds:
97 ret = ret && parentContains(point);
98 break;
99 case ReleaseWithinBounds:
100 // no change to ret: depends only whether it's the already-tracking point ID
101 break;
102 }
103 break;
104 case QEventPoint::Stationary:
105 // If the point hasn't moved since last time, the return value should be the same as last time.
106 // If we return false here, QQuickPointerHandler::handlePointerEvent() will call setActive(false).
107 ret = point.id() == this->point().id();
108 break;
109 case QEventPoint::Unknown:
110 break;
111 }
112 // If this is the grabber, returning false from this function will cancel the grab,
113 // so onGrabChanged(this, CancelGrabExclusive, point) and setPressed(false) will be called.
114 // But when m_gesturePolicy is DragThreshold, we don't get an exclusive grab, but
115 // we still don't want to be pressed anymore.
116 if (!ret && point.id() == this->point().id())
117 setPressed(press: false, cancel: true, event: const_cast<QPointerEvent *>(event), point&: const_cast<QEventPoint &>(point));
118 return ret;
119}
120
121void QQuickTapHandler::handleEventPoint(QPointerEvent *event, QEventPoint &point)
122{
123 const bool isTouch = QQuickDeliveryAgentPrivate::isTouchEvent(ev: event);
124 switch (point.state()) {
125 case QEventPoint::Pressed:
126 setPressed(press: true, cancel: false, event, point);
127 break;
128 case QEventPoint::Released: {
129 if (isTouch || (static_cast<const QSinglePointEvent *>(event)->buttons() & acceptedButtons()) == Qt::NoButton)
130 setPressed(press: false, cancel: false, event, point);
131 break;
132 }
133 default:
134 break;
135 }
136
137 QQuickSinglePointHandler::handleEventPoint(event, point);
138
139 // If TapHandler only needs a passive grab, it should not block other items and handlers from reacting.
140 // If the point is accepted, QQuickItemPrivate::localizedTouchEvent() would skip it.
141 if (isTouch && m_gesturePolicy == DragThreshold)
142 point.setAccepted(false);
143}
144
145/*!
146 \qmlproperty real QtQuick::TapHandler::longPressThreshold
147
148 The time in seconds that an \l eventPoint must be pressed in order to
149 trigger a long press gesture and emit the \l longPressed() signal.
150 If the point is released before this time limit, a tap can be detected
151 if the \l gesturePolicy constraint is satisfied. The default value is
152 QStyleHints::mousePressAndHoldInterval() converted to seconds.
153*/
154qreal QQuickTapHandler::longPressThreshold() const
155{
156 return longPressThresholdMilliseconds() / 1000.0;
157}
158
159void QQuickTapHandler::setLongPressThreshold(qreal longPressThreshold)
160{
161 int ms = qRound(d: longPressThreshold * 1000);
162 if (m_longPressThreshold == ms)
163 return;
164
165 m_longPressThreshold = ms;
166 emit longPressThresholdChanged();
167}
168
169int QQuickTapHandler::longPressThresholdMilliseconds() const
170{
171 return (m_longPressThreshold < 0 ? QGuiApplication::styleHints()->mousePressAndHoldInterval() : m_longPressThreshold);
172}
173
174void QQuickTapHandler::timerEvent(QTimerEvent *event)
175{
176 if (event->timerId() == m_longPressTimer.timerId()) {
177 m_longPressTimer.stop();
178 qCDebug(lcTapHandler) << objectName() << "longPressed";
179 emit longPressed();
180 } else if (event->timerId() == m_doubleTapTimer.timerId()) {
181 m_doubleTapTimer.stop();
182 qCDebug(lcTapHandler) << objectName() << "double-tap timer expired; taps:" << m_tapCount;
183 Q_ASSERT(m_exclusiveSignals == (SingleTap | DoubleTap));
184 if (m_tapCount == 1)
185 emit singleTapped(eventPoint: m_singleTapReleasedPoint, m_singleTapReleasedButton);
186 else if (m_tapCount == 2)
187 emit doubleTapped(eventPoint: m_singleTapReleasedPoint, m_singleTapReleasedButton);
188 }
189}
190
191/*!
192 \qmlproperty enumeration QtQuick::TapHandler::gesturePolicy
193
194 The spatial constraint for a tap or long press gesture to be recognized,
195 in addition to the constraint that the release must occur before
196 \l longPressThreshold has elapsed. If these constraints are not satisfied,
197 the \l tapped signal is not emitted, and \l tapCount is not incremented.
198 If the spatial constraint is violated, \l pressed transitions immediately
199 from true to false, regardless of the time held.
200
201 The \c gesturePolicy also affects grab behavior as described below.
202
203 \table
204 \header
205 \li Constant
206 \li Description
207 \row
208 \li \c TapHandler.DragThreshold
209 \image pointerHandlers/tapHandlerOverlappingButtons.webp
210 Grab on press: \e passive
211 \li (the default value) The \l eventPoint must not move significantly.
212 If the mouse, finger or stylus moves past the system-wide drag
213 threshold (QStyleHints::startDragDistance), the tap gesture is
214 canceled, even if the device or finger is still pressed. This policy
215 can be useful whenever TapHandler needs to cooperate with other
216 input handlers (for example \l DragHandler) or event-handling Items
217 (for example \l {Qt Quick Controls}), because in this case TapHandler
218 will not take the exclusive grab, but merely a
219 \l {QPointerEvent::addPassiveGrabber()}{passive grab}.
220 That is, \c DragThreshold is especially useful to \e augment
221 existing behavior: it reacts to tap/click/long-press even when
222 another item or handler is already reacting, perhaps even in a
223 different layer of the UI. The following snippet shows one
224 TapHandler as used in one component; but if we stack up two
225 instances of the component, you will see the handlers in both of them
226 react simultaneously when a press occurs over both of them, because
227 the passive grab does not stop event propagation:
228 \quotefromfile pointerHandlers/tapHandlerOverlappingButtons.qml
229 \skipto Item
230 \printuntil component Button
231 \skipto TapHandler
232 \printuntil }
233 \skipuntil Text {
234 \skipuntil }
235 \printuntil Button
236 \printuntil Button
237 \printuntil }
238
239 \row
240 \li \c TapHandler.WithinBounds
241 \image pointerHandlers/tapHandlerButtonWithinBounds.webp
242 Grab on press: \e exclusive
243 \li If the \l eventPoint leaves the bounds of the \c parent Item, the tap
244 gesture is canceled. The TapHandler will take the
245 \l {QPointerEvent::setExclusiveGrabber}{exclusive grab} on
246 press, but will release the grab as soon as the boundary constraint
247 is no longer satisfied.
248 \snippet pointerHandlers/tapHandlerButtonWithinBounds.qml 1
249
250 \row
251 \li \c TapHandler.ReleaseWithinBounds
252 \image pointerHandlers/tapHandlerButtonReleaseWithinBounds.webp
253 Grab on press: \e exclusive
254 \li At the time of release (the mouse button is released or the finger
255 is lifted), if the \l eventPoint is outside the bounds of the
256 \c parent Item, a tap gesture is not recognized. This corresponds to
257 typical behavior for button widgets: you can cancel a click by
258 dragging outside the button, and you can also change your mind by
259 dragging back inside the button before release. Note that it's
260 necessary for TapHandler to take the
261 \l {QPointerEvent::setExclusiveGrabber}{exclusive grab} on press
262 and retain it until release in order to detect this gesture.
263 \snippet pointerHandlers/tapHandlerButtonReleaseWithinBounds.qml 1
264
265 \row
266 \li \c TapHandler.DragWithinBounds
267 \image pointerHandlers/dragReleaseMenu.webp
268 Grab on press: \e exclusive
269 \li On press, TapHandler takes the
270 \l {QPointerEvent::setExclusiveGrabber}{exclusive grab}; after that,
271 the \l eventPoint can be dragged within the bounds of the \c parent
272 item, while the \l timeHeld property keeps counting, and the
273 \l longPressed() signal will be emitted regardless of drag distance.
274 However, like \c WithinBounds, if the point leaves the bounds,
275 the tap gesture is \l {PointerHandler::}{canceled()}, \l active()
276 becomes \c false, and \l timeHeld stops counting. This is suitable
277 for implementing press-drag-release components, such as menus, in
278 which a single TapHandler detects press, \c timeHeld drives an
279 "opening" animation, and then the user can drag to a menu item and
280 release, while never leaving the bounds of the parent scene containing
281 the menu. This value was added in Qt 6.3.
282 \snippet pointerHandlers/dragReleaseMenu.qml 1
283 \endtable
284
285 The \l {Qt Quick Examples - Pointer Handlers} demonstrates some use cases for these.
286
287 \note If you find that TapHandler is reacting in cases that conflict with
288 some other behavior, the first thing you should try is to think about which
289 \c gesturePolicy is appropriate. If you cannot fix it by changing \c gesturePolicy,
290 some cases are better served by adjusting \l {PointerHandler::}{grabPermissions},
291 either in this handler, or in another handler that should \e prevent TapHandler
292 from reacting.
293*/
294void QQuickTapHandler::setGesturePolicy(QQuickTapHandler::GesturePolicy gesturePolicy)
295{
296 if (m_gesturePolicy == gesturePolicy)
297 return;
298
299 m_gesturePolicy = gesturePolicy;
300 emit gesturePolicyChanged();
301}
302
303/*!
304 \qmlproperty enumeration QtQuick::TapHandler::exclusiveSignals
305 \since 6.5
306
307 Determines the exclusivity of the singleTapped() and doubleTapped() signals.
308
309 \value NotExclusive (the default) singleTapped() and doubleTapped() are
310 emitted immediately when the user taps once or twice, respectively.
311
312 \value SingleTap singleTapped() is emitted immediately when the user taps
313 once, and doubleTapped() is never emitted.
314
315 \value DoubleTap doubleTapped() is emitted immediately when the user taps
316 twice, and singleTapped() is never emitted.
317
318 \value (SingleTap | DoubleTap) Both signals are delayed until
319 QStyleHints::mouseDoubleClickInterval(), such that either singleTapped()
320 or doubleTapped() can be emitted, but not both. But if 3 or more taps
321 occur within \c mouseDoubleClickInterval, neither signal is emitted.
322
323 \note The remaining signals such as tapped() and tapCountChanged() are
324 always emitted immediately, regardless of this property.
325*/
326void QQuickTapHandler::setExclusiveSignals(QQuickTapHandler::ExclusiveSignals exc)
327{
328 if (m_exclusiveSignals == exc)
329 return;
330
331 m_exclusiveSignals = exc;
332 emit exclusiveSignalsChanged();
333}
334
335/*!
336 \qmlproperty bool QtQuick::TapHandler::pressed
337 \readonly
338
339 Holds true whenever the mouse or touch point is pressed,
340 and any movement since the press is compliant with the current
341 \l gesturePolicy. When the \l eventPoint is released or the policy is
342 violated, \e pressed will change to false.
343*/
344void QQuickTapHandler::setPressed(bool press, bool cancel, QPointerEvent *event, QEventPoint &point)
345{
346 if (m_pressed != press) {
347 qCDebug(lcTapHandler) << objectName() << "pressed" << m_pressed << "->" << press
348 << (cancel ? "CANCEL" : "") << point << "gp" << m_gesturePolicy;
349 m_pressed = press;
350 connectPreRenderSignal(conn: press);
351 updateTimeHeld();
352 if (press) {
353 m_longPressTimer.start(msec: longPressThresholdMilliseconds(), obj: this);
354 m_holdTimer.start();
355 } else {
356 m_longPressTimer.stop();
357 m_holdTimer.invalidate();
358 }
359 if (press) {
360 // on press, grab before emitting changed signals
361 if (m_gesturePolicy == DragThreshold)
362 setPassiveGrab(event, point, grab: press);
363 else
364 setExclusiveGrab(ev: event, point, grab: press);
365 }
366 if (!cancel && !press && parentContains(point)) {
367 if (point.timeHeld() < longPressThreshold()) {
368 // Assuming here that pointerEvent()->timestamp() is in ms.
369 const quint64 ts = event->timestamp();
370 const quint64 interval = ts - m_lastTapTimestamp;
371 const auto distanceSquared = QVector2D(point.scenePosition() - m_lastTapPos).lengthSquared();
372 const auto singleTapReleasedButton = event->isSinglePointEvent() ? static_cast<QSinglePointEvent *>(event)->button() : Qt::NoButton;
373 if ((interval < m_multiTapInterval && distanceSquared <
374 (event->device()->type() == QInputDevice::DeviceType::Mouse ?
375 m_mouseMultiClickDistanceSquared : m_touchMultiTapDistanceSquared))
376 && m_singleTapReleasedButton == singleTapReleasedButton) {
377 ++m_tapCount;
378 } else {
379 m_singleTapReleasedButton = singleTapReleasedButton;
380 m_singleTapReleasedPoint = point;
381 m_tapCount = 1;
382 }
383 qCDebug(lcTapHandler) << objectName() << "tapped" << m_tapCount << "times; interval since last:" << interval
384 << "sec; distance since last:" << qSqrt(v: distanceSquared);
385 auto button = event->isSinglePointEvent() ? static_cast<QSinglePointEvent *>(event)->button() : Qt::NoButton;
386 emit tapped(eventPoint: point, button);
387 emit tapCountChanged();
388 switch (m_exclusiveSignals) {
389 case NotExclusive:
390 if (m_tapCount == 1)
391 emit singleTapped(eventPoint: point, button);
392 else if (m_tapCount == 2)
393 emit doubleTapped(eventPoint: point, button);
394 break;
395 case SingleTap:
396 if (m_tapCount == 1)
397 emit singleTapped(eventPoint: point, button);
398 break;
399 case DoubleTap:
400 if (m_tapCount == 2)
401 emit doubleTapped(eventPoint: point, button);
402 break;
403 case (SingleTap | DoubleTap):
404 if (m_tapCount == 1) {
405 qCDebug(lcTapHandler) << objectName() << "waiting to emit singleTapped:" << m_multiTapInterval << "ms";
406 m_doubleTapTimer.start(msec: m_multiTapInterval, obj: this);
407 }
408 }
409 qCDebug(lcTapHandler) << objectName() << "tap" << m_tapCount << "after" << event->timestamp() - m_lastTapTimestamp << "ms";
410
411 m_lastTapTimestamp = ts;
412 m_lastTapPos = point.scenePosition();
413 } else {
414 qCDebug(lcTapHandler) << objectName() << "tap threshold" << longPressThreshold() << "exceeded:" << point.timeHeld();
415 }
416 }
417 emit pressedChanged();
418 if (!press && m_gesturePolicy != DragThreshold) {
419 // on release, ungrab after emitting changed signals
420 setExclusiveGrab(ev: event, point, grab: press);
421 }
422 if (cancel) {
423 emit canceled(point);
424 setExclusiveGrab(ev: event, point, grab: false);
425 // In case there is a filtering parent (Flickable), we should not give up the passive grab,
426 // so that it can continue to filter future events.
427 d_func()->reset();
428 emit pointChanged();
429 }
430 }
431}
432
433void QQuickTapHandler::onGrabChanged(QQuickPointerHandler *grabber, QPointingDevice::GrabTransition transition,
434 QPointerEvent *ev, QEventPoint &point)
435{
436 QQuickSinglePointHandler::onGrabChanged(grabber, transition, event: ev, point);
437 bool isCanceled = transition == QPointingDevice::CancelGrabExclusive || transition == QPointingDevice::CancelGrabPassive;
438 if (grabber == this && (isCanceled || point.state() == QEventPoint::Released))
439 setPressed(press: false, cancel: isCanceled, event: ev, point);
440}
441
442void QQuickTapHandler::connectPreRenderSignal(bool conn)
443{
444 auto par = parentItem();
445 if (!par)
446 return;
447 if (conn)
448 connect(sender: par->window(), signal: &QQuickWindow::beforeSynchronizing, context: this, slot: &QQuickTapHandler::updateTimeHeld);
449 else
450 disconnect(sender: par->window(), signal: &QQuickWindow::beforeSynchronizing, receiver: this, slot: &QQuickTapHandler::updateTimeHeld);
451}
452
453void QQuickTapHandler::updateTimeHeld()
454{
455 emit timeHeldChanged();
456}
457
458/*!
459 \qmlproperty int QtQuick::TapHandler::tapCount
460 \readonly
461
462 The number of taps which have occurred within the time and space
463 constraints to be considered a single gesture. The counter is reset to 1
464 if the button changed. For example, to detect a triple-tap, you can write:
465
466 \qml
467 Rectangle {
468 width: 100; height: 30
469 signal tripleTap
470 TapHandler {
471 acceptedButtons: Qt.AllButtons
472 onTapped: if (tapCount == 3) tripleTap()
473 }
474 }
475 \endqml
476*/
477
478/*!
479 \qmlproperty real QtQuick::TapHandler::timeHeld
480 \readonly
481
482 The amount of time in seconds that a pressed point has been held, without
483 moving beyond the drag threshold. It will be updated at least once per
484 frame rendered, which enables rendering an animation showing the progress
485 towards an action which will be triggered by a long-press. It is also
486 possible to trigger one of a series of actions depending on how long the
487 press is held.
488
489 A value of less than zero means no point is being held within this
490 handler's \l [QML] Item.
491
492 \note If \l gesturePolicy is set to \c TapHandler.DragWithinBounds,
493 \c timeHeld does not stop counting even when the pressed point is moved
494 beyond the drag threshold, but only when the point leaves the \l {Item::}
495 {parent} item's \l {QtQuick::Item::contains()}{bounds}.
496*/
497
498/*!
499 \qmlsignal QtQuick::TapHandler::tapped(eventPoint eventPoint, Qt::MouseButton button)
500
501 This signal is emitted each time the \c parent Item is tapped.
502
503 That is, if you press and release a touchpoint or button within a time
504 period less than \l longPressThreshold, while any movement does not exceed
505 the drag threshold, then the \c tapped signal will be emitted at the time
506 of release. The \a eventPoint signal parameter contains information
507 from the release event about the point that was tapped, and \a button
508 is the \l {Qt::MouseButton}{mouse button} that was clicked, or \c NoButton
509 on a touchscreen.
510
511 \snippet pointerHandlers/tapHandlerOnTapped.qml 0
512*/
513
514/*!
515 \qmlsignal QtQuick::TapHandler::singleTapped(eventPoint eventPoint, Qt::MouseButton button)
516 \since 5.11
517
518 This signal is emitted when the \c parent Item is tapped once.
519 After an amount of time greater than QStyleHints::mouseDoubleClickInterval,
520 it can be tapped again; but if the time until the next tap is less,
521 \l tapCount will increase. The \a eventPoint signal parameter contains
522 information from the release event about the point that was tapped, and
523 \a button is the \l {Qt::MouseButton}{mouse button} that was clicked, or
524 \c NoButton on a touchscreen.
525*/
526
527/*!
528 \qmlsignal QtQuick::TapHandler::doubleTapped(eventPoint eventPoint, Qt::MouseButton button)
529 \since 5.11
530
531 This signal is emitted when the \c parent Item is tapped twice within a
532 short span of time (QStyleHints::mouseDoubleClickInterval()) and distance
533 (QStyleHints::mouseDoubleClickDistance() or
534 QStyleHints::touchDoubleTapDistance()). This signal always occurs after
535 \l singleTapped, \l tapped, and \l tapCountChanged. The \a eventPoint
536 signal parameter contains information from the release event about the
537 point that was tapped, and \a button is the
538 \l {Qt::MouseButton}{mouse button} that was clicked, or \c NoButton
539 on a touchscreen.
540*/
541
542/*!
543 \qmlsignal QtQuick::TapHandler::longPressed()
544
545 This signal is emitted when the \c parent Item is pressed and held for a
546 time period greater than \l longPressThreshold. That is, if you press and
547 hold a touchpoint or button, while any movement does not exceed the drag
548 threshold, then the \c longPressed signal will be emitted at the time that
549 \l timeHeld exceeds \l longPressThreshold.
550*/
551
552/*!
553 \qmlsignal QtQuick::TapHandler::tapCountChanged()
554
555 This signal is emitted when the \c parent Item is tapped once or more (within
556 a specified time and distance span) and when the present \c tapCount differs
557 from the previous \c tapCount.
558*/
559QT_END_NAMESPACE
560
561#include "moc_qquicktaphandler_p.cpp"
562

source code of qtdeclarative/src/quick/handlers/qquicktaphandler.cpp