forked from qt-creator/qt-creator
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:
@@ -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>
|
||||
|
@@ -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;
|
||||
|
@@ -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)
|
||||
@@ -371,28 +366,19 @@ 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 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 PluginSpec *pluginSpec = ExtensionsModel::pluginSpecForName(
|
||||
extension.plugins.constFirst().name);
|
||||
if (pluginSpec)
|
||||
return dataFromPluginSpec(pluginSpec, role);
|
||||
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;
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
void ExtensionsModel::setExtensionsJson(const QByteArray &json)
|
||||
{
|
||||
|
71
src/plugins/extensionmanager/testdata/augmentedplugindata.json
vendored
Normal file
71
src/plugins/extensionmanager/testdata/augmentedplugindata.json
vendored
Normal 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"
|
||||
}
|
||||
]
|
||||
}
|
@@ -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"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
Reference in New Issue
Block a user