From d8d758f0e17d616840d2321476733b497a5c1b3a Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Mon, 19 Mar 2018 23:16:22 -0400 Subject: [PATCH 1/6] Streamlined searcher code * Remove searching of group title and notes * End search when selecting a new group * Correct entry searcher tests to align with new code --- src/core/EntrySearcher.cpp | 66 +++++++++++++------------------------ src/core/EntrySearcher.h | 6 ++-- src/gui/DatabaseWidget.cpp | 7 ++-- src/gui/DatabaseWidget.h | 2 +- src/gui/SearchWidget.cpp | 1 + src/gui/SearchWidget.h | 8 ++--- tests/TestEntrySearcher.cpp | 7 ++-- 7 files changed, 38 insertions(+), 59 deletions(-) diff --git a/src/core/EntrySearcher.cpp b/src/core/EntrySearcher.cpp index 3413f1cd08..b181ad3898 100644 --- a/src/core/EntrySearcher.cpp +++ b/src/core/EntrySearcher.cpp @@ -22,62 +22,39 @@ QList EntrySearcher::search(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity) { - if (!group->resolveSearchingEnabled()) { - return QList(); - } - - return searchEntries(searchTerm, group, caseSensitivity); -} + QList results; -QList -EntrySearcher::searchEntries(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity) -{ - QList searchResult; - - const QList& entryList = group->entries(); - for (Entry* entry : entryList) { - searchResult.append(matchEntry(searchTerm, entry, caseSensitivity)); + if (group->resolveSearchingEnabled()) { + results.append(searchEntries(searchTerm, group->entries(), caseSensitivity)); } - const QList& children = group->children(); - for (Group* childGroup : children) { - if (childGroup->searchingEnabled() != Group::Disable) { - if (matchGroup(searchTerm, childGroup, caseSensitivity)) { - searchResult.append(childGroup->entriesRecursive()); - } else { - searchResult.append(searchEntries(searchTerm, childGroup, caseSensitivity)); - } + for (Group* childGroup : group->children()) { + if (childGroup->resolveSearchingEnabled()) { + results.append(searchEntries(searchTerm, childGroup->entries(), caseSensitivity)); } } - return searchResult; + return results; } -QList EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity) +QList EntrySearcher::searchEntries(const QString& searchTerm, const QList& entries, + Qt::CaseSensitivity caseSensitivity) { - const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts); - for (const QString& word : wordList) { - if (!wordMatch(word, entry, caseSensitivity)) { - return QList(); - } + QList results; + for (Entry* entry : entries) { + if (matchEntry(searchTerm, entry, caseSensitivity)) { + results.append(entry); + } } - - return QList() << entry; -} - -bool EntrySearcher::wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity) -{ - return entry->resolvePlaceholder(entry->title()).contains(word, caseSensitivity) - || entry->resolvePlaceholder(entry->username()).contains(word, caseSensitivity) - || entry->resolvePlaceholder(entry->url()).contains(word, caseSensitivity) - || entry->resolvePlaceholder(entry->notes()).contains(word, caseSensitivity); + return results; } -bool EntrySearcher::matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity) +bool EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry, + Qt::CaseSensitivity caseSensitivity) { const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts); for (const QString& word : wordList) { - if (!wordMatch(word, group, caseSensitivity)) { + if (!wordMatch(word, entry, caseSensitivity)) { return false; } } @@ -85,7 +62,10 @@ bool EntrySearcher::matchGroup(const QString& searchTerm, const Group* group, Qt return true; } -bool EntrySearcher::wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity) +bool EntrySearcher::wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity) { - return group->name().contains(word, caseSensitivity) || group->notes().contains(word, caseSensitivity); + return entry->resolvePlaceholder(entry->title()).contains(word, caseSensitivity) + || entry->resolvePlaceholder(entry->username()).contains(word, caseSensitivity) + || entry->resolvePlaceholder(entry->url()).contains(word, caseSensitivity) + || entry->resolvePlaceholder(entry->notes()).contains(word, caseSensitivity); } diff --git a/src/core/EntrySearcher.h b/src/core/EntrySearcher.h index 343734737d..def5eb8f6f 100644 --- a/src/core/EntrySearcher.h +++ b/src/core/EntrySearcher.h @@ -30,11 +30,9 @@ class EntrySearcher QList search(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity); private: - QList searchEntries(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity); - QList matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity); + QList searchEntries(const QString& searchTerm, const QList& entries, Qt::CaseSensitivity caseSensitivity); + bool matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity); bool wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity); - bool matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity); - bool wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity); }; #endif // KEEPASSX_ENTRYSEARCHER_H diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 3faf43a65f..a4f014bf6f 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -1047,9 +1047,12 @@ void DatabaseWidget::setSearchLimitGroup(bool state) void DatabaseWidget::onGroupChanged(Group* group) { - // Intercept group changes if in search mode - if (isInSearchMode()) { + if (isInSearchMode() && m_searchLimitGroup) { + // Perform new search if we are limiting search to the current group search(m_lastSearchText); + } else if (isInSearchMode()) { + // Otherwise cancel search + emit clearSearch(); } else { m_entryView->setGroup(group); } diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index a5cf538d7c..8f268c94ac 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -138,7 +138,7 @@ class DatabaseWidget : public QStackedWidget void mainSplitterSizesChanged(); void previewSplitterSizesChanged(); void entryViewStateChanged(); - void updateSearch(QString text); + void clearSearch(); public slots: void createEntry(); diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 40c63036c5..96bd05a5b3 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -113,6 +113,7 @@ void SearchWidget::connectSignals(SignalMultiplexer& mx) mx.connect(this, SIGNAL(limitGroupChanged(bool)), SLOT(setSearchLimitGroup(bool))); mx.connect(this, SIGNAL(copyPressed()), SLOT(copyPassword())); mx.connect(this, SIGNAL(downPressed()), SLOT(setFocus())); + mx.connect(SIGNAL(clearSearch()), m_ui->searchEdit, SLOT(clear())); mx.connect(m_ui->searchEdit, SIGNAL(returnPressed()), SLOT(switchToEntryEdit())); } diff --git a/src/gui/SearchWidget.h b/src/gui/SearchWidget.h index 0ec3287c19..39e17bcf49 100644 --- a/src/gui/SearchWidget.h +++ b/src/gui/SearchWidget.h @@ -36,14 +36,16 @@ class SearchWidget : public QWidget public: explicit SearchWidget(QWidget* parent = nullptr); - ~SearchWidget(); + ~SearchWidget() override; + + Q_DISABLE_COPY(SearchWidget) void connectSignals(SignalMultiplexer& mx); void setCaseSensitive(bool state); void setLimitGroup(bool state); protected: - bool eventFilter(QObject* obj, QEvent* event); + bool eventFilter(QObject* obj, QEvent* event) override; signals: void search(const QString& text); @@ -69,8 +71,6 @@ private slots: QTimer* m_searchTimer; QAction* m_actionCaseSensitive; QAction* m_actionLimitGroup; - - Q_DISABLE_COPY(SearchWidget) }; #endif // SEARCHWIDGET_H diff --git a/tests/TestEntrySearcher.cpp b/tests/TestEntrySearcher.cpp index 659f7a4897..0c0a2c3e45 100644 --- a/tests/TestEntrySearcher.cpp +++ b/tests/TestEntrySearcher.cpp @@ -53,7 +53,6 @@ void TestEntrySearcher::testSearch() group2111->setParent(group211); group1->setSearchingEnabled(Group::Disable); - group11->setSearchingEnabled(Group::Enable); Entry* eRoot = new Entry(); eRoot->setNotes("test search term test"); @@ -88,15 +87,13 @@ void TestEntrySearcher::testSearch() e3b->setGroup(group3); m_searchResult = m_entrySearcher.search("search term", m_groupRoot, Qt::CaseInsensitive); - QCOMPARE(m_searchResult.count(), 3); + QCOMPARE(m_searchResult.count(), 2); m_searchResult = m_entrySearcher.search("search term", group211, Qt::CaseInsensitive); QCOMPARE(m_searchResult.count(), 1); + // Parent group disabled search m_searchResult = m_entrySearcher.search("search term", group11, Qt::CaseInsensitive); - QCOMPARE(m_searchResult.count(), 1); - - m_searchResult = m_entrySearcher.search("search term", group1, Qt::CaseInsensitive); QCOMPARE(m_searchResult.count(), 0); } From 4b57fcb5633229667622e08e505c0eb2512cb1d0 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Wed, 21 Mar 2018 21:52:57 -0400 Subject: [PATCH 2/6] Clean up Entry Model/View code --- src/gui/DatabaseWidget.cpp | 12 ++++---- src/gui/DatabaseWidget.h | 4 +-- src/gui/entry/EntryModel.cpp | 4 +-- src/gui/entry/EntryModel.h | 16 +++++----- src/gui/entry/EntryView.cpp | 58 ++++++++++-------------------------- src/gui/entry/EntryView.h | 11 +++---- tests/TestEntryModel.cpp | 4 +-- 7 files changed, 37 insertions(+), 72 deletions(-) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index a4f014bf6f..ebec29b46f 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -100,7 +100,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) m_entryView = new EntryView(rightHandSideWidget); m_entryView->setObjectName("entryView"); m_entryView->setContextMenuPolicy(Qt::CustomContextMenu); - m_entryView->setGroup(db->rootGroup()); + m_entryView->displayGroup(db->rootGroup()); connect(m_entryView, SIGNAL(customContextMenuRequested(QPoint)), SLOT(emitEntryContextMenuRequested(QPoint))); // Add a notification for when we are searching @@ -291,7 +291,7 @@ bool DatabaseWidget::isUsernamesHidden() const /** * Set state of entry view 'Hide Usernames' setting */ -void DatabaseWidget::setUsernamesHidden(const bool hide) +void DatabaseWidget::setUsernamesHidden(bool hide) { m_entryView->setUsernamesHidden(hide); } @@ -307,7 +307,7 @@ bool DatabaseWidget::isPasswordsHidden() const /** * Set state of entry view 'Hide Passwords' setting */ -void DatabaseWidget::setPasswordsHidden(const bool hide) +void DatabaseWidget::setPasswordsHidden(bool hide) { m_entryView->setPasswordsHidden(hide); } @@ -1018,7 +1018,7 @@ void DatabaseWidget::search(const QString& searchtext) QList searchResult = EntrySearcher().search(searchtext, searchGroup, caseSensitive); - m_entryView->setEntryList(searchResult); + m_entryView->displaySearch(searchResult); m_lastSearchText = searchtext; // Display a label detailing our search results @@ -1054,7 +1054,7 @@ void DatabaseWidget::onGroupChanged(Group* group) // Otherwise cancel search emit clearSearch(); } else { - m_entryView->setGroup(group); + m_entryView->displayGroup(group); } } @@ -1069,7 +1069,7 @@ void DatabaseWidget::endSearch() emit listModeAboutToActivate(); // Show the normal entry view of the current group - m_entryView->setGroup(currentGroup()); + m_entryView->displayGroup(currentGroup()); emit listModeActivated(); } diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 8f268c94ac..a5d881ff99 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -93,9 +93,9 @@ class DatabaseWidget : public QStackedWidget QList previewSplitterSizes() const; void setPreviewSplitterSizes(const QList& sizes); bool isUsernamesHidden() const; - void setUsernamesHidden(const bool hide); + void setUsernamesHidden(bool hide); bool isPasswordsHidden() const; - void setPasswordsHidden(const bool hide); + void setPasswordsHidden(bool hide); QByteArray entryViewState() const; bool setEntryViewState(const QByteArray& state) const; void clearAllWidgets(); diff --git a/src/gui/entry/EntryModel.cpp b/src/gui/entry/EntryModel.cpp index 2edc49b255..194c4b571a 100644 --- a/src/gui/entry/EntryModel.cpp +++ b/src/gui/entry/EntryModel.cpp @@ -72,10 +72,9 @@ void EntryModel::setGroup(Group* group) makeConnections(group); endResetModel(); - emit switchedToListMode(); } -void EntryModel::setEntryList(const QList& entries) +void EntryModel::setEntries(const QList& entries) { beginResetModel(); @@ -109,7 +108,6 @@ void EntryModel::setEntryList(const QList& entries) } endResetModel(); - emit switchedToSearchMode(); } int EntryModel::rowCount(const QModelIndex& parent) const diff --git a/src/gui/entry/EntryModel.h b/src/gui/entry/EntryModel.h index 3e9f2824ab..5f405bd41b 100644 --- a/src/gui/entry/EntryModel.h +++ b/src/gui/entry/EntryModel.h @@ -60,22 +60,20 @@ class EntryModel : public QAbstractTableModel QStringList mimeTypes() const override; QMimeData* mimeData(const QModelIndexList& indexes) const override; - void setEntryList(const QList& entries); - void setPaperClipPixmap(const QPixmap& paperclip); + void setGroup(Group* group); + void setEntries(const QList& entries); + bool isUsernamesHidden() const; + void setUsernamesHidden(bool hide); bool isPasswordsHidden() const; + void setPasswordsHidden(bool hide); + + void setPaperClipPixmap(const QPixmap& paperclip); signals: - void switchedToListMode(); - void switchedToSearchMode(); void usernamesHiddenChanged(); void passwordsHiddenChanged(); -public slots: - void setGroup(Group* group); - void setUsernamesHidden(bool hide); - void setPasswordsHidden(bool hide); - private slots: void entryAboutToAdd(Entry* entry); void entryAdded(Entry* entry); diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp index ecdccd7bb8..28d5ec2bbd 100644 --- a/src/gui/entry/EntryView.cpp +++ b/src/gui/entry/EntryView.cpp @@ -50,11 +50,7 @@ EntryView::EntryView(QWidget* parent) setDefaultDropAction(Qt::MoveAction); connect(this, SIGNAL(doubleClicked(QModelIndex)), SLOT(emitEntryActivated(QModelIndex))); - connect(selectionModel(), - SIGNAL(selectionChanged(QItemSelection,QItemSelection)), SIGNAL(entrySelectionChanged())); - - connect(m_model, SIGNAL(switchedToListMode()), SLOT(switchToListMode())); - connect(m_model, SIGNAL(switchedToSearchMode()), SLOT(switchToSearchMode())); + connect(selectionModel(), SIGNAL(selectionChanged(QItemSelection,QItemSelection)), SIGNAL(entrySelectionChanged())); connect(m_model, SIGNAL(usernamesHiddenChanged()), SIGNAL(viewStateChanged())); connect(m_model, SIGNAL(passwordsHiddenChanged()), SIGNAL(viewStateChanged())); @@ -158,16 +154,25 @@ void EntryView::focusOutEvent(QFocusEvent* event) QTreeView::focusOutEvent(event); } -void EntryView::setGroup(Group* group) +void EntryView::displayGroup(Group* group) { m_model->setGroup(group); + header()->hideSection(EntryModel::ParentGroup); setFirstEntryActive(); + m_inSearchMode = false; } -void EntryView::setEntryList(const QList& entries) +void EntryView::displaySearch(const QList& entries) { - m_model->setEntryList(entries); + m_model->setEntries(entries); + header()->showSection(EntryModel::ParentGroup); + + // Reset sort column to 'Group', overrides DatabaseWidgetStateSync + m_sortModel->sort(EntryModel::ParentGroup, Qt::AscendingOrder); + sortByColumn(EntryModel::ParentGroup, Qt::AscendingOrder); + setFirstEntryActive(); + m_inSearchMode = true; } void EntryView::setFirstEntryActive() @@ -227,39 +232,6 @@ Entry* EntryView::entryFromIndex(const QModelIndex& index) } } -/** - * Switch to list mode, i.e. list entries of group - */ -void EntryView::switchToListMode() -{ - if (!m_inSearchMode) { - return; - } - - header()->hideSection(EntryModel::ParentGroup); - m_inSearchMode = false; -} - -/** - * Switch to search mode, i.e. list search results - */ -void EntryView::switchToSearchMode() -{ - if (m_inSearchMode) { - return; - } - - header()->showSection(EntryModel::ParentGroup); - - // Always set sorting to column 'Group', as it does not feel right to - // have the last known sort configuration of search view restored by - // 'DatabaseWidgetStateSync', which is what happens without this - m_sortModel->sort(EntryModel::ParentGroup, Qt::AscendingOrder); - sortByColumn(EntryModel::ParentGroup, Qt::AscendingOrder); - - m_inSearchMode = true; -} - /** * Get current state of 'Hide Usernames' setting (NOTE: just pass-through for * m_model) @@ -272,7 +244,7 @@ bool EntryView::isUsernamesHidden() const /** * Set state of 'Hide Usernames' setting (NOTE: just pass-through for m_model) */ -void EntryView::setUsernamesHidden(const bool hide) +void EntryView::setUsernamesHidden(bool hide) { bool block = m_hideUsernamesAction->signalsBlocked(); m_hideUsernamesAction->blockSignals(true); @@ -294,7 +266,7 @@ bool EntryView::isPasswordsHidden() const /** * Set state of 'Hide Passwords' setting (NOTE: just pass-through for m_model) */ -void EntryView::setPasswordsHidden(const bool hide) +void EntryView::setPasswordsHidden(bool hide) { bool block = m_hidePasswordsAction->signalsBlocked(); m_hidePasswordsAction->blockSignals(true); diff --git a/src/gui/entry/EntryView.h b/src/gui/entry/EntryView.h index 2030f0ec72..fa002c717e 100644 --- a/src/gui/entry/EntryView.h +++ b/src/gui/entry/EntryView.h @@ -39,19 +39,18 @@ class EntryView : public QTreeView Entry* currentEntry(); void setCurrentEntry(Entry* entry); Entry* entryFromIndex(const QModelIndex& index); - void setEntryList(const QList& entries); bool inSearchMode(); int numberOfSelectedEntries(); void setFirstEntryActive(); bool isUsernamesHidden() const; - void setUsernamesHidden(const bool hide); + void setUsernamesHidden(bool hide); bool isPasswordsHidden() const; - void setPasswordsHidden(const bool hide); + void setPasswordsHidden(bool hide); QByteArray viewState() const; bool setViewState(const QByteArray& state); -public slots: - void setGroup(Group* group); + void displayGroup(Group* group); + void displaySearch(const QList& entries); signals: void entryActivated(Entry* entry, EntryModel::ModelColumn column); @@ -65,8 +64,6 @@ public slots: private slots: void emitEntryActivated(const QModelIndex& index); - void switchToListMode(); - void switchToSearchMode(); void showHeaderMenu(const QPoint& position); void toggleColumnVisibility(QAction* action); void fitColumnsToWindow(); diff --git a/tests/TestEntryModel.cpp b/tests/TestEntryModel.cpp index 49939e256f..e32de24669 100644 --- a/tests/TestEntryModel.cpp +++ b/tests/TestEntryModel.cpp @@ -307,7 +307,7 @@ void TestEntryModel::testProxyModel() QList entryList; entryList << entry; - modelSource->setEntryList(entryList); + modelSource->setEntries(entryList); /** * @author Fonic @@ -346,7 +346,7 @@ void TestEntryModel::testDatabaseDelete() Entry* entry2 = new Entry(); entry2->setGroup(db2->rootGroup()); - model->setEntryList(QList() << entry1 << entry2); + model->setEntries(QList() << entry1 << entry2); QCOMPARE(model->rowCount(), 2); From 4b983251cb464d104aafd95ea56e19b5c404247a Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sun, 25 Mar 2018 16:24:30 -0400 Subject: [PATCH 3/6] Add advanced search term parser * Support quoted strings & per-field searching * Support regex and exact matching * Simplify search sequence * Make search widget larger * Add regex converter to Tools namespace --- src/browser/BrowserService.cpp | 2 +- src/core/EntrySearcher.cpp | 142 ++++++++++++++++++++++++++++----- src/core/EntrySearcher.h | 39 ++++++++- src/core/Tools.cpp | 29 +++++++ src/core/Tools.h | 3 + src/gui/DatabaseWidget.cpp | 11 ++- src/gui/DatabaseWidget.h | 3 +- src/gui/SearchWidget.cpp | 2 +- src/gui/SearchWidget.ui | 15 ++++ src/gui/entry/EntryView.cpp | 4 +- src/gui/entry/EntryView.h | 6 +- tests/TestEntrySearcher.cpp | 107 ++++++++++++++++++------- tests/TestEntrySearcher.h | 3 +- tests/gui/TestGui.cpp | 3 +- 14 files changed, 302 insertions(+), 67 deletions(-) diff --git a/src/browser/BrowserService.cpp b/src/browser/BrowserService.cpp index a69508026d..a1315ad497 100644 --- a/src/browser/BrowserService.cpp +++ b/src/browser/BrowserService.cpp @@ -390,7 +390,7 @@ QList BrowserService::searchEntries(Database* db, const QString& hostnam return entries; } - for (Entry* entry : EntrySearcher().search(baseDomain(hostname), rootGroup, Qt::CaseInsensitive)) { + for (Entry* entry : EntrySearcher().search(baseDomain(hostname), rootGroup)) { QString entryUrl = entry->url(); QUrl entryQUrl(entryUrl); QString entryScheme = entryQUrl.scheme(); diff --git a/src/core/EntrySearcher.cpp b/src/core/EntrySearcher.cpp index b181ad3898..6614ab4637 100644 --- a/src/core/EntrySearcher.cpp +++ b/src/core/EntrySearcher.cpp @@ -19,42 +19,91 @@ #include "EntrySearcher.h" #include "core/Group.h" +#include "core/Tools.h" -QList EntrySearcher::search(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity) +EntrySearcher::EntrySearcher(bool caseSensitive) + : m_caseSensitive(caseSensitive) + , m_termParser(R"re(([-*+]+)?(?:(\w*):)?(?:(?=")"((?:[^"\\]|\\.)*)"|([^ ]*))( |$))re") + // Group 1 = modifiers, Group 2 = field, Group 3 = quoted string, Group 4 = unquoted string { - QList results; +} - if (group->resolveSearchingEnabled()) { - results.append(searchEntries(searchTerm, group->entries(), caseSensitivity)); - } +QList EntrySearcher::search(const QString& searchString, const Group* baseGroup, bool forceSearch) +{ + Q_ASSERT(baseGroup); - for (Group* childGroup : group->children()) { - if (childGroup->resolveSearchingEnabled()) { - results.append(searchEntries(searchTerm, childGroup->entries(), caseSensitivity)); + QList results; + for (const auto group : baseGroup->groupsRecursive(true)) { + if (forceSearch || group->resolveSearchingEnabled()) { + results.append(searchEntries(searchString, group->entries())); } } return results; } -QList EntrySearcher::searchEntries(const QString& searchTerm, const QList& entries, - Qt::CaseSensitivity caseSensitivity) +QList EntrySearcher::searchEntries(const QString& searchString, const QList& entries) { QList results; for (Entry* entry : entries) { - if (matchEntry(searchTerm, entry, caseSensitivity)) { + if (searchEntryImpl(searchString, entry)) { results.append(entry); } } return results; } -bool EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry, - Qt::CaseSensitivity caseSensitivity) +void EntrySearcher::setCaseSensitive(bool state) +{ + m_caseSensitive = state; +} + +bool EntrySearcher::isCaseSensitive() { - const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts); - for (const QString& word : wordList) { - if (!wordMatch(word, entry, caseSensitivity)) { + return m_caseSensitive; +} + +bool EntrySearcher::searchEntryImpl(const QString& searchString, Entry* entry) +{ + // Pre-load in case they are needed + auto attributes = QStringList(entry->attributes()->keys()); + auto attachments = QStringList(entry->attachments()->keys()); + + bool found; + auto searchTerms = parseSearchTerms(searchString); + + for (const auto& term : searchTerms) { + switch (term->field) { + case Field::Title: + found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch(); + break; + case Field::Username: + found = term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch(); + break; + case Field::Password: + found = term->regex.match(entry->resolvePlaceholder(entry->password())).hasMatch(); + break; + case Field::Url: + found = term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch(); + break; + case Field::Notes: + found = term->regex.match(entry->notes()).hasMatch(); + break; + case Field::Attribute: + found = !attributes.filter(term->regex).empty(); + break; + case Field::Attachment: + found = !attachments.filter(term->regex).empty(); + break; + default: + found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch() || + term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch() || + term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch() || + term->regex.match(entry->notes()).hasMatch(); + } + + // Short circuit if we failed to match or we matched and are excluding this term + if (!found || term->exclude) { return false; } } @@ -62,10 +111,61 @@ bool EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry, return true; } -bool EntrySearcher::wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity) +QList > EntrySearcher::parseSearchTerms(const QString& searchString) { - return entry->resolvePlaceholder(entry->title()).contains(word, caseSensitivity) - || entry->resolvePlaceholder(entry->username()).contains(word, caseSensitivity) - || entry->resolvePlaceholder(entry->url()).contains(word, caseSensitivity) - || entry->resolvePlaceholder(entry->notes()).contains(word, caseSensitivity); + auto terms = QList >(); + + auto results = m_termParser.globalMatch(searchString); + while (results.hasNext()) { + auto result = results.next(); + auto term = QSharedPointer::create(); + + // Quoted string group + term->word = result.captured(3); + + // If empty, use the unquoted string group + if (term->word.isEmpty()) { + term->word = result.captured(4); + } + + // If still empty, ignore this match + if (term->word.isEmpty()) { + continue; + } + + auto mods = result.captured(1); + + // Convert term to regex + term->regex = Tools::convertToRegex(term->word, !mods.contains("*"), mods.contains("+"), m_caseSensitive); + + // Exclude modifier + term->exclude = mods.contains("-"); + + // Determine the field to search + QString field = result.captured(2); + if (!field.isEmpty()) { + auto cs = Qt::CaseInsensitive; + if (field.compare("title", cs) == 0) { + term->field = Field::Title; + } else if (field.startsWith("user", cs)) { + term->field = Field::Username; + } else if (field.startsWith("pass", cs)) { + term->field = Field::Password; + } else if (field.compare("url", cs) == 0) { + term->field = Field::Url; + } else if (field.compare("notes", cs) == 0) { + term->field = Field::Notes; + } else if (field.startsWith("attr", cs)) { + term->field = Field::Attribute; + } else if (field.startsWith("attach", cs)) { + term->field = Field::Attachment; + } else { + term->field = Field::Undefined; + } + } + + terms.append(term); + } + + return terms; } diff --git a/src/core/EntrySearcher.h b/src/core/EntrySearcher.h index def5eb8f6f..ec71a7ce1e 100644 --- a/src/core/EntrySearcher.h +++ b/src/core/EntrySearcher.h @@ -20,6 +20,7 @@ #define KEEPASSX_ENTRYSEARCHER_H #include +#include class Group; class Entry; @@ -27,12 +28,42 @@ class Entry; class EntrySearcher { public: - QList search(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity); + explicit EntrySearcher(bool caseSensitive = false); + + QList search(const QString& searchString, const Group* baseGroup, bool forceSearch = false); + QList searchEntries(const QString& searchString, const QList& entries); + + void setCaseSensitive(bool state); + bool isCaseSensitive(); private: - QList searchEntries(const QString& searchTerm, const QList& entries, Qt::CaseSensitivity caseSensitivity); - bool matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity); - bool wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity); + bool searchEntryImpl(const QString& searchString, Entry* entry); + + enum class Field { + Undefined, + Title, + Username, + Password, + Url, + Notes, + Attribute, + Attachment + }; + + struct SearchTerm + { + Field field; + QString word; + QRegularExpression regex; + bool exclude; + }; + + QList > parseSearchTerms(const QString& searchString); + + bool m_caseSensitive; + QRegularExpression m_termParser; + + friend class TestEntrySearcher; }; #endif // KEEPASSX_ENTRYSEARCHER_H diff --git a/src/core/Tools.cpp b/src/core/Tools.cpp index ded3a16518..362cfa9375 100644 --- a/src/core/Tools.cpp +++ b/src/core/Tools.cpp @@ -26,6 +26,8 @@ #include #include #include +#include + #include #include @@ -199,4 +201,31 @@ void wait(int ms) } } +// Escape common regex symbols except for *, ?, and | +auto regexEscape = QRegularExpression(R"re(([-[\]{}()+.,\\\/^$#]))re"); + +QRegularExpression convertToRegex(const QString& string, bool useWildcards, bool exactMatch, bool caseSensitive) +{ + QString pattern = string; + + // Wildcard support (*, ?, |) + if (useWildcards) { + pattern.replace(regexEscape, "\\\\1"); + pattern.replace("*", ".*"); + pattern.replace("?", "."); + } + + // Exact modifier + if (exactMatch) { + pattern = "^" + pattern + "$"; + } + + auto regex = QRegularExpression(pattern); + if (!caseSensitive) { + regex.setPatternOptions(QRegularExpression::CaseInsensitiveOption); + } + + return regex; +} + } // namespace Tools diff --git a/src/core/Tools.h b/src/core/Tools.h index 13d9869f7d..37214f0691 100644 --- a/src/core/Tools.h +++ b/src/core/Tools.h @@ -27,6 +27,7 @@ #include class QIODevice; +class QRegularExpression; namespace Tools { @@ -38,6 +39,8 @@ bool isHex(const QByteArray& ba); bool isBase64(const QByteArray& ba); void sleep(int ms); void wait(int ms); +QRegularExpression convertToRegex(const QString& string, bool useWildcards = false, bool exactMatch = false, + bool caseSensitive = false); template RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index ebec29b46f..701ba588c2 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -209,7 +209,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) m_fileWatchUnblockTimer.setSingleShot(true); m_ignoreAutoReload = false; - m_searchCaseSensitive = false; + m_EntrySearcher = new EntrySearcher(false); m_searchLimitGroup = config()->get("SearchLimitGroup", false).toBool(); #ifdef WITH_XC_SSHAGENT @@ -227,6 +227,7 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent) DatabaseWidget::~DatabaseWidget() { + delete m_EntrySearcher; } DatabaseWidget::Mode DatabaseWidget::currentMode() const @@ -1012,17 +1013,15 @@ void DatabaseWidget::search(const QString& searchtext) emit searchModeAboutToActivate(); - Qt::CaseSensitivity caseSensitive = m_searchCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive; - Group* searchGroup = m_searchLimitGroup ? currentGroup() : m_db->rootGroup(); - QList searchResult = EntrySearcher().search(searchtext, searchGroup, caseSensitive); + QList searchResult = m_EntrySearcher->search(searchtext, searchGroup); m_entryView->displaySearch(searchResult); m_lastSearchText = searchtext; // Display a label detailing our search results - if (searchResult.size() > 0) { + if (!searchResult.isEmpty()) { m_searchingLabel->setText(tr("Search Results (%1)").arg(searchResult.size())); } else { m_searchingLabel->setText(tr("No Results")); @@ -1035,7 +1034,7 @@ void DatabaseWidget::search(const QString& searchtext) void DatabaseWidget::setSearchCaseSensitive(bool state) { - m_searchCaseSensitive = state; + m_EntrySearcher->setCaseSensitive(state); refreshSearch(); } diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index a5d881ff99..d0c4e2042f 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -37,6 +37,7 @@ class EditEntryWidget; class EditGroupWidget; class Entry; class EntryView; +class EntrySearcher; class Group; class GroupView; class KeePass1OpenWidget; @@ -246,8 +247,8 @@ private slots: QString m_databaseFileName; // Search state + EntrySearcher* m_EntrySearcher; QString m_lastSearchText; - bool m_searchCaseSensitive; bool m_searchLimitGroup; // CSV import state diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 96bd05a5b3..ba8b616d87 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -137,7 +137,7 @@ void SearchWidget::startSearchTimer() if (!m_searchTimer->isActive()) { m_searchTimer->stop(); } - m_searchTimer->start(100); + m_searchTimer->start(300); } void SearchWidget::startSearch() diff --git a/src/gui/SearchWidget.ui b/src/gui/SearchWidget.ui index 438a242c78..19b274a88e 100644 --- a/src/gui/SearchWidget.ui +++ b/src/gui/SearchWidget.ui @@ -34,6 +34,9 @@ Qt::Horizontal + + QSizePolicy::Minimum + 30 @@ -44,6 +47,18 @@ + + + 0 + 0 + + + + + 0 + 0 + + padding:3px diff --git a/src/gui/entry/EntryView.cpp b/src/gui/entry/EntryView.cpp index 28d5ec2bbd..64eca5ee3d 100644 --- a/src/gui/entry/EntryView.cpp +++ b/src/gui/entry/EntryView.cpp @@ -58,9 +58,9 @@ EntryView::EntryView(QWidget* parent) m_headerMenu->setTitle(tr("Customize View")); m_headerMenu->addSection(tr("Customize View")); - m_hideUsernamesAction = m_headerMenu->addAction(tr("Hide Usernames"), m_model, SLOT(setUsernamesHidden(bool))); + m_hideUsernamesAction = m_headerMenu->addAction(tr("Hide Usernames"), this, SLOT(setUsernamesHidden(bool))); m_hideUsernamesAction->setCheckable(true); - m_hidePasswordsAction = m_headerMenu->addAction(tr("Hide Passwords"), m_model, SLOT(setPasswordsHidden(bool))); + m_hidePasswordsAction = m_headerMenu->addAction(tr("Hide Passwords"), this, SLOT(setPasswordsHidden(bool))); m_hidePasswordsAction->setCheckable(true); m_headerMenu->addSeparator(); diff --git a/src/gui/entry/EntryView.h b/src/gui/entry/EntryView.h index fa002c717e..7666995991 100644 --- a/src/gui/entry/EntryView.h +++ b/src/gui/entry/EntryView.h @@ -43,9 +43,7 @@ class EntryView : public QTreeView int numberOfSelectedEntries(); void setFirstEntryActive(); bool isUsernamesHidden() const; - void setUsernamesHidden(bool hide); bool isPasswordsHidden() const; - void setPasswordsHidden(bool hide); QByteArray viewState() const; bool setViewState(const QByteArray& state); @@ -57,6 +55,10 @@ class EntryView : public QTreeView void entrySelectionChanged(); void viewStateChanged(); +public slots: + void setUsernamesHidden(bool hide); + void setPasswordsHidden(bool hide); + protected: void keyPressEvent(QKeyEvent* event) override; void focusInEvent(QFocusEvent* event) override; diff --git a/tests/TestEntrySearcher.cpp b/tests/TestEntrySearcher.cpp index 0c0a2c3e45..51be468b55 100644 --- a/tests/TestEntrySearcher.cpp +++ b/tests/TestEntrySearcher.cpp @@ -22,23 +22,32 @@ QTEST_GUILESS_MAIN(TestEntrySearcher) void TestEntrySearcher::initTestCase() { - m_groupRoot = new Group(); + m_rootGroup = new Group(); } void TestEntrySearcher::cleanupTestCase() { - delete m_groupRoot; + delete m_rootGroup; } void TestEntrySearcher::testSearch() { + /** + * Root + * - group1 (search disabled) + * - group11 + * - group2 + * - group21 + * - group211 + * - group2111 + */ Group* group1 = new Group(); Group* group2 = new Group(); Group* group3 = new Group(); - group1->setParent(m_groupRoot); - group2->setParent(m_groupRoot); - group3->setParent(m_groupRoot); + group1->setParent(m_rootGroup); + group2->setParent(m_rootGroup); + group3->setParent(m_rootGroup); Group* group11 = new Group(); @@ -55,15 +64,15 @@ void TestEntrySearcher::testSearch() group1->setSearchingEnabled(Group::Disable); Entry* eRoot = new Entry(); - eRoot->setNotes("test search term test"); - eRoot->setGroup(m_groupRoot); + eRoot->setTitle("test search term test"); + eRoot->setGroup(m_rootGroup); Entry* eRoot2 = new Entry(); eRoot2->setNotes("test term test"); - eRoot2->setGroup(m_groupRoot); + eRoot2->setGroup(m_rootGroup); Entry* e1 = new Entry(); - e1->setNotes("test search term test"); + e1->setUsername("test search term test"); e1->setGroup(group1); Entry* e11 = new Entry(); @@ -71,29 +80,37 @@ void TestEntrySearcher::testSearch() e11->setGroup(group11); Entry* e2111 = new Entry(); - e2111->setNotes("test search term test"); + e2111->setTitle("test search term test"); e2111->setGroup(group2111); Entry* e2111b = new Entry(); e2111b->setNotes("test search test"); + e2111b->setPassword("testpass"); e2111b->setGroup(group2111); Entry* e3 = new Entry(); - e3->setNotes("test search term test"); + e3->setUrl("test search term test"); e3->setGroup(group3); Entry* e3b = new Entry(); - e3b->setNotes("test search test"); + e3b->setTitle("test search test"); + e3b->setPassword("realpass"); e3b->setGroup(group3); - m_searchResult = m_entrySearcher.search("search term", m_groupRoot, Qt::CaseInsensitive); - QCOMPARE(m_searchResult.count(), 2); + m_searchResult = m_entrySearcher.search("search", m_rootGroup); + QCOMPARE(m_searchResult.count(), 5); - m_searchResult = m_entrySearcher.search("search term", group211, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("search term", m_rootGroup); + QCOMPARE(m_searchResult.count(), 3); + + m_searchResult = m_entrySearcher.search("search term", group211); + QCOMPARE(m_searchResult.count(), 1); + + m_searchResult = m_entrySearcher.search("password:testpass", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); - // Parent group disabled search - m_searchResult = m_entrySearcher.search("search term", group11, Qt::CaseInsensitive); + // Parent group has search disabled + m_searchResult = m_entrySearcher.search("search term", group11); QCOMPARE(m_searchResult.count(), 0); } @@ -102,38 +119,74 @@ void TestEntrySearcher::testAndConcatenationInSearch() Entry* entry = new Entry(); entry->setNotes("abc def ghi"); entry->setTitle("jkl"); - entry->setGroup(m_groupRoot); + entry->setGroup(m_rootGroup); - m_searchResult = m_entrySearcher.search("", m_groupRoot, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); - m_searchResult = m_entrySearcher.search("def", m_groupRoot, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("def", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); - m_searchResult = m_entrySearcher.search(" abc ghi ", m_groupRoot, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search(" abc ghi ", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); - m_searchResult = m_entrySearcher.search("ghi ef", m_groupRoot, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("ghi ef", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); - m_searchResult = m_entrySearcher.search("abc ef xyz", m_groupRoot, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("abc ef xyz", m_rootGroup); QCOMPARE(m_searchResult.count(), 0); - m_searchResult = m_entrySearcher.search("abc kl", m_groupRoot, Qt::CaseInsensitive); + m_searchResult = m_entrySearcher.search("abc kl", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); } void TestEntrySearcher::testAllAttributesAreSearched() { Entry* entry = new Entry(); - entry->setGroup(m_groupRoot); + entry->setGroup(m_rootGroup); entry->setTitle("testTitle"); entry->setUsername("testUsername"); entry->setUrl("testUrl"); entry->setNotes("testNote"); - m_searchResult = - m_entrySearcher.search("testTitle testUsername testUrl testNote", m_groupRoot, Qt::CaseInsensitive); + // Default is to AND all terms together + m_searchResult = m_entrySearcher.search("testTitle testUsername testUrl testNote", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); } + +void TestEntrySearcher::testSearchTermParser() +{ + // Test standard search terms + auto terms = m_entrySearcher.parseSearchTerms("-test \"quoted \\\"string\\\"\" user:user pass:\"test me\" noquote "); + + QCOMPARE(terms.length(), 5); + + QCOMPARE(terms[0]->field, EntrySearcher::Field::Undefined); + QCOMPARE(terms[0]->word, QString("test")); + QCOMPARE(terms[0]->exclude, true); + + QCOMPARE(terms[1]->field, EntrySearcher::Field::Undefined); + QCOMPARE(terms[1]->word, QString("quoted \\\"string\\\"")); + QCOMPARE(terms[1]->exclude, false); + + QCOMPARE(terms[2]->field, EntrySearcher::Field::Username); + QCOMPARE(terms[2]->word, QString("user")); + + QCOMPARE(terms[3]->field, EntrySearcher::Field::Password); + QCOMPARE(terms[3]->word, QString("test me")); + + QCOMPARE(terms[4]->field, EntrySearcher::Field::Undefined); + QCOMPARE(terms[4]->word, QString("noquote")); + + // Test wildcard and regex search terms + terms = m_entrySearcher.parseSearchTerms("+url:*.google.com *user:\\d+\\w{2}"); + + QCOMPARE(terms.length(), 2); + + QCOMPARE(terms[0]->field, EntrySearcher::Field::Url); + QCOMPARE(terms[0]->regex.pattern(), QString("^.*\\.google\\.com$")); + + QCOMPARE(terms[1]->field, EntrySearcher::Field::Username); + QCOMPARE(terms[1]->regex.pattern(), QString("\\d+\\w{2}")); +} diff --git a/tests/TestEntrySearcher.h b/tests/TestEntrySearcher.h index 17d486c22b..f385d618e0 100644 --- a/tests/TestEntrySearcher.h +++ b/tests/TestEntrySearcher.h @@ -34,9 +34,10 @@ private slots: void testAndConcatenationInSearch(); void testSearch(); void testAllAttributesAreSearched(); + void testSearchTermParser(); private: - Group* m_groupRoot; + Group* m_rootGroup; EntrySearcher m_entrySearcher; QList m_searchResult; }; diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index 9b04dd18e1..f48d0777cd 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -841,9 +841,10 @@ void TestGui::testSearch() // Ensure Down focuses on entry view when search text is selected QTest::keyClick(searchTextEdit, Qt::Key_Down); QTRY_VERIFY(entryView->hasFocus()); + QCOMPARE(entryView->selectionModel()->currentIndex().row(), 0); // Test that password copies (entry has focus) QClipboard* clipboard = QApplication::clipboard(); - QTest::keyClick(searchTextEdit, Qt::Key_C, Qt::ControlModifier); + QTest::keyClick(entryView, Qt::Key_C, Qt::ControlModifier); QModelIndex searchedItem = entryView->model()->index(0, 1); Entry* searchedEntry = entryView->entryFromIndex(searchedItem); QTRY_COMPARE(searchedEntry->password(), clipboard->text()); From d6ffee5e99905a3bf237fc554b5e9271097cc849 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Thu, 15 Nov 2018 17:37:16 -0500 Subject: [PATCH 4/6] Implement search auto-clear and goto group * Search clears if the search box does not have focus for 5 minutes (fixes #2178) * Goto group from search results after double clicking the group name (fixes #2043) --- src/gui/DatabaseWidget.cpp | 8 ++++++++ src/gui/SearchWidget.cpp | 10 +++++++++- src/gui/SearchWidget.h | 1 + 3 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index 701ba588c2..5c8d7bc9da 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -893,6 +893,14 @@ void DatabaseWidget::entryActivationSignalReceived(Entry* entry, EntryModel::Mod setupTotp(); } break; + case EntryModel::ParentGroup: + // Call this first to clear out of search mode, otherwise + // the desired entry is not properly selected + endSearch(); + emit clearSearch(); + m_groupView->setCurrentGroup(entry->group()); + m_entryView->setCurrentEntry(entry); + break; // TODO: switch to 'Notes' tab in details view/pane // case EntryModel::Notes: // break; diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index ba8b616d87..6441502b00 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -30,15 +30,18 @@ SearchWidget::SearchWidget(QWidget* parent) : QWidget(parent) , m_ui(new Ui::SearchWidget()) + , m_searchTimer(new QTimer(this)) + , m_clearSearchTimer(new QTimer(this)) { m_ui->setupUi(this); - m_searchTimer = new QTimer(this); m_searchTimer->setSingleShot(true); + m_clearSearchTimer->setSingleShot(true); connect(m_ui->searchEdit, SIGNAL(textChanged(QString)), SLOT(startSearchTimer())); connect(m_ui->clearIcon, SIGNAL(triggered(bool)), m_ui->searchEdit, SLOT(clear())); connect(m_searchTimer, SIGNAL(timeout()), this, SLOT(startSearch())); + connect(m_clearSearchTimer, SIGNAL(timeout()), m_ui->searchEdit, SLOT(clear())); connect(this, SIGNAL(escapePressed()), m_ui->searchEdit, SLOT(clear())); new QShortcut(QKeySequence::Find, this, SLOT(searchFocus()), nullptr, Qt::ApplicationShortcut); @@ -101,6 +104,11 @@ bool SearchWidget::eventFilter(QObject* obj, QEvent* event) return true; } } + } else if (event->type() == QEvent::FocusOut) { + // Auto-clear search after 5 minutes + m_clearSearchTimer->start(300000); + } else if (event->type() == QEvent::FocusIn) { + m_clearSearchTimer->stop(); } return QObject::eventFilter(obj, event); diff --git a/src/gui/SearchWidget.h b/src/gui/SearchWidget.h index 39e17bcf49..6f4387004a 100644 --- a/src/gui/SearchWidget.h +++ b/src/gui/SearchWidget.h @@ -69,6 +69,7 @@ private slots: private: const QScopedPointer m_ui; QTimer* m_searchTimer; + QTimer* m_clearSearchTimer; QAction* m_actionCaseSensitive; QAction* m_actionLimitGroup; }; From 880c3aeb349d2990ae3a297f06d79d79f25b4402 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Thu, 1 Nov 2018 21:33:27 -0400 Subject: [PATCH 5/6] Add search help pop-up * Support ! modifier (same as '-') * Create reusable PopupHelpWidget as self-contained popup that can be positioned around a parent widget and will follow the movement and sizing of the window * Eliminated KEEPASSXC_MAIN_WINDOW macro and replaced with getMainWindow() function * Add tests to cover search help show/hide --- .../application/16x16/actions/system-help.png | Bin 0 -> 897 bytes .../application/22x22/actions/system-help.png | Bin 0 -> 1222 bytes .../application/32x32/actions/system-help.png | Bin 0 -> 2120 bytes src/CMakeLists.txt | 3 +- src/browser/BrowserService.cpp | 4 +- src/core/EntrySearcher.cpp | 3 +- src/gui/Application.cpp | 11 - src/gui/Application.h | 4 - src/gui/MainWindow.cpp | 5 + src/gui/MainWindow.h | 10 +- src/gui/SearchHelpWidget.ui | 458 ++++++++++++++++++ src/gui/SearchWidget.cpp | 29 +- src/gui/SearchWidget.h | 5 + src/gui/SearchWidget.ui | 5 + src/gui/masterkey/KeyFileEditWidget.cpp | 4 +- src/gui/widgets/PopupHelpWidget.cpp | 99 ++++ src/gui/widgets/PopupHelpWidget.h | 48 ++ src/keys/YkChallengeResponseKey.cpp | 6 +- src/main.cpp | 1 - tests/gui/TestGui.cpp | 12 + 20 files changed, 671 insertions(+), 36 deletions(-) create mode 100644 share/icons/application/16x16/actions/system-help.png create mode 100644 share/icons/application/22x22/actions/system-help.png create mode 100644 share/icons/application/32x32/actions/system-help.png create mode 100644 src/gui/SearchHelpWidget.ui create mode 100644 src/gui/widgets/PopupHelpWidget.cpp create mode 100644 src/gui/widgets/PopupHelpWidget.h diff --git a/share/icons/application/16x16/actions/system-help.png b/share/icons/application/16x16/actions/system-help.png new file mode 100644 index 0000000000000000000000000000000000000000..75ebaf7f5f73f57df9cc93159479a672b49176e4 GIT binary patch literal 897 zcmV-{1AhF8P)ll^X4{Z>T-^z>G~pp+RTU*DH>_u zACN&16chTPK!1pS$uAKU{TieonFJR0O9X{qP?pLdjm$y~@e9tTuJ-NJx!t+Do?SZB z7rZ$h?mq9&`+1(vQL-%KAWcn8wAE^L7>&j%mSuB7q0k7=^Pikf=Nb+|4ECv~r>D{H z_iu`#C~s_R$fQtFBodLw#>T$3wY6FHexFRx-rlY*E-oIi+wEoHa2S(IK}>og;G=OI zH88MSIUF`<5s$|sE|<$$S6BBM1R6W!=H|XEDJdzN^Q@ue!xBbUInahI==5m_(CK*E z@59IMK9FRb!{KNn4J}E7uCA`C^78U){y-3q`u>2;$bqyO#0|Rvcdz84+I9#=eJTd$ z;~1Rb6VbWC!ov4S1Jb=w5Cpoz?T4Vt#=Kv|R8WPMcT;%M^9?7hnJ{xAyde=CE-zx+ z6!P=)&ye#lsX#E9OsBTCw&3=NSfjTQiwY28fIF(jJv$VJXV)>aA|Wo)7@Ozeu%&|I zxEdva#bU`Od&2_HAhac6JtkpOkU%4jTbGaHRo_nxEy)OOituj;?ema9)$a}avK*eJ>X4h59 zT0xdjR*(isk`j#v6oZwOm0^uWgWG4dV8jSWQ{-haXt!(3(4CCYD51X5t zrQ{@bc6L_O*4FluSjeh>ey#qW#2lTBOSUxJsWd|*#naQ%U#hFC&w;ROkTfYR zE$ucK3??$NP^na)X-C2g7Z-Vt$Ftnq+xwRMZ2bV1iB?Iiv@Zcg0H7vfccdYrhyK1{*`rB9wAjfeS6r z-haLReodR)NO|;|d~>+>96smVbH2Y*!8A?$A9COT34&k)P)c@_#Z=3Z$(t#MXkiIt z5U)5B)CHivySw|PmX;P@6{)18qzWWSlKy6mkB`p{4h~KMctDJ;eGmqx%}nyinwpx+ zv$L~z4Z|>ZcXv%$md$WDY_6@Xam&xi_dJHj0(gNeSXumFUFpf5o}RY8zP?VI&E|e8 zM=>Jw1Jm z_w&Z%5WfCx15IbDA?5%Hf|@CZBMNTc{}b=OR*W}Kd*E<5u3aP@~7uUZQkyRitj~T~gl5{1N$G@H!7^7A&sve6sXl?| z&K|z|c?P$ag?-(}kLG~2zP=t{mKD6oWlDL(iK9*+oo%w+CLkwEz_gr;sjwY+xehq# znPP>s>M9G9UmFUA76EL)3g+5(I-Pj$tP9UXgGuP2T;SOvz*{8Xn@ctL>f(!Ns0?W(G6I9ld09O86(uGvgZ_}*T6h(30nU7%LR~g$*H~piI zgs#C<6;MbJ-Bsbzws7@AB^ta195uPMwe^bM@Bfi+I&0j(F9<%x7fM%GS7(#AK&miTYt=@#&Tzj=0Y kmc;^DPgzT~hM58KFXXsKDcM2yZ~y=R07*qoM6N<$g81D##{d8T literal 0 HcmV?d00001 diff --git a/share/icons/application/32x32/actions/system-help.png b/share/icons/application/32x32/actions/system-help.png new file mode 100644 index 0000000000000000000000000000000000000000..8a9eb1a827fd0eaf1f5be508de78c32eed68a163 GIT binary patch literal 2120 zcmV-O2)Fl%P)MvdBPMi;O~2rGiiMI;q3v1v_PDPFi(2|^bZVYx3ZW`)A;0t*WZqJtMC zB_hky^Gx?+J~rLL^iR*sGv9vaJLi3$^WM(8-vBTsi1AHWIu z98VPy5%G3iUEK|*(>Wv*qQAc%eSLlC?d=@_&hgv8z`zjKb07Cp&X@_vPpwoc{Ypzq z^9Bb8osS+p!o!CTaqr$e+`fGqU0q#pI2`Ec=pgu;bNt3NTuU-e?x!42*6^f!LU3^K zvKBFfa`0q5Jw2cTyWI{kjppWN@M*Kz2>Be>a4q+6FXvk*k8&x0Gy>zf_|Ba>J;Icr zX}i0-L7`NTQ_+Y6=M31CT!NJ`I=rFQVMVkS!SRI%KUSImntDY0^x+8S9cO5P8Lpgu-qd&TU6j zh8Z!J1evTapt*Ddk?AHJyktgnb`w;8sl%I*S8=hZ4y~=NpbW~QOub&8M?I1&Mwp&a z%YdWbXgadbEHz`zi8}1R(usptZzDXb9p{TJ=>O_5V zH6rA+3Be!NV5#adEX{2o0m`K88)8u3FacIB77yk*(}(6_5N$nU!1}Xo*qq*tkiT|e z(>aHDZ-wg5wWw+A#Ld~ zu~<5(S0XTz7YfsaC6z1#4pqP}z72kf9q`q(K@r;mpBO8ijg|;}0dG|~TD$H;8VMI` z;I$VT%^yf0c(PLO;j24OvjUlg5%w+0;SwQLjYc8Qy9nz(w?x--p$hm)P!+ zz>EkZ;xcYR8b)&`Ccl@5r?+1d1au<_Y|O@lx6gu#DVsX(-n~0Wz3vu}2#jih=_&&Z z<}OHMO{@;z*-?zA-!GOEkXt}bKxP4149H!u{}KJhR^jf0Y*V_vFBT&*%8wuS+mV5TF&l zMqqs=RA)*?1fjuTaAkq44#?9mucir#C==$!+d%?KK|pc13Ccqa@I6!yKlM#4P*-Db zT0LHgt-`Zm<(Rpz3{!WPVDgS4Jo#S1zX^m~!MLBLA-BRjA`R2i(_M8yRtee3NQH>L z+I-3YWsC)Lj|f*BvEfIFRxC=kU~x(V0+VXd+0*B`2Y_FkG+<$DCHzGJQiSUk*)-#1c(479J|coG_6M?_Pz^yXVo;=AdG^`DJ&3+il1;O1g~5IJ-ia{h$#))R+-? zq6y1CvEp?>U|B*1?||W(aD9>vOX91rSW|@;)fMpBTY!n1Gmu%_K;I9aUNkyRl}hEh z4L$S&vg?>vsUXpeMWVj{;P6enoFa()t^rGuO?W9h7rRrncy)g^7Dkuh#h6Mw_hA`k z?bc%6&P-(L8yWc{u4A1}C+i11bVTgo^~QcT-ZEQ(Psey-7?S(u$Cmp3HB#j=iQl%9}1oV+EIOvvnrOs~bdgkmgE#|AzqvQ1Jo(tW zVmS$KZ|@g`&k7$rc;NgW+H&T*Ge1ASkaFb8N3(#Jz%&7c05`r76ciLCnn_w?Xe@tB znCC@BMJ=mWua4q=mmGyl?o*x!NGE^N^+(g!wQ;GyTLK~e{{A1S)#?JRR@))kx?T}r zkA!80_!JFBuQ+W-baZq9*Ks}falcEBQsBERKAs4;llhjwx5-#wvcL=%sRaV`=L&%! yfwcmw1YQ>i5bzh6E%1GTi8Q^tQg;IXSNsdZr>XzdpmVeU0000bringToFront(); + getMainWindow()->bringToFront(); m_bringToFrontRequested = true; } @@ -901,7 +901,7 @@ void BrowserService::databaseUnlocked(DatabaseWidget* dbWidget) { if (dbWidget) { if (m_bringToFrontRequested) { - KEEPASSXC_MAIN_WINDOW->lower(); + getMainWindow()->lower(); m_bringToFrontRequested = false; } emit databaseUnlocked(); diff --git a/src/core/EntrySearcher.cpp b/src/core/EntrySearcher.cpp index 6614ab4637..f582d62e7c 100644 --- a/src/core/EntrySearcher.cpp +++ b/src/core/EntrySearcher.cpp @@ -96,6 +96,7 @@ bool EntrySearcher::searchEntryImpl(const QString& searchString, Entry* entry) found = !attachments.filter(term->regex).empty(); break; default: + // Terms without a specific field try to match title, username, url, and notes found = term->regex.match(entry->resolvePlaceholder(entry->title())).hasMatch() || term->regex.match(entry->resolvePlaceholder(entry->username())).hasMatch() || term->regex.match(entry->resolvePlaceholder(entry->url())).hasMatch() || @@ -139,7 +140,7 @@ QList > EntrySearcher::parseSearchTerm term->regex = Tools::convertToRegex(term->word, !mods.contains("*"), mods.contains("+"), m_caseSensitive); // Exclude modifier - term->exclude = mods.contains("-"); + term->exclude = mods.contains("-") || mods.contains("!"); // Determine the field to search QString field = result.captured(2); diff --git a/src/gui/Application.cpp b/src/gui/Application.cpp index b67f542c63..03cd0e5515 100644 --- a/src/gui/Application.cpp +++ b/src/gui/Application.cpp @@ -49,7 +49,6 @@ namespace Application::Application(int& argc, char** argv) : QApplication(argc, argv) - , m_mainWindow(nullptr) #ifdef Q_OS_UNIX , m_unixSignalNotifier(nullptr) #endif @@ -143,16 +142,6 @@ Application::~Application() } } -QWidget* Application::mainWindow() const -{ - return m_mainWindow; -} - -void Application::setMainWindow(QWidget* mainWindow) -{ - m_mainWindow = mainWindow; -} - bool Application::event(QEvent* event) { // Handle Apple QFileOpenEvent from finder (double click on .kdbx file) diff --git a/src/gui/Application.h b/src/gui/Application.h index 7b1a77f607..9a3ef756b7 100644 --- a/src/gui/Application.h +++ b/src/gui/Application.h @@ -37,9 +37,7 @@ class Application : public QApplication public: Application(int& argc, char** argv); - QWidget* mainWindow() const; ~Application() override; - void setMainWindow(QWidget* mainWindow); bool event(QEvent* event) override; bool isAlreadyRunning() const; @@ -60,8 +58,6 @@ private slots: void socketReadyRead(); private: - QWidget* m_mainWindow; - #if defined(Q_OS_UNIX) /** * Register Unix signals such as SIGINT and SIGTERM for clean shutdown. diff --git a/src/gui/MainWindow.cpp b/src/gui/MainWindow.cpp index d9f9a0557d..67e4533925 100644 --- a/src/gui/MainWindow.cpp +++ b/src/gui/MainWindow.cpp @@ -109,12 +109,17 @@ class BrowserPlugin : public ISettingsPage const QString MainWindow::BaseWindowTitle = "KeePassXC"; +MainWindow* g_MainWindow = nullptr; +MainWindow* getMainWindow() { return g_MainWindow; } + MainWindow::MainWindow() : m_ui(new Ui::MainWindow()) , m_trayIcon(nullptr) , m_appExitCalled(false) , m_appExiting(false) { + g_MainWindow = this; + m_ui->setupUi(this); #if defined(Q_OS_UNIX) && !defined(Q_OS_MACOS) && !defined(QT_NO_DBUS) diff --git a/src/gui/MainWindow.h b/src/gui/MainWindow.h index caf2a0c58c..38b1c93081 100644 --- a/src/gui/MainWindow.h +++ b/src/gui/MainWindow.h @@ -148,8 +148,12 @@ private slots: bool m_appExiting; }; -#define KEEPASSXC_MAIN_WINDOW \ - (qobject_cast(qApp) ? qobject_cast(qobject_cast(qApp)->mainWindow()) \ - : nullptr) +/** + * Return instance of MainWindow created on app load + * non-gui instances will return nullptr + * + * @return MainWindow instance or nullptr + */ +MainWindow* getMainWindow(); #endif // KEEPASSX_MAINWINDOW_H diff --git a/src/gui/SearchHelpWidget.ui b/src/gui/SearchHelpWidget.ui new file mode 100644 index 0000000000..daa3a851e2 --- /dev/null +++ b/src/gui/SearchHelpWidget.ui @@ -0,0 +1,458 @@ + + + SearchHelpWidget + + + + 0 + 0 + 334 + 249 + + + + Search Help + + + false + + + #SearchHelpWidget { background-color: #ffffff } + + + QFrame::Box + + + QFrame::Plain + + + + 6 + + + QLayout::SetDefaultConstraint + + + 5 + + + 5 + + + 5 + + + 5 + + + + + + 0 + 0 + + + + + 75 + true + + + + Search terms are as follows: [modifiers][field:]["]term["] + + + + + + + + 0 + 0 + + + + + 75 + true + + + + Every search term must match (ie, logical AND) + + + + + + + + + + 50 + false + + + + Modifiers + + + + 8 + + + 8 + + + 9 + + + 10 + + + 9 + + + 9 + + + + + + 10 + 0 + + + + + 75 + true + + + + ! + + + false + + + Qt::AlignCenter + + + + + + + exclude term from results + + + + + + + match term exactly + + + + + + + + 10 + 0 + + + + + 75 + true + + + + * + + + Qt::AlignCenter + + + + + + + use regex in term + + + + + + + + 10 + 0 + + + + + 75 + true + + + + + + + + Qt::AlignCenter + + + + + + + + + + + 50 + false + + + + Fields + + + false + + + + 15 + + + 10 + + + 15 + + + 8 + + + 5 + + + + + username + + + + + + + password + + + + + + + title + + + + + + + url + + + + + + + notes + + + + + + + attribute + + + + + + + attachment + + + + + + + + + + + + + + Term Wildcards + + + + 8 + + + 8 + + + + + + 10 + 0 + + + + + 75 + true + + + + * + + + false + + + Qt::AlignCenter + + + + + + + + 10 + 0 + + + + + 75 + true + + + + ? + + + Qt::AlignCenter + + + + + + + + 10 + 0 + + + + + 75 + true + + + + | + + + Qt::AlignCenter + + + + + + + match anything + + + + + + + match one + + + + + + + logical OR + + + + + + + + + + Examples + + + + 8 + + + + + + 0 + 0 + + + + user:name1 url:google + + + + + + + + 0 + 0 + + + + user:"name1|name2" + + + + + + + + 0 + 0 + + + + +user:name1 *notes:"secret \d" + + + + + + + + + + + + + diff --git a/src/gui/SearchWidget.cpp b/src/gui/SearchWidget.cpp index 6441502b00..cde8995761 100644 --- a/src/gui/SearchWidget.cpp +++ b/src/gui/SearchWidget.cpp @@ -18,6 +18,7 @@ #include "SearchWidget.h" #include "ui_SearchWidget.h" +#include "ui_SearchHelpWidget.h" #include #include @@ -26,6 +27,7 @@ #include "core/Config.h" #include "core/FilePath.h" +#include "gui/widgets/PopupHelpWidget.h" SearchWidget::SearchWidget(QWidget* parent) : QWidget(parent) @@ -35,11 +37,17 @@ SearchWidget::SearchWidget(QWidget* parent) { m_ui->setupUi(this); + m_helpWidget = new PopupHelpWidget(m_ui->searchEdit); + m_helpWidget->setOffset(QPoint(0,1)); + Ui::SearchHelpWidget helpUi; + helpUi.setupUi(m_helpWidget); + m_searchTimer->setSingleShot(true); m_clearSearchTimer->setSingleShot(true); connect(m_ui->searchEdit, SIGNAL(textChanged(QString)), SLOT(startSearchTimer())); connect(m_ui->clearIcon, SIGNAL(triggered(bool)), m_ui->searchEdit, SLOT(clear())); + connect(m_ui->helpIcon, SIGNAL(triggered()), SLOT(toggleHelp())); connect(m_searchTimer, SIGNAL(timeout()), this, SLOT(startSearch())); connect(m_clearSearchTimer, SIGNAL(timeout()), m_ui->searchEdit, SLOT(clear())); connect(this, SIGNAL(escapePressed()), m_ui->searchEdit, SLOT(clear())); @@ -65,6 +73,9 @@ SearchWidget::SearchWidget(QWidget* parent) m_ui->searchIcon->setMenu(searchMenu); m_ui->searchEdit->addAction(m_ui->searchIcon, QLineEdit::LeadingPosition); + m_ui->helpIcon->setIcon(filePath()->icon("actions", "system-help")); + m_ui->searchEdit->addAction(m_ui->helpIcon, QLineEdit::TrailingPosition); + m_ui->clearIcon->setIcon(filePath()->icon("actions", "edit-clear-locationbar-rtl")); m_ui->clearIcon->setVisible(false); m_ui->searchEdit->addAction(m_ui->clearIcon, QLineEdit::TrailingPosition); @@ -86,13 +97,6 @@ bool SearchWidget::eventFilter(QObject* obj, QEvent* event) if (keyEvent->key() == Qt::Key_Escape) { emit escapePressed(); return true; - } else if (keyEvent->matches(QKeySequence::Copy)) { - // If Control+C is pressed in the search edit when no text - // is selected, copy the password of the current entry - if (!m_ui->searchEdit->hasSelectedText()) { - emit copyPressed(); - return true; - } } else if (keyEvent->matches(QKeySequence::MoveToNextLine)) { if (m_ui->searchEdit->cursorPosition() == m_ui->searchEdit->text().length()) { // If down is pressed at EOL, move the focus to the entry view @@ -111,7 +115,7 @@ bool SearchWidget::eventFilter(QObject* obj, QEvent* event) m_clearSearchTimer->stop(); } - return QObject::eventFilter(obj, event); + return QWidget::eventFilter(obj, event); } void SearchWidget::connectSignals(SignalMultiplexer& mx) @@ -188,3 +192,12 @@ void SearchWidget::searchFocus() m_ui->searchEdit->setFocus(); m_ui->searchEdit->selectAll(); } + +void SearchWidget::toggleHelp() +{ + if (m_helpWidget->isVisible()) { + m_helpWidget->hide(); + } else { + m_helpWidget->show(); + } +} diff --git a/src/gui/SearchWidget.h b/src/gui/SearchWidget.h index 6f4387004a..43dd764305 100644 --- a/src/gui/SearchWidget.h +++ b/src/gui/SearchWidget.h @@ -30,6 +30,8 @@ namespace Ui class SearchWidget; } +class PopupHelpWidget; + class SearchWidget : public QWidget { Q_OBJECT @@ -45,6 +47,7 @@ class SearchWidget : public QWidget void setLimitGroup(bool state); protected: + // Filter key presses in the search field bool eventFilter(QObject* obj, QEvent* event) override; signals: @@ -65,9 +68,11 @@ private slots: void updateCaseSensitive(); void updateLimitGroup(); void searchFocus(); + void toggleHelp(); private: const QScopedPointer m_ui; + PopupHelpWidget* m_helpWidget; QTimer* m_searchTimer; QTimer* m_clearSearchTimer; QAction* m_actionCaseSensitive; diff --git a/src/gui/SearchWidget.ui b/src/gui/SearchWidget.ui index 19b274a88e..93fbbdee51 100644 --- a/src/gui/SearchWidget.ui +++ b/src/gui/SearchWidget.ui @@ -81,6 +81,11 @@ Clear + + + Search Help + + searchEdit diff --git a/src/gui/masterkey/KeyFileEditWidget.cpp b/src/gui/masterkey/KeyFileEditWidget.cpp index 14ac4879ca..c694e2c5aa 100644 --- a/src/gui/masterkey/KeyFileEditWidget.cpp +++ b/src/gui/masterkey/KeyFileEditWidget.cpp @@ -45,7 +45,7 @@ bool KeyFileEditWidget::addToCompositeKey(QSharedPointer key) } if (fileKey->type() != FileKey::Hashed) { - QMessageBox::warning(KEEPASSXC_MAIN_WINDOW, + QMessageBox::warning(getMainWindow(), tr("Legacy key file format"), tr("You are using a legacy key file format which may become\n" "unsupported in the future.\n\n" @@ -100,7 +100,7 @@ void KeyFileEditWidget::createKeyFile() QString errorMsg; bool created = FileKey::create(fileName, &errorMsg); if (!created) { - MessageBox::critical(KEEPASSXC_MAIN_WINDOW, tr("Error creating key file"), + MessageBox::critical(getMainWindow(), tr("Error creating key file"), tr("Unable to create key file: %1").arg(errorMsg), QMessageBox::Button::Ok); } else { m_compUi->keyFileCombo->setEditText(fileName); diff --git a/src/gui/widgets/PopupHelpWidget.cpp b/src/gui/widgets/PopupHelpWidget.cpp new file mode 100644 index 0000000000..45e19e81ec --- /dev/null +++ b/src/gui/widgets/PopupHelpWidget.cpp @@ -0,0 +1,99 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "PopupHelpWidget.h" + +#include + +#include "gui/MainWindow.h" + +PopupHelpWidget::PopupHelpWidget(QWidget* parent) + : QFrame(parent) + , m_parentWindow(parent->window()) + , m_appWindow(getMainWindow()) + , m_offset({0, 0}) + , m_corner(Qt::BottomLeftCorner) +{ + Q_ASSERT(parent); + + setWindowFlags(Qt::FramelessWindowHint | Qt::Tool); + hide(); + + m_appWindow->installEventFilter(this); + parent->installEventFilter(this); +} + +PopupHelpWidget::~PopupHelpWidget() +{ + m_parentWindow->removeEventFilter(this); + parentWidget()->removeEventFilter(this); +} + +void PopupHelpWidget::setOffset(const QPoint& offset) +{ + m_offset = offset; + if (isVisible()) { + alignWithParent(); + } +} + +void PopupHelpWidget::setPosition(Qt::Corner corner) +{ + m_corner = corner; + if (isVisible()) { + alignWithParent(); + } +} + +bool PopupHelpWidget::eventFilter(QObject* obj, QEvent* event) +{ + if (obj == parentWidget() && event->type() == QEvent::FocusOut) { + hide(); + } else if (obj == m_appWindow && (event->type() == QEvent::Move || event->type() == QEvent::Resize)) { + if (isVisible()) { + alignWithParent(); + } + } + return QFrame::eventFilter(obj, event); +} + +void PopupHelpWidget::showEvent(QShowEvent* event) +{ + alignWithParent(); + QFrame::showEvent(event); +} + +void PopupHelpWidget::alignWithParent() +{ + QPoint pos; + switch (m_corner) { + case Qt::TopLeftCorner: + pos = parentWidget()->geometry().topLeft() + m_offset - QPoint(0, height()); + break; + case Qt::TopRightCorner: + pos = parentWidget()->geometry().topRight() + m_offset - QPoint(width(), height()); + break; + case Qt::BottomRightCorner: + pos = parentWidget()->geometry().bottomRight() + m_offset - QPoint(width(), 0); + break; + default: + pos = parentWidget()->geometry().bottomLeft() + m_offset; + break; + } + + move(m_parentWindow->mapToGlobal(pos)); +} \ No newline at end of file diff --git a/src/gui/widgets/PopupHelpWidget.h b/src/gui/widgets/PopupHelpWidget.h new file mode 100644 index 0000000000..66dac2b306 --- /dev/null +++ b/src/gui/widgets/PopupHelpWidget.h @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2018 KeePassXC Team + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 2 or (at your option) + * version 3 of the License. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#ifndef KEEPASSXC_POPUPHELPWIDGET_H +#define KEEPASSXC_POPUPHELPWIDGET_H + +#include +#include + +class PopupHelpWidget : public QFrame +{ + Q_OBJECT +public: + explicit PopupHelpWidget(QWidget* parent); + ~PopupHelpWidget() override; + + void setOffset(const QPoint& offset); + void setPosition(Qt::Corner corner); + +protected: + bool eventFilter(QObject* obj, QEvent* event) override; + void showEvent(QShowEvent* event) override; + +private: + void alignWithParent(); + QPointer m_parentWindow; + QPointer m_appWindow; + + QPoint m_offset; + Qt::Corner m_corner; +}; + + +#endif //KEEPASSXC_POPUPHELPWIDGET_H diff --git a/src/keys/YkChallengeResponseKey.cpp b/src/keys/YkChallengeResponseKey.cpp index b2a40bd234..ade1b63248 100644 --- a/src/keys/YkChallengeResponseKey.cpp +++ b/src/keys/YkChallengeResponseKey.cpp @@ -37,9 +37,9 @@ YkChallengeResponseKey::YkChallengeResponseKey(int slot, bool blocking) , m_slot(slot) , m_blocking(blocking) { - if (KEEPASSXC_MAIN_WINDOW) { - connect(this, SIGNAL(userInteractionRequired()), KEEPASSXC_MAIN_WINDOW, SLOT(showYubiKeyPopup())); - connect(this, SIGNAL(userConfirmed()), KEEPASSXC_MAIN_WINDOW, SLOT(hideYubiKeyPopup())); + if (getMainWindow()) { + connect(this, SIGNAL(userInteractionRequired()), getMainWindow(), SLOT(showYubiKeyPopup())); + connect(this, SIGNAL(userConfirmed()), getMainWindow(), SLOT(hideYubiKeyPopup())); } } diff --git a/src/main.cpp b/src/main.cpp index 9764c52d16..c811fe62c4 100644 --- a/src/main.cpp +++ b/src/main.cpp @@ -111,7 +111,6 @@ int main(int argc, char** argv) } MainWindow mainWindow; - app.setMainWindow(&mainWindow); QObject::connect(&app, SIGNAL(anotherInstanceStarted()), &mainWindow, SLOT(bringToFront())); QObject::connect(&app, SIGNAL(applicationActivated()), &mainWindow, SLOT(bringToFront())); QObject::connect(&app, SIGNAL(openFile(QString)), &mainWindow, SLOT(openDatabase(QString))); diff --git a/tests/gui/TestGui.cpp b/tests/gui/TestGui.cpp index f48d0777cd..0adeabd955 100644 --- a/tests/gui/TestGui.cpp +++ b/tests/gui/TestGui.cpp @@ -802,10 +802,22 @@ void TestGui::testSearch() auto* clearButton = searchWidget->findChild("clearIcon"); QVERIFY(!clearButton->isVisible()); + auto* helpButton = searchWidget->findChild("helpIcon"); + auto* helpPanel = searchWidget->findChild("SearchHelpWidget"); + QVERIFY(helpButton->isVisible()); + QVERIFY(!helpPanel->isVisible()); + // Enter search QTest::mouseClick(searchTextEdit, Qt::LeftButton); QTRY_VERIFY(searchTextEdit->hasFocus()); QTRY_VERIFY(!clearButton->isVisible()); + // Show/Hide search help + helpButton->trigger(); + QTRY_VERIFY(helpPanel->isVisible()); + QTest::mouseClick(searchTextEdit, Qt::LeftButton); + QTRY_VERIFY(helpPanel->isVisible()); + helpButton->trigger(); + QTRY_VERIFY(!helpPanel->isVisible()); // Search for "ZZZ" QTest::keyClicks(searchTextEdit, "ZZZ"); QTRY_COMPARE(searchTextEdit->text(), QString("ZZZ")); From 340076974e05ee58ea43635d09f110dcb987caa5 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Sat, 17 Nov 2018 09:55:57 -0500 Subject: [PATCH 6/6] Correct logic error in EntrySearcher and add more tests --- src/core/EntrySearcher.cpp | 4 ++-- tests/TestEntrySearcher.cpp | 23 +++++++++++++++++++++-- tests/TestEntrySearcher.h | 4 ++-- 3 files changed, 25 insertions(+), 6 deletions(-) diff --git a/src/core/EntrySearcher.cpp b/src/core/EntrySearcher.cpp index f582d62e7c..82e6eaa0c5 100644 --- a/src/core/EntrySearcher.cpp +++ b/src/core/EntrySearcher.cpp @@ -23,7 +23,7 @@ EntrySearcher::EntrySearcher(bool caseSensitive) : m_caseSensitive(caseSensitive) - , m_termParser(R"re(([-*+]+)?(?:(\w*):)?(?:(?=")"((?:[^"\\]|\\.)*)"|([^ ]*))( |$))re") + , m_termParser(R"re(([-!*+]+)?(?:(\w*):)?(?:(?=")"((?:[^"\\]|\\.)*)"|([^ ]*))( |$))re") // Group 1 = modifiers, Group 2 = field, Group 3 = quoted string, Group 4 = unquoted string { } @@ -104,7 +104,7 @@ bool EntrySearcher::searchEntryImpl(const QString& searchString, Entry* entry) } // Short circuit if we failed to match or we matched and are excluding this term - if (!found || term->exclude) { + if ((!found && !term->exclude) || (found && term->exclude)) { return false; } } diff --git a/tests/TestEntrySearcher.cpp b/tests/TestEntrySearcher.cpp index 51be468b55..eee9b91017 100644 --- a/tests/TestEntrySearcher.cpp +++ b/tests/TestEntrySearcher.cpp @@ -20,12 +20,12 @@ QTEST_GUILESS_MAIN(TestEntrySearcher) -void TestEntrySearcher::initTestCase() +void TestEntrySearcher::init() { m_rootGroup = new Group(); } -void TestEntrySearcher::cleanupTestCase() +void TestEntrySearcher::cleanup() { delete m_rootGroup; } @@ -71,6 +71,7 @@ void TestEntrySearcher::testSearch() eRoot2->setNotes("test term test"); eRoot2->setGroup(m_rootGroup); + // Searching is disabled for these Entry* e1 = new Entry(); e1->setUsername("test search term test"); e1->setGroup(group1); @@ -78,6 +79,7 @@ void TestEntrySearcher::testSearch() Entry* e11 = new Entry(); e11->setNotes("test search term test"); e11->setGroup(group11); + // End searching disabled Entry* e2111 = new Entry(); e2111->setTitle("test search term test"); @@ -85,6 +87,7 @@ void TestEntrySearcher::testSearch() Entry* e2111b = new Entry(); e2111b->setNotes("test search test"); + e2111b->setUsername("user123"); e2111b->setPassword("testpass"); e2111b->setGroup(group2111); @@ -94,9 +97,11 @@ void TestEntrySearcher::testSearch() Entry* e3b = new Entry(); e3b->setTitle("test search test"); + e3b->setUsername("test@email.com"); e3b->setPassword("realpass"); e3b->setGroup(group3); + // Simple search term testing m_searchResult = m_entrySearcher.search("search", m_rootGroup); QCOMPARE(m_searchResult.count(), 5); @@ -106,9 +111,23 @@ void TestEntrySearcher::testSearch() m_searchResult = m_entrySearcher.search("search term", group211); QCOMPARE(m_searchResult.count(), 1); + // Test advanced search terms m_searchResult = m_entrySearcher.search("password:testpass", m_rootGroup); QCOMPARE(m_searchResult.count(), 1); + m_searchResult = m_entrySearcher.search("!user:email.com", m_rootGroup); + QCOMPARE(m_searchResult.count(), 5); + + m_searchResult = m_entrySearcher.search("*user:\".*@.*\\.com\"", m_rootGroup); + QCOMPARE(m_searchResult.count(), 1); + + m_searchResult = m_entrySearcher.search("+user:email", m_rootGroup); + QCOMPARE(m_searchResult.count(), 0); + + // Terms are logical AND together + m_searchResult = m_entrySearcher.search("password:pass user:user", m_rootGroup); + QCOMPARE(m_searchResult.count(), 1); + // Parent group has search disabled m_searchResult = m_entrySearcher.search("search term", group11); QCOMPARE(m_searchResult.count(), 0); diff --git a/tests/TestEntrySearcher.h b/tests/TestEntrySearcher.h index f385d618e0..e10b1b544c 100644 --- a/tests/TestEntrySearcher.h +++ b/tests/TestEntrySearcher.h @@ -28,8 +28,8 @@ class TestEntrySearcher : public QObject Q_OBJECT private slots: - void initTestCase(); - void cleanupTestCase(); + void init(); + void cleanup(); void testAndConcatenationInSearch(); void testSearch();