2 SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
3 SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
5 SPDX-License-Identifier: GPL-2.0-or-later
8 #include "tagsselector.h"
10 #include "../dolphinquery.h"
12 #include <KCoreDirLister>
13 #include <KLocalizedString>
14 #include <KProtocolInfo>
17 #include <QStringList>
19 using namespace Search
;
24 * @brief Provides the list of tags to all TagsSelectors.
26 * 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
27 * with such a tag exists if we did not find it automatically.
28 * @note Use the tagsList() function below instead of constructing TagsList objects yourself.
30 class TagsList
: public QStringList
, public QObject
36 m_tagsLister
->openUrl(QUrl(QStringLiteral("tags:/")), KCoreDirLister::OpenUrlFlag::Reload
);
37 connect(m_tagsLister
.get(), &KCoreDirLister::itemsAdded
, this, [this](const QUrl
&, const KFileItemList
&items
) {
38 for (const KFileItem
&item
: items
) {
42 sort(Qt::CaseInsensitive
);
46 virtual ~TagsList() = default;
48 void addTag(const QString
&tag
)
54 sort(Qt::CaseInsensitive
);
57 /** Used to access to the itemsAdded signal so outside users of this class know when items were added. */
58 KCoreDirLister
*tagsLister() const
60 return m_tagsLister
.get();
64 std::unique_ptr
<KCoreDirLister
> m_tagsLister
= std::make_unique
<KCoreDirLister
>();
68 * @returns a list of all tags found since the construction of the first TagsSelector object.
69 * @note Use this function instead of constructing additional TagsList objects.
73 static TagsList
*g_tagsList
= new TagsList
;
78 Search::TagsSelector::TagsSelector(std::shared_ptr
<const DolphinQuery
> dolphinQuery
, QWidget
*parent
)
80 , UpdatableStateInterface
{dolphinQuery
}
82 setToolButtonStyle(Qt::ToolButtonTextBesideIcon
);
83 setPopupMode(QToolButton::InstantPopup
);
85 auto menu
= new QMenu
{this};
87 connect(menu
, &QMenu::aboutToShow
, this, [this]() {
88 TagsList
*tags
= tagsList();
89 // 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.
90 // However this selector is connected to the itemsAdded() signal, so we will add any new tags eventually.
91 tags
->tagsLister()->updateDirectory(tags
->tagsLister()->url());
92 updateMenu(m_searchConfiguration
);
95 TagsList
*tags
= tagsList();
96 if (tags
->isEmpty()) {
97 // 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.
101 &KCoreDirLister::itemsAdded
,
106 Qt::SingleShotConnection
);
109 connect(tags
->tagsLister(), &KCoreDirLister::itemsAdded
, this, [this, menu
]() {
110 if (menu
->isVisible()) {
111 updateMenu(m_searchConfiguration
);
115 updateStateToMatch(std::move(dolphinQuery
));
118 void TagsSelector::removeRestriction()
120 Q_ASSERT(!m_searchConfiguration
->requiredTags().isEmpty());
121 DolphinQuery searchConfigurationCopy
= *m_searchConfiguration
;
122 searchConfigurationCopy
.setRequiredTags({});
123 Q_EMIT
configurationChanged(std::move(searchConfigurationCopy
));
126 void TagsSelector::updateMenu(const std::shared_ptr
<const DolphinQuery
> &dolphinQuery
)
128 if (!KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) {
131 const bool menuWasVisible
= menu()->isVisible();
132 if (menuWasVisible
) {
133 menu()->hide(); // The menu needs to be hidden now, then updated, and then shown again.
135 // Delete all existing actions in the menu
136 for (QAction
*action
: menu()->actions()) {
137 action
->deleteLater();
141 const TagsList
*tags
= tagsList();
142 const bool onlyOneTagExists
= tags
->count() == 1;
144 for (const QString
&tag
: *tags
) {
145 QAction
*tagAction
= new QAction
{QIcon::fromTheme(QStringLiteral("tag")), tag
, menu()};
146 tagAction
->setCheckable(true);
147 tagAction
->setChecked(dolphinQuery
->requiredTags().contains(tag
));
148 connect(tagAction
, &QAction::triggered
, this, [this, tag
, onlyOneTagExists
](bool checked
) {
149 QStringList requiredTags
= m_searchConfiguration
->requiredTags();
150 if (checked
== requiredTags
.contains(tag
)) {
151 return; // Already selected.
154 requiredTags
.append(tag
);
156 requiredTags
.removeOne(tag
);
158 DolphinQuery searchConfigurationCopy
= *m_searchConfiguration
;
159 searchConfigurationCopy
.setRequiredTags(requiredTags
);
160 Q_EMIT
configurationChanged(std::move(searchConfigurationCopy
));
162 if (!onlyOneTagExists
) {
163 // Keep the menu open to allow easier tag multi-selection.
167 menu()->addAction(tagAction
);
169 if (menuWasVisible
) {
174 void TagsSelector::updateState(const std::shared_ptr
<const DolphinQuery
> &dolphinQuery
)
176 if (dolphinQuery
->requiredTags().count()) {
177 setIcon(QIcon::fromTheme(QStringLiteral("tag")));
178 setText(dolphinQuery
->requiredTags().join(i18nc("list separator for file tags e.g. all images tagged 'family & party & 2025'", " && ")));
180 setIcon(QIcon
{}); // No icon for the empty state
181 setText(i18nc("@action:button Required tags for search results: None", "None"));
183 for (const auto &tag
: dolphinQuery
->requiredTags()) {
184 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.
186 if (menu()->isVisible()) {
187 updateMenu(dolphinQuery
);