Internal
Public Access
RdpDisplayWidget::keyPressEvent() dropped every event where QKeyEvent::isAutoRepeat() was true, which discards the entire repeat stream Qt generates while a key is held -- so holding a key only ever produced a single keystroke on the remote machine. Auto-repeat presses need to reach the remote server so it can perform its own typematic repeat, exactly as a physical keyboard held down would; only release events should filter isAutoRepeat() (kept as-is), since Qt uses a synthetic release/press pair purely to normalize platform auto-repeat quirks, and forwarding that synthetic release would send a spurious key-up for a key still physically held. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
281 lines
7.6 KiB
C++
281 lines
7.6 KiB
C++
#include "rdp_display_widget.h"
|
|
|
|
#include <QCursor>
|
|
#include <QKeyEvent>
|
|
#include <QMouseEvent>
|
|
#include <QPainter>
|
|
#include <QPixmap>
|
|
#include <QResizeEvent>
|
|
#include <QTimer>
|
|
#include <QWheelEvent>
|
|
#include <QtGlobal>
|
|
|
|
namespace {
|
|
QSize sanitizeSize(const QSize& size)
|
|
{
|
|
return QSize(qMax(1, size.width()), qMax(1, size.height()));
|
|
}
|
|
}
|
|
|
|
RdpDisplayWidget::RdpDisplayWidget(QWidget* parent)
|
|
: QWidget(parent), m_remoteSize(1280, 720), m_cursorMode(CursorMode::Default)
|
|
{
|
|
setFocusPolicy(Qt::StrongFocus);
|
|
setMouseTracking(true);
|
|
setAutoFillBackground(false);
|
|
setMinimumSize(320, 200);
|
|
|
|
QTimer::singleShot(0, this, [this]() {
|
|
const QSize size = sanitizeSize(this->size());
|
|
emit viewportSizeChanged(size.width(), size.height());
|
|
});
|
|
}
|
|
|
|
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;
|
|
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);
|
|
const QSize size = sanitizeSize(event->size());
|
|
emit viewportSizeChanged(size.width(), size.height());
|
|
applyCursor();
|
|
}
|
|
|
|
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<int>(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<int>(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<int>(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<int>(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<int>(normalizedX * remote.width()), remote.width() - 1);
|
|
const int remoteY = qBound(0, static_cast<int>(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);
|
|
}
|