diff --git a/Scripts/Editor/NodeEditorAction.cs b/Scripts/Editor/NodeEditorAction.cs index 409eea8..6d0cc7c 100644 --- a/Scripts/Editor/NodeEditorAction.cs +++ b/Scripts/Editor/NodeEditorAction.cs @@ -6,22 +6,20 @@ namespace XNodeEditor { public partial class NodeEditorWindow { public static bool isPanning { get; private set; } - public static Vector2 dragOffset; + public static Vector2[] dragOffset; - private bool IsDraggingNode { get { return draggedNode != null; } } + //private bool IsDraggingNode { get { return draggedNode != null; } } private bool IsDraggingPort { get { return draggedOutput != null; } } private bool IsHoveringPort { get { return hoveredPort != null; } } private bool IsHoveringNode { get { return hoveredNode != null; } } - private bool HasSelectedNode { get { return selectedNode != null; } } - + public bool CanDragNodeHeader { get; private set; } + public bool DidDragNodeHeader { get; private set; } private XNode.Node hoveredNode = null; - [NonSerialized] private XNode.Node selectedNode = null; - [NonSerialized] private XNode.Node draggedNode = null; + //[NonSerialized] private XNode.Node draggedNode = null; [NonSerialized] private XNode.NodePort hoveredPort = null; [NonSerialized] private XNode.NodePort draggedOutput = null; [NonSerialized] private XNode.NodePort draggedOutputTarget = null; - private Rect nodeRects; public void Controls() { @@ -45,12 +43,18 @@ namespace XNodeEditor { draggedOutputTarget = null; } Repaint(); - } else if (IsDraggingNode) { - draggedNode.position = WindowToGridPosition(e.mousePosition) + dragOffset; - if (NodeEditorPreferences.gridSnap) { - draggedNode.position.x = (Mathf.Round((draggedNode.position.x + 8) / 16) * 16) - 8; - draggedNode.position.y = (Mathf.Round((draggedNode.position.y + 8) / 16) * 16) - 8; + } else if (CanDragNodeHeader) { + for (int i = 0; i < Selection.objects.Length; i++) { + if (Selection.objects[i] is XNode.Node) { + XNode.Node node = Selection.objects[i] as XNode.Node; + node.position = WindowToGridPosition(e.mousePosition) + dragOffset[i]; + if (NodeEditorPreferences.gridSnap) { + node.position.x = (Mathf.Round((node.position.x + 8) / 16) * 16) - 8; + node.position.y = (Mathf.Round((node.position.y + 8) / 16) * 16) - 8; + } + } } + DidDragNodeHeader = true; Repaint(); } } else if (e.button == 1 || e.button == 2) { @@ -66,7 +70,11 @@ namespace XNodeEditor { case EventType.MouseDown: Repaint(); if (e.button == 0) { - SelectNode(hoveredNode); + + if (hoveredNode == null) Selection.activeObject = null; + else if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode, e.control || e.shift); + else if (e.control || e.shift) DeselectNode(hoveredNode); + if (IsHoveringPort) { if (hoveredPort.IsOutput) { draggedOutput = hoveredPort; @@ -82,8 +90,15 @@ namespace XNodeEditor { } } } else if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { - draggedNode = hoveredNode; - dragOffset = hoveredNode.position - WindowToGridPosition(e.mousePosition); + DidDragNodeHeader = false; + CanDragNodeHeader = true; + dragOffset = new Vector2[Selection.objects.Length]; + for (int i = 0; i < dragOffset.Length; i++) { + if (Selection.objects[i] is XNode.Node) { + XNode.Node node = Selection.objects[i] as XNode.Node; + dragOffset[i] = node.position - WindowToGridPosition(e.mousePosition); + } + } } } break; @@ -104,16 +119,22 @@ namespace XNodeEditor { EditorUtility.SetDirty(graph); Repaint(); AssetDatabase.SaveAssets(); - } else if (IsDraggingNode) { - draggedNode = null; + } else if (CanDragNodeHeader) { + CanDragNodeHeader = false; AssetDatabase.SaveAssets(); } else if (GUIUtility.hotControl != 0) { AssetDatabase.SaveAssets(); } + + if (IsHoveringNode && !DidDragNodeHeader && !e.control) { + SelectNode(hoveredNode, false); + Repaint(); + } } else if (e.button == 1) { if (!isPanning) { if (IsHoveringNode && IsHoveringTitle(hoveredNode)) { - ShowNodeContextMenu(hoveredNode); + if (!Selection.Contains(hoveredNode)) SelectNode(hoveredNode,false); + ShowNodeContextMenu(); } else if (!IsHoveringNode) { ShowGraphContextMenu(); } @@ -121,6 +142,11 @@ namespace XNodeEditor { isPanning = false; } break; + case EventType.KeyDown: + if (e.keyCode == KeyCode.Delete) RemoveSelectedNodes(); + else if (e.keyCode == KeyCode.D && e.control) DublicateSelectedNodes(); + Repaint(); + break; } } @@ -136,6 +162,31 @@ namespace XNodeEditor { Repaint(); } + /// Remove nodes in the graph in Selection.objects + public void RemoveSelectedNodes() { + foreach (UnityEngine.Object item in Selection.objects) { + if (item is XNode.Node) { + XNode.Node node = item as XNode.Node; + graph.RemoveNode(node); + } + } + } + + // Dublicate selected nodes and select the dublicates + public void DublicateSelectedNodes() { + UnityEngine.Object[] newNodes = new UnityEngine.Object[Selection.objects.Length]; + for (int i = 0; i < Selection.objects.Length; i++) { + if (Selection.objects[i] is XNode.Node) { + XNode.Node node = Selection.objects[i] as XNode.Node; + if (node.graph != graph) continue; // ignore nodes selected in another graph + XNode.Node n = graph.CopyNode(node); + n.position = node.position + new Vector2(30, 30); + newNodes[i] = n; + } + } + Selection.objects = newNodes; + } + /// Draw a connection as we are dragging it public void DrawDraggedConnection() { if (IsDraggingPort) { diff --git a/Scripts/Editor/NodeEditorGUI.cs b/Scripts/Editor/NodeEditorGUI.cs index f8ab6d5..0153965 100644 --- a/Scripts/Editor/NodeEditorGUI.cs +++ b/Scripts/Editor/NodeEditorGUI.cs @@ -6,7 +6,8 @@ using UnityEngine; namespace XNodeEditor { /// Contains GUI methods public partial class NodeEditorWindow { - NodeGraphEditor currentGraphEditor; + private NodeGraphEditor currentGraphEditor; + private List selectionCache; private void OnGUI() { Event e = Event.current; @@ -74,23 +75,30 @@ namespace XNodeEditor { return GUILayout.Button(name, EditorStyles.toolbarDropDown, GUILayout.Width(width)); } - /// Show right-click context menu for a node - public void ShowNodeContextMenu(XNode.Node node) { + /// Show right-click context menu for selected nodes + public void ShowNodeContextMenu() { GenericMenu contextMenu = new GenericMenu(); - contextMenu.AddItem(new GUIContent("Move To Top"), false, () => { - int index; - while ((index = graph.nodes.IndexOf(node)) != graph.nodes.Count - 1) { - graph.nodes[index] = graph.nodes[index + 1]; - graph.nodes[index + 1] = node; - } - }); - contextMenu.AddItem(new GUIContent("Duplicate"), false, () => { - XNode.Node n = graph.CopyNode(node); - n.position = node.position + new Vector2(30, 30); - }); - contextMenu.AddItem(new GUIContent("Remove"), false, () => graph.RemoveNode(node)); + // If only one node is selected + if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { + XNode.Node node = Selection.activeObject as XNode.Node; + contextMenu.AddItem(new GUIContent("Move To Top"), false, () => { + int index; + while ((index = graph.nodes.IndexOf(node)) != graph.nodes.Count - 1) { + graph.nodes[index] = graph.nodes[index + 1]; + graph.nodes[index + 1] = node; + } + }); + } + + contextMenu.AddItem(new GUIContent("Duplicate"), false, DublicateSelectedNodes); + contextMenu.AddItem(new GUIContent("Remove"), false, RemoveSelectedNodes); + + // If only one node is selected + if (Selection.objects.Length == 1 && Selection.activeObject is XNode.Node) { + XNode.Node node = Selection.activeObject as XNode.Node; + AddCustomContextMenuItems(contextMenu, node); + } - AddCustomContextMenuItems(contextMenu, node); contextMenu.DropDown(new Rect(Event.current.mousePosition, Vector2.zero)); } @@ -168,17 +176,20 @@ namespace XNodeEditor { private void DrawNodes() { Event e = Event.current; + if (e.type == EventType.Layout) { + selectionCache = new List(Selection.objects); + } if (e.type == EventType.Repaint) { portConnectionPoints.Clear(); nodeWidths.Clear(); } - //Selected node is hashed before and after node GUI to detect changes + //Active node is hashed before and after node GUI to detect changes int nodeHash = 0; System.Reflection.MethodInfo onValidate = null; - if (selectedNode != null) { - onValidate = selectedNode.GetType().GetMethod("OnValidate"); - if (onValidate != null) nodeHash = selectedNode.GetHashCode(); + if (Selection.activeObject != null && Selection.activeObject is XNode.Node) { + onValidate = Selection.activeObject.GetType().GetMethod("OnValidate"); + if (onValidate != null) nodeHash = Selection.activeObject.GetHashCode(); } BeginZoomed(position, zoom); @@ -206,9 +217,23 @@ namespace XNodeEditor { GUILayout.BeginArea(new Rect(nodePos, new Vector2(nodeEditor.GetWidth(), 4000))); - GUIStyle style = NodeEditorResources.styles.nodeBody; - GUI.color = nodeEditor.GetTint(); - GUILayout.BeginVertical(new GUIStyle(style)); + bool selected = selectionCache.Contains(graph.nodes[n]); + + if (selected) { + GUIStyle style = new GUIStyle(NodeEditorResources.styles.nodeBody); + GUIStyle highlightStyle = new GUIStyle(NodeEditorResources.styles.nodeHighlight); + highlightStyle.padding = style.padding; + style.padding = new RectOffset(); + GUI.color = nodeEditor.GetTint(); + GUILayout.BeginVertical(new GUIStyle(style)); + GUI.color = Color.white; + GUILayout.BeginVertical(new GUIStyle(highlightStyle)); + } else { + GUIStyle style = NodeEditorResources.styles.nodeBody; + GUI.color = nodeEditor.GetTint(); + GUILayout.BeginVertical(new GUIStyle(style)); + } + GUI.color = guiColor; EditorGUI.BeginChangeCheck(); @@ -235,6 +260,7 @@ namespace XNodeEditor { } GUILayout.EndVertical(); + if (selected) GUILayout.EndVertical(); if (e.type != EventType.Layout) { //Check if we are hovering this node @@ -267,8 +293,8 @@ namespace XNodeEditor { //If a change in hash is detected in the selected node, call OnValidate method. //This is done through reflection because OnValidate is only relevant in editor, //and thus, the code should not be included in build. - if (selectedNode != null) { - if (onValidate != null && nodeHash != selectedNode.GetHashCode()) onValidate.Invoke(selectedNode, null); + if (nodeHash != 0) { + if (onValidate != null && nodeHash != Selection.activeObject.GetHashCode()) onValidate.Invoke(Selection.activeObject, null); } } diff --git a/Scripts/Editor/NodeEditorResources.cs b/Scripts/Editor/NodeEditorResources.cs index 490743b..9b55c8f 100644 --- a/Scripts/Editor/NodeEditorResources.cs +++ b/Scripts/Editor/NodeEditorResources.cs @@ -9,13 +9,15 @@ namespace XNodeEditor { private static Texture2D _dotOuter; public static Texture2D nodeBody { get { return _nodeBody != null ? _nodeBody : _nodeBody = Resources.Load("xnode_node"); } } private static Texture2D _nodeBody; + public static Texture2D nodeHighlight { get { return _nodeHighlight != null ? _nodeHighlight : _nodeHighlight = Resources.Load("xnode_node_highlight"); } } + private static Texture2D _nodeHighlight; // Styles public static Styles styles { get { return _styles != null ? _styles : _styles = new Styles(); } } public static Styles _styles = null; public class Styles { - public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip; + public GUIStyle inputPort, outputPort, nodeHeader, nodeBody, tooltip, nodeHighlight; public Styles() { GUIStyle baseStyle = new GUIStyle("Label"); @@ -39,6 +41,10 @@ namespace XNodeEditor { nodeBody.border = new RectOffset(32, 32, 32, 32); nodeBody.padding = new RectOffset(16, 16, 4, 16); + nodeHighlight = new GUIStyle(); + nodeHighlight.normal.background = NodeEditorResources.nodeHighlight; + nodeHighlight.border = new RectOffset(32, 32, 32, 32); + tooltip = new GUIStyle("helpBox"); tooltip.alignment = TextAnchor.MiddleCenter; } @@ -50,8 +56,8 @@ namespace XNodeEditor { for (int y = 0; y < 64; y++) { for (int x = 0; x < 64; x++) { Color col = bg; - if (y % 16 == 0 || x % 16 == 0) col = Color.Lerp(line,bg,0.65f); - if (y == 63 || x == 63) col = Color.Lerp(line,bg,0.35f); + if (y % 16 == 0 || x % 16 == 0) col = Color.Lerp(line, bg, 0.65f); + if (y == 63 || x == 63) col = Color.Lerp(line, bg, 0.35f); cols[(y * 64) + x] = col; } } diff --git a/Scripts/Editor/NodeEditorWindow.cs b/Scripts/Editor/NodeEditorWindow.cs index 3c79e71..7761a8f 100644 --- a/Scripts/Editor/NodeEditorWindow.cs +++ b/Scripts/Editor/NodeEditorWindow.cs @@ -77,8 +77,18 @@ namespace XNodeEditor { return new Vector2(xOffset, yOffset); } - public void SelectNode(XNode.Node node) { - selectedNode = node; + public void SelectNode(XNode.Node node, bool add) { + if (add) { + List selection = new List(Selection.objects); + selection.Add(node); + Selection.objects = selection.ToArray(); + } else Selection.activeObject = node; + } + + public void DeselectNode(XNode.Node node) { + List selection = new List(Selection.objects); + selection.Remove(node); + Selection.objects = selection.ToArray(); } [OnOpenAsset(0)] diff --git a/Scripts/Editor/Resources/xnode_node_highlight.png b/Scripts/Editor/Resources/xnode_node_highlight.png new file mode 100644 index 0000000..f1bb27f Binary files /dev/null and b/Scripts/Editor/Resources/xnode_node_highlight.png differ diff --git a/Scripts/Editor/Resources/xnode_node_highlight.png.meta b/Scripts/Editor/Resources/xnode_node_highlight.png.meta new file mode 100644 index 0000000..21b6034 --- /dev/null +++ b/Scripts/Editor/Resources/xnode_node_highlight.png.meta @@ -0,0 +1,87 @@ +fileFormatVersion: 2 +guid: 2ab2b92d7e1771b47bba0a46a6f0f6d5 +timeCreated: 1516610730 +licenseType: Free +TextureImporter: + fileIDToRecycleName: {} + externalObjects: {} + serializedVersion: 4 + mipmaps: + mipMapMode: 0 + enableMipMap: 0 + sRGBTexture: 0 + linearTexture: 0 + fadeOut: 0 + borderMipMap: 0 + mipMapsPreserveCoverage: 0 + alphaTestReferenceValue: 0.5 + mipMapFadeDistanceStart: 1 + mipMapFadeDistanceEnd: 3 + bumpmap: + convertToNormalMap: 0 + externalNormalMap: 0 + heightScale: 0.25 + normalMapFilter: 0 + isReadable: 0 + grayScaleToAlpha: 0 + generateCubemap: 6 + cubemapConvolution: 0 + seamlessCubemap: 0 + textureFormat: 1 + maxTextureSize: 2048 + textureSettings: + serializedVersion: 2 + filterMode: -1 + aniso: 1 + mipBias: -1 + wrapU: 1 + wrapV: 1 + wrapW: -1 + nPOTScale: 0 + lightmap: 0 + compressionQuality: 50 + spriteMode: 0 + spriteExtrude: 1 + spriteMeshType: 1 + alignment: 0 + spritePivot: {x: 0.5, y: 0.5} + spriteBorder: {x: 0, y: 0, z: 0, w: 0} + spritePixelsToUnits: 100 + alphaUsage: 1 + alphaIsTransparency: 1 + spriteTessellationDetail: -1 + textureType: 2 + textureShape: 1 + maxTextureSizeSet: 0 + compressionQualitySet: 0 + textureFormatSet: 0 + platformSettings: + - buildTarget: DefaultTexturePlatform + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + - buildTarget: Standalone + maxTextureSize: 2048 + resizeAlgorithm: 0 + textureFormat: -1 + textureCompression: 1 + compressionQuality: 50 + crunchedCompression: 0 + allowsAlphaSplitting: 0 + overridden: 0 + androidETC2FallbackOverride: 0 + spriteSheet: + serializedVersion: 2 + sprites: [] + outline: [] + physicsShape: [] + spritePackingTag: + userData: + assetBundleName: + assetBundleVariant: