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 | |
46 | QT_BEGIN_NAMESPACE |
47 | |
48 | Q_LOGGING_CATEGORY(lcTapHandler, "qt.quick.handler.tap" ) |
49 | |
50 | qreal QQuickTapHandler::m_multiTapInterval(0.0); |
51 | // single tap distance is the same as the drag threshold |
52 | int QQuickTapHandler::m_mouseMultiClickDistanceSquared(-1); |
53 | int 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 | |
88 | QQuickTapHandler::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 | |
100 | bool 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 | |
149 | void 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 | */ |
173 | qreal QQuickTapHandler::longPressThreshold() const |
174 | { |
175 | return longPressThresholdMilliseconds() / 1000.0; |
176 | } |
177 | |
178 | void 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 | |
188 | int QQuickTapHandler::longPressThresholdMilliseconds() const |
189 | { |
190 | return (m_longPressThreshold < 0 ? QGuiApplication::styleHints()->mousePressAndHoldInterval() : m_longPressThreshold); |
191 | } |
192 | |
193 | void 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 | */ |
243 | void 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 | */ |
261 | void 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 | |
322 | void 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 | |
330 | void 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 | |
338 | void 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 | */ |
433 | QT_END_NAMESPACE |
434 | |