Internal
Public Access
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:
+2
-1
@@ -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)
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
+477
-25
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user