ExtensionManager: Implement Extension details design

This implements the design for the "right side" of the extension
manager.

The introduced "header" shows the extension icon in a slightly bigger
variant. The "Install..." button that starts downloading and installing
of a plugin moved to the newly desigend "header".

The previous HTML based prototype has been split up into separate items
in order to achieve specialized sections like the images and tags.

Images are loaded via TaskTree and displayed as static image or as
animation.

Change-Id: Ifaf4a46c0a4789e77e76f9a44c8a15ee74c5e8df
Reviewed-by: Cristian Adam <cristian.adam@qt.io>
This commit is contained in:
Alessandro Portale
2024-06-06 14:40:58 +02:00
parent a41c4de3df
commit 81163b431e
12 changed files with 648 additions and 205 deletions

View File

@@ -2,10 +2,14 @@
<qresource prefix="/extensionmanager">
<file>images/download.png</file>
<file>images/download@2x.png</file>
<file>images/extensionbig.png</file>
<file>images/extensionbig@2x.png</file>
<file>images/extensionsmall.png</file>
<file>images/extensionsmall@2x.png</file>
<file>images/mode_extensionmanager_mask.png</file>
<file>images/mode_extensionmanager_mask@2x.png</file>
<file>images/packbig.png</file>
<file>images/packbig@2x.png</file>
<file>images/packsmall.png</file>
<file>images/packsmall@2x.png</file>
</qresource>

View File

@@ -2,5 +2,6 @@
<qresource prefix="/extensionmanager">
<file>testdata/defaultpacks.json</file>
<file>testdata/thirdpartyplugins.json</file>
<file>testdata/varieddata.json</file>
</qresource>
</RCC>

View File

@@ -33,16 +33,56 @@
#include <utils/utilsicons.h>
#include <QAction>
#include <QApplication>
#include <QBuffer>
#include <QCheckBox>
#include <QHBoxLayout>
#include <QImageReader>
#include <QMessageBox>
#include <QTextBrowser>
#include <QMovie>
#include <QPainter>
#include <QProgressDialog>
#include <QScrollArea>
#include <QSignalMapper>
using namespace Core;
using namespace Utils;
using namespace StyleHelper;
using namespace WelcomePageHelpers;
namespace ExtensionManager::Internal {
constexpr TextFormat h5TF
{Theme::Token_Text_Default, UiElement::UiElementH5};
constexpr TextFormat h6TF
{h5TF.themeColor, UiElement::UiElementH6};
constexpr TextFormat h6CapitalTF
{Theme::Token_Text_Muted, UiElement::UiElementH6Capital};
constexpr TextFormat contentTF
{Theme::Token_Text_Default, UiElement::UiElementBody2};
static QLabel *sectionTitle(const TextFormat &tf, const QString &title)
{
QLabel *label = tfLabel(tf, true);
label->setText(title);
return label;
};
static QWidget *toScrollableColumn(QWidget *widget)
{
widget->setContentsMargins(SpacingTokens::ExVPaddingGapXl, SpacingTokens::ExVPaddingGapXl,
SpacingTokens::ExVPaddingGapXl, SpacingTokens::ExVPaddingGapXl);
widget->setSizePolicy(QSizePolicy::Ignored, QSizePolicy::Minimum);
auto scrollArea = new QScrollArea;
scrollArea->setWidget(widget);
scrollArea->setWidgetResizable(true);
scrollArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
scrollArea->setFrameStyle(QFrame::NoFrame);
return scrollArea;
};
class CollapsingWidget : public QWidget
{
public:
@@ -68,6 +108,169 @@ private:
int m_width = 100;
};
class HeadingWidget : public QWidget
{
static constexpr QSize iconBgS{68, 68};
static constexpr int dividerH = 16;
Q_OBJECT
public:
explicit HeadingWidget(QWidget *parent = nullptr)
: QWidget(parent)
{
m_icon = new QLabel;
m_icon->setFixedSize(iconBgS);
static const TextFormat titleTF
{Theme::Token_Text_Default, UiElementH4};
static const TextFormat vendorTF
{Theme::Token_Text_Accent, UiElementLabelMedium};
static const TextFormat dlTF
{Theme::Token_Text_Muted, vendorTF.uiElement};
static const TextFormat detailsTF
{Theme::Token_Text_Default, UiElementBody2};
m_title = tfLabel(titleTF);
m_vendor = new Button({}, Button::SmallLink);
m_vendor->setContentsMargins({});
m_divider = new QLabel;
m_divider->setFixedSize(1, dividerH);
WelcomePageHelpers::setBackgroundColor(m_divider, dlTF.themeColor);
m_dlIcon = new QLabel;
const QPixmap dlIcon = Icon({{":/extensionmanager/images/download.png", dlTF.themeColor}},
Icon::Tint).pixmap();
m_dlIcon->setPixmap(dlIcon);
m_dlCount = tfLabel(dlTF);
m_dlCount->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Preferred);
m_details = tfLabel(detailsTF);
installButton = new Button(Tr::tr("Install..."), Button::MediumPrimary);
installButton->setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
installButton->hide();
using namespace Layouting;
Row {
m_icon,
Column {
m_title,
st,
Row {
m_vendor,
Widget {
bindTo(&m_dlCountItems),
Row {
Space(SpacingTokens::HGapXs),
m_divider,
Space(SpacingTokens::HGapXs),
m_dlIcon,
Space(SpacingTokens::HGapXxs),
m_dlCount,
noMargin, spacing(0),
},
},
},
st,
m_details,
spacing(0),
},
Column {
installButton,
st,
},
noMargin, spacing(SpacingTokens::ExPaddingGapL),
}.attachTo(this);
setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Maximum);
m_dlCountItems->setVisible(false);
connect(installButton, &QAbstractButton::pressed,
this, &HeadingWidget::pluginInstallationRequested);
connect(m_vendor, &QAbstractButton::pressed, this, [this]() {
emit vendorClicked(m_currentVendor);
});
update({});
}
void update(const QModelIndex &current)
{
if (!current.isValid())
return;
m_icon->setPixmap(icon(current));
const QString name = current.data(RoleName).toString();
m_title->setText(name);
m_currentVendor = current.data(RoleVendor).toString();
m_vendor->setText(m_currentVendor);
const int dlCount = current.data(RoleDownloadCount).toInt();
const bool showDlCount = dlCount > 0;
if (showDlCount)
m_dlCount->setText(QString::number(dlCount));
m_dlCountItems->setVisible(showDlCount);
const auto pluginData = current.data(RolePlugins).value<PluginsData>();
if (current.data(RoleItemType).toInt() == ItemTypePack) {
const int pluginsCount = pluginData.count();
const QString details = Tr::tr("Pack contains %n plugins.", nullptr, pluginsCount);
m_details->setText(details);
} else {
m_details->setText({});
}
const ItemType itemType = current.data(RoleItemType).value<ItemType>();
const bool isPack = itemType == ItemTypePack;
const bool isRemotePlugin = !(isPack || ExtensionsModel::pluginSpecForName(name));
installButton->setVisible(isRemotePlugin && !pluginData.empty());
if (installButton->isVisible())
installButton->setToolTip(pluginData.constFirst().second);
}
signals:
void pluginInstallationRequested();
void vendorClicked(const QString &vendor);
private:
static QPixmap icon(const QModelIndex &index)
{
const qreal dpr = qApp->devicePixelRatio();
QPixmap pixmap(iconBgS * dpr);
pixmap.fill(Qt::transparent);
pixmap.setDevicePixelRatio(dpr);
const QRect bgR(QPoint(), pixmap.deviceIndependentSize().toSize());
QPainter p(&pixmap);
QLinearGradient gradient(bgR.topRight(), bgR.bottomLeft());
gradient.setStops(iconGradientStops(index));
constexpr int iconRectRounding = 4;
WelcomePageHelpers::drawCardBackground(&p, bgR, gradient, Qt::NoPen, iconRectRounding);
// Icon
constexpr Theme::Color color = Theme::Token_Basic_White;
static const QIcon pack = Icon({{":/extensionmanager/images/packbig.png", color}},
Icon::Tint).icon();
static const QIcon extension = Icon({{":/extensionmanager/images/extensionbig.png",
color}}, Icon::Tint).icon();
const ItemType itemType = index.data(RoleItemType).value<ItemType>();
(itemType == ItemTypePack ? pack : extension).paint(&p, bgR);
return pixmap;
}
QLabel *m_icon;
QLabel *m_title;
Button *m_vendor;
QLabel *m_divider;
QLabel *m_dlIcon;
QLabel *m_dlCount;
QWidget *m_dlCountItems;
QLabel *m_details;
QAbstractButton *installButton;
QString m_currentVendor;
};
class PluginStatusWidget : public QWidget
{
public:
@@ -128,49 +331,149 @@ private:
QString m_pluginName;
};
class TagList : public QWidget
{
Q_OBJECT
public:
explicit TagList(QWidget *parent = nullptr)
: QWidget(parent)
{
QHBoxLayout *layout = new QHBoxLayout(this);
setLayout(layout);
layout->setContentsMargins({});
m_signalMapper = new QSignalMapper(this);
connect(m_signalMapper, &QSignalMapper::mappedString, this, &TagList::tagSelected);
}
void setTags(const QStringList &tags)
{
if (m_container) {
delete m_container;
m_container = nullptr;
}
if (!tags.empty()) {
m_container = new QWidget(this);
layout()->addWidget(m_container);
using namespace Layouting;
Flow flow {};
flow.setNoMargins();
flow.setSpacing(SpacingTokens::HGapXs);
for (const QString &tag : tags) {
QAbstractButton *tagButton = new Button(tag, Button::Tag);
connect(tagButton, &QAbstractButton::clicked,
m_signalMapper, qOverload<>(&QSignalMapper::map));
m_signalMapper->setMapping(tagButton, tag);
flow.addItem(tagButton);
}
flow.attachTo(m_container);
}
updateGeometry();
}
signals:
void tagSelected(const QString &tag);
private:
QWidget *m_container = nullptr;
QSignalMapper *m_signalMapper;
};
class ExtensionManagerWidgetPrivate
{
public:
QString currentItemName;
ExtensionsBrowser *leftColumn;
ExtensionsBrowser *extensionBrowser;
CollapsingWidget *secondaryDescriptionWidget;
QTextBrowser *primaryDescription;
QTextBrowser *secondaryDescription;
HeadingWidget *headingWidget;
QWidget *primaryContent;
QWidget *secondaryContent;
QLabel *description;
QLabel *linksTitle;
QLabel *links;
QLabel *imageTitle;
QLabel *image;
QBuffer imageDataBuffer;
QMovie imageMovie;
QLabel *tagsTitle;
TagList *tags;
QLabel *platformsTitle;
QLabel *platforms;
QLabel *dependenciesTitle;
QLabel *dependencies;
QLabel *packExtensionsTitle;
QLabel *packExtensions;
PluginStatusWidget *pluginStatus;
QAbstractButton *installButton;
PluginsData currentItemPlugins;
Tasking::TaskTreeRunner taskTreeRunner;
Tasking::TaskTreeRunner dlTaskTreeRunner;
Tasking::TaskTreeRunner imgTaskTreeRunner;
};
ExtensionManagerWidget::ExtensionManagerWidget(QWidget *parent)
: ResizeSignallingWidget(parent)
, d(new ExtensionManagerWidgetPrivate)
{
d->leftColumn = new ExtensionsBrowser;
d->extensionBrowser = new ExtensionsBrowser;
auto descriptionColumns = new QWidget;
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);
d->pluginStatus = new PluginStatusWidget;
d->installButton = new Button(Tr::tr("Install..."), Button::MediumPrimary);
d->installButton->hide();
d->headingWidget = new HeadingWidget;
d->description = tfLabel(contentTF, false);
d->description->setWordWrap(true);
d->linksTitle = sectionTitle(h6CapitalTF, Tr::tr("More information"));
d->links = tfLabel(contentTF, false);
d->imageTitle = sectionTitle(h6CapitalTF, {});
d->image = new QLabel;
d->imageMovie.setDevice(&d->imageDataBuffer);
using namespace Layouting;
auto primary = new QWidget;
const auto spL = spacing(SpacingTokens::VPaddingL);
Column {
d->description,
Column { d->linksTitle, d->links, spL },
Column { d->imageTitle, d->image, spL },
st,
noMargin, spacing(SpacingTokens::ExVPaddingGapXl),
}.attachTo(primary);
d->primaryContent = toScrollableColumn(primary);
d->tagsTitle = sectionTitle(h6TF, Tr::tr("Tags"));
d->tags = new TagList;
d->platformsTitle = sectionTitle(h6TF, Tr::tr("Platforms"));
d->platforms = tfLabel(contentTF, false);
d->dependenciesTitle = sectionTitle(h6TF, Tr::tr("Dependencies"));
d->dependencies = tfLabel(contentTF, false);
d->packExtensionsTitle = sectionTitle(h6TF, Tr::tr("Extensions in pack"));
d->packExtensions = tfLabel(contentTF, false);
d->pluginStatus = new PluginStatusWidget;
auto secondary = new QWidget;
const auto spXxs = spacing(SpacingTokens::VPaddingXxs);
Column {
sectionTitle(h6CapitalTF, Tr::tr("Extension details")),
Column {
Column { d->tagsTitle, d->tags, spXxs },
Column { d->platformsTitle, d->platforms, spXxs },
Column { d->dependenciesTitle, d->dependencies, spXxs },
Column { d->packExtensionsTitle, d->packExtensions, spXxs },
spacing(SpacingTokens::VPaddingL),
},
st,
noMargin, spacing(SpacingTokens::ExVPaddingGapXl),
}.attachTo(secondary);
d->secondaryContent = toScrollableColumn(secondary);
Row {
WelcomePageHelpers::createRule(Qt::Vertical),
Column {
d->secondaryDescription,
d->secondaryContent,
d->pluginStatus,
d->installButton,
},
noMargin, spacing(0),
}.attachTo(d->secondaryDescriptionWidget);
@@ -178,34 +481,44 @@ ExtensionManagerWidget::ExtensionManagerWidget(QWidget *parent)
Row {
WelcomePageHelpers::createRule(Qt::Vertical),
Row {
d->primaryDescription,
noMargin,
Column {
Column {
d->headingWidget,
customMargins(SpacingTokens::ExVPaddingGapXl, SpacingTokens::ExVPaddingGapXl,
SpacingTokens::ExVPaddingGapXl, SpacingTokens::ExVPaddingGapXl),
},
d->primaryContent,
},
},
d->secondaryDescriptionWidget,
noMargin, spacing(0),
}.attachTo(descriptionColumns);
Row {
Space(StyleHelper::SpacingTokens::ExVPaddingGapXl),
d->leftColumn,
Space(SpacingTokens::ExVPaddingGapXl),
d->extensionBrowser,
descriptionColumns,
noMargin, spacing(0),
}.attachTo(this);
WelcomePageHelpers::setBackgroundColor(this, Theme::Token_Background_Default);
connect(d->leftColumn, &ExtensionsBrowser::itemSelected,
connect(d->extensionBrowser, &ExtensionsBrowser::itemSelected,
this, &ExtensionManagerWidget::updateView);
connect(this, &ResizeSignallingWidget::resized, this, [this](const QSize &size) {
const int intendedLeftColumnWidth = size.width() - 580;
d->leftColumn->adjustToWidth(intendedLeftColumnWidth);
const int intendedBrowserColumnWidth = size.width() - 580;
d->extensionBrowser->adjustToWidth(intendedBrowserColumnWidth);
const bool secondaryDescriptionVisible = size.width() > 970;
const int secondaryDescriptionWidth = secondaryDescriptionVisible ? 264 : 0;
d->secondaryDescriptionWidget->setWidth(secondaryDescriptionWidth);
});
connect(d->installButton, &QAbstractButton::pressed, this, [this]() {
connect(d->headingWidget, &HeadingWidget::pluginInstallationRequested, this, [this](){
fetchAndInstallPlugin(QUrl::fromUserInput(d->currentItemPlugins.constFirst().second));
});
connect(d->tags, &TagList::tagSelected, d->extensionBrowser, &ExtensionsBrowser::setFilter);
connect(d->headingWidget, &HeadingWidget::vendorClicked,
d->extensionBrowser, &ExtensionsBrowser::setFilter);
updateView({});
}
@@ -216,199 +529,116 @@ ExtensionManagerWidget::~ExtensionManagerWidget()
void ExtensionManagerWidget::updateView(const QModelIndex &current)
{
const QString h5Css =
StyleHelper::fontToCssProperties(StyleHelper::uiFont(StyleHelper::UiElementH5))
+ "; 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("; margin-top: 0px; color: %1;")
.arg(creatorColor(Theme::Token_Text_Muted).name());
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"><br/>
)").arg(bodyStyle);
const QString htmlEnd = QString(R"(
</body></html>
)");
d->headingWidget->update(current);
if (!current.isValid()) {
const QString emptyHtml = htmlStart + htmlEnd;
d->primaryDescription->setText(emptyHtml);
d->secondaryDescription->setText(emptyHtml);
const bool showContent = current.isValid();
d->primaryContent->setVisible(showContent);
d->secondaryContent->setVisible(showContent);
if (!showContent)
return;
}
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);
{
QString description = htmlStart;
auto toContentParagraph = [](const QString &text) {
const QString pHtml = QString::fromLatin1("<p style=\"margin-top:0;margin-bottom:0;"
"line-height:%1px\">%2</p>")
.arg(contentTF.lineHeight()).arg(text);
return pHtml;
};
QString descriptionHtml;
{
const TextData textData = current.data(RoleDescriptionText).value<TextData>();
const bool hasDescription = !textData.isEmpty();
if (hasDescription) {
const QString headerCssTemplate =
";margin-top:%1;margin-bottom:%2;padding-top:0;padding-bottom:0;";
const QString h4Css = fontToCssProperties(uiFont(UiElementH4))
+ headerCssTemplate.arg(0).arg(SpacingTokens::VGapL);
const QString h5Css = fontToCssProperties(uiFont(UiElementH5))
+ headerCssTemplate.arg(SpacingTokens::ExVPaddingGapXl)
.arg(SpacingTokens::VGapL);
QString descriptionHtml;
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)
QString::fromLatin1("<div style=\"%1\">%2</div>%3")
.arg(descriptionHtml.isEmpty() ? h4Css : h5Css)
.arg(text.first)
.arg(text.second.join("<br/>"));
.arg(toContentParagraph(text.second.join("<br/>")));
descriptionHtml.append(paragraph);
}
descriptionHtml.prepend(QString::fromLatin1("<body style=\"color:%1;\">")
.arg(creatorColor(Theme::Token_Text_Default).name()));
descriptionHtml.append("</body>");
d->description->setText(descriptionHtml);
}
description.append(descriptionHtml);
d->description->setVisible(hasDescription);
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()) {
const bool hasLinks = !linksData.isEmpty();
if (hasLinks) {
QString linksHtml;
const QStringList links = transform(linksData, [](const LinksData::Type &link) {
const QString anchor = link.first.isEmpty() ? link.second : link.first;
return QString::fromLatin1("<a href=\"%1\">%2 &gt;</a>")
.arg(link.second).arg(anchor);
return QString::fromLatin1(R"(<a href="%1" style="color:%2">%3 &gt;</a>)")
.arg(link.second)
.arg(creatorColor(Theme::Token_Text_Accent).name())
.arg(anchor);
});
linksHtml = links.join("<br/>");
description.append(QString::fromLatin1("<p>%1</p>").arg(linksHtml));
d->links->setText(toContentParagraph(linksHtml));
}
d->linksTitle->setVisible(hasLinks);
d->links->setVisible(hasLinks);
d->imgTaskTreeRunner.reset();
d->imageMovie.stop();
d->imageDataBuffer.close();
d->image->clear();
const ImagesData imagesData = current.data(RoleDescriptionImages).value<ImagesData>();
if (!imagesData.isEmpty()) {
const QString examplesBoxCss =
QString::fromLatin1("height: 168px; background-color: %1; ")
.arg(creatorColor(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 bool hasImages = !imagesData.isEmpty();
if (hasImages) {
const ImagesData::Type &image = imagesData.constFirst(); // Only show one image
d->imageTitle->setText(image.first);
fetchAndDisplayImage(image.second);
}
// 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);
d->primaryDescription->setText(description);
d->imageTitle->setVisible(hasImages);
d->image->setVisible(hasImages);
}
{
QString description = htmlStart;
description.append(QString(R"(
<p style="%1">%2</p>
)").arg(h6CapitalCss)
.arg(Tr::tr("Extension details")));
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(creatorColor(Theme::Token_Stroke_Subtle).name());
const QStringList tagsFmt = 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("&nbsp;")));
}
d->tags->setTags(tags);
const bool hasTags = !tags.isEmpty();
d->tagsTitle->setVisible(hasTags);
d->tags->setVisible(hasTags);
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 bool hasPlatforms = !platforms.isEmpty();
if (hasPlatforms)
d->platforms->setText(toContentParagraph(platforms.join("<br/>")));
d->platformsTitle->setVisible(hasPlatforms);
d->platforms->setVisible(hasPlatforms);
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));
}
const bool hasDependencies = !dependencies.isEmpty();
if (hasDependencies)
d->dependencies->setText(toContentParagraph(dependencies.join("<br/>")));
d->dependenciesTitle->setVisible(hasDependencies);
d->dependencies->setVisible(hasDependencies);
if (isPack) {
const PluginsData plugins = current.data(RolePlugins).value<PluginsData>();
const bool hasExtensions = isPack && !plugins.isEmpty();
if (hasExtensions) {
const QStringList extensions = 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(extensionsFmt));
d->packExtensions->setText(toContentParagraph(extensions.join("<br/>")));
}
description.append(htmlEnd);
d->secondaryDescription->setText(description);
d->packExtensionsTitle->setVisible(hasExtensions);
d->packExtensions->setVisible(hasExtensions);
}
}
@@ -469,7 +699,56 @@ void ExtensionManagerWidget::fetchAndInstallPlugin(const QUrl &url)
onGroupDone(onPluginInstallation),
};
d->taskTreeRunner.start(group);
d->dlTaskTreeRunner.start(group);
}
void ExtensionManagerWidget::fetchAndDisplayImage(const QUrl &url)
{
using namespace Tasking;
struct StorageStruct
{
QByteArray imageData;
QUrl url;
};
Storage<StorageStruct> storage;
const auto onFetchSetup = [url, storage](NetworkQuery &query) {
storage->url = url;
query.setRequest(QNetworkRequest(url));
query.setNetworkAccessManager(NetworkAccessManager::instance());
};
const auto onFetchDone = [storage](const NetworkQuery &query, DoneWith result) {
if (result == DoneWith::Success)
storage->imageData = query.reply()->readAll();
};
const auto onShowImage = [storage, this]() {
if (storage->imageData.isEmpty())
return;
d->imageDataBuffer.setData(storage->imageData);
if (!d->imageDataBuffer.open(QIODevice::ReadOnly))
return;
QImageReader reader(&d->imageDataBuffer);
const bool animated = reader.supportsAnimation();
if (animated) {
d->image->setMovie(&d->imageMovie);
d->imageMovie.start();
} else {
const QPixmap pixmap = QPixmap::fromImage(reader.read());
d->image->setPixmap(pixmap);
}
};
Group group{
storage,
NetworkQueryTask{onFetchSetup, onFetchDone},
onGroupDone(onShowImage),
};
d->imgTaskTreeRunner.start(group);
}
} // ExtensionManager::Internal
#include "extensionmanagerwidget.moc"

View File

@@ -14,6 +14,7 @@ public:
private:
void updateView(const QModelIndex &current);
void fetchAndInstallPlugin(const QUrl &url);
void fetchAndDisplayImage(const QUrl &url);
class ExtensionManagerWidgetPrivate *d = nullptr;
};

View File

@@ -26,6 +26,7 @@
#include <solutions/tasking/tasktree.h>
#include <solutions/tasking/tasktreerunner.h>
#include <utils/elidinglabel.h>
#include <utils/fancylineedit.h>
#include <utils/icon.h>
#include <utils/layoutbuilder.h>
@@ -141,10 +142,7 @@ public:
}
{
QLinearGradient gradient(iconBgR.topRight(), iconBgR.bottomLeft());
const QColor startColor = creatorColor(Utils::Theme::Token_Gradient01_Start);
const QColor endColor = creatorColor(Utils::Theme::Token_Gradient01_End);
gradient.setColorAt(0, startColor);
gradient.setColorAt(1, endColor);
gradient.setStops(iconGradientStops(index));
constexpr int iconRectRounding = 4;
drawCardBackground(painter, iconBgR, gradient, Qt::NoPen, iconRectRounding);
@@ -267,11 +265,13 @@ ExtensionsBrowser::ExtensionsBrowser(QWidget *parent)
{
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
auto manageLabel = new QLabel(Tr::tr("Manage Extensions"));
manageLabel->setFont(uiFont(UiElementH1));
static const TextFormat titleTF
{Theme::Token_Text_Default, UiElementH2};
QLabel *titleLabel = tfLabel(titleTF);
titleLabel->setText(Tr::tr("Manage Extensions"));
d->searchBox = new SearchBox;
d->searchBox->setFixedWidth(itemWidth);
d->searchBox->setPlaceholderText(Tr::tr("Search"));
d->updateButton = new Button(Tr::tr("Install..."), Button::MediumPrimary);
d->model = new ExtensionsModel(this);
@@ -294,11 +294,17 @@ ExtensionsBrowser::ExtensionsBrowser(QWidget *parent)
using namespace Layouting;
Column {
Space(15),
manageLabel,
Space(15),
Row { d->searchBox, st, d->updateButton, Space(extraListViewWidth() + gapSize) },
Space(gapSize),
Column {
titleLabel,
customMargins(0, VPaddingM, 0, VPaddingM),
},
Row {
d->searchBox,
d->updateButton,
spacing(gapSize),
customMargins(0, VPaddingM, extraListViewWidth() + gapSize, VPaddingM),
},
Space(ExPaddingGapL),
d->extensionsView,
noMargin, spacing(0),
}.attachTo(this);
@@ -335,6 +341,11 @@ ExtensionsBrowser::~ExtensionsBrowser()
delete d;
}
void ExtensionsBrowser::setFilter(const QString &filter)
{
d->searchBox->setText(filter);
}
void ExtensionsBrowser::adjustToWidth(const int width)
{
const int widthForItems = width - extraListViewWidth();
@@ -358,8 +369,6 @@ int ExtensionsBrowser::extraListViewWidth() const
void ExtensionsBrowser::fetchExtensions()
{
// d->model->setExtensionsJson(testData("thirdpartyplugins")); return;
using namespace Tasking;
const auto onQuerySetup = [](NetworkQuery &query) {
@@ -367,12 +376,11 @@ void ExtensionsBrowser::fetchExtensions()
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());
const QString request = url.arg(host) + requestTemplate
.arg(QCoreApplication::applicationVersion())
.arg(QSysInfo::productType())
.arg(QSysInfo::productVersion())
.arg(QSysInfo::currentCpuArchitecture());
query.setRequest(QNetworkRequest(QUrl::fromUserInput(request)));
query.setNetworkAccessManager(NetworkAccessManager::instance());
@@ -380,6 +388,7 @@ void ExtensionsBrowser::fetchExtensions()
const auto onQueryDone = [this](const NetworkQuery &query, DoneWith result) {
if (result != DoneWith::Success) {
#ifdef WITH_TESTS
// Available test sets: "defaultpacks", "varieddata", "thirdpartyplugins"
d->model->setExtensionsJson(testData("defaultpacks"));
#endif // WITH_TESTS
return;
@@ -395,4 +404,33 @@ void ExtensionsBrowser::fetchExtensions()
d->taskTreeRunner.start(group);
}
QLabel *tfLabel(const TextFormat &tf, bool singleLine)
{
QLabel *label = singleLine ? new Utils::ElidingLabel : new QLabel;
if (singleLine)
label->setFixedHeight(tf.lineHeight());
label->setFont(tf.font());
label->setAlignment(Qt::Alignment(tf.drawTextFlags));
QPalette pal = label->palette();
pal.setColor(QPalette::WindowText, tf.color());
label->setPalette(pal);
return label;
}
QGradientStops iconGradientStops(const QModelIndex &index)
{
const bool isVendorExtension = index.data(RoleVendor).toString() == "The Qt Company Ltd";
const QColor startColor = creatorColor(isVendorExtension ? Theme::Token_Gradient01_Start
: Theme::Token_Gradient02_Start);
const QColor endColor = creatorColor(isVendorExtension ? Theme::Token_Gradient01_End
: Theme::Token_Gradient02_End);
const QGradientStops gradient = {
{0, startColor},
{1, endColor},
};
return gradient;
}
} // ExtensionManager::Internal

View File

@@ -5,6 +5,12 @@
#include <QWidget>
QT_FORWARD_DECLARE_CLASS(QLabel)
namespace Core::WelcomePageHelpers {
class TextFormat;
}
namespace ExtensionManager::Internal {
class ExtensionsBrowser final : public QWidget
@@ -15,6 +21,8 @@ public:
ExtensionsBrowser(QWidget *parent = nullptr);
~ExtensionsBrowser();
void setFilter(const QString &filter);
void adjustToWidth(const int width);
QSize sizeHint() const override;
@@ -29,4 +37,7 @@ private:
class ExtensionsBrowserPrivate *d = nullptr;
};
QLabel *tfLabel(const Core::WelcomePageHelpers::TextFormat &tf, bool singleLine = true);
QGradientStops iconGradientStops(const QModelIndex &index);
} // ExtensionManager::Internal

Binary file not shown.

After

Width:  |  Height:  |  Size: 509 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 455 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 785 B

View File

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

View File

@@ -3818,6 +3818,20 @@
style="fill:none;stroke:#000000;stroke-width:2;stroke-linejoin:round"
sodipodi:nodetypes="cccccccccccc" />
</g>
<g
id="src/plugins/extensionmanager/images/packbig">
<use
x="0"
y="0"
xlink:href="#backgroundRect_32_28"
id="use16"
style="display:inline"
transform="matrix(0.97058824,0,0,1,-39.794118,403)" />
<path
id="path13"
d="m 16.5,456.16667 v 16 M 30.5,450 16.5,456.16667 2.5,450 m 0,16.66666 14,6.16667 14,-6.16667 v -17.33333 l -14,-5.83333 -14,5.83333 z"
style="fill:none;stroke:#000000;stroke-width:3;stroke-linejoin:round" />
</g>
<g
id="src/plugins/extensionmanager/images/extensionsmall">
<use
@@ -3838,6 +3852,25 @@
id="path32048"
sodipodi:nodetypes="ccccc" />
</g>
<g
id="src/plugins/extensionmanager/images/extensionbig">
<use
x="0"
y="0"
xlink:href="#backgroundRect_32_28"
id="use13"
style="display:inline"
transform="matrix(0.97058824,0,0,1,-5.794118,403)" />
<path
style="fill:#000000;fill-opacity:1;stroke:#000000;stroke-width:3;stroke-linecap:round;stroke-linejoin:round"
d="M 55.454565,470.65463 64.5,466.667 v -11.17266 l -9,3.95638 z"
id="path19"
sodipodi:nodetypes="ccccc" />
<path
id="path14"
style="fill:none;stroke:#000000;stroke-width:3;stroke-linejoin:round;stroke-linecap:round"
d="m 50.5,456.167 v 5.333 m 0,5.333 v 5.334 m 14,-22.167 -4.6667,2.056 m -4.6666,2.055 -4.6667,2.056 -4.6667,-2.056 M 41.1667,452.056 36.5,450 m 4.6667,-2.611 -4.6667,1.944 v 5.778 m 0,5.778 v 5.778 l 4.6667,2.055 m 4.6666,2.056 4.6667,2.055 m 14,-23.5 -4.6667,-1.944 M 55.1667,445.444 50.5,443.5 l -4.6667,1.944" />
</g>
<g
id="src/plugins/extensionmanager/images/download">
<rect

Before

Width:  |  Height:  |  Size: 373 KiB

After

Width:  |  Height:  |  Size: 374 KiB