ExtensionManager: Adapt service response parser to latest version

api/v1 on https://qc-extensions.qt.io/api-doc

Change-Id: Ic559029f61d238810e21e586ab7a64c5e847e38d
Reviewed-by: Marcus Tillmanns <marcus.tillmanns@qt.io>
This commit is contained in:
Alessandro Portale
2024-09-06 12:30:45 +02:00
parent af75109ba5
commit c83b4e64f7
10 changed files with 411 additions and 814 deletions

View File

@@ -1,8 +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>
</qresource> </qresource>
</RCC> </RCC>

View File

@@ -57,14 +57,14 @@ namespace ExtensionManager::Internal {
Q_LOGGING_CATEGORY(widgetLog, "qtc.extensionmanager.widget", QtWarningMsg) Q_LOGGING_CATEGORY(widgetLog, "qtc.extensionmanager.widget", QtWarningMsg)
constexpr TextFormat h5TF
{Theme::Token_Text_Default, UiElement::UiElementH5};
constexpr TextFormat h6TF
{h5TF.themeColor, UiElement::UiElementH6};
constexpr TextFormat h6CapitalTF
{Theme::Token_Text_Muted, UiElement::UiElementH6Capital};
constexpr TextFormat contentTF constexpr TextFormat contentTF
{Theme::Token_Text_Default, UiElement::UiElementBody2}; {Theme::Token_Text_Default, UiElement::UiElementBody2};
constexpr TextFormat h5TF
{contentTF.themeColor, UiElement::UiElementH5};
constexpr TextFormat h6TF
{contentTF.themeColor, UiElement::UiElementH6};
constexpr TextFormat h6CapitalTF
{Theme::Token_Text_Muted, UiElement::UiElementH6Capital};
static QLabel *sectionTitle(const TextFormat &tf, const QString &title) static QLabel *sectionTitle(const TextFormat &tf, const QString &title)
{ {
@@ -215,9 +215,9 @@ public:
m_dlCount->setText(QString::number(dlCount)); m_dlCount->setText(QString::number(dlCount));
m_dlCountItems->setVisible(showDlCount); m_dlCountItems->setVisible(showDlCount);
const auto pluginData = current.data(RolePlugins).value<PluginsData>(); const QStringList plugins = current.data(RolePlugins).toStringList();
if (current.data(RoleItemType).toInt() == ItemTypePack) { if (current.data(RoleItemType).toInt() == ItemTypePack) {
const int pluginsCount = pluginData.count(); const int pluginsCount = plugins.count();
const QString details = Tr::tr("Pack contains %n plugins.", nullptr, pluginsCount); const QString details = Tr::tr("Pack contains %n plugins.", nullptr, pluginsCount);
m_details->setText(details); m_details->setText(details);
} else { } else {
@@ -227,9 +227,10 @@ public:
const ItemType itemType = current.data(RoleItemType).value<ItemType>(); const ItemType itemType = current.data(RoleItemType).value<ItemType>();
const bool isPack = itemType == ItemTypePack; const bool isPack = itemType == ItemTypePack;
const bool isRemotePlugin = !(isPack || pluginSpecForId(current.data(RoleId).toString())); const bool isRemotePlugin = !(isPack || pluginSpecForId(current.data(RoleId).toString()));
installButton->setVisible(isRemotePlugin && !pluginData.empty()); const QString downloadUrl = current.data(RoleDownloadUrl).toString();
installButton->setVisible(isRemotePlugin && !downloadUrl.isEmpty());
if (installButton->isVisible()) if (installButton->isVisible())
installButton->setToolTip(pluginData.constFirst().second); installButton->setToolTip(downloadUrl);
} }
signals: signals:
@@ -383,25 +384,17 @@ public:
private: private:
void updateView(const QModelIndex &current); void updateView(const QModelIndex &current);
void fetchAndInstallPlugin(const QUrl &url); void fetchAndInstallPlugin(const QUrl &url);
void fetchAndDisplayImage(const QUrl &url);
QString m_currentItemName; QString m_currentItemName;
ExtensionsModel *m_extensionModel;
ExtensionsBrowser *m_extensionBrowser; ExtensionsBrowser *m_extensionBrowser;
CollapsingWidget *m_secondaryDescriptionWidget; CollapsingWidget *m_secondaryDescriptionWidget;
HeadingWidget *m_headingWidget; HeadingWidget *m_headingWidget;
QWidget *m_primaryContent; QWidget *m_primaryContent;
QWidget *m_secondaryContent; QWidget *m_secondaryContent;
QLabel *m_description; QLabel *m_description;
QLabel *m_linksTitle;
QLabel *m_links;
QLabel *m_imageTitle;
QLabel *m_image;
QBuffer m_imageDataBuffer;
QMovie m_imageMovie;
QLabel *m_tagsTitle; QLabel *m_tagsTitle;
TagList *m_tags; TagList *m_tags;
QLabel *m_compatVersionTitle;
QLabel *m_compatVersion;
QLabel *m_platformsTitle; QLabel *m_platformsTitle;
QLabel *m_platforms; QLabel *m_platforms;
QLabel *m_dependenciesTitle; QLabel *m_dependenciesTitle;
@@ -409,14 +402,14 @@ private:
QLabel *m_packExtensionsTitle; QLabel *m_packExtensionsTitle;
QLabel *m_packExtensions; QLabel *m_packExtensions;
PluginStatusWidget *m_pluginStatus; PluginStatusWidget *m_pluginStatus;
PluginsData m_currentItemPlugins; QString m_currentDownloadUrl;
Tasking::TaskTreeRunner m_dlTaskTreeRunner; Tasking::TaskTreeRunner m_dlTaskTreeRunner;
Tasking::TaskTreeRunner m_imgTaskTreeRunner;
}; };
ExtensionManagerWidget::ExtensionManagerWidget() ExtensionManagerWidget::ExtensionManagerWidget()
{ {
m_extensionBrowser = new ExtensionsBrowser; m_extensionModel = new ExtensionsModel(this);
m_extensionBrowser = new ExtensionsBrowser(m_extensionModel);
auto descriptionColumns = new QWidget; auto descriptionColumns = new QWidget;
m_secondaryDescriptionWidget = new CollapsingWidget; m_secondaryDescriptionWidget = new CollapsingWidget;
@@ -425,21 +418,12 @@ ExtensionManagerWidget::ExtensionManagerWidget()
m_description->setWordWrap(true); m_description->setWordWrap(true);
m_description->setTextInteractionFlags(Qt::TextBrowserInteraction); m_description->setTextInteractionFlags(Qt::TextBrowserInteraction);
m_description->setOpenExternalLinks(true); m_description->setOpenExternalLinks(true);
m_linksTitle = sectionTitle(h6CapitalTF, Tr::tr("More information"));
m_links = tfLabel(contentTF, false);
m_links->setOpenExternalLinks(true);
m_links->setTextInteractionFlags(Qt::TextBrowserInteraction);
m_imageTitle = sectionTitle(h6CapitalTF, {});
m_image = new QLabel;
m_imageMovie.setDevice(&m_imageDataBuffer);
using namespace Layouting; using namespace Layouting;
auto primary = new QWidget; auto primary = new QWidget;
const auto spL = spacing(SpacingTokens::VPaddingL); const auto spL = spacing(SpacingTokens::VPaddingL);
Column { Column {
m_description, m_description,
Column { m_linksTitle, m_links, spL },
Column { m_imageTitle, m_image, spL },
st, st,
noMargin, spacing(SpacingTokens::ExVPaddingGapXl), noMargin, spacing(SpacingTokens::ExVPaddingGapXl),
}.attachTo(primary); }.attachTo(primary);
@@ -447,8 +431,6 @@ ExtensionManagerWidget::ExtensionManagerWidget()
m_tagsTitle = sectionTitle(h6TF, Tr::tr("Tags")); m_tagsTitle = sectionTitle(h6TF, Tr::tr("Tags"));
m_tags = new TagList; m_tags = new TagList;
m_compatVersionTitle = sectionTitle(h6TF, Tr::tr("Compatibility"));
m_compatVersion = tfLabel(contentTF, false);
m_platformsTitle = sectionTitle(h6TF, Tr::tr("Platforms")); m_platformsTitle = sectionTitle(h6TF, Tr::tr("Platforms"));
m_platforms = tfLabel(contentTF, false); m_platforms = tfLabel(contentTF, false);
m_dependenciesTitle = sectionTitle(h6TF, Tr::tr("Dependencies")); m_dependenciesTitle = sectionTitle(h6TF, Tr::tr("Dependencies"));
@@ -463,7 +445,6 @@ ExtensionManagerWidget::ExtensionManagerWidget()
sectionTitle(h6CapitalTF, Tr::tr("Extension details")), sectionTitle(h6CapitalTF, Tr::tr("Extension details")),
Column { Column {
Column { m_tagsTitle, m_tags, spXxs }, Column { m_tagsTitle, m_tags, spXxs },
Column { m_compatVersionTitle, m_compatVersion, spXxs },
Column { m_platformsTitle, m_platforms, spXxs }, Column { m_platformsTitle, m_platforms, spXxs },
Column { m_dependenciesTitle, m_dependencies, spXxs }, Column { m_dependenciesTitle, m_dependencies, spXxs },
Column { m_packExtensionsTitle, m_packExtensions, spXxs }, Column { m_packExtensionsTitle, m_packExtensions, spXxs },
@@ -522,7 +503,7 @@ ExtensionManagerWidget::ExtensionManagerWidget()
m_secondaryDescriptionWidget->setWidth(secondaryDescriptionWidth); m_secondaryDescriptionWidget->setWidth(secondaryDescriptionWidth);
}); });
connect(m_headingWidget, &HeadingWidget::pluginInstallationRequested, this, [this](){ connect(m_headingWidget, &HeadingWidget::pluginInstallationRequested, this, [this](){
fetchAndInstallPlugin(QUrl::fromUserInput(m_currentItemPlugins.constFirst().second)); fetchAndInstallPlugin(QUrl::fromUserInput(m_currentDownloadUrl));
}); });
connect(m_tags, &TagList::tagSelected, m_extensionBrowser, &ExtensionsBrowser::setFilter); connect(m_tags, &TagList::tagSelected, m_extensionBrowser, &ExtensionsBrowser::setFilter);
connect(m_headingWidget, &HeadingWidget::vendorClicked, connect(m_headingWidget, &HeadingWidget::vendorClicked,
@@ -546,7 +527,10 @@ static QString markdownToHtml(const QString &markdown)
blockFormat.setBottomMargin(SpacingTokens::VGapL); blockFormat.setBottomMargin(SpacingTokens::VGapL);
QTextCursor cursor(block); QTextCursor cursor(block);
cursor.mergeBlockFormat(blockFormat); cursor.mergeBlockFormat(blockFormat);
const TextFormat headingTf = blockFormat.headingLevel() == 1 ? h5TF : h6TF; const TextFormat headingTf =
blockFormat.headingLevel() == 1 ? h5TF
: blockFormat.headingLevel() == 2 ? h6TF
: h6CapitalTF;
const QFont headingFont = headingTf.font(); const QFont headingFont = headingTf.font();
for (auto it = block.begin(); !(it.atEnd()); ++it) { for (auto it = block.begin(); !(it.atEnd()); ++it) {
QTextFragment fragment = it.fragment(); QTextFragment fragment = it.fragment();
@@ -555,9 +539,10 @@ static QString markdownToHtml(const QString &markdown)
cursor.setPosition(fragment.position()); cursor.setPosition(fragment.position());
cursor.setPosition(fragment.position() + fragment.length(), QTextCursor::KeepAnchor); cursor.setPosition(fragment.position() + fragment.length(), QTextCursor::KeepAnchor);
if (blockFormat.hasProperty(QTextFormat::HeadingLevel)) { if (blockFormat.hasProperty(QTextFormat::HeadingLevel)) {
charFormat.setFontCapitalization(headingFont.capitalization());
charFormat.setFontFamilies(headingFont.families()); charFormat.setFontFamilies(headingFont.families());
charFormat.setFontWeight(headingFont.weight());
charFormat.setFontPointSize(headingFont.pointSizeF()); charFormat.setFontPointSize(headingFont.pointSizeF());
charFormat.setFontWeight(headingFont.weight());
charFormat.setForeground(headingTf.color()); charFormat.setForeground(headingTf.color());
} else if (charFormat.isAnchor()) { } else if (charFormat.isAnchor()) {
charFormat.setForeground(creatorColor(Theme::Token_Text_Accent)); charFormat.setForeground(creatorColor(Theme::Token_Text_Accent));
@@ -587,96 +572,60 @@ void ExtensionManagerWidget::updateView(const QModelIndex &current)
m_currentItemName = current.data(RoleName).toString(); m_currentItemName = current.data(RoleName).toString();
const bool isPack = current.data(RoleItemType) == ItemTypePack; const bool isPack = current.data(RoleItemType) == ItemTypePack;
m_pluginStatus->setPluginId(isPack ? QString() : current.data(RoleId).toString()); m_pluginStatus->setPluginId(isPack ? QString() : current.data(RoleId).toString());
m_currentItemPlugins = current.data(RolePlugins).value<PluginsData>(); m_currentDownloadUrl = current.data(RoleDownloadUrl).toString();
auto toContentParagraph = [](const QString &text) { {
const QStringList description = {
"# " + m_currentItemName,
current.data(RoleDescriptionShort).toString(),
"",
current.data(RoleDescriptionLong).toString()
};
const QString descriptionMarkdown = description.join("\n");
m_description->setText(markdownToHtml(descriptionMarkdown));
}
{
auto idToDisplayName = [this](const QString &id) {
const QModelIndex dependencyIndex = m_extensionModel->indexOfId(id);
return dependencyIndex.data(RoleName).toString();
};
auto toContentParagraph = [](const QStringList &text) {
const QString lines = text.join("<br/>");
const QString pHtml = QString::fromLatin1("<p style=\"margin-top:0;margin-bottom:0;" const QString pHtml = QString::fromLatin1("<p style=\"margin-top:0;margin-bottom:0;"
"line-height:%1px\">%2</p>") "line-height:%1px\">%2</p>")
.arg(contentTF.lineHeight()).arg(text); .arg(contentTF.lineHeight()).arg(lines);
return pHtml; return pHtml;
}; };
{
const TextData textData = current.data(RoleDescriptionText).value<TextData>();
const bool hasDescription = !textData.isEmpty();
if (hasDescription) {
QString descriptionMarkdown;
for (const TextData::Type &text : textData) {
if (!text.first.isEmpty()) {
const QLatin1String headingMark(descriptionMarkdown.isEmpty() ? "#" : "\n\n##");
descriptionMarkdown.append(headingMark + " " + text.first + "\n");
}
descriptionMarkdown.append(text.second.join("\n"));
}
m_description->setText(markdownToHtml(descriptionMarkdown));
}
m_description->setVisible(hasDescription);
const LinksData linksData = current.data(RoleDescriptionLinks).value<LinksData>();
const bool hasLinks = !linksData.isEmpty();
if (hasLinks) {
QString linksHtml;
const QStringList links = transform(linksData, [](const LinksData::Type &link) {
const QString anchor = link.first.isEmpty() ? link.second : link.first;
return QString::fromLatin1(R"(<a href="%1" style="color:%2">%3 &gt;</a>)")
.arg(link.second)
.arg(creatorColor(Theme::Token_Text_Accent).name())
.arg(anchor);
});
linksHtml = links.join("<br/>");
m_links->setText(toContentParagraph(linksHtml));
}
m_linksTitle->setVisible(hasLinks);
m_links->setVisible(hasLinks);
m_imgTaskTreeRunner.reset();
m_imageMovie.stop();
m_imageDataBuffer.close();
m_image->clear();
const ImagesData imagesData = current.data(RoleDescriptionImages).value<ImagesData>();
const bool hasImages = !imagesData.isEmpty();
if (hasImages) {
const ImagesData::Type &image = imagesData.constFirst(); // Only show one image
m_imageTitle->setText(image.first);
fetchAndDisplayImage(image.second);
}
m_imageTitle->setVisible(hasImages);
m_image->setVisible(hasImages);
}
{
const QStringList tags = current.data(RoleTags).toStringList(); const QStringList tags = current.data(RoleTags).toStringList();
m_tags->setTags(tags); m_tags->setTags(tags);
const bool hasTags = !tags.isEmpty(); const bool hasTags = !tags.isEmpty();
m_tagsTitle->setVisible(hasTags); m_tagsTitle->setVisible(hasTags);
m_tags->setVisible(hasTags); m_tags->setVisible(hasTags);
const QString compatVersion = current.data(RoleCompatVersion).toString();
const bool hasCompatVersion = !compatVersion.isEmpty();
if (hasCompatVersion)
m_compatVersion->setText(compatVersion);
m_compatVersionTitle->setVisible(hasCompatVersion);
m_compatVersion->setVisible(hasCompatVersion);
const QStringList platforms = current.data(RolePlatforms).toStringList(); const QStringList platforms = current.data(RolePlatforms).toStringList();
const bool hasPlatforms = !platforms.isEmpty(); const bool hasPlatforms = !platforms.isEmpty();
if (hasPlatforms) if (hasPlatforms)
m_platforms->setText(toContentParagraph(platforms.join("<br/>"))); m_platforms->setText(toContentParagraph(platforms));
m_platformsTitle->setVisible(hasPlatforms); m_platformsTitle->setVisible(hasPlatforms);
m_platforms->setVisible(hasPlatforms); m_platforms->setVisible(hasPlatforms);
const QStringList dependencies = current.data(RoleDependencies).toStringList(); const QStringList dependencies = current.data(RoleDependencies).toStringList();
const bool hasDependencies = !dependencies.isEmpty(); const bool hasDependencies = !dependencies.isEmpty();
if (hasDependencies) if (hasDependencies) {
m_dependencies->setText(toContentParagraph(dependencies.join("<br/>"))); const QStringList displayNames = transform(dependencies, idToDisplayName);
m_dependencies->setText(toContentParagraph(displayNames));
}
m_dependenciesTitle->setVisible(hasDependencies); m_dependenciesTitle->setVisible(hasDependencies);
m_dependencies->setVisible(hasDependencies); m_dependencies->setVisible(hasDependencies);
const PluginsData plugins = current.data(RolePlugins).value<PluginsData>(); const QStringList plugins = current.data(RolePlugins).toStringList();
const bool hasExtensions = isPack && !plugins.isEmpty(); const bool hasExtensions = isPack && !plugins.isEmpty();
if (hasExtensions) { if (hasExtensions) {
const QStringList extensions = transform(plugins, &QPair<QString, QString>::first); const QStringList displayNames = transform(plugins, idToDisplayName);
m_packExtensions->setText(toContentParagraph(extensions.join("<br/>"))); m_packExtensions->setText(toContentParagraph(displayNames));
} }
m_packExtensionsTitle->setVisible(hasExtensions); m_packExtensionsTitle->setVisible(hasExtensions);
m_packExtensions->setVisible(hasExtensions); m_packExtensions->setVisible(hasExtensions);
@@ -743,60 +692,6 @@ void ExtensionManagerWidget::fetchAndInstallPlugin(const QUrl &url)
m_dlTaskTreeRunner.start(group); m_dlTaskTreeRunner.start(group);
} }
void ExtensionManagerWidget::fetchAndDisplayImage(const QUrl &url)
{
using namespace Tasking;
struct StorageStruct
{
QByteArray imageData;
QUrl url;
};
Storage<StorageStruct> storage;
const auto onFetchSetup = [url, storage](NetworkQuery &query) {
storage->url = url;
query.setRequest(QNetworkRequest(url));
query.setNetworkAccessManager(NetworkAccessManager::instance());
qCDebug(widgetLog).noquote() << "Sending image request:" << url.toDisplayString();
};
const auto onFetchDone = [storage](const NetworkQuery &query, DoneWith result) {
qCDebug(widgetLog) << "Got image QNetworkReply:" << query.reply()->error();
if (result == DoneWith::Success)
storage->imageData = query.reply()->readAll();
};
const auto onShowImage = [storage, this]() {
if (storage->imageData.isEmpty())
return;
m_imageDataBuffer.setData(storage->imageData);
qCDebug(widgetLog).noquote() << "Image reponse size:"
<< QLocale::system().formattedDataSize(
m_imageDataBuffer.size());
if (!m_imageDataBuffer.open(QIODevice::ReadOnly))
return;
QImageReader reader(&m_imageDataBuffer);
const bool animated = reader.supportsAnimation();
if (animated) {
m_image->setMovie(&m_imageMovie);
m_imageMovie.start();
} else {
const QPixmap pixmap = QPixmap::fromImage(reader.read());
m_image->setPixmap(pixmap);
}
qCDebug(widgetLog) << "Image dimensions:" << reader.size();
qCDebug(widgetLog) << "Image is animated:" << animated;
};
Group group{
storage,
NetworkQueryTask{onFetchSetup, onFetchDone},
onGroupDone(onShowImage),
};
m_imgTaskTreeRunner.start(group);
}
QWidget *createExtensionManagerWidget() QWidget *createExtensionManagerWidget()
{ {
return new ExtensionManagerWidget; return new ExtensionManagerWidget;

View File

@@ -277,7 +277,7 @@ public:
painter->setFont(countTF.font()); painter->setFont(countTF.font());
painter->setPen(countTF.color()); painter->setPen(countTF.color());
const PluginsData plugins = index.data(RolePlugins).value<PluginsData>(); const QStringList plugins = index.data(RolePlugins).toStringList();
painter->drawText(smallCircle, countTF.drawTextFlags, QString::number(plugins.count())); painter->drawText(smallCircle, countTF.drawTextFlags, QString::number(plugins.count()));
} }
{ {
@@ -487,10 +487,12 @@ public:
SpinnerSolution::Spinner *m_spinner; SpinnerSolution::Spinner *m_spinner;
}; };
ExtensionsBrowser::ExtensionsBrowser(QWidget *parent) ExtensionsBrowser::ExtensionsBrowser(ExtensionsModel *model, QWidget *parent)
: QWidget(parent) : QWidget(parent)
, d(new ExtensionsBrowserPrivate) , d(new ExtensionsBrowserPrivate)
{ {
d->model = model;
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
static const TextFormat titleTF static const TextFormat titleTF
@@ -501,8 +503,6 @@ ExtensionsBrowser::ExtensionsBrowser(QWidget *parent)
d->searchBox = new SearchBox; d->searchBox = new SearchBox;
d->searchBox->setPlaceholderText(Tr::tr("Search")); d->searchBox->setPlaceholderText(Tr::tr("Search"));
d->model = new ExtensionsModel(this);
d->searchProxyModel = new QSortFilterProxyModel(this); d->searchProxyModel = new QSortFilterProxyModel(this);
d->searchProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive); d->searchProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
d->searchProxyModel->setFilterRole(RoleSearchText); d->searchProxyModel->setFilterRole(RoleSearchText);
@@ -622,28 +622,11 @@ void ExtensionsBrowser::showEvent(QShowEvent *event)
QWidget::showEvent(event); QWidget::showEvent(event);
} }
static QString customOsTypeToString(OsType osType)
{
switch (osType) {
case OsTypeWindows:
return "Windows";
case OsTypeLinux:
return "Linux";
case OsTypeMac:
return "macOS";
case OsTypeOtherUnix:
return "Other Unix";
case OsTypeOther:
default:
return "Other";
}
}
void ExtensionsBrowser::fetchExtensions() void ExtensionsBrowser::fetchExtensions()
{ {
#ifdef WITH_TESTS #ifdef WITH_TESTS
// Uncomment for testing with local json data. // Uncomment for testing with local json data.
// Available: "augmentedplugindata", "defaultpacks", "varieddata", "thirdpartyplugins" // Available: "defaultpacks", "thirdpartyplugins"
// d->model->setExtensionsJson(testData("defaultpacks")); return; // d->model->setExtensionsJson(testData("defaultpacks")); return;
#endif // WITH_TESTS #endif // WITH_TESTS
@@ -655,14 +638,8 @@ void ExtensionsBrowser::fetchExtensions()
using namespace Tasking; using namespace Tasking;
const auto onQuerySetup = [this](NetworkQuery &query) { const auto onQuerySetup = [this](NetworkQuery &query) {
const QString url = "%1/api/v1/search?request="; const QString url = "%1/api/v1/search";
const QString requestTemplate const QString request = url.arg(settings().externalRepoUrl());
= R"({"qtc_version":"%1","host_os":"%2","host_os_version":"%3","host_architecture":"%4","page_size":200})";
const QString request = url.arg(settings().externalRepoUrl()) + requestTemplate
.arg(QCoreApplication::applicationVersion())
.arg(customOsTypeToString(HostOsInfo::hostOs()))
.arg(QSysInfo::productVersion())
.arg(QSysInfo::currentCpuArchitecture());
query.setRequest(QNetworkRequest(QUrl::fromUserInput(request))); query.setRequest(QNetworkRequest(QUrl::fromUserInput(request)));
query.setNetworkAccessManager(NetworkAccessManager::instance()); query.setNetworkAccessManager(NetworkAccessManager::instance());
qCDebug(browserLog).noquote() << "Sending JSON request:" << request; qCDebug(browserLog).noquote() << "Sending JSON request:" << request;

View File

@@ -13,12 +13,14 @@ class TextFormat;
namespace ExtensionManager::Internal { namespace ExtensionManager::Internal {
class ExtensionsModel;
class ExtensionsBrowser final : public QWidget class ExtensionsBrowser final : public QWidget
{ {
Q_OBJECT Q_OBJECT
public: public:
ExtensionsBrowser(QWidget *parent = nullptr); ExtensionsBrowser(ExtensionsModel *model, QWidget *parent = nullptr);
~ExtensionsBrowser(); ~ExtensionsBrowser();
void setFilter(const QString &filter); void setFilter(const QString &filter);

View File

@@ -5,7 +5,8 @@
#include "extensionmanagertr.h" #include "extensionmanagertr.h"
#include "utils/algorithm.h" #include <utils/algorithm.h>
#include <utils/hostosinfo.h>
#include <coreplugin/coreconstants.h> #include <coreplugin/coreconstants.h>
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
@@ -21,272 +22,197 @@
#include <QStandardItemModel> #include <QStandardItemModel>
#include <QVersionNumber> #include <QVersionNumber>
#include <functional>
using namespace ExtensionSystem; using namespace ExtensionSystem;
using namespace Core; using namespace Core;
using namespace Utils; using namespace Utils;
namespace ExtensionManager::Internal { namespace ExtensionManager::Internal {
const char EXTENSION_KEY_ID[] = "id";
Q_LOGGING_CATEGORY(modelLog, "qtc.extensionmanager.model", QtWarningMsg) Q_LOGGING_CATEGORY(modelLog, "qtc.extensionmanager.model", QtWarningMsg)
struct Dependency
{
QString id;
QString version;
};
using Dependencies = QList<Dependency>;
struct Plugin
{
QString copyright;
Dependencies dependencies;
bool isInternal = false;
QString id;
QString name;
QString packageUrl;
QString vendor;
QString version;
};
using Plugins = QList<Plugin>;
struct Description {
ImagesData images;
LinksData links;
TextData text;
};
struct Extension {
QString compatVersion;
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 vendorId;
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({
.id = metaDataObj.value("Id").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),
.id = metaDataObj.value("Id").toString(),
.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 = {
.compatVersion = obj.value("compatibility").toString(),
.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(),
.vendorId = obj.value("vendor_id").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 {
.id = pd.id,
.version = pd.version,
};
});
const Plugin plugin = {
.copyright = pluginSpec->copyright(),
.dependencies = dependencies,
.id = pluginSpec->id(),
.name = pluginSpec->name(),
.packageUrl = {},
.vendor = pluginSpec->vendor(),
.version = pluginSpec->version(),
};
const QStringList lines = pluginSpec->description().split('\n')
+ pluginSpec->longDescription().split('\n');
const TextData text = {{pluginSpec->name(), lines}};
LinksData links;
if (const QString url = pluginSpec->url(); !url.isEmpty())
links.append({{}, url});
if (const QString docUrl = pluginSpec->documentationUrl(); !docUrl.isEmpty())
links.append({{Tr::tr("Documentation")}, docUrl});
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 = {
.compatVersion = pluginSpec->compatVersion(),
.copyright = pluginSpec->copyright(),
.description = description,
.id = pluginSpec->id(),
.license = pluginSpec->license(),
.name = pluginSpec->name(),
.platforms = platforms,
.plugins = {plugin},
.tags = {},
.type = ItemTypeExtension,
.vendor = pluginSpec->vendor(),
.vendorId = pluginSpec->vendorId(),
.version = pluginSpec->version(),
};
return extension;
}
class ExtensionsModelPrivate class ExtensionsModelPrivate
{ {
public: public:
void setExtensions(const Extensions &extensions); void addUnlistedLocalPlugins();
void addUnlistedLocalExtensions();
Extensions extensions; static QVariant dataFromRemotePack(const QJsonObject &json, int role);
static QVariant dataFromRemotePlugin(const QJsonObject &json, int role);
QVariant dataFromRemoteExtension(int index, int role) const;
QVariant dataFromLocalPlugin(int index, int role) const;
QJsonArray responseItems;
PluginSpecs localPlugins;
}; };
void ExtensionsModelPrivate::setExtensions(const Extensions &extensions) void ExtensionsModelPrivate::addUnlistedLocalPlugins()
{ {
this->extensions = extensions; QStringList responseExtensions;
qCDebug(modelLog) << "Number of extensions from JSON:" << this->extensions.count(); for (const QJsonValueConstRef &responseItem : responseItems)
addUnlistedLocalExtensions(); responseExtensions << responseItem.toObject().value("id").toString();
qCDebug(modelLog) << "Number of extensions with added local ones:" << this->extensions.count();
localPlugins.clear();
for (PluginSpec *plugin : PluginManager::plugins())
if (!responseExtensions.contains(plugin->id()))
localPlugins.append(plugin);
qCDebug(modelLog) << "Number of extensions from JSON:" << responseExtensions.count();
qCDebug(modelLog) << "Number of added local plugins:" << localPlugins.count();
} }
void ExtensionsModelPrivate::addUnlistedLocalExtensions() QString joinedStringList(const QJsonValue &json)
{ {
const QStringList listedModelExtensions = transform(extensions, &Extension::name); if (json.isArray()) {
for (const PluginSpec *plugin : PluginManager::plugins()) const QStringList lines = json.toVariant().toStringList();
if (!listedModelExtensions.contains(plugin->name())) return lines.join("\n");
extensions.append(extensionFromPluginSpec(plugin)); }
return json.toString();
}
QString descriptionWithLinks(const QString &description, const QString &url,
const QString &documentationUrl)
{
QStringList fragments;
const QString mdLink("[%1](%2)");
if (!url.isEmpty())
fragments.append(mdLink.arg(url).arg(url));
if (!documentationUrl.isEmpty())
fragments.append(mdLink.arg(Tr::tr("Documentation")).arg(documentationUrl));
if (!fragments.isEmpty())
fragments.prepend("### " + Tr::tr("More Information"));
fragments.prepend(description);
return fragments.join("\n\n");
}
QVariant ExtensionsModelPrivate::dataFromRemotePack(const QJsonObject &json, int role)
{
switch (role) {
case RoleDescriptionLong:
return joinedStringList(json.value("long_description"));
case RoleDescriptionShort:
return joinedStringList(json.value("description"));
case RoleItemType:
return ItemTypePack;
case RolePlugins:
return json.value("plugins").toVariant().toStringList();
default:
break;
}
return {};
}
QVariant ExtensionsModelPrivate::dataFromRemotePlugin(const QJsonObject &json, int role)
{
const QJsonObject metaData = json.value("metadata").toObject();
switch (role) {
case RoleCopyright:
return metaData.value("Copyright");
case RoleDownloadUrl: {
const QJsonArray sources = json.value("sources").toArray();
const QString thisPlatform = customOsTypeToString(HostOsInfo::hostOs());
const QString thisArch = QSysInfo::currentCpuArchitecture();
for (const QJsonValue source : sources) {
const QJsonObject sourceObject = source.toObject();
const QJsonObject platform = sourceObject.value("platform").toObject();
if (platform.isEmpty() // Might be a Lua plugin
|| (platform.value("name").toString() == thisPlatform
&& platform.value("architecture") == thisArch))
return sourceObject.value("url").toString();
}
break;
}
case RoleItemType:
return ItemTypeExtension;
case RoleDescriptionLong: {
const QString description = joinedStringList(metaData.value("LongDescription"));
const QString url = metaData.value("Url").toString();
const QString documentationUrl = metaData.value("DocumentationUrl").toString();
return descriptionWithLinks(description, url, documentationUrl);
}
case RoleDescriptionShort:
return joinedStringList(metaData.value("Description"));
default:
break;
}
return {};
}
QVariant ExtensionsModelPrivate::dataFromRemoteExtension(int index, int role) const
{
const QJsonObject json = responseItems.at(index).toObject();
switch (role) {
case Qt::DisplayRole:
case RoleName:
return json.value("display_name");
case RoleDownloadCount:
return json.value("downloads");
case RoleId:
return json.value(EXTENSION_KEY_ID);
case RoleTags:
return json.value("tags").toVariant().toStringList();
case RoleVendor:
return json.value("display_vendor");
default:
break;
}
const QJsonObject pluginObject = json.value("plugin").toObject();
if (!pluginObject.isEmpty())
return dataFromRemotePlugin(pluginObject, role);
const QJsonObject packObject = json.value("pack").toObject();
if (!packObject.isEmpty())
return dataFromRemotePack(packObject, role);
return {};
}
QVariant ExtensionsModelPrivate::dataFromLocalPlugin(int index, int role) const
{
const PluginSpec *pluginSpec = localPlugins.at(index);
switch (role) {
case Qt::DisplayRole:
case RoleName:
return pluginSpec->displayName();
case RoleCopyright:
return pluginSpec->copyright();
case RoleDependencies: {
const QStringList dependencies
= transform(pluginSpec->dependencies(), &PluginDependency::id);
return dependencies;
}
case RoleDescriptionLong:
return descriptionWithLinks(pluginSpec->longDescription(), pluginSpec->url(),
pluginSpec->documentationUrl());
case RoleDescriptionShort:
return pluginSpec->description();
case RoleId:
return pluginSpec->id();
case RoleItemType:
return ItemTypeExtension;
case RolePlatforms: {
const QString platformsPattern = pluginSpec->platformSpecification().pattern();
const QStringList platforms = platformsPattern.isEmpty()
? QStringList({customOsTypeToString(OsTypeMac),
customOsTypeToString(OsTypeWindows),
customOsTypeToString(OsTypeLinux)})
: QStringList(platformsPattern);
return platforms;
}
case RoleVendor:
return pluginSpec->vendor();
case RoleVendorId:
return pluginSpec->vendorId();
default:
break;
}
return {};
} }
ExtensionsModel::ExtensionsModel(QObject *parent) ExtensionsModel::ExtensionsModel(QObject *parent)
@@ -302,77 +228,7 @@ ExtensionsModel::~ExtensionsModel()
int ExtensionsModel::rowCount([[maybe_unused]] const QModelIndex &parent) const int ExtensionsModel::rowCount([[maybe_unused]] const QModelIndex &parent) const
{ {
return d->extensions.count(); return d->responseItems.count() + d->localPlugins.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.id).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 Utils::findOr(
QStringList{extension.name, extension.id},
"No name found",
std::not_fn(&QString::isEmpty));
case RoleCompatVersion:
return extension.compatVersion;
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.id, 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();
case RoleVendorId:
return extension.vendorId;
default:
break;
}
return {};
} }
ExtensionState extensionState(const QModelIndex &index) ExtensionState extensionState(const QModelIndex &index)
@@ -392,10 +248,8 @@ static QString searchText(const QModelIndex &index)
QStringList searchTexts; QStringList searchTexts;
searchTexts.append(index.data(RoleName).toString()); searchTexts.append(index.data(RoleName).toString());
searchTexts.append(index.data(RoleTags).toStringList()); searchTexts.append(index.data(RoleTags).toStringList());
for (const auto &data : index.data(RoleDescriptionText).value<TextData>()) { searchTexts.append(index.data(RoleDescriptionShort).toString());
searchTexts.append(data.first); searchTexts.append(index.data(RoleDescriptionLong).toString());
searchTexts.append(data.second);
}
searchTexts.append(index.data(RoleVendor).toString()); searchTexts.append(index.data(RoleVendor).toString());
return searchTexts.join(" "); return searchTexts.join(" ");
} }
@@ -407,28 +261,56 @@ QVariant ExtensionsModel::data(const QModelIndex &index, int role) const
if (role == RoleSearchText) if (role == RoleSearchText)
return searchText(index); return searchText(index);
const Extension &extension = d->extensions.at(index.row()); const bool isRemoteExtension = index.row() < d->responseItems.count();
const QVariant extensionData = dataFromExtension(extension, role); const int itemIndex = index.row() - (isRemoteExtension ? 0 : d->responseItems.count());
// If data is unavailable, retrieve it from the first contained plugin
if (extensionData.isNull() && !extension.plugins.isEmpty()) { return isRemoteExtension ? d->dataFromRemoteExtension(itemIndex, role)
const QString firstPluginId = extension.plugins.constFirst().id; : d->dataFromLocalPlugin(itemIndex, role);
const Extension firstPluginExtension
= findOrDefault(d->extensions, Utils::equal(&Extension::id, firstPluginId));
if (firstPluginExtension.name.isEmpty())
return {};
return dataFromExtension(firstPluginExtension, role);
} }
return extensionData;
QModelIndex ExtensionsModel::indexOfId(const QString &extensionId) const
{
const int localIndex = indexOf(d->localPlugins, equal(&PluginSpec::id, extensionId));
if (localIndex >= 0)
return index(d->responseItems.count() + localIndex);
for (int remoteIndex = 0; const QJsonValueConstRef vlaue : d->responseItems) {
if (vlaue.toObject().value(EXTENSION_KEY_ID) == extensionId)
return index(remoteIndex);
++remoteIndex;
}
return {};
} }
void ExtensionsModel::setExtensionsJson(const QByteArray &json) void ExtensionsModel::setExtensionsJson(const QByteArray &json)
{ {
const Extensions extensions = parseExtensionsRepoReply(json);
beginResetModel(); beginResetModel();
d->setExtensions(extensions); QJsonParseError error;
const QJsonObject jsonObj = QJsonDocument::fromJson(json, &error).object();
qCDebug(modelLog) << "QJsonParseError:" << error.errorString();
d->responseItems = jsonObj.value("items").toArray();
d->addUnlistedLocalPlugins();
endResetModel(); endResetModel();
} }
QString customOsTypeToString(OsType osType)
{
switch (osType) {
case OsTypeWindows:
return "Windows";
case OsTypeLinux:
return "Linux";
case OsTypeMac:
return "macOS";
case OsTypeOtherUnix:
return "Other Unix";
case OsTypeOther:
default:
return "Other";
}
}
PluginSpec *pluginSpecForId(const QString &pluginId) PluginSpec *pluginSpecForId(const QString &pluginId)
{ {
return findOrDefault(PluginManager::plugins(), equal(&PluginSpec::id, pluginId)); return findOrDefault(PluginManager::plugins(), equal(&PluginSpec::id, pluginId));

View File

@@ -3,6 +3,8 @@
#pragma once #pragma once
#include <utils/osspecificaspects.h>
#include <QAbstractListModel> #include <QAbstractListModel>
namespace ExtensionSystem { namespace ExtensionSystem {
@@ -11,13 +13,6 @@ class PluginSpec;
namespace ExtensionManager::Internal { namespace ExtensionManager::Internal {
using QPairList = QList<QPair<QString, QString> >;
using ImagesData = QPairList; // { <caption, url>, ... }
using LinksData = QPairList; // { <name, url>, ... }
using PluginsData = QPairList; // { <name, url>, ... }
using TextData = QList<QPair<QString, QStringList> >; // { <header, text>, ... }
enum ItemType { enum ItemType {
ItemTypePack, ItemTypePack,
ItemTypeExtension, ItemTypeExtension,
@@ -32,22 +27,19 @@ enum ExtensionState {
enum Role { enum Role {
RoleName = Qt::UserRole, RoleName = Qt::UserRole,
RoleCompatVersion,
RoleCopyright, RoleCopyright,
RoleDependencies, RoleDependencies,
RoleDescriptionImages, RoleDescriptionLong,
RoleDescriptionLinks, RoleDescriptionShort,
RoleDescriptionText,
RoleDownloadCount, RoleDownloadCount,
RoleDownloadUrl,
RoleExtensionState, RoleExtensionState,
RoleId, RoleId,
RoleItemType, RoleItemType,
RoleLicense, RoleLicense,
RoleLocation,
RolePlatforms, RolePlatforms,
RolePlugins, RolePlugins,
RoleSearchText, RoleSearchText,
RoleSize,
RoleTags, RoleTags,
RoleVendor, RoleVendor,
RoleVendorId, RoleVendorId,
@@ -63,12 +55,14 @@ public:
int rowCount(const QModelIndex &parent = {}) const; int rowCount(const QModelIndex &parent = {}) const;
QVariant data(const QModelIndex &index, int role) const; QVariant data(const QModelIndex &index, int role) const;
QModelIndex indexOfId(const QString &extensionId) const;
void setExtensionsJson(const QByteArray &json); void setExtensionsJson(const QByteArray &json);
private: private:
class ExtensionsModelPrivate *d = nullptr; class ExtensionsModelPrivate *d = nullptr;
}; };
QString customOsTypeToString(Utils::OsType osType);
ExtensionSystem::PluginSpec *pluginSpecForId(const QString &pluginId); ExtensionSystem::PluginSpec *pluginSpecForId(const QString &pluginId);
#ifdef WITH_TESTS #ifdef WITH_TESTS
@@ -76,6 +70,3 @@ QObject *createExtensionsModelTest();
#endif #endif
} // ExtensionManager::Internal } // ExtensionManager::Internal
Q_DECLARE_METATYPE(ExtensionManager::Internal::QPairList)
Q_DECLARE_METATYPE(ExtensionManager::Internal::TextData)

View File

@@ -1,71 +0,0 @@
{
"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

@@ -1,126 +1,114 @@
{ {
"items": [ "items": [
{ {
"name": "Essentials", "id": "essentials",
"display_name": "Essentials",
"tags": [ "Essentials" ], "tags": [ "Essentials" ],
"platforms": [ "macOS", "Windows", "Linux" ], "license": "open-source",
"license": "os", "vendor_id": "theqtcompany",
"is_pack": true, "display_vendor": "The Qt Company",
"description": { "version": "14.0.2",
"paragraphs": [ "pack": {
{ "description": "Get started",
"text": [ "long_description": [
"Basic services, such as editing and debugging code, viewing images, and adding resources to applications." "Basic services, such as editing and debugging code, viewing images, and adding resources to applications.",
"",
"Online documentation: https://doc.qt.io/qtcreator/creator-coding-navigating.html"
], ],
"header": "Get started"
}
],
"links": [
{
"url": "https://doc.qt.io/qtcreator/creator-coding-navigating.html",
"link_text": "Online documentation"
}
]
},
"plugins": [ "plugins": [
{ "meta_data": { "Name": "BinEditor" } }, "bineditor",
{ "meta_data": { "Name": "Debugger" } }, "debugger",
{ "meta_data": { "Name": "DiffEditor" } }, "diffeditor",
{ "meta_data": { "Name": "ImageViewer" } }, "imageviewer",
{ "meta_data": { "Name": "Macros" } }, "macros",
{ "meta_data": { "Name": "LanguageClient" } }, "languageclient",
{ "meta_data": { "Name": "ResourceEditor" } } "resourceeditor"
] ]
}
}, },
{ {
"name": "C++ Support", "id": "cppsupport",
"display_name": "C++ Support",
"tags": [ "Programming Language", "C++" ], "tags": [ "Programming Language", "C++" ],
"platforms": [ "macOS", "Windows", "Linux" ], "license": "open-source",
"license": "os", "vendor_id": "theqtcompany",
"is_pack": true, "display_vendor": "The Qt Company",
"description": { "version": "14.0.2",
"paragraphs": [ "pack": {
{ "description": "Get started",
"text": [ "long_description": [
"Tools for developing Qt C++ applications." "Tools for developing Qt C++ applications."
], ],
"header": "Get started"
}
]
},
"plugins": [ "plugins": [
{ "meta_data": { "Name": "ClangCodeModel" } }, "clangcodemodel",
{ "meta_data": { "Name": "ClangFormat" } }, "clangformat",
{ "meta_data": { "Name": "ClassView" } }, "classview",
{ "meta_data": { "Name": "CppEditor" } } "cppeditor"
] ]
}
}, },
{ {
"name": "QML Support", "id": "qmlsupport",
"display_name": "QML Support",
"tags": [ "Programming Language", "QML" ], "tags": [ "Programming Language", "QML" ],
"platforms": [ "macOS", "Windows", "Linux" ], "license": "open-source",
"license": "os", "vendor_id": "theqtcompany",
"is_pack": true, "display_vendor": "The Qt Company",
"description": { "version": "14.0.2",
"paragraphs": [ "pack": {
{ "description": "Get started",
"text": [ "long_description": [
"Tools for developing Qt Quick applications." "Tools for developing Qt Quick applications."
], ],
"header": "Get started"
}
]
},
"plugins": [ "plugins": [
{ "meta_data": { "Name": "QmlJSEditor" } }, "qmljseditor",
{ "meta_data": { "Name": "QmlJSTools" } }, "qmljstools",
{ "meta_data": { "Name": "QmlPreview" } }, "qmlpreview",
{ "meta_data": { "Name": "QmlProfiler" } } "qmlprofiler"
] ]
}
}, },
{ {
"name": "Visual QML Editor", "id": "visualqmleditor",
"display_name": "Visual QML Editor",
"tags": [ "Visual UI editor", "qml", "Quick" ], "tags": [ "Visual UI editor", "qml", "Quick" ],
"platforms": [ "macOS", "Windows", "Linux" ], "license": "open-source",
"license": "os", "vendor_id": "theqtcompany",
"is_pack": true, "display_vendor": "The Qt Company",
"description": { "version": "14.0.2",
"paragraphs": [ "pack": {
{ "description": "Get started",
"text": [ "long_description": [
"Tools for creating Qt Quick UIs." "Tools for creating Qt Quick UIs."
], ],
"header": "Get started"
}
]
},
"plugins": [ "plugins": [
{ "meta_data": { "Name": "QmlDesigner" } } "qmldesigner"
] ]
}
}, },
{ {
"name": "Visual Widget Editor", "id": "visualwidgeteditor",
"display_name": "Visual Widget Editor",
"tags": [ "Visual UI editor", "C++", "Widgets" ], "tags": [ "Visual UI editor", "C++", "Widgets" ],
"platforms": [ "macOS", "Windows", "Linux" ], "license": "open-source",
"license": "os", "vendor_id": "theqtcompany",
"is_pack": true, "display_vendor": "The Qt Company",
"description": { "version": "14.0.2",
"paragraphs": [ "pack": {
{ "description": "Get started",
"text": [ "long_description": [
"Visual tool for creating Qt widget-based UIs." "Visual tool for creating Qt widget-based UIs."
], ],
"header": "Get started"
}
]
},
"plugins": [ "plugins": [
{ "meta_data": { "Name": "Designer" } } "designer"
] ]
} }
}
] ]
} }

View File

@@ -1,38 +1,49 @@
{ {
"items": [ "items": [
{ {
"name": "SpellChecker", "id": "spellchecker",
"display_name": "SpellChecker",
"tags": [ "Editor" ], "tags": [ "Editor" ],
"platforms": [ "macOS", "Windows", "Linux" ], "license": "open-source",
"license": "os", "vendor_id": "carelcombrink",
"is_pack": false, "display_vendor": "Carel Combrink",
"description": { "downloads": 2333,
"paragraphs": [ "plugin": {
{ "metadata": {
"text": [ "Copyright": "(C) 2015 - 2024 Carel Combrink",
"Description": "Foo",
"LongDescription": [
"Spellcheck comments in source files." "Spellcheck comments in source files."
], ],
"header": "Get started" "Url" : "https://github.com/CJCombrink/SpellChecker-Plugin",
} "DocumentationUrl" : "https://github.com/CJCombrink/SpellChecker-Plugin?tab=readme-ov-file#spellchecker-plugin"
],
"links": [
{
"url": "https://github.com/CJCombrink/SpellChecker-Plugin",
"link_text": "GitHub page"
}
]
}, },
"plugins": [ "sources": [
{ {
"meta_data": { "url": "https://github.com/CJCombrink/SpellChecker-Plugin/releases/download/v3.7.0/SpellChecker-Plugin_QtC14.0.0_linux_x64.tar.gz",
"Name": "SpellChecker", "platform": {
"Copyright": "(C) 2015 - 2024 Carel Combrink" "name": "Linux",
}, "architecture": "x86_64"
"url": "https://github.com/CJCombrink/SpellChecker-Plugin/releases/download/v3.6.0/SpellChecker-Plugin_QtC13.0.0_macos_x64.tar.gz" }
},
{
"url": "https://github.com/CJCombrink/SpellChecker-Plugin/releases/download/v3.7.0/SpellChecker-Plugin_QtC14.0.0_macos_x64.tar.gz",
"platform": {
"name": "macOS",
"architecture": "x86_64"
}
},
{
"url": "https://github.com/CJCombrink/SpellChecker-Plugin/releases/download/v3.7.0/SpellChecker-Plugin_QtC14.0.0_win64_x64.zip",
"platform": {
"name": "Windows",
"architecture": "x86_64"
} }
],
"vendor": "Carel Combrink",
"copyright": "(C) 2015 - 2024 Carel Combrink"
} }
] ]
} }
}
]
}

View File

@@ -1,76 +0,0 @@
{
"items": [
{
"name": "Few tags",
"tags": [ "Tag one", "Tag two"]
},
{
"name": "Many tags",
"tags": [ "Tag_01", "Tag_02", "Tag_03", "Tag_04", "Tag_05", "Tag_06", "Tag_07", "Tag_08", "Tag_09", "Tag_10", "Tag_11", "Tag_12", "Tag_13", "Tag_14", "Tag_15", "Tag_16", "Tag_17", "Tag_18", "Tag_19", "Tag_20", "Tag_21", "Tag_22", "Tag_23", "Tag_24", "Tag_25", "Tag_26", "Tag_27", "Tag_28", "Tag_29", "Tag_30", "And_a_very_long_tag_without_spaces", "Ok, a last long tag without spaces, but that sgould be enough"],
"description": {
"paragraphs": [
{
"text": [
"... and a few long ones"
]
}
]
}
},
{
"name": "One static image",
"description": {
"paragraphs": [
{
"text": [
"png"
]
}
],
"images": [
{
"image_label": "Screenshot",
"url": "https://bugreports.qt.io/secure/attachment/147354/VirtualNodesShownAsNotExisting.png"
}
]
}
},
{
"name": "One animated image",
"description": {
"paragraphs": [
{
"text": [
"gif (animated)"
]
}
],
"images": [
{
"image_label": "Screencast",
"url": "https://bugreports.qt.io/secure/attachment/156058/156058_DragAndCopyOnLinux.gif"
}
]
}
},
{
"name": "Vendor, no download count",
"vendor": "Vendor name"
},
{
"name": "No vendor, but download count",
"download_count": 12345
},
{
"name": "Vendor and download count",
"vendor": "Vendor name",
"download_count": 12345
}
]
}