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 "itemlibraryassetimportdialog.h"
26 #include "ui_itemlibraryassetimportdialog.h"
27 
28 #include "qmldesignerplugin.h"
29 #include "qmldesignerconstants.h"
30 #include "model.h"
31 #include "nodemetainfo.h"
32 #include "variantproperty.h"
33 
34 #include "utils/outputformatter.h"
35 #include "theme.h"
36 
37 #include <projectexplorer/project.h>
38 #include <projectexplorer/session.h>
39 #include <coreplugin/icore.h>
40 
41 #include <QFileInfo>
42 #include <QDir>
43 #include <QLoggingCategory>
44 #include <QTimer>
45 #include <QJsonArray>
46 #include <QJsonDocument>
47 #include <QJsonParseError>
48 #include <QPushButton>
49 #include <QGridLayout>
50 #include <QLabel>
51 #include <QCheckBox>
52 #include <QSpinBox>
53 #include <QScrollBar>
54 #include <QTabBar>
55 #include <QScrollArea>
56 #include <QMessageBox>
57 #include <QFileDialog>
58 
59 namespace QmlDesigner {
60 
61 namespace {
62 
addFormattedMessage(Utils::OutputFormatter * formatter,const QString & str,const QString & srcPath,Utils::OutputFormat format)63 static void addFormattedMessage(Utils::OutputFormatter *formatter, const QString &str,
64                                 const QString &srcPath, Utils::OutputFormat format) {
65     if (!formatter)
66         return;
67     QString msg = str;
68     if (!srcPath.isEmpty())
69         msg += QStringLiteral(": \"%1\"").arg(srcPath);
70     msg += QLatin1Char('\n');
71     formatter->appendMessage(msg, format);
72     formatter->plainTextEdit()->verticalScrollBar()->setValue(
73                 formatter->plainTextEdit()->verticalScrollBar()->maximum());
74 }
75 
76 static const int rowHeight = 26;
77 
78 }
79 
ItemLibraryAssetImportDialog(const QStringList & importFiles,const QString & defaulTargetDirectory,const QVariantMap & supportedExts,const QVariantMap & supportedOpts,const QJsonObject & defaultOpts,const QSet<QString> & preselectedFilesForOverwrite,QWidget * parent)80 ItemLibraryAssetImportDialog::ItemLibraryAssetImportDialog(
81         const QStringList &importFiles, const QString &defaulTargetDirectory,
82         const QVariantMap &supportedExts, const QVariantMap &supportedOpts,
83         const QJsonObject &defaultOpts, const QSet<QString> &preselectedFilesForOverwrite,
84         QWidget *parent)
85     : QDialog(parent)
86     , ui(new Ui::ItemLibraryAssetImportDialog)
87     , m_importer(this)
88     , m_preselectedFilesForOverwrite(preselectedFilesForOverwrite)
89 {
90     setModal(true);
91     ui->setupUi(this);
92 
93     m_outputFormatter = new Utils::OutputFormatter;
94     m_outputFormatter->setPlainTextEdit(ui->plainTextEdit);
95 
96     // Skip unsupported assets
97     QHash<QString, bool> supportMap;
98     for (const auto &file : importFiles) {
99         QString suffix = QFileInfo(file).suffix().toLower();
100         if (!supportMap.contains(suffix)) {
101             bool supported = false;
102             for (const auto &exts : supportedExts) {
103                 if (exts.toStringList().contains(suffix)) {
104                     supported = true;
105                     break;
106                 }
107             }
108             supportMap.insert(suffix, supported);
109         }
110         if (supportMap[suffix])
111             m_quick3DFiles << file;
112     }
113 
114     if (m_quick3DFiles.size() != importFiles.size())
115         addWarning("Cannot import 3D and other assets simultaneously. Skipping non-3D assets.");
116 
117     ui->buttonBox->button(QDialogButtonBox::Ok)->setText(tr("Import"));
118     connect(ui->buttonBox->button(QDialogButtonBox::Ok), &QPushButton::clicked,
119             this, &ItemLibraryAssetImportDialog::onImport);
120 
121     ui->buttonBox->button(QDialogButtonBox::Close)->setDefault(true);
122 
123     QStringList importPaths;
124     auto doc = QmlDesignerPlugin::instance()->currentDesignDocument();
125     if (doc) {
126         Model *model = doc->currentModel();
127         if (model)
128             importPaths = model->importPaths();
129     }
130 
131     QString targetDir = defaulTargetDirectory;
132 
133     ProjectExplorer::Project *currentProject = ProjectExplorer::SessionManager::projectForFile(doc->fileName());
134     if (currentProject)
135         targetDir = currentProject->projectDirectory().toString();
136 
137     // Import is always done under known folder. The order of preference for folder is:
138     // 1) An existing QUICK_3D_ASSETS_FOLDER under DEFAULT_ASSET_IMPORT_FOLDER project import path
139     // 2) An existing QUICK_3D_ASSETS_FOLDER under any project import path
140     // 3) New QUICK_3D_ASSETS_FOLDER under DEFAULT_ASSET_IMPORT_FOLDER project import path
141     // 4) New QUICK_3D_ASSETS_FOLDER under any project import path
142     // 5) New QUICK_3D_ASSETS_FOLDER under new DEFAULT_ASSET_IMPORT_FOLDER under project
143     const QString defaultAssetFolder = QLatin1String(Constants::DEFAULT_ASSET_IMPORT_FOLDER);
144     const QString quick3DFolder = QLatin1String(Constants::QUICK_3D_ASSETS_FOLDER);
145     QString candidatePath = targetDir + defaultAssetFolder + quick3DFolder;
146     int candidatePriority = 5;
147 
148     for (const auto &importPath : qAsConst(importPaths)) {
149         if (importPath.startsWith(targetDir)) {
150             const bool isDefaultFolder = importPath.endsWith(defaultAssetFolder);
151             const QString assetFolder = importPath + quick3DFolder;
152             const bool exists = QFileInfo::exists(assetFolder);
153             if (exists) {
154                 if (isDefaultFolder) {
155                     // Priority one location, stop looking
156                     candidatePath = assetFolder;
157                     break;
158                 } else if (candidatePriority > 2) {
159                     candidatePriority = 2;
160                     candidatePath = assetFolder;
161                 }
162             } else {
163                 if (candidatePriority > 3 && isDefaultFolder) {
164                     candidatePriority = 3;
165                     candidatePath = assetFolder;
166                 } else if (candidatePriority > 4) {
167                     candidatePriority = 4;
168                     candidatePath = assetFolder;
169                 }
170             }
171         }
172     }
173     m_quick3DImportPath = candidatePath;
174 
175     if (!m_quick3DFiles.isEmpty()) {
176         QVector<QJsonObject> groups;
177 
178         auto optIt = supportedOpts.constBegin();
179         int optIndex = 0;
180         while (optIt != supportedOpts.constEnd()) {
181             QJsonObject options = QJsonObject::fromVariantMap(qvariant_cast<QVariantMap>(optIt.value()));
182             m_importOptions << options.value("options").toObject();
183             auto it = defaultOpts.constBegin();
184             while (it != defaultOpts.constEnd()) {
185                 if (m_importOptions.last().contains(it.key())) {
186                     QJsonObject optObj = m_importOptions.last()[it.key()].toObject();
187                     QJsonValue value(it.value().toObject()["value"]);
188                     optObj.insert("value", value);
189                     m_importOptions.last().insert(it.key(), optObj);
190                 }
191                 ++it;
192             }
193             groups << options.value("groups").toObject();
194             const auto &exts = optIt.key().split(':');
195             for (const auto &ext : exts)
196                 m_extToImportOptionsMap.insert(ext, optIndex);
197             ++optIt;
198             ++optIndex;
199         }
200 
201         // Create tab for each supported extension group that also has files included in the import
202         QMap<QString, int> tabMap; // QMap used for alphabetical order
203         for (const auto &file : qAsConst(m_quick3DFiles)) {
204             auto extIt = supportedExts.constBegin();
205             QString ext = QFileInfo(file).suffix().toLower();
206             while (extIt != supportedExts.constEnd()) {
207                 if (!tabMap.contains(extIt.key()) && extIt.value().toStringList().contains(ext)) {
208                     tabMap.insert(extIt.key(), m_extToImportOptionsMap.value(ext));
209                     break;
210                 }
211                 ++extIt;
212             }
213         }
214 
215         ui->tabWidget->clear();
216         auto tabIt = tabMap.constBegin();
217         while (tabIt != tabMap.constEnd()) {
218             createTab(tabIt.key(), tabIt.value(), groups[tabIt.value()]);
219             ++tabIt;
220         }
221 
222         // Pad all tabs to same height
223         for (int i = 0; i < ui->tabWidget->count(); ++i) {
224             auto optionsArea = qobject_cast<QScrollArea *>(ui->tabWidget->widget(i));
225             if (optionsArea && optionsArea->widget()) {
226                 auto grid = qobject_cast<QGridLayout *>(optionsArea->widget()->layout());
227                 if (grid) {
228                     int rows = grid->rowCount();
229                     for (int j = rows; j < m_optionsRows; ++j) {
230                         grid->addWidget(new QWidget(optionsArea->widget()), j, 0);
231                         grid->setRowMinimumHeight(j, rowHeight);
232                     }
233                 }
234             }
235         }
236 
237         ui->tabWidget->setCurrentIndex(0);
238     }
239 
240     connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked,
241             this, &ItemLibraryAssetImportDialog::onClose);
242     connect(ui->tabWidget, &QTabWidget::currentChanged,
243             this, &ItemLibraryAssetImportDialog::updateUi);
244 
245     connect(&m_importer, &ItemLibraryAssetImporter::errorReported,
246             this, &ItemLibraryAssetImportDialog::addError);
247     connect(&m_importer, &ItemLibraryAssetImporter::warningReported,
248             this, &ItemLibraryAssetImportDialog::addWarning);
249     connect(&m_importer, &ItemLibraryAssetImporter::infoReported,
250             this, &ItemLibraryAssetImportDialog::addInfo);
251     connect(&m_importer, &ItemLibraryAssetImporter::importNearlyFinished,
252             this, &ItemLibraryAssetImportDialog::onImportNearlyFinished);
253     connect(&m_importer, &ItemLibraryAssetImporter::importFinished,
254             this, &ItemLibraryAssetImportDialog::onImportFinished);
255     connect(&m_importer, &ItemLibraryAssetImporter::progressChanged,
256             this, &ItemLibraryAssetImportDialog::setImportProgress);
257 
258     addInfo(tr("Select import options and press \"Import\" to import the following files:"));
259     for (const auto &file : qAsConst(m_quick3DFiles))
260         addInfo(file);
261 
262     QTimer::singleShot(0, [this]() {
263         ui->tabWidget->setMaximumHeight(m_optionsHeight + ui->tabWidget->tabBar()->height() + 10);
264         updateUi();
265     });
266 }
267 
~ItemLibraryAssetImportDialog()268 ItemLibraryAssetImportDialog::~ItemLibraryAssetImportDialog()
269 {
270     delete ui;
271 }
272 
updateImport(const ModelNode & updateNode,const QVariantMap & supportedExts,const QVariantMap & supportedOpts)273 void ItemLibraryAssetImportDialog::updateImport(const ModelNode &updateNode,
274                                                 const QVariantMap &supportedExts,
275                                                 const QVariantMap &supportedOpts)
276 {
277     QString errorMsg;
278     const ModelNode &node = updateNode;
279     if (node.isValid() && node.hasMetaInfo()) {
280         QString compFileName = node.metaInfo().componentFileName(); // absolute path
281         bool preselectNodeSource = false;
282         if (compFileName.isEmpty()) {
283             // Node is not a file component, so we have to check if the current doc itself is
284             compFileName = node.model()->fileUrl().toLocalFile();
285             preselectNodeSource = true;
286         }
287         QFileInfo compFileInfo{compFileName};
288 
289         // Find to top asset folder
290         const QString assetFolder = QLatin1String(Constants::QUICK_3D_ASSETS_FOLDER).mid(1);
291         const QStringList parts = compFileName.split('/');
292         int i = parts.size() - 1;
293         int previousSize = 0;
294         for (; i >= 0; --i) {
295             if (parts[i] == assetFolder)
296                 break;
297             previousSize = parts[i].size();
298         }
299         if (i >= 0) {
300             const QString assetPath = compFileName.left(compFileName.lastIndexOf(assetFolder)
301                                                         + assetFolder.size() + previousSize + 1);
302             const QDir assetDir(assetPath);
303 
304             // Find import options and the original source scene
305             const QString jsonFileName = assetDir.absoluteFilePath(
306                         Constants::QUICK_3D_ASSET_IMPORT_DATA_NAME);
307             QFile jsonFile{jsonFileName};
308             if (jsonFile.open(QIODevice::ReadOnly)) {
309                 QJsonParseError jsonError;
310                 const QByteArray fileData = jsonFile.readAll();
311                 auto jsonDocument = QJsonDocument::fromJson(fileData, &jsonError);
312                 jsonFile.close();
313                 if (jsonError.error == QJsonParseError::NoError) {
314                     QJsonObject jsonObj = jsonDocument.object();
315                     const QJsonObject options = jsonObj.value(
316                                 Constants::QUICK_3D_ASSET_IMPORT_DATA_OPTIONS_KEY).toObject();
317                     QString sourcePath = jsonObj.value(
318                                 Constants::QUICK_3D_ASSET_IMPORT_DATA_SOURCE_KEY).toString();
319                     if (options.isEmpty() || sourcePath.isEmpty()) {
320                         errorMsg = QCoreApplication::translate(
321                                     "ModelNodeOperations",
322                                     "Asset import data file \"%1\" is invalid.").arg(jsonFileName);
323                     } else {
324                         QFileInfo sourceInfo{sourcePath};
325                         if (!sourceInfo.exists()) {
326                             // Unable to find original scene source, launch file dialog to locate it
327                             QString initialPath;
328                             ProjectExplorer::Project *currentProject
329                                     = ProjectExplorer::SessionManager::projectForFile(
330                                         Utils::FilePath::fromString(compFileName));
331                             if (currentProject)
332                                 initialPath = currentProject->projectDirectory().toString();
333                             else
334                                 initialPath = compFileInfo.absolutePath();
335                             QStringList selectedFiles = QFileDialog::getOpenFileNames(
336                                         Core::ICore::dialogParent(),
337                                         tr("Locate 3D Asset \"%1\"").arg(sourceInfo.fileName()),
338                                         initialPath, sourceInfo.fileName());
339                             if (!selectedFiles.isEmpty()
340                                     && QFileInfo{selectedFiles[0]}.fileName() == sourceInfo.fileName()) {
341                                 sourcePath = selectedFiles[0];
342                                 sourceInfo.setFile(sourcePath);
343                             }
344                         }
345                         if (sourceInfo.exists()) {
346                             // In case of a selected node inside an imported component, preselect
347                             // any file pointed to by a "source" property of the node.
348                             QSet<QString> preselectedFiles;
349                             if (preselectNodeSource && updateNode.hasProperty("source")) {
350                                 QString source = updateNode.variantProperty("source").value().toString();
351                                 if (QFileInfo{source}.isRelative())
352                                     source = QDir{compFileInfo.absolutePath()}.absoluteFilePath(source);
353                                 preselectedFiles.insert(source);
354                             }
355                             auto importDlg = new ItemLibraryAssetImportDialog(
356                                         {sourceInfo.absoluteFilePath()},
357                                         node.model()->fileUrl().toLocalFile(),
358                                         supportedExts, supportedOpts, options,
359                                         preselectedFiles, Core::ICore::mainWindow());
360                             importDlg->show();
361 
362                         } else {
363                             errorMsg = QCoreApplication::translate(
364                                         "ModelNodeOperations", "Unable to locate source scene \"%1\".")
365                                     .arg(sourceInfo.fileName());
366                         }
367                     }
368                 } else {
369                     errorMsg = jsonError.errorString();
370                 }
371             } else {
372                 errorMsg = QCoreApplication::translate("ModelNodeOperations",
373                                                        "Opening asset import data file \"%1\" failed.")
374                         .arg(jsonFileName);
375             }
376         } else {
377             errorMsg = QCoreApplication::translate("ModelNodeOperations",
378                                                    "Unable to resolve asset import path.");
379         }
380     }
381 
382     if (!errorMsg.isEmpty()) {
383         QMessageBox::warning(
384                     qobject_cast<QWidget *>(Core::ICore::dialogParent()),
385                     QCoreApplication::translate("ModelNodeOperations", "Import Update Failed"),
386                     QCoreApplication::translate("ModelNodeOperations",
387                                                 "Failed to update import.\nError:\n%1").arg(errorMsg),
388                     QMessageBox::Close);
389     }
390 }
391 
createTab(const QString & tabLabel,int optionsIndex,const QJsonObject & groups)392 void ItemLibraryAssetImportDialog::createTab(const QString &tabLabel, int optionsIndex,
393                                              const QJsonObject &groups)
394 {
395     const int checkBoxColWidth = 18;
396     const int labelMinWidth = 130;
397     const int controlMinWidth = 65;
398     const int columnSpacing = 16;
399     int rowIndex[2] = {0, 0};
400 
401     QJsonObject &options = m_importOptions[optionsIndex];
402 
403     // First index has ungrouped widgets, rest are groups
404     // First item in each real group is group label
405     QVector<QVector<QPair<QWidget *, QWidget *>>> widgets;
406     QHash<QString, int> groupIndexMap;
407     QHash<QString, QPair<QWidget *, QWidget *>> optionToWidgetsMap;
408     QHash<QString, QJsonArray> conditionMap;
409     QHash<QWidget *, QWidget *> conditionalWidgetMap;
410     QHash<QString, QString> optionToGroupMap;
411 
412     auto optionsArea = new QScrollArea(ui->tabWidget);
413     optionsArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
414     auto optionsAreaContents = new QWidget(optionsArea);
415 
416     auto layout = new QGridLayout(optionsAreaContents);
417     layout->setColumnMinimumWidth(0, checkBoxColWidth);
418     layout->setColumnMinimumWidth(1, labelMinWidth);
419     layout->setColumnMinimumWidth(2, controlMinWidth);
420     layout->setColumnMinimumWidth(3, columnSpacing);
421     layout->setColumnMinimumWidth(4, checkBoxColWidth);
422     layout->setColumnMinimumWidth(5, labelMinWidth);
423     layout->setColumnMinimumWidth(6, controlMinWidth);
424     layout->setColumnStretch(0, 0);
425     layout->setColumnStretch(1, 4);
426     layout->setColumnStretch(2, 2);
427     layout->setColumnStretch(3, 0);
428     layout->setColumnStretch(4, 0);
429     layout->setColumnStretch(5, 4);
430     layout->setColumnStretch(6, 2);
431 
432     widgets.append(QVector<QPair<QWidget *, QWidget *>>());
433 
434     for (const auto &group : groups) {
435         const QString name = group.toObject().value("name").toString();
436         const QJsonArray items = group.toObject().value("items").toArray();
437         for (const auto &item : items)
438             optionToGroupMap.insert(item.toString(), name);
439         auto groupLabel = new QLabel(name, optionsAreaContents);
440         QFont labelFont = groupLabel->font();
441         labelFont.setBold(true);
442         groupLabel->setFont(labelFont);
443         widgets.append({{groupLabel, nullptr}});
444         groupIndexMap.insert(name, widgets.size() - 1);
445     }
446 
447     const auto optKeys = options.keys();
448     for (const auto &optKey : optKeys) {
449         QJsonObject optObj = options.value(optKey).toObject();
450         const QString optName = optObj.value("name").toString();
451         const QString optDesc = optObj.value("description").toString();
452         const QString optType = optObj.value("type").toString();
453         QJsonObject optRange = optObj.value("range").toObject();
454         QJsonValue optValue = optObj.value("value");
455         QJsonArray conditions = optObj.value("conditions").toArray();
456 
457         auto *optLabel = new QLabel(optionsAreaContents);
458         optLabel->setText(optName);
459         optLabel->setToolTip(optDesc);
460 
461         QWidget *optControl = nullptr;
462         if (optType == "Boolean") {
463             auto *optCheck = new QCheckBox(optionsAreaContents);
464             optCheck->setChecked(optValue.toBool());
465             optControl = optCheck;
466             QObject::connect(optCheck, &QCheckBox::toggled,
467                              [this, optCheck, optKey, optionsIndex]() {
468                 QJsonObject optObj = m_importOptions[optionsIndex].value(optKey).toObject();
469                 QJsonValue value(optCheck->isChecked());
470                 optObj.insert("value", value);
471                 m_importOptions[optionsIndex].insert(optKey, optObj);
472             });
473         } else if (optType == "Real") {
474             auto *optSpin = new QDoubleSpinBox(optionsAreaContents);
475             double min = -999999999.;
476             double max = 999999999.;
477             double step = 1.;
478             int decimals = 3;
479             if (!optRange.isEmpty()) {
480                 min = optRange.value("minimum").toDouble();
481                 max = optRange.value("maximum").toDouble();
482                 // Ensure step is reasonable for small ranges
483                 double range = max - min;
484                 while (range <= 10.) {
485                     step /= 10.;
486                     range *= 10.;
487                     if (step < 0.02)
488                         ++decimals;
489                 }
490 
491             }
492             optSpin->setRange(min, max);
493             optSpin->setDecimals(decimals);
494             optSpin->setValue(optValue.toDouble());
495             optSpin->setSingleStep(step);
496             optSpin->setMinimumWidth(controlMinWidth);
497             optControl = optSpin;
498             QObject::connect(optSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
499                              [this, optSpin, optKey, optionsIndex]() {
500                 QJsonObject optObj = m_importOptions[optionsIndex].value(optKey).toObject();
501                 QJsonValue value(optSpin->value());
502                 optObj.insert("value", value);
503                 m_importOptions[optionsIndex].insert(optKey, optObj);
504             });
505         } else {
506             qWarning() << __FUNCTION__ << "Unsupported option type:" << optType;
507             continue;
508         }
509         optControl->setToolTip(optDesc);
510 
511         if (!conditions.isEmpty())
512             conditionMap.insert(optKey, conditions);
513 
514         const QString &groupName = optionToGroupMap.value(optKey);
515         if (!groupName.isEmpty() && groupIndexMap.contains(groupName))
516             widgets[groupIndexMap[groupName]].append({optLabel, optControl});
517         else
518             widgets[0].append({optLabel, optControl});
519         optionToWidgetsMap.insert(optKey, {optLabel, optControl});
520     }
521 
522     // Handle conditions
523     auto it = conditionMap.constBegin();
524     while (it != conditionMap.constEnd()) {
525         const QString &option = it.key();
526         const QJsonArray &conditions = it.value();
527         const auto &conWidgets = optionToWidgetsMap.value(option);
528         QWidget *conLabel = conWidgets.first;
529         QWidget *conControl = conWidgets.second;
530         // Currently we only support single condition per option, though the schema allows for
531         // multiple, as no real life option currently has multiple conditions and connections
532         // get complicated if we need to comply to multiple conditions.
533         if (!conditions.isEmpty() && conLabel && conControl) {
534             const auto &conObj = conditions[0].toObject();
535             const QString optItem = conObj.value("property").toString();
536             const auto &optWidgets = optionToWidgetsMap.value(optItem);
537             const QString optMode = conObj.value("mode").toString();
538             const QVariant optValue = conObj.value("value").toVariant();
539             enum class Mode { equals, notEquals, greaterThan, lessThan };
540             Mode mode;
541             if (optMode == "NotEquals")
542                 mode = Mode::notEquals;
543             else if (optMode == "GreaterThan")
544                 mode = Mode::greaterThan;
545             else if (optMode == "LessThan")
546                 mode = Mode::lessThan;
547             else
548                 mode = Mode::equals; // Default to equals
549 
550             if (optWidgets.first && optWidgets.second) {
551                 auto optCb = qobject_cast<QCheckBox *>(optWidgets.second);
552                 auto optSpin = qobject_cast<QDoubleSpinBox *>(optWidgets.second);
553                 if (optCb) {
554                     auto enableConditionally = [optValue](QCheckBox *cb, QWidget *w1,
555                             QWidget *w2, Mode mode) {
556                         bool equals = (mode == Mode::equals) == optValue.toBool();
557                         bool enable = cb->isChecked() == equals;
558                         w1->setEnabled(enable);
559                         w2->setEnabled(enable);
560                     };
561                     enableConditionally(optCb, conLabel, conControl, mode);
562                     if (conditionalWidgetMap.contains(optCb))
563                         conditionalWidgetMap.insert(optCb, nullptr);
564                     else
565                         conditionalWidgetMap.insert(optCb, conControl);
566                     QObject::connect(
567                                 optCb, &QCheckBox::toggled,
568                                 [optCb, conLabel, conControl, mode, enableConditionally]() {
569                         enableConditionally(optCb, conLabel, conControl, mode);
570                     });
571                 }
572                 if (optSpin) {
573                     auto enableConditionally = [optValue](QDoubleSpinBox *sb, QWidget *w1,
574                             QWidget *w2, Mode mode) {
575                         bool enable = false;
576                         double value = optValue.toDouble();
577                         if (mode == Mode::equals)
578                             enable = qFuzzyCompare(value, sb->value());
579                         else if (mode == Mode::notEquals)
580                             enable = !qFuzzyCompare(value, sb->value());
581                         else if (mode == Mode::greaterThan)
582                             enable = sb->value() > value;
583                         else if (mode == Mode::lessThan)
584                             enable = sb->value() < value;
585                         w1->setEnabled(enable);
586                         w2->setEnabled(enable);
587                     };
588                     enableConditionally(optSpin, conLabel, conControl, mode);
589                     QObject::connect(
590                                 optSpin, QOverload<double>::of(&QDoubleSpinBox::valueChanged),
591                                 [optSpin, conLabel, conControl, mode, enableConditionally]() {
592                         enableConditionally(optSpin, conLabel, conControl, mode);
593                     });
594                 }
595             }
596         }
597         ++it;
598     }
599 
600     // Combine options where a non-boolean option depends on a boolean option that no other
601     // option depends on
602     auto condIt = conditionalWidgetMap.constBegin();
603     while (condIt != conditionalWidgetMap.constEnd()) {
604         if (condIt.value()) {
605             // Find and fix widget pairs
606             for (int i = 0; i < widgets.size(); ++i) {
607                 auto &groupWidgets = widgets[i];
608                 auto widgetIt = groupWidgets.begin();
609                 while (widgetIt != groupWidgets.end()) {
610                     if (widgetIt->second == condIt.value()
611                             && !qobject_cast<QCheckBox *>(condIt.value())) {
612                         if (widgetIt->first)
613                             widgetIt->first->hide();
614                         groupWidgets.erase(widgetIt);
615                     } else {
616                         ++widgetIt;
617                     }
618                 }
619                 // If group was left with less than two actual members, disband the group
620                 // and move the remaining member to ungrouped options
621                 // Note: <= 2 instead of < 2 because each group has group label member
622                 if (i != 0 && groupWidgets.size() <= 2) {
623                     widgets[0].prepend(groupWidgets[1]);
624                     groupWidgets[0].first->hide(); // hide group label
625                     groupWidgets.clear();
626                 }
627             }
628         }
629         ++condIt;
630     }
631 
632     auto incrementColIndex = [&](int col) {
633         layout->setRowMinimumHeight(rowIndex[col], rowHeight);
634         ++rowIndex[col];
635     };
636 
637     auto insertOptionToLayout = [&](int col, const QPair<QWidget *, QWidget *> &optionWidgets) {
638         layout->addWidget(optionWidgets.first, rowIndex[col], col * 4 + 1, 1, 2);
639         int adj = qobject_cast<QCheckBox *>(optionWidgets.second) ? 0 : 2;
640         layout->addWidget(optionWidgets.second, rowIndex[col], col * 4 + adj);
641         if (!adj) {
642             // Check box option may have additional conditional value field
643             QWidget *condWidget = conditionalWidgetMap.value(optionWidgets.second);
644             if (condWidget)
645                 layout->addWidget(condWidget, rowIndex[col], col * 4 + 2);
646         }
647         incrementColIndex(col);
648     };
649 
650     if (widgets.size() == 1 && widgets[0].isEmpty()) {
651         layout->addWidget(new QLabel(tr("No options available for this type."),
652                                      optionsAreaContents), 0, 0, 2, 7, Qt::AlignCenter);
653         incrementColIndex(0);
654         incrementColIndex(0);
655     }
656 
657     // Add option widgets to layout. Grouped options are added to the tops of the columns
658     for (int i = 1; i < widgets.size(); ++i) {
659         int col = rowIndex[1] < rowIndex[0] ? 1 : 0;
660         const auto &groupWidgets = widgets[i];
661         if (!groupWidgets.isEmpty()) {
662             // First widget in each group is the group label
663             layout->addWidget(groupWidgets[0].first, rowIndex[col], col * 4, 1, 3);
664             incrementColIndex(col);
665             for (int j = 1; j < groupWidgets.size(); ++j)
666                 insertOptionToLayout(col, groupWidgets[j]);
667             // Add a separator line after each group
668             auto *separator = new QFrame(optionsAreaContents);
669             separator->setMaximumHeight(1);
670             separator->setFrameShape(QFrame::HLine);
671             separator->setFrameShadow(QFrame::Sunken);
672             separator->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed);
673             layout->addWidget(separator, rowIndex[col], col * 4, 1, 3);
674             incrementColIndex(col);
675         }
676     }
677 
678     // Ungrouped options are spread evenly under the groups
679     int totalRowCount = (rowIndex[0] + rowIndex[1] + widgets[0].size() + 1) / 2;
680     for (const auto &rowWidgets : qAsConst(widgets[0])) {
681         int col = rowIndex[0] < totalRowCount ? 0 : 1;
682         insertOptionToLayout(col, rowWidgets);
683     }
684 
685     int optionRows = qMax(rowIndex[0], rowIndex[1]);
686     m_optionsRows = qMax(m_optionsRows, optionRows);
687     m_optionsHeight = qMax(rowHeight * optionRows + 16, m_optionsHeight);
688     layout->setContentsMargins(8, 8, 8, 8);
689     optionsAreaContents->setContentsMargins(0, 0, 0, 0);
690     optionsAreaContents->setLayout(layout);
691     optionsAreaContents->setMinimumWidth(
692                 (checkBoxColWidth + labelMinWidth + controlMinWidth) * 2  + columnSpacing);
693     optionsAreaContents->setObjectName("optionsAreaContents"); // For stylesheet
694 
695     optionsArea->setWidget(optionsAreaContents);
696     optionsArea->setStyleSheet("QScrollArea {background-color: transparent}");
697     optionsAreaContents->setStyleSheet(
698                 "QWidget#optionsAreaContents {background-color: transparent}");
699 
700     ui->tabWidget->addTab(optionsArea, tr("%1 options").arg(tabLabel));
701 }
702 
updateUi()703 void ItemLibraryAssetImportDialog::updateUi()
704 {
705     auto optionsArea = qobject_cast<QScrollArea *>(ui->tabWidget->currentWidget());
706     if (optionsArea) {
707         auto optionsAreaContents = optionsArea->widget();
708         int scrollBarWidth = optionsArea->verticalScrollBar()->isVisible()
709                 ? optionsArea->verticalScrollBar()->width() : 0;
710         optionsAreaContents->resize(optionsArea->contentsRect().width()
711                                     - scrollBarWidth - 8, m_optionsHeight);
712     }
713 }
714 
resizeEvent(QResizeEvent * event)715 void ItemLibraryAssetImportDialog::resizeEvent(QResizeEvent *event)
716 {
717     Q_UNUSED(event)
718     updateUi();
719 }
720 
setCloseButtonState(bool importing)721 void ItemLibraryAssetImportDialog::setCloseButtonState(bool importing)
722 {
723     ui->buttonBox->button(QDialogButtonBox::Close)->setEnabled(true);
724     ui->buttonBox->button(QDialogButtonBox::Close)->setText(importing ? tr("Cancel") : tr("Close"));
725 }
726 
addError(const QString & error,const QString & srcPath)727 void ItemLibraryAssetImportDialog::addError(const QString &error, const QString &srcPath)
728 {
729     addFormattedMessage(m_outputFormatter, error, srcPath, Utils::StdErrFormat);
730 }
731 
addWarning(const QString & warning,const QString & srcPath)732 void ItemLibraryAssetImportDialog::addWarning(const QString &warning, const QString &srcPath)
733 {
734     addFormattedMessage(m_outputFormatter, warning, srcPath, Utils::StdOutFormat);
735 }
736 
addInfo(const QString & info,const QString & srcPath)737 void ItemLibraryAssetImportDialog::addInfo(const QString &info, const QString &srcPath)
738 {
739     addFormattedMessage(m_outputFormatter, info, srcPath, Utils::NormalMessageFormat);
740 }
741 
onImport()742 void ItemLibraryAssetImportDialog::onImport()
743 {
744     ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false);
745     setCloseButtonState(true);
746     ui->progressBar->setValue(0);
747 
748     if (!m_quick3DFiles.isEmpty()) {
749         m_importer.importQuick3D(m_quick3DFiles, m_quick3DImportPath,
750                                  m_importOptions, m_extToImportOptionsMap,
751                                  m_preselectedFilesForOverwrite);
752     }
753 }
754 
setImportProgress(int value,const QString & text)755 void ItemLibraryAssetImportDialog::setImportProgress(int value, const QString &text)
756 {
757     ui->progressLabel->setText(text);
758     if (value < 0)
759         ui->progressBar->setRange(0, 0);
760     else
761         ui->progressBar->setRange(0, 100);
762     ui->progressBar->setValue(value);
763 }
764 
onImportNearlyFinished()765 void ItemLibraryAssetImportDialog::onImportNearlyFinished()
766 {
767     // Canceling import is no longer doable
768     ui->buttonBox->button(QDialogButtonBox::Close)->setEnabled(false);
769 }
770 
onImportFinished()771 void ItemLibraryAssetImportDialog::onImportFinished()
772 {
773     setCloseButtonState(false);
774     if (m_importer.isCancelled()) {
775         QString interruptStr = tr("Import interrupted.");
776         addError(interruptStr);
777         setImportProgress(0, interruptStr);
778     } else {
779         QString doneStr = tr("Import done.");
780         addInfo(doneStr);
781         setImportProgress(100, doneStr);
782     }
783 }
784 
onClose()785 void ItemLibraryAssetImportDialog::onClose()
786 {
787     if (m_importer.isImporting()) {
788         addInfo(tr("Canceling import."));
789         m_importer.cancelImport();
790     } else {
791         reject();
792         close();
793         deleteLater();
794     }
795 }
796 
797 }
798