Internal
Public Access
Add VNC remote cursor shape sync
Implements RFB's Cursor pseudo-encoding (RFC 6143 SS7.8.2, type -239): a FramebufferUpdate rectangle carrying a cursor shape instead of screen content (x/y are the hotspot, not position; width/height are the cursor image size), decoded into an ARGB32 QImage using the rectangle's RGB pixel data plus its opacity bitmask, then never painted into the framebuffer. A 0x0 rectangle means "hide the cursor" per spec. VncDisplayWidget gains RdpDisplayWidget's setCursorImage/Hidden/ Default() + applyCursor() shape, reusing its own renderRect()/ effectiveRemoteSize() so cursor scaling works correctly in both the scale-to-fit and actual-size display modes with no special-casing. VNC never emits cursorReset() (RFB's Cursor pseudo-encoding has no "reset to system default" signal, unlike RDP's SetDefault callback) -- setCursorDefault() exists for symmetry but is unused today. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -274,6 +274,8 @@ SessionTab::SessionTab(const Profile& profile,
|
||||
[this](const QImage& image, const QPoint& hotspot) {
|
||||
if (m_rdpDisplay != nullptr) {
|
||||
m_rdpDisplay->setCursorImage(image, hotspot);
|
||||
} else if (m_vncDisplay != nullptr) {
|
||||
m_vncDisplay->setCursorImage(image, hotspot);
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
@@ -283,6 +285,8 @@ SessionTab::SessionTab(const Profile& profile,
|
||||
[this]() {
|
||||
if (m_rdpDisplay != nullptr) {
|
||||
m_rdpDisplay->setCursorHidden();
|
||||
} else if (m_vncDisplay != nullptr) {
|
||||
m_vncDisplay->setCursorHidden();
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
@@ -292,6 +296,8 @@ SessionTab::SessionTab(const Profile& profile,
|
||||
[this]() {
|
||||
if (m_rdpDisplay != nullptr) {
|
||||
m_rdpDisplay->setCursorDefault();
|
||||
} else if (m_vncDisplay != nullptr) {
|
||||
m_vncDisplay->setCursorDefault();
|
||||
}
|
||||
},
|
||||
Qt::QueuedConnection);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
#include "vnc_display_widget.h"
|
||||
|
||||
#include <QCursor>
|
||||
#include <QEvent>
|
||||
#include <QKeyEvent>
|
||||
#include <QMouseEvent>
|
||||
#include <QPainter>
|
||||
#include <QPixmap>
|
||||
#include <QResizeEvent>
|
||||
#include <QTimer>
|
||||
#include <QWheelEvent>
|
||||
@@ -30,7 +32,8 @@ VncDisplayWidget::VncDisplayWidget(QWidget* parent)
|
||||
: QWidget(parent),
|
||||
m_remoteSize(1280, 720),
|
||||
m_resizeDebounceTimer(new QTimer(this)),
|
||||
m_scaleToFit(true)
|
||||
m_scaleToFit(true),
|
||||
m_cursorMode(CursorMode::Default)
|
||||
{
|
||||
setFocusPolicy(Qt::StrongFocus);
|
||||
setMouseTracking(true);
|
||||
@@ -83,6 +86,7 @@ void VncDisplayWidget::setScaleToFit(bool scaleToFit)
|
||||
}
|
||||
m_scaleToFit = scaleToFit;
|
||||
applySizeConstraint();
|
||||
applyCursor();
|
||||
update();
|
||||
}
|
||||
|
||||
@@ -131,6 +135,7 @@ void VncDisplayWidget::resizeEvent(QResizeEvent* event)
|
||||
{
|
||||
QWidget::resizeEvent(event);
|
||||
scheduleViewportGeometryEmit();
|
||||
applyCursor();
|
||||
}
|
||||
|
||||
bool VncDisplayWidget::event(QEvent* event)
|
||||
@@ -282,3 +287,64 @@ QSize VncDisplayWidget::effectiveRemoteSize() const
|
||||
}
|
||||
return QSize(1280, 720);
|
||||
}
|
||||
|
||||
void VncDisplayWidget::setCursorImage(const QImage& image, const QPoint& hotspot)
|
||||
{
|
||||
m_cursorImage = image;
|
||||
m_cursorHotspot = hotspot;
|
||||
m_cursorMode = CursorMode::Custom;
|
||||
applyCursor();
|
||||
}
|
||||
|
||||
void VncDisplayWidget::setCursorHidden()
|
||||
{
|
||||
m_cursorMode = CursorMode::Hidden;
|
||||
applyCursor();
|
||||
}
|
||||
|
||||
void VncDisplayWidget::setCursorDefault()
|
||||
{
|
||||
m_cursorMode = CursorMode::Default;
|
||||
applyCursor();
|
||||
}
|
||||
|
||||
void VncDisplayWidget::applyCursor()
|
||||
{
|
||||
if (m_cursorMode == CursorMode::Hidden) {
|
||||
setCursor(Qt::BlankCursor);
|
||||
return;
|
||||
}
|
||||
|
||||
if (m_cursorMode == CursorMode::Default || m_cursorImage.isNull()) {
|
||||
unsetCursor();
|
||||
return;
|
||||
}
|
||||
|
||||
// renderRect()/effectiveRemoteSize() already account for both display
|
||||
// modes: in actual-size mode the scale factor naturally comes out to
|
||||
// 1.0 (see applySizeConstraint()'s comment), so no special-casing is
|
||||
// needed here beyond reusing the same geometry helpers RDP's version
|
||||
// uses for its single (always scale-to-fit) mode.
|
||||
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));
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
#define ORBITHUB_VNC_DISPLAY_WIDGET_H
|
||||
|
||||
#include <QImage>
|
||||
#include <QPoint>
|
||||
#include <QWidget>
|
||||
|
||||
class QKeyEvent;
|
||||
@@ -12,8 +13,7 @@ class QTimer;
|
||||
class QWheelEvent;
|
||||
|
||||
// Renders a VNC framebuffer and forwards local input, scaled-to-fit --
|
||||
// same shape as RdpDisplayWidget, minus cursor-shape sync (out of scope for
|
||||
// the initial VNC implementation; the system cursor is left alone).
|
||||
// same shape as RdpDisplayWidget, including remote cursor-shape sync.
|
||||
class VncDisplayWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -38,6 +38,15 @@ public:
|
||||
return m_scaleToFit;
|
||||
}
|
||||
|
||||
// Mirrors RdpDisplayWidget's cursor handling. VNC's Cursor pseudo-
|
||||
// encoding never signals "reset to default" the way RDP's SetDefault
|
||||
// callback does -- it only ever supplies a shape or hides the cursor --
|
||||
// so setCursorDefault() exists for symmetry/future use but VNC sessions
|
||||
// never call it today.
|
||||
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);
|
||||
void mouseMoveInput(int x, int y);
|
||||
@@ -59,10 +68,19 @@ protected:
|
||||
bool focusNextPrevChild(bool next) override;
|
||||
|
||||
private:
|
||||
enum class CursorMode {
|
||||
Default,
|
||||
Hidden,
|
||||
Custom,
|
||||
};
|
||||
|
||||
QImage m_frame;
|
||||
QSize m_remoteSize;
|
||||
QTimer* m_resizeDebounceTimer;
|
||||
bool m_scaleToFit;
|
||||
QImage m_cursorImage;
|
||||
QPoint m_cursorHotspot;
|
||||
CursorMode m_cursorMode;
|
||||
|
||||
QRectF renderRect() const;
|
||||
QPoint mapToRemote(const QPointF& pos) const;
|
||||
@@ -70,6 +88,7 @@ private:
|
||||
void emitViewportGeometry();
|
||||
void scheduleViewportGeometryEmit();
|
||||
void applySizeConstraint();
|
||||
void applyCursor();
|
||||
};
|
||||
|
||||
#endif
|
||||
|
||||
@@ -32,8 +32,13 @@ constexpr quint8 kServerMsgServerCutText = 3;
|
||||
// of truth for what we tell the server we can decode via SetEncodings.
|
||||
constexpr qint32 kEncRaw = 0;
|
||||
constexpr qint32 kEncCopyRect = 1;
|
||||
// RFC 6143 SS7.8.2 "Cursor pseudo-encoding": not a real screen-content
|
||||
// encoding -- a rectangle with this type carries a cursor shape update
|
||||
// instead (hotspot in x/y, image dims in width/height), never painted into
|
||||
// the framebuffer.
|
||||
constexpr qint32 kEncCursor = -239;
|
||||
|
||||
constexpr std::array<qint32, 2> kAnnouncedEncodings = { kEncRaw, kEncCopyRect };
|
||||
constexpr std::array<qint32, 3> kAnnouncedEncodings = { kEncRaw, kEncCopyRect, kEncCursor };
|
||||
|
||||
quint16 readU16BE(const QByteArray& buf, int offset)
|
||||
{
|
||||
@@ -320,6 +325,7 @@ void VncSessionBackend::onSocketDisconnected()
|
||||
&& m_rfbState != RfbState::WaitingRectangleHeader
|
||||
&& m_rfbState != RfbState::WaitingRawPixelData
|
||||
&& m_rfbState != RfbState::WaitingCopyRectSource
|
||||
&& m_rfbState != RfbState::WaitingCursorPixelData
|
||||
&& m_rfbState != RfbState::WaitingSetColourMapHeader
|
||||
&& m_rfbState != RfbState::WaitingSetColourMapData
|
||||
&& m_rfbState != RfbState::WaitingServerCutTextHeader
|
||||
@@ -810,6 +816,9 @@ void VncSessionBackend::processReceiveBuffer()
|
||||
case kEncCopyRect:
|
||||
m_rfbState = RfbState::WaitingCopyRectSource;
|
||||
break;
|
||||
case kEncCursor:
|
||||
m_rfbState = RfbState::WaitingCursorPixelData;
|
||||
break;
|
||||
default: {
|
||||
// SetEncodings (see kAnnouncedEncodings) is entirely
|
||||
// client-controlled, so a spec-compliant server will never
|
||||
@@ -878,6 +887,60 @@ void VncSessionBackend::processReceiveBuffer()
|
||||
break;
|
||||
}
|
||||
|
||||
case RfbState::WaitingCursorPixelData: {
|
||||
// Cursor pseudo-encoding (RFC 6143 SS7.8.2): x/y in the already-
|
||||
// parsed rectangle header are the hotspot, not screen position;
|
||||
// width/height are the cursor image's own dimensions. Never
|
||||
// painted into m_framebuffer. Payload is width*height pixels in
|
||||
// our negotiated 32bpp format, followed by a row-padded,
|
||||
// MSB-first-per-byte opacity bitmask.
|
||||
const int width = m_currentRectangle.width;
|
||||
const int height = m_currentRectangle.height;
|
||||
const qint64 maskRowBytes = (static_cast<qint64>(width) + 7) / 8;
|
||||
const qint64 pixelBytes = static_cast<qint64>(width) * height * 4;
|
||||
const qint64 maskBytes = maskRowBytes * height;
|
||||
const qint64 totalBytes = pixelBytes + maskBytes;
|
||||
if (totalBytes < 0 || totalBytes > std::numeric_limits<int>::max()) {
|
||||
failConnection(QStringLiteral("The VNC server sent an implausibly large cursor image."),
|
||||
QStringLiteral("Cursor rectangle %1x%2").arg(width).arg(height));
|
||||
return;
|
||||
}
|
||||
if (!haveBytes(static_cast<int>(totalBytes))) {
|
||||
return;
|
||||
}
|
||||
|
||||
// A 0x0 cursor rectangle is the spec's way of saying "hide the
|
||||
// cursor"; treat any other degenerate (zero-area) size the same
|
||||
// way rather than trying to build an empty QImage.
|
||||
if (width <= 0 || height <= 0) {
|
||||
emit cursorHidden();
|
||||
} else {
|
||||
QImage cursorImage(width, height, QImage::Format_ARGB32);
|
||||
const auto* pixelData = reinterpret_cast<const uchar*>(m_recvBuffer.constData());
|
||||
const uchar* maskData = pixelData + pixelBytes;
|
||||
for (int y = 0; y < height; ++y) {
|
||||
for (int x = 0; x < width; ++x) {
|
||||
const uchar* px = pixelData + ((static_cast<qint64>(y) * width + x) * 4);
|
||||
// Matches our negotiated SetPixelFormat: little-
|
||||
// endian 32bpp, R at shift 16 / G at 8 / B at 0 --
|
||||
// byte order B,G,R,pad.
|
||||
const uchar b = px[0];
|
||||
const uchar g = px[1];
|
||||
const uchar r = px[2];
|
||||
const uchar maskByte = maskData[y * maskRowBytes + (x / 8)];
|
||||
const bool opaque = (maskByte & (0x80 >> (x % 8))) != 0;
|
||||
cursorImage.setPixel(x, y, qRgba(r, g, b, opaque ? 255 : 0));
|
||||
}
|
||||
}
|
||||
emit cursorImageChanged(cursorImage,
|
||||
QPoint(m_currentRectangle.x, m_currentRectangle.y));
|
||||
}
|
||||
|
||||
m_recvBuffer.remove(0, static_cast<int>(totalBytes));
|
||||
onRectangleFinished();
|
||||
break;
|
||||
}
|
||||
|
||||
case RfbState::WaitingSetColourMapHeader: {
|
||||
if (!haveBytes(5)) {
|
||||
return;
|
||||
|
||||
@@ -21,8 +21,9 @@ class QTcpSocket;
|
||||
// Scope (see plan / issue #3 for the full rationale): standard VNC
|
||||
// Authentication (security type 2) and no-auth (type 1) only -- not
|
||||
// Apple's Screen Sharing scheme (type 30). Raw + CopyRect encodings only.
|
||||
// No dynamic resize, no remote cursor shape sync. Clipboard sync (Latin-1
|
||||
// only, per RFB's ServerCutText/ClientCutText) is supported.
|
||||
// No dynamic resize. Clipboard sync (Latin-1 only, per RFB's
|
||||
// ServerCutText/ClientCutText) and remote cursor shape sync (the Cursor
|
||||
// pseudo-encoding) are supported.
|
||||
class VncSessionBackend : public SessionBackend
|
||||
{
|
||||
Q_OBJECT
|
||||
@@ -81,6 +82,7 @@ private:
|
||||
WaitingRectangleHeader,
|
||||
WaitingRawPixelData,
|
||||
WaitingCopyRectSource,
|
||||
WaitingCursorPixelData,
|
||||
WaitingSetColourMapHeader,
|
||||
WaitingSetColourMapData,
|
||||
WaitingServerCutTextHeader,
|
||||
|
||||
Reference in New Issue
Block a user