on xenial help sign in
Improve DirectionalDragArea
ed260ebDaniel d'Andrada
Components/DragHandle.qml+20−21
@@ -48,9 +48,12 @@ EdgeDragArea {
48 id: dragArea
48 id: dragArea
49 objectName: "dragHandle"
49 objectName: "dragHandle"
50
50
51− // Disable gesture detection by default when hinting is used.
51+ // Disable most of the gesture recognition parameters by default when hinting is used as
52− // It doesn't make sense to have both.
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.
53− distanceThreshold: hintDisplacement > 0 ? 0 : units.gu(1.5)
54+ distanceThreshold: hintDisplacement > 0 ? 0 : defaultDistanceThreshold
55+ maxSilenceTime: hintDisplacement > 0 ? 60*60*1000 : defaultMaxSilenceTime
56+ maxDeviation: hintDisplacement > 0 ? 999999 : defaultMaxDeviation
54
57
55 property bool stretch: false
58 property bool stretch: false
56
59
@@ -181,29 +184,25 @@ EdgeDragArea {
181 }
184 }
182 }
185 }
183
186
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−
195 onStatusChanged: {
187 onStatusChanged: {
196 if (status === DirectionalDragArea.WaitingForTouch) {
188 if (status === DirectionalDragArea.WaitingForTouch) {
189+ hintingAnimation.stop();
197 if (d.previousStatus === DirectionalDragArea.Recognized) {
190 if (d.previousStatus === DirectionalDragArea.Recognized) {
198 d.onFinishedRecognizedGesture();
191 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();
199 }
205 }
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];
207 }
206 }
208
207
209 d.previousStatus = status;
208 d.previousStatus = status;
Components/EdgeDragArea.qml+12−4
@@ -29,8 +29,16 @@ DirectionalDragArea {
29 //
29 //
30 // The idea here is that it's better having lax rules than false negatives.
30 // The idea here is that it's better having lax rules than false negatives.
31 // False negatives are very frustrating to the user.
31 // False negatives are very frustrating to the user.
32− maxDeviation: units.gu(3)
32+ maxDeviation: defaultMaxDeviation
33− wideningAngle: 50
33+ wideningAngle: defaultWideningAngle
34− distanceThreshold: units.gu(1.5)
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)
35− minSpeed: units.gu(0) // some people were getting false negatives with it enabled.
41+ // some people were getting false negatives with it enabled.
42+ readonly property real defaultMinSpeed: units.gu(0)
43+ readonly property int defaultMaxSilenceTime: 200
36}
44}
Components/Stage.qml+8−7
@@ -482,19 +482,20 @@ Showable {
482 switchToApplicationAnimationController.progress += 0.4 * delta
482 switchToApplicationAnimationController.progress += 0.4 * delta
483 }
483 }
484
484
485− property bool gotRejected: false
485+ property int lastStatus: -1
486
486
487 onStatusChanged: {
487 onStatusChanged: {
488 if (status == DirectionalDragArea.Undecided) {
488 if (status == DirectionalDragArea.Undecided) {
489− gotRejected = false
490 onUndecided();
489 onUndecided();
491− } else if (status == DirectionalDragArea.Rejected) {
492− switchToApplicationAnimationController.completeToBeginningWithSignal();
493− gotRejected = true;
494 } else if (status == DirectionalDragArea.WaitingForTouch) {
490 } else if (status == DirectionalDragArea.WaitingForTouch) {
495− if (!gotRejected)
491+ if (lastStatus == DirectionalDragArea.Undecided) {
492+ // got rejected
493+ switchToApplicationAnimationController.completeToBeginningWithSignal();
494+ } else {
496− onGestureEnded()
495+ onGestureEnded();
496+ }
497 }
497 }
498+ lastStatus = status;
498 }
499 }
499
500
500 function onUndecided() {
501 function onUndecided() {
plugins/Ubuntu/Gestures/AxisVelocityCalculator.cpp+9−18
@@ -21,25 +21,19 @@
21#include "AxisVelocityCalculator.h"
21#include "AxisVelocityCalculator.h"
22#include <QtCore/QElapsedTimer>
22#include <QtCore/QElapsedTimer>
23
23
24−class RealTimeSource : public UbuntuGestures::TimeSource {
25−public:
26− RealTimeSource()
27− : UbuntuGestures::TimeSource() {
24+using namespace UbuntuGestures;
28− m_timer.start();
29− }
30− virtual qint64 msecsSinceReference() {
31− return m_timer.elapsed();
32− }
33−private:
34− QElapsedTimer m_timer;
35−};
36
25
37AxisVelocityCalculator::AxisVelocityCalculator(QObject *parent)
26AxisVelocityCalculator::AxisVelocityCalculator(QObject *parent)
27+ : AxisVelocityCalculator(SharedTimeSource(new RealTimeSource), parent)
28+{
29+}
30+
31+AxisVelocityCalculator::AxisVelocityCalculator(const SharedTimeSource &timeSource,
32+ QObject *parent)
38 : QObject(parent)
33 : QObject(parent)
39− , m_timeSource(new RealTimeSource)
34+ , m_timeSource(timeSource)
40 , m_trackedPosition(0.0)
35 , m_trackedPosition(0.0)
41{
36{
42− m_timeSource->setParent(this);
43 reset();
37 reset();
44}
38}
45
39
@@ -142,11 +136,8 @@ int AxisVelocityCalculator::numSamples() const
142 }
136 }
143}
137}
144
138
145−void AxisVelocityCalculator::setTimeSource(UbuntuGestures::TimeSource *timeSource)
139+void AxisVelocityCalculator::setTimeSource(const SharedTimeSource &timeSource)
146{
140{
147− if (m_timeSource && m_timeSource->parent() == this)
148− delete m_timeSource;
149−
150 m_timeSource = timeSource;
141 m_timeSource = timeSource;
151
142
152 if (numSamples() > 0) {
143 if (numSamples() > 0) {
plugins/Ubuntu/Gestures/AxisVelocityCalculator.h+9−16
@@ -24,19 +24,7 @@
24#include "UbuntuGesturesGlobal.h"
24#include "UbuntuGesturesGlobal.h"
25#include <stdint.h>
25#include <stdint.h>
26#include <QtCore/QObject>
26#include <QtCore/QObject>
27−
27+#include "TimeSource.h"
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−}
40
28
41/*
29/*
42 Estimates the current velocity of a finger based on recent movement along an axis
30 Estimates the current velocity of a finger based on recent movement along an axis
@@ -73,10 +61,15 @@ class UBUNTUGESTURES_EXPORT AxisVelocityCalculator : public QObject
73public:
61public:
74
62
75 /*
63 /*
76− Regular constructor
64+ Regular, simple, constructor
77 */
65 */
78 AxisVelocityCalculator(QObject *parent = 0);
66 AxisVelocityCalculator(QObject *parent = 0);
79
67
68+ /*
69+ Constructor that takes a TimeSource
70+ */
71+ AxisVelocityCalculator(const UbuntuGestures::SharedTimeSource &timeSource, QObject *parent = 0);
72+
80 virtual ~AxisVelocityCalculator();
73 virtual ~AxisVelocityCalculator();
81
74
82 qreal trackedPosition() const;
75 qreal trackedPosition() const;
@@ -97,7 +90,7 @@ public:
97 /*
90 /*
98 Replaces the TimeSource with the given one. Useful for testing purposes.
91 Replaces the TimeSource with the given one. Useful for testing purposes.
99 */
92 */
100− void setTimeSource(UbuntuGestures::TimeSource *timeSource);
93+ void setTimeSource(const UbuntuGestures::SharedTimeSource &timeSource);
101
94
102 /*
95 /*
103 The minimum amount of samples needed for a velocity calculation.
96 The minimum amount of samples needed for a velocity calculation.
@@ -145,7 +138,7 @@ private:
145 int m_samplesRead; /* index of the oldest sample available. -1 if buffer is empty */
138 int m_samplesRead; /* index of the oldest sample available. -1 if buffer is empty */
146 int m_samplesWrite; /* index where the next sample will be written */
139 int m_samplesWrite; /* index where the next sample will be written */
147
140
148− UbuntuGestures::TimeSource *m_timeSource;
141+ UbuntuGestures::SharedTimeSource m_timeSource;
149
142
150 qreal m_trackedPosition;
143 qreal m_trackedPosition;
151};
144};
plugins/Ubuntu/Gestures/CMakeLists.txt+1
@@ -6,6 +6,7 @@ set(UbuntuGestureQml_SOURCES
6 AxisVelocityCalculator.cpp
6 AxisVelocityCalculator.cpp
7 Direction.cpp
7 Direction.cpp
8 DirectionalDragArea.cpp
8 DirectionalDragArea.cpp
9+ TimeSource.cpp
9)
10)
10
11
11add_definitions(-DUBUNTUGESTURES_LIBRARY)
12add_definitions(-DUBUNTUGESTURES_LIBRARY)
plugins/Ubuntu/Gestures/DirectionalDragArea.cpp+307−71
@@ -18,6 +18,75 @@
18
18
19#include <QtCore/qmath.h>
19#include <QtCore/qmath.h>
20#include <QtCore/QTimer>
20#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
21
90
22// Essentially a QTimer wrapper
91// Essentially a QTimer wrapper
23class RecognitionTimer : public UbuntuGestures::AbstractTimer
92class RecognitionTimer : public UbuntuGestures::AbstractTimer
@@ -48,9 +117,12 @@ DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
48 , m_minSpeed(0)
117 , m_minSpeed(0)
49 , m_maxSilenceTime(200)
118 , m_maxSilenceTime(200)
50 , m_silenceTime(0)
119 , m_silenceTime(0)
120+ , m_compositionTime(60)
51 , m_numSamplesOnLastSpeedCheck(0)
121 , m_numSamplesOnLastSpeedCheck(0)
52 , m_recognitionTimer(0)
122 , m_recognitionTimer(0)
53 , m_velocityCalculator(0)
123 , m_velocityCalculator(0)
124+ , m_timeSource(new RealTimeSource)
125+ , m_activeTouches(m_timeSource)
54{
126{
55 setRecognitionTimer(new RecognitionTimer(this));
127 setRecognitionTimer(new RecognitionTimer(this));
56 m_recognitionTimer->setInterval(60);
128 m_recognitionTimer->setInterval(60);
@@ -73,8 +145,8 @@ void DirectionalDragArea::setDirection(Direction::Type direction)
73
145
74void DirectionalDragArea::setMaxDeviation(qreal value)
146void DirectionalDragArea::setMaxDeviation(qreal value)
75{
147{
76− if (m_dampedPos.maxDelta() != value) {
148+ if (m_dampedScenePos.maxDelta() != value) {
77− m_dampedPos.setMaxDelta(value);
149+ m_dampedScenePos.setMaxDelta(value);
78 Q_EMIT maxDeviationChanged(value);
150 Q_EMIT maxDeviationChanged(value);
79 }
151 }
80}
152}
@@ -118,6 +190,14 @@ void DirectionalDragArea::setMaxSilenceTime(int value)
118 }
190 }
119}
191}
120
192
193+void DirectionalDragArea::setCompositionTime(int value)
194+{
195+ if (m_compositionTime != value) {
196+ m_compositionTime = value;
197+ Q_EMIT compositionTimeChanged(value);
198+ }
199+}
200+
121void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
201void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
122{
202{
123 int interval = 0;
203 int interval = 0;
@@ -141,9 +221,11 @@ void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *tim
141 }
221 }
142}
222}
143
223
144−void DirectionalDragArea::setTimeSource(UbuntuGestures::TimeSource *timeSource)
224+void DirectionalDragArea::setTimeSource(const SharedTimeSource &timeSource)
145{
225{
226+ m_timeSource = timeSource;
146 m_velocityCalculator->setTimeSource(timeSource);
227 m_velocityCalculator->setTimeSource(timeSource);
228+ m_activeTouches.m_timeSource = timeSource;
147}
229}
148
230
149qreal DirectionalDragArea::distance() const
231qreal DirectionalDragArea::distance() const
@@ -186,6 +268,12 @@ qreal DirectionalDragArea::touchSceneY() const
186
268
187void DirectionalDragArea::touchEvent(QTouchEvent *event)
269void DirectionalDragArea::touchEvent(QTouchEvent *event)
188{
270{
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+
189 if (!isEnabled() || !isVisible()) {
277 if (!isEnabled() || !isVisible()) {
190 QQuickItem::touchEvent(event);
278 QQuickItem::touchEvent(event);
191 return;
279 return;
@@ -198,76 +286,124 @@ void DirectionalDragArea::touchEvent(QTouchEvent *event)
198 case Undecided:
286 case Undecided:
199 touchEvent_undecided(event);
287 touchEvent_undecided(event);
200 break;
288 break;
201− case Recognized:
289+ default: // Recognized:
202 touchEvent_recognized(event);
290 touchEvent_recognized(event);
203 break;
291 break;
204− default: // Rejected
205− touchEvent_rejected(event);
206− break;
207 }
292 }
293+
294+ m_activeTouches.update(event);
208}
295}
209
296
210void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
297void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
211{
298{
212− if ((event->touchPointStates() && (Qt::TouchPointPressed || Qt::TouchPointMoved))
299+ if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
213− && event->touchPoints().count() == 1) {
300+ // Nothing to see here. No touch starting in this event.
214− const QTouchEvent::TouchPoint &touchPoint = event->touchPoints()[0];
301+ return;
215− m_startPos = touchPoint.pos();
302+ }
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);
225
303
226− if (m_distanceThreshold > 0)
304+ if (isWithinTouchCompositionWindow()) {
227− setStatus(Undecided);
305+ // too close to the last touch start. So we consider them as starting roughly at the same time.
228− else
306+ // Can't be a single-touch gesture.
229− setStatus(Recognized);
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;
230 }
311 }
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);
231}
346}
232
347
233void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
348void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
234{
349{
235 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
350 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
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+
236− const QPointF &touchPos = touchPoint->pos();
360+ const QPointF &touchScenePos = touchPoint->scenePos();
237
361
238 if (touchPoint->state() == Qt::TouchPointReleased) {
362 if (touchPoint->state() == Qt::TouchPointReleased) {
239 // touch has ended before recognition concluded
363 // touch has ended before recognition concluded
364+ DDA_DEBUG("Touch has ended before recognition concluded");
240 setStatus(WaitingForTouch);
365 setStatus(WaitingForTouch);
241 return;
366 return;
242 }
367 }
243
368
244− if (event->touchPointStates().testFlag(Qt::TouchPointPressed)
369+ if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
245− || event->touchPoints().count() > 1) {
246 // multi-finger drags are not accepted
370 // multi-finger drags are not accepted
371+ DDA_DEBUG("Multi-finger drags are not accepted");
247− setStatus(Rejected);
372+ setStatus(WaitingForTouch);
248 return;
373 return;
249 }
374 }
250
375
251− m_previousDampedPos.setX(m_dampedPos.x());
376+ m_previousDampedScenePos.setX(m_dampedScenePos.x());
252− m_previousDampedPos.setY(m_dampedPos.y());
377+ m_previousDampedScenePos.setY(m_dampedScenePos.y());
253− m_dampedPos.update(touchPos);
378+ m_dampedScenePos.update(touchScenePos);
254− updateVelocityCalculator(touchPos);
379+ updateVelocityCalculator(touchScenePos);
255
380
256 if (!pointInsideAllowedArea()) {
381 if (!pointInsideAllowedArea()) {
382+ DDA_DEBUG("Rejecting gesture because touch point is outside allowed area.");
257− setStatus(Rejected);
383+ setStatus(WaitingForTouch);
258 return;
384 return;
259 }
385 }
260
386
261 if (!movingInRightDirection()) {
387 if (!movingInRightDirection()) {
388+ DDA_DEBUG("Rejecting gesture becauuse touch point is moving in the wrong direction.");
262− setStatus(Rejected);
389+ setStatus(WaitingForTouch);
263 return;
390 return;
264 }
391 }
265
392
266− setPreviousPos(touchPos);
393+ setPreviousPos(touchPoint->pos());
267− setPreviousScenePos(touchPoint->scenePos());
394+ setPreviousScenePos(touchScenePos);
268
395
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+
269− if (movedFarEnough(touchPos)) {
403+ if (movedFarEnough(touchScenePos)) {
270 setStatus(Recognized);
404 setStatus(Recognized);
405+ } else {
406+ DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
271 }
407 }
272}
408}
273
409
@@ -275,20 +411,18 @@ void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
275{
411{
276 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
412 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
277
413
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.";
282 setStatus(WaitingForTouch);
418 setStatus(WaitingForTouch);
283− }
419+ } else {
284−}
420+ setPreviousPos(touchPoint->pos());
285−
421+ setPreviousScenePos(touchPoint->scenePos());
286−void DirectionalDragArea::touchEvent_rejected(QTouchEvent *event)
287−{
288− const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
289
422
290− if (!touchPoint || touchPoint->state() == Qt::TouchPointReleased) {
423+ if (touchPoint->state() == Qt::TouchPointReleased) {
291− setStatus(WaitingForTouch);
424+ setStatus(WaitingForTouch);
425+ }
292 }
426 }
293}
427}
294
428
@@ -307,8 +441,8 @@ const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouch
307
441
308bool DirectionalDragArea::pointInsideAllowedArea() const
442bool DirectionalDragArea::pointInsideAllowedArea() const
309{
443{
310− qreal dX = m_dampedPos.x() - m_startPos.x();
444+ qreal dX = m_dampedScenePos.x() - m_startScenePos.x();
311− qreal dY = m_dampedPos.y() - m_startPos.y();
445+ qreal dY = m_dampedScenePos.y() - m_startScenePos.y();
312
446
313 switch (m_direction) {
447 switch (m_direction) {
314 case Direction::Upwards:
448 case Direction::Upwards:
@@ -326,22 +460,26 @@ bool DirectionalDragArea::movingInRightDirection() const
326{
460{
327 switch (m_direction) {
461 switch (m_direction) {
328 case Direction::Upwards:
462 case Direction::Upwards:
329− return m_dampedPos.y() <= m_previousDampedPos.y();
463+ return m_dampedScenePos.y() <= m_previousDampedScenePos.y();
330 case Direction::Downwards:
464 case Direction::Downwards:
331− return m_dampedPos.y() >= m_previousDampedPos.y();
465+ return m_dampedScenePos.y() >= m_previousDampedScenePos.y();
332 case Direction::Leftwards:
466 case Direction::Leftwards:
333− return m_dampedPos.x() <= m_previousDampedPos.x();
467+ return m_dampedScenePos.x() <= m_previousDampedScenePos.x();
334 default: // Direction::Rightwards:
468 default: // Direction::Rightwards:
335− return m_dampedPos.x() >= m_previousDampedPos.x();
469+ return m_dampedScenePos.x() >= m_previousDampedScenePos.x();
336 }
470 }
337}
471}
338
472
339bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
473bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
340{
474{
475+ if (m_distanceThreshold > 0) {
341− if (Direction::isHorizontal(m_direction))
476+ if (Direction::isHorizontal(m_direction))
342− return qFabs(point.x() - m_startPos.x()) > m_distanceThreshold;
477+ return qFabs(point.x() - m_startScenePos.x()) > m_distanceThreshold;
343− else
478+ else
344− return qFabs(point.y() - m_startPos.y()) > m_distanceThreshold;
479+ return qFabs(point.y() - m_startScenePos.y()) > m_distanceThreshold;
480+ } else {
481+ return true;
482+ }
345}
483}
346
484
347void DirectionalDragArea::checkSpeed()
485void DirectionalDragArea::checkSpeed()
@@ -351,7 +489,8 @@ void DirectionalDragArea::checkSpeed()
351 qreal minSpeedMsecs = m_minSpeed / 1000.0;
489 qreal minSpeedMsecs = m_minSpeed / 1000.0;
352
490
353 if (speed < minSpeedMsecs) {
491 if (speed < minSpeedMsecs) {
492+ DDA_DEBUG("Rejecting gesture because it's below minimum speed.");
354− setStatus(Rejected);
493+ setStatus(WaitingForTouch);
355 }
494 }
356 }
495 }
357
496
@@ -359,7 +498,8 @@ void DirectionalDragArea::checkSpeed()
359 m_silenceTime += m_recognitionTimer->interval();
498 m_silenceTime += m_recognitionTimer->interval();
360
499
361 if (m_silenceTime > m_maxSilenceTime) {
500 if (m_silenceTime > m_maxSilenceTime) {
501+ DDA_DEBUG("Rejecting gesture because it's silence time has been exceeded.");
362− setStatus(Rejected);
502+ setStatus(WaitingForTouch);
363 }
503 }
364 } else {
504 } else {
365 m_silenceTime = 0;
505 m_silenceTime = 0;
@@ -381,11 +521,13 @@ void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
381 m_status = newStatus;
521 m_status = newStatus;
382 Q_EMIT statusChanged(m_status);
522 Q_EMIT statusChanged(m_status);
383
523
524+ #if DIRECTIONALDRAGAREA_DEBUG
525+ qDebug() << "[DDA]" << statusToString(oldStatus) << "->" << statusToString(newStatus);
526+ #endif
527+
384 switch (newStatus) {
528 switch (newStatus) {
385 case WaitingForTouch:
529 case WaitingForTouch:
386− if (oldStatus != DirectionalDragArea::Rejected) {
387− Q_EMIT draggingChanged(false);
530+ Q_EMIT draggingChanged(false);
388− }
389 break;
531 break;
390 case Undecided:
532 case Undecided:
391 m_recognitionTimer->start();
533 m_recognitionTimer->start();
@@ -395,9 +537,6 @@ void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
395 if (oldStatus == WaitingForTouch)
537 if (oldStatus == WaitingForTouch)
396 Q_EMIT draggingChanged(true);
538 Q_EMIT draggingChanged(true);
397 break;
539 break;
398− case Rejected:
399− Q_EMIT draggingChanged(false);
400− break;
401 default:
540 default:
402 // no-op
541 // no-op
403 break;
542 break;
@@ -406,8 +545,6 @@ void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
406
545
407void DirectionalDragArea::setPreviousPos(const QPointF &point)
546void DirectionalDragArea::setPreviousPos(const QPointF &point)
408{
547{
409− Q_ASSERT(m_status != Rejected);
410−
411 bool xChanged = m_previousPos.x() != point.x();
548 bool xChanged = m_previousPos.x() != point.x();
412 bool yChanged = m_previousPos.y() != point.y();
549 bool yChanged = m_previousPos.y() != point.y();
413
550
@@ -428,8 +565,6 @@ void DirectionalDragArea::setPreviousPos(const QPointF &point)
428
565
429void DirectionalDragArea::setPreviousScenePos(const QPointF &point)
566void DirectionalDragArea::setPreviousScenePos(const QPointF &point)
430{
567{
431− Q_ASSERT(m_status != Rejected);
432−
433 bool xChanged = m_previousScenePos.x() != point.x();
568 bool xChanged = m_previousScenePos.x() != point.x();
434 bool yChanged = m_previousScenePos.y() != point.y();
569 bool yChanged = m_previousScenePos.y() != point.y();
435
570
@@ -457,5 +592,106 @@ void DirectionalDragArea::updateVelocityCalculator(const QPointF &point)
457 }
592 }
458}
593}
459
594
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+
460// Because we are defining a new QObject-based class (RecognitionTimer) here.
696// Because we are defining a new QObject-based class (RecognitionTimer) here.
461#include "DirectionalDragArea.moc"
697#include "DirectionalDragArea.moc"
plugins/Ubuntu/Gestures/DirectionalDragArea.h+50−13
@@ -117,9 +117,17 @@ class UBUNTUGESTURES_EXPORT DirectionalDragArea : public QQuickItem {
117 Q_PROPERTY(int maxSilenceTime READ maxSilenceTime
117 Q_PROPERTY(int maxSilenceTime READ maxSilenceTime
118 WRITE setMaxSilenceTime
118 WRITE setMaxSilenceTime
119 NOTIFY maxSilenceTimeChanged)
119 NOTIFY maxSilenceTimeChanged)
120+
120 //
121 //
121 /////
122 /////
122
123
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+
123 Q_ENUMS(Direction)
131 Q_ENUMS(Direction)
124 Q_ENUMS(Status)
132 Q_ENUMS(Status)
125public:
133public:
@@ -130,26 +138,23 @@ public:
130
138
131 // Describes the state of the directional drag gesture.
139 // Describes the state of the directional drag gesture.
132 enum Status {
140 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.
134 WaitingForTouch,
143 WaitingForTouch,
135
144
136 // A touch point has landed on this area but it's not know yet whether it is
145 // A touch point has landed on this area but it's not know yet whether it is
137 // performing a drag in the correct direction.
146 // 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.
138 Undecided, //Recognizing,
149 Undecided, //Recognizing,
139
150
140 // There's a touch point in this area and it performed a drag in the correct
151 // There's a touch point in this area and it performed a drag in the correct
141 // direction.
152 // direction.
142 //
153 //
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
144 // that touch point ends. The gesture will remain in the Recognized state even if
155 // that touch point ends. The gesture will remain in the Recognized state even if
145 // the touch point starts moving in other directions or halts.
156 // the touch point starts moving in other directions or halts.
146 Recognized,
157 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
153 };
158 };
154 Status status() const { return m_status; }
159 Status status() const { return m_status; }
155
160
@@ -164,7 +169,7 @@ public:
164
169
165 bool dragging() const { return (m_status == Undecided) || (m_status == Recognized); }
170 bool dragging() const { return (m_status == Undecided) || (m_status == Recognized); }
166
171
167− qreal maxDeviation() const { return m_dampedPos.maxDelta(); }
172+ qreal maxDeviation() const { return m_dampedScenePos.maxDelta(); }
168 void setMaxDeviation(qreal value);
173 void setMaxDeviation(qreal value);
169
174
170 qreal wideningAngle() const;
175 qreal wideningAngle() const;
@@ -179,13 +184,16 @@ public:
179 int maxSilenceTime() const { return m_maxSilenceTime; }
184 int maxSilenceTime() const { return m_maxSilenceTime; }
180 void setMaxSilenceTime(int value);
185 void setMaxSilenceTime(int value);
181
186
187+ int compositionTime() const { return m_compositionTime; }
188+ void setCompositionTime(int value);
189+
182 // Replaces the existing Timer with the given one.
190 // Replaces the existing Timer with the given one.
183 //
191 //
184 // Useful for providing a fake timer when testing.
192 // Useful for providing a fake timer when testing.
185 void setRecognitionTimer(UbuntuGestures::AbstractTimer *timer);
193 void setRecognitionTimer(UbuntuGestures::AbstractTimer *timer);
186
194
187 // Useful for testing, where a fake time source can be supplied
195 // Useful for testing, where a fake time source can be supplied
188− void setTimeSource(UbuntuGestures::TimeSource *timeSource);
196+ void setTimeSource(const UbuntuGestures::SharedTimeSource &timeSource);
189
197
190Q_SIGNALS:
198Q_SIGNALS:
191 void directionChanged(Direction::Type direction);
199 void directionChanged(Direction::Type direction);
@@ -198,6 +206,7 @@ Q_SIGNALS:
198 void distanceThresholdChanged(qreal value);
206 void distanceThresholdChanged(qreal value);
199 void minSpeedChanged(qreal value);
207 void minSpeedChanged(qreal value);
200 void maxSilenceTimeChanged(int value);
208 void maxSilenceTimeChanged(int value);
209+ void compositionTimeChanged(int value);
201 void touchXChanged(qreal value);
210 void touchXChanged(qreal value);
202 void touchYChanged(qreal value);
211 void touchYChanged(qreal value);
203 void touchSceneXChanged(qreal value);
212 void touchSceneXChanged(qreal value);
@@ -213,7 +222,6 @@ private:
213 void touchEvent_absent(QTouchEvent *event);
222 void touchEvent_absent(QTouchEvent *event);
214 void touchEvent_undecided(QTouchEvent *event);
223 void touchEvent_undecided(QTouchEvent *event);
215 void touchEvent_recognized(QTouchEvent *event);
224 void touchEvent_recognized(QTouchEvent *event);
216− void touchEvent_rejected(QTouchEvent *event);
217 bool pointInsideAllowedArea() const;
225 bool pointInsideAllowedArea() const;
218 bool movingInRightDirection() const;
226 bool movingInRightDirection() const;
219 bool movedFarEnough(const QPointF &point) const;
227 bool movedFarEnough(const QPointF &point) const;
@@ -222,6 +230,7 @@ private:
222 void setPreviousPos(const QPointF &point);
230 void setPreviousPos(const QPointF &point);
223 void setPreviousScenePos(const QPointF &point);
231 void setPreviousScenePos(const QPointF &point);
224 void updateVelocityCalculator(const QPointF &point);
232 void updateVelocityCalculator(const QPointF &point);
233+ bool isWithinTouchCompositionWindow();
225
234
226 Status m_status;
235 Status m_status;
227
236
@@ -233,8 +242,8 @@ private:
233
242
234 // A movement damper is used in some of the gesture recognition calculations
243 // A movement damper is used in some of the gesture recognition calculations
235 // to get rid of noise or small oscillations in the touch position.
244 // to get rid of noise or small oscillations in the touch position.
236− DampedPointF m_dampedPos;
245+ DampedPointF m_dampedScenePos;
237− QPointF m_previousDampedPos;
246+ QPointF m_previousDampedScenePos;
238
247
239 Direction::Type m_direction;
248 Direction::Type m_direction;
240 qreal m_wideningAngle; // in degrees
249 qreal m_wideningAngle; // in degrees
@@ -243,9 +252,37 @@ private:
243 qreal m_minSpeed;
252 qreal m_minSpeed;
244 int m_maxSilenceTime; // in milliseconds
253 int m_maxSilenceTime; // in milliseconds
245 int m_silenceTime; // in milliseconds
254 int m_silenceTime; // in milliseconds
255+ int m_compositionTime; // in milliseconds
246 int m_numSamplesOnLastSpeedCheck;
256 int m_numSamplesOnLastSpeedCheck;
247 UbuntuGestures::AbstractTimer *m_recognitionTimer;
257 UbuntuGestures::AbstractTimer *m_recognitionTimer;
248 AxisVelocityCalculator *m_velocityCalculator;
258 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;
249};
286};
250
287
251#endif // DIRECTIONAL_DRAG_AREA_H
288#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 {
31 y: followDragArea()
31 y: followDragArea()
32
32
33 function followDragArea() {
33 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
38 }
35 }
39 }
36 }
40
37
@@ -66,15 +63,10 @@ Item {
66 dragAreaRect.opacity = 0.3
63 dragAreaRect.opacity = 0.3
67 launcher.y = Qt.binding(launcher.followDragArea)
64 launcher.y = Qt.binding(launcher.followDragArea)
68 break;
65 break;
69− case DirectionalDragArea.Recognized:
66+ defaut: // DirectionalDragArea.Recognized:
70 dragAreaRect.color = "green"
67 dragAreaRect.color = "green"
71 dragAreaRect.opacity = 0.5
68 dragAreaRect.opacity = 0.5
72 break;
69 break;
73− default: //case DirectionalDragArea.Rejected:
74− dragAreaRect.color = "red"
75− dragAreaRect.opacity = 0.5
76− launcher.y = -launcher.height
77− break;
78 }
70 }
79 }
71 }
80
72
tests/plugins/Ubuntu/Gestures/LeftwardsLauncher.qml+5−13
@@ -32,13 +32,10 @@ Item {
32 y: 0
32 y: 0
33
33
34 function followDragArea() {
34 function followDragArea() {
35− if (dragArea.status === DirectionalDragArea.Rejected)
36− return root.width
37− else
38− return dragArea.distance > -width ?
35+ return dragArea.distance > -width ?
39− root.width + dragArea.distance
36+ root.width + dragArea.distance
40− :
37+ :
41− root.width - width
38+ root.width - width
42 }
39 }
43 }
40 }
44
41
@@ -69,15 +66,10 @@ Item {
69 dragAreaRect.opacity = 0.3
66 dragAreaRect.opacity = 0.3
70 launcher.x = Qt.binding(launcher.followDragArea)
67 launcher.x = Qt.binding(launcher.followDragArea)
71 break;
68 break;
72− case DirectionalDragArea.Recognized:
69+ default: //case DirectionalDragArea.Recognized:
73 dragAreaRect.color = "green"
70 dragAreaRect.color = "green"
74 dragAreaRect.opacity = 0.5
71 dragAreaRect.opacity = 0.5
75 break;
72 break;
76− default: //case DirectionalDragArea.Rejected:
77− dragAreaRect.color = "red"
78− dragAreaRect.opacity = 0.5
79− launcher.x = -launcher.height
80− break;
81 }
73 }
82 }
74 }
83
75
tests/plugins/Ubuntu/Gestures/RightwardsLauncher.qml+3−10
@@ -19,6 +19,7 @@ import Ubuntu.Gestures 0.1
19import Ubuntu.Components 0.1
19import Ubuntu.Components 0.1
20
20
21Item {
21Item {
22+ objectName: "rightwardsLauncher"
22
23
23 function reset() { launcher.x = -launcher.width }
24 function reset() { launcher.x = -launcher.width }
24
25
@@ -31,10 +32,7 @@ Item {
31 y: 0
32 y: 0
32
33
33 function followDragArea() {
34 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
38 }
36 }
39 }
37 }
40
38
@@ -66,15 +64,10 @@ Item {
66 dragAreaRect.opacity = 0.3
64 dragAreaRect.opacity = 0.3
67 launcher.x = Qt.binding(launcher.followDragArea)
65 launcher.x = Qt.binding(launcher.followDragArea)
68 break;
66 break;
69− case DirectionalDragArea.Recognized:
67+ default: //case DirectionalDragArea.Recognized:
70 dragAreaRect.color = "green"
68 dragAreaRect.color = "green"
71 dragAreaRect.opacity = 0.5
69 dragAreaRect.opacity = 0.5
72 break;
70 break;
73− default: //case DirectionalDragArea.Rejected:
74− dragAreaRect.color = "red"
75− dragAreaRect.opacity = 0.5
76− launcher.x = -launcher.height
77− break;
78 }
71 }
79 }
72 }
80
73
tests/plugins/Ubuntu/Gestures/UpwardsLauncher.qml+5−13
@@ -32,13 +32,10 @@ Item {
32 y: root.height
32 y: root.height
33
33
34 function followDragArea() {
34 function followDragArea() {
35− if (dragArea.status === DirectionalDragArea.Rejected)
36− return root.height
37− else
38− return dragArea.distance > -height ?
35+ return dragArea.distance > -height ?
39− root.height + dragArea.distance
36+ root.height + dragArea.distance
40− :
37+ :
41− root.height - height
38+ root.height - height
42 }
39 }
43 }
40 }
44
41
@@ -69,15 +66,10 @@ Item {
69 dragAreaRect.opacity = 0.3
66 dragAreaRect.opacity = 0.3
70 launcher.y = Qt.binding(launcher.followDragArea)
67 launcher.y = Qt.binding(launcher.followDragArea)
71 break;
68 break;
72− case DirectionalDragArea.Recognized:
69+ default: //case DirectionalDragArea.Recognized:
73 dragAreaRect.color = "green"
70 dragAreaRect.color = "green"
74 dragAreaRect.opacity = 0.5
71 dragAreaRect.opacity = 0.5
75 break;
72 break;
76− default: //case DirectionalDragArea.Rejected:
77− dragAreaRect.color = "red"
78− dragAreaRect.opacity = 0.5
79− launcher.y = -launcher.height
80− break;
81 }
73 }
82 }
74 }
83
75
tests/plugins/Ubuntu/Gestures/edgeDragExample.qml+7−1
@@ -33,7 +33,13 @@ Rectangle {
33 }
33 }
34 }
34 }
35
35
36+ // NB: Do not anchor it as we will move it programmatically from the test
36− RightwardsLauncher { id: hpLauncher; anchors.fill: parent }
37+ RightwardsLauncher {
38+ id: hpLauncher;
39+ width: parent.width
40+ height: parent.height
41+ }
42+
37 LeftwardsLauncher { id: hnLauncher; anchors.fill: parent }
43 LeftwardsLauncher { id: hnLauncher; anchors.fill: parent }
38 DownwardsLauncher { id: vpLauncher; anchors.fill: parent }
44 DownwardsLauncher { id: vpLauncher; anchors.fill: parent }
39 UpwardsLauncher { id: vnLauncher; anchors.fill: parent }
45 UpwardsLauncher { id: vnLauncher; anchors.fill: parent }
tests/plugins/Ubuntu/Gestures/tst_AxisVelocityCalculator.cpp+3−3
@@ -54,12 +54,12 @@ private Q_SLOTS:
54
54
55private:
55private:
56 AxisVelocityCalculator *velCalc;
56 AxisVelocityCalculator *velCalc;
57− FakeTimeSource *fakeTimeSource;
57+ QSharedPointer<FakeTimeSource> fakeTimeSource;
58};
58};
59
59
60void tst_AxisVelocityCalculator::init()
60void tst_AxisVelocityCalculator::init()
61{
61{
62− fakeTimeSource = new FakeTimeSource;
62+ fakeTimeSource.reset(new FakeTimeSource);
63
63
64 velCalc = new AxisVelocityCalculator;
64 velCalc = new AxisVelocityCalculator;
65 velCalc->setTimeSource(fakeTimeSource);
65 velCalc->setTimeSource(fakeTimeSource);
@@ -68,7 +68,7 @@ void tst_AxisVelocityCalculator::init()
68void tst_AxisVelocityCalculator::cleanup()
68void tst_AxisVelocityCalculator::cleanup()
69{
69{
70 delete velCalc;
70 delete velCalc;
71− delete fakeTimeSource;
71+ fakeTimeSource.reset();
72}
72}
73
73
74void tst_AxisVelocityCalculator::simpleSamples()
74void tst_AxisVelocityCalculator::simpleSamples()
tests/plugins/Ubuntu/Gestures/tst_DirectionalDragArea.cpp+285−96
@@ -19,23 +19,38 @@
19#include <qpa/qwindowsysteminterface.h>
19#include <qpa/qwindowsysteminterface.h>
20#include <QtQuick/QQuickView>
20#include <QtQuick/QQuickView>
21#include <QtQml/QQmlEngine>
21#include <QtQml/QQmlEngine>
22+#include <QPointer>
22
23
23#include <DirectionalDragArea.h>
24#include <DirectionalDragArea.h>
24
25
26+using namespace UbuntuGestures;
27+
25−class FakeTimer : public UbuntuGestures::AbstractTimer
28+class FakeTimer : public AbstractTimer
26{
29{
27 Q_OBJECT
30 Q_OBJECT
28public:
31public:
29− FakeTimer(QObject *parent = 0)
32+ FakeTimer(const SharedTimeSource &timeSource, QObject *parent = 0)
30− : UbuntuGestures::AbstractTimer(parent)
33+ : UbuntuGestures::AbstractTimer(parent),
34+ m_timeSource(timeSource)
31 {}
35 {}
32
36
33− virtual int interval() const { return m_duration; }
37+ int interval() const override { return m_interval; }
34− virtual void setInterval(int msecs) { m_duration = msecs; }
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+ }
35
48
36− void emitTimeout() {Q_EMIT timeout();}
49+ qint64 nextTimeoutTime() const { return m_nextTimeoutTime; }
37private:
50private:
38− int m_duration;
51+ int m_interval;
52+ SharedTimeSource m_timeSource;
53+ qint64 m_nextTimeoutTime;
39};
54};
40
55
41class FakeTimeSource : public UbuntuGestures::TimeSource
56class FakeTimeSource : public UbuntuGestures::TimeSource
@@ -67,14 +82,17 @@ private Q_SLOTS:
67 void maxSilenceTime();
82 void maxSilenceTime();
68 void sceneXAndX();
83 void sceneXAndX();
69 void sceneYAndY();
84 void sceneYAndY();
70− void emitDraggingChangedOnDirectRecognition();
85+ void twoFingerTap();
86+ void movingDDA();
87+ void ignoreOldFinger();
71
88
72private:
89private:
73 QQuickView *createView();
90 QQuickView *createView();
91+ void passTime(qint64 timeSpan);
74 QQuickView *view;
92 QQuickView *view;
75 QTouchDevice *device;
93 QTouchDevice *device;
76 FakeTimer *fakeTimer;
94 FakeTimer *fakeTimer;
77− FakeTimeSource *fakeTimeSource;
95+ QSharedPointer<FakeTimeSource> fakeTimeSource;
78};
96};
79
97
80void tst_DirectionalDragArea::initTestCase()
98void tst_DirectionalDragArea::initTestCase()
@@ -101,8 +119,8 @@ void tst_DirectionalDragArea::init()
101 QVERIFY(view->rootObject() != 0);
119 QVERIFY(view->rootObject() != 0);
102 qApp->processEvents();
120 qApp->processEvents();
103
121
122+ fakeTimeSource.reset(new FakeTimeSource);
104− fakeTimer = new FakeTimer;
123+ fakeTimer = new FakeTimer(fakeTimeSource);
105− fakeTimeSource = new FakeTimeSource;
106}
124}
107
125
108void tst_DirectionalDragArea::cleanup()
126void tst_DirectionalDragArea::cleanup()
@@ -113,8 +131,7 @@ void tst_DirectionalDragArea::cleanup()
113 delete fakeTimer;
131 delete fakeTimer;
114 fakeTimer = 0;
132 fakeTimer = 0;
115
133
116− delete fakeTimeSource;
134+ fakeTimeSource.reset();
117− fakeTimeSource = 0;
118}
135}
119
136
120QQuickView *tst_DirectionalDragArea::createView()
137QQuickView *tst_DirectionalDragArea::createView()
@@ -127,6 +144,23 @@ QQuickView *tst_DirectionalDragArea::createView()
127 return window;
144 return window;
128}
145}
129
146
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+
130namespace {
164namespace {
131QPointF calculateInitialTouchPos(DirectionalDragArea *edgeDragArea, QQuickView *view)
165QPointF calculateInitialTouchPos(DirectionalDragArea *edgeDragArea, QQuickView *view)
132{
166{
@@ -180,7 +214,7 @@ void tst_DirectionalDragArea::edgeDrag()
180 QFETCH(QString, dragAreaObjectName);
214 QFETCH(QString, dragAreaObjectName);
181 QFETCH(qreal, wideningAngleMultiplier);
215 QFETCH(qreal, wideningAngleMultiplier);
182 QFETCH(qreal, dragDistanceFactor);
216 QFETCH(qreal, dragDistanceFactor);
183− QFETCH(int, expectedGestureRecognition);
217+ QFETCH(bool, expectGestureRecognition);
184
218
185 DirectionalDragArea *edgeDragArea =
219 DirectionalDragArea *edgeDragArea =
186 view->rootObject()->findChild<DirectionalDragArea*>(dragAreaObjectName);
220 view->rootObject()->findChild<DirectionalDragArea*>(dragAreaObjectName);
@@ -196,7 +230,10 @@ void tst_DirectionalDragArea::edgeDrag()
196 qreal desiredDragDistance = edgeDragArea->distanceThreshold()*dragDistanceFactor;
230 qreal desiredDragDistance = edgeDragArea->distanceThreshold()*dragDistanceFactor;
197 QPointF dragDirectionVector = calculateDirectionVector(edgeDragArea,
231 QPointF dragDirectionVector = calculateDirectionVector(edgeDragArea,
198 wideningAngleMultiplier);
232 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;
200
237
201 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
238 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
202
239
@@ -211,15 +248,16 @@ void tst_DirectionalDragArea::edgeDrag()
211 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
248 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
212 }
249 }
213
250
214− do {
251+ for (int i = 0; i < totalMovementSteps; ++i) {
215 touchPoint += touchMovement;
252 touchPoint += touchMovement;
253+ passTime(movementTimeStepMs);
216 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
254 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
217− } while ((touchPoint - initialTouchPos).manhattanLength() < desiredDragDistance);
255+ }
218
256
219− if (expectedGestureRecognition)
257+ if (expectGestureRecognition)
220 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
258 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
221
259
222− if (edgeDragArea->status() == DirectionalDragArea::Rejected) {
260+ if (edgeDragArea->status() == DirectionalDragArea::WaitingForTouch) {
223 QCOMPARE(edgeDragArea->dragging(), false);
261 QCOMPARE(edgeDragArea->dragging(), false);
224 QCOMPARE(draggingSpy.count(), 2);
262 QCOMPARE(draggingSpy.count(), 2);
225 }
263 }
@@ -235,49 +273,49 @@ void tst_DirectionalDragArea::edgeDrag_data()
235 QTest::addColumn<QString>("dragAreaObjectName");
273 QTest::addColumn<QString>("dragAreaObjectName");
236 QTest::addColumn<qreal>("wideningAngleMultiplier");
274 QTest::addColumn<qreal>("wideningAngleMultiplier");
237 QTest::addColumn<qreal>("dragDistanceFactor");
275 QTest::addColumn<qreal>("dragDistanceFactor");
238− QTest::addColumn<int>("expectedGestureRecognition");
276+ QTest::addColumn<bool>("expectGestureRecognition");
239
277
240 QTest::newRow("rightwards, tiny drag")
278 QTest::newRow("rightwards, tiny drag")
241− << "hpDragArea" << 0.0 << 0.2 << 0;
279+ << "hpDragArea" << 0.0 << 0.2 << false;
242
280
243 QTest::newRow("rightwards, straight drag")
281 QTest::newRow("rightwards, straight drag")
244− << "hpDragArea" << 0.0 << 3.0 << 1;
282+ << "hpDragArea" << 0.0 << 3.0 << true;
245
283
246 QTest::newRow("rightwards, diagonal drag")
284 QTest::newRow("rightwards, diagonal drag")
247− << "hpDragArea" << 0.9 << 3.0 << 1;
285+ << "hpDragArea" << 0.9 << 3.0 << true;
248
286
249 QTest::newRow("rightwards, overly diagonal drag")
287 QTest::newRow("rightwards, overly diagonal drag")
250− << "hpDragArea" << 2.0 << 3.0 << 0;
288+ << "hpDragArea" << 2.0 << 3.0 << false;
251
289
252 QTest::newRow("leftwards, tiny drag")
290 QTest::newRow("leftwards, tiny drag")
253− << "hnDragArea" << 0.0 << 0.2 << 0;
291+ << "hnDragArea" << 0.0 << 0.2 << false;
254
292
255 QTest::newRow("leftwards, straight drag")
293 QTest::newRow("leftwards, straight drag")
256− << "hnDragArea" << 0.0 << 3.0 << 1;
294+ << "hnDragArea" << 0.0 << 3.0 << true;
257
295
258 QTest::newRow("leftwards, diagonal drag")
296 QTest::newRow("leftwards, diagonal drag")
259− << "hnDragArea" << 0.9 << 3.0 << 1;
297+ << "hnDragArea" << 0.9 << 3.0 << true;
260
298
261 QTest::newRow("downwards, tiny drag")
299 QTest::newRow("downwards, tiny drag")
262− << "vpDragArea" << 0.0 << 0.2 << 0;
300+ << "vpDragArea" << 0.0 << 0.2 << false;
263
301
264 QTest::newRow("downwards, straight drag")
302 QTest::newRow("downwards, straight drag")
265− << "vpDragArea" << 0.0 << 3.0 << 1;
303+ << "vpDragArea" << 0.0 << 3.0 << true;
266
304
267 QTest::newRow("downwards, diagonal drag")
305 QTest::newRow("downwards, diagonal drag")
268− << "vpDragArea" << 0.9 << 3.0 << 1;
306+ << "vpDragArea" << 0.9 << 3.0 << true;
269
307
270 QTest::newRow("upwards, tiny drag")
308 QTest::newRow("upwards, tiny drag")
271− << "vnDragArea" << 0.0 << 0.2 << 0;
309+ << "vnDragArea" << 0.0 << 0.2 << false;
272
310
273 QTest::newRow("upwards, straight drag")
311 QTest::newRow("upwards, straight drag")
274− << "vnDragArea" << 0.0 << 3.0 << 1;
312+ << "vnDragArea" << 0.0 << 3.0 << true;
275
313
276 QTest::newRow("upwards, diagonal drag")
314 QTest::newRow("upwards, diagonal drag")
277− << "vnDragArea" << 0.9 << 3.0 << 1;
315+ << "vnDragArea" << 0.9 << 3.0 << true;
278
316
279 QTest::newRow("upwards, overly diagonal drag")
317 QTest::newRow("upwards, overly diagonal drag")
280− << "vnDragArea" << 2.0 << 3.0 << 0;
318+ << "vnDragArea" << 2.0 << 3.0 << false;
281}
319}
282
320
283/*
321/*
@@ -298,25 +336,33 @@ void tst_DirectionalDragArea::dragWithShortDirectionChange()
298
336
299 qreal desiredDragDistance = edgeDragArea->distanceThreshold()*2.0;
337 qreal desiredDragDistance = edgeDragArea->distanceThreshold()*2.0;
300 QPointF dragDirectionVector(1.0, 0.0);
338 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;
302
343
303 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
344 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
304
345
305 // Move a bit in the proper direction
346 // Move a bit in the proper direction
306 for (int i=0; i < 3; ++i) {
347 for (int i=0; i < 3; ++i) {
307 touchPoint += touchMovement;
348 touchPoint += touchMovement;
349+ passTime(touchStepTimeMs);
308 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
350 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
309 }
351 }
310
352
311 // Then a sudden and small movement to the opposite direction
353 // 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);
313 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
356 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
314
357
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.
316 do {
360 do {
317 touchPoint += touchMovement;
361 touchPoint += touchMovement;
362+ passTime(touchStepTimeMs);
318 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
363 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));
320
366
321 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
367 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
322
368
@@ -329,7 +375,8 @@ void tst_DirectionalDragArea::dragWithShortDirectionChange()
329 */
375 */
330void tst_DirectionalDragArea::minSpeed()
376void tst_DirectionalDragArea::minSpeed()
331{
377{
332− QFETCH(int, minSpeedMsecsDeviation);
378+ QFETCH(qreal, minSpeed);
379+ QFETCH(qreal, speed);
333 QFETCH(int, expectedStatusAfterSpeedCheck);
380 QFETCH(int, expectedStatusAfterSpeedCheck);
334
381
335 DirectionalDragArea *edgeDragArea =
382 DirectionalDragArea *edgeDragArea =
@@ -338,44 +385,40 @@ void tst_DirectionalDragArea::minSpeed()
338 edgeDragArea->setRecognitionTimer(fakeTimer);
385 edgeDragArea->setRecognitionTimer(fakeTimer);
339 edgeDragArea->setTimeSource(fakeTimeSource);
386 edgeDragArea->setTimeSource(fakeTimeSource);
340
387
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+
341 QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view);
394 QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view);
342 QPointF touchPoint = initialTouchPos;
395 QPointF touchPoint = initialTouchPos;
343
396
344 QPointF dragDirectionVector(1.0, 0.0);
397 QPointF dragDirectionVector(1.0, 0.0);
345− qreal distanceStep = edgeDragArea->distanceThreshold() * 0.1f;
398+ qint64 timeStepMsecs = 10;
399+ qreal distanceStep = (speed / 1000.0f) * timeStepMsecs;
346 QPointF touchMovement = dragDirectionVector * distanceStep;
400 QPointF touchMovement = dragDirectionVector * distanceStep;
347− qreal minSpeedMsecs = edgeDragArea->minSpeed() / 1000.0;
348− qint64 timeStepMsecs = qFloor(distanceStep / minSpeedMsecs) + minSpeedMsecsDeviation;
349
401
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;
355 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
402 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
356
403
357− // Move a bit in the proper direction
404+ // Move for a while to ensure our speed check is performed a couple of times
358− for (int i=0; i < 4; ++i) {
405+ for (int i=0; i < 20; ++i) {
359 touchPoint += touchMovement;
406 touchPoint += touchMovement;
360− fakeTimeSource->m_msecsSinceReference += timeStepMsecs;
407+ passTime(timeStepMsecs);
361 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
408 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
362 }
409 }
363
410
364− QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
365−
366− // Force the periodic speed check.
367− fakeTimer->emitTimeout();
368−
369 QCOMPARE((int)edgeDragArea->status(), expectedStatusAfterSpeedCheck);
411 QCOMPARE((int)edgeDragArea->status(), expectedStatusAfterSpeedCheck);
370}
412}
371
413
372void tst_DirectionalDragArea::minSpeed_data()
414void tst_DirectionalDragArea::minSpeed_data()
373{
415{
374− QTest::addColumn<int>("minSpeedMsecsDeviation");
416+ QTest::addColumn<qreal>("minSpeed");
417+ QTest::addColumn<qreal>("speed");
375 QTest::addColumn<int>("expectedStatusAfterSpeedCheck");
418 QTest::addColumn<int>("expectedStatusAfterSpeedCheck");
376
419
377− QTest::newRow("slower than minSpeed") << 20 << (int)DirectionalDragArea::Rejected;
420+ QTest::newRow("slower than minSpeed") << 100.0 << 50.0 << (int)DirectionalDragArea::WaitingForTouch;
378− QTest::newRow("faster than minSpeed") << -20 << (int)DirectionalDragArea::Undecided;
421+ QTest::newRow("faster than minSpeed") << 100.0 << 150.0 << (int)DirectionalDragArea::Undecided;
379}
422}
380
423
381/*
424/*
@@ -391,32 +434,37 @@ void tst_DirectionalDragArea::recognitionTimerUsage()
391 edgeDragArea->setRecognitionTimer(fakeTimer);
434 edgeDragArea->setRecognitionTimer(fakeTimer);
392 edgeDragArea->setTimeSource(fakeTimeSource);
435 edgeDragArea->setTimeSource(fakeTimeSource);
393
436
437+ // don't let it interfere with our test
438+ edgeDragArea->setMinSpeed(0.0);
439+
440+ int timeStepMs = 5; // some arbitrary small value.
441+
394 QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view);
442 QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view);
395 QPointF touchPoint = initialTouchPos;
443 QPointF touchPoint = initialTouchPos;
396
444
397 QPointF dragDirectionVector(1.0, 0.0);
445 QPointF dragDirectionVector(1.0, 0.0);
398 QPointF touchMovement = dragDirectionVector * (edgeDragArea->distanceThreshold() * 0.2f);
446 QPointF touchMovement = dragDirectionVector * (edgeDragArea->distanceThreshold() * 0.2f);
399
447
448+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
400 QVERIFY(!fakeTimer->isRunning());
449 QVERIFY(!fakeTimer->isRunning());
401
450
402 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
451 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
403
452
453+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
404 QVERIFY(fakeTimer->isRunning());
454 QVERIFY(fakeTimer->isRunning());
405
455
406− // Move a bit in the proper direction
456+ // Move beyond distance threshold and composition time to ensure recognition
407− for (int i=0; i < 3; ++i) {
457+ while (fakeTimeSource->m_msecsSinceReference <= edgeDragArea->compositionTime() ||
458+ (touchPoint - initialTouchPos).manhattanLength() <= edgeDragArea->distanceThreshold()) {
459+
460+ QCOMPARE(edgeDragArea->status() == DirectionalDragArea::Undecided, fakeTimer->isRunning());
461+
408 touchPoint += touchMovement;
462 touchPoint += touchMovement;
463+ passTime(timeStepMs);
409 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
464 QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
410 }
465 }
411
466
412− QVERIFY(fakeTimer->isRunning());
413−
414− // Move beyond distance threshold
415− touchPoint += 3*touchMovement;
416− QTest::touchEvent(view, device).move(0, touchPoint.toPoint());
417−
418 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
467 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
419−
420 QVERIFY(!fakeTimer->isRunning());
468 QVERIFY(!fakeTimer->isRunning());
421}
469}
422
470
@@ -432,29 +480,23 @@ void tst_DirectionalDragArea::maxSilenceTime()
432 edgeDragArea->setRecognitionTimer(fakeTimer);
480 edgeDragArea->setRecognitionTimer(fakeTimer);
433 edgeDragArea->setTimeSource(fakeTimeSource);
481 edgeDragArea->setTimeSource(fakeTimeSource);
434
482
483+ // Make sure this property is not disabled
484+ edgeDragArea->setMaxSilenceTime(100);
485+
435 QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view);
486 QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view);
436 QPointF touchPoint = initialTouchPos;
487 QPointF touchPoint = initialTouchPos;
437
488
438− fakeTimeSource->m_msecsSinceReference = 0;
439 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
489 QTest::touchEvent(view, device).press(0, touchPoint.toPoint());
440
490
441 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
491 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided);
492+ QVERIFY(fakeTimer->isRunning());
442
493
443− int neededTimeouts = qCeil((qreal)edgeDragArea->maxSilenceTime() / (qreal)fakeTimer->interval());
494+ // Force timer to timeout until after maxSilenceTime has been reached
444− if ((edgeDragArea->maxSilenceTime() % fakeTimer->interval()) == 0) {
495+ while (fakeTimeSource->m_msecsSinceReference < edgeDragArea->maxSilenceTime()) {
445− ++neededTimeouts;
496+ passTime(fakeTimer->interval());
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− }
455 }
497 }
456
498
457− QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Rejected);
499+ QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
458}
500}
459
501
460/*
502/*
@@ -471,7 +513,6 @@ void tst_DirectionalDragArea::sceneXAndX()
471
513
472 QPointF touchScenePos(view->width() - (edgeDragArea->width()/2.0f), view->height()/2.0f);
514 QPointF touchScenePos(view->width() - (edgeDragArea->width()/2.0f), view->height()/2.0f);
473
515
474− fakeTimeSource->m_msecsSinceReference = 0;
475 QTest::touchEvent(view, device).press(0, touchScenePos.toPoint());
516 QTest::touchEvent(view, device).press(0, touchScenePos.toPoint());
476
517
477 QSignalSpy touchXSpy(edgeDragArea, SIGNAL(touchXChanged(qreal)));
518 QSignalSpy touchXSpy(edgeDragArea, SIGNAL(touchXChanged(qreal)));
@@ -500,7 +541,6 @@ void tst_DirectionalDragArea::sceneYAndY()
500
541
501 QPointF touchScenePos(view->width()/2.0f, view->height() - (edgeDragArea->height()/2.0f));
542 QPointF touchScenePos(view->width()/2.0f, view->height() - (edgeDragArea->height()/2.0f));
502
543
503− fakeTimeSource->m_msecsSinceReference = 0;
504 QTest::touchEvent(view, device).press(0, touchScenePos.toPoint());
544 QTest::touchEvent(view, device).press(0, touchScenePos.toPoint());
505
545
506 QSignalSpy touchYSpy(edgeDragArea, SIGNAL(touchYChanged(qreal)));
546 QSignalSpy touchYSpy(edgeDragArea, SIGNAL(touchYChanged(qreal)));
@@ -516,11 +556,12 @@ void tst_DirectionalDragArea::sceneYAndY()
516}
556}
517
557
518/*
558/*
519− Emit draggingChanged() when status change from WaitingForTouch directly to Recognized.
559+ Regression test for https://bugs.launchpad.net/bugs/1228336
520− This happens when the gesture recognition is effectively disabled (e.g. by setting a
560+
521− distanceThreshold of zero)
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.
522 */
563 */
523−void tst_DirectionalDragArea::emitDraggingChangedOnDirectRecognition()
564+void tst_DirectionalDragArea::twoFingerTap()
524{
565{
525 DirectionalDragArea *edgeDragArea =
566 DirectionalDragArea *edgeDragArea =
526 view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
567 view->rootObject()->findChild<DirectionalDragArea*>("hpDragArea");
@@ -528,22 +569,170 @@ void tst_DirectionalDragArea::emitDraggingChangedOnDirectRecognition()
528 edgeDragArea->setRecognitionTimer(fakeTimer);
569 edgeDragArea->setRecognitionTimer(fakeTimer);
529 edgeDragArea->setTimeSource(fakeTimeSource);
570 edgeDragArea->setTimeSource(fakeTimeSource);
530
571
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);
532
575
533− QSignalSpy draggingSpy(edgeDragArea, SIGNAL(draggingChanged(bool)));
576+ qint64 timeStepMsecs = 5; // some arbitrary, small value
534
577
535− // That will make it completetly skip the recognition phase ("Undecided" status).
578+ // Perform the first two-finger tap
536− edgeDragArea->setDistanceThreshold(0.0);
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);
537
583
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.
538 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
593 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch);
539− QCOMPARE(edgeDragArea->dragging(), false);
540
594
541− fakeTimeSource->m_msecsSinceReference = 0;
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+
542− QTest::touchEvent(view, device).press(0, touchPos.toPoint());
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+ }
543
676
544 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
677 QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized);
545− QCOMPARE(draggingSpy.count(), 1);
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+
546− QCOMPARE(edgeDragArea->dragging(), true);
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);
547}
736}
548
737
549QTEST_MAIN(tst_DirectionalDragArea)
738QTEST_MAIN(tst_DirectionalDragArea)
tests/qmltests/Components/tst_DragHandle.cpp+4−5
@@ -41,7 +41,7 @@ class FakeTimeSource : public UbuntuGestures::TimeSource
41{
41{
42public:
42public:
43 FakeTimeSource() { m_msecsSinceReference = 0; }
43 FakeTimeSource() { m_msecsSinceReference = 0; }
44− virtual qint64 msecsSinceReference() {return m_msecsSinceReference;}
44+ qint64 msecsSinceReference() override {return m_msecsSinceReference;}
45 qint64 m_msecsSinceReference;
45 qint64 m_msecsSinceReference;
46};
46};
47
47
@@ -73,7 +73,7 @@ private:
73 QQuickView *m_view;
73 QQuickView *m_view;
74 QTouchDevice *m_device;
74 QTouchDevice *m_device;
75 FakeTimer *m_fakeTimer;
75 FakeTimer *m_fakeTimer;
76− FakeTimeSource *m_fakeTimeSource;
76+ QSharedPointer<FakeTimeSource> m_fakeTimeSource;
77};
77};
78
78
79
79
@@ -102,7 +102,7 @@ void tst_DragHandle::init()
102 qApp->processEvents();
102 qApp->processEvents();
103
103
104 m_fakeTimer = new FakeTimer;
104 m_fakeTimer = new FakeTimer;
105− m_fakeTimeSource = new FakeTimeSource;
105+ m_fakeTimeSource.reset(new FakeTimeSource);
106}
106}
107
107
108void tst_DragHandle::cleanup()
108void tst_DragHandle::cleanup()
@@ -113,8 +113,7 @@ void tst_DragHandle::cleanup()
113 delete m_fakeTimer;
113 delete m_fakeTimer;
114 m_fakeTimer = 0;
114 m_fakeTimer = 0;
115
115
116− delete m_fakeTimeSource;
116+ m_fakeTimeSource.reset();
117− m_fakeTimeSource = 0;
118}
117}
119
118
120QQuickView *tst_DragHandle::createView()
119QQuickView *tst_DragHandle::createView()
tests/qmltests/Panel/tst_Panel.qml−1
@@ -52,7 +52,6 @@ Item {
52 {tag: "fullscreen", fullscreenFlag: true }
52 {tag: "fullscreen", fullscreenFlag: true }
53 ]}
53 ]}
54
54
55−
56 function init() {
55 function init() {
57 searchClicked = false;
56 searchClicked = false;
58 panel.indicators.hide();
57 panel.indicators.hide();
tests/qmltests/tst_Shell.qml+35−12
@@ -55,19 +55,42 @@ Item {
55 when: windowShown
55 when: windowShown
56
56
57 function initTestCase() {
57 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;
59
74
60− // Ensure DashHome is loaded before continuing
75+ var greeter = findChild(shell, "greeter");
61− var dashContentList = findChild(shell, "dashContentList");
76+ ok &= greeter !== undefined;
62− waitForRendering(dashContentList);
77+
63− var homeLoader = findChild(dashContentList, "home.scope loader");
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+
64− verify(homeLoader);
91+ verify(ok);
65− tryCompareFunction(function() {return homeLoader.item !== undefined;}, true);
92+
66−
93+ swipeAwayGreeter();
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);
71 }
94 }
72
95
73 function cleanup() {
96 function cleanup() {
tests/utils/modules/Unity/Test/UnityTestCase.qml+34
@@ -14,6 +14,7 @@
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
14 * along with this program. If not, see <http://www.gnu.org/licenses/>.
15 */
15 */
16
16
17+import QtQuick 2.0
17import QtTest 1.0
18import QtTest 1.0
18import Ubuntu.Components 0.1
19import Ubuntu.Components 0.1
19import Unity.Test 0.1 as UT
20import Unity.Test 0.1 as UT
@@ -297,4 +298,37 @@ TestCase {
297 event.release(0 /* touchId */, x, y)
298 event.release(0 /* touchId */, x, y)
298 event.commit()
299 event.commit()
299 }
300 }
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+ }
300}
334}