3493e97655
Summary: The clipboard sync is done by a dedicated helper binary launched by KWin. This helper binary is forced to xcb platform to piggy-back on Qt's implementation of the X11 clipboard. In addition it implements the Wayland clipboard - which is much simpler. Reading the Wayland clipboard is based on the implementation in QtWayland. KWin internally knows the DataDeviceInterface belonging to the helper application. Whenever an xwayland client is focussed, this DataDevice is allowed to set the selection and KWin manually updates the current selection in the SeatInterface. By that the sync from X11 to Wayland is implemented. When afterwards a Wayland client is selected, it's sent the current selection which references the X clipboard and a data transfer can be initiated in the normal Wayland way. For the other direction KWin sends the current selection to the helper's DataDevice whenever an xwayland window is focused. The helper application reads the Wayland clipboard and sets it on the X11 clipboard. Thus the Wayland clipboard is synced to X11. The approach used here will also be useful for implementing a clipboard manager (aka klipper). Currently the implementation is not yet fully completed. We need to make sure that the helper application gets restarted in case of a crash. Test Plan: See added test case Reviewers: #plasma_on_wayland, #kwin Subscribers: plasma-devel, kwin Tags: #plasma_on_wayland, #kwin Differential Revision: https://phabricator.kde.org/D1973
174 lines
6.2 KiB
C++
174 lines
6.2 KiB
C++
/********************************************************************
|
|
KWin - the KDE window manager
|
|
This file is part of the KDE project.
|
|
|
|
Copyright (C) 2016 Martin Gräßlin <mgraesslin@kde.org>
|
|
|
|
This program is free software; you can redistribute it and/or modify
|
|
it under the terms of the GNU General Public License as published by
|
|
the Free Software Foundation; either version 2 of the License, or
|
|
(at your option) any later version.
|
|
|
|
This program is distributed in the hope that it will be useful,
|
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
GNU General Public License for more details.
|
|
|
|
You should have received a copy of the GNU General Public License
|
|
along with this program. If not, see <http://www.gnu.org/licenses/>.
|
|
*********************************************************************/
|
|
#include "waylandclipboard.h"
|
|
|
|
#include <KWayland/Client/connection_thread.h>
|
|
#include <KWayland/Client/datadevicemanager.h>
|
|
#include <KWayland/Client/datadevice.h>
|
|
#include <KWayland/Client/datasource.h>
|
|
#include <KWayland/Client/event_queue.h>
|
|
#include <KWayland/Client/seat.h>
|
|
#include <KWayland/Client/registry.h>
|
|
|
|
#include <QClipboard>
|
|
#include <QFile>
|
|
#include <QGuiApplication>
|
|
#include <QThread>
|
|
#include <QMimeData>
|
|
#include <QMimeType>
|
|
#include <qplatformdefs.h>
|
|
|
|
#include <unistd.h>
|
|
|
|
using namespace KWayland::Client;
|
|
|
|
WaylandClipboard::WaylandClipboard(QObject *parent)
|
|
: QObject(parent)
|
|
, m_thread(new QThread)
|
|
, m_connectionThread(new ConnectionThread)
|
|
{
|
|
m_connectionThread->moveToThread(m_thread);
|
|
m_thread->start();
|
|
|
|
connect(m_connectionThread, &ConnectionThread::connected, this, &WaylandClipboard::setup, Qt::QueuedConnection);
|
|
|
|
m_connectionThread->initConnection();
|
|
|
|
connect(qApp->clipboard(), &QClipboard::changed, this,
|
|
[this] (QClipboard::Mode mode) {
|
|
if (mode != QClipboard::Clipboard) {
|
|
return;
|
|
}
|
|
// TODO: do we need to take a copy of the clipboard in order to keep it after the X application quit?
|
|
if (!m_dataDeviceManager || !m_dataDevice) {
|
|
return;
|
|
}
|
|
auto source = m_dataDeviceManager->createDataSource(this);
|
|
auto mimeData = qApp->clipboard()->mimeData();
|
|
const auto formats = mimeData->formats();
|
|
for (const auto &format : formats) {
|
|
source->offer(format);
|
|
}
|
|
connect(source, &DataSource::sendDataRequested, this,
|
|
[] (const QString &type, qint32 fd) {
|
|
auto mimeData = qApp->clipboard()->mimeData();
|
|
if (!mimeData->hasFormat(type)) {
|
|
close(fd);
|
|
return;
|
|
}
|
|
const auto data = mimeData->data(type);
|
|
QFile writePipe;
|
|
if (writePipe.open(fd, QIODevice::WriteOnly, QFile::AutoCloseHandle)) {
|
|
writePipe.write(data);
|
|
writePipe.close();
|
|
} else {
|
|
close(fd);
|
|
}
|
|
}
|
|
);
|
|
m_dataDevice->setSelection(0, source);
|
|
delete m_dataSource;
|
|
m_dataSource = source;
|
|
m_connectionThread->flush();
|
|
}
|
|
);
|
|
}
|
|
|
|
WaylandClipboard::~WaylandClipboard()
|
|
{
|
|
m_connectionThread->deleteLater();
|
|
m_thread->quit();
|
|
m_thread->wait();
|
|
}
|
|
|
|
static int readData(int fd, QByteArray &data)
|
|
{
|
|
// implementation based on QtWayland file qwaylanddataoffer.cpp
|
|
char buf[4096];
|
|
int retryCount = 0;
|
|
int n;
|
|
while (true) {
|
|
n = QT_READ(fd, buf, sizeof buf);
|
|
if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK) && ++retryCount < 1000) {
|
|
usleep(1000);
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
if (n > 0) {
|
|
data.append(buf, n);
|
|
n = readData(fd, data);
|
|
}
|
|
return n;
|
|
}
|
|
|
|
void WaylandClipboard::setup()
|
|
{
|
|
EventQueue *queue = new EventQueue(this);
|
|
queue->setup(m_connectionThread);
|
|
|
|
Registry *registry = new Registry(this);
|
|
registry->setEventQueue(queue);
|
|
registry->create(m_connectionThread);
|
|
connect(registry, &Registry::interfacesAnnounced, this,
|
|
[this, registry] {
|
|
const auto seatInterface = registry->interface(Registry::Interface::Seat);
|
|
if (seatInterface.name != 0) {
|
|
m_seat = registry->createSeat(seatInterface.name, seatInterface.version, this);
|
|
}
|
|
const auto ddmInterface = registry->interface(Registry::Interface::DataDeviceManager);
|
|
if (ddmInterface.name != 0) {
|
|
m_dataDeviceManager = registry->createDataDeviceManager(ddmInterface.name, ddmInterface.version, this);
|
|
}
|
|
if (m_seat && m_dataDeviceManager) {
|
|
m_dataDevice = m_dataDeviceManager->getDataDevice(m_seat, this);
|
|
connect(m_dataDevice, &DataDevice::selectionOffered, this,
|
|
[this] (DataOffer *offer) {
|
|
if (offer->offeredMimeTypes().isEmpty()) {
|
|
return;
|
|
}
|
|
int pipeFds[2];
|
|
if (pipe(pipeFds) != 0) {
|
|
return;
|
|
}
|
|
const auto mimeType = offer->offeredMimeTypes().first();
|
|
offer->receive(mimeType, pipeFds[1]);
|
|
m_connectionThread->flush();
|
|
close(pipeFds[1]);
|
|
QByteArray content;
|
|
if (readData(pipeFds[0], content) != 0) {
|
|
content = QByteArray();
|
|
}
|
|
close(pipeFds[0]);
|
|
QMimeData *mimeData = new QMimeData();
|
|
mimeData->setData(mimeType.name(), content);
|
|
qApp->clipboard()->setMimeData(mimeData);
|
|
}
|
|
);
|
|
connect(m_dataDevice, &DataDevice::selectionCleared, this,
|
|
[this] {
|
|
qApp->clipboard()->clear();
|
|
}
|
|
);
|
|
}
|
|
}
|
|
);
|
|
registry->setup();
|
|
}
|