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