Quit settles a pending gradient, a pixel move and open dialogs; Dither › Scanlines (CRT) on all cores (13.17)

This commit is contained in:
ZacharyZhang-NY committed 2026-10-01 17:14:47 -04:00
1 parent 765e071c0c
commit 5bfbc6cd5d
12 files changed
+913 -32

No files matched your search

+246
View File
@@ -0,0 +1,246 @@
#include "DialogDesk.h"
#include "Document/ProjectWorkspace.h"
#include "SelectionFixtures.h"
#include "UI/ProjectWorkspaceView.h"
#include <QtTest>
// Swift 1.4.4: quit applies canvas edits and cancels dialogs.
namespace {
ImportedImage filled(const QColor &colour)
{
QImage pixels(100, 20, QImage::Format_RGBA8888_Premultiplied);
pixels.fill(colour);
return ImportedImage(pixels, pixels, QStringLiteral("Fixture"));
}
// Swift's await: true once the call's callback has run.
bool awaited(const std::function<void(std::function<void()>)> &call)
{
bool done = false;
call([&done] { done = true; });
return QTest::qWaitFor([&done] { return done; }, 10'000);
}
std::optional<bool> answer(ProjectWorkspace &workspace)
{
std::optional<bool> result;
workspace.confirmQuit([&](bool value) { result = value; });
return QTest::qWaitFor([&] { return result.has_value(); }, 10'000) ? result : std::nullopt;
}
// Swift's test: a gradient waiting for Apply.
void pendGradient(EditorSession &session)
{
session.createDocument(100, 20);
session.addBlankLayer();
session.selectTool(NavigationTool::gradient);
session.beginGradient(QPointF(0, 10));
session.moveGradient(std::nullopt, QPointF(100, 10));
session.endGradientDrag();
}
ImageIdentity identity(const EditorSession &session)
{
return session.activeLayer().value().asset.value().identity();
}
}
class QuitPendingEditsTests : public QObject {
Q_OBJECT
private slots:
void quittingAppliesAPendingGradient();
void quittingAppliesAPixelMove();
void quittingCancelsAnOpenDialog();
void quittingCancelsTheOtherDialogs();
void quittingCancelsAnAdjustmentsEditor();
void aWorkspaceGoneMidSettleCallsNothing();
void closingTheWindowAppliesTheGradientThenAsks();
void aBusyProjectHoldsTheQuit();
void aQuitWhileManagingIsRefusedAtOnce();
};
void QuitPendingEditsTests::quittingAppliesAPendingGradient()
{
ProjectWorkspace workspace;
EditorSession &session = workspace.current().session;
pendGradient(session);
QVERIFY(!workspace.canSwitch());
const int count = session.history.undoCount();
QVERIFY(awaited([&](std::function<void()> done) { workspace.settlePendingEdits(std::move(done)); }));
QVERIFY(!session.gradientEdit());
QCOMPARE(session.history.undoCount(), count + 1);
QCOMPARE(session.history.undoName(), QString("Gradient"));
QVERIFY(workspace.canSwitch());
}
void QuitPendingEditsTests::quittingAppliesAPixelMove()
{
ProjectWorkspace workspace;
EditorSession &session = workspace.current().session;
session.createDocument(100, 20);
session.insert(filled(QColor(200, 100, 50)));
session.applySelection(rectPath(QRectF(10, 5, 10, 10)), SelectionMode::replace, "Select");
QVERIFY(session.beginPixelMove());
session.movePixels(QSizeF(30, 0));
QVERIFY(!workspace.canSwitch());
const int count = session.history.undoCount();
QVERIFY(awaited([&](std::function<void()> done) { workspace.settlePendingEdits(std::move(done)); }));
QVERIFY(!session.pixelMove());
QCOMPARE(session.history.undoCount(), count + 1);
QCOMPARE(session.history.undoName(), QString("Move Pixels"));
QCOMPARE(session.selection().value().path.boundingRect(), QRectF(40, 5, 10, 10));
QVERIFY(workspace.canSwitch());
}
void QuitPendingEditsTests::quittingCancelsAnOpenDialog()
{
ProjectWorkspace workspace;
EditorSession &session = workspace.current().session;
session.createDocument(100, 20);
session.insert(filled(QColor(200, 100, 50)));
const ImageIdentity original = identity(session);
const int count = session.history.undoCount();
const std::vector<std::function<void()>> opens{[&] { session.beginFilter(FilterKind::gaussianBlur); },
[&] { session.beginHueSaturation(); }, [&] { session.beginLevels(); }};
for (const std::function<void()> &open : opens) {
open();
QVERIFY(!workspace.canSwitch());
QVERIFY(awaited([&](std::function<void()> done) { workspace.settlePendingEdits(std::move(done)); }));
QVERIFY(workspace.canSwitch());
QVERIFY(identity(session) == original);
}
QVERIFY(!session.filterEdit() && !session.hueSaturation() && !session.levels());
QCOMPARE(session.history.undoCount(), count);
}
void QuitPendingEditsTests::quittingCancelsTheOtherDialogs()
{
ProjectWorkspace workspace;
EditorSession &session = workspace.current().session;
session.createDocument(100, 20);
session.insert(filled(QColor(200, 100, 50)));
const QPainterPath before = rectPath(QRectF(10, 5, 10, 10));
session.applySelection(before, SelectionMode::replace, "Select");
const PaletteColor foreground = session.foregroundColor();
const auto settle = [&] { return awaited([&](std::function<void()> done) { workspace.settlePendingEdits(std::move(done)); }); };
// Color Range: the selection goes back as it was.
session.beginColorRange();
QVERIFY(session.colorRange() && !workspace.canSwitch());
QVERIFY(settle());
QVERIFY(!session.colorRange());
QCOMPARE(session.selection().value().path.boundingRect(), before.boundingRect());
// Expand, Contract or Feather: its panel goes.
session.setSelectionAmountOperation(SelectionAmountOperation::feather);
QVERIFY(!workspace.canSwitch());
QVERIFY(settle());
QVERIFY(!session.selectionAmountOperation());
// The colour picker: closed, its colour not taken.
session.openColorPicker(false);
session.setColorPickerHSB(PickerHSB(120, 1, 1));
QVERIFY(!workspace.canSwitch());
QVERIFY(settle());
QVERIFY(!session.colorPicker());
QVERIFY(session.foregroundColor() == foreground);
QVERIFY(workspace.canSwitch());
}
void QuitPendingEditsTests::quittingCancelsAnAdjustmentsEditor()
{
ProjectWorkspace workspace;
EditorSession &session = workspace.current().session;
session.createDocument(100, 20);
session.insert(filled(Qt::white));
session.addAdjustment(AdjustmentKind::exposure);
const QUuid id = session.adjustmentEditingID().value();
const LayerAdjustment made = session.activeLayer().value().adjustment.value();
QVERIFY(awaited([&](std::function<void()> done) { session.beginAdjustmentEditing(id, std::move(done)); }));
FilterSettings settings = session.filterEdit().value().settings;
settings.exposure.exposure = 2;
session.updateFilter(settings, true);
QVERIFY(!(session.activeLayer().value().adjustment.value() == made));
QVERIFY(!workspace.canSwitch());
QVERIFY(awaited([&](std::function<void()> done) { workspace.settlePendingEdits(std::move(done)); }));
QVERIFY(!session.adjustmentEditingID() && !session.adjustmentOriginal() && !session.filterEdit());
QVERIFY(session.activeLayer().value().adjustment.value() == made);
QVERIFY(workspace.canSwitch());
}
void QuitPendingEditsTests::aWorkspaceGoneMidSettleCallsNothing()
{
auto workspace = std::make_unique<ProjectWorkspace>();
const std::shared_ptr<ProjectTab> tab = workspace->tabs().front();
pendGradient(tab->session);
bool called = false;
workspace->settlePendingEdits([&called] { called = true; });
workspace.reset();
// The tab lives on and its gradient lands; no answer.
QTRY_VERIFY(!tab->session.gradientEdit());
QTest::qWait(100);
QVERIFY(!called);
QCOMPARE(tab->session.history.undoName(), QString("Gradient"));
}
void QuitPendingEditsTests::closingTheWindowAppliesTheGradientThenAsks()
{
ProjectWorkspace workspace;
ProjectWorkspaceView view(workspace);
view.show();
EditorSession &session = workspace.current().session;
pendGradient(session);
session.history.markSaved();
DialogDesk desk;
desk.replies = {"Cancel"};
// The gradient is a step by the time it asks.
desk.note = [&] { return QStringLiteral("pending=%1 step=%2").arg(session.gradientEdit().has_value()).arg(session.history.undoName()); };
view.close();
QTRY_COMPARE(desk.seen.size(), 1);
QVERIFY2(desk.seen[0].startsWith("alert|2|Save changes to Untitled?") && desk.seen[0].endsWith("|pending=0 step=Gradient"),
qPrintable(desk.seen[0]));
QTRY_VERIFY(!workspace.isManaging());
QVERIFY(view.isVisible());
}
void QuitPendingEditsTests::aBusyProjectHoldsTheQuit()
{
ProjectWorkspace workspace;
EditorSession &session = workspace.current().session;
pendGradient(session);
session.setIsProjectBusy(true);
DialogDesk desk;
QTest::ignoreMessage(QtWarningMsg, "Quit refused: the project is still busy");
QCOMPARE(answer(workspace), std::optional(false));
// Nothing asked, nothing applied: the gradient waits.
QVERIFY(desk.seen.isEmpty());
QVERIFY(session.gradientEdit());
QVERIFY(!workspace.isManaging());
session.setIsProjectBusy(false);
}
void QuitPendingEditsTests::aQuitWhileManagingIsRefusedAtOnce()
{
ProjectWorkspace workspace;
EditorSession &session = workspace.current().session;
session.createDocument(100, 20);
DialogDesk desk;
desk.replies = {"Cancel"};
std::optional<bool> second;
bool settled = false;
bool answered = false;
// While the first quit asks, a second is refused unasked.
desk.note = [&] {
session.setSelectionAmountOperation(SelectionAmountOperation::feather);
workspace.confirmQuit([&](bool value) { second = value; });
answered = QTest::qWaitFor([&] { return second.has_value(); }, 5000);
settled = !session.selectionAmountOperation();
session.setSelectionAmountOperation(std::nullopt);
return QString();
};
QCOMPARE(answer(workspace), std::optional(false));
QCOMPARE(desk.seen.size(), 1);
QVERIFY(answered);
QCOMPARE(second, std::optional(false));
QVERIFY(!settled);
}
QTEST_MAIN(QuitPendingEditsTests)
#include "QuitPendingEditsTests.moc"