365 lines
15 KiB
C++
365 lines
15 KiB
C++
#include "Document/ProjectWorkspace.h"
|
|
#include "MenuFixtures.h"
|
|
#include "SelectionFixtures.h"
|
|
#include "SessionFixtures.h"
|
|
#include <QClipboard>
|
|
#include <QMimeData>
|
|
#include <QtTest>
|
|
|
|
// Swift 1.2.5: whole layers copied, pasted and duplicated.
|
|
namespace {
|
|
ImportedImage square(const QString &name, int side = 2)
|
|
{
|
|
QImage image(side, side, QImage::Format_RGBA8888_Premultiplied);
|
|
image.fill(Qt::red);
|
|
return ImportedImage(image, QImage(), name);
|
|
}
|
|
|
|
// Three red layers, A under B under C.
|
|
QUuid fill(EditorSession &session, std::vector<QUuid> &ids)
|
|
{
|
|
session.createDocument(20, 20);
|
|
for (const char *name : {"A", "B", "C"}) {
|
|
session.insert(square(QString::fromLatin1(name)), QPointF(4 + 6 * ids.size(), 5));
|
|
ids.push_back(session.activeLayerID().value());
|
|
}
|
|
return ids.front();
|
|
}
|
|
|
|
// A clipboard history's replay: every format, written anew.
|
|
QMimeData *saved()
|
|
{
|
|
auto *copy = new QMimeData;
|
|
const QMimeData *data = QGuiApplication::clipboard()->mimeData();
|
|
for (const QString &format : data->formats())
|
|
copy->setData(format, data->data(format));
|
|
// Offscreen reads no image from PNG bytes; a desktop does.
|
|
copy->setImageData(data->imageData());
|
|
return copy;
|
|
}
|
|
|
|
// Bottom to top, as the document lists them.
|
|
QString names(const EditorSession &session)
|
|
{
|
|
QStringList result;
|
|
for (const ImageLayer &layer : session.document().value().layers)
|
|
result << layer.name;
|
|
return result.join(QLatin1Char(','));
|
|
}
|
|
}
|
|
|
|
class LayerClipboardTests : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void copyWithoutASelectionTakesTheLayersWhole();
|
|
void pasteDuplicatesInTheSameProject();
|
|
void anotherCopyOrAGoneLayerEndsTheLayerPaste();
|
|
void pasteBringsTheLayersIntoAnotherProject();
|
|
void severalDuplicatesStackAboveTheTopmost();
|
|
void foldersLeaveTheAnchorToThePixels();
|
|
void theEditMenuCopiesAndPastesAFolder();
|
|
void clipboardNoticesKeepTheCopy();
|
|
void aRestoredOldCopyStaysExpired();
|
|
void theLatestCopyAnywhereWins();
|
|
void aLayerTheSourceTabLacksIsNotSelected();
|
|
};
|
|
|
|
void LayerClipboardTests::copyWithoutASelectionTakesTheLayersWhole()
|
|
{
|
|
EditorSession session;
|
|
std::vector<QUuid> ids;
|
|
fill(session, ids);
|
|
session.selectLayers({}, std::nullopt);
|
|
QVERIFY(!session.canCopyLayer());
|
|
session.selectLayers({ids[2], ids[0]}, ids[2]);
|
|
QVERIFY(session.canCopyLayer());
|
|
session.copySelection();
|
|
// Pixels too: a layer's copy also serves other apps.
|
|
QCOMPARE(session.copiedLayer().value().ids, (std::vector<QUuid>{ids[0], ids[2]}));
|
|
QVERIFY(session.pixelClipboard() && QGuiApplication::clipboard()->mimeData()->hasImage());
|
|
// A folder has no pixels: its id alone goes out.
|
|
session.selectLayers({ids[1]}, ids[1]);
|
|
session.groupSelectedLayers();
|
|
const QUuid folder = session.activeLayerID().value();
|
|
session.selectLayers({folder, ids[1]}, folder);
|
|
session.copySelection();
|
|
QCOMPARE(session.copiedLayer().value().ids, std::vector<QUuid>{folder});
|
|
QVERIFY(!session.pixelClipboard());
|
|
QCOMPARE(QGuiApplication::clipboard()->mimeData()->data("com.compositor.copied-layer"), uuidString(folder).toUtf8());
|
|
// A selection or a chosen mask copies pixels, not layers.
|
|
session.selectLayer(ids[0]);
|
|
session.selectAll();
|
|
QVERIFY(!session.canCopyLayer() && session.canCopyPixels());
|
|
session.copySelection();
|
|
QVERIFY(!session.copiedLayer() && session.pixelClipboard());
|
|
session.deselect();
|
|
session.addMask(true);
|
|
QVERIFY(session.isMaskSelected() && !session.canCopyLayer());
|
|
session.selectLayerTarget(ids[0], false);
|
|
QVERIFY(session.canCopyLayer());
|
|
session.setIsProjectBusy(true);
|
|
QVERIFY(!session.canCopyLayer());
|
|
}
|
|
|
|
void LayerClipboardTests::pasteDuplicatesInTheSameProject()
|
|
{
|
|
ProjectWorkspace workspace;
|
|
EditorSession &session = workspace.current().session;
|
|
std::vector<QUuid> ids;
|
|
fill(session, ids);
|
|
session.selectLayers({ids[0], ids[1]}, ids[0]);
|
|
session.copySelection();
|
|
const int steps = session.history.undoCount();
|
|
QVERIFY(workspace.pasteCopiedLayer());
|
|
QCOMPARE(session.history.undoCount(), steps + 1);
|
|
QCOMPARE(session.history.undoName(), QString("Paste"));
|
|
// Stacked above the topmost original, in their order.
|
|
QCOMPARE(names(session), QString("A,B,A copy,B copy,C"));
|
|
const std::vector<ImageLayer> layers = session.document().value().layers;
|
|
QCOMPARE(session.selectedLayerIDs(), (QSet<QUuid>{layers[2].id, layers[3].id}));
|
|
QCOMPARE(session.activeLayerID().value(), layers[2].id);
|
|
// With C active, the first copied layer's copy leads.
|
|
session.undo();
|
|
session.selectLayer(ids[2]);
|
|
QVERIFY(workspace.pasteCopiedLayer());
|
|
QCOMPARE(session.activeLayer().value().name, QString("A copy"));
|
|
session.undo();
|
|
QVERIFY(workspace.pasteCopiedLayer());
|
|
// A busy project refuses; the paste goes on to pixels.
|
|
session.setIsProjectBusy(true);
|
|
QVERIFY(!workspace.pasteCopiedLayer());
|
|
session.setIsProjectBusy(false);
|
|
QVERIFY(workspace.pasteCopiedLayer());
|
|
QCOMPARE(session.document().value().layers.size(), size_t(7));
|
|
}
|
|
|
|
void LayerClipboardTests::anotherCopyOrAGoneLayerEndsTheLayerPaste()
|
|
{
|
|
ProjectWorkspace workspace;
|
|
EditorSession &session = workspace.current().session;
|
|
std::vector<QUuid> ids;
|
|
fill(session, ids);
|
|
session.selectLayers({ids[0], ids[1]}, ids[0]);
|
|
session.copySelection();
|
|
// A layer gone since stays out; all gone, nothing pastes.
|
|
session.selectLayer(ids[0]);
|
|
session.deleteLayerOrMask();
|
|
QVERIFY(workspace.pasteCopiedLayer());
|
|
QCOMPARE(names(session), QString("B,B copy,C"));
|
|
session.selectLayer(ids[1]);
|
|
session.deleteLayerOrMask();
|
|
QVERIFY(!workspace.pasteCopiedLayer());
|
|
// Another app's copy ends it too.
|
|
session.selectLayer(ids[2]);
|
|
session.copySelection();
|
|
QVERIFY(session.copiedLayer());
|
|
QGuiApplication::clipboard()->setText(QStringLiteral("elsewhere"));
|
|
QVERIFY(!workspace.pasteCopiedLayer());
|
|
QCOMPARE(session.document().value().layers.size(), size_t(2));
|
|
}
|
|
|
|
void LayerClipboardTests::pasteBringsTheLayersIntoAnotherProject()
|
|
{
|
|
ProjectWorkspace workspace;
|
|
EditorSession &from = workspace.current().session;
|
|
std::vector<QUuid> ids;
|
|
fill(from, ids);
|
|
from.selectLayer(ids[2]);
|
|
LayerEffects effects;
|
|
effects.stroke = StrokeEffect();
|
|
from.setEffects(effects);
|
|
from.selectLayers({ids[0], ids[2]}, ids[2]);
|
|
from.copySelection();
|
|
ProjectTab &target = workspace.addTab();
|
|
target.session.createDocument(40, 30);
|
|
QCOMPARE(workspace.selectedID(), target.id);
|
|
QVERIFY(workspace.pasteCopiedLayer());
|
|
QTRY_COMPARE(target.session.document().value().layers.size(), size_t(2));
|
|
QCOMPARE(target.session.history.undoName(), QString("Copy Layers from Project"));
|
|
const std::vector<ImageLayer> layers = target.session.document().value().layers;
|
|
QCOMPARE(names(target.session), QString("A,C"));
|
|
// A and C keep their gap, centred on the canvas.
|
|
QCOMPARE(layers[0].transform.origin, QPointF(13, 14));
|
|
QCOMPARE(layers[1].transform.origin, QPointF(25, 14));
|
|
QVERIFY(layers[1].effects == effects);
|
|
QCOMPARE(target.session.activeLayerID().value(), layers[0].id);
|
|
QCOMPARE(target.session.selectedLayerIDs(), (QSet<QUuid>{layers[0].id, layers[1].id}));
|
|
QCOMPARE(from.document().value().layers.size(), size_t(3));
|
|
}
|
|
|
|
void LayerClipboardTests::foldersLeaveTheAnchorToThePixels()
|
|
{
|
|
ProjectWorkspace workspace;
|
|
EditorSession &from = workspace.current().session;
|
|
from.createDocument(20, 20);
|
|
from.insert(square("A"), QPointF(4, 5));
|
|
const QUuid a = from.activeLayerID().value();
|
|
from.addGroup();
|
|
const QUuid first = from.activeLayerID().value();
|
|
from.addGroup();
|
|
const QUuid second = from.activeLayerID().value();
|
|
ProjectTab &target = workspace.addTab();
|
|
target.session.createDocument(40, 30);
|
|
const auto copy = [&](const std::vector<QUuid> &ids) {
|
|
bool done = false;
|
|
workspace.copyLayers(ids, target.id, std::nullopt, [&] { done = true; });
|
|
return QTest::qWaitFor([&] { return done; }, 10'000);
|
|
};
|
|
// A folder spans the canvas; only A's box counts.
|
|
QVERIFY(copy({a, first}));
|
|
QCOMPARE(target.session.document().value().layers.front().transform.origin, QPointF(19, 14));
|
|
// Folders alone: the first one's centre.
|
|
target.session.undo();
|
|
QVERIFY(copy({first, second}));
|
|
QCOMPARE(target.session.document().value().layers.front().transform.origin, QPointF(10, 5));
|
|
}
|
|
|
|
void LayerClipboardTests::severalDuplicatesStackAboveTheTopmost()
|
|
{
|
|
EditorSession session;
|
|
std::vector<QUuid> ids;
|
|
fill(session, ids);
|
|
// One copy sits just above its original.
|
|
session.selectLayer(ids[0]);
|
|
session.duplicateActiveLayer();
|
|
QCOMPARE(names(session), QString("A,A copy,B,C"));
|
|
session.undo();
|
|
// C goes into a folder; A's copy joins C's there.
|
|
session.selectLayer(ids[2]);
|
|
session.groupSelectedLayers();
|
|
const QUuid folder = session.activeLayerID().value();
|
|
// C is active, not first: its copy leads.
|
|
session.selectLayers({ids[0], ids[2]}, ids[2]);
|
|
const int steps = session.history.undoCount();
|
|
session.duplicateActiveLayer();
|
|
QCOMPARE(session.history.undoCount(), steps + 1);
|
|
QCOMPARE(session.history.undoName(), QString("Duplicate Layer"));
|
|
QCOMPARE(names(session), QString("A,B,Folder 1,C,A copy,C copy"));
|
|
const std::vector<ImageLayer> layers = session.document().value().layers;
|
|
QVERIFY(layers[4].parentID == folder && layers[5].parentID == folder);
|
|
QCOMPARE(session.activeLayerID().value(), layers[5].id);
|
|
QCOMPARE(session.selectedLayerIDs(), (QSet<QUuid>{layers[4].id, layers[5].id}));
|
|
session.undo();
|
|
QCOMPARE(names(session), QString("A,B,Folder 1,C"));
|
|
}
|
|
|
|
void LayerClipboardTests::theEditMenuCopiesAndPastesAFolder()
|
|
{
|
|
Bar bar;
|
|
bar.session().createDocument(20, 20);
|
|
bar.session().insert(square("A"));
|
|
bar.session().groupSelectedLayers();
|
|
bar.action("copy").trigger();
|
|
bar.action("paste").trigger();
|
|
// The folder's copy holds a copy of A.
|
|
QCOMPARE(names(bar.session()), QString("Folder 1,Folder 1 copy,A,A"));
|
|
const std::vector<ImageLayer> layers = bar.session().document().value().layers;
|
|
QVERIFY(layers[2].parentID == layers[1].id && layers[3].parentID == layers[0].id);
|
|
QCOMPARE(bar.session().history.undoName(), QString("Paste"));
|
|
}
|
|
|
|
void LayerClipboardTests::clipboardNoticesKeepTheCopy()
|
|
{
|
|
// Wayland announces one copy twice; neither ends it.
|
|
ProjectWorkspace workspace;
|
|
EditorSession &session = workspace.current().session;
|
|
std::vector<QUuid> ids;
|
|
fill(session, ids);
|
|
session.selectLayer(ids[0]);
|
|
session.copySelection();
|
|
emit QGuiApplication::clipboard()->dataChanged();
|
|
emit QGuiApplication::clipboard()->dataChanged();
|
|
QVERIFY(workspace.pasteCopiedLayer());
|
|
QCOMPARE(names(session), QString("A,A copy,B,C"));
|
|
// Pixels copied under a selection go back in place too.
|
|
session.selectLayer(ids[1]);
|
|
session.applySelection(rectPath(QRectF(9, 4, 2, 2)), SelectionMode::replace, QStringLiteral("Select"));
|
|
session.copySelection();
|
|
emit QGuiApplication::clipboard()->dataChanged();
|
|
QVERIFY(!workspace.pasteCopiedLayer() && session.canPaste());
|
|
session.paste();
|
|
QCOMPARE(session.activeLayer().value().transform.origin, QPointF(9, 4));
|
|
}
|
|
|
|
void LayerClipboardTests::aRestoredOldCopyStaysExpired()
|
|
{
|
|
ProjectWorkspace workspace;
|
|
EditorSession &session = workspace.current().session;
|
|
std::vector<QUuid> ids;
|
|
fill(session, ids);
|
|
// Another app sets the same entry again: theirs now.
|
|
session.selectLayer(ids[0]);
|
|
session.copySelection();
|
|
QGuiApplication::clipboard()->setMimeData(saved());
|
|
QVERIFY(!workspace.pasteCopiedLayer());
|
|
// A layer copy, replaced by text, then restored from history.
|
|
session.copySelection();
|
|
QMimeData *old = saved();
|
|
QGuiApplication::clipboard()->setText(QStringLiteral("elsewhere"));
|
|
QGuiApplication::clipboard()->setMimeData(old);
|
|
QVERIFY(!EditorSession::clipboardHolds(session.copiedLayer().value().data));
|
|
QVERIFY(!workspace.pasteCopiedLayer());
|
|
// Its pixels come back as another app's: centred, canvas-sized.
|
|
session.paste();
|
|
QCOMPARE(names(session), QString("A,Layer 1,B,C"));
|
|
QCOMPARE(session.activeLayer().value().size(), QSizeF(20, 20));
|
|
// Selected pixels replayed so land centred, not in place.
|
|
session.selectLayer(ids[1]);
|
|
session.applySelection(rectPath(QRectF(9, 4, 2, 2)), SelectionMode::replace, QStringLiteral("Select"));
|
|
session.copySelection();
|
|
old = saved();
|
|
QGuiApplication::clipboard()->setText(QStringLiteral("elsewhere"));
|
|
QGuiApplication::clipboard()->setMimeData(old);
|
|
session.paste();
|
|
QCOMPARE(session.activeLayer().value().transform.origin, QPointF(9, 9));
|
|
}
|
|
|
|
void LayerClipboardTests::theLatestCopyAnywhereWins()
|
|
{
|
|
ProjectWorkspace workspace;
|
|
EditorSession &first = workspace.current().session;
|
|
first.createDocument(20, 20);
|
|
first.addGroup();
|
|
first.copySelection();
|
|
EditorSession &second = workspace.addTab().session;
|
|
second.createDocument(20, 20);
|
|
second.addGroup();
|
|
second.addGroup();
|
|
second.copySelection();
|
|
// The second copy ends the first's at once.
|
|
QVERIFY(!EditorSession::clipboardHolds(first.copiedLayer().value().data));
|
|
QVERIFY(EditorSession::clipboardHolds(second.copiedLayer().value().data));
|
|
// A live object elsewhere is no clipboard of ours.
|
|
QMimeData elsewhere;
|
|
QVERIFY(!EditorSession::clipboardHolds(QPointer<QMimeData>(&elsewhere)));
|
|
// The second tab's folder is the one pasted.
|
|
QVERIFY(workspace.pasteCopiedLayer());
|
|
QCOMPARE(names(second), QString("Folder 1,Folder 2,Folder 2 copy"));
|
|
QCOMPARE(names(first), QString("Folder 1"));
|
|
}
|
|
|
|
void LayerClipboardTests::aLayerTheSourceTabLacksIsNotSelected()
|
|
{
|
|
// Two tabs of one project share ids; the first wins.
|
|
ProjectWorkspace workspace;
|
|
EditorSession &first = workspace.current().session;
|
|
first.createDocument(20, 20);
|
|
first.insert(square("Shared"));
|
|
const QUuid shared = first.activeLayerID().value();
|
|
ProjectTab &second = workspace.addTab();
|
|
second.session.installProject(first.projectSnapshot().value(), QString("twin.comp"));
|
|
second.session.insert(square("Own"));
|
|
second.session.selectLayers({shared, second.session.activeLayerID().value()}, shared);
|
|
second.session.copySelection();
|
|
ProjectTab &target = workspace.addTab();
|
|
target.session.createDocument(20, 20);
|
|
QVERIFY(workspace.pasteCopiedLayer());
|
|
QTRY_COMPARE(names(target.session), QString("Shared"));
|
|
const QUuid copy = target.session.document().value().layers.front().id;
|
|
QCOMPARE(target.session.selectedLayerIDs(), QSet<QUuid>{copy});
|
|
QCOMPARE(target.session.activeLayerID().value(), copy);
|
|
}
|
|
|
|
QTEST_MAIN(LayerClipboardTests)
|
|
#include "LayerClipboardTests.moc"
|