Index: CMakeLists.txt =================================================================== --- CMakeLists.txt +++ CMakeLists.txt @@ -640,13 +640,31 @@ install(TARGETS kdeinit_kwin_x11 ${INSTALL_TARGETS_DEFAULT_ARGS} ) install(TARGETS kwin_x11 ${INSTALL_TARGETS_DEFAULT_ARGS} ) +set(kwin_XWAYLAND_SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/xwayland.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/databridge.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/selection.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/selection_source.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/transfer.cpp + ${CMAKE_CURRENT_SOURCE_DIR}/xwl/clipboard.cpp + ) +include(ECMQtDeclareLoggingCategory) +ecm_qt_declare_logging_category(kwin_XWAYLAND_SRCS + HEADER + xwayland_logging.h + IDENTIFIER + KWIN_XWL + CATEGORY_NAME + kwin_xwl + DEFAULT_SEVERITY + Critical +) + set(kwin_WAYLAND_SRCS tabletmodemanager.cpp main_wayland.cpp - xwl/xwayland.cpp ) - -add_executable(kwin_wayland ${kwin_WAYLAND_SRCS}) +add_executable(kwin_wayland ${kwin_WAYLAND_SRCS} ${kwin_XWAYLAND_SRCS}) target_link_libraries(kwin_wayland kwin) if (HAVE_LIBCAP) target_link_libraries(kwin_wayland ${Libcap_LIBRARIES}) Index: atoms.h =================================================================== --- atoms.h +++ atoms.h @@ -67,9 +67,17 @@ Xcb::Atom gtk_frame_extents; Xcb::Atom kwin_dbus_service; Xcb::Atom utf8_string; + Xcb::Atom text; + Xcb::Atom uri_list; Xcb::Atom wl_surface_id; Xcb::Atom kde_net_wm_appmenu_service_name; Xcb::Atom kde_net_wm_appmenu_object_path; + Xcb::Atom clipboard; + Xcb::Atom timestamp; + Xcb::Atom targets; + Xcb::Atom delete_atom; + Xcb::Atom incr; + Xcb::Atom wl_selection; /** * @internal Index: atoms.cpp =================================================================== --- atoms.cpp +++ atoms.cpp @@ -58,9 +58,17 @@ , gtk_frame_extents(QByteArrayLiteral("_GTK_FRAME_EXTENTS")) , kwin_dbus_service(QByteArrayLiteral("_ORG_KDE_KWIN_DBUS_SERVICE")) , utf8_string(QByteArrayLiteral("UTF8_STRING")) + , text(QByteArrayLiteral("TEXT")) + , uri_list(QByteArrayLiteral("text/uri-list")) , wl_surface_id(QByteArrayLiteral("WL_SURFACE_ID")) , kde_net_wm_appmenu_service_name(QByteArrayLiteral("_KDE_NET_WM_APPMENU_SERVICE_NAME")) , kde_net_wm_appmenu_object_path(QByteArrayLiteral("_KDE_NET_WM_APPMENU_OBJECT_PATH")) + , clipboard(QByteArrayLiteral("CLIPBOARD")) + , timestamp(QByteArrayLiteral("TIMESTAMP")) + , targets(QByteArrayLiteral("TARGETS")) + , delete_atom(QByteArrayLiteral("DELETE")) + , incr(QByteArrayLiteral("INCR")) + , wl_selection(QByteArrayLiteral("WL_SELECTION")) , m_dtSmWindowInfo(QByteArrayLiteral("_DT_SM_WINDOW_INFO")) , m_motifSupport(QByteArrayLiteral("_MOTIF_WM_INFO")) , m_helpersRetrieved(false) Index: autotests/integration/CMakeLists.txt =================================================================== --- autotests/integration/CMakeLists.txt +++ autotests/integration/CMakeLists.txt @@ -1,6 +1,6 @@ add_subdirectory(helper) -add_library(KWinIntegrationTestFramework STATIC ../../xwl/xwayland.cpp kwin_wayland_test.cpp test_helpers.cpp) +add_library(KWinIntegrationTestFramework STATIC kwin_wayland_test.cpp test_helpers.cpp ${kwin_XWAYLAND_SRCS}) target_link_libraries(KWinIntegrationTestFramework kwin Qt5::Test) function(integrationTest) Index: autotests/integration/kwin_wayland_test.cpp =================================================================== --- autotests/integration/kwin_wayland_test.cpp +++ autotests/integration/kwin_wayland_test.cpp @@ -65,6 +65,10 @@ if (effects) { static_cast(effects)->unloadAllEffects(); } + if (m_xwayland) { + // needs to be done before workspace gets destroyed + m_xwayland->prepareDestroy(); + } destroyWorkspace(); waylandServer()->dispatch(); if (QStyle *s = style()) { Index: autotests/integration/xclipboardsync_test.cpp =================================================================== --- autotests/integration/xclipboardsync_test.cpp +++ autotests/integration/xclipboardsync_test.cpp @@ -23,6 +23,7 @@ #include "screens.h" #include "wayland_server.h" #include "workspace.h" +#include "../../xwl/databridge.h" #include @@ -64,11 +65,11 @@ QCOMPARE(screens()->geometry(0), QRect(0, 0, 1280, 1024)); QCOMPARE(screens()->geometry(1), QRect(1280, 0, 1280, 1024)); waylandServer()->initWorkspace(); - // wait till the xclipboard sync data device is created - while (waylandServer()->xclipboardSyncDataDevice().isNull()) { + // wait till the DataBridge sync data device is created + while (Xwl::DataBridge::self()->dataDeviceIface() == nullptr) { QCoreApplication::processEvents(QEventLoop::WaitForMoreEvents); } - QVERIFY(!waylandServer()->xclipboardSyncDataDevice().isNull()); + QVERIFY(Xwl::DataBridge::self()->dataDeviceIface() != nullptr); } void XClipboardSyncTest::cleanup() @@ -90,8 +91,8 @@ QTest::addColumn("copyPlatform"); QTest::addColumn("pastePlatform"); - QTest::newRow("x11-wayland") << QStringLiteral("xcb") << QStringLiteral("wayland"); - QTest::newRow("wayland-x11") << QStringLiteral("wayland") << QStringLiteral("xcb"); + QTest::newRow("x11->wayland") << QStringLiteral("xcb") << QStringLiteral("wayland"); + QTest::newRow("wayland->x11") << QStringLiteral("wayland") << QStringLiteral("xcb"); } void XClipboardSyncTest::testSync() @@ -106,10 +107,12 @@ QVERIFY(clientAddedSpy.isValid()); QSignalSpy shellClientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(shellClientAddedSpy.isValid()); - QSignalSpy clipboardChangedSpy(waylandServer()->xclipboardSyncDataDevice().data(), &KWayland::Server::DataDeviceInterface::selectionChanged); + QSignalSpy clipboardChangedSpy(Xwl::DataBridge::self()->dataDeviceIface(), &KWayland::Server::DataDeviceInterface::selectionChanged); QVERIFY(clipboardChangedSpy.isValid()); QProcessEnvironment environment = QProcessEnvironment::systemEnvironment(); + + // start the copy process QFETCH(QString, copyPlatform); environment.insert(QStringLiteral("QT_QPA_PLATFORM"), copyPlatform); environment.insert(QStringLiteral("WAYLAND_DISPLAY"), s_socketName); @@ -166,6 +169,7 @@ QCOMPARE(clientAddedSpy.count(), 1); QCOMPARE(shellClientAddedSpy.count(), 1); QVERIFY(pasteClient); + if (workspace()->activeClient() != pasteClient) { QSignalSpy clientActivatedSpy(workspace(), &Workspace::clientActivated); QVERIFY(clientActivatedSpy.isValid()); @@ -177,6 +181,8 @@ QCOMPARE(finishedSpy.first().first().toInt(), 0); delete m_pasteProcess; m_pasteProcess = nullptr; + delete m_copyProcess; + m_copyProcess = nullptr; } WAYLANDTEST_MAIN(XClipboardSyncTest) Index: main_wayland.cpp =================================================================== --- main_wayland.cpp +++ main_wayland.cpp @@ -118,6 +118,10 @@ if (effects) { static_cast(effects)->unloadAllEffects(); } + if (m_xwayland) { + // needs to be done before workspace gets destroyed + m_xwayland->prepareDestroy(); + } destroyWorkspace(); waylandServer()->dispatch(); Index: wayland_server.h =================================================================== --- wayland_server.h +++ wayland_server.h @@ -35,6 +35,8 @@ { class ConnectionThread; class Registry; +class Seat; +class DataDeviceManager; class ShmPool; class Surface; } @@ -48,6 +50,7 @@ class IdleInterface; class ShellInterface; class SeatInterface; +class DataDeviceManagerInterface; class ServerSideDecorationManagerInterface; class ServerSideDecorationPaletteManagerInterface; class SurfaceInterface; @@ -96,6 +99,9 @@ KWayland::Server::SeatInterface *seat() { return m_seat; } + KWayland::Server::DataDeviceManagerInterface *dataDeviceManager() { + return m_dataDeviceManager; + } KWayland::Server::ShellInterface *shell() { return m_shell; } @@ -167,6 +173,12 @@ QPointer xclipboardSyncDataDevice() const { return m_xclipbaordSync.ddi; } + KWayland::Client::Seat *internalSeat() { + return m_internalConnection.seat; + } + KWayland::Client::DataDeviceManager *internalDataDeviceManager() { + return m_internalConnection.ddm; + } KWayland::Client::ShmPool *internalShmPool() { return m_internalConnection.shm; } @@ -221,6 +233,7 @@ KWayland::Server::Display *m_display = nullptr; KWayland::Server::CompositorInterface *m_compositor = nullptr; KWayland::Server::SeatInterface *m_seat = nullptr; + KWayland::Server::DataDeviceManagerInterface *m_dataDeviceManager = nullptr; KWayland::Server::ShellInterface *m_shell = nullptr; KWayland::Server::XdgShellInterface *m_xdgShell = nullptr; KWayland::Server::XdgShellInterface *m_xdgShell6 = nullptr; @@ -244,6 +257,8 @@ KWayland::Client::ConnectionThread *client = nullptr; QThread *clientThread = nullptr; KWayland::Client::Registry *registry = nullptr; + KWayland::Client::Seat *seat = nullptr; + KWayland::Client::DataDeviceManager *ddm = nullptr; KWayland::Client::ShmPool *shm = nullptr; bool interfacesAnnounced = false; Index: wayland_server.cpp =================================================================== --- wayland_server.cpp +++ wayland_server.cpp @@ -30,6 +30,8 @@ #include #include #include +#include +#include #include #include // Server @@ -86,7 +88,7 @@ qRegisterMetaType(); connect(kwinApp(), &Application::screensCreated, this, &WaylandServer::initOutputs); - connect(kwinApp(), &Application::x11ConnectionChanged, this, &WaylandServer::setupX11ClipboardSync); +// connect(kwinApp(), &Application::x11ConnectionChanged, this, &WaylandServer::setupX11ClipboardSync); } WaylandServer::~WaylandServer() @@ -108,6 +110,8 @@ } delete m_internalConnection.registry; + delete m_internalConnection.seat; + delete m_internalConnection.ddm; delete m_internalConnection.shm; dispatch(); m_internalConnection.client->deleteLater(); @@ -232,9 +236,9 @@ m_seat->create(); m_display->createPointerGestures(PointerGesturesInterfaceVersion::UnstableV1, m_display)->create(); m_display->createPointerConstraints(PointerConstraintsInterfaceVersion::UnstableV1, m_display)->create(); - auto ddm = m_display->createDataDeviceManager(m_display); - ddm->create(); - connect(ddm, &DataDeviceManagerInterface::dataDeviceCreated, this, + m_dataDeviceManager = m_display->createDataDeviceManager(m_display); + m_dataDeviceManager->create(); + connect(m_dataDeviceManager, &DataDeviceManagerInterface::dataDeviceCreated, this, [this] (DataDeviceInterface *ddi) { if (ddi->client() == m_xclipbaordSync.client && m_xclipbaordSync.client != nullptr) { m_xclipbaordSync.ddi = QPointer(ddi); @@ -608,8 +612,17 @@ } ); connect(registry, &Registry::interfacesAnnounced, this, - [this] { + [this, registry] { m_internalConnection.interfacesAnnounced = true; + + const auto seatInterface = registry->interface(Registry::Interface::Seat); + if (seatInterface.name != 0) { + m_internalConnection.seat = registry->createSeat(seatInterface.name, seatInterface.version, this); + } + const auto ddmInterface = registry->interface(Registry::Interface::DataDeviceManager); + if (ddmInterface.name != 0) { + m_internalConnection.ddm = registry->createDataDeviceManager(ddmInterface.name, ddmInterface.version, this); + } } ); registry->setup(); Index: xwl/clipboard.h =================================================================== --- xwl/clipboard.h +++ xwl/clipboard.h @@ -17,43 +17,46 @@ You should have received a copy of the GNU General Public License along with this program. If not, see . *********************************************************************/ -#ifndef KWIN_XWL_XWAYLAND -#define KWIN_XWL_XWAYLAND +#ifndef KWIN_XWL_CLIPBOARD +#define KWIN_XWL_CLIPBOARD -#include +#include "selection.h" -class QProcess; - -class xcb_screen_t; +namespace KWayland { +namespace Server { +class DataDeviceInterface; +} +} namespace KWin { -class ApplicationWaylandAbstract; - namespace Xwl { -class Xwayland : public QObject +/* + * Represents the X clipboard, which is on Wayland side just called + * @e selection. + */ +class Clipboard : public Selection { Q_OBJECT public: - Xwayland(ApplicationWaylandAbstract *app, QObject *parent = nullptr); - virtual ~Xwayland(); - - void init(); - -Q_SIGNALS: - void criticalError(int code); + explicit Clipboard(xcb_atom_t atom, QObject *parent); private: - void createX11Connection(); - void continueStartupWithX(); - - int m_xcbConnectionFd = -1; - QProcess *m_xwaylandProcess = nullptr; - QMetaObject::Connection m_xwaylandFailConnection; - - ApplicationWaylandAbstract *m_app; + void doHandleXfixesNotify(xcb_xfixes_selection_notify_event_t *event) override; + void x11OffersChanged(const QVector &added, const QVector &removed) override; + /* + * React to Wl selection change. + */ + void wlSelectionChanged(KWayland::Server::DataDeviceInterface *ddi); + /* + * Check the current state of the selection and if a source needs + * to be created or destroyed. + */ + void checkWlSource(); + + QMetaObject::Connection m_checkConnection; }; } Index: xwl/clipboard.cpp =================================================================== --- /dev/null +++ xwl/clipboard.cpp @@ -0,0 +1,180 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#include "clipboard.h" + +#include "xwayland.h" +#include "databridge.h" +#include "selection_source.h" +#include "transfer.h" + +#include "wayland_server.h" +#include "workspace.h" +#include "abstract_client.h" + +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include + +namespace KWin { +namespace Xwl { + +Clipboard::Clipboard(xcb_atom_t atom, QObject *parent) + : Selection(atom, parent) +{ + auto *xcbConn = kwinApp()->x11Connection(); + + const uint32_t clipboardValues[] = { XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | + XCB_EVENT_MASK_PROPERTY_CHANGE }; + xcb_create_window(xcbConn, + XCB_COPY_FROM_PARENT, + window(), + kwinApp()->x11RootWindow(), + 0, 0, + 10, 10, + 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + Xwayland::self()->xcbScreen()->root_visual, + XCB_CW_EVENT_MASK, + clipboardValues); + registerXfixes(); + xcb_flush(xcbConn); + + connect(waylandServer()->seat(), &KWayland::Server::SeatInterface::selectionChanged, + this, &Clipboard::wlSelectionChanged); +} + +void Clipboard::wlSelectionChanged(KWayland::Server::DataDeviceInterface *ddi) +{ + if (ddi && ddi != DataBridge::self()->dataDeviceIface()) { + // Wayland native client provides new selection + if (!m_checkConnection) { + m_checkConnection = connect(workspace(), &Workspace::clientActivated, + this, [this](AbstractClient *ac) { + Q_UNUSED(ac); + checkWlSource(); + }); + } + // remove previous source so checkWlSource() can create a new one + setWlSource(nullptr); + } + checkWlSource(); +} + +void Clipboard::checkWlSource() +{ + auto ddi = waylandServer()->seat()->selection(); + auto removeSource = [this] { + if (wlSource()) { + setWlSource(nullptr); + ownSelection(false); + } + }; + /* + * Wayland source gets created when: + * - the Wl selection exists, + * - its source is not Xwayland, + * - a client is active, + * - this client is an Xwayland one. + * + * Otherwise the Wayland source gets destroyed to shield + * against snooping X clients. + * + */ + if (!ddi || DataBridge::self()->dataDeviceIface() == ddi) { + // Xwayland source or no source + disconnect(m_checkConnection); + m_checkConnection = QMetaObject::Connection(); + removeSource(); + return; + } + if (!workspace()->activeClient() || !workspace()->activeClient()->inherits("KWin::Client")) { + // no active client or active client is Wayland native + removeSource(); + return; + } + // Xwayland client is active and we need a Wayland source + if (wlSource()) { + // source already exists, nothing more to do + return; + } + auto *wls = new WlSource(this, ddi); + setWlSource(wls); + auto *dsi = ddi->selection(); + if (dsi) { + wls->setDataSourceIface(dsi); + } + connect(ddi, &KWayland::Server::DataDeviceInterface::selectionChanged, + wls, &WlSource::setDataSourceIface); + ownSelection(true); +} + +void Clipboard::doHandleXfixesNotify(xcb_xfixes_selection_notify_event_t *event) +{ + createX11Source(event); + auto *xSrc = x11Source(); + if (xSrc) { + xSrc->getTargets(); + } +} + +void Clipboard::x11OffersChanged(const QVector &added, const QVector &removed) +{ + auto *xSrc = x11Source(); + if (!xSrc) { + return; + } + + const auto offers = xSrc->offers(); + const bool hasOffers = offers.size() > 0; + + if (hasOffers) { + if (!xSrc->dataSource() || !removed.isEmpty()) { + // create new Wl DataSource if there is none or when types + // were removed (Wl Data Sources can only add types) + auto *ddm = waylandServer()->internalDataDeviceManager(); + auto *ds = ddm->createDataSource(xSrc); + + // also offers directly the currently available types + xSrc->setDataSource(ds); + DataBridge::self()->dataDevice()->setSelection(0, ds); + waylandServer()->seat()->setSelection(DataBridge::self()->dataDeviceIface()); + } else if (auto *ds = xSrc->dataSource()) { + for (const auto &mime : added) { + ds->offer(mime); + } + } + } else { + waylandServer()->seat()->setSelection(nullptr); + } + waylandServer()->internalClientConection()->flush(); + waylandServer()->dispatch(); +} + +} +} Index: xwl/databridge.h =================================================================== --- /dev/null +++ xwl/databridge.h @@ -0,0 +1,86 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#ifndef KWIN_XWL_DATABRIDGE +#define KWIN_XWL_DATABRIDGE + +#include + +#include + +class xcb_xfixes_selection_notify_event_t; + +namespace KWayland { +namespace Client { +class DataDevice; +} +namespace Server { +class DataDeviceInterface; +class SurfaceInterface; +} +} + +namespace KWin +{ + +namespace Xwl +{ +class Xwayland; +class Clipboard; + +/* + * Interface class for all data sharing in the context of X selections + * and Wayland's internal mechanism. + * + * Exists only once per Xwayland session. + */ +class DataBridge : public QObject +{ + Q_OBJECT +public: + static DataBridge* self(); + + DataBridge(QObject *parent = nullptr); + ~DataBridge(); + + bool filterEvent(xcb_generic_event_t *event); + + KWayland::Client::DataDevice *dataDevice() const { + return m_dd; + } + KWayland::Server::DataDeviceInterface *dataDeviceIface() const { + return m_ddi; + } + +private: + void init(); + + bool handleXfixesNotify(xcb_xfixes_selection_notify_event_t *event); + + Clipboard *m_clipboard = nullptr; + + /* Internal data device interface */ + KWayland::Client::DataDevice *m_dd = nullptr; + KWayland::Server::DataDeviceInterface *m_ddi = nullptr; +}; + +} +} + +#endif Index: xwl/databridge.cpp =================================================================== --- /dev/null +++ xwl/databridge.cpp @@ -0,0 +1,106 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#include "databridge.h" +#include "xwayland.h" +#include "selection.h" +#include "clipboard.h" + +#include "atoms.h" +#include "wayland_server.h" +#include "workspace.h" +#include "abstract_client.h" + +// KWayland +#include +#include + +#include +#include +#include + +namespace KWin { +namespace Xwl { + +static DataBridge *s_self = nullptr; +DataBridge* DataBridge::self() +{ + return s_self; +} + +DataBridge::DataBridge(QObject *parent) + : QObject(parent) +{ + s_self = this; + auto *ddm = waylandServer()->internalDataDeviceManager(); + auto *seat = waylandServer()->internalSeat(); + m_dd = ddm->getDataDevice(seat, this); + waylandServer()->dispatch(); + + const auto *ddmi = waylandServer()->dataDeviceManager(); + auto *dc = new QMetaObject::Connection(); + *dc = connect(ddmi, &KWayland::Server::DataDeviceManagerInterface::dataDeviceCreated, this, + [this, dc](KWayland::Server::DataDeviceInterface *ddi) { + if (m_ddi || ddi->client() != waylandServer()->internalConnection()) { + return; + } + QObject::disconnect(*dc); + delete dc; + m_ddi = ddi; + init(); + } + ); +} + +DataBridge::~DataBridge() +{ + s_self = nullptr; +} + +void DataBridge::init() +{ + m_clipboard = new Clipboard(atoms->clipboard, this); + waylandServer()->dispatch(); +} + +bool DataBridge::filterEvent(xcb_generic_event_t *event) +{ + if (m_clipboard->filterEvent(event)) { + return true; + } + if (event->response_type - Xwayland::self()->xfixes()->first_event == XCB_XFIXES_SELECTION_NOTIFY) { + return handleXfixesNotify((xcb_xfixes_selection_notify_event_t *)event); + } + return false; +} + +bool DataBridge::handleXfixesNotify(xcb_xfixes_selection_notify_event_t *event) +{ + auto getSelection = [this](xcb_atom_t atom) -> Selection* { + if (atom == atoms->clipboard) { + return m_clipboard; + } + return nullptr; + }; + auto *sel = getSelection(event->selection); + return sel && sel->handleXfixesNotify(event); +} + +} +} Index: xwl/selection.h =================================================================== --- /dev/null +++ xwl/selection.h @@ -0,0 +1,138 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#ifndef KWIN_XWL_SELECTION +#define KWIN_XWL_SELECTION + +#include +#include + +#include + +struct xcb_xfixes_selection_notify_event_t; + +class QTimer; + +namespace KWin +{ +namespace Xwl +{ +class TransferWltoX; +class TransferXtoWl; +class WlSource; +class X11Source; + +/* + * Base class representing generic X selections and their respective + * Wayland counter-parts. + * + * The class needs to be subclassed and adjusted according to the + * selection, but provides common fucntionality to be expected of all + * selections. + * + * A selection should exist through the whole runtime of an Xwayland + * session. + * + * Independently of each other the class holds the currently active + * source instance and active transfers relative to the represented + * selection. + */ +class Selection : public QObject +{ + Q_OBJECT +public: + static xcb_atom_t mimeTypeToAtom(const QString &mimeType); + static xcb_atom_t mimeTypeToAtomLiteral(const QString &mimeType); + static QStringList atomToMimeTypes(xcb_atom_t atom); + + // on selection owner changes by X clients (Xwl -> Wl) + bool handleXfixesNotify(xcb_xfixes_selection_notify_event_t *event); + bool filterEvent(xcb_generic_event_t *event); + void sendSelNotify(xcb_selection_request_event_t *event, bool success); + + xcb_atom_t atom() const { + return m_atom; + } + xcb_window_t window() const { + return m_window; + } + +Q_SIGNALS: + void transferFinished(xcb_timestamp_t eventTime); + +protected: + explicit Selection(xcb_atom_t atom, QObject *parent); + void registerXfixes(); + + virtual void doHandleXfixesNotify(xcb_xfixes_selection_notify_event_t *event) = 0; + virtual void x11OffersChanged(const QVector &added, const QVector &removed) = 0; + + virtual bool handleClientMessage(xcb_client_message_event_t *event) { + Q_UNUSED(event); + return false; + } + // sets the current provider of the selection + void setWlSource(WlSource *src); + WlSource* wlSource() const { + return m_wlSrc; + } + void createX11Source(xcb_xfixes_selection_notify_event_t *event); + X11Source* x11Source() const { + return m_xSrc; + } + // must be called in order to provide data from Wl to X + void ownSelection(bool own); + void setWindow(xcb_window_t window) { + m_window = window; + } + +private: + bool handleSelRequest(xcb_selection_request_event_t *event); + bool handleSelNotify(xcb_selection_notify_event_t *event); + bool handlePropNotify(xcb_property_notify_event_t *event); + + void startTransferToWayland(xcb_atom_t target, qint32 fd); + void startTransferToX(xcb_selection_request_event_t *event, qint32 fd); + + // Timeout transfers, which have become inactive due to client errors. + void timeoutTransfers(); + void startTimeoutTransfersTimer(); + void endTimeoutTransfersTimer(); + + xcb_atom_t m_atom = XCB_ATOM_NONE; + xcb_window_t m_window = XCB_WINDOW_NONE; + xcb_timestamp_t m_timestamp; + + // Active source, if any. Only one of them at max can exist + // at the same time. + WlSource *m_wlSrc = nullptr; + X11Source *m_xSrc = nullptr; + + // active transfers + QVector m_wlToXTransfers; + QVector m_xToWlTransfers; + QTimer *m_timeoutTransfers = nullptr; + + bool m_disownPending = false; +}; + +} +} + +#endif Index: xwl/selection.cpp =================================================================== --- /dev/null +++ xwl/selection.cpp @@ -0,0 +1,354 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#include "selection.h" +#include "databridge.h" +#include "selection_source.h" +#include "transfer.h" + +#include "atoms.h" +#include "workspace.h" +#include "abstract_client.h" + +#include +#include + +#include + +namespace KWin { +namespace Xwl { + +xcb_atom_t Selection::mimeTypeToAtom(const QString &mimeType) +{ + if (mimeType == "text/plain;charset=utf-8") { + return atoms->utf8_string; + } else if (mimeType == "text/plain") { + return atoms->text; + } else if (mimeType == "text/x-uri") { + return atoms->uri_list; + } + return mimeTypeToAtomLiteral(mimeType); +} + +xcb_atom_t Selection::mimeTypeToAtomLiteral(const QString &mimeType) +{ + return Xcb::Atom(mimeType.toLatin1(), false, kwinApp()->x11Connection()); +} + +QStringList Selection::atomToMimeTypes(xcb_atom_t atom) +{ + QStringList mimeTypes; + + if (atom == atoms->utf8_string) { + mimeTypes << QString::fromLatin1("text/plain;charset=utf-8"); + } else if (atom == atoms->text) { + mimeTypes << QString::fromLatin1("text/plain"); + } else if (atom == atoms->uri_list) { + mimeTypes << "text/uri-list" << "text/x-uri"; + } else { + auto *xcbConn = kwinApp()->x11Connection(); + xcb_get_atom_name_cookie_t nameCookie = xcb_get_atom_name(xcbConn, atom); + xcb_get_atom_name_reply_t *nameReply = xcb_get_atom_name_reply(xcbConn, nameCookie, NULL); + if (nameReply == NULL) { + return QStringList(); + } + + size_t len = xcb_get_atom_name_name_length(nameReply); + char *name = xcb_get_atom_name_name(nameReply); + mimeTypes << QString::fromLatin1(name, len); + free(nameReply); + } + return mimeTypes; +} + +Selection::Selection(xcb_atom_t atom, QObject *parent) + : QObject(parent), + m_atom(atom) +{ + auto *xcbConn = kwinApp()->x11Connection(); + m_window = xcb_generate_id(kwinApp()->x11Connection()); + xcb_flush(xcbConn); +} + +bool Selection::handleXfixesNotify(xcb_xfixes_selection_notify_event_t *event) +{ + if (event->window != m_window) { + return false; + } + if (event->selection != m_atom) { + return false; + } + if (m_disownPending) { + /* notify of our own disown - ignore it */ + m_disownPending = false; + return true; + } + if (event->owner == m_window && m_wlSrc) { + /* When we claim a selection we must use XCB_TIME_CURRENT, + * grab the actual timestamp here to answer TIMESTAMP requests + * correctly + */ + m_wlSrc->setTimestamp(event->timestamp); + m_timestamp = event->timestamp; + return true; + } + /* + * Being here means some other X window has claimed the selection. + */ + delete m_xSrc; + m_xSrc = nullptr; + const auto *ac = workspace()->activeClient(); + if (!ac || !ac->inherits("KWin::Client")) { + // selections are only allowed to be aquired when Xwayland has focus + // TODO: can we make this stronger (window id comparision)? + return true; + } + doHandleXfixesNotify(event); + return true; +} + +bool Selection::filterEvent(xcb_generic_event_t *event) +{ + switch (event->response_type & XCB_EVENT_RESPONSE_TYPE_MASK) { + case XCB_SELECTION_NOTIFY: + if (handleSelNotify((xcb_selection_notify_event_t *)event)) { + return true; + } + case XCB_PROPERTY_NOTIFY: + if (handlePropNotify((xcb_property_notify_event_t *)event)) { + return true; + } + case XCB_SELECTION_REQUEST: + if (handleSelRequest((xcb_selection_request_event_t *)event)) { + return true; + } + case XCB_CLIENT_MESSAGE: + if (handleClientMessage((xcb_client_message_event_t *)event)) { + return true; + } + default: + return false; + } +} + +void Selection::sendSelNotify(xcb_selection_request_event_t *event, bool success) +{ + xcb_selection_notify_event_t notify; + notify.response_type = XCB_SELECTION_NOTIFY; + notify.sequence = 0; + notify.time = event->time; + notify.requestor = event->requestor; + notify.selection = event->selection; + notify.target = event->target; + notify.property = success ? event->property : (xcb_atom_t)XCB_ATOM_NONE; + + auto *xcbConn = kwinApp()->x11Connection(); + xcb_send_event(xcbConn, + 0, + event->requestor, + XCB_EVENT_MASK_NO_EVENT, + (const char *)¬ify); + xcb_flush(xcbConn); +} + +void Selection::registerXfixes() +{ + auto *xcbConn = kwinApp()->x11Connection(); + const uint32_t mask = XCB_XFIXES_SELECTION_EVENT_MASK_SET_SELECTION_OWNER | + XCB_XFIXES_SELECTION_EVENT_MASK_SELECTION_WINDOW_DESTROY | + XCB_XFIXES_SELECTION_EVENT_MASK_SELECTION_CLIENT_CLOSE; + xcb_xfixes_select_selection_input(kwinApp()->x11Connection(), + m_window, + m_atom, + mask); + xcb_flush(xcbConn); +} + +void Selection::setWlSource(WlSource *src) +{ + delete m_wlSrc; + delete m_xSrc; + m_wlSrc = nullptr; + m_xSrc = nullptr; + if (src) { + m_wlSrc = src; + connect(src, &WlSource::transferReady, this, &Selection::startTransferToX); + } +} + +void Selection::createX11Source(xcb_xfixes_selection_notify_event_t *event) +{ + delete m_wlSrc; + delete m_xSrc; + m_wlSrc = nullptr; + m_xSrc = nullptr; + if (event->owner == XCB_WINDOW_NONE) { + return; + } + m_xSrc = new X11Source(this, event); + + connect(m_xSrc, &X11Source::offersChanged, this, &Selection::x11OffersChanged); + connect(m_xSrc, &X11Source::transferReady, this, &Selection::startTransferToWayland); +} + +void Selection::ownSelection(bool own) +{ + auto *xcbConn = kwinApp()->x11Connection(); + if (own) { + xcb_set_selection_owner(xcbConn, + m_window, + m_atom, + XCB_TIME_CURRENT_TIME); + } else { + m_disownPending = true; + xcb_set_selection_owner(xcbConn, + XCB_WINDOW_NONE, + m_atom, + m_timestamp); + } + xcb_flush(xcbConn); +} + +bool Selection::handleSelRequest(xcb_selection_request_event_t *event) +{ + if (event->selection != m_atom) { + return false; + } + + if (!workspace()->activeClient() || !workspace()->activeClient()->inherits("KWin::Client")) { + // Receiving Wayland selection not allowed when no Xwayland surface active + // filter the event, but don't act upon it + sendSelNotify(event, false); + return true; + } + + if (m_window != event->owner || !m_wlSrc) { + if (event->time < m_timestamp) { + // cancel earlier attempts at receiving a selection + // TODO: is this for sure without problems? + sendSelNotify(event, false); + return true; + } + return false; + } + return m_wlSrc->handleSelRequest(event); +} + +bool Selection::handleSelNotify(xcb_selection_notify_event_t *event) +{ + if (m_xSrc && m_xSrc->handleSelNotify(event)) { + return true; + } + for (auto *transfer : m_xToWlTransfers) { + if (transfer->handleSelNotify(event)) { + return true; + } + } + return false; +} + +bool Selection::handlePropNotify(xcb_property_notify_event_t *event) +{ + for (auto *transfer : m_xToWlTransfers) { + if (transfer->handlePropNotify(event)) { + return true; + } + } + for (auto *transfer : m_wlToXTransfers) { + if (transfer->handlePropNotify(event)) { + return true; + } + } + return false; +} + +void Selection::startTransferToWayland(xcb_atom_t target, qint32 fd) +{ + // create new x to wl data transfer object + auto *transfer = new TransferXtoWl(m_atom, target, fd, m_xSrc->timestamp(), m_window, this); + m_xToWlTransfers << transfer; + + connect(transfer, &TransferXtoWl::finished, this, [this, transfer]() { + Q_EMIT transferFinished(transfer->timestamp()); + delete transfer; + m_xToWlTransfers.removeOne(transfer); + endTimeoutTransfersTimer(); + }); + startTimeoutTransfersTimer(); +} + +void Selection::startTransferToX(xcb_selection_request_event_t *event, qint32 fd) +{ + // create new wl to x data transfer object + auto *transfer = new TransferWltoX(m_atom, event, fd, this); + + connect(transfer, &TransferWltoX::selNotify, this, &Selection::sendSelNotify); + connect(transfer, &TransferWltoX::finished, this, [this, transfer]() { + Q_EMIT transferFinished(transfer->timestamp()); +// const bool wasActive = (transfer == m_wlToXTransfers[0]); + delete transfer; + m_wlToXTransfers.removeOne(transfer); + endTimeoutTransfersTimer(); +// if (wasActive && !m_wlToXTransfers.isEmpty()) { +// m_wlToXTransfers[0]->startTransferFromSource(); +// } + }); + + // add it to list of queued transfers + m_wlToXTransfers.append(transfer); + + // TODO: Do we need to serialize the transfers, or can we do + // them in parallel as we do it right now? + transfer->startTransferFromSource(); +// if (m_wlToXTransfers.size() == 1) { +// transfer->startTransferFromSource(); +// } + startTimeoutTransfersTimer(); +} + +void Selection::startTimeoutTransfersTimer() +{ + if (m_timeoutTransfers) { + return; + } + m_timeoutTransfers = new QTimer(this); + connect(m_timeoutTransfers, &QTimer::timeout, this, &Selection::timeoutTransfers); + m_timeoutTransfers->start(5000); +} + +void Selection::endTimeoutTransfersTimer() +{ + if (m_xToWlTransfers.isEmpty() && m_wlToXTransfers.isEmpty()) { + delete m_timeoutTransfers; + m_timeoutTransfers = nullptr; + } +} + +void Selection::timeoutTransfers() +{ + for (auto *transfer : m_xToWlTransfers) { + transfer->timeout(); + } + for (auto *transfer : m_wlToXTransfers) { + transfer->timeout(); + } +} + +} +} Index: xwl/selection_source.h =================================================================== --- /dev/null +++ xwl/selection_source.h @@ -0,0 +1,151 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#ifndef KWIN_XWL_SELECTION_SOURCE +#define KWIN_XWL_SELECTION_SOURCE + +#include +#include + +#include + +class QSocketNotifier; + +struct xcb_selection_request_event_t; +struct xcb_xfixes_selection_notify_event_t; + +namespace KWayland { +namespace Client { +class DataSource; +} +namespace Server { +class DataDeviceInterface; +class DataSourceInterface; +} +} + +namespace KWin +{ +namespace Xwl +{ +class Selection; + +/* + * Base class representing a data source. + */ +class SelSource : public QObject +{ + Q_OBJECT +public: + SelSource(Selection *sel); + + xcb_timestamp_t timestamp() const { + return m_timestamp; + } + void setTimestamp(xcb_timestamp_t time) { + m_timestamp = time; + } + +protected: + Selection *selection() const { + return m_sel; + } + +private: + xcb_timestamp_t m_timestamp = XCB_CURRENT_TIME; + Selection *m_sel; +}; + +/* + * Representing a Wayland native data source. + */ +class WlSource : public SelSource +{ + Q_OBJECT +public: + WlSource(Selection *sel, KWayland::Server::DataDeviceInterface *ddi); + void setDataSourceIface(KWayland::Server::DataSourceInterface *dsi); + + bool handleSelRequest(xcb_selection_request_event_t *event); + void sendTargets(xcb_selection_request_event_t *event); + void sendTimestamp(xcb_selection_request_event_t *event); + + void receiveOffer(const QString &mime); + void sendSelNotify(xcb_selection_request_event_t *event, bool success); + +Q_SIGNALS: + void transferReady(xcb_selection_request_event_t *event, qint32 fd); + +private: + bool checkStartTransfer(xcb_selection_request_event_t *event); + + KWayland::Server::DataDeviceInterface *m_ddi = nullptr; + KWayland::Server::DataSourceInterface *m_dsi = nullptr; + + QVector m_offers; + QMetaObject::Connection m_offerCon; +}; + +using Mimes = QVector >; + +/* + * Representing an X data source. + */ +class X11Source : public SelSource +{ + Q_OBJECT +public: + X11Source(Selection *sel, xcb_xfixes_selection_notify_event_t *event); + + /* @param ds must exist. + * + * X11Source does not take ownership of it in general, but if the function + * is called again, it will delete the previous data source. + */ + void setDataSource(KWayland::Client::DataSource *ds); + KWayland::Client::DataSource* dataSource() const { + return m_ds; + } + void getTargets(); + + Mimes offers() const { + return m_offers; + } + void setOffers(const Mimes &offers); + + bool handleSelNotify(xcb_selection_notify_event_t *event); + +Q_SIGNALS: + void offersChanged(QVector added, QVector removed); + void transferReady(xcb_atom_t target, qint32 fd); + +private: + void handleTargets(); + void startTransfer(const QString &mimeName, qint32 fd); + + xcb_window_t m_owner; + KWayland::Client::DataSource *m_ds = nullptr; + + Mimes m_offers; +}; + +} +} + +#endif Index: xwl/selection_source.cpp =================================================================== --- /dev/null +++ xwl/selection_source.cpp @@ -0,0 +1,314 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#include "selection_source.h" +#include "selection.h" +#include "transfer.h" + +#include "atoms.h" +#include "wayland_server.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include + +#include + +namespace KWin { +namespace Xwl { + +SelSource::SelSource(Selection *sel) + : QObject(sel), + m_sel(sel) +{ +} + +WlSource::WlSource(Selection *sel, KWayland::Server::DataDeviceInterface *ddi) + : SelSource(sel), + m_ddi(ddi) +{ + Q_ASSERT(ddi); +} + +void WlSource::setDataSourceIface(KWayland::Server::DataSourceInterface *dsi) +{ + if (m_dsi == dsi) { + return; + } + for (const auto &mime : dsi->mimeTypes()) { + m_offers << mime; + } + m_offerCon = connect(dsi, + &KWayland::Server::DataSourceInterface::mimeTypeOffered, + this, &WlSource::receiveOffer); + m_dsi = dsi; +} + +void WlSource::receiveOffer(const QString &mime) +{ + m_offers << mime; +} + +void WlSource::sendSelNotify(xcb_selection_request_event_t *event, bool success) +{ + selection()->sendSelNotify(event, success); +} + +bool WlSource::handleSelRequest(xcb_selection_request_event_t *event) +{ + if (event->target == atoms->targets) { + sendTargets(event); + } else if (event->target == atoms->timestamp) { + sendTimestamp(event); + } else if (event->target == atoms->delete_atom) { + sendSelNotify(event, true); + } else { + // try to send mime data + if (!checkStartTransfer(event)) { + sendSelNotify(event, false); + } + } + return true; +} + +void WlSource::sendTargets(xcb_selection_request_event_t *event) +{ + QVector targets; + targets.resize(m_offers.size() + 2); + targets[0] = atoms->timestamp; + targets[1] = atoms->targets; + + size_t cnt = 2; + for (const auto mime : m_offers) { + targets[cnt] = Selection::mimeTypeToAtom(mime); + cnt++; + } + + xcb_change_property(kwinApp()->x11Connection(), + XCB_PROP_MODE_REPLACE, + event->requestor, + event->property, + XCB_ATOM_ATOM, + 32, cnt, targets.data()); + sendSelNotify(event, true); +} + +void WlSource::sendTimestamp(xcb_selection_request_event_t *event) +{ + const xcb_timestamp_t time = timestamp(); + xcb_change_property(kwinApp()->x11Connection(), + XCB_PROP_MODE_REPLACE, + event->requestor, + event->property, + XCB_ATOM_INTEGER, + 32, 1, &time); + + sendSelNotify(event, true); +} + +bool WlSource::checkStartTransfer(xcb_selection_request_event_t *event) +{ + // check interfaces available + if (!m_ddi || !m_dsi) { + return false; + } + + const auto targets = Selection::atomToMimeTypes(event->target); + if (targets.isEmpty()) { + qCDebug(KWIN_XWL) << "Unknown selection atom. Ignoring request."; + return false; + } + const auto firstTarget = targets[0]; + + auto cmp = [firstTarget](const QString &b) { + if (firstTarget == "text/uri-list") { + // Wayland sources might announce the old mime or the new standard + return firstTarget == b || b == "text/x-uri"; + } + return firstTarget == b; + }; + // check supported mimes + const auto offers = m_dsi->mimeTypes(); + const auto mimeIt = std::find_if(offers.begin(), offers.end(), cmp); + if (mimeIt == offers.end()) { + // Requested Mime not supported. Not sending selection. + return false; + } + + int p[2]; + if (pipe(p) == -1) { + qCWarning(KWIN_XWL) << "Pipe failed. Not sending selection."; + return false; + } + + m_dsi->requestData(*mimeIt, p[1]); + waylandServer()->dispatch(); + + Q_EMIT transferReady(new xcb_selection_request_event_t(*event), p[0]); + return true; +} + +X11Source::X11Source(Selection *sel, xcb_xfixes_selection_notify_event_t *event) + : SelSource(sel), + m_owner(event->owner) +{ + setTimestamp(event->timestamp); +} + +void X11Source::getTargets() +{ + auto *xcbConn = kwinApp()->x11Connection(); + /* will lead to a selection request event for the new owner */ + xcb_convert_selection(xcbConn, + selection()->window(), + selection()->atom(), + atoms->targets, + atoms->wl_selection, + timestamp()); + xcb_flush(xcbConn); +} + +using Mime = QPair; + +void X11Source::handleTargets() +{ + // receive targets + auto *xcbConn = kwinApp()->x11Connection(); + xcb_get_property_cookie_t cookie = xcb_get_property(xcbConn, + 1, + selection()->window(), + atoms->wl_selection, + XCB_GET_PROPERTY_TYPE_ANY, + 0, + 4096 + ); + auto *reply = xcb_get_property_reply(xcbConn, cookie, NULL); + if (reply == NULL) { + return; + } + if (reply->type != XCB_ATOM_ATOM) { + free(reply); + return; + } + + Mimes all; + QVector add, rm; + xcb_atom_t *value = static_cast(xcb_get_property_value(reply)); + for (uint32_t i = 0; i < reply->value_len; i++) { + if (value[i] == XCB_ATOM_NONE) { + continue; + } + + const auto mimeStrings = Selection::atomToMimeTypes(value[i]); + if (mimeStrings.isEmpty()) { + // TODO: this should never happen? assert? + continue; + } + + + const auto mimeIt = std::find_if(m_offers.begin(), m_offers.end(), + [value, i](const Mime &m) + { return m.second == value[i]; }); + + auto mimePair = Mime(mimeStrings[0], value[i]); + if (mimeIt == m_offers.end()) { + add << mimePair.first; + } else { + m_offers.removeAll(mimePair); + } + all << mimePair; + } + // all left in m_offers are not in the updated targets + for (const auto mimePair : m_offers) { + rm << mimePair.first; + } + m_offers = all; + + if (!add.isEmpty() || !rm.isEmpty()) { + Q_EMIT offersChanged(add, rm); + } + + free(reply); +} + +void X11Source::setDataSource(KWayland::Client::DataSource *ds) +{ + Q_ASSERT(ds); + if (m_ds) { + delete m_ds; + } + m_ds = ds; + + std::for_each(m_offers.begin(), m_offers.end(), + [ds](const Mime &offer){ + ds->offer(offer.first); + }); + connect(ds, &KWayland::Client::DataSource::sendDataRequested, + this, &X11Source::startTransfer); +} + +void X11Source::setOffers(const Mimes &offers) +{ + // TODO: share code with handleTargets and emit signals accordingly? + m_offers = offers; +} + +bool X11Source::handleSelNotify(xcb_selection_notify_event_t *event) +{ + if (event->requestor != selection()->window()) { + return false; + } + if (event->selection != selection()->atom()) { + return false; + } + if (event->property == XCB_ATOM_NONE) { + qCWarning(KWIN_XWL) << "Incoming X selection conversion failed"; + return true; + } + if (event->target == atoms->targets) { + handleTargets(); + return true; + } + return false; +} + +void X11Source::startTransfer(const QString &mimeName, qint32 fd) +{ + const auto mimeIt = std::find_if(m_offers.begin(), m_offers.end(), + [mimeName](const Mime &m) + { return m.first == mimeName; }); + if (mimeIt == m_offers.end()) { + qCDebug(KWIN_XWL) << "Sending X11 clipboard to Wayland failed: unsupported MIME."; + close(fd); + return; + } + + Q_EMIT transferReady((*mimeIt).second, fd); +} + + +} +} Index: xwl/transfer.h =================================================================== --- /dev/null +++ xwl/transfer.h @@ -0,0 +1,198 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#ifndef KWIN_XWL_TRANSFER +#define KWIN_XWL_TRANSFER + +#include +#include +#include + +#include + +namespace KWayland { +namespace Client { +class DataDevice; +class DataSource; +} +namespace Server { +class DataDeviceInterface; +} +} + +namespace KWin +{ +namespace Xwl +{ + +/* + * Represents for an arbitrary selection a data transfer between + * sender and receiver. + * + * Lives for the duration of the transfer and must be cleaned up + * externally afterwards. For that the owner should connect to the + * @c finished() signal. + */ +class Transfer : public QObject +{ + Q_OBJECT +public: + Transfer(xcb_atom_t selection, + qint32 fd, + xcb_timestamp_t timestamp, + QObject *parent = nullptr); + + virtual bool handlePropNotify(xcb_property_notify_event_t *event) = 0; + void timeout(); + xcb_timestamp_t timestamp() const { + return m_timestamp; + } + +Q_SIGNALS: + void finished(); + +protected: + void endTransfer(); + + xcb_atom_t atom() const { + return m_atom; + } + qint32 fd() const { + return m_fd; + } + + void setIncr(bool set) { + m_incr = set; + } + bool incr() const { + return m_incr; + } + void resetTimeout() { + m_timeout = false; + } + void createSocketNotifier(QSocketNotifier::Type type); + void clearSocketNotifier(); + QSocketNotifier* socketNotifier() const { + return m_sn; + } +private: + void closeFd(); + + xcb_atom_t m_atom; + qint32 m_fd; + xcb_timestamp_t m_timestamp = XCB_CURRENT_TIME; + + QSocketNotifier *m_sn = nullptr; + bool m_incr = false; + bool m_timeout = false; +}; + +/* + * Represents a transfer from a Wayland native source to an X window. + */ +class TransferWltoX : public Transfer +{ + Q_OBJECT +public: + TransferWltoX(xcb_atom_t selection, + xcb_selection_request_event_t *request, + qint32 fd, + QObject *parent = nullptr); + ~TransferWltoX(); + + void startTransferFromSource(); + bool handlePropNotify(xcb_property_notify_event_t *event) override; + +Q_SIGNALS: + void selNotify(xcb_selection_request_event_t *event, bool success); + +private: + void startIncr(); + void readWlSource(); + int flushSourceData(); + void handlePropDelete(); + + xcb_selection_request_event_t *m_request = nullptr; + + /* contains all received data portioned in chunks + * TODO: explain second QPair component + */ + QVector > chunks; + + bool propertyIsSet = false; + bool flushPropOnDelete = false; +}; + +/* + * Helper class for X to Wl transfers + */ +class DataReceiver +{ +public: + virtual ~DataReceiver(); + + void transferFromProperty(xcb_get_property_reply_t *reply); + + + virtual void setData(char *value, int length); + QByteArray data() const; + + void partRead(int length); + +protected: + void setDataInternal(QByteArray data) { + m_data = data; + } + +private: + xcb_get_property_reply_t *m_propertyReply = nullptr; + int m_propertyStart = 0; + QByteArray m_data; +}; + +/* + * Represents a transfer from an X window to a Wayland native client. + */ +class TransferXtoWl : public Transfer +{ + Q_OBJECT +public: + TransferXtoWl(xcb_atom_t selection, + xcb_atom_t target, + qint32 fd, + xcb_timestamp_t timestamp, xcb_window_t parentWindow, + QObject *parent = nullptr); + ~TransferXtoWl(); + + bool handleSelNotify(xcb_selection_notify_event_t *event); + bool handlePropNotify(xcb_property_notify_event_t *event) override; + +private: + void dataSourceWrite(); + void startTransfer(); + void getIncrChunk(); + + xcb_window_t m_window; + DataReceiver *m_receiver = nullptr; +}; + +} +} + +#endif Index: xwl/transfer.cpp =================================================================== --- /dev/null +++ xwl/transfer.cpp @@ -0,0 +1,506 @@ +/******************************************************************** + KWin - the KDE window manager + This file is part of the KDE project. + +Copyright 2018 Roman Gilg + +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, see . +*********************************************************************/ +#include "transfer.h" + +#include "xwayland.h" +#include "databridge.h" + +#include "atoms.h" +#include "wayland_server.h" +#include "workspace.h" +#include "abstract_client.h" + +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include + +#include + +namespace KWin { +namespace Xwl { + +// in Bytes: equals 64KB +static const uint32_t s_incrChunkSize = 63 * 1024; + +Transfer::Transfer(xcb_atom_t selection, qint32 fd, xcb_timestamp_t timestamp, QObject *parent) + : QObject(parent), + m_atom(selection), + m_fd(fd), + m_timestamp(timestamp) +{ +} + + +void Transfer::createSocketNotifier(QSocketNotifier::Type type) +{ + delete m_sn; + m_sn = new QSocketNotifier(m_fd, type, this); +} + +void Transfer::clearSocketNotifier() +{ + delete m_sn; + m_sn = nullptr; +} + +void Transfer::timeout() +{ + if (m_timeout) { + endTransfer(); + } + m_timeout = true; +} + +void Transfer::endTransfer() +{ + clearSocketNotifier(); + closeFd(); + Q_EMIT finished(); +} + +void Transfer::closeFd() +{ + if (m_fd < 0) { + return; + } + close(m_fd); + m_fd = -1; +} + +TransferWltoX::TransferWltoX(xcb_atom_t selection, xcb_selection_request_event_t *request, + qint32 fd, QObject *parent) + : Transfer(selection, fd, 0, parent), + m_request(request) +{ +} + +TransferWltoX::~TransferWltoX() +{ + delete m_request; + m_request = nullptr; +} + +void TransferWltoX::startTransferFromSource() +{ + createSocketNotifier(QSocketNotifier::Read); + connect(socketNotifier(), &QSocketNotifier::activated, this, + [this](int socket) { + Q_UNUSED(socket); + readWlSource(); + } + ); +} + +int TransferWltoX::flushSourceData() +{ + auto *xcbConn = kwinApp()->x11Connection(); + + xcb_change_property(xcbConn, + XCB_PROP_MODE_REPLACE, + m_request->requestor, + m_request->property, + m_request->target, + 8, + chunks.first().first.size(), + chunks.first().first.data()); + xcb_flush(xcbConn); + + propertyIsSet = true; + resetTimeout(); + + const auto rm = chunks.takeFirst(); + return rm.first.size(); +} + +void TransferWltoX::startIncr() +{ + Q_ASSERT(chunks.size() == 1); + + auto *xcbConn = kwinApp()->x11Connection(); + + uint32_t mask[] = { XCB_EVENT_MASK_PROPERTY_CHANGE }; + xcb_change_window_attributes (xcbConn, + m_request->requestor, + XCB_CW_EVENT_MASK, mask); + + // spec says to make the available space larger + const uint32_t chunkSpace = 1024 + s_incrChunkSize; + xcb_change_property(xcbConn, + XCB_PROP_MODE_REPLACE, + m_request->requestor, + m_request->property, + atoms->incr, + 32, 1, &chunkSpace); + xcb_flush(xcbConn); + + setIncr(true); + // first data will be flushed after the property has been deleted + // again by the requestor + flushPropOnDelete = true; + propertyIsSet = true; + Q_EMIT selNotify(m_request, true); +} + +void TransferWltoX::readWlSource() +{ + if (chunks.size() == 0 || + chunks.last().second == s_incrChunkSize) { + // append new chunk + auto next = QPair(); + next.first.resize(s_incrChunkSize); + next.second = 0; + chunks.append(next); + } + + const auto oldLen = chunks.last().second; + const auto avail = s_incrChunkSize - chunks.last().second; + Q_ASSERT(avail > 0); + + ssize_t readLen = read(fd(), chunks.last().first.data() + oldLen, avail); + if (readLen == -1) { + qCWarning(KWIN_XWL) << "Error reading in Wl data."; + + // TODO: cleanup X side? + endTransfer(); + return; + } + chunks.last().second = oldLen + readLen; + + if (readLen == 0) { + // at the fd end - complete transfer now + chunks.last().first.resize(chunks.last().second); + + if (incr()) { + // incremental transfer is to be completed now + flushPropOnDelete = true; + if (!propertyIsSet) { + // flush if target's property is not set at the moment + flushSourceData(); + } + clearSocketNotifier(); + } else { + // non incremental transfer is to be completed now, + // data can be transfered to X client via a single property set + flushSourceData(); + Q_EMIT selNotify(m_request, true); + endTransfer(); + } + } else if (chunks.last().second == s_incrChunkSize) { + // first chunk full, but not yet at fd end -> go incremental + if (incr()) { + flushPropOnDelete = true; + if (!propertyIsSet) { + // flush if target's property is not set at the moment + flushSourceData(); + } + } else { + // starting incremental transfer + startIncr(); + } + } + resetTimeout(); +} + +bool TransferWltoX::handlePropNotify(xcb_property_notify_event_t *event) +{ + if (event->window == m_request->requestor) { + if (event->state == XCB_PROPERTY_DELETE && + event->atom == m_request->property) { + handlePropDelete(); + } + return true; + } + return false; +} + +void TransferWltoX::handlePropDelete() +{ + if (!incr()) { + // non-incremental transfer: nothing to do + return; + } + propertyIsSet = false; + + if (flushPropOnDelete) { + if (!socketNotifier() && chunks.isEmpty()) { + // transfer complete + auto *xcbConn = kwinApp()->x11Connection(); + + uint32_t mask[] = {0}; + xcb_change_window_attributes (xcbConn, + m_request->requestor, + XCB_CW_EVENT_MASK, mask); + + xcb_change_property(xcbConn, + XCB_PROP_MODE_REPLACE, + m_request->requestor, + m_request->property, + m_request->target, + 8, 0, NULL); + xcb_flush(xcbConn); + flushPropOnDelete = false; + endTransfer(); + } else { + flushSourceData(); + } + } +} + +TransferXtoWl::TransferXtoWl(xcb_atom_t selection, xcb_atom_t target, qint32 fd, + xcb_timestamp_t timestamp, xcb_window_t parentWindow, + QObject *parent) + : Transfer(selection, fd, timestamp, parent) +{ + // create transfer window + auto *xcbConn = kwinApp()->x11Connection(); + m_window = xcb_generate_id(xcbConn); + const uint32_t values[] = { XCB_EVENT_MASK_SUBSTRUCTURE_NOTIFY | + XCB_EVENT_MASK_PROPERTY_CHANGE }; + xcb_create_window(xcbConn, + XCB_COPY_FROM_PARENT, + m_window, + parentWindow, + 0, 0, + 10, 10, + 0, + XCB_WINDOW_CLASS_INPUT_OUTPUT, + Xwayland::self()->xcbScreen()->root_visual, + XCB_CW_EVENT_MASK, + values); + // convert selection + xcb_convert_selection(xcbConn, + m_window, + selection, + target, + atoms->wl_selection, + timestamp); + xcb_flush(xcbConn); +} + +TransferXtoWl::~TransferXtoWl() +{ + auto *xcbConn = kwinApp()->x11Connection(); + xcb_destroy_window(xcbConn, m_window); + xcb_flush(xcbConn); + + delete m_receiver; + m_receiver = nullptr; +} + +bool TransferXtoWl::handlePropNotify(xcb_property_notify_event_t *event) +{ + if (event->window == m_window) { + if (event->state == XCB_PROPERTY_NEW_VALUE && + event->atom == atoms->wl_selection) { + getIncrChunk(); + } + return true; + } + return false; +} + +bool TransferXtoWl::handleSelNotify(xcb_selection_notify_event_t *event) +{ + if (event->requestor != m_window) { + return false; + } + if (event->selection != atom()) { + return false; + } + if (event->property == XCB_ATOM_NONE) { + qCWarning(KWIN_XWL) << "Incoming X selection conversion failed"; + return true; + } + if (event->target == atoms->targets) { + qCWarning(KWIN_XWL) << "Received targets too late"; + // TODO: or allow it? + return true; + } + if (m_receiver) { + // second selection notify element - misbehaving source + + // TODO: cancel this transfer? + return True; + } + + m_receiver = new DataReceiver; + startTransfer(); + return true; +} + +void TransferXtoWl::startTransfer() +{ + auto *xcbConn = kwinApp()->x11Connection(); + auto cookie = xcb_get_property(xcbConn, + 1, + m_window, + atoms->wl_selection, + XCB_GET_PROPERTY_TYPE_ANY, + 0, + 0x1fffffff + ); + + auto *reply = xcb_get_property_reply(xcbConn, cookie, NULL); + if (reply == NULL) { + qCWarning(KWIN_XWL) << "Can't get selection property."; + endTransfer(); + return; + } + + if (reply->type == atoms->incr) { + setIncr(true); + free(reply); + } else { + setIncr(false); + // reply's ownership is transferred + m_receiver->transferFromProperty(reply); + dataSourceWrite(); + } +} + +void TransferXtoWl::getIncrChunk() +{ + if (!incr()) { + // source tries to sent incrementally, but did not announce it before + return; + } + if (!m_receiver) { + // receive mechanism has not yet been setup + return; + } + auto *xcbConn = kwinApp()->x11Connection(); + + auto cookie = xcb_get_property(xcbConn, + 0, + m_window, + atoms->wl_selection, + XCB_GET_PROPERTY_TYPE_ANY, + 0, + 0x1fffffff); + + auto *reply = xcb_get_property_reply(xcbConn, cookie, NULL); + if (reply == NULL) { + qCWarning(KWIN_XWL) << "Can't get selection property."; + endTransfer(); + return; + } + + if (xcb_get_property_value_length(reply) > 0) { + // reply's ownership is transferred + m_receiver->transferFromProperty(reply); + dataSourceWrite(); + } else { + // Transfer complete + free(reply); + endTransfer(); + } +} + +DataReceiver::~DataReceiver() +{ + if (m_propertyReply) { + free(m_propertyReply); + m_propertyReply = nullptr; + } +} + +void DataReceiver::transferFromProperty(xcb_get_property_reply_t *reply) +{ + m_propertyStart = 0; + m_propertyReply = reply; + + setData(static_cast(xcb_get_property_value(reply)), + xcb_get_property_value_length(reply)); +} + +void DataReceiver::setData(char *value, int length) +{ + // simply set data without copy + m_data = QByteArray::fromRawData(value, length); +} + +QByteArray DataReceiver::data() const +{ + return QByteArray::fromRawData(m_data.data() + m_propertyStart, + m_data.size() - m_propertyStart); +} + +void DataReceiver::partRead(int length) +{ + m_propertyStart += length; + if (m_propertyStart == m_data.size()) { + Q_ASSERT(m_propertyReply); + free(m_propertyReply); + m_propertyReply = nullptr; + } +} + +void TransferXtoWl::dataSourceWrite() +{ + QByteArray property = m_receiver->data(); + + ssize_t len = write(fd(), property.constData(), property.size()); + if (len == -1) { + qCWarning(KWIN_XWL) << "X11 to Wayland write error on fd:" << fd(); + endTransfer(); + return; + } + + m_receiver->partRead(len); + if (len == property.size()) { + // property completely transferred + if (incr()) { + clearSocketNotifier(); + auto *xcbConn = kwinApp()->x11Connection(); + xcb_delete_property(xcbConn, + m_window, + atoms->wl_selection); + xcb_flush(xcbConn); + } else { + // transfer complete + endTransfer(); + } + } else { + if (!socketNotifier()) { + createSocketNotifier(QSocketNotifier::Write); + connect(socketNotifier(), &QSocketNotifier::activated, this, + [this](int socket) { + Q_UNUSED(socket); + dataSourceWrite(); + } + ); + } + } + resetTimeout(); +} + +} +} Index: xwl/xwayland.h =================================================================== --- xwl/xwayland.h +++ xwl/xwayland.h @@ -22,6 +22,8 @@ #include +#include + class QProcess; class xcb_screen_t; @@ -32,15 +34,26 @@ namespace Xwl { +class DataBridge; class Xwayland : public QObject { Q_OBJECT public: + static Xwayland* self(); + Xwayland(ApplicationWaylandAbstract *app, QObject *parent = nullptr); virtual ~Xwayland(); void init(); + void prepareDestroy(); + + xcb_screen_t *xcbScreen() const { + return m_xcbScreen; + } + const xcb_query_extension_reply_t *xfixes() const { + return m_xfixes; + } Q_SIGNALS: void criticalError(int code); @@ -53,6 +66,10 @@ QProcess *m_xwaylandProcess = nullptr; QMetaObject::Connection m_xwaylandFailConnection; + xcb_screen_t *m_xcbScreen = nullptr; + const xcb_query_extension_reply_t *m_xfixes = nullptr; + DataBridge *m_dataBridge = nullptr; + ApplicationWaylandAbstract *m_app; }; Index: xwl/xwayland.cpp =================================================================== --- xwl/xwayland.cpp +++ xwl/xwayland.cpp @@ -19,6 +19,8 @@ along with this program. If not, see . *********************************************************************/ #include "xwayland.h" +#include "databridge.h" + #include "wayland_server.h" #include "main_wayland.h" #include "utils.h" @@ -69,10 +71,17 @@ namespace Xwl { +Xwayland *s_self = nullptr; +Xwayland* Xwayland::self() +{ + return s_self; +} + Xwayland::Xwayland(ApplicationWaylandAbstract *app, QObject *parent) : QObject(parent), m_app(app) { + s_self = this; } Xwayland::~Xwayland() @@ -93,6 +102,7 @@ } waylandServer()->destroyXWaylandConnection(); } + s_self = nullptr; } void Xwayland::init() @@ -166,6 +176,12 @@ close(pipeFds[1]); } +void Xwayland::prepareDestroy() +{ + delete m_dataBridge; + m_dataBridge = nullptr; +} + void Xwayland::createX11Connection() { int screenNumber = 0; @@ -181,6 +197,10 @@ return; } + xcb_screen_iterator_t iter = xcb_setup_roots_iterator(xcb_get_setup(c)); + m_xcbScreen = iter.data; + Q_ASSERT(m_xcbScreen); + m_app->setX11Connection(c); // we don't support X11 multi-head in Wayland m_app->setX11ScreenNumber(screenNumber); @@ -199,6 +219,10 @@ QSocketNotifier *notifier = new QSocketNotifier(xcb_get_file_descriptor(xcbConn), QSocketNotifier::Read, this); auto processXcbEvents = [this, xcbConn] { while (auto event = xcb_poll_for_event(xcbConn)) { + if (m_dataBridge->filterEvent(event)) { + free(event); + continue; + } long result = 0; QThread::currentThread()->eventDispatcher()->filterNativeEvent(QByteArrayLiteral("xcb_generic_event_t"), event, &result); free(event); @@ -209,13 +233,18 @@ connect(QThread::currentThread()->eventDispatcher(), &QAbstractEventDispatcher::aboutToBlock, this, processXcbEvents); connect(QThread::currentThread()->eventDispatcher(), &QAbstractEventDispatcher::awake, this, processXcbEvents); + xcb_prefetch_extension_data(xcbConn, &xcb_xfixes_id); + m_xfixes = xcb_get_extension_data(xcbConn, &xcb_xfixes_id); + // create selection owner for WM_S0 - magic X display number expected by XWayland KSelectionOwner owner("WM_S0", xcbConn, m_app->x11RootWindow()); owner.claim(true); m_app->createAtoms(); m_app->setupEventFilters(); + m_dataBridge = new DataBridge; + // Check whether another windowmanager is running const uint32_t maskValues[] = {XCB_EVENT_MASK_SUBSTRUCTURE_REDIRECT}; ScopedCPointer redirectCheck(xcb_request_check(connection(),