clipboard: add support for wayland DataDeviceControl

This commit is contained in:
Aleix Pol 2019-07-21 11:29:17 +02:00
parent aa7d6d2339
commit 9ee096d18c
7 changed files with 354 additions and 53 deletions

View file

@ -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()

View file

@ -19,35 +19,27 @@
*/
#include "clipboardlistener.h"
#include "clipboardlistenerqt.h"
#ifdef WITH_KWAYLAND
#include "clipboardlistenerwayland.h"
#endif
ClipboardListener::ClipboardListener()
: clipboard(QGuiApplication::clipboard())
{}
ClipboardListener* ClipboardListener::instance()
{
#ifdef Q_OS_MAC
connect(&m_clipboardMonitorTimer, &QTimer::timeout, this, [this](){ updateClipboard(QClipboard::Clipboard); });
m_clipboardMonitorTimer.start(1000); // Refresh 1s
static ClipboardListener* me = nullptr;
if (!me) {
#ifdef WITH_KWAYLAND
if (QGuiApplication::platformName().startsWith(QLatin1String("wayland"), Qt::CaseInsensitive)) {
me = new ClipboardListenerWayland();
}
else
#endif
connect(clipboard, &QClipboard::changed, this, &ClipboardListener::updateClipboard);
}
void ClipboardListener::updateClipboard(QClipboard::Mode mode)
{
if (mode != QClipboard::Clipboard) {
return;
{
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;
}

View file

@ -21,7 +21,6 @@
#ifndef CLIPBOARDLISTENER_H
#define CLIPBOARDLISTENER_H
#include <QTimer>
#include <QObject>
#include <QClipboard>
#include <QGuiApplication>
@ -33,28 +32,12 @@ 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);

View file

@ -0,0 +1,54 @@
/**
* Copyright 2016 Albert Vaca <albertvaka@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#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);
}

View file

@ -0,0 +1,46 @@
/**
* Copyright 2016 Albert Vaca <albertvaka@gmail.com>
*
* 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 <https://www.gnu.org/licenses/>.
*/
#ifndef CLIPBOARDLISTENERQT_H
#define CLIPBOARDLISTENERQT_H
#include "clipboardlistener.h"
#include <QTimer>
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

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
#include "clipboardlistenerwayland.h"
#include <KWayland/Client/registry.h>
#include <KWayland/Client/datacontroldevice.h>
#include <KWayland/Client/seat.h>
#include <KWayland/Client/connection_thread.h>
#include <QDebug>
#include <QMimeType>
#include <QMimeData>
#include <unistd.h>
#include <signal.h>
#include <fcntl.h>
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;
}
}
}

View file

@ -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 <https://www.gnu.org/licenses/>.
*/
#ifndef CLIPBOARDLISTENERWAYLAND_H
#define CLIPBOARDLISTENERWAYLAND_H
#include "clipboardlistener.h"
#include <KWayland/Client/datacontroldevicemanager.h>
#include <KWayland/Client/datacontrolsource.h>
#include <KWayland/Client/datacontroloffer.h>
#include <KWayland/Client/connection_thread.h>
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