#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 #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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()) { 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(ProfileSortOrder::NameAsc)); m_sortBox->addItem(QStringLiteral("Protocol"), static_cast(ProfileSortOrder::ProtocolAsc)); m_sortBox->addItem(QStringLiteral("Host"), static_cast(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 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 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 folderNodes; const std::vector 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(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& 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 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& 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("") : profile.username.trimmed()); } return profile.username.trimmed().isEmpty() ? QStringLiteral("") : 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 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 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& 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(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 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 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 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 = 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(); }