Files
OmaPhoto/tests/SelectionCanvasTests.cpp
T

363 lines
17 KiB
C++
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
#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"