#include "CanvasFixtures.h" #include // The canvas: Zoom, Hand, Space, the wheel, a pinch. class CanvasNavigationTests : public QObject { Q_OBJECT private slots: void theZoomToolClicksAndDrags(); void theHandToolAndSpaceDragTheView(); void theWheelPansAndZoomsWithAModifier(); void aPinchZoomsAboutThePointer(); void theMiddleButtonPansFromAnyTool(); }; void CanvasNavigationTests::theZoomToolClicksAndDrags() { Shown shown; CanvasView &canvas = *shown.canvas; EditorSession &session = shown.session; shown.settle(); session.zoom(1); session.selectTool(NavigationTool::zoom); const QSizeF size = shown.documentSize(); const QPointF click(120, 90); const QPointF under = session.viewport.documentPoint(click, size); QTest::mouseClick(&canvas, Qt::LeftButton, Qt::NoModifier, click.toPoint()); QCOMPARE(session.viewport.zoom(), 2.0); QCOMPARE(session.viewport.documentPoint(click, size), under); QTest::mouseClick(&canvas, Qt::LeftButton, Qt::AltModifier, click.toPoint()); QCOMPARE(session.viewport.zoom(), 1.0); // Dragging 100 points doubles about the start; release adds nothing. const QPointF start(260, 110); const QPointF anchor = session.viewport.documentPoint(start, size); QTest::mousePress(&canvas, Qt::LeftButton, Qt::NoModifier, start.toPoint()); drag(canvas, QPointF(360, 110)); QCOMPARE(session.viewport.zoom(), 2.0); QCOMPARE(session.viewport.documentPoint(start, size), anchor); drag(canvas, QPointF(310, 110)); QVERIFY(qFuzzyCompare(session.viewport.zoom(), std::sqrt(2.0))); QCOMPARE(session.viewport.documentPoint(start, size), anchor); QTest::mouseRelease(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(310, 110)); QVERIFY(qFuzzyCompare(session.viewport.zoom(), std::sqrt(2.0))); // Two points of drift still count as a click. QTest::mousePress(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(100, 100)); drag(canvas, QPointF(102, 100)); QTest::mouseRelease(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(102, 100)); QVERIFY(qFuzzyCompare(session.viewport.zoom(), 2 * std::sqrt(2.0))); // Busy, importing or without a document a press does nothing. session.setIsProjectBusy(true); QTest::mouseClick(&canvas, Qt::LeftButton, Qt::NoModifier, click.toPoint()); QVERIFY(qFuzzyCompare(session.viewport.zoom(), 2 * std::sqrt(2.0))); session.setIsProjectBusy(false); session.setIsImporting(true); QTest::mouseClick(&canvas, Qt::LeftButton, Qt::NoModifier, click.toPoint()); QVERIFY(qFuzzyCompare(session.viewport.zoom(), 2 * std::sqrt(2.0))); session.setIsImporting(false); QTest::mouseClick(&canvas, Qt::RightButton, Qt::NoModifier, click.toPoint()); QVERIFY(qFuzzyCompare(session.viewport.zoom(), 2 * std::sqrt(2.0))); Shown empty(std::nullopt); empty.session.selectTool(NavigationTool::zoom); QTest::mouseClick(empty.canvas, Qt::LeftButton, Qt::NoModifier, click.toPoint()); QCOMPARE(empty.session.viewport.zoom(), 1.0); } void CanvasNavigationTests::theHandToolAndSpaceDragTheView() { Shown shown; CanvasView &canvas = *shown.canvas; EditorSession &session = shown.session; shown.settle(); session.zoom(1); session.selectTool(NavigationTool::hand); canvas.synchronizeDisplay(); QCOMPARE(canvas.cursor().shape(), Qt::OpenHandCursor); QSignalSpy changes(&session, &EditorSession::changed); const QSizeF pan = session.viewport.pan; QTest::mousePress(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(100, 100)); QCOMPARE(canvas.cursor().shape(), Qt::ClosedHandCursor); drag(canvas, QPointF(130, 120)); QCOMPARE(session.viewport.pan, pan + QSizeF(30, 20)); QVERIFY(!session.viewport.followsFit()); QCOMPARE(changes.count(), 1); drag(canvas, QPointF(135, 120)); QCOMPARE(session.viewport.pan, pan + QSizeF(35, 20)); QCOMPARE(changes.count(), 2); QTest::mouseRelease(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(135, 120)); QCOMPARE(canvas.cursor().shape(), Qt::OpenHandCursor); drag(canvas, QPointF(150, 120)); QCOMPARE(session.viewport.pan, pan + QSizeF(35, 20)); // Space drags the view in any tool. session.selectTool(NavigationTool::move); canvas.synchronizeDisplay(); QCOMPARE(canvas.cursor().shape(), Qt::ArrowCursor); QTRY_VERIFY(canvas.hasFocus()); QTest::keyPress(&canvas, Qt::Key_Space); QCOMPARE(canvas.cursor().shape(), Qt::OpenHandCursor); QTest::mousePress(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(50, 50)); QCOMPARE(canvas.cursor().shape(), Qt::ClosedHandCursor); // Swift 1.4.7: Space's repeats keep the hand closed all through. for (int repeat = 0; repeat < 3; ++repeat) { QKeyEvent release(QEvent::KeyRelease, Qt::Key_Space, Qt::NoModifier, QStringLiteral(" "), true); QApplication::sendEvent(&canvas, &release); QKeyEvent press(QEvent::KeyPress, Qt::Key_Space, Qt::NoModifier, QStringLiteral(" "), true); QApplication::sendEvent(&canvas, &press); QCOMPARE(canvas.cursor().shape(), Qt::ClosedHandCursor); } drag(canvas, QPointF(60, 70)); QCOMPARE(canvas.cursor().shape(), Qt::ClosedHandCursor); QCOMPARE(session.viewport.pan, pan + QSizeF(45, 40)); QTest::mouseRelease(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(60, 70)); // Space still held: the hand opens with the button. QCOMPARE(canvas.cursor().shape(), Qt::OpenHandCursor); // A repeat's release is no release: the hand stays. QKeyEvent repeated(QEvent::KeyRelease, Qt::Key_Space, Qt::NoModifier, QStringLiteral(" "), true); QApplication::sendEvent(&canvas, &repeated); QCOMPARE(canvas.cursor().shape(), Qt::OpenHandCursor); QTest::keyRelease(&canvas, Qt::Key_Space); QCOMPARE(canvas.cursor().shape(), Qt::ArrowCursor); // Space let go mid-pan: the pan goes on, closed. QTest::keyPress(&canvas, Qt::Key_Space); QTest::mousePress(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(50, 50)); QTest::keyRelease(&canvas, Qt::Key_Space); QCOMPARE(canvas.cursor().shape(), Qt::ClosedHandCursor); drag(canvas, QPointF(55, 50)); QCOMPARE(canvas.cursor().shape(), Qt::ClosedHandCursor); QCOMPARE(session.viewport.pan, pan + QSizeF(50, 40)); QTest::mouseRelease(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(55, 50)); QCOMPARE(canvas.cursor().shape(), Qt::ArrowCursor); // Importing, a press starts no drag. session.selectTool(NavigationTool::hand); session.setIsImporting(true); QTest::mousePress(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(50, 50)); drag(canvas, QPointF(90, 90)); QCOMPARE(session.viewport.pan, pan + QSizeF(50, 40)); QTest::mouseRelease(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(90, 90)); } void CanvasNavigationTests::theWheelPansAndZoomsWithAModifier() { Shown shown; CanvasView &canvas = *shown.canvas; EditorSession &session = shown.session; shown.settle(); session.zoom(1); const QSizeF size = shown.documentSize(); const QPointF pointer(120, 90); const double lines = QApplication::wheelScrollLines(); QSizeF pan = session.viewport.pan; // Pixels count as they are, and win over notches. wheel(canvas, pointer, QPoint(0, -30), QPoint(0, -240), Qt::NoModifier); pan += QSizeF(0, -30); QCOMPARE(session.viewport.pan, pan); QVERIFY(!session.viewport.followsFit()); // A notch is a few lines, twelve points each. wheel(canvas, pointer, QPoint(), QPoint(0, 120), Qt::NoModifier); pan += QSizeF(0, 12 * lines); QCOMPARE(session.viewport.pan, pan); wheel(canvas, pointer, QPoint(), QPoint(-60, 0), Qt::NoModifier); pan += QSizeF(-6 * lines, 0); QCOMPARE(session.viewport.pan, pan); // Ctrl or Alt zooms about the pointer, by Swift's curve. const QPointF under = session.viewport.documentPoint(pointer, size); wheel(canvas, pointer, QPoint(), QPoint(0, 120), Qt::ControlModifier); QVERIFY(qFuzzyCompare(session.viewport.zoom(), std::exp(-lines * 0.015))); QCOMPARE(session.viewport.documentPoint(pointer, size), under); wheel(canvas, pointer, QPoint(0, 10), QPoint(), Qt::AltModifier); QVERIFY(qFuzzyCompare(session.viewport.zoom(), std::exp(-lines * 0.015) * std::exp(-10 * 0.015))); QCOMPARE(session.viewport.documentPoint(pointer, size), under); // Without a document the wheel does nothing. Shown empty(std::nullopt); wheel(*empty.canvas, pointer, QPoint(0, -30), QPoint(), Qt::NoModifier); QCOMPARE(empty.session.viewport.pan, QSizeF(0, 0)); wheel(*empty.canvas, pointer, QPoint(), QPoint(0, 120), Qt::ControlModifier); QCOMPARE(empty.session.viewport.zoom(), 1.0); } void CanvasNavigationTests::aPinchZoomsAboutThePointer() { Shown shown; shown.settle(); shown.session.zoom(1); const QSizeF size = shown.documentSize(); const QPointF pointer(120, 90); const QPointF under = shown.session.viewport.documentPoint(pointer, size); QNativeGestureEvent pinch(Qt::ZoomNativeGesture, QPointingDevice::primaryPointingDevice(), 2, pointer, pointer, shown.canvas->mapToGlobal(pointer.toPoint()), 0.5, QPointF()); QVERIFY(QApplication::sendEvent(shown.canvas, &pinch)); QCOMPARE(shown.session.viewport.zoom(), 1.5); QCOMPARE(shown.session.viewport.documentPoint(pointer, size), under); // Other gestures pass on. QNativeGestureEvent swipe(Qt::SwipeNativeGesture, QPointingDevice::primaryPointingDevice(), 3, pointer, pointer, shown.canvas->mapToGlobal(pointer.toPoint()), 0.5, QPointF()); QApplication::sendEvent(shown.canvas, &swipe); QCOMPARE(shown.session.viewport.zoom(), 1.5); } void CanvasNavigationTests::theMiddleButtonPansFromAnyTool() { Shown shown; CanvasView &canvas = *shown.canvas; EditorSession &session = shown.session; shown.settle(); session.zoom(1); session.selectTool(NavigationTool::brush); canvas.synchronizeDisplay(); const auto move = [&](QPointF to, Qt::MouseButtons buttons) { QMouseEvent event(QEvent::MouseMove, to, to, canvas.mapToGlobal(to.toPoint()), Qt::NoButton, buttons, Qt::NoModifier); QApplication::sendEvent(&canvas, &event); }; move(QPointF(100, 100), Qt::NoButton); const Qt::CursorShape brush = canvas.cursor().shape(); QVERIFY(canvas.brushCursor().circle().has_value()); const QSizeF pan = session.viewport.pan; QSignalSpy changes(&session, &EditorSession::changed); // The circle hides and nothing paints; the view follows. QTest::mousePress(&canvas, Qt::MiddleButton, Qt::NoModifier, QPoint(100, 100)); QCOMPARE(canvas.cursor().shape(), Qt::ClosedHandCursor); QVERIFY(!canvas.brushCursor().circle().has_value()); move(QPointF(130, 120), Qt::MiddleButton); QCOMPARE(session.viewport.pan, pan + QSizeF(30, 20)); QCOMPARE(changes.count(), 1); move(QPointF(150, 150), Qt::MiddleButton); QCOMPARE(session.viewport.pan, pan + QSizeF(50, 50)); move(QPointF(130, 120), Qt::MiddleButton); QCOMPARE(session.viewport.pan, pan + QSizeF(30, 20)); QVERIFY(!session.brushStroke() && !canvas.brushCursor().circle().has_value()); QCOMPARE(canvas.cursor().shape(), Qt::ClosedHandCursor); QTest::mouseRelease(&canvas, Qt::MiddleButton, Qt::NoModifier, QPoint(130, 120)); QCOMPARE(canvas.cursor().shape(), brush); QVERIFY(canvas.brushCursor().circle().has_value()); // Beside a Hand drag each button keeps its own point. session.selectTool(NavigationTool::hand); canvas.synchronizeDisplay(); QTest::mousePress(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(50, 50)); QTest::mousePress(&canvas, Qt::MiddleButton, Qt::NoModifier, QPoint(200, 200)); move(QPointF(60, 50), Qt::LeftButton | Qt::MiddleButton); QCOMPARE(session.viewport.pan, pan + QSizeF(30, 20) + QSizeF(10, 0) + QSizeF(-140, -150)); QTest::mouseRelease(&canvas, Qt::MiddleButton, Qt::NoModifier, QPoint(60, 50)); QCOMPARE(canvas.cursor().shape(), Qt::ClosedHandCursor); QTest::mouseRelease(&canvas, Qt::LeftButton, Qt::NoModifier, QPoint(60, 50)); QCOMPARE(canvas.cursor().shape(), Qt::OpenHandCursor); // A lost release: a buttonless move ends the pan. const QSizeF held = session.viewport.pan; QTest::mousePress(&canvas, Qt::MiddleButton, Qt::NoModifier, QPoint(50, 50)); move(QPointF(70, 70), Qt::NoButton); QCOMPARE(canvas.cursor().shape(), Qt::OpenHandCursor); move(QPointF(90, 90), Qt::MiddleButton); QCOMPARE(session.viewport.pan, held); // Without a document the middle button does nothing. Shown empty(std::nullopt); empty.settle(); const Qt::CursorShape before = empty.canvas->cursor().shape(); QTest::mousePress(empty.canvas, Qt::MiddleButton, Qt::NoModifier, QPoint(50, 50)); QCOMPARE(empty.canvas->cursor().shape(), before); } QTEST_MAIN(CanvasNavigationTests) #include "CanvasNavigationTests.moc"