┌   ┐
54
└   ┘

summaryrefslogtreecommitdiff
path: root/src/search/bar.cpp
diff options
context:
space:
mode:
Diffstat (limited to 'src/search/bar.cpp')
-rw-r--r--src/search/bar.cpp356
1 files changed, 356 insertions, 0 deletions
diff --git a/src/search/bar.cpp b/src/search/bar.cpp
new file mode 100644
index 000000000..18707ef23
--- /dev/null
+++ b/src/search/bar.cpp
@@ -0,0 +1,356 @@
+/*
+ SPDX-FileCopyrightText: 2010 Peter Penz <[email protected]>
+ SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]>
+
+ SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "bar.h"
+#include "global.h"
+
+#include "barsecondrowflowlayout.h"
+#include "chip.h"
+#include "dolphin_searchsettings.h"
+#include "dolphinplacesmodelsingleton.h"
+#include "dolphinquery.h"
+#include "popup.h"
+#include "widgetmenu.h"
+
+#include "config-dolphin.h"
+#include <KLocalizedString>
+
+#include <QApplication>
+#include <QHBoxLayout>
+#include <QIcon>
+#include <QKeyEvent>
+#include <QLineEdit>
+#include <QMenu>
+#include <QScrollArea>
+#include <QTimer>
+#include <QToolButton>
+
+using namespace Search;
+
+namespace
+{
+/**
+ * @see Bar::IsSearchConfigured().
+ */
+bool isSearchConfigured(const std::shared_ptr<const DolphinQuery> &searchConfiguration)
+{
+ return !searchConfiguration->searchTerm().isEmpty()
+ || (searchConfiguration->searchTool() != SearchTool::Filenamesearch
+ && (searchConfiguration->fileType() != KFileMetaData::Type::Empty || searchConfiguration->modifiedSinceDate().isValid()
+ || searchConfiguration->minimumRating() > 0 || !searchConfiguration->requiredTags().isEmpty()));
+};
+}
+
+Bar::Bar(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent)
+ : AnimatedHeightWidget(parent)
+ , UpdatableStateInterface{dolphinQuery}
+{
+ QWidget *contentsContainer = prepareContentsContainer();
+
+ // Create search box
+ m_searchTermEditor = new QLineEdit(contentsContainer);
+ m_searchTermEditor->setClearButtonEnabled(true);
+ connect(m_searchTermEditor, &QLineEdit::returnPressed, this, &Bar::slotReturnPressed);
+ connect(m_searchTermEditor, &QLineEdit::textEdited, this, &Bar::slotSearchTermEdited);
+ setFocusProxy(m_searchTermEditor);
+
+ // 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_searchTermEditor->addAction(m_saveSearchAction, QLineEdit::TrailingPosition);
+ connect(m_saveSearchAction, &QAction::triggered, this, &Bar::slotSaveSearch);
+
+ // Filter button
+ auto filterButton = new QToolButton(contentsContainer);
+ filterButton->setIcon(QIcon::fromTheme(QStringLiteral("view-filter")));
+ filterButton->setText(i18nc("@action:button for changing search options", "Filter"));
+ filterButton->setAutoRaise(true);
+ filterButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ filterButton->setPopupMode(QToolButton::InstantPopup);
+ filterButton->setAttribute(Qt::WA_CustomWhatsThis);
+ m_popup = new Popup{m_searchConfiguration, this};
+ connect(m_popup, &QMenu::aboutToShow, this, [this]() {
+ m_popup->updateStateToMatch(m_searchConfiguration);
+ });
+ connect(m_popup, &Popup::configurationChanged, this, &Bar::slotConfigurationChanged);
+ connect(m_popup, &Popup::showMessage, this, &Bar::showMessage);
+ connect(m_popup, &Popup::showInstallationProgress, this, &Bar::showInstallationProgress);
+ filterButton->setMenu(m_popup);
+
+ // 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, [this]() {
+ setVisible(false, WithAnimation);
+ });
+
+ // Apply layout for the search input row
+ QHBoxLayout *firstRowLayout = new QHBoxLayout{};
+ firstRowLayout->setContentsMargins(0, 0, 0, 0);
+ firstRowLayout->addWidget(m_searchTermEditor);
+ firstRowLayout->addWidget(filterButton);
+ firstRowLayout->addWidget(closeButton);
+
+ // Create "From Here" and "Your files" buttons
+ m_fromHereButton = new QToolButton(contentsContainer);
+ m_fromHereButton->setText(i18nc("action:button search from here", "Here"));
+ m_fromHereButton->setAutoRaise(true);
+ m_fromHereButton->setCheckable(true);
+ connect(m_fromHereButton, &QToolButton::clicked, this, [this]() {
+ if (m_searchConfiguration->searchLocations() == SearchLocations::FromHere) {
+ return; // Already selected.
+ }
+ SearchSettings::setLocation(QStringLiteral("FromHere"));
+ SearchSettings::self()->save();
+ DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+ searchConfigurationCopy.setSearchLocations(SearchLocations::FromHere);
+ slotConfigurationChanged(std::move(searchConfigurationCopy));
+ });
+
+ m_everywhereButton = new QToolButton(contentsContainer);
+ m_everywhereButton->setText(i18nc("action:button search everywhere", "Everywhere"));
+ m_everywhereButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+ m_everywhereButton->setAutoRaise(true);
+ m_everywhereButton->setCheckable(true);
+ connect(m_everywhereButton, &QToolButton::clicked, this, [this]() {
+ if (m_searchConfiguration->searchLocations() == SearchLocations::Everywhere) {
+ return; // Already selected.
+ }
+ SearchSettings::setLocation(QStringLiteral("Everywhere"));
+ SearchSettings::self()->save();
+ DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+ searchConfigurationCopy.setSearchLocations(SearchLocations::Everywhere);
+ slotConfigurationChanged(std::move(searchConfigurationCopy));
+ });
+
+ // Apply layout for the location buttons and chips row
+ m_secondRowLayout = new BarSecondRowFlowLayout{nullptr};
+ m_secondRowLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL);
+ connect(m_secondRowLayout, &BarSecondRowFlowLayout::heightHintChanged, this, [this]() {
+ if (isEnabled()) {
+ AnimatedHeightWidget::setVisible(true, WithAnimation);
+ }
+ // If this Search::Bar is not enabled we can safely assume that this widget is currently in an animation to hide itself and we do nothing.
+ });
+ m_secondRowLayout->addWidget(m_fromHereButton);
+ m_secondRowLayout->addWidget(m_everywhereButton);
+
+ m_topLayout = new QVBoxLayout(contentsContainer);
+ m_topLayout->setContentsMargins(Dolphin::LAYOUT_SPACING_SMALL, Dolphin::LAYOUT_SPACING_SMALL, Dolphin::LAYOUT_SPACING_SMALL, Dolphin::LAYOUT_SPACING_SMALL);
+ m_topLayout->setSpacing(Dolphin::LAYOUT_SPACING_SMALL);
+ m_topLayout->addLayout(firstRowLayout);
+ m_topLayout->addLayout(m_secondRowLayout);
+
+ setWhatsThis(xi18nc(
+ "@info:whatsthis search bar",
+ "<para>This helps you find files and folders.<list><item>Enter a <emphasis>search term</emphasis> in the input field.</item><item>Decide where to "
+ "search by pressing the location buttons below the search field. “Here” refers to the location that was open prior to starting a search, so navigating "
+ "to a different location first can narrow down the search.</item><item>Press the “%1” button to further refine the manner of searching or the "
+ "results.</item><item>Press the “Save” icon to add the current search configuration to the <emphasis>Places panel</emphasis>.</item></list></para>",
+ filterButton->text()));
+
+ // 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, &Bar::commitCurrentConfiguration);
+
+ updateStateToMatch(dolphinQuery);
+}
+
+QString Bar::text() const
+{
+ return m_searchTermEditor->text();
+}
+
+void Bar::setSearchPath(const QUrl &url)
+{
+ if (url == m_searchConfiguration->searchPath()) {
+ return;
+ }
+
+ DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+ searchConfigurationCopy.setSearchPath(url);
+ updateStateToMatch(std::make_shared<const DolphinQuery>(std::move(searchConfigurationCopy)));
+}
+
+void Bar::selectAll()
+{
+ m_searchTermEditor->setFocus();
+ m_searchTermEditor->selectAll();
+}
+
+void Bar::setVisible(bool visible, Animated animated)
+{
+ if (!visible) {
+ m_startSearchTimer->stop();
+ Q_EMIT urlChangeRequested(m_searchConfiguration->searchPath());
+ if (isAncestorOf(QApplication::focusWidget())) {
+ Q_EMIT focusViewRequest();
+ }
+ }
+ AnimatedHeightWidget::setVisible(visible, animated);
+ Q_EMIT visibilityChanged(visible);
+}
+
+void Bar::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery)
+{
+ m_searchTermEditor->setText(dolphinQuery->searchTerm());
+ // When the Popup is closed users might not know whether they are searching in file names or contents. This can be problematic when users do not find a
+ // file and then assume it doesn't exist. We consider searching for names matching the search term the default and only show a generic "Search…" text as
+ // the placeholder then. But when names are not searched we change the placeholder message to make this clear.
+ m_searchTermEditor->setPlaceholderText(dolphinQuery->searchTool() == SearchTool::Filenamesearch
+ && dolphinQuery->searchThrough() == SearchThrough::FileContents
+ ? i18nc("@info:placeholder", "Search in file contents…")
+ : i18n("Search…"));
+ m_saveSearchAction->setEnabled(::isSearchConfigured(dolphinQuery));
+ m_fromHereButton->setChecked(dolphinQuery->searchLocations() == SearchLocations::FromHere);
+ m_everywhereButton->setChecked(dolphinQuery->searchLocations() == SearchLocations::Everywhere);
+
+ if (m_popup && m_popup->isVisible()) {
+ // The user actually sees the popup, so update it now! Normally the popup is only updated when Popup::aboutToShow() is emitted.
+ m_popup->updateStateToMatch(dolphinQuery);
+ }
+
+ /// Update tooltip
+ const QUrl cleanedUrl = dolphinQuery->searchPath().adjusted(QUrl::RemoveUserInfo | QUrl::StripTrailingSlash);
+ m_fromHereButton->setToolTip(
+ xi18nc("@info:tooltip", "Limit the search to <filename>%1</filename> and its subfolders.", cleanedUrl.toString(QUrl::PreferLocalFile)));
+ m_everywhereButton->setToolTip(
+ dolphinQuery->searchTool() == SearchTool::Filenamesearch
+ // clang-format off
+ // clang-format is turned off because we need to make sure the i18n call is in a single row or the i18n comment above will not be extracted.
+ // See https://commits.kde.org/kxmlgui/a31135046e1b3335b5d7bbbe6aa9a883ce3284c1
+ // i18n: The "Everywhere" button makes Dolphin search all files in "/" recursively. "From the root up" is meant to
+ // communicate this colloquially while containing the technical term "root". It is fine to drop the technicalities here
+ // and only to communicate that everything in the file system is supposed to be searched here.
+ ? i18nc("@info:tooltip", "Search all directories from the root up.")
+ // i18n: Tooltip for "Everywhere" button as opposed to searching for files in specific folders. The search tool uses
+ // file indexing and will therefore only be able to search through directories which have been put into a data base.
+ // Please make sure your translation of the path to the Search settings page is identical to translation there.
+ : xi18nc("@info:tooltip", "Search all indexed locations.<nl/><nl/>Configure which locations are indexed in <interface>System Settings|Workspace|Search</interface>."));
+ // clang-format on
+
+ auto updateChip = [this, &dolphinQuery]<typename Selector>(bool shouldExist, Chip<Selector> *chip) -> Chip<Selector> * {
+ if (shouldExist) {
+ if (!chip) {
+ chip = new Chip<Selector>{dolphinQuery, nullptr};
+ chip->hide();
+ chip->setMaximumHeight(m_fromHereButton->height());
+ connect(chip, &ChipBase::configurationChanged, this, &Bar::slotConfigurationChanged);
+ m_secondRowLayout->addWidget(chip); // Transfers ownership
+ chip->show(); // Only showing the chip after it was added to the correct layout avoids a bug which shows the chip at the top of the bar.
+ } else {
+ chip->updateStateToMatch(dolphinQuery);
+ }
+ return chip;
+ }
+ if (chip) {
+ chip->deleteLater();
+ }
+ return nullptr;
+ };
+
+ m_fileTypeSelectorChip = updateChip.template operator()<FileTypeSelector>(dolphinQuery->searchTool() != SearchTool::Filenamesearch
+ && dolphinQuery->fileType() != KFileMetaData::Type::Empty,
+ m_fileTypeSelectorChip);
+ m_modifiedSinceDateSelectorChip =
+ updateChip.template operator()<DateSelector>(dolphinQuery->searchTool() != SearchTool::Filenamesearch && dolphinQuery->modifiedSinceDate().isValid(),
+ m_modifiedSinceDateSelectorChip);
+ m_minimumRatingSelectorChip =
+ updateChip.template operator()<MinimumRatingSelector>(dolphinQuery->searchTool() != SearchTool::Filenamesearch && dolphinQuery->minimumRating() > 0,
+ m_minimumRatingSelectorChip);
+ m_requiredTagsSelectorChip =
+ updateChip.template operator()<TagsSelector>(dolphinQuery->searchTool() != SearchTool::Filenamesearch && dolphinQuery->requiredTags().count(),
+ m_requiredTagsSelectorChip);
+}
+
+void Bar::keyPressEvent(QKeyEvent *event)
+{
+ QWidget::keyReleaseEvent(event);
+ if (event->key() == Qt::Key_Escape) {
+ if (m_searchTermEditor->text().isEmpty()) {
+ setVisible(false, WithAnimation);
+ } else {
+ // Clear the text input
+ slotSearchTermEdited(QString());
+ }
+ }
+}
+
+void Bar::keyReleaseEvent(QKeyEvent *event)
+{
+ QWidget::keyReleaseEvent(event);
+ if (event->key() == Qt::Key_Down) {
+ Q_EMIT focusViewRequest();
+ }
+}
+
+void Bar::slotConfigurationChanged(const DolphinQuery &searchConfiguration)
+{
+ Q_ASSERT_X(*m_searchConfiguration != searchConfiguration, "Bar::updateState()", "Redundantly updating to a state that is identical to the previous state.");
+ updateStateToMatch(std::make_shared<const DolphinQuery>(searchConfiguration));
+
+ commitCurrentConfiguration();
+}
+
+void Bar::slotSearchTermEdited(const QString &text)
+{
+ DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+ searchConfigurationCopy.setSearchTerm(text);
+ updateStateToMatch(std::make_shared<const DolphinQuery>(searchConfigurationCopy));
+
+ m_startSearchTimer->start();
+}
+
+void Bar::slotReturnPressed()
+{
+ commitCurrentConfiguration();
+ Q_EMIT focusViewRequest();
+}
+
+void Bar::commitCurrentConfiguration()
+{
+ m_startSearchTimer->stop();
+ // We return early and avoid searching when the user has not given any information we can search for. They might for example have deleted the search term.
+ // In that case we want to show the files of the normal location again.
+ if (!isSearchConfigured()) {
+ Q_EMIT urlChangeRequested(m_searchConfiguration->searchPath());
+ return;
+ }
+ Q_EMIT urlChangeRequested(m_searchConfiguration->toUrl());
+}
+
+void Bar::slotSaveSearch()
+{
+ Q_ASSERT_X(isSearchConfigured(),
+ "Search::Bar::slotSaveSearch()",
+ "Search::Bar::isSearchConfigured() considers this search invalid, so the user should not be able to save this search. The button to save should "
+ "be disabled.");
+ const QUrl searchUrl = m_searchConfiguration->toUrl();
+ Q_ASSERT(searchUrl.isValid() && isSupportedSearchScheme(searchUrl.scheme()));
+ DolphinPlacesModelSingleton::instance().placesModel()->addPlace(m_searchConfiguration->title(), searchUrl, QStringLiteral("folder-saved-search-symbolic"));
+}
+
+bool Bar::isSearchConfigured() const
+{
+ return ::isSearchConfigured(m_searchConfiguration);
+}
+
+QString Bar::queryTitle() const
+{
+ return m_searchConfiguration->title();
+}
+
+int Bar::preferredHeight() const
+{
+ return m_secondRowLayout->geometry().y() + m_secondRowLayout->sizeHint().height() + Dolphin::LAYOUT_SPACING_SMALL;
+}