Node graph save functionality

Change-Id: Ic43649b68d6488a77a7b7ac055ef5377877d7646
Reviewed-by: spyro-adb <adb@spyro-soft.com>
This commit is contained in:
Andrzej Biniek
2024-12-14 14:57:45 +01:00
committed by spyro-adb
parent e17d213580
commit db42dde3b4
33 changed files with 682 additions and 14 deletions

View File

@@ -22,6 +22,7 @@ TreeViewDelegate {
readonly property string suffix: model.fileName.substr(-4)
readonly property bool isFont: root.suffix === ".ttf" || root.suffix === ".otf"
readonly property bool isEffect: root.suffix === ".qep"
readonly property bool isNodeGraph: root.suffix === ".qng"
property bool currFileSelected: false
property int initialDepth: -1
property bool __isDirectory: assetsModel.isDirectory(model.filePath)
@@ -202,6 +203,10 @@ TreeViewDelegate {
AssetsLibraryBackend.tooltipBackend.hideTooltip()
if (mouse.button === Qt.LeftButton && root.isEffect)
AssetsLibraryBackend.rootView.openEffectComposer(filePath)
if (mouse.button === Qt.LeftButton && root.isNodeGraph){
AssetsLibraryBackend.rootView.openNodeGraphEditor(filePath)
}
}
ToolTip {

View File

@@ -12,6 +12,7 @@ GridItem {
readonly property bool isFont: root.suffix === ".ttf" || root.suffix === ".otf"
readonly property bool isEffect: root.suffix === ".qep"
readonly property bool isMaterial: root.suffix === ".mat"
readonly property bool isNodeGraph: root.suffix === ".qng"
icon.source: "image://qmldesigner_assets/" + model.filePath
@@ -48,10 +49,15 @@ GridItem {
mouseArea.onDoubleClicked: (mouse) => {
if (mouse.button === Qt.LeftButton) {
if (root.isEffect)
if (root.isEffect) {
root.rootView.openEffectComposer(filePath)
else if (root.isMaterial)
}
else if (root.isMaterial) {
root.rootView.openMaterialEditor(filePath)
}
else if (root.isNodeGraph) {
root.rootView.openNodeGraphEditor(filePath)
}
}
}

View File

@@ -9,12 +9,195 @@ import StudioTheme as StudioTheme
import QuickQanava as Qan
import Editor as Editor
import Nodes as Nodes
import NodeGraphEditorBackend
Item {
id: root
property string nodeGraphFileName
// Invoked after save changes is done
property var onSaveChangesCallback: () => {}
function initEdges(edges) {
for (let edge in edges) {
let outNodeUuid = edges[edge].outNodeUuid;
let inNodeUuid = edges[edge].inNodeUuid;
let n1, n2;
for (let i = 0; i < graphView.graph.getNodeCount(); i++) {
if (graphView.graph.nodes.at(i).item.uuid == outNodeUuid) {
n1 = graphView.graph.nodes.at(i);
break;
}
}
for (let i = 0; i < graphView.graph.getNodeCount(); i++) {
if (graphView.graph.nodes.at(i).item.uuid == inNodeUuid) {
n2 = graphView.graph.nodes.at(i);
break;
}
}
let e = graphView.graph.insertEdge(n1, n2);
let p1 = n1.item.findPort(edges[edge].outPortName);
let p2 = n2.item.findPort(edges[edge].inPortName);
graphView.graph.bindEdge(e, p1, p2);
if (p2.dataBinding) {
p2.dataBinding(n1.item.value);
} else {
// TODO: change old implementation
n2.item.value[p2.dataName] = Qt.binding(() => {
if (n1.dataName !== "")
return n1.item.value[p1.dataName];
return n1.item.value;
});
}
}
}
function initNodes(nodes) {
graphView.graph.clearGraph();
let n = undefined;
for (let node in nodes) {
switch (nodes[node].type) {
case "Material":
n = graphView.graph.insertNode(Nodes.Components.material);
break;
case "Color":
n = graphView.graph.insertNode(Nodes.Components.color);
n.item.value.color = nodes[node].value.color;
break;
case "Metalness":
n = graphView.graph.insertNode(Nodes.Components.metalness);
break;
case "BaseColor":
n = graphView.graph.insertNode(Nodes.Components.baseColor);
break;
case "RealSpinBox":
n = graphView.graph.insertNode(Nodes.Components.realSpinBox);
n.item.value.floating = nodes[node].value.floating;
break;
case "Texture":
n = graphView.graph.insertNode(Nodes.Components.texture);
break;
case "CheckBox":
n = graphView.graph.insertNode(Nodes.Components.checkBox);
n.item.value.binary = nodes[node].value.binary;
break;
case "ComboBox":
n = graphView.graph.insertNode(Nodes.Components.comboBox);
n.item.value.text = nodes[node].value.text;
break;
case "Roughness":
n = graphView.graph.insertNode(Nodes.Components.roughness);
break;
}
n.item.x = nodes[node].x;
n.item.y = nodes[node].y;
n.item.uuid = nodes[node].uuid;
}
}
// Invoked from C++ side when open node graph is requested and there are unsaved changes
function promptToSaveBeforeOpen(newNodeGraphName) {
nodeGraphFileName = newNodeGraphName;
saveChangesDialog.open();
}
function updateGraphData() {
let arr = {
nodes: [],
edges: []
};
for (let i = 0; i < graphView.graph.nodes.length; i++) {
let node = {
uuid: graphView.graph.nodes.at(i).item.uuid,
x: graphView.graph.nodes.at(i).item.x,
y: graphView.graph.nodes.at(i).item.y,
type: graphView.graph.nodes.at(i).item.type
};
//Set the actual value only to the OUT nodes
if (node.type == "Color" || node.type == "RealSpinBox" || node.type == "CheckBox" || node.type == "ComboBox") {
node.value = graphView.graph.nodes.at(i).item.value;
}
arr["nodes"].push(node);
}
for (let i = 0; i < graphView.graph.edges.length; i++) {
let edge = {
outNodeUuid: graphView.graph.edges.at(i).item.sourceItem.node.item.uuid,
inNodeUuid: graphView.graph.edges.at(i).item.destinationItem.node.item.uuid,
inPortName: graphView.graph.edges.at(i).item.destinationItem.dataId,
outPortName: graphView.graph.edges.at(i).item.sourceItem.dataId
};
arr["edges"].push(edge);
}
let newGraphData = JSON.stringify(arr);
if (NodeGraphEditorBackend.nodeGraphEditorModel.graphData !== newGraphData) {
NodeGraphEditorBackend.nodeGraphEditorModel.graphData = newGraphData;
}
}
Connections {
target: NodeGraphEditorBackend.nodeGraphEditorModel
onNodeGraphLoaded: {
graphView.graph.clearGraph();
let jsonString = NodeGraphEditorBackend.nodeGraphEditorModel.graphData;
let jsonObject = JSON.parse(jsonString);
let nodes = jsonObject.nodes;
let edges = jsonObject.edges;
initNodes(nodes);
initEdges(edges);
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = false;
}
}
SaveAsDialog {
id: saveAsDialog
anchors.centerIn: parent
onSave: {
updateGraphData();
NodeGraphEditorBackend.nodeGraphEditorModel.saveFile(fileName);
NodeGraphEditorBackend.nodeGraphEditorModel.openFileName(fileName);
}
}
SaveChangesDialog {
id: saveChangesDialog
anchors.centerIn: parent
onDiscard: {
NodeGraphEditorBackend.nodeGraphEditorModel.currentFileName = nodeGraphFileName;
NodeGraphEditorBackend.nodeGraphEditorModel.openFileName(NodeGraphEditorBackend.nodeGraphEditorModel.currentFileName);
}
onRejected: {}
onSave: {
if (NodeGraphEditorBackend.nodeGraphEditorModel.currentFileName !== "") {
/*Save current graph data to the backend*/
updateGraphData();
/*Save old data, before opening new node graph*/
NodeGraphEditorBackend.nodeGraphEditorModel.saveFile(NodeGraphEditorBackend.nodeGraphEditorModel.currentFileName);
NodeGraphEditorBackend.nodeGraphEditorModel.currentFileName = root.nodeGraphFileName;
/*Open new node graph*/
NodeGraphEditorBackend.nodeGraphEditorModel.openFileName(NodeGraphEditorBackend.nodeGraphEditorModel.currentFileName);
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = false;
} else {
saveAsDialog.open();
}
}
}
Editor.ContextMenu {
id: contextMenu
@@ -49,6 +232,47 @@ Item {
newNodeGraphDialog.open();
}
}
HelperWidgets.AbstractButton {
buttonIcon: StudioTheme.Constants.remove_medium
style: StudioTheme.Values.viewBarButtonStyle
tooltip: qsTr("Clear graph.")
onClicked: () => {
graphView.graph.clearGraph();
}
}
HelperWidgets.AbstractButton {
buttonIcon: StudioTheme.Constants.save_medium
enabled: NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges && NodeGraphEditorBackend.nodeGraphEditorModel.currentFileName != ""
style: StudioTheme.Values.viewBarButtonStyle
tooltip: qsTr("Save.")
onClicked: () => {
if (NodeGraphEditorBackend.nodeGraphEditorModel.currentFileName !== "") {
updateGraphData();
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = false;
NodeGraphEditorBackend.nodeGraphEditorModel.saveFile(NodeGraphEditorBackend.nodeGraphEditorModel.currentFileName);
} else {
saveAsDialog.open();
}
}
}
HelperWidgets.AbstractButton {
buttonIcon: StudioTheme.Constants.saveAs_medium
style: StudioTheme.Values.viewBarButtonStyle
tooltip: qsTr("Save as ...")
onClicked: () => {
saveAsDialog.open();
}
}
Label {
text: NodeGraphEditorBackend.nodeGraphEditorModel.currentFileName
}
}
}

View File

@@ -0,0 +1,106 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import EffectComposerBackend
StudioControls.Dialog {
id: root
property alias fileName: nameText.text
signal save(string fileName)
closePolicy: Popup.CloseOnEscape
implicitHeight: 160
implicitWidth: 350
modal: true
title: qsTr("Save node graph")
contentItem: Item {
Column {
spacing: 2
Row {
id: row
Text {
anchors.verticalCenter: parent.verticalCenter
color: StudioTheme.Values.themeTextColor
text: qsTr("Node graph name: ")
}
StudioControls.TextField {
id: nameText
actionIndicator.visible: false
translationIndicator.visible: false
Keys.onEnterPressed: btnSave.onClicked()
Keys.onEscapePressed: root.reject()
Keys.onReturnPressed: btnSave.onClicked()
onTextChanged: {
let errMsg = "";
if (/[^A-Za-z0-9_]+/.test(text))
errMsg = qsTr("Name contains invalid characters.");
// if (!/^[A-Z]/.test(text))
// errMsg = qsTr("Name must start with a capital letter.")
if (text.length < 1)
errMsg = qsTr("Name must have at least 1 character.");
else if (/\s/.test(text))
errMsg = qsTr("Name cannot contain white space.");
emptyText.text = errMsg;
btnSave.enabled = errMsg.length === 0;
}
}
}
Text {
id: emptyText
anchors.right: row.right
color: StudioTheme.Values.themeError
}
}
Row {
anchors.bottom: parent.bottom
anchors.right: parent.right
spacing: 2
HelperWidgets.Button {
id: btnSave
enabled: nameText.text !== ""
text: qsTr("Save")
onClicked: {
if (!enabled) // needed since this event handler can be triggered from keyboard events
return;
root.save(nameText.text);
root.accept(); // TODO: confirm before overriding node graph with same name
}
}
HelperWidgets.Button {
text: qsTr("Cancel")
onClicked: {
root.reject();
}
}
}
}
onOpened: {
nameText.text = "";
nameText.selectAll();
nameText.forceActiveFocus();
emptyText.text = "";
}
}

View File

@@ -0,0 +1,63 @@
// Copyright (C) 2023 The Qt Company Ltd.
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import QtQuick.Controls
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import StudioTheme as StudioTheme
import EffectComposerBackend
StudioControls.Dialog {
id: root
signal discard
signal save
closePolicy: Popup.CloseOnEscape
implicitHeight: 130
implicitWidth: 300
modal: true
title: qsTr("Save Changes")
contentItem: Item {
Text {
color: StudioTheme.Values.themeTextColor
text: qsTr("Current node graph has unsaved changes.")
}
HelperWidgets.Button {
anchors.bottom: parent.bottom
text: qsTr("Cancel")
width: 60
onClicked: root.reject()
}
Row {
anchors.bottom: parent.bottom
anchors.right: parent.right
spacing: 2
HelperWidgets.Button {
text: qsTr("Save")
width: 50
onClicked: {
root.save();
root.accept();
}
}
HelperWidgets.Button {
text: qsTr("Discard Changes")
width: 110
onClicked: {
root.discard();
root.accept();
}
}
}
}
}

View File

@@ -4,6 +4,7 @@
import QtQuick
import QuickQanava as Qan
import NodeGraphEditorBackend
Qan.Graph {
id: root
@@ -34,7 +35,7 @@ Qan.Graph {
});
}
}
onNodeRemoved: node => {}
onNodeRemoved: node => {NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true;}
onOnEdgeRemoved: edge => {
const srcNode = edge.getSource();
const dstNode = edge.getDestination();
@@ -43,5 +44,13 @@ Qan.Graph {
// TODO: add reset binding function
dstNode.item.value[dstPortItem.dataName] = dstNode.item.reset[dstPortItem.dataName];
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true;
}
onEdgeInserted: {
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true;
}
onNodeInserted: {
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true;
}
}

View File

@@ -11,4 +11,5 @@ Qan.Port {
property var dataBinding: null
property string dataName: ""
property string dataType: ""
property string dataId: ""
}

View File

@@ -23,7 +23,8 @@ Qan.NodeItem {
property var pin: []
property var pout: []
}
readonly property string uuid: NodeGraphEditorBackend.widget.generateUUID()
property string type
property string uuid: NodeGraphEditorBackend.widget.generateUUID()
Layout.preferredHeight: 60
Layout.preferredWidth: 100
@@ -34,6 +35,12 @@ Qan.NodeItem {
Component.onCompleted: {
internal.configurePorts(root.graph);
}
onXChanged: {
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true;
}
onYChanged: {
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true;
}
Qan.RectNodeTemplate {
anchors.fill: parent
@@ -56,6 +63,7 @@ Qan.NodeItem {
}
portItem.dataName = data.alias;
portItem.dataType = data.type;
portItem.dataId = data.id;
};
root.portsMetaData.pin.forEach(data => {

View File

@@ -24,6 +24,7 @@ Base {
Layout.preferredHeight: 150
Layout.preferredWidth: 150
type: "BaseColor"
portsMetaData: QtObject {
property var pin: [

View File

@@ -5,14 +5,17 @@ import QtQuick
import QtQuick.Layouts
import StudioControls as StudioControls
import NodeGraphEditorBackend
Base {
id: root
readonly property QtObject value: QtObject {
property bool binary: checkBox.checked
property bool binary
}
type: "CheckBox"
portsMetaData: QtObject {
property var pin: []
property var pout: [
@@ -34,5 +37,11 @@ Base {
actionIndicatorVisible: false
anchors.centerIn: parent
checked: root.value.binary
onCheckedChanged: {
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true;
root.value.binary = checked;
}
}
}

View File

@@ -3,14 +3,21 @@
import QtQuick
import QtQuick.Layouts
import NodeGraphEditorBackend
Base {
id: root
readonly property QtObject value: QtObject {
property color color
onColorChanged: {
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true;
}
}
type: "Color"
portsMetaData: QtObject {
property var pin: []
property var pout: [

View File

@@ -2,7 +2,6 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
import QtQuick
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
@@ -18,7 +17,6 @@ StudioControls.PopupDialog {
function open(showItem) {
loader.ensureActive();
colorPopup.show(showItem);
loader.updateOriginalColor();
}

View File

@@ -6,15 +6,21 @@ import QtQuick.Layouts
import HelperWidgets as HelperWidgets
import StudioControls as StudioControls
import NodeGraphEditorBackend
Base {
id: root
property QtObject value: QtObject {
property url text: `image://qmldesigner_nodegrapheditor/${comboBox.currentValue}`
onTextChanged: {
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true;
}
}
Layout.preferredWidth: 175
type: "ComboBox"
portsMetaData: QtObject {
property var pin: []

View File

@@ -19,6 +19,7 @@ Base {
Layout.preferredHeight: 150
Layout.preferredWidth: 150
type: "Material"
portsMetaData: QtObject {
property var pin: [

View File

@@ -22,6 +22,7 @@ Base {
Layout.preferredHeight: 150
Layout.preferredWidth: 150
type: "Metalness"
portsMetaData: QtObject {
property var pin: [

View File

@@ -5,15 +5,17 @@ import QtQuick
import QtQuick.Layouts
import StudioControls as StudioControls
import NodeGraphEditorBackend
Base {
id: root
readonly property QtObject value: QtObject {
property real floating: realSpinBox.realValue
property real floating
}
Layout.preferredWidth: 175
type: "RealSpinBox"
portsMetaData: QtObject {
property var pin: []
@@ -37,5 +39,11 @@ Base {
actionIndicatorVisible: false
anchors.centerIn: parent
decimals: 2
realValue: root.value.floating
onRealValueChanged: {
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true;
root.value.floating = realValue;
}
}
}

View File

@@ -22,6 +22,7 @@ Base {
Layout.preferredHeight: 150
Layout.preferredWidth: 150
type: "Roughness"
portsMetaData: QtObject {
property var pin: [

View File

@@ -7,6 +7,7 @@ import QtQuick3D as QtQuick3D
import QuickQanava as Qan
import NodeGraphEditorBackend
import NodeGraphEditorBackend
Base {
@@ -19,10 +20,14 @@ Base {
Layout.preferredHeight: 150
Layout.preferredWidth: 150
type: "Texture"
Component.onCompleted: {
node.label = "Texture";
}
onValueChanged: {
NodeGraphEditorBackend.nodeGraphEditorModel.hasUnsavedChanges = true;
}
Image {
anchors.centerIn: parent

View File

@@ -33,7 +33,6 @@ option(ENABLE_METAINFO_TRACING "Enable meta info tracing" ${ENV_QTC_ENABLE_METAI
add_feature_info("Meta info tracing" ${ENABLE_METAINFO_TRACING} "")
add_subdirectory(libs)
add_qtc_plugin(QmlDesigner
PLUGIN_RECOMMENDS QmlPreview
CONDITION TARGET QmlDesignerCore AND TARGET Qt::QuickWidgets AND TARGET Qt::Svg

View File

@@ -124,7 +124,7 @@ QPair<QPixmap, qint64> AssetsLibraryIconProvider::fetchPixmap(const QString &id,
type = "sound";
else if (asset.isVideo())
type = "video";
else if (asset.isEffect())
else if (asset.isEffect() || asset.isNodeGraph())
type = QmlDesigner::ModelNodeOperations::getEffectIcon(id);
QString pathTemplate = QString(":/AssetsLibrary/images/asset_%1%2.png").arg(type);

View File

@@ -612,6 +612,11 @@ QSet<QString> AssetsLibraryWidget::supportedAssetSuffixes(bool complex)
return suffixes;
}
void AssetsLibraryWidget::openNodeGraphEditor(const QString &filePath)
{
ModelNodeOperations::openNodeGraphEditor(filePath);
}
void AssetsLibraryWidget::openEffectComposer(const QString &filePath)
{
ModelNodeOperations::openEffectComposer(filePath);
@@ -695,6 +700,9 @@ QPair<QString, QByteArray> AssetsLibraryWidget::getAssetTypeAndData(const QStrin
} else if (asset.isEffect()) {
// Data: Effect Composer format (suffix)
return {Constants::MIME_TYPE_ASSET_EFFECT, asset.suffix().toUtf8()};
} else if (asset.isNodeGraph()) {
// Data: Node Grpah Editor format (suffix)
return {Constants::MIME_TYPE_ASSET_NODEGRAPH, asset.suffix().toUtf8()};
}
}
return {};

View File

@@ -85,6 +85,7 @@ public:
const QString &targetDirPath = {});
Q_INVOKABLE QSet<QString> supportedAssetSuffixes(bool complex);
Q_INVOKABLE void openEffectComposer(const QString &filePath);
Q_INVOKABLE void openNodeGraphEditor(const QString &filePath);
Q_INVOKABLE int qtVersion() const;
Q_INVOKABLE void invalidateThumbnail(const QString &id);
Q_INVOKABLE QSize imageSize(const QString &id);

View File

@@ -2213,6 +2213,13 @@ void openEffectComposer(const QString &filePath)
}
}
void openNodeGraphEditor(const QString &filePath)
{
QmlDesignerPlugin::instance()->mainWidget()->showDockWidget("NodeGraphEditor", true);
QmlDesignerPlugin::instance()->viewManager()
.emitCustomNotification("open_nodegrapheditor_graph", {}, {filePath});
}
void openOldEffectMaker(const QString &filePath)
{
const ProjectExplorer::Target *target = ProjectExplorer::ProjectTree::currentTarget();
@@ -2282,6 +2289,7 @@ QString getEffectsDefaultDirectory(const QString &defaultDir)
return getAssetDefaultDirectory("effects", defaultDir);
}
QString getEffectIcon(const QString &effectPath)
{
Utils::FilePath effectFile = QmlDesignerPlugin::instance()->documentManager()

View File

@@ -212,6 +212,7 @@ void editInEffectComposer(const SelectionContext &selectionContext);
QMLDESIGNERCOMPONENTS_EXPORT Utils::FilePath getEffectsImportDirectory();
QMLDESIGNERCOMPONENTS_EXPORT QString getEffectsDefaultDirectory(const QString &defaultDir = {});
void openEffectComposer(const QString &filePath);
void openNodeGraphEditor(const QString &filePath);
void openOldEffectMaker(const QString &filePath);
QString getEffectIcon(const QString &effectPath);
bool useLayerEffect();

View File

@@ -5,6 +5,9 @@
#include "nodegrapheditorview.h"
#include "nodegrapheditorview.h"
#include <qmldesignerplugin.h>
#include <QFileInfo>
namespace QmlDesigner {
NodeGraphEditorModel::NodeGraphEditorModel(NodeGraphEditorView *nodeGraphEditorView)
@@ -13,4 +16,113 @@ NodeGraphEditorModel::NodeGraphEditorModel(NodeGraphEditorView *nodeGraphEditorV
{
}
void NodeGraphEditorModel::saveFile(QString fileName){
auto directory = saveDirectory();
auto saveFile = QFile(directory + '/' + fileName + ".qng");
if (!saveFile.open(QIODevice::WriteOnly)) {
QString error = QString("Error: Couldn't save node graph file");
qCritical() << error;
return;
}
auto str = m_graphData.toStdString();
saveFile.write(str.c_str());
saveFile.close();
}
void NodeGraphEditorModel::openFile(QString filePath){
const QString nodeGraphName = QFileInfo(filePath).baseName();
setCurrentFileName(nodeGraphName);
QFile nodeGraphFile(filePath);
if (!nodeGraphFile.open(QIODevice::ReadOnly)) {
QString error = QString("Couldn't open node graph file: '%1'").arg(filePath);
qWarning() << qPrintable(error);
return;
}
QByteArray data = nodeGraphFile.readAll();
m_graphData = QString::fromStdString(data.toStdString());
graphDataChanged();
nodeGraphLoaded();
nodeGraphFile.close();
if (data.isEmpty())
return;
}
QString NodeGraphEditorModel::saveDirectory(){
if (m_saveDirectory!="") {
return m_saveDirectory;
} else {
const QString assetDir = "nodegraphs";
QString targetDir = QmlDesignerPlugin::instance()->documentManager().currentProjectDirPath().toString();
QString adjustedDefaultDirectory = targetDir;
Utils::FilePath contentPath = QmlDesignerPlugin::instance()->documentManager().currentResourcePath();
Utils::FilePath assetPath = contentPath.pathAppended(assetDir);
if (!assetPath.exists())
assetPath.createDir();
if (assetPath.exists() && assetPath.isDir())
adjustedDefaultDirectory = assetPath.toString();
m_saveDirectory = adjustedDefaultDirectory;
}
return m_saveDirectory;
}
void NodeGraphEditorModel::openFileName(QString fileName){
auto directory = saveDirectory();
const QString nodeGraphName = QFileInfo(fileName).baseName();
setCurrentFileName(nodeGraphName);
QFile nodeGraphFile(directory + "/" + fileName + ".qng");
if (!nodeGraphFile.open(QIODevice::ReadOnly)) {
QString error = QString("Couldn't open node graph file: '%1'").arg(nodeGraphFile.fileName());
qWarning() << qPrintable(error);
return;
}
QByteArray data = nodeGraphFile.readAll();
m_graphData = QString::fromStdString(data.toStdString());
graphDataChanged();
nodeGraphLoaded();
nodeGraphFile.close();
if (data.isEmpty())
return;
}
bool NodeGraphEditorModel::hasUnsavedChanges() const
{
return m_hasUnsavedChanges;
}
void NodeGraphEditorModel::setHasUnsavedChanges(bool val)
{
if (m_hasUnsavedChanges == val)
return;
m_hasUnsavedChanges = val;
emit hasUnsavedChangesChanged();
}
QString NodeGraphEditorModel::currentFileName() const{
return m_currentFileName;
}
void NodeGraphEditorModel::setCurrentFileName(const QString &newCurrentFileName){
if (m_currentFileName == newCurrentFileName)
return;
m_currentFileName = newCurrentFileName;
emit currentFileNameChanged();
}
void NodeGraphEditorModel::setGraphData(QString val){
if (m_graphData == val)
return;
m_graphData = val;
emit graphDataChanged();
};
QString NodeGraphEditorModel::graphData(){return m_graphData;}
} // namespace QmlDesigner

View File

@@ -16,9 +16,34 @@ class NodeGraphEditorModel : public QStandardItemModel
public:
NodeGraphEditorModel(NodeGraphEditorView *nodeGraphEditorView);
Q_INVOKABLE void openFile(QString filePath);
Q_INVOKABLE void openFileName(QString filePath);
Q_INVOKABLE void saveFile(QString fileName);
private:
QPointer<NodeGraphEditorView> m_editorView;
Q_PROPERTY(bool hasUnsavedChanges MEMBER m_hasUnsavedChanges WRITE setHasUnsavedChanges NOTIFY hasUnsavedChangesChanged)
Q_PROPERTY(QString currentFileName READ currentFileName WRITE setCurrentFileName NOTIFY currentFileNameChanged)
Q_PROPERTY(QString graphData READ graphData WRITE setGraphData NOTIFY graphDataChanged)
signals:
void graphDataChanged();
void nodeGraphLoaded();
void hasUnsavedChangesChanged();
void currentFileNameChanged();
public:
void setGraphData(QString val);
QString graphData();
bool hasUnsavedChanges() const;
void setHasUnsavedChanges(bool val);
QString currentFileName() const;
void setCurrentFileName(const QString &newCurrentFileName);
private:
QString saveDirectory();
QString m_graphData{""};
QString m_currentFileName{""};
QString m_saveDirectory{""};
bool m_hasUnsavedChanges{false};
};
} // namespace QmlDesigner

View File

@@ -2,7 +2,6 @@
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0+ OR GPL-3.0 WITH Qt-GPL-exception-1.0
#include "nodegrapheditorview.h"
#include "nodegrapheditormodel.h"
#include "nodegrapheditorwidget.h"
@@ -31,4 +30,16 @@ WidgetInfo NodeGraphEditorView::widgetInfo()
tr("Node Graph"));
}
void NodeGraphEditorView::customNotification([[maybe_unused]] const AbstractView *view,
const QString &identifier,
[[maybe_unused]] const QList<QmlDesigner::ModelNode> &nodeList,
const QList<QVariant> &data)
{
if (data.size() < 1)
return;
if (identifier == "open_nodegrapheditor_graph") {
const QString nodeGraphPath = data[0].toString();
m_editorWidget->openNodeGraph(nodeGraphPath);
}
}
} // namespace QmlDesigner

View File

@@ -21,8 +21,11 @@ public:
WidgetInfo widgetInfo() override;
private:
void customNotification(const AbstractView *view, const QString &identifier,
const QList<QmlDesigner::ModelNode> &nodeList, const QList<QVariant> &data) override;
QPointer<NodeGraphEditorModel> m_editorModel;
QPointer<NodeGraphEditorWidget> m_editorWidget;
};
} // namespace QmlDesigner

View File

@@ -41,9 +41,28 @@ static Utils::FilePath materialsPath()
return DocumentManager::currentResourcePath().pathAppended("materials");
}
void NodeGraphEditorWidget::doOpenNodeGraph(){
m_model->openFile(m_filePath);
}
void NodeGraphEditorWidget::openNodeGraph(const QString &path)
{
m_filePath = path;
if (m_model->hasUnsavedChanges()){
/*Pass new path to update if saved*/
auto newFile = QFileInfo(path).baseName();
QMetaObject::invokeMethod(quickWidget()->rootObject(), "promptToSaveBeforeOpen",Q_ARG(QVariant, newFile));
}
else{
doOpenNodeGraph();
}
}
NodeGraphEditorWidget::NodeGraphEditorWidget(NodeGraphEditorView *nodeGraphEditorView,
NodeGraphEditorModel *nodeGraphEditorModel)
: m_editorView(nodeGraphEditorView)
, m_model(nodeGraphEditorModel)
, m_imageProvider(nullptr)
, m_qmlSourceUpdateShortcut(nullptr)
{
@@ -64,6 +83,7 @@ NodeGraphEditorWidget::NodeGraphEditorWidget(NodeGraphEditorView *nodeGraphEdito
auto map = registerPropertyMap("NodeGraphEditorBackend");
map->setProperties({{"nodeGraphEditorModel", QVariant::fromValue(nodeGraphEditorModel)}});
map->setProperties({{"widget", QVariant::fromValue(this)}});
connect(m_qmlSourceUpdateShortcut, &QShortcut::activated, this, &NodeGraphEditorWidget::reloadQmlSource);
Theme::setupTheme(engine());

View File

@@ -30,9 +30,11 @@ public:
NodeGraphEditorWidget(NodeGraphEditorView *nodeGraphEditorView, NodeGraphEditorModel *nodeGraphEditorModel);
~NodeGraphEditorWidget() override = default;
static QString qmlSourcesPath();
Q_INVOKABLE QString generateUUID() const;
static QString qmlSourcesPath();
Q_INVOKABLE void doOpenNodeGraph();
void openNodeGraph(const QString &path);
protected:
void showEvent(QShowEvent *) override;
@@ -41,12 +43,13 @@ protected:
private:
void reloadQmlSource();
private:
QPointer<NodeGraphEditorView> m_editorView;
QPointer<NodeGraphEditorModel> m_model;
Internal::NodeGraphEditorImageProvider *m_imageProvider;
QShortcut *m_qmlSourceUpdateShortcut;
QElapsedTimer m_usageTimer;
QString m_filePath;
};
} // namespace QmlDesigner

View File

@@ -85,6 +85,13 @@ const QStringList &Asset::supportedEffectComposerSuffixes()
return retList;
}
const QStringList &Asset::supportedNodeGraphSuffixes()
{
// These are file types only supported by Node Graph Editor
static QStringList retList {"*.qng"};
return retList;
}
const QStringList &Asset::supportedMaterialSuffixes()
{
static QStringList retList {"*.mat"};
@@ -112,6 +119,7 @@ const QSet<QString> &Asset::supportedSuffixes()
insertSuffixes(supportedVideoSuffixes());
insertSuffixes(supportedTexture3DSuffixes());
insertSuffixes(supportedEffectComposerSuffixes());
insertSuffixes(supportedNodeGraphSuffixes());
insertSuffixes(supportedMaterialSuffixes());
insertSuffixes(supportedImported3dSuffixes());
}
@@ -196,6 +204,10 @@ bool Asset::isEffect() const
return m_type == Asset::Type::Effect;
}
bool Asset::isNodeGraph() const
{
return m_type == Asset::Type::NodeGraph;
}
bool Asset::isMaterial() const
{
return m_type == Asset::Type::Material;
@@ -252,6 +264,8 @@ void Asset::resolveType()
m_type = Asset::Type::Texture3D;
else if (supportedEffectComposerSuffixes().contains(m_suffix))
m_type = Asset::Type::Effect;
else if (supportedNodeGraphSuffixes().contains(m_suffix))
m_type = Asset::Type::NodeGraph;
else if (supportedMaterialSuffixes().contains(m_suffix))
m_type = Asset::Type::Material;
else if (supportedImported3dSuffixes().contains(m_suffix))

View File

@@ -26,6 +26,7 @@ public:
Video,
Texture3D,
Effect,
NodeGraph,
Material,
Imported3D
};
@@ -41,6 +42,7 @@ public:
static const QStringList &supportedVideoSuffixes();
static const QStringList &supportedTexture3DSuffixes();
static const QStringList &supportedEffectComposerSuffixes();
static const QStringList &supportedNodeGraphSuffixes();
static const QStringList &supportedMaterialSuffixes();
static const QStringList &supportedImported3dSuffixes();
static const QSet<QString> &supportedSuffixes();
@@ -64,6 +66,7 @@ public:
bool isHdrFile() const;
bool isKtxFile() const;
bool isEffect() const;
bool isNodeGraph() const;
bool isMaterial() const;
bool isImported3D() const;
bool isSupported() const;

View File

@@ -88,6 +88,7 @@ inline constexpr char MIME_TYPE_ASSET_TEXTURE3D[]
= "application/vnd.qtdesignstudio.asset.texture3d";
inline constexpr char MIME_TYPE_MODELNODE_LIST[] = "application/vnd.qtdesignstudio.modelnode.list";
inline constexpr char MIME_TYPE_ASSET_EFFECT[] = "application/vnd.qtdesignstudio.asset.effect";
inline constexpr char MIME_TYPE_ASSET_NODEGRAPH[] = "application/vnd.qtdesignstudio.asset.nodegraph";
// Menus
inline constexpr char M_VIEW_WORKSPACES[] = "QmlDesigner.Menu.View.Workspaces";