1/*
2 This file is part of the KDE libraries
3 SPDX-FileCopyrightText: 2001-2004 Anders Lund <anders@alweb.dk>
4
5 SPDX-License-Identifier: LGPL-2.0-only
6*/
7
8#include "kmimetypechooser.h"
9
10#include "kmimetypeeditor.h"
11#include <qmimedatabase.h>
12
13#include <QDialogButtonBox>
14#include <QLabel>
15#include <QLineEdit>
16#include <QPushButton>
17#include <QSortFilterProxyModel>
18#include <QStandardItemModel>
19#include <QStandardPaths>
20#include <QTreeView>
21#include <QVBoxLayout>
22
23// BEGIN KMimeTypeChooserPrivate
24class KMimeTypeChooserPrivate
25{
26public:
27 KMimeTypeChooserPrivate(KMimeTypeChooser *parent)
28 : q(parent)
29 {
30 }
31
32 void loadMimeTypes(const QStringList &selected = QStringList());
33 QList<const QStandardItem *> getCheckedItems();
34
35 void editMimeType();
36 void slotCurrentChanged(const QModelIndex &index);
37 void slotSycocaDatabaseChanged(const QStringList &);
38
39 KMimeTypeChooser *const q;
40 QTreeView *mimeTypeTree = nullptr;
41 QStandardItemModel *m_model = nullptr;
42 QSortFilterProxyModel *m_proxyModel = nullptr;
43 QLineEdit *m_filterLineEdit = nullptr;
44 QPushButton *btnEditMimeType = nullptr;
45
46 QString defaultgroup;
47 QStringList groups;
48 int visuals;
49};
50// END
51
52static const char s_keditfiletypeExecutable[] = "keditfiletype";
53
54// BEGIN KMimeTypeChooser
55KMimeTypeChooser::KMimeTypeChooser(const QString &text,
56 const QStringList &selMimeTypes,
57 const QString &defaultGroup,
58 const QStringList &groupsToShow,
59 int visuals,
60 QWidget *parent)
61 : QWidget(parent)
62 , d(new KMimeTypeChooserPrivate(this))
63{
64 d->defaultgroup = defaultGroup;
65 d->groups = groupsToShow;
66 if (visuals & EditButton) {
67 if (QStandardPaths::findExecutable(executableName: QString::fromLatin1(ba: s_keditfiletypeExecutable)).isEmpty()) {
68 visuals &= ~EditButton;
69 }
70 }
71 d->visuals = visuals;
72
73 QVBoxLayout *vboxLayout = new QVBoxLayout(this);
74 vboxLayout->setContentsMargins(left: 0, top: 0, right: 0, bottom: 0);
75 if (!text.isEmpty()) {
76 vboxLayout->addWidget(new QLabel(text, this));
77 }
78
79 d->mimeTypeTree = new QTreeView(this);
80 d->m_model = new QStandardItemModel(d->mimeTypeTree);
81 d->m_proxyModel = new QSortFilterProxyModel(d->mimeTypeTree);
82 d->m_proxyModel->setRecursiveFilteringEnabled(true);
83 d->m_proxyModel->setFilterKeyColumn(-1);
84 d->m_proxyModel->setSourceModel(d->m_model);
85 d->mimeTypeTree->setModel(d->m_proxyModel);
86
87 d->m_filterLineEdit = new QLineEdit(this);
88 d->m_filterLineEdit->setPlaceholderText(tr(s: "Search for file type or filename pattern...", c: "@info:placeholder"));
89 QLabel *filterLabel = new QLabel(tr(s: "&Filter:", c: "@label:textbox"));
90 filterLabel->setBuddy(d->m_filterLineEdit);
91 connect(sender: d->m_filterLineEdit, signal: &QLineEdit::textChanged, context: this, slot: [this](const QString &text) {
92 d->m_proxyModel->setFilterRegularExpression(
93 QRegularExpression(text, QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption));
94 });
95
96 QHBoxLayout *filterLayout = new QHBoxLayout();
97 filterLayout->addWidget(filterLabel);
98 filterLayout->addWidget(d->m_filterLineEdit);
99 vboxLayout->addLayout(layout: filterLayout);
100 d->m_filterLineEdit->setFocus();
101
102 vboxLayout->addWidget(d->mimeTypeTree);
103 QStringList headerLabels({tr(s: "MIME Type", c: "@title:column")});
104
105 if (visuals & Comments) {
106 headerLabels.append(t: tr(s: "Comment", c: "@title:column"));
107 }
108
109 if (visuals & Patterns) {
110 headerLabels.append(t: tr(s: "Patterns", c: "@title:column"));
111 }
112
113 d->m_model->setColumnCount(headerLabels.count());
114 d->m_model->setHorizontalHeaderLabels(headerLabels);
115 QFontMetrics fm(d->mimeTypeTree->fontMetrics());
116 // big enough for most names/comments, but not for the insanely long ones
117 const int optWidth = 20 * fm.averageCharWidth();
118 d->mimeTypeTree->setColumnWidth(column: 0, width: optWidth);
119 d->mimeTypeTree->setColumnWidth(column: 1, width: optWidth);
120
121 d->loadMimeTypes(selected: selMimeTypes);
122
123 if (visuals & EditButton) {
124 QHBoxLayout *buttonLayout = new QHBoxLayout();
125 buttonLayout->addStretch(stretch: 1);
126 d->btnEditMimeType = new QPushButton(tr(s: "&Edit...", c: "@action:button"), this);
127 buttonLayout->addWidget(d->btnEditMimeType);
128 d->btnEditMimeType->setEnabled(false);
129
130 connect(sender: d->btnEditMimeType, signal: &QPushButton::clicked, context: this, slot: [this]() {
131 d->editMimeType();
132 });
133 connect(sender: d->mimeTypeTree, signal: &QAbstractItemView::doubleClicked, context: this, slot: [this]() {
134 d->editMimeType();
135 });
136
137 connect(sender: d->mimeTypeTree, signal: &QTreeView::activated, context: this, slot: [this](const QModelIndex &index) {
138 d->slotCurrentChanged(index);
139 });
140
141 d->btnEditMimeType->setToolTip(tr(s: "Launch the MIME type editor", c: "@info:tooltip"));
142
143 vboxLayout->addLayout(layout: buttonLayout);
144 }
145}
146
147KMimeTypeChooser::~KMimeTypeChooser() = default;
148
149void KMimeTypeChooserPrivate::loadMimeTypes(const QStringList &_selectedMimeTypes)
150{
151 QStringList selMimeTypes;
152
153 if (!_selectedMimeTypes.isEmpty()) {
154 selMimeTypes = _selectedMimeTypes;
155 } else {
156 selMimeTypes = q->mimeTypes();
157 }
158
159 std::vector<QStandardItem *> parentGroups;
160 QMimeDatabase db;
161 const QList<QMimeType> mimetypes = db.allMimeTypes();
162
163 bool agroupisopen = false;
164 QStandardItem *idefault = nullptr; // open this, if all other fails
165 QStandardItem *firstChecked = nullptr; // make this one visible after the loop
166
167 for (const QMimeType &mt : mimetypes) {
168 const QString mimetype = mt.name();
169 const int index = mimetype.indexOf(c: QLatin1Char('/'));
170 // e.g. "text", "audio", "inode"
171 const QString maj = mimetype.left(n: index);
172
173 if (!groups.isEmpty() && !groups.contains(str: maj)) {
174 continue;
175 }
176
177 QStandardItem *groupItem;
178
179 auto it = std::find_if(first: parentGroups.cbegin(), last: parentGroups.cend(), pred: [maj](const QStandardItem *item) {
180 return maj == item->text();
181 });
182
183 if (it == parentGroups.cend()) {
184 groupItem = new QStandardItem(maj);
185 groupItem->setFlags(Qt::ItemIsEnabled);
186 // a dud item to fill the patterns column next to "groupItem" and setFlags() on it
187 QStandardItem *secondColumn = new QStandardItem();
188 secondColumn->setFlags(Qt::NoItemFlags);
189 QStandardItem *thirdColumn = new QStandardItem();
190 thirdColumn->setFlags(Qt::NoItemFlags);
191 m_model->appendRow(items: {groupItem, secondColumn, thirdColumn});
192 parentGroups.push_back(x: groupItem);
193 if (maj == defaultgroup) {
194 idefault = groupItem;
195 }
196 } else {
197 groupItem = *it;
198 }
199
200 // e.g. "html", "plain", "mp4"
201 const QString min = mimetype.mid(position: index + 1);
202 QStandardItem *mime = new QStandardItem(QIcon::fromTheme(name: mt.iconName()), min);
203 mime->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
204
205 QStandardItem *comments = nullptr;
206 if (visuals & KMimeTypeChooser::Comments) {
207 comments = new QStandardItem(mt.comment());
208 comments->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
209 }
210
211 QStandardItem *patterns = nullptr;
212
213 if (visuals & KMimeTypeChooser::Patterns) {
214 patterns = new QStandardItem(mt.globPatterns().join(sep: QLatin1String("; ")));
215 patterns->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
216 }
217
218 groupItem->appendRow(aitems: QList<QStandardItem *>({mime, comments, patterns}));
219
220 if (selMimeTypes.contains(str: mimetype)) {
221 mime->setCheckState(Qt::Checked);
222 const QModelIndex index = m_proxyModel->mapFromSource(sourceIndex: m_model->indexFromItem(item: groupItem));
223 mimeTypeTree->expand(index);
224 agroupisopen = true;
225 if (!firstChecked) {
226 firstChecked = mime;
227 }
228 } else {
229 mime->setCheckState(Qt::Unchecked);
230 }
231 }
232
233 m_model->sort(column: 0);
234
235 if (firstChecked) {
236 const QModelIndex index = m_proxyModel->mapFromSource(sourceIndex: m_model->indexFromItem(item: firstChecked));
237 mimeTypeTree->scrollTo(index);
238 }
239
240 if (!agroupisopen && idefault) {
241 const QModelIndex index = m_proxyModel->mapFromSource(sourceIndex: m_model->indexFromItem(item: idefault));
242 mimeTypeTree->expand(index);
243 mimeTypeTree->scrollTo(index);
244 }
245}
246
247void KMimeTypeChooserPrivate::editMimeType()
248{
249 QModelIndex mimeIndex = m_proxyModel->mapToSource(proxyIndex: mimeTypeTree->currentIndex());
250
251 // skip parent (non-leaf) nodes
252 if (m_model->hasChildren(parent: mimeIndex)) {
253 return;
254 }
255
256 if (mimeIndex.column() > 0) { // we need the item from column 0 to concatenate "mt" below
257 mimeIndex = m_model->sibling(row: mimeIndex.row(), column: 0, idx: mimeIndex);
258 }
259
260 const QStandardItem *item = m_model->itemFromIndex(index: mimeIndex);
261 const QString mt = (item->parent())->text() + QLatin1Char('/') + item->text();
262 KMimeTypeEditor::editMimeType(mimeType: mt, widget: q);
263
264 // TODO: use a QFileSystemWatcher on one of the shared-mime-info generated files, instead.
265 // q->connect( KSycoca::self(), SIGNAL(databaseChanged(QStringList)),
266 // q, SLOT(slotSycocaDatabaseChanged(QStringList)) );
267 // TODO: use QFileSystemWatcher to be told when keditfiletype changed a MIME type
268 // or a better idea: a QMimeDatabaseWatcher class in Qt itself
269}
270
271void KMimeTypeChooserPrivate::slotCurrentChanged(const QModelIndex &index)
272{
273 if (btnEditMimeType) {
274 const QModelIndex srcIndex = m_proxyModel->mapToSource(proxyIndex: index);
275 const QStandardItem *currentItem = m_model->itemFromIndex(index: srcIndex);
276 btnEditMimeType->setEnabled(currentItem && currentItem->parent());
277 }
278}
279
280// TODO: see editMimeType
281void KMimeTypeChooserPrivate::slotSycocaDatabaseChanged(const QStringList &changedResources)
282{
283 if (changedResources.contains(str: QLatin1String("xdgdata-mime"))) {
284 loadMimeTypes();
285 }
286}
287
288QList<const QStandardItem *> KMimeTypeChooserPrivate::getCheckedItems()
289{
290 QList<const QStandardItem *> lst;
291 const int rowCount = m_model->rowCount();
292 for (int i = 0; i < rowCount; ++i) {
293 const QStandardItem *groupItem = m_model->item(row: i);
294 const int childCount = groupItem->rowCount();
295 for (int j = 0; j < childCount; ++j) {
296 const QStandardItem *child = groupItem->child(row: j);
297 if (child->checkState() == Qt::Checked) {
298 lst.append(t: child);
299 }
300 }
301 }
302 return lst;
303}
304
305QStringList KMimeTypeChooser::mimeTypes() const
306{
307 QStringList mimeList;
308 const QList<const QStandardItem *> checkedItems = d->getCheckedItems();
309 mimeList.reserve(asize: checkedItems.size());
310 for (const QStandardItem *item : checkedItems) {
311 mimeList.append(t: item->parent()->text() + QLatin1Char('/') + item->text());
312 }
313 return mimeList;
314}
315
316QStringList KMimeTypeChooser::patterns() const
317{
318 QStringList patternList;
319 const QList<const QStandardItem *> checkedItems = d->getCheckedItems();
320 QMimeDatabase db;
321 for (const QStandardItem *item : checkedItems) {
322 QMimeType mime = db.mimeTypeForName(nameOrAlias: item->parent()->text() + QLatin1Char('/') + item->text());
323 Q_ASSERT(mime.isValid());
324 patternList += mime.globPatterns();
325 }
326 return patternList;
327}
328// END
329
330// BEGIN KMimeTypeChooserDialogPrivate
331
332class KMimeTypeChooserDialogPrivate
333{
334public:
335 KMimeTypeChooserDialogPrivate(KMimeTypeChooserDialog *parent)
336 : q(parent)
337 {
338 }
339
340 void init();
341
342 KMimeTypeChooserDialog *q;
343 KMimeTypeChooser *m_chooser;
344};
345
346// END
347
348// BEGIN KMimeTypeChooserDialog
349KMimeTypeChooserDialog::KMimeTypeChooserDialog(const QString &title,
350 const QString &text,
351 const QStringList &selMimeTypes,
352 const QString &defaultGroup,
353 const QStringList &groupsToShow,
354 int visuals,
355 QWidget *parent)
356 : QDialog(parent)
357 , d(new KMimeTypeChooserDialogPrivate(this))
358{
359 setWindowTitle(title);
360
361 d->m_chooser = new KMimeTypeChooser(text, selMimeTypes, defaultGroup, groupsToShow, visuals, this);
362 d->init();
363}
364
365KMimeTypeChooserDialog::KMimeTypeChooserDialog(const QString &title,
366 const QString &text,
367 const QStringList &selMimeTypes,
368 const QString &defaultGroup,
369 QWidget *parent)
370 : QDialog(parent)
371 , d(new KMimeTypeChooserDialogPrivate(this))
372{
373 setWindowTitle(title);
374
375 d->m_chooser = new KMimeTypeChooser(text,
376 selMimeTypes,
377 defaultGroup,
378 QStringList(),
379 KMimeTypeChooser::Comments | KMimeTypeChooser::Patterns | KMimeTypeChooser::EditButton,
380 this);
381 d->init();
382}
383
384KMimeTypeChooser *KMimeTypeChooserDialog::chooser()
385{
386 return d->m_chooser;
387}
388
389void KMimeTypeChooserDialogPrivate::init()
390{
391 QVBoxLayout *layout = new QVBoxLayout(q);
392
393 layout->addWidget(m_chooser);
394
395 QDialogButtonBox *buttonBox = new QDialogButtonBox(q);
396 buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
397 QObject::connect(sender: buttonBox, signal: &QDialogButtonBox::accepted, context: q, slot: &QDialog::accept);
398 QObject::connect(sender: buttonBox, signal: &QDialogButtonBox::rejected, context: q, slot: &QDialog::reject);
399 layout->addWidget(buttonBox);
400}
401
402KMimeTypeChooserDialog::~KMimeTypeChooserDialog() = default;
403
404QSize KMimeTypeChooserDialog::sizeHint() const
405{
406 QFontMetrics fm(fontMetrics());
407 const int viewableSize = fm.averageCharWidth() * 60;
408 return QSize(viewableSize, viewableSize);
409}
410
411// END KMimeTypeChooserDialog
412
413#include "moc_kmimetypechooser.cpp"
414

source code of kwidgetsaddons/src/kmimetypechooser.cpp