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"