Files
orbithub/src/profiles_window.cpp

975 lines
33 KiB
C++

#include "profiles_window.h"
#include "about_dialog.h"
#include "profile_dialog.h"
#include "profile_repository.h"
#include "profiles_tree_widget.h"
#include "session_window.h"
#include <QAction>
#include <QAbstractItemView>
#include <QComboBox>
#include <QHeaderView>
#include <QHBoxLayout>
#include <QLabel>
#include <QLineEdit>
#include <QInputDialog>
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
#include <QPushButton>
#include <QSet>
#include <QSettings>
#include <QSignalBlocker>
#include <QApplication>
#include <QStringList>
#include <QStyle>
#include <QTreeWidget>
#include <QTreeWidgetItem>
#include <QVariant>
#include <QVBoxLayout>
#include <QWidget>
namespace {
constexpr int kProfileIdRole = Qt::UserRole;
constexpr int kFolderPathRole = Qt::UserRole + 1;
QString normalizeFolderPathForView(const QString& value)
{
QString path = value.trimmed();
path.replace(QChar::fromLatin1('\\'), QChar::fromLatin1('/'));
const QStringList rawParts = path.split(QChar::fromLatin1('/'), Qt::SkipEmptyParts);
QStringList normalized;
for (const QString& rawPart : rawParts) {
const QString part = rawPart.trimmed();
if (!part.isEmpty()) {
normalized.push_back(part);
}
}
return normalized.join(QStringLiteral("/"));
}
QString joinFolderPath(const QString& parentFolder, const QString& childName)
{
const QString parent = normalizeFolderPathForView(parentFolder);
const QString child = normalizeFolderPathForView(childName);
if (child.isEmpty()) {
return parent;
}
if (parent.isEmpty()) {
return child;
}
return QStringLiteral("%1/%2").arg(parent, child);
}
QStringList splitFolderPath(const QString& folderPath)
{
const QString normalized = normalizeFolderPathForView(folderPath);
if (normalized.isEmpty()) {
return {};
}
return normalized.split(QChar::fromLatin1('/'), Qt::SkipEmptyParts);
}
bool profileHasTag(const Profile& profile, const QString& requestedTag)
{
const QString needle = requestedTag.trimmed();
if (needle.isEmpty()) {
return true;
}
const QStringList tags = profile.tags.split(QChar::fromLatin1(','), Qt::SkipEmptyParts);
for (const QString& tag : tags) {
if (tag.trimmed().compare(needle, Qt::CaseInsensitive) == 0) {
return true;
}
}
return false;
}
}
ProfilesWindow::ProfilesWindow(QWidget* parent)
: QMainWindow(parent),
m_searchBox(nullptr),
m_viewModeBox(nullptr),
m_sortBox(nullptr),
m_protocolFilterBox(nullptr),
m_tagFilterBox(nullptr),
m_profilesTree(nullptr),
m_newButton(nullptr),
m_editButton(nullptr),
m_deleteButton(nullptr),
m_repository(std::make_unique<ProfileRepository>())
{
setWindowTitle(QStringLiteral("OrbitHub Profiles"));
resize(860, 640);
setWindowIcon(QApplication::windowIcon());
setupUi();
if (!m_repository->initError().isEmpty()) {
QMessageBox::critical(this,
QStringLiteral("Database Error"),
QStringLiteral("Failed to initialize SQLite database: %1")
.arg(m_repository->initError()));
m_newButton->setEnabled(false);
m_editButton->setEnabled(false);
m_deleteButton->setEnabled(false);
m_searchBox->setEnabled(false);
m_profilesTree->setEnabled(false);
return;
}
loadUiPreferences();
loadProfiles();
}
ProfilesWindow::~ProfilesWindow() = default;
void ProfilesWindow::setupUi()
{
auto* central = new QWidget(this);
auto* rootLayout = new QVBoxLayout(central);
auto* searchLabel = new QLabel(QStringLiteral("Search"), central);
m_searchBox = new QLineEdit(central);
m_searchBox->setPlaceholderText(QStringLiteral("Filter by name, host, folder, or tags..."));
auto* viewModeLabel = new QLabel(QStringLiteral("View"), central);
m_viewModeBox = new QComboBox(central);
m_viewModeBox->addItem(QStringLiteral("List"));
m_viewModeBox->addItem(QStringLiteral("Folders"));
auto* sortLabel = new QLabel(QStringLiteral("Sort"), central);
m_sortBox = new QComboBox(central);
m_sortBox->addItem(QStringLiteral("Name"), static_cast<int>(ProfileSortOrder::NameAsc));
m_sortBox->addItem(QStringLiteral("Protocol"), static_cast<int>(ProfileSortOrder::ProtocolAsc));
m_sortBox->addItem(QStringLiteral("Host"), static_cast<int>(ProfileSortOrder::HostAsc));
auto* protocolFilterLabel = new QLabel(QStringLiteral("Protocol"), central);
m_protocolFilterBox = new QComboBox(central);
m_protocolFilterBox->addItem(QStringLiteral("All"));
m_protocolFilterBox->addItem(QStringLiteral("SSH"));
m_protocolFilterBox->addItem(QStringLiteral("RDP"));
m_protocolFilterBox->addItem(QStringLiteral("VNC"));
auto* tagFilterLabel = new QLabel(QStringLiteral("Tag"), central);
m_tagFilterBox = new QComboBox(central);
m_tagFilterBox->addItem(QStringLiteral("All"));
m_profilesTree = new ProfilesTreeWidget(central);
m_profilesTree->setSelectionMode(QAbstractItemView::SingleSelection);
m_profilesTree->setColumnCount(4);
m_profilesTree->setHeaderLabels(
{QStringLiteral("Name"), QStringLiteral("Protocol"), QStringLiteral("Server"), QStringLiteral("Tags")});
m_profilesTree->setRootIsDecorated(true);
m_profilesTree->setDragEnabled(true);
m_profilesTree->setAcceptDrops(true);
m_profilesTree->viewport()->setAcceptDrops(true);
m_profilesTree->setDropIndicatorShown(true);
m_profilesTree->setDragDropMode(QAbstractItemView::InternalMove);
m_profilesTree->setDefaultDropAction(Qt::MoveAction);
m_profilesTree->setContextMenuPolicy(Qt::CustomContextMenu);
m_profilesTree->header()->setSectionResizeMode(0, QHeaderView::Stretch);
m_profilesTree->header()->setSectionResizeMode(1, QHeaderView::ResizeToContents);
m_profilesTree->header()->setSectionResizeMode(2, QHeaderView::ResizeToContents);
m_profilesTree->header()->setSectionResizeMode(3, QHeaderView::Stretch);
auto* buttonRow = new QHBoxLayout();
m_newButton = new QPushButton(QStringLiteral("New"), central);
m_editButton = new QPushButton(QStringLiteral("Edit"), central);
m_deleteButton = new QPushButton(QStringLiteral("Delete"), central);
buttonRow->addWidget(m_newButton);
buttonRow->addWidget(m_editButton);
buttonRow->addWidget(m_deleteButton);
buttonRow->addStretch();
auto* filterRow = new QHBoxLayout();
filterRow->addWidget(searchLabel);
filterRow->addWidget(m_searchBox, 1);
filterRow->addWidget(viewModeLabel);
filterRow->addWidget(m_viewModeBox);
filterRow->addWidget(protocolFilterLabel);
filterRow->addWidget(m_protocolFilterBox);
filterRow->addWidget(tagFilterLabel);
filterRow->addWidget(m_tagFilterBox);
filterRow->addWidget(sortLabel);
filterRow->addWidget(m_sortBox);
rootLayout->addLayout(filterRow);
rootLayout->addWidget(m_profilesTree, 1);
rootLayout->addLayout(buttonRow);
setCentralWidget(central);
QMenu* fileMenu = menuBar()->addMenu(QStringLiteral("File"));
QAction* newProfileAction = fileMenu->addAction(QStringLiteral("New Profile"));
QAction* newFolderAction = fileMenu->addAction(QStringLiteral("New Folder"));
fileMenu->addSeparator();
QAction* quitAction = fileMenu->addAction(QStringLiteral("Quit"));
connect(newProfileAction,
&QAction::triggered,
this,
[this]() {
const QString folderPath = folderPathForItem(m_profilesTree->currentItem());
createProfile(folderPath);
});
connect(newFolderAction,
&QAction::triggered,
this,
[this]() {
const QString folderPath = folderPathForItem(m_profilesTree->currentItem());
createFolderInContext(folderPath);
});
connect(quitAction, &QAction::triggered, this, [this]() { close(); });
QMenu* helpMenu = menuBar()->addMenu(QStringLiteral("Help"));
QAction* aboutAction = helpMenu->addAction(QStringLiteral("About OrbitHub"));
connect(aboutAction,
&QAction::triggered,
this,
[this]() {
AboutDialog dialog(this);
dialog.exec();
});
connect(m_searchBox,
&QLineEdit::textChanged,
this,
[this](const QString&) {
saveUiPreferences();
loadProfiles();
});
connect(m_viewModeBox,
&QComboBox::currentIndexChanged,
this,
[this](int) {
saveUiPreferences();
loadProfiles();
});
connect(m_sortBox,
&QComboBox::currentIndexChanged,
this,
[this](int) {
saveUiPreferences();
loadProfiles();
});
connect(m_protocolFilterBox,
&QComboBox::currentIndexChanged,
this,
[this](int) {
saveUiPreferences();
loadProfiles();
});
connect(m_tagFilterBox,
&QComboBox::currentIndexChanged,
this,
[this](int) {
saveUiPreferences();
loadProfiles();
});
connect(m_profilesTree,
&QTreeWidget::itemDoubleClicked,
this,
[this](QTreeWidgetItem* item, int) { openSessionForItem(item); });
connect(m_profilesTree,
&QWidget::customContextMenuRequested,
this,
[this](const QPoint& pos) { showTreeContextMenu(pos); });
connect(m_profilesTree,
&ProfilesTreeWidget::itemsDropped,
this,
[this]() { persistFolderAssignmentsFromTree(); });
connect(m_newButton, &QPushButton::clicked, this, [this]() { createProfile(); });
connect(m_editButton, &QPushButton::clicked, this, [this]() { editSelectedProfile(); });
connect(m_deleteButton, &QPushButton::clicked, this, [this]() { deleteSelectedProfile(); });
}
void ProfilesWindow::loadProfiles()
{
m_profilesTree->clear();
m_profileCache.clear();
const QString query = m_searchBox == nullptr ? QString() : m_searchBox->text();
const std::vector<Profile> profiles = m_repository->listProfiles(query, selectedSortOrder());
if (!m_repository->lastError().isEmpty()) {
QMessageBox::warning(this,
QStringLiteral("Load Profiles"),
QStringLiteral("Failed to load profiles: %1")
.arg(m_repository->lastError()));
return;
}
updateTagFilterOptions(profiles);
const QString protocolFilter = selectedProtocolFilter();
const QString tagFilter = selectedTagFilter();
const bool folderView = isFolderViewEnabled();
m_profilesTree->setDragEnabled(folderView);
m_profilesTree->setAcceptDrops(folderView);
m_profilesTree->viewport()->setAcceptDrops(folderView);
m_profilesTree->setDropIndicatorShown(folderView);
m_profilesTree->setDragDropMode(folderView ? QAbstractItemView::InternalMove
: QAbstractItemView::NoDragDrop);
std::vector<Profile> filteredProfiles;
filteredProfiles.reserve(profiles.size());
for (const Profile& profile : profiles) {
if (!protocolFilter.isEmpty()
&& profile.protocol.compare(protocolFilter, Qt::CaseInsensitive) != 0) {
continue;
}
if (!tagFilter.isEmpty() && !profileHasTag(profile, tagFilter)) {
continue;
}
filteredProfiles.push_back(profile);
}
if (folderView) {
std::map<QString, QTreeWidgetItem*> folderNodes;
const std::vector<QString> explicitFolders = m_repository->listFolders();
for (const QString& folderPath : explicitFolders) {
if (query.trimmed().isEmpty()
|| folderPath.contains(query.trimmed(), Qt::CaseInsensitive)) {
upsertFolderNode(splitFolderPath(folderPath), folderNodes);
}
}
for (const Profile& profile : filteredProfiles) {
QTreeWidgetItem* parent = nullptr;
const QStringList folderParts = splitFolderPath(profile.folderPath);
if (!folderParts.isEmpty()) {
parent = upsertFolderNode(folderParts, folderNodes);
}
addProfileNode(parent, profile);
}
m_profilesTree->expandAll();
} else {
for (const Profile& profile : filteredProfiles) {
addProfileNode(nullptr, profile);
}
}
}
ProfileSortOrder ProfilesWindow::selectedSortOrder() const
{
if (m_sortBox == nullptr) {
return ProfileSortOrder::NameAsc;
}
const QVariant value = m_sortBox->currentData();
if (!value.isValid()) {
return ProfileSortOrder::NameAsc;
}
const int orderValue = value.toInt();
switch (static_cast<ProfileSortOrder>(orderValue)) {
case ProfileSortOrder::ProtocolAsc:
return ProfileSortOrder::ProtocolAsc;
case ProfileSortOrder::HostAsc:
return ProfileSortOrder::HostAsc;
case ProfileSortOrder::NameAsc:
default:
return ProfileSortOrder::NameAsc;
}
}
bool ProfilesWindow::isFolderViewEnabled() const
{
if (m_viewModeBox == nullptr) {
return false;
}
return m_viewModeBox->currentText().compare(QStringLiteral("Folders"), Qt::CaseInsensitive)
== 0;
}
QString ProfilesWindow::selectedProtocolFilter() const
{
if (m_protocolFilterBox == nullptr) {
return QString();
}
const QString selected = m_protocolFilterBox->currentText().trimmed();
if (selected.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0) {
return QString();
}
return selected;
}
QString ProfilesWindow::selectedTagFilter() const
{
if (m_tagFilterBox == nullptr) {
return QString();
}
const QString selected = m_tagFilterBox->currentText().trimmed();
if (selected.compare(QStringLiteral("All"), Qt::CaseInsensitive) == 0) {
return QString();
}
return selected;
}
void ProfilesWindow::updateTagFilterOptions(const std::vector<Profile>& profiles)
{
if (m_tagFilterBox == nullptr) {
return;
}
const QString previousSelection = m_tagFilterBox->currentText().trimmed();
QString desiredSelection = previousSelection;
if (!m_pendingTagFilterPreference.trimmed().isEmpty()) {
desiredSelection = m_pendingTagFilterPreference.trimmed();
}
QSet<QString> seen;
QStringList tags;
for (const Profile& profile : profiles) {
const QStringList splitTags =
profile.tags.split(QChar::fromLatin1(','), Qt::SkipEmptyParts);
for (const QString& rawTag : splitTags) {
const QString tag = rawTag.trimmed();
if (tag.isEmpty()) {
continue;
}
const QString dedupeKey = tag.toLower();
if (seen.contains(dedupeKey)) {
continue;
}
seen.insert(dedupeKey);
tags.push_back(tag);
}
}
tags.sort(Qt::CaseInsensitive);
const QSignalBlocker blocker(m_tagFilterBox);
m_tagFilterBox->clear();
m_tagFilterBox->addItem(QStringLiteral("All"));
for (const QString& tag : tags) {
m_tagFilterBox->addItem(tag);
}
int restoredIndex = m_tagFilterBox->findText(desiredSelection, Qt::MatchFixedString);
if (restoredIndex < 0) {
restoredIndex = 0;
}
m_tagFilterBox->setCurrentIndex(restoredIndex);
m_pendingTagFilterPreference.clear();
}
QTreeWidgetItem* ProfilesWindow::upsertFolderNode(const QStringList& folderParts,
std::map<QString, QTreeWidgetItem*>& folderNodes)
{
QString currentPath;
QTreeWidgetItem* parent = nullptr;
for (const QString& rawPart : folderParts) {
const QString part = rawPart.trimmed();
if (part.isEmpty()) {
continue;
}
currentPath = currentPath.isEmpty() ? part
: QStringLiteral("%1/%2").arg(currentPath, part);
const auto existing = folderNodes.find(currentPath);
if (existing != folderNodes.end()) {
parent = existing->second;
continue;
}
auto* folderItem = parent == nullptr ? new QTreeWidgetItem(m_profilesTree)
: new QTreeWidgetItem(parent);
folderItem->setText(0, part);
folderItem->setIcon(0, style()->standardIcon(QStyle::SP_DirIcon));
folderItem->setData(0, kFolderPathRole, currentPath);
folderItem->setFlags((folderItem->flags() | Qt::ItemIsDropEnabled) & ~Qt::ItemIsDragEnabled);
folderNodes.insert_or_assign(currentPath, folderItem);
parent = folderItem;
}
return parent;
}
void ProfilesWindow::addProfileNode(QTreeWidgetItem* parent, const Profile& profile)
{
auto* item = parent == nullptr ? new QTreeWidgetItem(m_profilesTree) : new QTreeWidgetItem(parent);
item->setText(0, profile.name);
item->setText(1, profile.protocol);
item->setText(2, QStringLiteral("%1:%2").arg(profile.host, QString::number(profile.port)));
item->setText(3, profile.tags);
item->setIcon(0, style()->standardIcon(QStyle::SP_ComputerIcon));
item->setData(0, kProfileIdRole, QVariant::fromValue(profile.id));
item->setData(0, kFolderPathRole, normalizeFolderPathForView(profile.folderPath));
item->setFlags((item->flags() | Qt::ItemIsDragEnabled | Qt::ItemIsSelectable) & ~Qt::ItemIsDropEnabled);
const QString identity = [&profile]() {
if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0
&& !profile.domain.trimmed().isEmpty()) {
return QStringLiteral("%1\\%2").arg(profile.domain.trimmed(),
profile.username.trimmed().isEmpty()
? QStringLiteral("<none>")
: profile.username.trimmed());
}
return profile.username.trimmed().isEmpty() ? QStringLiteral("<none>")
: profile.username.trimmed();
}();
QString tooltip = QStringLiteral("%1://%2@%3:%4\nAuth: %5")
.arg(profile.protocol,
identity,
profile.host,
QString::number(profile.port),
profile.authMode);
if (profile.protocol.compare(QStringLiteral("SSH"), Qt::CaseInsensitive) == 0) {
tooltip += QStringLiteral("\nKnown Hosts: %1").arg(profile.knownHostsPolicy);
} else if (profile.protocol.compare(QStringLiteral("RDP"), Qt::CaseInsensitive) == 0) {
tooltip += QStringLiteral("\nRDP Security: %1\nRDP Performance: %2")
.arg(profile.rdpSecurityMode, profile.rdpPerformanceProfile);
}
if (!profile.folderPath.trimmed().isEmpty()) {
tooltip += QStringLiteral("\nFolder: %1").arg(profile.folderPath.trimmed());
}
if (!profile.tags.trimmed().isEmpty()) {
tooltip += QStringLiteral("\nTags: %1").arg(profile.tags);
}
item->setToolTip(0, tooltip);
item->setToolTip(1, tooltip);
item->setToolTip(2, tooltip);
item->setToolTip(3, tooltip);
m_profileCache.insert_or_assign(profile.id, profile);
}
QString ProfilesWindow::folderPathForItem(const QTreeWidgetItem* item) const
{
if (item == nullptr) {
return QString();
}
const QVariant idValue = item->data(0, kProfileIdRole);
if (idValue.isValid()) {
const qint64 id = idValue.toLongLong();
const auto cacheIt = m_profileCache.find(id);
if (cacheIt != m_profileCache.end()) {
return normalizeFolderPathForView(cacheIt->second.folderPath);
}
}
return normalizeFolderPathForView(item->data(0, kFolderPathRole).toString());
}
void ProfilesWindow::showTreeContextMenu(const QPoint& pos)
{
if (m_profilesTree == nullptr) {
return;
}
QTreeWidgetItem* item = m_profilesTree->itemAt(pos);
if (item != nullptr) {
m_profilesTree->setCurrentItem(item);
}
const QString contextFolder = folderPathForItem(item);
const bool isProfileItem = item != nullptr && item->data(0, kProfileIdRole).isValid();
QMenu menu(this);
QAction* newConnectionAction = menu.addAction(QStringLiteral("New Connection"));
QAction* newFolderAction = menu.addAction(QStringLiteral("New Folder"));
QAction* connectAction = nullptr;
QAction* editAction = nullptr;
QAction* deleteAction = nullptr;
if (isProfileItem) {
menu.addSeparator();
connectAction = menu.addAction(QStringLiteral("Connect"));
editAction = menu.addAction(QStringLiteral("Edit"));
deleteAction = menu.addAction(QStringLiteral("Delete"));
}
QAction* chosen = menu.exec(m_profilesTree->viewport()->mapToGlobal(pos));
if (chosen == nullptr) {
return;
}
if (chosen == newConnectionAction) {
createProfile(contextFolder);
return;
}
if (chosen == newFolderAction) {
createFolderInContext(contextFolder);
return;
}
if (isProfileItem && chosen == connectAction) {
openSessionForItem(item);
return;
}
if (isProfileItem && chosen == editAction) {
editSelectedProfile();
return;
}
if (isProfileItem && chosen == deleteAction) {
deleteSelectedProfile();
return;
}
}
void ProfilesWindow::createFolderInContext(const QString& baseFolderPath)
{
bool accepted = false;
const QString folderName = QInputDialog::getText(
this,
QStringLiteral("New Folder"),
QStringLiteral("Folder name"),
QLineEdit::Normal,
QString(),
&accepted);
if (!accepted) {
return;
}
const QString normalized = joinFolderPath(baseFolderPath, folderName);
if (normalized.isEmpty()) {
QMessageBox::information(this,
QStringLiteral("New Folder"),
QStringLiteral("Folder name is required."));
return;
}
if (!m_repository->createFolder(normalized)) {
QMessageBox::warning(this,
QStringLiteral("New Folder"),
QStringLiteral("Failed to create folder: %1")
.arg(m_repository->lastError().isEmpty()
? QStringLiteral("unknown error")
: m_repository->lastError()));
return;
}
loadProfiles();
}
void ProfilesWindow::persistFolderAssignmentsFromTree()
{
if (!isFolderViewEnabled() || m_profilesTree == nullptr) {
return;
}
std::unordered_map<qint64, QString> assignments;
const int topLevelCount = m_profilesTree->topLevelItemCount();
for (int index = 0; index < topLevelCount; ++index) {
collectProfileAssignments(m_profilesTree->topLevelItem(index), QString(), assignments);
}
bool hadErrors = false;
int updatedCount = 0;
for (const auto& [id, newFolderPath] : assignments) {
auto cacheIt = m_profileCache.find(id);
Profile profile;
if (cacheIt != m_profileCache.end()) {
profile = cacheIt->second;
} else {
const std::optional<Profile> fetched = m_repository->getProfile(id);
if (!fetched.has_value()) {
continue;
}
profile = fetched.value();
}
const QString existing = normalizeFolderPathForView(profile.folderPath);
const QString target = normalizeFolderPathForView(newFolderPath);
if (existing == target) {
continue;
}
profile.folderPath = target;
if (!target.isEmpty()) {
m_repository->createFolder(target);
}
if (!m_repository->updateProfile(profile)) {
hadErrors = true;
continue;
}
m_profileCache.insert_or_assign(profile.id, profile);
++updatedCount;
}
if (hadErrors) {
QMessageBox::warning(this,
QStringLiteral("Move Profile"),
QStringLiteral("One or more profile moves could not be saved."));
}
if (updatedCount > 0 || hadErrors) {
loadProfiles();
}
}
void ProfilesWindow::collectProfileAssignments(
const QTreeWidgetItem* item,
const QString& parentFolderPath,
std::unordered_map<qint64, QString>& assignments) const
{
if (item == nullptr) {
return;
}
const QVariant idValue = item->data(0, kProfileIdRole);
if (idValue.isValid()) {
assignments.insert_or_assign(idValue.toLongLong(), normalizeFolderPathForView(parentFolderPath));
return;
}
const QString folderPath = joinFolderPath(parentFolderPath, item->text(0));
const int childCount = item->childCount();
for (int index = 0; index < childCount; ++index) {
collectProfileAssignments(item->child(index), folderPath, assignments);
}
}
void ProfilesWindow::loadUiPreferences()
{
QSettings settings;
const QString search = settings.value(QStringLiteral("profiles/searchText")).toString();
const QString viewMode =
settings.value(QStringLiteral("profiles/viewMode"), QStringLiteral("List")).toString();
const int sortValue = settings
.value(QStringLiteral("profiles/sortOrder"),
static_cast<int>(ProfileSortOrder::NameAsc))
.toInt();
const QString protocolFilter =
settings.value(QStringLiteral("profiles/protocolFilter"), QStringLiteral("All")).toString();
const QString tagFilter =
settings.value(QStringLiteral("profiles/tagFilter"), QStringLiteral("All")).toString();
m_pendingTagFilterPreference = tagFilter.trimmed();
if (m_searchBox != nullptr) {
const QSignalBlocker blocker(m_searchBox);
m_searchBox->setText(search);
}
if (m_viewModeBox != nullptr) {
int found = m_viewModeBox->findText(viewMode, Qt::MatchFixedString);
if (found < 0) {
found = 0;
}
const QSignalBlocker blocker(m_viewModeBox);
m_viewModeBox->setCurrentIndex(found);
}
if (m_sortBox != nullptr) {
int found = m_sortBox->findData(sortValue);
if (found < 0) {
found = 0;
}
const QSignalBlocker blocker(m_sortBox);
m_sortBox->setCurrentIndex(found);
}
if (m_protocolFilterBox != nullptr) {
int found = m_protocolFilterBox->findText(protocolFilter, Qt::MatchFixedString);
if (found < 0) {
found = 0;
}
const QSignalBlocker blocker(m_protocolFilterBox);
m_protocolFilterBox->setCurrentIndex(found);
}
if (m_tagFilterBox != nullptr) {
int found = m_tagFilterBox->findText(tagFilter, Qt::MatchFixedString);
if (found < 0) {
found = 0;
}
const QSignalBlocker blocker(m_tagFilterBox);
m_tagFilterBox->setCurrentIndex(found);
}
}
void ProfilesWindow::saveUiPreferences() const
{
QSettings settings;
if (m_searchBox != nullptr) {
settings.setValue(QStringLiteral("profiles/searchText"), m_searchBox->text());
}
if (m_viewModeBox != nullptr) {
settings.setValue(QStringLiteral("profiles/viewMode"), m_viewModeBox->currentText());
}
if (m_sortBox != nullptr) {
settings.setValue(QStringLiteral("profiles/sortOrder"), m_sortBox->currentData());
}
if (m_protocolFilterBox != nullptr) {
settings.setValue(QStringLiteral("profiles/protocolFilter"), m_protocolFilterBox->currentText());
}
if (m_tagFilterBox != nullptr) {
settings.setValue(QStringLiteral("profiles/tagFilter"), m_tagFilterBox->currentText());
}
}
std::optional<Profile> ProfilesWindow::selectedProfile() const
{
QTreeWidgetItem* item = m_profilesTree->currentItem();
if (item == nullptr) {
return std::nullopt;
}
const QVariant value = item->data(0, kProfileIdRole);
if (!value.isValid()) {
return std::nullopt;
}
const qint64 id = value.toLongLong();
const auto cacheIt = m_profileCache.find(id);
if (cacheIt != m_profileCache.end()) {
return cacheIt->second;
}
return m_repository->getProfile(id);
}
void ProfilesWindow::createProfile(const QString& defaultFolderPath)
{
ProfileDialog dialog(this);
dialog.setDialogTitle(QStringLiteral("New Profile"));
dialog.setDefaultFolderPath(defaultFolderPath);
if (dialog.exec() != QDialog::Accepted) {
return;
}
const Profile newProfile = dialog.profile();
if (!newProfile.folderPath.trimmed().isEmpty()) {
m_repository->createFolder(newProfile.folderPath);
}
if (!m_repository->createProfile(newProfile).has_value()) {
QMessageBox::warning(this,
QStringLiteral("Create Profile"),
QStringLiteral("Failed to create profile: %1")
.arg(m_repository->lastError().isEmpty()
? QStringLiteral("unknown error")
: m_repository->lastError()));
return;
}
loadProfiles();
}
void ProfilesWindow::editSelectedProfile()
{
const std::optional<Profile> selected = selectedProfile();
if (!selected.has_value()) {
QMessageBox::information(this,
QStringLiteral("Edit Profile"),
QStringLiteral("Select a profile first."));
return;
}
ProfileDialog dialog(this);
dialog.setDialogTitle(QStringLiteral("Edit Profile"));
dialog.setProfile(selected.value());
if (dialog.exec() != QDialog::Accepted) {
return;
}
Profile updated = dialog.profile();
updated.id = selected->id;
if (!updated.folderPath.trimmed().isEmpty()) {
m_repository->createFolder(updated.folderPath);
}
if (!m_repository->updateProfile(updated)) {
QMessageBox::warning(this,
QStringLiteral("Edit Profile"),
QStringLiteral("Failed to update profile: %1")
.arg(m_repository->lastError().isEmpty()
? QStringLiteral("unknown error")
: m_repository->lastError()));
return;
}
loadProfiles();
}
void ProfilesWindow::deleteSelectedProfile()
{
const std::optional<Profile> selected = selectedProfile();
if (!selected.has_value()) {
QMessageBox::information(this,
QStringLiteral("Delete Profile"),
QStringLiteral("Select a profile first."));
return;
}
const QMessageBox::StandardButton confirm = QMessageBox::question(
this,
QStringLiteral("Delete Profile"),
QStringLiteral("Delete profile '%1'?").arg(selected->name),
QMessageBox::Yes | QMessageBox::No,
QMessageBox::No);
if (confirm != QMessageBox::Yes) {
return;
}
if (!m_repository->deleteProfile(selected->id)) {
QMessageBox::warning(this,
QStringLiteral("Delete Profile"),
QStringLiteral("Failed to delete profile: %1")
.arg(m_repository->lastError().isEmpty()
? QStringLiteral("unknown error")
: m_repository->lastError()));
return;
}
loadProfiles();
}
void ProfilesWindow::openSessionForItem(QTreeWidgetItem* item)
{
if (item == nullptr) {
return;
}
const QVariant value = item->data(0, kProfileIdRole);
if (!value.isValid()) {
item->setExpanded(!item->isExpanded());
return;
}
const qint64 id = value.toLongLong();
const std::optional<Profile> profile = m_repository->getProfile(id);
if (!profile.has_value()) {
QMessageBox::warning(this,
QStringLiteral("Connect"),
QStringLiteral("Failed to load profile for session: %1")
.arg(m_repository->lastError().isEmpty()
? QStringLiteral("profile not found")
: m_repository->lastError()));
return;
}
if (m_sessionWindow.isNull()) {
m_sessionWindow = new SessionWindow(profile.value());
m_sessionWindow->setAttribute(Qt::WA_DeleteOnClose);
connect(m_sessionWindow, &QObject::destroyed, this, [this]() { m_sessionWindow = nullptr; });
} else {
m_sessionWindow->openProfile(profile.value());
}
m_sessionWindow->setWindowState(m_sessionWindow->windowState() & ~Qt::WindowMinimized);
m_sessionWindow->show();
m_sessionWindow->raise();
m_sessionWindow->activateWindow();
}