17 #define ACTIVETOUCHESINFO_DEBUG 0
18 #define DIRECTIONALDRAGAREA_DEBUG 0
20 #include "DirectionalDragArea.h"
22 #include <QQuickWindow>
23 #include <QtCore/qmath.h>
27 #pragma GCC diagnostic push
28 #pragma GCC diagnostic ignored "-pedantic"
29 #include <private/qquickwindow_p.h>
30 #pragma GCC diagnostic pop
33 #include "TouchOwnershipEvent.h"
34 #include "TouchRegistry.h"
35 #include "UnownedTouchEvent.h"
37 #include "DirectionalDragArea_p.h"
41 #if DIRECTIONALDRAGAREA_DEBUG
42 #define ddaDebug(params) qDebug().nospace() << "[DDA(" << qPrintable(objectName()) << ")] " << params
43 #include "DebugHelpers.h"
46 const char *statusToString(DirectionalDragAreaPrivate::Status status)
48 if (status == DirectionalDragAreaPrivate::WaitingForTouch) {
49 return "WaitingForTouch";
50 }
else if (status == DirectionalDragAreaPrivate::Undecided) {
58 #else // DIRECTIONALDRAGAREA_DEBUG
59 #define ddaDebug(params) ((void)0)
60 #endif // DIRECTIONALDRAGAREA_DEBUG
62 DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
64 , d(new DirectionalDragAreaPrivate(this))
66 d->setRecognitionTimer(
new Timer(
this));
67 d->recognitionTimer->setInterval(d->maxTime);
68 d->recognitionTimer->setSingleShot(
true);
70 connect(
this, &QQuickItem::enabledChanged, d, &DirectionalDragAreaPrivate::giveUpIfDisabledOrInvisible);
71 connect(
this, &QQuickItem::visibleChanged, d, &DirectionalDragAreaPrivate::giveUpIfDisabledOrInvisible);
74 Direction::Type DirectionalDragArea::direction()
const
79 void DirectionalDragArea::setDirection(Direction::Type direction)
81 if (direction != d->direction) {
82 d->direction = direction;
83 Q_EMIT directionChanged(d->direction);
87 void DirectionalDragAreaPrivate::setDistanceThreshold(qreal value)
89 if (distanceThreshold != value) {
90 distanceThreshold = value;
91 distanceThresholdSquared = distanceThreshold * distanceThreshold;
95 void DirectionalDragAreaPrivate::setMaxTime(
int value)
97 if (maxTime != value) {
99 recognitionTimer->setInterval(maxTime);
103 void DirectionalDragAreaPrivate::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
106 bool timerWasRunning =
false;
107 bool wasSingleShot =
false;
110 if (recognitionTimer) {
111 interval = recognitionTimer->interval();
112 timerWasRunning = recognitionTimer->isRunning();
113 if (recognitionTimer->parent() ==
this) {
114 delete recognitionTimer;
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();
128 void DirectionalDragAreaPrivate::setTimeSource(
const SharedTimeSource &timeSource)
130 this->timeSource = timeSource;
131 activeTouches.m_timeSource = timeSource;
134 qreal DirectionalDragArea::distance()
const
136 if (Direction::isHorizontal(d->direction)) {
137 return d->publicPos.x() - d->startPos.x();
139 return d->publicPos.y() - d->startPos.y();
143 void DirectionalDragAreaPrivate::updateSceneDistance()
145 QPointF totalMovement = publicScenePos - startScenePos;
146 sceneDistance = projectOntoDirectionVector(totalMovement);
149 qreal DirectionalDragArea::sceneDistance()
const
151 return d->sceneDistance;
154 qreal DirectionalDragArea::touchX()
const
156 return d->publicPos.x();
159 qreal DirectionalDragArea::touchY()
const
161 return d->publicPos.y();
164 qreal DirectionalDragArea::touchSceneX()
const
166 return d->publicScenePos.x();
169 qreal DirectionalDragArea::touchSceneY()
const
171 return d->publicScenePos.y();
174 bool DirectionalDragArea::dragging()
const
176 return d->status == DirectionalDragAreaPrivate::Recognized;
179 bool DirectionalDragArea::pressed()
const
181 return d->status != DirectionalDragAreaPrivate::WaitingForTouch;
184 bool DirectionalDragArea::immediateRecognition()
const
186 return d->immediateRecognition;
189 void DirectionalDragArea::setImmediateRecognition(
bool enabled)
191 if (d->immediateRecognition != enabled) {
192 d->immediateRecognition = enabled;
193 Q_EMIT immediateRecognitionChanged(enabled);
197 void DirectionalDragArea::removeTimeConstraints()
199 d->setMaxTime(60 * 60 * 1000);
200 d->compositionTime = 0;
201 ddaDebug(
"removed time constraints");
204 bool DirectionalDragArea::event(QEvent *event)
206 if (event->type() == TouchOwnershipEvent::touchOwnershipEventType()) {
207 d->touchOwnershipEvent(static_cast<TouchOwnershipEvent *>(event));
209 }
else if (event->type() == UnownedTouchEvent::unownedTouchEventType()) {
210 d->unownedTouchEvent(static_cast<UnownedTouchEvent *>(event));
213 return QQuickItem::event(event);
217 void DirectionalDragAreaPrivate::touchOwnershipEvent(TouchOwnershipEvent *event)
219 if (event->gained()) {
221 ids.append(event->touchId());
222 ddaDebug(
"grabbing touch");
223 q->grabTouchPoints(ids);
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();
240 TouchRegistry::instance()->addTouchWatcher(touchId, q);
242 setStatus(WaitingForTouch);
246 void DirectionalDragAreaPrivate::unownedTouchEvent(UnownedTouchEvent *unownedTouchEvent)
248 QTouchEvent *
event = unownedTouchEvent->touchEvent();
250 Q_ASSERT(!event->touchPointStates().testFlag(Qt::TouchPointPressed));
252 ddaDebug(
"Unowned " << timeSource->msecsSinceReference() <<
" " << qPrintable(touchEventToString(event)));
255 case WaitingForTouch:
259 Q_ASSERT(q->isEnabled() && q->isVisible());
260 unownedTouchEvent_undecided(unownedTouchEvent);
267 activeTouches.update(event);
270 void DirectionalDragAreaPrivate::unownedTouchEvent_undecided(UnownedTouchEvent *unownedTouchEvent)
272 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(unownedTouchEvent->touchEvent());
274 qCritical() <<
"DirectionalDragArea[status=Undecided]: touch " << touchId
275 <<
"missing from UnownedTouchEvent without first reaching state Qt::TouchPointReleased. "
276 "Considering it as released.";
278 TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, q);
279 setStatus(WaitingForTouch);
283 const QPointF &touchScenePos = touchPoint->scenePos();
285 if (touchPoint->state() == Qt::TouchPointReleased) {
287 ddaDebug(
"Touch has ended before recognition concluded");
288 TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, q);
289 setStatus(WaitingForTouch);
293 previousDampedScenePos.setX(dampedScenePos.x());
294 previousDampedScenePos.setY(dampedScenePos.y());
295 dampedScenePos.update(touchScenePos);
297 if (!movingInRightDirection()) {
298 ddaDebug(
"Rejecting gesture because touch point is moving in the wrong direction.");
299 TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, q);
301 TouchRegistry::instance()->addTouchWatcher(touchId, q);
302 setStatus(WaitingForTouch);
306 if (isWithinTouchCompositionWindow()) {
309 ddaDebug(
"Sill within composition window. Let's wait more.");
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);
322 TouchRegistry::instance()->addTouchWatcher(touchId, q);
323 setStatus(WaitingForTouch);
325 ddaDebug(
"Didn't move far enough yet. Let's wait more.");
329 void DirectionalDragArea::touchEvent(QTouchEvent *event)
334 ddaDebug(d->timeSource->msecsSinceReference() <<
" " << qPrintable(touchEventToString(event)));
336 if (!isEnabled() || !isVisible()) {
337 QQuickItem::touchEvent(event);
342 case DirectionalDragAreaPrivate::WaitingForTouch:
343 d->touchEvent_absent(event);
345 case DirectionalDragAreaPrivate::Undecided:
346 d->touchEvent_undecided(event);
349 d->touchEvent_recognized(event);
353 d->activeTouches.update(event);
356 void DirectionalDragAreaPrivate::touchEvent_absent(QTouchEvent *event)
360 if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
368 if (isWithinTouchCompositionWindow()) {
371 ddaDebug(
"A new touch point came in but we're still within time composition window. Ignoring it.");
375 const QList<QTouchEvent::TouchPoint> &touchPoints =
event->touchPoints();
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) {
386 newTouchPoint = &touchPoint;
392 allGood = sanityCheckRecognitionProperties();
394 qWarning(
"DirectionalDragArea: recognition properties are wrongly set. Gesture recognition"
400 Q_ASSERT(newTouchPoint);
402 startPos = newTouchPoint->pos();
403 startScenePos = newTouchPoint->scenePos();
404 touchId = newTouchPoint->id();
405 dampedScenePos.reset(startScenePos);
406 setPublicPos(startPos);
408 setPublicScenePos(startScenePos);
409 updateSceneDirectionVector();
411 if (recognitionIsDisabled()) {
413 ddaDebug(
"Gesture recognition is disabled. Requesting touch ownership immediately.");
414 TouchRegistry::instance()->requestTouchOwnership(touchId, q);
415 setStatus(Recognized);
419 TouchRegistry::instance()->addCandidateOwnerForTouch(touchId, q);
421 setStatus(Undecided);
427 watchPressedTouchPoints(touchPoints);
432 void DirectionalDragAreaPrivate::touchEvent_undecided(QTouchEvent *event)
434 Q_ASSERT(fetchTargetTouchPoint(event) ==
nullptr);
440 watchPressedTouchPoints(event->touchPoints());
442 if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
444 ddaDebug(
"Multi-finger drags are not accepted");
446 TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, q);
448 TouchRegistry::instance()->addTouchWatcher(touchId, q);
450 setStatus(WaitingForTouch);
454 void DirectionalDragAreaPrivate::touchEvent_recognized(QTouchEvent *event)
456 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
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);
464 setPublicPos(touchPoint->pos());
465 setPublicScenePos(touchPoint->scenePos());
467 if (touchPoint->state() == Qt::TouchPointReleased) {
468 setStatus(WaitingForTouch);
473 void DirectionalDragAreaPrivate::watchPressedTouchPoints(
const QList<QTouchEvent::TouchPoint> &touchPoints)
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);
483 bool DirectionalDragAreaPrivate::recognitionIsDisabled()
const
485 return immediateRecognition || (distanceThreshold <= 0 && compositionTime <= 0);
488 bool DirectionalDragAreaPrivate::sanityCheckRecognitionProperties()
490 return recognitionIsDisabled()
491 || (distanceThreshold < maxDistance && compositionTime < maxTime);
494 const QTouchEvent::TouchPoint *DirectionalDragAreaPrivate::fetchTargetTouchPoint(QTouchEvent *event)
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);
507 bool DirectionalDragAreaPrivate::movingInRightDirection()
const
509 if (direction == Direction::Horizontal || direction == Direction::Vertical) {
512 QPointF movementVector(dampedScenePos.x() - previousDampedScenePos.x(),
513 dampedScenePos.y() - previousDampedScenePos.y());
515 qreal scalarProjection = projectOntoDirectionVector(movementVector);
517 return scalarProjection >= 0.;
521 bool DirectionalDragAreaPrivate::movedFarEnoughAlongGestureAxis()
const
523 if (distanceThreshold <= 0.) {
527 QPointF totalMovement(dampedScenePos.x() - startScenePos.x(),
528 dampedScenePos.y() - startScenePos.y());
530 qreal scalarProjection = projectOntoDirectionVector(totalMovement);
532 ddaDebug(
" movedFarEnoughAlongGestureAxis: scalarProjection=" << scalarProjection
533 <<
", distanceThreshold=" << distanceThreshold);
535 if (direction == Direction::Horizontal || direction == Direction::Vertical) {
536 return qAbs(scalarProjection) > distanceThreshold;
538 return scalarProjection > distanceThreshold;
543 bool DirectionalDragAreaPrivate::isPastMaxDistance()
const
545 QPointF totalMovement(dampedScenePos.x() - startScenePos.x(),
546 dampedScenePos.y() - startScenePos.y());
548 qreal squaredDistance = totalMovement.x()*totalMovement.x() + totalMovement.y()*totalMovement.y();
549 return squaredDistance > maxDistance*maxDistance;
552 void DirectionalDragAreaPrivate::giveUpIfDisabledOrInvisible()
554 if (!q->isEnabled() || !q->isVisible()) {
555 if (status == Undecided) {
556 TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, q);
558 TouchRegistry::instance()->addTouchWatcher(touchId, q);
561 if (status != WaitingForTouch) {
562 ddaDebug(
"Resetting status because got disabled or made invisible");
563 setStatus(WaitingForTouch);
568 void DirectionalDragAreaPrivate::rejectGesture()
570 if (status == Undecided) {
571 ddaDebug(
"Rejecting gesture because it's taking too long to drag beyond the threshold.");
573 TouchRegistry::instance()->removeCandidateOwnerForTouch(touchId, q);
575 TouchRegistry::instance()->addTouchWatcher(touchId, q);
577 setStatus(WaitingForTouch);
581 void DirectionalDragAreaPrivate::setStatus(Status newStatus)
583 if (newStatus == status)
586 Status oldStatus = status;
588 if (oldStatus == Undecided) {
589 recognitionTimer->stop();
593 Q_EMIT statusChanged(status);
595 ddaDebug(statusToString(oldStatus) <<
" -> " << statusToString(newStatus));
598 case WaitingForTouch:
599 if (oldStatus == Recognized) {
600 Q_EMIT q->draggingChanged(
false);
602 Q_EMIT q->pressedChanged(
false);
605 recognitionTimer->start();
606 Q_EMIT q->pressedChanged(
true);
609 Q_EMIT q->draggingChanged(
true);
617 void DirectionalDragAreaPrivate::setPublicPos(
const QPointF &point)
619 bool xChanged = publicPos.x() != point.x();
620 bool yChanged = publicPos.y() != point.y();
624 Q_ASSERT(status == WaitingForTouch || status == Recognized);
626 if (status == Recognized && !recognitionIsDisabled()) {
633 QPointF delta = point - publicPos;
635 publicPos.rx() += 0.4 * delta.x();
636 publicPos.ry() += 0.4 * delta.y();
644 Q_EMIT q->touchXChanged(publicPos.x());
645 if (Direction::isHorizontal(direction))
646 Q_EMIT q->distanceChanged(q->distance());
650 Q_EMIT q->touchYChanged(publicPos.y());
651 if (Direction::isVertical(direction))
652 Q_EMIT q->distanceChanged(q->distance());
656 void DirectionalDragAreaPrivate::setPublicScenePos(
const QPointF &point)
658 bool xChanged = publicScenePos.x() != point.x();
659 bool yChanged = publicScenePos.y() != point.y();
661 if (!xChanged && !yChanged)
666 Q_ASSERT(status == WaitingForTouch || status == Recognized);
668 qreal oldSceneDistance = sceneDistance;
670 if (status == Recognized && !recognitionIsDisabled()) {
677 QPointF delta = point - publicScenePos;
679 publicScenePos.rx() += 0.4 * delta.x();
680 publicScenePos.ry() += 0.4 * delta.y();
684 publicScenePos = point;
687 updateSceneDistance();
689 if (oldSceneDistance != sceneDistance) {
690 Q_EMIT q->sceneDistanceChanged(sceneDistance);
694 Q_EMIT q->touchSceneXChanged(publicScenePos.x());
698 Q_EMIT q->touchSceneYChanged(publicScenePos.y());
702 bool DirectionalDragAreaPrivate::isWithinTouchCompositionWindow()
705 compositionTime > 0 &&
706 !activeTouches.isEmpty() &&
707 timeSource->msecsSinceReference() <=
708 activeTouches.mostRecentStartTime() + (qint64)compositionTime;
711 void DirectionalDragArea::itemChange(ItemChange change,
const ItemChangeData &value)
713 if (change == QQuickItem::ItemSceneChange) {
714 if (value.window !=
nullptr) {
715 value.window->installEventFilter(TouchRegistry::instance());
718 qreal pixelsPerMm = value.window->screen()->physicalDotsPerInch() / 25.4;
719 d->setPixelsPerMm(pixelsPerMm);
724 void DirectionalDragAreaPrivate::setPixelsPerMm(qreal pixelsPerMm)
726 dampedScenePos.setMaxDelta(1. * pixelsPerMm);
727 setDistanceThreshold(4. * pixelsPerMm);
728 maxDistance = 10. * pixelsPerMm;
733 ActiveTouchesInfo::ActiveTouchesInfo(
const SharedTimeSource &timeSource)
734 : m_timeSource(timeSource)
738 void ActiveTouchesInfo::update(QTouchEvent *event)
740 if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
742 #if ACTIVETOUCHESINFO_DEBUG
743 qDebug(
"[DDA::ActiveTouchesInfo] Nothing to Update");
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());
759 #if ACTIVETOUCHESINFO_DEBUG
760 QString ActiveTouchesInfo::toString()
762 QString
string =
"(";
765 QTextStream stream(&
string);
766 m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
767 stream <<
"(id=" << touchInfo->id <<
",startTime=" << touchInfo->startTime <<
")";
776 #endif // ACTIVETOUCHESINFO_DEBUG
778 void ActiveTouchesInfo::addTouchPoint(
int touchId)
780 ActiveTouchInfo &activeTouchInfo = m_touchInfoPool.getEmptySlot();
781 activeTouchInfo.id = touchId;
782 activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
784 #if ACTIVETOUCHESINFO_DEBUG
785 qDebug() <<
"[DDA::ActiveTouchesInfo]" << qPrintable(toString());
789 qint64 ActiveTouchesInfo::touchStartTime(
int touchId)
793 m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
794 if (touchId == touchInfo->id) {
795 result = touchInfo->startTime;
802 Q_ASSERT(result != -1);
806 void ActiveTouchesInfo::removeTouchPoint(
int touchId)
808 m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &touchInfo) {
809 if (touchId == touchInfo->id) {
810 m_touchInfoPool.freeSlot(touchInfo);
817 #if ACTIVETOUCHESINFO_DEBUG
818 qDebug() <<
"[DDA::ActiveTouchesInfo]" << qPrintable(toString());
822 qint64 ActiveTouchesInfo::mostRecentStartTime()
824 Q_ASSERT(!m_touchInfoPool.isEmpty());
826 qint64 highestStartTime = -1;
828 m_touchInfoPool.forEach([&](Pool<ActiveTouchInfo>::Iterator &activeTouchInfo) {
829 if (activeTouchInfo->startTime > highestStartTime) {
830 highestStartTime = activeTouchInfo->startTime;
835 return highestStartTime;
838 void DirectionalDragAreaPrivate::updateSceneDirectionVector()
840 QPointF localOrigin(0., 0.);
841 QPointF localDirection;
843 case Direction::Upwards:
844 localDirection.rx() = 0.;
845 localDirection.ry() = -1.;
847 case Direction::Downwards:
848 case Direction::Vertical:
849 localDirection.rx() = 0.;
850 localDirection.ry() = 1;
852 case Direction::Leftwards:
853 localDirection.rx() = -1.;
854 localDirection.ry() = 0.;
857 localDirection.rx() = 1.;
858 localDirection.ry() = 0.;
861 QPointF sceneOrigin = q->mapToScene(localOrigin);
862 QPointF sceneDirection = q->mapToScene(localDirection);
863 sceneDirectionVector = sceneDirection - sceneOrigin;
866 qreal DirectionalDragAreaPrivate::projectOntoDirectionVector(
const QPointF &sceneVector)
const
869 return sceneVector.x() * sceneDirectionVector.x() +
870 sceneVector.y() * sceneDirectionVector.y();
873 DirectionalDragAreaPrivate::DirectionalDragAreaPrivate(DirectionalDragArea *q)
875 , status(WaitingForTouch)
878 , direction(Direction::Rightwards)
879 , distanceThreshold(0)
880 , distanceThresholdSquared(0.)
882 , compositionTime(60)
883 , immediateRecognition(false)
884 , recognitionTimer(nullptr)
885 , timeSource(new RealTimeSource)
886 , activeTouches(timeSource)