aboutsummaryrefslogtreecommitdiffstats
path: root/src/plugins/marketplace
diff options
context:
space:
mode:
authorChristian Stenger <christian.stenger@qt.io>2020-05-11 16:45:06 +0200
committerChristian Stenger <christian.stenger@qt.io>2020-05-14 13:16:49 +0000
commitc3946529cab30205607bc0d343c97d4edfe53196 (patch)
treeeda2a26000a948b5c33092930c580e51c39461e1 /src/plugins/marketplace
parentdd9bed93f06d53d119946db6eb5cbf83ee67df32 (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.cpp242
-rw-r--r--src/plugins/marketplace/productlistmodel.h57
-rw-r--r--src/plugins/marketplace/qtmarketplacewelcomepage.cpp45
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 &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);
+ 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 &section, 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;
};