363 lines
17 KiB
C++
363 lines
17 KiB
C++
#include "SelectionCanvasFixtures.h"
|
||
|
||
// Marquee and Lasso gestures on the canvas: drags, keys, focus.
|
||
class SelectionCanvasTests : public QObject {
|
||
Q_OBJECT
|
||
private slots:
|
||
void marqueeDragsSelectShiftAddsAltSubtractsAndAFreshShiftSquares();
|
||
void theFreehandLassoDragsAnOutlineAndClicksMoveOrDeselect();
|
||
void thePolygonalLassoTakesCornersAndClosesByClickReturnOrDoubleClick();
|
||
void arrowsNudgeTheOutlineAndTheKeysMAndLChooseTools();
|
||
void focusLossEndsAMarqueeDraftAndAnOutlineMove();
|
||
void shiftDeleteBeginsAContentAwareFill();
|
||
void aCtrlDragMovesSelectedPixelsAndCtrlArrowsNudgeThem();
|
||
void shiftKeepsMovedPixelsOnALine();
|
||
};
|
||
|
||
void SelectionCanvasTests::marqueeDragsSelectShiftAddsAltSubtractsAndAFreshShiftSquares()
|
||
{
|
||
Canvas shown;
|
||
EditorSession &session = shown.session;
|
||
session.applySelection(rectPath(QRectF(5, 5, 10, 10)), SelectionMode::replace, "Select");
|
||
// Shift held throughout: an add, 40–70 × 40–50, not squared.
|
||
shown.drag(QPointF(40, 40), QPointF(70, 50), Qt::ShiftModifier);
|
||
QCOMPARE(session.history.undoName(), QString("Rectangular Marquee"));
|
||
QCOMPARE(shown.coverage(10, 10), 255);
|
||
QCOMPARE(shown.coverage(65, 45), 255);
|
||
QCOMPARE(shown.coverage(65, 60), 0);
|
||
// Shift pressed afresh mid-drag: squared to 20–40 × 60–80.
|
||
shown.press(QPointF(20, 60), Qt::ShiftModifier);
|
||
shown.move(QPointF(30, 65), Qt::ShiftModifier);
|
||
shown.move(QPointF(35, 68));
|
||
shown.move(QPointF(40, 70), Qt::ShiftModifier);
|
||
shown.release(QPointF(40, 70), Qt::ShiftModifier);
|
||
QCOMPARE(shown.coverage(30, 75), 255);
|
||
QCOMPARE(shown.coverage(65, 45), 255);
|
||
QCOMPARE(shown.coverage(10, 10), 255);
|
||
// Alt subtracts and never draws from the centre.
|
||
session.selectAll();
|
||
shown.drag(QPointF(40, 40), QPointF(60, 60), Qt::AltModifier);
|
||
QCOMPARE(shown.coverage(50, 50), 0);
|
||
QCOMPARE(shown.coverage(30, 30), 255);
|
||
// A key change reshapes the box before the pointer moves.
|
||
session.deselect();
|
||
shown.press(QPointF(100, 100));
|
||
shown.move(QPointF(130, 110));
|
||
QCOMPARE(session.lassoDraft().value().points[2], QPointF(130, 110));
|
||
QTest::keyPress(shown.window.windowHandle(), Qt::Key_Shift, Qt::ShiftModifier);
|
||
QCOMPARE(session.lassoDraft().value().points[2], QPointF(130, 130));
|
||
QTest::keyRelease(shown.window.windowHandle(), Qt::Key_Shift, Qt::NoModifier);
|
||
QCOMPARE(session.lassoDraft().value().points[2], QPointF(130, 110));
|
||
shown.release(QPointF(130, 110));
|
||
QCOMPARE(bounds(session), QRectF(100, 100, 30, 10));
|
||
// A click outside deselects; busy, a press draws nothing.
|
||
shown.click(QPointF(200, 200));
|
||
QVERIFY(!session.selection().has_value());
|
||
session.setIsProjectBusy(true);
|
||
shown.press(QPointF(10, 10));
|
||
QVERIFY(!session.lassoDraft().has_value());
|
||
session.setIsProjectBusy(false);
|
||
shown.release(QPointF(10, 10));
|
||
}
|
||
|
||
void SelectionCanvasTests::theFreehandLassoDragsAnOutlineAndClicksMoveOrDeselect()
|
||
{
|
||
Canvas shown;
|
||
EditorSession &session = shown.session;
|
||
session.selectTool(NavigationTool::lasso);
|
||
shown.press(QPointF(10, 10));
|
||
for (const QPointF point : {QPointF(90, 10), QPointF(90, 90), QPointF(10, 90)})
|
||
shown.move(point);
|
||
QCOMPARE(session.lassoDraft().value().points.size(), size_t(4));
|
||
shown.release(QPointF(10, 90));
|
||
QCOMPARE(session.history.undoName(), QString("Lasso"));
|
||
QCOMPARE(shown.coverage(50, 50), 255);
|
||
QCOMPARE(shown.coverage(95, 95), 0);
|
||
// Inside, a drag moves the outline; Shift keeps one axis.
|
||
shown.drag(QPointF(50, 50), QPointF(60, 55));
|
||
QCOMPARE(session.history.undoName(), QString("Move Selection"));
|
||
QCOMPARE(bounds(session), QRectF(20, 15, 80, 80));
|
||
shown.press(QPointF(50, 50));
|
||
shown.move(QPointF(70, 58), Qt::ShiftModifier);
|
||
shown.release(QPointF(70, 58), Qt::ShiftModifier);
|
||
QCOMPARE(bounds(session), QRectF(40, 15, 80, 80));
|
||
// Shift at the press adds an outline instead of moving.
|
||
shown.press(QPointF(50, 50), Qt::ShiftModifier);
|
||
shown.move(QPointF(200, 200), Qt::ShiftModifier);
|
||
shown.move(QPointF(200, 50), Qt::ShiftModifier);
|
||
shown.release(QPointF(200, 50), Qt::ShiftModifier);
|
||
QCOMPARE(session.history.undoName(), QString("Lasso"));
|
||
QCOMPARE(bounds(session), QRectF(40, 15, 160, 185));
|
||
// A click inside without a drag deselects, as outside.
|
||
shown.click(QPointF(50, 50));
|
||
QVERIFY(!session.selection().has_value());
|
||
QCOMPARE(session.history.undoName(), QString("Deselect"));
|
||
shown.click(QPointF(200, 200));
|
||
QVERIFY(!session.selection().has_value());
|
||
}
|
||
|
||
void SelectionCanvasTests::thePolygonalLassoTakesCornersAndClosesByClickReturnOrDoubleClick()
|
||
{
|
||
Canvas shown;
|
||
EditorSession &session = shown.session;
|
||
session.selectTool(NavigationTool::lasso);
|
||
session.setLassoKind(LassoKind::polygonal);
|
||
shown.click(QPointF(10, 10));
|
||
shown.click(QPointF(90, 10));
|
||
shown.click(QPointF(90, 90));
|
||
QCOMPARE(session.lassoDraft().value().points.size(), size_t(3));
|
||
// The rubber band follows the pointer; Backspace takes a corner.
|
||
shown.hover(QPointF(50, 50));
|
||
QCOMPARE(session.lassoDraft().value().cursor, std::optional(QPointF(50, 50)));
|
||
QTRY_VERIFY(shown.canvas->hasFocus());
|
||
QTest::keyClick(shown.canvas, Qt::Key_Backspace);
|
||
QCOMPARE(session.lassoDraft().value().points.size(), size_t(2));
|
||
shown.click(QPointF(90, 90));
|
||
// Near the first corner, with three, a click closes.
|
||
shown.click(QPointF(13, 12));
|
||
QVERIFY(!session.lassoDraft().has_value());
|
||
QCOMPARE(session.history.undoName(), QString("Polygonal Lasso"));
|
||
QCOMPARE(shown.coverage(80, 40), 255);
|
||
QCOMPARE(shown.coverage(20, 80), 0);
|
||
// Return closes; Escape cancels; a double click closes.
|
||
shown.click(QPointF(100, 100));
|
||
QVERIFY(session.lassoDraft().has_value());
|
||
shown.click(QPointF(200, 100));
|
||
shown.click(QPointF(150, 200));
|
||
QTest::keyClick(shown.canvas, Qt::Key_Return);
|
||
QCOMPARE(shown.coverage(150, 130), 255);
|
||
QCOMPARE(shown.coverage(50, 50), 0);
|
||
shown.click(QPointF(10, 200));
|
||
QTest::keyClick(shown.canvas, Qt::Key_Escape);
|
||
QVERIFY(!session.lassoDraft().has_value());
|
||
QCOMPARE(shown.coverage(150, 130), 255);
|
||
shown.click(QPointF(10, 250));
|
||
shown.click(QPointF(100, 250));
|
||
// A double click's first press adds the closing corner.
|
||
shown.click(QPointF(100, 290));
|
||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, QPoint(100, 290));
|
||
QVERIFY(!session.lassoDraft().has_value());
|
||
QCOMPARE(shown.coverage(70, 265), 255);
|
||
// With two corners a click by the first adds one.
|
||
shown.click(QPointF(300, 10));
|
||
shown.click(QPointF(350, 10));
|
||
shown.click(QPointF(302, 12));
|
||
QCOMPARE(session.lassoDraft().value().points.size(), size_t(3));
|
||
// A double click while Space pans closes nothing.
|
||
QTest::keyPress(shown.canvas, Qt::Key_Space);
|
||
QTest::mouseDClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, QPoint(320, 40));
|
||
QTest::mouseRelease(shown.canvas, Qt::LeftButton, Qt::NoModifier, QPoint(320, 40));
|
||
QTest::keyRelease(shown.canvas, Qt::Key_Space);
|
||
QCOMPARE(session.lassoDraft().value().points.size(), size_t(3));
|
||
session.cancelLasso();
|
||
}
|
||
|
||
void SelectionCanvasTests::arrowsNudgeTheOutlineAndTheKeysMAndLChooseTools()
|
||
{
|
||
Canvas shown;
|
||
EditorSession &session = shown.session;
|
||
session.applySelection(rectPath(QRectF(10, 10, 20, 20)), SelectionMode::replace, "Select");
|
||
QTRY_VERIFY(shown.canvas->hasFocus());
|
||
QTest::keyClick(shown.canvas, Qt::Key_Right);
|
||
QTest::keyClick(shown.canvas, Qt::Key_Down, Qt::ShiftModifier);
|
||
QCOMPARE(bounds(session), QRectF(11, 20, 20, 20));
|
||
QCOMPARE(session.history.undoName(), QString("Move Selection"));
|
||
// Alt-arrows are no nudge; a draft takes the arrows away.
|
||
QTest::keyClick(shown.canvas, Qt::Key_Left, Qt::AltModifier);
|
||
QCOMPARE(bounds(session), QRectF(11, 20, 20, 20));
|
||
session.beginLasso(QPointF(1, 1), SelectionMode::replace);
|
||
QTest::keyClick(shown.canvas, Qt::Key_Left);
|
||
QCOMPARE(bounds(session), QRectF(11, 20, 20, 20));
|
||
session.cancelLasso();
|
||
// M and L choose their tools; repeats and Shift too.
|
||
session.selectTool(NavigationTool::brush);
|
||
QTest::keyClick(shown.canvas, Qt::Key_M);
|
||
QVERIFY(session.tool() == NavigationTool::marquee && session.marqueeKind() == LassoKind::rectangle);
|
||
QTest::keyClick(shown.canvas, Qt::Key_M, Qt::ShiftModifier);
|
||
QVERIFY(session.tool() == NavigationTool::marquee && session.marqueeKind() == LassoKind::rectangle);
|
||
session.setMarqueeKind(LassoKind::ellipse);
|
||
QTest::keyClick(shown.canvas, Qt::Key_L);
|
||
QVERIFY(session.tool() == NavigationTool::lasso && session.lassoKind() == LassoKind::freehand);
|
||
QTest::keyClick(shown.canvas, Qt::Key_M);
|
||
QVERIFY(session.tool() == NavigationTool::marquee && session.marqueeKind() == LassoKind::ellipse);
|
||
// A held key's repeats choose nothing and end no draft.
|
||
session.selectTool(NavigationTool::lasso);
|
||
session.beginLasso(QPointF(1, 1), SelectionMode::replace);
|
||
QKeyEvent repeat(QEvent::KeyPress, Qt::Key_M, Qt::NoModifier, QStringLiteral("m"), true);
|
||
QApplication::sendEvent(shown.canvas, &repeat);
|
||
QVERIFY(session.tool() == NavigationTool::lasso && session.lassoDraft().has_value());
|
||
session.cancelLasso();
|
||
// In the Move tool the arrows nudge the layer.
|
||
session.insert(filled(10, 10, qRgba(255, 0, 0, 255), "Red"));
|
||
session.selectTool(NavigationTool::move);
|
||
QTest::keyClick(shown.canvas, Qt::Key_Right);
|
||
QCOMPARE(bounds(session), QRectF(11, 20, 20, 20));
|
||
QCOMPARE(session.history.undoName(), QString("Transform Layer"));
|
||
}
|
||
|
||
void SelectionCanvasTests::focusLossEndsAMarqueeDraftAndAnOutlineMove()
|
||
{
|
||
Canvas shown;
|
||
EditorSession &session = shown.session;
|
||
shown.press(QPointF(50, 50));
|
||
shown.move(QPointF(80, 80));
|
||
shown.canvas->clearFocus();
|
||
QVERIFY(!session.lassoDraft().has_value());
|
||
shown.release(QPointF(80, 80));
|
||
QVERIFY(!session.selection().has_value());
|
||
// A polygonal draft survives; a move ends as a step.
|
||
session.selectTool(NavigationTool::lasso);
|
||
session.setLassoKind(LassoKind::polygonal);
|
||
shown.click(QPointF(10, 10));
|
||
shown.canvas->clearFocus();
|
||
QVERIFY(session.lassoDraft().has_value());
|
||
session.cancelLasso();
|
||
session.selectAll();
|
||
// Twenty across: out of the canvas edges' snapping reach.
|
||
shown.press(QPointF(200, 150));
|
||
shown.move(QPointF(220, 150));
|
||
shown.canvas->clearFocus();
|
||
QCOMPARE(session.history.undoName(), QString("Move Selection"));
|
||
QVERIFY(session.canUndo());
|
||
shown.release(QPointF(220, 150));
|
||
QCOMPARE(bounds(session), QRectF(20, 0, 400, 300));
|
||
}
|
||
|
||
void SelectionCanvasTests::shiftDeleteBeginsAContentAwareFill()
|
||
{
|
||
Canvas shown;
|
||
EditorSession &session = shown.session;
|
||
QImage field = BrushRaster::context(400, 300, false);
|
||
field.fill(QColor(51, 153, 204));
|
||
QPainter red(&field);
|
||
red.fillRect(QRect(30, 30, 40, 40), Qt::red);
|
||
red.end();
|
||
session.insert(ImportedImage(field, field, "Field"));
|
||
QTRY_VERIFY(shown.canvas->hasFocus());
|
||
// Without a selection the keys do nothing.
|
||
QTest::keyClick(shown.canvas, Qt::Key_Backspace, Qt::ShiftModifier);
|
||
QVERIFY(!session.filterEdit().has_value());
|
||
shown.drag(QPointF(20, 20), QPointF(80, 80));
|
||
QTest::keyClick(shown.canvas, Qt::Key_Backspace, Qt::ShiftModifier);
|
||
QVERIFY(session.filterEdit().has_value());
|
||
QCOMPARE(session.filterEdit().value().kind, FilterKind::contentAwareFill);
|
||
session.cancelFilter();
|
||
QTest::keyClick(shown.canvas, Qt::Key_Delete, Qt::ShiftModifier);
|
||
QVERIFY(session.filterEdit().has_value());
|
||
// The canvas repaints once the preview lands.
|
||
QTRY_VERIFY(!session.filterEdit().value().preparing);
|
||
QVERIFY(shown.canvas->synchronizeDisplay());
|
||
QVERIFY(!shown.canvas->synchronizeDisplay());
|
||
// The preview stands in: the red block is gone.
|
||
const QColor shown_pixel = shown.canvas->grab().toImage().pixelColor(50, 50);
|
||
QVERIFY(std::abs(shown_pixel.red() - 51) <= 2 && std::abs(shown_pixel.blue() - 204) <= 2);
|
||
session.cancelFilter();
|
||
QCOMPARE(shown.canvas->grab().toImage().pixelColor(50, 50), QColor(Qt::red));
|
||
// Ctrl with it is no fill.
|
||
QTest::keyClick(shown.canvas, Qt::Key_Backspace, Qt::ShiftModifier | Qt::ControlModifier);
|
||
QVERIFY(!session.filterEdit().has_value());
|
||
}
|
||
|
||
void SelectionCanvasTests::aCtrlDragMovesSelectedPixelsAndCtrlArrowsNudgeThem()
|
||
{
|
||
Canvas shown;
|
||
EditorSession &session = shown.session;
|
||
QImage halves = BrushRaster::context(400, 300, false);
|
||
QPainter painter(&halves);
|
||
painter.fillRect(QRect(0, 0, 200, 300), Qt::red);
|
||
painter.fillRect(QRect(200, 0, 200, 300), Qt::blue);
|
||
painter.end();
|
||
session.insert(ImportedImage(halves, halves, "Halves"));
|
||
session.selectTool(NavigationTool::marquee);
|
||
session.applySelection(rectPath(QRectF(20, 20, 40, 40)), SelectionMode::replace, "Select");
|
||
shown.canvas->synchronizeDisplay();
|
||
const int count = session.history.undoCount();
|
||
// Ctrl inside: the pixels lift and follow the pointer.
|
||
shown.press(QPointF(40, 40), Qt::ControlModifier);
|
||
QVERIFY(session.pixelMove());
|
||
shown.move(QPointF(260, 40), Qt::ControlModifier);
|
||
const QImage moving = shown.canvas->grab().toImage();
|
||
QCOMPARE(moving.pixelColor(260, 40), QColor(Qt::red));
|
||
QVERIFY(moving.pixelColor(40, 40) != QColor(Qt::red));
|
||
QCOMPARE(session.displayedSelection().value().path.boundingRect(), QRectF(240, 20, 40, 40));
|
||
shown.release(QPointF(260, 40), Qt::ControlModifier);
|
||
QTRY_VERIFY(!session.pixelMove());
|
||
QCOMPARE(session.history.undoCount(), count + 1);
|
||
QCOMPARE(session.history.undoName(), QString("Move Pixels"));
|
||
// Ctrl-arrow in any tool, ten with Shift.
|
||
session.selectTool(NavigationTool::brush);
|
||
QTRY_VERIFY(shown.canvas->hasFocus());
|
||
QTest::keyClick(shown.canvas, Qt::Key_Right, Qt::ControlModifier | Qt::ShiftModifier);
|
||
QTRY_COMPARE(session.history.undoCount(), count + 2);
|
||
QCOMPARE(session.selection().value().path.boundingRect(), QRectF(250, 20, 40, 40));
|
||
// With Alt the arrow is not the nudge's.
|
||
QTest::keyClick(shown.canvas, Qt::Key_Right, Qt::ControlModifier | Qt::AltModifier);
|
||
QCOMPARE(session.history.undoCount(), count + 2);
|
||
// Ctrl-Alt copies; losing the keys mid-drag cancels.
|
||
session.selectTool(NavigationTool::marquee);
|
||
// The nudge's move ends a turn after its step lands.
|
||
QTRY_VERIFY(!session.pixelMove());
|
||
shown.press(QPointF(270, 40), Qt::ControlModifier | Qt::AltModifier);
|
||
QVERIFY(session.pixelMove() && session.pixelMove()->duplicate);
|
||
shown.move(QPointF(300, 40), Qt::ControlModifier | Qt::AltModifier);
|
||
QFocusEvent out(QEvent::FocusOut, Qt::ActiveWindowFocusReason);
|
||
QApplication::sendEvent(shown.canvas, &out);
|
||
QVERIFY(!session.pixelMove());
|
||
shown.release(QPointF(300, 40));
|
||
QCOMPARE(session.history.undoCount(), count + 2);
|
||
// Ctrl outside the selection lifts nothing.
|
||
shown.press(QPointF(380, 280), Qt::ControlModifier);
|
||
QVERIFY(!session.pixelMove());
|
||
shown.release(QPointF(380, 280), Qt::ControlModifier);
|
||
// An open outline keeps the pixels still under Ctrl-arrows.
|
||
session.applySelection(rectPath(QRectF(250, 20, 40, 40)), SelectionMode::replace, "Select");
|
||
QVERIFY(session.selection().has_value());
|
||
session.selectTool(NavigationTool::lasso);
|
||
session.setLassoKind(LassoKind::polygonal);
|
||
shown.click(QPointF(300, 200));
|
||
QVERIFY(session.lassoDraft().has_value());
|
||
const int drafting = session.history.undoCount();
|
||
QTest::keyClick(shown.canvas, Qt::Key_Right, Qt::ControlModifier);
|
||
QTest::qWait(50);
|
||
QCOMPARE(session.history.undoCount(), drafting);
|
||
QVERIFY(session.lassoDraft().has_value() && !session.pixelMove());
|
||
}
|
||
|
||
void SelectionCanvasTests::shiftKeepsMovedPixelsOnALine()
|
||
{
|
||
Canvas shown;
|
||
EditorSession &session = shown.session;
|
||
QImage red = BrushRaster::context(400, 300, false);
|
||
red.fill(Qt::red);
|
||
session.insert(ImportedImage(red, red, "Red"));
|
||
session.applySelection(rectPath(QRectF(20, 20, 40, 40)), SelectionMode::replace, "Select");
|
||
shown.canvas->synchronizeDisplay();
|
||
const auto box = [&] { return session.displayedSelection().value().path.boundingRect(); };
|
||
shown.press(QPointF(40, 40), Qt::ControlModifier);
|
||
QVERIFY(session.pixelMove());
|
||
// Across: the drag went further that way.
|
||
shown.move(QPointF(100, 55), Qt::ControlModifier | Qt::ShiftModifier);
|
||
QCOMPARE(box(), QRectF(80, 20, 40, 40));
|
||
// Down, then a tie keeps across.
|
||
shown.move(QPointF(50, 100), Qt::ControlModifier | Qt::ShiftModifier);
|
||
QCOMPARE(box(), QRectF(20, 80, 40, 40));
|
||
shown.move(QPointF(10, 10), Qt::ControlModifier | Qt::ShiftModifier);
|
||
QCOMPARE(box(), QRectF(-10, 20, 40, 40));
|
||
// Lengths, not signs: left beats a shorter down.
|
||
shown.move(QPointF(0, 50), Qt::ControlModifier | Qt::ShiftModifier);
|
||
QCOMPARE(box(), QRectF(-20, 20, 40, 40));
|
||
// And up beats a shorter right.
|
||
shown.move(QPointF(45, 0), Qt::ControlModifier | Qt::ShiftModifier);
|
||
QCOMPARE(box(), QRectF(20, -20, 40, 40));
|
||
// Without Shift both ways.
|
||
shown.move(QPointF(60, 55), Qt::ControlModifier);
|
||
QCOMPARE(box(), QRectF(40, 35, 40, 40));
|
||
shown.move(QPointF(70, 45), Qt::ControlModifier | Qt::ShiftModifier);
|
||
shown.release(QPointF(70, 45), Qt::ControlModifier | Qt::ShiftModifier);
|
||
QTRY_VERIFY(!session.pixelMove());
|
||
QCOMPARE(session.selection().value().path.boundingRect(), QRectF(50, 20, 40, 40));
|
||
}
|
||
|
||
QTEST_MAIN(SelectionCanvasTests)
|
||
#include "SelectionCanvasTests.moc"
|