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> <RCC>
<qresource prefix="/extensionmanager"> <qresource prefix="/extensionmanager">
<file>testdata/augmentedplugindata.json</file>
<file>testdata/defaultpacks.json</file> <file>testdata/defaultpacks.json</file>
<file>testdata/thirdpartyplugins.json</file> <file>testdata/thirdpartyplugins.json</file>
<file>testdata/varieddata.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) { const auto onQueryDone = [this](const NetworkQuery &query, DoneWith result) {
if (result != DoneWith::Success) { if (result != DoneWith::Success) {
#ifdef WITH_TESTS #ifdef WITH_TESTS
// Available test sets: "defaultpacks", "varieddata", "thirdpartyplugins" // Available: "augmentedplugindata", "defaultpacks", "varieddata", "thirdpartyplugins"
d->model->setExtensionsJson(testData("defaultpacks")); d->model->setExtensionsJson(testData("defaultpacks"));
#endif // WITH_TESTS #endif // WITH_TESTS
return; return;

View File

@@ -36,8 +36,8 @@ using Dependencies = QList<Dependency>;
struct Plugin struct Plugin
{ {
Dependencies dependencies;
QString copyright; QString copyright;
Dependencies dependencies;
bool isInternal = false; bool isInternal = false;
QString name; QString name;
QString packageUrl; QString packageUrl;
@@ -69,23 +69,29 @@ struct Extension {
}; };
using Extensions = QList<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) static Plugin pluginFromJson(const QJsonObject &obj)
{ {
const QJsonObject metaDataObj = obj.value("meta_data").toObject(); 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 { return {
.dependencies = dependencies,
.copyright = metaDataObj.value("Copyright").toString(), .copyright = metaDataObj.value("Copyright").toString(),
.dependencies = dependenciesFromJson(metaDataObj),
.isInternal = obj.value("is_internal").toBool(false), .isInternal = obj.value("is_internal").toBool(false),
.name = metaDataObj.value("Name").toString(), .name = metaDataObj.value("Name").toString(),
.packageUrl = obj.value("url").toString(), .packageUrl = obj.value("url").toString(),
@@ -192,30 +198,78 @@ static Extensions parseExtensionsRepoReply(const QByteArray &jsonData)
return parsedExtensions; 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 class ExtensionsModelPrivate
{ {
public: public:
void setExtensions(const Extensions &extensions); void setExtensions(const Extensions &extensions);
void removeLocalExtensions(); void addUnlistedLocalExtensions();
Extensions allExtensions; // Original, complete extensions entries Extensions extensions;
Extensions absentExtensions; // All packs + plugin extensions that are not (yet) installed
}; };
void ExtensionsModelPrivate::setExtensions(const Extensions &extensions) void ExtensionsModelPrivate::setExtensions(const Extensions &extensions)
{ {
allExtensions = extensions; this->extensions = extensions;
removeLocalExtensions(); addUnlistedLocalExtensions();
} }
void ExtensionsModelPrivate::removeLocalExtensions() void ExtensionsModelPrivate::addUnlistedLocalExtensions()
{ {
const QStringList installedPlugins = transform(PluginManager::plugins(), &PluginSpec::name); const QStringList listedModelExtensions = transform(extensions, &Extension::name);
absentExtensions.clear(); for (const PluginSpec *plugin : PluginManager::plugins())
for (const Extension &extension : allExtensions) { if (!listedModelExtensions.contains(plugin->name()))
if (extension.type == ItemTypePack || !installedPlugins.contains(extension.name)) extensions.append(extensionFromPluginSpec(plugin));
absentExtensions.append(extension);
}
} }
ExtensionsModel::ExtensionsModel(QObject *parent) ExtensionsModel::ExtensionsModel(QObject *parent)
@@ -231,66 +285,7 @@ ExtensionsModel::~ExtensionsModel()
int ExtensionsModel::rowCount([[maybe_unused]] const QModelIndex &parent) const int ExtensionsModel::rowCount([[maybe_unused]] const QModelIndex &parent) const
{ {
const int remoteExtnsionsCount = d->absentExtensions.count(); return d->extensions.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 {};
} }
static QStringList dependenciesFromExtension(const Extension &extension) static QStringList dependenciesFromExtension(const Extension &extension)
@@ -371,28 +366,19 @@ QVariant ExtensionsModel::data(const QModelIndex &index, int role) const
if (role == RoleSearchText) if (role == RoleSearchText)
return searchText(index); return searchText(index);
const bool itemIsLocalPlugin = index.row() >= d->absentExtensions.count(); const Extension &extension = d->extensions.at(index.row());
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); const QVariant extensionData = dataFromExtension(extension, role);
// If data is unavailable, retrieve it from the first contained plugin // If data is unavailable, retrieve it from the first contained plugin
if (extensionData.isNull() && !extension.plugins.isEmpty()) { if (extensionData.isNull() && !extension.plugins.isEmpty()) {
const PluginSpec *pluginSpec = ExtensionsModel::pluginSpecForName( const QString firstPluginName = extension.plugins.constFirst().name;
extension.plugins.constFirst().name); const Extension firstPluginExtension =
if (pluginSpec) findOrDefault(d->extensions, Utils::equal(&Extension::name, firstPluginName));
return dataFromPluginSpec(pluginSpec, role); if (firstPluginExtension.name.isEmpty())
return {};
return dataFromExtension(firstPluginExtension, role);
} }
return extensionData; return extensionData;
} }
return {};
}
void ExtensionsModel::setExtensionsJson(const QByteArray &json) 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": [ "plugins": [
{ "meta_data": { "Name": "Designer" } } { "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"
} }
] ]
} }