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