diff options
author | Alessandro Portale <[email protected]> | 2024-05-29 16:00:22 +0200 |
---|---|---|
committer | Alessandro Portale <[email protected]> | 2024-05-30 16:32:48 +0000 |
commit | 1a1d9381704bcf37aa55fa23c2c9711b91c535c1 (patch) | |
tree | 95208d9d6bee5956e19b33295315a319509ccb81 /src/plugins/extensionmanager/extensionmanagerwidget.cpp | |
parent | fb2a1ecd37d384906f1e0f3bb7a1b4b494a8381d (diff) |
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 <[email protected]>
Diffstat (limited to 'src/plugins/extensionmanager/extensionmanagerwidget.cpp')
-rw-r--r-- | src/plugins/extensionmanager/extensionmanagerwidget.cpp | 475 |
1 files changed, 328 insertions, 147 deletions
diff --git a/src/plugins/extensionmanager/extensionmanagerwidget.cpp b/src/plugins/extensionmanager/extensionmanagerwidget.cpp index 027378ede1c..78acb32e28e 100644 --- a/src/plugins/extensionmanager/extensionmanagerwidget.cpp +++ b/src/plugins/extensionmanager/extensionmanagerwidget.cpp @@ -3,26 +3,40 @@ #include "extensionmanagerwidget.h" -#include "extensionmanagerconstants.h" #include "extensionmanagertr.h" #include "extensionsbrowser.h" +#include "extensionsmodel.h" #include <coreplugin/coreconstants.h> #include <coreplugin/icontext.h> #include <coreplugin/icore.h> #include <coreplugin/iwelcomepage.h> +#include <coreplugin/plugininstallwizard.h> #include <coreplugin/welcomepagehelper.h> +#include <extensionsystem/pluginmanager.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/fileutils.h> +#include <utils/hostosinfo.h> #include <utils/icon.h> +#include <utils/infolabel.h> #include <utils/layoutbuilder.h> +#include <utils/networkaccessmanager.h> #include <utils/stylehelper.h> +#include <utils/temporarydirectory.h> #include <utils/utilsicons.h> #include <QAction> +#include <QCheckBox> +#include <QMessageBox> #include <QTextBrowser> +#include <QProgressDialog> using namespace Core; using namespace Utils; @@ -54,80 +68,172 @@ private: 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; - m_secondarDescriptionWidget = new CollapsingWidget; + d->secondaryDescriptionWidget = new CollapsingWidget; + + d->primaryDescription = new QTextBrowser; + d->primaryDescription->setOpenExternalLinks(true); + d->primaryDescription->setFrameStyle(QFrame::NoFrame); + + d->secondaryDescription = new QTextBrowser; + d->secondaryDescription->setFrameStyle(QFrame::NoFrame); - m_primaryDescription = new QTextBrowser; - m_primaryDescription->setOpenExternalLinks(true); - m_primaryDescription->setFrameStyle(QFrame::NoFrame); + d->pluginStatus = new PluginStatusWidget; - m_secondaryDescription = new QTextBrowser; - m_secondaryDescription->setFrameStyle(QFrame::NoFrame); + d->installButton = new Button(Tr::tr("Install..."), Button::MediumPrimary); + d->installButton->hide(); using namespace Layouting; Row { WelcomePageHelpers::createRule(Qt::Vertical), - m_secondaryDescription, + Column { + d->secondaryDescription, + d->pluginStatus, + d->installButton, + }, noMargin, spacing(0), - }.attachTo(m_secondarDescriptionWidget); + }.attachTo(d->secondaryDescriptionWidget); Row { WelcomePageHelpers::createRule(Qt::Vertical), Row { - m_primaryDescription, + d->primaryDescription, noMargin, }, - m_secondarDescriptionWidget, + d->secondaryDescriptionWidget, noMargin, spacing(0), }.attachTo(descriptionColumns); Row { Space(StyleHelper::SpacingTokens::ExVPaddingGapXl), - m_leftColumn, + d->leftColumn, descriptionColumns, noMargin, spacing(0), }.attachTo(this); WelcomePageHelpers::setBackgroundColor(this, Theme::Token_Background_Default); - connect(m_leftColumn, &ExtensionsBrowser::itemSelected, + connect(d->leftColumn, &ExtensionsBrowser::itemSelected, this, &ExtensionManagerWidget::updateView); connect(this, &ResizeSignallingWidget::resized, this, [this](const QSize &size) { const int intendedLeftColumnWidth = size.width() - 580; - m_leftColumn->adjustToWidth(intendedLeftColumnWidth); + d->leftColumn->adjustToWidth(intendedLeftColumnWidth); const bool secondaryDescriptionVisible = size.width() > 970; const int secondaryDescriptionWidth = secondaryDescriptionVisible ? 264 : 0; - m_secondarDescriptionWidget->setWidth(secondaryDescriptionWidth); + d->secondaryDescriptionWidget->setWidth(secondaryDescriptionWidth); + }); + connect(d->installButton, &QAbstractButton::pressed, this, [this]() { + fetchAndInstallPlugin(QUrl::fromUserInput(d->currentItemPlugins.constFirst().second)); }); - updateView({}, {}); + updateView({}); +} + +ExtensionManagerWidget::~ExtensionManagerWidget() +{ + delete d; } -void ExtensionManagerWidget::updateView(const QModelIndex ¤t, - [[maybe_unused]] const QModelIndex &previous) +void ExtensionManagerWidget::updateView(const QModelIndex ¤t) { const QString h5Css = StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH5)) - + "; margin-top: 28px;"; + + "; margin-top: 0px;"; const QString h6Css = StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH6)) + "; margin-top: 28px;"; const QString h6CapitalCss = StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH6Capital)) - + QString::fromLatin1("; color: %1;") + + QString::fromLatin1("; margin-top: 0px; color: %1;") .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;") .arg(creatorColor(Theme::Token_Text_Default).name()) .arg(creatorColor(Theme::Token_Background_Muted).name()) .arg(StyleHelper::SpacingTokens::ExVPaddingGapXl); const QString htmlStart = QString(R"( <html> - <body style="%1"> + <body style="%1"><br/> )").arg(bodyStyle); const QString htmlEnd = QString(R"( </body></html> @@ -135,161 +241,236 @@ void ExtensionManagerWidget::updateView(const QModelIndex ¤t, if (!current.isValid()) { const QString emptyHtml = htmlStart + htmlEnd; - m_primaryDescription->setText(emptyHtml); - m_secondaryDescription->setText(emptyHtml); + d->primaryDescription->setText(emptyHtml); + d->secondaryDescription->setText(emptyHtml); return; } - const ItemData data = itemData(current); - const bool isPack = data.type == ItemTypePack; - const ExtensionSystem::PluginSpec *extension = data.plugins.first(); + d->currentItemName = current.data().toString(); + const bool isPack = current.data(RoleItemType) == ItemTypePack; + 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; - description.append(QString(R"( - <div style="%1"><br/>%2</div> - <p>%3</p> - )").arg(h5Css) - .arg(shortDescription) - .arg(longDescription)); - - description.append(QString(R"( - <div style="%1">%2</div> - <p>%3</p> - )").arg(h6Css) - .arg(Tr::tr("Get started")) - .arg(Tr::tr("Install the extension from above. Installation starts automatically. " - "You can always uninstall the extension afterwards."))); + QString descriptionHtml; + { + const TextData textData = current.data(RoleDescriptionText).value<TextData>(); + for (const TextData::Type &text : textData) { + if (text.second.isEmpty()) + 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::fromLatin1("<div style=\"%1\">%2</div>") + .arg(h6Css) + .arg(Tr::tr("More information"))); + const LinksData linksData = current.data(RoleDescriptionLinks).value<LinksData>(); + if (!linksData.isEmpty()) { + QString linksHtml; + const QStringList links = Utils::transform(linksData, [](const LinksData::Type &link) { + const QString anchor = link.first.isEmpty() ? link.second : link.first; + return QString::fromLatin1("<a href=\"%1\">%2 ></a>") + .arg(link.second).arg(anchor); + }); + linksHtml = links.join("<br/>"); + description.append(QString::fromLatin1("<p>%1</p>").arg(linksHtml)); + } - description.append(QString(R"( - <div style="%1">%2</div> - <p> - <a href="%3">%4 ></a> - <br/> - <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 QString examplesBoxCss = - QString::fromLatin1("height: 168px; background-color: %1; ") - .arg(creatorColor(Theme::Token_Background_Default).name()); - description.append(QString(R"( - <div style="%1">%2</div> - <p style="%3"> - <br/><br/><br/><br/><br/><br/><br/><br/><br/><br/><br/> - </p> - )").arg(h6CapitalCss) - .arg(Tr::tr("Examples")) - .arg(examplesBoxCss)); + const ImagesData imagesData = current.data(RoleDescriptionImages).value<ImagesData>(); + if (!imagesData.isEmpty()) { + const QString examplesBoxCss = + QString::fromLatin1("height: 168px; background-color: %1; ") + .arg(creatorTheme()->color(Theme::Token_Background_Default).name()); + description.append(QString(R"( + <br/> + <div style="%1">%2</div> + <p style="%3"> + <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> + )").arg(h6CapitalCss) + .arg(Tr::tr("Examples")) + .arg(examplesBoxCss)); + } - const QString captionStrongCss = StyleHelper::fontToCssProperties( - StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong)); - description.append(QString(R"( - <div style="%1">%2</div> - <p> - <table> - <tr><td style="%3">%4</td><td>%5</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(Tr::tr("Extension library details")) - .arg(captionStrongCss) - .arg(Tr::tr("Size")) - .arg("547 MB") - .arg(Tr::tr("Version")) - .arg(extension->version()) - .arg(Tr::tr("Location")) - .arg(location.toUserOutput())); + // Library details vanished from the Figma designs. The data is available, though. + const bool showDetails = false; + if (showDetails) { + const QString captionStrongCss = StyleHelper::fontToCssProperties( + 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"( + <div style="%1">%2</div> + <p> + <table> + <tr><td style="%3">%4</td><td>%5</td></tr> + <tr><td style="%3">%6</td><td>%7</td></tr> + )").arg(h6Css) + .arg(Tr::tr("Extension library details")) + .arg(captionStrongCss) + .arg(Tr::tr("Size")) + .arg(sizeFmt) + .arg(Tr::tr("Version")) + .arg(version)); + if (!location.isEmpty()) { + 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); - m_primaryDescription->setText(description); + d->primaryDescription->setText(description); } { QString description = htmlStart; description.append(QString(R"( - <p style="%1"><br/>%2</p> + <p style="%1">%2</p> )").arg(h6CapitalCss) .arg(Tr::tr("Extension details"))); - description.append(QString(R"( - <div style="%1">%2</div> - <p>%3</p> - )").arg(h6Css) - .arg(Tr::tr("Released")) - .arg("23.5.2023")); - - const QString tagTemplate = QString(R"( - <td style="border: 1px solid %1; padding: 3px; ">%2</td> - )").arg(creatorColor(Theme::Token_Stroke_Subtle).name()); - const QStringList tags = Utils::transform(data.tags, - [&tagTemplate] (const QString &tag) { - return tagTemplate.arg(tag); - }); - description.append(QString(R"( - <div style="%1">%2</div> - <p>%3</p> - )").arg(h6Css) - .arg(Tr::tr("Related tags")) - .arg(tags.join(" "))); + const QStringList tags = current.data(RoleTags).toStringList(); + if (!tags.isEmpty()) { + const QString tagTemplate = QString(R"( + <td style="border: 1px solid %1; padding: 3px; ">%2</td> + )").arg(creatorTheme()->color(Theme::Token_Stroke_Subtle).name()); + const QStringList tagsFmt = Utils::transform(tags, [&tagTemplate](const QString &tag) { + return tagTemplate.arg(tag); + }); + description.append(QString(R"( + <div style="%1">%2</div> + <p>%3</p> + )").arg(h6Css) + .arg(Tr::tr("Related tags")) + .arg(tagsFmt.join(" "))); + } - description.append(QString(R"( - <div style="%1">%2</div> - <p> - macOS<br/> - Windows<br/> - Linux - </p> - )").arg(h6Css) - .arg(Tr::tr("Platforms"))); - - QStringList dependencies; - for (const ExtensionSystem::PluginSpec *spec : data.plugins) { - dependencies.append(Utils::transform(spec->dependencies(), - &ExtensionSystem::PluginDependency::toString)); + const QStringList platforms = current.data(RolePlatforms).toStringList(); + if (!platforms.isEmpty()) { + description.append(QString(R"( + <div style="%1">%2</div> + <p>%3</p> + )").arg(h6Css) + .arg(Tr::tr("Platforms")) + .arg(platforms.join("<br/>"))); + } + + const QStringList dependencies = current.data(RoleDependencies).toStringList(); + if (!dependencies.isEmpty()) { + const QString dependenciesFmt = dependencies.join("<br/>"); + description.append(QString(R"( + <div style="%1">%2</div> + <p>%3</p> + )").arg(h6Css) + .arg(Tr::tr("Dependencies")) + .arg(dependenciesFmt)); } - dependencies.removeDuplicates(); - dependencies.sort(); - description.append(QString(R"( - <div style="%1">%2</div> - <p>%3</p> - )").arg(h6Css) - .arg(Tr::tr("Dependencies")) - .arg(dependencies.isEmpty() ? "-" : dependencies.join("<br/>"))); if (isPack) { - const QStringList extensions = Utils::transform(data.plugins, - &ExtensionSystem::PluginSpec::name); + const PluginsData plugins = current.data(RolePlugins).value<PluginsData>(); + const QStringList extensions = Utils::transform(plugins, + &QPair<QString, QString>::first); + const QString extensionsFmt = extensions.join("<br/>"); description.append(QString(R"( <div style="%1">%2</div> <p>%3</p> )").arg(h6Css) - .arg(Tr::tr("Extensions in pack")) - .arg(extensions.join("<br/>"))); + .arg(Tr::tr("Extensions in pack")) + .arg(extensionsFmt)); } 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 |