254 lines
11 KiB
C++
254 lines
11 KiB
C++
#include "Document/EditorSession.h"
|
|
#include "IO/CanvasResizer.h"
|
|
#include "IO/ImageExporter.h"
|
|
#include "IO/ImageResizer.h"
|
|
#include <QTemporaryDir>
|
|
#include <QtTest>
|
|
|
|
// The session cases of Swift's GroupTests.
|
|
namespace {
|
|
std::vector<QUuid> rowIDs(const EditorSession &session)
|
|
{
|
|
std::vector<QUuid> result;
|
|
for (const LayerHierarchy::Entry &row : session.layerRows())
|
|
result.push_back(row.layer.id);
|
|
return result;
|
|
}
|
|
|
|
std::vector<QUuid> renderIDs(const EditorSession &session)
|
|
{
|
|
std::vector<QUuid> result;
|
|
for (const ImageLayer &layer : session.document().value().renderLayers())
|
|
result.push_back(layer.id);
|
|
return result;
|
|
}
|
|
|
|
std::optional<QUuid> parentOf(const EditorSession &session, QUuid id)
|
|
{
|
|
for (const ImageLayer &layer : session.document().value().layers) {
|
|
if (layer.id == id)
|
|
return layer.parentID;
|
|
}
|
|
throw std::runtime_error("no such layer");
|
|
}
|
|
}
|
|
|
|
class GroupSessionTests : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void nestedGroupsMoveOutCollapseAndDeleteUndo();
|
|
void layersArePlacedAboveBelowAndInsideFolders();
|
|
void placingRespectsTheNestingLimit();
|
|
void hiddenParentOverridesChildrenAndExportOrderFollowsGroups();
|
|
void groupsRoundTripAndSurviveImageAndCanvasResize();
|
|
void rowsOfASessionWithoutADocument();
|
|
};
|
|
|
|
void GroupSessionTests::nestedGroupsMoveOutCollapseAndDeleteUndo()
|
|
{
|
|
EditorSession session;
|
|
session.createDocument(100, 100);
|
|
session.addGroup();
|
|
const QUuid outer = session.activeLayerID().value();
|
|
session.addGroup();
|
|
const QUuid inner = session.activeLayerID().value();
|
|
session.addBlankLayer();
|
|
const QUuid child = session.activeLayerID().value();
|
|
QCOMPARE(session.activeLayer().value().parentID, std::optional(inner));
|
|
QVERIFY(!session.placeLayer(outer, inner));
|
|
QVERIFY(!session.placeLayer(inner, inner));
|
|
// The panel asks before a drop; validation never sees these.
|
|
QVERIFY(!session.canPlaceLayer(outer, inner));
|
|
QVERIFY(!session.canPlaceLayer(inner, inner));
|
|
QVERIFY(!session.canPlaceLayer(outer, child));
|
|
QVERIFY(!session.canPlaceLayer(outer, QUuid::createUuid()));
|
|
QVERIFY(session.canPlaceLayer(child, outer));
|
|
session.toggleGroupExpansion(outer);
|
|
QCOMPARE(rowIDs(session), std::vector<QUuid>{outer});
|
|
QCOMPARE(session.activeLayerID(), std::optional(outer));
|
|
session.toggleGroupExpansion(outer);
|
|
std::vector<int> depths;
|
|
for (const LayerHierarchy::Entry &row : session.layerRows())
|
|
depths.push_back(row.depth);
|
|
QCOMPARE(depths, (std::vector<int>{0, 1, 2}));
|
|
session.selectLayer(child);
|
|
session.moveActiveLayerOutOfGroup();
|
|
QCOMPARE(session.activeLayer().value().parentID, std::optional(outer));
|
|
QCOMPARE(session.history.undoName(), QString("Move Layer"));
|
|
session.undo();
|
|
QCOMPARE(parentOf(session, child), std::optional(inner));
|
|
session.selectLayer(outer);
|
|
session.deleteActiveLayer();
|
|
QVERIFY(session.document().value().layers.empty());
|
|
session.undo();
|
|
QCOMPARE(int(session.document().value().layers.size()), 3);
|
|
QCOMPARE(parentOf(session, child), std::optional(inner));
|
|
}
|
|
|
|
void GroupSessionTests::layersArePlacedAboveBelowAndInsideFolders()
|
|
{
|
|
EditorSession session;
|
|
session.createDocument(100, 100);
|
|
session.addBlankLayer();
|
|
const QUuid bottom = session.activeLayerID().value();
|
|
session.addBlankLayer();
|
|
const QUuid middle = session.activeLayerID().value();
|
|
session.addGroup();
|
|
const QUuid folder = session.activeLayerID().value();
|
|
session.addBlankLayer();
|
|
const QUuid inside = session.activeLayerID().value();
|
|
// To the top of the root, then to its bottom.
|
|
QVERIFY(session.placeLayer(bottom, std::nullopt));
|
|
QCOMPARE(session.document().value().layers.back().id, bottom);
|
|
QCOMPARE(session.activeLayerID(), std::optional(bottom));
|
|
QVERIFY(session.placeLayer(bottom, std::nullopt, std::nullopt, true));
|
|
QCOMPARE(session.document().value().layers.front().id, bottom);
|
|
// Just above a sibling; the target shares the new parent.
|
|
QVERIFY(session.placeLayer(bottom, std::nullopt, middle));
|
|
QCOMPARE(rowIDs(session), (std::vector<QUuid>{folder, inside, bottom, middle}));
|
|
QVERIFY(!session.placeLayer(bottom, std::nullopt, inside));
|
|
// A target beats the bottom.
|
|
QVERIFY(session.placeLayer(bottom, std::nullopt, middle, true));
|
|
QCOMPARE(rowIDs(session), (std::vector<QUuid>{folder, inside, bottom, middle}));
|
|
QVERIFY(!session.placeLayer(bottom, std::nullopt, bottom));
|
|
QVERIFY(!session.placeLayer(bottom, std::nullopt, QUuid::createUuid()));
|
|
// Into a folded folder, which opens, above a layer inside.
|
|
session.selectLayer(middle);
|
|
session.toggleGroupExpansion(folder);
|
|
QVERIFY(session.placeLayer(bottom, folder, inside));
|
|
QVERIFY(!session.collapsedGroupIDs().contains(folder));
|
|
QCOMPARE(parentOf(session, bottom), std::optional(folder));
|
|
QCOMPARE(rowIDs(session), (std::vector<QUuid>{folder, bottom, inside, middle}));
|
|
// Only folders take layers; strangers and busy sessions move none.
|
|
QVERIFY(!session.placeLayer(bottom, middle));
|
|
QVERIFY(!session.canPlaceLayer(bottom, middle));
|
|
QVERIFY(!session.placeLayer(QUuid::createUuid(), std::nullopt));
|
|
QVERIFY(!session.placeLayer(bottom, QUuid::createUuid()));
|
|
QVERIFY(session.canPlaceLayer(bottom, std::nullopt) && session.canPlaceLayer(bottom, folder));
|
|
session.setIsImporting(true);
|
|
QVERIFY(!session.canPlaceLayer(bottom, std::nullopt));
|
|
QVERIFY(!session.placeLayer(bottom, std::nullopt));
|
|
// Outside a folder there is no group to leave.
|
|
session.setIsImporting(false);
|
|
session.selectLayer(middle);
|
|
const int count = session.history.undoCount();
|
|
session.moveActiveLayerOutOfGroup();
|
|
QCOMPARE(session.history.undoCount(), count);
|
|
// A layer leaves its folder to sit just above it.
|
|
session.selectLayer(std::nullopt);
|
|
session.addBlankLayer();
|
|
const QUuid top = session.activeLayerID().value();
|
|
session.selectLayer(inside);
|
|
session.moveActiveLayerOutOfGroup();
|
|
QCOMPARE(rowIDs(session), (std::vector<QUuid>{top, inside, folder, bottom, middle}));
|
|
}
|
|
|
|
void GroupSessionTests::placingRespectsTheNestingLimit()
|
|
{
|
|
EditorSession session;
|
|
session.createDocument(8, 8);
|
|
// Each new folder goes inside the active one: 64 deep.
|
|
for (int depth = 0; depth < 64; ++depth)
|
|
session.addGroup();
|
|
const QUuid deepest = session.activeLayerID().value();
|
|
QCOMPARE(int(session.document().value().layers.size()), 64);
|
|
session.selectLayer(std::nullopt);
|
|
session.addGroup();
|
|
const QUuid folder = session.activeLayerID().value();
|
|
session.selectLayer(std::nullopt);
|
|
session.addBlankLayer();
|
|
const QUuid leaf = session.activeLayerID().value();
|
|
const std::optional<CanvasDocument> before = session.document();
|
|
// A folder may go no deeper; a layer still may.
|
|
QVERIFY(session.canPlaceLayer(folder, deepest));
|
|
QVERIFY(!session.placeLayer(folder, deepest));
|
|
QCOMPARE(session.document(), before);
|
|
QVERIFY(session.placeLayer(leaf, deepest));
|
|
QCOMPARE(parentOf(session, leaf), std::optional(deepest));
|
|
}
|
|
|
|
void GroupSessionTests::hiddenParentOverridesChildrenAndExportOrderFollowsGroups()
|
|
{
|
|
EditorSession session;
|
|
session.createDocument(64, 32);
|
|
session.addGroup();
|
|
const QUuid group = session.activeLayerID().value();
|
|
QTemporaryDir folder;
|
|
QImage fixture(64, 32, QImage::Format_RGBA8888);
|
|
fixture.fill(Qt::red);
|
|
QVERIFY(fixture.save(folder.filePath("fixture.png"), "PNG"));
|
|
bool done = false;
|
|
session.importImages({QUrl::fromLocalFile(folder.filePath("fixture.png"))}, std::nullopt, [&] { done = true; });
|
|
QVERIFY(QTest::qWaitFor([&] { return done; }, 10'000));
|
|
const QUuid child = session.activeLayerID().value();
|
|
QCOMPARE(session.activeLayer().value().parentID, std::optional(group));
|
|
const ImageIdentity source = session.activeLayer().value().asset.value().identity();
|
|
session.toggleLayerVisibility(group);
|
|
QVERIFY(session.activeLayer().value().isVisible);
|
|
QVERIFY(session.document().value().renderLayers().empty());
|
|
QVERIFY(!session.canTransform());
|
|
// A hidden folder exports nothing of what it holds.
|
|
const QImage hiddenExport = QImage::fromData(ImageExporter::pngData(session.projectSnapshot().value()), "PNG");
|
|
QCOMPARE(hiddenExport.size(), QSize(64, 32));
|
|
QCOMPARE(hiddenExport.pixelColor(0, 0).alpha(), 0);
|
|
session.toggleLayerVisibility(group);
|
|
QCOMPARE(renderIDs(session), std::vector<QUuid>{child});
|
|
QCOMPARE(QImage::fromData(ImageExporter::pngData(session.projectSnapshot().value()), "PNG").pixelColor(0, 0), QColor(255, 0, 0));
|
|
QVERIFY(session.activeLayer().value().asset.value().identity() == source);
|
|
session.selectLayer(std::nullopt);
|
|
session.addGroup();
|
|
const QUuid other = session.activeLayerID().value();
|
|
session.addBlankLayer();
|
|
const QUuid otherChild = session.activeLayerID().value();
|
|
QCOMPARE(renderIDs(session), (std::vector<QUuid>{child, otherChild}));
|
|
session.selectLayer(other);
|
|
session.moveActiveLayer(-1);
|
|
QCOMPARE(renderIDs(session), (std::vector<QUuid>{otherChild, child}));
|
|
QCOMPARE(parentOf(session, otherChild), std::optional(other));
|
|
}
|
|
|
|
void GroupSessionTests::groupsRoundTripAndSurviveImageAndCanvasResize()
|
|
{
|
|
EditorSession session;
|
|
session.createDocument(20, 10);
|
|
session.addGroup();
|
|
const QUuid group = session.activeLayerID().value();
|
|
session.renameLayer(group, "Artwork");
|
|
session.addBlankLayer();
|
|
const QUuid child = session.activeLayerID().value();
|
|
const ProjectSnapshot snapshot = session.projectSnapshot().value();
|
|
QCOMPARE(snapshot.manifest.version, qint64(9));
|
|
const auto record = [](const ProjectSnapshot &from, QUuid id) {
|
|
for (const ProjectLayerRecord &layer : from.manifest.layers) {
|
|
if (layer.id == id)
|
|
return layer;
|
|
}
|
|
throw std::runtime_error("no such record");
|
|
};
|
|
QTemporaryDir folder;
|
|
const QString path = folder.filePath("Groups.comp");
|
|
ProjectStore::save(snapshot, path);
|
|
const ProjectSnapshot loaded = ProjectStore::load(path);
|
|
QCOMPARE(record(loaded, group).isGroup, std::optional(true));
|
|
QCOMPARE(record(loaded, group).name, QString("Artwork"));
|
|
QCOMPARE(record(loaded, child).parentID, std::optional(group));
|
|
const ProjectSnapshot resized = ImageResizer::resize(loaded, {.width = 40, .height = 20, .resolution = 72});
|
|
const ProjectSnapshot cropped = CanvasResizer::resize(resized, {.width = 30, .height = 15});
|
|
QCOMPARE(record(cropped, child).parentID, std::optional(group));
|
|
QCOMPARE(record(cropped, group).isGroup, std::optional(true));
|
|
// A version 1 project keeps its version through the store.
|
|
ProjectSnapshot legacy{.manifest = {.documentID = QUuid::createUuid(), .width = 20, .height = 10, .activeLayerID = std::nullopt, .layers = {}},
|
|
.images = {}};
|
|
legacy.manifest.version = 1;
|
|
ProjectStore::save(legacy, path);
|
|
QCOMPARE(ProjectStore::load(path).manifest.version, qint64(1));
|
|
}
|
|
|
|
void GroupSessionTests::rowsOfASessionWithoutADocument()
|
|
{
|
|
QVERIFY(EditorSession().layerRows().empty());
|
|
}
|
|
|
|
QTEST_GUILESS_MAIN(GroupSessionTests)
|
|
#include "GroupSessionTests.moc"
|