diff options
Diffstat (limited to 'src/plugins/qmldesigner/assetexporterplugin/assetexporter.cpp')
-rw-r--r-- | src/plugins/qmldesigner/assetexporterplugin/assetexporter.cpp | 377 |
1 files changed, 377 insertions, 0 deletions
diff --git a/src/plugins/qmldesigner/assetexporterplugin/assetexporter.cpp b/src/plugins/qmldesigner/assetexporterplugin/assetexporter.cpp new file mode 100644 index 0000000000..1ea1e09e92 --- /dev/null +++ b/src/plugins/qmldesigner/assetexporterplugin/assetexporter.cpp @@ -0,0 +1,377 @@ +/**************************************************************************** +** +** 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 "assetexporter.h" +#include "componentexporter.h" +#include "exportnotification.h" + +#include "rewriterview.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) +} + +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), + m_project(project), + m_view(view) +{ + connect(m_view, &AssetExporterView::loadingFinished, this, &AssetExporter::onQmlFileLoaded); + connect(m_view, &AssetExporterView::loadingError, this, &AssetExporter::notifyLoadError); +} + +AssetExporter::~AssetExporter() +{ + cancel(); +} + +void AssetExporter::exportQml(const Utils::FilePaths &qmlFiles, const Utils::FilePath &exportPath, + bool exportAssets) +{ + ExportNotification::addInfo(tr("Exporting metadata at %1. Export assets: ") + .arg(exportPath.toUserOutput()) + .arg(exportAssets? tr("Yes") : tr("No"))); + notifyProgress(0.0); + m_exportFiles = qmlFiles; + m_totalFileCount = m_exportFiles.count(); + m_components = QJsonArray(); + m_exportPath = exportPath; + m_currentState.change(ParsingState::Parsing); + triggerLoadNextFile(); + if (exportAssets) + m_assetDumper = make_unique<AssetDumper>(); + else + m_assetDumper.reset(); +} + +void AssetExporter::cancel() +{ + if (!m_cancelled) { + ExportNotification::addInfo(tr("Cancelling export.")); + m_assetDumper.reset(); + m_cancelled = true; + } +} + +bool AssetExporter::isBusy() const +{ + return m_currentState == AssetExporter::ParsingState::Parsing || + m_currentState == AssetExporter::ParsingState::ExportingAssets || + m_currentState == AssetExporter::ParsingState::WritingJson; +} + +Utils::FilePath AssetExporter::exportAsset(const QmlObjectNode &node, const QString &uuid) +{ + if (m_cancelled) + return {}; + Utils::FilePath assetPath = m_exportPath.pathAppended(QString("assets/%1.png").arg(uuid)); + if (m_assetDumper) + m_assetDumper->dumpAsset(node.toQmlItemNode().instanceRenderPixmap(), assetPath); + return assetPath; +} + +void AssetExporter::exportComponent(const ModelNode &rootNode) +{ + qCDebug(loggerInfo) << "Exporting component" << rootNode.id(); + Component exporter(*this, rootNode); + exporter.exportComponent(); + m_components.append(exporter.json()); + notifyProgress((m_totalFileCount - m_exportFiles.count()) * 0.8 / m_totalFileCount); +} + +void AssetExporter::notifyLoadError(AssetExporterView::LoadState state) +{ + QString errorStr = tr("Unknown error."); + switch (state) { + case AssetExporterView::LoadState::Exausted: + errorStr = tr("Loading file is taking too long."); + break; + case AssetExporterView::LoadState::QmlErrorState: + errorStr = tr("Cannot parse. QML file has errors."); + break; + default: + return; + } + qCDebug(loggerError) << "QML load error:" << errorStr; + ExportNotification::addError(tr("Loading QML failed. %1").arg(errorStr)); +} + +void AssetExporter::notifyProgress(double value) const +{ + emit exportProgressChanged(value); +} + +void AssetExporter::onQmlFileLoaded() +{ + QTC_ASSERT(m_view && m_view->model(), qCDebug(loggerError) << "Null model"; return); + qCDebug(loggerInfo) << "Qml file load done" << m_view->model()->fileUrl(); + exportComponent(m_view->rootModelNode()); + QString error; + if (!m_view->saveQmlFile(&error)) { + ExportNotification::addError(tr("Error saving QML file. %1") + .arg(error.isEmpty()? tr("Unknown") : error)); + } + triggerLoadNextFile(); +} + +QByteArray AssetExporter::generateUuid(const ModelNode &node) +{ + QByteArray uuid; + do { + uuid = generateHash(node.id()); + } while (m_usedHashes.contains(uuid)); + m_usedHashes.insert(uuid); + return uuid; +} + +void AssetExporter::triggerLoadNextFile() +{ + QTimer::singleShot(0, this, &AssetExporter::loadNextFile); +} + +void AssetExporter::loadNextFile() +{ + if (m_cancelled || m_exportFiles.isEmpty()) { + notifyProgress(0.8); + m_currentState.change(ParsingState::ParsingFinished); + writeMetadata(); + return; + } + + // Load the next pending file. + const Utils::FilePath file = m_exportFiles.takeFirst(); + ExportNotification::addInfo(tr("Exporting file %1.").arg(file.toUserOutput())); + qCDebug(loggerInfo) << "Loading next file" << file; + m_view->loadQmlFile(file); +} + +void AssetExporter::writeMetadata() const +{ + if (m_cancelled) { + notifyProgress(1.0); + ExportNotification::addInfo(tr("Export cancelled.")); + m_currentState.change(ParsingState::ExportingDone); + return; + } + + 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); + QJsonDocument doc(jsonRoot); + if (doc.isNull() || doc.isEmpty()) { + ExportNotification::addError(tr("Empty JSON document.")); + } else { + Utils::FileSaver saver(metadataPath.toString(), QIODevice::Text); + saver.write(doc.toJson(QJsonDocument::Indented)); + if (!saver.finalize()) { + ExportNotification::addError(tr("Writing metadata failed. %1"). + arg(saver.errorString())); + } + } + notifyProgress(1.0); + ExportNotification::addInfo(tr("Export finished.")); + if (m_assetDumper) + m_assetDumper->quitDumper(); + m_currentState.change(ParsingState::ExportingDone); +} + +AssetExporter::State::State(AssetExporter &exporter) : + m_assetExporter(exporter) +{ + +} + +void AssetExporter::State::change(const ParsingState &state) +{ + qCDebug(loggerInfo()) << "Assetimporter State change: Old: " << m_state << "New: " << state; + if (m_state != state) { + m_state = state; + m_assetExporter.stateChanged(m_state); + } +} + +QDebug operator<<(QDebug os, const AssetExporter::ParsingState &s) +{ + os << static_cast<std::underlying_type<QmlDesigner::AssetExporter::ParsingState>::type>(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())); + } +} + +} |