From 851e456210f6f154dbee21bec3e314c6c1b01354 Mon Sep 17 00:00:00 2001 From: Aleix Pol Date: Thu, 23 Jan 2020 16:57:08 +0100 Subject: [PATCH] Use the RemoteDesktop portal to input from wayland Cross-desktop approach to moving the cursor remotely on wayland. Should work on X11 too, so we can consider drop the other one as well. It adds support for receiving full text as well, which didn't use to be possible. --- CMakeLists.txt | 1 + .../org.kde.kdeconnect.daemon.desktop.cmake | 1 - plugins/mousepad/CMakeLists.txt | 11 +- plugins/mousepad/abstractremoteinput.cpp | 1 + plugins/mousepad/abstractremoteinput.h | 3 + plugins/mousepad/waylandremoteinput.cpp | 221 +++++++++-- plugins/mousepad/waylandremoteinput.h | 35 +- .../xdp_dbus_remotedesktop_interface.xml | 374 ++++++++++++++++++ 8 files changed, 599 insertions(+), 48 deletions(-) create mode 100644 plugins/mousepad/xdp_dbus_remotedesktop_interface.xml diff --git a/CMakeLists.txt b/CMakeLists.txt index 0636c9530..faa92c667 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -85,6 +85,7 @@ else() find_package(Qt${QT_MAJOR_VERSION} REQUIRED COMPONENTS WaylandClient) find_package(PlasmaWaylandProtocols REQUIRED) find_package(WaylandProtocols REQUIRED) + pkg_check_modules(XkbCommon IMPORTED_TARGET xkbcommon) endif() find_package(KF5PeopleVCard) diff --git a/daemon/org.kde.kdeconnect.daemon.desktop.cmake b/daemon/org.kde.kdeconnect.daemon.desktop.cmake index 64dfba3a3..42b7e4bf5 100644 --- a/daemon/org.kde.kdeconnect.daemon.desktop.cmake +++ b/daemon/org.kde.kdeconnect.daemon.desktop.cmake @@ -3,7 +3,6 @@ Type=Application Exec=${KDE_INSTALL_FULL_LIBEXECDIR}/kdeconnectd X-KDE-StartupNotify=false X-KDE-autostart-phase=1 -X-KDE-Wayland-Interfaces=org_kde_kwin_fake_input X-GNOME-Autostart-enabled=true NoDisplay=true Icon=kdeconnect diff --git a/plugins/mousepad/CMakeLists.txt b/plugins/mousepad/CMakeLists.txt index edf8c155a..526949a70 100644 --- a/plugins/mousepad/CMakeLists.txt +++ b/plugins/mousepad/CMakeLists.txt @@ -1,6 +1,13 @@ kdeconnect_add_plugin(kdeconnect_mousepad SOURCES mousepadplugin.cpp abstractremoteinput.cpp) if(UNIX AND NOT APPLE) + qt5_add_dbus_interface( + SRCS + ${CMAKE_CURRENT_SOURCE_DIR}/xdp_dbus_remotedesktop_interface.xml + xdp_dbus_remotedesktop_interface + ) + + target_sources(kdeconnect_mousepad PUBLIC waylandremoteinput.cpp ${SRCS}) find_package(LibFakeKey QUIET) set_package_properties(LibFakeKey PROPERTIES DESCRIPTION "fake key events" @@ -20,7 +27,7 @@ if(UNIX AND NOT APPLE) endif() target_sources(kdeconnect_mousepad PRIVATE ${wayland_SRCS}) - target_link_libraries(kdeconnect_mousepad Wayland::Client Qt::WaylandClient) + target_link_libraries(kdeconnect_mousepad Wayland::Client Qt::WaylandClient PkgConfig::XkbCommon) target_sources(kdeconnect_mousepad PUBLIC waylandremoteinput.cpp) set(HAVE_WAYLAND TRUE) @@ -37,7 +44,7 @@ endif() set(HAVE_WINDOWS ${WIN32}) set(HAVE_X11 ${LibFakeKey_FOUND}) set(HAVE_MACOS ${APPLE}) -configure_file(config-mousepad.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-mousepad.h ) +configure_file(config-mousepad.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-mousepad.h) target_link_libraries(kdeconnect_mousepad kdeconnectcore Qt::Gui KF5::I18n) diff --git a/plugins/mousepad/abstractremoteinput.cpp b/plugins/mousepad/abstractremoteinput.cpp index 8ba8e9cb2..3c7ad3368 100644 --- a/plugins/mousepad/abstractremoteinput.cpp +++ b/plugins/mousepad/abstractremoteinput.cpp @@ -6,6 +6,7 @@ #include "abstractremoteinput.h" +Q_LOGGING_CATEGORY(KDECONNECT_PLUGIN_MOUSEPAD, "kdeconnect.plugin.mousepad") AbstractRemoteInput::AbstractRemoteInput(QObject *parent) : QObject(parent) { diff --git a/plugins/mousepad/abstractremoteinput.h b/plugins/mousepad/abstractremoteinput.h index 3b70d0c10..2c4f07ea0 100644 --- a/plugins/mousepad/abstractremoteinput.h +++ b/plugins/mousepad/abstractremoteinput.h @@ -7,10 +7,13 @@ #ifndef ABSTRACTREMOTEINPUT_H #define ABSTRACTREMOTEINPUT_H +#include #include #include +Q_DECLARE_LOGGING_CATEGORY(KDECONNECT_PLUGIN_MOUSEPAD) + class AbstractRemoteInput : public QObject { Q_OBJECT diff --git a/plugins/mousepad/waylandremoteinput.cpp b/plugins/mousepad/waylandremoteinput.cpp index 9ab2c24fa..96be436ab 100644 --- a/plugins/mousepad/waylandremoteinput.cpp +++ b/plugins/mousepad/waylandremoteinput.cpp @@ -1,5 +1,6 @@ /** * SPDX-FileCopyrightText: 2015 Martin Gräßlin + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ @@ -10,42 +11,177 @@ #include #include - -#include "qwayland-fake-input.h" -#include +#include #include +#include -class FakeInput : public QWaylandClientExtensionTemplate, public QtWayland::org_kde_kwin_fake_input +namespace { -public: - FakeInput() - : QWaylandClientExtensionTemplate(4) - { - } +// Translation table to keep in sync within all the implementations +int SpecialKeysMap[] = { + 0, // Invalid + KEY_BACKSPACE, // 1 + KEY_TAB, // 2 + KEY_LINEFEED, // 3 + KEY_LEFT, // 4 + KEY_UP, // 5 + KEY_RIGHT, // 6 + KEY_DOWN, // 7 + KEY_PAGEUP, // 8 + KEY_PAGEDOWN, // 9 + KEY_HOME, // 10 + KEY_END, // 11 + KEY_ENTER, // 12 + KEY_DELETE, // 13 + KEY_ESC, // 14 + KEY_SYSRQ, // 15 + KEY_SCROLLLOCK, // 16 + 0, // 17 + 0, // 18 + 0, // 19 + 0, // 20 + KEY_F1, // 21 + KEY_F2, // 22 + KEY_F3, // 23 + KEY_F4, // 24 + KEY_F5, // 25 + KEY_F6, // 26 + KEY_F7, // 27 + KEY_F8, // 28 + KEY_F9, // 29 + KEY_F10, // 30 + KEY_F11, // 31 + KEY_F12, // 32 }; +} + +Q_GLOBAL_STATIC(RemoteDesktopSession, s_session); + +RemoteDesktopSession::RemoteDesktopSession() + : iface(new OrgFreedesktopPortalRemoteDesktopInterface(QLatin1String("org.freedesktop.portal.Desktop"), + QLatin1String("/org/freedesktop/portal/desktop"), + QDBusConnection::sessionBus(), + this)) +{ +} + +void RemoteDesktopSession::createSession() +{ + if (isValid()) { + qCDebug(KDECONNECT_PLUGIN_MOUSEPAD) << "pass, already created"; + return; + } + + m_connecting = true; + + // create session + const auto handleToken = QStringLiteral("kdeconnect%1").arg(QRandomGenerator::global()->generate()); + const auto sessionParameters = QVariantMap{{QLatin1String("session_handle_token"), handleToken}, {QLatin1String("handle_token"), handleToken}}; + auto sessionReply = iface->CreateSession(sessionParameters); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(sessionReply); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, sessionReply](QDBusPendingCallWatcher *self) { + self->deleteLater(); + if (sessionReply.isError()) { + qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Could not create the remote control session" << sessionReply.error(); + m_connecting = false; + return; + } + + bool b = QDBusConnection::sessionBus().connect(QString(), + sessionReply.value().path(), + QLatin1String("org.freedesktop.portal.Request"), + QLatin1String("Response"), + this, + SLOT(handleXdpSessionCreated(uint, QVariantMap))); + Q_ASSERT(b); + + qCDebug(KDECONNECT_PLUGIN_MOUSEPAD) << "authenticating" << sessionReply.value().path(); + }); +} + +void RemoteDesktopSession::handleXdpSessionCreated(uint code, const QVariantMap &results) +{ + if (code != 0) { + qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Failed to create session with code" << code << results; + return; + } + m_connecting = false; + m_xdpPath = QDBusObjectPath(results.value(QLatin1String("session_handle")).toString()); + const QVariantMap startParameters = { + {QLatin1String("handle_token"), QStringLiteral("kdeconnect%1").arg(QRandomGenerator::global()->generate())}, + {QStringLiteral("types"), QVariant::fromValue(7)}, // request all (KeyBoard, Pointer, TouchScreen) + }; + + QDBusConnection::sessionBus().connect(QString(), + m_xdpPath.path(), + QLatin1String("org.freedesktop.portal.Session"), + QLatin1String("Closed"), + this, + SLOT(handleXdpSessionFinished(uint, QVariantMap))); + + auto reply = iface->SelectDevices(m_xdpPath, startParameters); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, reply](QDBusPendingCallWatcher *self) { + self->deleteLater(); + if (reply.isError()) { + qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Could not start the remote control session" << reply.error(); + m_connecting = false; + return; + } + + bool b = QDBusConnection::sessionBus().connect(QString(), + reply.value().path(), + QLatin1String("org.freedesktop.portal.Request"), + QLatin1String("Response"), + this, + SLOT(handleXdpSessionConfigured(uint, QVariantMap))); + Q_ASSERT(b); + qCDebug(KDECONNECT_PLUGIN_MOUSEPAD) << "configuring" << reply.value().path(); + }); +} + +void RemoteDesktopSession::handleXdpSessionConfigured(uint code, const QVariantMap &results) +{ + if (code != 0) { + qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Failed to configure session with code" << code << results; + m_connecting = false; + return; + } + const QVariantMap startParameters = { + {QLatin1String("handle_token"), QStringLiteral("kdeconnect%1").arg(QRandomGenerator::global()->generate())}, + }; + auto reply = iface->Start(m_xdpPath, {}, startParameters); + QDBusPendingCallWatcher *watcher = new QDBusPendingCallWatcher(reply); + connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, reply](QDBusPendingCallWatcher *self) { + self->deleteLater(); + if (reply.isError()) { + qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Could not start the remote control session" << reply.error(); + m_connecting = false; + } + }); +} + +void RemoteDesktopSession::handleXdpSessionFinished(uint /*code*/, const QVariantMap & /*results*/) +{ + m_xdpPath = {}; +} WaylandRemoteInput::WaylandRemoteInput(QObject *parent) : AbstractRemoteInput(parent) - , m_waylandAuthenticationRequested(false) { - m_fakeInput = new FakeInput; } WaylandRemoteInput::~WaylandRemoteInput() { - delete m_fakeInput; } bool WaylandRemoteInput::handlePacket(const NetworkPacket &np) { - if (!m_fakeInput->isActive()) { - return true; - } - - if (!m_waylandAuthenticationRequested) { - m_fakeInput->authenticate(i18n("KDE Connect"), i18n("Use your phone as a touchpad and keyboard")); - m_waylandAuthenticationRequested = true; + if (!s_session->isValid()) { + qCWarning(KDECONNECT_PLUGIN_MOUSEPAD) << "Unable to handle remote input. RemoteDesktop portal not authenticated"; + s_session->createSession(); + return false; } const float dx = np.get(QStringLiteral("dx"), 0); @@ -63,36 +199,43 @@ bool WaylandRemoteInput::handlePacket(const NetworkPacket &np) if (isSingleClick || isDoubleClick || isMiddleClick || isRightClick || isSingleHold || isSingleRelease || isScroll || !key.isEmpty() || specialKey) { if (isSingleClick) { - m_fakeInput->button(BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); - m_fakeInput->button(BTN_LEFT, WL_POINTER_BUTTON_STATE_RELEASED); - + s_session->iface->NotifyPointerButton(s_session->m_xdpPath, {}, BTN_LEFT, 1); + s_session->iface->NotifyPointerButton(s_session->m_xdpPath, {}, BTN_LEFT, 0); } else if (isDoubleClick) { - m_fakeInput->button(BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); - m_fakeInput->button(BTN_LEFT, WL_POINTER_BUTTON_STATE_RELEASED); - - m_fakeInput->button(BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); - m_fakeInput->button(BTN_LEFT, WL_POINTER_BUTTON_STATE_RELEASED); + s_session->iface->NotifyPointerButton(s_session->m_xdpPath, {}, BTN_LEFT, 1); + s_session->iface->NotifyPointerButton(s_session->m_xdpPath, {}, BTN_LEFT, 0); + s_session->iface->NotifyPointerButton(s_session->m_xdpPath, {}, BTN_LEFT, 1); + s_session->iface->NotifyPointerButton(s_session->m_xdpPath, {}, BTN_LEFT, 0); } else if (isMiddleClick) { - m_fakeInput->button(BTN_MIDDLE, WL_POINTER_BUTTON_STATE_PRESSED); - m_fakeInput->button(BTN_MIDDLE, WL_POINTER_BUTTON_STATE_RELEASED); + s_session->iface->NotifyPointerButton(s_session->m_xdpPath, {}, BTN_MIDDLE, 1); + s_session->iface->NotifyPointerButton(s_session->m_xdpPath, {}, BTN_MIDDLE, 0); } else if (isRightClick) { - m_fakeInput->button(BTN_RIGHT, WL_POINTER_BUTTON_STATE_PRESSED); - m_fakeInput->button(BTN_RIGHT, WL_POINTER_BUTTON_STATE_RELEASED); + s_session->iface->NotifyPointerButton(s_session->m_xdpPath, {}, BTN_RIGHT, 1); + s_session->iface->NotifyPointerButton(s_session->m_xdpPath, {}, BTN_RIGHT, 0); } else if (isSingleHold) { // For drag'n drop - m_fakeInput->button(BTN_LEFT, WL_POINTER_BUTTON_STATE_PRESSED); + s_session->iface->NotifyPointerButton(s_session->m_xdpPath, {}, BTN_LEFT, 1); } else if (isSingleRelease) { // For drag'n drop. NEVER USED (release is done by tapping, which actually triggers a isSingleClick). Kept here for future-proofness. - m_fakeInput->button(BTN_LEFT, WL_POINTER_BUTTON_STATE_RELEASED); + s_session->iface->NotifyPointerButton(s_session->m_xdpPath, {}, BTN_LEFT, 0); } else if (isScroll) { - m_fakeInput->axis(WL_POINTER_AXIS_VERTICAL_SCROLL, wl_fixed_from_double(-dy)); - - } else if (!key.isEmpty() || specialKey) { - // TODO: implement key support + s_session->iface->NotifyPointerAxis(s_session->m_xdpPath, {}, dx, dy); + } else if (specialKey) { + s_session->iface->NotifyKeyboardKeycode(s_session->m_xdpPath, {}, SpecialKeysMap[specialKey], 1); + s_session->iface->NotifyKeyboardKeycode(s_session->m_xdpPath, {}, SpecialKeysMap[specialKey], 0); + } else if (!key.isEmpty()) { + for (const QChar character : key) { + const auto keysym = xkb_utf32_to_keysym(character.unicode()); + if (keysym != XKB_KEY_NoSymbol) { + s_session->iface->NotifyKeyboardKeysym(s_session->m_xdpPath, {}, keysym, 1).waitForFinished(); + s_session->iface->NotifyKeyboardKeysym(s_session->m_xdpPath, {}, keysym, 0).waitForFinished(); + } else { + qCDebug(KDECONNECT_PLUGIN_MOUSEPAD) << "Cannot send character" << character; + } + } } - } else { // Is a mouse move event - m_fakeInput->pointer_motion(wl_fixed_from_double(dx), wl_fixed_from_double(dy)); + s_session->iface->NotifyPointerMotion(s_session->m_xdpPath, {}, dx, dy); } return true; } diff --git a/plugins/mousepad/waylandremoteinput.h b/plugins/mousepad/waylandremoteinput.h index 244ebb657..44648506a 100644 --- a/plugins/mousepad/waylandremoteinput.h +++ b/plugins/mousepad/waylandremoteinput.h @@ -1,5 +1,6 @@ /** * SPDX-FileCopyrightText: 2018 Albert Vaca Cintora + * SPDX-FileCopyrightText: 2020 Aleix Pol Gonzalez * * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL */ @@ -8,9 +9,33 @@ #define WAYLANDREMOTEINPUT_H #include "abstractremoteinput.h" +#include "xdp_dbus_remotedesktop_interface.h" +#include class FakeInput; +class RemoteDesktopSession : public QObject +{ + Q_OBJECT +public: + RemoteDesktopSession(); + void createSession(); + bool isValid() const + { + return m_connecting || !m_xdpPath.path().isEmpty(); + } + OrgFreedesktopPortalRemoteDesktopInterface *const iface; + QDBusObjectPath m_xdpPath; + bool m_connecting = false; + +private Q_SLOTS: + void handleXdpSessionCreated(uint code, const QVariantMap &results); + void handleXdpSessionConfigured(uint code, const QVariantMap &results); + void handleXdpSessionFinished(uint code, const QVariantMap &results); + +private: +}; + class WaylandRemoteInput : public AbstractRemoteInput { Q_OBJECT @@ -20,12 +45,10 @@ public: ~WaylandRemoteInput(); bool handlePacket(const NetworkPacket &np) override; - -private: - void setupWaylandIntegration(); - - FakeInput *m_fakeInput; - bool m_waylandAuthenticationRequested; + bool hasKeyboardSupport() override + { + return true; + } }; #endif diff --git a/plugins/mousepad/xdp_dbus_remotedesktop_interface.xml b/plugins/mousepad/xdp_dbus_remotedesktop_interface.xml new file mode 100644 index 000000000..054ea2b63 --- /dev/null +++ b/plugins/mousepad/xdp_dbus_remotedesktop_interface.xml @@ -0,0 +1,374 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +