1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: Stephan Kulow <coolo@kde.org>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "kfilefiltercombo.h"
9#include "kfilefilter.h"
10#include "kfilefiltercombo_debug.h"
11
12#include <KLocalizedString>
13#include <QDebug>
14#include <QEvent>
15#include <QLineEdit>
16#include <QMimeDatabase>
17
18#include <config-kiofilewidgets.h>
19
20#include <algorithm>
21#include <utility>
22
23class KFileFilterComboPrivate
24{
25public:
26 explicit KFileFilterComboPrivate(KFileFilterCombo *qq)
27 : q(qq)
28 {
29 }
30
31 void slotFilterChanged();
32
33 KFileFilterCombo *const q;
34 // true when setMimeFilter was called
35 bool m_isMimeFilter = false;
36 QString m_lastFilter;
37 KFileFilter m_defaultFilter = KFileFilter::fromFilterString(i18nc("Default mime type filter that shows all file types", "*|All Files")).first();
38
39 QList<KFileFilter> m_filters;
40 bool m_allTypes;
41};
42
43KFileFilterCombo::KFileFilterCombo(QWidget *parent)
44 : KComboBox(true, parent)
45 , d(new KFileFilterComboPrivate(this))
46{
47 setTrapReturnKey(true);
48 setInsertPolicy(QComboBox::NoInsert);
49 connect(sender: this, signal: &QComboBox::activated, context: this, slot: &KFileFilterCombo::filterChanged);
50 connect(sender: this, signal: &KComboBox::returnPressed, context: this, slot: &KFileFilterCombo::filterChanged);
51 connect(sender: this, signal: &KFileFilterCombo::filterChanged, context: this, slot: [this]() {
52 d->slotFilterChanged();
53 });
54 d->m_allTypes = false;
55}
56
57KFileFilterCombo::~KFileFilterCombo() = default;
58
59void KFileFilterCombo::setFilters(const QList<KFileFilter> &filters, const KFileFilter &defaultFilter)
60{
61 clear();
62 d->m_filters.clear();
63 bool hasAllFilesFilter = false;
64 QMimeDatabase db;
65
66 const QList<KFileFilter> validFilters = [&filters] {
67 QList<KFileFilter> res;
68 res.reserve(asize: filters.size());
69 std::ranges::copy_if(filters, std::back_inserter(x&: res), [](const KFileFilter &f) {
70 return f.isValid();
71 });
72 return res;
73 }();
74
75 if (validFilters.isEmpty()) {
76 d->m_filters = {d->m_defaultFilter};
77 addItem(atext: d->m_defaultFilter.label());
78
79 d->m_lastFilter = currentText();
80 return;
81 }
82
83 d->m_allTypes = defaultFilter.isEmpty() && (validFilters.count() > 1);
84
85 if (!validFilters.isEmpty() && validFilters.first().mimePatterns().isEmpty()) {
86 d->m_allTypes = false;
87 }
88
89 // If there's MIME types that have the same comment, we will show the extension
90 // in addition to the MIME type comment
91 QHash<QString, int> allTypeComments;
92 for (const KFileFilter &filter : validFilters) {
93 allTypeComments[filter.label()] += 1;
94 }
95
96 for (const KFileFilter &filter : validFilters) {
97 const QStringList mimeTypes = filter.mimePatterns();
98
99 const bool isAllFileFilters = std::any_of(first: mimeTypes.cbegin(), last: mimeTypes.cend(), pred: [&db](const QString &mimeTypeName) {
100 const QMimeType type = db.mimeTypeForName(nameOrAlias: mimeTypeName);
101
102 if (!type.isValid()) {
103 qCWarning(KIO_KFILEWIDGETS_KFILEFILTERCOMBO) << mimeTypeName << "is not a valid MIME type";
104 return false;
105 }
106
107 return type.name().startsWith(s: QLatin1String("all/")) || type.isDefault();
108 });
109
110 if (isAllFileFilters) {
111 hasAllFilesFilter = true;
112 continue;
113 }
114
115 if (allTypeComments.value(key: filter.label()) > 1) {
116 QStringList mimeSuffixes;
117
118 for (const QString &mimeTypeName : filter.mimePatterns()) {
119 const QMimeType type = db.mimeTypeForName(nameOrAlias: mimeTypeName);
120 mimeSuffixes << type.suffixes();
121 }
122
123 const QString label = i18nc("%1 is the mimetype name, %2 is the extensions", "%1 (%2)", filter.label(), mimeSuffixes.join(QLatin1String(", ")));
124 KFileFilter newFilter(label, filter.filePatterns(), filter.mimePatterns());
125
126 d->m_filters.append(t: newFilter);
127 addItem(atext: newFilter.label());
128 } else {
129 d->m_filters.append(t: filter);
130 addItem(atext: filter.label());
131 }
132
133 if (filter == defaultFilter) {
134 setCurrentIndex(count() - 1);
135 }
136 }
137
138 if (count() == 1) {
139 d->m_allTypes = false;
140 }
141
142 if (d->m_allTypes) {
143 QStringList allMimePatterns;
144 QStringList allFilePatterns;
145 for (const KFileFilter &filter : std::as_const(t&: d->m_filters)) {
146 allMimePatterns << filter.mimePatterns();
147 allFilePatterns << filter.filePatterns();
148 }
149
150 KFileFilter allSupportedFilesFilter;
151
152 if (count() <= 3) { // show the MIME type comments of at max 3 types
153 QStringList allComments;
154 for (const KFileFilter &filter : std::as_const(t&: d->m_filters)) {
155 allComments << filter.label();
156 }
157
158 allSupportedFilesFilter = KFileFilter(allComments.join(QStringLiteral(", ")), allFilePatterns, allMimePatterns);
159 } else {
160 allSupportedFilesFilter = KFileFilter(i18n("All Supported Files"), allFilePatterns, allMimePatterns);
161 }
162
163 insertItem(aindex: 0, atext: allSupportedFilesFilter.label());
164 d->m_filters.prepend(t: allSupportedFilesFilter);
165 setCurrentIndex(0);
166 }
167
168 if (hasAllFilesFilter) {
169 addItem(i18n("All Files"));
170
171 KFileFilter allFilter(i18n("All Files"), {}, {QStringLiteral("application/octet-stream")});
172
173 d->m_filters.append(t: allFilter);
174
175 if (defaultFilter == allFilter) {
176 setCurrentIndex(count() - 1);
177 }
178 }
179
180 d->m_lastFilter = currentText();
181
182 Q_EMIT filterChanged();
183}
184
185KFileFilter KFileFilterCombo::currentFilter() const
186{
187 if (currentText() != itemText(index: currentIndex())) {
188 // The user edited the text
189
190 const QList<KFileFilter> filter = KFileFilter::fromFilterString(filterString: currentText());
191
192 if (!filter.isEmpty()) {
193 return filter.first();
194 } else {
195 return KFileFilter();
196 }
197 } else {
198 if (currentIndex() == -1) {
199 return KFileFilter();
200 }
201
202 return d->m_filters[currentIndex()];
203 }
204}
205
206bool KFileFilterCombo::showsAllTypes() const
207{
208 return d->m_allTypes;
209}
210
211QList<KFileFilter> KFileFilterCombo::filters() const
212{
213 return d->m_filters;
214}
215
216void KFileFilterCombo::setCurrentFilter(const KFileFilter &filter)
217{
218 auto it = std::find(first: d->m_filters.cbegin(), last: d->m_filters.cend(), val: filter);
219
220 if (it == d->m_filters.cend()) {
221 qCWarning(KIO_KFILEWIDGETS_KFILEFILTERCOMBO) << "KFileFilterCombo::setCurrentFilter: Could not find file filter" << filter;
222 setCurrentIndex(-1);
223 Q_EMIT filterChanged();
224 return;
225 }
226
227 setCurrentIndex(std::distance(first: d->m_filters.cbegin(), last: it));
228 Q_EMIT filterChanged();
229}
230
231void KFileFilterComboPrivate::slotFilterChanged()
232{
233 m_lastFilter = q->currentText();
234}
235
236bool KFileFilterCombo::eventFilter(QObject *o, QEvent *e)
237{
238 if (o == lineEdit() && e->type() == QEvent::FocusOut) {
239 if (currentText() != d->m_lastFilter) {
240 Q_EMIT filterChanged();
241 }
242 }
243
244 return KComboBox::eventFilter(watched: o, event: e);
245}
246
247void KFileFilterCombo::setDefaultFilter(const KFileFilter &filter)
248{
249 d->m_defaultFilter = filter;
250}
251
252KFileFilter KFileFilterCombo::defaultFilter() const
253{
254 return d->m_defaultFilter;
255}
256
257#include "moc_kfilefiltercombo.cpp"
258

source code of kio/src/filewidgets/kfilefiltercombo.cpp