diff --git a/share/qtcreator/qmldesigner/effectMakerQmlSources/EffectMaker.qml b/share/qtcreator/qmldesigner/effectMakerQmlSources/EffectMaker.qml index a94987b9f6d..2430739c469 100644 --- a/share/qtcreator/qmldesigner/effectMakerQmlSources/EffectMaker.qml +++ b/share/qtcreator/qmldesigner/effectMakerQmlSources/EffectMaker.qml @@ -19,7 +19,7 @@ Item { SaveDialog { id: saveDialog anchors.centerIn: parent - onAccepted: print("TODO: export and save effect files") + onAccepted: EffectMakerBackend.effectMakerModel.exportComposition(saveDialog.compositionName) } Column { diff --git a/share/qtcreator/qmldesigner/effectMakerQmlSources/SaveDialog.qml b/share/qtcreator/qmldesigner/effectMakerQmlSources/SaveDialog.qml index 309dd433395..f04a81db9f9 100644 --- a/share/qtcreator/qmldesigner/effectMakerQmlSources/SaveDialog.qml +++ b/share/qtcreator/qmldesigner/effectMakerQmlSources/SaveDialog.qml @@ -17,8 +17,10 @@ StudioControls.Dialog { modal: true implicitWidth: 250 + property string compositionName: null + onOpened: { - nameText.text = "" + nameText.text = "" //TODO: Generate unique name emptyText.opacity = 0 nameText.forceActiveFocus() } @@ -47,7 +49,9 @@ StudioControls.Dialog { validator: validator onTextChanged: { - emptyText.opacity = nameText.text === "" ? 1 : 0 + let validator = /^[A-Z]\w{2,}[A-Za-z0-9_]*$/ + emptyText.visible = text.length > 0 && !validator.test(text) + btnSave.enabled = !emptyText.visible } Keys.onEnterPressed: btnSave.onClicked() Keys.onReturnPressed: btnSave.onClicked() @@ -76,7 +80,10 @@ StudioControls.Dialog { text: qsTr("Save") enabled: nameText.text !== "" - onClicked: root.accept() + onClicked: { + root.compositionName = nameText.text + root.accept() //TODO: Check if name is unique + } } HelperWidgets.Button { diff --git a/src/plugins/effectmakernew/compositionnode.cpp b/src/plugins/effectmakernew/compositionnode.cpp index a57ac74e94f..796b06151f9 100644 --- a/src/plugins/effectmakernew/compositionnode.cpp +++ b/src/plugins/effectmakernew/compositionnode.cpp @@ -104,6 +104,7 @@ void CompositionNode::parse(const QString &qenPath) for (const auto /*QJsonValueRef*/ &prop : jsonProps) { const auto uniform = new Uniform(prop.toObject(), qenPath); m_unifomrsModel.addUniform(uniform); + m_uniforms.append(uniform); g_propertyData.insert(uniform->name(), uniform->value()); } @@ -123,5 +124,15 @@ void CompositionNode::parse(const QString &qenPath) } } +QList CompositionNode::uniforms() const +{ + return m_uniforms; +} + +QString CompositionNode::name() const +{ + return m_name; +} + } // namespace EffectMaker diff --git a/src/plugins/effectmakernew/compositionnode.h b/src/plugins/effectmakernew/compositionnode.h index 2637bfb3879..37203ef4219 100644 --- a/src/plugins/effectmakernew/compositionnode.h +++ b/src/plugins/effectmakernew/compositionnode.h @@ -39,6 +39,10 @@ public: bool isEnabled() const; void setIsEnabled(bool newIsEnabled); + QString name() const; + + QList uniforms() const; + signals: void uniformsModelChanged(); void isEnabledChanged(); @@ -54,6 +58,8 @@ private: QStringList m_requiredNodes; bool m_isEnabled = true; + QList m_uniforms; + EffectMakerUniformsModel m_unifomrsModel; }; diff --git a/src/plugins/effectmakernew/effectmakermodel.cpp b/src/plugins/effectmakernew/effectmakermodel.cpp index 5a49dcb59ce..20879526bae 100644 --- a/src/plugins/effectmakernew/effectmakermodel.cpp +++ b/src/plugins/effectmakernew/effectmakermodel.cpp @@ -18,6 +18,8 @@ #include #include +#include + #include #include @@ -355,6 +357,170 @@ void EffectMakerModel::setEffectError(const QString &errorMessage, int type, int Q_EMIT effectErrorChanged(); } +QString variantAsDataString(const Uniform::Type type, const QVariant &variant) +{ + QString s; + switch (type) { + case Uniform::Type::Bool: + s = variant.toBool() ? QString("true") : QString("false"); + break; + case Uniform::Type::Int: + s = QString::number(variant.toInt()); + break; + case Uniform::Type::Float: + s = QString::number(variant.toDouble()); + break; + case Uniform::Type::Vec2: { + QStringList list; + QVector2D v2 = variant.value(); + list << QString::number(v2.x()); + list << QString::number(v2.y()); + s = list.join(", "); + break; + } + case Uniform::Type::Vec3: { + QStringList list; + QVector3D v3 = variant.value(); + list << QString::number(v3.x()); + list << QString::number(v3.y()); + list << QString::number(v3.z()); + s = list.join(", "); + break; + } + case Uniform::Type::Vec4: { + QStringList list; + QVector4D v4 = variant.value(); + list << QString::number(v4.x()); + list << QString::number(v4.y()); + list << QString::number(v4.z()); + list << QString::number(v4.w()); + s = list.join(", "); + break; + } + case Uniform::Type::Color: { + QStringList list; + QColor c = variant.value(); + list << QString::number(c.redF(), 'g', 3); + list << QString::number(c.greenF(), 'g', 3); + list << QString::number(c.blueF(), 'g', 3); + list << QString::number(c.alphaF(), 'g', 3); + s = list.join(", "); + break; + } + case Uniform::Type::Sampler: + case Uniform::Type::Define: { + s = variant.toString(); + break; + } + } + return s; +} + +QJsonObject nodeToJson(const CompositionNode &node) +{ + QJsonObject nodeObject; + nodeObject.insert("name", node.name()); + if (!node.description().isEmpty()) + nodeObject.insert("description", node.description()); + nodeObject.insert("enabled", node.isEnabled()); + nodeObject.insert("version", 1); + // Add properties + QJsonArray propertiesArray; + const QList uniforms = node.uniforms(); + for (const Uniform *uniform : uniforms) { + QJsonObject uniformObject; + uniformObject.insert("name", QString(uniform->name())); + QString type = Uniform::stringFromType(uniform->type()); + uniformObject.insert("type", type); + + QString value = variantAsDataString(uniform->type(), uniform->value()); + if (uniform->type() == Uniform::Type::Sampler) + value = QFileInfo(value).fileName(); + uniformObject.insert("value", value); + + QString defaultValue = variantAsDataString(uniform->type(), uniform->defaultValue()); + if (uniform->type() == Uniform::Type::Sampler) { + defaultValue = QFileInfo(value).fileName(); + if (uniform->enableMipmap()) + uniformObject.insert("enableMipmap", uniform->enableMipmap()); + } + uniformObject.insert("defaultValue", defaultValue); + if (!uniform->description().isEmpty()) + uniformObject.insert("description", uniform->description()); + if (uniform->type() == Uniform::Type::Float + || uniform->type() == Uniform::Type::Int + || uniform->type() == Uniform::Type::Vec2 + || uniform->type() == Uniform::Type::Vec3 + || uniform->type() == Uniform::Type::Vec4) { + uniformObject.insert("minValue", variantAsDataString(uniform->type(), uniform->minValue())); + uniformObject.insert("maxValue", variantAsDataString(uniform->type(), uniform->maxValue())); + } + if (!uniform->customValue().isEmpty()) + uniformObject.insert("customValue", uniform->customValue()); + if (uniform->useCustomValue()) + uniformObject.insert("useCustomValue", true); + + propertiesArray.append(uniformObject); + } + if (!propertiesArray.isEmpty()) + nodeObject.insert("properties", propertiesArray); + + // Add shaders + if (!node.fragmentCode().trimmed().isEmpty()) { + QJsonArray fragmentCodeArray; + const QStringList fsLines = node.fragmentCode().split('\n'); + for (const QString &line : fsLines) + fragmentCodeArray.append(line); + + if (!fragmentCodeArray.isEmpty()) + nodeObject.insert("fragmentCode", fragmentCodeArray); + } + if (!node.vertexCode().trimmed().isEmpty()) { + QJsonArray vertexCodeArray; + const QStringList vsLines = node.vertexCode().split('\n'); + for (const QString &line : vsLines) + vertexCodeArray.append(line); + + if (!vertexCodeArray.isEmpty()) + nodeObject.insert("vertexCode", vertexCodeArray); + } + + return nodeObject; +} + +void EffectMakerModel::exportComposition(const QString &name) +{ + const QString effectsAssetsDir = QmlDesigner::ModelNodeOperations::getEffectsDefaultDirectory(); + const QString path = effectsAssetsDir + QDir::separator() + name + ".qep"; + auto saveFile = QFile(path); + if (!saveFile.open(QIODevice::WriteOnly)) { + QString error = QString("Error: Couldn't save composition file: '%1'").arg(path); + qWarning() << error; + return; + } + + QJsonObject json; + // File format version + json.insert("version", 1); + + // Add nodes + QJsonArray nodesArray; + for (const CompositionNode *node : std::as_const(m_nodes)) { + QJsonObject nodeObject = nodeToJson(*node); + nodesArray.append(nodeObject); + } + + if (!nodesArray.isEmpty()) + json.insert("nodes", nodesArray); + + QJsonObject rootJson; + rootJson.insert("QEP", json); + QJsonDocument jsonDoc(rootJson); + + saveFile.write(jsonDoc.toJson()); + saveFile.close(); +} + void EffectMakerModel::resetEffectError(int type) { if (m_effectErrors.contains(type)) { diff --git a/src/plugins/effectmakernew/effectmakermodel.h b/src/plugins/effectmakernew/effectmakermodel.h index 3be8701deae..5f7550bb37f 100644 --- a/src/plugins/effectmakernew/effectmakermodel.h +++ b/src/plugins/effectmakernew/effectmakermodel.h @@ -83,6 +83,8 @@ public: Q_INVOKABLE void resetEffectError(int type); Q_INVOKABLE void setEffectError(const QString &errorMessage, int type = -1, int lineNumber = -1); + Q_INVOKABLE void exportComposition(const QString &name); + signals: void isEmptyChanged(); void selectedIndexChanged(int idx); diff --git a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp index 1ed1c09b0c1..53c7b7fa216 100644 --- a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp +++ b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.cpp @@ -1683,6 +1683,12 @@ Utils::FilePath getEffectsImportDirectory() QString getEffectsDefaultDirectory(const QString &defaultDir) { + if (defaultDir.isEmpty()) { + return Utils::FilePath::fromString(getAssetDefaultDirectory( + "effects", + QmlDesignerPlugin::instance()->documentManager().currentProjectDirPath().toString())).toString(); + } + return getAssetDefaultDirectory("effects", defaultDir); } diff --git a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h index c73530e02dd..7d7a985283a 100644 --- a/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h +++ b/src/plugins/qmldesigner/components/componentcore/modelnodeoperations.h @@ -125,7 +125,7 @@ void openSignalDialog(const SelectionContext &selectionContext); void updateImported3DAsset(const SelectionContext &selectionContext); QMLDESIGNERCOMPONENTS_EXPORT Utils::FilePath getEffectsImportDirectory(); -QMLDESIGNERCOMPONENTS_EXPORT QString getEffectsDefaultDirectory(const QString &defaultDir); +QMLDESIGNERCOMPONENTS_EXPORT QString getEffectsDefaultDirectory(const QString &defaultDir = {}); void openEffectMaker(const QString &filePath); QString getEffectIcon(const QString &effectPath); bool useLayerEffect();