12.2a: folders duplicate with their contents, folder opacity, format version 8, View > Snap (Compositor 1.1.5–1.1.6)
This commit is contained in:
1 parent
ee34c338d4
commit
f1b2c65c1e
52 files changed
+909
-239
No files matched your search
@@ -15,8 +15,6 @@ private slots:
|
||||
void fileEntriesFollowTheControllersGate();
|
||||
void undoAndRedoCarryTheirNamesAndKeepAFieldsOwn();
|
||||
void theMenuBarBorrowsFocusAndTheFieldKeepsUndo();
|
||||
void viewEntriesZoomTheFrontSession();
|
||||
void thePixelGridEntryTogglesTheSession();
|
||||
void layerEntriesFollowTheActiveLayer();
|
||||
void duplicateMergeAndFlipEntriesActOnTheLayers();
|
||||
void transformEntriesFollowTheMoveTool();
|
||||
@@ -32,7 +30,7 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand()
|
||||
{"newCanvas", "Ctrl+N"}, {"openProject", "Ctrl+O"}, {"importImages", ""}, {"save", "Ctrl+S"}, {"saveAs", "Ctrl+Shift+S"},
|
||||
{"exportPNG", "Ctrl+Shift+E"}, {"exportJPEG", "Ctrl+Alt+Shift+S"},
|
||||
{"closeProject", "Ctrl+W"}, {"undo", "Ctrl+Z"}, {"redo", "Ctrl+Shift+Z"}, {"fit", "Ctrl+0"}, {"actualPixels", "Ctrl+1"},
|
||||
{"zoomIn", "Ctrl+="}, {"zoomOut", "Ctrl+-"}, {"pixelGrid", ""}, {"transformControls", "Ctrl+H"}, {"transformLayer", "Ctrl+T"},
|
||||
{"zoomIn", "Ctrl+="}, {"zoomOut", "Ctrl+-"}, {"pixelGrid", ""}, {"snap", ""}, {"transformControls", "Ctrl+H"}, {"transformLayer", "Ctrl+T"},
|
||||
{"layerViaCopy", "Ctrl+J"}, {"cut", "Ctrl+X"}, {"copy", "Ctrl+C"}, {"copyMerged", "Ctrl+Shift+C"}, {"paste", "Ctrl+V"}, {"fillForeground", "Alt+Backspace"}, {"fillBackground", "Ctrl+Backspace"}, {"clearSelectionPixels", ""}, {"contentAwareFill", "Shift+Backspace"}, {"clippingMask", "Ctrl+Alt+G"}, {"groupLayers", "Ctrl+G"},
|
||||
{"moveOutOfFolder", ""}, {"newBlankLayer", "Ctrl+Shift+N"}, {"renameLayer", ""}, {"layerVisibility", ""}, {"moveLayerUp", "Ctrl+]"}, {"moveLayerDown", "Ctrl+["},
|
||||
{"mergeLayers", "Ctrl+E"}, {"flipHorizontal", ""}, {"flipVertical", ""}, {"deleteLayer", ""},
|
||||
@@ -53,7 +51,7 @@ void CompositorMenusTests::everyEntryHasSwiftsShortcutWithCtrlForCommand()
|
||||
QCOMPARE(titles, (QStringList{"&File", "&Edit", "&View", "&Select", "&Image", "Fil&ter", "&Layer"}));
|
||||
QCOMPARE(menus[0]->actions().size(), 11);
|
||||
QCOMPARE(menus[1]->actions().size(), 12);
|
||||
QCOMPARE(menus[2]->actions().size(), 6);
|
||||
QCOMPARE(menus[2]->actions().size(), 7);
|
||||
QCOMPARE(menus[3]->actions().size(), 10);
|
||||
QCOMPARE(menus[4]->actions().size(), 13);
|
||||
QCOMPARE(menus[5]->actions().size(), 5);
|
||||
@@ -226,43 +224,6 @@ void CompositorMenusTests::theMenuBarBorrowsFocusAndTheFieldKeepsUndo()
|
||||
QTRY_COMPARE(undo.text(), QString("Undo New Blank Layer"));
|
||||
}
|
||||
|
||||
void CompositorMenusTests::viewEntriesZoomTheFrontSession()
|
||||
{
|
||||
Bar bar;
|
||||
for (const char *name : {"fit", "actualPixels", "zoomIn", "zoomOut"})
|
||||
QVERIFY(!bar.action(name).isEnabled());
|
||||
bar.session().createDocument(400, 300);
|
||||
for (const char *name : {"fit", "actualPixels", "zoomIn", "zoomOut"})
|
||||
QVERIFY(bar.action(name).isEnabled());
|
||||
bar.session().zoom(0.5);
|
||||
bar.action("actualPixels").trigger();
|
||||
QCOMPARE(bar.session().viewport.zoom(), 1.0);
|
||||
bar.action("zoomIn").trigger();
|
||||
QCOMPARE(bar.session().viewport.zoom(), 1.25);
|
||||
bar.action("zoomOut").trigger();
|
||||
bar.action("zoomOut").trigger();
|
||||
QCOMPARE(bar.session().viewport.zoom(), 0.8);
|
||||
bar.action("fit").trigger();
|
||||
QVERIFY(bar.session().viewport.followsFit());
|
||||
}
|
||||
|
||||
void CompositorMenusTests::thePixelGridEntryTogglesTheSession()
|
||||
{
|
||||
Bar bar;
|
||||
QAction &grid = bar.action("pixelGrid");
|
||||
QVERIFY(grid.isCheckable() && grid.isChecked() && grid.isEnabled());
|
||||
QCOMPARE(grid.text(), QString("Pixel Grid (800% and above)"));
|
||||
grid.trigger();
|
||||
QVERIFY(!bar.session().showsPixelGrid() && !grid.isChecked());
|
||||
grid.trigger();
|
||||
QVERIFY(bar.session().showsPixelGrid() && grid.isChecked());
|
||||
// The session's own change reaches the mark.
|
||||
bar.session().setShowsPixelGrid(false);
|
||||
QVERIFY(!grid.isChecked());
|
||||
bar.workspace.newCanvas();
|
||||
QVERIFY(grid.isChecked() && grid.isEnabled());
|
||||
}
|
||||
|
||||
void CompositorMenusTests::layerEntriesFollowTheActiveLayer()
|
||||
{
|
||||
Bar bar;
|
||||
|
||||
@@ -199,6 +199,11 @@ void CropCanvasTests::snappingReachesEightViewPoints()
|
||||
const QPointF from = shown.session.viewport.viewPoint(QPointF(100, 50), size), to = shown.session.viewport.viewPoint(QPointF(390, 170), size);
|
||||
shown.drag(from, to);
|
||||
QCOMPARE(shown.frame(), std::optional(QRectF(100, 50, 300, 120)));
|
||||
// Snap off: the edges stay where the pointer puts them.
|
||||
shown.session.setSnappingEnabled(false);
|
||||
shown.session.setCropRect(std::nullopt);
|
||||
shown.drag(from, to);
|
||||
QCOMPARE(shown.frame(), std::optional(QRectF(100, 50, 290, 120)));
|
||||
}
|
||||
|
||||
void CropCanvasTests::theCursorShowsWhatAPressTakes()
|
||||
|
||||
@@ -43,7 +43,6 @@ private slots:
|
||||
void foldingAFolderTakesTheSelectionFromItsContents();
|
||||
void deletingAFolderTakesItsContents();
|
||||
void movingStaysAmongSiblings();
|
||||
void duplicatingALayerByDraggingPlacesTheCopyAsOneStep();
|
||||
void aNewCanvasCanStartWithALayer();
|
||||
void sessionChangesAreLogged();
|
||||
};
|
||||
@@ -445,50 +444,5 @@ void EditorSessionTests::sessionChangesAreLogged()
|
||||
}
|
||||
|
||||
// Alt-dragging a row drops a duplicate, one step.
|
||||
void EditorSessionTests::duplicatingALayerByDraggingPlacesTheCopyAsOneStep()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(800, 600);
|
||||
for (int each = 0; each < 3; ++each)
|
||||
session.addBlankLayer();
|
||||
const std::vector<LayerHierarchy::Entry> rows = session.layerRows();
|
||||
const ProjectLayerRecord top = rows.front().layer, bottom = rows.back().layer;
|
||||
const std::optional<CanvasDocument> before = session.document();
|
||||
const int count = session.history.undoCount();
|
||||
QVERIFY(session.duplicateLayer(bottom.id, std::nullopt, top.id));
|
||||
const std::vector<LayerHierarchy::Entry> after = session.layerRows();
|
||||
QCOMPARE(after.size(), size_t(4));
|
||||
QCOMPARE(session.history.undoCount(), count + 1);
|
||||
QCOMPARE(session.history.undoName(), QString("Duplicate Layer"));
|
||||
QCOMPARE(after.front().layer.name, bottom.name + " copy");
|
||||
QCOMPARE(after.front().layer.id, session.activeLayerID());
|
||||
QVERIFY(indexOf(session.document().value().layers, bottom.id) >= 0);
|
||||
session.undo();
|
||||
QCOMPARE(session.document(), before);
|
||||
// Folders are not duplicated this way; neither is a stranger.
|
||||
session.addGroup();
|
||||
const QUuid folder = session.activeLayerID().value();
|
||||
QVERIFY(!session.duplicateLayer(folder, std::nullopt, std::nullopt, true));
|
||||
QVERIFY(!session.duplicateLayer(QUuid::createUuid(), std::nullopt));
|
||||
QVERIFY(!session.duplicateLayer(bottom.id, bottom.id));
|
||||
QCOMPARE(session.layerRows().size(), size_t(4));
|
||||
// Into a folder, at its top.
|
||||
QVERIFY(session.duplicateLayer(bottom.id, folder));
|
||||
QCOMPARE(layerWith(session, session.activeLayerID().value()).parentID, std::optional(folder));
|
||||
session.setIsImporting(true);
|
||||
QVERIFY(!session.duplicateLayer(bottom.id, std::nullopt));
|
||||
session.setIsImporting(false);
|
||||
// The copy sits right above; a folder makes none.
|
||||
session.selectLayer(top.id);
|
||||
session.duplicateActiveLayer();
|
||||
const int original = indexOf(session.document().value().layers, top.id);
|
||||
QCOMPARE(session.document().value().layers[original + 1].name, top.name + " copy");
|
||||
QCOMPARE(session.activeLayerID(), std::optional(session.document().value().layers[original + 1].id));
|
||||
const size_t kept = session.document().value().layers.size();
|
||||
session.selectLayer(folder);
|
||||
session.duplicateActiveLayer();
|
||||
QCOMPARE(session.document().value().layers.size(), kept);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(EditorSessionTests)
|
||||
#include "EditorSessionTests.moc"
|
||||
+24
-10
@@ -147,7 +147,8 @@ void EffectsPanelTests::theSheetEditsItsEffect()
|
||||
QCOMPARE(size.text(), QString("4"));
|
||||
type(size, "12");
|
||||
QCOMPARE(editor.effects().stroke->size, 12.0);
|
||||
QCOMPARE(slider.value(), 22);
|
||||
// The slider spans 0 to 20; typing reaches 500.
|
||||
QCOMPARE(slider.value(), 600);
|
||||
// No finite number changes nothing; the field goes back.
|
||||
type(size, "abc");
|
||||
QVERIFY(editor.effects().stroke->size == 12 && size.text() == "12");
|
||||
@@ -158,6 +159,8 @@ void EffectsPanelTests::theSheetEditsItsEffect()
|
||||
QCOMPARE(editor.effects().stroke->size, 13.0);
|
||||
QTest::keyClick(&size, Qt::Key_Up, Qt::ShiftModifier);
|
||||
QCOMPARE(editor.effects().stroke->size, 23.0);
|
||||
// Past its end the thumb rests there, writing nothing back.
|
||||
QCOMPARE(slider.value(), 1000);
|
||||
// A step replaces pending typing, even where it clamps.
|
||||
typing(size, "12");
|
||||
QTest::keyClick(&size, Qt::Key_Up);
|
||||
@@ -172,12 +175,12 @@ void EffectsPanelTests::theSheetEditsItsEffect()
|
||||
size.clearFocus();
|
||||
QCOMPARE(editor.effects().stroke->size, 500.0);
|
||||
// The slider sets any value; the field rounds, ties even.
|
||||
slider.setValue(500);
|
||||
QVERIFY(editor.effects().stroke->size == 250.5 && size.text() == "250");
|
||||
slider.setValue(525);
|
||||
QVERIFY(editor.effects().stroke->size == 10.5 && size.text() == "10");
|
||||
// Leaving an untouched field keeps the exact value.
|
||||
size.setFocus();
|
||||
size.clearFocus();
|
||||
QCOMPARE(editor.effects().stroke->size, 250.5);
|
||||
QCOMPARE(editor.effects().stroke->size, 10.5);
|
||||
// Typing waits while other changes arrive.
|
||||
auto &opacity = find<QLineEdit>(sheet, "opacityField");
|
||||
QCOMPARE(opacity.text(), QString("100"));
|
||||
@@ -206,6 +209,11 @@ void EffectsPanelTests::theSheetEditsItsEffect()
|
||||
editor.session.setColorPickerHSB(PickerHSB(PaletteColor{1, 0, 0}));
|
||||
QCOMPARE(editor.effects().stroke->color(), (PaletteColor{1, 0, 0}));
|
||||
QTRY_VERIFY(paints > 0);
|
||||
// A black rim, a white ring inside, then the colour.
|
||||
const QImage shot = swatch.grab().toImage();
|
||||
QVERIFY2(shot.pixelColor(0, 9) == QColor(Qt::black), qPrintable(shot.pixelColor(0, 9).name()));
|
||||
QVERIFY2(shot.pixelColor(1, 9) == QColor(Qt::white), qPrintable(shot.pixelColor(1, 9).name()));
|
||||
QCOMPARE(shot.pixelColor(18, 9), QColor(Qt::red));
|
||||
editor.session.closeColorPicker(true);
|
||||
// Return commits, then goes on to OK, keeping everything.
|
||||
typing(size, "7");
|
||||
@@ -227,7 +235,7 @@ void EffectsPanelTests::aWheelOnTheSliderReplacesTheTyping()
|
||||
QWheelEvent wheel(QPointF(10, 5), slider.mapToGlobal(QPointF(10, 5)), QPoint(), QPoint(0, -120), Qt::NoButton, Qt::NoModifier,
|
||||
Qt::NoScrollPhase, false);
|
||||
QApplication::sendEvent(&slider, &wheel);
|
||||
const double wheeled = 1 + 499.0 * (6 - QApplication::wheelScrollLines()) / 1000;
|
||||
const double wheeled = 20.0 * (200 - QApplication::wheelScrollLines()) / 1000;
|
||||
QCOMPARE(editor.effects().stroke->size, wheeled);
|
||||
QVERIFY(size.hasFocus() && size.text() == QString::number(std::lround(wheeled)) && !size.isModified());
|
||||
// Return then keeps the slider's value, going on to OK.
|
||||
@@ -276,11 +284,13 @@ void EffectsPanelTests::eachKindHasSwiftsControls_data()
|
||||
QTest::addColumn<LayerEffectKind>("kind");
|
||||
QTest::addColumn<QString>("headline");
|
||||
QTest::addColumn<QStringList>("fields");
|
||||
const QStringList shadow{"Opacity:0:100", "Angle:-180:180", "Distance:0:300", "Blur:0:300"};
|
||||
QTest::newRow("stroke") << LayerEffectKind::stroke << QString("Stroke") << QStringList{"Size:1:500", "Opacity:0:100"};
|
||||
// Title, typed floor and ceiling, then the slider's.
|
||||
const QStringList shadow{"Opacity:0:100:100", "Angle:-180:180:180", "Distance:0:5000:100", "Blur:0:500:100"};
|
||||
QTest::newRow("stroke") << LayerEffectKind::stroke << QString("Stroke") << QStringList{"Size:0:500:20", "Opacity:0:100:100"};
|
||||
QTest::newRow("shadow") << LayerEffectKind::shadow << QString("Drop Shadow") << shadow;
|
||||
QTest::newRow("overlay") << LayerEffectKind::colorOverlay << QString("Color Overlay") << QStringList{"Opacity:0:100"};
|
||||
QTest::newRow("inner shadow") << LayerEffectKind::innerShadow << QString("Inner Shadow") << shadow;
|
||||
QTest::newRow("overlay") << LayerEffectKind::colorOverlay << QString("Color Overlay") << QStringList{"Opacity:0:100:100"};
|
||||
QTest::newRow("inner shadow") << LayerEffectKind::innerShadow << QString("Inner Shadow")
|
||||
<< QStringList{"Opacity:0:100:100", "Angle:-180:180:180", "Distance:0:5000:50", "Blur:0:500:100"};
|
||||
}
|
||||
|
||||
void EffectsPanelTests::eachKindHasSwiftsControls()
|
||||
@@ -312,6 +322,10 @@ void EffectsPanelTests::eachKindHasSwiftsControls()
|
||||
QCOMPARE(field.text(), parts[2]);
|
||||
type(field, "-99999");
|
||||
QCOMPARE(field.text(), parts[1]);
|
||||
// The slider's end sets its range's top.
|
||||
auto &slider = *sheet.findChild<QSlider *>(parts[0].toLower() + "Slider");
|
||||
slider.setValue(1000);
|
||||
QCOMPARE(field.text(), parts[3]);
|
||||
}
|
||||
QCOMPARE(names, titles);
|
||||
}
|
||||
@@ -342,7 +356,7 @@ void EffectsPanelTests::cancelAndEscapePutTheEffectBack()
|
||||
editor.session.selectEffect(LayerEffectKind::shadow, editor.layer, true);
|
||||
QCOMPARE(find<QLineEdit>(*editor.sheet(), "distanceField").text(), QString("20"));
|
||||
find<QSlider>(*editor.sheet(), "distanceSlider").setValue(400);
|
||||
QCOMPARE(editor.effects().shadow->distance, 120.0);
|
||||
QCOMPARE(editor.effects().shadow->distance, 40.0);
|
||||
find<QPushButton>(*editor.sheet(), "effectsCancel").click();
|
||||
QVERIFY(editor.effects().shadow->distance == 20 && !editor.session.effectsEditing());
|
||||
// A new overlay's panel: Escape takes the overlay away again.
|
||||
|
||||
@@ -0,0 +1,300 @@
|
||||
#include "Document/LiveLayerMask.h"
|
||||
#include "Rendering/EditorCanvas.h"
|
||||
#include "IO/ImageExporter.h"
|
||||
#include "RenderFixtures.h"
|
||||
#include "SessionFixtures.h"
|
||||
#include <QTemporaryDir>
|
||||
#include <QtTest>
|
||||
|
||||
// A folder's opacity dims each layer inside it (Swift 1.1.6).
|
||||
namespace {
|
||||
ImportedImage filled(int width, int height, QRgb premultiplied, const QString &name)
|
||||
{
|
||||
const QImage image = solid(width, height, premultiplied);
|
||||
return ImportedImage(image, image, name);
|
||||
}
|
||||
|
||||
// The layer wrapped in a folder, the folder left active.
|
||||
QUuid wrap(EditorSession &session, QUuid id)
|
||||
{
|
||||
session.selectLayers({id}, id);
|
||||
session.groupSelectedLayers();
|
||||
return session.activeLayerID().value();
|
||||
}
|
||||
|
||||
void dim(EditorSession &session, QUuid id, double opacity)
|
||||
{
|
||||
session.selectLayer(id);
|
||||
session.setLayerOpacity(opacity);
|
||||
}
|
||||
|
||||
QImage exported(const EditorSession &session)
|
||||
{
|
||||
return ImageExporter::render(session.projectSnapshot().value()).image.convertToFormat(QImage::Format_RGBA8888_Premultiplied);
|
||||
}
|
||||
|
||||
int alpha(const QImage &image, int x, int y)
|
||||
{
|
||||
return image.constScanLine(y)[x * 4 + 3];
|
||||
}
|
||||
|
||||
std::optional<std::pair<double, std::optional<QUuid>>> none(QUuid)
|
||||
{
|
||||
return std::nullopt;
|
||||
}
|
||||
}
|
||||
|
||||
class FolderOpacityTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void opacitiesMultiplyUpTheTree();
|
||||
void theExportAndTheCompositeDimInsideAFolder();
|
||||
void anAdjustmentDimsWithItsFolder();
|
||||
void effectsDimWithTheirFolder();
|
||||
void theCanvasShowsAndFollowsAFoldersOpacity();
|
||||
void theCanvasDimsEveryWayALayerDraws_data();
|
||||
void theCanvasDimsEveryWayALayerDraws();
|
||||
void aClippingSourceBakesDimmed();
|
||||
void aDimmedFolderSavesAndReopens();
|
||||
};
|
||||
|
||||
void FolderOpacityTests::opacitiesMultiplyUpTheTree()
|
||||
{
|
||||
const QUuid inner = QUuid::createUuid(), outer = QUuid::createUuid();
|
||||
const auto tree = [&](QUuid id) -> std::optional<std::pair<double, std::optional<QUuid>>> {
|
||||
if (id == inner)
|
||||
return std::pair(0.5, std::optional(outer));
|
||||
if (id == outer)
|
||||
return std::pair(0.8, std::optional<QUuid>());
|
||||
return std::nullopt;
|
||||
};
|
||||
QCOMPARE(LayerOpacity::effective(0.5, inner, tree), 0.2);
|
||||
QCOMPARE(LayerOpacity::effective(0.5, std::nullopt, tree), 0.5);
|
||||
// A parent nobody knows ends the walk.
|
||||
QCOMPARE(LayerOpacity::effective(0.5, QUuid::createUuid(), none), 0.5);
|
||||
// A loop stops after 64 folders, as Swift's does.
|
||||
const QUuid loop = QUuid::createUuid();
|
||||
const auto ring = [&](QUuid) -> std::optional<std::pair<double, std::optional<QUuid>>> { return std::pair(0.9, std::optional(loop)); };
|
||||
QCOMPARE(LayerOpacity::effective(1, loop, ring), std::pow(0.9, 64));
|
||||
// A document that repeats an id is a bug.
|
||||
CanvasDocument twice(4, 4);
|
||||
twice.layers = {ImageLayer(QStringLiteral("One"), QSizeF(4, 4))};
|
||||
twice.layers.push_back(twice.layers.front());
|
||||
QVERIFY_THROWS_EXCEPTION(std::logic_error, twice.effectiveOpacities());
|
||||
// The document's map and each record's agree.
|
||||
EditorSession session;
|
||||
session.createDocument(4, 4);
|
||||
session.insert(filled(4, 4, qRgba(255, 0, 0, 255), "Red"));
|
||||
const QUuid red = session.activeLayerID().value();
|
||||
dim(session, red, 0.5);
|
||||
const QUuid folder = wrap(session, red);
|
||||
const QUuid top = wrap(session, folder);
|
||||
dim(session, folder, 0.5);
|
||||
dim(session, top, 0.8);
|
||||
const QHash<QUuid, double> opacities = session.document().value().effectiveOpacities();
|
||||
QCOMPARE(opacities.value(red), 0.2);
|
||||
QCOMPARE(opacities.value(folder), 0.4);
|
||||
QCOMPARE(opacities.value(top), 0.8);
|
||||
std::map<QUuid, ProjectLayerRecord> records;
|
||||
const ProjectSnapshot snapshot = session.projectSnapshot().value();
|
||||
for (const ProjectLayerRecord &record : snapshot.manifest.layers)
|
||||
records.emplace(record.id, record);
|
||||
QCOMPARE(records.at(red).effectiveOpacity(records), 0.2);
|
||||
// The layer itself still reads its own opacity.
|
||||
QCOMPARE(layerWith(session, red).opacity, 0.5);
|
||||
}
|
||||
|
||||
void FolderOpacityTests::theExportAndTheCompositeDimInsideAFolder()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(4, 4);
|
||||
session.insert(filled(4, 4, qRgba(255, 0, 0, 255), "Red"));
|
||||
const QUuid red = session.activeLayerID().value();
|
||||
const QUuid folder = wrap(session, red);
|
||||
QCOMPARE(alpha(exported(session), 1, 1), 255);
|
||||
dim(session, folder, 0.5);
|
||||
QCOMPARE(alpha(exported(session), 1, 1), 127);
|
||||
// Nested, each folder multiplies in.
|
||||
const QUuid top = wrap(session, folder);
|
||||
dim(session, top, 0.5);
|
||||
QCOMPARE(alpha(exported(session), 1, 1), 63);
|
||||
QImage composite = BrushRaster::context(4, 4, false);
|
||||
QPainter painter(&composite);
|
||||
session.drawLiveComposite(session.document().value(), painter);
|
||||
painter.end();
|
||||
QCOMPARE(alpha(composite, 1, 1), 63);
|
||||
}
|
||||
|
||||
void FolderOpacityTests::anAdjustmentDimsWithItsFolder()
|
||||
{
|
||||
// Half an adjustment equals a whole one, half folded.
|
||||
const auto scene = [](bool folded) {
|
||||
auto session = std::make_unique<EditorSession>();
|
||||
session->createDocument(4, 4);
|
||||
session->insert(filled(4, 4, qRgba(255, 0, 0, 255), "Red"));
|
||||
session->addAdjustment(AdjustmentKind::gradientMap);
|
||||
session->setAdjustmentEditingID(std::nullopt);
|
||||
const QUuid adjustment = session->activeLayerID().value();
|
||||
if (folded)
|
||||
dim(*session, wrap(*session, adjustment), 0.5);
|
||||
else
|
||||
dim(*session, adjustment, 0.5);
|
||||
QImage composite = BrushRaster::context(4, 4, false);
|
||||
QPainter painter(&composite);
|
||||
session->drawLiveComposite(session->document().value(), painter);
|
||||
painter.end();
|
||||
return std::pair(exported(*session), composite);
|
||||
};
|
||||
const auto half = scene(false), folded = scene(true);
|
||||
QCOMPARE(folded.first, half.first);
|
||||
QCOMPARE(folded.second, half.second);
|
||||
QVERIFY(half.first.pixelColor(1, 1) != QColor(Qt::red) && half.second.pixelColor(1, 1) != QColor(Qt::red));
|
||||
}
|
||||
|
||||
void FolderOpacityTests::effectsDimWithTheirFolder()
|
||||
{
|
||||
// A stroked layer: half alone, or whole, half folded.
|
||||
const auto scene = [](bool folded) {
|
||||
auto session = std::make_unique<EditorSession>();
|
||||
session->createDocument(20, 20);
|
||||
session->insert(filled(10, 10, qRgba(255, 0, 0, 255), "Red"));
|
||||
const QUuid red = session->activeLayerID().value();
|
||||
LayerEffects effects;
|
||||
effects.stroke = StrokeEffect();
|
||||
session->setEffects(effects, red);
|
||||
if (folded)
|
||||
dim(*session, wrap(*session, red), 0.5);
|
||||
else
|
||||
dim(*session, red, 0.5);
|
||||
QImage composite = BrushRaster::context(20, 20, false);
|
||||
QPainter painter(&composite);
|
||||
session->drawLiveComposite(session->document().value(), painter);
|
||||
painter.end();
|
||||
return std::pair(exported(*session), composite);
|
||||
};
|
||||
const auto half = scene(false), folded = scene(true);
|
||||
QCOMPARE(folded.first, half.first);
|
||||
QCOMPARE(folded.second, half.second);
|
||||
QVERIFY(alpha(half.first, 10, 10) == 127 && alpha(half.first, 4, 10) > 0);
|
||||
}
|
||||
|
||||
void FolderOpacityTests::theCanvasShowsAndFollowsAFoldersOpacity()
|
||||
{
|
||||
EditorSession session;
|
||||
CanvasView canvas(session);
|
||||
session.createDocument(20, 20);
|
||||
session.setShowsTransformControls(false);
|
||||
canvas.resize(40, 40);
|
||||
session.viewport.resize(QSizeF(40, 40), 1, QSizeF(20, 20));
|
||||
session.zoom(1);
|
||||
// White below, so canvas and export both show opaque.
|
||||
session.insert(filled(20, 20, qRgba(255, 255, 255, 255), "White"));
|
||||
session.insert(filled(20, 20, qRgba(255, 0, 0, 255), "Red"));
|
||||
const QUuid folder = wrap(session, session.activeLayerID().value());
|
||||
canvas.synchronizeDisplay();
|
||||
const QPoint middle = session.viewport.viewPoint(QPointF(10, 10), QSizeF(20, 20)).toPoint();
|
||||
QCOMPARE(canvas.grab().toImage().pixelColor(middle), QColor(Qt::red));
|
||||
// A folder holds no pixels, yet its opacity redraws.
|
||||
dim(session, folder, 0.5);
|
||||
QVERIFY(canvas.synchronizeDisplay());
|
||||
const QColor shown = canvas.grab().toImage().pixelColor(middle);
|
||||
const QColor expected = exported(session).pixelColor(10, 10);
|
||||
QVERIFY2(std::abs(shown.green() - expected.green()) <= 1 && std::abs(expected.green() - 128) <= 1 && shown.red() == 255,
|
||||
qPrintable(shown.name() + expected.name()));
|
||||
}
|
||||
|
||||
void FolderOpacityTests::aClippingSourceBakesDimmed()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(4, 4);
|
||||
session.insert(filled(4, 4, qRgba(255, 0, 0, 255), "Base"));
|
||||
const QUuid base = session.activeLayerID().value();
|
||||
session.insert(filled(4, 4, qRgba(0, 255, 0, 255), "Clipped"));
|
||||
const QUuid clipped = session.activeLayerID().value();
|
||||
session.toggleClippingMask(clipped);
|
||||
session.selectLayers({base, clipped}, clipped);
|
||||
session.groupSelectedLayers();
|
||||
const QUuid folder = session.activeLayerID().value();
|
||||
const QImage whole = LiveMaskBaker::bake(session.projectSnapshot().value(), clipped).value().image();
|
||||
QCOMPARE(qAlpha(whole.pixel(1, 1)), 255);
|
||||
dim(session, folder, 0.5);
|
||||
const QImage dimmed = LiveMaskBaker::bake(session.projectSnapshot().value(), clipped).value().image();
|
||||
QCOMPARE(qAlpha(dimmed.pixel(1, 1)), 127);
|
||||
// A folder around the folder dims the source again.
|
||||
dim(session, wrap(session, folder), 0.5);
|
||||
const QImage twice = LiveMaskBaker::bake(session.projectSnapshot().value(), clipped).value().image();
|
||||
QCOMPARE(qAlpha(twice.pixel(1, 1)), 63);
|
||||
}
|
||||
|
||||
void FolderOpacityTests::aDimmedFolderSavesAndReopens()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
EditorSession session;
|
||||
session.createDocument(4, 4);
|
||||
session.insert(filled(4, 4, qRgba(255, 0, 0, 255), "Red"));
|
||||
const QUuid folder = wrap(session, session.activeLayerID().value());
|
||||
dim(session, folder, 0.5);
|
||||
QCOMPARE(layerWith(session, folder).opacity, 0.5);
|
||||
const QString path = root.filePath("Dimmed.comp");
|
||||
ProjectStore::save(session.projectSnapshot().value(), path);
|
||||
const ProjectSnapshot loaded = ProjectStore::load(path);
|
||||
QCOMPARE(loaded.manifest.version, qint64(8));
|
||||
for (const ProjectLayerRecord &record : loaded.manifest.layers) {
|
||||
if (record.isGroup == true)
|
||||
QCOMPARE(record.opacity, std::optional(0.5));
|
||||
}
|
||||
}
|
||||
|
||||
void FolderOpacityTests::theCanvasDimsEveryWayALayerDraws_data()
|
||||
{
|
||||
QTest::addColumn<QString>("way");
|
||||
QTest::newRow("adjustment") << "adjustment";
|
||||
QTest::newRow("effects") << "effects";
|
||||
QTest::newRow("warp") << "warp";
|
||||
}
|
||||
|
||||
void FolderOpacityTests::theCanvasDimsEveryWayALayerDraws()
|
||||
{
|
||||
QFETCH(QString, way);
|
||||
// The canvas, half alone against whole in a half folder.
|
||||
const auto scene = [&way](bool folded) {
|
||||
EditorSession session;
|
||||
CanvasView canvas(session);
|
||||
session.createDocument(20, 20);
|
||||
session.setShowsTransformControls(false);
|
||||
canvas.resize(40, 40);
|
||||
session.viewport.resize(QSizeF(40, 40), 1, QSizeF(20, 20));
|
||||
session.zoom(1);
|
||||
session.insert(filled(20, 20, qRgba(255, 255, 255, 255), "White"));
|
||||
session.insert(filled(10, 10, qRgba(255, 0, 0, 255), "Red"));
|
||||
QUuid target = session.activeLayerID().value();
|
||||
if (way == "adjustment") {
|
||||
session.addAdjustment(AdjustmentKind::gradientMap);
|
||||
session.setAdjustmentEditingID(std::nullopt);
|
||||
target = session.activeLayerID().value();
|
||||
} else if (way == "effects") {
|
||||
LayerEffects effects;
|
||||
effects.stroke = StrokeEffect();
|
||||
session.setEffects(effects, target);
|
||||
}
|
||||
dim(session, folded ? wrap(session, target) : target, 0.5);
|
||||
if (way == "warp") {
|
||||
session.selectLayer(target);
|
||||
session.selectTool(NavigationTool::blur);
|
||||
session.beginBrush(QPointF(10, 10));
|
||||
if (!session.warpStroke())
|
||||
throw std::runtime_error("no warp began");
|
||||
}
|
||||
canvas.synchronizeDisplay();
|
||||
if (way == "effects" && !QTest::qWaitFor([&] { canvas.grab(); return session.effectsPreviews.rendered(target).has_value(); }))
|
||||
throw std::runtime_error("the effects never rendered");
|
||||
return canvas.grab().toImage();
|
||||
};
|
||||
const QImage half = scene(false), folded = scene(true);
|
||||
QCOMPARE(folded, half);
|
||||
const QPoint inside(20, 20);
|
||||
QVERIFY2(half.pixelColor(inside) != QColor(Qt::red) && half.pixelColor(inside) != QColor(Qt::white), qPrintable(half.pixelColor(inside).name()));
|
||||
}
|
||||
|
||||
QTEST_MAIN(FolderOpacityTests)
|
||||
#include "FolderOpacityTests.moc"
|
||||
@@ -217,7 +217,7 @@ void GroupSessionTests::groupsRoundTripAndSurviveImageAndCanvasResize()
|
||||
session.addBlankLayer();
|
||||
const QUuid child = session.activeLayerID().value();
|
||||
const ProjectSnapshot snapshot = session.projectSnapshot().value();
|
||||
QCOMPARE(snapshot.manifest.version, qint64(7));
|
||||
QCOMPARE(snapshot.manifest.version, qint64(8));
|
||||
const auto record = [](const ProjectSnapshot &from, QUuid id) {
|
||||
for (const ProjectLayerRecord &layer : from.manifest.layers) {
|
||||
if (layer.id == id)
|
||||
|
||||
@@ -261,7 +261,7 @@ void GroupTests::projectErrorsCarryTheirMessages()
|
||||
const ProjectError version = ProjectError::unsupportedVersion(9);
|
||||
QCOMPARE(version.kind, ProjectError::Kind::version);
|
||||
QCOMPARE(version.version, std::optional<int>(9));
|
||||
QCOMPARE(QString(version.what()), QString("This project uses format version 9. This app supports versions 1–7."));
|
||||
QCOMPARE(QString(version.what()), QString("This project uses format version 9. This app supports versions 1–8."));
|
||||
QVERIFY_THROWS_EXCEPTION(std::logic_error, ProjectError(ProjectError::Kind::version));
|
||||
|
||||
const ProjectError missingImage(ProjectError::Kind::missingImage);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
#include "UI/LayerAppearanceControls.h"
|
||||
#include "UI/BlendModePicker.h"
|
||||
#include <QLabel>
|
||||
#include <QtTest>
|
||||
|
||||
// The opacity slider and field, one undo step a drag.
|
||||
@@ -52,6 +53,21 @@ void LayerAppearanceControlsTests::theControlsFollowTheActiveLayerAndItsGate()
|
||||
QVERIFY(controls.widget.findChild<BlendModePicker *>() != nullptr);
|
||||
controls.session.selectLayers({controls.session.document().value().layers.front().id, controls.session.document().value().layers.back().id}, std::nullopt);
|
||||
QVERIFY(!controls.widget.isEnabled());
|
||||
// A folder: its opacity edits, its blend mode rests.
|
||||
controls.session.addGroup();
|
||||
QVERIFY(controls.widget.isEnabled() && controls.slider.isEnabled() && controls.field.isEnabled());
|
||||
const auto blendRow = [&] {
|
||||
for (QLabel *label : controls.widget.findChildren<QLabel *>()) {
|
||||
if (label->text() == "Blend")
|
||||
return label;
|
||||
}
|
||||
throw std::runtime_error("no Blend caption");
|
||||
};
|
||||
QVERIFY(!controls.widget.findChild<BlendModePicker *>()->isEnabled() && !blendRow()->isEnabled());
|
||||
controls.slider.setValue(500);
|
||||
QCOMPARE(controls.opacity(), 0.5);
|
||||
controls.session.selectLayer(controls.session.document().value().layers.front().id);
|
||||
QVERIFY(controls.widget.findChild<BlendModePicker *>()->isEnabled() && blendRow()->isEnabled());
|
||||
}
|
||||
|
||||
void LayerAppearanceControlsTests::aSliderDragIsOneUndoStep()
|
||||
|
||||
@@ -119,7 +119,7 @@ void LayerAppearanceTests::appearancePersistsThroughSaveResizeAndTransparentExpo
|
||||
const QString path = folder.filePath("Appearance.comp");
|
||||
ProjectStore::save(snapshot, path);
|
||||
const ProjectSnapshot loaded = ProjectStore::load(path);
|
||||
QCOMPARE(loaded.manifest.version, qint64(7));
|
||||
QCOMPARE(loaded.manifest.version, qint64(8));
|
||||
const ProjectSnapshot resized = ImageResizer::resize(loaded, {.width = 8, .height = 8, .resolution = 72});
|
||||
const ProjectSnapshot canvas = CanvasResizer::resize(resized, {.width = 12, .height = 12});
|
||||
const auto record = std::find_if(canvas.manifest.layers.begin(), canvas.manifest.layers.end(),
|
||||
@@ -253,7 +253,8 @@ void LayerAppearanceTests::selectedLayersTakeOneOpacityInOneStep()
|
||||
session.setSelectedLayersOpacity(0.5);
|
||||
QCOMPARE(layerWith(session, first).opacity, 0.5);
|
||||
QCOMPARE(layerWith(session, second).opacity, 0.5);
|
||||
QCOMPARE(layerWith(session, folder).opacity, 1.0);
|
||||
// A selected folder takes it too.
|
||||
QCOMPARE(layerWith(session, folder).opacity, 0.5);
|
||||
QCOMPARE(layerWith(session, apart).opacity, 1.0);
|
||||
QCOMPARE(session.history.undoCount(), count + 1);
|
||||
QCOMPARE(session.history.undoName(), QString("Layer Opacity"));
|
||||
@@ -326,13 +327,27 @@ void LayerAppearanceTests::onlyOnePixelLayerHasAnAppearanceToEdit()
|
||||
QVERIFY(session.canEditAppearance());
|
||||
session.addGroup();
|
||||
const QUuid folder = session.activeLayerID().value();
|
||||
// A folder takes an opacity; blending stays per layer.
|
||||
QVERIFY(!session.canEditAppearance());
|
||||
QVERIFY(session.canEditOpacity());
|
||||
session.setLayerBlendMode(LayerBlendMode::screen);
|
||||
session.setLayerOpacity(0.5);
|
||||
session.previewBlendMode(LayerBlendMode::screen, folder);
|
||||
QVERIFY(!session.blendPreview().has_value());
|
||||
QCOMPARE(layerWith(session, folder).blendMode, LayerBlendMode::normal);
|
||||
QCOMPARE(layerWith(session, folder).opacity, 1.0);
|
||||
session.setLayerOpacity(0.5);
|
||||
QCOMPARE(layerWith(session, folder).opacity, 0.5);
|
||||
QCOMPARE(session.history.undoName(), QString("Layer Opacity"));
|
||||
session.beginOpacityEdit();
|
||||
session.setLayerOpacity(0.25);
|
||||
session.setLayerOpacity(0.2);
|
||||
session.finishOpacityEdit();
|
||||
QCOMPARE(layerWith(session, folder).opacity, 0.2);
|
||||
session.undo();
|
||||
QCOMPARE(layerWith(session, folder).opacity, 0.5);
|
||||
// Two layers selected: neither gate holds.
|
||||
session.selectLayers({folder, session.document().value().layers.front().id}, folder);
|
||||
QVERIFY(!session.canEditOpacity());
|
||||
session.selectLayer(folder);
|
||||
session.addBlankLayer();
|
||||
QVERIFY(session.canEditAppearance());
|
||||
session.setShowsImporter(true);
|
||||
@@ -441,10 +456,9 @@ void LayerAppearanceTests::moveToolNumberKeysSetSelectedLayersOpacityAsOneUndo()
|
||||
const int count = session.history.undoCount();
|
||||
session.typeOpacityDigit(5, 10);
|
||||
for (const ImageLayer &layer : session.document().value().layers) {
|
||||
if (layer.id == first || layer.id == second)
|
||||
// The folder takes it too, since 1.1.6.
|
||||
if (layer.id == first || layer.id == second || layer.id == folder)
|
||||
QCOMPARE(layer.opacity, 0.5);
|
||||
if (layer.id == folder)
|
||||
QCOMPARE(layer.opacity, 1.0);
|
||||
}
|
||||
QCOMPARE(session.history.undoCount(), count + 1);
|
||||
}
|
||||
|
||||
@@ -65,13 +65,15 @@ void LayerDragGestureTests::altShowsTheClippingAndDuplicateCursors()
|
||||
QVERIFY(image(CanvasView::loadSelectionCursor(1)).pixelColor(11, 8).lightness() > 128);
|
||||
QVERIFY(image(CanvasView::loadSelectionCursor(1)).pixelColor(21, 18).alpha() > 0);
|
||||
QCOMPARE(image(CanvasView::loadSelectionCursor(1)).pixelColor(21, 21).alpha(), 0);
|
||||
// The bottom row clips to nothing; a folder duplicates not.
|
||||
// The bottom row clips nothing; a folder has no strip.
|
||||
const QPoint lastStrip = shown.list.cells().at(1)->mapTo(&shown.list, QPoint(200, LayerCell::rowHeight - 4));
|
||||
QCOMPARE(shown.list.cursorFor(lastStrip, Qt::AltModifier).shape(), Qt::ArrowCursor);
|
||||
session->selectLayer(shown.id(1));
|
||||
session->addGroup();
|
||||
const QPoint folderBody = shown.list.cells().at(1)->mapTo(&shown.list, QPoint(200, 20));
|
||||
QCOMPARE(shown.list.cursorFor(folderBody, Qt::AltModifier).shape(), Qt::ArrowCursor);
|
||||
QCOMPARE(image(shown.list.cursorFor(folderBody, Qt::AltModifier)), image(CanvasView::duplicateCursor(ratio)));
|
||||
const QPoint folderStrip = shown.list.cells().at(1)->mapTo(&shown.list, QPoint(200, LayerCell::rowHeight - 4));
|
||||
QCOMPARE(image(shown.list.cursorFor(folderStrip, Qt::AltModifier)), image(CanvasView::duplicateCursor(ratio)));
|
||||
// A mask thumbnail copies, or loads with Ctrl; busy, nothing.
|
||||
session->selectLayer(shown.id(0));
|
||||
session->addLayerMask();
|
||||
|
||||
@@ -125,11 +125,25 @@ void LayerDragTests::anAltDragDropsDuplicates()
|
||||
QCOMPARE(session->history.undoName(), QString("Duplicate Layers"));
|
||||
session->undo();
|
||||
QCOMPARE(shown.names().size(), size_t(3));
|
||||
// A folder among the dragged rows: no copy at all.
|
||||
session->selectLayer(shown.id(2));
|
||||
// A folder among the dragged rows copies with its contents.
|
||||
const QUuid held = shown.id(2);
|
||||
session->selectLayer(held);
|
||||
session->addGroup();
|
||||
QVERIFY(!shown.drop(shown.rows({0, 2}), Qt::CopyAction, shown.below()));
|
||||
QVERIFY(session->placeLayer(held, session->activeLayerID().value()));
|
||||
QCOMPARE(shown.names().size(), size_t(4));
|
||||
QVERIFY(session->document().value().layers[size_t(indexOf(session->document().value().layers, shown.id(2)))].isGroup);
|
||||
QVERIFY(shown.drop(shown.rows({0, 2}), Qt::CopyAction, shown.below()));
|
||||
QCOMPARE(session->history.undoName(), QString("Duplicate Layers"));
|
||||
const std::vector<ImageLayer> &layers = session->document().value().layers;
|
||||
QCOMPARE(int(layers.size()), 7);
|
||||
std::vector<QUuid> folders;
|
||||
for (const ImageLayer &layer : layers) {
|
||||
if (layer.isGroup)
|
||||
folders.push_back(layer.id);
|
||||
}
|
||||
QCOMPARE(int(folders.size()), 2);
|
||||
for (const QUuid &folder : folders)
|
||||
QCOMPARE(int(session->descendantIDs(folder).size()), 1);
|
||||
}
|
||||
|
||||
void LayerDragTests::aMaskDragCopiesTheMaskOntoTheRowUnderIt()
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
#include "Document/EditorSession.h"
|
||||
#include "SessionFixtures.h"
|
||||
#include <QtTest>
|
||||
|
||||
// Duplicating layers and folders in the session.
|
||||
class LayerDuplicateTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void duplicatingALayerByDraggingPlacesTheCopyAsOneStep();
|
||||
void aFolderCopyKeepsLinksInsideAndStopsAtTheLimit();
|
||||
};
|
||||
|
||||
void LayerDuplicateTests::duplicatingALayerByDraggingPlacesTheCopyAsOneStep()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(800, 600);
|
||||
for (int each = 0; each < 3; ++each)
|
||||
session.addBlankLayer();
|
||||
const std::vector<LayerHierarchy::Entry> rows = session.layerRows();
|
||||
const ProjectLayerRecord top = rows.front().layer, bottom = rows.back().layer;
|
||||
const std::optional<CanvasDocument> before = session.document();
|
||||
const int count = session.history.undoCount();
|
||||
QVERIFY(session.duplicateLayer(bottom.id, std::nullopt, top.id));
|
||||
const std::vector<LayerHierarchy::Entry> after = session.layerRows();
|
||||
QCOMPARE(after.size(), size_t(4));
|
||||
QCOMPARE(session.history.undoCount(), count + 1);
|
||||
QCOMPARE(session.history.undoName(), QString("Duplicate Layer"));
|
||||
QCOMPARE(after.front().layer.name, bottom.name + " copy");
|
||||
QCOMPARE(after.front().layer.id, session.activeLayerID());
|
||||
QVERIFY(indexOf(session.document().value().layers, bottom.id) >= 0);
|
||||
session.undo();
|
||||
QCOMPARE(session.document(), before);
|
||||
session.addGroup();
|
||||
const QUuid folder = session.activeLayerID().value();
|
||||
// A stranger or a place inside itself is refused.
|
||||
QVERIFY(!session.duplicateLayer(QUuid::createUuid(), std::nullopt));
|
||||
QVERIFY(!session.duplicateLayer(bottom.id, bottom.id));
|
||||
QVERIFY(!session.duplicateLayer(folder, folder));
|
||||
QCOMPARE(session.layerRows().size(), size_t(4));
|
||||
// Into a folder, at its top.
|
||||
QVERIFY(session.duplicateLayer(bottom.id, folder));
|
||||
const QUuid inside = session.activeLayerID().value();
|
||||
QCOMPARE(layerWith(session, inside).parentID, std::optional(folder));
|
||||
session.setIsImporting(true);
|
||||
QVERIFY(!session.duplicateLayer(bottom.id, std::nullopt));
|
||||
session.setIsImporting(false);
|
||||
// A folder copies with what it holds, at the bottom.
|
||||
session.toggleGroupExpansion(folder);
|
||||
const int layers = int(session.document().value().layers.size());
|
||||
QVERIFY(session.duplicateLayer(folder, std::nullopt, std::nullopt, true));
|
||||
const ImageLayer copy = layerWith(session, session.activeLayerID().value());
|
||||
QVERIFY(copy.isGroup && copy.id != folder && !copy.parentID);
|
||||
QCOMPARE(copy.name, layerWith(session, folder).name + " copy");
|
||||
const QSet<QUuid> held = session.descendantIDs(copy.id);
|
||||
QCOMPARE(int(held.size()), 1);
|
||||
const ImageLayer child = layerWith(session, *held.begin());
|
||||
// The child keeps its name and moves to the copy.
|
||||
QVERIFY(child.id != inside && child.name == layerWith(session, inside).name && child.parentID == copy.id);
|
||||
QVERIFY(session.collapsedGroupIDs().contains(copy.id));
|
||||
QCOMPARE(int(session.document().value().layers.size()), layers + 2);
|
||||
QCOMPARE(session.layerRows().back().layer.id, copy.id);
|
||||
session.undo();
|
||||
QCOMPARE(int(session.document().value().layers.size()), layers);
|
||||
// The copy sits right above its source.
|
||||
session.selectLayer(top.id);
|
||||
session.duplicateActiveLayer();
|
||||
const int original = indexOf(session.document().value().layers, top.id);
|
||||
QCOMPARE(session.document().value().layers[original + 1].name, top.name + " copy");
|
||||
QCOMPARE(session.activeLayerID(), std::optional(session.document().value().layers[original + 1].id));
|
||||
}
|
||||
|
||||
void LayerDuplicateTests::aFolderCopyKeepsLinksInsideAndStopsAtTheLimit()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(40, 40, true);
|
||||
const QUuid base = session.activeLayerID().value();
|
||||
QImage white(10, 10, QImage::Format_RGBA8888_Premultiplied);
|
||||
white.fill(Qt::white);
|
||||
session.insert(ImportedImage(white, white, QStringLiteral("White")));
|
||||
const QUuid clipped = session.activeLayerID().value();
|
||||
QVERIFY(session.canToggleClippingMask(clipped));
|
||||
session.toggleClippingMask(clipped);
|
||||
session.selectLayers({base, clipped}, clipped);
|
||||
session.groupSelectedLayers();
|
||||
const QUuid folder = session.activeLayerID().value();
|
||||
session.duplicateActiveLayer();
|
||||
const QUuid copy = session.activeLayerID().value();
|
||||
QVERIFY(copy != folder);
|
||||
// Links inside the copy point inside the copy.
|
||||
std::optional<QUuid> copiedBase, copiedClip;
|
||||
for (const QUuid &id : session.descendantIDs(copy)) {
|
||||
const ImageLayer layer = layerWith(session, id);
|
||||
(layer.maskSourceID ? copiedClip : copiedBase) = id;
|
||||
}
|
||||
QCOMPARE(layerWith(session, copiedClip.value()).maskSourceID, copiedBase);
|
||||
QVERIFY(copiedBase != base);
|
||||
QCOMPARE(layerWith(session, clipped).maskSourceID, std::optional(base));
|
||||
// A copy alone keeps links and a parent outside it.
|
||||
session.selectLayer(clipped);
|
||||
session.duplicateActiveLayer();
|
||||
const ImageLayer lone = layerWith(session, session.activeLayerID().value());
|
||||
QVERIFY(lone.id != clipped && lone.maskSourceID == base && lone.parentID == folder);
|
||||
session.undo();
|
||||
// Past 10,000 layers nothing is copied or recorded.
|
||||
ProjectSnapshot snapshot = session.projectSnapshot().value();
|
||||
// Three copies into 9,997 reach 10,000 exactly: allowed.
|
||||
while (snapshot.manifest.layers.size() < 9'997)
|
||||
snapshot.manifest.layers.push_back(ProjectLayerRecord{.id = QUuid::createUuid(), .name = "Blank", .isVisible = true,
|
||||
.transform = {.origin = {0, 0}, .size = {40, 40}}, .imageFile = std::nullopt});
|
||||
session.installProject(snapshot, QString());
|
||||
session.selectLayer(folder);
|
||||
session.duplicateActiveLayer();
|
||||
QVERIFY(session.activeLayerID() != folder);
|
||||
QCOMPARE(int(session.document().value().layers.size()), 10'000);
|
||||
// Past it nothing is copied or recorded.
|
||||
session.selectLayer(folder);
|
||||
const int steps = session.history.undoCount();
|
||||
session.duplicateActiveLayer();
|
||||
QCOMPARE(session.activeLayerID(), std::optional(folder));
|
||||
QCOMPARE(int(session.document().value().layers.size()), 10'000);
|
||||
QCOMPARE(session.history.undoCount(), steps);
|
||||
QVERIFY(!session.duplicateLayer(folder, std::nullopt, std::nullopt, true));
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(LayerDuplicateTests)
|
||||
#include "LayerDuplicateTests.moc"
|
||||
@@ -160,7 +160,7 @@ void LayerMaskSessionTests::projectAndPNGPreserveCoverageAndDisabledState()
|
||||
rewrite(*session, [&](ProjectSnapshot &snapshot) { record(snapshot, id).maskEnabled = enabled; });
|
||||
ProjectStore::save(session->projectSnapshot().value(), path);
|
||||
const ProjectSnapshot loaded = ProjectStore::load(path);
|
||||
QCOMPARE(loaded.manifest.version, qint64(7));
|
||||
QCOMPARE(loaded.manifest.version, qint64(8));
|
||||
session->installProject(loaded, path);
|
||||
QCOMPARE(session->activeLayer().value().mask.value().isEnabled, enabled);
|
||||
const QImage image = QImage::fromData(ImageExporter::pngData(loaded), "PNG");
|
||||
@@ -240,7 +240,7 @@ void LayerMaskSessionTests::folderMasksSaveResizeAndNeedTheNewFormat()
|
||||
const ProjectSnapshot snapshot = session->projectSnapshot().value();
|
||||
ProjectStore::save(snapshot, path);
|
||||
const ProjectSnapshot loaded = ProjectStore::load(path);
|
||||
QVERIFY(loaded.manifest.version == 7 && loaded.masks.count(folder) == 1);
|
||||
QVERIFY(loaded.manifest.version == 8 && loaded.masks.count(folder) == 1);
|
||||
QCOMPARE(alphas(ImageExporter::render(loaded).image), (QList<int>{255, 0, 128, 255}));
|
||||
const ProjectSnapshot resized = ImageResizer::resize(loaded, {.width = 4, .height = 4, .resolution = 72, .sampling = LayerSampling::nearest});
|
||||
QCOMPARE(alphas(ImageExporter::render(resized).image),
|
||||
|
||||
@@ -350,6 +350,9 @@ void NativeLayerListTests::layerListToolKeysAndTransformNudge()
|
||||
shown.list.setFocus();
|
||||
QTest::keyClick(&shown.list, Qt::Key_H);
|
||||
QCOMPARE(session.tool(), NavigationTool::hand);
|
||||
// T chooses Type, since Swift 1.1.6.
|
||||
QTest::keyClick(&shown.list, Qt::Key_T);
|
||||
QCOMPARE(session.tool(), NavigationTool::type);
|
||||
QTest::keyClick(&shown.list, Qt::Key_V);
|
||||
QCOMPARE(session.tool(), NavigationTool::move);
|
||||
session.beginTransform();
|
||||
|
||||
@@ -309,10 +309,10 @@ void ProjectManifestTests::validationFollowsTheFormatsVersions_data()
|
||||
QTest::addColumn<std::optional<ProjectError::Kind>>("refused");
|
||||
const auto invalid = std::optional(ProjectError::Kind::invalid), tooLarge = std::optional(ProjectError::Kind::tooLarge);
|
||||
const std::optional<ProjectError::Kind> fine;
|
||||
for (int version = 1; version <= 7; ++version)
|
||||
for (int version = 1; version <= 8; ++version)
|
||||
QTest::addRow("plain at version %d", version) << "" << version << fine;
|
||||
QTest::newRow("version 0") << "" << 0 << std::optional(ProjectError::Kind::version);
|
||||
QTest::newRow("version 8") << "" << 8 << std::optional(ProjectError::Kind::version);
|
||||
QTest::newRow("version 9") << "" << 9 << std::optional(ProjectError::Kind::version);
|
||||
QTest::newRow("another format") << "format" << 7 << invalid;
|
||||
QTest::newRow("another colour space") << "colorSpace" << 7 << invalid;
|
||||
QTest::newRow("resolution 0.5") << "resolution=0.5" << 7 << invalid;
|
||||
@@ -349,7 +349,9 @@ void ProjectManifestTests::validationFollowsTheFormatsVersions_data()
|
||||
QTest::newRow("an empty folder at version 1") << "folderEmpty" << 1 << invalid;
|
||||
QTest::newRow("an empty folder at version 2") << "folderEmpty" << 2 << fine;
|
||||
QTest::newRow("a folder that blends") << "folderBlend" << 7 << invalid;
|
||||
QTest::newRow("a folder that fades") << "folderOpacity" << 7 << invalid;
|
||||
QTest::newRow("a folder that fades before version 8") << "folderOpacity" << 7 << invalid;
|
||||
QTest::newRow("a folder that fades at version 8") << "folderOpacity" << 8 << fine;
|
||||
QTest::newRow("a folder that blends at version 8") << "folderBlend" << 8 << invalid;
|
||||
QTest::newRow("a folder with pixels") << "folderPixels" << 7 << invalid;
|
||||
QTest::newRow("a folder mask before version 6") << "folderMask" << 5 << invalid;
|
||||
QTest::newRow("a folder mask at version 6") << "folderMask" << 6 << fine;
|
||||
|
||||
@@ -122,7 +122,7 @@ void ProjectSessionTests::aSnapshotGivesBackWhatWasInstalled()
|
||||
const ProjectSnapshot again = session.projectSnapshot().value();
|
||||
// The same manifest to the byte; pixels by identity.
|
||||
QCOMPARE(again.manifest.encoded(), snapshot.manifest.encoded());
|
||||
QCOMPARE(again.manifest.version, qint64(7));
|
||||
QCOMPARE(again.manifest.version, qint64(8));
|
||||
QCOMPARE(int(again.images.size()), 2);
|
||||
QCOMPARE(int(again.masks.size()), 2);
|
||||
for (const auto &[id, image] : snapshot.images)
|
||||
|
||||
@@ -167,7 +167,7 @@ void ProjectTests::unsupportedCorruptAndUnsafeMetadataAreRejected()
|
||||
};
|
||||
// A version out of range is named whatever else fails.
|
||||
const QByteArray future = R"({"format": "com.compositor.project", "version": 42, "layers": "from the future"})";
|
||||
for (const qint64 strange : {qint64(42), qint64(8), qint64(0)}) {
|
||||
for (const qint64 strange : {qint64(42), qint64(9), qint64(0)}) {
|
||||
try {
|
||||
overwrite(metadata, QByteArray(future).replace("42", QByteArray::number(strange)));
|
||||
ProjectStore::load(path);
|
||||
@@ -182,12 +182,12 @@ void ProjectTests::unsupportedCorruptAndUnsafeMetadataAreRejected()
|
||||
QCOMPARE(loading(QByteArray(future).replace("42", "1")), std::optional(ProjectError::Kind::invalid));
|
||||
// Another format is invalid whatever version it claims.
|
||||
QCOMPARE(loading(QByteArray(future).replace("compositor", "example")), std::optional(ProjectError::Kind::invalid));
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"version\": 7", "\"version\": 0")), std::optional(ProjectError::Kind::version));
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"version\": 8", "\"version\": 0")), std::optional(ProjectError::Kind::version));
|
||||
// Swift's Int holds 64 bits, each one: no double between.
|
||||
const qint64 most = std::numeric_limits<qint64>::max(), least = std::numeric_limits<qint64>::min();
|
||||
for (const qint64 wild : {qint64(2147483648), qint64(-2147483648), qint64(-1), qint64(9007199254740993), most, least}) {
|
||||
try {
|
||||
overwrite(metadata, QByteArray(good).replace("\"version\": 7", "\"version\": " + QByteArray::number(wild)));
|
||||
overwrite(metadata, QByteArray(good).replace("\"version\": 8", "\"version\": " + QByteArray::number(wild)));
|
||||
ProjectStore::load(path);
|
||||
QFAIL("a wild version opened");
|
||||
} catch (const ProjectError &error) {
|
||||
@@ -197,19 +197,19 @@ void ProjectTests::unsupportedCorruptAndUnsafeMetadataAreRejected()
|
||||
}
|
||||
}
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"width\": 100", "\"width\": 3000000000")), std::optional(ProjectError::Kind::tooLarge));
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"version\": 7", "\"version\": 9223372036854775808")), std::optional(ProjectError::Kind::invalid));
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"version\": 8", "\"version\": 9223372036854775808")), std::optional(ProjectError::Kind::invalid));
|
||||
// Declared: Qt rounds a token just past Int64's minimum.
|
||||
try {
|
||||
overwrite(metadata, QByteArray(good).replace("\"version\": 7", "\"version\": -9223372036854775809"));
|
||||
overwrite(metadata, QByteArray(good).replace("\"version\": 8", "\"version\": -9223372036854775809"));
|
||||
ProjectStore::load(path);
|
||||
QFAIL("a version below every integer opened");
|
||||
} catch (const ProjectError &error) {
|
||||
QCOMPARE(error.version, std::optional(least));
|
||||
}
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"version\": 7", "\"version\": 1e19")), std::optional(ProjectError::Kind::invalid));
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"version\": 7", "\"version\": 7.5")), std::optional(ProjectError::Kind::invalid));
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"version\": 7", "\"version\": 1e300")), std::optional(ProjectError::Kind::invalid));
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"version\": 7", "\"version\": \"7\"")), std::optional(ProjectError::Kind::invalid));
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"version\": 8", "\"version\": 1e19")), std::optional(ProjectError::Kind::invalid));
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"version\": 8", "\"version\": 7.5")), std::optional(ProjectError::Kind::invalid));
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"version\": 8", "\"version\": 1e300")), std::optional(ProjectError::Kind::invalid));
|
||||
QCOMPARE(loading(QByteArray(good).replace("\"version\": 8", "\"version\": \"7\"")), std::optional(ProjectError::Kind::invalid));
|
||||
QCOMPARE(loading(QByteArray(good).replace("com.compositor.project", "com.example.project")), std::optional(ProjectError::Kind::invalid));
|
||||
QCOMPARE(loading("not json"), std::optional(ProjectError::Kind::invalid));
|
||||
QCOMPARE(loading("[1, 2]"), std::optional(ProjectError::Kind::invalid));
|
||||
|
||||
@@ -168,9 +168,12 @@ void TransformOverlayTests::handlesAndGuidesArePaintedWhereTheyBelong()
|
||||
QCOMPARE(shot.pixelColor(190, 140), QColor(Qt::white));
|
||||
QCOMPARE(shot.pixelColor(210, 160), QColor(Qt::white));
|
||||
QCOMPARE(shot.pixelColor(200, 112), QColor(Qt::white));
|
||||
// The accent outline runs between the handles, over the red.
|
||||
QVERIFY(shot.pixelColor(190, 145) != QColor(Qt::red));
|
||||
QVERIFY(shot.pixelColor(190, 145).blue() > shot.pixelColor(190, 145).red());
|
||||
// The accent alone runs between handles, half over red.
|
||||
const QColor accent = canvas.palette().color(QPalette::Highlight);
|
||||
const QColor edge = shot.pixelColor(190, 145);
|
||||
QVERIFY2(std::abs(edge.red() - (accent.red() + 255) / 2) <= 2 && std::abs(edge.green() - accent.green() / 2) <= 2
|
||||
&& std::abs(edge.blue() - accent.blue() / 2) <= 2,
|
||||
qPrintable(edge.name()));
|
||||
session.setShowsTransformControls(false);
|
||||
QVERIFY(overlay.drawnRect(canvas.rect()).isEmpty());
|
||||
shot = canvas.grab().toImage();
|
||||
|
||||
@@ -73,6 +73,18 @@ void TransformSnapTests::snappedMoveNudgesWithinToleranceAndKeepsTheGuides()
|
||||
const LayerTransform near{.origin = {77, 20}, .size = {20, 10}};
|
||||
QCOMPARE(session.snappedMove(near, {a}, 2).origin, QPointF(75, 20));
|
||||
QCOMPARE(session.snappedMove(near, {a, b}, 2).origin, QPointF(77, 20));
|
||||
// View > Snap off: every draft stays put, guides gone.
|
||||
const LayerTransform snapped = session.snappedMove(draft, {a}, 5);
|
||||
QVERIFY(snapped != draft && !session.snapGuides.xs.empty());
|
||||
QSignalSpy changes(&session, &EditorSession::changed);
|
||||
session.setSnappingEnabled(false);
|
||||
QVERIFY(!session.snappingEnabled() && changes.count() == 1);
|
||||
QCOMPARE(session.snapGuides, SnapGuides{});
|
||||
session.snapGuides = SnapGuides{{1}, {2}};
|
||||
QCOMPARE(session.snappedMove(draft, {a}, 5), draft);
|
||||
QCOMPARE(session.snapGuides, SnapGuides{});
|
||||
session.setSnappingEnabled(true);
|
||||
QCOMPARE(session.snappedMove(draft, {a}, 5), snapped);
|
||||
}
|
||||
|
||||
void TransformSnapTests::dragsEditsAreNotPersistentAndEndsClearTheGuides()
|
||||
|
||||
+38
-12
@@ -24,7 +24,7 @@ private slots:
|
||||
void moveRotateAndShiftConstraints();
|
||||
void previewCommitCancelAndUndoPreserveSources();
|
||||
void duplicateTransformPreservesOriginalAndSupportsUndoAndCancel();
|
||||
void duplicateTransformCopiesEveryPixelLayerButNoFolder();
|
||||
void duplicateTransformCopiesLayersAndFoldersWithTheirContents();
|
||||
void scalePercentSetsBothSidesAboutTheCenter();
|
||||
void noOpInvalidValuesAndSwitchingTools();
|
||||
void rotatedHitTesting();
|
||||
@@ -373,7 +373,7 @@ void TransformTests::duplicateTransformPreservesOriginalAndSupportsUndoAndCancel
|
||||
QCOMPARE(int(session.document().value().layers.size()), 1);
|
||||
}
|
||||
|
||||
void TransformTests::duplicateTransformCopiesEveryPixelLayerButNoFolder()
|
||||
void TransformTests::duplicateTransformCopiesLayersAndFoldersWithTheirContents()
|
||||
{
|
||||
EditorSession session;
|
||||
session.createDocument(400, 200);
|
||||
@@ -383,16 +383,16 @@ void TransformTests::duplicateTransformCopiesEveryPixelLayerButNoFolder()
|
||||
const QUuid second = session.activeLayerID().value();
|
||||
session.addGroup();
|
||||
const QUuid folder = session.activeLayerID().value();
|
||||
// A folder has no pixels to copy; layers do.
|
||||
// Every selected root copies, an empty folder too.
|
||||
session.selectLayers({first, second, folder}, first);
|
||||
session.beginDuplicateTransform();
|
||||
QCOMPARE(int(session.document().value().layers.size()), 5);
|
||||
QCOMPARE(session.selectedLayerIDs().size(), 2);
|
||||
QVERIFY(!session.selectedLayerIDs().contains(first) && !session.selectedLayerIDs().contains(second));
|
||||
QCOMPARE(int(session.document().value().layers.size()), 6);
|
||||
QCOMPARE(session.selectedLayerIDs().size(), 3);
|
||||
QVERIFY(!session.selectedLayerIDs().contains(first) && !session.selectedLayerIDs().contains(folder));
|
||||
// The topmost copy leads; copies sit above their sources.
|
||||
QCOMPARE(session.document().value().layers[1].name, QString("Painted layer copy"));
|
||||
QCOMPARE(session.activeLayerID(), std::optional(session.document().value().layers[3].id));
|
||||
QVERIFY(session.transformsAsGroup());
|
||||
QCOMPARE(session.document().value().layers[5].name, QString("Folder 1 copy"));
|
||||
QCOMPARE(session.activeLayerID(), std::optional(session.document().value().layers[5].id));
|
||||
session.cancelTransform();
|
||||
QCOMPARE(int(session.document().value().layers.size()), 3);
|
||||
QCOMPARE(session.selectedLayerIDs(), (QSet<QUuid>{first, second, folder}));
|
||||
@@ -402,13 +402,21 @@ void TransformTests::duplicateTransformCopiesEveryPixelLayerButNoFolder()
|
||||
session.commitTransform();
|
||||
QCOMPARE(session.history.undoName(), QString("Duplicate Layers"));
|
||||
QCOMPARE(int(session.document().value().layers.size()), 5);
|
||||
// A folder alone has no pixels: nothing begins.
|
||||
// A folder carries its contents; a selected child rides along.
|
||||
QVERIFY(session.placeLayer(second, folder));
|
||||
session.selectLayer(folder);
|
||||
QVERIFY(session.canTransform());
|
||||
session.toggleGroupExpansion(folder);
|
||||
session.selectLayers({folder, second}, folder);
|
||||
session.beginDuplicateTransform();
|
||||
QVERIFY(!session.transformEdit().has_value());
|
||||
QCOMPARE(int(session.document().value().layers.size()), 7);
|
||||
const QUuid copy = session.activeLayerID().value();
|
||||
QCOMPARE(session.selectedLayerIDs(), QSet<QUuid>{copy});
|
||||
const std::vector<ImageLayer> &shown = session.document().value().layers;
|
||||
QVERIFY(shown[size_t(indexOf(shown, copy))].isGroup && session.descendantIDs(copy).size() == 1);
|
||||
QVERIFY(session.collapsedGroupIDs().contains(copy));
|
||||
session.cancelTransform();
|
||||
QCOMPARE(int(session.document().value().layers.size()), 5);
|
||||
QCOMPARE(session.collapsedGroupIDs(), QSet<QUuid>{folder});
|
||||
QCOMPARE(session.selectedLayerIDs(), (QSet<QUuid>{folder, second}));
|
||||
QVERIFY(session.canUndo());
|
||||
session.setIsProjectBusy(true);
|
||||
session.selectLayer(first);
|
||||
@@ -417,6 +425,24 @@ void TransformTests::duplicateTransformCopiesEveryPixelLayerButNoFolder()
|
||||
session.setIsProjectBusy(false);
|
||||
QVERIFY(session.canUndo());
|
||||
QCOMPARE(int(session.document().value().layers.size()), 5);
|
||||
// Past 10,000 layers nothing copies and nothing is recorded.
|
||||
ProjectSnapshot full = session.projectSnapshot().value();
|
||||
while (full.manifest.layers.size() < 10'000)
|
||||
full.manifest.layers.push_back(ProjectLayerRecord{.id = QUuid::createUuid(), .name = "Blank", .isVisible = true,
|
||||
.transform = {.origin = {0, 0}, .size = {40, 40}}, .imageFile = std::nullopt});
|
||||
session.installProject(full, QString());
|
||||
session.selectLayers({first, second}, second);
|
||||
const int steps = session.history.undoCount();
|
||||
session.beginDuplicateTransform();
|
||||
QVERIFY(!session.transformEdit().has_value());
|
||||
QCOMPARE(session.selectedLayerIDs(), (QSet<QUuid>{first, second}));
|
||||
QCOMPARE(session.activeLayerID(), std::optional(second));
|
||||
QCOMPARE(session.history.undoCount(), steps);
|
||||
// No transaction stays open: the next edit steps.
|
||||
session.selectLayer(first);
|
||||
session.setLayerOpacity(0.3);
|
||||
QVERIFY(session.canUndo());
|
||||
QCOMPARE(session.history.undoCount(), steps + 1);
|
||||
}
|
||||
|
||||
QTEST_GUILESS_MAIN(TransformTests)
|
||||
|
||||
@@ -404,7 +404,7 @@ void TypeControlsTests::aTextRowShowsAndOpensItsText()
|
||||
QVERIFY(QTest::qWaitForWindowActive(&list));
|
||||
LayerCell &textRow = *list.cells().at(0), &blankRow = *list.cells().at(1);
|
||||
const auto dimensions = [](LayerCell &row) { return row.findChild<QLabel *>("layerDimensions")->text(); };
|
||||
QCOMPARE(dimensions(textRow), QString("Text · Double-click to edit"));
|
||||
QCOMPARE(dimensions(textRow), QString("Text"));
|
||||
QCOMPARE(dimensions(blankRow), QString("400 × 300 px"));
|
||||
QCOMPARE(textRow.thumbnail().toolTip(), QString("Editable text layer"));
|
||||
QCOMPARE(textRow.thumbnail().accessibleName(), QString("Select text: Row"));
|
||||
|
||||
@@ -0,0 +1,58 @@
|
||||
#include "MenuFixtures.h"
|
||||
|
||||
// The View menu: zoom, the pixel grid and Snap.
|
||||
class ViewMenuTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void viewEntriesZoomTheFrontSession();
|
||||
void thePixelGridEntryTogglesTheSession();
|
||||
};
|
||||
|
||||
void ViewMenuTests::viewEntriesZoomTheFrontSession()
|
||||
{
|
||||
Bar bar;
|
||||
for (const char *name : {"fit", "actualPixels", "zoomIn", "zoomOut"})
|
||||
QVERIFY(!bar.action(name).isEnabled());
|
||||
bar.session().createDocument(400, 300);
|
||||
for (const char *name : {"fit", "actualPixels", "zoomIn", "zoomOut"})
|
||||
QVERIFY(bar.action(name).isEnabled());
|
||||
bar.session().zoom(0.5);
|
||||
bar.action("actualPixels").trigger();
|
||||
QCOMPARE(bar.session().viewport.zoom(), 1.0);
|
||||
bar.action("zoomIn").trigger();
|
||||
QCOMPARE(bar.session().viewport.zoom(), 1.25);
|
||||
bar.action("zoomOut").trigger();
|
||||
bar.action("zoomOut").trigger();
|
||||
QCOMPARE(bar.session().viewport.zoom(), 0.8);
|
||||
bar.action("fit").trigger();
|
||||
QVERIFY(bar.session().viewport.followsFit());
|
||||
}
|
||||
|
||||
void ViewMenuTests::thePixelGridEntryTogglesTheSession()
|
||||
{
|
||||
Bar bar;
|
||||
QAction &grid = bar.action("pixelGrid");
|
||||
QVERIFY(grid.isCheckable() && grid.isChecked() && grid.isEnabled());
|
||||
QCOMPARE(grid.text(), QString("Pixel Grid (800% and above)"));
|
||||
grid.trigger();
|
||||
QVERIFY(!bar.session().showsPixelGrid() && !grid.isChecked());
|
||||
grid.trigger();
|
||||
QVERIFY(bar.session().showsPixelGrid() && grid.isChecked());
|
||||
// The session's own change reaches the mark.
|
||||
bar.session().setShowsPixelGrid(false);
|
||||
QVERIFY(!grid.isChecked());
|
||||
bar.workspace.newCanvas();
|
||||
QVERIFY(grid.isChecked() && grid.isEnabled());
|
||||
// Snap, beneath it, follows the session the same way.
|
||||
QAction &snap = bar.action("snap");
|
||||
QVERIFY(snap.isCheckable() && snap.isChecked() && snap.isEnabled() && snap.text() == "Snap");
|
||||
snap.trigger();
|
||||
QVERIFY(!bar.session().snappingEnabled() && !snap.isChecked());
|
||||
snap.trigger();
|
||||
QVERIFY(bar.session().snappingEnabled() && snap.isChecked());
|
||||
bar.session().setSnappingEnabled(false);
|
||||
QVERIFY(!snap.isChecked());
|
||||
}
|
||||
|
||||
QTEST_MAIN(ViewMenuTests)
|
||||
#include "ViewMenuTests.moc"
|
||||
Reference in new issue
Block a user