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