Milestone 8 UX: folder tree workflows, about dialog, and app icon polish
This commit is contained in:
@@ -1,40 +1,112 @@
|
||||
#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 <QListWidget>
|
||||
#include <QListWidgetItem>
|
||||
#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 {
|
||||
QString formatProfileListItem(const Profile& profile)
|
||||
constexpr int kProfileIdRole = Qt::UserRole;
|
||||
constexpr int kFolderPathRole = Qt::UserRole + 1;
|
||||
|
||||
QString normalizeFolderPathForView(const QString& value)
|
||||
{
|
||||
return QStringLiteral("%1 [%2 %3:%4]")
|
||||
.arg(profile.name, profile.protocol, profile.host, QString::number(profile.port));
|
||||
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_profilesList(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(640, 620);
|
||||
resize(860, 640);
|
||||
setWindowIcon(QApplication::windowIcon());
|
||||
|
||||
setupUi();
|
||||
|
||||
@@ -47,10 +119,11 @@ ProfilesWindow::ProfilesWindow(QWidget* parent)
|
||||
m_editButton->setEnabled(false);
|
||||
m_deleteButton->setEnabled(false);
|
||||
m_searchBox->setEnabled(false);
|
||||
m_profilesList->setEnabled(false);
|
||||
m_profilesTree->setEnabled(false);
|
||||
return;
|
||||
}
|
||||
|
||||
loadUiPreferences();
|
||||
loadProfiles();
|
||||
}
|
||||
|
||||
@@ -63,10 +136,47 @@ void ProfilesWindow::setupUi()
|
||||
|
||||
auto* searchLabel = new QLabel(QStringLiteral("Search"), central);
|
||||
m_searchBox = new QLineEdit(central);
|
||||
m_searchBox->setPlaceholderText(QStringLiteral("Filter by name or host..."));
|
||||
m_searchBox->setPlaceholderText(QStringLiteral("Filter by name, host, folder, or tags..."));
|
||||
|
||||
m_profilesList = new QListWidget(central);
|
||||
m_profilesList->setSelectionMode(QAbstractItemView::SingleSelection);
|
||||
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);
|
||||
@@ -78,34 +188,117 @@ void ProfilesWindow::setupUi()
|
||||
buttonRow->addWidget(m_deleteButton);
|
||||
buttonRow->addStretch();
|
||||
|
||||
rootLayout->addWidget(searchLabel);
|
||||
rootLayout->addWidget(m_searchBox);
|
||||
rootLayout->addWidget(m_profilesList, 1);
|
||||
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& text) { loadProfiles(text); });
|
||||
|
||||
connect(m_profilesList,
|
||||
&QListWidget::itemDoubleClicked,
|
||||
[this](const QString&) {
|
||||
saveUiPreferences();
|
||||
loadProfiles();
|
||||
});
|
||||
connect(m_viewModeBox,
|
||||
&QComboBox::currentIndexChanged,
|
||||
this,
|
||||
[this](QListWidgetItem* item) { openSessionForItem(item); });
|
||||
[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(const QString& query)
|
||||
void ProfilesWindow::loadProfiles()
|
||||
{
|
||||
m_profilesList->clear();
|
||||
m_profilesTree->clear();
|
||||
m_profileCache.clear();
|
||||
|
||||
const std::vector<Profile> profiles = m_repository->listProfiles(query);
|
||||
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"),
|
||||
@@ -114,44 +307,522 @@ void ProfilesWindow::loadProfiles(const QString& query)
|
||||
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) {
|
||||
auto* item = new QListWidgetItem(formatProfileListItem(profile), m_profilesList);
|
||||
item->setData(Qt::UserRole, QVariant::fromValue(profile.id));
|
||||
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.isEmpty() ? QStringLiteral("<none>") : profile.username;
|
||||
}();
|
||||
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 (!protocolFilter.isEmpty()
|
||||
&& profile.protocol.compare(protocolFilter, Qt::CaseInsensitive) != 0) {
|
||||
continue;
|
||||
}
|
||||
item->setToolTip(tooltip);
|
||||
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
|
||||
{
|
||||
QListWidgetItem* item = m_profilesList->currentItem();
|
||||
QTreeWidgetItem* item = m_profilesTree->currentItem();
|
||||
if (item == nullptr) {
|
||||
return std::nullopt;
|
||||
}
|
||||
|
||||
const QVariant value = item->data(Qt::UserRole);
|
||||
const QVariant value = item->data(0, kProfileIdRole);
|
||||
if (!value.isValid()) {
|
||||
return std::nullopt;
|
||||
}
|
||||
@@ -165,16 +836,22 @@ std::optional<Profile> ProfilesWindow::selectedProfile() const
|
||||
return m_repository->getProfile(id);
|
||||
}
|
||||
|
||||
void ProfilesWindow::createProfile()
|
||||
void ProfilesWindow::createProfile(const QString& defaultFolderPath)
|
||||
{
|
||||
ProfileDialog dialog(this);
|
||||
dialog.setDialogTitle(QStringLiteral("New Profile"));
|
||||
dialog.setDefaultFolderPath(defaultFolderPath);
|
||||
|
||||
if (dialog.exec() != QDialog::Accepted) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!m_repository->createProfile(dialog.profile()).has_value()) {
|
||||
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")
|
||||
@@ -184,7 +861,7 @@ void ProfilesWindow::createProfile()
|
||||
return;
|
||||
}
|
||||
|
||||
loadProfiles(m_searchBox->text());
|
||||
loadProfiles();
|
||||
}
|
||||
|
||||
void ProfilesWindow::editSelectedProfile()
|
||||
@@ -207,6 +884,9 @@ void ProfilesWindow::editSelectedProfile()
|
||||
|
||||
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,
|
||||
@@ -218,7 +898,7 @@ void ProfilesWindow::editSelectedProfile()
|
||||
return;
|
||||
}
|
||||
|
||||
loadProfiles(m_searchBox->text());
|
||||
loadProfiles();
|
||||
}
|
||||
|
||||
void ProfilesWindow::deleteSelectedProfile()
|
||||
@@ -252,17 +932,18 @@ void ProfilesWindow::deleteSelectedProfile()
|
||||
return;
|
||||
}
|
||||
|
||||
loadProfiles(m_searchBox->text());
|
||||
loadProfiles();
|
||||
}
|
||||
|
||||
void ProfilesWindow::openSessionForItem(QListWidgetItem* item)
|
||||
void ProfilesWindow::openSessionForItem(QTreeWidgetItem* item)
|
||||
{
|
||||
if (item == nullptr) {
|
||||
return;
|
||||
}
|
||||
|
||||
const QVariant value = item->data(Qt::UserRole);
|
||||
const QVariant value = item->data(0, kProfileIdRole);
|
||||
if (!value.isValid()) {
|
||||
item->setExpanded(!item->isExpanded());
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user