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 &currentChar : 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