Files
qt-creator/src/plugins/extensionmanager/extensionsmodel.cpp
Alessandro Portale aefc50f1de 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 <cristian.adam@qt.io>
2024-06-26 09:23:05 +00:00

397 lines
12 KiB
C++

// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "extensionsmodel.h"
#include "utils/algorithm.h"
#include <coreplugin/coreconstants.h>
#include <coreplugin/icore.h>
#include <extensionsystem/iplugin.h>
#include <extensionsystem/pluginspec.h>
#include <extensionsystem/pluginview.h>
#include <extensionsystem/pluginmanager.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QStandardItemModel>
#include <QVersionNumber>
using namespace ExtensionSystem;
using namespace Core;
using namespace Utils;
namespace ExtensionManager::Internal {
Q_LOGGING_CATEGORY(modelLog, "qtc.extensionmanager.model", QtWarningMsg)
struct Dependency
{
QString name;
QString version;
};
using Dependencies = QList<Dependency>;
struct Plugin
{
QString copyright;
Dependencies dependencies;
bool isInternal = false;
QString name;
QString packageUrl;
QString vendor;
QString version;
};
using Plugins = QList<Plugin>;
struct Description {
ImagesData images;
LinksData links;
TextData text;
};
struct Extension {
QString copyright;
Description description;
int downloadCount = -1;
QString id;
QString license;
QString name;
QStringList platforms;
Plugins plugins;
qint64 size = 0;
QStringList tags;
ItemType type = ItemTypePack;
QString vendor;
QString version;
};
using Extensions = QList<Extension>;
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();
return {
.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(),
.vendor = metaDataObj.value("Vendor").toString(),
.version = metaDataObj.value("Version").toString(),
};
}
static Description descriptionFromJson(const QJsonObject &obj)
{
TextData descriptionText;
const QJsonArray paragraphsArray = obj.value("paragraphs").toArray();
for (const QJsonValueConstRef &paragraphVal : paragraphsArray) {
const QJsonObject &paragraphObj = paragraphVal.toObject();
const QJsonArray &textArray = paragraphObj.value("text").toArray();
QStringList textLines;
for (const QJsonValueConstRef &textVal : textArray)
textLines.append(textVal.toString());
descriptionText.append({
paragraphObj.value("header").toString(),
textLines,
});
}
LinksData links;
const QJsonArray linksArray = obj.value("links").toArray();
for (const QJsonValueConstRef &linkVal : linksArray) {
const QJsonObject &linkObj = linkVal.toObject();
links.append({
linkObj.value("link_text").toString(),
linkObj.value("url").toString(),
});
}
ImagesData images;
const QJsonArray imagesArray = obj.value("images").toArray();
for (const QJsonValueConstRef &imageVal : imagesArray) {
const QJsonObject &imageObj = imageVal.toObject();
images.append({
imageObj.value("image_label").toString(),
imageObj.value("url").toString(),
});
}
const Description description = {
.images = images,
.links = links,
.text = descriptionText,
};
return description;
}
static Extension extensionFromJson(const QJsonObject &obj)
{
Plugins plugins;
const QJsonArray pluginsArray = obj.value("plugins").toArray();
for (const QJsonValueConstRef &pluginVal : pluginsArray)
plugins.append(pluginFromJson(pluginVal.toObject()));
QStringList tags;
const QJsonArray tagsArray = obj.value("tags").toArray();
for (const QJsonValueConstRef &tagVal : tagsArray)
tags.append(tagVal.toString());
QStringList platforms;
const QJsonArray platformsArray = obj.value("platforms").toArray();
for (const QJsonValueConstRef &platformsVal : platformsArray)
platforms.append(platformsVal.toString());
const QJsonObject descriptionObj = obj.value("description").toObject();
const Description description = descriptionFromJson(descriptionObj);
const Extension extension = {
.copyright = obj.value("copyright").toString(),
.description = description,
.downloadCount = obj.value("download_count").toInt(-1),
.id = obj.value("id").toString(),
.license = obj.value("license").toString(),
.name = obj.value("name").toString(),
.platforms = platforms,
.plugins = plugins,
.size = obj.value("total_size").toInteger(),
.tags = tags,
.type = obj.value("is_pack").toBool(true) ? ItemTypePack : ItemTypeExtension,
.vendor = obj.value("vendor").toString(),
.version = obj.value("version").toString(),
};
return extension;
}
static Extensions parseExtensionsRepoReply(const QByteArray &jsonData)
{
// https://qc-extensions.qt.io/api-docs
Extensions parsedExtensions;
const QJsonObject jsonObj = QJsonDocument::fromJson(jsonData).object();
const QJsonArray items = jsonObj.value("items").toArray();
for (const QJsonValueConstRef &itemVal : items) {
const QJsonObject itemObj = itemVal.toObject();
const Extension extension = extensionFromJson(itemObj);
parsedExtensions.append(extension);
}
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 addUnlistedLocalExtensions();
Extensions extensions;
};
void ExtensionsModelPrivate::setExtensions(const Extensions &extensions)
{
this->extensions = extensions;
addUnlistedLocalExtensions();
}
void ExtensionsModelPrivate::addUnlistedLocalExtensions()
{
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)
: QAbstractListModel(parent)
, d(new ExtensionsModelPrivate)
{
}
ExtensionsModel::~ExtensionsModel()
{
delete d;
}
int ExtensionsModel::rowCount([[maybe_unused]] const QModelIndex &parent) const
{
return d->extensions.count();
}
static QStringList dependenciesFromExtension(const Extension &extension)
{
QStringList dependencies;
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);
dependencies.append(withVersion);
}
}
dependencies.sort();
dependencies.removeDuplicates();
return dependencies;
}
static QVariant dataFromExtension(const Extension &extension, int role)
{
switch (role) {
case Qt::DisplayRole:
case RoleName:
return extension.name;
case RoleCopyright:
return !extension.copyright.isEmpty() ? extension.copyright : QVariant();
case RoleDependencies:
return dependenciesFromExtension(extension);
case RoleDescriptionImages:
return QVariant::fromValue(extension.description.images);
case RoleDescriptionLinks:
return QVariant::fromValue(extension.description.links);
case RoleDescriptionText:
return QVariant::fromValue(extension.description.text);
case RoleDownloadCount:
return extension.downloadCount;
case RoleId:
return extension.id;
case RoleItemType:
return extension.type;
case RoleLicense:
return extension.license;
case RoleLocation:
break;
case RolePlatforms:
return extension.platforms;
case RolePlugins: {
PluginsData plugins;
for (const Plugin &plugin : extension.plugins)
plugins.append(qMakePair(plugin.name, plugin.packageUrl));
return QVariant::fromValue(plugins);
}
case RoleSize:
return extension.size;
case RoleTags:
return extension.tags;
case RoleVendor:
return !extension.vendor.isEmpty() ? extension.vendor : QVariant();
case RoleVersion:
return !extension.version.isEmpty() ? extension.version : QVariant();
default:
break;
}
return {};
}
static QString searchText(const QModelIndex &index)
{
QStringList searchTexts;
searchTexts.append(index.data(RoleName).toString());
searchTexts.append(index.data(RoleTags).toStringList());
searchTexts.append(index.data(RoleDescriptionText).toStringList());
searchTexts.append(index.data(RoleVendor).toString());
return searchTexts.join(" ");
}
QVariant ExtensionsModel::data(const QModelIndex &index, int role) const
{
if (role == RoleSearchText)
return searchText(index);
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 extensionData;
}
void ExtensionsModel::setExtensionsJson(const QByteArray &json)
{
const Extensions extensions = parseExtensionsRepoReply(json);
beginResetModel();
d->setExtensions(extensions);
endResetModel();
}
PluginSpec *ExtensionsModel::pluginSpecForName(const QString &pluginName)
{
return findOrDefault(PluginManager::plugins(), equal(&PluginSpec::name, pluginName));
}
} // ExtensionManager::Internal