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