on xenial help sign in
Improve DirectionalDragArea

- Removed Rejected status, simplifying state machine - Added compositionTime property. Multi-finger scenarios are better handled now. - Refactored TimeSource in Ubuntu.Gestures plugin - Added an easy way to debug DirectionalDragArea by having switchable debug prints - Updated tests to also simulate the passage of time - Use touch point scene coordinates for gesture recognition so that moving the DirectionalDragArea (as in a hinting DragHandle) won't affect it. Fixes: https://bugs.launchpad.net/bugs/1228336. Approved by PS Jenkins bot, Albert Astals Cid, Michael Zanetti.

commited260eb6361dc46965ec8190ee01494b4ef114f2 authorDaniel d'Andrada <daniel.dandrada@canonical.com> dateThu 7 Nov 2013, 23:51 (13 years ago) parent56e5cc7f22c71d
21 files changed+902−314
Components/DragHandle.qml+20−21 Components/EdgeDragArea.qml+12−4 Components/Stage.qml+8−7 plugins/Ubuntu/Gestures/AxisVelocityCalculator.cpp+9−18 plugins/Ubuntu/Gestures/AxisVelocityCalculator.h+9−16 plugins/Ubuntu/Gestures/CMakeLists.txt+1 plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+307−71 plugins/Ubuntu/Gestures/DirectionalDragArea.h+50−13 plugins/Ubuntu/Gestures/TimeSource.cpp+49 plugins/Ubuntu/Gestures/TimeSource.h+54 tests/plugins/Ubuntu/Gestures/DownwardsLauncher.qml+2−10 tests/plugins/Ubuntu/Gestures/LeftwardsLauncher.qml+5−13 tests/plugins/Ubuntu/Gestures/RightwardsLauncher.qml+3−10 tests/plugins/Ubuntu/Gestures/UpwardsLauncher.qml+5−13 tests/plugins/Ubuntu/Gestures/edgeDragExample.qml+7−1 tests/plugins/Ubuntu/Gestures/tst_AxisVelocityCalculator.cpp+3−3 tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.cpp+285−96 tests/qmltests/Components/tst_DragHandle.cpp+4−5 tests/qmltests/Panel/tst_Panel.qml−1 tests/qmltests/tst_Shell.qml+35−12 tests/utils/modules/Unity/Test/UnityTestCase.qml+34
Components/DragHandle.qml+20−21
@@ -48,9 +48,12 @@ EdgeDragArea {
4848 id: dragArea
4949 objectName: "dragHandle"
5050
51− // Disable gesture detection by default when hinting is used.
52− // It doesn't make sense to have both.
53− distanceThreshold: hintDisplacement > 0 ? 0 : units.gu(1.5)
51+ // Disable most of the gesture recognition parameters by default when hinting is used as
52+ // it conflicts with the hinting idea.
53+ // The only part we keep in this situation is that it must be a single-finger gesture.
54+ distanceThreshold: hintDisplacement > 0 ? 0 : defaultDistanceThreshold
55+ maxSilenceTime: hintDisplacement > 0 ? 60*60*1000 : defaultMaxSilenceTime
56+ maxDeviation: hintDisplacement > 0 ? 999999 : defaultMaxDeviation
5457
5558 property bool stretch: false
5659
@@ -181,29 +184,25 @@ EdgeDragArea {
181184 }
182185 }
183186
184− onDraggingChanged: {
185− if (dragging) {
186− if (hintDisplacement > 0) {
187− hintingAnimation.targetValue = d.startValue;
188− hintingAnimation.start();
189− }
190− } else {
191− hintingAnimation.stop();
192− }
193− }
194−
195187 onStatusChanged: {
196188 if (status === DirectionalDragArea.WaitingForTouch) {
189+ hintingAnimation.stop();
197190 if (d.previousStatus === DirectionalDragArea.Recognized) {
198191 d.onFinishedRecognizedGesture();
192+ } else /* d.previousStatus === DirectionalDragArea.Undecided */ {
193+ // Gesture was rejected.
194+ d.rollbackDrag();
195+ }
196+ } else /* Undecided || Recognized */ {
197+ if (d.previousStatus === DirectionalDragArea.WaitingForTouch ||
198+ d.previousStatus === undefined) {
199+ dragEvaluator.reset();
200+ d.startValue = parent[d.targetProp];
201+ }
202+ if (hintDisplacement > 0) {
203+ hintingAnimation.targetValue = d.startValue;
204+ hintingAnimation.start();
199205 }
200− d.startValue = parent[d.targetProp];
201− }
202−
203− if (d.previousStatus === DirectionalDragArea.WaitingForTouch ||
204− d.previousStatus === undefined) {
205− dragEvaluator.reset();
206− d.startValue = parent[d.targetProp];
207206 }
208207
209208 d.previousStatus = status;
Components/EdgeDragArea.qml+12−4
@@ -29,8 +29,16 @@ DirectionalDragArea {
2929 //
3030 // The idea here is that it's better having lax rules than false negatives.
3131 // False negatives are very frustrating to the user.
32− maxDeviation: units.gu(3)
33− wideningAngle: 50
34− distanceThreshold: units.gu(1.5)
35− minSpeed: units.gu(0) // some people were getting false negatives with it enabled.
32+ maxDeviation: defaultMaxDeviation
33+ wideningAngle: defaultWideningAngle
34+ distanceThreshold: defaultDistanceThreshold
35+ minSpeed: defaultMinSpeed
36+ maxSilenceTime: defaultMaxSilenceTime
37+
38+ readonly property real defaultMaxDeviation: units.gu(3)
39+ readonly property real defaultWideningAngle: 50
40+ readonly property real defaultDistanceThreshold: units.gu(1.5)
41+ // some people were getting false negatives with it enabled.
42+ readonly property real defaultMinSpeed: units.gu(0)
43+ readonly property int defaultMaxSilenceTime: 200
3644}
Components/Stage.qml+8−7
@@ -482,19 +482,20 @@ Showable {
482482 switchToApplicationAnimationController.progress += 0.4 * delta
483483 }
484484
485− property bool gotRejected: false
485+ property int lastStatus: -1
486486
487487 onStatusChanged: {
488488 if (status == DirectionalDragArea.Undecided) {
489− gotRejected = false
490489 onUndecided();
491− } else if (status == DirectionalDragArea.Rejected) {
492− switchToApplicationAnimationController.completeToBeginningWithSignal();
493− gotRejected = true;
494490 } else if (status == DirectionalDragArea.WaitingForTouch) {
495− if (!gotRejected)
496− onGestureEnded()
491+ if (lastStatus == DirectionalDragArea.Undecided) {
492+ // got rejected
493+ switchToApplicationAnimationController.completeToBeginningWithSignal();
494+ } else {
495+ onGestureEnded();
496+ }
497497 }
498+ lastStatus = status;
498499 }
499500
500501 function onUndecided() {
plugins/Ubuntu/Gestures/AxisVelocityCalculator.cpp+9−18
@@ -21,25 +21,19 @@
2121#include "AxisVelocityCalculator.h"
2222#include <QtCore/QElapsedTimer>
2323
24−class RealTimeSource : public UbuntuGestures::TimeSource {
25−public:
26− RealTimeSource()
27− : UbuntuGestures::TimeSource() {
28− m_timer.start();
29− }
30− virtual qint64 msecsSinceReference() {
31− return m_timer.elapsed();
32− }
33−private:
34− QElapsedTimer m_timer;
35−};
24+using namespace UbuntuGestures;
3625
3726AxisVelocityCalculator::AxisVelocityCalculator(QObject *parent)
27+ : AxisVelocityCalculator(SharedTimeSource(new RealTimeSource), parent)
28+{
29+}
30+
31+AxisVelocityCalculator::AxisVelocityCalculator(const SharedTimeSource &timeSource,
32+ QObject *parent)
3833 : QObject(parent)
39− , m_timeSource(new RealTimeSource)
34+ , m_timeSource(timeSource)
4035 , m_trackedPosition(0.0)
4136{
42− m_timeSource->setParent(this);
4337 reset();
4438}
4539
@@ -142,11 +136,8 @@ int AxisVelocityCalculator::numSamples() const
142136 }
143137}
144138
145−void AxisVelocityCalculator::setTimeSource(UbuntuGestures::TimeSource *timeSource)
139+void AxisVelocityCalculator::setTimeSource(const SharedTimeSource &timeSource)
146140{
147− if (m_timeSource && m_timeSource->parent() == this)
148− delete m_timeSource;
149−
150141 m_timeSource = timeSource;
151142
152143 if (numSamples() > 0) {
plugins/Ubuntu/Gestures/AxisVelocityCalculator.h+9−16
@@ -24,19 +24,7 @@
2424#include "UbuntuGesturesGlobal.h"
2525#include <stdint.h>
2626#include <QtCore/QObject>
27−
28−namespace UbuntuGestures {
29−/*
30− Interface for a time source.
31− */
32−class UBUNTUGESTURES_EXPORT TimeSource : public QObject {
33− Q_OBJECT
34−public:
35− virtual ~TimeSource() {}
36− /* Returns the current time in milliseconds since some reference time in the past. */
37− virtual qint64 msecsSinceReference() = 0;
38−};
39−}
27+#include "TimeSource.h"
4028
4129/*
4230 Estimates the current velocity of a finger based on recent movement along an axis
@@ -73,10 +61,15 @@ class UBUNTUGESTURES_EXPORT AxisVelocityCalculator : public QObject
7361public:
7462
7563 /*
76− Regular constructor
64+ Regular, simple, constructor
7765 */
7866 AxisVelocityCalculator(QObject *parent = 0);
7967
68+ /*
69+ Constructor that takes a TimeSource
70+ */
71+ AxisVelocityCalculator(const UbuntuGestures::SharedTimeSource &timeSource, QObject *parent = 0);
72+
8073 virtual ~AxisVelocityCalculator();
8174
8275 qreal trackedPosition() const;
@@ -97,7 +90,7 @@ public:
9790 /*
9891 Replaces the TimeSource with the given one. Useful for testing purposes.
9992 */
100− void setTimeSource(UbuntuGestures::TimeSource *timeSource);
93+ void setTimeSource(const UbuntuGestures::SharedTimeSource &timeSource);
10194
10295 /*
10396 The minimum amount of samples needed for a velocity calculation.
@@ -145,7 +138,7 @@ private:
145138 int m_samplesRead; /* index of the oldest sample available. -1 if buffer is empty */
146139 int m_samplesWrite; /* index where the next sample will be written */
147140
148− UbuntuGestures::TimeSource *m_timeSource;
141+ UbuntuGestures::SharedTimeSource m_timeSource;
149142
150143 qreal m_trackedPosition;
151144};
plugins/Ubuntu/Gestures/CMakeLists.txt+1
@@ -6,6 +6,7 @@ set(UbuntuGestureQml_SOURCES
66 AxisVelocityCalculator.cpp
77 Direction.cpp
88 DirectionalDragArea.cpp
9+ TimeSource.cpp
910)
1011
1112add_definitions(-DUBUNTUGESTURES_LIBRARY)
plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+307−71
@@ -18,6 +18,75 @@
1818
1919#include <QtCore/qmath.h>
2020#include <QtCore/QTimer>
21+#include <QDebug>
22+
23+using namespace UbuntuGestures;
24+
25+#define DIRECTIONALDRAGAREA_DEBUG 0
26+
27+#if DIRECTIONALDRAGAREA_DEBUG
28+#define DDA_DEBUG(msg) qDebug("[DDA] " msg)
29+namespace {
30+QString touchPointStateToString(Qt::TouchPointState state) {
31+ switch (state) {
32+ case Qt::TouchPointPressed:
33+ return QString("pressed");
34+ case Qt::TouchPointMoved:
35+ return QString("moved");
36+ case Qt::TouchPointStationary:
37+ return QString("stationary");
38+ default: // Qt::TouchPointReleased:
39+ return QString("released");
40+ }
41+}
42+QString touchEventToString(QTouchEvent *ev)
43+{
44+ QString message;
45+
46+ switch (ev->type()) {
47+ case QEvent::TouchBegin:
48+ message.append("TouchBegin ");
49+ break;
50+ case QEvent::TouchUpdate:
51+ message.append("TouchUpdate ");
52+ break;
53+ case QEvent::TouchEnd:
54+ message.append("TouchEnd ");
55+ break;
56+ default: //QEvent::TouchCancel
57+ message.append("TouchCancel ");
58+ }
59+
60+ for (int i=0; i < ev->touchPoints().size(); ++i) {
61+
62+ const QTouchEvent::TouchPoint& touchPoint = ev->touchPoints().at(i);
63+ message.append(
64+ QString("(id:%1, state:%2, scenePos:(%3,%4)) ")
65+ .arg(touchPoint.id())
66+ .arg(touchPointStateToString(touchPoint.state()))
67+ .arg(touchPoint.scenePos().x())
68+ .arg(touchPoint.scenePos().y())
69+ );
70+ }
71+
72+ return message;
73+}
74+
75+const char *statusToString(DirectionalDragArea::Status status)
76+{
77+ if (status == DirectionalDragArea::WaitingForTouch) {
78+ return "WaitingForTouch";
79+ } else if (status == DirectionalDragArea::Undecided) {
80+ return "Undecided";
81+ } else {
82+ return "Recognized";
83+ }
84+}
85+
86+} // namespace {
87+#else // DIRECTIONALDRAGAREA_DEBUG
88+#define DDA_DEBUG(msg) do{}while(0)
89+#endif // DIRECTIONALDRAGAREA_DEBUG
2190
2291// Essentially a QTimer wrapper
2392class RecognitionTimer : public UbuntuGestures::AbstractTimer
@@ -48,9 +117,12 @@ DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
48117 , m_minSpeed(0)
49118 , m_maxSilenceTime(200)
50119 , m_silenceTime(0)
120+ , m_compositionTime(60)
51121 , m_numSamplesOnLastSpeedCheck(0)
52122 , m_recognitionTimer(0)
53123 , m_velocityCalculator(0)
124+ , m_timeSource(new RealTimeSource)
125+ , m_activeTouches(m_timeSource)
54126{
55127 setRecognitionTimer(new RecognitionTimer(this));
56128 m_recognitionTimer->setInterval(60);
@@ -73,8 +145,8 @@ void DirectionalDragArea::setDirection(Direction::Type direction)
73145
74146void DirectionalDragArea::setMaxDeviation(qreal value)
75147{
76− if (m_dampedPos.maxDelta() != value) {
77− m_dampedPos.setMaxDelta(value);
148+ if (m_dampedScenePos.maxDelta() != value) {
149+ m_dampedScenePos.setMaxDelta(value);
78150 Q_EMIT maxDeviationChanged(value);
79151 }
80152}
@@ -118,6 +190,14 @@ void DirectionalDragArea::setMaxSilenceTime(int value)
118190 }
119191}
120192
193+void DirectionalDragArea::setCompositionTime(int value)
194+{
195+ if (m_compositionTime != value) {
196+ m_compositionTime = value;
197+ Q_EMIT compositionTimeChanged(value);
198+ }
199+}
200+
121201void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
122202{
123203 int interval = 0;
@@ -141,9 +221,11 @@ void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *tim
141221 }
142222}
143223
144−void DirectionalDragArea::setTimeSource(UbuntuGestures::TimeSource *timeSource)
224+void DirectionalDragArea::setTimeSource(const SharedTimeSource &timeSource)
145225{
226+ m_timeSource = timeSource;
146227 m_velocityCalculator->setTimeSource(timeSource);
228+ m_activeTouches.m_timeSource = timeSource;
147229}
148230
149231qreal DirectionalDragArea::distance() const
@@ -186,6 +268,12 @@ qreal DirectionalDragArea::touchSceneY() const
186268
187269void DirectionalDragArea::touchEvent(QTouchEvent *event)
188270{
271+ #if DIRECTIONALDRAGAREA_DEBUG
272+ // TODO Consider using qCDebug() when available (Qt 5.2)
273+ qDebug() << "[DDA]" << m_timeSource->msecsSinceReference()
274+ << qPrintable(touchEventToString(event));
275+ #endif
276+
189277 if (!isEnabled() || !isVisible()) {
190278 QQuickItem::touchEvent(event);
191279 return;
@@ -198,76 +286,124 @@ void DirectionalDragArea::touchEvent(QTouchEvent *event)
198286 case Undecided:
199287 touchEvent_undecided(event);
200288 break;
201− case Recognized:
289+ default: // Recognized:
202290 touchEvent_recognized(event);
203291 break;
204− default: // Rejected
205− touchEvent_rejected(event);
206− break;
207292 }
293+
294+ m_activeTouches.update(event);
208295}
209296
210297void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
211298{
212− if ((event->touchPointStates() && (Qt::TouchPointPressed || Qt::TouchPointMoved))
213− && event->touchPoints().count() == 1) {
214− const QTouchEvent::TouchPoint &touchPoint = event->touchPoints()[0];
215− m_startPos = touchPoint.pos();
216− m_startScenePos = touchPoint.scenePos();
217− m_touchId = touchPoint.id();
218− m_dampedPos.reset(m_startPos);
219− updateVelocityCalculator(m_startPos);
220− m_velocityCalculator->reset();
221− m_numSamplesOnLastSpeedCheck = 0;
222− m_silenceTime = 0;
223− setPreviousPos(m_startPos);
224− setPreviousScenePos(m_startScenePos);
299+ if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
300+ // Nothing to see here. No touch starting in this event.
301+ return;
302+ }
225303
226− if (m_distanceThreshold > 0)
227− setStatus(Undecided);
228− else
229− setStatus(Recognized);
304+ if (isWithinTouchCompositionWindow()) {
305+ // too close to the last touch start. So we consider them as starting roughly at the same time.
306+ // Can't be a single-touch gesture.
307+ #if DIRECTIONALDRAGAREA_DEBUG
308+ qDebug("[DDA] A new touch point came in but we're still within time composition window. Ignoring it.");
309+ #endif
310+ return;
230311 }
312+
313+ const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
314+
315+ const QTouchEvent::TouchPoint *newTouchPoint = nullptr;
316+ for (int i = 0; i < touchPoints.count(); ++i) {
317+ const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
318+ if (touchPoint.state() == Qt::TouchPointPressed) {
319+ if (newTouchPoint) {
320+ // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture
321+ return;
322+ } else {
323+ // that's our candidate
324+ m_touchId = touchPoint.id();
325+ newTouchPoint = &touchPoint;
326+ }
327+ }
328+ }
329+
330+ Q_ASSERT(newTouchPoint);
331+
332+ // If we have made this far, we are good to go to the next status.
333+
334+ m_startPos = newTouchPoint->pos();
335+ m_startScenePos = newTouchPoint->scenePos();
336+ m_touchId = newTouchPoint->id();
337+ m_dampedScenePos.reset(m_startScenePos);
338+ updateVelocityCalculator(m_startScenePos);
339+ m_velocityCalculator->reset();
340+ m_numSamplesOnLastSpeedCheck = 0;
341+ m_silenceTime = 0;
342+ setPreviousPos(m_startPos);
343+ setPreviousScenePos(m_startScenePos);
344+
345+ setStatus(Undecided);
231346}
232347
233348void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
234349{
235350 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
236− const QPointF &touchPos = touchPoint->pos();
351+
352+ if (!touchPoint) {
353+ qCritical() << "DirectionalDragArea[status=Undecided]: touch " << m_touchId
354+ << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
355+ "Considering it as released.";
356+ setStatus(WaitingForTouch);
357+ return;
358+ }
359+
360+ const QPointF &touchScenePos = touchPoint->scenePos();
237361
238362 if (touchPoint->state() == Qt::TouchPointReleased) {
239363 // touch has ended before recognition concluded
364+ DDA_DEBUG("Touch has ended before recognition concluded");
240365 setStatus(WaitingForTouch);
241366 return;
242367 }
243368
244− if (event->touchPointStates().testFlag(Qt::TouchPointPressed)
245− || event->touchPoints().count() > 1) {
369+ if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
246370 // multi-finger drags are not accepted
247− setStatus(Rejected);
371+ DDA_DEBUG("Multi-finger drags are not accepted");
372+ setStatus(WaitingForTouch);
248373 return;
249374 }
250375
251− m_previousDampedPos.setX(m_dampedPos.x());
252− m_previousDampedPos.setY(m_dampedPos.y());
253− m_dampedPos.update(touchPos);
254− updateVelocityCalculator(touchPos);
376+ m_previousDampedScenePos.setX(m_dampedScenePos.x());
377+ m_previousDampedScenePos.setY(m_dampedScenePos.y());
378+ m_dampedScenePos.update(touchScenePos);
379+ updateVelocityCalculator(touchScenePos);
255380
256381 if (!pointInsideAllowedArea()) {
257− setStatus(Rejected);
382+ DDA_DEBUG("Rejecting gesture because touch point is outside allowed area.");
383+ setStatus(WaitingForTouch);
258384 return;
259385 }
260386
261387 if (!movingInRightDirection()) {
262− setStatus(Rejected);
388+ DDA_DEBUG("Rejecting gesture becauuse touch point is moving in the wrong direction.");
389+ setStatus(WaitingForTouch);
263390 return;
264391 }
265392
266− setPreviousPos(touchPos);
267− setPreviousScenePos(touchPoint->scenePos());
393+ setPreviousPos(touchPoint->pos());
394+ setPreviousScenePos(touchScenePos);
268395
269− if (movedFarEnough(touchPos)) {
396+ if (isWithinTouchCompositionWindow()) {
397+ // There's still time for some new touch to appear and ruin our party as it would be combined
398+ // with our m_touchId one and therefore deny the possibility of a single-finger gesture.
399+ DDA_DEBUG("Sill within composition window. Let's wait more.");
400+ return;
401+ }
402+
403+ if (movedFarEnough(touchScenePos)) {
270404 setStatus(Recognized);
405+ } else {
406+ DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
271407 }
272408}
273409
@@ -275,20 +411,18 @@ void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
275411{
276412 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
277413
278− setPreviousPos(touchPoint->pos());
279− setPreviousScenePos(touchPoint->scenePos());
280−
281− if (touchPoint->state() == Qt::TouchPointReleased) {
414+ if (!touchPoint) {
415+ qCritical() << "DirectionalDragArea[status=Recognized]: touch " << m_touchId
416+ << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
417+ "Considering it as released.";
282418 setStatus(WaitingForTouch);
283− }
284−}
285−
286−void DirectionalDragArea::touchEvent_rejected(QTouchEvent *event)
287−{
288− const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
419+ } else {
420+ setPreviousPos(touchPoint->pos());
421+ setPreviousScenePos(touchPoint->scenePos());
289422
290− if (!touchPoint || touchPoint->state() == Qt::TouchPointReleased) {
291− setStatus(WaitingForTouch);
423+ if (touchPoint->state() == Qt::TouchPointReleased) {
424+ setStatus(WaitingForTouch);
425+ }
292426 }
293427}
294428
@@ -307,8 +441,8 @@ const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouch
307441
308442bool DirectionalDragArea::pointInsideAllowedArea() const
309443{
310− qreal dX = m_dampedPos.x() - m_startPos.x();
311− qreal dY = m_dampedPos.y() - m_startPos.y();
444+ qreal dX = m_dampedScenePos.x() - m_startScenePos.x();
445+ qreal dY = m_dampedScenePos.y() - m_startScenePos.y();
312446
313447 switch (m_direction) {
314448 case Direction::Upwards:
@@ -326,22 +460,26 @@ bool DirectionalDragArea::movingInRightDirection() const
326460{
327461 switch (m_direction) {
328462 case Direction::Upwards:
329− return m_dampedPos.y() <= m_previousDampedPos.y();
463+ return m_dampedScenePos.y() <= m_previousDampedScenePos.y();
330464 case Direction::Downwards:
331− return m_dampedPos.y() >= m_previousDampedPos.y();
465+ return m_dampedScenePos.y() >= m_previousDampedScenePos.y();
332466 case Direction::Leftwards:
333− return m_dampedPos.x() <= m_previousDampedPos.x();
467+ return m_dampedScenePos.x() <= m_previousDampedScenePos.x();
334468 default: // Direction::Rightwards:
335− return m_dampedPos.x() >= m_previousDampedPos.x();
469+ return m_dampedScenePos.x() >= m_previousDampedScenePos.x();
336470 }
337471}
338472
339473bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
340474{
341− if (Direction::isHorizontal(m_direction))
342− return qFabs(point.x() - m_startPos.x()) > m_distanceThreshold;
343− else
344− return qFabs(point.y() - m_startPos.y()) > m_distanceThreshold;
475+ if (m_distanceThreshold > 0) {
476+ if (Direction::isHorizontal(m_direction))
477+ return qFabs(point.x() - m_startScenePos.x()) > m_distanceThreshold;
478+ else
479+ return qFabs(point.y() - m_startScenePos.y()) > m_distanceThreshold;
480+ } else {
481+ return true;
482+ }
345483}
346484
347485void DirectionalDragArea::checkSpeed()
@@ -351,7 +489,8 @@ void DirectionalDragArea::checkSpeed()
351489 qreal minSpeedMsecs = m_minSpeed / 1000.0;
352490
353491 if (speed < minSpeedMsecs) {
354− setStatus(Rejected);
492+ DDA_DEBUG("Rejecting gesture because it's below minimum speed.");
493+ setStatus(WaitingForTouch);
355494 }
356495 }
357496
@@ -359,7 +498,8 @@ void DirectionalDragArea::checkSpeed()
359498 m_silenceTime += m_recognitionTimer->interval();
360499
361500 if (m_silenceTime > m_maxSilenceTime) {
362− setStatus(Rejected);
501+ DDA_DEBUG("Rejecting gesture because it's silence time has been exceeded.");
502+ setStatus(WaitingForTouch);
363503 }
364504 } else {
365505 m_silenceTime = 0;
@@ -381,11 +521,13 @@ void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
381521 m_status = newStatus;
382522 Q_EMIT statusChanged(m_status);
383523
524+ #if DIRECTIONALDRAGAREA_DEBUG
525+ qDebug() << "[DDA]" << statusToString(oldStatus) << "->" << statusToString(newStatus);
526+ #endif
527+
384528 switch (newStatus) {
385529 case WaitingForTouch:
386− if (oldStatus != DirectionalDragArea::Rejected) {
387− Q_EMIT draggingChanged(false);
388− }
530+ Q_EMIT draggingChanged(false);
389531 break;
390532 case Undecided:
391533 m_recognitionTimer->start();
@@ -395,9 +537,6 @@ void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
395537 if (oldStatus == WaitingForTouch)
396538 Q_EMIT draggingChanged(true);
397539 break;
398− case Rejected:
399− Q_EMIT draggingChanged(false);
400− break;
401540 default:
402541 // no-op
403542 break;
@@ -406,8 +545,6 @@ void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
406545
407546void DirectionalDragArea::setPreviousPos(const QPointF &point)
408547{
409− Q_ASSERT(m_status != Rejected);
410−
411548 bool xChanged = m_previousPos.x() != point.x();
412549 bool yChanged = m_previousPos.y() != point.y();
413550
@@ -428,8 +565,6 @@ void DirectionalDragArea::setPreviousPos(const QPointF &point)
428565
429566void DirectionalDragArea::setPreviousScenePos(const QPointF &point)
430567{
431− Q_ASSERT(m_status != Rejected);
432−
433568 bool xChanged = m_previousScenePos.x() != point.x();
434569 bool yChanged = m_previousScenePos.y() != point.y();
435570
@@ -457,5 +592,106 @@ void DirectionalDragArea::updateVelocityCalculator(const QPointF &point)
457592 }
458593}
459594
595+bool DirectionalDragArea::isWithinTouchCompositionWindow()
596+{
597+ return !m_activeTouches.isEmpty() &&
598+ m_timeSource->msecsSinceReference() <=
599+ m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
600+}
601+
602+//************************** ActiveTouchesInfo **************************
603+
604+DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(const SharedTimeSource &timeSource)
605+ : m_timeSource(timeSource), m_lastUsedIndex(-1)
606+{
607+ // Estimate of the maximum number of active touches we might reach.
608+ // Not a problem if it ends up being an underestimate as this is just
609+ // an optimization.
610+ m_vector.resize(3);
611+}
612+
613+void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
614+{
615+ if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
616+ // nothing to update
617+ return;
618+ }
619+
620+ const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
621+ for (int i = 0; i < touchPoints.count(); ++i) {
622+ const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
623+ if (touchPoint.state() == Qt::TouchPointPressed) {
624+ addTouchPoint(touchPoint);
625+ } else if (touchPoint.state() == Qt::TouchPointReleased) {
626+ removeTouchPoint(touchPoint);
627+ }
628+ }
629+}
630+
631+void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
632+{
633+ ActiveTouchInfo &activeTouchInfo = getEmptySlot();
634+ activeTouchInfo.id = touchPoint.id();
635+ activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
636+}
637+
638+void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
639+{
640+ for (int i = 0; i <= m_lastUsedIndex; ++i) {
641+ if (touchPoint.id() == m_vector.at(i).id) {
642+ freeSlot(i);
643+ return;
644+ }
645+ }
646+ Q_ASSERT(false); // shouldn't reach this point
647+}
648+
649+DirectionalDragArea::ActiveTouchInfo &DirectionalDragArea::ActiveTouchesInfo::getEmptySlot()
650+{
651+ Q_ASSERT(m_lastUsedIndex < m_vector.size());
652+
653+ // Look for an in-between vacancy first
654+ for (int i = 0; i < m_lastUsedIndex; ++i) {
655+ ActiveTouchInfo &activeTouchInfo = m_vector[i];
656+ if (!activeTouchInfo.isValid()) {
657+ return activeTouchInfo;
658+ }
659+ }
660+
661+ ++m_lastUsedIndex;
662+ if (m_lastUsedIndex >= m_vector.size()) {
663+ m_vector.resize(m_lastUsedIndex + 1);
664+ }
665+
666+ return m_vector[m_lastUsedIndex];
667+}
668+
669+void DirectionalDragArea::ActiveTouchesInfo::freeSlot(int index)
670+{
671+ m_vector[index].reset();
672+ if (index == m_lastUsedIndex) {
673+ do {
674+ --m_lastUsedIndex;
675+ } while (m_lastUsedIndex >= 0 && !m_vector.at(m_lastUsedIndex).isValid());
676+ }
677+}
678+
679+qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
680+{
681+ Q_ASSERT(m_lastUsedIndex >= 0);
682+
683+ qint64 highestStartTime = m_vector.at(0).startTime;
684+ int i = 1;
685+ do {
686+ const ActiveTouchInfo &activeTouchInfo = m_vector.at(i);
687+ if (activeTouchInfo.isValid() && activeTouchInfo.startTime > highestStartTime) {
688+ highestStartTime = activeTouchInfo.startTime;
689+ }
690+ ++i;
691+ } while (i < m_lastUsedIndex);
692+
693+ return highestStartTime;
694+}
695+
460696// Because we are defining a new QObject-based class (RecognitionTimer) here.
461697#include "DirectionalDragArea.moc"
plugins/Ubuntu/Gestures/DirectionalDragArea.h+50−13
@@ -117,9 +117,17 @@ class UBUNTUGESTURES_EXPORT DirectionalDragArea : public QQuickItem {
117117 Q_PROPERTY(int maxSilenceTime READ maxSilenceTime
118118 WRITE setMaxSilenceTime
119119 NOTIFY maxSilenceTimeChanged)
120+
120121 //
121122 /////
122123
124+ // Maximum time (in milliseconds) after the start of a given touch point where
125+ // subsequent touch starts are grouped with the first one into an N-touches gesture
126+ // (e.g. a two-fingers tap or drag).
127+ Q_PROPERTY(int compositionTime READ compositionTime
128+ WRITE setCompositionTime
129+ NOTIFY compositionTimeChanged)
130+
123131 Q_ENUMS(Direction)
124132 Q_ENUMS(Status)
125133public:
@@ -130,26 +138,23 @@ public:
130138
131139 // Describes the state of the directional drag gesture.
132140 enum Status {
133− // There's no touch point over this area.
141+ // Waiting for a new touch point to land on this area. No gesture is being processed
142+ // or tracked.
134143 WaitingForTouch,
135144
136145 // A touch point has landed on this area but it's not know yet whether it is
137146 // performing a drag in the correct direction.
147+ // If it's decided that the touch point is not performing a directional drag gesture,
148+ // it will be rejected/ignored and status will return to WaitingForTouch.
138149 Undecided, //Recognizing,
139150
140151 // There's a touch point in this area and it performed a drag in the correct
141152 // direction.
142153 //
143− // Once recognized, the gesture state will move back to Absent only once
154+ // Once recognized, the gesture state will move back to WaitingForTouch only once
144155 // that touch point ends. The gesture will remain in the Recognized state even if
145156 // the touch point starts moving in other directions or halts.
146157 Recognized,
147−
148− // A gesture was performed but it wasn't a single-touch drag in the correct
149− // direction.
150− // It will remain in this state until there are no more touch points over this
151− // area, at which point it will move to Absent state.
152− Rejected
153158 };
154159 Status status() const { return m_status; }
155160
@@ -164,7 +169,7 @@ public:
164169
165170 bool dragging() const { return (m_status == Undecided) || (m_status == Recognized); }
166171
167− qreal maxDeviation() const { return m_dampedPos.maxDelta(); }
172+ qreal maxDeviation() const { return m_dampedScenePos.maxDelta(); }
168173 void setMaxDeviation(qreal value);
169174
170175 qreal wideningAngle() const;
@@ -179,13 +184,16 @@ public:
179184 int maxSilenceTime() const { return m_maxSilenceTime; }
180185 void setMaxSilenceTime(int value);
181186
187+ int compositionTime() const { return m_compositionTime; }
188+ void setCompositionTime(int value);
189+
182190 // Replaces the existing Timer with the given one.
183191 //
184192 // Useful for providing a fake timer when testing.
185193 void setRecognitionTimer(UbuntuGestures::AbstractTimer *timer);
186194
187195 // Useful for testing, where a fake time source can be supplied
188− void setTimeSource(UbuntuGestures::TimeSource *timeSource);
196+ void setTimeSource(const UbuntuGestures::SharedTimeSource &timeSource);
189197
190198Q_SIGNALS:
191199 void directionChanged(Direction::Type direction);
@@ -198,6 +206,7 @@ Q_SIGNALS:
198206 void distanceThresholdChanged(qreal value);
199207 void minSpeedChanged(qreal value);
200208 void maxSilenceTimeChanged(int value);
209+ void compositionTimeChanged(int value);
201210 void touchXChanged(qreal value);
202211 void touchYChanged(qreal value);
203212 void touchSceneXChanged(qreal value);
@@ -213,7 +222,6 @@ private:
213222 void touchEvent_absent(QTouchEvent *event);
214223 void touchEvent_undecided(QTouchEvent *event);
215224 void touchEvent_recognized(QTouchEvent *event);
216− void touchEvent_rejected(QTouchEvent *event);
217225 bool pointInsideAllowedArea() const;
218226 bool movingInRightDirection() const;
219227 bool movedFarEnough(const QPointF &point) const;
@@ -222,6 +230,7 @@ private:
222230 void setPreviousPos(const QPointF &point);
223231 void setPreviousScenePos(const QPointF &point);
224232 void updateVelocityCalculator(const QPointF &point);
233+ bool isWithinTouchCompositionWindow();
225234
226235 Status m_status;
227236
@@ -233,8 +242,8 @@ private:
233242
234243 // A movement damper is used in some of the gesture recognition calculations
235244 // to get rid of noise or small oscillations in the touch position.
236− DampedPointF m_dampedPos;
237− QPointF m_previousDampedPos;
245+ DampedPointF m_dampedScenePos;
246+ QPointF m_previousDampedScenePos;
238247
239248 Direction::Type m_direction;
240249 qreal m_wideningAngle; // in degrees
@@ -243,9 +252,37 @@ private:
243252 qreal m_minSpeed;
244253 int m_maxSilenceTime; // in milliseconds
245254 int m_silenceTime; // in milliseconds
255+ int m_compositionTime; // in milliseconds
246256 int m_numSamplesOnLastSpeedCheck;
247257 UbuntuGestures::AbstractTimer *m_recognitionTimer;
248258 AxisVelocityCalculator *m_velocityCalculator;
259+
260+ UbuntuGestures::SharedTimeSource m_timeSource;
261+
262+ // Information about an active touch point
263+ struct ActiveTouchInfo {
264+ ActiveTouchInfo() : id(-1), startTime(-1) {}
265+ bool isValid() const { return id != -1; }
266+ void reset() { id = -1; }
267+ int id;
268+ qint64 startTime;
269+ };
270+ class ActiveTouchesInfo {
271+ public:
272+ ActiveTouchesInfo(const UbuntuGestures::SharedTimeSource &timeSource);
273+ void update(QTouchEvent *event);
274+ ActiveTouchInfo &touchInfo(int id);
275+ qint64 mostRecentStartTime();
276+ UbuntuGestures::SharedTimeSource m_timeSource;
277+ bool isEmpty() const { return m_lastUsedIndex == -1; }
278+ private:
279+ void addTouchPoint(const QTouchEvent::TouchPoint &touchPoint);
280+ ActiveTouchInfo &getEmptySlot();
281+ void freeSlot(int index);
282+ void removeTouchPoint(const QTouchEvent::TouchPoint &touchPoint);
283+ QVector<struct ActiveTouchInfo> m_vector;
284+ int m_lastUsedIndex;
285+ } m_activeTouches;
249286};
250287
251288#endif // DIRECTIONAL_DRAG_AREA_H
plugins/Ubuntu/Gestures/TimeSource.cppnew file+49
@@ -0,0 +1,49 @@
1+/*
2+ * Copyright (C) 2013 - Canonical Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it
5+ * under the terms of the GNU Lesser General Public License, as
6+ * published by the Free Software Foundation; either version 2.1 or 3.0
7+ * of the License.
8+ *
9+ * This program is distributed in the hope that it will be useful, but
10+ * WITHOUT ANY WARRANTY; without even the implied warranties of
11+ * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR
12+ * PURPOSE. See the applicable version of the GNU Lesser General Public
13+ * License for more details.
14+ *
15+ * You should have received a copy of both the GNU Lesser General Public
16+ * License along with this program. If not, see <http://www.gnu.org/licenses/>
17+ *
18+ * Authored by: Daniel d'Andrada <daniel.dandrada@canonical.com>
19+ */
20+
21+#include "TimeSource.h"
22+
23+#include <QElapsedTimer>
24+
25+namespace UbuntuGestures {
26+class RealTimeSourcePrivate {
27+public:
28+ QElapsedTimer timer;
29+};
30+}
31+
32+using namespace UbuntuGestures;
33+
34+RealTimeSource::RealTimeSource()
35+ : UbuntuGestures::TimeSource()
36+ , d(new RealTimeSourcePrivate)
37+{
38+ d->timer.start();
39+}
40+
41+RealTimeSource::~RealTimeSource()
42+{
43+ delete d;
44+}
45+
46+qint64 RealTimeSource::msecsSinceReference()
47+{
48+ return d->timer.elapsed();
49+}
plugins/Ubuntu/Gestures/TimeSource.hnew file+54
@@ -0,0 +1,54 @@
1+/*
2+ * Copyright (C) 2013 - Canonical Ltd.
3+ *
4+ * This program is free software: you can redistribute it and/or modify it
5+ * under the terms of the GNU Lesser General Public License, as
6+ * published by the Free Software Foundation; either version 2.1 or 3.0
7+ * of the License.
8+ *
9+ * This program is distributed in the hope that it will be useful, but
10+ * WITHOUT ANY WARRANTY; without even the implied warranties of
11+ * MERCHANTABILITY, SATISFACTORY QUALITY or FITNESS FOR A PARTICULAR
12+ * PURPOSE. See the applicable version of the GNU Lesser General Public
13+ * License for more details.
14+ *
15+ * You should have received a copy of both the GNU Lesser General Public
16+ * License along with this program. If not, see <http://www.gnu.org/licenses/>
17+ *
18+ * Authored by: Daniel d'Andrada <daniel.dandrada@canonical.com>
19+ */
20+
21+#ifndef UBUNTUGESTURES_TIMESOURCE_H
22+#define UBUNTUGESTURES_TIMESOURCE_H
23+
24+#include "UbuntuGesturesGlobal.h"
25+#include <QSharedPointer>
26+
27+namespace UbuntuGestures {
28+/*
29+ Interface for a time source.
30+ */
31+class UBUNTUGESTURES_EXPORT TimeSource {
32+public:
33+ virtual ~TimeSource() {}
34+ /* Returns the current time in milliseconds since some reference time in the past. */
35+ virtual qint64 msecsSinceReference() = 0;
36+};
37+typedef QSharedPointer<TimeSource> SharedTimeSource;
38+
39+/*
40+ Implementation of a time source
41+ */
42+class RealTimeSourcePrivate;
43+class RealTimeSource : public TimeSource {
44+public:
45+ RealTimeSource();
46+ virtual ~RealTimeSource();
47+ qint64 msecsSinceReference() override;
48+private:
49+ RealTimeSourcePrivate *d;
50+};
51+
52+} // namespace UbuntuGestures
53+
54+#endif // UBUNTUGESTURES_TIMESOURCE_H
tests/plugins/Ubuntu/Gestures/DownwardsLauncher.qml+2−10
@@ -31,10 +31,7 @@ Item {
3131 y: followDragArea()
3232
3333 function followDragArea() {
34− if (dragArea.status === DirectionalDragArea.Rejected)
35− return -height
36− else
37− return dragArea.distance < height ? -height + dragArea.distance : 0
34+ return dragArea.distance < height ? -height + dragArea.distance : 0
3835 }
3936 }
4037
@@ -66,15 +63,10 @@ Item {
6663 dragAreaRect.opacity = 0.3
6764 launcher.y = Qt.binding(launcher.followDragArea)
6865 break;
69− case DirectionalDragArea.Recognized:
66+ defaut: // DirectionalDragArea.Recognized:
7067 dragAreaRect.color = "green"
7168 dragAreaRect.opacity = 0.5
7269 break;
73− default: //case DirectionalDragArea.Rejected:
74− dragAreaRect.color = "red"
75− dragAreaRect.opacity = 0.5
76− launcher.y = -launcher.height
77− break;
7870 }
7971 }
8072
tests/plugins/Ubuntu/Gestures/LeftwardsLauncher.qml+5−13
@@ -32,13 +32,10 @@ Item {
3232 y: 0
3333
3434 function followDragArea() {
35− if (dragArea.status === DirectionalDragArea.Rejected)
36− return root.width
37− else
38− return dragArea.distance > -width ?
39− root.width + dragArea.distance
40− :
41− root.width - width
35+ return dragArea.distance > -width ?
36+ root.width + dragArea.distance
37+ :
38+ root.width - width
4239 }
4340 }
4441
@@ -69,15 +66,10 @@ Item {
6966 dragAreaRect.opacity = 0.3
7067 launcher.x = Qt.binding(launcher.followDragArea)
7168 break;
72− case DirectionalDragArea.Recognized:
69+ default: //case DirectionalDragArea.Recognized:
7370 dragAreaRect.color = "green"
7471 dragAreaRect.opacity = 0.5
7572 break;
76− default: //case DirectionalDragArea.Rejected:
77− dragAreaRect.color = "red"
78− dragAreaRect.opacity = 0.5
79− launcher.x = -launcher.height
80− break;
8173 }
8274 }
8375
tests/plugins/Ubuntu/Gestures/RightwardsLauncher.qml+3−10
@@ -19,6 +19,7 @@ import Ubuntu.Gestures 0.1
1919import Ubuntu.Components 0.1
2020
2121Item {
22+ objectName: "rightwardsLauncher"
2223
2324 function reset() { launcher.x = -launcher.width }
2425
@@ -31,10 +32,7 @@ Item {
3132 y: 0
3233
3334 function followDragArea() {
34− if (dragArea.status === DirectionalDragArea.Rejected)
35− return -width
36− else
37− return dragArea.distance < width ? -width + dragArea.distance : 0
35+ return dragArea.distance < width ? -width + dragArea.distance : 0
3836 }
3937 }
4038
@@ -66,15 +64,10 @@ Item {
6664 dragAreaRect.opacity = 0.3
6765 launcher.x = Qt.binding(launcher.followDragArea)
6866 break;
69− case DirectionalDragArea.Recognized:
67+ default: //case DirectionalDragArea.Recognized:
7068 dragAreaRect.color = "green"
7169 dragAreaRect.opacity = 0.5
7270 break;
73− default: //case DirectionalDragArea.Rejected:
74− dragAreaRect.color = "red"
75− dragAreaRect.opacity = 0.5
76− launcher.x = -launcher.height
77− break;
7871 }
7972 }
8073
tests/plugins/Ubuntu/Gestures/UpwardsLauncher.qml+5−13
@@ -32,13 +32,10 @@ Item {
3232 y: root.height
3333
3434 function followDragArea() {
35− if (dragArea.status === DirectionalDragArea.Rejected)
36− return root.height
37− else
38− return dragArea.distance > -height ?
39− root.height + dragArea.distance
40− :
41− root.height - height
35+ return dragArea.distance > -height ?
36+ root.height + dragArea.distance
37+ :
38+ root.height - height
4239 }
4340 }
4441
@@ -69,15 +66,10 @@ Item {
6966 dragAreaRect.opacity = 0.3
7067 launcher.y = Qt.binding(launcher.followDragArea)
7168 break;
72− case DirectionalDragArea.Recognized:
69+ default: //case DirectionalDragArea.Recognized:
7370 dragAreaRect.color = "green"
7471 dragAreaRect.opacity = 0.5
7572 break;
76− default: //case DirectionalDragArea.Rejected:
77− dragAreaRect.color = "red"
78− dragAreaRect.opacity = 0.5
79− launcher.y = -launcher.height
80− break;
8173 }
8274 }
8375
tests/plugins/Ubuntu/Gestures/edgeDragExample.qml+7−1
@@ -33,7 +33,13 @@ Rectangle {
3333 }
3434 }
3535
36− RightwardsLauncher { id: hpLauncher; anchors.fill: parent }
36+ // NB: Do not anchor it as we will move it programmatically from the test
37+ RightwardsLauncher {
38+ id: hpLauncher;
39+ width: parent.width
40+ height: parent.height
41+ }
42+
3743 LeftwardsLauncher { id: hnLauncher; anchors.fill: parent }
3844 DownwardsLauncher { id: vpLauncher; anchors.fill: parent }
3945 UpwardsLauncher { id: vnLauncher; anchors.fill: parent }
tests/plugins/Ubuntu/Gestures/tst_AxisVelocityCalculator.cpp+3−3
@@ -54,12 +54,12 @@ private Q_SLOTS:
5454
5555private:
5656 AxisVelocityCalculator *velCalc;
57− FakeTimeSource *fakeTimeSource;
57+ QSharedPointer<FakeTimeSource> fakeTimeSource;
5858};
5959
6060void tst_AxisVelocityCalculator::init()
6161{
62− fakeTimeSource = new FakeTimeSource;
62+ fakeTimeSource.reset(new FakeTimeSource);
6363
6464 velCalc = new AxisVelocityCalculator;
6565 velCalc->setTimeSource(fakeTimeSource);
@@ -68,7 +68,7 @@ void tst_AxisVelocityCalculator::init()
6868void tst_AxisVelocityCalculator::cleanup()
6969{
7070 delete velCalc;
71− delete fakeTimeSource;
71+ fakeTimeSource.reset();
7272}
7373
7474void tst_AxisVelocityCalculator::simpleSamples()
tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.cpp+285−96
@@ -19,23 +19,38 @@
1919#include <qpa/qwindowsysteminterface.h>
2020#include <QtQuick/QQuickView>
2121#include <QtQml/QQmlEngine>
22+#include <QPointer>
2223
2324#include <DirectionalDragArea.h>
2425
25−class FakeTimer : public UbuntuGestures::AbstractTimer
26+using namespace UbuntuGestures;
27+
28+class FakeTimer : public AbstractTimer
2629{
2730 Q_OBJECT
2831public:
29− FakeTimer(QObject *parent = 0)
30− : UbuntuGestures::AbstractTimer(parent)
32+ FakeTimer(const SharedTimeSource &timeSource, QObject *parent = 0)
33+ : UbuntuGestures::AbstractTimer(parent),
34+ m_timeSource(timeSource)
3135 {}
3236
33− virtual int interval() const { return m_duration; }
34− virtual void setInterval(int msecs) { m_duration = msecs; }
37+ int interval() const override { return m_interval; }
38+ void setInterval(int msecs) override { m_interval = msecs; }
39+ void start() override {
40+ AbstractTimer::start();
41+ m_nextTimeoutTime = m_timeSource->msecsSinceReference() + (qint64)m_interval;
42+ }
43+
44+ void emitTimeout() {
45+ m_nextTimeoutTime += m_interval;
46+ Q_EMIT timeout();
47+ }
3548
36− void emitTimeout() {Q_EMIT timeout();}
49+ qint64 nextTimeoutTime() const { return m_nextTimeoutTime; }
3750private:
38− int m_duration;
51+ int m_interval;
52+ SharedTimeSource m_timeSource;
53+ qint64 m_nextTimeoutTime;
3954};
4055
4156class FakeTimeSource : public UbuntuGestures::TimeSource
@@ -67,14 +82,17 @@ private Q_SLOTS:
6782 void maxSilenceTime();
6883 void sceneXAndX();
6984 void sceneYAndY();
70− void emitDraggingChangedOnDirectRecognition();
85+ void twoFingerTap();
86+ void movingDDA();
87+ void ignoreOldFinger();
7188
7289private:
7390 QQuickView *createView();
91+ void passTime(qint64 timeSpan);
7492 QQuickView *view;
7593 QTouchDevice *device;
7694 FakeTimer *fakeTimer;
77− FakeTimeSource *fakeTimeSource;
95+ QSharedPointer<FakeTimeSource> fakeTimeSource;
7896};
7997
8098void tst_DirectionalDragArea::initTestCase()
@@ -101,8 +119,8 @@ void tst_DirectionalDragArea::init()
101119 QVERIFY(view->rootObject() != 0);
102120 qApp->processEvents();
103121
104− fakeTimer = new FakeTimer;
105− fakeTimeSource = new FakeTimeSource;
122+ fakeTimeSource.reset(new FakeTimeSource);
123+ fakeTimer = new FakeTimer(fakeTimeSource);
106124}
107125
108126void tst_DirectionalDragArea::cleanup()
@@ -113,8 +131,7 @@ void tst_DirectionalDragArea::cleanup()
113131 delete fakeTimer;
114132 fakeTimer = 0;
115133
116− delete fakeTimeSource;
117− fakeTimeSource = 0;
134+ fakeTimeSource.reset();
118135}
119136
120137QQuickView *tst_DirectionalDragArea::createView()
@@ -127,6 +144,23 @@ QQuickView *tst_DirectionalDragArea::createView()
127144 return window;
128145}
129146
147+void tst_DirectionalDragArea::passTime(qint64 timeSpan)
148+{
149+ qint64 finalTime = fakeTimeSource->m_msecsSinceReference + timeSpan;
150+
151+ if (fakeTimer->isRunning() && finalTime >= fakeTimer->nextTimeoutTime()) {
152+ fakeTimeSource->m_msecsSinceReference = fakeTimer->nextTimeoutTime();
153+ fakeTimer->emitTimeout();
154+
155+ qint64 timeSpanRemainder = finalTime - fakeTimeSource->m_msecsSinceReference;
156+ if (timeSpanRemainder > 0) {
157+ passTime(timeSpanRemainder);
158+ }
159+ } else {
160+ fakeTimeSource->m_msecsSinceReference = finalTime;
161+ }
162+}
163+
130164namespace {
131165QPointF calculateInitialTouchPos(DirectionalDragArea *edgeDragArea, QQuickView *view)
132166{
@@ -180,7 +214,7 @@ void tst_DirectionalDragArea::edgeDrag()
180214 QFETCH(QString, dragAreaObjectName);
181215 QFETCH(qreal, wideningAngleMultiplier);
182216 QFETCH(qreal, dragDistanceFactor);
183− QFETCH(int, expectedGestureRecognition);
217+ QFETCH(bool, expectGestureRecognition);
184218
185219 DirectionalDragArea *edgeDragArea =
186220 view->rootObject()->findChild<DirectionalDragArea*>(dragAreaObjectName);
@@ -196,7 +230,10 @@ void tst_DirectionalDragArea::edgeDrag()
196230 qreal desiredDragDistance = edgeDragArea->distanceThreshold()*dragDistanceFactor;
197231 QPointF dragDirectionVector = calculateDirectionVector(edgeDragArea,
198232 wideningAngleMultiplier);
199− QPointF touchMovement = dragDirectionVector * (edgeDragArea->distanceThreshold() * 0.1f);
233+ qreal movementStepDistance = edgeDragArea->distanceThreshold() * 0.1f;
234+ QPointF touchMovement = dragDirectionVector * movementStepDistance;
235+ int totalMovementSteps = qCeil(desiredDragDistance / movementStepDistance);
236+ int movementTimeStepMs = (edgeDragArea->compositionTime() * 1.5f) / totalMovementSteps;
200237
201238 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
202239
@@ -211,15 +248,16 @@ void tst_DirectionalDragArea::edgeDrag()
211248 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
212249 }
213250
214− do {
251+ for (int i = 0; i < totalMovementSteps; ++i) {
215252 touchPoint += touchMovement;
253+ passTime(movementTimeStepMs);
216254 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
217− } while ((touchPoint - initialTouchPos).manhattanLength() < desiredDragDistance);
255+ }
218256
219− if (expectedGestureRecognition)
257+ if (expectGestureRecognition)
220258 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
221259
222− if (edgeDragArea->status() == DirectionalDragArea::Rejected) {
260+ if (edgeDragArea->status() == DirectionalDragArea::WaitingForTouch) {
223261 QCOMPARE(edgeDragArea->dragging(), false);
224262 QCOMPARE(draggingSpy.count(), 2);
225263 }
@@ -235,49 +273,49 @@ void tst_DirectionalDragArea::edgeDrag_data()
235273 QTest::addColumn<QString>("dragAreaObjectName");
236274 QTest::addColumn<qreal>("wideningAngleMultiplier");
237275 QTest::addColumn<qreal>("dragDistanceFactor");
238− QTest::addColumn<int>("expectedGestureRecognition");
276+ QTest::addColumn<bool>("expectGestureRecognition");
239277
240278 QTest::newRow("rightwards, tiny drag")
241− << "hpDragArea" << 0.0 << 0.2 << 0;
279+ << "hpDragArea" << 0.0 << 0.2 << false;
242280
243281 QTest::newRow("rightwards, straight drag")
244− << "hpDragArea" << 0.0 << 3.0 << 1;
282+ << "hpDragArea" << 0.0 << 3.0 << true;
245283
246284 QTest::newRow("rightwards, diagonal drag")
247− << "hpDragArea" << 0.9 << 3.0 << 1;
285+ << "hpDragArea" << 0.9 << 3.0 << true;
248286
249287 QTest::newRow("rightwards, overly diagonal drag")
250− << "hpDragArea" << 2.0 << 3.0 << 0;
288+ << "hpDragArea" << 2.0 << 3.0 << false;
251289
252290 QTest::newRow("leftwards, tiny drag")
253− << "hnDragArea" << 0.0 << 0.2 << 0;
291+ << "hnDragArea" << 0.0 << 0.2 << false;
254292
255293 QTest::newRow("leftwards, straight drag")
256− << "hnDragArea" << 0.0 << 3.0 << 1;
294+ << "hnDragArea" << 0.0 << 3.0 << true;
257295
258296 QTest::newRow("leftwards, diagonal drag")
259− << "hnDragArea" << 0.9 << 3.0 << 1;
297+ << "hnDragArea" << 0.9 << 3.0 << true;
260298
261299 QTest::newRow("downwards, tiny drag")
262− << "vpDragArea" << 0.0 << 0.2 << 0;
300+ << "vpDragArea" << 0.0 << 0.2 << false;
263301
264302 QTest::newRow("downwards, straight drag")
265− << "vpDragArea" << 0.0 << 3.0 << 1;
303+ << "vpDragArea" << 0.0 << 3.0 << true;
266304
267305 QTest::newRow("downwards, diagonal drag")
268− << "vpDragArea" << 0.9 << 3.0 << 1;
306+ << "vpDragArea" << 0.9 << 3.0 << true;
269307
270308 QTest::newRow("upwards, tiny drag")
271− << "vnDragArea" << 0.0 << 0.2 << 0;
309+ << "vnDragArea" << 0.0 << 0.2 << false;
272310
273311 QTest::newRow("upwards, straight drag")
274− << "vnDragArea" << 0.0 << 3.0 << 1;
312+ << "vnDragArea" << 0.0 << 3.0 << true;
275313
276314 QTest::newRow("upwards, diagonal drag")
277− << "vnDragArea" << 0.9 << 3.0 << 1;
315+ << "vnDragArea" << 0.9 << 3.0 << true;
278316
279317 QTest::newRow("upwards, overly diagonal drag")
280− << "vnDragArea" << 2.0 << 3.0 << 0;
318+ << "vnDragArea" << 2.0 << 3.0 << false;
281319}
282320
283321/*
@@ -298,25 +336,33 @@ void tst_DirectionalDragArea::dragWithShortDirectionChange()
298336
299337 qreal desiredDragDistance = edgeDragArea->distanceThreshold()*2.0;
300338 QPointF dragDirectionVector(1.0, 0.0);
301− QPointF touchMovement = dragDirectionVector * (edgeDragArea->distanceThreshold() * 0.1f);
339+ qreal touchStepDistance = edgeDragArea->distanceThreshold() * 0.1f;
340+ // make sure we are above minimum speed
341+ int touchStepTimeMs = (touchStepDistance / (edgeDragArea->minSpeed() * 5.0f)) * 1000.0f;
342+ QPointF touchMovement = dragDirectionVector * touchStepDistance;
302343
303344 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
304345
305346 // Move a bit in the proper direction
306347 for (int i=0; i < 3; ++i) {
307348 touchPoint += touchMovement;
349+ passTime(touchStepTimeMs);
308350 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
309351 }
310352
311353 // Then a sudden and small movement to the opposite direction
312− touchPoint -= dragDirectionVector * (edgeDragArea->maxDeviation() * 0.7);
354+ touchPoint -= touchMovement*0.2;
355+ passTime(touchStepTimeMs*0.2);
313356 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
314357
315− // And then resume movment in the correct direction until it crosses the distance threshold.
358+ // And then resume movment in the correct direction until it crosses the distance and time
359+ // thresholds.
316360 do {
317361 touchPoint += touchMovement;
362+ passTime(touchStepTimeMs);
318363 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
319− } while ((touchPoint - initialTouchPos).manhattanLength() < desiredDragDistance);
364+ } while ((touchPoint - initialTouchPos).manhattanLength() < desiredDragDistance
365+ || fakeTimeSource->m_msecsSinceReference < (edgeDragArea->compositionTime() * 1.5f));
320366
321367 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
322368
@@ -329,7 +375,8 @@ void tst_DirectionalDragArea::dragWithShortDirectionChange()
329375 */
330376void tst_DirectionalDragArea::minSpeed()
331377{
332− QFETCH(int, minSpeedMsecsDeviation);
378+ QFETCH(qreal, minSpeed);
379+ QFETCH(qreal, speed);
333380 QFETCH(int, expectedStatusAfterSpeedCheck);
334381
335382 DirectionalDragArea *edgeDragArea =
@@ -338,44 +385,40 @@ void tst_DirectionalDragArea::minSpeed()
338385 edgeDragArea->setRecognitionTimer(fakeTimer);
339386 edgeDragArea->setTimeSource(fakeTimeSource);
340387
388+ // A really long, unattainable, number. We don't want it getting recognized before
389+ // the speed checks we want have been performed
390+ edgeDragArea->setDistanceThreshold(500000);
391+
392+ edgeDragArea->setMinSpeed(minSpeed);
393+
341394 QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view);
342395 QPointF touchPoint = initialTouchPos;
343396
344397 QPointF dragDirectionVector(1.0, 0.0);
345− qreal distanceStep = edgeDragArea->distanceThreshold() * 0.1f;
398+ qint64 timeStepMsecs = 10;
399+ qreal distanceStep = (speed / 1000.0f) * timeStepMsecs;
346400 QPointF touchMovement = dragDirectionVector * distanceStep;
347− qreal minSpeedMsecs = edgeDragArea->minSpeed() / 1000.0;
348− qint64 timeStepMsecs = qFloor(distanceStep / minSpeedMsecs) + minSpeedMsecsDeviation;
349401
350− // if it fails it means the params set in the QML file are not in harmony with the values
351− // used in this test
352− Q_ASSERT(timeStepMsecs > 0);
353−
354− fakeTimeSource->m_msecsSinceReference = 0;
355402 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
356403
357− // Move a bit in the proper direction
358− for (int i=0; i < 4; ++i) {
404+ // Move for a while to ensure our speed check is performed a couple of times
405+ for (int i=0; i < 20; ++i) {
359406 touchPoint += touchMovement;
360− fakeTimeSource->m_msecsSinceReference += timeStepMsecs;
407+ passTime(timeStepMsecs);
361408 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
362409 }
363410
364− QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
365−
366− // Force the periodic speed check.
367− fakeTimer->emitTimeout();
368−
369411 QCOMPARE((int)edgeDragArea->status(), expectedStatusAfterSpeedCheck);
370412}
371413
372414void tst_DirectionalDragArea::minSpeed_data()
373415{
374− QTest::addColumn<int>("minSpeedMsecsDeviation");
416+ QTest::addColumn<qreal>("minSpeed");
417+ QTest::addColumn<qreal>("speed");
375418 QTest::addColumn<int>("expectedStatusAfterSpeedCheck");
376419
377− QTest::newRow("slower than minSpeed") << 20 << (int)DirectionalDragArea::Rejected;
378− QTest::newRow("faster than minSpeed") << -20 << (int)DirectionalDragArea::Undecided;
420+ QTest::newRow("slower than minSpeed") << 100.0 << 50.0 << (int)DirectionalDragArea::WaitingForTouch;
421+ QTest::newRow("faster than minSpeed") << 100.0 << 150.0 << (int)DirectionalDragArea::Undecided;
379422}
380423
381424/*
@@ -391,32 +434,37 @@ void tst_DirectionalDragArea::recognitionTimerUsage()
391434 edgeDragArea->setRecognitionTimer(fakeTimer);
392435 edgeDragArea->setTimeSource(fakeTimeSource);
393436
437+ // don't let it interfere with our test
438+ edgeDragArea->setMinSpeed(0.0);
439+
440+ int timeStepMs = 5; // some arbitrary small value.
441+
394442 QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view);
395443 QPointF touchPoint = initialTouchPos;
396444
397445 QPointF dragDirectionVector(1.0, 0.0);
398446 QPointF touchMovement = dragDirectionVector * (edgeDragArea->distanceThreshold() * 0.2f);
399447
448+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
400449 QVERIFY(!fakeTimer->isRunning());
401450
402451 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
403452
453+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
404454 QVERIFY(fakeTimer->isRunning());
405455
406− // Move a bit in the proper direction
407− for (int i=0; i < 3; ++i) {
456+ // Move beyond distance threshold and composition time to ensure recognition
457+ while (fakeTimeSource->m_msecsSinceReference <= edgeDragArea->compositionTime() ||
458+ (touchPoint - initialTouchPos).manhattanLength() <= edgeDragArea->distanceThreshold()) {
459+
460+ QCOMPARE(edgeDragArea->status() == DirectionalDragArea::Undecided, fakeTimer->isRunning());
461+
408462 touchPoint += touchMovement;
463+ passTime(timeStepMs);
409464 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
410465 }
411466
412− QVERIFY(fakeTimer->isRunning());
413−
414− // Move beyond distance threshold
415− touchPoint += 3*touchMovement;
416− QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
417−
418467 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
419−
420468 QVERIFY(!fakeTimer->isRunning());
421469}
422470
@@ -432,29 +480,23 @@ void tst_DirectionalDragArea::maxSilenceTime()
432480 edgeDragArea->setRecognitionTimer(fakeTimer);
433481 edgeDragArea->setTimeSource(fakeTimeSource);
434482
483+ // Make sure this property is not disabled
484+ edgeDragArea->setMaxSilenceTime(100);
485+
435486 QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view);
436487 QPointF touchPoint = initialTouchPos;
437488
438− fakeTimeSource->m_msecsSinceReference = 0;
439489 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
440490
441491 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
492+ QVERIFY(fakeTimer->isRunning());
442493
443− int neededTimeouts = qCeil((qreal)edgeDragArea->maxSilenceTime() / (qreal)fakeTimer->interval());
444− if ((edgeDragArea->maxSilenceTime() % fakeTimer->interval()) == 0) {
445− ++neededTimeouts;
446− }
447−
448− // Force the periodic speed check.
449− for (int i = 0; i < neededTimeouts; ++i) {
450− fakeTimer->emitTimeout();
451−
452− if (i < neededTimeouts - 1) {
453− QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
454− }
494+ // Force timer to timeout until after maxSilenceTime has been reached
495+ while (fakeTimeSource->m_msecsSinceReference < edgeDragArea->maxSilenceTime()) {
496+ passTime(fakeTimer->interval());
455497 }
456498
457− QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Rejected);
499+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
458500}
459501
460502/*
@@ -471,7 +513,6 @@ void tst_DirectionalDragArea::sceneXAndX()
471513
472514 QPointF touchScenePos(view->width() - (edgeDragArea->width()/2.0f), view->height()/2.0f);
473515
474− fakeTimeSource->m_msecsSinceReference = 0;
475516 QTest::touchEvent(view, device).press(0, touchScenePos.toPoint());
476517
477518 QSignalSpy touchXSpy(edgeDragArea, SIGNAL(touchXChanged(qreal)));
@@ -500,7 +541,6 @@ void tst_DirectionalDragArea::sceneYAndY()
500541
501542 QPointF touchScenePos(view->width()/2.0f, view->height() - (edgeDragArea->height()/2.0f));
502543
503− fakeTimeSource->m_msecsSinceReference = 0;
504544 QTest::touchEvent(view, device).press(0, touchScenePos.toPoint());
505545
506546 QSignalSpy touchYSpy(edgeDragArea, SIGNAL(touchYChanged(qreal)));
@@ -516,11 +556,12 @@ void tst_DirectionalDragArea::sceneYAndY()
516556}
517557
518558/*
519− Emit draggingChanged() when status change from WaitingForTouch directly to Recognized.
520− This happens when the gesture recognition is effectively disabled (e.g. by setting a
521− distanceThreshold of zero)
559+ Regression test for https://bugs.launchpad.net/bugs/1228336
560+
561+ Perform a quick two-finger double-tap and check that DirectionalDragArea properly
562+ rejects those touch points. In the bug above it got confused and crashed.
522563 */
523−void tst_DirectionalDragArea::emitDraggingChangedOnDirectRecognition()
564+void tst_DirectionalDragArea::twoFingerTap()
524565{
525566 DirectionalDragArea *edgeDragArea =
526567 view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
@@ -528,22 +569,170 @@ void tst_DirectionalDragArea::emitDraggingChangedOnDirectRecognition()
528569 edgeDragArea->setRecognitionTimer(fakeTimer);
529570 edgeDragArea->setTimeSource(fakeTimeSource);
530571
531− QPointF touchPos = calculateInitialTouchPos(edgeDragArea, view);
572+ // Make touches evenly spaced along the edgeDragArea
573+ QPoint touchAPos(edgeDragArea->width()/2.0f, view->height()*0.33f);
574+ QPoint touchBPos(edgeDragArea->width()/2.0f, view->height()*0.66f);
532575
533− QSignalSpy draggingSpy(edgeDragArea, SIGNAL(draggingChanged(bool)));
576+ qint64 timeStepMsecs = 5; // some arbitrary, small value
534577
535− // That will make it completetly skip the recognition phase ("Undecided" status).
536− edgeDragArea->setDistanceThreshold(0.0);
578+ // Perform the first two-finger tap
579+ // NB: using move() instead of stationary() becasue in the latter you cannot provide
580+ // the touch position and therefore it's left with some garbage numbers.
581+ QTest::touchEvent(view, device)
582+ .press(0, touchAPos);
537583
584+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
585+
586+ passTime(timeStepMsecs);
587+ QTest::touchEvent(view, device)
588+ .move(0, touchAPos)
589+ .press(1, touchBPos);
590+
591+ // A second touch point appeared during recognition, reject immediately as this
592+ // can't be a single-touch gesture anymore.
538593 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
539− QCOMPARE(edgeDragArea->dragging(), false);
540594
541− fakeTimeSource->m_msecsSinceReference = 0;
542− QTest::touchEvent(view, device).press(0, touchPos.toPoint());
595+ passTime(timeStepMsecs);
596+ QTest::touchEvent(view, device)
597+ .release(0, touchAPos)
598+ .move(1, touchBPos);
599+
600+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
601+
602+ passTime(timeStepMsecs);
603+ QTest::touchEvent(view, device)
604+ .release(1, touchBPos);
605+
606+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
607+
608+ // Perform the second two-finger tap
609+
610+ passTime(timeStepMsecs);
611+ QTest::touchEvent(view, device)
612+ .press(0, touchAPos);
613+
614+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
615+
616+ passTime(timeStepMsecs);
617+ QTest::touchEvent(view, device)
618+ .move(0, touchAPos)
619+ .press(1, touchBPos);
620+
621+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
622+
623+ passTime(timeStepMsecs);
624+ QTest::touchEvent(view, device)
625+ .release(0, touchAPos)
626+ .move(1, touchBPos);
627+
628+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
629+
630+ passTime(timeStepMsecs);
631+ QTest::touchEvent(view, device)
632+ .release(1, touchBPos);
633+
634+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
635+}
636+
637+/*
638+ Tests that gesture recognition works normally even if the DirectionalDragArea moves
639+ during recognition.
640+ This effectively means that we have to do gesture recognition with scene coordinates
641+ instead of local item coordinates.
642+ */
643+void tst_DirectionalDragArea::movingDDA()
644+{
645+ QQuickItem *rightwardsLauncher = view->rootObject()->findChild<QQuickItem*>("rightwardsLauncher");
646+ Q_ASSERT(rightwardsLauncher != 0);
647+
648+ DirectionalDragArea *edgeDragArea =
649+ rightwardsLauncher->findChild<DirectionalDragArea*>("hpDragArea");
650+ Q_ASSERT(edgeDragArea != 0);
651+ edgeDragArea->setRecognitionTimer(fakeTimer);
652+ edgeDragArea->setTimeSource(fakeTimeSource);
653+
654+ QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view);
655+ QPointF touchPoint = initialTouchPos;
656+
657+ qreal desiredDragDistance = edgeDragArea->distanceThreshold()*2.0f;
658+ QPointF dragDirectionVector(1.0f, 0.0f);
659+
660+ qreal movementStepDistance = edgeDragArea->distanceThreshold() * 0.1f;
661+ QPointF touchMovement = dragDirectionVector * movementStepDistance;
662+ int totalMovementSteps = qCeil(desiredDragDistance / movementStepDistance);
663+ int movementTimeStepMs = (edgeDragArea->compositionTime() * 1.5f) / totalMovementSteps;
664+
665+ QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
666+
667+ // Move it far ahead along the direction of the gesture
668+ // rightwardsLauncher is a parent of our DirectionalDragArea. So moving it will move our DDA
669+ rightwardsLauncher->setX(rightwardsLauncher->x() + edgeDragArea->distanceThreshold() * 5.0f);
670+
671+ for (int i = 0; i < totalMovementSteps; ++i) {
672+ touchPoint += touchMovement;
673+ passTime(movementTimeStepMs);
674+ QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
675+ }
543676
544677 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
545− QCOMPARE(draggingSpy.count(), 1);
546− QCOMPARE(edgeDragArea->dragging(), true);
678+
679+ QTest::touchEvent(view, device).release(0, touchPoint.toPoint());
680+}
681+
682+/*
683+ The presence of and old, rejected, touch point lying on the DirectionalDragArea
684+ shouldn't impede the recognition of a gesture from a new touch point.
685+ */
686+void tst_DirectionalDragArea::ignoreOldFinger()
687+{
688+ DirectionalDragArea *edgeDragArea =
689+ view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
690+ Q_ASSERT(edgeDragArea != 0);
691+ edgeDragArea->setRecognitionTimer(fakeTimer);
692+ edgeDragArea->setTimeSource(fakeTimeSource);
693+
694+ // Make touches evenly spaced along the edgeDragArea
695+ QPoint touch0Pos(edgeDragArea->width()/2.0f, view->height()*0.33f);
696+ QPoint touch1Pos(edgeDragArea->width()/2.0f, view->height()*0.66f);
697+
698+ QTest::touchEvent(view, device).press(0, touch0Pos);
699+
700+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
701+
702+ // leave it lying around for some time
703+ passTime(qMax(edgeDragArea->maxSilenceTime(), edgeDragArea->compositionTime()) * 10);
704+
705+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
706+
707+ qreal desiredDragDistance = edgeDragArea->distanceThreshold()*2.0f;
708+ QPointF dragDirectionVector(1.0f, 0.0f);
709+
710+ qreal movementStepDistance = edgeDragArea->distanceThreshold() * 0.1f;
711+ QPointF touchMovement = dragDirectionVector * movementStepDistance;
712+ int totalMovementSteps = qCeil(desiredDragDistance / movementStepDistance);
713+ int movementTimeStepMs = (edgeDragArea->compositionTime() * 1.5f) / totalMovementSteps;
714+
715+ QTest::touchEvent(view, device)
716+ .move(0, touch0Pos)
717+ .press(1, touch1Pos);
718+
719+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
720+
721+ for (int i = 0; i < totalMovementSteps; ++i) {
722+ touch1Pos += touchMovement.toPoint();
723+ passTime(movementTimeStepMs);
724+ QTest::touchEvent(view, device)
725+ .move(0, touch0Pos)
726+ .move(1, touch1Pos);
727+ }
728+
729+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
730+
731+ QTest::touchEvent(view, device)
732+ .move(0, touch0Pos)
733+ .release(1, touch1Pos);
734+
735+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
547736}
548737
549738QTEST_MAIN(tst_DirectionalDragArea)
tests/qmltests/Components/tst_DragHandle.cpp+4−5
@@ -41,7 +41,7 @@ class FakeTimeSource : public UbuntuGestures::TimeSource
4141{
4242public:
4343 FakeTimeSource() { m_msecsSinceReference = 0; }
44− virtual qint64 msecsSinceReference() {return m_msecsSinceReference;}
44+ qint64 msecsSinceReference() override {return m_msecsSinceReference;}
4545 qint64 m_msecsSinceReference;
4646};
4747
@@ -73,7 +73,7 @@ private:
7373 QQuickView *m_view;
7474 QTouchDevice *m_device;
7575 FakeTimer *m_fakeTimer;
76− FakeTimeSource *m_fakeTimeSource;
76+ QSharedPointer<FakeTimeSource> m_fakeTimeSource;
7777};
7878
7979
@@ -102,7 +102,7 @@ void tst_DragHandle::init()
102102 qApp->processEvents();
103103
104104 m_fakeTimer = new FakeTimer;
105− m_fakeTimeSource = new FakeTimeSource;
105+ m_fakeTimeSource.reset(new FakeTimeSource);
106106}
107107
108108void tst_DragHandle::cleanup()
@@ -113,8 +113,7 @@ void tst_DragHandle::cleanup()
113113 delete m_fakeTimer;
114114 m_fakeTimer = 0;
115115
116− delete m_fakeTimeSource;
117− m_fakeTimeSource = 0;
116+ m_fakeTimeSource.reset();
118117}
119118
120119QQuickView *tst_DragHandle::createView()
tests/qmltests/Panel/tst_Panel.qml−1
@@ -52,7 +52,6 @@ Item {
5252 {tag: "fullscreen", fullscreenFlag: true }
5353 ]}
5454
55−
5655 function init() {
5756 searchClicked = false;
5857 panel.indicators.hide();
tests/qmltests/tst_Shell.qml+35−12
@@ -55,19 +55,42 @@ Item {
5555 when: windowShown
5656
5757 function initTestCase() {
58− swipeAwayGreeter();
58+ var ok = false;
59+ var attempts = 0;
60+ var maxAttempts = 1000;
61+
62+ // Qt loads a qml scene asynchronously. So early on, some findChild() calls made in
63+ // tests may fail because the desired child item wasn't loaded yet.
64+ // Thus here we try to ensure the scene has been fully loaded before proceeding with the tests.
65+ // As I couldn't find an API in QQuickView & friends to tell me that the scene is 100% loaded
66+ // (all items instantiated, etc), I resort to checking the existence of some key items until
67+ // repeatedly until they're all there.
68+ do {
69+ var dashContentList = findChild(shell, "dashContentList");
70+ waitForRendering(dashContentList);
71+ var homeLoader = findChild(dashContentList, "home.scope loader");
72+ ok = homeLoader !== undefined
73+ && homeLoader.item !== undefined;
5974
60− // Ensure DashHome is loaded before continuing
61− var dashContentList = findChild(shell, "dashContentList");
62− waitForRendering(dashContentList);
63− var homeLoader = findChild(dashContentList, "home.scope loader");
64− verify(homeLoader);
65− tryCompareFunction(function() {return homeLoader.item !== undefined;}, true);
66−
67− // FIXME: Desperate measure to ensure Jenkins has the scene graph fully loaded
68− // before it calls the test functions (otherwise findChild calls will fail).
69− // The code above just didn't solve it.
70− wait(2000);
75+ var greeter = findChild(shell, "greeter");
76+ ok &= greeter !== undefined;
77+
78+ var launcherPanel = findChild(shell, "launcherPanel");
79+ ok &= launcherPanel !== undefined;
80+
81+ attempts++;
82+ if (!ok) {
83+ console.log("Attempt " + attempts + " failed. Waiting a bit before trying again.");
84+ // wait a bit before retrying
85+ wait(100);
86+ } else {
87+ console.log("All seem fine after " + attempts + " attempts.");
88+ }
89+ } while (!ok && attempts <= maxAttempts);
90+
91+ verify(ok);
92+
93+ swipeAwayGreeter();
7194 }
7295
7396 function cleanup() {
tests/utils/modules/Unity/Test/UnityTestCase.qml+34
@@ -14,6 +14,7 @@
1414 * along with this program. If not, see <http://www.gnu.org/licenses/>.
1515 */
1616
17+import QtQuick 2.0
1718import QtTest 1.0
1819import Ubuntu.Components 0.1
1920import Unity.Test 0.1 as UT
@@ -297,4 +298,37 @@ TestCase {
297298 event.release(0 /* touchId */, x, y)
298299 event.commit()
299300 }
301+
302+ Component.onCompleted: {
303+ var rootItem = parent;
304+ while (rootItem.parent != undefined) {
305+ rootItem = rootItem.parent;
306+ }
307+ removeTimeConstraintsFromDirectionalDragAreas(rootItem);
308+ }
309+
310+ /*
311+ In qmltests, sequences of touch events are sent all at once, unlike in "real life".
312+ Also qmltests might run really slowly, e.g. when run from inside virtual machines.
313+ Thus to remove a variable that qmltests cannot really control, namely time, this
314+ function removes all constraints from DirectionalDragAreas that are sensible to
315+ elapsed time.
316+
317+ This effectively makes DirectionalDragAreas easier to fool.
318+ */
319+ function removeTimeConstraintsFromDirectionalDragAreas(item) {
320+
321+ // use duck-typing to identify a DirectionalDragArea
322+ if (item.minSpeed != undefined
323+ && item.maxSilenceTime != undefined
324+ && item.compositionTime != undefined) {
325+ item.minSpeed = 0;
326+ item.maxSilenceTime = 60 * 60 * 1000;
327+ item.compositionTime = 0;
328+ } else {
329+ for (var i in item.children) {
330+ removeTimeConstraintsFromDirectionalDragAreas(item.children[i]);
331+ }
332+ }
333+ }
300334}