From 062a0a9dbd8391accdefccd2f9a43d4190b08424 Mon Sep 17 00:00:00 2001 From: Miikka Heikkinen Date: Thu, 10 Oct 2019 12:18:38 +0300 Subject: [PATCH] Implement support for UIA/UIP importing Each different type of import file gets its own options tab in import dialog. Item library icon for each imported component is generated at import time depending on component root element type. Change-Id: I95824b43d251d02d8e032b6a3e45a18d1d509b80 Fixes: QDS-1152 Reviewed-by: Mahmoud Badri Reviewed-by: Thomas Hartmann --- .../itemlibraryassetimportdialog.cpp | 662 ++++++++++-------- .../itemlibraryassetimportdialog.h | 8 +- .../itemlibraryassetimportdialog.ui | 43 +- .../itemlibrary/itemlibraryassetimporter.cpp | 92 ++- .../itemlibrary/itemlibraryassetimporter.h | 7 +- .../itemlibrary/itemlibrarywidget.cpp | 32 +- .../metainfo/subcomponentmanager.cpp | 35 +- .../qmldesigner/qmldesignerconstants.h | 2 + 8 files changed, 502 insertions(+), 379 deletions(-) diff --git a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.cpp b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.cpp index f1eb737aed2..bfdbfe5c224 100644 --- a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.cpp +++ b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.cpp @@ -44,6 +44,7 @@ #include #include #include +#include namespace QmlDesigner { @@ -62,6 +63,8 @@ static void addFormattedMessage(Utils::OutputFormatter *formatter, const QString formatter->plainTextEdit()->verticalScrollBar()->maximum()); } +static const int rowHeight = 26; + } ItemLibraryAssetImportDialog::ItemLibraryAssetImportDialog(const QStringList &importFiles, @@ -140,301 +143,67 @@ ItemLibraryAssetImportDialog::ItemLibraryAssetImportDialog(const QStringList &im } m_quick3DImportPath = candidatePath; - // Create UI controls for options - if (!importFiles.isEmpty()) { - QJsonObject supportedOptions = QJsonObject::fromVariantMap( - m_importer.supportedOptions(importFiles[0])); - m_importOptions = supportedOptions.value("options").toObject(); - const QJsonObject groups = supportedOptions.value("groups").toObject(); + if (!m_quick3DFiles.isEmpty()) { + const QHash allOptions = m_importer.allOptions(); + const QHash supportedExtensions = m_importer.supportedExtensions(); + QVector groups; - const int checkBoxColWidth = 18; - const int labelMinWidth = 130; - const int controlMinWidth = 65; - const int columnSpacing = 16; - const int rowHeight = 26; - int rowIndex[2] = {0, 0}; - - // First index has ungrouped widgets, rest are groups - // First item in each real group is group label - QVector>> widgets; - QHash groupIndexMap; - QHash> optionToWidgetsMap; - QHash conditionMap; - QHash conditionalWidgetMap; - QHash optionToGroupMap; - - auto layout = new QGridLayout(ui->optionsAreaContents); - layout->setColumnMinimumWidth(0, checkBoxColWidth); - layout->setColumnMinimumWidth(1, labelMinWidth); - layout->setColumnMinimumWidth(2, controlMinWidth); - layout->setColumnMinimumWidth(3, columnSpacing); - layout->setColumnMinimumWidth(4, checkBoxColWidth); - layout->setColumnMinimumWidth(5, labelMinWidth); - layout->setColumnMinimumWidth(6, controlMinWidth); - layout->setColumnStretch(0, 0); - layout->setColumnStretch(1, 4); - layout->setColumnStretch(2, 2); - layout->setColumnStretch(3, 0); - layout->setColumnStretch(4, 0); - layout->setColumnStretch(5, 4); - layout->setColumnStretch(6, 2); - - widgets.append(QVector>()); - - for (const auto group : groups) { - const QString name = group.toObject().value("name").toString(); - const QJsonArray items = group.toObject().value("items").toArray(); - for (const auto item : items) - optionToGroupMap.insert(item.toString(), name); - auto groupLabel = new QLabel(name, ui->optionsAreaContents); - QFont labelFont = groupLabel->font(); - labelFont.setBold(true); - groupLabel->setFont(labelFont); - widgets.append({{groupLabel, nullptr}}); - groupIndexMap.insert(name, widgets.size() - 1); + auto optIt = allOptions.constBegin(); + int optIndex = 0; + while (optIt != allOptions.constEnd()) { + QJsonObject options = QJsonObject::fromVariantMap(optIt.value()); + m_importOptions << options.value("options").toObject(); + groups << options.value("groups").toObject(); + const auto &exts = optIt.key().split(':'); + for (const auto &ext : exts) + m_extToImportOptionsMap.insert(ext, optIndex); + ++optIt; + ++optIndex; } - const auto optKeys = m_importOptions.keys(); - for (const auto &optKey : optKeys) { - QJsonObject optObj = m_importOptions.value(optKey).toObject(); - const QString optName = optObj.value("name").toString(); - const QString optDesc = optObj.value("description").toString(); - const QString optType = optObj.value("type").toString(); - QJsonObject optRange = optObj.value("range").toObject(); - QJsonValue optValue = optObj.value("value"); - QJsonArray conditions = optObj.value("conditions").toArray(); - - QWidget *optControl = nullptr; - if (optType == "Boolean") { - auto *optCheck = new QCheckBox(ui->optionsAreaContents); - optCheck->setChecked(optValue.toBool()); - optControl = optCheck; - QObject::connect(optCheck, &QCheckBox::toggled, [this, optCheck, optKey]() { - QJsonObject optObj = m_importOptions.value(optKey).toObject(); - QJsonValue value(optCheck->isChecked()); - optObj.insert("value", value); - m_importOptions.insert(optKey, optObj); - }); - } else if (optType == "Real") { - auto *optSpin = new QDoubleSpinBox(ui->optionsAreaContents); - double min = -999999999.; - double max = 999999999.; - double step = 1.; - int decimals = 3; - if (!optRange.isEmpty()) { - min = optRange.value("minimum").toDouble(); - max = optRange.value("maximum").toDouble(); - // Ensure step is reasonable for small ranges - double range = max - min; - while (range <= 10.) { - step /= 10.; - range *= 10.; - if (step < 0.02) - ++decimals; - } - + // Create tab for each supported extension group that also has files included in the import + QMap tabMap; // QMap used for alphabetical order + for (const auto &file : qAsConst(m_quick3DFiles)) { + auto extIt = supportedExtensions.constBegin(); + QString ext = QFileInfo(file).suffix(); + while (extIt != supportedExtensions.constEnd()) { + if (!tabMap.contains(extIt.key()) && extIt.value().contains(ext)) { + tabMap.insert(extIt.key(), m_extToImportOptionsMap.value(ext)); + break; } - optSpin->setRange(min, max); - optSpin->setDecimals(decimals); - optSpin->setValue(optValue.toDouble()); - optSpin->setSingleStep(step); - optSpin->setMinimumWidth(controlMinWidth); - optControl = optSpin; - QObject::connect(optSpin, QOverload::of(&QDoubleSpinBox::valueChanged), - [this, optSpin, optKey]() { - QJsonObject optObj = m_importOptions.value(optKey).toObject(); - QJsonValue value(optSpin->value()); - optObj.insert("value", value); - m_importOptions.insert(optKey, optObj); - }); - } else { - qWarning() << __FUNCTION__ << "Unsupported option type:" << optType; - continue; + ++extIt; } - - if (!conditions.isEmpty()) - conditionMap.insert(optKey, conditions); - - auto *optLabel = new QLabel(ui->optionsAreaContents); - optLabel->setText(optName); - optLabel->setToolTip(optDesc); - optControl->setToolTip(optDesc); - - const QString &groupName = optionToGroupMap.value(optKey); - if (!groupName.isEmpty() && groupIndexMap.contains(groupName)) - widgets[groupIndexMap[groupName]].append({optLabel, optControl}); - else - widgets[0].append({optLabel, optControl}); - optionToWidgetsMap.insert(optKey, {optLabel, optControl}); } - // Handle conditions - auto it = conditionMap.constBegin(); - while (it != conditionMap.constEnd()) { - const QString &option = it.key(); - const QJsonArray &conditions = it.value(); - const auto &conWidgets = optionToWidgetsMap.value(option); - QWidget *conLabel = conWidgets.first; - QWidget *conControl = conWidgets.second; - // Currently we only support single condition per option, though the schema allows for - // multiple, as no real life option currently has multiple conditions and connections - // get complicated if we need to comply to multiple conditions. - if (!conditions.isEmpty() && conLabel && conControl) { - const auto &conObj = conditions[0].toObject(); - const QString optItem = conObj.value("property").toString(); - const auto &optWidgets = optionToWidgetsMap.value(optItem); - const QString optMode = conObj.value("mode").toString(); - const QVariant optValue = conObj.value("value").toVariant(); - enum class Mode { equals, notEquals, greaterThan, lessThan }; - Mode mode; - if (optMode == "NotEquals") - mode = Mode::notEquals; - else if (optMode == "GreaterThan") - mode = Mode::greaterThan; - else if (optMode == "LessThan") - mode = Mode::lessThan; - else - mode = Mode::equals; // Default to equals + ui->tabWidget->clear(); + auto tabIt = tabMap.constBegin(); + while (tabIt != tabMap.constEnd()) { + createTab(tabIt.key(), tabIt.value(), groups[tabIt.value()]); + ++tabIt; + } - if (optWidgets.first && optWidgets.second) { - auto optCb = qobject_cast(optWidgets.second); - auto optSpin = qobject_cast(optWidgets.second); - if (optCb) { - auto enableConditionally = [optValue](QCheckBox *cb, QWidget *w1, - QWidget *w2, Mode mode) { - bool equals = (mode == Mode::equals) == optValue.toBool(); - bool enable = cb->isChecked() == equals; - w1->setEnabled(enable); - w2->setEnabled(enable); - }; - enableConditionally(optCb, conLabel, conControl, mode); - if (conditionalWidgetMap.contains(optCb)) - conditionalWidgetMap.insert(optCb, nullptr); - else - conditionalWidgetMap.insert(optCb, conControl); - QObject::connect( - optCb, &QCheckBox::toggled, - [optCb, conLabel, conControl, mode, enableConditionally]() { - enableConditionally(optCb, conLabel, conControl, mode); - }); - } - if (optSpin) { - auto enableConditionally = [optValue](QDoubleSpinBox *sb, QWidget *w1, - QWidget *w2, Mode mode) { - bool enable = false; - double value = optValue.toDouble(); - if (mode == Mode::equals) - enable = qFuzzyCompare(value, sb->value()); - else if (mode == Mode::notEquals) - enable = !qFuzzyCompare(value, sb->value()); - else if (mode == Mode::greaterThan) - enable = sb->value() > value; - else if (mode == Mode::lessThan) - enable = sb->value() < value; - w1->setEnabled(enable); - w2->setEnabled(enable); - }; - enableConditionally(optSpin, conLabel, conControl, mode); - QObject::connect( - optSpin, QOverload::of(&QDoubleSpinBox::valueChanged), - [optSpin, conLabel, conControl, mode, enableConditionally]() { - enableConditionally(optSpin, conLabel, conControl, mode); - }); + // Pad all tabs to same height + for (int i = 0; i < ui->tabWidget->count(); ++i) { + auto optionsArea = qobject_cast(ui->tabWidget->widget(i)); + if (optionsArea && optionsArea->widget()) { + auto grid = qobject_cast(optionsArea->widget()->layout()); + if (grid) { + int rows = grid->rowCount(); + for (int j = rows; j < m_optionsRows; ++j) { + grid->addWidget(new QWidget(optionsArea->widget()), j, 0); + grid->setRowMinimumHeight(j, rowHeight); } } } - ++it; } - // Combine options where a non-boolean option depends on a boolean option that no other - // option depends on - auto condIt = conditionalWidgetMap.constBegin(); - while (condIt != conditionalWidgetMap.constEnd()) { - if (condIt.value()) { - // Find and fix widget pairs - for (int i = 0; i < widgets.size(); ++i) { - auto &groupWidgets = widgets[i]; - auto widgetIt = groupWidgets.begin(); - while (widgetIt != groupWidgets.end()) { - if (widgetIt->second == condIt.value()) { - if (widgetIt->first) - widgetIt->first->hide(); - groupWidgets.erase(widgetIt); - } else { - ++widgetIt; - } - } - // If group was left with less than two actual members, disband the group - // and move the remaining member to ungrouped options - // Note: <= 2 instead of < 2 because each group has group label member - if (i != 0 && groupWidgets.size() <= 2) { - widgets[0].prepend(groupWidgets[1]); - groupWidgets[0].first->hide(); // hide group label - groupWidgets.clear(); - } - } - } - ++condIt; - } - - auto incrementColIndex = [&](int col) { - layout->setRowMinimumHeight(rowIndex[col], rowHeight); - ++rowIndex[col]; - }; - - auto insertOptionToLayout = [&](int col, const QPair &optionWidgets) { - layout->addWidget(optionWidgets.first, rowIndex[col], col * 4 + 1, 1, 2); - int adj = qobject_cast(optionWidgets.second) ? 0 : 2; - layout->addWidget(optionWidgets.second, rowIndex[col], col * 4 + adj); - if (!adj) { - // Check box option may have additional conditional value field - QWidget *condWidget = conditionalWidgetMap.value(optionWidgets.second); - if (condWidget) - layout->addWidget(condWidget, rowIndex[col], col * 4 + 2); - } - incrementColIndex(col); - }; - - // Add option widgets to layout. Grouped options are added to the tops of the columns - for (int i = 1; i < widgets.size(); ++i) { - int col = rowIndex[1] < rowIndex[0] ? 1 : 0; - const auto &groupWidgets = widgets[i]; - if (!groupWidgets.isEmpty()) { - // First widget in each group is the group label - layout->addWidget(groupWidgets[0].first, rowIndex[col], col * 4, 1, 3); - incrementColIndex(col); - for (int j = 1; j < groupWidgets.size(); ++j) - insertOptionToLayout(col, groupWidgets[j]); - // Add a separator line after each group - auto *separator = new QFrame(ui->optionsAreaContents); - separator->setMaximumHeight(1); - separator->setFrameShape(QFrame::HLine); - separator->setFrameShadow(QFrame::Sunken); - separator->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); - layout->addWidget(separator, rowIndex[col], col * 4, 1, 3); - incrementColIndex(col); - } - } - - // Ungrouped options are spread evenly under the groups - int totalRowCount = (rowIndex[0] + rowIndex[1] + widgets[0].size() + 1) / 2; - for (const auto &rowWidgets : qAsConst(widgets[0])) { - int col = rowIndex[0] < totalRowCount ? 0 : 1; - insertOptionToLayout(col, rowWidgets); - } - - ui->optionsAreaContents->setLayout(layout); - ui->optionsAreaContents->setMinimumSize( - checkBoxColWidth * 2 + labelMinWidth * 2 + controlMinWidth * 2 + columnSpacing, - rowHeight * qMax(rowIndex[0], rowIndex[1])); + ui->tabWidget->setCurrentIndex(0); } - ui->optionsArea->setStyleSheet("QScrollArea {background-color: transparent}"); - ui->optionsAreaContents->setStyleSheet( - "QWidget#optionsAreaContents {background-color: transparent}"); - connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, this, &ItemLibraryAssetImportDialog::onClose); + connect(ui->tabWidget, &QTabWidget::currentChanged, + this, &ItemLibraryAssetImportDialog::updateUi); connect(&m_importer, &ItemLibraryAssetImporter::errorReported, this, &ItemLibraryAssetImportDialog::addError); @@ -454,7 +223,8 @@ ItemLibraryAssetImportDialog::ItemLibraryAssetImportDialog(const QStringList &im addInfo(file); QTimer::singleShot(0, [this]() { - resizeEvent(nullptr); + ui->tabWidget->setMaximumHeight(m_optionsHeight + ui->tabWidget->tabBar()->height() + 10); + updateUi(); }); } @@ -463,16 +233,333 @@ ItemLibraryAssetImportDialog::~ItemLibraryAssetImportDialog() delete ui; } +void ItemLibraryAssetImportDialog::createTab(const QString &tabLabel, int optionsIndex, + const QJsonObject &groups) +{ + const int checkBoxColWidth = 18; + const int labelMinWidth = 130; + const int controlMinWidth = 65; + const int columnSpacing = 16; + int rowIndex[2] = {0, 0}; + + QJsonObject &options = m_importOptions[optionsIndex]; + + // First index has ungrouped widgets, rest are groups + // First item in each real group is group label + QVector>> widgets; + QHash groupIndexMap; + QHash> optionToWidgetsMap; + QHash conditionMap; + QHash conditionalWidgetMap; + QHash optionToGroupMap; + + auto optionsArea = new QScrollArea(ui->tabWidget); + optionsArea->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff); + auto optionsAreaContents = new QWidget(optionsArea); + + auto layout = new QGridLayout(optionsAreaContents); + layout->setColumnMinimumWidth(0, checkBoxColWidth); + layout->setColumnMinimumWidth(1, labelMinWidth); + layout->setColumnMinimumWidth(2, controlMinWidth); + layout->setColumnMinimumWidth(3, columnSpacing); + layout->setColumnMinimumWidth(4, checkBoxColWidth); + layout->setColumnMinimumWidth(5, labelMinWidth); + layout->setColumnMinimumWidth(6, controlMinWidth); + layout->setColumnStretch(0, 0); + layout->setColumnStretch(1, 4); + layout->setColumnStretch(2, 2); + layout->setColumnStretch(3, 0); + layout->setColumnStretch(4, 0); + layout->setColumnStretch(5, 4); + layout->setColumnStretch(6, 2); + + widgets.append(QVector>()); + + for (const auto group : groups) { + const QString name = group.toObject().value("name").toString(); + const QJsonArray items = group.toObject().value("items").toArray(); + for (const auto item : items) + optionToGroupMap.insert(item.toString(), name); + auto groupLabel = new QLabel(name, optionsAreaContents); + QFont labelFont = groupLabel->font(); + labelFont.setBold(true); + groupLabel->setFont(labelFont); + widgets.append({{groupLabel, nullptr}}); + groupIndexMap.insert(name, widgets.size() - 1); + } + + const auto optKeys = options.keys(); + for (const auto &optKey : optKeys) { + QJsonObject optObj = options.value(optKey).toObject(); + const QString optName = optObj.value("name").toString(); + const QString optDesc = optObj.value("description").toString(); + const QString optType = optObj.value("type").toString(); + QJsonObject optRange = optObj.value("range").toObject(); + QJsonValue optValue = optObj.value("value"); + QJsonArray conditions = optObj.value("conditions").toArray(); + + QWidget *optControl = nullptr; + if (optType == "Boolean") { + auto *optCheck = new QCheckBox(optionsAreaContents); + optCheck->setChecked(optValue.toBool()); + optControl = optCheck; + QObject::connect(optCheck, &QCheckBox::toggled, + [this, optCheck, optKey, optionsIndex]() { + QJsonObject optObj = m_importOptions[optionsIndex].value(optKey).toObject(); + QJsonValue value(optCheck->isChecked()); + optObj.insert("value", value); + m_importOptions[optionsIndex].insert(optKey, optObj); + }); + } else if (optType == "Real") { + auto *optSpin = new QDoubleSpinBox(optionsAreaContents); + double min = -999999999.; + double max = 999999999.; + double step = 1.; + int decimals = 3; + if (!optRange.isEmpty()) { + min = optRange.value("minimum").toDouble(); + max = optRange.value("maximum").toDouble(); + // Ensure step is reasonable for small ranges + double range = max - min; + while (range <= 10.) { + step /= 10.; + range *= 10.; + if (step < 0.02) + ++decimals; + } + + } + optSpin->setRange(min, max); + optSpin->setDecimals(decimals); + optSpin->setValue(optValue.toDouble()); + optSpin->setSingleStep(step); + optSpin->setMinimumWidth(controlMinWidth); + optControl = optSpin; + QObject::connect(optSpin, QOverload::of(&QDoubleSpinBox::valueChanged), + [this, optSpin, optKey, optionsIndex]() { + QJsonObject optObj = m_importOptions[optionsIndex].value(optKey).toObject(); + QJsonValue value(optSpin->value()); + optObj.insert("value", value); + m_importOptions[optionsIndex].insert(optKey, optObj); + }); + } else { + qWarning() << __FUNCTION__ << "Unsupported option type:" << optType; + continue; + } + + if (!conditions.isEmpty()) + conditionMap.insert(optKey, conditions); + + auto *optLabel = new QLabel(optionsAreaContents); + optLabel->setText(optName); + optLabel->setToolTip(optDesc); + optControl->setToolTip(optDesc); + + const QString &groupName = optionToGroupMap.value(optKey); + if (!groupName.isEmpty() && groupIndexMap.contains(groupName)) + widgets[groupIndexMap[groupName]].append({optLabel, optControl}); + else + widgets[0].append({optLabel, optControl}); + optionToWidgetsMap.insert(optKey, {optLabel, optControl}); + } + + // Handle conditions + auto it = conditionMap.constBegin(); + while (it != conditionMap.constEnd()) { + const QString &option = it.key(); + const QJsonArray &conditions = it.value(); + const auto &conWidgets = optionToWidgetsMap.value(option); + QWidget *conLabel = conWidgets.first; + QWidget *conControl = conWidgets.second; + // Currently we only support single condition per option, though the schema allows for + // multiple, as no real life option currently has multiple conditions and connections + // get complicated if we need to comply to multiple conditions. + if (!conditions.isEmpty() && conLabel && conControl) { + const auto &conObj = conditions[0].toObject(); + const QString optItem = conObj.value("property").toString(); + const auto &optWidgets = optionToWidgetsMap.value(optItem); + const QString optMode = conObj.value("mode").toString(); + const QVariant optValue = conObj.value("value").toVariant(); + enum class Mode { equals, notEquals, greaterThan, lessThan }; + Mode mode; + if (optMode == "NotEquals") + mode = Mode::notEquals; + else if (optMode == "GreaterThan") + mode = Mode::greaterThan; + else if (optMode == "LessThan") + mode = Mode::lessThan; + else + mode = Mode::equals; // Default to equals + + if (optWidgets.first && optWidgets.second) { + auto optCb = qobject_cast(optWidgets.second); + auto optSpin = qobject_cast(optWidgets.second); + if (optCb) { + auto enableConditionally = [optValue](QCheckBox *cb, QWidget *w1, + QWidget *w2, Mode mode) { + bool equals = (mode == Mode::equals) == optValue.toBool(); + bool enable = cb->isChecked() == equals; + w1->setEnabled(enable); + w2->setEnabled(enable); + }; + enableConditionally(optCb, conLabel, conControl, mode); + if (conditionalWidgetMap.contains(optCb)) + conditionalWidgetMap.insert(optCb, nullptr); + else + conditionalWidgetMap.insert(optCb, conControl); + QObject::connect( + optCb, &QCheckBox::toggled, + [optCb, conLabel, conControl, mode, enableConditionally]() { + enableConditionally(optCb, conLabel, conControl, mode); + }); + } + if (optSpin) { + auto enableConditionally = [optValue](QDoubleSpinBox *sb, QWidget *w1, + QWidget *w2, Mode mode) { + bool enable = false; + double value = optValue.toDouble(); + if (mode == Mode::equals) + enable = qFuzzyCompare(value, sb->value()); + else if (mode == Mode::notEquals) + enable = !qFuzzyCompare(value, sb->value()); + else if (mode == Mode::greaterThan) + enable = sb->value() > value; + else if (mode == Mode::lessThan) + enable = sb->value() < value; + w1->setEnabled(enable); + w2->setEnabled(enable); + }; + enableConditionally(optSpin, conLabel, conControl, mode); + QObject::connect( + optSpin, QOverload::of(&QDoubleSpinBox::valueChanged), + [optSpin, conLabel, conControl, mode, enableConditionally]() { + enableConditionally(optSpin, conLabel, conControl, mode); + }); + } + } + } + ++it; + } + + // Combine options where a non-boolean option depends on a boolean option that no other + // option depends on + auto condIt = conditionalWidgetMap.constBegin(); + while (condIt != conditionalWidgetMap.constEnd()) { + if (condIt.value()) { + // Find and fix widget pairs + for (int i = 0; i < widgets.size(); ++i) { + auto &groupWidgets = widgets[i]; + auto widgetIt = groupWidgets.begin(); + while (widgetIt != groupWidgets.end()) { + if (widgetIt->second == condIt.value() + && !qobject_cast(condIt.value())) { + if (widgetIt->first) + widgetIt->first->hide(); + groupWidgets.erase(widgetIt); + } else { + ++widgetIt; + } + } + // If group was left with less than two actual members, disband the group + // and move the remaining member to ungrouped options + // Note: <= 2 instead of < 2 because each group has group label member + if (i != 0 && groupWidgets.size() <= 2) { + widgets[0].prepend(groupWidgets[1]); + groupWidgets[0].first->hide(); // hide group label + groupWidgets.clear(); + } + } + } + ++condIt; + } + + auto incrementColIndex = [&](int col) { + layout->setRowMinimumHeight(rowIndex[col], rowHeight); + ++rowIndex[col]; + }; + + auto insertOptionToLayout = [&](int col, const QPair &optionWidgets) { + layout->addWidget(optionWidgets.first, rowIndex[col], col * 4 + 1, 1, 2); + int adj = qobject_cast(optionWidgets.second) ? 0 : 2; + layout->addWidget(optionWidgets.second, rowIndex[col], col * 4 + adj); + if (!adj) { + // Check box option may have additional conditional value field + QWidget *condWidget = conditionalWidgetMap.value(optionWidgets.second); + if (condWidget) + layout->addWidget(condWidget, rowIndex[col], col * 4 + 2); + } + incrementColIndex(col); + }; + + if (widgets.size() == 1 && widgets[0].isEmpty()) { + layout->addWidget(new QLabel(tr("No options available for this type."), + optionsAreaContents), 0, 0, 2, 7, Qt::AlignCenter); + incrementColIndex(0); + incrementColIndex(0); + } + + // Add option widgets to layout. Grouped options are added to the tops of the columns + for (int i = 1; i < widgets.size(); ++i) { + int col = rowIndex[1] < rowIndex[0] ? 1 : 0; + const auto &groupWidgets = widgets[i]; + if (!groupWidgets.isEmpty()) { + // First widget in each group is the group label + layout->addWidget(groupWidgets[0].first, rowIndex[col], col * 4, 1, 3); + incrementColIndex(col); + for (int j = 1; j < groupWidgets.size(); ++j) + insertOptionToLayout(col, groupWidgets[j]); + // Add a separator line after each group + auto *separator = new QFrame(optionsAreaContents); + separator->setMaximumHeight(1); + separator->setFrameShape(QFrame::HLine); + separator->setFrameShadow(QFrame::Sunken); + separator->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Fixed); + layout->addWidget(separator, rowIndex[col], col * 4, 1, 3); + incrementColIndex(col); + } + } + + // Ungrouped options are spread evenly under the groups + int totalRowCount = (rowIndex[0] + rowIndex[1] + widgets[0].size() + 1) / 2; + for (const auto &rowWidgets : qAsConst(widgets[0])) { + int col = rowIndex[0] < totalRowCount ? 0 : 1; + insertOptionToLayout(col, rowWidgets); + } + + int optionRows = qMax(rowIndex[0], rowIndex[1]); + m_optionsRows = qMax(m_optionsRows, optionRows); + m_optionsHeight = qMax(rowHeight * optionRows + 16, m_optionsHeight); + layout->setContentsMargins(8, 8, 8, 8); + optionsAreaContents->setContentsMargins(0, 0, 0, 0); + optionsAreaContents->setLayout(layout); + optionsAreaContents->setMinimumWidth( + (checkBoxColWidth + labelMinWidth + controlMinWidth) * 2 + columnSpacing); + optionsAreaContents->setObjectName("optionsAreaContents"); // For stylesheet + + optionsArea->setWidget(optionsAreaContents); + optionsArea->setStyleSheet("QScrollArea {background-color: transparent}"); + optionsAreaContents->setStyleSheet( + "QWidget#optionsAreaContents {background-color: transparent}"); + + ui->tabWidget->addTab(optionsArea, tr("%1 options").arg(tabLabel)); +} + +void ItemLibraryAssetImportDialog::updateUi() +{ + auto optionsArea = qobject_cast(ui->tabWidget->currentWidget()); + if (optionsArea) { + auto optionsAreaContents = optionsArea->widget(); + int scrollBarWidth = optionsArea->verticalScrollBar()->isVisible() + ? optionsArea->verticalScrollBar()->width() : 0; + optionsAreaContents->resize(optionsArea->contentsRect().width() + - scrollBarWidth - 8, m_optionsHeight); + } +} + void ItemLibraryAssetImportDialog::resizeEvent(QResizeEvent *event) { Q_UNUSED(event) - int scrollBarWidth = ui->optionsArea->verticalScrollBar()->isVisible() - ? ui->optionsArea->verticalScrollBar()->width() : 0; - ui->tabWidget->setMaximumHeight(ui->optionsAreaContents->height() - + ui->tabWidget->tabBar()->height() + 10); - ui->optionsArea->resize(ui->tabWidget->currentWidget()->size()); - ui->optionsAreaContents->resize(ui->optionsArea->contentsRect().width() - - scrollBarWidth - 8, 0); + updateUi(); } void ItemLibraryAssetImportDialog::setCloseButtonState(bool importing) @@ -504,7 +591,7 @@ void ItemLibraryAssetImportDialog::onImport() if (!m_quick3DFiles.isEmpty()) { m_importer.importQuick3D(m_quick3DFiles, m_quick3DImportPath, - m_importOptions.toVariantMap()); + m_importOptions, m_extToImportOptionsMap); } } @@ -549,4 +636,5 @@ void ItemLibraryAssetImportDialog::onClose() deleteLater(); } } + } diff --git a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.h b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.h index 52fbcc09998..713e3fd20b4 100644 --- a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.h +++ b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.h @@ -66,12 +66,18 @@ private: void onImportFinished(); void onClose(); + void createTab(const QString &tabLabel, int optionsIndex, const QJsonObject &groups); + void updateUi(); + Ui::ItemLibraryAssetImportDialog *ui = nullptr; Utils::OutputFormatter *m_outputFormatter = nullptr; QStringList m_quick3DFiles; QString m_quick3DImportPath; ItemLibraryAssetImporter m_importer; - QJsonObject m_importOptions; + QVector m_importOptions; + QHash m_extToImportOptionsMap; + int m_optionsHeight = 0; + int m_optionsRows = 0; }; } diff --git a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.ui b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.ui index 710135ad042..e6b02863574 100644 --- a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.ui +++ b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.ui @@ -7,7 +7,7 @@ 0 0 631 - 740 + 750 @@ -32,47 +32,6 @@ Import Options - - - - 0 - 0 - 611 - 351 - - - - - 0 - 2 - - - - Qt::ScrollBarAlwaysOff - - - false - - - - - 0 - 0 - 0 - 0 - - - - - 0 - 0 - - - - false - - - diff --git a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.cpp b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.cpp index deb50e0a661..c67b527abb5 100644 --- a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.cpp +++ b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.cpp @@ -32,6 +32,7 @@ #include #include #include +#include #include #include #include @@ -63,7 +64,8 @@ ItemLibraryAssetImporter::~ItemLibraryAssetImporter() { void ItemLibraryAssetImporter::importQuick3D(const QStringList &inputFiles, const QString &importPath, - const QVariantMap &options) + const QVector &options, + const QHash &extToImportOptionsMap) { if (m_isImporting) cancelImport(); @@ -79,7 +81,7 @@ void ItemLibraryAssetImporter::importQuick3D(const QStringList &inputFiles, m_importPath = importPath; - parseFiles(inputFiles, options); + parseFiles(inputFiles, options, extToImportOptionsMap); if (!isCancelled()) { // Don't allow cancel anymore as existing asset overwrites are not trivially recoverable. @@ -221,7 +223,9 @@ void ItemLibraryAssetImporter::reset() #endif } -void ItemLibraryAssetImporter::parseFiles(const QStringList &filePaths, const QVariantMap &options) +void ItemLibraryAssetImporter::parseFiles(const QStringList &filePaths, + const QVector &options, + const QHash &extToImportOptionsMap) { if (isCancelled()) return; @@ -236,8 +240,11 @@ void ItemLibraryAssetImporter::parseFiles(const QStringList &filePaths, const QV for (const QString &file : filePaths) { if (isCancelled()) return; - if (isQuick3DAsset(file)) - parseQuick3DAsset(file, options); + if (isQuick3DAsset(file)) { + QVariantMap varOpts; + int index = extToImportOptionsMap.value(QFileInfo(file).suffix()); + parseQuick3DAsset(file, options[index].toVariantMap()); + } notifyProgress(qRound(++count * quota), progressTitle); } notifyProgress(100, progressTitle); @@ -296,31 +303,70 @@ void ItemLibraryAssetImporter::parseQuick3DAsset(const QString &file, const QVar return; } - // Generate qmldir file - outDir.setNameFilters({QStringLiteral("*.qml")}); - const QFileInfoList qmlFiles = outDir.entryInfoList(QDir::Files); - - if (!qmlFiles.isEmpty()) { - QString qmldirFileName = outDir.absoluteFilePath(QStringLiteral("qmldir")); + // Generate qmldir file if importer doesn't already make one + QString qmldirFileName = outDir.absoluteFilePath(QStringLiteral("qmldir")); + if (!QFileInfo(qmldirFileName).exists()) { QSaveFile qmldirFile(qmldirFileName); QString version = QStringLiteral("1.0"); - if (qmldirFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { - for (const auto &fi : qmlFiles) { + + // Note: Currently Quick3D importers only generate externally usable qml files on the top + // level of the import directory, so we don't search subdirectories. The qml files in + // subdirs assume they are used within the context of the toplevel qml files. + QDirIterator qmlIt(outDir.path(), {QStringLiteral("*.qml")}, QDir::Files); + if (qmlIt.hasNext()) { + outDir.mkdir(Constants::QUICK_3D_ASSET_ICON_DIR); + if (qmldirFile.open(QIODevice::WriteOnly | QIODevice::Truncate)) { QString qmlInfo; - qmlInfo.append(fi.baseName()); - qmlInfo.append(QLatin1Char(' ')); - qmlInfo.append(version); - qmlInfo.append(QLatin1Char(' ')); - qmlInfo.append(fi.fileName()); + qmlInfo.append("module "); + qmlInfo.append(m_importPath.split('/').last()); + qmlInfo.append("."); + qmlInfo.append(assetName); + qmlInfo.append('\n'); + while (qmlIt.hasNext()) { + qmlIt.next(); + QFileInfo fi = QFileInfo(qmlIt.filePath()); + qmlInfo.append(fi.baseName()); + qmlInfo.append(' '); + qmlInfo.append(version); + qmlInfo.append(' '); + qmlInfo.append(outDir.relativeFilePath(qmlIt.filePath())); + qmlInfo.append('\n'); + + // Generate item library icon for qml file based on root component + QFile qmlFile(qmlIt.filePath()); + if (qmlFile.open(QIODevice::ReadOnly)) { + QString iconFileName = outDir.path() + '/' + + Constants::QUICK_3D_ASSET_ICON_DIR + '/' + fi.baseName() + + Constants::QUICK_3D_ASSET_LIBRARY_ICON_SUFFIX; + QString iconFileName2x = iconFileName + "@2x"; + QByteArray content = qmlFile.readAll(); + int braceIdx = content.indexOf('{'); + if (braceIdx != -1) { + int nlIdx = content.lastIndexOf('\n', braceIdx); + QByteArray rootItem = content.mid(nlIdx, braceIdx - nlIdx).trimmed(); + if (rootItem == "Node") { + QFile::copy(":/ItemLibrary/images/item-3D_model-icon.png", + iconFileName); + QFile::copy(":/ItemLibrary/images/item-3D_model-icon@2x.png", + iconFileName2x); + } else { + QFile::copy(":/ItemLibrary/images/item-default-icon.png", + iconFileName); + QFile::copy(":/ItemLibrary/images/item-default-icon@2x.png", + iconFileName2x); + } + } + } + } qmldirFile.write(qmlInfo.toUtf8()); + qmldirFile.commit(); + } else { + addError(tr("Failed to create qmldir file for asset: \"%1\"").arg(assetName)); } - qmldirFile.commit(); - } else { - addError(tr("Failed to create qmldir file for asset: \"%1\"").arg(assetName)); } } - // Gather generated files + // Gather all generated files const int outDirPathSize = outDir.path().size(); QDirIterator dirIt(outDir.path(), QDir::Files, QDirIterator::Subdirectories); QHash assetFiles; @@ -334,7 +380,7 @@ void ItemLibraryAssetImporter::parseQuick3DAsset(const QString &file, const QVar // Copy the original asset into a subdirectory assetFiles.insert(sourceInfo.absoluteFilePath(), - targetDirPath + QStringLiteral("/source model/") + sourceInfo.fileName()); + targetDirPath + QStringLiteral("/source scene/") + sourceInfo.fileName()); m_importFiles.insert(assetFiles); #else diff --git a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.h b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.h index 6c6b4f972b4..76af5b80139 100644 --- a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.h +++ b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.h @@ -27,6 +27,7 @@ #include #include #include +#include #include "import.h" @@ -46,7 +47,8 @@ public: ~ItemLibraryAssetImporter(); void importQuick3D(const QStringList &inputFiles, const QString &importPath, - const QVariantMap &options); + const QVector &options, + const QHash &extToImportOptionsMap); bool isImporting() const; void cancelImport(); @@ -72,7 +74,8 @@ signals: private: void notifyFinished(); void reset(); - void parseFiles(const QStringList &filePaths, const QVariantMap &options); + void parseFiles(const QStringList &filePaths, const QVector &options, + const QHash &extToImportOptionsMap); void parseQuick3DAsset(const QString &file, const QVariantMap &options); void copyImportedFiles(); diff --git a/src/plugins/qmldesigner/components/itemlibrary/itemlibrarywidget.cpp b/src/plugins/qmldesigner/components/itemlibrary/itemlibrarywidget.cpp index da056aaf6b7..8010ed61075 100644 --- a/src/plugins/qmldesigner/components/itemlibrary/itemlibrarywidget.cpp +++ b/src/plugins/qmldesigner/components/itemlibrary/itemlibrarywidget.cpp @@ -209,19 +209,27 @@ ItemLibraryWidget::ItemLibraryWidget(QWidget *parent) : QSSGAssetImportManager importManager; QHash supportedExtensions = importManager.getSupportedExtensions(); - // Skip if 3D model handlers have already been added - const QList handlers = actionManager->addResourceHandler(); - QSet handlerCats; - for (const auto &h : handlers) - handlerCats.insert(h.category); + // All things importable by QSSGAssetImportManager are considered to be in the same category + // so we don't get multiple separate import dialogs when different file types are imported. + const QString category = tr("3D Assets"); - const auto categories = supportedExtensions.keys(); - for (const auto &category : categories) { - if (handlerCats.contains(category)) - continue; - const auto extensions = supportedExtensions[category]; - for (const auto &ext : extensions) - add3DHandler(category, ext); + // Skip if 3D asset handlers have already been added + const QList handlers = actionManager->addResourceHandler(); + bool categoryAlreadyAdded = false; + for (const auto &handler : handlers) { + if (handler.category == category) { + categoryAlreadyAdded = true; + break; + } + } + + if (!categoryAlreadyAdded) { + const auto groups = supportedExtensions.keys(); + for (const auto &group : groups) { + const auto extensions = supportedExtensions[group]; + for (const auto &ext : extensions) + add3DHandler(category, ext); + } } #endif diff --git a/src/plugins/qmldesigner/designercore/metainfo/subcomponentmanager.cpp b/src/plugins/qmldesigner/designercore/metainfo/subcomponentmanager.cpp index 0c707ff837b..d173a83749a 100644 --- a/src/plugins/qmldesigner/designercore/metainfo/subcomponentmanager.cpp +++ b/src/plugins/qmldesigner/designercore/metainfo/subcomponentmanager.cpp @@ -35,6 +35,7 @@ #include #include +#include #include #include @@ -387,19 +388,29 @@ void SubComponentManager::parseQuick3DAssetDir(const QString &assetPath) for (auto &import : qAsConst(m_imports)) { if (import.isLibraryImport() && assets.contains(import.url())) { assets.removeOne(import.url()); - ItemLibraryEntry itemLibraryEntry; - const QString name = import.url().mid(import.url().indexOf(QLatin1Char('.')) + 1); - const QString type = import.url() + QLatin1Char('.') + name; - // For now we assume version is always 1.0 as that's what importer hardcodes - itemLibraryEntry.setType(type.toUtf8(), 1, 0); - itemLibraryEntry.setName(name); - itemLibraryEntry.setCategory(tr("My Quick3D Components")); - itemLibraryEntry.setRequiredImport(import.url()); - itemLibraryEntry.setLibraryEntryIconPath(iconPath); - itemLibraryEntry.setTypeIcon(QIcon(iconPath)); + QDirIterator qmlIt(assetDir.filePath(import.url().split('.').last()), + {QStringLiteral("*.qml")}, QDir::Files); + while (qmlIt.hasNext()) { + qmlIt.next(); + const QString name = qmlIt.fileInfo().baseName(); + const QString type = import.url() + QLatin1Char('.') + name; + // For now we assume version is always 1.0 as that's what importer hardcodes + ItemLibraryEntry itemLibraryEntry; + itemLibraryEntry.setType(type.toUtf8(), 1, 0); + itemLibraryEntry.setName(name); + itemLibraryEntry.setCategory(tr("My Quick3D Components")); + itemLibraryEntry.setRequiredImport(import.url()); + QString iconName = qmlIt.fileInfo().absolutePath() + '/' + + Constants::QUICK_3D_ASSET_ICON_DIR + '/' + name + + Constants::QUICK_3D_ASSET_LIBRARY_ICON_SUFFIX; + if (!QFileInfo(iconName).exists()) + iconName = iconPath; + itemLibraryEntry.setLibraryEntryIconPath(iconName); + itemLibraryEntry.setTypeIcon(QIcon(iconName)); - if (!model()->metaInfo().itemLibraryInfo()->containsEntry(itemLibraryEntry)) - model()->metaInfo().itemLibraryInfo()->addEntries({itemLibraryEntry}); + if (!model()->metaInfo().itemLibraryInfo()->containsEntry(itemLibraryEntry)) + model()->metaInfo().itemLibraryInfo()->addEntries({itemLibraryEntry}); + } } } diff --git a/src/plugins/qmldesigner/qmldesignerconstants.h b/src/plugins/qmldesigner/qmldesignerconstants.h index 7d4079e75fa..82445f4e78b 100644 --- a/src/plugins/qmldesigner/qmldesignerconstants.h +++ b/src/plugins/qmldesigner/qmldesignerconstants.h @@ -51,6 +51,8 @@ const char EXPORT_AS_IMAGE[] = "QmlDesigner.ExportAsImage"; const char QML_DESIGNER_SUBFOLDER[] = "/designer/"; const char QUICK_3D_ASSETS_FOLDER[] = "/Quick3DAssets"; +const char QUICK_3D_ASSET_LIBRARY_ICON_SUFFIX[] = "_libicon"; +const char QUICK_3D_ASSET_ICON_DIR[] = "_icons"; const char DEFAULT_ASSET_IMPORT_FOLDER[] = "/asset_imports"; const char QT_QUICK_3D_MODULE_NAME[] = "QtQuick3D";