]> cloud.milkyroute.net Git - dolphin.git/blob - src/search/selectors/tagsselector.cpp
95d7ff52aa4cd026c9415abf1eaf2ea66c8c37e2
[dolphin.git] / src / search / selectors / tagsselector.cpp
1 /*
2 SPDX-FileCopyrightText: 2019 Ismael Asensio <isma.af@mgmail.com>
3 SPDX-FileCopyrightText: 2025 Felix Ernst <felixernst@kde.org>
4
5 SPDX-License-Identifier: GPL-2.0-or-later
6 */
7
8 #include "tagsselector.h"
9
10 #include "../dolphinquery.h"
11
12 #include <KCoreDirLister>
13 #include <KLocalizedString>
14 #include <KProtocolInfo>
15
16 #include <QMenu>
17 #include <QStringList>
18
19 using namespace Search;
20
21 namespace
22 {
23 /**
24 * @brief Provides the list of tags to all TagsSelectors.
25 *
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.
29 */
30 class TagsList : public QStringList, public QObject
31 {
32 public:
33 TagsList()
34 : QStringList{}
35 {
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) {
39 append(item.text());
40 }
41 removeDuplicates();
42 sort(Qt::CaseInsensitive);
43 });
44 };
45
46 virtual ~TagsList() = default;
47
48 void addTag(const QString &tag)
49 {
50 if (contains(tag)) {
51 return;
52 }
53 append(tag);
54 sort(Qt::CaseInsensitive);
55 };
56
57 /** Used to access to the itemsAdded signal so outside users of this class know when items were added. */
58 KCoreDirLister *tagsLister() const
59 {
60 return m_tagsLister.get();
61 };
62
63 private:
64 std::unique_ptr<KCoreDirLister> m_tagsLister = std::make_unique<KCoreDirLister>();
65 };
66
67 /**
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.
70 */
71 TagsList *tagsList()
72 {
73 static TagsList *g_tagsList = new TagsList;
74 return g_tagsList;
75 }
76 }
77
78 Search::TagsSelector::TagsSelector(std::shared_ptr<const DolphinQuery> dolphinQuery, QWidget *parent)
79 : QToolButton{parent}
80 , UpdatableStateInterface{dolphinQuery}
81 {
82 setToolButtonStyle(Qt::ToolButtonTextBesideIcon);
83 setPopupMode(QToolButton::InstantPopup);
84
85 auto menu = new QMenu{this};
86 setMenu(menu);
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);
93 });
94
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.
98 setEnabled(false);
99 connect(
100 tags->tagsLister(),
101 &KCoreDirLister::itemsAdded,
102 this,
103 [this]() {
104 setEnabled(true);
105 },
106 Qt::SingleShotConnection);
107 }
108
109 connect(tags->tagsLister(), &KCoreDirLister::itemsAdded, this, [this, menu]() {
110 if (menu->isVisible()) {
111 updateMenu(m_searchConfiguration);
112 }
113 });
114
115 updateStateToMatch(std::move(dolphinQuery));
116 }
117
118 void TagsSelector::removeRestriction()
119 {
120 Q_ASSERT(!m_searchConfiguration->requiredTags().isEmpty());
121 DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
122 searchConfigurationCopy.setRequiredTags({});
123 Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
124 }
125
126 void TagsSelector::updateMenu(const std::shared_ptr<const DolphinQuery> &dolphinQuery)
127 {
128 if (!KProtocolInfo::isKnownProtocol(QStringLiteral("tags"))) {
129 return;
130 }
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.
134 }
135 // Delete all existing actions in the menu
136 for (QAction *action : menu()->actions()) {
137 action->deleteLater();
138 }
139 menu()->clear();
140 // Populate the menu
141 const TagsList *tags = tagsList();
142 const bool onlyOneTagExists = tags->count() == 1;
143
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.
152 }
153 if (checked) {
154 requiredTags.append(tag);
155 } else {
156 requiredTags.removeOne(tag);
157 }
158 DolphinQuery searchConfigurationCopy = *m_searchConfiguration;
159 searchConfigurationCopy.setRequiredTags(requiredTags);
160 Q_EMIT configurationChanged(std::move(searchConfigurationCopy));
161
162 if (!onlyOneTagExists) {
163 // Keep the menu open to allow easier tag multi-selection.
164 menu()->show();
165 }
166 });
167 menu()->addAction(tagAction);
168 }
169 if (menuWasVisible) {
170 menu()->show();
171 }
172 }
173
174 void TagsSelector::updateState(const std::shared_ptr<const DolphinQuery> &dolphinQuery)
175 {
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'", " && ")));
179 } else {
180 setIcon(QIcon{}); // No icon for the empty state
181 setText(i18nc("@action:button Required tags for search results: None", "None"));
182 }
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.
185 }
186 if (menu()->isVisible()) {
187 updateMenu(dolphinQuery);
188 }
189 }