┌   ┐
54
└   ┘

summaryrefslogtreecommitdiff
path: root/src/search/selectors
diff options
context:
space:
mode:
authorFelix Ernst <[email protected]>2025-04-07 21:09:00 +0000
committerFelix Ernst <[email protected]>2025-04-07 21:09:00 +0000
commit4102ccb80457eea44ea280f0ace2a419602bc34b (patch)
tree841e039cf9864276c968a397a2ae75c363199342 /src/search/selectors
parentbfc177d3d1bc5a4a241e35d59086e4824e7c0bd3 (diff)
Rewrite search integration
This huge commit is a nearly complete rewrite of the Dolphin search code. It implements most of the improved Dolphin search UI/UX as designed and discussed in a collaborative effort by Kristen McWilliam, Jin Liu, Andy Betts, Tagwerk, a few others and me. See https://invent.kde.org/system/dolphin/-/issues/46. # Notable changes - A toggle to change the search tool is provided as most contributors deemed that useful in https://invent.kde.org/system/dolphin/-/merge_requests/642#note_985112. - The default search is changed to filenamesearch for maximum reliability. - Removing all search parameters will take users back to the view state prior to starting a search instead of keeping the search results open. - The UI for choosing file types or modification dates has been made more powerful with more granularity and more options. - Most search parameters can be configured from a popup menu which gives us extra space for extra clarity. - Labels and help buttons as well as hyperlinks to settings makes sure the user always knows why some search parameters are unavailable in some contexts. - Chips show important search parameters while the popup is closed. They allow quickly removing filters. - The titles of the search and the input field placeholder message change to make clear whether file names or file contents are searched. - When the user actively switches the search tool, whether content should be searched, or whether to search everywhere, this is preserved for the initial state of the search bar when the user opens it the next time after restarting Dolphin. # Architecture - The new DolphinQuery class is independent of the UI and contains all search parameters modifiable in Dolphin as easy setters and getters. - DolphinQuery objects are also used to update the states of every component in the search UI. There is now a clear separation of UI and search configuration/DolphinQuery. - DolphinQuery is responsible for exporting to and importing from search URLs. - The search UI always reflects the currently configured DolphinQuery no matter if the user changed the UI to change the DolphinQuery or loaded a DolphinQuery/older search URL which then is reflected in the UI. - I tried to simplify all classes and their interaction between each other as much as possible. - I added some tests BUG: 386754 CCBUG: 435119 CCBUG: 458761 BUG: 446387 BUG: 470136 CCBUG: 471556 CCBUG: 475439 CCBUG: 477969 BUG: 480001 BUG: 483578 BUG: 488047 BUG: 488845 BUG: 500103 FIXED-IN: 25.08
Diffstat (limited to 'src/search/selectors')
-rw-r--r--src/search/selectors/dateselector.cpp61
-rw-r--r--src/search/selectors/dateselector.h42
-rw-r--r--src/search/selectors/filetypeselector.cpp81
-rw-r--r--src/search/selectors/filetypeselector.h36
-rw-r--r--src/search/selectors/minimumratingselector.cpp51
-rw-r--r--src/search/selectors/minimumratingselector.h42
-rw-r--r--src/search/selectors/tagsselector.cpp189
-rw-r--r--src/search/selectors/tagsselector.h45
8 files changed, 547 insertions, 0 deletions
diff --git a/src/search/selectors/dateselector.cpp b/src/search/selectors/dateselector.cpp
new file mode 100644
index 000000000..2951b1dc4
--- /dev/null
+++ b/src/search/selectors/dateselector.cpp
@@ -0,0 +1,61 @@
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]>
+
+ 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
index 000000000..99cecec06
--- /dev/null
+++ b/src/search/selectors/dateselector.h
@@ -0,0 +1,42 @@
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]>
+
+ 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
index 000000000..7852aced2
--- /dev/null
+++ b/src/search/selectors/filetypeselector.cpp
@@ -0,0 +1,81 @@
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]>
+
+ 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
index 000000000..bfc827344
--- /dev/null
+++ b/src/search/selectors/filetypeselector.h
@@ -0,0 +1,36 @@
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]>
+
+ 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
index 000000000..e46dfd4e0
--- /dev/null
+++ b/src/search/selectors/minimumratingselector.cpp
@@ -0,0 +1,51 @@
+/*
+ SPDX-FileCopyrightText: 2012 Peter Penz <[email protected]>
+ SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]>
+
+ 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
index 000000000..02364cd1a
--- /dev/null
+++ b/src/search/selectors/minimumratingselector.h
@@ -0,0 +1,42 @@
+/*
+ SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]>
+
+ 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
index 000000000..95d7ff52a
--- /dev/null
+++ b/src/search/selectors/tagsselector.cpp
@@ -0,0 +1,189 @@
+/*
+ SPDX-FileCopyrightText: 2019 Ismael Asensio <[email protected]>
+ SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]>
+
+ 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
index 000000000..386cbb924
--- /dev/null
+++ b/src/search/selectors/tagsselector.h
@@ -0,0 +1,45 @@
+/*
+ SPDX-FileCopyrightText: 2019 Ismael Asensio <[email protected]>
+ SPDX-FileCopyrightText: 2025 Felix Ernst <[email protected]>
+
+ 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