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"> <qresource prefix="/extensionmanager">
<file>images/download.png</file> <file>images/download.png</file>
<file>images/download@2x.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.png</file>
<file>images/extensionsmall@2x.png</file> <file>images/extensionsmall@2x.png</file>
<file>images/mode_extensionmanager_mask.png</file> <file>images/mode_extensionmanager_mask.png</file>
<file>images/mode_extensionmanager_mask@2x.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.png</file>
<file>images/packsmall@2x.png</file> <file>images/packsmall@2x.png</file>
</qresource> </qresource>

View File

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

View File

@@ -33,16 +33,56 @@
#include <utils/utilsicons.h> #include <utils/utilsicons.h>
#include <QAction> #include <QAction>
#include <QApplication>
#include <QBuffer>
#include <QCheckBox> #include <QCheckBox>
#include <QHBoxLayout>
#include <QImageReader>
#include <QMessageBox> #include <QMessageBox>
#include <QTextBrowser> #include <QMovie>
#include <QPainter>
#include <QProgressDialog> #include <QProgressDialog>
#include <QScrollArea>
#include <QSignalMapper>
using namespace Core; using namespace Core;
using namespace Utils; using namespace Utils;
using namespace StyleHelper;
using namespace WelcomePageHelpers;
namespace ExtensionManager::Internal { 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 class CollapsingWidget : public QWidget
{ {
public: public:
@@ -68,6 +108,169 @@ private:
int m_width = 100; 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 class PluginStatusWidget : public QWidget
{ {
public: public:
@@ -128,49 +331,149 @@ private:
QString m_pluginName; 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 class ExtensionManagerWidgetPrivate
{ {
public: public:
QString currentItemName; QString currentItemName;
ExtensionsBrowser *leftColumn; ExtensionsBrowser *extensionBrowser;
CollapsingWidget *secondaryDescriptionWidget; CollapsingWidget *secondaryDescriptionWidget;
QTextBrowser *primaryDescription; HeadingWidget *headingWidget;
QTextBrowser *secondaryDescription; 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; PluginStatusWidget *pluginStatus;
QAbstractButton *installButton;
PluginsData currentItemPlugins; PluginsData currentItemPlugins;
Tasking::TaskTreeRunner taskTreeRunner; Tasking::TaskTreeRunner dlTaskTreeRunner;
Tasking::TaskTreeRunner imgTaskTreeRunner;
}; };
ExtensionManagerWidget::ExtensionManagerWidget(QWidget *parent) ExtensionManagerWidget::ExtensionManagerWidget(QWidget *parent)
: ResizeSignallingWidget(parent) : ResizeSignallingWidget(parent)
, d(new ExtensionManagerWidgetPrivate) , d(new ExtensionManagerWidgetPrivate)
{ {
d->leftColumn = new ExtensionsBrowser; d->extensionBrowser = new ExtensionsBrowser;
auto descriptionColumns = new QWidget; auto descriptionColumns = new QWidget;
d->secondaryDescriptionWidget = new CollapsingWidget; d->secondaryDescriptionWidget = new CollapsingWidget;
d->primaryDescription = new QTextBrowser; d->headingWidget = new HeadingWidget;
d->primaryDescription->setOpenExternalLinks(true); d->description = tfLabel(contentTF, false);
d->primaryDescription->setFrameStyle(QFrame::NoFrame); d->description->setWordWrap(true);
d->linksTitle = sectionTitle(h6CapitalTF, Tr::tr("More information"));
d->secondaryDescription = new QTextBrowser; d->links = tfLabel(contentTF, false);
d->secondaryDescription->setFrameStyle(QFrame::NoFrame); d->imageTitle = sectionTitle(h6CapitalTF, {});
d->image = new QLabel;
d->pluginStatus = new PluginStatusWidget; d->imageMovie.setDevice(&d->imageDataBuffer);
d->installButton = new Button(Tr::tr("Install..."), Button::MediumPrimary);
d->installButton->hide();
using namespace Layouting; 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 { Row {
WelcomePageHelpers::createRule(Qt::Vertical), WelcomePageHelpers::createRule(Qt::Vertical),
Column { Column {
d->secondaryDescription, d->secondaryContent,
d->pluginStatus, d->pluginStatus,
d->installButton,
}, },
noMargin, spacing(0), noMargin, spacing(0),
}.attachTo(d->secondaryDescriptionWidget); }.attachTo(d->secondaryDescriptionWidget);
@@ -178,34 +481,44 @@ ExtensionManagerWidget::ExtensionManagerWidget(QWidget *parent)
Row { Row {
WelcomePageHelpers::createRule(Qt::Vertical), WelcomePageHelpers::createRule(Qt::Vertical),
Row { Row {
d->primaryDescription, Column {
noMargin, Column {
d->headingWidget,
customMargins(SpacingTokens::ExVPaddingGapXl, SpacingTokens::ExVPaddingGapXl,
SpacingTokens::ExVPaddingGapXl, SpacingTokens::ExVPaddingGapXl),
},
d->primaryContent,
},
}, },
d->secondaryDescriptionWidget, d->secondaryDescriptionWidget,
noMargin, spacing(0), noMargin, spacing(0),
}.attachTo(descriptionColumns); }.attachTo(descriptionColumns);
Row { Row {
Space(StyleHelper::SpacingTokens::ExVPaddingGapXl), Space(SpacingTokens::ExVPaddingGapXl),
d->leftColumn, d->extensionBrowser,
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(d->leftColumn, &ExtensionsBrowser::itemSelected, connect(d->extensionBrowser, &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 intendedBrowserColumnWidth = size.width() - 580;
d->leftColumn->adjustToWidth(intendedLeftColumnWidth); d->extensionBrowser->adjustToWidth(intendedBrowserColumnWidth);
const bool secondaryDescriptionVisible = size.width() > 970; const bool secondaryDescriptionVisible = size.width() > 970;
const int secondaryDescriptionWidth = secondaryDescriptionVisible ? 264 : 0; const int secondaryDescriptionWidth = secondaryDescriptionVisible ? 264 : 0;
d->secondaryDescriptionWidget->setWidth(secondaryDescriptionWidth); 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)); 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({}); updateView({});
} }
@@ -216,199 +529,116 @@ ExtensionManagerWidget::~ExtensionManagerWidget()
void ExtensionManagerWidget::updateView(const QModelIndex &current) void ExtensionManagerWidget::updateView(const QModelIndex &current)
{ {
const QString h5Css = d->headingWidget->update(current);
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>
)");
if (!current.isValid()) { const bool showContent = current.isValid();
const QString emptyHtml = htmlStart + htmlEnd; d->primaryContent->setVisible(showContent);
d->primaryDescription->setText(emptyHtml); d->secondaryContent->setVisible(showContent);
d->secondaryDescription->setText(emptyHtml); if (!showContent)
return; return;
}
d->currentItemName = current.data().toString(); d->currentItemName = current.data().toString();
const bool isPack = current.data(RoleItemType) == ItemTypePack; const bool isPack = current.data(RoleItemType) == ItemTypePack;
d->pluginStatus->setPluginName(isPack ? QString() : d->currentItemName); d->pluginStatus->setPluginName(isPack ? QString() : d->currentItemName);
const bool isRemotePlugin = !(isPack || ExtensionsModel::pluginSpecForName(d->currentItemName));
d->currentItemPlugins = current.data(RolePlugins).value<PluginsData>(); d->currentItemPlugins = current.data(RolePlugins).value<PluginsData>();
d->installButton->setVisible(isRemotePlugin && !d->currentItemPlugins.empty());
if (!d->currentItemPlugins.empty()) auto toContentParagraph = [](const QString &text) {
d->installButton->setToolTip(d->currentItemPlugins.constFirst().second); 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 description = htmlStart; const TextData textData = current.data(RoleDescriptionText).value<TextData>();
const bool hasDescription = !textData.isEmpty();
QString descriptionHtml; if (hasDescription) {
{ const QString headerCssTemplate =
const TextData textData = current.data(RoleDescriptionText).value<TextData>(); ";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) { for (const TextData::Type &text : textData) {
if (text.second.isEmpty()) if (text.second.isEmpty())
continue; continue;
const QString paragraph = const QString paragraph =
QString::fromLatin1("<div style=\"%1\">%2</div><p>%3</p>") QString::fromLatin1("<div style=\"%1\">%2</div>%3")
.arg(descriptionHtml.isEmpty() ? h5Css : h6Css) .arg(descriptionHtml.isEmpty() ? h4Css : h5Css)
.arg(text.first) .arg(text.first)
.arg(text.second.join("<br/>")); .arg(toContentParagraph(text.second.join("<br/>")));
descriptionHtml.append(paragraph); 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>(); const LinksData linksData = current.data(RoleDescriptionLinks).value<LinksData>();
if (!linksData.isEmpty()) { const bool hasLinks = !linksData.isEmpty();
if (hasLinks) {
QString linksHtml; QString linksHtml;
const QStringList links = transform(linksData, [](const LinksData::Type &link) { const QStringList links = transform(linksData, [](const LinksData::Type &link) {
const QString anchor = link.first.isEmpty() ? link.second : link.first; const QString anchor = link.first.isEmpty() ? link.second : link.first;
return QString::fromLatin1("<a href=\"%1\">%2 &gt;</a>") return QString::fromLatin1(R"(<a href="%1" style="color:%2">%3 &gt;</a>)")
.arg(link.second).arg(anchor); .arg(link.second)
.arg(creatorColor(Theme::Token_Text_Accent).name())
.arg(anchor);
}); });
linksHtml = links.join("<br/>"); 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>(); const ImagesData imagesData = current.data(RoleDescriptionImages).value<ImagesData>();
if (!imagesData.isEmpty()) { const bool hasImages = !imagesData.isEmpty();
const QString examplesBoxCss = if (hasImages) {
QString::fromLatin1("height: 168px; background-color: %1; ") const ImagesData::Type &image = imagesData.constFirst(); // Only show one image
.arg(creatorColor(Theme::Token_Background_Default).name()); d->imageTitle->setText(image.first);
description.append(QString(R"( fetchAndDisplayImage(image.second);
<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));
} }
d->imageTitle->setVisible(hasImages);
// Library details vanished from the Figma designs. The data is available, though. d->image->setVisible(hasImages);
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);
} }
{ {
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(); const QStringList tags = current.data(RoleTags).toStringList();
if (!tags.isEmpty()) { d->tags->setTags(tags);
const QString tagTemplate = QString(R"( const bool hasTags = !tags.isEmpty();
<td style="border: 1px solid %1; padding: 3px; ">%2</td> d->tagsTitle->setVisible(hasTags);
)").arg(creatorColor(Theme::Token_Stroke_Subtle).name()); d->tags->setVisible(hasTags);
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;")));
}
const QStringList platforms = current.data(RolePlatforms).toStringList(); const QStringList platforms = current.data(RolePlatforms).toStringList();
if (!platforms.isEmpty()) { const bool hasPlatforms = !platforms.isEmpty();
description.append(QString(R"( if (hasPlatforms)
<div style="%1">%2</div> d->platforms->setText(toContentParagraph(platforms.join("<br/>")));
<p>%3</p> d->platformsTitle->setVisible(hasPlatforms);
)").arg(h6Css) d->platforms->setVisible(hasPlatforms);
.arg(Tr::tr("Platforms"))
.arg(platforms.join("<br/>")));
}
const QStringList dependencies = current.data(RoleDependencies).toStringList(); const QStringList dependencies = current.data(RoleDependencies).toStringList();
if (!dependencies.isEmpty()) { const bool hasDependencies = !dependencies.isEmpty();
const QString dependenciesFmt = dependencies.join("<br/>"); if (hasDependencies)
description.append(QString(R"( d->dependencies->setText(toContentParagraph(dependencies.join("<br/>")));
<div style="%1">%2</div> d->dependenciesTitle->setVisible(hasDependencies);
<p>%3</p> d->dependencies->setVisible(hasDependencies);
)").arg(h6Css)
.arg(Tr::tr("Dependencies"))
.arg(dependenciesFmt));
}
if (isPack) { const PluginsData plugins = current.data(RolePlugins).value<PluginsData>();
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 QStringList extensions = transform(plugins, &QPair<QString, QString>::first);
const QString extensionsFmt = extensions.join("<br/>"); d->packExtensions->setText(toContentParagraph(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->packExtensionsTitle->setVisible(hasExtensions);
description.append(htmlEnd); d->packExtensions->setVisible(hasExtensions);
d->secondaryDescription->setText(description);
} }
} }
@@ -469,7 +699,56 @@ void ExtensionManagerWidget::fetchAndInstallPlugin(const QUrl &url)
onGroupDone(onPluginInstallation), 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 } // ExtensionManager::Internal
#include "extensionmanagerwidget.moc"

View File

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

View File

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

View File

@@ -5,6 +5,12 @@
#include <QWidget> #include <QWidget>
QT_FORWARD_DECLARE_CLASS(QLabel)
namespace Core::WelcomePageHelpers {
class TextFormat;
}
namespace ExtensionManager::Internal { namespace ExtensionManager::Internal {
class ExtensionsBrowser final : public QWidget class ExtensionsBrowser final : public QWidget
@@ -15,6 +21,8 @@ public:
ExtensionsBrowser(QWidget *parent = nullptr); ExtensionsBrowser(QWidget *parent = nullptr);
~ExtensionsBrowser(); ~ExtensionsBrowser();
void setFilter(const QString &filter);
void adjustToWidth(const int width); void adjustToWidth(const int width);
QSize sizeHint() const override; QSize sizeHint() const override;
@@ -29,4 +37,7 @@ private:
class ExtensionsBrowserPrivate *d = nullptr; class ExtensionsBrowserPrivate *d = nullptr;
}; };
QLabel *tfLabel(const Core::WelcomePageHelpers::TextFormat &tf, bool singleLine = true);
QGradientStops iconGradientStops(const QModelIndex &index);
} // ExtensionManager::Internal } // 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" style="fill:none;stroke:#000000;stroke-width:2;stroke-linejoin:round"
sodipodi:nodetypes="cccccccccccc" /> sodipodi:nodetypes="cccccccccccc" />
</g> </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 <g
id="src/plugins/extensionmanager/images/extensionsmall"> id="src/plugins/extensionmanager/images/extensionsmall">
<use <use
@@ -3838,6 +3852,25 @@
id="path32048" id="path32048"
sodipodi:nodetypes="ccccc" /> sodipodi:nodetypes="ccccc" />
</g> </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 <g
id="src/plugins/extensionmanager/images/download"> id="src/plugins/extensionmanager/images/download">
<rect <rect

Before

Width:  |  Height:  |  Size: 373 KiB

After

Width:  |  Height:  |  Size: 374 KiB