File indexing completed on 2024-04-28 16:44:26

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