Add Qt Marketplace plugin

Provides a simple integration into the welcome page.

Task-number: QTCREATORBUG-23452
Change-Id: I3e615dcd6dfd9e401159ea6d30b48737edb1e1f9
Reviewed-by: David Schulz <david.schulz@qt.io>
Reviewed-by: Christian Stenger <christian.stenger@qt.io>
This commit is contained in:
Christian Stenger
2020-01-13 13:35:00 +01:00
parent d08c0f31c4
commit bf7b16d9ca
13 changed files with 671 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ add_subdirectory(texteditor)
add_subdirectory(serialterminal) add_subdirectory(serialterminal)
add_subdirectory(helloworld) add_subdirectory(helloworld)
add_subdirectory(imageviewer) add_subdirectory(imageviewer)
add_subdirectory(marketplace)
add_subdirectory(updateinfo) add_subdirectory(updateinfo)
add_subdirectory(welcome) add_subdirectory(welcome)

View File

@@ -0,0 +1,7 @@
add_qtc_plugin(Marketplace
PLUGIN_DEPENDS Core
SOURCES
marketplaceplugin.h
productlistmodel.cpp productlistmodel.h
qtmarketplacewelcomepage.cpp qtmarketplacewelcomepage.h
)

View File

@@ -0,0 +1,18 @@
{
\"Name\" : \"Marketplace\",
\"Version\" : \"$$QTCREATOR_VERSION\",
\"CompatVersion\" : \"$$QTCREATOR_COMPAT_VERSION\",
\"Vendor\" : \"The Qt Company Ltd\",
\"Copyright\" : \"(C) $$QTCREATOR_COPYRIGHT_YEAR The Qt Company Ltd\",
\"License\" : [ \"Commercial Usage\",
\"\",
\"Licensees holding valid Qt Commercial licenses may use this plugin in accordance with the Qt Commercial License Agreement provided with the Software or, alternatively, in accordance with the terms contained in a written agreement between you and The Qt Company.\",
\"\",
\"GNU General Public License Usage\",
\"\",
\"Alternatively, this file may be used under the terms of the GNU General Public License version 3 as published by the Free Software Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT included in the packaging of this file. Please review the following information to ensure the GNU General Public License requirements will be met: https://www.gnu.org/licenses/gpl-3.0.html.\"
],
\"Description\" : \"Qt Marketplace plugin.\",
\"Url\" : \"http://www.qt.io\",
$$dependencyList
}

View File

@@ -0,0 +1,16 @@
TARGET = Marketplace
TEMPLATE = lib
QT += network
include(../../qtcreatorplugin.pri)
DEFINES += MARKETPLACE_LIBRARY
SOURCES += \
productlistmodel.cpp \
qtmarketplacewelcomepage.cpp
HEADERS += \
marketplaceplugin.h \
productlistmodel.h \
qtmarketplacewelcomepage.h

View File

@@ -0,0 +1,17 @@
import qbs
QtcPlugin {
name: "Marketplace"
Depends { name: "Core" }
Depends { name: "Utils" }
Depends { name: "Qt.widgets" }
Depends { name: "Qt.network" }
files: [
"marketplaceplugin.h",
"productlistmodel.cpp", "productlistmodel.h",
"qtmarketplacewelcomepage.cpp", "qtmarketplacewelcomepage.h",
]
}

View File

@@ -0,0 +1,7 @@
QTC_PLUGIN_NAME = Marketplace
QTC_PLUGIN_DEPENDS += \
coreplugin
QTC_LIB_DEPENDS += \
utils

View File

@@ -0,0 +1,49 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#pragma once
#include <extensionsystem/iplugin.h>
#include "qtmarketplacewelcomepage.h"
namespace Marketplace {
class MarketplacePlugin : public ExtensionSystem::IPlugin
{
Q_OBJECT
Q_PLUGIN_METADATA(IID "org.qt-project.Qt.QtCreatorPlugin" FILE "Marketplace.json")
public:
MarketplacePlugin() = default;
bool initialize(const QStringList &, QString *) final { return true; }
void extensionsInitialized() final {}
private:
Internal::QtMarketplaceWelcomePage welcomePage;
};
} // namespace Marketplace

View File

@@ -0,0 +1,250 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "productlistmodel.h"
#include "marketplaceplugin.h"
#include <utils/algorithm.h>
#include <utils/executeondestruction.h>
#include <utils/networkaccessmanager.h>
#include <utils/qtcassert.h>
#include <QJsonArray>
#include <QJsonDocument>
#include <QNetworkAccessManager>
#include <QNetworkReply>
#include <QNetworkRequest>
#include <QPixmapCache>
#include <QRegularExpression>
#include <QTimer>
#include <QUrl>
namespace Marketplace {
namespace Internal {
static const QNetworkRequest constructRequest(const QString &collection)
{
QString url("https://marketplace.qt.io");
if (collection.isEmpty())
url.append("/collections.json");
else
url.append("/collections/").append(collection).append("/products.json");
return QNetworkRequest(url);
}
static const QString plainTextFromHtml(const QString &original)
{
QString plainText(original);
QRegularExpression breakReturn("<\\s*br/?\\s*>", QRegularExpression::CaseInsensitiveOption);
plainText.replace(breakReturn, "\n"); // "translate" <br/> into newline
plainText.remove(QRegularExpression("<[^>]*>")); // remove all tags
plainText = plainText.trimmed();
plainText.replace(QRegularExpression("\n{3,}"), "\n\n"); // consolidate some newlines
// FIXME the description text is usually too long and needs to get elided sensibly
return (plainText.length() > 157) ? plainText.left(157).append("...") : plainText;
}
ProductListModel::ProductListModel(QObject *parent)
: Core::ListModel(parent)
{
}
void ProductListModel::updateCollections()
{
emit toggleProgressIndicator(true);
QNetworkReply *reply = Utils::NetworkAccessManager::instance()->get(constructRequest({}));
connect(reply, &QNetworkReply::finished,
this, [this, reply]() { onFetchCollectionsFinished(reply); });
}
QPixmap ProductListModel::fetchPixmapAndUpdatePixmapCache(const QString &url) const
{
const_cast<ProductListModel *>(this)->queueImageForDownload(url);
return QPixmap();
}
void ProductListModel::onFetchCollectionsFinished(QNetworkReply *reply)
{
QTC_ASSERT(reply, return);
Utils::ExecuteOnDestruction replyDeleter([reply]() { reply->deleteLater(); });
if (reply->error() == QNetworkReply::NoError) {
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
if (doc.isNull())
return;
const QJsonArray collections = doc.object().value("collections").toArray();
for (int i = 0, end = collections.size(); i < end; ++i) {
const QJsonObject obj = collections.at(i).toObject();
const auto handle = obj.value("handle").toString();
const int productsCount = obj.value("products_count").toInt();
if (productsCount > 0 && handle != "all-products")
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);
else
emit errorOccurred(reply->error(), reply->errorString());
}
}
void ProductListModel::onFetchSingleCollectionFinished(QNetworkReply *reply)
{
emit toggleProgressIndicator(false);
QTC_ASSERT(reply, return);
Utils::ExecuteOnDestruction replyDeleter([reply]() { reply->deleteLater(); });
QList<Core::ListItem *> productsForCollection;
if (reply->error() == QNetworkReply::NoError) {
const QJsonDocument doc = QJsonDocument::fromJson(reply->readAll());
if (doc.isNull())
return;
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) {
return static_cast<const ProductItem *>(it)->handle == handle;
});
if (foundItem)
continue;
ProductItem *product = new ProductItem;
product->name = obj.value("title").toString();
product->description = plainTextFromHtml(obj.value("body_html").toString());
product->handle = handle;
for (auto val : obj.value("tags").toArray())
product->tags.append(val.toString());
const auto images = obj.value("images").toArray();
if (!images.isEmpty()) {
auto imgObj = images.first().toObject();
const QJsonValue imageSrc = imgObj.value("src");
if (imageSrc.isString())
product->imageUrl = imageSrc.toString();
}
productsForCollection.append(product);
}
if (!productsForCollection.isEmpty()) {
beginInsertRows(QModelIndex(), m_items.size(), m_items.size() + productsForCollection.size());
m_items.append(productsForCollection);
endInsertRows();
}
} else {
// bad.. but we still might be able to fetch another collection
qWarning() << "Failed to fetch collection:" << reply->errorString() << reply->error();
}
if (!m_pendingCollections.isEmpty()) // more collections? go ahead..
fetchCollectionsContents();
else if (m_items.isEmpty())
emit errorOccurred(0, "Failed to fetch any collection.");
}
void ProductListModel::fetchCollectionsContents()
{
QTC_ASSERT(!m_pendingCollections.isEmpty(), return);
const QString collection = m_pendingCollections.dequeue();
QNetworkReply *reply
= Utils::NetworkAccessManager::instance()->get(constructRequest(collection));
connect(reply, &QNetworkReply::finished,
this, [this, reply]() { onFetchSingleCollectionFinished(reply); });
}
void ProductListModel::queueImageForDownload(const QString &url)
{
m_pendingImages.insert(url);
if (!m_isDownloadingImage)
fetchNextImage();
}
void ProductListModel::fetchNextImage()
{
if (m_pendingImages.isEmpty()) {
m_isDownloadingImage = false;
return;
}
const auto it = m_pendingImages.begin();
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
fetchNextImage();
return;
}
m_isDownloadingImage = true;
QNetworkReply *reply = Utils::NetworkAccessManager::instance()->get(QNetworkRequest(nextUrl));
connect(reply, &QNetworkReply::finished,
this, [this, reply]() { onImageDownloadFinished(reply); });
}
void ProductListModel::onImageDownloadFinished(QNetworkReply *reply)
{
QTC_ASSERT(reply, return);
Utils::ExecuteOnDestruction replyDeleter([reply]() { reply->deleteLater(); });
if (reply->error() == QNetworkReply::NoError) {
const QByteArray data = reply->readAll();
QPixmap pixmap;
if (pixmap.loadFromData(data)) {
const QString url = reply->request().url().toString();
QPixmapCache::insert(url, pixmap.scaled(ProductListModel::defaultImageSize,
Qt::KeepAspectRatio, Qt::SmoothTransformation));
updateModelIndexesForUrl(url);
}
} // handle error not needed - it's okay'ish to have no images as long as the rest works
fetchNextImage();
}
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

View File

@@ -0,0 +1,77 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#pragma once
#include <coreplugin/welcomepagehelper.h>
#include <QQueue>
QT_BEGIN_NAMESPACE
class QNetworkReply;
QT_END_NAMESPACE
namespace Marketplace {
namespace Internal {
class ProductItem : public Core::ListItem
{
public:
QString handle;
};
class ProductListModel : public Core::ListModel
{
Q_OBJECT
public:
explicit ProductListModel(QObject *parent);
void updateCollections();
signals:
void errorOccurred(int errorCode, const QString &errorString);
void toggleProgressIndicator(bool show);
protected:
QPixmap fetchPixmapAndUpdatePixmapCache(const QString &url) const override;
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);
QQueue<QString> m_pendingCollections;
QSet<QString> m_pendingImages;
bool m_isDownloadingImage = false;
};
} // namespace Internal
} // namespace Marketplace
Q_DECLARE_METATYPE(Marketplace::Internal::ProductItem *)

View File

@@ -0,0 +1,178 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#include "qtmarketplacewelcomepage.h"
#include "productlistmodel.h"
#include <coreplugin/welcomepagehelper.h>
#include <utils/fancylineedit.h>
#include <utils/progressindicator.h>
#include <utils/theme/theme.h>
#include <utils/qtcassert.h>
#include <QDesktopServices>
#include <QLabel>
#include <QLineEdit>
#include <QShowEvent>
#include <QVBoxLayout>
namespace Marketplace {
namespace Internal {
using namespace Utils;
QString QtMarketplaceWelcomePage::title() const
{
return tr("Marketplace");
}
int QtMarketplaceWelcomePage::priority() const
{
return 60;
}
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..."));
auto vbox = new QVBoxLayout(this);
vbox->setContentsMargins(30, sideMargin, 0, 0);
auto hbox = new QHBoxLayout;
hbox->addWidget(searchBox);
hbox->addSpacing(sideMargin);
vbox->addItem(hbox);
m_errorLabel = new QLabel(this);
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);
auto progressIndicator = new Utils::ProgressIndicator(ProgressIndicatorSize::Large, this);
progressIndicator->attachToWidget(gridView);
progressIndicator->hide();
connect(m_productModel, &ProductListModel::toggleProgressIndicator,
progressIndicator, &Utils::ProgressIndicator::setVisible);
connect(m_productModel, &ProductListModel::errorOccurred,
[this, searchBox](int, const QString &message) {
m_errorLabel->setAlignment(Qt::AlignHCenter);
QFont f(m_errorLabel->font());
f.setPixelSize(20);
m_errorLabel->setFont(f);
const QString txt
= QtMarketplaceWelcomePage::tr(
"<p>Could not fetch data from Qt Marketplace.</p><p>Try with your browser "
"instead: <a href='https://marketplace.qt.io'>https://marketplace.qt.io</a>"
"</p><br/><p><small><i>Error: %1</i></small></p>").arg(message);
m_errorLabel->setText(txt);
m_errorLabel->setVisible(true);
searchBox->setVisible(false);
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);
}
void showEvent(QShowEvent *event) override
{
if (!m_initialized) {
m_initialized = true;
m_productModel->updateCollections();
}
QWidget::showEvent(event);
}
void resizeEvent(QResizeEvent *ev) final
{
QWidget::resizeEvent(ev);
m_gridModel.setColumnCount(bestColumnCount());
}
int bestColumnCount() const
{
return qMax(1, width() / (Core::GridProxyModel::GridItemWidth
+ Core::GridProxyModel::GridItemGap));
}
void onTagClicked(const QString &tag)
{
QString text = m_searcher->text();
m_searcher->setText(text + QString("tag:\"%1\" ").arg(tag));
}
private:
ProductItemDelegate m_productDelegate;
ProductListModel *m_productModel = nullptr;
QLabel *m_errorLabel = nullptr;
QLineEdit *m_searcher = nullptr;
Core::GridProxyModel m_gridModel;
bool m_initialized = false;
};
QWidget *QtMarketplaceWelcomePage::createWidget() const
{
return new QtMarketplacePageWidget;
}
} // namespace Internal
} // namespace Marketplace

View File

@@ -0,0 +1,48 @@
/****************************************************************************
**
** Copyright (C) 2019 The Qt Company Ltd.
** Contact: https://www.qt.io/licensing/
**
** This file is part of Qt Creator.
**
** Commercial License Usage
** Licensees holding valid commercial Qt licenses may use this file in
** accordance with the commercial license agreement provided with the
** Software or, alternatively, in accordance with the terms contained in
** a written agreement between you and The Qt Company. For licensing terms
** and conditions see https://www.qt.io/terms-conditions. For further
** information use the contact form at https://www.qt.io/contact-us.
**
** GNU General Public License Usage
** Alternatively, this file may be used under the terms of the GNU
** General Public License version 3 as published by the Free Software
** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
** included in the packaging of this file. Please review the following
** information to ensure the GNU General Public License requirements will
** be met: https://www.gnu.org/licenses/gpl-3.0.html.
**
****************************************************************************/
#pragma once
#include <coreplugin/iwelcomepage.h>
#include <QCoreApplication>
namespace Marketplace {
namespace Internal {
class QtMarketplaceWelcomePage : public Core::IWelcomePage
{
Q_DECLARE_TR_FUNCTIONS(Marketplace::Internal::QtMarketplaceWelcomePage)
public:
QtMarketplaceWelcomePage() = default;
QString title() const final;
int priority() const final;
Core::Id id() const final;
QWidget *createWidget() const final;
};
} // namespace Internal
} // namespace Marketplace

View File

@@ -63,7 +63,8 @@ SUBDIRS = \
qmlpreview \ qmlpreview \
studiowelcome \ studiowelcome \
webassembly \ webassembly \
mcusupport mcusupport \
marketplace
qtHaveModule(serialport) { qtHaveModule(serialport) {
SUBDIRS += serialterminal SUBDIRS += serialterminal

View File

@@ -46,6 +46,7 @@ Project {
"ios/ios.qbs", "ios/ios.qbs",
"languageclient/languageclient.qbs", "languageclient/languageclient.qbs",
"macros/macros.qbs", "macros/macros.qbs",
"marketplace/marketplace.qbs",
"mcusupport/mcusupport.qbs", "mcusupport/mcusupport.qbs",
"mercurial/mercurial.qbs", "mercurial/mercurial.qbs",
"modeleditor/modeleditor.qbs", "modeleditor/modeleditor.qbs",