qpa: Implement clipboard

This allows copy pasting text between the overview and regular clients.

QMimeData::data() has a synchronous API. It is a problem for us, the
compositor, because it means we need to block the main thread to read
the mime data. This change adds a one second timeout. If no data arrives
within the next 1 second, the qpa will give up in order to avoid freezing
the screen further. Not sure how this can be handled better without
changing the API of the QMimeData to add async overloads.

BUG: 445751
This commit is contained in:
Vlad Zahorodnii 2022-11-25 12:16:03 +02:00
parent 7d4e993155
commit 95a8163c5b
6 changed files with 290 additions and 1 deletions

View file

@ -1,6 +1,7 @@
add_library(KWinQpaPlugin OBJECT)
target_sources(KWinQpaPlugin PRIVATE
backingstore.cpp
clipboard.cpp
eglhelpers.cpp
eglplatformcontext.cpp
integration.cpp

View file

@ -0,0 +1,207 @@
/*
SPDX-FileCopyrightText: 2022 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#include "plugins/qpa/clipboard.h"
#include "utils/filedescriptor.h"
#include "wayland/display.h"
#include "wayland/seat.h"
#include "wayland_server.h"
#include <QThreadPool>
#include <fcntl.h>
#include <poll.h>
#include <unistd.h>
namespace KWin::QPA
{
ClipboardDataSource::ClipboardDataSource(QMimeData *mimeData, QObject *parent)
: AbstractDataSource(parent)
, m_mimeData(mimeData)
{
}
QMimeData *ClipboardDataSource::mimeData() const
{
return m_mimeData;
}
static void writeData(FileDescriptor fd, const QByteArray &buffer)
{
size_t remainingSize = buffer.size();
pollfd pfds[1];
pfds[0].fd = fd.get();
pfds[0].events = POLLOUT;
while (true) {
const int ready = poll(pfds, 1, 5000);
if (ready < 0) {
if (errno != EINTR) {
return;
}
} else if (ready == 0) {
return;
} else if (!(pfds[0].revents & POLLOUT)) {
return;
} else {
const char *chunk = buffer.constData() + (buffer.size() - remainingSize);
const ssize_t n = write(fd.get(), chunk, remainingSize);
if (n < 0) {
return;
} else if (n == 0) {
return;
} else {
remainingSize -= n;
if (remainingSize == 0) {
return;
}
}
}
}
}
void ClipboardDataSource::requestData(const QString &mimeType, qint32 fd)
{
const QByteArray data = m_mimeData->data(mimeType);
QThreadPool::globalInstance()->start([data, fd]() {
writeData(FileDescriptor(fd), data);
});
}
void ClipboardDataSource::cancel()
{
}
QStringList ClipboardDataSource::mimeTypes() const
{
return m_mimeData->formats();
}
ClipboardMimeData::ClipboardMimeData(AbstractDataSource *dataSource)
: m_dataSource(dataSource)
{
}
static QVariant readData(FileDescriptor fd)
{
pollfd pfd;
pfd.fd = fd.get();
pfd.events = POLLIN;
QByteArray buffer;
while (true) {
const int ready = poll(&pfd, 1, 1000);
if (ready < 0) {
if (errno != EINTR) {
return QVariant();
}
} else if (ready == 0) {
return QVariant();
} else {
char chunk[4096];
const ssize_t n = read(fd.get(), chunk, sizeof chunk);
if (n < 0) {
return QVariant();
} else if (n == 0) {
return buffer;
} else if (n > 0) {
buffer.append(chunk, n);
}
}
}
}
QVariant ClipboardMimeData::retrieveData(const QString &mimeType, QMetaType preferredType) const
{
int pipeFds[2];
if (pipe2(pipeFds, O_CLOEXEC) != 0) {
return QVariant();
}
m_dataSource->requestData(mimeType, pipeFds[1]);
waylandServer()->display()->flush();
return readData(FileDescriptor(pipeFds[0]));
}
Clipboard::Clipboard()
{
}
void Clipboard::initialize()
{
connect(waylandServer()->seat(), &SeatInterface::selectionChanged, this, [this](AbstractDataSource *selection) {
if (selection && m_ownSelection.get() != selection) {
m_externalMimeData = std::make_unique<ClipboardMimeData>(selection);
} else {
m_externalMimeData.reset();
}
emitChanged(QClipboard::Clipboard);
});
}
QMimeData *Clipboard::mimeData(QClipboard::Mode mode)
{
switch (mode) {
case QClipboard::Clipboard:
if (m_ownSelection) {
if (waylandServer()->seat()->selection() == m_ownSelection.get()) {
return m_ownSelection->mimeData();
}
}
return m_externalMimeData ? m_externalMimeData.get() : &m_emptyData;
default:
return &m_emptyData;
}
}
void Clipboard::setMimeData(QMimeData *data, QClipboard::Mode mode)
{
static const QString plain = QStringLiteral("text/plain");
static const QString utf8 = QStringLiteral("text/plain;charset=utf-8");
if (data && data->hasFormat(plain) && !data->hasFormat(utf8)) {
data->setData(utf8, data->data(plain));
}
switch (mode) {
case QClipboard::Clipboard:
if (data) {
auto oldSelection = std::move(m_ownSelection);
m_ownSelection = std::make_unique<ClipboardDataSource>(data);
waylandServer()->seat()->setSelection(m_ownSelection.get(), waylandServer()->display()->nextSerial());
} else {
if (waylandServer()->seat()->selection() == m_ownSelection.get()) {
waylandServer()->seat()->setSelection(nullptr, waylandServer()->display()->nextSerial());
}
m_ownSelection.reset();
}
break;
default:
break;
}
}
bool Clipboard::supportsMode(QClipboard::Mode mode) const
{
return mode == QClipboard::Clipboard;
}
bool Clipboard::ownsMode(QClipboard::Mode mode) const
{
switch (mode) {
case QClipboard::Clipboard:
return m_ownSelection && waylandServer()->seat()->selection() == m_ownSelection.get();
default:
return false;
}
}
}

View file

@ -0,0 +1,69 @@
/*
SPDX-FileCopyrightText: 2022 Vlad Zahorodnii <vlad.zahorodnii@kde.org>
SPDX-License-Identifier: GPL-2.0-or-later
*/
#pragma once
#include "wayland/abstract_data_source.h"
#include <QMimeData>
#include <qpa/qplatformclipboard.h>
namespace KWin::QPA
{
class ClipboardDataSource : public AbstractDataSource
{
Q_OBJECT
public:
explicit ClipboardDataSource(QMimeData *mimeData, QObject *parent = nullptr);
QMimeData *mimeData() const;
void requestData(const QString &mimeType, qint32 fd) override;
void cancel() override;
QStringList mimeTypes() const override;
private:
QMimeData *m_mimeData;
};
class ClipboardMimeData : public QMimeData
{
Q_OBJECT
public:
explicit ClipboardMimeData(AbstractDataSource *dataSource);
protected:
QVariant retrieveData(const QString &mimetype, QMetaType preferredType) const override;
private:
AbstractDataSource *m_dataSource;
};
class Clipboard : public QObject, public QPlatformClipboard
{
Q_OBJECT
public:
Clipboard();
void initialize();
QMimeData *mimeData(QClipboard::Mode mode = QClipboard::Clipboard) override;
void setMimeData(QMimeData *data, QClipboard::Mode mode = QClipboard::Clipboard) override;
bool supportsMode(QClipboard::Mode mode) const override;
bool ownsMode(QClipboard::Mode mode) const override;
private:
QMimeData m_emptyData;
std::unique_ptr<ClipboardMimeData> m_externalMimeData;
std::unique_ptr<ClipboardDataSource> m_ownSelection;
};
}

View file

@ -10,6 +10,7 @@
#include "integration.h"
#include "backingstore.h"
#include "clipboard.h"
#include "eglplatformcontext.h"
#include "logging.h"
#include "offscreensurface.h"
@ -51,6 +52,7 @@ Integration::Integration()
, m_fontDb(new QGenericUnixFontDatabase())
, m_nativeInterface(new QPlatformNativeInterface())
, m_services(new QGenericUnixServices())
, m_clipboard(new Clipboard())
{
}
@ -184,6 +186,8 @@ void Integration::handleWorkspaceCreated()
for (Output *output : outputs) {
handleOutputEnabled(output);
}
m_clipboard->initialize();
}
void Integration::handleOutputEnabled(Output *output)
@ -224,6 +228,11 @@ QPlatformServices *Integration::services() const
return m_services.get();
}
QPlatformClipboard *Integration::clipboard() const
{
return m_clipboard.get();
}
}
}

View file

@ -23,6 +23,7 @@ class Output;
namespace QPA
{
class Clipboard;
class Screen;
class Integration : public QObject, public QPlatformIntegration
@ -44,6 +45,7 @@ public:
QPlatformAccessibility *accessibility() const override;
QPlatformNativeInterface *nativeInterface() const override;
QPlatformServices *services() const override;
QPlatformClipboard *clipboard() const override;
void initialize() override;
QHash<Output *, Screen *> screens() const;
@ -60,6 +62,7 @@ private:
QPlatformPlaceholderScreen *m_dummyScreen = nullptr;
QHash<Output *, Screen *> m_screens;
std::unique_ptr<QGenericUnixServices> m_services;
std::unique_ptr<Clipboard> m_clipboard;
};
}

View file

@ -122,7 +122,7 @@ public:
*/
void setDefaultMaxBufferSize(size_t max);
private Q_SLOTS:
public Q_SLOTS:
void flush();
Q_SIGNALS: