488 lines
21 KiB
C++
488 lines
21 KiB
C++
#include "Document/BrushStroke.h"
|
||
#include "IO/ImageExporter.h"
|
||
#include "IO/ProjectStore.h"
|
||
#include "SelectionFixtures.h"
|
||
#include "AddressSpaceLimit.h"
|
||
#include <QClipboard>
|
||
#include <QColorSpace>
|
||
#include <QGuiApplication>
|
||
#include <QMimeData>
|
||
#include <QRegularExpression>
|
||
#include <malloc.h>
|
||
|
||
// Copy, Copy Merged, Paste, Layer via Copy; the clipboard.
|
||
namespace {
|
||
// A 100×40 layer: red on the left, blue right.
|
||
std::unique_ptr<EditorSession> makeSession()
|
||
{
|
||
auto session = std::make_unique<EditorSession>();
|
||
session->createDocument(100, 40, true);
|
||
QImage image = BrushRaster::context(100, 40, false);
|
||
QPainter painter(&image);
|
||
painter.fillRect(QRect(0, 0, 50, 40), Qt::red);
|
||
painter.fillRect(QRect(50, 0, 50, 40), Qt::blue);
|
||
painter.end();
|
||
session->insert(ImportedImage(image, image, "Halves"));
|
||
session->deleteLayer(session->document().value().layers[0].id);
|
||
return session;
|
||
}
|
||
|
||
void select(EditorSession &session, const QRectF &rect)
|
||
{
|
||
session.applySelection(rectPath(rect), SelectionMode::replace, "Select");
|
||
}
|
||
|
||
// Within two of each channel, for rounding on the way.
|
||
bool near(const std::vector<int> &pixel, const std::vector<int> &expected)
|
||
{
|
||
for (size_t i = 0; i < 4; ++i) {
|
||
if (std::abs(pixel[i] - expected[i]) > 2)
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
std::vector<int> pixel(const QImage &image, int x, int y)
|
||
{
|
||
const QImage bytes = image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
||
const uchar *at = bytes.constScanLine(y) + x * 4;
|
||
return {at[0], at[1], at[2], at[3]};
|
||
}
|
||
|
||
// Renders one layer alone, to read its own pixels.
|
||
std::vector<int> layerPixel(const EditorSession &session, QUuid id, int x, int y)
|
||
{
|
||
ProjectSnapshot solo = session.projectSnapshot().value();
|
||
std::erase_if(solo.manifest.layers, [&](const ProjectLayerRecord &layer) { return layer.id != id; });
|
||
return pixel(ImageExporter::render(solo).image, x, y);
|
||
}
|
||
}
|
||
|
||
class SelectionClipboardTests : public QObject {
|
||
Q_OBJECT
|
||
private slots:
|
||
void copyAndPastePutsPixelsOnANewLayerInPlace();
|
||
void layerViaCopyCopiesTheSelectionOrDuplicatesTheLayer();
|
||
void lassoShapedSelectionCopiesAndPastes();
|
||
void copyMergedTakesEveryVisibleLayerNotJustTheActiveOne();
|
||
void aCopyTakesTheLayersOwnPixelsAtTheirTransformedPlaces();
|
||
void aMaskCopiesAsOpaqueGray();
|
||
void aSoftSelectionEdgeClipsBothMaskPasses();
|
||
void theCopyRegionRoundsOffFloatNoiseAndStopsAtTheCanvas();
|
||
void anotherAppsImagePastesCentredAndUnstalesOurs();
|
||
void copyingAndPastingAreRefusedWhereSwiftRefusesThem();
|
||
void anImageTooLargeToCopyWarnsWithoutAnAlert();
|
||
void cutLeavesAHoleAndPasteRestoresThePixels();
|
||
};
|
||
|
||
void SelectionClipboardTests::copyAndPastePutsPixelsOnANewLayerInPlace()
|
||
{
|
||
const auto session = makeSession();
|
||
const QUuid source = session->activeLayerID().value();
|
||
// Straddles red and blue.
|
||
select(*session, QRectF(40, 10, 20, 20));
|
||
session->copySelection();
|
||
const int count = session->history.undoCount();
|
||
QVERIFY(session->canPaste());
|
||
QVERIFY(QGuiApplication::clipboard()->mimeData()->hasFormat("image/png"));
|
||
session->paste();
|
||
QCOMPARE(session->history.undoCount(), count + 1);
|
||
QCOMPARE(session->history.undoName(), QString("Paste"));
|
||
const ImageLayer pasted = session->activeLayer().value();
|
||
QVERIFY(pasted.id != source && pasted.name == "Layer 1" && !session->selection().has_value());
|
||
QCOMPARE(indexOf(session->document().value().layers, pasted.id), indexOf(session->document().value().layers, source) + 1);
|
||
QCOMPARE(pasted.transform.origin, QPointF(40, 10));
|
||
QCOMPARE(pasted.size(), QSizeF(20, 20));
|
||
QCOMPARE(layerPixel(*session, pasted.id, 45, 15), (std::vector<int>{255, 0, 0, 255}));
|
||
QCOMPARE(layerPixel(*session, pasted.id, 55, 15), (std::vector<int>{0, 0, 255, 255}));
|
||
QCOMPARE(layerPixel(*session, pasted.id, 30, 15)[3], 0);
|
||
// The source is untouched.
|
||
QCOMPARE(layerPixel(*session, source, 45, 15), (std::vector<int>{255, 0, 0, 255}));
|
||
session->undo();
|
||
QCOMPARE(session->document().value().layers.size(), size_t(1));
|
||
// Pasted again: the next free names; a folder's child.
|
||
session->paste();
|
||
session->paste();
|
||
QCOMPARE(session->activeLayer().value().name, QString("Layer 2"));
|
||
session->addGroup();
|
||
const QUuid folder = session->activeLayerID().value();
|
||
session->paste();
|
||
QCOMPARE(session->activeLayer().value().parentID, std::optional(folder));
|
||
QCOMPARE(session->activeLayer().value().name, QString("Layer 3"));
|
||
}
|
||
|
||
void SelectionClipboardTests::layerViaCopyCopiesTheSelectionOrDuplicatesTheLayer()
|
||
{
|
||
const auto session = makeSession();
|
||
const ImageLayer source = session->activeLayer().value();
|
||
select(*session, QRectF(60, 0, 10, 40));
|
||
session->layerViaCopy();
|
||
QCOMPARE(session->history.undoName(), QString("Layer via Copy"));
|
||
QCOMPARE(session->document().value().layers.size(), size_t(2));
|
||
QCOMPARE(session->activeLayer().value().size(), QSizeF(10, 40));
|
||
QVERIFY(!session->selection().has_value());
|
||
QCOMPARE(layerPixel(*session, session->activeLayerID().value(), 65, 20), (std::vector<int>{0, 0, 255, 255}));
|
||
session->selectLayer(source.id);
|
||
session->layerViaCopy();
|
||
QCOMPARE(session->history.undoName(), QString("Duplicate Layer"));
|
||
QCOMPARE(session->activeLayer().value().name, source.name + " copy");
|
||
QVERIFY(session->activeLayer().value().asset.value().identity() == source.asset.value().identity());
|
||
// A folder duplicates; a folder under a selection copies nothing.
|
||
session->addGroup();
|
||
const QUuid folder = session->activeLayerID().value();
|
||
session->layerViaCopy();
|
||
QCOMPARE(session->history.undoName(), QString("Duplicate Layer"));
|
||
QCOMPARE(session->activeLayer().value().name, QString("Folder 1 copy"));
|
||
session->selectLayer(folder);
|
||
select(*session, QRectF(0, 0, 10, 10));
|
||
const int count = session->history.undoCount();
|
||
session->layerViaCopy();
|
||
QCOMPARE(session->history.undoCount(), count);
|
||
// Its chosen mask has pixels, yet Swift copies none.
|
||
session->deselect();
|
||
session->addMask(true);
|
||
select(*session, QRectF(0, 0, 10, 10));
|
||
QVERIFY(session->isMaskSelected() && session->canCopyPixels());
|
||
const int masked = session->history.undoCount();
|
||
session->layerViaCopy();
|
||
QCOMPARE(session->history.undoCount(), masked);
|
||
const int later = masked - count;
|
||
// An empty selection copies nothing either.
|
||
session->selectLayer(source.id);
|
||
session->applySelection(rectPath(QRectF(0, 0, 100, 40)), SelectionMode::subtract, "Subtract");
|
||
session->layerViaCopy();
|
||
QCOMPARE(session->history.undoCount(), count + later + 1);
|
||
}
|
||
|
||
void SelectionClipboardTests::lassoShapedSelectionCopiesAndPastes()
|
||
{
|
||
const auto session = makeSession();
|
||
session->selectTool(NavigationTool::lasso);
|
||
// A triangle over the red half.
|
||
session->beginLasso(QPointF(10, 5), SelectionMode::replace);
|
||
session->extendLasso(QPointF(40, 5));
|
||
session->extendLasso(QPointF(10, 35));
|
||
session->finishLasso();
|
||
QVERIFY(session->history.undoName() == "Lasso" && session->canCopyPixels());
|
||
session->copySelection();
|
||
session->paste();
|
||
const ImageLayer pasted = session->activeLayer().value();
|
||
QCOMPARE(pasted.transform.origin, QPointF(10, 5));
|
||
QCOMPARE(pasted.size(), QSizeF(30, 30));
|
||
QCOMPARE(layerPixel(*session, pasted.id, 15, 10), (std::vector<int>{255, 0, 0, 255}));
|
||
// Past the diagonal, and a soft edge along it.
|
||
QCOMPARE(layerPixel(*session, pasted.id, 38, 33)[3], 0);
|
||
int soft = 0;
|
||
for (int x = 10; x < 40; ++x) {
|
||
const int alpha = layerPixel(*session, pasted.id, x, 40 - x + 4)[3];
|
||
soft += alpha > 0 && alpha < 255;
|
||
}
|
||
QVERIFY(soft > 0);
|
||
}
|
||
|
||
void SelectionClipboardTests::copyMergedTakesEveryVisibleLayerNotJustTheActiveOne()
|
||
{
|
||
const auto session = makeSession();
|
||
// A small green layer over the red and blue one.
|
||
QImage square = BrushRaster::context(20, 20, false);
|
||
square.fill(Qt::green);
|
||
session->insert(ImportedImage(square, square, "Green"), QPointF(70, 20));
|
||
const QUuid green = session->activeLayerID().value();
|
||
QCOMPARE(layerWith(*session, green).transform.origin, QPointF(60, 10));
|
||
select(*session, QRectF(55, 5, 30, 30));
|
||
// The active layer alone: just the green square.
|
||
session->copySelection();
|
||
session->paste();
|
||
QUuid pasted = session->activeLayerID().value();
|
||
QCOMPARE(layerPixel(*session, pasted, 65, 15), (std::vector<int>{0, 255, 0, 255}));
|
||
QCOMPARE(layerPixel(*session, pasted, 58, 8)[3], 0);
|
||
session->undo();
|
||
session->selectLayer(green);
|
||
select(*session, QRectF(55, 5, 30, 30));
|
||
QVERIFY(session->canCopyMerged());
|
||
session->copyMergedSelection();
|
||
session->paste();
|
||
pasted = session->activeLayerID().value();
|
||
QCOMPARE(layerPixel(*session, pasted, 65, 15), (std::vector<int>{0, 255, 0, 255}));
|
||
QCOMPARE(layerPixel(*session, pasted, 58, 8), (std::vector<int>{0, 0, 255, 255}));
|
||
session->undo();
|
||
// A triangle: the far corner clear, its hypotenuse soft.
|
||
session->selectLayer(green);
|
||
session->selectTool(NavigationTool::lasso);
|
||
lasso(*session, {QPointF(55, 5), QPointF(85, 5), QPointF(55, 35)});
|
||
QCOMPARE(session->history.undoName(), QString("Lasso"));
|
||
session->copyMergedSelection();
|
||
session->paste();
|
||
pasted = session->activeLayerID().value();
|
||
QCOMPARE(layerPixel(*session, pasted, 62, 12), (std::vector<int>{0, 255, 0, 255}));
|
||
QCOMPARE(layerPixel(*session, pasted, 83, 33)[3], 0);
|
||
const int edge = layerPixel(*session, pasted, 69, 20)[3];
|
||
QVERIFY(edge > 0 && edge < 255);
|
||
session->undo();
|
||
// Hidden layers are left out.
|
||
session->selectLayer(green);
|
||
session->toggleLayerVisibility(green);
|
||
select(*session, QRectF(55, 5, 30, 30));
|
||
session->copyMergedSelection();
|
||
session->paste();
|
||
pasted = session->activeLayerID().value();
|
||
QCOMPARE(layerPixel(*session, pasted, 65, 15), (std::vector<int>{0, 0, 255, 255}));
|
||
// With nothing drawn there is nothing to merge.
|
||
const auto blank = std::make_unique<EditorSession>();
|
||
blank->createDocument(8, 8, true);
|
||
QVERIFY(!blank->canCopyMerged());
|
||
}
|
||
|
||
void SelectionClipboardTests::aCopyTakesTheLayersOwnPixelsAtTheirTransformedPlaces()
|
||
{
|
||
const auto session = makeSession();
|
||
// A 20 by 10 yellow bar centred at (50, 20).
|
||
QImage bar = BrushRaster::context(20, 10, false);
|
||
bar.fill(Qt::yellow);
|
||
session->insert(ImportedImage(bar, bar, "Bar"), QPointF(50, 20));
|
||
const QUuid id = session->activeLayerID().value();
|
||
QCOMPARE(layerWith(*session, id).transform.origin, QPointF(40, 15));
|
||
// Hidden, faint, masked out and turned upright: the pixels stay.
|
||
QImage black = BrushRaster::context(20, 10, true);
|
||
black.fill(Qt::black);
|
||
rewrite(*session, [&](ProjectSnapshot &snapshot) {
|
||
setMask(snapshot, id, LayerMask::assetFrom(black));
|
||
record(snapshot, id).isVisible = false;
|
||
record(snapshot, id).opacity = 0.25;
|
||
record(snapshot, id).transform.rotation = 90;
|
||
});
|
||
session->selectLayerTarget(id, false);
|
||
select(*session, QRectF(30, 5, 40, 30));
|
||
session->copySelection();
|
||
session->paste();
|
||
const ImageLayer pasted = session->activeLayer().value();
|
||
QCOMPARE(pasted.transform.origin, QPointF(30, 5));
|
||
QCOMPARE(layerPixel(*session, pasted.id, 50, 12), (std::vector<int>{255, 255, 0, 255}));
|
||
QCOMPARE(layerPixel(*session, pasted.id, 50, 28), (std::vector<int>{255, 255, 0, 255}));
|
||
QCOMPARE(layerPixel(*session, pasted.id, 42, 20)[3], 0);
|
||
QCOMPARE(layerPixel(*session, pasted.id, 58, 20)[3], 0);
|
||
}
|
||
|
||
void SelectionClipboardTests::aMaskCopiesAsOpaqueGray()
|
||
{
|
||
const auto session = makeSession();
|
||
const QUuid id = session->activeLayerID().value();
|
||
QImage mask = BrushRaster::context(100, 40, true);
|
||
mask.fill(Qt::white);
|
||
for (int y = 0; y < 40; ++y)
|
||
std::fill_n(mask.scanLine(y), 30, uchar(0));
|
||
rewrite(*session, [&](ProjectSnapshot &snapshot) { setMask(snapshot, id, LayerMask::assetFrom(mask)); });
|
||
session->selectLayerTarget(id, true);
|
||
select(*session, QRectF(20, 10, 20, 20));
|
||
QVERIFY(session->canCopyPixels());
|
||
session->copySelection();
|
||
session->paste();
|
||
const QUuid pasted = session->activeLayerID().value();
|
||
QCOMPARE(layerPixel(*session, pasted, 25, 15), (std::vector<int>{0, 0, 0, 255}));
|
||
QCOMPARE(layerPixel(*session, pasted, 35, 15), (std::vector<int>{255, 255, 255, 255}));
|
||
QCOMPARE(layerPixel(*session, pasted, 45, 15)[3], 0);
|
||
session->undo();
|
||
// A mask placed apart: outside it, its edges' mean gray.
|
||
rewrite(*session, [&](ProjectSnapshot &snapshot) {
|
||
record(snapshot, id).maskPlacement = LayerTransform{.origin = {60, 0}, .size = {100, 40}};
|
||
record(snapshot, id).maskLinked = false;
|
||
});
|
||
session->selectLayerTarget(id, true);
|
||
select(*session, QRectF(20, 10, 20, 20));
|
||
session->copySelection();
|
||
session->paste();
|
||
const std::vector<int> ground = layerPixel(*session, session->activeLayerID().value(), 25, 15);
|
||
const int tone = qRound(LayerMask::background(layerWith(*session, id).mask.value().asset.thumbnail) * 255);
|
||
QVERIFY(tone > 0);
|
||
QCOMPARE(ground, (std::vector<int>{tone, tone, tone, 255}));
|
||
session->undo();
|
||
// Placed away, this mask's black edges cover the region.
|
||
QImage inverse = BrushRaster::context(100, 40, true);
|
||
inverse.fill(Qt::black);
|
||
for (int y = 5; y < 35; ++y)
|
||
std::fill_n(inverse.scanLine(y) + 15, 30, uchar(255));
|
||
rewrite(*session, [&](ProjectSnapshot &snapshot) {
|
||
setMask(snapshot, id, LayerMask::assetFrom(inverse));
|
||
record(snapshot, id).maskPlacement = LayerTransform{.origin = {60, 0}, .size = {100, 40}};
|
||
record(snapshot, id).maskLinked = false;
|
||
});
|
||
session->selectLayerTarget(id, true);
|
||
select(*session, QRectF(20, 10, 20, 20));
|
||
session->copySelection();
|
||
session->paste();
|
||
QCOMPARE(layerPixel(*session, session->activeLayerID().value(), 25, 15), (std::vector<int>{0, 0, 0, 255}));
|
||
session->undo();
|
||
// A folder copies its mask alone; without one, nothing.
|
||
session->selectLayer(id);
|
||
session->addGroup();
|
||
QVERIFY(!session->canCopyPixels());
|
||
session->addLayerMask(false);
|
||
QVERIFY(session->canCopyPixels());
|
||
}
|
||
|
||
void SelectionClipboardTests::aSoftSelectionEdgeClipsBothMaskPasses()
|
||
{
|
||
const auto session = makeSession();
|
||
const QUuid id = session->activeLayerID().value();
|
||
session->addMask(true);
|
||
session->selectLayerTarget(id, true);
|
||
// Half a pixel in: half coverage at the left column.
|
||
select(*session, QRectF(20.5, 10, 20, 20));
|
||
session->copySelection();
|
||
session->paste();
|
||
const QUuid pasted = session->activeLayerID().value();
|
||
// Ground at half, then white at half over it: 192.
|
||
QVERIFY(near(layerPixel(*session, pasted, 20, 15), {128, 128, 128, 192}));
|
||
QCOMPARE(layerPixel(*session, pasted, 25, 15), (std::vector<int>{255, 255, 255, 255}));
|
||
QCOMPARE(layerPixel(*session, pasted, 19, 15), (std::vector<int>{0, 0, 0, 0}));
|
||
// The layer's pixels take the coverage once.
|
||
session->selectLayerTarget(id, false);
|
||
select(*session, QRectF(20.5, 10, 20, 20));
|
||
session->copySelection();
|
||
session->paste();
|
||
QVERIFY(near(layerPixel(*session, session->activeLayerID().value(), 20, 15), {128, 0, 0, 128}));
|
||
}
|
||
|
||
void SelectionClipboardTests::theCopyRegionRoundsOffFloatNoiseAndStopsAtTheCanvas()
|
||
{
|
||
const auto session = makeSession();
|
||
QCOMPARE(session->selectionCopyRegion(), std::optional(QRectF(0, 0, 100, 40)));
|
||
session->applySelection(rectPath(QRectF(9.9995, 10.0004, 30.0009, 19.9993)), SelectionMode::replace, "Select");
|
||
QCOMPARE(session->selectionCopyRegion(), std::optional(QRectF(10, 10, 30, 20)));
|
||
session->applySelection(rectPath(QRectF(10.3, 10.3, 30, 20)), SelectionMode::replace, "Select");
|
||
QCOMPARE(session->selectionCopyRegion(), std::optional(QRectF(10, 10, 31, 21)));
|
||
// Beyond the canvas the region stops; nudged off it, nothing.
|
||
session->selectAll();
|
||
session->nudgeSelection(90, 0);
|
||
QCOMPARE(session->selectionCopyRegion(), std::optional(QRectF(90, 0, 10, 40)));
|
||
session->nudgeSelection(10, 0);
|
||
QVERIFY(!session->selectionCopyRegion().has_value());
|
||
QVERIFY(session->canCopyPixels());
|
||
const int count = session->history.undoCount();
|
||
session->copySelection();
|
||
session->layerViaCopy();
|
||
QCOMPARE(session->history.undoCount(), count);
|
||
QVERIFY(!session->pixelClipboard().has_value());
|
||
}
|
||
|
||
void SelectionClipboardTests::anotherAppsImagePastesCentredAndUnstalesOurs()
|
||
{
|
||
const auto session = makeSession();
|
||
select(*session, QRectF(0, 0, 10, 10));
|
||
session->copySelection();
|
||
QVERIFY(session->canPaste());
|
||
// Another app copies a picture: Paste centres that instead.
|
||
QImage picture(20, 10, QImage::Format_ARGB32);
|
||
picture.fill(QColor(0, 255, 0, 255));
|
||
QGuiApplication::clipboard()->setImage(picture);
|
||
QVERIFY(session->canPaste());
|
||
session->paste();
|
||
const ImageLayer pasted = session->activeLayer().value();
|
||
QCOMPARE(pasted.transform.origin, QPointF(40, 15));
|
||
QCOMPARE(pasted.size(), QSizeF(20, 10));
|
||
QCOMPARE(pasted.asset.value().image().format(), QImage::Format_RGBA8888_Premultiplied);
|
||
QCOMPARE(layerPixel(*session, pasted.id, 50, 20), (std::vector<int>{0, 255, 0, 255}));
|
||
// Linear gray 128 is sRGB 188, as CoreGraphics converts it.
|
||
QImage linear(4, 4, QImage::Format_ARGB32);
|
||
linear.fill(QColor(128, 128, 128, 255));
|
||
linear.setColorSpace(QColorSpace::SRgbLinear);
|
||
QGuiApplication::clipboard()->setImage(linear);
|
||
session->paste();
|
||
const ImageLayer converted = session->activeLayer().value();
|
||
QVERIFY(near(layerPixel(*session, converted.id, 50, 20), {188, 188, 188, 255}));
|
||
QCOMPARE(converted.asset.value().image().colorSpace(), QColorSpace(QColorSpace::SRgb));
|
||
// Text on the clipboard pastes nothing.
|
||
QGuiApplication::clipboard()->setText(QStringLiteral("words"));
|
||
QVERIFY(!session->canPaste());
|
||
const int count = session->history.undoCount();
|
||
session->paste();
|
||
QCOMPARE(session->history.undoCount(), count);
|
||
// Our own copy again: back in place.
|
||
session->copySelection();
|
||
session->paste();
|
||
QCOMPARE(session->activeLayer().value().transform.origin, QPointF(0, 0));
|
||
}
|
||
|
||
void SelectionClipboardTests::copyingAndPastingAreRefusedWhereSwiftRefusesThem()
|
||
{
|
||
EditorSession session;
|
||
QVERIFY(!session.canCopyPixels() && !session.canCopyMerged() && !session.canPaste());
|
||
session.createDocument(20, 10, true);
|
||
// A blank layer has no pixels to copy.
|
||
QVERIFY(!session.canCopyPixels());
|
||
QImage image = BrushRaster::context(20, 10, false);
|
||
image.fill(Qt::red);
|
||
session.insert(ImportedImage(image, image, "Red"));
|
||
QVERIFY(session.canCopyPixels());
|
||
session.copySelection();
|
||
QVERIFY(session.canPaste());
|
||
// An empty selection copies nothing; busy, nothing at all.
|
||
select(session, QRectF(0, 0, 5, 5));
|
||
session.applySelection(rectPath(QRectF(0, 0, 20, 10)), SelectionMode::subtract, "Subtract");
|
||
QVERIFY(!session.canCopyPixels() && !session.canCopyMerged());
|
||
session.deselect();
|
||
session.setIsProjectBusy(true);
|
||
QVERIFY(!session.canCopyPixels() && !session.canPaste());
|
||
session.setIsProjectBusy(false);
|
||
QVERIFY(session.canPaste());
|
||
// A paste's signal, a copy's silence.
|
||
QSignalSpy changes(&session, &EditorSession::changed);
|
||
session.copySelection();
|
||
QCOMPARE(changes.count(), 0);
|
||
session.paste();
|
||
QVERIFY(changes.count() > 0);
|
||
// A cleared clipboard holds no data object at all.
|
||
QGuiApplication::clipboard()->clear();
|
||
QVERIFY(!session.canPaste());
|
||
const int count = session.history.undoCount();
|
||
session.paste();
|
||
QCOMPARE(session.history.undoCount(), count);
|
||
}
|
||
|
||
void SelectionClipboardTests::anImageTooLargeToCopyWarnsWithoutAnAlert()
|
||
{
|
||
const auto session = makeSession();
|
||
// Half a megabyte of bits; its working copy needs sixteen.
|
||
const QImage large(2000, 2000, QImage::Format_Mono);
|
||
QVERIFY(!large.isNull());
|
||
QGuiApplication::clipboard()->setImage(large);
|
||
QVERIFY(session->canPaste());
|
||
const int count = session->history.undoCount();
|
||
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(QStringLiteral("the clipboard's image could not be copied")));
|
||
{
|
||
malloc_trim(0);
|
||
const AddressSpaceLimit limit(4 * 1024 * 1024);
|
||
session->paste();
|
||
}
|
||
QCOMPARE(session->history.undoCount(), count);
|
||
QVERIFY(!session->brushError().has_value());
|
||
}
|
||
|
||
void SelectionClipboardTests::cutLeavesAHoleAndPasteRestoresThePixels()
|
||
{
|
||
const auto session = makeSession();
|
||
const QUuid source = session->activeLayerID().value();
|
||
select(*session, QRectF(0, 0, 10, 10));
|
||
const int count = session->history.undoCount();
|
||
bool done = false;
|
||
session->cutSelection([&] { done = true; });
|
||
QTRY_VERIFY(done);
|
||
QVERIFY(session->pixelClipboard().has_value());
|
||
QCOMPARE(session->history.undoName(), QString("Clear"));
|
||
QCOMPARE(session->history.undoCount(), count + 1);
|
||
QCOMPARE(layerPixel(*session, source, 5, 5)[3], 0);
|
||
QCOMPARE(layerPixel(*session, source, 15, 5), (std::vector<int>{255, 0, 0, 255}));
|
||
// The pixels come back where they were.
|
||
session->paste();
|
||
QCOMPARE(layerPixel(*session, session->activeLayerID().value(), 5, 5), (std::vector<int>{255, 0, 0, 255}));
|
||
// Without a selection, nothing is cut.
|
||
session->deselect();
|
||
done = false;
|
||
session->cutSelection([&] { done = true; });
|
||
QTRY_VERIFY(done);
|
||
QCOMPARE(session->history.undoName(), QString("Paste"));
|
||
}
|
||
|
||
QTEST_MAIN(SelectionClipboardTests)
|
||
#include "SelectionClipboardTests.moc"
|