1 /*
2 * Copyright (C) 2017 KeePassXC Team <team@keepassxc.org>
3 *
4 * This program is free software: you can redistribute it and/or modify
5 * it under the terms of the GNU General Public License as published by
6 * the Free Software Foundation, either version 2 or (at your option)
7 * version 3 of the License.
8 *
9 * This program is distributed in the hope that it will be useful,
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 * GNU General Public License for more details.
13 *
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <http://www.gnu.org/licenses/>.
16 */
17
18 #include "SearchWidget.h"
19 #include "ui_SearchHelpWidget.h"
20 #include "ui_SearchWidget.h"
21
22 #include <QKeyEvent>
23 #include <QMenu>
24 #include <QShortcut>
25 #include <QToolButton>
26
27 #include "core/Config.h"
28 #include "core/Resources.h"
29 #include "gui/widgets/PopupHelpWidget.h"
30
SearchWidget(QWidget * parent)31 SearchWidget::SearchWidget(QWidget* parent)
32 : QWidget(parent)
33 , m_ui(new Ui::SearchWidget())
34 , m_searchTimer(new QTimer(this))
35 , m_clearSearchTimer(new QTimer(this))
36 {
37 m_ui->setupUi(this);
38 setFocusProxy(m_ui->searchEdit);
39
40 m_helpWidget = new PopupHelpWidget(m_ui->searchEdit);
41 Ui::SearchHelpWidget helpUi;
42 helpUi.setupUi(m_helpWidget);
43
44 m_searchTimer->setSingleShot(true);
45 m_clearSearchTimer->setSingleShot(true);
46
47 connect(m_ui->searchEdit, SIGNAL(textChanged(QString)), SLOT(startSearchTimer()));
48 connect(m_ui->helpIcon, SIGNAL(triggered()), SLOT(toggleHelp()));
49 connect(m_ui->searchIcon, SIGNAL(triggered()), SLOT(showSearchMenu()));
50 connect(m_searchTimer, SIGNAL(timeout()), SLOT(startSearch()));
51 connect(m_clearSearchTimer, SIGNAL(timeout()), SLOT(clearSearch()));
52 connect(this, SIGNAL(escapePressed()), SLOT(clearSearch()));
53
54 m_ui->searchEdit->setPlaceholderText(tr("Search (%1)...", "Search placeholder text, %1 is the keyboard shortcut")
55 .arg(QKeySequence(QKeySequence::Find).toString(QKeySequence::NativeText)));
56 m_ui->searchEdit->installEventFilter(this);
57
58 m_searchMenu = new QMenu(this);
59 m_actionCaseSensitive = m_searchMenu->addAction(tr("Case sensitive"), this, SLOT(updateCaseSensitive()));
60 m_actionCaseSensitive->setObjectName("actionSearchCaseSensitive");
61 m_actionCaseSensitive->setCheckable(true);
62
63 m_actionLimitGroup = m_searchMenu->addAction(tr("Limit search to selected group"), this, SLOT(updateLimitGroup()));
64 m_actionLimitGroup->setObjectName("actionSearchLimitGroup");
65 m_actionLimitGroup->setCheckable(true);
66 m_actionLimitGroup->setChecked(config()->get(Config::SearchLimitGroup).toBool());
67
68 m_ui->searchIcon->setIcon(resources()->icon("system-search"));
69 m_ui->searchEdit->addAction(m_ui->searchIcon, QLineEdit::LeadingPosition);
70
71 m_ui->helpIcon->setIcon(resources()->icon("system-help"));
72 m_ui->searchEdit->addAction(m_ui->helpIcon, QLineEdit::TrailingPosition);
73
74 // Fix initial visibility of actions (bug in Qt)
75 for (QToolButton* toolButton : m_ui->searchEdit->findChildren<QToolButton*>()) {
76 toolButton->setVisible(toolButton->defaultAction()->isVisible());
77 }
78 }
79
~SearchWidget()80 SearchWidget::~SearchWidget()
81 {
82 }
83
eventFilter(QObject * obj,QEvent * event)84 bool SearchWidget::eventFilter(QObject* obj, QEvent* event)
85 {
86 if (event->type() == QEvent::KeyPress) {
87 QKeyEvent* keyEvent = static_cast<QKeyEvent*>(event);
88 if (keyEvent->key() == Qt::Key_Escape) {
89 emit escapePressed();
90 return true;
91 } else if (keyEvent->matches(QKeySequence::Copy)) {
92 // If Control+C is pressed in the search edit when no text
93 // is selected, copy the password of the current entry.
94 if (!m_ui->searchEdit->hasSelectedText()) {
95 emit copyPressed();
96 return true;
97 }
98 } else if (keyEvent->matches(QKeySequence::MoveToNextLine)) {
99 if (m_ui->searchEdit->cursorPosition() == m_ui->searchEdit->text().length()) {
100 // If down is pressed at EOL, move the focus to the entry view
101 emit downPressed();
102 return true;
103 } else {
104 // Otherwise move the cursor to EOL
105 m_ui->searchEdit->setCursorPosition(m_ui->searchEdit->text().length());
106 return true;
107 }
108 }
109 } else if (event->type() == QEvent::FocusOut) {
110 if (config()->get(Config::Security_ClearSearch).toBool()) {
111 int timeout = config()->get(Config::Security_ClearSearchTimeout).toInt();
112 if (timeout > 0) {
113 // Auto-clear search after set timeout (5 minutes by default)
114 m_clearSearchTimer->start(timeout * 60000); // 60 sec * 1000 ms
115 }
116 }
117 emit lostFocus();
118 } else if (event->type() == QEvent::FocusIn) {
119 // Never clear the search if we are using it
120 m_clearSearchTimer->stop();
121 }
122
123 return QWidget::eventFilter(obj, event);
124 }
125
connectSignals(SignalMultiplexer & mx)126 void SearchWidget::connectSignals(SignalMultiplexer& mx)
127 {
128 // Connects basically only to the current DatabaseWidget, but allows to switch between instances!
129 mx.connect(this, SIGNAL(search(QString)), SLOT(search(QString)));
130 mx.connect(this, SIGNAL(caseSensitiveChanged(bool)), SLOT(setSearchCaseSensitive(bool)));
131 mx.connect(this, SIGNAL(limitGroupChanged(bool)), SLOT(setSearchLimitGroup(bool)));
132 mx.connect(this, SIGNAL(copyPressed()), SLOT(copyPassword()));
133 mx.connect(this, SIGNAL(downPressed()), SLOT(focusOnEntries()));
134 mx.connect(SIGNAL(clearSearch()), this, SLOT(clearSearch()));
135 mx.connect(SIGNAL(entrySelectionChanged()), this, SLOT(resetSearchClearTimer()));
136 mx.connect(SIGNAL(currentModeChanged(DatabaseWidget::Mode)), this, SLOT(resetSearchClearTimer()));
137 mx.connect(SIGNAL(databaseUnlocked()), this, SLOT(focusSearch()));
138 mx.connect(m_ui->searchEdit, SIGNAL(returnPressed()), SLOT(switchToEntryEdit()));
139 }
140
databaseChanged(DatabaseWidget * dbWidget)141 void SearchWidget::databaseChanged(DatabaseWidget* dbWidget)
142 {
143 if (dbWidget != nullptr) {
144 // Set current search text from this database
145 m_ui->searchEdit->setText(dbWidget->getCurrentSearch());
146 // Enforce search policy
147 emit caseSensitiveChanged(m_actionCaseSensitive->isChecked());
148 emit limitGroupChanged(m_actionLimitGroup->isChecked());
149 } else {
150 clearSearch();
151 }
152 }
153
startSearchTimer()154 void SearchWidget::startSearchTimer()
155 {
156 if (!m_searchTimer->isActive()) {
157 m_searchTimer->stop();
158 }
159 m_searchTimer->start(100);
160 }
161
startSearch()162 void SearchWidget::startSearch()
163 {
164 if (!m_searchTimer->isActive()) {
165 m_searchTimer->stop();
166 }
167
168 search(m_ui->searchEdit->text());
169 }
170
resetSearchClearTimer()171 void SearchWidget::resetSearchClearTimer()
172 {
173 // Restart the search clear timer if it is running
174 if (m_clearSearchTimer->isActive()) {
175 m_clearSearchTimer->start();
176 }
177 }
178
updateCaseSensitive()179 void SearchWidget::updateCaseSensitive()
180 {
181 emit caseSensitiveChanged(m_actionCaseSensitive->isChecked());
182 }
183
setCaseSensitive(bool state)184 void SearchWidget::setCaseSensitive(bool state)
185 {
186 m_actionCaseSensitive->setChecked(state);
187 updateCaseSensitive();
188 }
189
updateLimitGroup()190 void SearchWidget::updateLimitGroup()
191 {
192 config()->set(Config::SearchLimitGroup, m_actionLimitGroup->isChecked());
193 emit limitGroupChanged(m_actionLimitGroup->isChecked());
194 }
195
setLimitGroup(bool state)196 void SearchWidget::setLimitGroup(bool state)
197 {
198 m_actionLimitGroup->setChecked(state);
199 updateLimitGroup();
200 }
201
focusSearch()202 void SearchWidget::focusSearch()
203 {
204 m_ui->searchEdit->setFocus();
205 m_ui->searchEdit->selectAll();
206 }
207
clearSearch()208 void SearchWidget::clearSearch()
209 {
210 m_ui->searchEdit->clear();
211 emit searchCanceled();
212 }
213
toggleHelp()214 void SearchWidget::toggleHelp()
215 {
216 if (m_helpWidget->isVisible()) {
217 m_helpWidget->hide();
218 } else {
219 m_helpWidget->show();
220 }
221 }
222
showSearchMenu()223 void SearchWidget::showSearchMenu()
224 {
225 m_searchMenu->exec(m_ui->searchEdit->mapToGlobal(m_ui->searchEdit->rect().bottomLeft()));
226 }
227