using System; using System.Collections.Generic; using System.Linq; using UnityEditor.AddressableAssets.Build; using UnityEditor.AddressableAssets.Settings; using UnityEngine; using UnityEngine.TestTools; namespace UnityEditor.AddressableAssets.GUI { using Object = UnityEngine.Object; [InitializeOnLoad, ExcludeFromCoverage] internal static class AddressableAssetInspectorGUI { static GUIStyle s_ToggleMixed; static GUIContent s_AddressableAssetToggleText; static GUIContent s_SystemSettingsGUIContent = new GUIContent("System Settings", "View Addressable Asset Settings"); static GUIContent s_GroupsDropdownLabelContent = new GUIContent("Group", "The Addressable Group that this asset is assigned to."); static string s_GroupsDropdownControlName = nameof(AddressableAssetInspectorGUI) + ".GroupsPopupField"; static Texture s_GroupsCaretTexture = null; static Texture s_FolderTexture = null; static AddressableAssetInspectorGUI() { s_ToggleMixed = null; s_AddressableAssetToggleText = new GUIContent("Addressable", "Check this to mark this asset as an Addressable Asset, which includes it in the bundled data and makes it loadable via script by its address."); Editor.finishedDefaultHeaderGUI += OnPostHeaderGUI; } static void SetAaEntry(AddressableAssetSettings aaSettings, List targetInfos, bool create) { if (create && aaSettings.DefaultGroup.ReadOnly) { Debug.LogError("Current default group is ReadOnly. Cannot add addressable assets to it"); return; } Undo.RecordObject(aaSettings, "AddressableAssetSettings"); if (!create) { List removedEntries = new List(targetInfos.Count); for (int i = 0; i < targetInfos.Count; ++i) { AddressableAssetEntry e = aaSettings.FindAssetEntry(targetInfos[i].Guid); AddressableAssetUtility.OpenAssetIfUsingVCIntegration(e.parentGroup); removedEntries.Add(e); aaSettings.RemoveAssetEntry(removedEntries[i], false); } if (removedEntries.Count > 0) aaSettings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryRemoved, removedEntries, true, false); } else { AddressableAssetGroup parentGroup = aaSettings.DefaultGroup; var resourceTargets = targetInfos.Where(ti => AddressableAssetUtility.IsInResources(ti.Path)); if (resourceTargets.Any()) { var resourcePaths = resourceTargets.Select(t => t.Path).ToList(); var resourceGuids = resourceTargets.Select(t => t.Guid).ToList(); AddressableAssetUtility.SafeMoveResourcesToGroup(aaSettings, parentGroup, resourcePaths, resourceGuids); } var otherTargetInfos = targetInfos.Except(resourceTargets); List otherTargetGuids = new List(targetInfos.Count); foreach (var info in otherTargetInfos) otherTargetGuids.Add(info.Guid); var entriesCreated = new List(); var entriesMoved = new List(); aaSettings.CreateOrMoveEntries(otherTargetGuids, parentGroup, entriesCreated, entriesMoved, false, false); bool openedInVC = false; if (entriesMoved.Count > 0) { AddressableAssetUtility.OpenAssetIfUsingVCIntegration(parentGroup); openedInVC = true; aaSettings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entriesMoved, true, false); } if (entriesCreated.Count > 0) { if (!openedInVC) AddressableAssetUtility.OpenAssetIfUsingVCIntegration(parentGroup); aaSettings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryAdded, entriesCreated, true, false); } } } [UnityEngine.TestTools.ExcludeFromCoverage] static void OnPostHeaderGUI(Editor editor) { var aaSettings = AddressableAssetSettingsDefaultObject.Settings; if (editor.targets.Length > 0) { // only display for the Prefab/Model importer not the displayed GameObjects if (editor.targets[0].GetType() == typeof(GameObject)) return; foreach (var t in editor.targets) { if (t is AddressableAssetGroupSchema) { GUILayout.BeginHorizontal(); GUILayout.Label("Profile: " + AddressableAssetSettingsDefaultObject.GetSettings(true).profileSettings .GetProfileName(AddressableAssetSettingsDefaultObject.GetSettings(true).activeProfileId)); GUILayout.FlexibleSpace(); if (GUILayout.Button(s_SystemSettingsGUIContent, "MiniButton")) { EditorGUIUtility.PingObject(AddressableAssetSettingsDefaultObject.Settings); Selection.activeObject = AddressableAssetSettingsDefaultObject.Settings; } GUILayout.EndHorizontal(); return; } } List targetInfos = GatherTargetInfos(editor.targets, aaSettings); if (targetInfos.Count == 0) return; bool targetHasAddressableSubObject = false; int mainAssetsAddressable = 0; int subAssetsAddressable = 0; foreach (TargetInfo info in targetInfos) { if (info.MainAssetEntry == null) continue; if (info.MainAssetEntry.IsSubAsset) subAssetsAddressable++; else mainAssetsAddressable++; if (!info.IsMainAsset) targetHasAddressableSubObject = true; } // Overrides a DisabledScope in the EditorElement.cs that disables GUI drawn in the header when the asset cannot be edited. bool prevEnabledState = UnityEngine.GUI.enabled; if (targetHasAddressableSubObject) UnityEngine.GUI.enabled = false; else { UnityEngine.GUI.enabled = true; foreach (var info in targetInfos) { if (!info.IsMainAsset) { UnityEngine.GUI.enabled = false; break; } } } int totalAddressableCount = mainAssetsAddressable + subAssetsAddressable; if (totalAddressableCount == 0) // nothing is addressable { if (GUILayout.Toggle(false, s_AddressableAssetToggleText, GUILayout.ExpandWidth(false))) SetAaEntry(AddressableAssetSettingsDefaultObject.GetSettings(true), targetInfos, true); } else if (totalAddressableCount == editor.targets.Length) // everything is addressable { var entryInfo = targetInfos[targetInfos.Count - 1]; if (entryInfo == null || entryInfo.MainAssetEntry == null) throw new NullReferenceException("EntryInfo incorrect for Addressables content."); GUILayout.BeginHorizontal(); if (mainAssetsAddressable > 0 && subAssetsAddressable > 0) { if (s_ToggleMixed == null) s_ToggleMixed = new GUIStyle("ToggleMixed"); if (GUILayout.Toggle(false, s_AddressableAssetToggleText, s_ToggleMixed, GUILayout.ExpandWidth(false))) SetAaEntry(aaSettings, targetInfos, true); } else if (mainAssetsAddressable > 0) { if (!GUILayout.Toggle(true, s_AddressableAssetToggleText, GUILayout.ExpandWidth(false))) { SetAaEntry(aaSettings, targetInfos, false); UnityEngine.GUI.enabled = prevEnabledState; GUIUtility.ExitGUI(); } } else if (GUILayout.Toggle(false, s_AddressableAssetToggleText, GUILayout.ExpandWidth(false))) SetAaEntry(aaSettings, targetInfos, true); if (editor.targets.Length == 1) { if (!entryInfo.IsMainAsset || entryInfo.MainAssetEntry.IsSubAsset) { bool preAddressPrevEnabledState = UnityEngine.GUI.enabled; UnityEngine.GUI.enabled = false; string address = entryInfo.Address + (entryInfo.IsMainAsset ? "" : $"[{entryInfo.TargetObject.name}]"); EditorGUILayout.DelayedTextField(address, GUILayout.ExpandWidth(true)); UnityEngine.GUI.enabled = preAddressPrevEnabledState; } else { string newAddress = EditorGUILayout.DelayedTextField(entryInfo.Address, GUILayout.ExpandWidth(true)); if (newAddress != entryInfo.Address) { if (newAddress.Contains('[') && newAddress.Contains(']')) Debug.LogErrorFormat("Rename of address '{0}' cannot contain '[ ]'.", entryInfo.Address); else { entryInfo.MainAssetEntry.address = newAddress; AddressableAssetUtility.OpenAssetIfUsingVCIntegration(entryInfo.MainAssetEntry.parentGroup, true); } } } } else { FindUniqueAssetGuids(targetInfos, out var uniqueAssetGuids, out var uniqueAddressableAssetGuids); EditorGUILayout.LabelField(uniqueAddressableAssetGuids.Count + " out of " + uniqueAssetGuids.Count + " assets are addressable."); } DrawSelectEntriesButton(targetInfos); GUILayout.EndHorizontal(); GUILayout.BeginHorizontal(); DrawGroupsDropdown(aaSettings, targetInfos); GUILayout.EndHorizontal(); } else // mixed addressable selected { GUILayout.BeginHorizontal(); if (s_ToggleMixed == null) s_ToggleMixed = new GUIStyle("ToggleMixed"); if (GUILayout.Toggle(false, s_AddressableAssetToggleText, s_ToggleMixed, GUILayout.ExpandWidth(false))) SetAaEntry(AddressableAssetSettingsDefaultObject.GetSettings(true), targetInfos, true); FindUniqueAssetGuids(targetInfos, out var uniqueAssetGuids, out var uniqueAddressableAssetGuids); EditorGUILayout.LabelField(uniqueAddressableAssetGuids.Count + " out of " + uniqueAssetGuids.Count + " assets are addressable."); DrawSelectEntriesButton(targetInfos); GUILayout.EndHorizontal(); } UnityEngine.GUI.enabled = prevEnabledState; } } // Caching due to Gathering TargetInfos is an expensive operation // The InspectorGUI needs to call this multiple times per layout and paint private static AddressableAssetSettings.Cache> s_Cache = null; internal static List GatherTargetInfos(Object[] targets, AddressableAssetSettings aaSettings) { int selectionHashCode = targets[0].GetHashCode(); for (int i = 1; i < targets.Length; ++i) selectionHashCode = selectionHashCode * 31 ^ targets[i].GetHashCode(); List targetInfos = null; if (s_Cache == null && aaSettings != null) s_Cache = new AddressableAssetSettings.Cache>(aaSettings); if (s_Cache != null && s_Cache.TryGetCached(selectionHashCode, out targetInfos)) return targetInfos; targetInfos = new List(targets.Length); AddressableAssetEntry entry; foreach (var t in targets) { if (AddressableAssetUtility.TryGetPathAndGUIDFromTarget(t, out var path, out var guid)) { var mainAssetType = AssetDatabase.GetMainAssetTypeAtPath(path); // Is asset if (mainAssetType != null && !BuildUtility.IsEditorAssembly(mainAssetType.Assembly)) { bool isMainAsset = t is AssetImporter || AssetDatabase.IsMainAsset(t); var info = new TargetInfo() {TargetObject = t, Guid = guid, Path = path, IsMainAsset = isMainAsset}; if (aaSettings != null) { entry = aaSettings.FindAssetEntry(guid, true); if (entry != null) info.MainAssetEntry = entry; } targetInfos.Add(info); } } } if (s_Cache != null && targetInfos != null && targetInfos.Count > 0) s_Cache.Add(selectionHashCode, targetInfos); return targetInfos; } internal static void FindUniqueAssetGuids(List targetInfos, out HashSet uniqueAssetGuids, out HashSet uniqueAddressableAssetGuids) { uniqueAssetGuids = new HashSet(); uniqueAddressableAssetGuids = new HashSet(); foreach (TargetInfo info in targetInfos) { uniqueAssetGuids.Add(info.Guid); if (info.MainAssetEntry != null) uniqueAddressableAssetGuids.Add(info.Guid); } } static void DrawSelectEntriesButton(List targets) { var prevGuiEnabled = UnityEngine.GUI.enabled; UnityEngine.GUI.enabled = true; if (GUILayout.Button("Select")) { AddressableAssetsWindow.Init(); var window = EditorWindow.GetWindow(); List entries = new List(targets.Count); foreach (TargetInfo info in targets) { if (info.MainAssetEntry != null) { if (info.IsMainAsset == false && ProjectConfigData.ShowSubObjectsInGroupView) { List subs = new List(); info.MainAssetEntry.GatherAllAssets(subs, false, true, true); foreach (AddressableAssetEntry sub in subs) { if (sub.TargetAsset == info.TargetObject) { entries.Add(sub); break; } } } else entries.Add(info.MainAssetEntry); } } if (entries.Count > 0) window.SelectAssetsInGroupEditor(entries); } UnityEngine.GUI.enabled = prevGuiEnabled; } static void DrawGroupsDropdown(AddressableAssetSettings settings, List targets) { bool canEditGroup = true; bool mixedGroups = false; AddressableAssetGroup displayGroup = null; var entries = new List(); foreach (TargetInfo info in targets) { AddressableAssetEntry entry = info.MainAssetEntry; if (entry == null) { canEditGroup = false; } else { entries.Add(entry); if (entry.ReadOnly || entry.parentGroup.ReadOnly) { canEditGroup = false; } if (displayGroup == null) displayGroup = entry.parentGroup; else if (entry.parentGroup != displayGroup) { mixedGroups = true; } } } using (new EditorGUI.DisabledScope(!canEditGroup)) { GUILayout.Label(s_GroupsDropdownLabelContent); if (mixedGroups) EditorGUI.showMixedValue = true; UnityEngine.GUI.SetNextControlName(s_GroupsDropdownControlName); float iconHeight = EditorGUIUtility.singleLineHeight - EditorGUIUtility.standardVerticalSpacing * 3; Vector2 iconSize = EditorGUIUtility.GetIconSize(); EditorGUIUtility.SetIconSize(new Vector2(iconHeight, iconHeight)); if (s_FolderTexture == null) { s_FolderTexture = EditorGUIUtility.IconContent("Folder Icon").image; } GUIContent groupGUIContent = new GUIContent(displayGroup.Name, s_FolderTexture); Rect groupFieldRect = GUILayoutUtility.GetRect(groupGUIContent, EditorStyles.objectField); EditorGUI.DropdownButton(groupFieldRect, groupGUIContent, FocusType.Keyboard, EditorStyles.objectField); EditorGUIUtility.SetIconSize(new Vector2(iconSize.x, iconSize.y)); if (mixedGroups) EditorGUI.showMixedValue = false; float pickerWidth = 12f; Rect groupFieldRectNoPicker = new Rect(groupFieldRect); groupFieldRectNoPicker.xMax = groupFieldRect.xMax - pickerWidth * 1.33f; Rect pickerRect = new Rect(groupFieldRectNoPicker.xMax, groupFieldRectNoPicker.y, pickerWidth, groupFieldRectNoPicker.height); bool isPickerPressed = Event.current.clickCount == 1 && pickerRect.Contains(Event.current.mousePosition); DrawCaret(pickerRect); if (canEditGroup) { bool isEnterKeyPressed = Event.current.type == EventType.KeyDown && Event.current.isKey && (Event.current.keyCode == KeyCode.KeypadEnter || Event.current.keyCode == KeyCode.Return); bool enterKeyRequestsPopup = isEnterKeyPressed && (s_GroupsDropdownControlName == UnityEngine.GUI.GetNameOfFocusedControl()); if (isPickerPressed || enterKeyRequestsPopup) { EditorWindow.GetWindow(true, "Select Addressable Group").Initialize(settings, entries, !mixedGroups, true, Event.current.mousePosition, AddressableAssetUtility.MoveEntriesToGroup); } bool isDragging = Event.current.type == EventType.DragUpdated && groupFieldRectNoPicker.Contains(Event.current.mousePosition); bool isDropping = Event.current.type == EventType.DragPerform && groupFieldRectNoPicker.Contains(Event.current.mousePosition); HandleDragAndDrop(settings, entries, isDragging, isDropping); } if (!mixedGroups) { if (Event.current.clickCount == 1 && groupFieldRectNoPicker.Contains(Event.current.mousePosition)) { UnityEngine.GUI.FocusControl(s_GroupsDropdownControlName); AddressableAssetsWindow.Init(); var window = EditorWindow.GetWindow(); window.SelectGroupInGroupEditor(displayGroup, false); } if (Event.current.clickCount == 2 && groupFieldRectNoPicker.Contains(Event.current.mousePosition)) { AddressableAssetsWindow.Init(); var window = EditorWindow.GetWindow(); window.SelectGroupInGroupEditor(displayGroup, true); } } } } static void DrawCaret(Rect pickerRect) { if (s_GroupsCaretTexture == null) { s_GroupsCaretTexture = EditorGUIUtility.IconContent("d_pick").image; } UnityEngine.GUI.DrawTexture(pickerRect, s_GroupsCaretTexture, ScaleMode.ScaleToFit); } static void HandleDragAndDrop(AddressableAssetSettings settings, List aaEntries, bool isDragging, bool isDropping) { var groupItems = DragAndDrop.GetGenericData("AssetEntryTreeViewItem") as List; if (isDragging) { bool canDragGroup = groupItems != null && groupItems.Count == 1 && groupItems[0].IsGroup && !groupItems[0].group.ReadOnly; DragAndDrop.visualMode = canDragGroup ? DragAndDropVisualMode.Copy : DragAndDropVisualMode.Rejected; } else if (isDropping) { if (groupItems != null) { var group = groupItems[0].group; AddressableAssetUtility.MoveEntriesToGroup(settings, aaEntries, group); } } } internal class TargetInfo { public UnityEngine.Object TargetObject; public string Guid; public string Path; public bool IsMainAsset; public AddressableAssetEntry MainAssetEntry; public string Address { get { if (MainAssetEntry == null) throw new NullReferenceException("No Entry set for Target info with AssetPath " + Path); return MainAssetEntry.address; } } } } }