Move bar: held Ctrl and Shift flip Auto Select and the lock, values apply as one step, resize handles snap (13.13)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 16:56:16 -04:00
1 parent 765e071c0c
commit b50657bcdb
17 files changed
+703 -78

No files matched your search

+85
View File
@@ -0,0 +1,85 @@
#include "CanvasFixtures.h"
// Swift's resize snapping on the canvas: handles, Ctrl, Shift.
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 ResizeSnapCanvasTests : public QObject {
Q_OBJECT
private slots:
void resizedEdgesSnapUnlessCtrl();
};
void ResizeSnapCanvasTests::resizedEdgesSnapUnlessCtrl()
{
Canvas shown;
EditorSession &session = shown.session;
session.setLocksTransformRatio(false);
// The right edge, from 250 to four short of 400.
shown.press(QPointF(250, 150));
shown.move(QPointF(396, 150));
QCOMPARE(session.transformEdit().value().draft, (LayerTransform{.origin = {150, 100}, .size = {250, 100}}));
QCOMPARE(session.snapGuides, (SnapGuides{{400}, {}}));
// Ctrl mid-drag frees it.
shown.move(QPointF(396, 150), Qt::ControlModifier);
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(246, 100));
shown.release(QPointF(396, 150), Qt::ControlModifier);
QCOMPARE(layerWith(session, shown.red).transform.size, QSizeF(246, 100));
// Snapping off, nothing snaps.
session.undo();
session.setSnappingEnabled(false);
shown.drag(QPointF(250, 150), QPointF(396, 150));
QCOMPARE(layerWith(session, shown.red).transform.size, QSizeF(246, 100));
QCOMPARE(session.snapGuides, SnapGuides{});
// Locked, Shift frees the lock: a corner snaps both edges.
session.undo();
session.setSnappingEnabled(true);
session.setLocksTransformRatio(true);
shown.press(QPointF(250, 200));
shown.move(QPointF(396, 296), Qt::ShiftModifier);
QCOMPARE(session.transformEdit().value().draft, (LayerTransform{.origin = {150, 100}, .size = {250, 200}}));
QCOMPARE(session.snapGuides, (SnapGuides{{400}, {300}}));
// Kept proportional, only the nearer edge snaps.
shown.move(QPointF(397, 349));
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(250, 250));
QCOMPARE(session.snapGuides, (SnapGuides{{400}, {}}));
shown.release(QPointF(397, 349));
}
QTEST_MAIN(ResizeSnapCanvasTests)
#include "ResizeSnapCanvasTests.moc"
+157
View File
@@ -0,0 +1,157 @@
#include "SessionFixtures.h"
#include <QtTest>
// Swift's ResizeSnapTests: a resized layer's edges snap, as moves do.
namespace {
struct Resized {
EditorSession session;
QUuid layer;
// 300 by 200: the layer at 10; another at 150.
Resized()
{
session.createDocument(300, 200);
place(QPointF(150, 150), QSizeF(40, 40));
place(QPointF(10, 10), QSizeF(100, 100));
layer = session.activeLayerID().value();
}
void place(QPointF origin, QSizeF size)
{
session.insert(ImportedImage(QImage(20, 20, QImage::Format_RGBA8888_Premultiplied), QImage(), "Layer"));
session.beginTransform();
session.previewTransform(LayerTransform{.origin = origin, .size = size});
session.commitTransform();
}
LayerTransform resize(int handle, QPointF start, QPointF point, bool lockRatio)
{
const TransformDrag drag{layerWith(session, layer).transform, start, {TransformDrag::Kind::resize, handle}, std::nullopt};
const QPointF snapped = session.snappedResizePoint(point, drag, lockRatio, {layer}, 5, [&](QPointF to) {
return drag.updated(to, lockRatio, false);
});
return drag.updated(snapped, lockRatio, false).rounded();
}
};
}
class ResizeSnapTests : public QObject {
Q_OBJECT
private slots:
void aSideHandleSnapsItsEdge();
void aProportionalCornerSnapsItsNearerEdgeAndKeepsTheRatio();
void aTurnedLayerDoesntSnap();
void eachEdgeSnapsOnItsOwn();
void theDraggedSideAloneSnaps();
void snappingOffOrAnotherDragLeavesThePoint();
};
void ResizeSnapTests::aSideHandleSnapsItsEdge()
{
Resized resized;
QCOMPARE(LayerTransform::handles[3], QPointF(1, 0.5));
// The right edge, three short of the other layer's left.
const LayerTransform result = resized.resize(3, QPointF(110, 60), QPointF(147, 60), false);
QCOMPARE(result.origin.x() + result.size.width(), 150.0);
QCOMPARE(result.size.height(), 100.0);
QCOMPARE(resized.session.snapGuides, (SnapGuides{{150}, {}}));
// Out of reach, it does not.
const LayerTransform free = resized.resize(3, QPointF(110, 60), QPointF(130, 60), false);
QCOMPARE(free.origin.x() + free.size.width(), 130.0);
QCOMPARE(resized.session.snapGuides, SnapGuides{});
// The left handle snaps the left edge, to the canvas.
const LayerTransform left = resized.resize(7, QPointF(10, 60), QPointF(3, 60), false);
QCOMPARE(left.origin.x(), 0.0);
QCOMPARE(left.origin.x() + left.size.width(), 110.0);
}
void ResizeSnapTests::aProportionalCornerSnapsItsNearerEdgeAndKeepsTheRatio()
{
Resized resized;
// Bottom right toward 146, 148: the bottom edge snaps.
const LayerTransform result = resized.resize(4, QPointF(110, 110), QPointF(146, 148), true);
QCOMPARE(result.origin.y() + result.size.height(), 150.0);
QVERIFY(std::abs(result.size.width() - result.size.height()) <= 1);
// A tie goes to the right edge, the first.
QCOMPARE(resized.session.snapGuides, (SnapGuides{{150}, {}}));
// A top at 148 lies nearer the bottom: it snaps.
resized.place(QPointF(260, 148), QSizeF(10, 10));
resized.session.selectLayer(resized.layer);
const LayerTransform bottom = resized.resize(4, QPointF(110, 110), QPointF(147, 147), true);
QCOMPARE(bottom, (LayerTransform{.origin = {10, 10}, .size = {138, 138}}));
QCOMPARE(resized.session.snapGuides, (SnapGuides{{}, {148}}));
// Free, both edges snap, each to its nearest.
const LayerTransform free = resized.resize(4, QPointF(110, 110), QPointF(147, 147), false);
QCOMPARE(free, (LayerTransform{.origin = {10, 10}, .size = {140, 138}}));
}
void ResizeSnapTests::aTurnedLayerDoesntSnap()
{
Resized resized;
LayerTransform turned = layerWith(resized.session, resized.layer).transform;
turned.rotation = 20;
const TransformDrag drag{turned, QPointF(110, 60), {TransformDrag::Kind::resize, 3}, std::nullopt};
// Wherever the handle goes, the pointer stays put.
for (double x = 100; x <= 200; x += 0.5) {
resized.session.snapGuides = SnapGuides{{1}, {2}};
const QPointF point(x, 60);
QCOMPARE(resized.session.snappedResizePoint(point, drag, false, {resized.layer}, 5, [&](QPointF to) { return drag.updated(to, false, false); }),
point);
QCOMPARE(resized.session.snapGuides, SnapGuides{});
}
}
void ResizeSnapTests::eachEdgeSnapsOnItsOwn()
{
Resized resized;
// Free, a corner snaps both edges, each to its line.
const LayerTransform result = resized.resize(4, QPointF(110, 110), QPointF(146, 147), false);
QCOMPARE(result, (LayerTransform{.origin = {10, 10}, .size = {140, 140}}));
QCOMPARE(resized.session.snapGuides, (SnapGuides{{150}, {150}}));
// The top left corner moves the top and left edges.
const LayerTransform corner = resized.resize(0, QPointF(10, 10), QPointF(2, 4), false);
QCOMPARE(corner, (LayerTransform{.origin = {0, 0}, .size = {110, 110}}));
// An edge no step moves keeps the pointer; guide shown.
const TransformDrag drag{layerWith(resized.session, resized.layer).transform, QPointF(110, 60), {TransformDrag::Kind::resize, 3}, std::nullopt};
const QPointF still = resized.session.snappedResizePoint(QPointF(147, 60), drag, false, {resized.layer}, 5, [&](QPointF) {
return LayerTransform{.origin = {10, 10}, .size = {138, 100}};
});
QCOMPARE(still, QPointF(147, 60));
QCOMPARE(resized.session.snapGuides, (SnapGuides{{150}, {}}));
}
// The dragged side tells its edge, wherever the press began.
void ResizeSnapTests::theDraggedSideAloneSnaps()
{
Resized resized;
// Pressed left of the layer, the right edge still moves.
const LayerTransform far = resized.resize(3, QPointF(-50, 60), QPointF(-13, 60), false);
QCOMPARE(far.origin.x() + far.size.width(), 150.0);
QCOMPARE(far.origin.x(), 10.0);
// A top on a line: side drags never snap it.
resized.session.beginTransform();
resized.session.previewTransform(LayerTransform{.origin = {10, 98}, .size = {100, 60}});
resized.session.commitTransform();
resized.resize(3, QPointF(110, 128), QPointF(147, 128), false);
QCOMPARE(resized.session.snapGuides, (SnapGuides{{150}, {}}));
// Nor a bottom handle the left edge, near the canvas's.
resized.session.beginTransform();
resized.session.previewTransform(LayerTransform{.origin = {2, 10}, .size = {100, 100}});
resized.session.commitTransform();
const LayerTransform down = resized.resize(5, QPointF(52, 110), QPointF(52, 148), false);
QCOMPARE(down, (LayerTransform{.origin = {2, 10}, .size = {100, 140}}));
QCOMPARE(resized.session.snapGuides, (SnapGuides{{}, {150}}));
}
void ResizeSnapTests::snappingOffOrAnotherDragLeavesThePoint()
{
Resized resized;
resized.session.setSnappingEnabled(false);
const LayerTransform result = resized.resize(3, QPointF(110, 60), QPointF(147, 60), false);
QCOMPARE(result.origin.x() + result.size.width(), 147.0);
resized.session.setSnappingEnabled(true);
const TransformDrag move{layerWith(resized.session, resized.layer).transform, QPointF(110, 60), {TransformDrag::Kind::move, 3}, std::nullopt};
QCOMPARE(resized.session.snappedResizePoint(QPointF(147, 60), move, false, {resized.layer}, 5, [&](QPointF to) { return move.updated(to, false, false); }),
QPointF(147, 60));
}
QTEST_GUILESS_MAIN(ResizeSnapTests)
#include "ResizeSnapTests.moc"
+34 -23
View File
@@ -27,62 +27,72 @@ void ScrubbableTransformTests::theTransformLabelsScrubWholeNumbersOverTyping()
bar.activateWindow();
QVERIFY(QTest::qWaitForWindowActive(&bar));
const QPointF origin = session.activeLayer().value().transform.origin;
// The first drag begins the edit, as typing does.
const int base = session.history.undoCount();
const auto shown = [&] { return session.activeLayer().value().transform; };
// Each drag applies when let go, one step.
drag(scrubbed(bar, "X"), 7);
QCOMPARE(session.transformEdit().value().draft.origin.x(), std::round(origin.x()) + 7);
QVERIFY(!session.transformEdit().has_value());
QCOMPARE(shown().origin.x(), std::round(origin.x()) + 7);
QCOMPARE(session.history.undoCount(), base + 1);
QCOMPARE(session.history.undoName(), QString("Transform Layer"));
// A scrub writes over what is typed in its field.
QLineEdit &width = find<QLineEdit>(bar, "transformW");
width.setFocus();
width.selectAll();
QTest::keyClicks(&width, QStringLiteral("3"));
drag(scrubbed(bar, "W"), 10);
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(13, 20));
QCOMPARE(shown().size, QSizeF(13, 20));
QCOMPARE(width.text(), QString("13"));
drag(scrubbed(bar, "W"), -100);
QCOMPARE(session.transformEdit().value().draft.size.width(), 1.0);
QCOMPARE(shown().size.width(), 1.0);
drag(scrubbed(bar, "H"), 40000);
QCOMPARE(session.transformEdit().value().draft.size.height(), 30000.0);
QCOMPARE(shown().size.height(), 30000.0);
drag(scrubbed(bar, "°"), 30);
QCOMPARE(session.transformEdit().value().draft.rotation, 30.0);
QCOMPARE(shown().rotation, 30.0);
drag(scrubbed(bar, "°"), -900);
QCOMPARE(session.transformEdit().value().draft.rotation, -0.0);
QCOMPARE(shown().rotation, -0.0);
// Unfocused, the field shows what the session keeps.
QCOMPARE(find<QLineEdit>(bar, "transform°").text(), QString("0"));
QCOMPARE(width.text(), QString("1"));
drag(scrubbed(bar, "Y"), -90000);
QCOMPARE(session.transformEdit().value().draft.origin.y(), -30000.0);
QCOMPARE(shown().origin.y(), -30000.0);
// A fractional start lands on a whole number.
QLineEdit &x = find<QLineEdit>(bar, "transformX");
x.setFocus();
x.selectAll();
QTest::keyClicks(&x, QStringLiteral("10.4"));
QVERIFY(session.transformEdit().value().fromFields);
QCOMPARE(session.transformEdit().value().draft.origin.x(), 10.4);
drag(scrubbed(bar, "X"), 3);
QCOMPARE(session.transformEdit().value().draft.origin.x(), 13.0);
QCOMPARE(shown().origin.x(), 13.0);
QCOMPARE(x.text(), QString("13"));
// Every range reaches its ends exactly.
drag(scrubbed(bar, "X"), -90000);
QCOMPARE(session.transformEdit().value().draft.origin.x(), -30000.0);
QCOMPARE(shown().origin.x(), -30000.0);
drag(scrubbed(bar, "X"), 90000);
QCOMPARE(session.transformEdit().value().draft.origin.x(), 30000.0);
QCOMPARE(shown().origin.x(), 30000.0);
drag(scrubbed(bar, "Y"), 90000);
QCOMPARE(session.transformEdit().value().draft.origin.y(), 30000.0);
QCOMPARE(shown().origin.y(), 30000.0);
drag(scrubbed(bar, "W"), 90000);
QCOMPARE(session.transformEdit().value().draft.size.width(), 30000.0);
QCOMPARE(shown().size.width(), 30000.0);
drag(scrubbed(bar, "H"), -90000);
QCOMPARE(session.transformEdit().value().draft.size.height(), 1.0);
QCOMPARE(shown().size.height(), 1.0);
// 360 reaches the setter, whose remainder leaves none.
drag(scrubbed(bar, "°"), 9000);
QCOMPARE(session.transformEdit().value().draft.rotation, 0.0);
QCOMPARE(shown().rotation, 0.0);
drag(scrubbed(bar, "°"), 359);
QCOMPARE(session.transformEdit().value().draft.rotation, 359.0);
QCOMPARE(shown().rotation, 359.0);
// Scale snaps whole percents too.
session.cancelTransform();
const auto restart = [&] {
while (session.history.undoCount() > base)
session.undo();
};
restart();
drag(scrubbed(bar, "Scale"), 5);
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(42, 21));
session.cancelTransform();
QCOMPARE(shown().size, QSizeF(42, 21));
restart();
drag(scrubbed(bar, "Scale"), 90000);
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(12000, 6000));
QCOMPARE(shown().size, QSizeF(12000, 6000));
QCOMPARE(find<QLineEdit>(bar, "transformScale").text(), QString("30000"));
// While distorted the numbers rest, labels included.
session.cancelTransform();
@@ -101,7 +111,7 @@ void ScrubbableTransformTests::scaleScrubsDownToATenth()
TransformInspector bar(session);
bar.show();
drag(scrubbed(bar, "Scale"), -90000);
QCOMPARE(session.transformEdit().value().draft.size, QSizeF(2, 1));
QCOMPARE(session.activeLayer().value().transform.size, QSizeF(2, 1));
QCOMPARE(find<QLineEdit>(bar, "transformScale").text(), QString("0.10"));
}
@@ -204,9 +214,10 @@ void ScrubbableTransformTests::aTransformScrubEndsWithItsLayer()
QVERIFY(!session.transformEdit().has_value());
QCOMPARE(layerWith(session, first).transform.origin.x(), x);
QCOMPARE(layerWith(session, second).transform.origin.x(), std::round(x) + 10);
// A fresh press scrubs the new layer.
// A fresh press scrubs the new layer, applied on release.
drag(label, 5);
QCOMPARE(session.transformEdit().value().draft.origin.x(), std::round(x) + 5);
QVERIFY(!session.transformEdit().has_value());
QCOMPARE(layerWith(session, first).transform.origin.x(), std::round(x) + 5);
}
QTEST_MAIN(ScrubbableTransformTests)
+2
View File
@@ -216,6 +216,8 @@ compositor_test(TransformOverlayTests)
compositor_test(TransformPressTests)
compositor_test(TransformInspectorTests)
compositor_test(TransformSnapTests)
compositor_test(ResizeSnapTests)
compositor_test(ResizeSnapCanvasTests)
compositor_test(CursorTests)
compositor_test(DistortCanvasTests)
compositor_test(DistortMaskTests)
+121 -12
View File
@@ -1,4 +1,5 @@
#include "SessionFixtures.h"
#include "UI/HeldModifiers.h"
#include "UI/LayerIcons.h"
#include "UI/TransformInspector.h"
#include <QtTest>
@@ -57,6 +58,7 @@ private slots:
void aMenuBorrowsTheFocusAndTheBarsKeysApplyOrCancel();
void theLockIconFollowsThePalette();
void theFieldsTakeTheSpareRoom();
void heldKeysShowFlippedInTheBar();
};
void TransformInspectorTests::theFieldsShowTheLayerAndTypingPreviews()
@@ -73,12 +75,18 @@ void TransformInspectorTests::theFieldsShowTheLayerAndTypingPreviews()
QCOMPARE(bar.field("°").text(), QString("0"));
QCOMPARE(bar.control<QComboBox>("transformSampling").currentText(), QString("High quality"));
QCOMPARE(bar.field("X").accessibleName(), QString("X"));
// Typing begins a Ctrl+T edit and previews it.
// Typing previews in an edit of the fields' own.
const int base = bar.session.history.undoCount();
bar.type("X", "90");
QVERIFY(bar.session.transformEdit().value().persistent);
QVERIFY(!bar.session.transformEdit().value().persistent);
QVERIFY(bar.session.transformEdit().value().fromFields);
QCOMPARE(bar.draft().origin, QPointF(90, 40));
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin, QPointF(80, 40));
// Leaving the field applies it, one step.
bar.type("Y", "12.5");
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin, QPointF(90, 40));
QCOMPARE(bar.session.history.undoCount(), base + 1);
QCOMPARE(bar.session.history.undoName(), QString("Transform Layer"));
QCOMPARE(bar.draft().origin, QPointF(90, 12.5));
// Width locked carries the height; unlocked, alone.
bar.type("W", "80");
@@ -114,7 +122,7 @@ void TransformInspectorTests::theFieldsShowTheLayerAndTypingPreviews()
QVERIFY(bar.draft().flipX);
bar.control<QPushButton>("flipVertical").click();
QVERIFY(bar.draft().flipY);
// Apply commits the edit as one step; Cancel drops it.
// Apply commits the open edit; Cancel drops it.
const LayerTransform drafted = bar.draft();
bar.control<QPushButton>("applyTransform").click();
QVERIFY(!bar.session.transformEdit().has_value());
@@ -141,12 +149,16 @@ void TransformInspectorTests::returnEscapeAndArrowsInAField()
QTest::keyClick(&x, Qt::Key_Down, Qt::ShiftModifier);
QCOMPARE(bar.draft().origin.x(), 71.0);
QCOMPARE(x.text(), QString("71"));
// Return hands the canvas the keys; the edit waits there.
// Return applies and hands the canvas the keys.
const int request = bar.session.canvasFocusRequest();
const int steps = bar.session.history.undoCount();
QTest::keyClick(&x, Qt::Key_Return);
QVERIFY(!x.hasFocus());
QCOMPARE(bar.session.canvasFocusRequest(), request + 1);
QVERIFY(bar.session.transformEdit().has_value());
QVERIFY(!bar.session.transformEdit().has_value());
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin.x(), 71.0);
QCOMPARE(bar.session.history.undoCount(), steps + 1);
// Escape throws the fields' change away.
x.setFocus();
QTest::keyClicks(&x, "2");
QCOMPARE(x.text(), QString("712"));
@@ -154,13 +166,28 @@ void TransformInspectorTests::returnEscapeAndArrowsInAField()
QTest::keyClick(&x, Qt::Key_Escape);
QVERIFY(!x.hasFocus());
QCOMPARE(bar.session.canvasFocusRequest(), request + 2);
QCOMPARE(bar.draft().origin.x(), 712.0);
QVERIFY(!bar.session.transformEdit().has_value());
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin.x(), 71.0);
QCOMPARE(x.text(), QString("71"));
QCOMPARE(bar.session.history.undoCount(), steps + 1);
// Ctrl+T's edit takes typing and waits through Return and Escape.
bar.session.beginTransform();
x.setFocus();
QTest::keyClicks(&x, "5");
QCOMPARE(bar.draft().origin.x(), 715.0);
QVERIFY(!bar.session.transformEdit().value().fromFields);
QTest::keyClick(&x, Qt::Key_Return);
QCOMPARE(bar.draft().origin.x(), 715.0);
x.setFocus();
QTest::keyClick(&x, Qt::Key_Escape);
QCOMPARE(bar.draft().origin.x(), 715.0);
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin.x(), 71.0);
// While a field is typed in, the numbers wait outside.
x.setFocus();
LayerTransform moved = bar.draft();
moved.origin.setX(30);
bar.session.previewTransform(moved);
QCOMPARE(x.text(), QString("712"));
QCOMPARE(x.text(), QString("715"));
QCOMPARE(bar.field("Y").text(), QString("40"));
x.clearFocus();
QCOMPARE(x.text(), QString("30"));
@@ -172,7 +199,9 @@ void TransformInspectorTests::settingsButtonsAndTheTitleFollowTheSession()
QCheckBox &autoSelect = bar.control<QCheckBox>("transformAutoSelect");
QCheckBox &controls = bar.control<QCheckBox>("showTransformControls");
QVERIFY(!autoSelect.isChecked() && controls.isChecked());
QCOMPARE(autoSelect.toolTip(), QString("Select layers by clicking the canvas. When off, hold Ctrl to select a layer."));
QCOMPARE(autoSelect.toolTip(), QString("Select layers by clicking the canvas. Hold Ctrl to turn it the other way while you click."));
QCOMPARE(bar.control<QToolButton>("locksTransformRatio").toolTip(),
QString("Lock aspect ratio. Hold Shift while dragging a handle to turn it the other way."));
autoSelect.click();
QVERIFY(bar.session.transformAutoSelect());
controls.click();
@@ -258,14 +287,25 @@ void TransformInspectorTests::aMenuBorrowsTheFocusAndTheBarsKeysApplyOrCancel()
apply.setFocus(Qt::PopupFocusReason);
QCOMPARE(x.text(), QString("12.5"));
x.setFocus();
// Another window in front keeps it too.
apply.setFocus(Qt::ActiveWindowFocusReason);
QCOMPARE(x.text(), QString("12.5"));
QVERIFY(bar.session.transformEdit().has_value());
x.setFocus();
apply.setFocus(Qt::TabFocusReason);
QCOMPARE(x.text(), QString("12.50"));
// Return on Apply applies; Escape on a toggle cancels.
apply.setFocus();
QVERIFY(apply.hasFocus());
QTest::keyClick(&apply, Qt::Key_Return);
QVERIFY(!bar.session.transformEdit().has_value());
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin.x(), 12.5);
// Return on Apply applies Ctrl+T's; Escape on a toggle cancels.
bar.session.beginTransform();
bar.type("X", "14");
apply.setFocus();
QVERIFY(apply.hasFocus());
QVERIFY(bar.session.transformEdit().has_value());
QTest::keyClick(&apply, Qt::Key_Return);
QVERIFY(!bar.session.transformEdit().has_value());
QCOMPARE(layerWith(bar.session, bar.layer).transform.origin.x(), 14.0);
bar.session.beginTransform();
bar.type("Y", "7");
QCOMPARE(bar.draft().origin.y(), 7.0);
QCheckBox &controls = bar.control<QCheckBox>("showTransformControls");
@@ -306,5 +346,74 @@ void TransformInspectorTests::theFieldsTakeTheSpareRoom()
QCOMPARE(flip.width(), flip.sizeHint().width());
}
// Swift's HeldModifiers: Ctrl flips Auto Select, Shift the lock.
void TransformInspectorTests::heldKeysShowFlippedInTheBar()
{
Bar bar;
QVERIFY(bar.active);
QCheckBox &autoSelect = bar.control<QCheckBox>("transformAutoSelect");
QToolButton &lock = bar.control<QToolButton>("locksTransformRatio");
HeldModifiers &held = HeldModifiers::shared();
autoSelect.setFocus();
QVERIFY(!autoSelect.isChecked() && lock.isChecked());
// A modifier's own press counts it, whatever the event says.
QTest::keyPress(&autoSelect, Qt::Key_Control, Qt::NoModifier);
QCOMPARE(held.flags(), Qt::KeyboardModifiers(Qt::ControlModifier));
QVERIFY(autoSelect.isChecked());
QVERIFY(!bar.session.transformAutoSelect());
// Only a change is announced.
QSignalSpy announced(&held, &HeldModifiers::changed);
const auto press = [&](Qt::KeyboardModifiers modifiers) {
QKeyEvent event(QEvent::KeyPress, Qt::Key_A, modifiers);
QApplication::sendEvent(&autoSelect, &event);
};
press(Qt::ControlModifier);
QCOMPARE(announced.count(), 0);
press(Qt::ControlModifier | Qt::ShiftModifier);
QCOMPARE(announced.count(), 1);
press(Qt::ControlModifier);
QCOMPARE(announced.count(), 2);
// A click while held writes the other way.
autoSelect.click();
QVERIFY(bar.session.transformAutoSelect());
QVERIFY(!autoSelect.isChecked());
QTest::keyRelease(&autoSelect, Qt::Key_Control, Qt::ControlModifier);
QCOMPARE(held.flags(), Qt::KeyboardModifiers());
QVERIFY(autoSelect.isChecked());
// Shift flips the lock likewise; Alt and Meta count, unshown.
QTest::keyPress(&autoSelect, Qt::Key_Shift, Qt::AltModifier | Qt::MetaModifier | Qt::KeypadModifier);
QCOMPARE(held.flags(), Qt::ShiftModifier | Qt::AltModifier | Qt::MetaModifier);
QVERIFY(!lock.isChecked());
QVERIFY(autoSelect.isChecked());
lock.click();
QVERIFY(!bar.session.locksTransformRatio());
QVERIFY(lock.isChecked());
QTest::keyRelease(&autoSelect, Qt::Key_Shift, Qt::ShiftModifier);
QVERIFY(!lock.isChecked());
QTest::keyRelease(&autoSelect, Qt::Key_Alt, Qt::AltModifier);
QTest::keyRelease(&autoSelect, Qt::Key_Meta, Qt::MetaModifier);
QCOMPARE(held.flags(), Qt::KeyboardModifiers());
// Left presses and releases read modifiers; others do not.
QTest::mousePress(bar.header.get(), Qt::LeftButton, Qt::ControlModifier, QPoint(2, 2));
QCOMPARE(held.flags(), Qt::KeyboardModifiers(Qt::ControlModifier));
QTest::mouseRelease(bar.header.get(), Qt::RightButton, Qt::NoModifier, QPoint(2, 2));
QCOMPARE(held.flags(), Qt::KeyboardModifiers(Qt::ControlModifier));
QTest::mouseRelease(bar.header.get(), Qt::LeftButton, Qt::NoModifier, QPoint(2, 2));
QCOMPARE(held.flags(), Qt::KeyboardModifiers());
// Another app in front lets every key go.
QTest::keyPress(&autoSelect, Qt::Key_Shift, Qt::ShiftModifier);
QVERIFY(lock.isChecked());
emit qApp->applicationStateChanged(Qt::ApplicationInactive);
QCOMPARE(held.flags(), Qt::KeyboardModifiers());
QVERIFY(!lock.isChecked());
// Typing in a field, keys count as none.
QLineEdit &x = bar.field("X");
x.setFocus();
QTest::keyPress(&x, Qt::Key_Control, Qt::ControlModifier);
QCOMPARE(held.flags(), Qt::KeyboardModifiers());
QVERIFY(autoSelect.isChecked() == bar.session.transformAutoSelect());
QTest::keyRelease(&x, Qt::Key_Control, Qt::NoModifier);
}
QTEST_MAIN(TransformInspectorTests)
#include "TransformInspectorTests.moc"
+86 -2
View File
@@ -51,6 +51,10 @@ private slots:
void escapeMidDragRestoresAndAPersistentEditWaitsForReturn();
void losingTheKeysOrTheWheelMidDrag();
void aSelectionDragsAsOneBox();
void ctrlFlipsAutoSelectWhileHeld_data();
void ctrlFlipsAutoSelectWhileHeld();
void ctrlShiftClickAddsALayerWithAutoSelectOn();
void aCanvasDragAppliesTheFieldsEditFirst();
};
void TransformPressTests::draggingOutsideTheLayerMovesIt()
@@ -120,12 +124,13 @@ void TransformPressTests::handlesResizeAndTheGripRotates()
// 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));
// Resizing never snaps: five past the middle stays five past.
// 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 = {55, 100}}));
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);
@@ -381,5 +386,84 @@ void TransformPressTests::aSelectionDragsAsOneBox()
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"