aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins
diff options
context:
space:
mode:
authorAlessandro Portale <[email protected]>2024-06-06 14:40:58 +0200
committerAlessandro Portale <[email protected]>2024-06-24 11:47:30 +0000
commit81163b431e68f281bd7c6cdd4e6c8ba279ff9155 (patch)
treeaa8ee7561a710bff3991bd41236171d2a3a33987 /src/plugins
parenta41c4de3df3cd120095da646e656597e10f6e3bf (diff)
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 <[email protected]>
Diffstat (limited to 'src/plugins')
-rw-r--r--src/plugins/extensionmanager/extensionmanager.qrc4
-rw-r--r--src/plugins/extensionmanager/extensionmanager_test.qrc1
-rw-r--r--src/plugins/extensionmanager/extensionmanagerwidget.cpp649
-rw-r--r--src/plugins/extensionmanager/extensionmanagerwidget.h1
-rw-r--r--src/plugins/extensionmanager/extensionsbrowser.cpp78
-rw-r--r--src/plugins/extensionmanager/extensionsbrowser.h11
-rw-r--r--src/plugins/extensionmanager/images/extensionbig.pngbin0 -> 509 bytes
-rw-r--r--src/plugins/extensionmanager/images/[email protected]bin0 -> 1037 bytes
-rw-r--r--src/plugins/extensionmanager/images/packbig.pngbin0 -> 455 bytes
-rw-r--r--src/plugins/extensionmanager/images/[email protected]bin0 -> 785 bytes
-rw-r--r--src/plugins/extensionmanager/testdata/varieddata.json76
11 files changed, 615 insertions, 205 deletions
diff --git a/src/plugins/extensionmanager/extensionmanager.qrc b/src/plugins/extensionmanager/extensionmanager.qrc
index b6a3554cb1f..b552aaf7b59 100644
--- a/src/plugins/extensionmanager/extensionmanager.qrc
+++ b/src/plugins/extensionmanager/extensionmanager.qrc
@@ -2,10 +2,14 @@
<qresource prefix="/extensionmanager">
<file>images/download.png</file>
<file>images/[email protected]</file>
+ <file>images/extensionbig.png</file>
+ <file>images/[email protected]</file>
<file>images/extensionsmall.png</file>
<file>images/[email protected]</file>
<file>images/mode_extensionmanager_mask.png</file>
<file>images/[email protected]</file>
+ <file>images/packbig.png</file>
+ <file>images/[email protected]</file>
<file>images/packsmall.png</file>
<file>images/[email protected]</file>
</qresource>
diff --git a/src/plugins/extensionmanager/extensionmanager_test.qrc b/src/plugins/extensionmanager/extensionmanager_test.qrc
index 4c4d59f002d..f8a11b4e084 100644
--- a/src/plugins/extensionmanager/extensionmanager_test.qrc
+++ b/src/plugins/extensionmanager/extensionmanager_test.qrc
@@ -2,5 +2,6 @@
<qresource prefix="/extensionmanager">
<file>testdata/defaultpacks.json</file>
<file>testdata/thirdpartyplugins.json</file>
+ <file>testdata/varieddata.json</file>
</qresource>
</RCC>
diff --git a/src/plugins/extensionmanager/extensionmanagerwidget.cpp b/src/plugins/extensionmanager/extensionmanagerwidget.cpp
index 363b8cb90ee..dea2f9e806f 100644
--- a/src/plugins/extensionmanager/extensionmanagerwidget.cpp
+++ b/src/plugins/extensionmanager/extensionmanagerwidget.cpp
@@ -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->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;
- d->installButton = new Button(Tr::tr("Install..."), Button::MediumPrimary);
- d->installButton->hide();
+ 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);
- using namespace Layouting;
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>
- )");
-
- if (!current.isValid()) {
- const QString emptyHtml = htmlStart + htmlEnd;
- d->primaryDescription->setText(emptyHtml);
- d->secondaryDescription->setText(emptyHtml);
+ d->headingWidget->update(current);
+
+ 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 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));
- }
-
- // 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>
- )"));
+ 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);
}
-
- 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));
- }
-
- if (isPack) {
- const PluginsData plugins = current.data(RolePlugins).value<PluginsData>();
+ const bool hasDependencies = !dependencies.isEmpty();
+ if (hasDependencies)
+ d->dependencies->setText(toContentParagraph(dependencies.join("<br/>")));
+ d->dependenciesTitle->setVisible(hasDependencies);
+ d->dependencies->setVisible(hasDependencies);
+
+ 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"
diff --git a/src/plugins/extensionmanager/extensionmanagerwidget.h b/src/plugins/extensionmanager/extensionmanagerwidget.h
index aeaad3db07c..dbc02daeca8 100644
--- a/src/plugins/extensionmanager/extensionmanagerwidget.h
+++ b/src/plugins/extensionmanager/extensionmanagerwidget.h
@@ -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;
};
diff --git a/src/plugins/extensionmanager/extensionsbrowser.cpp b/src/plugins/extensionmanager/extensionsbrowser.cpp
index ccf814cd7c8..330c2395920 100644
--- a/src/plugins/extensionmanager/extensionsbrowser.cpp
+++ b/src/plugins/extensionmanager/extensionsbrowser.cpp
@@ -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
diff --git a/src/plugins/extensionmanager/extensionsbrowser.h b/src/plugins/extensionmanager/extensionsbrowser.h
index d0467aa2162..b49bcfaba12 100644
--- a/src/plugins/extensionmanager/extensionsbrowser.h
+++ b/src/plugins/extensionmanager/extensionsbrowser.h
@@ -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
diff --git a/src/plugins/extensionmanager/images/extensionbig.png b/src/plugins/extensionmanager/images/extensionbig.png
new file mode 100644
index 00000000000..6600ff9fb06
--- /dev/null
+++ b/src/plugins/extensionmanager/images/extensionbig.png
Binary files differ
diff --git a/src/plugins/extensionmanager/images/[email protected] b/src/plugins/extensionmanager/images/[email protected]
new file mode 100644
index 00000000000..f44905ebc56
--- /dev/null
+++ b/src/plugins/extensionmanager/images/[email protected]
Binary files differ
diff --git a/src/plugins/extensionmanager/images/packbig.png b/src/plugins/extensionmanager/images/packbig.png
new file mode 100644
index 00000000000..b69acad9770
--- /dev/null
+++ b/src/plugins/extensionmanager/images/packbig.png
Binary files differ
diff --git a/src/plugins/extensionmanager/images/[email protected] b/src/plugins/extensionmanager/images/[email protected]
new file mode 100644
index 00000000000..7fb684a9c2a
--- /dev/null
+++ b/src/plugins/extensionmanager/images/[email protected]
Binary files differ
diff --git a/src/plugins/extensionmanager/testdata/varieddata.json b/src/plugins/extensionmanager/testdata/varieddata.json
new file mode 100644
index 00000000000..245d08ad844
--- /dev/null
+++ b/src/plugins/extensionmanager/testdata/varieddata.json
@@ -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
+ }
+ ]
+}