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
24 class KMimeTypeChooserPrivate
25 {
26 public:
KMimeTypeChooserPrivate(KMimeTypeChooser * parent)27 KMimeTypeChooserPrivate(KMimeTypeChooser *parent)
28 : q(parent)
29 {
30 }
31
32 void loadMimeTypes(const QStringList &selected = QStringList());
33 QVector<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
52 static const char s_keditfiletypeExecutable[] = "keditfiletype5";
53
54 // BEGIN KMimeTypeChooser
KMimeTypeChooser(const QString & text,const QStringList & selMimeTypes,const QString & defaultGroup,const QStringList & groupsToShow,int visuals,QWidget * parent)55 KMimeTypeChooser::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(QString::fromLatin1(s_keditfiletypeExecutable)).isEmpty()) {
68 visuals &= ~EditButton;
69 }
70 }
71 d->visuals = visuals;
72
73 QVBoxLayout *vboxLayout = new QVBoxLayout(this);
74 vboxLayout->setContentsMargins(0, 0, 0, 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("Search for file type or filename pattern...", "@info:placeholder"));
89 QLabel *filterLabel = new QLabel(tr("&Filter:", "@label:textbox"));
90 filterLabel->setBuddy(d->m_filterLineEdit);
91 connect(d->m_filterLineEdit, &QLineEdit::textChanged, this, [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(filterLayout);
100 d->m_filterLineEdit->setFocus();
101
102 vboxLayout->addWidget(d->mimeTypeTree);
103 QStringList headerLabels({tr("MIME Type", "@title:column")});
104
105 if (visuals & Comments) {
106 headerLabels.append(tr("Comment", "@title:column"));
107 }
108
109 if (visuals & Patterns) {
110 headerLabels.append(tr("Patterns", "@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(0, optWidth);
119 d->mimeTypeTree->setColumnWidth(1, optWidth);
120
121 d->loadMimeTypes(selMimeTypes);
122
123 if (visuals & EditButton) {
124 QHBoxLayout *buttonLayout = new QHBoxLayout();
125 buttonLayout->addStretch(1);
126 d->btnEditMimeType = new QPushButton(tr("&Edit...", "@action:button"), this);
127 buttonLayout->addWidget(d->btnEditMimeType);
128 d->btnEditMimeType->setEnabled(false);
129
130 connect(d->btnEditMimeType, &QPushButton::clicked, this, [this]() {
131 d->editMimeType();
132 });
133 connect(d->mimeTypeTree, &QAbstractItemView::doubleClicked, this, [this]() {
134 d->editMimeType();
135 });
136
137 connect(d->mimeTypeTree, &QTreeView::activated, this, [this](const QModelIndex &index) {
138 d->slotCurrentChanged(index);
139 });
140
141 d->btnEditMimeType->setToolTip(tr("Launch the MIME type editor", "@info:tooltip"));
142
143 vboxLayout->addLayout(buttonLayout);
144 }
145 }
146
147 KMimeTypeChooser::~KMimeTypeChooser() = default;
148
loadMimeTypes(const QStringList & _selectedMimeTypes)149 void 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(QLatin1Char('/'));
170 // e.g. "text", "audio", "inode"
171 const QString maj = mimetype.left(index);
172
173 if (!groups.isEmpty() && !groups.contains(maj)) {
174 continue;
175 }
176
177 QStandardItem *groupItem;
178
179 auto it = std::find_if(parentGroups.cbegin(), parentGroups.cend(), [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({groupItem, secondColumn, thirdColumn});
192 parentGroups.push_back(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(index + 1);
202 QStandardItem *mime = new QStandardItem(QIcon::fromTheme(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(QLatin1String("; ")));
215 patterns->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
216 }
217
218 groupItem->appendRow(QList<QStandardItem *>({mime, comments, patterns}));
219
220 if (selMimeTypes.contains(mimetype)) {
221 mime->setCheckState(Qt::Checked);
222 const QModelIndex index = m_proxyModel->mapFromSource(m_model->indexFromItem(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(0);
234
235 if (firstChecked) {
236 const QModelIndex index = m_proxyModel->mapFromSource(m_model->indexFromItem(firstChecked));
237 mimeTypeTree->scrollTo(index);
238 }
239
240 if (!agroupisopen && idefault) {
241 const QModelIndex index = m_proxyModel->mapFromSource(m_model->indexFromItem(idefault));
242 mimeTypeTree->expand(index);
243 mimeTypeTree->scrollTo(index);
244 }
245 }
246
editMimeType()247 void KMimeTypeChooserPrivate::editMimeType()
248 {
249 QModelIndex mimeIndex = m_proxyModel->mapToSource(mimeTypeTree->currentIndex());
250
251 // skip parent (non-leaf) nodes
252 if (m_model->hasChildren(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(mimeIndex.row(), 0, mimeIndex);
258 }
259
260 const QStandardItem *item = m_model->itemFromIndex(mimeIndex);
261 const QString mt = (item->parent())->text() + QLatin1Char('/') + item->text();
262 KMimeTypeEditor::editMimeType(mt, q);
263
264 // KF5 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 #pragma message("KF5 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
slotCurrentChanged(const QModelIndex & index)271 void KMimeTypeChooserPrivate::slotCurrentChanged(const QModelIndex &index)
272 {
273 if (btnEditMimeType) {
274 const QModelIndex srcIndex = m_proxyModel->mapToSource(index);
275 const QStandardItem *currentItem = m_model->itemFromIndex(srcIndex);
276 btnEditMimeType->setEnabled(currentItem && currentItem->parent());
277 }
278 }
279
280 // TODO: see editMimeType
slotSycocaDatabaseChanged(const QStringList & changedResources)281 void KMimeTypeChooserPrivate::slotSycocaDatabaseChanged(const QStringList &changedResources)
282 {
283 if (changedResources.contains(QLatin1String("xdgdata-mime"))) {
284 loadMimeTypes();
285 }
286 }
287
getCheckedItems()288 QVector<const QStandardItem *> KMimeTypeChooserPrivate::getCheckedItems()
289 {
290 QVector<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(i);
294 const int childCount = groupItem->rowCount();
295 for (int j = 0; j < childCount; ++j) {
296 const QStandardItem *child = groupItem->child(j);
297 if (child->checkState() == Qt::Checked) {
298 lst.append(child);
299 }
300 }
301 }
302 return lst;
303 }
304
mimeTypes() const305 QStringList KMimeTypeChooser::mimeTypes() const
306 {
307 QStringList mimeList;
308 const QVector<const QStandardItem *> checkedItems = d->getCheckedItems();
309 mimeList.reserve(checkedItems.size());
310 for (const QStandardItem *item : checkedItems) {
311 mimeList.append(item->parent()->text() + QLatin1Char('/') + item->text());
312 }
313 return mimeList;
314 }
315
patterns() const316 QStringList KMimeTypeChooser::patterns() const
317 {
318 QStringList patternList;
319 const QVector<const QStandardItem *> checkedItems = d->getCheckedItems();
320 QMimeDatabase db;
321 for (const QStandardItem *item : checkedItems) {
322 QMimeType mime = db.mimeTypeForName(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
332 class KMimeTypeChooserDialogPrivate
333 {
334 public:
KMimeTypeChooserDialogPrivate(KMimeTypeChooserDialog * parent)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
KMimeTypeChooserDialog(const QString & title,const QString & text,const QStringList & selMimeTypes,const QString & defaultGroup,const QStringList & groupsToShow,int visuals,QWidget * parent)349 KMimeTypeChooserDialog::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
KMimeTypeChooserDialog(const QString & title,const QString & text,const QStringList & selMimeTypes,const QString & defaultGroup,QWidget * parent)365 KMimeTypeChooserDialog::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
chooser()384 KMimeTypeChooser *KMimeTypeChooserDialog::chooser()
385 {
386 return d->m_chooser;
387 }
388
init()389 void 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(buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept);
398 QObject::connect(buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject);
399 layout->addWidget(buttonBox);
400 }
401
402 KMimeTypeChooserDialog::~KMimeTypeChooserDialog() = default;
403
sizeHint() const404 QSize 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