2 * Copyright (C) 2013 Canonical, Ltd.
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.
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.
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/>.
17#include "DirectionalDragArea.h"
19#include <QtCore/qmath.h>
20#include <QtCore/QTimer>
23using namespace UbuntuGestures;
25#define DIRECTIONALDRAGAREA_DEBUG 0
27#if DIRECTIONALDRAGAREA_DEBUG
28#define DDA_DEBUG(msg) qDebug("[DDA] " msg)
30QString 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");
38 default: // Qt::TouchPointReleased:
39 return QString("released");
42QString 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 ");
56 default: //QEvent::TouchCancel
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())
75const 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
91// Essentially a QTimer wrapper
92class 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(); }
109DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
111 , m_status(WaitingForTouch)
113 , m_direction(Direction::Rightwards)
115 , m_wideningFactor(0)
116 , m_distanceThreshold(0)
117 , m_distanceThresholdSquared(0.)
119 , m_maxSilenceTime(200)
121 , m_compositionTime(60)
122 , m_numSamplesOnLastSpeedCheck(0)
123 , m_recognitionTimer(0)
124 , m_velocityCalculator(0)
125 , m_timeSource(new RealTimeSource)
126 , m_activeTouches(m_timeSource)
128 setRecognitionTimer(new RecognitionTimer(this));
129 m_recognitionTimer->setInterval(60);
131 m_velocityCalculator = new AxisVelocityCalculator(this);
134Direction::Type DirectionalDragArea::direction() const
139void DirectionalDragArea::setDirection(Direction::Type direction)
141 if (direction != m_direction) {
142 m_direction = direction;
143 Q_EMIT directionChanged(m_direction);
147void DirectionalDragArea::setMaxDeviation(qreal value)
149 if (m_dampedScenePos.maxDelta() != value) {
150 m_dampedScenePos.setMaxDelta(value);
151 Q_EMIT maxDeviationChanged(value);
155qreal DirectionalDragArea::wideningAngle() const
157 return m_wideningAngle;
160void DirectionalDragArea::setWideningAngle(qreal angle)
162 if (angle == m_wideningAngle)
165 m_wideningAngle = angle;
167 // wideningFactor = pow(cosine(angle), 2)
169 qreal angleRadians = angle * M_PI / 180.0;
170 m_wideningFactor = qCos(angleRadians);
171 m_wideningFactor = m_wideningFactor * m_wideningFactor;
174 Q_EMIT wideningAngleChanged(angle);
177void DirectionalDragArea::setDistanceThreshold(qreal value)
179 if (m_distanceThreshold != value) {
180 m_distanceThreshold = value;
181 m_distanceThresholdSquared = m_distanceThreshold * m_distanceThreshold;
182 Q_EMIT distanceThresholdChanged(value);
186void DirectionalDragArea::setMinSpeed(qreal value)
188 if (m_minSpeed != value) {
190 Q_EMIT minSpeedChanged(value);
194void DirectionalDragArea::setMaxSilenceTime(int value)
196 if (m_maxSilenceTime != value) {
197 m_maxSilenceTime = value;
198 Q_EMIT maxSilenceTimeChanged(value);
202void DirectionalDragArea::setCompositionTime(int value)
204 if (m_compositionTime != value) {
205 m_compositionTime = value;
206 Q_EMIT compositionTimeChanged(value);
210void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
213 bool timerWasRunning = false;
215 // can be null when called from the constructor
216 if (m_recognitionTimer) {
217 interval = m_recognitionTimer->interval();
218 timerWasRunning = m_recognitionTimer->isRunning();
219 if (m_recognitionTimer->parent() == this) {
220 delete m_recognitionTimer;
224 m_recognitionTimer = timer;
225 timer->setInterval(interval);
226 connect(timer, &UbuntuGestures::AbstractTimer::timeout,
227 this, &DirectionalDragArea::checkSpeed);
228 if (timerWasRunning) {
229 m_recognitionTimer->start();
233void DirectionalDragArea::setTimeSource(const SharedTimeSource &timeSource)
235 m_timeSource = timeSource;
236 m_velocityCalculator->setTimeSource(timeSource);
237 m_activeTouches.m_timeSource = timeSource;
240qreal DirectionalDragArea::distance() const
242 if (Direction::isHorizontal(m_direction)) {
243 return m_previousPos.x() - m_startPos.x();
245 return m_previousPos.y() - m_startPos.y();
249void DirectionalDragArea::updateSceneDistance()
251 QPointF totalMovement = m_previousScenePos - m_startScenePos;
252 m_sceneDistance = projectOntoDirectionVector(totalMovement);
255qreal DirectionalDragArea::sceneDistance() const
257 return m_sceneDistance;
260qreal DirectionalDragArea::touchX() const
262 return m_previousPos.x();
265qreal DirectionalDragArea::touchY() const
267 return m_previousPos.y();
270qreal DirectionalDragArea::touchSceneX() const
272 return m_previousScenePos.x();
275qreal DirectionalDragArea::touchSceneY() const
277 return m_previousScenePos.y();
280void DirectionalDragArea::touchEvent(QTouchEvent *event)
282 #if DIRECTIONALDRAGAREA_DEBUG
283 // TODO Consider using qCDebug() when available (Qt 5.2)
284 qDebug() << "[DDA]" << m_timeSource->msecsSinceReference()
285 << qPrintable(touchEventToString(event));
288 if (!isEnabled() || !isVisible()) {
289 QQuickItem::touchEvent(event);
294 case WaitingForTouch:
295 touchEvent_absent(event);
298 touchEvent_undecided(event);
300 default: // Recognized:
301 touchEvent_recognized(event);
305 m_activeTouches.update(event);
308void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
310 if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
311 // Nothing to see here. No touch starting in this event.
315 if (isWithinTouchCompositionWindow()) {
316 // too close to the last touch start. So we consider them as starting roughly at the same time.
317 // Can't be a single-touch gesture.
318 #if DIRECTIONALDRAGAREA_DEBUG
319 qDebug("[DDA] A new touch point came in but we're still within time composition window. Ignoring it.");
324 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
326 const QTouchEvent::TouchPoint *newTouchPoint = nullptr;
327 for (int i = 0; i < touchPoints.count(); ++i) {
328 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
329 if (touchPoint.state() == Qt::TouchPointPressed) {
331 // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture
334 // that's our candidate
335 m_touchId = touchPoint.id();
336 newTouchPoint = &touchPoint;
341 Q_ASSERT(newTouchPoint);
343 // If we have made this far, we are good to go to the next status.
345 m_startPos = newTouchPoint->pos();
346 m_startScenePos = newTouchPoint->scenePos();
347 m_touchId = newTouchPoint->id();
348 m_dampedScenePos.reset(m_startScenePos);
349 m_velocityCalculator->setTrackedPosition(0.);
350 m_velocityCalculator->reset();
351 m_numSamplesOnLastSpeedCheck = 0;
353 setPreviousPos(m_startPos);
354 setPreviousScenePos(m_startScenePos);
355 updateSceneDirectionVector();
357 setStatus(Undecided);
360void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
362 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
365 qCritical() << "DirectionalDragArea[status=Undecided]: touch " << m_touchId
366 << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
367 "Considering it as released.";
368 setStatus(WaitingForTouch);
372 const QPointF &touchScenePos = touchPoint->scenePos();
374 if (touchPoint->state() == Qt::TouchPointReleased) {
375 // touch has ended before recognition concluded
376 DDA_DEBUG("Touch has ended before recognition concluded");
377 setStatus(WaitingForTouch);
381 if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
382 // multi-finger drags are not accepted
383 DDA_DEBUG("Multi-finger drags are not accepted");
384 setStatus(WaitingForTouch);
388 m_previousDampedScenePos.setX(m_dampedScenePos.x());
389 m_previousDampedScenePos.setY(m_dampedScenePos.y());
390 m_dampedScenePos.update(touchScenePos);
391 updateVelocityCalculator(touchScenePos);
393 if (!pointInsideAllowedArea()) {
394 DDA_DEBUG("Rejecting gesture because touch point is outside allowed area.");
395 setStatus(WaitingForTouch);
399 if (!movingInRightDirection()) {
400 DDA_DEBUG("Rejecting gesture becauuse touch point is moving in the wrong direction.");
401 setStatus(WaitingForTouch);
405 setPreviousPos(touchPoint->pos());
406 setPreviousScenePos(touchScenePos);
408 if (isWithinTouchCompositionWindow()) {
409 // There's still time for some new touch to appear and ruin our party as it would be combined
410 // with our m_touchId one and therefore deny the possibility of a single-finger gesture.
411 DDA_DEBUG("Sill within composition window. Let's wait more.");
415 if (movedFarEnough(touchScenePos)) {
416 setStatus(Recognized);
418 DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
422void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
424 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
427 qCritical() << "DirectionalDragArea[status=Recognized]: touch " << m_touchId
428 << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
429 "Considering it as released.";
430 setStatus(WaitingForTouch);
432 setPreviousPos(touchPoint->pos());
433 setPreviousScenePos(touchPoint->scenePos());
435 if (touchPoint->state() == Qt::TouchPointReleased) {
436 setStatus(WaitingForTouch);
441const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
443 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
444 const QTouchEvent::TouchPoint *touchPoint = 0;
445 for (int i = 0; i < touchPoints.size(); ++i) {
446 if (touchPoints.at(i).id() == m_touchId) {
447 touchPoint = &touchPoints.at(i);
454bool DirectionalDragArea::pointInsideAllowedArea() const
456 // NB: Using squared values to avoid computing the square root to find
457 // the length totalMovement
459 QPointF totalMovement(m_dampedScenePos.x() - m_startScenePos.x(),
460 m_dampedScenePos.y() - m_startScenePos.y());
462 qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
463 totalMovement.y() * totalMovement.y();
465 if (squaredTotalMovSize == 0.) {
470 qreal projectedMovement = projectOntoDirectionVector(totalMovement);
473 qreal cosineAngleSquared = (projectedMovement * projectedMovement) / squaredTotalMovSize;
476 // angle_between_movement_vector_and_gesture_direction_vector <= widening_angle
477 return cosineAngleSquared >= m_wideningFactor;
480bool DirectionalDragArea::movingInRightDirection() const
482 QPointF movementVector(m_dampedScenePos.x() - m_previousDampedScenePos.x(),
483 m_dampedScenePos.y() - m_previousDampedScenePos.y());
485 qreal scalarProjection = projectOntoDirectionVector(movementVector);
487 return scalarProjection >= 0.;
490bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
492 if (m_distanceThreshold <= 0.) {
493 // distance threshold check is disabled
496 QPointF totalMovement(point.x() - m_startScenePos.x(),
497 point.y() - m_startScenePos.y());
499 qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
500 totalMovement.y() * totalMovement.y();
502 return squaredTotalMovSize > m_distanceThresholdSquared;
506void DirectionalDragArea::checkSpeed()
508 if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
509 qreal speed = qFabs(m_velocityCalculator->calculate());
510 qreal minSpeedMsecs = m_minSpeed / 1000.0;
512 if (speed < minSpeedMsecs) {
513 DDA_DEBUG("Rejecting gesture because it's below minimum speed.");
514 setStatus(WaitingForTouch);
518 if (m_velocityCalculator->numSamples() == m_numSamplesOnLastSpeedCheck) {
519 m_silenceTime += m_recognitionTimer->interval();
521 if (m_silenceTime > m_maxSilenceTime) {
522 DDA_DEBUG("Rejecting gesture because it's silence time has been exceeded.");
523 setStatus(WaitingForTouch);
528 m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
531void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
533 if (newStatus == m_status)
536 DirectionalDragArea::Status oldStatus = m_status;
538 if (oldStatus == Undecided) {
539 m_recognitionTimer->stop();
542 m_status = newStatus;
543 Q_EMIT statusChanged(m_status);
545 #if DIRECTIONALDRAGAREA_DEBUG
546 qDebug() << "[DDA]" << statusToString(oldStatus) << "->" << statusToString(newStatus);
550 case WaitingForTouch:
551 Q_EMIT draggingChanged(false);
554 m_recognitionTimer->start();
555 Q_EMIT draggingChanged(true);
558 if (oldStatus == WaitingForTouch)
559 Q_EMIT draggingChanged(true);
567void DirectionalDragArea::setPreviousPos(const QPointF &point)
569 bool xChanged = m_previousPos.x() != point.x();
570 bool yChanged = m_previousPos.y() != point.y();
572 m_previousPos = point;
575 Q_EMIT touchXChanged(point.x());
576 if (Direction::isHorizontal(m_direction))
577 Q_EMIT distanceChanged(distance());
581 Q_EMIT touchYChanged(point.y());
582 if (Direction::isVertical(m_direction))
583 Q_EMIT distanceChanged(distance());
587void DirectionalDragArea::setPreviousScenePos(const QPointF &point)
589 bool xChanged = m_previousScenePos.x() != point.x();
590 bool yChanged = m_previousScenePos.y() != point.y();
592 if (!xChanged && !yChanged)
595 qreal oldSceneDistance = sceneDistance();
596 m_previousScenePos = point;
597 updateSceneDistance();
599 if (oldSceneDistance != sceneDistance()) {
600 Q_EMIT sceneDistanceChanged(sceneDistance());
604 Q_EMIT touchSceneXChanged(point.x());
608 Q_EMIT touchSceneYChanged(point.y());
612void DirectionalDragArea::updateVelocityCalculator(const QPointF &scenePos)
614 QPointF totalSceneMovement = scenePos - m_startScenePos;
616 qreal scalarProjection = projectOntoDirectionVector(totalSceneMovement);
618 m_velocityCalculator->setTrackedPosition(scalarProjection);
621bool DirectionalDragArea::isWithinTouchCompositionWindow()
623 return !m_activeTouches.isEmpty() &&
624 m_timeSource->msecsSinceReference() <=
625 m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
628//************************** ActiveTouchesInfo **************************
630DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(const SharedTimeSource &timeSource)
631 : m_timeSource(timeSource), m_lastUsedIndex(-1)
633 // Estimate of the maximum number of active touches we might reach.
634 // Not a problem if it ends up being an underestimate as this is just
639void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
641 if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
646 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
647 for (int i = 0; i < touchPoints.count(); ++i) {
648 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
649 if (touchPoint.state() == Qt::TouchPointPressed) {
650 addTouchPoint(touchPoint);
651 } else if (touchPoint.state() == Qt::TouchPointReleased) {
652 removeTouchPoint(touchPoint);
657void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
659 ActiveTouchInfo &activeTouchInfo = getEmptySlot();
660 activeTouchInfo.id = touchPoint.id();
661 activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
664void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
666 for (int i = 0; i <= m_lastUsedIndex; ++i) {
667 if (touchPoint.id() == m_vector.at(i).id) {
672 Q_ASSERT(false); // shouldn't reach this point
675DirectionalDragArea::ActiveTouchInfo &DirectionalDragArea::ActiveTouchesInfo::getEmptySlot()
677 Q_ASSERT(m_lastUsedIndex < m_vector.size());
679 // Look for an in-between vacancy first
680 for (int i = 0; i < m_lastUsedIndex; ++i) {
681 ActiveTouchInfo &activeTouchInfo = m_vector[i];
682 if (!activeTouchInfo.isValid()) {
683 return activeTouchInfo;
688 if (m_lastUsedIndex >= m_vector.size()) {
689 m_vector.resize(m_lastUsedIndex + 1);
692 return m_vector[m_lastUsedIndex];
695void DirectionalDragArea::ActiveTouchesInfo::freeSlot(int index)
697 m_vector[index].reset();
698 if (index == m_lastUsedIndex) {
701 } while (m_lastUsedIndex >= 0 && !m_vector.at(m_lastUsedIndex).isValid());
705qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
707 Q_ASSERT(m_lastUsedIndex >= 0);
709 qint64 highestStartTime = m_vector.at(0).startTime;
712 const ActiveTouchInfo &activeTouchInfo = m_vector.at(i);
713 if (activeTouchInfo.isValid() && activeTouchInfo.startTime > highestStartTime) {
714 highestStartTime = activeTouchInfo.startTime;
717 } while (i < m_lastUsedIndex);
719 return highestStartTime;
722void DirectionalDragArea::updateSceneDirectionVector()
724 QPointF localOrigin(0., 0.);
725 QPointF localDirection;
726 switch (m_direction) {
727 case Direction::Upwards:
728 localDirection.rx() = 0.;
729 localDirection.ry() = -1.;
731 case Direction::Downwards:
732 localDirection.rx() = 0.;
733 localDirection.ry() = 1;
735 case Direction::Leftwards:
736 localDirection.rx() = -1.;
737 localDirection.ry() = 0.;
739 default: // Direction::Rightwards:
740 localDirection.rx() = 1.;
741 localDirection.ry() = 0.;
744 QPointF sceneOrigin = mapToScene(localOrigin);
745 QPointF sceneDirection = mapToScene(localDirection);
746 m_sceneDirectionVector = sceneDirection - sceneOrigin;
749qreal DirectionalDragArea::projectOntoDirectionVector(const QPointF &sceneVector) const
751 // same as dot product as m_sceneDirectionVector is a unit vector
752 return sceneVector.x() * m_sceneDirectionVector.x() +
753 sceneVector.y() * m_sceneDirectionVector.y();
756// Because we are defining a new QObject-based class (RecognitionTimer) here.
757#include "DirectionalDragArea.moc"
keysesc to close
- j k
- move down, up
- enter
- open
- esc
- back, or up a level
- 1 2 3 4
- readme, files, commits, refs
- /
- filter
- y
- copy the clone command
- c
- compare the chosen commit with another (or shift-click both)
- ?
- show or hide this list