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