From 95a8163c5b5970243ca9d13244026e98bb1b32e1 Mon Sep 17 00:00:00 2001 From: Vlad Zahorodnii Date: Fri, 25 Nov 2022 12:16:03 +0200 Subject: [PATCH] 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 --- src/plugins/qpa/CMakeLists.txt | 1 + src/plugins/qpa/clipboard.cpp | 207 ++++++++++++++++++++++++++++++++ src/plugins/qpa/clipboard.h | 69 +++++++++++ src/plugins/qpa/integration.cpp | 9 ++ src/plugins/qpa/integration.h | 3 + src/wayland/display.h | 2 +- 6 files changed, 290 insertions(+), 1 deletion(-) create mode 100644 src/plugins/qpa/clipboard.cpp create mode 100644 src/plugins/qpa/clipboard.h diff --git a/src/plugins/qpa/CMakeLists.txt b/src/plugins/qpa/CMakeLists.txt index e1f5dc2793..62b7a12a38 100644 --- a/src/plugins/qpa/CMakeLists.txt +++ b/src/plugins/qpa/CMakeLists.txt @@ -1,6 +1,7 @@ add_library(KWinQpaPlugin OBJECT) target_sources(KWinQpaPlugin PRIVATE backingstore.cpp + clipboard.cpp eglhelpers.cpp eglplatformcontext.cpp integration.cpp diff --git a/src/plugins/qpa/clipboard.cpp b/src/plugins/qpa/clipboard.cpp new file mode 100644 index 0000000000..37bd0f98ba --- /dev/null +++ b/src/plugins/qpa/clipboard.cpp @@ -0,0 +1,207 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + 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 + +#include +#include +#include + +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(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(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; + } +} + +} diff --git a/src/plugins/qpa/clipboard.h b/src/plugins/qpa/clipboard.h new file mode 100644 index 0000000000..a85746d0e5 --- /dev/null +++ b/src/plugins/qpa/clipboard.h @@ -0,0 +1,69 @@ +/* + SPDX-FileCopyrightText: 2022 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "wayland/abstract_data_source.h" + +#include + +#include + +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 m_externalMimeData; + std::unique_ptr m_ownSelection; +}; + +} diff --git a/src/plugins/qpa/integration.cpp b/src/plugins/qpa/integration.cpp index af5c517922..be55379c0c 100644 --- a/src/plugins/qpa/integration.cpp +++ b/src/plugins/qpa/integration.cpp @@ -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(); +} + } } diff --git a/src/plugins/qpa/integration.h b/src/plugins/qpa/integration.h index 318709f297..78a85c8eb0 100644 --- a/src/plugins/qpa/integration.h +++ b/src/plugins/qpa/integration.h @@ -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 screens() const; @@ -60,6 +62,7 @@ private: QPlatformPlaceholderScreen *m_dummyScreen = nullptr; QHash m_screens; std::unique_ptr m_services; + std::unique_ptr m_clipboard; }; } diff --git a/src/wayland/display.h b/src/wayland/display.h index c6bd8e407c..d27949475c 100644 --- a/src/wayland/display.h +++ b/src/wayland/display.h @@ -122,7 +122,7 @@ public: */ void setDefaultMaxBufferSize(size_t max); -private Q_SLOTS: +public Q_SLOTS: void flush(); Q_SIGNALS: