diff options
| author | Felix Ernst <[email protected]> | 2025-04-07 21:09:00 +0000 |
|---|---|---|
| committer | Felix Ernst <[email protected]> | 2025-04-07 21:09:00 +0000 |
| commit | 4102ccb80457eea44ea280f0ace2a419602bc34b (patch) | |
| tree | 841e039cf9864276c968a397a2ae75c363199342 /src/search/dolphinsearchbox.cpp | |
| parent | bfc177d3d1bc5a4a241e35d59086e4824e7c0bd3 (diff) | |
Rewrite search integration
This huge commit is a nearly complete rewrite of the Dolphin search
code. It implements most of the improved Dolphin search UI/UX as
designed and discussed in a collaborative effort by Kristen McWilliam,
Jin Liu, Andy Betts, Tagwerk, a few others and me.
See https://invent.kde.org/system/dolphin/-/issues/46.
# Notable changes
- A toggle to change the search tool is provided as most contributors
deemed that useful in
https://invent.kde.org/system/dolphin/-/merge_requests/642#note_985112.
- The default search is changed to filenamesearch for maximum
reliability.
- Removing all search parameters will take users back to the view state
prior to starting a search instead of keeping the search results open.
- The UI for choosing file types or modification dates has been made
more powerful with more granularity and more options.
- Most search parameters can be configured from a popup menu which
gives us extra space for extra clarity.
- Labels and help buttons as well as hyperlinks to settings makes sure
the user always knows why some search parameters are unavailable in
some contexts.
- Chips show important search parameters while the popup is closed.
They allow quickly removing filters.
- The titles of the search and the input field placeholder message
change to make clear whether file names or file contents are searched.
- When the user actively switches the search tool, whether content
should be searched, or whether to search everywhere, this is preserved
for the initial state of the search bar when the user opens it the next
time after restarting Dolphin.
# Architecture
- The new DolphinQuery class is independent of the UI and contains all
search parameters modifiable in Dolphin as easy setters and getters.
- DolphinQuery objects are also used to update the states of every
component in the search UI. There is now a clear separation of UI and
search configuration/DolphinQuery.
- DolphinQuery is responsible for exporting to and importing from
search URLs.
- The search UI always reflects the currently configured DolphinQuery
no matter if the user changed the UI to change the DolphinQuery or
loaded a DolphinQuery/older search URL which then is reflected in the
UI.
- I tried to simplify all classes and their interaction between each
other as much as possible.
- I added some tests
BUG: 386754
CCBUG: 435119
CCBUG: 458761
BUG: 446387
BUG: 470136
CCBUG: 471556
CCBUG: 475439
CCBUG: 477969
BUG: 480001
BUG: 483578
BUG: 488047
BUG: 488845
BUG: 500103
FIXED-IN: 25.08
Diffstat (limited to 'src/search/dolphinsearchbox.cpp')
| -rw-r--r-- | src/search/dolphinsearchbox.cpp | 575 |
1 files changed, 0 insertions, 575 deletions
diff --git a/src/search/dolphinsearchbox.cpp b/src/search/dolphinsearchbox.cpp deleted file mode 100644 index ee425501d..000000000 --- a/src/search/dolphinsearchbox.cpp +++ /dev/null @@ -1,575 +0,0 @@ -/* - * SPDX-FileCopyrightText: 2010 Peter Penz <[email protected]> - * - * SPDX-License-Identifier: GPL-2.0-or-later - */ - -#include "dolphinsearchbox.h" -#include "global.h" - -#include "dolphin_searchsettings.h" -#include "dolphinfacetswidget.h" -#include "dolphinplacesmodelsingleton.h" -#include "dolphinquery.h" - -#include "config-dolphin.h" -#include <KIO/ApplicationLauncherJob> -#include <KLocalizedString> -#include <KSeparator> -#include <KService> -#if HAVE_BALOO -#include <Baloo/IndexerConfig> -#include <Baloo/Query> -#endif - -#include <QButtonGroup> -#include <QDir> -#include <QFontDatabase> -#include <QHBoxLayout> -#include <QIcon> -#include <QKeyEvent> -#include <QLineEdit> -#include <QScrollArea> -#include <QShowEvent> -#include <QTimer> -#include <QToolButton> -#include <QUrlQuery> - -DolphinSearchBox::DolphinSearchBox(QWidget *parent) - : AnimatedHeightWidget(parent) - , m_startedSearching(false) - , m_active(true) - , m_topLayout(nullptr) - , m_searchInput(nullptr) - , m_saveSearchAction(nullptr) - , m_optionsScrollArea(nullptr) - , m_fileNameButton(nullptr) - , m_contentButton(nullptr) - , m_separator(nullptr) - , m_fromHereButton(nullptr) - , m_everywhereButton(nullptr) - , m_facetsWidget(nullptr) - , m_searchPath() - , m_startSearchTimer(nullptr) - , m_initialized(false) -{ -} - -DolphinSearchBox::~DolphinSearchBox() -{ - saveSettings(); -} - -void DolphinSearchBox::setText(const QString &text) -{ - if (m_searchInput->text() != text) { - m_searchInput->setText(text); - } -} - -QString DolphinSearchBox::text() const -{ - return m_searchInput->text(); -} - -void DolphinSearchBox::setSearchPath(const QUrl &url) -{ - if (url == m_searchPath || !m_initialized) { - return; - } - - const QUrl cleanedUrl = url.adjusted(QUrl::RemoveUserInfo | QUrl::StripTrailingSlash); - - if (cleanedUrl.path() == QDir::homePath()) { - m_fromHereButton->setChecked(false); - m_everywhereButton->setChecked(true); - if (!m_searchPath.isEmpty()) { - return; - } - } else { - m_everywhereButton->setChecked(false); - m_fromHereButton->setChecked(true); - } - - m_searchPath = url; - - QFontMetrics metrics(m_fromHereButton->font()); - const int maxWidth = metrics.height() * 8; - - QString location = cleanedUrl.fileName(); - if (location.isEmpty()) { - location = cleanedUrl.toString(QUrl::PreferLocalFile); - } - const QString elidedLocation = metrics.elidedText(location, Qt::ElideMiddle, maxWidth); - m_fromHereButton->setText(i18nc("action:button", "From Here (%1)", elidedLocation)); - m_fromHereButton->setToolTip(i18nc("action:button", "Limit search to '%1' and its subfolders", cleanedUrl.toString(QUrl::PreferLocalFile))); -} - -QUrl DolphinSearchBox::searchPath() const -{ - return m_everywhereButton->isChecked() ? QUrl::fromLocalFile(QDir::homePath()) : m_searchPath; -} - -QUrl DolphinSearchBox::urlForSearching() const -{ - QUrl url; - - if (isIndexingEnabled()) { - url = balooUrlForSearching(); - } else { - url.setScheme(QStringLiteral("filenamesearch")); - - QUrlQuery query; - query.addQueryItem(QStringLiteral("search"), QUrl::toPercentEncoding(m_searchInput->text())); - if (m_contentButton->isChecked()) { - query.addQueryItem(QStringLiteral("checkContent"), QStringLiteral("yes")); - } - - query.addQueryItem(QStringLiteral("url"), QUrl::toPercentEncoding(searchPath().url())); - query.addQueryItem(QStringLiteral("title"), QUrl::toPercentEncoding(queryTitle(m_searchInput->text()))); - - url.setQuery(query); - } - - return url; -} - -void DolphinSearchBox::fromSearchUrl(const QUrl &url) -{ - if (DolphinQuery::supportsScheme(url.scheme())) { - const DolphinQuery query = DolphinQuery::fromSearchUrl(url); - updateFromQuery(query); - } else if (url.scheme() == QLatin1String("filenamesearch")) { - const QUrlQuery query(url); - setText(query.queryItemValue(QStringLiteral("search"), QUrl::FullyDecoded)); - if (m_searchPath.scheme() != url.scheme()) { - m_searchPath = QUrl(); - } - setSearchPath(QUrl::fromUserInput(query.queryItemValue(QStringLiteral("url"), QUrl::FullyDecoded), QString(), QUrl::AssumeLocalFile)); - m_contentButton->setChecked(query.queryItemValue(QStringLiteral("checkContent")) == QLatin1String("yes")); - } else { - setText(QString()); - m_searchPath = QUrl(); - setSearchPath(url); - } - - updateFacetsVisible(); -} - -void DolphinSearchBox::selectAll() -{ - m_searchInput->selectAll(); -} - -void DolphinSearchBox::setActive(bool active) -{ - if (active != m_active) { - m_active = active; - - if (active) { - Q_EMIT activated(); - } - } -} - -bool DolphinSearchBox::isActive() const -{ - return m_active; -} - -void DolphinSearchBox::setVisible(bool visible, Animated animated) -{ - if (visible) { - init(); - } - AnimatedHeightWidget::setVisible(visible, animated); -} - -void DolphinSearchBox::showEvent(QShowEvent *event) -{ - if (!event->spontaneous()) { - m_searchInput->setFocus(); - m_startedSearching = false; - } -} - -void DolphinSearchBox::hideEvent(QHideEvent *event) -{ - Q_UNUSED(event) - m_startedSearching = false; - if (m_startSearchTimer) { - m_startSearchTimer->stop(); - } -} - -void DolphinSearchBox::keyReleaseEvent(QKeyEvent *event) -{ - QWidget::keyReleaseEvent(event); - if (event->key() == Qt::Key_Escape) { - if (m_searchInput->text().isEmpty()) { - emitCloseRequest(); - } else { - m_searchInput->clear(); - } - } else if (event->key() == Qt::Key_Down) { - Q_EMIT focusViewRequest(); - } -} - -bool DolphinSearchBox::eventFilter(QObject *obj, QEvent *event) -{ - switch (event->type()) { - case QEvent::FocusIn: - // #379135: we get the FocusIn event when we close a tab but we don't want to emit - // the activated() signal before the removeTab() call in DolphinTabWidget::closeTab() returns. - // To avoid this issue, we delay the activation of the search box. - // We also don't want to schedule the activation process if we are already active, - // otherwise we can enter in a loop of FocusIn/FocusOut events with the searchbox of another tab. - if (!isActive()) { - QTimer::singleShot(0, this, [this] { - setActive(true); - setFocus(); - }); - } - break; - - default: - break; - } - - return QObject::eventFilter(obj, event); -} - -void DolphinSearchBox::emitSearchRequest() -{ - m_startSearchTimer->stop(); - m_startedSearching = true; - m_saveSearchAction->setEnabled(true); - Q_EMIT searchRequest(); -} - -void DolphinSearchBox::emitCloseRequest() -{ - m_startSearchTimer->stop(); - m_startedSearching = false; - m_saveSearchAction->setEnabled(false); - Q_EMIT closeRequest(); -} - -void DolphinSearchBox::slotConfigurationChanged() -{ - saveSettings(); - if (m_startedSearching) { - emitSearchRequest(); - } -} - -void DolphinSearchBox::slotSearchTextChanged(const QString &text) -{ - if (text.isEmpty()) { - // Restore URL when search box is cleared by closing and reopening the box. - emitCloseRequest(); - Q_EMIT openRequest(); - } else { - m_startSearchTimer->start(); - } - Q_EMIT searchTextChanged(text); -} - -void DolphinSearchBox::slotReturnPressed() -{ - if (m_searchInput->text().isEmpty()) { - return; - } - - emitSearchRequest(); - Q_EMIT focusViewRequest(); -} - -void DolphinSearchBox::slotFacetChanged() -{ - m_startedSearching = true; - m_startSearchTimer->stop(); - Q_EMIT searchRequest(); -} - -void DolphinSearchBox::slotSearchSaved() -{ - const QUrl searchURL = urlForSearching(); - if (searchURL.isValid()) { - const QString label = i18n("Search for %1 in %2", text(), searchPath().fileName()); - DolphinPlacesModelSingleton::instance().placesModel()->addPlace(label, searchURL, QStringLiteral("folder-saved-search-symbolic")); - } -} - -void DolphinSearchBox::initButton(QToolButton *button) -{ - button->installEventFilter(this); - button->setAutoExclusive(true); - button->setAutoRaise(true); - button->setCheckable(true); - connect(button, &QToolButton::clicked, this, &DolphinSearchBox::slotConfigurationChanged); -} - -void DolphinSearchBox::loadSettings() -{ - if (SearchSettings::location() == QLatin1String("Everywhere")) { - m_everywhereButton->setChecked(true); - } else { - m_fromHereButton->setChecked(true); - } - - if (SearchSettings::what() == QLatin1String("Content")) { - m_contentButton->setChecked(true); - } else { - m_fileNameButton->setChecked(true); - } - - updateFacetsVisible(); -} - -void DolphinSearchBox::saveSettings() -{ - if (m_initialized) { - SearchSettings::setLocation(m_fromHereButton->isChecked() ? QStringLiteral("FromHere") : QStringLiteral("Everywhere")); - SearchSettings::setWhat(m_fileNameButton->isChecked() ? QStringLiteral("FileName") : QStringLiteral("Content")); - SearchSettings::self()->save(); - } -} - -void DolphinSearchBox::init() -{ - if (m_initialized) { - return; // This object is already initialised. - } - - QWidget *contentsContainer = prepareContentsContainer(); - - // Create search box - m_searchInput = new QLineEdit(contentsContainer); - m_searchInput->setPlaceholderText(i18n("Search…")); - m_searchInput->installEventFilter(this); - m_searchInput->setClearButtonEnabled(true); - m_searchInput->setFont(QFontDatabase::systemFont(QFontDatabase::GeneralFont)); - connect(m_searchInput, &QLineEdit::returnPressed, this, &DolphinSearchBox::slotReturnPressed); - connect(m_searchInput, &QLineEdit::textChanged, this, &DolphinSearchBox::slotSearchTextChanged); - setFocusProxy(m_searchInput); - - // Add "Save search" button inside search box - m_saveSearchAction = new QAction(this); - m_saveSearchAction->setIcon(QIcon::fromTheme(QStringLiteral("document-save-symbolic"))); - m_saveSearchAction->setText(i18nc("action:button", "Save this search to quickly access it again in the future")); - m_saveSearchAction->setEnabled(false); - m_searchInput->addAction(m_saveSearchAction, QLineEdit::TrailingPosition); - connect(m_saveSearchAction, &QAction::triggered, this, &DolphinSearchBox::slotSearchSaved); - - // Create close button - QToolButton *closeButton = new QToolButton(contentsContainer); - closeButton->setAutoRaise(true); - closeButton->setIcon(QIcon::fromTheme(QStringLiteral("dialog-close"))); - closeButton->setToolTip(i18nc("@info:tooltip", "Quit searching")); - connect(closeButton, &QToolButton::clicked, this, &DolphinSearchBox::emitCloseRequest); - - // Apply layout for the search input - QHBoxLayout *searchInputLayout = new QHBoxLayout(); - searchInputLayout->setContentsMargins(0, 0, 0, 0); - searchInputLayout->addWidget(m_searchInput); - searchInputLayout->addWidget(closeButton); - - // Create "Filename" and "Content" button - m_fileNameButton = new QToolButton(contentsContainer); - m_fileNameButton->setText(i18nc("action:button", "Filename")); - initButton(m_fileNameButton); - - m_contentButton = new QToolButton(); - m_contentButton->setText(i18nc("action:button", "Content")); - initButton(m_contentButton); - - QButtonGroup *searchWhatGroup = new QButtonGroup(contentsContainer); - searchWhatGroup->addButton(m_fileNameButton); - searchWhatGroup->addButton(m_contentButton); - - m_separator = new KSeparator(Qt::Vertical, contentsContainer); - - // Create "From Here" and "Your files" buttons - m_fromHereButton = new QToolButton(contentsContainer); - m_fromHereButton->setText(i18nc("action:button", "From Here")); - initButton(m_fromHereButton); - - m_everywhereButton = new QToolButton(contentsContainer); - m_everywhereButton->setText(i18nc("action:button", "Your files")); - m_everywhereButton->setToolTip(i18nc("action:button", "Search in your home directory")); - m_everywhereButton->setIcon(QIcon::fromTheme(QStringLiteral("user-home"))); - m_everywhereButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - initButton(m_everywhereButton); - - QButtonGroup *searchLocationGroup = new QButtonGroup(contentsContainer); - searchLocationGroup->addButton(m_fromHereButton); - searchLocationGroup->addButton(m_everywhereButton); - - KService::Ptr kfind = KService::serviceByDesktopName(QStringLiteral("org.kde.kfind")); - - QToolButton *kfindToolsButton = nullptr; - if (kfind) { - kfindToolsButton = new QToolButton(contentsContainer); - kfindToolsButton->setAutoRaise(true); - kfindToolsButton->setPopupMode(QToolButton::InstantPopup); - kfindToolsButton->setIcon(QIcon::fromTheme("arrow-down-double")); - kfindToolsButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon); - kfindToolsButton->setText(i18n("Open %1", kfind->name())); - kfindToolsButton->setIcon(QIcon::fromTheme(kfind->icon())); - - connect(kfindToolsButton, &QToolButton::clicked, this, [this, kfind] { - auto *job = new KIO::ApplicationLauncherJob(kfind); - job->setUrls({m_searchPath}); - job->start(); - }); - } - - // Create "Facets" widget - m_facetsWidget = new DolphinFacetsWidget(contentsContainer); - m_facetsWidget->installEventFilter(this); - m_facetsWidget->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Maximum); - m_facetsWidget->layout()->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); - connect(m_facetsWidget, &DolphinFacetsWidget::facetChanged, this, &DolphinSearchBox::slotFacetChanged); - - // Put the options into a QScrollArea. This prevents increasing the view width - // in case that not enough width for the options is available. - QWidget *optionsContainer = new QWidget(contentsContainer); - - // Apply layout for the options - QHBoxLayout *optionsLayout = new QHBoxLayout(optionsContainer); - optionsLayout->setContentsMargins(0, 0, 0, 0); - optionsLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); - optionsLayout->addWidget(m_fileNameButton); - optionsLayout->addWidget(m_contentButton); - optionsLayout->addWidget(m_separator); - optionsLayout->addWidget(m_fromHereButton); - optionsLayout->addWidget(m_everywhereButton); - optionsLayout->addWidget(new KSeparator(Qt::Vertical, contentsContainer)); - if (kfindToolsButton) { - optionsLayout->addWidget(kfindToolsButton); - } - optionsLayout->addStretch(1); - - m_optionsScrollArea = new QScrollArea(contentsContainer); - m_optionsScrollArea->setFrameShape(QFrame::NoFrame); - m_optionsScrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_optionsScrollArea->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); - m_optionsScrollArea->setMaximumHeight(optionsContainer->sizeHint().height()); - m_optionsScrollArea->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Fixed); - m_optionsScrollArea->setWidget(optionsContainer); - m_optionsScrollArea->setWidgetResizable(true); - - m_topLayout = new QVBoxLayout(contentsContainer); - m_topLayout->setContentsMargins(0, Dolphin::LAYOUT_SPACING_SMALL, 0, 0); - m_topLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL); - m_topLayout->addLayout(searchInputLayout); - m_topLayout->addWidget(m_optionsScrollArea); - m_topLayout->addWidget(m_facetsWidget); - - loadSettings(); - - // The searching should be started automatically after the user did not change - // the text for a while - m_startSearchTimer = new QTimer(this); - m_startSearchTimer->setSingleShot(true); - m_startSearchTimer->setInterval(500); - connect(m_startSearchTimer, &QTimer::timeout, this, &DolphinSearchBox::emitSearchRequest); - - m_initialized = true; -} - -QString DolphinSearchBox::queryTitle(const QString &text) const -{ - return i18nc("@title UDS_DISPLAY_NAME for a KIO directory listing. %1 is the query the user entered.", "Query Results from '%1'", text); -} - -QUrl DolphinSearchBox::balooUrlForSearching() const -{ -#if HAVE_BALOO - const QString text = m_searchInput->text(); - - Baloo::Query query; - query.addType(m_facetsWidget->facetType()); - - QStringList queryStrings = m_facetsWidget->searchTerms(); - - if (m_contentButton->isChecked()) { - queryStrings << text; - } else if (!text.isEmpty()) { - queryStrings << QStringLiteral("filename:\"%1\"").arg(text); - } - - if (m_fromHereButton->isChecked()) { - query.setIncludeFolder(m_searchPath.toLocalFile()); - } - - query.setSearchString(queryStrings.join(QLatin1Char(' '))); - - return query.toSearchUrl(queryTitle(text)); -#else - return QUrl(); -#endif -} - -void DolphinSearchBox::updateFromQuery(const DolphinQuery &query) -{ - // Block all signals to avoid unnecessary "searchRequest" signals - // while we adjust the search text and the facet widget. - blockSignals(true); - - const QString customDir = query.includeFolder(); - if (!customDir.isEmpty()) { - setSearchPath(QUrl::fromLocalFile(customDir)); - } else { - setSearchPath(QUrl::fromLocalFile(QDir::homePath())); - } - - setText(query.text()); - - if (query.hasContentSearch()) { - m_contentButton->setChecked(true); - } else if (query.hasFileName()) { - m_fileNameButton->setChecked(true); - } - - m_facetsWidget->resetSearchTerms(); - m_facetsWidget->setFacetType(query.type()); - const QStringList searchTerms = query.searchTerms(); - for (const QString &searchTerm : searchTerms) { - m_facetsWidget->setSearchTerm(searchTerm); - } - - m_startSearchTimer->stop(); - blockSignals(false); -} - -void DolphinSearchBox::updateFacetsVisible() -{ - const bool indexingEnabled = isIndexingEnabled(); - m_facetsWidget->setEnabled(indexingEnabled); - m_facetsWidget->setVisible(indexingEnabled); - - // The m_facetsWidget might have changed visibility. We smoothly animate towards the updated height. - if (isVisible() && isEnabled()) { - setVisible(true, WithAnimation); - } -} - -bool DolphinSearchBox::isIndexingEnabled() const -{ -#if HAVE_BALOO - const Baloo::IndexerConfig searchInfo; - return searchInfo.fileIndexingEnabled() && !searchPath().isEmpty() && searchInfo.shouldBeIndexed(searchPath().toLocalFile()); -#else - return false; -#endif -} - -int DolphinSearchBox::preferredHeight() const -{ - return m_initialized ? m_topLayout->sizeHint().height() : 0; -} - -#include "moc_dolphinsearchbox.cpp" |
