From 8b169842340b90a3cc04021aec5683c80447cdcb Mon Sep 17 00:00:00 2001 From: David Edmundson Date: Thu, 14 May 2020 22:08:15 +0100 Subject: [PATCH 1/3] [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 --- CMakeLists.txt | 3 + klipper/CMakeLists.txt | 6 +- klipper/klipper.cpp | 7 +- klipper/klipper.h | 3 +- klipper/systemclipboard/CMakeLists.txt | 27 ++ klipper/systemclipboard/qtclipboard.cpp | 44 +++ klipper/systemclipboard/qtclipboard.h | 31 ++ klipper/systemclipboard/systemclipboard.cpp | 48 +++ klipper/systemclipboard/systemclipboard.h | 60 ++++ klipper/systemclipboard/tests/CMakeLists.txt | 5 + klipper/systemclipboard/tests/paste.cpp | 45 +++ klipper/systemclipboard/waylandclipboard.cpp | 315 ++++++++++++++++++ klipper/systemclipboard/waylandclipboard.h | 37 ++ .../wlr-data-control-unstable-v1.xml | 278 ++++++++++++++++ 14 files changed, 904 insertions(+), 5 deletions(-) create mode 100644 klipper/systemclipboard/CMakeLists.txt create mode 100644 klipper/systemclipboard/qtclipboard.cpp create mode 100644 klipper/systemclipboard/qtclipboard.h create mode 100644 klipper/systemclipboard/systemclipboard.cpp create mode 100644 klipper/systemclipboard/systemclipboard.h create mode 100644 klipper/systemclipboard/tests/CMakeLists.txt create mode 100644 klipper/systemclipboard/tests/paste.cpp create mode 100644 klipper/systemclipboard/waylandclipboard.cpp create mode 100644 klipper/systemclipboard/waylandclipboard.h create mode 100644 klipper/systemclipboard/wlr-data-control-unstable-v1.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index 9d16a22b3d..17a923d1c2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/klipper/CMakeLists.txt b/klipper/CMakeLists.txt index 5d400c61c4..25bbcdb4dc 100644 --- a/klipper/CMakeLists.txt +++ b/klipper/CMakeLists.txt @@ -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) diff --git a/klipper/klipper.cpp b/klipper/klipper.cpp index d022c4a43f..00b7edbb7e 100644 --- a/klipper/klipper.cpp +++ b/klipper/klipper.cpp @@ -52,6 +52,8 @@ #include "historystringitem.h" #include "klipperpopup.h" +#include "systemclipboard.h" + #ifdef HAVE_PRISON #include #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); diff --git a/klipper/klipper.h b/klipper/klipper.h index 2651b0fd78..44ac5c2930 100644 --- a/klipper/klipper.h +++ b/klipper/klipper.h @@ -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; diff --git a/klipper/systemclipboard/CMakeLists.txt b/klipper/systemclipboard/CMakeLists.txt new file mode 100644 index 0000000000..bb90fa4dca --- /dev/null +++ b/klipper/systemclipboard/CMakeLists.txt @@ -0,0 +1,27 @@ +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() diff --git a/klipper/systemclipboard/qtclipboard.cpp b/klipper/systemclipboard/qtclipboard.cpp new file mode 100644 index 0000000000..884458e937 --- /dev/null +++ b/klipper/systemclipboard/qtclipboard.cpp @@ -0,0 +1,44 @@ +/* + Copyright (C) 2020 David Edmundson + + 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 +#include + +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); +} diff --git a/klipper/systemclipboard/qtclipboard.h b/klipper/systemclipboard/qtclipboard.h new file mode 100644 index 0000000000..2a29121878 --- /dev/null +++ b/klipper/systemclipboard/qtclipboard.h @@ -0,0 +1,31 @@ +/* + Copyright (C) 2020 David Edmundson + + 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; +}; + diff --git a/klipper/systemclipboard/systemclipboard.cpp b/klipper/systemclipboard/systemclipboard.cpp new file mode 100644 index 0000000000..d40a233077 --- /dev/null +++ b/klipper/systemclipboard/systemclipboard.cpp @@ -0,0 +1,48 @@ +/* + Copyright (C) 2020 David Edmundson + + 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 +#include + +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) +{ +} + diff --git a/klipper/systemclipboard/systemclipboard.h b/klipper/systemclipboard/systemclipboard.h new file mode 100644 index 0000000000..4ce169e23e --- /dev/null +++ b/klipper/systemclipboard/systemclipboard.h @@ -0,0 +1,60 @@ +/* + Copyright (C) 2020 David Edmundson + + 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 +#include +#include + +/** + * 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); +}; diff --git a/klipper/systemclipboard/tests/CMakeLists.txt b/klipper/systemclipboard/tests/CMakeLists.txt new file mode 100644 index 0000000000..da0566691f --- /dev/null +++ b/klipper/systemclipboard/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +add_executable(pasteclient paste.cpp) + +target_link_libraries(pasteclient + systemclipboard +) diff --git a/klipper/systemclipboard/tests/paste.cpp b/klipper/systemclipboard/tests/paste.cpp new file mode 100644 index 0000000000..844d5a91f3 --- /dev/null +++ b/klipper/systemclipboard/tests/paste.cpp @@ -0,0 +1,45 @@ +/* + * Copyright 2020 David Edmundson + * + * 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 +#include + +#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(); +} diff --git a/klipper/systemclipboard/waylandclipboard.cpp b/klipper/systemclipboard/waylandclipboard.cpp new file mode 100644 index 0000000000..c890c516e7 --- /dev/null +++ b/klipper/systemclipboard/waylandclipboard.cpp @@ -0,0 +1,315 @@ +/* + Copyright (C) 2020 David Edmundson + + 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 +#include +#include +#include +#include + +#include + +#include + +#include + +#include "qwayland-wlr-data-control-unstable-v1.h" + +class DataControlDeviceManager : public QWaylandClientExtensionTemplate + , public QtWayland::zwlr_data_control_manager_v1 +{ + Q_OBJECT +public: + DataControlDeviceManager() + : QWaylandClientExtensionTemplate(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(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(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(); + } + +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 selection); + DataControlOffer *receivedSelection() { + return m_receivedSelection.get(); + } + +Q_SIGNALS: + void receivedSelectionChanged(); +protected: + void zwlr_data_control_device_v1_data_offer(struct ::zwlr_data_control_offer_v1 *id) override { + new DataControlOffer(id); + // this will become memory managed when we retrieve the selection event + // a compositor calling data_offer without doing that would be a bug + } + + void zwlr_data_control_device_v1_selection(struct ::zwlr_data_control_offer_v1 *id) override { + if(!id ) { + m_receivedSelection.reset(); + } else { + auto deriv = QtWayland::zwlr_data_control_offer_v1::fromObject(id); + auto offer = dynamic_cast(deriv); // dynamic because of the dual inheritance + m_receivedSelection.reset(offer); + } + emit receivedSelectionChanged(); + } + +private: + std::unique_ptr m_selection; // selection set locally + std::unique_ptr m_receivedSelection; // latest selection set from externally to here +}; + + +void DataControlDevice::setSelection(std::unique_ptr selection) +{ + m_selection = std::move(selection); + connect(m_selection.get(), &DataControlSource::cancelled, this, [this]() { + m_selection.reset(); + }); + set_selection(m_selection->object()); +} + +WaylandClipboard::WaylandClipboard(QObject *parent) + : SystemClipboard(parent) + , m_manager(new DataControlDeviceManager) +{ + connect(m_manager.get(), &DataControlDeviceManager::activeChanged, this, [this]() { + if (m_manager->isActive()) { + + QPlatformNativeInterface *native = qApp->platformNativeInterface(); + if (!native) { + return; + } + auto seat = static_cast(native->nativeResourceForIntegration("wl_seat")); + if (!seat) { + return; + } + + m_device.reset(new DataControlDevice(m_manager->get_data_device(seat))); + + connect(m_device.get(), &DataControlDevice::receivedSelectionChanged, this, [this]() { + emit changed(QClipboard::Clipboard); + }); + } else { + m_device.reset(); + } + }); +} + +void WaylandClipboard::setMimeData(QMimeData *mime, QClipboard::Mode mode) +{ + if (!m_device) { + return; + } + auto source = std::make_unique(m_manager->create_data_source(), mime); + if (mode == QClipboard::Clipboard) { + m_device->setSelection(std::move(source)); + } +} + +void WaylandClipboard::clear(QClipboard::Mode mode) +{ + if (!m_device) { + return; + } + if (mode == QClipboard::Clipboard) { + m_device->set_selection(nullptr); + } else if (mode == QClipboard::Selection) { + m_device->set_primary_selection(nullptr); + } +} + +const QMimeData *WaylandClipboard::mimeData(QClipboard::Mode mode) const +{ + if (!m_device) { + return nullptr; + } + if (mode == QClipboard::Clipboard) { + return m_device->receivedSelection(); + } + return nullptr; +} + +#include "waylandclipboard.moc" diff --git a/klipper/systemclipboard/waylandclipboard.h b/klipper/systemclipboard/waylandclipboard.h new file mode 100644 index 0000000000..7d950cb12b --- /dev/null +++ b/klipper/systemclipboard/waylandclipboard.h @@ -0,0 +1,37 @@ +/* + Copyright (C) 2020 David Edmundson + + 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" +#include + +class DataControlDevice; +class DataControlDeviceManager; + +class WaylandClipboard : public SystemClipboard +{ +public: + WaylandClipboard(QObject *parent); + void setMimeData(QMimeData *mime, QClipboard::Mode mode) override; + void clear(QClipboard::Mode mode) override; + const QMimeData *mimeData(QClipboard::Mode mode) const override; +private: + std::unique_ptr m_manager; + std::unique_ptr m_device; +}; diff --git a/klipper/systemclipboard/wlr-data-control-unstable-v1.xml b/klipper/systemclipboard/wlr-data-control-unstable-v1.xml new file mode 100644 index 0000000000..75e8671b0d --- /dev/null +++ b/klipper/systemclipboard/wlr-data-control-unstable-v1.xml @@ -0,0 +1,278 @@ + + + + Copyright © 2018 Simon Ser + Copyright © 2019 Ivan Molodetskikh + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + This protocol allows a privileged client to control data devices. In + particular, the client will be able to manage the current selection and take + the role of a clipboard manager. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This interface is a manager that allows creating per-seat data device + controls. + + + + + Create a new data source. + + + + + + + Create a data device that can be used to manage a seat's selection. + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + This interface allows a client to manage a seat's selection. + + When the seat is destroyed, this object becomes inert. + + + + + This request asks the compositor to set the selection to the data from + the source on behalf of the client. + + The given source may not be used in any further set_selection or + set_primary_selection requests. Attempting to use a previously used + source is a protocol error. + + To unset the selection, set the source to NULL. + + + + + + + Destroys the data device object. + + + + + + The data_offer event introduces a new wlr_data_control_offer object, + which will subsequently be used in either the + wlr_data_control_device.selection event (for the regular clipboard + selections) or the wlr_data_control_device.primary_selection event (for + the primary clipboard selections). Immediately following the + wlr_data_control_device.data_offer event, the new data_offer object + will send out wlr_data_control_offer.offer events to describe the MIME + types it offers. + + + + + + + The selection event is sent out to notify the client of a new + wlr_data_control_offer for the selection for this device. The + wlr_data_control_device.data_offer and the wlr_data_control_offer.offer + events are sent out immediately before this event to introduce the data + offer object. The selection event is sent to a client when a new + selection is set. The wlr_data_control_offer is valid until a new + wlr_data_control_offer or NULL is received. The client must destroy the + previous selection wlr_data_control_offer, if any, upon receiving this + event. + + The first selection event is sent upon binding the + wlr_data_control_device object. + + + + + + + This data control object is no longer valid and should be destroyed by + the client. + + + + + + + + The primary_selection event is sent out to notify the client of a new + wlr_data_control_offer for the primary selection for this device. The + wlr_data_control_device.data_offer and the wlr_data_control_offer.offer + events are sent out immediately before this event to introduce the data + offer object. The primary_selection event is sent to a client when a + new primary selection is set. The wlr_data_control_offer is valid until + a new wlr_data_control_offer or NULL is received. The client must + destroy the previous primary selection wlr_data_control_offer, if any, + upon receiving this event. + + If the compositor supports primary selection, the first + primary_selection event is sent upon binding the + wlr_data_control_device object. + + + + + + + This request asks the compositor to set the primary selection to the + data from the source on behalf of the client. + + The given source may not be used in any further set_selection or + set_primary_selection requests. Attempting to use a previously used + source is a protocol error. + + To unset the primary selection, set the source to NULL. + + The compositor will ignore this request if it does not support primary + selection. + + + + + + + + + + + + The wlr_data_control_source object is the source side of a + wlr_data_control_offer. It is created by the source client in a data + transfer and provides a way to describe the offered data and a way to + respond to requests to transfer the data. + + + + + + + + + This request adds a MIME type to the set of MIME types advertised to + targets. Can be called several times to offer multiple types. + + Calling this after wlr_data_control_device.set_selection is a protocol + error. + + + + + + + Destroys the data source object. + + + + + + Request for data from the client. Send the data as the specified MIME + type over the passed file descriptor, then close it. + + + + + + + + This data source is no longer valid. The data source has been replaced + by another data source. + + The client should clean up and destroy this data source. + + + + + + + A wlr_data_control_offer represents a piece of data offered for transfer + by another client (the source client). The offer describes the different + MIME types that the data can be converted to and provides the mechanism + for transferring the data directly from the source client. + + + + + To transfer the offered data, the client issues this request and + indicates the MIME type it wants to receive. The transfer happens + through the passed file descriptor (typically created with the pipe + system call). The source client writes the data in the MIME type + representation requested and then closes the file descriptor. + + The receiving client reads from the read end of the pipe until EOF and + then closes its end, at which point the transfer is complete. + + This request may happen multiple times for different MIME types. + + + + + + + + Destroys the data offer object. + + + + + + Sent immediately after creating the wlr_data_control_offer object. + One event per offered MIME type. + + + + + -- GitLab From 17347c21077f914797ab7eaf96edc46ce33813f6 Mon Sep 17 00:00:00 2001 From: David Edmundson Date: Thu, 23 Jul 2020 11:04:29 +0100 Subject: [PATCH 2/3] [klipper] Treat null mimedata as the same as an empty clipboard On QtClipboard a QMimeData object is always returned an object even if it's blank. The wayland implementation behaves slightly differently and it is adjusted to act the same instead of being an error. --- klipper/klipper.cpp | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/klipper/klipper.cpp b/klipper/klipper.cpp index 00b7edbb7e..7fd7589454 100644 --- a/klipper/klipper.cpp +++ b/klipper/klipper.cpp @@ -718,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 ) { -- GitLab From 9e7b1d59784ec31a64457d18aed5a5a69d7a5c8d Mon Sep 17 00:00:00 2001 From: David Edmundson Date: Thu, 23 Jul 2020 11:04:44 +0100 Subject: [PATCH 3/3] [klipper] Reuse local QMimeData if we still own the clipboard This avoids trying to pass data to ourselves --- klipper/systemclipboard/waylandclipboard.cpp | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/klipper/systemclipboard/waylandclipboard.cpp b/klipper/systemclipboard/waylandclipboard.cpp index c890c516e7..d32a9321e2 100644 --- a/klipper/systemclipboard/waylandclipboard.cpp +++ b/klipper/systemclipboard/waylandclipboard.cpp @@ -166,6 +166,10 @@ public: destroy(); } + QMimeData *mimeData() { + return m_mimeData; + } + Q_SIGNALS: void cancelled(); @@ -212,12 +216,16 @@ public: } void setSelection(std::unique_ptr selection); - DataControlOffer *receivedSelection() { + QMimeData *receivedSelection() { return m_receivedSelection.get(); } + QMimeData *selection() { + return m_selection ? m_selection->mimeData() : nullptr; + } Q_SIGNALS: void receivedSelectionChanged(); + void selectionChanged(); protected: void zwlr_data_control_device_v1_data_offer(struct ::zwlr_data_control_offer_v1 *id) override { new DataControlOffer(id); @@ -247,8 +255,10 @@ void DataControlDevice::setSelection(std::unique_ptr selectio m_selection = std::move(selection); connect(m_selection.get(), &DataControlSource::cancelled, this, [this]() { m_selection.reset(); + Q_EMIT selectionChanged(); }); set_selection(m_selection->object()); + Q_EMIT selectionChanged(); } WaylandClipboard::WaylandClipboard(QObject *parent) @@ -272,6 +282,9 @@ WaylandClipboard::WaylandClipboard(QObject *parent) connect(m_device.get(), &DataControlDevice::receivedSelectionChanged, this, [this]() { emit changed(QClipboard::Clipboard); }); + connect(m_device.get(), &DataControlDevice::selectionChanged, this, [this]() { + emit changed(QClipboard::Clipboard); + }); } else { m_device.reset(); } @@ -307,7 +320,8 @@ const QMimeData *WaylandClipboard::mimeData(QClipboard::Mode mode) const return nullptr; } if (mode == QClipboard::Clipboard) { - return m_device->receivedSelection(); + // return our locally set selection if it's not cancelled to avoid copying data to ourselves + return m_device->selection() ? m_device->selection() : m_device->receivedSelection(); } return nullptr; } -- GitLab