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:
ksmithandClaude Sonnet 5 committed 2026-09-07 14:53:03 -06:00
1 parent 27cc3a3bb2
commit 793fdd9366
8 files changed
+654 -27

No files matched your search

+477 -25
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,24 +1972,47 @@ 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;
}
auto* dispContext = reinterpret_cast<DispClientContext*>(channelInterface);
if (dispContext == nullptr) {
if (std::strcmp(name, DISP_DVC_CHANNEL_NAME) == 0) {
auto* dispContext = reinterpret_cast<DispClientContext*>(channelInterface);
if (dispContext == nullptr) {
return;
}
dispContext->custom = this;
dispContext->DisplayControlCaps = orbitDisplayControlCaps;
{
std::lock_guard<std::mutex> guard(m_displayControlMutex);
m_displayControlContext = dispContext;
m_displayControlReady = false;
}
emit eventLogged(QStringLiteral("RDP display-control channel connected."));
return;
}
dispContext->custom = this;
dispContext->DisplayControlCaps = orbitDisplayControlCaps;
if (std::strcmp(name, CLIPRDR_SVC_CHANNEL_NAME) == 0) {
auto* cliprdrContext = reinterpret_cast<CliprdrClientContext*>(channelInterface);
if (cliprdrContext == nullptr) {
return;
}
{
std::lock_guard<std::mutex> guard(m_displayControlMutex);
m_displayControlContext = dispContext;
m_displayControlReady = false;
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."));
}
emit eventLogged(QStringLiteral("RDP display-control channel connected."));
#else
Q_UNUSED(name);
Q_UNUSED(channelInterface);
@@ -1713,24 +2022,43 @@ 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;
bool cleared = false;
bool hadDisplayControl = false;
{
std::lock_guard<std::mutex> guard(m_displayControlMutex);
hadDisplayControl = (m_displayControlContext != nullptr) || m_displayControlReady;
if (channelInterface == nullptr || m_displayControlContext == channelInterface) {
m_displayControlContext = nullptr;
m_displayControlReady = false;
cleared = true;
if (matchesDisp) {
bool cleared = false;
bool hadDisplayControl = false;
{
std::lock_guard<std::mutex> guard(m_displayControlMutex);
hadDisplayControl = (m_displayControlContext != nullptr) || m_displayControlReady;
if (channelInterface == nullptr || m_displayControlContext == channelInterface) {
m_displayControlContext = nullptr;
m_displayControlReady = false;
cleared = true;
}
}
if (cleared && hadDisplayControl) {
emit eventLogged(QStringLiteral("RDP display-control channel disconnected."));
}
}
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);
@@ -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(