diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs
index f720cfe..ca3a87f 100644
--- a/Scripts/Editor/NodeEditorGUILayout.cs
+++ b/Scripts/Editor/NodeEditorGUILayout.cs
@@ -6,6 +6,7 @@ using System.Reflection;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
+using XNode;
namespace XNodeEditor {
/// xNode-specific version of
@@ -44,9 +45,22 @@ namespace XNodeEditor {
Rect rect = new Rect();
float spacePadding = 0;
- SpaceAttribute spaceAttribute;
- if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out spaceAttribute)) spacePadding = spaceAttribute.height;
+ if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out SpaceAttribute spaceAttribute))
+ spacePadding = spaceAttribute.height;
+ bool dplUseConnectionNames = false;
+ bool dplShowEntryIndexes = false;
+ string dplDefaultEntryName = null;
+ string dplEntryFormat = null;
+ //Get the dynamic list settings attribute if any
+ if (NodeEditorUtilities.GetCachedAttrib(port.node.GetType(), property.name, out Node.DynamicPortListSettingsAttribute dplSettingsAttribute))
+ {
+ dplUseConnectionNames = dplSettingsAttribute.useConnectionNames;
+ dplShowEntryIndexes = dplSettingsAttribute.showEntryIndexes;
+ dplDefaultEntryName = dplSettingsAttribute.defaultEntryName;
+ dplEntryFormat = dplSettingsAttribute.entryFormat;
+ }
+
// If property is an input, display a regular property field and put a port handle on the left side
if (port.direction == XNode.NodePort.IO.Input) {
// Get data from [Input] attribute
@@ -69,20 +83,23 @@ namespace XNodeEditor {
if (dynamicPortList) {
Type type = GetType(property);
- XNode.Node.ConnectionType connectionType = inputAttribute != null ? inputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
- DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType);
+ XNode.Node.ConnectionType connectionType = inputAttribute != null ?
+ inputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
+ Node.TypeConstraint typeConstraint = inputAttribute.typeConstraint;
+ DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType, typeConstraint,
+ dplUseConnectionNames, dplShowEntryIndexes, dplDefaultEntryName, dplEntryFormat);
return;
}
switch (showBacking) {
case XNode.Node.ShowBackingValue.Unconnected:
// Display a label if port is connected
- if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName));
+ if (port.IsConnected) EditorGUILayout.LabelField(label ?? new GUIContent(property.displayName));
// Display an editable property field if port is not connected
else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
break;
case XNode.Node.ShowBackingValue.Never:
// Display a label
- EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName));
+ EditorGUILayout.LabelField(label ?? new GUIContent(property.displayName));
break;
case XNode.Node.ShowBackingValue.Always:
// Display an editable property field
@@ -115,28 +132,34 @@ namespace XNodeEditor {
if (dynamicPortList) {
Type type = GetType(property);
XNode.Node.ConnectionType connectionType = outputAttribute != null ? outputAttribute.connectionType : XNode.Node.ConnectionType.Multiple;
- DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType);
+ Node.TypeConstraint typeConstraint = outputAttribute.typeConstraint;
+ DynamicPortList(property.name, type, property.serializedObject, port.direction, connectionType, typeConstraint,
+ dplUseConnectionNames, dplShowEntryIndexes, dplDefaultEntryName, dplEntryFormat);
return;
}
switch (showBacking) {
case XNode.Node.ShowBackingValue.Unconnected:
// Display a label if port is connected
- if (port.IsConnected) EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName), NodeEditorResources.OutputPort, GUILayout.MinWidth(30));
+ if (port.IsConnected) EditorGUILayout.LabelField(label ?? new GUIContent(property.displayName),
+ NodeEditorResources.OutputPort, GUILayout.MinWidth(30));
// Display an editable property field if port is not connected
else EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
break;
case XNode.Node.ShowBackingValue.Never:
// Display a label
- EditorGUILayout.LabelField(label != null ? label : new GUIContent(property.displayName), NodeEditorResources.OutputPort, GUILayout.MinWidth(30));
+ EditorGUILayout.LabelField(label ?? new GUIContent(property.displayName),
+ NodeEditorResources.OutputPort, GUILayout.MinWidth(30));
break;
case XNode.Node.ShowBackingValue.Always:
// Display an editable property field
EditorGUILayout.PropertyField(property, label, includeChildren, GUILayout.MinWidth(30));
break;
+ default:
+ throw new ArgumentOutOfRangeException();
}
rect = GUILayoutUtility.GetLastRect();
- rect.position = rect.position + new Vector2(rect.width, spacePadding);
+ rect.position += new Vector2(rect.width, spacePadding);
}
rect.size = new Vector2(16, 16);
@@ -167,24 +190,33 @@ namespace XNodeEditor {
public static void PortField(GUIContent label, XNode.NodePort port, params GUILayoutOption[] options) {
if (port == null) return;
if (options == null) options = new GUILayoutOption[] { GUILayout.MinWidth(30) };
- Vector2 position = Vector3.zero;
- GUIContent content = label != null ? label : new GUIContent(ObjectNames.NicifyVariableName(port.fieldName));
+ Vector2 position;
+ GUIContent content = label ?? new GUIContent(ObjectNames.NicifyVariableName(port.fieldName));
- // If property is an input, display a regular property field and put a port handle on the left side
- if (port.direction == XNode.NodePort.IO.Input) {
- // Display a label
- EditorGUILayout.LabelField(content, options);
+ switch (port.direction)
+ {
+ // If property is an input, display a regular property field and put a port handle on the left side
+ // If property is an output, display a text label and put a port handle on the right side
+ case XNode.NodePort.IO.Input:
+ {
+ // Display a label
+ EditorGUILayout.LabelField(content, options);
- Rect rect = GUILayoutUtility.GetLastRect();
- position = rect.position - new Vector2(16, 0);
- }
- // If property is an output, display a text label and put a port handle on the right side
- else if (port.direction == XNode.NodePort.IO.Output) {
- // Display a label
- EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options);
+ Rect rect = GUILayoutUtility.GetLastRect();
+ position = rect.position - new Vector2(16, 0);
+ break;
+ }
+ case XNode.NodePort.IO.Output:
+ {
+ // Display a label
+ EditorGUILayout.LabelField(content, NodeEditorResources.OutputPort, options);
- Rect rect = GUILayoutUtility.GetLastRect();
- position = rect.position + new Vector2(rect.width, 0);
+ Rect rect = GUILayoutUtility.GetLastRect();
+ position = rect.position + new Vector2(rect.width, 0);
+ break;
+ }
+ default:
+ throw new ArgumentOutOfRangeException();
}
PortField(position, port);
}
@@ -208,16 +240,22 @@ namespace XNodeEditor {
/// Add a port field to previous layout element.
public static void AddPortField(XNode.NodePort port) {
if (port == null) return;
- Rect rect = new Rect();
+ Rect rect;
- // If property is an input, display a regular property field and put a port handle on the left side
- if (port.direction == XNode.NodePort.IO.Input) {
- rect = GUILayoutUtility.GetLastRect();
- rect.position = rect.position - new Vector2(16, 0);
- // If property is an output, display a text label and put a port handle on the right side
- } else if (port.direction == XNode.NodePort.IO.Output) {
- rect = GUILayoutUtility.GetLastRect();
- rect.position = rect.position + new Vector2(rect.width, 0);
+ switch (port.direction)
+ {
+ // If property is an input, display a regular property field and put a port handle on the left side
+ case XNode.NodePort.IO.Input:
+ rect = GUILayoutUtility.GetLastRect();
+ rect.position -= new Vector2(16, 0);
+ // If property is an output, display a text label and put a port handle on the right side
+ break;
+ case XNode.NodePort.IO.Output:
+ rect = GUILayoutUtility.GetLastRect();
+ rect.position += new Vector2(rect.width, 0);
+ break;
+ default:
+ throw new ArgumentOutOfRangeException();
}
rect.size = new Vector2(16, 16);
@@ -257,7 +295,7 @@ namespace XNodeEditor {
[Obsolete("Use DynamicPortList instead")]
public static void InstancePortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action onCreation = null) {
- DynamicPortList(fieldName, type, serializedObject, io, connectionType, typeConstraint, onCreation);
+ DynamicPortList(fieldName, type, serializedObject, io, connectionType, typeConstraint, false, false, "port", null, onCreation);
}
#endregion
@@ -265,44 +303,66 @@ namespace XNodeEditor {
public static bool IsDynamicPortListPort(XNode.NodePort port) {
string[] parts = port.fieldName.Split(' ');
if (parts.Length != 2) return false;
- Dictionary cache;
- if (ReorderableListCache.TryGetValue(port.node, out cache)) {
- ReorderableList list;
- if (cache.TryGetValue(parts[0], out list)) return true;
- }
- return false;
+
+ return ReorderableListCache.TryGetValue(port.node, out var cache) &&
+ cache.TryGetValue(parts[0], out _);
}
/// Draw an editable list of dynamic ports. Port names are named as "[fieldName] [index]"
- /// Supply a list for editable values
- /// Value type of added dynamic ports
- /// The serializedObject of the node
- /// Connection type of added dynamic ports
- /// Called on the list on creation. Use this if you want to customize the created ReorderableList
- public static void DynamicPortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple, XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None, Action onCreation = null) {
+ /// Supply a list for editable values.
+ /// Value type of added dynamic ports.
+ /// The serializedObject of the node.
+ /// Connection type of added dynamic ports.
+ /// The type constraint to use on the ports.
+ /// The format to use for entry name generation. An example format would be:
+ /// {portName} [{index}]
+ ///
+ /// {portName} is replaced by the port name.
+ /// {index} is replaced by the port index.
+ ///
+ /// Called on the list on creation. Use this if you want to customize the created ReorderableList.
+ /// Set to true to use the port connection to generate the item names on the labels.
+ /// Set to true to show entry indexes on the entry labels.
+ /// Set this to a string to be used to generate the default entry name.
+ public static void DynamicPortList(string fieldName, Type type, SerializedObject serializedObject, XNode.NodePort.IO io,
+ XNode.Node.ConnectionType connectionType = XNode.Node.ConnectionType.Multiple,
+ XNode.Node.TypeConstraint typeConstraint = XNode.Node.TypeConstraint.None,
+ bool useConnectionNames = false,
+ bool showEntryIndexes = false,
+ string defaultEntryName = null,
+ string entryFormat = null,
+ Action onCreation = null) {
XNode.Node node = serializedObject.targetObject as XNode.Node;
+ if (node == null)
+ {
+ Debug.LogWarning($"Can't draw dynamic port list of null node for the field {fieldName}. Skipped.");
+ return;
+ }
var indexedPorts = node.DynamicPorts.Select(x => {
string[] split = x.fieldName.Split(' ');
- if (split != null && split.Length == 2 && split[0] == fieldName) {
- int i = -1;
- if (int.TryParse(split[1], out i)) {
- return new { index = i, port = x };
- }
- }
- return new { index = -1, port = (XNode.NodePort) null };
+
+ if (split.Length != 2 || split[0] != fieldName) return new {index = -1, port = (XNode.NodePort) null};
+
+ return int.TryParse(split[1], out var i) ?
+ new { index = i, port = x } :
+ new { index = -1, port = (XNode.NodePort) null };
+
}).Where(x => x.port != null);
+
List dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
ReorderableList list = null;
- Dictionary rlc;
- if (ReorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) {
+ if (ReorderableListCache.TryGetValue(serializedObject.targetObject, out var rlc)) {
if (!rlc.TryGetValue(fieldName, out list)) list = null;
}
// If a ReorderableList isn't cached for this array, do so.
if (list == null) {
SerializedProperty arrayData = serializedObject.FindProperty(fieldName);
- list = CreateReorderableList(fieldName, dynamicPorts, arrayData, type, serializedObject, io, connectionType, typeConstraint, onCreation);
+
+ list = CreateReorderableList(fieldName, dynamicPorts, arrayData, type, serializedObject, io, connectionType,
+ typeConstraint, useConnectionNames, showEntryIndexes, defaultEntryName, entryFormat, onCreation);
+
if (ReorderableListCache.TryGetValue(serializedObject.targetObject, out rlc)) rlc.Add(fieldName, list);
else ReorderableListCache.Add(serializedObject.targetObject, new Dictionary() { { fieldName, list } });
}
@@ -310,7 +370,21 @@ namespace XNodeEditor {
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) {
+ 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,
+ bool useConnectionNames,
+ bool showEntryIndexes,
+ string defaultEntryName,
+ string entryFormat,
+ Action onCreation) {
+
bool hasArrayData = arrayData != null && arrayData.isArray;
XNode.Node node = serializedObject.targetObject as XNode.Node;
ReorderableList list = new ReorderableList(dynamicPorts, null, true, true, true, true);
@@ -319,6 +393,22 @@ namespace XNodeEditor {
list.drawElementCallback =
(Rect rect, int index, bool isActive, bool isFocused) => {
XNode.NodePort port = node.GetPort(fieldName + " " + index);
+
+ string connectedTo = port != null ? port.fieldName : defaultEntryName;
+ if (useConnectionNames && port != null && port.IsConnected)
+ {
+ connectedTo = port.Connection.node.name;
+ }
+
+ //Get the format to render
+ string format = string.IsNullOrEmpty(entryFormat) ? "{portName} {index}" : entryFormat;
+
+ //Assemble the final name
+ string finalName =
+ format
+ .Replace("{portName}", useConnectionNames ? connectedTo : (port != null ? port.fieldName : defaultEntryName))
+ .Replace("{index}", showEntryIndexes ? $"{index}" : string.Empty);
+
if (hasArrayData) {
if (arrayData.arraySize <= index) {
EditorGUI.LabelField(rect, "Array[" + index + "] data out of range");
@@ -326,12 +416,15 @@ namespace XNodeEditor {
}
SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index);
EditorGUI.PropertyField(rect, itemData, true);
- } else EditorGUI.LabelField(rect, port != null ? port.fieldName : "");
- if (port != null) {
- Vector2 pos = rect.position + (port.IsOutput?new Vector2(rect.width + 6, 0) : new Vector2(-36, 0));
- NodeEditorGUILayout.PortField(pos, port);
- }
+ } else EditorGUI.LabelField(rect, finalName);
+
+ if (port == null) return;
+ Vector2 pos = rect.position + (port.IsOutput ?
+ new Vector2(rect.width + 6, 0) :
+ new Vector2(-36, 0));
+ NodeEditorGUILayout.PortField(pos, port);
};
+
list.elementHeightCallback =
(int index) => {
if (hasArrayData) {
@@ -340,14 +433,17 @@ namespace XNodeEditor {
return EditorGUI.GetPropertyHeight(itemData);
} else return EditorGUIUtility.singleLineHeight;
};
+
list.drawHeaderCallback =
(Rect rect) => {
EditorGUI.LabelField(rect, label);
};
+
list.onSelectCallback =
(ReorderableList rl) => {
reorderableListIndex = rl.index;
};
+
list.onReorderCallback =
(ReorderableList rl) => {
@@ -382,8 +478,9 @@ namespace XNodeEditor {
serializedObject.Update();
// Move array data if there is any
- if (hasArrayData) {
- arrayData.MoveArrayElement(reorderableListIndex, rl.index);
+ if (hasArrayData)
+ {
+ arrayData?.MoveArrayElement(reorderableListIndex, rl.index);
}
// Apply changes
@@ -392,6 +489,7 @@ namespace XNodeEditor {
NodeEditorWindow.current.Repaint();
EditorApplication.delayCall += NodeEditorWindow.current.Repaint;
};
+
list.onAddCallback =
(ReorderableList rl) => {
// Add dynamic port postfixed with an index number
@@ -408,18 +506,20 @@ namespace XNodeEditor {
}
serializedObject.ApplyModifiedProperties();
};
+
list.onRemoveCallback =
(ReorderableList rl) => {
var indexedPorts = node.DynamicPorts.Select(x => {
string[] split = x.fieldName.Split(' ');
- if (split != null && split.Length == 2 && split[0] == fieldName) {
- int i = -1;
- if (int.TryParse(split[1], out i)) {
- return new { index = i, port = x };
- }
- }
- return new { index = -1, port = (XNode.NodePort) null };
+
+ if (split.Length != 2 || split[0] != fieldName)
+ return new {index = -1, port = (XNode.NodePort) null};
+
+ return int.TryParse(split[1], out var i) ?
+ new { index = i, port = x } :
+ new { index = -1, port = (XNode.NodePort) null };
+
}).Where(x => x.port != null);
dynamicPorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList();
@@ -453,7 +553,9 @@ namespace XNodeEditor {
Debug.Log(rl.list[0]);
return;
}
+
arrayData.DeleteArrayElementAtIndex(index);
+
// Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues
if (dynamicPorts.Count <= arrayData.arraySize) {
while (dynamicPorts.Count <= arrayData.arraySize) {
@@ -484,7 +586,8 @@ namespace XNodeEditor {
serializedObject.ApplyModifiedProperties();
serializedObject.Update();
}
- if (onCreation != null) onCreation(list);
+
+ onCreation?.Invoke(list);
return list;
}
}
diff --git a/Scripts/Node.cs b/Scripts/Node.cs
index 27e32c7..f028078 100644
--- a/Scripts/Node.cs
+++ b/Scripts/Node.cs
@@ -311,6 +311,33 @@ namespace XNode {
[Obsolete("Use constructor with TypeConstraint")]
public OutputAttribute(ShowBackingValue backingValue, ConnectionType connectionType, bool dynamicPortList) : this(backingValue, connectionType, TypeConstraint.None, dynamicPortList) { }
}
+
+ ///
+ /// Use this to specify how a dynamic port list should be configured.
+ ///
+ [AttributeUsage(AttributeTargets.Field)]
+ public class DynamicPortListSettingsAttribute : Attribute
+ {
+ ///
+ /// Set this to true to use the first connection name as the name of each entry in the port list.
+ ///
+ public bool useConnectionNames;
+
+ ///
+ /// Set to true to show the entry index in the entry name.
+ ///
+ public bool showEntryIndexes;
+
+ ///
+ /// Use this to define the entry name for elements in the list.
+ ///
+ public string defaultEntryName = null;
+
+ ///
+ /// The format to use to display the list values.
+ ///
+ public string entryFormat = null;
+ }
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false)]
public class CreateNodeMenuAttribute : Attribute {