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
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: {

View File

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

View File

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

View File

@@ -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,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) {
if (event.key === Qt.Key_Escape && root.popup.opened)
if (event.key === Qt.Key_Escape && root.popup.opened) {
confirmRemoveForm.visible = false
root.popup.close()
}
}
}
}
}

View File

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

View File

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

View File

@@ -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 <QFileDialog>
#include <QLibraryInfo>
#include <QStandardPaths>
#include <QTemporaryDir>
#include <QVector2D>
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<int, QByteArray> EffectComposerModel::roleNames() const
{
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
{
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<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
{
static const int intMin = std::numeric_limits<int>::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

View File

@@ -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<int, QByteArray> 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<CompositionNode *> m_nodes;
QPointer<EffectComposerNodesModel> m_effectComposerNodesModel;
int m_selectedIndex = -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
#include "effectcomposernodesmodel.h"
#include "effectutils.h"
#include <utils/algorithm.h>
@@ -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<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();
}
@@ -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<QString, QString> 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

View File

@@ -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<EffectNodesCategory *> categories() const { return m_categories; }
void updateCanBeAdded(const QStringList &uniforms, const QStringList &nodeNames);
QHash<QString, QString> 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<QString, QHash<QString, QString>> m_defaultImagesHash;
QStringList m_builtInNodeNames;
QStringList m_customNodeNames;
EffectNode *m_builtinCustomNode = nullptr;
EffectNodesCategory *m_customCategory = nullptr;
};
} // namespace EffectComposer

View File

@@ -75,7 +75,6 @@ static QList<QmlDesigner::ModelNode> 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<EffectComposerModel> EffectComposerWidget::effectComposerModel() const
QPointer<EffectComposerNodesModel> 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());
}

View File

@@ -47,6 +47,7 @@ public:
QPointer<EffectComposerNodesModel> 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<EffectComposerModel> m_effectComposerModel;
QPointer<EffectComposerNodesModel> m_effectComposerNodesModel;
QPointer<EffectComposerView> m_effectComposerView;
QPointer<StudioQuickWidget> m_quickWidget;
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
#include "effectnode.h"
#include "compositionnode.h"
#include "effectutils.h"
#include "uniform.h"
#include <QDir>
@@ -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<Uniform *> uniforms = node.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(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<QString> m_uniformNames;
QHash<QString, QString> m_defaultImagesHash;
};

View File

@@ -3,6 +3,8 @@
#include "effectnodescategory.h"
#include <utils/algorithm.h>
namespace EffectComposer {
EffectNodesCategory::EffectNodesCategory(const QString &name, const QList<EffectNode *> &nodes)
@@ -19,5 +21,19 @@ QList<EffectNode *> EffectNodesCategory::nodes() const
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

View File

@@ -14,13 +14,18 @@ class EffectNodesCategory : public QObject
Q_OBJECT
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:
EffectNodesCategory(const QString &name, const QList<EffectNode *> &nodes);
QString name() const;
QList<EffectNode *> nodes() const;
void setNodes(const QList<EffectNode *> &nodes);
void removeNode(const QString &nodeName);
signals:
void nodesChanged();
private:
QString m_name;

View File

@@ -6,6 +6,8 @@
#include <coreplugin/icore.h>
#include <QJsonArray>
#include <QRegularExpression>
#include <QStandardPaths>
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

View File

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