]> cloud.milkyroute.net Git - dolphin.git/commitdiff
Rewrite search integration
authorFelix Ernst <felixernst@kde.org>
Mon, 7 Apr 2025 21:09:00 +0000 (21:09 +0000)
committerFelix Ernst <felixernst@kde.org>
Mon, 7 Apr 2025 21:09:00 +0000 (21:09 +0000)
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

39 files changed:
src/CMakeLists.txt
src/config-dolphin.h.cmake
src/dolphinmainwindow.cpp
src/dolphinrecenttabsmenu.cpp
src/dolphinviewcontainer.cpp
src/dolphinviewcontainer.h
src/global.h
src/search/bar.cpp [new file with mode: 0644]
src/search/bar.h [new file with mode: 0644]
src/search/barsecondrowflowlayout.cpp [new file with mode: 0644]
src/search/barsecondrowflowlayout.h [new file with mode: 0644]
src/search/chip.cpp [new file with mode: 0644]
src/search/chip.h [new file with mode: 0644]
src/search/configurationpopup.h [new file with mode: 0644]
src/search/dolphin_searchsettings.kcfg
src/search/dolphinfacetswidget.cpp [deleted file]
src/search/dolphinfacetswidget.h [deleted file]
src/search/dolphinquery.cpp
src/search/dolphinquery.h
src/search/dolphinsearchbox.cpp [deleted file]
src/search/dolphinsearchbox.h [deleted file]
src/search/popup.cpp [new file with mode: 0644]
src/search/popup.h [new file with mode: 0644]
src/search/selectors/dateselector.cpp [new file with mode: 0644]
src/search/selectors/dateselector.h [new file with mode: 0644]
src/search/selectors/filetypeselector.cpp [new file with mode: 0644]
src/search/selectors/filetypeselector.h [new file with mode: 0644]
src/search/selectors/minimumratingselector.cpp [new file with mode: 0644]
src/search/selectors/minimumratingselector.h [new file with mode: 0644]
src/search/selectors/tagsselector.cpp [new file with mode: 0644]
src/search/selectors/tagsselector.h [new file with mode: 0644]
src/search/updatablestateinterface.h [new file with mode: 0644]
src/search/widgetmenu.cpp [new file with mode: 0644]
src/search/widgetmenu.h [new file with mode: 0644]
src/tests/CMakeLists.txt
src/tests/dolphinmainwindowtest.cpp
src/tests/dolphinquerytest.cpp
src/tests/dolphinsearchbartest.cpp [new file with mode: 0644]
src/tests/dolphinsearchboxtest.cpp [deleted file]

index 6e52772e023801a8977baf9dbe1562067aafb233..4e6e10f0dc9eea06167ae87e4dd853b957612693 100644 (file)
@@ -2,6 +2,7 @@ include(ECMAddAppIcon)
 
 set(ADMIN_WORKER_PACKAGE_NAME "kio-admin")
 set(FILELIGHT_PACKAGE_NAME "filelight")
+set(KFIND_PACKAGE_NAME "kfind")
 
 configure_file(config-dolphin.h.cmake ${CMAKE_CURRENT_BINARY_DIR}/config-dolphin.h)
 
@@ -304,9 +305,16 @@ target_sources(dolphinstatic PRIVATE
     panels/folders/treeviewcontextmenu.cpp
     panels/folders/folderspanel.cpp
     panels/terminal/terminalpanel.cpp
-    search/dolphinfacetswidget.cpp
+    search/bar.cpp
+    search/barsecondrowflowlayout.cpp
+    search/chip.cpp
     search/dolphinquery.cpp
-    search/dolphinsearchbox.cpp
+    search/popup.cpp
+    search/selectors/dateselector.cpp
+    search/selectors/filetypeselector.cpp
+    search/selectors/minimumratingselector.cpp
+    search/selectors/tagsselector.cpp
+    search/widgetmenu.cpp
     selectionmode/actiontexthelper.cpp
     selectionmode/actionwithwidget.cpp
     selectionmode/backgroundcolorhelper.cpp
@@ -365,9 +373,16 @@ target_sources(dolphinstatic PRIVATE
     panels/folders/treeviewcontextmenu.h
     panels/folders/folderspanel.h
     panels/terminal/terminalpanel.h
-    search/dolphinfacetswidget.h
+    search/bar.h
+    search/barsecondrowflowlayout.h
+    search/chip.h
     search/dolphinquery.h
-    search/dolphinsearchbox.h
+    search/popup.h
+    search/selectors/dateselector.h
+    search/selectors/filetypeselector.h
+    search/selectors/minimumratingselector.h
+    search/selectors/tagsselector.h
+    search/widgetmenu.h
     selectionmode/actiontexthelper.h
     selectionmode/actionwithwidget.h
     selectionmode/backgroundcolorhelper.h
index 871adc9707d0fa1a978b14e859feaf65277e728a..b7d6c35f7501d361198c4a271714c5c5409962c3 100644 (file)
@@ -5,8 +5,13 @@
 #cmakedefine01 HAVE_TERMINAL
 #cmakedefine01 HAVE_X11
 
+#define KDE_INSTALL_FULL_DATADIR "${KDE_INSTALL_FULL_DATADIR}"
+
 /** The name of the package that needs to be installed so URLs starting with "admin:" can be opened in Dolphin. */
 #cmakedefine ADMIN_WORKER_PACKAGE_NAME "@ADMIN_WORKER_PACKAGE_NAME@"
 
 /** The name of the KDE Filelight package. */
 #cmakedefine FILELIGHT_PACKAGE_NAME "@FILELIGHT_PACKAGE_NAME@"
+
+/** The name of the KFind package. */
+#cmakedefine KFIND_PACKAGE_NAME "@KFIND_PACKAGE_NAME@"
index 573582bdde2a72a03510e003ac1499976607d524..12b8be955949fca38e21a12cb427d1947e37f797 100644 (file)
@@ -26,6 +26,7 @@
 #include "panels/folders/folderspanel.h"
 #include "panels/places/placespanel.h"
 #include "panels/terminal/terminalpanel.h"
+#include "search/dolphinquery.h"
 #include "selectionmode/actiontexthelper.h"
 #include "settings/dolphinsettingsdialog.h"
 #include "statusbar/dolphinstatusbar.h"
@@ -404,9 +405,9 @@ void DolphinMainWindow::slotTerminalDirectoryChanged(const QUrl &url)
         m_tearDownFromPlacesRequested = false;
     }
 
-    m_activeViewContainer->setAutoGrabFocus(false);
+    m_activeViewContainer->setGrabFocusOnUrlChange(false);
     changeUrl(url);
-    m_activeViewContainer->setAutoGrabFocus(true);
+    m_activeViewContainer->setGrabFocusOnUrlChange(true);
 }
 
 void DolphinMainWindow::slotEditableStateChanged(bool editable)
@@ -487,7 +488,7 @@ void DolphinMainWindow::addToPlaces()
     }
     if (url.isValid()) {
         QString icon;
-        if (m_activeViewContainer->isSearchModeEnabled()) {
+        if (isSearchUrl(url)) {
             icon = QStringLiteral("folder-saved-search-symbolic");
         } else {
             icon = KIO::iconNameForUrl(url);
@@ -892,13 +893,14 @@ void DolphinMainWindow::paste()
 
 void DolphinMainWindow::find()
 {
-    m_activeViewContainer->setSearchModeEnabled(true);
+    m_activeViewContainer->setSearchBarVisible(true);
+    m_activeViewContainer->setFocusToSearchBar();
 }
 
 void DolphinMainWindow::updateSearchAction()
 {
     QAction *toggleSearchAction = actionCollection()->action(QStringLiteral("toggle_search"));
-    toggleSearchAction->setChecked(m_activeViewContainer->isSearchModeEnabled());
+    toggleSearchAction->setChecked(m_activeViewContainer->isSearchBarVisible());
 }
 
 void DolphinMainWindow::updatePasteAction()
@@ -935,9 +937,13 @@ QAction *DolphinMainWindow::urlNavigatorHistoryAction(const KUrlNavigator *urlNa
 {
     const QUrl url = urlNavigator->locationUrl(historyIndex);
 
-    QString text = url.toDisplayString(QUrl::PreferLocalFile);
+    QString text;
 
-    if (!urlNavigator->showFullPath()) {
+    if (isSearchUrl(url)) {
+        text = Search::DolphinQuery(url, QUrl{}).title();
+    } else if (urlNavigator->showFullPath()) {
+        text = url.toDisplayString(QUrl::PreferLocalFile);
+    } else {
         const KFilePlacesModel *placesModel = DolphinPlacesModelSingleton::instance().placesModel();
 
         const QModelIndex closestIdx = placesModel->closestItem(url);
@@ -1604,9 +1610,6 @@ void DolphinMainWindow::activeViewChanged(DolphinViewContainer *viewContainer)
     m_activeViewContainer = viewContainer;
 
     if (oldViewContainer) {
-        const QAction *toggleSearchAction = actionCollection()->action(QStringLiteral("toggle_search"));
-        toggleSearchAction->disconnect(oldViewContainer);
-
         // Disconnect all signals between the old view container (container,
         // view and url navigator) and main window.
         oldViewContainer->disconnect(this);
@@ -1861,9 +1864,7 @@ void DolphinMainWindow::setupActions()
                                       "<para>This helps you "
                                       "find files and folders by opening a <emphasis>search bar</emphasis>. "
                                       "There you can enter search terms and specify settings to find the "
-                                      "items you are looking for.</para><para>Use this help again on "
-                                      "the search bar so we can have a look at it while the settings are "
-                                      "explained.</para>"));
+                                      "items you are looking for.</para>"));
 
     // toggle_search acts as a copy of the main searchAction to be used mainly
     // in the toolbar, with no default shortcut attached, to avoid messing with
@@ -1875,6 +1876,13 @@ void DolphinMainWindow::setupActions()
     toggleSearchAction->setToolTip(searchAction->toolTip());
     toggleSearchAction->setWhatsThis(searchAction->whatsThis());
     toggleSearchAction->setCheckable(true);
+    connect(toggleSearchAction, &QAction::triggered, this, [this](bool checked) {
+        if (checked) {
+            find();
+        } else {
+            m_activeViewContainer->setSearchBarVisible(false);
+        }
+    });
 
     QAction *toggleSelectionModeAction = actionCollection()->addAction(QStringLiteral("toggle_selection_mode"));
     // i18n: This action toggles a selection mode.
@@ -2641,14 +2649,11 @@ void DolphinMainWindow::connectViewSignals(DolphinViewContainer *container)
     connect(container, &DolphinViewContainer::showFilterBarChanged, this, &DolphinMainWindow::updateFilterBarAction);
     connect(container, &DolphinViewContainer::writeStateChanged, this, &DolphinMainWindow::slotWriteStateChanged);
     slotWriteStateChanged(container->view()->isFolderWritable());
-    connect(container, &DolphinViewContainer::searchModeEnabledChanged, this, &DolphinMainWindow::updateSearchAction);
+    connect(container, &DolphinViewContainer::searchBarVisibilityChanged, this, &DolphinMainWindow::updateSearchAction);
     connect(container, &DolphinViewContainer::captionChanged, this, &DolphinMainWindow::updateWindowTitle);
     connect(container, &DolphinViewContainer::tabRequested, this, &DolphinMainWindow::openNewTab);
     connect(container, &DolphinViewContainer::activeTabRequested, this, &DolphinMainWindow::openNewTabAndActivate);
 
-    const QAction *toggleSearchAction = actionCollection()->action(QStringLiteral("toggle_search"));
-    connect(toggleSearchAction, &QAction::triggered, container, &DolphinViewContainer::setSearchModeEnabled);
-
     // Make the toggled state of the selection mode actions visually follow the selection mode state of the view.
     auto toggleSelectionModeAction = actionCollection()->action(QStringLiteral("toggle_selection_mode"));
     toggleSelectionModeAction->setChecked(m_activeViewContainer->isSelectionModeEnabled());
index 74aaf232ed3f0ab21de8dde34f24c10245712f0c..d9b103c0cc0ae3d3bcbf2c433323556f335d916b 100644 (file)
@@ -32,12 +32,8 @@ DolphinRecentTabsMenu::DolphinRecentTabsMenu(QObject *parent)
 void DolphinRecentTabsMenu::rememberClosedTab(const QUrl &url, const QByteArray &state)
 {
     QAction *action = new QAction(menu());
-    if (DolphinQuery::supportsScheme(url.scheme())) {
-        const DolphinQuery query = DolphinQuery::fromSearchUrl(url);
-        action->setText(i18n("Search for %1 in %2", query.text(), query.includeFolder()));
-    } else if (url.scheme() == QLatin1String("filenamesearch")) {
-        const QUrlQuery query(url);
-        action->setText(i18n("Search for %1 in %2", query.queryItemValue(QStringLiteral("search")), query.queryItemValue(QStringLiteral("url"))));
+    if (Search::isSupportedSearchScheme(url.scheme())) {
+        action->setText(Search::DolphinQuery{url, QUrl{}}.title());
     } else {
         action->setText(url.path());
     }
index dcf159cda1fc4e9d01406bf9a7ead203d0c1ce2e..6d08c47c7d03990f4fed2c4e06b9d2887fb03fd4 100644 (file)
@@ -18,7 +18,7 @@
 #include "filterbar/filterbar.h"
 #include "global.h"
 #include "kitemviews/kitemlistcontainer.h"
-#include "search/dolphinsearchbox.h"
+#include "search/bar.h"
 #include "selectionmode/topbar.h"
 #include "statusbar/dolphinstatusbar.h"
 
 #include <QUrl>
 #include <QUrlQuery>
 
+bool isSearchUrl(const QUrl &url)
+{
+    return url.scheme().contains(QLatin1String("search"));
+}
+
 // An overview of the widgets contained by this ViewContainer
 struct LayoutStructure {
-    int searchBox = 0;
+    int searchBar = 0;
     int adminBar = 1;
     int messageWidget = 2;
     int selectionModeTopBar = 3;
@@ -67,7 +72,7 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent)
     , m_topLayout(nullptr)
     , m_urlNavigator{new DolphinUrlNavigator(url)}
     , m_urlNavigatorConnected{nullptr}
-    , m_searchBox(nullptr)
+    , m_searchBar(nullptr)
     , m_searchModeEnabled(false)
     , m_adminBar{nullptr}
     , m_authorizeToEnterFolderAction{nullptr}
@@ -79,7 +84,7 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent)
     , m_statusBar(nullptr)
     , m_statusBarTimer(nullptr)
     , m_statusBarTimestamp()
-    , m_autoGrabFocus(true)
+    , m_grabFocusOnUrlChange{true}
 {
     hide();
 
@@ -87,26 +92,6 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent)
     m_topLayout->setSpacing(0);
     m_topLayout->setContentsMargins(0, 0, 0, 0);
 
-    m_searchBox = new DolphinSearchBox(this);
-    m_searchBox->setVisible(false, WithoutAnimation);
-    connect(m_searchBox, &DolphinSearchBox::activated, this, &DolphinViewContainer::activate);
-    connect(m_searchBox, &DolphinSearchBox::openRequest, this, &DolphinViewContainer::openSearchBox);
-    connect(m_searchBox, &DolphinSearchBox::closeRequest, this, &DolphinViewContainer::closeSearchBox);
-    connect(m_searchBox, &DolphinSearchBox::searchRequest, this, &DolphinViewContainer::startSearching);
-    connect(m_searchBox, &DolphinSearchBox::focusViewRequest, this, &DolphinViewContainer::requestFocus);
-    m_searchBox->setWhatsThis(xi18nc("@info:whatsthis findbar",
-                                     "<para>This helps you find files and folders. Enter a <emphasis>"
-                                     "search term</emphasis> and specify search settings with the "
-                                     "buttons at the bottom:<list><item>Filename/Content: "
-                                     "Does the item you are looking for contain the search terms "
-                                     "within its filename or its contents?<nl/>The contents of images, "
-                                     "audio files and videos will not be searched.</item><item>"
-                                     "From Here/Everywhere: Do you want to search in this "
-                                     "folder and its sub-folders or everywhere?</item><item>"
-                                     "More Options: Click this to search by media type, access "
-                                     "time or rating.</item><item>More Search Tools: Install other "
-                                     "means to find an item.</item></list></para>"));
-
     m_messageWidget = new KMessageWidget(this);
     m_messageWidget->setCloseButtonVisible(true);
     m_messageWidget->setPosition(KMessageWidget::Header);
@@ -188,7 +173,6 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent)
     KIO::FileUndoManager *undoManager = KIO::FileUndoManager::self();
     connect(undoManager, &KIO::FileUndoManager::jobRecordingFinished, this, &DolphinViewContainer::delayedStatusBarUpdate);
 
-    m_topLayout->addWidget(m_searchBox, positionFor.searchBox, 0);
     m_topLayout->addWidget(m_messageWidget, positionFor.messageWidget, 0);
     m_topLayout->addWidget(m_view, positionFor.view, 0);
     m_topLayout->addWidget(m_filterBar, positionFor.filterBar, 0);
@@ -211,7 +195,7 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent)
     });
     m_statusBar->setHidden(false);
 
-    setSearchModeEnabled(isSearchUrl(url));
+    setSearchBarVisible(isSearchUrl(url));
 
     // Update view as the ContentDisplaySettings change
     // this happens here and not in DolphinView as DolphinviewContainer and DolphinView are not in the same build target ATM
@@ -222,8 +206,6 @@ DolphinViewContainer::DolphinViewContainer(const QUrl &url, QWidget *parent)
     connect(placesModel, &KFilePlacesModel::rowsInserted, this, &DolphinViewContainer::slotPlacesModelChanged);
     connect(placesModel, &KFilePlacesModel::rowsRemoved, this, &DolphinViewContainer::slotPlacesModelChanged);
 
-    connect(this, &DolphinViewContainer::searchModeEnabledChanged, this, &DolphinViewContainer::captionChanged);
-
     QApplication::instance()->installEventFilter(this);
 }
 
@@ -243,7 +225,6 @@ KFileItem DolphinViewContainer::rootItem() const
 
 void DolphinViewContainer::setActive(bool active)
 {
-    m_searchBox->setActive(active);
     if (m_urlNavigatorConnected) {
         m_urlNavigatorConnected->setActive(active);
     }
@@ -255,19 +236,9 @@ bool DolphinViewContainer::isActive() const
     return m_view->isActive();
 }
 
-void DolphinViewContainer::setAutoGrabFocus(bool grab)
+void DolphinViewContainer::setGrabFocusOnUrlChange(bool grabFocus)
 {
-    m_autoGrabFocus = grab;
-}
-
-bool DolphinViewContainer::autoGrabFocus() const
-{
-    return m_autoGrabFocus;
-}
-
-QString DolphinViewContainer::currentSearchText() const
-{
-    return m_searchBox->text();
+    m_grabFocusOnUrlChange = grabFocus;
 }
 
 const DolphinStatusBar *DolphinViewContainer::statusBar() const
@@ -359,6 +330,79 @@ void DolphinViewContainer::disconnectUrlNavigator()
     m_urlNavigatorConnected = nullptr;
 }
 
+void DolphinViewContainer::setSearchBarVisible(bool visible)
+{
+    if (!visible) {
+        if (isSearchBarVisible()) {
+            m_searchBar->setVisible(false, WithAnimation);
+        }
+        return;
+    }
+
+    if (!m_searchBar) {
+        m_searchBar = new Search::Bar(std::make_shared<const Search::DolphinQuery>(m_urlNavigator->locationUrl(), QUrl{} /** will be set below. */), this);
+        connect(m_searchBar, &Search::Bar::urlChangeRequested, this, [this](const QUrl &url) {
+            m_view->setViewPropertiesContext(isSearchUrl(url) ? QStringLiteral("search") : QString());
+            setGrabFocusOnUrlChange(false); // Prevent loss of focus while typing or refining a search.
+            setUrl(url);
+            setGrabFocusOnUrlChange(true);
+        });
+        connect(m_searchBar, &Search::Bar::focusViewRequest, this, &DolphinViewContainer::requestFocus);
+        connect(m_searchBar, &Search::Bar::showMessage, this, [this](const QString &message, KMessageWidget::MessageType messageType) {
+            showMessage(message, messageType);
+        });
+        connect(m_searchBar,
+                &Search::Bar::showInstallationProgress,
+                m_statusBar,
+                [this](const QString &currentlyRunningTaskTitle, int installationProgressPercent) {
+                    m_statusBar->showProgress(currentlyRunningTaskTitle, installationProgressPercent, DolphinStatusBar::CancelLoading::Disallowed);
+                });
+        connect(m_searchBar, &Search::Bar::visibilityChanged, this, &DolphinViewContainer::searchBarVisibilityChanged);
+        m_topLayout->addWidget(m_searchBar, positionFor.searchBar, 0);
+    }
+
+    m_searchBar->setVisible(true, WithAnimation);
+
+    // The Search::Bar has been set visible but its state does not yet match with this view container or view.
+    // The view might for example already be searching because it was opened with a search URL. The Search::Bar needs to be updated to show the parameters of
+    // that search. And even if there is no search URL loaded in the view currently, we still need to figure out where the Search::Bar should be searching if
+    // the user starts a search from there. Let's figure out the search location in this method and let the DolphinQuery constructor figure out the rest from
+    // the current m_urlNavigator->locationUrl().
+    for (int i = m_urlNavigator->historyIndex(); i < m_urlNavigator->historySize(); i++) {
+        QUrl url = m_urlNavigator->locationUrl(i);
+        if (isSearchUrl(url)) {
+            // The previous location was a search URL. Try to see if that search URL has a valid search path so we keep searching in the same location.
+            const auto searchPath = Search::DolphinQuery(url, QUrl{}).searchPath(); // DolphinQuery is great at extracting the search path from a search URL.
+            if (searchPath.isValid()) {
+                m_searchBar->updateStateToMatch(std::make_shared<const Search::DolphinQuery>(m_urlNavigator->locationUrl(), searchPath));
+                return;
+            }
+        } else if (url.scheme() == QLatin1String("tags")) {
+            continue; // We avoid setting a tags url as the backup search path because a DolphinQuery constructed from a tags url will already search tagged
+                      // items everywhere.
+        } else {
+            m_searchBar->updateStateToMatch(std::make_shared<const Search::DolphinQuery>(m_urlNavigator->locationUrl(), url));
+            return;
+        }
+    }
+    // We could not find any URL fit for searching in the history. This might happen because this view only ever loaded a search which searches "Everywhere"
+    // and therefore there is no specific search path to choose from. But the Search::Bar *needs* to know a search path because the user might switch from
+    // searching "Everywhere" to "Here" and it is everybody's guess what "Here" is supposed to mean in that context… We'll simply fall back to the user's home
+    // path for lack of a better option.
+    m_searchBar->updateStateToMatch(std::make_shared<const Search::DolphinQuery>(m_urlNavigator->locationUrl(), QUrl::fromUserInput(QDir::homePath())));
+}
+
+bool DolphinViewContainer::isSearchBarVisible() const
+{
+    return m_searchBar && m_searchBar->isVisible() && m_searchBar->isEnabled();
+}
+
+void DolphinViewContainer::setFocusToSearchBar()
+{
+    Q_ASSERT(isSearchBarVisible());
+    m_searchBar->selectAll();
+}
+
 void DolphinViewContainer::setSelectionModeEnabled(bool enabled, KActionCollection *actionCollection, SelectionMode::BottomBar::Contents bottomBarContents)
 {
     const bool wasEnabled = m_view->selectionMode();
@@ -496,53 +540,12 @@ bool DolphinViewContainer::isFilterBarVisible() const
     return m_filterBar->isEnabled(); // Gets disabled in AnimatedHeightWidget while animating towards a hidden state.
 }
 
-void DolphinViewContainer::setSearchModeEnabled(bool enabled)
-{
-    m_searchBox->setVisible(enabled, WithAnimation);
-
-    if (enabled) {
-        const QUrl &locationUrl = m_urlNavigator->locationUrl();
-        m_searchBox->fromSearchUrl(locationUrl);
-    }
-
-    if (enabled == isSearchModeEnabled()) {
-        if (enabled && !m_searchBox->hasFocus()) {
-            m_searchBox->setFocus();
-            m_searchBox->selectAll();
-        }
-        return;
-    }
-
-    if (!enabled) {
-        m_view->setViewPropertiesContext(QString());
-
-        // Restore the URL for the URL navigator. If Dolphin has been
-        // started with a search-URL, the home URL is used as fallback.
-        QUrl url = m_searchBox->searchPath();
-        if (url.isEmpty() || !url.isValid() || isSearchUrl(url)) {
-            url = Dolphin::homeUrl();
-        }
-        if (m_urlNavigatorConnected) {
-            m_urlNavigatorConnected->setLocationUrl(url);
-        }
-    }
-
-    m_searchModeEnabled = enabled;
-
-    Q_EMIT searchModeEnabledChanged(enabled);
-}
-
-bool DolphinViewContainer::isSearchModeEnabled() const
-{
-    return m_searchModeEnabled;
-}
-
 QString DolphinViewContainer::placesText() const
 {
     QString text;
 
-    if (isSearchModeEnabled()) {
-        text = i18n("Search for %1 in %2", m_searchBox->text(), m_searchBox->searchPath().fileName());
+    if (isSearchBarVisible() && m_searchBar->isSearchConfigured()) {
+        text = m_searchBar->queryTitle();
     } else {
         text = url().adjusted(QUrl::StripTrailingSlash).fileName();
         if (text.isEmpty()) {
@@ -564,7 +567,7 @@ void DolphinViewContainer::reload()
 
 QString DolphinViewContainer::captionWindowTitle() const
 {
-    if (GeneralSettings::showFullPathInTitlebar() && !isSearchModeEnabled()) {
+    if (GeneralSettings::showFullPathInTitlebar() && (!isSearchBarVisible() || !m_searchBar->isSearchConfigured())) {
         if (!url().isLocalFile()) {
             return url().adjusted(QUrl::StripTrailingSlash).toString();
         }
@@ -579,18 +582,14 @@ QString DolphinViewContainer::caption() const
     // see KUrlNavigatorPrivate::firstButtonText().
     if (url().path().isEmpty() || url().path() == QLatin1Char('/')) {
         QUrlQuery query(url());
-        const QString title = query.queryItemValue(QStringLiteral("title"));
+        const QString title = query.queryItemValue(QStringLiteral("title"), QUrl::FullyDecoded);
         if (!title.isEmpty()) {
             return title;
         }
     }
 
-    if (isSearchModeEnabled()) {
-        if (currentSearchText().isEmpty()) {
-            return i18n("Search");
-        } else {
-            return i18n("Search for %1", currentSearchText());
-        }
+    if (isSearchBarVisible() && m_searchBar->isSearchConfigured()) {
+        return m_searchBar->queryTitle();
     }
 
     KFilePlacesModel *placesModel = DolphinPlacesModelSingleton::instance().placesModel();
@@ -628,6 +627,9 @@ void DolphinViewContainer::setUrl(const QUrl &newUrl)
 {
     if (newUrl != m_urlNavigator->locationUrl()) {
         m_urlNavigator->setLocationUrl(newUrl);
+        if (m_searchBar && !Search::isSupportedSearchScheme(newUrl.scheme())) {
+            m_searchBar->setSearchPath(newUrl);
+        }
     }
 }
 
@@ -862,13 +864,17 @@ void DolphinViewContainer::slotUrlNavigatorLocationChanged(const QUrl &url)
     }
 
     if (KProtocolManager::supportsListing(url)) {
-        const bool searchBoxInitialized = isSearchModeEnabled() && m_searchBox->text().isEmpty();
-        setSearchModeEnabled(isSearchUrl(url) || searchBoxInitialized);
+        if (isSearchUrl(url)) {
+            setSearchBarVisible(true);
+        } else if (m_searchBar && m_searchBar->isSearchConfigured()) {
+            // Hide the search bar because it shows an outdated search which the user does not care about anymore.
+            setSearchBarVisible(false);
+        }
 
         m_view->setUrl(url);
         tryRestoreViewState();
 
-        if (m_autoGrabFocus && isActive() && !isSearchModeEnabled()) {
+        if (m_grabFocusOnUrlChange && isActive()) {
             // When an URL has been entered, the view should get the focus.
             // The focus must be requested asynchronously, as changing the URL might create
             // a new view widget.
@@ -923,10 +929,7 @@ void DolphinViewContainer::redirect(const QUrl &oldUrl, const QUrl &newUrl)
     // URL history.
     m_urlNavigator->saveLocationState(QByteArray());
     m_urlNavigator->setLocationUrl(newUrl);
-    if (m_searchBox->isActive()) {
-        m_searchBox->setSearchPath(newUrl);
-    }
-    setSearchModeEnabled(isSearchUrl(newUrl));
+    setSearchBarVisible(isSearchUrl(newUrl));
 
     m_urlNavigator->blockSignals(block);
 }
@@ -936,31 +939,6 @@ void DolphinViewContainer::requestFocus()
     m_view->setFocus();
 }
 
-void DolphinViewContainer::startSearching()
-{
-    Q_CHECK_PTR(m_urlNavigatorConnected);
-    const QUrl url = m_searchBox->urlForSearching();
-    if (url.isValid() && !url.isEmpty()) {
-        m_view->setViewPropertiesContext(QStringLiteral("search"));
-        // If we open a new tab that has a search assigned to it, we can't
-        // update the urlNavigator, since there is none connected to that tab.
-        // See BUG:500101
-        if (m_urlNavigatorConnected) {
-            m_urlNavigatorConnected->setLocationUrl(url);
-        }
-    }
-}
-
-void DolphinViewContainer::openSearchBox()
-{
-    setSearchModeEnabled(true);
-}
-
-void DolphinViewContainer::closeSearchBox()
-{
-    setSearchModeEnabled(false);
-}
-
 void DolphinViewContainer::stopDirectoryLoading()
 {
     m_view->stopLoading();
@@ -1001,7 +979,7 @@ void DolphinViewContainer::showErrorMessage(const QString &message)
 
 void DolphinViewContainer::slotPlacesModelChanged()
 {
-    if (!GeneralSettings::showFullPathInTitlebar() && !isSearchModeEnabled()) {
+    if (!GeneralSettings::showFullPathInTitlebar()) {
         Q_EMIT captionChanged();
     }
 }
@@ -1043,11 +1021,6 @@ void DolphinViewContainer::slotOpenUrlFinished(KJob *job)
     }
 }
 
-bool DolphinViewContainer::isSearchUrl(const QUrl &url) const
-{
-    return url.scheme().contains(QLatin1String("search"));
-}
-
 void DolphinViewContainer::saveViewState()
 {
     QByteArray locationState;
index a509bab3d91a514517205ed2c24a1865621505a7..e827c08856e1a740c2e17a8b175d6b116a623b0d 100644 (file)
@@ -31,7 +31,10 @@ class FilterBar;
 class QAction;
 class QGridLayout;
 class QUrl;
-class DolphinSearchBox;
+namespace Search
+{
+class Bar;
+}
 class DolphinStatusBar;
 class KFileItemList;
 namespace SelectionMode
@@ -39,6 +42,11 @@ namespace SelectionMode
 class TopBar;
 }
 
+/**
+ * @return True if the URL protocol is a search URL (e. g. baloosearch:// or filenamesearch://).
+ */
+bool isSearchUrl(const QUrl &url);
+
 /**
  * @short Represents a view for the directory content
  *        including the navigation bar, filter bar and status bar.
@@ -78,10 +86,7 @@ public:
      * as soon as the URL has been changed. Per default the grabbing
      * of the focus is enabled.
      */
-    void setAutoGrabFocus(bool grab);
-    bool autoGrabFocus() const;
-
-    QString currentSearchText() const;
+    void setGrabFocusOnUrlChange(bool grabFocus);
 
     const DolphinStatusBar *statusBar() const;
     DolphinStatusBar *statusBar();
@@ -133,6 +138,28 @@ public:
      */
     void disconnectUrlNavigator();
 
+    /**
+     * Sets the visibility of this objects search configuration user interface. This search bar is the primary interface in Dolphin to search for files and
+     * folders.
+     *
+     * The signal searchBarVisibilityChanged will be emitted when the new visibility state is different from the old.
+     *
+     * Typically an animation will play when the search bar is shown or hidden, so the visibility of the bar will not necessarily match @p visible when this
+     * method returns. Instead use isSearchBarVisible(), which will always communicate the visibility state the search bar is heading to.
+     *
+     * @see Search::Bar.
+     * @see isSearchBarVisible().
+     */
+    void setSearchBarVisible(bool visible);
+
+    /** @returns true if the search bar is visible while not being in the process to hide itself. */
+    bool isSearchBarVisible() const;
+
+    /**
+     * Moves keyboard focus to the search bar. The search term is fully selected to allow easy replacing.
+     */
+    void setFocusToSearchBar();
+
     /**
      * Sets a selection mode that is useful for quick and easy selecting or deselecting of files.
      * This method is the central authority about enabling or disabling selection mode:
@@ -166,9 +193,6 @@ public:
      *           false, if it is hidden or currently animating towards a hidden state. */
     bool isFilterBarVisible() const;
 
-    /** Returns true if the search mode is enabled. */
-    bool isSearchModeEnabled() const;
-
     /**
      * @return Text that should be used for the current URL when creating
      *         a new place.
@@ -221,12 +245,6 @@ public Q_SLOTS:
      */
     void setFilterBarVisible(bool visible);
 
-    /**
-     * Enables the search mode, if \p enabled is true. In the search mode the URL navigator
-     * will be hidden and replaced by a line editor that allows to enter a search term.
-     */
-    void setSearchModeEnabled(bool enabled);
-
     /** Used to notify the m_selectionModeBottomBar that there is no other ViewContainer in the tab. */
     void slotSplitTabDisabled();
 
@@ -236,9 +254,14 @@ Q_SIGNALS:
      */
     void showFilterBarChanged(bool shown);
     /**
-     * Is emitted whenever the search mode has changed its state.
+     * Is emitted whenever a change to the search bar's visibility is invoked. The visibility change might not have actually already taken effect by the time
+     * this signal is emitted because typically the showing and hiding is animated.
+     * @param visible The visibility state the search bar is going to end up at.
+     * @see Search::Bar.
+     * @see setSearchBarVisible().
+     * @see isSearchBarVisible().
      */
-    void searchModeEnabledChanged(bool enabled);
+    void searchBarVisibilityChanged(bool visible);
 
     void selectionModeChanged(bool enabled);
 
@@ -373,13 +396,6 @@ private Q_SLOTS:
     /** Requests the focus for the view \a m_view. */
     void requestFocus();
 
-    /**
-     * Gets the search URL from the searchbox and starts searching.
-     */
-    void startSearching();
-    void openSearchBox();
-    void closeSearchBox();
-
     /**
      * Stops the loading of a directory. Is connected with the "stopPressed" signal
      * from the statusbar.
@@ -411,11 +427,6 @@ private Q_SLOTS:
     void slotOpenUrlFinished(KJob *job);
 
 private:
-    /**
-     * @return True if the URL protocol is a search URL (e. g. baloosearch:// or filenamesearch://).
-     */
-    bool isSearchUrl(const QUrl &url) const;
-
     /**
      * Saves the state of the current view: contents position,
      * root URL, ...
@@ -463,7 +474,7 @@ private:
      */
     QPointer<DolphinUrlNavigator> m_urlNavigatorConnected;
 
-    DolphinSearchBox *m_searchBox;
+    Search::Bar *m_searchBar;
     bool m_searchModeEnabled;
 
     /// A bar shown at the top of the view to signify that the view is currently viewed and acted on with elevated privileges.
@@ -486,7 +497,7 @@ private:
     DolphinStatusBar *m_statusBar;
     QTimer *m_statusBarTimer; // Triggers a delayed update
     QElapsedTimer m_statusBarTimestamp; // Time in ms since last update
-    bool m_autoGrabFocus;
+    bool m_grabFocusOnUrlChange;
     /**
      * The visual state to be applied to the next UrlNavigator that gets
      * connected to this ViewContainer.
index 9161ed877a910bd394a995f94811497db311b446..81ade47fa03db96bf259697ec4bfccdab5750868 100644 (file)
@@ -47,10 +47,10 @@ QVector<QPair<QSharedPointer<OrgKdeDolphinMainWindowInterface>, QStringList>> do
 QPair<QString, Qt::SortOrder> sortOrderForUrl(QUrl &url);
 
 /**
- * TODO: Move this somewhere global to all KDE apps, not just Dolphin
+ * TODO: Use global KDE spacings instead of Dolphin-specific ones once we have them.
  */
 constexpr int VERTICAL_SPACER_HEIGHT = 12;
-constexpr int LAYOUT_SPACING_SMALL = 2;
+constexpr int LAYOUT_SPACING_SMALL = 4;
 }
 
 enum Animated { WithAnimation, WithoutAnimation };
diff --git a/src/search/bar.cpp b/src/search/bar.cpp
new file mode 100644 (file)
index 0000000..18707ef
--- /dev/null
@@ -0,0 +1,356 @@
+/*
+    SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz19@gmail.com>
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    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;
+}
diff --git a/src/search/bar.h b/src/search/bar.h
new file mode 100644 (file)
index 0000000..e681b8a
--- /dev/null
@@ -0,0 +1,206 @@
+/*
+    SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz19@gmail.com>
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#ifndef SEARCHBAR_H
+#define SEARCHBAR_H
+
+#include "animatedheightwidget.h"
+#include "dolphinquery.h"
+#include "updatablestateinterface.h"
+
+#include <KMessageWidget>
+
+#include <QUrl>
+
+class DolphinSearchBarTest;
+class QHBoxLayout;
+class QLineEdit;
+class QToolButton;
+class QVBoxLayout;
+
+namespace Search
+{
+class BarSecondRowFlowLayout;
+template<class Selector>
+class Chip;
+class DateSelector;
+class FileTypeSelector;
+class MinimumRatingSelector;
+class Popup;
+class TagsSelector;
+
+/**
+ * @brief User interface for searching files and folders.
+ *
+ * This Bar is both for configuring a new search as well as showing the search parameter of any search URL opened in Dolphin.
+ * There are many search parameters whose availability can depend on various conditions. Those include:
+ * - Where to search: Everywhere or below the current directory
+ * - What to search: Filenames or content
+ * - How to search: Which search tool to use
+ * - etc.
+ *
+ * The class which defines the state of this Bar and its children is DolphinQuery.
+ * @see DolphinQuery.
+ */
+class Bar : public AnimatedHeightWidget, public UpdatableStateInterface
+{
+    Q_OBJECT
+
+public:
+    /**
+     * @brief Constructs a Search::Bar with an initial state matching @p dolphinQuery and with parent @p parent.
+     */
+    explicit Bar(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr);
+
+    /**
+     * Returns the text that should be used as input
+     * for searching.
+     */
+    QString text() const;
+
+    /**
+     * Sets the current path that is used as root for searching files.
+     * If @url is the Home dir, "From Here" is selected instead.
+     */
+    void setSearchPath(const QUrl &url);
+
+    /**
+     * Selects the whole text of the search box.
+     */
+    void selectAll();
+
+    /**
+     * All showing and hiding of this bar is supposed to go through this method. When hiding this bar, it emits all the necessary signals to restore the view
+     * container to a non-search URL.
+     * This method also aims to make sure that visibilityChanged() will be emitted no matter from where setVisible() is called. This way the "Find" action can
+     * be properly un/checked.
+     * @see AnimatedHeightWidget::setVisible().
+     */
+    void setVisible(bool visible, Animated animated);
+
+    /**
+     * @returns false, when the search UI has not yet been changed to search for anything specific. For example when no search term has been entered yet.
+     *          Otherwise returns true, for example when a search term has been entered or there is a search request for all files of a specific file type or
+     *          with a specific modification date.
+     */
+    bool isSearchConfigured() const;
+
+    /**
+     * @returns the title for the search that is currently configured in this bar.
+     * @see DolphinQuery::title().
+     */
+    QString queryTitle() const;
+
+Q_SIGNALS:
+    /**
+     * This signals a request for the attached view container to switch to @p url.
+     * A URL for searching is requested when the user actively engages with this UI to trigger a search.
+     * A non-search URL is requested when this search UI is closed and no search results should be displayed anymore.
+     */
+    void urlChangeRequested(const QUrl &url);
+
+    /**
+     * Is emitted when the bar should receive focus. This is usually triggered by a user action that implies that this bar should no longer have focus.
+     */
+    void focusViewRequest();
+
+    /**
+     * Requests for @p message with the given @p messageType to be shown to the user in a non-modal way.
+     */
+    void showMessage(const QString &message, KMessageWidget::MessageType messageType);
+
+    /**
+     * Requests for a progress update to be shown to the user in a non-modal way.
+     * @param currentlyRunningTaskTitle     The task that is currently progressing.
+     * @param installationProgressPercent   The current percentage of completion.
+     */
+    void showInstallationProgress(const QString &currentlyRunningTaskTitle, int installationProgressPercent);
+
+    /**
+     * Is emitted when a change of the visibility of this bar is invoked in any way. This can happen from code calling from outside this class, for example
+     * when the user triggered a keyboard shortcut to show this bar, or from inside, for example because the close button on this bar was pressed or an Escape
+     * key press was received.
+     */
+    void visibilityChanged(bool visible);
+
+protected:
+    /** Handles Escape key presses to clear the search field or close this bar. */
+    void keyPressEvent(QKeyEvent *event) override;
+    /** Allows moving the focus to the view with the Down arrow key. */
+    void keyReleaseEvent(QKeyEvent *event) override;
+
+private Q_SLOTS:
+    /**
+     * Is called when any component within this Bar emits a configurationChanged() signal.
+     * This method is then responsible to communicate the changed search configuration to every other interested party by calling
+     * UpdatableStateInterface::updateStateToMatch() methods and commiting the new search configuration.
+     * @see UpdatableStateInterface::updateStateToMatch().
+     * @see commitCurrentConfiguration().
+     */
+    void slotConfigurationChanged(const DolphinQuery &searchConfiguration);
+
+    /**
+     * Changes the m_searchConfiguration in response to the user editing the search term. If no further changes to the search term happen within a time limit,
+     * the new search configuration will eventually be commited.
+     * @see commitCurrentConfiguration.
+     */
+    void slotSearchTermEdited(const QString &text);
+
+    /**
+     * Commits the current search configuration and then requests moving focus away from this bar and to the view.
+     * @see commitCurrentConfiguration.
+     */
+    void slotReturnPressed();
+
+    /**
+     * Translates the current m_searchConfiguration into URLs which are then emitted through the urlChangeRequested() signal.
+     * If the current m_searchConfiguration is a valid search, a searchUrl is emitted. If it is not a valid search, i.e. when isSearchConfigured() is false,
+     * the search path is instead emitted so the view returns to showing a normal folder instead of search results.
+     * @see urlChangeRequested().
+     */
+    void commitCurrentConfiguration();
+
+    /** Adds the current search as a link/favorite to the Places panel. */
+    void slotSaveSearch();
+
+private:
+    /**
+     * This Search::Bar always represents a search configuration. This method takes a new @p dolphinQuery i.e. search configuration and updates itself and all
+     * child widgets to match it. This way the user always knows which search parameters lead to the query results that appear in the view.
+     * @see UpdatableStateInterface::updateStateToMatch().
+     */
+    void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override;
+
+    /** @see AnimatedHeightWidget::preferredHeight() */
+    int preferredHeight() const override;
+
+private:
+    QVBoxLayout *m_topLayout = nullptr;
+
+    // The widgets below are sorted by their tab order.
+
+    QLineEdit *m_searchTermEditor = nullptr;
+    QAction *m_saveSearchAction = nullptr;
+    /// The main popup of this bar that allows configuring most search parameters.
+    Popup *m_popup = nullptr;
+    BarSecondRowFlowLayout *m_secondRowLayout = nullptr;
+    QToolButton *m_fromHereButton = nullptr;
+    QToolButton *m_everywhereButton = nullptr;
+    Chip<FileTypeSelector> *m_fileTypeSelectorChip = nullptr;
+    Chip<DateSelector> *m_modifiedSinceDateSelectorChip = nullptr;
+    Chip<MinimumRatingSelector> *m_minimumRatingSelectorChip = nullptr;
+    Chip<TagsSelector> *m_requiredTagsSelectorChip = nullptr;
+
+    /// Starts a new search when the user has finished typing the search term.
+    QTimer *m_startSearchTimer = nullptr;
+
+    friend DolphinSearchBarTest;
+};
+
+}
+
+#endif
diff --git a/src/search/barsecondrowflowlayout.cpp b/src/search/barsecondrowflowlayout.cpp
new file mode 100644 (file)
index 0000000..29e3513
--- /dev/null
@@ -0,0 +1,150 @@
+/*
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "barsecondrowflowlayout.h"
+
+#include <QWidget>
+
+#include <vector>
+
+using namespace Search;
+
+namespace
+{
+constexpr int searchLocationButtonsCount = 2;
+}
+
+BarSecondRowFlowLayout::BarSecondRowFlowLayout(QWidget *parent)
+    : QLayout{parent}
+{
+    setContentsMargins(0, 0, 0, 0);
+}
+
+BarSecondRowFlowLayout::~BarSecondRowFlowLayout()
+{
+    QLayoutItem *item;
+    while ((item = takeAt(0)))
+        delete item;
+}
+
+void BarSecondRowFlowLayout::addItem(QLayoutItem *item)
+{
+    itemList.append(item);
+}
+
+Qt::Orientations BarSecondRowFlowLayout::expandingDirections() const
+{
+    return {};
+}
+
+bool BarSecondRowFlowLayout::hasHeightForWidth() const
+{
+    return false;
+}
+
+int BarSecondRowFlowLayout::count() const
+{
+    return itemList.size();
+}
+
+QLayoutItem *BarSecondRowFlowLayout::itemAt(int index) const
+{
+    return itemList.value(index);
+}
+
+QSize BarSecondRowFlowLayout::sizeHint() const
+{
+    const QRect rect = geometry();
+    QSize size;
+    for (const QLayoutItem *item : std::as_const(itemList)) {
+        size = size.expandedTo(QSize{item->geometry().right() - rect.x(), item->geometry().bottom() - rect.y()});
+    }
+    return size;
+}
+
+void BarSecondRowFlowLayout::setGeometry(const QRect &rect)
+{
+    const int oldHeightHint = sizeHint().height();
+    QLayout::setGeometry(rect);
+    const bool isLeftToRight = itemAt(0)->widget()->layoutDirection() == Qt::LeftToRight;
+    int x = rect.left();
+    int y = rect.top();
+
+    /// The search location buttons are treated differently. They are meant to be in the same row and aligned the other way.
+    int totalLocationButtonWidth = 0;
+    for (int i = 0; i < searchLocationButtonsCount; i++) {
+        totalLocationButtonWidth += itemAt(i)->widget()->sizeHint().width();
+    }
+    if (totalLocationButtonWidth > rect.width()) {
+        /// There is not enough space so we will smush all the location buttons into the first row.
+        for (int i = 0; i < searchLocationButtonsCount; i++) {
+            QWidget *widget = itemAt(i)->widget();
+            const int targetWidth = qMin(widget->sizeHint().width(), rect.width() / searchLocationButtonsCount);
+            widget->setGeometry(isLeftToRight ? x : rect.right() - x - targetWidth, y, targetWidth, widget->sizeHint().height());
+            x += widget->width();
+        }
+    } else {
+        for (int i = 0; i < searchLocationButtonsCount; i++) {
+            QWidget *widget = itemAt(i)->widget();
+            QSize preferredSize = widget->sizeHint();
+            widget->setGeometry(isLeftToRight ? x : rect.right() - x - preferredSize.width(), y, preferredSize.width(), preferredSize.height());
+            x += widget->width() + spacing();
+        }
+    }
+
+    // We want to align all further widgets the other way. We do this by first filling up the row like usual and then moving all widgets of the current row by
+    // the remaining space.
+    std::vector<QWidget *> currentRowWidgets;
+    for (int i = searchLocationButtonsCount; i < count(); i++) {
+        QWidget *widget = itemAt(i)->widget();
+        const int remainingSpace = rect.right() - x + spacing();
+        if (widget->sizeHint().width() < remainingSpace) {
+            QSize preferredSize = widget->sizeHint();
+            widget->setGeometry(isLeftToRight ? x : rect.right() - x - preferredSize.width(), y, preferredSize.width(), preferredSize.height());
+            x += widget->width() + spacing();
+            currentRowWidgets.push_back(widget);
+            continue;
+        }
+
+        // There is not enough space for the next widget. We need to open up a new row.
+        // Right align all the widgets of the previous row.
+        for (QWidget *widget : std::as_const(currentRowWidgets)) {
+            widget->setGeometry(widget->geometry().translated(isLeftToRight ? remainingSpace : -remainingSpace, 0));
+        }
+        currentRowWidgets.clear();
+
+        x = 0;
+        y += itemAt(i - 1)->widget()->height() + spacing();
+
+        QSize preferredSize = widget->sizeHint();
+        const int targetWidth = qMin(preferredSize.width(), rect.width());
+        widget->setGeometry(isLeftToRight ? x : rect.right() - x - targetWidth, y, targetWidth, preferredSize.height());
+        x += widget->width() + spacing();
+        currentRowWidgets.push_back(widget);
+    }
+
+    // Right align all the widgets of the previous row.
+    int remainingSpace = rect.right() - x + spacing();
+    for (QWidget *widget : std::as_const(currentRowWidgets)) {
+        widget->setGeometry(widget->geometry().translated(isLeftToRight ? remainingSpace : -remainingSpace, 0));
+    }
+
+    if (sizeHint().height() != oldHeightHint) {
+        Q_EMIT heightHintChanged();
+    }
+}
+
+QSize BarSecondRowFlowLayout::minimumSize() const
+{
+    return QSize{0, sizeHint().height()};
+}
+
+QLayoutItem *BarSecondRowFlowLayout::takeAt(int index)
+{
+    if (index >= 0 && index < itemList.size())
+        return itemList.takeAt(index);
+    return nullptr;
+}
diff --git a/src/search/barsecondrowflowlayout.h b/src/search/barsecondrowflowlayout.h
new file mode 100644 (file)
index 0000000..3f9891e
--- /dev/null
@@ -0,0 +1,48 @@
+/*
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef BARSECONDROWFLOWLAYOUT_H
+#define BARSECONDROWFLOWLAYOUT_H
+
+#include <QLayout>
+
+namespace Search
+{
+
+/**
+ * @brief The layout for all Search::Bar contents which are not in the first row.
+ *
+ * For left-to-right languages the search location buttons are kept left-aligned while the chips are right-aligned. When there is not enough space for all the
+ * widgts in the current row, a new row is started and the Search::Bar is notified that it needs to resize itself.
+ */
+class BarSecondRowFlowLayout : public QLayout
+{
+    Q_OBJECT
+
+public:
+    explicit BarSecondRowFlowLayout(QWidget *parent);
+    ~BarSecondRowFlowLayout();
+
+    void addItem(QLayoutItem *item) override;
+    Qt::Orientations expandingDirections() const override;
+    bool hasHeightForWidth() const override;
+    int count() const override;
+    QLayoutItem *itemAt(int index) const override;
+    QSize minimumSize() const override;
+    void setGeometry(const QRect &rect) override;
+    QSize sizeHint() const override;
+    QLayoutItem *takeAt(int index) override;
+
+Q_SIGNALS:
+    void heightHintChanged();
+
+private:
+    QList<QLayoutItem *> itemList;
+};
+
+}
+
+#endif // BARSECONDROWFLOWLAYOUT_H
diff --git a/src/search/chip.cpp b/src/search/chip.cpp
new file mode 100644 (file)
index 0000000..2d2914b
--- /dev/null
@@ -0,0 +1,52 @@
+/*
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "chip.h"
+
+#include <KColorUtils>
+#include <KLocalizedString>
+#include <QPaintEvent>
+#include <QStylePainter>
+#include <QToolButton>
+
+using namespace Search;
+
+ChipBase::ChipBase(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent)
+    : QWidget{parent}
+    , UpdatableStateInterface{dolphinQuery}
+{
+    m_removeButton = new QToolButton{this};
+    m_removeButton->setText(i18nc("@action:button", "Remove Filter"));
+    m_removeButton->setIcon(QIcon::fromTheme("list-remove"));
+    m_removeButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
+    m_removeButton->setAutoRaise(true);
+
+    auto layout = new QHBoxLayout{this};
+    layout->setContentsMargins(0, 0, 0, 0);
+    layout->setSpacing(0);
+}
+
+void ChipBase::paintEvent(QPaintEvent *event)
+{
+    QStylePainter painter(this);
+    painter.setRenderHint(QPainter::Antialiasing);
+    QColor penColor = KColorUtils::mix(palette().base().color(), palette().text().color(), 0.3);
+    // QPainter is bad at drawing lines that are exactly 1px.
+    // Using QPen::setCosmetic(true) with a 1px pen width
+    // doesn't look quite as good as just using 1.001px.
+    qreal penWidth = 1.001;
+    qreal penMargin = penWidth / 2;
+    QPen pen(penColor, penWidth);
+    pen.setCosmetic(true);
+    QRectF rect = event->rect();
+    rect.adjust(penMargin, penMargin, -penMargin, -penMargin);
+    painter.setBrush(palette().base());
+    painter.setPen(pen);
+    painter.drawRoundedRect(rect, 5, 5); // 5 is the current default Breeze corner radius
+    QWidget::paintEvent(event);
+}
+
+#include "moc_chip.cpp"
diff --git a/src/search/chip.h b/src/search/chip.h
new file mode 100644 (file)
index 0000000..94e5196
--- /dev/null
@@ -0,0 +1,101 @@
+/*
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef CHIP_H
+#define CHIP_H
+
+#include "dolphinquery.h"
+#include "selectors/dateselector.h"
+#include "selectors/filetypeselector.h"
+#include "selectors/minimumratingselector.h"
+#include "selectors/tagsselector.h"
+#include "updatablestateinterface.h"
+
+#include <QComboBox>
+#include <QLayout>
+#include <QToolButton>
+#include <QWidget>
+
+#include <type_traits>
+
+namespace Search
+{
+
+/**
+ * @brief The non-template base class for the template class Chip.
+ *
+ * @see Chip below.
+ *
+ * Template classes cannot have Qt signals. This class works around that by being a non-template class which the template Chip class then inherits from.
+ * This base class contains all non-template logic of Chip.
+ */
+class ChipBase : public QWidget, public UpdatableStateInterface
+{
+    Q_OBJECT
+
+public:
+    ChipBase(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr);
+
+Q_SIGNALS:
+    /** Is emitted whenever settings have changed and a new search might be necessary. */
+    void configurationChanged(const DolphinQuery &dolphinQuery);
+
+protected:
+    void paintEvent(QPaintEvent *event) override;
+
+protected:
+    QToolButton *m_removeButton = nullptr;
+};
+
+/**
+ * @brief A button-sized UI component for modifying or removing search filters.
+ *
+ * A template widget is taken and this Chip forms a button-like outline around it. The Chip has a close button on the side to remove itself, which communicates
+ * to the user that the effect of the widget inside this Chip no longer applies. The functionality of the widget inside is not affected by the Chip.
+ *
+ * Most logic of this class is in the non-template ChipBase base class.
+ * @see ChipBase above.
+ */
+template<class Selector>
+class Chip : public ChipBase
+{
+public:
+    Chip(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr)
+        : ChipBase{dolphinQuery, parent}
+        , m_selector{new Selector{dolphinQuery, this}}
+    {
+        // Make the selector flat within the chip.
+        if constexpr (std::is_base_of<QComboBox, Selector>::value) {
+            m_selector->setFrame(false);
+        } else if constexpr (std::is_base_of<QToolButton, Selector>::value) {
+            m_selector->setAutoRaise(true);
+        }
+        setFocusProxy(m_selector);
+        setTabOrder(m_selector, m_removeButton);
+
+        connect(m_selector, &Selector::configurationChanged, this, &ChipBase::configurationChanged);
+
+        // The m_removeButton does not directly remove the Chip. Instead the Selector's removeRestriction() method will emit ChipBase::configurationChanged()
+        // with a DolphinQuery object that effectively removes the effects of the Selector. This in turn will then eventually remove this Chip when the new
+        // state of the Search UI components is propagated through the various UpdatableStateInterface::updateStateToMatch() methods.
+        connect(m_removeButton, &QAbstractButton::clicked, m_selector, &Selector::removeRestriction);
+
+        layout()->addWidget(m_selector);
+        layout()->addWidget(m_removeButton);
+    };
+
+private:
+    void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override
+    {
+        m_selector->updateStateToMatch(dolphinQuery);
+    }
+
+private:
+    Selector *const m_selector;
+};
+}
+
+#endif
diff --git a/src/search/configurationpopup.h b/src/search/configurationpopup.h
new file mode 100644 (file)
index 0000000..e69de29
index 1acebf8749bce149b57c6587dced758269dff4cb..59e6f58e3fd1b8479556817c1c76cede39598753 100644 (file)
         </entry>
         <entry name="What" type="String">
             <label>What</label>
-            <default>FileName</default>
+            <default>FileNames</default>
+        </entry>
+        <entry name="SearchTool" type="String">
+            <label>SearchTool</label>
+            <default>Filenamesearch</default>
         </entry>
     </group>
 </kcfg>
diff --git a/src/search/dolphinfacetswidget.cpp b/src/search/dolphinfacetswidget.cpp
deleted file mode 100644 (file)
index da36caa..0000000
+++ /dev/null
@@ -1,312 +0,0 @@
-/*
- *  SPDX-FileCopyrightText: 2012 Peter Penz <peter.penz19@gmail.com>
- *  SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
- *
- *  SPDX-License-Identifier: GPL-2.0-or-later
- */
-
-#include "dolphinfacetswidget.h"
-
-#include <KLocalizedString>
-#include <KProtocolInfo>
-
-#include <QComboBox>
-#include <QDate>
-#include <QEvent>
-#include <QHBoxLayout>
-#include <QIcon>
-#include <QMenu>
-#include <QToolButton>
-
-DolphinFacetsWidget::DolphinFacetsWidget(QWidget *parent)
-    : QWidget(parent)
-    , m_typeSelector(nullptr)
-    , m_dateSelector(nullptr)
-    , m_ratingSelector(nullptr)
-    , m_tagsSelector(nullptr)
-{
-    m_typeSelector = new QComboBox(this);
-    m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("none")), i18nc("@item:inlistbox", "Any Type"), QString());
-    m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("inode-directory")), i18nc("@item:inlistbox", "Folders"), QStringLiteral("Folder"));
-    m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("text-x-generic")), i18nc("@item:inlistbox", "Documents"), QStringLiteral("Document"));
-    m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("image-x-generic")), i18nc("@item:inlistbox", "Images"), QStringLiteral("Image"));
-    m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("audio-x-generic")), i18nc("@item:inlistbox", "Audio Files"), QStringLiteral("Audio"));
-    m_typeSelector->addItem(QIcon::fromTheme(QStringLiteral("video-x-generic")), i18nc("@item:inlistbox", "Videos"), QStringLiteral("Video"));
-    initComboBox(m_typeSelector);
-
-    const QDate currentDate = QDate::currentDate();
-
-    m_dateSelector = new QComboBox(this);
-    m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("view-calendar")), i18nc("@item:inlistbox", "Any Date"), QDate());
-    m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("go-jump-today")), i18nc("@item:inlistbox", "Today"), currentDate);
-    m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("go-jump-today")), i18nc("@item:inlistbox", "Yesterday"), currentDate.addDays(-1));
-    m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("view-calendar-week")),
-                            i18nc("@item:inlistbox", "This Week"),
-                            currentDate.addDays(1 - currentDate.dayOfWeek()));
-    m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("view-calendar-month")),
-                            i18nc("@item:inlistbox", "This Month"),
-                            currentDate.addDays(1 - currentDate.day()));
-    m_dateSelector->addItem(QIcon::fromTheme(QStringLiteral("view-calendar-year")),
-                            i18nc("@item:inlistbox", "This Year"),
-                            currentDate.addDays(1 - currentDate.dayOfYear()));
-    initComboBox(m_dateSelector);
-
-    m_ratingSelector = new QComboBox(this);
-    m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("non-starred-symbolic")), i18nc("@item:inlistbox", "Any Rating"), 0);
-    m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "1 or more"), 1);
-    m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "2 or more"), 2);
-    m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "3 or more"), 3);
-    m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "4 or more"), 4);
-    m_ratingSelector->addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "Highest Rating"), 5);
-    initComboBox(m_ratingSelector);
-
-    m_clearTagsAction = new QAction(QIcon::fromTheme(QStringLiteral("edit-clear-all")), i18nc("@action:inmenu", "Clear Selection"), this);
-    connect(m_clearTagsAction, &QAction::triggered, this, [this]() {
-        resetSearchTags();
-        Q_EMIT facetChanged();
-    });
-
-    m_tagsSelector = new QToolButton(this);
-    m_tagsSelector->setIcon(QIcon::fromTheme(QStringLiteral("tag")));
-    m_tagsSelector->setMenu(new QMenu(this));
-    m_tagsSelector->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
-    m_tagsSelector->setPopupMode(QToolButton::MenuButtonPopup);
-    m_tagsSelector->setAutoRaise(true);
-    updateTagsSelector();
-
-    connect(m_tagsSelector, &QToolButton::clicked, m_tagsSelector, &QToolButton::showMenu);
-    connect(m_tagsSelector->menu(), &QMenu::aboutToShow, this, &DolphinFacetsWidget::updateTagsMenu);
-    connect(&m_tagsLister, &KCoreDirLister::itemsAdded, this, &DolphinFacetsWidget::updateTagsMenuItems);
-    updateTagsMenu();
-
-    QHBoxLayout *topLayout = new QHBoxLayout(this);
-    topLayout->setContentsMargins(0, 0, 0, 0);
-    topLayout->addWidget(m_typeSelector);
-    topLayout->addWidget(m_dateSelector);
-    topLayout->addWidget(m_ratingSelector);
-    topLayout->addWidget(m_tagsSelector);
-
-    resetSearchTerms();
-}
-
-DolphinFacetsWidget::~DolphinFacetsWidget()
-{
-}
-
-void DolphinFacetsWidget::changeEvent(QEvent *event)
-{
-    if (event->type() == QEvent::EnabledChange) {
-        if (isEnabled()) {
-            updateTagsSelector();
-        } else {
-            resetSearchTerms();
-        }
-    }
-}
-
-QSize DolphinFacetsWidget::minimumSizeHint() const
-{
-    return QSize(0, m_typeSelector->minimumHeight());
-}
-
-void DolphinFacetsWidget::resetSearchTerms()
-{
-    m_typeSelector->setCurrentIndex(0);
-    m_dateSelector->setCurrentIndex(0);
-    m_ratingSelector->setCurrentIndex(0);
-
-    resetSearchTags();
-}
-
-QStringList DolphinFacetsWidget::searchTerms() const
-{
-    QStringList terms;
-
-    if (m_ratingSelector->currentIndex() > 0) {
-        const int rating = m_ratingSelector->currentData().toInt() * 2;
-        terms << QStringLiteral("rating>=%1").arg(rating);
-    }
-
-    if (m_dateSelector->currentIndex() > 0) {
-        const QDate date = m_dateSelector->currentData().toDate();
-        terms << QStringLiteral("modified>=%1").arg(date.toString(Qt::ISODate));
-    }
-
-    if (!m_searchTags.isEmpty()) {
-        for (auto const &tag : m_searchTags) {
-            if (tag.contains(QLatin1Char(' '))) {
-                terms << QStringLiteral("tag:\"%1\"").arg(tag);
-            } else {
-                terms << QStringLiteral("tag:%1").arg(tag);
-            }
-        }
-    }
-
-    return terms;
-}
-
-QString DolphinFacetsWidget::facetType() const
-{
-    return m_typeSelector->currentData().toString();
-}
-
-bool DolphinFacetsWidget::isSearchTerm(const QString &term) const
-{
-    static const QLatin1String searchTokens[]{QLatin1String("modified>="), QLatin1String("rating>="), QLatin1String("tag:"), QLatin1String("tag=")};
-
-    for (const auto &searchToken : searchTokens) {
-        if (term.startsWith(searchToken)) {
-            return true;
-        }
-    }
-    return false;
-}
-
-void DolphinFacetsWidget::setSearchTerm(const QString &term)
-{
-    if (term.startsWith(QLatin1String("modified>="))) {
-        const QString value = term.mid(10);
-        const QDate date = QDate::fromString(value, Qt::ISODate);
-        setTimespan(date);
-    } else if (term.startsWith(QLatin1String("rating>="))) {
-        const QString value = term.mid(8);
-        const int stars = value.toInt() / 2;
-        setRating(stars);
-    } else if (term.startsWith(QLatin1String("tag:")) || term.startsWith(QLatin1String("tag="))) {
-        const QString value = term.mid(4);
-        addSearchTag(value);
-    }
-}
-
-void DolphinFacetsWidget::setFacetType(const QString &type)
-{
-    for (int index = 0; index <= m_typeSelector->count(); index++) {
-        if (type == m_typeSelector->itemData(index).toString()) {
-            m_typeSelector->setCurrentIndex(index);
-            break;
-        }
-    }
-}
-
-void DolphinFacetsWidget::setRating(const int stars)
-{
-    if (stars < 0 || stars > 5) {
-        return;
-    }
-    m_ratingSelector->setCurrentIndex(stars);
-}
-
-void DolphinFacetsWidget::setTimespan(const QDate &date)
-{
-    if (!date.isValid()) {
-        return;
-    }
-    m_dateSelector->setCurrentIndex(0);
-    for (int index = 1; index <= m_dateSelector->count(); index++) {
-        if (date >= m_dateSelector->itemData(index).toDate()) {
-            m_dateSelector->setCurrentIndex(index);
-            break;
-        }
-    }
-}
-
-void DolphinFacetsWidget::addSearchTag(const QString &tag)
-{
-    if (tag.isEmpty() || m_searchTags.contains(tag)) {
-        return;
-    }
-    m_searchTags.append(tag);
-    m_searchTags.sort();
-    updateTagsSelector();
-}
-
-void DolphinFacetsWidget::removeSearchTag(const QString &tag)
-{
-    if (tag.isEmpty() || !m_searchTags.contains(tag)) {
-        return;
-    }
-    m_searchTags.removeAll(tag);
-    updateTagsSelector();
-}
-
-void DolphinFacetsWidget::resetSearchTags()
-{
-    m_searchTags = QStringList();
-    updateTagsSelector();
-    updateTagsMenu();
-}
-
-void DolphinFacetsWidget::initComboBox(QComboBox *combo)
-{
-    combo->setFrame(false);
-    combo->setMinimumHeight(parentWidget()->height());
-    combo->setCurrentIndex(0);
-    connect(combo, &QComboBox::activated, this, &DolphinFacetsWidget::facetChanged);
-}
-
-void DolphinFacetsWidget::updateTagsSelector()
-{
-    const bool hasListedTags = !m_tagsSelector->menu()->isEmpty();
-    const bool hasSelectedTags = !m_searchTags.isEmpty();
-
-    if (hasSelectedTags) {
-        const QString tagsText = m_searchTags.join(i18nc("String list separator", ", "));
-        m_tagsSelector->setText(i18ncp("@action:button %2 is a list of tags", "Tag: %2", "Tags: %2", m_searchTags.count(), tagsText));
-    } else {
-        m_tagsSelector->setText(i18nc("@action:button", "Add Tags"));
-    }
-
-    m_tagsSelector->setEnabled(isEnabled() && (hasListedTags || hasSelectedTags));
-    m_clearTagsAction->setEnabled(hasSelectedTags);
-}
-
-void DolphinFacetsWidget::updateTagsMenu()
-{
-    updateTagsMenuItems({}, {});
-    if (KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) {
-        m_tagsLister.openUrl(QUrl(QStringLiteral("tags:/")), KCoreDirLister::OpenUrlFlag::Reload);
-    }
-}
-
-void DolphinFacetsWidget::updateTagsMenuItems(const QUrl &, const KFileItemList &items)
-{
-    QMenu *tagsMenu = m_tagsSelector->menu();
-    tagsMenu->clear();
-
-    QStringList allTags = QStringList(m_searchTags);
-    for (const KFileItem &item : items) {
-        allTags.append(item.name());
-    }
-    allTags.sort(Qt::CaseInsensitive);
-    allTags.removeDuplicates();
-
-    const bool onlyOneTag = allTags.count() == 1;
-
-    for (const QString &tagName : std::as_const(allTags)) {
-        QAction *action = tagsMenu->addAction(QIcon::fromTheme(QStringLiteral("tag")), tagName);
-        action->setCheckable(true);
-        action->setChecked(m_searchTags.contains(tagName));
-
-        connect(action, &QAction::triggered, this, [this, tagName, onlyOneTag](bool isChecked) {
-            if (isChecked) {
-                addSearchTag(tagName);
-            } else {
-                removeSearchTag(tagName);
-            }
-            Q_EMIT facetChanged();
-
-            if (!onlyOneTag) {
-                m_tagsSelector->menu()->show();
-            }
-        });
-    }
-
-    if (allTags.count() > 1) {
-        tagsMenu->addSeparator();
-        tagsMenu->addAction(m_clearTagsAction);
-    }
-
-    updateTagsSelector();
-}
-
-#include "moc_dolphinfacetswidget.cpp"
diff --git a/src/search/dolphinfacetswidget.h b/src/search/dolphinfacetswidget.h
deleted file mode 100644 (file)
index c7358e2..0000000
+++ /dev/null
@@ -1,84 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2012 Peter Penz <peter.penz19@gmail.com>
- *
- * SPDX-License-Identifier: GPL-2.0-or-later
- */
-
-#ifndef DOLPHINFACETSWIDGET_H
-#define DOLPHINFACETSWIDGET_H
-
-#include <KCoreDirLister>
-#include <QWidget>
-
-class QComboBox;
-class QDate;
-class QEvent;
-class QToolButton;
-
-/**
- * @brief Allows to filter search-queries by facets.
- *
- * TODO: The current implementation is a temporary
- * workaround for the 4.9 release and represents no
- * real facets-implementation yet: There have been
- * some Dolphin specific user-interface and interaction
- * issues since 4.6 by embedding the Nepomuk facet-widget
- * into a QDockWidget (this is unrelated to the
- * Nepomuk facet-widget itself). Now in combination
- * with the search-shortcuts in the Places Panel some
- * existing issues turned into real showstoppers.
- *
- * So the longterm plan is to use the Nepomuk facets
- * again as soon as possible.
- */
-class DolphinFacetsWidget : public QWidget
-{
-    Q_OBJECT
-
-public:
-    explicit DolphinFacetsWidget(QWidget *parent = nullptr);
-    ~DolphinFacetsWidget() override;
-
-    QStringList searchTerms() const;
-    QString facetType() const;
-
-    bool isSearchTerm(const QString &term) const;
-    void setSearchTerm(const QString &term);
-    void resetSearchTerms();
-
-    void setFacetType(const QString &type);
-
-    QSize minimumSizeHint() const override;
-
-Q_SIGNALS:
-    void facetChanged();
-
-protected:
-    void changeEvent(QEvent *event) override;
-
-private Q_SLOTS:
-    void updateTagsMenu();
-    void updateTagsMenuItems(const QUrl &, const KFileItemList &items);
-
-private:
-    void setRating(const int stars);
-    void setTimespan(const QDate &date);
-    void addSearchTag(const QString &tag);
-    void removeSearchTag(const QString &tag);
-    void resetSearchTags();
-
-    void initComboBox(QComboBox *combo);
-    void updateTagsSelector();
-
-private:
-    QComboBox *m_typeSelector;
-    QComboBox *m_dateSelector;
-    QComboBox *m_ratingSelector;
-    QToolButton *m_tagsSelector;
-
-    QStringList m_searchTags;
-    KCoreDirLister m_tagsLister;
-    QAction *m_clearTagsAction;
-};
-
-#endif
index ed2a6a766b98a6d988aa23b721629e5e5c73654f..e23f1f08e9cbe7c1a6ffda9dae479925a1b63979 100644 (file)
@@ -1,20 +1,75 @@
 /*
- * SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
- *
- * SPDX-License-Identifier: GPL-2.0-or-later
- */
+    SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
 
-#include "dolphinquery.h"
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
 
-#include <QRegularExpression>
+#include "dolphinquery.h"
 
 #include "config-dolphin.h"
 #if HAVE_BALOO
+#include <Baloo/IndexerConfig>
 #include <Baloo/Query>
 #endif
+#include "dolphinplacesmodelsingleton.h"
+
+#include <KFileMetaData/TypeInfo>
+#include <KLocalizedString>
+
+#include <QRegularExpression>
+#include <QUrlQuery>
+
+using namespace Search;
+
+bool Search::isSupportedSearchScheme(const QString &urlScheme)
+{
+    static const QStringList supportedSchemes = {
+        QStringLiteral("filenamesearch"),
+        QStringLiteral("baloosearch"),
+        QStringLiteral("tags"),
+    };
+
+    return supportedSchemes.contains(urlScheme);
+}
+
+bool g_testMode = false;
+
+bool Search::isIndexingEnabledIn(QUrl directory)
+{
+    if (g_testMode) {
+        return true; // For unit-testing, let's pretend everything is indexed correctly.
+    }
+
+#if HAVE_BALOO
+    const Baloo::IndexerConfig searchInfo;
+    return searchInfo.fileIndexingEnabled() && !directory.isEmpty() && searchInfo.shouldBeIndexed(directory.toLocalFile());
+#else
+    Q_UNUSED(directory)
+    return false;
+#endif
+}
+
+bool Search::isContentIndexingEnabled()
+{
+    if (g_testMode) {
+        return true; // For unit-testing, let's pretend everything is indexed correctly.
+    }
+
+#if HAVE_BALOO
+    return !Baloo::IndexerConfig{}.onlyBasicIndexing();
+#else
+    return false;
+#endif
+}
 
 namespace
 {
+/** The path to be passed so Baloo searches everywhere. */
+constexpr auto balooSearchEverywherePath = QLatin1String("");
+/** The path to be passed so Filenamesearch searches everywhere. */
+constexpr auto filenamesearchEverywherePath = QLatin1String("file:///");
+
 #if HAVE_BALOO
 /** Checks if a given term in the Baloo::Query::searchString() is a special search term
  * @return: the specific search token of the term, or an empty QString() if none is found
@@ -23,6 +78,7 @@ QString searchTermToken(const QString &term)
 {
     static const QLatin1String searchTokens[]{QLatin1String("filename:"),
                                               QLatin1String("modified>="),
+                                              QLatin1String("modified>"),
                                               QLatin1String("rating>="),
                                               QLatin1String("tag:"),
                                               QLatin1String("tag=")};
@@ -69,110 +125,323 @@ QString trimChar(const QString &text, const QLatin1Char aChar)
 }
 }
 
-DolphinQuery DolphinQuery::fromSearchUrl(const QUrl &searchUrl)
+Search::DolphinQuery::DolphinQuery(const QUrl &url, const QUrl &backupSearchPath)
 {
-    DolphinQuery model;
-    model.m_searchUrl = searchUrl;
+    if (url.scheme() == QLatin1String("filenamesearch")) {
+        m_searchTool = SearchTool::Filenamesearch;
+        const QUrlQuery query(url);
+        const QString filenamesearchSearchPathString = query.queryItemValue(QStringLiteral("url"), QUrl::FullyDecoded);
+        const QUrl filenamesearchSearchPathUrl = QUrl::fromUserInput(filenamesearchSearchPathString, QString(), QUrl::AssumeLocalFile);
+        if (!filenamesearchSearchPathUrl.isValid() || filenamesearchSearchPathString == filenamesearchEverywherePath) {
+            // The parsed search location is either invalid or matches a string that represents searching "everywhere".
+            m_searchLocations = SearchLocations::Everywhere;
+            m_searchPath = backupSearchPath;
+        } else {
+            m_searchLocations = SearchLocations::FromHere;
+            m_searchPath = filenamesearchSearchPathUrl;
+        }
+        m_searchTerm = query.queryItemValue(QStringLiteral("search"), QUrl::FullyDecoded);
+        m_searchThrough = query.queryItemValue(QStringLiteral("checkContent")) == QLatin1String("yes") ? SearchThrough::FileContents : SearchThrough::FileNames;
+        return;
+    }
+
+#if HAVE_BALOO
+    if (url.scheme() == QLatin1String("baloosearch")) {
+        m_searchTool = SearchTool::Baloo;
+        initializeFromBalooQuery(Baloo::Query::fromSearchUrl(url), backupSearchPath);
+        return;
+    }
+#endif
 
-    if (searchUrl.scheme() == QLatin1String("baloosearch")) {
-        model.parseBalooQuery();
-    } else if (searchUrl.scheme() == QLatin1String("tags")) {
+    if (url.scheme() == QLatin1String("tags")) {
+#if HAVE_BALOO
+        m_searchTool = SearchTool::Baloo;
+#endif
+        m_searchLocations = SearchLocations::Everywhere;
+        m_searchPath = backupSearchPath;
         // tags can contain # symbols or slashes within the Url
-        QString tag = trimChar(searchUrl.toString(QUrl::RemoveScheme), QLatin1Char('/'));
-        model.m_searchTerms << QStringLiteral("tag:%1").arg(tag);
+        const auto tag = trimChar(url.toString(QUrl::RemoveScheme), QLatin1Char('/'));
+        if (!tag.isEmpty()) {
+            m_requiredTags.append(trimChar(url.toString(QUrl::RemoveScheme), QLatin1Char('/')));
+        }
+        return;
     }
 
-    return model;
+    m_searchPath = url;
+    switchToPreferredSearchTool();
 }
 
-bool DolphinQuery::supportsScheme(const QString &urlScheme)
+QUrl DolphinQuery::toUrl() const
 {
-    static const QStringList supportedSchemes = {
-        QStringLiteral("baloosearch"),
-        QStringLiteral("tags"),
-    };
+    // The following pre-conditions are sanity checks on this DolphinQuery object. If they fail, the issue is that we ever allowed the DolphinQuery to be in an
+    // inconsistent state to begin with. This should be fixed by bringing this DolphinQuery object into a reasonable state at the end of the constructors or
+    // setter methods which caused this impossible-to-fulfill combination of conditions.
+    Q_ASSERT_X(m_searchLocations == SearchLocations::Everywhere || m_searchPath.isValid(),
+               "DolphinQuery::toUrl()",
+               "We are supposed to search in a specific location but we do not know where!");
+#if HAVE_BALOO
+    Q_ASSERT_X(m_searchLocations == SearchLocations::Everywhere || m_searchTool != SearchTool::Baloo || isIndexingEnabledIn(m_searchPath),
+               "DolphinQuery::toUrl()",
+               "We are asking Baloo to search in a location which Baloo is not supposed to have indexed!");
+#endif
 
-    return supportedSchemes.contains(urlScheme);
-}
+    QUrl url;
 
-void DolphinQuery::parseBalooQuery()
-{
 #if HAVE_BALOO
-    const Baloo::Query query = Baloo::Query::fromSearchUrl(m_searchUrl);
+    /// Create Baloo search URL
+    if (m_searchTool == SearchTool::Baloo) {
+        Baloo::Query query;
+        if (m_fileType != KFileMetaData::Type::Empty) {
+            query.addType(KFileMetaData::TypeInfo{m_fileType}.name());
+        }
 
-    m_includeFolder = query.includeFolder();
+        QStringList balooQueryStrings = m_unrecognizedBalooQueryStrings;
 
-    const QStringList types = query.types();
-    m_fileType = types.isEmpty() ? QString() : types.first();
+        if (m_searchThrough == SearchThrough::FileContents) {
+            balooQueryStrings << m_searchTerm;
+        } else if (!m_searchTerm.isEmpty()) {
+            balooQueryStrings << QStringLiteral("filename:\"%1\"").arg(m_searchTerm);
+        }
 
-    QStringList textParts;
-    QString fileName;
+        if (m_searchLocations == SearchLocations::FromHere) {
+            query.setIncludeFolder(m_searchPath.toLocalFile());
+        }
 
-    const QStringList subTerms = splitOutsideQuotes(query.searchString());
-    for (const QString &subTerm : subTerms) {
-        const QString token = searchTermToken(subTerm);
-        const QString value = stripQuotes(subTerm.mid(token.length()));
+        if (m_modifiedSinceDate.isValid()) {
+            balooQueryStrings << QStringLiteral("modified>=%1").arg(m_modifiedSinceDate.toString(Qt::ISODate));
+        }
 
-        if (token == QLatin1String("filename:")) {
-            if (!value.isEmpty()) {
-                fileName = value;
-                m_hasFileName = true;
+        if (m_minimumRating >= 1) {
+            balooQueryStrings << QStringLiteral("rating>=%1").arg(m_minimumRating);
+        }
+
+        for (const auto &tag : m_requiredTags) {
+            if (tag.contains(QLatin1Char(' '))) {
+                balooQueryStrings << QStringLiteral("tag:\"%1\"").arg(tag);
+            } else {
+                balooQueryStrings << QStringLiteral("tag:%1").arg(tag);
             }
-            continue;
-        } else if (!token.isEmpty()) {
-            m_searchTerms << token + value;
-            continue;
-        } else if (subTerm == QLatin1String("AND") && subTerm != subTerms.at(0) && subTerm != subTerms.back()) {
-            continue;
-        } else if (!value.isEmpty()) {
-            textParts << value;
-            m_hasContentSearch = true;
         }
+
+        query.setSearchString(balooQueryStrings.join(QLatin1Char(' ')));
+
+        return query.toSearchUrl(QUrl::toPercentEncoding(title()));
     }
+#endif
 
-    if (m_hasFileName) {
-        if (m_hasContentSearch) {
-            textParts << QStringLiteral("filename:\"%1\"").arg(fileName);
-        } else {
-            textParts << fileName;
-        }
+    /// Create Filenamsearch search URL
+    url.setScheme(QStringLiteral("filenamesearch"));
+
+    QUrlQuery qUrlQuery;
+    qUrlQuery.addQueryItem(QStringLiteral("search"), QUrl::toPercentEncoding(m_searchTerm));
+    if (m_searchThrough == SearchThrough::FileContents) {
+        qUrlQuery.addQueryItem(QStringLiteral("checkContent"), QStringLiteral("yes"));
     }
 
-    m_searchText = textParts.join(QLatin1Char(' '));
-#endif
+    if (m_searchLocations == SearchLocations::FromHere && m_searchPath.isValid()) {
+        qUrlQuery.addQueryItem(QStringLiteral("url"), QUrl::toPercentEncoding(m_searchPath.url()));
+    } else {
+        // Search in root which is considered searching "everywhere".
+        qUrlQuery.addQueryItem(QStringLiteral("url"), QUrl::toPercentEncoding(filenamesearchEverywherePath));
+    }
+    qUrlQuery.addQueryItem(QStringLiteral("title"), QUrl::toPercentEncoding(title()));
+
+    url.setQuery(qUrlQuery);
+    return url;
 }
 
-QUrl DolphinQuery::searchUrl() const
+void DolphinQuery::setSearchLocations(SearchLocations searchLocations)
 {
-    return m_searchUrl;
+    m_searchLocations = searchLocations;
+    switchToPreferredSearchTool();
 }
 
-QString DolphinQuery::text() const
+void DolphinQuery::setSearchPath(const QUrl &searchPath)
 {
-    return m_searchText;
+    m_searchPath = searchPath;
+    switchToPreferredSearchTool();
 }
 
-QString DolphinQuery::type() const
+void DolphinQuery::setSearchThrough(SearchThrough searchThrough)
 {
-    return m_fileType;
+    m_searchThrough = searchThrough;
+    switchToPreferredSearchTool();
 }
 
-QStringList DolphinQuery::searchTerms() const
+void DolphinQuery::switchToPreferredSearchTool()
 {
-    return m_searchTerms;
+    const bool isIndexingEnabledInCurrentSearchLocation = m_searchLocations == SearchLocations::Everywhere || isIndexingEnabledIn(m_searchPath);
+    const bool searchThroughFileContentsWithoutIndexing = m_searchThrough == SearchThrough::FileContents && !isContentIndexingEnabled();
+    if (!isIndexingEnabledInCurrentSearchLocation || searchThroughFileContentsWithoutIndexing) {
+        m_searchTool = SearchTool::Filenamesearch;
+        return;
+    }
+#if HAVE_BALOO
+    // The current search location allows searching with Baloo. We switch to Baloo if this is the saved user preference.
+    if (SearchSettings::searchTool() == QStringLiteral("Baloo")) {
+        m_searchTool = SearchTool::Baloo;
+    }
+#endif
 }
 
-QString DolphinQuery::includeFolder() const
+#if HAVE_BALOO
+void DolphinQuery::initializeFromBalooQuery(const Baloo::Query &balooQuery, const QUrl &backupSearchPath)
 {
-    return m_includeFolder;
+    const QString balooSearchPathString = balooQuery.includeFolder();
+    const QUrl balooSearchPathUrl = QUrl::fromUserInput(balooSearchPathString, QString(), QUrl::AssumeLocalFile);
+    if (!balooSearchPathUrl.isValid() || balooSearchPathString == balooSearchEverywherePath) {
+        // The parsed search location is either invalid or matches a string that represents searching "everywhere" i.e. in all indexed locations.
+        m_searchLocations = SearchLocations::Everywhere;
+        m_searchPath = backupSearchPath;
+    } else {
+        m_searchLocations = SearchLocations::FromHere;
+        m_searchPath = balooSearchPathUrl;
+    }
+
+    const QStringList types = balooQuery.types();
+    // We currently only allow searching for one file type at once. (Searching for more seems out of scope for Dolphin anyway IMO.)
+    m_fileType = types.isEmpty() ? KFileMetaData::Type::Empty : KFileMetaData::TypeInfo::fromName(types.first()).type();
+
+    /// If nothing is requested, we use the default.
+    std::optional<SearchThrough> requestedToSearchThrough;
+    const QStringList subTerms = splitOutsideQuotes(balooQuery.searchString());
+    for (const QString &subTerm : subTerms) {
+        const QString token = searchTermToken(subTerm);
+        const QString value = stripQuotes(subTerm.mid(token.length()));
+
+        if (token == QLatin1String("filename:")) {
+            // This query is meant to not search in file contents.
+            if (!value.isEmpty()) {
+                if (m_searchTerm.isEmpty()) { // Seems like we already received a search term for the content search. We don't overwrite it because the Dolphin
+                                              // UI does not support searching for differing strings in content and file name.
+                    m_searchTerm = value;
+                }
+                if (!requestedToSearchThrough.has_value()) { // If requested to search thorugh contents, searching file names is already implied.
+                    requestedToSearchThrough = SearchThrough::FileNames;
+                }
+            }
+            continue;
+        } else if (token.startsWith(QLatin1String("modified>="))) {
+            m_modifiedSinceDate = QDate::fromString(value, Qt::ISODate);
+            continue;
+        } else if (token.startsWith(QLatin1String("modified>"))) {
+            m_modifiedSinceDate = QDate::fromString(value, Qt::ISODate).addDays(1);
+            continue;
+        } else if (token.startsWith(QLatin1String("rating>="))) {
+            m_minimumRating = value.toInt();
+            continue;
+        } else if (token.startsWith(QLatin1String("tag"))) {
+            m_requiredTags.append(value);
+            continue;
+        } else if (!token.isEmpty()) {
+            m_unrecognizedBalooQueryStrings << token + value;
+            continue;
+        } else if (subTerm == QLatin1String("AND") && subTerm != subTerms.at(0) && subTerm != subTerms.back()) {
+            continue;
+        } else if (!value.isEmpty()) {
+            // An empty token means this is just blank text, which is where the generic search term is located.
+            if (!m_searchTerm.isEmpty()) {
+                // Multiple search terms are separated by spaces.
+                m_searchTerm.append(QLatin1Char{' '});
+            }
+            m_searchTerm.append(value);
+            requestedToSearchThrough = SearchThrough::FileContents;
+        }
+    }
+    if (requestedToSearchThrough.has_value()) {
+        m_searchThrough = requestedToSearchThrough.value();
+    }
 }
+#endif // HAVE_BALOO
 
-bool DolphinQuery::hasContentSearch() const
+QString DolphinQuery::title() const
 {
-    return m_hasContentSearch;
+    if (m_searchLocations == SearchLocations::FromHere) {
+        QString prettySearchLocation;
+        KFilePlacesModel *placesModel = DolphinPlacesModelSingleton::instance().placesModel();
+        QModelIndex url_index = placesModel->closestItem(m_searchPath);
+        if (url_index.isValid() && placesModel->url(url_index).matches(m_searchPath, QUrl::StripTrailingSlash)) {
+            prettySearchLocation = placesModel->text(url_index);
+        } else {
+            prettySearchLocation = m_searchPath.fileName();
+        }
+        if (prettySearchLocation.isEmpty()) {
+            prettySearchLocation = m_searchPath.toString(QUrl::RemoveAuthority);
+        }
+
+        // A great title clearly identifies a search results page among many tabs, windows, or links in the Places panel.
+        // Using the search term is phenomenal for this because the user has typed this very specific string with a clear intention, so we definitely want to
+        // reuse the search term in the title if possible.
+        if (!m_searchTerm.isEmpty()) {
+            if (m_searchThrough == SearchThrough::FileNames) {
+                return i18nc("@title of a search results page. %1 is the search term a user entered, %2 is a folder name",
+                             "Search results for “%1” in %2",
+                             m_searchTerm,
+                             prettySearchLocation);
+            }
+            Q_ASSERT(m_searchThrough == SearchThrough::FileContents);
+            return i18nc("@title of a search results page. %1 is the search term a user entered, %2 is a folder name",
+                         "Files containing “%1” in %2",
+                         m_searchTerm,
+                         prettySearchLocation);
+        }
+        if (!m_requiredTags.isEmpty()) {
+            if (m_requiredTags.count() == 1) {
+                return i18nc("@title of a search results page. %1 is a tag e.g. 'important'. %2 is a folder name",
+                             "Search items tagged “%1” in %2",
+                             m_requiredTags.constFirst(),
+                             prettySearchLocation);
+            }
+            return i18nc("@title of a search results page. %1 and %2 are tags e.g. 'important'. %3 is a folder name",
+                         "Search items tagged “%1” and “%2” in %3",
+                         m_requiredTags.constFirst(),
+                         m_requiredTags.constLast(),
+                         prettySearchLocation);
+        }
+        if (m_fileType != KFileMetaData::Type::Empty) {
+            return i18nc("@title of a search results page for items of a specified type. %1 is a file type e.g. 'Document', 'Folder'. %2 is a folder name",
+                         "%1 search results in %2",
+                         KFileMetaData::TypeInfo{m_fileType}.displayName(),
+                         prettySearchLocation);
+        }
+        // Everything else failed so we use a very generic title.
+        return i18nc("@title of a search results page with items matching pre-defined conditions. %1 is a folder name",
+                     "Search results in %1",
+                     prettySearchLocation);
+    }
+
+    Q_ASSERT(m_searchLocations == SearchLocations::Everywhere);
+    // A great title clearly identifies a search results page among many tabs, windows, or links in the Places panel.
+    // Using the search term is phenomenal for this because the user has typed this very specific string with a clear intention, so we definitely want to reuse
+    // the search term in the title if possible.
+    if (!m_searchTerm.isEmpty()) {
+        if (m_searchThrough == SearchThrough::FileNames) {
+            return i18nc("@title of a search results page. %1 is the search term a user entered", "Search results for “%1”", m_searchTerm);
+        }
+        Q_ASSERT(m_searchThrough == SearchThrough::FileContents);
+        return i18nc("@title of a search results page. %1 is the search term a user entered", "Files containing “%1”", m_searchTerm);
+    }
+    if (!m_requiredTags.isEmpty()) {
+        if (m_requiredTags.count() == 1) {
+            return i18nc("@title of a search results page. %1 is a tag e.g. 'important'", "Search items tagged “%1”", m_requiredTags.constFirst());
+        }
+        return i18nc("@title of a search results page. %1 and %2 are tags e.g. 'important'",
+                     "Search items tagged “%1” and “%2”",
+                     m_requiredTags.constFirst(),
+                     m_requiredTags.constLast());
+    }
+    if (m_fileType != KFileMetaData::Type::Empty) {
+        // i18n: Results page for items of a specified type. %1 is a file type e.g. 'Audio', 'Document', 'Folder', 'Archive'. 'Presentation'.
+        // If putting such a file type at the start does not work in your language in this context, you might want to translate this liberally with
+        // something along the lines of 'Search items of type “%1”'.
+        return i18nc("@title of a search. %1 is file type", "%1 search results", KFileMetaData::TypeInfo{m_fileType}.displayName());
+    }
+    // Everything else failed so we use a very generic title.
+    return i18nc("@title of a search results page with items matching pre-defined conditions", "Search results");
 }
 
-bool DolphinQuery::hasFileName() const
+/** For testing Baloo in DolphinQueryTest even if it is not indexing any locations yet. */
+void Search::setTestMode()
 {
-    return m_hasFileName;
-}
+    g_testMode = true;
+};
index 1334958f153953ff2902e966b61d1f282c8b548c..6893e1855cc5fde95abfd5b754ea982aa6ad3af3 100644 (file)
 /*
- * SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
- *
- * SPDX-License-Identifier: GPL-2.0-or-later
- */
+    SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
 
 #ifndef DOLPHINQUERY_H
 #define DOLPHINQUERY_H
 
+#include "config-dolphin.h"
 #include "dolphin_export.h"
+#include "dolphin_searchsettings.h"
+
+#include <KFileMetaData/Types>
 
 #include <QString>
 #include <QUrl>
 
+#if HAVE_BALOO
+namespace Baloo
+{
+class Query;
+}
+#endif
+
+class DolphinQueryTest;
+
+namespace Search
+{
+
+/** Specifies which locations the user expects to be searched for matches. */
+enum class SearchLocations {
+    FromHere, /// Search in m_searchUrl and its sub-folders.
+    Everywhere, /// Search "Everywhere" as far as possible.
+};
+
+/** Specifies if items should be added to the search results when their file names or contents matches the search term. */
+enum class SearchThrough {
+    FileNames,
+    FileContents, // This option currently also includes any searches that search both through FileContents and FileNames at once because we currently provide
+                  // no option to toggle between only searching FileContents or FileContents & FileNames for any search tool.
+};
+
+enum class SearchTool {
+    Filenamesearch, // Contrary to its name, it can actually also search in file contents.
+#if HAVE_BALOO
+    Baloo,
+#endif
+};
+
+/** @returns whether Baloo is configured to have indexed the @p directory. */
+bool isIndexingEnabledIn(QUrl directory);
+
+/** @returns whether Baloo is configured to index file contents. */
+bool isContentIndexingEnabled();
+
+/** @returns whether the @p urlScheme should be considered a search scheme. */
+bool isSupportedSearchScheme(const QString &urlScheme);
+
 /**
- * @brief Simple query model that parses a Baloo search Url and extracts its
- * separate components to be displayed on dolphin search box.
+ * @brief An object that fully specifies a search configuration.
+ *
+ * A DolphinQuery encompasses all state information to uniquely identify a search. It describes the search term, search tool, search options, and requirements
+ * towards results. As such it can fully contain all state information of the Search::Bar because the search bars only goal is configuring and then triggering
+ * a search.
+ *
+ * The @a toUrl() method constructs a search URL from the DolphinQuery which Dolphin can open to start searching. Such a search URL can also be transformed
+ * back into a DolphinQuery object through the DolphinQuery constructor.
+ *
+ * When a DolphinQuery object is constructed or changed with incompatible conditions, like asking to search with an index-based search tool in a search path
+ * which is not indexed, DolphinQuery tries to fix itself so the final search can give meaningful results.
+ * Another example of this would be a DolphinQuery that is restricted to only search for images, which is then changed to use a search tool which does not
+ * allow restricting results to images. In that case the DolphinQuery object would not necessarily need to fix itself, but the exported search URL will ignore
+ * the image restriction, and the search user interface will need to update itself to make clear that the image restriction is ignored.
+ *
+ * Most widgets in the search UI have a `updateState()` method that takes a DolphinQuery as an argument. These methods will update the components' state to be
+ * in line with the DolphinQuery object's configuration.
  */
 class DolphinQuery
 {
 public:
-    /** Parses the components of @p searchUrl for the supported schemes */
-    static DolphinQuery fromSearchUrl(const QUrl &searchUrl);
-    /** Checks whether the DolphinQuery supports the given @p urlScheme */
-    static bool supportsScheme(const QString &urlScheme);
-
-    /** @return the \a searchUrl passed to Baloo::Query::fromSearchUrl() */
-    QUrl searchUrl() const;
-    /** @return the user text part of the query, to be shown in the searchbar */
-    QString text() const;
-    /** @return the first of Baloo::Query::types(), or an empty string */
-    QString type() const;
-    /** @return a list of the search terms of the Baloo::Query that act as a filter,
-     * such as \"rating>= <i>value<i>\" or \"modified>= <i>date<i>\"*/
-    QStringList searchTerms() const;
-    /** @return Baloo::Query::includeFolder(), that is, the initial directory
-     * for the query or an empty string if its a global search" */
-    QString includeFolder() const;
-    /** @return whether the query includes search in file content */
-    bool hasContentSearch() const;
-    /** @return whether the query includes a filter by fileName */
-    bool hasFileName() const;
+    /**
+     * @brief Automagically constructs a DolphinQuery based on the given @p url.
+     * @param url In the most usual case @p url is considered the search path and the DolphinQuery object is initialized based on saved user preferences.
+     *            However, if the @p url has query information encoded in itself, which is supposed to be the case if the QUrl::scheme() of the @p url is
+     *            "baloosearch", "tags", or "filenamesearch", this constructor retrieves all the information from the @p url and initializes the DolphinQuery
+     *            with it.
+     * @param backupSearchPath The last non-search location the user was on.
+     *                         A DolphinQuery object should always be fully constructible from the main @p url parameter. However, the data encoded in @url
+     *                         might not contain any search path, for example because the constructed DolphinQuery object is supposed to search "everywhere".
+     *                         This is fine until this DolphinQuery object is switched to search in a specific location instead. In that case, this
+     *                         @p backupSearchPath will become the new searchPath() of this DolphinQuery.
+     */
+    explicit DolphinQuery(const QUrl &url, const QUrl &backupSearchPath);
+
+    /**
+     * @returns a representation of this DolphinQuery as a QUrl. This QUrl can be opened in Dolphin to trigger a search that is identical to the conditions
+     *          provided by this DolphinQuery object.
+     */
+    QUrl toUrl() const;
+
+    void setSearchLocations(SearchLocations searchLocations);
+    inline SearchLocations searchLocations() const
+    {
+        return m_searchLocations;
+    };
+
+    /**
+     * Set this query to search in @p searchPath. However, if @a searchLocations() is set to "Everywhere", @p searchPath is effectively ignored because it is
+     * assumed that searching everywhere also includes @p searchPath.
+     */
+    void setSearchPath(const QUrl &searchPath);
+    /**
+     * @returns in which specific directory this query will search if the search location is not set to "Everywhere". When searching "Everywhere" this url is
+     *          ignored completely.
+     */
+    inline QUrl searchPath() const
+    {
+        return m_searchPath;
+    };
+
+    /**
+     * Set whether search results should match the search term with their names or contain it in their file contents.
+     */
+    void setSearchThrough(SearchThrough searchThrough);
+    inline SearchThrough searchThrough() const
+    {
+        return m_searchThrough;
+    };
+
+    /**
+     * Set the search tool or backend that will be used to @p searchTool.
+     */
+    inline void setSearchTool(SearchTool searchTool)
+    {
+        m_searchTool = searchTool;
+        // We do not remove any search parameters here, even if the new search tool does not support them. This is an attempt to avoid that we unnecessarily
+        // throw away configuration data. Non-applicable search parameters will be lost when exporting this DolphinQuery to a URL,
+        // but such an export won't happen if the changed DolphinQuery is not a valid search e.g. because the searchTerm().isEmpty() and every other search
+        // parameter is not supported by the new search tool.
+    };
+    /** @returns the search tool to be used for this search. */
+    inline SearchTool searchTool() const
+    {
+        return m_searchTool;
+    };
+
+    /**
+     * Sets the search text the user entered into the search field to @p searchTerm.
+     */
+    inline void setSearchTerm(const QString &searchTerm)
+    {
+        m_searchTerm = searchTerm;
+    };
+    /** @return the search text the user entered into the search field. */
+    inline QString searchTerm() const
+    {
+        return m_searchTerm;
+    };
+
+    /**
+     * Sets the type every search result should have.
+     */
+    inline void setFileType(const KFileMetaData::Type::Type &fileType)
+    {
+        m_fileType = fileType;
+    };
+    /**
+     * @return the requested file type this search will be restricted to.
+     */
+    inline KFileMetaData::Type::Type fileType() const
+    {
+        return m_fileType;
+    };
+
+    /**
+     * Sets the date since when every search result needs to have been modified.
+     */
+    inline void setModifiedSinceDate(const QDate &modifiedLaterThanDate)
+    {
+        m_modifiedSinceDate = modifiedLaterThanDate;
+    };
+    /**
+     * @return the date since when every search result needs to have been modified.
+     */
+    inline QDate modifiedSinceDate() const
+    {
+        return m_modifiedSinceDate;
+    };
+
+    /**
+     * @param minimumRating the minimum rating value every search result needs to at least have to be considered a valid result of this query.
+     *                      Values <= 0 mean no restriction. 1 is half a star, 2 one full star, etc. 10 is typically the maximum in KDE software.
+     */
+    inline void setMinimumRating(int minimumRating)
+    {
+        m_minimumRating = minimumRating;
+    };
+    /**
+     * @returns the minimum rating every search result is requested to have.
+     * @see setMinimumRating().
+     */
+    inline int minimumRating() const
+    {
+        return m_minimumRating;
+    };
+
+    /**
+     * @param requiredTags All the tags every search result is required to have.
+     */
+    inline void setRequiredTags(const QStringList &requiredTags)
+    {
+        m_requiredTags = requiredTags;
+    };
+    /**
+     * @returns all the tags every search result is required to have.
+     */
+    inline QStringList requiredTags() const
+    {
+        return m_requiredTags;
+    };
+
+    bool operator==(const DolphinQuery &) const = default;
+
+    /**
+     * @returns a title to be used in user-facing situations to represent this DolphinQuery, such as "Query Results from 'importantFile'".
+     */
+    QString title() const;
 
 private:
-    /** Calls Baloo::Query::fromSearchUrl() on the current searchUrl
-     * and parses the result to extract its separate components */
-    void parseBalooQuery();
+#if HAVE_BALOO
+    /** Parses a Baloo::Query to extract its separate components */
+    void initializeFromBalooQuery(const Baloo::Query &balooQuery, const QUrl &backupSearchPath);
+#endif
+
+    /**
+     * Switches to the user's preferred search tool if this is possible. If the preferred search tool cannot perform a search within this DolphinQuery's
+     * conditions, a different search tool will be used instead.
+     */
+    void switchToPreferredSearchTool();
 
 private:
-    QUrl m_searchUrl;
-    QString m_searchText;
-    QString m_fileType;
-    QStringList m_searchTerms;
-    QString m_includeFolder;
-    bool m_hasContentSearch = false;
-    bool m_hasFileName = false;
+    /** Specifies which locations will be searched for the search terms. */
+    SearchLocations m_searchLocations = SearchSettings::location() == QLatin1String("Everywhere") ? SearchLocations::Everywhere : SearchLocations::FromHere;
+
+    /**
+     * Specifies where searching will begin.
+     * @note The value of this variable is ignored when this query is set to search "Everywhere".
+     */
+    QUrl m_searchPath;
+
+    /** Specifies whether file names, file contents, or both will be searched for the search terms. */
+    SearchThrough m_searchThrough = SearchSettings::what() == QLatin1String("FileContents") ? SearchThrough::FileContents : SearchThrough::FileNames;
+
+    /** Specifies which search tool will be used for the search. */
+#if HAVE_BALOO
+    SearchTool m_searchTool = SearchSettings::searchTool() == QLatin1String("Baloo") ? SearchTool::Baloo : SearchTool::Filenamesearch;
+#else
+    SearchTool m_searchTool = SearchTool::Filenamesearch;
+#endif
+
+    QString m_searchTerm;
+    /** Specifies which file type all search results should have. "Empty" means there is no restriction on file type. */
+    KFileMetaData::Type::Type m_fileType = KFileMetaData::Type::Empty;
+
+    /** All search results are requested to be modified later than or equal to this date. Null or invalid dates mean no restriction. */
+    QDate m_modifiedSinceDate;
+
+    /**
+     * All search results are requested to have at least this rating.
+     * If the minimum rating is less than or equal to 0, this variable is ignored.
+     * 1 is generally considered half a star in KDE software, 2 a full star, etc. Generally 10 is considered the max rating i.e. 5/5 stars or a song marked as
+     * one of your favourites in a music application. Higher values are AFAIK not used in KDE applications but other software might use a different scale.
+     */
+    int m_minimumRating = 0;
+
+    /** All the tags every search result is required to have. */
+    QStringList m_requiredTags;
+
+    /**
+     * @brief Any imported Baloo search parameters (token:value pairs) which Dolphin does not understand are stored in this list unmodified.
+     * Dolphin only allows modifying a certain selection of search parameters, but there are more. This is a bit of an unfortunate situation because we can not
+     * represent every single query in the user interface without creating a mess of a UI. However, we also don't want to drop these extra parameters because
+     * that would unexpectedly modify the query.
+     * So this variable simply stores anything we don't recognize and reproduces it when exporting to a Baloo URL.
+     */
+    QStringList m_unrecognizedBalooQueryStrings;
+
+    friend DolphinQueryTest;
 };
 
+/**
+ * For testing Baloo in DolphinQueryTest even if it is not indexing any locations yet.
+ * The test mode makes sure that DolphinQuery can be set to use Baloo even if Baloo has not indexed any locations yet.
+ */
+void setTestMode();
+}
+
 #endif //DOLPHINQUERY_H
diff --git a/src/search/dolphinsearchbox.cpp b/src/search/dolphinsearchbox.cpp
deleted file mode 100644 (file)
index ee42550..0000000
+++ /dev/null
@@ -1,575 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz19@gmail.com>
- *
- * 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"
diff --git a/src/search/dolphinsearchbox.h b/src/search/dolphinsearchbox.h
deleted file mode 100644 (file)
index 6a847ba..0000000
+++ /dev/null
@@ -1,189 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2010 Peter Penz <peter.penz19@gmail.com>
- *
- * SPDX-License-Identifier: GPL-2.0-or-later
- */
-
-#ifndef DOLPHINSEARCHBOX_H
-#define DOLPHINSEARCHBOX_H
-
-#include "animatedheightwidget.h"
-
-#include <QUrl>
-
-class DolphinFacetsWidget;
-class DolphinQuery;
-class QLineEdit;
-class KSeparator;
-class QToolButton;
-class QScrollArea;
-class QLabel;
-class QVBoxLayout;
-
-/**
- * @brief Input box for searching files with or without Baloo.
- *
- * The widget allows to specify:
- * - Where to search: Everywhere or below the current directory
- * - What to search: Filenames or content
- *
- * If Baloo is available and the current folder is indexed, further
- * options are offered.
- */
-class DolphinSearchBox : public AnimatedHeightWidget
-{
-    Q_OBJECT
-
-public:
-    explicit DolphinSearchBox(QWidget *parent = nullptr);
-    ~DolphinSearchBox() override;
-
-    /**
-     * Sets the text that should be used as input for
-     * searching.
-     */
-    void setText(const QString &text);
-
-    /**
-     * Returns the text that should be used as input
-     * for searching.
-     */
-    QString text() const;
-
-    /**
-     * Sets the current path that is used as root for searching files.
-     * If @url is the Home dir, "From Here" is selected instead.
-     */
-    void setSearchPath(const QUrl &url);
-    QUrl searchPath() const;
-
-    /** @return URL that will start the searching of files. */
-    QUrl urlForSearching() const;
-
-    /**
-     * Extracts information from the given search \a url to
-     * initialize the search box properly.
-     */
-    void fromSearchUrl(const QUrl &url);
-
-    /**
-     * Selects the whole text of the search box.
-     */
-    void selectAll();
-
-    /**
-     * Set the search box to the active mode, if \a active
-     * is true. The active mode is default. The inactive mode only differs
-     * visually from the active mode, no change of the behavior is given.
-     *
-     * Using the search box in the inactive mode is useful when having split views,
-     * where the inactive view is indicated by an search box visually.
-     */
-    void setActive(bool active);
-
-    /**
-     * @return True, if the search box is in the active mode.
-     * @see    DolphinSearchBox::setActive()
-     */
-    bool isActive() const;
-
-    /*
-     * @see AnimatedHeightWidget::setVisible()
-     * @see QWidget::setVisible()
-     */
-    void setVisible(bool visible, Animated animated);
-
-protected:
-    void showEvent(QShowEvent *event) override;
-    void hideEvent(QHideEvent *event) override;
-    void keyReleaseEvent(QKeyEvent *event) override;
-    bool eventFilter(QObject *obj, QEvent *event) override;
-
-Q_SIGNALS:
-    /**
-     * Is emitted when a searching should be triggered.
-     */
-    void searchRequest();
-
-    /**
-     * Is emitted when the user has changed a character of
-     * the text that should be used as input for searching.
-     */
-    void searchTextChanged(const QString &text);
-
-    /**
-     * Emitted as soon as the search box should get closed.
-     */
-    void closeRequest();
-
-    /**
-     * Is emitted when the search box should be opened.
-     */
-    void openRequest();
-
-    /**
-     * Is emitted, if the searchbox has been activated by
-     * an user interaction
-     * @see DolphinSearchBox::setActive()
-     */
-    void activated();
-    void focusViewRequest();
-
-private Q_SLOTS:
-    void emitSearchRequest();
-    void emitCloseRequest();
-    void slotConfigurationChanged();
-    void slotSearchTextChanged(const QString &text);
-    void slotReturnPressed();
-    void slotFacetChanged();
-    void slotSearchSaved();
-
-private:
-    void initButton(QToolButton *button);
-    void loadSettings();
-    void saveSettings();
-    void init();
-
-    /**
-     * @return URL that represents the Baloo query for starting the search.
-     */
-    QUrl balooUrlForSearching() const;
-
-    /**
-     * Sets the searchbox UI with the parameters established by the \a query
-     */
-    void updateFromQuery(const DolphinQuery &query);
-
-    void updateFacetsVisible();
-
-    bool isIndexingEnabled() const;
-
-    /** @see AnimatedHeightWidget::preferredHeight() */
-    int preferredHeight() const override;
-
-private:
-    QString queryTitle(const QString &text) const;
-
-    bool m_startedSearching;
-    bool m_active;
-
-    QVBoxLayout *m_topLayout;
-
-    QLineEdit *m_searchInput;
-    QAction *m_saveSearchAction;
-    QScrollArea *m_optionsScrollArea;
-    QToolButton *m_fileNameButton;
-    QToolButton *m_contentButton;
-    KSeparator *m_separator;
-    QToolButton *m_fromHereButton;
-    QToolButton *m_everywhereButton;
-    DolphinFacetsWidget *m_facetsWidget;
-
-    QUrl m_searchPath;
-
-    QTimer *m_startSearchTimer;
-
-    bool m_initialized;
-};
-
-#endif
diff --git a/src/search/popup.cpp b/src/search/popup.cpp
new file mode 100644 (file)
index 0000000..ce0fb21
--- /dev/null
@@ -0,0 +1,378 @@
+/*
+    This file is part of the KDE project
+    SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "popup.h"
+
+#include "config-dolphin.h"
+#include "dolphinpackageinstaller.h"
+#include "dolphinquery.h"
+#include "global.h"
+#include "selectors/dateselector.h"
+#include "selectors/filetypeselector.h"
+#include "selectors/minimumratingselector.h"
+#include "selectors/tagsselector.h"
+
+#include <KContextualHelpButton>
+#include <KDialogJobUiDelegate>
+#include <KIO/ApplicationLauncherJob>
+#include <KIO/CommandLauncherJob>
+#include <KLocalizedString>
+#include <KService>
+
+#include <QButtonGroup>
+#ifdef Q_OS_WIN
+#include <QDesktopServices>
+#endif
+#include <QHBoxLayout>
+#include <QLabel>
+#include <QMenu>
+#include <QRadioButton>
+#include <QStandardPaths>
+#include <QToolButton>
+#include <QVBoxLayout>
+
+namespace
+{
+constexpr auto kFindDesktopName = "org.kde.kfind";
+}
+
+using namespace Search;
+
+QString Search::filenamesearchUiName()
+{
+    // i18n: Localized name for the Filenamesearch search tool for use in user interfaces.
+    return i18n("Simple search");
+};
+
+QString Search::balooUiName()
+{
+    // i18n: Localized name for the Baloo search tool for use in user interfaces.
+    return i18n("File Indexing");
+};
+
+Popup::Popup(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent)
+    : WidgetMenu{parent}
+    , UpdatableStateInterface{dolphinQuery}
+{
+}
+
+QWidget *Popup::init()
+{
+    auto containerWidget = new QWidget{this};
+    containerWidget->setContentsMargins(Dolphin::VERTICAL_SPACER_HEIGHT,
+                                        Dolphin::VERTICAL_SPACER_HEIGHT,
+                                        Dolphin::VERTICAL_SPACER_HEIGHT, // Using the same value for every spacing in this containerWidget looks nice.
+                                        Dolphin::VERTICAL_SPACER_HEIGHT);
+    auto verticalMainLayout = new QVBoxLayout{containerWidget};
+    verticalMainLayout->setSpacing((2 * Dolphin::VERTICAL_SPACER_HEIGHT) / 3); // A bit less spacing between rows than when adding an explicit spacer.
+
+    /// Add UI to switch between only searching in file names or also in contents.
+    auto searchInLabel = new QLabel{i18nc("@title:group", "Search in:"), containerWidget};
+    searchInLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
+    verticalMainLayout->addWidget(searchInLabel);
+
+    m_searchInFileNamesRadioButton = new QRadioButton{i18nc("@option:radio Search in:", "File names"), containerWidget};
+    connect(m_searchInFileNamesRadioButton, &QAbstractButton::clicked, this, [this]() {
+        if (m_searchConfiguration->searchThrough() == SearchThrough::FileNames) {
+            return; // Already selected.
+        }
+        SearchSettings::setWhat(QStringLiteral("FileNames"));
+        SearchSettings::self()->save();
+        DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+        searchConfigurationCopy.setSearchThrough(SearchThrough::FileNames);
+        Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
+    });
+    verticalMainLayout->addWidget(m_searchInFileNamesRadioButton);
+
+    m_searchInFileContentsRadioButton = new QRadioButton{containerWidget};
+    connect(m_searchInFileContentsRadioButton, &QAbstractButton::clicked, this, [this]() {
+        if (m_searchConfiguration->searchThrough() == SearchThrough::FileContents) {
+            return; // Already selected.
+        }
+        SearchSettings::setWhat(QStringLiteral("FileContents"));
+        SearchSettings::self()->save();
+        DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+        searchConfigurationCopy.setSearchThrough(SearchThrough::FileContents);
+        Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
+    });
+    verticalMainLayout->addWidget(m_searchInFileContentsRadioButton);
+
+    auto searchInButtonGroup = new QButtonGroup{this};
+    searchInButtonGroup->addButton(m_searchInFileNamesRadioButton);
+    searchInButtonGroup->addButton(m_searchInFileContentsRadioButton);
+
+    /// Add UI to switch between search tools.
+    // When we build without Baloo, there is only one search tool available, so we skip adding the UI to switch.
+#if HAVE_BALOO
+    verticalMainLayout->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT);
+
+    auto searchUsingLabel = new QLabel{i18nc("@title:group", "Search using:"), containerWidget};
+    searchUsingLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
+    verticalMainLayout->addWidget(searchUsingLabel);
+
+    /// Initialize the Filenamesearch row.
+    m_filenamesearchRadioButton = new QRadioButton{filenamesearchUiName(), containerWidget};
+    connect(m_filenamesearchRadioButton, &QAbstractButton::clicked, this, [this]() {
+        if (m_searchConfiguration->searchTool() == SearchTool::Filenamesearch) {
+            return; // Already selected.
+        }
+        SearchSettings::setSearchTool(QStringLiteral("Filenamesearch"));
+        SearchSettings::self()->save();
+        DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+        searchConfigurationCopy.setSearchTool(SearchTool::Filenamesearch);
+        Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
+    });
+
+    m_filenamesearchContextualHelpButton = new KContextualHelpButton(
+        xi18nc("@info about a search tool",
+               "<para>For searching in file contents <application>%1</application> attempts to use third-party search tools if they are available on this "
+               "system and are expected to lead to better or faster results. <application>ripgrep</application> and <application>ripgrep-all</application> "
+               "might improve your search experience if they are installed. <application>ripgrep-all</application> in particular enables searches in more "
+               "file types (e.g. pdf, docx, sqlite, jpg, movie subtitles (mkv, mp4)).</para><para>The manner in which these search tools are invoked can be "
+               "configured by editing a script file. Copy it from <filename>%2</filename> to <filename>%3</filename> before modifying your copy. If any "
+               "issues arise, delete your copy <filename>%3</filename> to revert your changes.</para>",
+               filenamesearchUiName(),
+               QStringLiteral("%1/kio_filenamesearch/kio-filenamesearch-grep").arg(KDE_INSTALL_FULL_DATADIR),
+               QStringLiteral("%1/kio_filenamesearch/kio-filenamesearch-grep").arg(QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation))),
+        m_filenamesearchRadioButton,
+        containerWidget);
+
+    auto filenamesearchRowLayout = new QHBoxLayout;
+    filenamesearchRowLayout->addWidget(m_filenamesearchRadioButton);
+    filenamesearchRowLayout->addWidget(m_filenamesearchContextualHelpButton);
+    filenamesearchRowLayout->addStretch(); // for left-alignment
+    verticalMainLayout->addLayout(filenamesearchRowLayout);
+
+    /// Initialize the Baloo row.
+    m_balooRadioButton = new QRadioButton{balooUiName(), containerWidget};
+    connect(m_balooRadioButton, &QAbstractButton::clicked, this, [this]() {
+        if (m_searchConfiguration->searchTool() == SearchTool::Baloo) {
+            return; // Already selected.
+        }
+        SearchSettings::setSearchTool(QStringLiteral("Baloo"));
+        SearchSettings::self()->save();
+        DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+        searchConfigurationCopy.setSearchTool(SearchTool::Baloo);
+        Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
+    });
+
+    m_balooContextualHelpButton = new KContextualHelpButton(QString(), m_balooRadioButton, containerWidget);
+
+    auto balooSettingsButton = new QToolButton{containerWidget};
+    balooSettingsButton->setText(i18nc("@action:button %1 is software name", "Configure %1…", balooUiName()));
+    balooSettingsButton->setIcon(QIcon::fromTheme("configure"));
+    balooSettingsButton->setToolTip(balooSettingsButton->text());
+    balooSettingsButton->setToolButtonStyle(Qt::ToolButtonIconOnly);
+    balooSettingsButton->setAutoRaise(true);
+    balooSettingsButton->setFixedHeight(m_balooRadioButton->sizeHint().height());
+    connect(balooSettingsButton, &QToolButton::clicked, this, [containerWidget] {
+        // Code taken from KCMLauncher::openSystemSettings() in the KCMUtil KDE framework.
+        constexpr auto systemSettings = "systemsettings";
+        KIO::CommandLauncherJob *openBalooSettingsJob;
+        // Open in System Settings if it's available
+        if (KService::serviceByDesktopName(systemSettings)) {
+            openBalooSettingsJob = new KIO::CommandLauncherJob(systemSettings, {"kcm_baloofile"}, containerWidget);
+            openBalooSettingsJob->setDesktopName(systemSettings);
+        } else {
+            openBalooSettingsJob = new KIO::CommandLauncherJob(QStringLiteral("kcmshell6"), {"kcm_baloofile"}, containerWidget);
+        }
+        openBalooSettingsJob->setUiDelegate(new KDialogJobUiDelegate(KJobUiDelegate::AutoHandlingEnabled, containerWidget));
+        openBalooSettingsJob->start();
+    });
+
+    auto balooRowLayout = new QHBoxLayout;
+    balooRowLayout->addWidget(m_balooRadioButton);
+    balooRowLayout->addWidget(m_balooContextualHelpButton);
+    balooRowLayout->addWidget(balooSettingsButton);
+    balooRowLayout->addStretch(); // for left-alignment
+    verticalMainLayout->addLayout(balooRowLayout);
+
+    auto searchUsingButtonGroup = new QButtonGroup{this};
+    searchUsingButtonGroup->addButton(m_filenamesearchRadioButton);
+    searchUsingButtonGroup->addButton(m_balooRadioButton);
+
+    verticalMainLayout->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT);
+
+    /// Add extra search filters like date, tags, rating, etc.
+    m_selectorsLayoutWidget = new QWidget{containerWidget};
+    if (m_searchConfiguration->searchTool() == SearchTool::Filenamesearch) {
+        m_selectorsLayoutWidget->hide();
+    }
+    auto selectorsLayout = new QGridLayout{m_selectorsLayoutWidget};
+    selectorsLayout->setContentsMargins(0, 0, 0, 0);
+    selectorsLayout->setSpacing(verticalMainLayout->spacing());
+
+    auto typeSelectorTitle = new QLabel{i18nc("@title:group for filtering files based on their type", "File Type:"), containerWidget};
+    typeSelectorTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
+    selectorsLayout->addWidget(typeSelectorTitle, 1, 0);
+
+    m_typeSelector = new FileTypeSelector{m_searchConfiguration, containerWidget};
+    connect(m_typeSelector, &FileTypeSelector::configurationChanged, this, &Popup::configurationChanged);
+    selectorsLayout->addWidget(m_typeSelector, 2, 0);
+
+    auto dateSelectorTitle = new QLabel{i18nc("@title:group for filtering files by modified date", "Modified since:"), containerWidget};
+    dateSelectorTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
+    selectorsLayout->addWidget(dateSelectorTitle, 1, 1);
+
+    m_dateSelector = new DateSelector{m_searchConfiguration, containerWidget};
+    m_dateSelector->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); // Make sure this button is as wide as the other button in this column.
+    connect(m_dateSelector, &DateSelector::configurationChanged, this, &Popup::configurationChanged);
+    selectorsLayout->addWidget(m_dateSelector, 2, 1);
+
+    auto ratingSelectorTitle = new QLabel{i18nc("@title:group for selecting a minimum rating of search results", "Rating:"), containerWidget};
+    ratingSelectorTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
+    selectorsLayout->addWidget(ratingSelectorTitle, 3, 0);
+
+    m_ratingSelector = new MinimumRatingSelector{m_searchConfiguration, containerWidget};
+    connect(m_ratingSelector, &MinimumRatingSelector::configurationChanged, this, &Popup::configurationChanged);
+    selectorsLayout->addWidget(m_ratingSelector, 4, 0);
+
+    auto tagsSelectorTitle = new QLabel{i18nc("@title:group for selecting required tags for search results", "Tags:"), containerWidget};
+    tagsSelectorTitle->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
+    selectorsLayout->addWidget(tagsSelectorTitle, 3, 1);
+
+    m_tagsSelector = new TagsSelector{m_searchConfiguration, containerWidget};
+    m_tagsSelector->setSizePolicy(QSizePolicy::Minimum, QSizePolicy::Fixed); // Make sure this button is as wide as the other button in this column.
+    connect(m_tagsSelector, &TagsSelector::configurationChanged, this, &Popup::configurationChanged);
+    selectorsLayout->addWidget(m_tagsSelector, 4, 1);
+
+    verticalMainLayout->addWidget(m_selectorsLayoutWidget);
+#endif // HAVE_BALOO
+
+    /**
+     * Dolphin cannot provide every advanced search workflow, so here at the end we need to push users to more dedicated search tools if what Dolphin provides
+     * turns out to be insufficient.
+     */
+    verticalMainLayout->addSpacing(Dolphin::VERTICAL_SPACER_HEIGHT);
+
+    auto kfindLabel = new QLabel{i18nc("@label above 'Install KFind'/'Open KFind' button", "For more advanced searches:"), containerWidget};
+    kfindLabel->setTextInteractionFlags(Qt::TextSelectableByMouse | Qt::TextSelectableByKeyboard | Qt::LinksAccessibleByKeyboard);
+    verticalMainLayout->addWidget(kfindLabel);
+
+    m_kFindButton = new QToolButton{containerWidget};
+    m_kFindButton->setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+    connect(m_kFindButton, &QToolButton::clicked, this, &Popup::slotKFindButtonClicked);
+    verticalMainLayout->addWidget(m_kFindButton);
+
+    return containerWidget;
+}
+
+void Popup::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery)
+{
+    m_searchInFileNamesRadioButton->setChecked(dolphinQuery->searchThrough() == SearchThrough::FileNames);
+    m_searchInFileContentsRadioButton->setChecked(dolphinQuery->searchThrough() == SearchThrough::FileContents);
+
+    // When we build without Baloo, there is only one search tool available and no UI to switch.
+#if HAVE_BALOO
+    m_filenamesearchRadioButton->setChecked(dolphinQuery->searchTool() == SearchTool::Filenamesearch);
+    m_filenamesearchContextualHelpButton->setVisible(dolphinQuery->searchThrough() == SearchThrough::FileContents);
+
+    if (dolphinQuery->searchLocations() != SearchLocations::Everywhere && !isIndexingEnabledIn(dolphinQuery->searchPath())) {
+        m_balooRadioButton->setToolTip(
+            xi18nc("@info:tooltip",
+                   "<para>Searching in <filename>%1</filename> using <application>%2</application> is currently not possible because "
+                   "<application>%2</application> is configured to never create a search index of that location.</para>",
+                   dolphinQuery->searchPath().adjusted(QUrl::RemoveUserInfo | QUrl::StripTrailingSlash).toString(QUrl::PreferLocalFile),
+                   balooUiName()));
+        m_balooRadioButton->setDisabled(true);
+    } else if (dolphinQuery->searchThrough() == SearchThrough::FileContents && !isContentIndexingEnabled()) {
+        m_balooRadioButton->setToolTip(xi18nc("@info:tooltip",
+                                              "<para>Searching through file contents using <application>%1</application> is currently not possible because "
+                                              "<application>%1</application> is configured to never create a search index for file contents.</para>",
+                                              balooUiName()));
+        m_balooRadioButton->setDisabled(true);
+    } else {
+        m_balooRadioButton->setToolTip(QString());
+        m_balooRadioButton->setEnabled(true);
+    }
+    m_balooContextualHelpButton->setContextualHelpText(
+        i18nc("@info make a warning paragraph bold before other paragraphs", "<b>%1</b>", m_balooRadioButton->toolTip())
+        + xi18nc(
+            "@info about a search tool",
+            "<para><application>%1</application> uses a database for searching. The database is created by indexing your files in the background based on "
+            "how <application>%1</application> is configured.<list><item><application>%1</application> provides results extremely "
+            "quickly.</item><item>Allows searching for file types, dates, tags, etc.</item><item>Only searches in indexed folders. Configure which folders "
+            "should be indexed in <application>System Settings</application>.</item><item>When the searched locations contain links to other files or "
+            "folders, those will not be searched or show up in search results.</item><item>Hidden files and folders and their contents might also not be "
+            "searched depending on how <application>%1</application> is configured.</item></list></para>",
+            balooUiName()));
+
+    m_balooRadioButton->setChecked(dolphinQuery->searchTool() == SearchTool::Baloo);
+    m_balooRadioButton->setChecked(false);
+
+    if (m_balooRadioButton->isChecked()) {
+        m_searchInFileContentsRadioButton->setText(i18nc("@option:radio Search in:", "File names and contents"));
+        m_typeSelector->updateStateToMatch(dolphinQuery);
+        m_dateSelector->updateStateToMatch(dolphinQuery);
+        m_ratingSelector->updateStateToMatch(dolphinQuery);
+        m_tagsSelector->updateStateToMatch(dolphinQuery);
+    } else {
+#endif // HAVE_BALOO
+        m_searchInFileContentsRadioButton->setText(i18nc("@option:radio Search in:", "File contents"));
+#if HAVE_BALOO
+    }
+
+    /// Show/Hide Baloo-specific selectors.
+    m_selectorsLayoutWidget->setVisible(m_balooRadioButton->isChecked());
+    const int columnWidth = std::max(
+        {m_typeSelector->sizeHint().width(), m_dateSelector->sizeHint().width(), m_ratingSelector->sizeHint().width(), m_tagsSelector->sizeHint().width()});
+    static_cast<QGridLayout *>(m_selectorsLayoutWidget->layout())->setColumnMinimumWidth(0, columnWidth);
+    static_cast<QGridLayout *>(m_selectorsLayoutWidget->layout())->setColumnMinimumWidth(1, columnWidth);
+    resizeToFitContents();
+#endif // HAVE_BALOO
+
+    KService::Ptr kFind = KService::serviceByDesktopName(kFindDesktopName);
+    if (kFind) {
+        m_kFindButton->setText(i18nc("@action:button 1 is KFind app name", "Open %1", kFind->name()));
+        m_kFindButton->setIcon(QIcon::fromTheme(kFind->icon()));
+    } else {
+        m_kFindButton->setText(i18nc("@action:button", "Install KFind…"));
+        m_kFindButton->setIcon(QIcon::fromTheme(QStringLiteral("kfind"), QIcon::fromTheme(QStringLiteral("install"))));
+    }
+}
+
+void Popup::slotKFindButtonClicked()
+{
+    /// Open KFind if it is installed.
+    KService::Ptr kFind = KService::serviceByDesktopName(kFindDesktopName);
+    if (kFind) {
+        auto *job = new KIO::ApplicationLauncherJob(kFind);
+        job->setUrls({m_searchConfiguration->searchPath()});
+        job->start();
+        return;
+    }
+
+    /// Otherwise, install KFind.
+#ifdef Q_OS_WIN
+    QDesktopServices::openUrl(QUrl("https://apps.kde.org/kfind"));
+#else
+    auto packageInstaller = new DolphinPackageInstaller(
+        KFIND_PACKAGE_NAME,
+        QUrl("appstream://org.kde.kfind.desktop"),
+        []() {
+            return KService::serviceByDesktopName(kFindDesktopName);
+        },
+        this);
+    connect(packageInstaller, &KJob::result, this, [this](KJob *job) {
+        Q_EMIT showInstallationProgress(QString(), 100); // Hides the progress information in the status bar.
+        if (job->error()) {
+            Q_EMIT showMessage(job->errorString(), KMessageWidget::Error);
+        } else {
+            Q_EMIT showMessage(xi18nc("@info", "<application>KFind</application> installed successfully."), KMessageWidget::Positive);
+            updateStateToMatch(m_searchConfiguration); // Updates m_kfindButton from an "Install KFind…" to an "Open KFind" button.
+        }
+    });
+    const auto installationTaskText{i18nc("@info:status", "Installing KFind")};
+    Q_EMIT showInstallationProgress(installationTaskText, -1);
+    connect(packageInstaller, &KJob::percentChanged, this, [this, installationTaskText](KJob * /* job */, long unsigned int percent) {
+        if (percent < 100) { // Ignore some weird reported values.
+            Q_EMIT showInstallationProgress(installationTaskText, percent);
+        }
+    });
+    packageInstaller->start();
+#endif
+}
diff --git a/src/search/popup.h b/src/search/popup.h
new file mode 100644 (file)
index 0000000..423c9dd
--- /dev/null
@@ -0,0 +1,99 @@
+/*
+    This file is part of the KDE project
+    SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef POPUP_H
+#define POPUP_H
+
+#include "dolphinquery.h"
+#include "updatablestateinterface.h"
+#include "widgetmenu.h"
+
+#include <KMessageWidget>
+
+#include <QUrl>
+
+class KContextualHelpButton;
+class QRadioButton;
+class QToolButton;
+
+namespace Search
+{
+class DateSelector;
+class FileTypeSelector;
+class MinimumRatingSelector;
+class TagsSelector;
+
+/** @returns the localized name for the Filenamesearch search tool for use in user interfaces. */
+QString filenamesearchUiName();
+
+/** @returns the localized name for the Baloo search tool for use in user interfaces. */
+QString balooUiName();
+
+/**
+ * This object contains most of the UI to set the search configuration.
+ */
+class Popup : public WidgetMenu, public UpdatableStateInterface
+{
+    Q_OBJECT
+
+public:
+    explicit Popup(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr);
+
+Q_SIGNALS:
+    /** Is emitted whenever settings have changed and a new search might be necessary. */
+    void configurationChanged(const DolphinQuery &dolphinQuery);
+
+    /**
+     * Requests for @p message with the given @p messageType to be shown to the user in a non-modal way.
+     */
+    void showMessage(const QString &message, KMessageWidget::MessageType messageType);
+
+    /**
+     * Requests for a progress update to be shown to the user in a non-modal way.
+     * @param currentlyRunningTaskTitle     The task that is currently progressing.
+     * @param installationProgressPercent   The current percentage of completion.
+     */
+    void showInstallationProgress(const QString &currentlyRunningTaskTitle, int installationProgressPercent);
+
+private:
+    QWidget *init() override;
+
+    void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override;
+
+private Q_SLOTS:
+    /**
+     * Opens KFind if KFind is installed.
+     * If KFind is not installed, this method asynchronously starts a Filelight installation using DolphinPackageInstaller. @see DolphinPackageInstaller.
+     * Installation success or failure is reported through showMessage(). @see Popup::showMessage().
+     * Installation progress is reported through showInstallationProgress(). @see Popup::showInstallationProgress().
+     */
+    void slotKFindButtonClicked();
+
+private:
+    QRadioButton *m_searchInFileNamesRadioButton = nullptr;
+    QRadioButton *m_searchInFileContentsRadioButton = nullptr;
+    QRadioButton *m_filenamesearchRadioButton = nullptr;
+    KContextualHelpButton *m_filenamesearchContextualHelpButton = nullptr;
+    QRadioButton *m_balooRadioButton = nullptr;
+    KContextualHelpButton *m_balooContextualHelpButton = nullptr;
+    /** A container widget for easy showing/hiding of all selectors. */
+    QWidget *m_selectorsLayoutWidget = nullptr;
+    /** Allows to set the file type each search result is expected to have. */
+    FileTypeSelector *m_typeSelector = nullptr;
+    /** Allows to set a date since when each search result needs to have been modified. */
+    DateSelector *m_dateSelector = nullptr;
+    /** Allows selecting the minimum rating search results are expected to have. */
+    MinimumRatingSelector *m_ratingSelector = nullptr;
+    /** Allows to set tags which each search result is required to have. */
+    TagsSelector *m_tagsSelector = nullptr;
+    /** A button that allows installing or opening KFind. */
+    QToolButton *m_kFindButton = nullptr;
+};
+
+}
+
+#endif // POPUP_H
diff --git a/src/search/selectors/dateselector.cpp b/src/search/selectors/dateselector.cpp
new file mode 100644 (file)
index 0000000..2951b1d
--- /dev/null
@@ -0,0 +1,61 @@
+/*
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "dateselector.h"
+
+#include "../dolphinquery.h"
+
+#include <KDatePicker>
+#include <KDatePickerPopup>
+#include <KFormat>
+#include <KLocalizedString>
+
+using namespace Search;
+
+Search::DateSelector::DateSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent)
+    : QToolButton{parent}
+    , UpdatableStateInterface{dolphinQuery}
+    , m_datePickerPopup{
+          new KDatePickerPopup{KDatePickerPopup::NoDate | KDatePickerPopup::DatePicker | KDatePickerPopup::Words, dolphinQuery->modifiedSinceDate(), this}}
+{
+    setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+    setPopupMode(QToolButton::InstantPopup);
+
+    m_datePickerPopup->setDateRange(QDate{}, QDate::currentDate());
+    connect(m_datePickerPopup, &KDatePickerPopup::dateChanged, this, [this](const QDate &activatedDate) {
+        if (activatedDate == m_searchConfiguration->modifiedSinceDate()) {
+            return; // Already selected.
+        }
+        DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+        searchConfigurationCopy.setModifiedSinceDate(activatedDate);
+        Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
+    });
+    setMenu(m_datePickerPopup);
+
+    updateStateToMatch(std::move(dolphinQuery));
+}
+
+void DateSelector::removeRestriction()
+{
+    Q_ASSERT(m_searchConfiguration->modifiedSinceDate().isValid());
+    DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+    searchConfigurationCopy.setModifiedSinceDate(QDate{});
+    Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
+}
+
+void DateSelector::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery)
+{
+    m_datePickerPopup->setDate(dolphinQuery->modifiedSinceDate());
+    if (!dolphinQuery->modifiedSinceDate().isValid()) {
+        setIcon(QIcon{}); // No icon for the empty state
+        setText(i18nc("@item:inlistbox", "Any Date"));
+        return;
+    }
+    setIcon(QIcon::fromTheme(QStringLiteral("view-calendar")));
+    QLocale local;
+    KFormat formatter(local);
+    setText(formatter.formatRelativeDate(dolphinQuery->modifiedSinceDate(), QLocale::ShortFormat));
+}
diff --git a/src/search/selectors/dateselector.h b/src/search/selectors/dateselector.h
new file mode 100644 (file)
index 0000000..99cecec
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef DATESELECTOR_H
+#define DATESELECTOR_H
+
+#include "../updatablestateinterface.h"
+
+#include <QToolButton>
+
+class KDatePickerPopup;
+
+namespace Search
+{
+
+class DateSelector : public QToolButton, public UpdatableStateInterface
+{
+    Q_OBJECT
+
+public:
+    explicit DateSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr);
+
+    /** Causes configurationChanged() to be emitted with a DolphinQuery object that does not contain any restriction settable by this class. */
+    void removeRestriction();
+
+Q_SIGNALS:
+    /** Is emitted whenever settings have changed and a new search might be necessary. */
+    void configurationChanged(const DolphinQuery &dolphinQuery);
+
+private:
+    void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override;
+
+private:
+    KDatePickerPopup *m_datePickerPopup = nullptr;
+};
+
+}
+
+#endif // DATESELECTOR_H
diff --git a/src/search/selectors/filetypeselector.cpp b/src/search/selectors/filetypeselector.cpp
new file mode 100644 (file)
index 0000000..7852ace
--- /dev/null
@@ -0,0 +1,81 @@
+/*
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "filetypeselector.h"
+
+#include "../dolphinquery.h"
+
+#include <KFileMetaData/TypeInfo>
+#include <KLocalizedString>
+
+using namespace Search;
+
+FileTypeSelector::FileTypeSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent)
+    : QComboBox{parent}
+    , UpdatableStateInterface{dolphinQuery}
+{
+    for (KFileMetaData::Type::Type type = KFileMetaData::Type::FirstType; type <= KFileMetaData::Type::LastType; type = KFileMetaData::Type::Type(type + 1)) {
+        switch (type) {
+        case KFileMetaData::Type::Empty:
+            addItem(/** No icon for the empty state */ i18nc("@item:inlistbox", "Any Type"), type);
+            continue;
+        case KFileMetaData::Type::Archive:
+            addItem(QIcon::fromTheme(QStringLiteral("package-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type);
+            continue;
+        case KFileMetaData::Type::Audio:
+            addItem(QIcon::fromTheme(QStringLiteral("audio-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type);
+            continue;
+        case KFileMetaData::Type::Video:
+            addItem(QIcon::fromTheme(QStringLiteral("video-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type);
+            continue;
+        case KFileMetaData::Type::Image:
+            addItem(QIcon::fromTheme(QStringLiteral("image-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type);
+            continue;
+        case KFileMetaData::Type::Document:
+            addItem(QIcon::fromTheme(QStringLiteral("text-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type);
+            continue;
+        case KFileMetaData::Type::Spreadsheet:
+            addItem(QIcon::fromTheme(QStringLiteral("table")), KFileMetaData::TypeInfo{type}.displayName(), type);
+            continue;
+        case KFileMetaData::Type::Presentation:
+            addItem(QIcon::fromTheme(QStringLiteral("view-presentation")), KFileMetaData::TypeInfo{type}.displayName(), type);
+            continue;
+        case KFileMetaData::Type::Text:
+            addItem(QIcon::fromTheme(QStringLiteral("text-x-generic")), KFileMetaData::TypeInfo{type}.displayName(), type);
+            continue;
+        case KFileMetaData::Type::Folder:
+            addItem(QIcon::fromTheme(QStringLiteral("inode-directory")), KFileMetaData::TypeInfo{type}.displayName(), type);
+            continue;
+        default:
+            addItem(QIcon(), KFileMetaData::TypeInfo{type}.displayName(), type);
+        }
+    }
+
+    connect(this, &QComboBox::activated, this, [this](int activatedIndex) {
+        auto activatedType = itemData(activatedIndex).value<KFileMetaData::Type::Type>();
+        if (activatedType == m_searchConfiguration->fileType()) {
+            return; // Already selected.
+        }
+        DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+        searchConfigurationCopy.setFileType(activatedType);
+        Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
+    });
+
+    updateStateToMatch(std::move(dolphinQuery));
+}
+
+void Search::FileTypeSelector::removeRestriction()
+{
+    Q_ASSERT(m_searchConfiguration->fileType() != KFileMetaData::Type::Empty);
+    DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+    searchConfigurationCopy.setFileType(KFileMetaData::Type::Empty);
+    Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
+}
+
+void FileTypeSelector::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery)
+{
+    setCurrentIndex(findData(dolphinQuery->fileType()));
+}
diff --git a/src/search/selectors/filetypeselector.h b/src/search/selectors/filetypeselector.h
new file mode 100644 (file)
index 0000000..bfc8273
--- /dev/null
@@ -0,0 +1,36 @@
+/*
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef FILETYPESELECTOR_H
+#define FILETYPESELECTOR_H
+
+#include "../updatablestateinterface.h"
+
+#include <QComboBox>
+
+namespace Search
+{
+
+class FileTypeSelector : public QComboBox, public UpdatableStateInterface
+{
+    Q_OBJECT
+
+public:
+    explicit FileTypeSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr);
+
+    /** Causes configurationChanged() to be emitted with a DolphinQuery object that does not contain any restriction settable by this class. */
+    void removeRestriction();
+
+Q_SIGNALS:
+    /** Is emitted whenever settings have changed and a new search might be necessary. */
+    void configurationChanged(const DolphinQuery &dolphinQuery);
+
+private:
+    void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override;
+};
+}
+
+#endif // FILETYPESELECTOR_H
diff --git a/src/search/selectors/minimumratingselector.cpp b/src/search/selectors/minimumratingselector.cpp
new file mode 100644 (file)
index 0000000..e46dfd4
--- /dev/null
@@ -0,0 +1,51 @@
+/*
+    SPDX-FileCopyrightText: 2012 Peter Penz <peter.penz19@gmail.com>
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "minimumratingselector.h"
+
+#include "../dolphinquery.h"
+
+#include <KLocalizedString>
+
+using namespace Search;
+
+MinimumRatingSelector::MinimumRatingSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent)
+    : QComboBox{parent}
+    , UpdatableStateInterface{dolphinQuery}
+{
+    addItem(/** No icon for the empty state */ i18nc("@item:inlistbox", "Any Rating"), 0);
+    addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "1 or more"), 2);
+    addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "2 or more"), 4);
+    addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "3 or more"), 6);
+    addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox", "4 or more"), 8);
+    addItem(QIcon::fromTheme(QStringLiteral("starred-symbolic")), i18nc("@item:inlistbox 5 star rating, has a star icon in front", "5"), 10);
+
+    connect(this, &QComboBox::activated, this, [this](int activatedIndex) {
+        auto activatedMinimumRating = itemData(activatedIndex).value<int>();
+        if (activatedMinimumRating == m_searchConfiguration->minimumRating()) {
+            return; // Already selected.
+        }
+        DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+        searchConfigurationCopy.setMinimumRating(activatedMinimumRating);
+        Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
+    });
+
+    updateStateToMatch(std::move(dolphinQuery));
+}
+
+void MinimumRatingSelector::removeRestriction()
+{
+    Q_ASSERT(m_searchConfiguration->minimumRating() > 0);
+    DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+    searchConfigurationCopy.setMinimumRating(0);
+    Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
+}
+
+void MinimumRatingSelector::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery)
+{
+    setCurrentIndex(findData(dolphinQuery->minimumRating()));
+}
diff --git a/src/search/selectors/minimumratingselector.h b/src/search/selectors/minimumratingselector.h
new file mode 100644 (file)
index 0000000..02364cd
--- /dev/null
@@ -0,0 +1,42 @@
+/*
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef MINIMUMRATINGSELECTOR_H
+#define MINIMUMRATINGSELECTOR_H
+
+#include "../updatablestateinterface.h"
+
+#include <QComboBox>
+
+namespace Search
+{
+
+/**
+ * @brief Select the minimum rating search results should have.
+ * Values <= 0 mean no restriction. 1 is half a star, 2 one full star, etc. 10 is typically the maximum in KDE software.
+ * Since this box only allows selecting full star ratings, the possible values are 0, 2, 4, 6, 8, 10.
+ */
+class MinimumRatingSelector : public QComboBox, public UpdatableStateInterface
+{
+    Q_OBJECT
+
+public:
+    explicit MinimumRatingSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr);
+
+    /** Causes configurationChanged() to be emitted with a DolphinQuery object that does not contain any restriction settable by this class. */
+    void removeRestriction();
+
+Q_SIGNALS:
+    /** Is emitted whenever settings have changed and a new search might be necessary. */
+    void configurationChanged(const DolphinQuery &dolphinQuery);
+
+private:
+    void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override;
+};
+
+}
+
+#endif // MINIMUMRATINGSELECTOR_H
diff --git a/src/search/selectors/tagsselector.cpp b/src/search/selectors/tagsselector.cpp
new file mode 100644 (file)
index 0000000..95d7ff5
--- /dev/null
@@ -0,0 +1,189 @@
+/*
+    SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#include "tagsselector.h"
+
+#include "../dolphinquery.h"
+
+#include <KCoreDirLister>
+#include <KLocalizedString>
+#include <KProtocolInfo>
+
+#include <QMenu>
+#include <QStringList>
+
+using namespace Search;
+
+namespace
+{
+/**
+ * @brief Provides the list of tags to all TagsSelectors.
+ *
+ * This QStringList of tags populates itself. Additional tags the user is actively searching for can be added with addTag() even though we assume that no file
+ * with such a tag exists if we did not find it automatically.
+ * @note Use the tagsList() function below instead of constructing TagsList objects yourself.
+ */
+class TagsList : public QStringList, public QObject
+{
+public:
+    TagsList()
+        : QStringList{}
+    {
+        m_tagsLister->openUrl(QUrl(QStringLiteral("tags:/")), KCoreDirLister::OpenUrlFlag::Reload);
+        connect(m_tagsLister.get(), &KCoreDirLister::itemsAdded, this, [this](const QUrl &, const KFileItemList &items) {
+            for (const KFileItem &item : items) {
+                append(item.text());
+            }
+            removeDuplicates();
+            sort(Qt::CaseInsensitive);
+        });
+    };
+
+    virtual ~TagsList() = default;
+
+    void addTag(const QString &tag)
+    {
+        if (contains(tag)) {
+            return;
+        }
+        append(tag);
+        sort(Qt::CaseInsensitive);
+    };
+
+    /** Used to access to the itemsAdded signal so outside users of this class know when items were added. */
+    KCoreDirLister *tagsLister() const
+    {
+        return m_tagsLister.get();
+    };
+
+private:
+    std::unique_ptr<KCoreDirLister> m_tagsLister = std::make_unique<KCoreDirLister>();
+};
+
+/**
+ * @returns a list of all tags found since the construction of the first TagsSelector object.
+ * @note Use this function instead of constructing additional TagsList objects.
+ */
+TagsList *tagsList()
+{
+    static TagsList *g_tagsList = new TagsList;
+    return g_tagsList;
+}
+}
+
+Search::TagsSelector::TagsSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent)
+    : QToolButton{parent}
+    , UpdatableStateInterface{dolphinQuery}
+{
+    setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
+    setPopupMode(QToolButton::InstantPopup);
+
+    auto menu = new QMenu{this};
+    setMenu(menu);
+    connect(menu, &QMenu::aboutToShow, this, [this]() {
+        TagsList *tags = tagsList();
+        // The TagsList might not have been updated for a while and new tags might be available. We update now, but this is unfortunately not instant.
+        // However this selector is connected to the itemsAdded() signal, so we will add any new tags eventually.
+        tags->tagsLister()->updateDirectory(tags->tagsLister()->url());
+        updateMenu(m_searchConfiguration);
+    });
+
+    TagsList *tags = tagsList();
+    if (tags->isEmpty()) {
+        // Either there really are no tags or the TagsList has not loaded the tags yet. It only begins loading the first time tagsList() is globally called.
+        setEnabled(false);
+        connect(
+            tags->tagsLister(),
+            &KCoreDirLister::itemsAdded,
+            this,
+            [this]() {
+                setEnabled(true);
+            },
+            Qt::SingleShotConnection);
+    }
+
+    connect(tags->tagsLister(), &KCoreDirLister::itemsAdded, this, [this, menu]() {
+        if (menu->isVisible()) {
+            updateMenu(m_searchConfiguration);
+        }
+    });
+
+    updateStateToMatch(std::move(dolphinQuery));
+}
+
+void TagsSelector::removeRestriction()
+{
+    Q_ASSERT(!m_searchConfiguration->requiredTags().isEmpty());
+    DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+    searchConfigurationCopy.setRequiredTags({});
+    Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
+}
+
+void TagsSelector::updateMenu(const std::shared_ptr<const DolphinQuery> &dolphinQuery)
+{
+    if (!KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) {
+        return;
+    }
+    const bool menuWasVisible = menu()->isVisible();
+    if (menuWasVisible) {
+        menu()->hide(); // The menu needs to be hidden now, then updated, and then shown again.
+    }
+    // Delete all existing actions in the menu
+    for (QAction *action : menu()->actions()) {
+        action->deleteLater();
+    }
+    menu()->clear();
+    // Populate the menu
+    const TagsList *tags = tagsList();
+    const bool onlyOneTagExists = tags->count() == 1;
+
+    for (const QString &tag : *tags) {
+        QAction *tagAction = new QAction{QIcon::fromTheme(QStringLiteral("tag")), tag, menu()};
+        tagAction->setCheckable(true);
+        tagAction->setChecked(dolphinQuery->requiredTags().contains(tag));
+        connect(tagAction, &QAction::triggered, this, [this, tag, onlyOneTagExists](bool checked) {
+            QStringList requiredTags = m_searchConfiguration->requiredTags();
+            if (checked == requiredTags.contains(tag)) {
+                return; // Already selected.
+            }
+            if (checked) {
+                requiredTags.append(tag);
+            } else {
+                requiredTags.removeOne(tag);
+            }
+            DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
+            searchConfigurationCopy.setRequiredTags(requiredTags);
+            Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
+
+            if (!onlyOneTagExists) {
+                // Keep the menu open to allow easier tag multi-selection.
+                menu()->show();
+            }
+        });
+        menu()->addAction(tagAction);
+    }
+    if (menuWasVisible) {
+        menu()->show();
+    }
+}
+
+void TagsSelector::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery)
+{
+    if (dolphinQuery->requiredTags().count()) {
+        setIcon(QIcon::fromTheme(QStringLiteral("tag")));
+        setText(dolphinQuery->requiredTags().join(i18nc("list separator for file tags e.g. all images tagged 'family & party & 2025'", " && ")));
+    } else {
+        setIcon(QIcon{}); // No icon for the empty state
+        setText(i18nc("@action:button Required tags for search results: None", "None"));
+    }
+    for (const auto &tag : dolphinQuery->requiredTags()) {
+        tagsList()->addTag(tag); // We add it just in case this tag is not (or no longer) available on the system. This way the UI always works as expected.
+    }
+    if (menu()->isVisible()) {
+        updateMenu(dolphinQuery);
+    }
+}
diff --git a/src/search/selectors/tagsselector.h b/src/search/selectors/tagsselector.h
new file mode 100644 (file)
index 0000000..386cbb9
--- /dev/null
@@ -0,0 +1,45 @@
+/*
+    SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
+
+#ifndef TAGSSELECTOR_H
+#define TAGSSELECTOR_H
+
+#include "../updatablestateinterface.h"
+
+#include <QToolButton>
+
+namespace Search
+{
+
+class TagsSelector : public QToolButton, public UpdatableStateInterface
+{
+    Q_OBJECT
+
+public:
+    explicit TagsSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent = nullptr);
+
+    /** Causes configurationChanged() to be emitted with a DolphinQuery object that does not contain any restriction settable by this class. */
+    void removeRestriction();
+
+Q_SIGNALS:
+    /** Is emitted whenever settings have changed and a new search might be necessary. */
+    void configurationChanged(const DolphinQuery &dolphinQuery);
+
+private:
+    /**
+     * Updates the menu items for the various tags based on @p dolphinQuery and the available tags.
+     * This method should only be called when the menu is QMenu::aboutToShow() or the menu is currently visible already while this selector's state changes.
+     * If the menu is open when this method is called, the menu will automatically be reopened to reflect the updated contents.
+     */
+    void updateMenu(const std::shared_ptr<const DolphinQuery> &dolphinQuery);
+
+    void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) override;
+};
+
+}
+
+#endif // TAGSSELECTOR_H
diff --git a/src/search/updatablestateinterface.h b/src/search/updatablestateinterface.h
new file mode 100644 (file)
index 0000000..79bc25f
--- /dev/null
@@ -0,0 +1,56 @@
+/*
+    SPDX-FileCopyrightText: 2024 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef UPDATABLESTATEINTERFACE_H
+#define UPDATABLESTATEINTERFACE_H
+
+#include <QtAssert>
+
+#include <memory>
+
+namespace Search
+{
+class DolphinQuery;
+
+class UpdatableStateInterface
+{
+public:
+    inline explicit UpdatableStateInterface(std::shared_ptr<const DolphinQuery> dolphinQuery)
+        : m_searchConfiguration{std::move(dolphinQuery)} {};
+
+    virtual ~UpdatableStateInterface(){};
+
+    /**
+     * Updates this object and its child widgets so their states are correctly described by the @p dolphinQuery.
+     * This method is always initially called on the Search::Bar which in turn calls this method on its child widgets. That is because the Search::Bar is the
+     * ancestor widget of all classes implementing UpdatableStateInterface, and from Search::Bar::updateStateToMatch() the changed state represented by the
+     * @p dolphinQuery is propagated to all other UpdatableStateInterfaces through UpdatableStateInterface::updateState() calls.
+     */
+    inline void updateStateToMatch(std::shared_ptr<const DolphinQuery> dolphinQuery)
+    {
+        Q_ASSERT_X(m_searchConfiguration, "UpdatableStateInterface::updateStateToMatch()", "An UpdatableStateInterface should always have a consistent state.");
+        updateState(dolphinQuery);
+        m_searchConfiguration = std::move(dolphinQuery);
+        Q_ASSERT_X(m_searchConfiguration, "UpdatableStateInterface::updateStateToMatch()", "An UpdatableStateInterface should always have a consistent state.");
+    };
+
+private:
+    /**
+     * Implementations of this method initialize the state of this object and its child widgets to represent the state of the @p dolphinQuery.
+     * This method is only ever called from UpdatableStateInterface::updateStateToMatch().
+     */
+    virtual void updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery) = 0;
+
+protected:
+    /**
+     * The DolphinQuery that was used to initialize this object's state.
+     */
+    std::shared_ptr<const DolphinQuery> m_searchConfiguration;
+};
+
+}
+
+#endif // UPDATABLESTATEINTERFACE_H
diff --git a/src/search/widgetmenu.cpp b/src/search/widgetmenu.cpp
new file mode 100644 (file)
index 0000000..701a0ce
--- /dev/null
@@ -0,0 +1,67 @@
+/*
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#include "widgetmenu.h"
+
+#include <QApplication>
+#include <QShowEvent>
+#include <QWidgetAction>
+
+using namespace Search;
+
+Search::WidgetMenu::WidgetMenu(QWidget *parent)
+    : QMenu{parent}
+{
+    connect(
+        this,
+        &QMenu::aboutToShow,
+        this,
+        [this]() {
+            auto widgetAction = new QWidgetAction{this};
+            auto widget = init();
+            Q_CHECK_PTR(widget);
+            widgetAction->setDefaultWidget(widget); // Transfers ownership to the widgetAction.
+            addAction(widgetAction);
+        },
+        Qt::SingleShotConnection);
+}
+
+bool WidgetMenu::focusNextPrevChild(bool next)
+{
+    return QWidget::focusNextPrevChild(next);
+}
+
+void WidgetMenu::mouseReleaseEvent(QMouseEvent *event)
+{
+    return QWidget::mouseReleaseEvent(event);
+}
+
+void WidgetMenu::resizeToFitContents()
+{
+    auto *widgetAction = static_cast<QWidgetAction *>(actions().first());
+    auto focusedChildWidget = QApplication::focusWidget();
+    if (!widgetAction->defaultWidget()->isAncestorOf(focusedChildWidget)) {
+        focusedChildWidget = nullptr;
+    }
+
+    // Removing and readding the widget triggers the resize.
+    removeAction(widgetAction);
+    addAction(widgetAction);
+
+    // The previous removing and readding removed the focus from any child widgets. We return the focus to where it was.
+    if (focusedChildWidget) {
+        focusedChildWidget->setFocus();
+    }
+}
+
+void WidgetMenu::showEvent(QShowEvent *event)
+{
+    if (!event->spontaneous()) {
+        auto widgetAction = static_cast<QWidgetAction *>(actions().first());
+        widgetAction->defaultWidget()->setFocus();
+    }
+    QMenu::showEvent(event);
+}
diff --git a/src/search/widgetmenu.h b/src/search/widgetmenu.h
new file mode 100644 (file)
index 0000000..def7535
--- /dev/null
@@ -0,0 +1,63 @@
+/*
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL
+*/
+
+#ifndef WIDGETMENU_H
+#define WIDGETMENU_H
+
+#include <QMenu>
+
+class QMouseEvent;
+class QShowEvent;
+
+namespace Search
+{
+
+/**
+ * @brief A QMenu that contains nothing but a lazily constructed widget.
+ *
+ * Usually QMenus contain a list of actions. WidgetMenu allows showing any QWidget instead. This is useful to show popups, random text, or full user interfaces
+ * when a button is pressed or a menu action in a QMenu is hovered.
+ *
+ * This class also encapsulates lazy construction of the widget within. It will only be created when this menu is actually being opened.
+ */
+class WidgetMenu : public QMenu
+{
+public:
+    explicit WidgetMenu(QWidget *parent = nullptr);
+
+protected:
+    /**
+     * Overrides the weird QMenu Tab key handling with the usual QWidget one.
+     */
+    bool focusNextPrevChild(bool next) override;
+
+    /**
+     * Overrides the QMenu behaviour of closing itself when clicked with the non-closing QWidget one.
+     */
+    void mouseReleaseEvent(QMouseEvent *event) override;
+
+    /**
+     * This unfortuantely needs to be explicitly called to resize the WidgetMenu because the size of a QMenu will not automatically change to fit the QWidgets
+     * within.
+     */
+    void resizeToFitContents();
+
+    /**
+     * Move focus to the widget when this WidgetMenu is shown.
+     */
+    void showEvent(QShowEvent *event) override;
+
+private:
+    /**
+     * @return the widget which is contained in this WidgetMenu. This method is at most called once per WidgetMenu object when the WidgetMenu is about to be
+     *         shown for the first time. The ownership of the widget will be transfered to an internal QWidgetAction.
+     */
+    virtual QWidget *init() = 0;
+};
+
+}
+
+#endif // WIDGETMENU_H
index 1ef82e3d9e85c1050117f6700a5495504f6830a8..e77d7d7bf7d7bf90e59aaeb10bb3a905f5178e93 100644 (file)
@@ -42,10 +42,10 @@ target_link_libraries(kfileitemmodelbenchmark dolphinprivate Qt6::Test)
 # KItemListKeyboardSearchManagerTest
 ecm_add_test(kitemlistkeyboardsearchmanagertest.cpp LINK_LIBRARIES dolphinprivate Qt6::Test)
 
-# DolphinSearchBox
+# DolphinSearchBar
 if (KF6Baloo_FOUND)
-    ecm_add_test(dolphinsearchboxtest.cpp
-    TEST_NAME dolphinsearchboxtest
+    ecm_add_test(dolphinsearchbartest.cpp
+    TEST_NAME dolphinsearchbartest
     LINK_LIBRARIES dolphinprivate dolphinstatic Qt6::Test)
 endif()
 
index c7d2f4b6d7fee2870e92c51151414b0674498524..4ae36e7e6b6d6c110528b6f76ae0a9338284c9a1 100644 (file)
@@ -204,7 +204,7 @@ void DolphinMainWindowTest::testClosingTabsWithSearchBoxVisible()
     QVERIFY(tabWidget);
 
     // Show search box on first tab.
-    tabWidget->currentTabPage()->activeViewContainer()->setSearchModeEnabled(true);
+    tabWidget->currentTabPage()->activeViewContainer()->setSearchBarVisible(true);
 
     tabWidget->openNewActivatedTab(QUrl::fromLocalFile(QDir::homePath()));
     QCOMPARE(tabWidget->count(), 2);
index b6feab686ff404e58cf924ced72e4a1b1b8f7891..76cea999f1f82f64fa2316406425cbeb07f2881c 100644 (file)
@@ -1,13 +1,15 @@
 /*
- * SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@gmail.com>
- *
- * SPDX-License-Identifier: GPL-2.0-or-later
- */
+    SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
+    SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
+
+    SPDX-License-Identifier: GPL-2.0-or-later
+*/
 
 #include "search/dolphinquery.h"
 
 #include <QTest>
 
+#include <QDate>
 #include <QJsonDocument>
 #include <QJsonObject>
 #include <QStandardPaths>
@@ -23,6 +25,7 @@ private Q_SLOTS:
     void initTestCase();
     void testBalooSearchParsing_data();
     void testBalooSearchParsing();
+    void testExportImport();
 };
 
 /**
@@ -48,6 +51,7 @@ QUrl balooQueryUrl(const QString &searchString)
 void DolphinQueryTest::initTestCase()
 {
     QStandardPaths::setTestModeEnabled(true);
+    Search::setTestMode();
 }
 
 /**
@@ -56,8 +60,10 @@ void DolphinQueryTest::initTestCase()
 void DolphinQueryTest::testBalooSearchParsing_data()
 {
     QTest::addColumn<QUrl>("searchUrl");
-    QTest::addColumn<QString>("expectedText");
-    QTest::addColumn<QStringList>("expectedTerms");
+    QTest::addColumn<QString>("expectedSearchTerm");
+    QTest::addColumn<QDate>("expectedModifiedSinceDate");
+    QTest::addColumn<int>("expectedMinimumRating");
+    QTest::addColumn<QStringList>("expectedTags");
     QTest::addColumn<bool>("hasContent");
     QTest::addColumn<bool>("hasFileName");
 
@@ -73,78 +79,80 @@ void DolphinQueryTest::testBalooSearchParsing_data()
 
     const QString rating = QStringLiteral("rating>=2");
     const QString modified = QStringLiteral("modified>=2019-08-07");
+    QDate modifiedDate;
+    modifiedDate.setDate(2019, 8, 7);
 
     const QString tag = QStringLiteral("tag:tagA");
     const QString tagS = QStringLiteral("tag:\"tagB with spaces\""); // in search url
     const QString tagR = QStringLiteral("tag:tagB with spaces"); // in result term
 
+    const QLatin1String tagA{"tagA"};
+    const QLatin1String tagBWithSpaces{"tagB with spaces"};
+
     // Test for "Content"
-    QTest::newRow("content") << balooQueryUrl(text) << text << QStringList() << true << false;
-    QTest::newRow("content/space") << balooQueryUrl(textS) << textS << QStringList() << true << false;
-    QTest::newRow("content/quoted") << balooQueryUrl(textQ) << textS << QStringList() << true << false;
-    QTest::newRow("content/empty") << balooQueryUrl("") << "" << QStringList() << false << false;
-    QTest::newRow("content/single_quote") << balooQueryUrl("\"") << "\"" << QStringList() << true << false;
-    QTest::newRow("content/double_quote") << balooQueryUrl("\"\"") << "" << QStringList() << false << false;
+    QTest::newRow("content") << balooQueryUrl(text) << text << QDate{} << 0 << QStringList() << true << true;
+    QTest::newRow("content/space") << balooQueryUrl(textS) << textS << QDate{} << 0 << QStringList() << true << true;
+    QTest::newRow("content/quoted") << balooQueryUrl(textQ) << textS << QDate{} << 0 << QStringList() << true << true;
+    QTest::newRow("content/empty") << balooQueryUrl("") << "" << QDate{} << 0 << QStringList() << false << false;
+    QTest::newRow("content/single_quote") << balooQueryUrl("\"") << "\"" << QDate{} << 0 << QStringList() << true << true;
+    QTest::newRow("content/double_quote") << balooQueryUrl("\"\"") << "" << QDate{} << 0 << QStringList() << false << false;
 
     // Test for "FileName"
-    QTest::newRow("filename") << balooQueryUrl(filename) << text << QStringList() << false << true;
-    QTest::newRow("filename/space") << balooQueryUrl(filenameS) << textS << QStringList() << false << true;
-    QTest::newRow("filename/quoted") << balooQueryUrl(filenameQ) << textQ << QStringList() << false << true;
-    QTest::newRow("filename/mixed") << balooQueryUrl(filenameM) << textM << QStringList() << false << true;
-    QTest::newRow("filename/empty") << balooQueryUrl("filename:") << "" << QStringList() << false << false;
-    QTest::newRow("filename/single_quote") << balooQueryUrl("filename:\"") << "\"" << QStringList() << false << true;
-    QTest::newRow("filename/double_quote") << balooQueryUrl("filename:\"\"") << "" << QStringList() << false << false;
+    QTest::newRow("filename") << balooQueryUrl(filename) << text << QDate{} << 0 << QStringList() << false << true;
+    QTest::newRow("filename/space") << balooQueryUrl(filenameS) << textS << QDate{} << 0 << QStringList() << false << true;
+    QTest::newRow("filename/quoted") << balooQueryUrl(filenameQ) << textQ << QDate{} << 0 << QStringList() << false << true;
+    QTest::newRow("filename/mixed") << balooQueryUrl(filenameM) << textM << QDate{} << 0 << QStringList() << false << true;
+    QTest::newRow("filename/empty") << balooQueryUrl("filename:") << "" << QDate{} << 0 << QStringList() << false << false;
+    QTest::newRow("filename/single_quote") << balooQueryUrl("filename:\"") << "\"" << QDate{} << 0 << QStringList() << false << true;
+    QTest::newRow("filename/double_quote") << balooQueryUrl("filename:\"\"") << "" << QDate{} << 0 << QStringList() << false << false;
 
     // Combined content and filename search
-    QTest::newRow("content+filename") << balooQueryUrl(text + " " + filename) << text + " " + filename << QStringList() << true << true;
+    QTest::newRow("content+filename") << balooQueryUrl(text + " " + filename) << text << QDate{} << 0 << QStringList() << true << true;
 
-    QTest::newRow("content+filename/quoted") << balooQueryUrl(textQ + " " + filenameQ) << textS + " " + filenameQ << QStringList() << true << true;
+    QTest::newRow("content+filename/quoted") << balooQueryUrl(textQ + " " + filenameQ) << textS << QDate{} << 0 << QStringList() << true << true;
 
     // Test for rating
-    QTest::newRow("rating") << balooQueryUrl(rating) << "" << QStringList({rating}) << false << false;
-    QTest::newRow("rating+content") << balooQueryUrl(rating + " " + text) << text << QStringList({rating}) << true << false;
-    QTest::newRow("rating+filename") << balooQueryUrl(rating + " " + filename) << text << QStringList({rating}) << false << true;
+    QTest::newRow("rating") << balooQueryUrl(rating) << "" << QDate{} << 2 << QStringList() << false << false;
+    QTest::newRow("rating+content") << balooQueryUrl(rating + " " + text) << text << QDate{} << 2 << QStringList() << true << true;
+    QTest::newRow("rating+filename") << balooQueryUrl(rating + " " + filename) << text << QDate{} << 2 << QStringList() << false << true;
 
     // Test for modified date
-    QTest::newRow("modified") << balooQueryUrl(modified) << "" << QStringList({modified}) << false << false;
-    QTest::newRow("modified+content") << balooQueryUrl(modified + " " + text) << text << QStringList({modified}) << true << false;
-    QTest::newRow("modified+filename") << balooQueryUrl(modified + " " + filename) << text << QStringList({modified}) << false << true;
+    QTest::newRow("modified") << balooQueryUrl(modified) << "" << modifiedDate << 0 << QStringList() << false << false;
+    QTest::newRow("modified+content") << balooQueryUrl(modified + " " + text) << text << modifiedDate << 0 << QStringList() << true << true;
+    QTest::newRow("modified+filename") << balooQueryUrl(modified + " " + filename) << text << modifiedDate << 0 << QStringList() << false << true;
 
     // Test for tags
-    QTest::newRow("tag") << balooQueryUrl(tag) << "" << QStringList({tag}) << false << false;
-    QTest::newRow("tag/space") << balooQueryUrl(tagS) << "" << QStringList({tagR}) << false << false;
-    QTest::newRow("tag/double") << balooQueryUrl(tag + " " + tagS) << "" << QStringList({tag, tagR}) << false << false;
-    QTest::newRow("tag+content") << balooQueryUrl(tag + " " + text) << text << QStringList({tag}) << true << false;
-    QTest::newRow("tag+filename") << balooQueryUrl(tag + " " + filename) << text << QStringList({tag}) << false << true;
+    QTest::newRow("tag") << balooQueryUrl(tag) << "" << QDate{} << 0 << QStringList{tagA} << false << false;
+    QTest::newRow("tag/space") << balooQueryUrl(tagS) << "" << QDate{} << 0 << QStringList{tagBWithSpaces} << false << false;
+    QTest::newRow("tag/double") << balooQueryUrl(tag + " " + tagS) << "" << QDate{} << 0 << QStringList{tagA, tagBWithSpaces} << false << false;
+    QTest::newRow("tag+content") << balooQueryUrl(tag + " " + text) << text << QDate{} << 0 << QStringList{tagA} << true << true;
+    QTest::newRow("tag+filename") << balooQueryUrl(tag + " " + filename) << text << QDate{} << 0 << QStringList{tagA} << false << true;
 
     // Combined search terms
-    QTest::newRow("searchTerms") << balooQueryUrl(rating + " AND " + modified + " AND " + tag + " AND " + tagS) << ""
-                                 << QStringList({modified, rating, tag, tagR}) << false << false;
+    QTest::newRow("searchTerms") << balooQueryUrl(rating + " AND " + modified + " AND " + tag + " AND " + tagS) << "" << modifiedDate << 2
+                                 << QStringList{tagA, tagBWithSpaces} << false << false;
 
-    QTest::newRow("searchTerms+content") << balooQueryUrl(rating + " AND " + modified + " " + text + " " + tag + " AND " + tagS) << text
-                                         << QStringList({modified, rating, tag, tagR}) << true << false;
+    QTest::newRow("searchTerms+content") << balooQueryUrl(rating + " AND " + modified + " " + text + " " + tag + " AND " + tagS) << text << modifiedDate << 2
+                                         << QStringList{tagA, tagBWithSpaces} << true << true;
 
-    QTest::newRow("searchTerms+filename") << balooQueryUrl(rating + " AND " + modified + " " + filename + " " + tag + " AND " + tagS) << text
-                                          << QStringList({modified, rating, tag, tagR}) << false << true;
+    QTest::newRow("searchTerms+filename") << balooQueryUrl(rating + " AND " + modified + " " + filename + " " + tag + " AND " + tagS) << text << modifiedDate
+                                          << 2 << QStringList{tagA, tagBWithSpaces} << false << true;
 
-    QTest::newRow("allTerms") << balooQueryUrl(text + " " + filename + " " + rating + " AND " + modified + " AND " + tag) << text + " " + filename
-                              << QStringList({modified, rating, tag}) << true << true;
+    QTest::newRow("allTerms") << balooQueryUrl(text + " " + filename + " " + rating + " AND " + modified + " AND " + tag) << text << modifiedDate << 2
+                              << QStringList{tagA} << true << true;
 
-    QTest::newRow("allTerms/space") << balooQueryUrl(textS + " " + filenameS + " " + rating + " AND " + modified + " AND " + tagS) << textS + " " + filenameS
-                                    << QStringList({modified, rating, tagR}) << true << true;
+    QTest::newRow("allTerms/space") << balooQueryUrl(textS + " " + filenameS + " " + rating + " AND " + modified + " AND " + tagS) << textS << modifiedDate << 2
+                                    << QStringList{tagBWithSpaces} << true << true;
 
     // Test tags:/ URL scheme
     const auto tagUrl = [](const QString &tag) {
         return QUrl(QStringLiteral("tags:/%1/").arg(tag));
     };
-    const auto tagTerms = [](const QString &tag) {
-        return QStringList{QStringLiteral("tag:%1").arg(tag)};
-    };
 
-    QTest::newRow("tagsUrl") << tagUrl("tagA") << "" << tagTerms("tagA") << false << false;
-    QTest::newRow("tagsUrl/space") << tagUrl("tagB with spaces") << "" << tagTerms("tagB with spaces") << false << false;
-    QTest::newRow("tagsUrl/hash") << tagUrl("tagC#hash") << "" << tagTerms("tagC#hash") << false << false;
-    QTest::newRow("tagsUrl/slash") << tagUrl("tagD/with/slash") << "" << tagTerms("tagD/with/slash") << false << false;
+    QTest::newRow("tagsUrl") << tagUrl(tagA) << "" << QDate{} << 0 << QStringList{tagA} << false << false;
+    QTest::newRow("tagsUrl/space") << tagUrl(tagBWithSpaces) << "" << QDate{} << 0 << QStringList{tagBWithSpaces} << false << false;
+    QTest::newRow("tagsUrl/hash") << tagUrl("tagC#hash") << "" << QDate{} << 0 << QStringList{QStringLiteral("tagC#hash")} << false << false;
+    QTest::newRow("tagsUrl/slash") << tagUrl("tagD/with/slash") << "" << QDate{} << 0 << QStringList{QStringLiteral("tagD/with/slash")} << false << false;
 }
 
 /**
@@ -155,31 +163,173 @@ void DolphinQueryTest::testBalooSearchParsing_data()
 void DolphinQueryTest::testBalooSearchParsing()
 {
     QFETCH(QUrl, searchUrl);
-    QFETCH(QString, expectedText);
-    QFETCH(QStringList, expectedTerms);
+    QFETCH(QString, expectedSearchTerm);
+    QFETCH(QDate, expectedModifiedSinceDate);
+    QFETCH(int, expectedMinimumRating);
+    QFETCH(QStringList, expectedTags);
     QFETCH(bool, hasContent);
     QFETCH(bool, hasFileName);
 
-    const DolphinQuery query = DolphinQuery::fromSearchUrl(searchUrl);
+    const Search::DolphinQuery query = Search::DolphinQuery{searchUrl, /** No backupSearchPath should be needed because searchUrl should be valid. */ QUrl{}};
 
     // Checkt that the URL is supported
-    QVERIFY(DolphinQuery::supportsScheme(searchUrl.scheme()));
+    QVERIFY(Search::isSupportedSearchScheme(searchUrl.scheme()));
 
     // Check for parsed text (would be displayed on the input search bar)
-    QCOMPARE(query.text(), expectedText);
+    QCOMPARE(query.searchTerm(), expectedSearchTerm);
+
+    QCOMPARE(query.modifiedSinceDate(), expectedModifiedSinceDate);
+
+    QCOMPARE(query.minimumRating(), expectedMinimumRating);
 
-    // Check for parsed search terms (would be displayed by the facetsWidget)
-    QStringList searchTerms = query.searchTerms();
-    searchTerms.sort();
+    QCOMPARE(query.requiredTags(), expectedTags);
 
-    QCOMPARE(searchTerms.count(), expectedTerms.count());
-    for (int i = 0; i < expectedTerms.count(); i++) {
-        QCOMPARE(searchTerms.at(i), expectedTerms.at(i));
-    }
+    // Check that there were no unrecognized baloo query parameters in the above strings.
+    Q_ASSERT(query.m_unrecognizedBalooQueryStrings.isEmpty());
 
-    // Check for filename and content detection
-    QCOMPARE(query.hasContentSearch(), hasContent);
-    QCOMPARE(query.hasFileName(), hasFileName);
+    // Check if a search term is looked up in the file names or contents
+    QCOMPARE(query.searchThrough() == Search::SearchThrough::FileContents && !query.searchTerm().isEmpty(), hasContent);
+    QCOMPARE(!query.searchTerm().isEmpty(), hasFileName); // The file names are always also searched even when searching through file contents.
+}
+
+/**
+ * Tests whether exporting a DolphinQuery object to a URL and then constructing a DolphinQuery object from that URL recreates the same DolphinQuery.
+ */
+void DolphinQueryTest::testExportImport()
+{
+    /// Initialize the DolphinQuery with some standard settings.
+    const QUrl searchPath1{"file:///someNonExistentUrl"};
+    Search::DolphinQuery query{searchPath1, searchPath1};
+    query.setSearchLocations(Search::SearchLocations::FromHere);
+    QVERIFY(query.searchLocations() == Search::SearchLocations::FromHere);
+    query.setSearchThrough(Search::SearchThrough::FileNames);
+    QVERIFY(query.searchThrough() == Search::SearchThrough::FileNames);
+    query.setSearchTool(Search::SearchTool::Filenamesearch);
+    QVERIFY(query.searchTool() == Search::SearchTool::Filenamesearch);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath1)); // Export then import
+
+    /// Test that exporting and importing works as expected no matter which aspect we change.
+    query.setSearchThrough(Search::SearchThrough::FileContents);
+    QVERIFY(query.searchThrough() == Search::SearchThrough::FileContents);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath1)); // Export then import
+
+    constexpr QLatin1String searchTerm1{"abc"};
+    query.setSearchTerm(searchTerm1);
+    QVERIFY(query.searchTerm() == searchTerm1);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath1)); // Export then import
+
+    query.setSearchThrough(Search::SearchThrough::FileNames);
+    QVERIFY(query.searchThrough() == Search::SearchThrough::FileNames);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath1)); // Export then import
+
+    QVERIFY(query.searchPath() == searchPath1);
+    const QUrl searchPath2{"file:///someNonExistentUrl2"};
+    query.setSearchPath(searchPath2);
+    QVERIFY(query.searchPath() == searchPath2);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because otherUrl is imported.
+
+    query.setSearchLocations(Search::SearchLocations::Everywhere);
+    QVERIFY(query.searchLocations() == Search::SearchLocations::Everywhere);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath2)); // Export then import. searchPath2 is required to match as the fallback.
+
+    QVERIFY(query.searchTerm() == searchTerm1);
+    constexpr QLatin1String searchTerm2{"xyz"};
+    query.setSearchTerm(searchTerm2);
+    QVERIFY(query.searchTerm() == searchTerm2);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath2)); // Export then import
+
+    QVERIFY(query.searchLocations() == Search::SearchLocations::Everywhere);
+    query.setSearchLocations(Search::SearchLocations::FromHere);
+    QVERIFY(query.searchLocations() == Search::SearchLocations::FromHere);
+    QVERIFY(query.searchPath() == searchPath2);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported.
+
+#if HAVE_BALOO
+    /// Test Baloo search queries
+    query.setSearchTool(Search::SearchTool::Baloo);
+    QVERIFY(query.searchTool() == Search::SearchTool::Baloo);
+    QVERIFY(query.searchTerm() == searchTerm2);
+    QVERIFY(query.searchLocations() == Search::SearchLocations::FromHere);
+    QVERIFY(query.searchPath() == searchPath2);
+    QVERIFY(query.searchThrough() == Search::SearchThrough::FileNames);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported.
+
+    /// Test that exporting and importing works as expected no matter which aspect we change.
+    query.setSearchThrough(Search::SearchThrough::FileContents);
+    QVERIFY(query.searchThrough() == Search::SearchThrough::FileContents);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported.
+
+    query.setSearchTerm(searchTerm1);
+    QVERIFY(query.searchTerm() == searchTerm1);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported.
+
+    query.setSearchThrough(Search::SearchThrough::FileNames);
+    QVERIFY(query.searchThrough() == Search::SearchThrough::FileNames);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported.
+
+    QVERIFY(query.searchPath() == searchPath2);
+    query.setSearchPath(searchPath1);
+    QVERIFY(query.searchPath() == searchPath1);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath2 is imported.
+
+    query.setSearchLocations(Search::SearchLocations::Everywhere);
+    QVERIFY(query.searchLocations() == Search::SearchLocations::Everywhere);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath1)); // Export then import. searchPath1 is required to match as the fallback.
+
+    QVERIFY(query.searchTerm() == searchTerm1);
+    query.setSearchTerm(searchTerm2);
+    QVERIFY(query.searchTerm() == searchTerm2);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), searchPath1)); // Export then import
+
+    QVERIFY(query.searchLocations() == Search::SearchLocations::Everywhere);
+    query.setSearchLocations(Search::SearchLocations::FromHere);
+    QVERIFY(query.searchLocations() == Search::SearchLocations::FromHere);
+    QVERIFY(query.searchPath() == searchPath1);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported.
+
+    QVERIFY(query.fileType() == KFileMetaData::Type::Empty);
+    query.setFileType(KFileMetaData::Type::Archive);
+    QVERIFY(query.fileType() == KFileMetaData::Type::Archive);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported.
+
+    QVERIFY(!query.modifiedSinceDate().isValid());
+    QDate modifiedDate;
+    modifiedDate.setDate(2018, 6, 3); // World Bicycle Day
+    query.setModifiedSinceDate(modifiedDate);
+    QVERIFY(query.modifiedSinceDate() == modifiedDate);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported.
+
+    QVERIFY(query.minimumRating() == 0);
+    query.setMinimumRating(4);
+    QVERIFY(query.minimumRating() == 4);
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported.
+
+    QVERIFY(query.requiredTags().isEmpty());
+    query.setRequiredTags({searchTerm1, searchTerm2});
+    QVERIFY(query.requiredTags().contains(searchTerm1));
+    QVERIFY(query.requiredTags().contains(searchTerm2));
+    QVERIFY(query == Search::DolphinQuery(query.toUrl(), QUrl{})); // Export then import. The QUrl{} fallback does not matter because searchPath1 is imported.
+
+    QVERIFY(query.searchTool() == Search::SearchTool::Baloo);
+    QVERIFY(query.searchThrough() == Search::SearchThrough::FileNames);
+    QVERIFY(query.searchPath() == searchPath1);
+    QVERIFY(query.searchTerm() == searchTerm2);
+    QVERIFY(query.searchLocations() == Search::SearchLocations::FromHere);
+    QVERIFY(query.fileType() == KFileMetaData::Type::Archive);
+    QVERIFY(query.modifiedSinceDate() == modifiedDate);
+    QVERIFY(query.minimumRating() == 4);
+
+    /// Changing the search tool should not immediately drop all the extra information even if the search tool might not support searching for them.
+    /// This is mostly an attempt to not drop properties set by the user earlier than we have to.
+    query.setSearchTool(Search::SearchTool::Filenamesearch);
+    QVERIFY(query.searchThrough() == Search::SearchThrough::FileNames);
+    QVERIFY(query.searchPath() == searchPath1);
+    QVERIFY(query.searchTerm() == searchTerm2);
+    QVERIFY(query.searchLocations() == Search::SearchLocations::FromHere);
+    QVERIFY(query.fileType() == KFileMetaData::Type::Archive);
+    QVERIFY(query.modifiedSinceDate() == modifiedDate);
+    QVERIFY(query.minimumRating() == 4);
+#endif
 }
 
 QTEST_MAIN(DolphinQueryTest)
diff --git a/src/tests/dolphinsearchbartest.cpp b/src/tests/dolphinsearchbartest.cpp
new file mode 100644 (file)
index 0000000..0efb05d
--- /dev/null
@@ -0,0 +1,124 @@
+/*
+ * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
+ *
+ * SPDX-License-Identifier: GPL-2.0-or-later
+ */
+
+#include "search/bar.h"
+#include "search/popup.h"
+
+#include <QLineEdit>
+#include <QSignalSpy>
+#include <QStandardPaths>
+#include <QTest>
+#include <QToolButton>
+
+class DolphinSearchBarTest : public QObject
+{
+    Q_OBJECT
+
+private Q_SLOTS:
+    void initTestCase();
+    void init();
+    void cleanup();
+
+    void testPopupLazyLoading();
+    void testTextClearing();
+    void testUrlChangeSignals();
+
+private:
+    Search::Bar *m_searchBar;
+};
+
+void DolphinSearchBarTest::initTestCase()
+{
+    QStandardPaths::setTestModeEnabled(true);
+}
+
+void DolphinSearchBarTest::init()
+{
+    const auto homeUrl{QUrl::fromUserInput(QDir::homePath())};
+    m_searchBar = new Search::Bar(std::make_shared<const Search::DolphinQuery>(homeUrl, homeUrl));
+}
+
+void DolphinSearchBarTest::cleanup()
+{
+    delete m_searchBar;
+}
+
+void DolphinSearchBarTest::testPopupLazyLoading()
+{
+    m_searchBar->setVisible(true, WithoutAnimation);
+    QVERIFY2(m_searchBar->m_popup->isEmpty(), "The popup should only be populated or updated when it was opened at least once by the user.");
+}
+
+/**
+ * The test verifies whether the automatic clearing of the text works correctly.
+ * The text may not get cleared when the search bar gets visible or invisible,
+ * as this would clear the text when switching between tabs.
+ */
+void DolphinSearchBarTest::testTextClearing()
+{
+    m_searchBar->setVisible(true, WithoutAnimation);
+    QVERIFY(m_searchBar->text().isEmpty());
+    QVERIFY(!m_searchBar->isSearchConfigured());
+
+    m_searchBar->updateStateToMatch(std::make_shared<const Search::DolphinQuery>(QUrl::fromUserInput("filenamesearch:?search=xyz"), QUrl{}));
+    m_searchBar->setVisible(false, WithoutAnimation);
+    m_searchBar->setVisible(true, WithoutAnimation);
+    QCOMPARE(m_searchBar->text(), QStringLiteral("xyz"));
+    QVERIFY(m_searchBar->isSearchConfigured());
+    QVERIFY(!m_searchBar->queryTitle().isEmpty());
+
+    QTest::keyClick(m_searchBar, Qt::Key_Escape);
+    QVERIFY(m_searchBar->text().isEmpty());
+    QVERIFY(!m_searchBar->isSearchConfigured());
+}
+
+void DolphinSearchBarTest::testUrlChangeSignals()
+{
+    QSignalSpy spyUrlChangeRequested(m_searchBar, &Search::Bar::urlChangeRequested);
+    m_searchBar->setVisible(true, WithoutAnimation);
+    QVERIFY2(spyUrlChangeRequested.isEmpty(), "Opening the Search::Bar should not trigger anything.");
+
+    m_searchBar->m_everywhereButton->click();
+    m_searchBar->m_fromHereButton->click();
+    QVERIFY(!m_searchBar->isSearchConfigured());
+
+    while (!spyUrlChangeRequested.isEmpty()) {
+        const QUrl requestedUrl = qvariant_cast<QUrl>(spyUrlChangeRequested.takeFirst().at(0));
+        QVERIFY2(!Search::isSupportedSearchScheme(requestedUrl.scheme()) && requestedUrl == m_searchBar->m_searchConfiguration->searchPath(),
+                 "The search is still not in a state to search for anything specific, so any requested URLs would be identical to the current search path of "
+                 "the Search::Bar.");
+    }
+
+    Search::DolphinQuery searchConfiguration = *m_searchBar->m_searchConfiguration;
+    searchConfiguration.setSearchTerm(QStringLiteral("searchTerm"));
+    m_searchBar->updateStateToMatch(std::make_shared<const Search::DolphinQuery>(searchConfiguration));
+    QVERIFY2(m_searchBar->isSearchConfigured(), "The search bar now has enough information to trigger a meaningful search.");
+    QVERIFY2(spyUrlChangeRequested.isEmpty(), "The visual state was updated to match a new search configuration, but the user never triggered a search.");
+
+    m_searchBar->commitCurrentConfiguration(); // We pretend to be a user interaction that would trigger a search to happen.
+    const QUrl requestedUrl = qvariant_cast<QUrl>(spyUrlChangeRequested.takeFirst().at(0));
+    QVERIFY2(Search::isSupportedSearchScheme(requestedUrl.scheme()), "The search bar requested to open a search url and therefore start searching.");
+    QVERIFY2(spyUrlChangeRequested.isEmpty(), "The search URL was requested exactly once, so no additional urlChangeRequested signals should exist.");
+
+    Search::DolphinQuery searchConfiguration2 = *m_searchBar->m_searchConfiguration;
+    searchConfiguration2.setSearchTerm(QString(""));
+    m_searchBar->updateStateToMatch(std::make_shared<const Search::DolphinQuery>(searchConfiguration2));
+    QVERIFY2(!m_searchBar->isSearchConfigured(), "The search bar does not have enough information anymore to trigger a meaningful search.");
+    QVERIFY2(spyUrlChangeRequested.isEmpty(), "The visual state was updated to match a new search configuration, but the user never triggered a search.");
+
+    m_searchBar->commitCurrentConfiguration(); // We pretend to be a user interaction that would trigger a search to happen.
+    const QUrl requestedUrl2 = qvariant_cast<QUrl>(spyUrlChangeRequested.takeFirst().at(0));
+    QVERIFY2(!Search::isSupportedSearchScheme(requestedUrl2.scheme()) && requestedUrl2 == m_searchBar->m_searchConfiguration->searchPath(),
+             "The Search::Bar is not in a state to search for anything specific, so the search bar requests to show the previously visited location normally "
+             "again instead of any previous search URL.");
+    QVERIFY2(spyUrlChangeRequested.isEmpty(), "The non-search URL was requested exactly once, so no additional urlChangeRequested signals should exist.");
+
+    QVERIFY2(m_searchBar->m_popup->isEmpty(), "Through all of this, the popup should still be empty because it was never opened by the user.");
+}
+
+QTEST_MAIN(DolphinSearchBarTest)
+
+#include "dolphinsearchbartest.moc"
diff --git a/src/tests/dolphinsearchboxtest.cpp b/src/tests/dolphinsearchboxtest.cpp
deleted file mode 100644 (file)
index bda6090..0000000
+++ /dev/null
@@ -1,63 +0,0 @@
-/*
- * SPDX-FileCopyrightText: 2011 Peter Penz <peter.penz19@gmail.com>
- *
- * SPDX-License-Identifier: GPL-2.0-or-later
- */
-
-#include "search/dolphinsearchbox.h"
-
-#include <QStandardPaths>
-#include <QTest>
-
-class DolphinSearchBoxTest : public QObject
-{
-    Q_OBJECT
-
-private Q_SLOTS:
-    void initTestCase();
-    void init();
-    void cleanup();
-
-    void testTextClearing();
-
-private:
-    DolphinSearchBox *m_searchBox;
-};
-
-void DolphinSearchBoxTest::initTestCase()
-{
-    QStandardPaths::setTestModeEnabled(true);
-}
-
-void DolphinSearchBoxTest::init()
-{
-    m_searchBox = new DolphinSearchBox();
-}
-
-void DolphinSearchBoxTest::cleanup()
-{
-    delete m_searchBox;
-}
-
-/**
- * The test verifies whether the automatic clearing of the text works correctly.
- * The text may not get cleared when the searchbox gets visible or invisible,
- * as this would clear the text when switching between tabs.
- */
-void DolphinSearchBoxTest::testTextClearing()
-{
-    m_searchBox->setVisible(true, WithoutAnimation);
-    QVERIFY(m_searchBox->text().isEmpty());
-
-    m_searchBox->setText("xyz");
-    m_searchBox->setVisible(false, WithoutAnimation);
-    m_searchBox->setVisible(true, WithoutAnimation);
-    QCOMPARE(m_searchBox->text(), QStringLiteral("xyz"));
-
-    QTest::keyClick(m_searchBox, Qt::Key_Escape);
-    QVERIFY(m_searchBox->text().isEmpty());
-}
-
-QTEST_MAIN(DolphinSearchBoxTest)
-
-#include "dolphinsearchboxtest.moc"