diff --git a/src/plugins/eis/CMakeLists.txt b/src/plugins/eis/CMakeLists.txt index 557925f241..7e55d886d2 100644 --- a/src/plugins/eis/CMakeLists.txt +++ b/src/plugins/eis/CMakeLists.txt @@ -10,12 +10,22 @@ ecm_qt_declare_logging_category(eis DEFAULT_SEVERITY Debug ) +ecm_qt_declare_logging_category(eis + HEADER inputcapture_logging.h + IDENTIFIER KWIN_INPUTCAPTURE + CATEGORY_NAME kwin_inputcapture + DEFAULT_SEVERITY Debug +) + target_sources(eis PRIVATE main.cpp eisdevice.cpp eisbackend.cpp eiscontext.cpp eisplugin.cpp + eisinputcapture.cpp + eisinputcapturemanager.cpp + eisinputcapturefilter.cpp ) target_link_libraries(eis PRIVATE kwin Libeis::Libeis XKB::XKB) diff --git a/src/plugins/eis/eisinputcapture.cpp b/src/plugins/eis/eisinputcapture.cpp new file mode 100644 index 0000000000..be69a693d6 --- /dev/null +++ b/src/plugins/eis/eisinputcapture.cpp @@ -0,0 +1,326 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "eisinputcapture.h" + +#include "eisinputcapturemanager.h" +#include "inputcapture_logging.h" + +#include "core/output.h" +#include "cursor.h" +#include "input.h" +#include "input_event.h" +#include "input_event_spy.h" +#include "workspace.h" + +#include +#include + +namespace KWin +{ + +static void eis_log_handler(eis *eis, eis_log_priority priority, const char *message, eis_log_context *context) +{ + switch (priority) { + case EIS_LOG_PRIORITY_DEBUG: + qCDebug(KWIN_INPUTCAPTURE) << "Libeis:" << message; + break; + case EIS_LOG_PRIORITY_INFO: + qCInfo(KWIN_INPUTCAPTURE) << "Libeis:" << message; + break; + case EIS_LOG_PRIORITY_WARNING: + qCWarning(KWIN_INPUTCAPTURE) << "Libeis:" << message; + break; + case EIS_LOG_PRIORITY_ERROR: + qCritical(KWIN_INPUTCAPTURE) << "Libeis:" << message; + break; + } +} + +static eis_device *createDevice(eis_seat *seat, const QByteArray &name) +{ + auto device = eis_seat_new_device(seat); + + auto client = eis_seat_get_client(seat); + const char *clientName = eis_client_get_name(client); + const QByteArray deviceName = clientName + (' ' + name); + eis_device_configure_name(device, deviceName); + return device; +} + +static eis_device *createPointer(eis_seat *seat) +{ + auto device = createDevice(seat, "capture pointer"); + eis_device_configure_capability(device, EIS_DEVICE_CAP_POINTER); + eis_device_configure_capability(device, EIS_DEVICE_CAP_SCROLL); + eis_device_configure_capability(device, EIS_DEVICE_CAP_BUTTON); + eis_device_add(device); + eis_device_resume(device); + return device; +} + +static eis_device *createAbsoluteDevice(eis_seat *seat) +{ + auto device = createDevice(seat, "capture absolute device"); + auto eisDevice = device; + eis_device_configure_capability(eisDevice, EIS_DEVICE_CAP_POINTER_ABSOLUTE); + eis_device_configure_capability(eisDevice, EIS_DEVICE_CAP_SCROLL); + eis_device_configure_capability(eisDevice, EIS_DEVICE_CAP_BUTTON); + eis_device_configure_capability(eisDevice, EIS_DEVICE_CAP_TOUCH); + + const auto outputs = workspace()->outputs(); + for (const auto output : outputs) { + auto region = eis_device_new_region(eisDevice); + const QRect outputGeometry = output->geometry(); + eis_region_set_offset(region, outputGeometry.x(), outputGeometry.y()); + eis_region_set_size(region, outputGeometry.width(), outputGeometry.height()); + eis_region_set_physical_scale(region, output->scale()); + eis_region_add(region); + eis_region_unref(region); + }; + eis_device_add(device); + eis_device_resume(device); + return device; +} + +static eis_device *createKeyboard(eis_seat *seat, const RamFile &keymap) +{ + auto device = createDevice(seat, "capture keyboard"); + eis_device_configure_capability(device, EIS_DEVICE_CAP_KEYBOARD); + + if (keymap.isValid()) { + auto eisKeymap = eis_device_new_keymap(device, EIS_KEYMAP_TYPE_XKB, keymap.fd(), keymap.size()); + eis_keymap_add(eisKeymap); + eis_keymap_unref(eisKeymap); + } + + eis_device_add(device); + eis_device_resume(device); + return device; +} + +EisInputCapture::EisInputCapture(EisInputCaptureManager *manager, const QString &dbusService, QFlags allowedCapabilities) + : dbusService(dbusService) + , m_manager(manager) + , m_allowedCapabilities(allowedCapabilities) + , m_eis(eis_new(this)) + , m_socketNotifier(eis_get_fd(m_eis), QSocketNotifier::Read) +{ + eis_setup_backend_fd(m_eis); + eis_log_set_priority(m_eis, EIS_LOG_PRIORITY_DEBUG); + eis_log_set_handler(m_eis, eis_log_handler); + connect(&m_socketNotifier, &QSocketNotifier::activated, this, &EisInputCapture::handleEvents); + static int counter = 0; + m_dbusPath = QStringLiteral("/org/kde/KWin/EIS/InputCapture/%1").arg(++counter); + QDBusConnection::sessionBus().registerObject(m_dbusPath, "org.kde.KWin.EIS.InputCapture", this, QDBusConnection::ExportAllInvokables | QDBusConnection::ExportAllSignals); +} + +EisInputCapture::~EisInputCapture() +{ + if (m_absoluteDevice) { + eis_device_unref(m_absoluteDevice); + } + if (m_pointer) { + eis_device_unref(m_pointer); + } + if (m_keyboard) { + eis_device_unref(m_keyboard); + } + if (m_seat) { + eis_seat_unref(m_seat); + } + if (m_client) { + eis_client_disconnect(m_client); + } + eis_unref(m_eis); +} + +QString EisInputCapture::dbusPath() const +{ + return m_dbusPath; +} + +QList EisInputCapture::barriers() const +{ + return m_barriers; +} + +eis_device *EisInputCapture::pointer() const +{ + return m_pointer; +} + +eis_device *EisInputCapture::keyboard() const +{ + return m_keyboard; +} + +eis_device *EisInputCapture::absoluteDevice() const +{ + return m_absoluteDevice; +} + +void EisInputCapture::activate(const QPointF &position) +{ + Q_EMIT activated(++m_activationId, position); + if (m_pointer) { + eis_device_start_emulating(m_pointer, m_activationId); + } + if (m_keyboard) { + eis_device_start_emulating(m_keyboard, m_activationId); + } + if (m_absoluteDevice) { + eis_device_start_emulating(m_absoluteDevice, m_activationId); + } +} + +void EisInputCapture::deactivate() +{ + Q_EMIT deactivated(m_activationId); + if (m_pointer) { + eis_device_stop_emulating(m_pointer); + } + if (m_keyboard) { + eis_device_stop_emulating(m_keyboard); + } + if (m_absoluteDevice) { + eis_device_stop_emulating(m_absoluteDevice); + } +} + +void EisInputCapture::enable(const QList> &barriers) +{ + m_barriers.clear(); + m_barriers.reserve(barriers.size()); + for (const auto &[p1, p2] : barriers) { + if (p1.x() == p2.x()) { + m_barriers.push_back({.orientation = Qt::Vertical, .position = p1.x(), .start = p1.y(), .end = p2.y()}); + } else if (p1.y() == p2.y()) { + m_barriers.push_back({.orientation = Qt::Horizontal, .position = p1.y(), .start = p1.x(), .end = p2.x()}); + } + } +} + +void EisInputCapture::disable() +{ + if (m_manager->activeCapture() == this) { + deactivate(); + } + m_barriers.clear(); + Q_EMIT disabled(); +} + +void EisInputCapture::release(const QPointF &cursorPosition, bool applyPosition) +{ + if (m_manager->activeCapture() != this) { + return; + } + if (applyPosition) { + Cursors::self()->mouse()->setPos(cursorPosition); + } + deactivate(); +} + +QDBusUnixFileDescriptor EisInputCapture::connectToEIS() +{ + return QDBusUnixFileDescriptor(eis_backend_fd_add_client(m_eis)); +} + +void EisInputCapture::handleEvents() +{ + eis_dispatch(m_eis); + while (eis_event *const event = eis_get_event(m_eis)) { + switch (eis_event_get_type(event)) { + case EIS_EVENT_CLIENT_CONNECT: { + auto client = eis_event_get_client(event); + const char *clientName = eis_client_get_name(client); + if (eis_client_is_sender(client)) { + qCWarning(KWIN_INPUTCAPTURE) << "disconnecting receiving client" << clientName; + eis_client_disconnect(client); + break; + } + if (m_client) { + qCWarning(KWIN_INPUTCAPTURE) << "unexpected client connection" << clientName; + eis_client_disconnect(client); + break; + } + eis_client_connect(client); + + m_client = client; + m_seat = eis_client_new_seat(client, QByteArrayLiteral(" input capture seat").prepend(clientName)); + constexpr std::array allCapabilities{EIS_DEVICE_CAP_POINTER, EIS_DEVICE_CAP_POINTER_ABSOLUTE, EIS_DEVICE_CAP_KEYBOARD, EIS_DEVICE_CAP_TOUCH, EIS_DEVICE_CAP_SCROLL, EIS_DEVICE_CAP_BUTTON}; + for (auto capability : allCapabilities) { + if (m_allowedCapabilities & capability) { + eis_seat_configure_capability(m_seat, capability); + } + } + + eis_seat_add(m_seat); + qCDebug(KWIN_INPUTCAPTURE) << "New eis client" << clientName; + break; + } + case EIS_EVENT_CLIENT_DISCONNECT: { + auto client = eis_event_get_client(event); + if (client != m_client) { + break; + } + qCDebug(KWIN_INPUTCAPTURE) << "Client disconnected" << eis_client_get_name(client); + eis_seat_unref(std::exchange(m_seat, nullptr)); + eis_client_unref(std::exchange(m_client, nullptr)); + break; + } + case EIS_EVENT_SEAT_BIND: { + auto seat = eis_event_get_seat(event); + qCDebug(KWIN_INPUTCAPTURE) << "Client" << eis_client_get_name(eis_event_get_client(event)) << "bound to seat" << eis_seat_get_name(seat); + if (eis_event_seat_has_capability(event, EIS_DEVICE_CAP_POINTER_ABSOLUTE) || eis_event_seat_has_capability(event, EIS_DEVICE_CAP_TOUCH)) { + if (!m_absoluteDevice) { + m_absoluteDevice = createAbsoluteDevice(seat); + } + } else if (m_absoluteDevice) { + eis_device_remove(m_absoluteDevice); + eis_device_unref(std::exchange(m_absoluteDevice, nullptr)); + } + if (eis_event_seat_has_capability(event, EIS_DEVICE_CAP_POINTER)) { + if (!m_pointer) { + m_pointer = createPointer(seat); + } + } else if (m_pointer) { + eis_device_remove(m_pointer); + eis_device_unref(std::exchange(m_pointer, nullptr)); + } + if (eis_event_seat_has_capability(event, EIS_DEVICE_CAP_KEYBOARD)) { + if (!m_keyboard) { + m_keyboard = createKeyboard(seat, m_manager->keyMap()); + } + } else if (m_keyboard) { + eis_device_remove(m_keyboard); + eis_device_unref(std::exchange(m_keyboard, nullptr)); + } + break; + } + case EIS_EVENT_DEVICE_CLOSED: { + auto device = eis_event_get_device(event); + qCDebug(KWIN_INPUTCAPTURE) << "Device" << eis_device_get_name(device) << "closed by client"; + if (device == m_pointer) { + m_pointer = nullptr; + } else if (device == m_keyboard) { + m_keyboard = nullptr; + } else if (device == m_absoluteDevice) { + m_absoluteDevice = nullptr; + } + eis_device_remove(device); + eis_device_unref(device); + break; + } + default: + qCDebug(KWIN_INPUTCAPTURE) << "Unexpected event" << eis_event_get_type(event); + break; + } + eis_event_unref(event); + } +} + +} diff --git a/src/plugins/eis/eisinputcapture.h b/src/plugins/eis/eisinputcapture.h new file mode 100644 index 0000000000..7fdd1cbb14 --- /dev/null +++ b/src/plugins/eis/eisinputcapture.h @@ -0,0 +1,72 @@ +/* + 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 + +#include "eisinputcapturemanager.h" +#include "input_event_spy.h" + +#include + +#include + +namespace KWin +{ +class BarrierSpy; + +class EisInputCapture : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.EIS.InputCapture") +public: + EisInputCapture(EisInputCaptureManager *manager, const QString &dbusService, QFlags allowedCapabilities); + ~EisInputCapture(); + + const QString dbusService; + + QList barriers() const; + QString dbusPath() const; + + eis_device *pointer() const; + eis_device *keyboard() const; + eis_device *absoluteDevice() const; + + void activate(const QPointF &position); + + Q_INVOKABLE QDBusUnixFileDescriptor connectToEIS(); + Q_INVOKABLE void enable(const QList> &barriers); + Q_INVOKABLE void disable(); + Q_INVOKABLE void release(const QPointF &cursorPosition, bool applyPosition); +Q_SIGNALS: + void disabled(); + void activated(int activationId, const QPointF &cursorPosition); + void deactivated(int activationId); + +private: + void handleEvents(); + void createDevice(); + void deactivate(); + + EisInputCaptureManager *m_manager; + QList m_barriers; + QString m_dbusPath; + QFlags m_allowedCapabilities; + eis *m_eis; + QSocketNotifier m_socketNotifier; + eis_client *m_client = nullptr; + eis_seat *m_seat = nullptr; + eis_device *m_pointer = nullptr; + eis_device *m_keyboard = nullptr; + eis_device *m_absoluteDevice = nullptr; + int m_activationId = 0; +}; + +} diff --git a/src/plugins/eis/eisinputcapturefilter.cpp b/src/plugins/eis/eisinputcapturefilter.cpp new file mode 100644 index 0000000000..6c76797579 --- /dev/null +++ b/src/plugins/eis/eisinputcapturefilter.cpp @@ -0,0 +1,233 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "eisinputcapturefilter.h" + +#include "eisinputcapture.h" +#include "eisinputcapturemanager.h" + +#include "input_event.h" + +#include + +namespace KWin +{ +EisInputCaptureFilter::EisInputCaptureFilter(EisInputCaptureManager *manager) + : m_manager(manager) +{ +} + +void EisInputCaptureFilter::clearTouches() +{ + for (const auto touch : m_touches) { + eis_touch_unref(touch); + } + m_touches.clear(); +} + +bool EisInputCaptureFilter::pointerEvent(MouseEvent *event, quint32 nativeButton) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto pointer = m_manager->activeCapture()->pointer()) { + if (event->type() == QMouseEvent::MouseMove) { + eis_device_pointer_motion(pointer, event->delta().x(), event->delta().y()); + } else if (event->type() == QMouseEvent::MouseButtonPress || event->type() == QMouseEvent::MouseButtonRelease) { + eis_device_button_button(pointer, nativeButton, event->type() == QMouseEvent::MouseButtonPress); + } + } + return true; +} + +bool EisInputCaptureFilter::pointerFrame() +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto pointer = m_manager->activeCapture()->pointer()) { + eis_device_frame(pointer, std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); + } + return true; +} + +bool EisInputCaptureFilter::wheelEvent(WheelEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto pointer = m_manager->activeCapture()->pointer()) { + if (event->delta()) { + if (event->deltaV120()) { + if (event->orientation() == Qt::Horizontal) { + eis_device_scroll_discrete(pointer, event->deltaV120(), 0); + } else { + eis_device_scroll_discrete(pointer, 0, event->deltaV120()); + } + } else { + if (event->orientation() == Qt::Horizontal) { + eis_device_scroll_delta(pointer, event->delta(), 0); + } else { + eis_device_scroll_delta(pointer, 0, event->delta()); + } + } + } else { + if (event->orientation() == Qt::Horizontal) { + eis_device_scroll_stop(pointer, true, false); + } else { + eis_device_scroll_stop(pointer, false, true); + } + } + } + return true; +} + +bool EisInputCaptureFilter::keyEvent(KeyEvent *event) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto keyboard = m_manager->activeCapture()->keyboard()) { + eis_device_keyboard_key(keyboard, event->nativeScanCode(), event->type() == QKeyEvent::KeyPress); + eis_device_frame(keyboard, std::chrono::duration_cast(event->timestamp()).count()); + } + return true; +} + +bool EisInputCaptureFilter::touchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto abs = m_manager->activeCapture()->absoluteDevice()) { + auto touch = eis_device_touch_new(abs); + m_touches.insert(id, touch); + eis_touch_down(touch, pos.x(), pos.y()); + } + return true; +} + +bool EisInputCaptureFilter::touchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (auto touch = m_touches.value(id)) { + eis_touch_motion(touch, pos.x(), pos.y()); + } + return true; +} + +bool EisInputCaptureFilter::touchUp(qint32 id, std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + if (auto touch = m_touches.take(id)) { + eis_touch_up(touch); + eis_touch_unref(touch); + } + return false; +} +bool EisInputCaptureFilter::touchCancel() +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::touchFrame() +{ + if (!m_manager->activeCapture()) { + return false; + } + if (const auto abs = m_manager->activeCapture()->absoluteDevice()) { + eis_device_frame(abs, std::chrono::duration_cast(std::chrono::steady_clock::now().time_since_epoch()).count()); + } + return true; +} + +bool EisInputCaptureFilter::pinchGestureBegin(int fingerCount, std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::pinchGestureUpdate(qreal scale, qreal angleDelta, const QPointF &delta, std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} + +bool EisInputCaptureFilter::pinchGestureEnd(std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::pinchGestureCancelled(std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} + +bool EisInputCaptureFilter::swipeGestureBegin(int fingerCount, std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::swipeGestureUpdate(const QPointF &delta, std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::swipeGestureEnd(std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::swipeGestureCancelled(std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} + +bool EisInputCaptureFilter::holdGestureBegin(int fingerCount, std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::holdGestureEnd(std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +bool EisInputCaptureFilter::holdGestureCancelled(std::chrono::microseconds time) +{ + if (!m_manager->activeCapture()) { + return false; + } + return true; +} +} diff --git a/src/plugins/eis/eisinputcapturefilter.h b/src/plugins/eis/eisinputcapturefilter.h new file mode 100644 index 0000000000..def9f17d14 --- /dev/null +++ b/src/plugins/eis/eisinputcapturefilter.h @@ -0,0 +1,56 @@ +/* + 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 "input.h" + +extern "C" { +struct eis_touch; +}; + +namespace KWin +{ +class EisInputCaptureManager; + +class EisInputCaptureFilter : public InputEventFilter +{ +public: + EisInputCaptureFilter(EisInputCaptureManager *m_manager); + + void clearTouches(); + + bool pointerEvent(MouseEvent *event, quint32 nativeButton) override; + bool pointerFrame() override; + bool wheelEvent(WheelEvent *event) override; + + bool keyEvent(KeyEvent *event) override; + + bool touchDown(qint32 id, const QPointF &pos, std::chrono::microseconds time) override; + bool touchMotion(qint32 id, const QPointF &pos, std::chrono::microseconds time) override; + bool touchUp(qint32 id, std::chrono::microseconds time) override; + bool touchCancel() override; + bool touchFrame() override; + + bool pinchGestureBegin(int fingerCount, std::chrono::microseconds time) override; + bool pinchGestureUpdate(qreal scale, qreal angleDelta, const QPointF &delta, std::chrono::microseconds time) override; + bool pinchGestureEnd(std::chrono::microseconds time) override; + bool pinchGestureCancelled(std::chrono::microseconds time) override; + + bool swipeGestureBegin(int fingerCount, std::chrono::microseconds time) override; + bool swipeGestureUpdate(const QPointF &delta, std::chrono::microseconds time) override; + bool swipeGestureEnd(std::chrono::microseconds time) override; + bool swipeGestureCancelled(std::chrono::microseconds time) override; + + bool holdGestureBegin(int fingerCount, std::chrono::microseconds time) override; + bool holdGestureEnd(std::chrono::microseconds time) override; + bool holdGestureCancelled(std::chrono::microseconds time) override; + +private: + EisInputCaptureManager *m_manager; + QHash m_touches; +}; +} diff --git a/src/plugins/eis/eisinputcapturemanager.cpp b/src/plugins/eis/eisinputcapturemanager.cpp new file mode 100644 index 0000000000..20cca4144a --- /dev/null +++ b/src/plugins/eis/eisinputcapturemanager.cpp @@ -0,0 +1,195 @@ +/* + SPDX-FileCopyrightText: 2024 David Redondo + + SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL +*/ + +#include "eisinputcapturemanager.h" + +#include "eisinputcapture.h" +#include "eisinputcapturefilter.h" +#include "inputcapture_logging.h" + +#include "core/output.h" +#include "cursor.h" +#include "input_event.h" +#include "input_event_spy.h" +#include "keyboard_input.h" +#include "keyboard_layout.h" +#include "workspace.h" +#include "xkb.h" + +#include +#include +#include +#include + +#include + +namespace KWin +{ + +class BarrierSpy : public InputEventSpy +{ +public: + BarrierSpy(EisInputCaptureManager *manager) + : manager(manager) + { + } + void pointerEvent(KWin::MouseEvent *event) override + { + if (manager->activeCapture()) { + return; + } + for (const auto &capture : manager->m_inputCaptures) { + for (const auto &barrier : capture->barriers()) { + // Detect the user trying to move out of the workArea and across the barrier: + // Both current and previous positions are on the barrier but there was an orthogonal delta + if (barrier.hitTest(event->pos()) && barrier.hitTest(previousPos) && ((barrier.orientation == Qt::Vertical && event->delta().x() != 0) || (barrier.orientation == Qt::Horizontal && event->delta().y() != 0))) { + qCDebug(KWIN_INPUTCAPTURE) << "Activating input capture, crossing" + << "barrier(" << barrier.orientation << barrier.position << "[" << barrier.start << "," << barrier.end << "])" + << "at" << event->pos() << "with" << event->delta(); + manager->barrierHit(capture.get(), event->pos() + event->delta()); + break; + } + } + } + previousPos = event->pos(); + } + void keyEvent(KWin::KeyEvent *event) override + { + if (manager->activeCapture() && event->key() == Qt::Key_Escape && event->modifiers() == (Qt::ControlModifier | Qt::ShiftModifier)) { + manager->activeCapture()->disable(); + } + } + +private: + QKeyCombination currentCombination; + EisInputCaptureManager *manager; + QPoint previousPos; +}; + +bool EisInputCaptureBarrier::hitTest(const QPoint &point) const +{ + if (orientation == Qt::Vertical) { + return point.x() == position && start <= point.y() && point.y() <= end; + } + return point.y() == position && start <= point.x() && point.x() <= end; +} + +EisInputCaptureManager::EisInputCaptureManager() + : m_serviceWatcher(new QDBusServiceWatcher(this)) + , m_barrierSpy(std::make_unique(this)) + , m_inputFilter(std::make_unique(this)) +{ + qDBusRegisterMetaType>(); + qDBusRegisterMetaType>>(); + + const auto keymap = input()->keyboard()->xkb()->keymapContents(); + m_keymapFile = RamFile("input capture keymap", keymap.data(), keymap.size(), RamFile::Flag::SealWrite); + connect(input()->keyboard()->keyboardLayout(), &KeyboardLayout::layoutChanged, this, [this] { + const auto keymap = input()->keyboard()->xkb()->keymapContents(); + m_keymapFile = RamFile("input capture keymap", keymap.data(), keymap.size(), RamFile::Flag::SealWrite); + }); + + m_serviceWatcher->setConnection(QDBusConnection::sessionBus()); + m_serviceWatcher->setWatchMode(QDBusServiceWatcher::WatchForUnregistration); + connect(m_serviceWatcher, &QDBusServiceWatcher::serviceUnregistered, this, [this](const QString &service) { + if (m_activeCapture && m_activeCapture->dbusService == service) { + deactivate(); + } + std::erase_if(m_inputCaptures, [&service](const std::unique_ptr &capture) { + return capture->dbusService == service; + }); + m_serviceWatcher->removeWatchedService(service); + }); + + QDBusConnection::sessionBus().registerObject("/org/kde/KWin/EIS/InputCapture", "org.kde.KWin.EIS.InputCaptureManager", this, QDBusConnection::ExportAllInvokables | QDBusConnection::ExportAllSignals); +} + +EisInputCaptureManager::~EisInputCaptureManager() +{ + if (input()) { + input()->uninstallInputEventFilter(m_inputFilter.get()); + input()->uninstallInputEventSpy(m_barrierSpy.get()); + } +} + +const RamFile &EisInputCaptureManager::keyMap() const +{ + return m_keymapFile; +} + +void EisInputCaptureManager::removeInputCapture(const QDBusObjectPath &capture) +{ + auto it = std::ranges::find(m_inputCaptures, capture.path(), &EisInputCapture::dbusPath); + if (it == std::ranges::end(m_inputCaptures)) { + return; + } + if (m_activeCapture == it->get()) { + deactivate(); + } + m_inputCaptures.erase(it); + if (m_inputCaptures.empty()) { + input()->uninstallInputEventSpy(m_barrierSpy.get()); + } +} + +QDBusObjectPath EisInputCaptureManager::addInputCapture(int capabilities) +{ + constexpr int keyboardPortal = 1; + constexpr int pointerPortal = 2; + constexpr int touchPortal = 4; + QFlags eisCapabilities; + if (capabilities & keyboardPortal) { + eisCapabilities |= EIS_DEVICE_CAP_KEYBOARD; + } + if (capabilities & pointerPortal) { + eisCapabilities |= EIS_DEVICE_CAP_POINTER; + eisCapabilities |= EIS_DEVICE_CAP_POINTER_ABSOLUTE; + eisCapabilities |= EIS_DEVICE_CAP_BUTTON; + eisCapabilities |= EIS_DEVICE_CAP_SCROLL; + } + if (capabilities & touchPortal) { + eisCapabilities |= EIS_DEVICE_CAP_TOUCH; + } + + const QString dbusService = message().service(); + m_serviceWatcher->addWatchedService(dbusService); + + if (m_inputCaptures.empty()) { + input()->installInputEventSpy(m_barrierSpy.get()); + } + auto &capture = m_inputCaptures.emplace_back(std::make_unique(this, dbusService, eisCapabilities)); + connect(capture.get(), &EisInputCapture::deactivated, this, [this] { + deactivate(); + }); + return QDBusObjectPath(capture->dbusPath()); +} + +void EisInputCaptureManager::barrierHit(KWin::EisInputCapture *capture, const QPointF &position) +{ + if (m_activeCapture) { + return; + } + m_activeCapture = capture; + capture->activate(position); + input()->prependInputEventFilter(m_inputFilter.get()); + // Even though the input events are filtered out the cursor is updated on screen which looks weird + Cursors::self()->hideCursor(); +} + +void EisInputCaptureManager::deactivate() +{ + m_activeCapture = nullptr; + m_inputFilter->clearTouches(); + input()->uninstallInputEventFilter(m_inputFilter.get()); + Cursors::self()->showCursor(); +} + +EisInputCapture *EisInputCaptureManager::activeCapture() +{ + return m_activeCapture; +} + +} diff --git a/src/plugins/eis/eisinputcapturemanager.h b/src/plugins/eis/eisinputcapturemanager.h new file mode 100644 index 0000000000..9d7b919de3 --- /dev/null +++ b/src/plugins/eis/eisinputcapturemanager.h @@ -0,0 +1,60 @@ +/* + 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 "utils/ramfile.h" + +#include +#include +#include +#include + +#include + +class QDBusServiceWatcher; + +namespace KWin +{ +class BarrierSpy; +class EisInputCapture; +class EisInputCaptureFilter; + +struct EisInputCaptureBarrier +{ + const Qt::Orientation orientation; + const int position; + const int start; + const int end; + bool hitTest(const QPoint &point) const; +}; + +class EisInputCaptureManager : public QObject, public QDBusContext +{ + Q_OBJECT +public: + EisInputCaptureManager(); + ~EisInputCaptureManager(); + + Q_INVOKABLE QDBusObjectPath addInputCapture(int capabilities); + Q_INVOKABLE void removeInputCapture(const QDBusObjectPath &capture); + + const RamFile &keyMap() const; + + void barrierHit(EisInputCapture *capture, const QPointF &position); + EisInputCapture *activeCapture(); + void deactivate(); + +private: + RamFile m_keymapFile; + QDBusServiceWatcher *m_serviceWatcher; + std::unique_ptr m_barrierSpy; + std::unique_ptr m_inputFilter; + std::vector> m_inputCaptures; + EisInputCapture *m_activeCapture = nullptr; + friend class BarrierSpy; +}; +} diff --git a/src/plugins/eis/eisplugin.cpp b/src/plugins/eis/eisplugin.cpp index 1de09a735e..d2e16f4731 100644 --- a/src/plugins/eis/eisplugin.cpp +++ b/src/plugins/eis/eisplugin.cpp @@ -7,11 +7,13 @@ #include "eisplugin.h" #include "eisbackend.h" +#include "eisinputcapturemanager.h" #include "input.h" EisPlugin::EisPlugin() : Plugin() + , m_inputCapture(std::make_unique()) { KWin::input()->addInputBackend(std::make_unique()); } diff --git a/src/plugins/eis/eisplugin.h b/src/plugins/eis/eisplugin.h index 56d58a0234..b1b1ef936e 100644 --- a/src/plugins/eis/eisplugin.h +++ b/src/plugins/eis/eisplugin.h @@ -8,10 +8,18 @@ #include "plugin.h" +namespace KWin +{ +class EisInputCaptureManager; +}; + class EisPlugin : public KWin::Plugin { Q_OBJECT public: EisPlugin(); ~EisPlugin() override; + +private: + std::unique_ptr m_inputCapture; };