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  // TODO: Report bug to Qt
263  if (window()) {
264  QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(window());
265  if (windowPrivate->touchMouseId == event->touchId() && window()->mouseGrabberItem()) {
266  DDA_DEBUG("removing mouse grabber");
267  window()->mouseGrabberItem()->ungrabMouse();
268  }
269  }
270  } else {
271  // We still wanna know when it ends for keeping the composition time window up-to-date
272  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
273 
274  setStatus(WaitingForTouch);
275  }
276 }
277 
278 void DirectionalDragArea::unownedTouchEvent(UnownedTouchEvent *unownedTouchEvent)
279 {
280  QTouchEvent *event = unownedTouchEvent->touchEvent();
281 
282  Q_ASSERT(!event->touchPointStates().testFlag(Qt::TouchPointPressed));
283 
284  #if DIRECTIONALDRAGAREA_DEBUG
285  // TODO Consider using qCDebug() when available (Qt 5.2)
286  qDebug() << "[DDA] Unowned" << m_timeSource->msecsSinceReference()
287  << qPrintable(touchEventToString(event));
288  #endif
289 
290  switch (m_status) {
291  case WaitingForTouch:
292  // do nothing
293  break;
294  case Undecided:
295  Q_ASSERT(isEnabled() && isVisible());
296  unownedTouchEvent_undecided(unownedTouchEvent);
297  break;
298  default: // Recognized:
299  // do nothing
300  break;
301  }
302 
303  m_activeTouches.update(event);
304 }
305 
306 void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unownedTouchEvent)
307 {
308  const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(unownedTouchEvent->touchEvent());
309  if (!touchPoint) {
310  qCritical() << "DirectionalDragArea[status=Undecided]: touch " << m_touchId
311  << "missing from UnownedTouchEvent without first reaching state Qt::TouchPointReleased. "
312  "Considering it as released.";
313 
314  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
315  setStatus(WaitingForTouch);
316  return;
317  }
318 
319  const QPointF &touchScenePos = touchPoint->scenePos();
320 
321  if (touchPoint->state() == Qt::TouchPointReleased) {
322  // touch has ended before recognition concluded
323  DDA_DEBUG("Touch has ended before recognition concluded");
324  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
325  emitSignalIfTapped();
326  setStatus(WaitingForTouch);
327  return;
328  }
329 
330  m_previousDampedScenePos.setX(m_dampedScenePos.x());
331  m_previousDampedScenePos.setY(m_dampedScenePos.y());
332  m_dampedScenePos.update(touchScenePos);
333  updateVelocityCalculator(touchScenePos);
334 
335  if (!pointInsideAllowedArea()) {
336  DDA_DEBUG("Rejecting gesture because touch point is outside allowed area.");
337  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
338  // We still wanna know when it ends for keeping the composition time window up-to-date
339  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
340  setStatus(WaitingForTouch);
341  return;
342  }
343 
344  if (!movingInRightDirection()) {
345  DDA_DEBUG("Rejecting gesture because touch point is moving in the wrong direction.");
346  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
347  // We still wanna know when it ends for keeping the composition time window up-to-date
348  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
349  setStatus(WaitingForTouch);
350  return;
351  }
352 
353  setPreviousPos(touchPoint->pos());
354  setPreviousScenePos(touchScenePos);
355 
356  if (isWithinTouchCompositionWindow()) {
357  // There's still time for some new touch to appear and ruin our party as it would be combined
358  // with our m_touchId one and therefore deny the possibility of a single-finger gesture.
359  DDA_DEBUG("Sill within composition window. Let's wait more.");
360  return;
361  }
362 
363  if (movedFarEnough(touchScenePos)) {
364  TouchRegistry::instance()->requestTouchOwnership(m_touchId, this);
365  setStatus(Recognized);
366  } else {
367  DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
368  }
369 }
370 
371 void DirectionalDragArea::touchEvent(QTouchEvent *event)
372 {
373  // TODO: Consider when more than one touch starts in the same event (although it's not possible
374  // with Mir's android-input). Have to track them all. Consider it a plus/bonus.
375 
376  #if DIRECTIONALDRAGAREA_DEBUG
377  // TODO Consider using qCDebug() when available (Qt 5.2)
378  qDebug() << "[DDA]" << m_timeSource->msecsSinceReference()
379  << qPrintable(touchEventToString(event));
380  #endif
381 
382  if (!isEnabled() || !isVisible()) {
383  QQuickItem::touchEvent(event);
384  return;
385  }
386 
387  switch (m_status) {
388  case WaitingForTouch:
389  touchEvent_absent(event);
390  break;
391  case Undecided:
392  touchEvent_undecided(event);
393  break;
394  default: // Recognized:
395  touchEvent_recognized(event);
396  break;
397  }
398 
399  m_activeTouches.update(event);
400 }
401 
402 void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
403 {
404  // TODO: accept/reject is for the whole event, not per touch id. See how that affects us.
405 
406  if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
407  // Nothing to see here. No touch starting in this event.
408  return;
409  }
410 
411  // to be proven wrong, if that's the case
412  bool allGood = true;
413 
414  if (isWithinTouchCompositionWindow()) {
415  // too close to the last touch start. So we consider them as starting roughly at the same time.
416  // Can't be a single-touch gesture.
417  #if DIRECTIONALDRAGAREA_DEBUG
418  qDebug("[DDA] A new touch point came in but we're still within time composition window. Ignoring it.");
419  #endif
420  allGood = false;
421  }
422 
423  const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
424 
425  const QTouchEvent::TouchPoint *newTouchPoint = nullptr;
426  for (int i = 0; i < touchPoints.count() && allGood; ++i) {
427  const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
428  if (touchPoint.state() == Qt::TouchPointPressed) {
429  if (newTouchPoint) {
430  // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture
431  allGood = false;
432  } else {
433  // that's our candidate
434  m_touchId = touchPoint.id();
435  newTouchPoint = &touchPoint;
436  }
437  }
438  }
439 
440  if (allGood) {
441  Q_ASSERT(newTouchPoint);
442 
443  m_startPos = newTouchPoint->pos();
444  m_startScenePos = newTouchPoint->scenePos();
445  m_touchId = newTouchPoint->id();
446  m_dampedScenePos.reset(m_startScenePos);
447  m_velocityCalculator->setTrackedPosition(0.);
448  m_velocityCalculator->reset();
449  m_numSamplesOnLastSpeedCheck = 0;
450  m_silenceTime = 0;
451  setPreviousPos(m_startPos);
452  setPreviousScenePos(m_startScenePos);
453  updateSceneDirectionVector();
454 
455  if (recognitionIsDisabled()) {
456  // Behave like a dumb TouchArea
457  TouchRegistry::instance()->requestTouchOwnership(m_touchId, this);
458  setStatus(Recognized);
459  event->accept();
460  } else {
461  // just monitor the touch points for now.
462  TouchRegistry::instance()->addCandidateOwnerForTouch(m_touchId, this);
463 
464  setStatus(Undecided);
465  // Let the item below have it. We will monitor it and grab it later if a gesture
466  // gets recognized.
467  event->ignore();
468  }
469  } else {
470  watchPressedTouchPoints(touchPoints);
471  event->ignore();
472  }
473 }
474 
475 void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
476 {
477  Q_ASSERT(event->type() == QEvent::TouchBegin);
478  Q_ASSERT(fetchTargetTouchPoint(event) == nullptr);
479 
480  // We're not interested in new touch points. We already have our candidate (m_touchId).
481  // But we do want to know when those new touches end for keeping the composition time
482  // window up-to-date
483  event->ignore();
484  watchPressedTouchPoints(event->touchPoints());
485 
486  if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
487  // multi-finger drags are not accepted
488  DDA_DEBUG("Multi-finger drags are not accepted");
489 
490  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
491  // We still wanna know when it ends for keeping the composition time window up-to-date
492  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
493 
494  setStatus(WaitingForTouch);
495  }
496 }
497 
498 void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
499 {
500  const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
501 
502  if (!touchPoint) {
503  qCritical() << "DirectionalDragArea[status=Recognized]: touch " << m_touchId
504  << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
505  "Considering it as released.";
506  setStatus(WaitingForTouch);
507  } else {
508  setPreviousPos(touchPoint->pos());
509  setPreviousScenePos(touchPoint->scenePos());
510 
511  if (touchPoint->state() == Qt::TouchPointReleased) {
512  emitSignalIfTapped();
513  setStatus(WaitingForTouch);
514  }
515  }
516 }
517 
518 void DirectionalDragArea::watchPressedTouchPoints(const QList<QTouchEvent::TouchPoint> &touchPoints)
519 {
520  for (int i = 0; i < touchPoints.count(); ++i) {
521  const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
522  if (touchPoint.state() == Qt::TouchPointPressed) {
523  TouchRegistry::instance()->addTouchWatcher(touchPoint.id(), this);
524  }
525  }
526 }
527 
528 bool DirectionalDragArea::recognitionIsDisabled() const
529 {
530  return distanceThreshold() <= 0 && compositionTime() <= 0;
531 }
532 
533 void DirectionalDragArea::emitSignalIfTapped()
534 {
535  qint64 touchDuration = m_timeSource->msecsSinceReference() - m_activeTouches.touchStartTime(m_touchId);
536  if (touchDuration <= maxTapDuration()) {
537  Q_EMIT tapped();
538  }
539 }
540 
541 const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
542 {
543  const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
544  const QTouchEvent::TouchPoint *touchPoint = 0;
545  for (int i = 0; i < touchPoints.size(); ++i) {
546  if (touchPoints.at(i).id() == m_touchId) {
547  touchPoint = &touchPoints.at(i);
548  break;
549  }
550  }
551  return touchPoint;
552 }
553 
554 bool DirectionalDragArea::pointInsideAllowedArea() const
555 {
556  // NB: Using squared values to avoid computing the square root to find
557  // the length totalMovement
558 
559  QPointF totalMovement(m_dampedScenePos.x() - m_startScenePos.x(),
560  m_dampedScenePos.y() - m_startScenePos.y());
561 
562  qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
563  totalMovement.y() * totalMovement.y();
564 
565  if (squaredTotalMovSize == 0.) {
566  // didn't move
567  return true;
568  }
569 
570  qreal projectedMovement = projectOntoDirectionVector(totalMovement);
571 
572 
573  qreal cosineAngleSquared = (projectedMovement * projectedMovement) / squaredTotalMovSize;
574 
575  // Same as:
576  // angle_between_movement_vector_and_gesture_direction_vector <= widening_angle
577  return cosineAngleSquared >= m_wideningFactor;
578 }
579 
580 bool DirectionalDragArea::movingInRightDirection() const
581 {
582  QPointF movementVector(m_dampedScenePos.x() - m_previousDampedScenePos.x(),
583  m_dampedScenePos.y() - m_previousDampedScenePos.y());
584 
585  qreal scalarProjection = projectOntoDirectionVector(movementVector);
586 
587  return scalarProjection >= 0.;
588 }
589 
590 bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
591 {
592  if (m_distanceThreshold <= 0.) {
593  // distance threshold check is disabled
594  return true;
595  } else {
596  QPointF totalMovement(point.x() - m_startScenePos.x(),
597  point.y() - m_startScenePos.y());
598 
599  qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
600  totalMovement.y() * totalMovement.y();
601 
602  return squaredTotalMovSize > m_distanceThresholdSquared;
603  }
604 }
605 
606 void DirectionalDragArea::checkSpeed()
607 {
608  Q_ASSERT(m_status == Undecided);
609 
610  if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
611  qreal speed = qFabs(m_velocityCalculator->calculate());
612  qreal minSpeedMsecs = m_minSpeed / 1000.0;
613 
614  if (speed < minSpeedMsecs) {
615  DDA_DEBUG("Rejecting gesture because it's below minimum speed.");
616  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
617  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
618  setStatus(WaitingForTouch);
619  }
620  }
621 
622  if (m_velocityCalculator->numSamples() == m_numSamplesOnLastSpeedCheck) {
623  m_silenceTime += m_recognitionTimer->interval();
624 
625  if (m_silenceTime > m_maxSilenceTime) {
626  DDA_DEBUG("Rejecting gesture because its silence time has been exceeded.");
627  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
628  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
629  setStatus(WaitingForTouch);
630  }
631  } else {
632  m_silenceTime = 0;
633  }
634  m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
635 }
636 
637 void DirectionalDragArea::giveUpIfDisabledOrInvisible()
638 {
639  if (!isEnabled() || !isVisible()) {
640  if (m_status == Undecided) {
641  TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this);
642  // We still wanna know when it ends for keeping the composition time window up-to-date
643  TouchRegistry::instance()->addTouchWatcher(m_touchId, this);
644  }
645 
646  if (m_status != WaitingForTouch) {
647  DDA_DEBUG("Resetting status because got disabled or made invisible");
648  setStatus(WaitingForTouch);
649  }
650  }
651 }
652 
653 void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
654 {
655  if (newStatus == m_status)
656  return;
657 
658  DirectionalDragArea::Status oldStatus = m_status;
659 
660  if (oldStatus == Undecided) {
661  m_recognitionTimer->stop();
662  }
663 
664  m_status = newStatus;
665  Q_EMIT statusChanged(m_status);
666 
667  #if DIRECTIONALDRAGAREA_DEBUG
668  qDebug() << "[DDA]" << statusToString(oldStatus) << "->" << statusToString(newStatus);
669  #endif
670 
671  switch (newStatus) {
672  case WaitingForTouch:
673  Q_EMIT draggingChanged(false);
674  break;
675  case Undecided:
676  m_recognitionTimer->start();
677  Q_EMIT draggingChanged(true);
678  break;
679  case Recognized:
680  if (oldStatus == WaitingForTouch)
681  Q_EMIT draggingChanged(true);
682  break;
683  default:
684  // no-op
685  break;
686  }
687 }
688 
689 void DirectionalDragArea::setPreviousPos(const QPointF &point)
690 {
691  bool xChanged = m_previousPos.x() != point.x();
692  bool yChanged = m_previousPos.y() != point.y();
693 
694  m_previousPos = point;
695 
696  if (xChanged) {
697  Q_EMIT touchXChanged(point.x());
698  if (Direction::isHorizontal(m_direction))
699  Q_EMIT distanceChanged(distance());
700  }
701 
702  if (yChanged) {
703  Q_EMIT touchYChanged(point.y());
704  if (Direction::isVertical(m_direction))
705  Q_EMIT distanceChanged(distance());
706  }
707 }
708 
709 void DirectionalDragArea::setPreviousScenePos(const QPointF &point)
710 {
711  bool xChanged = m_previousScenePos.x() != point.x();
712  bool yChanged = m_previousScenePos.y() != point.y();
713 
714  if (!xChanged && !yChanged)
715  return;
716 
717  qreal oldSceneDistance = sceneDistance();
718  m_previousScenePos = point;
719  updateSceneDistance();
720 
721  if (oldSceneDistance != sceneDistance()) {
722  Q_EMIT sceneDistanceChanged(sceneDistance());
723  }
724 
725  if (xChanged) {
726  Q_EMIT touchSceneXChanged(point.x());
727  }
728 
729  if (yChanged) {
730  Q_EMIT touchSceneYChanged(point.y());
731  }
732 }
733 
734 void DirectionalDragArea::updateVelocityCalculator(const QPointF &scenePos)
735 {
736  QPointF totalSceneMovement = scenePos - m_startScenePos;
737 
738  qreal scalarProjection = projectOntoDirectionVector(totalSceneMovement);
739 
740  m_velocityCalculator->setTrackedPosition(scalarProjection);
741 }
742 
743 bool DirectionalDragArea::isWithinTouchCompositionWindow()
744 {
745  return
746  compositionTime() > 0 &&
747  !m_activeTouches.isEmpty() &&
748  m_timeSource->msecsSinceReference() <=
749  m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
750 }
751 
752 //************************** ActiveTouchesInfo **************************
753 
754 DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(const SharedTimeSource &timeSource)
755  : m_timeSource(timeSource)
756 {
757 }
758 
759 void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
760 {
761  if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
762  // nothing to update
763  #if ACTIVETOUCHESINFO_DEBUG
764  qDebug("[DDA::ActiveTouchesInfo] Nothing to Update");
765  #endif
766  return;
767  }
768 
769  const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
770  for (int i = 0; i < touchPoints.count(); ++i) {
771  const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
772  if (touchPoint.state() == Qt::TouchPointPressed) {
773  addTouchPoint(touchPoint.id());
774  } else if (touchPoint.state() == Qt::TouchPointReleased) {
775  removeTouchPoint(touchPoint.id());
776  }
777  }
778 }
779 
780 #if ACTIVETOUCHESINFO_DEBUG
781 QString DirectionalDragArea::ActiveTouchesInfo::toString()
782 {
783  QString string = "(";
784 
785  {
786  QTextStream stream(&string);
787  m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
788  stream << "(id=" << touchInfo->id << ",startTime=" << touchInfo->startTime << ")";
789  return true;
790  });
791  }
792 
793  string.append(")");
794 
795  return string;
796 }
797 #endif // ACTIVETOUCHESINFO_DEBUG
798 
799 void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(int touchId)
800 {
801  ActiveTouchInfo &activeTouchInfo = m_touchInfoPool.getEmptySlot();
802  activeTouchInfo.id = touchId;
803  activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
804 
805  #if ACTIVETOUCHESINFO_DEBUG
806  qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString());
807  #endif
808 }
809 
810 qint64 DirectionalDragArea::ActiveTouchesInfo::touchStartTime(int touchId)
811 {
812  qint64 result = -1;
813 
814  m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
815  if (touchId == touchInfo->id) {
816  result = touchInfo->startTime;
817  return false;
818  } else {
819  return true;
820  }
821  });
822 
823  Q_ASSERT(result != -1);
824  return result;
825 }
826 
827 void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(int touchId)
828 {
829  m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
830  if (touchId == touchInfo->id) {
831  m_touchInfoPool.freeSlot(touchInfo);
832  return false;
833  } else {
834  return true;
835  }
836  });
837 
838  #if ACTIVETOUCHESINFO_DEBUG
839  qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString());
840  #endif
841 }
842 
843 qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
844 {
845  Q_ASSERT(!m_touchInfoPool.isEmpty());
846 
847  qint64 highestStartTime = -1;
848 
849  m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &activeTouchInfo) {
850  if (activeTouchInfo->startTime > highestStartTime) {
851  highestStartTime = activeTouchInfo->startTime;
852  }
853  return true;
854  });
855 
856  return highestStartTime;
857 }
858 
859 void DirectionalDragArea::updateSceneDirectionVector()
860 {
861  QPointF localOrigin(0., 0.);
862  QPointF localDirection;
863  switch (m_direction) {
864  case Direction::Upwards:
865  localDirection.rx() = 0.;
866  localDirection.ry() = -1.;
867  break;
868  case Direction::Downwards:
869  localDirection.rx() = 0.;
870  localDirection.ry() = 1;
871  break;
872  case Direction::Leftwards:
873  localDirection.rx() = -1.;
874  localDirection.ry() = 0.;
875  break;
876  default: // Direction::Rightwards:
877  localDirection.rx() = 1.;
878  localDirection.ry() = 0.;
879  break;
880  }
881  QPointF sceneOrigin = mapToScene(localOrigin);
882  QPointF sceneDirection = mapToScene(localDirection);
883  m_sceneDirectionVector = sceneDirection - sceneOrigin;
884 }
885 
886 qreal DirectionalDragArea::projectOntoDirectionVector(const QPointF &sceneVector) const
887 {
888  // same as dot product as m_sceneDirectionVector is a unit vector
889  return sceneVector.x() * m_sceneDirectionVector.x() +
890  sceneVector.y() * m_sceneDirectionVector.y();
891 }
892 
893 // Because we are defining a new QObject-based class (RecognitionTimer) here.
894 #include "DirectionalDragArea.moc"