2023-11-13 16:35:01 +01:00
|
|
|
// Copyright (C) 2023 The Qt Company Ltd.
|
|
|
|
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
|
|
|
|
|
|
|
|
|
#include "extensionsbrowser.h"
|
|
|
|
|
|
2024-01-08 09:52:22 +01:00
|
|
|
#include "extensionmanagertr.h"
|
2024-05-29 16:00:22 +02:00
|
|
|
#include "extensionsmodel.h"
|
|
|
|
|
#include "utils/hostosinfo.h"
|
|
|
|
|
|
|
|
|
|
#ifdef WITH_TESTS
|
|
|
|
|
#include "extensionmanager_test.h"
|
|
|
|
|
#endif // WITH_TESTS
|
2024-01-08 09:52:22 +01:00
|
|
|
|
2023-11-13 16:35:01 +01:00
|
|
|
#include <coreplugin/coreconstants.h>
|
|
|
|
|
#include <coreplugin/icontext.h>
|
|
|
|
|
#include <coreplugin/icore.h>
|
2024-05-29 16:00:22 +02:00
|
|
|
#include <coreplugin/plugininstallwizard.h>
|
2023-11-13 16:35:01 +01:00
|
|
|
#include <coreplugin/welcomepagehelper.h>
|
|
|
|
|
|
|
|
|
|
#include <extensionsystem/iplugin.h>
|
|
|
|
|
#include <extensionsystem/pluginspec.h>
|
|
|
|
|
#include <extensionsystem/pluginview.h>
|
|
|
|
|
#include <extensionsystem/pluginmanager.h>
|
|
|
|
|
|
2024-05-29 16:00:22 +02:00
|
|
|
#include <solutions/tasking/networkquery.h>
|
|
|
|
|
#include <solutions/tasking/tasktree.h>
|
|
|
|
|
#include <solutions/tasking/tasktreerunner.h>
|
|
|
|
|
|
2023-11-13 16:35:01 +01:00
|
|
|
#include <utils/fancylineedit.h>
|
|
|
|
|
#include <utils/icon.h>
|
|
|
|
|
#include <utils/layoutbuilder.h>
|
2024-05-29 16:00:22 +02:00
|
|
|
#include <utils/networkaccessmanager.h>
|
2023-11-13 16:35:01 +01:00
|
|
|
#include <utils/stylehelper.h>
|
|
|
|
|
|
|
|
|
|
#include <QItemDelegate>
|
|
|
|
|
#include <QLabel>
|
|
|
|
|
#include <QListView>
|
|
|
|
|
#include <QMessageBox>
|
|
|
|
|
#include <QPainter>
|
|
|
|
|
#include <QPainterPath>
|
|
|
|
|
#include <QStyle>
|
|
|
|
|
|
|
|
|
|
using namespace ExtensionSystem;
|
|
|
|
|
using namespace Core;
|
|
|
|
|
using namespace Utils;
|
|
|
|
|
|
|
|
|
|
namespace ExtensionManager::Internal {
|
|
|
|
|
|
|
|
|
|
Q_LOGGING_CATEGORY(browserLog, "qtc.extensionmanager.browser", QtWarningMsg)
|
|
|
|
|
|
|
|
|
|
constexpr QSize itemSize = {330, 86};
|
2024-02-12 17:19:41 +01:00
|
|
|
constexpr int gapSize = StyleHelper::SpacingTokens::ExVPaddingGapXl;
|
2023-11-13 16:35:01 +01:00
|
|
|
constexpr QSize cellSize = {itemSize.width() + gapSize, itemSize.height() + gapSize};
|
|
|
|
|
|
|
|
|
|
static QColor colorForExtensionName(const QString &name)
|
|
|
|
|
{
|
|
|
|
|
const size_t hash = qHash(name);
|
|
|
|
|
return QColor::fromHsv(hash % 360, 180, 110);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
class ExtensionItemDelegate : public QItemDelegate
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
explicit ExtensionItemDelegate(QObject *parent = nullptr)
|
|
|
|
|
: QItemDelegate(parent)
|
|
|
|
|
{
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void paint(QPainter *painter, const QStyleOptionViewItem &option, const QModelIndex &index)
|
|
|
|
|
const override
|
|
|
|
|
{
|
|
|
|
|
painter->save();
|
|
|
|
|
painter->setRenderHint(QPainter::Antialiasing);
|
|
|
|
|
|
2024-05-29 16:00:22 +02:00
|
|
|
const QString itemName = index.data().toString();
|
|
|
|
|
const bool isPack = index.data(RoleItemType) == ItemTypePack;
|
2023-11-13 16:35:01 +01:00
|
|
|
const QRectF itemRect(option.rect.topLeft(), itemSize);
|
|
|
|
|
{
|
|
|
|
|
const bool selected = option.state & QStyle::State_Selected;
|
|
|
|
|
const bool hovered = option.state & QStyle::State_MouseOver;
|
2024-02-29 16:02:33 +01:00
|
|
|
const QColor fillColor =
|
2024-05-29 11:45:22 +02:00
|
|
|
creatorColor(hovered ? WelcomePageHelpers::cardHoverBackground
|
2024-02-29 16:02:33 +01:00
|
|
|
: WelcomePageHelpers::cardDefaultBackground);
|
|
|
|
|
const QColor strokeColor =
|
2024-05-29 11:45:22 +02:00
|
|
|
creatorColor(selected ? Theme::Token_Stroke_Strong
|
2024-02-29 16:02:33 +01:00
|
|
|
: hovered ? WelcomePageHelpers::cardHoverStroke
|
|
|
|
|
: WelcomePageHelpers::cardDefaultStroke);
|
2024-01-26 13:28:36 +01:00
|
|
|
WelcomePageHelpers::drawCardBackground(painter, itemRect, fillColor, strokeColor);
|
2023-11-13 16:35:01 +01:00
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
constexpr QRectF bigCircle(16, 16, 48, 48);
|
|
|
|
|
constexpr double gradientMargin = 0.14645;
|
|
|
|
|
const QRectF bigCircleLocal = bigCircle.translated(itemRect.topLeft());
|
|
|
|
|
QPainterPath bigCirclePath;
|
|
|
|
|
bigCirclePath.addEllipse(bigCircleLocal);
|
|
|
|
|
QLinearGradient gradient(bigCircleLocal.topLeft(), bigCircleLocal.bottomRight());
|
|
|
|
|
const QColor startColor = isPack ? qRgb(0x1e, 0x99, 0x6e)
|
2024-05-29 16:00:22 +02:00
|
|
|
: colorForExtensionName(itemName);
|
2023-11-13 16:35:01 +01:00
|
|
|
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({{":/extensionmanager/images/packsmall.png",
|
|
|
|
|
Theme::Token_Text_Default}}, Icon::Tint).icon();
|
|
|
|
|
static const QIcon extensionIcon =
|
|
|
|
|
Icon({{":/extensionmanager/images/extensionsmall.png",
|
|
|
|
|
Theme::Token_Text_Default}}, Icon::Tint).icon();
|
|
|
|
|
QRectF iconRect(0, 0, 32, 32);
|
|
|
|
|
iconRect.moveCenter(bigCircleLocal.center());
|
|
|
|
|
(isPack ? packIcon : extensionIcon).paint(painter, iconRect.toRect());
|
|
|
|
|
}
|
|
|
|
|
if (isPack) {
|
|
|
|
|
constexpr QRectF smallCircle(47, 50, 18, 18);
|
|
|
|
|
constexpr qreal strokeWidth = 1;
|
|
|
|
|
constexpr qreal shrink = strokeWidth / 2;
|
|
|
|
|
constexpr QRectF smallCircleAdjusted = smallCircle.adjusted(shrink, shrink,
|
|
|
|
|
-shrink, -shrink);
|
|
|
|
|
const QRectF smallCircleLocal = smallCircleAdjusted.translated(itemRect.topLeft());
|
2024-05-29 11:45:22 +02:00
|
|
|
const QColor fillColor = creatorColor(Theme::Token_Foreground_Muted);
|
|
|
|
|
const QColor strokeColor = creatorColor(Theme::Token_Stroke_Subtle);
|
2023-11-13 16:35:01 +01:00
|
|
|
painter->setBrush(fillColor);
|
|
|
|
|
painter->setPen(strokeColor);
|
|
|
|
|
painter->drawEllipse(smallCircleLocal);
|
|
|
|
|
|
|
|
|
|
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong));
|
2024-05-29 11:45:22 +02:00
|
|
|
const QColor textColor = creatorColor(Theme::Token_Text_Default);
|
2023-11-13 16:35:01 +01:00
|
|
|
painter->setPen(textColor);
|
2024-05-29 16:00:22 +02:00
|
|
|
const PluginsData plugins = index.data(RolePlugins).value<PluginsData>();
|
|
|
|
|
painter->drawText(
|
|
|
|
|
smallCircleLocal,
|
|
|
|
|
QString::number(plugins.count()),
|
|
|
|
|
QTextOption(Qt::AlignCenter));
|
2023-11-13 16:35:01 +01:00
|
|
|
}
|
|
|
|
|
{
|
|
|
|
|
constexpr int textX = 80;
|
2024-02-12 17:19:41 +01:00
|
|
|
constexpr int rightMargin = StyleHelper::SpacingTokens::ExVPaddingGapXl;
|
2023-11-13 16:35:01 +01:00
|
|
|
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));
|
2024-05-29 11:45:22 +02:00
|
|
|
painter->setPen(creatorColor(Theme::Token_Text_Default));
|
2023-11-13 16:35:01 +01:00
|
|
|
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementH6));
|
2024-05-29 16:00:22 +02:00
|
|
|
const QString titleElided
|
|
|
|
|
= painter->fontMetrics().elidedText(itemName, elideMode, maxTextWidth);
|
2023-11-13 16:35:01 +01:00
|
|
|
painter->drawText(titleOrigin, titleElided);
|
|
|
|
|
|
|
|
|
|
constexpr int copyrightY = 52;
|
|
|
|
|
const QPointF copyrightOrigin(itemRect.topLeft() + QPointF(textX, copyrightY));
|
2024-05-29 11:45:22 +02:00
|
|
|
painter->setPen(creatorColor(Theme::Token_Text_Muted));
|
2023-11-13 16:35:01 +01:00
|
|
|
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaptionStrong));
|
2024-05-29 16:00:22 +02:00
|
|
|
const QString copyright = index.data(RoleCopyright).toString();
|
|
|
|
|
const QString copyrightElided
|
|
|
|
|
= painter->fontMetrics().elidedText(copyright, elideMode, maxTextWidth);
|
2023-11-13 16:35:01 +01:00
|
|
|
painter->drawText(copyrightOrigin, copyrightElided);
|
|
|
|
|
|
|
|
|
|
constexpr int tagsY = 70;
|
|
|
|
|
const QPointF tagsOrigin(itemRect.topLeft() + QPointF(textX, tagsY));
|
2024-05-29 16:00:22 +02:00
|
|
|
const QString tags = index.data(RoleTags).toStringList().join(", ");
|
2024-05-29 11:45:22 +02:00
|
|
|
painter->setPen(creatorColor(Theme::Token_Text_Default));
|
2023-11-13 16:35:01 +01:00
|
|
|
painter->setFont(StyleHelper::uiFont(StyleHelper::UiElementCaption));
|
|
|
|
|
const QString tagsElided = painter->fontMetrics().elidedText(
|
|
|
|
|
tags, elideMode, maxTextWidth);
|
|
|
|
|
painter->drawText(tagsOrigin, tagsElided);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
painter->restore();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QSize sizeHint([[maybe_unused]] const QStyleOptionViewItem &option,
|
|
|
|
|
[[maybe_unused]] const QModelIndex &index) const override
|
|
|
|
|
{
|
|
|
|
|
return cellSize;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-05-29 16:00:22 +02:00
|
|
|
class ExtensionsBrowserPrivate
|
|
|
|
|
{
|
|
|
|
|
public:
|
|
|
|
|
ExtensionsModel *model;
|
|
|
|
|
QLineEdit *searchBox;
|
|
|
|
|
QAbstractButton *updateButton;
|
|
|
|
|
QListView *extensionsView;
|
|
|
|
|
QItemSelectionModel *selectionModel = nullptr;
|
|
|
|
|
QSortFilterProxyModel *filterProxyModel;
|
|
|
|
|
int columnsCount = 2;
|
|
|
|
|
Tasking::TaskTreeRunner taskTreeRunner;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
ExtensionsBrowser::ExtensionsBrowser(QWidget *parent)
|
|
|
|
|
: QWidget(parent)
|
|
|
|
|
, d(new ExtensionsBrowserPrivate)
|
2023-11-13 16:35:01 +01:00
|
|
|
{
|
|
|
|
|
setSizePolicy(QSizePolicy::Maximum, QSizePolicy::Preferred);
|
|
|
|
|
|
|
|
|
|
auto manageLabel = new QLabel(Tr::tr("Manage Extensions"));
|
|
|
|
|
manageLabel->setFont(StyleHelper::uiFont(StyleHelper::UiElementH1));
|
|
|
|
|
|
2024-05-29 16:00:22 +02:00
|
|
|
d->searchBox = new Core::SearchBox;
|
|
|
|
|
d->searchBox->setFixedWidth(itemSize.width());
|
|
|
|
|
|
|
|
|
|
d->updateButton = new Button(Tr::tr("Install..."), Button::MediumPrimary);
|
2023-11-13 16:35:01 +01:00
|
|
|
|
2024-05-29 16:00:22 +02:00
|
|
|
d->model = new ExtensionsModel(this);
|
2023-11-13 16:35:01 +01:00
|
|
|
|
2024-05-29 16:00:22 +02:00
|
|
|
d->filterProxyModel = new QSortFilterProxyModel(this);
|
|
|
|
|
d->filterProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
|
|
|
|
|
d->filterProxyModel->setFilterRole(RoleSearchText);
|
|
|
|
|
d->filterProxyModel->setSortRole(RoleItemType);
|
|
|
|
|
d->filterProxyModel->setSourceModel(d->model);
|
2023-11-13 16:35:01 +01:00
|
|
|
|
2024-05-29 16:00:22 +02:00
|
|
|
d->extensionsView = new QListView;
|
|
|
|
|
d->extensionsView->setFrameStyle(QFrame::NoFrame);
|
|
|
|
|
d->extensionsView->setItemDelegate(new ExtensionItemDelegate(this));
|
|
|
|
|
d->extensionsView->setResizeMode(QListView::Adjust);
|
|
|
|
|
d->extensionsView->setSelectionMode(QListView::SingleSelection);
|
|
|
|
|
d->extensionsView->setUniformItemSizes(true);
|
|
|
|
|
d->extensionsView->setViewMode(QListView::IconMode);
|
|
|
|
|
d->extensionsView->setModel(d->filterProxyModel);
|
|
|
|
|
d->extensionsView->setMouseTracking(true);
|
2023-11-13 16:35:01 +01:00
|
|
|
|
|
|
|
|
using namespace Layouting;
|
|
|
|
|
Column {
|
|
|
|
|
Space(15),
|
|
|
|
|
manageLabel,
|
|
|
|
|
Space(15),
|
2024-05-29 16:00:22 +02:00
|
|
|
Row { d->searchBox, st, d->updateButton, Space(extraListViewWidth() + gapSize) },
|
2023-11-13 16:35:01 +01:00
|
|
|
Space(gapSize),
|
2024-05-29 16:00:22 +02:00
|
|
|
d->extensionsView,
|
2024-05-14 10:33:01 +02:00
|
|
|
noMargin, spacing(0),
|
2023-11-13 16:35:01 +01:00
|
|
|
}.attachTo(this);
|
|
|
|
|
|
2024-02-12 17:19:41 +01:00
|
|
|
WelcomePageHelpers::setBackgroundColor(this, Theme::Token_Background_Default);
|
2024-05-29 16:00:22 +02:00
|
|
|
WelcomePageHelpers::setBackgroundColor(d->extensionsView, Theme::Token_Background_Default);
|
|
|
|
|
WelcomePageHelpers::setBackgroundColor(d->extensionsView->viewport(),
|
2024-02-12 17:19:41 +01:00
|
|
|
Theme::Token_Background_Default);
|
2023-11-13 16:35:01 +01:00
|
|
|
|
|
|
|
|
auto updateModel = [this] {
|
2024-05-29 16:00:22 +02:00
|
|
|
d->filterProxyModel->sort(0);
|
|
|
|
|
|
|
|
|
|
if (d->selectionModel == nullptr) {
|
|
|
|
|
d->selectionModel = new QItemSelectionModel(d->filterProxyModel,
|
|
|
|
|
d->extensionsView);
|
|
|
|
|
d->extensionsView->setSelectionModel(d->selectionModel);
|
|
|
|
|
connect(d->extensionsView->selectionModel(), &QItemSelectionModel::currentChanged,
|
2023-11-13 16:35:01 +01:00
|
|
|
this, &ExtensionsBrowser::itemSelected);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2024-05-29 16:00:22 +02:00
|
|
|
connect(d->updateButton, &QAbstractButton::pressed, this, []() {
|
|
|
|
|
executePluginInstallWizard();
|
|
|
|
|
});
|
2023-11-13 16:35:01 +01:00
|
|
|
connect(ExtensionSystem::PluginManager::instance(),
|
|
|
|
|
&ExtensionSystem::PluginManager::pluginsChanged, this, updateModel);
|
2024-05-29 16:00:22 +02:00
|
|
|
connect(ExtensionSystem::PluginManager::instance(),
|
|
|
|
|
&ExtensionSystem::PluginManager::initializationDone,
|
|
|
|
|
this, &ExtensionsBrowser::fetchExtensions);
|
|
|
|
|
connect(d->searchBox, &QLineEdit::textChanged,
|
|
|
|
|
d->filterProxyModel, &QSortFilterProxyModel::setFilterWildcard);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ExtensionsBrowser::~ExtensionsBrowser()
|
|
|
|
|
{
|
|
|
|
|
delete d;
|
2023-11-13 16:35:01 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
void ExtensionsBrowser::adjustToWidth(const int width)
|
|
|
|
|
{
|
|
|
|
|
const int widthForItems = width - extraListViewWidth();
|
2024-05-29 16:00:22 +02:00
|
|
|
d->columnsCount = qMax(1, qFloor(widthForItems / cellSize.width()));
|
|
|
|
|
d->updateButton->setVisible(d->columnsCount > 1);
|
2023-11-13 16:35:01 +01:00
|
|
|
updateGeometry();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
QSize ExtensionsBrowser::sizeHint() const
|
|
|
|
|
{
|
2024-05-29 16:00:22 +02:00
|
|
|
const int columsWidth = d->columnsCount * cellSize.width();
|
2023-11-13 16:35:01 +01:00
|
|
|
return { columsWidth + extraListViewWidth(), 0};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
int ExtensionsBrowser::extraListViewWidth() const
|
|
|
|
|
{
|
|
|
|
|
// TODO: Investigate "transient" scrollbar, just for this list view.
|
2024-05-29 16:00:22 +02:00
|
|
|
return d->extensionsView->style()->pixelMetric(QStyle::PM_ScrollBarExtent)
|
2023-11-13 16:35:01 +01:00
|
|
|
+ 1; // Needed
|
|
|
|
|
}
|
|
|
|
|
|
2024-05-29 16:00:22 +02:00
|
|
|
void ExtensionsBrowser::fetchExtensions()
|
|
|
|
|
{
|
|
|
|
|
// d->model->setExtensionsJson(testData("thirdpartyplugins")); return;
|
|
|
|
|
|
|
|
|
|
using namespace Tasking;
|
|
|
|
|
|
|
|
|
|
const auto onQuerySetup = [](NetworkQuery &query) {
|
|
|
|
|
const QString host = "https://qc-extensions.qt.io";
|
|
|
|
|
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());
|
|
|
|
|
|
|
|
|
|
query.setRequest(QNetworkRequest(QUrl::fromUserInput(request)));
|
|
|
|
|
query.setNetworkAccessManager(NetworkAccessManager::instance());
|
|
|
|
|
};
|
|
|
|
|
const auto onQueryDone = [this](const NetworkQuery &query, DoneWith result) {
|
|
|
|
|
if (result != DoneWith::Success) {
|
|
|
|
|
#ifdef WITH_TESTS
|
|
|
|
|
d->model->setExtensionsJson(testData("defaultpacks"));
|
|
|
|
|
#endif // WITH_TESTS
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
const QByteArray response = query.reply()->readAll();
|
|
|
|
|
d->model->setExtensionsJson(response);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
Group group {
|
|
|
|
|
NetworkQueryTask{onQuerySetup, onQueryDone},
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
d->taskTreeRunner.start(group);
|
|
|
|
|
}
|
|
|
|
|
|
2023-11-13 16:35:01 +01:00
|
|
|
} // ExtensionManager::Internal
|