diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index f974db170b..7b6ba6c537 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -6432,6 +6432,14 @@ Expect some bugs and minor issues, this version is meant for testing purposes. + + Search Entries in selected Group + + + + Search Here… + + ManageDatabase diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index 8ef9ede40f..63f44bc102 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -46,6 +46,7 @@ #include "gui/SearchWidget.h" #include "gui/ShortcutSettingsPage.h" #include "gui/entry/EntryView.h" +#include "gui/group/GroupView.h" #include "gui/osutils/OSUtils.h" #include "gui/remote/RemoteSettings.h" @@ -412,6 +413,7 @@ MainWindow::MainWindow() m_ui->actionEntryDownloadIcon->setIcon(icons()->icon("favicon-download")); m_ui->actionGroupSortAsc->setIcon(icons()->icon("sort-alphabetical-ascending")); m_ui->actionGroupSortDesc->setIcon(icons()->icon("sort-alphabetical-descending")); + m_ui->actionGroupSearchHere->setIcon(icons()->icon("system-search")); m_ui->actionGroupNew->setIcon(icons()->icon("group-new")); m_ui->actionGroupEdit->setIcon(icons()->icon("group-edit")); @@ -550,6 +552,7 @@ MainWindow::MainWindow() m_actionMultiplexer.connect(m_ui->actionGroupSortAsc, SIGNAL(triggered()), SLOT(sortGroupsAsc())); m_actionMultiplexer.connect(m_ui->actionGroupSortDesc, SIGNAL(triggered()), SLOT(sortGroupsDesc())); m_actionMultiplexer.connect(m_ui->actionGroupDownloadFavicons, SIGNAL(triggered()), SLOT(downloadAllFavicons())); + connect(m_ui->actionGroupSearchHere, SIGNAL(triggered()), this, SLOT(searchInGroup())); connect(m_ui->actionSettings, SIGNAL(toggled(bool)), SLOT(switchToSettings(bool))); connect(m_ui->actionPasswordGenerator, SIGNAL(toggled(bool)), SLOT(togglePasswordGenerator(bool))); @@ -1039,6 +1042,7 @@ void MainWindow::updateMenuActionState() m_ui->actionGroupDownloadFavicons->setVisible(!inRecycleBin); #endif m_ui->actionGroupDownloadFavicons->setEnabled(groupSelected && groupHasEntries && !inRecycleBin); + m_ui->actionGroupSearchHere->setEnabled(groupSelected); // Database Menu m_ui->actionDatabaseSave->setEnabled(databaseUnlocked && m_ui->tabWidget->canSave()); @@ -1535,6 +1539,16 @@ void MainWindow::clearSSHAgent() #endif } +void MainWindow::searchInGroup() +{ + auto dbWidget = m_ui->tabWidget->currentDatabaseWidget(); + if (dbWidget && dbWidget->isVisible() && dbWidget->isEntryViewActive() && dbWidget->groupView()->currentGroup()) { + focusSearchWidget(); + Group* currentGroup = dbWidget->groupView()->currentGroup(); + m_searchWidget->setSearchGroupName(currentGroup->fullPath()); + } +} + void MainWindow::saveWindowInformation() { if (isVisible()) { @@ -2156,6 +2170,7 @@ void MainWindow::initActionCollection() m_ui->actionGroupSortAsc, m_ui->actionGroupSortDesc, m_ui->actionGroupEmptyRecycleBin, + m_ui->actionGroupSearchHere, // Tools Menu m_ui->actionPasswordGenerator, m_ui->actionClearSSHAgent, diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index 8effd06f43..b5456b95b3 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -155,6 +155,7 @@ private slots: void enableMenuAndToolbar(); void disableMenuAndToolbar(); void clearSSHAgent(); + void searchInGroup(); private: static const QString BaseWindowTitle; diff --git a/src/gui/MainWindow.ui b/src/gui/MainWindow.ui index 79f2ab36ca..2e6ff6c31e 100644 --- a/src/gui/MainWindow.ui +++ b/src/gui/MainWindow.ui @@ -366,6 +366,8 @@ + + @@ -1367,6 +1369,14 @@ QAction::TextHeuristicRole + + + Search Here… + + + Search Entries in selected Group + + diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 13c08318ae..adbe13f331 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -223,6 +223,18 @@ void SearchWidget::setLimitGroup(bool state) updateLimitGroup(); } +void SearchWidget::setSearchGroupName(const QString& name) +{ + // Quote and escape the group name so it is treated as a single group term, + // even if it contains spaces or special characters. + QString escapedName = name; + escapedName.replace("\"", "\\\""); + QString searchText = QStringLiteral("g:\"") + escapedName + QStringLiteral("\" "); + + m_ui->searchEdit->setText(searchText); + m_ui->searchEdit->setFocus(); +} + void SearchWidget::focusSearch() { m_ui->searchEdit->setFocus(); diff --git a/src/gui/SearchWidget.h b/src/gui/SearchWidget.h index bfc75de673..ed4eba6d10 100644 --- a/src/gui/SearchWidget.h +++ b/src/gui/SearchWidget.h @@ -46,6 +46,7 @@ class SearchWidget : public QWidget void connectSignals(SignalMultiplexer& mx); void setCaseSensitive(bool state); void setLimitGroup(bool state); + void setSearchGroupName(const QString& name); protected: // Filter key presses in the search field diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 4c95669290..eacf888570 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -2500,6 +2500,62 @@ void TestGui::addCannedEntries() QTest::mouseClick(entryNewWidget, Qt::LeftButton); QTest::keyClicks(titleEdit, "something 3"); QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + + addGroup("Finance"); + addGroup("Entertainment"); + + addEntry("Finance", "Chase", "user1", "password"); + addEntry("Finance", "Amex", "user1", "password123"); + addEntry("Finance", "Capital One", "user1", "password456"); + + addEntry("Entertainment", "Netflix", "user1", "password"); + addEntry("Entertainment", "Hulu", "user1", "password321"); + addEntry("Entertainment", "Apple TV", "user1", "password123"); +} + +void TestGui::addGroup(const QString& name) +{ + // Find buttons for group creation + auto* editGroupWidget = m_dbWidget->findChild("editGroupWidget"); + auto* nameEdit = editGroupWidget->findChild("editName"); + auto* editGroupWidgetButtonBox = editGroupWidget->findChild("buttonBox"); + + // Add group with specified name + Group* rootGroup = m_db->rootGroup(); + m_dbWidget->groupView()->setCurrentGroup(rootGroup); // Add group on root level + m_dbWidget->createGroup(); + QTest::keyClicks(nameEdit, name); + QTest::mouseClick(editGroupWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + m_dbWidget->groupView()->setCurrentGroup(rootGroup); // Reset to root level +} + +void TestGui::addEntry(const QString& groupName, const QString& title, const QString& username, const QString& password) +{ + // Find buttons + auto* toolBar = m_mainWindow->findChild("toolBar"); + QWidget* entryNewWidget = toolBar->widgetForAction(m_mainWindow->findChild("actionEntryNew")); + auto* editEntryWidget = m_dbWidget->findChild("editEntryWidget"); + auto* titleEdit = editEntryWidget->findChild("titleEdit"); + auto* usernameComboBox = editEntryWidget->findChild("usernameComboBox"); + auto* passwordEdit = + editEntryWidget->findChild("passwordEdit")->findChild("passwordEdit"); + auto* editEntryWidgetButtonBox = editEntryWidget->findChild("buttonBox"); + + // Add entry to specified group + QVERIFY(m_dbWidget->currentGroup()); + Group* group = m_dbWidget->currentGroup()->findChildByName(groupName); + m_dbWidget->groupView()->setCurrentGroup(group); + + QTest::mouseClick(entryNewWidget, Qt::LeftButton); + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::EditEntryMode); + + QTest::keyClicks(titleEdit, title); + QTest::keyClicks(usernameComboBox, username); + QTest::keyClicks(passwordEdit, password); + QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton); + + QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::Mode::ViewMode); + m_dbWidget->groupView()->setCurrentGroup(m_db->rootGroup()); } void TestGui::checkDatabase(const QString& filePath, const QString& expectedDbName) @@ -2584,3 +2640,22 @@ void TestGui::clickIndex(const QModelIndex& index, view->scrollTo(index); QTest::mouseClick(view->viewport(), button, stateKey, view->visualRect(index).center()); } + +void TestGui::testSearchHere() +{ + addCannedEntries(); + + QToolBar* toolBar = m_mainWindow->findChild("toolBar"); + SearchWidget* searchWidget = toolBar->findChild("SearchWidget"); + QVERIFY(searchWidget->isEnabled()); + + Group* entertainmentGroup = m_dbWidget->currentGroup()->findChildByName("Entertainment"); + m_dbWidget->groupView()->setCurrentGroup(entertainmentGroup); + + triggerAction("actionGroupSearchHere"); + QVERIFY(searchWidget->hasFocus()); + + QLineEdit* searchEdit = searchWidget->findChild("searchEdit"); + QString expectedText = QString("g:\"") + entertainmentGroup->fullPath() + QString("\" "); + QCOMPARE(searchEdit->text(), expectedText); +} diff --git a/tests/gui/TestGui.h b/tests/gui/TestGui.h index 514f7ce95b..dc40f21a5a 100644 --- a/tests/gui/TestGui.h +++ b/tests/gui/TestGui.h @@ -71,9 +71,12 @@ private slots: void testTrayRestoreHide(); void testShortcutConfig(); void testMenuActionStates(); + void testSearchHere(); private: void addCannedEntries(); + void addGroup(const QString& name); + void addEntry(const QString& groupName, const QString& title, const QString& username, const QString& password); void checkDatabase(const QString& filePath, const QString& expectedDbName); void checkDatabase(const QString& filePath = {}); void triggerAction(const QString& name);