using System; using System.Collections.Generic; using System.Linq; using UnityEditor.Experimental.UIElements.GraphView; using UnityEditor.Graphing.Util; using UnityEngine; using UnityEditor.Graphing; using UnityEngine.Experimental.UIElements; using Edge = UnityEditor.Experimental.UIElements.GraphView.Edge; namespace UnityEditor.ShaderGraph.Drawing { public sealed class MaterialGraphView : GraphView, IDropTarget { public MaterialGraphView() { AddStyleSheetPath("Styles/MaterialGraphView"); serializeGraphElements = SerializeGraphElementsImplementation; canPasteSerializedData = CanPasteSerializedDataImplementation; unserializeAndPaste = UnserializeAndPasteImplementation; deleteSelection = DeleteSelectionImplementation; } public MaterialGraphView(AbstractMaterialGraph graph) : this() { this.graph = graph; } public AbstractMaterialGraph graph { get; private set; } public Action onConvertToSubgraphClick { get; set; } public override List GetCompatiblePorts(Port startAnchor, NodeAdapter nodeAdapter) { var compatibleAnchors = new List(); var startSlot = startAnchor.GetSlot(); if (startSlot == null) return compatibleAnchors; var startStage = startSlot.shaderStage; if (startStage == ShaderStage.Dynamic) startStage = NodeUtils.FindEffectiveShaderStage(startSlot.owner, startSlot.isOutputSlot); foreach (var candidateAnchor in ports.ToList()) { var candidateSlot = candidateAnchor.GetSlot(); if (!startSlot.IsCompatibleWith(candidateSlot)) continue; if (startStage != ShaderStage.Dynamic) { var candidateStage = candidateSlot.shaderStage; if (candidateStage == ShaderStage.Dynamic) candidateStage = NodeUtils.FindEffectiveShaderStage(candidateSlot.owner, !startSlot.isOutputSlot); if (candidateStage != ShaderStage.Dynamic && candidateStage != startStage) continue; } compatibleAnchors.Add(candidateAnchor); } return compatibleAnchors; } public override void BuildContextualMenu(ContextualMenuPopulateEvent evt) { base.BuildContextualMenu(evt); if (evt.target is GraphView || evt.target is Node) { evt.menu.AppendAction("Convert To Sub-graph", ConvertToSubgraph, ConvertToSubgraphStatus); evt.menu.AppendAction("Convert To Inline Node", ConvertToInlineNode, ConvertToInlineNodeStatus); evt.menu.AppendAction("Convert To Property", ConvertToProperty, ConvertToPropertyStatus); if (selection.OfType().Count() == 1) { evt.menu.AppendSeparator(); evt.menu.AppendAction("Open Documentation", SeeDocumentation, SeeDocumentationStatus); } if (selection.OfType().Count() == 1 && selection.OfType().First().node is SubGraphNode) { evt.menu.AppendSeparator(); evt.menu.AppendAction("Open Sub Graph", OpenSubGraph, ContextualMenu.MenuAction.AlwaysEnabled); } } else if (evt.target is BlackboardField) { evt.menu.AppendAction("Delete", (e) => DeleteSelectionImplementation("Delete", AskUser.DontAskUser), (e) => canDeleteSelection ? ContextualMenu.MenuAction.StatusFlags.Normal : ContextualMenu.MenuAction.StatusFlags.Disabled); } if (evt.target is MaterialGraphView) { evt.menu.AppendAction("Collapse Previews", CollapsePreviews, ContextualMenu.MenuAction.AlwaysEnabled); evt.menu.AppendAction("Expand Previews", ExpandPreviews, ContextualMenu.MenuAction.AlwaysEnabled); evt.menu.AppendSeparator(); } } void CollapsePreviews(EventBase evt) { graph.owner.RegisterCompleteObjectUndo("Collapse Previews"); foreach (AbstractMaterialNode node in graph.GetNodes()) { node.previewExpanded = false; } } void ExpandPreviews(EventBase evt) { graph.owner.RegisterCompleteObjectUndo("Expand Previews"); foreach (AbstractMaterialNode node in graph.GetNodes()) { node.previewExpanded = true; } } void SeeDocumentation(EventBase evt) { var node = selection.OfType().First().node; if (node.documentationURL != null) System.Diagnostics.Process.Start(node.documentationURL); } void OpenSubGraph(EventBase evt) { SubGraphNode subgraphNode = selection.OfType().First().node as SubGraphNode; var path = AssetDatabase.GetAssetPath(subgraphNode.subGraphAsset); ShaderGraphImporterEditor.ShowGraphEditWindow(path); } ContextualMenu.MenuAction.StatusFlags SeeDocumentationStatus(EventBase eventBase) { if (selection.OfType().First().node.documentationURL == null) return ContextualMenu.MenuAction.StatusFlags.Disabled; return ContextualMenu.MenuAction.StatusFlags.Normal; } ContextualMenu.MenuAction.StatusFlags ConvertToPropertyStatus(EventBase eventBase) { if (selection.OfType().Any(v => v.node != null)) { if (selection.OfType().Any(v => v.node is IPropertyFromNode)) return ContextualMenu.MenuAction.StatusFlags.Normal; return ContextualMenu.MenuAction.StatusFlags.Disabled; } return ContextualMenu.MenuAction.StatusFlags.Hidden; } void ConvertToProperty(EventBase eventBase) { var selectedNodeViews = selection.OfType().Select(x => x.node).ToList(); foreach (var node in selectedNodeViews) { if (!(node is IPropertyFromNode)) continue; var converter = node as IPropertyFromNode; var prop = converter.AsShaderProperty(); graph.AddShaderProperty(prop); var propNode = new PropertyNode(); propNode.drawState = node.drawState; graph.AddNode(propNode); propNode.propertyGuid = prop.guid; var oldSlot = node.FindSlot(converter.outputSlotId); var newSlot = propNode.FindSlot(PropertyNode.OutputSlotId); foreach (var edge in graph.GetEdges(oldSlot.slotReference)) graph.Connect(newSlot.slotReference, edge.inputSlot); graph.RemoveNode(node); } } ContextualMenu.MenuAction.StatusFlags ConvertToInlineNodeStatus(EventBase eventBase) { if (selection.OfType().Any(v => v.node != null)) { if (selection.OfType().Any(v => v.node is PropertyNode)) return ContextualMenu.MenuAction.StatusFlags.Normal; return ContextualMenu.MenuAction.StatusFlags.Disabled; } return ContextualMenu.MenuAction.StatusFlags.Hidden; } void ConvertToInlineNode(EventBase eventBase) { var selectedNodeViews = selection.OfType() .Select(x => x.node) .OfType(); foreach (var propNode in selectedNodeViews) ((AbstractMaterialGraph)propNode.owner).ReplacePropertyNodeWithConcreteNode(propNode); } ContextualMenu.MenuAction.StatusFlags ConvertToSubgraphStatus(EventBase eventBase) { if (onConvertToSubgraphClick == null) return ContextualMenu.MenuAction.StatusFlags.Hidden; return selection.OfType().Any(v => v.node != null) ? ContextualMenu.MenuAction.StatusFlags.Normal : ContextualMenu.MenuAction.StatusFlags.Hidden; } void ConvertToSubgraph(EventBase eventBase) { onConvertToSubgraphClick(); } public delegate void OnSelectionChanged(IEnumerable nodes); public OnSelectionChanged onSelectionChanged; void SelectionChanged() { var selectedNodes = selection.OfType().Where(x => x.userData is INode); if (onSelectionChanged != null) onSelectionChanged(selectedNodes.Select(x => x.userData as INode)); } public override void AddToSelection(ISelectable selectable) { base.AddToSelection(selectable); SelectionChanged(); } public override void RemoveFromSelection(ISelectable selectable) { base.RemoveFromSelection(selectable); SelectionChanged(); } public override void ClearSelection() { base.ClearSelection(); SelectionChanged(); } string SerializeGraphElementsImplementation(IEnumerable elements) { var graph = new CopyPasteGraph(elements.OfType().Select(x => (INode)x.node), elements.OfType().Select(x => x.userData).OfType()); return JsonUtility.ToJson(graph, true); } bool CanPasteSerializedDataImplementation(string serializedData) { return CopyPasteGraph.FromJson(serializedData) != null; } void UnserializeAndPasteImplementation(string operationName, string serializedData) { graph.owner.RegisterCompleteObjectUndo(operationName); var pastedGraph = CopyPasteGraph.FromJson(serializedData); this.InsertCopyPasteGraph(pastedGraph); } void DeleteSelectionImplementation(string operationName, GraphView.AskUser askUser) { graph.owner.RegisterCompleteObjectUndo(operationName); graph.RemoveElements(selection.OfType().Where(v => !(v.node is SubGraphOutputNode)).Select(x => (INode)x.node), selection.OfType().Select(x => x.userData).OfType()); bool userNotified = false; foreach (var selectable in selection) { var field = selectable as BlackboardField; if (field != null && field.userData != null) { if (!userNotified) { if (EditorUtility.DisplayDialog("Sub Graph Will Change", "If you remove a property and save the sub graph, you might change other graphs that are using this sub graph.\n\nDo you want to continue?", "Yes", "No")) { userNotified = true; } else { return; } } if (userNotified) { var property = (IShaderProperty)field.userData; graph.RemoveShaderProperty(property.guid); } } } selection.Clear(); } public bool CanAcceptDrop(List selection) { return selection.OfType().Any(); } public EventPropagation DragUpdated(IMGUIEvent evt, IEnumerable selection, IDropTarget dropTarget) { return EventPropagation.Continue; } public EventPropagation DragPerform(IMGUIEvent evt, IEnumerable selection, IDropTarget dropTarget) { return EventPropagation.Continue; } public EventPropagation DragExited() { return EventPropagation.Continue; } } public static class GraphViewExtensions { internal static void InsertCopyPasteGraph(this MaterialGraphView graphView, CopyPasteGraph copyGraph) { if (copyGraph == null) return; using (var remappedNodesDisposable = ListPool.GetDisposable()) using (var remappedEdgesDisposable = ListPool.GetDisposable()) { var remappedNodes = remappedNodesDisposable.value; var remappedEdges = remappedEdgesDisposable.value; copyGraph.InsertInGraph(graphView.graph, remappedNodes, remappedEdges); // Add new elements to selection graphView.ClearSelection(); graphView.graphElements.ForEach(element => { var edge = element as Edge; if (edge != null && remappedEdges.Contains(edge.userData as IEdge)) graphView.AddToSelection(edge); var nodeView = element as MaterialNodeView; if (nodeView != null && remappedNodes.Contains(nodeView.node)) graphView.AddToSelection(nodeView); }); } } } }