/* * Copyright (C) 2013-2014 Canonical, Ltd. * * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; version 3. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ #define ACTIVETOUCHESINFO_DEBUG 0 #define DIRECTIONALDRAGAREA_DEBUG 0 #include "DirectionalDragArea.h" #include #include #include #pragma GCC diagnostic push #pragma GCC diagnostic ignored "-pedantic" #include #pragma GCC diagnostic pop // local #include "TouchOwnershipEvent.h" #include "TouchRegistry.h" #include "UnownedTouchEvent.h" using namespace UbuntuGestures; #if DIRECTIONALDRAGAREA_DEBUG #define ddaDebug(params) qDebug().nospace() << "[DDA(" << qPrintable(objectName()) << ")] " << params #include "DebugHelpers.h" namespace { const char *statusToString(DirectionalDragArea::Status status) { if (status == DirectionalDragArea::WaitingForTouch) { return "WaitingForTouch"; } else if (status == DirectionalDragArea::Undecided) { return "Undecided"; } else { return "Recognized"; } } } // namespace { #else // DIRECTIONALDRAGAREA_DEBUG #define ddaDebug(params) ((void)0) #endif // DIRECTIONALDRAGAREA_DEBUG DirectionalDragArea::DirectionalDragArea(QQuickItem *parent) : QQuickItem(parent) , m_status(WaitingForTouch) , m_sceneDistance(0) , m_touchId(-1) , m_direction(Direction::Rightwards) , m_wideningAngle(0) , m_wideningFactor(0) , m_distanceThreshold(0) , m_distanceThresholdSquared(0.) , m_minSpeed(0) , m_maxSilenceTime(200) , m_silenceTime(0) , m_compositionTime(60) , m_numSamplesOnLastSpeedCheck(0) , m_recognitionTimer(0) , m_velocityCalculator(0) , m_timeSource(new RealTimeSource) , m_activeTouches(m_timeSource) { setRecognitionTimer(new Timer(this)); m_recognitionTimer->setInterval(60); m_recognitionTimer->setSingleShot(false); m_velocityCalculator = new AxisVelocityCalculator(this); connect(this, &QQuickItem::enabledChanged, this, &DirectionalDragArea::giveUpIfDisabledOrInvisible); connect(this, &QQuickItem::visibleChanged, this, &DirectionalDragArea::giveUpIfDisabledOrInvisible); } Direction::Type DirectionalDragArea::direction() const { return m_direction; } void DirectionalDragArea::setDirection(Direction::Type direction) { if (direction != m_direction) { m_direction = direction; Q_EMIT directionChanged(m_direction); } } void DirectionalDragArea::setMaxDeviation(qreal value) { if (m_dampedScenePos.maxDelta() != value) { m_dampedScenePos.setMaxDelta(value); Q_EMIT maxDeviationChanged(value); } } qreal DirectionalDragArea::wideningAngle() const { return m_wideningAngle; } void DirectionalDragArea::setWideningAngle(qreal angle) { if (angle == m_wideningAngle) return; m_wideningAngle = angle; // wideningFactor = pow(cosine(angle), 2) { qreal angleRadians = angle * M_PI / 180.0; m_wideningFactor = qCos(angleRadians); m_wideningFactor = m_wideningFactor * m_wideningFactor; } Q_EMIT wideningAngleChanged(angle); } void DirectionalDragArea::setDistanceThreshold(qreal value) { if (m_distanceThreshold != value) { m_distanceThreshold = value; m_distanceThresholdSquared = m_distanceThreshold * m_distanceThreshold; Q_EMIT distanceThresholdChanged(value); } } void DirectionalDragArea::setMinSpeed(qreal value) { if (m_minSpeed != value) { m_minSpeed = value; Q_EMIT minSpeedChanged(value); } } void DirectionalDragArea::setMaxSilenceTime(int value) { if (m_maxSilenceTime != value) { m_maxSilenceTime = value; Q_EMIT maxSilenceTimeChanged(value); } } void DirectionalDragArea::setCompositionTime(int value) { if (m_compositionTime != value) { m_compositionTime = value; Q_EMIT compositionTimeChanged(value); } } void DirectionalDragArea::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer) { int interval = 0; bool timerWasRunning = false; bool wasSingleShot = false; // can be null when called from the constructor if (m_recognitionTimer) { interval = m_recognitionTimer->interval(); timerWasRunning = m_recognitionTimer->isRunning(); if (m_recognitionTimer->parent() == this) { delete m_recognitionTimer; } } m_recognitionTimer = timer; timer->setInterval(interval); timer->setSingleShot(wasSingleShot); connect(timer, &UbuntuGestures::AbstractTimer::timeout, this, &DirectionalDragArea::checkSpeed); if (timerWasRunning) { m_recognitionTimer->start(); } } void DirectionalDragArea::setTimeSource(const SharedTimeSource &timeSource) { m_timeSource = timeSource; m_velocityCalculator->setTimeSource(timeSource); m_activeTouches.m_timeSource = timeSource; } qreal DirectionalDragArea::distance() const { if (Direction::isHorizontal(m_direction)) { return m_previousPos.x() - m_startPos.x(); } else { return m_previousPos.y() - m_startPos.y(); } } void DirectionalDragArea::updateSceneDistance() { QPointF totalMovement = m_previousScenePos - m_startScenePos; m_sceneDistance = projectOntoDirectionVector(totalMovement); } qreal DirectionalDragArea::sceneDistance() const { return m_sceneDistance; } qreal DirectionalDragArea::touchX() const { return m_previousPos.x(); } qreal DirectionalDragArea::touchY() const { return m_previousPos.y(); } qreal DirectionalDragArea::touchSceneX() const { return m_previousScenePos.x(); } qreal DirectionalDragArea::touchSceneY() const { return m_previousScenePos.y(); } bool DirectionalDragArea::event(QEvent *event) { if (event->type() == TouchOwnershipEvent::touchOwnershipEventType()) { touchOwnershipEvent(static_cast(event)); return true; } else if (event->type() == UnownedTouchEvent::unownedTouchEventType()) { unownedTouchEvent(static_cast(event)); return true; } else { return QQuickItem::event(event); } } void DirectionalDragArea::touchOwnershipEvent(TouchOwnershipEvent *event) { if (event->gained()) { QVector ids; ids.append(event->touchId()); ddaDebug("grabbing touch"); grabTouchPoints(ids); // Work around for Qt bug. If we grab a touch that is being used for mouse pointer // emulation it will cause the emulation logic to go nuts. // Thus we have to also grab the mouse in this case. // // The fix for this bug has landed in Qt 5.4 (https://codereview.qt-project.org/96887) // TODO: Remove this workaround once we start using Qt 5.4 if (window()) { QQuickWindowPrivate *windowPrivate = QQuickWindowPrivate::get(window()); if (windowPrivate->touchMouseId == event->touchId() && window()->mouseGrabberItem()) { ddaDebug("removing mouse grabber"); window()->mouseGrabberItem()->ungrabMouse(); } } } else { // We still wanna know when it ends for keeping the composition time window up-to-date TouchRegistry::instance()->addTouchWatcher(m_touchId, this); setStatus(WaitingForTouch); } } void DirectionalDragArea::unownedTouchEvent(UnownedTouchEvent *unownedTouchEvent) { QTouchEvent *event = unownedTouchEvent->touchEvent(); Q_ASSERT(!event->touchPointStates().testFlag(Qt::TouchPointPressed)); ddaDebug("Unowned " << m_timeSource->msecsSinceReference() << " " << qPrintable(touchEventToString(event))); switch (m_status) { case WaitingForTouch: // do nothing break; case Undecided: Q_ASSERT(isEnabled() && isVisible()); unownedTouchEvent_undecided(unownedTouchEvent); break; default: // Recognized: // do nothing break; } m_activeTouches.update(event); } void DirectionalDragArea::unownedTouchEvent_undecided(UnownedTouchEvent *unownedTouchEvent) { const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(unownedTouchEvent->touchEvent()); if (!touchPoint) { qCritical() << "DirectionalDragArea[status=Undecided]: touch " << m_touchId << "missing from UnownedTouchEvent without first reaching state Qt::TouchPointReleased. " "Considering it as released."; TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); setStatus(WaitingForTouch); return; } const QPointF &touchScenePos = touchPoint->scenePos(); if (touchPoint->state() == Qt::TouchPointReleased) { // touch has ended before recognition concluded ddaDebug("Touch has ended before recognition concluded"); TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); emitSignalIfTapped(); setStatus(WaitingForTouch); return; } m_previousDampedScenePos.setX(m_dampedScenePos.x()); m_previousDampedScenePos.setY(m_dampedScenePos.y()); m_dampedScenePos.update(touchScenePos); updateVelocityCalculator(touchScenePos); if (!pointInsideAllowedArea()) { ddaDebug("Rejecting gesture because touch point is outside allowed area."); TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); // We still wanna know when it ends for keeping the composition time window up-to-date TouchRegistry::instance()->addTouchWatcher(m_touchId, this); setStatus(WaitingForTouch); return; } if (!movingInRightDirection()) { ddaDebug("Rejecting gesture because touch point is moving in the wrong direction."); TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); // We still wanna know when it ends for keeping the composition time window up-to-date TouchRegistry::instance()->addTouchWatcher(m_touchId, this); setStatus(WaitingForTouch); return; } setPreviousPos(touchPoint->pos()); setPreviousScenePos(touchScenePos); if (isWithinTouchCompositionWindow()) { // There's still time for some new touch to appear and ruin our party as it would be combined // with our m_touchId one and therefore deny the possibility of a single-finger gesture. ddaDebug("Sill within composition window. Let's wait more."); return; } if (movedFarEnough(touchScenePos)) { TouchRegistry::instance()->requestTouchOwnership(m_touchId, this); setStatus(Recognized); } else { ddaDebug("Didn't move far enough yet. Let's wait more."); } } void DirectionalDragArea::touchEvent(QTouchEvent *event) { // TODO: Consider when more than one touch starts in the same event (although it's not possible // with Mir's android-input). Have to track them all. Consider it a plus/bonus. ddaDebug(m_timeSource->msecsSinceReference() << " " << qPrintable(touchEventToString(event))); if (!isEnabled() || !isVisible()) { QQuickItem::touchEvent(event); return; } switch (m_status) { case WaitingForTouch: touchEvent_absent(event); break; case Undecided: touchEvent_undecided(event); break; default: // Recognized: touchEvent_recognized(event); break; } m_activeTouches.update(event); } void DirectionalDragArea::touchEvent_absent(QTouchEvent *event) { // TODO: accept/reject is for the whole event, not per touch id. See how that affects us. if (!event->touchPointStates().testFlag(Qt::TouchPointPressed)) { // Nothing to see here. No touch starting in this event. return; } // to be proven wrong, if that's the case bool allGood = true; if (isWithinTouchCompositionWindow()) { // too close to the last touch start. So we consider them as starting roughly at the same time. // Can't be a single-touch gesture. ddaDebug("A new touch point came in but we're still within time composition window. Ignoring it."); allGood = false; } const QList &touchPoints = event->touchPoints(); const QTouchEvent::TouchPoint *newTouchPoint = nullptr; for (int i = 0; i < touchPoints.count() && allGood; ++i) { const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i); if (touchPoint.state() == Qt::TouchPointPressed) { if (newTouchPoint) { // more than one touch starting in this QTouchEvent. Can't be a single-touch gesture allGood = false; } else { // that's our candidate m_touchId = touchPoint.id(); newTouchPoint = &touchPoint; } } } if (allGood) { Q_ASSERT(newTouchPoint); m_startPos = newTouchPoint->pos(); m_startScenePos = newTouchPoint->scenePos(); m_touchId = newTouchPoint->id(); m_dampedScenePos.reset(m_startScenePos); m_velocityCalculator->setTrackedPosition(0.); m_velocityCalculator->reset(); m_numSamplesOnLastSpeedCheck = 0; m_silenceTime = 0; setPreviousPos(m_startPos); setPreviousScenePos(m_startScenePos); updateSceneDirectionVector(); if (recognitionIsDisabled()) { // Behave like a dumb TouchArea ddaDebug("Gesture recognition is disabled. Requesting touch ownership immediately."); TouchRegistry::instance()->requestTouchOwnership(m_touchId, this); setStatus(Recognized); event->accept(); } else { // just monitor the touch points for now. TouchRegistry::instance()->addCandidateOwnerForTouch(m_touchId, this); setStatus(Undecided); // Let the item below have it. We will monitor it and grab it later if a gesture // gets recognized. event->ignore(); } } else { watchPressedTouchPoints(touchPoints); event->ignore(); } } void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event) { Q_ASSERT(event->type() == QEvent::TouchBegin); Q_ASSERT(fetchTargetTouchPoint(event) == nullptr); // We're not interested in new touch points. We already have our candidate (m_touchId). // But we do want to know when those new touches end for keeping the composition time // window up-to-date event->ignore(); watchPressedTouchPoints(event->touchPoints()); if (event->touchPointStates().testFlag(Qt::TouchPointPressed) && isWithinTouchCompositionWindow()) { // multi-finger drags are not accepted ddaDebug("Multi-finger drags are not accepted"); TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); // We still wanna know when it ends for keeping the composition time window up-to-date TouchRegistry::instance()->addTouchWatcher(m_touchId, this); setStatus(WaitingForTouch); } } void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event) { const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event); if (!touchPoint) { qCritical() << "DirectionalDragArea[status=Recognized]: touch " << m_touchId << "missing from QTouchEvent without first reaching state Qt::TouchPointReleased. " "Considering it as released."; setStatus(WaitingForTouch); } else { setPreviousPos(touchPoint->pos()); setPreviousScenePos(touchPoint->scenePos()); if (touchPoint->state() == Qt::TouchPointReleased) { emitSignalIfTapped(); setStatus(WaitingForTouch); } } } void DirectionalDragArea::watchPressedTouchPoints(const QList &touchPoints) { for (int i = 0; i < touchPoints.count(); ++i) { const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i); if (touchPoint.state() == Qt::TouchPointPressed) { TouchRegistry::instance()->addTouchWatcher(touchPoint.id(), this); } } } bool DirectionalDragArea::recognitionIsDisabled() const { return distanceThreshold() <= 0 && compositionTime() <= 0; } void DirectionalDragArea::emitSignalIfTapped() { qint64 touchDuration = m_timeSource->msecsSinceReference() - m_activeTouches.touchStartTime(m_touchId); if (touchDuration <= maxTapDuration()) { Q_EMIT tapped(); } } const QTouchEvent::TouchPoint *DirectionalDragArea::fetchTargetTouchPoint(QTouchEvent *event) { const QList &touchPoints = event->touchPoints(); const QTouchEvent::TouchPoint *touchPoint = 0; for (int i = 0; i < touchPoints.size(); ++i) { if (touchPoints.at(i).id() == m_touchId) { touchPoint = &touchPoints.at(i); break; } } return touchPoint; } bool DirectionalDragArea::pointInsideAllowedArea() const { // NB: Using squared values to avoid computing the square root to find // the length totalMovement QPointF totalMovement(m_dampedScenePos.x() - m_startScenePos.x(), m_dampedScenePos.y() - m_startScenePos.y()); qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() + totalMovement.y() * totalMovement.y(); if (squaredTotalMovSize == 0.) { // didn't move return true; } qreal projectedMovement = projectOntoDirectionVector(totalMovement); qreal cosineAngleSquared = (projectedMovement * projectedMovement) / squaredTotalMovSize; // Same as: // angle_between_movement_vector_and_gesture_direction_vector <= widening_angle return cosineAngleSquared >= m_wideningFactor; } bool DirectionalDragArea::movingInRightDirection() const { if (m_direction == Direction::Horizontal) { return true; } else { QPointF movementVector(m_dampedScenePos.x() - m_previousDampedScenePos.x(), m_dampedScenePos.y() - m_previousDampedScenePos.y()); qreal scalarProjection = projectOntoDirectionVector(movementVector); return scalarProjection >= 0.; } } bool DirectionalDragArea::movedFarEnough(const QPointF &point) const { if (m_distanceThreshold <= 0.) { // distance threshold check is disabled return true; } else { QPointF totalMovement(point.x() - m_startScenePos.x(), point.y() - m_startScenePos.y()); qreal squaredTotalMovSize = totalMovement.x() * totalMovement.x() + totalMovement.y() * totalMovement.y(); return squaredTotalMovSize > m_distanceThresholdSquared; } } void DirectionalDragArea::checkSpeed() { Q_ASSERT(m_status == Undecided); if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) { qreal speed = qFabs(m_velocityCalculator->calculate()); qreal minSpeedMsecs = m_minSpeed / 1000.0; if (speed < minSpeedMsecs) { ddaDebug("Rejecting gesture because it's below minimum speed."); TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); TouchRegistry::instance()->addTouchWatcher(m_touchId, this); setStatus(WaitingForTouch); } } if (m_velocityCalculator->numSamples() == m_numSamplesOnLastSpeedCheck) { m_silenceTime += m_recognitionTimer->interval(); if (m_silenceTime > m_maxSilenceTime) { ddaDebug("Rejecting gesture because its silence time has been exceeded."); TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); TouchRegistry::instance()->addTouchWatcher(m_touchId, this); setStatus(WaitingForTouch); } } else { m_silenceTime = 0; } m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples(); } void DirectionalDragArea::giveUpIfDisabledOrInvisible() { if (!isEnabled() || !isVisible()) { if (m_status == Undecided) { TouchRegistry::instance()->removeCandidateOwnerForTouch(m_touchId, this); // We still wanna know when it ends for keeping the composition time window up-to-date TouchRegistry::instance()->addTouchWatcher(m_touchId, this); } if (m_status != WaitingForTouch) { ddaDebug("Resetting status because got disabled or made invisible"); setStatus(WaitingForTouch); } } } void DirectionalDragArea::setStatus(DirectionalDragArea::Status newStatus) { if (newStatus == m_status) return; DirectionalDragArea::Status oldStatus = m_status; if (oldStatus == Undecided) { m_recognitionTimer->stop(); } m_status = newStatus; Q_EMIT statusChanged(m_status); ddaDebug(statusToString(oldStatus) << " -> " << statusToString(newStatus)); switch (newStatus) { case WaitingForTouch: Q_EMIT draggingChanged(false); break; case Undecided: m_recognitionTimer->start(); Q_EMIT draggingChanged(true); break; case Recognized: if (oldStatus == WaitingForTouch) Q_EMIT draggingChanged(true); break; default: // no-op break; } } void DirectionalDragArea::setPreviousPos(const QPointF &point) { bool xChanged = m_previousPos.x() != point.x(); bool yChanged = m_previousPos.y() != point.y(); m_previousPos = point; if (xChanged) { Q_EMIT touchXChanged(point.x()); if (Direction::isHorizontal(m_direction)) Q_EMIT distanceChanged(distance()); } if (yChanged) { Q_EMIT touchYChanged(point.y()); if (Direction::isVertical(m_direction)) Q_EMIT distanceChanged(distance()); } } void DirectionalDragArea::setPreviousScenePos(const QPointF &point) { bool xChanged = m_previousScenePos.x() != point.x(); bool yChanged = m_previousScenePos.y() != point.y(); if (!xChanged && !yChanged) return; qreal oldSceneDistance = sceneDistance(); m_previousScenePos = point; updateSceneDistance(); if (oldSceneDistance != sceneDistance()) { Q_EMIT sceneDistanceChanged(sceneDistance()); } if (xChanged) { Q_EMIT touchSceneXChanged(point.x()); } if (yChanged) { Q_EMIT touchSceneYChanged(point.y()); } } void DirectionalDragArea::updateVelocityCalculator(const QPointF &scenePos) { QPointF totalSceneMovement = scenePos - m_startScenePos; qreal scalarProjection = projectOntoDirectionVector(totalSceneMovement); m_velocityCalculator->setTrackedPosition(scalarProjection); } bool DirectionalDragArea::isWithinTouchCompositionWindow() { return compositionTime() > 0 && !m_activeTouches.isEmpty() && m_timeSource->msecsSinceReference() <= m_activeTouches.mostRecentStartTime() + (qint64)compositionTime(); } //************************** ActiveTouchesInfo ************************** DirectionalDragArea::ActiveTouchesInfo::ActiveTouchesInfo(const SharedTimeSource &timeSource) : m_timeSource(timeSource) { } void DirectionalDragArea::ActiveTouchesInfo::update(QTouchEvent *event) { if (!(event->touchPointStates() & (Qt::TouchPointPressed | Qt::TouchPointReleased))) { // nothing to update #if ACTIVETOUCHESINFO_DEBUG qDebug("[DDA::ActiveTouchesInfo] Nothing to Update"); #endif return; } const QList &touchPoints = event->touchPoints(); for (int i = 0; i < touchPoints.count(); ++i) { const QTouchEvent::TouchPoint &touchPoint = touchPoints.at(i); if (touchPoint.state() == Qt::TouchPointPressed) { addTouchPoint(touchPoint.id()); } else if (touchPoint.state() == Qt::TouchPointReleased) { removeTouchPoint(touchPoint.id()); } } } #if ACTIVETOUCHESINFO_DEBUG QString DirectionalDragArea::ActiveTouchesInfo::toString() { QString string = "("; { QTextStream stream(&string); m_touchInfoPool.forEach([&](Pool::Iterator &touchInfo) { stream << "(id=" << touchInfo->id << ",startTime=" << touchInfo->startTime << ")"; return true; }); } string.append(")"); return string; } #endif // ACTIVETOUCHESINFO_DEBUG void DirectionalDragArea::ActiveTouchesInfo::addTouchPoint(int touchId) { ActiveTouchInfo &activeTouchInfo = m_touchInfoPool.getEmptySlot(); activeTouchInfo.id = touchId; activeTouchInfo.startTime = m_timeSource->msecsSinceReference(); #if ACTIVETOUCHESINFO_DEBUG qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString()); #endif } qint64 DirectionalDragArea::ActiveTouchesInfo::touchStartTime(int touchId) { qint64 result = -1; m_touchInfoPool.forEach([&](Pool::Iterator &touchInfo) { if (touchId == touchInfo->id) { result = touchInfo->startTime; return false; } else { return true; } }); Q_ASSERT(result != -1); return result; } void DirectionalDragArea::ActiveTouchesInfo::removeTouchPoint(int touchId) { m_touchInfoPool.forEach([&](Pool::Iterator &touchInfo) { if (touchId == touchInfo->id) { m_touchInfoPool.freeSlot(touchInfo); return false; } else { return true; } }); #if ACTIVETOUCHESINFO_DEBUG qDebug() << "[DDA::ActiveTouchesInfo]" << qPrintable(toString()); #endif } qint64 DirectionalDragArea::ActiveTouchesInfo::mostRecentStartTime() { Q_ASSERT(!m_touchInfoPool.isEmpty()); qint64 highestStartTime = -1; m_touchInfoPool.forEach([&](Pool::Iterator &activeTouchInfo) { if (activeTouchInfo->startTime > highestStartTime) { highestStartTime = activeTouchInfo->startTime; } return true; }); return highestStartTime; } void DirectionalDragArea::updateSceneDirectionVector() { QPointF localOrigin(0., 0.); QPointF localDirection; switch (m_direction) { case Direction::Upwards: localDirection.rx() = 0.; localDirection.ry() = -1.; break; case Direction::Downwards: localDirection.rx() = 0.; localDirection.ry() = 1; break; case Direction::Leftwards: localDirection.rx() = -1.; localDirection.ry() = 0.; break; default: // Direction::Rightwards || Direction.Horizontal localDirection.rx() = 1.; localDirection.ry() = 0.; break; } QPointF sceneOrigin = mapToScene(localOrigin); QPointF sceneDirection = mapToScene(localDirection); m_sceneDirectionVector = sceneDirection - sceneOrigin; } qreal DirectionalDragArea::projectOntoDirectionVector(const QPointF &sceneVector) const { // same as dot product as m_sceneDirectionVector is a unit vector return sceneVector.x() * m_sceneDirectionVector.x() + sceneVector.y() * m_sceneDirectionVector.y(); }