From 2d61a39dedfb2718cdb6c26b370b7e4103db9c4a Mon Sep 17 00:00:00 2001 From: Shawn Rutledge Date: Tue, 28 Jul 2015 15:24:29 +0200 Subject: Introduce TapHandler MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Device-agnostic tap/click detection. Also detect whether the taps or clicks occur close enough together in both time and space to be considered part of a multi-tap gesture. Change-Id: I41a378feea3340b9f0409118273746a289641d6c Reviewed-by: Jan Arve Sæther --- src/quick/handlers/handlers.pri | 3 + src/quick/handlers/qquickhandlersmodule.cpp | 4 +- src/quick/handlers/qquicktaphandler.cpp | 177 ++++++++++++++++++++++++++++ src/quick/handlers/qquicktaphandler_p.h | 104 ++++++++++++++++ tests/manual/pointer/main.qml | 1 + tests/manual/pointer/qml.qrc | 1 + tests/manual/pointer/tapHandler.qml | 136 +++++++++++++++++++++ 7 files changed, 425 insertions(+), 1 deletion(-) create mode 100644 src/quick/handlers/qquicktaphandler.cpp create mode 100644 src/quick/handlers/qquicktaphandler_p.h create mode 100644 tests/manual/pointer/tapHandler.qml diff --git a/src/quick/handlers/handlers.pri b/src/quick/handlers/handlers.pri index f32f330d4e..749e4cc4e0 100644 --- a/src/quick/handlers/handlers.pri +++ b/src/quick/handlers/handlers.pri @@ -7,6 +7,7 @@ HEADERS += \ $$PWD/qquickpointersinglehandler_p.h \ $$PWD/qquickhandlersmodule_p.h \ $$PWD/qquickpointerdevicehandler_p.h \ + $$PWD/qquicktaphandler_p.h \ $$PWD/qquickhandlersmodule_p.h \ SOURCES += \ @@ -16,4 +17,6 @@ SOURCES += \ $$PWD/qquickmultipointerhandler.cpp \ $$PWD/qquickpinchhandler.cpp \ $$PWD/qquickpointersinglehandler.cpp \ + $$PWD/qquicktaphandler.cpp \ $$PWD/qquickhandlersmodule.cpp \ + diff --git a/src/quick/handlers/qquickhandlersmodule.cpp b/src/quick/handlers/qquickhandlersmodule.cpp index 697a198e67..4bfdc020d8 100644 --- a/src/quick/handlers/qquickhandlersmodule.cpp +++ b/src/quick/handlers/qquickhandlersmodule.cpp @@ -1,6 +1,6 @@ /**************************************************************************** ** -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2017 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the QtQuick module of the Qt Toolkit. @@ -41,6 +41,7 @@ #include "qquickpointerhandler_p.h" #include "qquickdraghandler_p.h" #include "qquickpinchhandler_p.h" +#include "qquicktaphandler_p.h" static void initResources() { @@ -80,6 +81,7 @@ static void qt_quickhandlers_defineModule(const char *uri, int major, int minor) qmlRegisterUncreatableType(uri, major, minor, "DragAxis", QQuickDragHandler::tr("DragAxis is only available as a grouped property of DragHandler")); qmlRegisterType(uri,major,minor,"PinchHandler"); + qmlRegisterType(uri,major,minor,"TapHandler"); } void QQuickHandlersModule::defineModule() diff --git a/src/quick/handlers/qquicktaphandler.cpp b/src/quick/handlers/qquicktaphandler.cpp new file mode 100644 index 0000000000..e0e66be01c --- /dev/null +++ b/src/quick/handlers/qquicktaphandler.cpp @@ -0,0 +1,177 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#include "qquicktaphandler_p.h" +#include +#include +#include + +QT_BEGIN_NAMESPACE + +qreal QQuickTapHandler::m_multiTapInterval(0.0); +// single tap distance is the same as the drag threshold +int QQuickTapHandler::m_mouseMultiClickDistanceSquared(-1); +int QQuickTapHandler::m_touchMultiTapDistanceSquared(-1); + +/*! + \qmltype TapHandler + \instantiates QQuickTapHandler + \inqmlmodule QtQuick + \ingroup qtquick-handlers + \brief Handler for taps and clicks + + TapHandler is a handler for taps on a touchscreen or clicks on a mouse. + + It requires that any movement between the press and release remains + less than the drag threshold for a single tap, and less than + QPlatformTheme::MouseDoubleClickDistance for multi-tap gestures + (double-tap, triple-tap, etc.) with the mouse, or 10 pixels with touch. + It also requires that the time between press and release remains + less than QStyleHints::mouseDoubleClickInterval() for a single tap, + and that the time from one press/release sequence to the next remains + less than QStyleHints::mouseDoubleClickInterval() for multi-tap gestures. + + Note that buttons (such as QPushButton) are often implemented not to care + whether the press and release occur close together: if you press the button + and then change your mind, you need to drag all the way off the edge of the + button in order to cancel the click. If you want to achieve such behavior, + it's enough to use a PointerHandler and consider the button clicked on + every \l {QQuickPointerHandler:}{released} event. But TapHandler requires + that the events occur close together in both space and time, which is anyway + necessary to detect double clicks or multi-click gestures. + + \sa MouseArea +*/ + +QQuickTapHandler::QQuickTapHandler(QObject *parent) + : QQuickPointerSingleHandler(parent) + , m_pressed(false) + , m_tapCount(0) + , m_lastTapTimestamp(0.0) +{ + setAcceptedButtons(Qt::LeftButton); + if (m_mouseMultiClickDistanceSquared < 0) { + m_multiTapInterval = qApp->styleHints()->mouseDoubleClickInterval() / 1000.0; + m_mouseMultiClickDistanceSquared = QGuiApplicationPrivate::platformTheme()-> + themeHint(QPlatformTheme::MouseDoubleClickDistance).toInt(); + m_mouseMultiClickDistanceSquared *= m_mouseMultiClickDistanceSquared; + m_touchMultiTapDistanceSquared = QGuiApplicationPrivate::platformTheme()-> + themeHint(QPlatformTheme::TouchDoubleTapDistance).toInt(); + m_touchMultiTapDistanceSquared *= m_touchMultiTapDistanceSquared; + } +} + +QQuickTapHandler::~QQuickTapHandler() +{ +} + +bool QQuickTapHandler::wantsEventPoint(QQuickEventPoint *point) +{ + if (point->state() == QQuickEventPoint::Pressed && parentContains(point)) + return true; + // If the user has not dragged too far, it could be a tap. + // Otherwise we want to give up the grab so that a competing handler + // (e.g. DragHandler) gets a chance to take over. + // Don't forget to emit released in case of a cancel. + return !point->isDraggedOverThreshold(); +} + +void QQuickTapHandler::handleEventPoint(QQuickEventPoint *point) +{ + switch (point->state()) { + case QQuickEventPoint::Pressed: + setPressed(true, false, point); + break; + case QQuickEventPoint::Released: + if ((point->pointerEvent()->buttons() & m_acceptedButtons) == Qt::NoButton) + setPressed(false, false, point); + break; + default: + break; + } +} + +void QQuickTapHandler::setPressed(bool press, bool cancel, QQuickEventPoint *point) +{ + if (m_pressed != press) { + m_pressed = press; + if (!cancel && !press && point->timeHeld() < m_multiTapInterval) { + // Assuming here that pointerEvent()->timestamp() is in ms. + qreal ts = point->pointerEvent()->timestamp() / 1000.0; + if (ts - m_lastTapTimestamp < m_multiTapInterval && + QVector2D(point->scenePos() - m_lastTapPos).lengthSquared() < + (point->pointerEvent()->device()->type() == QQuickPointerDevice::Mouse ? + m_mouseMultiClickDistanceSquared : m_touchMultiTapDistanceSquared)) + ++m_tapCount; + else + m_tapCount = 1; + emit tapped(point); + emit tapCountChanged(); + m_lastTapTimestamp = ts; + m_lastTapPos = point->scenePos(); + } + emit pressedChanged(); + } +} + +void QQuickTapHandler::handleGrabCancel(QQuickEventPoint *point) +{ + QQuickPointerSingleHandler::handleGrabCancel(point); + setPressed(false, true, point); +} + +/*! + \qmlproperty tapCount + + The number of taps which have occurred within the time and space + constraints to be considered a single gesture. For example, to detect + a double-tap, you can write + + \qml + Rectangle { + width: 100; height: 30 + signal doubleTap + TapHandler { + acceptedButtons: Qt.AllButtons + onTapped: if (tapCount == 2) doubleTap() + } + } +*/ + +QT_END_NAMESPACE diff --git a/src/quick/handlers/qquicktaphandler_p.h b/src/quick/handlers/qquicktaphandler_p.h new file mode 100644 index 0000000000..c11d51ee03 --- /dev/null +++ b/src/quick/handlers/qquicktaphandler_p.h @@ -0,0 +1,104 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the QtQuick module of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:LGPL$ +** Commercial License Usage +** Licensees holding valid commercial Qt licenses may use this file in +** accordance with the commercial license agreement provided with the +** Software or, alternatively, in accordance with the terms contained in +** a written agreement between you and The Qt Company. For licensing terms +** and conditions see https://www.qt.io/terms-conditions. For further +** information use the contact form at https://www.qt.io/contact-us. +** +** GNU Lesser General Public License Usage +** Alternatively, this file may be used under the terms of the GNU Lesser +** General Public License version 3 as published by the Free Software +** Foundation and appearing in the file LICENSE.LGPL3 included in the +** packaging of this file. Please review the following information to +** ensure the GNU Lesser General Public License version 3 requirements +** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. +** +** GNU General Public License Usage +** Alternatively, this file may be used under the terms of the GNU +** General Public License version 2.0 or (at your option) the GNU General +** Public license version 3 or any later version approved by the KDE Free +** Qt Foundation. The licenses are as published by the Free Software +** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 +** included in the packaging of this file. Please review the following +** information to ensure the GNU General Public License requirements will +** be met: https://www.gnu.org/licenses/gpl-2.0.html and +** https://www.gnu.org/licenses/gpl-3.0.html. +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +#ifndef QQUICKTAPHANDLER_H +#define QQUICKTAPHANDLER_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include "qquickitem.h" +#include "qevent.h" +#include "qquickpointersinglehandler_p.h" + +QT_BEGIN_NAMESPACE + +class Q_AUTOTEST_EXPORT QQuickTapHandler : public QQuickPointerSingleHandler +{ + Q_OBJECT + Q_PROPERTY(bool isPressed READ isPressed NOTIFY pressedChanged) + Q_PROPERTY(int tapCount READ tapCount NOTIFY tapCountChanged) + +public: + QQuickTapHandler(QObject *parent = 0); + ~QQuickTapHandler(); + + bool wantsEventPoint(QQuickEventPoint *point) override; + void handleEventPoint(QQuickEventPoint *point) override; + + bool isPressed() const { return m_pressed; } + + int tapCount() const { return m_tapCount; } + +Q_SIGNALS: + void pressedChanged(); + void tapCountChanged(); + void tapped(QQuickEventPoint *point); + +protected: + void handleGrabCancel(QQuickEventPoint *point) override; + +private: + void setPressed(bool press, bool cancel, QQuickEventPoint *point); + +private: + bool m_pressed; + int m_tapCount; + QPointF m_lastTapPos; + qreal m_lastTapTimestamp; + + static qreal m_tapInterval; + static qreal m_multiTapInterval; + static int m_mouseMultiClickDistanceSquared; + static int m_touchMultiTapDistanceSquared; +}; + +QT_END_NAMESPACE + +QML_DECLARE_TYPE(QQuickTapHandler) + +#endif // QQUICKTAPHANDLER_H diff --git a/tests/manual/pointer/main.qml b/tests/manual/pointer/main.qml index 601065081d..b06d3cf155 100644 --- a/tests/manual/pointer/main.qml +++ b/tests/manual/pointer/main.qml @@ -59,6 +59,7 @@ Window { addExample("fling animation", "DragHandler: after dragging, use an animation to simulate momentum", Qt.resolvedUrl("flingAnimation.qml")) addExample("fake Flickable", "implementation of a simplified Flickable using only Items, DragHandler and MomentumAnimation", Qt.resolvedUrl("fakeFlickable.qml")) addExample("photo surface", "re-implementation of the existing photo surface demo using Handlers", Qt.resolvedUrl("photosurface.qml")) + addExample("tap", "TapHandler: device-agnostic tap/click detection for buttons", Qt.resolvedUrl("tapHandler.qml")) } } } diff --git a/tests/manual/pointer/qml.qrc b/tests/manual/pointer/qml.qrc index 87fc021680..4535a26802 100644 --- a/tests/manual/pointer/qml.qrc +++ b/tests/manual/pointer/qml.qrc @@ -9,6 +9,7 @@ photosurface.qml pinchHandler.qml singlePointHandlerProperties.qml + tapHandler.qml content/FakeFlickable.qml content/MomentumAnimation.qml content/Slider.qml diff --git a/tests/manual/pointer/tapHandler.qml b/tests/manual/pointer/tapHandler.qml new file mode 100644 index 0000000000..d099fc7faf --- /dev/null +++ b/tests/manual/pointer/tapHandler.qml @@ -0,0 +1,136 @@ +/**************************************************************************** +** +** Copyright (C) 2017 The Qt Company Ltd. +** Contact: https://www.qt.io/licensing/ +** +** This file is part of the manual tests of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.8 +import Qt.labs.handlers 1.0 +import "qrc:/quick/shared/" as Examples + +Item { + width: 480 + height: 320 + + Rectangle { + id: rect + anchors.fill: parent; anchors.margins: 40 + border.width: 3; border.color: "transparent" + color: handler.isPressed ? "lightsteelblue" : "darkgrey" + + TapHandler { + id: handler + acceptedButtons: (leftAllowedCB.checked ? Qt.LeftButton : Qt.NoButton) | + (middleAllowedCB.checked ? Qt.MiddleButton : Qt.NoButton) | + (rightAllowedCB.checked ? Qt.RightButton : Qt.NoButton) + onCanceled: { + console.log("canceled @ " + pos) + borderBlink.blinkColor = "red" + borderBlink.start() + } + onTapped: { + console.log("tapped @ " + point.pos + " button(s) " + point.event.button + " tapCount " + tapCount) + if (tapCount > 1) { + tapCountLabel.text = tapCount + flashAnimation.start() + } else { + switch (point.event.button) { + case Qt.LeftButton: borderBlink.blinkColor = "green"; break; + case Qt.MiddleButton: borderBlink.blinkColor = "orange"; break; + case Qt.RightButton: borderBlink.blinkColor = "magenta"; break; + } + borderBlink.start() + } + } + } + + Text { + id: tapCountLabel + anchors.centerIn: parent + font.pixelSize: 72 + font.weight: Font.Black + SequentialAnimation { + id: flashAnimation + PropertyAction { target: tapCountLabel; property: "visible"; value: true } + PropertyAction { target: tapCountLabel; property: "opacity"; value: 1.0 } + PropertyAction { target: tapCountLabel; property: "scale"; value: 1.0 } + ParallelAnimation { + NumberAnimation { + target: tapCountLabel + property: "opacity" + to: 0 + duration: 500 + } + NumberAnimation { + target: tapCountLabel + property: "scale" + to: 1.5 + duration: 500 + } + } + } + } + + SequentialAnimation { + id: borderBlink + property color blinkColor: "blue" + loops: 3 + ScriptAction { script: rect.border.color = borderBlink.blinkColor } + PauseAnimation { duration: 100 } + ScriptAction { script: rect.border.color = "transparent" } + PauseAnimation { duration: 100 } + } + } + + Row { + spacing: 6 + Text { text: "accepted mouse clicks:"; anchors.verticalCenter: leftAllowedCB.verticalCenter } + Examples.CheckBox { + id: leftAllowedCB + checked: true + text: "left click" + } + Examples.CheckBox { + id: middleAllowedCB + text: "middle click" + } + Examples.CheckBox { + id: rightAllowedCB + text: "right click" + } + } +} -- cgit v1.2.3