Files
OmaPhoto/tests/AdjustmentPanelTests.cpp
T
雷电芽衣andClaude c864304695
Release / deb (push) Canceled after 0s
Release / rpm (push) Canceled after 0s
Release / arch (push) Canceled after 0s
Release / publish (push) Canceled after 0s
OmaPhoto 1.0.0: the Linux port of Compositor
A layered image editor for Linux and Omarchy, ported from Compositor
(github.com/robbietilton/Compositor), whose Swift source is its
specification. It reads and writes Compositor's .comp projects.

Packages for Ubuntu, Fedora and Arch come from the release workflow;
scripts/install/ holds one install script per distribution family,
and flake.nix serves NixOS.

Co-Authored-By: Claude <noreply@anthropic.com>
2026-09-26 00:44:52 -04:00

228 lines
11 KiB
C++

#include "ContentView.h"
#include "MenuFixtures.h"
#include "UI/LayerIcons.h"
#include "UI/LayersPanel.h"
#include "UI/NativeLayerList.h"
#include <QLabel>
#include <QLineEdit>
#include <QMenu>
#include <QToolButton>
#include <QtTest>
// Adjustment layers in the window: footer, rows, menu, sheets.
namespace {
template <typename Widget> Widget &find(QWidget &root, const char *name)
{
Widget *found = root.findChild<Widget *>(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<QLabel *>()) {
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<QToolButton>(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<QAction *> 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<QUuid> 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<QLabel *>("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<AdjustmentKind, LayerIcon> 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<QImage> 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<QToolButton *>("maskLink")->isHidden());
QVERIFY(!list.cells().back()->findChild<QToolButton *>("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<QLineEdit *>("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<const char *, const char *> entries[] = {{"newHueSaturationAdjustment", "Hue/Saturation…"}, {"newLevelsAdjustment", "Levels…"},
{"newCurvesAdjustment", "Curves…"}, {"newExposureAdjustment", "Exposure…"},
{"newGradientMapAdjustment", "Gradient Map…"}, {"newGrainAdjustment", "Grain…"}};
const QList<QAction *> 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"