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
|
- Verified live against a real, independently implemented VNC server
|
||||||
(TightVNC on Windows): connect with VNC Authentication, correct
|
(TightVNC on Windows): connect with VNC Authentication, correct
|
||||||
framebuffer dimensions and pixel data, clean disconnect, reconnect
|
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
|
Known gaps (explicit scope decisions, not oversights -- see issue #3 for
|
||||||
follow-up tracking):
|
follow-up tracking):
|
||||||
@@ -141,9 +150,10 @@ follow-up tracking):
|
|||||||
server -- only standard VNC Authentication (type 2) and no-auth (type 1)
|
server -- only standard VNC Authentication (type 2) and no-auth (type 1)
|
||||||
- Raw + CopyRect encodings only -- no Hextile/ZRLE/Tight compression, so
|
- Raw + CopyRect encodings only -- no Hextile/ZRLE/Tight compression, so
|
||||||
bandwidth usage is higher over slow links than a full VNC client
|
bandwidth usage is higher over slow links than a full VNC client
|
||||||
- No dynamic resize (connects at the server's native resolution, scaled to
|
- No dynamic resize (connects at the server's native resolution; the
|
||||||
fit locally -- the same way `RdpDisplayWidget` already renders
|
`Scale to Fit`/`Actual Size` toggle changes how that fixed resolution is
|
||||||
regardless of server resolution, so not a UX regression vs. RDP)
|
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 remote cursor shape sync (local default cursor only)
|
||||||
- No clipboard sync
|
- No clipboard sync
|
||||||
|
|
||||||
|
|||||||
+33
-1
@@ -24,6 +24,7 @@
|
|||||||
#include <QComboBox>
|
#include <QComboBox>
|
||||||
#include <QProcessEnvironment>
|
#include <QProcessEnvironment>
|
||||||
#include <QPushButton>
|
#include <QPushButton>
|
||||||
|
#include <QScrollArea>
|
||||||
#include <QThread>
|
#include <QThread>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QToolButton>
|
#include <QToolButton>
|
||||||
@@ -81,6 +82,7 @@ SessionTab::SessionTab(const Profile& profile,
|
|||||||
m_sshTerminal(nullptr),
|
m_sshTerminal(nullptr),
|
||||||
m_rdpDisplay(nullptr),
|
m_rdpDisplay(nullptr),
|
||||||
m_vncDisplay(nullptr),
|
m_vncDisplay(nullptr),
|
||||||
|
m_vncScrollArea(nullptr),
|
||||||
m_terminalOutput(nullptr),
|
m_terminalOutput(nullptr),
|
||||||
m_eventLog(nullptr),
|
m_eventLog(nullptr),
|
||||||
m_toggleEventsButton(nullptr),
|
m_toggleEventsButton(nullptr),
|
||||||
@@ -102,6 +104,10 @@ SessionTab::SessionTab(const Profile& profile,
|
|||||||
|
|
||||||
setupUi();
|
setupUi();
|
||||||
|
|
||||||
|
if (m_vncDisplay != nullptr) {
|
||||||
|
m_vncDisplay->setScaleToFit(preferences.vncScaleToFit);
|
||||||
|
}
|
||||||
|
|
||||||
if (m_useKodoTermForSsh) {
|
if (m_useKodoTermForSsh) {
|
||||||
connect(m_sshTerminal,
|
connect(m_sshTerminal,
|
||||||
&KodoTerm::finished,
|
&KodoTerm::finished,
|
||||||
@@ -457,6 +463,28 @@ bool SessionTab::supportsZoom() const
|
|||||||
return m_useKodoTermForSsh || m_terminalOutput != nullptr;
|
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()
|
void SessionTab::zoomIn()
|
||||||
{
|
{
|
||||||
if (m_useKodoTermForSsh && m_sshTerminal != nullptr) {
|
if (m_useKodoTermForSsh && m_sshTerminal != nullptr) {
|
||||||
@@ -731,7 +759,11 @@ void SessionTab::setupUi()
|
|||||||
rootLayout->addWidget(m_rdpDisplay, 1);
|
rootLayout->addWidget(m_rdpDisplay, 1);
|
||||||
} else if (m_profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
|
} else if (m_profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) {
|
||||||
m_vncDisplay = new VncDisplayWidget(this);
|
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 {
|
} else {
|
||||||
m_terminalOutput = new TerminalView(this);
|
m_terminalOutput = new TerminalView(this);
|
||||||
QFont fallbackFont = defaultTerminalFont();
|
QFont fallbackFont = defaultTerminalFont();
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ class QLineEdit;
|
|||||||
class QComboBox;
|
class QComboBox;
|
||||||
class QLabel;
|
class QLabel;
|
||||||
class QPushButton;
|
class QPushButton;
|
||||||
|
class QScrollArea;
|
||||||
class KodoTerm;
|
class KodoTerm;
|
||||||
|
|
||||||
struct SessionUiPreferences
|
struct SessionUiPreferences
|
||||||
@@ -30,6 +31,7 @@ struct SessionUiPreferences
|
|||||||
QString terminalThemeName = QStringLiteral("Dark");
|
QString terminalThemeName = QStringLiteral("Dark");
|
||||||
bool eventsPanelExpanded = false;
|
bool eventsPanelExpanded = false;
|
||||||
int terminalFontPointSize = 0;
|
int terminalFontPointSize = 0;
|
||||||
|
bool vncScaleToFit = true;
|
||||||
};
|
};
|
||||||
|
|
||||||
class SessionTab : public QWidget
|
class SessionTab : public QWidget
|
||||||
@@ -62,6 +64,9 @@ public:
|
|||||||
void clearEvents();
|
void clearEvents();
|
||||||
void copyEvents() const;
|
void copyEvents() const;
|
||||||
void exportEventsToFile();
|
void exportEventsToFile();
|
||||||
|
bool supportsVncScaleToggle() const;
|
||||||
|
void setVncScaleToFit(bool scaleToFit);
|
||||||
|
bool vncScaleToFit() const;
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void tabTitleChanged(const QString& title);
|
void tabTitleChanged(const QString& title);
|
||||||
@@ -69,6 +74,7 @@ signals:
|
|||||||
void terminalThemeChanged(const QString& themeName);
|
void terminalThemeChanged(const QString& themeName);
|
||||||
void terminalFontSizeChanged(int pointSize);
|
void terminalFontSizeChanged(int pointSize);
|
||||||
void eventsPanelVisibilityChanged(bool expanded);
|
void eventsPanelVisibilityChanged(bool expanded);
|
||||||
|
void vncScaleModeChanged(bool scaleToFit);
|
||||||
void requestConnect(const SessionConnectOptions& options);
|
void requestConnect(const SessionConnectOptions& options);
|
||||||
void requestDisconnect();
|
void requestDisconnect();
|
||||||
void requestReconnect(const SessionConnectOptions& options);
|
void requestReconnect(const SessionConnectOptions& options);
|
||||||
@@ -111,6 +117,7 @@ private:
|
|||||||
KodoTerm* m_sshTerminal;
|
KodoTerm* m_sshTerminal;
|
||||||
RdpDisplayWidget* m_rdpDisplay;
|
RdpDisplayWidget* m_rdpDisplay;
|
||||||
VncDisplayWidget* m_vncDisplay;
|
VncDisplayWidget* m_vncDisplay;
|
||||||
|
QScrollArea* m_vncScrollArea;
|
||||||
TerminalView* m_terminalOutput;
|
TerminalView* m_terminalOutput;
|
||||||
QPlainTextEdit* m_eventLog;
|
QPlainTextEdit* m_eventLog;
|
||||||
QToolButton* m_toggleEventsButton;
|
QToolButton* m_toggleEventsButton;
|
||||||
|
|||||||
@@ -120,6 +120,20 @@ SessionWindow::SessionWindow(QWidget* parent)
|
|||||||
setFontSizeAction = menu.addAction(QStringLiteral("Set Font Size..."));
|
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));
|
QAction* chosen = menu.exec(m_tabs->tabBar()->mapToGlobal(pos));
|
||||||
if (chosen == disconnectAction) {
|
if (chosen == disconnectAction) {
|
||||||
tab->disconnectSession();
|
tab->disconnectSession();
|
||||||
@@ -156,6 +170,10 @@ SessionWindow::SessionWindow(QWidget* parent)
|
|||||||
if (accepted) {
|
if (accepted) {
|
||||||
tab->setTerminalFontPointSize(newSize);
|
tab->setTerminalFontPointSize(newSize);
|
||||||
}
|
}
|
||||||
|
} else if (scaleToFitAction != nullptr && chosen == scaleToFitAction) {
|
||||||
|
tab->setVncScaleToFit(true);
|
||||||
|
} else if (actualSizeAction != nullptr && chosen == actualSizeAction) {
|
||||||
|
tab->setVncScaleToFit(false);
|
||||||
} else {
|
} else {
|
||||||
for (QAction* themeAction : themeActions) {
|
for (QAction* themeAction : themeActions) {
|
||||||
if (chosen == themeAction) {
|
if (chosen == themeAction) {
|
||||||
@@ -288,6 +306,13 @@ void SessionWindow::addSessionTab(const Profile& profile)
|
|||||||
m_preferences.eventsPanelExpanded = expanded;
|
m_preferences.eventsPanelExpanded = expanded;
|
||||||
saveUiPreferences();
|
saveUiPreferences();
|
||||||
});
|
});
|
||||||
|
connect(tab,
|
||||||
|
&SessionTab::vncScaleModeChanged,
|
||||||
|
this,
|
||||||
|
[this](bool scaleToFit) {
|
||||||
|
m_preferences.vncScaleToFit = scaleToFit;
|
||||||
|
saveUiPreferences();
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionWindow::updateTabTitle(SessionTab* tab, const QString& title)
|
void SessionWindow::updateTabTitle(SessionTab* tab, const QString& title)
|
||||||
@@ -319,6 +344,8 @@ void SessionWindow::loadUiPreferences()
|
|||||||
settings.value(QStringLiteral("session/eventsPanelExpanded"), false).toBool();
|
settings.value(QStringLiteral("session/eventsPanelExpanded"), false).toBool();
|
||||||
m_preferences.terminalFontPointSize =
|
m_preferences.terminalFontPointSize =
|
||||||
settings.value(QStringLiteral("session/terminalFontPointSize"), 0).toInt();
|
settings.value(QStringLiteral("session/terminalFontPointSize"), 0).toInt();
|
||||||
|
m_preferences.vncScaleToFit =
|
||||||
|
settings.value(QStringLiteral("session/vncScaleToFit"), true).toBool();
|
||||||
}
|
}
|
||||||
|
|
||||||
void SessionWindow::saveUiPreferences() const
|
void SessionWindow::saveUiPreferences() const
|
||||||
@@ -330,4 +357,5 @@ void SessionWindow::saveUiPreferences() const
|
|||||||
m_preferences.eventsPanelExpanded);
|
m_preferences.eventsPanelExpanded);
|
||||||
settings.setValue(QStringLiteral("session/terminalFontPointSize"),
|
settings.setValue(QStringLiteral("session/terminalFontPointSize"),
|
||||||
m_preferences.terminalFontPointSize);
|
m_preferences.terminalFontPointSize);
|
||||||
|
settings.setValue(QStringLiteral("session/vncScaleToFit"), m_preferences.vncScaleToFit);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,10 @@ constexpr int kResizeDebounceMs = 150;
|
|||||||
}
|
}
|
||||||
|
|
||||||
VncDisplayWidget::VncDisplayWidget(QWidget* parent)
|
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);
|
setFocusPolicy(Qt::StrongFocus);
|
||||||
setMouseTracking(true);
|
setMouseTracking(true);
|
||||||
@@ -48,6 +51,7 @@ void VncDisplayWidget::setFrame(const QImage& frame)
|
|||||||
|
|
||||||
m_frame = frame;
|
m_frame = frame;
|
||||||
m_remoteSize = sanitizeSize(frame.size());
|
m_remoteSize = sanitizeSize(frame.size());
|
||||||
|
applySizeConstraint();
|
||||||
update();
|
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
|
// size, so drop the stale one rather than stretch it by the wrong
|
||||||
// factor until a correctly-sized frame lands.
|
// factor until a correctly-sized frame lands.
|
||||||
m_frame = QImage();
|
m_frame = QImage();
|
||||||
|
applySizeConstraint();
|
||||||
update();
|
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()
|
void VncDisplayWidget::clearFrame()
|
||||||
{
|
{
|
||||||
m_frame = QImage();
|
m_frame = QImage();
|
||||||
|
|||||||
@@ -25,6 +25,19 @@ public:
|
|||||||
void setRemoteDesktopSize(int width, int height);
|
void setRemoteDesktopSize(int width, int height);
|
||||||
void clearFrame();
|
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:
|
signals:
|
||||||
void keyInput(int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers);
|
void keyInput(int key, quint32 nativeScanCode, const QString& text, bool pressed, int modifiers);
|
||||||
void mouseMoveInput(int x, int y);
|
void mouseMoveInput(int x, int y);
|
||||||
@@ -49,12 +62,14 @@ private:
|
|||||||
QImage m_frame;
|
QImage m_frame;
|
||||||
QSize m_remoteSize;
|
QSize m_remoteSize;
|
||||||
QTimer* m_resizeDebounceTimer;
|
QTimer* m_resizeDebounceTimer;
|
||||||
|
bool m_scaleToFit;
|
||||||
|
|
||||||
QRectF renderRect() const;
|
QRectF renderRect() const;
|
||||||
QPoint mapToRemote(const QPointF& pos) const;
|
QPoint mapToRemote(const QPointF& pos) const;
|
||||||
QSize effectiveRemoteSize() const;
|
QSize effectiveRemoteSize() const;
|
||||||
void emitViewportGeometry();
|
void emitViewportGeometry();
|
||||||
void scheduleViewportGeometryEmit();
|
void scheduleViewportGeometryEmit();
|
||||||
|
void applySizeConstraint();
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
|
|||||||
Reference in New Issue
Block a user