TapHandler: add gesturePolicy

Until now it behaved as if this was set to DragThreshold: give up on
the tap as soon as you are clearly dragging rather than tapping.
But that's not what is normally wanted when building a Button control,
for example.  So provide 3 options: give up past the drag threshold,
when the pointer goes outside the bounds, or when it's released
outside the bounds.  The longPressThreshold also constrains all
three cases: holding (or dragging) for too long will not result
in an immediate cancellation, but it also will not be a tap gesture.

Change-Id: I95aec978e783892b55371391a27642751d91d9ff
Reviewed-by: Jan Arve Sæther <jan-arve.saether@qt.io>
This commit is contained in:
Shawn Rutledge 2016-10-18 18:45:49 +02:00
parent 5c639a07fd
commit cb78d5c91e
13 changed files with 400 additions and 22 deletions

View File

@ -58,23 +58,19 @@ int QQuickTapHandler::m_touchMultiTapDistanceSquared(-1);
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.
Detection of a valid tap gesture depends on \l gesturePolicy.
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.
button in order to cancel the click. Therefore the default
\l gesturePolicy is \l ReleaseWithinBounds. If you want to require
that the press and release are close together in both space and time,
set it to \l DragThreshold.
For multi-tap gestures (double-tap, triple-tap etc.), the distance moved
must not exceed QPlatformTheme::MouseDoubleClickDistance with mouse and
QPlatformTheme::TouchDoubleTapDistance with touch, and the time between
taps must not exceed QStyleHints::mouseDoubleClickInterval().
\sa MouseArea
*/
@ -82,6 +78,7 @@ int QQuickTapHandler::m_touchMultiTapDistanceSquared(-1);
QQuickTapHandler::QQuickTapHandler(QObject *parent)
: QQuickPointerSingleHandler(parent)
, m_pressed(false)
, m_gesturePolicy(ReleaseWithinBounds)
, m_tapCount(0)
, m_longPressThreshold(-1)
, m_lastTapTimestamp(0.0)
@ -102,15 +99,46 @@ QQuickTapHandler::~QQuickTapHandler()
{
}
static bool dragOverThreshold(QQuickEventPoint *point)
{
QPointF delta = point->scenePos() - point->scenePressPos();
return (QQuickWindowPrivate::dragOverThreshold(delta.x(), Qt::XAxis, point) ||
QQuickWindowPrivate::dragOverThreshold(delta.y(), Qt::YAxis, point));
}
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.
// If the user has not violated any constraint, 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();
bool ret = false;
switch (point->state()) {
case QQuickEventPoint::Pressed:
case QQuickEventPoint::Released:
ret = parentContains(point);
break;
default: // update or stationary
switch (m_gesturePolicy) {
case DragThreshold:
ret = !dragOverThreshold(point);
break;
case WithinBounds:
ret = parentContains(point);
break;
case ReleaseWithinBounds:
ret = true;
break;
}
break;
}
// If this is the grabber, returning false from this function will
// cancel the grab, so handleGrabCancel() and setPressed(false) will be called.
// But when m_gesturePolicy is DragThreshold, we don't grab, but
// we still don't want to be pressed anymore.
if (!ret)
setPressed(false, true, point);
return ret;
}
void QQuickTapHandler::handleEventPoint(QQuickEventPoint *point)
@ -134,7 +162,7 @@ void QQuickTapHandler::handleEventPoint(QQuickEventPoint *point)
The time in seconds that an event point must be pressed in order to
trigger a long press gesture and emit the \l longPressed() signal.
If the point is released before this time limit, a tap can be detected
if the other constraints are satisfied. The default value is
if the \l gesturePolicy constraint is satisfied. The default value is
QStyleHints::mousePressAndHoldInterval() converted to seconds.
*/
qreal QQuickTapHandler::longPressThreshold() const
@ -165,6 +193,54 @@ void QQuickTapHandler::timerEvent(QTimerEvent *event)
}
}
/*!
\qmlproperty gesturePolicy
The spatial constraint for a tap or long press gesture to be recognized,
in addition to the constraint that the release must occur before
\l longPressThreshold has elapsed. If these constraints are not satisfied,
the \l tapped signal is not emitted, and \l tapCount is not incremented.
If the spatial constraint is violated, \l isPressed transitions immediately
from true to false, regardless of the time held.
\c DragThreshold means that the event point must not move significantly.
If the mouse, finger or stylus moves past the system-wide drag threshold
(QStyleHints::startDragDistance), the tap gesture is canceled, even if
the button or finger is still pressed. This policy can be useful whenever
TapHandler needs to cooperate with other pointer handlers (for example
\l DragHandler), because in this case TapHandler will never grab.
\c WithinBounds means that if the event point leaves the bounds of the
\l target item, the tap gesture is canceled. The TapHandler will grab on
press, but release the grab as soon as the boundary constraint is no
longer satisfied.
\c ReleaseWithinBounds (the default value) means that at the time of release
(the mouse button is released or the finger is lifted), if the event point
is outside the bounds of the \l target item, a tap gesture is not
recognized. This is the default value, because it corresponds to typical
button behavior: you can cancel a click by dragging outside the button, and
you can also change your mind by dragging back inside the button before
release. Note that it's necessary for TapHandler to grab on press and
retain it until release (greedy grab) in order to detect this gesture.
*/
void QQuickTapHandler::setGesturePolicy(QQuickTapHandler::GesturePolicy gesturePolicy)
{
if (m_gesturePolicy == gesturePolicy)
return;
m_gesturePolicy = gesturePolicy;
emit gesturePolicyChanged();
}
/*!
\qmlproperty pressed
This property will be true whenever the mouse or touch point is pressed,
and any movement since the press is compliant with the current
\l gesturePolicy. When the event point is released or the policy is
violated, pressed will change to false.
*/
void QQuickTapHandler::setPressed(bool press, bool cancel, QQuickEventPoint *point)
{
if (m_pressed != press) {
@ -173,6 +249,8 @@ void QQuickTapHandler::setPressed(bool press, bool cancel, QQuickEventPoint *poi
m_longPressTimer.start(longPressThresholdMilliseconds(), this);
else
m_longPressTimer.stop();
if (m_gesturePolicy != DragThreshold)
setGrab(point, press);
if (!cancel && !press && point->timeHeld() < longPressThreshold()) {
// Assuming here that pointerEvent()->timestamp() is in ms.
qreal ts = point->pointerEvent()->timestamp() / 1000.0;

View File

@ -64,8 +64,16 @@ class Q_AUTOTEST_EXPORT QQuickTapHandler : public QQuickPointerSingleHandler
Q_PROPERTY(bool isPressed READ isPressed NOTIFY pressedChanged)
Q_PROPERTY(int tapCount READ tapCount NOTIFY tapCountChanged)
Q_PROPERTY(qreal longPressThreshold READ longPressThreshold WRITE setLongPressThreshold NOTIFY longPressThresholdChanged)
Q_PROPERTY(GesturePolicy gesturePolicy READ gesturePolicy WRITE setGesturePolicy NOTIFY gesturePolicyChanged)
public:
enum GesturePolicy {
DragThreshold,
WithinBounds,
ReleaseWithinBounds
};
Q_ENUM(GesturePolicy)
QQuickTapHandler(QObject *parent = 0);
~QQuickTapHandler();
@ -79,10 +87,14 @@ public:
qreal longPressThreshold() const;
void setLongPressThreshold(qreal longPressThreshold);
GesturePolicy gesturePolicy() const { return m_gesturePolicy; }
void setGesturePolicy(GesturePolicy gesturePolicy);
Q_SIGNALS:
void pressedChanged();
void tapCountChanged();
void longPressThresholdChanged();
void gesturePolicyChanged();
void tapped(QQuickEventPoint *point);
void longPressed();
@ -96,13 +108,13 @@ private:
private:
bool m_pressed;
GesturePolicy m_gesturePolicy;
int m_tapCount;
int m_longPressThreshold;
QBasicTimer m_longPressTimer;
QPointF m_lastTapPos;
qreal m_lastTapTimestamp;
static qreal m_tapInterval;
static qreal m_multiTapInterval;
static int m_mouseMultiClickDistanceSquared;
static int m_touchMultiTapDistanceSquared;

View File

@ -0,0 +1,57 @@
/****************************************************************************
**
** 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.0
SequentialAnimation {
id: tapFlash
running: false
PropertyAction { value: false }
PauseAnimation { duration: 100 }
PropertyAction { value: true }
PauseAnimation { duration: 100 }
PropertyAction { value: false }
PauseAnimation { duration: 100 }
PropertyAction { value: true }
PauseAnimation { duration: 100 }
PropertyAction { value: false }
PauseAnimation { duration: 100 }
PropertyAction { value: true }
}

View File

@ -0,0 +1,85 @@
/****************************************************************************
**
** 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
Rectangle {
id: root
property alias label: label.text
property alias pressed: tap.isPressed
property bool checked: false
property alias gesturePolicy: tap.gesturePolicy
signal tapped
width: label.implicitWidth * 1.5; height: label.implicitHeight * 2.0
border.color: "#9f9d9a"; border.width: 1; radius: height / 4; antialiasing: true
gradient: Gradient {
GradientStop { position: 0.0; color: tap.isPressed ? "#b8b5b2" : "#efebe7" }
GradientStop { position: 1.0; color: "#b8b5b2" }
}
TapHandler {
id: tap
objectName: label.text
onTapped: {
tapFlash.start()
root.tapped
}
}
Text {
id: label
font.pointSize: 14
text: "Button"
anchors.centerIn: parent
}
Rectangle {
anchors.fill: parent
color: "transparent"
border.width: 2; radius: root.radius; antialiasing: true
opacity: tapFlash.running ? 1 : 0
FlashAnimation on visible {
id: tapFlash
}
}
}

View File

@ -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 manual tests of the Qt Toolkit.
@ -46,6 +46,9 @@ Item {
property int value: 50
property int maximumValue: 99
property alias label: label.text
property alias tapEnabled: tap.enabled
property alias pressed: tap.isPressed
signal tapped
Rectangle {
id: slot
@ -70,7 +73,10 @@ Item {
anchors.horizontalCenterOffset: 1
radius: 5
color: "#4400FFFF"
opacity: dragHandler.active ? 1 : 0
opacity: dragHandler.active || tapFlash.running ? 1 : 0
FlashAnimation on visible {
id: tapFlash
}
}
Image {
id: knob
@ -90,6 +96,15 @@ Item {
yAxis.minimum: slot.y
yAxis.maximum: slot.height + slot.y - knob.height
}
TapHandler {
id: tap
objectName: label.text
gesturePolicy: TapHandler.DragThreshold
onTapped: {
tapFlash.start()
root.tapped
}
}
}
Text {

View File

@ -60,6 +60,7 @@ Window {
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"))
addExample("multibuttons", "TapHandler: gesturePolicy (99 red balloons)", Qt.resolvedUrl("multibuttons.qml"))
}
}
}

View File

@ -0,0 +1,95 @@
/****************************************************************************
**
** 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.0
import QtQuick.Particles 2.0
import QtQuick.Layouts 1.0
import Qt.labs.handlers 1.0
import "content"
Item {
width: 800
height: 800
ColumnLayout {
anchors.right: parent.right
spacing: 20
Text { text: "protagonist"; font.pointSize: 12 }
MultiButton {
id: balloonsButton
label: "Launch Balloons"
Layout.fillWidth: true
gesturePolicy: TapHandler.DragThreshold
}
Text { text: "the goons"; font.pointSize: 12 }
MultiButton {
id: missilesButton
label: "Launch Missiles"
Layout.fillWidth: true
gesturePolicy: TapHandler.WithinBounds
}
MultiButton {
id: fightersButton
label: "Launch Fighters"
Layout.fillWidth: true
gesturePolicy: TapHandler.ReleaseWithinBounds
}
}
ParticleSystem {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.leftMargin: 150
ImageParticle { source: "resources/balloon.png" }
Emitter { anchors.bottom: parent.bottom; enabled: balloonsButton.pressed; lifeSpan: 5000; size: 64
maximumEmitted: 99
emitRate: 50; velocity: PointDirection { x: 10; y: -150; yVariation: 30; xVariation: 50 } } }
ParticleSystem {
anchors.verticalCenter: parent.verticalCenter
anchors.right: parent.right
ImageParticle { source: "resources/fighter.png" }
Emitter { anchors.bottom: parent.bottom; enabled: fightersButton.pressed; lifeSpan: 15000; size: 204
emitRate: 3; velocity: PointDirection { x: -1000; y: -250; yVariation: 150; xVariation: 50 } } }
ParticleSystem {
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.rightMargin: 100
ImageParticle { source: "resources/missile.png"; autoRotation: true; rotation: 90 }
Emitter { anchors.bottom: parent.bottom; enabled: missilesButton.pressed; lifeSpan: 5000; size: 128
emitRate: 10; velocity: PointDirection { x: -200; y: -350; yVariation: 200; xVariation: 100 } } }
}

View File

@ -6,16 +6,22 @@
<file>joystick.qml</file>
<file>map.qml</file>
<file>mixer.qml</file>
<file>multibuttons.qml</file>
<file>photosurface.qml</file>
<file>pinchHandler.qml</file>
<file>singlePointHandlerProperties.qml</file>
<file>tapHandler.qml</file>
<file>content/FakeFlickable.qml</file>
<file>content/FlashAnimation.qml</file>
<file>content/MomentumAnimation.qml</file>
<file>content/MultiButton.qml</file>
<file>content/Slider.qml</file>
<file>resources/arrowhead.png</file>
<file>resources/balloon.png</file>
<file>resources/fighter.png</file>
<file>resources/grabbing-location.svg</file>
<file>resources/map.svgz</file>
<file>resources/missile.png</file>
<file>resources/mixer-knob.png</file>
<file>resources/mouse.png</file>
<file>resources/mouse_left.png</file>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.7 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.4 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

After

Width:  |  Height:  |  Size: 7.6 KiB

View File

@ -57,6 +57,9 @@ Item {
acceptedButtons: (leftAllowedCB.checked ? Qt.LeftButton : Qt.NoButton) |
(middleAllowedCB.checked ? Qt.MiddleButton : Qt.NoButton) |
(rightAllowedCB.checked ? Qt.RightButton : Qt.NoButton)
gesturePolicy: (policyDragThresholdCB.checked ? TapHandler.DragThreshold :
policyWithinBoundsCB.checked ? TapHandler.WithinBounds :
TapHandler.ReleaseWithinBounds)
onPressedButtonsChanged: switch (pressedButtons) {
case Qt.MiddleButton: borderBlink.blinkColor = "orange"; break;
case Qt.RightButton: borderBlink.blinkColor = "magenta"; break;
@ -141,5 +144,31 @@ Item {
id: rightAllowedCB
text: "right click"
}
Text { text: " gesture policy:"; anchors.verticalCenter: leftAllowedCB.verticalCenter }
Examples.CheckBox {
id: policyDragThresholdCB
text: "drag threshold"
onCheckedChanged: if (checked) {
policyWithinBoundsCB.checked = false;
policyReleaseWithinBoundsCB.checked = false;
}
}
Examples.CheckBox {
id: policyWithinBoundsCB
text: "within bounds"
onCheckedChanged: if (checked) {
policyDragThresholdCB.checked = false;
policyReleaseWithinBoundsCB.checked = false;
}
}
Examples.CheckBox {
id: policyReleaseWithinBoundsCB
checked: true
text: "release within bounds"
onCheckedChanged: if (checked) {
policyDragThresholdCB.checked = false;
policyWithinBoundsCB.checked = false;
}
}
}
}