Make categorized product/example view reusable

Extract a SectionedGridView from the SectionedProducts that are used in
the MarketPlace plugin, and make item delegate and pixmap fetching
function to be used with the model(s) pluggable.

Change-Id: I02aba87b27afd8ad18ff23346d1ac98da906db4b
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
Eike Ziller
2023-01-10 12:15:49 +01:00
parent 6d79c5c2b3
commit e3acf9262b
4 changed files with 223 additions and 163 deletions

View File

@@ -13,12 +13,14 @@
#include <QEasingCurve>
#include <QFontDatabase>
#include <QHeaderView>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QHoverEvent>
#include <QLabel>
#include <QMouseEvent>
#include <QPainter>
#include <QPixmapCache>
#include <QScrollArea>
#include <QTimer>
#include <qdrawutil.h>
@@ -117,6 +119,22 @@ void GridView::leaveEvent(QEvent *)
viewportEvent(&hev); // Seemingly needed to kill the hover paint.
}
SectionGridView::SectionGridView(QWidget *parent)
: GridView(parent)
{}
bool SectionGridView::hasHeightForWidth() const
{
return true;
}
int SectionGridView::heightForWidth(int width) const
{
const int columnCount = width / Core::ListItemDelegate::GridItemWidth;
const int rowCount = (model()->rowCount() + columnCount - 1) / columnCount;
return rowCount * Core::ListItemDelegate::GridItemHeight;
}
const QSize ListModel::defaultImageSize(214, 160);
ListModel::ListModel(QObject *parent)
@@ -126,10 +144,23 @@ ListModel::ListModel(QObject *parent)
ListModel::~ListModel()
{
qDeleteAll(m_items);
if (m_ownsItems)
qDeleteAll(m_items);
m_items.clear();
}
void ListModel::appendItems(const QList<ListItem *> &items)
{
beginInsertRows(QModelIndex(), m_items.size(), m_items.size() + items.size());
m_items.append(items);
endInsertRows();
}
const QList<ListItem *> ListModel::items() const
{
return m_items;
}
int ListModel::rowCount(const QModelIndex &) const
{
return m_items.size();
@@ -166,6 +197,11 @@ void ListModel::setPixmapFunction(const PixmapFunction &fetchPixmapAndUpdatePixm
m_fetchPixmapAndUpdatePixmapCache = fetchPixmapAndUpdatePixmapCache;
}
void ListModel::setOwnsItems(bool owns)
{
m_ownsItems = owns;
}
ListModelFilter::ListModelFilter(ListModel *sourceModel, QObject *parent) :
QSortFilterProxyModel(parent)
{
@@ -346,6 +382,11 @@ void ListModelFilter::setSearchString(const QString &arg)
delayedUpdateFilter();
}
ListModel *ListModelFilter::sourceListModel() const
{
return static_cast<ListModel *>(sourceModel());
}
bool ListModelFilter::leaveFilterAcceptsRowBeforeFiltering(const ListItem *, bool *) const
{
return false;
@@ -583,4 +624,93 @@ void ListItemDelegate::goon()
m_currentWidget->update(m_previousIndex);
}
SectionedGridView::SectionedGridView(QWidget *parent)
: QStackedWidget(parent)
, m_allItemsView(new Core::GridView(this))
{
auto allItemsModel = new ListModel(this);
allItemsModel->setPixmapFunction(m_pixmapFunction);
// it just "borrows" the items from the section models:
allItemsModel->setOwnsItems(false);
m_filteredAllItemsModel = new Core::ListModelFilter(allItemsModel, this);
auto area = new QScrollArea(this);
area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
area->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
area->setFrameShape(QFrame::NoFrame);
area->setWidgetResizable(true);
auto sectionedView = new QWidget;
auto layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->addStretch();
sectionedView->setLayout(layout);
area->setWidget(sectionedView);
addWidget(area);
m_allItemsView->setModel(m_filteredAllItemsModel);
addWidget(m_allItemsView);
}
SectionedGridView::~SectionedGridView() = default;
void SectionedGridView::setItemDelegate(QAbstractItemDelegate *delegate)
{
m_allItemsView->setItemDelegate(delegate);
for (GridView *view : std::as_const(m_gridViews))
view->setItemDelegate(delegate);
}
void SectionedGridView::setPixmapFunction(const Core::ListModel::PixmapFunction &pixmapFunction)
{
m_pixmapFunction = pixmapFunction;
auto allProducts = static_cast<ListModel *>(m_filteredAllItemsModel->sourceModel());
allProducts->setPixmapFunction(pixmapFunction);
for (ListModel *model : std::as_const(m_sectionModels))
model->setPixmapFunction(pixmapFunction);
}
void SectionedGridView::setSearchString(const QString &searchString)
{
int view = searchString.isEmpty() ? 0 // sectioned view
: 1; // search view
setCurrentIndex(view);
m_filteredAllItemsModel->setSearchString(searchString);
}
ListModel *SectionedGridView::addSection(const Section &section, const QList<ListItem *> &items)
{
auto model = new ListModel(this);
model->setPixmapFunction(m_pixmapFunction);
model->appendItems(items);
auto gridView = new SectionGridView(this);
gridView->setItemDelegate(m_allItemsView->itemDelegate());
gridView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
gridView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
gridView->setModel(model);
m_sectionModels.insert(section, model);
const auto it = m_gridViews.insert(section, gridView);
auto sectionLabel = new QLabel(section.name);
sectionLabel->setContentsMargins(0, Core::WelcomePageHelpers::ItemGap, 0, 0);
sectionLabel->setFont(Core::WelcomePageHelpers::brandFont());
auto scrollArea = qobject_cast<QScrollArea *>(widget(0));
auto vbox = qobject_cast<QVBoxLayout *>(scrollArea->widget()->layout());
// insert new section depending on its priority, but before the last (stretch) item
int position = std::distance(m_gridViews.begin(), it) * 2; // a section has a label and a grid
QTC_ASSERT(position <= vbox->count() - 1, position = vbox->count() - 1);
vbox->insertWidget(position, sectionLabel);
vbox->insertWidget(position + 1, gridView);
// add the items also to the all products model to be able to search correctly
auto allProducts = static_cast<ListModel *>(m_filteredAllItemsModel->sourceModel());
allProducts->appendItems(items);
return model;
}
} // namespace Core

View File

@@ -7,10 +7,11 @@
#include "iwelcomepage.h"
#include <QElapsedTimer>
#include <QListView>
#include <QPointer>
#include <QSortFilterProxyModel>
#include <QStackedWidget>
#include <QStyledItemDelegate>
#include <QListView>
#include <functional>
#include <optional>
@@ -40,10 +41,20 @@ class CORE_EXPORT GridView : public QListView
{
public:
explicit GridView(QWidget *parent);
protected:
void leaveEvent(QEvent *) final;
};
class CORE_EXPORT SectionGridView : public GridView
{
public:
explicit SectionGridView(QWidget *parent);
bool hasHeightForWidth() const;
int heightForWidth(int width) const;
};
using OptModelIndex = std::optional<QModelIndex>;
class CORE_EXPORT ListItem
@@ -66,15 +77,21 @@ public:
explicit ListModel(QObject *parent);
~ListModel() override;
void appendItems(const QList<ListItem *> &items);
const QList<ListItem *> items() const;
int rowCount(const QModelIndex &parent = QModelIndex()) const final;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
void setPixmapFunction(const PixmapFunction &fetchPixmapAndUpdatePixmapCache);
static const QSize defaultImageSize;
void setOwnsItems(bool owns);
protected:
QList<ListItem *> m_items;
PixmapFunction m_fetchPixmapAndUpdatePixmapCache;
bool m_ownsItems = true;
};
class CORE_EXPORT ListModelFilter : public QSortFilterProxyModel
@@ -84,6 +101,8 @@ public:
void setSearchString(const QString &arg);
ListModel *sourceListModel() const;
protected:
virtual bool leaveFilterAcceptsRowBeforeFiltering(const ListItem *item,
bool *earlyExitResult) const;
@@ -142,6 +161,45 @@ private:
mutable QPixmap m_blurredThumbnail;
};
class CORE_EXPORT Section
{
public:
friend bool operator<(const Section &lhs, const Section &rhs)
{
if (lhs.priority < rhs.priority)
return true;
return lhs.priority > rhs.priority ? false : lhs.name < rhs.name;
}
friend bool operator==(const Section &lhs, const Section &rhs)
{
return lhs.priority == rhs.priority && lhs.name == rhs.name;
}
QString name;
int priority;
};
class CORE_EXPORT SectionedGridView : public QStackedWidget
{
public:
explicit SectionedGridView(QWidget *parent = nullptr);
~SectionedGridView();
void setItemDelegate(QAbstractItemDelegate *delegate);
void setPixmapFunction(const Core::ListModel::PixmapFunction &pixmapFunction);
void setSearchString(const QString &searchString);
Core::ListModel *addSection(const Section &section, const QList<Core::ListItem *> &items);
private:
QMap<Section, Core::ListModel *> m_sectionModels;
QMap<Section, Core::GridView *> m_gridViews;
Core::GridView *m_allItemsView = nullptr;
Core::ListModelFilter *m_filteredAllItemsModel = nullptr;
Core::ListModel::PixmapFunction m_pixmapFunction;
};
} // namespace Core
Q_DECLARE_METATYPE(Core::ListItem *)

View File

@@ -14,49 +14,20 @@
#include <QJsonArray>
#include <QJsonDocument>
#include <QJsonObject>
#include <QLabel>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPixmapCache>
#include <QRegularExpression>
#include <QScrollArea>
#include <QTimer>
#include <QUrl>
#include <QVBoxLayout>
using namespace Core;
namespace Marketplace {
namespace Internal {
/**
* @brief AllProductsModel does not own its items. Using this model only to display
* the same items stored inside other models without the need to duplicate the items.
*/
class AllProductsModel : public ProductListModel
{
public:
explicit AllProductsModel(QObject *parent) : ProductListModel(parent) {}
~AllProductsModel() override { m_items.clear(); }
};
class ProductGridView : public Core::GridView
{
public:
ProductGridView(QWidget *parent) : Core::GridView(parent) {}
bool hasHeightForWidth() const override
{
return true;
}
int heightForWidth(int width) const override
{
const int columnCount = width / Core::ListItemDelegate::GridItemWidth;
const int rowCount = (model()->rowCount() + columnCount - 1) / columnCount;
return rowCount * Core::ListItemDelegate::GridItemHeight;
}
};
class ProductItemDelegate : public Core::ListItemDelegate
{
public:
@@ -69,28 +40,6 @@ public:
}
};
ProductListModel::ProductListModel(QObject *parent)
: Core::ListModel(parent)
{
setPixmapFunction([this](const QString &url) -> QPixmap {
if (auto sectionedProducts = qobject_cast<SectionedProducts *>(this->parent()))
sectionedProducts->queueImageForDownload(url);
return {};
});
}
void ProductListModel::appendItems(const QList<Core::ListItem *> &items)
{
beginInsertRows(QModelIndex(), m_items.size(), m_items.size() + items.size());
m_items.append(items);
endInsertRows();
}
const QList<Core::ListItem *> ProductListModel::items() const
{
return m_items;
}
static const QNetworkRequest constructRequest(const QString &collection)
{
QString url("https://marketplace.qt.io");
@@ -128,37 +77,22 @@ static int priority(const QString &collection)
}
SectionedProducts::SectionedProducts(QWidget *parent)
: QStackedWidget(parent)
, m_allProductsView(new Core::GridView(this))
, m_filteredAllProductsModel(new Core::ListModelFilter(new AllProductsModel(this), this))
: SectionedGridView(parent)
, m_productDelegate(new ProductItemDelegate)
{
auto area = new QScrollArea(this);
area->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
area->setVerticalScrollBarPolicy(Qt::ScrollBarAsNeeded);
area->setFrameShape(QFrame::NoFrame);
area->setWidgetResizable(true);
auto sectionedView = new QWidget;
auto layout = new QVBoxLayout;
layout->setContentsMargins(0, 0, 0, 0);
layout->addStretch();
sectionedView->setLayout(layout);
area->setWidget(sectionedView);
addWidget(area);
m_allProductsView->setItemDelegate(m_productDelegate);
m_allProductsView->setModel(m_filteredAllProductsModel);
addWidget(m_allProductsView);
connect(m_productDelegate, &ProductItemDelegate::tagClicked,
this, &SectionedProducts::onTagClicked);
setItemDelegate(m_productDelegate);
setPixmapFunction([this](const QString &url) -> QPixmap {
queueImageForDownload(url);
return {};
});
connect(m_productDelegate,
&ProductItemDelegate::tagClicked,
this,
&SectionedProducts::onTagClicked);
}
SectionedProducts::~SectionedProducts()
{
qDeleteAll(m_gridViews);
delete m_productDelegate;
}
@@ -286,12 +220,15 @@ void SectionedProducts::queueImageForDownload(const QString &url)
fetchNextImage();
}
void SectionedProducts::setSearchString(const QString &searchString)
static void updateModelIndexesForUrl(ListModel *model, const QString &url)
{
int view = searchString.isEmpty() ? 0 // sectioned view
: 1; // search view
setCurrentIndex(view);
m_filteredAllProductsModel->setSearchString(searchString);
const QList<ListItem *> items = model->items();
for (int row = 0, end = items.size(); row < end; ++row) {
if (items.at(row)->imageUrl == url) {
const QModelIndex index = model->index(row);
emit model->dataChanged(index, index, {ListModel::ItemImageRole, Qt::DisplayRole});
}
}
}
void SectionedProducts::fetchNextImage()
@@ -307,8 +244,8 @@ void SectionedProducts::fetchNextImage()
if (QPixmapCache::find(nextUrl, nullptr)) {
// this image is already cached it might have been added while downloading
for (ProductListModel *model : std::as_const(m_productModels))
model->updateModelIndexesForUrl(nextUrl);
for (ListModel *model : std::as_const(m_productModels))
updateModelIndexesForUrl(model, nextUrl);
fetchNextImage();
return;
}
@@ -332,12 +269,13 @@ void SectionedProducts::onImageDownloadFinished(QNetworkReply *reply)
if (pixmap.loadFromData(data, imageFormat.toLatin1())) {
const QString url = imageUrl.toString();
const int dpr = qApp->devicePixelRatio();
pixmap = pixmap.scaled(ProductListModel::defaultImageSize * dpr,
Qt::KeepAspectRatio, Qt::SmoothTransformation);
pixmap = pixmap.scaled(ListModel::defaultImageSize * dpr,
Qt::KeepAspectRatio,
Qt::SmoothTransformation);
pixmap.setDevicePixelRatio(dpr);
QPixmapCache::insert(url, pixmap);
for (ProductListModel *model : std::as_const(m_productModels))
model->updateModelIndexesForUrl(url);
for (ListModel *model : std::as_const(m_productModels))
updateModelIndexesForUrl(model, url);
}
} // handle error not needed - it's okay'ish to have no images as long as the rest works
@@ -347,33 +285,7 @@ void SectionedProducts::onImageDownloadFinished(QNetworkReply *reply)
void SectionedProducts::addNewSection(const Section &section, const QList<Core::ListItem *> &items)
{
QTC_ASSERT(!items.isEmpty(), return);
ProductListModel *productModel = new ProductListModel(this);
productModel->appendItems(items);
auto filteredModel = new Core::ListModelFilter(productModel, this);
auto gridView = new ProductGridView(this);
gridView->setItemDelegate(m_productDelegate);
gridView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
gridView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
gridView->setModel(filteredModel);
m_productModels.insert(section, productModel);
const auto it = m_gridViews.insert(section, gridView);
auto sectionLabel = new QLabel(section.name);
sectionLabel->setContentsMargins(0, Core::WelcomePageHelpers::ItemGap, 0, 0);
sectionLabel->setFont(Core::WelcomePageHelpers::brandFont());
auto scrollArea = qobject_cast<QScrollArea *>(widget(0));
auto vbox = qobject_cast<QVBoxLayout *>(scrollArea->widget()->layout());
// insert new section depending on its priority, but before the last (stretch) item
int position = std::distance(m_gridViews.begin(), it) * 2; // a section has a label and a grid
QTC_ASSERT(position <= vbox->count() - 1, position = vbox->count() - 1);
vbox->insertWidget(position, sectionLabel);
vbox->insertWidget(position + 1, gridView);
// add the items also to the all products model to be able to search correctly
auto allProducts = static_cast<ProductListModel *>(m_filteredAllProductsModel->sourceModel());
allProducts->appendItems(items);
m_productModels.append(addSection(section, items));
}
void SectionedProducts::onTagClicked(const QString &tag)
@@ -385,18 +297,10 @@ void SectionedProducts::onTagClicked(const QString &tag)
QList<Core::ListItem *> SectionedProducts::items()
{
QList<Core::ListItem *> result;
for (const ProductListModel *model : std::as_const(m_productModels))
for (const ListModel *model : std::as_const(m_productModels))
result.append(model->items());
return result;
}
void ProductListModel::updateModelIndexesForUrl(const QString &url)
{
for (int row = 0, end = m_items.size(); row < end; ++row) {
if (m_items.at(row)->imageUrl == url)
emit dataChanged(index(row), index(row), {ItemImageRole, Qt::DisplayRole});
}
}
} // namespace Internal
} // namespace Marketplace

View File

@@ -24,34 +24,7 @@ public:
QString handle;
};
class ProductListModel : public Core::ListModel
{
public:
explicit ProductListModel(QObject *parent);
void appendItems(const QList<Core::ListItem *> &items);
const QList<Core::ListItem *> items() const;
void updateModelIndexesForUrl(const QString &url);
};
struct Section
{
friend bool operator<(const Section &lhs, const Section &rhs)
{
if (lhs.priority < rhs.priority)
return true;
return lhs.priority > rhs.priority ? false : lhs.name < rhs.name;
}
friend bool operator==(const Section &lhs, const Section &rhs)
{
return lhs.priority == rhs.priority && lhs.name == rhs.name;
}
QString name;
int priority;
};
class SectionedProducts : public QStackedWidget
class SectionedProducts : public Core::SectionedGridView
{
Q_OBJECT
public:
@@ -59,8 +32,6 @@ public:
~SectionedProducts() override;
void updateCollections();
void queueImageForDownload(const QString &url);
void setColumnCount(int columns);
void setSearchString(const QString &searchString);
signals:
void errorOccurred(int errorCode, const QString &errorString);
@@ -74,7 +45,7 @@ private:
void fetchNextImage();
void onImageDownloadFinished(QNetworkReply *reply);
void addNewSection(const Section &section, const QList<Core::ListItem *> &items);
void addNewSection(const Core::Section &section, const QList<Core::ListItem *> &items);
void onTagClicked(const QString &tag);
QList<Core::ListItem *> items();
@@ -82,10 +53,7 @@ private:
QQueue<QString> m_pendingCollections;
QSet<QString> m_pendingImages;
QMap<QString, QString> m_collectionTitles;
QMap<Section, ProductListModel *> m_productModels;
QMap<Section, Core::GridView *> m_gridViews;
Core::GridView *m_allProductsView = nullptr;
Core::ListModelFilter *m_filteredAllProductsModel = nullptr;
QList<Core::ListModel *> m_productModels;
ProductItemDelegate *m_productDelegate = nullptr;
bool m_isDownloadingImage = false;
int m_columnCount = 1;