ExtensionManager: Update ExtensionManager list items design

This updates the list delegate for ExtensionManager items to match the
latest Figma design.

Change-Id: I769026caa1e08feea4f71d901d1bda01d74ab0a2
Reviewed-by: hjk <hjk@qt.io>
This commit is contained in:
Alessandro Portale
2024-05-31 21:31:09 +02:00
parent bbfb7542a7
commit 74e4e1053a
9 changed files with 179 additions and 92 deletions

View File

@@ -1,5 +1,7 @@
<RCC> <RCC>
<qresource prefix="/extensionmanager"> <qresource prefix="/extensionmanager">
<file>images/download.png</file>
<file>images/download@2x.png</file>
<file>images/extensionsmall.png</file> <file>images/extensionsmall.png</file>
<file>images/extensionsmall@2x.png</file> <file>images/extensionsmall@2x.png</file>
<file>images/mode_extensionmanager_mask.png</file> <file>images/mode_extensionmanager_mask.png</file>

View File

@@ -40,27 +40,37 @@
#include <QPainterPath> #include <QPainterPath>
#include <QStyle> #include <QStyle>
using namespace ExtensionSystem;
using namespace Core; using namespace Core;
using namespace ExtensionSystem;
using namespace Utils; using namespace Utils;
using namespace StyleHelper;
using namespace SpacingTokens;
using namespace WelcomePageHelpers;
namespace ExtensionManager::Internal { namespace ExtensionManager::Internal {
Q_LOGGING_CATEGORY(browserLog, "qtc.extensionmanager.browser", QtWarningMsg) Q_LOGGING_CATEGORY(browserLog, "qtc.extensionmanager.browser", QtWarningMsg)
constexpr QSize itemSize = {330, 86}; constexpr int gapSize = ExVPaddingGapXl;
constexpr int gapSize = StyleHelper::SpacingTokens::ExVPaddingGapXl; constexpr int itemWidth = 330;
constexpr QSize cellSize = {itemSize.width() + gapSize, itemSize.height() + gapSize}; constexpr int cellWidth = itemWidth + HPaddingL;
static QColor colorForExtensionName(const QString &name)
{
const size_t hash = qHash(name);
return QColor::fromHsv(hash % 360, 180, 110);
}
class ExtensionItemDelegate : public QItemDelegate class ExtensionItemDelegate : public QItemDelegate
{ {
public: public:
constexpr static QSize dividerS{1, 16};
constexpr static QSize iconBgS{50, 50};
constexpr static TextFormat itemNameTF
{Theme::Token_Text_Default, UiElement::UiElementH6};
constexpr static TextFormat countTF
{Theme::Token_Text_Default, UiElement::UiElementLabelSmall,
Qt::AlignCenter | Qt::TextDontClip};
constexpr static TextFormat vendorTF
{Theme::Token_Text_Muted, UiElement::UiElementLabelSmall,
Qt::AlignVCenter | Qt::TextDontClip};
constexpr static TextFormat tagsTF
{Theme::Token_Text_Default, UiElement::UiElementCaption};
explicit ExtensionItemDelegate(QObject *parent = nullptr) explicit ExtensionItemDelegate(QObject *parent = nullptr)
: QItemDelegate(parent) : QItemDelegate(parent)
{ {
@@ -69,12 +79,54 @@ public:
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index) void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index)
const override const override
{ {
// +---------------+-------+---------------+----------------------------------------------------------------------+---------------+-----------+
// | | | | (ExPaddingGapL) | | |
// | | | +-------------------------------------------------------------+--------+ | |
// | | | | <itemName> |<status>| | |
// | | | +-------------------------------------------------------------+--------+ | |
// | | | | (VGapXxs) | | |
// | | | +--------+--------+--------------+--------+--------+---------+---------+ | |
// |(ExPaddingGapL)|<icon> |(ExPaddingGapL)|<vendor>|(HGapXs)|<divider>(h16)|(HGapXs)|<dlIcon>|(HGapXxs)|<dlCount>|(ExPaddingGapL)|(HPaddingL)|
// | |(50x50)| +--------+--------+--------------+--------+--------+---------+---------+ | |
// | | | | (VGapXxs) | | |
// | | | +----------------------------------------------------------------------+ | |
// | | | | <tags> | | |
// | | | +----------------------------------------------------------------------+ | |
// | | | | (ExPaddingGapL) | | |
// +---------------+-------+---------------+----------------------------------------------------------------------+---------------+-----------+
// | (ExVPaddingGapXl) |
// +------------------------------------------------------------------------------------------------------------------------------------------+
const QRect bgRGlobal = option.rect.adjusted(0, 0, -HPaddingL, -gapSize);
const QRect bgR = bgRGlobal.translated(-option.rect.topLeft());
const int middleColumnW = bgR.width() - ExPaddingGapL - iconBgS.width() - ExPaddingGapL
- ExPaddingGapL;
int x = bgR.x();
int y = bgR.y();
x += ExPaddingGapL;
const QRect iconBgR(x, y + (bgR.height() - iconBgS.height()) / 2,
iconBgS.width(), iconBgS.height());
x += iconBgS.width() + ExPaddingGapL;
y += ExPaddingGapL;
const QRect itemNameR(x, y, middleColumnW, itemNameTF.lineHeight());
const QString itemName = index.data().toString();
y += itemNameR.height() + VGapXxs;
const QRect vendorRowR(x, y, middleColumnW, vendorRowHeight());
QRect vendorR = vendorRowR;
y += vendorRowR.height() + VGapXxs;
const QRect tagsR(x, y, middleColumnW, tagsTF.lineHeight());
QTC_CHECK(option.rect.height() - 1 == tagsR.bottom() + ExPaddingGapL + gapSize);
painter->save(); painter->save();
painter->setRenderHint(QPainter::Antialiasing); painter->setRenderHint(QPainter::Antialiasing);
painter->translate(bgRGlobal.topLeft());
const QString itemName = index.data().toString();
const bool isPack = index.data(RoleItemType) == ItemTypePack; const bool isPack = index.data(RoleItemType) == ItemTypePack;
const QRectF itemRect(option.rect.topLeft(), itemSize);
{ {
const bool selected = option.state & QStyle::State_Selected; const bool selected = option.state & QStyle::State_Selected;
const bool hovered = option.state & QStyle::State_MouseOver; const bool hovered = option.state & QStyle::State_MouseOver;
@@ -85,94 +137,114 @@ public:
creatorColor(selected ? Theme::Token_Stroke_Strong creatorColor(selected ? Theme::Token_Stroke_Strong
: hovered ? WelcomePageHelpers::cardHoverStroke : hovered ? WelcomePageHelpers::cardHoverStroke
: WelcomePageHelpers::cardDefaultStroke); : WelcomePageHelpers::cardDefaultStroke);
WelcomePageHelpers::drawCardBackground(painter, itemRect, fillColor, strokeColor); WelcomePageHelpers::drawCardBackground(painter, bgR, fillColor, strokeColor);
} }
{ {
constexpr QRectF bigCircle(16, 16, 48, 48); QLinearGradient gradient(iconBgR.topRight(), iconBgR.bottomLeft());
constexpr double gradientMargin = 0.14645; const QColor startColor = creatorColor(Utils::Theme::Token_Gradient01_Start);
const QRectF bigCircleLocal = bigCircle.translated(itemRect.topLeft()); const QColor endColor = creatorColor(Utils::Theme::Token_Gradient01_End);
QPainterPath bigCirclePath; gradient.setColorAt(0, startColor);
bigCirclePath.addEllipse(bigCircleLocal); gradient.setColorAt(1, endColor);
QLinearGradient gradient(bigCircleLocal.topLeft(), bigCircleLocal.bottomRight()); constexpr int iconRectRounding = 4;
const QColor startColor = isPack ? qRgb(0x1e, 0x99, 0x6e) drawCardBackground(painter, iconBgR, gradient, Qt::NoPen, iconRectRounding);
: colorForExtensionName(itemName);
const QColor endColor = isPack ? qRgb(0x07, 0x6b, 0x6d) : startColor.lighter(150);
gradient.setColorAt(gradientMargin, startColor);
gradient.setColorAt(1 - gradientMargin, endColor);
painter->fillPath(bigCirclePath, gradient);
static const QIcon packIcon = // Icon
Icon({{":/extensionmanager/images/packsmall.png", constexpr Theme::Color color = Theme::Token_Basic_White;
Theme::Token_Text_Default}}, Icon::Tint).icon(); static const QIcon pack = Icon({{":/extensionmanager/images/packsmall.png", color}},
static const QIcon extensionIcon = Icon::Tint).icon();
Icon({{":/extensionmanager/images/extensionsmall.png", static const QIcon extension = Icon({{":/extensionmanager/images/extensionsmall.png",
Theme::Token_Text_Default}}, Icon::Tint).icon(); color}}, Icon::Tint).icon();
QRectF iconRect(0, 0, 32, 32); (isPack ? pack : extension).paint(painter, iconBgR);
iconRect.moveCenter(bigCircleLocal.center());
(isPack ? packIcon : extensionIcon).paint(painter, iconRect.toRect());
} }
if (isPack) { if (isPack) {
constexpr QRectF smallCircle(47, 50, 18, 18); constexpr int circleSize = 18;
constexpr qreal strokeWidth = 1; constexpr int circleOverlap = 3; // Protrusion from lower right corner of iconRect
constexpr qreal shrink = strokeWidth / 2; const QRect smallCircle(iconBgR.right() + 1 + circleOverlap - circleSize,
constexpr QRectF smallCircleAdjusted = smallCircle.adjusted(shrink, shrink, iconBgR.bottom() + 1 + circleOverlap - circleSize,
-shrink, -shrink); circleSize, circleSize);
const QRectF smallCircleLocal = smallCircleAdjusted.translated(itemRect.topLeft());
const QColor fillColor = creatorColor(Theme::Token_Foreground_Muted); const QColor fillColor = creatorColor(Theme::Token_Foreground_Muted);
const QColor strokeColor = creatorColor(Theme::Token_Stroke_Subtle); const QColor strokeColor = creatorColor(Theme::Token_Stroke_Subtle);
painter->setBrush(fillColor); drawCardBackground(painter, smallCircle, fillColor, strokeColor, circleSize / 2);
painter->setPen(strokeColor);
painter->drawEllipse(smallCircleLocal);
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong)); painter->setFont(countTF.font());
const QColor textColor = creatorColor(Theme::Token_Text_Default); painter->setPen(countTF.color());
painter->setPen(textColor);
const PluginsData plugins = index.data(RolePlugins).value<PluginsData>(); const PluginsData plugins = index.data(RolePlugins).value<PluginsData>();
painter->drawText( painter->drawText(smallCircle, countTF.drawTextFlags, QString::number(plugins.count()));
smallCircleLocal,
QString::number(plugins.count()),
QTextOption(Qt::AlignCenter));
} }
{ {
constexpr int textX = 80; painter->setPen(itemNameTF.color());
constexpr int rightMargin = StyleHelper::SpacingTokens::ExVPaddingGapXl; painter->setFont(itemNameTF.font());
constexpr int maxTextWidth = itemSize.width() - textX - rightMargin;
constexpr Qt::TextElideMode elideMode = Qt::ElideRight;
constexpr int titleY = 30;
const QPointF titleOrigin(itemRect.topLeft() + QPointF(textX, titleY));
painter->setPen(creatorColor(Theme::Token_Text_Default));
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementH6));
const QString titleElided const QString titleElided
= painter->fontMetrics().elidedText(itemName, elideMode, maxTextWidth); = painter->fontMetrics().elidedText(itemName, Qt::ElideRight, itemNameR.width());
painter->drawText(titleOrigin, titleElided); painter->drawText(itemNameR, itemNameTF.drawTextFlags, titleElided);
}
{
const QString vendor = index.data(RoleVendor).toString();
const QFontMetrics fm(vendorTF.font());
painter->setPen(vendorTF.color());
painter->setFont(vendorTF.font());
constexpr int copyrightY = 52; if (const int dlCount = index.data(RoleDownloadCount).toInt(); dlCount > 0) {
const QPointF copyrightOrigin(itemRect.topLeft() + QPointF(textX, copyrightY)); constexpr QSize dlIconS(16, 16);
painter->setPen(creatorColor(Theme::Token_Text_Muted)); const QString dlCountString = QString::number(dlCount);
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong)); const int dlCountW = fm.horizontalAdvance(dlCountString);
const QString copyright = index.data(RoleCopyright).toString(); const int dlItemsW = HGapXs + dividerS.width() + HGapXs + dlIconS.width()
const QString copyrightElided + HGapXxs + dlCountW;
= painter->fontMetrics().elidedText(copyright, elideMode, maxTextWidth); const int vendorW = fm.horizontalAdvance(vendor);
painter->drawText(copyrightOrigin, copyrightElided); vendorR.setWidth(qMin(middleColumnW - dlItemsW, vendorW));
constexpr int tagsY = 70; QRect dividerR = vendorRowR;
const QPointF tagsOrigin(itemRect.topLeft() + QPointF(textX, tagsY)); dividerR.setLeft(vendorR.right() + HGapXs);
const QString tags = index.data(RoleTags).toStringList().join(", "); dividerR.setWidth(dividerS.width());
painter->setPen(creatorColor(Theme::Token_Text_Default)); painter->fillRect(dividerR, vendorTF.color());
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaption));
const QString tagsElided = painter->fontMetrics().elidedText( QRect dlIconR = vendorRowR;
tags, elideMode, maxTextWidth); dlIconR.setLeft(dividerR.right() + HGapXs);
painter->drawText(tagsOrigin, tagsElided); dlIconR.setWidth(dlIconS.width());
static const QIcon dlIcon = Icon({{":/extensionmanager/images/download.png",
vendorTF.themeColor}}, Icon::Tint).icon();
dlIcon.paint(painter, dlIconR);
QRect dlCountR = vendorRowR;
dlCountR.setLeft(dlIconR.right() + HGapXxs);
painter->drawText(dlCountR, vendorTF.drawTextFlags, dlCountString);
}
const QString vendorElided = fm.elidedText(vendor, Qt::ElideRight, vendorR.width());
painter->drawText(vendorR, vendorTF.drawTextFlags, vendorElided);
}
{
const QStringList tagList = index.data(RoleTags).toStringList();
const QString tags = tagList.join(", ");
painter->setPen(tagsTF.color());
painter->setFont(tagsTF.font());
const QString tagsElided
= painter->fontMetrics().elidedText(tags, Qt::ElideRight, tagsR.width());
painter->drawText(tagsR, tagsTF.drawTextFlags, tagsElided);
} }
painter->restore(); painter->restore();
} }
static int vendorRowHeight()
{
return qMax(vendorTF.lineHeight(), dividerS.height());
}
QSize sizeHint([[maybe_unused]] const QStyleOptionViewItem &option, QSize sizeHint([[maybe_unused]] const QStyleOptionViewItem &option,
[[maybe_unused]] const QModelIndex &index) const override [[maybe_unused]] const QModelIndex &index) const override
{ {
return cellSize; const int middleColumnH =
itemNameTF.lineHeight()
+ VGapXxs
+ vendorRowHeight()
+ VGapXxs
+ tagsTF.lineHeight();
const int height =
ExPaddingGapL
+ qMax(iconBgS.height(), middleColumnH)
+ ExPaddingGapL;
return {cellWidth, height + gapSize};
} }
}; };
@@ -196,11 +268,10 @@ ExtensionsBrowser::ExtensionsBrowser(QWidget *parent)
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred); setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
auto manageLabel = new QLabel(Tr::tr("Manage Extensions")); auto manageLabel = new QLabel(Tr::tr("Manage Extensions"));
manageLabel->setFont(StyleHelper::uiFont(StyleHelper::UiElementH1)); manageLabel->setFont(uiFont(UiElementH1));
d->searchBox = new SearchBox; d->searchBox = new SearchBox;
d->searchBox->setFixedWidth(itemSize.width()); d->searchBox->setFixedWidth(itemWidth);
d->updateButton = new Button(Tr::tr("Install..."), Button::MediumPrimary); d->updateButton = new Button(Tr::tr("Install..."), Button::MediumPrimary);
d->model = new ExtensionsModel(this); d->model = new ExtensionsModel(this);
@@ -267,14 +338,14 @@ ExtensionsBrowser::~ExtensionsBrowser()
void ExtensionsBrowser::adjustToWidth(const int width) void ExtensionsBrowser::adjustToWidth(const int width)
{ {
const int widthForItems = width - extraListViewWidth(); const int widthForItems = width - extraListViewWidth();
d->columnsCount = qMax(1, qFloor(widthForItems / cellSize.width())); d->columnsCount = qMax(1, qFloor(widthForItems / cellWidth));
d->updateButton->setVisible(d->columnsCount > 1); d->updateButton->setVisible(d->columnsCount > 1);
updateGeometry(); updateGeometry();
} }
QSize ExtensionsBrowser::sizeHint() const QSize ExtensionsBrowser::sizeHint() const
{ {
const int columsWidth = d->columnsCount * cellSize.width(); const int columsWidth = d->columnsCount * cellWidth;
return { columsWidth + extraListViewWidth(), 0}; return { columsWidth + extraListViewWidth(), 0};
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 137 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 207 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 164 B

After

Width:  |  Height:  |  Size: 314 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 196 B

After

Width:  |  Height:  |  Size: 504 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 271 B

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

Before

Width:  |  Height:  |  Size: 454 B

After

Width:  |  Height:  |  Size: 418 B

View File

@@ -3772,7 +3772,7 @@
height="100%" /> height="100%" />
<path <path
id="path6795" id="path6795"
d="m 16,423.5 v 11 M 26,418 16,423.5 6,418 m 0,11.5 10,5.5 10,-5.5 v -11 L 16,413 6,418.5 Z" d="m 16,422.5 v 12 M 27,418 16,422.5 5,418 m 0,12.5 11,4.5 11,-4.5 v -13 L 16,413 5,417.5 Z"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linejoin:round" style="fill:none;stroke:#000000;stroke-width:2;stroke-linejoin:round"
sodipodi:nodetypes="cccccccccccc" /> sodipodi:nodetypes="cccccccccccc" />
</g> </g>
@@ -3787,15 +3787,29 @@
width="100%" width="100%"
height="100%" /> height="100%" />
<path <path
id="path4530" id="path6795-3"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linejoin:round" style="display:inline;fill:none;stroke:#000000;stroke-width:2;stroke-linejoin:round;stroke-linecap:round"
d="M 49.5,422 H 56 Z M 38,424 h 5.5 z m 11.5,2 H 56 Z" d="m 48,422.5 v 4 m 0,4 v 4 M 59,418 c 0,0 -1.6471,0.674 -3.6667,1.5 M 51.6667,421 C 49.6472,421.826 48,422.5 48,422.5 c 0,0 -1.6471,-0.674 -3.6667,-1.5 m -3.6666,-1.5 C 38.6472,418.674 37,418 37,418 m 7.3333,15.5 C 46.3528,434.326 48,435 48,435 m 11,-17.5 c 0,0 -1.6471,-0.674 -3.6667,-1.5 m -3.6666,-1.5 C 49.6472,413.674 48,413 48,413 c 0,0 -1.6471,0.674 -3.6667,1.5 M 40.6667,416 C 38.6472,416.826 37,417.5 37,417.5 v 4.333 m 0,4.334 v 4.333 c 0,0 1.6471,0.674 3.6667,1.5" />
sodipodi:nodetypes="ccccccccc" />
<path <path
style="fill:#000000;fill-opacity:1" style="fill:#000000;stroke:#000000;stroke-width:2;stroke-linejoin:round"
d="m 50,420 h -3.5 c -1.5,0 -4,0.5 -4,4 0,3.5 2.5,4 4,4 H 50 c 1.5,0 2,-1 2,-1.5 v -5 C 52,421 51.5,420 50,420 Z" d="M 59,421.75 V 430.5 l -8,3.25 V 425 Z"
id="path4682" id="path32048"
sodipodi:nodetypes="cczccccc" /> sodipodi:nodetypes="ccccc" />
</g>
<g
id="src/plugins/extensionmanager/images/download">
<rect
y="408"
x="65"
height="16"
width="16"
id="use15-0-0"
style="display:inline;fill:#ffffff" />
<path
id="path7"
style="fill:none;stroke:#000000;stroke-width:2;stroke-linecap:round"
d="m 67,422 h 12 m -6,-3.5 V 410 m -3,5.5 3,3 3.03461,-3"
sodipodi:nodetypes="ccccccc" />
</g> </g>
<g <g
id="src/libs/utils/images/eyeoverlay"> id="src/libs/utils/images/eyeoverlay">

Before

Width:  |  Height:  |  Size: 374 KiB

After

Width:  |  Height:  |  Size: 375 KiB