using System; using System.Collections; using System.Collections.Generic; using System.Diagnostics; using System.IO; using System.Linq; using System.Reflection; using System.Threading.Tasks; using UnityEditor.AddressableAssets.Settings; using UnityEditor.IMGUI.Controls; using UnityEditor.SceneManagement; using UnityEngine; using UnityEngine.AddressableAssets; using UnityEngine.Assertions; using UnityEngine.U2D; using Debug = UnityEngine.Debug; namespace UnityEditor.AddressableAssets.GUI { using Object = UnityEngine.Object; [CustomPropertyDrawer(typeof(AssetReference), true)] class AssetReferenceDrawer : PropertyDrawer { public string newGuid; public string newGuidPropertyPath; internal string m_AssetName; internal Rect assetDropDownRect; internal const string noAssetString = "None (AddressableAsset)"; internal const string forceAddressableString = "Make Addressable - "; internal AssetReference m_AssetRefObject; internal GUIContent m_label; internal bool m_ReferencesSame = true; internal List m_Restrictions = null; SubassetPopup m_SubassetPopup; private Texture m_CaretTexture = null; internal const string k_FieldControlPrefix = "AssetReferenceField"; internal const string k_SubFieldControlPrefix = "AssetReferenceSubField"; internal SerializedProperty assetProperty; internal bool subassetPickerSelectionChanged; internal List Restrictions => m_Restrictions; private string GetControlName(string prefix, string name) { return $"{prefix}_{name}"; } /// /// Validates that the referenced asset allowable for this asset reference. /// /// The path to the asset in question. /// Whether the referenced asset is valid. public bool ValidateAsset(string path) { return AssetReferenceDrawerUtilities.ValidateAsset(m_AssetRefObject, Restrictions, path); } internal bool ValidateAsset(IReferenceEntryData entryData) { return AssetReferenceDrawerUtilities.ValidateAsset(m_AssetRefObject, Restrictions, entryData); } /* * The AssetReference class is not a Unity.Object or a base type so a lot of SerializedProperty's * functionalities doesn't work, because type-checking is done in the API to check whether an operation * can be executed or not. In the engine, one of the way changes are detected is when a new value is set * through the class' value setters (see MarkPropertyModified() in SerializedProperty.cpp). So in order to * trigger a change, we modify a sub-property instead. */ void TriggerOnValidate(SerializedProperty property) { if (property != null) { property.serializedObject.ApplyModifiedProperties(); property.serializedObject.Update(); // This is actually what triggers the OnValidate() method. // Since 'm_EditorAssetChanged' is of a recognized type and is a sub-property of AssetReference, both // are flagged as changed and OnValidate() is called. property.FindPropertyRelative("m_EditorAssetChanged").boolValue = false; } } public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { if (property == null || label == null) { Debug.LogError("Error rendering drawer for AssetReference property."); return; } string labelText = label.text; m_ReferencesSame = true; m_AssetRefObject = property.GetActualObjectForSerializedProperty(fieldInfo, ref labelText); assetProperty = property; labelText = ObjectNames.NicifyVariableName(labelText); if (labelText != label.text || string.IsNullOrEmpty(label.text)) { label = new GUIContent(labelText, label.tooltip); } m_label = label; if (m_AssetRefObject == null) { return; } EditorGUI.BeginProperty(position, label, property); if (m_Restrictions == null) m_Restrictions = AssetReferenceDrawerUtilities.GatherFilters(property); string guid = m_AssetRefObject.AssetGUID; var aaSettings = AddressableAssetSettingsDefaultObject.Settings; var isNotAddressable = ApplySelectionChanges(property, aaSettings, ref guid); assetDropDownRect = EditorGUI.PrefixLabel(position, label); var nameToUse = AssetReferenceDrawerUtilities.GetNameForAsset(ref m_ReferencesSame, property, isNotAddressable, fieldInfo, m_label.text); bool isDragging = Event.current.type == EventType.DragUpdated && position.Contains(Event.current.mousePosition); bool isDropping = Event.current.type == EventType.DragPerform && position.Contains(Event.current.mousePosition); bool shouldDrawSubAssetsControl = false; List subAssetsList = PrepareForSubAssetsControl(ref shouldDrawSubAssetsControl); if (shouldDrawSubAssetsControl) assetDropDownRect = new Rect(assetDropDownRect.position, new Vector2(assetDropDownRect.width / 2, assetDropDownRect.height)); bool isEnterKeyPressed = Event.current.type == EventType.KeyDown && Event.current.isKey && (Event.current.keyCode == KeyCode.KeypadEnter || Event.current.keyCode == KeyCode.Return); DrawControl(property, nameToUse, isNotAddressable, guid, isEnterKeyPressed); if (shouldDrawSubAssetsControl) DrawSubAssetsControl(property, subAssetsList, isEnterKeyPressed); HandleDragAndDrop(property, isDragging, isDropping, guid); EditorGUI.EndProperty(); } private List PrepareForSubAssetsControl(ref bool shouldDrawSubAssetsControl) { List subAssets = null; bool selectedSubObject = !string.IsNullOrEmpty(m_AssetRefObject.SubObjectName); if (m_AssetRefObject.editorAsset != null && m_ReferencesSame && !selectedSubObject) subAssets = AssetReferenceDrawerUtilities.GetSubAssetsList(m_AssetRefObject); shouldDrawSubAssetsControl = selectedSubObject || (subAssets != null && subAssets.Count > 1); return subAssets; } internal bool ApplySelectionChanges(SerializedProperty property, AddressableAssetSettings aaSettings, ref string guid) { var checkToForceAddressable = string.Empty; if (!string.IsNullOrEmpty(newGuid) && newGuidPropertyPath == property.propertyPath) { if (newGuid == noAssetString) { if (AssetReferenceDrawerUtilities.SetObject(ref m_AssetRefObject, ref m_ReferencesSame, property, null, fieldInfo, m_label.text, out guid)) TriggerOnValidate(property); newGuid = string.Empty; } else if (newGuid == forceAddressableString) { checkToForceAddressable = guid; newGuid = string.Empty; } else if (guid != newGuid) { if (AssetReferenceDrawerUtilities.SetObject(ref m_AssetRefObject, ref m_ReferencesSame, property, AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(newGuid)), fieldInfo, m_label.text, out guid)) { checkToForceAddressable = newGuid; TriggerOnValidate(property); } newGuid = string.Empty; } } bool isNotAddressable = false; m_AssetName = noAssetString; if (aaSettings != null && !string.IsNullOrEmpty(guid)) { isNotAddressable = AssetReferenceDrawerUtilities.CheckForNewEntry(ref m_AssetName, aaSettings, guid, checkToForceAddressable); } return isNotAddressable; } private void DrawControl(SerializedProperty property, string nameToUse, bool isNotAddressable, string guid, bool isEnterKeyPressed) { float pickerWidth = 12f; Rect pickerRect = assetDropDownRect; pickerRect.width = pickerWidth; pickerRect.x = assetDropDownRect.xMax - pickerWidth * 1.33f; string controlName = GetControlName(k_FieldControlPrefix, property.name); bool isPickerPressed = Event.current.type == EventType.MouseDown && Event.current.button == 0 && pickerRect.Contains(Event.current.mousePosition); var asset = m_AssetRefObject?.editorAsset; if (asset != null && m_ReferencesSame) { float iconHeight = EditorGUIUtility.singleLineHeight - EditorGUIUtility.standardVerticalSpacing * 3; Vector2 iconSize = EditorGUIUtility.GetIconSize(); EditorGUIUtility.SetIconSize(new Vector2(iconHeight, iconHeight)); string assetPath = AssetDatabase.GUIDToAssetPath(m_AssetRefObject.AssetGUID); Texture2D assetIcon = AssetDatabase.GetCachedIcon(assetPath) as Texture2D; UnityEngine.GUI.SetNextControlName(controlName); if (EditorGUI.DropdownButton(assetDropDownRect, new GUIContent(nameToUse, assetIcon), FocusType.Keyboard, EditorStyles.objectField)) { if (Event.current.clickCount == 1) { UnityEngine.GUI.FocusControl(controlName); EditorGUIUtility.PingObject(asset); } if (Event.current.clickCount == 2) { AssetDatabase.OpenAsset(asset); GUIUtility.ExitGUI(); } } EditorGUIUtility.SetIconSize(iconSize); } else { UnityEngine.GUI.SetNextControlName(controlName); if (EditorGUI.DropdownButton(assetDropDownRect, new GUIContent(nameToUse), FocusType.Keyboard, EditorStyles.objectField)) UnityEngine.GUI.FocusControl(controlName); } DrawCaret(pickerRect); bool enterKeyRequestsPopup = isEnterKeyPressed && (controlName == UnityEngine.GUI.GetNameOfFocusedControl()); if (isPickerPressed || enterKeyRequestsPopup) { newGuidPropertyPath = property.propertyPath; var nonAddressedOption = isNotAddressable ? m_AssetName : string.Empty; EditorWindow.GetWindow(true, "Select Addressable Asset").Initialize(this, guid, nonAddressedOption, enterKeyRequestsPopup, Event.current.mousePosition); } } private void DrawCaret(Rect pickerRect) { if (m_CaretTexture == null) { m_CaretTexture = EditorGUIUtility.IconContent("d_pick").image; } if (m_CaretTexture != null) { UnityEngine.GUI.DrawTexture(pickerRect, m_CaretTexture, ScaleMode.ScaleToFit); } } private void HandleDragAndDrop(SerializedProperty property, bool isDragging, bool isDropping, string guid) { var aaSettings = AddressableAssetSettingsDefaultObject.Settings; //During the drag, doing a light check on asset validity. The in-depth check happens during a drop, and should include a log if it fails. var rejectedDrag = false; if (isDragging) { if (aaSettings == null) rejectedDrag = true; else { var aaEntries = DragAndDrop.GetGenericData("AssetEntryTreeViewItem") as List; rejectedDrag = AssetReferenceDrawerUtilities.ValidateDrag(m_AssetRefObject, Restrictions, aaEntries, DragAndDrop.objectReferences, DragAndDrop.paths); } DragAndDrop.visualMode = rejectedDrag ? DragAndDropVisualMode.Rejected : DragAndDropVisualMode.Copy; } if (!rejectedDrag && isDropping) { var aaEntries = DragAndDrop.GetGenericData("AssetEntryTreeViewItem") as List; if (aaEntries != null) DragAndDropFromAddressableGroupWindow(aaEntries, guid, property); else if (DragAndDrop.paths != null) DragAndDropNotFromAddressableGroupWindow(DragAndDrop.paths, guid, property, aaSettings); } } internal void DragAndDropFromAddressableGroupWindow(List aaEntries, string guid, SerializedProperty property) { if (aaEntries.Count == 1) { var item = aaEntries[0]; if (item.entry != null) { if (item.entry.IsInResources) Addressables.LogWarning("Cannot use an AssetReference on an asset in Resources. Move asset out of Resources first."); else { if (AssetReferenceDrawerUtilities.SetObject(ref m_AssetRefObject, ref m_ReferencesSame, property, item.entry.TargetAsset, fieldInfo, m_label.text, out guid)) TriggerOnValidate(property); } } } else if (fieldInfo.FieldType.IsArray || fieldInfo.FieldType.IsAssignableFrom(typeof(List))) { string arrayName = SerializedPropertyExtensions.GetPropertyPathArrayName(property.propertyPath); SerializedProperty arrayProperty = property.serializedObject.FindProperty(arrayName); foreach (AssetEntryTreeViewItem item in aaEntries) { AddressableAssetEntry entry = item.entry; if (entry.IsInResources) Addressables.LogWarning("Cannot use an AssetReference on an asset in Resources. Move asset out of Resources first."); else { arrayProperty.InsertArrayElementAtIndex(arrayProperty.arraySize); arrayProperty.serializedObject.ApplyModifiedProperties(); arrayProperty.serializedObject.Update(); SerializedProperty elementProperty = arrayProperty.GetArrayElementAtIndex(arrayProperty.arraySize - 1); string labelText = ""; AssetReference assetRef = elementProperty.GetActualObjectForSerializedProperty(fieldInfo, ref labelText); bool referencesSame = false; Object obj = entry.TargetAsset; if (AssetReferenceDrawerUtilities.SetObject(ref assetRef, ref referencesSame, elementProperty, obj, fieldInfo, m_label.text, out guid)) TriggerOnValidate(elementProperty); else { arrayProperty.DeleteArrayElementAtIndex(arrayProperty.arraySize - 1); arrayProperty.serializedObject.ApplyModifiedProperties(); arrayProperty.serializedObject.Update(); } } } } } internal void DragAndDropNotFromAddressableGroupWindow(string[] paths, string guid, SerializedProperty property, AddressableAssetSettings aaSettings) { if (paths.Length == 1) { string path = paths[0]; if (AddressableAssetUtility.IsInResources(path)) Addressables.LogWarning( "Cannot use an AssetReference on an asset in Resources. Move asset out of Resources first. "); else if (!AddressableAssetUtility.IsPathValidForEntry(path)) Addressables.LogWarning("Dragged asset is not valid as an Asset Reference. " + path); else { Object obj; if (DragAndDrop.objectReferences != null && DragAndDrop.objectReferences.Length == 1) obj = DragAndDrop.objectReferences[0]; else obj = AssetDatabase.LoadAssetAtPath(path); if (AssetReferenceDrawerUtilities.SetObject(ref m_AssetRefObject, ref m_ReferencesSame, property, obj, fieldInfo, m_label.text, out guid)) { TriggerOnValidate(property); aaSettings = AddressableAssetSettingsDefaultObject.GetSettings(true); var entry = aaSettings.FindAssetEntry(guid); if (entry == null && !string.IsNullOrEmpty(guid)) { string assetName; if (!aaSettings.IsAssetPathInAddressableDirectory(path, out assetName)) { aaSettings.CreateOrMoveEntry(guid, aaSettings.DefaultGroup); newGuid = guid; } } } } } else if (fieldInfo.FieldType.IsArray || fieldInfo.FieldType.IsAssignableFrom(typeof(List))) { string arrayName = SerializedPropertyExtensions.GetPropertyPathArrayName(property.propertyPath); SerializedProperty arrayProperty = property.serializedObject.FindProperty(arrayName); for (int i = 0; i < paths.Length; i++) { string path = paths[i]; if (AddressableAssetUtility.IsInResources(path)) Addressables.LogWarning("Cannot use an AssetReference on an asset in Resources. Move asset out of Resources first."); else if (!AddressableAssetUtility.IsPathValidForEntry(path)) Addressables.LogWarning("Dragged asset is not valid as an Asset Reference. " + path); else { arrayProperty.InsertArrayElementAtIndex(arrayProperty.arraySize); arrayProperty.serializedObject.ApplyModifiedProperties(); arrayProperty.serializedObject.Update(); SerializedProperty elementProperty = arrayProperty.GetArrayElementAtIndex(arrayProperty.arraySize - 1); string labelText = ""; AssetReference assetRef = elementProperty.GetActualObjectForSerializedProperty(fieldInfo, ref labelText); bool referencesSame = false; Object obj = (DragAndDrop.objectReferences != null && i < DragAndDrop.objectReferences.Length) ? DragAndDrop.objectReferences[i] : AssetDatabase.LoadAssetAtPath(path); if (AssetReferenceDrawerUtilities.SetObject(ref assetRef, ref referencesSame, elementProperty, obj, fieldInfo, m_label.text, out guid)) { TriggerOnValidate(elementProperty); aaSettings = AddressableAssetSettingsDefaultObject.GetSettings(true); var entry = aaSettings.FindAssetEntry(guid); if (entry == null && !string.IsNullOrEmpty(guid) && !aaSettings.IsAssetPathInAddressableDirectory(path, out string assetName)) aaSettings.CreateOrMoveEntry(guid, aaSettings.DefaultGroup); } else { arrayProperty.DeleteArrayElementAtIndex(arrayProperty.arraySize - 1); arrayProperty.serializedObject.ApplyModifiedProperties(); arrayProperty.serializedObject.Update(); } } } } } private void DrawSubAssetsControl(SerializedProperty property, List subAssets, bool isEnterKeyPressed) { var objRect = new Rect(assetDropDownRect.xMax, assetDropDownRect.y, assetDropDownRect.width, assetDropDownRect.height); float pickerWidth = 12f; Rect pickerRect = objRect; pickerRect.width = pickerWidth; pickerRect.x = objRect.xMax - pickerWidth * 1.33f; bool multipleSubassets = false; string controlName = GetControlName(k_SubFieldControlPrefix, property.name); // Check if targetObjects have multiple different selected if (property.serializedObject.targetObjects.Length > 1) multipleSubassets = AssetReferenceDrawerUtilities.CheckTargetObjectsSubassetsAreDifferent(property, m_AssetRefObject.SubObjectName, fieldInfo, m_label.text); bool isPickerPressed = Event.current.type == EventType.MouseDown && Event.current.button == 0 && pickerRect.Contains(Event.current.mousePosition); bool enterKeyRequestsPopup = isEnterKeyPressed && (controlName == UnityEngine.GUI.GetNameOfFocusedControl()); if (isPickerPressed || enterKeyRequestsPopup) { // Do custom popup with scroll to pick subasset if (m_SubassetPopup == null || m_SubassetPopup.m_property != property) { m_SubassetPopup = CreateSubAssetPopup(property, subAssets ?? AssetReferenceDrawerUtilities.GetSubAssetsList(m_AssetRefObject), Event.current.mousePosition); } } if (m_SubassetPopup != null && subassetPickerSelectionChanged) { m_SubassetPopup.UpdateSubAssets(); } // Show selected name GUIContent nameSelected = new GUIContent("--"); if (!multipleSubassets) nameSelected.text = AssetReferenceDrawerUtilities.FormatName(m_AssetRefObject.SubObjectName); UnityEngine.GUI.SetNextControlName(controlName); if (EditorGUI.DropdownButton(objRect, nameSelected, FocusType.Keyboard, EditorStyles.objectField)) UnityEngine.GUI.FocusControl(controlName); // Draw picker arrow DrawCaret(pickerRect); } internal void GetSelectedSubassetIndex(List subAssets, out int selIndex, out string[] objNames) { var subAssetNames = subAssets.Select(sa => sa == null ? "" : $"{AssetReferenceDrawerUtilities.FormatName(sa.name)}:{sa.GetType()}").ToList(); objNames = subAssetNames.ToArray(); selIndex = subAssetNames.IndexOf($"{m_AssetRefObject.SubObjectName}:{m_AssetRefObject.SubOjbectType}"); if (selIndex == -1) selIndex = 0; } SubassetPopup CreateSubAssetPopup(SerializedProperty property, List subAssets, Vector2 mouseLocation) { GetSelectedSubassetIndex(subAssets, out int selIndex, out string[] objNames); var subassetPopupWindow = EditorWindow.GetWindow(true, "Select Subasset"); subassetPopupWindow.Initialize(selIndex, objNames, subAssets, property, this, mouseLocation); return subassetPopupWindow; } } class SubassetPopup : EditorWindow { internal int SelectedIndex = 0; internal SerializedProperty m_property; private string[] m_objNames; private List m_subAssets; private AssetReferenceDrawer m_drawer; private Vector2 m_scrollPosition; public void Initialize(int selIndex, string[] objNames, List subAssets, SerializedProperty property, AssetReferenceDrawer drawer, Vector2 mouseLocation) { SelectedIndex = selIndex; m_objNames = objNames; m_property = property; m_drawer = drawer; m_subAssets = subAssets; Rect r = this.position; mouseLocation = GUIUtility.GUIToScreenPoint(mouseLocation); r.position = mouseLocation; this.position = r; } private void OnLostFocus() { Close(); } public void UpdateSubAssets() { if (m_drawer.subassetPickerSelectionChanged) { if (!AssetReferenceDrawerUtilities.SetSubAssets(m_property, m_subAssets[SelectedIndex], m_drawer.fieldInfo, m_drawer.m_label.text)) { Debug.LogError("Unable to set all of the objects selected subassets"); } else { m_property.serializedObject.ApplyModifiedProperties(); m_property.serializedObject.Update(); m_property.FindPropertyRelative("m_EditorAssetChanged").boolValue = false; } m_drawer.subassetPickerSelectionChanged = false; } } private void HandleKeyboardEvents() { if (Event.current.type == EventType.KeyDown && Event.current.isKey) { if (Event.current.keyCode == KeyCode.DownArrow && SelectedIndex < m_objNames.Length - 1) { m_property.serializedObject.ApplyModifiedPropertiesWithoutUndo(); // quickly shifting between the selected assets prevents some modifications from being applied ChangeSelectedAsset(SelectedIndex + 1); } else if (Event.current.keyCode == KeyCode.UpArrow && SelectedIndex > 0) { m_property.serializedObject.ApplyModifiedPropertiesWithoutUndo(); ChangeSelectedAsset(SelectedIndex - 1); } else if (Event.current.keyCode == KeyCode.KeypadEnter || Event.current.keyCode == KeyCode.Return) { Close(); } } } private void ChangeSelectedAsset(int newIndex) { if (SelectedIndex != newIndex) { SelectedIndex = newIndex; m_drawer.subassetPickerSelectionChanged = true; UpdateSubAssets(); } } private void OnGUI() { Rect rect = this.position; var labelStyle = new GUIStyle(UnityEngine.GUI.skin.label); labelStyle.contentOffset = new Vector2(10, 0); var selectedLabelStyle = new GUIStyle(labelStyle); Color selectionColor = new Color(0.3f, 0.5f, 0.7f); selectedLabelStyle.normal.textColor = selectionColor; selectedLabelStyle.hover.textColor = selectionColor; selectedLabelStyle.active.textColor = selectionColor; HandleKeyboardEvents(); EditorGUILayout.BeginVertical(); m_scrollPosition = EditorGUILayout.BeginScrollView(m_scrollPosition, GUILayout.Width(rect.width), GUILayout.Height(rect.height)); for (int i = 0; i < m_objNames.Length; i++) { if (i == SelectedIndex) GUILayout.Label(m_objNames[i], selectedLabelStyle); else GUILayout.Label(m_objNames[i], labelStyle); if (GUILayoutUtility.GetLastRect().Contains(Event.current.mousePosition) && Event.current.clickCount > 0) { if (Event.current.clickCount == 2) Close(); else ChangeSelectedAsset(i); break; } } EditorGUILayout.EndScrollView(); EditorGUILayout.EndVertical(); } } class AssetReferencePopup : EditorWindow { AssetReferenceTreeView m_Tree; TreeViewState m_TreeState; bool m_ShouldClose; void ForceClose() { m_ShouldClose = true; } string m_CurrentName = string.Empty; AssetReferenceDrawer m_Drawer; string m_GUID; string m_NonAddressedAsset; SearchField m_SearchField; public void Initialize(AssetReferenceDrawer drawer, string guid, string nonAddressedAsset, bool openedByEnterKey, Vector2 mouseLocation) { m_Drawer = drawer; m_GUID = guid; m_NonAddressedAsset = nonAddressedAsset; m_SearchField = new SearchField(); m_ShouldClose = false; Rect r = this.position; mouseLocation = GUIUtility.GUIToScreenPoint(mouseLocation); r.position = mouseLocation; this.position = r; m_SearchField.SetFocus(); m_SearchField.downOrUpArrowKeyPressed += () => { m_Tree.SetFocus(); }; if (m_Tree != null) m_Tree.SetInitialSelection(m_Drawer.m_AssetName); } private void OnLostFocus() { ForceClose(); } private void OnGUI() { Rect rect = this.position; int border = 4; int topPadding = 12; int searchHeight = 20; var searchRect = new Rect(border, topPadding, rect.width - border * 2, searchHeight); var remainTop = topPadding + searchHeight + border; var remainingRect = new Rect(border, topPadding + searchHeight + border, rect.width - border * 2, rect.height - remainTop - border); m_CurrentName = m_SearchField.OnGUI(searchRect, m_CurrentName); if (m_Tree == null) { if (m_TreeState == null) m_TreeState = new TreeViewState(); m_Tree = new AssetReferenceTreeView(m_TreeState, m_Drawer, this, m_GUID, m_NonAddressedAsset); m_Tree.Reload(); m_Tree.SetInitialSelection(m_Drawer.m_AssetName); } bool isKeyPressed = Event.current.type == EventType.KeyDown && Event.current.isKey; bool isEnterKeyPressed = isKeyPressed && (Event.current.keyCode == KeyCode.KeypadEnter || Event.current.keyCode == KeyCode.Return); bool isUpOrDownArrowPressed = isKeyPressed && (Event.current.keyCode == KeyCode.UpArrow || Event.current.keyCode == KeyCode.DownArrow); if (isUpOrDownArrowPressed) m_Tree.SetFocus(); m_Tree.searchString = m_CurrentName; m_Tree.IsEnterKeyPressed = isEnterKeyPressed; m_Tree.OnGUI(remainingRect); if (m_ShouldClose || isEnterKeyPressed) { GUIUtility.hotControl = 0; Close(); } } sealed class AssetRefTreeViewItem : TreeViewItem { public string AssetPath; private string m_Guid; public string Guid { get { if (string.IsNullOrEmpty(m_Guid)) m_Guid = AssetDatabase.AssetPathToGUID(AssetPath); return m_Guid; } } public AssetRefTreeViewItem(int id, int depth, string displayName, string path) : base(id, depth, displayName) { AssetPath = path; icon = AssetDatabase.GetCachedIcon(path) as Texture2D; } } internal class AssetReferenceTreeView : TreeView { AssetReferenceDrawer m_Drawer; AssetReferencePopup m_Popup; string m_GUID; string m_NonAddressedAsset; Texture2D m_WarningIcon; internal bool IsEnterKeyPressed { get; set; } public AssetReferenceTreeView(TreeViewState state, AssetReferenceDrawer drawer, AssetReferencePopup popup, string guid, string nonAddressedAsset) : base(state) { m_Drawer = drawer; m_Popup = popup; showBorder = true; showAlternatingRowBackgrounds = true; m_GUID = guid; m_NonAddressedAsset = nonAddressedAsset; m_WarningIcon = EditorGUIUtility.FindTexture("console.warnicon"); } public override void OnGUI(Rect rect) { base.OnGUI(rect); if (IsEnterKeyPressed && HasFocus()) { m_Popup.ForceClose(); } } protected override bool CanMultiSelect(TreeViewItem item) { return false; } protected override void DoubleClickedItem(int id) { var assetRefItem = FindItem(id, rootItem) as AssetRefTreeViewItem; if (assetRefItem != null && assetRefItem.Guid == m_GUID) m_Popup.ForceClose(); else if (assetRefItem != null && !string.IsNullOrEmpty(assetRefItem.AssetPath)) { m_Drawer.newGuid = assetRefItem.Guid; if (string.IsNullOrEmpty(m_Drawer.newGuid)) m_Drawer.newGuid = assetRefItem.AssetPath; } else { m_Drawer.newGuid = AssetReferenceDrawer.noAssetString; } m_Popup.ForceClose(); } protected override void SelectionChanged(IList selectedIds) { if (selectedIds != null && selectedIds.Count == 1) { string oldGuid = m_Drawer.newGuid; var assetRefItem = FindItem(selectedIds[0], rootItem) as AssetRefTreeViewItem; if (assetRefItem != null && !string.IsNullOrEmpty(assetRefItem.AssetPath)) { m_Drawer.newGuid = assetRefItem.Guid; if (string.IsNullOrEmpty(m_Drawer.newGuid)) m_Drawer.newGuid = assetRefItem.AssetPath; } else { m_Drawer.newGuid = AssetReferenceDrawer.noAssetString; } SetFocus(); if (m_Drawer.assetProperty != null && oldGuid != null) m_Drawer.ApplySelectionChanges(m_Drawer.assetProperty, AddressableAssetSettingsDefaultObject.Settings, ref oldGuid); } } protected override IList BuildRows(TreeViewItem root) { if (string.IsNullOrEmpty(searchString)) { return base.BuildRows(root); } List rows = new List(); foreach (var child in rootItem.children) { if (child.displayName.IndexOf(searchString, StringComparison.OrdinalIgnoreCase) >= 0) rows.Add(child); } return rows; } protected override TreeViewItem BuildRoot() { var root = new TreeViewItem(-1, -1); var aaSettings = AddressableAssetSettingsDefaultObject.Settings; if (aaSettings == null) { var message = "Use 'Window->Addressables' to initialize."; root.AddChild(new AssetRefTreeViewItem(message.GetHashCode(), 0, message, string.Empty)); } else { if (!string.IsNullOrEmpty(m_NonAddressedAsset)) { var item = new AssetRefTreeViewItem(m_NonAddressedAsset.GetHashCode(), 0, AssetReferenceDrawer.forceAddressableString + m_NonAddressedAsset, AssetReferenceDrawer.forceAddressableString); item.icon = m_WarningIcon; root.AddChild(item); } root.AddChild(new AssetRefTreeViewItem(AssetReferenceDrawer.noAssetString.GetHashCode(), 0, AssetReferenceDrawer.noAssetString, string.Empty)); var allAssets = new List(); aaSettings.GatherAllAssetReferenceDrawableEntries(allAssets); foreach (var entry in allAssets) { if (!entry.IsInResources && m_Drawer.ValidateAsset(entry)) { var child = new AssetRefTreeViewItem(entry.AssetPath.GetHashCode(), 0, entry.address, entry.AssetPath); root.AddChild(child); } } } return root; } internal void SetInitialSelection(string assetString) { foreach (var child in rootItem.children) { if (child.displayName.IndexOf(assetString, StringComparison.OrdinalIgnoreCase) >= 0) { SetSelection(new List {child.id}); return; } } } } } /// /// Used to manipulate data from a serialized property. /// public static class SerializedPropertyExtensions { /// /// Used to extract the target object from a serialized property. /// /// The type of the object to extract. /// The property containing the object. /// The field data. /// The label name. /// Returns the target object type. public static T GetActualObjectForSerializedProperty(this SerializedProperty property, FieldInfo field, ref string label) { try { if (property == null || field == null) return default(T); var serializedObject = property.serializedObject; if (serializedObject == null) { return default(T); } var targetObject = serializedObject.targetObject; if (property.depth > 0) { var slicedName = property.propertyPath.Split('.').ToList(); List arrayCounts = new List(); for (int index = 0; index < slicedName.Count; index++) { arrayCounts.Add(-1); var currName = slicedName[index]; #if NET_UNITY_4_8 if (currName.EndsWith(']')) #else if (currName.EndsWith("]")) #endif { var arraySlice = currName.Split('[', ']'); if (arraySlice.Length >= 2) { arrayCounts[index - 2] = Convert.ToInt32(arraySlice[1]); slicedName[index] = string.Empty; slicedName[index - 1] = string.Empty; } } } while (string.IsNullOrEmpty(slicedName.Last())) { int i = slicedName.Count - 1; slicedName.RemoveAt(i); arrayCounts.RemoveAt(i); } #if NET_UNITY_4_8 if (property.propertyPath.EndsWith(']')) #else if (property.propertyPath.EndsWith("]")) #endif { var slice = property.propertyPath.Split('[', ']'); if (slice.Length >= 2) label = "Element " + slice[slice.Length - 2]; } return DescendHierarchy(targetObject, slicedName, arrayCounts, 0); } var obj = field.GetValue(targetObject); return (T)obj; } catch { return default(T); } } static T DescendHierarchy(object targetObject, List splitName, List splitCounts, int depth) { if (depth >= splitName.Count) return default(T); var currName = splitName[depth]; if (string.IsNullOrEmpty(currName)) return DescendHierarchy(targetObject, splitName, splitCounts, depth + 1); int arrayIndex = splitCounts[depth]; var newField = targetObject.GetType().GetField(currName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); if (newField == null) { Type baseType = targetObject.GetType().BaseType; while (baseType != null && newField == null) { newField = baseType.GetField(currName, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); baseType = baseType.BaseType; } } var newObj = newField.GetValue(targetObject); if (depth == splitName.Count - 1) { T actualObject = default(T); if (arrayIndex >= 0) { if (newObj.GetType().IsArray && ((System.Array)newObj).Length > arrayIndex) actualObject = (T)((System.Array)newObj).GetValue(arrayIndex); var newObjList = newObj as IList; if (newObjList != null && newObjList.Count > arrayIndex) { actualObject = (T)newObjList[arrayIndex]; //if (actualObject == null) // actualObject = new T(); } } else { actualObject = (T)newObj; } return actualObject; } else if (arrayIndex >= 0) { if (newObj is IList) { IList list = (IList)newObj; newObj = list[arrayIndex]; } else if (newObj is System.Array) { System.Array a = (System.Array)newObj; newObj = a.GetValue(arrayIndex); } } return DescendHierarchy(newObj, splitName, splitCounts, depth + 1); } internal static string GetPropertyPathArrayName(string propertyPath) { #if NET_UNITY_4_8 if (propertyPath.EndsWith(']')) #else if (propertyPath.EndsWith("]")) #endif { int leftBracket = propertyPath.LastIndexOf('['); if (leftBracket > -1) { string arrayString = propertyPath.Substring(0, leftBracket); if (arrayString.EndsWith(".data", StringComparison.OrdinalIgnoreCase)) return arrayString.Substring(0, arrayString.Length - 5); // remove ".data" } } return string.Empty; } } /// /// Used to restrict a class to only allow items with specific labels. /// [AttributeUsage(AttributeTargets.Class)] public class AssetReferenceSurrogateAttribute : Attribute { /// /// The type of the attribute. /// public Type TargetType; /// /// Construct a new AssetReferenceSurrogateAttribute. /// /// The Type of the class in question. public AssetReferenceSurrogateAttribute(Type type) { TargetType = type; } } /// /// Surrogate to AssetReferenceUIRestriction. /// This surrogate class provides the editor-side implementation of AssetReferenceUIRestriction attribute /// Used to restrict an AssetReference field or property to only allow items with specific labels. This is only enforced through the UI. /// [AssetReferenceSurrogate(typeof(AssetReferenceUIRestriction))] public class AssetReferenceUIRestrictionSurrogate : AssetReferenceUIRestriction { AssetReferenceUIRestriction data; /// /// Sets the AssetReferenceUIRestriction for this surrogate /// /// To initialize AssetReferenceUIRestriction for surrogate public virtual void Init(AssetReferenceUIRestriction initData) { data = initData; } /// /// Validates the referenced asset allowable for this asset reference. /// /// The Object to validate. /// Whether the referenced asset is valid. public override bool ValidateAsset(Object obj) { return data.ValidateAsset(obj); } internal virtual bool ValidateAsset(IReferenceEntryData entryData) { return data.ValidateAsset(entryData?.AssetPath); } } /// /// Surrogate to AssetReferenceUILabelRestriction /// This surrogate class provides the editor-side implementation of AssetReferenceUILabelRestriction attribute /// Used to restrict an AssetReference field or property to only allow items wil specific labels. This is only enforced through the UI. /// [AssetReferenceSurrogate(typeof(AssetReferenceUILabelRestriction))] public class AssetReferenceUILabelRestrictionSurrogate : AssetReferenceUIRestrictionSurrogate { AssetReferenceUILabelRestriction data; /// /// Sets the AssetReferenceUILabelRestriction for this surrogate /// /// To initialize AssetReferenceUILabelRestriction field public override void Init(AssetReferenceUIRestriction initData) { data = initData as AssetReferenceUILabelRestriction; } /// public override bool ValidateAsset(Object obj) { var path = AssetDatabase.GetAssetOrScenePath(obj); return ValidateAsset(path); } /// public override bool ValidateAsset(string path) { if (AddressableAssetSettingsDefaultObject.Settings == null) return false; var guid = AssetDatabase.AssetPathToGUID(path); var entry = AddressableAssetSettingsDefaultObject.Settings.FindAssetEntry(guid, true); return ValidateAsset(entry); } internal override bool ValidateAsset(IReferenceEntryData entry) { if (entry != null) { foreach (var label in data.m_AllowedLabels) { if (entry.labels.Contains(label)) return true; } } return false; } /// public override string ToString() { return data.ToString(); } } /// /// Utility Class /// public class AssetReferenceUtility { /// /// Finds surrogate class for an Assembly with a particular TargetType /// /// Target Type to search /// Type of the surrogate found for the Assembly with a particular Target Type. public static Type GetSurrogate(Type targetType) { if (targetType == null) { Debug.LogError("targetType cannot be null"); return null; } Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies(); List typesList = new List(); foreach (Assembly assem in assemblies) { var assemblyTypeList = GatherTargetTypesFromAssembly(assem, targetType, out bool concreteTypeFound); if (concreteTypeFound == true) return assemblyTypeList[0]; typesList.AddRange(assemblyTypeList); } if (typesList.Count == 0) return null; typesList.Sort(AssetReferenceUtility.CompareTypes); return typesList[0]; } static int CompareTypes(object x, object y) { Type t1 = (Type)x; Type t2 = (Type)y; if (t1 == t2) return 0; else if (t1.IsAssignableFrom(t2)) return 1; else return -1; } private static List GatherTargetTypesFromAssembly(Assembly assembly, Type targetType, out bool concreteTypeFound) { List assignableTypesList = new List(); var typeList = assembly.GetTypes().Where(attrType => typeof(AssetReferenceUIRestrictionSurrogate).IsAssignableFrom(attrType)).ToList(); foreach (var type in typeList) { var customAttribute = type.GetCustomAttribute(); if (customAttribute == null) continue; if (customAttribute.TargetType == targetType) { assignableTypesList.Clear(); assignableTypesList.Add(type); concreteTypeFound = true; return assignableTypesList; } if (customAttribute.TargetType.IsAssignableFrom(targetType)) { assignableTypesList.Add(type); } } concreteTypeFound = false; return assignableTypesList; } } }