forked from qt-creator/qt-creator
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:
@@ -14,6 +14,10 @@ Rectangle {
|
|||||||
border.color: StudioTheme.Values.themeControlOutline
|
border.color: StudioTheme.Values.themeControlOutline
|
||||||
color: StudioTheme.Values.themeSectionHeadBackground
|
color: StudioTheme.Values.themeSectionHeadBackground
|
||||||
|
|
||||||
|
property alias text: textLabel.text
|
||||||
|
property alias acceptButtonLabel: acceptButton.text
|
||||||
|
property alias cancelButtonLabel: cancelButton.text
|
||||||
|
|
||||||
signal accepted()
|
signal accepted()
|
||||||
signal canceled()
|
signal canceled()
|
||||||
|
|
||||||
@@ -29,13 +33,13 @@ Rectangle {
|
|||||||
width: parent.width
|
width: parent.width
|
||||||
height: 50
|
height: 50
|
||||||
Text {
|
Text {
|
||||||
|
id: textLabel
|
||||||
anchors.centerIn: parent
|
anchors.centerIn: parent
|
||||||
color: StudioTheme.Values.themeTextColor
|
color: StudioTheme.Values.themeTextColor
|
||||||
font.bold: true
|
font.bold: true
|
||||||
font.pixelSize: StudioTheme.Values.baseFontSize
|
font.pixelSize: StudioTheme.Values.baseFontSize
|
||||||
horizontalAlignment: Text.AlignLeft
|
horizontalAlignment: Text.AlignLeft
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
text: qsTr("The property is in use in the shader code.\nAre you sure you want to remove it?")
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -64,7 +68,7 @@ Rectangle {
|
|||||||
id: acceptButton
|
id: acceptButton
|
||||||
width: 80
|
width: 80
|
||||||
height: 30
|
height: 30
|
||||||
text: qsTr("Remove")
|
text: qsTr("Accept")
|
||||||
padding: 4
|
padding: 4
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
@@ -60,56 +60,118 @@ HelperWidgets.Section {
|
|||||||
leftPadding: StudioTheme.Values.toolbarSpacing
|
leftPadding: StudioTheme.Values.toolbarSpacing
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: nodeConfirmFormParent
|
||||||
|
width: parent.width
|
||||||
|
height: childrenRect.height
|
||||||
|
}
|
||||||
|
|
||||||
TextEdit {
|
TextEdit {
|
||||||
id: warningText
|
id: infoText
|
||||||
|
|
||||||
visible: false
|
visible: false
|
||||||
height: 60
|
height: 60
|
||||||
width: root.width
|
width: root.width
|
||||||
text: qsTr("A node with this name already exists.\nSuffix was added to make the name unique.")
|
|
||||||
horizontalAlignment: Text.AlignHCenter
|
horizontalAlignment: Text.AlignHCenter
|
||||||
verticalAlignment: Text.AlignVCenter
|
verticalAlignment: Text.AlignVCenter
|
||||||
color: StudioTheme.Values.themeWarning
|
|
||||||
readOnly: true
|
readOnly: true
|
||||||
|
wrapMode: TextEdit.WordWrap
|
||||||
|
|
||||||
|
function showNodeExistsWarning(enable)
|
||||||
|
{
|
||||||
|
infoText.text = qsTr("An effect with this name already exists.\nSuffix was added to make the name unique.")
|
||||||
|
infoTimer.restart()
|
||||||
|
infoText.visible = enable
|
||||||
|
infoText.color = StudioTheme.Values.themeWarning
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNodeAddedToLibraryInfo(message)
|
||||||
|
{
|
||||||
|
let errorTag = "@ERROR@"
|
||||||
|
if (message.startsWith(errorTag)) {
|
||||||
|
infoText.text = message.substring(errorTag.length)
|
||||||
|
infoText.color = StudioTheme.Values.themeError
|
||||||
|
} else {
|
||||||
|
infoText.text = message
|
||||||
|
infoText.color = StudioTheme.Values.themeInteraction
|
||||||
|
}
|
||||||
|
infoTimer.restart()
|
||||||
|
infoText.visible = message !== ""
|
||||||
|
}
|
||||||
|
|
||||||
|
function showNeedRenameInfo()
|
||||||
|
{
|
||||||
|
infoText.text = qsTr("A built-in effect with this name already exists in the library.\nPlease rename the effect before adding it to the library.")
|
||||||
|
infoText.visible = true
|
||||||
|
infoText.color = StudioTheme.Values.themeWarning
|
||||||
|
}
|
||||||
|
|
||||||
onVisibleChanged: {
|
onVisibleChanged: {
|
||||||
if (warningText.visible) {
|
if (infoText.visible)
|
||||||
warningTimer.running = true
|
|
||||||
root.expanded = true // so that warning is visible
|
root.expanded = true // so that warning is visible
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
Timer {
|
Timer {
|
||||||
id: warningTimer
|
id: infoTimer
|
||||||
|
|
||||||
interval: 12000
|
interval: 12000
|
||||||
repeat: false
|
repeat: false
|
||||||
running: false
|
running: false
|
||||||
|
|
||||||
onTriggered: {
|
onTriggered: {
|
||||||
warningText.visible = false
|
infoText.visible = false
|
||||||
warningTimer.running = false
|
infoTimer.running = false
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ConfirmPropertyRemoveForm {
|
ConfirmForm {
|
||||||
id: confirmRemoveForm
|
id: confirmForm
|
||||||
|
|
||||||
property int uniformIndex: -1
|
property int confirmIndex: -1
|
||||||
|
property bool confirmingRemove: false
|
||||||
|
|
||||||
width: root.width - StudioTheme.Values.scrollBarThicknessHover - 8
|
width: root.width - StudioTheme.Values.scrollBarThicknessHover - 8
|
||||||
visible: false
|
visible: false
|
||||||
|
|
||||||
onHeightChanged: root.emitEnsure(confirmRemoveForm)
|
onHeightChanged: root.emitEnsure(confirmForm)
|
||||||
onVisibleChanged: root.emitEnsure(confirmRemoveForm)
|
onVisibleChanged: root.emitEnsure(confirmForm)
|
||||||
|
|
||||||
onAccepted: {
|
function showConfirmRemove(formParent, uniformIndex)
|
||||||
confirmRemoveForm.parent = root
|
{
|
||||||
nodeUniformsModel.remove(confirmRemoveForm.uniformIndex)
|
confirmForm.parent = formParent
|
||||||
|
confirmForm.confirmIndex = uniformIndex
|
||||||
|
text = qsTr("The property is in use in the shader code.\nAre you sure you want to remove it?")
|
||||||
|
acceptButtonLabel = qsTr("Remove")
|
||||||
|
confirmForm.confirmingRemove = true
|
||||||
|
confirmForm.visible = true
|
||||||
}
|
}
|
||||||
|
|
||||||
onCanceled: confirmRemoveForm.parent = root
|
function showConfirmLibraryAdd(nodeIndex)
|
||||||
|
{
|
||||||
|
confirmForm.parent = nodeConfirmFormParent
|
||||||
|
confirmForm.confirmIndex = nodeIndex
|
||||||
|
text = qsTr("The effect is already added into the library.\nAre you sure you want to update it?")
|
||||||
|
acceptButtonLabel = qsTr("Update")
|
||||||
|
confirmForm.confirmingRemove = false
|
||||||
|
confirmForm.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function clear()
|
||||||
|
{
|
||||||
|
confirmForm.visible = false
|
||||||
|
confirmForm.parent = root
|
||||||
|
}
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
confirmForm.clear()
|
||||||
|
if (confirmForm.confirmingRemove)
|
||||||
|
nodeUniformsModel.remove(confirmForm.confirmIndex)
|
||||||
|
else
|
||||||
|
infoText.showNodeAddedToLibraryInfo(root.backendModel.addNodeToLibraryNode(confirmForm.confirmIndex))
|
||||||
|
}
|
||||||
|
|
||||||
|
onCanceled: confirmForm.clear()
|
||||||
}
|
}
|
||||||
|
|
||||||
MouseArea {
|
MouseArea {
|
||||||
@@ -149,7 +211,7 @@ HelperWidgets.Section {
|
|||||||
|
|
||||||
onEditingFinished: {
|
onEditingFinished: {
|
||||||
nameEditField.visible = false
|
nameEditField.visible = false
|
||||||
warningText.visible = !root.backendModel.changeNodeName(modelIndex, nameEditText.text)
|
infoText.showNodeExistsWarning(!root.backendModel.changeNodeName(modelIndex, nameEditText.text))
|
||||||
}
|
}
|
||||||
|
|
||||||
onActiveFocusChanged: {
|
onActiveFocusChanged: {
|
||||||
@@ -180,14 +242,19 @@ HelperWidgets.Section {
|
|||||||
iconColor: StudioTheme.Values.themeTextColor
|
iconColor: StudioTheme.Values.themeTextColor
|
||||||
iconScale: nameEditButton.containsMouse ? 1.2 : 1
|
iconScale: nameEditButton.containsMouse ? 1.2 : 1
|
||||||
implicitWidth: width
|
implicitWidth: width
|
||||||
tooltip: qsTr("Edit effect node name")
|
tooltip: qsTr("Edit effect name")
|
||||||
|
|
||||||
onPressed: (event) => {
|
function handlePress()
|
||||||
|
{
|
||||||
nameEditText.text = nodeName
|
nameEditText.text = nodeName
|
||||||
nameEditText.initializing = true
|
nameEditText.initializing = true
|
||||||
nameEditField.visible = true
|
nameEditField.visible = true
|
||||||
nameEditText.forceActiveFocus()
|
nameEditText.forceActiveFocus()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
onPressed: (event) => {
|
||||||
|
nameEditButton.handlePress()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -223,17 +290,16 @@ HelperWidgets.Section {
|
|||||||
|
|
||||||
onReset: nodeUniformsModel.resetData(index)
|
onReset: nodeUniformsModel.resetData(index)
|
||||||
onRemove: {
|
onRemove: {
|
||||||
|
confirmForm.clear()
|
||||||
if (uniformIsInUse) {
|
if (uniformIsInUse) {
|
||||||
confirmRemoveForm.parent = effectCompositionNodeUniform.editPropertyFormParent
|
confirmForm.showConfirmRemove(
|
||||||
confirmRemoveForm.uniformIndex = index
|
effectCompositionNodeUniform.editPropertyFormParent, index)
|
||||||
confirmRemoveForm.visible = true
|
|
||||||
} else {
|
} else {
|
||||||
nodeUniformsModel.remove(index)
|
nodeUniformsModel.remove(index)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
onEdit: {
|
onEdit: {
|
||||||
confirmRemoveForm.visible = false
|
confirmForm.clear()
|
||||||
confirmRemoveForm.parent = root
|
|
||||||
addPropertyForm.parent = effectCompositionNodeUniform.editPropertyFormParent
|
addPropertyForm.parent = effectCompositionNodeUniform.editPropertyFormParent
|
||||||
let dispNames = nodeUniformsModel.displayNames()
|
let dispNames = nodeUniformsModel.displayNames()
|
||||||
let filteredDispNames = dispNames.filter(name => name !== uniformDisplayName);
|
let filteredDispNames = dispNames.filter(name => name !== uniformDisplayName);
|
||||||
@@ -294,9 +360,14 @@ HelperWidgets.Section {
|
|||||||
enabled: !addPropertyForm.visible
|
enabled: !addPropertyForm.visible
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
HelperWidgets.ToolTipArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
tooltip: qsTr("Add new property to the effect.")
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
}
|
||||||
|
|
||||||
onClicked: {
|
onClicked: {
|
||||||
confirmRemoveForm.visible = false
|
confirmForm.clear()
|
||||||
confirmRemoveForm.parent = root
|
|
||||||
root.editedUniformIndex = -1
|
root.editedUniformIndex = -1
|
||||||
addPropertyForm.parent = addProperty
|
addPropertyForm.parent = addProperty
|
||||||
addPropertyForm.reservedDispNames = nodeUniformsModel.displayNames()
|
addPropertyForm.reservedDispNames = nodeUniformsModel.displayNames()
|
||||||
@@ -310,7 +381,50 @@ HelperWidgets.Section {
|
|||||||
height: 30
|
height: 30
|
||||||
text: qsTr("Show Code")
|
text: qsTr("Show Code")
|
||||||
anchors.verticalCenter: parent.verticalCenter
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
onClicked: root.backendModel.openCodeEditor(index)
|
|
||||||
|
HelperWidgets.ToolTipArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
tooltip: qsTr("Open the shader code editor.")
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: {
|
||||||
|
confirmForm.clear()
|
||||||
|
root.backendModel.openCodeEditor(index)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
HelperWidgets.Button {
|
||||||
|
id: addToLibraryButton
|
||||||
|
width: 100
|
||||||
|
height: 30
|
||||||
|
text: qsTr("Add to Library")
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
HelperWidgets.ToolTipArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
tooltip: qsTr("Add the effect to the effect library.\nYou can reuse effects added to the library in other effect compositions.")
|
||||||
|
acceptedButtons: Qt.NoButton
|
||||||
|
}
|
||||||
|
|
||||||
|
function handleClicked()
|
||||||
|
{
|
||||||
|
confirmForm.clear()
|
||||||
|
if (root.backendModel.canAddNodeToLibrary(index)) {
|
||||||
|
if (root.backendModel.nodeExists(index)) {
|
||||||
|
infoText.visible = false
|
||||||
|
confirmForm.showConfirmLibraryAdd(index)
|
||||||
|
} else {
|
||||||
|
infoText.showNodeAddedToLibraryInfo(root.backendModel
|
||||||
|
.addNodeToLibraryNode(index))
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
infoText.showNeedRenameInfo()
|
||||||
|
nameEditButton.handlePress()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onClicked: addToLibraryButton.handleClicked()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,16 +15,18 @@ Rectangle {
|
|||||||
width: 140
|
width: 140
|
||||||
height: 32
|
height: 32
|
||||||
|
|
||||||
color: mouseArea.containsMouse && modelData.canBeAdded
|
color: mouseArea.containsMouse && modelData.canBeAdded && root.enabled
|
||||||
? StudioTheme.Values.themeControlBackgroundInteraction : "transparent"
|
? StudioTheme.Values.themeControlBackgroundInteraction : "transparent"
|
||||||
|
|
||||||
signal addEffectNode(var nodeQenPath)
|
signal addEffectNode(var nodeQenPath)
|
||||||
|
signal removeEffectNodeFromLibrary(var nodeName)
|
||||||
|
|
||||||
ToolTipArea {
|
ToolTipArea {
|
||||||
id: mouseArea
|
id: mouseArea
|
||||||
|
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
acceptedButtons: Qt.LeftButton
|
acceptedButtons: Qt.LeftButton
|
||||||
|
visible: root.enabled
|
||||||
|
|
||||||
tooltip: modelData.canBeAdded ? modelData.nodeDescription
|
tooltip: modelData.canBeAdded ? modelData.nodeDescription
|
||||||
: qsTr("An effect with same properties already exists, this effect cannot be added.")
|
: qsTr("An effect with same properties already exists, this effect cannot be added.")
|
||||||
@@ -59,6 +61,28 @@ Rectangle {
|
|||||||
anchors.verticalCenter: nodeIcon.verticalCenter
|
anchors.verticalCenter: nodeIcon.verticalCenter
|
||||||
wrapMode: Text.WordWrap
|
wrapMode: Text.WordWrap
|
||||||
width: parent.width - parent.spacing - nodeIcon.width
|
width: parent.width - parent.spacing - nodeIcon.width
|
||||||
|
|
||||||
|
IconButton {
|
||||||
|
id: removeButton
|
||||||
|
|
||||||
|
visible: root.enabled && modelData.canBeRemoved
|
||||||
|
&& (mouseArea.containsMouse || removeButton.containsMouse)
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
anchors.rightMargin: StudioTheme.Values.iconAreaWidth / 4
|
||||||
|
icon: StudioTheme.Constants.close_small
|
||||||
|
transparentBg: false
|
||||||
|
buttonSize: StudioTheme.Values.iconAreaWidth
|
||||||
|
iconSize: StudioTheme.Values.smallIconFontSize
|
||||||
|
iconColor: StudioTheme.Values.themeTextColor
|
||||||
|
iconScale: removeButton.containsMouse ? 1.2 : 1
|
||||||
|
implicitWidth: width
|
||||||
|
tooltip: qsTr("Remove custom effect from the library.")
|
||||||
|
|
||||||
|
onPressed: (event) => {
|
||||||
|
root.removeEffectNodeFromLibrary(modelData.nodeName)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -78,8 +78,10 @@ StudioControls.ComboBox {
|
|||||||
flags: Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
|
flags: Qt.Tool | Qt.FramelessWindowHint | Qt.WindowStaysOnTopHint
|
||||||
|
|
||||||
onActiveFocusItemChanged: {
|
onActiveFocusItemChanged: {
|
||||||
if (!window.activeFocusItem && !root.hovered && root.popup.opened)
|
if (!window.activeFocusItem && !root.hovered && root.popup.opened) {
|
||||||
root.popup.close()
|
root.popup.close()
|
||||||
|
confirmRemoveForm.visible = false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Rectangle {
|
Rectangle {
|
||||||
@@ -92,6 +94,7 @@ StudioControls.ComboBox {
|
|||||||
HelperWidgets.ScrollView {
|
HelperWidgets.ScrollView {
|
||||||
anchors.fill: parent
|
anchors.fill: parent
|
||||||
anchors.margins: 1
|
anchors.margins: 1
|
||||||
|
enabled: !confirmRemoveForm.visible
|
||||||
|
|
||||||
Row {
|
Row {
|
||||||
id: row
|
id: row
|
||||||
@@ -118,10 +121,15 @@ StudioControls.ComboBox {
|
|||||||
|
|
||||||
EffectNode {
|
EffectNode {
|
||||||
onAddEffectNode: (nodeQenPath) => {
|
onAddEffectNode: (nodeQenPath) => {
|
||||||
EffectComposerBackend.rootView.addEffectNode(modelData.nodeQenPath)
|
EffectComposerBackend.rootView.addEffectNode(nodeQenPath)
|
||||||
|
confirmRemoveForm.visible = false
|
||||||
root.popup.close()
|
root.popup.close()
|
||||||
root.nodeJustAdded = true
|
root.nodeJustAdded = true
|
||||||
}
|
}
|
||||||
|
onRemoveEffectNodeFromLibrary: (nodeName) => {
|
||||||
|
confirmRemoveForm.visible = true
|
||||||
|
confirmRemoveForm.nodeName = nodeName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -129,10 +137,36 @@ StudioControls.ComboBox {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ConfirmForm {
|
||||||
|
id: confirmRemoveForm
|
||||||
|
|
||||||
|
property string nodeName
|
||||||
|
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
width: 350
|
||||||
|
height: 110
|
||||||
|
visible: false
|
||||||
|
text: qsTr("The node removal from library cannot be undone.\nAre you sure you want to remove node:\n'%1'?")
|
||||||
|
.arg(confirmRemoveForm.nodeName)
|
||||||
|
acceptButtonLabel: qsTr("Remove")
|
||||||
|
|
||||||
|
onAccepted: {
|
||||||
|
EffectComposerBackend.rootView.removeEffectNodeFromLibrary(confirmRemoveForm.nodeName)
|
||||||
|
confirmRemoveForm.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
onCanceled: confirmRemoveForm.visible = false
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
Keys.onPressed: function(event) {
|
Keys.onPressed: function(event) {
|
||||||
if (event.key === Qt.Key_Escape && root.popup.opened)
|
if (event.key === Qt.Key_Escape && root.popup.opened) {
|
||||||
|
confirmRemoveForm.visible = false
|
||||||
root.popup.close()
|
root.popup.close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
@@ -160,6 +160,14 @@ bool CompositionNode::isCustom() const
|
|||||||
return m_isCustom;
|
return m_isCustom;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void CompositionNode::setCustom(bool enable)
|
||||||
|
{
|
||||||
|
if (enable != m_isCustom) {
|
||||||
|
m_isCustom = enable;
|
||||||
|
emit isCustomChanged();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
CompositionNode::NodeType CompositionNode::type() const
|
CompositionNode::NodeType CompositionNode::type() const
|
||||||
{
|
{
|
||||||
return m_type;
|
return m_type;
|
||||||
|
@@ -24,7 +24,7 @@ class CompositionNode : public QObject
|
|||||||
Q_PROPERTY(QString nodeName READ name WRITE setName NOTIFY nameChanged)
|
Q_PROPERTY(QString nodeName READ name WRITE setName NOTIFY nameChanged)
|
||||||
Q_PROPERTY(bool nodeEnabled READ isEnabled WRITE setIsEnabled NOTIFY isEnabledChanged)
|
Q_PROPERTY(bool nodeEnabled READ isEnabled WRITE setIsEnabled NOTIFY isEnabledChanged)
|
||||||
Q_PROPERTY(bool isDependency READ isDependency NOTIFY isDepencyChanged)
|
Q_PROPERTY(bool isDependency READ isDependency NOTIFY isDepencyChanged)
|
||||||
Q_PROPERTY(bool isCustom READ isCustom CONSTANT)
|
Q_PROPERTY(bool isCustom READ isCustom NOTIFY isCustomChanged)
|
||||||
Q_PROPERTY(QObject *nodeUniformsModel READ uniformsModel NOTIFY uniformsModelChanged)
|
Q_PROPERTY(QObject *nodeUniformsModel READ uniformsModel NOTIFY uniformsModelChanged)
|
||||||
Q_PROPERTY(
|
Q_PROPERTY(
|
||||||
QString fragmentCode
|
QString fragmentCode
|
||||||
@@ -59,6 +59,7 @@ public:
|
|||||||
|
|
||||||
bool isDependency() const;
|
bool isDependency() const;
|
||||||
bool isCustom() const;
|
bool isCustom() const;
|
||||||
|
void setCustom(bool enable);
|
||||||
|
|
||||||
QString name() const;
|
QString name() const;
|
||||||
void setName(const QString &name);
|
void setName(const QString &name);
|
||||||
@@ -89,6 +90,7 @@ signals:
|
|||||||
void fragmentCodeChanged();
|
void fragmentCodeChanged();
|
||||||
void vertexCodeChanged();
|
void vertexCodeChanged();
|
||||||
void nameChanged();
|
void nameChanged();
|
||||||
|
void isCustomChanged();
|
||||||
|
|
||||||
private slots:
|
private slots:
|
||||||
void onUniformRenamed(const QString &oldName, const QString &newName);
|
void onUniformRenamed(const QString &oldName, const QString &newName);
|
||||||
|
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
#include "compositionnode.h"
|
#include "compositionnode.h"
|
||||||
#include "effectcomposertr.h"
|
#include "effectcomposertr.h"
|
||||||
|
#include "effectcomposernodesmodel.h"
|
||||||
#include "effectshaderscodeeditor.h"
|
#include "effectshaderscodeeditor.h"
|
||||||
#include "effectutils.h"
|
#include "effectutils.h"
|
||||||
#include "propertyhandler.h"
|
#include "propertyhandler.h"
|
||||||
@@ -31,7 +32,6 @@
|
|||||||
#include <QFileDialog>
|
#include <QFileDialog>
|
||||||
#include <QLibraryInfo>
|
#include <QLibraryInfo>
|
||||||
#include <QStandardPaths>
|
#include <QStandardPaths>
|
||||||
#include <QTemporaryDir>
|
|
||||||
#include <QVector2D>
|
#include <QVector2D>
|
||||||
|
|
||||||
using namespace Qt::StringLiterals;
|
using namespace Qt::StringLiterals;
|
||||||
@@ -43,6 +43,7 @@ static constexpr int MAIN_CODE_EDITOR_INDEX = -2;
|
|||||||
|
|
||||||
EffectComposerModel::EffectComposerModel(QObject *parent)
|
EffectComposerModel::EffectComposerModel(QObject *parent)
|
||||||
: QAbstractListModel{parent}
|
: QAbstractListModel{parent}
|
||||||
|
, m_effectComposerNodesModel{new EffectComposerNodesModel(this)}
|
||||||
, m_codeEditorIndex(INVALID_CODE_EDITOR_INDEX)
|
, m_codeEditorIndex(INVALID_CODE_EDITOR_INDEX)
|
||||||
, m_shaderDir(QDir::tempPath() + "/qds_ec_XXXXXX")
|
, m_shaderDir(QDir::tempPath() + "/qds_ec_XXXXXX")
|
||||||
, m_currentPreviewColor("#dddddd")
|
, m_currentPreviewColor("#dddddd")
|
||||||
@@ -53,6 +54,11 @@ EffectComposerModel::EffectComposerModel(QObject *parent)
|
|||||||
connectCodeEditor();
|
connectCodeEditor();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
EffectComposerNodesModel *EffectComposerModel::effectComposerNodesModel() const
|
||||||
|
{
|
||||||
|
return m_effectComposerNodesModel.data();
|
||||||
|
}
|
||||||
|
|
||||||
QHash<int, QByteArray> EffectComposerModel::roleNames() const
|
QHash<int, QByteArray> EffectComposerModel::roleNames() const
|
||||||
{
|
{
|
||||||
static const QHash<int, QByteArray> roles = {
|
static const QHash<int, QByteArray> roles = {
|
||||||
@@ -74,7 +80,7 @@ int EffectComposerModel::rowCount(const QModelIndex &parent) const
|
|||||||
|
|
||||||
QVariant EffectComposerModel::data(const QModelIndex &index, int role) const
|
QVariant EffectComposerModel::data(const QModelIndex &index, int role) const
|
||||||
{
|
{
|
||||||
QTC_ASSERT(index.isValid() && index.row() < m_nodes.size(), return {});
|
QTC_ASSERT(index.isValid() && isValidIndex(index.row()), return {});
|
||||||
QTC_ASSERT(roleNames().contains(role), return {});
|
QTC_ASSERT(roleNames().contains(role), return {});
|
||||||
|
|
||||||
return m_nodes.at(index.row())->property(roleNames().value(role));
|
return m_nodes.at(index.row())->property(roleNames().value(role));
|
||||||
@@ -226,7 +232,7 @@ void EffectComposerModel::removeNode(int idx)
|
|||||||
// Returns false if new name was generated
|
// Returns false if new name was generated
|
||||||
bool EffectComposerModel::changeNodeName(int nodeIndex, const QString &name)
|
bool EffectComposerModel::changeNodeName(int nodeIndex, const QString &name)
|
||||||
{
|
{
|
||||||
QTC_ASSERT(nodeIndex >= 0 && nodeIndex < m_nodes.size(), return false);
|
QTC_ASSERT(isValidIndex(nodeIndex), return false);
|
||||||
|
|
||||||
QString trimmedName = name.trimmed();
|
QString trimmedName = name.trimmed();
|
||||||
const QString oldName = m_nodes[nodeIndex]->name();
|
const QString oldName = m_nodes[nodeIndex]->name();
|
||||||
@@ -237,8 +243,9 @@ bool EffectComposerModel::changeNodeName(int nodeIndex, const QString &name)
|
|||||||
const QStringList reservedNames = nodeNames();
|
const QStringList reservedNames = nodeNames();
|
||||||
|
|
||||||
// Matching is done case-insensitive as section headers are shown in all uppercase
|
// Matching is done case-insensitive as section headers are shown in all uppercase
|
||||||
QString newName = QmlDesigner::UniqueName::generate(trimmedName, [&oldName, &reservedNames] (const QString &checkName) -> bool {
|
QString newName = QmlDesigner::UniqueName::generate(trimmedName, [&] (const QString &checkName) -> bool {
|
||||||
return oldName != checkName && reservedNames.contains(checkName, Qt::CaseInsensitive);
|
return oldName != checkName && (reservedNames.contains(checkName, Qt::CaseInsensitive)
|
||||||
|
|| m_effectComposerNodesModel->nodeExists(checkName));
|
||||||
});
|
});
|
||||||
|
|
||||||
if (newName != oldName) {
|
if (newName != oldName) {
|
||||||
@@ -1326,6 +1333,91 @@ void EffectComposerModel::openMainCodeEditor()
|
|||||||
setCodeEditorIndex(MAIN_CODE_EDITOR_INDEX);
|
setCodeEditorIndex(MAIN_CODE_EDITOR_INDEX);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool EffectComposerModel::canAddNodeToLibrary(int idx)
|
||||||
|
{
|
||||||
|
QTC_ASSERT(isValidIndex(idx), return false);
|
||||||
|
CompositionNode *node = m_nodes.at(idx);
|
||||||
|
|
||||||
|
if (m_effectComposerNodesModel->isBuiltIn(node->name()))
|
||||||
|
return !m_effectComposerNodesModel->nodeExists(node->name());
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EffectComposerModel::nodeExists(int idx)
|
||||||
|
{
|
||||||
|
QTC_ASSERT(isValidIndex(idx), return false);
|
||||||
|
CompositionNode *node = m_nodes.at(idx);
|
||||||
|
|
||||||
|
return m_effectComposerNodesModel->nodeExists(node->name());
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EffectComposerModel::addNodeToLibraryNode(int idx)
|
||||||
|
{
|
||||||
|
const QString errorTag{"@ERROR@"};
|
||||||
|
QTC_ASSERT(isValidIndex(idx) && canAddNodeToLibrary(idx), return errorTag);
|
||||||
|
|
||||||
|
CompositionNode *node = m_nodes.at(idx);
|
||||||
|
|
||||||
|
// Adding node to library changes it to a custom one, as we can't overwrite builtin nodes
|
||||||
|
node->setCustom(true);
|
||||||
|
|
||||||
|
QString fileNameBase = EffectUtils::nodeNameToFileName(node->name());
|
||||||
|
Utils::FilePath nodePath = Utils::FilePath::fromString(EffectUtils::nodeLibraryPath())
|
||||||
|
.pathAppended(fileNameBase);
|
||||||
|
Utils::FilePath qenFile = nodePath.pathAppended(fileNameBase + ".qen");
|
||||||
|
|
||||||
|
QSet<QString> newFileNames = {qenFile.fileName()};
|
||||||
|
Utils::FilePaths oldFiles;
|
||||||
|
bool update = nodePath.exists();
|
||||||
|
if (update)
|
||||||
|
oldFiles = nodePath.dirEntries(QDir::Files);
|
||||||
|
else
|
||||||
|
nodePath.createDir();
|
||||||
|
|
||||||
|
QJsonObject rootObj;
|
||||||
|
QJsonObject nodeObject = nodeToJson(*node);
|
||||||
|
rootObj.insert("QEN", nodeObject);
|
||||||
|
QJsonDocument jsonDoc(rootObj);
|
||||||
|
Utils::expected_str<qint64> result = qenFile.writeFileContents(jsonDoc.toJson());
|
||||||
|
if (!result)
|
||||||
|
return errorTag + Tr::tr("Failed to write QEN file for effect:\n%1").arg(qenFile.fileName());
|
||||||
|
|
||||||
|
QList<Utils::FilePath> sources;
|
||||||
|
QStringList dests;
|
||||||
|
const QList<Uniform *> uniforms = node->uniforms();
|
||||||
|
for (Uniform *uniform : uniforms) {
|
||||||
|
const QString valueStr = uniform->value().toString();
|
||||||
|
if (uniform->type() == Uniform::Type::Sampler && !valueStr.isEmpty()) {
|
||||||
|
Utils::FilePath imagePath = Utils::FilePath::fromString(valueStr);
|
||||||
|
QString imageFilename = imagePath.fileName();
|
||||||
|
sources.append(imagePath);
|
||||||
|
dests.append(imageFilename);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (int i = 0; i < sources.count(); ++i) {
|
||||||
|
Utils::FilePath source = sources[i];
|
||||||
|
Utils::FilePath target = nodePath.pathAppended(dests[i]);
|
||||||
|
newFileNames.insert(target.fileName());
|
||||||
|
if (target.exists() && source.fileName() != target.fileName())
|
||||||
|
target.removeFile(); // Remove existing file for update
|
||||||
|
if (!target.exists() && !source.copyFile(target))
|
||||||
|
return errorTag + Tr::tr("Failed to copy effect resource:\n%1").arg(source.fileName());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete old content that was not overwritten
|
||||||
|
for (const Utils::FilePath &oldFile : std::as_const(oldFiles)) {
|
||||||
|
if (!newFileNames.contains(oldFile.fileName()))
|
||||||
|
oldFile.removeFile();
|
||||||
|
}
|
||||||
|
|
||||||
|
m_effectComposerNodesModel->loadCustomNodes();
|
||||||
|
|
||||||
|
QString action = update ? Tr::tr("updated in") : Tr::tr("added to");
|
||||||
|
return Tr::tr("Effect was %1 effect library.").arg(action);
|
||||||
|
}
|
||||||
|
|
||||||
QVariant EffectComposerModel::valueLimit(const QString &type, bool max) const
|
QVariant EffectComposerModel::valueLimit(const QString &type, bool max) const
|
||||||
{
|
{
|
||||||
static const int intMin = std::numeric_limits<int>::lowest();
|
static const int intMin = std::numeric_limits<int>::lowest();
|
||||||
@@ -1619,7 +1711,7 @@ void EffectComposerModel::saveResources(const QString &name)
|
|||||||
newFileNames.insert(target.fileName());
|
newFileNames.insert(target.fileName());
|
||||||
if (target.exists() && source.fileName() != target.fileName())
|
if (target.exists() && source.fileName() != target.fileName())
|
||||||
target.removeFile(); // Remove existing file for update
|
target.removeFile(); // Remove existing file for update
|
||||||
if (!source.copyFile(target) && !target.exists())
|
if (!target.exists() && !source.copyFile(target))
|
||||||
setEffectError(QString("Failed to copy file: %1").arg(source.toFSPathString()));
|
setEffectError(QString("Failed to copy file: %1").arg(source.toFSPathString()));
|
||||||
|
|
||||||
if (fileNameToUniformHash.contains(dests[i])) {
|
if (fileNameToUniformHash.contains(dests[i])) {
|
||||||
@@ -2724,7 +2816,7 @@ QString EffectComposerModel::stripFileFromURL(const QString &urlString) const
|
|||||||
|
|
||||||
void EffectComposerModel::addOrUpdateNodeUniform(int idx, const QVariantMap &data, int updateIndex)
|
void EffectComposerModel::addOrUpdateNodeUniform(int idx, const QVariantMap &data, int updateIndex)
|
||||||
{
|
{
|
||||||
QTC_ASSERT(m_nodes.size() > idx && idx >= 0, return);
|
QTC_ASSERT(isValidIndex(idx), return);
|
||||||
|
|
||||||
// Convert values to Uniform digestible strings
|
// Convert values to Uniform digestible strings
|
||||||
auto fixedData = data;
|
auto fixedData = data;
|
||||||
@@ -2755,4 +2847,9 @@ bool EffectComposerModel::writeToFile(
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool EffectComposerModel::isValidIndex(int idx) const
|
||||||
|
{
|
||||||
|
return m_nodes.size() > idx && idx >= 0;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace EffectComposer
|
} // namespace EffectComposer
|
||||||
|
@@ -29,6 +29,7 @@ class Process;
|
|||||||
namespace EffectComposer {
|
namespace EffectComposer {
|
||||||
|
|
||||||
class CompositionNode;
|
class CompositionNode;
|
||||||
|
class EffectComposerNodesModel;
|
||||||
class EffectShadersCodeEditor;
|
class EffectShadersCodeEditor;
|
||||||
struct ShaderEditorData;
|
struct ShaderEditorData;
|
||||||
class Uniform;
|
class Uniform;
|
||||||
@@ -68,6 +69,8 @@ class EffectComposerModel : public QAbstractListModel
|
|||||||
public:
|
public:
|
||||||
EffectComposerModel(QObject *parent = nullptr);
|
EffectComposerModel(QObject *parent = nullptr);
|
||||||
|
|
||||||
|
EffectComposerNodesModel *effectComposerNodesModel() const;
|
||||||
|
|
||||||
QHash<int, QByteArray> roleNames() const override;
|
QHash<int, QByteArray> roleNames() const override;
|
||||||
int rowCount(const QModelIndex & parent = QModelIndex()) const override;
|
int rowCount(const QModelIndex & parent = QModelIndex()) const override;
|
||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
@@ -130,6 +133,10 @@ public:
|
|||||||
Q_INVOKABLE void openCodeEditor(int idx);
|
Q_INVOKABLE void openCodeEditor(int idx);
|
||||||
Q_INVOKABLE void openMainCodeEditor();
|
Q_INVOKABLE void openMainCodeEditor();
|
||||||
|
|
||||||
|
Q_INVOKABLE bool canAddNodeToLibrary(int idx);
|
||||||
|
Q_INVOKABLE bool nodeExists(int idx);
|
||||||
|
Q_INVOKABLE QString addNodeToLibraryNode(int idx);
|
||||||
|
|
||||||
Q_INVOKABLE QVariant valueLimit(const QString &type, bool max) const;
|
Q_INVOKABLE QVariant valueLimit(const QString &type, bool max) const;
|
||||||
|
|
||||||
void openComposition(const QString &path);
|
void openComposition(const QString &path);
|
||||||
@@ -265,6 +272,7 @@ private:
|
|||||||
bool writeToFile(const QByteArray &buf, const QString &filename, FileType fileType);
|
bool writeToFile(const QByteArray &buf, const QString &filename, FileType fileType);
|
||||||
|
|
||||||
QList<CompositionNode *> m_nodes;
|
QList<CompositionNode *> m_nodes;
|
||||||
|
QPointer<EffectComposerNodesModel> m_effectComposerNodesModel;
|
||||||
|
|
||||||
int m_selectedIndex = -1;
|
int m_selectedIndex = -1;
|
||||||
int m_codeEditorIndex = -1;
|
int m_codeEditorIndex = -1;
|
||||||
|
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
#include "effectcomposernodesmodel.h"
|
#include "effectcomposernodesmodel.h"
|
||||||
|
|
||||||
#include "effectutils.h"
|
#include "effectutils.h"
|
||||||
|
|
||||||
#include <utils/algorithm.h>
|
#include <utils/algorithm.h>
|
||||||
@@ -12,6 +13,8 @@
|
|||||||
|
|
||||||
namespace EffectComposer {
|
namespace EffectComposer {
|
||||||
|
|
||||||
|
constexpr QStringView customCatName{u"Custom"};
|
||||||
|
|
||||||
EffectComposerNodesModel::EffectComposerNodesModel(QObject *parent)
|
EffectComposerNodesModel::EffectComposerNodesModel(QObject *parent)
|
||||||
: QAbstractListModel{parent}
|
: QAbstractListModel{parent}
|
||||||
{
|
{
|
||||||
@@ -53,7 +56,8 @@ void EffectComposerNodesModel::loadModel()
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
m_categories = {};
|
m_categories.clear();
|
||||||
|
m_builtInNodeNames.clear();
|
||||||
|
|
||||||
QDirIterator itCategories(nodesPath.toUrlishString(), QDir::Dirs | QDir::NoDotAndDotDot);
|
QDirIterator itCategories(nodesPath.toUrlishString(), QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
while (itCategories.hasNext()) {
|
while (itCategories.hasNext()) {
|
||||||
@@ -69,10 +73,11 @@ void EffectComposerNodesModel::loadModel()
|
|||||||
QDirIterator itEffects(categoryPath.toUrlishString(), {"*.qen"}, QDir::Files);
|
QDirIterator itEffects(categoryPath.toUrlishString(), {"*.qen"}, QDir::Files);
|
||||||
while (itEffects.hasNext()) {
|
while (itEffects.hasNext()) {
|
||||||
itEffects.next();
|
itEffects.next();
|
||||||
auto node = new EffectNode(itEffects.filePath());
|
auto node = new EffectNode(itEffects.filePath(), true);
|
||||||
if (!node->defaultImagesHash().isEmpty())
|
if (!node->defaultImagesHash().isEmpty())
|
||||||
m_defaultImagesHash.insert(node->name(), node->defaultImagesHash());
|
m_defaultImagesHash.insert(node->name(), node->defaultImagesHash());
|
||||||
effects.push_back(node);
|
effects.push_back(node);
|
||||||
|
m_builtInNodeNames.append(node->name());
|
||||||
}
|
}
|
||||||
|
|
||||||
catName[0] = catName[0].toUpper(); // capitalize first letter
|
catName[0] = catName[0].toUpper(); // capitalize first letter
|
||||||
@@ -80,11 +85,15 @@ void EffectComposerNodesModel::loadModel()
|
|||||||
|
|
||||||
EffectNodesCategory *category = new EffectNodesCategory(catName, effects);
|
EffectNodesCategory *category = new EffectNodesCategory(catName, effects);
|
||||||
m_categories.push_back(category);
|
m_categories.push_back(category);
|
||||||
|
|
||||||
|
if (catName == customCatName && !effects.isEmpty()) {
|
||||||
|
m_builtinCustomNode = effects[0];
|
||||||
|
m_customCategory = category;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const QString customCatName = "Custom";
|
|
||||||
std::sort(m_categories.begin(), m_categories.end(),
|
std::sort(m_categories.begin(), m_categories.end(),
|
||||||
[&customCatName](EffectNodesCategory *a, EffectNodesCategory *b) {
|
[](EffectNodesCategory *a, EffectNodesCategory *b) {
|
||||||
if (a->name() == customCatName)
|
if (a->name() == customCatName)
|
||||||
return false;
|
return false;
|
||||||
if (b->name() == customCatName)
|
if (b->name() == customCatName)
|
||||||
@@ -94,6 +103,40 @@ void EffectComposerNodesModel::loadModel()
|
|||||||
|
|
||||||
m_modelLoaded = true;
|
m_modelLoaded = true;
|
||||||
|
|
||||||
|
loadCustomNodes();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EffectComposerNodesModel::loadCustomNodes()
|
||||||
|
{
|
||||||
|
if (!m_customCategory)
|
||||||
|
return;
|
||||||
|
|
||||||
|
for (const QString &nodeName : std::as_const(m_customNodeNames))
|
||||||
|
m_defaultImagesHash.remove(nodeName);
|
||||||
|
|
||||||
|
m_customNodeNames.clear();
|
||||||
|
|
||||||
|
QList<EffectNode *> effects;
|
||||||
|
|
||||||
|
const Utils::FilePath nodeLibPath = Utils::FilePath::fromString(EffectUtils::nodeLibraryPath());
|
||||||
|
const Utils::FilePaths libraryNodes = nodeLibPath.dirEntries(QDir::Dirs | QDir::NoDotAndDotDot);
|
||||||
|
for (const Utils::FilePath &nodePath : libraryNodes) {
|
||||||
|
const Utils::FilePath qenPath = nodePath.pathAppended(nodePath.fileName() + ".qen");
|
||||||
|
auto node = new EffectNode(qenPath.toFSPathString(), false);
|
||||||
|
if (!node->defaultImagesHash().isEmpty())
|
||||||
|
m_defaultImagesHash.insert(node->name(), node->defaultImagesHash());
|
||||||
|
m_customNodeNames.append(node->name());
|
||||||
|
effects.push_back(node);
|
||||||
|
}
|
||||||
|
|
||||||
|
Utils::sort(effects, &EffectNode::name);
|
||||||
|
|
||||||
|
if (m_customCategory) {
|
||||||
|
if (m_builtinCustomNode)
|
||||||
|
effects.prepend(m_builtinCustomNode);
|
||||||
|
m_customCategory->setNodes(effects);
|
||||||
|
}
|
||||||
|
|
||||||
resetModel();
|
resetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -103,6 +146,17 @@ void EffectComposerNodesModel::resetModel()
|
|||||||
endResetModel();
|
endResetModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bool EffectComposerNodesModel::nodeExists(const QString &name)
|
||||||
|
{
|
||||||
|
return m_customNodeNames.contains(name, Qt::CaseInsensitive)
|
||||||
|
|| m_builtInNodeNames.contains(name, Qt::CaseInsensitive);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool EffectComposerNodesModel::isBuiltIn(const QString &name)
|
||||||
|
{
|
||||||
|
return m_builtInNodeNames.contains(name, Qt::CaseInsensitive);
|
||||||
|
}
|
||||||
|
|
||||||
void EffectComposerNodesModel::updateCanBeAdded(
|
void EffectComposerNodesModel::updateCanBeAdded(
|
||||||
const QStringList &uniforms, [[maybe_unused]] const QStringList &nodeNames)
|
const QStringList &uniforms, [[maybe_unused]] const QStringList &nodeNames)
|
||||||
{
|
{
|
||||||
@@ -126,4 +180,22 @@ QHash<QString, QString> EffectComposerNodesModel::defaultImagesForNode(const QSt
|
|||||||
return m_defaultImagesHash.value(name);
|
return m_defaultImagesHash.value(name);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EffectComposerNodesModel::removeEffectNode(const QString &name)
|
||||||
|
{
|
||||||
|
if (!m_customCategory || name.isEmpty())
|
||||||
|
return;
|
||||||
|
|
||||||
|
m_defaultImagesHash.remove(name);
|
||||||
|
m_customNodeNames.removeOne(name);
|
||||||
|
m_customCategory->removeNode(name);
|
||||||
|
|
||||||
|
QString fileNameBase = EffectUtils::nodeNameToFileName(name);
|
||||||
|
Utils::FilePath nodePath = Utils::FilePath::fromString(EffectUtils::nodeLibraryPath())
|
||||||
|
.pathAppended(fileNameBase);
|
||||||
|
if (nodePath.exists())
|
||||||
|
nodePath.removeRecursively();
|
||||||
|
|
||||||
|
resetModel();
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace EffectComposer
|
} // namespace EffectComposer
|
||||||
|
@@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
namespace EffectComposer {
|
namespace EffectComposer {
|
||||||
|
|
||||||
|
class EffectNode;
|
||||||
|
|
||||||
class EffectComposerNodesModel : public QAbstractListModel
|
class EffectComposerNodesModel : public QAbstractListModel
|
||||||
{
|
{
|
||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
@@ -26,14 +28,20 @@ public:
|
|||||||
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override;
|
||||||
|
|
||||||
void loadModel();
|
void loadModel();
|
||||||
|
void loadCustomNodes();
|
||||||
void resetModel();
|
void resetModel();
|
||||||
|
|
||||||
|
bool nodeExists(const QString &name);
|
||||||
|
bool isBuiltIn(const QString &name);
|
||||||
|
|
||||||
QList<EffectNodesCategory *> categories() const { return m_categories; }
|
QList<EffectNodesCategory *> categories() const { return m_categories; }
|
||||||
|
|
||||||
void updateCanBeAdded(const QStringList &uniforms, const QStringList &nodeNames);
|
void updateCanBeAdded(const QStringList &uniforms, const QStringList &nodeNames);
|
||||||
|
|
||||||
QHash<QString, QString> defaultImagesForNode(const QString &name) const;
|
QHash<QString, QString> defaultImagesForNode(const QString &name) const;
|
||||||
|
|
||||||
|
void removeEffectNode(const QString &name);
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString nodesSourcesPath() const;
|
QString nodesSourcesPath() const;
|
||||||
|
|
||||||
@@ -41,6 +49,10 @@ private:
|
|||||||
bool m_probeNodesDir = false;
|
bool m_probeNodesDir = false;
|
||||||
bool m_modelLoaded = false;
|
bool m_modelLoaded = false;
|
||||||
QHash<QString, QHash<QString, QString>> m_defaultImagesHash;
|
QHash<QString, QHash<QString, QString>> m_defaultImagesHash;
|
||||||
|
QStringList m_builtInNodeNames;
|
||||||
|
QStringList m_customNodeNames;
|
||||||
|
EffectNode *m_builtinCustomNode = nullptr;
|
||||||
|
EffectNodesCategory *m_customCategory = nullptr;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace EffectComposer
|
} // namespace EffectComposer
|
||||||
|
@@ -75,7 +75,6 @@ static QList<QmlDesigner::ModelNode> modelNodesFromMimeData(const QByteArray &mi
|
|||||||
|
|
||||||
EffectComposerWidget::EffectComposerWidget(EffectComposerView *view)
|
EffectComposerWidget::EffectComposerWidget(EffectComposerView *view)
|
||||||
: m_effectComposerModel{new EffectComposerModel(this)}
|
: m_effectComposerModel{new EffectComposerModel(this)}
|
||||||
, m_effectComposerNodesModel{new EffectComposerNodesModel(this)}
|
|
||||||
, m_effectComposerView(view)
|
, m_effectComposerView(view)
|
||||||
, m_quickWidget{new StudioQuickWidget(this)}
|
, m_quickWidget{new StudioQuickWidget(this)}
|
||||||
{
|
{
|
||||||
@@ -108,7 +107,7 @@ EffectComposerWidget::EffectComposerWidget(EffectComposerView *view)
|
|||||||
g_propertyData.insert(QString("blur_fs_path"), QString(blurPath + "bluritems.frag.qsb"));
|
g_propertyData.insert(QString("blur_fs_path"), QString(blurPath + "bluritems.frag.qsb"));
|
||||||
|
|
||||||
auto map = m_quickWidget->registerPropertyMap("EffectComposerBackend");
|
auto map = m_quickWidget->registerPropertyMap("EffectComposerBackend");
|
||||||
map->setProperties({{"effectComposerNodesModel", QVariant::fromValue(m_effectComposerNodesModel.data())},
|
map->setProperties({{"effectComposerNodesModel", QVariant::fromValue(effectComposerNodesModel().data())},
|
||||||
{"effectComposerModel", QVariant::fromValue(m_effectComposerModel.data())},
|
{"effectComposerModel", QVariant::fromValue(m_effectComposerModel.data())},
|
||||||
{"rootView", QVariant::fromValue(this)}});
|
{"rootView", QVariant::fromValue(this)}});
|
||||||
|
|
||||||
@@ -174,7 +173,7 @@ QPointer<EffectComposerModel> EffectComposerWidget::effectComposerModel() const
|
|||||||
|
|
||||||
QPointer<EffectComposerNodesModel> EffectComposerWidget::effectComposerNodesModel() const
|
QPointer<EffectComposerNodesModel> EffectComposerWidget::effectComposerNodesModel() const
|
||||||
{
|
{
|
||||||
return m_effectComposerNodesModel;
|
return m_effectComposerModel->effectComposerNodesModel();
|
||||||
}
|
}
|
||||||
|
|
||||||
void EffectComposerWidget::addEffectNode(const QString &nodeQenPath)
|
void EffectComposerWidget::addEffectNode(const QString &nodeQenPath)
|
||||||
@@ -188,6 +187,11 @@ void EffectComposerWidget::addEffectNode(const QString &nodeQenPath)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EffectComposerWidget::removeEffectNodeFromLibrary(const QString &nodeName)
|
||||||
|
{
|
||||||
|
effectComposerNodesModel()->removeEffectNode(nodeName);
|
||||||
|
}
|
||||||
|
|
||||||
void EffectComposerWidget::focusSection(int section)
|
void EffectComposerWidget::focusSection(int section)
|
||||||
{
|
{
|
||||||
Q_UNUSED(section)
|
Q_UNUSED(section)
|
||||||
@@ -209,7 +213,7 @@ QPoint EffectComposerWidget::globalPos(const QPoint &point) const
|
|||||||
|
|
||||||
QString EffectComposerWidget::uniformDefaultImage(const QString &nodeName, const QString &uniformName) const
|
QString EffectComposerWidget::uniformDefaultImage(const QString &nodeName, const QString &uniformName) const
|
||||||
{
|
{
|
||||||
return m_effectComposerNodesModel->defaultImagesForNode(nodeName).value(uniformName);
|
return effectComposerNodesModel()->defaultImagesForNode(nodeName).value(uniformName);
|
||||||
}
|
}
|
||||||
|
|
||||||
QString EffectComposerWidget::imagesPath() const
|
QString EffectComposerWidget::imagesPath() const
|
||||||
@@ -247,7 +251,7 @@ void EffectComposerWidget::dropNode(const QByteArray &mimeData)
|
|||||||
|
|
||||||
void EffectComposerWidget::updateCanBeAdded()
|
void EffectComposerWidget::updateCanBeAdded()
|
||||||
{
|
{
|
||||||
m_effectComposerNodesModel->updateCanBeAdded(m_effectComposerModel->uniformNames(),
|
effectComposerNodesModel()->updateCanBeAdded(m_effectComposerModel->uniformNames(),
|
||||||
m_effectComposerModel->nodeNames());
|
m_effectComposerModel->nodeNames());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -47,6 +47,7 @@ public:
|
|||||||
QPointer<EffectComposerNodesModel> effectComposerNodesModel() const;
|
QPointer<EffectComposerNodesModel> effectComposerNodesModel() const;
|
||||||
|
|
||||||
Q_INVOKABLE void addEffectNode(const QString &nodeQenPath);
|
Q_INVOKABLE void addEffectNode(const QString &nodeQenPath);
|
||||||
|
Q_INVOKABLE void removeEffectNodeFromLibrary(const QString &nodeName);
|
||||||
Q_INVOKABLE void focusSection(int section);
|
Q_INVOKABLE void focusSection(int section);
|
||||||
Q_INVOKABLE void doOpenComposition();
|
Q_INVOKABLE void doOpenComposition();
|
||||||
Q_INVOKABLE QRect screenRect() const;
|
Q_INVOKABLE QRect screenRect() const;
|
||||||
@@ -68,7 +69,6 @@ private:
|
|||||||
void handleImportScanTimer();
|
void handleImportScanTimer();
|
||||||
|
|
||||||
QPointer<EffectComposerModel> m_effectComposerModel;
|
QPointer<EffectComposerModel> m_effectComposerModel;
|
||||||
QPointer<EffectComposerNodesModel> m_effectComposerNodesModel;
|
|
||||||
QPointer<EffectComposerView> m_effectComposerView;
|
QPointer<EffectComposerView> m_effectComposerView;
|
||||||
QPointer<StudioQuickWidget> m_quickWidget;
|
QPointer<StudioQuickWidget> m_quickWidget;
|
||||||
QmlDesigner::QmlModelNodeProxy m_backendModelNode;
|
QmlDesigner::QmlModelNodeProxy m_backendModelNode;
|
||||||
|
@@ -2,7 +2,9 @@
|
|||||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||||
|
|
||||||
#include "effectnode.h"
|
#include "effectnode.h"
|
||||||
|
|
||||||
#include "compositionnode.h"
|
#include "compositionnode.h"
|
||||||
|
#include "effectutils.h"
|
||||||
#include "uniform.h"
|
#include "uniform.h"
|
||||||
|
|
||||||
#include <QDir>
|
#include <QDir>
|
||||||
@@ -10,26 +12,26 @@
|
|||||||
|
|
||||||
namespace EffectComposer {
|
namespace EffectComposer {
|
||||||
|
|
||||||
EffectNode::EffectNode(const QString &qenPath)
|
EffectNode::EffectNode(const QString &qenPath, bool isBuiltIn)
|
||||||
: m_qenPath(qenPath)
|
: m_qenPath(qenPath)
|
||||||
{
|
{
|
||||||
const QFileInfo fileInfo = QFileInfo(qenPath);
|
const QFileInfo fileInfo = QFileInfo(qenPath);
|
||||||
|
|
||||||
QString iconPath = QStringLiteral("%1/icon/%2.svg").arg(fileInfo.absolutePath(),
|
|
||||||
fileInfo.baseName());
|
|
||||||
if (!QFileInfo::exists(iconPath)) {
|
|
||||||
QDir parentDir = QDir(fileInfo.absolutePath());
|
|
||||||
parentDir.cdUp();
|
|
||||||
|
|
||||||
iconPath = QStringLiteral("%1/%2").arg(parentDir.path(), "placeholder.svg");
|
|
||||||
}
|
|
||||||
m_iconPath = QUrl::fromLocalFile(iconPath);
|
|
||||||
|
|
||||||
CompositionNode node({}, qenPath);
|
CompositionNode node({}, qenPath);
|
||||||
|
|
||||||
m_name = node.name();
|
m_name = node.name();
|
||||||
m_description = node.description();
|
m_description = node.description();
|
||||||
m_isCustom = node.isCustom();
|
m_isCustom = node.isCustom();
|
||||||
|
m_canBeRemoved = !isBuiltIn;
|
||||||
|
|
||||||
|
QString iconPath = QStringLiteral("%1/icon/%2.svg").arg(fileInfo.absolutePath(),
|
||||||
|
fileInfo.baseName());
|
||||||
|
if (!QFileInfo::exists(iconPath)) {
|
||||||
|
QString iconFileName = m_isCustom ? QString{"user_custom_effect.svg"}
|
||||||
|
: QString{"placeholder.svg"};
|
||||||
|
iconPath = QStringLiteral("%1/%2").arg(EffectUtils::nodesSourcesPath(), iconFileName);
|
||||||
|
}
|
||||||
|
m_iconPath = QUrl::fromLocalFile(iconPath);
|
||||||
|
|
||||||
const QList<Uniform *> uniforms = node.uniforms();
|
const QList<Uniform *> uniforms = node.uniforms();
|
||||||
for (const Uniform *uniform : uniforms) {
|
for (const Uniform *uniform : uniforms) {
|
||||||
|
@@ -18,9 +18,10 @@ class EffectNode : public QObject
|
|||||||
Q_PROPERTY(QUrl nodeIcon MEMBER m_iconPath CONSTANT)
|
Q_PROPERTY(QUrl nodeIcon MEMBER m_iconPath CONSTANT)
|
||||||
Q_PROPERTY(QString nodeQenPath MEMBER m_qenPath CONSTANT)
|
Q_PROPERTY(QString nodeQenPath MEMBER m_qenPath CONSTANT)
|
||||||
Q_PROPERTY(bool canBeAdded MEMBER m_canBeAdded NOTIFY canBeAddedChanged)
|
Q_PROPERTY(bool canBeAdded MEMBER m_canBeAdded NOTIFY canBeAddedChanged)
|
||||||
|
Q_PROPERTY(bool canBeRemoved MEMBER m_canBeRemoved CONSTANT)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
EffectNode(const QString &qenPath);
|
EffectNode(const QString &qenPath, bool isBuiltIn);
|
||||||
|
|
||||||
QString name() const;
|
QString name() const;
|
||||||
QString description() const;
|
QString description() const;
|
||||||
@@ -42,6 +43,7 @@ private:
|
|||||||
QUrl m_iconPath;
|
QUrl m_iconPath;
|
||||||
bool m_isCustom = false;
|
bool m_isCustom = false;
|
||||||
bool m_canBeAdded = true;
|
bool m_canBeAdded = true;
|
||||||
|
bool m_canBeRemoved = false;
|
||||||
QSet<QString> m_uniformNames;
|
QSet<QString> m_uniformNames;
|
||||||
QHash<QString, QString> m_defaultImagesHash;
|
QHash<QString, QString> m_defaultImagesHash;
|
||||||
};
|
};
|
||||||
|
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
#include "effectnodescategory.h"
|
#include "effectnodescategory.h"
|
||||||
|
|
||||||
|
#include <utils/algorithm.h>
|
||||||
|
|
||||||
namespace EffectComposer {
|
namespace EffectComposer {
|
||||||
|
|
||||||
EffectNodesCategory::EffectNodesCategory(const QString &name, const QList<EffectNode *> &nodes)
|
EffectNodesCategory::EffectNodesCategory(const QString &name, const QList<EffectNode *> &nodes)
|
||||||
@@ -19,5 +21,19 @@ QList<EffectNode *> EffectNodesCategory::nodes() const
|
|||||||
return m_categoryNodes;
|
return m_categoryNodes;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void EffectNodesCategory::setNodes(const QList<EffectNode *> &nodes)
|
||||||
|
{
|
||||||
|
m_categoryNodes = nodes;
|
||||||
|
|
||||||
|
emit nodesChanged();
|
||||||
|
}
|
||||||
|
|
||||||
|
void EffectNodesCategory::removeNode(const QString &nodeName)
|
||||||
|
{
|
||||||
|
Utils::eraseOne(m_categoryNodes, [nodeName](const EffectNode *node) {
|
||||||
|
return node->name() == nodeName;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace EffectComposer
|
} // namespace EffectComposer
|
||||||
|
|
||||||
|
@@ -14,13 +14,18 @@ class EffectNodesCategory : public QObject
|
|||||||
Q_OBJECT
|
Q_OBJECT
|
||||||
|
|
||||||
Q_PROPERTY(QString categoryName MEMBER m_name CONSTANT)
|
Q_PROPERTY(QString categoryName MEMBER m_name CONSTANT)
|
||||||
Q_PROPERTY(QList<EffectNode *> categoryNodes MEMBER m_categoryNodes CONSTANT)
|
Q_PROPERTY(QList<EffectNode *> categoryNodes READ nodes NOTIFY nodesChanged)
|
||||||
|
|
||||||
public:
|
public:
|
||||||
EffectNodesCategory(const QString &name, const QList<EffectNode *> &nodes);
|
EffectNodesCategory(const QString &name, const QList<EffectNode *> &nodes);
|
||||||
|
|
||||||
QString name() const;
|
QString name() const;
|
||||||
QList<EffectNode *> nodes() const;
|
QList<EffectNode *> nodes() const;
|
||||||
|
void setNodes(const QList<EffectNode *> &nodes);
|
||||||
|
void removeNode(const QString &nodeName);
|
||||||
|
|
||||||
|
signals:
|
||||||
|
void nodesChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QString m_name;
|
QString m_name;
|
||||||
|
@@ -6,6 +6,8 @@
|
|||||||
#include <coreplugin/icore.h>
|
#include <coreplugin/icore.h>
|
||||||
|
|
||||||
#include <QJsonArray>
|
#include <QJsonArray>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
#include <QStandardPaths>
|
||||||
|
|
||||||
namespace EffectComposer {
|
namespace EffectComposer {
|
||||||
|
|
||||||
@@ -31,4 +33,20 @@ QString EffectUtils::nodesSourcesPath()
|
|||||||
return Core::ICore::resourcePath("qmldesigner/effectComposerNodes").toUrlishString();
|
return Core::ICore::resourcePath("qmldesigner/effectComposerNodes").toUrlishString();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
QString EffectUtils::nodeLibraryPath()
|
||||||
|
{
|
||||||
|
QStandardPaths::StandardLocation location = QStandardPaths::DocumentsLocation;
|
||||||
|
|
||||||
|
return QStandardPaths::writableLocation(location)
|
||||||
|
+ "/QtDesignStudio/effect_composer/node_library";
|
||||||
|
}
|
||||||
|
|
||||||
|
QString EffectUtils::nodeNameToFileName(const QString &nodeName)
|
||||||
|
{
|
||||||
|
static const QRegularExpression re("[^a-zA-Z0-9]");
|
||||||
|
QString newName = nodeName;
|
||||||
|
newName.replace(re, "_");
|
||||||
|
return newName;
|
||||||
|
}
|
||||||
|
|
||||||
} // namespace EffectComposer
|
} // namespace EffectComposer
|
||||||
|
@@ -15,8 +15,9 @@ public:
|
|||||||
EffectUtils() = delete;
|
EffectUtils() = delete;
|
||||||
|
|
||||||
static QString codeFromJsonArray(const QJsonArray &codeArray);
|
static QString codeFromJsonArray(const QJsonArray &codeArray);
|
||||||
|
|
||||||
static QString nodesSourcesPath();
|
static QString nodesSourcesPath();
|
||||||
|
static QString nodeLibraryPath();
|
||||||
|
static QString nodeNameToFileName(const QString &nodeName);
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace EffectComposer
|
} // namespace EffectComposer
|
||||||
|
Reference in New Issue
Block a user