Unity 8
 All Classes Functions
DirectionalDragArea.cpp
1 /*
2  * Copyright (C) 2013-2014 Canonical, Ltd.
3  *
4  * This program is free software; you can redistribute it and/or modify
5  * it under the terms of the GNU General Public License as published by
6  * the Free Software Foundation; version 3.
7  *
8  * This program is distributed in the hope that it will be useful,
9  * but WITHOUT ANY WARRANTY; without even the implied warranty of
10  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11  * GNU General Public License for more details.
12  *
13  * You should have received a copy of the GNU General Public License
14  * along with this program. If not, see <http://www.gnu.org/licenses/>.
15  */
16 
17 #define ACTIVETOUCHESINFO_DEBUG 0
18 #define DIRECTIONALDRAGAREA_DEBUG 0
19 
20 #include "DirectionalDragArea.h"
21 
22 #include <QQuickWindow>
23 #include <QtCore/qmath.h>
24 #include <QDebug>
25 
26 #pragma GCC diagnostic push
27 #pragma GCC diagnostic ignored "-pedantic"
28 #include <private/qquickwindow_p.h>
29 #pragma GCC diagnostic pop
30 
31 // local
32 #include "TouchOwnershipEvent.h"
33 #include "TouchRegistry.h"
34 #include "UnownedTouchEvent.h"
35 
36 using namespace UbuntuGestures;
37 
38 #if DIRECTIONALDRAGAREA_DEBUG
39 #define DDA_DEBUG(msg) qDebug("[DDA] " msg)
40 #include "DebugHelpers.h"
41 
42 namespace {
43 const char *statusToString(DirectionalDragArea::Status status)
44 {
45  if (status == DirectionalDragArea::WaitingForTouch) {
46  return "WaitingForTouch";
47  } else if (status == DirectionalDragArea::Undecided) {
48  return "Undecided";
49  } else {
50  return "Recognized";
51  }
52 }
53 
54 } // namespace {
55 #else // DIRECTIONALDRAGAREA_DEBUG
56 #define DDA_DEBUG(msg) do{}while(0)
57 #endif // DIRECTIONALDRAGAREA_DEBUG
58 
59 
60 DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
61  : QQuickItem(parent)
62  , m_status(WaitingForTouch)
63  , m_sceneDistance(0)
64  , m_touchId(-1)
65  , m_direction(Direction::Rightwards)
66  , m_wideningAngle(0)
67  , m_wideningFactor(0)
68  , m_distanceThreshold(0)
69  , m_distanceThresholdSquared(0.)
70  , m_minSpeed(0)
71  , m_maxSilenceTime(200)
72  , m_silenceTime(0)
73  , m_compositionTime(60)
74  , m_numSamplesOnLastSpeedCheck(0)
75  , m_recognitionTimer(0)
76  , m_velocityCalculator(0)
77  , m_timeSource(new RealTimeSource)
78  , m_activeTouches(m_timeSource)
79 {
80  setRecognitionTimer(new Timer(this));
81  m_recognitionTimer->setInterval(60);
82  m_recognitionTimer->setSingleShot(false);
83 
84  m_velocityCalculator = new AxisVelocityCalculator(this);
85 
86  connect(this, &QQuickItem::enabledChanged, this, &DirectionalDragArea::giveUpIfDisabledOrInvisible);
87  connect(this, &QQuickItem::visibleChanged, this, &DirectionalDragArea::giveUpIfDisabledOrInvisible);
88 }
89 
90 Direction::Type DirectionalDragArea::direction() const
91 {
92  return m_direction;
93 }
94 
95 void DirectionalDragArea::setDirection(Direction::Type direction)
96 {
97  if (direction != m_direction) {
98  m_direction = direction;
99  Q_EMIT directionChanged(m_direction);
100  }
101 }
102 
103 void DirectionalDragArea::setMaxDeviation(qreal value)
104 {
105  if (m_dampedScenePos.maxDelta() != value) {
106  m_dampedScenePos.setMaxDelta(value);
107  Q_EMIT maxDeviationChanged(value);
108  }
109 }
110 
111 qreal DirectionalDragArea::wideningAngle() const
112 {
113  return m_wideningAngle;
114 }
115 
116 void DirectionalDragArea::setWideningAngle(qreal angle)
117 {
118  if (angle == m_wideningAngle)
119  return;
120 
121  m_wideningAngle = angle;
122 
123  // wideningFactor = pow(cosine(angle), 2)
124  {
125  qreal angleRadians = angle * M_PI / 180.0;
126  m_wideningFactor = qCos(angleRadians);
127  m_wideningFactor = m_wideningFactor * m_wideningFactor;
128  }
129 
130  Q_EMIT wideningAngleChanged(angle);
131 }
132 
133 void DirectionalDragArea::setDistanceThreshold(qreal value)
134 {
135  if (m_distanceThreshold != value) {
136  m_distanceThreshold = value;
137  m_distanceThresholdSquared = m_distanceThreshold * m_distanceThreshold;
138  Q_EMIT distanceThresholdChanged(value);
139  }
140 }
141 
142 void DirectionalDragArea::setMinSpeed(qreal value)
143 {
144  if (m_minSpeed != value) {
145  m_minSpeed = value;
146  Q_EMIT minSpeedChanged(value);
147  }
148 }
149 
150 void DirectionalDragArea::setMaxSilenceTime(int value)
151 {
152  if (m_maxSilenceTime != value) {
153  m_maxSilenceTime = value;
154  Q_EMIT maxSilenceTimeChanged(value);
155  }
156 }
157 
158 void DirectionalDragArea::setCompositionTime(int value)
159 {
160  if (m_compositionTime != value) {
161  m_compositionTime = value;
162  Q_EMIT compositionTimeChanged(value);
163  }
164 }
165 
166 void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
167 {
168  int interval = 0;
169  bool timerWasRunning = false;
170  bool wasSingleShot = false;
171 
172  // can be null when called from the constructor
173  if (m_recognitionTimer) {
174  interval = m_recognitionTimer->interval();
175  timerWasRunning = m_recognitionTimer->isRunning();
176  if (m_recognitionTimer->parent() == this) {
177  delete m_recognitionTimer;
178  }
179  }
180 
181  m_recognitionTimer = timer;
182  timer->setInterval(interval);
183  timer->setSingleShot(wasSingleShot);
184  connect(timer, &UbuntuGestures::AbstractTimer::timeout,
185  this, &DirectionalDragArea::checkSpeed);
186  if (timerWasRunning) {
187  m_recognitionTimer->start();
188  }
189 }
190 
191 void DirectionalDragArea::setTimeSource(const SharedTimeSource &timeSource)
192 {
193  m_timeSource = timeSource;
194  m_velocityCalculator->setTimeSource(timeSource);
195  m_activeTouches.m_timeSource = timeSource;
196 }
197 
198 qreal DirectionalDragArea::distance() const
199 {
200  if (Direction::isHorizontal(m_direction)) {
201  return m_previousPos.x() - m_startPos.x();
202  } else {
203  return m_previousPos.y() - m_startPos.y();
204  }
205 }
206 
207 void DirectionalDragArea::updateSceneDistance()
208 {
209  QPointF totalMovement = m_previousScenePos - m_startScenePos;
210  m_sceneDistance = projectOntoDirectionVector(totalMovement);
211 }
212 
213 qreal DirectionalDragArea::sceneDistance() const
214 {
215  return m_sceneDistance;
216 }
217 
218 qreal DirectionalDragArea::touchX() const
219 {
220  return m_previousPos.x();
221 }
222 
223 qreal DirectionalDragArea::touchY() const
224 {
225  return m_previousPos.y();
226 }
227 
228 qreal DirectionalDragArea::touchSceneX() const
229 {
230  return m_previousScenePos.x();
231 }
232 
233 qreal DirectionalDragArea::touchSceneY() const
234 {
235  return m_previousScenePos.y();
236 }
237 
238 bool DirectionalDragArea::event(QEvent *event)
239 {
240  if (event->type() == TouchOwnershipEvent::touchOwnershipEventType()) {
241  touchOwnershipEvent(static_cast<TouchOwnershipEvent *>(event));
242  return true;
243  } else if (event->type() == UnownedTouchEvent::unownedTouchEventType()) {
244  unownedTouchEvent(static_cast<UnownedTouchEvent *>(event));
245  return true;
246  } else {
247  return QQuickItem::event(event);
248  }
249 }
250 
251 void DirectionalDragArea::touchOwnershipEvent(TouchOwnershipEvent *event)
252 {
253  if (event->gained()) {
254  QVector<int> ids;
255  ids.append(event->touchId());
256  DDA_DEBUG("grabbing touch");
257  grabTouchPoints(ids);
258 
259  // Work around for Qt bug. If we grab a touch that is being used for mouse pointer
260  // emulation it will cause the emulation logic to go nuts.
261  // Thus we have to also grab the mouse in this case.
262  //
263  // The fix for this bug has landed in Qt 5.4 (https://codereview.qt-project.org/96887)
264  // TODO: Remove this workaround once we start using Qt 5.4
265  if (window()) {
266  QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(window());
267  if (windowPrivate->touchMouseId == event->touchId() && window()->mouseGrabberItem()) {
268  DDA_DEBUG("removing mouse grabber");
269  window()->mouseGrabberItem()->ungrabMouse();
270  }
271  }
272  } else {
273  // We still wanna know when it ends for keeping the composition time window up-to-date
274  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
275 
276  setStatus(WaitingForTouch);
277  }
278 }
279 
280 void DirectionalDragArea::unownedTouchEvent(UnownedTouchEvent *unownedTouchEvent)
281 {
282  QTouchEvent *event = unownedTouchEvent->touchEvent();
283 
284  Q_ASSERT(!event->touchPointStates().testFlag(Qt::TouchPointPressed));
285 
286  #if DIRECTIONALDRAGAREA_DEBUG
287  // TODO Consider using qCDebug() when available (Qt 5.2)
288  qDebug() << "[DDA] Unowned" << m_timeSource->msecsSinceReference()
289  << qPrintable(touchEventToString(event));
290  #endif
291 
292  switch (m_status) {
293  case WaitingForTouch:
294  // do nothing
295  break;
296  case Undecided:
297  Q_ASSERT(isEnabled() && isVisible());
298  unownedTouchEvent_undecided(unownedTouchEvent);
299  break;
300  default: // Recognized:
301  // do nothing
302  break;
303  }
304 
305  m_activeTouches.update(event);
306 }
307 
308 void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unownedTouchEvent)
309 {
310  const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(unownedTouchEvent->touchEvent());
311  if (!touchPoint) {
312  qCritical() << "DirectionalDragArea[status=Undecided]: touch " << m_touchId
313  << "missing from UnownedTouchEvent without first reaching state Qt::TouchPointReleased. "
314  "Considering it as released.";
315 
316  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
317  setStatus(WaitingForTouch);
318  return;
319  }
320 
321  const QPointF &touchScenePos = touchPoint->scenePos();
322 
323  if (touchPoint->state() == Qt::TouchPointReleased) {
324  // touch has ended before recognition concluded
325  DDA_DEBUG("Touch has ended before recognition concluded");
326  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
327  emitSignalIfTapped();
328  setStatus(WaitingForTouch);
329  return;
330  }
331 
332  m_previousDampedScenePos.setX(m_dampedScenePos.x());
333  m_previousDampedScenePos.setY(m_dampedScenePos.y());
334  m_dampedScenePos.update(touchScenePos);
335  updateVelocityCalculator(touchScenePos);
336 
337  if (!pointInsideAllowedArea()) {
338  DDA_DEBUG("Rejecting gesture because touch point is outside allowed area.");
339  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
340  // We still wanna know when it ends for keeping the composition time window up-to-date
341  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
342  setStatus(WaitingForTouch);
343  return;
344  }
345 
346  if (!movingInRightDirection()) {
347  DDA_DEBUG("Rejecting gesture because touch point is moving in the wrong direction.");
348  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
349  // We still wanna know when it ends for keeping the composition time window up-to-date
350  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
351  setStatus(WaitingForTouch);
352  return;
353  }
354 
355  setPreviousPos(touchPoint->pos());
356  setPreviousScenePos(touchScenePos);
357 
358  if (isWithinTouchCompositionWindow()) {
359  // There's still time for some new touch to appear and ruin our party as it would be combined
360  // with our m_touchId one and therefore deny the possibility of a single-finger gesture.
361  DDA_DEBUG("Sill within composition window. Let's wait more.");
362  return;
363  }
364 
365  if (movedFarEnough(touchScenePos)) {
366  TouchRegistry::instance()->requestTouchOwnership(m_touchId, this);
367  setStatus(Recognized);
368  } else {
369  DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
370  }
371 }
372 
373 void DirectionalDragArea::touchEvent(QTouchEvent *event)
374 {
375  // TODO: Consider when more than one touch starts in the same event (although it's not possible
376  // with Mir's android-input). Have to track them all. Consider it a plus/bonus.
377 
378  #if DIRECTIONALDRAGAREA_DEBUG
379  // TODO Consider using qCDebug() when available (Qt 5.2)
380  qDebug() << "[DDA]" << m_timeSource->msecsSinceReference()
381  << qPrintable(touchEventToString(event));
382  #endif
383 
384  if (!isEnabled() || !isVisible()) {
385  QQuickItem::touchEvent(event);
386  return;
387  }
388 
389  switch (m_status) {
390  case WaitingForTouch:
391  touchEvent_absent(event);
392  break;
393  case Undecided:
394  touchEvent_undecided(event);
395  break;
396  default: // Recognized:
397  touchEvent_recognized(event);
398  break;
399  }
400 
401  m_activeTouches.update(event);
402 }
403 
404 void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
405 {
406  // TODO: accept/reject is for the whole event, not per touch id. See how that affects us.
407 
408  if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
409  // Nothing to see here. No touch starting in this event.
410  return;
411  }
412 
413  // to be proven wrong, if that's the case
414  bool allGood = true;
415 
416  if (isWithinTouchCompositionWindow()) {
417  // too close to the last touch start. So we consider them as starting roughly at the same time.
418  // Can't be a single-touch gesture.
419  #if DIRECTIONALDRAGAREA_DEBUG
420  qDebug("[DDA] A new touch point came in but we're still within time composition window. Ignoring it.");
421  #endif
422  allGood = false;
423  }
424 
425  const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
426 
427  const QTouchEvent::TouchPoint *newTouchPoint = nullptr;
428  for (int i = 0; i < touchPoints.count() && allGood; ++i) {
429  const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
430  if (touchPoint.state() == Qt::TouchPointPressed) {
431  if (newTouchPoint) {
432  // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture
433  allGood = false;
434  } else {
435  // that's our candidate
436  m_touchId = touchPoint.id();
437  newTouchPoint = &touchPoint;
438  }
439  }
440  }
441 
442  if (allGood) {
443  Q_ASSERT(newTouchPoint);
444 
445  m_startPos = newTouchPoint->pos();
446  m_startScenePos = newTouchPoint->scenePos();
447  m_touchId = newTouchPoint->id();
448  m_dampedScenePos.reset(m_startScenePos);
449  m_velocityCalculator->setTrackedPosition(0.);
450  m_velocityCalculator->reset();
451  m_numSamplesOnLastSpeedCheck = 0;
452  m_silenceTime = 0;
453  setPreviousPos(m_startPos);
454  setPreviousScenePos(m_startScenePos);
455  updateSceneDirectionVector();
456 
457  if (recognitionIsDisabled()) {
458  // Behave like a dumb TouchArea
459  TouchRegistry::instance()->requestTouchOwnership(m_touchId, this);
460  setStatus(Recognized);
461  event->accept();
462  } else {
463  // just monitor the touch points for now.
464  TouchRegistry::instance()->addCandidateOwnerForTouch(m_touchId, this);
465 
466  setStatus(Undecided);
467  // Let the item below have it. We will monitor it and grab it later if a gesture
468  // gets recognized.
469  event->ignore();
470  }
471  } else {
472  watchPressedTouchPoints(touchPoints);
473  event->ignore();
474  }
475 }
476 
477 void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
478 {
479  Q_ASSERT(event->type() == QEvent::TouchBegin);
480  Q_ASSERT(fetchTargetTouchPoint(event) == nullptr);
481 
482  // We're not interested in new touch points. We already have our candidate (m_touchId).
483  // But we do want to know when those new touches end for keeping the composition time
484  // window up-to-date
485  event->ignore();
486  watchPressedTouchPoints(event->touchPoints());
487 
488  if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
489  // multi-finger drags are not accepted
490  DDA_DEBUG("Multi-finger drags are not accepted");
491 
492  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
493  // We still wanna know when it ends for keeping the composition time window up-to-date
494  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
495 
496  setStatus(WaitingForTouch);
497  }
498 }
499 
500 void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
501 {
502  const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
503 
504  if (!touchPoint) {
505  qCritical() << "DirectionalDragArea[status=Recognized]: touch " << m_touchId
506  << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
507  "Considering it as released.";
508  setStatus(WaitingForTouch);
509  } else {
510  setPreviousPos(touchPoint->pos());
511  setPreviousScenePos(touchPoint->scenePos());
512 
513  if (touchPoint->state() == Qt::TouchPointReleased) {
514  emitSignalIfTapped();
515  setStatus(WaitingForTouch);
516  }
517  }
518 }
519 
520 void DirectionalDragArea::watchPressedTouchPoints(const QList<QTouchEvent::TouchPoint> &touchPoints)
521 {
522  for (int i = 0; i < touchPoints.count(); ++i) {
523  const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
524  if (touchPoint.state() == Qt::TouchPointPressed) {
525  TouchRegistry::instance()->addTouchWatcher(touchPoint.id(), this);
526  }
527  }
528 }
529 
530 bool DirectionalDragArea::recognitionIsDisabled() const
531 {
532  return distanceThreshold() <= 0 && compositionTime() <= 0;
533 }
534 
535 void DirectionalDragArea::emitSignalIfTapped()
536 {
537  qint64 touchDuration = m_timeSource->msecsSinceReference() - m_activeTouches.touchStartTime(m_touchId);
538  if (touchDuration <= maxTapDuration()) {
539  Q_EMIT tapped();
540  }
541 }
542 
543 const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
544 {
545  const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
546  const QTouchEvent::TouchPoint *touchPoint = 0;
547  for (int i = 0; i < touchPoints.size(); ++i) {
548  if (touchPoints.at(i).id() == m_touchId) {
549  touchPoint = &touchPoints.at(i);
550  break;
551  }
552  }
553  return touchPoint;
554 }
555 
556 bool DirectionalDragArea::pointInsideAllowedArea() const
557 {
558  // NB: Using squared values to avoid computing the square root to find
559  // the length totalMovement
560 
561  QPointF totalMovement(m_dampedScenePos.x() - m_startScenePos.x(),
562  m_dampedScenePos.y() - m_startScenePos.y());
563 
564  qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
565  totalMovement.y() * totalMovement.y();
566 
567  if (squaredTotalMovSize == 0.) {
568  // didn't move
569  return true;
570  }
571 
572  qreal projectedMovement = projectOntoDirectionVector(totalMovement);
573 
574 
575  qreal cosineAngleSquared = (projectedMovement * projectedMovement) / squaredTotalMovSize;
576 
577  // Same as:
578  // angle_between_movement_vector_and_gesture_direction_vector <= widening_angle
579  return cosineAngleSquared >= m_wideningFactor;
580 }
581 
582 bool DirectionalDragArea::movingInRightDirection() const
583 {
584  QPointF movementVector(m_dampedScenePos.x() - m_previousDampedScenePos.x(),
585  m_dampedScenePos.y() - m_previousDampedScenePos.y());
586 
587  qreal scalarProjection = projectOntoDirectionVector(movementVector);
588 
589  return scalarProjection >= 0.;
590 }
591 
592 bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
593 {
594  if (m_distanceThreshold <= 0.) {
595  // distance threshold check is disabled
596  return true;
597  } else {
598  QPointF totalMovement(point.x() - m_startScenePos.x(),
599  point.y() - m_startScenePos.y());
600 
601  qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
602  totalMovement.y() * totalMovement.y();
603 
604  return squaredTotalMovSize > m_distanceThresholdSquared;
605  }
606 }
607 
608 void DirectionalDragArea::checkSpeed()
609 {
610  Q_ASSERT(m_status == Undecided);
611 
612  if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
613  qreal speed = qFabs(m_velocityCalculator->calculate());
614  qreal minSpeedMsecs = m_minSpeed / 1000.0;
615 
616  if (speed < minSpeedMsecs) {
617  DDA_DEBUG("Rejecting gesture because it's below minimum speed.");
618  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
619  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
620  setStatus(WaitingForTouch);
621  }
622  }
623 
624  if (m_velocityCalculator->numSamples() == m_numSamplesOnLastSpeedCheck) {
625  m_silenceTime += m_recognitionTimer->interval();
626 
627  if (m_silenceTime > m_maxSilenceTime) {
628  DDA_DEBUG("Rejecting gesture because its silence time has been exceeded.");
629  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
630  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
631  setStatus(WaitingForTouch);
632  }
633  } else {
634  m_silenceTime = 0;
635  }
636  m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
637 }
638 
639 void DirectionalDragArea::giveUpIfDisabledOrInvisible()
640 {
641  if (!isEnabled() || !isVisible()) {
642  if (m_status == Undecided) {
643  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
644  // We still wanna know when it ends for keeping the composition time window up-to-date
645  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
646  }
647 
648  if (m_status != WaitingForTouch) {
649  DDA_DEBUG("Resetting status because got disabled or made invisible");
650  setStatus(WaitingForTouch);
651  }
652  }
653 }
654 
655 void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
656 {
657  if (newStatus == m_status)
658  return;
659 
660  DirectionalDragArea::Status oldStatus = m_status;
661 
662  if (oldStatus == Undecided) {
663  m_recognitionTimer->stop();
664  }
665 
666  m_status = newStatus;
667  Q_EMIT statusChanged(m_status);
668 
669  #if DIRECTIONALDRAGAREA_DEBUG
670  qDebug() << "[DDA]" << statusToString(oldStatus) << "->" << statusToString(newStatus);
671  #endif
672 
673  switch (newStatus) {
674  case WaitingForTouch:
675  Q_EMIT draggingChanged(false);
676  break;
677  case Undecided:
678  m_recognitionTimer->start();
679  Q_EMIT draggingChanged(true);
680  break;
681  case Recognized:
682  if (oldStatus == WaitingForTouch)
683  Q_EMIT draggingChanged(true);
684  break;
685  default:
686  // no-op
687  break;
688  }
689 }
690 
691 void DirectionalDragArea::setPreviousPos(const QPointF &point)
692 {
693  bool xChanged = m_previousPos.x() != point.x();
694  bool yChanged = m_previousPos.y() != point.y();
695 
696  m_previousPos = point;
697 
698  if (xChanged) {
699  Q_EMIT touchXChanged(point.x());
700  if (Direction::isHorizontal(m_direction))
701  Q_EMIT distanceChanged(distance());
702  }
703 
704  if (yChanged) {
705  Q_EMIT touchYChanged(point.y());
706  if (Direction::isVertical(m_direction))
707  Q_EMIT distanceChanged(distance());
708  }
709 }
710 
711 void DirectionalDragArea::setPreviousScenePos(const QPointF &point)
712 {
713  bool xChanged = m_previousScenePos.x() != point.x();
714  bool yChanged = m_previousScenePos.y() != point.y();
715 
716  if (!xChanged && !yChanged)
717  return;
718 
719  qreal oldSceneDistance = sceneDistance();
720  m_previousScenePos = point;
721  updateSceneDistance();
722 
723  if (oldSceneDistance != sceneDistance()) {
724  Q_EMIT sceneDistanceChanged(sceneDistance());
725  }
726 
727  if (xChanged) {
728  Q_EMIT touchSceneXChanged(point.x());
729  }
730 
731  if (yChanged) {
732  Q_EMIT touchSceneYChanged(point.y());
733  }
734 }
735 
736 void DirectionalDragArea::updateVelocityCalculator(const QPointF &scenePos)
737 {
738  QPointF totalSceneMovement = scenePos - m_startScenePos;
739 
740  qreal scalarProjection = projectOntoDirectionVector(totalSceneMovement);
741 
742  m_velocityCalculator->setTrackedPosition(scalarProjection);
743 }
744 
745 bool DirectionalDragArea::isWithinTouchCompositionWindow()
746 {
747  return
748  compositionTime() > 0 &&
749  !m_activeTouches.isEmpty() &&
750  m_timeSource->msecsSinceReference() <=
751  m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
752 }
753 
754 //************************** ActiveTouchesInfo **************************
755 
756 DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(const SharedTimeSource &timeSource)
757  : m_timeSource(timeSource)
758 {
759 }
760 
761 void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
762 {
763  if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
764  // nothing to update
765  #if ACTIVETOUCHESINFO_DEBUG
766  qDebug("[DDA::ActiveTouchesInfo] Nothing to Update");
767  #endif
768  return;
769  }
770 
771  const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
772  for (int i = 0; i < touchPoints.count(); ++i) {
773  const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
774  if (touchPoint.state() == Qt::TouchPointPressed) {
775  addTouchPoint(touchPoint.id());
776  } else if (touchPoint.state() == Qt::TouchPointReleased) {
777  removeTouchPoint(touchPoint.id());
778  }
779  }
780 }
781 
782 #if ACTIVETOUCHESINFO_DEBUG
783 QString DirectionalDragArea::ActiveTouchesInfo::toString()
784 {
785  QString string = "(";
786 
787  {
788  QTextStream stream(&string);
789  m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
790  stream << "(id=" << touchInfo->id << ",startTime=" << touchInfo->startTime << ")";
791  return true;
792  });
793  }
794 
795  string.append(")");
796 
797  return string;
798 }
799 #endif // ACTIVETOUCHESINFO_DEBUG
800 
801 void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(int touchId)
802 {
803  ActiveTouchInfo &activeTouchInfo = m_touchInfoPool.getEmptySlot();
804  activeTouchInfo.id = touchId;
805  activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
806 
807  #if ACTIVETOUCHESINFO_DEBUG
808  qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString());
809  #endif
810 }
811 
812 qint64 DirectionalDragArea::ActiveTouchesInfo::touchStartTime(int touchId)
813 {
814  qint64 result = -1;
815 
816  m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
817  if (touchId == touchInfo->id) {
818  result = touchInfo->startTime;
819  return false;
820  } else {
821  return true;
822  }
823  });
824 
825  Q_ASSERT(result != -1);
826  return result;
827 }
828 
829 void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(int touchId)
830 {
831  m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
832  if (touchId == touchInfo->id) {
833  m_touchInfoPool.freeSlot(touchInfo);
834  return false;
835  } else {
836  return true;
837  }
838  });
839 
840  #if ACTIVETOUCHESINFO_DEBUG
841  qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString());
842  #endif
843 }
844 
845 qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
846 {
847  Q_ASSERT(!m_touchInfoPool.isEmpty());
848 
849  qint64 highestStartTime = -1;
850 
851  m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &activeTouchInfo) {
852  if (activeTouchInfo->startTime > highestStartTime) {
853  highestStartTime = activeTouchInfo->startTime;
854  }
855  return true;
856  });
857 
858  return highestStartTime;
859 }
860 
861 void DirectionalDragArea::updateSceneDirectionVector()
862 {
863  QPointF localOrigin(0., 0.);
864  QPointF localDirection;
865  switch (m_direction) {
866  case Direction::Upwards:
867  localDirection.rx() = 0.;
868  localDirection.ry() = -1.;
869  break;
870  case Direction::Downwards:
871  localDirection.rx() = 0.;
872  localDirection.ry() = 1;
873  break;
874  case Direction::Leftwards:
875  localDirection.rx() = -1.;
876  localDirection.ry() = 0.;
877  break;
878  default: // Direction::Rightwards:
879  localDirection.rx() = 1.;
880  localDirection.ry() = 0.;
881  break;
882  }
883  QPointF sceneOrigin = mapToScene(localOrigin);
884  QPointF sceneDirection = mapToScene(localDirection);
885  m_sceneDirectionVector = sceneDirection - sceneOrigin;
886 }
887 
888 qreal DirectionalDragArea::projectOntoDirectionVector(const QPointF &sceneVector) const
889 {
890  // same as dot product as m_sceneDirectionVector is a unit vector
891  return sceneVector.x() * m_sceneDirectionVector.x() +
892  sceneVector.y() * m_sceneDirectionVector.y();
893 }
894 
895 // Because we are defining a new QObject-based class (RecognitionTimer) here.
896 #include "DirectionalDragArea.moc"