QmlDesigner: Allow saving created effect nodes for later use

Effect nodes can be added to "effect node library", which is stored
in user's documents folder. Nodes in the library are available via
"Add Effect" combobox, where they are shown under custom category.

Fixes: QDS-14132
Change-Id: I753eeaffbfa9bf0f45d3dfcf643b52a86141f3be
Reviewed-by: Mahmoud Badri <mahmoud.badri@qt.io>
Reviewed-by: Ali Kianian <ali.kianian@qt.io>
This commit is contained in:
Miikka Heikkinen
2025-03-21 16:23:49 +02:00
parent a090d6d58f
commit 6e4fbae15c
18 changed files with 490 additions and 67 deletions

View File

@@ -14,6 +14,10 @@ Rectangle {
border.color: StudioTheme.Values.themeControlOutline border.color: StudioTheme.Values.themeControlOutline
color: StudioTheme.Values.themeSectionHeadBackground color: StudioTheme.Values.themeSectionHeadBackground
property alias text: textLabel.text
property alias acceptButtonLabel: acceptButton.text
property alias cancelButtonLabel: cancelButton.text
signal accepted() signal accepted()
signal canceled() signal canceled()
@@ -29,13 +33,13 @@ Rectangle {
width: parent.width width: parent.width
height: 50 height: 50
Text { Text {
id: textLabel
anchors.centerIn: parent anchors.centerIn: parent
color: StudioTheme.Values.themeTextColor color: StudioTheme.Values.themeTextColor
font.bold: true font.bold: true
font.pixelSize: StudioTheme.Values.baseFontSize font.pixelSize: StudioTheme.Values.baseFontSize
horizontalAlignment: Text.AlignLeft horizontalAlignment: Text.AlignLeft
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
text: qsTr("The property is in use in the shader code.\nAre you sure you want to remove it?")
} }
} }
@@ -64,7 +68,7 @@ Rectangle {
id: acceptButton id: acceptButton
width: 80 width: 80
height: 30 height: 30
text: qsTr("Remove") text: qsTr("Accept")
padding: 4 padding: 4
onClicked: { onClicked: {

View File

@@ -60,56 +60,118 @@ HelperWidgets.Section {
leftPadding: StudioTheme.Values.toolbarSpacing leftPadding: StudioTheme.Values.toolbarSpacing
} }
Item {
id: nodeConfirmFormParent
width: parent.width
height: childrenRect.height
}
TextEdit { TextEdit {
id: warningText id: infoText
visible: false visible: false
height: 60 height: 60
width: root.width width: root.width
text: qsTr("A node with this name already exists.\nSuffix was added to make the name unique.")
horizontalAlignment: Text.AlignHCenter horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter verticalAlignment: Text.AlignVCenter
color: StudioTheme.Values.themeWarning
readOnly: true readOnly: true
wrapMode: TextEdit.WordWrap
function showNodeExistsWarning(enable)
{
infoText.text = qsTr("An effect with this name already exists.\nSuffix was added to make the name unique.")
infoTimer.restart()
infoText.visible = enable
infoText.color = StudioTheme.Values.themeWarning
}
function showNodeAddedToLibraryInfo(message)
{
let errorTag = "@ERROR@"
if (message.startsWith(errorTag)) {
infoText.text = message.substring(errorTag.length)
infoText.color = StudioTheme.Values.themeError
} else {
infoText.text = message
infoText.color = StudioTheme.Values.themeInteraction
}
infoTimer.restart()
infoText.visible = message !== ""
}
function showNeedRenameInfo()
{
infoText.text = qsTr("A built-in effect with this name already exists in the library.\nPlease rename the effect before adding it to the library.")
infoText.visible = true
infoText.color = StudioTheme.Values.themeWarning
}
onVisibleChanged: { onVisibleChanged: {
if (warningText.visible) { if (infoText.visible)
warningTimer.running = true
root.expanded = true // so that warning is visible root.expanded = true // so that warning is visible
} }
}
Timer { Timer {
id: warningTimer id: infoTimer
interval: 12000 interval: 12000
repeat: false repeat: false
running: false running: false
onTriggered: { onTriggered: {
warningText.visible = false infoText.visible = false
warningTimer.running = false infoTimer.running = false
} }
} }
} }
ConfirmPropertyRemoveForm { ConfirmForm {
id: confirmRemoveForm id: confirmForm
property int uniformIndex: -1 property int confirmIndex: -1
property bool confirmingRemove: false
width: root.width - StudioTheme.Values.scrollBarThicknessHover - 8 width: root.width - StudioTheme.Values.scrollBarThicknessHover - 8
visible: false visible: false
onHeightChanged: root.emitEnsure(confirmRemoveForm) onHeightChanged: root.emitEnsure(confirmForm)
onVisibleChanged: root.emitEnsure(confirmRemoveForm) onVisibleChanged: root.emitEnsure(confirmForm)
onAccepted: { function showConfirmRemove(formParent, uniformIndex)
confirmRemoveForm.parent = root {
nodeUniformsModel.remove(confirmRemoveForm.uniformIndex) confirmForm.parent = formParent
confirmForm.confirmIndex = uniformIndex
text = qsTr("The property is in use in the shader code.\nAre you sure you want to remove it?")
acceptButtonLabel = qsTr("Remove")
confirmForm.confirmingRemove = true
confirmForm.visible = true
} }
onCanceled: confirmRemoveForm.parent = root function showConfirmLibraryAdd(nodeIndex)
{
confirmForm.parent = nodeConfirmFormParent
confirmForm.confirmIndex = nodeIndex
text = qsTr("The effect is already added into the library.\nAre you sure you want to update it?")
acceptButtonLabel = qsTr("Update")
confirmForm.confirmingRemove = false
confirmForm.visible = true
}
function clear()
{
confirmForm.visible = false
confirmForm.parent = root
}
onAccepted: {
confirmForm.clear()
if (confirmForm.confirmingRemove)
nodeUniformsModel.remove(confirmForm.confirmIndex)
else
infoText.showNodeAddedToLibraryInfo(root.backendModel.addNodeToLibraryNode(confirmForm.confirmIndex))
}
onCanceled: confirmForm.clear()
} }
MouseArea { MouseArea {
@@ -149,7 +211,7 @@ HelperWidgets.Section {
onEditingFinished: { onEditingFinished: {
nameEditField.visible = false nameEditField.visible = false
warningText.visible = !root.backendModel.changeNodeName(modelIndex, nameEditText.text) infoText.showNodeExistsWarning(!root.backendModel.changeNodeName(modelIndex, nameEditText.text))
} }
onActiveFocusChanged: { onActiveFocusChanged: {
@@ -180,14 +242,19 @@ HelperWidgets.Section {
iconColor: StudioTheme.Values.themeTextColor iconColor: StudioTheme.Values.themeTextColor
iconScale: nameEditButton.containsMouse ? 1.2 : 1 iconScale: nameEditButton.containsMouse ? 1.2 : 1
implicitWidth: width implicitWidth: width
tooltip: qsTr("Edit effect node name") tooltip: qsTr("Edit effect name")
onPressed: (event) => { function handlePress()
{
nameEditText.text = nodeName nameEditText.text = nodeName
nameEditText.initializing = true nameEditText.initializing = true
nameEditField.visible = true nameEditField.visible = true
nameEditText.forceActiveFocus() nameEditText.forceActiveFocus()
} }
onPressed: (event) => {
nameEditButton.handlePress()
}
} }
} }
} }
@@ -223,17 +290,16 @@ HelperWidgets.Section {
onReset: nodeUniformsModel.resetData(index) onReset: nodeUniformsModel.resetData(index)
onRemove: { onRemove: {
confirmForm.clear()
if (uniformIsInUse) { if (uniformIsInUse) {
confirmRemoveForm.parent = effectCompositionNodeUniform.editPropertyFormParent confirmForm.showConfirmRemove(
confirmRemoveForm.uniformIndex = index effectCompositionNodeUniform.editPropertyFormParent, index)
confirmRemoveForm.visible = true
} else { } else {
nodeUniformsModel.remove(index) nodeUniformsModel.remove(index)
} }
} }
onEdit: { onEdit: {
confirmRemoveForm.visible = false confirmForm.clear()
confirmRemoveForm.parent = root
addPropertyForm.parent = effectCompositionNodeUniform.editPropertyFormParent addPropertyForm.parent = effectCompositionNodeUniform.editPropertyFormParent
let dispNames = nodeUniformsModel.displayNames() let dispNames = nodeUniformsModel.displayNames()
let filteredDispNames = dispNames.filter(name => name !== uniformDisplayName); let filteredDispNames = dispNames.filter(name => name !== uniformDisplayName);
@@ -294,9 +360,14 @@ HelperWidgets.Section {
enabled: !addPropertyForm.visible enabled: !addPropertyForm.visible
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
HelperWidgets.ToolTipArea {
anchors.fill: parent
tooltip: qsTr("Add new property to the effect.")
acceptedButtons: Qt.NoButton
}
onClicked: { onClicked: {
confirmRemoveForm.visible = false confirmForm.clear()
confirmRemoveForm.parent = root
root.editedUniformIndex = -1 root.editedUniformIndex = -1
addPropertyForm.parent = addProperty addPropertyForm.parent = addProperty
addPropertyForm.reservedDispNames = nodeUniformsModel.displayNames() addPropertyForm.reservedDispNames = nodeUniformsModel.displayNames()
@@ -310,7 +381,50 @@ HelperWidgets.Section {
height: 30 height: 30
text: qsTr("Show Code") text: qsTr("Show Code")
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
onClicked: root.backendModel.openCodeEditor(index)
HelperWidgets.ToolTipArea {
anchors.fill: parent
tooltip: qsTr("Open the shader code editor.")
acceptedButtons: Qt.NoButton
}
onClicked: {
confirmForm.clear()
root.backendModel.openCodeEditor(index)
}
}
HelperWidgets.Button {
id: addToLibraryButton
width: 100
height: 30
text: qsTr("Add to Library")
anchors.verticalCenter: parent.verticalCenter
HelperWidgets.ToolTipArea {
anchors.fill: parent
tooltip: qsTr("Add the effect to the effect library.\nYou can reuse effects added to the library in other effect compositions.")
acceptedButtons: Qt.NoButton
}
function handleClicked()
{
confirmForm.clear()
if (root.backendModel.canAddNodeToLibrary(index)) {
if (root.backendModel.nodeExists(index)) {
infoText.visible = false
confirmForm.showConfirmLibraryAdd(index)
} else {
infoText.showNodeAddedToLibraryInfo(root.backendModel
.addNodeToLibraryNode(index))
}
} else {
infoText.showNeedRenameInfo()
nameEditButton.handlePress()
}
}
onClicked: addToLibraryButton.handleClicked()
} }
} }
} }

View File

@@ -15,16 +15,18 @@ Rectangle {
width: 140 width: 140
height: 32 height: 32
color: mouseArea.containsMouse && modelData.canBeAdded color: mouseArea.containsMouse && modelData.canBeAdded && root.enabled
? StudioTheme.Values.themeControlBackgroundInteraction : "transparent" ? StudioTheme.Values.themeControlBackgroundInteraction : "transparent"
signal addEffectNode(var nodeQenPath) signal addEffectNode(var nodeQenPath)
signal removeEffectNodeFromLibrary(var nodeName)
ToolTipArea { ToolTipArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
acceptedButtons: Qt.LeftButton acceptedButtons: Qt.LeftButton
visible: root.enabled
tooltip: modelData.canBeAdded ? modelData.nodeDescription tooltip: modelData.canBeAdded ? modelData.nodeDescription
: qsTr("An effect with same properties already exists, this effect cannot be added.") : qsTr("An effect with same properties already exists, this effect cannot be added.")
@@ -59,6 +61,28 @@ Rectangle {
anchors.verticalCenter: nodeIcon.verticalCenter anchors.verticalCenter: nodeIcon.verticalCenter
wrapMode: Text.WordWrap wrapMode: Text.WordWrap
width: parent.width - parent.spacing - nodeIcon.width width: parent.width - parent.spacing - nodeIcon.width
IconButton {
id: removeButton
visible: root.enabled && modelData.canBeRemoved
&& (mouseArea.containsMouse || removeButton.containsMouse)
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
anchors.rightMargin: StudioTheme.Values.iconAreaWidth / 4
icon: StudioTheme.Constants.close_small
transparentBg: false
buttonSize: StudioTheme.Values.iconAreaWidth
iconSize: StudioTheme.Values.smallIconFontSize
iconColor: StudioTheme.Values.themeTextColor
iconScale: removeButton.containsMouse ? 1.2 : 1
implicitWidth: width
tooltip: qsTr("Remove custom effect from the library.")
onPressed: (event) => {
root.removeEffectNodeFromLibrary(modelData.nodeName)
}
}
} }
} }
} }

View File

@@ -78,8 +78,10 @@ StudioControls.ComboBox {
flags: Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint flags: Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
onActiveFocusItemChanged: { onActiveFocusItemChanged: {
if (!window.activeFocusItem && !root.hovered && root.popup.opened) if (!window.activeFocusItem && !root.hovered && root.popup.opened) {
root.popup.close() root.popup.close()
confirmRemoveForm.visible = false
}
} }
Rectangle { Rectangle {
@@ -92,6 +94,7 @@ StudioControls.ComboBox {
HelperWidgets.ScrollView { HelperWidgets.ScrollView {
anchors.fill: parent anchors.fill: parent
anchors.margins: 1 anchors.margins: 1
enabled: !confirmRemoveForm.visible
Row { Row {
id: row id: row
@@ -118,10 +121,15 @@ StudioControls.ComboBox {
EffectNode { EffectNode {
onAddEffectNode: (nodeQenPath) => { onAddEffectNode: (nodeQenPath) => {
EffectComposerBackend.rootView.addEffectNode(modelData.nodeQenPath) EffectComposerBackend.rootView.addEffectNode(nodeQenPath)
confirmRemoveForm.visible = false
root.popup.close() root.popup.close()
root.nodeJustAdded = true root.nodeJustAdded = true
} }
onRemoveEffectNodeFromLibrary: (nodeName) => {
confirmRemoveForm.visible = true
confirmRemoveForm.nodeName = nodeName
}
} }
} }
} }
@@ -129,10 +137,36 @@ StudioControls.ComboBox {
} }
} }
ConfirmForm {
id: confirmRemoveForm
property string nodeName
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
width: 350
height: 110
visible: false
text: qsTr("The node removal from library cannot be undone.\nAre you sure you want to remove node:\n'%1'?")
.arg(confirmRemoveForm.nodeName)
acceptButtonLabel: qsTr("Remove")
onAccepted: {
EffectComposerBackend.rootView.removeEffectNodeFromLibrary(confirmRemoveForm.nodeName)
confirmRemoveForm.visible = false
}
onCanceled: confirmRemoveForm.visible = false
}
Keys.onPressed: function(event) { Keys.onPressed: function(event) {
if (event.key === Qt.Key_Escape && root.popup.opened) if (event.key === Qt.Key_Escape && root.popup.opened) {
confirmRemoveForm.visible = false
root.popup.close() root.popup.close()
} }
} }
} }
} }
}

View File

@@ -160,6 +160,14 @@ bool CompositionNode::isCustom() const
return m_isCustom; return m_isCustom;
} }
void CompositionNode::setCustom(bool enable)
{
if (enable != m_isCustom) {
m_isCustom = enable;
emit isCustomChanged();
}
}
CompositionNode::NodeType CompositionNode::type() const CompositionNode::NodeType CompositionNode::type() const
{ {
return m_type; return m_type;

View File

@@ -24,7 +24,7 @@ class CompositionNode : public QObject
Q_PROPERTY(QString nodeName READ name WRITE setName NOTIFY nameChanged) Q_PROPERTY(QString nodeName READ name WRITE setName NOTIFY nameChanged)
Q_PROPERTY(bool nodeEnabled READ isEnabled WRITE setIsEnabled NOTIFY isEnabledChanged) Q_PROPERTY(bool nodeEnabled READ isEnabled WRITE setIsEnabled NOTIFY isEnabledChanged)
Q_PROPERTY(bool isDependency READ isDependency NOTIFY isDepencyChanged) Q_PROPERTY(bool isDependency READ isDependency NOTIFY isDepencyChanged)
Q_PROPERTY(bool isCustom READ isCustom CONSTANT) Q_PROPERTY(bool isCustom READ isCustom NOTIFY isCustomChanged)
Q_PROPERTY(QObject *nodeUniformsModel READ uniformsModel NOTIFY uniformsModelChanged) Q_PROPERTY(QObject *nodeUniformsModel READ uniformsModel NOTIFY uniformsModelChanged)
Q_PROPERTY( Q_PROPERTY(
QString fragmentCode QString fragmentCode
@@ -59,6 +59,7 @@ public:
bool isDependency() const; bool isDependency() const;
bool isCustom() const; bool isCustom() const;
void setCustom(bool enable);
QString name() const; QString name() const;
void setName(const QString &name); void setName(const QString &name);
@@ -89,6 +90,7 @@ signals:
void fragmentCodeChanged(); void fragmentCodeChanged();
void vertexCodeChanged(); void vertexCodeChanged();
void nameChanged(); void nameChanged();
void isCustomChanged();
private slots: private slots:
void onUniformRenamed(const QString &oldName, const QString &newName); void onUniformRenamed(const QString &oldName, const QString &newName);

View File

@@ -5,6 +5,7 @@
#include "compositionnode.h" #include "compositionnode.h"
#include "effectcomposertr.h" #include "effectcomposertr.h"
#include "effectcomposernodesmodel.h"
#include "effectshaderscodeeditor.h" #include "effectshaderscodeeditor.h"
#include "effectutils.h" #include "effectutils.h"
#include "propertyhandler.h" #include "propertyhandler.h"
@@ -31,7 +32,6 @@
#include <QFileDialog> #include <QFileDialog>
#include <QLibraryInfo> #include <QLibraryInfo>
#include <QStandardPaths> #include <QStandardPaths>
#include <QTemporaryDir>
#include <QVector2D> #include <QVector2D>
using namespace Qt::StringLiterals; using namespace Qt::StringLiterals;
@@ -43,6 +43,7 @@ static constexpr int MAIN_CODE_EDITOR_INDEX = -2;
EffectComposerModel::EffectComposerModel(QObject *parent) EffectComposerModel::EffectComposerModel(QObject *parent)
: QAbstractListModel{parent} : QAbstractListModel{parent}
, m_effectComposerNodesModel{new EffectComposerNodesModel(this)}
, m_codeEditorIndex(INVALID_CODE_EDITOR_INDEX) , m_codeEditorIndex(INVALID_CODE_EDITOR_INDEX)
, m_shaderDir(QDir::tempPath() + "/qds_ec_XXXXXX") , m_shaderDir(QDir::tempPath() + "/qds_ec_XXXXXX")
, m_currentPreviewColor("#dddddd") , m_currentPreviewColor("#dddddd")
@@ -53,6 +54,11 @@ EffectComposerModel::EffectComposerModel(QObject *parent)
connectCodeEditor(); connectCodeEditor();
} }
EffectComposerNodesModel *EffectComposerModel::effectComposerNodesModel() const
{
return m_effectComposerNodesModel.data();
}
QHash<int, QByteArray> EffectComposerModel::roleNames() const QHash<int, QByteArray> EffectComposerModel::roleNames() const
{ {
static const QHash<int, QByteArray> roles = { static const QHash<int, QByteArray> roles = {
@@ -74,7 +80,7 @@ int EffectComposerModel::rowCount(const QModelIndex &parent) const
QVariant EffectComposerModel::data(const QModelIndex &index, int role) const QVariant EffectComposerModel::data(const QModelIndex &index, int role) const
{ {
QTC_ASSERT(index.isValid() && index.row() < m_nodes.size(), return {}); QTC_ASSERT(index.isValid() && isValidIndex(index.row()), return {});
QTC_ASSERT(roleNames().contains(role), return {}); QTC_ASSERT(roleNames().contains(role), return {});
return m_nodes.at(index.row())->property(roleNames().value(role)); return m_nodes.at(index.row())->property(roleNames().value(role));
@@ -226,7 +232,7 @@ void EffectComposerModel::removeNode(int idx)
// Returns false if new name was generated // Returns false if new name was generated
bool EffectComposerModel::changeNodeName(int nodeIndex, const QString &name) bool EffectComposerModel::changeNodeName(int nodeIndex, const QString &name)
{ {
QTC_ASSERT(nodeIndex >= 0 && nodeIndex < m_nodes.size(), return false); QTC_ASSERT(isValidIndex(nodeIndex), return false);
QString trimmedName = name.trimmed(); QString trimmedName = name.trimmed();
const QString oldName = m_nodes[nodeIndex]->name(); const QString oldName = m_nodes[nodeIndex]->name();
@@ -237,8 +243,9 @@ bool EffectComposerModel::changeNodeName(int nodeIndex, const QString &name)
const QStringList reservedNames = nodeNames(); const QStringList reservedNames = nodeNames();
// Matching is done case-insensitive as section headers are shown in all uppercase // Matching is done case-insensitive as section headers are shown in all uppercase
QString newName = QmlDesigner::UniqueName::generate(trimmedName, [&oldName, &reservedNames] (const QString &checkName) -> bool { QString newName = QmlDesigner::UniqueName::generate(trimmedName, [&] (const QString &checkName) -> bool {
return oldName != checkName && reservedNames.contains(checkName, Qt::CaseInsensitive); return oldName != checkName && (reservedNames.contains(checkName, Qt::CaseInsensitive)
|| m_effectComposerNodesModel->nodeExists(checkName));
}); });
if (newName != oldName) { if (newName != oldName) {
@@ -1326,6 +1333,91 @@ void EffectComposerModel::openMainCodeEditor()
setCodeEditorIndex(MAIN_CODE_EDITOR_INDEX); setCodeEditorIndex(MAIN_CODE_EDITOR_INDEX);
} }
bool EffectComposerModel::canAddNodeToLibrary(int idx)
{
QTC_ASSERT(isValidIndex(idx), return false);
CompositionNode *node = m_nodes.at(idx);
if (m_effectComposerNodesModel->isBuiltIn(node->name()))
return !m_effectComposerNodesModel->nodeExists(node->name());
return true;
}
bool EffectComposerModel::nodeExists(int idx)
{
QTC_ASSERT(isValidIndex(idx), return false);
CompositionNode *node = m_nodes.at(idx);
return m_effectComposerNodesModel->nodeExists(node->name());
}
QString EffectComposerModel::addNodeToLibraryNode(int idx)
{
const QString errorTag{"@ERROR@"};
QTC_ASSERT(isValidIndex(idx) && canAddNodeToLibrary(idx), return errorTag);
CompositionNode *node = m_nodes.at(idx);
// Adding node to library changes it to a custom one, as we can't overwrite builtin nodes
node->setCustom(true);
QString fileNameBase = EffectUtils::nodeNameToFileName(node->name());
Utils::FilePath nodePath = Utils::FilePath::fromString(EffectUtils::nodeLibraryPath())
.pathAppended(fileNameBase);
Utils::FilePath qenFile = nodePath.pathAppended(fileNameBase + ".qen");
QSet<QString> newFileNames = {qenFile.fileName()};
Utils::FilePaths oldFiles;
bool update = nodePath.exists();
if (update)
oldFiles = nodePath.dirEntries(QDir::Files);
else
nodePath.createDir();
QJsonObject rootObj;
QJsonObject nodeObject = nodeToJson(*node);
rootObj.insert("QEN", nodeObject);
QJsonDocument jsonDoc(rootObj);
Utils::expected_str<qint64> result = qenFile.writeFileContents(jsonDoc.toJson());
if (!result)
return errorTag + Tr::tr("Failed to write QEN file for effect:\n%1").arg(qenFile.fileName());
QList<Utils::FilePath> sources;
QStringList dests;
const QList<Uniform *> uniforms = node->uniforms();
for (Uniform *uniform : uniforms) {
const QString valueStr = uniform->value().toString();
if (uniform->type() == Uniform::Type::Sampler && !valueStr.isEmpty()) {
Utils::FilePath imagePath = Utils::FilePath::fromString(valueStr);
QString imageFilename = imagePath.fileName();
sources.append(imagePath);
dests.append(imageFilename);
}
}
for (int i = 0; i < sources.count(); ++i) {
Utils::FilePath source = sources[i];
Utils::FilePath target = nodePath.pathAppended(dests[i]);
newFileNames.insert(target.fileName());
if (target.exists() && source.fileName() != target.fileName())
target.removeFile(); // Remove existing file for update
if (!target.exists() && !source.copyFile(target))
return errorTag + Tr::tr("Failed to copy effect resource:\n%1").arg(source.fileName());
}
// Delete old content that was not overwritten
for (const Utils::FilePath &oldFile : std::as_const(oldFiles)) {
if (!newFileNames.contains(oldFile.fileName()))
oldFile.removeFile();
}
m_effectComposerNodesModel->loadCustomNodes();
QString action = update ? Tr::tr("updated in") : Tr::tr("added to");
return Tr::tr("Effect was %1 effect library.").arg(action);
}
QVariant EffectComposerModel::valueLimit(const QString &type, bool max) const QVariant EffectComposerModel::valueLimit(const QString &type, bool max) const
{ {
static const int intMin = std::numeric_limits<int>::lowest(); static const int intMin = std::numeric_limits<int>::lowest();
@@ -1619,7 +1711,7 @@ void EffectComposerModel::saveResources(const QString &name)
newFileNames.insert(target.fileName()); newFileNames.insert(target.fileName());
if (target.exists() && source.fileName() != target.fileName()) if (target.exists() && source.fileName() != target.fileName())
target.removeFile(); // Remove existing file for update target.removeFile(); // Remove existing file for update
if (!source.copyFile(target) && !target.exists()) if (!target.exists() && !source.copyFile(target))
setEffectError(QString("Failed to copy file: %1").arg(source.toFSPathString())); setEffectError(QString("Failed to copy file: %1").arg(source.toFSPathString()));
if (fileNameToUniformHash.contains(dests[i])) { if (fileNameToUniformHash.contains(dests[i])) {
@@ -2724,7 +2816,7 @@ QString EffectComposerModel::stripFileFromURL(const QString &urlString) const
void EffectComposerModel::addOrUpdateNodeUniform(int idx, const QVariantMap &data, int updateIndex) void EffectComposerModel::addOrUpdateNodeUniform(int idx, const QVariantMap &data, int updateIndex)
{ {
QTC_ASSERT(m_nodes.size() > idx && idx >= 0, return); QTC_ASSERT(isValidIndex(idx), return);
// Convert values to Uniform digestible strings // Convert values to Uniform digestible strings
auto fixedData = data; auto fixedData = data;
@@ -2755,4 +2847,9 @@ bool EffectComposerModel::writeToFile(
return true; return true;
} }
bool EffectComposerModel::isValidIndex(int idx) const
{
return m_nodes.size() > idx && idx >= 0;
}
} // namespace EffectComposer } // namespace EffectComposer

View File

@@ -29,6 +29,7 @@ class Process;
namespace EffectComposer { namespace EffectComposer {
class CompositionNode; class CompositionNode;
class EffectComposerNodesModel;
class EffectShadersCodeEditor; class EffectShadersCodeEditor;
struct ShaderEditorData; struct ShaderEditorData;
class Uniform; class Uniform;
@@ -68,6 +69,8 @@ class EffectComposerModel : public QAbstractListModel
public: public:
EffectComposerModel(QObject *parent = nullptr); EffectComposerModel(QObject *parent = nullptr);
EffectComposerNodesModel *effectComposerNodesModel() const;
QHash<int, QByteArray> roleNames() const override; QHash<int, QByteArray> roleNames() const override;
int rowCount(const QModelIndex & parent = QModelIndex()) const override; int rowCount(const QModelIndex & parent = QModelIndex()) const override;
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
@@ -130,6 +133,10 @@ public:
Q_INVOKABLE void openCodeEditor(int idx); Q_INVOKABLE void openCodeEditor(int idx);
Q_INVOKABLE void openMainCodeEditor(); Q_INVOKABLE void openMainCodeEditor();
Q_INVOKABLE bool canAddNodeToLibrary(int idx);
Q_INVOKABLE bool nodeExists(int idx);
Q_INVOKABLE QString addNodeToLibraryNode(int idx);
Q_INVOKABLE QVariant valueLimit(const QString &type, bool max) const; Q_INVOKABLE QVariant valueLimit(const QString &type, bool max) const;
void openComposition(const QString &path); void openComposition(const QString &path);
@@ -265,6 +272,7 @@ private:
bool writeToFile(const QByteArray &buf, const QString &filename, FileType fileType); bool writeToFile(const QByteArray &buf, const QString &filename, FileType fileType);
QList<CompositionNode *> m_nodes; QList<CompositionNode *> m_nodes;
QPointer<EffectComposerNodesModel> m_effectComposerNodesModel;
int m_selectedIndex = -1; int m_selectedIndex = -1;
int m_codeEditorIndex = -1; int m_codeEditorIndex = -1;

View File

@@ -2,6 +2,7 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "effectcomposernodesmodel.h" #include "effectcomposernodesmodel.h"
#include "effectutils.h" #include "effectutils.h"
#include <utils/algorithm.h> #include <utils/algorithm.h>
@@ -12,6 +13,8 @@
namespace EffectComposer { namespace EffectComposer {
constexpr QStringView customCatName{u"Custom"};
EffectComposerNodesModel::EffectComposerNodesModel(QObject *parent) EffectComposerNodesModel::EffectComposerNodesModel(QObject *parent)
: QAbstractListModel{parent} : QAbstractListModel{parent}
{ {
@@ -53,7 +56,8 @@ void EffectComposerNodesModel::loadModel()
return; return;
} }
m_categories = {}; m_categories.clear();
m_builtInNodeNames.clear();
QDirIterator itCategories(nodesPath.toUrlishString(), QDir::Dirs | QDir::NoDotAndDotDot); QDirIterator itCategories(nodesPath.toUrlishString(), QDir::Dirs | QDir::NoDotAndDotDot);
while (itCategories.hasNext()) { while (itCategories.hasNext()) {
@@ -69,10 +73,11 @@ void EffectComposerNodesModel::loadModel()
QDirIterator itEffects(categoryPath.toUrlishString(), {"*.qen"}, QDir::Files); QDirIterator itEffects(categoryPath.toUrlishString(), {"*.qen"}, QDir::Files);
while (itEffects.hasNext()) { while (itEffects.hasNext()) {
itEffects.next(); itEffects.next();
auto node = new EffectNode(itEffects.filePath()); auto node = new EffectNode(itEffects.filePath(), true);
if (!node->defaultImagesHash().isEmpty()) if (!node->defaultImagesHash().isEmpty())
m_defaultImagesHash.insert(node->name(), node->defaultImagesHash()); m_defaultImagesHash.insert(node->name(), node->defaultImagesHash());
effects.push_back(node); effects.push_back(node);
m_builtInNodeNames.append(node->name());
} }
catName[0] = catName[0].toUpper(); // capitalize first letter catName[0] = catName[0].toUpper(); // capitalize first letter
@@ -80,11 +85,15 @@ void EffectComposerNodesModel::loadModel()
EffectNodesCategory *category = new EffectNodesCategory(catName, effects); EffectNodesCategory *category = new EffectNodesCategory(catName, effects);
m_categories.push_back(category); m_categories.push_back(category);
if (catName == customCatName && !effects.isEmpty()) {
m_builtinCustomNode = effects[0];
m_customCategory = category;
}
} }
const QString customCatName = "Custom";
std::sort(m_categories.begin(), m_categories.end(), std::sort(m_categories.begin(), m_categories.end(),
[&customCatName](EffectNodesCategory *a, EffectNodesCategory *b) { [](EffectNodesCategory *a, EffectNodesCategory *b) {
if (a->name() == customCatName) if (a->name() == customCatName)
return false; return false;
if (b->name() == customCatName) if (b->name() == customCatName)
@@ -94,6 +103,40 @@ void EffectComposerNodesModel::loadModel()
m_modelLoaded = true; m_modelLoaded = true;
loadCustomNodes();
}
void EffectComposerNodesModel::loadCustomNodes()
{
if (!m_customCategory)
return;
for (const QString &nodeName : std::as_const(m_customNodeNames))
m_defaultImagesHash.remove(nodeName);
m_customNodeNames.clear();
QList<EffectNode *> effects;
const Utils::FilePath nodeLibPath = Utils::FilePath::fromString(EffectUtils::nodeLibraryPath());
const Utils::FilePaths libraryNodes = nodeLibPath.dirEntries(QDir::Dirs | QDir::NoDotAndDotDot);
for (const Utils::FilePath &nodePath : libraryNodes) {
const Utils::FilePath qenPath = nodePath.pathAppended(nodePath.fileName() + ".qen");
auto node = new EffectNode(qenPath.toFSPathString(), false);
if (!node->defaultImagesHash().isEmpty())
m_defaultImagesHash.insert(node->name(), node->defaultImagesHash());
m_customNodeNames.append(node->name());
effects.push_back(node);
}
Utils::sort(effects, &EffectNode::name);
if (m_customCategory) {
if (m_builtinCustomNode)
effects.prepend(m_builtinCustomNode);
m_customCategory->setNodes(effects);
}
resetModel(); resetModel();
} }
@@ -103,6 +146,17 @@ void EffectComposerNodesModel::resetModel()
endResetModel(); endResetModel();
} }
bool EffectComposerNodesModel::nodeExists(const QString &name)
{
return m_customNodeNames.contains(name, Qt::CaseInsensitive)
|| m_builtInNodeNames.contains(name, Qt::CaseInsensitive);
}
bool EffectComposerNodesModel::isBuiltIn(const QString &name)
{
return m_builtInNodeNames.contains(name, Qt::CaseInsensitive);
}
void EffectComposerNodesModel::updateCanBeAdded( void EffectComposerNodesModel::updateCanBeAdded(
const QStringList &uniforms, [[maybe_unused]] const QStringList &nodeNames) const QStringList &uniforms, [[maybe_unused]] const QStringList &nodeNames)
{ {
@@ -126,4 +180,22 @@ QHash<QString, QString> EffectComposerNodesModel::defaultImagesForNode(const QSt
return m_defaultImagesHash.value(name); return m_defaultImagesHash.value(name);
} }
void EffectComposerNodesModel::removeEffectNode(const QString &name)
{
if (!m_customCategory || name.isEmpty())
return;
m_defaultImagesHash.remove(name);
m_customNodeNames.removeOne(name);
m_customCategory->removeNode(name);
QString fileNameBase = EffectUtils::nodeNameToFileName(name);
Utils::FilePath nodePath = Utils::FilePath::fromString(EffectUtils::nodeLibraryPath())
.pathAppended(fileNameBase);
if (nodePath.exists())
nodePath.removeRecursively();
resetModel();
}
} // namespace EffectComposer } // namespace EffectComposer

View File

@@ -9,6 +9,8 @@
namespace EffectComposer { namespace EffectComposer {
class EffectNode;
class EffectComposerNodesModel : public QAbstractListModel class EffectComposerNodesModel : public QAbstractListModel
{ {
Q_OBJECT Q_OBJECT
@@ -26,14 +28,20 @@ public:
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
void loadModel(); void loadModel();
void loadCustomNodes();
void resetModel(); void resetModel();
bool nodeExists(const QString &name);
bool isBuiltIn(const QString &name);
QList<EffectNodesCategory *> categories() const { return m_categories; } QList<EffectNodesCategory *> categories() const { return m_categories; }
void updateCanBeAdded(const QStringList &uniforms, const QStringList &nodeNames); void updateCanBeAdded(const QStringList &uniforms, const QStringList &nodeNames);
QHash<QString, QString> defaultImagesForNode(const QString &name) const; QHash<QString, QString> defaultImagesForNode(const QString &name) const;
void removeEffectNode(const QString &name);
private: private:
QString nodesSourcesPath() const; QString nodesSourcesPath() const;
@@ -41,6 +49,10 @@ private:
bool m_probeNodesDir = false; bool m_probeNodesDir = false;
bool m_modelLoaded = false; bool m_modelLoaded = false;
QHash<QString, QHash<QString, QString>> m_defaultImagesHash; QHash<QString, QHash<QString, QString>> m_defaultImagesHash;
QStringList m_builtInNodeNames;
QStringList m_customNodeNames;
EffectNode *m_builtinCustomNode = nullptr;
EffectNodesCategory *m_customCategory = nullptr;
}; };
} // namespace EffectComposer } // namespace EffectComposer

View File

@@ -75,7 +75,6 @@ static QList<QmlDesigner::ModelNode> modelNodesFromMimeData(const QByteArray &mi
EffectComposerWidget::EffectComposerWidget(EffectComposerView *view) EffectComposerWidget::EffectComposerWidget(EffectComposerView *view)
: m_effectComposerModel{new EffectComposerModel(this)} : m_effectComposerModel{new EffectComposerModel(this)}
, m_effectComposerNodesModel{new EffectComposerNodesModel(this)}
, m_effectComposerView(view) , m_effectComposerView(view)
, m_quickWidget{new StudioQuickWidget(this)} , m_quickWidget{new StudioQuickWidget(this)}
{ {
@@ -108,7 +107,7 @@ EffectComposerWidget::EffectComposerWidget(EffectComposerView *view)
g_propertyData.insert(QString("blur_fs_path"), QString(blurPath + "bluritems.frag.qsb")); g_propertyData.insert(QString("blur_fs_path"), QString(blurPath + "bluritems.frag.qsb"));
auto map = m_quickWidget->registerPropertyMap("EffectComposerBackend"); auto map = m_quickWidget->registerPropertyMap("EffectComposerBackend");
map->setProperties({{"effectComposerNodesModel", QVariant::fromValue(m_effectComposerNodesModel.data())}, map->setProperties({{"effectComposerNodesModel", QVariant::fromValue(effectComposerNodesModel().data())},
{"effectComposerModel", QVariant::fromValue(m_effectComposerModel.data())}, {"effectComposerModel", QVariant::fromValue(m_effectComposerModel.data())},
{"rootView", QVariant::fromValue(this)}}); {"rootView", QVariant::fromValue(this)}});
@@ -174,7 +173,7 @@ QPointer<EffectComposerModel> EffectComposerWidget::effectComposerModel() const
QPointer<EffectComposerNodesModel> EffectComposerWidget::effectComposerNodesModel() const QPointer<EffectComposerNodesModel> EffectComposerWidget::effectComposerNodesModel() const
{ {
return m_effectComposerNodesModel; return m_effectComposerModel->effectComposerNodesModel();
} }
void EffectComposerWidget::addEffectNode(const QString &nodeQenPath) void EffectComposerWidget::addEffectNode(const QString &nodeQenPath)
@@ -188,6 +187,11 @@ void EffectComposerWidget::addEffectNode(const QString &nodeQenPath)
} }
} }
void EffectComposerWidget::removeEffectNodeFromLibrary(const QString &nodeName)
{
effectComposerNodesModel()->removeEffectNode(nodeName);
}
void EffectComposerWidget::focusSection(int section) void EffectComposerWidget::focusSection(int section)
{ {
Q_UNUSED(section) Q_UNUSED(section)
@@ -209,7 +213,7 @@ QPoint EffectComposerWidget::globalPos(const QPoint &point) const
QString EffectComposerWidget::uniformDefaultImage(const QString &nodeName, const QString &uniformName) const QString EffectComposerWidget::uniformDefaultImage(const QString &nodeName, const QString &uniformName) const
{ {
return m_effectComposerNodesModel->defaultImagesForNode(nodeName).value(uniformName); return effectComposerNodesModel()->defaultImagesForNode(nodeName).value(uniformName);
} }
QString EffectComposerWidget::imagesPath() const QString EffectComposerWidget::imagesPath() const
@@ -247,7 +251,7 @@ void EffectComposerWidget::dropNode(const QByteArray &mimeData)
void EffectComposerWidget::updateCanBeAdded() void EffectComposerWidget::updateCanBeAdded()
{ {
m_effectComposerNodesModel->updateCanBeAdded(m_effectComposerModel->uniformNames(), effectComposerNodesModel()->updateCanBeAdded(m_effectComposerModel->uniformNames(),
m_effectComposerModel->nodeNames()); m_effectComposerModel->nodeNames());
} }

View File

@@ -47,6 +47,7 @@ public:
QPointer<EffectComposerNodesModel> effectComposerNodesModel() const; QPointer<EffectComposerNodesModel> effectComposerNodesModel() const;
Q_INVOKABLE void addEffectNode(const QString &nodeQenPath); Q_INVOKABLE void addEffectNode(const QString &nodeQenPath);
Q_INVOKABLE void removeEffectNodeFromLibrary(const QString &nodeName);
Q_INVOKABLE void focusSection(int section); Q_INVOKABLE void focusSection(int section);
Q_INVOKABLE void doOpenComposition(); Q_INVOKABLE void doOpenComposition();
Q_INVOKABLE QRect screenRect() const; Q_INVOKABLE QRect screenRect() const;
@@ -68,7 +69,6 @@ private:
void handleImportScanTimer(); void handleImportScanTimer();
QPointer<EffectComposerModel> m_effectComposerModel; QPointer<EffectComposerModel> m_effectComposerModel;
QPointer<EffectComposerNodesModel> m_effectComposerNodesModel;
QPointer<EffectComposerView> m_effectComposerView; QPointer<EffectComposerView> m_effectComposerView;
QPointer<StudioQuickWidget> m_quickWidget; QPointer<StudioQuickWidget> m_quickWidget;
QmlDesigner::QmlModelNodeProxy m_backendModelNode; QmlDesigner::QmlModelNodeProxy m_backendModelNode;

View File

@@ -2,7 +2,9 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
#include "effectnode.h" #include "effectnode.h"
#include "compositionnode.h" #include "compositionnode.h"
#include "effectutils.h"
#include "uniform.h" #include "uniform.h"
#include <QDir> #include <QDir>
@@ -10,26 +12,26 @@
namespace EffectComposer { namespace EffectComposer {
EffectNode::EffectNode(const QString &qenPath) EffectNode::EffectNode(const QString &qenPath, bool isBuiltIn)
: m_qenPath(qenPath) : m_qenPath(qenPath)
{ {
const QFileInfo fileInfo = QFileInfo(qenPath); const QFileInfo fileInfo = QFileInfo(qenPath);
QString iconPath = QStringLiteral("%1/icon/%2.svg").arg(fileInfo.absolutePath(),
fileInfo.baseName());
if (!QFileInfo::exists(iconPath)) {
QDir parentDir = QDir(fileInfo.absolutePath());
parentDir.cdUp();
iconPath = QStringLiteral("%1/%2").arg(parentDir.path(), "placeholder.svg");
}
m_iconPath = QUrl::fromLocalFile(iconPath);
CompositionNode node({}, qenPath); CompositionNode node({}, qenPath);
m_name = node.name(); m_name = node.name();
m_description = node.description(); m_description = node.description();
m_isCustom = node.isCustom(); m_isCustom = node.isCustom();
m_canBeRemoved = !isBuiltIn;
QString iconPath = QStringLiteral("%1/icon/%2.svg").arg(fileInfo.absolutePath(),
fileInfo.baseName());
if (!QFileInfo::exists(iconPath)) {
QString iconFileName = m_isCustom ? QString{"user_custom_effect.svg"}
: QString{"placeholder.svg"};
iconPath = QStringLiteral("%1/%2").arg(EffectUtils::nodesSourcesPath(), iconFileName);
}
m_iconPath = QUrl::fromLocalFile(iconPath);
const QList<Uniform *> uniforms = node.uniforms(); const QList<Uniform *> uniforms = node.uniforms();
for (const Uniform *uniform : uniforms) { for (const Uniform *uniform : uniforms) {

View File

@@ -18,9 +18,10 @@ class EffectNode : public QObject
Q_PROPERTY(QUrl nodeIcon MEMBER m_iconPath CONSTANT) Q_PROPERTY(QUrl nodeIcon MEMBER m_iconPath CONSTANT)
Q_PROPERTY(QString nodeQenPath MEMBER m_qenPath CONSTANT) Q_PROPERTY(QString nodeQenPath MEMBER m_qenPath CONSTANT)
Q_PROPERTY(bool canBeAdded MEMBER m_canBeAdded NOTIFY canBeAddedChanged) Q_PROPERTY(bool canBeAdded MEMBER m_canBeAdded NOTIFY canBeAddedChanged)
Q_PROPERTY(bool canBeRemoved MEMBER m_canBeRemoved CONSTANT)
public: public:
EffectNode(const QString &qenPath); EffectNode(const QString &qenPath, bool isBuiltIn);
QString name() const; QString name() const;
QString description() const; QString description() const;
@@ -42,6 +43,7 @@ private:
QUrl m_iconPath; QUrl m_iconPath;
bool m_isCustom = false; bool m_isCustom = false;
bool m_canBeAdded = true; bool m_canBeAdded = true;
bool m_canBeRemoved = false;
QSet<QString> m_uniformNames; QSet<QString> m_uniformNames;
QHash<QString, QString> m_defaultImagesHash; QHash<QString, QString> m_defaultImagesHash;
}; };

View File

@@ -3,6 +3,8 @@
#include "effectnodescategory.h" #include "effectnodescategory.h"
#include <utils/algorithm.h>
namespace EffectComposer { namespace EffectComposer {
EffectNodesCategory::EffectNodesCategory(const QString &name, const QList<EffectNode *> &nodes) EffectNodesCategory::EffectNodesCategory(const QString &name, const QList<EffectNode *> &nodes)
@@ -19,5 +21,19 @@ QList<EffectNode *> EffectNodesCategory::nodes() const
return m_categoryNodes; return m_categoryNodes;
} }
void EffectNodesCategory::setNodes(const QList<EffectNode *> &nodes)
{
m_categoryNodes = nodes;
emit nodesChanged();
}
void EffectNodesCategory::removeNode(const QString &nodeName)
{
Utils::eraseOne(m_categoryNodes, [nodeName](const EffectNode *node) {
return node->name() == nodeName;
});
}
} // namespace EffectComposer } // namespace EffectComposer

View File

@@ -14,13 +14,18 @@ class EffectNodesCategory : public QObject
Q_OBJECT Q_OBJECT
Q_PROPERTY(QString categoryName MEMBER m_name CONSTANT) Q_PROPERTY(QString categoryName MEMBER m_name CONSTANT)
Q_PROPERTY(QList<EffectNode *> categoryNodes MEMBER m_categoryNodes CONSTANT) Q_PROPERTY(QList<EffectNode *> categoryNodes READ nodes NOTIFY nodesChanged)
public: public:
EffectNodesCategory(const QString &name, const QList<EffectNode *> &nodes); EffectNodesCategory(const QString &name, const QList<EffectNode *> &nodes);
QString name() const; QString name() const;
QList<EffectNode *> nodes() const; QList<EffectNode *> nodes() const;
void setNodes(const QList<EffectNode *> &nodes);
void removeNode(const QString &nodeName);
signals:
void nodesChanged();
private: private:
QString m_name; QString m_name;

View File

@@ -6,6 +6,8 @@
#include <coreplugin/icore.h> #include <coreplugin/icore.h>
#include <QJsonArray> #include <QJsonArray>
#include <QRegularExpression>
#include <QStandardPaths>
namespace EffectComposer { namespace EffectComposer {
@@ -31,4 +33,20 @@ QString EffectUtils::nodesSourcesPath()
return Core::ICore::resourcePath("qmldesigner/effectComposerNodes").toUrlishString(); return Core::ICore::resourcePath("qmldesigner/effectComposerNodes").toUrlishString();
} }
QString EffectUtils::nodeLibraryPath()
{
QStandardPaths::StandardLocation location = QStandardPaths::DocumentsLocation;
return QStandardPaths::writableLocation(location)
+ "/QtDesignStudio/effect_composer/node_library";
}
QString EffectUtils::nodeNameToFileName(const QString &nodeName)
{
static const QRegularExpression re("[^a-zA-Z0-9]");
QString newName = nodeName;
newName.replace(re, "_");
return newName;
}
} // namespace EffectComposer } // namespace EffectComposer

View File

@@ -15,8 +15,9 @@ public:
EffectUtils() = delete; EffectUtils() = delete;
static QString codeFromJsonArray(const QJsonArray &codeArray); static QString codeFromJsonArray(const QJsonArray &codeArray);
static QString nodesSourcesPath(); static QString nodesSourcesPath();
static QString nodeLibraryPath();
static QString nodeNameToFileName(const QString &nodeName);
}; };
} // namespace EffectComposer } // namespace EffectComposer