feat: clipboard support for portal remote desktop
Add libportal clipboard handling to the remote desktop. Supports both directions (server to Wayland client paste, and copy from the Wayland client back to the server) for UTF-8 text, plain text, and PNG/BMP images. The shared mime-type table, BMP/DIB and PNG encode/decode helpers, and portal selection read/write live in a new PortalClipboard helper used by both PortalInputCapture and PortalRemoteDesktop. If the portal returns a restored RemoteDesktop session without clipboard enabled (a token persisted before clipboard support existed), discard the restore token and reconnect so the user gets a fresh permission dialog. Otherwise the session would loop with a clipboard-less token forever.
This commit is contained in:
parent
66cb367473
commit
03a4800677
9 changed files with 537 additions and 318 deletions
|
|
@ -164,6 +164,12 @@ elseif(UNIX)
|
||||||
PortalRemoteDesktop.cpp
|
PortalRemoteDesktop.cpp
|
||||||
PortalRemoteDesktop.h
|
PortalRemoteDesktop.h
|
||||||
)
|
)
|
||||||
|
if(HAVE_LP_CLIPBOARD)
|
||||||
|
list(APPEND PLATFORM_SOURCES
|
||||||
|
PortalClipboard.cpp
|
||||||
|
PortalClipboard.h
|
||||||
|
)
|
||||||
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
endif()
|
endif()
|
||||||
|
|
|
||||||
|
|
@ -424,7 +424,13 @@ bool EiScreen::setClipboard(ClipboardID id, const IClipboard *clipboard)
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return IClipboard::copy(m_clipboard, clipboard);
|
bool ok = IClipboard::copy(m_clipboard, clipboard);
|
||||||
|
|
||||||
|
if (ok && m_portalRemoteDesktop && id == kClipboardClipboard) {
|
||||||
|
m_portalRemoteDesktop->claimClipboard();
|
||||||
|
}
|
||||||
|
|
||||||
|
return ok;
|
||||||
}
|
}
|
||||||
|
|
||||||
void EiScreen::checkClipboards()
|
void EiScreen::checkClipboards()
|
||||||
|
|
|
||||||
|
|
@ -82,6 +82,12 @@ public:
|
||||||
// Send clipboard event (needed by PortalInputCapture)
|
// Send clipboard event (needed by PortalInputCapture)
|
||||||
void sendClipboardEvent(EventTypes type, ClipboardID id) const;
|
void sendClipboardEvent(EventTypes type, ClipboardID id) const;
|
||||||
|
|
||||||
|
// Local clipboard cache (used by PortalRemoteDesktop to land selection reads)
|
||||||
|
EiClipboard *getClipboardCache() const
|
||||||
|
{
|
||||||
|
return m_clipboard;
|
||||||
|
}
|
||||||
|
|
||||||
// Maximum clipboard size in KB, configured by the server
|
// Maximum clipboard size in KB, configured by the server
|
||||||
size_t maximumClipboardSize() const
|
size_t maximumClipboardSize() const
|
||||||
{
|
{
|
||||||
|
|
|
||||||
332
src/lib/platform/PortalClipboard.cpp
Normal file
332
src/lib/platform/PortalClipboard.cpp
Normal file
|
|
@ -0,0 +1,332 @@
|
||||||
|
/*
|
||||||
|
* Deskflow -- mouse and keyboard sharing utility
|
||||||
|
* SPDX-FileCopyrightText: (C) 2026 Synergy App Ltd
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
||||||
|
*/
|
||||||
|
|
||||||
|
#include "platform/PortalClipboard.h"
|
||||||
|
|
||||||
|
#include "base/Log.h"
|
||||||
|
#include "platform/EiClipboard.h"
|
||||||
|
|
||||||
|
#include <cstring>
|
||||||
|
#include <poll.h>
|
||||||
|
#include <unistd.h>
|
||||||
|
|
||||||
|
#include <QBuffer>
|
||||||
|
#include <QByteArrayList>
|
||||||
|
#include <QDataStream>
|
||||||
|
#include <QFile>
|
||||||
|
#include <QImage>
|
||||||
|
#include <QList>
|
||||||
|
#include <QPair>
|
||||||
|
#include <QSet>
|
||||||
|
#include <QVarLengthArray>
|
||||||
|
#include <QtEndian>
|
||||||
|
|
||||||
|
namespace deskflow {
|
||||||
|
|
||||||
|
static constexpr int kBmpSignatureSize = 2;
|
||||||
|
static constexpr quint32 kBmpFileHeaderSize = 14;
|
||||||
|
static constexpr quint32 kMinDibHeaderSize = 12;
|
||||||
|
|
||||||
|
QByteArray PortalClipboard::formatMimeTypes(const char *const *mimeTypes)
|
||||||
|
{
|
||||||
|
if (!mimeTypes || !mimeTypes[0])
|
||||||
|
return QByteArrayLiteral("(none)");
|
||||||
|
|
||||||
|
QByteArrayList parts;
|
||||||
|
for (int i = 0; mimeTypes[i]; ++i)
|
||||||
|
parts.append(mimeTypes[i]);
|
||||||
|
return parts.join(", ");
|
||||||
|
}
|
||||||
|
|
||||||
|
const PortalClipboard::SupportedMime *PortalClipboard::findSupportedMime(const char *mime)
|
||||||
|
{
|
||||||
|
if (!mime)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
for (const auto &entry : kSupportedMimes) {
|
||||||
|
if (g_strcmp0(mime, entry.mime) == 0)
|
||||||
|
return &entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
const PortalClipboard::SupportedMime *PortalClipboard::pickSupportedMime(const char *const *available)
|
||||||
|
{
|
||||||
|
if (!available)
|
||||||
|
return nullptr;
|
||||||
|
|
||||||
|
for (const auto &entry : kSupportedMimes) {
|
||||||
|
if (g_strv_contains(available, entry.mime))
|
||||||
|
return &entry;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nullptr;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray PortalClipboard::dibToBmp(const QByteArray &dib)
|
||||||
|
{
|
||||||
|
if (dib.size() < static_cast<qint64>(sizeof(quint32)))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
quint32 headerSize;
|
||||||
|
std::memcpy(&headerSize, dib.constData(), sizeof(headerSize));
|
||||||
|
headerSize = qFromLittleEndian(headerSize);
|
||||||
|
if (headerSize < kMinDibHeaderSize || headerSize > static_cast<quint32>(dib.size()))
|
||||||
|
return {};
|
||||||
|
|
||||||
|
const auto fileSize = static_cast<quint32>(kBmpFileHeaderSize + dib.size());
|
||||||
|
const quint32 pixelOffset = kBmpFileHeaderSize + headerSize;
|
||||||
|
|
||||||
|
QByteArray bmp;
|
||||||
|
QDataStream ds(&bmp, QIODevice::WriteOnly);
|
||||||
|
ds.setByteOrder(QDataStream::LittleEndian);
|
||||||
|
ds.writeRawData("BM", kBmpSignatureSize);
|
||||||
|
ds << fileSize;
|
||||||
|
ds << quint32(0);
|
||||||
|
ds << pixelOffset;
|
||||||
|
ds.writeRawData(dib.constData(), static_cast<int>(dib.size()));
|
||||||
|
return bmp;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray PortalClipboard::bmpToDib(const QByteArray &bmp)
|
||||||
|
{
|
||||||
|
if (bmp.size() < kBmpFileHeaderSize)
|
||||||
|
return {};
|
||||||
|
|
||||||
|
return bmp.mid(kBmpFileHeaderSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray PortalClipboard::encodeFormat(IClipboard::Format format, const QByteArray &data)
|
||||||
|
{
|
||||||
|
if (data.isEmpty())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
if (format == IClipboard::Format::Bitmap) {
|
||||||
|
const auto bmpFile = dibToBmp(data);
|
||||||
|
if (bmpFile.isEmpty()) {
|
||||||
|
LOG_WARN("clipboard bitmap data is malformed");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QImage image;
|
||||||
|
if (!image.loadFromData(bmpFile, "BMP")) {
|
||||||
|
LOG_WARN("failed to decode clipboard bitmap");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray png;
|
||||||
|
QBuffer buf(&png);
|
||||||
|
buf.open(QIODevice::WriteOnly);
|
||||||
|
if (!image.save(&buf, "PNG")) {
|
||||||
|
LOG_WARN("failed to encode clipboard image as png");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return png;
|
||||||
|
}
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray PortalClipboard::decodeFormat(IClipboard::Format format, const QByteArray &bytes)
|
||||||
|
{
|
||||||
|
if (bytes.isEmpty())
|
||||||
|
return {};
|
||||||
|
|
||||||
|
if (format == IClipboard::Format::Bitmap) {
|
||||||
|
QImage image;
|
||||||
|
if (!image.loadFromData(bytes, "PNG")) {
|
||||||
|
LOG_WARN("failed to decode clipboard png");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray bmp;
|
||||||
|
QBuffer buf(&bmp);
|
||||||
|
buf.open(QIODevice::WriteOnly);
|
||||||
|
if (!image.save(&buf, "BMP")) {
|
||||||
|
LOG_WARN("failed to encode clipboard image as bmp");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
return bmpToDib(bmp);
|
||||||
|
}
|
||||||
|
return bytes;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray PortalClipboard::readSelectionBytes(XdpSession *session, const char *mime, qint64 maxBytes)
|
||||||
|
{
|
||||||
|
const int fd = xdp_session_selection_read(session, mime);
|
||||||
|
if (fd < 0) {
|
||||||
|
LOG_ERR("failed to read clipboard selection: invalid fd");
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile pipe;
|
||||||
|
if (!pipe.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
|
||||||
|
LOG_WARN("failed to wrap clipboard pipe");
|
||||||
|
::close(fd);
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray contents;
|
||||||
|
contents.reserve(std::min<qint64>(maxBytes, kChunkBytes));
|
||||||
|
while (contents.size() < maxBytes) {
|
||||||
|
pollfd pfd{fd, POLLIN, 0};
|
||||||
|
if (poll(&pfd, 1, kReadTimeoutMs) <= 0)
|
||||||
|
break;
|
||||||
|
|
||||||
|
const auto chunk = pipe.read(std::min<qint64>(kChunkBytes, maxBytes - contents.size()));
|
||||||
|
if (chunk.isEmpty())
|
||||||
|
break;
|
||||||
|
|
||||||
|
contents.append(chunk);
|
||||||
|
}
|
||||||
|
return contents;
|
||||||
|
}
|
||||||
|
|
||||||
|
void PortalClipboard::claimOwnership(EiClipboard *cache, XdpSession *session)
|
||||||
|
{
|
||||||
|
if (!cache || !session)
|
||||||
|
return;
|
||||||
|
|
||||||
|
cache->open(0);
|
||||||
|
QVarLengthArray<const char *, std::size(kSupportedMimes) + 1> mimeTypes;
|
||||||
|
for (const auto &entry : kSupportedMimes) {
|
||||||
|
if (cache->has(entry.format))
|
||||||
|
mimeTypes.append(entry.mime);
|
||||||
|
}
|
||||||
|
cache->close();
|
||||||
|
|
||||||
|
if (mimeTypes.isEmpty()) {
|
||||||
|
LOG_DEBUG("clipboard cache empty, nothing to claim");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
mimeTypes.append(nullptr);
|
||||||
|
|
||||||
|
LOG_DEBUG("claiming clipboard, mimes: %s", formatMimeTypes(mimeTypes.data()).constData());
|
||||||
|
xdp_session_set_selection(session, mimeTypes.data());
|
||||||
|
}
|
||||||
|
|
||||||
|
void PortalClipboard::serveSelectionTransfer(EiClipboard *cache, XdpSession *session, const char *mime, uint32_t serial)
|
||||||
|
{
|
||||||
|
LOG_DEBUG("clipboard selection transfer requested, mime: %s, serial: %u", mime, serial);
|
||||||
|
|
||||||
|
const auto *requested = findSupportedMime(mime);
|
||||||
|
if (!requested || !cache) {
|
||||||
|
LOG_DEBUG("rejecting clipboard selection, unsupported mime: %s", mime);
|
||||||
|
xdp_session_selection_write_done(session, serial, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache->open(0);
|
||||||
|
QByteArray raw;
|
||||||
|
const bool hasFormat = cache->has(requested->format);
|
||||||
|
if (hasFormat)
|
||||||
|
raw = QByteArray::fromStdString(cache->get(requested->format));
|
||||||
|
cache->close();
|
||||||
|
|
||||||
|
const auto data = encodeFormat(requested->format, raw);
|
||||||
|
if (data.isEmpty()) {
|
||||||
|
LOG_DEBUG("clipboard has no data for mime: %s", mime);
|
||||||
|
xdp_session_selection_write_done(session, serial, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int fd = xdp_session_selection_write(session, serial);
|
||||||
|
if (fd < 0) {
|
||||||
|
LOG_WARN("failed to open clipboard selection write fd");
|
||||||
|
xdp_session_selection_write_done(session, serial, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
QFile pipe;
|
||||||
|
if (!pipe.open(fd, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle)) {
|
||||||
|
LOG_WARN("failed to wrap clipboard pipe");
|
||||||
|
::close(fd);
|
||||||
|
xdp_session_selection_write_done(session, serial, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const char *buf = data.constData();
|
||||||
|
qint64 total = data.size();
|
||||||
|
qint64 written = 0;
|
||||||
|
while (written < total) {
|
||||||
|
pollfd pfd{fd, POLLOUT, 0};
|
||||||
|
if (poll(&pfd, 1, kWriteTimeoutMs) <= 0) {
|
||||||
|
LOG_ERR("timed out writing clipboard selection");
|
||||||
|
xdp_session_selection_write_done(session, serial, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
qint64 n = pipe.write(buf + written, total - written);
|
||||||
|
if (n <= 0) {
|
||||||
|
LOG_ERR("clipboard pipe write returned %lld", static_cast<long long>(n));
|
||||||
|
xdp_session_selection_write_done(session, serial, false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
written += n;
|
||||||
|
}
|
||||||
|
|
||||||
|
xdp_session_selection_write_done(session, serial, true);
|
||||||
|
LOG_DEBUG("clipboard selection transfer complete, bytes: %lld", static_cast<long long>(written));
|
||||||
|
}
|
||||||
|
|
||||||
|
bool PortalClipboard::readSelectionIntoCache(
|
||||||
|
EiClipboard *cache, XdpSession *session, const char *const *mimeTypes, qint64 maxBytes
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (!cache || !session || !mimeTypes || !mimeTypes[0])
|
||||||
|
return false;
|
||||||
|
|
||||||
|
if (!pickSupportedMime(mimeTypes)) {
|
||||||
|
LOG_DEBUG("clipboard no supported mime types: %s", formatMimeTypes(mimeTypes).constData());
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
QList<QPair<IClipboard::Format, QByteArray>> reads;
|
||||||
|
QSet<IClipboard::Format> seen;
|
||||||
|
for (const auto &entry : kSupportedMimes) {
|
||||||
|
if (seen.contains(entry.format))
|
||||||
|
continue;
|
||||||
|
if (!g_strv_contains(mimeTypes, entry.mime))
|
||||||
|
continue;
|
||||||
|
|
||||||
|
auto bytes = readSelectionBytes(session, entry.mime, maxBytes);
|
||||||
|
if (bytes.isEmpty()) {
|
||||||
|
LOG_DEBUG("clipboard read returned no data for mime: %s", entry.mime);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.format != IClipboard::Format::Bitmap) {
|
||||||
|
while (bytes.endsWith('\0'))
|
||||||
|
bytes.chop(1);
|
||||||
|
bytes.replace("\r\n", "\n");
|
||||||
|
}
|
||||||
|
|
||||||
|
auto data = decodeFormat(entry.format, bytes);
|
||||||
|
if (data.isEmpty())
|
||||||
|
continue;
|
||||||
|
|
||||||
|
reads.append({entry.format, std::move(data)});
|
||||||
|
seen.insert(entry.format);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (reads.isEmpty()) {
|
||||||
|
LOG_DEBUG("clipboard read produced no data, leaving existing clipboard intact");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cache->open(0);
|
||||||
|
cache->empty();
|
||||||
|
for (const auto &[format, data] : reads)
|
||||||
|
cache->add(format, data.toStdString());
|
||||||
|
cache->close();
|
||||||
|
|
||||||
|
LOG_DEBUG("clipboard read local selection, formats: %lld", static_cast<long long>(reads.size()));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace deskflow
|
||||||
63
src/lib/platform/PortalClipboard.h
Normal file
63
src/lib/platform/PortalClipboard.h
Normal file
|
|
@ -0,0 +1,63 @@
|
||||||
|
/*
|
||||||
|
* Deskflow -- mouse and keyboard sharing utility
|
||||||
|
* SPDX-FileCopyrightText: (C) 2026 Synergy App Ltd
|
||||||
|
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
||||||
|
*/
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include "deskflow/IClipboard.h"
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
|
||||||
|
#include <libportal/portal.h>
|
||||||
|
|
||||||
|
namespace deskflow {
|
||||||
|
|
||||||
|
class EiClipboard;
|
||||||
|
|
||||||
|
class PortalClipboard
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
struct SupportedMime
|
||||||
|
{
|
||||||
|
const char *mime;
|
||||||
|
IClipboard::Format format;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Listed in preference order: richer formats first.
|
||||||
|
static constexpr SupportedMime kSupportedMimes[] = {
|
||||||
|
{"image/png", IClipboard::Format::Bitmap},
|
||||||
|
{"text/plain;charset=utf-8", IClipboard::Format::Text},
|
||||||
|
{"text/plain", IClipboard::Format::Text},
|
||||||
|
};
|
||||||
|
|
||||||
|
static constexpr int kReadTimeoutMs = 200;
|
||||||
|
static constexpr int kWriteTimeoutMs = 200;
|
||||||
|
static constexpr qint64 kChunkBytes = 64 * 1024;
|
||||||
|
|
||||||
|
static QByteArray formatMimeTypes(const char *const *mimeTypes);
|
||||||
|
static const SupportedMime *findSupportedMime(const char *mime);
|
||||||
|
static const SupportedMime *pickSupportedMime(const char *const *available);
|
||||||
|
static QByteArray encodeFormat(IClipboard::Format format, const QByteArray &data);
|
||||||
|
static QByteArray decodeFormat(IClipboard::Format format, const QByteArray &bytes);
|
||||||
|
static QByteArray readSelectionBytes(XdpSession *session, const char *mime, qint64 maxBytes);
|
||||||
|
|
||||||
|
/// Advertise the cache's formats to the portal selection.
|
||||||
|
static void claimOwnership(EiClipboard *cache, XdpSession *session);
|
||||||
|
|
||||||
|
/// Respond to a selection-transfer signal by writing the cache's bytes for
|
||||||
|
/// \p mime to the portal-provided fd. Always calls write_done.
|
||||||
|
static void serveSelectionTransfer(EiClipboard *cache, XdpSession *session, const char *mime, uint32_t serial);
|
||||||
|
|
||||||
|
/// Read every supported format offered by the portal into the cache.
|
||||||
|
/// Returns true if any data was deposited.
|
||||||
|
static bool
|
||||||
|
readSelectionIntoCache(EiClipboard *cache, XdpSession *session, const char *const *mimeTypes, qint64 maxBytes);
|
||||||
|
|
||||||
|
private:
|
||||||
|
static QByteArray dibToBmp(const QByteArray &dib);
|
||||||
|
static QByteArray bmpToDib(const QByteArray &bmp);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace deskflow
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Deskflow -- mouse and keyboard sharing utility
|
* Deskflow -- mouse and keyboard sharing utility
|
||||||
* SPDX-FileCopyrightText: (C) 2025 - 2026 Deskflow Developers
|
* SPDX-FileCopyrightText: (C) 2024 - 2026 Deskflow Developers
|
||||||
* SPDX-FileCopyrightText: (C) 2024, 2026 Synergy App Ltd
|
* SPDX-FileCopyrightText: (C) 2024, 2026 Synergy App Ltd
|
||||||
* SPDX-FileCopyrightText: (C) 2022, 2026 Red Hat, Inc.
|
* SPDX-FileCopyrightText: (C) 2022, 2026 Red Hat, Inc.
|
||||||
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
||||||
|
|
@ -14,6 +14,10 @@
|
||||||
#include "deskflow/ClipboardTypes.h"
|
#include "deskflow/ClipboardTypes.h"
|
||||||
#include "platform/EiClipboard.h"
|
#include "platform/EiClipboard.h"
|
||||||
|
|
||||||
|
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
||||||
|
#include "platform/PortalClipboard.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
#ifdef HAVE_LIBPORTAL_INPUTCAPTURE_RESTORE
|
#ifdef HAVE_LIBPORTAL_INPUTCAPTURE_RESTORE
|
||||||
#include "common/Settings.h"
|
#include "common/Settings.h"
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -22,7 +26,6 @@
|
||||||
#include <cmath>
|
#include <cmath>
|
||||||
#include <limits>
|
#include <limits>
|
||||||
#include <poll.h>
|
#include <poll.h>
|
||||||
#include <set>
|
|
||||||
#include <sys/socket.h> // for EIS fd hack, remove
|
#include <sys/socket.h> // for EIS fd hack, remove
|
||||||
#include <sys/un.h> // for EIS fd hack, remove
|
#include <sys/un.h> // for EIS fd hack, remove
|
||||||
|
|
||||||
|
|
@ -244,37 +247,18 @@ void PortalInputCapture::handleSessionClosed(XdpSession *session)
|
||||||
m_signals.at(Signal::SessionClosed) = 0;
|
m_signals.at(Signal::SessionClosed) = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
QByteArray PortalInputCapture::formatMimeTypes(const char *const *mimeTypes)
|
|
||||||
{
|
|
||||||
if (!mimeTypes || !mimeTypes[0])
|
|
||||||
return QByteArrayLiteral("(none)");
|
|
||||||
|
|
||||||
QByteArrayList parts;
|
|
||||||
for (int i = 0; mimeTypes[i]; i++)
|
|
||||||
parts.append(mimeTypes[i]);
|
|
||||||
|
|
||||||
return parts.join(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
void PortalInputCapture::claimClipboardOwnership(XdpSession *session)
|
void PortalInputCapture::claimClipboardOwnership(XdpSession *session)
|
||||||
{
|
{
|
||||||
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
||||||
constexpr size_t count = std::size(kSupportedMimes);
|
PortalClipboard::claimOwnership(m_clipboard, session);
|
||||||
const char *mimeTypes[count + 1];
|
|
||||||
for (size_t i = 0; i < count; ++i)
|
|
||||||
mimeTypes[i] = kSupportedMimes[i].mime;
|
|
||||||
mimeTypes[count] = nullptr;
|
|
||||||
|
|
||||||
LOG_DEBUG("claiming clipboard: %s", formatMimeTypes(mimeTypes).constData());
|
|
||||||
xdp_session_set_selection(session, mimeTypes);
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
void PortalInputCapture::readClipboardSelection(XdpSession *session)
|
void PortalInputCapture::readClipboardSelection(XdpSession *session)
|
||||||
{
|
{
|
||||||
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
||||||
const qint64 kClipboardReadCap = static_cast<qint64>(m_screen->maximumClipboardSize()) * 1024;
|
const qint64 maxBytes = static_cast<qint64>(m_screen->maximumClipboardSize()) * 1024;
|
||||||
LOG_DEBUG("clipboard read cap: %lld bytes", static_cast<long long>(kClipboardReadCap));
|
LOG_DEBUG("clipboard read cap: %lld bytes", static_cast<long long>(maxBytes));
|
||||||
|
|
||||||
const char **mimeTypes = xdp_session_get_selection_mime_types(session);
|
const char **mimeTypes = xdp_session_get_selection_mime_types(session);
|
||||||
if (!mimeTypes) {
|
if (!mimeTypes) {
|
||||||
|
|
@ -282,54 +266,8 @@ void PortalInputCapture::readClipboardSelection(XdpSession *session)
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!pickSupportedMime(mimeTypes)) {
|
if (PortalClipboard::readSelectionIntoCache(m_clipboard, session, mimeTypes, maxBytes))
|
||||||
LOG_DEBUG(
|
m_screen->sendClipboardEvent(EventTypes::ClipboardGrabbed, kClipboardClipboard);
|
||||||
"clipboard no supported mime type in selection, available types: %s", formatMimeTypes(mimeTypes).constData()
|
|
||||||
);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::vector<std::pair<IClipboard::Format, std::string>> reads;
|
|
||||||
std::set<IClipboard::Format> seen;
|
|
||||||
for (const auto &entry : kSupportedMimes) {
|
|
||||||
if (seen.count(entry.format))
|
|
||||||
continue;
|
|
||||||
if (!g_strv_contains(mimeTypes, entry.mime))
|
|
||||||
continue;
|
|
||||||
|
|
||||||
LOG_DEBUG("clipboard reading selection for mime type: %s", entry.mime);
|
|
||||||
QByteArray bytes = readSelectionBytes(session, entry.mime, kClipboardReadCap);
|
|
||||||
if (bytes.isEmpty()) {
|
|
||||||
LOG_WARN("clipboard read returned no data for mime type: %s", entry.mime);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (entry.format != IClipboard::Format::Bitmap) {
|
|
||||||
while (bytes.endsWith('\0'))
|
|
||||||
bytes.chop(1);
|
|
||||||
bytes.replace("\r\n", "\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string data = decodeFormat(entry.format, bytes);
|
|
||||||
if (data.empty())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
reads.emplace_back(entry.format, std::move(data));
|
|
||||||
seen.insert(entry.format);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (reads.empty()) {
|
|
||||||
LOG_DEBUG("clipboard read produced no data, leaving existing clipboard intact");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_clipboard->open(0);
|
|
||||||
m_clipboard->empty();
|
|
||||||
for (const auto &[format, data] : reads)
|
|
||||||
m_clipboard->add(format, data);
|
|
||||||
m_clipboard->close();
|
|
||||||
|
|
||||||
m_screen->sendClipboardEvent(EventTypes::ClipboardGrabbed, kClipboardClipboard);
|
|
||||||
#else
|
#else
|
||||||
(void)session;
|
(void)session;
|
||||||
#endif
|
#endif
|
||||||
|
|
@ -338,81 +276,15 @@ void PortalInputCapture::readClipboardSelection(XdpSession *session)
|
||||||
void PortalInputCapture::handleSelectionTransfer(XdpSession *session, const char *mimeType, uint32_t serial)
|
void PortalInputCapture::handleSelectionTransfer(XdpSession *session, const char *mimeType, uint32_t serial)
|
||||||
{
|
{
|
||||||
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
||||||
constexpr int kClipboardWriteTimeoutMs = 200;
|
|
||||||
|
|
||||||
if (m_isActive) {
|
if (m_isActive) {
|
||||||
LOG_DEBUG("skipping clipboard selection transfer, clipboard is active");
|
LOG_DEBUG("skipping clipboard selection transfer, clipboard is active");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
PortalClipboard::serveSelectionTransfer(m_clipboard, session, mimeType, serial);
|
||||||
LOG_DEBUG("clipboard selection transfer requested, mime type: %s, serial: %u", mimeType, serial);
|
#else
|
||||||
|
(void)session;
|
||||||
const auto *requested = findSupportedMime(mimeType);
|
(void)mimeType;
|
||||||
if (!requested) {
|
(void)serial;
|
||||||
LOG_DEBUG("rejecting clipboard selection transfer, unsupported mime type: %s", mimeType);
|
|
||||||
xdp_session_selection_write_done(session, serial, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
int fd = xdp_session_selection_write(session, serial);
|
|
||||||
if (fd < 0) {
|
|
||||||
LOG_WARN("failed to write clipboard content: invalid fd");
|
|
||||||
xdp_session_selection_write_done(session, serial, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
m_clipboard->open(0);
|
|
||||||
std::string raw;
|
|
||||||
const bool hasFormat = m_clipboard->has(requested->format);
|
|
||||||
if (hasFormat)
|
|
||||||
raw = m_clipboard->get(requested->format);
|
|
||||||
m_clipboard->close();
|
|
||||||
|
|
||||||
LOG_DEBUG(
|
|
||||||
"clipboard format requested: %d, has: %d, raw bytes: %zu", static_cast<int>(requested->format), hasFormat,
|
|
||||||
raw.size()
|
|
||||||
);
|
|
||||||
|
|
||||||
QByteArray data = encodeFormat(requested->format, raw);
|
|
||||||
|
|
||||||
LOG_DEBUG("writing clipboard content, mime type: %s, bytes: %lld", mimeType, static_cast<long long>(data.size()));
|
|
||||||
|
|
||||||
QFile pipe;
|
|
||||||
if (!pipe.open(fd, QIODevice::WriteOnly, QFileDevice::AutoCloseHandle)) {
|
|
||||||
LOG_WARN("failed to wrap clipboard pipe");
|
|
||||||
::close(fd);
|
|
||||||
xdp_session_selection_write_done(session, serial, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const char *buf = data.constData();
|
|
||||||
qint64 total = data.size();
|
|
||||||
qint64 written = 0;
|
|
||||||
|
|
||||||
while (written < total) {
|
|
||||||
pollfd pfd{fd, POLLOUT, 0};
|
|
||||||
if (poll(&pfd, 1, kClipboardWriteTimeoutMs) <= 0) {
|
|
||||||
LOG_ERR("timed out writing clipboard selection");
|
|
||||||
xdp_session_selection_write_done(session, serial, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
qint64 n = pipe.write(buf + written, total - written);
|
|
||||||
if (n < 0) {
|
|
||||||
LOG_ERR("failed to write clipboard selection");
|
|
||||||
xdp_session_selection_write_done(session, serial, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
if (n == 0) {
|
|
||||||
LOG_ERR("clipboard pipe accepted no bytes");
|
|
||||||
xdp_session_selection_write_done(session, serial, false);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
written += n;
|
|
||||||
}
|
|
||||||
|
|
||||||
xdp_session_selection_write_done(session, serial, true);
|
|
||||||
LOG_DEBUG("clipboard write complete");
|
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -422,7 +294,7 @@ void PortalInputCapture::setupSession(XdpInputCaptureSession *session)
|
||||||
XdpSession *parentSession = xdp_input_capture_session_get_session(session);
|
XdpSession *parentSession = xdp_input_capture_session_get_session(session);
|
||||||
|
|
||||||
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
||||||
if (!xdp_session_is_clipboard_enabled(parentSession)) {
|
if (!xdp_session_is_clipboard_enabled(parentSession) && m_portalVersion > 1) {
|
||||||
// Restored sessions can pre-date clipboard support, leaving the channel
|
// Restored sessions can pre-date clipboard support, leaving the channel
|
||||||
// disabled even though we requested it. Drop the saved token and recreate
|
// disabled even though we requested it. Drop the saved token and recreate
|
||||||
// the session from scratch so the user gets a fresh permission dialog.
|
// the session from scratch so the user gets a fresh permission dialog.
|
||||||
|
|
@ -869,7 +741,7 @@ void PortalInputCapture::handleActivated(
|
||||||
const char **mimeTypes = xdp_session_get_selection_mime_types(session);
|
const char **mimeTypes = xdp_session_get_selection_mime_types(session);
|
||||||
|
|
||||||
if (mimeTypes && mimeTypes[0]) {
|
if (mimeTypes && mimeTypes[0]) {
|
||||||
LOG_DEBUG("clipboard current selection mime types: %s", formatMimeTypes(mimeTypes).constData());
|
LOG_DEBUG("clipboard current selection mime types: %s", PortalClipboard::formatMimeTypes(mimeTypes).constData());
|
||||||
if (!xdp_session_is_selection_owned_by_session(session))
|
if (!xdp_session_is_selection_owned_by_session(session))
|
||||||
readClipboardSelection(session);
|
readClipboardSelection(session);
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -965,149 +837,4 @@ void PortalInputCapture::glibThread(const void *)
|
||||||
LOG_DEBUG("shutting down glib thread");
|
LOG_DEBUG("shutting down glib thread");
|
||||||
}
|
}
|
||||||
|
|
||||||
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
|
||||||
|
|
||||||
const PortalInputCapture::SupportedMime *PortalInputCapture::findSupportedMime(const char *mime)
|
|
||||||
{
|
|
||||||
if (!mime)
|
|
||||||
return nullptr;
|
|
||||||
|
|
||||||
for (const auto &entry : kSupportedMimes) {
|
|
||||||
if (g_strcmp0(mime, entry.mime) == 0)
|
|
||||||
return &entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
const PortalInputCapture::SupportedMime *PortalInputCapture::pickSupportedMime(const char *const *available)
|
|
||||||
{
|
|
||||||
if (!available) {
|
|
||||||
LOG_WARN("cannot pick mime type, no mime types provided");
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto &entry : kSupportedMimes) {
|
|
||||||
if (g_strv_contains(available, entry.mime))
|
|
||||||
return &entry;
|
|
||||||
}
|
|
||||||
|
|
||||||
return nullptr;
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray PortalInputCapture::dibToBmp(const std::string &dib)
|
|
||||||
{
|
|
||||||
if (dib.size() < sizeof(quint32))
|
|
||||||
return {};
|
|
||||||
quint32 headerSize;
|
|
||||||
std::memcpy(&headerSize, dib.data(), sizeof(headerSize));
|
|
||||||
headerSize = qFromLittleEndian(headerSize);
|
|
||||||
if (headerSize < 12 || headerSize > dib.size())
|
|
||||||
return {};
|
|
||||||
|
|
||||||
const quint32 fileSize = static_cast<quint32>(14 + dib.size());
|
|
||||||
const quint32 pixelOffset = 14 + headerSize;
|
|
||||||
|
|
||||||
QByteArray bmp;
|
|
||||||
QDataStream ds(&bmp, QIODevice::WriteOnly);
|
|
||||||
ds.setByteOrder(QDataStream::LittleEndian);
|
|
||||||
ds.writeRawData("BM", 2);
|
|
||||||
ds << fileSize;
|
|
||||||
ds << quint32(0);
|
|
||||||
ds << pixelOffset;
|
|
||||||
ds.writeRawData(dib.data(), static_cast<int>(dib.size()));
|
|
||||||
return bmp;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string PortalInputCapture::bmpToDib(const QByteArray &bmp)
|
|
||||||
{
|
|
||||||
if (bmp.size() < 14)
|
|
||||||
return {};
|
|
||||||
return std::string(bmp.constData() + 14, bmp.size() - 14);
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray PortalInputCapture::encodeFormat(IClipboard::Format format, const std::string &data)
|
|
||||||
{
|
|
||||||
if (data.empty())
|
|
||||||
return {};
|
|
||||||
if (format == IClipboard::Format::Bitmap) {
|
|
||||||
QByteArray bmpFile = dibToBmp(data);
|
|
||||||
if (bmpFile.isEmpty()) {
|
|
||||||
LOG_WARN("clipboard bitmap data is malformed");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
QImage image;
|
|
||||||
if (!image.loadFromData(bmpFile, "BMP")) {
|
|
||||||
LOG_WARN("failed to decode clipboard bitmap");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
QByteArray png;
|
|
||||||
QBuffer buf(&png);
|
|
||||||
buf.open(QIODevice::WriteOnly);
|
|
||||||
if (!image.save(&buf, "PNG")) {
|
|
||||||
LOG_WARN("failed to encode clipboard image as png");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return png;
|
|
||||||
}
|
|
||||||
return QByteArray::fromStdString(data);
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string PortalInputCapture::decodeFormat(IClipboard::Format format, const QByteArray &bytes)
|
|
||||||
{
|
|
||||||
if (bytes.isEmpty())
|
|
||||||
return {};
|
|
||||||
if (format == IClipboard::Format::Bitmap) {
|
|
||||||
QImage image;
|
|
||||||
if (!image.loadFromData(bytes, "PNG")) {
|
|
||||||
LOG_WARN("failed to decode clipboard png");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
QByteArray bmp;
|
|
||||||
QBuffer buf(&bmp);
|
|
||||||
buf.open(QIODevice::WriteOnly);
|
|
||||||
if (!image.save(&buf, "BMP")) {
|
|
||||||
LOG_WARN("failed to encode clipboard image as bmp");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return bmpToDib(bmp);
|
|
||||||
}
|
|
||||||
return bytes.toStdString();
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray PortalInputCapture::readSelectionBytes(XdpSession *session, const char *mime, qint64 maxBytes)
|
|
||||||
{
|
|
||||||
constexpr int kClipboardReadTimeoutMs = 200;
|
|
||||||
constexpr qint64 kInitialReserveBytes = 64 * 1024;
|
|
||||||
|
|
||||||
int fd = xdp_session_selection_read(session, mime);
|
|
||||||
if (fd < 0) {
|
|
||||||
LOG_ERR("failed to read clipboard selection: invalid fd");
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QFile pipe;
|
|
||||||
if (!pipe.open(fd, QIODevice::ReadOnly, QFileDevice::AutoCloseHandle)) {
|
|
||||||
LOG_WARN("failed to wrap clipboard pipe");
|
|
||||||
::close(fd);
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
|
|
||||||
QByteArray contents;
|
|
||||||
contents.reserve(std::min(maxBytes, kInitialReserveBytes));
|
|
||||||
while (contents.size() < maxBytes) {
|
|
||||||
pollfd pfd{fd, POLLIN, 0};
|
|
||||||
if (poll(&pfd, 1, kClipboardReadTimeoutMs) <= 0)
|
|
||||||
break;
|
|
||||||
QByteArray chunk = pipe.read(maxBytes - contents.size());
|
|
||||||
if (chunk.isEmpty())
|
|
||||||
break;
|
|
||||||
contents.append(chunk);
|
|
||||||
}
|
|
||||||
|
|
||||||
return contents;
|
|
||||||
}
|
|
||||||
|
|
||||||
#endif // HAVE_LIBPORTAL_CLIPBOARD
|
|
||||||
|
|
||||||
} // namespace deskflow
|
} // namespace deskflow
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
/*
|
/*
|
||||||
* Deskflow -- mouse and keyboard sharing utility
|
* Deskflow -- mouse and keyboard sharing utility
|
||||||
* SPDX-FileCopyrightText: (C) 2025 - 2026 Deskflow Developers
|
* SPDX-FileCopyrightText: (C) 2024 - 2026 Deskflow Developers
|
||||||
* SPDX-FileCopyrightText: (C) 2024, 2026 Synergy App Ltd
|
* SPDX-FileCopyrightText: (C) 2024, 2026 Synergy App Ltd
|
||||||
* SPDX-FileCopyrightText: (C) 2022, 2026 Red Hat, Inc.
|
* SPDX-FileCopyrightText: (C) 2022, 2026 Red Hat, Inc.
|
||||||
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
||||||
|
|
@ -8,7 +8,6 @@
|
||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include "deskflow/IClipboard.h"
|
|
||||||
#include "mt/Thread.h"
|
#include "mt/Thread.h"
|
||||||
#include "platform/EiScreen.h"
|
#include "platform/EiScreen.h"
|
||||||
|
|
||||||
|
|
@ -63,28 +62,6 @@ private:
|
||||||
void readClipboardSelection(XdpSession *session);
|
void readClipboardSelection(XdpSession *session);
|
||||||
void claimClipboardOwnership(XdpSession *session);
|
void claimClipboardOwnership(XdpSession *session);
|
||||||
|
|
||||||
struct SupportedMime
|
|
||||||
{
|
|
||||||
const char *mime;
|
|
||||||
IClipboard::Format format;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Listed in preference order: richer formats first.
|
|
||||||
static constexpr SupportedMime kSupportedMimes[] = {
|
|
||||||
{"image/png", IClipboard::Format::Bitmap},
|
|
||||||
{"text/plain;charset=utf-8", IClipboard::Format::Text},
|
|
||||||
{"text/plain", IClipboard::Format::Text},
|
|
||||||
};
|
|
||||||
|
|
||||||
static QByteArray formatMimeTypes(const char *const *mimeTypes);
|
|
||||||
static const SupportedMime *findSupportedMime(const char *mime);
|
|
||||||
static const SupportedMime *pickSupportedMime(const char *const *available);
|
|
||||||
static QByteArray dibToBmp(const std::string &dib);
|
|
||||||
static std::string bmpToDib(const QByteArray &bmp);
|
|
||||||
static QByteArray encodeFormat(IClipboard::Format format, const std::string &data);
|
|
||||||
static std::string decodeFormat(IClipboard::Format format, const QByteArray &bytes);
|
|
||||||
static QByteArray readSelectionBytes(XdpSession *session, const char *mime, qint64 maxBytes);
|
|
||||||
|
|
||||||
/// g_signal_connect callback wrapper
|
/// g_signal_connect callback wrapper
|
||||||
static void sessionClosed(XdpSession *session, const gpointer data)
|
static void sessionClosed(XdpSession *session, const gpointer data)
|
||||||
{
|
{
|
||||||
|
|
|
||||||
|
|
@ -1,16 +1,21 @@
|
||||||
/*
|
/*
|
||||||
* Deskflow -- mouse and keyboard sharing utility
|
* Deskflow -- mouse and keyboard sharing utility
|
||||||
* SPDX-FileCopyrightText: (C) 2025 Deskflow Developers
|
* SPDX-FileCopyrightText: (C) 2024 - 2025 Deskflow Developers
|
||||||
* SPDX-FileCopyrightText: (C) 2024 Synergy App Ltd
|
* SPDX-FileCopyrightText: (C) 2024, 2026 Synergy App Ltd
|
||||||
* SPDX-FileCopyrightText: (C) 2022 Red Hat, Inc.
|
* SPDX-FileCopyrightText: (C) 2022 Red Hat, Inc.
|
||||||
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
* SPDX-License-Identifier: GPL-2.0-only WITH LicenseRef-OpenSSL-Exception
|
||||||
*/
|
*/
|
||||||
|
|
||||||
#include "platform/PortalRemoteDesktop.h"
|
#include "platform/PortalRemoteDesktop.h"
|
||||||
|
|
||||||
#include "base/Log.h"
|
#include "base/Log.h"
|
||||||
#include "base/TMethodJob.h"
|
#include "base/TMethodJob.h"
|
||||||
#include "common/Settings.h"
|
#include "common/Settings.h"
|
||||||
|
|
||||||
|
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
||||||
|
#include "platform/PortalClipboard.h"
|
||||||
|
#endif
|
||||||
|
|
||||||
namespace deskflow {
|
namespace deskflow {
|
||||||
|
|
||||||
PortalRemoteDesktop::PortalRemoteDesktop(EiScreen *screen, IEventQueue *events)
|
PortalRemoteDesktop::PortalRemoteDesktop(EiScreen *screen, IEventQueue *events)
|
||||||
|
|
@ -43,6 +48,10 @@ PortalRemoteDesktop::~PortalRemoteDesktop()
|
||||||
|
|
||||||
if (m_sessionSignalId)
|
if (m_sessionSignalId)
|
||||||
g_signal_handler_disconnect(m_session, m_sessionSignalId);
|
g_signal_handler_disconnect(m_session, m_sessionSignalId);
|
||||||
|
if (m_selectionTransferSignalId)
|
||||||
|
g_signal_handler_disconnect(m_session, m_selectionTransferSignalId);
|
||||||
|
if (m_selectionOwnerChangedSignalId)
|
||||||
|
g_signal_handler_disconnect(m_session, m_selectionOwnerChangedSignalId);
|
||||||
if (m_session)
|
if (m_session)
|
||||||
g_object_unref(m_session);
|
g_object_unref(m_session);
|
||||||
g_object_unref(m_portal);
|
g_object_unref(m_portal);
|
||||||
|
|
@ -89,6 +98,30 @@ void PortalRemoteDesktop::handleSessionStarted(GObject *object, GAsyncResult *re
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
||||||
|
if (!xdp_session_is_clipboard_enabled(session)) {
|
||||||
|
LOG_WARN("clipboard not enabled on remote desktop session, discarding restore token to force a fresh session");
|
||||||
|
Settings::setValue(Settings::Client::XdpRestoreToken, QString());
|
||||||
|
free(m_sessionRestoreToken);
|
||||||
|
m_sessionRestoreToken = nullptr;
|
||||||
|
if (m_selectionTransferSignalId) {
|
||||||
|
g_signal_handler_disconnect(session, m_selectionTransferSignalId);
|
||||||
|
m_selectionTransferSignalId = 0;
|
||||||
|
}
|
||||||
|
if (m_selectionOwnerChangedSignalId) {
|
||||||
|
g_signal_handler_disconnect(session, m_selectionOwnerChangedSignalId);
|
||||||
|
m_selectionOwnerChangedSignalId = 0;
|
||||||
|
}
|
||||||
|
if (m_sessionSignalId) {
|
||||||
|
g_signal_handler_disconnect(session, m_sessionSignalId);
|
||||||
|
m_sessionSignalId = 0;
|
||||||
|
}
|
||||||
|
g_clear_object(&m_session);
|
||||||
|
reconnect(0);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
|
||||||
m_sessionRestoreToken = xdp_session_get_restore_token(session);
|
m_sessionRestoreToken = xdp_session_get_restore_token(session);
|
||||||
if (m_sessionRestoreToken) {
|
if (m_sessionRestoreToken) {
|
||||||
Settings::setValue(Settings::Client::XdpRestoreToken, QString(m_sessionRestoreToken));
|
Settings::setValue(Settings::Client::XdpRestoreToken, QString(m_sessionRestoreToken));
|
||||||
|
|
@ -135,6 +168,14 @@ void PortalRemoteDesktop::handleInitSession(GObject *object, GAsyncResult *res)
|
||||||
// the static function
|
// the static function
|
||||||
m_sessionSignalId = g_signal_connect(G_OBJECT(session), "closed", G_CALLBACK(handleSessionClosedCallback), this);
|
m_sessionSignalId = g_signal_connect(G_OBJECT(session), "closed", G_CALLBACK(handleSessionClosedCallback), this);
|
||||||
|
|
||||||
|
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
||||||
|
xdp_session_request_clipboard(session);
|
||||||
|
m_selectionTransferSignalId =
|
||||||
|
g_signal_connect(G_OBJECT(session), "selection-transfer", G_CALLBACK(selectionTransferCallback), this);
|
||||||
|
m_selectionOwnerChangedSignalId =
|
||||||
|
g_signal_connect(G_OBJECT(session), "selection-owner-changed", G_CALLBACK(selectionOwnerChangedCallback), this);
|
||||||
|
#endif
|
||||||
|
|
||||||
LOG_DEBUG("portal remote desktop session starting");
|
LOG_DEBUG("portal remote desktop session starting");
|
||||||
xdp_session_start(
|
xdp_session_start(
|
||||||
session,
|
session,
|
||||||
|
|
@ -178,4 +219,48 @@ void PortalRemoteDesktop::glibThread(const void *)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void PortalRemoteDesktop::claimClipboard()
|
||||||
|
{
|
||||||
|
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
||||||
|
if (!m_session) {
|
||||||
|
LOG_DEBUG("portal remote desktop clipboard claim deferred, no session yet");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (!xdp_session_is_clipboard_enabled(m_session)) {
|
||||||
|
LOG_WARN("portal remote desktop clipboard not enabled on session, cannot claim");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
PortalClipboard::claimOwnership(m_screen->getClipboardCache(), m_session);
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void PortalRemoteDesktop::handleSelectionTransfer(XdpSession *session, const char *mimeType, uint32_t serial)
|
||||||
|
{
|
||||||
|
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
||||||
|
PortalClipboard::serveSelectionTransfer(m_screen->getClipboardCache(), session, mimeType, serial);
|
||||||
|
#else
|
||||||
|
(void)session;
|
||||||
|
(void)mimeType;
|
||||||
|
(void)serial;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
|
void PortalRemoteDesktop::handleSelectionOwnerChanged(XdpSession *session, char **mimeTypes, gboolean isOwner)
|
||||||
|
{
|
||||||
|
#ifdef HAVE_LIBPORTAL_CLIPBOARD
|
||||||
|
if (isOwner) {
|
||||||
|
LOG_DEBUG("portal remote desktop selection owner changed, we own it, ignoring");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const qint64 maxBytes = static_cast<qint64>(m_screen->maximumClipboardSize()) * 1024;
|
||||||
|
if (PortalClipboard::readSelectionIntoCache(m_screen->getClipboardCache(), session, mimeTypes, maxBytes))
|
||||||
|
m_screen->sendClipboardEvent(EventTypes::ClipboardGrabbed, kClipboardClipboard);
|
||||||
|
#else
|
||||||
|
(void)session;
|
||||||
|
(void)mimeTypes;
|
||||||
|
(void)isOwner;
|
||||||
|
#endif
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace deskflow
|
} // namespace deskflow
|
||||||
|
|
|
||||||
|
|
@ -13,6 +13,8 @@
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
#include <libportal/portal.h>
|
#include <libportal/portal.h>
|
||||||
|
|
||||||
|
#include <QByteArray>
|
||||||
|
|
||||||
namespace deskflow {
|
namespace deskflow {
|
||||||
|
|
||||||
class PortalRemoteDesktop
|
class PortalRemoteDesktop
|
||||||
|
|
@ -21,6 +23,8 @@ public:
|
||||||
PortalRemoteDesktop(EiScreen *screen, IEventQueue *events);
|
PortalRemoteDesktop(EiScreen *screen, IEventQueue *events);
|
||||||
~PortalRemoteDesktop();
|
~PortalRemoteDesktop();
|
||||||
|
|
||||||
|
void claimClipboard();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void glibThread(const void *);
|
void glibThread(const void *);
|
||||||
gboolean timeoutHandler() const;
|
gboolean timeoutHandler() const;
|
||||||
|
|
@ -28,14 +32,25 @@ private:
|
||||||
void handleInitSession(GObject *object, GAsyncResult *res);
|
void handleInitSession(GObject *object, GAsyncResult *res);
|
||||||
void handleSessionStarted(GObject *object, GAsyncResult *res);
|
void handleSessionStarted(GObject *object, GAsyncResult *res);
|
||||||
void handleSessionClosed(XdpSession *session);
|
void handleSessionClosed(XdpSession *session);
|
||||||
|
void handleSelectionTransfer(XdpSession *session, const char *mimeType, uint32_t serial);
|
||||||
|
void handleSelectionOwnerChanged(XdpSession *session, char **mimeTypes, gboolean isOwner);
|
||||||
void reconnect(unsigned int timeout = 1000);
|
void reconnect(unsigned int timeout = 1000);
|
||||||
|
|
||||||
/// g_signal_connect callback wrapper
|
|
||||||
static void handleSessionClosedCallback(XdpSession *session, gpointer data)
|
static void handleSessionClosedCallback(XdpSession *session, gpointer data)
|
||||||
{
|
{
|
||||||
static_cast<PortalRemoteDesktop *>(data)->handleSessionClosed(session);
|
static_cast<PortalRemoteDesktop *>(data)->handleSessionClosed(session);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static void selectionTransferCallback(XdpSession *session, const char *mimeType, uint32_t serial, gpointer data)
|
||||||
|
{
|
||||||
|
static_cast<PortalRemoteDesktop *>(data)->handleSelectionTransfer(session, mimeType, serial);
|
||||||
|
}
|
||||||
|
|
||||||
|
static void selectionOwnerChangedCallback(XdpSession *session, char **mimeTypes, gboolean isOwner, gpointer data)
|
||||||
|
{
|
||||||
|
static_cast<PortalRemoteDesktop *>(data)->handleSelectionOwnerChanged(session, mimeTypes, isOwner);
|
||||||
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
EiScreen *m_screen;
|
EiScreen *m_screen;
|
||||||
IEventQueue *m_events;
|
IEventQueue *m_events;
|
||||||
|
|
@ -48,6 +63,8 @@ private:
|
||||||
char *m_sessionRestoreToken = nullptr;
|
char *m_sessionRestoreToken = nullptr;
|
||||||
|
|
||||||
guint m_sessionSignalId = 0;
|
guint m_sessionSignalId = 0;
|
||||||
|
guint m_selectionTransferSignalId = 0;
|
||||||
|
guint m_selectionOwnerChangedSignalId = 0;
|
||||||
|
|
||||||
/// The number of successful sessions we've had already
|
/// The number of successful sessions we've had already
|
||||||
guint m_sessionIteration = 0;
|
guint m_sessionIteration = 0;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue