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/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();
if (!m_quick3DFiles.isEmpty()) {
const QHash<QString, QVariantMap> allOptions = m_importer.allOptions();
const QHash<QString, QStringList> supportedExtensions = m_importer.supportedExtensions();
QVector<QJsonObject> 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<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);
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
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<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);
});
// 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);
}
}
}
++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<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();
}
}
}

View File

@@ -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;
};
}

View File

@@ -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>

View File

@@ -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

View File

@@ -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();

View File

@@ -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
const QList<AddResourceHandler> handlers = actionManager->addResourceHandler();
QSet<QString> 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<AddResourceHandler> 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

View File

@@ -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));
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});
}
}
}

View File

@@ -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";