From 6e4fbae15c027fa75089408a8ae450aac04bf119 Mon Sep 17 00:00:00 2001 From: Miikka Heikkinen Date: Fri, 21 Mar 2025 16:23:49 +0200 Subject: [PATCH] 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 Reviewed-by: Ali Kianian --- ...PropertyRemoveForm.qml => ConfirmForm.qml} | 8 +- .../EffectCompositionNode.qml | 172 +++++++++++++++--- .../effectComposerQmlSources/EffectNode.qml | 26 ++- .../EffectNodesComboBox.qml | 40 +++- .../effectcomposer/compositionnode.cpp | 8 + src/plugins/effectcomposer/compositionnode.h | 4 +- .../effectcomposer/effectcomposermodel.cpp | 111 ++++++++++- .../effectcomposer/effectcomposermodel.h | 8 + .../effectcomposernodesmodel.cpp | 80 +++++++- .../effectcomposer/effectcomposernodesmodel.h | 12 ++ .../effectcomposer/effectcomposerwidget.cpp | 14 +- .../effectcomposer/effectcomposerwidget.h | 2 +- src/plugins/effectcomposer/effectnode.cpp | 24 +-- src/plugins/effectcomposer/effectnode.h | 4 +- .../effectcomposer/effectnodescategory.cpp | 16 ++ .../effectcomposer/effectnodescategory.h | 7 +- src/plugins/effectcomposer/effectutils.cpp | 18 ++ src/plugins/effectcomposer/effectutils.h | 3 +- 18 files changed, 490 insertions(+), 67 deletions(-) rename share/qtcreator/qmldesigner/effectComposerQmlSources/{ConfirmPropertyRemoveForm.qml => ConfirmForm.qml} (89%) diff --git a/share/qtcreator/qmldesigner/effectComposerQmlSources/ConfirmPropertyRemoveForm.qml b/share/qtcreator/qmldesigner/effectComposerQmlSources/ConfirmForm.qml similarity index 89% rename from share/qtcreator/qmldesigner/effectComposerQmlSources/ConfirmPropertyRemoveForm.qml rename to share/qtcreator/qmldesigner/effectComposerQmlSources/ConfirmForm.qml index c673d4fdebd..011c6028eaf 100644 --- a/share/qtcreator/qmldesigner/effectComposerQmlSources/ConfirmPropertyRemoveForm.qml +++ b/share/qtcreator/qmldesigner/effectComposerQmlSources/ConfirmForm.qml @@ -14,6 +14,10 @@ Rectangle { border.color: StudioTheme.Values.themeControlOutline color: StudioTheme.Values.themeSectionHeadBackground + property alias text: textLabel.text + property alias acceptButtonLabel: acceptButton.text + property alias cancelButtonLabel: cancelButton.text + signal accepted() signal canceled() @@ -29,13 +33,13 @@ Rectangle { width: parent.width height: 50 Text { + id: textLabel anchors.centerIn: parent color: StudioTheme.Values.themeTextColor font.bold: true font.pixelSize: StudioTheme.Values.baseFontSize horizontalAlignment: Text.AlignLeft 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 width: 80 height: 30 - text: qsTr("Remove") + text: qsTr("Accept") padding: 4 onClicked: { diff --git a/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectCompositionNode.qml b/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectCompositionNode.qml index 339381d86c2..243cd1ea317 100644 --- a/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectCompositionNode.qml +++ b/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectCompositionNode.qml @@ -60,56 +60,118 @@ HelperWidgets.Section { leftPadding: StudioTheme.Values.toolbarSpacing } + Item { + id: nodeConfirmFormParent + width: parent.width + height: childrenRect.height + } + TextEdit { - id: warningText + id: infoText visible: false height: 60 width: root.width - text: qsTr("A node with this name already exists.\nSuffix was added to make the name unique.") horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter - color: StudioTheme.Values.themeWarning 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: { - if (warningText.visible) { - warningTimer.running = true + if (infoText.visible) root.expanded = true // so that warning is visible - } } Timer { - id: warningTimer + id: infoTimer interval: 12000 repeat: false running: false onTriggered: { - warningText.visible = false - warningTimer.running = false + infoText.visible = false + infoTimer.running = false } } } - ConfirmPropertyRemoveForm { - id: confirmRemoveForm + ConfirmForm { + id: confirmForm - property int uniformIndex: -1 + property int confirmIndex: -1 + property bool confirmingRemove: false width: root.width - StudioTheme.Values.scrollBarThicknessHover - 8 visible: false - onHeightChanged: root.emitEnsure(confirmRemoveForm) - onVisibleChanged: root.emitEnsure(confirmRemoveForm) + onHeightChanged: root.emitEnsure(confirmForm) + onVisibleChanged: root.emitEnsure(confirmForm) - onAccepted: { - confirmRemoveForm.parent = root - nodeUniformsModel.remove(confirmRemoveForm.uniformIndex) + function showConfirmRemove(formParent, 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 { @@ -149,7 +211,7 @@ HelperWidgets.Section { onEditingFinished: { nameEditField.visible = false - warningText.visible = !root.backendModel.changeNodeName(modelIndex, nameEditText.text) + infoText.showNodeExistsWarning(!root.backendModel.changeNodeName(modelIndex, nameEditText.text)) } onActiveFocusChanged: { @@ -180,14 +242,19 @@ HelperWidgets.Section { iconColor: StudioTheme.Values.themeTextColor iconScale: nameEditButton.containsMouse ? 1.2 : 1 implicitWidth: width - tooltip: qsTr("Edit effect node name") + tooltip: qsTr("Edit effect name") - onPressed: (event) => { + function handlePress() + { nameEditText.text = nodeName nameEditText.initializing = true nameEditField.visible = true nameEditText.forceActiveFocus() } + + onPressed: (event) => { + nameEditButton.handlePress() + } } } } @@ -223,17 +290,16 @@ HelperWidgets.Section { onReset: nodeUniformsModel.resetData(index) onRemove: { + confirmForm.clear() if (uniformIsInUse) { - confirmRemoveForm.parent = effectCompositionNodeUniform.editPropertyFormParent - confirmRemoveForm.uniformIndex = index - confirmRemoveForm.visible = true + confirmForm.showConfirmRemove( + effectCompositionNodeUniform.editPropertyFormParent, index) } else { nodeUniformsModel.remove(index) } } onEdit: { - confirmRemoveForm.visible = false - confirmRemoveForm.parent = root + confirmForm.clear() addPropertyForm.parent = effectCompositionNodeUniform.editPropertyFormParent let dispNames = nodeUniformsModel.displayNames() let filteredDispNames = dispNames.filter(name => name !== uniformDisplayName); @@ -294,9 +360,14 @@ HelperWidgets.Section { enabled: !addPropertyForm.visible anchors.verticalCenter: parent.verticalCenter + HelperWidgets.ToolTipArea { + anchors.fill: parent + tooltip: qsTr("Add new property to the effect.") + acceptedButtons: Qt.NoButton + } + onClicked: { - confirmRemoveForm.visible = false - confirmRemoveForm.parent = root + confirmForm.clear() root.editedUniformIndex = -1 addPropertyForm.parent = addProperty addPropertyForm.reservedDispNames = nodeUniformsModel.displayNames() @@ -310,7 +381,50 @@ HelperWidgets.Section { height: 30 text: qsTr("Show Code") 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() } } } diff --git a/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectNode.qml b/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectNode.qml index 603a0a1832d..c2be310ed6b 100644 --- a/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectNode.qml +++ b/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectNode.qml @@ -15,16 +15,18 @@ Rectangle { width: 140 height: 32 - color: mouseArea.containsMouse && modelData.canBeAdded + color: mouseArea.containsMouse && modelData.canBeAdded && root.enabled ? StudioTheme.Values.themeControlBackgroundInteraction : "transparent" signal addEffectNode(var nodeQenPath) + signal removeEffectNodeFromLibrary(var nodeName) ToolTipArea { id: mouseArea anchors.fill: parent acceptedButtons: Qt.LeftButton + visible: root.enabled tooltip: modelData.canBeAdded ? modelData.nodeDescription : qsTr("An effect with same properties already exists, this effect cannot be added.") @@ -59,6 +61,28 @@ Rectangle { anchors.verticalCenter: nodeIcon.verticalCenter wrapMode: Text.WordWrap 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) + } + } } } } diff --git a/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectNodesComboBox.qml b/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectNodesComboBox.qml index 72e97699b2d..487f23b0aed 100644 --- a/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectNodesComboBox.qml +++ b/share/qtcreator/qmldesigner/effectComposerQmlSources/EffectNodesComboBox.qml @@ -78,8 +78,10 @@ StudioControls.ComboBox { flags: Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint onActiveFocusItemChanged: { - if (!window.activeFocusItem && !root.hovered && root.popup.opened) + if (!window.activeFocusItem && !root.hovered && root.popup.opened) { root.popup.close() + confirmRemoveForm.visible = false + } } Rectangle { @@ -92,6 +94,7 @@ StudioControls.ComboBox { HelperWidgets.ScrollView { anchors.fill: parent anchors.margins: 1 + enabled: !confirmRemoveForm.visible Row { id: row @@ -118,10 +121,15 @@ StudioControls.ComboBox { EffectNode { onAddEffectNode: (nodeQenPath) => { - EffectComposerBackend.rootView.addEffectNode(modelData.nodeQenPath) + EffectComposerBackend.rootView.addEffectNode(nodeQenPath) + confirmRemoveForm.visible = false root.popup.close() root.nodeJustAdded = true } + onRemoveEffectNodeFromLibrary: (nodeName) => { + confirmRemoveForm.visible = true + confirmRemoveForm.nodeName = nodeName + } } } } @@ -129,9 +137,35 @@ 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) { - if (event.key === Qt.Key_Escape && root.popup.opened) + if (event.key === Qt.Key_Escape && root.popup.opened) { + confirmRemoveForm.visible = false root.popup.close() + } } } } diff --git a/src/plugins/effectcomposer/compositionnode.cpp b/src/plugins/effectcomposer/compositionnode.cpp index fac263d847a..1792952c3c9 100644 --- a/src/plugins/effectcomposer/compositionnode.cpp +++ b/src/plugins/effectcomposer/compositionnode.cpp @@ -160,6 +160,14 @@ bool CompositionNode::isCustom() const return m_isCustom; } +void CompositionNode::setCustom(bool enable) +{ + if (enable != m_isCustom) { + m_isCustom = enable; + emit isCustomChanged(); + } +} + CompositionNode::NodeType CompositionNode::type() const { return m_type; diff --git a/src/plugins/effectcomposer/compositionnode.h b/src/plugins/effectcomposer/compositionnode.h index 6c84d429896..b3e81f07b49 100644 --- a/src/plugins/effectcomposer/compositionnode.h +++ b/src/plugins/effectcomposer/compositionnode.h @@ -24,7 +24,7 @@ class CompositionNode : public QObject Q_PROPERTY(QString nodeName READ name WRITE setName NOTIFY nameChanged) Q_PROPERTY(bool nodeEnabled READ isEnabled WRITE setIsEnabled NOTIFY isEnabledChanged) 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( QString fragmentCode @@ -59,6 +59,7 @@ public: bool isDependency() const; bool isCustom() const; + void setCustom(bool enable); QString name() const; void setName(const QString &name); @@ -89,6 +90,7 @@ signals: void fragmentCodeChanged(); void vertexCodeChanged(); void nameChanged(); + void isCustomChanged(); private slots: void onUniformRenamed(const QString &oldName, const QString &newName); diff --git a/src/plugins/effectcomposer/effectcomposermodel.cpp b/src/plugins/effectcomposer/effectcomposermodel.cpp index e38bb9165c4..b4dd306ad4d 100644 --- a/src/plugins/effectcomposer/effectcomposermodel.cpp +++ b/src/plugins/effectcomposer/effectcomposermodel.cpp @@ -5,6 +5,7 @@ #include "compositionnode.h" #include "effectcomposertr.h" +#include "effectcomposernodesmodel.h" #include "effectshaderscodeeditor.h" #include "effectutils.h" #include "propertyhandler.h" @@ -31,7 +32,6 @@ #include #include #include -#include #include using namespace Qt::StringLiterals; @@ -43,6 +43,7 @@ static constexpr int MAIN_CODE_EDITOR_INDEX = -2; EffectComposerModel::EffectComposerModel(QObject *parent) : QAbstractListModel{parent} + , m_effectComposerNodesModel{new EffectComposerNodesModel(this)} , m_codeEditorIndex(INVALID_CODE_EDITOR_INDEX) , m_shaderDir(QDir::tempPath() + "/qds_ec_XXXXXX") , m_currentPreviewColor("#dddddd") @@ -53,6 +54,11 @@ EffectComposerModel::EffectComposerModel(QObject *parent) connectCodeEditor(); } +EffectComposerNodesModel *EffectComposerModel::effectComposerNodesModel() const +{ + return m_effectComposerNodesModel.data(); +} + QHash EffectComposerModel::roleNames() const { static const QHash roles = { @@ -74,7 +80,7 @@ int EffectComposerModel::rowCount(const QModelIndex &parent) 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 {}); 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 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(); const QString oldName = m_nodes[nodeIndex]->name(); @@ -237,8 +243,9 @@ bool EffectComposerModel::changeNodeName(int nodeIndex, const QString &name) const QStringList reservedNames = nodeNames(); // 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 { - return oldName != checkName && reservedNames.contains(checkName, Qt::CaseInsensitive); + QString newName = QmlDesigner::UniqueName::generate(trimmedName, [&] (const QString &checkName) -> bool { + return oldName != checkName && (reservedNames.contains(checkName, Qt::CaseInsensitive) + || m_effectComposerNodesModel->nodeExists(checkName)); }); if (newName != oldName) { @@ -1326,6 +1333,91 @@ void EffectComposerModel::openMainCodeEditor() 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 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 result = qenFile.writeFileContents(jsonDoc.toJson()); + if (!result) + return errorTag + Tr::tr("Failed to write QEN file for effect:\n%1").arg(qenFile.fileName()); + + QList sources; + QStringList dests; + const QList 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 { static const int intMin = std::numeric_limits::lowest(); @@ -1619,7 +1711,7 @@ void EffectComposerModel::saveResources(const QString &name) newFileNames.insert(target.fileName()); if (target.exists() && source.fileName() != target.fileName()) 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())); 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) { - QTC_ASSERT(m_nodes.size() > idx && idx >= 0, return); + QTC_ASSERT(isValidIndex(idx), return); // Convert values to Uniform digestible strings auto fixedData = data; @@ -2755,4 +2847,9 @@ bool EffectComposerModel::writeToFile( return true; } +bool EffectComposerModel::isValidIndex(int idx) const +{ + return m_nodes.size() > idx && idx >= 0; +} + } // namespace EffectComposer diff --git a/src/plugins/effectcomposer/effectcomposermodel.h b/src/plugins/effectcomposer/effectcomposermodel.h index 534b58117d4..ccf211b6f16 100644 --- a/src/plugins/effectcomposer/effectcomposermodel.h +++ b/src/plugins/effectcomposer/effectcomposermodel.h @@ -29,6 +29,7 @@ class Process; namespace EffectComposer { class CompositionNode; +class EffectComposerNodesModel; class EffectShadersCodeEditor; struct ShaderEditorData; class Uniform; @@ -68,6 +69,8 @@ class EffectComposerModel : public QAbstractListModel public: EffectComposerModel(QObject *parent = nullptr); + EffectComposerNodesModel *effectComposerNodesModel() const; + QHash roleNames() const override; int rowCount(const QModelIndex & parent = QModelIndex()) 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 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; void openComposition(const QString &path); @@ -265,6 +272,7 @@ private: bool writeToFile(const QByteArray &buf, const QString &filename, FileType fileType); QList m_nodes; + QPointer m_effectComposerNodesModel; int m_selectedIndex = -1; int m_codeEditorIndex = -1; diff --git a/src/plugins/effectcomposer/effectcomposernodesmodel.cpp b/src/plugins/effectcomposer/effectcomposernodesmodel.cpp index 8bac3063346..6b0eea8f615 100644 --- a/src/plugins/effectcomposer/effectcomposernodesmodel.cpp +++ b/src/plugins/effectcomposer/effectcomposernodesmodel.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "effectcomposernodesmodel.h" + #include "effectutils.h" #include @@ -12,6 +13,8 @@ namespace EffectComposer { +constexpr QStringView customCatName{u"Custom"}; + EffectComposerNodesModel::EffectComposerNodesModel(QObject *parent) : QAbstractListModel{parent} { @@ -53,7 +56,8 @@ void EffectComposerNodesModel::loadModel() return; } - m_categories = {}; + m_categories.clear(); + m_builtInNodeNames.clear(); QDirIterator itCategories(nodesPath.toUrlishString(), QDir::Dirs | QDir::NoDotAndDotDot); while (itCategories.hasNext()) { @@ -69,10 +73,11 @@ void EffectComposerNodesModel::loadModel() QDirIterator itEffects(categoryPath.toUrlishString(), {"*.qen"}, QDir::Files); while (itEffects.hasNext()) { itEffects.next(); - auto node = new EffectNode(itEffects.filePath()); + auto node = new EffectNode(itEffects.filePath(), true); if (!node->defaultImagesHash().isEmpty()) m_defaultImagesHash.insert(node->name(), node->defaultImagesHash()); effects.push_back(node); + m_builtInNodeNames.append(node->name()); } catName[0] = catName[0].toUpper(); // capitalize first letter @@ -80,11 +85,15 @@ void EffectComposerNodesModel::loadModel() EffectNodesCategory *category = new EffectNodesCategory(catName, effects); 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(), - [&customCatName](EffectNodesCategory *a, EffectNodesCategory *b) { + [](EffectNodesCategory *a, EffectNodesCategory *b) { if (a->name() == customCatName) return false; if (b->name() == customCatName) @@ -94,6 +103,40 @@ void EffectComposerNodesModel::loadModel() 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 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(); } @@ -103,6 +146,17 @@ void EffectComposerNodesModel::resetModel() 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( const QStringList &uniforms, [[maybe_unused]] const QStringList &nodeNames) { @@ -126,4 +180,22 @@ QHash EffectComposerNodesModel::defaultImagesForNode(const QSt 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 diff --git a/src/plugins/effectcomposer/effectcomposernodesmodel.h b/src/plugins/effectcomposer/effectcomposernodesmodel.h index 532542918cf..f1de958f386 100644 --- a/src/plugins/effectcomposer/effectcomposernodesmodel.h +++ b/src/plugins/effectcomposer/effectcomposernodesmodel.h @@ -9,6 +9,8 @@ namespace EffectComposer { +class EffectNode; + class EffectComposerNodesModel : public QAbstractListModel { Q_OBJECT @@ -26,14 +28,20 @@ public: QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; void loadModel(); + void loadCustomNodes(); void resetModel(); + bool nodeExists(const QString &name); + bool isBuiltIn(const QString &name); + QList categories() const { return m_categories; } void updateCanBeAdded(const QStringList &uniforms, const QStringList &nodeNames); QHash defaultImagesForNode(const QString &name) const; + void removeEffectNode(const QString &name); + private: QString nodesSourcesPath() const; @@ -41,6 +49,10 @@ private: bool m_probeNodesDir = false; bool m_modelLoaded = false; QHash> m_defaultImagesHash; + QStringList m_builtInNodeNames; + QStringList m_customNodeNames; + EffectNode *m_builtinCustomNode = nullptr; + EffectNodesCategory *m_customCategory = nullptr; }; } // namespace EffectComposer diff --git a/src/plugins/effectcomposer/effectcomposerwidget.cpp b/src/plugins/effectcomposer/effectcomposerwidget.cpp index f5bc1d6b3df..838b41ca7ae 100644 --- a/src/plugins/effectcomposer/effectcomposerwidget.cpp +++ b/src/plugins/effectcomposer/effectcomposerwidget.cpp @@ -75,7 +75,6 @@ static QList modelNodesFromMimeData(const QByteArray &mi EffectComposerWidget::EffectComposerWidget(EffectComposerView *view) : m_effectComposerModel{new EffectComposerModel(this)} - , m_effectComposerNodesModel{new EffectComposerNodesModel(this)} , m_effectComposerView(view) , 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")); 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())}, {"rootView", QVariant::fromValue(this)}}); @@ -174,7 +173,7 @@ QPointer EffectComposerWidget::effectComposerModel() const QPointer EffectComposerWidget::effectComposerNodesModel() const { - return m_effectComposerNodesModel; + return m_effectComposerModel->effectComposerNodesModel(); } 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) { Q_UNUSED(section) @@ -209,7 +213,7 @@ QPoint EffectComposerWidget::globalPos(const QPoint &point) 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 @@ -247,7 +251,7 @@ void EffectComposerWidget::dropNode(const QByteArray &mimeData) void EffectComposerWidget::updateCanBeAdded() { - m_effectComposerNodesModel->updateCanBeAdded(m_effectComposerModel->uniformNames(), + effectComposerNodesModel()->updateCanBeAdded(m_effectComposerModel->uniformNames(), m_effectComposerModel->nodeNames()); } diff --git a/src/plugins/effectcomposer/effectcomposerwidget.h b/src/plugins/effectcomposer/effectcomposerwidget.h index 36493b71f1a..69c5ee1a009 100644 --- a/src/plugins/effectcomposer/effectcomposerwidget.h +++ b/src/plugins/effectcomposer/effectcomposerwidget.h @@ -47,6 +47,7 @@ public: QPointer effectComposerNodesModel() const; Q_INVOKABLE void addEffectNode(const QString &nodeQenPath); + Q_INVOKABLE void removeEffectNodeFromLibrary(const QString &nodeName); Q_INVOKABLE void focusSection(int section); Q_INVOKABLE void doOpenComposition(); Q_INVOKABLE QRect screenRect() const; @@ -68,7 +69,6 @@ private: void handleImportScanTimer(); QPointer m_effectComposerModel; - QPointer m_effectComposerNodesModel; QPointer m_effectComposerView; QPointer m_quickWidget; QmlDesigner::QmlModelNodeProxy m_backendModelNode; diff --git a/src/plugins/effectcomposer/effectnode.cpp b/src/plugins/effectcomposer/effectnode.cpp index b6be6e90362..3f888ee8c18 100644 --- a/src/plugins/effectcomposer/effectnode.cpp +++ b/src/plugins/effectcomposer/effectnode.cpp @@ -2,7 +2,9 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 #include "effectnode.h" + #include "compositionnode.h" +#include "effectutils.h" #include "uniform.h" #include @@ -10,26 +12,26 @@ namespace EffectComposer { -EffectNode::EffectNode(const QString &qenPath) +EffectNode::EffectNode(const QString &qenPath, bool isBuiltIn) : m_qenPath(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); m_name = node.name(); m_description = node.description(); 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 uniforms = node.uniforms(); for (const Uniform *uniform : uniforms) { diff --git a/src/plugins/effectcomposer/effectnode.h b/src/plugins/effectcomposer/effectnode.h index 609b37e207f..649ef40ab45 100644 --- a/src/plugins/effectcomposer/effectnode.h +++ b/src/plugins/effectcomposer/effectnode.h @@ -18,9 +18,10 @@ class EffectNode : public QObject Q_PROPERTY(QUrl nodeIcon MEMBER m_iconPath CONSTANT) Q_PROPERTY(QString nodeQenPath MEMBER m_qenPath CONSTANT) Q_PROPERTY(bool canBeAdded MEMBER m_canBeAdded NOTIFY canBeAddedChanged) + Q_PROPERTY(bool canBeRemoved MEMBER m_canBeRemoved CONSTANT) public: - EffectNode(const QString &qenPath); + EffectNode(const QString &qenPath, bool isBuiltIn); QString name() const; QString description() const; @@ -42,6 +43,7 @@ private: QUrl m_iconPath; bool m_isCustom = false; bool m_canBeAdded = true; + bool m_canBeRemoved = false; QSet m_uniformNames; QHash m_defaultImagesHash; }; diff --git a/src/plugins/effectcomposer/effectnodescategory.cpp b/src/plugins/effectcomposer/effectnodescategory.cpp index 824eecaa2b6..4de1226a4b6 100644 --- a/src/plugins/effectcomposer/effectnodescategory.cpp +++ b/src/plugins/effectcomposer/effectnodescategory.cpp @@ -3,6 +3,8 @@ #include "effectnodescategory.h" +#include + namespace EffectComposer { EffectNodesCategory::EffectNodesCategory(const QString &name, const QList &nodes) @@ -19,5 +21,19 @@ QList EffectNodesCategory::nodes() const return m_categoryNodes; } +void EffectNodesCategory::setNodes(const QList &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 diff --git a/src/plugins/effectcomposer/effectnodescategory.h b/src/plugins/effectcomposer/effectnodescategory.h index 7f92dcd6fce..5906b8ef12c 100644 --- a/src/plugins/effectcomposer/effectnodescategory.h +++ b/src/plugins/effectcomposer/effectnodescategory.h @@ -14,13 +14,18 @@ class EffectNodesCategory : public QObject Q_OBJECT Q_PROPERTY(QString categoryName MEMBER m_name CONSTANT) - Q_PROPERTY(QList categoryNodes MEMBER m_categoryNodes CONSTANT) + Q_PROPERTY(QList categoryNodes READ nodes NOTIFY nodesChanged) public: EffectNodesCategory(const QString &name, const QList &nodes); QString name() const; QList nodes() const; + void setNodes(const QList &nodes); + void removeNode(const QString &nodeName); + +signals: + void nodesChanged(); private: QString m_name; diff --git a/src/plugins/effectcomposer/effectutils.cpp b/src/plugins/effectcomposer/effectutils.cpp index 45a1ea54614..1a9eaf47da2 100644 --- a/src/plugins/effectcomposer/effectutils.cpp +++ b/src/plugins/effectcomposer/effectutils.cpp @@ -6,6 +6,8 @@ #include #include +#include +#include namespace EffectComposer { @@ -31,4 +33,20 @@ QString EffectUtils::nodesSourcesPath() 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 diff --git a/src/plugins/effectcomposer/effectutils.h b/src/plugins/effectcomposer/effectutils.h index 5b9c99d4968..c420ee7f45e 100644 --- a/src/plugins/effectcomposer/effectutils.h +++ b/src/plugins/effectcomposer/effectutils.h @@ -15,8 +15,9 @@ public: EffectUtils() = delete; static QString codeFromJsonArray(const QJsonArray &codeArray); - static QString nodesSourcesPath(); + static QString nodeLibraryPath(); + static QString nodeNameToFileName(const QString &nodeName); }; } // namespace EffectComposer