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