File indexing completed on 2024-04-28 17:03:10
0001 /* 0002 * SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz19@gmail.com> 0003 * 0004 * SPDX-License-Identifier: GPL-2.0-or-later 0005 */ 0006 0007 #include "dolphinsearchbox.h" 0008 #include "global.h" 0009 0010 #include "dolphin_searchsettings.h" 0011 #include "dolphinfacetswidget.h" 0012 #include "dolphinplacesmodelsingleton.h" 0013 #include "dolphinquery.h" 0014 0015 #include "config-dolphin.h" 0016 #include <KIO/ApplicationLauncherJob> 0017 #include <KLocalizedString> 0018 #include <KSeparator> 0019 #include <KService> 0020 #if HAVE_BALOO 0021 #include <Baloo/IndexerConfig> 0022 #include <Baloo/Query> 0023 #endif 0024 0025 #include <QButtonGroup> 0026 #include <QDir> 0027 #include <QFontDatabase> 0028 #include <QHBoxLayout> 0029 #include <QIcon> 0030 #include <QKeyEvent> 0031 #include <QLineEdit> 0032 #include <QScrollArea> 0033 #include <QShowEvent> 0034 #include <QTimer> 0035 #include <QToolButton> 0036 #include <QUrlQuery> 0037 0038 DolphinSearchBox::DolphinSearchBox(QWidget *parent) 0039 : QWidget(parent) 0040 , m_startedSearching(false) 0041 , m_active(true) 0042 , m_topLayout(nullptr) 0043 , m_searchInput(nullptr) 0044 , m_saveSearchAction(nullptr) 0045 , m_optionsScrollArea(nullptr) 0046 , m_fileNameButton(nullptr) 0047 , m_contentButton(nullptr) 0048 , m_separator(nullptr) 0049 , m_fromHereButton(nullptr) 0050 , m_everywhereButton(nullptr) 0051 , m_facetsWidget(nullptr) 0052 , m_searchPath() 0053 , m_startSearchTimer(nullptr) 0054 { 0055 } 0056 0057 DolphinSearchBox::~DolphinSearchBox() 0058 { 0059 saveSettings(); 0060 } 0061 0062 void DolphinSearchBox::setText(const QString &text) 0063 { 0064 if (m_searchInput->text() != text) { 0065 m_searchInput->setText(text); 0066 } 0067 } 0068 0069 QString DolphinSearchBox::text() const 0070 { 0071 return m_searchInput->text(); 0072 } 0073 0074 void DolphinSearchBox::setSearchPath(const QUrl &url) 0075 { 0076 if (url == m_searchPath) { 0077 return; 0078 } 0079 0080 const QUrl cleanedUrl = url.adjusted(QUrl::RemoveUserInfo | QUrl::StripTrailingSlash); 0081 0082 if (cleanedUrl.path() == QDir::homePath()) { 0083 m_fromHereButton->setChecked(false); 0084 m_everywhereButton->setChecked(true); 0085 if (!m_searchPath.isEmpty()) { 0086 return; 0087 } 0088 } else { 0089 m_everywhereButton->setChecked(false); 0090 m_fromHereButton->setChecked(true); 0091 } 0092 0093 m_searchPath = url; 0094 0095 QFontMetrics metrics(m_fromHereButton->font()); 0096 const int maxWidth = metrics.height() * 8; 0097 0098 QString location = cleanedUrl.fileName(); 0099 if (location.isEmpty()) { 0100 location = cleanedUrl.toString(QUrl::PreferLocalFile); 0101 } 0102 const QString elidedLocation = metrics.elidedText(location, Qt::ElideMiddle, maxWidth); 0103 m_fromHereButton->setText(i18nc("action:button", "From Here (%1)", elidedLocation)); 0104 m_fromHereButton->setToolTip(i18nc("action:button", "Limit search to '%1' and its subfolders", cleanedUrl.toString(QUrl::PreferLocalFile))); 0105 } 0106 0107 QUrl DolphinSearchBox::searchPath() const 0108 { 0109 return m_everywhereButton->isChecked() ? QUrl::fromLocalFile(QDir::homePath()) : m_searchPath; 0110 } 0111 0112 QUrl DolphinSearchBox::urlForSearching() const 0113 { 0114 QUrl url; 0115 0116 if (isIndexingEnabled()) { 0117 url = balooUrlForSearching(); 0118 } else { 0119 url.setScheme(QStringLiteral("filenamesearch")); 0120 0121 QUrlQuery query; 0122 query.addQueryItem(QStringLiteral("search"), m_searchInput->text()); 0123 if (m_contentButton->isChecked()) { 0124 query.addQueryItem(QStringLiteral("checkContent"), QStringLiteral("yes")); 0125 } 0126 0127 query.addQueryItem(QStringLiteral("url"), searchPath().url()); 0128 query.addQueryItem(QStringLiteral("title"), queryTitle(m_searchInput->text())); 0129 0130 url.setQuery(query); 0131 } 0132 0133 return url; 0134 } 0135 0136 void DolphinSearchBox::fromSearchUrl(const QUrl &url) 0137 { 0138 if (DolphinQuery::supportsScheme(url.scheme())) { 0139 const DolphinQuery query = DolphinQuery::fromSearchUrl(url); 0140 updateFromQuery(query); 0141 } else if (url.scheme() == QLatin1String("filenamesearch")) { 0142 const QUrlQuery query(url); 0143 setText(query.queryItemValue(QStringLiteral("search"))); 0144 if (m_searchPath.scheme() != url.scheme()) { 0145 m_searchPath = QUrl(); 0146 } 0147 setSearchPath(QUrl::fromUserInput(query.queryItemValue(QStringLiteral("url")), QString(), QUrl::AssumeLocalFile)); 0148 m_contentButton->setChecked(query.queryItemValue(QStringLiteral("checkContent")) == QLatin1String("yes")); 0149 } else { 0150 setText(QString()); 0151 m_searchPath = QUrl(); 0152 setSearchPath(url); 0153 } 0154 0155 updateFacetsVisible(); 0156 } 0157 0158 void DolphinSearchBox::selectAll() 0159 { 0160 m_searchInput->selectAll(); 0161 } 0162 0163 void DolphinSearchBox::setActive(bool active) 0164 { 0165 if (active != m_active) { 0166 m_active = active; 0167 0168 if (active) { 0169 Q_EMIT activated(); 0170 } 0171 } 0172 } 0173 0174 bool DolphinSearchBox::isActive() const 0175 { 0176 return m_active; 0177 } 0178 0179 bool DolphinSearchBox::event(QEvent *event) 0180 { 0181 if (event->type() == QEvent::Polish) { 0182 init(); 0183 } 0184 return QWidget::event(event); 0185 } 0186 0187 void DolphinSearchBox::showEvent(QShowEvent *event) 0188 { 0189 if (!event->spontaneous()) { 0190 m_searchInput->setFocus(); 0191 m_startedSearching = false; 0192 } 0193 } 0194 0195 void DolphinSearchBox::hideEvent(QHideEvent *event) 0196 { 0197 Q_UNUSED(event) 0198 m_startedSearching = false; 0199 m_startSearchTimer->stop(); 0200 } 0201 0202 void DolphinSearchBox::keyReleaseEvent(QKeyEvent *event) 0203 { 0204 QWidget::keyReleaseEvent(event); 0205 if (event->key() == Qt::Key_Escape) { 0206 if (m_searchInput->text().isEmpty()) { 0207 emitCloseRequest(); 0208 } else { 0209 m_searchInput->clear(); 0210 } 0211 } else if (event->key() == Qt::Key_Down) { 0212 Q_EMIT focusViewRequest(); 0213 } 0214 } 0215 0216 bool DolphinSearchBox::eventFilter(QObject *obj, QEvent *event) 0217 { 0218 switch (event->type()) { 0219 case QEvent::FocusIn: 0220 // #379135: we get the FocusIn event when we close a tab but we don't want to emit 0221 // the activated() signal before the removeTab() call in DolphinTabWidget::closeTab() returns. 0222 // To avoid this issue, we delay the activation of the search box. 0223 // We also don't want to schedule the activation process if we are already active, 0224 // otherwise we can enter in a loop of FocusIn/FocusOut events with the searchbox of another tab. 0225 if (!isActive()) { 0226 QTimer::singleShot(0, this, [this] { 0227 setActive(true); 0228 setFocus(); 0229 }); 0230 } 0231 break; 0232 0233 default: 0234 break; 0235 } 0236 0237 return QObject::eventFilter(obj, event); 0238 } 0239 0240 void DolphinSearchBox::emitSearchRequest() 0241 { 0242 m_startSearchTimer->stop(); 0243 m_startedSearching = true; 0244 m_saveSearchAction->setEnabled(true); 0245 Q_EMIT searchRequest(); 0246 } 0247 0248 void DolphinSearchBox::emitCloseRequest() 0249 { 0250 m_startSearchTimer->stop(); 0251 m_startedSearching = false; 0252 m_saveSearchAction->setEnabled(false); 0253 Q_EMIT closeRequest(); 0254 } 0255 0256 void DolphinSearchBox::slotConfigurationChanged() 0257 { 0258 saveSettings(); 0259 if (m_startedSearching) { 0260 emitSearchRequest(); 0261 } 0262 } 0263 0264 void DolphinSearchBox::slotSearchTextChanged(const QString &text) 0265 { 0266 if (text.isEmpty()) { 0267 // Restore URL when search box is cleared by closing and reopening the box. 0268 emitCloseRequest(); 0269 Q_EMIT openRequest(); 0270 } else { 0271 m_startSearchTimer->start(); 0272 } 0273 Q_EMIT searchTextChanged(text); 0274 } 0275 0276 void DolphinSearchBox::slotReturnPressed() 0277 { 0278 if (m_searchInput->text().isEmpty()) { 0279 return; 0280 } 0281 0282 emitSearchRequest(); 0283 Q_EMIT focusViewRequest(); 0284 } 0285 0286 void DolphinSearchBox::slotFacetChanged() 0287 { 0288 m_startedSearching = true; 0289 m_startSearchTimer->stop(); 0290 Q_EMIT searchRequest(); 0291 } 0292 0293 void DolphinSearchBox::slotSearchSaved() 0294 { 0295 const QUrl searchURL = urlForSearching(); 0296 if (searchURL.isValid()) { 0297 const QString label = i18n("Search for %1 in %2", text(), searchPath().fileName()); 0298 DolphinPlacesModelSingleton::instance().placesModel()->addPlace(label, searchURL, QStringLiteral("folder-saved-search-symbolic")); 0299 } 0300 } 0301 0302 void DolphinSearchBox::initButton(QToolButton *button) 0303 { 0304 button->installEventFilter(this); 0305 button->setAutoExclusive(true); 0306 button->setAutoRaise(true); 0307 button->setCheckable(true); 0308 connect(button, &QToolButton::clicked, this, &DolphinSearchBox::slotConfigurationChanged); 0309 } 0310 0311 void DolphinSearchBox::loadSettings() 0312 { 0313 if (SearchSettings::location() == QLatin1String("Everywhere")) { 0314 m_everywhereButton->setChecked(true); 0315 } else { 0316 m_fromHereButton->setChecked(true); 0317 } 0318 0319 if (SearchSettings::what() == QLatin1String("Content")) { 0320 m_contentButton->setChecked(true); 0321 } else { 0322 m_fileNameButton->setChecked(true); 0323 } 0324 0325 updateFacetsVisible(); 0326 } 0327 0328 void DolphinSearchBox::saveSettings() 0329 { 0330 SearchSettings::setLocation(m_fromHereButton->isChecked() ? QStringLiteral("FromHere") : QStringLiteral("Everywhere")); 0331 SearchSettings::setWhat(m_fileNameButton->isChecked() ? QStringLiteral("FileName") : QStringLiteral("Content")); 0332 SearchSettings::self()->save(); 0333 } 0334 0335 void DolphinSearchBox::init() 0336 { 0337 // Create search box 0338 m_searchInput = new QLineEdit(this); 0339 m_searchInput->setPlaceholderText(i18n("Search…")); 0340 m_searchInput->installEventFilter(this); 0341 m_searchInput->setClearButtonEnabled(true); 0342 m_searchInput->setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); 0343 connect(m_searchInput, &QLineEdit::returnPressed, this, &DolphinSearchBox::slotReturnPressed); 0344 connect(m_searchInput, &QLineEdit::textChanged, this, &DolphinSearchBox::slotSearchTextChanged); 0345 setFocusProxy(m_searchInput); 0346 0347 // Add "Save search" button inside search box 0348 m_saveSearchAction = new QAction(this); 0349 m_saveSearchAction->setIcon(QIcon::fromTheme(QStringLiteral("document-save-symbolic"))); 0350 m_saveSearchAction->setText(i18nc("action:button", "Save this search to quickly access it again in the future")); 0351 m_saveSearchAction->setEnabled(false); 0352 m_searchInput->addAction(m_saveSearchAction, QLineEdit::TrailingPosition); 0353 connect(m_saveSearchAction, &QAction::triggered, this, &DolphinSearchBox::slotSearchSaved); 0354 0355 // Create close button 0356 QToolButton *closeButton = new QToolButton(this); 0357 closeButton->setAutoRaise(true); 0358 closeButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); 0359 closeButton->setToolTip(i18nc("@info:tooltip", "Quit searching")); 0360 connect(closeButton, &QToolButton::clicked, this, &DolphinSearchBox::emitCloseRequest); 0361 0362 // Apply layout for the search input 0363 QHBoxLayout *searchInputLayout = new QHBoxLayout(); 0364 searchInputLayout->setContentsMargins(0, 0, 0, 0); 0365 searchInputLayout->addWidget(m_searchInput); 0366 searchInputLayout->addWidget(closeButton); 0367 0368 // Create "Filename" and "Content" button 0369 m_fileNameButton = new QToolButton(this); 0370 m_fileNameButton->setText(i18nc("action:button", "Filename")); 0371 initButton(m_fileNameButton); 0372 0373 m_contentButton = new QToolButton(); 0374 m_contentButton->setText(i18nc("action:button", "Content")); 0375 initButton(m_contentButton); 0376 0377 QButtonGroup *searchWhatGroup = new QButtonGroup(this); 0378 searchWhatGroup->addButton(m_fileNameButton); 0379 searchWhatGroup->addButton(m_contentButton); 0380 0381 m_separator = new KSeparator(Qt::Vertical, this); 0382 0383 // Create "From Here" and "Your files" buttons 0384 m_fromHereButton = new QToolButton(this); 0385 m_fromHereButton->setText(i18nc("action:button", "From Here")); 0386 initButton(m_fromHereButton); 0387 0388 m_everywhereButton = new QToolButton(this); 0389 m_everywhereButton->setText(i18nc("action:button", "Your files")); 0390 m_everywhereButton->setToolTip(i18nc("action:button", "Search in your home directory")); 0391 m_everywhereButton->setIcon(QIcon::fromTheme(QStringLiteral("user-home"))); 0392 m_everywhereButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); 0393 initButton(m_everywhereButton); 0394 0395 QButtonGroup *searchLocationGroup = new QButtonGroup(this); 0396 searchLocationGroup->addButton(m_fromHereButton); 0397 searchLocationGroup->addButton(m_everywhereButton); 0398 0399 KService::Ptr kfind = KService::serviceByDesktopName(QStringLiteral("org.kde.kfind")); 0400 0401 QToolButton *kfindToolsButton = nullptr; 0402 if (kfind) { 0403 kfindToolsButton = new QToolButton(this); 0404 kfindToolsButton->setAutoRaise(true); 0405 kfindToolsButton->setPopupMode(QToolButton::InstantPopup); 0406 kfindToolsButton->setIcon(QIcon::fromTheme("arrow-down-double")); 0407 kfindToolsButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); 0408 kfindToolsButton->setText(i18n("Open %1", kfind->name())); 0409 kfindToolsButton->setIcon(QIcon::fromTheme(kfind->icon())); 0410 0411 connect(kfindToolsButton, &QToolButton::clicked, this, [this, kfind] { 0412 auto *job = new KIO::ApplicationLauncherJob(kfind); 0413 job->setUrls({m_searchPath}); 0414 job->start(); 0415 }); 0416 } 0417 0418 // Create "Facets" widget 0419 m_facetsWidget = new DolphinFacetsWidget(this); 0420 m_facetsWidget->installEventFilter(this); 0421 m_facetsWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); 0422 m_facetsWidget->layout()->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); 0423 connect(m_facetsWidget, &DolphinFacetsWidget::facetChanged, this, &DolphinSearchBox::slotFacetChanged); 0424 0425 // Put the options into a QScrollArea. This prevents increasing the view width 0426 // in case that not enough width for the options is available. 0427 QWidget *optionsContainer = new QWidget(this); 0428 0429 // Apply layout for the options 0430 QHBoxLayout *optionsLayout = new QHBoxLayout(optionsContainer); 0431 optionsLayout->setContentsMargins(0, 0, 0, 0); 0432 optionsLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); 0433 optionsLayout->addWidget(m_fileNameButton); 0434 optionsLayout->addWidget(m_contentButton); 0435 optionsLayout->addWidget(m_separator); 0436 optionsLayout->addWidget(m_fromHereButton); 0437 optionsLayout->addWidget(m_everywhereButton); 0438 optionsLayout->addWidget(new KSeparator(Qt::Vertical, this)); 0439 if (kfindToolsButton) { 0440 optionsLayout->addWidget(kfindToolsButton); 0441 } 0442 optionsLayout->addStretch(1); 0443 0444 m_optionsScrollArea = new QScrollArea(this); 0445 m_optionsScrollArea->setFrameShape(QFrame::NoFrame); 0446 m_optionsScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0447 m_optionsScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); 0448 m_optionsScrollArea->setMaximumHeight(optionsContainer->sizeHint().height()); 0449 m_optionsScrollArea->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); 0450 m_optionsScrollArea->setWidget(optionsContainer); 0451 m_optionsScrollArea->setWidgetResizable(true); 0452 0453 m_topLayout = new QVBoxLayout(this); 0454 m_topLayout->setContentsMargins(0, Dolphin::LAYOUT_SPACING_SMALL, 0, 0); 0455 m_topLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); 0456 m_topLayout->addLayout(searchInputLayout); 0457 m_topLayout->addWidget(m_optionsScrollArea); 0458 m_topLayout->addWidget(m_facetsWidget); 0459 0460 loadSettings(); 0461 0462 // The searching should be started automatically after the user did not change 0463 // the text for a while 0464 m_startSearchTimer = new QTimer(this); 0465 m_startSearchTimer->setSingleShot(true); 0466 m_startSearchTimer->setInterval(500); 0467 connect(m_startSearchTimer, &QTimer::timeout, this, &DolphinSearchBox::emitSearchRequest); 0468 } 0469 0470 QString DolphinSearchBox::queryTitle(const QString &text) const 0471 { 0472 return i18nc("@title UDS_DISPLAY_NAME for a KIO directory listing. %1 is the query the user entered.", "Query Results from '%1'", text); 0473 } 0474 0475 QUrl DolphinSearchBox::balooUrlForSearching() const 0476 { 0477 #if HAVE_BALOO 0478 const QString text = m_searchInput->text(); 0479 0480 Baloo::Query query; 0481 query.addType(m_facetsWidget->facetType()); 0482 0483 QStringList queryStrings = m_facetsWidget->searchTerms(); 0484 0485 if (m_contentButton->isChecked()) { 0486 queryStrings << text; 0487 } else if (!text.isEmpty()) { 0488 queryStrings << QStringLiteral("filename:\"%1\"").arg(text); 0489 } 0490 0491 if (m_fromHereButton->isChecked()) { 0492 query.setIncludeFolder(m_searchPath.toLocalFile()); 0493 } 0494 0495 query.setSearchString(queryStrings.join(QLatin1Char(' '))); 0496 0497 return query.toSearchUrl(queryTitle(text)); 0498 #else 0499 return QUrl(); 0500 #endif 0501 } 0502 0503 void DolphinSearchBox::updateFromQuery(const DolphinQuery &query) 0504 { 0505 // Block all signals to avoid unnecessary "searchRequest" signals 0506 // while we adjust the search text and the facet widget. 0507 blockSignals(true); 0508 0509 const QString customDir = query.includeFolder(); 0510 if (!customDir.isEmpty()) { 0511 setSearchPath(QUrl::fromLocalFile(customDir)); 0512 } else { 0513 setSearchPath(QUrl::fromLocalFile(QDir::homePath())); 0514 } 0515 0516 setText(query.text()); 0517 0518 if (query.hasContentSearch()) { 0519 m_contentButton->setChecked(true); 0520 } else if (query.hasFileName()) { 0521 m_fileNameButton->setChecked(true); 0522 } 0523 0524 m_facetsWidget->resetSearchTerms(); 0525 m_facetsWidget->setFacetType(query.type()); 0526 const QStringList searchTerms = query.searchTerms(); 0527 for (const QString &searchTerm : searchTerms) { 0528 m_facetsWidget->setSearchTerm(searchTerm); 0529 } 0530 0531 m_startSearchTimer->stop(); 0532 blockSignals(false); 0533 } 0534 0535 void DolphinSearchBox::updateFacetsVisible() 0536 { 0537 const bool indexingEnabled = isIndexingEnabled(); 0538 m_facetsWidget->setEnabled(indexingEnabled); 0539 m_facetsWidget->setVisible(indexingEnabled); 0540 } 0541 0542 bool DolphinSearchBox::isIndexingEnabled() const 0543 { 0544 #if HAVE_BALOO 0545 const Baloo::IndexerConfig searchInfo; 0546 return searchInfo.fileIndexingEnabled() && !searchPath().isEmpty() && searchInfo.shouldBeIndexed(searchPath().toLocalFile()); 0547 #else 0548 return false; 0549 #endif 0550 } 0551 0552 #include "moc_dolphinsearchbox.cpp"