mirror of
https://github.com/keepassxreboot/keepassxc.git
synced 2025-01-23 05:01:19 -05:00
Merge pull request #1797 from keepassxreboot/feature/better-search
Implement advanced search
This commit is contained in:
commit
917c4cc18b
BIN
share/icons/application/16x16/actions/system-help.png
Normal file
BIN
share/icons/application/16x16/actions/system-help.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 897 B |
BIN
share/icons/application/22x22/actions/system-help.png
Normal file
BIN
share/icons/application/22x22/actions/system-help.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 1.2 KiB |
BIN
share/icons/application/32x32/actions/system-help.png
Normal file
BIN
share/icons/application/32x32/actions/system-help.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 2.1 KiB |
@ -126,7 +126,6 @@ set(keepassx_SOURCES
|
||||
gui/UnlockDatabaseWidget.cpp
|
||||
gui/UnlockDatabaseDialog.cpp
|
||||
gui/WelcomeWidget.cpp
|
||||
gui/widgets/ElidedLabel.cpp
|
||||
gui/csvImport/CsvImportWidget.cpp
|
||||
gui/csvImport/CsvImportWizard.cpp
|
||||
gui/csvImport/CsvParserModel.cpp
|
||||
@ -154,6 +153,8 @@ set(keepassx_SOURCES
|
||||
gui/dbsettings/DatabaseSettingsWidgetEncryption.cpp
|
||||
gui/dbsettings/DatabaseSettingsWidgetMasterKey.cpp
|
||||
gui/settings/SettingsWidget.cpp
|
||||
gui/widgets/ElidedLabel.cpp
|
||||
gui/widgets/PopupHelpWidget.cpp
|
||||
gui/wizard/NewDatabaseWizard.cpp
|
||||
gui/wizard/NewDatabaseWizardPage.cpp
|
||||
gui/wizard/NewDatabaseWizardPageMetaData.cpp
|
||||
|
@ -88,7 +88,7 @@ bool BrowserService::openDatabase(bool triggerUnlock)
|
||||
}
|
||||
|
||||
if (triggerUnlock) {
|
||||
KEEPASSXC_MAIN_WINDOW->bringToFront();
|
||||
getMainWindow()->bringToFront();
|
||||
m_bringToFrontRequested = true;
|
||||
}
|
||||
|
||||
@ -390,7 +390,7 @@ QList<Entry*> 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();
|
||||
@ -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();
|
||||
|
@ -19,65 +19,92 @@
|
||||
#include "EntrySearcher.h"
|
||||
|
||||
#include "core/Group.h"
|
||||
#include "core/Tools.h"
|
||||
|
||||
QList<Entry*> 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
|
||||
{
|
||||
if (!group->resolveSearchingEnabled()) {
|
||||
return QList<Entry*>();
|
||||
}
|
||||
|
||||
return searchEntries(searchTerm, group, caseSensitivity);
|
||||
}
|
||||
|
||||
QList<Entry*>
|
||||
EntrySearcher::searchEntries(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity)
|
||||
QList<Entry*> EntrySearcher::search(const QString& searchString, const Group* baseGroup, bool forceSearch)
|
||||
{
|
||||
QList<Entry*> searchResult;
|
||||
Q_ASSERT(baseGroup);
|
||||
|
||||
const QList<Entry*>& entryList = group->entries();
|
||||
for (Entry* entry : entryList) {
|
||||
searchResult.append(matchEntry(searchTerm, entry, caseSensitivity));
|
||||
}
|
||||
|
||||
const QList<Group*>& 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));
|
||||
}
|
||||
QList<Entry*> results;
|
||||
for (const auto group : baseGroup->groupsRecursive(true)) {
|
||||
if (forceSearch || group->resolveSearchingEnabled()) {
|
||||
results.append(searchEntries(searchString, group->entries()));
|
||||
}
|
||||
}
|
||||
|
||||
return searchResult;
|
||||
return results;
|
||||
}
|
||||
|
||||
QList<Entry*> EntrySearcher::matchEntry(const QString& searchTerm, Entry* entry, Qt::CaseSensitivity caseSensitivity)
|
||||
QList<Entry*> EntrySearcher::searchEntries(const QString& searchString, const QList<Entry*>& entries)
|
||||
{
|
||||
const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts);
|
||||
for (const QString& word : wordList) {
|
||||
if (!wordMatch(word, entry, caseSensitivity)) {
|
||||
return QList<Entry*>();
|
||||
}
|
||||
QList<Entry*> results;
|
||||
for (Entry* entry : entries) {
|
||||
if (searchEntryImpl(searchString, entry)) {
|
||||
results.append(entry);
|
||||
}
|
||||
}
|
||||
|
||||
return QList<Entry*>() << entry;
|
||||
return results;
|
||||
}
|
||||
|
||||
bool EntrySearcher::wordMatch(const QString& word, Entry* entry, Qt::CaseSensitivity caseSensitivity)
|
||||
void EntrySearcher::setCaseSensitive(bool state)
|
||||
{
|
||||
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);
|
||||
m_caseSensitive = state;
|
||||
}
|
||||
|
||||
bool EntrySearcher::matchGroup(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity)
|
||||
bool EntrySearcher::isCaseSensitive()
|
||||
{
|
||||
const QStringList wordList = searchTerm.split(QRegExp("\\s"), QString::SkipEmptyParts);
|
||||
for (const QString& word : wordList) {
|
||||
if (!wordMatch(word, group, 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:
|
||||
// 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() ||
|
||||
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) || (found && term->exclude)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@ -85,7 +112,61 @@ bool EntrySearcher::matchGroup(const QString& searchTerm, const Group* group, Qt
|
||||
return true;
|
||||
}
|
||||
|
||||
bool EntrySearcher::wordMatch(const QString& word, const Group* group, Qt::CaseSensitivity caseSensitivity)
|
||||
QList<QSharedPointer<EntrySearcher::SearchTerm> > EntrySearcher::parseSearchTerms(const QString& searchString)
|
||||
{
|
||||
return group->name().contains(word, caseSensitivity) || group->notes().contains(word, caseSensitivity);
|
||||
auto terms = QList<QSharedPointer<SearchTerm> >();
|
||||
|
||||
auto results = m_termParser.globalMatch(searchString);
|
||||
while (results.hasNext()) {
|
||||
auto result = results.next();
|
||||
auto term = QSharedPointer<SearchTerm>::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("-") || 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;
|
||||
}
|
||||
|
@ -20,6 +20,7 @@
|
||||
#define KEEPASSX_ENTRYSEARCHER_H
|
||||
|
||||
#include <QString>
|
||||
#include <QRegularExpression>
|
||||
|
||||
class Group;
|
||||
class Entry;
|
||||
@ -27,14 +28,42 @@ class Entry;
|
||||
class EntrySearcher
|
||||
{
|
||||
public:
|
||||
QList<Entry*> search(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity);
|
||||
explicit EntrySearcher(bool caseSensitive = false);
|
||||
|
||||
QList<Entry*> search(const QString& searchString, const Group* baseGroup, bool forceSearch = false);
|
||||
QList<Entry*> searchEntries(const QString& searchString, const QList<Entry*>& entries);
|
||||
|
||||
void setCaseSensitive(bool state);
|
||||
bool isCaseSensitive();
|
||||
|
||||
private:
|
||||
QList<Entry*> searchEntries(const QString& searchTerm, const Group* group, Qt::CaseSensitivity caseSensitivity);
|
||||
QList<Entry*> 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);
|
||||
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<QSharedPointer<SearchTerm> > parseSearchTerms(const QString& searchString);
|
||||
|
||||
bool m_caseSensitive;
|
||||
QRegularExpression m_termParser;
|
||||
|
||||
friend class TestEntrySearcher;
|
||||
};
|
||||
|
||||
#endif // KEEPASSX_ENTRYSEARCHER_H
|
||||
|
@ -26,6 +26,8 @@
|
||||
#include <QImageReader>
|
||||
#include <QLocale>
|
||||
#include <QStringList>
|
||||
#include <QRegularExpression>
|
||||
|
||||
#include <QElapsedTimer>
|
||||
|
||||
#include <cctype>
|
||||
@ -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
|
||||
|
@ -27,6 +27,7 @@
|
||||
#include <algorithm>
|
||||
|
||||
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 <typename RandomAccessIterator, typename T>
|
||||
RandomAccessIterator binaryFind(RandomAccessIterator begin, RandomAccessIterator end, const T& value)
|
||||
|
@ -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)
|
||||
|
@ -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.
|
||||
|
@ -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
|
||||
@ -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
|
||||
@ -291,7 +292,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 +308,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);
|
||||
}
|
||||
@ -892,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;
|
||||
@ -1012,17 +1021,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<Entry*> searchResult = EntrySearcher().search(searchtext, searchGroup, caseSensitive);
|
||||
QList<Entry*> searchResult = m_EntrySearcher->search(searchtext, searchGroup);
|
||||
|
||||
m_entryView->setEntryList(searchResult);
|
||||
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 +1042,7 @@ void DatabaseWidget::search(const QString& searchtext)
|
||||
|
||||
void DatabaseWidget::setSearchCaseSensitive(bool state)
|
||||
{
|
||||
m_searchCaseSensitive = state;
|
||||
m_EntrySearcher->setCaseSensitive(state);
|
||||
refreshSearch();
|
||||
}
|
||||
|
||||
@ -1047,11 +1054,14 @@ 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);
|
||||
m_entryView->displayGroup(group);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1066,7 +1076,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();
|
||||
}
|
||||
|
@ -37,6 +37,7 @@ class EditEntryWidget;
|
||||
class EditGroupWidget;
|
||||
class Entry;
|
||||
class EntryView;
|
||||
class EntrySearcher;
|
||||
class Group;
|
||||
class GroupView;
|
||||
class KeePass1OpenWidget;
|
||||
@ -93,9 +94,9 @@ public:
|
||||
QList<int> previewSplitterSizes() const;
|
||||
void setPreviewSplitterSizes(const QList<int>& 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();
|
||||
@ -138,7 +139,7 @@ signals:
|
||||
void mainSplitterSizesChanged();
|
||||
void previewSplitterSizesChanged();
|
||||
void entryViewStateChanged();
|
||||
void updateSearch(QString text);
|
||||
void clearSearch();
|
||||
|
||||
public slots:
|
||||
void createEntry();
|
||||
@ -246,8 +247,8 @@ private:
|
||||
QString m_databaseFileName;
|
||||
|
||||
// Search state
|
||||
EntrySearcher* m_EntrySearcher;
|
||||
QString m_lastSearchText;
|
||||
bool m_searchCaseSensitive;
|
||||
bool m_searchLimitGroup;
|
||||
|
||||
// CSV import state
|
||||
|
@ -109,12 +109,17 @@ public:
|
||||
|
||||
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)
|
||||
|
@ -148,8 +148,12 @@ private:
|
||||
bool m_appExiting;
|
||||
};
|
||||
|
||||
#define KEEPASSXC_MAIN_WINDOW \
|
||||
(qobject_cast<Application*>(qApp) ? qobject_cast<MainWindow*>(qobject_cast<Application*>(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
|
||||
|
458
src/gui/SearchHelpWidget.ui
Normal file
458
src/gui/SearchHelpWidget.ui
Normal file
@ -0,0 +1,458 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<ui version="4.0">
|
||||
<class>SearchHelpWidget</class>
|
||||
<widget class="QFrame" name="SearchHelpWidget">
|
||||
<property name="geometry">
|
||||
<rect>
|
||||
<x>0</x>
|
||||
<y>0</y>
|
||||
<width>334</width>
|
||||
<height>249</height>
|
||||
</rect>
|
||||
</property>
|
||||
<property name="windowTitle">
|
||||
<string>Search Help</string>
|
||||
</property>
|
||||
<property name="autoFillBackground">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">#SearchHelpWidget { background-color: #ffffff }</string>
|
||||
</property>
|
||||
<property name="frameShape">
|
||||
<enum>QFrame::Box</enum>
|
||||
</property>
|
||||
<property name="frameShadow">
|
||||
<enum>QFrame::Plain</enum>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout">
|
||||
<property name="spacing">
|
||||
<number>6</number>
|
||||
</property>
|
||||
<property name="sizeConstraint">
|
||||
<enum>QLayout::SetDefaultConstraint</enum>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_2">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Search terms are as follows: [modifiers][field:]["]term["]</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_24">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string>Every search term must match (ie, logical AND)</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout_2" stretch="0,0">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_2">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Modifiers</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout">
|
||||
<property name="horizontalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="leftMargin">
|
||||
<number>9</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>9</number>
|
||||
</property>
|
||||
<property name="bottomMargin">
|
||||
<number>9</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_4">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">!</string>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="label_7">
|
||||
<property name="text">
|
||||
<string>exclude term from results</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="label_8">
|
||||
<property name="text">
|
||||
<string>match term exactly</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_16">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">*</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="label_15">
|
||||
<property name="text">
|
||||
<string>use regex in term</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_6">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">+</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox">
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>50</weight>
|
||||
<bold>false</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="title">
|
||||
<string>Fields</string>
|
||||
</property>
|
||||
<property name="flat">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<layout class="QGridLayout" name="gridLayout">
|
||||
<property name="leftMargin">
|
||||
<number>15</number>
|
||||
</property>
|
||||
<property name="topMargin">
|
||||
<number>10</number>
|
||||
</property>
|
||||
<property name="rightMargin">
|
||||
<number>15</number>
|
||||
</property>
|
||||
<property name="horizontalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>5</number>
|
||||
</property>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_3">
|
||||
<property name="text">
|
||||
<string notr="true">username</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_11">
|
||||
<property name="text">
|
||||
<string notr="true">password</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label">
|
||||
<property name="text">
|
||||
<string notr="true">title</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="3" column="0">
|
||||
<widget class="QLabel" name="label_13">
|
||||
<property name="text">
|
||||
<string notr="true">url</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="label_5">
|
||||
<property name="text">
|
||||
<string notr="true">notes</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="label_12">
|
||||
<property name="text">
|
||||
<string notr="true">attribute</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="label_14">
|
||||
<property name="text">
|
||||
<string notr="true">attachment</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
<item>
|
||||
<layout class="QHBoxLayout" name="horizontalLayout">
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_4">
|
||||
<property name="title">
|
||||
<string>Term Wildcards</string>
|
||||
</property>
|
||||
<layout class="QFormLayout" name="formLayout_2">
|
||||
<property name="horizontalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<property name="verticalSpacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<item row="0" column="0">
|
||||
<widget class="QLabel" name="label_18">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">*</string>
|
||||
</property>
|
||||
<property name="scaledContents">
|
||||
<bool>false</bool>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="0">
|
||||
<widget class="QLabel" name="label_19">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">?</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="0">
|
||||
<widget class="QLabel" name="label_20">
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>10</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="font">
|
||||
<font>
|
||||
<weight>75</weight>
|
||||
<bold>true</bold>
|
||||
</font>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">|</string>
|
||||
</property>
|
||||
<property name="alignment">
|
||||
<set>Qt::AlignCenter</set>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="0" column="1">
|
||||
<widget class="QLabel" name="label_21">
|
||||
<property name="text">
|
||||
<string>match anything</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="1" column="1">
|
||||
<widget class="QLabel" name="label_22">
|
||||
<property name="text">
|
||||
<string>match one</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item row="2" column="1">
|
||||
<widget class="QLabel" name="label_23">
|
||||
<property name="text">
|
||||
<string>logical OR</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QGroupBox" name="groupBox_3">
|
||||
<property name="title">
|
||||
<string>Examples</string>
|
||||
</property>
|
||||
<layout class="QVBoxLayout" name="verticalLayout_2">
|
||||
<property name="spacing">
|
||||
<number>8</number>
|
||||
</property>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_9">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">user:name1 url:google</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_10">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">user:"name1|name2"</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLabel" name="label_17">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Preferred" vsizetype="Preferred">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="text">
|
||||
<string notr="true">+user:name1 *notes:"secret \d"</string>
|
||||
</property>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
</item>
|
||||
</layout>
|
||||
</item>
|
||||
</layout>
|
||||
</widget>
|
||||
<resources/>
|
||||
<connections/>
|
||||
</ui>
|
@ -18,6 +18,7 @@
|
||||
|
||||
#include "SearchWidget.h"
|
||||
#include "ui_SearchWidget.h"
|
||||
#include "ui_SearchHelpWidget.h"
|
||||
|
||||
#include <QKeyEvent>
|
||||
#include <QMenu>
|
||||
@ -26,19 +27,29 @@
|
||||
|
||||
#include "core/Config.h"
|
||||
#include "core/FilePath.h"
|
||||
#include "gui/widgets/PopupHelpWidget.h"
|
||||
|
||||
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_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()));
|
||||
|
||||
new QShortcut(QKeySequence::Find, this, SLOT(searchFocus()), nullptr, Qt::ApplicationShortcut);
|
||||
@ -62,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);
|
||||
@ -83,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
|
||||
@ -101,9 +108,14 @@ 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);
|
||||
return QWidget::eventFilter(obj, event);
|
||||
}
|
||||
|
||||
void SearchWidget::connectSignals(SignalMultiplexer& mx)
|
||||
@ -113,6 +125,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()));
|
||||
}
|
||||
|
||||
@ -136,7 +149,7 @@ void SearchWidget::startSearchTimer()
|
||||
if (!m_searchTimer->isActive()) {
|
||||
m_searchTimer->stop();
|
||||
}
|
||||
m_searchTimer->start(100);
|
||||
m_searchTimer->start(300);
|
||||
}
|
||||
|
||||
void SearchWidget::startSearch()
|
||||
@ -179,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();
|
||||
}
|
||||
}
|
||||
|
@ -30,20 +30,25 @@ namespace Ui
|
||||
class SearchWidget;
|
||||
}
|
||||
|
||||
class PopupHelpWidget;
|
||||
|
||||
class SearchWidget : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
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);
|
||||
// Filter key presses in the search field
|
||||
bool eventFilter(QObject* obj, QEvent* event) override;
|
||||
|
||||
signals:
|
||||
void search(const QString& text);
|
||||
@ -63,14 +68,15 @@ private slots:
|
||||
void updateCaseSensitive();
|
||||
void updateLimitGroup();
|
||||
void searchFocus();
|
||||
void toggleHelp();
|
||||
|
||||
private:
|
||||
const QScopedPointer<Ui::SearchWidget> m_ui;
|
||||
PopupHelpWidget* m_helpWidget;
|
||||
QTimer* m_searchTimer;
|
||||
QTimer* m_clearSearchTimer;
|
||||
QAction* m_actionCaseSensitive;
|
||||
QAction* m_actionLimitGroup;
|
||||
|
||||
Q_DISABLE_COPY(SearchWidget)
|
||||
};
|
||||
|
||||
#endif // SEARCHWIDGET_H
|
||||
|
@ -34,6 +34,9 @@
|
||||
<property name="orientation">
|
||||
<enum>Qt::Horizontal</enum>
|
||||
</property>
|
||||
<property name="sizeType">
|
||||
<enum>QSizePolicy::Minimum</enum>
|
||||
</property>
|
||||
<property name="sizeHint" stdset="0">
|
||||
<size>
|
||||
<width>30</width>
|
||||
@ -44,6 +47,18 @@
|
||||
</item>
|
||||
<item>
|
||||
<widget class="QLineEdit" name="searchEdit">
|
||||
<property name="sizePolicy">
|
||||
<sizepolicy hsizetype="Expanding" vsizetype="Fixed">
|
||||
<horstretch>0</horstretch>
|
||||
<verstretch>0</verstretch>
|
||||
</sizepolicy>
|
||||
</property>
|
||||
<property name="minimumSize">
|
||||
<size>
|
||||
<width>0</width>
|
||||
<height>0</height>
|
||||
</size>
|
||||
</property>
|
||||
<property name="styleSheet">
|
||||
<string notr="true">padding:3px</string>
|
||||
</property>
|
||||
@ -66,6 +81,11 @@
|
||||
<string>Clear</string>
|
||||
</property>
|
||||
</action>
|
||||
<action name="helpIcon">
|
||||
<property name="text">
|
||||
<string>Search Help</string>
|
||||
</property>
|
||||
</action>
|
||||
</widget>
|
||||
<tabstops>
|
||||
<tabstop>searchEdit</tabstop>
|
||||
|
@ -72,10 +72,9 @@ void EntryModel::setGroup(Group* group)
|
||||
makeConnections(group);
|
||||
|
||||
endResetModel();
|
||||
emit switchedToListMode();
|
||||
}
|
||||
|
||||
void EntryModel::setEntryList(const QList<Entry*>& entries)
|
||||
void EntryModel::setEntries(const QList<Entry*>& entries)
|
||||
{
|
||||
beginResetModel();
|
||||
|
||||
@ -109,7 +108,6 @@ void EntryModel::setEntryList(const QList<Entry*>& entries)
|
||||
}
|
||||
|
||||
endResetModel();
|
||||
emit switchedToSearchMode();
|
||||
}
|
||||
|
||||
int EntryModel::rowCount(const QModelIndex& parent) const
|
||||
|
@ -60,22 +60,20 @@ public:
|
||||
QStringList mimeTypes() const override;
|
||||
QMimeData* mimeData(const QModelIndexList& indexes) const override;
|
||||
|
||||
void setEntryList(const QList<Entry*>& entries);
|
||||
void setPaperClipPixmap(const QPixmap& paperclip);
|
||||
void setGroup(Group* group);
|
||||
void setEntries(const QList<Entry*>& 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);
|
||||
|
@ -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()));
|
||||
|
||||
@ -62,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();
|
||||
|
||||
@ -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<Entry*>& entries)
|
||||
void EntryView::displaySearch(const QList<Entry*>& 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);
|
||||
|
@ -39,25 +39,26 @@ public:
|
||||
Entry* currentEntry();
|
||||
void setCurrentEntry(Entry* entry);
|
||||
Entry* entryFromIndex(const QModelIndex& index);
|
||||
void setEntryList(const QList<Entry*>& entries);
|
||||
bool inSearchMode();
|
||||
int numberOfSelectedEntries();
|
||||
void setFirstEntryActive();
|
||||
bool isUsernamesHidden() const;
|
||||
void setUsernamesHidden(const bool hide);
|
||||
bool isPasswordsHidden() const;
|
||||
void setPasswordsHidden(const bool hide);
|
||||
QByteArray viewState() const;
|
||||
bool setViewState(const QByteArray& state);
|
||||
|
||||
public slots:
|
||||
void setGroup(Group* group);
|
||||
void displayGroup(Group* group);
|
||||
void displaySearch(const QList<Entry*>& entries);
|
||||
|
||||
signals:
|
||||
void entryActivated(Entry* entry, EntryModel::ModelColumn column);
|
||||
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;
|
||||
@ -65,8 +66,6 @@ protected:
|
||||
|
||||
private slots:
|
||||
void emitEntryActivated(const QModelIndex& index);
|
||||
void switchToListMode();
|
||||
void switchToSearchMode();
|
||||
void showHeaderMenu(const QPoint& position);
|
||||
void toggleColumnVisibility(QAction* action);
|
||||
void fitColumnsToWindow();
|
||||
|
@ -45,7 +45,7 @@ bool KeyFileEditWidget::addToCompositeKey(QSharedPointer<CompositeKey> 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);
|
||||
|
99
src/gui/widgets/PopupHelpWidget.cpp
Normal file
99
src/gui/widgets/PopupHelpWidget.cpp
Normal file
@ -0,0 +1,99 @@
|
||||
/*
|
||||
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#include "PopupHelpWidget.h"
|
||||
|
||||
#include <QEvent>
|
||||
|
||||
#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));
|
||||
}
|
48
src/gui/widgets/PopupHelpWidget.h
Normal file
48
src/gui/widgets/PopupHelpWidget.h
Normal file
@ -0,0 +1,48 @@
|
||||
/*
|
||||
* Copyright (C) 2018 KeePassXC Team <team@keepassxc.org>
|
||||
*
|
||||
* 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 <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
|
||||
#ifndef KEEPASSXC_POPUPHELPWIDGET_H
|
||||
#define KEEPASSXC_POPUPHELPWIDGET_H
|
||||
|
||||
#include <QPointer>
|
||||
#include <QFrame>
|
||||
|
||||
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<QWidget> m_parentWindow;
|
||||
QPointer<QWidget> m_appWindow;
|
||||
|
||||
QPoint m_offset;
|
||||
Qt::Corner m_corner;
|
||||
};
|
||||
|
||||
|
||||
#endif //KEEPASSXC_POPUPHELPWIDGET_H
|
@ -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()));
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -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)));
|
||||
|
@ -307,7 +307,7 @@ void TestEntryModel::testProxyModel()
|
||||
|
||||
QList<Entry*> entryList;
|
||||
entryList << entry;
|
||||
modelSource->setEntryList(entryList);
|
||||
modelSource->setEntries(entryList);
|
||||
|
||||
/**
|
||||
* @author Fonic <https://github.com/fonic>
|
||||
@ -346,7 +346,7 @@ void TestEntryModel::testDatabaseDelete()
|
||||
Entry* entry2 = new Entry();
|
||||
entry2->setGroup(db2->rootGroup());
|
||||
|
||||
model->setEntryList(QList<Entry*>() << entry1 << entry2);
|
||||
model->setEntries(QList<Entry*>() << entry1 << entry2);
|
||||
|
||||
QCOMPARE(model->rowCount(), 2);
|
||||
|
||||
|
@ -20,25 +20,34 @@
|
||||
|
||||
QTEST_GUILESS_MAIN(TestEntrySearcher)
|
||||
|
||||
void TestEntrySearcher::initTestCase()
|
||||
void TestEntrySearcher::init()
|
||||
{
|
||||
m_groupRoot = new Group();
|
||||
m_rootGroup = new Group();
|
||||
}
|
||||
|
||||
void TestEntrySearcher::cleanupTestCase()
|
||||
void TestEntrySearcher::cleanup()
|
||||
{
|
||||
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();
|
||||
|
||||
@ -53,50 +62,74 @@ void TestEntrySearcher::testSearch()
|
||||
group2111->setParent(group211);
|
||||
|
||||
group1->setSearchingEnabled(Group::Disable);
|
||||
group11->setSearchingEnabled(Group::Enable);
|
||||
|
||||
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);
|
||||
|
||||
// Searching is disabled for these
|
||||
Entry* e1 = new Entry();
|
||||
e1->setNotes("test search term test");
|
||||
e1->setUsername("test search term test");
|
||||
e1->setGroup(group1);
|
||||
|
||||
Entry* e11 = new Entry();
|
||||
e11->setNotes("test search term test");
|
||||
e11->setGroup(group11);
|
||||
// End searching disabled
|
||||
|
||||
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->setUsername("user123");
|
||||
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->setUsername("test@email.com");
|
||||
e3b->setPassword("realpass");
|
||||
e3b->setGroup(group3);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("search term", m_groupRoot, Qt::CaseInsensitive);
|
||||
// Simple search term testing
|
||||
m_searchResult = m_entrySearcher.search("search", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 5);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("search term", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 3);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("search term", group211, Qt::CaseInsensitive);
|
||||
m_searchResult = m_entrySearcher.search("search term", group211);
|
||||
QCOMPARE(m_searchResult.count(), 1);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("search term", group11, Qt::CaseInsensitive);
|
||||
// Test advanced search terms
|
||||
m_searchResult = m_entrySearcher.search("password:testpass", m_rootGroup);
|
||||
QCOMPARE(m_searchResult.count(), 1);
|
||||
|
||||
m_searchResult = m_entrySearcher.search("search term", group1, Qt::CaseInsensitive);
|
||||
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);
|
||||
}
|
||||
|
||||
@ -105,38 +138,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}"));
|
||||
}
|
||||
|
@ -28,15 +28,16 @@ class TestEntrySearcher : public QObject
|
||||
Q_OBJECT
|
||||
|
||||
private slots:
|
||||
void initTestCase();
|
||||
void cleanupTestCase();
|
||||
void init();
|
||||
void cleanup();
|
||||
|
||||
void testAndConcatenationInSearch();
|
||||
void testSearch();
|
||||
void testAllAttributesAreSearched();
|
||||
void testSearchTermParser();
|
||||
|
||||
private:
|
||||
Group* m_groupRoot;
|
||||
Group* m_rootGroup;
|
||||
EntrySearcher m_entrySearcher;
|
||||
QList<Entry*> m_searchResult;
|
||||
};
|
||||
|
@ -802,10 +802,22 @@ void TestGui::testSearch()
|
||||
auto* clearButton = searchWidget->findChild<QAction*>("clearIcon");
|
||||
QVERIFY(!clearButton->isVisible());
|
||||
|
||||
auto* helpButton = searchWidget->findChild<QAction*>("helpIcon");
|
||||
auto* helpPanel = searchWidget->findChild<QWidget*>("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"));
|
||||
@ -841,9 +853,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());
|
||||
|
Loading…
Reference in New Issue
Block a user