From 1506d87719d8faa5b68c0139918029a18d5be5a3 Mon Sep 17 00:00:00 2001 From: Keith Smith Date: Wed, 16 Sep 2026 09:18:43 -0600 Subject: [PATCH] Make username/password connect prompts hard to miss (issue #22) The inline prompt bar previously had no explicit styling and rendered in the same color as the rest of the tab; a tab showing the prompt in the background had no indication anything needed attention. The bar now uses a solid QPalette::Highlight fill with HighlightedText for the label and a hand-drawn contrasting badge, and a background tab gets a "(Needs input)" title suffix plus a distinct tab-bar color. A first pass at the tab color (#6a1b9a) was reported unreadable in dark mode; replaced with #ab47bc, tuned to match the visibility of the existing connection-state colors. Bump version to v2026.9.16.6. Co-Authored-By: Claude Sonnet 5 --- CMakeLists.txt | 2 +- docs/PROGRESS.md | 17 ++++++++++ src/session_tab.cpp | 75 ++++++++++++++++++++++++++++++++++++++++++ src/session_tab.h | 8 +++++ src/session_window.cpp | 38 +++++++++++++++++++++ src/session_window.h | 5 +++ 6 files changed, 144 insertions(+), 1 deletion(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index 4e3fc41..2e73207 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,6 +1,6 @@ cmake_minimum_required(VERSION 3.21) -project(OrbitHub VERSION 2026.9.16.5 LANGUAGES CXX) +project(OrbitHub VERSION 2026.9.16.6 LANGUAGES CXX) set(CMAKE_CXX_STANDARD 17) set(CMAKE_CXX_STANDARD_REQUIRED ON) diff --git a/docs/PROGRESS.md b/docs/PROGRESS.md index 49bcba2..3bce18d 100644 --- a/docs/PROGRESS.md +++ b/docs/PROGRESS.md @@ -209,6 +209,23 @@ Delivered: option, not the stale profile copy, is actually used) -- RDP has no equivalent fake-server test harness, so that side relies on mirroring the already-tested `m_activeOptions.password` pattern exactly +- Issue #22: the inline username/password prompt bar used to just be a + plain `QWidget` with `setAutoFillBackground(true)` and no explicit + color, which meant it rendered in the same color as everything else + around it and was easy to miss -- especially on a tab that wasn't the + active one, where there was previously no indication anything needed + attention at all. Now uses a solid `QPalette::Highlight` fill with + `QPalette::HighlightedText` for the label (the OS theme's own + guaranteed-contrasting pair, so it stays correct under both light and + dark themes without a hardcoded color) plus a hand-drawn "?" badge + (not a themed `QStyle` icon, whose own colors are outside our control + and could land close in hue to the bar's background); a background + tab showing the prompt gets its title suffixed "(Needs input)" and its + tab-bar text colored distinctly from the four connection-state colors. + A first pass at the tab color (`#6a1b9a`) was reported unreadable in + dark mode -- its perceived luminance was well below the four existing + state colors -- and was replaced with `#ab47bc`, tuned to roughly + match their visibility - Robustness fix: an unrecognized `FramebufferUpdate` rectangle encoding used to abort the connection generically; `kAnnouncedEncodings` is now the single source of truth for what `SetEncodings` announces and what diff --git a/src/session_tab.cpp b/src/session_tab.cpp index 0cac7cf..7cf13c9 100644 --- a/src/session_tab.cpp +++ b/src/session_tab.cpp @@ -17,10 +17,14 @@ #include #include #include +#include +#include +#include #include #include #include #include +#include #include #include #include @@ -59,6 +63,34 @@ TerminalTheme themeForName(const QString& themeName) return TerminalTheme::loadKonsoleTheme( QStringLiteral(":/KodoTermThemes/konsole/Breeze.colorscheme")); } + +// A filled circle with a bold "?", used on the username/password prompt +// bar (issue #22). Drawn by hand rather than pulled from a QStyle standard +// icon because a themed icon's own internal colors are outside our +// control and could end up close in hue to the bar's own background, +// undermining the contrast the bar is trying to achieve; painting it +// ourselves guarantees fillColor/textColor are exactly the same +// guaranteed-contrasting pair used for the rest of the bar. +QPixmap questionMarkBadgePixmap(const QColor& fillColor, const QColor& textColor, int diameter) +{ + QPixmap pixmap(diameter, diameter); + pixmap.fill(Qt::transparent); + + QPainter painter(&pixmap); + painter.setRenderHint(QPainter::Antialiasing, true); + painter.setPen(Qt::NoPen); + painter.setBrush(fillColor); + painter.drawEllipse(0, 0, diameter, diameter); + + QFont font = painter.font(); + font.setBold(true); + font.setPixelSize(static_cast(diameter * 0.65)); + painter.setFont(font); + painter.setPen(textColor); + painter.drawText(QRect(0, 0, diameter, diameter), Qt::AlignCenter, QStringLiteral("?")); + + return pixmap; +} } SessionTab::SessionTab(const Profile& profile, @@ -93,10 +125,12 @@ SessionTab::SessionTab(const Profile& profile, m_exportEventsButton(nullptr), m_eventsPanel(nullptr), m_passwordPromptBar(nullptr), + m_passwordPromptIcon(nullptr), m_passwordPromptLabel(nullptr), m_passwordPromptInput(nullptr), m_passwordPromptConnectButton(nullptr), m_passwordPromptCancelButton(nullptr), + m_awaitingUserInput(false), m_eventSeverityFilter(EventSeverity::Info), m_eventsPanelExpanded(preferences.eventsPanelExpanded) { @@ -341,9 +375,17 @@ SessionTab::~SessionTab() QString SessionTab::tabTitle() const { + if (m_awaitingUserInput) { + return QStringLiteral("%1 (Needs input)").arg(m_profile.name); + } return QStringLiteral("%1 (%2)").arg(m_profile.name, stateSuffix()); } +bool SessionTab::awaitingUserInput() const +{ + return m_awaitingUserInput; +} + void SessionTab::connectSession() { if (m_state == SessionState::Connecting || m_state == SessionState::Connected) { @@ -806,21 +848,42 @@ void SessionTab::setupUi() applyTerminalTheme(m_terminalThemeName); + // Styled distinctly (issue #22: this bar used to blend straight into + // the plain window background and was easy to miss, especially on a + // tab you weren't actively looking at). A solid QPalette::Highlight + // fill with QPalette::HighlightedText for the label is used rather + // than a subtle tint -- a faint tint proved to still be easy to miss, + // and Highlight/HighlightedText are the OS theme's own guaranteed- + // contrasting pair, so this stays readable under both light and dark + // themes without hardcoding a color. auto* passwordPromptLayout = new QHBoxLayout(); + m_passwordPromptIcon = new QLabel(this); + const QColor highlight = palette().color(QPalette::Highlight); + const QColor highlightedText = palette().color(QPalette::HighlightedText); + m_passwordPromptIcon->setPixmap(questionMarkBadgePixmap(highlightedText, highlight, 22)); m_passwordPromptLabel = new QLabel(this); + m_passwordPromptLabel->setObjectName(QStringLiteral("passwordPromptLabel")); 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_passwordPromptIcon); passwordPromptLayout->addWidget(m_passwordPromptLabel); passwordPromptLayout->addWidget(m_passwordPromptInput, 1); passwordPromptLayout->addWidget(m_passwordPromptConnectButton); passwordPromptLayout->addWidget(m_passwordPromptCancelButton); + passwordPromptLayout->setContentsMargins(10, 8, 10, 8); m_passwordPromptBar = new QWidget(this); + m_passwordPromptBar->setObjectName(QStringLiteral("passwordPromptBar")); m_passwordPromptBar->setLayout(passwordPromptLayout); m_passwordPromptBar->setAutoFillBackground(true); m_passwordPromptBar->setVisible(false); + m_passwordPromptBar->setStyleSheet( + QStringLiteral("QWidget#passwordPromptBar { background-color: %1; }" + "QWidget#passwordPromptBar QLabel#passwordPromptLabel " + "{ color: %2; font-weight: bold; font-size: 11pt; }") + .arg(highlight.name(), highlightedText.name())); rootLayout->addWidget(m_passwordPromptBar); connect(m_passwordPromptConnectButton, &QPushButton::clicked, this, [this]() { @@ -1161,12 +1224,24 @@ void SessionTab::showPasswordPrompt(const QString& labelText, m_passwordPromptInput->setEchoMode(maskInput ? QLineEdit::Password : QLineEdit::Normal); m_passwordPromptBar->setVisible(true); m_passwordPromptInput->setFocus(); + + if (!m_awaitingUserInput) { + m_awaitingUserInput = true; + emit awaitingUserInputChanged(true); + emit tabTitleChanged(tabTitle()); + } } void SessionTab::hidePasswordPrompt() { m_passwordPromptBar->setVisible(false); m_passwordPromptCallback = nullptr; + + if (m_awaitingUserInput) { + m_awaitingUserInput = false; + emit awaitingUserInputChanged(false); + emit tabTitleChanged(tabTitle()); + } } bool SessionTab::validateProfileForConnect() diff --git a/src/session_tab.h b/src/session_tab.h index db4fa35..b4401a7 100644 --- a/src/session_tab.h +++ b/src/session_tab.h @@ -67,10 +67,16 @@ public: bool supportsVncScaleToggle() const; void setVncScaleToFit(bool scaleToFit); bool vncScaleToFit() const; + bool awaitingUserInput() const; signals: void tabTitleChanged(const QString& title); void tabStateChanged(SessionState state); + // Fires whenever the inline username/password prompt bar is shown or + // hidden -- independent of tabStateChanged(), since the backend is + // still just "Connecting" while it's up. Lets SessionWindow mark a + // background tab that needs the user's attention (issue #22). + void awaitingUserInputChanged(bool waiting); void terminalThemeChanged(const QString& themeName); void terminalFontSizeChanged(int pointSize); void eventsPanelVisibilityChanged(bool expanded); @@ -129,11 +135,13 @@ private: QToolButton* m_exportEventsButton; QWidget* m_eventsPanel; QWidget* m_passwordPromptBar; + QLabel* m_passwordPromptIcon; QLabel* m_passwordPromptLabel; QLineEdit* m_passwordPromptInput; QPushButton* m_passwordPromptConnectButton; QPushButton* m_passwordPromptCancelButton; std::function)> m_passwordPromptCallback; + bool m_awaitingUserInput; enum class EventSeverity { Info, Warning, diff --git a/src/session_window.cpp b/src/session_window.cpp index 2b5be3d..f4fb26f 100644 --- a/src/session_window.cpp +++ b/src/session_window.cpp @@ -34,6 +34,19 @@ QColor tabColorForState(SessionState state, const QPalette& palette) return palette.color(QPalette::WindowText); } +// Distinct from all four tabColorForState() colors -- a tab awaiting a +// username/password prompt response needs to stand out even from a +// tab that's merely "Connecting" (issue #22), including when it isn't +// the one currently in view. #6a1b9a (a much darker violet) was tried +// first and reported unreadable against a dark-theme tab bar -- its +// perceived luminance is well below the other three colors above, which +// this one is tuned to roughly match so it reads about as well as they +// do in both light and dark themes. +QColor awaitingInputTabColor() +{ + return QColor(QStringLiteral("#ab47bc")); +} + QStringList terminalThemeNames() { return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")}; @@ -57,6 +70,7 @@ SessionWindow::SessionWindow(QWidget* parent) QWidget* tab = m_tabs->widget(index); if (auto* sessionTab = qobject_cast(tab)) { sessionTab->disconnectSession(); + m_tabStates.remove(sessionTab); } m_tabs->removeTab(index); delete tab; @@ -261,6 +275,7 @@ void SessionWindow::addSessionTab(const Profile& profile) } else { setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name)); } + m_tabStates.insert(tab, SessionState::Disconnected); m_tabs->tabBar()->setTabTextColor( index, tabColorForState(SessionState::Disconnected, m_tabs->palette())); @@ -272,6 +287,13 @@ void SessionWindow::addSessionTab(const Profile& profile) &SessionTab::tabStateChanged, this, [this, tab](SessionState state) { + m_tabStates.insert(tab, state); + if (tab->awaitingUserInput()) { + // Keep the "needs input" color on top -- it'll be + // restored to reflect this state once the prompt + // resolves (see awaitingUserInputChanged below). + return; + } for (int i = 0; i < m_tabs->count(); ++i) { if (m_tabs->widget(i) == tab) { m_tabs->tabBar()->setTabTextColor( @@ -280,6 +302,22 @@ void SessionWindow::addSessionTab(const Profile& profile) } } }); + connect(tab, + &SessionTab::awaitingUserInputChanged, + this, + [this, tab](bool waiting) { + for (int i = 0; i < m_tabs->count(); ++i) { + if (m_tabs->widget(i) != tab) { + continue; + } + const QColor color = waiting + ? awaitingInputTabColor() + : tabColorForState(m_tabStates.value(tab, SessionState::Disconnected), + m_tabs->palette()); + m_tabs->tabBar()->setTabTextColor(i, color); + return; + } + }); connect(tab, &SessionTab::terminalThemeChanged, this, diff --git a/src/session_window.h b/src/session_window.h index 2ff1b24..63e88df 100644 --- a/src/session_window.h +++ b/src/session_window.h @@ -4,6 +4,7 @@ #include "profile_repository.h" #include "session_tab.h" +#include #include class QTabWidget; @@ -21,6 +22,10 @@ private: QTabWidget* m_tabs; ProfilesWindow* m_profilesWidget; SessionUiPreferences m_preferences; + // Last known connection state per tab, so the tab color can be + // restored correctly once an awaitingUserInputChanged(false) fires + // (that signal is orthogonal to SessionState -- see session_tab.h). + QHash m_tabStates; void addSessionTab(const Profile& profile); void updateTabTitle(SessionTab* tab, const QString& title);