Files
OmaPhoto/tests/TransformPressTests.cpp
T

470 lines
21 KiB
C++

#include "CanvasFixtures.h"
// The Move tool on the canvas: drags, handles, snapping.
namespace {
// The document fills the canvas: view points are pixels.
struct Canvas : Shown {
QUuid red;
Canvas() : Shown(QSize(400, 300), QSize(400, 300))
{
settle();
session.zoom(1);
session.insert(filled(100, 100, qRgba(255, 0, 0, 255), "Red"));
red = session.activeLayerID().value();
session.selectTool(NavigationTool::move);
canvas->synchronizeDisplay();
if (session.viewport.viewPoint(QPointF(0, 0), documentSize()) != QPointF(0, 0))
throw std::runtime_error("the document does not fill the canvas");
}
QPointF origin() const { return layerWith(session, red).transform.origin; }
void press(QPointF at, Qt::KeyboardModifiers modifiers = Qt::NoModifier) { QTest::mousePress(canvas, Qt::LeftButton, modifiers, at.toPoint()); }
void move(QPointF to, Qt::KeyboardModifiers modifiers = Qt::NoModifier)
{
QMouseEvent event(QEvent::MouseMove, to, to, canvas->mapToGlobal(to.toPoint()), Qt::NoButton, Qt::LeftButton, modifiers);
QApplication::sendEvent(canvas, &event);
}
void release(QPointF at, Qt::KeyboardModifiers modifiers = Qt::NoModifier) { QTest::mouseRelease(canvas, Qt::LeftButton, modifiers, at.toPoint()); }
void drag(QPointF from, QPointF to, Qt::KeyboardModifiers modifiers = Qt::NoModifier)
{
press(from, modifiers);
move((from + to) / 2, modifiers);
move(to, modifiers);
release(to, modifiers);
}
void click(QPointF at, Qt::KeyboardModifiers modifiers = Qt::NoModifier)
{
press(at, modifiers);
release(at, modifiers);
}
};
}
class TransformPressTests : public QObject {
Q_OBJECT
private slots:
void draggingOutsideTheLayerMovesIt();
void altDraggingOutsideTheLayerDuplicatesIt();
void handlesResizeAndTheGripRotates();
void movesSnapToTheCanvasAndOtherLayersUnlessCtrl();
void autoSelectCanBeDisabledAndCtrlClickOverridesIt();
void autoSelectPicksTheForegroundOverASelectedBackground();
void escapeMidDragRestoresAndAPersistentEditWaitsForReturn();
void losingTheKeysOrTheWheelMidDrag();
void aSelectionDragsAsOneBox();
void ctrlFlipsAutoSelectWhileHeld_data();
void ctrlFlipsAutoSelectWhileHeld();
void ctrlShiftClickAddsALayerWithAutoSelectOn();
void aCanvasDragAppliesTheFieldsEditFirst();
};
void TransformPressTests::draggingOutsideTheLayerMovesIt()
{
Canvas shown;
QCOMPARE(shown.origin(), QPointF(150, 100));
// Thirteen down keeps the middle clear of the canvas's snap.
shown.drag(QPointF(20, 20), QPointF(40, 33));
QVERIFY(!shown.session.transformEdit().has_value());
QCOMPARE(shown.origin(), QPointF(170, 113));
QCOMPARE(shown.session.history.undoName(), QString("Transform Layer"));
// A press that never moves changes nothing and records nothing.
const int steps = shown.session.history.undoCount();
shown.click(QPointF(20, 20));
QCOMPARE(shown.session.history.undoCount(), steps);
QCOMPARE(shown.origin(), QPointF(170, 113));
// At 200% drags land on whole pixels; snapping halves.
shown.session.zoom(2);
shown.canvas->synchronizeDisplay();
shown.drag(QPointF(20, 20), QPointF(66, 20));
QCOMPARE(shown.origin(), QPointF(193, 113));
shown.drag(QPointF(20, 20), QPointF(21, 20));
QCOMPARE(shown.origin(), QPointF(194, 113));
shown.session.zoom(1);
shown.canvas->synchronizeDisplay();
// Nothing drags while the project is busy.
shown.session.setIsProjectBusy(true);
shown.drag(QPointF(20, 20), QPointF(60, 20));
QCOMPARE(shown.origin(), QPointF(194, 113));
}
void TransformPressTests::altDraggingOutsideTheLayerDuplicatesIt()
{
Canvas shown;
shown.drag(QPointF(20, 20), QPointF(40, 33), Qt::AltModifier);
const std::vector<ImageLayer> &layers = shown.session.document().value().layers;
QCOMPARE(int(layers.size()), 2);
QCOMPARE(layers[0].transform.origin, QPointF(150, 100));
QCOMPARE(layers[1].transform.origin, QPointF(170, 113));
QCOMPARE(layers[1].name, QString("Red copy"));
QVERIFY(!shown.session.transformEdit().has_value());
QCOMPARE(shown.session.history.undoName(), QString("Duplicate Layer"));
shown.session.undo();
QCOMPARE(int(shown.session.document().value().layers.size()), 1);
// Alt on a handle resizes about the middle, no copy.
shown.drag(QPointF(250, 200), QPointF(300, 250), Qt::AltModifier);
QCOMPARE(int(shown.session.document().value().layers.size()), 1);
QCOMPARE(layerWith(shown.session, shown.red).transform, (LayerTransform{.origin = {100, 50}, .size = {200, 200}}));
}
void TransformPressTests::handlesResizeAndTheGripRotates()
{
Canvas shown;
// Shift frees a locked corner: each side follows the pointer.
shown.drag(QPointF(250, 200), QPointF(300, 225), Qt::ShiftModifier);
QCOMPARE(layerWith(shown.session, shown.red).transform, (LayerTransform{.origin = {150, 100}, .size = {150, 125}}));
shown.session.undo();
// The bottom right corner dragged out: both sides grow, locked.
shown.drag(QPointF(250, 200), QPointF(300, 250));
LayerTransform transform = layerWith(shown.session, shown.red).transform;
QCOMPARE(transform.size, QSizeF(150, 150));
QCOMPARE(transform.origin, QPointF(150, 100));
shown.session.setLocksTransformRatio(false);
shown.drag(QPointF(300, 250), QPointF(300, 200));
transform = layerWith(shown.session, shown.red).transform;
QCOMPARE(transform.size, QSizeF(150, 100));
// An edge, away from its handle, resizes too.
shown.drag(QPointF(300, 170), QPointF(320, 170));
QCOMPARE(layerWith(shown.session, shown.red).transform.size, QSizeF(170, 100));
// The dragged edge snaps: five past the middle lands there.
shown.drag(QPointF(320, 170), QPointF(205, 170));
QCOMPARE(layerWith(shown.session, shown.red).transform, (LayerTransform{.origin = {150, 100}, .size = {50, 100}}));
// The grip: a quarter turn about the middle.
const QPointF center = layerWith(shown.session, shown.red).transform.center();
const QPointF grip = center + QPointF(0, -78);
QVERIFY(center == QPointF(175, 150));
shown.drag(grip, center + QPointF(78, 0));
transform = layerWith(shown.session, shown.red).transform;
QCOMPARE(transform.rotation, 90.0);
QCOMPARE(transform.center(), center);
QCOMPARE(shown.session.history.undoName(), QString("Transform Layer"));
}
void TransformPressTests::movesSnapToTheCanvasAndOtherLayersUnlessCtrl()
{
Canvas shown;
EditorSession &session = shown.session;
// Three short of the middle, the left edge snaps.
shown.press(QPointF(20, 20));
shown.move(QPointF(73, 20));
QCOMPARE(session.transformEdit().value().draft.origin, QPointF(200, 100));
QCOMPARE(session.snapGuides, (SnapGuides{{200}, {150}}));
shown.release(QPointF(73, 20));
QCOMPARE(shown.origin(), QPointF(200, 100));
QCOMPARE(session.snapGuides, SnapGuides{});
// Ctrl drags freely.
shown.press(QPointF(20, 20));
shown.move(QPointF(23, 20), Qt::ControlModifier);
QCOMPARE(session.transformEdit().value().draft.origin, QPointF(203, 100));
QCOMPARE(session.snapGuides, SnapGuides{});
shown.release(QPointF(23, 20), Qt::ControlModifier);
QCOMPARE(shown.origin(), QPointF(203, 100));
// Another layer's edge is the nearest target now.
session.insert(filled(50, 50, qRgba(0, 0, 255, 255), "Blue"), QPointF(330, 50));
session.selectLayer(shown.red);
shown.press(QPointF(20, 20));
shown.move(QPointF(21, 20));
QCOMPARE(session.transformEdit().value().draft.origin, QPointF(205, 100));
QCOMPARE(session.snapGuides, (SnapGuides{{305}, {150}}));
shown.release(QPointF(21, 20));
QCOMPARE(shown.origin(), QPointF(205, 100));
}
void TransformPressTests::autoSelectCanBeDisabledAndCtrlClickOverridesIt()
{
Canvas shown;
EditorSession &session = shown.session;
session.insert(filled(100, 100, qRgba(0, 0, 255, 255), "Blue"), QPointF(300, 50));
const QUuid blue = session.activeLayerID().value();
session.selectLayer(shown.red);
QVERIFY(!session.transformAutoSelect());
shown.click(QPointF(300, 50));
QCOMPARE(session.activeLayerID(), std::optional(shown.red));
session.setTransformAutoSelect(true);
shown.click(QPointF(300, 50));
QCOMPARE(session.activeLayerID(), std::optional(blue));
session.selectLayer(shown.red);
session.setTransformAutoSelect(false);
shown.click(QPointF(300, 50));
QCOMPARE(session.activeLayerID(), std::optional(shown.red));
shown.click(QPointF(300, 50), Qt::ControlModifier);
QCOMPARE(session.activeLayerID(), std::optional(blue));
QVERIFY(!session.transformAutoSelect());
// Ctrl+Shift adds the layer under the pointer to the selection.
shown.click(QPointF(200, 150), Qt::ControlModifier | Qt::ShiftModifier);
QCOMPARE(session.selectedLayerIDs(), (QSet<QUuid>{shown.red, blue}));
// With auto-select, empty canvas still drags the active layer.
session.selectLayer(shown.red);
session.setTransformAutoSelect(true);
shown.drag(QPointF(20, 250), QPointF(33, 250));
QCOMPARE(shown.origin(), QPointF(163, 100));
// Inside the active layer's box a layer above wins, auto-selecting.
session.insert(filled(100, 100, qRgba(0, 255, 0, 255), "Green"), QPointF(220, 120));
const QUuid green = session.activeLayerID().value();
session.selectLayer(shown.red);
shown.click(QPointF(213, 150));
QCOMPARE(session.activeLayerID(), std::optional(green));
session.selectLayer(shown.red);
session.setTransformAutoSelect(false);
shown.click(QPointF(213, 150));
QCOMPARE(session.activeLayerID(), std::optional(shown.red));
// Ctrl picks what lies under the pointer, active included.
shown.click(QPointF(213, 150), Qt::ControlModifier);
QCOMPARE(session.activeLayerID(), std::optional(green));
session.selectLayer(shown.red);
session.setTransformAutoSelect(true);
// A selection's box drags them all; outside it auto-select picks.
session.insert(filled(100, 100, qRgba(255, 255, 0, 255), "Yellow"), QPointF(60, 260));
const QUuid yellow = session.activeLayerID().value();
session.selectLayers({shown.red, blue}, shown.red);
shown.click(QPointF(300, 50));
QCOMPARE(session.selectedLayerIDs(), (QSet<QUuid>{shown.red, blue}));
shown.click(QPointF(60, 260));
QCOMPARE(session.activeLayerID(), std::optional(yellow));
QCOMPARE(session.selectedLayerIDs(), QSet<QUuid>{yellow});
}
void TransformPressTests::autoSelectPicksTheForegroundOverASelectedBackground()
{
Shown shown(QSize(400, 300), QSize(400, 300));
shown.settle();
EditorSession &session = shown.session;
session.zoom(1);
session.insert(filled(400, 300, qRgba(0, 0, 255, 255), "Sky"));
const QUuid background = session.activeLayerID().value();
session.insert(filled(80, 60, qRgba(255, 0, 0, 255), "Kite"), QPointF(190, 80));
const QUuid foreground = session.activeLayerID().value();
session.selectTool(NavigationTool::move);
shown.canvas->synchronizeDisplay();
const auto click = [&](QPoint at) {
QTest::mouseClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, at);
session.commitTransform();
};
// Swift's test: the background holds every press; the kite wins.
session.selectLayer(background);
session.setTransformAutoSelect(true);
click(QPoint(180, 70));
QCOMPARE(session.activeLayerID(), std::optional(foreground));
click(QPoint(20, 20));
QCOMPARE(session.activeLayerID(), std::optional(background));
click(QPoint(180, 70));
QCOMPARE(session.activeLayerID(), std::optional(foreground));
session.selectLayer(background);
session.setTransformAutoSelect(false);
click(QPoint(180, 70));
QCOMPARE(session.activeLayerID(), std::optional(background));
// A Ctrl+T edit keeps its layer; a plain press picks.
session.setTransformAutoSelect(true);
session.beginTransform();
QTest::mouseClick(shown.canvas, Qt::LeftButton, Qt::NoModifier, QPoint(180, 70));
QCOMPARE(session.activeLayerID(), std::optional(background));
session.cancelTransform();
session.setTransformAutoSelect(false);
// A wide mask box keeps its layer over lower ones.
session.selectLayer(foreground);
session.addMask();
session.toggleMaskLink(foreground);
session.selectLayerTarget(foreground, true);
QVERIFY(session.transformTargetsMask());
session.beginTransform();
session.previewTransform(LayerTransform(QPointF(40, 20), QSizeF(300, 200)));
session.commitTransform();
session.setTransformAutoSelect(true);
click(QPoint(60, 40));
QCOMPARE(session.activeLayerID(), std::optional(foreground));
// Over empty canvas nothing lies under the pointer.
session.toggleLayerVisibility(background);
click(QPoint(60, 40));
QCOMPARE(session.activeLayerID(), std::optional(foreground));
}
void TransformPressTests::escapeMidDragRestoresAndAPersistentEditWaitsForReturn()
{
Canvas shown;
EditorSession &session = shown.session;
QTRY_VERIFY(shown.canvas->hasFocus());
shown.press(QPointF(20, 20));
shown.move(QPointF(55, 40));
QCOMPARE(session.transformEdit().value().draft.origin, QPointF(185, 120));
QVERIFY(!session.transformEdit().value().persistent);
QTest::keyClick(shown.canvas, Qt::Key_Escape);
QVERIFY(!session.transformEdit().has_value());
shown.release(QPointF(55, 40));
QCOMPARE(shown.origin(), QPointF(150, 100));
// Ctrl+T's edit outlives the drag until Return applies it.
session.beginTransform();
QVERIFY(session.transformEdit().value().persistent);
shown.drag(QPointF(20, 20), QPointF(40, 33));
QVERIFY(session.transformEdit().has_value());
QCOMPARE(session.transformEdit().value().draft.origin, QPointF(170, 113));
QCOMPARE(shown.origin(), QPointF(150, 100));
QTest::keyClick(shown.canvas, Qt::Key_Return);
QVERIFY(!session.transformEdit().has_value());
QCOMPARE(shown.origin(), QPointF(170, 113));
// A snapped drag under Ctrl+T: guides go with the release.
session.beginTransform();
shown.press(QPointF(20, 20));
shown.move(QPointF(53, 20));
QCOMPARE(session.transformEdit().value().draft.origin, QPointF(200, 113));
QCOMPARE(session.snapGuides, (SnapGuides{{200}, {}}));
// The release repaints the guide away, though nothing commits.
struct PaintSpy : QObject {
std::vector<QRect> rects;
bool eventFilter(QObject *, QEvent *event) override
{
if (event->type() == QEvent::Paint)
rects.push_back(static_cast<QPaintEvent *>(event)->region().boundingRect());
return false;
}
} spy;
QCoreApplication::processEvents();
shown.canvas->installEventFilter(&spy);
shown.release(QPointF(53, 20));
QVERIFY(session.transformEdit().has_value());
QCOMPARE(session.snapGuides, SnapGuides{});
QCoreApplication::processEvents();
shown.canvas->removeEventFilter(&spy);
QVERIFY(std::any_of(spy.rects.begin(), spy.rects.end(), [&](const QRect &rect) { return rect == shown.canvas->rect(); }));
// Escape mid-resize puts the layer and the cursor back.
shown.press(QPointF(300, 213));
QCOMPARE(shown.canvas->cursor().shape(), Qt::SizeFDiagCursor);
shown.move(QPointF(350, 263));
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(150, 150));
QTest::keyClick(shown.canvas, Qt::Key_Escape);
QVERIFY(!session.transformEdit().has_value());
QVERIFY(shown.canvas->cursor().shape() != Qt::SizeFDiagCursor);
shown.release(QPointF(350, 263));
QCOMPARE(layerWith(session, shown.red).transform.size, QSizeF(100, 100));
}
void TransformPressTests::losingTheKeysOrTheWheelMidDrag()
{
Canvas shown;
EditorSession &session = shown.session;
shown.press(QPointF(20, 20));
shown.move(QPointF(60, 40));
// The wheel neither pans nor zooms mid-drag.
const CanvasViewport before = session.viewport;
wheel(*shown.canvas, QPointF(100, 100), QPoint(0, 30), QPoint(0, 120), Qt::NoModifier);
wheel(*shown.canvas, QPointF(100, 100), QPoint(0, 30), QPoint(0, 120), Qt::ControlModifier);
QVERIFY(session.viewport == before);
// Nor does a pinch.
QNativeGestureEvent pinch(Qt::ZoomNativeGesture, QPointingDevice::primaryPointingDevice(), 2, QPointF(100, 100), QPointF(100, 100),
shown.canvas->mapToGlobal(QPoint(100, 100)), 0.5, QPointF());
QApplication::sendEvent(shown.canvas, &pinch);
QVERIFY(session.viewport == before);
// The keys lost mid-drag: the layer goes back.
shown.canvas->clearFocus();
QVERIFY(!session.transformEdit().has_value());
QCOMPARE(shown.origin(), QPointF(150, 100));
shown.release(QPointF(60, 40));
QCOMPARE(shown.origin(), QPointF(150, 100));
wheel(*shown.canvas, QPointF(100, 100), QPoint(0, 30), QPoint(0, 120), Qt::NoModifier);
QVERIFY(!(session.viewport == before));
// Under Ctrl+T the edit stays, its draft put back.
session.viewport = before;
session.beginTransform();
shown.press(QPointF(20, 20));
shown.move(QPointF(55, 40));
QCOMPARE(session.transformEdit().value().draft.origin, QPointF(185, 120));
shown.canvas->clearFocus();
QVERIFY(session.transformEdit().value().persistent);
QCOMPARE(session.transformEdit().value().draft.origin, QPointF(150, 100));
shown.release(QPointF(55, 40));
QVERIFY(session.transformEdit().has_value());
}
void TransformPressTests::aSelectionDragsAsOneBox()
{
Canvas shown;
EditorSession &session = shown.session;
session.insert(filled(50, 50, qRgba(0, 0, 255, 255), "Blue"), QPointF(325, 275));
const QUuid blue = session.activeLayerID().value();
session.selectLayers({shown.red, blue}, shown.red);
shown.drag(QPointF(20, 20), QPointF(30, 32));
QCOMPARE(shown.origin(), QPointF(160, 112));
QCOMPARE(layerWith(session, blue).transform.origin, QPointF(310, 262));
QCOMPARE(session.history.undoName(), QString("Transform Layers"));
QCOMPARE(session.selectedLayerIDs(), (QSet<QUuid>{shown.red, blue}));
}
// A full-canvas blue layer under the red, and active.
static QUuid blueBelow(Canvas &shown)
{
shown.session.insert(filled(400, 300, qRgba(0, 0, 255, 255), "Blue"));
const QUuid blue = shown.session.activeLayerID().value();
shown.session.reorderLayers({1}, 0);
if (shown.session.document().value().layers[0].id != blue || shown.session.activeLayerID() != blue)
throw std::runtime_error("the blue layer is not active at the bottom");
shown.canvas->synchronizeDisplay();
return blue;
}
void TransformPressTests::ctrlFlipsAutoSelectWhileHeld_data()
{
QTest::addColumn<bool>("autoSelect");
QTest::newRow("off") << false;
QTest::newRow("on") << true;
}
// Swift's commandFlipsAutoSelectWhileHeld: Ctrl turns it the other way.
void TransformPressTests::ctrlFlipsAutoSelectWhileHeld()
{
QFETCH(bool, autoSelect);
Canvas shown;
const QUuid blue = blueBelow(shown);
shown.session.setTransformAutoSelect(autoSelect);
shown.drag(QPointF(210, 170), QPointF(230, 180), Qt::ControlModifier);
const QUuid moved = autoSelect ? blue : shown.red;
QCOMPARE(shown.session.activeLayerID(), std::optional(moved));
QCOMPARE(layerWith(shown.session, moved).transform.origin, autoSelect ? QPointF(20, 10) : QPointF(170, 110));
QCOMPARE(layerWith(shown.session, autoSelect ? shown.red : blue).transform.origin, autoSelect ? QPointF(150, 100) : QPointF(0, 0));
// Without Ctrl it picks as set.
shown.session.undo();
shown.session.selectLayer(blue);
shown.drag(QPointF(210, 170), QPointF(230, 180));
QCOMPARE(shown.session.activeLayerID(), std::optional(autoSelect ? shown.red : blue));
}
void TransformPressTests::ctrlShiftClickAddsALayerWithAutoSelectOn()
{
Canvas shown;
const QUuid blue = blueBelow(shown);
shown.session.setTransformAutoSelect(true);
shown.click(QPointF(210, 170), Qt::ControlModifier | Qt::ShiftModifier);
QCOMPARE(shown.session.selectedLayerIDs(), (QSet<QUuid>{shown.red, blue}));
// Past the active layer's box, Ctrl still keeps it.
shown.session.insert(filled(50, 50, qRgba(0, 255, 0, 255), "Green"), QPointF(330, 20));
shown.session.selectLayer(shown.red);
shown.drag(QPointF(340, 30), QPointF(353, 30), Qt::ControlModifier);
QCOMPARE(shown.session.activeLayerID(), std::optional(shown.red));
QCOMPARE(shown.origin(), QPointF(163, 100));
}
// The bar's pending value applies first, its own step.
void TransformPressTests::aCanvasDragAppliesTheFieldsEditFirst()
{
Canvas shown;
EditorSession &session = shown.session;
const int steps = session.history.undoCount();
session.beginTransform(false, true);
LayerTransform flipped = session.transformEdit().value().draft;
flipped.flipX = true;
session.previewTransform(flipped);
shown.drag(QPointF(20, 20), QPointF(40, 33));
QVERIFY(!session.transformEdit().has_value());
QCOMPARE(session.history.undoCount(), steps + 2);
QCOMPARE(shown.origin(), QPointF(170, 113));
QVERIFY(layerWith(session, shown.red).transform.flipX);
session.undo();
QCOMPARE(shown.origin(), QPointF(150, 100));
QVERIFY(layerWith(session, shown.red).transform.flipX);
// A Ctrl+T edit takes the drag into itself.
session.undo();
session.beginTransform();
shown.drag(QPointF(20, 20), QPointF(40, 33));
QCOMPARE(session.transformEdit().value().draft.origin, QPointF(170, 113));
QCOMPARE(session.history.undoCount(), steps);
}
QTEST_MAIN(TransformPressTests)
#include "TransformPressTests.moc"