1/****************************************************************************
2**
3** Copyright (C) 2019 The Qt Company Ltd.
4** Contact: https://www.qt.io/licensing/
5**
6** This file is part of the QtQuick module of the Qt Toolkit.
7**
8** $QT_BEGIN_LICENSE:LGPL$
9** Commercial License Usage
10** Licensees holding valid commercial Qt licenses may use this file in
11** accordance with the commercial license agreement provided with the
12** Software or, alternatively, in accordance with the terms contained in
13** a written agreement between you and The Qt Company. For licensing terms
14** and conditions see https://www.qt.io/terms-conditions. For further
15** information use the contact form at https://www.qt.io/contact-us.
16**
17** GNU Lesser General Public License Usage
18** Alternatively, this file may be used under the terms of the GNU Lesser
19** General Public License version 3 as published by the Free Software
20** Foundation and appearing in the file LICENSE.LGPL3 included in the
21** packaging of this file. Please review the following information to
22** ensure the GNU Lesser General Public License version 3 requirements
23** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
24**
25** GNU General Public License Usage
26** Alternatively, this file may be used under the terms of the GNU
27** General Public License version 2.0 or (at your option) the GNU General
28** Public license version 3 or any later version approved by the KDE Free
29** Qt Foundation. The licenses are as published by the Free Software
30** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
31** included in the packaging of this file. Please review the following
32** information to ensure the GNU General Public License requirements will
33** be met: https://www.gnu.org/licenses/gpl-2.0.html and
34** https://www.gnu.org/licenses/gpl-3.0.html.
35**
36** $QT_END_LICENSE$
37**
38****************************************************************************/
39
40#include "qquicktaphandler_p.h"
41#include "qquicksinglepointhandler_p_p.h"
42#include <qpa/qplatformtheme.h>
43#include <private/qguiapplication_p.h>
44#include <QtGui/qstylehints.h>
45
46QT_BEGIN_NAMESPACE
47
48Q_LOGGING_CATEGORY(lcTapHandler, "qt.quick.handler.tap")
49
50qreal QQuickTapHandler::m_multiTapInterval(0.0);
51// single tap distance is the same as the drag threshold
52int QQuickTapHandler::m_mouseMultiClickDistanceSquared(-1);
53int QQuickTapHandler::m_touchMultiTapDistanceSquared(-1);
54
55/*!
56 \qmltype TapHandler
57 \instantiates QQuickTapHandler
58 \inherits SinglePointHandler
59 \inqmlmodule QtQuick
60 \ingroup qtquick-input-handlers
61 \brief Handler for taps and clicks.
62
63 TapHandler is a handler for taps on a touchscreen or clicks on a mouse.
64
65 Detection of a valid tap gesture depends on \l gesturePolicy. The default
66 value is DragThreshold, which requires the press and release to be close
67 together in both space and time. In this case, DragHandler is able to
68 function using only a passive grab, and therefore does not interfere with
69 event delivery to any other Items or Input Handlers. So the default
70 gesturePolicy is useful when you want to modify behavior of an existing
71 control or Item by adding a TapHandler with bindings and/or JavaScript
72 callbacks.
73
74 Note that buttons (such as QPushButton) are often implemented not to care
75 whether the press and release occur close together: if you press the button
76 and then change your mind, you need to drag all the way off the edge of the
77 button in order to cancel the click. For this use case, set the
78 \l gesturePolicy to \c TapHandler.ReleaseWithinBounds.
79
80 For multi-tap gestures (double-tap, triple-tap etc.), the distance moved
81 must not exceed QStyleHints::mouseDoubleClickDistance() with mouse and
82 QStyleHints::touchDoubleTapDistance() with touch, and the time between
83 taps must not exceed QStyleHints::mouseDoubleClickInterval().
84
85 \sa MouseArea
86*/
87
88QQuickTapHandler::QQuickTapHandler(QQuickItem *parent)
89 : QQuickSinglePointHandler(parent)
90{
91 if (m_mouseMultiClickDistanceSquared < 0) {
92 m_multiTapInterval = qApp->styleHints()->mouseDoubleClickInterval() / 1000.0;
93 m_mouseMultiClickDistanceSquared = qApp->styleHints()->mouseDoubleClickDistance();
94 m_mouseMultiClickDistanceSquared *= m_mouseMultiClickDistanceSquared;
95 m_touchMultiTapDistanceSquared = qApp->styleHints()->touchDoubleTapDistance();
96 m_touchMultiTapDistanceSquared *= m_touchMultiTapDistanceSquared;
97 }
98}
99
100bool QQuickTapHandler::wantsEventPoint(QQuickEventPoint *point)
101{
102 if (!point->pointerEvent()->asPointerMouseEvent() &&
103 !point->pointerEvent()->asPointerTouchEvent() &&
104 !point->pointerEvent()->asPointerTabletEvent() )
105 return false;
106 // If the user has not violated any constraint, it could be a tap.
107 // Otherwise we want to give up the grab so that a competing handler
108 // (e.g. DragHandler) gets a chance to take over.
109 // Don't forget to emit released in case of a cancel.
110 bool ret = false;
111 bool overThreshold = d_func()->dragOverThreshold(point);
112 if (overThreshold) {
113 m_longPressTimer.stop();
114 m_holdTimer.invalidate();
115 }
116 switch (point->state()) {
117 case QQuickEventPoint::Pressed:
118 case QQuickEventPoint::Released:
119 ret = parentContains(point);
120 break;
121 case QQuickEventPoint::Updated:
122 switch (m_gesturePolicy) {
123 case DragThreshold:
124 ret = !overThreshold && parentContains(point);
125 break;
126 case WithinBounds:
127 ret = parentContains(point);
128 break;
129 case ReleaseWithinBounds:
130 ret = point->pointId() == this->point().id();
131 break;
132 }
133 break;
134 case QQuickEventPoint::Stationary:
135 // If the point hasn't moved since last time, the return value should be the same as last time.
136 // If we return false here, QQuickPointerHandler::handlePointerEvent() will call setActive(false).
137 ret = point->pointId() == this->point().id();
138 break;
139 }
140 // If this is the grabber, returning false from this function will cancel the grab,
141 // so onGrabChanged(this, CancelGrabExclusive, point) and setPressed(false) will be called.
142 // But when m_gesturePolicy is DragThreshold, we don't get an exclusive grab, but
143 // we still don't want to be pressed anymore.
144 if (!ret && point->pointId() == this->point().id())
145 setPressed(press: false, cancel: true, point);
146 return ret;
147}
148
149void QQuickTapHandler::handleEventPoint(QQuickEventPoint *point)
150{
151 switch (point->state()) {
152 case QQuickEventPoint::Pressed:
153 setPressed(press: true, cancel: false, point);
154 break;
155 case QQuickEventPoint::Released:
156 if ((point->pointerEvent()->buttons() & acceptedButtons()) == Qt::NoButton)
157 setPressed(press: false, cancel: false, point);
158 break;
159 default:
160 break;
161 }
162}
163
164/*!
165 \qmlproperty real QtQuick::TapHandler::longPressThreshold
166
167 The time in seconds that an event point must be pressed in order to
168 trigger a long press gesture and emit the \l longPressed() signal.
169 If the point is released before this time limit, a tap can be detected
170 if the \l gesturePolicy constraint is satisfied. The default value is
171 QStyleHints::mousePressAndHoldInterval() converted to seconds.
172*/
173qreal QQuickTapHandler::longPressThreshold() const
174{
175 return longPressThresholdMilliseconds() / 1000.0;
176}
177
178void QQuickTapHandler::setLongPressThreshold(qreal longPressThreshold)
179{
180 int ms = qRound(d: longPressThreshold * 1000);
181 if (m_longPressThreshold == ms)
182 return;
183
184 m_longPressThreshold = ms;
185 emit longPressThresholdChanged();
186}
187
188int QQuickTapHandler::longPressThresholdMilliseconds() const
189{
190 return (m_longPressThreshold < 0 ? QGuiApplication::styleHints()->mousePressAndHoldInterval() : m_longPressThreshold);
191}
192
193void QQuickTapHandler::timerEvent(QTimerEvent *event)
194{
195 if (event->timerId() == m_longPressTimer.timerId()) {
196 m_longPressTimer.stop();
197 qCDebug(lcTapHandler) << objectName() << "longPressed";
198 emit longPressed();
199 }
200}
201
202/*!
203 \qmlproperty enumeration QtQuick::TapHandler::gesturePolicy
204
205 The spatial constraint for a tap or long press gesture to be recognized,
206 in addition to the constraint that the release must occur before
207 \l longPressThreshold has elapsed. If these constraints are not satisfied,
208 the \l tapped signal is not emitted, and \l tapCount is not incremented.
209 If the spatial constraint is violated, \l pressed transitions immediately
210 from true to false, regardless of the time held.
211
212 The \c gesturePolicy also affects grab behavior as described below.
213
214 \value TapHandler.DragThreshold
215 (the default value) The event point must not move significantly.
216 If the mouse, finger or stylus moves past the system-wide drag
217 threshold (QStyleHints::startDragDistance), the tap gesture is
218 canceled, even if the button or finger is still pressed. This policy
219 can be useful whenever TapHandler needs to cooperate with other
220 input handlers (for example \l DragHandler) or event-handling Items
221 (for example QtQuick Controls), because in this case TapHandler
222 will not take the exclusive grab, but merely a
223 \l {QPointerEvent::addPassiveGrabber()}{passive grab}.
224
225 \value TapHandler.WithinBounds
226 If the event point leaves the bounds of the \c parent Item, the tap
227 gesture is canceled. The TapHandler will take the
228 \l {QPointerEvent::setExclusiveGrabber}{exclusive grab} on
229 press, but will release the grab as soon as the boundary constraint
230 is no longer satisfied.
231
232 \value TapHandler.ReleaseWithinBounds
233 At the time of release (the mouse button is released or the finger
234 is lifted), if the event point is outside the bounds of the
235 \c parent Item, a tap gesture is not recognized. This corresponds to
236 typical behavior for button widgets: you can cancel a click by
237 dragging outside the button, and you can also change your mind by
238 dragging back inside the button before release. Note that it's
239 necessary for TapHandler to take the
240 \l {QPointerEvent::setExclusiveGrabber}{exclusive grab} on press
241 and retain it until release in order to detect this gesture.
242*/
243void QQuickTapHandler::setGesturePolicy(QQuickTapHandler::GesturePolicy gesturePolicy)
244{
245 if (m_gesturePolicy == gesturePolicy)
246 return;
247
248 m_gesturePolicy = gesturePolicy;
249 emit gesturePolicyChanged();
250}
251
252/*!
253 \qmlproperty bool QtQuick::TapHandler::pressed
254 \readonly
255
256 Holds true whenever the mouse or touch point is pressed,
257 and any movement since the press is compliant with the current
258 \l gesturePolicy. When the event point is released or the policy is
259 violated, \e pressed will change to false.
260*/
261void QQuickTapHandler::setPressed(bool press, bool cancel, QQuickEventPoint *point)
262{
263 if (m_pressed != press) {
264 qCDebug(lcTapHandler) << objectName() << "pressed" << m_pressed << "->" << press << (cancel ? "CANCEL" : "") << point;
265 m_pressed = press;
266 connectPreRenderSignal(conn: press);
267 updateTimeHeld();
268 if (press) {
269 m_longPressTimer.start(msec: longPressThresholdMilliseconds(), obj: this);
270 m_holdTimer.start();
271 } else {
272 m_longPressTimer.stop();
273 m_holdTimer.invalidate();
274 }
275 if (press) {
276 // on press, grab before emitting changed signals
277 if (m_gesturePolicy == DragThreshold)
278 setPassiveGrab(point, grab: press);
279 else
280 setExclusiveGrab(point, grab: press);
281 }
282 if (!cancel && !press && parentContains(point)) {
283 if (point->timeHeld() < longPressThreshold()) {
284 // Assuming here that pointerEvent()->timestamp() is in ms.
285 qreal ts = point->pointerEvent()->timestamp() / 1000.0;
286 if (ts - m_lastTapTimestamp < m_multiTapInterval &&
287 QVector2D(point->scenePosition() - m_lastTapPos).lengthSquared() <
288 (point->pointerEvent()->device()->type() == QQuickPointerDevice::Mouse ?
289 m_mouseMultiClickDistanceSquared : m_touchMultiTapDistanceSquared))
290 ++m_tapCount;
291 else
292 m_tapCount = 1;
293 qCDebug(lcTapHandler) << objectName() << "tapped" << m_tapCount << "times";
294 emit tapped(eventPoint: point);
295 emit tapCountChanged();
296 if (m_tapCount == 1)
297 emit singleTapped(eventPoint: point);
298 else if (m_tapCount == 2)
299 emit doubleTapped(eventPoint: point);
300 m_lastTapTimestamp = ts;
301 m_lastTapPos = point->scenePosition();
302 } else {
303 qCDebug(lcTapHandler) << objectName() << "tap threshold" << longPressThreshold() << "exceeded:" << point->timeHeld();
304 }
305 }
306 emit pressedChanged();
307 if (!press && m_gesturePolicy != DragThreshold) {
308 // on release, ungrab after emitting changed signals
309 setExclusiveGrab(point, grab: press);
310 }
311 if (cancel) {
312 emit canceled(point);
313 setExclusiveGrab(point, grab: false);
314 // In case there is a filtering parent (Flickable), we should not give up the passive grab,
315 // so that it can continue to filter future events.
316 d_func()->reset();
317 emit pointChanged();
318 }
319 }
320}
321
322void QQuickTapHandler::onGrabChanged(QQuickPointerHandler *grabber, QQuickEventPoint::GrabTransition transition, QQuickEventPoint *point)
323{
324 QQuickSinglePointHandler::onGrabChanged(grabber, transition, point);
325 bool isCanceled = transition == QQuickEventPoint::CancelGrabExclusive || transition == QQuickEventPoint::CancelGrabPassive;
326 if (grabber == this && (isCanceled || point->state() == QQuickEventPoint::Released))
327 setPressed(press: false, cancel: isCanceled, point);
328}
329
330void QQuickTapHandler::connectPreRenderSignal(bool conn)
331{
332 if (conn)
333 connect(sender: parentItem()->window(), signal: &QQuickWindow::beforeSynchronizing, receiver: this, slot: &QQuickTapHandler::updateTimeHeld);
334 else
335 disconnect(sender: parentItem()->window(), signal: &QQuickWindow::beforeSynchronizing, receiver: this, slot: &QQuickTapHandler::updateTimeHeld);
336}
337
338void QQuickTapHandler::updateTimeHeld()
339{
340 emit timeHeldChanged();
341}
342
343/*!
344 \qmlproperty int QtQuick::TapHandler::tapCount
345 \readonly
346
347 The number of taps which have occurred within the time and space
348 constraints to be considered a single gesture. For example, to detect
349 a triple-tap, you can write:
350
351 \qml
352 Rectangle {
353 width: 100; height: 30
354 signal tripleTap
355 TapHandler {
356 acceptedButtons: Qt.AllButtons
357 onTapped: if (tapCount == 3) tripleTap()
358 }
359 }
360 \endqml
361*/
362
363/*!
364 \qmlproperty real QtQuick::TapHandler::timeHeld
365 \readonly
366
367 The amount of time in seconds that a pressed point has been held, without
368 moving beyond the drag threshold. It will be updated at least once per
369 frame rendered, which enables rendering an animation showing the progress
370 towards an action which will be triggered by a long-press. It is also
371 possible to trigger one of a series of actions depending on how long the
372 press is held.
373
374 A value of less than zero means no point is being held within this
375 handler's \l [QML] Item.
376*/
377
378/*!
379 \qmlsignal QtQuick::TapHandler::tapped(EventPoint eventPoint)
380
381 This signal is emitted each time the \c parent Item is tapped.
382
383 That is, if you press and release a touchpoint or button within a time
384 period less than \l longPressThreshold, while any movement does not exceed
385 the drag threshold, then the \c tapped signal will be emitted at the time
386 of release. The \a eventPoint signal parameter contains information
387 from the release event about the point that was tapped:
388
389 \snippet pointerHandlers/tapHandlerOnTapped.qml 0
390*/
391
392/*!
393 \qmlsignal QtQuick::TapHandler::singleTapped(EventPoint eventPoint)
394 \since 5.11
395
396 This signal is emitted when the \c parent Item is tapped once.
397 After an amount of time greater than QStyleHints::mouseDoubleClickInterval,
398 it can be tapped again; but if the time until the next tap is less,
399 \l tapCount will increase. The \a eventPoint signal parameter contains
400 information from the release event about the point that was tapped.
401*/
402
403/*!
404 \qmlsignal QtQuick::TapHandler::doubleTapped(EventPoint eventPoint)
405 \since 5.11
406
407 This signal is emitted when the \c parent Item is tapped twice within a
408 short span of time (QStyleHints::mouseDoubleClickInterval()) and distance
409 (QStyleHints::mouseDoubleClickDistance() or
410 QStyleHints::touchDoubleTapDistance()). This signal always occurs after
411 \l singleTapped, \l tapped, and \l tapCountChanged. The \a eventPoint
412 signal parameter contains information from the release event about the
413 point that was tapped.
414*/
415
416/*!
417 \qmlsignal QtQuick::TapHandler::longPressed()
418
419 This signal is emitted when the \c parent Item is pressed and held for a
420 time period greater than \l longPressThreshold. That is, if you press and
421 hold a touchpoint or button, while any movement does not exceed the drag
422 threshold, then the \c longPressed signal will be emitted at the time that
423 \l timeHeld exceeds \l longPressThreshold.
424*/
425
426/*!
427 \qmlsignal QtQuick::TapHandler::tapCountChanged()
428
429 This signal is emitted when the \c parent Item is tapped once or more (within
430 a specified time and distance span) and when the present \c tapCount differs
431 from the previous \c tapCount.
432*/
433QT_END_NAMESPACE
434

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