/* * Copyright (C) 2013 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 . */ #include "DirectionalDragArea.h" #include #include // Essentially a QTimer wrapper class RecognitionTimer : public UbuntuGestures::AbstractTimer { Q_OBJECT public: RecognitionTimer(QObject *parent) : UbuntuGestures::AbstractTimer(parent) { m_timer.setSingleShot(false); connect(&m_timer, &QTimer::timeout, this, &UbuntuGestures::AbstractTimer::timeout); } virtual int interval() const { return m_timer.interval(); } virtual void setInterval(int msecs) { m_timer.setInterval(msecs); } virtual void start() { m_timer.start(); UbuntuGestures::AbstractTimer::start(); } virtual void stop() { m_timer.stop(); UbuntuGestures::AbstractTimer::stop(); } private: QTimer m_timer; }; DirectionalDragArea::DirectionalDragArea(QQuickItem *parent) : QQuickItem(parent) , m_status(WaitingForTouch) , m_touchId(-1) , m_direction(Direction::Rightwards) , m_wideningAngle(0) , m_wideningFactor(0) , m_distanceThreshold(0) , m_minSpeed(0) , m_maxSilenceTime(200) , m_silenceTime(0) , m_numSamplesOnLastSpeedCheck(0) , m_recognitionTimer(0) , m_velocityCalculator(0) { setRecognitionTimer(new RecognitionTimer(this)); m_recognitionTimer->setInterval(60); m_velocityCalculator = new AxisVelocityCalculator(this); } 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_dampedPos.maxDelta() != value) { m_dampedPos.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; m_wideningFactor = qTan(angle * M_PI / 180.0); Q_EMIT wideningAngleChanged(angle); } void DirectionalDragArea::setDistanceThreshold(qreal value) { if (m_distanceThreshold != value) { m_distanceThreshold = value; 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::setRecognitionTimer(UbuntuGestures::AbstractTimer *timer) { int interval = 0; bool timerWasRunning = 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); connect(timer, &UbuntuGestures::AbstractTimer::timeout, this, &DirectionalDragArea::checkSpeed); if (timerWasRunning) { m_recognitionTimer->start(); } } void DirectionalDragArea::setTimeSource(UbuntuGestures::TimeSource *timeSource) { m_velocityCalculator->setTimeSource(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(); } } qreal DirectionalDragArea::sceneDistance() const { if (Direction::isHorizontal(m_direction)) { return m_previousScenePos.x() - m_startScenePos.x(); } else { return m_previousScenePos.y() - m_startScenePos.y(); } } 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(); } void DirectionalDragArea::touchEvent(QTouchEvent *event) { if (!isEnabled() || !isVisible()) { QQuickItem::touchEvent(event); return; } switch (m_status) { case WaitingForTouch: touchEvent_absent(event); break; case Undecided: touchEvent_undecided(event); break; case Recognized: touchEvent_recognized(event); break; default: // Rejected touchEvent_rejected(event); break; } } void DirectionalDragArea::touchEvent_absent(QTouchEvent *event) { if ((event->touchPointStates() && (Qt::TouchPointPressed || Qt::TouchPointMoved)) && event->touchPoints().count() == 1) { const QTouchEvent::TouchPoint &touchPoint = event->touchPoints()[0]; m_startPos = touchPoint.pos(); m_startScenePos = touchPoint.scenePos(); m_touchId = touchPoint.id(); m_dampedPos.reset(m_startPos); updateVelocityCalculator(m_startPos); m_velocityCalculator->reset(); m_numSamplesOnLastSpeedCheck = 0; m_silenceTime = 0; setPreviousPos(m_startPos); setPreviousScenePos(m_startScenePos); if (m_distanceThreshold > 0) setStatus(Undecided); else setStatus(Recognized); } } void DirectionalDragArea::touchEvent_undecided(QTouchEvent *event) { const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event); const QPointF &touchPos = touchPoint->pos(); if (touchPoint->state() == Qt::TouchPointReleased) { // touch has ended before recognition concluded setStatus(WaitingForTouch); return; } if (event->touchPointStates().testFlag(Qt::TouchPointPressed) || event->touchPoints().count() > 1) { // multi-finger drags are not accepted setStatus(Rejected); return; } m_previousDampedPos.setX(m_dampedPos.x()); m_previousDampedPos.setY(m_dampedPos.y()); m_dampedPos.update(touchPos); updateVelocityCalculator(touchPos); if (!pointInsideAllowedArea()) { setStatus(Rejected); return; } if (!movingInRightDirection()) { setStatus(Rejected); return; } setPreviousPos(touchPos); setPreviousScenePos(touchPoint->scenePos()); if (movedFarEnough(touchPos)) { setStatus(Recognized); } } void DirectionalDragArea::touchEvent_recognized(QTouchEvent *event) { const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event); setPreviousPos(touchPoint->pos()); setPreviousScenePos(touchPoint->scenePos()); if (touchPoint->state() == Qt::TouchPointReleased) { setStatus(WaitingForTouch); } } void DirectionalDragArea::touchEvent_rejected(QTouchEvent *event) { const QTouchEvent::TouchPoint *touchPoint = fetchTargetTouchPoint(event); if (!touchPoint || touchPoint->state() == Qt::TouchPointReleased) { setStatus(WaitingForTouch); } } 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 { qreal dX = m_dampedPos.x() - m_startPos.x(); qreal dY = m_dampedPos.y() - m_startPos.y(); switch (m_direction) { case Direction::Upwards: return dY <= 0 && qFabs(dX) <= qFabs(dY) * m_wideningFactor; case Direction::Downwards: return dY >= 0 && qFabs(dX) <= dY * m_wideningFactor; case Direction::Leftwards: return dX <= 0 && qFabs(dY) <= qFabs(dX) * m_wideningFactor; default: // Direction::Rightwards: return dX >= 0 && qFabs(dY) <= dX * m_wideningFactor; } } bool DirectionalDragArea::movingInRightDirection() const { switch (m_direction) { case Direction::Upwards: return m_dampedPos.y() <= m_previousDampedPos.y(); case Direction::Downwards: return m_dampedPos.y() >= m_previousDampedPos.y(); case Direction::Leftwards: return m_dampedPos.x() <= m_previousDampedPos.x(); default: // Direction::Rightwards: return m_dampedPos.x() >= m_previousDampedPos.x(); } } bool DirectionalDragArea::movedFarEnough(const QPointF &point) const { if (Direction::isHorizontal(m_direction)) return qFabs(point.x() - m_startPos.x()) > m_distanceThreshold; else return qFabs(point.y() - m_startPos.y()) > m_distanceThreshold; } void DirectionalDragArea::checkSpeed() { if (m_velocityCalculator->numSamples() >= AxisVelocityCalculator::MIN_SAMPLES_NEEDED) { qreal speed = qFabs(m_velocityCalculator->calculate()); qreal minSpeedMsecs = m_minSpeed / 1000.0; if (speed < minSpeedMsecs) { setStatus(Rejected); } } if (m_velocityCalculator->numSamples() == m_numSamplesOnLastSpeedCheck) { m_silenceTime += m_recognitionTimer->interval(); if (m_silenceTime > m_maxSilenceTime) { setStatus(Rejected); } } else { m_silenceTime = 0; } m_numSamplesOnLastSpeedCheck = m_velocityCalculator->numSamples(); } 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); switch (newStatus) { case WaitingForTouch: if (oldStatus != DirectionalDragArea::Rejected) { 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; case Rejected: Q_EMIT draggingChanged(false); break; default: // no-op break; } } void DirectionalDragArea::setPreviousPos(const QPointF &point) { Q_ASSERT(m_status != Rejected); 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) { Q_ASSERT(m_status != Rejected); bool xChanged = m_previousScenePos.x() != point.x(); bool yChanged = m_previousScenePos.y() != point.y(); m_previousScenePos = point; if (xChanged) { Q_EMIT touchSceneXChanged(point.x()); if (Direction::isHorizontal(m_direction)) Q_EMIT sceneDistanceChanged(sceneDistance()); } if (yChanged) { Q_EMIT touchSceneYChanged(point.y()); if (Direction::isVertical(m_direction)) Q_EMIT sceneDistanceChanged(sceneDistance()); } } void DirectionalDragArea::updateVelocityCalculator(const QPointF &point) { if (Direction::isHorizontal(m_direction)) { m_velocityCalculator->setTrackedPosition(point.x()); } else { m_velocityCalculator->setTrackedPosition(point.y()); } } // Because we are defining a new QObject-based class (RecognitionTimer) here. #include "DirectionalDragArea.moc"