Internal
Public Access
File -> Export Profiles... writes all profiles (plus explicit, including empty, folders) to a JSON file. File -> Import Profiles... reads one back, recreates folders, and inserts profiles as new rows so IDs never collide with the destination database. No credentials are ever persisted on a Profile in the first place (only privateKeyPath, a filesystem path), so nothing sensitive is exposed by an exported file. Verified with a standalone headless round-trip test against isolated app-data databases (SSH + RDP profiles, nested folders, all fields); caught and fixed a bug where import was redundantly creating an explicit folder row per profile that didn't exist in the original export. Fixes #17. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
329 lines
13 KiB
C++
329 lines
13 KiB
C++
#include "session_window.h"
|
|
|
|
#include "about_dialog.h"
|
|
#include "profiles_window.h"
|
|
#include "user_guide_dialog.h"
|
|
#include <QApplication>
|
|
#include "session_tab.h"
|
|
|
|
#include <QAction>
|
|
#include <QColor>
|
|
#include <QInputDialog>
|
|
#include <QMenu>
|
|
#include <QMenuBar>
|
|
#include <QPalette>
|
|
#include <QSettings>
|
|
#include <QStringList>
|
|
#include <QTabBar>
|
|
#include <QTabWidget>
|
|
|
|
namespace {
|
|
QColor tabColorForState(SessionState state, const QPalette& palette)
|
|
{
|
|
switch (state) {
|
|
case SessionState::Disconnected:
|
|
return palette.color(QPalette::WindowText);
|
|
case SessionState::Connecting:
|
|
return QColor(QStringLiteral("#9a6700"));
|
|
case SessionState::Connected:
|
|
return QColor(QStringLiteral("#2e7d32"));
|
|
case SessionState::Failed:
|
|
return QColor(QStringLiteral("#c62828"));
|
|
}
|
|
|
|
return palette.color(QPalette::WindowText);
|
|
}
|
|
|
|
QStringList terminalThemeNames()
|
|
{
|
|
return {QStringLiteral("Dark"), QStringLiteral("Light"), QStringLiteral("Solarized Dark")};
|
|
}
|
|
}
|
|
|
|
SessionWindow::SessionWindow(QWidget* parent)
|
|
: QMainWindow(parent), m_tabs(new QTabWidget(this)), m_profilesWidget(nullptr)
|
|
{
|
|
loadUiPreferences();
|
|
|
|
setWindowTitle(QStringLiteral("OrbitHub"));
|
|
resize(1080, 760);
|
|
setWindowIcon(QApplication::windowIcon());
|
|
|
|
m_tabs->setTabsClosable(true);
|
|
connect(m_tabs,
|
|
&QTabWidget::tabCloseRequested,
|
|
this,
|
|
[this](int index) {
|
|
QWidget* tab = m_tabs->widget(index);
|
|
if (auto* sessionTab = qobject_cast<SessionTab*>(tab)) {
|
|
sessionTab->disconnectSession();
|
|
}
|
|
m_tabs->removeTab(index);
|
|
delete tab;
|
|
if (sessionTabCount() == 0) {
|
|
setWindowTitle(QStringLiteral("OrbitHub"));
|
|
}
|
|
});
|
|
m_tabs->tabBar()->setContextMenuPolicy(Qt::CustomContextMenu);
|
|
connect(m_tabs->tabBar(),
|
|
&QWidget::customContextMenuRequested,
|
|
this,
|
|
[this](const QPoint& pos) {
|
|
const int index = m_tabs->tabBar()->tabAt(pos);
|
|
if (index < 0) {
|
|
return;
|
|
}
|
|
|
|
auto* tab = qobject_cast<SessionTab*>(m_tabs->widget(index));
|
|
if (tab == nullptr) {
|
|
return;
|
|
}
|
|
|
|
QMenu menu(this);
|
|
QAction* disconnectAction = menu.addAction(QStringLiteral("Disconnect"));
|
|
QAction* reconnectAction = menu.addAction(QStringLiteral("Reconnect"));
|
|
QAction* toggleEventsAction = menu.addAction(
|
|
tab->isEventsPanelExpanded() ? QStringLiteral("Hide Events")
|
|
: QStringLiteral("Show Events"));
|
|
QAction* copyEventsAction = menu.addAction(QStringLiteral("Copy Events"));
|
|
QAction* exportEventsAction = menu.addAction(QStringLiteral("Export Events"));
|
|
QAction* clearEventsAction = menu.addAction(QStringLiteral("Clear Events"));
|
|
QList<QAction*> themeActions;
|
|
|
|
if (tab->supportsThemeSelection()) {
|
|
menu.addSeparator();
|
|
QMenu* themeMenu = menu.addMenu(QStringLiteral("Theme"));
|
|
const QString currentTheme = tab->terminalThemeName();
|
|
for (const QString& themeName : terminalThemeNames()) {
|
|
QAction* themeAction = themeMenu->addAction(themeName);
|
|
themeAction->setCheckable(true);
|
|
themeAction->setChecked(
|
|
themeName.compare(currentTheme, Qt::CaseInsensitive) == 0);
|
|
themeActions.append(themeAction);
|
|
}
|
|
}
|
|
|
|
QAction* clearAction = nullptr;
|
|
if (tab->supportsClearAction()) {
|
|
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();
|
|
} else if (chosen == reconnectAction) {
|
|
tab->reconnectSession();
|
|
} else if (chosen == toggleEventsAction) {
|
|
tab->setEventsPanelExpanded(!tab->isEventsPanelExpanded());
|
|
} else if (chosen == copyEventsAction) {
|
|
tab->copyEvents();
|
|
} else if (chosen == exportEventsAction) {
|
|
tab->exportEventsToFile();
|
|
} else if (chosen == clearEventsAction) {
|
|
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) {
|
|
tab->setTerminalThemeName(themeAction->text());
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
});
|
|
|
|
QMenu* fileMenu = menuBar()->addMenu(QStringLiteral("File"));
|
|
QAction* newProfileAction = fileMenu->addAction(QStringLiteral("New Profile"));
|
|
QAction* newFolderAction = fileMenu->addAction(QStringLiteral("New Folder"));
|
|
fileMenu->addSeparator();
|
|
QAction* importProfilesAction = fileMenu->addAction(QStringLiteral("Import Profiles..."));
|
|
QAction* exportProfilesAction = fileMenu->addAction(QStringLiteral("Export Profiles..."));
|
|
fileMenu->addSeparator();
|
|
QAction* quitAction = fileMenu->addAction(QStringLiteral("Quit"));
|
|
|
|
connect(newProfileAction,
|
|
&QAction::triggered,
|
|
this,
|
|
[this]() { m_profilesWidget->createProfileInCurrentContext(); });
|
|
connect(newFolderAction,
|
|
&QAction::triggered,
|
|
this,
|
|
[this]() { m_profilesWidget->createFolderInCurrentContext(); });
|
|
connect(importProfilesAction,
|
|
&QAction::triggered,
|
|
this,
|
|
[this]() { m_profilesWidget->importProfiles(); });
|
|
connect(exportProfilesAction,
|
|
&QAction::triggered,
|
|
this,
|
|
[this]() { m_profilesWidget->exportProfiles(); });
|
|
connect(quitAction, &QAction::triggered, this, []() { qApp->quit(); });
|
|
|
|
QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("Help"));
|
|
QAction* userGuideAction = helpMenu->addAction(QStringLiteral("User Guide"));
|
|
connect(userGuideAction,
|
|
&QAction::triggered,
|
|
this,
|
|
[this]() {
|
|
UserGuideDialog dialog(this);
|
|
dialog.exec();
|
|
});
|
|
QAction* aboutAction = helpMenu->addAction(QStringLiteral("About OrbitHub"));
|
|
connect(aboutAction,
|
|
&QAction::triggered,
|
|
this,
|
|
[this]() {
|
|
AboutDialog dialog(this);
|
|
dialog.exec();
|
|
});
|
|
|
|
setCentralWidget(m_tabs);
|
|
|
|
m_profilesWidget = new ProfilesWindow(this);
|
|
const int profilesIndex = m_tabs->addTab(m_profilesWidget, QStringLiteral("Profiles"));
|
|
m_tabs->tabBar()->setTabButton(profilesIndex, QTabBar::LeftSide, nullptr);
|
|
m_tabs->tabBar()->setTabButton(profilesIndex, QTabBar::RightSide, nullptr);
|
|
connect(m_profilesWidget,
|
|
&ProfilesWindow::connectRequested,
|
|
this,
|
|
&SessionWindow::addSessionTab);
|
|
}
|
|
|
|
void SessionWindow::openProfile(const Profile& profile)
|
|
{
|
|
addSessionTab(profile);
|
|
}
|
|
|
|
void SessionWindow::addSessionTab(const Profile& profile)
|
|
{
|
|
auto* tab = new SessionTab(profile, m_preferences, this);
|
|
const int index = m_tabs->addTab(tab, tab->tabTitle());
|
|
m_tabs->setCurrentIndex(index);
|
|
if (sessionTabCount() > 1) {
|
|
setWindowTitle(QStringLiteral("OrbitHub Sessions"));
|
|
} else {
|
|
setWindowTitle(QStringLiteral("OrbitHub Session - %1").arg(profile.name));
|
|
}
|
|
m_tabs->tabBar()->setTabTextColor(
|
|
index, tabColorForState(SessionState::Disconnected, m_tabs->palette()));
|
|
|
|
connect(tab,
|
|
&SessionTab::tabTitleChanged,
|
|
this,
|
|
[this, tab](const QString& title) { updateTabTitle(tab, title); });
|
|
connect(tab,
|
|
&SessionTab::tabStateChanged,
|
|
this,
|
|
[this, tab](SessionState state) {
|
|
for (int i = 0; i < m_tabs->count(); ++i) {
|
|
if (m_tabs->widget(i) == tab) {
|
|
m_tabs->tabBar()->setTabTextColor(
|
|
i, tabColorForState(state, m_tabs->palette()));
|
|
return;
|
|
}
|
|
}
|
|
});
|
|
connect(tab,
|
|
&SessionTab::terminalThemeChanged,
|
|
this,
|
|
[this](const QString& themeName) {
|
|
m_preferences.terminalThemeName = themeName.trimmed().isEmpty()
|
|
? QStringLiteral("Dark")
|
|
: 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,
|
|
[this](bool expanded) {
|
|
m_preferences.eventsPanelExpanded = expanded;
|
|
saveUiPreferences();
|
|
});
|
|
}
|
|
|
|
void SessionWindow::updateTabTitle(SessionTab* tab, const QString& title)
|
|
{
|
|
for (int i = 0; i < m_tabs->count(); ++i) {
|
|
if (m_tabs->widget(i) == tab) {
|
|
m_tabs->setTabText(i, title);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
int SessionWindow::sessionTabCount() const
|
|
{
|
|
return m_tabs->count() - 1;
|
|
}
|
|
|
|
void SessionWindow::loadUiPreferences()
|
|
{
|
|
QSettings settings;
|
|
m_preferences.terminalThemeName =
|
|
settings.value(QStringLiteral("session/defaultTerminalTheme"), QStringLiteral("Dark"))
|
|
.toString()
|
|
.trimmed();
|
|
if (m_preferences.terminalThemeName.isEmpty()) {
|
|
m_preferences.terminalThemeName = QStringLiteral("Dark");
|
|
}
|
|
m_preferences.eventsPanelExpanded =
|
|
settings.value(QStringLiteral("session/eventsPanelExpanded"), false).toBool();
|
|
m_preferences.terminalFontPointSize =
|
|
settings.value(QStringLiteral("session/terminalFontPointSize"), 0).toInt();
|
|
}
|
|
|
|
void SessionWindow::saveUiPreferences() const
|
|
{
|
|
QSettings settings;
|
|
settings.setValue(QStringLiteral("session/defaultTerminalTheme"),
|
|
m_preferences.terminalThemeName);
|
|
settings.setValue(QStringLiteral("session/eventsPanelExpanded"),
|
|
m_preferences.eventsPanelExpanded);
|
|
settings.setValue(QStringLiteral("session/terminalFontPointSize"),
|
|
m_preferences.terminalFontPointSize);
|
|
}
|