17 #include "DirectionalDragArea.h"
19 #include <QtCore/qmath.h>
20 #include <QtCore/QTimer>
23 using namespace UbuntuGestures;
25 #define DIRECTIONALDRAGAREA_DEBUG 0
27 #if DIRECTIONALDRAGAREA_DEBUG
28 #define DDA_DEBUG(msg) qDebug("[DDA] " msg)
30 QString touchPointStateToString(Qt::TouchPointState state) {
32 case Qt::TouchPointPressed:
33 return QString(
"pressed");
34 case Qt::TouchPointMoved:
35 return QString(
"moved");
36 case Qt::TouchPointStationary:
37 return QString(
"stationary");
39 return QString(
"released");
42 QString touchEventToString(QTouchEvent *ev)
47 case QEvent::TouchBegin:
48 message.append(
"TouchBegin ");
50 case QEvent::TouchUpdate:
51 message.append(
"TouchUpdate ");
53 case QEvent::TouchEnd:
54 message.append(
"TouchEnd ");
57 message.append(
"TouchCancel ");
60 for (
int i=0; i < ev->touchPoints().size(); ++i) {
62 const QTouchEvent::TouchPoint& touchPoint = ev->touchPoints().at(i);
64 QString(
"(id:%1, state:%2, scenePos:(%3,%4)) ")
66 .arg(touchPointStateToString(touchPoint.state()))
67 .arg(touchPoint.scenePos().x())
68 .arg(touchPoint.scenePos().y())
75 const char *statusToString(DirectionalDragArea::Status status)
77 if (status == DirectionalDragArea::WaitingForTouch) {
78 return "WaitingForTouch";
79 }
else if (status == DirectionalDragArea::Undecided) {
87 #else // DIRECTIONALDRAGAREA_DEBUG
88 #define DDA_DEBUG(msg) do{}while(0)
89 #endif // DIRECTIONALDRAGAREA_DEBUG
92 class RecognitionTimer :
public UbuntuGestures::AbstractTimer
96 RecognitionTimer(QObject *parent) : UbuntuGestures::AbstractTimer(parent) {
97 m_timer.setSingleShot(
false);
98 connect(&m_timer, &QTimer::timeout,
99 this, &UbuntuGestures::AbstractTimer::timeout);
101 virtual int interval()
const {
return m_timer.interval(); }
102 virtual void setInterval(
int msecs) { m_timer.setInterval(msecs); }
103 virtual void start() { m_timer.start(); UbuntuGestures::AbstractTimer::start(); }
104 virtual void stop() { m_timer.stop(); UbuntuGestures::AbstractTimer::stop(); }
109 DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
111 , m_status(WaitingForTouch)
114 , m_direction(Direction::Rightwards)
116 , m_wideningFactor(0)
117 , m_distanceThreshold(0)
118 , m_distanceThresholdSquared(0.)
120 , m_maxSilenceTime(200)
122 , m_compositionTime(60)
123 , m_numSamplesOnLastSpeedCheck(0)
124 , m_recognitionTimer(0)
125 , m_velocityCalculator(0)
126 , m_timeSource(new RealTimeSource)
127 , m_activeTouches(m_timeSource)
129 setRecognitionTimer(
new RecognitionTimer(
this));
130 m_recognitionTimer->setInterval(60);
132 m_velocityCalculator =
new AxisVelocityCalculator(
this);
134 connect(
this, &QQuickItem::enabledChanged,
this, &DirectionalDragArea::onEnabledChanged);
137 Direction::Type DirectionalDragArea::direction()
const
142 void DirectionalDragArea::setDirection(Direction::Type direction)
144 if (direction != m_direction) {
145 m_direction = direction;
146 Q_EMIT directionChanged(m_direction);
150 void DirectionalDragArea::setMaxDeviation(qreal value)
152 if (m_dampedScenePos.maxDelta() != value) {
153 m_dampedScenePos.setMaxDelta(value);
154 Q_EMIT maxDeviationChanged(value);
158 qreal DirectionalDragArea::wideningAngle()
const
160 return m_wideningAngle;
163 void DirectionalDragArea::setWideningAngle(qreal angle)
165 if (angle == m_wideningAngle)
168 m_wideningAngle = angle;
172 qreal angleRadians = angle * M_PI / 180.0;
173 m_wideningFactor = qCos(angleRadians);
174 m_wideningFactor = m_wideningFactor * m_wideningFactor;
177 Q_EMIT wideningAngleChanged(angle);
180 void DirectionalDragArea::setDistanceThreshold(qreal value)
182 if (m_distanceThreshold != value) {
183 m_distanceThreshold = value;
184 m_distanceThresholdSquared = m_distanceThreshold * m_distanceThreshold;
185 Q_EMIT distanceThresholdChanged(value);
189 void DirectionalDragArea::setMinSpeed(qreal value)
191 if (m_minSpeed != value) {
193 Q_EMIT minSpeedChanged(value);
197 void DirectionalDragArea::setMaxSilenceTime(
int value)
199 if (m_maxSilenceTime != value) {
200 m_maxSilenceTime = value;
201 Q_EMIT maxSilenceTimeChanged(value);
205 void DirectionalDragArea::setCompositionTime(
int value)
207 if (m_compositionTime != value) {
208 m_compositionTime = value;
209 Q_EMIT compositionTimeChanged(value);
213 void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
216 bool timerWasRunning =
false;
219 if (m_recognitionTimer) {
220 interval = m_recognitionTimer->interval();
221 timerWasRunning = m_recognitionTimer->isRunning();
222 if (m_recognitionTimer->parent() ==
this) {
223 delete m_recognitionTimer;
227 m_recognitionTimer = timer;
228 timer->setInterval(interval);
229 connect(timer, &UbuntuGestures::AbstractTimer::timeout,
230 this, &DirectionalDragArea::checkSpeed);
231 if (timerWasRunning) {
232 m_recognitionTimer->start();
236 void DirectionalDragArea::setTimeSource(
const SharedTimeSource &timeSource)
238 m_timeSource = timeSource;
239 m_velocityCalculator->setTimeSource(timeSource);
240 m_activeTouches.m_timeSource = timeSource;
243 qreal DirectionalDragArea::distance()
const
245 if (Direction::isHorizontal(m_direction)) {
246 return m_previousPos.x() - m_startPos.x();
248 return m_previousPos.y() - m_startPos.y();
252 void DirectionalDragArea::updateSceneDistance()
254 QPointF totalMovement = m_previousScenePos - m_startScenePos;
255 m_sceneDistance = projectOntoDirectionVector(totalMovement);
258 qreal DirectionalDragArea::sceneDistance()
const
260 return m_sceneDistance;
263 qreal DirectionalDragArea::touchX()
const
265 return m_previousPos.x();
268 qreal DirectionalDragArea::touchY()
const
270 return m_previousPos.y();
273 qreal DirectionalDragArea::touchSceneX()
const
275 return m_previousScenePos.x();
278 qreal DirectionalDragArea::touchSceneY()
const
280 return m_previousScenePos.y();
283 void DirectionalDragArea::touchEvent(QTouchEvent *event)
285 #if DIRECTIONALDRAGAREA_DEBUG
287 qDebug() <<
"[DDA]" << m_timeSource->msecsSinceReference()
288 << qPrintable(touchEventToString(event));
291 if (!isEnabled() || !isVisible()) {
292 QQuickItem::touchEvent(event);
297 case WaitingForTouch:
298 touchEvent_absent(event);
301 touchEvent_undecided(event);
304 touchEvent_recognized(event);
308 m_activeTouches.update(event);
311 void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
313 if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
318 if (isWithinTouchCompositionWindow()) {
321 #if DIRECTIONALDRAGAREA_DEBUG
322 qDebug(
"[DDA] A new touch point came in but we're still within time composition window. Ignoring it.");
327 const QList<QTouchEvent::TouchPoint> &touchPoints =
event->touchPoints();
329 const QTouchEvent::TouchPoint *newTouchPoint =
nullptr;
330 for (
int i = 0; i < touchPoints.count(); ++i) {
331 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
332 if (touchPoint.state() == Qt::TouchPointPressed) {
338 m_touchId = touchPoint.id();
339 newTouchPoint = &touchPoint;
344 Q_ASSERT(newTouchPoint);
348 m_startPos = newTouchPoint->pos();
349 m_startScenePos = newTouchPoint->scenePos();
350 m_touchId = newTouchPoint->id();
351 m_dampedScenePos.reset(m_startScenePos);
352 m_velocityCalculator->setTrackedPosition(0.);
353 m_velocityCalculator->reset();
354 m_numSamplesOnLastSpeedCheck = 0;
356 setPreviousPos(m_startPos);
357 setPreviousScenePos(m_startScenePos);
358 updateSceneDirectionVector();
360 setStatus(Undecided);
363 void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
365 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
368 qCritical() <<
"DirectionalDragArea[status=Undecided]: touch " << m_touchId
369 <<
"missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
370 "Considering it as released.";
371 setStatus(WaitingForTouch);
375 const QPointF &touchScenePos = touchPoint->scenePos();
377 if (touchPoint->state() == Qt::TouchPointReleased) {
379 DDA_DEBUG(
"Touch has ended before recognition concluded");
380 setStatus(WaitingForTouch);
384 if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
386 DDA_DEBUG(
"Multi-finger drags are not accepted");
387 setStatus(WaitingForTouch);
391 m_previousDampedScenePos.setX(m_dampedScenePos.x());
392 m_previousDampedScenePos.setY(m_dampedScenePos.y());
393 m_dampedScenePos.update(touchScenePos);
394 updateVelocityCalculator(touchScenePos);
396 if (!pointInsideAllowedArea()) {
397 DDA_DEBUG(
"Rejecting gesture because touch point is outside allowed area.");
398 setStatus(WaitingForTouch);
402 if (!movingInRightDirection()) {
403 DDA_DEBUG(
"Rejecting gesture becauuse touch point is moving in the wrong direction.");
404 setStatus(WaitingForTouch);
408 setPreviousPos(touchPoint->pos());
409 setPreviousScenePos(touchScenePos);
411 if (isWithinTouchCompositionWindow()) {
414 DDA_DEBUG(
"Sill within composition window. Let's wait more.");
418 if (movedFarEnough(touchScenePos)) {
419 setStatus(Recognized);
421 DDA_DEBUG(
"Didn't move far enough yet. Let's wait more.");
425 void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
427 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
430 qCritical() <<
"DirectionalDragArea[status=Recognized]: touch " << m_touchId
431 <<
"missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
432 "Considering it as released.";
433 setStatus(WaitingForTouch);
435 setPreviousPos(touchPoint->pos());
436 setPreviousScenePos(touchPoint->scenePos());
438 if (touchPoint->state() == Qt::TouchPointReleased) {
439 setStatus(WaitingForTouch);
444 const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
446 const QList<QTouchEvent::TouchPoint> &touchPoints =
event->touchPoints();
447 const QTouchEvent::TouchPoint *touchPoint = 0;
448 for (
int i = 0; i < touchPoints.size(); ++i) {
449 if (touchPoints.at(i).id() == m_touchId) {
450 touchPoint = &touchPoints.at(i);
457 bool DirectionalDragArea::pointInsideAllowedArea()
const
462 QPointF totalMovement(m_dampedScenePos.x() - m_startScenePos.x(),
463 m_dampedScenePos.y() - m_startScenePos.y());
465 qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
466 totalMovement.y() * totalMovement.y();
468 if (squaredTotalMovSize == 0.) {
473 qreal projectedMovement = projectOntoDirectionVector(totalMovement);
476 qreal cosineAngleSquared = (projectedMovement * projectedMovement) / squaredTotalMovSize;
480 return cosineAngleSquared >= m_wideningFactor;
483 bool DirectionalDragArea::movingInRightDirection()
const
485 QPointF movementVector(m_dampedScenePos.x() - m_previousDampedScenePos.x(),
486 m_dampedScenePos.y() - m_previousDampedScenePos.y());
488 qreal scalarProjection = projectOntoDirectionVector(movementVector);
490 return scalarProjection >= 0.;
493 bool DirectionalDragArea::movedFarEnough(
const QPointF &point)
const
495 if (m_distanceThreshold <= 0.) {
499 QPointF totalMovement(point.x() - m_startScenePos.x(),
500 point.y() - m_startScenePos.y());
502 qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
503 totalMovement.y() * totalMovement.y();
505 return squaredTotalMovSize > m_distanceThresholdSquared;
509 void DirectionalDragArea::checkSpeed()
511 if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
512 qreal speed = qFabs(m_velocityCalculator->calculate());
513 qreal minSpeedMsecs = m_minSpeed / 1000.0;
515 if (speed < minSpeedMsecs) {
516 DDA_DEBUG(
"Rejecting gesture because it's below minimum speed.");
517 setStatus(WaitingForTouch);
521 if (m_velocityCalculator->numSamples() == m_numSamplesOnLastSpeedCheck) {
522 m_silenceTime += m_recognitionTimer->interval();
524 if (m_silenceTime > m_maxSilenceTime) {
525 DDA_DEBUG(
"Rejecting gesture because it's silence time has been exceeded.");
526 setStatus(WaitingForTouch);
531 m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
534 void DirectionalDragArea::onEnabledChanged()
536 if (!isEnabled() && m_status != WaitingForTouch) {
537 setStatus(WaitingForTouch);
541 void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
543 if (newStatus == m_status)
546 DirectionalDragArea::Status oldStatus = m_status;
548 if (oldStatus == Undecided) {
549 m_recognitionTimer->stop();
552 m_status = newStatus;
553 Q_EMIT statusChanged(m_status);
555 #if DIRECTIONALDRAGAREA_DEBUG
556 qDebug() <<
"[DDA]" << statusToString(oldStatus) <<
"->" << statusToString(newStatus);
560 case WaitingForTouch:
561 Q_EMIT draggingChanged(
false);
564 m_recognitionTimer->start();
565 Q_EMIT draggingChanged(
true);
568 if (oldStatus == WaitingForTouch)
569 Q_EMIT draggingChanged(
true);
577 void DirectionalDragArea::setPreviousPos(
const QPointF &point)
579 bool xChanged = m_previousPos.x() != point.x();
580 bool yChanged = m_previousPos.y() != point.y();
582 m_previousPos = point;
585 Q_EMIT touchXChanged(point.x());
586 if (Direction::isHorizontal(m_direction))
587 Q_EMIT distanceChanged(distance());
591 Q_EMIT touchYChanged(point.y());
592 if (Direction::isVertical(m_direction))
593 Q_EMIT distanceChanged(distance());
597 void DirectionalDragArea::setPreviousScenePos(
const QPointF &point)
599 bool xChanged = m_previousScenePos.x() != point.x();
600 bool yChanged = m_previousScenePos.y() != point.y();
602 if (!xChanged && !yChanged)
605 qreal oldSceneDistance = sceneDistance();
606 m_previousScenePos = point;
607 updateSceneDistance();
609 if (oldSceneDistance != sceneDistance()) {
610 Q_EMIT sceneDistanceChanged(sceneDistance());
614 Q_EMIT touchSceneXChanged(point.x());
618 Q_EMIT touchSceneYChanged(point.y());
622 void DirectionalDragArea::updateVelocityCalculator(
const QPointF &scenePos)
624 QPointF totalSceneMovement = scenePos - m_startScenePos;
626 qreal scalarProjection = projectOntoDirectionVector(totalSceneMovement);
628 m_velocityCalculator->setTrackedPosition(scalarProjection);
631 bool DirectionalDragArea::isWithinTouchCompositionWindow()
633 return !m_activeTouches.isEmpty() &&
634 m_timeSource->msecsSinceReference() <=
635 m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
640 DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(
const SharedTimeSource &timeSource)
641 : m_timeSource(timeSource), m_lastUsedIndex(-1)
649 void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
651 if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
656 const QList<QTouchEvent::TouchPoint> &touchPoints =
event->touchPoints();
657 for (
int i = 0; i < touchPoints.count(); ++i) {
658 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
659 if (touchPoint.state() == Qt::TouchPointPressed) {
660 addTouchPoint(touchPoint);
661 }
else if (touchPoint.state() == Qt::TouchPointReleased) {
662 removeTouchPoint(touchPoint);
667 void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(
const QTouchEvent::TouchPoint &touchPoint)
669 ActiveTouchInfo &activeTouchInfo = getEmptySlot();
670 activeTouchInfo.id = touchPoint.id();
671 activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
674 void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(
const QTouchEvent::TouchPoint &touchPoint)
676 for (
int i = 0; i <= m_lastUsedIndex; ++i) {
677 if (touchPoint.id() == m_vector.at(i).id) {
685 DirectionalDragArea::ActiveTouchInfo &DirectionalDragArea::ActiveTouchesInfo::getEmptySlot()
687 Q_ASSERT(m_lastUsedIndex < m_vector.size());
690 for (
int i = 0; i < m_lastUsedIndex; ++i) {
691 ActiveTouchInfo &activeTouchInfo = m_vector[i];
692 if (!activeTouchInfo.isValid()) {
693 return activeTouchInfo;
698 if (m_lastUsedIndex >= m_vector.size()) {
699 m_vector.resize(m_lastUsedIndex + 1);
702 return m_vector[m_lastUsedIndex];
705 void DirectionalDragArea::ActiveTouchesInfo::freeSlot(
int index)
707 m_vector[index].reset();
708 if (index == m_lastUsedIndex) {
711 }
while (m_lastUsedIndex >= 0 && !m_vector.at(m_lastUsedIndex).isValid());
715 qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
717 Q_ASSERT(m_lastUsedIndex >= 0);
719 qint64 highestStartTime = m_vector.at(0).startTime;
722 const ActiveTouchInfo &activeTouchInfo = m_vector.at(i);
723 if (activeTouchInfo.isValid() && activeTouchInfo.startTime > highestStartTime) {
724 highestStartTime = activeTouchInfo.startTime;
727 }
while (i < m_lastUsedIndex);
729 return highestStartTime;
732 void DirectionalDragArea::updateSceneDirectionVector()
734 QPointF localOrigin(0., 0.);
735 QPointF localDirection;
736 switch (m_direction) {
737 case Direction::Upwards:
738 localDirection.rx() = 0.;
739 localDirection.ry() = -1.;
741 case Direction::Downwards:
742 localDirection.rx() = 0.;
743 localDirection.ry() = 1;
745 case Direction::Leftwards:
746 localDirection.rx() = -1.;
747 localDirection.ry() = 0.;
750 localDirection.rx() = 1.;
751 localDirection.ry() = 0.;
754 QPointF sceneOrigin = mapToScene(localOrigin);
755 QPointF sceneDirection = mapToScene(localDirection);
756 m_sceneDirectionVector = sceneDirection - sceneOrigin;
759 qreal DirectionalDragArea::projectOntoDirectionVector(
const QPointF &sceneVector)
const
762 return sceneVector.x() * m_sceneDirectionVector.x() +
763 sceneVector.y() * m_sceneDirectionVector.y();
767 #include "DirectionalDragArea.moc"