Add VNC-only scale-to-fit vs actual-size display toggle

VNC has no equivalent of RDP's MS-RDPEDISP to request a different
resolution from the guest, so a high-resolution remote desktop
previously always got shrunk to fit the window, making text
illegible. Adds a per-tab "Display Mode" choice (tab-bar right-click)
between the existing scale-to-fit behavior and a new actual-size mode
that renders the framebuffer at its native pixel size inside a
QScrollArea. Reuses VncDisplayWidget's existing scale-to-fit render
math unchanged -- it degenerates to an exact 1:1 mapping once the
widget is fixed to the remote's own size. Persisted like the terminal
theme preference.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
2026-09-15 18:13:43 -06:00
co-authored by Claude Sonnet 5
parent 6da9dc6ca5
commit 0b5454197c
6 changed files with 129 additions and 5 deletions
+33 -1
View File
@@ -24,6 +24,7 @@
#include <QComboBox>
#include <QProcessEnvironment>
#include <QPushButton>
#include <QScrollArea>
#include <QThread>
#include <QTimer>
#include <QToolButton>
@@ -81,6 +82,7 @@ SessionTab::SessionTab(const Profile& profile,
m_sshTerminal(nullptr),
m_rdpDisplay(nullptr),
m_vncDisplay(nullptr),
m_vncScrollArea(nullptr),
m_terminalOutput(nullptr),
m_eventLog(nullptr),
m_toggleEventsButton(nullptr),
@@ -102,6 +104,10 @@ SessionTab::SessionTab(const Profile& profile,
setupUi();
if (m_vncDisplay != nullptr) {
m_vncDisplay->setScaleToFit(preferences.vncScaleToFit);
}
if (m_useKodoTermForSsh) {
connect(m_sshTerminal,
&KodoTerm::finished,
@@ -457,6 +463,28 @@ bool SessionTab::supportsZoom() const
return m_useKodoTermForSsh || m_terminalOutput != nullptr;
}
bool SessionTab::supportsVncScaleToggle() const
{
return m_vncDisplay != nullptr;
}
void SessionTab::setVncScaleToFit(bool scaleToFit)
{
if (m_vncDisplay == nullptr || m_vncDisplay->scaleToFit() == scaleToFit) {
return;
}
m_vncDisplay->setScaleToFit(scaleToFit);
appendEvent(scaleToFit ? QStringLiteral("Display mode set to scale to fit.")
: QStringLiteral("Display mode set to actual size."));
emit vncScaleModeChanged(scaleToFit);
}
bool SessionTab::vncScaleToFit() const
{
return m_vncDisplay != nullptr ? m_vncDisplay->scaleToFit() : true;
}
void SessionTab::zoomIn()
{
if (m_useKodoTermForSsh && m_sshTerminal != nullptr) {
@@ -731,7 +759,11 @@ void SessionTab::setupUi()
rootLayout->addWidget(m_rdpDisplay, 1);
} else if (m_profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
m_vncDisplay = new VncDisplayWidget(this);
rootLayout->addWidget(m_vncDisplay, 1);
m_vncScrollArea = new QScrollArea(this);
m_vncScrollArea->setWidget(m_vncDisplay);
m_vncScrollArea->setWidgetResizable(true);
m_vncScrollArea->setFrameShape(QFrame::NoFrame);
rootLayout->addWidget(m_vncScrollArea, 1);
} else {
m_terminalOutput = new TerminalView(this);
QFont fallbackFont = defaultTerminalFont();
+7
View File
@@ -23,6 +23,7 @@ class QLineEdit;
class QComboBox;
class QLabel;
class QPushButton;
class QScrollArea;
class KodoTerm;
struct SessionUiPreferences
@@ -30,6 +31,7 @@ struct SessionUiPreferences
QString terminalThemeName = QStringLiteral("Dark");
bool eventsPanelExpanded = false;
int terminalFontPointSize = 0;
bool vncScaleToFit = true;
};
class SessionTab : public QWidget
@@ -62,6 +64,9 @@ public:
void clearEvents();
void copyEvents() const;
void exportEventsToFile();
bool supportsVncScaleToggle() const;
void setVncScaleToFit(bool scaleToFit);
bool vncScaleToFit() const;
signals:
void tabTitleChanged(const QString& title);
@@ -69,6 +74,7 @@ signals:
void terminalThemeChanged(const QString& themeName);
void terminalFontSizeChanged(int pointSize);
void eventsPanelVisibilityChanged(bool expanded);
void vncScaleModeChanged(bool scaleToFit);
void requestConnect(const SessionConnectOptions& options);
void requestDisconnect();
void requestReconnect(const SessionConnectOptions& options);
@@ -111,6 +117,7 @@ private:
KodoTerm* m_sshTerminal;
RdpDisplayWidget* m_rdpDisplay;
VncDisplayWidget* m_vncDisplay;
QScrollArea* m_vncScrollArea;
TerminalView* m_terminalOutput;
QPlainTextEdit* m_eventLog;
QToolButton* m_toggleEventsButton;
+28
View File
@@ -120,6 +120,20 @@ SessionWindow::SessionWindow(QWidget* parent)
setFontSizeAction = menu.addAction(QStringLiteral("Set Font Size..."));
}
QAction* scaleToFitAction = nullptr;
QAction* actualSizeAction = nullptr;
if (tab->supportsVncScaleToggle()) {
menu.addSeparator();
QMenu* displayMenu = menu.addMenu(QStringLiteral("Display Mode"));
scaleToFitAction = displayMenu->addAction(QStringLiteral("Scale to Fit"));
scaleToFitAction->setCheckable(true);
scaleToFitAction->setChecked(tab->vncScaleToFit());
actualSizeAction = displayMenu->addAction(
QStringLiteral("Actual Size (Scrollbars)"));
actualSizeAction->setCheckable(true);
actualSizeAction->setChecked(!tab->vncScaleToFit());
}
QAction* chosen = menu.exec(m_tabs->tabBar()->mapToGlobal(pos));
if (chosen == disconnectAction) {
tab->disconnectSession();
@@ -156,6 +170,10 @@ SessionWindow::SessionWindow(QWidget* parent)
if (accepted) {
tab->setTerminalFontPointSize(newSize);
}
} else if (scaleToFitAction != nullptr && chosen == scaleToFitAction) {
tab->setVncScaleToFit(true);
} else if (actualSizeAction != nullptr && chosen == actualSizeAction) {
tab->setVncScaleToFit(false);
} else {
for (QAction* themeAction : themeActions) {
if (chosen == themeAction) {
@@ -288,6 +306,13 @@ void SessionWindow::addSessionTab(const Profile& profile)
m_preferences.eventsPanelExpanded = expanded;
saveUiPreferences();
});
connect(tab,
&SessionTab::vncScaleModeChanged,
this,
[this](bool scaleToFit) {
m_preferences.vncScaleToFit = scaleToFit;
saveUiPreferences();
});
}
void SessionWindow::updateTabTitle(SessionTab* tab, const QString& title)
@@ -319,6 +344,8 @@ void SessionWindow::loadUiPreferences()
settings.value(QStringLiteral("session/eventsPanelExpanded"), false).toBool();
m_preferences.terminalFontPointSize =
settings.value(QStringLiteral("session/terminalFontPointSize"), 0).toInt();
m_preferences.vncScaleToFit =
settings.value(QStringLiteral("session/vncScaleToFit"), true).toBool();
}
void SessionWindow::saveUiPreferences() const
@@ -330,4 +357,5 @@ void SessionWindow::saveUiPreferences() const
m_preferences.eventsPanelExpanded);
settings.setValue(QStringLiteral("session/terminalFontPointSize"),
m_preferences.terminalFontPointSize);
settings.setValue(QStringLiteral("session/vncScaleToFit"), m_preferences.vncScaleToFit);
}
+33 -1
View File
@@ -27,7 +27,10 @@ constexpr int kResizeDebounceMs = 150;
}
VncDisplayWidget::VncDisplayWidget(QWidget* parent)
: QWidget(parent), m_remoteSize(1280, 720), m_resizeDebounceTimer(new QTimer(this))
: QWidget(parent),
m_remoteSize(1280, 720),
m_resizeDebounceTimer(new QTimer(this)),
m_scaleToFit(true)
{
setFocusPolicy(Qt::StrongFocus);
setMouseTracking(true);
@@ -48,6 +51,7 @@ void VncDisplayWidget::setFrame(const QImage& frame)
m_frame = frame;
m_remoteSize = sanitizeSize(frame.size());
applySizeConstraint();
update();
}
@@ -68,9 +72,37 @@ void VncDisplayWidget::setRemoteDesktopSize(int width, int height)
// size, so drop the stale one rather than stretch it by the wrong
// factor until a correctly-sized frame lands.
m_frame = QImage();
applySizeConstraint();
update();
}
void VncDisplayWidget::setScaleToFit(bool scaleToFit)
{
if (m_scaleToFit == scaleToFit) {
return;
}
m_scaleToFit = scaleToFit;
applySizeConstraint();
update();
}
void VncDisplayWidget::applySizeConstraint()
{
if (m_scaleToFit) {
// Let the widget follow whatever it's placed in again (e.g. a
// QScrollArea in resizable mode, or a plain layout).
setMinimumSize(320, 200);
setMaximumSize(QWIDGETSIZE_MAX, QWIDGETSIZE_MAX);
} else {
// Fixed at the remote's actual pixel size. renderRect()'s
// scale-to-fit math naturally degenerates to an unscaled 1:1
// mapping once the widget's own bounds already equal the remote
// size, so no separate "actual size" rendering path is needed --
// this is the only thing that differs between the two modes.
setFixedSize(effectiveRemoteSize());
}
}
void VncDisplayWidget::clearFrame()
{
m_frame = QImage();
+15
View File
@@ -25,6 +25,19 @@ public:
void setRemoteDesktopSize(int width, int height);
void clearFrame();
// true (default): scale the whole remote screen to fit the widget,
// like RdpDisplayWidget. false: render at the remote's actual pixel
// size -- meant to be placed inside a QScrollArea, whose scrollbars
// then let the user pan around a screen larger than the window
// instead of shrinking small text to illegibility. VNC has no
// equivalent of RDP's MS-RDPEDISP to request a different resolution
// from the guest, so this is the only way to see it at native size.
void setScaleToFit(bool scaleToFit);
bool scaleToFit() const
{
return m_scaleToFit;
}
signals:
void keyInput(int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers);
void mouseMoveInput(int x, int y);
@@ -49,12 +62,14 @@ private:
QImage m_frame;
QSize m_remoteSize;
QTimer* m_resizeDebounceTimer;
bool m_scaleToFit;
QRectF renderRect() const;
QPoint mapToRemote(const QPointF& pos) const;
QSize effectiveRemoteSize() const;
void emitViewportGeometry();
void scheduleViewportGeometryEmit();
void applySizeConstraint();
};
#endif