diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index efeea09..f3ba931 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -1,152 +1,154 @@ set(KParts_LIB_SRCS partbase.cpp part.cpp + partloader.cpp openurlarguments.cpp readonlypart.cpp readwritepart.cpp plugin.cpp partmanager.cpp mainwindow.cpp event.cpp guiactivateevent.cpp partactivateevent.cpp partselectevent.cpp browserextension.cpp browserhostextension.cpp browserarguments.cpp liveconnectextension.cpp openurlevent.cpp windowargs.cpp historyprovider.cpp browserinterface.cpp browserrun.cpp browseropenorsavequestion.cpp statusbarextension.cpp scriptableextension.cpp textextension.cpp htmlextension.cpp htmlsettingsinterface.cpp selectorinterface.cpp fileinfoextension.cpp listingfilterextension.cpp listingnotificationextension.cpp ) include(ECMGenerateHeaders) ecm_generate_headers(KParts_CamelCase_HEADERS HEADER_NAMES BrowserArguments BrowserExtension BrowserHostExtension BrowserInterface BrowserOpenOrSaveQuestion BrowserRun Event FileInfoExtension GUIActivateEvent HistoryProvider HtmlExtension HtmlSettingsInterface ListingFilterExtension ListingNotificationExtension LiveConnectExtension MainWindow OpenUrlArguments OpenUrlEvent Part PartActivateEvent PartBase + PartLoader PartManager PartSelectEvent Plugin ReadOnlyPart ReadWritePart ScriptableExtension SelectorInterface StatusBarExtension TextExtension WindowArgs REQUIRED_HEADERS KParts_HEADERS PREFIX KParts ) install(FILES ${KParts_CamelCase_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KParts/KParts COMPONENT Devel) add_library(KF5Parts ${KParts_LIB_SRCS}) add_library(KF5::Parts ALIAS KF5Parts) ecm_generate_export_header(KF5Parts EXPORT_FILE_NAME ${KParts_BINARY_DIR}/kparts/kparts_export.h BASE_NAME KParts GROUP_BASE_NAME KF VERSION ${KF5_VERSION} DEPRECATED_BASE_VERSION 0 DEPRECATION_VERSIONS 3.0 4.4 5.0 EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT} ) set(KParts_BUILD_INCLUDE_DIRS ${KParts_BINARY_DIR} ${CMAKE_CURRENT_BINARY_DIR}) target_include_directories(KF5Parts PUBLIC "$") target_include_directories(KF5Parts INTERFACE "$" ) target_link_libraries(KF5Parts PUBLIC KF5::KIOWidgets #browserrun.h uses krun.h KF5::XmlGui # essential to the technology KF5::TextWidgets # needed for KFind, as interface PRIVATE KF5::I18n #few uses of i18n and i18nc, can be probably stripped down KF5::IconThemes #only used by KPart::iconLoader() ) set_target_properties(KF5Parts PROPERTIES VERSION ${KPARTS_VERSION_STRING} SOVERSION ${KPARTS_SOVERSION} EXPORT_NAME Parts ) install(TARGETS KF5Parts EXPORT KF5PartsTargets ${KF5_INSTALL_TARGETS_DEFAULT_ARGS}) install(FILES kpart.desktop krop.desktop krwp.desktop browserview.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR} ) install(FILES ${KParts_BINARY_DIR}/kparts/kparts_export.h ${KParts_HEADERS} DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KParts/kparts COMPONENT Devel ) install(FILES kde_terminal_interface.h DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF5}/KParts ) if(BUILD_QCH) ecm_add_qch( KF5Parts_QCH NAME KParts BASE_NAME KF5Parts VERSION ${KF5_VERSION} ORG_DOMAIN org.kde SOURCES # using only public headers, to cover only public API ${KParts_HEADERS} kde_terminal_interface.h MD_MAINPAGE "${CMAKE_SOURCE_DIR}/README.md" LINK_QCHS KF5KIO_QCH KF5XmlGui_QCH KF5TextWidgets_QCH INCLUDE_DIRS ${KParts_BUILD_INCLUDE_DIRS} BLANK_MACROS KPARTS_EXPORT KPARTS_DEPRECATED KPARTS_DEPRECATED_EXPORT "KPARTS_DEPRECATED_VERSION(x, y, t)" TAGFILE_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} QCH_INSTALL_DESTINATION ${KDE_INSTALL_QTQCHDIR} COMPONENT Devel ) endif() include(ECMGeneratePriFile) ecm_generate_pri_file(BASE_NAME KParts LIB_NAME KF5Parts DEPS "KIOWidgets KXmlGui KTextWidgets" FILENAME_VAR PRI_FILENAME INCLUDE_INSTALL_DIR ${KDE_INSTALL_INCLUDEDIR_KF5}/KParts) install(FILES ${PRI_FILENAME} DESTINATION ${ECM_MKSPECS_INSTALL_DIR}) diff --git a/src/partloader.cpp b/src/partloader.cpp new file mode 100644 index 0000000..1949be7 --- /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/src/partloader.h b/src/partloader.h new file mode 100644 index 0000000..c28cd0a --- /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/tests/partviewer.cpp b/tests/partviewer.cpp index ba4e588..6b1f766 100644 --- a/tests/partviewer.cpp +++ b/tests/partviewer.cpp @@ -1,102 +1,102 @@ /* Copyright (c) 2000 David Faure Copyright (c) 2000 Simon Hausmann This library is free software; you can redistribute it and/or modify it under the terms of the GNU Lesser General Public License as published by the Free Software Foundation; either version 2 of the License or ( at your option ) version 3 or, at the discretion of KDE e.V. ( which shall act as a proxy as in section 14 of the GPLv3 ), 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 Lesser 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 "partviewer.h" #include #include #include +#include #include #include #include #include #include #include PartViewer::PartViewer() { setXMLFile(QFINDTESTDATA("partviewer_shell.rc")); QAction *paOpen = new QAction(QIcon::fromTheme(QStringLiteral("document-open")), QStringLiteral("&Open file"), this); actionCollection()->addAction(QStringLiteral("file_open"), paOpen); connect(paOpen, SIGNAL(triggered()), this, SLOT(slotFileOpen())); QAction *paQuit = new QAction(QIcon::fromTheme(QStringLiteral("application-exit")), QStringLiteral("&Quit"), this); actionCollection()->addAction(QStringLiteral("file_quit"), paQuit); connect(paQuit, SIGNAL(triggered()), this, SLOT(close())); m_part = nullptr; // Set a reasonable size resize(600, 350); } PartViewer::~PartViewer() { delete m_part; } void PartViewer::openUrl(const QUrl &url) { 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) { qDebug() << "Loaded part" << m_part << "widget" << m_part->widget(); setCentralWidget(m_part->widget()); // Integrate its GUI createGUI(m_part); m_part->openUrl(url); } } void PartViewer::slotFileOpen() { QUrl url = QFileDialog::getOpenFileUrl(); if (!url.isEmpty()) { openUrl(url); } } int main(int argc, char **argv) { // This is a test window for showing any part QApplication app(argc, argv); PartViewer *shell = new PartViewer; if (argc > 1) { QUrl url = QUrl::fromUserInput(QLatin1String(argv[1])); shell->openUrl(url); } else { shell->slotFileOpen(); } shell->show(); return app.exec(); }