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