/* SPDX-FileCopyrightText: 2025 Vlad Zahorodnii SPDX-License-Identifier: GPL-2.0-or-later */ #include "kwin_wayland_test.h" #include "core/output.h" #include "pointer_input.h" #include "utils/pipe.h" #include "wayland_server.h" #include "window.h" #include "workspace.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace KWin { static const QString s_socketName = QStringLiteral("wayland_test_dnd-0"); static QFuture readMimeTypeData(KWayland::Client::DataOffer *offer, const QString &mimeType) { auto pipe = Pipe::create(O_CLOEXEC); if (!pipe) { return QFuture(); } offer->receive(mimeType, pipe->writeEndpoint.get()); return QtConcurrent::run([fd = std::move(pipe->readEndpoint)] { QFile file; if (!file.open(fd.get(), QFile::ReadOnly | QFile::Text)) { return QByteArray(); } return file.readAll(); }); } template static bool waitFuture(const QFuture &future) { QFutureWatcher watcher; QSignalSpy finishedSpy(&watcher, &QFutureWatcher::finished); watcher.setFuture(future); return finishedSpy.wait(); } class DndTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void init(); void cleanup(); void pointerDrag(); void pointerSubSurfaceDrag(); void pointerDragAction_data(); void pointerDragAction(); void inertPointerDragOffer(); void invalidSerialForPointerDrag(); void cancelPointerDrag(); void destroyPointerDragSource(); void noAcceptedMimeTypePointerDrag(); void secondPointerDrag(); void touchDrag(); void touchSubSurfaceDrag(); void touchDragAction_data(); void touchDragAction(); void inertTouchDragOffer(); void invalidSerialForTouchDrag(); void noAcceptedMimeTypeTouchDrag(); void secondTouchDrag(); void cancelTouchSequence(); void tabletDrag(); void tabletSubSurfaceDrag(); void tabletDragAction_data(); void tabletDragAction(); void inertTabletDragOffer(); void invalidSerialForTabletDrag(); void noAcceptedMimeTypeTabletDrag(); void secondTabletDrag(); private: QMimeDatabase m_mimeDb; }; void DndTest::initTestCase() { qRegisterMetaType(); QVERIFY(waylandServer()->init(s_socketName)); kwinApp()->start(); Test::setOutputConfig({ Rect(0, 0, 1280, 1024), Rect(1280, 0, 1280, 1024), }); const auto outputs = workspace()->outputs(); QCOMPARE(outputs.count(), 2); QCOMPARE(outputs[0]->geometry(), Rect(0, 0, 1280, 1024)); QCOMPARE(outputs[1]->geometry(), Rect(1280, 0, 1280, 1024)); // Generate tablet tool events so the corresponding tablet tool device is registered. Test::tabletToolProximityEvent(QPointF(640, 480), 0, 0, 0, 0, true, 0, 0); Test::tabletToolProximityEvent(QPointF(640, 480), 0, 0, 0, 0, false, 0, 0); } void DndTest::init() { QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::Seat | Test::AdditionalWaylandInterface::DataDeviceManager | Test::AdditionalWaylandInterface::WpTabletV2)); workspace()->setActiveOutput(QPoint(640, 512)); input()->pointer()->warp(QPoint(640, 512)); } void DndTest::cleanup() { Test::destroyWaylandConnection(); } void DndTest::pointerDrag() { // This test verifies that a simple pointer drag-and-drop operation between two Wayland clients works as expected. QVERIFY(Test::waitForWaylandPointer()); // Setup the data source. auto pointer = Test::waylandSeat()->createPointer(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); connect(dataSource, &KWayland::Client::DataSource::sendDataRequested, this, [](const QString &mimeType, int fd) { if (mimeType == QLatin1String("text/plain")) { const auto payload = QByteArrayLiteral("foo"); write(fd, payload.data(), payload.size()); } close(fd); }); QSignalSpy pointerButtonSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); QSignalSpy pointerEnteredSpy(pointer, &KWayland::Client::Pointer::entered); QSignalSpy pointerLeftSpy(pointer, &KWayland::Client::Pointer::left); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDragMotionSpy(dataDevice, &KWayland::Client::DataDevice::dragMotion); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); // Setup the source client. auto sourceSurface = Test::createSurface(); auto sourceShellSurface = Test::createXdgToplevelSurface(sourceSurface.get()); auto sourceWindow = Test::renderAndWaitForShown(sourceSurface.get(), QSize(100, 100), Qt::red); QVERIFY(sourceWindow); sourceWindow->move(QPointF(0, 0)); // Setup the target client. auto targetSurface = Test::createSurface(); auto targetShellSurface = Test::createXdgToplevelSurface(targetSurface.get()); auto targetWindow = Test::renderAndWaitForShown(targetSurface.get(), QSize(100, 100), Qt::red); QVERIFY(targetWindow); targetWindow->move(QPointF(100, 0)); // Move the pointer to the center of the source window and press the left mouse button. quint32 timestamp = 0; Test::pointerMotion(QPointF(50, 50), timestamp++); Test::pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(pointerButtonSpy.wait()); // Start a drag-and-drop operation. const quint32 buttonPressSerial = pointerButtonSpy.last().at(0).value(); dataDevice->startDrag(buttonPressSerial, dataSource, sourceSurface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(pointerLeftSpy.count(), 1); QVERIFY(!pointer->enteredSurface()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(50, 50)); QCOMPARE(dataDevice->dragSurface(), sourceSurface.get()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 1); QCOMPARE(dataDeviceDragMotionSpy.count(), 0); QCOMPARE(dataDeviceDragLeftSpy.count(), 0); QCOMPARE(dataDeviceDroppedSpy.count(), 0); // Accept our own offer. auto sourceOffer = dataDevice->takeDragOffer(); QCOMPARE(sourceOffer->offeredMimeTypes(), (QList{m_mimeDb.mimeTypeForName(QStringLiteral("text/plain"))})); QCOMPARE(sourceOffer->sourceDragAndDropActions(), KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(sourceOffer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::None); QSignalSpy sourceTargetsAcceptsSpy(dataSource, &KWayland::Client::DataSource::targetAccepts); sourceOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); sourceOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); QSignalSpy sourceOfferSelectedDragAndDropActionSpy(sourceOffer.get(), &KWayland::Client::DataOffer::selectedDragAndDropActionChanged); sourceOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(sourceOfferSelectedDragAndDropActionSpy.wait()); QCOMPARE(sourceOffer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Copy); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Copy); // Move the pointer a little bit. Test::pointerMotion(QPointF(60, 50), timestamp++); QVERIFY(dataDeviceDragMotionSpy.wait()); QCOMPARE(dataDeviceDragMotionSpy.last().at(0).value(), QPointF(60, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 1); QCOMPARE(dataDeviceDragMotionSpy.count(), 1); QCOMPARE(dataDeviceDragLeftSpy.count(), 0); QCOMPARE(dataDeviceDroppedSpy.count(), 0); // Move the pointer to the second client. Test::pointerMotion(QPointF(140, 50), timestamp++); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(40, 50)); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDevice->dragSurface(), targetSurface.get()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragMotionSpy.count(), 1); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDeviceDroppedSpy.count(), 0); // Accept the offer. auto targetOffer = dataDevice->takeDragOffer(); QCOMPARE(targetOffer->offeredMimeTypes(), (QList{m_mimeDb.mimeTypeForName(QStringLiteral("text/plain"))})); QCOMPARE(targetOffer->sourceDragAndDropActions(), KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(targetOffer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::None); targetOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); targetOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); QSignalSpy targetOfferSelectedDragAndDropActionSpy(targetOffer.get(), &KWayland::Client::DataOffer::selectedDragAndDropActionChanged); targetOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Move, KWayland::Client::DataDeviceManager::DnDAction::Move); QVERIFY(targetOfferSelectedDragAndDropActionSpy.wait()); QCOMPARE(targetOffer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); // Move the pointer a little bit. Test::pointerMotion(QPointF(160, 50), timestamp++); QVERIFY(dataDeviceDragMotionSpy.wait()); QCOMPARE(dataDeviceDragMotionSpy.last().at(0).value(), QPointF(60, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragMotionSpy.count(), 2); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDeviceDroppedSpy.count(), 0); // Drop. Test::pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(pointerEnteredSpy.wait()); QCOMPARE(pointer->enteredSurface(), targetSurface.get()); QCOMPARE(pointerEnteredSpy.last().at(1).value(), QPointF(60, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragMotionSpy.count(), 2); QCOMPARE(dataDeviceDragLeftSpy.count(), 2); QCOMPARE(dataDeviceDroppedSpy.count(), 1); // Ask for data. const QFuture plainData = readMimeTypeData(targetOffer.get(), QStringLiteral("text/plain")); QVERIFY(waitFuture(plainData)); QCOMPARE(plainData.result(), QByteArrayLiteral("foo")); // text/html hasn't been offered, so this should fail. const QFuture htmlData = readMimeTypeData(targetOffer.get(), QStringLiteral("text/html")); QVERIFY(waitFuture(htmlData)); QCOMPARE(htmlData.result(), QByteArray()); // Finish drag-and-drop. QSignalSpy dragAndDropFinishedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropFinished); targetOffer->dragAndDropFinished(); QVERIFY(dragAndDropFinishedSpy.wait()); } void DndTest::pointerSubSurfaceDrag() { // This test verifies that a drag-and-drop originating from a sub-surface works as expected. QVERIFY(Test::waitForWaylandPointer()); // Setup the data source. auto pointer = Test::waylandSeat()->createPointer(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy pointerButtonSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); QSignalSpy pointerEnteredSpy(pointer, &KWayland::Client::Pointer::entered); QSignalSpy pointerLeftSpy(pointer, &KWayland::Client::Pointer::left); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDragMotionSpy(dataDevice, &KWayland::Client::DataDevice::dragMotion); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); // Setup a window with a subsurface that covers the right half. auto parentSurface = Test::createSurface(); auto parentShellSurface = Test::createXdgToplevelSurface(parentSurface.get()); auto window = Test::renderAndWaitForShown(parentSurface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); auto childSurface = Test::createSurface(); auto subSurface = Test::createSubSurface(childSurface.get(), parentSurface.get()); subSurface->setPosition(QPoint(50, 0)); Test::render(childSurface.get(), QSize(50, 100), Qt::blue); parentSurface->commit(KWayland::Client::Surface::CommitFlag::None); // Move the pointer to the center of the subsurface and press the left mouse button. quint32 timestamp = 0; Test::pointerMotion(QPointF(75, 50), timestamp++); Test::pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(pointerButtonSpy.wait()); // Start a drag-and-drop operation. const quint32 buttonPressSerial = pointerButtonSpy.last().at(0).value(); dataDevice->startDrag(buttonPressSerial, dataSource, childSurface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(pointerLeftSpy.count(), 1); QVERIFY(!pointer->enteredSurface()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(25, 50)); QCOMPARE(dataDevice->dragSurface(), childSurface.get()); // Move the pointer to the parent surface. Test::pointerMotion(QPointF(10, 50), timestamp++); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(10, 50)); QCOMPARE(dataDevice->dragSurface(), parentSurface.get()); Test::pointerMotion(QPointF(15, 50), timestamp++); QVERIFY(dataDeviceDragMotionSpy.wait()); QCOMPARE(dataDeviceDragMotionSpy.last().at(0).value(), QPointF(15, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDevice->dragSurface(), parentSurface.get()); // Move the pointer to the child surface. Test::pointerMotion(QPointF(80, 50), timestamp++); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 3); QCOMPARE(dataDeviceDragLeftSpy.count(), 2); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(30, 50)); QCOMPARE(dataDevice->dragSurface(), childSurface.get()); Test::pointerMotion(QPointF(75, 50), timestamp++); QVERIFY(dataDeviceDragMotionSpy.wait()); QCOMPARE(dataDeviceDragMotionSpy.last().at(0).value(), QPointF(25, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 3); QCOMPARE(dataDeviceDragLeftSpy.count(), 2); QCOMPARE(dataDevice->dragSurface(), childSurface.get()); // Accept the data offer. auto offer = dataDevice->takeDragOffer(); QCOMPARE(offer->offeredMimeTypes(), (QList{m_mimeDb.mimeTypeForName(QStringLiteral("text/plain"))})); QCOMPARE(offer->sourceDragAndDropActions(), KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(offer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::None); QSignalSpy sourceTargetsAcceptsSpy(dataSource, &KWayland::Client::DataSource::targetAccepts); offer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); offer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); QSignalSpy targetOfferSelectedDragAndDropActionSpy(offer.get(), &KWayland::Client::DataOffer::selectedDragAndDropActionChanged); offer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Move, KWayland::Client::DataDeviceManager::DnDAction::Move); QVERIFY(targetOfferSelectedDragAndDropActionSpy.wait()); QCOMPARE(offer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); // Drop. Test::pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(pointerEnteredSpy.wait()); QCOMPARE(pointer->enteredSurface(), childSurface.get()); QCOMPARE(pointerEnteredSpy.last().at(1).value(), QPointF(25, 50)); QCOMPARE(dataDeviceDroppedSpy.count(), 1); // Finish drag-and-drop. QSignalSpy dragAndDropFinishedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropFinished); offer->dragAndDropFinished(); QVERIFY(dragAndDropFinishedSpy.wait()); } void DndTest::pointerDragAction_data() { QTest::addColumn("action"); QTest::addRow("ask") << KWayland::Client::DataDeviceManager::DnDAction::Ask; QTest::addRow("copy") << KWayland::Client::DataDeviceManager::DnDAction::Copy; QTest::addRow("move") << KWayland::Client::DataDeviceManager::DnDAction::Move; } void DndTest::pointerDragAction() { // This test verifies that the dnd actions are handled as expected. "Ask" action is a bit // special; if it has been selected, then the final action can be set after the drop. QVERIFY(Test::waitForWaylandPointer()); QFETCH(KWayland::Client::DataDeviceManager::DnDAction, action); // Setup the data source. auto pointer = Test::waylandSeat()->createPointer(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move | KWayland::Client::DataDeviceManager::DnDAction::Ask); QSignalSpy pointerButtonSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy dataSourceSelectedDragAndDropActionChangedSpy(dataSource, &KWayland::Client::DataSource::selectedDragAndDropActionChanged); // Setup the source window. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Move the pointer to the center of the surface and press the left mouse button. quint32 timestamp = 0; Test::pointerMotion(QPointF(50, 50), timestamp++); Test::pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(pointerButtonSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(pointerButtonSpy.last().at(0).value(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); const auto offer = dataDevice->takeDragOffer(); offer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); offer->setDragAndDropActions(action, action); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); QCOMPARE(dataSourceSelectedDragAndDropActionChangedSpy.count(), 1); QCOMPARE(dataSource->selectedDragAndDropAction(), action); // Drop. Test::pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(dataDeviceDroppedSpy.wait()); QCOMPARE(dataSourceSelectedDragAndDropActionChangedSpy.count(), 1); // Change action after drop. switch (action) { case KWayland::Client::DataDeviceManager::DnDAction::Ask: offer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); QCOMPARE(dataSourceSelectedDragAndDropActionChangedSpy.count(), 2); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Copy); break; case KWayland::Client::DataDeviceManager::DnDAction::Copy: case KWayland::Client::DataDeviceManager::DnDAction::Move: { const auto oppositeAction = action == KWayland::Client::DataDeviceManager::DnDAction::Copy ? KWayland::Client::DataDeviceManager::DnDAction::Move : KWayland::Client::DataDeviceManager::DnDAction::Copy; offer->setDragAndDropActions(oppositeAction, oppositeAction); QVERIFY(!dataSourceSelectedDragAndDropActionChangedSpy.wait(10)); break; } case KWayland::Client::DataDeviceManager::DnDAction::None: Q_UNREACHABLE(); } // Finish drag-and-drop. QSignalSpy dragAndDropFinishedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropFinished); offer->dragAndDropFinished(); QVERIFY(dragAndDropFinishedSpy.wait()); } void DndTest::inertPointerDragOffer() { // This test verifies that requests from a previous data offer don't affect the current drag-and-drop operation status. QVERIFY(Test::waitForWaylandPointer()); // Setup the data source. auto pointer = Test::waylandSeat()->createPointer(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy pointerButtonSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy sourceTargetsAcceptsSpy(dataSource, &KWayland::Client::DataSource::targetAccepts); QSignalSpy dataSourceSelectedDragAndDropActionChangedSpy(dataSource, &KWayland::Client::DataSource::selectedDragAndDropActionChanged); // Setup the source window. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Move the pointer to the center of the surface and press the left mouse button. quint32 timestamp = 0; Test::pointerMotion(QPointF(50, 50), timestamp++); Test::pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(pointerButtonSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(pointerButtonSpy.last().at(0).value(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); const auto firstOffer = dataDevice->takeDragOffer(); firstOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); firstOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); firstOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Move, KWayland::Client::DataDeviceManager::DnDAction::Move); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); // Move the pointer outside the surface. Test::pointerMotion(QPointF(150, 50), timestamp++); QVERIFY(dataDeviceDragLeftSpy.wait()); firstOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(!sourceTargetsAcceptsSpy.wait(10)); firstOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(!sourceTargetsAcceptsSpy.wait(10)); firstOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(!dataSourceSelectedDragAndDropActionChangedSpy.wait(10)); // Move the pointer back inside the surface, it will result in a new data offer. Test::pointerMotion(QPointF(75, 50), timestamp++); QVERIFY(dataDeviceDragEnteredSpy.wait()); const auto secondOffer = dataDevice->takeDragOffer(); secondOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); secondOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); secondOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Move, KWayland::Client::DataDeviceManager::DnDAction::Move); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); // The first offer shouldn't change the accepted mime type or selected action. firstOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(!sourceTargetsAcceptsSpy.wait(10)); firstOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(!sourceTargetsAcceptsSpy.wait(10)); firstOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(!dataSourceSelectedDragAndDropActionChangedSpy.wait(10)); // Drop. Test::pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(dataDeviceDroppedSpy.wait()); // Finish drag-and-drop. QSignalSpy dragAndDropFinishedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropFinished); firstOffer->dragAndDropFinished(); QVERIFY(!dragAndDropFinishedSpy.wait(10)); secondOffer->dragAndDropFinished(); QVERIFY(dragAndDropFinishedSpy.wait()); } void DndTest::invalidSerialForPointerDrag() { // This test verifies that the data source will be canceled if a dnd operation is forbidden. QVERIFY(Test::waitForWaylandPointer()); // Setup the data source. auto pointer = Test::waylandSeat()->createPointer(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); // Setup a window with a subsurface that covers the right half. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Move the pointer to the center of the subsurface and click the left mouse button. QSignalSpy pointerButtonSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); quint32 timestamp = 0; Test::pointerMotion(QPointF(75, 50), timestamp++); Test::pointerButtonPressed(BTN_LEFT, timestamp++); Test::pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(pointerButtonSpy.wait()); // The dnd operation will be rejected because LMB is already released. QSignalSpy dataSourceCanceledSpy(dataSource, &KWayland::Client::DataSource::cancelled); dataDevice->startDrag(pointerButtonSpy.last().at(0).value(), dataSource, surface.get()); QVERIFY(dataSourceCanceledSpy.wait()); } void DndTest::cancelPointerDrag() { // This test verifies that a drag-and-operation will be canceled after pressing the Esc key. QVERIFY(Test::waitForWaylandPointer()); // Setup the data source. auto pointer = Test::waylandSeat()->createPointer(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy pointerButtonSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy dataSourceCancelledSpy(dataSource, &KWayland::Client::DataSource::cancelled); // Setup the source window. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Move the pointer to the center of the surface and press the left mouse button. quint32 timestamp = 0; Test::pointerMotion(QPointF(50, 50), timestamp++); Test::pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(pointerButtonSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(pointerButtonSpy.last().at(0).value(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(50, 50)); QCOMPARE(dataDevice->dragSurface(), surface.get()); // Press the Esc key. Test::keyboardKeyPressed(KEY_ESC, timestamp++); Test::keyboardKeyReleased(KEY_ESC, timestamp++); QVERIFY(dataDeviceDragLeftSpy.wait()); QCOMPARE(dataSourceCancelledSpy.count(), 1); QCOMPARE(dataDeviceDroppedSpy.count(), 0); Test::pointerButtonReleased(BTN_LEFT, timestamp++); } void DndTest::destroyPointerDragSource() { // This test verifies that a drag-and-drop operation will be cancelled when the source is destroyed. QVERIFY(Test::waitForWaylandPointer()); // Setup the data source. auto pointer = Test::waylandSeat()->createPointer(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy pointerButtonSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); // Setup the source window. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Move the pointer to the center of the surface and press the left mouse button. quint32 timestamp = 0; Test::pointerMotion(QPointF(50, 50), timestamp++); Test::pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(pointerButtonSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(pointerButtonSpy.last().at(0).value(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(50, 50)); QCOMPARE(dataDevice->dragSurface(), surface.get()); // Nuke the data source. delete dataSource; dataSource = nullptr; QVERIFY(dataDeviceDragLeftSpy.wait()); QCOMPARE(dataDeviceDroppedSpy.count(), 0); Test::pointerButtonReleased(BTN_LEFT, timestamp++); } void DndTest::noAcceptedMimeTypePointerDrag() { // This test verifies that a drag-and-drop operation will be canceled if no mime type has been accepted. QVERIFY(Test::waitForWaylandPointer()); // Setup the data source. auto pointer = Test::waylandSeat()->createPointer(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy pointerButtonSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy dataSourceCancelledSpy(dataSource, &KWayland::Client::DataSource::cancelled); QSignalSpy dataSourceDropPerformedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropPerformed); // Setup the source window. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Move the pointer to the center of the surface and press the left mouse button. quint32 timestamp = 0; Test::pointerMotion(QPointF(50, 50), timestamp++); Test::pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(pointerButtonSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(pointerButtonSpy.last().at(0).value(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(50, 50)); QCOMPARE(dataDevice->dragSurface(), surface.get()); // Without accepted mime type, the drag should be canceled when the LMB is released. QSignalSpy dataSourceSelectedDragAndDropActionChangedSpy(dataSource, &KWayland::Client::DataSource::selectedDragAndDropActionChanged); auto offer = dataDevice->takeDragOffer(); offer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); // Finish the drag-and-drop operation. Test::pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(dataDeviceDragLeftSpy.wait()); QCOMPARE(dataDeviceDroppedSpy.count(), 0); QCOMPARE(dataSourceCancelledSpy.count(), 1); QCOMPARE(dataSourceDropPerformedSpy.count(), 1); } void DndTest::secondPointerDrag() { // This test verifies that no second drag operation will be started while there's another active drag. QVERIFY(Test::waitForWaylandPointer()); // Setup the data source. auto pointer = Test::waylandSeat()->createPointer(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy pointerButtonSpy(pointer, &KWayland::Client::Pointer::buttonStateChanged); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy dataSourceCancelledSpy(dataSource, &KWayland::Client::DataSource::cancelled); QSignalSpy dataSourceDropPerformedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropPerformed); auto secondDataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); secondDataSource->offer(QStringLiteral("text/plain")); secondDataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); // Setup the source window. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Move the pointer to the center of the surface and press the left mouse button. quint32 timestamp = 0; Test::pointerMotion(QPointF(50, 50), timestamp++); Test::pointerButtonPressed(BTN_LEFT, timestamp++); QVERIFY(pointerButtonSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(pointerButtonSpy.last().at(0).value(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(50, 50)); QCOMPARE(dataDevice->dragSurface(), surface.get()); // Attempt to start another drag-and-drop operation. QSignalSpy secondDataSourceCancelledSpy(secondDataSource, &KWayland::Client::DataSource::cancelled); dataDevice->startDrag(pointerButtonSpy.last().at(0).value(), secondDataSource, surface.get()); QVERIFY(secondDataSourceCancelledSpy.wait()); QCOMPARE(dataSourceCancelledSpy.count(), 0); // Finish the drag-and-drop operation. Test::pointerButtonReleased(BTN_LEFT, timestamp++); QVERIFY(dataDeviceDragLeftSpy.wait()); QCOMPARE(dataDeviceDroppedSpy.count(), 0); QCOMPARE(dataSourceCancelledSpy.count(), 1); QCOMPARE(dataSourceDropPerformedSpy.count(), 1); } void DndTest::touchDrag() { // This test verifies that a simple touch drag-and-drop operation between two Wayland surfaces works as expected. QVERIFY(Test::waitForWaylandTouch()); // Setup the data source. auto touch = Test::waylandSeat()->createTouch(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); connect(dataSource, &KWayland::Client::DataSource::sendDataRequested, this, [](const QString &mimeType, int fd) { if (mimeType == QLatin1String("text/plain")) { const auto payload = QByteArrayLiteral("foo"); write(fd, payload.data(), payload.size()); } close(fd); }); QSignalSpy touchSequenceStartedSpy(touch, &KWayland::Client::Touch::sequenceStarted); QSignalSpy touchSequenceEndedSpy(touch, &KWayland::Client::Touch::sequenceEnded); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDragMotionSpy(dataDevice, &KWayland::Client::DataDevice::dragMotion); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); // Setup the source client. auto sourceSurface = Test::createSurface(); auto sourceShellSurface = Test::createXdgToplevelSurface(sourceSurface.get()); auto sourceWindow = Test::renderAndWaitForShown(sourceSurface.get(), QSize(100, 100), Qt::red); QVERIFY(sourceWindow); sourceWindow->move(QPointF(0, 0)); // Setup the target client. auto targetSurface = Test::createSurface(); auto targetShellSurface = Test::createXdgToplevelSurface(targetSurface.get()); auto targetWindow = Test::renderAndWaitForShown(targetSurface.get(), QSize(100, 100), Qt::red); QVERIFY(targetWindow); targetWindow->move(QPointF(100, 0)); // Tap the source surface. quint32 timestamp = 0; Test::touchDown(0, QPointF(50, 50), timestamp++); QVERIFY(touchSequenceStartedSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(touchSequenceStartedSpy.last().at(0).value()->downSerial(), dataSource, sourceSurface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(50, 50)); QCOMPARE(dataDevice->dragSurface(), sourceSurface.get()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 1); QCOMPARE(dataDeviceDragMotionSpy.count(), 0); QCOMPARE(dataDeviceDragLeftSpy.count(), 0); QCOMPARE(dataDeviceDroppedSpy.count(), 0); // Accept our own offer. auto sourceOffer = dataDevice->takeDragOffer(); QCOMPARE(sourceOffer->offeredMimeTypes(), (QList{m_mimeDb.mimeTypeForName(QStringLiteral("text/plain"))})); QCOMPARE(sourceOffer->sourceDragAndDropActions(), KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(sourceOffer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::None); QSignalSpy sourceTargetsAcceptsSpy(dataSource, &KWayland::Client::DataSource::targetAccepts); sourceOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); sourceOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); QSignalSpy sourceOfferSelectedDragAndDropActionSpy(sourceOffer.get(), &KWayland::Client::DataOffer::selectedDragAndDropActionChanged); sourceOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(sourceOfferSelectedDragAndDropActionSpy.wait()); QCOMPARE(sourceOffer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Copy); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Copy); // Move the finger a little bit. Test::touchMotion(0, QPointF(60, 50), timestamp++); QVERIFY(dataDeviceDragMotionSpy.wait()); QCOMPARE(dataDeviceDragMotionSpy.last().at(0).value(), QPointF(60, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 1); QCOMPARE(dataDeviceDragMotionSpy.count(), 1); QCOMPARE(dataDeviceDragLeftSpy.count(), 0); QCOMPARE(dataDeviceDroppedSpy.count(), 0); // No drag motion event will be sent if an unrelated finger moves. Test::touchDown(1, QPointF(40, 40), timestamp++); Test::touchMotion(1, QPointF(45, 45), timestamp++); Test::touchUp(1, timestamp++); QVERIFY(!dataDeviceDragMotionSpy.wait(10)); // Move the finger to the target surface. Test::touchMotion(0, QPointF(140, 50), timestamp++); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(40, 50)); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDevice->dragSurface(), targetSurface.get()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragMotionSpy.count(), 1); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDeviceDroppedSpy.count(), 0); // Accept the offer. auto targetOffer = dataDevice->takeDragOffer(); QCOMPARE(targetOffer->offeredMimeTypes(), (QList{m_mimeDb.mimeTypeForName(QStringLiteral("text/plain"))})); QCOMPARE(targetOffer->sourceDragAndDropActions(), KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(targetOffer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::None); targetOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); targetOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); QSignalSpy targetOfferSelectedDragAndDropActionSpy(targetOffer.get(), &KWayland::Client::DataOffer::selectedDragAndDropActionChanged); targetOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Move, KWayland::Client::DataDeviceManager::DnDAction::Move); QVERIFY(targetOfferSelectedDragAndDropActionSpy.wait()); QCOMPARE(targetOffer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); // Move the finger a little bit. Test::touchMotion(0, QPointF(160, 50), timestamp++); QVERIFY(dataDeviceDragMotionSpy.wait()); QCOMPARE(dataDeviceDragMotionSpy.last().at(0).value(), QPointF(60, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragMotionSpy.count(), 2); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDeviceDroppedSpy.count(), 0); // Drop. Test::touchUp(0, timestamp++); QVERIFY(touchSequenceEndedSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragMotionSpy.count(), 2); QCOMPARE(dataDeviceDragLeftSpy.count(), 2); QCOMPARE(dataDeviceDroppedSpy.count(), 1); // Ask for data. const QFuture plainData = readMimeTypeData(targetOffer.get(), QStringLiteral("text/plain")); QVERIFY(waitFuture(plainData)); QCOMPARE(plainData.result(), QByteArrayLiteral("foo")); // text/html hasn't been offered, so this should fail. const QFuture htmlData = readMimeTypeData(targetOffer.get(), QStringLiteral("text/html")); QVERIFY(waitFuture(htmlData)); QCOMPARE(htmlData.result(), QByteArray()); // Finish drag-and-drop. QSignalSpy dragAndDropFinishedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropFinished); targetOffer->dragAndDropFinished(); QVERIFY(dragAndDropFinishedSpy.wait()); } void DndTest::touchSubSurfaceDrag() { // This test verifies that a drag-and-drop originating from a sub-surface works as expected. QVERIFY(Test::waitForWaylandTouch()); // Setup the data source. auto touch = Test::waylandSeat()->createTouch(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy touchSequenceStartedSpy(touch, &KWayland::Client::Touch::sequenceStarted); QSignalSpy touchSequenceEndedSpy(touch, &KWayland::Client::Touch::sequenceEnded); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDragMotionSpy(dataDevice, &KWayland::Client::DataDevice::dragMotion); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); // Setup a window with a subsurface that covers the right half. auto parentSurface = Test::createSurface(); auto parentShellSurface = Test::createXdgToplevelSurface(parentSurface.get()); auto window = Test::renderAndWaitForShown(parentSurface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); auto childSurface = Test::createSurface(); auto subSurface = Test::createSubSurface(childSurface.get(), parentSurface.get()); subSurface->setPosition(QPoint(50, 0)); Test::render(childSurface.get(), QSize(50, 100), Qt::blue); parentSurface->commit(KWayland::Client::Surface::CommitFlag::None); // Tap the child surface. quint32 timestamp = 0; Test::touchDown(0, QPointF(75, 50), timestamp++); QVERIFY(touchSequenceStartedSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(touchSequenceStartedSpy.last().at(0).value()->downSerial(), dataSource, childSurface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(25, 50)); QCOMPARE(dataDevice->dragSurface(), childSurface.get()); // Move the finger to the parent surface. Test::touchMotion(0, QPointF(10, 50), timestamp++); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(10, 50)); QCOMPARE(dataDevice->dragSurface(), parentSurface.get()); Test::touchMotion(0, QPointF(15, 50), timestamp++); QVERIFY(dataDeviceDragMotionSpy.wait()); QCOMPARE(dataDeviceDragMotionSpy.last().at(0).value(), QPointF(15, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDevice->dragSurface(), parentSurface.get()); // Move the finger to the child surface. Test::touchMotion(0, QPointF(80, 50), timestamp++); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 3); QCOMPARE(dataDeviceDragLeftSpy.count(), 2); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(30, 50)); QCOMPARE(dataDevice->dragSurface(), childSurface.get()); Test::touchMotion(0, QPointF(75, 50), timestamp++); QVERIFY(dataDeviceDragMotionSpy.wait()); QCOMPARE(dataDeviceDragMotionSpy.last().at(0).value(), QPointF(25, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 3); QCOMPARE(dataDeviceDragLeftSpy.count(), 2); QCOMPARE(dataDevice->dragSurface(), childSurface.get()); // Accept the data offer. auto offer = dataDevice->takeDragOffer(); QCOMPARE(offer->offeredMimeTypes(), (QList{m_mimeDb.mimeTypeForName(QStringLiteral("text/plain"))})); QCOMPARE(offer->sourceDragAndDropActions(), KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(offer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::None); QSignalSpy sourceTargetsAcceptsSpy(dataSource, &KWayland::Client::DataSource::targetAccepts); offer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); offer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); QSignalSpy targetOfferSelectedDragAndDropActionSpy(offer.get(), &KWayland::Client::DataOffer::selectedDragAndDropActionChanged); offer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Move, KWayland::Client::DataDeviceManager::DnDAction::Move); QVERIFY(targetOfferSelectedDragAndDropActionSpy.wait()); QCOMPARE(offer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); // Drop. Test::touchUp(0, timestamp++); QVERIFY(touchSequenceEndedSpy.wait()); QCOMPARE(dataDeviceDroppedSpy.count(), 1); // Finish drag-and-drop. QSignalSpy dragAndDropFinishedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropFinished); offer->dragAndDropFinished(); QVERIFY(dragAndDropFinishedSpy.wait()); } void DndTest::touchDragAction_data() { QTest::addColumn("action"); QTest::addRow("ask") << KWayland::Client::DataDeviceManager::DnDAction::Ask; QTest::addRow("copy") << KWayland::Client::DataDeviceManager::DnDAction::Copy; QTest::addRow("move") << KWayland::Client::DataDeviceManager::DnDAction::Move; } void DndTest::touchDragAction() { // This test verifies that the dnd actions are handled as expected. "Ask" action is a bit // special; if it has been selected, then the final action can be set after the drop. QVERIFY(Test::waitForWaylandTouch()); QFETCH(KWayland::Client::DataDeviceManager::DnDAction, action); // Setup the data source. auto touch = Test::waylandSeat()->createTouch(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move | KWayland::Client::DataDeviceManager::DnDAction::Ask); QSignalSpy touchSequenceStartedSpy(touch, &KWayland::Client::Touch::sequenceStarted); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy dataSourceSelectedDragAndDropActionChangedSpy(dataSource, &KWayland::Client::DataSource::selectedDragAndDropActionChanged); // Setup the source window. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Touch the surface in the center. quint32 timestamp = 0; Test::touchDown(0, QPointF(50, 50), timestamp++); QVERIFY(touchSequenceStartedSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(touchSequenceStartedSpy.last().at(0).value()->downSerial(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); const auto offer = dataDevice->takeDragOffer(); offer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); offer->setDragAndDropActions(action, action); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); QCOMPARE(dataSourceSelectedDragAndDropActionChangedSpy.count(), 1); QCOMPARE(dataSource->selectedDragAndDropAction(), action); // Drop. Test::touchUp(0, timestamp++); QVERIFY(dataDeviceDroppedSpy.wait()); QCOMPARE(dataSourceSelectedDragAndDropActionChangedSpy.count(), 1); // Change action after drop. switch (action) { case KWayland::Client::DataDeviceManager::DnDAction::Ask: offer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); QCOMPARE(dataSourceSelectedDragAndDropActionChangedSpy.count(), 2); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Copy); break; case KWayland::Client::DataDeviceManager::DnDAction::Copy: case KWayland::Client::DataDeviceManager::DnDAction::Move: { const auto oppositeAction = action == KWayland::Client::DataDeviceManager::DnDAction::Copy ? KWayland::Client::DataDeviceManager::DnDAction::Move : KWayland::Client::DataDeviceManager::DnDAction::Copy; offer->setDragAndDropActions(oppositeAction, oppositeAction); QVERIFY(!dataSourceSelectedDragAndDropActionChangedSpy.wait(10)); break; } case KWayland::Client::DataDeviceManager::DnDAction::None: Q_UNREACHABLE(); } // Finish drag-and-drop. QSignalSpy dragAndDropFinishedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropFinished); offer->dragAndDropFinished(); QVERIFY(dragAndDropFinishedSpy.wait()); } void DndTest::inertTouchDragOffer() { // This test verifies that requests from a previous data offer don't affect the current drag-and-drop operation status. QVERIFY(Test::waitForWaylandTouch()); // Setup the data source. auto touch = Test::waylandSeat()->createTouch(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy touchSequenceStartedSpy(touch, &KWayland::Client::Touch::sequenceStarted); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy sourceTargetsAcceptsSpy(dataSource, &KWayland::Client::DataSource::targetAccepts); QSignalSpy dataSourceSelectedDragAndDropActionChangedSpy(dataSource, &KWayland::Client::DataSource::selectedDragAndDropActionChanged); // Setup the source window. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Tap the surface in the center. quint32 timestamp = 0; Test::touchDown(0, QPointF(50, 50), timestamp++); QVERIFY(touchSequenceStartedSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(touchSequenceStartedSpy.last().at(0).value()->downSerial(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); const auto firstOffer = dataDevice->takeDragOffer(); firstOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); firstOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); firstOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Move, KWayland::Client::DataDeviceManager::DnDAction::Move); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); // Move the finger outside the surface. Test::touchMotion(0, QPointF(150, 50), timestamp++); QVERIFY(dataDeviceDragLeftSpy.wait()); firstOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(!sourceTargetsAcceptsSpy.wait(10)); firstOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(!sourceTargetsAcceptsSpy.wait(10)); firstOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(!dataSourceSelectedDragAndDropActionChangedSpy.wait(10)); // Move the finger back inside the surface, it will result in a new data offer. Test::touchMotion(0, QPointF(75, 50), timestamp++); QVERIFY(dataDeviceDragEnteredSpy.wait()); const auto secondOffer = dataDevice->takeDragOffer(); secondOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); secondOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); secondOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Move, KWayland::Client::DataDeviceManager::DnDAction::Move); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); // The first offer shouldn't change the accepted mime type or selected action. firstOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(!sourceTargetsAcceptsSpy.wait(10)); firstOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(!sourceTargetsAcceptsSpy.wait(10)); firstOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(!dataSourceSelectedDragAndDropActionChangedSpy.wait(10)); // Drop. Test::touchUp(0, timestamp++); QVERIFY(dataDeviceDroppedSpy.wait()); // Finish drag-and-drop. QSignalSpy dragAndDropFinishedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropFinished); firstOffer->dragAndDropFinished(); QVERIFY(!dragAndDropFinishedSpy.wait(10)); secondOffer->dragAndDropFinished(); QVERIFY(dragAndDropFinishedSpy.wait()); } void DndTest::invalidSerialForTouchDrag() { // This test verifies that the data source will be canceled if a dnd operation is forbidden. QVERIFY(Test::waitForWaylandTouch()); // Setup the data source. auto touch = Test::waylandSeat()->createTouch(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); // Setup a window with a subsurface that covers the right half. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Tap the surface. QSignalSpy touchSequenceStartedSpy(touch, &KWayland::Client::Touch::sequenceStarted); quint32 timestamp = 0; Test::touchDown(0, QPointF(75, 50), timestamp++); Test::touchUp(0, timestamp++); QVERIFY(touchSequenceStartedSpy.wait()); // The dnd operation will be rejected because the touch point is already released. QSignalSpy dataSourceCanceledSpy(dataSource, &KWayland::Client::DataSource::cancelled); dataDevice->startDrag(touchSequenceStartedSpy.last().at(0).value()->downSerial(), dataSource, surface.get()); QVERIFY(dataSourceCanceledSpy.wait()); } void DndTest::noAcceptedMimeTypeTouchDrag() { // This test verifies that a drag-and-drop operation will be cancelled if no mime type is accepted. QVERIFY(Test::waitForWaylandTouch()); // Setup the data source. auto touch = Test::waylandSeat()->createTouch(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy touchSequenceStartedSpy(touch, &KWayland::Client::Touch::sequenceStarted); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDragMotionSpy(dataDevice, &KWayland::Client::DataDevice::dragMotion); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy dataSourceCancelledSpy(dataSource, &KWayland::Client::DataSource::cancelled); QSignalSpy dataSourceDropPerformedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropPerformed); // Create a surface. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Tap the surface. quint32 timestamp = 0; Test::touchDown(0, QPointF(50, 50), timestamp++); QVERIFY(touchSequenceStartedSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(touchSequenceStartedSpy.last().at(0).value()->downSerial(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(50, 50)); QCOMPARE(dataDevice->dragSurface(), surface.get()); // Without accepted mime type, the drag should be canceled when the finger is lifted. QSignalSpy dataSourceSelectedDragAndDropActionChangedSpy(dataSource, &KWayland::Client::DataSource::selectedDragAndDropActionChanged); auto offer = dataDevice->takeDragOffer(); offer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); // Drop. Test::touchUp(0, timestamp++); QVERIFY(dataDeviceDragLeftSpy.wait()); QCOMPARE(dataDeviceDroppedSpy.count(), 0); QCOMPARE(dataSourceCancelledSpy.count(), 1); QCOMPARE(dataSourceDropPerformedSpy.count(), 1); } void DndTest::secondTouchDrag() { // This test verifies that no second drag operation will be started while there's another active drag. QVERIFY(Test::waitForWaylandTouch()); // Setup the data source. auto touch = Test::waylandSeat()->createTouch(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy touchSequenceStartedSpy(touch, &KWayland::Client::Touch::sequenceStarted); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDragMotionSpy(dataDevice, &KWayland::Client::DataDevice::dragMotion); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy dataSourceCancelledSpy(dataSource, &KWayland::Client::DataSource::cancelled); QSignalSpy dataSourceDropPerformedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropPerformed); auto secondDataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); secondDataSource->offer(QStringLiteral("text/plain")); secondDataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); // Create a surface. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Tap the surface. quint32 timestamp = 0; Test::touchDown(0, QPointF(50, 50), timestamp++); QVERIFY(touchSequenceStartedSpy.wait()); // Start a drag-and-drop operation. const quint32 downSerial = touchSequenceStartedSpy.last().at(0).value()->downSerial(); dataDevice->startDrag(downSerial, dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(50, 50)); QCOMPARE(dataDevice->dragSurface(), surface.get()); // Attempt to start another drag-and-drop operation. QSignalSpy secondDataSourceCancelledSpy(secondDataSource, &KWayland::Client::DataSource::cancelled); dataDevice->startDrag(downSerial, secondDataSource, surface.get()); QVERIFY(secondDataSourceCancelledSpy.wait()); QCOMPARE(dataSourceCancelledSpy.count(), 0); // Drop. Test::touchUp(0, timestamp++); QVERIFY(dataDeviceDragLeftSpy.wait()); QCOMPARE(dataDeviceDroppedSpy.count(), 0); QCOMPARE(dataSourceCancelledSpy.count(), 1); QCOMPARE(dataSourceDropPerformedSpy.count(), 1); } void DndTest::cancelTouchSequence() { // This test verifies that a drag-and-drop session will be cancelled when the input backend generates a touch cancel event. QVERIFY(Test::waitForWaylandTouch()); // Setup the data source. auto touch = Test::waylandSeat()->createTouch(Test::waylandSeat()); auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy touchSequenceStartedSpy(touch, &KWayland::Client::Touch::sequenceStarted); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy dataSourceCancelledSpy(dataSource, &KWayland::Client::DataSource::cancelled); QSignalSpy dataSourceDropPerformedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropPerformed); // Create a surface. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Tap the surface. quint32 timestamp = 0; Test::touchDown(0, QPointF(50, 50), timestamp++); QVERIFY(touchSequenceStartedSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(touchSequenceStartedSpy.last().at(0).value()->downSerial(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(50, 50)); QCOMPARE(dataDevice->dragSurface(), surface.get()); // Cancel the touch sequence. Test::touchCancel(); QVERIFY(dataDeviceDragLeftSpy.wait()); QCOMPARE(dataDeviceDroppedSpy.count(), 0); QCOMPARE(dataSourceCancelledSpy.count(), 1); QCOMPARE(dataSourceDropPerformedSpy.count(), 0); } void DndTest::tabletDrag() { // This test verifies that a simple tablet drag-and-drop operation between two Wayland surfaces works as expected. std::unique_ptr tabletSeat = Test::tabletManager()->createSeat(Test::waylandSeat()); QSignalSpy toolAddedSpy(tabletSeat.get(), &Test::WpTabletSeatV2::toolAdded); QVERIFY(toolAddedSpy.wait()); Test::WpTabletToolV2 *tabletTool = toolAddedSpy.last().at(0).value(); QVERIFY(Test::waitForWaylandTabletTool(tabletTool)); // Setup the data source. auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); connect(dataSource, &KWayland::Client::DataSource::sendDataRequested, this, [](const QString &mimeType, int fd) { if (mimeType == QLatin1String("text/plain")) { const auto payload = QByteArrayLiteral("foo"); write(fd, payload.data(), payload.size()); } close(fd); }); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDragMotionSpy(dataDevice, &KWayland::Client::DataDevice::dragMotion); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); // Setup the source client. auto sourceSurface = Test::createSurface(); auto sourceShellSurface = Test::createXdgToplevelSurface(sourceSurface.get()); auto sourceWindow = Test::renderAndWaitForShown(sourceSurface.get(), QSize(100, 100), Qt::red); QVERIFY(sourceWindow); sourceWindow->move(QPointF(0, 0)); // Setup the target client. auto targetSurface = Test::createSurface(); auto targetShellSurface = Test::createXdgToplevelSurface(targetSurface.get()); auto targetWindow = Test::renderAndWaitForShown(targetSurface.get(), QSize(100, 100), Qt::red); QVERIFY(targetWindow); targetWindow->move(QPointF(100, 0)); // Press the tip of the tablet tool in the center of the window. QSignalSpy tabletToolDownSpy(tabletTool, &Test::WpTabletToolV2::down); quint32 timestamp = 0; Test::tabletToolProximityEvent(QPointF(50, 50), 0, 0, 0, 0, true, 0, timestamp++); Test::tabletToolTipEvent(QPointF(50, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(tabletToolDownSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(tabletToolDownSpy.last().at(0).value(), dataSource, sourceSurface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(50, 50)); QCOMPARE(dataDevice->dragSurface(), sourceSurface.get()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 1); QCOMPARE(dataDeviceDragMotionSpy.count(), 0); QCOMPARE(dataDeviceDragLeftSpy.count(), 0); QCOMPARE(dataDeviceDroppedSpy.count(), 0); // Accept our own offer. auto sourceOffer = dataDevice->takeDragOffer(); QCOMPARE(sourceOffer->offeredMimeTypes(), (QList{m_mimeDb.mimeTypeForName(QStringLiteral("text/plain"))})); QCOMPARE(sourceOffer->sourceDragAndDropActions(), KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(sourceOffer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::None); QSignalSpy sourceTargetsAcceptsSpy(dataSource, &KWayland::Client::DataSource::targetAccepts); sourceOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); sourceOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); QSignalSpy sourceOfferSelectedDragAndDropActionSpy(sourceOffer.get(), &KWayland::Client::DataOffer::selectedDragAndDropActionChanged); sourceOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(sourceOfferSelectedDragAndDropActionSpy.wait()); QCOMPARE(sourceOffer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Copy); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Copy); // Move the tablet tool a little bit. Test::tabletToolAxisEvent(QPointF(60, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(dataDeviceDragMotionSpy.wait()); QCOMPARE(dataDeviceDragMotionSpy.last().at(0).value(), QPointF(60, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 1); QCOMPARE(dataDeviceDragMotionSpy.count(), 1); QCOMPARE(dataDeviceDragLeftSpy.count(), 0); QCOMPARE(dataDeviceDroppedSpy.count(), 0); // Move the tablet tool to the target surface. Test::tabletToolAxisEvent(QPointF(140, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(40, 50)); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDevice->dragSurface(), targetSurface.get()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragMotionSpy.count(), 1); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDeviceDroppedSpy.count(), 0); // Accept the offer. auto targetOffer = dataDevice->takeDragOffer(); QCOMPARE(targetOffer->offeredMimeTypes(), (QList{m_mimeDb.mimeTypeForName(QStringLiteral("text/plain"))})); QCOMPARE(targetOffer->sourceDragAndDropActions(), KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(targetOffer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::None); targetOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); targetOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); QSignalSpy targetOfferSelectedDragAndDropActionSpy(targetOffer.get(), &KWayland::Client::DataOffer::selectedDragAndDropActionChanged); targetOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Move, KWayland::Client::DataDeviceManager::DnDAction::Move); QVERIFY(targetOfferSelectedDragAndDropActionSpy.wait()); QCOMPARE(targetOffer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); // Move the tablet tool a little bit. Test::tabletToolAxisEvent(QPointF(160, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(dataDeviceDragMotionSpy.wait()); QCOMPARE(dataDeviceDragMotionSpy.last().at(0).value(), QPointF(60, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragMotionSpy.count(), 2); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDeviceDroppedSpy.count(), 0); // Drop. Test::tabletToolTipEvent(QPointF(160, 50), 0, 0, 0, 0, 0, false, 0, timestamp++); Test::tabletToolProximityEvent(QPointF(160, 50), 0, 0, 0, 0, false, 0, timestamp++); QVERIFY(dataDeviceDroppedSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragMotionSpy.count(), 2); QCOMPARE(dataDeviceDragLeftSpy.count(), 2); QCOMPARE(dataDeviceDroppedSpy.count(), 1); // Ask for data. const QFuture plainData = readMimeTypeData(targetOffer.get(), QStringLiteral("text/plain")); QVERIFY(waitFuture(plainData)); QCOMPARE(plainData.result(), QByteArrayLiteral("foo")); // text/html hasn't been offered, so this should fail. const QFuture htmlData = readMimeTypeData(targetOffer.get(), QStringLiteral("text/html")); QVERIFY(waitFuture(htmlData)); QCOMPARE(htmlData.result(), QByteArray()); // Finish drag-and-drop. QSignalSpy dragAndDropFinishedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropFinished); targetOffer->dragAndDropFinished(); QVERIFY(dragAndDropFinishedSpy.wait()); } void DndTest::tabletSubSurfaceDrag() { // This test verifies that a drag-and-drop originating from a sub-surface works as expected. std::unique_ptr tabletSeat = Test::tabletManager()->createSeat(Test::waylandSeat()); QSignalSpy toolAddedSpy(tabletSeat.get(), &Test::WpTabletSeatV2::toolAdded); QVERIFY(toolAddedSpy.wait()); Test::WpTabletToolV2 *tabletTool = toolAddedSpy.last().at(0).value(); QVERIFY(Test::waitForWaylandTabletTool(tabletTool)); // Setup the data source. auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDragMotionSpy(dataDevice, &KWayland::Client::DataDevice::dragMotion); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); // Setup a window with a subsurface that covers the right half. auto parentSurface = Test::createSurface(); auto parentShellSurface = Test::createXdgToplevelSurface(parentSurface.get()); auto window = Test::renderAndWaitForShown(parentSurface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); auto childSurface = Test::createSurface(); auto subSurface = Test::createSubSurface(childSurface.get(), parentSurface.get()); subSurface->setPosition(QPoint(50, 0)); Test::render(childSurface.get(), QSize(50, 100), Qt::blue); parentSurface->commit(KWayland::Client::Surface::CommitFlag::None); // Press the tip of the tablet tool in the center of the child surface. QSignalSpy tabletToolDownSpy(tabletTool, &Test::WpTabletToolV2::down); quint32 timestamp = 0; Test::tabletToolProximityEvent(QPointF(75, 50), 0, 0, 0, 0, true, 0, timestamp++); Test::tabletToolTipEvent(QPointF(75, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(tabletToolDownSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(tabletToolDownSpy.last().at(0).value(), dataSource, childSurface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(25, 50)); QCOMPARE(dataDevice->dragSurface(), childSurface.get()); // Move the tablet tool to the parent surface. Test::tabletToolAxisEvent(QPointF(10, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(10, 50)); QCOMPARE(dataDevice->dragSurface(), parentSurface.get()); Test::tabletToolAxisEvent(QPointF(15, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(dataDeviceDragMotionSpy.wait()); QCOMPARE(dataDeviceDragMotionSpy.last().at(0).value(), QPointF(15, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 2); QCOMPARE(dataDeviceDragLeftSpy.count(), 1); QCOMPARE(dataDevice->dragSurface(), parentSurface.get()); // Move the tablet tool to the child surface. Test::tabletToolAxisEvent(QPointF(80, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.count(), 3); QCOMPARE(dataDeviceDragLeftSpy.count(), 2); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(30, 50)); QCOMPARE(dataDevice->dragSurface(), childSurface.get()); Test::tabletToolAxisEvent(QPointF(75, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(dataDeviceDragMotionSpy.wait()); QCOMPARE(dataDeviceDragMotionSpy.last().at(0).value(), QPointF(25, 50)); QCOMPARE(dataDeviceDragEnteredSpy.count(), 3); QCOMPARE(dataDeviceDragLeftSpy.count(), 2); QCOMPARE(dataDevice->dragSurface(), childSurface.get()); // Accept the data offer. auto offer = dataDevice->takeDragOffer(); QCOMPARE(offer->offeredMimeTypes(), (QList{m_mimeDb.mimeTypeForName(QStringLiteral("text/plain"))})); QCOMPARE(offer->sourceDragAndDropActions(), KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(offer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::None); QSignalSpy sourceTargetsAcceptsSpy(dataSource, &KWayland::Client::DataSource::targetAccepts); offer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); offer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); QSignalSpy targetOfferSelectedDragAndDropActionSpy(offer.get(), &KWayland::Client::DataOffer::selectedDragAndDropActionChanged); offer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Move, KWayland::Client::DataDeviceManager::DnDAction::Move); QVERIFY(targetOfferSelectedDragAndDropActionSpy.wait()); QCOMPARE(offer->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); // Drop. Test::tabletToolTipEvent(QPointF(75, 50), 0, 0, 0, 0, 0, false, 0, timestamp++); Test::tabletToolProximityEvent(QPointF(75, 50), 0, 0, 0, 0, false, 0, timestamp++); QVERIFY(dataDeviceDroppedSpy.wait()); QCOMPARE(dataDeviceDroppedSpy.count(), 1); // Finish drag-and-drop. QSignalSpy dragAndDropFinishedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropFinished); offer->dragAndDropFinished(); QVERIFY(dragAndDropFinishedSpy.wait()); } void DndTest::tabletDragAction_data() { QTest::addColumn("action"); QTest::addRow("ask") << KWayland::Client::DataDeviceManager::DnDAction::Ask; QTest::addRow("copy") << KWayland::Client::DataDeviceManager::DnDAction::Copy; QTest::addRow("move") << KWayland::Client::DataDeviceManager::DnDAction::Move; } void DndTest::tabletDragAction() { // This test verifies that the dnd actions are handled as expected. "Ask" action is a bit // special; if it has been selected, then the final action can be set after the drop. QFETCH(KWayland::Client::DataDeviceManager::DnDAction, action); std::unique_ptr tabletSeat = Test::tabletManager()->createSeat(Test::waylandSeat()); QSignalSpy toolAddedSpy(tabletSeat.get(), &Test::WpTabletSeatV2::toolAdded); QVERIFY(toolAddedSpy.wait()); Test::WpTabletToolV2 *tabletTool = toolAddedSpy.last().at(0).value(); QVERIFY(Test::waitForWaylandTabletTool(tabletTool)); // Setup the data source. auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move | KWayland::Client::DataDeviceManager::DnDAction::Ask); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy dataSourceSelectedDragAndDropActionChangedSpy(dataSource, &KWayland::Client::DataSource::selectedDragAndDropActionChanged); // Setup the source window. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Press the tip of the tablet tool in the center of the window. QSignalSpy tabletToolDownSpy(tabletTool, &Test::WpTabletToolV2::down); quint32 timestamp = 0; Test::tabletToolProximityEvent(QPointF(50, 50), 0, 0, 0, 0, true, 0, timestamp++); Test::tabletToolTipEvent(QPointF(50, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(tabletToolDownSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(tabletToolDownSpy.last().at(0).value(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); const auto offer = dataDevice->takeDragOffer(); offer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); offer->setDragAndDropActions(action, action); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); QCOMPARE(dataSourceSelectedDragAndDropActionChangedSpy.count(), 1); QCOMPARE(dataSource->selectedDragAndDropAction(), action); // Drop. Test::tabletToolTipEvent(QPointF(50, 50), 0, 0, 0, 0, 0, false, 0, timestamp++); Test::tabletToolProximityEvent(QPointF(50, 50), 0, 0, 0, 0, false, 0, timestamp++); QVERIFY(dataDeviceDroppedSpy.wait()); QCOMPARE(dataSourceSelectedDragAndDropActionChangedSpy.count(), 1); // Change action after drop. switch (action) { case KWayland::Client::DataDeviceManager::DnDAction::Ask: offer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); QCOMPARE(dataSourceSelectedDragAndDropActionChangedSpy.count(), 2); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Copy); break; case KWayland::Client::DataDeviceManager::DnDAction::Copy: case KWayland::Client::DataDeviceManager::DnDAction::Move: { const auto oppositeAction = action == KWayland::Client::DataDeviceManager::DnDAction::Copy ? KWayland::Client::DataDeviceManager::DnDAction::Move : KWayland::Client::DataDeviceManager::DnDAction::Copy; offer->setDragAndDropActions(oppositeAction, oppositeAction); QVERIFY(!dataSourceSelectedDragAndDropActionChangedSpy.wait(10)); break; } case KWayland::Client::DataDeviceManager::DnDAction::None: Q_UNREACHABLE(); } // Finish drag-and-drop. QSignalSpy dragAndDropFinishedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropFinished); offer->dragAndDropFinished(); QVERIFY(dragAndDropFinishedSpy.wait()); } void DndTest::inertTabletDragOffer() { // This test verifies that requests from a previous data offer don't affect the current drag-and-drop operation status. std::unique_ptr tabletSeat = Test::tabletManager()->createSeat(Test::waylandSeat()); QSignalSpy toolAddedSpy(tabletSeat.get(), &Test::WpTabletSeatV2::toolAdded); QVERIFY(toolAddedSpy.wait()); Test::WpTabletToolV2 *tabletTool = toolAddedSpy.last().at(0).value(); QVERIFY(Test::waitForWaylandTabletTool(tabletTool)); // Setup the data source. auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy sourceTargetsAcceptsSpy(dataSource, &KWayland::Client::DataSource::targetAccepts); QSignalSpy dataSourceSelectedDragAndDropActionChangedSpy(dataSource, &KWayland::Client::DataSource::selectedDragAndDropActionChanged); // Setup the source window. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Press the tip of the tablet tool in the center of the window. QSignalSpy tabletToolDownSpy(tabletTool, &Test::WpTabletToolV2::down); quint32 timestamp = 0; Test::tabletToolProximityEvent(QPointF(50, 50), 0, 0, 0, 0, true, 0, timestamp++); Test::tabletToolTipEvent(QPointF(50, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(tabletToolDownSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(tabletToolDownSpy.last().at(0).value(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); const auto firstOffer = dataDevice->takeDragOffer(); firstOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); firstOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); firstOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Move, KWayland::Client::DataDeviceManager::DnDAction::Move); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); // Move the tablet tool tip outside the surface. Test::tabletToolAxisEvent(QPointF(150, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(dataDeviceDragLeftSpy.wait()); firstOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(!sourceTargetsAcceptsSpy.wait(10)); firstOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(!sourceTargetsAcceptsSpy.wait(10)); firstOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(!dataSourceSelectedDragAndDropActionChangedSpy.wait(10)); // Move the finger back inside the surface, it will result in a new data offer. Test::tabletToolAxisEvent(QPointF(75, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(dataDeviceDragEnteredSpy.wait()); const auto secondOffer = dataDevice->takeDragOffer(); secondOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QString()); secondOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(sourceTargetsAcceptsSpy.wait()); QCOMPARE(sourceTargetsAcceptsSpy.last().at(0).value(), QStringLiteral("text/plain")); secondOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Move, KWayland::Client::DataDeviceManager::DnDAction::Move); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); QCOMPARE(dataSource->selectedDragAndDropAction(), KWayland::Client::DataDeviceManager::DnDAction::Move); // The first offer shouldn't change the accepted mime type or selected action. firstOffer->accept(QString(), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(!sourceTargetsAcceptsSpy.wait(10)); firstOffer->accept(QStringLiteral("text/plain"), dataDeviceDragEnteredSpy.last().at(0).value()); QVERIFY(!sourceTargetsAcceptsSpy.wait(10)); firstOffer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(!dataSourceSelectedDragAndDropActionChangedSpy.wait(10)); // Drop. Test::tabletToolTipEvent(QPointF(75, 50), 0, 0, 0, 0, 0, false, 0, timestamp++); Test::tabletToolProximityEvent(QPointF(75, 50), 0, 0, 0, 0, false, 0, timestamp++); QVERIFY(dataDeviceDroppedSpy.wait()); // Finish drag-and-drop. QSignalSpy dragAndDropFinishedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropFinished); firstOffer->dragAndDropFinished(); QVERIFY(!dragAndDropFinishedSpy.wait(10)); secondOffer->dragAndDropFinished(); QVERIFY(dragAndDropFinishedSpy.wait()); } void DndTest::invalidSerialForTabletDrag() { // This test verifies that the data source will be canceled if a dnd operation is forbidden. std::unique_ptr tabletSeat = Test::tabletManager()->createSeat(Test::waylandSeat()); QSignalSpy toolAddedSpy(tabletSeat.get(), &Test::WpTabletSeatV2::toolAdded); QVERIFY(toolAddedSpy.wait()); Test::WpTabletToolV2 *tabletTool = toolAddedSpy.last().at(0).value(); QVERIFY(Test::waitForWaylandTabletTool(tabletTool)); // Setup the data source. auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); // Setup a window with a subsurface that covers the right half. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Press the tip of the tablet tool in the center of the window. QSignalSpy tabletToolDownSpy(tabletTool, &Test::WpTabletToolV2::down); quint32 timestamp = 0; Test::tabletToolProximityEvent(QPointF(50, 50), 0, 0, 0, 0, true, 0, timestamp++); Test::tabletToolTipEvent(QPointF(50, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); Test::tabletToolTipEvent(QPointF(50, 50), 1, 0, 0, 0, 0, false, 0, timestamp++); Test::tabletToolProximityEvent(QPointF(50, 50), 0, 0, 0, 0, false, 0, timestamp++); QVERIFY(tabletToolDownSpy.wait()); // The dnd operation will be rejected because the touch point is already released. QSignalSpy dataSourceCanceledSpy(dataSource, &KWayland::Client::DataSource::cancelled); dataDevice->startDrag(tabletToolDownSpy.last().at(0).value(), dataSource, surface.get()); QVERIFY(dataSourceCanceledSpy.wait()); } void DndTest::noAcceptedMimeTypeTabletDrag() { // This test verifies that a drag-and-drop operation will be cancelled if no mime type is accepted. std::unique_ptr tabletSeat = Test::tabletManager()->createSeat(Test::waylandSeat()); QSignalSpy toolAddedSpy(tabletSeat.get(), &Test::WpTabletSeatV2::toolAdded); QVERIFY(toolAddedSpy.wait()); Test::WpTabletToolV2 *tabletTool = toolAddedSpy.last().at(0).value(); QVERIFY(Test::waitForWaylandTabletTool(tabletTool)); // Setup the data source. auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDragMotionSpy(dataDevice, &KWayland::Client::DataDevice::dragMotion); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy dataSourceCancelledSpy(dataSource, &KWayland::Client::DataSource::cancelled); QSignalSpy dataSourceDropPerformedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropPerformed); // Create a surface. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Press the tip of the tablet tool in the center of the window. QSignalSpy tabletToolDownSpy(tabletTool, &Test::WpTabletToolV2::down); quint32 timestamp = 0; Test::tabletToolProximityEvent(QPointF(50, 50), 0, 0, 0, 0, true, 0, timestamp++); Test::tabletToolTipEvent(QPointF(50, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(tabletToolDownSpy.wait()); // Start a drag-and-drop operation. dataDevice->startDrag(tabletToolDownSpy.last().at(0).value(), dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(50, 50)); QCOMPARE(dataDevice->dragSurface(), surface.get()); // Without accepted mime type, the drag should be canceled when the finger is lifted. QSignalSpy dataSourceSelectedDragAndDropActionChangedSpy(dataSource, &KWayland::Client::DataSource::selectedDragAndDropActionChanged); auto offer = dataDevice->takeDragOffer(); offer->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy, KWayland::Client::DataDeviceManager::DnDAction::Copy); QVERIFY(dataSourceSelectedDragAndDropActionChangedSpy.wait()); // Drop. Test::tabletToolTipEvent(QPointF(50, 50), 0, 0, 0, 0, 0, false, 0, timestamp++); Test::tabletToolProximityEvent(QPointF(50, 50), 0, 0, 0, 0, false, 0, timestamp++); QVERIFY(dataDeviceDragLeftSpy.wait()); QCOMPARE(dataDeviceDroppedSpy.count(), 0); QCOMPARE(dataSourceCancelledSpy.count(), 1); QCOMPARE(dataSourceDropPerformedSpy.count(), 1); } void DndTest::secondTabletDrag() { // This test verifies that no second drag operation will be started while there's another active drag. std::unique_ptr tabletSeat = Test::tabletManager()->createSeat(Test::waylandSeat()); QSignalSpy toolAddedSpy(tabletSeat.get(), &Test::WpTabletSeatV2::toolAdded); QVERIFY(toolAddedSpy.wait()); Test::WpTabletToolV2 *tabletTool = toolAddedSpy.last().at(0).value(); QVERIFY(Test::waitForWaylandTabletTool(tabletTool)); // Setup the data source. auto dataDevice = Test::waylandDataDeviceManager()->getDataDevice(Test::waylandSeat(), Test::waylandSeat()); auto dataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); dataSource->offer(QStringLiteral("text/plain")); dataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); QSignalSpy dataDeviceDragEnteredSpy(dataDevice, &KWayland::Client::DataDevice::dragEntered); QSignalSpy dataDeviceDragLeftSpy(dataDevice, &KWayland::Client::DataDevice::dragLeft); QSignalSpy dataDeviceDragMotionSpy(dataDevice, &KWayland::Client::DataDevice::dragMotion); QSignalSpy dataDeviceDroppedSpy(dataDevice, &KWayland::Client::DataDevice::dropped); QSignalSpy dataSourceCancelledSpy(dataSource, &KWayland::Client::DataSource::cancelled); QSignalSpy dataSourceDropPerformedSpy(dataSource, &KWayland::Client::DataSource::dragAndDropPerformed); auto secondDataSource = Test::waylandDataDeviceManager()->createDataSource(Test::waylandSeat()); secondDataSource->offer(QStringLiteral("text/plain")); secondDataSource->setDragAndDropActions(KWayland::Client::DataDeviceManager::DnDAction::Copy | KWayland::Client::DataDeviceManager::DnDAction::Move); // Create a surface. auto surface = Test::createSurface(); auto shellSurface = Test::createXdgToplevelSurface(surface.get()); auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::red); QVERIFY(window); window->move(QPointF(0, 0)); // Press the tip of the tablet tool in the center of the window. QSignalSpy tabletToolDownSpy(tabletTool, &Test::WpTabletToolV2::down); quint32 timestamp = 0; Test::tabletToolProximityEvent(QPointF(50, 50), 0, 0, 0, 0, true, 0, timestamp++); Test::tabletToolTipEvent(QPointF(50, 50), 1, 0, 0, 0, 0, true, 0, timestamp++); QVERIFY(tabletToolDownSpy.wait()); // Start a drag-and-drop operation. const quint32 downSerial = tabletToolDownSpy.last().at(0).value(); dataDevice->startDrag(downSerial, dataSource, surface.get()); QVERIFY(dataDeviceDragEnteredSpy.wait()); QCOMPARE(dataDeviceDragEnteredSpy.last().at(1).value(), QPointF(50, 50)); QCOMPARE(dataDevice->dragSurface(), surface.get()); // Attempt to start another drag-and-drop operation. QSignalSpy secondDataSourceCancelledSpy(secondDataSource, &KWayland::Client::DataSource::cancelled); dataDevice->startDrag(downSerial, secondDataSource, surface.get()); QVERIFY(secondDataSourceCancelledSpy.wait()); QCOMPARE(dataSourceCancelledSpy.count(), 0); // Drop. Test::tabletToolTipEvent(QPointF(50, 50), 0, 0, 0, 0, 0, false, 0, timestamp++); Test::tabletToolProximityEvent(QPointF(50, 50), 0, 0, 0, 0, false, 0, timestamp++); QVERIFY(dataDeviceDragLeftSpy.wait()); QCOMPARE(dataDeviceDroppedSpy.count(), 0); QCOMPARE(dataSourceCancelledSpy.count(), 1); QCOMPARE(dataSourceDropPerformedSpy.count(), 1); } } // namespace KWin WAYLANDTEST_MAIN(KWin::DndTest) #include "dnd_test.moc"