forked from qt-creator/qt-creator
Marketplace: Use sections to display products
Fixes: QTCREATORBUG-23808 Change-Id: I2f69697c6ab2133ccf4567bf8f5185bac34a86c7 Reviewed-by: David Schulz <david.schulz@qt.io>
This commit is contained in:
@@ -32,20 +32,98 @@
|
|||||||
#include <utils/networkaccessmanager.h>
|
#include <utils/networkaccessmanager.h>
|
||||||
#include <utils/qtcassert.h>
|
#include <utils/qtcassert.h>
|
||||||
|
|
||||||
|
#include <QDesktopServices>
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
#include <QJsonDocument>
|
#include <QJsonDocument>
|
||||||
#include <QJsonObject>
|
#include <QJsonObject>
|
||||||
|
#include <QLabel>
|
||||||
#include <QNetworkAccessManager>
|
#include <QNetworkAccessManager>
|
||||||
#include <QNetworkReply>
|
#include <QNetworkReply>
|
||||||
#include <QNetworkRequest>
|
#include <QNetworkRequest>
|
||||||
#include <QPixmapCache>
|
#include <QPixmapCache>
|
||||||
#include <QRegularExpression>
|
#include <QRegularExpression>
|
||||||
|
#include <QScrollArea>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
#include <QUrl>
|
#include <QUrl>
|
||||||
|
|
||||||
namespace Marketplace {
|
namespace Marketplace {
|
||||||
namespace Internal {
|
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)
|
static const QNetworkRequest constructRequest(const QString &collection)
|
||||||
{
|
{
|
||||||
QString url("https://marketplace.qt.io");
|
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;
|
return (plainText.length() > 157) ? plainText.left(157).append("...") : plainText;
|
||||||
}
|
}
|
||||||
|
|
||||||
ProductListModel::ProductListModel(QObject *parent)
|
static int priority(const QString &collection)
|
||||||
: Core::ListModel(parent)
|
|
||||||
{
|
{
|
||||||
|
if (collection == "featured")
|
||||||
|
return 10;
|
||||||
|
if (collection == "from-qt-partners")
|
||||||
|
return 20;
|
||||||
|
return 50;
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductListModel::updateCollections()
|
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 SectionedProducts::updateCollections()
|
||||||
{
|
{
|
||||||
emit toggleProgressIndicator(true);
|
emit toggleProgressIndicator(true);
|
||||||
QNetworkReply *reply = Utils::NetworkAccessManager::instance()->get(constructRequest({}));
|
QNetworkReply *reply = Utils::NetworkAccessManager::instance()->get(constructRequest({}));
|
||||||
@@ -86,11 +204,12 @@ void ProductListModel::updateCollections()
|
|||||||
|
|
||||||
QPixmap ProductListModel::fetchPixmapAndUpdatePixmapCache(const QString &url) const
|
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();
|
return QPixmap();
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductListModel::onFetchCollectionsFinished(QNetworkReply *reply)
|
void SectionedProducts::onFetchCollectionsFinished(QNetworkReply *reply)
|
||||||
{
|
{
|
||||||
QTC_ASSERT(reply, return);
|
QTC_ASSERT(reply, return);
|
||||||
Utils::ExecuteOnDestruction replyDeleter([reply]() { reply->deleteLater(); });
|
Utils::ExecuteOnDestruction replyDeleter([reply]() { reply->deleteLater(); });
|
||||||
@@ -106,21 +225,23 @@ void ProductListModel::onFetchCollectionsFinished(QNetworkReply *reply)
|
|||||||
const auto handle = obj.value("handle").toString();
|
const auto handle = obj.value("handle").toString();
|
||||||
const int productsCount = obj.value("products_count").toInt();
|
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);
|
m_pendingCollections.append(handle);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!m_pendingCollections.isEmpty())
|
if (!m_pendingCollections.isEmpty())
|
||||||
fetchCollectionsContents();
|
fetchCollectionsContents();
|
||||||
} else {
|
} else {
|
||||||
QVariant status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
QVariant status = reply->attribute(QNetworkRequest::HttpStatusCodeAttribute);
|
||||||
if (status.isValid() && status.toInt() == 430)
|
if (status.isValid() && status.toInt() == 430)
|
||||||
QTimer::singleShot(30000, this, &ProductListModel::updateCollections);
|
QTimer::singleShot(30000, this, &SectionedProducts::updateCollections);
|
||||||
else
|
else
|
||||||
emit errorOccurred(reply->error(), reply->errorString());
|
emit errorOccurred(reply->error(), reply->errorString());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductListModel::onFetchSingleCollectionFinished(QNetworkReply *reply)
|
void SectionedProducts::onFetchSingleCollectionFinished(QNetworkReply *reply)
|
||||||
{
|
{
|
||||||
emit toggleProgressIndicator(false);
|
emit toggleProgressIndicator(false);
|
||||||
|
|
||||||
@@ -133,12 +254,19 @@ void ProductListModel::onFetchSingleCollectionFinished(QNetworkReply *reply)
|
|||||||
if (doc.isNull())
|
if (doc.isNull())
|
||||||
return;
|
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();
|
const QJsonArray products = doc.object().value("products").toArray();
|
||||||
for (int i = 0, end = products.size(); i < end; ++i) {
|
for (int i = 0, end = products.size(); i < end; ++i) {
|
||||||
const QJsonObject obj = products.at(i).toObject();
|
const QJsonObject obj = products.at(i).toObject();
|
||||||
const QString handle = obj.value("handle").toString();
|
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;
|
return static_cast<const ProductItem *>(it)->handle == handle;
|
||||||
});
|
});
|
||||||
if (foundItem)
|
if (foundItem)
|
||||||
@@ -164,9 +292,8 @@ void ProductListModel::onFetchSingleCollectionFinished(QNetworkReply *reply)
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!productsForCollection.isEmpty()) {
|
if (!productsForCollection.isEmpty()) {
|
||||||
beginInsertRows(QModelIndex(), m_items.size(), m_items.size() + productsForCollection.size());
|
Section section{m_collectionTitles.value(collectionHandle), priority(collectionHandle)};
|
||||||
m_items.append(productsForCollection);
|
addNewSection(section, productsForCollection);
|
||||||
endInsertRows();
|
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
// bad.. but we still might be able to fetch another collection
|
// 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..
|
if (!m_pendingCollections.isEmpty()) // more collections? go ahead..
|
||||||
fetchCollectionsContents();
|
fetchCollectionsContents();
|
||||||
else if (m_items.isEmpty())
|
else if (m_productModels.isEmpty())
|
||||||
emit errorOccurred(0, "Failed to fetch any collection.");
|
emit errorOccurred(0, "Failed to fetch any collection.");
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductListModel::fetchCollectionsContents()
|
void SectionedProducts::fetchCollectionsContents()
|
||||||
{
|
{
|
||||||
QTC_ASSERT(!m_pendingCollections.isEmpty(), return);
|
QTC_ASSERT(!m_pendingCollections.isEmpty(), return);
|
||||||
const QString collection = m_pendingCollections.dequeue();
|
const QString collection = m_pendingCollections.dequeue();
|
||||||
@@ -190,14 +317,34 @@ void ProductListModel::fetchCollectionsContents()
|
|||||||
this, [this, reply]() { onFetchSingleCollectionFinished(reply); });
|
this, [this, reply]() { onFetchSingleCollectionFinished(reply); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductListModel::queueImageForDownload(const QString &url)
|
void SectionedProducts::queueImageForDownload(const QString &url)
|
||||||
{
|
{
|
||||||
m_pendingImages.insert(url);
|
m_pendingImages.insert(url);
|
||||||
if (!m_isDownloadingImage)
|
if (!m_isDownloadingImage)
|
||||||
fetchNextImage();
|
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()) {
|
if (m_pendingImages.isEmpty()) {
|
||||||
m_isDownloadingImage = false;
|
m_isDownloadingImage = false;
|
||||||
@@ -208,8 +355,10 @@ void ProductListModel::fetchNextImage()
|
|||||||
const QString nextUrl = *it;
|
const QString nextUrl = *it;
|
||||||
m_pendingImages.erase(it);
|
m_pendingImages.erase(it);
|
||||||
|
|
||||||
if (QPixmapCache::find(nextUrl, nullptr)) { // this image is already cached
|
if (QPixmapCache::find(nextUrl, nullptr)) {
|
||||||
updateModelIndexesForUrl(nextUrl); // it might have been added while downloading
|
// this image is already cached it might have been added while downloading
|
||||||
|
for (ProductListModel *model : m_productModels.values())
|
||||||
|
model->updateModelIndexesForUrl(nextUrl);
|
||||||
fetchNextImage();
|
fetchNextImage();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -220,7 +369,7 @@ void ProductListModel::fetchNextImage()
|
|||||||
this, [this, reply]() { onImageDownloadFinished(reply); });
|
this, [this, reply]() { onImageDownloadFinished(reply); });
|
||||||
}
|
}
|
||||||
|
|
||||||
void ProductListModel::onImageDownloadFinished(QNetworkReply *reply)
|
void SectionedProducts::onImageDownloadFinished(QNetworkReply *reply)
|
||||||
{
|
{
|
||||||
QTC_ASSERT(reply, return);
|
QTC_ASSERT(reply, return);
|
||||||
Utils::ExecuteOnDestruction replyDeleter([reply]() { reply->deleteLater(); });
|
Utils::ExecuteOnDestruction replyDeleter([reply]() { reply->deleteLater(); });
|
||||||
@@ -232,13 +381,66 @@ void ProductListModel::onImageDownloadFinished(QNetworkReply *reply)
|
|||||||
const QString url = reply->request().url().toString();
|
const QString url = reply->request().url().toString();
|
||||||
QPixmapCache::insert(url, pixmap.scaled(ProductListModel::defaultImageSize,
|
QPixmapCache::insert(url, pixmap.scaled(ProductListModel::defaultImageSize,
|
||||||
Qt::KeepAspectRatio, Qt::SmoothTransformation));
|
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
|
} // handle error not needed - it's okay'ish to have no images as long as the rest works
|
||||||
|
|
||||||
fetchNextImage();
|
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)
|
void ProductListModel::updateModelIndexesForUrl(const QString &url)
|
||||||
{
|
{
|
||||||
for (int row = 0, end = m_items.size(); row < end; ++row) {
|
for (int row = 0, end = m_items.size(); row < end; ++row) {
|
||||||
|
|||||||
@@ -28,6 +28,7 @@
|
|||||||
#include <coreplugin/welcomepagehelper.h>
|
#include <coreplugin/welcomepagehelper.h>
|
||||||
|
|
||||||
#include <QQueue>
|
#include <QQueue>
|
||||||
|
#include <QStackedWidget>
|
||||||
|
|
||||||
QT_BEGIN_NAMESPACE
|
QT_BEGIN_NAMESPACE
|
||||||
class QNetworkReply;
|
class QNetworkReply;
|
||||||
@@ -36,6 +37,9 @@ QT_END_NAMESPACE
|
|||||||
namespace Marketplace {
|
namespace Marketplace {
|
||||||
namespace Internal {
|
namespace Internal {
|
||||||
|
|
||||||
|
class ProductGridView;
|
||||||
|
class ProductItemDelegate;
|
||||||
|
|
||||||
class ProductItem : public Core::ListItem
|
class ProductItem : public Core::ListItem
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
@@ -44,31 +48,72 @@ public:
|
|||||||
|
|
||||||
class ProductListModel : public Core::ListModel
|
class ProductListModel : public Core::ListModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
|
||||||
public:
|
public:
|
||||||
explicit ProductListModel(QObject *parent);
|
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 updateCollections();
|
||||||
|
void queueImageForDownload(const QString &url);
|
||||||
|
void setColumnCount(int columns);
|
||||||
|
void setSearchString(const QString &searchString);
|
||||||
|
|
||||||
signals:
|
signals:
|
||||||
void errorOccurred(int errorCode, const QString &errorString);
|
void errorOccurred(int errorCode, const QString &errorString);
|
||||||
void toggleProgressIndicator(bool show);
|
void toggleProgressIndicator(bool show);
|
||||||
|
void tagClicked(const QString &tag);
|
||||||
protected:
|
|
||||||
QPixmap fetchPixmapAndUpdatePixmapCache(const QString &url) const override;
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
void onFetchCollectionsFinished(QNetworkReply *reply);
|
void onFetchCollectionsFinished(QNetworkReply *reply);
|
||||||
void onFetchSingleCollectionFinished(QNetworkReply *reply);
|
void onFetchSingleCollectionFinished(QNetworkReply *reply);
|
||||||
void fetchCollectionsContents();
|
void fetchCollectionsContents();
|
||||||
|
|
||||||
void queueImageForDownload(const QString &url);
|
|
||||||
void fetchNextImage();
|
void fetchNextImage();
|
||||||
void onImageDownloadFinished(QNetworkReply *reply);
|
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;
|
QQueue<QString> m_pendingCollections;
|
||||||
QSet<QString> m_pendingImages;
|
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;
|
bool m_isDownloadingImage = false;
|
||||||
|
int m_columnCount = 1;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace Internal
|
} // namespace Internal
|
||||||
|
|||||||
@@ -60,27 +60,12 @@ Core::Id QtMarketplaceWelcomePage::id() const
|
|||||||
return "Marketplace";
|
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
|
class QtMarketplacePageWidget : public QWidget
|
||||||
{
|
{
|
||||||
public:
|
public:
|
||||||
QtMarketplacePageWidget()
|
QtMarketplacePageWidget()
|
||||||
: m_productModel(new ProductListModel(this))
|
|
||||||
{
|
{
|
||||||
const int sideMargin = 27;
|
const int sideMargin = 27;
|
||||||
auto filteredModel = new Core::ListModelFilter(m_productModel, this);
|
|
||||||
|
|
||||||
auto searchBox = new Core::SearchBox(this);
|
auto searchBox = new Core::SearchBox(this);
|
||||||
m_searcher = searchBox->m_lineEdit;
|
m_searcher = searchBox->m_lineEdit;
|
||||||
m_searcher->setPlaceholderText(QtMarketplaceWelcomePage::tr("Search in Marketplace..."));
|
m_searcher->setPlaceholderText(QtMarketplaceWelcomePage::tr("Search in Marketplace..."));
|
||||||
@@ -96,20 +81,15 @@ public:
|
|||||||
m_errorLabel->setVisible(false);
|
m_errorLabel->setVisible(false);
|
||||||
vbox->addWidget(m_errorLabel);
|
vbox->addWidget(m_errorLabel);
|
||||||
|
|
||||||
m_gridModel.setSourceModel(filteredModel);
|
m_sectionedProducts = new SectionedProducts(this);
|
||||||
|
|
||||||
auto gridView = new Core::GridView(this);
|
|
||||||
gridView->setModel(&m_gridModel);
|
|
||||||
gridView->setItemDelegate(&m_productDelegate);
|
|
||||||
vbox->addWidget(gridView);
|
|
||||||
|
|
||||||
auto progressIndicator = new Utils::ProgressIndicator(ProgressIndicatorSize::Large, this);
|
auto progressIndicator = new Utils::ProgressIndicator(ProgressIndicatorSize::Large, this);
|
||||||
progressIndicator->attachToWidget(gridView);
|
progressIndicator->attachToWidget(m_sectionedProducts);
|
||||||
progressIndicator->hide();
|
progressIndicator->hide();
|
||||||
|
vbox->addWidget(m_sectionedProducts);
|
||||||
|
|
||||||
connect(m_productModel, &ProductListModel::toggleProgressIndicator,
|
connect(m_sectionedProducts, &SectionedProducts::toggleProgressIndicator,
|
||||||
progressIndicator, &Utils::ProgressIndicator::setVisible);
|
progressIndicator, &Utils::ProgressIndicator::setVisible);
|
||||||
connect(m_productModel, &ProductListModel::errorOccurred,
|
connect(m_sectionedProducts, &SectionedProducts::errorOccurred,
|
||||||
[this, progressIndicator, searchBox](int, const QString &message) {
|
[this, progressIndicator, searchBox](int, const QString &message) {
|
||||||
progressIndicator->hide();
|
progressIndicator->hide();
|
||||||
progressIndicator->deleteLater();
|
progressIndicator->deleteLater();
|
||||||
@@ -128,17 +108,18 @@ public:
|
|||||||
connect(m_errorLabel, &QLabel::linkActivated,
|
connect(m_errorLabel, &QLabel::linkActivated,
|
||||||
this, []() { QDesktopServices::openUrl(QUrl("https://marketplace.qt.io")); });
|
this, []() { QDesktopServices::openUrl(QUrl("https://marketplace.qt.io")); });
|
||||||
});
|
});
|
||||||
connect(&m_productDelegate, &ProductItemDelegate::tagClicked,
|
|
||||||
this, &QtMarketplacePageWidget::onTagClicked);
|
|
||||||
connect(m_searcher, &QLineEdit::textChanged,
|
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
|
void showEvent(QShowEvent *event) override
|
||||||
{
|
{
|
||||||
if (!m_initialized) {
|
if (!m_initialized) {
|
||||||
m_initialized = true;
|
m_initialized = true;
|
||||||
m_productModel->updateCollections();
|
m_sectionedProducts->updateCollections();
|
||||||
}
|
}
|
||||||
QWidget::showEvent(event);
|
QWidget::showEvent(event);
|
||||||
}
|
}
|
||||||
@@ -146,7 +127,7 @@ public:
|
|||||||
void resizeEvent(QResizeEvent *ev) final
|
void resizeEvent(QResizeEvent *ev) final
|
||||||
{
|
{
|
||||||
QWidget::resizeEvent(ev);
|
QWidget::resizeEvent(ev);
|
||||||
m_gridModel.setColumnCount(bestColumnCount());
|
m_sectionedProducts->setColumnCount(bestColumnCount());
|
||||||
}
|
}
|
||||||
|
|
||||||
int bestColumnCount() const
|
int bestColumnCount() const
|
||||||
@@ -162,11 +143,9 @@ public:
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
ProductItemDelegate m_productDelegate;
|
SectionedProducts *m_sectionedProducts = nullptr;
|
||||||
ProductListModel *m_productModel = nullptr;
|
|
||||||
QLabel *m_errorLabel = nullptr;
|
QLabel *m_errorLabel = nullptr;
|
||||||
QLineEdit *m_searcher = nullptr;
|
QLineEdit *m_searcher = nullptr;
|
||||||
Core::GridProxyModel m_gridModel;
|
|
||||||
bool m_initialized = false;
|
bool m_initialized = false;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user