975 lines
33 KiB
C++
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();
|
|
}
|