/* * 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 #include #include #include #include #include class FakeTimer : public UbuntuGestures::AbstractTimer { Q_OBJECT public: FakeTimer(QObject *parent = 0) : UbuntuGestures::AbstractTimer(parent) {} virtual int interval() const { return m_duration; } virtual void setInterval(int msecs) { m_duration = msecs; } void emitTimeout() {Q_EMIT timeout();} private: int m_duration; }; class FakeTimeSource : public UbuntuGestures::TimeSource { public: FakeTimeSource() { m_msecsSinceReference = 0; } virtual qint64 msecsSinceReference() {return m_msecsSinceReference;} qint64 m_msecsSinceReference; }; class tst_DirectionalDragArea: public QObject { Q_OBJECT public: tst_DirectionalDragArea() : device(0) { } private Q_SLOTS: void initTestCase(); // will be called before the first test function is executed void cleanupTestCase(); // will be called after the last test function was executed. void init(); // called right before each and every test function is executed void cleanup(); // called right after each and every test function is executed void edgeDrag(); void edgeDrag_data(); void dragWithShortDirectionChange(); void minSpeed(); void minSpeed_data(); void recognitionTimerUsage(); void maxSilenceTime(); void sceneXAndX(); void sceneYAndY(); void emitDraggingChangedOnDirectRecognition(); private: QQuickView *createView(); QQuickView *view; QTouchDevice *device; FakeTimer *fakeTimer; FakeTimeSource *fakeTimeSource; }; void tst_DirectionalDragArea::initTestCase() { if (!device) { device = new QTouchDevice; device->setType(QTouchDevice::TouchScreen); QWindowSystemInterface::registerTouchDevice(device); } view = 0; } void tst_DirectionalDragArea::cleanupTestCase() { } void tst_DirectionalDragArea::init() { view = createView(); view->setSource(QUrl::fromLocalFile("edgeDragExample.qml")); view->show(); QVERIFY(QTest::qWaitForWindowExposed(view)); QVERIFY(view->rootObject() != 0); qApp->processEvents(); fakeTimer = new FakeTimer; fakeTimeSource = new FakeTimeSource; } void tst_DirectionalDragArea::cleanup() { delete view; view = 0; delete fakeTimer; fakeTimer = 0; delete fakeTimeSource; fakeTimeSource = 0; } QQuickView *tst_DirectionalDragArea::createView() { QQuickView *window = new QQuickView(0); window->setResizeMode(QQuickView::SizeRootObjectToView); window->resize(600, 600); window->engine()->addImportPath(QLatin1String(UBUNTU_GESTURES_PLUGIN_DIR)); return window; } namespace { QPointF calculateInitialTouchPos(DirectionalDragArea *edgeDragArea, QQuickView *view) { switch (edgeDragArea->direction()) { case Direction::Upwards: return QPointF(view->width()/2.0f, view->height() - (edgeDragArea->height()/2.0f)); case Direction::Downwards: return QPointF(view->width()/2.0f, edgeDragArea->height()/2.0f); case Direction::Leftwards: return QPointF(view->width() - (edgeDragArea->width()/2.0f), view->height()/2.0f); default: // Direction::Rightwards: return QPointF(edgeDragArea->width()/2.0f, view->height()/2.0f); } } QPointF calculateDirectionVector(DirectionalDragArea *edgeDragArea, qreal wideningAngleMultiplier) { qreal angleRadians = edgeDragArea->wideningAngle() * wideningAngleMultiplier * M_PI / 180.0; qreal angleCos = qCos(angleRadians); qreal angleSin = qSin(angleRadians); switch (edgeDragArea->direction()) { case Direction::Upwards: return QPointF(angleSin, -angleCos); case Direction::Downwards: return QPointF(angleSin, angleCos); case Direction::Leftwards: return QPointF(-angleCos, angleSin); default: // Direction::Rightwards: return QPointF(angleCos, angleSin); } } QPointF createTouchDeviation(DirectionalDragArea *edgeDragArea) { qreal deviation = edgeDragArea->maxDeviation() * 0.8; if (Direction::isHorizontal(edgeDragArea->direction())) { return QPointF(0, deviation); } else { return QPointF(deviation, 0); } } } void tst_DirectionalDragArea::edgeDrag() { QFETCH(QString, dragAreaObjectName); QFETCH(qreal, wideningAngleMultiplier); QFETCH(qreal, dragDistanceFactor); QFETCH(int, expectedGestureRecognition); DirectionalDragArea *edgeDragArea = view->rootObject()->findChild(dragAreaObjectName); QVERIFY(edgeDragArea != 0); edgeDragArea->setRecognitionTimer(fakeTimer); edgeDragArea->setTimeSource(fakeTimeSource); QSignalSpy draggingSpy(edgeDragArea, SIGNAL(draggingChanged(bool))); QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view); QPointF touchPoint = initialTouchPos; qreal desiredDragDistance = edgeDragArea->distanceThreshold()*dragDistanceFactor; QPointF dragDirectionVector = calculateDirectionVector(edgeDragArea, wideningAngleMultiplier); QPointF touchMovement = dragDirectionVector * (edgeDragArea->distanceThreshold() * 0.1f); QTest::touchEvent(view, device).press(0, touchPoint.toPoint()); QCOMPARE(draggingSpy.count(), 1); QCOMPARE(edgeDragArea->dragging(), true); if (wideningAngleMultiplier > 0) { // go close to the border of the valid area for this touch point // in order to make it easier to leave it by dragging at an angle // slightly bigger than the widening angle touchPoint += createTouchDeviation(edgeDragArea); QTest::touchEvent(view, device).move(0, touchPoint.toPoint()); } do { touchPoint += touchMovement; QTest::touchEvent(view, device).move(0, touchPoint.toPoint()); } while ((touchPoint - initialTouchPos).manhattanLength() < desiredDragDistance); if (expectedGestureRecognition) QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized); if (edgeDragArea->status() == DirectionalDragArea::Rejected) { QCOMPARE(edgeDragArea->dragging(), false); QCOMPARE(draggingSpy.count(), 2); } QTest::touchEvent(view, device).release(0, touchPoint.toPoint()); QCOMPARE(draggingSpy.count(), 2); QCOMPARE(edgeDragArea->dragging(), false); } void tst_DirectionalDragArea::edgeDrag_data() { QTest::addColumn("dragAreaObjectName"); QTest::addColumn("wideningAngleMultiplier"); QTest::addColumn("dragDistanceFactor"); QTest::addColumn("expectedGestureRecognition"); QTest::newRow("rightwards, tiny drag") << "hpDragArea" << 0.0 << 0.2 << 0; QTest::newRow("rightwards, straight drag") << "hpDragArea" << 0.0 << 3.0 << 1; QTest::newRow("rightwards, diagonal drag") << "hpDragArea" << 0.9 << 3.0 << 1; QTest::newRow("rightwards, overly diagonal drag") << "hpDragArea" << 2.0 << 3.0 << 0; QTest::newRow("leftwards, tiny drag") << "hnDragArea" << 0.0 << 0.2 << 0; QTest::newRow("leftwards, straight drag") << "hnDragArea" << 0.0 << 3.0 << 1; QTest::newRow("leftwards, diagonal drag") << "hnDragArea" << 0.9 << 3.0 << 1; QTest::newRow("downwards, tiny drag") << "vpDragArea" << 0.0 << 0.2 << 0; QTest::newRow("downwards, straight drag") << "vpDragArea" << 0.0 << 3.0 << 1; QTest::newRow("downwards, diagonal drag") << "vpDragArea" << 0.9 << 3.0 << 1; QTest::newRow("upwards, tiny drag") << "vnDragArea" << 0.0 << 0.2 << 0; QTest::newRow("upwards, straight drag") << "vnDragArea" << 0.0 << 3.0 << 1; QTest::newRow("upwards, diagonal drag") << "vnDragArea" << 0.9 << 3.0 << 1; QTest::newRow("upwards, overly diagonal drag") << "vnDragArea" << 2.0 << 3.0 << 0; } /* A directional drag should still be recognized if there is a momentaneous, small, change in the direction of a drag. That should be accounted as input noise and therefore ignored. */ void tst_DirectionalDragArea::dragWithShortDirectionChange() { DirectionalDragArea *edgeDragArea = view->rootObject()->findChild("hpDragArea"); QVERIFY(edgeDragArea != 0); edgeDragArea->setRecognitionTimer(fakeTimer); edgeDragArea->setTimeSource(fakeTimeSource); QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view); QPointF touchPoint = initialTouchPos; qreal desiredDragDistance = edgeDragArea->distanceThreshold()*2.0; QPointF dragDirectionVector(1.0, 0.0); QPointF touchMovement = dragDirectionVector * (edgeDragArea->distanceThreshold() * 0.1f); QTest::touchEvent(view, device).press(0, touchPoint.toPoint()); // Move a bit in the proper direction for (int i=0; i < 3; ++i) { touchPoint += touchMovement; QTest::touchEvent(view, device).move(0, touchPoint.toPoint()); } // Then a sudden and small movement to the opposite direction touchPoint -= dragDirectionVector * (edgeDragArea->maxDeviation() * 0.7); QTest::touchEvent(view, device).move(0, touchPoint.toPoint()); // And then resume movment in the correct direction until it crosses the distance threshold. do { touchPoint += touchMovement; QTest::touchEvent(view, device).move(0, touchPoint.toPoint()); } while ((touchPoint - initialTouchPos).manhattanLength() < desiredDragDistance); QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized); QTest::touchEvent(view, device).release(0, touchPoint.toPoint()); } /* Checks that a gesture will be rejected if it's slower than minSpeed while status is Undecided. */ void tst_DirectionalDragArea::minSpeed() { QFETCH(int, minSpeedMsecsDeviation); QFETCH(int, expectedStatusAfterSpeedCheck); DirectionalDragArea *edgeDragArea = view->rootObject()->findChild("hpDragArea"); QVERIFY(edgeDragArea != 0); edgeDragArea->setRecognitionTimer(fakeTimer); edgeDragArea->setTimeSource(fakeTimeSource); QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view); QPointF touchPoint = initialTouchPos; QPointF dragDirectionVector(1.0, 0.0); qreal distanceStep = edgeDragArea->distanceThreshold() * 0.1f; QPointF touchMovement = dragDirectionVector * distanceStep; qreal minSpeedMsecs = edgeDragArea->minSpeed() / 1000.0; qint64 timeStepMsecs = qFloor(distanceStep / minSpeedMsecs) + minSpeedMsecsDeviation; // if it fails it means the params set in the QML file are not in harmony with the values // used in this test Q_ASSERT(timeStepMsecs > 0); fakeTimeSource->m_msecsSinceReference = 0; QTest::touchEvent(view, device).press(0, touchPoint.toPoint()); // Move a bit in the proper direction for (int i=0; i < 4; ++i) { touchPoint += touchMovement; fakeTimeSource->m_msecsSinceReference += timeStepMsecs; QTest::touchEvent(view, device).move(0, touchPoint.toPoint()); } QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided); // Force the periodic speed check. fakeTimer->emitTimeout(); QCOMPARE((int)edgeDragArea->status(), expectedStatusAfterSpeedCheck); } void tst_DirectionalDragArea::minSpeed_data() { QTest::addColumn("minSpeedMsecsDeviation"); QTest::addColumn("expectedStatusAfterSpeedCheck"); QTest::newRow("slower than minSpeed") << 20 << (int)DirectionalDragArea::Rejected; QTest::newRow("faster than minSpeed") << -20 << (int)DirectionalDragArea::Undecided; } /* Checks that the recognition timer is started and stopped appropriately. I.e., check that it's running only while gesture recognition is taking place (status == Undecided) */ void tst_DirectionalDragArea::recognitionTimerUsage() { DirectionalDragArea *edgeDragArea = view->rootObject()->findChild("hpDragArea"); QVERIFY(edgeDragArea != 0); edgeDragArea->setRecognitionTimer(fakeTimer); edgeDragArea->setTimeSource(fakeTimeSource); QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view); QPointF touchPoint = initialTouchPos; QPointF dragDirectionVector(1.0, 0.0); QPointF touchMovement = dragDirectionVector * (edgeDragArea->distanceThreshold() * 0.2f); QVERIFY(!fakeTimer->isRunning()); QTest::touchEvent(view, device).press(0, touchPoint.toPoint()); QVERIFY(fakeTimer->isRunning()); // Move a bit in the proper direction for (int i=0; i < 3; ++i) { touchPoint += touchMovement; QTest::touchEvent(view, device).move(0, touchPoint.toPoint()); } QVERIFY(fakeTimer->isRunning()); // Move beyond distance threshold touchPoint += 3*touchMovement; QTest::touchEvent(view, device).move(0, touchPoint.toPoint()); QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized); QVERIFY(!fakeTimer->isRunning()); } /* A gesture should be rejected if too much time has passed without any new input events from it. */ void tst_DirectionalDragArea::maxSilenceTime() { DirectionalDragArea *edgeDragArea = view->rootObject()->findChild("hpDragArea"); QVERIFY(edgeDragArea != 0); edgeDragArea->setRecognitionTimer(fakeTimer); edgeDragArea->setTimeSource(fakeTimeSource); QPointF initialTouchPos = calculateInitialTouchPos(edgeDragArea, view); QPointF touchPoint = initialTouchPos; fakeTimeSource->m_msecsSinceReference = 0; QTest::touchEvent(view, device).press(0, touchPoint.toPoint()); QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided); int neededTimeouts = qCeil((qreal)edgeDragArea->maxSilenceTime() / (qreal)fakeTimer->interval()); if ((edgeDragArea->maxSilenceTime() % fakeTimer->interval()) == 0) { ++neededTimeouts; } // Force the periodic speed check. for (int i = 0; i < neededTimeouts; ++i) { fakeTimer->emitTimeout(); if (i < neededTimeouts - 1) { QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Undecided); } } QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Rejected); } /* Checks that it informs the X coordinate of the touch point in local and scene coordinates correctly. */ void tst_DirectionalDragArea::sceneXAndX() { DirectionalDragArea *edgeDragArea = view->rootObject()->findChild("hnDragArea"); QVERIFY(edgeDragArea != 0); edgeDragArea->setRecognitionTimer(fakeTimer); edgeDragArea->setTimeSource(fakeTimeSource); QPointF touchScenePos(view->width() - (edgeDragArea->width()/2.0f), view->height()/2.0f); fakeTimeSource->m_msecsSinceReference = 0; QTest::touchEvent(view, device).press(0, touchScenePos.toPoint()); QSignalSpy touchXSpy(edgeDragArea, SIGNAL(touchXChanged(qreal))); QSignalSpy touchSceneXSpy(edgeDragArea, SIGNAL(touchSceneXChanged(qreal))); touchScenePos.rx() = view->width() / 2; QTest::touchEvent(view, device).move(0, touchScenePos.toPoint()); QCOMPARE(touchXSpy.count(), 1); QCOMPARE(touchSceneXSpy.count(), 1); QCOMPARE(edgeDragArea->touchX(), touchScenePos.x() - edgeDragArea->x()); QCOMPARE(edgeDragArea->touchSceneX(), touchScenePos.x()); } /* Checks that it informs the Y coordinate of the touch point in local and scene coordinates correctly. */ void tst_DirectionalDragArea::sceneYAndY() { DirectionalDragArea *edgeDragArea = view->rootObject()->findChild("vnDragArea"); QVERIFY(edgeDragArea != 0); edgeDragArea->setRecognitionTimer(fakeTimer); edgeDragArea->setTimeSource(fakeTimeSource); QPointF touchScenePos(view->width()/2.0f, view->height() - (edgeDragArea->height()/2.0f)); fakeTimeSource->m_msecsSinceReference = 0; QTest::touchEvent(view, device).press(0, touchScenePos.toPoint()); QSignalSpy touchYSpy(edgeDragArea, SIGNAL(touchYChanged(qreal))); QSignalSpy touchSceneYSpy(edgeDragArea, SIGNAL(touchSceneYChanged(qreal))); touchScenePos.ry() = view->height() / 2; QTest::touchEvent(view, device).move(0, touchScenePos.toPoint()); QCOMPARE(touchYSpy.count(), 1); QCOMPARE(touchSceneYSpy.count(), 1); QCOMPARE(edgeDragArea->touchY(), touchScenePos.y() - edgeDragArea->y()); QCOMPARE(edgeDragArea->touchSceneY(), touchScenePos.y()); } /* Emit draggingChanged() when status change from WaitingForTouch directly to Recognized. This happens when the gesture recognition is effectively disabled (e.g. by setting a distanceThreshold of zero) */ void tst_DirectionalDragArea::emitDraggingChangedOnDirectRecognition() { DirectionalDragArea *edgeDragArea = view->rootObject()->findChild("hpDragArea"); QVERIFY(edgeDragArea != 0); edgeDragArea->setRecognitionTimer(fakeTimer); edgeDragArea->setTimeSource(fakeTimeSource); QPointF touchPos = calculateInitialTouchPos(edgeDragArea, view); QSignalSpy draggingSpy(edgeDragArea, SIGNAL(draggingChanged(bool))); // That will make it completetly skip the recognition phase ("Undecided" status). edgeDragArea->setDistanceThreshold(0.0); QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::WaitingForTouch); QCOMPARE(edgeDragArea->dragging(), false); fakeTimeSource->m_msecsSinceReference = 0; QTest::touchEvent(view, device).press(0, touchPos.toPoint()); QCOMPARE((int)edgeDragArea->status(), (int)DirectionalDragArea::Recognized); QCOMPARE(draggingSpy.count(), 1); QCOMPARE(edgeDragArea->dragging(), true); } QTEST_MAIN(tst_DirectionalDragArea) #include "tst_DirectionalDragArea.moc"