#include "rdp_display_widget.h" #include #include #include #include #include #include #include #include #include #include namespace { QSize sanitizeSize(const QSize& size) { return QSize(qMax(1, size.width()), qMax(1, size.height())); } qreal sanitizeDevicePixelRatio(qreal ratio) { if (!(ratio > 0.0)) { return 1.0; } return qBound(1.0, ratio, 4.0); } // Windows' virtual-display driver can visibly glitch (stale composited // content left on screen) when asked to change resolution repeatedly in // quick succession, which naturally happens as the window's layout settles // right after creation/connect. Coalescing bursts of resize events into one // request avoids triggering that. constexpr int kResizeDebounceMs = 150; } RdpDisplayWidget::RdpDisplayWidget(QWidget* parent) : QWidget(parent), m_remoteSize(1280, 720), m_cursorMode(CursorMode::Default), m_resizeDebounceTimer(new QTimer(this)) { setFocusPolicy(Qt::StrongFocus); setMouseTracking(true); setAutoFillBackground(false); setMinimumSize(320, 200); m_resizeDebounceTimer->setSingleShot(true); connect(m_resizeDebounceTimer, &QTimer::timeout, this, &RdpDisplayWidget::emitViewportGeometry); scheduleViewportGeometryEmit(); } void RdpDisplayWidget::setFrame(const QImage& frame) { if (frame.isNull()) { return; } m_frame = frame; m_remoteSize = sanitizeSize(frame.size()); update(); } void RdpDisplayWidget::setRemoteDesktopSize(int width, int height) { if (width < 1 || height < 1) { return; } const QSize nextSize(width, height); if (m_remoteSize == nextSize) { return; } m_remoteSize = nextSize; // The next actual frame (via setFrame) arrives asynchronously and isn't // guaranteed to be sized to match yet. Drawing the old frame stretched // to a renderRect() computed from the new m_remoteSize would scale it // by the wrong factor for the transition window, producing visibly // distorted/duplicated-looking content. Clear it and show the existing // "waiting for frame" placeholder until a correctly-sized frame lands. m_frame = QImage(); update(); } void RdpDisplayWidget::clearFrame() { m_frame = QImage(); 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); QPainter painter(this); painter.fillRect(rect(), QColor(QStringLiteral("#101214"))); const QRectF target = renderRect(); if (!m_frame.isNull()) { painter.drawImage(target, m_frame); } else { painter.setPen(QColor(QStringLiteral("#b0bec5"))); painter.drawText(rect(), Qt::AlignCenter, QStringLiteral("Waiting for remote desktop frame...")); } } void RdpDisplayWidget::resizeEvent(QResizeEvent* event) { QWidget::resizeEvent(event); scheduleViewportGeometryEmit(); applyCursor(); } bool RdpDisplayWidget::event(QEvent* event) { // Fires when this widget's effective screen changes (e.g. dragged to a // different monitor), which is what changes devicePixelRatio(). Newer // Qt versions add a more specific QEvent::DevicePixelRatioChange, but // this project's Qt 6.2 floor doesn't have it. if (event->type() == QEvent::ScreenChangeInternal) { scheduleViewportGeometryEmit(); } return QWidget::event(event); } void RdpDisplayWidget::scheduleViewportGeometryEmit() { // Restarting an already-running single-shot timer resets its countdown, // so a burst of resize events collapses into one emission after things // settle, rather than one request per event. m_resizeDebounceTimer->start(kResizeDebounceMs); } void RdpDisplayWidget::emitViewportGeometry() { const QSize logicalSize = sanitizeSize(this->size()); const qreal ratio = sanitizeDevicePixelRatio(this->devicePixelRatioF()); const QSize physicalSize(qRound(logicalSize.width() * ratio), qRound(logicalSize.height() * ratio)); emit viewportSizeChanged(physicalSize.width(), physicalSize.height()); emit displayScaleChanged(ratio); } void RdpDisplayWidget::keyPressEvent(QKeyEvent* event) { if (event == nullptr) { return; } // Auto-repeat presses must reach the remote server so it can perform // its own typematic repeat, exactly as a physical keyboard held down // would. Only release events filter out isAutoRepeat() (below), since // Qt uses a synthetic release/press pair purely to normalize platform // auto-repeat quirks -- forwarding that synthetic release would send a // spurious key-up for a key that is still physically held. emit keyInput(event->key(), event->nativeScanCode(), event->text(), true, static_cast(event->modifiers())); event->accept(); } void RdpDisplayWidget::keyReleaseEvent(QKeyEvent* event) { if (event == nullptr || event->isAutoRepeat()) { return; } emit keyInput(event->key(), event->nativeScanCode(), event->text(), false, static_cast(event->modifiers())); event->accept(); } bool RdpDisplayWidget::focusNextPrevChild(bool next) { Q_UNUSED(next); // Tab/Shift+Tab must reach keyPressEvent() and be forwarded to the // remote session instead of moving focus to the next local widget. return false; } void RdpDisplayWidget::mousePressEvent(QMouseEvent* event) { if (event == nullptr) { return; } setFocus(Qt::MouseFocusReason); const QPoint mapped = mapToRemote(event->position()); emit mouseButtonInput(mapped.x(), mapped.y(), static_cast(event->button()), true); event->accept(); } void RdpDisplayWidget::mouseReleaseEvent(QMouseEvent* event) { if (event == nullptr) { return; } const QPoint mapped = mapToRemote(event->position()); emit mouseButtonInput(mapped.x(), mapped.y(), static_cast(event->button()), false); event->accept(); } void RdpDisplayWidget::mouseMoveEvent(QMouseEvent* event) { if (event == nullptr) { return; } const QPoint mapped = mapToRemote(event->position()); emit mouseMoveInput(mapped.x(), mapped.y()); event->accept(); } void RdpDisplayWidget::wheelEvent(QWheelEvent* event) { if (event == nullptr) { return; } const QPoint mapped = mapToRemote(event->position()); const QPoint angle = event->angleDelta(); emit mouseWheelInput(mapped.x(), mapped.y(), angle.x(), angle.y()); event->accept(); } QRectF RdpDisplayWidget::renderRect() const { const QSize remote = effectiveRemoteSize(); const QRectF area = rect(); if (area.isEmpty()) { return QRectF(); } const qreal scale = qMin(area.width() / remote.width(), area.height() / remote.height()); const qreal drawWidth = remote.width() * scale; const qreal drawHeight = remote.height() * scale; const qreal x = area.x() + ((area.width() - drawWidth) * 0.5); const qreal y = area.y() + ((area.height() - drawHeight) * 0.5); return QRectF(x, y, drawWidth, drawHeight); } QPoint RdpDisplayWidget::mapToRemote(const QPointF& pos) const { const QSize remote = effectiveRemoteSize(); const QRectF target = renderRect(); if (target.isEmpty()) { return QPoint(0, 0); } const qreal clampedX = qBound(target.left(), pos.x(), target.right()); const qreal clampedY = qBound(target.top(), pos.y(), target.bottom()); const qreal normalizedX = (clampedX - target.left()) / qMax(1.0, target.width()); const qreal normalizedY = (clampedY - target.top()) / qMax(1.0, target.height()); const int remoteX = qBound(0, static_cast(normalizedX * remote.width()), remote.width() - 1); const int remoteY = qBound(0, static_cast(normalizedY * remote.height()), remote.height() - 1); return QPoint(remoteX, remoteY); } QSize RdpDisplayWidget::effectiveRemoteSize() const { if (m_remoteSize.width() > 0 && m_remoteSize.height() > 0) { return m_remoteSize; } if (!m_frame.isNull()) { return sanitizeSize(m_frame.size()); } return QSize(1280, 720); }