From e3acf9262b610a3cd01c871366c748a6a1258db1 Mon Sep 17 00:00:00 2001 From: Eike Ziller Date: Tue, 10 Jan 2023 12:15:49 +0100 Subject: [PATCH] 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 --- src/plugins/coreplugin/welcomepagehelper.cpp | 134 +++++++++++++++- src/plugins/coreplugin/welcomepagehelper.h | 60 +++++++- src/plugins/marketplace/productlistmodel.cpp | 154 ++++--------------- src/plugins/marketplace/productlistmodel.h | 38 +---- 4 files changed, 223 insertions(+), 163 deletions(-) diff --git a/src/plugins/coreplugin/welcomepagehelper.cpp b/src/plugins/coreplugin/welcomepagehelper.cpp index 55652cb6214..534888e5439 100644 --- a/src/plugins/coreplugin/welcomepagehelper.cpp +++ b/src/plugins/coreplugin/welcomepagehelper.cpp @@ -13,12 +13,14 @@ #include #include -#include #include +#include #include +#include #include #include #include +#include #include #include @@ -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 &items) +{ + beginInsertRows(QModelIndex(), m_items.size(), m_items.size() + items.size()); + m_items.append(items); + endInsertRows(); +} + +const QList 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(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(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 §ion, const QList &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(widget(0)); + auto vbox = qobject_cast(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(m_filteredAllItemsModel->sourceModel()); + allProducts->appendItems(items); + + return model; +} + } // namespace Core diff --git a/src/plugins/coreplugin/welcomepagehelper.h b/src/plugins/coreplugin/welcomepagehelper.h index 938b3bad5f3..353c5a6d3c8 100644 --- a/src/plugins/coreplugin/welcomepagehelper.h +++ b/src/plugins/coreplugin/welcomepagehelper.h @@ -7,10 +7,11 @@ #include "iwelcomepage.h" #include +#include #include #include +#include #include -#include #include #include @@ -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; class CORE_EXPORT ListItem @@ -66,15 +77,21 @@ public: explicit ListModel(QObject *parent); ~ListModel() override; + void appendItems(const QList &items); + const QList 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 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 §ion, const QList &items); + +private: + QMap m_sectionModels; + QMap m_gridViews; + Core::GridView *m_allItemsView = nullptr; + Core::ListModelFilter *m_filteredAllItemsModel = nullptr; + Core::ListModel::PixmapFunction m_pixmapFunction; +}; + } // namespace Core Q_DECLARE_METATYPE(Core::ListItem *) diff --git a/src/plugins/marketplace/productlistmodel.cpp b/src/plugins/marketplace/productlistmodel.cpp index 390b0c2c815..e4f7e12edce 100644 --- a/src/plugins/marketplace/productlistmodel.cpp +++ b/src/plugins/marketplace/productlistmodel.cpp @@ -14,49 +14,20 @@ #include #include #include -#include #include #include #include #include #include -#include #include #include #include +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(this->parent())) - sectionedProducts->queueImageForDownload(url); - return {}; - }); -} - -void ProductListModel::appendItems(const QList &items) -{ - beginInsertRows(QModelIndex(), m_items.size(), m_items.size() + items.size()); - m_items.append(items); - endInsertRows(); -} - -const QList 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 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 §ion, const QList &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(widget(0)); - auto vbox = qobject_cast(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(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 SectionedProducts::items() { QList 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 diff --git a/src/plugins/marketplace/productlistmodel.h b/src/plugins/marketplace/productlistmodel.h index 5c6607d8618..8da6e01ee90 100644 --- a/src/plugins/marketplace/productlistmodel.h +++ b/src/plugins/marketplace/productlistmodel.h @@ -24,34 +24,7 @@ public: QString handle; }; -class ProductListModel : public Core::ListModel -{ -public: - explicit ProductListModel(QObject *parent); - void appendItems(const QList &items); - const QList 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 §ion, const QList &items); + void addNewSection(const Core::Section §ion, const QList &items); void onTagClicked(const QString &tag); QList items(); @@ -82,10 +53,7 @@ private: QQueue m_pendingCollections; QSet m_pendingImages; QMap m_collectionTitles; - QMap m_productModels; - QMap m_gridViews; - Core::GridView *m_allProductsView = nullptr; - Core::ListModelFilter *m_filteredAllProductsModel = nullptr; + QList m_productModels; ProductItemDelegate *m_productDelegate = nullptr; bool m_isDownloadingImage = false; int m_columnCount = 1;