1 /****************************************************************************
2 **
3 ** Copyright (C) 2019 The Qt Company Ltd.
4 ** Contact: https://www.qt.io/licensing/
5 **
6 ** This file is part of Qt Creator.
7 **
8 ** Commercial License Usage
9 ** Licensees holding valid commercial Qt licenses may use this file in
10 ** accordance with the commercial license agreement provided with the
11 ** Software or, alternatively, in accordance with the terms contained in
12 ** a written agreement between you and The Qt Company. For licensing terms
13 ** and conditions see https://www.qt.io/terms-conditions. For further
14 ** information use the contact form at https://www.qt.io/contact-us.
15 **
16 ** GNU General Public License Usage
17 ** Alternatively, this file may be used under the terms of the GNU
18 ** General Public License version 3 as published by the Free Software
19 ** Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT
20 ** included in the packaging of this file. Please review the following
21 ** information to ensure the GNU General Public License requirements will
22 ** be met: https://www.gnu.org/licenses/gpl-3.0.html.
23 **
24 ****************************************************************************/
25 #include "itemlibraryassetimporter.h"
26 #include "assetimportupdatedialog.h"
27 #include "qmldesignerplugin.h"
28 #include "qmldesignerconstants.h"
29
30 #include "rewriterview.h"
31 #include "model.h"
32 #include "puppetcreator.h"
33 #include "rewritertransaction.h"
34 #include "rewritingexception.h"
35
36 #include <utils/algorithm.h>
37
38 #include <QApplication>
39 #include <QDir>
40 #include <QDirIterator>
41 #include <QFile>
42 #include <QJsonDocument>
43 #include <QJsonObject>
44 #include <QLoggingCategory>
45 #include <QMessageBox>
46 #include <QPushButton>
47 #include <QSaveFile>
48 #include <QTemporaryDir>
49
50 namespace
51 {
52 static Q_LOGGING_CATEGORY(importerLog, "qtc.itemlibrary.assetImporter", QtWarningMsg)
53 }
54
55 namespace QmlDesigner {
56
ItemLibraryAssetImporter(QObject * parent)57 ItemLibraryAssetImporter::ItemLibraryAssetImporter(QObject *parent) :
58 QObject (parent)
59 {
60 }
61
~ItemLibraryAssetImporter()62 ItemLibraryAssetImporter::~ItemLibraryAssetImporter() {
63 cancelImport();
64 delete m_tempDir;
65 };
66
importQuick3D(const QStringList & inputFiles,const QString & importPath,const QVector<QJsonObject> & options,const QHash<QString,int> & extToImportOptionsMap,const QSet<QString> & preselectedFilesForOverwrite)67 void ItemLibraryAssetImporter::importQuick3D(const QStringList &inputFiles,
68 const QString &importPath,
69 const QVector<QJsonObject> &options,
70 const QHash<QString, int> &extToImportOptionsMap,
71 const QSet<QString> &preselectedFilesForOverwrite)
72 {
73 if (m_isImporting)
74 cancelImport();
75 reset();
76 m_isImporting = true;
77
78 if (!m_tempDir->isValid()) {
79 addError(tr("Could not create a temporary directory for import."));
80 notifyFinished();
81 return;
82 }
83
84 m_importPath = importPath;
85
86 parseFiles(inputFiles, options, extToImportOptionsMap, preselectedFilesForOverwrite);
87
88 if (!isCancelled()) {
89 const auto parseData = m_parseData;
90 for (const auto &pd : parseData) {
91 if (!startImportProcess(pd)) {
92 addError(tr("Failed to start import 3D asset process."),
93 pd.sourceInfo.absoluteFilePath());
94 m_parseData.remove(pd.importId);
95 }
96 }
97 }
98
99 if (!isCancelled()) {
100 // Wait for puppet processes to finish
101 if (m_qmlPuppetProcesses.empty()) {
102 postImport();
103 } else {
104 m_qmlPuppetCount = static_cast<int>(m_qmlPuppetProcesses.size());
105 const QString progressTitle = tr("Importing 3D assets.");
106 addInfo(progressTitle);
107 notifyProgress(0, progressTitle);
108 }
109 }
110 }
111
isImporting() const112 bool ItemLibraryAssetImporter::isImporting() const
113 {
114 return m_isImporting;
115 }
116
cancelImport()117 void ItemLibraryAssetImporter::cancelImport()
118 {
119 m_cancelled = true;
120 if (m_isImporting)
121 notifyFinished();
122 }
123
addError(const QString & errMsg,const QString & srcPath) const124 void ItemLibraryAssetImporter::addError(const QString &errMsg, const QString &srcPath) const
125 {
126 qCDebug(importerLog) << "Error: "<< errMsg << srcPath;
127 emit errorReported(errMsg, srcPath);
128 }
129
addWarning(const QString & warningMsg,const QString & srcPath) const130 void ItemLibraryAssetImporter::addWarning(const QString &warningMsg, const QString &srcPath) const
131 {
132 qCDebug(importerLog) << "Warning: " << warningMsg << srcPath;
133 emit warningReported(warningMsg, srcPath);
134 }
135
addInfo(const QString & infoMsg,const QString & srcPath) const136 void ItemLibraryAssetImporter::addInfo(const QString &infoMsg, const QString &srcPath) const
137 {
138 qCDebug(importerLog) << "Info: " << infoMsg << srcPath;
139 emit infoReported(infoMsg, srcPath);
140 }
141
importProcessFinished(int exitCode,QProcess::ExitStatus exitStatus)142 void ItemLibraryAssetImporter::importProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
143 {
144 Q_UNUSED(exitStatus)
145
146 ++m_qmlImportFinishedCount;
147
148 m_qmlPuppetProcesses.erase(
149 std::remove_if(m_qmlPuppetProcesses.begin(), m_qmlPuppetProcesses.end(),
150 [&](const auto &entry) {
151 return !entry || entry->state() == QProcess::NotRunning;
152 }));
153
154 if (m_parseData.contains(-exitCode)) {
155 const ParseData pd = m_parseData.take(-exitCode);
156 addError(tr("Asset import process failed for: \"%1\".").arg(pd.sourceInfo.absoluteFilePath()));
157 }
158
159 if (m_qmlImportFinishedCount == m_qmlPuppetCount) {
160 notifyProgress(100);
161 QTimer::singleShot(0, this, &ItemLibraryAssetImporter::postImport);
162 } else {
163 notifyProgress(int(100. * (double(m_qmlImportFinishedCount) / double(m_qmlPuppetCount))));
164 }
165 }
166
iconProcessFinished(int exitCode,QProcess::ExitStatus exitStatus)167 void ItemLibraryAssetImporter::iconProcessFinished(int exitCode, QProcess::ExitStatus exitStatus)
168 {
169 Q_UNUSED(exitCode)
170 Q_UNUSED(exitStatus)
171
172 m_qmlPuppetProcesses.erase(
173 std::remove_if(m_qmlPuppetProcesses.begin(), m_qmlPuppetProcesses.end(),
174 [&](const auto &entry) {
175 return !entry || entry->state() == QProcess::NotRunning;
176 }));
177
178 if (m_qmlPuppetProcesses.empty()) {
179 notifyProgress(100);
180 QTimer::singleShot(0, this, &ItemLibraryAssetImporter::finalizeQuick3DImport);
181 } else {
182 notifyProgress(int(100. * (1. - (double(m_qmlPuppetProcesses.size()) / double(m_qmlPuppetCount)))));
183 }
184 }
185
notifyFinished()186 void ItemLibraryAssetImporter::notifyFinished()
187 {
188 m_isImporting = false;
189 emit importFinished();
190 }
191
reset()192 void ItemLibraryAssetImporter::reset()
193 {
194 m_isImporting = false;
195 m_cancelled = false;
196
197 delete m_tempDir;
198 m_tempDir = new QTemporaryDir;
199 m_importFiles.clear();
200 m_overwrittenImports.clear();
201 m_qmlPuppetProcesses.clear();
202 m_qmlPuppetCount = 0;
203 m_qmlImportFinishedCount = 0;
204 m_parseData.clear();
205 m_requiredImports.clear();
206 }
207
parseFiles(const QStringList & filePaths,const QVector<QJsonObject> & options,const QHash<QString,int> & extToImportOptionsMap,const QSet<QString> & preselectedFilesForOverwrite)208 void ItemLibraryAssetImporter::parseFiles(const QStringList &filePaths,
209 const QVector<QJsonObject> &options,
210 const QHash<QString, int> &extToImportOptionsMap,
211 const QSet<QString> &preselectedFilesForOverwrite)
212 {
213 if (isCancelled())
214 return;
215 const QString progressTitle = tr("Parsing files.");
216 addInfo(progressTitle);
217 notifyProgress(0, progressTitle);
218 uint count = 0;
219 double quota = 100.0 / filePaths.count();
220 std::function<void(double)> progress = [this, quota, &count, &progressTitle](double value) {
221 notifyProgress(qRound(quota * (count + value)), progressTitle);
222 };
223 for (const QString &file : filePaths) {
224 int index = extToImportOptionsMap.value(QFileInfo(file).suffix());
225 ParseData pd;
226 pd.options = options[index];
227 if (preParseQuick3DAsset(file, pd, preselectedFilesForOverwrite)) {
228 pd.importId = ++m_importIdCounter;
229 m_parseData.insert(pd.importId, pd);
230 }
231 notifyProgress(qRound(++count * quota), progressTitle);
232 }
233 }
234
preParseQuick3DAsset(const QString & file,ParseData & pd,const QSet<QString> & preselectedFilesForOverwrite)235 bool ItemLibraryAssetImporter::preParseQuick3DAsset(const QString &file, ParseData &pd,
236 const QSet<QString> &preselectedFilesForOverwrite)
237 {
238 pd.targetDir = QDir(m_importPath);
239 pd.outDir = QDir(m_tempDir->path());
240 pd.sourceInfo = QFileInfo(file);
241 pd.assetName = pd.sourceInfo.completeBaseName();
242
243 if (!pd.assetName.isEmpty()) {
244 // Fix name so it plays nice with imports
245 for (QChar ¤tChar : pd.assetName) {
246 if (!currentChar.isLetter() && !currentChar.isDigit())
247 currentChar = QLatin1Char('_');
248 }
249 const QChar firstChar = pd.assetName[0];
250 if (firstChar.isDigit())
251 pd.assetName[0] = QLatin1Char('_');
252 if (firstChar.isLower())
253 pd.assetName[0] = firstChar.toUpper();
254 }
255
256 pd.targetDirPath = pd.targetDir.filePath(pd.assetName);
257
258 if (pd.outDir.exists(pd.assetName)) {
259 addWarning(tr("Skipped import of duplicate asset: \"%1\".").arg(pd.assetName));
260 return false;
261 }
262
263 pd.originalAssetName = pd.assetName;
264 if (pd.targetDir.exists(pd.assetName)) {
265 // If we have a file system with case insensitive filenames, assetName may be
266 // different from the existing name. Modify assetName to ensure exact match to
267 // the overwritten old asset capitalization
268 const QStringList assetDirs = pd.targetDir.entryList({pd.assetName}, QDir::Dirs);
269 if (assetDirs.size() == 1) {
270 pd.assetName = assetDirs[0];
271 pd.targetDirPath = pd.targetDir.filePath(pd.assetName);
272 }
273 OverwriteResult result = preselectedFilesForOverwrite.isEmpty()
274 ? confirmAssetOverwrite(pd.assetName)
275 : OverwriteResult::Update;
276 if (result == OverwriteResult::Skip) {
277 addWarning(tr("Skipped import of existing asset: \"%1\".").arg(pd.assetName));
278 return false;
279 } else if (result == OverwriteResult::Update) {
280 // Add generated icons and existing source asset file, as those will always need
281 // to be overwritten
282 QSet<QString> alwaysOverwrite;
283 QString iconPath = pd.targetDirPath + '/' + Constants::QUICK_3D_ASSET_ICON_DIR;
284 // Note: Despite the name, QUICK_3D_ASSET_LIBRARY_ICON_SUFFIX is not a traditional file
285 // suffix. It's guaranteed to be in the generated icon filename, though.
286 QStringList filters {QStringLiteral("*%1*").arg(Constants::QUICK_3D_ASSET_LIBRARY_ICON_SUFFIX)};
287 QDirIterator iconIt(iconPath, filters, QDir::Files);
288 while (iconIt.hasNext()) {
289 iconIt.next();
290 alwaysOverwrite.insert(iconIt.fileInfo().absoluteFilePath());
291 }
292 alwaysOverwrite.insert(sourceSceneTargetFilePath(pd));
293 alwaysOverwrite.insert(pd.targetDirPath + '/' + Constants::QUICK_3D_ASSET_IMPORT_DATA_NAME);
294
295 Internal::AssetImportUpdateDialog dlg {pd.targetDirPath,
296 preselectedFilesForOverwrite,
297 alwaysOverwrite,
298 qobject_cast<QWidget *>(parent())};
299 int exitVal = dlg.exec();
300
301 QStringList overwriteFiles;
302 if (exitVal == QDialog::Accepted)
303 overwriteFiles = dlg.selectedFiles();
304 if (!overwriteFiles.isEmpty()) {
305 overwriteFiles.append(Utils::toList(alwaysOverwrite));
306 m_overwrittenImports.insert(pd.targetDirPath, overwriteFiles);
307 } else {
308 addWarning(tr("No files selected for overwrite, skipping import: \"%1\".").arg(pd.assetName));
309 return false;
310 }
311
312 } else {
313 m_overwrittenImports.insert(pd.targetDirPath, {});
314 }
315 }
316
317 pd.outDir.mkpath(pd.assetName);
318
319 if (!pd.outDir.cd(pd.assetName)) {
320 addError(tr("Could not access temporary asset directory: \"%1\".")
321 .arg(pd.outDir.filePath(pd.assetName)));
322 return false;
323 }
324 return true;
325 }
326
postParseQuick3DAsset(const ParseData & pd)327 void ItemLibraryAssetImporter::postParseQuick3DAsset(const ParseData &pd)
328 {
329 QDir outDir = pd.outDir;
330 if (pd.originalAssetName != pd.assetName) {
331 // Fix the generated qml file name
332 const QString assetQml = pd.originalAssetName + ".qml";
333 if (outDir.exists(assetQml))
334 outDir.rename(assetQml, pd.assetName + ".qml");
335 }
336
337 QHash<QString, QString> assetFiles;
338 const int outDirPathSize = outDir.path().size();
339 auto insertAsset = [&](const QString &filePath) {
340 QString targetPath = filePath.mid(outDirPathSize);
341 targetPath.prepend(pd.targetDirPath);
342 assetFiles.insert(filePath, targetPath);
343 };
344
345 // Generate qmldir file if importer doesn't already make one
346 QString qmldirFileName = outDir.absoluteFilePath(QStringLiteral("qmldir"));
347 if (!QFileInfo::exists(qmldirFileName)) {
348 QSaveFile qmldirFile(qmldirFileName);
349 QString version = QStringLiteral("1.0");
350
351 // Note: Currently Quick3D importers only generate externally usable qml files on the top
352 // level of the import directory, so we don't search subdirectories. The qml files in
353 // subdirs assume they are used within the context of the toplevel qml files.
354 QDirIterator qmlIt(outDir.path(), {QStringLiteral("*.qml")}, QDir::Files);
355 if (qmlIt.hasNext()) {
356 outDir.mkdir(Constants::QUICK_3D_ASSET_ICON_DIR);
357 if (qmldirFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
358 QString qmlInfo;
359 qmlInfo.append("module ");
360 qmlInfo.append(m_importPath.split('/').last());
361 qmlInfo.append(".");
362 qmlInfo.append(pd.assetName);
363 qmlInfo.append('\n');
364 m_requiredImports.append(Import::createLibraryImport(
365 QStringLiteral("%1.%2").arg(pd.targetDir.dirName(),
366 pd.assetName), version));
367 while (qmlIt.hasNext()) {
368 qmlIt.next();
369 QFileInfo fi = QFileInfo(qmlIt.filePath());
370 qmlInfo.append(fi.baseName());
371 qmlInfo.append(' ');
372 qmlInfo.append(version);
373 qmlInfo.append(' ');
374 qmlInfo.append(outDir.relativeFilePath(qmlIt.filePath()));
375 qmlInfo.append('\n');
376
377 // Generate item library icon for qml file based on root component
378 QFile qmlFile(qmlIt.filePath());
379 if (qmlFile.open(QIODevice::ReadOnly)) {
380 QString iconFileName = outDir.path() + '/'
381 + Constants::QUICK_3D_ASSET_ICON_DIR + '/' + fi.baseName()
382 + Constants::QUICK_3D_ASSET_LIBRARY_ICON_SUFFIX;
383 QString iconFileName2x = iconFileName + "@2x";
384 QByteArray content = qmlFile.readAll();
385 int braceIdx = content.indexOf('{');
386 if (braceIdx != -1) {
387 int nlIdx = content.lastIndexOf('\n', braceIdx);
388 QByteArray rootItem = content.mid(nlIdx, braceIdx - nlIdx).trimmed();
389 if (rootItem == "Node") { // a 3D object
390 // create hints file with proper hints
391 QFile file(outDir.path() + '/' + fi.baseName() + ".hints");
392 file.open(QIODevice::WriteOnly | QIODevice::Text);
393 QTextStream out(&file);
394 out << "visibleInNavigator: true" << Qt::endl;
395 out << "canBeDroppedInFormEditor: false" << Qt::endl;
396 out << "canBeDroppedInView3D: true" << Qt::endl;
397 file.close();
398
399 // Add quick3D import unless it is already added
400 if (m_requiredImports.first().url() != "QtQuick3D") {
401 QByteArray import3dStr{"import QtQuick3D"};
402 int importIdx = content.indexOf(import3dStr);
403 if (importIdx != -1 && importIdx < braceIdx) {
404 importIdx += import3dStr.size();
405 int nlIdx = content.indexOf('\n', importIdx);
406 QByteArray versionStr = content.mid(importIdx, nlIdx - importIdx).trimmed();
407 // There could be 'as abc' after version, so just take first part
408 QList<QByteArray> parts = versionStr.split(' ');
409 QString impVersion;
410 if (parts.size() >= 1)
411 impVersion = QString::fromUtf8(parts[0]);
412 m_requiredImports.prepend(Import::createLibraryImport(
413 "QtQuick3D", impVersion));
414 }
415 }
416 }
417 if (startIconProcess(24, iconFileName, qmlIt.filePath())) {
418 // Since icon is generated by external process, the file won't be
419 // ready for asset gathering below, so assume its generation succeeds
420 // and add it now.
421 insertAsset(iconFileName);
422 insertAsset(iconFileName2x);
423 }
424 }
425 }
426 }
427 qmldirFile.write(qmlInfo.toUtf8());
428 qmldirFile.commit();
429 } else {
430 addError(tr("Failed to create qmldir file for asset: \"%1\".").arg(pd.assetName));
431 }
432 }
433 }
434
435 // Generate import metadata file
436 const QString sourcePath = pd.sourceInfo.absoluteFilePath();
437 QString importDataFileName = outDir.absoluteFilePath(Constants::QUICK_3D_ASSET_IMPORT_DATA_NAME);
438 QSaveFile importDataFile(importDataFileName);
439 if (importDataFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) {
440 QJsonObject optObj;
441 optObj.insert(Constants::QUICK_3D_ASSET_IMPORT_DATA_OPTIONS_KEY, pd.options);
442 optObj.insert(Constants::QUICK_3D_ASSET_IMPORT_DATA_SOURCE_KEY, sourcePath);
443 importDataFile.write(QJsonDocument{optObj}.toJson());
444 importDataFile.commit();
445 }
446
447 // Gather all generated files
448 QDirIterator dirIt(outDir.path(), QDir::Files, QDirIterator::Subdirectories);
449 while (dirIt.hasNext()) {
450 dirIt.next();
451 insertAsset(dirIt.filePath());
452 }
453
454 // Copy the original asset into a subdirectory
455 assetFiles.insert(sourcePath, sourceSceneTargetFilePath(pd));
456 m_importFiles.insert(assetFiles);
457 }
458
copyImportedFiles()459 void ItemLibraryAssetImporter::copyImportedFiles()
460 {
461 if (!m_overwrittenImports.isEmpty()) {
462 const QString progressTitle = tr("Removing old overwritten assets.");
463 addInfo(progressTitle);
464 notifyProgress(0, progressTitle);
465
466 int counter = 0;
467 auto it = m_overwrittenImports.constBegin();
468 while (it != m_overwrittenImports.constEnd()) {
469 QDir dir(it.key());
470 if (dir.exists()) {
471 const auto &overwrittenFiles = it.value();
472 if (overwrittenFiles.isEmpty()) {
473 // Overwrite entire import
474 dir.removeRecursively();
475 } else {
476 // Overwrite just selected files
477 for (const auto &fileName : overwrittenFiles)
478 QFile::remove(fileName);
479 }
480 }
481 notifyProgress((100 * ++counter) / m_overwrittenImports.size(), progressTitle);
482 ++it;
483 }
484 }
485
486 if (!m_importFiles.isEmpty()) {
487 const QString progressTitle = tr("Copying asset files.");
488 addInfo(progressTitle);
489 notifyProgress(0, progressTitle);
490
491 int counter = 0;
492 for (const auto &assetFiles : qAsConst(m_importFiles)) {
493 // Only increase progress between entire assets instead of individual files, because
494 // progress notify leads to processEvents call, which can lead to various filesystem
495 // watchers triggering while library is still incomplete, leading to inconsistent model.
496 // This also speeds up the copying as incomplete folder is not parsed unnecessarily
497 // by filesystem watchers.
498 QHash<QString, QString>::const_iterator it = assetFiles.begin();
499 while (it != assetFiles.end()) {
500 if (QFileInfo::exists(it.key()) && !QFileInfo::exists(it.value())) {
501 QDir targetDir = QFileInfo(it.value()).dir();
502 if (!targetDir.exists())
503 targetDir.mkpath(".");
504 QFile::copy(it.key(), it.value());
505 }
506 ++it;
507 }
508 notifyProgress((100 * ++counter) / m_importFiles.size(), progressTitle);
509 }
510 notifyProgress(100, progressTitle);
511 }
512 }
513
notifyProgress(int value,const QString & text)514 void ItemLibraryAssetImporter::notifyProgress(int value, const QString &text)
515 {
516 m_progressTitle = text;
517 emit progressChanged(value, m_progressTitle);
518 keepUiAlive();
519 }
520
notifyProgress(int value)521 void ItemLibraryAssetImporter::notifyProgress(int value)
522 {
523 notifyProgress(value, m_progressTitle);
524 }
525
keepUiAlive() const526 void ItemLibraryAssetImporter::keepUiAlive() const
527 {
528 QApplication::processEvents();
529 }
530
confirmAssetOverwrite(const QString & assetName)531 ItemLibraryAssetImporter::OverwriteResult ItemLibraryAssetImporter::confirmAssetOverwrite(const QString &assetName)
532 {
533 const QString title = tr("Overwrite Existing Asset?");
534 const QString question = tr("Asset already exists. Overwrite existing or skip?\n\"%1\"").arg(assetName);
535
536 QMessageBox msgBox {QMessageBox::Question, title, question, QMessageBox::NoButton,
537 qobject_cast<QWidget *>(parent())};
538 QPushButton *updateButton = msgBox.addButton(tr("Overwrite Selected Files"), QMessageBox::NoRole);
539 QPushButton *overwriteButton = msgBox.addButton(tr("Overwrite All Files"), QMessageBox::NoRole);
540 QPushButton *skipButton = msgBox.addButton(tr("Skip"), QMessageBox::NoRole);
541 msgBox.setDefaultButton(overwriteButton);
542 msgBox.setEscapeButton(skipButton);
543
544 msgBox.exec();
545
546 if (msgBox.clickedButton() == updateButton)
547 return OverwriteResult::Update;
548 else if (msgBox.clickedButton() == overwriteButton)
549 return OverwriteResult::Overwrite;
550 return OverwriteResult::Skip;
551 }
552
startImportProcess(const ParseData & pd)553 bool ItemLibraryAssetImporter::startImportProcess(const ParseData &pd)
554 {
555 auto doc = QmlDesignerPlugin::instance()->currentDesignDocument();
556 Model *model = doc ? doc->currentModel() : nullptr;
557
558 if (model) {
559 PuppetCreator puppetCreator(doc->currentTarget(), model);
560 puppetCreator.createQml2PuppetExecutableIfMissing();
561 QStringList puppetArgs;
562 QJsonDocument optDoc(pd.options);
563
564 puppetArgs << "--import3dAsset" << pd.sourceInfo.absoluteFilePath()
565 << pd.outDir.absolutePath() << QString::number(pd.importId)
566 << QString::fromUtf8(optDoc.toJson());
567
568 QProcessUniquePointer process = puppetCreator.createPuppetProcess(
569 "custom",
570 {},
571 std::function<void()>(),
572 [&](int exitCode, QProcess::ExitStatus exitStatus) {
573 importProcessFinished(exitCode, exitStatus);
574 },
575 puppetArgs);
576
577 if (process->waitForStarted(5000)) {
578 m_qmlPuppetProcesses.push_back(std::move(process));
579 return true;
580 } else {
581 process.reset();
582 }
583 }
584 return false;
585 }
586
startIconProcess(int size,const QString & iconFile,const QString & iconSource)587 bool ItemLibraryAssetImporter::startIconProcess(int size, const QString &iconFile,
588 const QString &iconSource)
589 {
590 auto doc = QmlDesignerPlugin::instance()->currentDesignDocument();
591 Model *model = doc ? doc->currentModel() : nullptr;
592
593 if (model) {
594 PuppetCreator puppetCreator(doc->currentTarget(), model);
595 puppetCreator.createQml2PuppetExecutableIfMissing();
596 QStringList puppetArgs;
597 puppetArgs << "--rendericon" << QString::number(size) << iconFile << iconSource;
598 QProcessUniquePointer process = puppetCreator.createPuppetProcess(
599 "custom",
600 {},
601 std::function<void()>(),
602 [&](int exitCode, QProcess::ExitStatus exitStatus) {
603 iconProcessFinished(exitCode, exitStatus);
604 },
605 puppetArgs);
606
607 if (process->waitForStarted(5000)) {
608 m_qmlPuppetProcesses.push_back(std::move(process));
609 return true;
610 } else {
611 process.reset();
612 }
613 }
614 return false;
615 }
616
postImport()617 void ItemLibraryAssetImporter::postImport()
618 {
619 Q_ASSERT(m_qmlPuppetProcesses.empty());
620
621 if (!isCancelled()) {
622 for (const auto &pd : qAsConst(m_parseData))
623 postParseQuick3DAsset(pd);
624 }
625
626 if (!isCancelled()) {
627 // Wait for icon generation processes to finish
628 if (m_qmlPuppetProcesses.empty()) {
629 finalizeQuick3DImport();
630 } else {
631 m_qmlPuppetCount = static_cast<int>(m_qmlPuppetProcesses.size());
632 const QString progressTitle = tr("Generating icons.");
633 addInfo(progressTitle);
634 notifyProgress(0, progressTitle);
635 }
636 }
637 }
638
finalizeQuick3DImport()639 void ItemLibraryAssetImporter::finalizeQuick3DImport()
640 {
641 if (!isCancelled()) {
642 // Don't allow cancel anymore as existing asset overwrites are not trivially recoverable.
643 // Also, on Windows at least you can't delete a subdirectory of a watched directory,
644 // so complete rollback is no longer possible in any case.
645 emit importNearlyFinished();
646
647 copyImportedFiles();
648
649 auto doc = QmlDesignerPlugin::instance()->currentDesignDocument();
650 Model *model = doc ? doc->currentModel() : nullptr;
651 if (model && !m_importFiles.isEmpty()) {
652 const QString progressTitle = tr("Updating data model.");
653 addInfo(progressTitle);
654 notifyProgress(0, progressTitle);
655
656 // First we have to wait a while to ensure qmljs detects new files and updates its
657 // internal model. Then we make a non-change to the document to trigger qmljs snapshot
658 // update. There is an inbuilt delay before rewriter change actually updates the data
659 // model, so we need to wait for another moment to allow the change to take effect.
660 // Otherwise subsequent subcomponent manager update won't detect new imports properly.
661 QTimer *timer = new QTimer(parent());
662 static int counter;
663 counter = 0;
664 timer->callOnTimeout([this, timer, progressTitle, model, doc]() {
665 if (!isCancelled()) {
666 notifyProgress(++counter * 5, progressTitle);
667 if (counter < 10) {
668 // Do not proceed while application isn't active as the filesystem
669 // watcher qmljs uses won't trigger unless application is active
670 if (QApplication::applicationState() != Qt::ApplicationActive)
671 --counter;
672 } else if (counter == 10) {
673 model->rewriterView()->textModifier()->replace(0, 0, {});
674 } else if (counter == 19) {
675 try {
676 const QList<Import> currentImports = model->imports();
677 QList<Import> newImportsToAdd;
678 for (auto &imp : qAsConst(m_requiredImports)) {
679 if (!currentImports.contains(imp))
680 newImportsToAdd.append(imp);
681 }
682 if (!newImportsToAdd.isEmpty()) {
683 RewriterTransaction transaction
684 = model->rewriterView()->beginRewriterTransaction(
685 QByteArrayLiteral("ItemLibraryAssetImporter::finalizeQuick3DImport"));
686
687 model->changeImports(newImportsToAdd, {});
688 transaction.commit();
689 for (const Import &import : qAsConst(newImportsToAdd))
690 doc->updateSubcomponentManagerImport(import);
691 }
692 } catch (const RewritingException &e) {
693 addError(tr("Failed to update imports: %1").arg(e.description()));
694 }
695 } else if (counter >= 20) {
696 if (!m_overwrittenImports.isEmpty())
697 model->rewriterView()->emitCustomNotification("asset_import_update");
698 timer->stop();
699 notifyFinished();
700 }
701 } else {
702 timer->stop();
703 }
704 });
705 timer->start(100);
706 } else {
707 notifyFinished();
708 }
709 }
710 }
711
sourceSceneTargetFilePath(const ParseData & pd)712 QString ItemLibraryAssetImporter::sourceSceneTargetFilePath(const ParseData &pd)
713 {
714 return pd.targetDirPath + QStringLiteral("/source scene/") + pd.sourceInfo.fileName();
715 }
716
isCancelled() const717 bool ItemLibraryAssetImporter::isCancelled() const
718 {
719 keepUiAlive();
720 return m_cancelled;
721 }
722
723 } // QmlDesigner
724