using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Text.RegularExpressions; using UnityEditor.AddressableAssets.Settings; using UnityEditor.IMGUI.Controls; using UnityEngine; using UnityEngine.Assertions; using UnityEngine.AddressableAssets; using Debug = UnityEngine.Debug; using static UnityEditor.AddressableAssets.Settings.AddressablesFileEnumeration; using UnityEditor.AddressableAssets.Build; namespace UnityEditor.AddressableAssets.GUI { using Object = UnityEngine.Object; internal class AddressableAssetEntryTreeView : TreeView { AddressableAssetsSettingsGroupEditor m_Editor; internal string customSearchString = string.Empty; string m_FirstSelectedGroup; private readonly Dictionary m_SearchedEntries = new Dictionary(); private bool m_ForceSelectionClear = false; enum ColumnId { Notification, Id, Type, Path, Labels } ColumnId[] m_SortOptions = { ColumnId.Notification, ColumnId.Id, ColumnId.Type, ColumnId.Path, ColumnId.Labels }; internal AddressableAssetEntryTreeView(AddressableAssetSettings settings) : this(new TreeViewState(), CreateDefaultMultiColumnHeaderState(), new AddressableAssetsSettingsGroupEditor(ScriptableObject.CreateInstance())) { m_Editor.settings = settings; } public AddressableAssetEntryTreeView(TreeViewState state, MultiColumnHeaderState mchs, AddressableAssetsSettingsGroupEditor ed) : base(state, new MultiColumnHeader(mchs)) { showBorder = true; m_Editor = ed; columnIndexForTreeFoldouts = 1; multiColumnHeader.sortingChanged += OnSortingChanged; BuiltinSceneCache.sceneListChanged += OnScenesChanged; AddressablesAssetPostProcessor.OnPostProcess.Register(OnPostProcessAllAssets, 1); } GUIContent m_WarningIcon; GUIContent WarningIcon { get { if (m_WarningIcon == null) m_WarningIcon = EditorGUIUtility.IconContent("console.warnicon.sml"); return m_WarningIcon; } } internal TreeViewItem Root => rootItem; void OnScenesChanged() { if (m_Editor.settings == null) return; Reload(); } void OnSortingChanged(MultiColumnHeader mch) { //This is where the sort happens in the groups view SortChildren(rootItem); Reload(); } void OnPostProcessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) { foreach (Object obj in Selection.objects) { if (obj == null) continue; if (!AssetDatabase.TryGetGUIDAndLocalFileIdentifier(obj.GetInstanceID(), out string guid, out long localId)) { if (obj is GameObject go) { #if UNITY_2021_2_OR_NEWER if (UnityEditor.SceneManagement.PrefabStageUtility.GetPrefabStage(go) != null) return; #else if (UnityEditor.Experimental.SceneManagement.PrefabStageUtility.GetPrefabStage(go) != null) return; #endif var containingScene = go.scene; if (containingScene.IsValid() && containingScene.isLoaded) return; } m_ForceSelectionClear = true; return; } } } protected override void SelectionChanged(IList selectedIds) { if (selectedIds.Count == 1) { var item = FindItemInVisibleRows(selectedIds[0]); if (item != null && item.group != null) { m_FirstSelectedGroup = item.group.name; } } base.SelectionChanged(selectedIds); UnityEngine.Object[] selectedObjects = new UnityEngine.Object[selectedIds.Count]; for (int i = 0; i < selectedIds.Count; i++) { var item = FindItemInVisibleRows(selectedIds[i]); if (item != null) { if (item.group != null) selectedObjects[i] = item.group; else if (item.entry != null) selectedObjects[i] = item.entry.TargetAsset; } } // Make last selected group the first object in the array if (!string.IsNullOrEmpty(m_FirstSelectedGroup) && selectedObjects.Length > 1) { for (int i = 0; i < selectedObjects.Length - 1; ++i) { if (selectedObjects[i] != null && selectedObjects[i].name == m_FirstSelectedGroup) { var temp = selectedObjects[i]; selectedObjects[i] = selectedObjects[selectedIds.Count - 1]; selectedObjects[selectedIds.Count - 1] = temp; } } } Selection.objects = selectedObjects; // change selection } protected override TreeViewItem BuildRoot() { var root = new TreeViewItem(-1, -1); using (new AddressablesFileEnumerationScope(BuildAddressableTree(m_Editor.settings))) { foreach (var group in m_Editor.settings.groups) AddGroupChildrenBuild(group, root); } return root; } protected override IList BuildRows(TreeViewItem root) { if (!string.IsNullOrEmpty(searchString)) { var rows = base.BuildRows(root); SortHierarchical(rows); return rows; } if (!string.IsNullOrEmpty(customSearchString)) { SortChildren(root); return Search(base.BuildRows(root)); } SortChildren(root); return base.BuildRows(root); } internal IList Search(string search) { if (ProjectConfigData.HierarchicalSearch) { customSearchString = search; Reload(); } else { searchString = search; } return GetRows(); } protected IList Search(IList rows) { if (rows == null) return new List(); m_SearchedEntries.Clear(); List items = new List(rows.Count); foreach (TreeViewItem item in rows) { if (ProjectConfigData.HierarchicalSearch) { if (SearchHierarchical(item, customSearchString)) items.Add(item); } else if (DoesItemMatchSearch(item, searchString)) items.Add(item); } return items; } /* * Hierarchical search requirements : * An item is kept if : * - it matches * - an ancestor matches * - at least one descendant matches */ bool SearchHierarchical(TreeViewItem item, string search, bool? ancestorMatching = null) { var aeItem = item as AssetEntryTreeViewItem; if (aeItem == null || search == null) return false; if (m_SearchedEntries.ContainsKey(aeItem)) return m_SearchedEntries[aeItem]; if (ancestorMatching == null) ancestorMatching = DoesAncestorMatch(aeItem, search); bool isMatching = false; if (!ancestorMatching.Value) isMatching = DoesItemMatchSearch(aeItem, search); bool descendantMatching = false; if (!ancestorMatching.Value && !isMatching && aeItem.hasChildren) { foreach (var child in aeItem.children) { descendantMatching = SearchHierarchical(child, search, false); if (descendantMatching) break; } } bool keep = isMatching || ancestorMatching.Value || descendantMatching; m_SearchedEntries.Add(aeItem, keep); return keep; } private bool DoesAncestorMatch(TreeViewItem aeItem, string search) { if (aeItem == null) return false; var ancestor = aeItem.parent as AssetEntryTreeViewItem; bool isMatching = DoesItemMatchSearch(ancestor, search); while (ancestor != null && !isMatching) { ancestor = ancestor.parent as AssetEntryTreeViewItem; isMatching = DoesItemMatchSearch(ancestor, search); } return isMatching; } internal void ClearSearch() { customSearchString = string.Empty; searchString = string.Empty; m_SearchedEntries.Clear(); } internal void SwapSearchType() { string temp = customSearchString; customSearchString = searchString; searchString = temp; m_SearchedEntries.Clear(); } void SortChildren(TreeViewItem root) { if (!root.hasChildren) return; foreach (var child in root.children) { if (child != null && IsExpanded(child.id)) SortHierarchical(child.children); } } void SortHierarchical(IList children) { if (children == null) return; var sortedColumns = multiColumnHeader.state.sortedColumns; if (sortedColumns.Length == 0) return; List kids = new List(); List copy = new List(children); children.Clear(); foreach (var c in copy) { var child = c as AssetEntryTreeViewItem; if (child != null && child.entry != null) kids.Add(child); else children.Add(c); } ColumnId col = m_SortOptions[sortedColumns[0]]; bool ascending = multiColumnHeader.IsSortedAscending(sortedColumns[0]); IEnumerable orderedKids = kids; switch (col) { case ColumnId.Notification: case ColumnId.Type: break; case ColumnId.Path: orderedKids = kids.Order(l => l.entry.AssetPath, ascending); break; case ColumnId.Labels: orderedKids = OrderByLabels(kids, ascending); break; default: orderedKids = kids.Order(l => l.displayName, ascending); break; } foreach (var o in orderedKids) children.Add(o); foreach (var child in children) { if (child != null && IsExpanded(child.id)) SortHierarchical(child.children); } } IEnumerable OrderByLabels(List kids, bool ascending) { var emptyHalf = new List(); var namedHalf = new List(); foreach (var k in kids) { if (k.entry == null || k.entry.labels == null || k.entry.labels.Count < 1) emptyHalf.Add(k); else namedHalf.Add(k); } var orderedKids = namedHalf.Order(l => m_Editor.settings.labelTable.GetString(l.entry.labels, 200), ascending); List result = new List(); if (ascending) { result.AddRange(emptyHalf); result.AddRange(orderedKids); } else { result.AddRange(orderedKids); result.AddRange(emptyHalf); } return result; } protected override bool DoesItemMatchSearch(TreeViewItem item, string search) { if (string.IsNullOrEmpty(search)) return true; var aeItem = item as AssetEntryTreeViewItem; if (aeItem == null) return false; //check if item matches. if (aeItem.displayName.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) return true; if (aeItem.entry == null) return false; if (aeItem.entry.AssetPath.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) return true; foreach (string label in aeItem.entry.labels) { if (label.IndexOf(search, StringComparison.OrdinalIgnoreCase) >= 0) return true; } return false; } void AddGroupChildrenBuild(AddressableAssetGroup group, TreeViewItem root) { int depth = 0; AssetEntryTreeViewItem groupItem = null; if (ProjectConfigData.ShowGroupsAsHierarchy && group != null) { //// dash in name imitates hiearchy. TreeViewItem newRoot = root; var parts = group.Name.Split('-'); string partialRestore = ""; for (int index = 0; index < parts.Length - 1; index++) { TreeViewItem folderItem = null; partialRestore += parts[index]; int hash = partialRestore.GetHashCode(); if (!TryGetChild(newRoot, hash, ref folderItem)) { folderItem = new AssetEntryTreeViewItem(parts[index], depth, hash); newRoot.AddChild(folderItem); } depth++; newRoot = folderItem; } groupItem = new AssetEntryTreeViewItem(group, depth); newRoot.AddChild(groupItem); } else { groupItem = new AssetEntryTreeViewItem(group, 0); root.AddChild(groupItem); } if (group != null && group.entries.Count > 0) { foreach (var entry in group.entries) { AddAndRecurseEntriesBuild(entry, groupItem, depth + 1, IsExpanded(groupItem.id)); } } } bool TryGetChild(TreeViewItem root, int childHash, ref TreeViewItem childItem) { if (root.children == null) return false; foreach (var child in root.children) { if (child.id == childHash) { childItem = child; return true; } } return false; } void AddAndRecurseEntriesBuild(AddressableAssetEntry entry, AssetEntryTreeViewItem parent, int depth, bool expanded) { var item = new AssetEntryTreeViewItem(entry, depth); parent.AddChild(item); if (!expanded) { item.checkedForChildren = false; return; } RecurseEntryChildren(entry, item, depth); } internal void RecurseEntryChildren(AddressableAssetEntry entry, AssetEntryTreeViewItem item, int depth) { item.checkedForChildren = true; var subAssets = new List(); bool includeSubObjects = ProjectConfigData.ShowSubObjectsInGroupView && !entry.IsFolder && !string.IsNullOrEmpty(entry.guid); entry.GatherAllAssets(subAssets, false, entry.IsInResources, includeSubObjects); if (subAssets.Count > 0) { foreach (var e in subAssets) { if (e.guid.Length > 0 && e.address.Contains('[') && e.address.Contains(']')) Debug.LogErrorFormat("Subasset address '{0}' cannot contain '[ ]'.", e.address); AddAndRecurseEntriesBuild(e, item, depth + 1, IsExpanded(item.id)); } } } protected override void ExpandedStateChanged() { foreach (var id in state.expandedIDs) { var item = FindItem(id, rootItem); if (item != null && item.hasChildren) { foreach (AssetEntryTreeViewItem c in item.children) if (!c.checkedForChildren) RecurseEntryChildren(c.entry, c, c.depth + 1); } } } public override void OnGUI(Rect rect) { base.OnGUI(rect); //TODO - this occasionally causes a "hot control" issue. if (m_ForceSelectionClear || (Event.current.type == EventType.MouseDown && Event.current.button == 0 && rect.Contains(Event.current.mousePosition))) { SetSelection(new int[0], TreeViewSelectionOptions.FireSelectionChanged); if (m_ForceSelectionClear) m_ForceSelectionClear = false; } } protected override void BeforeRowsGUI() { base.BeforeRowsGUI(); if (Event.current.type == EventType.Repaint) { var rows = GetRows(); if (rows.Count > 0) { int first; int last; GetFirstAndLastVisibleRows(out first, out last); for (int rowId = first; rowId <= last; rowId++) { var aeI = rows[rowId] as AssetEntryTreeViewItem; if (aeI != null && aeI.entry != null) { DefaultStyles.backgroundEven.Draw(GetRowRect(rowId), false, false, false, false); } } } } } GUIStyle m_LabelStyle; protected override void RowGUI(RowGUIArgs args) { if (m_LabelStyle == null) { m_LabelStyle = new GUIStyle("PR Label"); if (m_LabelStyle == null) m_LabelStyle = UnityEngine.GUI.skin.GetStyle("Label"); } var item = args.item as AssetEntryTreeViewItem; if (item == null || item.group == null && item.entry == null) { using (new EditorGUI.DisabledScope(true)) base.RowGUI(args); } else { bool isReadOnly = item.group == null ? item.entry.ReadOnly : item.group.ReadOnly; if (item.group != null) { if (item.isRenaming && !args.isRenaming) item.isRenaming = false; } using (new EditorGUI.DisabledScope(isReadOnly)) { for (int i = 0; i < args.GetNumVisibleColumns(); ++i) CellGUI(args.GetCellRect(i), item, args.GetColumn(i), ref args); } } } void CellGUI(Rect cellRect, AssetEntryTreeViewItem item, int column, ref RowGUIArgs args) { CenterRectUsingSingleLineHeight(ref cellRect); switch ((ColumnId)column) { case ColumnId.Notification: bool flaggedForUpdateWarning = item.entry == null ? item.group.FlaggedDuringContentUpdateRestriction : item.entry.FlaggedDuringContentUpdateRestriction; if (flaggedForUpdateWarning) { var notification = WarningIcon; if (item.group != null) notification.tooltip = "This group contains assets with the setting �Prevent Updates� that have been modified. " + "To resolve, change the group setting, or move the assets to a different group."; else if (item.entry != null) notification.tooltip = "This asset has been modified, but it is in a group with the setting �Prevent Updates�. " + "To resolve, change the group setting, or move the asset to a different group."; UnityEngine.GUI.Label(cellRect, notification); } break; case ColumnId.Id: { args.rowRect = cellRect; base.RowGUI(args); } break; case ColumnId.Path: if (item.entry != null && Event.current.type == EventType.Repaint) { var path = item.entry.AssetPath; if (string.IsNullOrEmpty(path)) path = item.entry.ReadOnly ? "" : "Missing File"; m_LabelStyle.Draw(cellRect, path, false, false, args.selected, args.focused); } break; case ColumnId.Type: if (item.assetIcon != null) UnityEngine.GUI.DrawTexture(cellRect, item.assetIcon, ScaleMode.ScaleToFit, true); break; case ColumnId.Labels: if (item.entry != null && EditorGUI.DropdownButton(cellRect, new GUIContent(m_Editor.settings.labelTable.GetString(item.entry.labels, cellRect.width)), FocusType.Passive)) { var selection = GetItemsForContext(args.item.id); Dictionary labelCounts = new Dictionary(); List entries = new List(); var newSelection = new List(); foreach (var s in selection) { var aeItem = FindItem(s, rootItem) as AssetEntryTreeViewItem; if (aeItem == null || aeItem.entry == null) continue; entries.Add(aeItem.entry); newSelection.Add(s); foreach (var label in aeItem.entry.labels) { int count; labelCounts.TryGetValue(label, out count); count++; labelCounts[label] = count; } } SetSelection(newSelection); PopupWindow.Show(cellRect, new LabelMaskPopupContent(cellRect, m_Editor.settings, entries, labelCounts)); } break; } } IList GetItemsForContext(int row) { var selection = GetSelection(); if (selection.Contains(row)) return selection; selection = new List(); selection.Add(row); return selection; } public static MultiColumnHeaderState CreateDefaultMultiColumnHeaderState() { return new MultiColumnHeaderState(GetColumns()); } static MultiColumnHeaderState.Column[] GetColumns() { var retVal = new[] { new MultiColumnHeaderState.Column(), new MultiColumnHeaderState.Column(), new MultiColumnHeaderState.Column(), new MultiColumnHeaderState.Column(), new MultiColumnHeaderState.Column(), }; int counter = 0; retVal[counter].headerContent = new GUIContent(EditorGUIUtility.FindTexture("_Help@2x"), "Notifications"); retVal[counter].minWidth = 25; retVal[counter].width = 25; retVal[counter].maxWidth = 25; retVal[counter].headerTextAlignment = TextAlignment.Left; retVal[counter].canSort = false; retVal[counter].autoResize = true; counter++; retVal[counter].headerContent = new GUIContent("Group Name \\ Addressable Name", "Address used to load asset at runtime"); retVal[counter].minWidth = 100; retVal[counter].width = 260; retVal[counter].maxWidth = 10000; retVal[counter].headerTextAlignment = TextAlignment.Left; retVal[counter].canSort = true; retVal[counter].autoResize = true; counter++; retVal[counter].headerContent = new GUIContent(EditorGUIUtility.FindTexture("FilterByType"), "Asset type"); retVal[counter].minWidth = 20; retVal[counter].width = 20; retVal[counter].maxWidth = 20; retVal[counter].headerTextAlignment = TextAlignment.Left; retVal[counter].canSort = false; retVal[counter].autoResize = true; counter++; retVal[counter].headerContent = new GUIContent("Path", "Current Path of asset"); retVal[counter].minWidth = 100; retVal[counter].width = 150; retVal[counter].maxWidth = 10000; retVal[counter].headerTextAlignment = TextAlignment.Left; retVal[counter].canSort = true; retVal[counter].autoResize = true; counter++; retVal[counter].headerContent = new GUIContent("Labels", "Assets can have multiple labels"); retVal[counter].minWidth = 20; retVal[counter].width = 160; retVal[counter].maxWidth = 1000; retVal[counter].headerTextAlignment = TextAlignment.Left; retVal[counter].canSort = true; retVal[counter].autoResize = true; return retVal; } protected string CheckForRename(TreeViewItem item, bool isActualRename) { string result = string.Empty; var assetItem = item as AssetEntryTreeViewItem; if (assetItem != null) { if (assetItem.group != null && !assetItem.group.ReadOnly) result = "Rename"; else if (assetItem.entry != null && !assetItem.entry.ReadOnly) result = "Change Address"; if (isActualRename) assetItem.isRenaming = !string.IsNullOrEmpty(result); } return result; } protected override bool CanRename(TreeViewItem item) { return !string.IsNullOrEmpty(CheckForRename(item, true)); } AssetEntryTreeViewItem FindItemInVisibleRows(int id) { var rows = GetRows(); foreach (var r in rows) { if (r.id == id) { return r as AssetEntryTreeViewItem; } } return null; } protected override void RenameEnded(RenameEndedArgs args) { if (!args.acceptedRename) return; var item = FindItemInVisibleRows(args.itemID); if (item != null) { item.isRenaming = false; } if (args.originalName == args.newName) return; if (item != null) { if (args.newName != null && args.newName.Contains("[") && args.newName.Contains("]")) { args.acceptedRename = false; Debug.LogErrorFormat("Rename of address '{0}' cannot contain '[ ]'.", args.originalName); } else if (item.entry != null) { item.entry.address = args.newName; AddressableAssetUtility.OpenAssetIfUsingVCIntegration(item.entry.parentGroup, true); } else if (item.group != null) { if (m_Editor.settings.IsNotUniqueGroupName(args.newName)) { args.acceptedRename = false; Addressables.LogWarning("There is already a group named '" + args.newName + "'. Cannot rename this group to match"); } else { item.group.Name = args.newName; AddressableAssetUtility.OpenAssetIfUsingVCIntegration(item.group, true); AddressableAssetUtility.OpenAssetIfUsingVCIntegration(item.group.Settings, true); } } Reload(); } } protected override bool CanMultiSelect(TreeViewItem item) { return true; } protected override void DoubleClickedItem(int id) { var item = FindItemInVisibleRows(id); if (item != null) { Object o = null; if (item.entry != null) o = AssetDatabase.LoadAssetAtPath(item.entry.AssetPath); else if (item.group != null) o = item.group; if (o != null) { EditorGUIUtility.PingObject(o); Selection.activeObject = o; } } } bool m_ContextOnItem; protected override void ContextClicked() { if (m_ContextOnItem) { m_ContextOnItem = false; return; } GenericMenu menu = new GenericMenu(); PopulateGeneralContextMenu(ref menu); menu.ShowAsContext(); } void PopulateGeneralContextMenu(ref GenericMenu menu) { foreach (var templateObject in m_Editor.settings.GroupTemplateObjects) { Assert.IsNotNull(templateObject); menu.AddItem(new GUIContent("Create New Group/" + templateObject.name), false, CreateNewGroup, templateObject); } menu.AddItem(new GUIContent("Clear Content Update Warnings"), false, ClearContentUpdateWarnings); } void ClearContentUpdateWarnings() { foreach (var group in m_Editor.settings.groups) ContentUpdateScript.ClearContentUpdateNotifications(group); Reload(); } void HandleCustomContextMenuItemGroups(object context) { var d = context as Tuple>; AddressableAssetSettings.InvokeAssetGroupCommand(d.Item1, d.Item2.Select(s => s.group)); } void HandleCustomContextMenuItemEntries(object context) { var d = context as Tuple>; AddressableAssetSettings.InvokeAssetEntryCommand(d.Item1, d.Item2.Select(s => s.entry)); } protected override void ContextClickedItem(int id) { List selectedNodes = new List(); foreach (var nodeId in GetSelection()) { var item = FindItemInVisibleRows(nodeId); //TODO - this probably makes off-screen but selected items not get added to list. if (item != null) selectedNodes.Add(item); } if (selectedNodes.Count == 0) return; m_ContextOnItem = true; bool isGroup = false; bool isEntry = false; bool hasReadOnly = false; int resourceCount = 0; bool isResourcesHeader = false; bool isMissingPath = false; foreach (var item in selectedNodes) { if (item.group != null) { hasReadOnly |= item.group.ReadOnly; isGroup = true; } else if (item.entry != null) { if (item.entry.AssetPath == AddressableAssetEntry.ResourcesPath) { if (selectedNodes.Count > 1) return; isResourcesHeader = true; } else if (item.entry.AssetPath == AddressableAssetEntry.EditorSceneListPath) { return; } hasReadOnly |= item.entry.ReadOnly; hasReadOnly |= item.entry.parentGroup.ReadOnly; isEntry = true; resourceCount += item.entry.IsInResources ? 1 : 0; isMissingPath |= string.IsNullOrEmpty(item.entry.AssetPath); } else if (!string.IsNullOrEmpty(item.folderPath)) { hasReadOnly = true; } } if (isEntry && isGroup) return; GenericMenu menu = new GenericMenu(); if (isResourcesHeader) { menu.AddItem(new GUIContent("Move All Resources to Group..."), false, MoveAllResourcesToGroup, Event.current); } else if (!hasReadOnly) { if (isGroup) { var group = selectedNodes.First().group; if (!group.IsDefaultGroup()) menu.AddItem(new GUIContent("Remove Group(s)"), false, RemoveGroup, selectedNodes); menu.AddItem(new GUIContent("Simplify Addressable Names"), false, SimplifyAddresses, selectedNodes); if (selectedNodes.Count == 1) { if (!group.IsDefaultGroup() && group.CanBeSetAsDefault()) menu.AddItem(new GUIContent("Set as Default"), false, SetGroupAsDefault, selectedNodes); menu.AddItem(new GUIContent("Inspect Group Settings"), false, GoToGroupAsset, selectedNodes); } foreach (var i in AddressableAssetSettings.CustomAssetGroupCommands) menu.AddItem(new GUIContent(i), false, HandleCustomContextMenuItemGroups, new Tuple>(i, selectedNodes)); } else if (isEntry) { menu.AddItem(new GUIContent("Move Addressables to Group..."), false, MoveEntriesToGroup, new Tuple>(Event.current, selectedNodes)); menu.AddItem(new GUIContent("Move Addressables to New Group with settings from..."), false, MoveEntriesToNewGroupWithSettings, new Tuple>(Event.current, selectedNodes)); menu.AddItem(new GUIContent("Remove Addressables"), false, RemoveEntry, selectedNodes); menu.AddItem(new GUIContent("Simplify Addressable Names"), false, SimplifyAddresses, selectedNodes); if (selectedNodes.Count == 1) menu.AddItem(new GUIContent("Copy Address to Clipboard"), false, CopyAddressesToClipboard, selectedNodes); else if (selectedNodes.Count > 1) menu.AddItem(new GUIContent("Copy " + selectedNodes.Count + " Addresses to Clipboard"), false, CopyAddressesToClipboard, selectedNodes); foreach (var i in AddressableAssetSettings.CustomAssetEntryCommands) menu.AddItem(new GUIContent(i), false, HandleCustomContextMenuItemEntries, new Tuple>(i, selectedNodes)); } else menu.AddItem(new GUIContent("Clear missing references."), false, RemoveMissingReferences); } else { if (isEntry) { if (!isMissingPath) { if (resourceCount == selectedNodes.Count) { menu.AddItem(new GUIContent("Move Resources to Group..."), false, MoveResourcesToGroup, new Tuple>(Event.current, selectedNodes)); } } if (selectedNodes.Count == 1) menu.AddItem(new GUIContent("Copy Address to Clipboard"), false, CopyAddressesToClipboard, selectedNodes); else if (selectedNodes.Count > 1) menu.AddItem(new GUIContent("Copy " + selectedNodes.Count + " Addresses to Clipboard"), false, CopyAddressesToClipboard, selectedNodes); } } if (selectedNodes.Count == 1) { var label = CheckForRename(selectedNodes.First(), false); if (!string.IsNullOrEmpty(label)) menu.AddItem(new GUIContent(label), false, RenameItem, selectedNodes); } PopulateGeneralContextMenu(ref menu); menu.ShowAsContext(); } void GoToGroupAsset(object context) { List selectedNodes = context as List; if (selectedNodes == null || selectedNodes.Count == 0) return; var group = selectedNodes.First().group; if (group == null) return; EditorGUIUtility.PingObject(group); Selection.activeObject = group; } internal static void CopyAddressesToClipboard(object context) { List selectedNodes = context as List; string buffer = ""; foreach (AssetEntryTreeViewItem item in selectedNodes) buffer += item.entry.address + ","; buffer = buffer.TrimEnd(','); GUIUtility.systemCopyBuffer = buffer; } void MoveAllResourcesToGroup(object context) { var mouseEvent = context as Event; var entries = new List(); var targetGroup = context as AddressableAssetGroup; var firstId = GetSelection().First(); var item = FindItemInVisibleRows(firstId); if (item != null && item.children != null) { foreach(AssetEntryTreeViewItem child in item.children) { entries.Add(child.entry); } } else Debug.LogWarning("No Resources found to move"); if (entries.Count > 0) { var window = EditorWindow.GetWindow(true, "Select Addressable Group"); if (mouseEvent == null) window.Initialize(m_Editor.settings, entries, false, false, Vector2.zero, SafeMoveResourcesToGroup); else window.Initialize(m_Editor.settings, entries, false, false, mouseEvent.mousePosition, SafeMoveResourcesToGroup); } else Debug.LogWarning("No Resources found to move"); } void MoveResourcesToGroup(object context) { var pair = context as Tuple>; var entries = new List(); foreach (AssetEntryTreeViewItem item in pair.Item2) { if (item.entry != null) entries.Add(item.entry); } var window = EditorWindow.GetWindow(true, "Select Addressable Group"); if (pair.Item1 == null) window.Initialize(m_Editor.settings, entries, false, false, Vector2.zero, SafeMoveResourcesToGroup); else window.Initialize(m_Editor.settings, entries, false, false, pair.Item1.mousePosition, SafeMoveResourcesToGroup); } void SafeMoveResourcesToGroup(AddressableAssetSettings settings, List entries, AddressableAssetGroup group) { var guids = new List(); var paths = new List(); foreach (AddressableAssetEntry entry in entries) { if (entry != null) { guids.Add(entry.guid); paths.Add(entry.AssetPath); } } AddressableAssetUtility.SafeMoveResourcesToGroup(settings, group, paths, guids); } void MoveEntriesToNewGroupWithSettings(object context) { var pair = context as Tuple>; var entries = new List(); foreach(AssetEntryTreeViewItem item in pair.Item2) { if (item.entry != null) entries.Add(item.entry); } var window = EditorWindow.GetWindow(true, "Select Addressable Group"); if (pair.Item1 == null) window.Initialize(m_Editor.settings, entries, false, false, Vector2.zero, MoveEntriesToNewGroupWithSettings); else window.Initialize(m_Editor.settings, entries, false, false, pair.Item1.mousePosition, MoveEntriesToNewGroupWithSettings); } void MoveEntriesToNewGroupWithSettings(AddressableAssetSettings settings, List entries, AddressableAssetGroup group) { var newGroup = settings.CreateGroup(AddressableAssetSettings.kNewGroupName, false, false, true, group.Schemas); foreach (AddressableAssetEntry entry in entries) { settings.MoveEntry(entry, newGroup, entry.ReadOnly, true); } } void MoveEntriesToGroup(object context) { var pair = context as Tuple>; var entries = new List(); bool mixedGroups = false; AddressableAssetGroup displayGroup = null; foreach (AssetEntryTreeViewItem item in pair.Item2) { if (item.entry != null) { entries.Add(item.entry); if (displayGroup == null) displayGroup = item.entry.parentGroup; else if (item.entry.parentGroup != displayGroup) { mixedGroups = true; } } } var window = EditorWindow.GetWindow(true, "Select Addressable Group"); if (pair.Item1 == null) window.Initialize(m_Editor.settings, entries, !mixedGroups, false, Vector2.zero, AddressableAssetUtility.MoveEntriesToGroup); else window.Initialize(m_Editor.settings, entries, !mixedGroups, false, pair.Item1.mousePosition, AddressableAssetUtility.MoveEntriesToGroup); } internal void CreateNewGroup(object context) { var groupTemplate = context as AddressableAssetGroupTemplate; if (groupTemplate != null) { AddressableAssetGroup newGroup = m_Editor.settings.CreateGroup(groupTemplate.Name, false, false, true, null, groupTemplate.GetTypes()); groupTemplate.ApplyToAddressableAssetGroup(newGroup); } else { m_Editor.settings.CreateGroup("", false, false, false, null); Reload(); } } internal void SetGroupAsDefault(object context) { List selectedNodes = context as List; if (selectedNodes == null || selectedNodes.Count == 0) return; var group = selectedNodes.First().group; if (group == null) return; m_Editor.settings.DefaultGroup = group; Reload(); } protected void RemoveMissingReferences() { RemoveMissingReferencesImpl(); } internal void RemoveMissingReferencesImpl() { if (m_Editor.settings.RemoveMissingGroupReferences()) m_Editor.settings.SetDirty(AddressableAssetSettings.ModificationEvent.GroupRemoved, null, true, true); } protected void RemoveGroup(object context) { RemoveGroupImpl(context); } internal void RemoveGroupImpl(object context, bool forceRemoval = false) { if (forceRemoval || EditorUtility.DisplayDialog("Delete selected groups?", "Are you sure you want to delete the selected groups?\n\nYou cannot undo this action.", "Yes", "No")) { List selectedNodes = context as List; if (selectedNodes == null || selectedNodes.Count < 1) return; var groups = new List(); AssetDatabase.StartAssetEditing(); try { foreach (var item in selectedNodes) { m_Editor.settings.RemoveGroupInternal(item == null ? null : item.group, true, false); groups.Add(item.group); } } finally { AssetDatabase.StopAssetEditing(); } m_Editor.settings.SetDirty(AddressableAssetSettings.ModificationEvent.GroupRemoved, groups, true, true); AddressableAssetUtility.OpenAssetIfUsingVCIntegration(m_Editor.settings); } } protected void SimplifyAddresses(object context) { SimplifyAddressesImpl(context); } internal void SimplifyAddressesImpl(object context) { List selectedNodes = context as List; if (selectedNodes == null || selectedNodes.Count < 1) return; var entries = new List(); HashSet modifiedGroups = new HashSet(); foreach (var item in selectedNodes) { if (item.IsGroup) { foreach (var e in item.group.entries) { e.SetAddress(Path.GetFileNameWithoutExtension(e.address), false); entries.Add(e); } modifiedGroups.Add(item.group); } else { item.entry.SetAddress(Path.GetFileNameWithoutExtension(item.entry.address), false); entries.Add(item.entry); modifiedGroups.Add(item.entry.parentGroup); } } foreach (var g in modifiedGroups) { g.SetDirty(AddressableAssetSettings.ModificationEvent.EntryModified, entries, false, true); AddressableAssetUtility.OpenAssetIfUsingVCIntegration(g); } m_Editor.settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryModified, entries, true, false); } protected void RemoveEntry(object context) { RemoveEntryImpl(context); } internal void RemoveEntryImpl(object context, bool forceRemoval = false) { if (forceRemoval || EditorUtility.DisplayDialog("Delete selected entries?", "Are you sure you want to delete the selected entries?\n\nYou cannot undo this action.", "Yes", "No")) { List selectedNodes = context as List; if (selectedNodes == null || selectedNodes.Count < 1) return; var entries = new List(); HashSet modifiedGroups = new HashSet(); foreach (var item in selectedNodes) { if (item.entry != null) { entries.Add(item.entry); modifiedGroups.Add(item.entry.parentGroup); m_Editor.settings.RemoveAssetEntry(item.entry.guid, false); } } foreach (var g in modifiedGroups) { g.SetDirty(AddressableAssetSettings.ModificationEvent.EntryModified, entries, false, true); AddressableAssetUtility.OpenAssetIfUsingVCIntegration(g); } m_Editor.settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryRemoved, entries, true, false); } } protected void RenameItem(object context) { RenameItemImpl(context); } internal void RenameItemImpl(object context) { List selectedNodes = context as List; if (selectedNodes != null && selectedNodes.Count >= 1) { var item = selectedNodes.First(); if (CanRename(item)) BeginRename(item); } } protected override bool CanBeParent(TreeViewItem item) { var aeItem = item as AssetEntryTreeViewItem; if (aeItem != null && aeItem.group != null) return true; return false; } protected override void KeyEvent() { if (Event.current.type == EventType.KeyUp && Event.current.keyCode == KeyCode.Delete && GetSelection().Count > 0) { List selectedNodes = new List(); bool allGroups = true; bool allEntries = true; foreach (var nodeId in GetSelection()) { var item = FindItemInVisibleRows(nodeId); if (item != null) { selectedNodes.Add(item); if (item.entry == null) allEntries = false; else allGroups = false; } } if (allEntries) RemoveEntry(selectedNodes); if (allGroups) RemoveGroup(selectedNodes); } } protected override bool CanStartDrag(CanStartDragArgs args) { int resourcesCount = 0; foreach (var id in args.draggedItemIDs) { var item = FindItemInVisibleRows(id); if (item != null) { if (item.entry != null) { //can't drag the root "EditorSceneList" entry if (item.entry.guid == AddressableAssetEntry.EditorSceneListName) return false; //can't drag the root "Resources" entry if (item.entry.guid == AddressableAssetEntry.ResourcesName) return false; //if we're dragging resources, we should _only_ drag resources. if (item.entry.IsInResources) resourcesCount++; //if it's missing a path, it can't be moved. most likely this is a sub-asset. if (string.IsNullOrEmpty(item.entry.AssetPath)) return false; } } } if ((resourcesCount > 0) && (resourcesCount < args.draggedItemIDs.Count)) return false; return true; } protected override void SetupDragAndDrop(SetupDragAndDropArgs args) { DragAndDrop.PrepareStartDrag(); var selectedNodes = new List(); foreach (var id in args.draggedItemIDs) { var item = FindItemInVisibleRows(id); if (item.entry != null || item.@group != null) selectedNodes.Add(item); } DragAndDrop.paths = null; DragAndDrop.objectReferences = new Object[] { }; DragAndDrop.SetGenericData("AssetEntryTreeViewItem", selectedNodes); DragAndDrop.visualMode = selectedNodes.Count > 0 ? DragAndDropVisualMode.Copy : DragAndDropVisualMode.Rejected; DragAndDrop.StartDrag("AssetBundleTree"); } protected override DragAndDropVisualMode HandleDragAndDrop(DragAndDropArgs args) { DragAndDropVisualMode visualMode = DragAndDropVisualMode.None; var target = args.parentItem as AssetEntryTreeViewItem; if (target != null && target.entry != null && target.entry.ReadOnly) return DragAndDropVisualMode.Rejected; if (DragAndDrop.paths != null && DragAndDrop.paths.Length > 0) { visualMode = HandleDragAndDropPaths(target, args); } else { visualMode = HandleDragAndDropItems(target, args); } return visualMode; } DragAndDropVisualMode HandleDragAndDropItems(AssetEntryTreeViewItem target, DragAndDropArgs args) { DragAndDropVisualMode visualMode = DragAndDropVisualMode.None; var draggedNodes = DragAndDrop.GetGenericData("AssetEntryTreeViewItem") as List; if (draggedNodes != null && draggedNodes.Count > 0) { visualMode = DragAndDropVisualMode.Copy; AssetEntryTreeViewItem firstItem = draggedNodes.First(); bool isDraggingGroup = firstItem.IsGroup; bool isDraggingNestedGroup = isDraggingGroup && firstItem.parent != rootItem; bool dropParentIsRoot = args.parentItem == rootItem || args.parentItem == null; bool parentGroupIsReadOnly = target?.@group != null && target.@group.ReadOnly; if (isDraggingNestedGroup || isDraggingGroup && !dropParentIsRoot || !isDraggingGroup && dropParentIsRoot || parentGroupIsReadOnly) visualMode = DragAndDropVisualMode.Rejected; if (args.performDrop) { if (args.parentItem == null || args.parentItem == rootItem && visualMode != DragAndDropVisualMode.Rejected) { // Need to insert groups in reverse order because all groups will be inserted at the same index for (int i = draggedNodes.Count - 1; i >= 0; i--) { AssetEntryTreeViewItem node = draggedNodes[i]; AddressableAssetGroup group = node.@group; int index = m_Editor.settings.groups.FindIndex(g => g == group); if (index < args.insertAtIndex) args.insertAtIndex--; m_Editor.settings.groups.RemoveAt(index); if (args.insertAtIndex < 0 || args.insertAtIndex > m_Editor.settings.groups.Count) m_Editor.settings.groups.Insert(m_Editor.settings.groups.Count, group); else m_Editor.settings.groups.Insert(args.insertAtIndex, group); } m_Editor.settings.SetDirty(AddressableAssetSettings.ModificationEvent.GroupMoved, m_Editor.settings.groups, true, true); Reload(); } else { AddressableAssetGroup parent = null; if (target.group != null) parent = target.group; else if (target.entry != null) parent = target.entry.parentGroup; if (parent != null) { var entries = new List(); foreach (AssetEntryTreeViewItem node in draggedNodes) { entries.Add(node.entry); } if (entries.First().IsInResources) { SafeMoveResourcesToGroup(m_Editor.settings, entries, parent); } else { var modifiedGroups = new HashSet(); modifiedGroups.Add(parent); foreach (AddressableAssetEntry entry in entries) { modifiedGroups.Add(entry.parentGroup); m_Editor.settings.MoveEntry(entry, parent, false, false); } foreach (AddressableAssetGroup modifiedGroup in modifiedGroups) AddressableAssetUtility.OpenAssetIfUsingVCIntegration(modifiedGroup); m_Editor.settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entries, true, false); } } } } } return visualMode; } DragAndDropVisualMode HandleDragAndDropPaths(AssetEntryTreeViewItem target, DragAndDropArgs args) { DragAndDropVisualMode visualMode = DragAndDropVisualMode.None; bool containsGroup = false; foreach (var path in DragAndDrop.paths) { if (PathPointsToAssetGroup(path)) { containsGroup = true; break; } } bool parentGroupIsReadOnly = target?.@group != null && target.@group.ReadOnly; if (target == null && !containsGroup || parentGroupIsReadOnly) return DragAndDropVisualMode.Rejected; foreach (String path in DragAndDrop.paths) { if (!AddressableAssetUtility.IsPathValidForEntry(path) && (!PathPointsToAssetGroup(path) && target != rootItem)) return DragAndDropVisualMode.Rejected; } visualMode = DragAndDropVisualMode.Copy; if (args.performDrop && visualMode != DragAndDropVisualMode.Rejected) { if (!containsGroup) { AddressableAssetGroup parent = null; bool targetIsGroup = false; if (target.group != null) { parent = target.group; targetIsGroup = true; } else if (target.entry != null) parent = target.entry.parentGroup; if (parent != null) { var resourcePaths = new List(); var nonResourceGuids = new List(); foreach (var p in DragAndDrop.paths) { if (AddressableAssetUtility.IsInResources(p)) resourcePaths.Add(p); else nonResourceGuids.Add(AssetDatabase.AssetPathToGUID(p)); } bool canMarkNonResources = true; if (resourcePaths.Count > 0) canMarkNonResources = AddressableAssetUtility.SafeMoveResourcesToGroup(m_Editor.settings, parent, resourcePaths, null); if (canMarkNonResources) { if (nonResourceGuids.Count > 0) { var entriesMoved = new List(); var entriesCreated = new List(); m_Editor.settings.CreateOrMoveEntries(nonResourceGuids, parent, entriesCreated, entriesMoved, false, false); if (entriesMoved.Count > 0) m_Editor.settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryMoved, entriesMoved, true); if (entriesCreated.Count > 0) m_Editor.settings.SetDirty(AddressableAssetSettings.ModificationEvent.EntryAdded, entriesCreated, true); AddressableAssetUtility.OpenAssetIfUsingVCIntegration(parent); } if (targetIsGroup) { SetExpanded(target.id, true); } } } } else { bool modified = false; foreach (var p in DragAndDrop.paths) { if (PathPointsToAssetGroup(p)) { AddressableAssetGroup loadedGroup = AssetDatabase.LoadAssetAtPath(p); if (loadedGroup != null) { if (m_Editor.settings.FindGroup(g => g.Guid == loadedGroup.Guid) == null) { m_Editor.settings.groups.Add(loadedGroup); modified = true; } } } } if (modified) m_Editor.settings.SetDirty(AddressableAssetSettings.ModificationEvent.GroupAdded, m_Editor.settings, true, true); } } return visualMode; } private bool PathPointsToAssetGroup(string path) { return AssetDatabase.GetMainAssetTypeAtPath(path) == typeof(AddressableAssetGroup); } } class AssetEntryTreeViewItem : TreeViewItem { public AddressableAssetEntry entry; public AddressableAssetGroup group; public string folderPath; public Texture2D assetIcon; public bool isRenaming; public bool checkedForChildren = true; public AssetEntryTreeViewItem(AddressableAssetEntry e, int d) : base(e == null ? 0 : (e.address + e.guid).GetHashCode(), d, e == null ? "[Missing Reference]" : e.address) { entry = e; group = null; folderPath = string.Empty; assetIcon = entry == null ? null : AssetDatabase.GetCachedIcon(e.AssetPath) as Texture2D; isRenaming = false; } public AssetEntryTreeViewItem(AddressableAssetGroup g, int d) : base(g == null ? 0 : g.Guid.GetHashCode(), d, g == null ? "[Missing Reference]" : g.Name) { entry = null; group = g; folderPath = string.Empty; assetIcon = null; isRenaming = false; } public AssetEntryTreeViewItem(string folder, int d, int id) : base(id, d, string.IsNullOrEmpty(folder) ? "missing" : folder) { entry = null; group = null; folderPath = folder; assetIcon = null; isRenaming = false; } public bool IsGroup => group != null && entry == null; public override string displayName { get { if (!isRenaming && group != null && group.Default) return base.displayName + " (Default)"; return base.displayName; } set { base.displayName = value; } } } static class MyExtensionMethods { // Find digits in a string static Regex s_Regex = new Regex(@"\d+", RegexOptions.Compiled); public static IEnumerable Order(this IEnumerable items, Func selector, bool ascending) { if (EditorPrefs.HasKey("AllowAlphaNumericHierarchy") && EditorPrefs.GetBool("AllowAlphaNumericHierarchy")) { // Find the length of the longest number in the string int maxDigits = items .SelectMany(i => s_Regex.Matches(selector(i)).Cast().Select(digitChunk => (int?)digitChunk.Value.Length)) .Max() ?? 0; // in the evaluator, pad numbers with zeros so they all have the same length var tempSelector = selector; selector = i => s_Regex.Replace(tempSelector(i), match => match.Value.PadLeft(maxDigits, '0')); } return ascending ? items.OrderBy(selector) : items.OrderByDescending(selector); } } }