File indexing completed on 2024-04-28 05:27:03
0001 /* This file is part of the KDE project 0002 SPDX-FileCopyrightText: 2000-2008 David Faure <faure@kde.org> 0003 SPDX-FileCopyrightText: 2008 Urs Wolfer <uwolfer @ kde.org> 0004 SPDX-FileCopyrightText: 2022 Marco Rebhan <me@dblsaiko.net> 0005 0006 SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL 0007 */ 0008 0009 // Own 0010 #include "filetypesview.h" 0011 #include "mimetypewriter.h" 0012 0013 // Qt 0014 #include <QDBusConnection> 0015 #include <QDBusMessage> 0016 #include <QDebug> 0017 #include <QHBoxLayout> 0018 #include <QLabel> 0019 #include <QMimeDatabase> 0020 #include <QPushButton> 0021 #include <QStackedWidget> 0022 #include <QStandardPaths> 0023 #include <QVBoxLayout> 0024 0025 // KDE 0026 #include <KBuildSycocaProgressDialog> 0027 #include <KLineEdit> 0028 #include <KLocalizedString> 0029 #include <KPluginFactory> 0030 #include <KSycoca> 0031 0032 // Local 0033 #include "filegroupdetails.h" 0034 #include "filetypedetails.h" 0035 #include "kservicelistwidget.h" 0036 #include "multiapplydialog.h" 0037 #include "newtypedlg.h" 0038 #include "typeslisttreewidget.h" 0039 0040 K_PLUGIN_CLASS_WITH_JSON(FileTypesView, "kcm_filetypes.json") 0041 0042 FileTypesView::FileTypesView(QObject *parent, const KPluginMetaData &data) 0043 : KCModule(parent, data) 0044 { 0045 m_fileTypesConfig = KSharedConfig::openConfig(QStringLiteral("filetypesrc"), KConfig::NoGlobals); 0046 0047 setButtons(Help | Apply); 0048 QString wtstr; 0049 0050 QHBoxLayout *l = new QHBoxLayout(widget()); 0051 QVBoxLayout *leftLayout = new QVBoxLayout(); 0052 l->addLayout(leftLayout); 0053 0054 patternFilterLE = new KLineEdit(widget()); 0055 patternFilterLE->setClearButtonEnabled(true); 0056 patternFilterLE->setTrapReturnKey(true); 0057 patternFilterLE->setPlaceholderText(i18n("Search for file type or filename pattern...")); 0058 leftLayout->addWidget(patternFilterLE); 0059 0060 connect(patternFilterLE, &QLineEdit::textChanged, this, &FileTypesView::slotFilter); 0061 0062 wtstr = i18n( 0063 "Enter a part of a filename pattern, and only file types with a " 0064 "matching file pattern will appear in the list. Alternatively, enter " 0065 "a part of a file type name as it appears in the list."); 0066 0067 patternFilterLE->setWhatsThis(wtstr); 0068 0069 typesLV = new TypesListTreeWidget(widget()); 0070 0071 typesLV->setHeaderLabel(i18n("Known Types")); 0072 leftLayout->addWidget(typesLV); 0073 connect(typesLV, &QTreeWidget::currentItemChanged, this, &FileTypesView::updateDisplay); 0074 connect(typesLV, &QTreeWidget::itemDoubleClicked, this, &FileTypesView::slotDoubleClicked); 0075 0076 typesLV->setWhatsThis( 0077 i18n("Here you can see a hierarchical list of" 0078 " the file types which are known on your system. Click on the '+' sign" 0079 " to expand a category, or the '-' sign to collapse it. Select a file type" 0080 " (e.g. text/html for HTML files) to view/edit the information for that" 0081 " file type using the controls on the right.")); 0082 0083 QHBoxLayout *btnsLay = new QHBoxLayout(); 0084 leftLayout->addLayout(btnsLay); 0085 btnsLay->addStretch(1); 0086 QPushButton *addTypeB = new QPushButton(i18n("Add..."), widget()); 0087 addTypeB->setIcon(QIcon::fromTheme(QStringLiteral("list-add"))); 0088 connect(addTypeB, &QAbstractButton::clicked, this, &FileTypesView::addType); 0089 btnsLay->addWidget(addTypeB); 0090 0091 addTypeB->setWhatsThis(i18n("Click here to add a new file type.")); 0092 0093 m_removeTypeB = new QPushButton(i18n("&Remove"), widget()); 0094 m_removeTypeB->setIcon(QIcon::fromTheme(QStringLiteral("list-remove"))); 0095 connect(m_removeTypeB, &QAbstractButton::clicked, this, &FileTypesView::removeType); 0096 btnsLay->addWidget(m_removeTypeB); 0097 m_removeTypeB->setEnabled(false); 0098 m_removeButtonSaysRevert = false; 0099 0100 // For the right panel, prepare a widget stack 0101 m_widgetStack = new QStackedWidget(widget()); 0102 0103 l->addWidget(m_widgetStack); 0104 0105 // File Type Details 0106 m_details = new FileTypeDetails(m_widgetStack); 0107 m_details->allowMultiApply(true); 0108 connect(m_details, &FileTypeDetails::changed, this, &FileTypesView::setDirty); 0109 connect(m_details, &FileTypeDetails::embedMajor, this, &FileTypesView::slotEmbedMajor); 0110 connect(m_details, &FileTypeDetails::multiApply, this, &FileTypesView::multiApply); 0111 m_widgetStack->insertWidget(1, m_details /*id*/); 0112 0113 // File Group Details 0114 m_groupDetails = new FileGroupDetails(m_widgetStack); 0115 connect(m_groupDetails, &FileGroupDetails::changed, this, &FileTypesView::setDirty); 0116 m_widgetStack->insertWidget(2, m_groupDetails /*id*/); 0117 0118 // Widget shown on startup 0119 m_emptyWidget = new QLabel(i18n("Select a file type by name or by extension"), m_widgetStack); 0120 m_emptyWidget->setAlignment(Qt::AlignCenter); 0121 m_widgetStack->insertWidget(3, m_emptyWidget); 0122 0123 m_widgetStack->setCurrentWidget(m_emptyWidget); 0124 0125 connect(KSycoca::self(), qOverload<>(&KSycoca::databaseChanged), this, &FileTypesView::slotDatabaseChanged); 0126 } 0127 0128 FileTypesView::~FileTypesView() 0129 { 0130 } 0131 0132 void FileTypesView::setDirty(bool state) 0133 { 0134 setNeedsSave(state); 0135 m_dirty = state; 0136 } 0137 0138 // To order the mimetype list 0139 static bool mimeTypeLessThan(const QMimeType &m1, const QMimeType &m2) 0140 { 0141 return m1.name() < m2.name(); 0142 } 0143 0144 // Note that this method loses any newly-added (and not saved yet) mimetypes. 0145 // So this is really only for load(). 0146 void FileTypesView::readFileTypes() 0147 { 0148 typesLV->clear(); 0149 m_majorMap.clear(); 0150 m_itemList.clear(); 0151 0152 QMimeDatabase db; 0153 QList<QMimeType> mimetypes = db.allMimeTypes(); 0154 std::sort(mimetypes.begin(), mimetypes.end(), mimeTypeLessThan); 0155 auto it2(mimetypes.constBegin()); 0156 for (; it2 != mimetypes.constEnd(); ++it2) { 0157 const QString mimetype = (*it2).name(); 0158 const int index = mimetype.indexOf(QLatin1Char('/')); 0159 const QString maj = mimetype.left(index); 0160 0161 TypesListItem *groupItem = m_majorMap.value(maj); 0162 if (!groupItem) { 0163 groupItem = new TypesListItem(typesLV, maj); 0164 m_majorMap.insert(maj, groupItem); 0165 } 0166 0167 TypesListItem *item = new TypesListItem(groupItem, (*it2)); 0168 m_itemList.append(item); 0169 } 0170 updateDisplay(nullptr); 0171 } 0172 0173 void FileTypesView::slotEmbedMajor(const QString &major, bool &embed) 0174 { 0175 TypesListItem *groupItem = m_majorMap.value(major); 0176 if (!groupItem) { 0177 return; 0178 } 0179 0180 embed = (groupItem->mimeTypeData().autoEmbed() == MimeTypeData::Yes); 0181 } 0182 0183 void FileTypesView::multiApply(int type) 0184 { 0185 auto *current = static_cast<TypesListItem *>(typesLV->currentItem()); 0186 MultiApplyDialog d(current, m_itemList, widget()); 0187 0188 if (d.exec()) { 0189 bool changed = false; 0190 0191 for (auto *item : d.selected()) { 0192 if (type == KServiceListWidget::SERVICELIST_APPLICATIONS) { 0193 if (item->mimeTypeData().appServices() != current->mimeTypeData().appServices()) { 0194 item->mimeTypeData().setAppServices(current->mimeTypeData().appServices()); 0195 changed = true; 0196 } 0197 } else if (type == KServiceListWidget::SERVICELIST_SERVICES) { 0198 if (item->mimeTypeData().embedParts() != current->mimeTypeData().embedParts()) { 0199 item->mimeTypeData().setEmbedParts(current->mimeTypeData().embedParts()); 0200 changed = true; 0201 } 0202 } 0203 } 0204 0205 if (changed) { 0206 setDirty(true); 0207 } 0208 } 0209 } 0210 0211 void FileTypesView::slotFilter(const QString &patternFilter) 0212 { 0213 for (int i = 0; i < typesLV->topLevelItemCount(); ++i) { 0214 typesLV->topLevelItem(i)->setHidden(true); 0215 } 0216 0217 // insert all items and their group that match the filter 0218 for (TypesListItem *it : std::as_const(m_itemList)) { 0219 const MimeTypeData &mimeTypeData = it->mimeTypeData(); 0220 if (patternFilter.isEmpty() || mimeTypeData.matchesFilter(patternFilter)) { 0221 TypesListItem *group = m_majorMap.value(mimeTypeData.majorType()); 0222 Q_ASSERT(group); 0223 if (group) { 0224 group->setHidden(false); 0225 it->setHidden(false); 0226 } 0227 } else { 0228 it->setHidden(true); 0229 } 0230 } 0231 } 0232 0233 void FileTypesView::addType() 0234 { 0235 const QStringList allGroups = m_majorMap.keys(); 0236 0237 NewTypeDialog dialog(allGroups, widget()); 0238 0239 if (dialog.exec()) { 0240 const QString newMimeType = dialog.group() + QLatin1Char('/') + dialog.text(); 0241 0242 TypesListItem *group = m_majorMap.value(dialog.group()); 0243 if (!group) { 0244 group = new TypesListItem(typesLV, dialog.group()); 0245 m_majorMap.insert(dialog.group(), group); 0246 } 0247 0248 // find out if our group has been filtered out -> insert if necessary 0249 QTreeWidgetItem *item = typesLV->topLevelItem(0); 0250 bool insert = true; 0251 while (item) { 0252 if (item == group) { 0253 insert = false; 0254 break; 0255 } 0256 item = typesLV->itemBelow(item); 0257 } 0258 if (insert) { 0259 typesLV->addTopLevelItem(group); 0260 } 0261 0262 TypesListItem *tli = new TypesListItem(group, newMimeType); 0263 m_itemList.append(tli); 0264 0265 group->setExpanded(true); 0266 tli->setSelected(true); 0267 0268 setDirty(true); 0269 } 0270 } 0271 0272 void FileTypesView::removeType() 0273 { 0274 TypesListItem *current = static_cast<TypesListItem *>(typesLV->currentItem()); 0275 0276 if (!current) { 0277 return; 0278 } 0279 0280 const MimeTypeData &mimeTypeData = current->mimeTypeData(); 0281 0282 // Can't delete groups nor essential mimetypes (but the button should be 0283 // disabled already in these cases, so this is just extra safety). 0284 if (mimeTypeData.isMeta() || mimeTypeData.isEssential()) { 0285 return; 0286 } 0287 0288 if (!mimeTypeData.isNew()) { 0289 removedList.append(mimeTypeData.name()); 0290 } 0291 if (m_removeButtonSaysRevert) { 0292 // Nothing else to do for now, until saving 0293 updateDisplay(current); 0294 } else { 0295 QTreeWidgetItem *li = typesLV->itemAbove(current); 0296 if (!li) { 0297 li = typesLV->itemBelow(current); 0298 } 0299 if (!li) { 0300 li = current->parent(); 0301 } 0302 0303 current->parent()->takeChild(current->parent()->indexOfChild(current)); 0304 m_itemList.removeAll(current); 0305 if (li) { 0306 li->setSelected(true); 0307 } 0308 } 0309 setDirty(true); 0310 } 0311 0312 void FileTypesView::slotDoubleClicked(QTreeWidgetItem *item) 0313 { 0314 if (!item) { 0315 return; 0316 } 0317 item->setExpanded(!item->isExpanded()); 0318 } 0319 0320 void FileTypesView::updateDisplay(QTreeWidgetItem *item) 0321 { 0322 TypesListItem *tlitem = static_cast<TypesListItem *>(item); 0323 updateRemoveButton(tlitem); 0324 0325 if (!item) { 0326 m_widgetStack->setCurrentWidget(m_emptyWidget); 0327 return; 0328 } 0329 0330 const bool wasDirty = m_dirty; 0331 0332 MimeTypeData &mimeTypeData = tlitem->mimeTypeData(); 0333 0334 if (mimeTypeData.isMeta()) { // is a group 0335 m_widgetStack->setCurrentWidget(m_groupDetails); 0336 m_groupDetails->setMimeTypeData(&mimeTypeData); 0337 } else { 0338 m_widgetStack->setCurrentWidget(m_details); 0339 m_details->setMimeTypeData(&mimeTypeData); 0340 } 0341 0342 // Updating the display indirectly called change(true) 0343 if (!wasDirty) { 0344 setDirty(false); 0345 } 0346 } 0347 0348 void FileTypesView::updateRemoveButton(TypesListItem *tlitem) 0349 { 0350 bool canRemove = false; 0351 m_removeButtonSaysRevert = false; 0352 0353 if (tlitem) { 0354 const MimeTypeData &mimeTypeData = tlitem->mimeTypeData(); 0355 if (!mimeTypeData.isMeta() && !mimeTypeData.isEssential()) { 0356 if (mimeTypeData.isNew()) { 0357 canRemove = true; 0358 } else { 0359 // We can only remove mimetypes that we defined ourselves, not those from freedesktop.org 0360 const QString mimeType = mimeTypeData.name(); 0361 qDebug() << mimeType << "hasDefinitionFile:" << MimeTypeWriter::hasDefinitionFile(mimeType); 0362 if (MimeTypeWriter::hasDefinitionFile(mimeType)) { 0363 canRemove = true; 0364 0365 // Is there a global definition for it? 0366 const QStringList mimeFiles = QStandardPaths::locateAll(QStandardPaths::GenericDataLocation, // 0367 QLatin1String("mime/") + mimeType + QStringLiteral(".xml")); 0368 qDebug() << mimeFiles; 0369 if (mimeFiles.count() >= 2 /*a local and a global*/) { 0370 m_removeButtonSaysRevert = true; 0371 qDebug() << removedList; 0372 if (removedList.contains(mimeType)) { 0373 canRemove = false; // already on the "to be reverted" list, user needs to save now 0374 } 0375 } 0376 } 0377 } 0378 } 0379 } 0380 0381 if (m_removeButtonSaysRevert) { 0382 m_removeTypeB->setText(i18n("&Revert")); 0383 m_removeTypeB->setToolTip(i18n("Revert this file type to its initial system-wide definition")); 0384 m_removeTypeB->setWhatsThis( 0385 i18n("Click here to revert this file type to its initial system-wide definition, which undoes any changes made to the file type. Note that " 0386 "system-wide file types cannot be deleted. You can however empty their pattern list, to " 0387 "minimize the chances of them being used (but the file type determination from file contents can still end up using them).")); 0388 } else { 0389 m_removeTypeB->setText(i18n("&Remove")); 0390 m_removeTypeB->setToolTip(i18n("Delete this file type definition completely")); 0391 m_removeTypeB->setWhatsThis( 0392 i18n("Click here to delete this file type definition completely. This is only possible for user-defined file types. System-wide file types cannot " 0393 "be deleted. You can however empty their pattern list, to minimize the chances of " 0394 "them being used (but the file type determination from file contents can still end up using them).")); 0395 } 0396 0397 m_removeTypeB->setEnabled(canRemove); 0398 } 0399 0400 void FileTypesView::save() 0401 { 0402 bool needUpdateMimeDb = false; 0403 bool needUpdateSycoca = false; 0404 bool didIt = false; 0405 // first, remove those items which we are asked to remove. 0406 for (const QString &mime : std::as_const(removedList)) { 0407 MimeTypeWriter::removeOwnMimeType(mime); 0408 didIt = true; 0409 needUpdateMimeDb = true; 0410 needUpdateSycoca = true; // remove offers for this mimetype 0411 } 0412 removedList.clear(); 0413 0414 // now go through all entries and sync those which are dirty. 0415 // don't use typesLV, it may be filtered 0416 for (auto it = m_majorMap.cbegin(); it != m_majorMap.cend(); ++it) { 0417 TypesListItem *tli = it.value(); 0418 if (tli->mimeTypeData().isDirty()) { 0419 qDebug() << "Entry " << tli->name() << " is dirty. Saving."; 0420 if (tli->mimeTypeData().sync()) { 0421 needUpdateMimeDb = true; 0422 } 0423 didIt = true; 0424 } 0425 } 0426 0427 for (TypesListItem *tli : std::as_const(m_itemList)) { 0428 if (tli->mimeTypeData().isDirty()) { 0429 if (tli->mimeTypeData().isServiceListDirty()) { 0430 needUpdateSycoca = true; 0431 } 0432 qDebug() << "Entry " << tli->name() << " is dirty. Saving."; 0433 if (tli->mimeTypeData().sync()) { 0434 needUpdateMimeDb = true; 0435 } 0436 didIt = true; 0437 } 0438 } 0439 0440 m_fileTypesConfig->sync(); 0441 0442 setDirty(false); 0443 0444 if (needUpdateMimeDb) { 0445 MimeTypeWriter::runUpdateMimeDatabase(); 0446 } 0447 if (needUpdateSycoca) { 0448 KBuildSycocaProgressDialog::rebuildKSycoca(widget()); 0449 } 0450 0451 if (didIt) { // TODO make more specific: only if autoEmbed changed? Well, maybe this is useful for icon and glob changes too... 0452 // Trigger reparseConfiguration of filetypesrc in konqueror 0453 // TODO: the same for dolphin. Or we should probably define a global signal for this. 0454 // Or a KGlobalSettings thing. 0455 QDBusMessage message = QDBusMessage::createSignal(QStringLiteral("/KonqMain"), // 0456 QStringLiteral("org.kde.Konqueror.Main"), 0457 QStringLiteral("reparseConfiguration")); 0458 QDBusConnection::sessionBus().send(message); 0459 } 0460 0461 updateDisplay(typesLV->currentItem()); 0462 } 0463 0464 void FileTypesView::load() 0465 { 0466 widget()->setEnabled(false); 0467 widget()->setCursor(Qt::WaitCursor); 0468 0469 readFileTypes(); 0470 0471 widget()->unsetCursor(); 0472 setDirty(false); 0473 widget()->setEnabled(true); 0474 } 0475 0476 void FileTypesView::slotDatabaseChanged() 0477 { 0478 m_details->refresh(); 0479 0480 // ksycoca has new KMimeTypes objects for us, make sure to update 0481 // our 'copies' to be in sync with it. Not important for OK, but 0482 // important for Apply (how to differentiate those 2?). 0483 // See BR 35071. 0484 0485 for (TypesListItem *tli : std::as_const(m_itemList)) { 0486 tli->mimeTypeData().refresh(); 0487 } 0488 } 0489 0490 void FileTypesView::defaults() 0491 { 0492 } 0493 0494 #include "filetypesview.moc" 0495 0496 #include "moc_filetypesview.cpp"