diff options
Diffstat (limited to 'src/plugins/marketplace/productlistmodel.cpp')
-rw-r--r-- | src/plugins/marketplace/productlistmodel.cpp | 245 |
1 files changed, 225 insertions, 20 deletions
diff --git a/src/plugins/marketplace/productlistmodel.cpp b/src/plugins/marketplace/productlistmodel.cpp index cded10ee37..0e240cc243 100644 --- a/src/plugins/marketplace/productlistmodel.cpp +++ b/src/plugins/marketplace/productlistmodel.cpp @@ -32,20 +32,100 @@ #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> +#include <QVBoxLayout> 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; + + auto gridProxyModel = static_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 +151,53 @@ 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_gridModel(new Core::GridProxyModel) + , 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); + + m_gridModel->setSourceModel(m_filteredAllProductsModel); + m_allProductsView->setItemDelegate(m_productDelegate); + m_allProductsView->setModel(m_gridModel); + addWidget(m_allProductsView); + + connect(m_productDelegate, &ProductItemDelegate::tagClicked, + this, &SectionedProducts::onTagClicked); +} + +SectionedProducts::~SectionedProducts() { + qDeleteAll(m_gridViews.values()); + delete m_productDelegate; + delete m_gridModel; } -void ProductListModel::updateCollections() +void SectionedProducts::updateCollections() { emit toggleProgressIndicator(true); QNetworkReply *reply = Utils::NetworkAccessManager::instance()->get(constructRequest({})); @@ -86,11 +207,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 +228,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 +257,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 +295,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 +305,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 +320,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 +358,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 +372,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 +384,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 = static_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) { |