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