Internal
Public Access
Compare commits
3
Commits
v2026.9.26.2
..
main
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
997e2fb976 | ||
|
|
e6023a884f | ||
|
|
988637eb39 |
No files matched your search
@@ -0,0 +1,20 @@
|
|||||||
|
# OrbitHub
|
||||||
|
|
||||||
|
## Release checklist
|
||||||
|
|
||||||
|
Every release (new tag + build + publish) must include:
|
||||||
|
|
||||||
|
- **Update `README.md`** to match current reality before tagging:
|
||||||
|
latest published release version/link, the Implemented Features list
|
||||||
|
(add/adjust bullets for anything shipped since the last release), and
|
||||||
|
any status notes (e.g. VNC/Apple-auth caveats) that changed.
|
||||||
|
- Update `docs/PROGRESS.md` with a bullet describing what shipped.
|
||||||
|
- Bump the version in `CMakeLists.txt`.
|
||||||
|
- Build and test all reachable platforms (Linux local, Windows and macOS
|
||||||
|
over SSH — see prior release commits/sessions for the build scripts
|
||||||
|
used) before publishing.
|
||||||
|
|
||||||
|
`docs/USER_GUIDE.md` (in-app `Help -> User Guide` and the release PDF)
|
||||||
|
should also be checked for drift when a change affects user-facing
|
||||||
|
behavior, though it doesn't need a pass on every single release the way
|
||||||
|
README.md does.
|
||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
cmake_minimum_required(VERSION 3.21)
|
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 17)
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
|||||||
@@ -11,13 +11,12 @@ Supported target platforms:
|
|||||||
|
|
||||||
## Current Status
|
## Current Status
|
||||||
|
|
||||||
OrbitHub is in active development.
|
OrbitHub is in active development, with all planned milestones (M0-M10)
|
||||||
|
complete.
|
||||||
|
|
||||||
- Milestones completed: M0-M9
|
- Latest published release: `v2026.9.28`
|
||||||
- Current milestone: Milestone 10 (v1.0 Stabilization)
|
- Versioned by date rather than semantic/`v1.0`-style versioning; see
|
||||||
- Latest published release: `v2026.9.16`; a `v2026.9.16.6` release is
|
[docs/PROGRESS.md](docs/PROGRESS.md) for the full release history
|
||||||
drafted with several post-`.16` fixes and is pending a macOS installer
|
|
||||||
before publishing
|
|
||||||
- VNC (M6) is fully built out: Raw/CopyRect/Hextile/ZRLE/Tight encodings,
|
- VNC (M6) is fully built out: Raw/CopyRect/Hextile/ZRLE/Tight encodings,
|
||||||
bidirectional clipboard sync, remote cursor shape sync, and both
|
bidirectional clipboard sync, remote cursor shape sync, and both
|
||||||
scale-to-fit and actual-size display modes. Standard VNC Authentication
|
scale-to-fit and actual-size display modes. Standard VNC Authentication
|
||||||
@@ -31,7 +30,7 @@ Progress and milestone details:
|
|||||||
- [docs/PROGRESS.md](docs/PROGRESS.md)
|
- [docs/PROGRESS.md](docs/PROGRESS.md)
|
||||||
|
|
||||||
Latest published release (installers for Windows, Linux, and macOS):
|
Latest published release (installers for Windows, Linux, and macOS):
|
||||||
- [v2026.9.16](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.16)
|
- [v2026.9.28](https://git.darksingularity.org/DarkSingularity/orbithub/releases/tag/v2026.9.28)
|
||||||
|
|
||||||
User Guide:
|
User Guide:
|
||||||
- [docs/USER_GUIDE.md](docs/USER_GUIDE.md) (also available as a PDF attached to each release, and in-app via `Help -> User Guide`)
|
- [docs/USER_GUIDE.md](docs/USER_GUIDE.md) (also available as a PDF attached to each release, and in-app via `Help -> User Guide`)
|
||||||
@@ -69,6 +68,10 @@ An embedded RDP session in a tab.
|
|||||||
- Drag-and-drop profile moves between folders with persistence
|
- Drag-and-drop profile moves between folders with persistence
|
||||||
- Import/Export of the whole profile list as JSON, and one-way import from
|
- Import/Export of the whole profile list as JSON, and one-way import from
|
||||||
mRemoteNG connection XML files (passwords are never imported)
|
mRemoteNG connection XML files (passwords are never imported)
|
||||||
|
- Quick Connect: open a session without saving a profile first (`File`
|
||||||
|
menu or a button on the Profiles tab); a "Save as Profile..." action on
|
||||||
|
the resulting tab persists it afterward, pre-filled with what was
|
||||||
|
entered
|
||||||
|
|
||||||
### Session Experience
|
### Session Experience
|
||||||
|
|
||||||
@@ -97,6 +100,7 @@ An embedded RDP session in a tab.
|
|||||||
- Resize handling and resolution renegotiation
|
- Resize handling and resolution renegotiation
|
||||||
- Domain-aware authentication support
|
- Domain-aware authentication support
|
||||||
- RDP security/performance profile options
|
- RDP security/performance profile options
|
||||||
|
- Bidirectional clipboard sync
|
||||||
|
|
||||||
### VNC
|
### VNC
|
||||||
|
|
||||||
@@ -122,6 +126,7 @@ An embedded RDP session in a tab.
|
|||||||
- `File` menu:
|
- `File` menu:
|
||||||
- New Profile
|
- New Profile
|
||||||
- New Folder
|
- New Folder
|
||||||
|
- Quick Connect...
|
||||||
- Import Profiles...
|
- Import Profiles...
|
||||||
- Export Profiles...
|
- Export Profiles...
|
||||||
- Import from mRemoteNG...
|
- Import from mRemoteNG...
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
# OrbitHub Codex Build Prompt (Pure C++ / Qt6 Widgets)
|
|
||||||
|
|
||||||
You are Codex. You will implement OrbitHub entirely in C++ using Qt6 Widgets.
|
|
||||||
|
|
||||||
The previous Python/Shiboken implementation is abandoned.
|
|
||||||
|
|
||||||
## Rules
|
|
||||||
|
|
||||||
- Use modern CMake
|
|
||||||
- Use Qt6 Widgets only
|
|
||||||
- Use C++17 or newer
|
|
||||||
- Never block the GUI thread
|
|
||||||
- Use Git for checkpointing
|
|
||||||
- Maintain docs/PROGRESS.md
|
|
||||||
|
|
||||||
## Required Git Workflow
|
|
||||||
|
|
||||||
Create milestone branches:
|
|
||||||
- milestone-0-restart-cpp
|
|
||||||
- milestone-1-storage
|
|
||||||
|
|
||||||
Make small logical commits.
|
|
||||||
|
|
||||||
Tag milestone completions:
|
|
||||||
- v0-m0-done
|
|
||||||
- v0-m1-done
|
|
||||||
|
|
||||||
## Milestone 0 Deliverable
|
|
||||||
|
|
||||||
Implement:
|
|
||||||
- ProfilesWindow (QMainWindow)
|
|
||||||
- SessionWindow (QMainWindow)
|
|
||||||
- QTabWidget hosting placeholder QWidget
|
|
||||||
- main.cpp wiring
|
|
||||||
|
|
||||||
Stop after Milestone 0 is complete and tagged.
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
You are Codex and you will do ALL implementation work for OrbitHub.
|
|
||||||
|
|
||||||
1) Read docs/OrbitHub_v0_Spec_CPP_QtWidgets.md
|
|
||||||
2) Read docs/OrbitHub_Codex_Build_Prompt_CPP_QtWidgets.md
|
|
||||||
|
|
||||||
Follow the Build Prompt strictly.
|
|
||||||
Use Git branches, commits, and tags.
|
|
||||||
Update docs/PROGRESS.md.
|
|
||||||
|
|
||||||
Stop after Milestone 0 and tag v0-m0-done.
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
# OrbitHub v0 Specification (Pure C++ / Qt6 Widgets)
|
|
||||||
|
|
||||||
## Overview
|
|
||||||
|
|
||||||
OrbitHub is a cross-platform native desktop application for managing and launching remote sessions.
|
|
||||||
|
|
||||||
Platforms:
|
|
||||||
- Windows 11
|
|
||||||
- Linux (Mint/Ubuntu-family)
|
|
||||||
- macOS
|
|
||||||
|
|
||||||
Implementation:
|
|
||||||
- 100% C++
|
|
||||||
- Qt 6 Widgets (no Qt Quick)
|
|
||||||
- CMake build system
|
|
||||||
- MIT License
|
|
||||||
|
|
||||||
This replaces the earlier Python-based architecture.
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## UX Model
|
|
||||||
|
|
||||||
OrbitHub uses a two-window model:
|
|
||||||
|
|
||||||
1) Profiles Window (single instance)
|
|
||||||
- List of saved profiles
|
|
||||||
- Search box
|
|
||||||
- New / Edit / Delete
|
|
||||||
- Double-click connects
|
|
||||||
|
|
||||||
2) Session Window(s)
|
|
||||||
- QTabWidget
|
|
||||||
- One tab per session
|
|
||||||
- Close tab destroys session
|
|
||||||
- Placeholder view widget displays:
|
|
||||||
"OrbitHub Native Surface"
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 0
|
|
||||||
|
|
||||||
- Fresh C++/Qt repo scaffold
|
|
||||||
- App builds
|
|
||||||
- Profiles window launches
|
|
||||||
- Session window opens with placeholder tab
|
|
||||||
- Tag: v0-m0-done
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 1
|
|
||||||
|
|
||||||
- SQLite integration
|
|
||||||
- Profiles CRUD
|
|
||||||
- Connect loads profile name into tab
|
|
||||||
- Tag: v0-m1-done
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 2
|
|
||||||
|
|
||||||
- Extend profiles schema (`host`, `port`, `username`, `protocol`, `auth_mode`)
|
|
||||||
- Replace prompt-based create/edit with a structured profile dialog form
|
|
||||||
- Connect loads full profile details into session tab
|
|
||||||
- Session lifecycle states in UI (`Connecting`, `Connected`, `Failed`) with non-blocking updates
|
|
||||||
- Tag: v0-m2-done
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 3
|
|
||||||
|
|
||||||
- Real SSH backend using native `ssh` process (connect, disconnect, reconnect)
|
|
||||||
- Protocol backend abstraction with worker-thread execution
|
|
||||||
- RDP/VNC explicitly marked as not implemented in session UX
|
|
||||||
- Connect-time credential prompts (password/private key path) with no secret storage in DB
|
|
||||||
- Session tab controls: `Connect`, `Disconnect`, `Reconnect`, `Copy Error`
|
|
||||||
- Per-session timestamped event log and user-friendly error mapping
|
|
||||||
- Profile schema extended with `private_key_path` and `known_hosts_policy`
|
|
||||||
- Tag: v0-m3-done
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 4
|
|
||||||
|
|
||||||
- Interactive embedded SSH terminal (`KodoTerm` + `libvterm`)
|
|
||||||
- SSH host-key trust prompt flow for `Ask` policy
|
|
||||||
- Improved SSH auth flow for password / private key
|
|
||||||
- Terminal utilities and UX polish (theme, clear, resize/input behavior)
|
|
||||||
- Session lifecycle UX cleanup (auto-connect, disconnect on close, tab state indicators)
|
|
||||||
- Tag: v0-m4-done
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 5 - RDP Fully Working
|
|
||||||
|
|
||||||
- Implement complete RDP protocol support (replace not-implemented RDP path)
|
|
||||||
- Deliver a usable in-app RDP session experience consistent with SSH tab UX
|
|
||||||
- Support RDP connect/disconnect/reconnect lifecycle from OrbitHub
|
|
||||||
- Add required RDP-specific connect options in profile/session flows
|
|
||||||
- Normalize event/error reporting with existing SSH behavior
|
|
||||||
- Planned Tag: v0-m5-done
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 6 - VNC Fully Working
|
|
||||||
|
|
||||||
- Implement complete VNC protocol support (replace not-implemented VNC path)
|
|
||||||
- Deliver a usable in-app VNC session experience consistent with SSH/RDP tab UX
|
|
||||||
- Support VNC connect/disconnect/reconnect lifecycle from OrbitHub
|
|
||||||
- Add required VNC-specific connect options in profile/session flows
|
|
||||||
- Normalize event/error reporting with SSH/RDP behavior
|
|
||||||
- Planned Tag: v0-m6-done
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 7 - Cross-Platform Protocol Hardening
|
|
||||||
|
|
||||||
- Validate SSH/RDP/VNC behavior on Windows, Linux, and macOS
|
|
||||||
- Resolve platform-specific path/process/auth differences
|
|
||||||
- Improve diagnostics and failure messaging for common protocol issues
|
|
||||||
- Add protocol regression checklist and repeatable verification scripts
|
|
||||||
- Planned Tag: v0-m7-done
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 8 - Profile and Session UX Completion
|
|
||||||
|
|
||||||
- Complete profile fields/validation for all protocols
|
|
||||||
- Add quality-of-life controls for active sessions (tab context actions, defaults, persistence)
|
|
||||||
- Persist per-user UI/session preferences (theme, panel visibility, terminal defaults)
|
|
||||||
- Improve session history/event visibility and filtering
|
|
||||||
- Planned Tag: v0-m8-done
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 9 - Packaging and Distribution
|
|
||||||
|
|
||||||
- Produce distributable artifacts for Windows, Linux, and macOS
|
|
||||||
- Document dependency/runtime requirements per platform
|
|
||||||
- Add release build scripts for reproducible packaging
|
|
||||||
- Validate clean install + first-run flow on each platform
|
|
||||||
- Planned Tag: v0-m9-done
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
## Milestone 10 - v1.0 Stabilization
|
|
||||||
|
|
||||||
- End-to-end QA pass across core workflows (profile CRUD + SSH/RDP/VNC session lifecycle)
|
|
||||||
- Fix blocker/critical defects from validation
|
|
||||||
- Finalize docs (`BUILDING`, usage notes, known limitations)
|
|
||||||
- Prepare v1.0 release notes and final acceptance checklist
|
|
||||||
- Planned Tag: v1.0.0
|
|
||||||
+30
-5
@@ -424,11 +424,36 @@ 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.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)
|
- 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
|
## Milestone 10 - v1.0 Stabilization
|
||||||
|
|
||||||
Status: Planned
|
Status: Completed
|
||||||
|
|
||||||
Planned Scope:
|
All planned milestones (M0-M10) are now complete. OrbitHub has shipped a
|
||||||
- Run final regression and acceptance testing across all protocols
|
long series of dated releases (see the release list under Milestone 9,
|
||||||
- Resolve release-blocking defects
|
plus everything since) covering SSH, RDP, and VNC end to end, profile
|
||||||
- Finalize docs and publish v1.0 release notes/checklist
|
management (folders/tags/search/Import-Export/Quick Connect), and
|
||||||
|
packaging for all three platforms. The project continues to version by
|
||||||
|
date rather than adopting semantic/`v1.0`-style versioning.
|
||||||
|
|
||||||
|
Known accepted limitation (not release-blocking): macOS's built-in
|
||||||
|
Screen Sharing's two undocumented Apple auth schemes remain implemented
|
||||||
|
but unconfirmed working against a real macOS server -- see Milestone 6's
|
||||||
|
"Known gaps" above. Every other protocol/server combination in scope
|
||||||
|
works end to end.
|
||||||
+40
-2
@@ -65,7 +65,10 @@ ProfileDialog::ProfileDialog(QWidget* parent)
|
|||||||
m_rdpSecurityModeInput(new QComboBox(this)),
|
m_rdpSecurityModeInput(new QComboBox(this)),
|
||||||
m_rdpPerformanceProfileInput(new QComboBox(this)),
|
m_rdpPerformanceProfileInput(new QComboBox(this)),
|
||||||
m_protocolHint(new QLabel(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);
|
resize(560, 360);
|
||||||
|
|
||||||
@@ -145,6 +148,9 @@ ProfileDialog::ProfileDialog(QWidget* parent)
|
|||||||
form->addRow(QStringLiteral("RDP Security"), m_rdpSecurityModeInput);
|
form->addRow(QStringLiteral("RDP Security"), m_rdpSecurityModeInput);
|
||||||
form->addRow(QStringLiteral("RDP Performance"), m_rdpPerformanceProfileInput);
|
form->addRow(QStringLiteral("RDP Performance"), m_rdpPerformanceProfileInput);
|
||||||
|
|
||||||
|
m_nameLabel = form->labelForField(m_nameInput);
|
||||||
|
m_tagsLabel = form->labelForField(m_tagsInput);
|
||||||
|
|
||||||
auto* note = new QLabel(
|
auto* note = new QLabel(
|
||||||
QStringLiteral("Passwords are requested at connect time and are not stored."),
|
QStringLiteral("Passwords are requested at connect time and are not stored."),
|
||||||
this);
|
this);
|
||||||
@@ -176,6 +182,24 @@ void ProfileDialog::setDefaultFolderPath(const QString& folderPath)
|
|||||||
refreshAuthFields();
|
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)
|
void ProfileDialog::setProfile(const Profile& profile)
|
||||||
{
|
{
|
||||||
m_nameInput->setText(profile.name);
|
m_nameInput->setText(profile.name);
|
||||||
@@ -237,12 +261,26 @@ Profile ProfileDialog::profile() const
|
|||||||
profile.rdpPerformanceProfile = protocol == QStringLiteral("RDP")
|
profile.rdpPerformanceProfile = protocol == QStringLiteral("RDP")
|
||||||
? m_rdpPerformanceProfileInput->currentText()
|
? m_rdpPerformanceProfileInput->currentText()
|
||||||
: QStringLiteral("Balanced");
|
: 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;
|
return profile;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProfileDialog::accept()
|
void ProfileDialog::accept()
|
||||||
{
|
{
|
||||||
if (m_nameInput->text().trimmed().isEmpty()) {
|
if (!m_quickConnectMode && m_nameInput->text().trimmed().isEmpty()) {
|
||||||
QMessageBox::warning(this,
|
QMessageBox::warning(this,
|
||||||
QStringLiteral("Validation Error"),
|
QStringLiteral("Validation Error"),
|
||||||
QStringLiteral("Profile name is required."));
|
QStringLiteral("Profile name is required."));
|
||||||
|
|||||||
@@ -21,6 +21,12 @@ public:
|
|||||||
void setDialogTitle(const QString& title);
|
void setDialogTitle(const QString& title);
|
||||||
void setDefaultFolderPath(const QString& folderPath);
|
void setDefaultFolderPath(const QString& folderPath);
|
||||||
void setProfile(const Profile& profile);
|
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;
|
Profile profile() const;
|
||||||
|
|
||||||
protected:
|
protected:
|
||||||
@@ -42,7 +48,10 @@ private:
|
|||||||
QComboBox* m_rdpPerformanceProfileInput;
|
QComboBox* m_rdpPerformanceProfileInput;
|
||||||
QLabel* m_protocolHint;
|
QLabel* m_protocolHint;
|
||||||
QLabel* m_folderHint;
|
QLabel* m_folderHint;
|
||||||
|
QWidget* m_nameLabel;
|
||||||
|
QWidget* m_tagsLabel;
|
||||||
QString m_defaultFolderPath;
|
QString m_defaultFolderPath;
|
||||||
|
bool m_quickConnectMode;
|
||||||
|
|
||||||
void refreshAuthFields();
|
void refreshAuthFields();
|
||||||
};
|
};
|
||||||
|
|||||||
+24
-2
@@ -151,6 +151,7 @@ ProfilesWindow::ProfilesWindow(QWidget* parent)
|
|||||||
m_newButton(nullptr),
|
m_newButton(nullptr),
|
||||||
m_editButton(nullptr),
|
m_editButton(nullptr),
|
||||||
m_deleteButton(nullptr),
|
m_deleteButton(nullptr),
|
||||||
|
m_quickConnectButton(nullptr),
|
||||||
m_repository(std::make_unique<ProfileRepository>())
|
m_repository(std::make_unique<ProfileRepository>())
|
||||||
{
|
{
|
||||||
setupUi();
|
setupUi();
|
||||||
@@ -226,10 +227,12 @@ void ProfilesWindow::setupUi()
|
|||||||
m_newButton = new QPushButton(QStringLiteral("New"), this);
|
m_newButton = new QPushButton(QStringLiteral("New"), this);
|
||||||
m_editButton = new QPushButton(QStringLiteral("Edit"), this);
|
m_editButton = new QPushButton(QStringLiteral("Edit"), this);
|
||||||
m_deleteButton = new QPushButton(QStringLiteral("Delete"), this);
|
m_deleteButton = new QPushButton(QStringLiteral("Delete"), this);
|
||||||
|
m_quickConnectButton = new QPushButton(QStringLiteral("Quick Connect"), this);
|
||||||
|
|
||||||
buttonRow->addWidget(m_newButton);
|
buttonRow->addWidget(m_newButton);
|
||||||
buttonRow->addWidget(m_editButton);
|
buttonRow->addWidget(m_editButton);
|
||||||
buttonRow->addWidget(m_deleteButton);
|
buttonRow->addWidget(m_deleteButton);
|
||||||
|
buttonRow->addWidget(m_quickConnectButton);
|
||||||
buttonRow->addStretch();
|
buttonRow->addStretch();
|
||||||
|
|
||||||
auto* filterRow = new QHBoxLayout();
|
auto* filterRow = new QHBoxLayout();
|
||||||
@@ -300,6 +303,7 @@ void ProfilesWindow::setupUi()
|
|||||||
connect(m_newButton, &QPushButton::clicked, this, [this]() { createProfile(); });
|
connect(m_newButton, &QPushButton::clicked, this, [this]() { createProfile(); });
|
||||||
connect(m_editButton, &QPushButton::clicked, this, [this]() { editSelectedProfile(); });
|
connect(m_editButton, &QPushButton::clicked, this, [this]() { editSelectedProfile(); });
|
||||||
connect(m_deleteButton, &QPushButton::clicked, this, [this]() { deleteSelectedProfile(); });
|
connect(m_deleteButton, &QPushButton::clicked, this, [this]() { deleteSelectedProfile(); });
|
||||||
|
connect(m_quickConnectButton, &QPushButton::clicked, this, [this]() { quickConnect(); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProfilesWindow::loadProfiles()
|
void ProfilesWindow::loadProfiles()
|
||||||
@@ -932,7 +936,11 @@ void ProfilesWindow::createProfile(const QString& defaultFolderPath)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const Profile newProfile = dialog.profile();
|
createProfileFromExisting(dialog.profile());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool ProfilesWindow::createProfileFromExisting(const Profile& newProfile)
|
||||||
|
{
|
||||||
if (!newProfile.folderPath.trimmed().isEmpty()) {
|
if (!newProfile.folderPath.trimmed().isEmpty()) {
|
||||||
m_repository->createFolder(newProfile.folderPath);
|
m_repository->createFolder(newProfile.folderPath);
|
||||||
}
|
}
|
||||||
@@ -944,10 +952,24 @@ void ProfilesWindow::createProfile(const QString& defaultFolderPath)
|
|||||||
.arg(m_repository->lastError().isEmpty()
|
.arg(m_repository->lastError().isEmpty()
|
||||||
? QStringLiteral("unknown error")
|
? QStringLiteral("unknown error")
|
||||||
: m_repository->lastError()));
|
: m_repository->lastError()));
|
||||||
return;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
loadProfiles();
|
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()
|
void ProfilesWindow::editSelectedProfile()
|
||||||
|
|||||||
@@ -35,6 +35,18 @@ public:
|
|||||||
void exportProfiles();
|
void exportProfiles();
|
||||||
void importProfiles();
|
void importProfiles();
|
||||||
void importFromMRemoteNG();
|
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:
|
signals:
|
||||||
void connectRequested(const Profile& profile);
|
void connectRequested(const Profile& profile);
|
||||||
@@ -49,6 +61,7 @@ private:
|
|||||||
QPushButton* m_newButton;
|
QPushButton* m_newButton;
|
||||||
QPushButton* m_editButton;
|
QPushButton* m_editButton;
|
||||||
QPushButton* m_deleteButton;
|
QPushButton* m_deleteButton;
|
||||||
|
QPushButton* m_quickConnectButton;
|
||||||
std::unique_ptr<ProfileRepository> m_repository;
|
std::unique_ptr<ProfileRepository> m_repository;
|
||||||
std::unordered_map<qint64, Profile> m_profileCache;
|
std::unordered_map<qint64, Profile> m_profileCache;
|
||||||
QString m_pendingTagFilterPreference;
|
QString m_pendingTagFilterPreference;
|
||||||
|
|||||||
@@ -387,6 +387,16 @@ bool SessionTab::awaitingUserInput() const
|
|||||||
return m_awaitingUserInput;
|
return m_awaitingUserInput;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool SessionTab::isQuickConnect() const
|
||||||
|
{
|
||||||
|
return m_profile.id <= 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
Profile SessionTab::profileForSaving() const
|
||||||
|
{
|
||||||
|
return m_profile;
|
||||||
|
}
|
||||||
|
|
||||||
void SessionTab::connectSession()
|
void SessionTab::connectSession()
|
||||||
{
|
{
|
||||||
if (m_state == SessionState::Connecting || m_state == SessionState::Connected) {
|
if (m_state == SessionState::Connecting || m_state == SessionState::Connected) {
|
||||||
|
|||||||
@@ -69,6 +69,14 @@ public:
|
|||||||
void setVncScaleToFit(bool scaleToFit);
|
void setVncScaleToFit(bool scaleToFit);
|
||||||
bool vncScaleToFit() const;
|
bool vncScaleToFit() const;
|
||||||
bool awaitingUserInput() 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:
|
signals:
|
||||||
void tabTitleChanged(const QString& title);
|
void tabTitleChanged(const QString& title);
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
#include "session_window.h"
|
#include "session_window.h"
|
||||||
|
|
||||||
#include "about_dialog.h"
|
#include "about_dialog.h"
|
||||||
|
#include "profile_dialog.h"
|
||||||
#include "profiles_window.h"
|
#include "profiles_window.h"
|
||||||
#include "user_guide_dialog.h"
|
#include "user_guide_dialog.h"
|
||||||
#include <QApplication>
|
#include <QApplication>
|
||||||
@@ -148,6 +149,12 @@ SessionWindow::SessionWindow(QWidget* parent)
|
|||||||
actualSizeAction->setChecked(!tab->vncScaleToFit());
|
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));
|
QAction* chosen = menu.exec(m_tabs->tabBar()->mapToGlobal(pos));
|
||||||
if (chosen == disconnectAction) {
|
if (chosen == disconnectAction) {
|
||||||
tab->disconnectSession();
|
tab->disconnectSession();
|
||||||
@@ -188,6 +195,8 @@ SessionWindow::SessionWindow(QWidget* parent)
|
|||||||
tab->setVncScaleToFit(true);
|
tab->setVncScaleToFit(true);
|
||||||
} else if (actualSizeAction != nullptr && chosen == actualSizeAction) {
|
} else if (actualSizeAction != nullptr && chosen == actualSizeAction) {
|
||||||
tab->setVncScaleToFit(false);
|
tab->setVncScaleToFit(false);
|
||||||
|
} else if (saveAsProfileAction != nullptr && chosen == saveAsProfileAction) {
|
||||||
|
saveTabAsProfile(tab);
|
||||||
} else {
|
} else {
|
||||||
for (QAction* themeAction : themeActions) {
|
for (QAction* themeAction : themeActions) {
|
||||||
if (chosen == themeAction) {
|
if (chosen == themeAction) {
|
||||||
@@ -201,6 +210,7 @@ SessionWindow::SessionWindow(QWidget* parent)
|
|||||||
QMenu* fileMenu = menuBar()->addMenu(QStringLiteral("File"));
|
QMenu* fileMenu = menuBar()->addMenu(QStringLiteral("File"));
|
||||||
QAction* newProfileAction = fileMenu->addAction(QStringLiteral("New Profile"));
|
QAction* newProfileAction = fileMenu->addAction(QStringLiteral("New Profile"));
|
||||||
QAction* newFolderAction = fileMenu->addAction(QStringLiteral("New Folder"));
|
QAction* newFolderAction = fileMenu->addAction(QStringLiteral("New Folder"));
|
||||||
|
QAction* quickConnectAction = fileMenu->addAction(QStringLiteral("Quick Connect..."));
|
||||||
fileMenu->addSeparator();
|
fileMenu->addSeparator();
|
||||||
QAction* importProfilesAction = fileMenu->addAction(QStringLiteral("Import Profiles..."));
|
QAction* importProfilesAction = fileMenu->addAction(QStringLiteral("Import Profiles..."));
|
||||||
QAction* exportProfilesAction = fileMenu->addAction(QStringLiteral("Export Profiles..."));
|
QAction* exportProfilesAction = fileMenu->addAction(QStringLiteral("Export Profiles..."));
|
||||||
@@ -216,6 +226,10 @@ SessionWindow::SessionWindow(QWidget* parent)
|
|||||||
&QAction::triggered,
|
&QAction::triggered,
|
||||||
this,
|
this,
|
||||||
[this]() { m_profilesWidget->createFolderInCurrentContext(); });
|
[this]() { m_profilesWidget->createFolderInCurrentContext(); });
|
||||||
|
connect(quickConnectAction,
|
||||||
|
&QAction::triggered,
|
||||||
|
this,
|
||||||
|
[this]() { m_profilesWidget->quickConnect(); });
|
||||||
connect(importProfilesAction,
|
connect(importProfilesAction,
|
||||||
&QAction::triggered,
|
&QAction::triggered,
|
||||||
this,
|
this,
|
||||||
@@ -368,6 +382,19 @@ int SessionWindow::sessionTabCount() const
|
|||||||
return m_tabs->count() - 1;
|
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()
|
void SessionWindow::loadUiPreferences()
|
||||||
{
|
{
|
||||||
QSettings settings;
|
QSettings settings;
|
||||||
|
|||||||
@@ -32,6 +32,7 @@ private:
|
|||||||
void loadUiPreferences();
|
void loadUiPreferences();
|
||||||
void saveUiPreferences() const;
|
void saveUiPreferences() const;
|
||||||
int sessionTabCount() const;
|
int sessionTabCount() const;
|
||||||
|
void saveTabAsProfile(SessionTab* tab);
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif
|
#endif
|
||||||
Reference in new issue
Block a user