using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.UIElements; using UnityEditor.Graphing; using UnityEditorInternal; namespace UnityEditor.ShaderGraph.Drawing { internal class ReorderableSlotListView : VisualElement { int m_SelectedIndex = -1; SlotType m_SlotType; GUIStyle m_LabelStyle; IMGUIContainer m_Container; AbstractMaterialNode m_Node; ReorderableList m_ReorderableList; bool m_AllowBareResources; internal delegate void ListRecreatedDelegate(); ListRecreatedDelegate m_OnListRecreatedCallback = new ListRecreatedDelegate(() => { }); string label => string.Format("{0}s", m_SlotType.ToString()); public ReorderableList.AddCallbackDelegate OnAddCallback { get => m_ReorderableList?.onAddCallback; set => m_ReorderableList.onAddCallback = value; } public ReorderableList.RemoveCallbackDelegate OnRemoveCallback { get => m_ReorderableList.onRemoveCallback; set => m_ReorderableList.onRemoveCallback = value; } public ListRecreatedDelegate OnListRecreatedCallback { get => m_OnListRecreatedCallback; set => m_OnListRecreatedCallback = value; } public Func AllowedTypeCallback; internal ReorderableSlotListView(AbstractMaterialNode node, SlotType slotType, bool allowBareResources) { m_AllowBareResources = allowBareResources; styleSheets.Add(Resources.Load("Styles/ReorderableSlotListView")); m_Node = node; m_SlotType = slotType; m_Container = new IMGUIContainer(() => OnGUIHandler()) { name = "ListContainer" }; Add(m_Container); RecreateList(); AddCallbacks(); } internal void RecreateList() { // Get slots based on type List slots = new List(); if (m_SlotType == SlotType.Input) m_Node.GetInputSlots(slots); else m_Node.GetOutputSlots(slots); // Create reorderable list from IDs List slotIDs = slots.Select(s => s.id).ToList(); m_ReorderableList = new ReorderableList(slotIDs, typeof(int), true, true, true, true); m_OnListRecreatedCallback(); } private void OnGUIHandler() { if (m_ReorderableList == null) { RecreateList(); AddCallbacks(); } using (var changeCheckScope = new EditorGUI.ChangeCheckScope()) { m_ReorderableList.index = m_SelectedIndex; m_ReorderableList.DoLayoutList(); if (changeCheckScope.changed) m_Node.Dirty(ModificationScope.Node); } } private void AddCallbacks() { m_ReorderableList.drawHeaderCallback = (Rect rect) => { var labelRect = new Rect(rect.x, rect.y, rect.width - 10, rect.height); EditorGUI.LabelField(labelRect, label); }; // Draw Element m_ReorderableList.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => { // Slot is guaranteed to exist in this UI state MaterialSlot oldSlot = m_Node.FindSlot((int)m_ReorderableList.list[index]); EditorGUI.BeginChangeCheck(); var displayName = EditorGUI.DelayedTextField(new Rect(rect.x, rect.y, rect.width / 2, EditorGUIUtility.singleLineHeight), oldSlot.RawDisplayName(), EditorStyles.label); ConcreteSlotValueTypePopupName concreteValueTypePopupOrig = oldSlot.concreteValueType.ToConcreteSlotValueTypePopupName(oldSlot.bareResource); ConcreteSlotValueTypePopupName concreteValueTypePopupNew = (ConcreteSlotValueTypePopupName)EditorGUI.EnumPopup( new Rect(rect.x + rect.width / 2, rect.y, rect.width - rect.width / 2, EditorGUIUtility.singleLineHeight), GUIContent.none, concreteValueTypePopupOrig, e => { ConcreteSlotValueTypePopupName csvtpn = (ConcreteSlotValueTypePopupName)e; csvtpn.ToConcreteSlotValueType(out bool isBareResource); if (isBareResource && !m_AllowBareResources) return false; return AllowedTypeCallback?.Invoke(csvtpn) ?? true; } ); if (EditorGUI.EndChangeCheck()) { m_Node.owner.owner.RegisterCompleteObjectUndo("Modify Port"); displayName = NodeUtils.ConvertToValidHLSLIdentifier(displayName); if (displayName != oldSlot.RawDisplayName()) { using (var tempSlots = PooledList.Get()) { m_Node.GetSlots(tempSlots); // deduplicate against other slot shaderOutputNames displayName = GraphUtil.DeduplicateName(tempSlots.Where(p => p.id != oldSlot.id).Select(p => p.shaderOutputName), "{0}_{1}", displayName); } } ConcreteSlotValueType concreteValueType = concreteValueTypePopupNew.ToConcreteSlotValueType(out bool isBareResource); // Because the type may have changed, we can't (always) just modify the existing slot. So create a new one and replace it. var newSlot = MaterialSlot.CreateMaterialSlot(concreteValueType.ToSlotValueType(), oldSlot.id, displayName, displayName, m_SlotType, Vector4.zero); newSlot.CopyValuesFrom(oldSlot); m_Node.AddSlot(newSlot, false); newSlot.bareResource = isBareResource; List orderedSlotIds = new List(); if (m_SlotType == SlotType.Input) { orderedSlotIds.AddRange(m_ReorderableList.list.OfType()); List slots = new List(); m_Node.GetOutputSlots(slots); orderedSlotIds.AddRange(slots.Select(s => s.id)); } else { List slots = new List(); m_Node.GetInputSlots(slots); orderedSlotIds.AddRange(slots.Select(s => s.id)); orderedSlotIds.AddRange(m_ReorderableList.list.OfType()); } m_Node.SetSlotOrder(orderedSlotIds); RecreateList(); m_Node.ValidateNode(); } }; // Element height m_ReorderableList.elementHeightCallback = (int indexer) => { return m_ReorderableList.elementHeight; }; // Add callback delegates m_ReorderableList.onSelectCallback += SelectEntry; m_ReorderableList.onAddCallback += AddEntry; m_ReorderableList.onRemoveCallback += RemoveEntry; m_ReorderableList.onReorderCallback += ReorderEntries; } private void SelectEntry(ReorderableList list) { m_SelectedIndex = list.index; } private void AddEntry(ReorderableList list) { m_Node.owner.owner.RegisterCompleteObjectUndo("Add Port"); // Need to get all current slots to get the next valid ID List slots = new List(); m_Node.GetSlots(slots); int[] slotIDs = slots.Select(s => s.id).OrderByDescending(s => s).ToArray(); int newSlotID = slotIDs.Length > 0 ? slotIDs[0] + 1 : 0; string name = NodeUtils.GetDuplicateSafeNameForSlot(m_Node, newSlotID, "New"); // Create a new slot and add it var newSlot = MaterialSlot.CreateMaterialSlot(SlotValueType.Vector1, newSlotID, name, NodeUtils.GetHLSLSafeName(name), m_SlotType, Vector4.zero); m_Node.AddSlot(newSlot); // Select the new slot, then validate the node m_SelectedIndex = list.list.Count - 1; m_Node.ValidateNode(); } private void RemoveEntry(ReorderableList list) { m_Node.owner.owner.RegisterCompleteObjectUndo("Remove Port"); // Remove the slot from the node m_SelectedIndex = list.index; m_Node.RemoveSlot((int)m_ReorderableList.list[m_SelectedIndex]); // Then remove it from the list // Need to do this order to preserve the list indicies for previous step ReorderableList.defaultBehaviours.DoRemoveButton(list); // Validate m_Node.ValidateNode(); } private void ReorderEntries(ReorderableList list) { m_Node.owner.owner.RegisterCompleteObjectUndo("Reorder Ports"); // Get all the current slots List slots = new List(); if (m_SlotType == SlotType.Input) m_Node.GetInputSlots(slots); else m_Node.GetOutputSlots(slots); // Store the edges Dictionary> edgeDict = new Dictionary>(); foreach (MaterialSlot slot in slots) edgeDict.Add(slot, (List)slot.owner.owner.GetEdges(slot.slotReference)); // Get reorder slots so need to remove them all then re-add foreach (MaterialSlot slot in slots) m_Node.RemoveSlot(slot.id); // Order them by their slot ID slots = slots.OrderBy(s => s.id).ToList(); // Now add the slots back based on the list order // For each list entry get the slot with that ID for (int i = 0; i < list.list.Count; i++) { var currentSlot = slots.Where(s => s.id == (int)list.list[i]).FirstOrDefault(); m_Node.AddSlot(currentSlot); } // Reconnect the edges foreach (KeyValuePair> entry in edgeDict) { foreach (IEdge edge in entry.Value) { m_Node.owner.Connect(edge.outputSlot, edge.inputSlot); } } RecreateList(); m_Node.ValidateNode(); } } }