Outer Glow, brush Smoothing and the rest of Compositor 1.2.0
This commit is contained in:
1 parent
2cd0d1b86d
commit
374ba8ec73
32 files changed
+624
-47
No files matched your search
@@ -38,6 +38,7 @@ private slots:
|
||||
void eachBrushToolTitlesTheBarAndShowsItsPicker();
|
||||
void cloneStampShowsAlignedSampleAndTheSourceNote();
|
||||
void theSmearShowsItsThreeModes();
|
||||
void smoothingShowsWithTheBrushAlone();
|
||||
};
|
||||
|
||||
void BrushControlsTests::theTitleAndModesFollowTheSession()
|
||||
@@ -274,5 +275,36 @@ void BrushControlsTests::theSmearShowsItsThreeModes()
|
||||
QVERIFY(!modes[1]->isVisible());
|
||||
}
|
||||
|
||||
void BrushControlsTests::smoothingShowsWithTheBrushAlone()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(8, 8, true);
|
||||
session.selectTool(NavigationTool::brush);
|
||||
BrushControls bar(session);
|
||||
bar.show();
|
||||
auto &field = find<QLineEdit>(bar, "brushSmoothing");
|
||||
auto &slider = find<QSlider>(bar, "brushSmoothingSlider");
|
||||
QVERIFY(field.isVisible() && slider.isVisible());
|
||||
QCOMPARE(field.text(), QString("0"));
|
||||
QCOMPARE(field.toolTip(), QString("The brush trails the pointer on a string this long, so a shaky hand still draws a smooth line"));
|
||||
QVERIFY(slider.minimum() == 0 && slider.maximum() == 1000 && slider.width() == 100 && field.width() == 42);
|
||||
slider.setValue(255);
|
||||
QCOMPARE(session.brushSettings().smoothing, 25.5);
|
||||
QCOMPARE(field.text(), QString("26"));
|
||||
type(field, "140");
|
||||
QCOMPARE(session.brushSettings().smoothing, 100.0);
|
||||
QCOMPARE(slider.value(), 1000);
|
||||
QTest::keyClick(&field, Qt::Key_Down, Qt::ShiftModifier);
|
||||
QCOMPARE(session.brushSettings().smoothing, 90.0);
|
||||
// The other brush tools have their own feel: no Smoothing.
|
||||
for (const NavigationTool tool : {NavigationTool::spotHealing, NavigationTool::cloneStamp, NavigationTool::blur}) {
|
||||
session.selectTool(tool);
|
||||
QVERIFY(!field.isVisible() && !slider.isVisible());
|
||||
}
|
||||
session.selectTool(NavigationTool::brush);
|
||||
session.setBrushMode(BrushToolMode::erase);
|
||||
QVERIFY(field.isVisible());
|
||||
}
|
||||
|
||||
QTEST_MAIN(BrushControlsTests)
|
||||
#include "BrushControlsTests.moc"
|
||||
@@ -0,0 +1,119 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include <QtTest>
|
||||
|
||||
// Swift's Smoothing: the brush trails the pointer on a string.
|
||||
namespace {
|
||||
std::unique_ptr<EditorSession> painting(double smoothing, NavigationTool tool = NavigationTool::brush)
|
||||
{
|
||||
auto session = std::make_unique<EditorSession>();
|
||||
session->createDocument(200, 200, true);
|
||||
session->selectTool(tool);
|
||||
BrushSettings brush = session->brushSettings();
|
||||
brush.diameter = 4;
|
||||
brush.hardness = 1;
|
||||
brush.smoothing = smoothing;
|
||||
session->setBrushSettings(brush);
|
||||
return session;
|
||||
}
|
||||
|
||||
QPointF painted(const EditorSession &session)
|
||||
{
|
||||
return session.shiftLineStart().value();
|
||||
}
|
||||
|
||||
// The committed pixels at a document point, clear outside them.
|
||||
int ink(const EditorSession &session, int x, int y)
|
||||
{
|
||||
const ImageLayer &layer = session.document().value().layers.back();
|
||||
const QImage image = layer.asset.value().image();
|
||||
const QPoint at = QPoint(x, y) - layer.origin().toPoint();
|
||||
if (layer.size() != QSizeF(image.size()))
|
||||
throw std::runtime_error("the layer is scaled");
|
||||
return image.rect().contains(at) ? qAlpha(image.pixel(at)) : 0;
|
||||
}
|
||||
}
|
||||
|
||||
class BrushSmoothingTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void theBrushWaitsUntilTheStringIsTaut();
|
||||
void theStrokeEndsWhereTheHandDid();
|
||||
void theStringIsMeasuredOnScreen();
|
||||
void onlyTheBrushSmoothsAndACancelLetsGo();
|
||||
};
|
||||
|
||||
void BrushSmoothingTests::theBrushWaitsUntilTheStringIsTaut()
|
||||
{
|
||||
const auto session = painting(10);
|
||||
session->beginBrush(QPointF(50, 50));
|
||||
session->continueBrush(QPointF(55, 50));
|
||||
QCOMPARE(painted(*session), QPointF(50, 50));
|
||||
// Taut at exactly its length, the string still holds.
|
||||
session->continueBrush(QPointF(60, 50));
|
||||
QCOMPARE(painted(*session), QPointF(50, 50));
|
||||
session->continueBrush(QPointF(70, 50));
|
||||
QCOMPARE(painted(*session), QPointF(60, 50));
|
||||
// Pulled at an angle, the brush moves along the string.
|
||||
session->continueBrush(QPointF(70, 60));
|
||||
const double reach = 10 * std::sqrt(0.5);
|
||||
QVERIFY(std::abs(painted(*session).x() - (70 - reach)) < 1e-9 && std::abs(painted(*session).y() - (60 - reach)) < 1e-9);
|
||||
// Without Smoothing the brush is the pointer.
|
||||
const auto exact = painting(0);
|
||||
exact->beginBrush(QPointF(50, 50));
|
||||
exact->continueBrush(QPointF(51, 50));
|
||||
QCOMPARE(painted(*exact), QPointF(51, 50));
|
||||
}
|
||||
|
||||
void BrushSmoothingTests::theStrokeEndsWhereTheHandDid()
|
||||
{
|
||||
const auto session = painting(10);
|
||||
session->beginBrush(QPointF(50, 50));
|
||||
session->continueBrush(QPointF(80, 50));
|
||||
QVERIFY(session->finishBrushImmediately());
|
||||
QVERIFY(ink(*session, 79, 50) > 0 && ink(*session, 70, 50) > 0);
|
||||
QCOMPARE(ink(*session, 86, 50), 0);
|
||||
// A string never pulled taut still ends at the pointer.
|
||||
const auto still = painting(10);
|
||||
still->beginBrush(QPointF(50, 50));
|
||||
still->continueBrush(QPointF(57, 50));
|
||||
QVERIFY(still->finishBrushImmediately());
|
||||
QVERIFY(ink(*still, 56, 50) > 0);
|
||||
QCOMPARE(ink(*still, 62, 50), 0);
|
||||
}
|
||||
|
||||
void BrushSmoothingTests::theStringIsMeasuredOnScreen()
|
||||
{
|
||||
const auto session = painting(10);
|
||||
session->viewport.setZoom(2, QPointF(), session->document().value().size());
|
||||
session->beginBrush(QPointF(50, 50));
|
||||
session->continueBrush(QPointF(60, 50));
|
||||
QCOMPARE(painted(*session), QPointF(55, 50));
|
||||
}
|
||||
|
||||
void BrushSmoothingTests::onlyTheBrushSmoothsAndACancelLetsGo()
|
||||
{
|
||||
const auto healing = painting(10, NavigationTool::spotHealing);
|
||||
healing->beginBrush(QPointF(50, 50));
|
||||
healing->continueBrush(QPointF(52, 50));
|
||||
QCOMPARE(painted(*healing), QPointF(52, 50));
|
||||
// The eraser is the Brush in another mode: it smooths.
|
||||
const auto erasing = painting(10);
|
||||
erasing->setBrushMode(BrushToolMode::erase);
|
||||
erasing->beginBrush(QPointF(50, 50));
|
||||
erasing->continueBrush(QPointF(55, 50));
|
||||
QCOMPARE(painted(*erasing), QPointF(50, 50));
|
||||
// A cancelled stroke leaves no anchor for the next.
|
||||
const auto session = painting(10);
|
||||
session->beginBrush(QPointF(50, 50));
|
||||
session->continueBrush(QPointF(80, 50));
|
||||
session->cancelBrush();
|
||||
session->beginBrush(QPointF(100, 100));
|
||||
session->continueBrush(QPointF(115, 100));
|
||||
QCOMPARE(painted(*session), QPointF(105, 100));
|
||||
// A cancel drops the hand's point: nothing lands.
|
||||
session->cancelBrush();
|
||||
QVERIFY(session->finishBrushImmediately());
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(BrushSmoothingTests)
|
||||
#include "BrushSmoothingTests.moc"
|
||||
@@ -119,7 +119,7 @@ void EffectsPanelTests::theFooterAddsEachEffectAndOpensItsPanel()
|
||||
QStringList names;
|
||||
for (const QAction *entry : button.menu()->actions())
|
||||
names << entry->text();
|
||||
QCOMPARE(names, QStringList({"Stroke…", "Drop Shadow…", "Color Overlay…", "Inner Shadow…"}));
|
||||
QCOMPARE(names, QStringList({"Stroke…", "Drop Shadow…", "Color Overlay…", "Inner Shadow…", "Outer Glow…"}));
|
||||
QVERIFY(button.isEnabled());
|
||||
button.menu()->actions()[0]->trigger();
|
||||
QVERIFY((editor.session.effectsEditing() == LayerEffectSelection{editor.layer, LayerEffectKind::stroke}));
|
||||
@@ -291,6 +291,7 @@ void EffectsPanelTests::eachKindHasSwiftsControls_data()
|
||||
QTest::newRow("overlay") << LayerEffectKind::colorOverlay << QString("Color Overlay") << QStringList{"Opacity:0:100:100"};
|
||||
QTest::newRow("inner shadow") << LayerEffectKind::innerShadow << QString("Inner Shadow")
|
||||
<< QStringList{"Opacity:0:100:100", "Angle:-180:180:180", "Distance:0:5000:50", "Blur:0:500:100"};
|
||||
QTest::newRow("outer glow") << LayerEffectKind::outerGlow << QString("Outer Glow") << QStringList{"Size:0:500:100", "Opacity:0:100:100"};
|
||||
}
|
||||
|
||||
void EffectsPanelTests::eachKindHasSwiftsControls()
|
||||
@@ -309,6 +310,8 @@ void EffectsPanelTests::eachKindHasSwiftsControls()
|
||||
head = label;
|
||||
}
|
||||
QVERIFY(head && head->font().pixelSize() == 13 && head->font().weight() == QFont::DemiBold);
|
||||
// The stroke chooses its side; the rest show their colour.
|
||||
QCOMPARE(sheet.findChild<QWidget *>("strokePosition") != nullptr, kind == LayerEffectKind::stroke);
|
||||
// Swift's fields in order, each clamped to its range's ends.
|
||||
QStringList names;
|
||||
for (const QLineEdit *field : sheet.findChildren<QLineEdit *>())
|
||||
|
||||
@@ -144,10 +144,11 @@ void EffectsPreviewCacheTests::settingsKeepTheLastPreviewButNewPixelsDropIt()
|
||||
// Same kinds: the old preview shows while the new renders.
|
||||
layer.effects = outline(5);
|
||||
QCOMPARE(ask(cache, layer, landings).value().image.cacheKey(), first);
|
||||
QCOMPARE(landed(cache, layer, landings, 2).inset, 7.0);
|
||||
// Another kind: nothing is shown until it lands.
|
||||
const EffectsPreviewCache::Result second = landed(cache, layer, landings, 2);
|
||||
QCOMPARE(second.inset, 7.0);
|
||||
// Another kind shown or hidden: the last preview stands.
|
||||
layer.effects->colorOverlay = ColorOverlayEffect();
|
||||
QVERIFY(!ask(cache, layer, landings));
|
||||
QCOMPARE(ask(cache, layer, landings).value().image.cacheKey(), second.image.cacheKey());
|
||||
landed(cache, layer, landings, 3);
|
||||
// New pixels, or a mask switched on, drop it too.
|
||||
layer.asset = ImportedImage(filled(20, 10, QColor(255, 0, 0)), QImage(), QStringLiteral("Red"));
|
||||
@@ -318,6 +319,7 @@ void EffectsPreviewCacheTests::aReducedPreviewScalesEveryLength()
|
||||
effects.shadow = ShadowEffect{.angle = 45, .distance = 8, .blur = 3, .red = 1, .green = 0, .blue = 0, .opacity = 0.8};
|
||||
effects.colorOverlay = ColorOverlayEffect{.red = 0, .green = 1, .blue = 0, .opacity = 0.5};
|
||||
effects.innerShadow = InnerShadowEffect{.angle = 120, .distance = 30, .blur = 12, .red = 0, .green = 0, .blue = 0, .opacity = 0.7};
|
||||
effects.outerGlow = OuterGlowEffect{.size = 5, .red = 0, .green = 0, .blue = 1, .opacity = 0.6};
|
||||
const QImage image = noise(6000, 100, 7);
|
||||
const ImageLayer layer = layerOf(image, effects);
|
||||
const Landings landings;
|
||||
@@ -332,6 +334,7 @@ void EffectsPreviewCacheTests::aReducedPreviewScalesEveryLength()
|
||||
effects.shadow->blur *= factor;
|
||||
effects.innerShadow->distance *= factor;
|
||||
effects.innerShadow->blur *= factor;
|
||||
effects.outerGlow->size *= factor;
|
||||
QVERIFY(result.image == LayerEffectsRenderer::render(reduced, std::nullopt, effects).image);
|
||||
}
|
||||
|
||||
|
||||
@@ -88,7 +88,7 @@ QImage replayed(const QImage &pixels, const LayerEffects &effects)
|
||||
}
|
||||
const auto on = [](const auto &effect) { return effect && effect->isEnabled() && effect->opacity > 0; };
|
||||
const bool stroke = on(effects.stroke) && effects.stroke->size > 0;
|
||||
std::vector<float> ring(shader.shape.size()), cast(shader.shape.size()), inner(shader.shape.size());
|
||||
std::vector<float> ring(shader.shape.size()), cast(shader.shape.size()), inner(shader.shape.size()), glow(shader.shape.size());
|
||||
if (stroke) {
|
||||
const int reach = std::max(1, int(std::lround(effects.stroke->size)));
|
||||
const std::vector<float> moved = shader.spread(shader.spread(shader.shape, reach, effects.stroke->inside, true), reach, effects.stroke->inside, false);
|
||||
@@ -98,6 +98,9 @@ QImage replayed(const QImage &pixels, const LayerEffects &effects)
|
||||
if (on(effects.shadow))
|
||||
cast = shader.softened(shader.shift(shader.shape, float(effects.shadow->offset().width()), float(effects.shadow->offset().height())),
|
||||
float(effects.shadow->blur / 2));
|
||||
const bool glowing = on(effects.outerGlow) && effects.outerGlow->size > 0;
|
||||
if (glowing)
|
||||
glow = shader.softened(shader.shape, float(effects.outerGlow->size / 2));
|
||||
if (on(effects.innerShadow)) {
|
||||
const std::vector<float> moved = shader.softened(shader.shift(shader.shape, float(effects.innerShadow->offset().width()),
|
||||
float(effects.innerShadow->offset().height())),
|
||||
@@ -118,6 +121,8 @@ QImage replayed(const QImage &pixels, const LayerEffects &effects)
|
||||
};
|
||||
if (on(effects.shadow))
|
||||
blend(effects.shadow->color(), std::clamp(cast[index] * float(effects.shadow->opacity), 0.f, 1.f));
|
||||
if (glowing)
|
||||
blend(effects.outerGlow->color(), std::clamp(glow[index] * (1.f - shader.shape[index]) * float(effects.outerGlow->opacity), 0.f, 1.f));
|
||||
const float strokeCoverage = stroke ? std::clamp(ring[index] * float(effects.stroke->opacity), 0.f, 1.f) : 0.f;
|
||||
if (stroke && !effects.stroke->inside)
|
||||
blend(effects.stroke->color(), strokeCoverage);
|
||||
@@ -189,16 +194,19 @@ private slots:
|
||||
void LayerEffectsKernelTests::everyEffectMatchesTheShader_data()
|
||||
{
|
||||
QTest::addColumn<LayerEffects>("effects");
|
||||
LayerEffects outside, inside, shadow, overlay, inner, all, wide;
|
||||
LayerEffects outside, inside, shadow, overlay, inner, glow, hair, all, wide;
|
||||
outside.stroke = StrokeEffect{.size = 3, .red = 1, .green = 0.5, .blue = 0.25, .opacity = 0.8};
|
||||
inside.stroke = StrokeEffect{.size = 2.5, .red = 0.1, .green = 0.2, .blue = 0.9, .opacity = 1, .inside = true};
|
||||
shadow.shadow = ShadowEffect{.angle = 45, .distance = 3.5, .blur = 4, .red = 0.3, .green = 0.1, .blue = 0.6, .opacity = 0.75};
|
||||
overlay.colorOverlay = ColorOverlayEffect{.red = 0.9, .green = 0.8, .blue = 0.1, .opacity = 0.5};
|
||||
inner.innerShadow = InnerShadowEffect{.angle = -60, .distance = 2, .blur = 3, .red = 0, .green = 0.4, .blue = 0.2, .opacity = 0.9};
|
||||
glow.outerGlow = OuterGlowEffect{.size = 6, .red = 0.2, .green = 0.9, .blue = 0.4, .opacity = 0.8};
|
||||
hair.outerGlow = OuterGlowEffect{.size = 0.01, .opacity = 1};
|
||||
all = outside;
|
||||
all.shadow = shadow.shadow;
|
||||
all.colorOverlay = overlay.colorOverlay;
|
||||
all.innerShadow = inner.innerShadow;
|
||||
all.outerGlow = glow.outerGlow;
|
||||
all.shadow->blur = 0;
|
||||
wide.stroke = StrokeEffect{.size = 9, .red = 1, .opacity = 1};
|
||||
wide.shadow = ShadowEffect{.angle = 200, .distance = 0.5, .blur = 0.01, .opacity = 1};
|
||||
@@ -207,6 +215,8 @@ void LayerEffectsKernelTests::everyEffectMatchesTheShader_data()
|
||||
QTest::newRow("drop shadow") << shadow;
|
||||
QTest::newRow("colour overlay") << overlay;
|
||||
QTest::newRow("inner shadow") << inner;
|
||||
QTest::newRow("outer glow") << glow;
|
||||
QTest::newRow("a glow a hair wide") << hair;
|
||||
QTest::newRow("all, a sharp shadow") << all;
|
||||
QTest::newRow("wide stroke, a barely blurred shadow") << wide;
|
||||
}
|
||||
@@ -233,11 +243,15 @@ void LayerEffectsKernelTests::switchedOffOrEmptyEffectsLeaveThePixels()
|
||||
hidden.shadow = ShadowEffect{.enabled = false, .distance = 2, .blur = 0, .opacity = 1};
|
||||
hidden.colorOverlay = ColorOverlayEffect{.enabled = false, .red = 1};
|
||||
hidden.innerShadow = InnerShadowEffect{.enabled = false};
|
||||
hidden.outerGlow = OuterGlowEffect{.enabled = false};
|
||||
LayerEffects dim, flat;
|
||||
dim.outerGlow = OuterGlowEffect{.opacity = 0};
|
||||
flat.outerGlow = OuterGlowEffect{.size = 0};
|
||||
LayerEffects thin;
|
||||
thin.stroke = StrokeEffect{.size = 0, .red = 1};
|
||||
LayerEffects clear;
|
||||
clear.shadow = ShadowEffect{.opacity = 0};
|
||||
for (const LayerEffects &effects : {LayerEffects(), hidden, thin, clear})
|
||||
for (const LayerEffects &effects : {LayerEffects(), hidden, thin, clear, dim, flat})
|
||||
QCOMPARE(gap(LayerEffectsKernel::render(pixels, effects), pixels), 0);
|
||||
// Stroke sizes round to whole pixels, one at least.
|
||||
LayerEffects fraction, one;
|
||||
|
||||
@@ -20,17 +20,19 @@ void addRows()
|
||||
{
|
||||
QTest::addColumn<LayerEffects>("effects");
|
||||
const LayerEffects all = everyKind();
|
||||
LayerEffects outside, inside, cast, overlay, inner;
|
||||
LayerEffects outside, inside, cast, overlay, inner, glow;
|
||||
outside.stroke = all.stroke;
|
||||
inside.stroke = StrokeEffect{.size = 10, .red = 1, .green = 0, .blue = 1, .opacity = 0.9, .inside = true};
|
||||
cast.shadow = all.shadow;
|
||||
overlay.colorOverlay = all.colorOverlay;
|
||||
inner.innerShadow = all.innerShadow;
|
||||
glow.outerGlow = OuterGlowEffect{.size = 12, .red = 0, .green = 1, .blue = 1, .opacity = 0.9};
|
||||
QTest::newRow("outside stroke") << outside;
|
||||
QTest::newRow("inside stroke") << inside;
|
||||
QTest::newRow("shadow") << cast;
|
||||
QTest::newRow("overlay") << overlay;
|
||||
QTest::newRow("inner shadow") << inner;
|
||||
QTest::newRow("outer glow") << glow;
|
||||
QTest::newRow("every kind") << all;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ LayerEffects every()
|
||||
effects.shadow = ShadowEffect{.angle = 45, .distance = 12, .blur = 6, .red = 0.5, .green = 0.6, .blue = 0.7, .opacity = 0.8};
|
||||
effects.colorOverlay = ColorOverlayEffect{.enabled = true, .red = 0.9, .green = 0.25, .blue = 0.75, .opacity = 0.35};
|
||||
effects.innerShadow = InnerShadowEffect{.angle = -30, .distance = 3, .blur = 2, .red = 0.05, .green = 0.15, .blue = 0.95, .opacity = 0.65};
|
||||
effects.outerGlow = OuterGlowEffect{.size = 33, .red = 0.2, .green = 0.4, .blue = 0.6, .opacity = 0.55};
|
||||
return effects;
|
||||
}
|
||||
|
||||
@@ -79,7 +80,8 @@ private slots:
|
||||
void LayerEffectsTests::kindsKeepSwiftsNamesAndOrder()
|
||||
{
|
||||
const std::pair<LayerEffectKind, const char *> kinds[] = {{LayerEffectKind::stroke, "Stroke"}, {LayerEffectKind::shadow, "Drop Shadow"},
|
||||
{LayerEffectKind::colorOverlay, "Color Overlay"}, {LayerEffectKind::innerShadow, "Inner Shadow"}};
|
||||
{LayerEffectKind::colorOverlay, "Color Overlay"}, {LayerEffectKind::innerShadow, "Inner Shadow"},
|
||||
{LayerEffectKind::outerGlow, "Outer Glow"}};
|
||||
QCOMPARE(allLayerEffectKinds.size(), std::size(kinds));
|
||||
for (size_t index = 0; index < std::size(kinds); ++index) {
|
||||
QVERIFY(allLayerEffectKinds[index] == kinds[index].first);
|
||||
@@ -88,6 +90,7 @@ void LayerEffectsTests::kindsKeepSwiftsNamesAndOrder()
|
||||
// Swift's defaults.
|
||||
QVERIFY((StrokeEffect() == StrokeEffect{std::nullopt, 4, 0, 0, 0, 1, false}));
|
||||
QVERIFY((ShadowEffect() == ShadowEffect{std::nullopt, 90, 20, 20, 0, 0, 0, 0.5}));
|
||||
QVERIFY((OuterGlowEffect() == OuterGlowEffect{std::nullopt, 20, 1, 1, 1, 0.75}));
|
||||
QVERIFY((ColorOverlayEffect() == ColorOverlayEffect{std::nullopt, 0, 0, 0, 1}));
|
||||
QVERIFY((InnerShadowEffect() == InnerShadowEffect{std::nullopt, 90, 10, 10, 0, 0, 0, 0.5}));
|
||||
QCOMPARE(StrokeEffect::maxSize, 500.0);
|
||||
@@ -165,7 +168,7 @@ void LayerEffectsTests::theHelpersReadAndWriteOneKind()
|
||||
for (const LayerEffectKind kind : allLayerEffectKinds) {
|
||||
LayerEffects full = every();
|
||||
full.remove(kind);
|
||||
QVERIFY(!full.contains(kind) && full.kinds().size() == 3);
|
||||
QVERIFY(!full.contains(kind) && full.kinds().size() == 4);
|
||||
full.setColor(PaletteColor{1, 0, 0}, kind);
|
||||
QVERIFY(!full.color(kind));
|
||||
}
|
||||
@@ -190,7 +193,7 @@ void LayerEffectsTests::theHelpersReadAndWriteOneKind()
|
||||
void LayerEffectsTests::theManifestWritesSwiftsKeys()
|
||||
{
|
||||
const QJsonObject encoded = ManifestJson::encoded(every());
|
||||
QCOMPARE(encoded.keys(), (QStringList{"colorOverlay", "innerShadow", "shadow", "stroke"}));
|
||||
QCOMPARE(encoded.keys(), (QStringList{"colorOverlay", "innerShadow", "outerGlow", "shadow", "stroke"}));
|
||||
const QJsonObject stroke = encoded.value("stroke").toObject();
|
||||
QCOMPARE(stroke.keys(), (QStringList{"blue", "enabled", "green", "inside", "opacity", "red", "size"}));
|
||||
QVERIFY(stroke.value("enabled") == false && stroke.value("inside") == true && stroke.value("size") == 7);
|
||||
@@ -198,6 +201,8 @@ void LayerEffectsTests::theManifestWritesSwiftsKeys()
|
||||
QCOMPARE(encoded.value("shadow").toObject().keys(), (QStringList{"angle", "blue", "blur", "distance", "green", "opacity", "red"}));
|
||||
QCOMPARE(encoded.value("colorOverlay").toObject().keys(), (QStringList{"blue", "enabled", "green", "opacity", "red"}));
|
||||
QCOMPARE(encoded.value("innerShadow").toObject().value("angle"), QJsonValue(-30));
|
||||
QCOMPARE(encoded.value("outerGlow").toObject().keys(), (QStringList{"blue", "green", "opacity", "red", "size"}));
|
||||
QCOMPARE(encoded.value("outerGlow").toObject().value("size"), QJsonValue(33));
|
||||
QVERIFY(ManifestJson::effects(encoded) == every());
|
||||
// Absent effects stay out; null reads as absent.
|
||||
LayerEffects one;
|
||||
|
||||
@@ -0,0 +1,233 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include "Document/LayerEffects+Renderer.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "IO/ProjectStore+Json.h"
|
||||
#include <QPainter>
|
||||
#include <QtTest>
|
||||
|
||||
// Swift's OuterGlowTests: the glow's model, drawing and export.
|
||||
namespace {
|
||||
// A white square centred on clear, Swift's createSquareImage.
|
||||
QImage square(int size = 40, int inner = 20)
|
||||
{
|
||||
QImage image(size, size, QImage::Format_RGBA8888_Premultiplied);
|
||||
image.fill(Qt::transparent);
|
||||
QPainter painter(&image);
|
||||
const int origin = (size - inner) / 2;
|
||||
painter.fillRect(QRect(origin, origin, inner, inner), Qt::white);
|
||||
return image;
|
||||
}
|
||||
|
||||
QColor at(const LayerEffectsRenderer::Rendered &rendered, int x, int y)
|
||||
{
|
||||
const int inset = int(rendered.inset);
|
||||
return rendered.image.pixelColor(inset + x, inset + y);
|
||||
}
|
||||
|
||||
LayerEffects glowing(double size, double red, double green, double blue, double opacity)
|
||||
{
|
||||
LayerEffects effects;
|
||||
effects.outerGlow = OuterGlowEffect{.enabled = true, .size = size, .red = red, .green = green, .blue = blue, .opacity = opacity};
|
||||
return effects;
|
||||
}
|
||||
}
|
||||
|
||||
class OuterGlowTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void outerGlowDefaultsAndValidation();
|
||||
void outerGlowCodableRoundTrip();
|
||||
void layerEffectsIntegration();
|
||||
void layerEffectsCodableBackwardCompatibility();
|
||||
void outerGlowRendersOmnidirectionally();
|
||||
void outerGlowSizeAndOpacityVariations();
|
||||
void outerGlowRendersAroundTextGlyphs();
|
||||
void outerGlowCombinedWithStrokeAndDropShadow();
|
||||
void outerGlowPreservedInExport();
|
||||
void theGlowWidensTheMargin();
|
||||
void theSessionAddsCopiesAndCancelsIt();
|
||||
};
|
||||
|
||||
void OuterGlowTests::outerGlowDefaultsAndValidation()
|
||||
{
|
||||
const OuterGlowEffect glow;
|
||||
QVERIFY(glow.isEnabled() && glow.isValid());
|
||||
QCOMPARE(glow.size, 20.0);
|
||||
QCOMPARE(glow.opacity, 0.75);
|
||||
QVERIFY(glow.color() == (PaletteColor{1, 1, 1}));
|
||||
for (const auto &broken : std::vector<std::function<void(OuterGlowEffect &)>>{
|
||||
[](OuterGlowEffect &e) { e.size = -1; }, [](OuterGlowEffect &e) { e.size = 500.5; }, [](OuterGlowEffect &e) { e.size = qQNaN(); },
|
||||
[](OuterGlowEffect &e) { e.opacity = 1.5; }, [](OuterGlowEffect &e) { e.red = 2; }, [](OuterGlowEffect &e) { e.green = -0.1; },
|
||||
[](OuterGlowEffect &e) { e.blue = qInf(); }}) {
|
||||
OuterGlowEffect changed = glow;
|
||||
broken(changed);
|
||||
QVERIFY(!changed.isValid());
|
||||
LayerEffects effects;
|
||||
effects.outerGlow = changed;
|
||||
QVERIFY(!effects.isValid());
|
||||
}
|
||||
OuterGlowEffect edge = glow;
|
||||
edge.size = 500;
|
||||
QVERIFY(edge.isValid());
|
||||
}
|
||||
|
||||
void OuterGlowTests::outerGlowCodableRoundTrip()
|
||||
{
|
||||
const LayerEffects original = glowing(35, 0.2, 0.8, 1, 0.6);
|
||||
const QJsonObject encoded = ManifestJson::encoded(original);
|
||||
const QJsonObject glow = encoded.value("outerGlow").toObject();
|
||||
QCOMPARE(glow.keys(), (QStringList{"blue", "enabled", "green", "opacity", "red", "size"}));
|
||||
QCOMPARE(ManifestJson::effects(encoded), original);
|
||||
// Absent `enabled` stays absent, read as shown.
|
||||
LayerEffects shown = original;
|
||||
shown.outerGlow->enabled = std::nullopt;
|
||||
QVERIFY(!ManifestJson::encoded(shown).value("outerGlow").toObject().contains("enabled"));
|
||||
QCOMPARE(ManifestJson::effects(ManifestJson::encoded(shown)), shown);
|
||||
// A key of the wrong type, or missing, refuses it.
|
||||
QJsonObject wrong = encoded;
|
||||
QJsonObject bent = glow;
|
||||
bent.insert("size", "big");
|
||||
wrong.insert("outerGlow", bent);
|
||||
QVERIFY_THROWS_EXCEPTION(ProjectError, ManifestJson::effects(wrong));
|
||||
bent = glow;
|
||||
bent.remove("opacity");
|
||||
wrong.insert("outerGlow", bent);
|
||||
QVERIFY_THROWS_EXCEPTION(ProjectError, ManifestJson::effects(wrong));
|
||||
}
|
||||
|
||||
void OuterGlowTests::layerEffectsIntegration()
|
||||
{
|
||||
LayerEffects effects;
|
||||
QVERIFY(effects.isEmpty() && !effects.contains(LayerEffectKind::outerGlow));
|
||||
effects.outerGlow = OuterGlowEffect{.size = 25, .red = 1, .green = 0, .blue = 0, .opacity = 0.8};
|
||||
QVERIFY(!effects.isEmpty() && effects.contains(LayerEffectKind::outerGlow) && effects.isEnabled(LayerEffectKind::outerGlow));
|
||||
QVERIFY(effects.color(LayerEffectKind::outerGlow) == (PaletteColor{1, 0, 0}));
|
||||
QVERIFY((effects.kinds() == std::vector{LayerEffectKind::outerGlow}));
|
||||
effects.setColor(PaletteColor{0, 1, 0}, LayerEffectKind::outerGlow);
|
||||
QVERIFY(effects.outerGlow->green == 1 && effects.outerGlow->red == 0);
|
||||
effects.setEnabled(false, LayerEffectKind::outerGlow);
|
||||
QVERIFY(!effects.isEnabled(LayerEffectKind::outerGlow) && !effects.visible().outerGlow);
|
||||
effects.remove(LayerEffectKind::outerGlow);
|
||||
QVERIFY(!effects.outerGlow && effects.isEmpty());
|
||||
}
|
||||
|
||||
void OuterGlowTests::layerEffectsCodableBackwardCompatibility()
|
||||
{
|
||||
// Effects written before the glow read without it.
|
||||
const QJsonObject older{{"stroke", QJsonObject{{"size", 3}, {"red", 0}, {"green", 0}, {"blue", 0}, {"opacity", 1}, {"inside", false}}}};
|
||||
LayerEffects decoded = ManifestJson::effects(older);
|
||||
QCOMPARE(decoded.stroke.value().size, 3.0);
|
||||
QVERIFY(!decoded.outerGlow && decoded.isValid());
|
||||
decoded.outerGlow = OuterGlowEffect{.size = 15, .red = 1, .green = 0.5, .blue = 0, .opacity = 0.9};
|
||||
const LayerEffects again = ManifestJson::effects(ManifestJson::encoded(decoded));
|
||||
QCOMPARE(again.outerGlow.value().size, 15.0);
|
||||
QCOMPARE(again.outerGlow.value().opacity, 0.9);
|
||||
}
|
||||
|
||||
void OuterGlowTests::outerGlowRendersOmnidirectionally()
|
||||
{
|
||||
const LayerEffectsRenderer::Rendered rendered = LayerEffectsRenderer::render(square(), std::nullopt, glowing(10, 0, 1, 0, 1));
|
||||
QCOMPARE(rendered.inset, 32.0);
|
||||
// The square keeps its white; the glow shows all round.
|
||||
const QColor centre = at(rendered, 20, 20);
|
||||
QVERIFY(centre.alphaF() > 0.95 && centre.redF() > 0.95 && centre.greenF() > 0.95 && centre.blueF() > 0.95);
|
||||
const QColor sides[] = {at(rendered, 5, 20), at(rendered, 35, 20), at(rendered, 20, 5), at(rendered, 20, 35)};
|
||||
for (const QColor &side : sides) {
|
||||
QVERIFY(side.alphaF() > 0.1);
|
||||
QVERIFY(side.greenF() > 0.8 && side.redF() < 0.05);
|
||||
QVERIFY(std::abs(side.alphaF() - sides[0].alphaF()) < 0.05);
|
||||
}
|
||||
// Nothing reaches past three sigmas and a pixel.
|
||||
QCOMPARE(at(rendered, -17, 20).alpha(), 0);
|
||||
}
|
||||
|
||||
void OuterGlowTests::outerGlowSizeAndOpacityVariations()
|
||||
{
|
||||
const QImage image = square();
|
||||
const LayerEffectsRenderer::Rendered small = LayerEffectsRenderer::render(image, std::nullopt, glowing(4, 1, 0, 0, 1));
|
||||
const LayerEffectsRenderer::Rendered large = LayerEffectsRenderer::render(image, std::nullopt, glowing(20, 1, 0, 0, 1));
|
||||
QVERIFY(large.inset > small.inset);
|
||||
QVERIFY(at(large, 2, 20).alphaF() > at(small, 2, 20).alphaF());
|
||||
const LayerEffectsRenderer::Rendered low = LayerEffectsRenderer::render(image, std::nullopt, glowing(10, 0, 0, 1, 0.2));
|
||||
const LayerEffectsRenderer::Rendered high = LayerEffectsRenderer::render(image, std::nullopt, glowing(10, 0, 0, 1, 1));
|
||||
QVERIFY(at(high, 5, 20).alphaF() > at(low, 5, 20).alphaF());
|
||||
}
|
||||
|
||||
void OuterGlowTests::outerGlowRendersAroundTextGlyphs()
|
||||
{
|
||||
// A T: a bar over a stem, white on clear.
|
||||
QImage glyph(60, 60, QImage::Format_RGBA8888_Premultiplied);
|
||||
glyph.fill(Qt::transparent);
|
||||
QPainter painter(&glyph);
|
||||
painter.fillRect(QRect(15, 15, 30, 8), Qt::white);
|
||||
painter.fillRect(QRect(26, 23, 8, 22), Qt::white);
|
||||
painter.end();
|
||||
const LayerEffectsRenderer::Rendered rendered = LayerEffectsRenderer::render(glyph, std::nullopt, glowing(8, 0, 1, 1, 1));
|
||||
const QColor stem = at(rendered, 30, 30), above = at(rendered, 30, 12), notch = at(rendered, 20, 27);
|
||||
QVERIFY(stem.alphaF() > 0.9 && stem.redF() > 0.9 && stem.greenF() > 0.9 && stem.blueF() > 0.9);
|
||||
QVERIFY(above.alphaF() > 0.05 && above.greenF() > 0.5 && above.blueF() > 0.5);
|
||||
QVERIFY(notch.alphaF() > 0.05 && notch.greenF() > 0.5 && notch.blueF() > 0.5);
|
||||
}
|
||||
|
||||
void OuterGlowTests::outerGlowCombinedWithStrokeAndDropShadow()
|
||||
{
|
||||
LayerEffects effects = glowing(10, 1, 0, 0, 1);
|
||||
effects.stroke = StrokeEffect{.size = 3, .red = 0, .green = 0, .blue = 0, .opacity = 1, .inside = false};
|
||||
effects.shadow = ShadowEffect{.enabled = true, .angle = 180, .distance = 25, .blur = 4, .red = 0, .green = 0, .blue = 1, .opacity = 1};
|
||||
const LayerEffectsRenderer::Rendered rendered = LayerEffectsRenderer::render(square(50, 20), std::nullopt, effects);
|
||||
const QColor centre = at(rendered, 25, 25);
|
||||
QVERIFY(centre.redF() > 0.9 && centre.greenF() > 0.9 && centre.blueF() > 0.9);
|
||||
// The stroke lies over the glow, the glow over shadow.
|
||||
const QColor stroke = at(rendered, 13, 25);
|
||||
QVERIFY(stroke.alphaF() > 0.9 && stroke.redF() < 0.2 && stroke.greenF() < 0.2 && stroke.blueF() < 0.2);
|
||||
const QColor glow = at(rendered, 10, 25);
|
||||
QVERIFY(glow.alphaF() > 0.05 && glow.redF() > 0.6);
|
||||
const QColor shadow = at(rendered, 50, 25);
|
||||
QVERIFY(shadow.alphaF() > 0.1 && shadow.blueF() > 0.6);
|
||||
}
|
||||
|
||||
void OuterGlowTests::outerGlowPreservedInExport()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(100, 100);
|
||||
session.insert(ImportedImage(square(30, 10), QImage(), QStringLiteral("GlowLayer")), QPointF(35, 35));
|
||||
session.setEffects(glowing(15, 1, 0.5, 0, 1));
|
||||
const QImage exported = ImageExporter::render(session.projectSnapshot().value()).image;
|
||||
QVERIFY(exported.pixelColor(35, 35).alphaF() > 0.9);
|
||||
const QColor glow = exported.pixelColor(27, 35);
|
||||
QVERIFY(glow.alphaF() > 0.1 && glow.redF() > 0.6);
|
||||
}
|
||||
|
||||
void OuterGlowTests::theGlowWidensTheMargin()
|
||||
{
|
||||
LayerEffects effects = glowing(7.2, 1, 1, 1, 1);
|
||||
QCOMPARE(LayerEffectsRenderer::margin(effects), 24.0);
|
||||
effects.outerGlow->enabled = false;
|
||||
QCOMPARE(LayerEffectsRenderer::margin(effects), 2.0);
|
||||
}
|
||||
|
||||
void OuterGlowTests::theSessionAddsCopiesAndCancelsIt()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(40, 40);
|
||||
session.insert(ImportedImage(square(), QImage(), QStringLiteral("One")));
|
||||
const QUuid first = session.activeLayerID().value();
|
||||
session.addEffect(LayerEffectKind::outerGlow);
|
||||
QCOMPARE(session.history.undoName(), QString("Add Outer Glow"));
|
||||
QVERIFY(session.activeEffects().outerGlow == OuterGlowEffect());
|
||||
session.changeEffects([](LayerEffects &effects) { effects.outerGlow->size = 44; });
|
||||
session.finishEffectsEditing(false);
|
||||
QVERIFY(!session.activeEffects().outerGlow);
|
||||
session.addEffect(LayerEffectKind::outerGlow);
|
||||
session.changeEffects([](LayerEffects &effects) { effects.outerGlow->size = 44; });
|
||||
session.finishEffectsEditing(true);
|
||||
session.insert(ImportedImage(square(), QImage(), QStringLiteral("Two")));
|
||||
const QUuid second = session.activeLayerID().value();
|
||||
QVERIFY(session.canCopyEffect(LayerEffectKind::outerGlow, first, second));
|
||||
session.copyEffect(LayerEffectKind::outerGlow, first, second);
|
||||
QCOMPARE(session.history.undoName(), QString("Copy Outer Glow"));
|
||||
QCOMPARE(session.activeEffects().outerGlow.value().size, 44.0);
|
||||
}
|
||||
|
||||
QTEST_MAIN(OuterGlowTests)
|
||||
#include "OuterGlowTests.moc"
|
||||
@@ -1,4 +1,6 @@
|
||||
#include "MenuFixtures.h"
|
||||
#include "Rendering/EditorCanvas.h"
|
||||
#include "UI/NativeLayerList.h"
|
||||
#include <QApplication>
|
||||
#include <QLineEdit>
|
||||
#include <QRegularExpression>
|
||||
@@ -23,9 +25,12 @@ private slots:
|
||||
void SelectMenuTests::selectEntriesFollowTheSelectionAndActOnIt()
|
||||
{
|
||||
Bar bar;
|
||||
for (const char *name : {"selectAll", "deselect", "inverse", "layerPixels", "subject", "maskBlackAreas", "expandSelection", "contractSelection",
|
||||
"featherSelection"})
|
||||
for (const char *name : {"deselect", "inverse", "layerPixels", "subject", "maskBlackAreas", "expandSelection", "contractSelection", "featherSelection"})
|
||||
QVERIFY2(!bar.action(name).isEnabled(), name);
|
||||
// Select All never rests; without a canvas it does nothing.
|
||||
QVERIFY(bar.action("selectAll").isEnabled());
|
||||
bar.action("selectAll").trigger();
|
||||
QVERIFY(!bar.session().document());
|
||||
bar.session().createDocument(50, 50, true);
|
||||
QVERIFY(bar.action("subject").isEnabled());
|
||||
QVERIFY(bar.action("selectAll").isEnabled());
|
||||
@@ -95,10 +100,14 @@ void SelectMenuTests::selectAllInAFieldIsTheFields()
|
||||
Bar bar;
|
||||
bar.window.show();
|
||||
QVERIFY(QTest::qWaitForWindowExposed(&bar.window));
|
||||
// Without a canvas the entry rests; the welcome keeps Ctrl+A.
|
||||
// Without a canvas the entry selects the welcome's field.
|
||||
QLineEdit &width = *bar.window.findChild<QLineEdit *>("widthInput");
|
||||
QTRY_VERIFY(width.hasFocus());
|
||||
QVERIFY(!bar.action("selectAll").isEnabled());
|
||||
width.setText(QStringLiteral("640"));
|
||||
width.deselect();
|
||||
QVERIFY(bar.action("selectAll").isEnabled());
|
||||
bar.action("selectAll").trigger();
|
||||
QCOMPARE(width.selectedText(), QString("640"));
|
||||
bar.session().createDocument(50, 50, true);
|
||||
QTRY_VERIFY(bar.action("selectAll").isEnabled());
|
||||
// A rename in progress: the editor's text, never the canvas.
|
||||
@@ -112,6 +121,19 @@ void SelectMenuTests::selectAllInAFieldIsTheFields()
|
||||
QVERIFY(!bar.session().selection().has_value());
|
||||
QTest::keyClick(editor, Qt::Key_Escape);
|
||||
QTRY_VERIFY(!bar.session().renamingLayerID().has_value());
|
||||
// The list holds the keys: its rows, as Swift's table.
|
||||
bar.session().addBlankLayer();
|
||||
NativeLayerList *list = bar.window.findChild<NativeLayerList *>();
|
||||
list->setFocus();
|
||||
QTRY_VERIFY(list->hasFocus());
|
||||
const std::vector<ImageLayer> &layers = bar.session().document().value().layers;
|
||||
bar.action("selectAll").trigger();
|
||||
QCOMPARE(bar.session().selectedLayerIDs(), (QSet<QUuid>{layers[0].id, layers[1].id}));
|
||||
QCOMPARE(bar.session().activeLayerID(), std::optional(layers[1].id));
|
||||
QVERIFY(!bar.session().selection().has_value());
|
||||
// Elsewhere it selects the canvas.
|
||||
bar.window.findChild<CanvasView *>()->setFocus();
|
||||
QTRY_VERIFY(!list->hasFocus());
|
||||
bar.action("selectAll").trigger();
|
||||
QVERIFY(bar.session().selection().has_value());
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
#include "AddressSpaceLimit.h"
|
||||
#include "RenderFixtures.h"
|
||||
#include "SelectionFixtures.h"
|
||||
#include <QElapsedTimer>
|
||||
|
||||
// Masks from a selection, the delete target, and Invert.
|
||||
namespace {
|
||||
@@ -61,7 +60,7 @@ private slots:
|
||||
void maskFromSelectionLinesUpOnScaledLayers();
|
||||
void deletingWithTheMaskTargetedRemovesOnlyTheMask();
|
||||
void invertKeepsTransparencyStaysInSelectionAndWorksOnMasks();
|
||||
void invertIsFastOnLargeImagesAndHandlesUniformMasksWithASelection();
|
||||
void invertHandlesLargeImagesAndUniformMasksWithASelection();
|
||||
void invertWorksInEveryTool();
|
||||
void invertIsRefusedWhereSwiftRefusesIt();
|
||||
void anInvertComputedFromOtherPixelsIsDropped();
|
||||
@@ -242,20 +241,16 @@ void SelectionEditTests::invertKeepsTransparencyStaysInSelectionAndWorksOnMasks(
|
||||
QCOMPARE(int(placed.constScanLine(20)[75]), 255);
|
||||
}
|
||||
|
||||
void SelectionEditTests::invertIsFastOnLargeImagesAndHandlesUniformMasksWithASelection()
|
||||
void SelectionEditTests::invertHandlesLargeImagesAndUniformMasksWithASelection()
|
||||
{
|
||||
const auto session = editSession(4000, 3000);
|
||||
QImage image = BrushRaster::context(4000, 3000, false);
|
||||
image.fill(Qt::red);
|
||||
session->insert(ImportedImage(image, image, "Big"));
|
||||
QElapsedTimer clock;
|
||||
clock.start();
|
||||
// No clock: in a parallel suite it measures load (7c4512a).
|
||||
invert(*session);
|
||||
const qint64 whole = clock.restart();
|
||||
select(*session, QRectF(0, 0, 2000, 3000));
|
||||
invert(*session);
|
||||
const qint64 selected = clock.elapsed();
|
||||
QVERIFY2(whole < 1500 && selected < 1500, qPrintable(QStringLiteral("whole %1 ms, selected %2 ms").arg(whole).arg(selected)));
|
||||
const QImage result = render(*session);
|
||||
// Inverted twice, then once.
|
||||
QCOMPARE(pixel(result, 100, 100), (std::vector<int>{255, 0, 0, 255}));
|
||||
|
||||
@@ -51,6 +51,7 @@ inline QStringList described(const EditorSession &session)
|
||||
result << QStringLiteral("brush %1 %2 %3 rgb %4 %5 %6 erase %7 mode %8 white %9 stroke %10 revision %11").arg(tip.diameter).arg(tip.hardness).arg(tip.opacity)
|
||||
.arg(tip.red).arg(tip.green).arg(tip.blue).arg(int(tip.erasing)).arg(rawValue(session.brushMode())).arg(int(session.maskPaintWhite()))
|
||||
.arg(session.brushStroke() ? int(session.brushStroke()->patches().size()) : -1).arg(session.brushRevision());
|
||||
result << QStringLiteral("smoothing %1").arg(tip.smoothing);
|
||||
if (session.filterEdit().has_value()) {
|
||||
const FilterEdit &edit = session.filterEdit().value();
|
||||
result << QStringLiteral("filter %1 %2 preview %3 preparing %4 committing %5 error %6 grown %7").arg(rawValue(edit.kind), edit.layerID.toString())
|
||||
|
||||
Reference in new issue
Block a user