File indexing completed on 2025-02-02 14:20:05
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"