diff options
author | Alessandro Portale <[email protected]> | 2024-06-06 14:40:58 +0200 |
---|---|---|
committer | Alessandro Portale <[email protected]> | 2024-06-24 11:47:30 +0000 |
commit | 81163b431e68f281bd7c6cdd4e6c8ba279ff9155 (patch) | |
tree | aa8ee7561a710bff3991bd41236171d2a3a33987 /src/plugins | |
parent | a41c4de3df3cd120095da646e656597e10f6e3bf (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.qrc | 4 | ||||
-rw-r--r-- | src/plugins/extensionmanager/extensionmanager_test.qrc | 1 | ||||
-rw-r--r-- | src/plugins/extensionmanager/extensionmanagerwidget.cpp | 649 | ||||
-rw-r--r-- | src/plugins/extensionmanager/extensionmanagerwidget.h | 1 | ||||
-rw-r--r-- | src/plugins/extensionmanager/extensionsbrowser.cpp | 78 | ||||
-rw-r--r-- | src/plugins/extensionmanager/extensionsbrowser.h | 11 | ||||
-rw-r--r-- | src/plugins/extensionmanager/images/extensionbig.png | bin | 0 -> 509 bytes | |||
-rw-r--r-- | src/plugins/extensionmanager/images/[email protected] | bin | 0 -> 1037 bytes | |||
-rw-r--r-- | src/plugins/extensionmanager/images/packbig.png | bin | 0 -> 455 bytes | |||
-rw-r--r-- | src/plugins/extensionmanager/images/[email protected] | bin | 0 -> 785 bytes | |||
-rw-r--r-- | src/plugins/extensionmanager/testdata/varieddata.json | 76 |
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 ¤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<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 ¤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"( - <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 ></a>") - .arg(link.second).arg(anchor); + return QString::fromLatin1(R"(<a href="%1" style="color:%2">%3 ></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(" "))); - } + 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 ¤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 <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 Binary files differnew file mode 100644 index 00000000000..6600ff9fb06 --- /dev/null +++ b/src/plugins/extensionmanager/images/extensionbig.png diff --git a/src/plugins/extensionmanager/images/[email protected] b/src/plugins/extensionmanager/images/[email protected] Binary files differnew file mode 100644 index 00000000000..f44905ebc56 --- /dev/null +++ b/src/plugins/extensionmanager/images/[email protected] diff --git a/src/plugins/extensionmanager/images/packbig.png b/src/plugins/extensionmanager/images/packbig.png Binary files differnew file mode 100644 index 00000000000..b69acad9770 --- /dev/null +++ b/src/plugins/extensionmanager/images/packbig.png diff --git a/src/plugins/extensionmanager/images/[email protected] b/src/plugins/extensionmanager/images/[email protected] Binary files differnew file mode 100644 index 00000000000..7fb684a9c2a --- /dev/null +++ b/src/plugins/extensionmanager/images/[email protected] 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 + } + ] +} |