on 5bc8421 help sign in
unity8/plugins/Ubuntu/Gestures/DirectionalDragArea.cpp
461 lines 13 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>
22// Essentially a QTimer wrapper
23class RecognitionTimer : public UbuntuGestures::AbstractTimer
25 Q_OBJECT
26public:
27 RecognitionTimer(QObject *parent) : UbuntuGestures::AbstractTimer(parent) {
28 m_timer.setSingleShot(false);
29 connect(&m_timer, &QTimer::timeout,
30 this, &UbuntuGestures::AbstractTimer::timeout);
31 }
32 virtual int interval() const { return m_timer.interval(); }
33 virtual void setInterval(int msecs) { m_timer.setInterval(msecs); }
34 virtual void start() { m_timer.start(); UbuntuGestures::AbstractTimer::start(); }
35 virtual void stop() { m_timer.stop(); UbuntuGestures::AbstractTimer::stop(); }
36private:
37 QTimer m_timer;
38};
40DirectionalDragArea::DirectionalDragArea(QQuickItem *parent)
41 : QQuickItem(parent)
42 , m_status(WaitingForTouch)
43 , m_touchId(-1)
44 , m_direction(Direction::Rightwards)
45 , m_wideningAngle(0)
46 , m_wideningFactor(0)
47 , m_distanceThreshold(0)
48 , m_minSpeed(0)
49 , m_maxSilenceTime(200)
50 , m_silenceTime(0)
51 , m_numSamplesOnLastSpeedCheck(0)
52 , m_recognitionTimer(0)
53 , m_velocityCalculator(0)
55 setRecognitionTimer(new RecognitionTimer(this));
56 m_recognitionTimer->setInterval(60);
58 m_velocityCalculator = new AxisVelocityCalculator(this);
61Direction::Type DirectionalDragArea::direction() const
63 return m_direction;
66void DirectionalDragArea::setDirection(Direction::Type direction)
68 if (direction != m_direction) {
69 m_direction = direction;
70 Q_EMIT directionChanged(m_direction);
71 }
74void DirectionalDragArea::setMaxDeviation(qreal value)
76 if (m_dampedPos.maxDelta() != value) {
77 m_dampedPos.setMaxDelta(value);
78 Q_EMIT maxDeviationChanged(value);
79 }
82qreal DirectionalDragArea::wideningAngle() const
84 return m_wideningAngle;
87void DirectionalDragArea::setWideningAngle(qreal angle)
89 if (angle == m_wideningAngle)
90 return;
92 m_wideningAngle = angle;
93 m_wideningFactor = qTan(angle * M_PI / 180.0);
94 Q_EMIT wideningAngleChanged(angle);
97void DirectionalDragArea::setDistanceThreshold(qreal value)
99 if (m_distanceThreshold != value) {
100 m_distanceThreshold = value;
101 Q_EMIT distanceThresholdChanged(value);
102 }
105void DirectionalDragArea::setMinSpeed(qreal value)
107 if (m_minSpeed != value) {
108 m_minSpeed = value;
109 Q_EMIT minSpeedChanged(value);
110 }
113void DirectionalDragArea::setMaxSilenceTime(int value)
115 if (m_maxSilenceTime != value) {
116 m_maxSilenceTime = value;
117 Q_EMIT maxSilenceTimeChanged(value);
118 }
121void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
123 int interval = 0;
124 bool timerWasRunning = false;
126 // can be null when called from the constructor
127 if (m_recognitionTimer) {
128 interval = m_recognitionTimer->interval();
129 timerWasRunning = m_recognitionTimer->isRunning();
130 if (m_recognitionTimer->parent() == this) {
131 delete m_recognitionTimer;
132 }
133 }
135 m_recognitionTimer = timer;
136 timer->setInterval(interval);
137 connect(timer, &UbuntuGestures::AbstractTimer::timeout,
138 this, &DirectionalDragArea::checkSpeed);
139 if (timerWasRunning) {
140 m_recognitionTimer->start();
141 }
144void DirectionalDragArea::setTimeSource(UbuntuGestures::TimeSource *timeSource)
146 m_velocityCalculator->setTimeSource(timeSource);
149qreal DirectionalDragArea::distance() const
151 if (Direction::isHorizontal(m_direction)) {
152 return m_previousPos.x() - m_startPos.x();
153 } else {
154 return m_previousPos.y() - m_startPos.y();
155 }
158qreal DirectionalDragArea::sceneDistance() const
160 if (Direction::isHorizontal(m_direction)) {
161 return m_previousScenePos.x() - m_startScenePos.x();
162 } else {
163 return m_previousScenePos.y() - m_startScenePos.y();
164 }
167qreal DirectionalDragArea::touchX() const
169 return m_previousPos.x();
172qreal DirectionalDragArea::touchY() const
174 return m_previousPos.y();
177qreal DirectionalDragArea::touchSceneX() const
179 return m_previousScenePos.x();
182qreal DirectionalDragArea::touchSceneY() const
184 return m_previousScenePos.y();
187void DirectionalDragArea::touchEvent(QTouchEvent *event)
189 if (!isEnabled() || !isVisible()) {
190 QQuickItem::touchEvent(event);
191 return;
192 }
194 switch (m_status) {
195 case WaitingForTouch:
196 touchEvent_absent(event);
197 break;
198 case Undecided:
199 touchEvent_undecided(event);
200 break;
201 case Recognized:
202 touchEvent_recognized(event);
203 break;
204 default: // Rejected
205 touchEvent_rejected(event);
206 break;
207 }
210void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
212 if ((event->touchPointStates() && (Qt::TouchPointPressed || Qt::TouchPointMoved))
213 && event->touchPoints().count() == 1) {
214 const QTouchEvent::TouchPoint &touchPoint = event->touchPoints()[0];
215 m_startPos = touchPoint.pos();
216 m_startScenePos = touchPoint.scenePos();
217 m_touchId = touchPoint.id();
218 m_dampedPos.reset(m_startPos);
219 updateVelocityCalculator(m_startPos);
220 m_velocityCalculator->reset();
221 m_numSamplesOnLastSpeedCheck = 0;
222 m_silenceTime = 0;
223 setPreviousPos(m_startPos);
224 setPreviousScenePos(m_startScenePos);
226 if (m_distanceThreshold > 0)
227 setStatus(Undecided);
228 else
229 setStatus(Recognized);
230 }
233void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
235 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
236 const QPointF &touchPos = touchPoint->pos();
238 if (touchPoint->state() == Qt::TouchPointReleased) {
239 // touch has ended before recognition concluded
240 setStatus(WaitingForTouch);
241 return;
242 }
244 if (event->touchPointStates().testFlag(Qt::TouchPointPressed)
245 || event->touchPoints().count() > 1) {
246 // multi-finger drags are not accepted
247 setStatus(Rejected);
248 return;
249 }
251 m_previousDampedPos.setX(m_dampedPos.x());
252 m_previousDampedPos.setY(m_dampedPos.y());
253 m_dampedPos.update(touchPos);
254 updateVelocityCalculator(touchPos);
256 if (!pointInsideAllowedArea()) {
257 setStatus(Rejected);
258 return;
259 }
261 if (!movingInRightDirection()) {
262 setStatus(Rejected);
263 return;
264 }
266 setPreviousPos(touchPos);
267 setPreviousScenePos(touchPoint->scenePos());
269 if (movedFarEnough(touchPos)) {
270 setStatus(Recognized);
271 }
274void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
276 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
278 setPreviousPos(touchPoint->pos());
279 setPreviousScenePos(touchPoint->scenePos());
281 if (touchPoint->state() == Qt::TouchPointReleased) {
282 setStatus(WaitingForTouch);
283 }
286void DirectionalDragArea::touchEvent_rejected(QTouchEvent *event)
288 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
290 if (!touchPoint || touchPoint->state() == Qt::TouchPointReleased) {
291 setStatus(WaitingForTouch);
292 }
295const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
297 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
298 const QTouchEvent::TouchPoint *touchPoint = 0;
299 for (int i = 0; i < touchPoints.size(); ++i) {
300 if (touchPoints.at(i).id() == m_touchId) {
301 touchPoint = &touchPoints.at(i);
302 break;
303 }
304 }
305 return touchPoint;
308bool DirectionalDragArea::pointInsideAllowedArea() const
310 qreal dX = m_dampedPos.x() - m_startPos.x();
311 qreal dY = m_dampedPos.y() - m_startPos.y();
313 switch (m_direction) {
314 case Direction::Upwards:
315 return dY <= 0 && qFabs(dX) <= qFabs(dY) * m_wideningFactor;
316 case Direction::Downwards:
317 return dY >= 0 && qFabs(dX) <= dY * m_wideningFactor;
318 case Direction::Leftwards:
319 return dX <= 0 && qFabs(dY) <= qFabs(dX) * m_wideningFactor;
320 default: // Direction::Rightwards:
321 return dX >= 0 && qFabs(dY) <= dX * m_wideningFactor;
322 }
325bool DirectionalDragArea::movingInRightDirection() const
327 switch (m_direction) {
328 case Direction::Upwards:
329 return m_dampedPos.y() <= m_previousDampedPos.y();
330 case Direction::Downwards:
331 return m_dampedPos.y() >= m_previousDampedPos.y();
332 case Direction::Leftwards:
333 return m_dampedPos.x() <= m_previousDampedPos.x();
334 default: // Direction::Rightwards:
335 return m_dampedPos.x() >= m_previousDampedPos.x();
336 }
339bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
341 if (Direction::isHorizontal(m_direction))
342 return qFabs(point.x() - m_startPos.x()) > m_distanceThreshold;
343 else
344 return qFabs(point.y() - m_startPos.y()) > m_distanceThreshold;
347void DirectionalDragArea::checkSpeed()
349 if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
350 qreal speed = qFabs(m_velocityCalculator->calculate());
351 qreal minSpeedMsecs = m_minSpeed / 1000.0;
353 if (speed < minSpeedMsecs) {
354 setStatus(Rejected);
355 }
356 }
358 if (m_velocityCalculator->numSamples() == m_numSamplesOnLastSpeedCheck) {
359 m_silenceTime += m_recognitionTimer->interval();
361 if (m_silenceTime > m_maxSilenceTime) {
362 setStatus(Rejected);
363 }
364 } else {
365 m_silenceTime = 0;
366 }
367 m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
370void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
372 if (newStatus == m_status)
373 return;
375 DirectionalDragArea::Status oldStatus = m_status;
377 if (oldStatus == Undecided) {
378 m_recognitionTimer->stop();
379 }
381 m_status = newStatus;
382 Q_EMIT statusChanged(m_status);
384 switch (newStatus) {
385 case WaitingForTouch:
386 if (oldStatus != DirectionalDragArea::Rejected) {
387 Q_EMIT draggingChanged(false);
388 }
389 break;
390 case Undecided:
391 m_recognitionTimer->start();
392 Q_EMIT draggingChanged(true);
393 break;
394 case Recognized:
395 if (oldStatus == WaitingForTouch)
396 Q_EMIT draggingChanged(true);
397 break;
398 case Rejected:
399 Q_EMIT draggingChanged(false);
400 break;
401 default:
402 // no-op
403 break;
404 }
407void DirectionalDragArea::setPreviousPos(const QPointF &point)
409 Q_ASSERT(m_status != Rejected);
411 bool xChanged = m_previousPos.x() != point.x();
412 bool yChanged = m_previousPos.y() != point.y();
414 m_previousPos = point;
416 if (xChanged) {
417 Q_EMIT touchXChanged(point.x());
418 if (Direction::isHorizontal(m_direction))
419 Q_EMIT distanceChanged(distance());
420 }
422 if (yChanged) {
423 Q_EMIT touchYChanged(point.y());
424 if (Direction::isVertical(m_direction))
425 Q_EMIT distanceChanged(distance());
426 }
429void DirectionalDragArea::setPreviousScenePos(const QPointF &point)
431 Q_ASSERT(m_status != Rejected);
433 bool xChanged = m_previousScenePos.x() != point.x();
434 bool yChanged = m_previousScenePos.y() != point.y();
436 m_previousScenePos = point;
438 if (xChanged) {
439 Q_EMIT touchSceneXChanged(point.x());
440 if (Direction::isHorizontal(m_direction))
441 Q_EMIT sceneDistanceChanged(sceneDistance());
442 }
444 if (yChanged) {
445 Q_EMIT touchSceneYChanged(point.y());
446 if (Direction::isVertical(m_direction))
447 Q_EMIT sceneDistanceChanged(sceneDistance());
448 }
451void DirectionalDragArea::updateVelocityCalculator(const QPointF &point)
453 if (Direction::isHorizontal(m_direction)) {
454 m_velocityCalculator->setTrackedPosition(point.x());
455 } else {
456 m_velocityCalculator->setTrackedPosition(point.y());
457 }
460// Because we are defining a new QObject-based class (RecognitionTimer) here.
461#include "DirectionalDragArea.moc"