using System; using System.Linq; using UnityEditor; using UnityEngine; using XNode; using XNodeEditor.Internal; using Object = UnityEngine.Object; #if UNITY_2019_1_OR_NEWER && USE_ADVANCED_GENERIC_MENU using GenericMenu = XNodeEditor.AdvancedGenericMenu; #endif namespace XNodeEditor { /// Base class to derive custom Node Graph editors from. Use this to override how graphs are drawn in the editor. [CustomNodeGraphEditor(typeof(NodeGraph))] public class NodeGraphEditor : NodeEditorBase { [Obsolete("Use window.position instead")] public Rect position { get => 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() {} /// Called when NodeEditorWindow gains focus public virtual void OnWindowFocus() {} /// Called when NodeEditorWindow loses focus public virtual void OnWindowFocusLost() {} public virtual Texture2D GetGridTexture() { return NodeEditorPreferences.GetSettings().gridTexture; } public virtual Texture2D GetSecondaryGridTexture() { return NodeEditorPreferences.GetSettings().crossTexture; } /// Return default settings for this graph type. This is the settings the user will load if no previous settings have been saved. public virtual NodeEditorPreferences.Settings GetDefaultPreferences() { return new NodeEditorPreferences.Settings(); } /// Returns context node menu path. Null or empty strings for hidden nodes. public virtual string GetNodeMenuName(Type type) { //Check if type has the CreateNodeMenuAttribute Node.CreateNodeMenuAttribute attrib; if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path { return attrib.menuName; } // Return generated path return NodeEditorUtilities.NodeDefaultPath(type); } /// The order by which the menu items are displayed. public virtual int GetNodeMenuOrder(Type type) { //Check if type has the CreateNodeMenuAttribute Node.CreateNodeMenuAttribute attrib; if (NodeEditorUtilities.GetAttrib(type, out attrib)) // Return custom path { return attrib.order; } return 0; } /// /// Called before connecting two ports in the graph view to see if the output port is compatible with the input port /// public virtual bool CanConnect(NodePort output, NodePort input) { return output.CanConnectTo(input); } /// /// Add items for the context menu when right-clicking this node. /// Override to add custom menu items. /// /// /// Use it to filter only nodes with ports value type, compatible with this type /// Direction of the compatiblity public virtual void AddContextMenuItems(GenericMenu menu, Type compatibleType = null, NodePort.IO direction = NodePort.IO.Input) { Vector2 pos = NodeEditorWindow.current.WindowToGridPosition(Event.current.mousePosition); Type[] nodeTypes; if (compatibleType != null && NodeEditorPreferences.GetSettings().createFilter) { nodeTypes = NodeEditorUtilities .GetCompatibleNodesTypes(NodeEditorReflection.nodeTypes, compatibleType, direction) .OrderBy(GetNodeMenuOrder).ToArray(); } else { nodeTypes = NodeEditorReflection.nodeTypes.OrderBy(GetNodeMenuOrder).ToArray(); } for (int i = 0; i < nodeTypes.Length; i++) { Type type = nodeTypes[i]; //Get node context menu path string path = GetNodeMenuName(type); if (string.IsNullOrEmpty(path)) { continue; } // Check if user is allowed to add more of given node type Node.DisallowMultipleNodesAttribute disallowAttrib; bool disallowed = false; if (NodeEditorUtilities.GetAttrib(type, out disallowAttrib)) { int typeCount = target.nodes.Count(x => x.GetType() == type); if (typeCount >= disallowAttrib.max) { disallowed = true; } } // Add node entry to context menu if (disallowed) { menu.AddItem(new GUIContent(path), false, null); } else { menu.AddItem(new GUIContent(path), false, () => { Node node = CreateNode(type, pos); if (node != null) { NodeEditorWindow.current.AutoConnect(node); // handle null nodes to avoid nullref exceptions } }); } } menu.AddSeparator(""); if (NodeEditorWindow.copyBuffer != null && NodeEditorWindow.copyBuffer.Length > 0) { menu.AddItem(new GUIContent("Paste"), false, () => NodeEditorWindow.current.PasteNodes(pos)); } else { menu.AddDisabledItem(new GUIContent("Paste")); } menu.AddItem(new GUIContent("Preferences"), false, () => NodeEditorReflection.OpenPreferences()); menu.AddCustomContextMenuItems(target); } /// Returned gradient is used to color noodles /// The output this noodle comes from. Never null. /// The output this noodle comes from. Can be null if we are dragging the noodle. public virtual Gradient GetNoodleGradient(NodePort output, NodePort input) { Gradient grad = new Gradient(); // If dragging the noodle, draw solid, slightly transparent if (input == null) { Color a = GetTypeColor(output.ValueType); grad.SetKeys( new[] { new GradientColorKey(a, 0f) }, new[] { new GradientAlphaKey(0.6f, 0f) } ); } // If normal, draw gradient fading from one input color to the other else { Color a = GetTypeColor(output.ValueType); Color b = GetTypeColor(input.ValueType); // If any port is hovered, tint white if (window.hoveredPort == output || window.hoveredPort == input) { a = Color.Lerp(a, Color.white, 0.8f); b = Color.Lerp(b, Color.white, 0.8f); } grad.SetKeys( new[] { new GradientColorKey(a, 0f), new GradientColorKey(b, 1f) }, new[] { new GradientAlphaKey(1f, 0f), new GradientAlphaKey(1f, 1f) } ); } return grad; } /// Returned float is used for noodle thickness /// The output this noodle comes from. Never null. /// The output this noodle comes from. Can be null if we are dragging the noodle. public virtual float GetNoodleThickness(NodePort output, NodePort input) { return NodeEditorPreferences.GetSettings().noodleThickness; } public virtual NoodlePath GetNoodlePath(NodePort output, NodePort input) { return NodeEditorPreferences.GetSettings().noodlePath; } public virtual NoodleStroke GetNoodleStroke(NodePort output, NodePort input) { return NodeEditorPreferences.GetSettings().noodleStroke; } /// Returned color is used to color ports public virtual Color GetPortColor(NodePort port) { return GetTypeColor(port.ValueType); } /// /// The returned Style is used to configure the paddings and icon texture of the ports. /// Use these properties to customize your port style. /// The properties used is: /// [Left and Right], [Background] = border texture, /// and [Background] = dot texture; /// /// the owner of the style /// public virtual GUIStyle GetPortStyle(NodePort port) { if (port.direction == NodePort.IO.Input) { return NodeEditorResources.styles.inputPort; } return NodeEditorResources.styles.outputPort; } /// /// The returned color is used to color the background of the door. /// Usually used for outer edge effect /// public virtual Color GetPortBackgroundColor(NodePort port) { return Color.gray; } /// Returns generated color for a type. This color is editable in preferences public virtual Color GetTypeColor(Type type) { return NodeEditorPreferences.GetTypeColor(type); } /// Override to display custom tooltips public virtual string GetPortTooltip(NodePort port) { Type portType = port.ValueType; string tooltip = ""; tooltip = portType.PrettyName(); if (port.IsOutput) { object obj = port.node.GetValue(port); tooltip += " = " + (obj != null ? obj.ToString() : "null"); } return tooltip; } /// Deal with objects dropped into the graph through DragAndDrop public virtual void OnDropObjects(Object[] objects) { if (GetType() != typeof(NodeGraphEditor)) { Debug.Log("No OnDropObjects override defined for " + GetType()); } } /// Create a node and save it in the graph asset public virtual Node CreateNode(Type type, Vector2 position) { Undo.RecordObject(target, "Create Node"); Node node = target.AddNode(type); if (node == null) { return null; // handle null nodes to avoid nullref exceptions } Undo.RegisterCreatedObjectUndo(node, "Create Node"); node.position = position; if (node.name == null || node.name.Trim() == "") { node.name = NodeEditorUtilities.NodeDefaultName(type); } if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) { AssetDatabase.AddObjectToAsset(node, target); } if (NodeEditorPreferences.GetSettings().autoSave) { AssetDatabase.SaveAssets(); } NodeEditorWindow.RepaintAll(); return node; } /// Creates a copy of the original node in the graph public virtual Node CopyNode(Node original) { Undo.RecordObject(target, "Duplicate Node"); Node node = target.CopyNode(original); Undo.RegisterCreatedObjectUndo(node, "Duplicate Node"); node.name = original.name; if (!string.IsNullOrEmpty(AssetDatabase.GetAssetPath(target))) { AssetDatabase.AddObjectToAsset(node, target); } if (NodeEditorPreferences.GetSettings().autoSave) { AssetDatabase.SaveAssets(); } return node; } /// Return false for nodes that can't be removed public virtual bool CanRemove(Node node) { // Check graph attributes to see if this node is required Type graphType = target.GetType(); var attribs = Array.ConvertAll( graphType.GetCustomAttributes(typeof(NodeGraph.RequireNodeAttribute), true), x => x as NodeGraph.RequireNodeAttribute); if (attribs.Any(x => x.Requires(node.GetType()))) { if (target.nodes.Count(x => x.GetType() == node.GetType()) <= 1) { return false; } } return true; } /// Safely remove a node and all its connections. public virtual void RemoveNode(Node node) { if (!CanRemove(node)) { return; } // Remove the node Undo.RecordObject(node, "Delete Node"); Undo.RecordObject(target, "Delete Node"); foreach (NodePort port in node.Ports) foreach (NodePort conn in port.GetConnections()) { Undo.RecordObject(conn.node, "Delete Node"); } target.RemoveNode(node); Undo.DestroyObjectImmediate(node); if (NodeEditorPreferences.GetSettings().autoSave) { AssetDatabase.SaveAssets(); } } [AttributeUsage(AttributeTargets.Class)] public class CustomNodeGraphEditorAttribute : Attribute, INodeEditorAttrib { private readonly Type inspectedType; public string editorPrefsKey; /// Tells a NodeGraphEditor which Graph type it is an editor for /// Type that this editor can edit /// Define unique key for unique layout settings instance public CustomNodeGraphEditorAttribute(Type inspectedType, string editorPrefsKey = "xNode.Settings") { this.inspectedType = inspectedType; this.editorPrefsKey = editorPrefsKey; } public Type GetInspectedType() { return inspectedType; } } } }