on 551c708 help sign in
unity8/plugins/Ubuntu/Gestures/DirectionalDragArea.cpp
697 lines 21 kB history raw
1/*
2 * Copyright (C) 2013 Canonical, Ltd.
3 *
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.
7 *
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.
12 *
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/>.
15 */
17#include "DirectionalDragArea.h"
19#include <QtCore/qmath.h>
20#include <QtCore/QTimer>
21#include <QDebug>
23using namespace UbuntuGestures;
25#define DIRECTIONALDRAGAREA_DEBUG 0
27#if DIRECTIONALDRAGAREA_DEBUG
28#define DDA_DEBUG(msg) qDebug("[DDA] " msg)
29namespace {
30QString touchPointStateToString(Qt::TouchPointState state) {
31 switch (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");
40 }
42QString touchEventToString(QTouchEvent *ev)
44 QString message;
46 switch (ev->type()) {
47 case QEvent::TouchBegin:
48 message.append("TouchBegin ");
49 break;
50 case QEvent::TouchUpdate:
51 message.append("TouchUpdate ");
52 break;
53 case QEvent::TouchEnd:
54 message.append("TouchEnd ");
55 break;
56 default: //QEvent::TouchCancel
57 message.append("TouchCancel ");
58 }
60 for (int i=0; i < ev->touchPoints().size(); ++i) {
62 const QTouchEvent::TouchPoint& touchPoint = ev->touchPoints().at(i);
63 message.append(
64 QString("(id:%1, state:%2, scenePos:(%3,%4)) ")
65 .arg(touchPoint.id())
66 .arg(touchPointStateToString(touchPoint.state()))
67 .arg(touchPoint.scenePos().x())
68 .arg(touchPoint.scenePos().y())
69 );
70 }
72 return message;
75const char *statusToString(DirectionalDragArea::Status status)
77 if (status == DirectionalDragArea::WaitingForTouch) {
78 return "WaitingForTouch";
79 } else if (status == DirectionalDragArea::Undecided) {
80 return "Undecided";
81 } else {
82 return "Recognized";
83 }
86} // namespace {
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
94 Q_OBJECT
95public:
96 RecognitionTimer(QObject *parent) : UbuntuGestures::AbstractTimer(parent) {
97 m_timer.setSingleShot(false);
98 connect(&m_timer, &QTimer::timeout,
99 this, &UbuntuGestures::AbstractTimer::timeout);
100 }
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(); }
105private:
106 QTimer m_timer;
107};
109DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
110 : QQuickItem(parent)
111 , m_status(WaitingForTouch)
112 , m_touchId(-1)
113 , m_direction(Direction::Rightwards)
114 , m_wideningAngle(0)
115 , m_wideningFactor(0)
116 , m_distanceThreshold(0)
117 , m_minSpeed(0)
118 , m_maxSilenceTime(200)
119 , m_silenceTime(0)
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
135 return m_direction;
138void DirectionalDragArea::setDirection(Direction::Type direction)
140 if (direction != m_direction) {
141 m_direction = direction;
142 Q_EMIT directionChanged(m_direction);
143 }
146void DirectionalDragArea::setMaxDeviation(qreal value)
148 if (m_dampedScenePos.maxDelta() != value) {
149 m_dampedScenePos.setMaxDelta(value);
150 Q_EMIT maxDeviationChanged(value);
151 }
154qreal DirectionalDragArea::wideningAngle() const
156 return m_wideningAngle;
159void DirectionalDragArea::setWideningAngle(qreal angle)
161 if (angle == m_wideningAngle)
162 return;
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);
174 }
177void DirectionalDragArea::setMinSpeed(qreal value)
179 if (m_minSpeed != value) {
180 m_minSpeed = value;
181 Q_EMIT minSpeedChanged(value);
182 }
185void DirectionalDragArea::setMaxSilenceTime(int value)
187 if (m_maxSilenceTime != value) {
188 m_maxSilenceTime = value;
189 Q_EMIT maxSilenceTimeChanged(value);
190 }
193void DirectionalDragArea::setCompositionTime(int value)
195 if (m_compositionTime != value) {
196 m_compositionTime = value;
197 Q_EMIT compositionTimeChanged(value);
198 }
201void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
203 int interval = 0;
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;
212 }
213 }
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();
221 }
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();
235 } else {
236 return m_previousPos.y() - m_startPos.y();
237 }
240qreal DirectionalDragArea::sceneDistance() const
242 if (Direction::isHorizontal(m_direction)) {
243 return m_previousScenePos.x() - m_startScenePos.x();
244 } else {
245 return m_previousScenePos.y() - m_startScenePos.y();
246 }
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));
275 #endif
277 if (!isEnabled() || !isVisible()) {
278 QQuickItem::touchEvent(event);
279 return;
280 }
282 switch (m_status) {
283 case WaitingForTouch:
284 touchEvent_absent(event);
285 break;
286 case Undecided:
287 touchEvent_undecided(event);
288 break;
289 default: // Recognized:
290 touchEvent_recognized(event);
291 break;
292 }
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.
301 return;
302 }
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.");
309 #endif
310 return;
311 }
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) {
319 if (newTouchPoint) {
320 // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture
321 return;
322 } else {
323 // that's our candidate
324 m_touchId = touchPoint.id();
325 newTouchPoint = &touchPoint;
326 }
327 }
328 }
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;
341 m_silenceTime = 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);
352 if (!touchPoint) {
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);
357 return;
358 }
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);
366 return;
367 }
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);
373 return;
374 }
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);
384 return;
385 }
387 if (!movingInRightDirection()) {
388 DDA_DEBUG("Rejecting gesture becauuse touch point is moving in the wrong direction.");
389 setStatus(WaitingForTouch);
390 return;
391 }
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.");
400 return;
401 }
403 if (movedFarEnough(touchScenePos)) {
404 setStatus(Recognized);
405 } else {
406 DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
407 }
410void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
412 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
414 if (!touchPoint) {
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);
419 } else {
420 setPreviousPos(touchPoint->pos());
421 setPreviousScenePos(touchPoint->scenePos());
423 if (touchPoint->state() == Qt::TouchPointReleased) {
424 setStatus(WaitingForTouch);
425 }
426 }
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);
436 break;
437 }
438 }
439 return touchPoint;
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;
456 }
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();
470 }
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;
478 else
479 return qFabs(point.y() - m_startScenePos.y()) > m_distanceThreshold;
480 } else {
481 return true;
482 }
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);
494 }
495 }
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);
503 }
504 } else {
505 m_silenceTime = 0;
506 }
507 m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
510void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
512 if (newStatus == m_status)
513 return;
515 DirectionalDragArea::Status oldStatus = m_status;
517 if (oldStatus == Undecided) {
518 m_recognitionTimer->stop();
519 }
521 m_status = newStatus;
522 Q_EMIT statusChanged(m_status);
524 #if DIRECTIONALDRAGAREA_DEBUG
525 qDebug() << "[DDA]" << statusToString(oldStatus) << "->" << statusToString(newStatus);
526 #endif
528 switch (newStatus) {
529 case WaitingForTouch:
530 Q_EMIT draggingChanged(false);
531 break;
532 case Undecided:
533 m_recognitionTimer->start();
534 Q_EMIT draggingChanged(true);
535 break;
536 case Recognized:
537 if (oldStatus == WaitingForTouch)
538 Q_EMIT draggingChanged(true);
539 break;
540 default:
541 // no-op
542 break;
543 }
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;
553 if (xChanged) {
554 Q_EMIT touchXChanged(point.x());
555 if (Direction::isHorizontal(m_direction))
556 Q_EMIT distanceChanged(distance());
557 }
559 if (yChanged) {
560 Q_EMIT touchYChanged(point.y());
561 if (Direction::isVertical(m_direction))
562 Q_EMIT distanceChanged(distance());
563 }
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;
573 if (xChanged) {
574 Q_EMIT touchSceneXChanged(point.x());
575 if (Direction::isHorizontal(m_direction))
576 Q_EMIT sceneDistanceChanged(sceneDistance());
577 }
579 if (yChanged) {
580 Q_EMIT touchSceneYChanged(point.y());
581 if (Direction::isVertical(m_direction))
582 Q_EMIT sceneDistanceChanged(sceneDistance());
583 }
586void DirectionalDragArea::updateVelocityCalculator(const QPointF &point)
588 if (Direction::isHorizontal(m_direction)) {
589 m_velocityCalculator->setTrackedPosition(point.x());
590 } else {
591 m_velocityCalculator->setTrackedPosition(point.y());
592 }
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
609 // an optimization.
610 m_vector.resize(3);
613void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
615 if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
616 // nothing to update
617 return;
618 }
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);
627 }
628 }
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) {
642 freeSlot(i);
643 return;
644 }
645 }
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;
658 }
659 }
661 ++m_lastUsedIndex;
662 if (m_lastUsedIndex >= m_vector.size()) {
663 m_vector.resize(m_lastUsedIndex + 1);
664 }
666 return m_vector[m_lastUsedIndex];
669void DirectionalDragArea::ActiveTouchesInfo::freeSlot(int index)
671 m_vector[index].reset();
672 if (index == m_lastUsedIndex) {
673 do {
674 --m_lastUsedIndex;
675 } while (m_lastUsedIndex >= 0 && !m_vector.at(m_lastUsedIndex).isValid());
676 }
679qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
681 Q_ASSERT(m_lastUsedIndex >= 0);
683 qint64 highestStartTime = m_vector.at(0).startTime;
684 int i = 1;
685 do {
686 const ActiveTouchInfo &activeTouchInfo = m_vector.at(i);
687 if (activeTouchInfo.isValid() && activeTouchInfo.startTime > highestStartTime) {
688 highestStartTime = activeTouchInfo.startTime;
689 }
690 ++i;
691 } while (i < m_lastUsedIndex);
693 return highestStartTime;
696// Because we are defining a new QObject-based class (RecognitionTimer) here.
697#include "DirectionalDragArea.moc"