14.9: Filter › Last Filter on Ctrl+Meta+F

This commit is contained in:
ZacharyZhang-NY committed 2026-10-11 06:28:14 -04:00
1 parent 0136d9d41a
commit 886b18dd7f
14 files changed
+306 -9

No files matched your search

+212
View File
@@ -0,0 +1,212 @@
#include "ContentView.h"
#include "MenuFixtures.h"
#include "Document/DocumentLimits.h"
#include <QPainter>
// Filter › Last Filter: the last filter again, same settings.
namespace {
// Swift's fixture: 40 by 20, the left half white.
std::unique_ptr<EditorSession> halfWhite()
{
auto session = std::make_unique<EditorSession>();
session->createDocument(40, 20);
QImage image = BrushRaster::context(40, 20, false);
QPainter(&image).fillRect(QRect(0, 0, 20, 20), Qt::white);
session->insert(ImportedImage(image, image, QStringLiteral("Half")));
return session;
}
bool committed(EditorSession &session)
{
bool done = false;
session.commitFilter([&done] { done = true; });
return QTest::qWaitFor([&done] { return done; }, 20000);
}
bool repeated(EditorSession &session)
{
bool done = false;
session.repeatLastFilter([&done] { done = true; });
return QTest::qWaitFor([&done] { return done; }, 20000);
}
ImageLayer active(const EditorSession &session)
{
return session.activeLayer().value();
}
}
class LastFilterTests : public QObject {
Q_OBJECT
private slots:
void runsTheLastFilterAgainWithItsSettingsAsOneUndoStep();
void aCancelledFilterOrAnImageAdjustmentIsNotTheLastFilter();
void theGatesAndAPendingGradient();
void anAutomaticFilterRunsAgainThroughItsPreview();
void theMenuNamesItAndRunsWithoutThePanel();
void aRepeatThatCannotBeginOrLandLeavesNothingOpen();
};
void LastFilterTests::runsTheLastFilterAgainWithItsSettingsAsOneUndoStep()
{
const auto session = halfWhite();
QVERIFY(!session->canRepeatLastFilter() && !session->lastFilter());
session->beginFilter(FilterKind::gaussianBlur);
FilterSettings settings;
settings.radius = 3;
session->updateFilter(settings, true);
QVERIFY(committed(*session));
const ImageLayer once = active(*session);
QVERIFY(session->lastFilter() == FilterKind::gaussianBlur && session->canRepeatLastFilter());
const int count = session->history.undoCount();
QVERIFY(repeated(*session));
const ImageLayer twice = active(*session);
QVERIFY(!session->filterEdit() && session->history.undoCount() == count + 1 && session->history.undoName() == "Gaussian Blur");
QVERIFY(twice.asset->identity() != once.asset->identity());
// Blurred by 3 again, it spreads past the edge further.
QVERIFY(twice.transform.size.width() > once.transform.size.width());
QCOMPARE(session->filterSettings().radius, 3.0);
session->undo();
QVERIFY(active(*session).asset->identity() == once.asset->identity());
}
void LastFilterTests::aCancelledFilterOrAnImageAdjustmentIsNotTheLastFilter()
{
const auto session = halfWhite();
session->beginFilter(FilterKind::addNoise);
QVERIFY(committed(*session));
session->beginFilter(FilterKind::gaussianBlur);
session->cancelFilter();
session->beginFilter(FilterKind::curves);
session->cancelFilter();
QCOMPARE(session->lastFilter(), std::optional(FilterKind::addNoise));
// An Image menu adjustment committed is not recorded either.
session->beginFilter(FilterKind::exposure);
FilterSettings brighter = session->filterSettings();
brighter.exposure.exposure = 1;
session->updateFilter(brighter, true);
QVERIFY(committed(*session));
QCOMPARE(session->lastFilter(), std::optional(FilterKind::addNoise));
// Nothing to do: no step, and nothing left open.
QSignalSpy changes(session.get(), &EditorSession::changed);
session->setLastFilter(FilterKind::lensCorrection);
QCOMPARE(changes.count(), 1);
const int count = session->history.undoCount();
QVERIFY(repeated(*session));
QVERIFY(!session->filterEdit() && session->history.undoCount() == count && !session->isProjectBusy());
QVERIFY(!repeatsAsLastFilter(FilterKind::contentAwareFill) && !repeatsAsLastFilter(FilterKind::grain) && repeatsAsLastFilter(FilterKind::dither));
}
void LastFilterTests::theGatesAndAPendingGradient()
{
const auto session = halfWhite();
session->setLastFilter(FilterKind::addNoise);
QVERIFY(session->canRepeatLastFilter());
session->beginFilter(FilterKind::gaussianBlur);
QVERIFY(!session->canRepeatLastFilter());
// A refused run warns and calls back, changing nothing.
QTest::ignoreMessage(QtWarningMsg, "no last filter to run here");
QVERIFY(repeated(*session));
QCOMPARE(session->filterEdit()->kind, FilterKind::gaussianBlur);
session->cancelFilter();
session->beginHueSaturation();
QVERIFY(session->hueSaturation() && !session->canRepeatLastFilter());
session->cancelHueSaturation();
// Vignette asks Vignette's gate: an empty layer takes it.
session->addBlankLayer();
QVERIFY(!session->canRepeatLastFilter());
session->setLastFilter(FilterKind::vignette);
QVERIFY(session->canRepeatLastFilter());
// A pending gradient lands first, then the filter: two steps.
session->setLastFilter(FilterKind::addNoise);
session->selectLayer(session->document()->layers.front().id);
session->selectTool(NavigationTool::gradient);
session->beginGradient(QPointF(2, 2));
session->moveGradient(std::nullopt, QPointF(30, 2));
session->endGradientDrag();
QVERIFY(session->gradientEdit());
const int count = session->history.undoCount();
QVERIFY(repeated(*session));
QVERIFY(!session->gradientEdit() && session->history.undoCount() == count + 2 && session->history.undoName() == "Add Noise");
}
void LastFilterTests::anAutomaticFilterRunsAgainThroughItsPreview()
{
// Remove Background commits what its preview made.
auto session = std::make_unique<EditorSession>();
session->createDocument(400, 300);
QImage image = BrushRaster::context(400, 300, false);
image.fill(Qt::white);
QPainter painter(&image);
painter.setPen(Qt::NoPen);
painter.setBrush(QColor(217, 26, 26));
painter.drawEllipse(QPointF(200, 150), 80, 80);
painter.end();
session->insert(ImportedImage(image, image, QStringLiteral("Disc")));
session->setLastFilter(FilterKind::removeBackground);
const int count = session->history.undoCount();
QVERIFY(repeated(*session));
QVERIFY(session->history.undoCount() == count + 1 && session->history.undoName() == "Remove Background");
QVERIFY(active(*session).mask && !session->filterEdit());
}
void LastFilterTests::theMenuNamesItAndRunsWithoutThePanel()
{
Bar bar;
bar.window.show();
QVERIFY(QTest::qWaitForWindowActive(&bar.window));
QAction &entry = bar.action("lastFilter");
QVERIFY(entry.text() == "Last Filter" && !entry.isEnabled());
QCOMPARE(entry.shortcut(), QKeySequence(Qt::CTRL | Qt::META | Qt::Key_F));
bar.session().createDocument(40, 20);
bar.session().insert(white(), QPointF(20, 10));
bar.session().beginFilter(FilterKind::addNoise);
QTRY_VERIFY(bar.window.findChild<QWidget *>("filterPanel") && bar.window.findChild<QWidget *>("filterPanel")->isVisible());
QVERIFY(committed(bar.session()));
QTRY_VERIFY(!bar.window.findChild<QWidget *>("filterPanel")->isVisible());
QVERIFY(entry.text() == "Last Filter: Add Noise" && entry.isEnabled());
// Its key runs it at once; the panel never shows.
bool panelShown = false;
QObject::connect(&bar.session(), &EditorSession::changed, &bar.window, [&] {
panelShown = panelShown || bar.window.findChild<QWidget *>("filterPanel")->isVisible();
});
const int count = bar.session().history.undoCount();
QTest::keyClick(&bar.window, Qt::Key_F, Qt::ControlModifier | Qt::MetaModifier);
QTRY_COMPARE(bar.session().history.undoCount(), count + 1);
QTRY_VERIFY(!bar.session().isProjectBusy());
QVERIFY(!panelShown && !bar.session().filterEdit());
// While a filter is open it rests.
bar.session().beginFilter(FilterKind::gaussianBlur);
QVERIFY(!entry.isEnabled());
bar.session().cancelFilter();
QVERIFY(entry.isEnabled());
}
void LastFilterTests::aRepeatThatCannotBeginOrLandLeavesNothingOpen()
{
// A repeat begins without a preview, unless automatic.
const auto session = halfWhite();
session->beginFilter(FilterKind::gaussianBlur, true);
QVERIFY(session->filterEdit()->repeating && !session->filterEdit()->preview);
session->cancelFilter();
session->beginFilter(FilterKind::removeBackground, true);
QVERIFY(session->filterEdit()->repeating && session->filterEdit()->preview);
session->cancelFilter();
// No subject: the preview fails, and the repeat closes it.
session->setLastFilter(FilterKind::removeBackground);
const int count = session->history.undoCount();
QVERIFY(repeated(*session));
QVERIFY(!session->filterEdit() && session->history.undoCount() == count && !session->isProjectBusy());
// A blur that cannot grow begins nothing: an error.
EditorSession wide;
wide.createDocument(100, 10);
QImage line = BrushRaster::context(DocumentLimits::maxSide - 4, 1, false);
line.fill(Qt::white);
wide.insert(ImportedImage(line, QImage(), QStringLiteral("Line")));
wide.setLastFilter(FilterKind::gaussianBlur);
QVERIFY(repeated(wide));
QVERIFY(!wide.filterEdit() && wide.brushError() && !wide.isProjectBusy());
}
QTEST_MAIN(LastFilterTests)
#include "LastFilterTests.moc"