The format version in one place and the layer context menu (12.10a)
This commit is contained in:
1 parent
9ac3b1b81c
commit
fc076de71d
15 files changed
+700
-63
No files matched your search
@@ -0,0 +1,175 @@
|
||||
#include "LayerContextMenuFixtures.h"
|
||||
#include "ProjectFixtures.h"
|
||||
#include "SessionFixtures.h"
|
||||
#include <QLineEdit>
|
||||
|
||||
// The menu's gates at their edges, and its routing.
|
||||
class LayerContextMenuEdgeTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void aRefusedSelectionLeavesTheEntriesAtRest();
|
||||
void actionsReadTheActiveLayerWhenChosen();
|
||||
void aChosenEffectsLayerRowCountsAsUnselected();
|
||||
void theGatesHoldAtTheirEdges();
|
||||
void theMenuTargetsThePressedRowThroughARebuild();
|
||||
void aStaleEffectLeavesItsRowSelected();
|
||||
void anEntryWithNoActiveLayerDoesNothing();
|
||||
};
|
||||
|
||||
void LayerContextMenuEdgeTests::aRefusedSelectionLeavesTheEntriesAtRest()
|
||||
{
|
||||
// Levels open, nothing selected, then an effect's row.
|
||||
const auto session = paintedPair();
|
||||
Shown shown(*session);
|
||||
session->beginLevels();
|
||||
QVERIFY(session->levels());
|
||||
session->selectLayers({}, std::nullopt);
|
||||
QVERIFY(session->selectedLayerIDs().isEmpty() && !session->activeLayerID());
|
||||
auto *effect = shown.row(0).findChild<LayerEffectRow *>();
|
||||
QVERIFY(effect);
|
||||
const std::unique_ptr<QMenu> menu = shown.list.menuFor(shown.row(0), effect->mapTo(&shown.row(0), effect->rect().center()));
|
||||
QVERIFY(!session->activeLayerID());
|
||||
for (const char *name : {"duplicateLayer", "renameLayer", "deleteLayer", "clippingMask", "groupLayers", "moveOut", "addMask", "toggleMask",
|
||||
"deleteMask", "linkMask", "layerVisibility"})
|
||||
QVERIFY2(!item(*menu, name).isEnabled(), name);
|
||||
session->cancelLevels();
|
||||
}
|
||||
|
||||
void LayerContextMenuEdgeTests::actionsReadTheActiveLayerWhenChosen()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
const QUuid a = shown.row(0).layerID(), b = shown.row(1).layerID();
|
||||
// Built on A; B is active when chosen.
|
||||
std::unique_ptr<QMenu> menu = shown.menu(0);
|
||||
session->selectLayer(b);
|
||||
item(*menu, "layerVisibility").trigger();
|
||||
QVERIFY(layerWith(*session, a).isVisible && !layerWith(*session, b).isVisible);
|
||||
menu = shown.menu(0);
|
||||
session->selectLayer(b);
|
||||
item(*menu, "addWhiteMask").trigger();
|
||||
QVERIFY(!layerWith(*session, a).mask && layerWith(*session, b).mask && session->activeLayerID() == b);
|
||||
menu = shown.menu(0);
|
||||
session->selectLayer(b);
|
||||
item(*menu, "clippingMask").trigger();
|
||||
QVERIFY(!layerWith(*session, a).maskSourceID && layerWith(*session, b).maskSourceID);
|
||||
// Rename asks the gate again, as Swift's.
|
||||
menu = shown.menu(0);
|
||||
session->setIsProjectBusy(true);
|
||||
item(*menu, "renameLayer").trigger();
|
||||
QVERIFY(!session->renamingLayerID());
|
||||
session->setIsProjectBusy(false);
|
||||
}
|
||||
|
||||
void LayerContextMenuEdgeTests::aChosenEffectsLayerRowCountsAsUnselected()
|
||||
{
|
||||
// Swift's table shows no row while an effect is chosen.
|
||||
const auto session = paintedPair();
|
||||
Shown shown(*session);
|
||||
const QUuid top = shown.row(0).layerID();
|
||||
session->selectEffect(LayerEffectKind::stroke, top, false);
|
||||
QVERIFY(session->selectedLayerIDs().contains(top) && session->effectSelection());
|
||||
QLineEdit field;
|
||||
field.show();
|
||||
field.activateWindow();
|
||||
field.setFocus();
|
||||
QTRY_VERIFY(field.hasFocus());
|
||||
shown.list.activateWindow();
|
||||
QVERIFY(shown.menu(0));
|
||||
QTRY_VERIFY(shown.list.hasFocus());
|
||||
QVERIFY(!session->effectSelection() && session->selectedLayerIDs() == QSet<QUuid>{top});
|
||||
}
|
||||
|
||||
void LayerContextMenuEdgeTests::theGatesHoldAtTheirEdges()
|
||||
{
|
||||
// Nothing selected: the menu selects its row first.
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
const QUuid a = shown.row(0).layerID(), b = shown.row(1).layerID();
|
||||
session->selectLayers({}, std::nullopt);
|
||||
QVERIFY(shown.list.contextMenu(a));
|
||||
QCOMPARE(session->selectedLayerIDs(), QSet<QUuid>{a});
|
||||
// Two layers chosen: no mask entry.
|
||||
session->selectLayers({a, b}, a);
|
||||
std::unique_ptr<QMenu> menu = shown.list.contextMenu(a);
|
||||
for (const char *name : {"addMask", "addWhiteMask", "addBlackMask"})
|
||||
QVERIFY2(!item(*menu, name).isEnabled(), name);
|
||||
// A folder's mask never links.
|
||||
session->groupSelectedLayers();
|
||||
session->addMask(true);
|
||||
QVERIFY(session->activeLayer().value().isGroup && session->activeLayer().value().mask);
|
||||
menu = shown.list.contextMenu(session->activeLayerID().value());
|
||||
QVERIFY(!item(*menu, "linkMask").isEnabled() && item(*menu, "deleteMask").isEnabled());
|
||||
// Grouping stops at ten thousand layers.
|
||||
ProjectSnapshot crowded = twoLayers();
|
||||
while (crowded.manifest.layers.size() < 9'999)
|
||||
crowded.manifest.layers.push_back({.id = QUuid::createUuid(), .name = "n", .isVisible = true, .transform = {.origin = {0, 0}, .size = {1, 1}},
|
||||
.imageFile = std::nullopt});
|
||||
EditorSession packed;
|
||||
packed.installProject(crowded, QString());
|
||||
NativeLayerList list(packed);
|
||||
const QUuid last = packed.document().value().layers.back().id;
|
||||
packed.selectLayer(last);
|
||||
QVERIFY(item(*list.contextMenu(last), "groupLayers").isEnabled());
|
||||
packed.addBlankLayer();
|
||||
QCOMPARE(packed.document().value().layers.size(), size_t(10'000));
|
||||
QVERIFY(!item(*list.contextMenu(packed.activeLayerID().value()), "groupLayers").isEnabled());
|
||||
}
|
||||
|
||||
void LayerContextMenuEdgeTests::theMenuTargetsThePressedRowThroughARebuild()
|
||||
{
|
||||
// Committing new text rebuilds the rows under the press.
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
const QUuid pressed = shown.row(1).layerID();
|
||||
session->selectTool(NavigationTool::type);
|
||||
session->beginText(QPointF(10, 10), true);
|
||||
session->changeTextStyle([](LayerTextStyle &style) { style.content = QStringLiteral("new text"); });
|
||||
QVERIFY(session->textDraft());
|
||||
QPointer<LayerCell> cell = &shown.row(1);
|
||||
const std::unique_ptr<QMenu> menu = shown.list.menuFor(*cell, QPoint(cell->width() - 20, 20));
|
||||
QVERIFY(!cell && !session->textDraft());
|
||||
QCOMPARE(session->document().value().layers.size(), size_t(4));
|
||||
QCOMPARE(session->activeLayerID().value(), pressed);
|
||||
QCOMPARE(session->selectedLayerIDs(), QSet<QUuid>{pressed});
|
||||
item(*menu, "layerVisibility").trigger();
|
||||
QVERIFY(!layerWith(*session, pressed).isVisible);
|
||||
}
|
||||
|
||||
void LayerContextMenuEdgeTests::aStaleEffectLeavesItsRowSelected()
|
||||
{
|
||||
// Undo takes the effect; its row shows selected again.
|
||||
const auto session = paintedPair();
|
||||
Shown shown(*session);
|
||||
const QUuid top = shown.row(0).layerID();
|
||||
session->selectEffect(LayerEffectKind::stroke, top, false);
|
||||
session->undo();
|
||||
QVERIFY(session->effectSelection() && !session->selectedEffect());
|
||||
session->setRenamingLayerID(top);
|
||||
auto *field = shown.row(0).findChild<QLineEdit *>();
|
||||
QTRY_VERIFY(field && field->hasFocus());
|
||||
field->setText(QStringLiteral("Rename pending"));
|
||||
const std::unique_ptr<QMenu> menu = shown.menu(0);
|
||||
QVERIFY(field->hasFocus());
|
||||
QCOMPARE(session->renamingLayerID(), std::optional(top));
|
||||
QCOMPARE(layerWith(*session, top).name, QString("Red"));
|
||||
QVERIFY(!item(*menu, "duplicateLayer").isEnabled());
|
||||
}
|
||||
|
||||
void LayerContextMenuEdgeTests::anEntryWithNoActiveLayerDoesNothing()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
std::unique_ptr<QMenu> menu = shown.menu(0);
|
||||
QVERIFY(item(*menu, "layerVisibility").isEnabled());
|
||||
session->selectLayers({}, std::nullopt);
|
||||
QVERIFY(!session->activeLayerID());
|
||||
const int steps = session->history.undoCount();
|
||||
item(*menu, "layerVisibility").trigger();
|
||||
for (const ImageLayer &layer : session->document().value().layers)
|
||||
QVERIFY(layer.isVisible);
|
||||
QCOMPARE(session->history.undoCount(), steps);
|
||||
}
|
||||
|
||||
QTEST_MAIN(LayerContextMenuEdgeTests)
|
||||
#include "LayerContextMenuEdgeTests.moc"
|
||||
@@ -0,0 +1,63 @@
|
||||
#pragma once
|
||||
#include "UI/NativeLayerList.h"
|
||||
#include <QMenu>
|
||||
#include <QtTest>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
|
||||
// What the context menu tests share.
|
||||
namespace {
|
||||
// Two red pixel layers, the top one with a stroke.
|
||||
inline std::unique_ptr<EditorSession> paintedPair()
|
||||
{
|
||||
auto session = std::make_unique<EditorSession>();
|
||||
session->createDocument(8, 8);
|
||||
for (int layer = 0; layer < 2; ++layer) {
|
||||
QImage pixels(8, 8, QImage::Format_RGBA8888_Premultiplied);
|
||||
pixels.fill(Qt::red);
|
||||
session->insert(ImportedImage(pixels, pixels, QStringLiteral("Red")));
|
||||
}
|
||||
session->setEffects(LayerEffects{.stroke = StrokeEffect()});
|
||||
return session;
|
||||
}
|
||||
|
||||
// Swift's sessionWithThreeLayers: three blank layers, 800 by 600.
|
||||
inline std::unique_ptr<EditorSession> threeLayers()
|
||||
{
|
||||
auto session = std::make_unique<EditorSession>();
|
||||
session->createDocument(800, 600);
|
||||
for (int layer = 0; layer < 3; ++layer)
|
||||
session->addBlankLayer();
|
||||
return session;
|
||||
}
|
||||
|
||||
struct Shown {
|
||||
NativeLayerList list;
|
||||
explicit Shown(EditorSession &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)); }
|
||||
// A press on the row's name, away from every control.
|
||||
std::unique_ptr<QMenu> menu(int index) { return list.menuFor(row(index), QPoint(row(index).width() - 20, 20)); }
|
||||
};
|
||||
|
||||
inline QAction &item(QMenu &menu, const char *name)
|
||||
{
|
||||
auto *found = menu.findChild<QAction *>(QString::fromLatin1(name));
|
||||
if (!found)
|
||||
throw std::runtime_error(name);
|
||||
return *found;
|
||||
}
|
||||
|
||||
inline QStringList titles(const QMenu &menu)
|
||||
{
|
||||
QStringList result;
|
||||
for (const QAction *action : menu.actions())
|
||||
result << (action->isSeparator() ? QStringLiteral("-") : action->text());
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,316 @@
|
||||
#include "LayerContextMenuFixtures.h"
|
||||
#include "SessionFixtures.h"
|
||||
#include <QApplication>
|
||||
#include <QMenu>
|
||||
#include <QTimer>
|
||||
#include <QtTest>
|
||||
|
||||
// Swift's layer context menu tests (1.2.5), on the list.
|
||||
|
||||
class LayerContextMenuTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void testContextMenuContainsCoreLayerActions();
|
||||
void testRightClickOnUnselectedLayerSelectsIt();
|
||||
void testRightClickInsideMultiSelectionPreservesSelection();
|
||||
void testRightClickOutsideRowsReturnsNoMenu();
|
||||
void testContextMenuDuplicateUsesExistingDuplicateOperation();
|
||||
void testContextMenuMaskActionsFollowLayerState();
|
||||
void testContextMenuShowsCreateOrReleaseClippingAction();
|
||||
void testContextMenuUsesExistingMergeTitleAndAction();
|
||||
void testContextMenuActionsPreserveUndoRedo();
|
||||
void entriesFollowTheSelectionAndTheGates();
|
||||
void thumbnailsAndEffectsChooseWhatTheMenuTargets();
|
||||
void testContextMenuDuplicateFolderPreservesHierarchy();
|
||||
};
|
||||
|
||||
void LayerContextMenuTests::testContextMenuContainsCoreLayerActions()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
const std::unique_ptr<QMenu> menu = shown.menu(0);
|
||||
// Swift's order, the Layer Effects entry left out (5d7adba).
|
||||
QCOMPARE(titles(*menu), (QStringList{"Duplicate Layer", "Rename…", "Delete Layer", "-", "Create Clipping Mask", "Group Selected Layers",
|
||||
"Move Out of Folder", "Merge Down", "-", "Add Mask", "Disable Mask", "Delete Mask", "Unlink Mask", "-",
|
||||
"Hide Layer"}));
|
||||
QMenu *add = item(*menu, "addMask").menu();
|
||||
QVERIFY(add);
|
||||
QCOMPARE(titles(*add), (QStringList{"Reveal All (White)", "Hide All (Black)"}));
|
||||
}
|
||||
|
||||
void LayerContextMenuTests::testRightClickOnUnselectedLayerSelectsIt()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
const QUuid a = shown.row(0).layerID(), b = shown.row(1).layerID();
|
||||
session->selectLayers({a}, a);
|
||||
QVERIFY(shown.menu(1));
|
||||
QCOMPARE(session->selectedLayerIDs(), QSet<QUuid>{b});
|
||||
QCOMPARE(session->activeLayerID(), std::optional(b));
|
||||
// The list takes the keys, as Swift's first responder.
|
||||
QTRY_VERIFY(shown.list.hasFocus());
|
||||
// A real right-click shows the menu, for the row pressed.
|
||||
QStringList shownTitles;
|
||||
QTimer::singleShot(0, [&shownTitles] {
|
||||
auto *popup = qobject_cast<QMenu *>(QApplication::activePopupWidget());
|
||||
if (popup) {
|
||||
shownTitles = titles(*popup);
|
||||
popup->close();
|
||||
}
|
||||
});
|
||||
LayerCell &third = shown.row(2);
|
||||
QContextMenuEvent click(QContextMenuEvent::Mouse, QPoint(third.width() - 20, 20), third.mapToGlobal(QPoint(third.width() - 20, 20)));
|
||||
QApplication::sendEvent(&third, &click);
|
||||
QCOMPARE(shownTitles.value(0), QString("Duplicate Layer"));
|
||||
QCOMPARE(session->activeLayerID(), std::optional(third.layerID()));
|
||||
}
|
||||
|
||||
void LayerContextMenuTests::testRightClickInsideMultiSelectionPreservesSelection()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
const QSet<QUuid> all{shown.row(0).layerID(), shown.row(1).layerID(), shown.row(2).layerID()};
|
||||
session->selectLayers(all, shown.row(0).layerID());
|
||||
QVERIFY(shown.menu(1));
|
||||
QCOMPARE(session->selectedLayerIDs(), all);
|
||||
QCOMPARE(session->activeLayerID(), std::optional(shown.row(1).layerID()));
|
||||
}
|
||||
|
||||
void LayerContextMenuTests::testRightClickOutsideRowsReturnsNoMenu()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
const QSet<QUuid> before = session->selectedLayerIDs();
|
||||
// Below every row: no menu shows, the rows stay.
|
||||
QContextMenuEvent below(QContextMenuEvent::Mouse, QPoint(100, 380), shown.list.viewport()->mapToGlobal(QPoint(100, 380)));
|
||||
QApplication::sendEvent(shown.list.viewport(), &below);
|
||||
QVERIFY(!QApplication::activePopupWidget());
|
||||
QCOMPARE(session->selectedLayerIDs(), before);
|
||||
}
|
||||
|
||||
void LayerContextMenuTests::testContextMenuDuplicateUsesExistingDuplicateOperation()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
const ImageLayer target = session->activeLayer().value();
|
||||
const size_t count = session->document().value().layers.size();
|
||||
const int steps = session->history.undoCount();
|
||||
item(*shown.menu(0), "duplicateLayer").trigger();
|
||||
const std::vector<ImageLayer> &layers = session->document().value().layers;
|
||||
QCOMPARE(layers.size(), count + 1);
|
||||
QVERIFY(std::any_of(layers.begin(), layers.end(), [&](const ImageLayer &layer) { return layer.id == target.id; }));
|
||||
const ImageLayer copy = session->activeLayer().value();
|
||||
QVERIFY(copy.id != target.id);
|
||||
QCOMPARE(copy.name, target.name + " copy");
|
||||
QCOMPARE(session->history.undoCount(), steps + 1);
|
||||
QCOMPARE(session->history.undoName(), QString("Duplicate Layer"));
|
||||
}
|
||||
|
||||
void LayerContextMenuTests::testContextMenuMaskActionsFollowLayerState()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
std::unique_ptr<QMenu> menu = shown.menu(0);
|
||||
QVERIFY(item(*menu, "addMask").isEnabled() && !item(*menu, "deleteMask").isEnabled() && !item(*menu, "toggleMask").isEnabled());
|
||||
item(*menu, "addWhiteMask").trigger();
|
||||
QVERIFY(session->activeLayer().value().mask && session->activeLayer().value().mask.value().isEnabled);
|
||||
menu = shown.menu(0);
|
||||
QVERIFY(!item(*menu, "addMask").isEnabled() && !item(*menu, "addWhiteMask").isEnabled() && !item(*menu, "addBlackMask").isEnabled());
|
||||
QVERIFY(item(*menu, "toggleMask").isEnabled() && item(*menu, "deleteMask").isEnabled());
|
||||
QCOMPARE(item(*menu, "toggleMask").text(), QString("Disable Mask"));
|
||||
item(*menu, "toggleMask").trigger();
|
||||
QVERIFY(!session->activeLayer().value().mask.value().isEnabled);
|
||||
menu = shown.menu(0);
|
||||
QCOMPARE(item(*menu, "toggleMask").text(), QString("Enable Mask"));
|
||||
// Link follows the mask; the entries target the pixels first.
|
||||
QCOMPARE(item(*menu, "linkMask").text(), QString("Unlink Mask"));
|
||||
item(*menu, "linkMask").trigger();
|
||||
QVERIFY(!session->activeLayer().value().mask.value().isLinked);
|
||||
menu = shown.menu(0);
|
||||
QCOMPARE(item(*menu, "linkMask").text(), QString("Link Mask"));
|
||||
// With the mask chosen, an entry targets the pixels again.
|
||||
session->selectLayerTarget(session->activeLayerID().value(), true);
|
||||
item(*menu, "toggleMask").trigger();
|
||||
QVERIFY(session->activeLayer().value().mask.value().isEnabled && !session->isMaskSelected());
|
||||
session->selectLayerTarget(session->activeLayerID().value(), true);
|
||||
item(*menu, "deleteMask").trigger();
|
||||
QVERIFY(!session->activeLayer().value().mask && !session->isMaskSelected());
|
||||
// Black hides all.
|
||||
item(*shown.menu(0), "addBlackMask").trigger();
|
||||
QCOMPARE(session->activeLayer().value().mask.value().asset.image().pixelColor(0, 0), QColor(0, 0, 0));
|
||||
}
|
||||
|
||||
void LayerContextMenuTests::testContextMenuShowsCreateOrReleaseClippingAction()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
std::unique_ptr<QMenu> menu = shown.menu(0);
|
||||
QCOMPARE(item(*menu, "clippingMask").text(), QString("Create Clipping Mask"));
|
||||
QVERIFY(item(*menu, "clippingMask").isEnabled());
|
||||
item(*menu, "clippingMask").trigger();
|
||||
QVERIFY(session->activeLayer().value().maskSourceID);
|
||||
menu = shown.menu(0);
|
||||
QCOMPARE(item(*menu, "clippingMask").text(), QString("Release Clipping Mask"));
|
||||
QVERIFY(item(*menu, "clippingMask").isEnabled());
|
||||
item(*menu, "clippingMask").trigger();
|
||||
QVERIFY(!session->activeLayer().value().maskSourceID);
|
||||
// The bottom layer has nothing to clip to.
|
||||
QVERIFY(!item(*shown.menu(2), "clippingMask").isEnabled());
|
||||
}
|
||||
|
||||
void LayerContextMenuTests::testContextMenuUsesExistingMergeTitleAndAction()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
std::unique_ptr<QMenu> menu = shown.menu(0);
|
||||
QCOMPARE(item(*menu, "mergeLayers").text(), QString("Merge Down"));
|
||||
QCOMPARE(item(*menu, "mergeLayers").isEnabled(), session->canMergeLayers());
|
||||
session->selectLayers({shown.row(0).layerID(), shown.row(1).layerID()}, shown.row(0).layerID());
|
||||
menu = shown.menu(0);
|
||||
QCOMPARE(item(*menu, "mergeLayers").text(), QString("Merge Layers"));
|
||||
QVERIFY(item(*menu, "mergeLayers").isEnabled() && session->canMergeLayers());
|
||||
const size_t before = session->document().value().layers.size();
|
||||
item(*menu, "mergeLayers").trigger();
|
||||
QCOMPARE(session->document().value().layers.size(), before - 1);
|
||||
}
|
||||
|
||||
void LayerContextMenuTests::testContextMenuActionsPreserveUndoRedo()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
const std::vector<ImageLayer> initial = session->document().value().layers;
|
||||
item(*shown.menu(0), "duplicateLayer").trigger();
|
||||
QCOMPARE(session->document().value().layers.size(), size_t(4));
|
||||
session->undo();
|
||||
QVERIFY(session->document().value().layers == initial);
|
||||
session->redo();
|
||||
QCOMPARE(session->document().value().layers.size(), size_t(4));
|
||||
session->undo();
|
||||
item(*shown.menu(0), "addWhiteMask").trigger();
|
||||
QVERIFY(session->activeLayer().value().mask);
|
||||
session->undo();
|
||||
QVERIFY(!session->activeLayer().value().mask);
|
||||
const bool wasVisible = session->activeLayer().value().isVisible;
|
||||
QCOMPARE(item(*shown.menu(0), "layerVisibility").text(), QString("Hide Layer"));
|
||||
item(*shown.menu(0), "layerVisibility").trigger();
|
||||
QCOMPARE(session->activeLayer().value().isVisible, !wasVisible);
|
||||
QCOMPARE(item(*shown.menu(0), "layerVisibility").text(), QString("Show Layer"));
|
||||
session->undo();
|
||||
QCOMPARE(session->activeLayer().value().isVisible, wasVisible);
|
||||
}
|
||||
|
||||
void LayerContextMenuTests::entriesFollowTheSelectionAndTheGates()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
Shown shown(*session);
|
||||
// Rename for one layer; Delete names a selection or mask.
|
||||
std::unique_ptr<QMenu> menu = shown.menu(0);
|
||||
QVERIFY(item(*menu, "renameLayer").isEnabled() && item(*menu, "deleteLayer").text() == "Delete Layer");
|
||||
item(*menu, "renameLayer").trigger();
|
||||
QCOMPARE(session->renamingLayerID(), std::optional(shown.row(0).layerID()));
|
||||
session->setRenamingLayerID(std::nullopt);
|
||||
session->selectLayers({shown.row(0).layerID(), shown.row(1).layerID()}, shown.row(0).layerID());
|
||||
menu = shown.menu(1);
|
||||
QVERIFY(!item(*menu, "renameLayer").isEnabled() && item(*menu, "deleteLayer").text() == "Delete Selected Layers");
|
||||
item(*menu, "deleteLayer").trigger();
|
||||
QCOMPARE(session->document().value().layers.size(), size_t(1));
|
||||
// A chosen mask names Delete Mask.
|
||||
item(*shown.menu(0), "addWhiteMask").trigger();
|
||||
session->selectLayerTarget(shown.row(0).layerID(), true);
|
||||
menu = shown.list.contextMenu(shown.row(0).layerID());
|
||||
QCOMPARE(item(*menu, "deleteLayer").text(), QString("Delete Mask"));
|
||||
// Group, then Move Out for the layer inside.
|
||||
QVERIFY(item(*menu, "groupLayers").isEnabled() && !item(*menu, "moveOut").isEnabled());
|
||||
const QUuid child = shown.row(0).layerID();
|
||||
item(*menu, "groupLayers").trigger();
|
||||
QVERIFY(layerWith(*session, child).parentID.has_value());
|
||||
session->selectLayer(child);
|
||||
menu = shown.list.contextMenu(child);
|
||||
QVERIFY(item(*menu, "moveOut").isEnabled());
|
||||
item(*menu, "moveOut").trigger();
|
||||
QVERIFY(!session->activeLayer().value().parentID);
|
||||
// While a project is busy, the layer entries rest.
|
||||
session->groupSelectedLayers();
|
||||
session->selectLayer(child);
|
||||
session->addBlankLayer();
|
||||
const QUuid blank = session->activeLayerID().value();
|
||||
menu = shown.list.contextMenu(blank);
|
||||
QVERIFY(item(*menu, "moveOut").isEnabled() && item(*menu, "mergeLayers").isEnabled());
|
||||
session->setIsProjectBusy(true);
|
||||
menu = shown.list.contextMenu(blank);
|
||||
for (const char *name : {"duplicateLayer", "renameLayer", "deleteLayer", "groupLayers", "moveOut", "mergeLayers", "linkMask", "layerVisibility"})
|
||||
QVERIFY2(!item(*menu, name).isEnabled(), name);
|
||||
session->setIsProjectBusy(false);
|
||||
// An adjustment's mask never links, as Swift's.
|
||||
session->addAdjustment(AdjustmentKind::levels);
|
||||
session->setAdjustmentEditingID(std::nullopt);
|
||||
session->addMask(true);
|
||||
menu = shown.list.contextMenu(session->activeLayerID().value());
|
||||
QVERIFY(session->activeLayer().value().mask && !item(*menu, "linkMask").isEnabled() && item(*menu, "deleteMask").isEnabled());
|
||||
}
|
||||
|
||||
void LayerContextMenuTests::thumbnailsAndEffectsChooseWhatTheMenuTargets()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
session->addMask(true);
|
||||
Shown shown(*session);
|
||||
const QUuid top = shown.row(0).layerID();
|
||||
const auto at = [&](const char *name) {
|
||||
auto *button = shown.row(0).findChild<QWidget *>(QString::fromLatin1(name));
|
||||
return button->mapTo(&shown.row(0), button->rect().center());
|
||||
};
|
||||
// A selected row's mask thumbnail targets its mask; pixels, pixels.
|
||||
shown.list.menuFor(shown.row(0), at("maskThumbnail"));
|
||||
QVERIFY(session->isMaskSelected());
|
||||
shown.list.menuFor(shown.row(0), at("layerThumbnail"));
|
||||
QVERIFY(!session->isMaskSelected());
|
||||
// An unselected row's mask: it alone, its mask.
|
||||
session->selectLayer(shown.row(1).layerID());
|
||||
shown.list.menuFor(shown.row(0), at("maskThumbnail"));
|
||||
QVERIFY(session->selectedLayerIDs() == QSet<QUuid>{top} && session->isMaskSelected());
|
||||
// An effect's row chooses that effect; effects need pixels.
|
||||
EditorSession painted;
|
||||
painted.createDocument(8, 8);
|
||||
for (int layer = 0; layer < 2; ++layer) {
|
||||
QImage pixels(8, 8, QImage::Format_RGBA8888_Premultiplied);
|
||||
pixels.fill(Qt::red);
|
||||
painted.insert(ImportedImage(pixels, pixels, QStringLiteral("Red")));
|
||||
}
|
||||
painted.setEffects(LayerEffects{.stroke = StrokeEffect()});
|
||||
Shown both(painted);
|
||||
painted.selectLayer(both.row(1).layerID());
|
||||
auto *effect = both.row(0).findChild<LayerEffectRow *>();
|
||||
QVERIFY(effect);
|
||||
QVERIFY(both.list.menuFor(both.row(0), effect->mapTo(&both.row(0), effect->rect().center())));
|
||||
QVERIFY(painted.effectSelection() && painted.effectSelection().value().kind == LayerEffectKind::stroke);
|
||||
QCOMPARE(painted.effectSelection().value().layerID, both.row(0).layerID());
|
||||
// Anywhere else drops the chosen effect.
|
||||
both.menu(1);
|
||||
QVERIFY(!painted.effectSelection());
|
||||
}
|
||||
|
||||
void LayerContextMenuTests::testContextMenuDuplicateFolderPreservesHierarchy()
|
||||
{
|
||||
const auto session = threeLayers();
|
||||
QSet<QUuid> all;
|
||||
for (const ImageLayer &layer : session->document().value().layers)
|
||||
all.insert(layer.id);
|
||||
session->selectLayers(all, session->activeLayerID());
|
||||
session->groupSelectedLayers();
|
||||
const QUuid folder = session->activeLayerID().value();
|
||||
Shown shown(*session);
|
||||
std::unique_ptr<QMenu> menu = shown.list.contextMenu(folder);
|
||||
QVERIFY(item(*menu, "duplicateLayer").isEnabled());
|
||||
const size_t children = session->document().value().layers.size() - 1;
|
||||
item(*menu, "duplicateLayer").trigger();
|
||||
const ImageLayer copy = session->activeLayer().value();
|
||||
QVERIFY(copy.id != folder && copy.isGroup);
|
||||
size_t copied = 0;
|
||||
for (const ImageLayer &layer : session->document().value().layers)
|
||||
copied += layer.parentID == copy.id;
|
||||
QCOMPARE(copied, children);
|
||||
QCOMPARE(session->document().value().layers.size(), 2 * children + 2);
|
||||
}
|
||||
|
||||
QTEST_MAIN(LayerContextMenuTests)
|
||||
#include "LayerContextMenuTests.moc"
|
||||
@@ -6,6 +6,7 @@
|
||||
#include <QSettings>
|
||||
#include <QStackedWidget>
|
||||
#include <QToolButton>
|
||||
#include <QMenu>
|
||||
#include <QtTest>
|
||||
#include "SessionFixtures.h"
|
||||
|
||||
@@ -200,13 +201,8 @@ void LayersPanelTests::theMaskButtonUsesTheSelection()
|
||||
// The row's menu: black shows only the selection.
|
||||
session.undo();
|
||||
QVERIFY(session.selection().has_value());
|
||||
QAction *black = nullptr;
|
||||
for (QAction *action : panel.list().cells().at(0)->actions()) {
|
||||
if (action->objectName() == "addBlackMask")
|
||||
black = action;
|
||||
}
|
||||
QVERIFY(black);
|
||||
black->trigger();
|
||||
LayerCell &cell = *panel.list().cells().at(0);
|
||||
panel.list().menuFor(cell, QPoint(cell.width() - 20, 20))->findChild<QAction *>("addBlackMask")->trigger();
|
||||
QCOMPARE(session.history.undoName(), QString("Add Mask from Selection"));
|
||||
QCOMPARE(int(session.activeLayer().value().mask.value().asset.image().constScanLine(20)[30]), 255);
|
||||
QCOMPARE(int(session.activeLayer().value().mask.value().asset.image().constScanLine(5)[5]), 0);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
#include "UI/NativeLayerList.h"
|
||||
#include "SelectionFixtures.h"
|
||||
#include <QAction>
|
||||
#include <QMenu>
|
||||
#include <QtTest>
|
||||
|
||||
// The layer rows: what they show, select, rename and key.
|
||||
@@ -261,7 +262,10 @@ void NativeLayerListTests::disclosureLinkAndTheMenuReachTheSession()
|
||||
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) { shown.row(row).findChild<QAction *>(QString::fromLatin1(name))->trigger(); };
|
||||
const auto trigger = [&](int row, const char *name) {
|
||||
LayerCell &cell = shown.row(row);
|
||||
shown.list.menuFor(cell, QPoint(cell.width() - 20, 20))->findChild<QAction *>(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));
|
||||
@@ -274,7 +278,7 @@ void NativeLayerListTests::disclosureLinkAndTheMenuReachTheSession()
|
||||
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, "toggleVisibility");
|
||||
trigger(1, "layerVisibility");
|
||||
QVERIFY(!layerWith(session, green).isVisible);
|
||||
QCOMPARE(layerWith(session, green).parentID, std::optional(folder));
|
||||
trigger(1, "moveOut");
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
class ProjectTests : public QObject {
|
||||
Q_OBJECT
|
||||
private slots:
|
||||
void theCurrentFormatVersionIsOneTheReaderAccepts();
|
||||
void projectRoundTripSurvivesPackageMove();
|
||||
void masksAndLargeImagesRoundTrip();
|
||||
void overwriteReplacesPackageAndDropsRemovedAssets();
|
||||
@@ -20,6 +21,16 @@ private slots:
|
||||
void savesAndLoadsAreLogged();
|
||||
};
|
||||
|
||||
// Saves write current; loads take supported: they must agree.
|
||||
void ProjectTests::theCurrentFormatVersionIsOneTheReaderAccepts()
|
||||
{
|
||||
QVERIFY(ProjectManifest::supports(ProjectManifest::current));
|
||||
QVERIFY((ProjectManifest::supported == std::pair<qint64, qint64>(1, 9)));
|
||||
QCOMPARE(ProjectManifest{}.version, ProjectManifest::current);
|
||||
QVERIFY(!ProjectManifest::supports(0) && !ProjectManifest::supports(ProjectManifest::current + 1));
|
||||
QCOMPARE(QString(ProjectError::unsupportedVersion(10).what()), QString("This project uses format version 10. This app supports versions 1–9."));
|
||||
}
|
||||
|
||||
void ProjectTests::projectRoundTripSurvivesPackageMove()
|
||||
{
|
||||
QTemporaryDir root;
|
||||
|
||||
@@ -138,6 +138,8 @@ compositor_test(MaskTracingTests)
|
||||
compositor_test(MaskTransformTests)
|
||||
compositor_test(ProjectManifestTests)
|
||||
compositor_test(NativeLayerListTests)
|
||||
compositor_test(LayerContextMenuTests)
|
||||
compositor_test(LayerContextMenuEdgeTests)
|
||||
compositor_test(NavigationToolHeaderTests)
|
||||
compositor_test(NewCanvasSheetTests)
|
||||
compositor_test(OmarchyThemeTests)
|
||||
|
||||
Reference in new issue
Block a user