diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,6 +1,7 @@ set(KParts_LIB_SRCS partbase.cpp part.cpp + partloader.cpp openurlarguments.cpp readonlypart.cpp readwritepart.cpp @@ -56,6 +57,7 @@ Part PartActivateEvent PartBase + PartLoader PartManager PartSelectEvent Plugin diff --git a/src/partloader.h b/src/partloader.h new file mode 100644 --- /dev/null +++ b/src/partloader.h @@ -0,0 +1,108 @@ +/* This file is part of the KDE project + Copyright (C) 2020 David Faure + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#ifndef KPARTS_PARTLOADER_H +#define KPARTS_PARTLOADER_H + +#include +#include +#include +#include + +namespace KParts +{ + +/** + * Helper methods for locating and loading parts. + * This is based upon KPluginLoader and KPluginFactory, but it takes + * care of querying by mimetype, sorting the available parts by builtin + * preference and by user preference. + * @since 5.69 + */ +namespace PartLoader +{ + namespace Private + { + /** + * Helper for PartLoader::createPartInstanceForMimeType + * @internal + */ + KPARTS_EXPORT QObject *createPartInstanceForMimeTypeHelper(const char *iface, const QString &mimeType, QWidget *parentWidget, QObject *parent, QString *error); + } + + /** + * Locate all available KParts for a mimetype. + * @return a list of plugin metadata, sorted by preference. + * This takes care both of the builtin preference (set by developers) + * and of user preference (stored in mimeapps.list). + * + * This uses KPluginLoader::findPlugins, i.e. it requires the parts to + * provide the metadata as JSON embedded into the plugin. + * Until KF6, however, it also supports .desktop files as a fallback solution. + * + * To load a part from one of the KPluginMetaData instances returned here, + * simply do + * @code + * KPluginLoader loader(metaData.fileName()); + * m_part = loader.factory()->create(parentWidget, parent); + * @endcode + * + * @since 5.69 + */ + KPARTS_EXPORT QVector partsForMimeType(const QString &mimeType); + + /** + * Use this method to create a KParts part. It will try to create an object which inherits + * \p T. + * + * Example: + * \code + * m_part = KParts::PartLoader::createPartInstanceForMimeType( + * mimeType, this, this); + * if (m_part) { + * layout->addWidget(m_part->widget()); // Integrate the widget + * createGUI(m_part); // Integrate the actions + * m_part->openUrl(url); + * } + * \endcode + * + * \tparam T The interface for which an object should be created. The object will inherit \p T. + * \param mimeType The mimetype for which we need a KParts. + * \param parentWidget The parent widget for the part's widget. + * \param parent The parent of the part. + * \param error Optional output parameter, it will be set to the error string, if any. + * \returns A pointer to the created object is returned, or @c nullptr if an error occurred. + * @since 5.69 + */ + template + static T *createPartInstanceForMimeType(const QString &mimeType, QWidget *parentWidget = nullptr, QObject *parent = nullptr, + QString *error = nullptr) + { + QObject *o = Private::createPartInstanceForMimeTypeHelper(T::staticMetaObject.className(), mimeType, parentWidget, parent, error); + T *part = qobject_cast(o); + if (!part) { + delete o; + } + return part; + } + +} // namespace +} // namespace + +#endif diff --git a/src/partloader.cpp b/src/partloader.cpp new file mode 100644 --- /dev/null +++ b/src/partloader.cpp @@ -0,0 +1,195 @@ +/* This file is part of the KDE project + Copyright (C) 2020 David Faure + + This library is free software; you can redistribute it and/or + modify it under the terms of the GNU Library General Public + License as published by the Free Software Foundation; either + version 2 of the License, or (at your option) any later version. + + This library is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU + Library General Public License for more details. + + You should have received a copy of the GNU Library General Public License + along with this library; see the file COPYING.LIB. If not, write to + the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, + Boston, MA 02110-1301, USA. +*/ + +#include "partloader.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +#include // TODO KF6 REMOVE +#if KPARTS_VERSION <= QT_VERSION_CHECK(5, 900, 0) +#include +#include +#include +#include +#endif + +// We still use desktop files for translated descriptions in keditfiletype, +// and desktop file names then end up in mimeapps.list. +// Alternatively, that KCM could be ported to read the descriptions from the JSON metadata? +// KF6 TODO: at least make the KCM write out library names (into a different config file) +// so we don't need to do the lookup here every time. +static QString pluginForDesktopFile(const QString &desktopFile) +{ + KService::Ptr service = KService::serviceByStorageId(desktopFile); + if (!service) { + qDebug() << "mimeapps.list specifies unknown service" << desktopFile; + return {}; + } + return service->library(); +} + +static QStringList partsFromUserPreference(const QString &mimeType) +{ + auto config = KSharedConfig::openConfig(QStringLiteral("mimeapps.list")); + const QStringList desktopFiles = config->group(QStringLiteral("Added KDE Service Associations")).readXdgListEntry(mimeType); + QStringList parts; + parts.reserve(desktopFiles.count()); + for (const QString &desktopFile : desktopFiles) { + const QString part = pluginForDesktopFile(desktopFile); + if (!part.isEmpty()) { + parts.append(part); + } + } + return parts; +} + +// A plugin can support N mimetypes. Pick the one that is closest to @parent in the inheritance tree +// and return how far it is from that parent (0 = same mimetype, 1 = direct child, etc.) +static int pluginDistanceToMimeType(const KPluginMetaData &md, const QString &parent) +{ + QMimeDatabase db; + auto distanceToMimeType = [&](const QString &mime) { + if (mime == parent) + return 0; + const QStringList ancestors = db.mimeTypeForName(mime).allAncestors(); + const int dist = ancestors.indexOf(parent); + return dist == -1 ? 50 : dist + 1; + }; + const QStringList mimes = md.mimeTypes(); + int minDistance = 50; + for (const QString &mime : mimes) { + minDistance = std::min(minDistance, distanceToMimeType(mime)); + } + return minDistance; +} + +QVector KParts::PartLoader::partsForMimeType(const QString &mimeType) +{ + auto supportsMime = [&](const KPluginMetaData &md) { return md.supportsMimeType(mimeType); }; + QVector plugins = KPluginLoader::findPlugins(QStringLiteral("kf5/parts"), supportsMime); + +#if KPARTS_VERSION <= QT_VERSION_CHECK(5, 900, 0) + // KF5 compat code + + // I would compare library filenames, but KPluginMetaData::fileName looks like kf5/kparts/okteta and KService::library() is a full path + // The user actually sees translated names, let's ensure those don't look duplicated in the list. + auto isPluginForName = [](const QString &name) { + return [name](const KPluginMetaData &plugin) { return plugin.name() == name; }; + }; + + const KService::List offers = KMimeTypeTrader::self()->query(mimeType, QStringLiteral("KParts/ReadOnlyPart")); + for (const KService::Ptr &service : offers) { + KPluginInfo info(service); + if (info.isValid()) { + if (std::find_if(plugins.cbegin(), plugins.cend(), isPluginForName(info.name())) == plugins.cend()) { + plugins.append(info.toMetaData()); + } + } + } +#endif + + auto orderPredicate = [&](const KPluginMetaData &left, const KPluginMetaData &right) { + // We filtered based on "supports mimetype", but this didn't order from most-specific to least-specific. + const int leftDistance = pluginDistanceToMimeType(left, mimeType); + const int rightDistance = pluginDistanceToMimeType(right, mimeType); + if (leftDistance < rightDistance) + return true; + if (leftDistance > rightDistance) + return false; + // Plugins who support the same mimetype are then sorted by initial preference + return left.initialPreference() > right.initialPreference(); + }; + std::sort(plugins.begin(), plugins.end(), orderPredicate); + + const QStringList userParts = partsFromUserPreference(mimeType); + if (!userParts.isEmpty()) { + //for (const KPluginMetaData &plugin : plugins) { + // qDebug() << "unsorted:" << plugin.fileName() << plugin.initialPreference(); + //} + const auto defaultPlugins = plugins; + plugins.clear(); + for (const QString &userPart : userParts) { // e.g. kf5/parts/gvpart + auto matchesLibrary = [&](const KPluginMetaData &plugin) { return plugin.fileName().contains(userPart); }; + auto it = std::find_if(defaultPlugins.begin(), defaultPlugins.end(), matchesLibrary); + if (it != defaultPlugins.end()) { + plugins.push_back(*it); + } else { + qDebug() << "Part not found" << userPart; + } + } + // In case mimeapps.list lists "nothing good", append the default set to the end as fallback + plugins += defaultPlugins; + } + + //for (const KPluginMetaData &plugin : plugins) { + // qDebug() << plugin.fileName() << plugin.initialPreference(); + //} + return plugins; +} + +// KF6 TODO: make create(const char* iface...) public in KPluginFactory, remove this hack +class KPluginFactoryHack : public KPluginFactory +{ +public: + QObject *create(const char *iface, QWidget *parentWidget, QObject *parent, const QVariantList &args, const QString &keyword) override { + return KPluginFactory::create(iface, parentWidget, parent, args, keyword); + } +}; + +QObject *KParts::PartLoader::Private::createPartInstanceForMimeTypeHelper(const char *iface, const QString &mimeType, QWidget *parentWidget, QObject *parent, QString *error) +{ + const QVector plugins = KParts::PartLoader::partsForMimeType(mimeType); + for (const KPluginMetaData &plugin : plugins) { + KPluginLoader pluginLoader(plugin.fileName()); + // ## How can the keyword feature work with JSON metadata? + // ## Several desktop files could point to the same .so file, but a .so file only has one metadata builtin. + // ## Unlikely to be a problem here (multiple KParts in the same .so file?), but to be solved for KCMs with an array of "KPlugin" in the JSON file... + // ## It would then be used by KPluginLoader::findPlugins, and we'd use plugin.keyword() here. + const QString pluginKeyword; + KPluginFactory *factory = pluginLoader.factory(); + if (factory) { + QObject *obj = static_cast(factory)->create(iface, parentWidget, parent, QVariantList(), pluginKeyword); + if (error) { + if (!obj) { + *error = i18n("The plugin '%1' does not provide an interface '%2' with keyword '%3'", + plugin.fileName(), QString::fromLatin1(iface), pluginKeyword); + } else { + error->clear(); + } + } + if (obj) { + return obj; + } + } else if (error) { + *error = pluginLoader.errorString(); + } + pluginLoader.unload(); + } + if (error) { + *error = i18n("No part was found for mimeType %1", mimeType); + } + return nullptr; +} diff --git a/tests/partviewer.cpp b/tests/partviewer.cpp --- a/tests/partviewer.cpp +++ b/tests/partviewer.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include @@ -60,9 +61,8 @@ delete m_part; QMimeDatabase db; const QString mimeType = db.mimeTypeForUrl(url).name(); - m_part = KMimeTypeTrader::self()->createPartInstanceFromQuery(mimeType, - this, - this); + m_part = KParts::PartLoader::createPartInstanceForMimeType(mimeType, + this, this); if (m_part) {