Middle-button pan, persistent tool settings, Auto Select's foreground rule, the new icon (12.6b)

This commit is contained in:
雷电芽衣 committed 2026-09-27 10:53:42 -04:00
1 parent 5a2fa91bc7
commit 29eab10edc
27 files changed
+390 -22

No files matched your search

+62
View File
@@ -9,6 +9,7 @@ private slots:
void theHandToolAndSpaceDragTheView();
void theWheelPansAndZoomsWithAModifier();
void aPinchZoomsAboutThePointer();
void theMiddleButtonPansFromAnyTool();
};
void CanvasNavigationTests::theZoomToolClicksAndDrags()
@@ -167,5 +168,66 @@ void CanvasNavigationTests::aPinchZoomsAboutThePointer()
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"
+5 -2
View File
@@ -38,7 +38,7 @@ class DistortTests : public QObject {
Q_OBJECT
private slots:
void cornersRunClockwiseFromTheTopLeft();
void perspectiveMappingHitsTheCornersAndTwistedShapesAreRefused();
void perspectiveMappingHitsTheCornersAndDegenerateShapesAreRefused();
void distortingWarpsTheLayerIntoTheShapeAsOneUndoStep();
void distortedLayerIsTrimmedToItsVisiblePixels();
void foldedShapesWarpAsTwoTrianglesAndFlipsSwapCorners();
@@ -61,7 +61,7 @@ void DistortTests::cornersRunClockwiseFromTheTopLeft()
QVERIFY(!near(corners[1], corners[0]));
}
void DistortTests::perspectiveMappingHitsTheCornersAndTwistedShapesAreRefused()
void DistortTests::perspectiveMappingHitsTheCornersAndDegenerateShapesAreRefused()
{
const QTransform map = DistortWarp::homography(shape);
const Corners unit = {QPointF(0, 0), QPointF(1, 0), QPointF(1, 1), QPointF(0, 1)};
@@ -108,6 +108,9 @@ void DistortTests::distortingWarpsTheLayerIntoTheShapeAsOneUndoStep()
// A collapsed corner: ignored.
session.previewCorners({shape[0], shape[0], shape[2], shape[3]});
QCOMPARE(session.transformEdit().value().corners.value()[1], QPointF(30, 10));
// A folded shape is a distortion of its own.
session.previewCorners({shape[0], shape[2], shape[1], shape[3]});
QCOMPARE(session.transformEdit().value().corners.value()[1], shape[2]);
session.previewCorners(shape);
QCOMPARE(session.transformEdit().value().corners.value(), shape);
const int count = session.history.undoCount();
+2
View File
@@ -293,6 +293,8 @@ void SelectionCanvasTests::aCtrlDragMovesSelectedPixelsAndCtrlArrowsNudgeThem()
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);
+1
View File
@@ -204,6 +204,7 @@ compositor_test(PSDReaderLayerTests)
compositor_test(PSDImportTests)
compositor_test(RawImporterTests)
compositor_test(RawDevelopTests)
compositor_test(ToolDefaultsTests)
compositor_test(LayerEffectsRendererTests)
compositor_test(LayerFlipTests)
compositor_test(LayerMergeTests)
+123
View File
@@ -0,0 +1,123 @@
#include "Document/EditorSession.h"
#include "Document/ToolDefaults.h"
#include <QRegularExpression>
#include <QSettings>
#include <QTemporaryDir>
#include <QtTest>
// Swift's ToolDefaults: toggles that outlive the session and the run.
namespace {
struct Toggle {
QString key;
bool fallback;
std::function<bool(const EditorSession &)> read;
std::function<void(EditorSession &, bool)> write;
};
std::vector<Toggle> toggles()
{
return {
{"autoSelect", false, &EditorSession::transformAutoSelect, &EditorSession::setTransformAutoSelect},
{"transformControls", true, &EditorSession::showsTransformControls, &EditorSession::setShowsTransformControls},
{"pixelGrid", true, &EditorSession::showsPixelGrid, &EditorSession::setShowsPixelGrid},
{"grid", false, &EditorSession::showsGrid, &EditorSession::setShowsGrid},
{"guides", true, &EditorSession::showsGuides, &EditorSession::setShowsGuides},
{"rulers", false, &EditorSession::showsRulers, &EditorSession::setShowsRulers},
{"snap", true, &EditorSession::snapEnabled, &EditorSession::setSnapEnabled},
{"snapGuides", true, &EditorSession::snapToGuides, &EditorSession::setSnapToGuides},
{"snapGrid", false, &EditorSession::snapToGrid, &EditorSession::setSnapToGrid},
{"snapLayers", true, &EditorSession::snapToLayers, &EditorSession::setSnapToLayers},
{"snapBounds", true, &EditorSession::snapToDocumentBounds, &EditorSession::setSnapToDocumentBounds},
{"lockGuides", false, &EditorSession::locksGuides, &EditorSession::setLocksGuides},
};
}
}
class ToolDefaultsTests : public QObject {
Q_OBJECT
private slots:
void initTestCase();
void untilTheAppTurnsThemOnTheDefaultsHold();
void everyToggleOutlivesItsSession();
void showingAGuideRemembersGuidesShown();
void aStrangeValueIsIgnoredAndSaid();
private:
QTemporaryDir m_folder;
};
void ToolDefaultsTests::initTestCase()
{
// Settings land in a folder of the test's own.
QSettings::setPath(QSettings::NativeFormat, QSettings::UserScope, m_folder.path());
}
void ToolDefaultsTests::untilTheAppTurnsThemOnTheDefaultsHold()
{
QSettings().setValue("tool.grid", true);
QVERIFY(!ToolDefaults::boolean("grid", false));
ToolDefaults::set(false, "rulers");
QVERIFY(!QSettings().contains("tool.rulers"));
EditorSession session;
QVERIFY(!session.showsGrid());
session.setShowsRulers(true);
QVERIFY(!QSettings().contains("tool.rulers"));
QSettings().remove("tool.grid");
}
void ToolDefaultsTests::everyToggleOutlivesItsSession()
{
ToolDefaults::enable();
// An unset toggle is no strange value.
QTest::failOnWarning(QRegularExpression("ignoring the tool setting"));
for (const Toggle &toggle : toggles()) {
QVERIFY(!QSettings().contains("tool." + toggle.key));
QCOMPARE(toggle.read(EditorSession()), toggle.fallback);
{
EditorSession session;
toggle.write(session, !toggle.fallback);
}
QCOMPARE(QSettings().value("tool." + toggle.key).toBool(), !toggle.fallback);
QVERIFY2(toggle.read(EditorSession()) == !toggle.fallback, qPrintable(toggle.key));
// The next run reads text, as the file keeps it.
QSettings().setValue("tool." + toggle.key, !toggle.fallback ? "true" : "false");
QCOMPARE(ToolDefaults::boolean(toggle.key, toggle.fallback), !toggle.fallback);
QSettings().remove("tool." + toggle.key);
}
}
void ToolDefaultsTests::showingAGuideRemembersGuidesShown()
{
ToolDefaults::enable();
EditorSession session;
session.createDocument(100, 100);
for (const bool adding : {true, false}) {
session.setShowsGuides(false);
QCOMPARE(QSettings().value("tool.guides").toString(), QString("false"));
if (adding)
session.addGuide(CanvasGuide{QUuid::createUuid(), CanvasGuide::Axis::vertical, 40});
else
session.beginGuideCreation(CanvasGuide::Axis::horizontal, 20);
QVERIFY(session.showsGuides());
QCOMPARE(QSettings().value("tool.guides").toString(), QString("true"));
QVERIFY(EditorSession().showsGuides());
}
QSettings().remove("tool.guides");
}
void ToolDefaultsTests::aStrangeValueIsIgnoredAndSaid()
{
ToolDefaults::enable();
QSettings().setValue("tool.grid", "maybe");
QTest::ignoreMessage(QtWarningMsg, "ignoring the tool setting grid of maybe");
QVERIFY(!ToolDefaults::boolean("grid", false));
QTest::ignoreMessage(QtWarningMsg, "ignoring the tool setting grid of maybe");
QVERIFY(ToolDefaults::boolean("grid", true));
QSettings().setValue("tool.grid", 1);
QTest::ignoreMessage(QtWarningMsg, "ignoring the tool setting grid of 1");
QVERIFY(!ToolDefaults::boolean("grid", false));
QSettings().remove("tool.grid");
}
QTEST_GUILESS_MAIN(ToolDefaultsTests)
#include "ToolDefaultsTests.moc"
+62 -2
View File
@@ -47,6 +47,7 @@ private slots:
void handlesResizeAndTheGripRotates();
void movesSnapToTheCanvasAndOtherLayersUnlessCtrl();
void autoSelectCanBeDisabledAndCtrlClickOverridesIt();
void autoSelectPicksTheForegroundOverASelectedBackground();
void escapeMidDragRestoresAndAPersistentEditWaitsForReturn();
void losingTheKeysOrTheWheelMidDrag();
void aSelectionDragsAsOneBox();
@@ -190,16 +191,21 @@ void TransformPressTests::autoSelectCanBeDisabledAndCtrlClickOverridesIt()
session.setTransformAutoSelect(true);
shown.drag(QPointF(20, 250), QPointF(33, 250));
QCOMPARE(shown.origin(), QPointF(163, 100));
// Inside the active layer's box, it drags, whatever lies above.
// 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.
const QUuid green = session.activeLayerID() == shown.red ? session.document().value().layers.back().id : shown.red;
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();
@@ -211,6 +217,60 @@ void TransformPressTests::autoSelectCanBeDisabledAndCtrlClickOverridesIt()
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;