wayland: Check serial instead of focus for changing selections

Clients can have valid reasons to change the selection when the
same user action that also caused the selection request
to lose keyboard focus. This is notbaly the case for menus
created from a Plasma panel which itself will not take focus
but when clicking on action it only triggers after the menu
is closed.
This also matches what weston and sway do.
BUG:490803
This commit is contained in:
David Redondo 2024-08-02 11:41:20 +02:00
parent 8542c20030
commit 31018c000b
15 changed files with 73 additions and 67 deletions

View file

@ -21,7 +21,7 @@ public:
protected:
void paintEvent(QPaintEvent *event) override;
void focusInEvent(QFocusEvent *event) override;
void keyPressEvent(QKeyEvent *) override;
};
Window::Window()
@ -37,13 +37,9 @@ void Window::paintEvent(QPaintEvent *event)
p.fillRect(0, 0, width(), height(), Qt::red);
}
void Window::focusInEvent(QFocusEvent *event)
void Window::keyPressEvent(QKeyEvent *event)
{
QRasterWindow::focusInEvent(event);
// TODO: make it work without singleshot
QTimer::singleShot(100, [] {
qApp->clipboard()->setText(QStringLiteral("test"));
});
qApp->clipboard()->setText(QStringLiteral("test"));
}
int main(int argc, char *argv[])

View file

@ -10,6 +10,7 @@
#include "kwin_wayland_test.h"
#include "core/output.h"
#include "wayland/display.h"
#include "wayland/seat.h"
#include "wayland_server.h"
#include "window.h"
@ -20,6 +21,8 @@
#include <QProcessEnvironment>
#include <QSignalSpy>
#include <linux/input-event-codes.h>
using namespace KWin;
static const QString s_socketName = QStringLiteral("wayland_test_kwin_xwayland_selections-0");
@ -111,6 +114,10 @@ void XwaylandSelectionsTest::testSync()
workspace()->activateWindow(copyWindow);
}
QCOMPARE(workspace()->activeWindow(), copyWindow);
// Wait until the window has a surface so we can pass input to it
QTRY_VERIFY(copyWindow->surface());
Test::keyboardKeyPressed(KEY_C, waylandServer()->display()->nextSerial());
Test::keyboardKeyReleased(KEY_C, waylandServer()->display()->nextSerial());
clipboardChangedSpy.wait();
// start the paste process

View file

@ -173,7 +173,7 @@ void TestDataDevice::testCreate()
// this will probably fail, we need to make a selection client side
QVERIFY(!m_seatInterface->selection());
m_seatInterface->setSelection(deviceInterface->selection());
m_seatInterface->setSelection(deviceInterface->selection(), m_display->nextSerial());
QCOMPARE(m_seatInterface->selection(), deviceInterface->selection());
// and destroy
@ -402,13 +402,13 @@ void TestDataDevice::testSetSelection()
QCOMPARE(dataOffer->offeredMimeTypes().last().name(), QStringLiteral("text/html"));
// now clear the selection
dataDevice->clearSelection(1);
dataDevice->clearSelection(2);
QVERIFY(selectionChangedSpy.wait());
QCOMPARE(selectionChangedSpy.count(), 2);
QVERIFY(!deviceInterface->selection());
// set another selection
dataDevice->setSelection(2, dataSource.get());
dataDevice->setSelection(3, dataSource.get());
QVERIFY(selectionChangedSpy.wait());
// now unbind the dataDevice
QSignalSpy unboundSpy(deviceInterface, &QObject::destroyed);
@ -509,14 +509,14 @@ void TestDataDevice::testReplaceSource()
QVERIFY(dataSource2->isValid());
dataSource2->offer(QStringLiteral("text/plain"));
QSignalSpy sourceCancelled2Spy(dataSource2.get(), &KWayland::Client::DataSource::cancelled);
dataDevice->setSelection(1, dataSource2.get());
dataDevice->setSelection(2, dataSource2.get());
QCOMPARE(selectionOfferedSpy.count(), 1);
QVERIFY(sourceCancelledSpy.wait());
QCOMPARE(selectionOfferedSpy.count(), 2);
QVERIFY(sourceCancelled2Spy.isEmpty());
// replace the data source with itself, ensure that it did not get cancelled
dataDevice->setSelection(1, dataSource2.get());
dataDevice->setSelection(3, dataSource2.get());
QVERIFY(!sourceCancelled2Spy.wait(500));
QCOMPARE(selectionOfferedSpy.count(), 2);
QVERIFY(sourceCancelled2Spy.isEmpty());
@ -527,7 +527,7 @@ void TestDataDevice::testReplaceSource()
std::unique_ptr<KWayland::Client::DataSource> dataSource3(m_dataDeviceManager->createDataSource());
QVERIFY(dataSource3->isValid());
dataSource3->offer(QStringLiteral("text/plain"));
dataDevice2->setSelection(1, dataSource3.get());
dataDevice2->setSelection(4, dataSource3.get());
QVERIFY(sourceCancelled2Spy.wait());
// try to crash by first destroying dataSource3 and setting a new DataSource
@ -535,7 +535,7 @@ void TestDataDevice::testReplaceSource()
QVERIFY(dataSource4->isValid());
dataSource4->offer(QStringLiteral("text/plain"));
dataSource3.reset();
dataDevice2->setSelection(1, dataSource4.get());
dataDevice2->setSelection(5, dataSource4.get());
QVERIFY(selectionOfferedSpy.wait());
auto dataOffer = selectionOfferedSpy.last()[0].value<KWayland::Client::DataOffer *>();

View file

@ -234,7 +234,7 @@ void SelectionTest::testClearOnEnter()
m_client2.dataDevice->setSelection(keyboardEnteredClient2Spy.first().first().value<quint32>(), dataSource2.get());
QVERIFY(selectionOfferedClient2Spy.wait());
// and clear
m_client2.dataDevice->clearSelection(keyboardEnteredClient2Spy.first().first().value<quint32>());
m_client2.dataDevice->clearSelection(keyboardEnteredClient2Spy.first().first().value<quint32>() + 1);
QVERIFY(selectionClearedClient2Spy.wait());
// now pass focus to first surface

View file

@ -1571,7 +1571,7 @@ void TestWaylandSeat::testSelection()
std::unique_ptr<KWayland::Client::DataSource> ds(ddm->createDataSource());
QVERIFY(ds->isValid());
ds->offer(QStringLiteral("text/plain"));
dd1->setSelection(0, ds.get());
dd1->setSelection(m_display->nextSerial(), ds.get());
QVERIFY(selectionSpy.wait());
QCOMPARE(selectionSpy.count(), 1);
auto ddi = m_seatInterface->selection();
@ -1581,7 +1581,7 @@ void TestWaylandSeat::testSelection()
QCOMPARE(df->offeredMimeTypes().first().name(), QStringLiteral("text/plain"));
// try to clear
dd1->setSelection(0);
dd1->setSelection(m_display->nextSerial());
QVERIFY(selectionClearedSpy.wait());
QCOMPARE(selectionClearedSpy.count(), 1);
QCOMPARE(selectionSpy.count(), 1);
@ -1594,30 +1594,30 @@ void TestWaylandSeat::testSelection()
QCoreApplication::processEvents();
// try to set Selection
dd1->setSelection(0, ds.get());
dd1->setSelection(m_display->nextSerial(), ds.get());
wl_display_flush(m_connection->display());
QCoreApplication::processEvents();
QCoreApplication::processEvents();
QCOMPARE(selectionSpy.count(), 1);
// let's unset the selection on the seat
m_seatInterface->setSelection(nullptr);
m_seatInterface->setSelection(nullptr, m_display->nextSerial());
// and pass focus back on our surface
m_seatInterface->setFocusedKeyboardSurface(serverSurface);
// we don't have a selection, so it should not send a selection
QVERIFY(sync());
QCOMPARE(selectionSpy.count(), 1);
// now let's set it manually
m_seatInterface->setSelection(ddi);
m_seatInterface->setSelection(ddi, m_display->nextSerial());
QCOMPARE(m_seatInterface->selection(), ddi);
QVERIFY(selectionSpy.wait());
QCOMPARE(selectionSpy.count(), 2);
// setting the same again should not change
m_seatInterface->setSelection(ddi);
m_seatInterface->setSelection(ddi, m_display->nextSerial());
QVERIFY(sync());
QCOMPARE(selectionSpy.count(), 2);
// now clear it manually
m_seatInterface->setSelection(nullptr);
m_seatInterface->setSelection(nullptr, m_display->nextSerial());
QVERIFY(selectionClearedSpy.wait());
QCOMPARE(selectionSpy.count(), 2);
@ -1627,10 +1627,10 @@ void TestWaylandSeat::testSelection()
std::unique_ptr<KWayland::Client::DataSource> ds2(ddm->createDataSource());
QVERIFY(ds2->isValid());
ds2->offer(QStringLiteral("text/plain"));
dd2->setSelection(0, ds2.get());
dd2->setSelection(m_display->nextSerial(), ds2.get());
QVERIFY(selectionSpy.wait());
QSignalSpy cancelledSpy(ds2.get(), &KWayland::Client::DataSource::cancelled);
m_seatInterface->setSelection(ddi);
m_seatInterface->setSelection(ddi, m_display->nextSerial());
QVERIFY(cancelledSpy.wait());
}
@ -1680,7 +1680,7 @@ void TestWaylandSeat::testDataDeviceForKeyboardSurface()
QVERIFY(ddiCreatedSpy.wait());
auto ddi = ddiCreatedSpy.first().first().value<DataDeviceInterface *>();
QVERIFY(ddi);
m_seatInterface->setSelection(ddi->selection());
m_seatInterface->setSelection(ddi->selection(), m_display->nextSerial());
// switch to other client
// create a surface and pass it keyboard focus

View file

@ -257,7 +257,7 @@ void DataControlInterfaceTest::testCopyToControl()
QSignalSpy selectionSpy(dataControlDevice.get(), &DataControlDevice::selection);
std::unique_ptr<TestDataSource> testSelection(new TestDataSource);
m_seat->setSelection(testSelection.get());
m_seat->setSelection(testSelection.get(), m_display->nextSerial());
// selection will be sent after we've been sent a new offer object and the mimes have been sent to that object
selectionSpy.wait();
@ -283,7 +283,7 @@ void DataControlInterfaceTest::testCopyToControlPrimarySelection()
QSignalSpy selectionSpy(dataControlDevice.get(), &DataControlDevice::primary_selection);
std::unique_ptr<TestDataSource> testSelection(new TestDataSource);
m_seat->setPrimarySelection(testSelection.get());
m_seat->setPrimarySelection(testSelection.get(), m_display->nextSerial());
// selection will be sent after we've been sent a new offer object and the mimes have been sent to that object
selectionSpy.wait();
@ -353,7 +353,7 @@ void DataControlInterfaceTest::testKlipperCase()
// Client A has a data source
std::unique_ptr<TestDataSource> testSelection(new TestDataSource);
m_seat->setSelection(testSelection.get());
m_seat->setSelection(testSelection.get(), m_display->nextSerial());
// klipper gets it
selectionSpy.wait();
@ -366,7 +366,7 @@ void DataControlInterfaceTest::testKlipperCase()
// Client A sets something else
std::unique_ptr<TestDataSource> testSelection2(new TestDataSource);
m_seat->setSelection(testSelection2.get());
m_seat->setSelection(testSelection2.get(), m_display->nextSerial());
// Meanwhile klipper updates with the old content
std::unique_ptr<DataControlSource> source(new DataControlSource);

View file

@ -140,7 +140,7 @@ void DataDeviceInterfacePrivate::data_device_set_selection(Resource *resource, w
selection->cancel();
}
selection = dataSource;
Q_EMIT q->selectionChanged(selection);
Q_EMIT q->selectionChanged(selection, serial);
}
void DataDeviceInterfacePrivate::data_device_release(QtWaylandServer::wl_data_device::Resource *resource)

View file

@ -109,7 +109,7 @@ public:
Q_SIGNALS:
void aboutToBeDestroyed();
void dragStarted(AbstractDataSource *source, SurfaceInterface *originSurface, quint32 serial, DragAndDropIcon *dragIcon);
void selectionChanged(KWin::DataSourceInterface *);
void selectionChanged(KWin::DataSourceInterface *, quint32 serial);
private:
friend class DataDeviceManagerInterfacePrivate;

View file

@ -62,7 +62,7 @@ void PrimarySelectionDeviceV1InterfacePrivate::zwp_primary_selection_device_v1_s
}
selection = dataSource;
if (selection) {
Q_EMIT q->selectionChanged(selection);
Q_EMIT q->selectionChanged(selection, serial);
}
}

View file

@ -45,7 +45,7 @@ public:
wl_client *client() const;
Q_SIGNALS:
void selectionChanged(KWin::PrimarySelectionSourceV1Interface *);
void selectionChanged(KWin::PrimarySelectionSourceV1Interface *, quint32 serial);
private:
friend class PrimarySelectionDeviceManagerV1InterfacePrivate;

View file

@ -150,8 +150,8 @@ void SeatInterfacePrivate::registerDataDevice(DataDeviceInterface *dataDevice)
globalKeyboard.focus.selections.removeOne(dataDevice);
};
QObject::connect(dataDevice, &QObject::destroyed, q, dataDeviceCleanup);
QObject::connect(dataDevice, &DataDeviceInterface::selectionChanged, q, [this, dataDevice] {
updateSelection(dataDevice);
QObject::connect(dataDevice, &DataDeviceInterface::selectionChanged, q, [this](DataSourceInterface *source, quint32 serial) {
updateSelection(source, serial);
});
QObject::connect(dataDevice,
&DataDeviceInterface::dragStarted,
@ -208,7 +208,7 @@ void SeatInterfacePrivate::registerDataControlDevice(DataControlDeviceV1Interfac
dataDevice->sendSelection(currentSelection);
return;
}
q->setSelection(dataDevice->selection());
q->setSelection(dataDevice->selection(), display->nextSerial());
});
QObject::connect(dataDevice, &DataControlDeviceV1Interface::primarySelectionChanged, q, [this, dataDevice] {
@ -222,7 +222,7 @@ void SeatInterfacePrivate::registerDataControlDevice(DataControlDeviceV1Interfac
dataDevice->sendPrimarySelection(currentPrimarySelection);
return;
}
q->setPrimarySelection(dataDevice->primarySelection());
q->setPrimarySelection(dataDevice->primarySelection(), display->nextSerial());
});
dataDevice->sendSelection(currentSelection);
@ -239,8 +239,8 @@ void SeatInterfacePrivate::registerPrimarySelectionDevice(PrimarySelectionDevice
globalKeyboard.focus.primarySelections.removeOne(primarySelectionDevice);
};
QObject::connect(primarySelectionDevice, &QObject::destroyed, q, dataDeviceCleanup);
QObject::connect(primarySelectionDevice, &PrimarySelectionDeviceV1Interface::selectionChanged, q, [this, primarySelectionDevice] {
updatePrimarySelection(primarySelectionDevice);
QObject::connect(primarySelectionDevice, &PrimarySelectionDeviceV1Interface::selectionChanged, q, [this](PrimarySelectionSourceV1Interface *source, quint32 serial) {
updatePrimarySelection(source, serial);
});
// is the new DataDevice for the current keyoard focus?
if (globalKeyboard.focus.surface) {
@ -306,30 +306,26 @@ void SeatInterfacePrivate::endDrag()
Q_EMIT q->dragEnded();
}
void SeatInterfacePrivate::updateSelection(DataDeviceInterface *dataDevice)
void SeatInterfacePrivate::updateSelection(DataSourceInterface *dataSource, quint32 serial)
{
DataSourceInterface *selection = dataDevice->selection();
// if the update is from the focussed window we should inform the active client
if (!(globalKeyboard.focus.surface && (*globalKeyboard.focus.surface->client() == dataDevice->client()))) {
if (selection) {
selection->cancel();
if (currentSelectionSerial - serial < UINT32_MAX / 2) {
if (dataSource) {
dataSource->cancel();
}
return;
}
q->setSelection(selection);
q->setSelection(dataSource, serial);
}
void SeatInterfacePrivate::updatePrimarySelection(PrimarySelectionDeviceV1Interface *primarySelectionDevice)
void SeatInterfacePrivate::updatePrimarySelection(PrimarySelectionSourceV1Interface *dataSource, quint32 serial)
{
PrimarySelectionSourceV1Interface *selection = primarySelectionDevice->selection();
// if the update is from the focussed window we should inform the active client
if (!(globalKeyboard.focus.surface && (*globalKeyboard.focus.surface->client() == primarySelectionDevice->client()))) {
if (selection) {
selection->cancel();
if (currentPrimarySelectionSerial - serial < UINT32_MAX / 2) {
if (dataSource) {
dataSource->cancel();
}
return;
}
q->setPrimarySelection(selection);
q->setPrimarySelection(dataSource, serial);
}
void SeatInterfacePrivate::sendCapabilities()
@ -1286,7 +1282,7 @@ AbstractDataSource *SeatInterface::selection() const
return d->currentSelection;
}
void SeatInterface::setSelection(AbstractDataSource *selection)
void SeatInterface::setSelection(AbstractDataSource *selection, quint32 serial)
{
if (d->currentSelection == selection) {
return;
@ -1298,13 +1294,14 @@ void SeatInterface::setSelection(AbstractDataSource *selection)
}
if (selection) {
auto cleanup = [this]() {
setSelection(nullptr);
auto cleanup = [this, serial]() {
setSelection(nullptr, serial);
};
connect(selection, &AbstractDataSource::aboutToBeDestroyed, this, cleanup);
}
d->currentSelection = selection;
d->currentSelectionSerial = serial;
for (auto focussedSelection : std::as_const(d->globalKeyboard.focus.selections)) {
focussedSelection->sendSelection(selection);
@ -1322,7 +1319,7 @@ AbstractDataSource *SeatInterface::primarySelection() const
return d->currentPrimarySelection;
}
void SeatInterface::setPrimarySelection(AbstractDataSource *selection)
void SeatInterface::setPrimarySelection(AbstractDataSource *selection, quint32 serial)
{
if (d->currentPrimarySelection == selection) {
return;
@ -1333,13 +1330,14 @@ void SeatInterface::setPrimarySelection(AbstractDataSource *selection)
}
if (selection) {
auto cleanup = [this]() {
setPrimarySelection(nullptr);
auto cleanup = [this, serial]() {
setPrimarySelection(nullptr, serial);
};
connect(selection, &AbstractDataSource::aboutToBeDestroyed, this, cleanup);
}
d->currentPrimarySelection = selection;
d->currentPrimarySelectionSerial = serial;
for (auto focussedSelection : std::as_const(d->globalKeyboard.focus.primarySelections)) {
focussedSelection->sendSelection(selection);

View file

@ -645,10 +645,10 @@ public:
* @see selection
* @see selectionChanged
*/
void setSelection(AbstractDataSource *selection);
void setSelection(AbstractDataSource *selection, quint32 serial);
AbstractDataSource *primarySelection() const;
void setPrimarySelection(AbstractDataSource *selection);
void setPrimarySelection(AbstractDataSource *selection, quint32 serial);
void startDrag(AbstractDataSource *source, SurfaceInterface *sourceSurface, int dragSerial = -1, DragAndDropIcon *dragIcon = nullptr);

View file

@ -28,6 +28,7 @@ class TextInputV1Interface;
class TextInputV2Interface;
class TextInputV3Interface;
class PrimarySelectionDeviceV1Interface;
class PrimarySelectionSourceV1Interface;
class DragAndDropIcon;
class SeatInterfacePrivate : public QtWaylandServer::wl_seat
@ -68,7 +69,9 @@ public:
// the last thing copied into the clipboard content
AbstractDataSource *currentSelection = nullptr;
quint32 currentSelectionSerial = 0;
AbstractDataSource *currentPrimarySelection = nullptr;
quint32 currentPrimarySelectionSerial = 0;
// Pointer related members
struct Pointer
@ -151,8 +154,8 @@ protected:
void seat_release(Resource *resource) override;
private:
void updateSelection(DataDeviceInterface *dataDevice);
void updatePrimarySelection(PrimarySelectionDeviceV1Interface *primarySelectionDevice);
void updateSelection(DataSourceInterface *dataSource, quint32 serial);
void updatePrimarySelection(PrimarySelectionSourceV1Interface *dataSource, quint32);
};
} // namespace KWin

View file

@ -11,6 +11,7 @@
#include "datasource.h"
#include "selection_source.h"
#include "wayland/display.h"
#include "wayland/seat.h"
#include "wayland_server.h"
#include "workspace.h"
@ -160,11 +161,11 @@ void Clipboard::x11OffersChanged(const QStringList &added, const QStringList &re
connect(newSelection.get(), &XwlDataSource::dataRequested, source, &X11Source::startTransfer);
// we keep the old selection around because setSelection needs it to be still alive
std::swap(m_selectionSource, newSelection);
waylandServer()->seat()->setSelection(m_selectionSource.get());
waylandServer()->seat()->setSelection(m_selectionSource.get(), waylandServer()->display()->nextSerial());
} else {
AbstractDataSource *currentSelection = waylandServer()->seat()->selection();
if (!ownsSelection(currentSelection)) {
waylandServer()->seat()->setSelection(nullptr);
waylandServer()->seat()->setSelection(nullptr, waylandServer()->display()->nextSerial());
m_selectionSource.reset();
}
}

View file

@ -12,6 +12,7 @@
#include "datasource.h"
#include "selection_source.h"
#include "wayland/display.h"
#include "wayland/seat.h"
#include "wayland_server.h"
#include "workspace.h"
@ -171,11 +172,11 @@ void Primary::x11OffersChanged(const QStringList &added, const QStringList &remo
connect(newSelection.get(), &XwlDataSource::dataRequested, source, &X11Source::startTransfer);
// we keep the old selection around because setPrimarySelection needs it to be still alive
std::swap(m_primarySelectionSource, newSelection);
waylandServer()->seat()->setPrimarySelection(m_primarySelectionSource.get());
waylandServer()->seat()->setPrimarySelection(m_primarySelectionSource.get(), waylandServer()->display()->nextSerial());
} else {
AbstractDataSource *currentSelection = waylandServer()->seat()->primarySelection();
if (!ownsSelection(currentSelection)) {
waylandServer()->seat()->setPrimarySelection(nullptr);
waylandServer()->seat()->setPrimarySelection(nullptr, waylandServer()->display()->nextSerial());
m_primarySelectionSource.reset();
}
}