Commit b1ee38d2 authored by David Edmundson's avatar David Edmundson
Browse files

[klipper] Port to use wayland clipboard

Summary:
Wayland has an entire new protocol for getting and setting clipboard
when we don't have focus. Unfortunately this means reinventing
QClipboard from the QPA.

Not mergable as-is, especially the hardcoded line in CMakeLists but
uploaded to show direction.

It uses the newly agreed approach of using QtWayland generated classes
in clients directly rather than writing full pimpl API-fixed wrappers
first,
as ultimately that didn't really help do anything.

Code is written so that it can be moved to KWindowSystem if needed by
KDEConnect or others.

Test Plan: Copying and pasting all over the place
parent eb6fc12f
......@@ -11,6 +11,9 @@ find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Svg Widgets Quick
find_package(ECM ${KF5_MIN_VERSION} REQUIRED NO_MODULE)
set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH})
set(CMAKE_CXX_STANDARD 14)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
include(KDEInstallDirs)
include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
......
......@@ -3,6 +3,9 @@ add_definitions(-DTRANSLATION_DOMAIN=\"klipper\")
add_definitions("-DQT_NO_CAST_FROM_ASCII -DQT_NO_CAST_TO_ASCII")
add_definitions(-DQT_NO_NARROWING_CONVERSIONS_IN_CONNECT)
add_definitions(-DQT_NO_URL_CAST_FROM_STRING)
add_subdirectory(systemclipboard)
set(libklipper_common_SRCS
klipper.cpp
urlgrabber.cpp
......@@ -37,7 +40,6 @@ kconfig_add_kcfg_files(libklipper_common_SRCS klippersettings.kcfgc)
set(klipper_KDEINIT_SRCS ${libklipper_common_SRCS} main.cpp tray.cpp)
kf5_add_kdeinit_executable(klipper ${klipper_KDEINIT_SRCS})
target_link_libraries(kdeinit_klipper
......@@ -54,6 +56,7 @@ target_link_libraries(kdeinit_klipper
KF5::WidgetsAddons
KF5::XmlGui
${ZLIB_LIBRARY}
systemclipboard
)
if (X11_FOUND)
target_link_libraries(kdeinit_klipper XCB::XCB Qt5::X11Extras)
......@@ -88,6 +91,7 @@ target_link_libraries(plasma_engine_clipboard
KF5::WindowSystem
KF5::XmlGui # KActionCollection
${ZLIB_LIBRARY}
systemclipboard
)
if (X11_FOUND)
target_link_libraries(plasma_engine_clipboard XCB::XCB Qt5::X11Extras)
......
......@@ -52,6 +52,8 @@
#include "historystringitem.h"
#include "klipperpopup.h"
#include "systemclipboard.h"
#ifdef HAVE_PRISON
#include <prison/Prison>
#endif
......@@ -102,10 +104,9 @@ Klipper::Klipper(QObject* parent, const KSharedConfigPtr& config, KlipperMode mo
QDBusConnection::sessionBus().registerObject(QStringLiteral("/klipper"), this, QDBusConnection::ExportScriptableSlots);
updateTimestamp(); // read initial X user time
m_clip = qApp->clipboard();
m_clip = SystemClipboard::instance();
connect( m_clip, &QClipboard::changed,
this, &Klipper::newClipData );
connect( m_clip, &SystemClipboard::changed, this, &Klipper::newClipData );
connect( &m_overflowClearTimer, &QTimer::timeout, this, &Klipper::slotClearOverflow);
......@@ -717,17 +718,18 @@ void Klipper::checkClipData( bool selectionMode )
qCDebug(KLIPPER_LOG) << "Checking clip data";
const QMimeData* data = m_clip->mimeData( selectionMode ? QClipboard::Selection : QClipboard::Clipboard );
if ( !data ) {
qCWarning(KLIPPER_LOG) << "No data in clipboard. This not not supposed to happen.";
return;
}
bool clipEmpty = false;
bool changed = true; // ### FIXME (only relevant under polling, might be better to simply remove polling and rely on XFixes)
bool clipEmpty = data->formats().isEmpty();
if (clipEmpty) {
// Might be a timeout. Try again
if ( !data ) {
clipEmpty = true;
} else {
clipEmpty = data->formats().isEmpty();
qCDebug(KLIPPER_LOG) << "was empty. Retried, now " << (clipEmpty?" still empty":" no longer empty");
if (clipEmpty) {
// Might be a timeout. Try again
clipEmpty = data->formats().isEmpty();
qCDebug(KLIPPER_LOG) << "was empty. Retried, now " << (clipEmpty?" still empty":" no longer empty");
}
}
if ( changed && clipEmpty && m_bNoNullClipboard ) {
......
......@@ -41,6 +41,7 @@ class QMenu;
class QMimeData;
class HistoryItem;
class KNotification;
class SystemClipboard;
enum class KlipperMode {
Standalone,
......@@ -159,7 +160,7 @@ private:
static void updateTimestamp();
QClipboard* m_clip;
SystemClipboard* m_clip;
QElapsedTimer m_showTimer;
......
find_package(QtWaylandScanner REQUIRED)
include_directories(SYSTEM ${Qt5Gui_PRIVATE_INCLUDE_DIRS}) # for native interface to get wl_seat
find_package(Wayland 1.15 COMPONENTS Client)
find_package(Qt5 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS WaylandClient)
set(systemclipboard_SRCS
systemclipboard.cpp
qtclipboard.cpp
waylandclipboard.cpp
)
ecm_add_qtwayland_client_protocol(systemclipboard_SRCS
PROTOCOL wlr-data-control-unstable-v1.xml
BASENAME wlr-data-control-unstable-v1
)
add_library(systemclipboard STATIC ${systemclipboard_SRCS})
target_link_libraries(systemclipboard
Qt5::Gui
Qt5::WaylandClient
Wayland::Client
KF5::WindowSystem
)
if(BUILD_TESTING)
add_subdirectory(tests)
endif()
/*
Copyright (C) 2020 David Edmundson <davidedmundson@kde.org>
This program is free software; you can redistribute it and/or
modify it under the terms of the Lesser 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 Lesser GNU General Public License
along with this program; see the file COPYING. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "qtclipboard.h"
#include <QClipboard>
#include <QGuiApplication>
QtClipboard::QtClipboard(QObject *parent)
: SystemClipboard(parent)
{
connect(qApp->clipboard(), &QClipboard::changed, this, &QtClipboard::changed);
}
void QtClipboard::setMimeData(QMimeData *mime, QClipboard::Mode mode)
{
qApp->clipboard()->setMimeData(mime, mode);
}
void QtClipboard::clear(QClipboard::Mode mode)
{
qApp->clipboard()->clear(mode);
}
const QMimeData *QtClipboard::mimeData(QClipboard::Mode mode) const
{
return qApp->clipboard()->mimeData(mode);
}
/*
Copyright (C) 2020 David Edmundson <davidedmundson@kde.org>
This program is free software; you can redistribute it and/or
modify it under the terms of the Lesser 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 Lesser GNU General Public License
along with this program; see the file COPYING. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#pragma once
#include "systemclipboard.h"
class QtClipboard : public SystemClipboard
{
public:
explicit QtClipboard(QObject *parent);
void setMimeData(QMimeData *mime, QClipboard::Mode mode) override;
void clear(QClipboard::Mode mode) override;
const QMimeData *mimeData(QClipboard::Mode mode) const override;
};
/*
Copyright (C) 2020 David Edmundson <davidedmundson@kde.org>
This program is free software; you can redistribute it and/or
modify it under the terms of the Lesser 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 Lesser GNU General Public License
along with this program; see the file COPYING. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "systemclipboard.h"
#include "qtclipboard.h"
#include "waylandclipboard.h"
#include <QGuiApplication>
#include <KWindowSystem>
SystemClipboard *SystemClipboard::instance()
{
if (!qApp || qApp->closingDown()) {
return nullptr;
}
static SystemClipboard *systemClipboard = nullptr;
if (!systemClipboard) {
if (KWindowSystem::isPlatformWayland()) {
systemClipboard = new WaylandClipboard(qApp);
} else {
systemClipboard = new QtClipboard(qApp);
}
}
return systemClipboard;
}
SystemClipboard::SystemClipboard(QObject *parent)
: QObject(parent)
{
}
/*
Copyright (C) 2020 David Edmundson <davidedmundson@kde.org>
This program is free software; you can redistribute it and/or
modify it under the terms of the Lesser 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 Lesser GNU General Public License
along with this program; see the file COPYING. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#pragma once
#include <QObject>
#include <QClipboard>
#include <QMimeData>
/**
* This class mimics QClipboard but unlike QClipboard it will continue
* to get updates even when our window does not have focus.
*
* This may require extra access permissions
*/
class SystemClipboard : public QObject
{
Q_OBJECT
public:
/**
* Returns a shared global SystemClipboard instance
*/
static SystemClipboard *instance();
/**
* Sets the clipboard to the new contents
* The clpboard takes ownership of mime
*/
//maybe I should unique_ptr it to be expressive, but then I don't match QClipboard?
virtual void setMimeData(QMimeData *mime, QClipboard::Mode mode) = 0;
/**
* Clears the current clipboard
*/
virtual void clear(QClipboard::Mode mode) = 0;
/**
* Returns the current mime data received by the clipboard
*/
virtual const QMimeData *mimeData(QClipboard::Mode mode) const = 0;
Q_SIGNALS:
void changed(QClipboard::Mode mode);
protected:
SystemClipboard(QObject *parent);
};
add_executable(pasteclient paste.cpp)
target_link_libraries(pasteclient
systemclipboard
)
/*
* Copyright 2020 David Edmundson <davidedmundson@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, write to the Free Software
* Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
*/
#include <QGuiApplication>
#include <QDebug>
#include "../systemclipboard/systemclipboard.h"
int main(int argc, char ** argv)
{
QGuiApplication app(argc, argv);
auto clip = SystemClipboard::instance();
QObject::connect(clip, &SystemClipboard::changed, &app, [clip](QClipboard::Mode mode) {
if (mode != QClipboard::Clipboard) {
return;
}
auto dbg = qDebug();
dbg << "New clipboard content: ";
if (clip->mimeData(QClipboard::Clipboard)) {
dbg << clip->mimeData(QClipboard::Clipboard)->text();
} else {
dbg << "[empty]";
}
});
qDebug() << "Watching for new clipboard content...";
app.exec();
}
/*
Copyright (C) 2020 David Edmundson <davidedmundson@kde.org>
This program is free software; you can redistribute it and/or
modify it under the terms of the Lesser 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 Lesser GNU General Public License
along with this program; see the file COPYING. If not, write to
the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor,
Boston, MA 02110-1301, USA.
*/
#include "waylandclipboard.h"
#include <QFile>
#include <QFutureWatcher>
#include <QPointer>
#include <QDebug>
#include <QGuiApplication>
#include <QtWaylandClient/QWaylandClientExtension>
#include <qpa/qplatformnativeinterface.h>
#include <unistd.h>
#include "qwayland-wlr-data-control-unstable-v1.h"
class DataControlDeviceManager : public QWaylandClientExtensionTemplate<DataControlDeviceManager>
, public QtWayland::zwlr_data_control_manager_v1
{
Q_OBJECT
public:
DataControlDeviceManager()
: QWaylandClientExtensionTemplate<DataControlDeviceManager>(1)
{
}
~DataControlDeviceManager() {
destroy();
}
};
class DataControlOffer : public QMimeData, public QtWayland::zwlr_data_control_offer_v1
{
Q_OBJECT
public:
DataControlOffer(struct ::zwlr_data_control_offer_v1 *id):
QtWayland::zwlr_data_control_offer_v1(id)
{
}
~DataControlOffer() {
destroy();
}
QStringList formats() const override
{
return m_receivedFormats;
}
bool hasFormat(const QString &format) const override {
return m_receivedFormats.contains(format);
}
protected:
void zwlr_data_control_offer_v1_offer(const QString &mime_type) override {
m_receivedFormats << mime_type;
}
QVariant retrieveData(const QString &mimeType, QVariant::Type type) const override;
private:
static bool readData(int fd, QByteArray &data);
QStringList m_receivedFormats;
};
QVariant DataControlOffer::retrieveData(const QString &mimeType, QVariant::Type type) const
{
if (!hasFormat(mimeType)) {
return QVariant();
}
Q_UNUSED(type);
int pipeFds[2];
if (pipe(pipeFds) != 0){
return QVariant();
}
auto t = const_cast<DataControlOffer*>(this);
t->receive(mimeType, pipeFds[1]);
close(pipeFds[1]);
/*
* Ideally we need to introduce a non-blocking QMimeData object
* Or a non-blocking constructor to QMimeData with the mimetypes that are relevant
*
* However this isn't actually any worse than X.
*/
QPlatformNativeInterface *native = qApp->platformNativeInterface();
auto display = static_cast<struct ::wl_display*>(native->nativeResourceForIntegration("wl_display"));
wl_display_flush(display);
QFile readPipe;
if (readPipe.open(pipeFds[0], QIODevice::ReadOnly)) {
QByteArray data;
if (readData(pipeFds[0], data)) {
return data;
}
close(pipeFds[0]);
}
return QVariant();
}
// reads data from a file descriptor with a timeout of 1 second
// true if data is read successfully
bool DataControlOffer::readData(int fd, QByteArray &data)
{
fd_set readset;
FD_ZERO(&readset);
FD_SET(fd, &readset);
struct timeval timeout;
timeout.tv_sec = 1;
timeout.tv_usec = 0;
Q_FOREVER {
int ready = select(FD_SETSIZE, &readset, nullptr, nullptr, &timeout);
if (ready < 0) {
qWarning() << "DataControlOffer: select() failed";
return false;
} else if (ready == 0) {
qWarning("DataControlOffer: timeout reading from pipe");
return false;
} else {
char buf[4096];
int n = read(fd, buf, sizeof buf);
if (n < 0) {
qWarning("DataControlOffer: read() failed");
return false;
} else if (n == 0) {
return true;
} else if (n > 0) {
data.append(buf, n);
}
}
}
}
class DataControlSource : public QObject, public QtWayland::zwlr_data_control_source_v1
{
Q_OBJECT
public:
DataControlSource(struct ::zwlr_data_control_source_v1 *id, QMimeData *mimeData);
DataControlSource();
~DataControlSource() {
destroy();
}
QMimeData *mimeData() {
return m_mimeData;
}
Q_SIGNALS:
void cancelled();
protected:
void zwlr_data_control_source_v1_send(const QString &mime_type, int32_t fd) override;
void zwlr_data_control_source_v1_cancelled() override;
private:
QMimeData *m_mimeData;
};
DataControlSource::DataControlSource(struct ::zwlr_data_control_source_v1 *id, QMimeData *mimeData)
: QtWayland::zwlr_data_control_source_v1(id)
, m_mimeData(mimeData)
{
for (const QString &format: mimeData->formats()) {
offer(format);
}
}
void DataControlSource::zwlr_data_control_source_v1_send(const QString &mime_type, int32_t fd)
{
QFile c;
if (c.open(fd, QFile::WriteOnly, QFile::AutoCloseHandle)) {
c.write(m_mimeData->data(mime_type));
c.close();
}
}
void DataControlSource::zwlr_data_control_source_v1_cancelled()
{
Q_EMIT cancelled();
}
class DataControlDevice : public QObject, public QtWayland::zwlr_data_control_device_v1
{
Q_OBJECT
public:
DataControlDevice(struct ::zwlr_data_control_device_v1 *id)
: QtWayland::zwlr_data_control_device_v1(id)
{}
~DataControlDevice() {
destroy();
}
void setSelection(std::unique_ptr<DataControlSource> selection);
QMimeData *receivedSelection() {
return m_receivedSelection.get();
}
QMimeData *selection() {
return m_selection ? m_selection->mimeData() : nullptr;
}
Q_SIGNALS: