840 lines
26 KiB
C++
840 lines
26 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);
|
|
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_signal_handler_disconnect(m_session, m_signals.at(SelectionTransfer));
|
|
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(XdpSession *session)
|
|
{
|
|
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
|
PortalClipboard::claimOwnership(m_clipboard, session);
|
|
#endif
|
|
}
|
|
|
|
void PortalInputCapture::readClipboardSelection(XdpSession *session)
|
|
{
|
|
#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)
|
|
{
|
|
#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);
|
|
|
|
Bounds portalBounds;
|
|
if (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 = scaleCoordinateBetweenRanges(x, screenLeft, screenRight, portalBounds.left, portalBounds.right);
|
|
auto mappedY = scaleCoordinateBetweenRanges(y, screenTop, screenBottom, portalBounds.top, portalBounds.bottom);
|
|
BarrierInfo releaseBarrier;
|
|
if (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;
|
|
const bool hasBarrierId = g_variant_lookup(options, "barrier_id", "u", &barrierId);
|
|
|
|
if (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;
|
|
|
|
// May not correctly handle different sized screens
|
|
auto zones = xdp_input_capture_session_get_zones(session);
|
|
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)) {
|
|
addBarrier(++id, BarrierSide::Left, x, y, w, h);
|
|
}
|
|
|
|
if (activeSides & static_cast<int>(RightMask)) {
|
|
addBarrier(++id, BarrierSide::Right, x, y, w, h);
|
|
}
|
|
|
|
if (activeSides & static_cast<int>(TopMask)) {
|
|
addBarrier(++id, BarrierSide::Top, x, y, w, h);
|
|
}
|
|
|
|
if (activeSides & static_cast<int>(BottomMask)) {
|
|
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
|