17 #include "DirectionalDragArea.h"
19 #include <QtCore/qmath.h>
20 #include <QtCore/QTimer>
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);
385 if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
387 DDA_DEBUG(
"Multi-finger drags are not accepted");
388 setStatus(WaitingForTouch);
392 m_previousDampedScenePos.setX(m_dampedScenePos.x());
393 m_previousDampedScenePos.setY(m_dampedScenePos.y());
394 m_dampedScenePos.update(touchScenePos);
395 updateVelocityCalculator(touchScenePos);
397 if (!pointInsideAllowedArea()) {
398 DDA_DEBUG(
"Rejecting gesture because touch point is outside allowed area.");
399 setStatus(WaitingForTouch);
403 if (!movingInRightDirection()) {
404 DDA_DEBUG(
"Rejecting gesture becauuse touch point is moving in the wrong direction.");
405 setStatus(WaitingForTouch);
409 setPreviousPos(touchPoint->pos());
410 setPreviousScenePos(touchScenePos);
412 if (isWithinTouchCompositionWindow()) {
415 DDA_DEBUG(
"Sill within composition window. Let's wait more.");
419 if (movedFarEnough(touchScenePos)) {
420 setStatus(Recognized);
422 DDA_DEBUG(
"Didn't move far enough yet. Let's wait more.");
426 void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
428 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
431 qCritical() <<
"DirectionalDragArea[status=Recognized]: touch " << m_touchId
432 <<
"missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
433 "Considering it as released.";
434 setStatus(WaitingForTouch);
436 setPreviousPos(touchPoint->pos());
437 setPreviousScenePos(touchPoint->scenePos());
439 if (touchPoint->state() == Qt::TouchPointReleased) {
440 setStatus(WaitingForTouch);
445 const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
447 const QList<QTouchEvent::TouchPoint> &touchPoints =
event->touchPoints();
448 const QTouchEvent::TouchPoint *touchPoint = 0;
449 for (
int i = 0; i < touchPoints.size(); ++i) {
450 if (touchPoints.at(i).id() == m_touchId) {
451 touchPoint = &touchPoints.at(i);
458 bool DirectionalDragArea::pointInsideAllowedArea()
const
463 QPointF totalMovement(m_dampedScenePos.x() - m_startScenePos.x(),
464 m_dampedScenePos.y() - m_startScenePos.y());
466 qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
467 totalMovement.y() * totalMovement.y();
469 if (squaredTotalMovSize == 0.) {
474 qreal projectedMovement = projectOntoDirectionVector(totalMovement);
477 qreal cosineAngleSquared = (projectedMovement * projectedMovement) / squaredTotalMovSize;
481 return cosineAngleSquared >= m_wideningFactor;
484 bool DirectionalDragArea::movingInRightDirection()
const
486 QPointF movementVector(m_dampedScenePos.x() - m_previousDampedScenePos.x(),
487 m_dampedScenePos.y() - m_previousDampedScenePos.y());
489 qreal scalarProjection = projectOntoDirectionVector(movementVector);
491 return scalarProjection >= 0.;
494 bool DirectionalDragArea::movedFarEnough(
const QPointF &point)
const
496 if (m_distanceThreshold <= 0.) {
500 QPointF totalMovement(point.x() - m_startScenePos.x(),
501 point.y() - m_startScenePos.y());
503 qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
504 totalMovement.y() * totalMovement.y();
506 return squaredTotalMovSize > m_distanceThresholdSquared;
510 void DirectionalDragArea::checkSpeed()
512 if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
513 qreal speed = qFabs(m_velocityCalculator->calculate());
514 qreal minSpeedMsecs = m_minSpeed / 1000.0;
516 if (speed < minSpeedMsecs) {
517 DDA_DEBUG(
"Rejecting gesture because it's below minimum speed.");
518 setStatus(WaitingForTouch);
522 if (m_velocityCalculator->numSamples() == m_numSamplesOnLastSpeedCheck) {
523 m_silenceTime += m_recognitionTimer->interval();
525 if (m_silenceTime > m_maxSilenceTime) {
526 DDA_DEBUG(
"Rejecting gesture because it's silence time has been exceeded.");
527 setStatus(WaitingForTouch);
532 m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
535 void DirectionalDragArea::onEnabledChanged()
537 if (!isEnabled() && m_status != WaitingForTouch) {
538 setStatus(WaitingForTouch);
542 void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
544 if (newStatus == m_status)
547 DirectionalDragArea::Status oldStatus = m_status;
549 if (oldStatus == Undecided) {
550 m_recognitionTimer->stop();
553 m_status = newStatus;
554 Q_EMIT statusChanged(m_status);
556 #if DIRECTIONALDRAGAREA_DEBUG
557 qDebug() <<
"[DDA]" << statusToString(oldStatus) <<
"->" << statusToString(newStatus);
561 case WaitingForTouch:
562 Q_EMIT draggingChanged(
false);
565 m_recognitionTimer->start();
566 Q_EMIT draggingChanged(
true);
569 if (oldStatus == WaitingForTouch)
570 Q_EMIT draggingChanged(
true);
578 void DirectionalDragArea::setPreviousPos(
const QPointF &point)
580 bool xChanged = m_previousPos.x() != point.x();
581 bool yChanged = m_previousPos.y() != point.y();
583 m_previousPos = point;
586 Q_EMIT touchXChanged(point.x());
587 if (Direction::isHorizontal(m_direction))
588 Q_EMIT distanceChanged(distance());
592 Q_EMIT touchYChanged(point.y());
593 if (Direction::isVertical(m_direction))
594 Q_EMIT distanceChanged(distance());
598 void DirectionalDragArea::setPreviousScenePos(
const QPointF &point)
600 bool xChanged = m_previousScenePos.x() != point.x();
601 bool yChanged = m_previousScenePos.y() != point.y();
603 if (!xChanged && !yChanged)
606 qreal oldSceneDistance = sceneDistance();
607 m_previousScenePos = point;
608 updateSceneDistance();
610 if (oldSceneDistance != sceneDistance()) {
611 Q_EMIT sceneDistanceChanged(sceneDistance());
615 Q_EMIT touchSceneXChanged(point.x());
619 Q_EMIT touchSceneYChanged(point.y());
623 void DirectionalDragArea::updateVelocityCalculator(
const QPointF &scenePos)
625 QPointF totalSceneMovement = scenePos - m_startScenePos;
627 qreal scalarProjection = projectOntoDirectionVector(totalSceneMovement);
629 m_velocityCalculator->setTrackedPosition(scalarProjection);
632 bool DirectionalDragArea::isWithinTouchCompositionWindow()
634 return !m_activeTouches.isEmpty() &&
635 m_timeSource->msecsSinceReference() <=
636 m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
641 DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(
const SharedTimeSource &timeSource)
642 : m_timeSource(timeSource), m_lastUsedIndex(-1)
650 void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
652 if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
657 const QList<QTouchEvent::TouchPoint> &touchPoints =
event->touchPoints();
658 for (
int i = 0; i < touchPoints.count(); ++i) {
659 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
660 if (touchPoint.state() == Qt::TouchPointPressed) {
661 addTouchPoint(touchPoint);
662 }
else if (touchPoint.state() == Qt::TouchPointReleased) {
663 removeTouchPoint(touchPoint);
668 void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(
const QTouchEvent::TouchPoint &touchPoint)
670 ActiveTouchInfo &activeTouchInfo = getEmptySlot();
671 activeTouchInfo.id = touchPoint.id();
672 activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
675 void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(
const QTouchEvent::TouchPoint &touchPoint)
677 for (
int i = 0; i <= m_lastUsedIndex; ++i) {
678 if (touchPoint.id() == m_vector.at(i).id) {
686 DirectionalDragArea::ActiveTouchInfo &DirectionalDragArea::ActiveTouchesInfo::getEmptySlot()
688 Q_ASSERT(m_lastUsedIndex < m_vector.size());
691 for (
int i = 0; i < m_lastUsedIndex; ++i) {
692 ActiveTouchInfo &activeTouchInfo = m_vector[i];
693 if (!activeTouchInfo.isValid()) {
694 return activeTouchInfo;
699 if (m_lastUsedIndex >= m_vector.size()) {
700 m_vector.resize(m_lastUsedIndex + 1);
703 return m_vector[m_lastUsedIndex];
706 void DirectionalDragArea::ActiveTouchesInfo::freeSlot(
int index)
708 m_vector[index].reset();
709 if (index == m_lastUsedIndex) {
712 }
while (m_lastUsedIndex >= 0 && !m_vector.at(m_lastUsedIndex).isValid());
716 qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
718 Q_ASSERT(m_lastUsedIndex >= 0);
720 qint64 highestStartTime = m_vector.at(0).startTime;
723 const ActiveTouchInfo &activeTouchInfo = m_vector.at(i);
724 if (activeTouchInfo.isValid() && activeTouchInfo.startTime > highestStartTime) {
725 highestStartTime = activeTouchInfo.startTime;
728 }
while (i < m_lastUsedIndex);
730 return highestStartTime;
733 void DirectionalDragArea::updateSceneDirectionVector()
735 QPointF localOrigin(0., 0.);
736 QPointF localDirection;
737 switch (m_direction) {
738 case Direction::Upwards:
739 localDirection.rx() = 0.;
740 localDirection.ry() = -1.;
742 case Direction::Downwards:
743 localDirection.rx() = 0.;
744 localDirection.ry() = 1;
746 case Direction::Leftwards:
747 localDirection.rx() = -1.;
748 localDirection.ry() = 0.;
751 localDirection.rx() = 1.;
752 localDirection.ry() = 0.;
755 QPointF sceneOrigin = mapToScene(localOrigin);
756 QPointF sceneDirection = mapToScene(localDirection);
757 m_sceneDirectionVector = sceneDirection - sceneOrigin;
760 qreal DirectionalDragArea::projectOntoDirectionVector(
const QPointF &sceneVector)
const
763 return sceneVector.x() * m_sceneDirectionVector.x() +
764 sceneVector.y() * m_sceneDirectionVector.y();
768 #include "DirectionalDragArea.moc"