RDP session fixes: correct keyboard scancodes, clipboard sync, cursor shapes

Fix RDP keyboard input using FreeRDP's authoritative X11-keycode-to-scancode
table instead of ad hoc bit math, which misread punctuation keys as unrelated
letter keys (e.g. apostrophe as B) because X11 keycode numbering only
coincidentally overlaps PC/AT scancodes.

Add bidirectional clipboard sync (CF_UNICODETEXT) over the cliprdr channel,
and RDP pointer/cursor shape sync so the local cursor reflects what the
remote OS wants displayed (resize handles, text I-beam, etc.) instead of
staying a static arrow.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-07 14:53:03 -06:00
co-authored by Claude Sonnet 5
parent 27cc3a3bb2
commit 793fdd9366
8 changed files with 654 additions and 27 deletions
+2 -1
View File
@@ -42,7 +42,8 @@ set(CHANNEL_DISP ON CACHE BOOL "" FORCE)
set(CHANNEL_DISP_CLIENT ON CACHE BOOL "" FORCE)
set(CHANNEL_AINPUT OFF CACHE BOOL "" FORCE)
set(CHANNEL_AUDIN OFF CACHE BOOL "" FORCE)
set(CHANNEL_CLIPRDR OFF CACHE BOOL "" FORCE)
set(CHANNEL_CLIPRDR ON CACHE BOOL "" FORCE)
set(CHANNEL_CLIPRDR_CLIENT ON CACHE BOOL "" FORCE)
set(CHANNEL_DRIVE OFF CACHE BOOL "" FORCE)
set(CHANNEL_ECHO OFF CACHE BOOL "" FORCE)
set(CHANNEL_ENCOMSP OFF CACHE BOOL "" FORCE)
+60 -1
View File
@@ -1,8 +1,10 @@
#include "rdp_display_widget.h"
#include <QCursor>
#include <QKeyEvent>
#include <QMouseEvent>
#include <QPainter>
#include <QPixmap>
#include <QResizeEvent>
#include <QTimer>
#include <QWheelEvent>
@@ -16,7 +18,7 @@ QSize sanitizeSize(const QSize& size)
}
RdpDisplayWidget::RdpDisplayWidget(QWidget* parent)
: QWidget(parent), m_remoteSize(1280, 720)
: QWidget(parent), m_remoteSize(1280, 720), m_cursorMode(CursorMode::Default)
{
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
@@ -61,6 +63,62 @@ void RdpDisplayWidget::clearFrame()
update();
}
void RdpDisplayWidget::setCursorImage(const QImage& image, const QPoint& hotspot)
{
m_cursorImage = image;
m_cursorHotspot = hotspot;
m_cursorMode = CursorMode::Custom;
applyCursor();
}
void RdpDisplayWidget::setCursorHidden()
{
m_cursorMode = CursorMode::Hidden;
applyCursor();
}
void RdpDisplayWidget::setCursorDefault()
{
m_cursorMode = CursorMode::Default;
applyCursor();
}
void RdpDisplayWidget::applyCursor()
{
if (m_cursorMode == CursorMode::Hidden) {
setCursor(Qt::BlankCursor);
return;
}
if (m_cursorMode == CursorMode::Default || m_cursorImage.isNull()) {
unsetCursor();
return;
}
const QSize remote = effectiveRemoteSize();
const QRectF target = renderRect();
if (remote.isEmpty() || target.isEmpty()) {
setCursor(QCursor(QPixmap::fromImage(m_cursorImage),
m_cursorHotspot.x(),
m_cursorHotspot.y()));
return;
}
const qreal scale = target.width() / remote.width();
QImage scaledImage = m_cursorImage;
if (!qFuzzyCompare(scale, 1.0)) {
scaledImage = m_cursorImage.scaled(
qMax(1, qRound(m_cursorImage.width() * scale)),
qMax(1, qRound(m_cursorImage.height() * scale)),
Qt::IgnoreAspectRatio,
Qt::SmoothTransformation);
}
const int hotX = qBound(0, qRound(m_cursorHotspot.x() * scale), scaledImage.width());
const int hotY = qBound(0, qRound(m_cursorHotspot.y() * scale), scaledImage.height());
setCursor(QCursor(QPixmap::fromImage(scaledImage), hotX, hotY));
}
void RdpDisplayWidget::paintEvent(QPaintEvent* event)
{
Q_UNUSED(event);
@@ -84,6 +142,7 @@ void RdpDisplayWidget::resizeEvent(QResizeEvent* event)
QWidget::resizeEvent(event);
const QSize size = sanitizeSize(event->size());
emit viewportSizeChanged(size.width(), size.height());
applyCursor();
}
void RdpDisplayWidget::keyPressEvent(QKeyEvent* event)
+13
View File
@@ -20,6 +20,9 @@ public:
void setFrame(const QImage& frame);
void setRemoteDesktopSize(int width, int height);
void clearFrame();
void setCursorImage(const QImage& image, const QPoint& hotspot);
void setCursorHidden();
void setCursorDefault();
signals:
void keyInput(int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers);
@@ -39,12 +42,22 @@ protected:
void wheelEvent(QWheelEvent* event) override;
private:
enum class CursorMode {
Default,
Custom,
Hidden,
};
QImage m_frame;
QSize m_remoteSize;
QImage m_cursorImage;
QPoint m_cursorHotspot;
CursorMode m_cursorMode;
QRectF renderRect() const;
QPoint mapToRemote(const QPointF& pos) const;
QSize effectiveRemoteSize() const;
void applyCursor();
};
#endif
+457 -5
View File
@@ -7,26 +7,33 @@
#include <cmath>
#include <cstring>
#include <new>
#include <utility>
#ifdef ORBITHUB_HAS_FREERDP
#include <freerdp/channels/channels.h>
#include <freerdp/channels/cliprdr.h>
#include <freerdp/channels/drdynvc.h>
#include <freerdp/channels/disp.h>
#include <freerdp/client/cliprdr.h>
#include <freerdp/addin.h>
#include <freerdp/client/channels.h>
#include <freerdp/client/cmdline.h>
#include <freerdp/client/disp.h>
#include <freerdp/codec/color.h>
#include <freerdp/display.h>
#include <freerdp/error.h>
#include <freerdp/event.h>
#include <freerdp/freerdp.h>
#include <freerdp/gdi/gdi.h>
#include <freerdp/graphics.h>
#include <freerdp/input.h>
#include <freerdp/locale/keyboard.h>
#include <freerdp/scancode.h>
#include <freerdp/settings.h>
#include <winpr/crt.h>
#include <winpr/synch.h>
#include <winpr/user.h>
#endif
namespace {
@@ -146,6 +153,16 @@ UINT orbitDisplayControlCaps(DispClientContext* context,
UINT32 maxNumMonitors,
UINT32 maxMonitorAreaFactorA,
UINT32 maxMonitorAreaFactorB);
UINT orbitCliprdrMonitorReady(CliprdrClientContext* context,
const CLIPRDR_MONITOR_READY* monitorReady);
UINT orbitCliprdrServerFormatList(CliprdrClientContext* context,
const CLIPRDR_FORMAT_LIST* formatList);
UINT orbitCliprdrServerFormatListResponse(
CliprdrClientContext* context, const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse);
UINT orbitCliprdrServerFormatDataRequest(
CliprdrClientContext* context, const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest);
UINT orbitCliprdrServerFormatDataResponse(
CliprdrClientContext* context, const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse);
BOOL orbitLoadChannels(freerdp* instance);
bool orbitLoadStaticChannel(rdpChannels* channels,
rdpSettings* settings,
@@ -246,6 +263,118 @@ BOOL orbitDesktopResize(rdpContext* context)
return TRUE;
}
// Custom rdpPointer subclass: FreeRDP allocates this with calloc() and only
// ever touches the base rdpPointer fields itself, so the trailing fields here
// must stay plain-old-data and be managed manually in New()/Free() (mirrors
// FreeRDP's own X11 client, client/X11/xf_graphics.c).
struct OrbitRdpPointer
{
rdpPointer pointer;
uchar* rgbaData;
UINT32 rgbaWidth;
UINT32 rgbaHeight;
};
BOOL orbitPointerNew(rdpContext* context, rdpPointer* pointer)
{
if (context == nullptr || pointer == nullptr || context->gdi == nullptr) {
return FALSE;
}
auto* orbitPointer = reinterpret_cast<OrbitRdpPointer*>(pointer);
orbitPointer->rgbaData = nullptr;
orbitPointer->rgbaWidth = 0;
orbitPointer->rgbaHeight = 0;
if (pointer->width == 0 || pointer->height == 0) {
return TRUE;
}
const size_t size = static_cast<size_t>(pointer->width) * pointer->height * 4;
auto* buffer = new (std::nothrow) uchar[size];
if (buffer == nullptr) {
return FALSE;
}
if (!freerdp_image_copy_from_pointer_data(buffer,
PIXEL_FORMAT_BGRA32,
0,
0,
0,
pointer->width,
pointer->height,
pointer->xorMaskData,
pointer->lengthXorMask,
pointer->andMaskData,
pointer->lengthAndMask,
pointer->xorBpp,
&context->gdi->palette)) {
delete[] buffer;
return FALSE;
}
orbitPointer->rgbaData = buffer;
orbitPointer->rgbaWidth = pointer->width;
orbitPointer->rgbaHeight = pointer->height;
return TRUE;
}
void orbitPointerFree(rdpContext* context, rdpPointer* pointer)
{
Q_UNUSED(context);
if (pointer == nullptr) {
return;
}
auto* orbitPointer = reinterpret_cast<OrbitRdpPointer*>(pointer);
delete[] orbitPointer->rgbaData;
orbitPointer->rgbaData = nullptr;
}
BOOL orbitPointerSet(rdpContext* context, rdpPointer* pointer)
{
if (context == nullptr || pointer == nullptr) {
return FALSE;
}
RdpSessionBackend* backend = backendFromContext(context);
if (backend == nullptr) {
return TRUE;
}
auto* orbitPointer = reinterpret_cast<OrbitRdpPointer*>(pointer);
if (orbitPointer->rgbaData == nullptr || orbitPointer->rgbaWidth == 0
|| orbitPointer->rgbaHeight == 0) {
return TRUE;
}
const QImage image(orbitPointer->rgbaData,
static_cast<int>(orbitPointer->rgbaWidth),
static_cast<int>(orbitPointer->rgbaHeight),
QImage::Format_ARGB32);
emit backend->cursorImageChanged(
image.copy(),
QPoint(static_cast<int>(pointer->xPos), static_cast<int>(pointer->yPos)));
return TRUE;
}
BOOL orbitPointerSetNull(rdpContext* context)
{
if (RdpSessionBackend* backend = backendFromContext(context)) {
emit backend->cursorHidden();
}
return TRUE;
}
BOOL orbitPointerSetDefault(rdpContext* context)
{
if (RdpSessionBackend* backend = backendFromContext(context)) {
emit backend->cursorReset();
}
return TRUE;
}
BOOL orbitPreConnect(freerdp* instance)
{
if (instance == nullptr || instance->context == nullptr || instance->context->settings == nullptr) {
@@ -274,6 +403,17 @@ BOOL orbitPostConnect(freerdp* instance)
context->update->EndPaint = orbitEndPaint;
context->update->DesktopResize = orbitDesktopResize;
if (context->graphics != nullptr) {
rdpPointer pointerCallbacks = {};
pointerCallbacks.size = sizeof(OrbitRdpPointer);
pointerCallbacks.New = orbitPointerNew;
pointerCallbacks.Free = orbitPointerFree;
pointerCallbacks.Set = orbitPointerSet;
pointerCallbacks.SetNull = orbitPointerSetNull;
pointerCallbacks.SetDefault = orbitPointerSetDefault;
graphics_register_pointer(context->graphics, &pointerCallbacks);
}
if (RdpSessionBackend* backend = backendFromContext(context)) {
const int width = static_cast<int>(
freerdp_settings_get_uint32(context->settings, FreeRDP_DesktopWidth));
@@ -291,6 +431,7 @@ void orbitPostDisconnect(freerdp* instance)
if (instance->context != nullptr) {
if (RdpSessionBackend* backend = backendFromContext(instance->context)) {
backend->onChannelDisconnectedEvent(DISP_DVC_CHANNEL_NAME, nullptr);
backend->onChannelDisconnectedEvent(CLIPRDR_SVC_CHANNEL_NAME, nullptr);
}
PubSub_UnsubscribeChannelConnected(instance->context->pubSub,
orbitOnChannelConnectedEventHandler);
@@ -337,6 +478,75 @@ UINT orbitDisplayControlCaps(DispClientContext* context,
return CHANNEL_RC_OK;
}
UINT orbitCliprdrMonitorReady(CliprdrClientContext* context,
const CLIPRDR_MONITOR_READY* monitorReady)
{
Q_UNUSED(monitorReady);
if (context == nullptr || context->custom == nullptr) {
return CHANNEL_RC_OK;
}
auto* backend = reinterpret_cast<RdpSessionBackend*>(context->custom);
backend->onCliprdrMonitorReady();
return CHANNEL_RC_OK;
}
UINT orbitCliprdrServerFormatList(CliprdrClientContext* context,
const CLIPRDR_FORMAT_LIST* formatList)
{
if (context == nullptr || context->custom == nullptr || formatList == nullptr) {
return CHANNEL_RC_OK;
}
bool hasUnicodeText = false;
for (UINT32 i = 0; i < formatList->numFormats; ++i) {
if (formatList->formats[i].formatId == CF_UNICODETEXT) {
hasUnicodeText = true;
break;
}
}
auto* backend = reinterpret_cast<RdpSessionBackend*>(context->custom);
backend->onCliprdrServerFormatList(hasUnicodeText);
return CHANNEL_RC_OK;
}
UINT orbitCliprdrServerFormatListResponse(
CliprdrClientContext* context, const CLIPRDR_FORMAT_LIST_RESPONSE* formatListResponse)
{
Q_UNUSED(context);
Q_UNUSED(formatListResponse);
return CHANNEL_RC_OK;
}
UINT orbitCliprdrServerFormatDataRequest(
CliprdrClientContext* context, const CLIPRDR_FORMAT_DATA_REQUEST* formatDataRequest)
{
if (context == nullptr || context->custom == nullptr || formatDataRequest == nullptr) {
return CHANNEL_RC_OK;
}
auto* backend = reinterpret_cast<RdpSessionBackend*>(context->custom);
backend->onCliprdrServerFormatDataRequest(formatDataRequest->requestedFormatId);
return CHANNEL_RC_OK;
}
UINT orbitCliprdrServerFormatDataResponse(
CliprdrClientContext* context, const CLIPRDR_FORMAT_DATA_RESPONSE* formatDataResponse)
{
if (context == nullptr || context->custom == nullptr || formatDataResponse == nullptr) {
return CHANNEL_RC_OK;
}
auto* backend = reinterpret_cast<RdpSessionBackend*>(context->custom);
const bool success = (formatDataResponse->common.msgFlags & CB_RESPONSE_OK) != 0;
backend->onCliprdrServerFormatDataResponse(
success,
reinterpret_cast<const uint8_t*>(formatDataResponse->requestedFormatData),
formatDataResponse->common.dataLen);
return CHANNEL_RC_OK;
}
BOOL orbitLoadChannels(freerdp* instance)
{
if (instance == nullptr || instance->context == nullptr || instance->context->settings == nullptr
@@ -383,6 +593,14 @@ BOOL orbitLoadChannels(freerdp* instance)
return TRUE;
}
if (!orbitLoadStaticChannel(channels, settings, CLIPRDR_SVC_CHANNEL_NAME, settings)) {
if (backend != nullptr) {
emit backend->eventLogged(
QStringLiteral("RDP warning: failed to load static '%1' channel; clipboard sync unavailable.")
.arg(QString::fromUtf8(CLIPRDR_SVC_CHANNEL_NAME)));
}
}
if (backend != nullptr) {
emit backend->eventLogged(QStringLiteral("RDP channel loader: display-control channels initialized."));
}
@@ -576,6 +794,25 @@ UINT32 scancodeFromNativeScanCode(quint32 nativeScanCode)
return RDP_SCANCODE_UNKNOWN;
}
#if defined(Q_OS_LINUX)
// On X11, Qt's nativeScanCode() is the raw X11 KeyCode, not a PC/AT set-1
// scancode. X11 keycode numbering only coincidentally overlaps with PC/AT
// scancodes for some keys and diverges for others (e.g. X11 keycode 0x30
// is the apostrophe/quote key, while PC/AT scancode 0x30 is the B key), so
// treating one as the other silently sends the wrong key. FreeRDP ships an
// authoritative X11-keycode -> RDP-scancode table; use it directly.
// The function is marked deprecated upstream ("implement yourself in
// client") but remains present and correct in this pinned FreeRDP build.
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic push
#pragma GCC diagnostic ignored "-Wdeprecated-declarations"
#endif
const DWORD mapped = freerdp_keyboard_get_rdp_scancode_from_x11_keycode(nativeScanCode);
#if defined(__GNUC__) || defined(__clang__)
#pragma GCC diagnostic pop
#endif
return mapped != 0 ? mapped : RDP_SCANCODE_UNKNOWN;
#else
quint32 code = 0;
bool extended = false;
if ((nativeScanCode & 0xFF000000u) == 0xE0000000u) {
@@ -607,6 +844,7 @@ UINT32 scancodeFromNativeScanCode(quint32 nativeScanCode)
}
return scancode;
#endif
}
UINT32 scancodeForQtKey(int key, Qt::KeyboardModifiers modifiers, quint32 nativeScanCode)
@@ -1001,7 +1239,8 @@ RdpSessionBackend::RdpSessionBackend(const Profile& profile, QObject* parent)
m_displayControlReady(false),
m_resizeFailureLogged(false),
m_lastResizeWidth(0),
m_lastResizeHeight(0)
m_lastResizeHeight(0),
m_cliprdrContext(nullptr)
{
}
@@ -1112,6 +1351,18 @@ void RdpSessionBackend::sendKeyEvent(int key,
enqueueInputEvent(event);
}
void RdpSessionBackend::setClipboardText(const QString& text)
{
if (!m_workerRunning.load()) {
return;
}
InputEvent event;
event.type = InputEventType::SetClipboardText;
event.text = text;
enqueueInputEvent(event);
}
void RdpSessionBackend::sendMouseMoveEvent(int x, int y)
{
if (!m_workerRunning.load()) {
@@ -1537,6 +1788,38 @@ bool RdpSessionBackend::sendDisplayResize(rdp_freerdp* instance, int width, int
#endif
}
void RdpSessionBackend::sendClipboardTextToRemote(rdp_freerdp* instance, const QString& text)
{
#ifndef ORBITHUB_HAS_FREERDP
Q_UNUSED(instance);
Q_UNUSED(text);
#else
Q_UNUSED(instance);
CliprdrClientContext* cliprdrContext = nullptr;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
m_pendingLocalClipboardText = text;
cliprdrContext = reinterpret_cast<CliprdrClientContext*>(m_cliprdrContext);
}
if (cliprdrContext == nullptr || cliprdrContext->ClientFormatList == nullptr) {
return;
}
CLIPRDR_FORMAT format = {};
format.formatId = CF_UNICODETEXT;
format.formatName = nullptr;
CLIPRDR_FORMAT_LIST formatList = {};
formatList.numFormats = 1;
formatList.formats = &format;
const UINT rc = cliprdrContext->ClientFormatList(cliprdrContext, &formatList);
Q_UNUSED(rc);
#endif
}
void RdpSessionBackend::processInputEvents(rdp_freerdp* instance)
{
#ifndef ORBITHUB_HAS_FREERDP
@@ -1661,6 +1944,9 @@ void RdpSessionBackend::processInputEvents(rdp_freerdp* instance)
}
break;
}
case InputEventType::SetClipboardText:
sendClipboardTextToRemote(instance, event.text);
break;
}
}
@@ -1686,10 +1972,11 @@ void RdpSessionBackend::processInputEvents(rdp_freerdp* instance)
void RdpSessionBackend::onChannelConnectedEvent(const char* name, void* channelInterface)
{
#ifdef ORBITHUB_HAS_FREERDP
if (name == nullptr || std::strcmp(name, DISP_DVC_CHANNEL_NAME) != 0) {
if (name == nullptr) {
return;
}
if (std::strcmp(name, DISP_DVC_CHANNEL_NAME) == 0) {
auto* dispContext = reinterpret_cast<DispClientContext*>(channelInterface);
if (dispContext == nullptr) {
return;
@@ -1704,6 +1991,28 @@ void RdpSessionBackend::onChannelConnectedEvent(const char* name, void* channelI
m_displayControlReady = false;
}
emit eventLogged(QStringLiteral("RDP display-control channel connected."));
return;
}
if (std::strcmp(name, CLIPRDR_SVC_CHANNEL_NAME) == 0) {
auto* cliprdrContext = reinterpret_cast<CliprdrClientContext*>(channelInterface);
if (cliprdrContext == nullptr) {
return;
}
cliprdrContext->custom = this;
cliprdrContext->MonitorReady = orbitCliprdrMonitorReady;
cliprdrContext->ServerFormatList = orbitCliprdrServerFormatList;
cliprdrContext->ServerFormatListResponse = orbitCliprdrServerFormatListResponse;
cliprdrContext->ServerFormatDataRequest = orbitCliprdrServerFormatDataRequest;
cliprdrContext->ServerFormatDataResponse = orbitCliprdrServerFormatDataResponse;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
m_cliprdrContext = cliprdrContext;
}
emit eventLogged(QStringLiteral("RDP clipboard channel connected."));
}
#else
Q_UNUSED(name);
Q_UNUSED(channelInterface);
@@ -1713,10 +2022,11 @@ void RdpSessionBackend::onChannelConnectedEvent(const char* name, void* channelI
void RdpSessionBackend::onChannelDisconnectedEvent(const char* name, void* channelInterface)
{
#ifdef ORBITHUB_HAS_FREERDP
if (name != nullptr && std::strcmp(name, DISP_DVC_CHANNEL_NAME) != 0) {
return;
}
const bool matchesDisp = name == nullptr || std::strcmp(name, DISP_DVC_CHANNEL_NAME) == 0;
const bool matchesCliprdr =
name == nullptr || std::strcmp(name, CLIPRDR_SVC_CHANNEL_NAME) == 0;
if (matchesDisp) {
bool cleared = false;
bool hadDisplayControl = false;
{
@@ -1732,6 +2042,24 @@ void RdpSessionBackend::onChannelDisconnectedEvent(const char* name, void* chann
if (cleared && hadDisplayControl) {
emit eventLogged(QStringLiteral("RDP display-control channel disconnected."));
}
}
if (matchesCliprdr) {
bool cleared = false;
bool hadCliprdr = false;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
hadCliprdr = m_cliprdrContext != nullptr;
if (channelInterface == nullptr || m_cliprdrContext == channelInterface) {
m_cliprdrContext = nullptr;
cleared = true;
}
}
if (cleared && hadCliprdr) {
emit eventLogged(QStringLiteral("RDP clipboard channel disconnected."));
}
}
#else
Q_UNUSED(name);
Q_UNUSED(channelInterface);
@@ -1769,6 +2097,130 @@ void RdpSessionBackend::onDisplayControlCaps(uint32_t maxNumMonitors,
#endif
}
void RdpSessionBackend::onCliprdrMonitorReady()
{
#ifdef ORBITHUB_HAS_FREERDP
CliprdrClientContext* cliprdrContext = nullptr;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
cliprdrContext = reinterpret_cast<CliprdrClientContext*>(m_cliprdrContext);
}
if (cliprdrContext == nullptr) {
return;
}
CLIPRDR_GENERAL_CAPABILITY_SET generalCapabilitySet = {};
generalCapabilitySet.capabilitySetType = CB_CAPSTYPE_GENERAL;
generalCapabilitySet.capabilitySetLength = CB_CAPSTYPE_GENERAL_LEN;
generalCapabilitySet.version = CB_CAPS_VERSION_2;
generalCapabilitySet.generalFlags = CB_USE_LONG_FORMAT_NAMES;
CLIPRDR_CAPABILITIES capabilities = {};
capabilities.cCapabilitiesSets = 1;
capabilities.capabilitySets = reinterpret_cast<CLIPRDR_CAPABILITY_SET*>(&generalCapabilitySet);
if (cliprdrContext->ClientCapabilities != nullptr) {
const UINT rc = cliprdrContext->ClientCapabilities(cliprdrContext, &capabilities);
Q_UNUSED(rc);
}
emit eventLogged(QStringLiteral("RDP clipboard channel ready."));
#endif
}
void RdpSessionBackend::onCliprdrServerFormatList(bool hasUnicodeText)
{
#ifdef ORBITHUB_HAS_FREERDP
CliprdrClientContext* cliprdrContext = nullptr;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
cliprdrContext = reinterpret_cast<CliprdrClientContext*>(m_cliprdrContext);
}
if (cliprdrContext == nullptr) {
return;
}
if (cliprdrContext->ClientFormatListResponse != nullptr) {
CLIPRDR_FORMAT_LIST_RESPONSE response = {};
response.common.msgFlags = CB_RESPONSE_OK;
const UINT rc = cliprdrContext->ClientFormatListResponse(cliprdrContext, &response);
Q_UNUSED(rc);
}
if (hasUnicodeText && cliprdrContext->ClientFormatDataRequest != nullptr) {
CLIPRDR_FORMAT_DATA_REQUEST request = {};
request.requestedFormatId = CF_UNICODETEXT;
const UINT rc = cliprdrContext->ClientFormatDataRequest(cliprdrContext, &request);
Q_UNUSED(rc);
}
#else
Q_UNUSED(hasUnicodeText);
#endif
}
void RdpSessionBackend::onCliprdrServerFormatDataRequest(uint32_t requestedFormatId)
{
#ifdef ORBITHUB_HAS_FREERDP
CliprdrClientContext* cliprdrContext = nullptr;
QString pendingText;
{
std::lock_guard<std::mutex> guard(m_cliprdrMutex);
cliprdrContext = reinterpret_cast<CliprdrClientContext*>(m_cliprdrContext);
pendingText = m_pendingLocalClipboardText;
}
if (cliprdrContext == nullptr || cliprdrContext->ClientFormatDataResponse == nullptr) {
return;
}
CLIPRDR_FORMAT_DATA_RESPONSE response = {};
QByteArray encoded;
if (requestedFormatId == CF_UNICODETEXT) {
const int byteLength = (pendingText.size() + 1) * static_cast<int>(sizeof(ushort));
encoded = QByteArray(reinterpret_cast<const char*>(pendingText.utf16()), byteLength);
}
response.common.msgFlags = encoded.isEmpty() ? CB_RESPONSE_FAIL : CB_RESPONSE_OK;
response.common.dataLen = static_cast<UINT32>(encoded.size());
response.requestedFormatData =
encoded.isEmpty() ? nullptr : reinterpret_cast<const BYTE*>(encoded.constData());
const UINT rc = cliprdrContext->ClientFormatDataResponse(cliprdrContext, &response);
Q_UNUSED(rc);
#else
Q_UNUSED(requestedFormatId);
#endif
}
void RdpSessionBackend::onCliprdrServerFormatDataResponse(bool success,
const uint8_t* data,
uint32_t size)
{
#ifdef ORBITHUB_HAS_FREERDP
if (!success || data == nullptr || size == 0) {
return;
}
const auto* utf16Data = reinterpret_cast<const char16_t*>(data);
const size_t charCount = size / sizeof(char16_t);
// CF_UNICODETEXT is NUL-terminated per MS-RDPECLIP; trim at the first NUL
// rather than trusting dataLen to exclude it.
size_t length = 0;
while (length < charCount && utf16Data[length] != 0) {
++length;
}
const QString text = QString::fromUtf16(utf16Data, static_cast<int>(length));
emit remoteClipboardTextChanged(text);
#else
Q_UNUSED(success);
Q_UNUSED(data);
Q_UNUSED(size);
#endif
}
void RdpSessionBackend::emitStateAsync(SessionState state, const QString& message)
{
QMetaObject::invokeMethod(
+11
View File
@@ -34,6 +34,7 @@ public slots:
void sendMouseMoveEvent(int x, int y) override;
void sendMouseButtonEvent(int x, int y, int button, bool pressed) override;
void sendMouseWheelEvent(int x, int y, int deltaX, int deltaY) override;
void setClipboardText(const QString& text) override;
private:
enum class InputEventType {
@@ -42,6 +43,7 @@ private:
MouseButton,
MouseWheel,
Resize,
SetClipboardText,
};
struct InputEvent {
@@ -84,6 +86,10 @@ private:
int m_lastResizeWidth;
int m_lastResizeHeight;
std::mutex m_cliprdrMutex;
void* m_cliprdrContext;
QString m_pendingLocalClipboardText;
void setState(SessionState state, const QString& message);
bool validateProfile(QString& message) const;
void startWorker();
@@ -92,12 +98,17 @@ private:
void enqueueInputEvent(const InputEvent& event);
void processInputEvents(rdp_freerdp* instance);
bool sendDisplayResize(rdp_freerdp* instance, int width, int height);
void sendClipboardTextToRemote(rdp_freerdp* instance, const QString& text);
public:
void onChannelConnectedEvent(const char* name, void* channelInterface);
void onChannelDisconnectedEvent(const char* name, void* channelInterface);
void onDisplayControlCaps(uint32_t maxNumMonitors,
uint32_t maxMonitorAreaFactorA,
uint32_t maxMonitorAreaFactorB);
void onCliprdrMonitorReady();
void onCliprdrServerFormatList(bool hasUnicodeText);
void onCliprdrServerFormatDataRequest(uint32_t requestedFormatId);
void onCliprdrServerFormatDataResponse(bool success, const uint8_t* data, uint32_t size);
private:
void emitStateAsync(SessionState state, const QString& message);
void emitConnectionFailureAsync(const QString& displayMessage, const QString& rawMessage);
+9
View File
@@ -5,6 +5,7 @@
#include <QImage>
#include <QObject>
#include <QPoint>
#include <QString>
#include <QtGlobal>
@@ -46,6 +47,10 @@ public slots:
virtual void sendInput(const QString& input) = 0;
virtual void confirmHostKey(bool trustHost) = 0;
virtual void updateTerminalSize(int columns, int rows) = 0;
virtual void setClipboardText(const QString& text)
{
Q_UNUSED(text);
}
virtual void sendKeyEvent(int key,
quint32 nativeScanCode,
const QString& text,
@@ -86,6 +91,10 @@ signals:
void hostKeyConfirmationRequested(const QString& prompt);
void frameUpdated(const QImage& frame);
void remoteDesktopSizeChanged(int width, int height);
void remoteClipboardTextChanged(const QString& text);
void cursorImageChanged(const QImage& image, const QPoint& hotspot);
void cursorHidden();
void cursorReset();
private:
Profile m_profile;
+77
View File
@@ -19,6 +19,7 @@
#include <QPlainTextEdit>
#include <QApplication>
#include <QClipboard>
#include <QMimeData>
#include <QComboBox>
#include <QProcessEnvironment>
#include <QPushButton>
@@ -74,6 +75,8 @@ SessionTab::SessionTab(const Profile& profile,
m_terminalFontPointSize(preferences.terminalFontPointSize > 0
? preferences.terminalFontPointSize
: 0),
m_clipboardSyncSupported(profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive)
== 0),
m_sshTerminal(nullptr),
m_rdpDisplay(nullptr),
m_terminalOutput(nullptr),
@@ -193,6 +196,11 @@ SessionTab::SessionTab(const Profile& profile,
m_backend,
&SessionBackend::sendMouseWheelEvent,
Qt::QueuedConnection);
connect(this,
&SessionTab::requestSetClipboardText,
m_backend,
&SessionBackend::setClipboardText,
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::stateChanged,
@@ -237,10 +245,49 @@ SessionTab::SessionTab(const Profile& profile,
}
},
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::remoteClipboardTextChanged,
this,
&SessionTab::onBackendRemoteClipboardTextChanged,
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::cursorImageChanged,
this,
[this](const QImage& image, const QPoint& hotspot) {
if (m_rdpDisplay != nullptr) {
m_rdpDisplay->setCursorImage(image, hotspot);
}
},
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::cursorHidden,
this,
[this]() {
if (m_rdpDisplay != nullptr) {
m_rdpDisplay->setCursorHidden();
}
},
Qt::QueuedConnection);
connect(m_backend,
&SessionBackend::cursorReset,
this,
[this]() {
if (m_rdpDisplay != nullptr) {
m_rdpDisplay->setCursorDefault();
}
},
Qt::QueuedConnection);
m_backendThread->start();
}
if (m_clipboardSyncSupported) {
connect(QApplication::clipboard(),
&QClipboard::dataChanged,
this,
&SessionTab::onSystemClipboardChanged);
}
setState(SessionState::Disconnected, QStringLiteral("Ready to connect."));
QTimer::singleShot(0, this, &SessionTab::connectSession);
}
@@ -615,6 +662,36 @@ void SessionTab::onBackendHostKeyConfirmationRequested(const QString& prompt)
emit requestHostKeyConfirmation(reply == QMessageBox::Yes);
}
void SessionTab::onBackendRemoteClipboardTextChanged(const QString& text)
{
if (text == m_lastSyncedClipboardText) {
return;
}
m_lastSyncedClipboardText = text;
QApplication::clipboard()->setText(text);
}
void SessionTab::onSystemClipboardChanged()
{
if (!m_clipboardSyncSupported || m_state != SessionState::Connected) {
return;
}
const QClipboard* clipboard = QApplication::clipboard();
if (!clipboard->mimeData()->hasText()) {
return;
}
const QString text = clipboard->text();
if (text == m_lastSyncedClipboardText) {
return;
}
m_lastSyncedClipboardText = text;
emit requestSetClipboardText(text);
}
void SessionTab::setupUi()
{
auto* rootLayout = new QVBoxLayout(this);
+5
View File
@@ -82,6 +82,7 @@ signals:
void requestMouseMoveEvent(int x, int y);
void requestMouseButtonEvent(int x, int y, int button, bool pressed);
void requestMouseWheelEvent(int x, int y, int deltaX, int deltaY);
void requestSetClipboardText(const QString& text);
private slots:
void onBackendStateChanged(SessionState state, const QString& message);
@@ -89,6 +90,8 @@ private slots:
void onBackendConnectionError(const QString& displayMessage, const QString& rawMessage);
void onBackendOutputReceived(const QString& text);
void onBackendHostKeyConfirmationRequested(const QString& prompt);
void onBackendRemoteClipboardTextChanged(const QString& text);
void onSystemClipboardChanged();
private:
Profile m_profile;
@@ -100,6 +103,8 @@ private:
SessionConnectOptions m_lastConnectOptions;
QString m_terminalThemeName;
int m_terminalFontPointSize;
QString m_lastSyncedClipboardText;
bool m_clipboardSyncSupported;
KodoTerm* m_sshTerminal;
RdpDisplayWidget* m_rdpDisplay;