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
+
+
+
+
+
+
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;
+};