341 lines
17 KiB
C++
341 lines
17 KiB
C++
#include "LayerDragFixtures.h"
|
|
|
|
// Layer list drops: rows and masks moved or copied.
|
|
class LayerDragTests : public QObject {
|
|
Q_OBJECT
|
|
private slots:
|
|
void aDragCarriesTheRowOrTheSelectionItBelongsTo();
|
|
void dropsMoveAboveARowIntoAFolderOrToTheBottom();
|
|
void dropsAreRefusedWhereTheyCannotLand();
|
|
void anAltDragDropsDuplicates();
|
|
void aMaskDragCopiesTheMaskOntoTheRowUnderIt();
|
|
void theIndicatorShowsWhereTheDropLands();
|
|
void anotherListsRowsLandNowhere();
|
|
void aDragNearTheEdgeScrollsTheList();
|
|
void aDragHeldAtTheEdgeScrollsOnAndAClipRefreshesTheCursor();
|
|
void leavingOrDroppingAtTheEdgeStopsTheScrolling();
|
|
};
|
|
|
|
void LayerDragTests::aDragCarriesTheRowOrTheSelectionItBelongsTo()
|
|
{
|
|
const auto session = sessionWithLayers(3);
|
|
Shown shown(*session);
|
|
session->selectLayers({shown.id(0), shown.id(2)}, shown.id(2));
|
|
const auto ids = [&](const QMimeData &data) { return QString::fromUtf8(data.data(ProjectWorkspace::layerType)).split('\n'); };
|
|
// The selection in list order, or the pressed row alone.
|
|
QCOMPARE(ids(*shown.list.dragData(*shown.list.cells().at(0))), (QStringList{uuidString(shown.id(0)), uuidString(shown.id(2))}));
|
|
QCOMPARE(ids(*shown.list.dragData(*shown.list.cells().at(2))), (QStringList{uuidString(shown.id(0)), uuidString(shown.id(2))}));
|
|
QCOMPARE(ids(*shown.list.dragData(*shown.list.cells().at(1))), QStringList{uuidString(shown.id(1))});
|
|
QVERIFY(shown.list.dragData(*shown.list.cells().at(1))->hasFormat(ProjectWorkspace::layerType));
|
|
}
|
|
|
|
void LayerDragTests::dropsMoveAboveARowIntoAFolderOrToTheBottom()
|
|
{
|
|
const auto session = sessionWithLayers(3);
|
|
Shown shown(*session);
|
|
QCOMPARE(shown.names(), (std::vector<QString>{"Layer 3", "Layer 2", "Layer 1"}));
|
|
// A plain row's middle: the lower half lands below.
|
|
QVERIFY(shown.drop(shown.rows({2}), Qt::MoveAction | Qt::CopyAction, shown.middle(0)));
|
|
QCOMPARE(shown.names(), (std::vector<QString>{"Layer 3", "Layer 1", "Layer 2"}));
|
|
const QPoint lower = shown.list.cells().at(2)->mapTo(&shown.list, QPoint(100, LayerCell::rowHeight - 4));
|
|
QVERIFY(shown.drop(shown.rows({0}), Qt::MoveAction, lower));
|
|
QCOMPARE(shown.names(), (std::vector<QString>{"Layer 1", "Layer 2", "Layer 3"}));
|
|
session->undo();
|
|
session->undo();
|
|
QCOMPARE(shown.names(), (std::vector<QString>{"Layer 3", "Layer 2", "Layer 1"}));
|
|
const int steps = session->history.undoCount();
|
|
// Dropped above the top row: a move, whatever Ctrl proposes.
|
|
QVERIFY(shown.drop(shown.rows({2}), Qt::MoveAction | Qt::CopyAction, shown.top(0), Qt::ControlModifier));
|
|
QCOMPARE(shown.names(), (std::vector<QString>{"Layer 1", "Layer 3", "Layer 2"}));
|
|
QCOMPARE(shown.lastAction, Qt::MoveAction);
|
|
QCOMPARE(session->history.undoName(), QString("Move Layer"));
|
|
QCOMPARE(session->history.undoCount(), steps + 1);
|
|
QCOMPARE(session->selectedLayerIDs(), QSet<QUuid>{shown.id(0)});
|
|
// Two rows dropped below every row keep their order.
|
|
QVERIFY(shown.drop(shown.rows({0, 1}), Qt::MoveAction | Qt::CopyAction, shown.below()));
|
|
QCOMPARE(shown.names(), (std::vector<QString>{"Layer 2", "Layer 1", "Layer 3"}));
|
|
QCOMPARE(session->history.undoName(), QString("Move Layers"));
|
|
QCOMPARE(session->selectedLayerIDs(), (QSet<QUuid>{shown.id(1), shown.id(2)}));
|
|
QCOMPARE(session->activeLayerID(), std::optional(shown.id(1)));
|
|
// Into a folder's middle: each lands on top, in order.
|
|
session->selectLayer(shown.id(0));
|
|
session->addGroup();
|
|
QCOMPARE(shown.names().front(), QString("Folder 1"));
|
|
const QUuid folder = shown.id(0);
|
|
QVERIFY(shown.drop(shown.rows({2, 3}), Qt::MoveAction | Qt::CopyAction, shown.middle(0)));
|
|
QCOMPARE(layerWith(*session, shown.id(1)).parentID, std::optional(folder));
|
|
QCOMPARE(layerWith(*session, shown.id(2)).parentID, std::optional(folder));
|
|
QCOMPARE(shown.names(), (std::vector<QString>{"Folder 1", "Layer 1", "Layer 3", "Layer 2"}));
|
|
// Above a child lands inside; the folder's top, outside.
|
|
QVERIFY(shown.drop(shown.rows({3}), Qt::MoveAction | Qt::CopyAction, shown.top(2)));
|
|
QCOMPARE(layerWith(*session, shown.id(2)).parentID, std::optional(folder));
|
|
QCOMPARE(shown.names(), (std::vector<QString>{"Folder 1", "Layer 1", "Layer 2", "Layer 3"}));
|
|
QVERIFY(shown.drop(shown.rows({3}), Qt::MoveAction | Qt::CopyAction, shown.top(0)));
|
|
QVERIFY(!layerWith(*session, shown.id(0)).parentID.has_value());
|
|
QCOMPARE(shown.names().front(), QString("Layer 3"));
|
|
session->undo();
|
|
QCOMPARE(shown.names().front(), QString("Folder 1"));
|
|
}
|
|
|
|
void LayerDragTests::dropsAreRefusedWhereTheyCannotLand()
|
|
{
|
|
const auto session = sessionWithLayers(2);
|
|
Shown shown(*session);
|
|
session->selectLayer(shown.id(0));
|
|
session->groupSelectedLayers();
|
|
const QUuid folder = shown.id(0);
|
|
// A folder into itself, or into its own child's row.
|
|
QVERIFY(!shown.drop(shown.rows({0}), Qt::MoveAction | Qt::CopyAction, shown.middle(0)));
|
|
QVERIFY(!shown.drop(shown.rows({0, 1}), Qt::MoveAction | Qt::CopyAction, shown.middle(0)));
|
|
QCOMPARE(layerWith(*session, shown.id(1)).parentID, std::optional(folder));
|
|
// Strangers, another type, an empty payload: nothing lands.
|
|
auto *stranger = new QMimeData;
|
|
stranger->setData(ProjectWorkspace::layerType, uuidString(QUuid::createUuid()).toUtf8());
|
|
QVERIFY(!shown.drop(stranger, Qt::MoveAction | Qt::CopyAction, shown.top(2)));
|
|
auto *text = new QMimeData;
|
|
text->setText(uuidString(shown.id(2)));
|
|
QVERIFY(!shown.drop(text, Qt::MoveAction | Qt::CopyAction, shown.top(0)));
|
|
QVERIFY(!shown.drop(shown.rows({}), Qt::MoveAction | Qt::CopyAction, shown.top(0)));
|
|
// A folder's contents ride with it, placed once.
|
|
QVERIFY(shown.drop(shown.rows({0, 1}), Qt::MoveAction | Qt::CopyAction, shown.below()));
|
|
QCOMPARE(shown.names(), (std::vector<QString>{"Layer 1", "Folder 1", "Layer 2"}));
|
|
QCOMPARE(layerWith(*session, shown.id(2)).parentID, std::optional(folder));
|
|
QCOMPARE(session->history.undoName(), QString("Move Layer"));
|
|
// Busy, no drop; a drop changing nothing records nothing.
|
|
const int steps = session->history.undoCount();
|
|
session->setIsImporting(true);
|
|
QVERIFY(!shown.drop(shown.rows({0}), Qt::MoveAction | Qt::CopyAction, shown.below()));
|
|
session->setIsImporting(false);
|
|
QCOMPARE(session->history.undoCount(), steps);
|
|
}
|
|
|
|
void LayerDragTests::anAltDragDropsDuplicates()
|
|
{
|
|
const auto session = sessionWithLayers(2);
|
|
Shown shown(*session);
|
|
const int steps = session->history.undoCount();
|
|
QVERIFY(shown.drop(shown.rows({1}), Qt::CopyAction, shown.top(0)));
|
|
QCOMPARE(shown.names(), (std::vector<QString>{"Layer 1 copy", "Layer 2", "Layer 1"}));
|
|
QCOMPARE(shown.lastAction, Qt::CopyAction);
|
|
QCOMPARE(session->history.undoName(), QString("Duplicate Layer"));
|
|
QCOMPARE(session->history.undoCount(), steps + 1);
|
|
QCOMPARE(session->activeLayerID(), std::optional(shown.id(0)));
|
|
QVERIFY(shown.drop(shown.rows({1, 2}), Qt::CopyAction, shown.below()));
|
|
QCOMPARE(shown.names(), (std::vector<QString>{"Layer 1 copy", "Layer 2", "Layer 1", "Layer 2 copy", "Layer 1 copy"}));
|
|
QCOMPARE(session->history.undoName(), QString("Duplicate Layers"));
|
|
session->undo();
|
|
QCOMPARE(shown.names().size(), size_t(3));
|
|
// A folder among the dragged rows copies with its contents.
|
|
const QUuid held = shown.id(2);
|
|
session->selectLayer(held);
|
|
session->addGroup();
|
|
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()
|
|
{
|
|
const auto session = sessionWithLayers(2);
|
|
Shown shown(*session);
|
|
session->selectLayer(shown.id(1));
|
|
session->addLayerMask();
|
|
const auto mask = [&](int row) {
|
|
auto *data = new QMimeData;
|
|
data->setData(NativeLayerList::maskType, uuidString(shown.id(row)).toUtf8());
|
|
return data;
|
|
};
|
|
QVERIFY(!shown.drop(mask(1), Qt::CopyAction, shown.middle(1)));
|
|
QVERIFY(!shown.drop(mask(0), Qt::CopyAction, shown.middle(1)));
|
|
QVERIFY(!layerWith(*session, shown.id(0)).mask.has_value());
|
|
QVERIFY(shown.drop(mask(1), Qt::CopyAction, shown.middle(0)));
|
|
QVERIFY(layerWith(*session, shown.id(0)).mask.has_value());
|
|
QCOMPARE(session->history.undoName(), QString("Copy Layer Mask"));
|
|
QVERIFY(!shown.drop(mask(1), Qt::CopyAction, shown.below()));
|
|
// No-id payloads name no mask, zeros included.
|
|
rewrite(*session, [&](ProjectSnapshot &snapshot) { record(snapshot, shown.id(1)).id = QUuid(); });
|
|
QCOMPARE(shown.id(1), QUuid());
|
|
session->selectLayer(QUuid());
|
|
session->addLayerMask();
|
|
QVERIFY(session->canCopyMask(QUuid(), shown.id(0)));
|
|
const int steps = session->history.undoCount();
|
|
auto *garbage = new QMimeData;
|
|
garbage->setData(NativeLayerList::maskType, "invalid mask id");
|
|
QVERIFY(!shown.drop(garbage, Qt::CopyAction, shown.middle(0)));
|
|
QCOMPARE(session->history.undoCount(), steps);
|
|
}
|
|
|
|
void LayerDragTests::theIndicatorShowsWhereTheDropLands()
|
|
{
|
|
const auto session = sessionWithLayers(3);
|
|
Shown shown(*session);
|
|
session->selectLayer(shown.id(0));
|
|
session->addGroup();
|
|
auto *column = shown.list.findChild<LayerColumn *>();
|
|
QVERIFY(column != nullptr);
|
|
const auto enter = [&](QMimeData *data, QPoint listPoint) {
|
|
const QPoint inViewport = shown.list.viewport()->mapFromParent(listPoint);
|
|
QDragEnterEvent event(DragAt(inViewport), Qt::MoveAction, data, Qt::LeftButton, Qt::NoModifier);
|
|
QApplication::sendEvent(shown.list.viewport(), &event);
|
|
delete data;
|
|
return event.isAccepted();
|
|
};
|
|
// A line above the row; a folder's middle fills it.
|
|
QVERIFY(enter(shown.rows({3}), shown.top(2)));
|
|
QCOMPARE(column->indicator, std::optional(QRect(0, shown.list.cells().at(2)->geometry().top() - 2, column->width(), 2)));
|
|
QVERIFY(!column->indicatorFills);
|
|
QVERIFY(enter(shown.rows({3}), shown.middle(0)));
|
|
QCOMPARE(column->indicator, std::optional(shown.list.cells().at(0)->geometry()));
|
|
QVERIFY(column->indicatorFills);
|
|
QVERIFY(enter(shown.rows({3}), shown.below()));
|
|
QCOMPARE(column->indicator, std::optional(QRect(0, shown.list.cells().back()->geometry().bottom(), column->width(), 2)));
|
|
// Refused, nothing shows; left, nothing stays.
|
|
QVERIFY(!enter(shown.rows({0}), shown.middle(0)));
|
|
QVERIFY(!column->indicator.has_value());
|
|
QVERIFY(enter(shown.rows({3}), shown.top(2)));
|
|
QDragLeaveEvent leave;
|
|
QApplication::sendEvent(shown.list.viewport(), &leave);
|
|
QVERIFY(!column->indicator.has_value());
|
|
}
|
|
|
|
void LayerDragTests::anotherListsRowsLandNowhere()
|
|
{
|
|
const auto session = sessionWithLayers(2);
|
|
Shown shown(*session);
|
|
NativeLayerList other(*session);
|
|
// The same ids, dragged from another list: refused.
|
|
std::unique_ptr<QMimeData> foreign = other.dragData(*other.cells().at(1));
|
|
auto *copy = new QMimeData;
|
|
for (const QString &format : foreign->formats())
|
|
copy->setData(format, foreign->data(format));
|
|
QVERIFY(!shown.drop(copy, Qt::MoveAction | Qt::CopyAction, shown.top(0)));
|
|
std::unique_ptr<QMimeData> own = shown.list.dragData(*shown.list.cells().at(1));
|
|
auto *mine = new QMimeData;
|
|
for (const QString &format : own->formats())
|
|
mine->setData(format, own->data(format));
|
|
QVERIFY(shown.drop(mine, Qt::MoveAction | Qt::CopyAction, shown.top(0)));
|
|
QCOMPARE(shown.names(), (std::vector<QString>{"Layer 1", "Layer 2"}));
|
|
}
|
|
|
|
void LayerDragTests::aDragNearTheEdgeScrollsTheList()
|
|
{
|
|
const auto session = sessionWithLayers(15);
|
|
Shown shown(*session);
|
|
shown.list.resize(252, 200);
|
|
QTRY_VERIFY(shown.list.verticalScrollBar()->maximum() > 0);
|
|
QMimeData *data = shown.rows({0});
|
|
const QPoint edge(100, shown.list.viewport()->height() - 2);
|
|
QDragEnterEvent enter(DragAt(edge), Qt::MoveAction, data, Qt::LeftButton, Qt::NoModifier);
|
|
QApplication::sendEvent(shown.list.viewport(), &enter);
|
|
// Held at the bottom edge, each move scrolls further.
|
|
for (int each = 0; each < 3; ++each) {
|
|
QDragMoveEvent move(DragAt(edge), Qt::MoveAction, data, Qt::LeftButton, Qt::NoModifier);
|
|
QApplication::sendEvent(shown.list.viewport(), &move);
|
|
}
|
|
QCOMPARE(shown.list.verticalScrollBar()->value(), 42);
|
|
auto *column = shown.list.findChild<LayerColumn *>();
|
|
QVERIFY(column->indicator.has_value());
|
|
QDragMoveEvent top(DragAt(QPoint(100, 1)), Qt::MoveAction, data, Qt::LeftButton, Qt::NoModifier);
|
|
QApplication::sendEvent(shown.list.viewport(), &top);
|
|
QCOMPARE(shown.list.verticalScrollBar()->value(), 27);
|
|
QDragLeaveEvent leave;
|
|
QApplication::sendEvent(shown.list.viewport(), &leave);
|
|
delete data;
|
|
}
|
|
|
|
void LayerDragTests::aDragHeldAtTheEdgeScrollsOnAndAClipRefreshesTheCursor()
|
|
{
|
|
const auto session = sessionWithLayers(15);
|
|
Shown shown(*session);
|
|
shown.list.resize(252, 200);
|
|
QTRY_VERIFY(shown.list.verticalScrollBar()->maximum() > 0);
|
|
QMimeData *data = shown.rows({0});
|
|
const QPoint edge(100, shown.list.viewport()->height() - 2);
|
|
QDragEnterEvent enter(DragAt(edge), Qt::MoveAction | Qt::CopyAction, data, Qt::LeftButton, Qt::NoModifier);
|
|
QApplication::sendEvent(shown.list.viewport(), &enter);
|
|
QDragMoveEvent move(DragAt(edge), Qt::MoveAction | Qt::CopyAction, data, Qt::LeftButton, Qt::NoModifier);
|
|
QApplication::sendEvent(shown.list.viewport(), &move);
|
|
// One move, then a still pointer: the list scrolls on.
|
|
QCOMPARE(shown.list.verticalScrollBar()->value(), 14);
|
|
QVERIFY(shown.list.autoscrolling());
|
|
QTRY_VERIFY(shown.list.verticalScrollBar()->value() >= 70);
|
|
auto *column = shown.list.findChild<LayerColumn *>();
|
|
QVERIFY(column->indicator.has_value());
|
|
// Back inside the scrolling rests; left, it stops.
|
|
QDragMoveEvent inside(DragAt(QPoint(100, 100)), Qt::MoveAction | Qt::CopyAction, data, Qt::LeftButton, Qt::NoModifier);
|
|
QApplication::sendEvent(shown.list.viewport(), &inside);
|
|
QVERIFY(!shown.list.autoscrolling());
|
|
const int resting = shown.list.verticalScrollBar()->value();
|
|
QTest::qWait(150);
|
|
QCOMPARE(shown.list.verticalScrollBar()->value(), resting);
|
|
QDragLeaveEvent leave;
|
|
QApplication::sendEvent(shown.list.viewport(), &leave);
|
|
const int rested = shown.list.verticalScrollBar()->value();
|
|
QTest::qWait(150);
|
|
QCOMPARE(shown.list.verticalScrollBar()->value(), rested);
|
|
QVERIFY(!column->indicator.has_value());
|
|
delete data;
|
|
// An Alt click on the strip clips; the cursor follows.
|
|
shown.list.verticalScrollBar()->setValue(0);
|
|
LayerCell &row = *shown.list.cells().at(0);
|
|
const QPoint strip(row.width() - 20, LayerCell::rowHeight - 4);
|
|
QTest::mouseClick(&row, Qt::LeftButton, Qt::AltModifier, strip);
|
|
QVERIFY(layerWith(*session, shown.id(0)).maskSourceID.has_value());
|
|
QCOMPARE(image(shown.list.viewport()->cursor()), image(NativeLayerList::clippingCursor(true, shown.list.devicePixelRatio())));
|
|
QTest::mouseClick(&row, Qt::LeftButton, Qt::AltModifier, strip);
|
|
QCOMPARE(image(shown.list.viewport()->cursor()), image(NativeLayerList::clippingCursor(false, shown.list.devicePixelRatio())));
|
|
}
|
|
|
|
void LayerDragTests::leavingOrDroppingAtTheEdgeStopsTheScrolling()
|
|
{
|
|
const auto session = sessionWithLayers(15);
|
|
Shown shown(*session);
|
|
shown.list.resize(252, 200);
|
|
QTRY_VERIFY(shown.list.verticalScrollBar()->maximum() > 0);
|
|
const QPoint edge(100, shown.list.viewport()->height() - 2);
|
|
const auto scrollAtTheEdge = [&](QMimeData *data) {
|
|
QDragEnterEvent enter(DragAt(edge), Qt::MoveAction | Qt::CopyAction, data, Qt::LeftButton, Qt::NoModifier);
|
|
QApplication::sendEvent(shown.list.viewport(), &enter);
|
|
QDragMoveEvent move(DragAt(edge), Qt::MoveAction | Qt::CopyAction, data, Qt::LeftButton, Qt::NoModifier);
|
|
QApplication::sendEvent(shown.list.viewport(), &move);
|
|
QVERIFY(shown.list.autoscrolling());
|
|
};
|
|
// Left at the edge, the timer stops for good.
|
|
QMimeData *data = shown.rows({0});
|
|
scrollAtTheEdge(data);
|
|
QDragLeaveEvent leave;
|
|
QApplication::sendEvent(shown.list.viewport(), &leave);
|
|
QVERIFY(!shown.list.autoscrolling());
|
|
delete data;
|
|
const int left = shown.list.verticalScrollBar()->value();
|
|
QTest::qWait(120);
|
|
QCOMPARE(shown.list.verticalScrollBar()->value(), left);
|
|
// Dropped at the edge, the same; the row lands.
|
|
data = shown.rows({0});
|
|
scrollAtTheEdge(data);
|
|
QDropEvent drop(edge, Qt::MoveAction | Qt::CopyAction, data, Qt::LeftButton, Qt::NoModifier);
|
|
QApplication::sendEvent(shown.list.viewport(), &drop);
|
|
QVERIFY(drop.isAccepted());
|
|
QVERIFY(!shown.list.autoscrolling());
|
|
delete data;
|
|
const int dropped = shown.list.verticalScrollBar()->value();
|
|
QTest::qWait(120);
|
|
QCOMPARE(shown.list.verticalScrollBar()->value(), dropped);
|
|
QCOMPARE(session->history.undoName(), QString("Move Layer"));
|
|
}
|
|
|
|
QTEST_MAIN(LayerDragTests)
|
|
#include "LayerDragTests.moc"
|