Compare commits

...
Author SHA1 Message Date
ksmithandClaude Sonnet 5 988637eb39 Add Quick Connect: open a session without saving a profile first
File > Quick Connect... and a matching Profiles-tab button open
ProfileDialog in a new quick-connect mode (Name/Tags/Folder hidden,
Name validation skipped) and hand the resulting in-memory profile
straight to a session tab -- nothing is written to the profile
database. The tab title is derived from host/username/protocol for
display only. A Quick Connect tab's context menu gets a "Save as
Profile..." action, shown only on tabs that were never saved, that
opens the normal profile dialog pre-filled to persist it afterward.

Bump version to v2026.9.28.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-28 16:48:00 -06:00
10 changed files with 150 additions and 5 deletions

No files matched your search

+1 -1
View File
@@ -1,6 +1,6 @@
cmake_minimum_required(VERSION 3.21)
project(OrbitHub VERSION 2026.9.26.2 LANGUAGES CXX)
project(OrbitHub VERSION 2026.9.28 LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
+17
View File
@@ -424,6 +424,23 @@ Git:
- Release: [v2026.9.14.2](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.14.2) — same-day patch fixing RDP display corruption (missing/misplaced taskbar) after resizing the session window (the client never resized its own display buffer for channel-driven RDP resizes)
- Release: [v2026.9.15](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.15) — adds Import/Export for profile lists (File menu)
- Quick Connect (issue #23): a `File > Quick Connect...` action and a
matching button on the Profiles tab open a connection without saving a
profile first. Reuses `ProfileDialog` via a new `setQuickConnectMode()`
toggle (Name/Tags/Folder hidden, Name validation skipped) rather than a
second dialog class, so all protocol-specific field logic
(`refreshAuthFields()`) stays in one place. The tab title is derived
from host/username/protocol (e.g. `user@host`) purely for display --
never written to the database. A Quick Connect tab's right-click menu
gets a "Save as Profile..." action (absent on normal profile-backed
tabs) that opens the same dialog pre-filled to persist it afterward.
Detecting a Quick Connect tab needed no new state: `ProfileDialog::
profile()` already always returns `id = -1` for an unsaved profile,
and Quick Connect is the only path today that hands such a profile to
`SessionWindow::addSessionTab()`. GUI-only change with no automated
test coverage (consistent with `ProfileDialog` never having had any);
verified manually by running the built binary
## Milestone 10 - v1.0 Stabilization
Status: Planned
+40 -2
View File
@@ -65,7 +65,10 @@ ProfileDialog::ProfileDialog(QWidget* parent)
m_rdpSecurityModeInput(new QComboBox(this)),
m_rdpPerformanceProfileInput(new QComboBox(this)),
m_protocolHint(new QLabel(this)),
m_folderHint(new QLabel(this))
m_folderHint(new QLabel(this)),
m_nameLabel(nullptr),
m_tagsLabel(nullptr),
m_quickConnectMode(false)
{
resize(560, 360);
@@ -145,6 +148,9 @@ ProfileDialog::ProfileDialog(QWidget* parent)
form->addRow(QStringLiteral("RDP Security"), m_rdpSecurityModeInput);
form->addRow(QStringLiteral("RDP Performance"), m_rdpPerformanceProfileInput);
m_nameLabel = form->labelForField(m_nameInput);
m_tagsLabel = form->labelForField(m_tagsInput);
auto* note = new QLabel(
QStringLiteral("Passwords are requested at connect time and are not stored."),
this);
@@ -176,6 +182,24 @@ void ProfileDialog::setDefaultFolderPath(const QString& folderPath)
refreshAuthFields();
}
void ProfileDialog::setQuickConnectMode(bool enabled)
{
m_quickConnectMode = enabled;
const bool showOrganizationalFields = !enabled;
if (m_nameLabel != nullptr) {
m_nameLabel->setVisible(showOrganizationalFields);
}
m_nameInput->setVisible(showOrganizationalFields);
if (m_tagsLabel != nullptr) {
m_tagsLabel->setVisible(showOrganizationalFields);
}
m_tagsInput->setVisible(showOrganizationalFields);
m_folderHint->setVisible(showOrganizationalFields);
refreshAuthFields();
}
void ProfileDialog::setProfile(const Profile& profile)
{
m_nameInput->setText(profile.name);
@@ -237,12 +261,26 @@ Profile ProfileDialog::profile() const
profile.rdpPerformanceProfile = protocol == QStringLiteral("RDP")
? m_rdpPerformanceProfileInput->currentText()
: QStringLiteral("Balanced");
if (m_quickConnectMode) {
// Name is never entered in Quick Connect mode -- derive something
// usable for the session tab's title. Only ever a starting point:
// never written to the database directly, and fully editable if
// the user later saves this connection as a real profile.
profile.name = profile.username.isEmpty()
? profile.host
: QStringLiteral("%1@%2").arg(profile.username, profile.host);
if (protocol != QStringLiteral("SSH")) {
profile.name = QStringLiteral("%1 (%2)").arg(profile.name, protocol);
}
}
return profile;
}
void ProfileDialog::accept()
{
if (m_nameInput->text().trimmed().isEmpty()) {
if (!m_quickConnectMode && m_nameInput->text().trimmed().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Validation Error"),
QStringLiteral("Profile name is required."));
+9
View File
@@ -21,6 +21,12 @@ public:
void setDialogTitle(const QString& title);
void setDefaultFolderPath(const QString& folderPath);
void setProfile(const Profile& profile);
// Hides Name/Tags/Folder and skips the Name-required validation, for
// Quick Connect (issue #23) -- a connection opened without ever being
// saved as a profile. profile() derives a usable name from
// host/username/protocol instead. Everything else (protocol-specific
// fields, defaults) behaves identically to a normal New Profile.
void setQuickConnectMode(bool enabled);
Profile profile() const;
protected:
@@ -42,7 +48,10 @@ private:
QComboBox* m_rdpPerformanceProfileInput;
QLabel* m_protocolHint;
QLabel* m_folderHint;
QWidget* m_nameLabel;
QWidget* m_tagsLabel;
QString m_defaultFolderPath;
bool m_quickConnectMode;
void refreshAuthFields();
};
+24 -2
View File
@@ -151,6 +151,7 @@ ProfilesWindow::ProfilesWindow(QWidget* parent)
m_newButton(nullptr),
m_editButton(nullptr),
m_deleteButton(nullptr),
m_quickConnectButton(nullptr),
m_repository(std::make_unique<ProfileRepository>())
{
setupUi();
@@ -226,10 +227,12 @@ void ProfilesWindow::setupUi()
m_newButton = new QPushButton(QStringLiteral("New"), this);
m_editButton = new QPushButton(QStringLiteral("Edit"), this);
m_deleteButton = new QPushButton(QStringLiteral("Delete"), this);
m_quickConnectButton = new QPushButton(QStringLiteral("Quick Connect"), this);
buttonRow->addWidget(m_newButton);
buttonRow->addWidget(m_editButton);
buttonRow->addWidget(m_deleteButton);
buttonRow->addWidget(m_quickConnectButton);
buttonRow->addStretch();
auto* filterRow = new QHBoxLayout();
@@ -300,6 +303,7 @@ void ProfilesWindow::setupUi()
connect(m_newButton, &QPushButton::clicked, this, [this]() { createProfile(); });
connect(m_editButton, &QPushButton::clicked, this, [this]() { editSelectedProfile(); });
connect(m_deleteButton, &QPushButton::clicked, this, [this]() { deleteSelectedProfile(); });
connect(m_quickConnectButton, &QPushButton::clicked, this, [this]() { quickConnect(); });
}
void ProfilesWindow::loadProfiles()
@@ -932,7 +936,11 @@ void ProfilesWindow::createProfile(const QString& defaultFolderPath)
return;
}
const Profile newProfile = dialog.profile();
createProfileFromExisting(dialog.profile());
}
bool ProfilesWindow::createProfileFromExisting(const Profile& newProfile)
{
if (!newProfile.folderPath.trimmed().isEmpty()) {
m_repository->createFolder(newProfile.folderPath);
}
@@ -944,10 +952,24 @@ void ProfilesWindow::createProfile(const QString& defaultFolderPath)
.arg(m_repository->lastError().isEmpty()
? QStringLiteral("unknown error")
: m_repository->lastError()));
return;
return false;
}
loadProfiles();
return true;
}
void ProfilesWindow::quickConnect()
{
ProfileDialog dialog(this);
dialog.setDialogTitle(QStringLiteral("Quick Connect"));
dialog.setQuickConnectMode(true);
if (dialog.exec() != QDialog::Accepted) {
return;
}
emit connectRequested(dialog.profile());
}
void ProfilesWindow::editSelectedProfile()
+13
View File
@@ -35,6 +35,18 @@ public:
void exportProfiles();
void importProfiles();
void importFromMRemoteNG();
// Opens the New/Edit Profile dialog with Name/Tags/Folder hidden (see
// ProfileDialog::setQuickConnectMode()) and, on accept, emits
// connectRequested() directly -- never writes to the repository
// (issue #23).
void quickConnect();
// Persists an already-built Profile (e.g. from ProfileDialog::profile())
// the same way createProfile() does after its own dialog accepts:
// optional folder creation, repository insert with a failure warning,
// then refresh. Used by SessionWindow's "Save as Profile..." action so
// repository access stays inside ProfilesWindow. Returns true on
// success.
bool createProfileFromExisting(const Profile& newProfile);
signals:
void connectRequested(const Profile& profile);
@@ -49,6 +61,7 @@ private:
QPushButton* m_newButton;
QPushButton* m_editButton;
QPushButton* m_deleteButton;
QPushButton* m_quickConnectButton;
std::unique_ptr<ProfileRepository> m_repository;
std::unordered_map<qint64, Profile> m_profileCache;
QString m_pendingTagFilterPreference;
+10
View File
@@ -387,6 +387,16 @@ bool SessionTab::awaitingUserInput() const
return m_awaitingUserInput;
}
bool SessionTab::isQuickConnect() const
{
return m_profile.id <= 0;
}
Profile SessionTab::profileForSaving() const
{
return m_profile;
}
void SessionTab::connectSession()
{
if (m_state == SessionState::Connecting || m_state == SessionState::Connected) {
+8
View File
@@ -69,6 +69,14 @@ public:
void setVncScaleToFit(bool scaleToFit);
bool vncScaleToFit() const;
bool awaitingUserInput() const;
// True if this tab's profile was never saved to the repository (i.e.
// it came from Quick Connect, issue #23, rather than a saved profile).
// ProfileDialog::profile() always returns id <= 0 for an unsaved
// profile, and Quick Connect is the only path that hands such a
// profile to SessionWindow::addSessionTab() -- every other path looks
// the profile up from the repository first, which returns id > 0.
bool isQuickConnect() const;
Profile profileForSaving() const;
signals:
void tabTitleChanged(const QString& title);
+27
View File
@@ -1,6 +1,7 @@
#include "session_window.h"
#include "about_dialog.h"
#include "profile_dialog.h"
#include "profiles_window.h"
#include "user_guide_dialog.h"
#include <QApplication>
@@ -148,6 +149,12 @@ SessionWindow::SessionWindow(QWidget* parent)
actualSizeAction->setChecked(!tab->vncScaleToFit());
}
QAction* saveAsProfileAction = nullptr;
if (tab->isQuickConnect()) {
menu.addSeparator();
saveAsProfileAction = menu.addAction(QStringLiteral("Save as Profile..."));
}
QAction* chosen = menu.exec(m_tabs->tabBar()->mapToGlobal(pos));
if (chosen == disconnectAction) {
tab->disconnectSession();
@@ -188,6 +195,8 @@ SessionWindow::SessionWindow(QWidget* parent)
tab->setVncScaleToFit(true);
} else if (actualSizeAction != nullptr && chosen == actualSizeAction) {
tab->setVncScaleToFit(false);
} else if (saveAsProfileAction != nullptr && chosen == saveAsProfileAction) {
saveTabAsProfile(tab);
} else {
for (QAction* themeAction : themeActions) {
if (chosen == themeAction) {
@@ -201,6 +210,7 @@ SessionWindow::SessionWindow(QWidget* parent)
QMenu* fileMenu = menuBar()->addMenu(QStringLiteral("File"));
QAction* newProfileAction = fileMenu->addAction(QStringLiteral("New Profile"));
QAction* newFolderAction = fileMenu->addAction(QStringLiteral("New Folder"));
QAction* quickConnectAction = fileMenu->addAction(QStringLiteral("Quick Connect..."));
fileMenu->addSeparator();
QAction* importProfilesAction = fileMenu->addAction(QStringLiteral("Import Profiles..."));
QAction* exportProfilesAction = fileMenu->addAction(QStringLiteral("Export Profiles..."));
@@ -216,6 +226,10 @@ SessionWindow::SessionWindow(QWidget* parent)
&QAction::triggered,
this,
[this]() { m_profilesWidget->createFolderInCurrentContext(); });
connect(quickConnectAction,
&QAction::triggered,
this,
[this]() { m_profilesWidget->quickConnect(); });
connect(importProfilesAction,
&QAction::triggered,
this,
@@ -368,6 +382,19 @@ int SessionWindow::sessionTabCount() const
return m_tabs->count() - 1;
}
void SessionWindow::saveTabAsProfile(SessionTab* tab)
{
ProfileDialog dialog(this);
dialog.setDialogTitle(QStringLiteral("Save as Profile"));
dialog.setProfile(tab->profileForSaving());
if (dialog.exec() != QDialog::Accepted) {
return;
}
m_profilesWidget->createProfileFromExisting(dialog.profile());
}
void SessionWindow::loadUiPreferences()
{
QSettings settings;
+1
View File
@@ -32,6 +32,7 @@ private:
void loadUiPreferences();
void saveUiPreferences() const;
int sessionTabCount() const;
void saveTabAsProfile(SessionTab* tab);
};
#endif