Internal
Public Access
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:
+13
-3
@@ -133,6 +133,15 @@ Delivered:
|
||||
- Verified live against a real, independently implemented VNC server
|
||||
(TightVNC on Windows): connect with VNC Authentication, correct
|
||||
framebuffer dimensions and pixel data, clean disconnect, reconnect
|
||||
- Per-tab VNC-only display mode toggle (tab-bar right-click ->
|
||||
`Display Mode`): `Scale to Fit` (default, matches RDP's behavior) or
|
||||
`Actual Size (Scrollbars)` -- renders the remote framebuffer at its
|
||||
native pixel size inside a `QScrollArea` so text isn't shrunk, at the
|
||||
cost of needing to scroll to see the whole screen. Reuses
|
||||
`VncDisplayWidget::renderRect()`'s existing scale-to-fit math unchanged:
|
||||
it degenerates to an exact 1:1 mapping once the widget's own bounds are
|
||||
fixed to the remote's size, so no separate rendering path was needed.
|
||||
Persisted across sessions like the terminal theme preference.
|
||||
|
||||
Known gaps (explicit scope decisions, not oversights -- see issue #3 for
|
||||
follow-up tracking):
|
||||
@@ -141,9 +150,10 @@ follow-up tracking):
|
||||
server -- only standard VNC Authentication (type 2) and no-auth (type 1)
|
||||
- Raw + CopyRect encodings only -- no Hextile/ZRLE/Tight compression, so
|
||||
bandwidth usage is higher over slow links than a full VNC client
|
||||
- No dynamic resize (connects at the server's native resolution, scaled to
|
||||
fit locally -- the same way `RdpDisplayWidget` already renders
|
||||
regardless of server resolution, so not a UX regression vs. RDP)
|
||||
- No dynamic resize (connects at the server's native resolution; the
|
||||
`Scale to Fit`/`Actual Size` toggle changes how that fixed resolution is
|
||||
displayed locally, not what resolution is requested from the guest --
|
||||
VNC has no equivalent of RDP's MS-RDPEDISP for that)
|
||||
- No remote cursor shape sync (local default cursor only)
|
||||
- No clipboard sync
|
||||
|
||||
|
||||
+33
-1
@@ -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();
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user