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>
This commit is contained in:
Alessandro Portale
2024-06-25 20:16:23 +02:00
parent d9d5b87141
commit aefc50f1de
5 changed files with 164 additions and 141 deletions

View File

@@ -1,5 +1,6 @@
<RCC>
<qresource prefix="/extensionmanager">
<file>testdata/augmentedplugindata.json</file>
<file>testdata/defaultpacks.json</file>
<file>testdata/thirdpartyplugins.json</file>
<file>testdata/varieddata.json</file>

View File

@@ -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;

View File

@@ -36,8 +36,8 @@ using Dependencies = QList<Dependency>;
struct Plugin
{
Dependencies dependencies;
QString copyright;
Dependencies dependencies;
bool isInternal = false;
QString name;
QString packageUrl;
@@ -69,23 +69,29 @@ struct Extension {
};
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();
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)

View File

@@ -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"
}
]
}

View File

@@ -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"
}
]
}