| 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 | |