diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index 33da9d3..10d780a 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -5,14 +5,22 @@ If you haven't already, join our [Discord channel](https://discord.gg/qgPrHv4)!
## Pull Requests
Try to keep your pull requests relevant, neat, and manageable. If you are adding multiple features, split them into separate PRs.
-* Avoid including irellevant whitespace or formatting changes.
-* Comment your code.
-* Spell check your code / comments
-* Use consistent formatting
+These are the main points to follow:
+
+1) Use formatting which is consistent with the rest of xNode base (see below)
+2) Keep _one feature_ per PR (see below)
+3) xNode aims to be compatible with C# 4.x, do not use new language features
+4) Avoid including irellevant whitespace or formatting changes
+5) Comment your code
+6) Spell check your code / comments
+7) Use concrete types, not *var*
+8) Use english language
## New features
xNode aims to be simple and extendible, not trying to fix all of Unity's shortcomings.
+Approved changes might be rejected if bundled with rejected changes, so keep PRs as separate as possible.
+
If your feature aims to cover something not related to editing nodes, it generally won't be accepted. If in doubt, ask on the Discord channel.
## Coding conventions
diff --git a/Scripts/Editor/GraphAndNodeEditor.cs b/Scripts/Editor/GraphAndNodeEditor.cs
new file mode 100644
index 0000000..c13f782
--- /dev/null
+++ b/Scripts/Editor/GraphAndNodeEditor.cs
@@ -0,0 +1,73 @@
+using UnityEditor;
+using UnityEngine;
+#if ODIN_INSPECTOR
+using Sirenix.OdinInspector.Editor;
+using Sirenix.Utilities;
+using Sirenix.Utilities.Editor;
+#endif
+
+namespace XNodeEditor {
+ /// Override graph inspector to show an 'Open Graph' button at the top
+ [CustomEditor(typeof(XNode.NodeGraph), true)]
+#if ODIN_INSPECTOR
+ public class GlobalGraphEditor : OdinEditor {
+ public override void OnInspectorGUI() {
+ if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
+ NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph);
+ }
+ base.OnInspectorGUI();
+ }
+ }
+#else
+ public class GlobalGraphEditor : Editor {
+ public override void OnInspectorGUI() {
+ serializedObject.Update();
+
+ if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
+ NodeEditorWindow.Open(serializedObject.targetObject as XNode.NodeGraph);
+ }
+
+ GUILayout.Space(EditorGUIUtility.singleLineHeight);
+ GUILayout.Label("Raw data", "BoldLabel");
+
+ DrawDefaultInspector();
+
+ serializedObject.ApplyModifiedProperties();
+ }
+ }
+#endif
+
+ [CustomEditor(typeof(XNode.Node), true)]
+#if ODIN_INSPECTOR
+ public class GlobalNodeEditor : OdinEditor {
+ public override void OnInspectorGUI() {
+ if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
+ SerializedProperty graphProp = serializedObject.FindProperty("graph");
+ NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
+ w.Home(); // Focus selected node
+ }
+ base.OnInspectorGUI();
+ }
+ }
+#else
+ public class GlobalNodeEditor : Editor {
+ public override void OnInspectorGUI() {
+ serializedObject.Update();
+
+ if (GUILayout.Button("Edit graph", GUILayout.Height(40))) {
+ SerializedProperty graphProp = serializedObject.FindProperty("graph");
+ NodeEditorWindow w = NodeEditorWindow.Open(graphProp.objectReferenceValue as XNode.NodeGraph);
+ w.Home(); // Focus selected node
+ }
+
+ GUILayout.Space(EditorGUIUtility.singleLineHeight);
+ GUILayout.Label("Raw data", "BoldLabel");
+
+ // Now draw the node itself.
+ DrawDefaultInspector();
+
+ serializedObject.ApplyModifiedProperties();
+ }
+ }
+#endif
+}
\ No newline at end of file
diff --git a/Scripts/Editor/GraphAndNodeEditor.cs.meta b/Scripts/Editor/GraphAndNodeEditor.cs.meta
new file mode 100644
index 0000000..5cc60df
--- /dev/null
+++ b/Scripts/Editor/GraphAndNodeEditor.cs.meta
@@ -0,0 +1,11 @@
+fileFormatVersion: 2
+guid: bdd6e443125ccac4dad0665515759637
+MonoImporter:
+ externalObjects: {}
+ serializedVersion: 2
+ defaultReferences: []
+ executionOrder: 0
+ icon: {instanceID: 0}
+ userData:
+ assetBundleName:
+ assetBundleVariant:
diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs
index eae80cd..edf66d6 100644
--- a/Scripts/Editor/NodeEditor.cs
+++ b/Scripts/Editor/NodeEditor.cs
@@ -21,7 +21,7 @@ namespace XNodeEditor {
public readonly static Dictionary portPositions = new Dictionary();
#if ODIN_INSPECTOR
- internal static bool inNodeEditor = false;
+ protected internal static bool inNodeEditor = false;
#endif
public virtual void OnHeaderGUI() {
@@ -129,9 +129,14 @@ namespace XNodeEditor {
public void Rename(string newName) {
if (newName == null || newName.Trim() == "") newName = NodeEditorUtilities.NodeDefaultName(target.GetType());
target.name = newName;
+ OnRename();
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
}
-
+
+ /// Called after this node's name has changed.
+ public virtual void OnRename() { }
+
+
[AttributeUsage(AttributeTargets.Class)]
public class CustomNodeEditorAttribute : Attribute,
XNodeEditor.Internal.NodeEditorBase.INodeEditorAttrib {
diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs
index 2581676..8021a16 100644
--- a/Scripts/Editor/NodeEditorAction.cs
+++ b/Scripts/Editor/NodeEditorAction.cs
@@ -58,10 +58,9 @@ namespace XNodeEditor {
case EventType.MouseDrag:
if (e.button == 0) {
if (IsDraggingPort) {
- if (IsHoveringPort && hoveredPort.IsInput && draggedOutput.CanConnectTo(hoveredPort)) {
- if (!draggedOutput.IsConnectedTo(hoveredPort)) {
- draggedOutputTarget = hoveredPort;
- }
+ // Set target even if we can't connect, so as to prevent auto-conn menu from opening erroneously
+ if (IsHoveringPort && hoveredPort.IsInput && !draggedOutput.IsConnectedTo(hoveredPort)) {
+ draggedOutputTarget = hoveredPort;
} else {
draggedOutputTarget = null;
}
@@ -205,8 +204,8 @@ namespace XNodeEditor {
if (e.button == 0) {
//Port drag release
if (IsDraggingPort) {
- //If connection is valid, save it
- if (draggedOutputTarget != null) {
+ // If connection is valid, save it
+ if (draggedOutputTarget != null && draggedOutput.CanConnectTo(draggedOutputTarget)) {
XNode.Node node = draggedOutputTarget.node;
if (graph.nodes.Count != 0) draggedOutput.Connect(draggedOutputTarget);
@@ -218,8 +217,8 @@ namespace XNodeEditor {
EditorUtility.SetDirty(graph);
}
}
- // Open context menu for auto-connection
- else if (NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) {
+ // Open context menu for auto-connection if there is no target node
+ else if (draggedOutputTarget == null && NodeEditorPreferences.GetSettings().dragToCreate && autoConnectOutput != null) {
GenericMenu menu = new GenericMenu();
graphEditor.AddContextMenuItems(menu);
menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero));
@@ -457,8 +456,8 @@ namespace XNodeEditor {
XNode.Node newNodeIn, newNodeOut;
if (substitutes.TryGetValue(inputPort.node, out newNodeIn) && substitutes.TryGetValue(outputPort.node, out newNodeOut)) {
- newNodeIn.UpdateStaticPorts();
- newNodeOut.UpdateStaticPorts();
+ newNodeIn.UpdatePorts();
+ newNodeOut.UpdatePorts();
inputPort = newNodeIn.GetInputPort(inputPort.fieldName);
outputPort = newNodeOut.GetOutputPort(outputPort.fieldName);
}
diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs
index f9333db..0b8a0cd 100644
--- a/Scripts/Editor/NodeEditorGUILayout.cs
+++ b/Scripts/Editor/NodeEditorGUILayout.cs
@@ -312,6 +312,8 @@ namespace XNodeEditor {
}).Where(x => x.port != null);
List dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
+ node.UpdatePorts();
+
ReorderableList list = null;
Dictionary rlc;
if (reorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) {
@@ -326,6 +328,7 @@ namespace XNodeEditor {
}
list.list = dynamicPorts;
list.DoLayoutList();
+
}
private static ReorderableList CreateReorderableList(string fieldName, List dynamicPorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action onCreation) {
@@ -337,7 +340,7 @@ namespace XNodeEditor {
list.drawElementCallback =
(Rect rect, int index, bool isActive, bool isFocused) => {
XNode.NodePort port = node.GetPort(fieldName + " " + index);
- if (hasArrayData) {
+ if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) {
if (arrayData.arraySize <= index) {
EditorGUI.LabelField(rect, "Array[" + index + "] data out of range");
return;
@@ -465,7 +468,7 @@ namespace XNodeEditor {
EditorUtility.SetDirty(node);
}
- if (hasArrayData) {
+ if (hasArrayData && arrayData.propertyType != SerializedPropertyType.String) {
if (arrayData.arraySize <= index) {
Debug.LogWarning("Attempted to remove array index " + index + " where only " + arrayData.arraySize + " exist - Skipped");
Debug.Log(rl.list[0]);
diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs
index a063410..1f64653 100644
--- a/Scripts/Editor/NodeEditorWindow.cs
+++ b/Scripts/Editor/NodeEditorWindow.cs
@@ -187,12 +187,13 @@ namespace XNodeEditor {
}
/// Open the provided graph in the NodeEditor
- public static void Open(XNode.NodeGraph graph) {
- if (!graph) return;
+ public static NodeEditorWindow Open(XNode.NodeGraph graph) {
+ if (!graph) return null;
NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow;
w.wantsMouseMove = true;
w.graph = graph;
+ return w;
}
/// Repaint all open NodeEditorWindows.
diff --git a/Scripts/Editor/RenamePopup.cs b/Scripts/Editor/RenamePopup.cs
index 564374e..f245d4e 100644
--- a/Scripts/Editor/RenamePopup.cs
+++ b/Scripts/Editor/RenamePopup.cs
@@ -49,6 +49,7 @@ namespace XNodeEditor {
if (input == null || input.Trim() == "") {
if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) {
target.name = NodeEditorUtilities.NodeDefaultName(target.GetType());
+ NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename();
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
Close();
target.TriggerOnValidate();
@@ -58,6 +59,7 @@ namespace XNodeEditor {
else {
if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) {
target.name = input;
+ NodeEditor.GetEditor((XNode.Node)target, NodeEditorWindow.current).OnRename();
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target));
Close();
target.TriggerOnValidate();
diff --git a/Scripts/Node.cs b/Scripts/Node.cs
index a07679a..4103d67 100644
--- a/Scripts/Node.cs
+++ b/Scripts/Node.cs
@@ -119,12 +119,12 @@ namespace XNode {
protected void OnEnable() {
if (graphHotfix != null) graph = graphHotfix;
graphHotfix = null;
- UpdateStaticPorts();
+ UpdatePorts();
Init();
}
- /// Update static ports to reflect class fields. This happens automatically on enable.
- public void UpdateStaticPorts() {
+ /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields. This happens automatically on enable or on redrawing a dynamic port list.
+ public void UpdatePorts() {
NodeDataCache.UpdatePorts(this, ports);
}
@@ -262,7 +262,7 @@ namespace XNode {
#region Attributes
/// Mark a serializable field as an input port. You can access this through
- [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
+ [AttributeUsage(AttributeTargets.Field)]
public class InputAttribute : Attribute {
public ShowBackingValue backingValue;
public ConnectionType connectionType;
@@ -285,7 +285,7 @@ namespace XNode {
}
/// Mark a serializable field as an output port. You can access this through
- [AttributeUsage(AttributeTargets.Field, AllowMultiple = true)]
+ [AttributeUsage(AttributeTargets.Field)]
public class OutputAttribute : Attribute {
public ShowBackingValue backingValue;
public ConnectionType connectionType;
diff --git a/Scripts/NodeDataCache.cs b/Scripts/NodeDataCache.cs
index 02e35a1..f66c856 100644
--- a/Scripts/NodeDataCache.cs
+++ b/Scripts/NodeDataCache.cs
@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEngine;
@@ -9,7 +10,7 @@ namespace XNode {
private static PortDataCache portDataCache;
private static bool Initialized { get { return portDataCache != null; } }
- /// Update static ports to reflect class fields.
+ /// Update static ports and dynamic ports managed by DynamicPortLists to reflect class fields.
public static void UpdatePorts(Node node, Dictionary ports) {
if (!Initialized) BuildCache();
@@ -17,6 +18,8 @@ namespace XNode {
Dictionary> removedPorts = new Dictionary>();
System.Type nodeType = node.GetType();
+ List dynamicListPorts = new List();
+
List typePortCache;
if (portDataCache.TryGetValue(nodeType, out typePortCache)) {
for (int i = 0; i < typePortCache.Count; i++) {
@@ -25,6 +28,7 @@ namespace XNode {
}
// Cleanup port dict - Remove nonexisting static ports - update static port types
+ // AND update dynamic ports (albeit only those in lists) too, in order to enforce proper serialisation.
// Loop through current node ports
foreach (NodePort port in ports.Values.ToList()) {
// If port still exists, check it it has been changed
@@ -43,6 +47,10 @@ namespace XNode {
port.ClearConnections();
ports.Remove(port.fieldName);
}
+ // If the port is dynamic and is managed by a dynamic port list, flag it for reference updates
+ else if (IsDynamicListPort(port)) {
+ dynamicListPorts.Add(port);
+ }
}
// Add missing ports
foreach (NodePort staticPort in staticPorts.Values) {
@@ -60,8 +68,58 @@ namespace XNode {
ports.Add(staticPort.fieldName, port);
}
}
+
+ // Finally, make sure dynamic list port settings correspond to the settings of their "backing port"
+ foreach (NodePort listPort in dynamicListPorts) {
+ // At this point we know that ports here are dynamic list ports
+ // which have passed name/"backing port" checks, ergo we can proceed more safely.
+ string backingPortName = listPort.fieldName.Split(' ')[0];
+ NodePort backingPort = staticPorts[backingPortName];
+
+ // Update port constraints. Creating a new port instead will break the editor, mandating the need for setters.
+ listPort.ValueType = GetBackingValueType(backingPort.ValueType);
+ listPort.direction = backingPort.direction;
+ listPort.connectionType = backingPort.connectionType;
+ listPort.typeConstraint = backingPort.typeConstraint;
+ }
}
+ ///
+ /// Extracts the underlying types from arrays and lists, the only serialisable collections which will have
+ /// their elements drawn otherwise. If the given type is not applicable (i.e. if the dynamic list port was not
+ /// defined as an array or a list), returns the given type itself.
+ ///
+ private static Type GetBackingValueType(Type portValType) {
+ if (portValType.HasElementType) {
+ return portValType.GetElementType();
+ }
+ if (portValType.IsGenericType && portValType.GetGenericTypeDefinition() == typeof(List<>)) {
+ return portValType.GetGenericArguments()[0];
+ }
+ return portValType;
+ }
+
+
+ /// Returns true if the given port is in a dynamic port list.
+ private static bool IsDynamicListPort(NodePort port) {
+ // Ports flagged as "dynamicPortList = true" end up having a "backing port" and a name with an index, but we have
+ // no guarantee that a dynamic port called "output 0" is an element in a list backed by a static "output" port.
+ // Thus, we need to check for attributes... (but at least we don't need to look at all fields this time)
+ string[] fieldNameParts = port.fieldName.Split(' ');
+ if (fieldNameParts.Length != 2) return false;
+
+ FieldInfo backingPortInfo = port.node.GetType().GetField(fieldNameParts[0]);
+ if (backingPortInfo == null) return false;
+
+ object[] attribs = backingPortInfo.GetCustomAttributes(true);
+ return attribs.Any(x => {
+ Node.InputAttribute inputAttribute = x as Node.InputAttribute;
+ Node.OutputAttribute outputAttribute = x as Node.OutputAttribute;
+ return inputAttribute != null && inputAttribute.dynamicPortList ||
+ outputAttribute != null && outputAttribute.dynamicPortList;
+ });
+ }
+
/// Cache node types
private static void BuildCache() {
portDataCache = new PortDataCache();
diff --git a/Scripts/NodePort.cs b/Scripts/NodePort.cs
index 58a3bd6..b2f1ad1 100644
--- a/Scripts/NodePort.cs
+++ b/Scripts/NodePort.cs
@@ -19,9 +19,18 @@ namespace XNode {
}
}
- public IO direction { get { return _direction; } }
- public Node.ConnectionType connectionType { get { return _connectionType; } }
- public Node.TypeConstraint typeConstraint { get { return _typeConstraint; } }
+ public IO direction {
+ get { return _direction; }
+ internal set { _direction = value; }
+ }
+ public Node.ConnectionType connectionType {
+ get { return _connectionType; }
+ internal set { _connectionType = value; }
+ }
+ public Node.TypeConstraint typeConstraint {
+ get { return _typeConstraint; }
+ internal set { _typeConstraint = value; }
+ }
/// Is this port connected to anytihng?
public bool IsConnected { get { return connections.Count != 0; } }