#include "IO/ProjectStore.h" #include "SessionFixtures.h" #include "UI/ProjectWorkspaceView.h" #include #include #include #include #include // A layer dragged to another tab, the canvas, or New. namespace { QMimeData *rowsOf(EditorSession &session) { QStringList lines; for (const ImageLayer &layer : session.document().value().layers) lines << uuidString(layer.id); auto *data = new QMimeData; data->setData(ProjectWorkspace::layerType, lines.join('\n').toUtf8()); return data; } // A drag entering a widget, then dropping or leaving. bool enter(QWidget &target, QMimeData *data, QPoint at = QPoint(5, 5)) { // Shift makes Qt propose the move, as plain drags do. QDragEnterEvent event(at, Qt::MoveAction | Qt::CopyAction, data, Qt::LeftButton, Qt::ShiftModifier); QApplication::sendEvent(&target, &event); return event.proposedAction() == Qt::MoveAction && event.isAccepted() && event.dropAction() == Qt::CopyAction; } // A row drag proposes a move; project targets copy. bool drop(QWidget &target, QMimeData *data, QPoint at = QPoint(5, 5)) { const bool entered = enter(target, data, at); QDropEvent event(at, Qt::MoveAction | Qt::CopyAction, data, Qt::LeftButton, Qt::ShiftModifier); QApplication::sendEvent(&target, &event); delete data; return entered && event.isAccepted() && event.dropAction() == Qt::CopyAction; } // A move after entering; only a valid one stays accepted. bool move(QWidget &target, QMimeData *data, QPoint at = QPoint(5, 5)) { QDragMoveEvent event(at, Qt::MoveAction | Qt::CopyAction, data, Qt::LeftButton, Qt::ShiftModifier); QApplication::sendEvent(&target, &event); return event.isAccepted() && event.dropAction() == Qt::CopyAction; } void leave(QWidget &target) { QDragLeaveEvent event; QApplication::sendEvent(&target, &event); } int layerCount(ProjectTab &tab) { return tab.session.document() ? int(tab.session.document().value().layers.size()) : 0; } struct Desk { ProjectWorkspace workspace; ProjectWorkspaceView window{workspace}; ProjectTab &first = workspace.current(); ProjectTab &second = workspace.addTab(false); Desk() { first.session.createDocument(8, 8); first.session.addBlankLayer(); first.session.addBlankLayer(); second.session.createDocument(8, 8); workspace.select(first.id); window.show(); if (!QTest::qWaitForWindowActive(&window)) throw std::runtime_error("the window never became active"); } ProjectTabButton &button(ProjectTab &tab) { for (ProjectTabButton *button : window.tabs()->buttons()) { if (button->tab->id == tab.id) return *button; } throw std::runtime_error("no button for the tab"); } }; } class ProjectDropTests : public QObject { Q_OBJECT private slots: void theWorkspaceKnowsADragsSourceTab(); void aTabTakesAnotherTabsLayersAndRefusesItsOwn(); void theCanvasTakesAnotherTabsLayerAtAPoint(); void theNewButtonOpensATabForADroppedLayer(); void altDragsAndBusyWorkspacesAreRefused(); }; void ProjectDropTests::theWorkspaceKnowsADragsSourceTab() { Desk desk; const QMimeData *data = rowsOf(desk.first.session); QCOMPARE(desk.workspace.draggedLayerSource(*data), std::optional(desk.first.id)); QVERIFY(!desk.workspace.canReceiveDrag(*data, desk.first.id)); QVERIFY(desk.workspace.canReceiveDrag(*data, desk.second.id)); QVERIFY(desk.workspace.canReceiveDrag(*data, std::nullopt)); delete data; // A stranger's id has no source: every target takes it. QMimeData stranger; stranger.setData(ProjectWorkspace::layerType, uuidString(QUuid::createUuid()).toUtf8()); QVERIFY(!desk.workspace.draggedLayerSource(stranger).has_value()); QVERIFY(desk.workspace.canReceiveDrag(stranger, desk.first.id)); QMimeData files; files.setText(QStringLiteral("not a layer")); QVERIFY(desk.workspace.canReceiveDrag(files, desk.first.id)); // A line that is no id names nothing, zeros included. const QUuid zeroed = desk.first.session.activeLayerID().value(); rewrite(desk.first.session, [&](ProjectSnapshot &snapshot) { record(snapshot, zeroed).id = QUuid(); }); QVERIFY(indexOf(desk.first.session.document().value().layers, QUuid()) >= 0); QMimeData garbage; garbage.setData(ProjectWorkspace::layerType, "not an id"); QVERIFY(!desk.workspace.draggedLayerSource(garbage).has_value()); QMimeData zero; zero.setData(ProjectWorkspace::layerType, uuidString(QUuid()).toUtf8()); QCOMPARE(desk.workspace.draggedLayerSource(zero), std::optional(desk.first.id)); } void ProjectDropTests::aTabTakesAnotherTabsLayersAndRefusesItsOwn() { Desk desk; ProjectTabButton &own = desk.button(desk.first), &other = desk.button(desk.second); QVERIFY(!enter(own, rowsOf(desk.first.session))); QCOMPARE(own.findChild("selectTab")->toolTip(), desk.first.title()); // Targeted, the other tab says so; left, it stops. QMimeData *data = rowsOf(desk.first.session); QVERIFY(enter(other, data)); QCOMPARE(other.findChild("selectTab")->toolTip(), QStringLiteral("Add to %1").arg(desk.second.title())); leave(other); QCOMPARE(other.findChild("selectTab")->toolTip(), desk.second.title()); delete data; // Dropped, both layers are copied, one after the other. QVERIFY(drop(other, rowsOf(desk.first.session))); QTRY_COMPARE(layerCount(desk.second), 2); QCOMPARE(layerCount(desk.first), 2); QCOMPARE(desk.workspace.current().id, desk.second.id); QCOMPARE(desk.second.session.document().value().layers.front().name, QString("Layer 1")); QVERIFY(!drop(other, rowsOf(desk.second.session))); } void ProjectDropTests::theCanvasTakesAnotherTabsLayerAtAPoint() { Desk desk; ContentView &editor = *desk.window.content(); QVERIFY(!editor.acceptsDrop(*std::unique_ptr(rowsOf(desk.first.session)))); desk.workspace.select(desk.second.id); ContentView &target = *desk.window.content(); // The canvas fits its view from the event loop first. QCoreApplication::processEvents(); QVERIFY(target.acceptsDrop(*std::unique_ptr(rowsOf(desk.first.session)))); auto *ring = target.findChild("dropRing"); QVERIFY(ring != nullptr && !ring->isVisible()); QMimeData *data = rowsOf(desk.first.session); QVERIFY(enter(target, data)); QVERIFY(ring->isVisible()); leave(target); QVERIFY(!ring->isVisible()); delete data; // Dropped on the canvas, the copy lands at that point. CanvasView &canvas = *target.findChild("editorCanvas"); const QPoint at = canvas.mapTo(&target, canvas.rect().center()); const QPointF centre = desk.second.session.viewport.documentPoint(canvas.rect().center(), QSizeF(8, 8)); QVERIFY(std::abs(centre.x() - 4) < 1 && std::abs(centre.y() - 4) < 1); QVERIFY(drop(target, rowsOf(desk.first.session), at)); QTRY_COMPARE(layerCount(desk.second), 2); QVERIFY(!ring->isVisible()); const ImageLayer copy = desk.second.session.document().value().layers.back(); QCOMPARE(copy.origin() + QPointF(copy.size().width() / 2, copy.size().height() / 2), centre); // A rename open in the target refuses the drop. desk.second.session.setRenamingLayerID(copy.id); QVERIFY(!target.acceptsDrop(*std::unique_ptr(rowsOf(desk.first.session)))); } void ProjectDropTests::theNewButtonOpensATabForADroppedLayer() { Desk desk; auto *button = desk.window.findChild("newCanvasButton"); QVERIFY(button != nullptr); const QString plain = button->toolTip(); QMimeData *data = rowsOf(desk.first.session); QVERIFY(enter(*button, data)); QCOMPARE(button->toolTip(), QString("Open in a new project tab")); leave(*button); QCOMPARE(button->toolTip(), plain); delete data; // Each dropped row opens its own tab, as Swift. QVERIFY(drop(*button, rowsOf(desk.first.session))); QTRY_COMPARE(int(desk.workspace.tabs().size()), 4); QTRY_COMPARE(layerCount(desk.workspace.current()), 1); QCOMPARE(layerCount(*desk.workspace.tabs().at(2)), 1); QVERIFY(desk.workspace.current().id != desk.first.id); // The action still makes a canvas on a click. QVERIFY(desk.window.findChild("newCanvasToolbar") == button->defaultAction()); } void ProjectDropTests::altDragsAndBusyWorkspacesAreRefused() { Desk desk; ProjectTabButton &other = desk.button(desk.second); auto *button = desk.window.findChild("newCanvasButton"); // Alt makes a layer drag a duplicate in its panel. QTest::keyPress(desk.window.windowHandle(), Qt::Key_Alt, Qt::AltModifier); QVERIFY(!enter(other, rowsOf(desk.first.session))); QVERIFY(!enter(*button, rowsOf(desk.first.session))); QTest::keyRelease(desk.window.windowHandle(), Qt::Key_Alt, Qt::NoModifier); QVERIFY(enter(other, rowsOf(desk.first.session))); leave(other); // Entered, then Alt pressed: Qt routes moves to that widget. QMimeData *data = rowsOf(desk.first.session); for (QWidget *target : {static_cast(&other), static_cast(button)}) { QVERIFY(enter(*target, data) && move(*target, data)); QTest::keyPress(desk.window.windowHandle(), Qt::Key_Alt, Qt::AltModifier); QVERIFY(!move(*target, data)); QTest::keyRelease(desk.window.windowHandle(), Qt::Key_Alt, Qt::NoModifier); leave(*target); } // The canvas entered, then its session busy: the same. desk.workspace.select(desk.second.id); ContentView &target = *desk.window.content(); QVERIFY(enter(target, data) && move(target, data)); desk.second.session.setIsProjectBusy(true); QVERIFY(!move(target, data)); desk.second.session.setIsProjectBusy(false); leave(target); delete data; desk.workspace.select(desk.first.id); // A busy source, and a workspace that cannot switch. desk.first.session.setIsProjectBusy(true); QVERIFY(!enter(other, rowsOf(desk.first.session))); QVERIFY(!enter(*button, rowsOf(desk.first.session))); desk.first.session.setIsProjectBusy(false); QVERIFY(enter(other, rowsOf(desk.first.session))); leave(other); QCOMPARE(layerCount(desk.second), 0); } QTEST_MAIN(ProjectDropTests) #include "ProjectDropTests.moc"