Make search always visible (PR #67)

* Moved search bar to toolbar and consolidated search options into dropdown list
* Updated GUI tests to be atomic and rewrote search tests
* Searches are saved between databases
* Search is cleared when all databases are closed
* Implemented global search shortcut (CTRL+F) and a notification bar when in search mode
This commit is contained in:
Jonathan White 2016-11-02 21:01:02 -04:00 committed by GitHub
parent 3f80134f07
commit 13983d0e51
15 changed files with 512 additions and 417 deletions

View File

@ -61,7 +61,7 @@ macro(add_gcc_compiler_flags FLAGS)
add_gcc_compiler_cflags("${FLAGS}")
endmacro(add_gcc_compiler_flags)
add_definitions(-DQT_NO_KEYWORDS -DQT_NO_EXCEPTIONS -DQT_STRICT_ITERATORS -DQT_NO_CAST_TO_ASCII)
add_definitions(-DQT_NO_EXCEPTIONS -DQT_STRICT_ITERATORS -DQT_NO_CAST_TO_ASCII)
add_gcc_compiler_flags("-fno-common -fstack-protector --param=ssp-buffer-size=4")
add_gcc_compiler_flags("-Wall -Wextra -Wundef -Wpointer-arith -Wno-long-long")

View File

@ -102,6 +102,7 @@ set(keepassx_SOURCES
gui/PasswordGeneratorWidget.cpp
gui/PasswordComboBox.cpp
gui/SettingsWidget.cpp
gui/SearchWidget.cpp
gui/SortFilterHideProxyModel.cpp
gui/UnlockDatabaseWidget.cpp
gui/WelcomeWidget.cpp

View File

@ -187,6 +187,9 @@ void AutoType::performGlobalAutoType(const QList<Database*>& dbList)
QList<Entry*> entryList;
QHash<Entry*, QString> sequenceHash;
// TODO: Check if there are any active databases here, if not do nothing
// TODO: Check if all databases are locked, if so ask to unlock them
for (Database* db : dbList) {
const QList<Entry*> dbEntries = db->rootGroup()->entriesRecursive();
for (Entry* entry : dbEntries) {

View File

@ -16,7 +16,6 @@
*/
#include "DatabaseWidget.h"
#include "ui_SearchWidget.h"
#include <QAction>
#include <QDesktopServices>
@ -25,8 +24,10 @@
#include <QLineEdit>
#include <QKeyEvent>
#include <QSplitter>
#include <QTimer>
#include <QLabel>
#include <QProcess>
#include <QHeaderView>
#include <QApplication>
#include "autotype/AutoType.h"
#include "core/Config.h"
@ -50,24 +51,16 @@
DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
: QStackedWidget(parent)
, m_db(db)
, m_searchUi(new Ui::SearchWidget())
, m_searchWidget(new QWidget())
, m_newGroup(nullptr)
, m_newEntry(nullptr)
, m_newParent(nullptr)
{
m_searchUi->setupUi(m_searchWidget);
m_searchTimer = new QTimer(this);
m_searchTimer->setSingleShot(true);
m_mainWidget = new QWidget(this);
QLayout* layout = new QHBoxLayout(m_mainWidget);
m_splitter = new QSplitter(m_mainWidget);
m_splitter->setChildrenCollapsible(false);
QWidget* rightHandSideWidget = new QWidget(m_splitter);
m_searchWidget->setParent(rightHandSideWidget);
m_groupView = new GroupView(db, m_splitter);
m_groupView->setObjectName("groupView");
@ -82,25 +75,24 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
connect(m_entryView, SIGNAL(customContextMenuRequested(QPoint)),
SLOT(emitEntryContextMenuRequested(QPoint)));
QAction* closeAction = new QAction(m_searchWidget);
QIcon closeIcon = filePath()->icon("actions", "dialog-close");
closeAction->setIcon(closeIcon);
m_searchUi->closeSearchButton->setDefaultAction(closeAction);
m_searchUi->closeSearchButton->setShortcut(Qt::Key_Escape);
m_searchWidget->hide();
m_searchUi->caseSensitiveCheckBox->setVisible(false);
m_searchUi->searchEdit->installEventFilter(this);
// Add a notification for when we are searching
m_searchingLabel = new QLabel();
m_searchingLabel->setText(tr("Searching..."));
m_searchingLabel->setAlignment(Qt::AlignCenter);
m_searchingLabel->setStyleSheet("background-color: rgb(255, 253, 160);"
"border: 2px solid rgb(190, 190, 190);"
"border-radius: 5px;");
QVBoxLayout* vLayout = new QVBoxLayout(rightHandSideWidget);
vLayout->setMargin(0);
vLayout->addWidget(m_searchWidget);
vLayout->addWidget(m_searchingLabel);
vLayout->addWidget(m_entryView);
m_searchingLabel->setVisible(false);
rightHandSideWidget->setLayout(vLayout);
setTabOrder(m_searchUi->searchRootRadioButton, m_entryView);
setTabOrder(m_entryView, m_groupView);
setTabOrder(m_groupView, m_searchWidget);
m_splitter->addWidget(m_groupView);
m_splitter->addWidget(rightHandSideWidget);
@ -158,13 +150,9 @@ DatabaseWidget::DatabaseWidget(Database* db, QWidget* parent)
connect(m_keepass1OpenWidget, SIGNAL(editFinished(bool)), SLOT(openDatabase(bool)));
connect(m_unlockDatabaseWidget, SIGNAL(editFinished(bool)), SLOT(unlockDatabase(bool)));
connect(this, SIGNAL(currentChanged(int)), this, SLOT(emitCurrentModeChanged()));
connect(m_searchUi->searchEdit, SIGNAL(textChanged(QString)), this, SLOT(startSearchTimer()));
connect(m_searchUi->caseSensitiveCheckBox, SIGNAL(toggled(bool)), this, SLOT(startSearch()));
connect(m_searchUi->searchCurrentRadioButton, SIGNAL(toggled(bool)), this, SLOT(startSearch()));
connect(m_searchUi->searchRootRadioButton, SIGNAL(toggled(bool)), this, SLOT(startSearch()));
connect(m_searchUi->searchEdit, SIGNAL(returnPressed()), m_entryView, SLOT(setFocus()));
connect(m_searchTimer, SIGNAL(timeout()), this, SLOT(search()));
connect(closeAction, SIGNAL(triggered()), this, SLOT(closeSearch()));
m_searchCaseSensitive = false;
m_searchCurrentGroup = false;
setCurrentWidget(m_mainWidget);
}
@ -764,118 +752,82 @@ void DatabaseWidget::switchToImportKeepass1(const QString& fileName)
setCurrentWidget(m_keepass1OpenWidget);
}
void DatabaseWidget::openSearch()
void DatabaseWidget::search(const QString& searchtext)
{
if (isInSearchMode()) {
m_searchUi->searchEdit->selectAll();
if (!m_searchUi->searchEdit->hasFocus()) {
m_searchUi->searchEdit->setFocus();
// make sure the search action is checked again
emitCurrentModeChanged();
if (searchtext.isEmpty())
{
endSearch();
return;
}
Q_EMIT searchModeAboutToActivate();
if (!isInSearchMode())
{
m_lastGroup = m_groupView->currentGroup();
Q_ASSERT(m_lastGroup);
}
Group* searchGroup = m_searchCurrentGroup ? m_lastGroup : m_db->rootGroup();
Qt::CaseSensitivity sensitivity = m_searchCaseSensitive ? Qt::CaseSensitive : Qt::CaseInsensitive;
QList<Entry*> searchResult = EntrySearcher().search(searchtext, searchGroup, sensitivity);
m_entryView->setEntryList(searchResult);
m_lastSearchText = searchtext;
// Display a label detailing our search results
if (searchResult.size() > 0) {
m_searchingLabel->setText(tr("Search Results (%1)").arg(searchResult.size()));
}
else {
showSearch();
m_searchingLabel->setText(tr("No Results"));
}
m_searchingLabel->setVisible(true);
Q_EMIT searchModeActivated();
}
void DatabaseWidget::closeSearch()
void DatabaseWidget::setSearchCaseSensitive(bool state)
{
m_searchCaseSensitive = state;
if (isInSearchMode())
search(m_lastSearchText);
}
void DatabaseWidget::setSearchCurrentGroup(bool state)
{
m_searchCurrentGroup = state;
if (isInSearchMode())
search(m_lastSearchText);
}
QString DatabaseWidget::getCurrentSearch()
{
return m_lastSearchText;
}
void DatabaseWidget::endSearch()
{
if (isInSearchMode())
{
Q_ASSERT(m_lastGroup);
Q_EMIT listModeAboutToActivate();
m_groupView->setCurrentGroup(m_lastGroup);
m_searchTimer->stop();
m_entryView->setGroup(m_lastGroup);
Q_EMIT listModeActivated();
}
void DatabaseWidget::showSearch()
{
Q_EMIT searchModeAboutToActivate();
m_searchUi->searchEdit->blockSignals(true);
m_searchUi->searchEdit->clear();
m_searchUi->searchEdit->blockSignals(false);
m_searchUi->searchCurrentRadioButton->blockSignals(true);
m_searchUi->searchRootRadioButton->blockSignals(true);
m_searchUi->searchRootRadioButton->setChecked(true);
m_searchUi->searchCurrentRadioButton->blockSignals(false);
m_searchUi->searchRootRadioButton->blockSignals(false);
m_lastGroup = m_groupView->currentGroup();
Q_ASSERT(m_lastGroup);
if (m_lastGroup == m_db->rootGroup()) {
m_searchUi->optionsWidget->hide();
m_searchUi->searchCurrentRadioButton->hide();
m_searchUi->searchRootRadioButton->hide();
}
else {
m_searchUi->optionsWidget->show();
m_searchUi->searchCurrentRadioButton->show();
m_searchUi->searchRootRadioButton->show();
m_searchUi->searchCurrentRadioButton->setText(tr("Current group")
.append(" (")
.append(m_lastGroup->name())
.append(")"));
}
m_groupView->setCurrentIndex(QModelIndex());
m_searchWidget->show();
search();
m_searchUi->searchEdit->setFocus();
Q_EMIT searchModeActivated();
}
void DatabaseWidget::search()
{
Q_ASSERT(m_lastGroup);
Group* searchGroup;
if (m_searchUi->searchCurrentRadioButton->isChecked()) {
searchGroup = m_lastGroup;
}
else if (m_searchUi->searchRootRadioButton->isChecked()) {
searchGroup = m_db->rootGroup();
}
else {
Q_ASSERT(false);
return;
}
Qt::CaseSensitivity sensitivity;
if (m_searchUi->caseSensitiveCheckBox->isChecked()) {
sensitivity = Qt::CaseSensitive;
}
else {
sensitivity = Qt::CaseInsensitive;
}
m_searchingLabel->setVisible(false);
m_searchingLabel->setText(tr("Searching..."));
QList<Entry*> searchResult = EntrySearcher().search(m_searchUi->searchEdit->text(), searchGroup, sensitivity);
m_entryView->setEntryList(searchResult);
}
void DatabaseWidget::startSearchTimer()
{
if (!m_searchTimer->isActive()) {
m_searchTimer->stop();
}
m_searchTimer->start(100);
}
void DatabaseWidget::startSearch()
{
if (!m_searchTimer->isActive()) {
m_searchTimer->stop();
}
search();
m_lastSearchText.clear();
}
void DatabaseWidget::emitGroupContextMenuRequested(const QPoint& pos)
@ -908,16 +860,12 @@ void DatabaseWidget::clearLastGroup(Group* group)
{
if (group) {
m_lastGroup = nullptr;
m_searchWidget->hide();
}
}
void DatabaseWidget::lock()
{
Q_ASSERT(currentMode() != DatabaseWidget::LockedMode);
if (isInSearchMode()) {
closeSearch();
}
if (m_groupView->currentGroup()) {
m_groupBeforeLock = m_groupView->currentGroup()->uuid();
@ -1008,34 +956,3 @@ bool DatabaseWidget::currentEntryHasNotes()
}
return !currentEntry->notes().isEmpty();
}
bool DatabaseWidget::eventFilter(QObject* object, QEvent* event)
{
if (object == m_searchUi->searchEdit) {
if (event->type() == QEvent::KeyPress) {
QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
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.
Entry* currentEntry = m_entryView->currentEntry();
if (currentEntry && !m_searchUi->searchEdit->hasSelectedText()) {
setClipboardTextAndMinimize(currentEntry->password());
return true;
}
}
else if (keyEvent->matches(QKeySequence::MoveToNextLine)) {
// If Down is pressed at EOL in the search edit, move
// the focus to the entry view.
if (!m_searchUi->searchEdit->hasSelectedText()
&& m_searchUi->searchEdit->cursorPosition() == m_searchUi->searchEdit->text().size()) {
m_entryView->setFocus();
return true;
}
}
}
}
return false;
}

View File

@ -39,12 +39,9 @@ class KeePass1OpenWidget;
class QFile;
class QMenu;
class QSplitter;
class QLabel;
class UnlockDatabaseWidget;
namespace Ui {
class SearchWidget;
}
class DatabaseWidget : public QStackedWidget
{
Q_OBJECT
@ -64,6 +61,7 @@ public:
bool dbHasKey() const;
bool canDeleteCurrentGroup() const;
bool isInSearchMode() const;
QString getCurrentSearch();
int addWidget(QWidget* w);
void setCurrentIndex(int index);
void setCurrentWidget(QWidget* widget);
@ -101,9 +99,7 @@ Q_SIGNALS:
void searchModeActivated();
void splitterSizesChanged();
void entryColumnSizesChanged();
protected:
bool eventFilter(QObject* object, QEvent* event) override;
void updateSearch(QString text);
public Q_SLOTS:
void createEntry();
@ -127,7 +123,11 @@ public Q_SLOTS:
void switchToOpenDatabase(const QString& fileName);
void switchToOpenDatabase(const QString& fileName, const QString& password, const QString& keyFile);
void switchToImportKeepass1(const QString& fileName);
void openSearch();
// Search related slots
void search(const QString& searchtext);
void setSearchCaseSensitive(bool state);
void setSearchCurrentGroup(bool state);
void endSearch();
private Q_SLOTS:
void entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column);
@ -144,11 +144,6 @@ private Q_SLOTS:
void unlockDatabase(bool accepted);
void emitCurrentModeChanged();
void clearLastGroup(Group* group);
void search();
void startSearch();
void startSearchTimer();
void showSearch();
void closeSearch();
private:
void setClipboardTextAndMinimize(const QString& text);
@ -156,8 +151,6 @@ private:
void replaceDatabase(Database* db);
Database* m_db;
const QScopedPointer<Ui::SearchWidget> m_searchUi;
QWidget* const m_searchWidget;
QWidget* m_mainWidget;
EditEntryWidget* m_editEntryWidget;
EditEntryWidget* m_historyEditEntryWidget;
@ -170,13 +163,18 @@ private:
QSplitter* m_splitter;
GroupView* m_groupView;
EntryView* m_entryView;
QLabel* m_searchingLabel;
Group* m_newGroup;
Entry* m_newEntry;
Group* m_newParent;
Group* m_lastGroup;
QTimer* m_searchTimer;
QString m_filename;
Uuid m_groupBeforeLock;
// Search state
QString m_lastSearchText;
bool m_searchCaseSensitive;
bool m_searchCurrentGroup;
};
#endif // KEEPASSX_DATABASEWIDGET_H

View File

@ -48,16 +48,13 @@ void DatabaseWidgetStateSync::setActive(DatabaseWidget* dbWidget)
if (m_activeDbWidget) {
m_blockUpdates = true;
if (!m_splitterSizes.isEmpty()) {
if (!m_splitterSizes.isEmpty())
m_activeDbWidget->setSplitterSizes(m_splitterSizes);
}
if (m_activeDbWidget->isGroupSelected()) {
restoreListView();
}
else {
if (m_activeDbWidget->isInSearchMode())
restoreSearchView();
}
else
restoreListView();
m_blockUpdates = false;

View File

@ -33,13 +33,15 @@
#include "gui/DatabaseRepairWidget.h"
#include "gui/FileDialog.h"
#include "gui/MessageBox.h"
#include "gui/SearchWidget.h"
#include "http/Service.h"
#include "http/HttpSettings.h"
#include "http/OptionDialog.h"
#include "gui/SettingsWidget.h"
class HttpPlugin: public ISettingsPage {
class HttpPlugin: public ISettingsPage
{
public:
HttpPlugin(DatabaseTabWidget * tabWidget) {
m_service = new Service(tabWidget);
@ -68,7 +70,7 @@ class HttpPlugin: public ISettingsPage {
}
private:
Service *m_service;
};
};
const QString MainWindow::BaseWindowTitle = "KeePassX";
@ -80,6 +82,12 @@ MainWindow::MainWindow()
m_ui->setupUi(this);
// Setup the search widget in the toolbar
SearchWidget *search = new SearchWidget();
search->connectSignals(m_actionMultiplexer);
m_searchWidgetAction = m_ui->toolBar->addWidget(search);
m_searchWidgetAction->setEnabled(false);
m_countDefaultAttributes = m_ui->menuEntryCopyAttribute->actions().size();
restoreGeometry(config()->get("GUI/MainWindowGeometry").toByteArray());
@ -125,7 +133,6 @@ MainWindow::MainWindow()
setShortcut(m_ui->actionDatabaseClose, QKeySequence::Close, Qt::CTRL + Qt::Key_W);
m_ui->actionLockDatabases->setShortcut(Qt::CTRL + Qt::Key_L);
setShortcut(m_ui->actionQuit, QKeySequence::Quit, Qt::CTRL + Qt::Key_Q);
setShortcut(m_ui->actionSearch, QKeySequence::Find, Qt::CTRL + Qt::Key_F);
m_ui->actionEntryNew->setShortcut(Qt::CTRL + Qt::Key_N);
m_ui->actionEntryEdit->setShortcut(Qt::CTRL + Qt::Key_E);
m_ui->actionEntryDelete->setShortcut(Qt::CTRL + Qt::Key_D);
@ -164,8 +171,6 @@ MainWindow::MainWindow()
m_ui->actionAbout->setIcon(filePath()->icon("actions", "help-about"));
m_ui->actionSearch->setIcon(filePath()->icon("actions", "system-search"));
m_actionMultiplexer.connect(SIGNAL(currentModeChanged(DatabaseWidget::Mode)),
this, SLOT(setMenuActionState(DatabaseWidget::Mode)));
m_actionMultiplexer.connect(SIGNAL(groupChanged()),
@ -177,6 +182,10 @@ MainWindow::MainWindow()
m_actionMultiplexer.connect(SIGNAL(entryContextMenuRequested(QPoint)),
this, SLOT(showEntryContextMenu(QPoint)));
// Notify search when the active database changes
connect(m_ui->tabWidget, SIGNAL(activateDatabaseChanged(DatabaseWidget*)),
search, SLOT(databaseChanged(DatabaseWidget*)));
connect(m_ui->tabWidget, SIGNAL(tabNameChanged()),
SLOT(updateWindowTitle()));
connect(m_ui->tabWidget, SIGNAL(currentChanged(int)),
@ -253,9 +262,6 @@ MainWindow::MainWindow()
connect(m_ui->actionAbout, SIGNAL(triggered()), SLOT(showAboutDialog()));
m_actionMultiplexer.connect(m_ui->actionSearch, SIGNAL(triggered()),
SLOT(openSearch()));
updateTrayIcon();
}
@ -367,13 +373,13 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionGroupNew->setEnabled(groupSelected);
m_ui->actionGroupEdit->setEnabled(groupSelected);
m_ui->actionGroupDelete->setEnabled(groupSelected && dbWidget->canDeleteCurrentGroup());
// TODO: get checked state from db widget
m_ui->actionSearch->setEnabled(true);
m_ui->actionChangeMasterKey->setEnabled(true);
m_ui->actionChangeDatabaseSettings->setEnabled(true);
m_ui->actionDatabaseSave->setEnabled(true);
m_ui->actionDatabaseSaveAs->setEnabled(true);
m_ui->actionExportCsv->setEnabled(true);
m_searchWidgetAction->setEnabled(true);
break;
}
case DatabaseWidget::EditMode:
@ -394,12 +400,13 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionEntryCopyNotes->setEnabled(false);
m_ui->menuEntryCopyAttribute->setEnabled(false);
m_ui->actionSearch->setEnabled(false);
m_ui->actionChangeMasterKey->setEnabled(false);
m_ui->actionChangeDatabaseSettings->setEnabled(false);
m_ui->actionDatabaseSave->setEnabled(false);
m_ui->actionDatabaseSaveAs->setEnabled(false);
m_ui->actionExportCsv->setEnabled(false);
m_searchWidgetAction->setEnabled(false);
break;
}
default:
@ -424,14 +431,14 @@ void MainWindow::setMenuActionState(DatabaseWidget::Mode mode)
m_ui->actionEntryCopyNotes->setEnabled(false);
m_ui->menuEntryCopyAttribute->setEnabled(false);
m_ui->actionSearch->setEnabled(false);
m_ui->actionChangeMasterKey->setEnabled(false);
m_ui->actionChangeDatabaseSettings->setEnabled(false);
m_ui->actionDatabaseSave->setEnabled(false);
m_ui->actionDatabaseSaveAs->setEnabled(false);
m_ui->actionDatabaseClose->setEnabled(false);
m_ui->actionExportCsv->setEnabled(false);
m_searchWidgetAction->setEnabled(false);
}
bool inDatabaseTabWidgetOrWelcomeWidget = inDatabaseTabWidget || inWelcomeWidget;

View File

@ -84,6 +84,7 @@ private:
const QScopedPointer<Ui::MainWindow> m_ui;
SignalMultiplexer m_actionMultiplexer;
QAction* m_clearHistoryAction;
QAction* m_searchWidgetAction;
QActionGroup* m_lastDatabasesActions;
QActionGroup* m_copyAdditionalAttributeActions;
QStringList m_openDatabases;

View File

@ -97,7 +97,7 @@
<x>0</x>
<y>0</y>
<width>800</width>
<height>20</height>
<height>26</height>
</rect>
</property>
<widget class="QMenu" name="menuFile">
@ -106,7 +106,7 @@
</property>
<widget class="QMenu" name="menuRecentDatabases">
<property name="title">
<string>Recent databases</string>
<string>&amp;Recent databases</string>
</property>
</widget>
<addaction name="actionDatabaseNew"/>
@ -127,20 +127,20 @@
</widget>
<widget class="QMenu" name="menuHelp">
<property name="title">
<string>Help</string>
<string>He&amp;lp</string>
</property>
<addaction name="actionAbout"/>
</widget>
<widget class="QMenu" name="menuEntries">
<property name="title">
<string>Entries</string>
<string>E&amp;ntries</string>
</property>
<widget class="QMenu" name="menuEntryCopyAttribute">
<property name="enabled">
<bool>false</bool>
</property>
<property name="title">
<string>Copy attribute to clipboard</string>
<string>Copy att&amp;ribute to clipboard</string>
</property>
<addaction name="actionEntryCopyTitle"/>
<addaction name="actionEntryCopyURL"/>
@ -156,11 +156,10 @@
<addaction name="menuEntryCopyAttribute"/>
<addaction name="actionEntryAutoType"/>
<addaction name="actionEntryOpenUrl"/>
<addaction name="actionSearch"/>
</widget>
<widget class="QMenu" name="menuGroups">
<property name="title">
<string>Groups</string>
<string>&amp;Groups</string>
</property>
<addaction name="actionGroupNew"/>
<addaction name="actionGroupEdit"/>
@ -175,7 +174,7 @@
</widget>
<widget class="QMenu" name="menuView">
<property name="title">
<string>View</string>
<string>&amp;View</string>
</property>
</widget>
<addaction name="menuFile"/>
@ -206,21 +205,21 @@
<addaction name="actionEntryCopyPassword"/>
<addaction name="separator"/>
<addaction name="actionLockDatabases"/>
<addaction name="actionSearch"/>
<addaction name="separator"/>
</widget>
<action name="actionQuit">
<property name="text">
<string>Quit</string>
<string>&amp;Quit</string>
</property>
</action>
<action name="actionAbout">
<property name="text">
<string>About</string>
<string>&amp;About</string>
</property>
</action>
<action name="actionDatabaseOpen">
<property name="text">
<string>Open database</string>
<string>&amp;Open database</string>
</property>
</action>
<action name="actionDatabaseSave">
@ -228,7 +227,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Save database</string>
<string>&amp;Save database</string>
</property>
</action>
<action name="actionDatabaseClose">
@ -236,12 +235,12 @@
<bool>false</bool>
</property>
<property name="text">
<string>Close database</string>
<string>&amp;Close database</string>
</property>
</action>
<action name="actionDatabaseNew">
<property name="text">
<string>New database</string>
<string>&amp;New database</string>
</property>
</action>
<action name="actionEntryNew">
@ -249,7 +248,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Add new entry</string>
<string>&amp;Add new entry</string>
</property>
</action>
<action name="actionEntryEdit">
@ -257,7 +256,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>View/Edit entry</string>
<string>&amp;View/Edit entry</string>
</property>
</action>
<action name="actionEntryDelete">
@ -265,7 +264,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Delete entry</string>
<string>&amp;Delete entry</string>
</property>
</action>
<action name="actionGroupNew">
@ -273,7 +272,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Add new group</string>
<string>&amp;Add new group</string>
</property>
</action>
<action name="actionGroupEdit">
@ -281,7 +280,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Edit group</string>
<string>&amp;Edit group</string>
</property>
</action>
<action name="actionGroupDelete">
@ -289,7 +288,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Delete group</string>
<string>&amp;Delete group</string>
</property>
</action>
<action name="actionDatabaseSaveAs">
@ -297,7 +296,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Save database as</string>
<string>Sa&amp;ve database as</string>
</property>
</action>
<action name="actionChangeMasterKey">
@ -305,7 +304,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Change master key</string>
<string>Change &amp;master key</string>
</property>
</action>
<action name="actionChangeDatabaseSettings">
@ -313,7 +312,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Database settings</string>
<string>&amp;Database settings</string>
</property>
<property name="toolTip">
<string>Database settings</string>
@ -321,7 +320,7 @@
</action>
<action name="actionImportKeePass1">
<property name="text">
<string>Import KeePass 1 database</string>
<string>&amp;Import KeePass 1 database</string>
</property>
</action>
<action name="actionEntryClone">
@ -329,15 +328,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Clone entry</string>
</property>
</action>
<action name="actionSearch">
<property name="enabled">
<bool>false</bool>
</property>
<property name="text">
<string>Find</string>
<string>&amp;Clone entry</string>
</property>
</action>
<action name="actionEntryCopyUsername">
@ -345,7 +336,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Copy username</string>
<string>Copy &amp;username</string>
</property>
<property name="toolTip">
<string>Copy username to clipboard</string>
@ -356,7 +347,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Copy password</string>
<string>Cop&amp;y password</string>
</property>
<property name="toolTip">
<string>Copy password to clipboard</string>
@ -364,7 +355,7 @@
</action>
<action name="actionSettings">
<property name="text">
<string>Settings</string>
<string>&amp;Settings</string>
</property>
</action>
<action name="actionEntryAutoType">
@ -372,7 +363,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Perform Auto-Type</string>
<string>&amp;Perform Auto-Type</string>
</property>
</action>
<action name="actionEntryOpenUrl">
@ -380,7 +371,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Open URL</string>
<string>&amp;Open URL</string>
</property>
</action>
<action name="actionLockDatabases">
@ -388,7 +379,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Lock databases</string>
<string>&amp;Lock databases</string>
</property>
</action>
<action name="actionEntryCopyTitle">
@ -396,7 +387,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Title</string>
<string>&amp;Title</string>
</property>
</action>
<action name="actionEntryCopyURL">
@ -404,7 +395,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>URL</string>
<string>&amp;URL</string>
</property>
</action>
<action name="actionEntryCopyNotes">
@ -412,7 +403,7 @@
<bool>false</bool>
</property>
<property name="text">
<string>Notes</string>
<string>&amp;Notes</string>
</property>
</action>
<action name="actionExportCsv">
@ -420,12 +411,12 @@
<bool>false</bool>
</property>
<property name="text">
<string>Export to CSV file</string>
<string>&amp;Export to CSV file</string>
</property>
</action>
<action name="actionRepairDatabase">
<property name="text">
<string>Repair database</string>
<string>Re&amp;pair database</string>
</property>
</action>
</widget>

124
src/gui/SearchWidget.cpp Normal file
View File

@ -0,0 +1,124 @@
/*
* Copyright (C) 2016 Jonathan White <support@dmapps.us>
*
* 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 "SearchWidget.h"
#include "ui_SearchWidget.h"
#include <QKeyEvent>
#include <QMenu>
#include <QShortcut>
#include "core/FilePath.h"
bool SearchEventFilter::eventFilter(QObject *obj, QEvent *event)
{
if (event->type() == QEvent::KeyPress) {
QKeyEvent *keyEvent = static_cast<QKeyEvent*>(event);
if (keyEvent->key() == Qt::Key_Escape) {
emit escapePressed();
return true;
}
}
return QObject::eventFilter(obj, event);
}
SearchWidget::SearchWidget(QWidget *parent)
: QWidget(parent)
, m_ui(new Ui::SearchWidget())
{
m_ui->setupUi(this);
m_searchTimer = new QTimer(this);
m_searchTimer->setSingleShot(true);
connect(m_ui->searchEdit, SIGNAL(textChanged(QString)), SLOT(startSearchTimer()));
connect(m_ui->searchEdit, SIGNAL(returnPressed()), SLOT(startSearch()));
connect(m_ui->searchIcon, SIGNAL(triggered(QAction*)), m_ui->searchEdit, SLOT(setFocus()));
connect(m_searchTimer, SIGNAL(timeout()), this, SLOT(startSearch()));
connect(&m_searchEventFilter, SIGNAL(escapePressed()), m_ui->searchEdit, SLOT(clear()));
new QShortcut(Qt::CTRL + Qt::Key_F, m_ui->searchEdit, SLOT(setFocus()), nullptr, Qt::ApplicationShortcut);
m_ui->searchEdit->installEventFilter(&m_searchEventFilter);
QMenu *searchMenu = new QMenu();
m_actionCaseSensitive = searchMenu->addAction(tr("Case Sensitive"), this, SLOT(updateCaseSensitive()));
m_actionCaseSensitive->setCheckable(true);
m_actionGroupSearch = searchMenu->addAction(tr("Search Current Group"), this, SLOT(updateGroupSearch()));
m_actionGroupSearch->setCheckable(true);
m_ui->searchIcon->setIcon(filePath()->icon("actions", "system-search"));
m_ui->searchIcon->setMenu(searchMenu);
m_ui->searchIcon->setPopupMode(QToolButton::MenuButtonPopup);
}
SearchWidget::~SearchWidget()
{
}
void SearchWidget::connectSignals(SignalMultiplexer& mx)
{
mx.connect(this, SIGNAL(search(QString)), SLOT(search(QString)));
mx.connect(this, SIGNAL(setCaseSensitive(bool)), SLOT(setSearchCaseSensitive(bool)));
mx.connect(this, SIGNAL(setGroupSearch(bool)), SLOT(setSearchCurrentGroup(bool)));
mx.connect(SIGNAL(groupChanged()), m_ui->searchEdit, SLOT(clear()));
}
void SearchWidget::databaseChanged(DatabaseWidget *dbWidget)
{
if (dbWidget != nullptr) {
// Set current search text from this database
m_ui->searchEdit->setText(dbWidget->getCurrentSearch());
// Enforce search policy
emit setCaseSensitive(m_actionCaseSensitive->isChecked());
emit setGroupSearch(m_actionGroupSearch->isChecked());
} else {
m_ui->searchEdit->clear();
}
}
void SearchWidget::startSearchTimer()
{
if (!m_searchTimer->isActive()) {
m_searchTimer->stop();
}
m_searchTimer->start(100);
}
void SearchWidget::startSearch()
{
if (!m_searchTimer->isActive()) {
m_searchTimer->stop();
}
search(m_ui->searchEdit->text());
}
void SearchWidget::updateCaseSensitive()
{
emit setCaseSensitive(m_actionCaseSensitive->isChecked());
}
void SearchWidget::updateGroupSearch()
{
emit setGroupSearch(m_actionGroupSearch->isChecked());
}

77
src/gui/SearchWidget.h Normal file
View File

@ -0,0 +1,77 @@
/*
* Copyright (C) 2016 Jonathan White <support@dmapps.us>
*
* 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 KEEPASSX_SEARCHWIDGET_H
#define KEEPASSX_SEARCHWIDGET_H
#include <QWidget>
#include <QTimer>
#include "gui/DatabaseWidget.h"
#include "core/SignalMultiplexer.h"
namespace Ui {
class SearchWidget;
}
class SearchEventFilter : public QObject
{
Q_OBJECT
signals:
void escapePressed();
protected:
virtual bool eventFilter(QObject *obj, QEvent *event) override;
};
class SearchWidget : public QWidget
{
Q_OBJECT
public:
explicit SearchWidget(QWidget *parent = 0);
~SearchWidget();
void connectSignals(SignalMultiplexer& mx);
signals:
void search(const QString &text);
void setCaseSensitive(bool state);
void setGroupSearch(bool state);
public slots:
void databaseChanged(DatabaseWidget* dbWidget);
private slots:
void startSearchTimer();
void startSearch();
void updateCaseSensitive();
void updateGroupSearch();
private:
const QScopedPointer<Ui::SearchWidget> m_ui;
QTimer* m_searchTimer;
SearchEventFilter m_searchEventFilter;
QAction *m_actionCaseSensitive;
QAction *m_actionGroupSearch;
Q_DISABLE_COPY(SearchWidget)
};
#endif // SEARCHWIDGET_H

View File

@ -7,7 +7,7 @@
<x>0</x>
<y>0</y>
<width>630</width>
<height>87</height>
<height>34</height>
</rect>
</property>
<layout class="QGridLayout" name="gridLayout">
@ -23,16 +23,16 @@
<property name="bottomMargin">
<number>0</number>
</property>
<item row="0" column="1">
<widget class="LineEdit" name="searchEdit"/>
</item>
<item row="0" column="0">
<layout class="QHBoxLayout" name="horizontalLayout">
<item>
<widget class="QToolButton" name="closeSearchButton">
<widget class="QToolButton" name="searchIcon">
<property name="focusPolicy">
<enum>Qt::ClickFocus</enum>
</property>
<property name="toolButtonStyle">
<enum>Qt::ToolButtonIconOnly</enum>
</property>
<property name="autoRaise">
<bool>true</bool>
</property>
@ -47,79 +47,14 @@
</item>
</layout>
</item>
<item row="1" column="1">
<widget class="QWidget" name="optionsWidget" native="true">
<layout class="QHBoxLayout" name="horizontalLayout_2">
<property name="leftMargin">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<property name="rightMargin">
<number>0</number>
</property>
<property name="bottomMargin">
<number>0</number>
</property>
<item>
<widget class="QCheckBox" name="caseSensitiveCheckBox">
<property name="text">
<string>Case sensitive</string>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="searchCurrentRadioButton">
<property name="text">
<string>Current group</string>
</property>
<property name="checked">
<bool>false</bool>
</property>
</widget>
</item>
<item>
<widget class="QRadioButton" name="searchRootRadioButton">
<property name="text">
<string>Root group</string>
</property>
<property name="checked">
<bool>true</bool>
</property>
</widget>
</item>
<item>
<spacer name="horizontalSpacer">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="sizeHint" stdset="0">
<size>
<width>255</width>
<height>1</height>
</size>
</property>
</spacer>
<item row="0" column="1">
<widget class="QLineEdit" name="searchEdit"/>
</item>
</layout>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>LineEdit</class>
<extends>QLineEdit</extends>
<header>gui/LineEdit.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>closeSearchButton</tabstop>
<tabstop>searchIcon</tabstop>
<tabstop>searchEdit</tabstop>
<tabstop>caseSensitiveCheckBox</tabstop>
<tabstop>searchCurrentRadioButton</tabstop>
<tabstop>searchRootRadioButton</tabstop>
</tabstops>
<resources/>
<connections/>

View File

@ -136,6 +136,9 @@ void GroupView::syncExpandedState(const QModelIndex& parent, int start, int end)
void GroupView::setCurrentGroup(Group* group)
{
if (group == nullptr)
setCurrentIndex(QModelIndex());
else
setCurrentIndex(m_model->index(group));
}

View File

@ -28,6 +28,7 @@
#include <QTest>
#include <QToolBar>
#include <QToolButton>
#include <QTimer>
#include "config-keepassx-tests.h"
#include "core/Config.h"
@ -59,20 +60,25 @@ void TestGui::initTestCase()
m_mainWindow->activateWindow();
Tools::wait(50);
// Load the NewDatabase.kdbx file into temporary storage
QByteArray tmpData;
QFile sourceDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx"));
QVERIFY(sourceDbFile.open(QIODevice::ReadOnly));
QVERIFY(Tools::readAllFromDevice(&sourceDbFile, tmpData));
sourceDbFile.close();
QVERIFY(m_orgDbFile.open());
m_orgDbFileName = QFileInfo(m_orgDbFile.fileName()).fileName();
QCOMPARE(m_orgDbFile.write(tmpData), static_cast<qint64>((tmpData.size())));
m_orgDbFile.close();
// Write the temp storage to a temp database file for use in our tests
QVERIFY(m_dbFile.open());
QCOMPARE(m_dbFile.write(tmpData), static_cast<qint64>((tmpData.size())));
m_dbFile.close();
m_dbFileName = QFileInfo(m_dbFile).fileName();
}
void TestGui::testOpenDatabase()
// Every test starts with opening the temp database
void TestGui::init()
{
fileDialog()->setNextFileName(m_orgDbFile.fileName());
fileDialog()->setNextFileName(m_dbFile.fileName());
triggerAction("actionDatabaseOpen");
QWidget* databaseOpenWidget = m_mainWindow->findChild<QWidget*>("databaseOpenWidget");
@ -81,60 +87,91 @@ void TestGui::testOpenDatabase()
QTest::keyClicks(editPassword, "a");
QTest::keyClick(editPassword, Qt::Key_Enter);
}
Tools::wait(100);
void TestGui::testTabs()
{
QCOMPARE(m_tabWidget->count(), 1);
QCOMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), m_orgDbFileName);
QVERIFY(m_tabWidget->currentDatabaseWidget());
m_dbWidget = m_tabWidget->currentDatabaseWidget();
m_db = m_dbWidget->database();
}
// Every test ends with closing the temp database without saving
void TestGui::cleanup()
{
// DO NOT save the database
MessageBox::setNextAnswer(QMessageBox::No);
triggerAction("actionDatabaseClose");
Tools::wait(100);
m_db = nullptr;
m_dbWidget = nullptr;
}
void TestGui::testTabs()
{
QCOMPARE(m_tabWidget->count(), 1);
QCOMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), m_dbFileName);
}
void TestGui::testEditEntry()
{
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
QModelIndex item = entryView->model()->index(0, 1);
QRect itemRect = entryView->visualRect(item);
QTest::mouseClick(entryView->viewport(), Qt::LeftButton, Qt::NoModifier, itemRect.center());
QToolBar* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
// Select the first entry in the database
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
QModelIndex entryItem = entryView->model()->index(0, 1);
clickIndex(entryItem, entryView, Qt::LeftButton);
// Confirm the edit action button is enabled
QAction* entryEditAction = m_mainWindow->findChild<QAction*>("actionEntryEdit");
QVERIFY(entryEditAction->isEnabled());
QToolBar* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
QVERIFY(entryEditWidget->isVisible());
QVERIFY(entryEditWidget->isEnabled());
QTest::mouseClick(entryEditWidget, Qt::LeftButton);
// Edit the first entry ("Sample Entry")
QTest::mouseClick(entryEditWidget, Qt::LeftButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode);
EditEntryWidget* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
QVERIFY(m_dbWidget->currentWidget() == editEntryWidget);
QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
QTest::keyClicks(titleEdit, "_test");
// Save the edit
QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
QVERIFY(editEntryWidgetButtonBox);
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
// make sure the database isn't marked as modified
// wait for modified timer
QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), m_orgDbFileName);
// Confirm edit was made
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::ViewMode);
Entry* entry = entryView->entryFromIndex(entryItem);
QCOMPARE(entry->title(), QString("Sample Entry_test"));
QCOMPARE(entry->historyItems().size(), 1);
// Confirm modified indicator is showing
QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("%1*").arg(m_dbFileName));
}
void TestGui::testAddEntry()
{
QToolBar* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
// Find the new entry action
QAction* entryNewAction = m_mainWindow->findChild<QAction*>("actionEntryNew");
QVERIFY(entryNewAction->isEnabled());
QToolBar* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
// Find the button associated with the new entry action
QWidget* entryNewWidget = toolBar->widgetForAction(entryNewAction);
QVERIFY(entryNewWidget->isVisible());
QVERIFY(entryNewWidget->isEnabled());
// Click the new entry button and check that we enter edit mode
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode);
// Add entry "test" and confirm added
EditEntryWidget* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
QTest::keyClicks(titleEdit, "test");
QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
@ -144,98 +181,106 @@ void TestGui::testAddEntry()
QCOMPARE(entry->title(), QString("test"));
QCOMPARE(entry->historyItems().size(), 0);
// wait for modified timer
QTRY_COMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("%1*").arg(m_orgDbFileName));
QAction* entryEditAction = m_mainWindow->findChild<QAction*>("actionEntryEdit");
QVERIFY(entryEditAction->isEnabled());
QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
QVERIFY(entryEditWidget->isVisible());
QVERIFY(entryEditWidget->isEnabled());
QTest::mouseClick(entryEditWidget, Qt::LeftButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode);
QTest::keyClicks(titleEdit, "something");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
QCOMPARE(entry->title(), QString("testsomething"));
QCOMPARE(entry->historyItems().size(), 1);
// Add entry "something 2"
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
QTest::keyClicks(titleEdit, "something 2");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
// Add entry "something 3"
QTest::mouseClick(entryNewWidget, Qt::LeftButton);
QTest::keyClicks(titleEdit, "something 3");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
// Confirm that 4 entries now exist
QTRY_COMPARE(entryView->model()->rowCount(), 4);
}
void TestGui::testSearch()
{
QAction* searchAction = m_mainWindow->findChild<QAction*>("actionSearch");
QVERIFY(searchAction->isEnabled());
QToolBar* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
QWidget* searchActionWidget = toolBar->widgetForAction(searchAction);
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
QLineEdit* searchEdit = m_dbWidget->findChild<QLineEdit*>("searchEdit");
QToolButton* clearSearch = m_dbWidget->findChild<QToolButton*>("clearButton");
// Add canned entries for consistent testing
testAddEntry();
QVERIFY(!searchEdit->isVisible());
QToolBar* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
QWidget* searchActionWidget = toolBar->findChild<QWidget*>("SearchWidget");
QVERIFY(searchActionWidget->isEnabled());
QLineEdit* searchEdit = searchActionWidget->findChild<QLineEdit*>("searchEdit");
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
QVERIFY(entryView->isVisible());
// Enter search
QTest::mouseClick(searchActionWidget, Qt::LeftButton);
QTest::mouseClick(searchEdit, Qt::LeftButton);
QTRY_VERIFY(searchEdit->hasFocus());
// Search for "ZZZ"
QTest::keyClicks(searchEdit, "ZZZ");
QTRY_COMPARE(entryView->model()->rowCount(), 0);
// Escape
QTest::keyClick(m_mainWindow, Qt::Key_Escape);
QTRY_VERIFY(!searchEdit->hasFocus());
// Enter search again
QTest::mouseClick(searchActionWidget, Qt::LeftButton);
QTRY_VERIFY(searchEdit->hasFocus());
// Input and clear
QTest::keyClicks(searchEdit, "ZZZ");
QTRY_COMPARE(searchEdit->text(), QString("ZZZ"));
QTest::mouseClick(clearSearch, Qt::LeftButton);
QTRY_COMPARE(searchEdit->text(), QString(""));
// Triggering search should select the existing text
QTest::keyClicks(searchEdit, "ZZZ");
QTest::mouseClick(searchActionWidget, Qt::LeftButton);
QTRY_VERIFY(m_dbWidget->isInSearchMode());
QTRY_COMPARE(entryView->model()->rowCount(), 0);
// Escape clears searchedit and retains focus
QTest::keyClick(searchEdit, Qt::Key_Escape);
QTRY_VERIFY(searchEdit->text().isEmpty());
QTRY_VERIFY(searchEdit->hasFocus());
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::ViewMode);
// Search for "some"
QTest::keyClicks(searchEdit, "some");
QTRY_COMPARE(entryView->model()->rowCount(), 4);
QTRY_VERIFY(m_dbWidget->isInSearchMode());
QTRY_COMPARE(entryView->model()->rowCount(), 3);
// Press Down to focus on the entry view
QVERIFY(!entryView->hasFocus());
QTest::keyClick(searchEdit, Qt::Key_Down);
QVERIFY(entryView->hasFocus());
QTest::keyClicks(searchEdit, "thing");
QTRY_COMPARE(entryView->model()->rowCount(), 2);
//QVERIFY(!entryView->hasFocus());
//QTest::keyClick(searchEdit, Qt::Key_Down);
//QVERIFY(entryView->hasFocus());
clickIndex(entryView->model()->index(0, 1), entryView, Qt::LeftButton);
// Try to edit the first entry from the search view
QModelIndex item = entryView->model()->index(0, 1);
Entry* entry = entryView->entryFromIndex(item);
QVERIFY(m_dbWidget->isInSearchMode());
clickIndex(item, entryView, Qt::LeftButton);
QAction* entryEditAction = m_mainWindow->findChild<QAction*>("actionEntryEdit");
QVERIFY(entryEditAction->isEnabled());
QWidget* entryEditWidget = toolBar->widgetForAction(entryEditAction);
QVERIFY(entryEditWidget->isVisible());
QVERIFY(entryEditWidget->isEnabled());
QTest::mouseClick(entryEditWidget, Qt::LeftButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::EditMode);
// Perform the edit and save it
EditEntryWidget* editEntryWidget = m_dbWidget->findChild<EditEntryWidget*>("editEntryWidget");
QLineEdit* titleEdit = editEntryWidget->findChild<QLineEdit*>("titleEdit");
QString origTitle = titleEdit->text();
QTest::keyClicks(titleEdit, "_edited");
QDialogButtonBox* editEntryWidgetButtonBox = editEntryWidget->findChild<QDialogButtonBox*>("buttonBox");
QTest::mouseClick(editEntryWidgetButtonBox->button(QDialogButtonBox::Ok), Qt::LeftButton);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::ViewMode);
// Confirm the edit was made and we are back in view mode
QTRY_VERIFY(m_dbWidget->isInSearchMode());
QCOMPARE(entry->title(), origTitle.append("_edited"));
clickIndex(entryView->model()->index(1, 0), entryView, Qt::LeftButton);
// Cancel search, should return to normal view
QTest::mouseClick(searchEdit, Qt::LeftButton);
QTest::keyClick(searchEdit, Qt::Key_Escape);
QTRY_COMPARE(m_dbWidget->currentMode(), DatabaseWidget::ViewMode);
//QCOMPARE(entryView->model()->rowCount(), 4);
// TODO: add tests to confirm case sensitive and group search
}
void TestGui::testDeleteEntry()
{
// Add canned entries for consistent testing
testAddEntry();
GroupView* groupView = m_dbWidget->findChild<GroupView*>("groupView");
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
QToolBar* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
QAction* entryDeleteAction = m_mainWindow->findChild<QAction*>("actionEntryDelete");
QWidget* entryDeleteWidget = toolBar->widgetForAction(entryDeleteAction);
QCOMPARE(m_dbWidget->currentMode(), DatabaseWidget::ViewMode);
clickIndex(entryView->model()->index(1, 0), entryView, Qt::LeftButton);
QVERIFY(entryDeleteWidget->isVisible());
QVERIFY(entryDeleteWidget->isEnabled());
QVERIFY(!m_db->metadata()->recycleBin());
@ -260,21 +305,7 @@ void TestGui::testSearch()
QCOMPARE(entryView->model()->rowCount(), 1);
QCOMPARE(m_db->metadata()->recycleBin()->entries().size(), 3);
QWidget* closeSearchButton = m_dbWidget->findChild<QToolButton*>("closeSearchButton");
QTest::mouseClick(closeSearchButton, Qt::LeftButton);
QCOMPARE(entryView->model()->rowCount(), 1);
}
void TestGui::testDeleteEntry()
{
GroupView* groupView = m_dbWidget->findChild<GroupView*>("groupView");
EntryView* entryView = m_dbWidget->findChild<EntryView*>("entryView");
QToolBar* toolBar = m_mainWindow->findChild<QToolBar*>("toolBar");
QAction* entryDeleteAction = m_mainWindow->findChild<QAction*>("actionEntryDelete");
QWidget* entryDeleteWidget = toolBar->widgetForAction(entryDeleteAction);
QCOMPARE(groupView->currentGroup(), m_db->rootGroup());
QModelIndex rootGroupIndex = groupView->model()->index(0, 0);
clickIndex(groupView->model()->index(groupView->model()->rowCount(rootGroupIndex) - 1, 0, rootGroupIndex),
groupView, Qt::LeftButton);
@ -363,28 +394,29 @@ void TestGui::testDragAndDropGroup()
dragAndDropGroup(groupModel->index(0, 0, rootIndex),
rootIndex,
-1, true, "NewDatabase", 5);
-1, true, "NewDatabase", 4);
}
void TestGui::testSaveAs()
{
QFileInfo fileInfo(m_orgDbFile.fileName());
QFileInfo fileInfo(m_dbFile.fileName());
QDateTime lastModified = fileInfo.lastModified();
m_db->metadata()->setName("SaveAs");
QTemporaryFile* tmpFile = new QTemporaryFile();
// open temporary file so it creates a filename
QVERIFY(tmpFile->open());
m_tmpFileName = tmpFile->fileName();
delete tmpFile;
fileDialog()->setNextFileName(m_tmpFileName);
QTemporaryFile tmpFile;
QVERIFY(tmpFile.open());
QString tmpFileName = tmpFile.fileName();
tmpFile.remove();
fileDialog()->setNextFileName(tmpFileName);
triggerAction("actionDatabaseSaveAs");
QCOMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("SaveAs"));
checkDatabase();
checkDatabase(tmpFileName);
fileInfo.refresh();
QCOMPARE(fileInfo.lastModified(), lastModified);
@ -433,40 +465,48 @@ void TestGui::testKeePass1Import()
QCOMPARE(m_tabWidget->count(), 2);
QCOMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()), QString("basic [New database]*"));
// Close the KeePass1 Database
MessageBox::setNextAnswer(QMessageBox::No);
triggerAction("actionDatabaseClose");
Tools::wait(100);
}
void TestGui::testDatabaseLocking()
{
MessageBox::setNextAnswer(QMessageBox::Cancel);
QString origDbName = m_tabWidget->tabText(0);
MessageBox::setNextAnswer(QMessageBox::Cancel);
triggerAction("actionLockDatabases");
QCOMPARE(m_tabWidget->tabText(0).remove('&'), QString("Save [locked]"));
QCOMPARE(m_tabWidget->tabText(1).remove('&'), QString("basic [New database]*"));
QCOMPARE(m_tabWidget->tabText(0).remove('&'), origDbName + " [locked]");
QWidget* dbWidget = m_tabWidget->currentDatabaseWidget();
QWidget* unlockDatabaseWidget = dbWidget->findChild<QWidget*>("unlockDatabaseWidget");
QWidget* editPassword = unlockDatabaseWidget->findChild<QLineEdit*>("editPassword");
QVERIFY(editPassword);
QTest::keyClicks(editPassword, "masterpw");
QTest::keyClicks(editPassword, "a");
QTest::keyClick(editPassword, Qt::Key_Enter);
QCOMPARE(m_tabWidget->tabText(m_tabWidget->currentIndex()).remove('&'), QString("basic [New database]*"));
QCOMPARE(m_tabWidget->tabText(0).remove('&'), origDbName);
}
void TestGui::cleanupTestCase()
{
delete m_mainWindow;
QFile::remove(m_tmpFileName);
}
void TestGui::checkDatabase()
void TestGui::checkDatabase(QString dbFileName)
{
if (dbFileName.isEmpty())
dbFileName = m_dbFile.fileName();
CompositeKey key;
key.addKey(PasswordKey("a"));
KeePass2Reader reader;
QScopedPointer<Database> dbSaved(reader.readDatabase(m_tmpFileName, key));
QScopedPointer<Database> dbSaved(reader.readDatabase(dbFileName, key));
QVERIFY(dbSaved);
QVERIFY(!reader.hasError());
QCOMPARE(dbSaved->metadata()->name(), m_db->metadata()->name());

View File

@ -34,7 +34,10 @@ class TestGui : public QObject
private Q_SLOTS:
void initTestCase();
void testOpenDatabase();
void init();
void cleanup();
void cleanupTestCase();
void testTabs();
void testEditEntry();
void testAddEntry();
@ -48,10 +51,9 @@ private Q_SLOTS:
void testDatabaseSettings();
void testKeePass1Import();
void testDatabaseLocking();
void cleanupTestCase();
private:
void checkDatabase();
void checkDatabase(QString dbFileName = "");
void triggerAction(const QString& name);
void dragAndDropGroup(const QModelIndex& sourceIndex, const QModelIndex& targetIndex, int row,
bool expectedResult, const QString& expectedParentName, int expectedPos);
@ -61,9 +63,8 @@ private:
MainWindow* m_mainWindow;
DatabaseTabWidget* m_tabWidget;
DatabaseWidget* m_dbWidget;
QTemporaryFile m_orgDbFile;
QString m_orgDbFileName;
QString m_tmpFileName;
QTemporaryFile m_dbFile;
QString m_dbFileName;
Database* m_db;
};