diff --git a/.gitignore b/.gitignore index 68af404..1460751 100644 --- a/.gitignore +++ b/.gitignore @@ -23,4 +23,8 @@ sysinfo.txt /Examples/ README.md.meta LICENSE.md.meta -CONTRIBUTING.md.meta \ No newline at end of file +CONTRIBUTING.md.meta + +.git.meta +.gitignore.meta +.gitattributes.meta diff --git a/Scripts/Editor/NodeEditor.cs b/Scripts/Editor/NodeEditor.cs index d222f9e..a6d1748 100644 --- a/Scripts/Editor/NodeEditor.cs +++ b/Scripts/Editor/NodeEditor.cs @@ -13,34 +13,9 @@ namespace XNodeEditor { /// Fires every whenever a node was modified through the editor public static Action onUpdateNode; public static Dictionary portPositions; - public int renaming; public virtual void OnHeaderGUI() { - string title = target.name; - if (renaming != 0) { - if (Selection.Contains(target)) { - int controlID = EditorGUIUtility.GetControlID(FocusType.Keyboard) + 1; - if (renaming == 1) { - EditorGUIUtility.keyboardControl = controlID; - EditorGUIUtility.editingTextField = true; - renaming = 2; - } - target.name = EditorGUILayout.TextField(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); - if (!EditorGUIUtility.editingTextField) { - Debug.Log("Finish renaming"); - Rename(target.name); - renaming = 0; - } - } - else { - // Selection changed, so stop renaming. - GUILayout.Label(title, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); - Rename(target.name); - renaming = 0; - } - } else { - GUILayout.Label(title, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); - } + GUILayout.Label(target.name, NodeEditorResources.styles.nodeHeader, GUILayout.Height(30)); } /// Draws standard field editors for all public fields @@ -63,7 +38,8 @@ namespace XNodeEditor { } // Iterate through instance ports and draw them in the order in which they are serialized - foreach(XNode.NodePort instancePort in target.InstancePorts) { + foreach (XNode.NodePort instancePort in target.InstancePorts) { + if (NodeEditorGUILayout.IsInstancePortListPort(instancePort)) continue; NodeEditorGUILayout.PortField(instancePort); } @@ -108,10 +84,7 @@ namespace XNodeEditor { } } - public void InitiateRename() { - renaming = 1; - } - + /// Rename the node asset. This will trigger a reimport of the node. public void Rename(string newName) { if (newName == null || newName.Trim() == "") newName = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name); target.name = newName; diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs index c641b7d..f3e3558 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -269,7 +269,7 @@ namespace XNodeEditor { } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, false); GenericMenu menu = new GenericMenu(); - NodeEditor.GetEditor(hoveredNode).AddContextMenuItems(menu); + NodeEditor.GetEditor(hoveredNode, this).AddContextMenuItems(menu); menu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); e.Use(); // Fixes copy/paste context menu appearing in Unity 5.6.6f2 - doesn't occur in 2018.3.2f1 Probably needs to be used in other places. } else if (!IsHoveringNode) { @@ -366,7 +366,12 @@ namespace XNodeEditor { public void RenameSelectedNode() { if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { XNode.Node node = Selection.activeObject as XNode.Node; - NodeEditor.GetEditor(node).InitiateRename(); + Vector2 size; + if (nodeSizes.TryGetValue(node, out size)) { + RenamePopup.Show(Selection.activeObject, size.x); + } else { + RenamePopup.Show(Selection.activeObject); + } } } diff --git a/Scripts/Editor/NodeEditorBase.cs b/Scripts/Editor/NodeEditorBase.cs index b94f290..ab463e6 100644 --- a/Scripts/Editor/NodeEditorBase.cs +++ b/Scripts/Editor/NodeEditorBase.cs @@ -14,10 +14,11 @@ namespace XNodeEditor.Internal { /// Custom editors defined with [CustomNodeEditor] private static Dictionary editorTypes; private static Dictionary editors = new Dictionary(); + public NodeEditorWindow window; public K target; public SerializedObject serializedObject; - public static T GetEditor(K target) { + public static T GetEditor(K target, NodeEditorWindow window) { if (target == null) return null; T editor; if (!editors.TryGetValue(target, out editor)) { @@ -26,9 +27,12 @@ namespace XNodeEditor.Internal { editor = Activator.CreateInstance(editorType) as T; editor.target = target; editor.serializedObject = new SerializedObject(target); + editor.window = window; + editor.OnCreate(); editors.Add(target, editor); } if (editor.target == null) editor.target = target; + if (editor.window != window) editor.window = window; if (editor.serializedObject == null) editor.serializedObject = new SerializedObject(target); return editor; } @@ -56,6 +60,9 @@ namespace XNodeEditor.Internal { } } + /// Called on creation, after references have been set + public virtual void OnCreate() { } + public interface INodeEditorAttrib { Type GetInspectedType(); } diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index 3033108..279daf1 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -18,9 +18,7 @@ namespace XNodeEditor { Event e = Event.current; Matrix4x4 m = GUI.matrix; if (graph == null) return; - graphEditor = NodeGraphEditor.GetEditor(graph); - graphEditor.position = position; - + ValidateGraphEditor(); Controls(); DrawGrid(position, zoom, panOffset); @@ -189,7 +187,7 @@ namespace XNodeEditor { Rect fromRect; if (!_portConnectionPoints.TryGetValue(output, out fromRect)) continue; - Color connectionColor = graphEditor.GetTypeColor(output.ValueType); + Color connectionColor = graphEditor.GetPortColor(output); for (int k = 0; k < output.ConnectionCount; k++) { XNode.NodePort input = output.GetConnection(k); @@ -293,7 +291,7 @@ namespace XNodeEditor { _portConnectionPoints = _portConnectionPoints.Where(x => x.Key.node != node).ToDictionary(kvp => kvp.Key, kvp => kvp.Value); } - NodeEditor nodeEditor = NodeEditor.GetEditor(node); + NodeEditor nodeEditor = NodeEditor.GetEditor(node, this); NodeEditor.portPositions = new Dictionary(); diff --git a/Scripts/Editor/NodeEditorGUILayout.cs b/Scripts/Editor/NodeEditorGUILayout.cs index 8a81423..a1c5d83 100644 --- a/Scripts/Editor/NodeEditorGUILayout.cs +++ b/Scripts/Editor/NodeEditorGUILayout.cs @@ -142,7 +142,7 @@ namespace XNodeEditor { Color backgroundColor = new Color32(90, 97, 105, 255); Color tint; if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint; - Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType); + Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); DrawPortHandle(rect, backgroundColor, col); // Register the handle position @@ -199,7 +199,7 @@ namespace XNodeEditor { Color backgroundColor = new Color32(90, 97, 105, 255); Color tint; if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint; - Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType); + Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); DrawPortHandle(rect, backgroundColor, col); // Register the handle position @@ -228,7 +228,7 @@ namespace XNodeEditor { Color backgroundColor = new Color32(90, 97, 105, 255); Color tint; if (NodeEditorWindow.nodeTint.TryGetValue(port.node.GetType(), out tint)) backgroundColor *= tint; - Color col = NodeEditorWindow.current.graphEditor.GetTypeColor(port.ValueType); + Color col = NodeEditorWindow.current.graphEditor.GetPortColor(port); DrawPortHandle(rect, backgroundColor, col); // Register the handle position @@ -254,6 +254,18 @@ namespace XNodeEditor { GUI.color = col; } + /// Is this port part of an InstancePortList? + public static bool IsInstancePortListPort(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; + } + /// Draw an editable list of instance ports. Port names are named as "[fieldName] [index]" /// Supply a list for editable values /// Value type of added instance ports @@ -263,13 +275,17 @@ namespace XNodeEditor { 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) { XNode.Node node = serializedObject.targetObject as XNode.Node; - Predicate isMatchingInstancePort = - x => { - string[] split = x.Split(' '); - if (split != null && split.Length == 2) return split[0] == fieldName; - else return false; - }; - List instancePorts = node.InstancePorts.Where(x => isMatchingInstancePort(x.fieldName)).OrderBy(x => x.fieldName).ToList(); + var indexedPorts = node.InstancePorts.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 }; + }); + List instancePorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); ReorderableList list = null; Dictionary rlc; @@ -289,7 +305,6 @@ namespace XNodeEditor { private static ReorderableList CreateReorderableList(string fieldName, List instancePorts, SerializedProperty arrayData, Type type, SerializedObject serializedObject, XNode.NodePort.IO io, XNode.Node.ConnectionType connectionType, XNode.Node.TypeConstraint typeConstraint, Action onCreation) { bool hasArrayData = arrayData != null && arrayData.isArray; - int arraySize = hasArrayData ? arrayData.arraySize : 0; XNode.Node node = serializedObject.targetObject as XNode.Node; ReorderableList list = new ReorderableList(instancePorts, null, true, true, true, true); string label = arrayData != null ? arrayData.displayName : ObjectNames.NicifyVariableName(fieldName); @@ -305,8 +320,10 @@ namespace XNodeEditor { SerializedProperty itemData = arrayData.GetArrayElementAtIndex(index); EditorGUI.PropertyField(rect, itemData, true); } else EditorGUI.LabelField(rect, port.fieldName); - Vector2 pos = rect.position + (port.IsOutput?new Vector2(rect.width + 6, 0) : new Vector2(-36, 0)); - NodeEditorGUILayout.PortField(pos, port); + if (port != null) { + Vector2 pos = rect.position + (port.IsOutput?new Vector2(rect.width + 6, 0) : new Vector2(-36, 0)); + NodeEditorGUILayout.PortField(pos, port); + } }; list.elementHeightCallback = (int index) => { @@ -379,11 +396,26 @@ namespace XNodeEditor { else node.AddInstanceInput(type, connectionType, typeConstraint, newName); serializedObject.Update(); EditorUtility.SetDirty(node); - if (hasArrayData) arrayData.InsertArrayElementAtIndex(arraySize); + if (hasArrayData) { + arrayData.InsertArrayElementAtIndex(arrayData.arraySize); + } serializedObject.ApplyModifiedProperties(); }; list.onRemoveCallback = (ReorderableList rl) => { + + var indexedPorts = node.InstancePorts.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 }; + }); + instancePorts = indexedPorts.OrderBy(x => x.index).Select(x => x.port).ToList(); + int index = rl.index; if (instancePorts.Count > index) { @@ -407,23 +439,21 @@ namespace XNodeEditor { if (hasArrayData) { arrayData.DeleteArrayElementAtIndex(index); - arraySize--; // Error handling. If the following happens too often, file a bug report at https://github.com/Siccity/xNode/issues - if (instancePorts.Count <= arraySize) { - while (instancePorts.Count <= arraySize) { - arrayData.DeleteArrayElementAtIndex(--arraySize); + if (instancePorts.Count <= arrayData.arraySize) { + while (instancePorts.Count <= arrayData.arraySize) { + arrayData.DeleteArrayElementAtIndex(arrayData.arraySize - 1); } UnityEngine.Debug.LogWarning("Array size exceeded instance ports size. Excess items removed."); } serializedObject.ApplyModifiedProperties(); serializedObject.Update(); } - }; if (hasArrayData) { int instancePortCount = instancePorts.Count; - while (instancePortCount < arraySize) { + while (instancePortCount < arrayData.arraySize) { // Add instance port postfixed with an index number string newName = arrayData.name + " 0"; int i = 0; @@ -433,9 +463,8 @@ namespace XNodeEditor { EditorUtility.SetDirty(node); instancePortCount++; } - while (arraySize < instancePortCount) { - arrayData.InsertArrayElementAtIndex(arraySize); - arraySize++; + while (arrayData.arraySize < instancePortCount) { + arrayData.InsertArrayElementAtIndex(arrayData.arraySize); } serializedObject.ApplyModifiedProperties(); serializedObject.Update(); diff --git a/Scripts/Editor/NodeEditorPreferences.cs b/Scripts/Editor/NodeEditorPreferences.cs index e896f34..bd2cb55 100644 --- a/Scripts/Editor/NodeEditorPreferences.cs +++ b/Scripts/Editor/NodeEditorPreferences.cs @@ -23,6 +23,7 @@ namespace XNodeEditor { [SerializeField] private Color32 _gridBgColor = new Color(0.18f, 0.18f, 0.18f); public Color32 gridBgColor { get { return _gridBgColor; } set { _gridBgColor = value; _gridTexture = null; } } + public float zoomOutLimit = 5f; public Color32 highlightColor = new Color32(255, 255, 255, 255); public bool gridSnap = true; public bool autoSave = true; @@ -113,7 +114,7 @@ namespace XNodeEditor { EditorGUILayout.LabelField("Grid", EditorStyles.boldLabel); settings.gridSnap = EditorGUILayout.Toggle(new GUIContent("Snap", "Hold CTRL in editor to invert"), settings.gridSnap); settings.zoomToMouse = EditorGUILayout.Toggle(new GUIContent("Zoom to Mouse", "Zooms towards mouse position"), settings.zoomToMouse); - + settings.zoomOutLimit = EditorGUILayout.FloatField(new GUIContent("Zoom out Limit", "Upper limit to zoom"), settings.zoomOutLimit); settings.gridLineColor = EditorGUILayout.ColorField("Color", settings.gridLineColor); settings.gridBgColor = EditorGUILayout.ColorField(" ", settings.gridBgColor); if (GUI.changed) { diff --git a/Scripts/Editor/NodeEditorReflection.cs b/Scripts/Editor/NodeEditorReflection.cs index a2be28e..18b72fa 100644 --- a/Scripts/Editor/NodeEditorReflection.cs +++ b/Scripts/Editor/NodeEditorReflection.cs @@ -75,7 +75,9 @@ namespace XNodeEditor { List types = new List(); System.Reflection.Assembly[] assemblies = System.AppDomain.CurrentDomain.GetAssemblies(); foreach (Assembly assembly in assemblies) { - types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); + try { + types.AddRange(assembly.GetTypes().Where(t => !t.IsAbstract && baseType.IsAssignableFrom(t)).ToArray()); + } catch(ReflectionTypeLoadException) {} } return types.ToArray(); } @@ -162,4 +164,4 @@ namespace XNodeEditor { } } } -} \ No newline at end of file +} diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs index 740fd20..a575c8d 100644 --- a/Scripts/Editor/NodeEditorWindow.cs +++ b/Scripts/Editor/NodeEditorWindow.cs @@ -61,15 +61,38 @@ namespace XNodeEditor { public XNode.NodeGraph graph; public Vector2 panOffset { get { return _panOffset; } set { _panOffset = value; Repaint(); } } private Vector2 _panOffset; - public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, 1f, 5f); Repaint(); } } + public float zoom { get { return _zoom; } set { _zoom = Mathf.Clamp(value, 1f, NodeEditorPreferences.GetSettings().zoomOutLimit); Repaint(); } } private float _zoom = 1; void OnFocus() { current = this; - graphEditor = NodeGraphEditor.GetEditor(graph); + ValidateGraphEditor(); if (graphEditor != null && NodeEditorPreferences.GetSettings().autoSave) AssetDatabase.SaveAssets(); } + [InitializeOnLoadMethod] + private static void OnLoad() { + Selection.selectionChanged -= OnSelectionChanged; + Selection.selectionChanged += OnSelectionChanged; + } + + /// Handle Selection Change events + private static void OnSelectionChanged() { + XNode.NodeGraph nodeGraph = Selection.activeObject as XNode.NodeGraph; + if (nodeGraph && !AssetDatabase.Contains(nodeGraph)) { + Open(nodeGraph); + } + } + + /// Make sure the graph editor is assigned and to the right object + private void ValidateGraphEditor() { + NodeGraphEditor graphEditor = NodeGraphEditor.GetEditor(graph, this); + if (this.graphEditor != graphEditor) { + this.graphEditor = graphEditor; + graphEditor.OnOpen(); + } + } + /// Create editor window public static NodeEditorWindow Init() { NodeEditorWindow w = CreateInstance(); @@ -147,14 +170,21 @@ namespace XNodeEditor { public static bool OnOpen(int instanceID, int line) { XNode.NodeGraph nodeGraph = EditorUtility.InstanceIDToObject(instanceID) as XNode.NodeGraph; if (nodeGraph != null) { - NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; - w.wantsMouseMove = true; - w.graph = nodeGraph; + Open(nodeGraph); return true; } return false; } + /// Open the provided graph in the NodeEditor + public static void Open(XNode.NodeGraph graph) { + if (!graph) return; + + NodeEditorWindow w = GetWindow(typeof(NodeEditorWindow), false, "xNode", true) as NodeEditorWindow; + w.wantsMouseMove = true; + w.graph = graph; + } + /// Repaint all open NodeEditorWindows. public static void RepaintAll() { NodeEditorWindow[] windows = Resources.FindObjectsOfTypeAll(); @@ -163,4 +193,4 @@ namespace XNodeEditor { } } } -} \ No newline at end of file +} diff --git a/Scripts/Editor/NodeGraphEditor.cs b/Scripts/Editor/NodeGraphEditor.cs index 81a53c8..8a9d2f0 100644 --- a/Scripts/Editor/NodeGraphEditor.cs +++ b/Scripts/Editor/NodeGraphEditor.cs @@ -8,13 +8,16 @@ namespace XNodeEditor { /// Base class to derive custom Node Graph editors from. Use this to override how graphs are drawn in the editor. [CustomNodeGraphEditor(typeof(XNode.NodeGraph))] public class NodeGraphEditor : XNodeEditor.Internal.NodeEditorBase { - /// The position of the window in screen space. - public Rect position; + [Obsolete("Use window.position instead")] + public Rect position { get { return window.position; } set { window.position = value; } } /// Are we currently renaming a node? protected bool isRenaming; public virtual void OnGUI() { } + /// Called when opened by NodeEditorWindow + public virtual void OnOpen() { } + public virtual Texture2D GetGridTexture() { return NodeEditorPreferences.GetSettings().gridTexture; } @@ -57,6 +60,10 @@ namespace XNodeEditor { NodeEditorWindow.AddCustomContextMenuItems(menu, target); } + public virtual Color GetPortColor(XNode.NodePort port) { + return GetTypeColor(port.ValueType); + } + public virtual Color GetTypeColor(Type type) { return NodeEditorPreferences.GetTypeColor(type); } diff --git a/Scripts/Editor/RenamePopup.cs b/Scripts/Editor/RenamePopup.cs new file mode 100644 index 0000000..a49a948 --- /dev/null +++ b/Scripts/Editor/RenamePopup.cs @@ -0,0 +1,66 @@ +using UnityEditor; +using UnityEngine; + +namespace XNodeEditor { + /// Utility for renaming assets + public class RenamePopup : EditorWindow { + public static RenamePopup current { get; private set; } + public Object target; + public string input; + + private bool firstFrame = true; + + /// Show a rename popup for an asset at mouse position. Will trigger reimport of the asset on apply. + public static RenamePopup Show(Object target, float width = 200) { + RenamePopup window = EditorWindow.GetWindow(true, "Rename " + target.name, true); + if (current != null) current.Close(); + current = window; + window.target = target; + window.input = target.name; + window.minSize = new Vector2(100, 44); + window.position = new Rect(0, 0, width, 44); + GUI.FocusControl("ClearAllFocus"); + window.UpdatePositionToMouse(); + return window; + } + + private void UpdatePositionToMouse() { + if (Event.current == null) return; + Vector3 mousePoint = GUIUtility.GUIToScreenPoint(Event.current.mousePosition); + Rect pos = position; + pos.x = mousePoint.x - position.width * 0.5f; + pos.y = mousePoint.y - 10; + position = pos; + } + + private void OnLostFocus() { + // Make the popup close on lose focus + Close(); + } + + private void OnGUI() { + if (firstFrame) { + UpdatePositionToMouse(); + firstFrame = false; + } + input = EditorGUILayout.TextField(input); + Event e = Event.current; + // If input is empty, revert name to default instead + if (input == null || input.Trim() == "") { + if (GUILayout.Button("Revert to default") || (e.isKey && e.keyCode == KeyCode.Return)) { + target.name = UnityEditor.ObjectNames.NicifyVariableName(target.GetType().Name); + AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); + Close(); + } + } + // Rename asset to input text + else { + if (GUILayout.Button("Apply") || (e.isKey && e.keyCode == KeyCode.Return)) { + target.name = input; + AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(target)); + Close(); + } + } + } + } +} \ No newline at end of file diff --git a/Scripts/Editor/RenamePopup.cs.meta b/Scripts/Editor/RenamePopup.cs.meta new file mode 100644 index 0000000..5c40a02 --- /dev/null +++ b/Scripts/Editor/RenamePopup.cs.meta @@ -0,0 +1,13 @@ +fileFormatVersion: 2 +guid: 4ef3ddc25518318469bce838980c64be +timeCreated: 1552067957 +licenseType: Free +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Scripts/Node.cs b/Scripts/Node.cs index 8785280..74eb309 100644 --- a/Scripts/Node.cs +++ b/Scripts/Node.cs @@ -127,6 +127,8 @@ namespace XNode { /// Remove an instance port from the node public void RemoveInstancePort(string fieldName) { + NodePort instancePort = GetPort(fieldName); + if (instancePort == null) throw new ArgumentException("port " + fieldName + " doesn't exist"); RemoveInstancePort(GetPort(fieldName)); }