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 QPlatformTheme::MouseDoubleClickDistance with mouse and
82 QPlatformTheme::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 = QGuiApplicationPrivate::platformTheme()->
94 themeHint(QPlatformTheme::MouseDoubleClickDistance).toInt();
95 m_mouseMultiClickDistanceSquared *= m_mouseMultiClickDistanceSquared;
96 m_touchMultiTapDistanceSquared = QGuiApplicationPrivate::platformTheme()->
97 themeHint(QPlatformTheme::TouchDoubleTapDistance).toInt();
98 m_touchMultiTapDistanceSquared *= m_touchMultiTapDistanceSquared;
99 }
100}
101
102static bool dragOverThreshold(const QQuickEventPoint *point)
103{
104 QPointF delta = point->scenePosition() - point->scenePressPosition();
105 return (QQuickWindowPrivate::dragOverThreshold(delta.x(), Qt::XAxis, point) ||
106 QQuickWindowPrivate::dragOverThreshold(delta.y(), Qt::YAxis, point));
107}
108
109bool QQuickTapHandler::wantsEventPoint(QQuickEventPoint *point)
110{
111 if (!point->pointerEvent()->asPointerMouseEvent() &&
112 !point->pointerEvent()->asPointerTouchEvent() &&
113 !point->pointerEvent()->asPointerTabletEvent() )
114 return false;
115 // If the user has not violated any constraint, it could be a tap.
116 // Otherwise we want to give up the grab so that a competing handler
117 // (e.g. DragHandler) gets a chance to take over.
118 // Don't forget to emit released in case of a cancel.
119 bool ret = false;
120 bool overThreshold = dragOverThreshold(point);
121 if (overThreshold) {
122 m_longPressTimer.stop();
123 m_holdTimer.invalidate();
124 }
125 switch (point->state()) {
126 case QQuickEventPoint::Pressed:
127 case QQuickEventPoint::Released:
128 ret = parentContains(point);
129 break;
130 case QQuickEventPoint::Updated:
131 switch (m_gesturePolicy) {
132 case DragThreshold:
133 ret = !overThreshold && parentContains(point);
134 break;
135 case WithinBounds:
136 ret = parentContains(point);
137 break;
138 case ReleaseWithinBounds:
139 ret = point->pointId() == this->point().id();
140 break;
141 }
142 break;
143 case QQuickEventPoint::Stationary:
144 // Never react in any way when the point hasn't moved.
145 // In autotests, the point's position may not even be correct, because
146 // QTest::touchEvent(window, touchDevice).stationary(1)
147 // provides no opportunity to give a position, so it ends up being random.
148 break;
149 }
150 // If this is the grabber, returning false from this function will cancel the grab,
151 // so onGrabChanged(this, CancelGrabExclusive, point) and setPressed(false) will be called.
152 // But when m_gesturePolicy is DragThreshold, we don't get an exclusive grab, but
153 // we still don't want to be pressed anymore.
154 if (!ret && point->pointId() == this->point().id() && point->state() != QQuickEventPoint::Stationary)
155 setPressed(false, true, point);
156 return ret;
157}
158
159void QQuickTapHandler::handleEventPoint(QQuickEventPoint *point)
160{
161 switch (point->state()) {
162 case QQuickEventPoint::Pressed:
163 setPressed(true, false, point);
164 break;
165 case QQuickEventPoint::Released:
166 if ((point->pointerEvent()->buttons() & acceptedButtons()) == Qt::NoButton)
167 setPressed(false, false, point);
168 break;
169 default:
170 break;
171 }
172}
173
174/*!
175 \qmlproperty real QtQuick::TapHandler::longPressThreshold
176
177 The time in seconds that an event point must be pressed in order to
178 trigger a long press gesture and emit the \l longPressed() signal.
179 If the point is released before this time limit, a tap can be detected
180 if the \l gesturePolicy constraint is satisfied. The default value is
181 QStyleHints::mousePressAndHoldInterval() converted to seconds.
182*/
183qreal QQuickTapHandler::longPressThreshold() const
184{
185 return longPressThresholdMilliseconds() / 1000.0;
186}
187
188void QQuickTapHandler::setLongPressThreshold(qreal longPressThreshold)
189{
190 int ms = qRound(longPressThreshold * 1000);
191 if (m_longPressThreshold == ms)
192 return;
193
194 m_longPressThreshold = ms;
195 emit longPressThresholdChanged();
196}
197
198int QQuickTapHandler::longPressThresholdMilliseconds() const
199{
200 return (m_longPressThreshold < 0 ? QGuiApplication::styleHints()->mousePressAndHoldInterval() : m_longPressThreshold);
201}
202
203void QQuickTapHandler::timerEvent(QTimerEvent *event)
204{
205 if (event->timerId() == m_longPressTimer.timerId()) {
206 m_longPressTimer.stop();
207 qCDebug(lcTapHandler) << objectName() << "longPressed";
208 emit longPressed();
209 }
210}
211
212/*!
213 \qmlproperty enumeration QtQuick::TapHandler::gesturePolicy
214
215 The spatial constraint for a tap or long press gesture to be recognized,
216 in addition to the constraint that the release must occur before
217 \l longPressThreshold has elapsed. If these constraints are not satisfied,
218 the \l tapped signal is not emitted, and \l tapCount is not incremented.
219 If the spatial constraint is violated, \l pressed transitions immediately
220 from true to false, regardless of the time held.
221
222 \value TapHandler.DragThreshold
223 (the default value) The event point must not move significantly.
224 If the mouse, finger or stylus moves past the system-wide drag
225 threshold (QStyleHints::startDragDistance), the tap gesture is
226 canceled, even if the button or finger is still pressed. This policy
227 can be useful whenever TapHandler needs to cooperate with other
228 input handlers (for example \l DragHandler) or event-handling Items
229 (for example QtQuick Controls), because in this case TapHandler
230 will not take the exclusive grab, but merely a passive grab.
231
232 \value TapHandler.WithinBounds
233 If the event point leaves the bounds of the \c parent Item, the tap
234 gesture is canceled. The TapHandler will take the exclusive grab on
235 press, but will release the grab as soon as the boundary constraint
236 is no longer satisfied.
237
238 \value TapHandler.ReleaseWithinBounds
239 At the time of release (the mouse button is released or the finger
240 is lifted), if the event point is outside the bounds of the
241 \c parent Item, a tap gesture is not recognized. This corresponds to
242 typical behavior for button widgets: you can cancel a click by
243 dragging outside the button, and you can also change your mind by
244 dragging back inside the button before release. Note that it's
245 necessary for TapHandler take the exclusive grab on press and retain
246 it until release in order to detect this gesture.
247*/
248void QQuickTapHandler::setGesturePolicy(QQuickTapHandler::GesturePolicy gesturePolicy)
249{
250 if (m_gesturePolicy == gesturePolicy)
251 return;
252
253 m_gesturePolicy = gesturePolicy;
254 emit gesturePolicyChanged();
255}
256
257/*!
258 \qmlproperty bool QtQuick::TapHandler::pressed
259 \readonly
260
261 Holds true whenever the mouse or touch point is pressed,
262 and any movement since the press is compliant with the current
263 \l gesturePolicy. When the event point is released or the policy is
264 violated, \e pressed will change to false.
265*/
266void QQuickTapHandler::setPressed(bool press, bool cancel, QQuickEventPoint *point)
267{
268 if (m_pressed != press) {
269 qCDebug(lcTapHandler) << objectName() << "pressed" << m_pressed << "->" << press << (cancel ? "CANCEL" : "") << point;
270 m_pressed = press;
271 connectPreRenderSignal(press);
272 updateTimeHeld();
273 if (press) {
274 m_longPressTimer.start(longPressThresholdMilliseconds(), this);
275 m_holdTimer.start();
276 } else {
277 m_longPressTimer.stop();
278 m_holdTimer.invalidate();
279 }
280 if (press) {
281 // on press, grab before emitting changed signals
282 if (m_gesturePolicy == DragThreshold)
283 setPassiveGrab(point, press);
284 else
285 setExclusiveGrab(point, press);
286 }
287 if (!cancel && !press && parentContains(point)) {
288 if (point->timeHeld() < longPressThreshold()) {
289 // Assuming here that pointerEvent()->timestamp() is in ms.
290 qreal ts = point->pointerEvent()->timestamp() / 1000.0;
291 if (ts - m_lastTapTimestamp < m_multiTapInterval &&
292 QVector2D(point->scenePosition() - m_lastTapPos).lengthSquared() <
293 (point->pointerEvent()->device()->type() == QQuickPointerDevice::Mouse ?
294 m_mouseMultiClickDistanceSquared : m_touchMultiTapDistanceSquared))
295 ++m_tapCount;
296 else
297 m_tapCount = 1;
298 qCDebug(lcTapHandler) << objectName() << "tapped" << m_tapCount << "times";
299 emit tapped(point);
300 emit tapCountChanged();
301 if (m_tapCount == 1)
302 emit singleTapped(point);
303 else if (m_tapCount == 2)
304 emit doubleTapped(point);
305 m_lastTapTimestamp = ts;
306 m_lastTapPos = point->scenePosition();
307 } else {
308 qCDebug(lcTapHandler) << objectName() << "tap threshold" << longPressThreshold() << "exceeded:" << point->timeHeld();
309 }
310 }
311 emit pressedChanged();
312 if (!press && m_gesturePolicy != DragThreshold) {
313 // on release, ungrab after emitting changed signals
314 setExclusiveGrab(point, press);
315 }
316 if (cancel) {
317 emit canceled(point);
318 setExclusiveGrab(point, false);
319 // In case there is a filtering parent (Flickable), we should not give up the passive grab,
320 // so that it can continue to filter future events.
321 d_func()->reset();
322 emit pointChanged();
323 }
324 }
325}
326
327void QQuickTapHandler::onGrabChanged(QQuickPointerHandler *grabber, QQuickEventPoint::GrabTransition transition, QQuickEventPoint *point)
328{
329 QQuickSinglePointHandler::onGrabChanged(grabber, transition, point);
330 bool isCanceled = transition == QQuickEventPoint::CancelGrabExclusive || transition == QQuickEventPoint::CancelGrabPassive;
331 if (grabber == this && (isCanceled || point->state() == QQuickEventPoint::Released))
332 setPressed(false, isCanceled, point);
333}
334
335void QQuickTapHandler::connectPreRenderSignal(bool conn)
336{
337 if (conn)
338 connect(parentItem()->window(), &QQuickWindow::beforeSynchronizing, this, &QQuickTapHandler::updateTimeHeld);
339 else
340 disconnect(parentItem()->window(), &QQuickWindow::beforeSynchronizing, this, &QQuickTapHandler::updateTimeHeld);
341}
342
343void QQuickTapHandler::updateTimeHeld()
344{
345 emit timeHeldChanged();
346}
347
348/*!
349 \qmlproperty int QtQuick::TapHandler::tapCount
350 \readonly
351
352 The number of taps which have occurred within the time and space
353 constraints to be considered a single gesture. For example, to detect
354 a triple-tap, you can write:
355
356 \qml
357 Rectangle {
358 width: 100; height: 30
359 signal tripleTap
360 TapHandler {
361 acceptedButtons: Qt.AllButtons
362 onTapped: if (tapCount == 3) tripleTap()
363 }
364 }
365 \endqml
366*/
367
368/*!
369 \qmlproperty real QtQuick::TapHandler::timeHeld
370 \readonly
371
372 The amount of time in seconds that a pressed point has been held, without
373 moving beyond the drag threshold. It will be updated at least once per
374 frame rendered, which enables rendering an animation showing the progress
375 towards an action which will be triggered by a long-press. It is also
376 possible to trigger one of a series of actions depending on how long the
377 press is held.
378
379 A value of less than zero means no point is being held within this
380 handler's \l [QML] Item.
381*/
382
383/*!
384 \qmlsignal QtQuick::TapHandler::tapped(EventPoint eventPoint)
385
386 This signal is emitted each time the \c parent Item is tapped.
387
388 That is, if you press and release a touchpoint or button within a time
389 period less than \l longPressThreshold, while any movement does not exceed
390 the drag threshold, then the \c tapped signal will be emitted at the time
391 of release. The \c eventPoint signal parameter contains information
392 from the release event about the point that was tapped:
393
394 \snippet pointerHandlers/tapHandlerOnTapped.qml 0
395*/
396
397/*!
398 \qmlsignal QtQuick::TapHandler::singleTapped(EventPoint eventPoint)
399 \since 5.11
400
401 This signal is emitted when the \c parent Item is tapped once.
402 After an amount of time greater than QStyleHints::mouseDoubleClickInterval,
403 it can be tapped again; but if the time until the next tap is less,
404 \l tapCount will increase. The \c eventPoint signal parameter contains
405 information from the release event about the point that was tapped.
406*/
407
408/*!
409 \qmlsignal QtQuick::TapHandler::doubleTapped(EventPoint eventPoint)
410 \since 5.11
411
412 This signal is emitted when the \c parent Item is tapped twice within a
413 short span of time (QStyleHints::mouseDoubleClickInterval) and distance
414 (QPlatformTheme::MouseDoubleClickDistance or
415 QPlatformTheme::TouchDoubleTapDistance). This signal always occurs after
416 \l singleTapped, \l tapped, and \l tapCountChanged. The \c eventPoint
417 signal parameter contains information from the release event about the
418 point that was tapped.
419*/
420
421/*!
422 \qmlsignal QtQuick::TapHandler::longPressed
423
424 This signal is emitted when the \c parent Item is pressed and held for a
425 time period greater than \l longPressThreshold. That is, if you press and
426 hold a touchpoint or button, while any movement does not exceed the drag
427 threshold, then the \c longPressed signal will be emitted at the time that
428 \l timeHeld exceeds \l longPressThreshold.
429*/
430
431/*!
432 \qmlsignal QtQuick::TapHandler::tapCountChanged
433
434 This signal is emitted when the \c parent Item is tapped once or more (within
435 a specified time and distance span) and when the present \c tapCount differs
436 from the previous \c tapCount.
437*/
438QT_END_NAMESPACE
439