diff --git a/CMakeLists.txt b/CMakeLists.txt index c8d1b354e2..caa2104520 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,7 @@ cmake_minimum_required(VERSION 3.1 FATAL_ERROR) -project(KWin) -set(PROJECT_VERSION "5.20.80") -set(PROJECT_VERSION_MAJOR 5) +set(PROJECT_VERSION "5.20.80") # Handled by release scripts +project(KWin VERSION ${PROJECT_VERSION}) set(QT_MIN_VERSION "5.15.0") set(KF5_MIN_VERSION "5.74") @@ -506,6 +505,8 @@ set(kwin_SRCS overlaywindow.cpp placement.cpp platform.cpp + plugin.cpp + pluginmanager.cpp pointer_input.cpp popup_input_filter.cpp rootinfo_filter.cpp @@ -633,6 +634,7 @@ qt5_add_dbus_adaptor(kwin_SRCS org.kde.kwin.ColorCorrect.xml colorcorrection/col qt5_add_dbus_adaptor(kwin_SRCS ${kwin_effects_dbus_xml} effects.h KWin::EffectsHandlerImpl) qt5_add_dbus_adaptor(kwin_SRCS org.kde.KWin.VirtualDesktopManager.xml dbusinterface.h KWin::VirtualDesktopManagerDBusInterface) qt5_add_dbus_adaptor(kwin_SRCS org.kde.KWin.Session.xml sm.h KWin::SessionManager) +qt5_add_dbus_adaptor(kwin_SRCS org.kde.KWin.Plugins.xml dbusinterface.h KWin::PluginManagerDBusInterface) if (KWIN_BUILD_RUNNERS) qt5_add_dbus_adaptor(kwin_SRCS "runners/org.kde.krunner1.xml" runners/windowsrunnerinterface.h KWin::WindowsRunner) endif() @@ -781,30 +783,8 @@ set(kwin_WAYLAND_SRCS main_wayland.cpp tabletmodemanager.cpp ) -ecm_qt_declare_logging_category(kwin_WAYLAND_SRCS - HEADER - kwinscreencast_logging.h - IDENTIFIER - KWIN_SCREENCAST - CATEGORY_NAME - kwin_screencast - DEFAULT_SEVERITY - Warning -) add_executable(kwin_wayland ${kwin_WAYLAND_SRCS} ${kwin_XWAYLAND_SRCS}) - - -if (PipeWire_FOUND) - target_sources(kwin_wayland - PRIVATE - screencast/eglnativefence.cpp - screencast/screencastmanager.cpp - screencast/pipewirecore.cpp - screencast/pipewirestream.cpp) - target_link_libraries(kwin_wayland PkgConfig::PipeWire) -endif() - target_link_libraries(kwin_wayland kwin KF5::Crash @@ -834,6 +814,10 @@ target_link_libraries(kwin_wayland KF5IdleTimeKWinPlugin ) +if (PipeWire_FOUND) + target_link_libraries(kwin_wayland KWinScreencastPlugin) +endif() + ########### install files ############### install(FILES kwin.kcfg DESTINATION ${KCFG_INSTALL_DIR} RENAME ${KWIN_NAME}.kcfg) @@ -846,6 +830,7 @@ install( org.kde.kwin.ColorCorrect.xml org.kde.kwin.Compositing.xml org.kde.kwin.Effects.xml + org.kde.KWin.Plugins.xml DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} ) diff --git a/autotests/integration/kwin_wayland_test.cpp b/autotests/integration/kwin_wayland_test.cpp index 3ba87233df..99fddee84b 100644 --- a/autotests/integration/kwin_wayland_test.cpp +++ b/autotests/integration/kwin_wayland_test.cpp @@ -8,6 +8,7 @@ */ #include "kwin_wayland_test.h" #include "../../platform.h" +#include "../../pluginmanager.h" #include "../../composite.h" #include "../../effects.h" #include "../../wayland_server.h" @@ -130,6 +131,7 @@ void WaylandTestApplication::performStartup() // try creating the Wayland Backend createInput(); createBackend(); + PluginManager::create(this); } void WaylandTestApplication::createBackend() diff --git a/config-kwin.h.cmake b/config-kwin.h.cmake index 25003e01bd..68b1ec1084 100644 --- a/config-kwin.h.cmake +++ b/config-kwin.h.cmake @@ -1,3 +1,7 @@ +#define KWIN_VERSION_MAJOR ${PROJECT_VERSION_MAJOR} +#define KWIN_VERSION_MINOR ${PROJECT_VERSION_MINOR} +#define KWIN_VERSION_PATCH ${PROJECT_VERSION_PATCH} + #cmakedefine KWIN_BUILD_DECORATIONS 1 #cmakedefine KWIN_BUILD_TABBOX 1 #cmakedefine KWIN_BUILD_ACTIVITIES 1 diff --git a/dbusinterface.cpp b/dbusinterface.cpp index fcd9502068..ef66a3d79f 100644 --- a/dbusinterface.cpp +++ b/dbusinterface.cpp @@ -10,6 +10,7 @@ // own #include "dbusinterface.h" #include "compositingadaptor.h" +#include "pluginsadaptor.h" #include "virtualdesktopmanageradaptor.h" // kwin @@ -20,6 +21,7 @@ #include "main.h" #include "placement.h" #include "platform.h" +#include "pluginmanager.h" #include "kwinadaptor.h" #include "scene.h" #include "unmanaged.h" @@ -515,4 +517,35 @@ void VirtualDesktopManagerDBusInterface::removeDesktop(const QString &id) m_manager->removeVirtualDesktop(id.toUtf8()); } +PluginManagerDBusInterface::PluginManagerDBusInterface(PluginManager *manager) + : QObject(manager) + , m_manager(manager) +{ + new PluginsAdaptor(this); + + QDBusConnection::sessionBus().registerObject(QStringLiteral("/Plugins"), + QStringLiteral("org.kde.KWin.Plugins"), + this); +} + +QStringList PluginManagerDBusInterface::loadedPlugins() const +{ + return m_manager->loadedPlugins(); +} + +QStringList PluginManagerDBusInterface::availablePlugins() const +{ + return m_manager->availablePlugins(); +} + +bool PluginManagerDBusInterface::LoadPlugin(const QString &name) +{ + return m_manager->loadPlugin(name); +} + +void PluginManagerDBusInterface::UnloadPlugin(const QString &name) +{ + m_manager->unloadPlugin(name); +} + } // namespace diff --git a/dbusinterface.h b/dbusinterface.h index 6b8e2b4fa4..85644f85b2 100644 --- a/dbusinterface.h +++ b/dbusinterface.h @@ -19,6 +19,7 @@ namespace KWin { class Compositor; +class PluginManager; class VirtualDesktopManager; /** @@ -238,6 +239,28 @@ private: VirtualDesktopManager *m_manager; }; +class PluginManagerDBusInterface : public QObject +{ + Q_OBJECT + Q_CLASSINFO("D-Bus Interface", "org.kde.KWin.Plugins") + + Q_PROPERTY(QStringList LoadedPlugins READ loadedPlugins) + Q_PROPERTY(QStringList AvailablePlugins READ availablePlugins) + +public: + explicit PluginManagerDBusInterface(PluginManager *manager); + + QStringList loadedPlugins() const; + QStringList availablePlugins() const; + +public Q_SLOTS: + bool LoadPlugin(const QString &name); + void UnloadPlugin(const QString &name); + +private: + PluginManager *m_manager; +}; + } // namespace #endif // KWIN_DBUS_INTERFACE_H diff --git a/main_wayland.cpp b/main_wayland.cpp index aa9223fc11..216dda0d5d 100644 --- a/main_wayland.cpp +++ b/main_wayland.cpp @@ -14,11 +14,9 @@ // kwin #include "platform.h" #include "effects.h" +#include "pluginmanager.h" #include "tabletmodemanager.h" -#ifdef PipeWire_FOUND -#include "screencast/screencastmanager.h" -#endif #include "wayland_server.h" #include "xwl/xwayland.h" @@ -64,6 +62,9 @@ Q_IMPORT_PLUGIN(KWinIntegrationPlugin) Q_IMPORT_PLUGIN(KGlobalAccelImpl) Q_IMPORT_PLUGIN(KWindowSystemKWinPlugin) Q_IMPORT_PLUGIN(KWinIdleTimePoller) +#ifdef PipeWire_FOUND +Q_IMPORT_PLUGIN(ScreencastManagerFactory) +#endif namespace KWin { @@ -157,9 +158,7 @@ void ApplicationWayland::performStartup() InputMethod::create(this); createBackend(); TabletModeManager::create(this); -#ifdef PipeWire_FOUND - new ScreencastManager(this); -#endif + PluginManager::create(this); } void ApplicationWayland::createBackend() diff --git a/main_x11.cpp b/main_x11.cpp index 1f356d4fd3..32d27a4637 100644 --- a/main_x11.cpp +++ b/main_x11.cpp @@ -13,6 +13,7 @@ #include #include "platform.h" +#include "pluginmanager.h" #include "sm.h" #include "workspace.h" #include "xcbutils.h" @@ -248,6 +249,7 @@ void ApplicationX11::performStartup() connect(platform(), &Platform::screensQueried, this, [this] { createWorkspace(); + PluginManager::create(this); Xcb::sync(); // Trigger possible errors, there's still a chance to abort diff --git a/org.kde.KWin.Plugins.xml b/org.kde.KWin.Plugins.xml new file mode 100644 index 0000000000..43a3f736d6 --- /dev/null +++ b/org.kde.KWin.Plugins.xml @@ -0,0 +1,32 @@ + + + + + + + + + + + + + + + + + + + + + diff --git a/plugin.cpp b/plugin.cpp new file mode 100644 index 0000000000..cc869fab1d --- /dev/null +++ b/plugin.cpp @@ -0,0 +1,22 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "plugin.h" + +namespace KWin +{ + +Plugin::Plugin(QObject *parent) + : QObject(parent) +{ +} + +PluginFactory::PluginFactory(QObject *parent) + : QObject(parent) +{ +} + +} // namespace KWin diff --git a/plugin.h b/plugin.h new file mode 100644 index 0000000000..09a7891384 --- /dev/null +++ b/plugin.h @@ -0,0 +1,52 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include + +#include "kwinglobals.h" + +#include + +namespace KWin +{ + +#define KWIN_PLUGIN_API_VERSION QT_VERSION_CHECK(KWIN_VERSION_MAJOR, \ + KWIN_VERSION_MINOR, \ + KWIN_VERSION_PATCH) + +#define PluginFactory_iid "org.kde.kwin.PluginFactoryInterface" + +/** + * The Plugin class is the baseclass for all binary compositor extensions. + * + * Note that a binary extension must be recompiled with every new KWin release. + */ +class KWIN_EXPORT Plugin : public QObject +{ + Q_OBJECT + +public: + explicit Plugin(QObject *parent = nullptr); +}; + +/** + * The PluginFactory class creates binary compositor extensions. + */ +class KWIN_EXPORT PluginFactory : public QObject +{ + Q_OBJECT + +public: + explicit PluginFactory(QObject *parent = nullptr); + + virtual Plugin *create() const = 0; +}; + +} // namespace KWin + +Q_DECLARE_INTERFACE(KWin::PluginFactory, PluginFactory_iid) diff --git a/pluginmanager.cpp b/pluginmanager.cpp new file mode 100644 index 0000000000..3b7903779f --- /dev/null +++ b/pluginmanager.cpp @@ -0,0 +1,188 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#include "pluginmanager.h" +#include "dbusinterface.h" +#include "main.h" +#include "plugin.h" +#include "utils.h" + +#include +#include +#include +#include + +namespace KWin +{ + +KWIN_SINGLETON_FACTORY(PluginManager) + +static const QString s_pluginDirectory = QStringLiteral("kwin/plugins"); + +static QJsonValue readPluginInfo(const QJsonObject &metadata, const QString &key) +{ + return metadata.value(QLatin1String("KPlugin")).toObject().value(key); +} + +PluginManager::PluginManager(QObject *parent) + : QObject(parent) +{ + const KConfigGroup config(kwinApp()->config(), QStringLiteral("Plugins")); + + auto checkEnabled = [&config](const QString &pluginId, const QJsonObject &metadata) { + const QString configKey = pluginId + QLatin1String("Enabled"); + if (config.hasKey(configKey)) { + return config.readEntry(configKey, false); + } + return readPluginInfo(metadata, QStringLiteral("EnabledByDefault")).toBool(false); + }; + + const QVector staticPlugins = QPluginLoader::staticPlugins(); + for (const QStaticPlugin &staticPlugin : staticPlugins) { + const QJsonObject rootMetaData = staticPlugin.metaData(); + if (rootMetaData.value(QLatin1String("IID")) != QLatin1String(PluginFactory_iid)) { + continue; + } + + const QJsonObject pluginMetaData = rootMetaData.value(QLatin1String("MetaData")).toObject(); + const QString pluginId = readPluginInfo(pluginMetaData, QStringLiteral("Id")).toString(); + if (pluginId.isEmpty()) { + continue; + } + if (m_staticPlugins.contains(pluginId)) { + qCWarning(KWIN_CORE) << "Conflicting plugin id" << pluginId; + continue; + } + + m_staticPlugins.insert(pluginId, staticPlugin); + + if (checkEnabled(pluginId, pluginMetaData)) { + loadStaticPlugin(pluginId); + } + } + + const QVector plugins = KPluginLoader::findPlugins(s_pluginDirectory); + for (const KPluginMetaData &metadata : plugins) { + if (m_plugins.contains(metadata.pluginId())) { + qCWarning(KWIN_CORE) << "Conflicting plugin id" << metadata.pluginId(); + continue; + } + if (checkEnabled(metadata.pluginId(), metadata.rawData())) { + loadDynamicPlugin(metadata); + } + } + + new PluginManagerDBusInterface(this); +} + +PluginManager::~PluginManager() +{ + s_self = nullptr; +} + +QStringList PluginManager::loadedPlugins() const +{ + return m_plugins.keys(); +} + +QStringList PluginManager::availablePlugins() const +{ + QStringList ret = m_staticPlugins.keys(); + + const QVector plugins = KPluginLoader::findPlugins(s_pluginDirectory); + for (const KPluginMetaData &metadata : plugins) { + ret.append(metadata.pluginId()); + } + + return ret; +} + +bool PluginManager::loadPlugin(const QString &pluginId) +{ + if (m_plugins.contains(pluginId)) { + qCDebug(KWIN_CORE) << "Plugin with id" << pluginId << "is already loaded"; + return false; + } + return loadStaticPlugin(pluginId) || loadDynamicPlugin(pluginId); +} + +bool PluginManager::loadStaticPlugin(const QString &pluginId) +{ + auto staticIt = m_staticPlugins.find(pluginId); + if (staticIt == m_staticPlugins.end()) { + return false; + } + + QScopedPointer factory(qobject_cast(staticIt->instance())); + if (!factory) { + qCWarning(KWIN_CORE) << "Failed to get plugin factory for" << pluginId; + return false; + } + + return instantiatePlugin(pluginId, factory.data()); +} + +bool PluginManager::loadDynamicPlugin(const QString &pluginId) +{ + const auto offers = KPluginLoader::findPluginsById(s_pluginDirectory, pluginId); + for (const KPluginMetaData &metadata : offers) { + if (loadDynamicPlugin(metadata)) { + return true; + } + } + return false; +} + +bool PluginManager::loadDynamicPlugin(const KPluginMetaData &metadata) +{ + if (!metadata.isValid()) { + qCDebug(KWIN_CORE) << "PluginManager::loadPlugin needs a valid plugin metadata"; + return false; + } + + const QString pluginId = metadata.pluginId(); + KPluginLoader pluginLoader(metadata.fileName()); + if (pluginLoader.pluginVersion() != KWIN_PLUGIN_API_VERSION) { + qCWarning(KWIN_CORE) << pluginId << "has mismatching plugin version"; + return false; + } + + QScopedPointer factory(qobject_cast(pluginLoader.instance())); + if (!factory) { + qCWarning(KWIN_CORE) << "Failed to get plugin factory for" << pluginId; + return false; + } + + return instantiatePlugin(pluginId, factory.data()); +} + +bool PluginManager::instantiatePlugin(const QString &pluginId, PluginFactory *factory) +{ + Plugin *plugin = factory->create(); + if (!plugin) { + return false; + } + + m_plugins.insert(pluginId, plugin); + plugin->setParent(this); + + connect(plugin, &QObject::destroyed, this, [this, pluginId]() { + m_plugins.remove(pluginId); + }); + + return true; +} + +void PluginManager::unloadPlugin(const QString &pluginId) +{ + Plugin *plugin = m_plugins.take(pluginId); + if (!plugin) { + qCWarning(KWIN_CORE) << "No plugin with the specified id:" << pluginId; + } + delete plugin; +} + +} // namespace KWin diff --git a/pluginmanager.h b/pluginmanager.h new file mode 100644 index 0000000000..b90fa368a1 --- /dev/null +++ b/pluginmanager.h @@ -0,0 +1,51 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: GPL-2.0-or-later +*/ + +#pragma once + +#include "kwinglobals.h" + +#include +#include +#include + +#include + +namespace KWin +{ + +class Plugin; +class PluginFactory; + +/** + * The PluginManager class loads and unloads binary compositor extensions. + */ +class KWIN_EXPORT PluginManager : public QObject +{ + Q_OBJECT + +public: + ~PluginManager() override; + + QStringList loadedPlugins() const; + QStringList availablePlugins() const; + +public Q_SLOTS: + bool loadPlugin(const QString &pluginId); + void unloadPlugin(const QString &pluginId); + +private: + bool loadStaticPlugin(const QString &pluginId); + bool loadDynamicPlugin(const KPluginMetaData &metadata); + bool loadDynamicPlugin(const QString &pluginId); + bool instantiatePlugin(const QString &pluginId, PluginFactory *factory); + + QHash m_plugins; + QHash m_staticPlugins; + KWIN_SINGLETON(PluginManager) +}; + +} // namespace KWin diff --git a/plugins/CMakeLists.txt b/plugins/CMakeLists.txt index 9f26622d9d..a80b502cdd 100644 --- a/plugins/CMakeLists.txt +++ b/plugins/CMakeLists.txt @@ -9,3 +9,6 @@ add_subdirectory(kpackage) if (KWIN_BUILD_DECORATIONS) add_subdirectory(kdecorations) endif() +if (PipeWire_FOUND) + add_subdirectory(screencast) +endif() diff --git a/plugins/screencast/CMakeLists.txt b/plugins/screencast/CMakeLists.txt new file mode 100644 index 0000000000..a44c6eecb0 --- /dev/null +++ b/plugins/screencast/CMakeLists.txt @@ -0,0 +1,18 @@ +set(screencast_SOURCES + eglnativefence.cpp + main.cpp + pipewirecore.cpp + pipewirestream.cpp + screencastmanager.cpp +) + +ecm_qt_declare_logging_category(screencast_SOURCES + HEADER kwinscreencast_logging.h + IDENTIFIER KWIN_SCREENCAST + CATEGORY_NAME kwin_screencast + DEFAULT_SEVERITY Warning +) + +add_library(KWinScreencastPlugin OBJECT ${screencast_SOURCES}) +target_compile_definitions(KWinScreencastPlugin PRIVATE QT_STATICPLUGIN) +target_link_libraries(KWinScreencastPlugin kwin PkgConfig::PipeWire) diff --git a/screencast/eglnativefence.cpp b/plugins/screencast/eglnativefence.cpp similarity index 100% rename from screencast/eglnativefence.cpp rename to plugins/screencast/eglnativefence.cpp diff --git a/screencast/eglnativefence.h b/plugins/screencast/eglnativefence.h similarity index 100% rename from screencast/eglnativefence.h rename to plugins/screencast/eglnativefence.h diff --git a/plugins/screencast/main.cpp b/plugins/screencast/main.cpp new file mode 100644 index 0000000000..001c7718a1 --- /dev/null +++ b/plugins/screencast/main.cpp @@ -0,0 +1,46 @@ +/* + SPDX-FileCopyrightText: 2020 Vlad Zahorodnii + + SPDX-License-Identifier: LGPL-2.0-or-later +*/ + +#include "screencastmanager.h" +#include "main.h" + +#include + +using namespace KWin; + +class KWIN_EXPORT ScreencastManagerFactory : public PluginFactory +{ + Q_OBJECT + Q_PLUGIN_METADATA(IID PluginFactory_iid FILE "metadata.json") + Q_INTERFACES(KWin::PluginFactory) + +public: + explicit ScreencastManagerFactory(QObject *parent = nullptr); + + Plugin *create() const override; +}; + +ScreencastManagerFactory::ScreencastManagerFactory(QObject *parent) + : PluginFactory(parent) +{ +} + +Plugin *ScreencastManagerFactory::create() const +{ + switch (kwinApp()->operationMode()) { + case Application::OperationModeX11: + return nullptr; + case Application::OperationModeXwayland: + case Application::OperationModeWaylandOnly: + return new ScreencastManager(); + default: + return nullptr; + } +} + +K_EXPORT_PLUGIN_VERSION(KWIN_PLUGIN_API_VERSION) + +#include "main.moc" diff --git a/plugins/screencast/metadata.json b/plugins/screencast/metadata.json new file mode 100644 index 0000000000..819d9268c8 --- /dev/null +++ b/plugins/screencast/metadata.json @@ -0,0 +1,6 @@ +{ + "KPlugin": { + "EnabledByDefault": true, + "Id": "kwin5_plugin_screencast" + } +} diff --git a/screencast/pipewirecore.cpp b/plugins/screencast/pipewirecore.cpp similarity index 100% rename from screencast/pipewirecore.cpp rename to plugins/screencast/pipewirecore.cpp diff --git a/screencast/pipewirecore.h b/plugins/screencast/pipewirecore.h similarity index 100% rename from screencast/pipewirecore.h rename to plugins/screencast/pipewirecore.h diff --git a/screencast/pipewirestream.cpp b/plugins/screencast/pipewirestream.cpp similarity index 100% rename from screencast/pipewirestream.cpp rename to plugins/screencast/pipewirestream.cpp diff --git a/screencast/pipewirestream.h b/plugins/screencast/pipewirestream.h similarity index 100% rename from screencast/pipewirestream.h rename to plugins/screencast/pipewirestream.h diff --git a/screencast/screencastmanager.cpp b/plugins/screencast/screencastmanager.cpp similarity index 99% rename from screencast/screencastmanager.cpp rename to plugins/screencast/screencastmanager.cpp index 691a5efafe..0db18a5f97 100644 --- a/screencast/screencastmanager.cpp +++ b/plugins/screencast/screencastmanager.cpp @@ -28,7 +28,7 @@ namespace KWin { ScreencastManager::ScreencastManager(QObject *parent) - : QObject(parent) + : Plugin(parent) , m_screencast(waylandServer()->display()->createScreencastV1Interface(this)) { connect(m_screencast, &KWaylandServer::ScreencastV1Interface::windowScreencastRequested, diff --git a/screencast/screencastmanager.h b/plugins/screencast/screencastmanager.h similarity index 93% rename from screencast/screencastmanager.h rename to plugins/screencast/screencastmanager.h index 8229ce44aa..b8b9d14348 100644 --- a/screencast/screencastmanager.h +++ b/plugins/screencast/screencastmanager.h @@ -8,6 +8,8 @@ #pragma once +#include "plugin.h" + #include namespace KWin @@ -15,7 +17,7 @@ namespace KWin class PipeWireStream; -class ScreencastManager : public QObject +class ScreencastManager : public Plugin { Q_OBJECT diff --git a/workspace.cpp b/workspace.cpp index 3443005a49..687288343f 100644 --- a/workspace.cpp +++ b/workspace.cpp @@ -33,6 +33,7 @@ #include "netinfo.h" #include "outline.h" #include "placement.h" +#include "pluginmanager.h" #include "rules.h" #include "screenedge.h" #include "screens.h" @@ -1669,6 +1670,20 @@ QString Workspace::supportInformation() const support.append(static_cast(effects)->supportInformation(effect)); support.append(QStringLiteral("\n")); } + support.append(QLatin1String("\nLoaded Plugins:\n")); + support.append(QLatin1String("---------------\n")); + QStringList loadedPlugins = PluginManager::self()->loadedPlugins(); + loadedPlugins.sort(); + for (const QString &plugin : qAsConst(loadedPlugins)) { + support.append(plugin + QLatin1Char('\n')); + } + support.append(QLatin1String("\nAvailable Plugins:\n")); + support.append(QLatin1String("------------------\n")); + QStringList availablePlugins = PluginManager::self()->availablePlugins(); + availablePlugins.sort(); + for (const QString &plugin : qAsConst(availablePlugins)) { + support.append(plugin + QLatin1Char('\n')); + } } else { support.append(QStringLiteral("Compositing is not active\n")); }