diff --git a/src/plugins/extensionmanager/extensionmanager.qrc b/src/plugins/extensionmanager/extensionmanager.qrc
index c156db17e7d..c0325bbb20b 100644
--- a/src/plugins/extensionmanager/extensionmanager.qrc
+++ b/src/plugins/extensionmanager/extensionmanager.qrc
@@ -8,11 +8,15 @@
images/extensionbig@2x.png
images/extensionsmall.png
images/extensionsmall@2x.png
+ images/filter.png
+ images/filter@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
+ images/sort.png
+ images/sort@2x.png
diff --git a/src/plugins/extensionmanager/extensionsbrowser.cpp b/src/plugins/extensionmanager/extensionsbrowser.cpp
index 99c13fabdba..ebeb7de4622 100644
--- a/src/plugins/extensionmanager/extensionsbrowser.cpp
+++ b/src/plugins/extensionmanager/extensionsbrowser.cpp
@@ -27,6 +27,7 @@
#include
#include
+#include
#include
#include
#include
@@ -59,6 +60,96 @@ constexpr int gapSize = HGapL;
constexpr int itemWidth = 330;
constexpr int cellWidth = itemWidth + gapSize;
+class OptionChooser : public QComboBox
+{
+public:
+ OptionChooser(const FilePath &iconMask, const QString &textTemplate, QWidget *parent = nullptr)
+ : QComboBox(parent)
+ , m_iconDefault(Icon({{iconMask, m_colorDefault}}, Icon::Tint).icon())
+ , m_iconActive(Icon({{iconMask, m_colorActive}}, Icon::Tint).icon())
+ , m_textTemplate(textTemplate)
+ {
+ setMouseTracking(true);
+ connect(this, &QComboBox::currentIndexChanged, this, &QWidget::updateGeometry);
+ }
+
+protected:
+ void paintEvent([[maybe_unused]] QPaintEvent *event) override
+ {
+ // +------------+------+---------+---------------+------------+
+ // | | | | (VPaddingXs) | |
+ // | | | +---------------+ |
+ // |(HPaddingXs)|(icon)|(HGapXxs)||(HPaddingXs)|
+ // | | | +---------------+ |
+ // | | | | (VPaddingXs) | |
+ // +------------+------+---------+---------------+------------+
+
+ const bool active = currentIndex() > 0;
+ const bool hover = underMouse();
+ const TextFormat &tF = (active || hover) ? m_itemActiveTf : m_itemDefaultTf;
+
+ const QRect iconRect(HPaddingXs, 0, m_iconSize.width(), height());
+ const int textX = iconRect.right() + 1 + HGapXxs;
+ const QRect textRect(textX, VPaddingXs,
+ width() - HPaddingXs - textX, tF.lineHeight());
+
+ QPainter p(this);
+ (active ? m_iconActive : m_iconDefault).paint(&p, iconRect);
+ p.setPen(tF.color());
+ p.setFont(tF.font());
+ const QString elidedText = p.fontMetrics().elidedText(currentFormattedText(),
+ Qt::ElideRight,
+ textRect.width() + HPaddingXs);
+ p.drawText(textRect, tF.drawTextFlags, elidedText);
+ }
+
+ void enterEvent(QEnterEvent *event) override
+ {
+ QComboBox::enterEvent(event);
+ update();
+ }
+
+ void leaveEvent(QEvent *event) override
+ {
+ QComboBox::leaveEvent(event);
+ update();
+ }
+
+private:
+ QSize sizeHint() const override
+ {
+ const QFontMetrics fm(m_itemDefaultTf.font());
+ const int textWidth = fm.horizontalAdvance(currentFormattedText());
+ const int width =
+ HPaddingXs
+ + m_iconSize.width()
+ + HGapXxs
+ + textWidth
+ + HPaddingXs;
+ const int height =
+ VPaddingXs
+ + m_itemDefaultTf.lineHeight()
+ + VPaddingXs;
+ return {width, height};
+ }
+
+ QString currentFormattedText() const
+ {
+ return m_textTemplate.arg(currentText());
+ }
+
+ constexpr static Theme::Color m_colorDefault = Theme::Token_Text_Muted;
+ constexpr static Theme::Color m_colorActive = Theme::Token_Text_Default;
+ constexpr static QSize m_iconSize{16, 16};
+ constexpr static TextFormat m_itemDefaultTf
+ {m_colorDefault, UiElement::UiElementLabelMedium};
+ constexpr static TextFormat m_itemActiveTf
+ {m_colorActive, m_itemDefaultTf.uiElement};
+ const QIcon m_iconDefault;
+ const QIcon m_iconActive;
+ const QString m_textTemplate;
+};
+
static QString extensionStateDisplayString(ExtensionState state)
{
switch (state) {
@@ -280,38 +371,117 @@ public:
class SortFilterProxyModel : public QSortFilterProxyModel
{
public:
- SortFilterProxyModel(QObject *parent = nullptr);
+ struct SortOption {
+ const QString displayName;
+ const Role role;
+ const Qt::SortOrder order = Qt::AscendingOrder;
+ };
+
+ struct FilterOption {
+ const QString displayName;
+ const std::function indexAcceptedFunc;
+ };
+
+ SortFilterProxyModel(QObject *parent = nullptr)
+ : QSortFilterProxyModel(parent)
+ {
+ setSortCaseSensitivity(Qt::CaseInsensitive);
+ }
+
+ static const QList &sortOptions()
+ {
+ static const QList options = {
+ {Tr::tr("Name"), RoleName},
+ {Tr::tr("Vendor"), RoleVendor},
+ {Tr::tr("Popularity"), RoleDownloadCount, Qt::DescendingOrder},
+ };
+ return options;
+ }
+
+ void setSortOption(int index)
+ {
+ QTC_ASSERT(index < sortOptions().count(), index = 0);
+ m_sortOptionIndex = index;
+ const SortOption &option = sortOptions().at(index);
+
+ // Ensure some order for cases with insufficient data, e.g. RoleDownloadCount
+ setSortRole(RoleName);
+ sort(0);
+ if (option.role == RoleName)
+ return; // Already sorted.
+
+ setSortRole(option.role);
+ sort(0, option.order);
+ }
+
+ static const QList &filterOptions()
+ {
+ static const QList options = {
+ {
+ Tr::tr("All"),
+ []([[maybe_unused]] const QModelIndex &index) {
+ return true;
+ },
+ },
+ {
+ Tr::tr("Extension packs"),
+ [](const QModelIndex &index) {
+ return index.data(RoleItemType).value() == ItemTypePack;
+ },
+ },
+ {
+ Tr::tr("Individual extensions"),
+ [](const QModelIndex &index) {
+ return index.data(RoleItemType).value() == ItemTypeExtension;
+ },
+ },
+ };
+ return options;
+ }
+
+ void setFilterOption(int index)
+ {
+ QTC_ASSERT(index < filterOptions().count(), index = 0);
+ beginResetModel();
+ m_filterOptionIndex = index;
+ endResetModel();
+ }
protected:
- bool lessThan(const QModelIndex &left, const QModelIndex &right) const override;
+ bool lessThan(const QModelIndex &left, const QModelIndex &right) const override
+ {
+ const SortOption &option = sortOptions().at(m_sortOptionIndex);
+ const ItemType leftType = left.data(RoleItemType).value();
+ const ItemType rightType = right.data(RoleItemType).value();
+ if (leftType != rightType)
+ return option.order == Qt::AscendingOrder ? leftType < rightType
+ : leftType > rightType;
+
+ return QSortFilterProxyModel::lessThan(left, right);
+ }
+
+ bool filterAcceptsRow(int source_row, const QModelIndex &source_parent) const override
+ {
+ const QModelIndex index = sourceModel()->index(source_row, 0, source_parent);
+ return filterOptions().at(m_filterOptionIndex).indexAcceptedFunc(index);
+ }
+
+ int m_filterOptionIndex = 0;
+ int m_sortOptionIndex = 0;
};
-SortFilterProxyModel::SortFilterProxyModel(QObject *parent)
- : QSortFilterProxyModel(parent)
-{
-}
-
-bool SortFilterProxyModel::lessThan(const QModelIndex &left, const QModelIndex &right) const
-{
- const ItemType leftType = left.data(RoleItemType).value();
- const ItemType rightType = right.data(RoleItemType).value();
- if (leftType != rightType)
- return leftType < rightType;
-
- const QString leftName = left.data(RoleName).toString();
- const QString rightName = right.data(RoleName).toString();
- return leftName < rightName;
-}
-
class ExtensionsBrowserPrivate
{
public:
bool dataFetched = false;
ExtensionsModel *model;
QLineEdit *searchBox;
+ OptionChooser *filterChooser;
+ OptionChooser *sortChooser;
QListView *extensionsView;
QItemSelectionModel *selectionModel = nullptr;
- SortFilterProxyModel *filterProxyModel;
+ QSortFilterProxyModel *searchProxyModel;
+ SortFilterProxyModel *sortFilterProxyModel;
int columnsCount = 2;
Tasking::TaskTreeRunner taskTreeRunner;
SpinnerSolution::Spinner *m_spinner;
@@ -333,11 +503,22 @@ ExtensionsBrowser::ExtensionsBrowser(QWidget *parent)
d->model = new ExtensionsModel(this);
- d->filterProxyModel = new SortFilterProxyModel(this);
- d->filterProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
- d->filterProxyModel->setFilterRole(RoleSearchText);
- d->filterProxyModel->setSortRole(RoleItemType);
- d->filterProxyModel->setSourceModel(d->model);
+ d->searchProxyModel = new QSortFilterProxyModel(this);
+ d->searchProxyModel->setFilterCaseSensitivity(Qt::CaseInsensitive);
+ d->searchProxyModel->setFilterRole(RoleSearchText);
+ d->searchProxyModel->setSourceModel(d->model);
+
+ d->sortFilterProxyModel = new SortFilterProxyModel(this);
+ d->sortFilterProxyModel->setSourceModel(d->searchProxyModel);
+
+ d->filterChooser = new OptionChooser(":/extensionmanager/images/filter.png",
+ Tr::tr("Filter by: %1"));
+ d->filterChooser->addItems(Utils::transform(SortFilterProxyModel::filterOptions(),
+ &SortFilterProxyModel::FilterOption::displayName));
+
+ d->sortChooser = new OptionChooser(":/extensionmanager/images/sort.png", Tr::tr("Sort by: %1"));
+ d->sortChooser->addItems(Utils::transform(SortFilterProxyModel::sortOptions(),
+ &SortFilterProxyModel::SortOption::displayName));
d->extensionsView = new QListView;
d->extensionsView->setFrameStyle(QFrame::NoFrame);
@@ -346,7 +527,7 @@ ExtensionsBrowser::ExtensionsBrowser(QWidget *parent)
d->extensionsView->setSelectionMode(QListView::SingleSelection);
d->extensionsView->setUniformItemSizes(true);
d->extensionsView->setViewMode(QListView::IconMode);
- d->extensionsView->setModel(d->filterProxyModel);
+ d->extensionsView->setModel(d->sortFilterProxyModel);
d->extensionsView->setMouseTracking(true);
using namespace Layouting;
@@ -360,7 +541,13 @@ ExtensionsBrowser::ExtensionsBrowser(QWidget *parent)
spacing(gapSize),
customMargins(0, VPaddingM, extraListViewWidth() + gapSize, VPaddingM),
},
- Space(ExPaddingGapL),
+ Row {
+ d->filterChooser,
+ Space(HGapS),
+ d->sortChooser,
+ st,
+ customMargins(0, 0, extraListViewWidth() + gapSize, 0),
+ },
d->extensionsView,
noMargin, spacing(0),
}.attachTo(this);
@@ -374,10 +561,10 @@ ExtensionsBrowser::ExtensionsBrowser(QWidget *parent)
d->m_spinner->hide();
auto updateModel = [this] {
- d->filterProxyModel->sort(0);
+ d->sortFilterProxyModel->sort(0);
if (d->selectionModel == nullptr) {
- d->selectionModel = new QItemSelectionModel(d->filterProxyModel,
+ d->selectionModel = new QItemSelectionModel(d->sortFilterProxyModel,
d->extensionsView);
d->extensionsView->setSelectionModel(d->selectionModel);
connect(d->extensionsView->selectionModel(), &QItemSelectionModel::currentChanged,
@@ -387,7 +574,11 @@ ExtensionsBrowser::ExtensionsBrowser(QWidget *parent)
connect(PluginManager::instance(), &PluginManager::pluginsChanged, this, updateModel);
connect(d->searchBox, &QLineEdit::textChanged,
- d->filterProxyModel, &QSortFilterProxyModel::setFilterWildcard);
+ d->searchProxyModel, &QSortFilterProxyModel::setFilterWildcard);
+ connect(d->sortChooser, &OptionChooser::currentIndexChanged,
+ d->sortFilterProxyModel, &SortFilterProxyModel::setSortOption);
+ connect(d->filterChooser, &OptionChooser::currentIndexChanged,
+ d->sortFilterProxyModel, &SortFilterProxyModel::setFilterOption);
}
ExtensionsBrowser::~ExtensionsBrowser()
diff --git a/src/plugins/extensionmanager/images/filter.png b/src/plugins/extensionmanager/images/filter.png
new file mode 100644
index 00000000000..52c93ebcabd
Binary files /dev/null and b/src/plugins/extensionmanager/images/filter.png differ
diff --git a/src/plugins/extensionmanager/images/filter@2x.png b/src/plugins/extensionmanager/images/filter@2x.png
new file mode 100644
index 00000000000..e8a18038c1d
Binary files /dev/null and b/src/plugins/extensionmanager/images/filter@2x.png differ
diff --git a/src/plugins/extensionmanager/images/sort.png b/src/plugins/extensionmanager/images/sort.png
new file mode 100644
index 00000000000..eb98b5fd566
Binary files /dev/null and b/src/plugins/extensionmanager/images/sort.png differ
diff --git a/src/plugins/extensionmanager/images/sort@2x.png b/src/plugins/extensionmanager/images/sort@2x.png
new file mode 100644
index 00000000000..8a82201052b
Binary files /dev/null and b/src/plugins/extensionmanager/images/sort@2x.png differ
diff --git a/src/tools/icons/qtcreatoricons.svg b/src/tools/icons/qtcreatoricons.svg
index dcf5942a335..fbeee7845f9 100644
--- a/src/tools/icons/qtcreatoricons.svg
+++ b/src/tools/icons/qtcreatoricons.svg
@@ -3906,6 +3906,44 @@
d="m 69,431 1.5,1.5 2.5,-3"
id="path10003" />
+
+
+
+
+
+
+
+
+