From 27cc3a3bb21d618a6a82a568c14941be109049b0 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Mon, 7 Sep 2026 12:02:03 -0600 Subject: [PATCH] Session UX: inline password prompt and terminal font size controls Replace the modal RDP/SSH password dialog with an inline prompt bar embedded in the session tab instead of a separate popup window. Add per-tab terminal font size controls (increase/decrease/reset/set exact point size) via the tab context menu, with the chosen size persisted across sessions. Co-Authored-By: Claude Sonnet 5 --- src/session_tab.cpp | 327 +++++++++++++++++++++++++++++++---------- src/session_tab.h | 23 ++- src/session_window.cpp | 48 ++++++ src/terminal_view.cpp | 8 + src/terminal_view.h | 1 + 5 files changed, 325 insertions(+), 82 deletions(-) diff --git a/src/session_tab.cpp b/src/session_tab.cpp index 47de9f3..1617173 100644 --- a/src/session_tab.cpp +++ b/src/session_tab.cpp @@ -13,7 +13,6 @@ #include #include #include -#include #include #include #include @@ -22,6 +21,7 @@ #include #include #include +#include #include #include #include @@ -71,6 +71,9 @@ SessionTab::SessionTab(const Profile& profile, m_terminalThemeName(preferences.terminalThemeName.trimmed().isEmpty() ? QStringLiteral("Dark") : preferences.terminalThemeName.trimmed()), + m_terminalFontPointSize(preferences.terminalFontPointSize > 0 + ? preferences.terminalFontPointSize + : 0), m_sshTerminal(nullptr), m_rdpDisplay(nullptr), m_terminalOutput(nullptr), @@ -81,6 +84,11 @@ SessionTab::SessionTab(const Profile& profile, m_clearEventsButton(nullptr), m_exportEventsButton(nullptr), m_eventsPanel(nullptr), + m_passwordPromptBar(nullptr), + m_passwordPromptLabel(nullptr), + m_passwordPromptInput(nullptr), + m_passwordPromptConnectButton(nullptr), + m_passwordPromptCancelButton(nullptr), m_eventSeverityFilter(EventSeverity::Info), m_eventsPanelExpanded(preferences.eventsPanelExpanded) { @@ -265,21 +273,20 @@ void SessionTab::connectSession() return; } - const std::optional options = buildConnectOptions(); - if (!options.has_value()) { - return; - } - - m_lastConnectOptions = options.value(); - - if (m_useKodoTermForSsh) { - if (!startSshTerminal(options.value())) { + requestConnectOptions([this](std::optional options) { + if (!options.has_value()) { return; } - return; - } - emit requestConnect(options.value()); + m_lastConnectOptions = options.value(); + + if (m_useKodoTermForSsh) { + startSshTerminal(options.value()); + return; + } + + emit requestConnect(options.value()); + }); } void SessionTab::disconnectSession() @@ -305,24 +312,25 @@ void SessionTab::reconnectSession() return; } - const std::optional options = buildConnectOptions(); - if (!options.has_value()) { - return; - } - - m_lastConnectOptions = options.value(); - - if (m_useKodoTermForSsh) { - if (m_sshTerminal != nullptr) { - m_sshTerminal->kill(); + requestConnectOptions([this](std::optional options) { + if (!options.has_value()) { + return; } - QTimer::singleShot(50, - this, - [this, options]() { startSshTerminal(options.value()); }); - return; - } - emit requestReconnect(options.value()); + m_lastConnectOptions = options.value(); + + if (m_useKodoTermForSsh) { + if (m_sshTerminal != nullptr) { + m_sshTerminal->kill(); + } + QTimer::singleShot(50, + this, + [this, options]() { startSshTerminal(options.value()); }); + return; + } + + emit requestReconnect(options.value()); + }); } void SessionTab::clearTerminal() @@ -380,6 +388,84 @@ bool SessionTab::supportsClearAction() const return m_useKodoTermForSsh || m_terminalOutput != nullptr; } +bool SessionTab::supportsZoom() const +{ + return m_useKodoTermForSsh || m_terminalOutput != nullptr; +} + +void SessionTab::zoomIn() +{ + if (m_useKodoTermForSsh && m_sshTerminal != nullptr) { + m_sshTerminal->zoomIn(); + m_terminalFontPointSize = m_sshTerminal->getConfig().font.pointSize(); + } else if (m_terminalOutput != nullptr) { + m_terminalFontPointSize = m_terminalOutput->font().pointSize() + 1; + m_terminalOutput->setFontPointSize(m_terminalFontPointSize); + } else { + return; + } + + emit terminalFontSizeChanged(m_terminalFontPointSize); +} + +void SessionTab::zoomOut() +{ + if (m_useKodoTermForSsh && m_sshTerminal != nullptr) { + m_sshTerminal->zoomOut(); + m_terminalFontPointSize = m_sshTerminal->getConfig().font.pointSize(); + } else if (m_terminalOutput != nullptr) { + const int newSize = m_terminalOutput->font().pointSize() - 1; + if (newSize < 6) { + return; + } + m_terminalFontPointSize = newSize; + m_terminalOutput->setFontPointSize(m_terminalFontPointSize); + } else { + return; + } + + emit terminalFontSizeChanged(m_terminalFontPointSize); +} + +void SessionTab::resetZoom() +{ + if (m_useKodoTermForSsh && m_sshTerminal != nullptr) { + m_sshTerminal->resetZoom(); + m_terminalFontPointSize = m_sshTerminal->getConfig().font.pointSize(); + } else if (m_terminalOutput != nullptr) { + m_terminalFontPointSize = defaultTerminalFont().pointSize(); + m_terminalOutput->setFontPointSize(m_terminalFontPointSize); + } else { + return; + } + + emit terminalFontSizeChanged(m_terminalFontPointSize); +} + +void SessionTab::setTerminalFontPointSize(int pointSize) +{ + const int clamped = qBound(6, pointSize, 72); + + if (m_useKodoTermForSsh && m_sshTerminal != nullptr) { + KodoTermConfig config = m_sshTerminal->getConfig(); + config.font.setPointSize(clamped); + m_sshTerminal->setConfig(config); + m_terminalFontPointSize = clamped; + } else if (m_terminalOutput != nullptr) { + m_terminalFontPointSize = clamped; + m_terminalOutput->setFontPointSize(clamped); + } else { + return; + } + + emit terminalFontSizeChanged(m_terminalFontPointSize); +} + +int SessionTab::terminalFontPointSize() const +{ + return m_terminalFontPointSize; +} + bool SessionTab::isEventsPanelExpanded() const { return m_eventsPanelExpanded; @@ -535,7 +621,10 @@ void SessionTab::setupUi() if (m_useKodoTermForSsh) { m_sshTerminal = new KodoTerm(this); - const QFont terminalFont = defaultTerminalFont(); + QFont terminalFont = defaultTerminalFont(); + if (m_terminalFontPointSize > 0) { + terminalFont.setPointSize(m_terminalFontPointSize); + } KodoTermConfig config = m_sshTerminal->getConfig(); config.font = terminalFont; @@ -548,7 +637,11 @@ void SessionTab::setupUi() rootLayout->addWidget(m_rdpDisplay, 1); } else { m_terminalOutput = new TerminalView(this); - m_terminalOutput->setFont(defaultTerminalFont()); + QFont fallbackFont = defaultTerminalFont(); + if (m_terminalFontPointSize > 0) { + fallbackFont.setPointSize(m_terminalFontPointSize); + } + m_terminalOutput->setFont(fallbackFont); m_terminalOutput->setMinimumHeight(260); m_terminalOutput->setReadOnly(true); if (m_profile.protocol.compare(QStringLiteral("VNC"), Qt::CaseInsensitive) == 0) { @@ -563,6 +656,45 @@ void SessionTab::setupUi() applyTerminalTheme(m_terminalThemeName); + auto* passwordPromptLayout = new QHBoxLayout(); + m_passwordPromptLabel = new QLabel(this); + m_passwordPromptInput = new QLineEdit(this); + m_passwordPromptInput->setEchoMode(QLineEdit::Password); + m_passwordPromptConnectButton = new QPushButton(QStringLiteral("Connect"), this); + m_passwordPromptCancelButton = new QPushButton(QStringLiteral("Cancel"), this); + passwordPromptLayout->addWidget(m_passwordPromptLabel); + passwordPromptLayout->addWidget(m_passwordPromptInput, 1); + passwordPromptLayout->addWidget(m_passwordPromptConnectButton); + passwordPromptLayout->addWidget(m_passwordPromptCancelButton); + + m_passwordPromptBar = new QWidget(this); + m_passwordPromptBar->setLayout(passwordPromptLayout); + m_passwordPromptBar->setAutoFillBackground(true); + m_passwordPromptBar->setVisible(false); + rootLayout->addWidget(m_passwordPromptBar); + + connect(m_passwordPromptConnectButton, &QPushButton::clicked, this, [this]() { + if (!m_passwordPromptCallback) { + return; + } + const QString password = m_passwordPromptInput->text(); + const auto callback = m_passwordPromptCallback; + hidePasswordPrompt(); + callback(password); + }); + connect(m_passwordPromptCancelButton, &QPushButton::clicked, this, [this]() { + if (!m_passwordPromptCallback) { + return; + } + const auto callback = m_passwordPromptCallback; + hidePasswordPrompt(); + callback(std::nullopt); + }); + connect(m_passwordPromptInput, + &QLineEdit::returnPressed, + m_passwordPromptConnectButton, + &QPushButton::click); + auto* eventsHeader = new QHBoxLayout(); m_toggleEventsButton = new QToolButton(this); m_toggleEventsButton->setCheckable(true); @@ -676,77 +808,85 @@ void SessionTab::setupUi() } } -std::optional SessionTab::buildConnectOptions() +void SessionTab::requestConnectOptions( + std::function)> callback) { - SessionConnectOptions options; - options.knownHostsPolicy = m_profile.knownHostsPolicy; + SessionConnectOptions baseOptions; + baseOptions.knownHostsPolicy = m_profile.knownHostsPolicy; const bool isSsh = m_profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0; const bool isRdp = m_profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0; if (!isSsh && !isRdp) { - return options; + callback(baseOptions); + return; } if (isRdp) { if (m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) != 0) { - return options; + callback(baseOptions); + return; } - bool accepted = false; - const QString password = QInputDialog::getText( - this, - QStringLiteral("RDP Password"), - QStringLiteral("Password for %1:") - .arg(m_profile.username.trimmed().isEmpty() - ? m_profile.host - : QStringLiteral("%1@%2").arg(m_profile.username, m_profile.host)), - QLineEdit::Password, - QString(), - &accepted); - if (!accepted) { - return std::nullopt; - } + const QString label = QStringLiteral("RDP password for %1:") + .arg(m_profile.username.trimmed().isEmpty() + ? m_profile.host + : QStringLiteral("%1@%2").arg(m_profile.username, m_profile.host)); - if (password.isEmpty()) { - QMessageBox::warning(this, - QStringLiteral("Connect"), - QStringLiteral("Password is required for password authentication.")); - return std::nullopt; - } + showPasswordPrompt( + label, + [this, baseOptions, callback](std::optional password) { + if (!password.has_value()) { + callback(std::nullopt); + return; + } - options.password = password; - return options; + if (password->isEmpty()) { + QMessageBox::warning( + this, + QStringLiteral("Connect"), + QStringLiteral("Password is required for password authentication.")); + callback(std::nullopt); + return; + } + + SessionConnectOptions options = baseOptions; + options.password = password.value(); + callback(options); + }); + return; } if (m_useKodoTermForSsh && m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) { // Password is entered directly in terminal prompt. - return options; + callback(baseOptions); + return; } if (m_profile.authMode.compare(QStringLiteral("Password"), Qt::CaseInsensitive) == 0) { - bool accepted = false; - const QString password = QInputDialog::getText(this, - QStringLiteral("SSH Password"), - QStringLiteral("Password for %1@%2:") - .arg(m_profile.username, m_profile.host), - QLineEdit::Password, - QString(), - &accepted); - if (!accepted) { - return std::nullopt; - } + showPasswordPrompt( + QStringLiteral("SSH password for %1@%2:").arg(m_profile.username, m_profile.host), + [this, baseOptions, callback](std::optional password) { + if (!password.has_value()) { + callback(std::nullopt); + return; + } - if (password.isEmpty()) { - QMessageBox::warning(this, - QStringLiteral("Connect"), - QStringLiteral("Password is required for password authentication.")); - return std::nullopt; - } + if (password->isEmpty()) { + QMessageBox::warning( + this, + QStringLiteral("Connect"), + QStringLiteral("Password is required for password authentication.")); + callback(std::nullopt); + return; + } - options.password = password; - return options; + SessionConnectOptions options = baseOptions; + options.password = password.value(); + callback(options); + }); + return; } QString keyPath = m_profile.privateKeyPath.trimmed(); @@ -756,7 +896,8 @@ std::optional SessionTab::buildConnectOptions() QString(), QStringLiteral("All Files (*)")); if (keyPath.isEmpty()) { - return std::nullopt; + callback(std::nullopt); + return; } } @@ -764,11 +905,35 @@ std::optional SessionTab::buildConnectOptions() QMessageBox::warning(this, QStringLiteral("Connect"), QStringLiteral("Private key file not found: %1").arg(keyPath)); - return std::nullopt; + callback(std::nullopt); + return; } + SessionConnectOptions options = baseOptions; options.privateKeyPath = keyPath; - return options; + callback(options); +} + +void SessionTab::showPasswordPrompt(const QString& labelText, + std::function)> callback) +{ + if (m_passwordPromptCallback) { + const auto previousCallback = m_passwordPromptCallback; + m_passwordPromptCallback = nullptr; + previousCallback(std::nullopt); + } + + m_passwordPromptCallback = std::move(callback); + m_passwordPromptLabel->setText(labelText); + m_passwordPromptInput->clear(); + m_passwordPromptBar->setVisible(true); + m_passwordPromptInput->setFocus(); +} + +void SessionTab::hidePasswordPrompt() +{ + m_passwordPromptBar->setVisible(false); + m_passwordPromptCallback = nullptr; } bool SessionTab::validateProfileForConnect() diff --git a/src/session_tab.h b/src/session_tab.h index 6e70a8d..556b4aa 100644 --- a/src/session_tab.h +++ b/src/session_tab.h @@ -8,6 +8,7 @@ #include #include +#include #include #include @@ -19,12 +20,15 @@ class RdpDisplayWidget; class QToolButton; class QLineEdit; class QComboBox; +class QLabel; +class QPushButton; class KodoTerm; struct SessionUiPreferences { QString terminalThemeName = QStringLiteral("Dark"); bool eventsPanelExpanded = false; + int terminalFontPointSize = 0; }; class SessionTab : public QWidget @@ -46,6 +50,12 @@ public: QString terminalThemeName() const; bool supportsThemeSelection() const; bool supportsClearAction() const; + bool supportsZoom() const; + void zoomIn(); + void zoomOut(); + void resetZoom(); + void setTerminalFontPointSize(int pointSize); + int terminalFontPointSize() const; bool isEventsPanelExpanded() const; void setEventsPanelExpanded(bool expanded); void clearEvents(); @@ -56,6 +66,7 @@ signals: void tabTitleChanged(const QString& title); void tabStateChanged(SessionState state); void terminalThemeChanged(const QString& themeName); + void terminalFontSizeChanged(int pointSize); void eventsPanelVisibilityChanged(bool expanded); void requestConnect(const SessionConnectOptions& options); void requestDisconnect(); @@ -88,6 +99,7 @@ private: QString m_lastError; SessionConnectOptions m_lastConnectOptions; QString m_terminalThemeName; + int m_terminalFontPointSize; KodoTerm* m_sshTerminal; RdpDisplayWidget* m_rdpDisplay; @@ -99,6 +111,12 @@ private: QToolButton* m_clearEventsButton; QToolButton* m_exportEventsButton; QWidget* m_eventsPanel; + QWidget* m_passwordPromptBar; + QLabel* m_passwordPromptLabel; + QLineEdit* m_passwordPromptInput; + QPushButton* m_passwordPromptConnectButton; + QPushButton* m_passwordPromptCancelButton; + std::function)> m_passwordPromptCallback; enum class EventSeverity { Info, Warning, @@ -114,7 +132,10 @@ private: bool m_eventsPanelExpanded; void setupUi(); - std::optional buildConnectOptions(); + void requestConnectOptions(std::function)> callback); + void showPasswordPrompt(const QString& labelText, + std::function)> callback); + void hidePasswordPrompt(); bool validateProfileForConnect(); void appendEvent(const QString& message); void setState(SessionState state, const QString& message); diff --git a/src/session_window.cpp b/src/session_window.cpp index fccd5d1..829ea13 100644 --- a/src/session_window.cpp +++ b/src/session_window.cpp @@ -6,6 +6,7 @@ #include #include +#include #include #include #include @@ -105,6 +106,18 @@ SessionWindow::SessionWindow(const Profile& profile, QWidget* parent) clearAction = menu.addAction(QStringLiteral("Clear")); } + QAction* zoomInAction = nullptr; + QAction* zoomOutAction = nullptr; + QAction* resetZoomAction = nullptr; + QAction* setFontSizeAction = nullptr; + if (tab->supportsZoom()) { + menu.addSeparator(); + zoomInAction = menu.addAction(QStringLiteral("Increase Font Size")); + zoomOutAction = menu.addAction(QStringLiteral("Decrease Font Size")); + resetZoomAction = menu.addAction(QStringLiteral("Reset Font Size")); + setFontSizeAction = menu.addAction(QStringLiteral("Set Font Size...")); + } + QAction* chosen = menu.exec(m_tabs->tabBar()->mapToGlobal(pos)); if (chosen == disconnectAction) { tab->disconnectSession(); @@ -120,6 +133,27 @@ SessionWindow::SessionWindow(const Profile& profile, QWidget* parent) tab->clearEvents(); } else if (clearAction != nullptr && chosen == clearAction) { tab->clearTerminal(); + } else if (zoomInAction != nullptr && chosen == zoomInAction) { + tab->zoomIn(); + } else if (zoomOutAction != nullptr && chosen == zoomOutAction) { + tab->zoomOut(); + } else if (resetZoomAction != nullptr && chosen == resetZoomAction) { + tab->resetZoom(); + } else if (setFontSizeAction != nullptr && chosen == setFontSizeAction) { + bool accepted = false; + const int currentSize = + tab->terminalFontPointSize() > 0 ? tab->terminalFontPointSize() : 10; + const int newSize = QInputDialog::getInt(this, + QStringLiteral("Set Font Size"), + QStringLiteral("Font size (points):"), + currentSize, + 6, + 72, + 1, + &accepted); + if (accepted) { + tab->setTerminalFontPointSize(newSize); + } } else { for (QAction* themeAction : themeActions) { if (chosen == themeAction) { @@ -185,6 +219,16 @@ void SessionWindow::addSessionTab(const Profile& profile) : themeName.trimmed(); saveUiPreferences(); }); + connect(tab, + &SessionTab::terminalFontSizeChanged, + this, + [this](int pointSize) { + if (pointSize <= 0) { + return; + } + m_preferences.terminalFontPointSize = pointSize; + saveUiPreferences(); + }); connect(tab, &SessionTab::eventsPanelVisibilityChanged, this, @@ -216,6 +260,8 @@ void SessionWindow::loadUiPreferences() } m_preferences.eventsPanelExpanded = settings.value(QStringLiteral("session/eventsPanelExpanded"), false).toBool(); + m_preferences.terminalFontPointSize = + settings.value(QStringLiteral("session/terminalFontPointSize"), 0).toInt(); } void SessionWindow::saveUiPreferences() const @@ -225,4 +271,6 @@ void SessionWindow::saveUiPreferences() const m_preferences.terminalThemeName); settings.setValue(QStringLiteral("session/eventsPanelExpanded"), m_preferences.eventsPanelExpanded); + settings.setValue(QStringLiteral("session/terminalFontPointSize"), + m_preferences.terminalFontPointSize); } diff --git a/src/terminal_view.cpp b/src/terminal_view.cpp index 4ed5512..e2f34a2 100644 --- a/src/terminal_view.cpp +++ b/src/terminal_view.cpp @@ -52,6 +52,14 @@ void TerminalView::setThemeName(const QString& themeName) applyThemePalette(paletteByName(themeName)); } +void TerminalView::setFontPointSize(int pointSize) +{ + QFont updatedFont = font(); + updatedFont.setPointSize(pointSize); + setFont(updatedFont); + emitTerminalSize(); +} + void TerminalView::appendTerminalData(const QString& data) { if (data.isEmpty()) { diff --git a/src/terminal_view.h b/src/terminal_view.h index 20b6340..c534072 100644 --- a/src/terminal_view.h +++ b/src/terminal_view.h @@ -19,6 +19,7 @@ public: static QStringList themeNames(); void setThemeName(const QString& themeName); void appendTerminalData(const QString& data); + void setFontPointSize(int pointSize); signals: void inputGenerated(const QString& input);