using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using UnityEditor.Experimental.GraphView;
using System.Linq;
using System;
using UnityEditor.SceneManagement;
using System.Reflection;
using Status = UnityEngine.UIElements.DropdownMenuAction.Status;
using Object = UnityEngine.Object;
namespace GraphProcessor
{
///
/// Base class to write a custom view for a node
///
public class BaseGraphView : GraphView, IDisposable
{
public delegate void ComputeOrderUpdatedDelegate();
public delegate void NodeDuplicatedDelegate(BaseNode duplicatedNode, BaseNode newNode);
///
/// Graph that owns of the node
///
public BaseGraph graph;
///
/// Connector listener that will create the edges between ports
///
public BaseEdgeConnectorListener connectorListener;
///
/// List of all node views in the graph
///
///
///
public List< BaseNodeView > nodeViews = new List< BaseNodeView >();
///
/// Dictionary of the node views accessed view the node instance, faster than a Find in the node view list
///
///
///
///
public Dictionary< BaseNode, BaseNodeView > nodeViewsPerNode = new Dictionary< BaseNode, BaseNodeView >();
///
/// List of all edge views in the graph
///
///
///
public List< EdgeView > edgeViews = new List< EdgeView >();
///
/// List of all group views in the graph
///
///
///
public List< GroupView > groupViews = new List< GroupView >();
#if UNITY_2020_1_OR_NEWER
///
/// List of all sticky note views in the graph
///
///
///
public List< StickyNoteView > stickyNoteViews = new List();
#endif
///
/// List of all stack node views in the graph
///
///
///
public List< BaseStackNodeView > stackNodeViews = new List< BaseStackNodeView >();
Dictionary< Type, PinnedElementView > pinnedElements = new Dictionary< Type, PinnedElementView >();
CreateNodeMenuWindow createNodeMenu;
///
/// Triggered just after the graph is initialized
///
public event Action initialized;
///
/// Triggered just after the compute order of the graph is updated
///
public event ComputeOrderUpdatedDelegate computeOrderUpdated;
// Safe event relay from BaseGraph (safe because you are sure to always point on a valid BaseGraph
// when one of these events is called), a graph switch can occur between two call tho
///
/// Same event than BaseGraph.onExposedParameterListChanged
/// Safe event (not triggered in case the graph is null).
///
public event Action onExposedParameterListChanged;
///
/// Same event than BaseGraph.onExposedParameterModified
/// Safe event (not triggered in case the graph is null).
///
public event Action< ExposedParameter > onExposedParameterModified;
///
/// Triggered when a node is duplicated (crt-d) or copy-pasted (crtl-c/crtl-v)
///
public event NodeDuplicatedDelegate nodeDuplicated;
///
/// Object to handle nodes that shows their UI in the inspector.
///
[SerializeField]
protected NodeInspectorObject nodeInspector
{
get
{
if (graph.nodeInspectorReference == null)
graph.nodeInspectorReference = CreateNodeInspectorObject();
return graph.nodeInspectorReference as NodeInspectorObject;
}
}
///
/// Workaround object for creating exposed parameter property fields.
///
public ExposedParameterFieldFactory exposedParameterFactory { get; private set; }
public SerializedObject serializedGraph { get; private set; }
Dictionary nodeTypePerCreateAssetType = new Dictionary();
public BaseGraphView(EditorWindow window)
{
serializeGraphElements = SerializeGraphElementsCallback;
canPasteSerializedData = CanPasteSerializedDataCallback;
unserializeAndPaste = UnserializeAndPasteCallback;
graphViewChanged = GraphViewChangedCallback;
viewTransformChanged = ViewTransformChangedCallback;
elementResized = ElementResizedCallback;
RegisterCallback< KeyDownEvent >(KeyDownCallback);
RegisterCallback< DragPerformEvent >(DragPerformedCallback);
RegisterCallback< DragUpdatedEvent >(DragUpdatedCallback);
RegisterCallback< MouseDownEvent >(MouseDownCallback);
RegisterCallback< MouseUpEvent >(MouseUpCallback);
InitializeManipulators();
SetupZoom(0.05f, 2f);
Undo.undoRedoPerformed += ReloadView;
createNodeMenu = ScriptableObject.CreateInstance< CreateNodeMenuWindow >();
createNodeMenu.Initialize(this, window);
this.StretchToParentSize();
}
protected virtual NodeInspectorObject CreateNodeInspectorObject()
{
var inspector = ScriptableObject.CreateInstance();
inspector.name = "Node Inspector";
inspector.hideFlags = HideFlags.HideAndDontSave ^ HideFlags.NotEditable;
return inspector;
}
#region Callbacks
protected override bool canCopySelection
{
get { return selection.Any(e => e is BaseNodeView || e is GroupView); }
}
protected override bool canCutSelection
{
get { return selection.Any(e => e is BaseNodeView || e is GroupView); }
}
string SerializeGraphElementsCallback(IEnumerable elements)
{
var data = new CopyPasteHelper();
foreach (BaseNodeView nodeView in elements.Where(e => e is BaseNodeView))
{
data.copiedNodes.Add(JsonSerializer.SerializeNode(nodeView.nodeTarget));
foreach (var port in nodeView.nodeTarget.GetAllPorts())
{
if (port.portData.vertical)
{
foreach (var edge in port.GetEdges())
data.copiedEdges.Add(JsonSerializer.Serialize(edge));
}
}
}
foreach (GroupView groupView in elements.Where(e => e is GroupView))
data.copiedGroups.Add(JsonSerializer.Serialize(groupView.group));
foreach (EdgeView edgeView in elements.Where(e => e is EdgeView))
data.copiedEdges.Add(JsonSerializer.Serialize(edgeView.serializedEdge));
ClearSelection();
return JsonUtility.ToJson(data, true);
}
bool CanPasteSerializedDataCallback(string serializedData)
{
try {
return JsonUtility.FromJson(serializedData, typeof(CopyPasteHelper)) != null;
} catch {
return false;
}
}
void UnserializeAndPasteCallback(string operationName, string serializedData)
{
var data = JsonUtility.FromJson< CopyPasteHelper >(serializedData);
RegisterCompleteObjectUndo(operationName);
Dictionary copiedNodesMap = new Dictionary();
var unserializedGroups = data.copiedGroups.Select(g => JsonSerializer.Deserialize(g)).ToList();
foreach (var serializedNode in data.copiedNodes)
{
var node = JsonSerializer.DeserializeNode(serializedNode);
if (node == null)
continue ;
string sourceGUID = node.GUID;
graph.nodesPerGUID.TryGetValue(sourceGUID, out var sourceNode);
//Call OnNodeCreated on the new fresh copied node
node.createdFromDuplication = true;
node.createdWithinGroup = unserializedGroups.Any(g => g.innerNodeGUIDs.Contains(sourceGUID));
node.OnNodeCreated();
//And move a bit the new node
node.position.position += new Vector2(20, 20);
var newNodeView = AddNode(node);
// If the nodes were copied from another graph, then the source is null
if (sourceNode != null)
nodeDuplicated?.Invoke(sourceNode, node);
copiedNodesMap[sourceGUID] = node;
//Select the new node
AddToSelection(nodeViewsPerNode[node]);
}
foreach (var group in unserializedGroups)
{
//Same than for node
group.OnCreated();
// try to centre the created node in the screen
group.position.position += new Vector2(20, 20);
var oldGUIDList = group.innerNodeGUIDs.ToList();
group.innerNodeGUIDs.Clear();
foreach (var guid in oldGUIDList)
{
graph.nodesPerGUID.TryGetValue(guid, out var node);
// In case group was copied from another graph
if (node == null)
{
copiedNodesMap.TryGetValue(guid, out node);
group.innerNodeGUIDs.Add(node.GUID);
}
else
{
group.innerNodeGUIDs.Add(copiedNodesMap[guid].GUID);
}
}
AddGroup(group);
}
foreach (var serializedEdge in data.copiedEdges)
{
var edge = JsonSerializer.Deserialize(serializedEdge);
edge.Deserialize();
// Find port of new nodes:
copiedNodesMap.TryGetValue(edge.inputNode.GUID, out var oldInputNode);
copiedNodesMap.TryGetValue(edge.outputNode.GUID, out var oldOutputNode);
// We avoid to break the graph by replacing unique connections:
if (oldInputNode == null && !edge.inputPort.portData.acceptMultipleEdges || !edge.outputPort.portData.acceptMultipleEdges)
continue;
oldInputNode = oldInputNode ?? edge.inputNode;
oldOutputNode = oldOutputNode ?? edge.outputNode;
var inputPort = oldInputNode.GetPort(edge.inputPort.fieldName, edge.inputPortIdentifier);
var outputPort = oldOutputNode.GetPort(edge.outputPort.fieldName, edge.outputPortIdentifier);
var newEdge = SerializableEdge.CreateNewEdge(graph, inputPort, outputPort);
if (nodeViewsPerNode.ContainsKey(oldInputNode) && nodeViewsPerNode.ContainsKey(oldOutputNode))
{
var edgeView = CreateEdgeView();
edgeView.userData = newEdge;
edgeView.input = nodeViewsPerNode[oldInputNode].GetPortViewFromFieldName(newEdge.inputFieldName, newEdge.inputPortIdentifier);
edgeView.output = nodeViewsPerNode[oldOutputNode].GetPortViewFromFieldName(newEdge.outputFieldName, newEdge.outputPortIdentifier);
Connect(edgeView);
}
}
}
public virtual EdgeView CreateEdgeView()
{
return new EdgeView();
}
GraphViewChange GraphViewChangedCallback(GraphViewChange changes)
{
if (changes.elementsToRemove != null)
{
RegisterCompleteObjectUndo("Remove Graph Elements");
// Destroy priority of objects
// We need nodes to be destroyed first because we can have a destroy operation that uses node connections
changes.elementsToRemove.Sort((e1, e2) => {
int GetPriority(GraphElement e)
{
if (e is BaseNodeView)
return 0;
else
return 1;
}
return GetPriority(e1).CompareTo(GetPriority(e2));
});
//Handle ourselves the edge and node remove
changes.elementsToRemove.RemoveAll(e => {
switch (e)
{
case EdgeView edge:
Disconnect(edge);
return true;
case BaseNodeView nodeView:
// For vertical nodes, we need to delete them ourselves as it's not handled by GraphView
foreach (var pv in nodeView.inputPortViews.Concat(nodeView.outputPortViews))
if (pv.orientation == Orientation.Vertical)
foreach (var edge in pv.GetEdges().ToList())
Disconnect(edge);
nodeInspector.NodeViewRemoved(nodeView);
ExceptionToLog.Call(() => nodeView.OnRemoved());
graph.RemoveNode(nodeView.nodeTarget);
UpdateSerializedProperties();
RemoveElement(nodeView);
if (Selection.activeObject == nodeInspector)
UpdateNodeInspectorSelection();
SyncSerializedPropertyPathes();
return true;
case GroupView group:
graph.RemoveGroup(group.group);
UpdateSerializedProperties();
RemoveElement(group);
return true;
case ExposedParameterFieldView blackboardField:
graph.RemoveExposedParameter(blackboardField.parameter);
UpdateSerializedProperties();
return true;
case BaseStackNodeView stackNodeView:
graph.RemoveStackNode(stackNodeView.stackNode);
UpdateSerializedProperties();
RemoveElement(stackNodeView);
return true;
#if UNITY_2020_1_OR_NEWER
case StickyNoteView stickyNoteView:
graph.RemoveStickyNote(stickyNoteView.note);
UpdateSerializedProperties();
RemoveElement(stickyNoteView);
return true;
#endif
}
return false;
});
}
return changes;
}
void GraphChangesCallback(GraphChanges changes)
{
if (changes.removedEdge != null)
{
var edge = edgeViews.FirstOrDefault(e => e.serializedEdge == changes.removedEdge);
DisconnectView(edge);
}
}
void ViewTransformChangedCallback(GraphView view)
{
if (graph != null)
{
graph.position = viewTransform.position;
graph.scale = viewTransform.scale;
}
}
void ElementResizedCallback(VisualElement elem)
{
var groupView = elem as GroupView;
if (groupView != null)
groupView.group.size = groupView.GetPosition().size;
}
public override List< Port > GetCompatiblePorts(Port startPort, NodeAdapter nodeAdapter)
{
var compatiblePorts = new List< Port >();
compatiblePorts.AddRange(ports.ToList().Where(p => {
var portView = p as PortView;
if (portView.owner == (startPort as PortView).owner)
return false;
if (p.direction == startPort.direction)
return false;
//Check for type assignability
if (!BaseGraph.TypesAreConnectable(startPort.portType, p.portType))
return false;
//Check if the edge already exists
if (portView.GetEdges().Any(e => e.input == startPort || e.output == startPort))
return false;
return true;
}));
return compatiblePorts;
}
///
/// Build the contextual menu shown when right clicking inside the graph view
///
///
public override void BuildContextualMenu(ContextualMenuPopulateEvent evt)
{
base.BuildContextualMenu(evt);
BuildGroupContextualMenu(evt, 1);
BuildStickyNoteContextualMenu(evt, 2);
BuildViewContextualMenu(evt);
BuildSelectAssetContextualMenu(evt);
BuildSaveAssetContextualMenu(evt);
BuildHelpContextualMenu(evt);
}
///
/// Add the New Group entry to the context menu
///
///
protected virtual void BuildGroupContextualMenu(ContextualMenuPopulateEvent evt, int menuPosition = -1)
{
if (menuPosition == -1)
menuPosition = evt.menu.MenuItems().Count;
Vector2 position = (evt.currentTarget as VisualElement).ChangeCoordinatesTo(contentViewContainer, evt.localMousePosition);
evt.menu.InsertAction(menuPosition, "Create Group", (e) => AddSelectionsToGroup(AddGroup(new Group("Create Group", position))), DropdownMenuAction.AlwaysEnabled);
}
///
/// -Add the New Sticky Note entry to the context menu
///
///
protected virtual void BuildStickyNoteContextualMenu(ContextualMenuPopulateEvent evt, int menuPosition = -1)
{
if (menuPosition == -1)
menuPosition = evt.menu.MenuItems().Count;
#if UNITY_2020_1_OR_NEWER
Vector2 position = (evt.currentTarget as VisualElement).ChangeCoordinatesTo(contentViewContainer, evt.localMousePosition);
evt.menu.InsertAction(menuPosition, "Create Sticky Note", (e) => AddStickyNote(new StickyNote("Create Note", position)), DropdownMenuAction.AlwaysEnabled);
#endif
}
///
/// Add the View entry to the context menu
///
///
protected virtual void BuildViewContextualMenu(ContextualMenuPopulateEvent evt)
{
evt.menu.AppendAction("View/Processor", (e) => ToggleView< ProcessorView >(), (e) => GetPinnedElementStatus< ProcessorView >());
}
///
/// Add the Select Asset entry to the context menu
///
///
protected virtual void BuildSelectAssetContextualMenu(ContextualMenuPopulateEvent evt)
{
evt.menu.AppendAction("Select Asset", (e) => EditorGUIUtility.PingObject(graph), DropdownMenuAction.AlwaysEnabled);
}
///
/// Add the Save Asset entry to the context menu
///
///
protected virtual void BuildSaveAssetContextualMenu(ContextualMenuPopulateEvent evt)
{
evt.menu.AppendAction("Save Asset", (e) => {
EditorUtility.SetDirty(graph);
AssetDatabase.SaveAssets();
}, DropdownMenuAction.AlwaysEnabled);
}
///
/// Add the Help entry to the context menu
///
///
protected void BuildHelpContextualMenu(ContextualMenuPopulateEvent evt)
{
evt.menu.AppendAction("Help/Reset Pinned Windows", e => {
foreach (var kp in pinnedElements)
kp.Value.ResetPosition();
});
}
protected virtual void KeyDownCallback(KeyDownEvent e)
{
if (e.keyCode == KeyCode.S && e.commandKey)
{
SaveGraphToDisk();
e.StopPropagation();
}
else if(nodeViews.Count > 0 && e.commandKey && e.altKey)
{
// Node Aligning shortcuts
switch(e.keyCode)
{
case KeyCode.LeftArrow:
nodeViews[0].AlignToLeft();
e.StopPropagation();
break;
case KeyCode.RightArrow:
nodeViews[0].AlignToRight();
e.StopPropagation();
break;
case KeyCode.UpArrow:
nodeViews[0].AlignToTop();
e.StopPropagation();
break;
case KeyCode.DownArrow:
nodeViews[0].AlignToBottom();
e.StopPropagation();
break;
case KeyCode.C:
nodeViews[0].AlignToCenter();
e.StopPropagation();
break;
case KeyCode.M:
nodeViews[0].AlignToMiddle();
e.StopPropagation();
break;
}
}
}
void MouseUpCallback(MouseUpEvent e)
{
schedule.Execute(() => {
if (DoesSelectionContainsInspectorNodes())
UpdateNodeInspectorSelection();
}).ExecuteLater(1);
}
void MouseDownCallback(MouseDownEvent e)
{
// When left clicking on the graph (not a node or something else)
if (e.button == 0)
{
// Close all settings windows:
nodeViews.ForEach(v => v.CloseSettings());
}
if (DoesSelectionContainsInspectorNodes())
UpdateNodeInspectorSelection();
}
bool DoesSelectionContainsInspectorNodes()
{
var selectedNodes = selection.Where(s => s is BaseNodeView).ToList();
var selectedNodesNotInInspector = selectedNodes.Except(nodeInspector.selectedNodes).ToList();
var nodeInInspectorWithoutSelectedNodes = nodeInspector.selectedNodes.Except(selectedNodes).ToList();
return selectedNodesNotInInspector.Any() || nodeInInspectorWithoutSelectedNodes.Any();
}
void DragPerformedCallback(DragPerformEvent e)
{
var mousePos = (e.currentTarget as VisualElement).ChangeCoordinatesTo(contentViewContainer, e.localMousePosition);
var dragData = DragAndDrop.GetGenericData("DragSelection") as List< ISelectable >;
// Drag and Drop for elements inside the graph
if (dragData != null)
{
var exposedParameterFieldViews = dragData.OfType();
if (exposedParameterFieldViews.Any())
{
foreach (var paramFieldView in exposedParameterFieldViews)
{
RegisterCompleteObjectUndo("Create Parameter Node");
var paramNode = BaseNode.CreateFromType< ParameterNode >(mousePos);
paramNode.parameterGUID = paramFieldView.parameter.guid;
AddNode(paramNode);
}
}
}
// External objects drag and drop
if (DragAndDrop.objectReferences.Length > 0)
{
RegisterCompleteObjectUndo("Create Node From Object(s)");
foreach (var obj in DragAndDrop.objectReferences)
{
var objectType = obj.GetType();
foreach (var kp in nodeTypePerCreateAssetType)
{
if (kp.Key.IsAssignableFrom(objectType))
{
try
{
var node = BaseNode.CreateFromType(kp.Value.nodeType, mousePos);
if ((bool)kp.Value.initalizeNodeFromObject.Invoke(node, new []{obj}))
{
AddNode(node);
break;
}
}
catch (Exception exception)
{
Debug.LogException(exception);
}
}
}
}
}
}
void DragUpdatedCallback(DragUpdatedEvent e)
{
var dragData = DragAndDrop.GetGenericData("DragSelection") as List;
var dragObjects = DragAndDrop.objectReferences;
bool dragging = false;
if (dragData != null)
{
// Handle drag from exposed parameter view
if (dragData.OfType().Any())
{
dragging = true;
}
}
if (dragObjects.Length > 0)
dragging = true;
if (dragging)
DragAndDrop.visualMode = DragAndDropVisualMode.Generic;
UpdateNodeInspectorSelection();
}
#endregion
#region Initialization
void ReloadView()
{
// Force the graph to reload his data (Undo have updated the serialized properties of the graph
// so the one that are not serialized need to be synchronized)
graph.Deserialize();
// Get selected nodes
var selectedNodeGUIDs = new List();
foreach (var e in selection)
{
if (e is BaseNodeView v && this.Contains(v))
selectedNodeGUIDs.Add(v.nodeTarget.GUID);
}
// Remove everything
RemoveNodeViews();
RemoveEdges();
RemoveGroups();
#if UNITY_2020_1_OR_NEWER
RemoveStrickyNotes();
#endif
RemoveStackNodeViews();
UpdateSerializedProperties();
// And re-add with new up to date datas
InitializeNodeViews();
InitializeEdgeViews();
InitializeGroups();
InitializeStickyNotes();
InitializeStackNodes();
Reload();
UpdateComputeOrder();
// Restore selection after re-creating all views
// selection = nodeViews.Where(v => selectedNodeGUIDs.Contains(v.nodeTarget.GUID)).Select(v => v as ISelectable).ToList();
foreach (var guid in selectedNodeGUIDs)
{
AddToSelection(nodeViews.FirstOrDefault(n => n.nodeTarget.GUID == guid));
}
UpdateNodeInspectorSelection();
}
public void Initialize(BaseGraph graph)
{
if (this.graph != null)
{
SaveGraphToDisk();
// Close pinned windows from old graph:
ClearGraphElements();
NodeProvider.UnloadGraph(graph);
}
this.graph = graph;
exposedParameterFactory = new ExposedParameterFieldFactory(graph);
UpdateSerializedProperties();
connectorListener = CreateEdgeConnectorListener();
// When pressing ctrl-s, we save the graph
EditorSceneManager.sceneSaved += _ => SaveGraphToDisk();
RegisterCallback(e => {
if (e.keyCode == KeyCode.S && e.actionKey)
SaveGraphToDisk();
});
ClearGraphElements();
InitializeGraphView();
InitializeNodeViews();
InitializeEdgeViews();
InitializeViews();
InitializeGroups();
InitializeStickyNotes();
InitializeStackNodes();
initialized?.Invoke();
UpdateComputeOrder();
InitializeView();
NodeProvider.LoadGraph(graph);
// Register the nodes that can be created from assets
foreach (var nodeInfo in NodeProvider.GetNodeMenuEntries(graph))
{
var interfaces = nodeInfo.type.GetInterfaces();
var exceptInheritedInterfaces = interfaces.Except(interfaces.SelectMany(t => t.GetInterfaces()));
foreach (var i in interfaces)
{
if (i.IsGenericType && i.GetGenericTypeDefinition() == typeof(ICreateNodeFrom<>))
{
var genericArgumentType = i.GetGenericArguments()[0];
var initializeFunction = nodeInfo.type.GetMethod(
nameof(ICreateNodeFrom