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 <mahmoud.badri@qt.io>
Reviewed-by: Thomas Hartmann <thomas.hartmann@qt.io>
This commit is contained in:
Miikka Heikkinen
2019-10-10 12:18:38 +03:00
parent 1278c01c1c
commit 062a0a9dbd
8 changed files with 502 additions and 379 deletions

View File

@@ -44,6 +44,7 @@
#include <QtWidgets/qspinbox.h> #include <QtWidgets/qspinbox.h>
#include <QtWidgets/qscrollbar.h> #include <QtWidgets/qscrollbar.h>
#include <QtWidgets/qtabbar.h> #include <QtWidgets/qtabbar.h>
#include <QtWidgets/qscrollarea.h>
namespace QmlDesigner { namespace QmlDesigner {
@@ -62,6 +63,8 @@ static void addFormattedMessage(Utils::OutputFormatter *formatter, const QString
formatter->plainTextEdit()->verticalScrollBar()->maximum()); formatter->plainTextEdit()->verticalScrollBar()->maximum());
} }
static const int rowHeight = 26;
} }
ItemLibraryAssetImportDialog::ItemLibraryAssetImportDialog(const QStringList &importFiles, ItemLibraryAssetImportDialog::ItemLibraryAssetImportDialog(const QStringList &importFiles,
@@ -140,301 +143,67 @@ ItemLibraryAssetImportDialog::ItemLibraryAssetImportDialog(const QStringList &im
} }
m_quick3DImportPath = candidatePath; m_quick3DImportPath = candidatePath;
// Create UI controls for options if (!m_quick3DFiles.isEmpty()) {
if (!importFiles.isEmpty()) { const QHash<QString, QVariantMap> allOptions = m_importer.allOptions();
QJsonObject supportedOptions = QJsonObject::fromVariantMap( const QHash<QString, QStringList> supportedExtensions = m_importer.supportedExtensions();
m_importer.supportedOptions(importFiles[0])); QVector<QJsonObject> groups;
m_importOptions = supportedOptions.value("options").toObject();
const QJsonObject groups = supportedOptions.value("groups").toObject();
const int checkBoxColWidth = 18; auto optIt = allOptions.constBegin();
const int labelMinWidth = 130; int optIndex = 0;
const int controlMinWidth = 65; while (optIt != allOptions.constEnd()) {
const int columnSpacing = 16; QJsonObject options = QJsonObject::fromVariantMap(optIt.value());
const int rowHeight = 26; m_importOptions << options.value("options").toObject();
int rowIndex[2] = {0, 0}; groups << options.value("groups").toObject();
const auto &exts = optIt.key().split(':');
// First index has ungrouped widgets, rest are groups for (const auto &ext : exts)
// First item in each real group is group label m_extToImportOptionsMap.insert(ext, optIndex);
QVector<QVector<QPair<QWidget *, QWidget *>>> widgets; ++optIt;
QHash<QString, int> groupIndexMap; ++optIndex;
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);
} }
const auto optKeys = m_importOptions.keys(); // Create tab for each supported extension group that also has files included in the import
for (const auto &optKey : optKeys) { QMap<QString, int> tabMap; // QMap used for alphabetical order
QJsonObject optObj = m_importOptions.value(optKey).toObject(); for (const auto &file : qAsConst(m_quick3DFiles)) {
const QString optName = optObj.value("name").toString(); auto extIt = supportedExtensions.constBegin();
const QString optDesc = optObj.value("description").toString(); QString ext = QFileInfo(file).suffix();
const QString optType = optObj.value("type").toString(); while (extIt != supportedExtensions.constEnd()) {
QJsonObject optRange = optObj.value("range").toObject(); if (!tabMap.contains(extIt.key()) && extIt.value().contains(ext)) {
QJsonValue optValue = optObj.value("value"); tabMap.insert(extIt.key(), m_extToImportOptionsMap.value(ext));
QJsonArray conditions = optObj.value("conditions").toArray(); break;
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;
}
} }
optSpin->setRange(min, max); ++extIt;
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;
} }
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 ui->tabWidget->clear();
auto it = conditionMap.constBegin(); auto tabIt = tabMap.constBegin();
while (it != conditionMap.constEnd()) { while (tabIt != tabMap.constEnd()) {
const QString &option = it.key(); createTab(tabIt.key(), tabIt.value(), groups[tabIt.value()]);
const QJsonArray &conditions = it.value(); ++tabIt;
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) { // Pad all tabs to same height
auto optCb = qobject_cast<QCheckBox *>(optWidgets.second); for (int i = 0; i < ui->tabWidget->count(); ++i) {
auto optSpin = qobject_cast<QDoubleSpinBox *>(optWidgets.second); auto optionsArea = qobject_cast<QScrollArea *>(ui->tabWidget->widget(i));
if (optCb) { if (optionsArea && optionsArea->widget()) {
auto enableConditionally = [optValue](QCheckBox *cb, QWidget *w1, auto grid = qobject_cast<QGridLayout *>(optionsArea->widget()->layout());
QWidget *w2, Mode mode) { if (grid) {
bool equals = (mode == Mode::equals) == optValue.toBool(); int rows = grid->rowCount();
bool enable = cb->isChecked() == equals; for (int j = rows; j < m_optionsRows; ++j) {
w1->setEnabled(enable); grid->addWidget(new QWidget(optionsArea->widget()), j, 0);
w2->setEnabled(enable); grid->setRowMinimumHeight(j, rowHeight);
};
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 ui->tabWidget->setCurrentIndex(0);
// 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<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->optionsArea->setStyleSheet("QScrollArea {background-color: transparent}");
ui->optionsAreaContents->setStyleSheet(
"QWidget#optionsAreaContents {background-color: transparent}");
connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked, connect(ui->buttonBox->button(QDialogButtonBox::Close), &QPushButton::clicked,
this, &ItemLibraryAssetImportDialog::onClose); this, &ItemLibraryAssetImportDialog::onClose);
connect(ui->tabWidget, &QTabWidget::currentChanged,
this, &ItemLibraryAssetImportDialog::updateUi);
connect(&m_importer, &ItemLibraryAssetImporter::errorReported, connect(&m_importer, &ItemLibraryAssetImporter::errorReported,
this, &ItemLibraryAssetImportDialog::addError); this, &ItemLibraryAssetImportDialog::addError);
@@ -454,7 +223,8 @@ ItemLibraryAssetImportDialog::ItemLibraryAssetImportDialog(const QStringList &im
addInfo(file); addInfo(file);
QTimer::singleShot(0, [this]() { 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; 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) void ItemLibraryAssetImportDialog::resizeEvent(QResizeEvent *event)
{ {
Q_UNUSED(event) Q_UNUSED(event)
int scrollBarWidth = ui->optionsArea->verticalScrollBar()->isVisible() updateUi();
? 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);
} }
void ItemLibraryAssetImportDialog::setCloseButtonState(bool importing) void ItemLibraryAssetImportDialog::setCloseButtonState(bool importing)
@@ -504,7 +591,7 @@ void ItemLibraryAssetImportDialog::onImport()
if (!m_quick3DFiles.isEmpty()) { if (!m_quick3DFiles.isEmpty()) {
m_importer.importQuick3D(m_quick3DFiles, m_quick3DImportPath, m_importer.importQuick3D(m_quick3DFiles, m_quick3DImportPath,
m_importOptions.toVariantMap()); m_importOptions, m_extToImportOptionsMap);
} }
} }
@@ -549,4 +636,5 @@ void ItemLibraryAssetImportDialog::onClose()
deleteLater(); deleteLater();
} }
} }
} }

View File

@@ -66,12 +66,18 @@ private:
void onImportFinished(); void onImportFinished();
void onClose(); void onClose();
void createTab(const QString &tabLabel, int optionsIndex, const QJsonObject &groups);
void updateUi();
Ui::ItemLibraryAssetImportDialog *ui = nullptr; Ui::ItemLibraryAssetImportDialog *ui = nullptr;
Utils::OutputFormatter *m_outputFormatter = nullptr; Utils::OutputFormatter *m_outputFormatter = nullptr;
QStringList m_quick3DFiles; QStringList m_quick3DFiles;
QString m_quick3DImportPath; QString m_quick3DImportPath;
ItemLibraryAssetImporter m_importer; ItemLibraryAssetImporter m_importer;
QJsonObject m_importOptions; QVector<QJsonObject> m_importOptions;
QHash<QString, int> m_extToImportOptionsMap;
int m_optionsHeight = 0;
int m_optionsRows = 0;
}; };
} }

View File

@@ -7,7 +7,7 @@
<x>0</x> <x>0</x>
<y>0</y> <y>0</y>
<width>631</width> <width>631</width>
<height>740</height> <height>750</height>
</rect> </rect>
</property> </property>
<property name="windowTitle"> <property name="windowTitle">
@@ -32,47 +32,6 @@
<attribute name="title"> <attribute name="title">
<string>Import Options</string> <string>Import Options</string>
</attribute> </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>
</widget> </widget>
</item> </item>

View File

@@ -32,6 +32,7 @@
#include <QtCore/qdir.h> #include <QtCore/qdir.h>
#include <QtCore/qdiriterator.h> #include <QtCore/qdiriterator.h>
#include <QtCore/qsavefile.h> #include <QtCore/qsavefile.h>
#include <QtCore/qfile.h>
#include <QtCore/qloggingcategory.h> #include <QtCore/qloggingcategory.h>
#include <QtCore/qtemporarydir.h> #include <QtCore/qtemporarydir.h>
#include <QtWidgets/qapplication.h> #include <QtWidgets/qapplication.h>
@@ -63,7 +64,8 @@ ItemLibraryAssetImporter::~ItemLibraryAssetImporter() {
void ItemLibraryAssetImporter::importQuick3D(const QStringList &inputFiles, void ItemLibraryAssetImporter::importQuick3D(const QStringList &inputFiles,
const QString &importPath, const QString &importPath,
const QVariantMap &options) const QVector<QJsonObject> &options,
const QHash<QString, int> &extToImportOptionsMap)
{ {
if (m_isImporting) if (m_isImporting)
cancelImport(); cancelImport();
@@ -79,7 +81,7 @@ void ItemLibraryAssetImporter::importQuick3D(const QStringList &inputFiles,
m_importPath = importPath; m_importPath = importPath;
parseFiles(inputFiles, options); parseFiles(inputFiles, options, extToImportOptionsMap);
if (!isCancelled()) { if (!isCancelled()) {
// Don't allow cancel anymore as existing asset overwrites are not trivially recoverable. // Don't allow cancel anymore as existing asset overwrites are not trivially recoverable.
@@ -221,7 +223,9 @@ void ItemLibraryAssetImporter::reset()
#endif #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()) if (isCancelled())
return; return;
@@ -236,8 +240,11 @@ void ItemLibraryAssetImporter::parseFiles(const QStringList &filePaths, const QV
for (const QString &file : filePaths) { for (const QString &file : filePaths) {
if (isCancelled()) if (isCancelled())
return; return;
if (isQuick3DAsset(file)) if (isQuick3DAsset(file)) {
parseQuick3DAsset(file, options); QVariantMap varOpts;
int index = extToImportOptionsMap.value(QFileInfo(file).suffix());
parseQuick3DAsset(file, options[index].toVariantMap());
}
notifyProgress(qRound(++count * quota), progressTitle); notifyProgress(qRound(++count * quota), progressTitle);
} }
notifyProgress(100, progressTitle); notifyProgress(100, progressTitle);
@@ -296,31 +303,70 @@ void ItemLibraryAssetImporter::parseQuick3DAsset(const QString &file, const QVar
return; return;
} }
// Generate qmldir file // Generate qmldir file if importer doesn't already make one
outDir.setNameFilters({QStringLiteral("*.qml")}); QString qmldirFileName = outDir.absoluteFilePath(QStringLiteral("qmldir"));
const QFileInfoList qmlFiles = outDir.entryInfoList(QDir::Files); if (!QFileInfo(qmldirFileName).exists()) {
if (!qmlFiles.isEmpty()) {
QString qmldirFileName = outDir.absoluteFilePath(QStringLiteral("qmldir"));
QSaveFile qmldirFile(qmldirFileName); QSaveFile qmldirFile(qmldirFileName);
QString version = QStringLiteral("1.0"); 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; QString qmlInfo;
qmlInfo.append(fi.baseName()); qmlInfo.append("module ");
qmlInfo.append(QLatin1Char(' ')); qmlInfo.append(m_importPath.split('/').last());
qmlInfo.append(version); qmlInfo.append(".");
qmlInfo.append(QLatin1Char(' ')); qmlInfo.append(assetName);
qmlInfo.append(fi.fileName()); 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.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(); const int outDirPathSize = outDir.path().size();
QDirIterator dirIt(outDir.path(), QDir::Files, QDirIterator::Subdirectories); QDirIterator dirIt(outDir.path(), QDir::Files, QDirIterator::Subdirectories);
QHash<QString, QString> assetFiles; QHash<QString, QString> assetFiles;
@@ -334,7 +380,7 @@ void ItemLibraryAssetImporter::parseQuick3DAsset(const QString &file, const QVar
// Copy the original asset into a subdirectory // Copy the original asset into a subdirectory
assetFiles.insert(sourceInfo.absoluteFilePath(), assetFiles.insert(sourceInfo.absoluteFilePath(),
targetDirPath + QStringLiteral("/source model/") + sourceInfo.fileName()); targetDirPath + QStringLiteral("/source scene/") + sourceInfo.fileName());
m_importFiles.insert(assetFiles); m_importFiles.insert(assetFiles);
#else #else

View File

@@ -27,6 +27,7 @@
#include <QtCore/qobject.h> #include <QtCore/qobject.h>
#include <QtCore/qstringlist.h> #include <QtCore/qstringlist.h>
#include <QtCore/qhash.h> #include <QtCore/qhash.h>
#include <QtCore/qjsonobject.h>
#include "import.h" #include "import.h"
@@ -46,7 +47,8 @@ public:
~ItemLibraryAssetImporter(); ~ItemLibraryAssetImporter();
void importQuick3D(const QStringList &inputFiles, const QString &importPath, void importQuick3D(const QStringList &inputFiles, const QString &importPath,
const QVariantMap &options); const QVector<QJsonObject> &options,
const QHash<QString, int> &extToImportOptionsMap);
bool isImporting() const; bool isImporting() const;
void cancelImport(); void cancelImport();
@@ -72,7 +74,8 @@ signals:
private: private:
void notifyFinished(); void notifyFinished();
void reset(); 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 parseQuick3DAsset(const QString &file, const QVariantMap &options);
void copyImportedFiles(); void copyImportedFiles();

View File

@@ -209,19 +209,27 @@ ItemLibraryWidget::ItemLibraryWidget(QWidget *parent) :
QSSGAssetImportManager importManager; QSSGAssetImportManager importManager;
QHash<QString, QStringList> supportedExtensions = importManager.getSupportedExtensions(); 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
const QList<AddResourceHandler> handlers = actionManager->addResourceHandler(); // so we don't get multiple separate import dialogs when different file types are imported.
QSet<QString> handlerCats; const QString category = tr("3D Assets");
for (const auto &h : handlers)
handlerCats.insert(h.category);
const auto categories = supportedExtensions.keys(); // Skip if 3D asset handlers have already been added
for (const auto &category : categories) { const QList<AddResourceHandler> handlers = actionManager->addResourceHandler();
if (handlerCats.contains(category)) bool categoryAlreadyAdded = false;
continue; for (const auto &handler : handlers) {
const auto extensions = supportedExtensions[category]; if (handler.category == category) {
for (const auto &ext : extensions) categoryAlreadyAdded = true;
add3DHandler(category, ext); 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 #endif

View File

@@ -35,6 +35,7 @@
#include <coreplugin/messagebox.h> #include <coreplugin/messagebox.h>
#include <QDir> #include <QDir>
#include <QDirIterator>
#include <QMessageBox> #include <QMessageBox>
#include <QUrl> #include <QUrl>
@@ -387,19 +388,29 @@ void SubComponentManager::parseQuick3DAssetDir(const QString &assetPath)
for (auto &import : qAsConst(m_imports)) { for (auto &import : qAsConst(m_imports)) {
if (import.isLibraryImport() && assets.contains(import.url())) { if (import.isLibraryImport() && assets.contains(import.url())) {
assets.removeOne(import.url()); assets.removeOne(import.url());
ItemLibraryEntry itemLibraryEntry; QDirIterator qmlIt(assetDir.filePath(import.url().split('.').last()),
const QString name = import.url().mid(import.url().indexOf(QLatin1Char('.')) + 1); {QStringLiteral("*.qml")}, QDir::Files);
const QString type = import.url() + QLatin1Char('.') + name; while (qmlIt.hasNext()) {
// For now we assume version is always 1.0 as that's what importer hardcodes qmlIt.next();
itemLibraryEntry.setType(type.toUtf8(), 1, 0); const QString name = qmlIt.fileInfo().baseName();
itemLibraryEntry.setName(name); const QString type = import.url() + QLatin1Char('.') + name;
itemLibraryEntry.setCategory(tr("My Quick3D Components")); // For now we assume version is always 1.0 as that's what importer hardcodes
itemLibraryEntry.setRequiredImport(import.url()); ItemLibraryEntry itemLibraryEntry;
itemLibraryEntry.setLibraryEntryIconPath(iconPath); itemLibraryEntry.setType(type.toUtf8(), 1, 0);
itemLibraryEntry.setTypeIcon(QIcon(iconPath)); 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)) if (!model()->metaInfo().itemLibraryInfo()->containsEntry(itemLibraryEntry))
model()->metaInfo().itemLibraryInfo()->addEntries({itemLibraryEntry}); model()->metaInfo().itemLibraryInfo()->addEntries({itemLibraryEntry});
}
} }
} }

View File

@@ -51,6 +51,8 @@ const char EXPORT_AS_IMAGE[] = "QmlDesigner.ExportAsImage";
const char QML_DESIGNER_SUBFOLDER[] = "/designer/"; const char QML_DESIGNER_SUBFOLDER[] = "/designer/";
const char QUICK_3D_ASSETS_FOLDER[] = "/Quick3DAssets"; 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 DEFAULT_ASSET_IMPORT_FOLDER[] = "/asset_imports";
const char QT_QUICK_3D_MODULE_NAME[] = "QtQuick3D"; const char QT_QUICK_3D_MODULE_NAME[] = "QtQuick3D";