From 57fbeb4a5f6f2194c06949f9f1d8b94186dd671d Mon Sep 17 00:00:00 2001 From: Ted Robertson <10043369+tredondo@users.noreply.github.com> Date: Tue, 12 May 2026 14:57:11 -0700 Subject: [PATCH] gui: replace flat group ComboBox with tree view in PasskeyImportDialog MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The group selector in the passkey import dialog used a QComboBox populated with flat slash-separated paths (e.g. "Root/SaaS/AWS"). For databases with many groups this list is hard to navigate and is inconsistent with the rest of the UI, where group selection uses a QTreeView. Replace the ComboBox with two QRadioButtons ("Default passkeys group" / "Select group:") and a QTreeView backed by a GroupModelNoRecycle proxy — the same pattern already used by DatabaseSettingsWidgetFdoSecrets. The recycle-bin group is hidden. When "Select group" is chosen and nothing is selected yet the root is auto-selected so there is always a valid destination. The dialog minimum height is raised to accommodate the tree view. Relevant files: src/gui/passkeys/PasskeyImportDialog.ui, src/gui/passkeys/PasskeyImportDialog.h, src/gui/passkeys/PasskeyImportDialog.cpp --- src/gui/passkeys/PasskeyImportDialog.cpp | 111 ++++++++++++++++++++--- src/gui/passkeys/PasskeyImportDialog.h | 5 +- src/gui/passkeys/PasskeyImportDialog.ui | 80 +++++++++++++++- 3 files changed, 180 insertions(+), 16 deletions(-) diff --git a/src/gui/passkeys/PasskeyImportDialog.cpp b/src/gui/passkeys/PasskeyImportDialog.cpp index 179b2ed96d..c250aae5a8 100644 --- a/src/gui/passkeys/PasskeyImportDialog.cpp +++ b/src/gui/passkeys/PasskeyImportDialog.cpp @@ -21,8 +21,55 @@ #include "browser/BrowserService.h" #include "core/Metadata.h" #include "gui/MainWindow.h" +#include "gui/group/GroupModel.h" #include #include +#include + +// --------------------------------------------------------------------------- +// GroupModelNoRecycle +// Thin QSortFilterProxyModel that hides the recycle bin group (same pattern +// as DatabaseSettingsWidgetFdoSecrets::GroupModelNoRecycle). +// --------------------------------------------------------------------------- +class PasskeyImportDialog::GroupModelNoRecycle : public QSortFilterProxyModel +{ + Q_OBJECT + + Database* m_db; + +public: + explicit GroupModelNoRecycle(Database* db, QObject* parent = nullptr) + : QSortFilterProxyModel(parent) + , m_db(db) + { + Q_ASSERT(db); + setSourceModel(new GroupModel(m_db, this)); + } + + Group* groupFromIndex(const QModelIndex& index) const + { + auto* groupModel = qobject_cast(sourceModel()); + Q_ASSERT(groupModel); + return groupModel->groupFromIndex(mapToSource(index)); + } + +protected: + bool filterAcceptsRow(int sourceRow, const QModelIndex& sourceParent) const override + { + const auto sourceIdx = sourceModel()->index(sourceRow, 0, sourceParent); + if (!sourceIdx.isValid()) { + return false; + } + auto* groupModel = qobject_cast(sourceModel()); + Q_ASSERT(groupModel); + const auto* group = groupModel->groupFromIndex(sourceIdx); + const auto* recycleBin = m_db->metadata()->recycleBin(); + return group && !group->isRecycled() + && (!recycleBin || group->uuid() != recycleBin->uuid()); + } +}; + +// --------------------------------------------------------------------------- PasskeyImportDialog::PasskeyImportDialog(QWidget* parent) : QDialog(parent) @@ -38,7 +85,26 @@ PasskeyImportDialog::PasskeyImportDialog(QWidget* parent) connect(m_ui->cancelButton, SIGNAL(clicked()), SLOT(reject())); connect(m_ui->selectDatabaseCombobBox, SIGNAL(currentIndexChanged(int)), SLOT(changeDatabase(int))); connect(m_ui->selectEntryComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changeEntry(int))); - connect(m_ui->selectGroupComboBox, SIGNAL(currentIndexChanged(int)), SLOT(changeGroup(int))); + + // When the user toggles between "default group" and "select group", + // re-evaluate which group UUID is current. + connect(m_ui->useDefaultGroupRadio, &QRadioButton::toggled, + this, &PasskeyImportDialog::onGroupSelectionChanged); + + // When "Select group" becomes checked and nothing is selected yet, + // auto-select the root item so the user always has a valid destination. + connect(m_ui->selectCustomGroupRadio, &QRadioButton::toggled, this, [this](bool checked) { + if (checked && m_groupModel + && !m_ui->selectGroupTreeView->selectionModel()->hasSelection()) { + const auto rootIdx = m_groupModel->index(0, 0); + if (rootIdx.isValid()) { + m_ui->selectGroupTreeView->selectionModel()->select( + rootIdx, QItemSelectionModel::SelectCurrent); + m_ui->selectGroupTreeView->setCurrentIndex(rootIdx); + } + } + onGroupSelectionChanged(); + }); } PasskeyImportDialog::~PasskeyImportDialog() @@ -169,16 +235,29 @@ void PasskeyImportDialog::addGroups() return; } - m_ui->selectGroupComboBox->clear(); - m_ui->selectGroupComboBox->addItem(tr("Default passkeys group (Imported Passkeys)"), {}); - - for (const auto& group : m_selectedDatabase->rootGroup()->groupsRecursive(true)) { - if (!group || group->isRecycled() || group == m_selectedDatabase->metadata()->recycleBin()) { - continue; - } + // Disconnect any signal still bound to the old selection model. + if (auto* sm = m_ui->selectGroupTreeView->selectionModel()) { + disconnect(sm, nullptr, this, nullptr); + } - m_ui->selectGroupComboBox->addItem(group->fullPath(), group->uuid()); + // Reset to the default group and uncheck any custom selection. + m_selectedGroupUuid = QUuid(); + { + QSignalBlocker b(m_ui->useDefaultGroupRadio); + m_ui->useDefaultGroupRadio->setChecked(true); } + + // (Re-)build the tree model, filtering out the recycle bin. + m_groupModel.reset(new GroupModelNoRecycle(m_selectedDatabase.data(), this)); + m_ui->selectGroupTreeView->setModel(m_groupModel.data()); + m_ui->selectGroupTreeView->expandAll(); + + connect(m_ui->selectGroupTreeView->selectionModel(), + &QItemSelectionModel::selectionChanged, + this, + &PasskeyImportDialog::onGroupSelectionChanged); + + emit updateEntries(); } void PasskeyImportDialog::changeDatabase(int index) @@ -193,8 +272,18 @@ void PasskeyImportDialog::changeEntry(int index) m_selectedEntryUuid = m_ui->selectEntryComboBox->itemData(index).value(); } -void PasskeyImportDialog::changeGroup(int index) +void PasskeyImportDialog::onGroupSelectionChanged() { - m_selectedGroupUuid = m_ui->selectGroupComboBox->itemData(index).value(); + if (m_ui->useDefaultGroupRadio->isChecked()) { + m_selectedGroupUuid = QUuid(); + } else if (m_groupModel) { + const auto indexes = m_ui->selectGroupTreeView->selectionModel()->selectedIndexes(); + if (!indexes.isEmpty()) { + const auto* group = m_groupModel->groupFromIndex(indexes.first()); + m_selectedGroupUuid = group ? group->uuid() : QUuid(); + } + } emit updateEntries(); } + +#include "PasskeyImportDialog.moc" diff --git a/src/gui/passkeys/PasskeyImportDialog.h b/src/gui/passkeys/PasskeyImportDialog.h index 920a6a020a..7c502ae586 100644 --- a/src/gui/passkeys/PasskeyImportDialog.h +++ b/src/gui/passkeys/PasskeyImportDialog.h @@ -52,6 +52,8 @@ class PasskeyImportDialog : public QDialog private: void addDatabases(); + class GroupModelNoRecycle; + signals: void updateEntries(); void updateGroups(); @@ -61,10 +63,11 @@ private slots: void addGroups(); void changeDatabase(int index); void changeEntry(int index); - void changeGroup(int index); + void onGroupSelectionChanged(); private: QScopedPointer m_ui; + QScopedPointer m_groupModel; QSharedPointer m_selectedDatabase; QUuid m_selectedDatabaseUuid; QUuid m_selectedEntryUuid; diff --git a/src/gui/passkeys/PasskeyImportDialog.ui b/src/gui/passkeys/PasskeyImportDialog.ui index 3b00809970..69ade4be0d 100644 --- a/src/gui/passkeys/PasskeyImportDialog.ui +++ b/src/gui/passkeys/PasskeyImportDialog.ui @@ -7,7 +7,7 @@ 0 0 500 - 300 + 500 @@ -19,7 +19,7 @@ 400 - 300 + 420 @@ -89,10 +89,62 @@ Group + + Qt::AlignLeading|Qt::AlignLeft|Qt::AlignTop + - + + + 4 + + + + + Default passkeys group (Imported Passkeys) + + + true + + + groupButtonGroup + + + + + + + Select group: + + + groupButtonGroup + + + + + + + false + + + + 0 + 120 + + + + QAbstractItemView::NoEditTriggers + + + true + + + false + + + + @@ -170,5 +222,25 @@ - + + + selectCustomGroupRadio + toggled(bool) + selectGroupTreeView + setEnabled(bool) + + + 249 + 150 + + + 249 + 220 + + + + + + +