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>
|
<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>
|
||||||
|
@@ -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;
|
||||||
|
@@ -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,27 +366,18 @@ 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)
|
||||||
|
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": [
|
"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"
|
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user