#include "AcceptanceFixtures.h" #include "Document/LayerAppearance.h" #include "Document/SmudgeLiquify.h" #include "UI/NativeLayerList.h" #include "UI/TransformInspector.h" #include "UI/TypeControls.h" #include #include // The acceptance run, part five: 1.4.5's editing. class AcceptanceEditTests : public QObject { Q_OBJECT private slots: void initTestCase() { QStandardPaths::setTestModeEnabled(true); } void textPaintingSmearAndMasks(); void moveKeysAndBlending(); }; namespace { QImage filled(int width, int height, QColor color) { QImage image = BrushRaster::context(width, height, false); image.fill(color); return image; } QUuid add(App &app, const QImage &image, const QString &name, QPointF centre) { app.session().insert(ImportedImage(image, image, name), centre); return app.session().activeLayerID().value(); } int red(App &app, QUuid id, int x, int y) { for (const ImageLayer &layer : app.session().document().value().layers) if (layer.id == id) return qRed(layer.asset.value().image().pixel(x, y)); throw std::runtime_error("no such layer"); } // The canvas's pixel at a document point, at 100%. QColor onCanvas(App &app, QPointF point) { const QPointF view = app.session().viewport.viewPoint(point + QPointF(0.5, 0.5), app.session().document().value().size()); return app.canvas().grab().toImage().pixelColor(int(std::floor(view.x())), int(std::floor(view.y()))); } bool near(QColor a, QColor b) { return qAbs(a.red() - b.red()) <= 2 && qAbs(a.green() - b.green()) <= 2 && qAbs(a.blue() - b.blue()) <= 2; } } void AcceptanceEditTests::textPaintingSmearAndMasks() { App app; app.session().createDocument(1200, 800, true); // A face for the selected word, a preview, and Undo. app.session().selectTool(NavigationTool::type); app.press(QPointF(100, 200)); app.release(QPointF(100, 200)); QTRY_VERIFY(app.session().textDraft()); QTest::keyClicks(&app.canvas(), QStringLiteral("Hello World")); for (int letter = 0; letter < 5; ++letter) QTest::keyClick(&app.canvas(), Qt::Key_Left, Qt::ShiftModifier); auto *faces = app.window.findChild(QStringLiteral("typeFont")); QVERIFY(faces); const QString mono = QStringLiteral("DejaVuSansMono"); // Pointing at a face previews it; Escape puts it back. QTest::mouseMove(faces->view()->viewport(), popupRow(*faces, mono)); QTRY_COMPARE(app.session().textDraft().value().style.fontNameAt(6), mono); QTest::keyClick(faces->view(), Qt::Key_Escape); QTRY_VERIFY(app.session().textDraft().value().style.fontNameAt(6) != mono); choose(*faces, mono); QTRY_COMPARE(app.session().textDraft().value().style.fontRuns.value(), (std::vector{{6, 5, mono}})); app.window.activateWindow(); QVERIFY(QTest::qWaitForWindowActive(&app.window)); app.canvas().setFocus(); QTRY_VERIFY(app.canvas().hasFocus()); QTest::keyClick(&app.canvas(), Qt::Key_End); QTest::keyClicks(&app.canvas(), QStringLiteral("!!")); QTest::keyClick(&app.canvas(), Qt::Key_Z, Qt::ControlModifier); QCOMPARE(app.session().textDraft().value().style.content, QStringLiteral("Hello World")); QTest::keyClick(&app.canvas(), Qt::Key_Return, Qt::ControlModifier); QVERIFY(!app.session().textDraft()); // A brush paints in a scaled-down layer's own pixels. const QUuid half = add(app, filled(800, 800, Qt::black), QStringLiteral("Half"), QPointF(600, 400)); app.session().beginTransform(); LayerTransform quarter = app.session().activeLayer().value().transform; quarter.origin = QPointF(500, 300); quarter.size = QSizeF(200, 200); app.session().previewTransform(quarter); app.session().commitTransform(); app.session().selectTool(NavigationTool::brush); app.session().setBrushSettings({.diameter = 20, .hardness = 1, .red = 1, .green = 1, .blue = 1}); app.drag(QPointF(600, 330), QPointF(600, 470)); app.settle(); QCOMPARE(app.session().activeLayer().value().asset.value().size(), QSize(800, 800)); int painted = 0; for (int x = 0; x < 800; ++x) painted += red(app, half, x, 400) > 128; QVERIFY2(painted >= 76 && painted <= 84, qPrintable(QString::number(painted))); // A hidden layer says why it cannot paint. app.session().toggleLayerVisibility(half); app.press(QPointF(560, 400)); app.release(QPointF(560, 400)); QVERIFY(app.session().brushError().value().contains(QStringLiteral("is hidden"))); app.session().setBrushError(std::nullopt); app.session().toggleLayerVisibility(half); // The Blur brush's Radius sets how far it softens. QImage step = filled(400, 200, Qt::black); QPainter(&step).fillRect(200, 0, 200, 200, Qt::white); const QUuid edge = add(app, step, QStringLiteral("Edge"), QPointF(200, 600)); const auto softened = [&](const QString &radius) { app.session().selectTool(NavigationTool::blur); app.session().setBlurMode(BlurToolMode::blur); app.session().setBrushSettings({.diameter = 120, .hardness = 1, .opacity = 1}); // The bar's own Radius field. auto *field = app.window.findChild(QStringLiteral("blurRadius")); field->setFocus(); field->selectAll(); QTest::keyClicks(field, radius); QTest::keyClick(field, Qt::Key_Return); app.drag(QPointF(200, 520), QPointF(200, 680)); app.settle(); int soft = 0; for (int x = 100; x < 300; ++x) soft += red(app, edge, x, 100) > 10 && red(app, edge, x, 100) < 245; app.action("undo").trigger(); return soft; }; const int narrow = softened(QStringLiteral("2")), wide = softened(QStringLiteral("20")); QVERIFY2(narrow < 16 && wide > 40, qPrintable(QString("%1 %2").arg(narrow).arg(wide))); // Liquify keeps the stripes sharp as it pushes them. QImage stripes = filled(400, 200, Qt::black); for (int x = 0; x < 400; x += 20) QPainter(&stripes).fillRect(x, 0, 10, 200, Qt::white); const QUuid striped = add(app, stripes, QStringLiteral("Stripes"), QPointF(800, 600)); app.session().selectTool(NavigationTool::blur); app.session().setBlurMode(BlurToolMode::liquify); app.session().setBrushSettings({.diameter = 80, .hardness = 0.5, .opacity = 0.7}); app.drag(QPointF(720, 600), QPointF(840, 600)); app.settle(); int moved = 0, grey = 0; for (int x = 0; x < 400; ++x) { const int value = red(app, striped, x, 100); moved += value != qRed(stripes.pixel(x, 100)); grey += value > 40 && value < 215; } QVERIFY2(moved > 20 && grey * 5 < moved, qPrintable(QString("%1 %2").arg(moved).arg(grey))); // Smudge leaves one fading trail, no copies along it. QImage block = filled(400, 200, Qt::white); QPainter(&block).fillRect(0, 0, 100, 200, Qt::red); const QUuid smeared = add(app, block, QStringLiteral("Block"), QPointF(200, 200)); app.session().setBlurMode(BlurToolMode::smudge); app.session().setBrushSettings({.diameter = 60, .hardness = 0.5, .opacity = 0.8}); app.drag(QPointF(50, 200), QPointF(300, 200)); app.settle(); const auto green = [&](int x) { for (const ImageLayer &layer : app.session().document().value().layers) if (layer.id == smeared) return qGreen(layer.asset.value().image().pixel(x, 100)); throw std::runtime_error("no such layer"); }; QVERIFY2(green(110) < 200, qPrintable(QString::number(green(110)))); for (int x = 110; x < 300; x += 10) QVERIFY2(green(x + 10) >= green(x) - 3, qPrintable(QString("%1: %2 then %3").arg(x).arg(green(x)).arg(green(x + 10)))); app.session().selectLayer(striped); // The mask button reveals; Alt hides; Alt-click shows alone. app.session().selectTool(NavigationTool::marquee); app.drag(QPointF(650, 520), QPointF(750, 680)); auto *mask = app.window.findChild(QStringLiteral("addLayerMask")); QTest::mouseClick(mask, Qt::LeftButton); QImage shown = app.session().activeLayer().value().mask.value().asset.image(); QCOMPARE(qGray(shown.pixel(100, 100)), 255); QCOMPARE(qGray(shown.pixel(5, 100)), 0); app.action("undo").trigger(); QTest::mouseClick(mask, Qt::LeftButton, Qt::AltModifier); shown = app.session().activeLayer().value().mask.value().asset.image(); QVERIFY(qGray(shown.pixel(100, 100)) == 0 && qGray(shown.pixel(5, 100)) == 255); LayerThumbnailButton *thumbnail = nullptr; for (LayerCell *cell : app.window.findChild()->cells()) if (cell->layerID() == striped) thumbnail = cell->findChild("maskThumbnail"); QVERIFY(thumbnail); QTest::mouseClick(thumbnail, Qt::LeftButton, Qt::AltModifier, thumbnail->rect().center()); QCOMPARE(app.session().maskAloneLayer().value().id, striped); QTRY_VERIFY(app.window.findChild(QStringLiteral("maskAloneBadge"))->isVisible()); // The canvas shows the mask: black where hidden, white past. QTRY_COMPARE(onCanvas(app, QPointF(700, 600)), QColor(Qt::black)); QCOMPARE(onCanvas(app, QPointF(620, 600)), QColor(Qt::white)); QCOMPARE(onCanvas(app, QPointF(50, 150)), QColor(Qt::white)); app.shot(QStringLiteral("18-mask-alone")); } void AcceptanceEditTests::moveKeysAndBlending() { App app; QVERIFY(scene(1200, 800).save(app.path("photo.png"))); app.session().createDocument(1200, 800); app.importFile(app.path("photo.png")); // Held Ctrl flips Auto Select in the bar. app.session().selectTool(NavigationTool::move); auto *autoSelect = app.window.findChild(QStringLiteral("transformAutoSelect")); QVERIFY(autoSelect); const bool before = autoSelect->isChecked(); QTest::keyPress(&app.canvas(), Qt::Key_Control, Qt::ControlModifier); QCOMPARE(autoSelect->isChecked(), !before); QTest::keyRelease(&app.canvas(), Qt::Key_Control, Qt::NoModifier); QCOMPARE(autoSelect->isChecked(), before); // A typed width applies as one step on leaving. auto *width = app.window.findChild(QStringLiteral("transformW")); QVERIFY(width); const int steps = app.session().history.undoCount(); width->setFocus(); QTRY_VERIFY(width->hasFocus()); width->selectAll(); QTest::keyClicks(width, QStringLiteral("600")); app.canvas().setFocus(); QTRY_COMPARE(app.session().activeLayer().value().transform.size.width(), 600.0); QCOMPARE(app.session().history.undoCount(), steps + 1); app.action("undo").trigger(); // Soft Light and a Linear Dodge stack show as exported. app.session().zoom(1); const QUuid grey = add(app, filled(1200, 800, QColor(77, 77, 77)), QStringLiteral("Grey"), QPointF(600, 400)); app.session().setLayerBlendMode(LayerBlendMode::softLight); const QList points{QPointF(600, 400), QPointF(300, 100), QPointF(900, 700)}; for (const QPointF point : points) QTRY_VERIFY2(near(onCanvas(app, point), app.shown(int(point.x()), int(point.y()))), qPrintable(onCanvas(app, point).name())); add(app, filled(1200, 800, QColor(0, 0, 160, 160)), QStringLiteral("Clipped"), QPointF(600, 400)); app.session().toggleClippingMask(app.session().activeLayerID().value()); app.session().selectLayer(grey); app.session().setLayerBlendMode(LayerBlendMode::linearDodge); for (const QPointF point : points) QTRY_VERIFY2(near(onCanvas(app, point), app.shown(int(point.x()), int(point.y()))), qPrintable(onCanvas(app, point).name())); } QTEST_MAIN(AcceptanceEditTests) #include "AcceptanceEditTests.moc"