diff --git a/src/wayland/autotests/server/test_datacontrol_interface.cpp b/src/wayland/autotests/server/test_datacontrol_interface.cpp index aabfb83c03..d32e2fce50 100644 --- a/src/wayland/autotests/server/test_datacontrol_interface.cpp +++ b/src/wayland/autotests/server/test_datacontrol_interface.cpp @@ -139,6 +139,7 @@ private Q_SLOTS: void cleanup(); void testCopyToControl(); void testCopyFromControl(); + void testKlipperCase(); private: KWayland::Client::ConnectionThread *m_connection; @@ -302,6 +303,46 @@ void DataControlInterfaceTest::testCopyFromControl() QVERIFY(m_seat->selection()); QCOMPARE(m_seat->selection()->mimeTypes(), QStringList({"cheese/test1", "cheese/test2"})); } + +void DataControlInterfaceTest::testKlipperCase() +{ + // This tests the setup of klipper's real world operation and a race with a common pattern seen between clients and klipper + // The client's behaviour is faked with direct access to the seat + + QScopedPointer dataControlDevice(new DataControlDevice); + dataControlDevice->init(m_dataControlDeviceManager->get_data_device(*m_clientSeat)); + + QSignalSpy newOfferSpy(dataControlDevice.data(), &DataControlDevice::dataControlOffer); + QSignalSpy selectionSpy(dataControlDevice.data(), &DataControlDevice::selection); + QSignalSpy serverSelectionChangedSpy(m_seat, &SeatInterface::selectionChanged); + + // Client A has a data source + QScopedPointer testSelection(new TestDataSource); + m_seat->setSelection(testSelection.data()); + + // klipper gets it + selectionSpy.wait(); + + // Client A deletes it + testSelection.reset(); + + //klipper gets told + selectionSpy.wait(); + + // Client A sets something else + QScopedPointer testSelection2(new TestDataSource); + m_seat->setSelection(testSelection2.data()); + + // Meanwhile klipper updates with the old content + QScopedPointer source(new DataControlSource); + source->init(m_dataControlDeviceManager->create_data_source()); + source->offer("fromKlipper/test1"); + source->offer("application/x-kde-onlyReplaceEmpty"); + + dataControlDevice->set_selection(source->object()); + + QVERIFY(!serverSelectionChangedSpy.wait(10)); + QCOMPARE(m_seat->selection(), testSelection2.data()); } diff --git a/src/wayland/seat_interface.cpp b/src/wayland/seat_interface.cpp index 563042fdaa..034633e23c 100644 --- a/src/wayland/seat_interface.cpp +++ b/src/wayland/seat_interface.cpp @@ -345,9 +345,17 @@ void SeatInterface::Private::registerDataControlDevice(DataControlDeviceV1Interf QObject::connect(dataDevice, &QObject::destroyed, q, dataDeviceCleanup); QObject::connect(dataDevice, &DataControlDeviceV1Interface::selectionChanged, q, - [this, dataDevice] { - q->setSelection(dataDevice->selection()); + [this, dataDevice] { + // Special klipper workaround to avoid a race + // If the mimetype x-kde-onlyReplaceEmpty is set, and we've had another update in the meantime, do nothing + // See https://github.com/swaywm/wlr-protocols/issues/92 + if (dataDevice->selection() && dataDevice->selection()->mimeTypes().contains(QLatin1String("application/x-kde-onlyReplaceEmpty")) && + currentSelection) { + dataDevice->selection()->cancel(); + return; } + q->setSelection(dataDevice->selection()); + } ); QObject::connect(dataDevice, &DataControlDeviceV1Interface::selectionCleared, q,