forked from qt-creator/qt-creator
ExtensionManager: Introduce extensions service response parser and model
This adds a parser for the JSON response of the extension rest API. The data, combined with the PluginSpecs of local plugins, serve as data model for the extension mode view. A couple of "packs" are provided as test data. Change-Id: I5ce961a9de9bf54ca745e5e5a5e584b1698e6ac6 Reviewed-by: Cristian Adam <cristian.adam@qt.io>
This commit is contained in:
@@ -9,4 +9,14 @@ add_qtc_plugin(ExtensionManager
|
|||||||
extensionmanagerwidget.h
|
extensionmanagerwidget.h
|
||||||
extensionsbrowser.cpp
|
extensionsbrowser.cpp
|
||||||
extensionsbrowser.h
|
extensionsbrowser.h
|
||||||
|
extensionsmodel.cpp
|
||||||
|
extensionsmodel.h
|
||||||
|
)
|
||||||
|
|
||||||
|
extend_qtc_plugin(ExtensionManager
|
||||||
|
CONDITION WITH_TESTS
|
||||||
|
SOURCES
|
||||||
|
extensionmanager_test.cpp
|
||||||
|
extensionmanager_test.h
|
||||||
|
extensionmanager_test.qrc
|
||||||
)
|
)
|
||||||
|
@@ -14,5 +14,15 @@ QtcPlugin {
|
|||||||
"extensionmanagerwidget.h",
|
"extensionmanagerwidget.h",
|
||||||
"extensionsbrowser.cpp",
|
"extensionsbrowser.cpp",
|
||||||
"extensionsbrowser.h",
|
"extensionsbrowser.h",
|
||||||
|
"extensionsmodel.cpp",
|
||||||
|
"extensionsmodel.h",
|
||||||
|
]
|
||||||
|
|
||||||
|
QtcTestFiles {
|
||||||
|
files: [
|
||||||
|
"extensionmanager_test.h",
|
||||||
|
"extensionmanager_test.cpp",
|
||||||
|
"extensionmanager_test.qrc",
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
40
src/plugins/extensionmanager/extensionmanager_test.cpp
Normal file
40
src/plugins/extensionmanager/extensionmanager_test.cpp
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (C) 2024 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
|
#include "extensionmanager_test.h"
|
||||||
|
|
||||||
|
#include "extensionsmodel.h"
|
||||||
|
|
||||||
|
#include <utils/fileutils.h>
|
||||||
|
|
||||||
|
#include <QTest>
|
||||||
|
|
||||||
|
namespace ExtensionManager::Internal {
|
||||||
|
|
||||||
|
class ExtensionsModelTest final : public QObject
|
||||||
|
{
|
||||||
|
Q_OBJECT
|
||||||
|
|
||||||
|
private slots:
|
||||||
|
void testRepositoryJsonParser();
|
||||||
|
};
|
||||||
|
|
||||||
|
void ExtensionsModelTest::testRepositoryJsonParser()
|
||||||
|
{
|
||||||
|
ExtensionsModel model;
|
||||||
|
model.setExtensionsJson(testData("defaultpacks"));
|
||||||
|
}
|
||||||
|
|
||||||
|
QObject *createExtensionsModelTest()
|
||||||
|
{
|
||||||
|
return new ExtensionsModelTest;
|
||||||
|
}
|
||||||
|
|
||||||
|
QByteArray testData(const QString &id)
|
||||||
|
{
|
||||||
|
return Utils::FileReader::fetchQrc(":/extensionmanager/testdata/" + id + ".json");
|
||||||
|
}
|
||||||
|
|
||||||
|
} // ExtensionManager::Internal
|
||||||
|
|
||||||
|
#include "extensionmanager_test.moc"
|
14
src/plugins/extensionmanager/extensionmanager_test.h
Normal file
14
src/plugins/extensionmanager/extensionmanager_test.h
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
// Copyright (C) 2024 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QObject>
|
||||||
|
|
||||||
|
namespace ExtensionManager::Internal {
|
||||||
|
|
||||||
|
QObject *createExtensionsModelTest();
|
||||||
|
|
||||||
|
QByteArray testData(const QString &id);
|
||||||
|
|
||||||
|
} // namespace ExtensionManager::Internal
|
6
src/plugins/extensionmanager/extensionmanager_test.qrc
Normal file
6
src/plugins/extensionmanager/extensionmanager_test.qrc
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
<RCC>
|
||||||
|
<qresource prefix="/extensionmanager">
|
||||||
|
<file>testdata/defaultpacks.json</file>
|
||||||
|
<file>testdata/thirdpartyplugins.json</file>
|
||||||
|
</qresource>
|
||||||
|
</RCC>
|
@@ -5,6 +5,9 @@
|
|||||||
|
|
||||||
#include "extensionmanagerconstants.h"
|
#include "extensionmanagerconstants.h"
|
||||||
#include "extensionmanagerwidget.h"
|
#include "extensionmanagerwidget.h"
|
||||||
|
#ifdef WITH_TESTS
|
||||||
|
#include "extensionmanager_test.h"
|
||||||
|
#endif // WITH_TESTS
|
||||||
|
|
||||||
#include <coreplugin/actionmanager/actioncontainer.h>
|
#include <coreplugin/actionmanager/actioncontainer.h>
|
||||||
#include <coreplugin/actionmanager/actionmanager.h>
|
#include <coreplugin/actionmanager/actionmanager.h>
|
||||||
@@ -15,7 +18,6 @@
|
|||||||
#include <coreplugin/imode.h>
|
#include <coreplugin/imode.h>
|
||||||
|
|
||||||
#include <extensionsystem/iplugin.h>
|
#include <extensionsystem/iplugin.h>
|
||||||
#include <extensionsystem/pluginspec.h>
|
|
||||||
|
|
||||||
#include <utils/icon.h>
|
#include <utils/icon.h>
|
||||||
#include <utils/layoutbuilder.h>
|
#include <utils/layoutbuilder.h>
|
||||||
@@ -73,6 +75,10 @@ public:
|
|||||||
void initialize() final
|
void initialize() final
|
||||||
{
|
{
|
||||||
m_mode = new ExtensionManagerMode;
|
m_mode = new ExtensionManagerMode;
|
||||||
|
|
||||||
|
#ifdef WITH_TESTS
|
||||||
|
addTestCreator(createExtensionsModelTest);
|
||||||
|
#endif // WITH_TESTS
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
@@ -3,26 +3,40 @@
|
|||||||
|
|
||||||
#include "extensionmanagerwidget.h"
|
#include "extensionmanagerwidget.h"
|
||||||
|
|
||||||
#include "extensionmanagerconstants.h"
|
|
||||||
#include "extensionmanagertr.h"
|
#include "extensionmanagertr.h"
|
||||||
#include "extensionsbrowser.h"
|
#include "extensionsbrowser.h"
|
||||||
|
#include "extensionsmodel.h"
|
||||||
|
|
||||||
#include <coreplugin/coreconstants.h>
|
#include <coreplugin/coreconstants.h>
|
||||||
#include <coreplugin/icontext.h>
|
#include <coreplugin/icontext.h>
|
||||||
#include <coreplugin/icore.h>
|
#include <coreplugin/icore.h>
|
||||||
#include <coreplugin/iwelcomepage.h>
|
#include <coreplugin/iwelcomepage.h>
|
||||||
|
#include <coreplugin/plugininstallwizard.h>
|
||||||
#include <coreplugin/welcomepagehelper.h>
|
#include <coreplugin/welcomepagehelper.h>
|
||||||
|
|
||||||
|
#include <extensionsystem/pluginmanager.h>
|
||||||
#include <extensionsystem/pluginspec.h>
|
#include <extensionsystem/pluginspec.h>
|
||||||
|
|
||||||
|
#include <solutions/tasking/networkquery.h>
|
||||||
|
#include <solutions/tasking/tasktree.h>
|
||||||
|
#include <solutions/tasking/tasktreerunner.h>
|
||||||
|
|
||||||
#include <utils/algorithm.h>
|
#include <utils/algorithm.h>
|
||||||
|
#include <utils/fileutils.h>
|
||||||
|
#include <utils/hostosinfo.h>
|
||||||
#include <utils/icon.h>
|
#include <utils/icon.h>
|
||||||
|
#include <utils/infolabel.h>
|
||||||
#include <utils/layoutbuilder.h>
|
#include <utils/layoutbuilder.h>
|
||||||
|
#include <utils/networkaccessmanager.h>
|
||||||
#include <utils/stylehelper.h>
|
#include <utils/stylehelper.h>
|
||||||
|
#include <utils/temporarydirectory.h>
|
||||||
#include <utils/utilsicons.h>
|
#include <utils/utilsicons.h>
|
||||||
|
|
||||||
#include <QAction>
|
#include <QAction>
|
||||||
|
#include <QCheckBox>
|
||||||
|
#include <QMessageBox>
|
||||||
#include <QTextBrowser>
|
#include <QTextBrowser>
|
||||||
|
#include <QProgressDialog>
|
||||||
|
|
||||||
using namespace Core;
|
using namespace Core;
|
||||||
using namespace Utils;
|
using namespace Utils;
|
||||||
@@ -54,71 +68,163 @@ private:
|
|||||||
int m_width = 100;
|
int m_width = 100;
|
||||||
};
|
};
|
||||||
|
|
||||||
ExtensionManagerWidget::ExtensionManagerWidget()
|
class PluginStatusWidget : public QWidget
|
||||||
{
|
{
|
||||||
m_leftColumn = new ExtensionsBrowser;
|
public:
|
||||||
|
explicit PluginStatusWidget(QWidget *parent = nullptr)
|
||||||
|
: QWidget(parent)
|
||||||
|
{
|
||||||
|
m_label = new InfoLabel;
|
||||||
|
m_checkBox = new QCheckBox(Tr::tr("Load on Start"));
|
||||||
|
|
||||||
|
using namespace Layouting;
|
||||||
|
Column {
|
||||||
|
m_label,
|
||||||
|
m_checkBox,
|
||||||
|
}.attachTo(this);
|
||||||
|
|
||||||
|
connect(m_checkBox, &QCheckBox::clicked, this, [this](bool checked) {
|
||||||
|
ExtensionSystem::PluginSpec *spec = ExtensionsModel::pluginSpecForName(m_pluginName);
|
||||||
|
if (spec == nullptr)
|
||||||
|
return;
|
||||||
|
spec->setEnabledBySettings(checked);
|
||||||
|
ExtensionSystem::PluginManager::writeSettings();
|
||||||
|
});
|
||||||
|
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
void setPluginName(const QString &name)
|
||||||
|
{
|
||||||
|
m_pluginName = name;
|
||||||
|
update();
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
void update()
|
||||||
|
{
|
||||||
|
const ExtensionSystem::PluginSpec *spec = ExtensionsModel::pluginSpecForName(m_pluginName);
|
||||||
|
setVisible(spec != nullptr);
|
||||||
|
if (spec == nullptr)
|
||||||
|
return;
|
||||||
|
|
||||||
|
if (spec->hasError()) {
|
||||||
|
m_label->setType(InfoLabel::Error);
|
||||||
|
m_label->setText(Tr::tr("Error"));
|
||||||
|
} else if (spec->state() == ExtensionSystem::PluginSpec::Running) {
|
||||||
|
m_label->setType(InfoLabel::Ok);
|
||||||
|
m_label->setText(Tr::tr("Loaded"));
|
||||||
|
} else {
|
||||||
|
m_label->setType(InfoLabel::NotOk);
|
||||||
|
m_label->setText(Tr::tr("Not loaded"));
|
||||||
|
}
|
||||||
|
|
||||||
|
m_checkBox->setChecked(spec->isRequired() || spec->isEnabledBySettings());
|
||||||
|
m_checkBox->setEnabled(!spec->isRequired());
|
||||||
|
}
|
||||||
|
|
||||||
|
InfoLabel *m_label;
|
||||||
|
QCheckBox *m_checkBox;
|
||||||
|
QString m_pluginName;
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExtensionManagerWidgetPrivate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
QString currentItemName;
|
||||||
|
ExtensionsBrowser *leftColumn;
|
||||||
|
CollapsingWidget *secondaryDescriptionWidget;
|
||||||
|
QTextBrowser *primaryDescription;
|
||||||
|
QTextBrowser *secondaryDescription;
|
||||||
|
PluginStatusWidget *pluginStatus;
|
||||||
|
QAbstractButton *installButton;
|
||||||
|
PluginsData currentItemPlugins;
|
||||||
|
Tasking::TaskTreeRunner taskTreeRunner;
|
||||||
|
};
|
||||||
|
|
||||||
|
ExtensionManagerWidget::ExtensionManagerWidget(QWidget *parent)
|
||||||
|
: ResizeSignallingWidget(parent)
|
||||||
|
, d(new ExtensionManagerWidgetPrivate)
|
||||||
|
{
|
||||||
|
d->leftColumn = new ExtensionsBrowser;
|
||||||
|
|
||||||
auto descriptionColumns = new QWidget;
|
auto descriptionColumns = new QWidget;
|
||||||
|
|
||||||
m_secondarDescriptionWidget = new CollapsingWidget;
|
d->secondaryDescriptionWidget = new CollapsingWidget;
|
||||||
|
|
||||||
m_primaryDescription = new QTextBrowser;
|
d->primaryDescription = new QTextBrowser;
|
||||||
m_primaryDescription->setOpenExternalLinks(true);
|
d->primaryDescription->setOpenExternalLinks(true);
|
||||||
m_primaryDescription->setFrameStyle(QFrame::NoFrame);
|
d->primaryDescription->setFrameStyle(QFrame::NoFrame);
|
||||||
|
|
||||||
m_secondaryDescription = new QTextBrowser;
|
d->secondaryDescription = new QTextBrowser;
|
||||||
m_secondaryDescription->setFrameStyle(QFrame::NoFrame);
|
d->secondaryDescription->setFrameStyle(QFrame::NoFrame);
|
||||||
|
|
||||||
|
d->pluginStatus = new PluginStatusWidget;
|
||||||
|
|
||||||
|
d->installButton = new Button(Tr::tr("Install..."), Button::MediumPrimary);
|
||||||
|
d->installButton->hide();
|
||||||
|
|
||||||
using namespace Layouting;
|
using namespace Layouting;
|
||||||
Row {
|
Row {
|
||||||
WelcomePageHelpers::createRule(Qt::Vertical),
|
WelcomePageHelpers::createRule(Qt::Vertical),
|
||||||
m_secondaryDescription,
|
Column {
|
||||||
|
d->secondaryDescription,
|
||||||
|
d->pluginStatus,
|
||||||
|
d->installButton,
|
||||||
|
},
|
||||||
noMargin, spacing(0),
|
noMargin, spacing(0),
|
||||||
}.attachTo(m_secondarDescriptionWidget);
|
}.attachTo(d->secondaryDescriptionWidget);
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
WelcomePageHelpers::createRule(Qt::Vertical),
|
WelcomePageHelpers::createRule(Qt::Vertical),
|
||||||
Row {
|
Row {
|
||||||
m_primaryDescription,
|
d->primaryDescription,
|
||||||
noMargin,
|
noMargin,
|
||||||
},
|
},
|
||||||
m_secondarDescriptionWidget,
|
d->secondaryDescriptionWidget,
|
||||||
noMargin, spacing(0),
|
noMargin, spacing(0),
|
||||||
}.attachTo(descriptionColumns);
|
}.attachTo(descriptionColumns);
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
Space(StyleHelper::SpacingTokens::ExVPaddingGapXl),
|
Space(StyleHelper::SpacingTokens::ExVPaddingGapXl),
|
||||||
m_leftColumn,
|
d->leftColumn,
|
||||||
descriptionColumns,
|
descriptionColumns,
|
||||||
noMargin, spacing(0),
|
noMargin, spacing(0),
|
||||||
}.attachTo(this);
|
}.attachTo(this);
|
||||||
|
|
||||||
WelcomePageHelpers::setBackgroundColor(this, Theme::Token_Background_Default);
|
WelcomePageHelpers::setBackgroundColor(this, Theme::Token_Background_Default);
|
||||||
|
|
||||||
connect(m_leftColumn, &ExtensionsBrowser::itemSelected,
|
connect(d->leftColumn, &ExtensionsBrowser::itemSelected,
|
||||||
this, &ExtensionManagerWidget::updateView);
|
this, &ExtensionManagerWidget::updateView);
|
||||||
connect(this, &ResizeSignallingWidget::resized, this, [this](const QSize &size) {
|
connect(this, &ResizeSignallingWidget::resized, this, [this](const QSize &size) {
|
||||||
const int intendedLeftColumnWidth = size.width() - 580;
|
const int intendedLeftColumnWidth = size.width() - 580;
|
||||||
m_leftColumn->adjustToWidth(intendedLeftColumnWidth);
|
d->leftColumn->adjustToWidth(intendedLeftColumnWidth);
|
||||||
const bool secondaryDescriptionVisible = size.width() > 970;
|
const bool secondaryDescriptionVisible = size.width() > 970;
|
||||||
const int secondaryDescriptionWidth = secondaryDescriptionVisible ? 264 : 0;
|
const int secondaryDescriptionWidth = secondaryDescriptionVisible ? 264 : 0;
|
||||||
m_secondarDescriptionWidget->setWidth(secondaryDescriptionWidth);
|
d->secondaryDescriptionWidget->setWidth(secondaryDescriptionWidth);
|
||||||
});
|
});
|
||||||
updateView({}, {});
|
connect(d->installButton, &QAbstractButton::pressed, this, [this]() {
|
||||||
|
fetchAndInstallPlugin(QUrl::fromUserInput(d->currentItemPlugins.constFirst().second));
|
||||||
|
});
|
||||||
|
updateView({});
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExtensionManagerWidget::updateView(const QModelIndex ¤t,
|
ExtensionManagerWidget::~ExtensionManagerWidget()
|
||||||
[[maybe_unused]] const QModelIndex &previous)
|
{
|
||||||
|
delete d;
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExtensionManagerWidget::updateView(const QModelIndex ¤t)
|
||||||
{
|
{
|
||||||
const QString h5Css =
|
const QString h5Css =
|
||||||
StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH5))
|
StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH5))
|
||||||
+ "; margin-top: 28px;";
|
+ "; margin-top: 0px;";
|
||||||
const QString h6Css =
|
const QString h6Css =
|
||||||
StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH6))
|
StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH6))
|
||||||
+ "; margin-top: 28px;";
|
+ "; margin-top: 28px;";
|
||||||
const QString h6CapitalCss =
|
const QString h6CapitalCss =
|
||||||
StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH6Capital))
|
StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH6Capital))
|
||||||
+ QString::fromLatin1("; color: %1;")
|
+ QString::fromLatin1("; margin-top: 0px; color: %1;")
|
||||||
.arg(creatorColor(Theme::Token_Text_Muted).name());
|
.arg(creatorColor(Theme::Token_Text_Muted).name());
|
||||||
const QString bodyStyle = QString::fromLatin1("color: %1; background-color: %2; "
|
const QString bodyStyle = QString::fromLatin1("color: %1; background-color: %2; "
|
||||||
"margin-left: %3px; margin-right: %3px;")
|
"margin-left: %3px; margin-right: %3px;")
|
||||||
@@ -127,7 +233,7 @@ void ExtensionManagerWidget::updateView(const QModelIndex ¤t,
|
|||||||
.arg(StyleHelper::SpacingTokens::ExVPaddingGapXl);
|
.arg(StyleHelper::SpacingTokens::ExVPaddingGapXl);
|
||||||
const QString htmlStart = QString(R"(
|
const QString htmlStart = QString(R"(
|
||||||
<html>
|
<html>
|
||||||
<body style="%1">
|
<body style="%1"><br/>
|
||||||
)").arg(bodyStyle);
|
)").arg(bodyStyle);
|
||||||
const QString htmlEnd = QString(R"(
|
const QString htmlEnd = QString(R"(
|
||||||
</body></html>
|
</body></html>
|
||||||
@@ -135,114 +241,129 @@ void ExtensionManagerWidget::updateView(const QModelIndex ¤t,
|
|||||||
|
|
||||||
if (!current.isValid()) {
|
if (!current.isValid()) {
|
||||||
const QString emptyHtml = htmlStart + htmlEnd;
|
const QString emptyHtml = htmlStart + htmlEnd;
|
||||||
m_primaryDescription->setText(emptyHtml);
|
d->primaryDescription->setText(emptyHtml);
|
||||||
m_secondaryDescription->setText(emptyHtml);
|
d->secondaryDescription->setText(emptyHtml);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const ItemData data = itemData(current);
|
d->currentItemName = current.data().toString();
|
||||||
const bool isPack = data.type == ItemTypePack;
|
const bool isPack = current.data(RoleItemType) == ItemTypePack;
|
||||||
const ExtensionSystem::PluginSpec *extension = data.plugins.first();
|
d->pluginStatus->setPluginName(isPack ? QString() : d->currentItemName);
|
||||||
|
const bool isRemotePlugin = !(isPack || ExtensionsModel::pluginSpecForName(d->currentItemName));
|
||||||
|
d->currentItemPlugins = current.data(RolePlugins).value<PluginsData>();
|
||||||
|
d->installButton->setVisible(isRemotePlugin && !d->currentItemPlugins.empty());
|
||||||
|
if (!d->currentItemPlugins.empty())
|
||||||
|
d->installButton->setToolTip(d->currentItemPlugins.constFirst().second);
|
||||||
|
|
||||||
{
|
{
|
||||||
const QString shortDescription =
|
|
||||||
isPack ? QLatin1String("Short description for pack ") + data.name
|
|
||||||
: extension->description();
|
|
||||||
QString longDescription =
|
|
||||||
isPack ? QLatin1String("Some longer text that describes the purpose and functionality "
|
|
||||||
"of the extensions that are part of pack ") + data.name
|
|
||||||
: extension->longDescription();
|
|
||||||
longDescription.replace("\n", "<br/>");
|
|
||||||
const FilePath location = isPack ? extension->location() : extension->filePath();
|
|
||||||
|
|
||||||
QString description = htmlStart;
|
QString description = htmlStart;
|
||||||
|
|
||||||
description.append(QString(R"(
|
QString descriptionHtml;
|
||||||
<div style="%1"><br/>%2</div>
|
{
|
||||||
<p>%3</p>
|
const TextData textData = current.data(RoleDescriptionText).value<TextData>();
|
||||||
)").arg(h5Css)
|
for (const TextData::Type &text : textData) {
|
||||||
.arg(shortDescription)
|
if (text.second.isEmpty())
|
||||||
.arg(longDescription));
|
continue;
|
||||||
|
const QString paragraph =
|
||||||
|
QString::fromLatin1("<div style=\"%1\">%2</div><p>%3</p>")
|
||||||
|
.arg(descriptionHtml.isEmpty() ? h5Css : h6Css)
|
||||||
|
.arg(text.first)
|
||||||
|
.arg(text.second.join("<br/>"));
|
||||||
|
descriptionHtml.append(paragraph);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
description.append(descriptionHtml);
|
||||||
|
|
||||||
description.append(QString(R"(
|
description.append(QString::fromLatin1("<div style=\"%1\">%2</div>")
|
||||||
<div style="%1">%2</div>
|
.arg(h6Css)
|
||||||
<p>%3</p>
|
.arg(Tr::tr("More information")));
|
||||||
)").arg(h6Css)
|
const LinksData linksData = current.data(RoleDescriptionLinks).value<LinksData>();
|
||||||
.arg(Tr::tr("Get started"))
|
if (!linksData.isEmpty()) {
|
||||||
.arg(Tr::tr("Install the extension from above. Installation starts automatically. "
|
QString linksHtml;
|
||||||
"You can always uninstall the extension afterwards.")));
|
const QStringList links = Utils::transform(linksData, [](const LinksData::Type &link) {
|
||||||
|
const QString anchor = link.first.isEmpty() ? link.second : link.first;
|
||||||
description.append(QString(R"(
|
return QString::fromLatin1("<a href=\"%1\">%2 ></a>")
|
||||||
<div style="%1">%2</div>
|
.arg(link.second).arg(anchor);
|
||||||
<p>
|
});
|
||||||
<a href="%3">%4 ></a>
|
linksHtml = links.join("<br/>");
|
||||||
<br/>
|
description.append(QString::fromLatin1("<p>%1</p>").arg(linksHtml));
|
||||||
<a href="%5">%6 ></a>
|
}
|
||||||
</p>
|
|
||||||
)").arg(h6Css)
|
|
||||||
.arg(Tr::tr("More information"))
|
|
||||||
.arg(Tr::tr("Online Documentation"))
|
|
||||||
.arg("https://doc.qt.io/qtcreator/")
|
|
||||||
.arg(Tr::tr("Tutorials"))
|
|
||||||
.arg("https://doc.qt.io/qtcreator/creator-tutorials.html"));
|
|
||||||
|
|
||||||
|
const ImagesData imagesData = current.data(RoleDescriptionImages).value<ImagesData>();
|
||||||
|
if (!imagesData.isEmpty()) {
|
||||||
const QString examplesBoxCss =
|
const QString examplesBoxCss =
|
||||||
QString::fromLatin1("height: 168px; background-color: %1; ")
|
QString::fromLatin1("height: 168px; background-color: %1; ")
|
||||||
.arg(creatorColor(Theme::Token_Background_Default).name());
|
.arg(creatorTheme()->color(Theme::Token_Background_Default).name());
|
||||||
description.append(QString(R"(
|
description.append(QString(R"(
|
||||||
|
<br/>
|
||||||
<div style="%1">%2</div>
|
<div style="%1">%2</div>
|
||||||
<p style="%3">
|
<p style="%3">
|
||||||
<br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/>
|
<br/><br/><br/><br/><br/>
|
||||||
|
TODO: Load imagea asynchronously, and show them in a QLabel.
|
||||||
|
Also Use QMovie for animated images.
|
||||||
|
<br/><br/><br/><br/><br/>
|
||||||
</p>
|
</p>
|
||||||
)").arg(h6CapitalCss)
|
)").arg(h6CapitalCss)
|
||||||
.arg(Tr::tr("Examples"))
|
.arg(Tr::tr("Examples"))
|
||||||
.arg(examplesBoxCss));
|
.arg(examplesBoxCss));
|
||||||
|
}
|
||||||
|
|
||||||
|
// Library details vanished from the Figma designs. The data is available, though.
|
||||||
|
const bool showDetails = false;
|
||||||
|
if (showDetails) {
|
||||||
const QString captionStrongCss = StyleHelper::fontToCssProperties(
|
const QString captionStrongCss = StyleHelper::fontToCssProperties(
|
||||||
StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong));
|
StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong));
|
||||||
|
const QLocale locale;
|
||||||
|
const uint size = current.data(RoleSize).toUInt();
|
||||||
|
const QString sizeFmt = locale.formattedDataSize(size);
|
||||||
|
const FilePath location = FilePath::fromVariant(current.data(RoleLocation));
|
||||||
|
const QString version = current.data(RoleVersion).toString();
|
||||||
description.append(QString(R"(
|
description.append(QString(R"(
|
||||||
<div style="%1">%2</div>
|
<div style="%1">%2</div>
|
||||||
<p>
|
<p>
|
||||||
<table>
|
<table>
|
||||||
<tr><td style="%3">%4</td><td>%5</td></tr>
|
<tr><td style="%3">%4</td><td>%5</td></tr>
|
||||||
<tr><td style="%3">%6</td><td>%7</td></tr>
|
<tr><td style="%3">%6</td><td>%7</td></tr>
|
||||||
<tr><td style="%3">%8</td><td>%9</td></tr>
|
|
||||||
</table>
|
|
||||||
</p>
|
|
||||||
)").arg(h6Css)
|
)").arg(h6Css)
|
||||||
.arg(Tr::tr("Extension library details"))
|
.arg(Tr::tr("Extension library details"))
|
||||||
.arg(captionStrongCss)
|
.arg(captionStrongCss)
|
||||||
.arg(Tr::tr("Size"))
|
.arg(Tr::tr("Size"))
|
||||||
.arg("547 MB")
|
.arg(sizeFmt)
|
||||||
.arg(Tr::tr("Version"))
|
.arg(Tr::tr("Version"))
|
||||||
.arg(extension->version())
|
.arg(version));
|
||||||
.arg(Tr::tr("Location"))
|
if (!location.isEmpty()) {
|
||||||
.arg(location.toUserOutput()));
|
const QString locationFmt =
|
||||||
|
HostOsInfo::isWindowsHost() ? location.toUserOutput()
|
||||||
|
: location.withTildeHomePath();
|
||||||
|
description.append(QString(R"(
|
||||||
|
<tr><td style="%3">%1</td><td>%2</td></tr>
|
||||||
|
)").arg(Tr::tr("Location"))
|
||||||
|
.arg(locationFmt));
|
||||||
|
}
|
||||||
|
description.append(QString(R"(
|
||||||
|
</table>
|
||||||
|
</p>
|
||||||
|
)"));
|
||||||
|
}
|
||||||
|
|
||||||
description.append(htmlEnd);
|
description.append(htmlEnd);
|
||||||
m_primaryDescription->setText(description);
|
d->primaryDescription->setText(description);
|
||||||
}
|
}
|
||||||
|
|
||||||
{
|
{
|
||||||
QString description = htmlStart;
|
QString description = htmlStart;
|
||||||
|
|
||||||
description.append(QString(R"(
|
description.append(QString(R"(
|
||||||
<p style="%1"><br/>%2</p>
|
<p style="%1">%2</p>
|
||||||
)").arg(h6CapitalCss)
|
)").arg(h6CapitalCss)
|
||||||
.arg(Tr::tr("Extension details")));
|
.arg(Tr::tr("Extension details")));
|
||||||
|
|
||||||
description.append(QString(R"(
|
const QStringList tags = current.data(RoleTags).toStringList();
|
||||||
<div style="%1">%2</div>
|
if (!tags.isEmpty()) {
|
||||||
<p>%3</p>
|
|
||||||
)").arg(h6Css)
|
|
||||||
.arg(Tr::tr("Released"))
|
|
||||||
.arg("23.5.2023"));
|
|
||||||
|
|
||||||
const QString tagTemplate = QString(R"(
|
const QString tagTemplate = QString(R"(
|
||||||
<td style="border: 1px solid %1; padding: 3px; ">%2</td>
|
<td style="border: 1px solid %1; padding: 3px; ">%2</td>
|
||||||
)").arg(creatorColor(Theme::Token_Stroke_Subtle).name());
|
)").arg(creatorTheme()->color(Theme::Token_Stroke_Subtle).name());
|
||||||
const QStringList tags = Utils::transform(data.tags,
|
const QStringList tagsFmt = Utils::transform(tags, [&tagTemplate](const QString &tag) {
|
||||||
[&tagTemplate] (const QString &tag) {
|
|
||||||
return tagTemplate.arg(tag);
|
return tagTemplate.arg(tag);
|
||||||
});
|
});
|
||||||
description.append(QString(R"(
|
description.append(QString(R"(
|
||||||
@@ -250,46 +371,106 @@ void ExtensionManagerWidget::updateView(const QModelIndex ¤t,
|
|||||||
<p>%3</p>
|
<p>%3</p>
|
||||||
)").arg(h6Css)
|
)").arg(h6Css)
|
||||||
.arg(Tr::tr("Related tags"))
|
.arg(Tr::tr("Related tags"))
|
||||||
.arg(tags.join(" ")));
|
.arg(tagsFmt.join(" ")));
|
||||||
|
}
|
||||||
|
|
||||||
|
const QStringList platforms = current.data(RolePlatforms).toStringList();
|
||||||
|
if (!platforms.isEmpty()) {
|
||||||
description.append(QString(R"(
|
description.append(QString(R"(
|
||||||
<div style="%1">%2</div>
|
<div style="%1">%2</div>
|
||||||
<p>
|
<p>%3</p>
|
||||||
macOS<br/>
|
|
||||||
Windows<br/>
|
|
||||||
Linux
|
|
||||||
</p>
|
|
||||||
)").arg(h6Css)
|
)").arg(h6Css)
|
||||||
.arg(Tr::tr("Platforms")));
|
.arg(Tr::tr("Platforms"))
|
||||||
|
.arg(platforms.join("<br/>")));
|
||||||
QStringList dependencies;
|
|
||||||
for (const ExtensionSystem::PluginSpec *spec : data.plugins) {
|
|
||||||
dependencies.append(Utils::transform(spec->dependencies(),
|
|
||||||
&ExtensionSystem::PluginDependency::toString));
|
|
||||||
}
|
}
|
||||||
dependencies.removeDuplicates();
|
|
||||||
dependencies.sort();
|
const QStringList dependencies = current.data(RoleDependencies).toStringList();
|
||||||
|
if (!dependencies.isEmpty()) {
|
||||||
|
const QString dependenciesFmt = dependencies.join("<br/>");
|
||||||
description.append(QString(R"(
|
description.append(QString(R"(
|
||||||
<div style="%1">%2</div>
|
<div style="%1">%2</div>
|
||||||
<p>%3</p>
|
<p>%3</p>
|
||||||
)").arg(h6Css)
|
)").arg(h6Css)
|
||||||
.arg(Tr::tr("Dependencies"))
|
.arg(Tr::tr("Dependencies"))
|
||||||
.arg(dependencies.isEmpty() ? "-" : dependencies.join("<br/>")));
|
.arg(dependenciesFmt));
|
||||||
|
}
|
||||||
|
|
||||||
if (isPack) {
|
if (isPack) {
|
||||||
const QStringList extensions = Utils::transform(data.plugins,
|
const PluginsData plugins = current.data(RolePlugins).value<PluginsData>();
|
||||||
&ExtensionSystem::PluginSpec::name);
|
const QStringList extensions = Utils::transform(plugins,
|
||||||
|
&QPair<QString, QString>::first);
|
||||||
|
const QString extensionsFmt = extensions.join("<br/>");
|
||||||
description.append(QString(R"(
|
description.append(QString(R"(
|
||||||
<div style="%1">%2</div>
|
<div style="%1">%2</div>
|
||||||
<p>%3</p>
|
<p>%3</p>
|
||||||
)").arg(h6Css)
|
)").arg(h6Css)
|
||||||
.arg(Tr::tr("Extensions in pack"))
|
.arg(Tr::tr("Extensions in pack"))
|
||||||
.arg(extensions.join("<br/>")));
|
.arg(extensionsFmt));
|
||||||
}
|
}
|
||||||
|
|
||||||
description.append(htmlEnd);
|
description.append(htmlEnd);
|
||||||
m_secondaryDescription->setText(description);
|
d->secondaryDescription->setText(description);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ExtensionManagerWidget::fetchAndInstallPlugin(const QUrl &url)
|
||||||
|
{
|
||||||
|
using namespace Tasking;
|
||||||
|
|
||||||
|
struct StorageStruct
|
||||||
|
{
|
||||||
|
StorageStruct() {
|
||||||
|
progressDialog.reset(new QProgressDialog(Tr::tr("Downloading Plugin..."),
|
||||||
|
Tr::tr("Cancel"), 0, 0,
|
||||||
|
Core::ICore::dialogParent()));
|
||||||
|
progressDialog->setWindowModality(Qt::ApplicationModal);
|
||||||
|
progressDialog->setFixedSize(progressDialog->sizeHint());
|
||||||
|
progressDialog->setAutoClose(false);
|
||||||
|
progressDialog->show(); // TODO: Should not be needed. Investigate possible QT_BUG
|
||||||
|
}
|
||||||
|
std::unique_ptr<QProgressDialog> progressDialog;
|
||||||
|
QByteArray packageData;
|
||||||
|
QUrl url;
|
||||||
|
};
|
||||||
|
Storage<StorageStruct> storage;
|
||||||
|
|
||||||
|
const auto onQuerySetup = [url, storage](NetworkQuery &query) {
|
||||||
|
storage->url = url;
|
||||||
|
query.setRequest(QNetworkRequest(url));
|
||||||
|
query.setNetworkAccessManager(NetworkAccessManager::instance());
|
||||||
|
};
|
||||||
|
const auto onQueryDone = [storage](const NetworkQuery &query, DoneWith result) {
|
||||||
|
storage->progressDialog->close();
|
||||||
|
if (result == DoneWith::Success) {
|
||||||
|
storage->packageData = query.reply()->readAll();
|
||||||
|
} else {
|
||||||
|
QMessageBox::warning(
|
||||||
|
ICore::dialogParent(),
|
||||||
|
Tr::tr("Download Error"),
|
||||||
|
Tr::tr("Could not download Plugin") + "\n\n" + storage->url.toString() + "\n\n"
|
||||||
|
+ Tr::tr("Code: %1.").arg(query.reply()->error()));
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto onPluginInstallation = [storage]() {
|
||||||
|
if (storage->packageData.isEmpty())
|
||||||
|
return;
|
||||||
|
const FilePath source = FilePath::fromUrl(storage->url);
|
||||||
|
TempFileSaver saver(TemporaryDirectory::masterDirectoryPath()
|
||||||
|
+ "/XXXXXX" + source.fileName());
|
||||||
|
|
||||||
|
saver.write(storage->packageData);
|
||||||
|
if (saver.finalize(ICore::dialogParent()))
|
||||||
|
executePluginInstallWizard(saver.filePath());;
|
||||||
|
};
|
||||||
|
|
||||||
|
Group group{
|
||||||
|
storage,
|
||||||
|
NetworkQueryTask{onQuerySetup, onQueryDone},
|
||||||
|
onGroupDone(onPluginInstallation),
|
||||||
|
};
|
||||||
|
|
||||||
|
d->taskTreeRunner.start(group);
|
||||||
|
}
|
||||||
|
|
||||||
} // ExtensionManager::Internal
|
} // ExtensionManager::Internal
|
||||||
|
@@ -3,27 +3,19 @@
|
|||||||
|
|
||||||
#include <coreplugin/welcomepagehelper.h>
|
#include <coreplugin/welcomepagehelper.h>
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
class QTextBrowser;
|
|
||||||
QT_END_NAMESPACE
|
|
||||||
|
|
||||||
namespace ExtensionManager::Internal {
|
namespace ExtensionManager::Internal {
|
||||||
|
|
||||||
class CollapsingWidget;
|
|
||||||
class ExtensionsBrowser;
|
|
||||||
|
|
||||||
class ExtensionManagerWidget final : public Core::ResizeSignallingWidget
|
class ExtensionManagerWidget final : public Core::ResizeSignallingWidget
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
explicit ExtensionManagerWidget();
|
explicit ExtensionManagerWidget(QWidget *parent = nullptr);
|
||||||
|
~ExtensionManagerWidget();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void updateView(const QModelIndex ¤t, [[maybe_unused]] const QModelIndex &previous);
|
void updateView(const QModelIndex ¤t);
|
||||||
|
void fetchAndInstallPlugin(const QUrl &url);
|
||||||
|
|
||||||
ExtensionsBrowser *m_leftColumn;
|
class ExtensionManagerWidgetPrivate *d = nullptr;
|
||||||
CollapsingWidget *m_secondarDescriptionWidget;
|
|
||||||
QTextBrowser *m_primaryDescription;
|
|
||||||
QTextBrowser *m_secondaryDescription;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // ExtensionManager::Internal
|
} // ExtensionManager::Internal
|
||||||
|
@@ -4,10 +4,17 @@
|
|||||||
#include "extensionsbrowser.h"
|
#include "extensionsbrowser.h"
|
||||||
|
|
||||||
#include "extensionmanagertr.h"
|
#include "extensionmanagertr.h"
|
||||||
|
#include "extensionsmodel.h"
|
||||||
|
#include "utils/hostosinfo.h"
|
||||||
|
|
||||||
|
#ifdef WITH_TESTS
|
||||||
|
#include "extensionmanager_test.h"
|
||||||
|
#endif // WITH_TESTS
|
||||||
|
|
||||||
#include <coreplugin/coreconstants.h>
|
#include <coreplugin/coreconstants.h>
|
||||||
#include <coreplugin/icontext.h>
|
#include <coreplugin/icontext.h>
|
||||||
#include <coreplugin/icore.h>
|
#include <coreplugin/icore.h>
|
||||||
|
#include <coreplugin/plugininstallwizard.h>
|
||||||
#include <coreplugin/welcomepagehelper.h>
|
#include <coreplugin/welcomepagehelper.h>
|
||||||
|
|
||||||
#include <extensionsystem/iplugin.h>
|
#include <extensionsystem/iplugin.h>
|
||||||
@@ -15,9 +22,14 @@
|
|||||||
#include <extensionsystem/pluginview.h>
|
#include <extensionsystem/pluginview.h>
|
||||||
#include <extensionsystem/pluginmanager.h>
|
#include <extensionsystem/pluginmanager.h>
|
||||||
|
|
||||||
|
#include <solutions/tasking/networkquery.h>
|
||||||
|
#include <solutions/tasking/tasktree.h>
|
||||||
|
#include <solutions/tasking/tasktreerunner.h>
|
||||||
|
|
||||||
#include <utils/fancylineedit.h>
|
#include <utils/fancylineedit.h>
|
||||||
#include <utils/icon.h>
|
#include <utils/icon.h>
|
||||||
#include <utils/layoutbuilder.h>
|
#include <utils/layoutbuilder.h>
|
||||||
|
#include <utils/networkaccessmanager.h>
|
||||||
#include <utils/stylehelper.h>
|
#include <utils/stylehelper.h>
|
||||||
|
|
||||||
#include <QItemDelegate>
|
#include <QItemDelegate>
|
||||||
@@ -26,7 +38,6 @@
|
|||||||
#include <QMessageBox>
|
#include <QMessageBox>
|
||||||
#include <QPainter>
|
#include <QPainter>
|
||||||
#include <QPainterPath>
|
#include <QPainterPath>
|
||||||
#include <QStandardItemModel>
|
|
||||||
#include <QStyle>
|
#include <QStyle>
|
||||||
|
|
||||||
using namespace ExtensionSystem;
|
using namespace ExtensionSystem;
|
||||||
@@ -37,277 +48,16 @@ namespace ExtensionManager::Internal {
|
|||||||
|
|
||||||
Q_LOGGING_CATEGORY(browserLog, "qtc.extensionmanager.browser", QtWarningMsg)
|
Q_LOGGING_CATEGORY(browserLog, "qtc.extensionmanager.browser", QtWarningMsg)
|
||||||
|
|
||||||
using Tags = QStringList;
|
|
||||||
|
|
||||||
constexpr QSize itemSize = {330, 86};
|
constexpr QSize itemSize = {330, 86};
|
||||||
constexpr int gapSize = StyleHelper::SpacingTokens::ExVPaddingGapXl;
|
constexpr int gapSize = StyleHelper::SpacingTokens::ExVPaddingGapXl;
|
||||||
constexpr QSize cellSize = {itemSize.width() + gapSize, itemSize.height() + gapSize};
|
constexpr QSize cellSize = {itemSize.width() + gapSize, itemSize.height() + gapSize};
|
||||||
|
|
||||||
enum Role {
|
|
||||||
RoleName = Qt::UserRole,
|
|
||||||
RoleItemType,
|
|
||||||
RoleTags,
|
|
||||||
RolePluginSpecs,
|
|
||||||
RoleSearchText,
|
|
||||||
};
|
|
||||||
|
|
||||||
ItemData itemData(const QModelIndex &index)
|
|
||||||
{
|
|
||||||
return {
|
|
||||||
index.data(RoleName).toString(),
|
|
||||||
index.data(RoleItemType).value<ItemType>(),
|
|
||||||
index.data(RoleTags).toStringList(),
|
|
||||||
index.data(RolePluginSpecs).value<PluginSpecList>(),
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
static QColor colorForExtensionName(const QString &name)
|
static QColor colorForExtensionName(const QString &name)
|
||||||
{
|
{
|
||||||
const size_t hash = qHash(name);
|
const size_t hash = qHash(name);
|
||||||
return QColor::fromHsv(hash % 360, 180, 110);
|
return QColor::fromHsv(hash % 360, 180, 110);
|
||||||
}
|
}
|
||||||
|
|
||||||
static QStandardItemModel *extensionsModel()
|
|
||||||
{
|
|
||||||
// The new extensions concept renames plugins to extensions and adds "packs" which are
|
|
||||||
// groups of extensions.
|
|
||||||
//
|
|
||||||
// TODO: The "meta data" here which is injected into the model is only a place holder that
|
|
||||||
// helps exploring the upcoming extensions concept.
|
|
||||||
//
|
|
||||||
// Before this loses the WIP prefix, we should at least have a concrete idea of how the data
|
|
||||||
// is structured and where it lives. Ideally, it continues to reside exclusively in the
|
|
||||||
// extension meta data.
|
|
||||||
//
|
|
||||||
// The grouping of extensions into packs could be done via extension tag. Extensions and will
|
|
||||||
// receive tags and if possible screen shots.
|
|
||||||
// Packs will also have a complete set of meta data. That could be an accumulation of the data
|
|
||||||
// of the contained extensions. Or simply the data from the "first" extension in a pack.
|
|
||||||
|
|
||||||
static const char tagBuildTools[] = "Build Tools";
|
|
||||||
static const char tagCodeAnalyzing[] = "Code Analyzing";
|
|
||||||
static const char tagConnectivity[] = "Connectivity";
|
|
||||||
static const char tagCore[] = "Core";
|
|
||||||
static const char tagCpp[] = "C++";
|
|
||||||
static const char tagEditorConvenience[] = "Editor Convenience";
|
|
||||||
static const char tagEditor[] = "Editor";
|
|
||||||
static const char tagEssentials[] = "Essentials";
|
|
||||||
static const char tagGlsl[] = "GLSL";
|
|
||||||
static const char tagPackageManager[] = "Package Manager";
|
|
||||||
static const char tagPlatformSupport[] = "Platform Support";
|
|
||||||
static const char tagProgrammingLanguage[] = "Programming Language";
|
|
||||||
static const char tagPython[] = "Python";
|
|
||||||
static const char tagQml[] = "QML";
|
|
||||||
static const char tagQuick[] = "Quick";
|
|
||||||
static const char tagService[] = "Service";
|
|
||||||
static const char tagTestAutomation[] = "Test Automation";
|
|
||||||
static const char tagUiEditor[] = "Visual UI Editor" ;
|
|
||||||
static const char tagVersionControl[] = "Version Control";
|
|
||||||
static const char tagVisualEditor[] = "Visual editor";
|
|
||||||
static const char tagWidgets[] = "Widgets";
|
|
||||||
|
|
||||||
static const char tagTagUndefined[] = "Tag undefined";
|
|
||||||
|
|
||||||
static const struct {
|
|
||||||
const QString name;
|
|
||||||
const QStringList extensions;
|
|
||||||
const Tags tags;
|
|
||||||
} packs[] = {
|
|
||||||
{"Core",
|
|
||||||
{"Core", "Help", "ProjectExplorer", "TextEditor", "Welcome", "GenericProjectManager",
|
|
||||||
"QtSupport"},
|
|
||||||
{tagCore}
|
|
||||||
},
|
|
||||||
{"Core (from Installer)",
|
|
||||||
{"LicenseChecker", "Marketplace", "UpdateInfo"},
|
|
||||||
{tagCore}
|
|
||||||
},
|
|
||||||
{"Essentials",
|
|
||||||
{"Bookmarks", "BinEditor", "Debugger", "DiffEditor", "ImageViewer", "Macros",
|
|
||||||
"LanguageClient", "ResourceEditor"},
|
|
||||||
{tagEssentials}
|
|
||||||
},
|
|
||||||
{"C++ Language support",
|
|
||||||
{"ClangCodeModel", "ClangFormat", "ClassView", "CppEditor"},
|
|
||||||
{tagProgrammingLanguage, tagCpp}
|
|
||||||
},
|
|
||||||
{"QML Language Support (Qt Quick libraries)",
|
|
||||||
{"QmlJSEditor", "QmlJSTools", "QmlPreview", "QmlProfiler", "QmlProjectManager"},
|
|
||||||
{tagProgrammingLanguage, tagQml}
|
|
||||||
},
|
|
||||||
{"Visual QML UI Editor",
|
|
||||||
{"QmlDesigner", "QmlDesignerBase"},
|
|
||||||
{tagUiEditor, tagQml, tagQuick}
|
|
||||||
},
|
|
||||||
{"Visual C++ Widgets UI Editor",
|
|
||||||
{"Designer"},
|
|
||||||
{tagUiEditor, tagCpp, tagWidgets}
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
static const struct {
|
|
||||||
const QString name;
|
|
||||||
const Tags tags;
|
|
||||||
} extensions[] = {
|
|
||||||
{"GLSLEditor", {tagProgrammingLanguage, tagGlsl}},
|
|
||||||
{"Nim", {tagProgrammingLanguage}},
|
|
||||||
{"Python", {tagProgrammingLanguage, tagPython}},
|
|
||||||
{"Haskell", {tagProgrammingLanguage}},
|
|
||||||
|
|
||||||
{"ModelEditor", {tagVisualEditor}},
|
|
||||||
{"ScxmlEditor", {tagVisualEditor}},
|
|
||||||
|
|
||||||
{"Bazaar", {tagVersionControl}},
|
|
||||||
{"CVS", {tagVersionControl}},
|
|
||||||
{"ClearCase", {tagVersionControl}},
|
|
||||||
{"Fossil", {tagVersionControl}},
|
|
||||||
{"Git", {tagVersionControl}},
|
|
||||||
{"Mercurial", {tagVersionControl}},
|
|
||||||
{"Perforce", {tagVersionControl}},
|
|
||||||
{"Subversion", {tagVersionControl}},
|
|
||||||
{"VcsBase", {tagVersionControl}},
|
|
||||||
{"GitLab", {tagVersionControl, tagService}},
|
|
||||||
|
|
||||||
{"AutoTest", {tagTestAutomation}},
|
|
||||||
{"Squish", {tagTestAutomation}},
|
|
||||||
{"Coco", {tagTestAutomation}},
|
|
||||||
|
|
||||||
{"Vcpkg", {tagPackageManager}},
|
|
||||||
{"Conan", {tagPackageManager}},
|
|
||||||
|
|
||||||
{"Copilot", {tagEditorConvenience}},
|
|
||||||
{"EmacsKeys", {tagEditorConvenience}},
|
|
||||||
{"FakeVim", {tagEditorConvenience}},
|
|
||||||
{"Terminal", {tagEditorConvenience}},
|
|
||||||
{"Todo", {tagEditorConvenience}},
|
|
||||||
{"CodePaster", {tagEditorConvenience}},
|
|
||||||
{"Beautifier", {tagEditorConvenience}},
|
|
||||||
|
|
||||||
{"SerialTerminal", {tagConnectivity}},
|
|
||||||
|
|
||||||
{"SilverSearcher", {tagEditor}},
|
|
||||||
|
|
||||||
{"AutotoolsProjectManager", {tagBuildTools}},
|
|
||||||
{"CMakeProjectManager", {tagBuildTools}},
|
|
||||||
{"CompilationDatabaseProjectManager", {tagBuildTools}},
|
|
||||||
{"IncrediBuild", {tagBuildTools}},
|
|
||||||
{"MesonProjectManager", {tagBuildTools}},
|
|
||||||
{"QbsProjectManager", {tagBuildTools}},
|
|
||||||
{"QmakeProjectManager", {tagBuildTools}},
|
|
||||||
|
|
||||||
{"Axivion", {tagCodeAnalyzing}},
|
|
||||||
{"ClangTools", {tagCodeAnalyzing}},
|
|
||||||
{"Cppcheck", {tagCodeAnalyzing}},
|
|
||||||
{"CtfVisualizer", {tagCodeAnalyzing}},
|
|
||||||
{"PerfProfiler", {tagCodeAnalyzing}},
|
|
||||||
{"Valgrind", {tagCodeAnalyzing}},
|
|
||||||
|
|
||||||
{"Android", {tagPlatformSupport}},
|
|
||||||
{"BareMetal", {tagPlatformSupport}},
|
|
||||||
{"Boot2Qt", {tagPlatformSupport}},
|
|
||||||
{"Ios", {tagPlatformSupport}},
|
|
||||||
{"McuSupport", {tagPlatformSupport}},
|
|
||||||
{"Qnx", {tagPlatformSupport}},
|
|
||||||
{"RemoteLinux", {tagPlatformSupport}},
|
|
||||||
{"SafeRenderer", {tagPlatformSupport}},
|
|
||||||
{"VxWorks", {tagPlatformSupport}},
|
|
||||||
{"WebAssembly", {tagPlatformSupport}},
|
|
||||||
{"Docker", {tagPlatformSupport}},
|
|
||||||
|
|
||||||
// Missing in Kimmo's excel sheet:
|
|
||||||
{"CompilerExplorer", {tagTagUndefined}},
|
|
||||||
{"ExtensionManager", {tagTagUndefined}},
|
|
||||||
{"ScreenRecorder", {tagTagUndefined}},
|
|
||||||
};
|
|
||||||
|
|
||||||
QList<QStandardItem*> items;
|
|
||||||
QStringList expectedExtensions;
|
|
||||||
QStringList unexpectedExtensions;
|
|
||||||
QHash<const QString, const PluginSpec*> installedPlugins;
|
|
||||||
for (const PluginSpec *ps : PluginManager::plugins()) {
|
|
||||||
installedPlugins.insert(ps->name(), ps);
|
|
||||||
unexpectedExtensions.append(ps->name());
|
|
||||||
}
|
|
||||||
|
|
||||||
const auto handleExtension = [&] (const ItemData &extension, bool addToBrowser) {
|
|
||||||
if (!installedPlugins.contains(extension.name)) {
|
|
||||||
expectedExtensions.append(extension.name);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
unexpectedExtensions.removeOne(extension.name);
|
|
||||||
|
|
||||||
if (addToBrowser) {
|
|
||||||
QStandardItem *item = new QStandardItem;
|
|
||||||
const PluginSpecList pluginSpecs = {installedPlugins.value(extension.name)};
|
|
||||||
item->setData(ItemTypeExtension, RoleItemType);
|
|
||||||
item->setData(QVariant::fromValue(extension.tags), RoleTags);
|
|
||||||
item->setData(QVariant::fromValue<PluginSpecList>(pluginSpecs), RolePluginSpecs);
|
|
||||||
item->setData(extension.name, RoleName);
|
|
||||||
items.append(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
};
|
|
||||||
|
|
||||||
const bool addPackedExtensionsToBrowser = true; // TODO: Determine how we want this. As setting?
|
|
||||||
for (const auto &pack : packs) {
|
|
||||||
PluginSpecList pluginSpecs;
|
|
||||||
for (const QString &extension : pack.extensions) {
|
|
||||||
const ItemData extensionData = {extension, {}, pack.tags, {}};
|
|
||||||
if (!handleExtension(extensionData, addPackedExtensionsToBrowser))
|
|
||||||
continue;
|
|
||||||
pluginSpecs.append(installedPlugins.value(extension));
|
|
||||||
}
|
|
||||||
if (pluginSpecs.isEmpty())
|
|
||||||
continue;
|
|
||||||
|
|
||||||
QStandardItem *item = new QStandardItem;
|
|
||||||
item->setData(ItemTypePack, RoleItemType);
|
|
||||||
item->setData(QVariant::fromValue(pack.tags), RoleTags);
|
|
||||||
item->setData(QVariant::fromValue<PluginSpecList>(pluginSpecs), RolePluginSpecs);
|
|
||||||
item->setData(pack.name, RoleName);
|
|
||||||
items.append(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
for (const auto &extension : extensions) {
|
|
||||||
const ItemData extensionData = {extension.name, {}, extension.tags, {}};
|
|
||||||
handleExtension(extensionData, true);
|
|
||||||
}
|
|
||||||
|
|
||||||
QStandardItemModel *result = new QStandardItemModel;
|
|
||||||
for (auto item : items) {
|
|
||||||
QStringList searchTexts;
|
|
||||||
searchTexts.append(item->data(RoleName).toString());
|
|
||||||
searchTexts.append(item->data(RoleTags).toStringList());
|
|
||||||
const PluginSpecList pluginSpecs = item->data(RolePluginSpecs).value<PluginSpecList>();
|
|
||||||
for (auto pluginSpec : pluginSpecs) {
|
|
||||||
searchTexts.append(pluginSpec->name());
|
|
||||||
searchTexts.append(pluginSpec->description());
|
|
||||||
searchTexts.append(pluginSpec->longDescription());
|
|
||||||
searchTexts.append(pluginSpec->category());
|
|
||||||
searchTexts.append(pluginSpec->copyright());
|
|
||||||
}
|
|
||||||
searchTexts.removeDuplicates();
|
|
||||||
item->setData(searchTexts.join(" "), RoleSearchText);
|
|
||||||
|
|
||||||
item->setDragEnabled(false);
|
|
||||||
item->setEditable(false);
|
|
||||||
|
|
||||||
result->appendRow(item);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (browserLog().isDebugEnabled()) {
|
|
||||||
if (!expectedExtensions.isEmpty())
|
|
||||||
qCDebug(browserLog) << "Expected extensions/plugins are not installed:"
|
|
||||||
<< expectedExtensions.join(", ");
|
|
||||||
if (!unexpectedExtensions.isEmpty())
|
|
||||||
qCDebug(browserLog) << "Unexpected extensions/plugins are installed:"
|
|
||||||
<< unexpectedExtensions.join(", ");
|
|
||||||
}
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
class ExtensionItemDelegate : public QItemDelegate
|
class ExtensionItemDelegate : public QItemDelegate
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -322,8 +72,8 @@ public:
|
|||||||
painter->save();
|
painter->save();
|
||||||
painter->setRenderHint(QPainter::Antialiasing);
|
painter->setRenderHint(QPainter::Antialiasing);
|
||||||
|
|
||||||
const ItemData data = itemData(index);
|
const QString itemName = index.data().toString();
|
||||||
const bool isPack = data.type == ItemTypePack;
|
const bool isPack = index.data(RoleItemType) == ItemTypePack;
|
||||||
const QRectF itemRect(option.rect.topLeft(), itemSize);
|
const QRectF itemRect(option.rect.topLeft(), itemSize);
|
||||||
{
|
{
|
||||||
const bool selected = option.state & QStyle::State_Selected;
|
const bool selected = option.state & QStyle::State_Selected;
|
||||||
@@ -345,7 +95,7 @@ public:
|
|||||||
bigCirclePath.addEllipse(bigCircleLocal);
|
bigCirclePath.addEllipse(bigCircleLocal);
|
||||||
QLinearGradient gradient(bigCircleLocal.topLeft(), bigCircleLocal.bottomRight());
|
QLinearGradient gradient(bigCircleLocal.topLeft(), bigCircleLocal.bottomRight());
|
||||||
const QColor startColor = isPack ? qRgb(0x1e, 0x99, 0x6e)
|
const QColor startColor = isPack ? qRgb(0x1e, 0x99, 0x6e)
|
||||||
: colorForExtensionName(data.name);
|
: colorForExtensionName(itemName);
|
||||||
const QColor endColor = isPack ? qRgb(0x07, 0x6b, 0x6d) : startColor.lighter(150);
|
const QColor endColor = isPack ? qRgb(0x07, 0x6b, 0x6d) : startColor.lighter(150);
|
||||||
gradient.setColorAt(gradientMargin, startColor);
|
gradient.setColorAt(gradientMargin, startColor);
|
||||||
gradient.setColorAt(1 - gradientMargin, endColor);
|
gradient.setColorAt(1 - gradientMargin, endColor);
|
||||||
@@ -377,7 +127,10 @@ public:
|
|||||||
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong));
|
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong));
|
||||||
const QColor textColor = creatorColor(Theme::Token_Text_Default);
|
const QColor textColor = creatorColor(Theme::Token_Text_Default);
|
||||||
painter->setPen(textColor);
|
painter->setPen(textColor);
|
||||||
painter->drawText(smallCircleLocal, QString::number(data.plugins.count()),
|
const PluginsData plugins = index.data(RolePlugins).value<PluginsData>();
|
||||||
|
painter->drawText(
|
||||||
|
smallCircleLocal,
|
||||||
|
QString::number(plugins.count()),
|
||||||
QTextOption(Qt::AlignCenter));
|
QTextOption(Qt::AlignCenter));
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
@@ -390,21 +143,22 @@ public:
|
|||||||
const QPointF titleOrigin(itemRect.topLeft() + QPointF(textX, titleY));
|
const QPointF titleOrigin(itemRect.topLeft() + QPointF(textX, titleY));
|
||||||
painter->setPen(creatorColor(Theme::Token_Text_Default));
|
painter->setPen(creatorColor(Theme::Token_Text_Default));
|
||||||
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementH6));
|
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementH6));
|
||||||
const QString titleElided = painter->fontMetrics().elidedText(
|
const QString titleElided
|
||||||
data.name, elideMode, maxTextWidth);
|
= painter->fontMetrics().elidedText(itemName, elideMode, maxTextWidth);
|
||||||
painter->drawText(titleOrigin, titleElided);
|
painter->drawText(titleOrigin, titleElided);
|
||||||
|
|
||||||
constexpr int copyrightY = 52;
|
constexpr int copyrightY = 52;
|
||||||
const QPointF copyrightOrigin(itemRect.topLeft() + QPointF(textX, copyrightY));
|
const QPointF copyrightOrigin(itemRect.topLeft() + QPointF(textX, copyrightY));
|
||||||
painter->setPen(creatorColor(Theme::Token_Text_Muted));
|
painter->setPen(creatorColor(Theme::Token_Text_Muted));
|
||||||
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong));
|
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong));
|
||||||
const QString copyrightElided = painter->fontMetrics().elidedText(
|
const QString copyright = index.data(RoleCopyright).toString();
|
||||||
data.plugins.first()->copyright(), elideMode, maxTextWidth);
|
const QString copyrightElided
|
||||||
|
= painter->fontMetrics().elidedText(copyright, elideMode, maxTextWidth);
|
||||||
painter->drawText(copyrightOrigin, copyrightElided);
|
painter->drawText(copyrightOrigin, copyrightElided);
|
||||||
|
|
||||||
constexpr int tagsY = 70;
|
constexpr int tagsY = 70;
|
||||||
const QPointF tagsOrigin(itemRect.topLeft() + QPointF(textX, tagsY));
|
const QPointF tagsOrigin(itemRect.topLeft() + QPointF(textX, tagsY));
|
||||||
const QString tags = data.tags.join(", ");
|
const QString tags = index.data(RoleTags).toStringList().join(", ");
|
||||||
painter->setPen(creatorColor(Theme::Token_Text_Default));
|
painter->setPen(creatorColor(Theme::Token_Text_Default));
|
||||||
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaption));
|
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaption));
|
||||||
const QString tagsElided = painter->fontMetrics().elidedText(
|
const QString tagsElided = painter->fontMetrics().elidedText(
|
||||||
@@ -422,87 +176,154 @@ public:
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
ExtensionsBrowser::ExtensionsBrowser()
|
class ExtensionsBrowserPrivate
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ExtensionsModel *model;
|
||||||
|
QLineEdit *searchBox;
|
||||||
|
QAbstractButton *updateButton;
|
||||||
|
QListView *extensionsView;
|
||||||
|
QItemSelectionModel *selectionModel = nullptr;
|
||||||
|
QSortFilterProxyModel *filterProxyModel;
|
||||||
|
int columnsCount = 2;
|
||||||
|
Tasking::TaskTreeRunner taskTreeRunner;
|
||||||
|
};
|
||||||
|
|
||||||
|
ExtensionsBrowser::ExtensionsBrowser(QWidget *parent)
|
||||||
|
: QWidget(parent)
|
||||||
|
, d(new ExtensionsBrowserPrivate)
|
||||||
{
|
{
|
||||||
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
|
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
|
||||||
|
|
||||||
auto manageLabel = new QLabel(Tr::tr("Manage Extensions"));
|
auto manageLabel = new QLabel(Tr::tr("Manage Extensions"));
|
||||||
manageLabel->setFont(StyleHelper::uiFont(StyleHelper::UiElementH1));
|
manageLabel->setFont(StyleHelper::uiFont(StyleHelper::UiElementH1));
|
||||||
|
|
||||||
m_searchBox = new Core::SearchBox;
|
d->searchBox = new Core::SearchBox;
|
||||||
m_searchBox->setFixedWidth(itemSize.width());
|
d->searchBox->setFixedWidth(itemSize.width());
|
||||||
|
|
||||||
m_updateButton = new Button(Tr::tr("Install..."), Button::MediumPrimary);
|
d->updateButton = new Button(Tr::tr("Install..."), Button::MediumPrimary);
|
||||||
|
|
||||||
m_filterProxyModel = new QSortFilterProxyModel(this);
|
d->model = new ExtensionsModel(this);
|
||||||
m_filterProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
|
||||||
m_filterProxyModel->setFilterRole(RoleSearchText);
|
|
||||||
m_filterProxyModel->setSortRole(RoleItemType);
|
|
||||||
|
|
||||||
m_extensionsView = new QListView;
|
d->filterProxyModel = new QSortFilterProxyModel(this);
|
||||||
m_extensionsView->setFrameStyle(QFrame::NoFrame);
|
d->filterProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
||||||
m_extensionsView->setItemDelegate(new ExtensionItemDelegate(this));
|
d->filterProxyModel->setFilterRole(RoleSearchText);
|
||||||
m_extensionsView->setResizeMode(QListView::Adjust);
|
d->filterProxyModel->setSortRole(RoleItemType);
|
||||||
m_extensionsView->setSelectionMode(QListView::SingleSelection);
|
d->filterProxyModel->setSourceModel(d->model);
|
||||||
m_extensionsView->setUniformItemSizes(true);
|
|
||||||
m_extensionsView->setViewMode(QListView::IconMode);
|
d->extensionsView = new QListView;
|
||||||
m_extensionsView->setModel(m_filterProxyModel);
|
d->extensionsView->setFrameStyle(QFrame::NoFrame);
|
||||||
m_extensionsView->setMouseTracking(true);
|
d->extensionsView->setItemDelegate(new ExtensionItemDelegate(this));
|
||||||
|
d->extensionsView->setResizeMode(QListView::Adjust);
|
||||||
|
d->extensionsView->setSelectionMode(QListView::SingleSelection);
|
||||||
|
d->extensionsView->setUniformItemSizes(true);
|
||||||
|
d->extensionsView->setViewMode(QListView::IconMode);
|
||||||
|
d->extensionsView->setModel(d->filterProxyModel);
|
||||||
|
d->extensionsView->setMouseTracking(true);
|
||||||
|
|
||||||
using namespace Layouting;
|
using namespace Layouting;
|
||||||
Column {
|
Column {
|
||||||
Space(15),
|
Space(15),
|
||||||
manageLabel,
|
manageLabel,
|
||||||
Space(15),
|
Space(15),
|
||||||
Row { m_searchBox, st, m_updateButton, Space(extraListViewWidth() + gapSize) },
|
Row { d->searchBox, st, d->updateButton, Space(extraListViewWidth() + gapSize) },
|
||||||
Space(gapSize),
|
Space(gapSize),
|
||||||
m_extensionsView,
|
d->extensionsView,
|
||||||
noMargin, spacing(0),
|
noMargin, spacing(0),
|
||||||
}.attachTo(this);
|
}.attachTo(this);
|
||||||
|
|
||||||
WelcomePageHelpers::setBackgroundColor(this, Theme::Token_Background_Default);
|
WelcomePageHelpers::setBackgroundColor(this, Theme::Token_Background_Default);
|
||||||
WelcomePageHelpers::setBackgroundColor(m_extensionsView, Theme::Token_Background_Default);
|
WelcomePageHelpers::setBackgroundColor(d->extensionsView, Theme::Token_Background_Default);
|
||||||
WelcomePageHelpers::setBackgroundColor(m_extensionsView->viewport(),
|
WelcomePageHelpers::setBackgroundColor(d->extensionsView->viewport(),
|
||||||
Theme::Token_Background_Default);
|
Theme::Token_Background_Default);
|
||||||
|
|
||||||
auto updateModel = [this] {
|
auto updateModel = [this] {
|
||||||
m_model.reset(extensionsModel());
|
d->filterProxyModel->sort(0);
|
||||||
m_filterProxyModel->setSourceModel(m_model.data());
|
|
||||||
m_filterProxyModel->sort(0);
|
|
||||||
|
|
||||||
if (m_selectionModel == nullptr) {
|
if (d->selectionModel == nullptr) {
|
||||||
m_selectionModel = new QItemSelectionModel(m_filterProxyModel, m_extensionsView);
|
d->selectionModel = new QItemSelectionModel(d->filterProxyModel,
|
||||||
m_extensionsView->setSelectionModel(m_selectionModel);
|
d->extensionsView);
|
||||||
connect(m_extensionsView->selectionModel(), &QItemSelectionModel::currentChanged,
|
d->extensionsView->setSelectionModel(d->selectionModel);
|
||||||
|
connect(d->extensionsView->selectionModel(), &QItemSelectionModel::currentChanged,
|
||||||
this, &ExtensionsBrowser::itemSelected);
|
this, &ExtensionsBrowser::itemSelected);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
connect(d->updateButton, &QAbstractButton::pressed, this, []() {
|
||||||
|
executePluginInstallWizard();
|
||||||
|
});
|
||||||
connect(ExtensionSystem::PluginManager::instance(),
|
connect(ExtensionSystem::PluginManager::instance(),
|
||||||
&ExtensionSystem::PluginManager::pluginsChanged, this, updateModel);
|
&ExtensionSystem::PluginManager::pluginsChanged, this, updateModel);
|
||||||
connect(m_searchBox, &QLineEdit::textChanged,
|
connect(ExtensionSystem::PluginManager::instance(),
|
||||||
m_filterProxyModel, &QSortFilterProxyModel::setFilterWildcard);
|
&ExtensionSystem::PluginManager::initializationDone,
|
||||||
|
this, &ExtensionsBrowser::fetchExtensions);
|
||||||
|
connect(d->searchBox, &QLineEdit::textChanged,
|
||||||
|
d->filterProxyModel, &QSortFilterProxyModel::setFilterWildcard);
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtensionsBrowser::~ExtensionsBrowser()
|
||||||
|
{
|
||||||
|
delete d;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ExtensionsBrowser::adjustToWidth(const int width)
|
void ExtensionsBrowser::adjustToWidth(const int width)
|
||||||
{
|
{
|
||||||
const int widthForItems = width - extraListViewWidth();
|
const int widthForItems = width - extraListViewWidth();
|
||||||
m_columnsCount = qMax(1, qFloor(widthForItems / cellSize.width()));
|
d->columnsCount = qMax(1, qFloor(widthForItems / cellSize.width()));
|
||||||
m_updateButton->setVisible(m_columnsCount > 1);
|
d->updateButton->setVisible(d->columnsCount > 1);
|
||||||
updateGeometry();
|
updateGeometry();
|
||||||
}
|
}
|
||||||
|
|
||||||
QSize ExtensionsBrowser::sizeHint() const
|
QSize ExtensionsBrowser::sizeHint() const
|
||||||
{
|
{
|
||||||
const int columsWidth = m_columnsCount * cellSize.width();
|
const int columsWidth = d->columnsCount * cellSize.width();
|
||||||
return { columsWidth + extraListViewWidth(), 0};
|
return { columsWidth + extraListViewWidth(), 0};
|
||||||
}
|
}
|
||||||
|
|
||||||
int ExtensionsBrowser::extraListViewWidth() const
|
int ExtensionsBrowser::extraListViewWidth() const
|
||||||
{
|
{
|
||||||
// TODO: Investigate "transient" scrollbar, just for this list view.
|
// TODO: Investigate "transient" scrollbar, just for this list view.
|
||||||
return m_extensionsView->style()->pixelMetric(QStyle::PM_ScrollBarExtent)
|
return d->extensionsView->style()->pixelMetric(QStyle::PM_ScrollBarExtent)
|
||||||
+ 1; // Needed
|
+ 1; // Needed
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void ExtensionsBrowser::fetchExtensions()
|
||||||
|
{
|
||||||
|
// d->model->setExtensionsJson(testData("thirdpartyplugins")); return;
|
||||||
|
|
||||||
|
using namespace Tasking;
|
||||||
|
|
||||||
|
const auto onQuerySetup = [](NetworkQuery &query) {
|
||||||
|
const QString host = "https://qc-extensions.qt.io";
|
||||||
|
const QString url = "%1/api/v1/search?request=";
|
||||||
|
const QString requestTemplate
|
||||||
|
= R"({"version":"%1","host_os":"%2","host_os_version":"%3","host_architecture":"%4","page_size":200})";
|
||||||
|
const QString request = url.arg(host)
|
||||||
|
+ requestTemplate
|
||||||
|
.arg("2.2") // .arg(QCoreApplication::applicationVersion())
|
||||||
|
.arg("macOS") // .arg(QSysInfo::productType())
|
||||||
|
.arg("12") // .arg(QSysInfo::productVersion())
|
||||||
|
.arg("arm64"); // .arg(QSysInfo::currentCpuArchitecture());
|
||||||
|
|
||||||
|
query.setRequest(QNetworkRequest(QUrl::fromUserInput(request)));
|
||||||
|
query.setNetworkAccessManager(NetworkAccessManager::instance());
|
||||||
|
};
|
||||||
|
const auto onQueryDone = [this](const NetworkQuery &query, DoneWith result) {
|
||||||
|
if (result != DoneWith::Success) {
|
||||||
|
#ifdef WITH_TESTS
|
||||||
|
d->model->setExtensionsJson(testData("defaultpacks"));
|
||||||
|
#endif // WITH_TESTS
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const QByteArray response = query.reply()->readAll();
|
||||||
|
d->model->setExtensionsJson(response);
|
||||||
|
};
|
||||||
|
|
||||||
|
Group group {
|
||||||
|
NetworkQueryTask{onQuerySetup, onQueryDone},
|
||||||
|
};
|
||||||
|
|
||||||
|
d->taskTreeRunner.start(group);
|
||||||
|
}
|
||||||
|
|
||||||
} // ExtensionManager::Internal
|
} // ExtensionManager::Internal
|
||||||
|
@@ -3,66 +3,30 @@
|
|||||||
|
|
||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <utils/theme/theme.h>
|
|
||||||
|
|
||||||
#include <QStandardItemModel>
|
|
||||||
#include <QWidget>
|
#include <QWidget>
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
|
||||||
class QAbstractButton;
|
|
||||||
class QItemSelectionModel;
|
|
||||||
class QLineEdit;
|
|
||||||
class QListView;
|
|
||||||
class QSortFilterProxyModel;
|
|
||||||
QT_END_NAMESPACE
|
|
||||||
|
|
||||||
namespace ExtensionSystem
|
|
||||||
{
|
|
||||||
class PluginSpec;
|
|
||||||
}
|
|
||||||
|
|
||||||
namespace ExtensionManager::Internal {
|
namespace ExtensionManager::Internal {
|
||||||
|
|
||||||
using PluginSpecList = QList<const ExtensionSystem::PluginSpec *>;
|
|
||||||
using Tags = QStringList;
|
|
||||||
|
|
||||||
enum ItemType {
|
|
||||||
ItemTypePack,
|
|
||||||
ItemTypeExtension,
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ItemData {
|
|
||||||
const QString name;
|
|
||||||
const ItemType type = ItemTypeExtension;
|
|
||||||
const Tags tags;
|
|
||||||
const PluginSpecList plugins;
|
|
||||||
};
|
|
||||||
|
|
||||||
ItemData itemData(const QModelIndex &index);
|
|
||||||
|
|
||||||
class ExtensionsBrowser final : public QWidget
|
class ExtensionsBrowser final : public QWidget
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
public:
|
public:
|
||||||
ExtensionsBrowser();
|
ExtensionsBrowser(QWidget *parent = nullptr);
|
||||||
|
~ExtensionsBrowser();
|
||||||
|
|
||||||
void adjustToWidth(const int width);
|
void adjustToWidth(const int width);
|
||||||
QSize sizeHint() const override;
|
QSize sizeHint() const override;
|
||||||
|
|
||||||
|
int extraListViewWidth() const; // Space for scrollbar, etc.
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void itemSelected(const QModelIndex ¤t, const QModelIndex &previous);
|
void itemSelected(const QModelIndex ¤t, const QModelIndex &previous);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
int extraListViewWidth() const; // Space for scrollbar, etc.
|
void fetchExtensions();
|
||||||
|
|
||||||
QScopedPointer<QStandardItemModel> m_model;
|
class ExtensionsBrowserPrivate *d = nullptr;
|
||||||
QLineEdit *m_searchBox;
|
|
||||||
QAbstractButton *m_updateButton;
|
|
||||||
QListView *m_extensionsView;
|
|
||||||
QItemSelectionModel *m_selectionModel = nullptr;
|
|
||||||
QSortFilterProxyModel *m_filterProxyModel;
|
|
||||||
int m_columnsCount = 2;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
} // ExtensionManager::Internal
|
} // ExtensionManager::Internal
|
||||||
|
411
src/plugins/extensionmanager/extensionsmodel.cpp
Normal file
411
src/plugins/extensionmanager/extensionsmodel.cpp
Normal file
@@ -0,0 +1,411 @@
|
|||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
|
#include "extensionsmodel.h"
|
||||||
|
|
||||||
|
#include "extensionsbrowser.h"
|
||||||
|
|
||||||
|
#include "extensionmanagertr.h"
|
||||||
|
#include "utils/algorithm.h"
|
||||||
|
|
||||||
|
#include <coreplugin/coreconstants.h>
|
||||||
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
|
#include <extensionsystem/iplugin.h>
|
||||||
|
#include <extensionsystem/pluginspec.h>
|
||||||
|
#include <extensionsystem/pluginview.h>
|
||||||
|
#include <extensionsystem/pluginmanager.h>
|
||||||
|
|
||||||
|
#include <QJsonArray>
|
||||||
|
#include <QJsonDocument>
|
||||||
|
#include <QJsonObject>
|
||||||
|
#include <QStandardItemModel>
|
||||||
|
#include <QVersionNumber>
|
||||||
|
|
||||||
|
using namespace ExtensionSystem;
|
||||||
|
using namespace Core;
|
||||||
|
using namespace Utils;
|
||||||
|
|
||||||
|
namespace ExtensionManager::Internal {
|
||||||
|
|
||||||
|
Q_LOGGING_CATEGORY(modelLog, "qtc.extensionmanager.model", QtWarningMsg)
|
||||||
|
|
||||||
|
struct PluginDependency
|
||||||
|
{
|
||||||
|
QString name;
|
||||||
|
QString version;
|
||||||
|
};
|
||||||
|
using PluginDependencies = QList<PluginDependency>;
|
||||||
|
|
||||||
|
struct Plugin
|
||||||
|
{
|
||||||
|
PluginDependencies dependencies;
|
||||||
|
QString copyright;
|
||||||
|
QString name;
|
||||||
|
QString packageUrl;
|
||||||
|
QString vendor;
|
||||||
|
QString version;
|
||||||
|
};
|
||||||
|
using Plugins = QList<Plugin>;
|
||||||
|
|
||||||
|
struct Description {
|
||||||
|
ImagesData images;
|
||||||
|
LinksData links;
|
||||||
|
TextData text;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Extension {
|
||||||
|
QString copyright;
|
||||||
|
Description description;
|
||||||
|
QString license;
|
||||||
|
QString name;
|
||||||
|
QStringList platforms;
|
||||||
|
Plugins plugins;
|
||||||
|
qint64 size = 0;
|
||||||
|
QStringList tags;
|
||||||
|
ItemType type = ItemTypePack;
|
||||||
|
QString vendor;
|
||||||
|
QString version;
|
||||||
|
};
|
||||||
|
using Extensions = QList<Extension>;
|
||||||
|
|
||||||
|
static Plugin pluginFromJson(const QJsonObject &obj)
|
||||||
|
{
|
||||||
|
const QJsonObject metaDataObj = obj.value("meta_data").toObject();
|
||||||
|
|
||||||
|
const QJsonArray dependenciesArray = metaDataObj.value("Dependencies").toArray();
|
||||||
|
PluginDependencies dependencies;
|
||||||
|
for (const QJsonValueConstRef &dependencyVal : dependenciesArray) {
|
||||||
|
const QJsonObject dependencyObj = dependencyVal.toObject();
|
||||||
|
dependencies.append(PluginDependency{
|
||||||
|
.name = dependencyObj.value("Name").toString(),
|
||||||
|
.version = dependencyObj.value("Version").toString(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
.dependencies = dependencies,
|
||||||
|
.copyright = metaDataObj.value("Copyright").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 ¶graphVal : paragraphsArray) {
|
||||||
|
const QJsonObject ¶graphObj = 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 = {
|
||||||
|
.copyright = obj.value("copyright").toString(),
|
||||||
|
.description = description,
|
||||||
|
.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(),
|
||||||
|
.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;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ExtensionsModelPrivate : public QObject
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ExtensionsModelPrivate(ExtensionsModel *parent)
|
||||||
|
: q(parent)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
void setExtensions(const Extensions &extensions);
|
||||||
|
void removeLocalExtensions();
|
||||||
|
|
||||||
|
ExtensionsModel *q;
|
||||||
|
|
||||||
|
Extensions allExtensions; // Original, complete extensions entries
|
||||||
|
Extensions absentExtensions; // All packs + plugin extensions that are not (yet) installed
|
||||||
|
};
|
||||||
|
|
||||||
|
void ExtensionsModelPrivate::setExtensions(const Extensions &extensions)
|
||||||
|
{
|
||||||
|
allExtensions = extensions;
|
||||||
|
removeLocalExtensions();
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExtensionsModelPrivate::removeLocalExtensions()
|
||||||
|
{
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtensionsModel::ExtensionsModel(QObject *parent)
|
||||||
|
: QAbstractListModel(parent)
|
||||||
|
, d(new ExtensionsModelPrivate(this))
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
ExtensionsModel::~ExtensionsModel()
|
||||||
|
{
|
||||||
|
delete d;
|
||||||
|
}
|
||||||
|
|
||||||
|
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(),
|
||||||
|
&ExtensionSystem::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)
|
||||||
|
{
|
||||||
|
QStringList dependencies;
|
||||||
|
for (const Plugin &plugin : extension.plugins) {
|
||||||
|
for (const PluginDependency &dependency : plugin.dependencies) {
|
||||||
|
const QString withVersion = QString::fromLatin1("%1 (%2)").arg(dependency.name)
|
||||||
|
.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 extension.name;
|
||||||
|
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 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.name, 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();
|
||||||
|
default:
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
static QString searchText(const QModelIndex &index)
|
||||||
|
{
|
||||||
|
QStringList searchTexts;
|
||||||
|
searchTexts.append(index.data(RoleName).toString());
|
||||||
|
searchTexts.append(index.data(RoleTags).toStringList());
|
||||||
|
searchTexts.append(index.data(RoleDescriptionText).toStringList());
|
||||||
|
searchTexts.append(index.data(RoleVendor).toString());
|
||||||
|
return searchTexts.join(" ");
|
||||||
|
}
|
||||||
|
|
||||||
|
QVariant ExtensionsModel::data(const QModelIndex &index, int role) const
|
||||||
|
{
|
||||||
|
if (role == RoleSearchText)
|
||||||
|
return searchText(index);
|
||||||
|
|
||||||
|
const bool itemIsLocalPlugin = index.row() >= d->absentExtensions.count();
|
||||||
|
if (itemIsLocalPlugin) {
|
||||||
|
const PluginSpecs &pluginSpecs = PluginManager::plugins();
|
||||||
|
const int pluginIndex = index.row() - d->absentExtensions.count();
|
||||||
|
QTC_ASSERT(pluginIndex >= 0 && pluginIndex <= pluginSpecs.size(), return {});
|
||||||
|
const PluginSpec *plugin = pluginSpecs.at(pluginIndex);
|
||||||
|
return dataFromPluginSpec(plugin, role);
|
||||||
|
} else {
|
||||||
|
const Extension &extension = d->absentExtensions.at(index.row());
|
||||||
|
const QVariant extensionData = dataFromExtension(extension, role);
|
||||||
|
|
||||||
|
// If data is unavailable, retrieve it from the first contained plugin
|
||||||
|
if (extensionData.isNull() && !extension.plugins.isEmpty()) {
|
||||||
|
const PluginSpec *pluginSpec = ExtensionsModel::pluginSpecForName(
|
||||||
|
extension.plugins.constFirst().name);
|
||||||
|
if (pluginSpec)
|
||||||
|
return dataFromPluginSpec(pluginSpec, role);
|
||||||
|
}
|
||||||
|
return extensionData;
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
void ExtensionsModel::setExtensionsJson(const QByteArray &json)
|
||||||
|
{
|
||||||
|
const Extensions extensions = parseExtensionsRepoReply(json);
|
||||||
|
beginResetModel();
|
||||||
|
d->setExtensions(extensions);
|
||||||
|
endResetModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
PluginSpec *ExtensionsModel::pluginSpecForName(const QString &pluginName)
|
||||||
|
{
|
||||||
|
return Utils::findOrDefault(PluginManager::plugins(),
|
||||||
|
Utils::equal(&PluginSpec::name, pluginName));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // ExtensionManager::Internal
|
68
src/plugins/extensionmanager/extensionsmodel.h
Normal file
68
src/plugins/extensionmanager/extensionsmodel.h
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
// Copyright (C) 2023 The Qt Company Ltd.
|
||||||
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <QAbstractListModel>
|
||||||
|
|
||||||
|
namespace ExtensionSystem {
|
||||||
|
class PluginSpec;
|
||||||
|
}
|
||||||
|
|
||||||
|
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 {
|
||||||
|
ItemTypePack,
|
||||||
|
ItemTypeExtension,
|
||||||
|
};
|
||||||
|
|
||||||
|
enum Role {
|
||||||
|
RoleName = Qt::UserRole,
|
||||||
|
RoleCopyright,
|
||||||
|
RoleDependencies,
|
||||||
|
RoleDescriptionImages,
|
||||||
|
RoleDescriptionLinks,
|
||||||
|
RoleDescriptionText,
|
||||||
|
RoleItemType,
|
||||||
|
RoleLicense,
|
||||||
|
RoleLocation,
|
||||||
|
RolePlatforms,
|
||||||
|
RolePlugins,
|
||||||
|
RoleSearchText,
|
||||||
|
RoleSize,
|
||||||
|
RoleTags,
|
||||||
|
RoleVendor,
|
||||||
|
RoleVersion,
|
||||||
|
};
|
||||||
|
|
||||||
|
class ExtensionsModel : public QAbstractListModel
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
ExtensionsModel(QObject *parent = nullptr);
|
||||||
|
~ExtensionsModel();
|
||||||
|
|
||||||
|
int rowCount(const QModelIndex &parent = {}) const;
|
||||||
|
QVariant data(const QModelIndex &index, int role) const;
|
||||||
|
|
||||||
|
void setExtensionsJson(const QByteArray &json);
|
||||||
|
static ExtensionSystem::PluginSpec *pluginSpecForName(const QString &pluginName);
|
||||||
|
|
||||||
|
private:
|
||||||
|
class ExtensionsModelPrivate *d = nullptr;
|
||||||
|
};
|
||||||
|
|
||||||
|
#ifdef WITH_TESTS
|
||||||
|
QObject *createExtensionsModelTest();
|
||||||
|
#endif
|
||||||
|
|
||||||
|
} // ExtensionManager::Internal
|
||||||
|
|
||||||
|
Q_DECLARE_METATYPE(ExtensionManager::Internal::QPairList)
|
||||||
|
Q_DECLARE_METATYPE(ExtensionManager::Internal::TextData)
|
161
src/plugins/extensionmanager/testdata/defaultpacks.json
vendored
Normal file
161
src/plugins/extensionmanager/testdata/defaultpacks.json
vendored
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"name": "Essentials",
|
||||||
|
"tags": [ "Essentials" ],
|
||||||
|
"platforms": [ "macOS", "Windows", "Linux" ],
|
||||||
|
"license": "os",
|
||||||
|
"is_pack": true,
|
||||||
|
"description": {
|
||||||
|
"paragraphs": [
|
||||||
|
{
|
||||||
|
"text": [
|
||||||
|
"Basic services, such as editing and debugging code, viewing images, and adding resources to applications."
|
||||||
|
],
|
||||||
|
"header": "Get started"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"links": [
|
||||||
|
{
|
||||||
|
"url": "https://doc.qt.io/qtcreator/creator-coding-navigating.html",
|
||||||
|
"link_text": "Online documentation"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{ "meta_data": { "Name": "BinEditor" } },
|
||||||
|
{ "meta_data": { "Name": "Debugger" } },
|
||||||
|
{ "meta_data": { "Name": "DiffEditor" } },
|
||||||
|
{ "meta_data": { "Name": "ImageViewer" } },
|
||||||
|
{ "meta_data": { "Name": "Macros" } },
|
||||||
|
{ "meta_data": { "Name": "LanguageClient" } },
|
||||||
|
{ "meta_data": { "Name": "ResourceEditor" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "C++ Support",
|
||||||
|
"tags": [ "Programming Language", "C++" ],
|
||||||
|
"platforms": [ "macOS", "Windows", "Linux" ],
|
||||||
|
"license": "os",
|
||||||
|
"is_pack": true,
|
||||||
|
"description": {
|
||||||
|
"paragraphs": [
|
||||||
|
{
|
||||||
|
"text": [
|
||||||
|
"Tools for developing Qt C++ applications."
|
||||||
|
],
|
||||||
|
"header": "Get started"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{ "meta_data": { "Name": "ClangCodeModel" } },
|
||||||
|
{ "meta_data": { "Name": "ClangFormat" } },
|
||||||
|
{ "meta_data": { "Name": "ClassView" } },
|
||||||
|
{ "meta_data": { "Name": "CppEditor" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "QML Support",
|
||||||
|
"tags": [ "Programming Language", "QML" ],
|
||||||
|
"platforms": [ "macOS", "Windows", "Linux" ],
|
||||||
|
"license": "os",
|
||||||
|
"is_pack": true,
|
||||||
|
"description": {
|
||||||
|
"paragraphs": [
|
||||||
|
{
|
||||||
|
"text": [
|
||||||
|
"Tools for developing Qt Quick applications."
|
||||||
|
],
|
||||||
|
"header": "Get started"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{ "meta_data": { "Name": "QmlJSEditor" } },
|
||||||
|
{ "meta_data": { "Name": "QmlJSTools" } },
|
||||||
|
{ "meta_data": { "Name": "QmlPreview" } },
|
||||||
|
{ "meta_data": { "Name": "QmlProfiler" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Visual QML Editor",
|
||||||
|
"tags": [ "Visual UI editor", "qml", "Quick" ],
|
||||||
|
"platforms": [ "macOS", "Windows", "Linux" ],
|
||||||
|
"license": "os",
|
||||||
|
"is_pack": true,
|
||||||
|
"description": {
|
||||||
|
"paragraphs": [
|
||||||
|
{
|
||||||
|
"text": [
|
||||||
|
"Tools for creating Qt Quick UIs."
|
||||||
|
],
|
||||||
|
"header": "Get started"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"plugins": [
|
||||||
|
{ "meta_data": { "Name": "QmlDesigner" } }
|
||||||
|
]
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
"name": "Visual Widget Editor",
|
||||||
|
"tags": [ "Visual UI editor", "C++", "Widgets" ],
|
||||||
|
"platforms": [ "macOS", "Windows", "Linux" ],
|
||||||
|
"license": "os",
|
||||||
|
"is_pack": true,
|
||||||
|
"description": {
|
||||||
|
"paragraphs": [
|
||||||
|
{
|
||||||
|
"text": [
|
||||||
|
"Visual tool for creating Qt widget-based UIs."
|
||||||
|
],
|
||||||
|
"header": "Get started"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"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"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
38
src/plugins/extensionmanager/testdata/thirdpartyplugins.json
vendored
Normal file
38
src/plugins/extensionmanager/testdata/thirdpartyplugins.json
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"items": [
|
||||||
|
{
|
||||||
|
"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