using System; using System.Collections.Generic; using System.Linq; using System.Text; using UnityEditor.Graphing; using UnityEditor.Graphing.Util; using UnityEngine; using UnityEditor.UIElements; using UnityEditor.Experimental.GraphView; using UnityEngine.UIElements; using UnityEditor.Searcher; using UnityEngine.Profiling; using UnityEngine.Pool; namespace UnityEditor.ShaderGraph.Drawing { internal struct NodeEntry { public string[] title; public AbstractMaterialNode node; public int compatibleSlotId; public string slotName; } class SearchWindowProvider : ScriptableObject { internal EditorWindow m_EditorWindow; internal GraphData m_Graph; internal GraphView m_GraphView; internal Texture2D m_Icon; public List currentNodeEntries; public ShaderPort connectedPort { get; set; } public bool nodeNeedsRepositioning { get; set; } public SlotReference targetSlotReference { get; internal set; } public Vector2 targetPosition { get; internal set; } public VisualElement target { get; internal set; } public bool regenerateEntries { get; set; } private const string k_HiddenFolderName = "Hidden"; ShaderStageCapability m_ConnectedSlotCapability; // calculated in GenerateNodeEntries public void Initialize(EditorWindow editorWindow, GraphData graph, GraphView graphView) { m_EditorWindow = editorWindow; m_Graph = graph; m_GraphView = graphView; GenerateNodeEntries(); // Transparent icon to trick search window into indenting items m_Icon = new Texture2D(1, 1); m_Icon.SetPixel(0, 0, new Color(0, 0, 0, 0)); m_Icon.Apply(); } void OnDestroy() { if (m_Icon != null) { DestroyImmediate(m_Icon); m_Icon = null; } } List m_Ids; List m_Slots = new List(); public void GenerateNodeEntries() { Profiler.BeginSample("SearchWindowProvider.GenerateNodeEntries"); // First build up temporary data structure containing group & title as an array of strings (the last one is the actual title) and associated node type. List nodeEntries = new List(); bool hideCustomInterpolators = m_Graph.activeTargets.All(at => at.ignoreCustomInterpolators); if (connectedPort != null) { var slot = connectedPort.slot; // Precalculate slot compatibility to avoid traversing graph for every added entry. m_ConnectedSlotCapability = slot.stageCapability; if (m_ConnectedSlotCapability == ShaderStageCapability.All || slot.owner is SubGraphNode) { m_ConnectedSlotCapability = NodeUtils.GetEffectiveShaderStageCapability(slot, true) & NodeUtils.GetEffectiveShaderStageCapability(slot, false); } } else { m_ConnectedSlotCapability = ShaderStageCapability.All; } if (target is ContextView contextView) { // Iterate all BlockFieldDescriptors currently cached on GraphData foreach (var field in m_Graph.blockFieldDescriptors) { if (field.isHidden) continue; // Test stage if (field.shaderStage != contextView.contextData.shaderStage) continue; // Create title List title = ListPool.Get(); if (!string.IsNullOrEmpty(field.path)) { var path = field.path.Split('/').ToList(); title.AddRange(path); } title.Add(field.displayName); // Create and initialize BlockNode instance then add entry var node = (BlockNode)Activator.CreateInstance(typeof(BlockNode)); node.Init(field); AddEntries(node, title.ToArray(), nodeEntries); } SortEntries(nodeEntries); if (contextView.contextData.shaderStage == ShaderStage.Vertex && !hideCustomInterpolators) { var customBlockNodeStub = (BlockNode)Activator.CreateInstance(typeof(BlockNode)); customBlockNodeStub.InitCustomDefault(); AddEntries(customBlockNodeStub, new string[] { "Custom Interpolator" }, nodeEntries); } currentNodeEntries = nodeEntries; return; } Profiler.BeginSample("SearchWindowProvider.GenerateNodeEntries.IterateKnowNodes"); foreach (var type in NodeClassCache.knownNodeTypes) { if ((!type.IsClass || type.IsAbstract) || type == typeof(PropertyNode) || type == typeof(KeywordNode) || type == typeof(DropdownNode) || type == typeof(SubGraphNode)) continue; TitleAttribute titleAttribute = NodeClassCache.GetAttributeOnNodeType(type); if (titleAttribute != null) { var node = (AbstractMaterialNode)Activator.CreateInstance(type); if (!node.ExposeToSearcher) continue; if (ShaderGraphPreferences.allowDeprecatedBehaviors && node.latestVersion > 0) { var versions = node.allowedNodeVersions ?? Enumerable.Range(0, node.latestVersion + 1); bool multiple = (versions.Count() > 1); foreach (int i in versions) { var depNode = (AbstractMaterialNode)Activator.CreateInstance(type); depNode.ChangeVersion(i); if (multiple) AddEntries(depNode, titleAttribute.title.Append($"V{i}").ToArray(), nodeEntries); else AddEntries(depNode, titleAttribute.title, nodeEntries); } } else { AddEntries(node, titleAttribute.title, nodeEntries); } } } Profiler.EndSample(); Profiler.BeginSample("SearchWindowProvider.GenerateNodeEntries.IterateSubgraphAssets"); foreach (var asset in NodeClassCache.knownSubGraphAssets) { if (asset == null) continue; var node = new SubGraphNode { asset = asset }; var title = asset.path.Split('/').ToList(); if (asset.descendents.Contains(m_Graph.assetGuid) || asset.assetGuid == m_Graph.assetGuid) { continue; } if (string.IsNullOrEmpty(asset.path)) { AddEntries(node, new string[1] { asset.name }, nodeEntries); } else if (title[0] != k_HiddenFolderName) { title.Add(asset.name); AddEntries(node, title.ToArray(), nodeEntries); } } Profiler.EndSample(); Profiler.BeginSample("SearchWindowProvider.GenerateNodeEntries.IterateGraphInputs"); foreach (var property in m_Graph.properties) { if (property is Serialization.MultiJsonInternal.UnknownShaderPropertyType) continue; var node = new PropertyNode(); node.property = property; AddEntries(node, new[] { "Properties", "Property: " + property.displayName }, nodeEntries); } foreach (var keyword in m_Graph.keywords) { var node = new KeywordNode(); node.keyword = keyword; AddEntries(node, new[] { "Keywords", "Keyword: " + keyword.displayName }, nodeEntries); } foreach (var dropdown in m_Graph.dropdowns) { var node = new DropdownNode(); node.dropdown = dropdown; AddEntries(node, new[] { "Dropdowns", "dropdown: " + dropdown.displayName }, nodeEntries); } if (!hideCustomInterpolators) { foreach (var cibnode in m_Graph.vertexContext.blocks.Where(b => b.value.isCustomBlock)) { var node = Activator.CreateInstance(); node.ConnectToCustomBlock(cibnode.value); AddEntries(node, new[] { "Custom Interpolator", cibnode.value.customName }, nodeEntries); } } Profiler.EndSample(); SortEntries(nodeEntries); currentNodeEntries = nodeEntries; Profiler.EndSample(); } void SortEntries(List nodeEntries) { // Sort the entries lexicographically by group then title with the requirement that items always comes before sub-groups in the same group. // Example result: // - Art/BlendMode // - Art/Adjustments/ColorBalance // - Art/Adjustments/Contrast nodeEntries.Sort((entry1, entry2) => { for (var i = 0; i < entry1.title.Length; i++) { if (i >= entry2.title.Length) return 1; var value = entry1.title[i].CompareTo(entry2.title[i]); if (value != 0) { // Make sure that leaves go before nodes if (entry1.title.Length != entry2.title.Length && (i == entry1.title.Length - 1 || i == entry2.title.Length - 1)) { //once nodes are sorted, sort slot entries by slot order instead of alphebetically var alphaOrder = entry1.title.Length < entry2.title.Length ? -1 : 1; var slotOrder = entry1.compatibleSlotId.CompareTo(entry2.compatibleSlotId); return alphaOrder.CompareTo(slotOrder); } return value; } } return 0; }); } void AddEntries(AbstractMaterialNode node, string[] title, List addNodeEntries) { if (m_Graph.isSubGraph && !node.allowedInSubGraph) return; if (!m_Graph.isSubGraph && !node.allowedInMainGraph) return; if (connectedPort == null) { addNodeEntries.Add(new NodeEntry { node = node, title = title, compatibleSlotId = -1 }); return; } var connectedSlot = connectedPort.slot; m_Slots.Clear(); node.GetSlots(m_Slots); foreach (var slot in m_Slots) { if (!slot.IsCompatibleWith(connectedSlot)) { continue; } if (!slot.IsCompatibleStageWith(m_ConnectedSlotCapability)) { continue; } //var entryTitle = new string[title.Length]; //title.CopyTo(entryTitle, 0); //entryTitle[entryTitle.Length - 1] += ": " + slot.displayName; addNodeEntries.Add(new NodeEntry { title = title, node = node, compatibleSlotId = slot.id, slotName = slot.displayName }); } } } class SearcherProvider : SearchWindowProvider { public Searcher.Searcher LoadSearchWindow() { if (regenerateEntries) { GenerateNodeEntries(); regenerateEntries = false; } //create empty root for searcher tree var root = new List(); var dummyEntry = new NodeEntry(); foreach (var nodeEntry in currentNodeEntries) { SearcherItem item = null; SearcherItem parent = null; for (int i = 0; i < nodeEntry.title.Length; i++) { var pathEntry = nodeEntry.title[i]; List children = parent != null ? parent.Children : root; item = children.Find(x => x.Name == pathEntry); if (item == null) { //if we have slot entries and are at a leaf, add the slot name to the entry title if (nodeEntry.compatibleSlotId != -1 && i == nodeEntry.title.Length - 1) item = new SearchNodeItem(pathEntry + ": " + nodeEntry.slotName, nodeEntry, nodeEntry.node.synonyms); //if we don't have slot entries and are at a leaf, add userdata to the entry else if (nodeEntry.compatibleSlotId == -1 && i == nodeEntry.title.Length - 1) item = new SearchNodeItem(pathEntry, nodeEntry, nodeEntry.node.synonyms); //if we aren't a leaf, don't add user data else item = new SearchNodeItem(pathEntry, dummyEntry, null); if (parent != null) { parent.AddChild(item); } else { children.Add(item); } } parent = item; if (parent.Depth == 0 && !root.Contains(parent)) root.Add(parent); } } var nodeDatabase = SearcherDatabase.Create(root, string.Empty, false); return new Searcher.Searcher(nodeDatabase, new SearchWindowAdapter("Create Node")); } public bool OnSearcherSelectEntry(SearcherItem entry, Vector2 screenMousePosition) { if (entry == null || (entry as SearchNodeItem).NodeGUID.node == null) return true; var nodeEntry = (entry as SearchNodeItem).NodeGUID; if (nodeEntry.node is PropertyNode propNode) if (propNode.property is Serialization.MultiJsonInternal.UnknownShaderPropertyType) return true; var node = CopyNodeForGraph(nodeEntry.node); var windowRoot = m_EditorWindow.rootVisualElement; var windowMousePosition = windowRoot.ChangeCoordinatesTo(windowRoot.parent, screenMousePosition); //- m_EditorWindow.position.position); var graphMousePosition = m_GraphView.contentViewContainer.WorldToLocal(windowMousePosition); m_Graph.owner.RegisterCompleteObjectUndo("Add " + node.name); if (node is BlockNode blockNode) { if (!(target is ContextView contextView)) return true; // ensure custom blocks have a unique name provided the existing context. if (blockNode.isCustomBlock) { HashSet usedNames = new HashSet(); foreach (var other in contextView.contextData.blocks) usedNames.Add(other.value.descriptor.displayName); blockNode.customName = GraphUtil.SanitizeName(usedNames, "{0}_{1}", blockNode.descriptor.displayName); } // Test against all current BlockNodes in the Context // Never allow duplicate BlockNodes else if (contextView.contextData.blocks.Where(x => x.value.name == blockNode.name).FirstOrDefault().value != null) { return true; } // Insert block to Data blockNode.owner = m_Graph; int index = contextView.GetInsertionIndex(screenMousePosition); m_Graph.AddBlock(blockNode, contextView.contextData, index); return true; } var drawState = node.drawState; drawState.position = new Rect(graphMousePosition, Vector2.zero); node.drawState = drawState; m_Graph.AddNode(node); if (connectedPort != null) { var connectedSlot = connectedPort.slot; var connectedSlotReference = connectedSlot.owner.GetSlotReference(connectedSlot.id); var compatibleSlotReference = node.GetSlotReference(nodeEntry.compatibleSlotId); var fromReference = connectedSlot.isOutputSlot ? connectedSlotReference : compatibleSlotReference; var toReference = connectedSlot.isOutputSlot ? compatibleSlotReference : connectedSlotReference; m_Graph.Connect(fromReference, toReference); nodeNeedsRepositioning = true; targetSlotReference = compatibleSlotReference; targetPosition = graphMousePosition; } return true; } public AbstractMaterialNode CopyNodeForGraph(AbstractMaterialNode oldNode) { var newNode = (AbstractMaterialNode)Activator.CreateInstance(oldNode.GetType()); if (ShaderGraphPreferences.allowDeprecatedBehaviors && oldNode.sgVersion != newNode.sgVersion) { newNode.ChangeVersion(oldNode.sgVersion); } if (newNode is SubGraphNode subgraphNode) { subgraphNode.asset = ((SubGraphNode)oldNode).asset; } else if (newNode is PropertyNode propertyNode) { propertyNode.owner = m_Graph; propertyNode.property = ((PropertyNode)oldNode).property; propertyNode.owner = null; } else if (newNode is KeywordNode keywordNode) { keywordNode.owner = m_Graph; keywordNode.keyword = ((KeywordNode)oldNode).keyword; keywordNode.owner = null; } else if (newNode is DropdownNode dropdownNode) { dropdownNode.owner = m_Graph; dropdownNode.dropdown = ((DropdownNode)oldNode).dropdown; dropdownNode.owner = null; } else if (newNode is BlockNode blockNode) { blockNode.owner = m_Graph; blockNode.Init(((BlockNode)oldNode).descriptor); blockNode.owner = null; } else if (newNode is CustomInterpolatorNode cinode) { cinode.owner = m_Graph; cinode.ConnectToCustomBlockByName(((CustomInterpolatorNode)oldNode).customBlockNodeName); cinode.owner = null; } return newNode; } } }