File indexing completed on 2024-04-28 15:32:07

0001 /*
0002     This file is part of the KDE libraries
0003     SPDX-FileCopyrightText: 2001-2004 Anders Lund <anders@alweb.dk>
0004 
0005     SPDX-License-Identifier: LGPL-2.0-only
0006 */
0007 
0008 #include "kmimetypechooser.h"
0009 
0010 #include "kmimetypeeditor.h"
0011 #include <qmimedatabase.h>
0012 
0013 #include <QDialogButtonBox>
0014 #include <QLabel>
0015 #include <QLineEdit>
0016 #include <QPushButton>
0017 #include <QSortFilterProxyModel>
0018 #include <QStandardItemModel>
0019 #include <QStandardPaths>
0020 #include <QTreeView>
0021 #include <QVBoxLayout>
0022 
0023 // BEGIN KMimeTypeChooserPrivate
0024 class KMimeTypeChooserPrivate
0025 {
0026 public:
0027     KMimeTypeChooserPrivate(KMimeTypeChooser *parent)
0028         : q(parent)
0029     {
0030     }
0031 
0032     void loadMimeTypes(const QStringList &selected = QStringList());
0033     QVector<const QStandardItem *> getCheckedItems();
0034 
0035     void editMimeType();
0036     void slotCurrentChanged(const QModelIndex &index);
0037     void slotSycocaDatabaseChanged(const QStringList &);
0038 
0039     KMimeTypeChooser *const q;
0040     QTreeView *mimeTypeTree = nullptr;
0041     QStandardItemModel *m_model = nullptr;
0042     QSortFilterProxyModel *m_proxyModel = nullptr;
0043     QLineEdit *m_filterLineEdit = nullptr;
0044     QPushButton *btnEditMimeType = nullptr;
0045 
0046     QString defaultgroup;
0047     QStringList groups;
0048     int visuals;
0049 };
0050 // END
0051 
0052 static const char s_keditfiletypeExecutable[] = "keditfiletype5";
0053 
0054 // BEGIN KMimeTypeChooser
0055 KMimeTypeChooser::KMimeTypeChooser(const QString &text,
0056                                    const QStringList &selMimeTypes,
0057                                    const QString &defaultGroup,
0058                                    const QStringList &groupsToShow,
0059                                    int visuals,
0060                                    QWidget *parent)
0061     : QWidget(parent)
0062     , d(new KMimeTypeChooserPrivate(this))
0063 {
0064     d->defaultgroup = defaultGroup;
0065     d->groups = groupsToShow;
0066     if (visuals & EditButton) {
0067         if (QStandardPaths::findExecutable(QString::fromLatin1(s_keditfiletypeExecutable)).isEmpty()) {
0068             visuals &= ~EditButton;
0069         }
0070     }
0071     d->visuals = visuals;
0072 
0073     QVBoxLayout *vboxLayout = new QVBoxLayout(this);
0074     vboxLayout->setContentsMargins(0, 0, 0, 0);
0075     if (!text.isEmpty()) {
0076         vboxLayout->addWidget(new QLabel(text, this));
0077     }
0078 
0079     d->mimeTypeTree = new QTreeView(this);
0080     d->m_model = new QStandardItemModel(d->mimeTypeTree);
0081     d->m_proxyModel = new QSortFilterProxyModel(d->mimeTypeTree);
0082     d->m_proxyModel->setRecursiveFilteringEnabled(true);
0083     d->m_proxyModel->setFilterKeyColumn(-1);
0084     d->m_proxyModel->setSourceModel(d->m_model);
0085     d->mimeTypeTree->setModel(d->m_proxyModel);
0086 
0087     d->m_filterLineEdit = new QLineEdit(this);
0088     d->m_filterLineEdit->setPlaceholderText(tr("Search for file type or filename pattern...", "@info:placeholder"));
0089     QLabel *filterLabel = new QLabel(tr("&Filter:", "@label:textbox"));
0090     filterLabel->setBuddy(d->m_filterLineEdit);
0091     connect(d->m_filterLineEdit, &QLineEdit::textChanged, this, [this](const QString &text) {
0092         d->m_proxyModel->setFilterRegularExpression(
0093             QRegularExpression(text, QRegularExpression::CaseInsensitiveOption | QRegularExpression::UseUnicodePropertiesOption));
0094     });
0095 
0096     QHBoxLayout *filterLayout = new QHBoxLayout();
0097     filterLayout->addWidget(filterLabel);
0098     filterLayout->addWidget(d->m_filterLineEdit);
0099     vboxLayout->addLayout(filterLayout);
0100     d->m_filterLineEdit->setFocus();
0101 
0102     vboxLayout->addWidget(d->mimeTypeTree);
0103     QStringList headerLabels({tr("MIME Type", "@title:column")});
0104 
0105     if (visuals & Comments) {
0106         headerLabels.append(tr("Comment", "@title:column"));
0107     }
0108 
0109     if (visuals & Patterns) {
0110         headerLabels.append(tr("Patterns", "@title:column"));
0111     }
0112 
0113     d->m_model->setColumnCount(headerLabels.count());
0114     d->m_model->setHorizontalHeaderLabels(headerLabels);
0115     QFontMetrics fm(d->mimeTypeTree->fontMetrics());
0116     // big enough for most names/comments, but not for the insanely long ones
0117     const int optWidth = 20 * fm.averageCharWidth();
0118     d->mimeTypeTree->setColumnWidth(0, optWidth);
0119     d->mimeTypeTree->setColumnWidth(1, optWidth);
0120 
0121     d->loadMimeTypes(selMimeTypes);
0122 
0123     if (visuals & EditButton) {
0124         QHBoxLayout *buttonLayout = new QHBoxLayout();
0125         buttonLayout->addStretch(1);
0126         d->btnEditMimeType = new QPushButton(tr("&Edit...", "@action:button"), this);
0127         buttonLayout->addWidget(d->btnEditMimeType);
0128         d->btnEditMimeType->setEnabled(false);
0129 
0130         connect(d->btnEditMimeType, &QPushButton::clicked, this, [this]() {
0131             d->editMimeType();
0132         });
0133         connect(d->mimeTypeTree, &QAbstractItemView::doubleClicked, this, [this]() {
0134             d->editMimeType();
0135         });
0136 
0137         connect(d->mimeTypeTree, &QTreeView::activated, this, [this](const QModelIndex &index) {
0138             d->slotCurrentChanged(index);
0139         });
0140 
0141         d->btnEditMimeType->setToolTip(tr("Launch the MIME type editor", "@info:tooltip"));
0142 
0143         vboxLayout->addLayout(buttonLayout);
0144     }
0145 }
0146 
0147 KMimeTypeChooser::~KMimeTypeChooser() = default;
0148 
0149 void KMimeTypeChooserPrivate::loadMimeTypes(const QStringList &_selectedMimeTypes)
0150 {
0151     QStringList selMimeTypes;
0152 
0153     if (!_selectedMimeTypes.isEmpty()) {
0154         selMimeTypes = _selectedMimeTypes;
0155     } else {
0156         selMimeTypes = q->mimeTypes();
0157     }
0158 
0159     std::vector<QStandardItem *> parentGroups;
0160     QMimeDatabase db;
0161     const QList<QMimeType> mimetypes = db.allMimeTypes();
0162 
0163     bool agroupisopen = false;
0164     QStandardItem *idefault = nullptr; // open this, if all other fails
0165     QStandardItem *firstChecked = nullptr; // make this one visible after the loop
0166 
0167     for (const QMimeType &mt : mimetypes) {
0168         const QString mimetype = mt.name();
0169         const int index = mimetype.indexOf(QLatin1Char('/'));
0170         // e.g. "text", "audio", "inode"
0171         const QString maj = mimetype.left(index);
0172 
0173         if (!groups.isEmpty() && !groups.contains(maj)) {
0174             continue;
0175         }
0176 
0177         QStandardItem *groupItem;
0178 
0179         auto it = std::find_if(parentGroups.cbegin(), parentGroups.cend(), [maj](const QStandardItem *item) {
0180             return maj == item->text();
0181         });
0182 
0183         if (it == parentGroups.cend()) {
0184             groupItem = new QStandardItem(maj);
0185             groupItem->setFlags(Qt::ItemIsEnabled);
0186             // a dud item to fill the patterns column next to "groupItem" and setFlags() on it
0187             QStandardItem *secondColumn = new QStandardItem();
0188             secondColumn->setFlags(Qt::NoItemFlags);
0189             QStandardItem *thirdColumn = new QStandardItem();
0190             thirdColumn->setFlags(Qt::NoItemFlags);
0191             m_model->appendRow({groupItem, secondColumn, thirdColumn});
0192             parentGroups.push_back(groupItem);
0193             if (maj == defaultgroup) {
0194                 idefault = groupItem;
0195             }
0196         } else {
0197             groupItem = *it;
0198         }
0199 
0200         // e.g. "html", "plain", "mp4"
0201         const QString min = mimetype.mid(index + 1);
0202         QStandardItem *mime = new QStandardItem(QIcon::fromTheme(mt.iconName()), min);
0203         mime->setFlags(Qt::ItemIsSelectable | Qt::ItemIsUserCheckable | Qt::ItemIsEnabled);
0204 
0205         QStandardItem *comments = nullptr;
0206         if (visuals & KMimeTypeChooser::Comments) {
0207             comments = new QStandardItem(mt.comment());
0208             comments->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
0209         }
0210 
0211         QStandardItem *patterns = nullptr;
0212 
0213         if (visuals & KMimeTypeChooser::Patterns) {
0214             patterns = new QStandardItem(mt.globPatterns().join(QLatin1String("; ")));
0215             patterns->setFlags(Qt::ItemIsSelectable | Qt::ItemIsEnabled);
0216         }
0217 
0218         groupItem->appendRow(QList<QStandardItem *>({mime, comments, patterns}));
0219 
0220         if (selMimeTypes.contains(mimetype)) {
0221             mime->setCheckState(Qt::Checked);
0222             const QModelIndex index = m_proxyModel->mapFromSource(m_model->indexFromItem(groupItem));
0223             mimeTypeTree->expand(index);
0224             agroupisopen = true;
0225             if (!firstChecked) {
0226                 firstChecked = mime;
0227             }
0228         } else {
0229             mime->setCheckState(Qt::Unchecked);
0230         }
0231     }
0232 
0233     m_model->sort(0);
0234 
0235     if (firstChecked) {
0236         const QModelIndex index = m_proxyModel->mapFromSource(m_model->indexFromItem(firstChecked));
0237         mimeTypeTree->scrollTo(index);
0238     }
0239 
0240     if (!agroupisopen && idefault) {
0241         const QModelIndex index = m_proxyModel->mapFromSource(m_model->indexFromItem(idefault));
0242         mimeTypeTree->expand(index);
0243         mimeTypeTree->scrollTo(index);
0244     }
0245 }
0246 
0247 void KMimeTypeChooserPrivate::editMimeType()
0248 {
0249     QModelIndex mimeIndex = m_proxyModel->mapToSource(mimeTypeTree->currentIndex());
0250 
0251     // skip parent (non-leaf) nodes
0252     if (m_model->hasChildren(mimeIndex)) {
0253         return;
0254     }
0255 
0256     if (mimeIndex.column() > 0) { // we need the item from column 0 to concatenate "mt" below
0257         mimeIndex = m_model->sibling(mimeIndex.row(), 0, mimeIndex);
0258     }
0259 
0260     const QStandardItem *item = m_model->itemFromIndex(mimeIndex);
0261     const QString mt = (item->parent())->text() + QLatin1Char('/') + item->text();
0262     KMimeTypeEditor::editMimeType(mt, q);
0263 
0264     // TODO: use a QFileSystemWatcher on one of the shared-mime-info generated files, instead.
0265     // q->connect( KSycoca::self(), SIGNAL(databaseChanged(QStringList)),
0266     //            q, SLOT(slotSycocaDatabaseChanged(QStringList)) );
0267     // TODO: use QFileSystemWatcher to be told when keditfiletype changed a MIME type
0268     // or a better idea: a QMimeDatabaseWatcher class in Qt itself
0269 }
0270 
0271 void KMimeTypeChooserPrivate::slotCurrentChanged(const QModelIndex &index)
0272 {
0273     if (btnEditMimeType) {
0274         const QModelIndex srcIndex = m_proxyModel->mapToSource(index);
0275         const QStandardItem *currentItem = m_model->itemFromIndex(srcIndex);
0276         btnEditMimeType->setEnabled(currentItem && currentItem->parent());
0277     }
0278 }
0279 
0280 // TODO: see editMimeType
0281 void KMimeTypeChooserPrivate::slotSycocaDatabaseChanged(const QStringList &changedResources)
0282 {
0283     if (changedResources.contains(QLatin1String("xdgdata-mime"))) {
0284         loadMimeTypes();
0285     }
0286 }
0287 
0288 QVector<const QStandardItem *> KMimeTypeChooserPrivate::getCheckedItems()
0289 {
0290     QVector<const QStandardItem *> lst;
0291     const int rowCount = m_model->rowCount();
0292     for (int i = 0; i < rowCount; ++i) {
0293         const QStandardItem *groupItem = m_model->item(i);
0294         const int childCount = groupItem->rowCount();
0295         for (int j = 0; j < childCount; ++j) {
0296             const QStandardItem *child = groupItem->child(j);
0297             if (child->checkState() == Qt::Checked) {
0298                 lst.append(child);
0299             }
0300         }
0301     }
0302     return lst;
0303 }
0304 
0305 QStringList KMimeTypeChooser::mimeTypes() const
0306 {
0307     QStringList mimeList;
0308     const QVector<const QStandardItem *> checkedItems = d->getCheckedItems();
0309     mimeList.reserve(checkedItems.size());
0310     for (const QStandardItem *item : checkedItems) {
0311         mimeList.append(item->parent()->text() + QLatin1Char('/') + item->text());
0312     }
0313     return mimeList;
0314 }
0315 
0316 QStringList KMimeTypeChooser::patterns() const
0317 {
0318     QStringList patternList;
0319     const QVector<const QStandardItem *> checkedItems = d->getCheckedItems();
0320     QMimeDatabase db;
0321     for (const QStandardItem *item : checkedItems) {
0322         QMimeType mime = db.mimeTypeForName(item->parent()->text() + QLatin1Char('/') + item->text());
0323         Q_ASSERT(mime.isValid());
0324         patternList += mime.globPatterns();
0325     }
0326     return patternList;
0327 }
0328 // END
0329 
0330 // BEGIN KMimeTypeChooserDialogPrivate
0331 
0332 class KMimeTypeChooserDialogPrivate
0333 {
0334 public:
0335     KMimeTypeChooserDialogPrivate(KMimeTypeChooserDialog *parent)
0336         : q(parent)
0337     {
0338     }
0339 
0340     void init();
0341 
0342     KMimeTypeChooserDialog *q;
0343     KMimeTypeChooser *m_chooser;
0344 };
0345 
0346 // END
0347 
0348 // BEGIN KMimeTypeChooserDialog
0349 KMimeTypeChooserDialog::KMimeTypeChooserDialog(const QString &title,
0350                                                const QString &text,
0351                                                const QStringList &selMimeTypes,
0352                                                const QString &defaultGroup,
0353                                                const QStringList &groupsToShow,
0354                                                int visuals,
0355                                                QWidget *parent)
0356     : QDialog(parent)
0357     , d(new KMimeTypeChooserDialogPrivate(this))
0358 {
0359     setWindowTitle(title);
0360 
0361     d->m_chooser = new KMimeTypeChooser(text, selMimeTypes, defaultGroup, groupsToShow, visuals, this);
0362     d->init();
0363 }
0364 
0365 KMimeTypeChooserDialog::KMimeTypeChooserDialog(const QString &title,
0366                                                const QString &text,
0367                                                const QStringList &selMimeTypes,
0368                                                const QString &defaultGroup,
0369                                                QWidget *parent)
0370     : QDialog(parent)
0371     , d(new KMimeTypeChooserDialogPrivate(this))
0372 {
0373     setWindowTitle(title);
0374 
0375     d->m_chooser = new KMimeTypeChooser(text,
0376                                         selMimeTypes,
0377                                         defaultGroup,
0378                                         QStringList(),
0379                                         KMimeTypeChooser::Comments | KMimeTypeChooser::Patterns | KMimeTypeChooser::EditButton,
0380                                         this);
0381     d->init();
0382 }
0383 
0384 KMimeTypeChooser *KMimeTypeChooserDialog::chooser()
0385 {
0386     return d->m_chooser;
0387 }
0388 
0389 void KMimeTypeChooserDialogPrivate::init()
0390 {
0391     QVBoxLayout *layout = new QVBoxLayout(q);
0392 
0393     layout->addWidget(m_chooser);
0394 
0395     QDialogButtonBox *buttonBox = new QDialogButtonBox(q);
0396     buttonBox->setStandardButtons(QDialogButtonBox::Ok | QDialogButtonBox::Cancel);
0397     QObject::connect(buttonBox, &QDialogButtonBox::accepted, q, &QDialog::accept);
0398     QObject::connect(buttonBox, &QDialogButtonBox::rejected, q, &QDialog::reject);
0399     layout->addWidget(buttonBox);
0400 }
0401 
0402 KMimeTypeChooserDialog::~KMimeTypeChooserDialog() = default;
0403 
0404 QSize KMimeTypeChooserDialog::sizeHint() const
0405 {
0406     QFontMetrics fm(fontMetrics());
0407     const int viewableSize = fm.averageCharWidth() * 60;
0408     return QSize(viewableSize, viewableSize);
0409 }
0410 
0411 // END KMimeTypeChooserDialog
0412 
0413 #include "moc_kmimetypechooser.cpp"