diff options
author | Christian Stenger <christian.stenger@qt.io> | 2020-05-11 16:45:06 +0200 |
---|---|---|
committer | Christian Stenger <christian.stenger@qt.io> | 2020-05-14 13:16:49 +0000 |
commit | c3946529cab30205607bc0d343c97d4edfe53196 (patch) | |
tree | eda2a26000a948b5c33092930c580e51c39461e1 /src/plugins/marketplace | |
parent | dd9bed93f06d53d119946db6eb5cbf83ee67df32 (diff) |
Marketplace: Use sections to display products
Fixes: QTCREATORBUG-23808
Change-Id: I2f69697c6ab2133ccf4567bf8f5185bac34a86c7
Reviewed-by: David Schulz <david.schulz@qt.io>
Diffstat (limited to 'src/plugins/marketplace')
-rw-r--r-- | src/plugins/marketplace/productlistmodel.cpp | 242 | ||||
-rw-r--r-- | src/plugins/marketplace/productlistmodel.h | 57 | ||||
-rw-r--r-- | src/plugins/marketplace/qtmarketplacewelcomepage.cpp | 45 |
3 files changed, 285 insertions, 59 deletions
diff --git a/src/plugins/marketplace/productlistmodel.cpp b/src/plugins/marketplace/productlistmodel.cpp index cded10ee37..da77e6ce20 100644 --- a/src/plugins/marketplace/productlistmodel.cpp +++ b/src/plugins/marketplace/productlistmodel.cpp @@ -32,20 +32,98 @@ #include <utils/networkaccessmanager.h> #include <utils/qtcassert.h> +#include <QDesktopServices> #include <QJsonArray> #include <QJsonDocument> #include <QJsonObject> +#include <QLabel> #include <QNetworkAccessManager> #include <QNetworkReply> #include <QNetworkRequest> #include <QPixmapCache> #include <QRegularExpression> +#include <QScrollArea> #include <QTimer> #include <QUrl> 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) {} + QSize viewportSizeHint() const override + { + if (!model()) + return Core::GridView::viewportSizeHint(); + + static int gridW = Core::GridProxyModel::GridItemWidth + Core::GridProxyModel::GridItemGap; + static int gridH = Core::GridProxyModel::GridItemHeight + Core::GridProxyModel::GridItemGap; + return QSize(model()->columnCount() * gridW, model()->rowCount() * gridH); + } + + void setColumnCount(int columnCount) + { + if (columnCount < 1) + columnCount = 1; + if (auto gridProxyModel = dynamic_cast<Core::GridProxyModel *>(model())) + gridProxyModel->setColumnCount(columnCount); + } +}; + +class ProductItemDelegate : public Core::ListItemDelegate +{ +public: + QSize sizeHint(const QStyleOptionViewItem &option, const QModelIndex &index) const override + { + const Core::ListItem *item = index.data(Core::ListModel::ItemRole).value<Core::ListItem *>(); + + // "empty" items (last row of a section) + if (!item) + return Core::ListItemDelegate::sizeHint(option, index); + + return QSize(Core::GridProxyModel::GridItemWidth + Core::GridProxyModel::GridItemGap, + Core::GridProxyModel::GridItemHeight + Core::GridProxyModel::GridItemGap); + } + + void clickAction(const Core::ListItem *item) const override + { + QTC_ASSERT(item, return); + auto productItem = static_cast<const ProductItem *>(item); + const QUrl url(QString("https://marketplace.qt.io/products/").append(productItem->handle)); + QDesktopServices::openUrl(url); + } +}; + +ProductListModel::ProductListModel(QObject *parent) + : Core::ListModel(parent) +{ +} + +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"); @@ -71,12 +149,52 @@ static const QString plainTextFromHtml(const QString &original) return (plainText.length() > 157) ? plainText.left(157).append("...") : plainText; } -ProductListModel::ProductListModel(QObject *parent) - : Core::ListModel(parent) +static int priority(const QString &collection) +{ + if (collection == "featured") + return 10; + if (collection == "from-qt-partners") + return 20; + return 50; +} + +SectionedProducts::SectionedProducts(QWidget *parent) + : QStackedWidget(parent) + , m_allProductsView(new ProductGridView(this)) + , m_filteredAllProductsModel(new Core::ListModelFilter(new AllProductsModel(this), this)) + , 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->addStretch(); + sectionedView->setLayout(layout); + area->setWidget(sectionedView); + + addWidget(area); + + auto gridModel = new Core::GridProxyModel; + gridModel->setSourceModel(m_filteredAllProductsModel); + m_allProductsView->setItemDelegate(m_productDelegate); + m_allProductsView->setModel(gridModel); + addWidget(m_allProductsView); + + connect(m_productDelegate, &ProductItemDelegate::tagClicked, + this, &SectionedProducts::onTagClicked); +} + +SectionedProducts::~SectionedProducts() { + qDeleteAll(m_gridViews.values()); + delete m_productDelegate; } -void ProductListModel::updateCollections() +void SectionedProducts::updateCollections() { emit toggleProgressIndicator(true); QNetworkReply *reply = Utils::NetworkAccessManager::instance()->get(constructRequest({})); @@ -86,11 +204,12 @@ void ProductListModel::updateCollections() QPixmap ProductListModel::fetchPixmapAndUpdatePixmapCache(const QString &url) const { - const_cast<ProductListModel *>(this)->queueImageForDownload(url); + if (auto sectionedProducts = qobject_cast<SectionedProducts *>(parent())) + sectionedProducts->queueImageForDownload(url); return QPixmap(); } -void ProductListModel::onFetchCollectionsFinished(QNetworkReply *reply) +void SectionedProducts::onFetchCollectionsFinished(QNetworkReply *reply) { QTC_ASSERT(reply, return); Utils::ExecuteOnDestruction replyDeleter([reply]() { reply->deleteLater(); }); @@ -106,21 +225,23 @@ void ProductListModel::onFetchCollectionsFinished(QNetworkReply *reply) const auto handle = obj.value("handle").toString(); const int productsCount = obj.value("products_count").toInt(); - if (productsCount > 0 && handle != "all-products" && handle != "qt-education-1") + if (productsCount > 0 && handle != "all-products" && handle != "qt-education-1") { + m_collectionTitles.insert(handle, obj.value("title").toString()); m_pendingCollections.append(handle); + } } if (!m_pendingCollections.isEmpty()) fetchCollectionsContents(); } else { QVariant status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute); if (status.isValid() && status.toInt() == 430) - QTimer::singleShot(30000, this, &ProductListModel::updateCollections); + QTimer::singleShot(30000, this, &SectionedProducts::updateCollections); else emit errorOccurred(reply->error(), reply->errorString()); } } -void ProductListModel::onFetchSingleCollectionFinished(QNetworkReply *reply) +void SectionedProducts::onFetchSingleCollectionFinished(QNetworkReply *reply) { emit toggleProgressIndicator(false); @@ -133,12 +254,19 @@ void ProductListModel::onFetchSingleCollectionFinished(QNetworkReply *reply) if (doc.isNull()) return; + QString collectionHandle = reply->url().path(); + if (QTC_GUARD(collectionHandle.endsWith("/products.json"))) { + collectionHandle.chop(14); + collectionHandle = collectionHandle.mid(collectionHandle.lastIndexOf('/') + 1); + } + + const QList<Core::ListItem *> presentItems = items(); const QJsonArray products = doc.object().value("products").toArray(); for (int i = 0, end = products.size(); i < end; ++i) { const QJsonObject obj = products.at(i).toObject(); const QString handle = obj.value("handle").toString(); - bool foundItem = Utils::findOrDefault(m_items, [handle](const Core::ListItem *it) { + bool foundItem = Utils::findOrDefault(presentItems, [handle](const Core::ListItem *it) { return static_cast<const ProductItem *>(it)->handle == handle; }); if (foundItem) @@ -164,9 +292,8 @@ void ProductListModel::onFetchSingleCollectionFinished(QNetworkReply *reply) } if (!productsForCollection.isEmpty()) { - beginInsertRows(QModelIndex(), m_items.size(), m_items.size() + productsForCollection.size()); - m_items.append(productsForCollection); - endInsertRows(); + Section section{m_collectionTitles.value(collectionHandle), priority(collectionHandle)}; + addNewSection(section, productsForCollection); } } else { // bad.. but we still might be able to fetch another collection @@ -175,11 +302,11 @@ void ProductListModel::onFetchSingleCollectionFinished(QNetworkReply *reply) if (!m_pendingCollections.isEmpty()) // more collections? go ahead.. fetchCollectionsContents(); - else if (m_items.isEmpty()) + else if (m_productModels.isEmpty()) emit errorOccurred(0, "Failed to fetch any collection."); } -void ProductListModel::fetchCollectionsContents() +void SectionedProducts::fetchCollectionsContents() { QTC_ASSERT(!m_pendingCollections.isEmpty(), return); const QString collection = m_pendingCollections.dequeue(); @@ -190,14 +317,34 @@ void ProductListModel::fetchCollectionsContents() this, [this, reply]() { onFetchSingleCollectionFinished(reply); }); } -void ProductListModel::queueImageForDownload(const QString &url) +void SectionedProducts::queueImageForDownload(const QString &url) { m_pendingImages.insert(url); if (!m_isDownloadingImage) fetchNextImage(); } -void ProductListModel::fetchNextImage() +void SectionedProducts::setColumnCount(int columns) +{ + if (columns < 1) + columns = 1; + m_columnCount = columns; + for (ProductGridView *view : m_gridViews.values()) { + view->setColumnCount(columns); + view->setFixedSize(view->viewportSizeHint()); + } + m_allProductsView->setColumnCount(columns); +} + +void SectionedProducts::setSearchString(const QString &searchString) +{ + int view = searchString.isEmpty() ? 0 // sectioned view + : 1; // search view + setCurrentIndex(view); + m_filteredAllProductsModel->setSearchString(searchString); +} + +void SectionedProducts::fetchNextImage() { if (m_pendingImages.isEmpty()) { m_isDownloadingImage = false; @@ -208,8 +355,10 @@ void ProductListModel::fetchNextImage() const QString nextUrl = *it; m_pendingImages.erase(it); - if (QPixmapCache::find(nextUrl, nullptr)) { // this image is already cached - updateModelIndexesForUrl(nextUrl); // it might have been added while downloading + if (QPixmapCache::find(nextUrl, nullptr)) { + // this image is already cached it might have been added while downloading + for (ProductListModel *model : m_productModels.values()) + model->updateModelIndexesForUrl(nextUrl); fetchNextImage(); return; } @@ -220,7 +369,7 @@ void ProductListModel::fetchNextImage() this, [this, reply]() { onImageDownloadFinished(reply); }); } -void ProductListModel::onImageDownloadFinished(QNetworkReply *reply) +void SectionedProducts::onImageDownloadFinished(QNetworkReply *reply) { QTC_ASSERT(reply, return); Utils::ExecuteOnDestruction replyDeleter([reply]() { reply->deleteLater(); }); @@ -232,13 +381,66 @@ void ProductListModel::onImageDownloadFinished(QNetworkReply *reply) const QString url = reply->request().url().toString(); QPixmapCache::insert(url, pixmap.scaled(ProductListModel::defaultImageSize, Qt::KeepAspectRatio, Qt::SmoothTransformation)); - updateModelIndexesForUrl(url); + for (ProductListModel *model : m_productModels.values()) + model->updateModelIndexesForUrl(url); } } // handle error not needed - it's okay'ish to have no images as long as the rest works fetchNextImage(); } +void SectionedProducts::addNewSection(const Section §ion, 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); + Core::GridProxyModel *gridModel = new Core::GridProxyModel; + gridModel->setSourceModel(filteredModel); + auto gridView = new ProductGridView(this); + gridView->setItemDelegate(m_productDelegate); + gridView->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + gridView->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + gridView->setModel(gridModel); + gridModel->setColumnCount(m_columnCount); + + m_productModels.insert(section, productModel); + m_gridViews.insert(section, gridView); + + QFont f = font(); + f.setPixelSize(16); + auto sectionLabel = new QLabel(section.name); + sectionLabel->setFont(f); + 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 = m_gridViews.keys().indexOf(section) * 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); + gridView->setFixedSize(gridView->viewportSizeHint()); + + // add the items also to the all products model to be able to search correctly + auto allProducts = dynamic_cast<ProductListModel *>(m_filteredAllProductsModel->sourceModel()); + allProducts->appendItems(items); + m_allProductsView->setColumnCount(m_columnCount); +} + +void SectionedProducts::onTagClicked(const QString &tag) +{ + setCurrentIndex(1 /* search */); + emit tagClicked(tag); +} + +QList<Core::ListItem *> SectionedProducts::items() +{ + QList<Core::ListItem *> result; + for (const ProductListModel *model : m_productModels.values()) + result.append(model->items()); + return result; +} + void ProductListModel::updateModelIndexesForUrl(const QString &url) { for (int row = 0, end = m_items.size(); row < end; ++row) { diff --git a/src/plugins/marketplace/productlistmodel.h b/src/plugins/marketplace/productlistmodel.h index 032008d6f7..cda47b52da 100644 --- a/src/plugins/marketplace/productlistmodel.h +++ b/src/plugins/marketplace/productlistmodel.h @@ -28,6 +28,7 @@ #include <coreplugin/welcomepagehelper.h> #include <QQueue> +#include <QStackedWidget> QT_BEGIN_NAMESPACE class QNetworkReply; @@ -36,6 +37,9 @@ QT_END_NAMESPACE namespace Marketplace { namespace Internal { +class ProductGridView; +class ProductItemDelegate; + class ProductItem : public Core::ListItem { public: @@ -44,31 +48,72 @@ public: class ProductListModel : public Core::ListModel { - Q_OBJECT public: explicit ProductListModel(QObject *parent); + void appendItems(const QList<Core::ListItem *> &items); + const QList<Core::ListItem *> items() const; + void updateModelIndexesForUrl(const QString &url); + +protected: + QPixmap fetchPixmapAndUpdatePixmapCache(const QString &url) const override; +}; + +struct Section +{ + QString name; + int priority; +}; + +inline 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; +} + +inline bool operator==(const Section &lhs, const Section &rhs) +{ + return lhs.priority == rhs.priority && lhs.name == rhs.name; +} + +class SectionedProducts : public QStackedWidget +{ + Q_OBJECT +public: + explicit SectionedProducts(QWidget *parent); + ~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); void toggleProgressIndicator(bool show); - -protected: - QPixmap fetchPixmapAndUpdatePixmapCache(const QString &url) const override; + void tagClicked(const QString &tag); private: void onFetchCollectionsFinished(QNetworkReply *reply); void onFetchSingleCollectionFinished(QNetworkReply *reply); void fetchCollectionsContents(); - void queueImageForDownload(const QString &url); void fetchNextImage(); void onImageDownloadFinished(QNetworkReply *reply); - void updateModelIndexesForUrl(const QString &url); + void addNewSection(const Section §ion, const QList<Core::ListItem *> &items); + void onTagClicked(const QString &tag); + + QList<Core::ListItem *> items(); QQueue<QString> m_pendingCollections; QSet<QString> m_pendingImages; + QMap<QString, QString> m_collectionTitles; + QMap<Section, ProductListModel *> m_productModels; + QMap<Section, ProductGridView *> m_gridViews; + ProductGridView *m_allProductsView = nullptr; + Core::ListModelFilter *m_filteredAllProductsModel = nullptr; + ProductItemDelegate *m_productDelegate = nullptr; bool m_isDownloadingImage = false; + int m_columnCount = 1; }; } // namespace Internal diff --git a/src/plugins/marketplace/qtmarketplacewelcomepage.cpp b/src/plugins/marketplace/qtmarketplacewelcomepage.cpp index 01f13f7701..beea69f670 100644 --- a/src/plugins/marketplace/qtmarketplacewelcomepage.cpp +++ b/src/plugins/marketplace/qtmarketplacewelcomepage.cpp @@ -60,27 +60,12 @@ Core::Id QtMarketplaceWelcomePage::id() const return "Marketplace"; } -class ProductItemDelegate : public Core::ListItemDelegate -{ -public: - void clickAction(const Core::ListItem *item) const override - { - QTC_ASSERT(item, return); - auto productItem = static_cast<const ProductItem *>(item); - const QUrl url(QString("https://marketplace.qt.io/products/").append(productItem->handle)); - QDesktopServices::openUrl(url); - } -}; - class QtMarketplacePageWidget : public QWidget { public: QtMarketplacePageWidget() - : m_productModel(new ProductListModel(this)) { const int sideMargin = 27; - auto filteredModel = new Core::ListModelFilter(m_productModel, this); - auto searchBox = new Core::SearchBox(this); m_searcher = searchBox->m_lineEdit; m_searcher->setPlaceholderText(QtMarketplaceWelcomePage::tr("Search in Marketplace...")); @@ -96,20 +81,15 @@ public: m_errorLabel->setVisible(false); vbox->addWidget(m_errorLabel); - m_gridModel.setSourceModel(filteredModel); - - auto gridView = new Core::GridView(this); - gridView->setModel(&m_gridModel); - gridView->setItemDelegate(&m_productDelegate); - vbox->addWidget(gridView); - + m_sectionedProducts = new SectionedProducts(this); auto progressIndicator = new Utils::ProgressIndicator(ProgressIndicatorSize::Large, this); - progressIndicator->attachToWidget(gridView); + progressIndicator->attachToWidget(m_sectionedProducts); progressIndicator->hide(); + vbox->addWidget(m_sectionedProducts); - connect(m_productModel, &ProductListModel::toggleProgressIndicator, + connect(m_sectionedProducts, &SectionedProducts::toggleProgressIndicator, progressIndicator, &Utils::ProgressIndicator::setVisible); - connect(m_productModel, &ProductListModel::errorOccurred, + connect(m_sectionedProducts, &SectionedProducts::errorOccurred, [this, progressIndicator, searchBox](int, const QString &message) { progressIndicator->hide(); progressIndicator->deleteLater(); @@ -128,17 +108,18 @@ public: connect(m_errorLabel, &QLabel::linkActivated, this, []() { QDesktopServices::openUrl(QUrl("https://marketplace.qt.io")); }); }); - connect(&m_productDelegate, &ProductItemDelegate::tagClicked, - this, &QtMarketplacePageWidget::onTagClicked); + connect(m_searcher, &QLineEdit::textChanged, - filteredModel, &Core::ListModelFilter::setSearchString); + m_sectionedProducts, &SectionedProducts::setSearchString); + connect(m_sectionedProducts, &SectionedProducts::tagClicked, + this, &QtMarketplacePageWidget::onTagClicked); } void showEvent(QShowEvent *event) override { if (!m_initialized) { m_initialized = true; - m_productModel->updateCollections(); + m_sectionedProducts->updateCollections(); } QWidget::showEvent(event); } @@ -146,7 +127,7 @@ public: void resizeEvent(QResizeEvent *ev) final { QWidget::resizeEvent(ev); - m_gridModel.setColumnCount(bestColumnCount()); + m_sectionedProducts->setColumnCount(bestColumnCount()); } int bestColumnCount() const @@ -162,11 +143,9 @@ public: } private: - ProductItemDelegate m_productDelegate; - ProductListModel *m_productModel = nullptr; + SectionedProducts *m_sectionedProducts = nullptr; QLabel *m_errorLabel = nullptr; QLineEdit *m_searcher = nullptr; - Core::GridProxyModel m_gridModel; bool m_initialized = false; }; |