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"