on 14ca310 help sign in
unity8/plugins/Ubuntu/Gestures/DirectionalDragArea.cpp
767 lines 23 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_sceneDistance(0)
113 , m_touchId(-1)
114 , m_direction(Direction::Rightwards)
115 , m_wideningAngle(0)
116 , m_wideningFactor(0)
117 , m_distanceThreshold(0)
118 , m_distanceThresholdSquared(0.)
119 , m_minSpeed(0)
120 , m_maxSilenceTime(200)
121 , m_silenceTime(0)
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);
137Direction::Type DirectionalDragArea::direction() const
139 return m_direction;
142void DirectionalDragArea::setDirection(Direction::Type direction)
144 if (direction != m_direction) {
145 m_direction = direction;
146 Q_EMIT directionChanged(m_direction);
147 }
150void DirectionalDragArea::setMaxDeviation(qreal value)
152 if (m_dampedScenePos.maxDelta() != value) {
153 m_dampedScenePos.setMaxDelta(value);
154 Q_EMIT maxDeviationChanged(value);
155 }
158qreal DirectionalDragArea::wideningAngle() const
160 return m_wideningAngle;
163void DirectionalDragArea::setWideningAngle(qreal angle)
165 if (angle == m_wideningAngle)
166 return;
168 m_wideningAngle = angle;
170 // wideningFactor = pow(cosine(angle), 2)
171 {
172 qreal angleRadians = angle * M_PI / 180.0;
173 m_wideningFactor = qCos(angleRadians);
174 m_wideningFactor = m_wideningFactor * m_wideningFactor;
175 }
177 Q_EMIT wideningAngleChanged(angle);
180void 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);
186 }
189void DirectionalDragArea::setMinSpeed(qreal value)
191 if (m_minSpeed != value) {
192 m_minSpeed = value;
193 Q_EMIT minSpeedChanged(value);
194 }
197void DirectionalDragArea::setMaxSilenceTime(int value)
199 if (m_maxSilenceTime != value) {
200 m_maxSilenceTime = value;
201 Q_EMIT maxSilenceTimeChanged(value);
202 }
205void DirectionalDragArea::setCompositionTime(int value)
207 if (m_compositionTime != value) {
208 m_compositionTime = value;
209 Q_EMIT compositionTimeChanged(value);
210 }
213void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer)
215 int interval = 0;
216 bool timerWasRunning = false;
218 // can be null when called from the constructor
219 if (m_recognitionTimer) {
220 interval = m_recognitionTimer->interval();
221 timerWasRunning = m_recognitionTimer->isRunning();
222 if (m_recognitionTimer->parent() == this) {
223 delete m_recognitionTimer;
224 }
225 }
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();
233 }
236void DirectionalDragArea::setTimeSource(const SharedTimeSource &timeSource)
238 m_timeSource = timeSource;
239 m_velocityCalculator->setTimeSource(timeSource);
240 m_activeTouches.m_timeSource = timeSource;
243qreal DirectionalDragArea::distance() const
245 if (Direction::isHorizontal(m_direction)) {
246 return m_previousPos.x() - m_startPos.x();
247 } else {
248 return m_previousPos.y() - m_startPos.y();
249 }
252void DirectionalDragArea::updateSceneDistance()
254 QPointF totalMovement = m_previousScenePos - m_startScenePos;
255 m_sceneDistance = projectOntoDirectionVector(totalMovement);
258qreal DirectionalDragArea::sceneDistance() const
260 return m_sceneDistance;
263qreal DirectionalDragArea::touchX() const
265 return m_previousPos.x();
268qreal DirectionalDragArea::touchY() const
270 return m_previousPos.y();
273qreal DirectionalDragArea::touchSceneX() const
275 return m_previousScenePos.x();
278qreal DirectionalDragArea::touchSceneY() const
280 return m_previousScenePos.y();
283void DirectionalDragArea::touchEvent(QTouchEvent *event)
285 #if DIRECTIONALDRAGAREA_DEBUG
286 // TODO Consider using qCDebug() when available (Qt 5.2)
287 qDebug() << "[DDA]" << m_timeSource->msecsSinceReference()
288 << qPrintable(touchEventToString(event));
289 #endif
291 if (!isEnabled() || !isVisible()) {
292 QQuickItem::touchEvent(event);
293 return;
294 }
296 switch (m_status) {
297 case WaitingForTouch:
298 touchEvent_absent(event);
299 break;
300 case Undecided:
301 touchEvent_undecided(event);
302 break;
303 default: // Recognized:
304 touchEvent_recognized(event);
305 break;
306 }
308 m_activeTouches.update(event);
311void DirectionalDragArea::touchEvent_absent(QTouchEvent *event)
313 if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) {
314 // Nothing to see here. No touch starting in this event.
315 return;
316 }
318 if (isWithinTouchCompositionWindow()) {
319 // too close to the last touch start. So we consider them as starting roughly at the same time.
320 // Can't be a single-touch gesture.
321 #if DIRECTIONALDRAGAREA_DEBUG
322 qDebug("[DDA] A new touch point came in but we're still within time composition window. Ignoring it.");
323 #endif
324 return;
325 }
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) {
333 if (newTouchPoint) {
334 // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture
335 return;
336 } else {
337 // that's our candidate
338 m_touchId = touchPoint.id();
339 newTouchPoint = &touchPoint;
340 }
341 }
342 }
344 Q_ASSERT(newTouchPoint);
346 // If we have made this far, we are good to go to the next status.
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;
355 m_silenceTime = 0;
356 setPreviousPos(m_startPos);
357 setPreviousScenePos(m_startScenePos);
358 updateSceneDirectionVector();
360 setStatus(Undecided);
363void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event)
365 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
367 if (!touchPoint) {
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);
372 return;
373 }
375 const QPointF &touchScenePos = touchPoint->scenePos();
377 if (touchPoint->state() == Qt::TouchPointReleased) {
378 // touch has ended before recognition concluded
379 DDA_DEBUG("Touch has ended before recognition concluded");
380 setStatus(WaitingForTouch);
381 return;
382 }
384 if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) {
385 // multi-finger drags are not accepted
386 DDA_DEBUG("Multi-finger drags are not accepted");
387 setStatus(WaitingForTouch);
388 return;
389 }
391 m_previousDampedScenePos.setX(m_dampedScenePos.x());
392 m_previousDampedScenePos.setY(m_dampedScenePos.y());
393 m_dampedScenePos.update(touchScenePos);
394 updateVelocityCalculator(touchScenePos);
396 if (!pointInsideAllowedArea()) {
397 DDA_DEBUG("Rejecting gesture because touch point is outside allowed area.");
398 setStatus(WaitingForTouch);
399 return;
400 }
402 if (!movingInRightDirection()) {
403 DDA_DEBUG("Rejecting gesture becauuse touch point is moving in the wrong direction.");
404 setStatus(WaitingForTouch);
405 return;
406 }
408 setPreviousPos(touchPoint->pos());
409 setPreviousScenePos(touchScenePos);
411 if (isWithinTouchCompositionWindow()) {
412 // There's still time for some new touch to appear and ruin our party as it would be combined
413 // with our m_touchId one and therefore deny the possibility of a single-finger gesture.
414 DDA_DEBUG("Sill within composition window. Let's wait more.");
415 return;
416 }
418 if (movedFarEnough(touchScenePos)) {
419 setStatus(Recognized);
420 } else {
421 DDA_DEBUG("Didn't move far enough yet. Let's wait more.");
422 }
425void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event)
427 const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event);
429 if (!touchPoint) {
430 qCritical() << "DirectionalDragArea[status=Recognized]: touch " << m_touchId
431 << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. "
432 "Considering it as released.";
433 setStatus(WaitingForTouch);
434 } else {
435 setPreviousPos(touchPoint->pos());
436 setPreviousScenePos(touchPoint->scenePos());
438 if (touchPoint->state() == Qt::TouchPointReleased) {
439 setStatus(WaitingForTouch);
440 }
441 }
444const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event)
446 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
447 const QTouchEvent::TouchPoint *touchPoint = 0;
448 for (int i = 0; i < touchPoints.size(); ++i) {
449 if (touchPoints.at(i).id() == m_touchId) {
450 touchPoint = &touchPoints.at(i);
451 break;
452 }
453 }
454 return touchPoint;
457bool DirectionalDragArea::pointInsideAllowedArea() const
459 // NB: Using squared values to avoid computing the square root to find
460 // the length totalMovement
462 QPointF totalMovement(m_dampedScenePos.x() - m_startScenePos.x(),
463 m_dampedScenePos.y() - m_startScenePos.y());
465 qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
466 totalMovement.y() * totalMovement.y();
468 if (squaredTotalMovSize == 0.) {
469 // didn't move
470 return true;
471 }
473 qreal projectedMovement = projectOntoDirectionVector(totalMovement);
476 qreal cosineAngleSquared = (projectedMovement * projectedMovement) / squaredTotalMovSize;
478 // Same as:
479 // angle_between_movement_vector_and_gesture_direction_vector <= widening_angle
480 return cosineAngleSquared >= m_wideningFactor;
483bool DirectionalDragArea::movingInRightDirection() const
485 QPointF movementVector(m_dampedScenePos.x() - m_previousDampedScenePos.x(),
486 m_dampedScenePos.y() - m_previousDampedScenePos.y());
488 qreal scalarProjection = projectOntoDirectionVector(movementVector);
490 return scalarProjection >= 0.;
493bool DirectionalDragArea::movedFarEnough(const QPointF &point) const
495 if (m_distanceThreshold <= 0.) {
496 // distance threshold check is disabled
497 return true;
498 } else {
499 QPointF totalMovement(point.x() - m_startScenePos.x(),
500 point.y() - m_startScenePos.y());
502 qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() +
503 totalMovement.y() * totalMovement.y();
505 return squaredTotalMovSize > m_distanceThresholdSquared;
506 }
509void DirectionalDragArea::checkSpeed()
511 if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) {
512 qreal speed = qFabs(m_velocityCalculator->calculate());
513 qreal minSpeedMsecs = m_minSpeed / 1000.0;
515 if (speed < minSpeedMsecs) {
516 DDA_DEBUG("Rejecting gesture because it's below minimum speed.");
517 setStatus(WaitingForTouch);
518 }
519 }
521 if (m_velocityCalculator->numSamples() == m_numSamplesOnLastSpeedCheck) {
522 m_silenceTime += m_recognitionTimer->interval();
524 if (m_silenceTime > m_maxSilenceTime) {
525 DDA_DEBUG("Rejecting gesture because it's silence time has been exceeded.");
526 setStatus(WaitingForTouch);
527 }
528 } else {
529 m_silenceTime = 0;
530 }
531 m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples();
534void DirectionalDragArea::onEnabledChanged()
536 if (!isEnabled() && m_status != WaitingForTouch) {
537 setStatus(WaitingForTouch);
538 }
541void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus)
543 if (newStatus == m_status)
544 return;
546 DirectionalDragArea::Status oldStatus = m_status;
548 if (oldStatus == Undecided) {
549 m_recognitionTimer->stop();
550 }
552 m_status = newStatus;
553 Q_EMIT statusChanged(m_status);
555 #if DIRECTIONALDRAGAREA_DEBUG
556 qDebug() << "[DDA]" << statusToString(oldStatus) << "->" << statusToString(newStatus);
557 #endif
559 switch (newStatus) {
560 case WaitingForTouch:
561 Q_EMIT draggingChanged(false);
562 break;
563 case Undecided:
564 m_recognitionTimer->start();
565 Q_EMIT draggingChanged(true);
566 break;
567 case Recognized:
568 if (oldStatus == WaitingForTouch)
569 Q_EMIT draggingChanged(true);
570 break;
571 default:
572 // no-op
573 break;
574 }
577void DirectionalDragArea::setPreviousPos(const QPointF &point)
579 bool xChanged = m_previousPos.x() != point.x();
580 bool yChanged = m_previousPos.y() != point.y();
582 m_previousPos = point;
584 if (xChanged) {
585 Q_EMIT touchXChanged(point.x());
586 if (Direction::isHorizontal(m_direction))
587 Q_EMIT distanceChanged(distance());
588 }
590 if (yChanged) {
591 Q_EMIT touchYChanged(point.y());
592 if (Direction::isVertical(m_direction))
593 Q_EMIT distanceChanged(distance());
594 }
597void DirectionalDragArea::setPreviousScenePos(const QPointF &point)
599 bool xChanged = m_previousScenePos.x() != point.x();
600 bool yChanged = m_previousScenePos.y() != point.y();
602 if (!xChanged && !yChanged)
603 return;
605 qreal oldSceneDistance = sceneDistance();
606 m_previousScenePos = point;
607 updateSceneDistance();
609 if (oldSceneDistance != sceneDistance()) {
610 Q_EMIT sceneDistanceChanged(sceneDistance());
611 }
613 if (xChanged) {
614 Q_EMIT touchSceneXChanged(point.x());
615 }
617 if (yChanged) {
618 Q_EMIT touchSceneYChanged(point.y());
619 }
622void DirectionalDragArea::updateVelocityCalculator(const QPointF &scenePos)
624 QPointF totalSceneMovement = scenePos - m_startScenePos;
626 qreal scalarProjection = projectOntoDirectionVector(totalSceneMovement);
628 m_velocityCalculator->setTrackedPosition(scalarProjection);
631bool DirectionalDragArea::isWithinTouchCompositionWindow()
633 return !m_activeTouches.isEmpty() &&
634 m_timeSource->msecsSinceReference() <=
635 m_activeTouches.mostRecentStartTime() + (qint64)compositionTime();
638//************************** ActiveTouchesInfo **************************
640DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(const SharedTimeSource &timeSource)
641 : m_timeSource(timeSource), m_lastUsedIndex(-1)
643 // Estimate of the maximum number of active touches we might reach.
644 // Not a problem if it ends up being an underestimate as this is just
645 // an optimization.
646 m_vector.resize(3);
649void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event)
651 if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) {
652 // nothing to update
653 return;
654 }
656 const QList<QTouchEvent::TouchPoint> &touchPoints = event->touchPoints();
657 for (int i = 0; i < touchPoints.count(); ++i) {
658 const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i);
659 if (touchPoint.state() == Qt::TouchPointPressed) {
660 addTouchPoint(touchPoint);
661 } else if (touchPoint.state() == Qt::TouchPointReleased) {
662 removeTouchPoint(touchPoint);
663 }
664 }
667void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
669 ActiveTouchInfo &activeTouchInfo = getEmptySlot();
670 activeTouchInfo.id = touchPoint.id();
671 activeTouchInfo.startTime = m_timeSource->msecsSinceReference();
674void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(const QTouchEvent::TouchPoint &touchPoint)
676 for (int i = 0; i <= m_lastUsedIndex; ++i) {
677 if (touchPoint.id() == m_vector.at(i).id) {
678 freeSlot(i);
679 return;
680 }
681 }
682 Q_ASSERT(false); // shouldn't reach this point
685DirectionalDragArea::ActiveTouchInfo &DirectionalDragArea::ActiveTouchesInfo::getEmptySlot()
687 Q_ASSERT(m_lastUsedIndex < m_vector.size());
689 // Look for an in-between vacancy first
690 for (int i = 0; i < m_lastUsedIndex; ++i) {
691 ActiveTouchInfo &activeTouchInfo = m_vector[i];
692 if (!activeTouchInfo.isValid()) {
693 return activeTouchInfo;
694 }
695 }
697 ++m_lastUsedIndex;
698 if (m_lastUsedIndex >= m_vector.size()) {
699 m_vector.resize(m_lastUsedIndex + 1);
700 }
702 return m_vector[m_lastUsedIndex];
705void DirectionalDragArea::ActiveTouchesInfo::freeSlot(int index)
707 m_vector[index].reset();
708 if (index == m_lastUsedIndex) {
709 do {
710 --m_lastUsedIndex;
711 } while (m_lastUsedIndex >= 0 && !m_vector.at(m_lastUsedIndex).isValid());
712 }
715qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime()
717 Q_ASSERT(m_lastUsedIndex >= 0);
719 qint64 highestStartTime = m_vector.at(0).startTime;
720 int i = 1;
721 do {
722 const ActiveTouchInfo &activeTouchInfo = m_vector.at(i);
723 if (activeTouchInfo.isValid() && activeTouchInfo.startTime > highestStartTime) {
724 highestStartTime = activeTouchInfo.startTime;
725 }
726 ++i;
727 } while (i < m_lastUsedIndex);
729 return highestStartTime;
732void DirectionalDragArea::updateSceneDirectionVector()
734 QPointF localOrigin(0., 0.);
735 QPointF localDirection;
736 switch (m_direction) {
737 case Direction::Upwards:
738 localDirection.rx() = 0.;
739 localDirection.ry() = -1.;
740 break;
741 case Direction::Downwards:
742 localDirection.rx() = 0.;
743 localDirection.ry() = 1;
744 break;
745 case Direction::Leftwards:
746 localDirection.rx() = -1.;
747 localDirection.ry() = 0.;
748 break;
749 default: // Direction::Rightwards:
750 localDirection.rx() = 1.;
751 localDirection.ry() = 0.;
752 break;
753 }
754 QPointF sceneOrigin = mapToScene(localOrigin);
755 QPointF sceneDirection = mapToScene(localDirection);
756 m_sceneDirectionVector = sceneDirection - sceneOrigin;
759qreal DirectionalDragArea::projectOntoDirectionVector(const QPointF &sceneVector) const
761 // same as dot product as m_sceneDirectionVector is a unit vector
762 return sceneVector.x() * m_sceneDirectionVector.x() +
763 sceneVector.y() * m_sceneDirectionVector.y();
766// Because we are defining a new QObject-based class (RecognitionTimer) here.
767#include "DirectionalDragArea.moc"