diff options
8 files changed, 503 insertions, 380 deletions
diff --git a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.cpp b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.cpp index f1eb737aed..bfdbfe5c22 100644 --- a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.cpp +++ b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.cpp @@ -44,6 +44,7 @@ #include <QtWidgets/qspinbox.h> #include <QtWidgets/qscrollbar.h> #include <QtWidgets/qtabbar.h> +#include <QtWidgets/qscrollarea.h> 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(); - - 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<QVector<QPair<QWidget *, QWidget *>>> widgets; - QHash<QString, int> groupIndexMap; - QHash<QString, QPair<QWidget *, QWidget *>> optionToWidgetsMap; - QHash<QString, QJsonArray> conditionMap; - QHash<QWidget *, QWidget *> conditionalWidgetMap; - QHash<QString, QString> 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<QPair<QWidget *, QWidget *>>()); - - 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); + if (!m_quick3DFiles.isEmpty()) { + const QHash<QString, QVariantMap> allOptions = m_importer.allOptions(); + const QHash<QString, QStringList> supportedExtensions = m_importer.supportedExtensions(); + QVector<QJsonObject> groups; + + 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<QString, int> 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<double>::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 - - if (optWidgets.first && optWidgets.second) { - auto optCb = qobject_cast<QCheckBox *>(optWidgets.second); - auto optSpin = qobject_cast<QDoubleSpinBox *>(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<double>::of(&QDoubleSpinBox::valueChanged), - [optSpin, conLabel, conControl, mode, enableConditionally]() { - enableConditionally(optSpin, conLabel, conControl, mode); - }); - } - } - } - ++it; + ui->tabWidget->clear(); + auto tabIt = tabMap.constBegin(); + while (tabIt != tabMap.constEnd()) { + createTab(tabIt.key(), tabIt.value(), groups[tabIt.value()]); + ++tabIt; } - // 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(); + // Pad all tabs to same height + for (int i = 0; i < ui->tabWidget->count(); ++i) { + auto optionsArea = qobject_cast<QScrollArea *>(ui->tabWidget->widget(i)); + if (optionsArea && optionsArea->widget()) { + auto grid = qobject_cast<QGridLayout *>(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); } } } - ++condIt; } - auto incrementColIndex = [&](int col) { - layout->setRowMinimumHeight(rowIndex[col], rowHeight); - ++rowIndex[col]; - }; - - auto insertOptionToLayout = [&](int col, const QPair<QWidget *, QWidget *> &optionWidgets) { - layout->addWidget(optionWidgets.first, rowIndex[col], col * 4 + 1, 1, 2); - int adj = qobject_cast<QCheckBox *>(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<QVector<QPair<QWidget *, QWidget *>>> widgets; + QHash<QString, int> groupIndexMap; + QHash<QString, QPair<QWidget *, QWidget *>> optionToWidgetsMap; + QHash<QString, QJsonArray> conditionMap; + QHash<QWidget *, QWidget *> conditionalWidgetMap; + QHash<QString, QString> 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<QPair<QWidget *, QWidget *>>()); + + 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<double>::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<QCheckBox *>(optWidgets.second); + auto optSpin = qobject_cast<QDoubleSpinBox *>(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<double>::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<QCheckBox *>(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<QWidget *, QWidget *> &optionWidgets) { + layout->addWidget(optionWidgets.first, rowIndex[col], col * 4 + 1, 1, 2); + int adj = qobject_cast<QCheckBox *>(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<QScrollArea *>(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 52fbcc0999..713e3fd20b 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<QJsonObject> m_importOptions; + QHash<QString, int> 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 710135ad04..e6b0286357 100644 --- a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.ui +++ b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimportdialog.ui @@ -7,7 +7,7 @@ <x>0</x> <y>0</y> <width>631</width> - <height>740</height> + <height>750</height> </rect> </property> <property name="windowTitle"> @@ -32,47 +32,6 @@ <attribute name="title"> <string>Import Options</string> </attribute> - <widget class="QScrollArea" name="optionsArea"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>611</width> - <height>351</height> - </rect> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>2</verstretch> - </sizepolicy> - </property> - <property name="horizontalScrollBarPolicy"> - <enum>Qt::ScrollBarAlwaysOff</enum> - </property> - <property name="widgetResizable"> - <bool>false</bool> - </property> - <widget class="QWidget" name="optionsAreaContents"> - <property name="geometry"> - <rect> - <x>0</x> - <y>0</y> - <width>0</width> - <height>0</height> - </rect> - </property> - <property name="sizePolicy"> - <sizepolicy hsizetype="Expanding" vsizetype="Expanding"> - <horstretch>0</horstretch> - <verstretch>0</verstretch> - </sizepolicy> - </property> - <property name="autoFillBackground"> - <bool>false</bool> - </property> - </widget> - </widget> </widget> </widget> </item> diff --git a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.cpp b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.cpp index deb50e0a66..c67b527abb 100644 --- a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.cpp +++ b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.cpp @@ -32,6 +32,7 @@ #include <QtCore/qdir.h> #include <QtCore/qdiriterator.h> #include <QtCore/qsavefile.h> +#include <QtCore/qfile.h> #include <QtCore/qloggingcategory.h> #include <QtCore/qtemporarydir.h> #include <QtWidgets/qapplication.h> @@ -63,7 +64,8 @@ ItemLibraryAssetImporter::~ItemLibraryAssetImporter() { void ItemLibraryAssetImporter::importQuick3D(const QStringList &inputFiles, const QString &importPath, - const QVariantMap &options) + const QVector<QJsonObject> &options, + const QHash<QString, int> &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<QJsonObject> &options, + const QHash<QString, int> &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<QString, QString> 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 6c6b4f972b..76af5b8013 100644 --- a/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.h +++ b/src/plugins/qmldesigner/components/itemlibrary/itemlibraryassetimporter.h @@ -27,6 +27,7 @@ #include <QtCore/qobject.h> #include <QtCore/qstringlist.h> #include <QtCore/qhash.h> +#include <QtCore/qjsonobject.h> #include "import.h" @@ -46,7 +47,8 @@ public: ~ItemLibraryAssetImporter(); void importQuick3D(const QStringList &inputFiles, const QString &importPath, - const QVariantMap &options); + const QVector<QJsonObject> &options, + const QHash<QString, int> &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<QJsonObject> &options, + const QHash<QString, int> &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 da056aaf6b..8010ed6107 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<QString, QStringList> supportedExtensions = importManager.getSupportedExtensions(); - // Skip if 3D model handlers have already been added + // 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"); + + // Skip if 3D asset handlers have already been added const QList<AddResourceHandler> handlers = actionManager->addResourceHandler(); - QSet<QString> handlerCats; - for (const auto &h : handlers) - handlerCats.insert(h.category); - - 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); + 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 0c707ff837..d173a83749 100644 --- a/src/plugins/qmldesigner/designercore/metainfo/subcomponentmanager.cpp +++ b/src/plugins/qmldesigner/designercore/metainfo/subcomponentmanager.cpp @@ -35,6 +35,7 @@ #include <coreplugin/messagebox.h> #include <QDir> +#include <QDirIterator> #include <QMessageBox> #include <QUrl> @@ -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)); - - if (!model()->metaInfo().itemLibraryInfo()->containsEntry(itemLibraryEntry)) - model()->metaInfo().itemLibraryInfo()->addEntries({itemLibraryEntry}); + 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}); + } } } diff --git a/src/plugins/qmldesigner/qmldesignerconstants.h b/src/plugins/qmldesigner/qmldesignerconstants.h index 7d4079e75f..82445f4e78 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"; |