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