From 81163b431e68f281bd7c6cdd4e6c8ba279ff9155 Mon Sep 17 00:00:00 2001 From: Alessandro Portale Date: Thu, 6 Jun 2024 14:40:58 +0200 Subject: [PATCH] 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 --- .../extensionmanager/extensionmanager.qrc | 4 + .../extensionmanager_test.qrc | 1 + .../extensionmanagerwidget.cpp | 649 +++++++++++++----- .../extensionmanager/extensionmanagerwidget.h | 1 + .../extensionmanager/extensionsbrowser.cpp | 78 ++- .../extensionmanager/extensionsbrowser.h | 11 + .../extensionmanager/images/extensionbig.png | Bin 0 -> 509 bytes .../images/extensionbig@2x.png | Bin 0 -> 1037 bytes .../extensionmanager/images/packbig.png | Bin 0 -> 455 bytes .../extensionmanager/images/packbig@2x.png | Bin 0 -> 785 bytes .../extensionmanager/testdata/varieddata.json | 76 ++ src/tools/icons/qtcreatoricons.svg | 33 + 12 files changed, 648 insertions(+), 205 deletions(-) create mode 100644 src/plugins/extensionmanager/images/extensionbig.png create mode 100644 src/plugins/extensionmanager/images/extensionbig@2x.png create mode 100644 src/plugins/extensionmanager/images/packbig.png create mode 100644 src/plugins/extensionmanager/images/packbig@2x.png create mode 100644 src/plugins/extensionmanager/testdata/varieddata.json 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 @@ images/download.png images/download@2x.png + images/extensionbig.png + images/extensionbig@2x.png images/extensionsmall.png images/extensionsmall@2x.png images/mode_extensionmanager_mask.png images/mode_extensionmanager_mask@2x.png + images/packbig.png + images/packbig@2x.png images/packsmall.png images/packsmall@2x.png 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 @@ testdata/defaultpacks.json testdata/thirdpartyplugins.json + testdata/varieddata.json 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 #include +#include +#include #include +#include +#include #include -#include +#include +#include #include +#include +#include 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 ¤t) + { + 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(); + 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(); + 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 == 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 ¤t) { - 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"( - -
- )").arg(bodyStyle); - const QString htmlEnd = QString(R"( - - )"); + 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(); - d->installButton->setVisible(isRemotePlugin && !d->currentItemPlugins.empty()); - if (!d->currentItemPlugins.empty()) - d->installButton->setToolTip(d->currentItemPlugins.constFirst().second); + + auto toContentParagraph = [](const QString &text) { + const QString pHtml = QString::fromLatin1("

%2

") + .arg(contentTF.lineHeight()).arg(text); + return pHtml; + }; { - QString description = htmlStart; - - QString descriptionHtml; - { - const TextData textData = current.data(RoleDescriptionText).value(); + const TextData textData = current.data(RoleDescriptionText).value(); + 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("
%2

%3

") - .arg(descriptionHtml.isEmpty() ? h5Css : h6Css) + QString::fromLatin1("
%2
%3") + .arg(descriptionHtml.isEmpty() ? h4Css : h5Css) .arg(text.first) - .arg(text.second.join("
")); + .arg(toContentParagraph(text.second.join("
"))); descriptionHtml.append(paragraph); } + descriptionHtml.prepend(QString::fromLatin1("") + .arg(creatorColor(Theme::Token_Text_Default).name())); + descriptionHtml.append(""); + d->description->setText(descriptionHtml); } - description.append(descriptionHtml); + d->description->setVisible(hasDescription); - description.append(QString::fromLatin1("
%2
") - .arg(h6Css) - .arg(Tr::tr("More information"))); const LinksData linksData = current.data(RoleDescriptionLinks).value(); - 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("%2 >") - .arg(link.second).arg(anchor); + return QString::fromLatin1(R"(%3 >)") + .arg(link.second) + .arg(creatorColor(Theme::Token_Text_Accent).name()) + .arg(anchor); }); linksHtml = links.join("
"); - description.append(QString::fromLatin1("

%1

").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(); - if (!imagesData.isEmpty()) { - const QString examplesBoxCss = - QString::fromLatin1("height: 168px; background-color: %1; ") - .arg(creatorColor(Theme::Token_Background_Default).name()); - description.append(QString(R"( -
-
%2
-

-




- TODO: Load imagea asynchronously, and show them in a QLabel. - Also Use QMovie for animated images. -




-

- )").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"( -
%2
-

- - - - )").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"( - - )").arg(Tr::tr("Location")) - .arg(locationFmt)); - } - description.append(QString(R"( -
%4%5
%6%7
%1%2
-

- )")); - } - - description.append(htmlEnd); - d->primaryDescription->setText(description); + d->imageTitle->setVisible(hasImages); + d->image->setVisible(hasImages); } { - QString description = htmlStart; - - description.append(QString(R"( -

%2

- )").arg(h6CapitalCss) - .arg(Tr::tr("Extension details"))); - const QStringList tags = current.data(RoleTags).toStringList(); - if (!tags.isEmpty()) { - const QString tagTemplate = QString(R"( - %2 - )").arg(creatorColor(Theme::Token_Stroke_Subtle).name()); - const QStringList tagsFmt = transform(tags, [&tagTemplate](const QString &tag) { - return tagTemplate.arg(tag); - }); - description.append(QString(R"( -
%2
-

%3

- )").arg(h6Css) - .arg(Tr::tr("Related tags")) - .arg(tagsFmt.join(" "))); - } + 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"( -
%2
-

%3

- )").arg(h6Css) - .arg(Tr::tr("Platforms")) - .arg(platforms.join("
"))); - } + const bool hasPlatforms = !platforms.isEmpty(); + if (hasPlatforms) + d->platforms->setText(toContentParagraph(platforms.join("
"))); + d->platformsTitle->setVisible(hasPlatforms); + d->platforms->setVisible(hasPlatforms); const QStringList dependencies = current.data(RoleDependencies).toStringList(); - if (!dependencies.isEmpty()) { - const QString dependenciesFmt = dependencies.join("
"); - description.append(QString(R"( -
%2
-

%3

- )").arg(h6Css) - .arg(Tr::tr("Dependencies")) - .arg(dependenciesFmt)); - } + const bool hasDependencies = !dependencies.isEmpty(); + if (hasDependencies) + d->dependencies->setText(toContentParagraph(dependencies.join("
"))); + d->dependenciesTitle->setVisible(hasDependencies); + d->dependencies->setVisible(hasDependencies); - if (isPack) { - const PluginsData plugins = current.data(RolePlugins).value(); + const PluginsData plugins = current.data(RolePlugins).value(); + const bool hasExtensions = isPack && !plugins.isEmpty(); + if (hasExtensions) { const QStringList extensions = transform(plugins, &QPair::first); - const QString extensionsFmt = extensions.join("
"); - description.append(QString(R"( -
%2
-

%3

- )").arg(h6Css) - .arg(Tr::tr("Extensions in pack")) - .arg(extensionsFmt)); + d->packExtensions->setText(toContentParagraph(extensions.join("
"))); } - - 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 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 ¤t); 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 #include +#include #include #include #include @@ -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 +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 0000000000000000000000000000000000000000..6600ff9fb0627ed9f2ed72a4607b4d51be7be5d5 GIT binary patch literal 509 zcmeAS@N?(olHy`uVBq!ia0y~yU{GXWU{K;0WlT6NfSC|TUc0b3AM7+(&%b#4qm~#%kAZ) z3C&6^mF?;0_m`jle6L!!R{kIVvewss*ZzIIFmr~*T;3JjY5u+mm)#~tALX5GP*%FI z+2Qt&=O4`y0`%AQBt9wFvp4nT+Xd+hSA~gJHRVo^zIB|xH(|%M>CA0sq?n3ovlm7z z)4O4ut-oNM&a;MdS1;7JuAb}QxI}Np>Y1UlxjR^t%$J>+r8KGjpD&Br?Zh($na8EA zy%#25u=^S8df?QbGdnL|*c-8V+YR2!^Uj|MU*mk>hQ`Eo=CLkrYj4*CRj5zXkhGT8 zD_U$(v+Qa=f5V;~)|;{qpDf|j@SXqS;}zSaq+_ZDE327VrH<%0xO;T$R^F|~kgKRT zO=Bj*9yM1MpC>1!Cn+(hsYR*(aqi-nQR>>oF(Xvjm8CD|R@KI*s+viBQS)jyE^Ovo ztx}XUbC)~&F$QgwQ%O56c|Tcdcz`?a>$;Ytl==k+)ce`)%W?de9g&!DpW~R{{@2$47l{Cg`V z{cv5Yz-wFLI;D%lk;U7xI_An53%qf>9k!<9_WS(FjVDUBt#r`gcqOTD!`REZ z=^UHO>1C$MVoIBJl$ED>WGh-d%D8*{?~X(78*rdpspqWun?XEMh-r!)a=x`%_fd?$7!fVbPzu^DZ3q zRI74a&*Eshto43**j@4T!;ddXt>kEu)ITy)zYiM! zmxO+ZS;df2vg7DMJ%xyG&q74lEt?DKZJI3?{jl6S@fB;^1UF@aEqQs{q!>aYF|Wfyhjqc1fHH}N>aJmSvSFvM{;K}L8@IJ7&pa^wnpDuw#Ff0+ zD_;M4?S1mFtMyI(ABpTMD!;v)nA5mOjr~Vn?$+hP*DTnM{M@bbNKumck?qeJy_@DJ zxwJ_&?O7@0BCtZyW1Xt=omEO4e0uIIA%dR_loj$GxuqyBOke-l?Z(6loB!<){HM2g zWA|lywLF(6GQKi}Q!O4D6|%_MH$}#@v|npwJ90y`^MGn#jM0&K*$xcB@At_hI-eBy z?JugR;)?D{>`J>9n zCEGK>fqnm8u_ISKT`Hb7gr8er(aY~$^GWaet%5T@Tsx9a);KSTI2?1zdWy8wiT0hR zA6_ti%2#RF;vOT%E2BNdbP0l+XkK1v}64 literal 0 HcmV?d00001 diff --git a/src/plugins/extensionmanager/images/packbig.png b/src/plugins/extensionmanager/images/packbig.png new file mode 100644 index 0000000000000000000000000000000000000000..b69acad9770c3ebb493d5d6df478bb03f4c2533e GIT binary patch literal 455 zcmeAS@N?(olHy`uVBq!ia0y~yU{GXWU{K;7j&aFaNyv z(3*FpyTP?Ly^i%$-I^lC&p$rDa|-!$_tnvsJfkaaYWfRSOa5bTG}S2I@A&cH?7Y+s z6#@aQCm1@<)*Bvr_&$wuf}w+`0KealmSqnvvvgcf)M|LAwX0vHet(C&7GGc>>o>0A z6M`%LOcY3)(BL5&bM(JFcj*DC9+3nay}~D!Ii+3)N9k~dUT~nmjOgmMCS7Px7M3mHQnUeEB`>t?u%B# zQ5o}ptxwvxGv_GS`QIwt(6#$1lY(CK3(h4nU-$U&%=yFI;KE>0+;P>`{q)rP&tDiR zG`yShKBDN`{=0DxQjQ&XvGuq7x=Rns`&Q(#HS* literal 0 HcmV?d00001 diff --git a/src/plugins/extensionmanager/images/packbig@2x.png b/src/plugins/extensionmanager/images/packbig@2x.png new file mode 100644 index 0000000000000000000000000000000000000000..7fb684a9c2a2b9a68da337cd506cc9920cca8f54 GIT binary patch literal 785 zcmeAS@N?(olHy`uVBq!ia0y~yU~pn!U~u7JU|?VnNZX^yz`%6F)5S3);_%xUz8=z! zBFEJOcW)C_-FiXn)}E)J1HpFVFcOmy~sqR6t&$)r8O`NSGl-?!6SzsYR0 zuVl8;SSKKFcJ6tp=tAA^Mv68<>l>8Sw(LC}#VPpG;k@OZiBt8Ty*ai)w5r^tt3&a^ z6+2;5uQM-vtR)OWTr{lS$7B?|)o^urx?DZ8tlOhXtJ$FGMp|+j!^TMIiHaPVGPg~c z-t?Is5|mNOuYI+$=FiGo$9HrKwA}gpSAbcl(S44wL&?E!(^hiuclc~|n6Jm@xIJ)E zaKzj*HPKZn(i6&8Kiz`yYI-0*nrwfFyiPkbF4 zui1K}qef`W;w52s-rH3LHkGZPGi7p2(}~-C-1{oJS@oo^^WDgL!r`_5XI`h-{jm4x zTOPht)cAW?o|*5ze%kG;?oXOGnQ<^SXW!9F>Ns)x_Q}^v_hqj$U!;73rRh?|N_{;$ z4qru|uU7?2U1UG*R}>4{su;xmXXBk`y))g9Xgi5;FJ1Q0wzy;OM>U-(3u2@r&Ppg6 zt!ADRb9m83yZINKRi3?Ny6Ca>s9#BjulxxC_RDK8Y%UI5&Gq7~U`0ukyOYw9ebOt} z{3~V+nyJ`y^k{d2l(OH3Q$cs$vmQNC_<(VmV0&oG3jc^pAB?B&>-fKL&4yc2t&>hQ z*>znr*p(V_&oD4ug{>mG%Jtd%2f~I2-u=(qb>*mbz@{Qz?z{=?ll~Y6ehc3mIOpgx xJ^3wD(^(HZcK9UpU3}-+&d#%UrruvPhjBTZs + + + + + + + + +