diff --git a/plugins/clipboard/CMakeLists.txt b/plugins/clipboard/CMakeLists.txt index 3481a9a17..f55ff5080 100644 --- a/plugins/clipboard/CMakeLists.txt +++ b/plugins/clipboard/CMakeLists.txt @@ -1,8 +1,11 @@ -set(kdeconnect_clipboard_SRCS - clipboardplugin.cpp - clipboardlistener.cpp -) +find_package(KF5 ${KF5_MIN_VERSION} QUIET OPTIONAL_COMPONENTS Wayland) -kdeconnect_add_plugin(kdeconnect_clipboard JSON kdeconnect_clipboard.json SOURCES ${kdeconnect_clipboard_SRCS}) +kdeconnect_add_plugin(kdeconnect_clipboard JSON kdeconnect_clipboard.json SOURCES clipboardplugin.cpp clipboardlistener.cpp clipboardlistenerqt.cpp) target_link_libraries(kdeconnect_clipboard kdeconnectcore Qt5::Gui) + +if(TARGET KF5::WaylandClient) + target_sources(kdeconnect_clipboard PRIVATE clipboardlistenerwayland.cpp) + target_compile_definitions(kdeconnect_clipboard PRIVATE -DWITH_KWAYLAND) + target_link_libraries(kdeconnect_clipboard KF5::WaylandClient) +endif() diff --git a/plugins/clipboard/clipboardlistener.cpp b/plugins/clipboard/clipboardlistener.cpp index abd43876a..fa82ad49a 100644 --- a/plugins/clipboard/clipboardlistener.cpp +++ b/plugins/clipboard/clipboardlistener.cpp @@ -19,35 +19,27 @@ */ #include "clipboardlistener.h" - -ClipboardListener::ClipboardListener() - : clipboard(QGuiApplication::clipboard()) -{ -#ifdef Q_OS_MAC - connect(&m_clipboardMonitorTimer, &QTimer::timeout, this, [this](){ updateClipboard(QClipboard::Clipboard); }); - m_clipboardMonitorTimer.start(1000); // Refresh 1s +#include "clipboardlistenerqt.h" +#ifdef WITH_KWAYLAND +#include "clipboardlistenerwayland.h" #endif - connect(clipboard, &QClipboard::changed, this, &ClipboardListener::updateClipboard); -} -void ClipboardListener::updateClipboard(QClipboard::Mode mode) +ClipboardListener::ClipboardListener() +{} + +ClipboardListener* ClipboardListener::instance() { - if (mode != QClipboard::Clipboard) { - return; + static ClipboardListener* me = nullptr; + if (!me) { +#ifdef WITH_KWAYLAND + if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) { + me = new ClipboardListenerWayland(); + } + else +#endif + { + me = new ClipboardListenerQt(); + } } - - QString content = clipboard->text(); - - if (content == currentContent) { - return; - } - currentContent = content; - - Q_EMIT clipboardChanged(content); -} - -void ClipboardListener::setText(const QString& content) -{ - currentContent = content; - clipboard->setText(content); + return me; } diff --git a/plugins/clipboard/clipboardlistener.h b/plugins/clipboard/clipboardlistener.h index 20973b12b..2bac5f278 100644 --- a/plugins/clipboard/clipboardlistener.h +++ b/plugins/clipboard/clipboardlistener.h @@ -21,7 +21,6 @@ #ifndef CLIPBOARDLISTENER_H #define CLIPBOARDLISTENER_H -#include #include #include #include @@ -29,32 +28,16 @@ /** * Wrapper around QClipboard, which emits clipboardChanged only when it really changed */ -class ClipboardListener : public QObject +class ClipboardListener : public QObject { Q_OBJECT -private: - ClipboardListener(); - QString currentContent; - QClipboard* clipboard; -#ifdef Q_OS_MAC - QTimer m_clipboardMonitorTimer; -#endif - public: + ClipboardListener(); - static ClipboardListener* instance() - { - static ClipboardListener* me = nullptr; - if (!me) { - me = new ClipboardListener(); - } - return me; - } + static ClipboardListener* instance(); - void updateClipboard(QClipboard::Mode mode); - - void setText(const QString& content); + virtual void setText(const QString& content) = 0; Q_SIGNALS: void clipboardChanged(const QString& content); diff --git a/plugins/clipboard/clipboardlistenerqt.cpp b/plugins/clipboard/clipboardlistenerqt.cpp new file mode 100644 index 000000000..c53f34b66 --- /dev/null +++ b/plugins/clipboard/clipboardlistenerqt.cpp @@ -0,0 +1,54 @@ +/** + * Copyright 2016 Albert Vaca + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 . + */ + +#include "clipboardlistenerqt.h" + +ClipboardListenerQt::ClipboardListenerQt() + : ClipboardListener() + , m_clipboard(QGuiApplication::clipboard()) +{ +#ifdef Q_OS_MAC + connect(&m_clipboardMonitorTimer, &QTimer::timeout, this, [this](){ updateClipboard(QClipboard::Clipboard); }); + m_clipboardMonitorTimer.start(1000); // Refresh 1s +#endif + connect(m_clipboard, &QClipboard::changed, this, &ClipboardListenerQt::updateClipboard); +} + +void ClipboardListenerQt::setText(const QString& content) +{ + m_currentContent = content; + m_clipboard->setText(content); +} + +void ClipboardListenerQt::updateClipboard(QClipboard::Mode mode) +{ + if (mode != QClipboard::Clipboard) { + return; + } + + QString content = m_clipboard->text(); + + if (content == m_currentContent) { + return; + } + m_currentContent = content; + + Q_EMIT clipboardChanged(content); +} diff --git a/plugins/clipboard/clipboardlistenerqt.h b/plugins/clipboard/clipboardlistenerqt.h new file mode 100644 index 000000000..a46f7913f --- /dev/null +++ b/plugins/clipboard/clipboardlistenerqt.h @@ -0,0 +1,46 @@ +/** + * Copyright 2016 Albert Vaca + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 . + */ + +#ifndef CLIPBOARDLISTENERQT_H +#define CLIPBOARDLISTENERQT_H + +#include "clipboardlistener.h" +#include + +class ClipboardListenerQt : public ClipboardListener +{ + Q_OBJECT +public: + ClipboardListenerQt(); + + void setText(const QString & content) override; + +private: + void updateClipboard(QClipboard::Mode mode); + + QString m_currentContent; + QClipboard* m_clipboard; +#ifdef Q_OS_MAC + QTimer m_clipboardMonitorTimer; +#endif + +}; + +#endif diff --git a/plugins/clipboard/clipboardlistenerwayland.cpp b/plugins/clipboard/clipboardlistenerwayland.cpp new file mode 100644 index 000000000..008e99448 --- /dev/null +++ b/plugins/clipboard/clipboardlistenerwayland.cpp @@ -0,0 +1,172 @@ +/** + * Copyright 2019 Aleix Pol Gonzalez + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 . + */ + +#include "clipboardlistenerwayland.h" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +using namespace KWayland::Client; + +ClipboardListenerWayland::ClipboardListenerWayland() +{ + auto registry = new Registry(this); + + m_waylandConnection = ConnectionThread::fromApplication(this); + if (!m_waylandConnection) { + qWarning() << "Failed getting Wayland connection from QPA"; + return; + } + registry->create(m_waylandConnection); + registry->setup(); + + const auto ifaces = registry->interfaces(KWayland::Client::Registry::Interface::DataControlDeviceManager); + connect(registry, &Registry::dataControlDeviceManagerAnnounced, this, [this, registry](quint32 name, quint32 version) { + m_manager = registry->createDataControlDeviceManager(name, version, this); + m_source = m_manager->createDataSource(this); + connect(m_source, &DataControlSource::sendDataRequested, this, &ClipboardListenerWayland::sendData); + }); + + connect(registry, &Registry::interfacesAnnounced, this, [this] { + Q_ASSERT(m_seat); + m_dataControlDevice = m_manager->getDataDevice(m_seat, this); + connect(m_dataControlDevice, &DataControlDevice::selectionOffered, this, &ClipboardListenerWayland::selectionReceived); + }); +} + +const QString textMimetype = QStringLiteral("text/plain"); + +void ClipboardListenerWayland::setText(const QString& content) +{ + if (m_currentContent == content) + return; + + m_currentContent = content; + m_source->offer(textMimetype); +} + +void ClipboardListenerWayland::sendData(const QString& mimeType, qint32 fd) +{ + Q_ASSERT(mimeType == textMimetype); + + QByteArray content = m_currentContent.toUtf8(); + if (!content.isEmpty()) { + // Create a sigpipe handler that does nothing, or clients may be forced to terminate + // if the pipe is closed in the other end. + struct sigaction action, oldAction; + action.sa_handler = SIG_IGN; + sigemptyset (&action.sa_mask); + action.sa_flags = 0; + + sigaction(SIGPIPE, &action, &oldAction); + write(fd, content.constData(), content.size()); + sigaction(SIGPIPE, &oldAction, nullptr); + } + close(fd); +} + +static inline int qt_safe_pipe(int pipefd[2], int flags = 0) +{ + Q_ASSERT((flags & ~O_NONBLOCK) == 0); + +#ifdef QT_THREADSAFE_CLOEXEC + // use pipe2 + flags |= O_CLOEXEC; + return ::pipe2(pipefd, flags); // pipe2 is documented not to return EINTR +#else + int ret = ::pipe(pipefd); + if (ret == -1) + return -1; + + ::fcntl(pipefd[0], F_SETFD, FD_CLOEXEC); + ::fcntl(pipefd[1], F_SETFD, FD_CLOEXEC); + + // set non-block too? + if (flags & O_NONBLOCK) { + ::fcntl(pipefd[0], F_SETFL, ::fcntl(pipefd[0], F_GETFL) | O_NONBLOCK); + ::fcntl(pipefd[1], F_SETFL, ::fcntl(pipefd[1], F_GETFL) | O_NONBLOCK); + } + + return 0; +#endif +} + +static int readData(int fd, QByteArray &data) +{ + char buf[4096]; + int retryCount = 0; + int n; + while (true) { + n = read(fd, buf, sizeof buf); + if (n == -1 && (errno == EAGAIN || errno == EWOULDBLOCK) && ++retryCount < 1000) + usleep(1000); + else + break; + } + if (retryCount >= 1000) + qWarning("ClipboardListenerWayland: timeout reading from pipe"); + if (n > 0) { + data.append(buf, n); + n = readData(fd, data); + } + return n; +} + +QString ClipboardListenerWayland::readString(const QMimeType& mimeType, KWayland::Client::DataControlOffer* offer) +{ + int pipefd[2]; + if (qt_safe_pipe(pipefd, O_NONBLOCK) == -1) { + qWarning() << "unable to open offer"; + return {}; + } + + offer->receive(mimeType, pipefd[1]); + m_waylandConnection->flush(); + + close(pipefd[1]); + + QByteArray content; + if (readData(pipefd[0], content) != 0) { + qWarning("ClipboardListenerWayland: error reading data for mimeType %s", qPrintable(mimeType.name())); + content = {}; + } + + close(pipefd[0]); + return QString::fromUtf8(content); +} + +void ClipboardListenerWayland::selectionReceived(KWayland::Client::DataControlOffer* offer) +{ + const auto mimetypes = offer->offeredMimeTypes(); + for (const auto &mimetype : mimetypes) { + if (mimetype.inherits(textMimetype)) { + m_currentContent = readString(mimetype, offer); + break; + } + } +} diff --git a/plugins/clipboard/clipboardlistenerwayland.h b/plugins/clipboard/clipboardlistenerwayland.h new file mode 100644 index 000000000..f63e919a9 --- /dev/null +++ b/plugins/clipboard/clipboardlistenerwayland.h @@ -0,0 +1,51 @@ +/** + * Copyright 2019 Aleix Pol Gonzalez + * + * 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) version 3 or any later version + * accepted by the membership of KDE e.V. (or its successor approved + * by the membership of KDE e.V.), which shall act as a proxy + * defined in Section 14 of version 3 of the license. + * + * 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 . + */ + +#ifndef CLIPBOARDLISTENERWAYLAND_H +#define CLIPBOARDLISTENERWAYLAND_H + +#include "clipboardlistener.h" +#include +#include +#include +#include + +class ClipboardListenerWayland : public ClipboardListener +{ + Q_OBJECT +public: + ClipboardListenerWayland(); + + void setText(const QString & content) override; + +private: + void sendData(const QString &mimeType, qint32 fd); + void selectionReceived(KWayland::Client::DataControlOffer* offer); + QString readString(const QMimeType& mimeType, KWayland::Client::DataControlOffer* offer); + + QString m_currentContent; + KWayland::Client::ConnectionThread* m_waylandConnection = nullptr; + KWayland::Client::DataControlDeviceManager* m_manager = nullptr; + KWayland::Client::DataControlSource* m_source = nullptr; + KWayland::Client::Seat* m_seat = nullptr; + KWayland::Client::DataControlDevice* m_dataControlDevice = nullptr; +}; + +#endif