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; namespace UnityEditor.ShaderGraph.Drawing { class SearchWindowProvider : ScriptableObject, ISearchWindowProvider { EditorWindow m_EditorWindow; GraphData m_Graph; GraphView m_GraphView; Texture2D m_Icon; public ShaderPort connectedPort { get; set; } public bool nodeNeedsRepositioning { get; set; } public SlotReference targetSlotReference { get; private set; } public Vector2 targetPosition { get; private set; } private const string k_HiddenFolderName = "Hidden"; public void Initialize(EditorWindow editorWindow, GraphData graph, GraphView graphView) { m_EditorWindow = editorWindow; m_Graph = graph; m_GraphView = graphView; // 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; } } struct NodeEntry { public string[] title; public AbstractMaterialNode node; public int compatibleSlotId; } List m_Ids; List m_Slots = new List(); public List CreateSearchTree(SearchWindowContext context) { // 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. var nodeEntries = new List(); foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies()) { foreach (var type in assembly.GetTypesOrNothing()) { if (type.IsClass && !type.IsAbstract && (type.IsSubclassOf(typeof(AbstractMaterialNode))) && type != typeof(PropertyNode) && type != typeof(KeywordNode) && type != typeof(SubGraphNode)) { var attrs = type.GetCustomAttributes(typeof(TitleAttribute), false) as TitleAttribute[]; if (attrs != null && attrs.Length > 0) { var node = (AbstractMaterialNode)Activator.CreateInstance(type); AddEntries(node, attrs[0].title, nodeEntries); } } } } foreach (var guid in AssetDatabase.FindAssets(string.Format("t:{0}", typeof(SubGraphAsset)))) { var asset = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); 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); } } foreach (var property in m_Graph.properties) { var node = new PropertyNode(); node.owner = m_Graph; node.propertyGuid = property.guid; node.owner = null; AddEntries(node, new[] { "Properties", "Property: " + property.displayName }, nodeEntries); } foreach (var keyword in m_Graph.keywords) { var node = new KeywordNode(); node.owner = m_Graph; node.keywordGuid = keyword.guid; node.owner = null; AddEntries(node, new[] { "Keywords", "Keyword: " + keyword.displayName }, 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)) return entry1.title.Length < entry2.title.Length ? -1 : 1; return value; } } return 0; }); //* Build up the data structure needed by SearchWindow. // `groups` contains the current group path we're in. var groups = new List(); // First item in the tree is the title of the window. var tree = new List { new SearchTreeGroupEntry(new GUIContent("Create Node"), 0), }; foreach (var nodeEntry in nodeEntries) { // `createIndex` represents from where we should add new group entries from the current entry's group path. var createIndex = int.MaxValue; // Compare the group path of the current entry to the current group path. for (var i = 0; i < nodeEntry.title.Length - 1; i++) { var group = nodeEntry.title[i]; if (i >= groups.Count) { // The current group path matches a prefix of the current entry's group path, so we add the // rest of the group path from the currrent entry. createIndex = i; break; } if (groups[i] != group) { // A prefix of the current group path matches a prefix of the current entry's group path, // so we remove everyfrom from the point where it doesn't match anymore, and then add the rest // of the group path from the current entry. groups.RemoveRange(i, groups.Count - i); createIndex = i; break; } } // Create new group entries as needed. // If we don't need to modify the group path, `createIndex` will be `int.MaxValue` and thus the loop won't run. for (var i = createIndex; i < nodeEntry.title.Length - 1; i++) { var group = nodeEntry.title[i]; groups.Add(group); tree.Add(new SearchTreeGroupEntry(new GUIContent(group)) { level = i + 1 }); } // Finally, add the actual entry. tree.Add(new SearchTreeEntry(new GUIContent(nodeEntry.title.Last(), m_Icon)) { level = nodeEntry.title.Length, userData = nodeEntry }); } return tree; } void AddEntries(AbstractMaterialNode node, string[] title, List nodeEntries) { if (m_Graph.isSubGraph && !node.allowedInSubGraph) return; if (!m_Graph.isSubGraph && !node.allowedInMainGraph) return; if (connectedPort == null) { nodeEntries.Add(new NodeEntry { node = node, title = title, compatibleSlotId = -1 }); return; } var connectedSlot = connectedPort.slot; m_Slots.Clear(); node.GetSlots(m_Slots); var hasSingleSlot = m_Slots.Count(s => s.isOutputSlot != connectedSlot.isOutputSlot) == 1; m_Slots.RemoveAll(slot => { var materialSlot = (MaterialSlot)slot; return !materialSlot.IsCompatibleWith(connectedSlot); }); m_Slots.RemoveAll(slot => { var materialSlot = (MaterialSlot)slot; return !materialSlot.IsCompatibleStageWith(connectedSlot); }); if (hasSingleSlot && m_Slots.Count == 1) { nodeEntries.Add(new NodeEntry { node = node, title = title, compatibleSlotId = m_Slots.First().id }); return; } foreach (var slot in m_Slots) { var entryTitle = new string[title.Length]; title.CopyTo(entryTitle, 0); entryTitle[entryTitle.Length - 1] += ": " + slot.displayName; nodeEntries.Add(new NodeEntry { title = entryTitle, node = node, compatibleSlotId = slot.id }); } } public bool OnSelectEntry(SearchTreeEntry entry, SearchWindowContext context) { var nodeEntry = (NodeEntry)entry.userData; var node = nodeEntry.node; var drawState = node.drawState; var windowRoot = m_EditorWindow.rootVisualElement; var windowMousePosition = windowRoot.ChangeCoordinatesTo(windowRoot.parent, context.screenMousePosition - m_EditorWindow.position.position); var graphMousePosition = m_GraphView.contentViewContainer.WorldToLocal(windowMousePosition); drawState.position = new Rect(graphMousePosition, Vector2.zero); node.drawState = drawState; m_Graph.owner.RegisterCompleteObjectUndo("Add " + node.name); 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; } } }