#include "UI/NativeLayerList.h" #include "SelectionFixtures.h" #include #include #include // The layer rows: what they show, select, rename and key. namespace { ImportedImage filled(int width, int height, QRgb premultiplied, const QString &name) { QImage image(width, height, QImage::Format_RGBA8888_Premultiplied); image.fill(QColor::fromRgba(premultiplied)); return ImportedImage(image, image, name); } std::unique_ptr sessionWithThreeLayers() { auto session = std::make_unique(); session->createDocument(800, 600); for (int each = 0; each < 3; ++each) session->addBlankLayer(); return session; } // A shown list in an active window. struct Shown { EditorSession &session; NativeLayerList list; explicit Shown(EditorSession &session) : session(session), list(session) { list.resize(252, 400); list.show(); if (!QTest::qWaitForWindowActive(&list)) throw std::runtime_error("the list never became active"); } LayerCell &row(int index) { return *list.cells().at(size_t(index)); } QString name(int index) { return row(index).findChild("layerName")->text(); } QString dimensions(int index) { return row(index).findChild("layerDimensions")->text(); } // A click on the row's name, away from every control. void click(int index, Qt::KeyboardModifiers modifiers = Qt::NoModifier) { QTest::mouseClick(&row(index), Qt::LeftButton, modifiers, QPoint(row(index).width() - 20, 20)); } QSet selected() { return session.selectedLayerIDs(); } QUuid id(int index) { return row(index).layerID(); } }; } class NativeLayerListTests : public QObject { Q_OBJECT private slots: void rowsFollowTheHierarchyTopDown(); void nativeSelectionDoesNotReloadRowsAndReorderKeepsIdentity(); void clicksSelectExtendAndRangeAndTargetPixelsOverAMask(); void thumbnailsTargetTheLayerOrItsMaskAndShiftTogglesIt(); void theEyeTogglesAndSwipesInOneStep(); void disclosureLinkAndTheMenuReachTheSession(); void renamingTypesInTheRowAndReturnsFocus(); void layerListToolKeysAndTransformNudge(); void altClickOnTheStripClipsToTheLayerBelow(); void thumbnailsRedrawOnlyWhenTheirPictureChanges(); void ctrlClicksOnThumbnailsLoadSelections(); void deleteClearsASelectionOrDeletesTheLayer(); }; void NativeLayerListTests::rowsFollowTheHierarchyTopDown() { EditorSession session; Shown shown(session); QCOMPARE(shown.list.cells().size(), size_t(0)); session.createDocument(400, 200); session.insert(filled(100, 50, qRgba(255, 0, 0, 255), "Red")); session.addBlankLayer(); QCOMPARE(shown.list.cells().size(), size_t(2)); // The top of the document is the top row. QCOMPARE(shown.name(0), QString("Layer 1")); QCOMPARE(shown.name(1), QString("Red")); QCOMPARE(shown.dimensions(0), QString("400 × 200 px")); // A name shows as typed, never as markup. QCOMPARE(shown.row(0).findChild("layerName")->textFormat(), Qt::PlainText); QCOMPARE(shown.row(0).findChild("layerDimensions")->textFormat(), Qt::PlainText); QCOMPARE(shown.dimensions(1), QString("100 × 50 px")); // The canvas box, 36 wide, at ratio 2. QCOMPARE(shown.row(1).thumbnail().icon().availableSizes(), QList{QSize(72, 36)}); QVERIFY(!shown.row(1).maskThumbnail().isVisible()); QVERIFY(!shown.row(1).findChild("layerDisclosure")->isVisible()); // A folder: its child steps in; collapsed, the child goes. session.selectLayer(shown.id(1)); session.groupSelectedLayers(); QCOMPARE(shown.list.cells().size(), size_t(3)); QCOMPARE(shown.dimensions(1), QString("Folder")); QVERIFY(shown.row(1).findChild("layerDisclosure")->isVisible()); QCOMPARE(shown.name(2), QString("Red")); const int stepped = shown.row(2).findChild("layerDisclosure")->x(); QCOMPARE(stepped - shown.row(1).findChild("layerDisclosure")->x(), 24); session.toggleGroupExpansion(shown.id(1)); QCOMPARE(shown.list.cells().size(), size_t(2)); session.toggleGroupExpansion(shown.id(1)); // A clipped layer shows its mark and its source. session.selectLayer(shown.id(0)); session.addBlankLayer(); QVERIFY(session.linkMask(shown.id(1), shown.id(0))); QCOMPARE(shown.name(0), QString("↳ Layer 2")); QCOMPARE(shown.dimensions(0), QString("Clipped to Layer 1")); QCOMPARE(shown.row(0).thumbnail().x() - shown.row(1).thumbnail().x(), 24); // A hidden layer fades; a mask shows thumbnail and link. session.toggleLayerVisibility(shown.id(1)); QCOMPARE(shown.row(1).graphicsEffect()->property("opacity").toDouble(), 0.35); QCOMPARE(shown.row(0).graphicsEffect()->property("opacity").toDouble(), 1.0); session.selectLayer(shown.id(1)); session.addLayerMask(); QVERIFY(shown.row(1).maskThumbnail().isVisible()); QVERIFY(shown.row(1).findChild("maskLink")->isVisible()); QCOMPARE(shown.row(1).maskThumbnail().icon().availableSizes(), QList{QSize(60, 30)}); session.toggleLayerMask(); QVERIFY(shown.row(1).findChild("maskDisabledMark")->isVisible()); } void NativeLayerListTests::nativeSelectionDoesNotReloadRowsAndReorderKeepsIdentity() { const auto session = sessionWithThreeLayers(); Shown shown(*session); const std::vector cells = shown.list.cells(); const QUuid bottom = session->document().value().layers.front().id; shown.click(2); QCOMPARE(session->activeLayerID(), std::optional(bottom)); QCOMPARE(shown.list.cells(), cells); QVERIFY(session->placeLayer(bottom, std::nullopt)); QCOMPARE(shown.id(0), bottom); QVERIFY(session->selectedLayerIDs().contains(bottom)); QVERIFY(session->placeLayer(bottom, std::nullopt, std::nullopt, true)); QCOMPARE(shown.id(2), bottom); QVERIFY(!session->placeLayer(QUuid::createUuid(), std::nullopt)); QVERIFY(!session->placeLayer(bottom, std::nullopt, QUuid::createUuid())); session->setIsImporting(true); QVERIFY(!session->placeLayer(bottom, std::nullopt)); } void NativeLayerListTests::clicksSelectExtendAndRangeAndTargetPixelsOverAMask() { const auto session = sessionWithThreeLayers(); Shown shown(*session); shown.click(0); QCOMPARE(shown.selected(), QSet{shown.id(0)}); QVERIFY(shown.list.hasFocus()); shown.click(2, Qt::ControlModifier); QCOMPARE(shown.selected(), (QSet{shown.id(0), shown.id(2)})); shown.click(2, Qt::ControlModifier); QCOMPARE(shown.selected(), QSet{shown.id(0)}); shown.click(2, Qt::ShiftModifier); QCOMPARE(shown.selected(), (QSet{shown.id(0), shown.id(1), shown.id(2)})); QCOMPARE(session->activeLayerID(), std::optional(shown.id(2))); // A plain click inside a multi-selection keeps it. shown.click(1); QCOMPARE(shown.selected().size(), 3); shown.click(1, Qt::ControlModifier); QCOMPARE(shown.selected().size(), 2); shown.click(1); QCOMPARE(shown.selected(), QSet{shown.id(1)}); // With the mask targeted, the name takes the layer back. session->addLayerMask(); QVERIFY(session->isMaskSelected()); shown.click(1); QVERIFY(!session->isMaskSelected()); QCOMPARE(shown.selected(), QSet{shown.id(1)}); // Inside a multi-selection the mask target stays put. QTest::mouseClick(&shown.row(1).maskThumbnail(), Qt::LeftButton); session->selectLayers({shown.id(1), shown.id(2)}, shown.id(1)); QVERIFY(session->isMaskSelected()); shown.click(1); QVERIFY(session->isMaskSelected()); QCOMPARE(shown.selected(), (QSet{shown.id(1), shown.id(2)})); } void NativeLayerListTests::thumbnailsTargetTheLayerOrItsMaskAndShiftTogglesIt() { const auto session = sessionWithThreeLayers(); Shown shown(*session); shown.click(0); session->addLayerMask(); QVERIFY(session->isMaskSelected()); QTest::mouseClick(&shown.row(0).thumbnail(), Qt::LeftButton); QVERIFY(!session->isMaskSelected()); QCOMPARE(shown.selected(), QSet{shown.id(0)}); QTest::mouseClick(&shown.row(0).maskThumbnail(), Qt::LeftButton); QVERIFY(session->isMaskSelected()); QVERIFY(session->activeLayer().value().mask.value().isEnabled); QTest::mouseClick(&shown.row(0).maskThumbnail(), Qt::LeftButton, Qt::ShiftModifier); QVERIFY(!session->activeLayer().value().mask.value().isEnabled && session->isMaskSelected()); QVERIFY(shown.row(0).maskThumbnail().isTargeted() && !shown.row(0).thumbnail().isTargeted()); // Shift on the layer thumbnail ranges the rows, targets nothing. QTest::mouseClick(&shown.row(2).thumbnail(), Qt::LeftButton, Qt::ShiftModifier); QCOMPARE(shown.selected(), (QSet{shown.id(0), shown.id(1), shown.id(2)})); QVERIFY(!layerWith(*session, shown.id(0)).mask.value().isEnabled); QCOMPARE(session->activeLayerID(), std::optional(shown.id(2))); // Alt on a thumbnail is the row's; the target stays. session->selectLayers({shown.id(0)}, shown.id(0)); QTest::mouseClick(&shown.row(0).maskThumbnail(), Qt::LeftButton); QTest::mouseClick(&shown.row(0).thumbnail(), Qt::LeftButton, Qt::AltModifier); QVERIFY(session->isMaskSelected()); // Another row's thumbnail selects that row; Ctrl selects none. QTest::mouseClick(&shown.row(0).thumbnail(), Qt::LeftButton); QTest::mouseClick(&shown.row(2).thumbnail(), Qt::LeftButton); QCOMPARE(shown.selected(), QSet{shown.id(2)}); QVERIFY(!session->isMaskSelected()); QVERIFY(shown.row(2).thumbnail().isTargeted() && !shown.row(0).thumbnail().isTargeted()); // A multi-selection shows no ring. session->extendSelection(shown.id(0)); QVERIFY(!shown.row(2).thumbnail().isTargeted() && !shown.row(0).thumbnail().isTargeted()); session->selectLayers({shown.id(2)}, shown.id(2)); QTest::mouseClick(&shown.row(0).thumbnail(), Qt::LeftButton, Qt::ControlModifier); QCOMPARE(shown.selected(), QSet{shown.id(2)}); // Importing, thumbnails answer no click. session->setIsImporting(true); QVERIFY(!shown.row(0).thumbnail().isEnabled() && !shown.row(0).maskThumbnail().isEnabled()); QTest::mouseClick(&shown.row(0).thumbnail(), Qt::LeftButton); QCOMPARE(shown.selected(), QSet{shown.id(2)}); } void NativeLayerListTests::theEyeTogglesAndSwipesInOneStep() { const auto session = sessionWithThreeLayers(); Shown shown(*session); auto *eye = shown.row(0).findChild("layerEye"); QCOMPARE(eye->accessibleName(), QString("Hide Layer 3")); QTest::mouseClick(eye, Qt::LeftButton); QVERIFY(!session->document().value().layers.back().isVisible); QCOMPARE(eye->accessibleName(), QString("Show Layer 3")); QCOMPARE(session->history.undoName(), QString("Hide Layer")); // Dragged down a row: the eyes passed change, no other. session->toggleLayerVisibility(shown.id(1)); session->toggleLayerVisibility(shown.id(2)); const int steps = session->history.undoCount(); const QPoint end(10, 16 + LayerCell::rowHeight); QTest::mousePress(eye, Qt::LeftButton, Qt::NoModifier, QPoint(10, 16)); QMouseEvent move(QEvent::MouseMove, QPointF(end), eye->mapToGlobal(end), Qt::NoButton, Qt::LeftButton, Qt::NoModifier); QApplication::sendEvent(eye, &move); QTest::mouseRelease(eye, Qt::LeftButton, Qt::NoModifier, end); QVERIFY(layerWith(*session, shown.id(0)).isVisible && layerWith(*session, shown.id(1)).isVisible); QVERIFY(!layerWith(*session, shown.id(2)).isVisible); QCOMPARE(session->history.undoCount(), steps + 1); session->undo(); QVERIFY(!layerWith(*session, shown.id(0)).isVisible && !layerWith(*session, shown.id(1)).isVisible); // Busy, the eye is disabled. session->setIsProjectBusy(true); QVERIFY(!eye->isEnabled()); } void NativeLayerListTests::disclosureLinkAndTheMenuReachTheSession() { EditorSession session; session.createDocument(20, 10); session.insert(filled(4, 4, qRgba(0, 0, 255, 255), "Blue")); session.insert(filled(4, 4, qRgba(0, 255, 0, 255), "Green")); session.groupSelectedLayers(); Shown shown(session); const QUuid folder = shown.id(0), green = shown.id(1), blue = shown.id(2); QTest::mouseClick(shown.row(0).findChild("layerDisclosure"), Qt::LeftButton); QVERIFY(session.collapsedGroupIDs().contains(folder)); QCOMPARE(shown.list.cells().size(), size_t(2)); QTest::mouseClick(shown.row(0).findChild("layerDisclosure"), Qt::LeftButton); QCOMPARE(shown.list.cells().size(), size_t(3)); // The green row's menu acts on green, whatever is active. session.selectLayer(blue); const auto trigger = [&](int row, const char *name) { LayerCell &cell = shown.row(row); shown.list.menuFor(cell, QPoint(cell.width() - 20, 20))->findChild(QString::fromLatin1(name))->trigger(); }; trigger(1, "addBlackMask"); QVERIFY(layerWith(session, green).mask.has_value()); QCOMPARE(layerWith(session, green).mask.value().asset.image().pixelColor(0, 0), QColor(0, 0, 0)); QCOMPARE(session.activeLayerID(), std::optional(green)); QTest::mouseClick(shown.row(1).findChild("maskLink"), Qt::LeftButton); QVERIFY(!layerWith(session, green).mask.value().isLinked); trigger(1, "toggleMask"); QVERIFY(!layerWith(session, green).mask.value().isEnabled); trigger(1, "deleteMask"); QVERIFY(!layerWith(session, green).mask.has_value()); trigger(1, "addWhiteMask"); QCOMPARE(layerWith(session, green).mask.value().asset.image().pixelColor(0, 0), QColor(255, 255, 255)); trigger(1, "layerVisibility"); QVERIFY(!layerWith(session, green).isVisible); QCOMPARE(layerWith(session, green).parentID, std::optional(folder)); trigger(1, "moveOut"); QVERIFY(!layerWith(session, green).parentID.has_value()); const auto rowOf = [&](QUuid id) { for (int row = 0; row < 3; ++row) { if (shown.id(row) == id) return row; } throw std::runtime_error("no row"); }; trigger(rowOf(folder), "renameLayer"); QCOMPARE(session.renamingLayerID(), std::optional(folder)); session.setRenamingLayerID(std::nullopt); // Delete takes the row's layer, or its whole selection. session.selectLayers({green, blue}, blue); trigger(rowOf(green), "deleteLayer"); QCOMPARE(session.document().value().layers.size(), size_t(1)); trigger(0, "deleteLayer"); QVERIFY(session.document().value().layers.empty()); } void NativeLayerListTests::renamingTypesInTheRowAndReturnsFocus() { const auto session = sessionWithThreeLayers(); Shown shown(*session); shown.click(1); QTest::mouseDClick(&shown.row(1), Qt::LeftButton, Qt::NoModifier, QPoint(shown.row(1).width() - 20, 20)); QCOMPARE(session->renamingLayerID(), std::optional(shown.id(1))); auto *editor = shown.row(1).findChild("layerNameEditor"); QTRY_VERIFY(editor->isVisible() && editor->hasFocus()); QCOMPARE(editor->selectedText(), QString("Layer 2")); QTest::keyClicks(editor, "Sky"); QTest::keyClick(editor, Qt::Key_Return); QCOMPARE(layerWith(*session, shown.id(1)).name, QString("Sky")); QVERIFY(!session->renamingLayerID().has_value()); QVERIFY(!editor->isVisible() && shown.list.hasFocus()); QCOMPARE(shown.name(1), QString("Sky")); // Escape keeps the old name; leaving keeps what was typed. session->setRenamingLayerID(shown.id(1)); QTRY_VERIFY(editor->isVisible()); QTest::keyClicks(editor, "Nope"); QTest::keyClick(editor, Qt::Key_Escape); QCOMPARE(layerWith(*session, shown.id(1)).name, QString("Sky")); QVERIFY(!session->renamingLayerID().has_value()); session->setRenamingLayerID(shown.id(1)); QTRY_VERIFY(editor->isVisible()); editor->selectAll(); QTest::keyClicks(editor, "Sea"); shown.list.setFocus(); QCOMPARE(layerWith(*session, shown.id(1)).name, QString("Sea")); // A pending rename closes the edits: nothing moves under it. session->setRenamingLayerID(shown.id(1)); QTRY_VERIFY(editor->isVisible()); QVERIFY(!session->placeLayer(shown.id(1), std::nullopt)); QVERIFY(!session->canEditLayers()); QTest::keyClick(editor, Qt::Key_Escape); // A pixel thumbnail's double click renames; busy, nothing. QTest::mouseDClick(&shown.row(1).thumbnail(), Qt::LeftButton); QCOMPARE(session->renamingLayerID(), std::optional(shown.id(1))); QTest::keyClick(editor, Qt::Key_Escape); session->setIsProjectBusy(true); session->setRenamingLayerID(shown.id(1)); QVERIFY(!editor->isVisible()); } void NativeLayerListTests::layerListToolKeysAndTransformNudge() { EditorSession session; session.createDocument(100, 100); session.insert(filled(10, 10, qRgba(255, 0, 0, 255), "Red")); Shown shown(session); 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(); QTest::keyClick(&shown.list, Qt::Key_Right); QCOMPARE(session.transformEdit().value().draft.origin.x(), 46.0); QCOMPARE(session.activeLayer().value().origin().x(), 45.0); QTest::keyClick(&shown.list, Qt::Key_Escape); QVERIFY(!session.transformEdit().has_value()); QCOMPARE(session.activeLayer().value().origin().x(), 45.0); QTest::keyClick(&shown.list, Qt::Key_Down, Qt::ShiftModifier); QCOMPARE(session.activeLayer().value().origin().y(), 55.0); QTest::keyClick(&shown.list, Qt::Key_Return); QVERIFY(!session.transformEdit().has_value()); QCOMPARE(session.activeLayer().value().origin().y(), 55.0); // Ctrl-letters are the menu's; other tools' arrows walk rows. QTest::keyClick(&shown.list, Qt::Key_H, Qt::ControlModifier); QCOMPARE(session.tool(), NavigationTool::move); session.addBlankLayer(); session.selectTool(NavigationTool::brush); QCOMPARE(session.activeLayerID(), std::optional(shown.id(0))); QTest::keyClick(&shown.list, Qt::Key_Down); QCOMPARE(session.activeLayerID(), std::optional(shown.id(1))); QTest::keyClick(&shown.list, Qt::Key_Down); QCOMPARE(session.activeLayerID(), std::optional(shown.id(1))); QTest::keyClick(&shown.list, Qt::Key_Up); QCOMPARE(session.activeLayerID(), std::optional(shown.id(0))); QTest::keyClick(&shown.list, Qt::Key_E); QCOMPARE(session.tool(), NavigationTool::brush); } void NativeLayerListTests::altClickOnTheStripClipsToTheLayerBelow() { const auto session = sessionWithThreeLayers(); Shown shown(*session); const QPoint strip(shown.row(0).width() - 20, LayerCell::rowHeight - 4); QTest::mouseClick(&shown.row(0), Qt::LeftButton, Qt::AltModifier, strip); QCOMPARE(layerWith(*session, shown.id(0)).maskSourceID, std::optional(shown.id(1))); QCOMPARE(shown.name(0), QString("↳ Layer 3")); QTest::mouseClick(&shown.row(0), Qt::LeftButton, Qt::AltModifier, strip); QVERIFY(!layerWith(*session, shown.id(0)).maskSourceID.has_value()); // Above the strip Alt selects plainly; Ctrl-Alt too. QTest::mouseClick(&shown.row(2), Qt::LeftButton, Qt::AltModifier, QPoint(shown.row(2).width() - 20, 20)); QCOMPARE(shown.selected(), QSet{shown.id(2)}); QVERIFY(!layerWith(*session, shown.id(2)).maskSourceID.has_value()); QTest::mouseClick(&shown.row(0), Qt::LeftButton, Qt::AltModifier, QPoint(shown.row(0).width() - 20, LayerCell::rowHeight - 14)); QCOMPARE(shown.selected(), QSet{shown.id(0)}); QVERIFY(!layerWith(*session, shown.id(0)).maskSourceID.has_value()); QTest::mouseClick(&shown.row(0), Qt::LeftButton, Qt::AltModifier | Qt::ControlModifier, strip); QVERIFY(!layerWith(*session, shown.id(0)).maskSourceID.has_value()); } void NativeLayerListTests::thumbnailsRedrawOnlyWhenTheirPictureChanges() { EditorSession session; session.createDocument(40, 20); session.insert(filled(10, 10, qRgba(255, 0, 0, 255), "Red")); Shown shown(session); const qint64 before = shown.row(0).thumbnail().icon().cacheKey(); session.renameLayer(shown.id(0), "Rose"); session.selectLayer(shown.id(0)); session.setLayerOpacity(0.5); QCOMPARE(shown.row(0).thumbnail().icon().cacheKey(), before); session.nudgeLayer(1, 0); QVERIFY(shown.row(0).thumbnail().icon().cacheKey() != before); const qint64 moved = shown.row(0).thumbnail().icon().cacheKey(); session.addLayerMask(); QCOMPARE(shown.row(0).thumbnail().icon().cacheKey(), moved); const qint64 mask = shown.row(0).maskThumbnail().icon().cacheKey(); session.toggleLayerMask(); QCOMPARE(shown.row(0).maskThumbnail().icon().cacheKey(), mask); } void NativeLayerListTests::deleteClearsASelectionOrDeletesTheLayer() { const auto session = sessionWithThreeLayers(); QImage red(8, 8, QImage::Format_RGBA8888_Premultiplied); red.fill(Qt::red); session->insert(ImportedImage(red, red, "Red")); Shown shown(*session); shown.list.setFocus(); // The red sits mid-canvas; the selection takes its left half. const QPointF origin = session->activeLayer().value().transform.origin; session->applySelection(rectPath(QRectF(origin, QSizeF(4, 8))), SelectionMode::replace, "Select"); QTest::keyClick(&shown.list, Qt::Key_Delete); QTRY_COMPARE(session->history.undoName(), QString("Clear")); session->deselect(); const int layers = int(session->document().value().layers.size()); QTest::keyClick(&shown.list, Qt::Key_Backspace); QCOMPARE(int(session->document().value().layers.size()), layers - 1); // With Ctrl the key is not Delete's. QTest::keyClick(&shown.list, Qt::Key_Delete, Qt::ControlModifier); QCOMPARE(int(session->document().value().layers.size()), layers - 1); } QTEST_MAIN(NativeLayerListTests) #include "NativeLayerListTests.moc" void NativeLayerListTests::ctrlClicksOnThumbnailsLoadSelections() { EditorSession session; session.createDocument(400, 200); session.insert(filled(100, 50, qRgba(255, 0, 0, 255), "Red"), QPointF(100, 100)); const QUuid red = session.activeLayerID().value(); QImage block(100, 50, QImage::Format_Grayscale8); block.fill(Qt::white); for (int y = 10; y < 30; ++y) std::fill_n(block.scanLine(y) + 10, 20, uchar(0)); rewrite(session, [&](ProjectSnapshot &snapshot) { setMask(snapshot, red, LayerMask::assetFrom(block)); }); session.selectLayerTarget(red, false); Shown shown(session); LayerCell &row = shown.row(0); QCOMPARE(row.thumbnail().toolTip(), QString("Select image pixels")); QCOMPARE(row.maskThumbnail().toolTip(), QString("Select layer mask; Alt-click to view it alone; Shift-click to enable/disable; Ctrl-click to select its black areas (Ctrl-Shift adds, Ctrl-Alt subtracts)")); // Ctrl on the picture: its opaque pixels, the row untouched. QTest::mouseClick(&row.thumbnail(), Qt::LeftButton, Qt::ControlModifier); QCOMPARE(session.history.undoName(), QString("Load Layer Selection")); QCOMPARE(session.selection().value().path.boundingRect(), QRectF(50, 75, 100, 50)); QVERIFY(!session.isMaskSelected() && row.thumbnail().isTargeted()); // Ctrl-Alt on the mask subtracts; Ctrl-Shift adds back. QTest::mouseClick(&row.maskThumbnail(), Qt::LeftButton, Qt::ControlModifier | Qt::AltModifier); QCOMPARE(session.history.undoName(), QString("Load Mask Selection")); QCOMPARE(int(session.selection().value().coverage(400, 200).constScanLine(90)[70]), 0); QCOMPARE(int(session.selection().value().coverage(400, 200).constScanLine(90)[120]), 255); QVERIFY(!session.isMaskSelected()); session.applySelection(rectPath(QRectF(0, 0, 10, 10)), SelectionMode::replace, "Select"); QTest::mouseClick(&row.maskThumbnail(), Qt::LeftButton, Qt::ControlModifier | Qt::ShiftModifier); QCOMPARE(session.selection().value().path.boundingRect(), QRectF(0, 0, 80, 105)); // Plain Ctrl replaces; a Ctrl double click loads twice. session.selectAll(); QTest::mouseClick(&row.maskThumbnail(), Qt::LeftButton, Qt::ControlModifier); QCOMPARE(session.selection().value().path.boundingRect(), QRectF(60, 85, 20, 20)); const int count = session.history.undoCount(); QTest::mouseDClick(&row.thumbnail(), Qt::LeftButton, Qt::ControlModifier); QCOMPARE(session.history.undoCount(), count + 1); QVERIFY(!row.isRenaming()); }