#include "ContentView.h" #include "MenuFixtures.h" #include "UI/LayerIcons.h" #include "UI/LayersPanel.h" #include "UI/NativeLayerList.h" #include #include #include #include #include // Adjustment layers in the window: footer, rows, menu, sheets. namespace { template Widget &find(QWidget &root, const char *name) { Widget *found = root.findChild(QString::fromLatin1(name)); if (!found) throw std::runtime_error(std::string("no widget named ") + name); return *found; } ImportedImage whiteImage(int width, int height) { QImage pixels(width, height, QImage::Format_RGBA8888_Premultiplied); pixels.fill(Qt::white); return ImportedImage(pixels, pixels, QStringLiteral("White")); } // A shown label with these words, if any. QLabel *shownLabel(QWidget &root, const QString &words) { for (QLabel *label : root.findChildren()) { if (label->text() == words && label->isVisible()) return label; } return nullptr; } bool inked(const QImage &image, int x, int y) { return qAlpha(image.pixel(x, y)) > 128; } } class AdjustmentPanelTests : public QObject { Q_OBJECT private slots: void theFooterAddsEachKindAndItsEditorOpens(); void rowsShowTheirGlyphAndOpenOnADoubleClick(); void theLayerMenuAddsAndEditsAdjustments(); void theSheetsSayTheyReadWhatLiesBeneath(); }; void AdjustmentPanelTests::theFooterAddsEachKindAndItsEditorOpens() { EditorSession session; LayersPanel panel(session); panel.show(); QVERIFY(QTest::qWaitForWindowExposed(&panel)); auto &button = find(panel, "addAdjustment"); QCOMPARE(button.toolTip(), QString("New adjustment layer")); QCOMPARE(button.accessibleName(), QString("New adjustment layer")); QCOMPARE(button.popupMode(), QToolButton::InstantPopup); QVERIFY(!button.isEnabled()); // Swift's circle.lefthalf.filled, in the footer's ink. const QImage icon = LayerIcons::pixmap(LayerIcon::halfFilledCircle, 16, panel.palette().color(QPalette::PlaceholderText), panel.devicePixelRatio()).toImage(); QCOMPARE(button.icon().pixmap(QSize(16, 16), panel.devicePixelRatio()).toImage().convertToFormat(icon.format()), icon); // Swift's Menu: the six kinds by name, no ellipsis. const QList entries = button.menu()->actions(); QCOMPARE(entries.size(), qsizetype(allAdjustmentKinds.size())); for (size_t index = 0; index < allAdjustmentKinds.size(); ++index) QCOMPARE(entries[qsizetype(index)]->text(), rawValue(allAdjustmentKinds[index])); session.createDocument(4, 4); session.insert(whiteImage(4, 4)); QVERIFY(button.isEnabled()); entries[1]->trigger(); QCOMPARE(session.activeLayer().value().adjustment.value().kind, AdjustmentKind::levels); // The panel's task opens the editor; the footer rests meanwhile. QTRY_VERIFY(session.levels().has_value()); QVERIFY(session.adjustmentOriginal() && !button.isEnabled()); session.cancelLevels(); QVERIFY(button.isEnabled() && !session.adjustmentEditingID()); // A request made again begins again. session.setAdjustmentEditingID(session.activeLayerID()); QTRY_VERIFY(session.levels().has_value()); session.commitLevels(); QTRY_VERIFY(!session.adjustmentEditingID()); } void AdjustmentPanelTests::rowsShowTheirGlyphAndOpenOnADoubleClick() { EditorSession session; session.createDocument(40, 30); session.insert(whiteImage(40, 30)); const QUuid pixels = session.activeLayerID().value(); session.addMask(); std::vector ids; for (const AdjustmentKind kind : allAdjustmentKinds) { session.addAdjustment(kind); session.setAdjustmentEditingID(std::nullopt); ids.push_back(session.activeLayerID().value()); } session.addMask(); NativeLayerList list(session); list.resize(252, 500); list.show(); QVERIFY(QTest::qWaitForWindowActive(&list)); // Top first: the last kind added heads the list. QCOMPARE(list.cells().size(), size_t(7)); for (size_t index = 0; index < allAdjustmentKinds.size(); ++index) { LayerCell &row = *list.cells().at(allAdjustmentKinds.size() - 1 - index); const QColor ink = row.palette().color(QPalette::WindowText); QCOMPARE(row.findChild("layerDimensions")->text(), QString("Adjustment · Double-click to edit")); QCOMPARE(row.thumbnail().size(), QSize(36, 36)); const QImage expected = LayerIcons::adjustmentThumbnail(allAdjustmentKinds[index], ink, 1).toImage(); QCOMPARE(row.thumbnail().icon().pixmap(QSize(36, 36), 1).toImage().convertToFormat(expected.format()), expected); } // Each kind its own glyph, as Swift's symbols. const std::pair symbols[] = {{AdjustmentKind::hsv, LayerIcon::halfFilledCircle}, {AdjustmentKind::levels, LayerIcon::sliders}, {AdjustmentKind::curves, LayerIcon::curvePath}, {AdjustmentKind::exposure, LayerIcon::plusMinusCircle}, {AdjustmentKind::gradientMap, LayerIcon::paintPalette}, {AdjustmentKind::grain, LayerIcon::circleGrid}}; QList glyphs; for (const auto &[kind, symbol] : symbols) { QVERIFY(LayerIcons::symbol(kind) == symbol); glyphs << LayerIcons::adjustmentThumbnail(kind, Qt::black, 1).toImage(); } for (qsizetype first = 0; first < glyphs.size(); ++first) { for (qsizetype second = first + 1; second < glyphs.size(); ++second) QVERIFY(glyphs[first] != glyphs[second]); } // The glyph is 1.2 times the rail's, centred: 21.6 points. const QImage circle = glyphs.front(); QVERIFY(inked(circle, 9, 18) && inked(circle, 26, 18) && !inked(circle, 5, 18) && !inked(circle, 30, 18)); // The curve turns a quarter clockwise: its dot moves right. const QImage curve = LayerIcons::adjustmentThumbnail(AdjustmentKind::curves, Qt::black, 1).toImage(); const QImage upright = LayerIcons::pixmap(LayerIcon::curvePath, 36, Qt::black, 1).toImage(); QVERIFY(inked(curve, 24, 11) && !inked(curve, 11, 11)); QVERIFY(inked(upright, 7, 7) && !inked(upright, 29, 7)); // The masks: an adjustment's shows no link, the pixels' does. QVERIFY(list.cells().front()->findChild("maskLink")->isHidden()); QVERIFY(!list.cells().back()->findChild("maskLink")->isHidden()); // A double click on the glyph asks for its editor. session.selectLayer(pixels); LayerCell &levels = *list.cells().at(allAdjustmentKinds.size() - 2); QTest::mouseDClick(&levels.thumbnail(), Qt::LeftButton); QCOMPARE(session.activeLayerID(), std::optional(ids[1])); QCOMPARE(session.adjustmentEditingID(), std::optional(ids[1])); QVERIFY(!session.renamingLayerID()); // While one is asked for, rows refuse the gesture. QTest::mouseDClick(&levels, Qt::LeftButton, Qt::NoModifier, QPoint(levels.width() - 20, 20)); QVERIFY(!session.renamingLayerID()); // On the name it renames, as every layer. session.setAdjustmentEditingID(std::nullopt); QTest::mouseDClick(&levels, Qt::LeftButton, Qt::NoModifier, QPoint(levels.width() - 20, 20)); QCOMPARE(session.renamingLayerID(), std::optional(ids[1])); QVERIFY(!session.adjustmentEditingID()); QTest::keyClick(levels.findChild("layerNameEditor"), Qt::Key_Escape); QVERIFY(!session.renamingLayerID()); } void AdjustmentPanelTests::theLayerMenuAddsAndEditsAdjustments() { Bar bar; QAction &adjustments = bar.action("newAdjustmentLayer"), &edit = bar.action("editAdjustment"); QCOMPARE(adjustments.text(), QString("New Adjustment Layer")); QCOMPARE(edit.text(), QString("Edit Adjustment…")); QVERIFY(!adjustments.isEnabled() && !edit.isEnabled()); const std::pair entries[] = {{"newHueSaturationAdjustment", "Hue/Saturation…"}, {"newLevelsAdjustment", "Levels…"}, {"newCurvesAdjustment", "Curves…"}, {"newExposureAdjustment", "Exposure…"}, {"newGradientMapAdjustment", "Gradient Map…"}, {"newGrainAdjustment", "Grain…"}}; const QList listed = adjustments.menu()->actions(); QCOMPARE(listed.size(), qsizetype(std::size(entries))); for (qsizetype index = 0; index < listed.size(); ++index) { QCOMPARE(listed[index]->objectName(), QString(entries[index].first)); QCOMPARE(listed[index]->text(), QString(entries[index].second)); } bar.session().createDocument(4, 4); bar.session().insert(whiteImage(4, 4)); const QUuid pixels = bar.session().activeLayerID().value(); QVERIFY(adjustments.isEnabled() && !edit.isEnabled()); bar.action("newCurvesAdjustment").trigger(); const QUuid curves = bar.session().activeLayerID().value(); // The window's panel opens the filter sheet on it. QTRY_VERIFY(bar.session().filterEdit().has_value()); QVERIFY(bar.session().filterEdit().value().kind == FilterKind::curves); QVERIFY(!adjustments.isEnabled() && !edit.isEnabled()); bar.session().cancelFilter(); QVERIFY(adjustments.isEnabled() && edit.isEnabled()); edit.trigger(); QCOMPARE(bar.session().adjustmentEditingID(), std::optional(curves)); QTRY_VERIFY(bar.session().filterEdit().has_value()); bar.session().cancelFilter(); bar.session().selectLayer(pixels); QVERIFY(adjustments.isEnabled() && !edit.isEnabled()); } void AdjustmentPanelTests::theSheetsSayTheyReadWhatLiesBeneath() { EditorSession session; ContentView view(session); view.resize(900, 600); view.show(); QVERIFY(QTest::qWaitForWindowExposed(&view)); session.createDocument(20, 20); session.insert(whiteImage(20, 20)); session.selectAll(); for (const AdjustmentKind kind : {AdjustmentKind::levels, AdjustmentKind::hsv, AdjustmentKind::grain}) { session.addAdjustment(kind); QTRY_VERIFY(session.adjustmentOriginal().has_value()); QTest::qWait(50); const auto shown = [&](const char *words) { for (QWidget *top : QApplication::topLevelWidgets()) { if (top->isVisible() && shownLabel(*top, QString::fromUtf8(words))) return true; } return false; }; // No selection limits a layer's settings. QVERIFY(!shown("Limited to the selection")); if (kind == AdjustmentKind::levels) QVERIFY(shown("Underlying pixels · alpha-weighted histogram")); session.finishAdjustmentEditing(false); } } QTEST_MAIN(AdjustmentPanelTests) #include "AdjustmentPanelTests.moc"