AssetExport: Export assets from renderable nodes

Task-number: QDS-1555
Change-Id: I3d5b60ee8214aeee054587f45045beea020d1f13
Reviewed-by: Leena Miettinen <riitta-leena.miettinen@qt.io>
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Vikas Pachdha
2020-06-19 10:00:47 +02:00
parent 000281fed7
commit 9daf5c130d
9 changed files with 288 additions and 5 deletions

View File

@@ -52,6 +52,7 @@ add_qtc_plugin(assetexporterplugin
assetexporterplugin/componentexporter.h assetexporterplugin/componentexporter.cpp
assetexporterplugin/exportnotification.h assetexporterplugin/exportnotification.cpp
assetexporterplugin/filepathmodel.h assetexporterplugin/filepathmodel.cpp
assetexporterplugin/parsers/assetnodeparser.h assetexporterplugin/parsers/assetnodeparser.cpp
assetexporterplugin/parsers/modelitemnodeparser.h assetexporterplugin/parsers/modelitemnodeparser.cpp
assetexporterplugin/parsers/modelnodeparser.h assetexporterplugin/parsers/modelnodeparser.cpp
assetexporterplugin/parsers/textnodeparser.h assetexporterplugin/parsers/textnodeparser.cpp

View File

@@ -26,15 +26,39 @@
#include "componentexporter.h"
#include "exportnotification.h"
#include "qmlitemnode.h"
#include "qmlobjectnode.h"
#include "utils/qtcassert.h"
#include "utils/runextensions.h"
#include "variantproperty.h"
#include <QCryptographicHash>
#include <QDir>
#include <QJsonArray>
#include <QJsonDocument>
#include <QLoggingCategory>
#include <QWaitCondition>
#include <random>
#include <queue>
using namespace ProjectExplorer;
using namespace std;
namespace {
bool makeParentPath(const Utils::FilePath &path)
{
QDir d;
return d.mkpath(path.toFileInfo().absolutePath());
}
QByteArray generateHash(const QString &token) {
static uint counter = 0;
std::mt19937 gen(std::random_device().operator()());
std::uniform_int_distribution<> distribution(1, 99999);
QByteArray data = QString("%1%2%3").arg(token).arg(++counter).arg(distribution(gen)).toLatin1();
return QCryptographicHash::hash(data, QCryptographicHash::Md5).toHex();
}
Q_LOGGING_CATEGORY(loggerInfo, "qtc.designer.assetExportPlugin.assetExporter", QtInfoMsg)
Q_LOGGING_CATEGORY(loggerWarn, "qtc.designer.assetExportPlugin.assetExporter", QtWarningMsg)
Q_LOGGING_CATEGORY(loggerError, "qtc.designer.assetExportPlugin.assetExporter", QtCriticalMsg)
@@ -42,6 +66,34 @@ Q_LOGGING_CATEGORY(loggerError, "qtc.designer.assetExportPlugin.assetExporter",
namespace QmlDesigner {
class AssetDumper
{
public:
AssetDumper();
~AssetDumper();
void dumpAsset(const QPixmap &p, const Utils::FilePath &path);
/* Keeps on dumping until all assets are dumped, then quits */
void quitDumper();
/* Aborts dumping */
void abortDumper();
private:
void addAsset(const QPixmap &p, const Utils::FilePath &path);
void doDumping(QFutureInterface<void> &fi);
void savePixmap(const QPixmap &p, Utils::FilePath &path) const;
QFuture<void> m_dumpFuture;
QMutex m_queueMutex;
QWaitCondition m_queueCondition;
std::queue<std::pair<QPixmap, Utils::FilePath>> m_assets;
std::atomic<bool> m_quitDumper;
};
AssetExporter::AssetExporter(AssetExporterView *view, ProjectExplorer::Project *project, QObject *parent) :
QObject(parent),
m_currentState(*this),
@@ -71,11 +123,13 @@ void AssetExporter::exportQml(const Utils::FilePaths &qmlFiles, const Utils::Fil
m_exportPath = exportPath;
m_currentState.change(ParsingState::Parsing);
triggerLoadNextFile();
m_assetDumper = make_unique<AssetDumper>();
}
void AssetExporter::cancel()
{
// TODO Cancel export
m_assetDumper.reset();
}
bool AssetExporter::isBusy() const
@@ -85,6 +139,21 @@ bool AssetExporter::isBusy() const
m_currentState == AssetExporter::ParsingState::WritingJson;
}
Utils::FilePath AssetExporter::exportAsset(const QmlObjectNode &node)
{
// TODO: Use this hash as UUID and add to the node.
QByteArray hash;
do {
hash = generateHash(node.id());
} while (m_usedHashes.contains(hash));
m_usedHashes.insert(hash);
Utils::FilePath assetPath = m_exportPath.pathAppended(QString("assets/%1.png")
.arg(QString::fromLatin1(hash)));
m_assetDumper->dumpAsset(node.toQmlItemNode().instanceRenderPixmap(), assetPath);
return assetPath;
}
void AssetExporter::exportComponent(const ModelNode &rootNode)
{
qCDebug(loggerInfo) << "Exporting component" << rootNode.id();
@@ -149,6 +218,7 @@ void AssetExporter::writeMetadata() const
Utils::FilePath metadataPath = m_exportPath.pathAppended(m_exportPath.fileName() + ".metadata");
ExportNotification::addInfo(tr("Writing metadata to file %1.").
arg(metadataPath.toUserOutput()));
makeParentPath(metadataPath);
m_currentState.change(ParsingState::WritingJson);
QJsonObject jsonRoot; // TODO: Write plugin info to root
jsonRoot.insert("artboards", m_components);
@@ -165,6 +235,7 @@ void AssetExporter::writeMetadata() const
}
notifyProgress(1.0);
ExportNotification::addInfo(tr("Export finished."));
m_assetDumper->quitDumper();
m_currentState.change(ParsingState::ExportingDone);
}
@@ -189,4 +260,93 @@ QDebug operator<<(QDebug os, const AssetExporter::ParsingState &s)
return os;
}
AssetDumper::AssetDumper():
m_quitDumper(false)
{
m_dumpFuture = Utils::runAsync(&AssetDumper::doDumping, this);
}
AssetDumper::~AssetDumper()
{
abortDumper();
}
void AssetDumper::dumpAsset(const QPixmap &p, const Utils::FilePath &path)
{
addAsset(p, path);
}
void AssetDumper::quitDumper()
{
m_quitDumper = true;
m_queueCondition.wakeAll();
if (!m_dumpFuture.isFinished())
m_dumpFuture.waitForFinished();
}
void AssetDumper::abortDumper()
{
if (!m_dumpFuture.isFinished()) {
m_dumpFuture.cancel();
m_queueCondition.wakeAll();
m_dumpFuture.waitForFinished();
}
}
void AssetDumper::addAsset(const QPixmap &p, const Utils::FilePath &path)
{
QMutexLocker locker(&m_queueMutex);
qDebug() << "Save Asset:" << path;
m_assets.push({p, path});
}
void AssetDumper::doDumping(QFutureInterface<void> &fi)
{
auto haveAsset = [this] (std::pair<QPixmap, Utils::FilePath> *asset) {
QMutexLocker locker(&m_queueMutex);
if (m_assets.empty())
return false;
*asset = m_assets.front();
m_assets.pop();
return true;
};
forever {
std::pair<QPixmap, Utils::FilePath> asset;
if (haveAsset(&asset)) {
if (fi.isCanceled())
break;
savePixmap(asset.first, asset.second);
} else {
if (m_quitDumper)
break;
QMutexLocker locker(&m_queueMutex);
m_queueCondition.wait(&m_queueMutex);
}
if (fi.isCanceled())
break;
}
fi.reportFinished();
}
void AssetDumper::savePixmap(const QPixmap &p, Utils::FilePath &path) const
{
if (p.isNull()) {
qCDebug(loggerWarn) << "Dumping null pixmap" << path;
return;
}
if (!makeParentPath(path)) {
ExportNotification::addError(AssetExporter::tr("Error creating asset directory. %1")
.arg(path.fileName()));
return;
}
if (!p.save(path.toString())) {
ExportNotification::addError(AssetExporter::tr("Error saving asset. %1")
.arg(path.fileName()));
}
}
}

View File

@@ -29,6 +29,7 @@
#include <QJsonArray>
#include <QJsonObject>
#include <QSet>
#include <memory>
@@ -41,6 +42,7 @@ class Project;
}
namespace QmlDesigner {
class AssetDumper;
class AssetExporter : public QObject
{
@@ -68,6 +70,8 @@ public:
void cancel();
bool isBusy() const;
Utils::FilePath exportAsset(const QmlObjectNode& node);
signals:
void stateChanged(ParsingState);
void exportProgressChanged(double) const;
@@ -82,6 +86,7 @@ private:
void loadNextFile();
void onQmlFileLoaded();
private:
mutable class State {
public:
@@ -96,6 +101,8 @@ private:
Utils::FilePaths m_exportFiles;
Utils::FilePath m_exportPath;
QJsonArray m_components;
QSet<QByteArray> m_usedHashes;
std::unique_ptr<AssetDumper> m_assetDumper;
};
QDebug operator<< (QDebug os, const QmlDesigner::AssetExporter::ParsingState& s);

View File

@@ -34,6 +34,7 @@
#include "parsers/modelitemnodeparser.h"
#include "parsers/textnodeparser.h"
#include "parsers/assetnodeparser.h"
#include "coreplugin/actionmanager/actionmanager.h"
#include "coreplugin/actionmanager/actioncontainer.h"
@@ -68,8 +69,9 @@ AssetExporterPlugin::AssetExporterPlugin() :
viewManager.registerViewTakingOwnership(m_view);
// Add parsers templates for factory instantiation.
ComponentExporter::addNodeParser<ItemNodeParser>();
ComponentExporter::addNodeParser<TextNodeParser>();
Component::addNodeParser<ItemNodeParser>();
Component::addNodeParser<TextNodeParser>();
Component::addNodeParser<AssetNodeParser>();
// Instantiate actions created by the plugin.
addActions();
@@ -93,7 +95,8 @@ void AssetExporterPlugin::onExport()
return;
FilePathModel model(startupProject);
auto exportDir = startupProject->projectFilePath().parentDir();
QString exportDirName = startupProject->displayName() + "_export";
auto exportDir = startupProject->projectFilePath().parentDir().pathAppended(exportDirName);
AssetExporter assetExporter(m_view, startupProject);
AssetExportDialog assetExporterDialog(exportDir, assetExporter, model);
assetExporterDialog.exec();

View File

@@ -15,6 +15,7 @@ HEADERS += \
componentexporter.h \
exportnotification.h \
filepathmodel.h \
parsers/assetnodeparser.h \
parsers/modelitemnodeparser.h \
parsers/modelnodeparser.h \
parsers/textnodeparser.h
@@ -27,6 +28,7 @@ SOURCES += \
componentexporter.cpp \
exportnotification.cpp \
filepathmodel.cpp \
parsers/assetnodeparser.cpp \
parsers/modelitemnodeparser.cpp \
parsers/modelnodeparser.cpp \
parsers/textnodeparser.cpp

View File

@@ -47,6 +47,8 @@ QtcProduct {
"exportnotification.h",
"filepathmodel.cpp",
"filepathmodel.h",
"parsers/assetnodeparser.cpp",
"parsers/assetnodeparser.h",
"parsers/modelitemnodeparser.cpp",
"parsers/modelitemnodeparser.h",
"parsers/modelnodeparser.cpp",

View File

@@ -57,7 +57,7 @@ const char ImportsTag[] = "extraImports";
const char UuidTag[] = "uuid";
const char ClipTag[] = "clip";
const char AssetDataTag[] = "assetData";
const char AssetPath[] = "assetPath";
const char AssetPathTag[] = "assetPath";
const char AssetBoundsTag[] = "assetBounds";
const char OpacityTag[] = "opacity";

View File

@@ -0,0 +1,66 @@
/****************************************************************************
**
** Copyright (C) 2020 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 "assetnodeparser.h"
#include "assetexportpluginconstants.h"
#include "assetexporter.h"
#include "qmlitemnode.h"
#include "componentexporter.h"
#include "utils/fileutils.h"
#include <QPixmap>
namespace QmlDesigner {
using namespace Constants;
AssetNodeParser::AssetNodeParser(const QByteArrayList &lineage, const ModelNode &node) :
ItemNodeParser(lineage, node)
{
}
bool AssetNodeParser::isExportable() const
{
auto hasType = [this](const QByteArray &type) {
return lineage().contains(type);
};
return hasType("QtQuick.Image") || hasType("QtQuick.Rectangle");
}
QJsonObject AssetNodeParser::json(Component &component) const
{
QJsonObject jsonObject = ItemNodeParser::json(component);
QPixmap asset = objectNode().toQmlItemNode().instanceRenderPixmap();
Utils::FilePath assetPath = component.exporter().exportAsset(objectNode());
QJsonObject assetData;
assetData.insert(AssetPathTag, assetPath.toString());
jsonObject.insert(AssetDataTag, assetData);
return jsonObject;
}
}

View File

@@ -0,0 +1,42 @@
/****************************************************************************
**
** Copyright (C) 2020 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 "modelitemnodeparser.h"
namespace QmlDesigner {
class Component;
class AssetNodeParser : public ItemNodeParser
{
public:
AssetNodeParser(const QByteArrayList &lineage, const ModelNode &node);
~AssetNodeParser() override = default;
bool isExportable() const override;
int priority() const override { return 200; }
QJsonObject json(Component &component) const override;
};
}