// Copyright (c) 2015 - 2023 Doozy Entertainment. All Rights Reserved. // This code can only be used under the standard Unity Asset Store End User License Agreement // A Copy of the EULA APPENDIX 1 is available at http://unity3d.com/company/legal/as_terms using System; using System.Collections.Generic; using System.Linq; using Doozy.Runtime.Common.Extensions; using Doozy.Runtime.Nody; using Doozy.Runtime.Nody.Nodes.Internal; using Doozy.Runtime.Nody.Nodes.System; using Doozy.Runtime.UIElements.Extensions; using UnityEditor; using UnityEditor.Experimental.GraphView; using UnityEngine; using UnityEngine.UIElements; using FlowGraph = Doozy.Runtime.Nody.FlowGraph; using Object = UnityEngine.Object; // ReSharper disable MemberCanBePrivate.Global // ReSharper disable UnusedAutoPropertyAccessor.Local // ReSharper disable ForCanBeConvertedToForeach // ReSharper disable LoopCanBeConvertedToQuery namespace Doozy.Editor.Nody { public class FlowGraphView : GraphView { public Action OnNodeSelected; public FlowGraph flowGraph { get; internal set; } public NodyNodeSearchWindow searchWindow { get; private set; } private GridBackground gridBackground { get; set; } public Vector2 cachedMousePosition { get; private set; } public Vector2 graphMousePosition => contentViewContainer.WorldToLocal(cachedMousePosition); public ContentDragger contentDragger { get; } public SelectionDragger selectionDragger { get; } public RectangleSelector rectangleSelector { get; } private IEnumerable portViews => ports.ToList().Cast(); public FlowGraphView() { InjectGridBackground(); CreateSearchWindow(); this.SetStyleFlexGrow(1); this.AddManipulator(contentDragger = new ContentDragger()); this.AddManipulator(selectionDragger = new SelectionDragger()); this.AddManipulator(rectangleSelector = new RectangleSelector()); SetupZoom(0.1f, 5f); RegisterCallback(OnMouseMoveEvent); Undo.undoRedoPerformed -= UndoRedoPerformed; Undo.undoRedoPerformed += UndoRedoPerformed; } public void ClearGraphView(bool clearInspector) { graphViewChanged -= OnGraphViewChanged; DeleteElements(graphElements.ToList()); graphViewChanged += OnGraphViewChanged; if (clearInspector) ClearInspector(); } public void RefreshNodeViews() { if (flowGraph == null) return; graphViewChanged -= OnGraphViewChanged; DeleteElements(nodes); graphViewChanged += OnGraphViewChanged; for (int i = 0; i < flowGraph.nodes.Count; i++) { FlowNode node = flowGraph.nodes[i]; FlowNodeView nodeView = CreateNodeView(node); nodeView.UpdatePresenterPosition(); } } public void RefreshEdges() { if (flowGraph == null) return; graphViewChanged -= OnGraphViewChanged; DeleteElements(edges); graphViewChanged += OnGraphViewChanged; for (int i = 0; i < flowGraph.outputPorts.Count; i++) { FlowPort outputPort = flowGraph.outputPorts[i]; if (!outputPort.isConnected) continue; FlowPortView outputPortView = GetPortView(outputPort); if (outputPortView == null) continue; for (int j = 0; j < outputPort.connections.Count; j++) { string inputPortId = outputPort.connections[j]; FlowPortView inputPortView = GetPortView(inputPortId); if (inputPortView == null) continue; FlowEdgeView edgeView = outputPortView.ConnectTo(inputPortView); AddElement(edgeView); } } } public FlowNode CreateNode(Type type, bool createView = false, bool recordUndo = true) { if (flowGraph == null) return null; var node = ScriptableObject.CreateInstance(type) as FlowNode; Debug.Assert(node != null, nameof(node) + " != null"); node.name = ObjectNames.NicifyVariableName(type.Name.Replace("Node", "")); if (recordUndo) Undo.RecordObject(flowGraph, $"{ObjectNames.NicifyVariableName(nameof(CreateNode))}"); flowGraph.nodes.Add(node); node.SetFlowGraph(flowGraph); node.SetPosition(graphMousePosition); EditorUtility.SetDirty(flowGraph); AssetDatabase.AddObjectToAsset(node, flowGraph); if (recordUndo) Undo.RegisterCreatedObjectUndo(node, $"{ObjectNames.NicifyVariableName(nameof(CreateNode))}"); // AssetDatabase.SaveAssets(); AssetDatabase.SaveAssetIfDirty(node); AssetDatabase.SaveAssetIfDirty(flowGraph); if (createView) CreateNodeView(node); return node; } public FlowNodeView GetNodeView(FlowNode flowNode) { return GetNodeByGuid(flowNode.nodeId) as FlowNodeView; } public void CenterGraphOnNode(FlowNode node, bool selectNode = false) { schedule.Execute(() => { if (flowGraph == null) return; if (node == null) return; FlowNodeView nodeView = GetNodeView(node); ClearSelection(); AddToSelection(nodeView); FrameSelection(); if (selectNode) return; ClearSelection(); }); } public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) { // Vector2 mousePosition = evt.mousePosition; if (evt.target is FlowPortView flowPortView) { evt.menu.AppendAction ( "Delete", dropdownMenuAction => DeletePort(flowPortView), dropdownMenuAction => flowPortView.canDeletePort ? DropdownMenuAction.Status.Normal : DropdownMenuAction.Status.Disabled ); return; } base.BuildContextualMenu(evt); if (evt.target is GraphView || evt.target is Node || evt.target is Group) { if (evt.target is FlowNodeView) { evt.menu.RemoveItemAt(1); evt.menu.RemoveItemAt(1); evt.menu.RemoveItemAt(1); evt.menu.RemoveItemAt(1); evt.menu.RemoveItemAt(2); evt.menu.RemoveItemAt(2); } else { evt.menu.RemoveItemAt(1); evt.menu.RemoveItemAt(1); evt.menu.RemoveItemAt(1); evt.menu.RemoveItemAt(1); evt.menu.RemoveItemAt(1); evt.menu.RemoveItemAt(3); } } } public override void AddToSelection(ISelectable selectable) { base.AddToSelection(selectable); if (selection.Count > 1) { OnNodeSelected?.Invoke(null); return; } if (selectable is FlowNodeView nodeView) OnNodeSelected?.Invoke(nodeView); } public override void RemoveFromSelection(ISelectable selectable) { base.RemoveFromSelection(selectable); if (selection.Count > 1) OnNodeSelected?.Invoke(null); switch (selection.LastOrDefault()) { case null: return; case FlowNodeView nodeView: OnNodeSelected?.Invoke(nodeView); return; default: OnNodeSelected?.Invoke(null); break; } } public override void ClearSelection() { base.ClearSelection(); OnNodeSelected?.Invoke(null); } public override EventPropagation DeleteSelection() { EventPropagation result = base.DeleteSelection(); OnNodeSelected?.Invoke(null); return result; } public override List GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter) { return ports .ToList() .Where(endPort => endPort.direction != startPort.direction && endPort.node != startPort.node) .ToList(); } internal void PopulateView(FlowGraph graphReference) { ClearGraphView(true); // clear the view (from the previous graph) flowGraph = graphReference; // set graph reference if (flowGraph == null) return; // graph null -> stop CleanGraph(); // clean graph (remove nulls) AddRootNode(); // add root node (if missing) flowGraph.UpdateNodes(); // update nodes RefreshNodeViews(); // create node views RefreshEdges(); // create edges } private void InjectGridBackground() { Insert(0, gridBackground = new GridBackground()); //ToDo: check if a simple image would not be better as the background // gridBackground.RegisterCallback(evt => // { // typeof(GridBackground).GetField("m_GridBackgroundColor", BindingFlags.NonPublic | BindingFlags.Instance)? // .SetValue(gridBackground, EditorColors.Nody.GridBackground); // // typeof(GridBackground).GetField("m_LineColor", BindingFlags.NonPublic | BindingFlags.Instance)? // .SetValue(gridBackground, EditorColors.Nody.LineColor); // // typeof(GridBackground).GetField("m_ThickLineColor", BindingFlags.NonPublic | BindingFlags.Instance)? // .SetValue(gridBackground, EditorColors.Nody.ThickLineColor); // // typeof(GridBackground).GetField("m_Spacing", BindingFlags.NonPublic | BindingFlags.Instance)? // .SetValue(gridBackground, 12); // }); } private void CreateSearchWindow() { searchWindow = ScriptableObject .CreateInstance() .SetGraphView(this); nodeCreationRequest = context => SearchWindow.Open ( new SearchWindowContext(context.screenMousePosition), searchWindow ); } private void OnMouseMoveEvent(IMouseEvent evt) { cachedMousePosition = evt.mousePosition; } private void UndoRedoPerformed() { // Debug.Log($"UndoRedoPerformed"); // ClearGraphView(false); //clear graph view if (flowGraph == null) return; //stop if no graph is loaded var tempSelection = selection.ToList(); //save current selected flowGraph.UpdateNodes(); //update nodes RefreshNodeViews(); //create node views RefreshEdges(); //create edges EditorUtility.SetDirty(flowGraph); //mark graph as dirty tempSelection.RemoveNulls(); //remove nulls from temp selection (in case a selected thing got deleted) tempSelection.ForEach(AddToSelection); //add temp back to selection // AssetDatabase.SaveAssetIfDirty(flowGraph); // AssetDatabase.SaveAssets(); // NodyInspectorWindow.instance.UpdateSelection((FlowNodeView)selection.FirstOrDefault()); } /// Deselect current loaded flowGraph and any of its node (if selected) private void ClearInspector() { if (flowGraph == null) return; if (Selection.activeObject == flowGraph) //check if the graph asset is selected in the Inspector -> if it is -> deselect the graph { Selection.activeObject = null; } else //the graph was not selected in the Inspector -> check each node { for (int i = 0; i < flowGraph.nodes.Count; i++) { FlowNode node = flowGraph.nodes[i]; if (Selection.activeObject != node) continue; Selection.activeObject = null; //one of the graph's nodes was selected in the Inspector -> deselect the node break; } } } private FlowPortView GetPortView(FlowPort flowPort) { foreach (FlowPortView portView in portViews) if (portView.flowPort == flowPort) return portView; return null; } private FlowPortView GetPortView(string portId) { foreach (FlowPortView portView in portViews) if (portView.flowPort.portId == portId) return portView; return null; } private GraphViewChange OnGraphViewChanged(GraphViewChange graphViewChange) { graphViewChange.elementsToRemove?.ToList().ForEach(element => { switch (element) { case null: return; //Edge Removal case FlowEdgeView edgeView: DisconnectPorts(edgeView); break; //Node Removal case FlowNodeView nodeView: DeleteNode(nodeView); break; } }); //Edge Creation graphViewChange.edgesToCreate?.ToList().ForEach(edge => { if (!(edge is FlowEdgeView edgeView)) return; var outputPortView = (FlowPortView)edgeView.output; if (outputPortView == null) return; edgeView.outputPortView = outputPortView; var inputPortView = (FlowPortView)edgeView.input; if (inputPortView == null) return; edgeView.inputPortView = inputPortView; ConnectPorts(edgeView.outputPortView.flowPort, inputPortView.flowPort); }); // AssetDatabase.SaveAssets(); return graphViewChange; } private void ConnectPorts(FlowPort outputPort, FlowPort inputPort) { Undo.RecordObjects(new Object[] { outputPort.node, inputPort.node }, "Connect"); if (outputPort.acceptsOnlyOneConnection) if (outputPort.isConnected) NodyUtils.DisconnectPort(outputPort, flowGraph); if (inputPort.acceptsOnlyOneConnection) if (inputPort.isConnected) NodyUtils.DisconnectPort(inputPort, flowGraph); outputPort.connections.Add(inputPort.portId); outputPort.onConnected?.Invoke(inputPort); inputPort.connections.Add(outputPort.portId); inputPort.onConnected?.Invoke(outputPort); EditorUtility.SetDirty(outputPort.node); EditorUtility.SetDirty(inputPort.node); } private void DeleteNode(FlowNodeView nodeView) { if (flowGraph == null) return; //null graph -> stop if (nodeView == null) return; //null node view -> stop if (nodeView.flowNode == null) return; //null node reference -> stop if (nodeView.flowNode.canBeDeleted == false) return; //node marked not to be deleted -> stop if (nodeView.flowNode.nodeType == NodeType.System) return; //node is system node -> stop Undo.RecordObject(flowGraph, "Delete Node"); //save undo for graph NodyUtils.DisconnectNode(nodeView.flowNode, flowGraph); //disconnect node flowGraph.nodes.Remove(nodeView.flowNode); //remove node from graph EditorUtility.SetDirty(flowGraph); //mark graph as dirty Undo.DestroyObjectImmediate(nodeView.flowNode); //save undo for node and destroy the node (asset) // AssetDatabase.SaveAssets(); //save assets AssetDatabase.SaveAssetIfDirty(flowGraph); } private void DeletePort(FlowPortView portView) { if (flowGraph == null) return; //null graph -> stop if (portView == null) return; //null port view -> stop if (portView.flowPort == null) return; //null port reference -> stop if (portView.flowNode == null) return; //null node reference -> stop FlowPort port = portView.flowPort; //target port FlowNode node = portView.flowNode; //target node if (!node.CanDeletePort(port.portId)) return; //cannot delete port -> stop //port is valid and can be deleted if (port.isConnected) //port is connected -> need to disconnect first and mark everything for undo { var nodeViews = new List(); var flowNodes = new List(); var otherPorts = new List(); for (int i = 0; i < port.connections.Count; i++) { string otherPortId = port.connections[i]; FlowPortView otherPortView = GetPortView(otherPortId); //get other port's view FlowPort otherPort = otherPortView?.flowPort; //get a reference to the other port if (otherPort == null) continue; //null port -> skip this id otherPorts.Add(otherPort); //add other ports (to disconnect) flowNodes.Add(otherPort.node); //add other nodes (to save for undo and mark as dirty) nodeViews.Add(otherPortView.nodeView); } flowNodes.Add(node); //add the node to the undo objects list nodeViews.Add(portView.nodeView); // ReSharper disable once CoVariantArrayConversion Undo.RecordObjects(flowNodes.ToArray(), "Delete Port"); //save undo for all the nodes for (int i = 0; i < otherPorts.Count; i++) //disconnect all ports { FlowPort otherPort = otherPorts[i]; NodyUtils.DisconnectPortFromPort(port, otherPort); } port.connections.Clear(); //sanity check - remove all connection port ids from target port node.DeletePort(port.portId); //delete the port for (int i = 0; i < flowNodes.Count; i++) { FlowNode n = flowNodes[i]; EditorUtility.SetDirty(n); //mark all nodes as dirty AssetDatabase.SaveAssetIfDirty(n); //save this s$%t } // AssetDatabase.SaveAssets(); for (int i = 0; i < nodeViews.Count; i++) { FlowNodeView nodeView = nodeViews[i]; nodeView.RefreshNodeView(); //refresh node views nodeView.RefreshPortsViews(); //refresh node port views } // if (NodyInspectorWindow.isOpen) // inspector.Refresh(); return; //stop here } //port wasn't connected -> mark for undo and delete it Undo.RecordObject(node, "Delete Port"); //save undo for the node node.DeletePort(port.portId); //delete the port EditorUtility.SetDirty(node); //mark node as dirty AssetDatabase.SaveAssetIfDirty(node); //save this shit portView.nodeView.RefreshNodeView(); //refresh node views portView.nodeView.RefreshPortsViews(); //refresh node port views // if (NodyInspectorWindow.isOpen) // inspector.Refresh(); } private static void DisconnectPorts(FlowEdgeView edgeView) { if (edgeView?.outputPortView?.flowPort == null) return; if (edgeView.inputPortView?.flowPort == null) return; DisconnectPorts(edgeView.outputPortView.flowPort, edgeView.inputPortView.flowPort); edgeView.outputPortView.nodeView.RefreshNodeView(); edgeView.inputPortView.nodeView.RefreshNodeView(); } private static void DisconnectPorts(FlowPort p1, FlowPort p2) { Undo.RecordObjects(new Object[] { p1.node, p2.node }, "Disconnect"); NodyUtils.DisconnectPortFromPort(p1, p2); EditorUtility.SetDirty(p1.node); EditorUtility.SetDirty(p2.node); } private void AddRootNode() //ToDo: add subgraph settings (EnterNode instead of StartNode) { if (flowGraph == null) return; if (flowGraph.rootNode != null) return; for (int i = 0; i < flowGraph.nodes.Count; i++) { FlowNode node = flowGraph.nodes[i]; if (!(node is StartNode rootNode)) continue; flowGraph.rootNode = rootNode; EditorUtility.SetDirty(flowGraph); AssetDatabase.SaveAssetIfDirty(flowGraph); break; } if (flowGraph.rootNode != null) return; flowGraph.rootNode = CreateNode(typeof(StartNode), false, false).SetPosition(Vector2.zero); flowGraph.nodes.Remove(flowGraph.rootNode); flowGraph.nodes.Insert(0, flowGraph.rootNode); EditorUtility.SetDirty(flowGraph); AssetDatabase.SaveAssetIfDirty(flowGraph); FrameAll(); } private FlowNodeView CreateNodeView(FlowNode node) { var nodeView = FlowNodeView.GetView(this, node); nodeView.OnNodeSelected = OnNodeSelected; AddElement(nodeView); switch (flowGraph.graphState) { case GraphState.Running: case GraphState.Paused: if (node is GlobalNode globalNode) { nodeView.UpdateNodeState(NodeState.Running); } if (nodeView.flowNode.flowGraph.activeNode == node) { nodeView.UpdateNodeState(NodeState.Active); } break; case GraphState.Idle: //ignored break; } return nodeView; } private void CleanGraph() { if (flowGraph == null) return; bool flowIsDirty = false; //remove null nodes for (int i = flowGraph.nodes.Count - 1; i >= 0; i--) { if (flowGraph.nodes[i] != null) continue; flowGraph.nodes.RemoveAt(i); flowIsDirty = true; } if (flowIsDirty) { EditorUtility.SetDirty(flowGraph); // RefreshGraphView(); } } } }