]> cloud.milkyroute.net Git - dolphin.git/blob - src/search/dolphinsearchbox.cpp
d846e5b6c44a7ef8487b26aa62ee6d54dc19f671
[dolphin.git] / src / search / dolphinsearchbox.cpp
1 /***************************************************************************
2 * Copyright (C) 2010 by Peter Penz <peter.penz19@gmail.com> *
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 of the License, or *
7 * (at your option) any later version. *
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, write to the *
16 * Free Software Foundation, Inc., *
17 * 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA *
18 * **************************************************************************/
19
20 #include "dolphinsearchbox.h"
21
22 #include "dolphin_searchsettings.h"
23 #include "dolphinfacetswidget.h"
24 #include "panels/places/placesitemmodel.h"
25
26 #include <KLocalizedString>
27 #include <KNS3/KMoreToolsMenuFactory>
28 #include <KSeparator>
29 #include <config-baloo.h>
30 #ifdef HAVE_BALOO
31 #include <Baloo/Query>
32 #include <Baloo/IndexerConfig>
33 #endif
34
35 #include <QButtonGroup>
36 #include <QDir>
37 #include <QFontDatabase>
38 #include <QHBoxLayout>
39 #include <QIcon>
40 #include <QLabel>
41 #include <QLineEdit>
42 #include <QScrollArea>
43 #include <QTimer>
44 #include <QToolButton>
45 #include <QUrlQuery>
46
47 DolphinSearchBox::DolphinSearchBox(QWidget* parent) :
48 QWidget(parent),
49 m_startedSearching(false),
50 m_active(true),
51 m_topLayout(nullptr),
52 m_searchInput(nullptr),
53 m_saveSearchAction(nullptr),
54 m_optionsScrollArea(nullptr),
55 m_fileNameButton(nullptr),
56 m_contentButton(nullptr),
57 m_separator(nullptr),
58 m_fromHereButton(nullptr),
59 m_everywhereButton(nullptr),
60 m_facetsToggleButton(nullptr),
61 m_facetsWidget(nullptr),
62 m_searchPath(),
63 m_startSearchTimer(nullptr)
64 {
65 }
66
67 DolphinSearchBox::~DolphinSearchBox()
68 {
69 saveSettings();
70 }
71
72 void DolphinSearchBox::setText(const QString& text)
73 {
74 m_searchInput->setText(text);
75 }
76
77 QString DolphinSearchBox::text() const
78 {
79 return m_searchInput->text();
80 }
81
82 void DolphinSearchBox::setSearchPath(const QUrl& url)
83 {
84 m_searchPath = url;
85
86 QFontMetrics metrics(m_fromHereButton->font());
87 const int maxWidth = metrics.height() * 8;
88
89 const QUrl cleanedUrl = url.adjusted(QUrl::RemoveUserInfo | QUrl::StripTrailingSlash);
90 QString location = cleanedUrl.fileName();
91 if (location.isEmpty()) {
92 location = cleanedUrl.toString(QUrl::PreferLocalFile);
93 }
94 if (m_fromHereButton->isChecked() && cleanedUrl.path() == QDir::homePath()) {
95 m_fromHereButton->setChecked(false);
96 m_everywhereButton->setChecked(true);
97 } else {
98 m_fromHereButton->setChecked(true);
99 m_everywhereButton->setChecked(false);
100 }
101
102 const QString elidedLocation = metrics.elidedText(location, Qt::ElideMiddle, maxWidth);
103 m_fromHereButton->setText(i18nc("action:button", "From Here (%1)", elidedLocation));
104 m_fromHereButton->setToolTip(i18nc("action:button", "Limit search to '%1' and its subfolders", cleanedUrl.toString(QUrl::PreferLocalFile)));
105
106 bool hasFacetsSupport = false;
107 #ifdef HAVE_BALOO
108 const Baloo::IndexerConfig searchInfo;
109 hasFacetsSupport = searchInfo.fileIndexingEnabled() && searchInfo.shouldBeIndexed(m_searchPath.toLocalFile());
110 #endif
111 m_facetsWidget->setEnabled(hasFacetsSupport);
112 }
113
114 QUrl DolphinSearchBox::searchPath() const
115 {
116 return m_searchPath;
117 }
118
119 QUrl DolphinSearchBox::urlForSearching() const
120 {
121 QUrl url;
122 bool useBalooSearch = false;
123 #ifdef HAVE_BALOO
124 const Baloo::IndexerConfig searchInfo;
125 useBalooSearch = searchInfo.fileIndexingEnabled() && searchInfo.shouldBeIndexed(m_searchPath.toLocalFile());
126 #endif
127 if (useBalooSearch) {
128 url = balooUrlForSearching();
129 } else {
130 url.setScheme(QStringLiteral("filenamesearch"));
131
132 QUrlQuery query;
133 query.addQueryItem(QStringLiteral("search"), m_searchInput->text());
134 if (m_contentButton->isChecked()) {
135 query.addQueryItem(QStringLiteral("checkContent"), QStringLiteral("yes"));
136 }
137
138 QString encodedUrl;
139 if (m_everywhereButton->isChecked()) {
140 encodedUrl = QDir::homePath();
141 } else {
142 encodedUrl = m_searchPath.url();
143 }
144 query.addQueryItem(QStringLiteral("url"), encodedUrl);
145
146 url.setQuery(query);
147 }
148
149 return url;
150 }
151
152 void DolphinSearchBox::fromSearchUrl(const QUrl& url)
153 {
154 if (url.scheme() == QLatin1String("baloosearch")) {
155 fromBalooSearchUrl(url);
156 } else if (url.scheme() == QLatin1String("filenamesearch")) {
157 const QUrlQuery query(url);
158 setText(query.queryItemValue(QStringLiteral("search")));
159 setSearchPath(QUrl::fromUserInput(query.queryItemValue(QStringLiteral("url")), QString(), QUrl::AssumeLocalFile));
160 m_contentButton->setChecked(query.queryItemValue(QStringLiteral("checkContent")) == QLatin1String("yes"));
161 } else {
162 setText(QString());
163 setSearchPath(url);
164 }
165 }
166
167 void DolphinSearchBox::selectAll()
168 {
169 m_searchInput->selectAll();
170 }
171
172 void DolphinSearchBox::setActive(bool active)
173 {
174 if (active != m_active) {
175 m_active = active;
176
177 if (active) {
178 emit activated();
179 }
180 }
181 }
182
183 bool DolphinSearchBox::isActive() const
184 {
185 return m_active;
186 }
187
188 bool DolphinSearchBox::event(QEvent* event)
189 {
190 if (event->type() == QEvent::Polish) {
191 init();
192 }
193 return QWidget::event(event);
194 }
195
196 void DolphinSearchBox::showEvent(QShowEvent* event)
197 {
198 if (!event->spontaneous()) {
199 m_searchInput->setFocus();
200 m_startedSearching = false;
201 }
202 }
203
204 void DolphinSearchBox::hideEvent(QHideEvent* event)
205 {
206 Q_UNUSED(event);
207 m_startedSearching = false;
208 m_startSearchTimer->stop();
209 }
210
211 void DolphinSearchBox::keyReleaseEvent(QKeyEvent* event)
212 {
213 QWidget::keyReleaseEvent(event);
214 if (event->key() == Qt::Key_Escape) {
215 if (m_searchInput->text().isEmpty()) {
216 emit closeRequest();
217 } else {
218 m_searchInput->clear();
219 }
220 }
221 }
222
223 bool DolphinSearchBox::eventFilter(QObject* obj, QEvent* event)
224 {
225 switch (event->type()) {
226 case QEvent::FocusIn:
227 // #379135: we get the FocusIn event when we close a tab but we don't want to emit
228 // the activated() signal before the removeTab() call in DolphinTabWidget::closeTab() returns.
229 // To avoid this issue, we delay the activation of the search box.
230 // We also don't want to schedule the activation process if we are already active,
231 // otherwise we can enter in a loop of FocusIn/FocusOut events with the searchbox of another tab.
232 if (!isActive()) {
233 QTimer::singleShot(0, this, [this] {
234 setActive(true);
235 setFocus();
236 });
237 }
238 break;
239
240 default:
241 break;
242 }
243
244 return QObject::eventFilter(obj, event);
245 }
246
247 void DolphinSearchBox::emitSearchRequest()
248 {
249 m_startSearchTimer->stop();
250 m_startedSearching = true;
251 m_saveSearchAction->setEnabled(true);
252 emit searchRequest();
253 }
254
255 void DolphinSearchBox::emitCloseRequest()
256 {
257 m_startSearchTimer->stop();
258 m_startedSearching = false;
259 m_saveSearchAction->setEnabled(false);
260 emit closeRequest();
261 }
262
263 void DolphinSearchBox::slotConfigurationChanged()
264 {
265 saveSettings();
266 if (m_startedSearching) {
267 emitSearchRequest();
268 }
269 }
270
271 void DolphinSearchBox::slotSearchTextChanged(const QString& text)
272 {
273
274 if (text.isEmpty()) {
275 m_startSearchTimer->stop();
276 } else {
277 m_startSearchTimer->start();
278 }
279 emit searchTextChanged(text);
280 }
281
282 void DolphinSearchBox::slotReturnPressed()
283 {
284 emitSearchRequest();
285 emit returnPressed();
286 }
287
288 void DolphinSearchBox::slotFacetsButtonToggled()
289 {
290 const bool facetsIsVisible = !m_facetsWidget->isVisible();
291 m_facetsWidget->setVisible(facetsIsVisible);
292 updateFacetsToggleButton();
293 }
294
295 void DolphinSearchBox::slotFacetChanged()
296 {
297 m_startedSearching = true;
298 m_startSearchTimer->stop();
299 emit searchRequest();
300 }
301
302 void DolphinSearchBox::slotSearchSaved()
303 {
304 const QUrl searchURL = urlForSearching();
305 if (searchURL.isValid()) {
306 PlacesItemModel model;
307 const QString label = i18n("Search for %1 in %2", text(), searchPath().fileName());
308 model.createPlacesItem(label,
309 searchURL,
310 QStringLiteral("folder-saved-search-symbolic"));
311 }
312 }
313
314 void DolphinSearchBox::initButton(QToolButton* button)
315 {
316 button->installEventFilter(this);
317 button->setAutoExclusive(true);
318 button->setAutoRaise(true);
319 button->setCheckable(true);
320 connect(button, &QToolButton::clicked, this, &DolphinSearchBox::slotConfigurationChanged);
321 }
322
323 void DolphinSearchBox::loadSettings()
324 {
325 if (SearchSettings::location() == QLatin1String("Everywhere")) {
326 m_everywhereButton->setChecked(true);
327 } else {
328 m_fromHereButton->setChecked(true);
329 }
330
331 if (SearchSettings::what() == QLatin1String("Content")) {
332 m_contentButton->setChecked(true);
333 } else {
334 m_fileNameButton->setChecked(true);
335 }
336
337 m_facetsWidget->setVisible(SearchSettings::showFacetsWidget());
338 }
339
340 void DolphinSearchBox::saveSettings()
341 {
342 SearchSettings::setLocation(m_fromHereButton->isChecked() ? QStringLiteral("FromHere") : QStringLiteral("Everywhere"));
343 SearchSettings::setWhat(m_fileNameButton->isChecked() ? QStringLiteral("FileName") : QStringLiteral("Content"));
344 SearchSettings::setShowFacetsWidget(m_facetsToggleButton->isChecked());
345 SearchSettings::self()->save();
346 }
347
348 void DolphinSearchBox::init()
349 {
350 // Create close button
351 QToolButton* closeButton = new QToolButton(this);
352 closeButton->setAutoRaise(true);
353 closeButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close")));
354 closeButton->setToolTip(i18nc("@info:tooltip", "Quit searching"));
355 connect(closeButton, &QToolButton::clicked, this, &DolphinSearchBox::emitCloseRequest);
356
357 // Create search box
358 m_searchInput = new QLineEdit(this);
359 m_searchInput->setPlaceholderText(i18n("Search..."));
360 m_searchInput->installEventFilter(this);
361 m_searchInput->setClearButtonEnabled(true);
362 m_searchInput->setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
363 connect(m_searchInput, &QLineEdit::returnPressed,
364 this, &DolphinSearchBox::slotReturnPressed);
365 connect(m_searchInput, &QLineEdit::textChanged,
366 this, &DolphinSearchBox::slotSearchTextChanged);
367 setFocusProxy(m_searchInput);
368
369 // Add "Save search" button inside search box
370 m_saveSearchAction = new QAction(this);
371 m_saveSearchAction->setIcon (QIcon::fromTheme(QStringLiteral("document-save-symbolic")));
372 m_saveSearchAction->setText(i18nc("action:button", "Save this search to quickly access it again in the future"));
373 m_saveSearchAction->setEnabled(false);
374 m_searchInput->addAction(m_saveSearchAction, QLineEdit::TrailingPosition);
375 connect(m_saveSearchAction, &QAction::triggered, this, &DolphinSearchBox::slotSearchSaved);
376
377 // Apply layout for the search input
378 QHBoxLayout* searchInputLayout = new QHBoxLayout();
379 searchInputLayout->setContentsMargins(0, 0, 0, 0);
380 searchInputLayout->addWidget(closeButton);
381 searchInputLayout->addWidget(m_searchInput);
382
383 // Create "Filename" and "Content" button
384 m_fileNameButton = new QToolButton(this);
385 m_fileNameButton->setText(i18nc("action:button", "Filename"));
386 initButton(m_fileNameButton);
387
388 m_contentButton = new QToolButton();
389 m_contentButton->setText(i18nc("action:button", "Content"));
390 initButton(m_contentButton);
391
392 QButtonGroup* searchWhatGroup = new QButtonGroup(this);
393 searchWhatGroup->addButton(m_fileNameButton);
394 searchWhatGroup->addButton(m_contentButton);
395
396 m_separator = new KSeparator(Qt::Vertical, this);
397
398 // Create "From Here" and "Your files" buttons
399 m_fromHereButton = new QToolButton(this);
400 m_fromHereButton->setText(i18nc("action:button", "From Here"));
401 initButton(m_fromHereButton);
402
403 m_everywhereButton = new QToolButton(this);
404 m_everywhereButton->setText(i18nc("action:button", "Your files"));
405 m_everywhereButton->setToolTip(i18nc("action:button", "Search in your home directory"));
406 m_everywhereButton->setIcon(QIcon::fromTheme(QStringLiteral("user-home")));
407 m_everywhereButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
408 initButton(m_everywhereButton);
409
410 QButtonGroup* searchLocationGroup = new QButtonGroup(this);
411 searchLocationGroup->addButton(m_fromHereButton);
412 searchLocationGroup->addButton(m_everywhereButton);
413
414 auto moreSearchToolsButton = new QToolButton(this);
415 moreSearchToolsButton->setAutoRaise(true);
416 moreSearchToolsButton->setPopupMode(QToolButton::InstantPopup);
417 moreSearchToolsButton->setIcon(QIcon::fromTheme("arrow-down-double"));
418 moreSearchToolsButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
419 moreSearchToolsButton->setText(i18n("More Search Tools"));
420 moreSearchToolsButton->setMenu(new QMenu(this));
421 connect(moreSearchToolsButton->menu(), &QMenu::aboutToShow, moreSearchToolsButton->menu(), [this, moreSearchToolsButton]()
422 {
423 m_menuFactory.reset(new KMoreToolsMenuFactory("dolphin/search-tools"));
424 moreSearchToolsButton->menu()->clear();
425 m_menuFactory->fillMenuFromGroupingNames(moreSearchToolsButton->menu(), { "files-find" }, this->m_searchPath);
426 } );
427
428 // Create "Facets" widgets
429 m_facetsToggleButton = new QToolButton(this);
430 m_facetsToggleButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
431 initButton(m_facetsToggleButton);
432 connect(m_facetsToggleButton, &QToolButton::clicked, this, &DolphinSearchBox::slotFacetsButtonToggled);
433
434 m_facetsWidget = new DolphinFacetsWidget(this);
435 m_facetsWidget->installEventFilter(this);
436 m_facetsWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
437 connect(m_facetsWidget, &DolphinFacetsWidget::facetChanged, this, &DolphinSearchBox::slotFacetChanged);
438
439 // Apply layout for the options
440 QHBoxLayout* optionsLayout = new QHBoxLayout();
441 optionsLayout->setContentsMargins(0, 0, 0, 0);
442 optionsLayout->addWidget(m_fileNameButton);
443 optionsLayout->addWidget(m_contentButton);
444 optionsLayout->addWidget(m_separator);
445 optionsLayout->addWidget(m_fromHereButton);
446 optionsLayout->addWidget(m_everywhereButton);
447 optionsLayout->addWidget(new KSeparator(Qt::Vertical, this));
448 optionsLayout->addWidget(m_facetsToggleButton);
449 optionsLayout->addWidget(moreSearchToolsButton);
450 optionsLayout->addStretch(1);
451
452 // Put the options into a QScrollArea. This prevents increasing the view width
453 // in case that not enough width for the options is available.
454 QWidget* optionsContainer = new QWidget(this);
455 optionsContainer->setLayout(optionsLayout);
456
457 m_optionsScrollArea = new QScrollArea(this);
458 m_optionsScrollArea->setFrameShape(QFrame::NoFrame);
459 m_optionsScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
460 m_optionsScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
461 m_optionsScrollArea->setMaximumHeight(optionsContainer->sizeHint().height());
462 m_optionsScrollArea->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
463 m_optionsScrollArea->setWidget(optionsContainer);
464 m_optionsScrollArea->setWidgetResizable(true);
465
466 m_topLayout = new QVBoxLayout(this);
467 m_topLayout->setContentsMargins(0, 0, 0, 0);
468 m_topLayout->addLayout(searchInputLayout);
469 m_topLayout->addWidget(m_optionsScrollArea);
470 m_topLayout->addWidget(m_facetsWidget);
471
472 loadSettings();
473
474 // The searching should be started automatically after the user did not change
475 // the text within one second
476 m_startSearchTimer = new QTimer(this);
477 m_startSearchTimer->setSingleShot(true);
478 m_startSearchTimer->setInterval(1000);
479 connect(m_startSearchTimer, &QTimer::timeout, this, &DolphinSearchBox::emitSearchRequest);
480
481 updateFacetsToggleButton();
482 }
483
484 QUrl DolphinSearchBox::balooUrlForSearching() const
485 {
486 #ifdef HAVE_BALOO
487 const QString text = m_searchInput->text();
488
489 Baloo::Query query;
490 query.addType(m_facetsWidget->facetType());
491
492 QStringList queryStrings;
493 QString ratingQuery = m_facetsWidget->ratingTerm();
494 if (!ratingQuery.isEmpty()) {
495 queryStrings << ratingQuery;
496 }
497
498 if (m_contentButton->isChecked()) {
499 queryStrings << text;
500 } else if (!text.isEmpty()) {
501 queryStrings << QStringLiteral("filename:\"%1\"").arg(text);
502 }
503
504 if (m_fromHereButton->isChecked()) {
505 query.setIncludeFolder(m_searchPath.toLocalFile());
506 }
507
508 query.setSearchString(queryStrings.join(QStringLiteral(" ")));
509
510 return query.toSearchUrl(i18nc("@title UDS_DISPLAY_NAME for a KIO directory listing. %1 is the query the user entered.",
511 "Query Results from '%1'", text));
512 #else
513 return QUrl();
514 #endif
515 }
516
517 void DolphinSearchBox::fromBalooSearchUrl(const QUrl& url)
518 {
519 #ifdef HAVE_BALOO
520 const Baloo::Query query = Baloo::Query::fromSearchUrl(url);
521
522 // Block all signals to avoid unnecessary "searchRequest" signals
523 // while we adjust the search text and the facet widget.
524 blockSignals(true);
525
526 const QString customDir = query.includeFolder();
527 if (!customDir.isEmpty()) {
528 setSearchPath(QUrl::fromLocalFile(customDir));
529 } else {
530 setSearchPath(QUrl::fromLocalFile(QDir::homePath()));
531 }
532
533 setText(query.searchString());
534
535 QStringList types = query.types();
536 if (!types.isEmpty()) {
537 m_facetsWidget->setFacetType(types.first());
538 }
539
540 const QStringList subTerms = query.searchString().split(' ', QString::SkipEmptyParts);
541 foreach (const QString& subTerm, subTerms) {
542 if (subTerm.startsWith(QLatin1String("filename:"))) {
543 const QString value = subTerm.mid(9);
544 setText(value);
545 } else if (m_facetsWidget->isRatingTerm(subTerm)) {
546 m_facetsWidget->setRatingTerm(subTerm);
547 }
548 }
549
550 m_startSearchTimer->stop();
551 blockSignals(false);
552 #else
553 Q_UNUSED(url);
554 #endif
555 }
556
557 void DolphinSearchBox::updateFacetsToggleButton()
558 {
559 const bool facetsIsVisible = SearchSettings::showFacetsWidget();
560 m_facetsToggleButton->setChecked(facetsIsVisible ? true : false);
561 m_facetsToggleButton->setIcon(QIcon::fromTheme(facetsIsVisible ? QStringLiteral("arrow-up-double") : QStringLiteral("arrow-down-double")));
562 m_facetsToggleButton->setText(facetsIsVisible ? i18nc("action:button", "Fewer Options") : i18nc("action:button", "More Options"));
563 }
564