From aefc50f1de4c532fe7ac4c5aafa7e20a134ad79f Mon Sep 17 00:00:00 2001 From: Alessandro Portale Date: Tue, 25 Jun 2024 20:16:23 +0200 Subject: [PATCH] ExtensionManager: Prioritize plugin metadata data from service This change enables the Qt Creator Extension service data to override data of locally installed plugins (so far it was the other way around). We might want to add more description text, links, images or tags to plugins. And we want to be able do that independently of Qt Creator releases. This change also allows for substantial simplification of the "data merging" code. The parsing of extension plugin dependencies from the json data needed to be fixed. See dependenciesFromJson() Change-Id: Ia0433f0e0c7a0f13c43e0569c0915b7d08f7370a Reviewed-by: Cristian Adam --- .../extensionmanager_test.qrc | 1 + .../extensionmanager/extensionsbrowser.cpp | 2 +- .../extensionmanager/extensionsmodel.cpp | 196 ++++++++---------- .../testdata/augmentedplugindata.json | 71 +++++++ .../testdata/defaultpacks.json | 35 ---- 5 files changed, 164 insertions(+), 141 deletions(-) create mode 100644 src/plugins/extensionmanager/testdata/augmentedplugindata.json diff --git a/src/plugins/extensionmanager/extensionmanager_test.qrc b/src/plugins/extensionmanager/extensionmanager_test.qrc index f8a11b4e084..e7e934e5316 100644 --- a/src/plugins/extensionmanager/extensionmanager_test.qrc +++ b/src/plugins/extensionmanager/extensionmanager_test.qrc @@ -1,5 +1,6 @@ + testdata/augmentedplugindata.json testdata/defaultpacks.json testdata/thirdpartyplugins.json testdata/varieddata.json diff --git a/src/plugins/extensionmanager/extensionsbrowser.cpp b/src/plugins/extensionmanager/extensionsbrowser.cpp index cd4c591097d..bfe4fb102f8 100644 --- a/src/plugins/extensionmanager/extensionsbrowser.cpp +++ b/src/plugins/extensionmanager/extensionsbrowser.cpp @@ -381,7 +381,7 @@ void ExtensionsBrowser::fetchExtensions() const auto onQueryDone = [this](const NetworkQuery &query, DoneWith result) { if (result != DoneWith::Success) { #ifdef WITH_TESTS - // Available test sets: "defaultpacks", "varieddata", "thirdpartyplugins" + // Available: "augmentedplugindata", "defaultpacks", "varieddata", "thirdpartyplugins" d->model->setExtensionsJson(testData("defaultpacks")); #endif // WITH_TESTS return; diff --git a/src/plugins/extensionmanager/extensionsmodel.cpp b/src/plugins/extensionmanager/extensionsmodel.cpp index 56fcc54ad8b..702e3019f75 100644 --- a/src/plugins/extensionmanager/extensionsmodel.cpp +++ b/src/plugins/extensionmanager/extensionsmodel.cpp @@ -36,8 +36,8 @@ using Dependencies = QList; struct Plugin { - Dependencies dependencies; QString copyright; + Dependencies dependencies; bool isInternal = false; QString name; QString packageUrl; @@ -69,23 +69,29 @@ struct Extension { }; using Extensions = QList; +static const Dependencies dependenciesFromJson(const QJsonObject &obj) +{ + const QJsonArray dependenciesArray = obj.value("Dependencies").toArray(); + Dependencies dependencies; + for (const QJsonValueConstRef &dependencyVal : dependenciesArray) { + const QJsonObject dependencyObj = dependencyVal.toObject(); + const QJsonObject metaDataObj = dependencyObj.value("meta_data").toObject(); + dependencies.append({ + .name = metaDataObj.value("Name").toString(), + .version = metaDataObj.value("Version").toString(), + }); + } + + return dependencies; +} + static Plugin pluginFromJson(const QJsonObject &obj) { const QJsonObject metaDataObj = obj.value("meta_data").toObject(); - const QJsonArray dependenciesArray = metaDataObj.value("Dependencies").toArray(); - Dependencies dependencies; - for (const QJsonValueConstRef &dependencyVal : dependenciesArray) { - const QJsonObject dependencyObj = dependencyVal.toObject(); - dependencies.append(Dependency{ - .name = dependencyObj.value("Name").toString(), - .version = dependencyObj.value("Version").toString(), - }); - } - return { - .dependencies = dependencies, .copyright = metaDataObj.value("Copyright").toString(), + .dependencies = dependenciesFromJson(metaDataObj), .isInternal = obj.value("is_internal").toBool(false), .name = metaDataObj.value("Name").toString(), .packageUrl = obj.value("url").toString(), @@ -192,30 +198,78 @@ static Extensions parseExtensionsRepoReply(const QByteArray &jsonData) return parsedExtensions; } +static Extension extensionFromPluginSpec(const PluginSpec *pluginSpec) +{ + const Dependencies dependencies = transform(pluginSpec->dependencies(), + [](const PluginDependency &pd) -> Dependency { + return { + .name = pd.name, + .version = pd.version, + }; + }); + const Plugin plugin = { + .copyright = pluginSpec->copyright(), + .dependencies = dependencies, + .name = pluginSpec->name(), + .packageUrl = {}, + .vendor = pluginSpec->vendor(), + .version = pluginSpec->version(), + }; + + const QStringList lines = pluginSpec->description().split('\n', Qt::SkipEmptyParts) + + pluginSpec->longDescription().split('\n', Qt::SkipEmptyParts); + const TextData text = {{ pluginSpec->name(), lines }}; + LinksData links; + if (const QString url = pluginSpec->url(); !url.isEmpty()) + links.append({{}, url}); + const Description description = { + .images = {}, + .links = links, + .text = text, + }; + + const QString platformsPattern = pluginSpec->platformSpecification().pattern(); + const QStringList platforms = platformsPattern.isEmpty() + ? QStringList({"macOS", "Windows", "Linux"}) + : QStringList(platformsPattern); + + const Extension extension = { + .copyright = pluginSpec->copyright(), + .description = description, + .id = {}, + .license = pluginSpec->license(), + .name = pluginSpec->name(), + .platforms = platforms, + .plugins = {plugin}, + .tags = {}, + .type = ItemTypeExtension, + .vendor = pluginSpec->vendor(), + .version = pluginSpec->version(), + }; + return extension; +} + class ExtensionsModelPrivate { public: void setExtensions(const Extensions &extensions); - void removeLocalExtensions(); + void addUnlistedLocalExtensions(); - Extensions allExtensions; // Original, complete extensions entries - Extensions absentExtensions; // All packs + plugin extensions that are not (yet) installed + Extensions extensions; }; void ExtensionsModelPrivate::setExtensions(const Extensions &extensions) { - allExtensions = extensions; - removeLocalExtensions(); + this->extensions = extensions; + addUnlistedLocalExtensions(); } -void ExtensionsModelPrivate::removeLocalExtensions() +void ExtensionsModelPrivate::addUnlistedLocalExtensions() { - const QStringList installedPlugins = transform(PluginManager::plugins(), &PluginSpec::name); - absentExtensions.clear(); - for (const Extension &extension : allExtensions) { - if (extension.type == ItemTypePack || !installedPlugins.contains(extension.name)) - absentExtensions.append(extension); - } + const QStringList listedModelExtensions = transform(extensions, &Extension::name); + for (const PluginSpec *plugin : PluginManager::plugins()) + if (!listedModelExtensions.contains(plugin->name())) + extensions.append(extensionFromPluginSpec(plugin)); } ExtensionsModel::ExtensionsModel(QObject *parent) @@ -231,66 +285,7 @@ ExtensionsModel::~ExtensionsModel() int ExtensionsModel::rowCount([[maybe_unused]] const QModelIndex &parent) const { - const int remoteExtnsionsCount = d->absentExtensions.count(); - const int installedPluginsCount = PluginManager::plugins().count(); - return remoteExtnsionsCount + installedPluginsCount; -} - -static QVariant dataFromPluginSpec(const PluginSpec *pluginSpec, int role) -{ - switch (role) { - case Qt::DisplayRole: - case RoleName: - return pluginSpec->name(); - case RoleCopyright: - return pluginSpec->copyright(); - case RoleDependencies: { - QStringList dependencies = transform(pluginSpec->dependencies(), - &PluginDependency::toString); - dependencies.sort(); - return dependencies; - } - case RoleDescriptionImages: - break; - case RoleDescriptionLinks: { - const QString url = pluginSpec->url(); - if (!url.isEmpty()) { - const LinksData links = {{{}, url}}; - return QVariant::fromValue(links); - } - break; - } - case RoleDescriptionText: { - QStringList lines = pluginSpec->description().split('\n', Qt::SkipEmptyParts); - lines.append(pluginSpec->longDescription().split('\n', Qt::SkipEmptyParts)); - const TextData text = {{ pluginSpec->name(), lines }}; - return QVariant::fromValue(text); - } - case RoleItemType: - return ItemTypeExtension; - case RoleLicense: - return pluginSpec->license(); - case RoleLocation: - return pluginSpec->filePath().toVariant(); - case RolePlatforms: { - const QString pattern = pluginSpec->platformSpecification().pattern(); - const QStringList platforms = pattern.isEmpty() - ? QStringList({"macOS", "Windows", "Linux"}) - : QStringList(pattern); - return platforms; - } - case RoleSize: - return pluginSpec->filePath().fileSize(); - case RoleTags: - break; - case RoleVendor: - return pluginSpec->vendor(); - case RoleVersion: - return pluginSpec->version(); - default: - break; - } - return {}; + return d->extensions.count(); } static QStringList dependenciesFromExtension(const Extension &extension) @@ -299,7 +294,7 @@ static QStringList dependenciesFromExtension(const Extension &extension) for (const Plugin &plugin : extension.plugins) { for (const Dependency &dependency : plugin.dependencies) { const QString withVersion = QString::fromLatin1("%1 (%2)").arg(dependency.name) - .arg(dependency.version); + .arg(dependency.version); dependencies.append(withVersion); } } @@ -371,27 +366,18 @@ QVariant ExtensionsModel::data(const QModelIndex &index, int role) const if (role == RoleSearchText) return searchText(index); - const bool itemIsLocalPlugin = index.row() >= d->absentExtensions.count(); - if (itemIsLocalPlugin) { - const PluginSpecs &pluginSpecs = PluginManager::plugins(); - const int pluginIndex = index.row() - d->absentExtensions.count(); - QTC_ASSERT(pluginIndex >= 0 && pluginIndex <= pluginSpecs.size(), return {}); - const PluginSpec *plugin = pluginSpecs.at(pluginIndex); - return dataFromPluginSpec(plugin, role); - } else { - const Extension &extension = d->absentExtensions.at(index.row()); - const QVariant extensionData = dataFromExtension(extension, role); - - // If data is unavailable, retrieve it from the first contained plugin - if (extensionData.isNull() && !extension.plugins.isEmpty()) { - const PluginSpec *pluginSpec = ExtensionsModel::pluginSpecForName( - extension.plugins.constFirst().name); - if (pluginSpec) - return dataFromPluginSpec(pluginSpec, role); - } - return extensionData; + const Extension &extension = d->extensions.at(index.row()); + const QVariant extensionData = dataFromExtension(extension, role); + // If data is unavailable, retrieve it from the first contained plugin + if (extensionData.isNull() && !extension.plugins.isEmpty()) { + const QString firstPluginName = extension.plugins.constFirst().name; + const Extension firstPluginExtension = + findOrDefault(d->extensions, Utils::equal(&Extension::name, firstPluginName)); + if (firstPluginExtension.name.isEmpty()) + return {}; + return dataFromExtension(firstPluginExtension, role); } - return {}; + return extensionData; } void ExtensionsModel::setExtensionsJson(const QByteArray &json) diff --git a/src/plugins/extensionmanager/testdata/augmentedplugindata.json b/src/plugins/extensionmanager/testdata/augmentedplugindata.json new file mode 100644 index 00000000000..32efe15b708 --- /dev/null +++ b/src/plugins/extensionmanager/testdata/augmentedplugindata.json @@ -0,0 +1,71 @@ +{ + "items": [ + { + "name": "ScreenRecorder", + "description": { + "paragraphs": [ + { + "header": "Screen Recorder plugin", + "text": [ + "With FFmpeg, you can record your screens and save the recordings as animated images or videos.", + "To record screens:", + "", + "- Select Tools > Screen Recording.", + "- Select to select the screen to record from and to set the recorded screen area.", + "- Select to start recording.", + "- Select when you are done recording.", + "- Select Crop and Trim to edit the recording.", + "- Select Export to save the recording as an animated image or a video." + ] + }, + { + "header": "Set the screen and area to record", + "text": [ + "Set the screen and the area to record in the Screen Recording Options dialog.", + "To select a screen and area:", + "", + "- In Display, select the display to record.", + "- In Recorded screen area, drag the guides to set the x and y coordinates of the starting point for the recording area, as well as the width and height of the area.", + "- Select OK to return to the Record Screen dialog." + ] + } + ], + "images": [ + { + "image_label": "Create animated imges like this", + "url": "https://bugreports.qt.io/secure/attachment/156058/156058_DragAndCopyOnLinux.gif" + } + ], + "links": [ + { + "link_text": "Documentation", + "url": "https://doc.qt.io/qtcreator/creator-how-to-record-screens.html" + }, + { + "link_text": "Homepage", + "url": "https://www.qt.io/" + } + ] + }, + "is_pack": false, + "plugins": [ + { + "meta_data": { + "Name": "ScreenRecorder", + "Dependencies": [ + { + "meta_data": { + "Name": "Core", + "Version": "13.0.2" + } + } + ], + "Version": "13.0.2" + } + } + ], + "tags": [ "Utility", "Docs" ], + "vendor": "The Qt Company Ltd" + } + ] +} diff --git a/src/plugins/extensionmanager/testdata/defaultpacks.json b/src/plugins/extensionmanager/testdata/defaultpacks.json index 0323b08d27e..d26b0a42c60 100644 --- a/src/plugins/extensionmanager/testdata/defaultpacks.json +++ b/src/plugins/extensionmanager/testdata/defaultpacks.json @@ -121,41 +121,6 @@ "plugins": [ { "meta_data": { "Name": "Designer" } } ] - }, - - { - "name": "SpellChecker", - "tags": [ "Editor" ], - "platforms": [ "macOS", "Windows", "Linux" ], - "license": "os", - "is_pack": false, - "description": { - "paragraphs": [ - { - "text": [ - "Spellcheck comments in source files." - ], - "header": "Get started" - } - ], - "links": [ - { - "url": "https://github.com/CJCombrink/SpellChecker-Plugin", - "link_text": "GitHub page" - } - ] - }, - "plugins": [ - { - "meta_data": { - "Name": "SpellChecker", - "Copyright": "(C) 2015 - 2024 Carel Combrink" - }, - "url": "https://github.com/CJCombrink/SpellChecker-Plugin/releases/download/v3.6.0/SpellChecker-Plugin_QtC13.0.0_macos_x64.tar.gz" - } - ], - "vendor": "Carel Combrink", - "copyright": "(C) 2015 - 2024 Carel Combrink" } ] }