diff --git a/klipper/CMakeLists.txt b/klipper/CMakeLists.txt --- a/klipper/CMakeLists.txt +++ b/klipper/CMakeLists.txt @@ -3,6 +3,9 @@ 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 @@ 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 @@ KF5::WidgetsAddons KF5::XmlGui ${ZLIB_LIBRARY} + systemclipboard ) if (X11_FOUND) target_link_libraries(kdeinit_klipper XCB::XCB Qt5::X11Extras) @@ -88,6 +91,7 @@ KF5::WindowSystem KF5::XmlGui # KActionCollection ${ZLIB_LIBRARY} + systemclipboard ) if (X11_FOUND) target_link_libraries(plasma_engine_clipboard XCB::XCB Qt5::X11Extras) @@ -102,5 +106,7 @@ add_subdirectory(autotests) endif() +add_subdirectory(tests) + install( FILES klipper.categories DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} ) diff --git a/klipper/klipper.h b/klipper/klipper.h --- a/klipper/klipper.h +++ b/klipper/klipper.h @@ -41,6 +41,7 @@ class QMimeData; class HistoryItem; class KNotification; +class SystemClipboard; enum class KlipperMode { Standalone, @@ -159,7 +160,7 @@ static void updateTimestamp(); - QClipboard* m_clip; + SystemClipboard* m_clip; QElapsedTimer m_showTimer; diff --git a/klipper/klipper.cpp b/klipper/klipper.cpp --- 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 @@ 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/systemclipboard/CMakeLists.txt b/klipper/systemclipboard/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/klipper/systemclipboard/CMakeLists.txt @@ -0,0 +1,23 @@ +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 /home/david/projects/kde5/src/kde/workspace/kwayland-server/src/protocols/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 + KF5::WindowSystem + Wayland::Client + ) diff --git a/klipper/systemclipboard/qtclipboard.h b/klipper/systemclipboard/qtclipboard.h new file mode 100644 --- /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: + 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/qtclipboard.cpp b/klipper/systemclipboard/qtclipboard.cpp new file mode 100644 --- /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/systemclipboard.h b/klipper/systemclipboard/systemclipboard.h new file mode 100644 --- /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/systemclipboard.cpp b/klipper/systemclipboard/systemclipboard.cpp new file mode 100644 --- /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/waylandclipboard.h b/klipper/systemclipboard/waylandclipboard.h new file mode 100644 --- /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: + QScopedPointer m_manager; + std::unique_ptr m_device; +}; diff --git a/klipper/systemclipboard/waylandclipboard.cpp b/klipper/systemclipboard/waylandclipboard.cpp new file mode 100644 --- /dev/null +++ b/klipper/systemclipboard/waylandclipboard.cpp @@ -0,0 +1,285 @@ +/* + 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 + +#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: + QStringList m_receivedFormats; +}; + + +QVariant DataControlOffer::retrieveData(const QString &mimeType, QVariant::Type type) const +{ + if (!hasFormat(mimeType)) { + return QVariant(); + } + + qDebug() << "Getting me some data" << mimeType; + 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]); + + /* + * Obviously this shouldn't block. + * At a super bare minimum it should have Qt's timeout code + * Ideally we need to introduce a non-blocking QMimeData object + */ + + 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; + data = readPipe.readAll(); + qDebug() << "Retrieved: " << data; + close(pipeFds[0]); + return data; + } + + qDebug() << "failed :("; + return QVariant(); +} + +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); + } + offer(QStringLiteral("set_from_klipper")); // hack to detect loops +} + +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); + m_receivedSelection.reset(offer); + } + if (m_receivedSelection && m_receivedSelection->hasFormat(QStringLiteral("set_from_klipper"))) { + return; + } + 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.data(), &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::unique_ptr(new DataControlSource(m_manager->create_data_source(), mime)); + if (mode == QClipboard::Selection) { + 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/tests/CMakeLists.txt b/klipper/tests/CMakeLists.txt new file mode 100644 --- /dev/null +++ b/klipper/tests/CMakeLists.txt @@ -0,0 +1,5 @@ +add_executable(pasteclient paste.cpp) + +target_link_libraries(pasteclient + systemclipboard +) diff --git a/klipper/tests/paste.cpp b/klipper/tests/paste.cpp new file mode 100644 --- /dev/null +++ b/klipper/tests/paste.cpp @@ -0,0 +1,27 @@ +#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(); +}