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