deskflow/src/lib/platform/PortalInputCapture.cpp
piterpunk 0d21f54cb2 fix(wayland): Corrects the cursor coordinates mapping at primary desktop
Remapping the cursor coordinates to the portal edge size makes the mouse
position being relative to the edge size, in a multi-monitor environment
this edge can be in only one of the monitors and the cursor coordinates
got wrong.

This changes uses the full height and the full width of the combined
monitors to set the mouse cursor position, cropped by the portal edge
size.
2026-08-04 09:42:22 -04:00

878 lines
27 KiB
C++

/*
* Deskflow -- mouse and keyboard sharing utility
* SPDX-FileCopyrightText: (C) 2024 - 2026 Deskflow Developers
* SPDX-FileCopyrightText: (C) 2024, 2026 Synergy App Ltd
* SPDX-FileCopyrightText: (C) 2022, 2026 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 "deskflow/ClipboardTypes.h"
#include "platform/EiClipboard.h"
#ifdef HAVE_LIBPORTAL_CLIPBOARD
#include "platform/PortalClipboard.h"
#endif
#ifdef HAVE_LIBPORTAL_INPUTCAPTURE_RESTORE
#include "common/Settings.h"
#endif
#include <algorithm>
#include <cmath>
#include <limits>
#include <poll.h>
#include <sys/socket.h> // for EIS fd hack, remove
#include <sys/un.h> // for EIS fd hack, remove
#include <QBuffer>
#include <QByteArray>
#include <QByteArrayList>
#include <QDataStream>
#include <QFile>
#include <QImage>
#include <QtEndian>
namespace deskflow {
const char *PortalInputCapture::barrierSideName(BarrierSide side)
{
using enum BarrierSide;
switch (side) {
case Left:
return "left";
case Right:
return "right";
case Top:
return "top";
case Bottom:
return "bottom";
}
return "unknown";
}
int PortalInputCapture::scaleCoordinateBetweenRanges(
double value, int sourceMin, int sourceMax, int destinationMin, int destinationMax
)
{
if (sourceMax <= sourceMin || destinationMax <= destinationMin) {
return destinationMin;
}
const auto clamped = std::clamp(value, static_cast<double>(sourceMin), static_cast<double>(sourceMax));
const auto fraction = (clamped - sourceMin) / (sourceMax - sourceMin);
const auto mapped = static_cast<int>(std::lround(destinationMin + fraction * (destinationMax - destinationMin)));
return std::clamp(mapped, destinationMin, destinationMax);
}
bool PortalInputCapture::getPortalBounds(Bounds &bounds) const
{
if (m_barrierInfo.empty()) {
return false;
}
bounds.left = std::numeric_limits<gint>::max();
bounds.top = std::numeric_limits<gint>::max();
bounds.right = std::numeric_limits<gint>::min();
bounds.bottom = std::numeric_limits<gint>::min();
for (const auto &info : m_barrierInfo) {
bounds.left = std::min(bounds.left, info.x);
bounds.top = std::min(bounds.top, info.y);
bounds.right = std::max(bounds.right, info.x + static_cast<gint>(info.width) - 1);
bounds.bottom = std::max(bounds.bottom, info.y + static_cast<gint>(info.height) - 1);
}
return true;
}
bool PortalInputCapture::getClosestReleaseBarrier(
double x, double y, int screenLeft, int screenTop, int screenRight, int screenBottom, const Bounds &portalBounds,
BarrierInfo &barrier
) const
{
const auto activeSides = m_screen->activeSides();
using enum DirectionMask;
auto side = BarrierSide::Left;
auto sideDistance = std::numeric_limits<double>::max();
const auto considerSide = [&side, &sideDistance](BarrierSide candidateSide, double candidateDistance) {
if (candidateDistance < sideDistance) {
side = candidateSide;
sideDistance = candidateDistance;
}
};
if (activeSides & static_cast<int>(LeftMask)) {
considerSide(BarrierSide::Left, std::abs(x - screenLeft));
}
if (activeSides & static_cast<int>(RightMask)) {
considerSide(BarrierSide::Right, std::abs(x - screenRight));
}
if (activeSides & static_cast<int>(TopMask)) {
considerSide(BarrierSide::Top, std::abs(y - screenTop));
}
if (activeSides & static_cast<int>(BottomMask)) {
considerSide(BarrierSide::Bottom, std::abs(y - screenBottom));
}
if (sideDistance == std::numeric_limits<double>::max()) {
return false;
}
const auto portalX = scaleCoordinateBetweenRanges(x, screenLeft, screenRight, portalBounds.left, portalBounds.right);
const auto portalY = scaleCoordinateBetweenRanges(y, screenTop, screenBottom, portalBounds.top, portalBounds.bottom);
auto bestDistance = std::numeric_limits<int>::max();
for (const auto &info : m_barrierInfo) {
if (info.side != side) {
continue;
}
const auto left = info.x;
const auto top = info.y;
const auto right = info.x + static_cast<gint>(info.width) - 1;
const auto bottom = info.y + static_cast<gint>(info.height) - 1;
auto distance = 0;
switch (side) {
case BarrierSide::Left:
case BarrierSide::Right:
if (portalY < top) {
distance = top - portalY;
} else if (portalY > bottom) {
distance = portalY - bottom;
}
break;
case BarrierSide::Top:
case BarrierSide::Bottom:
if (portalX < left) {
distance = left - portalX;
} else if (portalX > right) {
distance = portalX - right;
}
break;
}
if (distance < bestDistance) {
bestDistance = distance;
barrier = info;
}
}
return bestDistance != std::numeric_limits<int>::max();
}
PortalInputCapture::PortalInputCapture(EiScreen *screen, IEventQueue *events)
: m_screen{screen},
m_events{events},
m_portalVersion(0),
m_portal{xdp_portal_new()}
{
// Create clipboard for primary clipboard ID
m_clipboard = new EiClipboard(kClipboardClipboard);
m_glibMainLoop = g_main_loop_new(nullptr, true);
auto tMethodJob = new TMethodJob<PortalInputCapture>(this, &PortalInputCapture::glibThread);
m_glibThread = new Thread(tMethodJob);
auto captureCallback = [](gpointer data) { return static_cast<PortalInputCapture *>(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);
if (m_signals.at(SessionClosed) != 0)
g_signal_handler_disconnect(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));
#ifdef HAVE_LIBPORTAL_CLIPBOARD
g_signal_handler_disconnect(parentSession, m_signals.at(SelectionTransfer));
#endif
g_object_unref(m_session);
}
for (auto b : m_barriers) {
g_object_unref(b);
}
m_barriers.clear();
g_object_unref(m_portal);
delete m_clipboard;
}
EiClipboard *PortalInputCapture::getClipboard(ClipboardID id) const
{
// Currently only supporting primary clipboard
if (id == kClipboardClipboard) {
return m_clipboard;
}
return nullptr;
}
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::claimClipboardOwnership([[maybe_unused]] XdpSession *session) const
{
#ifdef HAVE_LIBPORTAL_CLIPBOARD
PortalClipboard::claimOwnership(m_clipboard, session);
#endif
}
void PortalInputCapture::readClipboardSelection(XdpSession *session) const
{
#ifdef HAVE_LIBPORTAL_CLIPBOARD
const qint64 maxBytes = static_cast<qint64>(m_screen->maximumClipboardSize()) * 1024;
LOG_DEBUG("clipboard read cap: %lld bytes", static_cast<long long>(maxBytes));
const char **mimeTypes = xdp_session_get_selection_mime_types(session);
if (!mimeTypes) {
LOG_DEBUG("clipboard has no mime types available to read");
return;
}
if (PortalClipboard::readSelectionIntoCache(m_clipboard, session, mimeTypes, maxBytes))
m_screen->sendClipboardEvent(EventTypes::ClipboardGrabbed, kClipboardClipboard);
#else
(void)session;
#endif
}
void PortalInputCapture::handleSelectionTransfer(XdpSession *session, const char *mimeType, uint32_t serial) const
{
#ifdef HAVE_LIBPORTAL_CLIPBOARD
if (m_isActive) {
LOG_DEBUG("skipping clipboard selection transfer, clipboard is active");
return;
}
PortalClipboard::serveSelectionTransfer(m_clipboard, session, mimeType, serial);
#else
(void)session;
(void)mimeType;
(void)serial;
#endif
}
void PortalInputCapture::setupSession(XdpInputCaptureSession *session)
{
g_autoptr(GError) error = nullptr;
XdpSession *parentSession = xdp_input_capture_session_get_session(session);
#ifdef HAVE_LIBPORTAL_CLIPBOARD
if (!xdp_session_is_clipboard_enabled(parentSession) && m_portalVersion > 1) {
// Restored sessions can pre-date clipboard support, leaving the channel
// disabled even though we requested it. Drop the saved token and recreate
// the session from scratch so the user gets a fresh permission dialog.
LOG_WARN("clipboard not enabled on session, discarding restore token to force a fresh session");
#ifdef HAVE_LIBPORTAL_INPUTCAPTURE_RESTORE
Settings::setValue(Settings::Server::XdpRestoreToken, QString());
#endif
g_object_unref(m_session);
m_session = nullptr;
g_idle_add([](gpointer data) { return static_cast<PortalInputCapture *>(data)->initSession(); }, this);
return;
}
#endif
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;
m_signals.at(Disabled) = g_signal_connect(G_OBJECT(session), "disabled", G_CALLBACK(disabled), this);
m_signals.at(Activated) = g_signal_connect(G_OBJECT(session), "activated", G_CALLBACK(activated), this);
m_signals.at(Deactivated) = g_signal_connect(G_OBJECT(session), "deactivated", G_CALLBACK(deactivated), this);
m_signals.at(ZonesChanged) = g_signal_connect(G_OBJECT(session), "zones-changed", G_CALLBACK(zonesChanged), this);
m_signals.at(SessionClosed) = g_signal_connect(G_OBJECT(parentSession), "closed", G_CALLBACK(sessionClosed), this);
handleZonesChanged(session, nullptr);
#ifdef HAVE_LIBPORTAL_CLIPBOARD
m_signals.at(SelectionTransfer) =
g_signal_connect(G_OBJECT(parentSession), "selection-transfer", G_CALLBACK(selectionTransfer), this);
#endif
}
void PortalInputCapture::handleInitSession(GObject *object, GAsyncResult *res)
{
g_autoptr(GError) error = nullptr;
LOG_DEBUG("portal input capture session initialized");
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;
setupSession(session);
}
void PortalInputCapture::handleStart(GObject *object, GAsyncResult *res)
{
g_autoptr(GError) error = nullptr;
#ifdef HAVE_LIBPORTAL_INPUTCAPTURE_RESTORE
LOG_DEBUG("portal input capture session initialized");
if (!xdp_input_capture_session_start_finish(m_session, res, &error)) {
LOG_ERR("failed to start input capture session, quitting: %s", error->message);
g_main_loop_quit(m_glibMainLoop);
m_events->addEvent(Event(EventTypes::Quit));
return;
}
auto restoreToken = QString(xdp_input_capture_session_get_restore_token(m_session));
if (!restoreToken.isEmpty()) {
Settings::setValue(Settings::Server::XdpRestoreToken, restoreToken);
}
#endif
setupSession(m_session);
}
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);
std::erase_if(m_barrierInfo, [id](const auto &info) { return info.id == id; });
break;
}
}
it = it->next;
}
}
g_list_free_full(failed_list, g_object_unref);
enable();
}
std::pair<int, int>
PortalInputCapture::mapPortalActivationToScreenPosition(guint barrierId, double rawX, double rawY) const
{
auto x = static_cast<int>(rawX);
auto y = static_cast<int>(rawY);
const auto it = std::ranges::find_if(m_barrierInfo, [barrierId](const auto &info) { return info.id == barrierId; });
if (it == m_barrierInfo.end()) {
LOG_DEBUG("activated barrier %u is not in the current pointer barrier set", barrierId);
return {x, y};
}
const auto zoneLeft = it->x;
const auto zoneTop = it->y;
const auto zoneRight = it->x + static_cast<gint>(it->width) - 1;
const auto zoneBottom = it->y + static_cast<gint>(it->height) - 1;
std::int32_t screenX;
std::int32_t screenY;
std::int32_t screenW;
std::int32_t screenH;
m_screen->getShape(screenX, screenY, screenW, screenH);
if (Bounds portalBounds; getPortalBounds(portalBounds)) {
x = scaleCoordinateBetweenRanges(rawX, portalBounds.left, portalBounds.right, screenX, screenX + screenW - 1);
y = scaleCoordinateBetweenRanges(rawY, portalBounds.top, portalBounds.bottom, screenY, screenY + screenH - 1);
} else {
x = std::clamp(x, zoneLeft, zoneRight);
y = std::clamp(y, zoneTop, zoneBottom);
}
// The portal reports per-output zones, while Deskflow models the whole computer as one screen.
// Use the activated barrier to preserve the intended switch direction in Deskflow's aggregate coordinates.
using enum BarrierSide;
switch (it->side) {
case Left:
x = screenX;
break;
case Right:
x = screenX + screenW - 1;
break;
case Top:
y = screenY;
break;
case Bottom:
y = screenY + screenH - 1;
break;
}
return {x, y};
}
std::pair<double, double> PortalInputCapture::mapPortalReleasePosition(double x, double y) const
{
std::int32_t screenX;
std::int32_t screenY;
std::int32_t screenW;
std::int32_t screenH;
m_screen->getShape(screenX, screenY, screenW, screenH);
const auto screenLeft = screenX;
const auto screenTop = screenY;
const auto screenRight = screenX + screenW - 1;
const auto screenBottom = screenY + screenH - 1;
const auto jumpZoneSize = m_screen->getJumpZoneSize();
Bounds portalBounds;
if (!getPortalBounds(portalBounds)) {
return {x, y};
}
auto mappedX = static_cast<std::int32_t>(std::lround(x));
auto mappedY = static_cast<std::int32_t>(std::lround(y));
if (BarrierInfo releaseBarrier;
getClosestReleaseBarrier(x, y, screenLeft, screenTop, screenRight, screenBottom, portalBounds, releaseBarrier)) {
const Bounds releaseBounds = {
releaseBarrier.x, releaseBarrier.y, releaseBarrier.x + static_cast<gint>(releaseBarrier.width) - 1,
releaseBarrier.y + static_cast<gint>(releaseBarrier.height) - 1
};
using enum BarrierSide;
switch (releaseBarrier.side) {
case Left:
mappedX = std::min(releaseBounds.left + jumpZoneSize, releaseBounds.right);
mappedY = std::clamp(mappedY, releaseBounds.top, releaseBounds.bottom);
break;
case Right:
mappedX = std::max(releaseBounds.right - jumpZoneSize, releaseBounds.left);
mappedY = std::clamp(mappedY, releaseBounds.top, releaseBounds.bottom);
break;
case Top:
mappedX = std::clamp(mappedX, releaseBounds.left, releaseBounds.right);
mappedY = std::min(releaseBounds.top + jumpZoneSize, releaseBounds.bottom);
break;
case Bottom:
mappedX = std::clamp(mappedX, releaseBounds.left, releaseBounds.right);
mappedY = std::max(releaseBounds.bottom - jumpZoneSize, releaseBounds.top);
break;
}
}
return {mappedX, mappedY};
}
void PortalInputCapture::addBarrier(
guint id, BarrierSide side, gint zoneX, gint zoneY, guint zoneWidth, guint zoneHeight
)
{
gint x1 = 0;
gint x2 = 0;
gint y1 = 0;
gint y2 = 0;
using enum BarrierSide;
switch (side) {
case Left:
x1 = zoneX;
y1 = zoneY;
x2 = zoneX;
y2 = zoneY + static_cast<gint>(zoneHeight) - 1;
break;
case Right:
x1 = zoneX + static_cast<gint>(zoneWidth);
y1 = zoneY;
x2 = x1;
y2 = zoneY + static_cast<gint>(zoneHeight) - 1;
break;
case Top:
x1 = zoneX;
y1 = zoneY;
x2 = zoneX + static_cast<gint>(zoneWidth) - 1;
y2 = zoneY;
break;
case Bottom:
x1 = zoneX;
y1 = zoneY + static_cast<gint>(zoneHeight);
x2 = zoneX + static_cast<gint>(zoneWidth) - 1;
y2 = y1;
break;
}
LOG_DEBUG("barrier (%s) %u at %d,%d-%d,%d", barrierSideName(side), 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)
));
m_barrierInfo.push_back({id, side, zoneX, zoneY, zoneWidth, zoneHeight, x1, y1, x2, y2});
}
gboolean PortalInputCapture::initSession()
{
LOG_DEBUG("setting up input capture session");
XdpInputCaptureSession *session;
#ifdef HAVE_LIBPORTAL_INPUTCAPTURE_RESTORE
g_autoptr(GError) error = nullptr;
m_portalVersion = xdp_portal_get_input_capture_version_sync(
m_portal,
nullptr, // Cancellable
nullptr
);
LOG_DEBUG("input capture version %d", m_portalVersion);
switch (m_portalVersion) {
case 0:
LOG_WARN("portal bug: input capture version is %d", m_portalVersion);
// fallthrough
case 1:
xdp_portal_create_input_capture_session(
m_portal,
nullptr, // parent
static_cast<XdpInputCapability>(XDP_INPUT_CAPABILITY_KEYBOARD | XDP_INPUT_CAPABILITY_POINTER),
nullptr, // cancellable
[](GObject *obj, GAsyncResult *res, gpointer data) {
static_cast<PortalInputCapture *>(data)->handleInitSession(obj, res);
},
this
);
break;
default:
session = xdp_portal_create_input_capture_session2_sync(
m_portal,
nullptr, // Cancellable
&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 FALSE;
}
m_session = session;
xdp_session_request_clipboard(xdp_input_capture_session_get_session(session));
xdp_input_capture_session_set_session_persistence(session, XDP_INPUT_CAPTURE_SESSION_PERSISTENCE_PERSISTENT);
if (auto sessionToken = Settings::value(Settings::Server::XdpRestoreToken).toByteArray(); !sessionToken.isEmpty()) {
xdp_input_capture_session_set_restore_token(session, strdup(sessionToken.data()));
}
xdp_input_capture_session_start(
m_session,
nullptr, // parent
static_cast<XdpInputCapability>(XDP_INPUT_CAPABILITY_KEYBOARD | XDP_INPUT_CAPABILITY_POINTER),
nullptr, // cancellable
[](GObject *obj, GAsyncResult *res, gpointer data) {
static_cast<PortalInputCapture *>(data)->handleStart(obj, res);
},
this
);
break;
}
#else
xdp_portal_create_input_capture_session(
m_portal,
nullptr, // parent
static_cast<XdpInputCapability>(XDP_INPUT_CAPABILITY_KEYBOARD | XDP_INPUT_CAPABILITY_POINTER),
nullptr, // cancellable
[](GObject *obj, GAsyncResult *res, gpointer data) {
static_cast<PortalInputCapture *>(data)->handleInitSession(obj, res);
},
this
);
#endif
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)
{
const auto [mappedX, mappedY] = mapPortalReleasePosition(x, y);
LOG_DEBUG(
"releasing input capture session, id=%d x=%.1f y=%.1f mapped=%.1f,%.1f", m_activationId, x, y, mappedX, mappedY
);
xdp_input_capture_session_release_at(m_session, m_activationId, mappedX, mappedY);
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<PortalInputCapture *>(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)) {
auto warpX = static_cast<int>(x);
auto warpY = static_cast<int>(y);
guint barrierId = 0;
if (const bool hasBarrierId = g_variant_lookup(options, "barrier_id", "u", &barrierId);
hasBarrierId && barrierId > 0) {
auto [mappedX, mappedY] = mapPortalActivationToScreenPosition(barrierId, x, y);
warpX = mappedX;
warpY = mappedY;
} else if (!hasBarrierId) {
LOG_DEBUG("portal activation has no barrier id, using raw cursor position");
} else {
LOG_DEBUG("portal activation barrier id is zero, using raw cursor position");
}
m_screen->warpCursor(warpX, warpY);
m_events->addEvent(Event(
EventTypes::PrimaryScreenMotionOnPrimary, m_screen->getEventTarget(),
IPrimaryScreen::MotionInfo::alloc(warpX, warpY)
));
} else {
LOG_WARN("failed to get cursor position");
}
} else {
LOG_WARN("activation has no options");
}
m_activationId = activationId;
m_isActive = true;
#ifdef HAVE_LIBPORTAL_CLIPBOARD
if (m_session) {
LOG_DEBUG("reading clipboard selection on activation");
m_screen->sendClipboardEvent(EventTypes::ClipboardGrabbed, kClipboardClipboard);
XdpSession *session = xdp_input_capture_session_get_session(m_session);
const char **mimeTypes = xdp_session_get_selection_mime_types(session);
if (mimeTypes && mimeTypes[0]) {
LOG_DEBUG("clipboard current selection mime types: %s", PortalClipboard::formatMimeTypes(mimeTypes).constData());
if (!xdp_session_is_selection_owned_by_session(session))
readClipboardSelection(session);
} else {
LOG_DEBUG("no current clipboard selection");
}
claimClipboardOwnership(session);
LOG_DEBUG("activation clipboard handling complete");
} else {
LOG_WARN("input capture activated without a session, skipping clipboard read");
}
#endif
}
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();
m_barrierInfo.clear();
const auto activeSides = m_screen->activeSides();
using enum DirectionMask;
auto zones = xdp_input_capture_session_get_zones(session);
// First pass: compute the bounding box (union) of all input-capture zones.
// A pointer barrier must lie on the outer boundary of the combined desktop and
// be adjacent to a single monitor edge. A barrier placed on an internal edge
// between two adjacent monitors is rejected by the portal ("adjacent to
// multiple monitor edges"), and a single rejected barrier fails the whole
// barrier set - so on a multi-monitor server input capture never engages.
gint unionLeft = 0;
gint unionTop = 0;
gint unionRight = 0;
gint unionBottom = 0;
bool boundsInit = false;
for (auto z = zones; z != nullptr; z = z->next) {
guint w;
guint h;
gint x;
gint y;
g_object_get(z->data, "width", &w, "height", &h, "x", &x, "y", &y, nullptr);
const gint right = x + static_cast<gint>(w);
const gint bottom = y + static_cast<gint>(h);
if (!boundsInit) {
unionLeft = x;
unionTop = y;
unionRight = right;
unionBottom = bottom;
boundsInit = true;
} else {
unionLeft = std::min(unionLeft, x);
unionTop = std::min(unionTop, y);
unionRight = std::max(unionRight, right);
unionBottom = std::max(unionBottom, bottom);
}
}
// Second pass: only add a barrier for a zone edge that is part of the outer
// boundary of the union (i.e. not an internal seam between two monitors).
guint id = 0;
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);
if ((activeSides & static_cast<int>(LeftMask)) && x == unionLeft) {
addBarrier(++id, BarrierSide::Left, x, y, w, h);
}
if ((activeSides & static_cast<int>(RightMask)) && (x + static_cast<gint>(w)) == unionRight) {
addBarrier(++id, BarrierSide::Right, x, y, w, h);
}
if ((activeSides & static_cast<int>(TopMask)) && y == unionTop) {
addBarrier(++id, BarrierSide::Top, x, y, w, h);
}
if ((activeSides & static_cast<int>(BottomMask)) && (y + static_cast<gint>(h)) == unionBottom) {
addBarrier(++id, BarrierSide::Bottom, x, y, w, h);
}
zones = zones->next;
}
GList *list = nullptr;
for (auto const &b : m_barriers) {
list = g_list_append(list, b);
}
if (list != nullptr) {
xdp_input_capture_session_set_pointer_barriers(
m_session, list,
nullptr, // cancellable
[](GObject *obj, GAsyncResult *res, gpointer data) {
static_cast<PortalInputCapture *>(data)->handleSetPointerBarriers(obj, res);
},
this
);
} else {
LOG_WARN("no input capture pointer barriers found");
}
}
void PortalInputCapture::glibThread(const 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