From 793fdd936625c3f0d2e077618941afd24f81a118 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Mon, 7 Sep 2026 14:53:03 -0600 Subject: [PATCH] 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 --- CMakeLists.txt | 3 +- src/rdp_display_widget.cpp | 61 ++++- src/rdp_display_widget.h | 13 + src/rdp_session_backend.cpp | 502 ++++++++++++++++++++++++++++++++++-- src/rdp_session_backend.h | 11 + src/session_backend.h | 9 + src/session_tab.cpp | 77 ++++++ src/session_tab.h | 5 + 8 files changed, 654 insertions(+), 27 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 2260068..f8e3387 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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) diff --git a/src/rdp_display_widget.cpp b/src/rdp_display_widget.cpp index 1d9c62f..121e6f7 100644 --- a/src/rdp_display_widget.cpp +++ b/src/rdp_display_widget.cpp @@ -1,8 +1,10 @@ #include "rdp_display_widget.h" +#include #include #include #include +#include #include #include #include @@ -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) diff --git a/src/rdp_display_widget.h b/src/rdp_display_widget.h index b7372f4..d9861d1 100644 --- a/src/rdp_display_widget.h +++ b/src/rdp_display_widget.h @@ -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 diff --git a/src/rdp_session_backend.cpp b/src/rdp_session_backend.cpp index 2118894..44b7822 100644 --- a/src/rdp_session_backend.cpp +++ b/src/rdp_session_backend.cpp @@ -7,26 +7,33 @@ #include #include +#include #include #ifdef ORBITHUB_HAS_FREERDP #include +#include #include #include +#include #include #include #include #include +#include #include #include #include #include #include +#include #include +#include #include #include #include #include +#include #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(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(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(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(pointer); + if (orbitPointer->rgbaData == nullptr || orbitPointer->rgbaWidth == 0 + || orbitPointer->rgbaHeight == 0) { + return TRUE; + } + + const QImage image(orbitPointer->rgbaData, + static_cast(orbitPointer->rgbaWidth), + static_cast(orbitPointer->rgbaHeight), + QImage::Format_ARGB32); + + emit backend->cursorImageChanged( + image.copy(), + QPoint(static_cast(pointer->xPos), static_cast(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( 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(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(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(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(context->custom); + const bool success = (formatDataResponse->common.msgFlags & CB_RESPONSE_OK) != 0; + backend->onCliprdrServerFormatDataResponse( + success, + reinterpret_cast(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 guard(m_cliprdrMutex); + m_pendingLocalClipboardText = text; + cliprdrContext = reinterpret_cast(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(channelInterface); - if (dispContext == nullptr) { + if (std::strcmp(name, DISP_DVC_CHANNEL_NAME) == 0) { + auto* dispContext = reinterpret_cast(channelInterface); + if (dispContext == nullptr) { + return; + } + + dispContext->custom = this; + dispContext->DisplayControlCaps = orbitDisplayControlCaps; + + { + std::lock_guard 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(channelInterface); + if (cliprdrContext == nullptr) { + return; + } - { - std::lock_guard 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 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 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 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 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 guard(m_cliprdrMutex); + cliprdrContext = reinterpret_cast(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(&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 guard(m_cliprdrMutex); + cliprdrContext = reinterpret_cast(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 guard(m_cliprdrMutex); + cliprdrContext = reinterpret_cast(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(sizeof(ushort)); + encoded = QByteArray(reinterpret_cast(pendingText.utf16()), byteLength); + } + + response.common.msgFlags = encoded.isEmpty() ? CB_RESPONSE_FAIL : CB_RESPONSE_OK; + response.common.dataLen = static_cast(encoded.size()); + response.requestedFormatData = + encoded.isEmpty() ? nullptr : reinterpret_cast(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(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(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( diff --git a/src/rdp_session_backend.h b/src/rdp_session_backend.h index 1484205..1153ad4 100644 --- a/src/rdp_session_backend.h +++ b/src/rdp_session_backend.h @@ -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); diff --git a/src/session_backend.h b/src/session_backend.h index 779be4c..15f2c6d 100644 --- a/src/session_backend.h +++ b/src/session_backend.h @@ -5,6 +5,7 @@ #include #include +#include #include #include @@ -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; diff --git a/src/session_tab.cpp b/src/session_tab.cpp index 1617173..5c3c8f8 100644 --- a/src/session_tab.cpp +++ b/src/session_tab.cpp @@ -19,6 +19,7 @@ #include #include #include +#include #include #include #include @@ -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); diff --git a/src/session_tab.h b/src/session_tab.h index 556b4aa..9a6741f 100644 --- a/src/session_tab.h +++ b/src/session_tab.h @@ -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;