]> cloud.milkyroute.net Git - dolphin.git/blob - src/search/dolphinsearchbox.cpp
flatpak: Switch to non-preview runtime
[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 if (m_startSearchTimer) {
200 m_startSearchTimer->stop();
201 }
202 }
203
204 void DolphinSearchBox::keyReleaseEvent(QKeyEvent *event)
205 {
206 QWidget::keyReleaseEvent(event);
207 if (event->key() == Qt::Key_Escape) {
208 if (m_searchInput->text().isEmpty()) {
209 emitCloseRequest();
210 } else {
211 m_searchInput->clear();
212 }
213 } else if (event->key() == Qt::Key_Down) {
214 Q_EMIT focusViewRequest();
215 }
216 }
217
218 bool DolphinSearchBox::eventFilter(QObject *obj, QEvent *event)
219 {
220 switch (event->type()) {
221 case QEvent::FocusIn:
222 // #379135: we get the FocusIn event when we close a tab but we don't want to emit
223 // the activated() signal before the removeTab() call in DolphinTabWidget::closeTab() returns.
224 // To avoid this issue, we delay the activation of the search box.
225 // We also don't want to schedule the activation process if we are already active,
226 // otherwise we can enter in a loop of FocusIn/FocusOut events with the searchbox of another tab.
227 if (!isActive()) {
228 QTimer::singleShot(0, this, [this] {
229 setActive(true);
230 setFocus();
231 });
232 }
233 break;
234
235 default:
236 break;
237 }
238
239 return QObject::eventFilter(obj, event);
240 }
241
242 void DolphinSearchBox::emitSearchRequest()
243 {
244 m_startSearchTimer->stop();
245 m_startedSearching = true;
246 m_saveSearchAction->setEnabled(true);
247 Q_EMIT searchRequest();
248 }
249
250 void DolphinSearchBox::emitCloseRequest()
251 {
252 m_startSearchTimer->stop();
253 m_startedSearching = false;
254 m_saveSearchAction->setEnabled(false);
255 Q_EMIT closeRequest();
256 }
257
258 void DolphinSearchBox::slotConfigurationChanged()
259 {
260 saveSettings();
261 if (m_startedSearching) {
262 emitSearchRequest();
263 }
264 }
265
266 void DolphinSearchBox::slotSearchTextChanged(const QString &text)
267 {
268 if (text.isEmpty()) {
269 // Restore URL when search box is cleared by closing and reopening the box.
270 emitCloseRequest();
271 Q_EMIT openRequest();
272 } else {
273 m_startSearchTimer->start();
274 }
275 Q_EMIT searchTextChanged(text);
276 }
277
278 void DolphinSearchBox::slotReturnPressed()
279 {
280 if (m_searchInput->text().isEmpty()) {
281 return;
282 }
283
284 emitSearchRequest();
285 Q_EMIT focusViewRequest();
286 }
287
288 void DolphinSearchBox::slotFacetChanged()
289 {
290 m_startedSearching = true;
291 m_startSearchTimer->stop();
292 Q_EMIT searchRequest();
293 }
294
295 void DolphinSearchBox::slotSearchSaved()
296 {
297 const QUrl searchURL = urlForSearching();
298 if (searchURL.isValid()) {
299 const QString label = i18n("Search for %1 in %2", text(), searchPath().fileName());
300 DolphinPlacesModelSingleton::instance().placesModel()->addPlace(label, searchURL, QStringLiteral("folder-saved-search-symbolic"));
301 }
302 }
303
304 void DolphinSearchBox::initButton(QToolButton *button)
305 {
306 button->installEventFilter(this);
307 button->setAutoExclusive(true);
308 button->setAutoRaise(true);
309 button->setCheckable(true);
310 connect(button, &QToolButton::clicked, this, &DolphinSearchBox::slotConfigurationChanged);
311 }
312
313 void DolphinSearchBox::loadSettings()
314 {
315 if (SearchSettings::location() == QLatin1String("Everywhere")) {
316 m_everywhereButton->setChecked(true);
317 } else {
318 m_fromHereButton->setChecked(true);
319 }
320
321 if (SearchSettings::what() == QLatin1String("Content")) {
322 m_contentButton->setChecked(true);
323 } else {
324 m_fileNameButton->setChecked(true);
325 }
326
327 updateFacetsVisible();
328 }
329
330 void DolphinSearchBox::saveSettings()
331 {
332 SearchSettings::setLocation(m_fromHereButton->isChecked() ? QStringLiteral("FromHere") : QStringLiteral("Everywhere"));
333 SearchSettings::setWhat(m_fileNameButton->isChecked() ? QStringLiteral("FileName") : QStringLiteral("Content"));
334 SearchSettings::self()->save();
335 }
336
337 void DolphinSearchBox::init()
338 {
339 // Create search box
340 m_searchInput = new QLineEdit(this);
341 m_searchInput->setPlaceholderText(i18n("Search…"));
342 m_searchInput->installEventFilter(this);
343 m_searchInput->setClearButtonEnabled(true);
344 m_searchInput->setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont));
345 connect(m_searchInput, &QLineEdit::returnPressed, this, &DolphinSearchBox::slotReturnPressed);
346 connect(m_searchInput, &QLineEdit::textChanged, this, &DolphinSearchBox::slotSearchTextChanged);
347 setFocusProxy(m_searchInput);
348
349 // Add "Save search" button inside search box
350 m_saveSearchAction = new QAction(this);
351 m_saveSearchAction->setIcon(QIcon::fromTheme(QStringLiteral("document-save-symbolic")));
352 m_saveSearchAction->setText(i18nc("action:button", "Save this search to quickly access it again in the future"));
353 m_saveSearchAction->setEnabled(false);
354 m_searchInput->addAction(m_saveSearchAction, QLineEdit::TrailingPosition);
355 connect(m_saveSearchAction, &QAction::triggered, this, &DolphinSearchBox::slotSearchSaved);
356
357 // Create close button
358 QToolButton *closeButton = new QToolButton(this);
359 closeButton->setAutoRaise(true);
360 closeButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close")));
361 closeButton->setToolTip(i18nc("@info:tooltip", "Quit searching"));
362 connect(closeButton, &QToolButton::clicked, this, &DolphinSearchBox::emitCloseRequest);
363
364 // Apply layout for the search input
365 QHBoxLayout *searchInputLayout = new QHBoxLayout();
366 searchInputLayout->setContentsMargins(0, 0, 0, 0);
367 searchInputLayout->addWidget(m_searchInput);
368 searchInputLayout->addWidget(closeButton);
369
370 // Create "Filename" and "Content" button
371 m_fileNameButton = new QToolButton(this);
372 m_fileNameButton->setText(i18nc("action:button", "Filename"));
373 initButton(m_fileNameButton);
374
375 m_contentButton = new QToolButton();
376 m_contentButton->setText(i18nc("action:button", "Content"));
377 initButton(m_contentButton);
378
379 QButtonGroup *searchWhatGroup = new QButtonGroup(this);
380 searchWhatGroup->addButton(m_fileNameButton);
381 searchWhatGroup->addButton(m_contentButton);
382
383 m_separator = new KSeparator(Qt::Vertical, this);
384
385 // Create "From Here" and "Your files" buttons
386 m_fromHereButton = new QToolButton(this);
387 m_fromHereButton->setText(i18nc("action:button", "From Here"));
388 initButton(m_fromHereButton);
389
390 m_everywhereButton = new QToolButton(this);
391 m_everywhereButton->setText(i18nc("action:button", "Your files"));
392 m_everywhereButton->setToolTip(i18nc("action:button", "Search in your home directory"));
393 m_everywhereButton->setIcon(QIcon::fromTheme(QStringLiteral("user-home")));
394 m_everywhereButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
395 initButton(m_everywhereButton);
396
397 QButtonGroup *searchLocationGroup = new QButtonGroup(this);
398 searchLocationGroup->addButton(m_fromHereButton);
399 searchLocationGroup->addButton(m_everywhereButton);
400
401 KService::Ptr kfind = KService::serviceByDesktopName(QStringLiteral("org.kde.kfind"));
402
403 QToolButton *kfindToolsButton = nullptr;
404 if (kfind) {
405 kfindToolsButton = new QToolButton(this);
406 kfindToolsButton->setAutoRaise(true);
407 kfindToolsButton->setPopupMode(QToolButton::InstantPopup);
408 kfindToolsButton->setIcon(QIcon::fromTheme("arrow-down-double"));
409 kfindToolsButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
410 kfindToolsButton->setText(i18n("Open %1", kfind->name()));
411 kfindToolsButton->setIcon(QIcon::fromTheme(kfind->icon()));
412
413 connect(kfindToolsButton, &QToolButton::clicked, this, [this, kfind] {
414 auto *job = new KIO::ApplicationLauncherJob(kfind);
415 job->setUrls({m_searchPath});
416 job->start();
417 });
418 }
419
420 // Create "Facets" widget
421 m_facetsWidget = new DolphinFacetsWidget(this);
422 m_facetsWidget->installEventFilter(this);
423 m_facetsWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum);
424 m_facetsWidget->layout()->setSpacing(Dolphin::LAYOUT_SPACING_SMALL);
425 connect(m_facetsWidget, &DolphinFacetsWidget::facetChanged, this, &DolphinSearchBox::slotFacetChanged);
426
427 // Put the options into a QScrollArea. This prevents increasing the view width
428 // in case that not enough width for the options is available.
429 QWidget *optionsContainer = new QWidget(this);
430
431 // Apply layout for the options
432 QHBoxLayout *optionsLayout = new QHBoxLayout(optionsContainer);
433 optionsLayout->setContentsMargins(0, 0, 0, 0);
434 optionsLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL);
435 optionsLayout->addWidget(m_fileNameButton);
436 optionsLayout->addWidget(m_contentButton);
437 optionsLayout->addWidget(m_separator);
438 optionsLayout->addWidget(m_fromHereButton);
439 optionsLayout->addWidget(m_everywhereButton);
440 optionsLayout->addWidget(new KSeparator(Qt::Vertical, this));
441 if (kfindToolsButton) {
442 optionsLayout->addWidget(kfindToolsButton);
443 }
444 optionsLayout->addStretch(1);
445
446 m_optionsScrollArea = new QScrollArea(this);
447 m_optionsScrollArea->setFrameShape(QFrame::NoFrame);
448 m_optionsScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
449 m_optionsScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
450 m_optionsScrollArea->setMaximumHeight(optionsContainer->sizeHint().height());
451 m_optionsScrollArea->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed);
452 m_optionsScrollArea->setWidget(optionsContainer);
453 m_optionsScrollArea->setWidgetResizable(true);
454
455 m_topLayout = new QVBoxLayout(this);
456 m_topLayout->setContentsMargins(0, Dolphin::LAYOUT_SPACING_SMALL, 0, 0);
457 m_topLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL);
458 m_topLayout->addLayout(searchInputLayout);
459 m_topLayout->addWidget(m_optionsScrollArea);
460 m_topLayout->addWidget(m_facetsWidget);
461
462 loadSettings();
463
464 // The searching should be started automatically after the user did not change
465 // the text for a while
466 m_startSearchTimer = new QTimer(this);
467 m_startSearchTimer->setSingleShot(true);
468 m_startSearchTimer->setInterval(500);
469 connect(m_startSearchTimer, &QTimer::timeout, this, &DolphinSearchBox::emitSearchRequest);
470 }
471
472 QString DolphinSearchBox::queryTitle(const QString &text) const
473 {
474 return i18nc("@title UDS_DISPLAY_NAME for a KIO directory listing. %1 is the query the user entered.", "Query Results from '%1'", text);
475 }
476
477 QUrl DolphinSearchBox::balooUrlForSearching() const
478 {
479 #if HAVE_BALOO
480 const QString text = m_searchInput->text();
481
482 Baloo::Query query;
483 query.addType(m_facetsWidget->facetType());
484
485 QStringList queryStrings = m_facetsWidget->searchTerms();
486
487 if (m_contentButton->isChecked()) {
488 queryStrings << text;
489 } else if (!text.isEmpty()) {
490 queryStrings << QStringLiteral("filename:\"%1\"").arg(text);
491 }
492
493 if (m_fromHereButton->isChecked()) {
494 query.setIncludeFolder(m_searchPath.toLocalFile());
495 }
496
497 query.setSearchString(queryStrings.join(QLatin1Char(' ')));
498
499 return query.toSearchUrl(queryTitle(text));
500 #else
501 return QUrl();
502 #endif
503 }
504
505 void DolphinSearchBox::updateFromQuery(const DolphinQuery &query)
506 {
507 // Block all signals to avoid unnecessary "searchRequest" signals
508 // while we adjust the search text and the facet widget.
509 blockSignals(true);
510
511 const QString customDir = query.includeFolder();
512 if (!customDir.isEmpty()) {
513 setSearchPath(QUrl::fromLocalFile(customDir));
514 } else {
515 setSearchPath(QUrl::fromLocalFile(QDir::homePath()));
516 }
517
518 setText(query.text());
519
520 if (query.hasContentSearch()) {
521 m_contentButton->setChecked(true);
522 } else if (query.hasFileName()) {
523 m_fileNameButton->setChecked(true);
524 }
525
526 m_facetsWidget->resetSearchTerms();
527 m_facetsWidget->setFacetType(query.type());
528 const QStringList searchTerms = query.searchTerms();
529 for (const QString &searchTerm : searchTerms) {
530 m_facetsWidget->setSearchTerm(searchTerm);
531 }
532
533 m_startSearchTimer->stop();
534 blockSignals(false);
535 }
536
537 void DolphinSearchBox::updateFacetsVisible()
538 {
539 const bool indexingEnabled = isIndexingEnabled();
540 m_facetsWidget->setEnabled(indexingEnabled);
541 m_facetsWidget->setVisible(indexingEnabled);
542 }
543
544 bool DolphinSearchBox::isIndexingEnabled() const
545 {
546 #if HAVE_BALOO
547 const Baloo::IndexerConfig searchInfo;
548 return searchInfo.fileIndexingEnabled() && !searchPath().isEmpty() && searchInfo.shouldBeIndexed(searchPath().toLocalFile());
549 #else
550 return false;
551 #endif
552 }
553
554 #include "moc_dolphinsearchbox.cpp"