static bool IsTargetOrNested(SearchArg target, Object suspect) { if (!suspect) { return(false); } if (target.Target.GetInstanceID() == suspect.GetInstanceID() || target.Main.GetInstanceID() == (suspect).GetInstanceID()) { return(true); } if (target.SubAssets.TryGet(out var subassets)) { foreach (var asset in subassets) { if (asset.GetInstanceID() == (suspect).GetInstanceID()) { return(true); } } } return(false); }
public static void Upd(SearchArg arg) { if (arg.Target is DefaultAsset folder) { var path = AssetDatabase.GetAssetPath(folder); var store = Globals <BacklinkStore> .Value; arg.UnusedAssetsFiltered = store.UnusedFiles.Where(p => p.Key.StartsWith(path)).Select(p => p.Key).ToList(); arg.UnusedScenesFiltered = store.UnusedScenes.Where(p => p.Key.StartsWith(path)).Select(p => p.Key).ToList(); } }
// todo provide explicit scene arg public static void InScene(SearchArg arg, Scene currentScene) { var rootGameObjects = currentScene.GetRootGameObjects(); foreach (var suspect in Traverse(rootGameObjects)) { if (!SearchInChildProperties(arg, suspect, scene: true, out var entity)) { continue; } var s = entity.Set <InSceneResult>(); s.ScenePath = arg.Scene.path; } IEnumerable <Object> Traverse(GameObject[] roots) { foreach (var rootGo in roots) { foreach (var comp in GoAndChildComps(rootGo)) { yield return(comp); } } } IEnumerable <Object> GoAndChildComps(GameObject rr) { using (ComponentListPool.GetScoped(out var pooled)) { rr.GetComponents(pooled); foreach (var component in pooled) { if (component is Transform) { continue; } yield return(component); } } var transform = rr.transform; var childCount = transform.childCount; for (int i = 0; i < childCount; i++) { var g = transform.GetChild(i).gameObject; foreach (var res in GoAndChildComps(g)) { yield return(res); } } } }
static void InScene(SearchArg arg, Scene currentScene) { var allObjects = currentScene .GetRootGameObjects() .SelectMany(g => g.GetComponentsInChildren <Component>(true) .Where(c => c && !(c is Transform)) .Union(Enumerable.Repeat(g as Object, 1)) ) .ToArray(); var total = allObjects.Length; for (var i = 0; i < total; i++) { var suspect = allObjects[i]; if (SearchInChildProperties(arg, suspect, true, out var entity)) { var s = entity.Set <InSceneResult>(); s.ScenePath = arg.Scene.path; } } }
public static void Init(SearchArg arg, Object target, Scene scene = default) { Asr.IsNotNull(target, "Asset you're trying to search is corrupted"); arg.Target = target; arg.FilePath = AssetDatabase.GetAssetPath(arg.Target); if (!scene.IsValid()) { Upd(arg); arg.Main = AssetDatabase.LoadMainAssetAtPath(arg.FilePath); if (AssetDatabase.IsSubAsset(arg.Target)) { } else { switch (target) { case SceneAsset _: // todo support cross-scene references? // nested = all assets break; default: // AssetDatabase.IsMainAssetAtPathLoaded() var subAssets = AssetDatabase.LoadAllAssetsAtPath(arg.FilePath).Where(Predicate).ToArray(); arg.SubAssets = subAssets.Length == 0 ? default(Option <Object[]>) : subAssets; bool Predicate(Object s) { if (!s) { return(false); } return(s.GetInstanceID() != arg.Target.GetInstanceID()); } break; } } } else { switch (arg.Target) { case GameObject gg: arg.Main = gg; arg.Scene = scene; arg.SubAssets = gg.GetComponents <Component>().OfType <Object>().ToArray(); break; case Component component: { // treat like subAsset arg.Main = component.gameObject; arg.Scene = scene; break; } default: // project asset such as Material arg.Main = arg.Target; arg.Scene = scene; break; } } }
public static void FilesThatReference(SearchArg arg) { var store = Globals <BacklinkStore> .Value; var path1 = AssetDatabase.GetAssetPath(arg.Target); if (!store.Backward.TryGetValue(path1, out var dep)) { return; } foreach (var path in dep.Lookup) { var mainAsset = AssetDatabase.GetMainAssetTypeAtPath(path); if (mainAsset.IsAssignableFromInverse(typeof(SceneAsset))) { continue; } var any = false; if (mainAsset.IsAssignableFromInverse(typeof(GameObject))) { } else { var allAssetsAtPath = AssetDatabase.LoadAllAssetsAtPath(path); foreach (var suspect in allAssetsAtPath) { if (suspect is DefaultAsset || suspect is Transform || !suspect) { continue; } if (!SearchInChildProperties(arg, suspect, false, out var entity)) { continue; } entity.Set <FileResultTag>(); any = true; } } if (any) { continue; } // failed to find any property - just show main asset var e = World.NewEntity(); var gui = e.Set <SearchResultGui>(); gui.Properties = new List <SearchResultGui.PropertyData>(); var main = AssetDatabase.LoadMainAssetAtPath(path); gui.Label = new GUIContent() { image = AssetPreview.GetMiniThumbnail(main), text = path.Replace(AssetsRootPath, string.Empty) }; var res = e.Set <Result>(); res.MainFile = main; e.Set <FileResultTag>(); } }
static bool SearchInChildProperties(SearchArg arg, Object suspect, bool scene, out EcsEntity entity) { if (IsTargetOrNested(arg, suspect)) { entity = default; return(false); } if (!suspect) { entity = default; return(false); } var so = new SerializedObject(suspect); _tmp.Clear(); var queue = _tmp; var propIterator = so.GetIterator(); var prefabInstance = false; if (scene && !string.IsNullOrEmpty(arg.FilePath) && PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(suspect) == arg.FilePath) { prefabInstance = true; while (propIterator.NextVisible(true)) { if (propIterator.propertyType != SerializedPropertyType.ObjectReference) { continue; } if (!IsTargetOrNested(arg, propIterator.objectReferenceValue)) { continue; } queue.Enqueue(propIterator.Copy()); } } else { while (propIterator.Next(true)) { if (propIterator.propertyType != SerializedPropertyType.ObjectReference) { continue; } if (!IsTargetOrNested(arg, propIterator.objectReferenceValue)) { continue; } queue.Enqueue(propIterator.Copy()); } } if (queue.Count == 0 && !prefabInstance) { entity = default; return(false); } entity = World.NewEntityWith(out Result data); var gui = entity.Set <SearchResultGui>(); gui.Properties = new List <SearchResultGui.PropertyData>(); gui.SerializedObject = so; gui.Label = new GUIContent(); // init header Texture2D miniTypeThumbnail = null; if (scene) { switch (suspect) { case Component component: data.RootGo = component.gameObject; gui.TransformPath = AnimationUtility.CalculateTransformPath(component.transform, null); gui.Label.image = AssetPreview.GetMiniThumbnail(data.RootGo); gui.Label.text = gui.TransformPath; break; case GameObject go: data.RootGo = go; gui.Label.image = AssetPreview.GetMiniThumbnail(data.RootGo); gui.TransformPath = AnimationUtility.CalculateTransformPath(go.transform, null); gui.Label.text = gui.TransformPath; break; default: throw new NotImplementedException(); } miniTypeThumbnail = data.RootGo.GetInstanceID() == suspect.GetInstanceID() ? null : AssetPreview.GetMiniThumbnail(suspect); } else { data.File = suspect; data.FilePath = AssetDatabase.GetAssetPath(data.File); data.MainFile = AssetDatabase.LoadMainAssetAtPath(data.FilePath); // todo var prefabInstanceStatus = PrefabUtility.GetPrefabInstanceStatus(data.MainFile); switch (prefabInstanceStatus) { case PrefabInstanceStatus.Connected: case PrefabInstanceStatus.Disconnected: switch (data.File) { case Component comp: // transformPath = $"{AnimationUtility.CalculateTransformPath(comp.transform, null)}/".Replace("/", "/\n"); gui.TransformPath = $"{AnimationUtility.CalculateTransformPath(comp.transform, null)}"; break; case GameObject go: // transformPath = $"{AnimationUtility.CalculateTransformPath(go.transform, null)}/".Replace("/", "/\n"); gui.TransformPath = $"{AnimationUtility.CalculateTransformPath(go.transform, null)}"; break; default: // Assert.Fail("Not a component"); //todo break; } break; case PrefabInstanceStatus.NotAPrefab: case PrefabInstanceStatus.MissingAsset: if (!AssetDatabase.IsMainAsset(data.File)) { // {row.Main.name} gui.TransformPath = $"/{data.File.name}"; } break; } gui.Label.text = data.FilePath.Replace(AssetsRootPath, string.Empty); gui.Label.image = AssetDatabase.GetCachedIcon(data.FilePath); } gui.Label.tooltip = gui.TransformPath; // init properties (footer) while (queue.Count > 0) { var prop = queue.Dequeue(); var targetObject = prop.serializedObject.targetObject; var item = new SearchResultGui.PropertyData { Property = prop, Content = new GUIContent() }; item.Content.image = miniTypeThumbnail; item.Content.text = Nicify(prop, targetObject, gui.TransformPath); item.Content.tooltip = gui.TransformPath; var typeName = targetObject.GetType().Name; if (StringComparer.Ordinal.Equals(typeName, targetObject.name)) { item.Content.tooltip = $"{gui.TransformPath}.{prop.propertyPath}"; } else { item.Content.tooltip = $"{gui.TransformPath}({typeName}).{prop.propertyPath}"; } gui.Properties.Add(item: item); } return(true); }
static bool AskDeleteUnusedScenes(SearchArg arg, List <string> unusedScenes, WindowData windowData) { if (arg == null || unusedScenes == null) { return(false); } var hasUnusedScenes = unusedScenes.Count > 0; var previous = GUI.enabled; GUI.enabled = hasUnusedScenes; var guiContentRemoveScenes = windowData.Style.RemoveScene; if (GUILayout.Button(guiContentRemoveScenes, windowData.Style.RemoveUnusedBtn)) { var choice = EditorUtility.DisplayDialog( title: "Asset Cleaner", message: $"Do you really want to remove {unusedScenes.Count} scene(s)?", ok: "Remove", cancel: "Cancel"); if (choice) { EditorApplication.ExecuteMenuItem("File/Save Project"); var i = 0f; var total = (float)unusedScenes.Count; foreach (var scene in unusedScenes) { var path = Application.dataPath.Replace("Assets", scene); DeleteWithMeta(path); if (total >= _progressBarShowFromLevel) { var percent = i * 100 / total; if (Math.Abs(percent % 5f) < 0.01f) { if (EditorUtility.DisplayCancelableProgressBar( "Please wait...", "Deleting scenes...", percent)) { throw new Exception("Deleting aborted"); } } i++; } } if (total >= _progressBarShowFromLevel) { EditorUtility.ClearProgressBar(); } AssetDatabase.Refresh(); SearchUtils.Upd(arg); } GUI.enabled = previous; return(true); } GUI.enabled = previous; return(false); }
BacklinkStore.UnusedQty ShowObjectName(BacklinkStore store, WindowData windowData, TargetTypeEnum targetTypeEnum, SearchArg arg, string[] selectedGuids) { float TextWidth() { _buf2.text = _contentBuf.text; return(20f + GUI.skin.button.CalcSize(_buf2).x); } if (arg == null || arg.Main == null) { return(new BacklinkStore.UnusedQty()); } bool isMultiSelect = selectedGuids != null && selectedGuids.Length > 1; if (_contentBuf == null) { _contentBuf = new GUIContent { tooltip = $"Click to ping" }; } _contentBuf.image = isMultiSelect ? windowData.Style.MultiSelect.image : AssetPreview.GetMiniThumbnail(arg.Target); _contentBuf.text = string.Empty; _contentBuf.tooltip = string.Empty; if (!isMultiSelect) { switch (targetTypeEnum) { case TargetTypeEnum.Directory: if (!SearchArgMain.IsEmpty()) { _contentBuf.text = $"{arg.Main.name} (Folder)"; if (GUILayout.Button(_contentBuf, windowData.Style.CurrentBtn, GUILayout.MinWidth(TextWidth()))) { EditorGUIUtility.PingObject(arg.Main); } if (AskDeleteUnusedFiles(arg, arg.UnusedAssetsFiltered, windowData)) { return(new BacklinkStore.UnusedQty()); } if (AskDeleteUnusedScenes(arg, arg.UnusedScenesFiltered, windowData)) { return(new BacklinkStore.UnusedQty()); } } break; case TargetTypeEnum.File: _contentBuf.text = $"{arg.Main.name} (File Asset)"; if (GUILayout.Button(_contentBuf, windowData.Style.CurrentBtn, GUILayout.MinWidth(TextWidth()))) { EditorGUIUtility.PingObject(arg.Main); } bool hasUnusedFile = SearchUtils.IsUnused(arg.FilePath); var previous = GUI.enabled; GUI.enabled = hasUnusedFile; if (GUILayout.Button(windowData.Style.RemoveFile, windowData.Style.RemoveUnusedBtn)) { var choice = EditorUtility.DisplayDialog( title: "Asset Cleaner", message: $"Do you really want to remove file: \"{arg.Main.name}\"?", ok: "Remove", cancel: "Cancel"); if (choice) { EditorApplication.ExecuteMenuItem("File/Save Project"); DeleteWithMeta(arg.FilePath); AssetDatabase.Refresh(); SearchUtils.Upd(arg); } } GUI.enabled = previous; break; case TargetTypeEnum.Scene: _contentBuf.text = $"{arg.Main.name} (Scene)"; if (GUILayout.Button(_contentBuf, windowData.Style.CurrentBtn, GUILayout.MinWidth(TextWidth()))) { EditorGUIUtility.PingObject(arg.Main); } bool hasUnusedScene = SearchUtils.IsUnused(arg.FilePath); previous = GUI.enabled; GUI.enabled = hasUnusedScene; if (GUILayout.Button(windowData.Style.RemoveScene, windowData.Style.RemoveUnusedBtn)) { var choice = EditorUtility.DisplayDialog( title: "Asset Cleaner", message: $"Do you really want to remove scene: {arg.Main.name}?", ok: "Remove", cancel: "Cancel"); if (choice) { EditorApplication.ExecuteMenuItem("File/Save Project"); DeleteWithMeta(arg.FilePath); AssetDatabase.Refresh(); SearchUtils.Upd(arg); } } GUI.enabled = previous; break; case TargetTypeEnum.ObjectInScene: _contentBuf.text = $"{arg.Main.name} (Object in Scene)"; if (GUILayout.Button(_contentBuf, windowData.Style.CurrentBtn, GUILayout.MinWidth(TextWidth()))) { EditorGUIUtility.PingObject(arg.Main); } break; case TargetTypeEnum.ObjectInStage: _contentBuf.image = AssetPreview.GetMiniThumbnail(arg.Target); _contentBuf.text = $"{arg.Main.name} (Object in Staging)"; if (GUILayout.Button(_contentBuf, windowData.Style.RemoveUnusedBtn)) { EditorGUIUtility.PingObject(arg.Main); } break; default: if (GUILayout.Button($"{arg.Main.name} (Unknown Object Type)", windowData.Style.RemoveUnusedBtn)) { EditorGUIUtility.PingObject(arg.Main); } break; } } else { var unusedAssets = new List <string>(); var unusedScenes = new List <string>(); foreach (var guid in selectedGuids) { var path = AssetDatabase.GUIDToAssetPath(guid); if (store.UnusedFiles.TryGetValue(path, out _)) { unusedAssets.Add(path); } else if (store.UnusedScenes.TryGetValue(path, out _)) { unusedScenes.Add(path); } if (store.FoldersWithQty.TryGetValue(path, out _)) { SearchArg searchArg = new SearchArg() { FilePath = path, Target = AssetDatabase.LoadAssetAtPath <DefaultAsset>(path), Main = AssetDatabase.LoadAssetAtPath <DefaultAsset>(path) }; SearchUtils.Upd(searchArg); foreach (var unusedAssetPath in searchArg.UnusedAssetsFiltered) { if (store.UnusedFiles.TryGetValue(unusedAssetPath, out _)) { unusedAssets.Add(unusedAssetPath); } } foreach (var unusedScenePath in searchArg.UnusedScenesFiltered) { if (store.UnusedScenes.TryGetValue(unusedScenePath, out _)) { unusedScenes.Add(unusedScenePath); } } } } unusedAssets = unusedAssets.Distinct().ToList(); unusedScenes = unusedScenes.Distinct().ToList(); var assetSize = unusedAssets.Sum(CommonUtils.Size); var sceneSize = unusedScenes.Sum(CommonUtils.Size); _contentBuf.text = $"Assets: {unusedAssets.Count} ({CommonUtils.BytesToString(assetSize)}), Scenes: {unusedScenes.Count} ({CommonUtils.BytesToString(sceneSize)})"; ; GUILayout.Button(_contentBuf, windowData.Style.CurrentBtn, GUILayout.MinWidth(TextWidth())); if (AskDeleteUnusedFiles(arg, unusedAssets, windowData)) { return(new BacklinkStore.UnusedQty()); } if (AskDeleteUnusedScenes(arg, unusedScenes, windowData)) { return(new BacklinkStore.UnusedQty()); } return(new BacklinkStore.UnusedQty(unusedAssets.Count, unusedScenes.Count, assetSize + sceneSize)); } return(new BacklinkStore.UnusedQty()); }
static bool TryGetHelpInfo(SearchArg arg, out string msg, out MessageType msgType) { msgType = MessageType.Info; if (arg == null) { msg = default; return(false); } var path = arg.FilePath; if (string.IsNullOrEmpty(path)) { msg = default; return(false); } if (SearchUtils.IgnoredPaths(path, out var subPath)) { msg = $"Paths containing '{subPath}' are ignored. You could add or remove it in Settings tab"; return(true); } if (SearchUtils.IgnoredNonAssets(path) && !path.Eq("Assets")) { msg = $"Asset is outside of Assets folder"; return(true); } if (IgnoreTypes.Check(path, out var type)) { if (AssetDatabase.IsValidFolder(path)) { var scenes = arg.UnusedScenesFiltered?.Count; var files = arg.UnusedAssetsFiltered?.Count; if (scenes == 0 && files == 0) { msg = default; return(false); } var b = new StringBuilder(); b.Append("This directory contains: "); var any = false; if (files > 0) { any = true; b.Append($"unused files ({files})"); } if (scenes > 0) { if (any) { b.Append(", "); } b.Append($"unused scenes ({scenes})"); } b.Append( ".\nYou could delete them pressing corresponding button to the right.\nIf you don't want it to be inspected, please add it to Ignore list in the Settings tab"); msg = b.ToString(); return(true); } msg = $"Assets of type '{type.Name}' are ignored. Please contact support if you need to change it"; return(true); } // if (Filters.ScenePaths.GetEntitiesCount() == 0 && Filters.FileResultRows.GetEntitiesCount() == 0) { if (SearchUtils.IsUnused(path)) { type = AssetDatabase.GetMainAssetTypeAtPath(path); var name = type.IsAssignableFromInverse(typeof(SceneAsset)) ? "scene" : "file"; msg = $"This {name} has no explicit serialized usages and potentially could be removed. If you don't want it to be inspected, please add the containing folder to ignore list"; return(true); } msgType = default; msg = default; return(false); }
void ShowTabMain(Config conf, WindowData windowData) { var store = Globals <BacklinkStore> .Value; EditorGUIUtility.labelWidth = windowData.Window.position.width * .7f; int Hash() => DirtyUtils.HashCode(conf.Locked); var active = SearchArgMain.Get1[0]; if (conf.Locked && (windowData.FindFrom == FindModeEnum.File && (active == null || active.Main == null || !AssetDatabase.Contains(active.Main)))) { conf.Locked = false; AufCtx.World.NewEntityWith(out RequestRepaintEvt _); } var style = windowData.Style; var hash = Hash(); if (hash != Hash()) { PersistenceUtils.Save(in conf); AufCtx.World.NewEntityWith(out RequestRepaintEvt _); } // if (Globals<WindowData>.Get() == null) return; EditorGUILayout.Space(); SearchArg arg = default; foreach (var i in SearchArgMain) { arg = SearchArgMain.Get1[i]; if (arg != null && arg.Main != null) { break; } } if (arg == default) { return; } var targetTypeEnum = GetTargetType(windowData, arg?.Main); BacklinkStore.UnusedQty unusedQty = new BacklinkStore.UnusedQty(0, 0, 0); using (new EditorGUILayout.HorizontalScope()) { var enabledBuf = GUI.enabled; var selectedGuids = Selection.assetGUIDs; var undoRedoState = Globals <UndoRedoState> .Value; GUI.enabled = selectedGuids != null && !conf.Locked && undoRedoState.UndoEnabled; if (GUILayout.Button(style.ArrowL, style.ArrowBtn)) { AufCtx.World.NewEntityWith(out UndoEvt _); } GUI.enabled = selectedGuids != null && !conf.Locked && undoRedoState.RedoEnabled; if (GUILayout.Button(style.ArrowR, style.ArrowBtn)) { AufCtx.World.NewEntityWith(out RedoEvt _); } GUI.enabled = enabledBuf; if (conf.Locked) { if (GUILayout.Button(style.Lock, style.LockBtn)) { AufCtx.World.NewEntityWith(out SelectionChanged selectionChanged); conf.Locked = false; if (Selection.activeObject != arg.Target) { selectionChanged.From = FindModeEnum.Scene; selectionChanged.Scene = SceneManager.GetActiveScene(); selectionChanged.Target = Selection.activeObject; } else if (Selection.assetGUIDs is string[] guids) { // todo show info box multiple selection is unsupported if (guids.Length > 0) { var path = AssetDatabase.GUIDToAssetPath(guids[0]); selectionChanged.Target = AssetDatabase.LoadAssetAtPath <Object>(path); switch (Selection.selectionChanged.Target) { case DefaultAsset _: selectionChanged.From = FindModeEnum.File; break; case GameObject go when go.scene.isLoaded: selectionChanged.From = FindModeEnum.Scene; selectionChanged.Scene = SceneManager.GetActiveScene(); break; default: selectionChanged.From = FindModeEnum.File; break; } } else if (Selection.activeObject is GameObject go && go.scene.isLoaded) { selectionChanged.From = FindModeEnum.Scene; selectionChanged.Target = Selection.activeObject; selectionChanged.Scene = SceneManager.GetActiveScene(); } } } } else { var enabled = GUI.enabled; GUI.enabled = selectedGuids != null && selectedGuids.Length == 1; if (GUILayout.Button(style.Unlock, style.UnlockBtn)) { conf.Locked = true; } GUI.enabled = enabled; } unusedQty = ShowObjectName(store, windowData, targetTypeEnum, arg, selectedGuids); } bool isMultiSelect = Selection.assetGUIDs != null && Selection.assetGUIDs.Length > 1; if (conf.ShowInfoBox) { if (isMultiSelect && (unusedQty.UnusedFilesQty + unusedQty.UnusedScenesQty > 0)) { var msgUnusedFiles = (unusedQty.UnusedFilesQty > 0) ? $"unused files ({unusedQty.UnusedFilesQty})," : ""; var msgUnusedScenes = (unusedQty.UnusedScenesQty > 0) ? $"unused scenes ({unusedQty.UnusedScenesQty})," : ""; var msgMultiSelect = $"This multi-selection contains: " + msgUnusedFiles + msgUnusedScenes + $"\nYou could delete them pressing corresponding button to the right."; EditorGUILayout.HelpBox(msgMultiSelect, MessageType.Info); } else if (TryGetHelpInfo(arg, out var msg, out var msgType)) { EditorGUILayout.HelpBox(msg, msgType); } } if (targetTypeEnum != TargetTypeEnum.Directory && !isMultiSelect) { var windowData2 = Globals <WindowData> .Value; EditorGUILayout.BeginVertical(); { windowData2.ScrollPos = EditorGUILayout.BeginScrollView(windowData2.ScrollPos); { RenderRows(windowData2); //TODO? EditorGUILayout.Space(); } EditorGUILayout.EndScrollView(); } EditorGUILayout.EndVertical(); EditorGUILayout.Space(); } }