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)
118 , m_maxSilenceTime(200)
120 , m_compositionTime(60)
121 , m_numSamplesOnLastSpeedCheck(0)
122 , m_recognitionTimer(0)
123 , m_velocityCalculator(0)
124 , m_timeSource(new RealTimeSource)
125 , m_activeTouches(m_timeSource)
127 setRecognitionTimer(new RecognitionTimer(this));
128 m_recognitionTimer->setInterval(60);
130 m_velocityCalculator = new AxisVelocityCalculator(this);
133Direction::Type DirectionalDragArea::direction() const
138void DirectionalDragArea::setDirection(Direction::Type direction)
140 if (direction != m_direction) {
141 m_direction = direction;
142 Q_EMIT directionChanged(m_direction);
146void DirectionalDragArea::setMaxDeviation(qreal value)
148 if (m_dampedScenePos.maxDelta() != value) {
149 m_dampedScenePos.setMaxDelta(value);
150 Q_EMIT maxDeviationChanged(value);
154qreal DirectionalDragArea::wideningAngle() const
156 return m_wideningAngle;
159void DirectionalDragArea::setWideningAngle(qreal angle)
161 if (angle == m_wideningAngle)
164 m_wideningAngle = angle;
165 m_wideningFactor = qTan(angle * M_PI / 180.0);
166 Q_EMIT wideningAngleChanged(angle);
169void DirectionalDragArea::setDistanceThreshold(qreal value)
171 if (m_distanceThreshold != value) {
172 m_distanceThreshold = value;
173 Q_EMIT distanceThresholdChanged(value);
177void DirectionalDragArea::setMinSpeed(qreal value)
179 if (m_minSpeed != value) {
181 Q_EMIT minSpeedChanged(value);
185void DirectionalDragArea::setMaxSilenceTime(int value)
187 if (m_maxSilenceTime != value) {
188 m_maxSilenceTime = value;
189 Q_EMIT maxSilenceTimeChanged(value);
193void DirectionalDragArea::setCompositionTime(int value)
195 if (m_compositionTime != value) {
196 m_compositionTime = value;
197 Q_EMIT compositionTimeChanged(value);
201void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
204 bool timerWasRunning = false;
206 // can be null when called from the constructor
207 if (m_recognitionTimer) {
208 interval = m_recognitionTimer->interval();
209 timerWasRunning = m_recognitionTimer->isRunning();
210 if (m_recognitionTimer->parent() == this) {
211 delete m_recognitionTimer;
215 m_recognitionTimer = timer;
216 timer->setInterval(interval);
217 connect(timer, &UbuntuGestures::AbstractTimer::timeout,
218 this, &DirectionalDragArea::checkSpeed);
219 if (timerWasRunning) {
220 m_recognitionTimer->start();
224void DirectionalDragArea::setTimeSource(const SharedTimeSource &timeSource)
226 m_timeSource = timeSource;
227 m_velocityCalculator->setTimeSource(timeSource);
228 m_activeTouches.m_timeSource = timeSource;
231qreal DirectionalDragArea::distance() const
233 if (Direction::isHorizontal(m_direction)) {
234 return m_previousPos.x() - m_startPos.x();
236 return m_previousPos.y() - m_startPos.y();
240qreal DirectionalDragArea::sceneDistance() const
242 if (Direction::isHorizontal(m_direction)) {
243 return m_previousScenePos.x() - m_startScenePos.x();
245 return m_previousScenePos.y() - m_startScenePos.y();
249qreal DirectionalDragArea::touchX() const
251 return m_previousPos.x();
254qreal DirectionalDragArea::touchY() const
256 return m_previousPos.y();
259qreal DirectionalDragArea::touchSceneX() const
261 return m_previousScenePos.x();
264qreal DirectionalDragArea::touchSceneY() const
266 return m_previousScenePos.y();
269void DirectionalDragArea::touchEvent(QTouchEvent *event)
271 #if DIRECTIONALDRAGAREA_DEBUG
272 // TODO Consider using qCDebug() when available (Qt 5.2)
273 qDebug() << "[DDA]" << m_timeSource->msecsSinceReference()
274 << qPrintable(touchEventToString(event));
277 if (!isEnabled() || !isVisible()) {
278 QQuickItem::touchEvent(event);
283 case WaitingForTouch:
284 touchEvent_absent(event);
287 touchEvent_undecided(event);
289 default: // Recognized:
290 touchEvent_recognized(event);
294 m_activeTouches.update(event);
297void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
299 if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
300 // Nothing to see here. No touch starting in this event.
304 if (isWithinTouchCompositionWindow()) {
305 // too close to the last touch start. So we consider them as starting roughly at the same time.
306 // Can't be a single-touch gesture.
307 #if DIRECTIONALDRAGAREA_DEBUG
308 qDebug("[DDA] A new touch point came in but we're still within time composition window. Ignoring it.");
313 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
315 const QTouchEvent::TouchPoint *newTouchPoint = nullptr;
316 for (int i = 0; i < touchPoints.count(); ++i) {
317 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
318 if (touchPoint.state() == Qt::TouchPointPressed) {
320 // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture
323 // that's our candidate
324 m_touchId = touchPoint.id();
325 newTouchPoint = &touchPoint;
330 Q_ASSERT(newTouchPoint);
332 // If we have made this far, we are good to go to the next status.
334 m_startPos = newTouchPoint->pos();
335 m_startScenePos = newTouchPoint->scenePos();
336 m_touchId = newTouchPoint->id();
337 m_dampedScenePos.reset(m_startScenePos);
338 updateVelocityCalculator(m_startScenePos);
339 m_velocityCalculator->reset();
340 m_numSamplesOnLastSpeedCheck = 0;
342 setPreviousPos(m_startPos);
343 setPreviousScenePos(m_startScenePos);
345 setStatus(Undecided);
348void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
350 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
353 qCritical() << "DirectionalDragArea[status=Undecided]: touch " << m_touchId
354 << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
355 "Considering it as released.";
356 setStatus(WaitingForTouch);
360 const QPointF &touchScenePos = touchPoint->scenePos();
362 if (touchPoint->state() == Qt::TouchPointReleased) {
363 // touch has ended before recognition concluded
364 DDA_DEBUG("Touch has ended before recognition concluded");
365 setStatus(WaitingForTouch);
369 if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
370 // multi-finger drags are not accepted
371 DDA_DEBUG("Multi-finger drags are not accepted");
372 setStatus(WaitingForTouch);
376 m_previousDampedScenePos.setX(m_dampedScenePos.x());
377 m_previousDampedScenePos.setY(m_dampedScenePos.y());
378 m_dampedScenePos.update(touchScenePos);
379 updateVelocityCalculator(touchScenePos);
381 if (!pointInsideAllowedArea()) {
382 DDA_DEBUG("Rejecting gesture because touch point is outside allowed area.");
383 setStatus(WaitingForTouch);
387 if (!movingInRightDirection()) {
388 DDA_DEBUG("Rejecting gesture becauuse touch point is moving in the wrong direction.");
389 setStatus(WaitingForTouch);
393 setPreviousPos(touchPoint->pos());
394 setPreviousScenePos(touchScenePos);
396 if (isWithinTouchCompositionWindow()) {
397 // There's still time for some new touch to appear and ruin our party as it would be combined
398 // with our m_touchId one and therefore deny the possibility of a single-finger gesture.
399 DDA_DEBUG("Sill within composition window. Let's wait more.");
403 if (movedFarEnough(touchScenePos)) {
404 setStatus(Recognized);
406 DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
410void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
412 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
415 qCritical() << "DirectionalDragArea[status=Recognized]: touch " << m_touchId
416 << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
417 "Considering it as released.";
418 setStatus(WaitingForTouch);
420 setPreviousPos(touchPoint->pos());
421 setPreviousScenePos(touchPoint->scenePos());
423 if (touchPoint->state() == Qt::TouchPointReleased) {
424 setStatus(WaitingForTouch);
429const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
431 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
432 const QTouchEvent::TouchPoint *touchPoint = 0;
433 for (int i = 0; i < touchPoints.size(); ++i) {
434 if (touchPoints.at(i).id() == m_touchId) {
435 touchPoint = &touchPoints.at(i);
442bool DirectionalDragArea::pointInsideAllowedArea() const
444 qreal dX = m_dampedScenePos.x() - m_startScenePos.x();
445 qreal dY = m_dampedScenePos.y() - m_startScenePos.y();
447 switch (m_direction) {
448 case Direction::Upwards:
449 return dY <= 0 && qFabs(dX) <= qFabs(dY) * m_wideningFactor;
450 case Direction::Downwards:
451 return dY >= 0 && qFabs(dX) <= dY * m_wideningFactor;
452 case Direction::Leftwards:
453 return dX <= 0 && qFabs(dY) <= qFabs(dX) * m_wideningFactor;
454 default: // Direction::Rightwards:
455 return dX >= 0 && qFabs(dY) <= dX * m_wideningFactor;
459bool DirectionalDragArea::movingInRightDirection() const
461 switch (m_direction) {
462 case Direction::Upwards:
463 return m_dampedScenePos.y() <= m_previousDampedScenePos.y();
464 case Direction::Downwards:
465 return m_dampedScenePos.y() >= m_previousDampedScenePos.y();
466 case Direction::Leftwards:
467 return m_dampedScenePos.x() <= m_previousDampedScenePos.x();
468 default: // Direction::Rightwards:
469 return m_dampedScenePos.x() >= m_previousDampedScenePos.x();
473bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
475 if (m_distanceThreshold > 0) {
476 if (Direction::isHorizontal(m_direction))
477 return qFabs(point.x() - m_startScenePos.x()) > m_distanceThreshold;
479 return qFabs(point.y() - m_startScenePos.y()) > m_distanceThreshold;
485void DirectionalDragArea::checkSpeed()
487 if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
488 qreal speed = qFabs(m_velocityCalculator->calculate());
489 qreal minSpeedMsecs = m_minSpeed / 1000.0;
491 if (speed < minSpeedMsecs) {
492 DDA_DEBUG("Rejecting gesture because it's below minimum speed.");
493 setStatus(WaitingForTouch);
497 if (m_velocityCalculator->numSamples() == m_numSamplesOnLastSpeedCheck) {
498 m_silenceTime += m_recognitionTimer->interval();
500 if (m_silenceTime > m_maxSilenceTime) {
501 DDA_DEBUG("Rejecting gesture because it's silence time has been exceeded.");
502 setStatus(WaitingForTouch);
507 m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
510void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
512 if (newStatus == m_status)
515 DirectionalDragArea::Status oldStatus = m_status;
517 if (oldStatus == Undecided) {
518 m_recognitionTimer->stop();
521 m_status = newStatus;
522 Q_EMIT statusChanged(m_status);
524 #if DIRECTIONALDRAGAREA_DEBUG
525 qDebug() << "[DDA]" << statusToString(oldStatus) << "->" << statusToString(newStatus);
529 case WaitingForTouch:
530 Q_EMIT draggingChanged(false);
533 m_recognitionTimer->start();
534 Q_EMIT draggingChanged(true);
537 if (oldStatus == WaitingForTouch)
538 Q_EMIT draggingChanged(true);
546void DirectionalDragArea::setPreviousPos(const QPointF &point)
548 bool xChanged = m_previousPos.x() != point.x();
549 bool yChanged = m_previousPos.y() != point.y();
551 m_previousPos = point;
554 Q_EMIT touchXChanged(point.x());
555 if (Direction::isHorizontal(m_direction))
556 Q_EMIT distanceChanged(distance());
560 Q_EMIT touchYChanged(point.y());
561 if (Direction::isVertical(m_direction))
562 Q_EMIT distanceChanged(distance());
566void DirectionalDragArea::setPreviousScenePos(const QPointF &point)
568 bool xChanged = m_previousScenePos.x() != point.x();
569 bool yChanged = m_previousScenePos.y() != point.y();
571 m_previousScenePos = point;
574 Q_EMIT touchSceneXChanged(point.x());
575 if (Direction::isHorizontal(m_direction))
576 Q_EMIT sceneDistanceChanged(sceneDistance());
580 Q_EMIT touchSceneYChanged(point.y());
581 if (Direction::isVertical(m_direction))
582 Q_EMIT sceneDistanceChanged(sceneDistance());
586void DirectionalDragArea::updateVelocityCalculator(const QPointF &point)
588 if (Direction::isHorizontal(m_direction)) {
589 m_velocityCalculator->setTrackedPosition(point.x());
591 m_velocityCalculator->setTrackedPosition(point.y());
595bool DirectionalDragArea::isWithinTouchCompositionWindow()
597 return !m_activeTouches.isEmpty() &&
598 m_timeSource->msecsSinceReference() <=
599 m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
602//************************** ActiveTouchesInfo **************************
604DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(const SharedTimeSource &timeSource)
605 : m_timeSource(timeSource), m_lastUsedIndex(-1)
607 // Estimate of the maximum number of active touches we might reach.
608 // Not a problem if it ends up being an underestimate as this is just
613void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
615 if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
620 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
621 for (int i = 0; i < touchPoints.count(); ++i) {
622 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
623 if (touchPoint.state() == Qt::TouchPointPressed) {
624 addTouchPoint(touchPoint);
625 } else if (touchPoint.state() == Qt::TouchPointReleased) {
626 removeTouchPoint(touchPoint);
631void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
633 ActiveTouchInfo &activeTouchInfo = getEmptySlot();
634 activeTouchInfo.id = touchPoint.id();
635 activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
638void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
640 for (int i = 0; i <= m_lastUsedIndex; ++i) {
641 if (touchPoint.id() == m_vector.at(i).id) {
646 Q_ASSERT(false); // shouldn't reach this point
649DirectionalDragArea::ActiveTouchInfo &DirectionalDragArea::ActiveTouchesInfo::getEmptySlot()
651 Q_ASSERT(m_lastUsedIndex < m_vector.size());
653 // Look for an in-between vacancy first
654 for (int i = 0; i < m_lastUsedIndex; ++i) {
655 ActiveTouchInfo &activeTouchInfo = m_vector[i];
656 if (!activeTouchInfo.isValid()) {
657 return activeTouchInfo;
662 if (m_lastUsedIndex >= m_vector.size()) {
663 m_vector.resize(m_lastUsedIndex + 1);
666 return m_vector[m_lastUsedIndex];
669void DirectionalDragArea::ActiveTouchesInfo::freeSlot(int index)
671 m_vector[index].reset();
672 if (index == m_lastUsedIndex) {
675 } while (m_lastUsedIndex >= 0 && !m_vector.at(m_lastUsedIndex).isValid());
679qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
681 Q_ASSERT(m_lastUsedIndex >= 0);
683 qint64 highestStartTime = m_vector.at(0).startTime;
686 const ActiveTouchInfo &activeTouchInfo = m_vector.at(i);
687 if (activeTouchInfo.isValid() && activeTouchInfo.startTime > highestStartTime) {
688 highestStartTime = activeTouchInfo.startTime;
691 } while (i < m_lastUsedIndex);
693 return highestStartTime;
696// Because we are defining a new QObject-based class (RecognitionTimer) here.
697#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