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