/* * Deskflow -- mouse and keyboard sharing utility * SPDX-FileCopyrightText: (C) 2025 Deskflow Developers * SPDX-FileCopyrightText: (C) 2024 Symless Ltd. * SPDX-FileCopyrightText: (C) 2022 Red Hat, Inc. * SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception */ #include "platform/PortalInputCapture.h" #include "base/DirectionTypes.h" #include "base/Event.h" #include "base/Log.h" #include "base/TMethodJob.h" #include // for EIS fd hack, remove #include // for EIS fd hack, remove namespace deskflow { PortalInputCapture::PortalInputCapture(EiScreen *screen, IEventQueue *events) : m_screen{screen}, m_events{events}, m_portal{xdp_portal_new()} { m_glibMainLoop = g_main_loop_new(nullptr, true); auto tMethodJob = new TMethodJob(this, &PortalInputCapture::glibThread); m_glibThread = new Thread(tMethodJob); auto captureCallback = [](gpointer data) { return static_cast(data)->initSession(); }; g_idle_add(captureCallback, this); } PortalInputCapture::~PortalInputCapture() { if (g_main_loop_is_running(m_glibMainLoop)) g_main_loop_quit(m_glibMainLoop); if (m_glibThread) { m_glibThread->cancel(); m_glibThread->wait(); m_glibThread = nullptr; g_main_loop_unref(m_glibMainLoop); m_glibMainLoop = nullptr; } if (m_session) { using enum Signal; XdpSession *parentSession = xdp_input_capture_session_get_session(m_session); g_signal_handler_disconnect(G_OBJECT(parentSession), m_signals.at(SessionClosed)); g_signal_handler_disconnect(m_session, m_signals.at(Disabled)); g_signal_handler_disconnect(m_session, m_signals.at(Activated)); g_signal_handler_disconnect(m_session, m_signals.at(Deactivated)); g_signal_handler_disconnect(m_session, m_signals.at(ZonesChanged)); g_object_unref(m_session); } for (auto b : m_barriers) { g_object_unref(b); } m_barriers.clear(); g_object_unref(m_portal); } gboolean PortalInputCapture::timeoutHandler() const { return true; // keep re-triggering } void PortalInputCapture::handleSessionClosed(XdpSession *session) { LOG_ERR("portal input capture session was closed, exiting"); g_main_loop_quit(m_glibMainLoop); m_events->addEvent(Event(EventTypes::Quit)); g_signal_handler_disconnect(session, m_signals.at(Signal::SessionClosed)); m_signals.at(Signal::SessionClosed) = 0; } void PortalInputCapture::handleInitSession(GObject *object, GAsyncResult *res) { LOG_DEBUG("portal input capture session initialized"); g_autoptr(GError) error = nullptr; auto session = xdp_portal_create_input_capture_session_finish(XDP_PORTAL(object), res, &error); if (!session) { LOG_ERR("failed to initialize input capture session, quitting: %s", error->message); g_main_loop_quit(m_glibMainLoop); m_events->addEvent(Event(EventTypes::Quit)); return; } m_session = session; auto fd = xdp_input_capture_session_connect_to_eis(session, &error); if (fd < 0) { LOG_ERR("failed to connect to eis: %s", error->message); g_main_loop_quit(m_glibMainLoop); m_events->addEvent(Event(EventTypes::Quit)); return; } // Socket ownership is transferred to the EiScreen m_events->addEvent(Event(EventTypes::EIConnected, m_screen->getEventTarget(), EiScreen::EiConnectInfo::alloc(fd))); using enum Signal; XdpSession *parentSession = xdp_input_capture_session_get_session(session); m_signals.at(Disabled) = g_signal_connect(G_OBJECT(session), "disabled", G_CALLBACK(disabled), this); m_signals.at(Activated) = g_signal_connect(G_OBJECT(m_session), "activated", G_CALLBACK(activated), this); m_signals.at(Deactivated) = g_signal_connect(G_OBJECT(m_session), "deactivated", G_CALLBACK(deactivated), this); m_signals.at(ZonesChanged) = g_signal_connect(G_OBJECT(m_session), "zones-changed", G_CALLBACK(zonesChanged), this); m_signals.at(SessionClosed) = g_signal_connect(G_OBJECT(parentSession), "closed", G_CALLBACK(sessionClosed), this); handleZonesChanged(m_session, nullptr); } void PortalInputCapture::handleSetPointerBarriers(const GObject *, GAsyncResult *res) { g_autoptr(GError) error = nullptr; auto failed_list = xdp_input_capture_session_set_pointer_barriers_finish(m_session, res, &error); if (failed_list) { auto it = failed_list; while (it) { guint id; g_object_get(it->data, "id", &id, nullptr); for (auto elem = m_barriers.begin(); elem != m_barriers.end(); elem++) { if (*elem == it->data) { int x1; int x2; int y1; int y2; g_object_get(G_OBJECT(*elem), "x1", &x1, "x2", &x2, "y1", &y1, "y2", &y2, nullptr); LOG_WARN("failed to apply barrier %d (%d/%d-%d/%d)", id, x1, y1, x2, y2); g_object_unref(*elem); m_barriers.erase(elem); break; } } it = it->next; } } g_list_free_full(failed_list, g_object_unref); enable(); } gboolean PortalInputCapture::initSession() { LOG_DEBUG("setting up input capture session"); xdp_portal_create_input_capture_session( m_portal, nullptr, // parent static_cast(XDP_INPUT_CAPABILITY_KEYBOARD | XDP_INPUT_CAPABILITY_POINTER), nullptr, // cancellable [](GObject *obj, GAsyncResult *res, gpointer data) { static_cast(data)->handleInitSession(obj, res); }, this ); return false; } void PortalInputCapture::enable() { if (!m_enabled) { LOG_DEBUG("enabling the portal input capture session"); xdp_input_capture_session_enable(m_session); m_enabled = true; } } void PortalInputCapture::disable() { if (m_enabled) { LOG_DEBUG("disabling the portal input capture session"); xdp_input_capture_session_disable(m_session); m_enabled = false; } } void PortalInputCapture::release() { LOG_DEBUG("releasing input capture session, id=%d", m_activationId); xdp_input_capture_session_release(m_session, m_activationId); m_isActive = false; } void PortalInputCapture::release(double x, double y) { LOG_DEBUG("releasing input capture session, id=%d x=%.1f y=%.1f", m_activationId, x, y); xdp_input_capture_session_release_at(m_session, m_activationId, x, y); m_isActive = false; } void PortalInputCapture::handleDisabled(const XdpInputCaptureSession *, const GVariant *) { LOG_DEBUG("portal cb disabled"); if (!m_enabled) return; // Nothing to do m_enabled = false; m_isActive = false; // FIXME: need some better heuristics here of when we want to enable again // But we don't know *why* we got disabled (and it's doubtfull we ever // will), so we just assume that the zones will change or something and we // can re-enable again // ... very soon g_timeout_add( 1000, [](gpointer data) -> gboolean { static_cast(data)->enable(); return false; }, this ); } void PortalInputCapture::handleActivated( const XdpInputCaptureSession *, const std::uint32_t activationId, GVariant *options ) { LOG_DEBUG("portal cb activated, id=%d", activationId); if (options) { gdouble x; gdouble y; if (g_variant_lookup(options, "cursor_position", "(dd)", &x, &y)) { m_screen->warpCursor((int)x, (int)y); } else { LOG_WARN("failed to get cursor position"); } } else { LOG_WARN("activation has no options"); } m_activationId = activationId; m_isActive = true; } void PortalInputCapture::handleDeactivated( const XdpInputCaptureSession *, const std::uint32_t activationId, const GVariant * ) { LOG_DEBUG("cb deactivated, id=%i", activationId); m_isActive = false; } void PortalInputCapture::handleZonesChanged(XdpInputCaptureSession *session, const GVariant *) { for (auto b : m_barriers) g_object_unref(b); m_barriers.clear(); const auto activeSides = m_screen->activeSides(); using enum DirectionMask; // May not correctly handle different sized screens auto zones = xdp_input_capture_session_get_zones(session); while (zones != nullptr) { guint w; guint h; gint x; gint y; g_object_get(zones->data, "width", &w, "height", &h, "x", &x, "y", &y, nullptr); LOG_DEBUG("input capture zone, %dx%d@%d,%d", w, h, x, y); int x1; int x2; int y1; int y2; auto id = 0; if (activeSides & static_cast(LeftMask)) { id++; x1 = x; y1 = y; x2 = x; y2 = y + h - 1; LOG_DEBUG("barrier (left) %zd at %d,%d-%d,%d", id, x1, y1, x2, y2); m_barriers.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER(g_object_new( XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1, "x2", x2, "y2", y2, nullptr ))); } if (activeSides & static_cast(RightMask)) { id++; x1 = x + w; y1 = y; x2 = x + w; y2 = y + h - 1; LOG_DEBUG("barrier (right) %zd at %d,%d-%d,%d", id, x1, y1, x2, y2); m_barriers.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER(g_object_new( XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1, "x2", x2, "y2", y2, nullptr ))); } if (activeSides & static_cast(TopMask)) { id++; x1 = x; y1 = y; x2 = x + w - 1; y2 = y; LOG_DEBUG("barrier (top) %zd at %d,%d-%d,%d", id, x1, y1, x2, y2); m_barriers.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER(g_object_new( XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1, "x2", x2, "y2", y2, nullptr ))); } if (activeSides & static_cast(BottomMask)) { id++; x1 = x; y1 = y + h; x2 = x + w - 1; y2 = y + h; LOG_DEBUG("barrier (bottom) %zd at %d,%d-%d,%d", id, x1, y1, x2, y2); m_barriers.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER(g_object_new( XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1, "x2", x2, "y2", y2, nullptr ))); } zones = zones->next; } GList *list = nullptr; for (auto const &b : m_barriers) { list = g_list_append(list, b); } xdp_input_capture_session_set_pointer_barriers( m_session, list, nullptr, // cancellable [](GObject *obj, GAsyncResult *res, gpointer data) { static_cast(data)->handleSetPointerBarriers(obj, res); }, this ); } void PortalInputCapture::glibThread(void *) { auto context = g_main_loop_get_context(m_glibMainLoop); LOG_DEBUG("glib thread running"); while (g_main_loop_is_running(m_glibMainLoop)) { Thread::testCancel(); g_main_context_iteration(context, true); } LOG_DEBUG("shutting down glib thread"); } } // namespace deskflow