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