From e58c1a1415c3021480c4350369cef6def528574e Mon Sep 17 00:00:00 2001 From: David Redondo Date: Tue, 25 Jun 2024 11:28:38 +0200 Subject: [PATCH] Add shareinptudevices plugin This plugin provides synergy-like behavior of sharing input devices between machines by moving the cursor seamlessly between them. To this end this uses the InputCapture Portal to be notified when the cursor moves 'out of the screen'. For forwarding input the existing mousepad infrastructure is used. On the other side a tiny hidden plugin listens to mouse events to track when input should pass back to source machine. --- interfaces/CMakeLists.txt | 3 + .../org.freedesktop.portal.InputCapture.xml | 530 ++++++++++++++++++ .../org.freedesktop.portal.Request.xml | 91 +++ .../org.freedesktop.portal.Session.xml | 76 +++ plugins/CMakeLists.txt | 2 + plugins/shareinputdevices/CMakeLists.txt | 19 + plugins/shareinputdevices/README | 1 + .../shareinputdevices/inputcapturesession.cpp | 450 +++++++++++++++ .../shareinputdevices/inputcapturesession.h | 70 +++ .../kdeconnect_shareinputdevices.json | 23 + .../kdeconnect_shareinputdevices_config.qml | 79 +++ .../shareinputdevices_config.cpp | 80 +++ .../shareinputdevices_config.h | 29 + .../shareinputdevices_config.ui | 72 +++ .../shareinputdevicesplugin.cpp | 145 +++++ .../shareinputdevicesplugin.h | 31 + .../shareinputdevicesremote/CMakeLists.txt | 7 + .../kdeconnect_shareinputdevicesremote.json | 22 + .../shareinputdevicesremoteplugin.cpp | 108 ++++ .../shareinputdevicesremoteplugin.h | 32 ++ 20 files changed, 1870 insertions(+) create mode 100644 interfaces/systeminterfaces/org.freedesktop.portal.InputCapture.xml create mode 100644 interfaces/systeminterfaces/org.freedesktop.portal.Request.xml create mode 100644 interfaces/systeminterfaces/org.freedesktop.portal.Session.xml create mode 100644 plugins/shareinputdevices/CMakeLists.txt create mode 100644 plugins/shareinputdevices/README create mode 100644 plugins/shareinputdevices/inputcapturesession.cpp create mode 100644 plugins/shareinputdevices/inputcapturesession.h create mode 100644 plugins/shareinputdevices/kdeconnect_shareinputdevices.json create mode 100644 plugins/shareinputdevices/kdeconnect_shareinputdevices_config.qml create mode 100644 plugins/shareinputdevices/shareinputdevices_config.cpp create mode 100644 plugins/shareinputdevices/shareinputdevices_config.h create mode 100644 plugins/shareinputdevices/shareinputdevices_config.ui create mode 100644 plugins/shareinputdevices/shareinputdevicesplugin.cpp create mode 100644 plugins/shareinputdevices/shareinputdevicesplugin.h create mode 100644 plugins/shareinputdevicesremote/CMakeLists.txt create mode 100644 plugins/shareinputdevicesremote/kdeconnect_shareinputdevicesremote.json create mode 100644 plugins/shareinputdevicesremote/shareinputdevicesremoteplugin.cpp create mode 100644 plugins/shareinputdevicesremote/shareinputdevicesremoteplugin.h diff --git a/interfaces/CMakeLists.txt b/interfaces/CMakeLists.txt index da6a9dd36..c62866942 100644 --- a/interfaces/CMakeLists.txt +++ b/interfaces/CMakeLists.txt @@ -71,6 +71,9 @@ if (UNIX AND NOT APPLE) geninterface(systeminterfaces/org.mpris.MediaPlayer2.Player.xml generated/systeminterfaces/mprisplayer) geninterface(systeminterfaces/org.mpris.MediaPlayer2.xml generated/systeminterfaces/mprisroot) geninterface(systeminterfaces/org.freedesktop.portal.RemoteDesktop.xml generated/systeminterfaces/remotedesktop) + geninterface(systeminterfaces/org.freedesktop.portal.InputCapture.xml generated/systeminterfaces/inputcapture) + geninterface(systeminterfaces/org.freedesktop.portal.Request.xml generated/systeminterfaces/request) + geninterface(systeminterfaces/org.freedesktop.portal.Session.xml generated/systeminterfaces/session) endif() add_library(kdeconnectinterfaces STATIC) diff --git a/interfaces/systeminterfaces/org.freedesktop.portal.InputCapture.xml b/interfaces/systeminterfaces/org.freedesktop.portal.InputCapture.xml new file mode 100644 index 000000000..a6cc029b3 --- /dev/null +++ b/interfaces/systeminterfaces/org.freedesktop.portal.InputCapture.xml @@ -0,0 +1,530 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/interfaces/systeminterfaces/org.freedesktop.portal.Request.xml b/interfaces/systeminterfaces/org.freedesktop.portal.Request.xml new file mode 100644 index 000000000..b5d81110d --- /dev/null +++ b/interfaces/systeminterfaces/org.freedesktop.portal.Request.xml @@ -0,0 +1,91 @@ + + + + + + + + + + + + + + + + + + + diff --git a/interfaces/systeminterfaces/org.freedesktop.portal.Session.xml b/interfaces/systeminterfaces/org.freedesktop.portal.Session.xml new file mode 100644 index 000000000..6ead5e65e --- /dev/null +++ b/interfaces/systeminterfaces/org.freedesktop.portal.Session.xml @@ -0,0 +1,76 @@ + + + + + + + + + + + + + + + + + + + diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 83a3856de..991016b86 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -43,6 +43,8 @@ if(${CMAKE_SYSTEM_NAME} STREQUAL "Linux") if (TARGET KF6::ModemManagerQt) add_subdirectory(mmtelephony) endif() + add_subdirectory(shareinputdevices) + add_subdirectory(shareinputdevicesremote) endif() if(NOT APPLE) diff --git a/plugins/shareinputdevices/CMakeLists.txt b/plugins/shareinputdevices/CMakeLists.txt new file mode 100644 index 000000000..999f9b074 --- /dev/null +++ b/plugins/shareinputdevices/CMakeLists.txt @@ -0,0 +1,19 @@ +kdeconnect_add_plugin(kdeconnect_shareinputdevices SOURCES shareinputdevicesplugin.cpp inputcapturesession.cpp) + +pkg_check_modules(PKG_libei REQUIRED IMPORTED_TARGET libei-1.0) + +target_link_libraries(kdeconnect_shareinputdevices + kdeconnectcore + kdeconnectinterfaces + KF6::I18n + Qt::GuiPrivate + PkgConfig::PKG_libei +) + +kdeconnect_add_kcm(kdeconnect_shareinputdevices_config SOURCES shareinputdevices_config.cpp) +ki18n_wrap_ui(kdeconnect_shareinputdevices_config shareinputdevices_config.ui) +target_link_libraries(kdeconnect_shareinputdevices_config + kdeconnectpluginkcm + kdeconnectinterfaces + KF6::I18n +) diff --git a/plugins/shareinputdevices/README b/plugins/shareinputdevices/README new file mode 100644 index 000000000..0856a42cf --- /dev/null +++ b/plugins/shareinputdevices/README @@ -0,0 +1 @@ +This plugin allows sharing keyboard and mouse with another machine. diff --git a/plugins/shareinputdevices/inputcapturesession.cpp b/plugins/shareinputdevices/inputcapturesession.cpp new file mode 100644 index 000000000..c8bb29fc5 --- /dev/null +++ b/plugins/shareinputdevices/inputcapturesession.cpp @@ -0,0 +1,450 @@ +/** + * SPDX-FileCopyrightText: 2024 David Redondo + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "inputcapturesession.h" +#include "plugin_shareinputdevices_debug.h" + +#include "generated/systeminterfaces/inputcapture.h" +#include "generated/systeminterfaces/request.h" +#include "generated/systeminterfaces/session.h" + +#include +#include + +#include +#include + +#include +#include + +#include + +using namespace Qt::StringLiterals; + +static QString portalName() +{ + return u"org.freedesktop.portal.Desktop"_s; +} + +static QString portalPath() +{ + return u"/org/freedesktop/portal/desktop"_s; +}; + +static QString requestPath(const QString &token) +{ + static QString senderString = QDBusConnection::sessionBus().baseService().remove(0, 1).replace(u'.', u'_'); + return u"%1/request/%2/%3"_s.arg(portalPath()).arg(senderString).arg(token); +}; + +class Xkb +{ +public: + Xkb() + { + m_xkbcontext.reset(xkb_context_new(XKB_CONTEXT_NO_FLAGS)); + m_xkbkeymap.reset(xkb_keymap_new_from_names(m_xkbcontext.get(), nullptr, XKB_KEYMAP_COMPILE_NO_FLAGS)); + m_xkbstate.reset(xkb_state_new(m_xkbkeymap.get())); + } + Xkb(int keymapFd, int size) + { + m_xkbcontext.reset(xkb_context_new(XKB_CONTEXT_NO_FLAGS)); + char *map = static_cast(mmap(nullptr, size, PROT_READ, MAP_PRIVATE, keymapFd, 0)); + if (map != MAP_FAILED) { + m_xkbkeymap.reset(xkb_keymap_new_from_string(m_xkbcontext.get(), map, XKB_KEYMAP_FORMAT_TEXT_V1, XKB_KEYMAP_COMPILE_NO_FLAGS)); + munmap(map, size); + } + close(keymapFd); + if (!m_xkbkeymap) { + qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Failed to create keymap"; + m_xkbkeymap.reset(xkb_keymap_new_from_names(m_xkbcontext.get(), nullptr, XKB_KEYMAP_COMPILE_NO_FLAGS)); + } + m_xkbstate.reset(xkb_state_new(m_xkbkeymap.get())); + } + + void updateModifiers(uint32_t depressed, uint32_t latched, uint32_t locked, uint32_t group) + { + xkb_state_update_mask(m_xkbstate.get(), depressed, latched, locked, 0, 0, group); + } + + void updateKey(uint32_t key, bool pressed) + { + xkb_state_update_key(m_xkbstate.get(), key, pressed ? XKB_KEY_DOWN : XKB_KEY_UP); + } + + xkb_state *currentState() const + { + return m_xkbstate.get(); + } + +private: + template + using deleter = std::integral_constant; + std::unique_ptr> m_xkbcontext; + std::unique_ptr> m_xkbkeymap; + std::unique_ptr> m_xkbstate; +}; + +InputCaptureSession::InputCaptureSession(QObject *parent) + : QObject(parent) + , m_xkb(new Xkb) + , m_inputCapturePortal(new OrgFreedesktopPortalInputCaptureInterface(portalName(), portalPath(), QDBusConnection::sessionBus(), this)) + +{ + connect(m_inputCapturePortal, &OrgFreedesktopPortalInputCaptureInterface::Disabled, this, &InputCaptureSession::disabled); + connect(m_inputCapturePortal, &OrgFreedesktopPortalInputCaptureInterface::Activated, this, &InputCaptureSession::activated); + connect(m_inputCapturePortal, &OrgFreedesktopPortalInputCaptureInterface::Deactivated, this, &InputCaptureSession::deactivated); + connect(m_inputCapturePortal, &OrgFreedesktopPortalInputCaptureInterface::ZonesChanged, this, &InputCaptureSession::zonesChanged); + + const QString token = u"kdeconnect_shareinputdevices%1"_s.arg(QRandomGenerator::global()->generate()); + auto request = new OrgFreedesktopPortalRequestInterface(portalName(), requestPath(token), QDBusConnection::sessionBus(), this); + connect(request, &OrgFreedesktopPortalRequestInterface::Response, request, &QObject::deleteLater); + connect(request, &OrgFreedesktopPortalRequestInterface::Response, this, &InputCaptureSession::sessionCreated); + auto call = m_inputCapturePortal->CreateSession(QString(), {{u"handle_token"_s, token}, {u"session_handle_token"_s, token}, {u"capabilities"_s, 3u}}); + connect(new QDBusPendingCallWatcher(call, this), &QDBusPendingCallWatcher::finished, request, [request](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + if (watcher->isError()) { + qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Error creating input capture session" << watcher->error(); + request->deleteLater(); + } + }); +} + +InputCaptureSession::~InputCaptureSession() +{ + if (m_session) { + m_session->Close(); + } + if (m_ei) { + ei_unref(m_ei); + } +} + +void InputCaptureSession::sessionCreated(uint response, const QVariantMap &results) +{ + if (response != 0) { + qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Couldn't create input capture session"; + return; + } + m_session = std::make_unique(portalName(), + results[u"session_handle"_s].value().path(), + QDBusConnection::sessionBus()); + connect(m_session.get(), &OrgFreedesktopPortalSessionInterface::Closed, this, &InputCaptureSession::sessionClosed); + + auto call = m_inputCapturePortal->ConnectToEIS(QDBusObjectPath(m_session->path()), {}); + connect(new QDBusPendingCallWatcher(call, this), &QDBusPendingCallWatcher::finished, this, [this](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + QDBusReply reply = *watcher; + if (!reply.isValid()) { + qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Error getting eis fd" << watcher->error(); + return; + } + qCDebug(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Received ei fd" << reply.value().fileDescriptor(); + setupEi(reply.value().takeFileDescriptor()); + }); + getZones(); +} + +void InputCaptureSession::getZones() +{ + const QString token = u"kdeconnect_shareinputdevices%1"_s.arg(QRandomGenerator::global()->generate()); + auto request = new OrgFreedesktopPortalRequestInterface(portalName(), requestPath(token), QDBusConnection::sessionBus(), this); + connect(request, &OrgFreedesktopPortalRequestInterface::Response, request, &QObject::deleteLater); + connect(request, &OrgFreedesktopPortalRequestInterface::Response, this, &InputCaptureSession::zonesReceived); + auto call = m_inputCapturePortal->GetZones(QDBusObjectPath(m_session->path()), {{u"handle_token"_s, token}}); + connect(new QDBusPendingCallWatcher(call, this), &QDBusPendingCallWatcher::finished, request, [request](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + if (watcher->isError()) { + qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Error getting zones" << watcher->error(); + request->deleteLater(); + } + }); +} + +void InputCaptureSession::zonesReceived(uint response, const QVariantMap &results) +{ + if (response != 0) { + qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Couldn't create input capture session"; + return; + } + m_currentZoneSet = results[u"zone_set"_s].toUInt(); + m_currentZones.clear(); + const QDBusArgument zoneArgument = results[u"zones"_s].value(); + zoneArgument.beginArray(); + while (!zoneArgument.atEnd()) { + int width; + int height; + uint x; + uint y; + zoneArgument.beginStructure(); + zoneArgument >> width >> height >> x >> y; + zoneArgument.endStructure(); + m_currentZones.push_back(QRect(x, y, width, height)); + } + zoneArgument.endArray(); + + setUpBarrier(); +} + +void InputCaptureSession::setUpBarrier() +{ + // Find the left/right/bottom/top-most screen + if (m_barrierEdge == Qt::LeftEdge) { + std::stable_sort(m_currentZones.begin(), m_currentZones.end(), [](const QRect &lhs, const QRect &rhs) { + return lhs.x() < rhs.x(); + }); + const auto &zone = m_currentZones.front(); + // Deliberate QRect::bottom usage, on a 1920x1080 screen this needs to be 1079 + m_barrier = {zone.x(), zone.y(), zone.x(), zone.bottom()}; + } else if (m_barrierEdge == Qt::RightEdge) { + std::stable_sort(m_currentZones.begin(), m_currentZones.end(), [](const QRect &lhs, const QRect &rhs) { + return lhs.x() + lhs.width() > rhs.x() + rhs.width(); + }); + const auto &zone = m_currentZones.front(); + m_barrier = {zone.x() + zone.width(), zone.y(), zone.x() + zone.width(), zone.bottom()}; + } else if (m_barrierEdge == Qt::TopEdge) { + std::stable_sort(m_currentZones.begin(), m_currentZones.end(), [](const QRect &lhs, const QRect &rhs) { + return lhs.y() < rhs.y(); + }); + const auto &zone = m_currentZones.front(); + // Same here with QRect::right + m_barrier = {zone.x(), zone.y(), zone.right(), zone.y()}; + } else { + std::stable_sort(m_currentZones.begin(), m_currentZones.end(), [](const QRect &lhs, const QRect &rhs) { + return lhs.y() + lhs.height() > rhs.y() + rhs.height(); + }); + const auto &zone = m_currentZones.front(); + m_barrier = {zone.x(), zone.y() + zone.height(), zone.right(), zone.y() + zone.height()}; + } + + const QString token = u"kdeconnect_shareinputdevices%1"_s.arg(QRandomGenerator::global()->generate()); + auto request = new OrgFreedesktopPortalRequestInterface(portalName(), requestPath(token), QDBusConnection::sessionBus(), this); + connect(request, &OrgFreedesktopPortalRequestInterface::Response, request, &QObject::deleteLater); + connect(request, &OrgFreedesktopPortalRequestInterface::Response, this, &InputCaptureSession::barriersSet); + auto call = m_inputCapturePortal->SetPointerBarriers( + QDBusObjectPath(m_session->path()), + {{u"handle_token"_s, token}}, + {{{u"barrier_id"_s, 1}, {u"position"_s, QVariant::fromValue(QList{m_barrier.x1(), m_barrier.y1(), m_barrier.x2(), m_barrier.y2()})}}}, + m_currentZoneSet); + connect(new QDBusPendingCallWatcher(call, this), &QDBusPendingCallWatcher::finished, request, [request](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + if (watcher->isError()) { + qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Error setting barriers" << watcher->error(); + request->deleteLater(); + } + }); +} + +void InputCaptureSession::barriersSet(uint response, const QVariantMap &results) +{ + if (response != 0) { + qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Couldn't set barriers"; + return; + } + auto failedBarriers = qdbus_cast>(results[u"failed_barriers"_s].value()); + if (!failedBarriers.empty()) { + qCInfo(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Failed barriers" << failedBarriers; + } + enable(); +} + +void InputCaptureSession::enable() +{ + auto call = m_inputCapturePortal->Enable(QDBusObjectPath(m_session->path()), {}); + connect(new QDBusPendingCallWatcher(call, this), &QDBusPendingCallWatcher::finished, this, [](QDBusPendingCallWatcher *watcher) { + watcher->deleteLater(); + if (watcher->isError()) { + qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Failed enabling input capture session" << watcher->error(); + } + }); +} + +void InputCaptureSession::setBarrierEdge(Qt::Edge edge) +{ + if (edge != m_barrierEdge) { + m_barrierEdge = edge; + if (!m_currentZones.empty()) { + setUpBarrier(); + } + } +} + +void InputCaptureSession::release(const QPointF &position) +{ + qDebug() << "releasing with" << position; + m_inputCapturePortal->Release(QDBusObjectPath(m_session->path()), {{u"cursor_position"_s, position}}); +} + +void InputCaptureSession::sessionClosed() +{ + qCCritical(KDECONNECT_PLUGIN_SHAREINPUTDEVICES()) << "input capture session was closed"; + m_session.reset(); + m_eiNotifier.reset(); +} + +void InputCaptureSession::activated(const QDBusObjectPath &sessionHandle, const QVariantMap &options) +{ + if (!m_session || sessionHandle.path() != m_session->path()) { + return; + } + m_currentActivationId = options[u"activation_id"_s].toUInt(); + // uint barrier_id = options[u"barrier_id"].toUInt(); + auto cursorPosition = qdbus_cast(options[u"cursor_position"_s].value()); + Q_EMIT started(m_barrier, cursorPosition - m_barrier.p1()); + for (const auto &event : queuedEiEvents) { + handleEiEvent(event); + } + queuedEiEvents.clear(); +} + +void InputCaptureSession::deactivated(const QDBusObjectPath &sessionHandle, const QVariantMap &options) +{ + if (!m_session || sessionHandle.path() != m_session->path()) { + return; + } + auto deactivatedId = options[u"activation_id"_s].toUInt(); + Q_UNUSED(deactivatedId) +} + +void InputCaptureSession::disabled(const QDBusObjectPath &sessionHandle, const QVariantMap &options) +{ + if (!m_session || sessionHandle.path() != m_session->path()) { + return; + } + auto disabledId = options[u"activation_id"_s].toUInt(); + Q_UNUSED(disabledId) +} + +void InputCaptureSession::zonesChanged(const QDBusObjectPath &sessionHandle, const QVariantMap &options) +{ + if (!m_session || sessionHandle.path() != m_session->path()) { + return; + } + if (options[u"zone_set"_s].toUInt() >= m_currentZoneSet) { + getZones(); + } +} + +void InputCaptureSession::setupEi(int fd) +{ + m_ei = ei_new_receiver(nullptr); + ei_setup_backend_fd(m_ei, fd); + m_eiNotifier = std::make_unique(fd, QSocketNotifier::Read); + connect(m_eiNotifier.get(), &QSocketNotifier::activated, this, [this] { + ei_dispatch(m_ei); + while (auto event = ei_get_event(m_ei)) { + handleEiEvent(event); + } + }); +} + +void InputCaptureSession::handleEiEvent(ei_event *event) +{ + const auto type = ei_event_get_type(event); + constexpr std::array inputEvents = { + EI_EVENT_FRAME, + EI_EVENT_POINTER_MOTION, + EI_EVENT_POINTER_MOTION_ABSOLUTE, + EI_EVENT_BUTTON_BUTTON, + EI_EVENT_SCROLL_DELTA, + EI_EVENT_SCROLL_DISCRETE, + EI_EVENT_SCROLL_STOP, + EI_EVENT_SCROLL_CANCEL, + EI_EVENT_KEYBOARD_MODIFIERS, + EI_EVENT_KEYBOARD_KEY, + EI_EVENT_TOUCH_DOWN, + EI_EVENT_TOUCH_MOTION, + EI_EVENT_TOUCH_UP, + }; + if (m_currentEisSequence > m_currentActivationId && std::find(inputEvents.begin(), inputEvents.end(), type) != inputEvents.end()) { + // Wait until DBus activated signal to have correct start position + queuedEiEvents.push_back(event); + return; + } + + switch (type) { + case EI_EVENT_CONNECT: + qCDebug(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Connected to ei"; + break; + case EI_EVENT_DISCONNECT: + qCWarning(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Disconnected from ei"; + break; + case EI_EVENT_SEAT_ADDED: { + auto seat = ei_event_get_seat(event); + ei_seat_bind_capabilities(seat, EI_DEVICE_CAP_KEYBOARD, EI_DEVICE_CAP_POINTER, EI_DEVICE_CAP_BUTTON, EI_DEVICE_CAP_SCROLL, nullptr); + break; + } + case EI_EVENT_SEAT_REMOVED: + break; + case EI_EVENT_DEVICE_ADDED: { + auto device = ei_event_get_device(event); + if (ei_device_has_capability(device, EI_DEVICE_CAP_KEYBOARD)) { + auto keymap = ei_device_keyboard_get_keymap(device); + if (ei_keymap_get_type(keymap) != EI_KEYMAP_TYPE_XKB) { + break; + } + m_xkb.reset(new Xkb(ei_keymap_get_fd(keymap), ei_keymap_get_size(keymap))); + } + } break; + case EI_EVENT_DEVICE_REMOVED: + break; + case EI_EVENT_DEVICE_START_EMULATING: + m_currentEisSequence = ei_event_emulating_get_sequence(event); + break; + case EI_EVENT_DEVICE_STOP_EMULATING: + break; + case EI_EVENT_FRAME: + break; + case EI_EVENT_POINTER_MOTION: + if (m_currentEisSequence < m_currentActivationId) { + queuedEiEvents.push_back(event); + } + Q_EMIT mouseMove(ei_event_pointer_get_dx(event), ei_event_pointer_get_dy(event)); + break; + case EI_EVENT_POINTER_MOTION_ABSOLUTE: + break; + case EI_EVENT_BUTTON_BUTTON: + Q_EMIT mouseButton(ei_event_button_get_button(event), ei_event_button_get_is_press(event)); + break; + case EI_EVENT_SCROLL_DELTA: + Q_EMIT scrollDelta(ei_event_scroll_get_dx(event), ei_event_scroll_get_dy(event)); + break; + case EI_EVENT_SCROLL_DISCRETE: + Q_EMIT scrollDiscrete(ei_event_scroll_get_discrete_dx(event), ei_event_scroll_get_discrete_dy(event)); + break; + case EI_EVENT_SCROLL_STOP: + break; + case EI_EVENT_SCROLL_CANCEL: + break; + case EI_EVENT_KEYBOARD_MODIFIERS: + m_xkb->updateModifiers(ei_event_keyboard_get_xkb_mods_depressed(event), + ei_event_keyboard_get_xkb_mods_latched(event), + ei_event_keyboard_get_xkb_mods_locked(event), + ei_event_keyboard_get_xkb_group(event)); + break; + case EI_EVENT_KEYBOARD_KEY: { + auto xkbKey = ei_event_keyboard_get_key(event) + 8; + m_xkb->updateKey(xkbKey, ei_event_keyboard_get_key_is_press(event)); + // mousepad plugin does press/release in one, trigger on press like remotekeyboard + if (ei_event_keyboard_get_key_is_press(event)) { + xkb_keysym_t sym = xkb_state_key_get_one_sym(m_xkb->currentState(), xkbKey); + Qt::KeyboardModifiers modifiers = QXkbCommon::modifiers(m_xkb->currentState(), sym); + auto qtKey = static_cast(QXkbCommon::keysymToQtKey(sym, modifiers)); + const QString text = QXkbCommon::lookupStringNoKeysymTransformations(sym); + Q_EMIT key(qtKey, modifiers, text); + } + break; + } + case EI_EVENT_TOUCH_DOWN: + case EI_EVENT_TOUCH_MOTION: + case EI_EVENT_TOUCH_UP: + case EI_EVENT_DEVICE_PAUSED: + case EI_EVENT_DEVICE_RESUMED: + qCDebug(KDECONNECT_PLUGIN_SHAREINPUTDEVICES) << "Unexpected event of type" << ei_event_get_type(event); + break; + } + ei_event_unref(event); +} diff --git a/plugins/shareinputdevices/inputcapturesession.h b/plugins/shareinputdevices/inputcapturesession.h new file mode 100644 index 000000000..d1dc09e09 --- /dev/null +++ b/plugins/shareinputdevices/inputcapturesession.h @@ -0,0 +1,70 @@ +/** + * SPDX-FileCopyrightText: 2024 David Redondo + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include + +#include + +class OrgFreedesktopPortalInputCaptureInterface; +class QSocketNotifier; +class OrgFreedesktopPortalSessionInterface; +class Xkb; +struct ei; +struct ei_event; + +class InputCaptureSession : public QObject +{ + Q_OBJECT +public: + InputCaptureSession(QObject *parent); + ~InputCaptureSession(); + + void setBarrierEdge(Qt::Edge edge); + void release(const QPointF &position); +Q_SIGNALS: + void started(const QLine &barrier, const QPointF &delta); + void mouseMove(double dx, double dy); + void mouseButton(int button, bool pressed); + void scrollDelta(double dx, double dy); + void scrollDiscrete(int dx, int dy); + void key(Qt::Key key, Qt::KeyboardModifiers modifiers, const QString &text); + +private: + void getZones(); + void setUpBarrier(); + void enable(); + + void sessionCreated(uint response, const QVariantMap &options); + void zonesReceived(uint response, const QVariantMap &results); + void barriersSet(uint response, const QVariantMap &results); + + void sessionClosed(); + void disabled(const QDBusObjectPath &sessionHandle, const QVariantMap &options); + void activated(const QDBusObjectPath &sessionHandle, const QVariantMap &options); + void deactivated(const QDBusObjectPath &sessionHandle, const QVariantMap &options); + void zonesChanged(const QDBusObjectPath &sessionHandle, const QVariantMap &options); + + void setupEi(int fd); + void handleEiEvent(ei_event *event); + + Qt::Edge m_barrierEdge; + QLine m_barrier; + + uint m_currentZoneSet = 0; + QList m_currentZones; + uint m_currentActivationId = 0; + uint m_currentEisSequence = 0; + QList queuedEiEvents; + std::unique_ptr m_eiNotifier; + std::unique_ptr m_session; + std::unique_ptr m_xkb; + ei *m_ei = nullptr; + OrgFreedesktopPortalInputCaptureInterface *m_inputCapturePortal; +}; diff --git a/plugins/shareinputdevices/kdeconnect_shareinputdevices.json b/plugins/shareinputdevices/kdeconnect_shareinputdevices.json new file mode 100644 index 000000000..257007ee2 --- /dev/null +++ b/plugins/shareinputdevices/kdeconnect_shareinputdevices.json @@ -0,0 +1,23 @@ +{ + "KPlugin": { + "Authors": [ + { + "Name": "David Redondo", + "Email": "kde@david-redondo.de" + } + ], + "EnabledByDefault": false, + "Icon": "dialog-input-devices", + "License": "GPL", + "Name": "Share input devices", + "Description": "Share mouse and keyboard with the remote device" + }, + "X-KDE-ConfigModule": "kdeconnect/kcms/kdeconnect_shareinputdevices_config", + "X-KdeConnect-OutgoingPacketType": [ + "kdeconnect.mousepad.request", + "kdeconnect.shareinputdevices.request" + ], + "X-KdeConnect-SupportedPacketType": [ + "kdeconnect.shareinputdevices" + ] +} diff --git a/plugins/shareinputdevices/kdeconnect_shareinputdevices_config.qml b/plugins/shareinputdevices/kdeconnect_shareinputdevices_config.qml new file mode 100644 index 000000000..5338e8573 --- /dev/null +++ b/plugins/shareinputdevices/kdeconnect_shareinputdevices_config.qml @@ -0,0 +1,79 @@ +/** + * SPDX-FileCopyrightText: 2024 David Redondo + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +import QtQml.Models +import QtQuick +import QtQuick.Controls as QQC2 +import QtQuick.Layouts +import org.kde.kcmutils as KCMUtils +import org.kde.kdeconnect 1.0 +import org.kde.kitemmodels as KItemModels +import org.kde.kirigami as Kirigami + +Kirigami.FormLayout { + id: root + + property string device + readonly property string pluginId: "kdeconnect_shareinputdevices" + + KdeConnectPluginConfig { + id: config + deviceId: device + pluginName: pluginId + + onConfigChanged: edgeComboBox.currentIndex = edgeComboBox.indexOfValue(getInt("edge", Qt.LeftEdge)) + } + + RowLayout { + Kirigami.FormData.label: i18nc("@label:listbox", "Leave Screen at") + QQC2.ComboBox { + id: edgeComboBox + textRole: "text" + valueRole: "value" + model: [ + { + text: i18nc("@item:inlistbox top edge of screen", "Top"), + value: Qt.TopEdge + }, + { + text: i18nc("@item:inlistbox left edge of screen", "Left"), + value: Qt.LeftEdge + }, + { + text: i18nc("@item:inlistbox right edge of screen", "Right"), + value: Qt.RightEdge + }, + { + text: i18nc("@item:inlistbox top edge of screen", "Bottom"), + value: Qt.BottomEdge + } + ] + Component.onCompleted: currentIndex = edgeComboBox.indexOfValue(config.getInt("edge", Qt.LeftEdge)) + } + KCMUtils.ContextualHelpButton { + icon.name: "data-warning" + toolTipText: i18nc("@info:tooltip", "Another device already reserved this edge. This may lead to unexpected results when both are connected at the same time.") + visible: configInstantiator.children.some((deviceConfig) => deviceConfig.getInt("edge", Qt.LeftEdge) == edgeComboBox.currentValue) + Instantiator { + id: configInstantiator + readonly property var children: Array.from({length: configInstantiator.count}, (value, index) => configInstantiator.objectAt(index)) + delegate: KdeConnectPluginConfig { + deviceId: model.deviceId + pluginName: pluginId + + } + model: KItemModels.KSortFilterProxyModel { + sourceModel: DevicesModel { + } + filterRowCallback: function(source_row, source_parent) { + const device = sourceModel.getDevice(source_row) + return device.id() !== root.device && device.isPluginEnabled(pluginId); + } + } + } + } + } +} diff --git a/plugins/shareinputdevices/shareinputdevices_config.cpp b/plugins/shareinputdevices/shareinputdevices_config.cpp new file mode 100644 index 000000000..8f1d80c1c --- /dev/null +++ b/plugins/shareinputdevices/shareinputdevices_config.cpp @@ -0,0 +1,80 @@ +/** + * SPDX-FileCopyrightText: 2024 David Redondo + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#include "shareinputdevices_config.h" + +#include + +#include + +using namespace Qt::StringLiterals; + +K_PLUGIN_CLASS(ShareInputDevicesConfig) + +constexpr auto defaultEdge = Qt::LeftEdge; +constexpr auto defaultIndex = 1; + +ShareInputDevicesConfig::ShareInputDevicesConfig(QObject *parent, const KPluginMetaData &data, const QVariantList &args) + : KdeConnectPluginKcm(parent, data, args) + , m_daemon(new DaemonDbusInterface) +{ + m_ui.setupUi(widget()); + m_ui.edgeContextualWarning->setVisible(false); + m_ui.edgeComboBox->setItemData(0, Qt::TopEdge); + m_ui.edgeComboBox->setItemData(1, Qt::LeftEdge); + m_ui.edgeComboBox->setItemData(2, Qt::RightEdge); + m_ui.edgeComboBox->setItemData(3, Qt::BottomEdge); + connect(m_ui.edgeComboBox, &QComboBox::currentIndexChanged, this, &ShareInputDevicesConfig::updateState); +} + +void ShareInputDevicesConfig::checkEdge() +{ + m_ui.edgeContextualWarning->setVisible(false); + const QStringList devices = m_daemon->devices(); + for (const auto &device : devices) { + if (device == deviceId()) { + continue; + } + if (!DeviceDbusInterface(device).isPluginEnabled(config()->pluginName())) { + continue; + } + if (KdeConnectPluginConfig(device, config()->pluginName()).getInt(u"edge"_s, defaultEdge) == m_ui.edgeComboBox->currentData()) { + m_ui.edgeContextualWarning->setVisible(true); + } + } +} + +void ShareInputDevicesConfig::updateState() +{ + const int current = m_ui.edgeComboBox->currentData().toInt(); + unmanagedWidgetChangeState(current != config()->getInt(u"edge"_s, defaultEdge)); + unmanagedWidgetDefaultState(current == defaultEdge); + checkEdge(); +} + +void ShareInputDevicesConfig::defaults() +{ + KCModule::defaults(); + m_ui.edgeComboBox->setCurrentIndex(defaultIndex); + updateState(); +} + +void ShareInputDevicesConfig::load() +{ + KCModule::load(); + const int index = m_ui.edgeComboBox->findData((config()->getInt(u"edge"_s, defaultEdge))); + m_ui.edgeComboBox->setCurrentIndex(index != -1 ? index : defaultIndex); + updateState(); +} + +void ShareInputDevicesConfig::save() +{ + KCModule::save(); + config()->set(u"edge"_s, m_ui.edgeComboBox->currentData()); + updateState(); +} + +#include "shareinputdevices_config.moc" diff --git a/plugins/shareinputdevices/shareinputdevices_config.h b/plugins/shareinputdevices/shareinputdevices_config.h new file mode 100644 index 000000000..cbd7f6eec --- /dev/null +++ b/plugins/shareinputdevices/shareinputdevices_config.h @@ -0,0 +1,29 @@ +/** + * SPDX-FileCopyrightText: 2024 David Redondo + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include "kcmplugin/kdeconnectpluginkcm.h" +#include "ui_shareinputdevices_config.h" + +class DaemonDbusInterface; + +class ShareInputDevicesConfig : public KdeConnectPluginKcm +{ + Q_OBJECT +public: + ShareInputDevicesConfig(QObject *parent, const KPluginMetaData &data, const QVariantList &args); + + void save() override; + void load() override; + void defaults() override; + +private: + void updateState(); + void checkEdge(); + Ui::ShareInputDevicesConfig m_ui; + DaemonDbusInterface *m_daemon; +}; diff --git a/plugins/shareinputdevices/shareinputdevices_config.ui b/plugins/shareinputdevices/shareinputdevices_config.ui new file mode 100644 index 000000000..3ce440ac0 --- /dev/null +++ b/plugins/shareinputdevices/shareinputdevices_config.ui @@ -0,0 +1,72 @@ + + + ShareInputDevicesConfig + + + + 0 + 0 + 298 + 66 + + + + Form + + + + + + Leave Screen at + + + + + + + 1 + + + + Top + + + + + Left + + + + + Right + + + + + Bottom + + + + + + + + + + + Another device already reserved this edge. This may lead to unexpected results when both are connected at the same time. + + + + + + + + KContextualHelpButton + QToolButton +
kcontextualhelpbutton.h
+
+
+ + +
diff --git a/plugins/shareinputdevices/shareinputdevicesplugin.cpp b/plugins/shareinputdevices/shareinputdevicesplugin.cpp new file mode 100644 index 000000000..c06b2e645 --- /dev/null +++ b/plugins/shareinputdevices/shareinputdevicesplugin.cpp @@ -0,0 +1,145 @@ +/** + * SPDX-FileCopyrightText: 2024 David Redondo + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ +#include "shareinputdevicesplugin.h" + +#include "inputcapturesession.h" +#include "plugin_shareinputdevices_debug.h" + +#include +#include + +#include +#include + +#include +#include + +#include + +using namespace Qt::StringLiterals; + +#define PACKET_TYPE_MOUSEPAD_REQUEST u"kdeconnect.mousepad.request"_s +#define PACKET_TYPE_SHAREINPUTDEVICES u"kdeconnect.shareinputdevices"_s +#define PACKET_TYPE_SHAREINPUTDEVICES_REQUEST u"kdeconnect.shareinputdevices.request"_s + +QMap specialKeysMap = { + // 0, // Invalid + {Qt::Key_Backspace, 1}, + {Qt::Key_Tab, 2}, + // XK_Linefeed, // 3 + {Qt::Key_Left, 4}, + {Qt::Key_Up, 5}, + {Qt::Key_Right, 6}, + {Qt::Key_Down, 7}, + {Qt::Key_PageUp, 8}, + {Qt::Key_PageDown, 9}, + {Qt::Key_Home, 10}, + {Qt::Key_End, 11}, + {Qt::Key_Return, 12}, + {Qt::Key_Enter, 12}, + {Qt::Key_Delete, 13}, + {Qt::Key_Escape, 14}, + {Qt::Key_SysReq, 15}, + {Qt::Key_ScrollLock, 16}, + // 0, // 17 + // 0, // 18 + // 0, // 19 + // 0, // 20 + {Qt::Key_F1, 21}, + {Qt::Key_F2, 22}, + {Qt::Key_F3, 23}, + {Qt::Key_F4, 24}, + {Qt::Key_F5, 25}, + {Qt::Key_F6, 26}, + {Qt::Key_F7, 27}, + {Qt::Key_F8, 28}, + {Qt::Key_F9, 29}, + {Qt::Key_F10, 30}, + {Qt::Key_F11, 31}, + {Qt::Key_F12, 32}, +}; + +K_PLUGIN_CLASS_WITH_JSON(ShareInputDevicesPlugin, "kdeconnect_shareinputdevices.json") + +ShareInputDevicesPlugin::ShareInputDevicesPlugin(QObject *parent, const QVariantList &args) + : KdeConnectPlugin(parent, args) + , m_inputCaptureSession(new InputCaptureSession(this)) +{ + connect(m_inputCaptureSession, &InputCaptureSession::started, this, [this](const QLine &barrier, const QPointF &delta) { + m_activatedBarrier = barrier; + NetworkPacket packet(PACKET_TYPE_SHAREINPUTDEVICES_REQUEST, {{u"startEdge"_s, configuredEdge()}, {u"deltax"_s, delta.x()}, {u"deltay"_s, delta.y()}}); + sendPacket(packet); + }); + connect(m_inputCaptureSession, &InputCaptureSession::mouseMove, this, [this](double x, double y) { + NetworkPacket packet(PACKET_TYPE_MOUSEPAD_REQUEST, {{u"dx"_s, x}, {u"dy"_s, y}}); + sendPacket(packet); + }); + connect(m_inputCaptureSession, &InputCaptureSession::mouseButton, this, [this](int button, bool pressed) { + NetworkPacket packet(PACKET_TYPE_MOUSEPAD_REQUEST); + // mousepad only supports separate press and release for left click currently, so send event on release even though not entirely correct + if (button == BTN_LEFT) { + packet.set(pressed ? u"singlehold"_s : u"singlerelease"_s, true); + } else if (button == BTN_RIGHT && pressed) { + packet.set(u"rightclick"_s, true); + } else if (button == BTN_MIDDLE) { + packet.set(u"middleclick"_s, true); + } + sendPacket(packet); + }); + connect(m_inputCaptureSession, &InputCaptureSession::scrollDelta, this, [this](double x, double y) { + qDebug() << "scrollDelta" << x << y; + // scrollDirection in kdeconnect is inverted compared to what we get here + NetworkPacket packet(PACKET_TYPE_MOUSEPAD_REQUEST, {{u"scroll"_s, true}, {u"dx"_s, x}, {u"dy"_s, y}}); + sendPacket(packet); + }); + connect(m_inputCaptureSession, &InputCaptureSession::scrollDiscrete, this, [this](int x, int y) { + qDebug() << "scolldiscrete" << x << y; + constexpr auto anglePer120Step = 15 / 120.0; + NetworkPacket packet(PACKET_TYPE_MOUSEPAD_REQUEST, {{u"scroll"_s, true}, {u"dx"_s, x * anglePer120Step}, {u"dy"_s, -y * anglePer120Step}}); + sendPacket(packet); + }); + connect(m_inputCaptureSession, &InputCaptureSession::key, this, [this](Qt::Key key, Qt::KeyboardModifiers modifiers, const QString &text) { + qDebug() << "sending key" << text << modifiers << specialKeysMap.value(key); + NetworkPacket packet(PACKET_TYPE_MOUSEPAD_REQUEST, + { + {u"key"_s, text}, + {u"specialKey"_s, specialKeysMap.value(key)}, + {u"shift"_s, static_cast(modifiers & Qt::ShiftModifier)}, + {u"ctrl"_s, static_cast(modifiers & Qt::ControlModifier)}, + {u"alt"_s, static_cast(modifiers & Qt::AltModifier)}, + {u"super"_s, static_cast(modifiers & Qt::MetaModifier)}, + }); + sendPacket(packet); + }); + + m_inputCaptureSession->setBarrierEdge(configuredEdge()); + connect(config(), &KdeConnectPluginConfig::configChanged, this, [this] { + m_inputCaptureSession->setBarrierEdge(configuredEdge()); + }); +} + +Qt::Edge ShareInputDevicesPlugin::configuredEdge() const +{ + return static_cast(config()->getInt(u"edge"_s, Qt::LeftEdge)); +} + +void ShareInputDevicesPlugin::receivePacket(const NetworkPacket &np) +{ + if (np.type() == PACKET_TYPE_SHAREINPUTDEVICES) { + if (np.has(u"releaseDeltax"_s)) { + const QPointF releaseDelta{np.get(u"releaseDeltax"_s), np.get(u"releaseDeltay"_s)}; + const QPointF releasePosition = m_activatedBarrier.p1() + releaseDelta; + m_inputCaptureSession->release(releasePosition); + } + } +} + +QString ShareInputDevicesPlugin::dbusPath() const +{ + return QLatin1String("/modules/kdeconnect/devices/%1/shareinputdevices").arg(device()->id()); +} + +#include "shareinputdevicesplugin.moc" diff --git a/plugins/shareinputdevices/shareinputdevicesplugin.h b/plugins/shareinputdevices/shareinputdevicesplugin.h new file mode 100644 index 000000000..45868e836 --- /dev/null +++ b/plugins/shareinputdevices/shareinputdevicesplugin.h @@ -0,0 +1,31 @@ +/** + * SPDX-FileCopyrightText: 2024 David Redondo + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include + +#include + +class InputCaptureSession; +class QScreen; + +class ShareInputDevicesPlugin : public KdeConnectPlugin +{ + Q_OBJECT +public: + ShareInputDevicesPlugin(QObject *parent, const QVariantList &args); + + void receivePacket(const NetworkPacket &np) override; + QString dbusPath() const override; + +private: + Qt::Edge configuredEdge() const; + InputCaptureSession *m_inputCaptureSession; + QLine m_activatedBarrier; +}; diff --git a/plugins/shareinputdevicesremote/CMakeLists.txt b/plugins/shareinputdevicesremote/CMakeLists.txt new file mode 100644 index 000000000..8c1599206 --- /dev/null +++ b/plugins/shareinputdevicesremote/CMakeLists.txt @@ -0,0 +1,7 @@ +kdeconnect_add_plugin(kdeconnect_shareinputdevicesremote SOURCES shareinputdevicesremoteplugin.cpp) + +target_link_libraries(kdeconnect_shareinputdevicesremote + kdeconnectcore + Qt::Gui + KF6::I18n +) diff --git a/plugins/shareinputdevicesremote/kdeconnect_shareinputdevicesremote.json b/plugins/shareinputdevicesremote/kdeconnect_shareinputdevicesremote.json new file mode 100644 index 000000000..e47bc4e0c --- /dev/null +++ b/plugins/shareinputdevicesremote/kdeconnect_shareinputdevicesremote.json @@ -0,0 +1,22 @@ +{ + "KPlugin": { + "Authors": [ + { + "Name": "David Redondo", + "Email": "kde@david-redondo.de" + } + ], + "EnabledByDefault": true, + "Icon": "dialog-input-devices", + "License": "GPL", + "Hidden": true + + }, + "X-KdeConnect-OutgoingPacketType": [ + "kdeconnect.shareinputdevices" + ], + "X-KdeConnect-SupportedPacketType": [ + "kdeconnect.mousepad.request", + "kdeconnect.shareinputdevices.request" + ] +} diff --git a/plugins/shareinputdevicesremote/shareinputdevicesremoteplugin.cpp b/plugins/shareinputdevicesremote/shareinputdevicesremoteplugin.cpp new file mode 100644 index 000000000..253b1ab27 --- /dev/null +++ b/plugins/shareinputdevicesremote/shareinputdevicesremoteplugin.cpp @@ -0,0 +1,108 @@ +/** + * SPDX-FileCopyrightText: 2024 David Redondo + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ +#include "shareinputdevicesremoteplugin.h" + +#include +#include + +#include + +#include +#include +#include +#include + +using namespace Qt::StringLiterals; + +#define PACKET_TYPE_MOUSEPAD_REQUEST u"kdeconnect.mousepad.request"_s +#define PACKET_TYPE_SHAREINPUTDEVICES u"kdeconnect.shareinputdevices"_s +#define PACKET_TYPE_SHAREINPUTDEVICES_REQUEST u"kdeconnect.shareinputdevices.request"_s + +K_PLUGIN_CLASS_WITH_JSON(ShareInputDevicesRemotePlugin, "kdeconnect_shareinputdevicesremote.json") + +ShareInputDevicesRemotePlugin::ShareInputDevicesRemotePlugin(QObject *parent, const QVariantList &args) + : KdeConnectPlugin(parent, args) +{ +} + +void ShareInputDevicesRemotePlugin::receivePacket(const NetworkPacket &np) +{ + if (np.type() == PACKET_TYPE_SHAREINPUTDEVICES_REQUEST) { + if (np.has(u"startEdge"_s)) { + const Qt::Edge startEdge = np.get(u"startEdge"_s); + const QPointF delta = {np.get(u"deltax"_s), np.get(u"deltay"_s)}; + auto screens = QGuiApplication::screens(); + auto mousePadPlugin = device()->plugin(u"kdeconnect_mousepad"_s); + if (screens.empty() || !mousePadPlugin) { + NetworkPacket packet(PACKET_TYPE_SHAREINPUTDEVICES, {{u"releaseDelta"_s, QPointF(0, 0)}}); + sendPacket(packet); + return; + } + // This is opposite to the sorting in inputcaptureession because here the opposite edge is wanted. + // For example if on the source side a left barrier was crossed, this is one is entered from the right. + if (startEdge == Qt::LeftEdge) { + std::stable_sort(screens.begin(), screens.end(), [](QScreen *lhs, QScreen *rhs) { + return lhs->geometry().x() + lhs->geometry().width() > rhs->geometry().x() + rhs->geometry().width(); + }); + m_enterEdge = Qt::RightEdge; + m_currentPosition = screens.front()->geometry().topRight() + delta; + } else if (startEdge == Qt::RightEdge) { + std::stable_sort(screens.begin(), screens.end(), [](QScreen *lhs, QScreen *rhs) { + return lhs->geometry().x() < rhs->geometry().x(); + }); + m_enterEdge = Qt::LeftEdge; + m_currentPosition = screens.front()->geometry().topLeft() + delta; + } else if (startEdge == Qt::TopEdge) { + std::stable_sort(screens.begin(), screens.end(), [](QScreen *lhs, QScreen *rhs) { + return lhs->geometry().y() + lhs->geometry().height() > rhs->geometry().y() + rhs->geometry().height(); + }); + m_enterEdge = Qt::BottomEdge; + m_currentPosition = screens.front()->geometry().bottomLeft() + delta; + } else { + std::stable_sort(screens.begin(), screens.end(), [](QScreen *lhs, QScreen *rhs) { + return lhs->geometry().y() < rhs->geometry().y(); + }); + m_enterEdge = Qt::TopEdge; + m_currentPosition = screens.front()->geometry().topLeft() + delta; + } + m_enterScreen = screens.front(); + // Send a packet from this local device to the mousepad plugin because the position could only + // be calculated here + NetworkPacket packet(PACKET_TYPE_MOUSEPAD_REQUEST, {{u"x"_s, m_currentPosition.x()}, {u"y"_s, m_currentPosition.y()}}); + mousePadPlugin->receivePacket(packet); + } + } + + if (np.type() == PACKET_TYPE_MOUSEPAD_REQUEST) { + if (np.has(u"dx"_s) && m_enterScreen) { + m_currentPosition += QPointF(np.get(u"dx"_s), np.get(u"dy"_s)); + auto sendRelease = [this](const QPointF &delta) { + NetworkPacket packet(PACKET_TYPE_SHAREINPUTDEVICES, {{u"releaseDeltax"_s, delta.x()}, {u"releaseDeltay"_s, delta.y()}}); + sendPacket(packet); + }; + if (m_enterEdge == Qt::LeftEdge && m_currentPosition.x() < m_enterScreen->geometry().x()) { + const QPointF delta = m_currentPosition - m_enterScreen->geometry().topLeft(); + sendRelease(delta); + } else if (m_enterEdge == Qt::RightEdge && m_currentPosition.x() > m_enterScreen->geometry().x() + m_enterScreen->geometry().width()) { + const QPointF delta = m_currentPosition - m_enterScreen->geometry().topRight(); + sendRelease(delta); + } else if (m_enterEdge == Qt::TopEdge && m_currentPosition.y() < m_enterScreen->geometry().y()) { + const QPointF delta = m_currentPosition - m_enterScreen->geometry().topLeft(); + sendRelease(delta); + } else if (m_enterEdge == Qt::BottomEdge && m_currentPosition.y() > m_enterScreen->geometry().y() + m_enterScreen->geometry().height()) { + const QPointF delta = m_currentPosition - m_enterScreen->geometry().bottomLeft(); + sendRelease(delta); + } + } + } +} + +QString ShareInputDevicesRemotePlugin::dbusPath() const +{ + return QLatin1String("/modules/kdeconnect/devices/%1/shareinputdevicesremote").arg(device()->id()); +} + +#include "shareinputdevicesremoteplugin.moc" diff --git a/plugins/shareinputdevicesremote/shareinputdevicesremoteplugin.h b/plugins/shareinputdevicesremote/shareinputdevicesremoteplugin.h new file mode 100644 index 000000000..1a4f0634c --- /dev/null +++ b/plugins/shareinputdevicesremote/shareinputdevicesremoteplugin.h @@ -0,0 +1,32 @@ +/** + * SPDX-FileCopyrightText: 2024 David Redondo + * + * SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL + */ + +#pragma once + +#include +#include +#include + +#include + +class InputCaptureSession; +class QScreen; + +class ShareInputDevicesRemotePlugin : public KdeConnectPlugin +{ + Q_OBJECT +public: + ShareInputDevicesRemotePlugin(QObject *parent, const QVariantList &args); + + void receivePacket(const NetworkPacket &np) override; + QString dbusPath() const override; + +private: + QScreen *m_enterScreen; + Qt::Edge m_enterEdge; + QPointF m_enterPosition; + QPointF m_currentPosition; +};