Merge pull request #1797 from keepassxreboot/feature/better-search

Implement advanced search
This commit is contained in:
Jonathan White 2018-11-17 11:51:27 -05:00 committed by GitHub
commit 917c4cc18b
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
32 changed files with 1057 additions and 207 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 897 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -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

View File

@ -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();

View File

@ -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;
}

View File

@ -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

View File

@ -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

View File

@ -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)

View File

@ -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)

View 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.

View File

@ -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();
}

View File

@ -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

View File

@ -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)

View File

@ -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
View 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:][&quot;]term[&quot;]</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:&quot;name1|name2&quot;</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:&quot;secret \d&quot;</string>
</property>
</widget>
</item>
</layout>
</widget>
</item>
</layout>
</item>
</layout>
</widget>
<resources/>
<connections/>
</ui>

View File

@ -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();
}
}

View File

@ -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

View File

@ -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>

View File

@ -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

View File

@ -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);

View File

@ -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);

View File

@ -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();

View File

@ -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);

View 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));
}

View 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

View File

@ -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()));
}
}

View File

@ -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)));

View File

@ -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);

View File

@ -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}"));
}

View File

@ -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;
};

View File

@ -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());