├── _config.yml ├── .gitattributes ├── Documentation └── APKInstaller.pdf ├── Scripts └── Editor │ ├── UnityUtilities.Editor.asmdef │ ├── AssetUtilities │ ├── HierarchyHistorySimple.cs │ ├── AssetUtilities.cs │ ├── FindMissingReferences.cs │ ├── FindMissingScripts.cs │ ├── FindUnusedAssets.cs │ ├── FindAssetUsages.cs │ ├── AssetDependencies.cs │ ├── AssetsHistory.cs │ └── HierarchyHistory.cs │ ├── MyShortcuts.cs │ ├── HierarchyUtilities.cs │ ├── EditorUtilities.cs │ ├── RectToolRounding.cs │ ├── OpenScenes.cs │ ├── AppDataUtility.cs │ ├── InspectorExtensions.cs │ ├── APKInstaller.cs │ ├── ComponentUtilities.cs │ ├── Common │ ├── EditorHelper.cs │ └── MyGUI.cs │ ├── PrefabUtilities.cs │ └── FileUtilities.cs ├── README.md ├── LICENSE └── .gitignore /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-hacker -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | * eol=lf 4 | -------------------------------------------------------------------------------- /Documentation/APKInstaller.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpsilonD3lta/UnityUtilities/HEAD/Documentation/APKInstaller.pdf -------------------------------------------------------------------------------- /Scripts/Editor/UnityUtilities.Editor.asmdef: -------------------------------------------------------------------------------- 1 | { 2 | "name": "UnityUtilities.Editor", 3 | "rootNamespace": "", 4 | "references": [], 5 | "includePlatforms": [ 6 | "Editor" 7 | ], 8 | "excludePlatforms": [], 9 | "allowUnsafeCode": false, 10 | "overrideReferences": false, 11 | "precompiledReferences": [], 12 | "autoReferenced": true, 13 | "defineConstraints": [], 14 | "versionDefines": [], 15 | "noEngineReferences": false 16 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # UnityCollection 2 | Moved scripts and assets that are suited to be checked in version control to separate repository: 3 | https://github.com/EpsilonD3lta/UnityCollection 4 | 5 | # UnityUtilities 6 | Collection of useful scripts for Unity. These scripts are mostly meant to not be version controlled. 7 | 8 | Repository contains scripts created by me or, if stated at the beginning of a script, from others (some of them I might have modified). 9 | Mind the licenses stated at the beginning of each script or method (those are not mine). Scripts/methods without license at the beginning are mine 10 | and are distributed under MIT license. 11 | 12 | Note that there might be bugs, use at your own risk. 13 | 14 | # Documentation 15 | ## [APKInstaller](Documentation/APKInstaller.pdf) 16 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Martin Kovar 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /Scripts/Editor/AssetUtilities/HierarchyHistorySimple.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | public class HierarchyHistorySimple : AssetsHistory 5 | { 6 | [MenuItem("Window/Hierarchy History Simple")] 7 | private static void CreateHierarchyHistory() 8 | { 9 | var window = GetWindow(typeof(HierarchyHistorySimple), false, "Hierarchy History Simple") as HierarchyHistorySimple; 10 | window.minSize = new Vector2(100, rowHeight + 1); 11 | window.Show(); 12 | } 13 | 14 | protected override void Awake() { } 15 | 16 | protected override void OnEnable() 17 | { 18 | // This is received even if invisible 19 | Selection.selectionChanged -= SelectionChanged; 20 | Selection.selectionChanged += SelectionChanged; 21 | wantsMouseEnterLeaveWindow = true; 22 | wantsMouseMove = true; 23 | 24 | LimitAndOrderHistory(); 25 | } 26 | protected override void SelectionChanged() 27 | { 28 | foreach (var t in Selection.transforms) 29 | { 30 | AddHistory(t.gameObject); 31 | LimitAndOrderHistory(); 32 | } 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # =============== # 2 | # Unity generated # 3 | # =============== # 4 | [Tt]emp/ 5 | [Oo]bj/ 6 | [Bb]uild 7 | /[Bb]uilds/ 8 | /[Ll]ibrary/ 9 | AssetBundles/Android/ 10 | AssetBundles/iOS/ 11 | sysinfo.txt 12 | *.stackdump 13 | /Assets/AssetStoreTools* 14 | *.apk 15 | 16 | # Jetbrain Rider Cache 17 | .idea/ 18 | Assets/Plugins/Editor/JetBrains* 19 | 20 | 21 | # ===================================== # 22 | # Visual Studio / MonoDevelop generated # 23 | # ===================================== # 24 | [Ee]xported[Oo]bj/ 25 | .vs/ 26 | /*.userprefs 27 | /*.csproj 28 | /*.pidb 29 | *.pidb.meta 30 | /*.suo 31 | /*.sln* 32 | /*.user 33 | .consulo/ 34 | /*.tmp 35 | /*.svd 36 | 37 | # ============ # 38 | # OS generated # 39 | # ============ # 40 | .DS_Store* 41 | ._* 42 | .Spotlight-V100 43 | .Trashes 44 | ehthumbs.db 45 | [Tt]humbs.db 46 | [Tt]humbs.db.meta 47 | [Dd]esktop.ini 48 | 49 | # =============== # 50 | # EDM4U generated # 51 | # =============== # 52 | Assets/Plugins/Android/*.aar 53 | Assets/Plugins/Android/*.aar.meta 54 | Assets/Plugins/Android/*.jar 55 | Assets/Plugins/Android/*.jar.meta 56 | 57 | # ======= # 58 | # Managed # 59 | # ======= # 60 | IgnoreSCM/ 61 | ExternalAssets/ 62 | TextMesh Pro/ 63 | *.unity 64 | *.meta 65 | Assets/IgnoreSCM/ 66 | Assets/IgnoreSCM.meta 67 | -------------------------------------------------------------------------------- /Scripts/Editor/MyShortcuts.cs: -------------------------------------------------------------------------------- 1 | using System.Reflection; 2 | using UnityEditor; 3 | using UnityEditor.ShortcutManagement; 4 | using UnityEngine; 5 | 6 | public class MyShortcuts 7 | { 8 | [Shortcut("Delete Alternative", KeyCode.X)] 9 | public static void DeleteAlternative() 10 | { 11 | EditorApplication.ExecuteMenuItem("Edit/Delete"); 12 | } 13 | 14 | [Shortcut("Mute Game View Audio", KeyCode.M)] 15 | public static void MuteGameViewAudio() 16 | { 17 | EditorUtility.audioMasterMute = !EditorUtility.audioMasterMute; 18 | } 19 | 20 | [Shortcut("Lock Inspector", KeyCode.C, ShortcutModifiers.Alt)] 21 | public static void ToggleInspectorLock() 22 | { 23 | ActiveEditorTracker.sharedTracker.isLocked = !ActiveEditorTracker.sharedTracker.isLocked; 24 | ActiveEditorTracker.sharedTracker.ForceRebuild(); 25 | } 26 | 27 | [Shortcut("Lock Project Tab", KeyCode.V, ShortcutModifiers.Alt)] 28 | public static void ToggleProjectTabLock() 29 | { 30 | var unityEditorAssembly = Assembly.GetAssembly(typeof(Editor)); 31 | var projectBrowserType = unityEditorAssembly.GetType("UnityEditor.ProjectBrowser"); 32 | var projectBrowsers = Resources.FindObjectsOfTypeAll(projectBrowserType); 33 | var isLockedProperty = projectBrowserType.GetProperty("isLocked", BindingFlags.Instance | BindingFlags.NonPublic); 34 | 35 | foreach (var p in projectBrowsers) 36 | { 37 | var isLockedOldValue = (bool)isLockedProperty.GetValue(p); 38 | isLockedProperty.SetValue(p, !isLockedOldValue); 39 | 40 | EditorWindow pw = (EditorWindow)p; 41 | pw.Repaint(); 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /Scripts/Editor/AssetUtilities/AssetUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEditor.ShortcutManagement; 5 | using UnityEngine; 6 | using Object = UnityEngine.Object; 7 | public class AssetUtilities 8 | { 9 | [MenuItem("Assets/Mark Assets Dirty", priority = 38)] 10 | public static void MarkDirty() 11 | { 12 | foreach (var obj in Selection.objects) 13 | { 14 | EditorUtility.SetDirty(obj); 15 | } 16 | } 17 | 18 | [MenuItem("Assets/Force Reserialize", priority = 39)] 19 | public static void ForceReserialize() 20 | { 21 | var assetPaths = Selection.assetGUIDs.ToList().Select(x => AssetDatabase.GUIDToAssetPath(x)); 22 | AssetDatabase.ForceReserializeAssets(assetPaths); 23 | } 24 | 25 | [Shortcut("Save ShaderGraphs", KeyCode.S, ShortcutModifiers.Control | ShortcutModifiers.Shift)] 26 | public static void SaveShaderGraphs() 27 | { 28 | var assembly = AppDomain.CurrentDomain.GetAssemblies() 29 | .FirstOrDefault(x => x.GetName().Name == "Unity.ShaderGraph.Editor"); 30 | string windowTypeName = "UnityEditor.ShaderGraph.Drawing.MaterialGraphEditWindow"; 31 | var windowType = assembly.GetType(windowTypeName); 32 | Object[] shaderGraphWindows = Resources.FindObjectsOfTypeAll(windowType); 33 | if (shaderGraphWindows != null && shaderGraphWindows.Length != 0) 34 | { 35 | foreach (var w in shaderGraphWindows) 36 | { 37 | var window = w as EditorWindow; 38 | window.SaveChanges(); 39 | } 40 | } 41 | 42 | // Also do regular save 43 | //EditorApplication.ExecuteMenuItem("File/Save"); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Scripts/Editor/HierarchyUtilities.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEditor.Presets; 3 | using UnityEngine; 4 | using UnityEngine.UI; 5 | 6 | public class HierarchyUtilities 7 | { 8 | [InitializeOnLoadMethod] 9 | private static void ObjectChangeEventsExample() 10 | { 11 | ObjectChangeEvents.changesPublished -= ChangesPublished; 12 | ObjectChangeEvents.changesPublished += ChangesPublished; 13 | } 14 | 15 | static void ChangesPublished(ref ObjectChangeEventStream stream) 16 | { 17 | for (int i = 0; i < stream.length; ++i) 18 | { 19 | if (stream.GetEventType(i) == ObjectChangeKind.CreateGameObjectHierarchy) 20 | { 21 | stream.GetCreateGameObjectHierarchyEvent(i, out var createGameObjectHierarchyEvent); 22 | var go = EditorUtility.InstanceIDToObject(createGameObjectHierarchyEvent.instanceId) as GameObject; 23 | if (!go) return; 24 | var spriteRenderer = go.GetComponent(); 25 | if (!spriteRenderer) return; 26 | var canvas = go.GetComponentInParent(); 27 | if (canvas) 28 | { 29 | Undo.RegisterFullObjectHierarchyUndo(go, "Replace SpriteRenderer"); 30 | var sprite = spriteRenderer.sprite; 31 | Object.DestroyImmediate(spriteRenderer); 32 | var image = go.AddComponent(); 33 | image.transform.localScale = Vector3.one; 34 | var presets = Preset.GetDefaultPresetsForType(new PresetType(image)); 35 | if (presets.Length > 0) 36 | presets[0].preset.ApplyTo(image); 37 | image.sprite = sprite; 38 | image.SetNativeSize(); 39 | image.rectTransform.position = Vector3.zero; 40 | image.rectTransform.anchoredPosition = Vector2.zero; 41 | } 42 | } 43 | } 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /Scripts/Editor/EditorUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Diagnostics; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using Debug = UnityEngine.Debug; 6 | 7 | public class EditorUtilities 8 | { 9 | [MenuItem("Editor/Recompile Scripts _F5")] 10 | public static void RecompileScripts() 11 | { 12 | UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); 13 | } 14 | 15 | [MenuItem("Editor/Recompile Scripts Clean &F5")] 16 | public static void RecompileScriptsClean() 17 | { 18 | UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(UnityEditor.Compilation.RequestScriptCompilationOptions.CleanBuildCache); 19 | } 20 | 21 | [MenuItem("Editor/Open Editor Log")] 22 | public static void OpenEditorLog() 23 | { 24 | var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); 25 | string VSCodePath = localAppData + "/Programs/Microsoft VS Code/Code.exe"; 26 | Debug.Log(VSCodePath + " \"" + localAppData + "/Unity/Editor/Editor.log\""); 27 | ProcessStartInfo process = new ProcessStartInfo( 28 | VSCodePath, " \"" + localAppData + "/Unity/Editor/Editor.log\"") 29 | { 30 | RedirectStandardOutput = true, 31 | RedirectStandardError = true, 32 | CreateNoWindow = true, 33 | UseShellExecute = false 34 | }; 35 | Process.Start(process); 36 | } 37 | } 38 | 39 | public class EditorLoopUpdater 40 | { 41 | private static bool isLooping = false; 42 | private static string PrefId => PlayerSettings.companyName + "." + PlayerSettings.productName + ".EpsilonDelta.EditorLoop"; 43 | 44 | [InitializeOnLoadMethod] 45 | public static void LoadSetting() 46 | { 47 | isLooping = EditorPrefs.GetBool(PrefId, false); 48 | if (isLooping) EditorApplication.update += QueryUpdate; 49 | Application.runInBackground = isLooping; 50 | } 51 | 52 | [MenuItem("Editor/Loop _F7")] 53 | public static void Loop() 54 | { 55 | isLooping = !isLooping; 56 | if (isLooping) EditorApplication.update += QueryUpdate; 57 | else EditorApplication.update -= QueryUpdate; 58 | //Application.runInBackground = isLooping; // This is not necessary and changes ProjectSettings 59 | EditorPrefs.SetBool(PrefId, isLooping); 60 | } 61 | 62 | [MenuItem("Editor/Loop _F7", true)] 63 | private static bool LoopValidate() 64 | { 65 | Menu.SetChecked("Editor/Loop", isLooping); 66 | return true; 67 | } 68 | 69 | public static void QueryUpdate() 70 | { 71 | EditorApplication.QueuePlayerLoopUpdate(); 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /Scripts/Editor/RectToolRounding.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | 4 | public class RectToolRounding 5 | { 6 | public static bool snappingOn = true; 7 | public static Vector2 anchorMin; 8 | public static Vector2 anchorMax; 9 | 10 | [InitializeOnLoadMethod] 11 | public static void Initialize() 12 | { 13 | SceneView.duringSceneGui += OnSceneGui; 14 | } 15 | 16 | [MenuItem("Editor/UI snapping")] 17 | public static void SwitchUISnapping() 18 | { 19 | snappingOn = !snappingOn; 20 | Debug.Log($"UI snapping: {snappingOn}"); 21 | } 22 | 23 | public static void OnSceneGui(SceneView sceneView) 24 | { 25 | if (!snappingOn || Application.isPlaying) return; 26 | if (Selection.transforms.Length == 1 && Selection.transforms[0] is RectTransform r) 27 | { 28 | // MouseDrag triggers when RectTool is dragged, but not when anchors are dragged 29 | if ((anchorMin != r.anchorMin || anchorMax != r.anchorMax) && Event.current.type != EventType.MouseDrag) return; 30 | if (r.drivenByObject != null) return; // When driven by layout groups etc. 31 | if (r.TryGetComponent(out Canvas canvas) && canvas.isRootCanvas && canvas.renderMode == RenderMode.WorldSpace) 32 | return; 33 | if (!TryGetComponentInParent(r, out Canvas _)) return; 34 | 35 | //Debug.Log($"{r.sizeDelta}, {r.offsetMin}, {r.offsetMax}, {r.anchoredPosition}"); 36 | r.sizeDelta = new Vector2(Round(r.sizeDelta.x), Round(r.sizeDelta.y)); 37 | 38 | if (r.anchorMin.x != r.anchorMax.x) 39 | { 40 | r.offsetMin = new Vector2(Round(r.offsetMin.x), r.offsetMin.y); 41 | r.offsetMax = new Vector2(Round(r.offsetMax.x), r.offsetMax.y); 42 | } 43 | else r.anchoredPosition = new Vector2(Round(r.anchoredPosition.x), r.anchoredPosition.y); 44 | if (r.anchorMin.y != r.anchorMax.y) 45 | { 46 | r.offsetMin = new Vector2(r.offsetMin.x, Round(r.offsetMin.y)); 47 | r.offsetMax = new Vector2(r.offsetMax.x, Round(r.offsetMax.y)); 48 | } 49 | else r.anchoredPosition = new Vector2(r.anchoredPosition.x, Round(r.anchoredPosition.y)); 50 | 51 | anchorMin = r.anchorMin; 52 | anchorMax = r.anchorMax; 53 | } 54 | } 55 | 56 | public static int Round(float x) 57 | { 58 | //return Mathf.RoundToInt(x); 59 | return (int)System.Math.Round(x, 0, System.MidpointRounding.AwayFromZero); 60 | } 61 | 62 | public static bool TryGetComponentInParent(Component component, out T c) 63 | { 64 | c = component.GetComponentInParent(); 65 | return c != null; 66 | } 67 | 68 | } 69 | -------------------------------------------------------------------------------- /Scripts/Editor/OpenScenes.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using static EditorHelper; 6 | using static MyGUI; 7 | 8 | public class OpenScenes : MyEditorWindow 9 | { 10 | private static TreeViewComparer treeViewComparer = new(); 11 | private Vector2 scroll; 12 | private int lastSelectedIndex = -1; 13 | private List sceneAssets = new(); 14 | private bool adjustSize = true; 15 | 16 | [MenuItem("File/Open Scenes... %o", false, 160)] 17 | protected static void CreateWindow() 18 | { 19 | var window = GetWindow(false, "Open Scenes"); 20 | window.autoRepaintOnSceneChange = true; 21 | window.minSize = new Vector2(100, 40); 22 | var scenePaths = AssetDatabase.FindAssets("t:scene", new string[] { "Assets" }) 23 | .Select(x => AssetDatabase.GUIDToAssetPath(x)).OrderBy(x => x, treeViewComparer); 24 | window.sceneAssets = scenePaths.Select(x => AssetDatabase.LoadMainAssetAtPath(x)).ToList(); 25 | } 26 | 27 | private void OnEnable() 28 | { 29 | wantsMouseEnterLeaveWindow = true; 30 | wantsMouseMove = true; 31 | } 32 | 33 | protected virtual void OnGUI() 34 | { 35 | var ev = Event.current; 36 | if (ev.type == EventType.MouseMove) Repaint(); 37 | if (ev.type == EventType.KeyDown) KeyboardNavigation( 38 | ev, ref lastSelectedIndex, sceneAssets, enterKey: OnEnterKey, escapeKey: OnEscapeKey); 39 | 40 | bool isAnyHover = false; 41 | scroll = EditorGUILayout.BeginScrollView(scroll); 42 | for (int i = 0; i < sceneAssets.Count; i++) 43 | { 44 | var obj = sceneAssets[i]; 45 | if (obj == null) continue; 46 | 47 | var guiStyle = new GUIStyle(); guiStyle.margin = new RectOffset(); 48 | Rect rect = EditorGUILayout.GetControlRect(false, objectRowHeight, guiStyle); 49 | var buttonResult = ObjectRow(rect, i, obj, sceneAssets, ref lastSelectedIndex, doubleClick: OnEnterKey); 50 | if (buttonResult.isHovered) { isAnyHover = true; hoverObject = obj; } 51 | } 52 | if (!isAnyHover) hoverObject = null; 53 | EditorGUILayout.EndScrollView(); 54 | if (adjustSize) 55 | { 56 | float height = sceneAssets.Count * objectRowHeight; 57 | float windowHeight = Mathf.Min(height, 1200f); 58 | position = new Rect(position.position, 59 | new Vector2(position.width, windowHeight)); 60 | adjustSize = false; 61 | } 62 | } 63 | 64 | private void OnEnterKey() 65 | { 66 | if (!docked) Close(); 67 | } 68 | 69 | //protected virtual void OpenScene(string scenePath, EventModifiers eventModifiers) 70 | //{ 71 | // if (EditorApplication.isPlaying || string.IsNullOrEmpty(scenePath)) return; 72 | 73 | // if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) 74 | // { 75 | // bool additive = eventModifiers == EventModifiers.Control; 76 | // var sceneMode = additive ? OpenSceneMode.Additive : OpenSceneMode.Single; 77 | // EditorSceneManager.OpenScene(scenePath, sceneMode); 78 | // Repaint(); 79 | // UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); 80 | // } 81 | 82 | // if (!docked) Close(); 83 | //} 84 | } 85 | -------------------------------------------------------------------------------- /Scripts/Editor/AssetUtilities/FindMissingReferences.cs: -------------------------------------------------------------------------------- 1 | using System.Linq; 2 | using System.Reflection; 3 | using UnityEditor; 4 | using UnityEditor.SceneManagement; 5 | using UnityEngine; 6 | // Creator and Copyright: https://github.com/liortal53/MissingReferencesUnity/blob/master/ 7 | // License: Apache License version 2.0: https://github.com/liortal53/MissingReferencesUnity/blob/master/LICENSE 8 | /// 9 | /// A helper editor script for finding missing references to objects. 10 | /// 11 | public class FindMissingReferences 12 | { 13 | private const string MENU_ROOT = "Tools/Find Missing References/"; 14 | 15 | /// 16 | /// Finds all missing references to objects in the currently loaded scene. 17 | /// 18 | [MenuItem(MENU_ROOT + "Search in scene", false)] 19 | public static void FindMissingRefsInCurrentScene() 20 | { 21 | var sceneObjects = GetSceneObjects(); 22 | FindMissingRefs(EditorSceneManager.GetActiveScene().path, sceneObjects); 23 | Debug.Log("FindMissingReferences finished scene"); 24 | } 25 | 26 | /// 27 | /// Finds all missing references to objects in all enabled scenes in the project. 28 | /// This works by loading the scenes one by one and checking for missing object references. 29 | /// 30 | [MenuItem(MENU_ROOT + "Search in all scenes", false, 1)] 31 | public static void FindMissingRefsInAllScenes() 32 | { 33 | foreach (var scene in EditorBuildSettings.scenes.Where(s => s.enabled)) 34 | { 35 | EditorSceneManager.OpenScene(scene.path); 36 | FindMissingRefsInCurrentScene(); 37 | } 38 | } 39 | 40 | /// 41 | /// Finds all missing references to objects in assets (objects from the project window). 42 | /// 43 | [MenuItem(MENU_ROOT + "Search in assets", false, 2)] 44 | public static void FindMissingReferencesInAssets() 45 | { 46 | var allAssets = AssetDatabase.GetAllAssetPaths().Where(path => path.StartsWith("Assets/")).ToArray(); 47 | var objs = allAssets.Select(a => AssetDatabase.LoadAssetAtPath(a, typeof(GameObject)) as GameObject).Where(a => a != null).ToArray(); 48 | 49 | FindMissingRefs("Project", objs); 50 | } 51 | 52 | private static void FindMissingRefs(string context, GameObject[] gameObjects) 53 | { 54 | if (gameObjects == null) 55 | { 56 | return; 57 | } 58 | 59 | foreach (var go in gameObjects) 60 | { 61 | var components = go.GetComponents(); 62 | 63 | foreach (var component in components) 64 | { 65 | // Missing components will be null, we can't find their type, etc. 66 | if (!component) 67 | { 68 | Debug.LogError($"Missing Component in GameObject: {GetFullPath(go)}", go); 69 | 70 | continue; 71 | } 72 | 73 | SerializedObject so = new SerializedObject(component); 74 | var sp = so.GetIterator(); 75 | 76 | var objRefValueMethod = typeof(SerializedProperty).GetProperty("objectReferenceStringValue", 77 | BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Public); 78 | 79 | // Iterate over the components' properties. 80 | while (sp.NextVisible(true)) 81 | { 82 | if (sp.propertyType == SerializedPropertyType.ObjectReference) 83 | { 84 | string objectReferenceStringValue = string.Empty; 85 | 86 | if (objRefValueMethod != null) 87 | { 88 | objectReferenceStringValue = (string)objRefValueMethod.GetGetMethod(true).Invoke(sp, new object[] { }); 89 | } 90 | 91 | if (sp.objectReferenceValue == null 92 | && (sp.objectReferenceInstanceIDValue != 0 || objectReferenceStringValue.StartsWith("Missing"))) 93 | { 94 | ShowError(context, go, component.GetType().Name, ObjectNames.NicifyVariableName(sp.name)); 95 | } 96 | } 97 | } 98 | } 99 | } 100 | } 101 | 102 | private static GameObject[] GetSceneObjects() 103 | { 104 | // Use this method since GameObject.FindObjectsOfType will not return disabled objects. 105 | return Resources.FindObjectsOfTypeAll() 106 | .Where(go => string.IsNullOrEmpty(AssetDatabase.GetAssetPath(go)) 107 | && go.hideFlags == HideFlags.None).ToArray(); 108 | } 109 | 110 | private static void ShowError(string context, GameObject go, string componentName, string propertyName) 111 | { 112 | var ERROR_TEMPLATE = "Missing Ref in: [{3}]{0}. Component: {1}, Property: {2}"; 113 | 114 | Debug.LogError(string.Format(ERROR_TEMPLATE, GetFullPath(go), componentName, propertyName, context), go); 115 | } 116 | 117 | private static string GetFullPath(GameObject go) 118 | { 119 | return go.transform.parent == null 120 | ? go.name 121 | : GetFullPath(go.transform.parent.gameObject) + "/" + go.name; 122 | } 123 | } -------------------------------------------------------------------------------- /Scripts/Editor/AssetUtilities/FindMissingScripts.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor; 3 | using UnityEditor.SceneManagement; 4 | using UnityEngine; 5 | using UnityEngine.SceneManagement; 6 | 7 | // Modified from: http://wiki.unity3d.com/index.php?title=FindMissingScripts&oldid=17367 8 | // License: Content is available under Creative Commons Attribution Share Alike https://www.apache.org/licenses/LICENSE-2.0 9 | public class FindMissingScripts : EditorWindow 10 | { 11 | string folderPath = ""; 12 | [MenuItem("Tools/Find Missing Scripts")] 13 | public static void FindMissingScriptsShow() 14 | { 15 | EditorWindow.GetWindow(typeof(FindMissingScripts)); 16 | } 17 | 18 | static int missingCount = -1; 19 | void OnGUI() 20 | { 21 | EditorGUILayout.LabelField("Folder path from Assets. Start with /, eg.: /Prefabs"); 22 | folderPath = EditorGUILayout.TextField(folderPath); 23 | 24 | EditorGUILayout.BeginHorizontal(); 25 | { 26 | EditorGUILayout.LabelField("Missing Scripts:"); 27 | EditorGUILayout.LabelField("" + (missingCount == -1 ? "---" : missingCount.ToString())); 28 | } 29 | EditorGUILayout.EndHorizontal(); 30 | 31 | if (GUILayout.Button("Find missing scripts")) 32 | { 33 | missingCount = 0; 34 | EditorUtility.DisplayProgressBar("Searching Prefabs", "", 0.0f); 35 | 36 | string[] files = System.IO.Directory.GetFiles(Application.dataPath + folderPath, "*.prefab", System.IO.SearchOption.AllDirectories); 37 | EditorUtility.DisplayCancelableProgressBar("Searching Prefabs", "Found " + files.Length + " prefabs", 0.0f); 38 | 39 | Scene currentScene = EditorSceneManager.GetActiveScene(); 40 | string scenePath = currentScene.path; 41 | EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); 42 | 43 | for (int i = 0; i < files.Length; i++) 44 | { 45 | string prefabPath = files[i].Replace(Application.dataPath, "Assets"); 46 | if (EditorUtility.DisplayCancelableProgressBar("Processing Prefabs " + i + "/" + files.Length, prefabPath, (float)i / (float)files.Length)) 47 | break; 48 | 49 | GameObject go = UnityEditor.AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)) as GameObject; 50 | 51 | if (go != null) 52 | { 53 | FindInGO(go); 54 | go = null; 55 | EditorUtility.UnloadUnusedAssetsImmediate(true); 56 | } 57 | } 58 | 59 | EditorUtility.DisplayProgressBar("Cleanup", "Cleaning up", 1.0f); 60 | EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); 61 | 62 | EditorUtility.UnloadUnusedAssetsImmediate(true); 63 | GC.Collect(); 64 | 65 | EditorUtility.ClearProgressBar(); 66 | } 67 | 68 | if (GUILayout.Button("Find missing scripts in selected GO")) 69 | { 70 | FindInSelected(); 71 | } 72 | } 73 | 74 | private static void FindInSelected() 75 | { 76 | GameObject[] go = Selection.gameObjects; 77 | missingCount = 0; 78 | foreach (GameObject g in go) 79 | { 80 | FindInSelectedGO(g); 81 | } 82 | Debug.Log(string.Format("Found {0} missing", missingCount)); 83 | } 84 | 85 | private static void FindInSelectedGO(GameObject g) 86 | { 87 | Component[] components = g.GetComponents(); 88 | for (int i = 0; i < components.Length; i++) 89 | { 90 | if (components[i] == null) 91 | { 92 | missingCount++; 93 | string s = g.name; 94 | Transform t = g.transform; 95 | while (t.parent != null) 96 | { 97 | s = t.parent.name + "/" + s; 98 | t = t.parent; 99 | } 100 | Debug.LogWarning(s + " has an empty script attached in position: " + i, g); 101 | } 102 | } 103 | // Now recurse through each child GO (if there are any): 104 | foreach (Transform childT in g.transform) 105 | { 106 | //Debug.Log("Searching " + childT.name + " " ); 107 | FindInSelectedGO(childT.gameObject); 108 | } 109 | } 110 | 111 | private static void FindInGO(GameObject go, string prefabName = "") 112 | { 113 | Component[] components = go.GetComponents(); 114 | for (int i = 0; i < components.Length; i++) 115 | { 116 | if (components[i] == null) 117 | { 118 | missingCount++; 119 | Transform t = go.transform; 120 | 121 | string componentPath = go.name; 122 | while (t.parent != null) 123 | { 124 | componentPath = t.parent.name + "/" + componentPath; 125 | t = t.parent; 126 | } 127 | Debug.LogWarning("Prefab " + prefabName + " has an empty script attached:\n" + componentPath, go); 128 | } 129 | } 130 | 131 | foreach (Transform child in go.transform) 132 | { 133 | FindInGO(child.gameObject, prefabName); 134 | } 135 | } 136 | } 137 | -------------------------------------------------------------------------------- /Scripts/Editor/AppDataUtility.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using System.IO; 3 | using UnityEditor; 4 | using UnityEngine; 5 | using Debug = UnityEngine.Debug; 6 | 7 | public class AppDataUtility : EditorWindow 8 | { 9 | [MenuItem("Tools/AppData Utility")] 10 | public static void ShowWindow() 11 | { 12 | Rect rect = new Rect(Screen.width / 2f, Screen.height / 2f, 220, 40); 13 | var window = GetWindow(title: "AppData Utility"); 14 | window.position = rect; 15 | window.minSize = new Vector2(100, 40); 16 | window.Show(); 17 | } 18 | 19 | private void OnGUI() 20 | { 21 | Event ev = Event.current; 22 | GUILayout.BeginHorizontal(); 23 | #if UNITY_EDITOR_OSX 24 | EditorUtility.RevealInFinder(Application.persistentDataPath); 25 | #else 26 | if (GUILayout.Button(EditorGUIUtility.IconContent("FolderOpened Icon"), 27 | GUILayout.MaxWidth(40), GUILayout.MaxHeight(17))) 28 | { 29 | string appDataPath = Application.persistentDataPath; 30 | appDataPath = appDataPath.Replace('/', '\\'); 31 | var storagePath = appDataPath + "\\" + "Storage"; 32 | if (Directory.Exists(storagePath)) appDataPath = storagePath; 33 | appDataPath = "\"" + appDataPath + "\""; 34 | ProcessStartInfo process = new ProcessStartInfo("explorer.exe", appDataPath) 35 | { 36 | RedirectStandardOutput = true, 37 | RedirectStandardError = true, 38 | CreateNoWindow = true, 39 | UseShellExecute = false 40 | }; 41 | Process.Start(process); 42 | } 43 | #endif 44 | GUIContent label = new GUIContent("AppData Path:", "Application.persistentDataPath"); 45 | GUILayout.Label(label, EditorStyles.boldLabel); 46 | GUILayout.TextField(Application.persistentDataPath); 47 | 48 | GUILayout.EndHorizontal(); 49 | GUILayout.BeginHorizontal(); 50 | 51 | if (GUILayout.Button("Delete Contents")) 52 | { 53 | if (ev.modifiers == EventModifiers.Shift || 54 | EditorUtility.DisplayDialog("AppData Utility", "Delete all files in the persistent data folder? This cannot be undone.", "Yes", "Cancel")) 55 | { 56 | var directoryInfo = new DirectoryInfo(Application.persistentDataPath); 57 | 58 | foreach (var file in directoryInfo.GetFiles()) 59 | file.Delete(); 60 | foreach (var dir in directoryInfo.GetDirectories()) 61 | dir.Delete(true); 62 | Debug.LogWarning("[AppData Utility] All folder contents were deleted."); 63 | } 64 | } 65 | 66 | if (GUILayout.Button("Delete PlayerPrefs")) 67 | { 68 | if (ev.modifiers == EventModifiers.Shift || 69 | EditorUtility.DisplayDialog("AppData Utility", "Delete all PlayerPrefs? This cannot be undone.", "Yes", "Cancel")) 70 | { 71 | PlayerPrefs.DeleteAll(); 72 | Debug.LogWarning("AppData Utility: All PlayerPrefs were deleted."); 73 | } 74 | } 75 | if (GUILayout.Button("Load Backup")) 76 | { 77 | if (ev.modifiers == EventModifiers.Shift || 78 | EditorUtility.DisplayDialog("AppData Utility", "Load Backup? This cannot be undone.", "Yes", "Cancel")) 79 | { 80 | var fromDir = Directory.GetParent(Application.persistentDataPath) + $"/{Application.productName}Backup"; 81 | var toDir = Application.persistentDataPath; 82 | if (!Directory.Exists(fromDir)) Debug.LogError($"[AppData Utility] {fromDir} does not exist"); 83 | else if (!Directory.Exists(toDir)) Debug.LogError($"[AppData Utility] {toDir} does not exist"); 84 | else 85 | { 86 | CopyFilesRecursively(fromDir, toDir); 87 | Debug.Log("[AppData Utility] Backup loaded"); 88 | } 89 | } 90 | } 91 | if (GUILayout.Button("Save Backup")) 92 | { 93 | if (EditorUtility.DisplayDialog("AppData Utility", "Save Backup? This cannot be undone.", "Yes", "Cancel")) 94 | { 95 | var fromDir = Application.persistentDataPath; 96 | var toDir = Directory.GetParent(Application.persistentDataPath) + $"/{Application.productName}Backup"; 97 | if (!Directory.Exists(fromDir)) Debug.LogError($"[AppData Utility] {fromDir} does not exist"); 98 | else 99 | { 100 | CopyFilesRecursively(fromDir, toDir); 101 | Debug.Log("[AppData Utility] Backup saved"); 102 | } 103 | } 104 | } 105 | GUILayout.FlexibleSpace(); 106 | GUILayout.EndHorizontal(); 107 | } 108 | 109 | private static void CopyFilesRecursively(string sourcePath, string targetPath) 110 | { 111 | // Create all of the directories 112 | foreach (string dirPath in Directory.GetDirectories(sourcePath, "*", SearchOption.AllDirectories)) 113 | { 114 | Directory.CreateDirectory(dirPath.Replace(sourcePath, targetPath)); 115 | } 116 | 117 | // Copy all the files & Replaces any files with the same name 118 | foreach (string newPath in Directory.GetFiles(sourcePath, "*", SearchOption.AllDirectories)) 119 | { 120 | File.Copy(newPath, newPath.Replace(sourcePath, targetPath), true); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Scripts/Editor/InspectorExtensions.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.IO; 3 | using UnityEditor; 4 | using UnityEditor.ShortcutManagement; 5 | using UnityEngine; 6 | 7 | public static class InspectorExtensions 8 | { 9 | [MenuItem("CONTEXT/RectTransform/Anchors to Corners")] 10 | public static void AnchorsToCorners(MenuCommand command) 11 | { 12 | if (Selection.transforms == null || Selection.transforms.Length == 0) 13 | return; 14 | 15 | Undo.IncrementCurrentGroup(); 16 | Undo.SetCurrentGroupName("AnchorsToCorners"); 17 | var undoGroup = Undo.GetCurrentGroup(); 18 | 19 | foreach (Transform transform in Selection.transforms) 20 | { 21 | RectTransform t = transform as RectTransform; 22 | RectTransform pt = Selection.activeTransform.parent as RectTransform; 23 | if (t == null || pt == null) return; 24 | 25 | Undo.RecordObject(t, "AnchorsToCorners"); 26 | 27 | Vector2 newAnchorsMin = new Vector2(t.anchorMin.x + t.offsetMin.x / pt.rect.width, 28 | t.anchorMin.y + t.offsetMin.y / pt.rect.height); 29 | Vector2 newAnchorsMax = new Vector2(t.anchorMax.x + t.offsetMax.x / pt.rect.width, 30 | t.anchorMax.y + t.offsetMax.y / pt.rect.height); 31 | t.anchorMin = newAnchorsMin; 32 | t.anchorMax = newAnchorsMax; 33 | t.offsetMin = t.offsetMax = new Vector2(0, 0); 34 | } 35 | Undo.CollapseUndoOperations(undoGroup); 36 | } 37 | 38 | [Shortcut("Anchors To Corners", KeyCode.T, ShortcutModifiers.Alt)] 39 | public static void AnchorsToCornersGlobal() => AnchorsToCorners(null); 40 | 41 | [MenuItem("CONTEXT/RectTransform/Corners to Anchors")] 42 | public static void CornersToAnchors(MenuCommand command) 43 | { 44 | if (Selection.transforms == null || Selection.transforms.Length == 0) 45 | return; 46 | 47 | Undo.IncrementCurrentGroup(); 48 | Undo.SetCurrentGroupName("CornersToAnchors"); 49 | var undoGroup = Undo.GetCurrentGroup(); 50 | 51 | foreach (Transform transform in Selection.transforms) 52 | { 53 | RectTransform t = transform as RectTransform; 54 | if (t == null) continue; 55 | 56 | Undo.RecordObject(t, "CornersToAnchors"); 57 | t.offsetMin = t.offsetMax = new Vector2(0, 0); 58 | } 59 | Undo.CollapseUndoOperations(undoGroup); 60 | } 61 | 62 | [Shortcut("MakeScreenshot", KeyCode.R, ShortcutModifiers.Alt, displayName = "Make Screenshot")] 63 | public static void Screenshot() => Screenshot(null); 64 | 65 | [MenuItem("CONTEXT/Camera/Screenshot")] 66 | public static void Screenshot(MenuCommand command) 67 | { 68 | if (!AssetDatabase.IsValidFolder("Assets/Screenshots")) 69 | { 70 | AssetDatabase.CreateFolder("Assets", "Screenshots"); 71 | } 72 | var path = $"Assets/Screenshots/Screenshot_{DateTime.Now:yyyy-MM-dd-HH_mm_ss}.png"; 73 | ScreenCapture.CaptureScreenshot(path); 74 | var timerStart = DateTime.Now; 75 | EditorApplication.update += Refresh; 76 | 77 | void Refresh() 78 | { 79 | if (timerStart.AddSeconds(0.5f) < DateTime.Now) 80 | { 81 | EditorApplication.update -= Refresh; 82 | AssetDatabase.ImportAsset(path); 83 | } 84 | } 85 | } 86 | 87 | [MenuItem("CONTEXT/Camera/ScreenshotTransparent")] 88 | public static void ScreenshotTransparent(MenuCommand command) 89 | { 90 | var camera = command.context as Camera; 91 | ScreenshotTransparent(camera, camera.pixelWidth, camera.pixelHeight); 92 | } 93 | 94 | [Shortcut("MakeIcon", KeyCode.R, ShortcutModifiers.Alt | ShortcutModifiers.Shift, displayName = "Make Icon")] 95 | public static void MakeIcon() => ScreenshotTransparent(Camera.allCameras[0], 512, 512); 96 | 97 | [MenuItem("CONTEXT/Camera/Screenshot Transparent 512x512")] 98 | public static void ScreenshotTransparent512(MenuCommand command) 99 | => ScreenshotTransparent((Camera)command.context, 512, 512); 100 | 101 | /// Works when camera background color is set to transparent 102 | public static void ScreenshotTransparent(Camera camera, int width, int height) 103 | { 104 | if (!AssetDatabase.IsValidFolder("Assets/Screenshots")) 105 | AssetDatabase.CreateFolder("Assets", "Screenshots"); 106 | var path = $"Assets/Screenshots/Screenshot_{DateTime.Now:yyyy-MM-dd-HH_mm_ss}.png"; 107 | 108 | Texture2D tempTexture = new Texture2D(width, height, TextureFormat.ARGB32, false); 109 | RenderTexture tempRenderTexture = RenderTexture.GetTemporary(tempTexture.width, tempTexture.height, 32); 110 | 111 | RenderTexture originalCamRenderTexture = camera.targetTexture; 112 | var originalActivaRenderTexture = RenderTexture.active; 113 | 114 | camera.targetTexture = tempRenderTexture; 115 | camera.Render(); 116 | camera.targetTexture = originalCamRenderTexture; 117 | 118 | RenderTexture.active = tempRenderTexture; 119 | tempTexture.ReadPixels(new Rect(0, 0, tempTexture.width, tempTexture.height), 0, 0); // Reads active RenderTexture 120 | tempTexture.Apply(); 121 | RenderTexture.active = originalActivaRenderTexture; 122 | RenderTexture.ReleaseTemporary(tempRenderTexture); 123 | 124 | byte[] bytes = tempTexture.EncodeToPNG(); 125 | UnityEngine.Object.DestroyImmediate(tempTexture); 126 | File.WriteAllBytes(path, bytes); 127 | AssetDatabase.Refresh(); 128 | } 129 | } -------------------------------------------------------------------------------- /Scripts/Editor/APKInstaller.cs: -------------------------------------------------------------------------------- 1 | using System.Diagnostics; 2 | using UnityEditor; 3 | 4 | namespace Tools 5 | { 6 | public static class APKInstaller 7 | { 8 | private static string adbPath = EditorPrefs.GetString("AndroidSdkRoot") + "/platform-tools/adb"; 9 | //This tool can be used as an alternative way to analyze apk file. 10 | //private static string aaptPath = EditorPrefs.GetString("AndroidSdkRoot") + "/build-tools/*/aapt"; 11 | private static string apkAnalyzerPath = EditorPrefs.GetString("AndroidSdkRoot") + "/cmdline-tools/latest/bin/apkanalyzer.bat"; 12 | 13 | [MenuItem("File/Install APK", priority = 211)] 14 | public static void InstallAPK() 15 | { 16 | string[] filter = new string[] { "Android build", "apk" }; 17 | string apkPath = EditorUtility.OpenFilePanelWithFilters("Choose .apk File", "", filter); 18 | if (string.IsNullOrEmpty(apkPath)) return; 19 | Install(apkPath); 20 | } 21 | 22 | [MenuItem("File/Install and run APK", priority = 211)] 23 | public static void InstallAndRunAPK() 24 | { 25 | string[] filter = new string[] { "Android build", "apk" }; 26 | string apkPath = EditorUtility.OpenFilePanelWithFilters("Choose .apk File", "", filter); 27 | if (string.IsNullOrEmpty(apkPath)) return; 28 | Install(apkPath, true); 29 | } 30 | 31 | public static void Install(string apkPath, bool run = false) 32 | { 33 | ProcessStartInfo process = new ProcessStartInfo(adbPath, "install -r \"" + apkPath + "\"") 34 | { 35 | RedirectStandardOutput = true, 36 | RedirectStandardError = true, 37 | CreateNoWindow = true, 38 | UseShellExecute = false 39 | }; 40 | var installProcess = Process.Start(process); 41 | EditorUtility.DisplayProgressBar("Installing APK", "Installing...", 0.5f); 42 | installProcess.WaitForExit(); 43 | 44 | string result = "Result: " + installProcess.StandardOutput.ReadLine() + ": " + installProcess.StandardOutput.ReadToEnd(); 45 | result += installProcess.StandardError.ReadToEnd(); 46 | 47 | if (installProcess.ExitCode != 0) 48 | UnityEngine.Debug.LogError(result); 49 | else 50 | UnityEngine.Debug.Log(result); 51 | EditorUtility.ClearProgressBar(); 52 | if (run) Run(); 53 | 54 | //Use RunApk(...) instead of Run(), if package name in player settings does not match apk package name 55 | //You need to have Command line tools installed, see documentation 56 | //if (run) RunApk(apkPath); 57 | } 58 | 59 | public static void Run() 60 | { 61 | string appIdentifier = PlayerSettings.GetApplicationIdentifier(BuildTargetGroup.Android); 62 | //This is the default Unity android launcher, however in my experience, it does not work properly. I use monkey launcher instead. 63 | //string mainActivity = "com.unity3d.player.UnityPlayerActivity"; 64 | //string adbCommand = "shell am start -a android.intent.action.MAIN -n " + appIdentifier + "/" + mainActivity; 65 | string adbCommand = "shell monkey -p \"" + appIdentifier + "\" 1"; 66 | 67 | var process = new ProcessStartInfo(adbPath, adbCommand) 68 | { 69 | RedirectStandardOutput = true, 70 | RedirectStandardError = true, 71 | CreateNoWindow = true, 72 | UseShellExecute = false 73 | }; 74 | var runProcess = Process.Start(process); 75 | string result = "Running app " + appIdentifier + ": " + runProcess.StandardOutput.ReadLine() + ": " + runProcess.StandardOutput.ReadToEnd(); 76 | result += runProcess.StandardError.ReadToEnd(); 77 | 78 | if (runProcess.ExitCode != 0) 79 | UnityEngine.Debug.LogError(result); 80 | else 81 | UnityEngine.Debug.Log(result); 82 | } 83 | 84 | /// 85 | /// Use this method, if package name in player settings does not match apk package name 86 | /// You need to have Command line tools installed, see documentation 87 | /// 88 | /// 89 | public static void RunApk(string apkPath) 90 | { 91 | ProcessStartInfo process = new ProcessStartInfo(apkAnalyzerPath, "manifest application-id \"" + apkPath + "\"") 92 | { 93 | RedirectStandardOutput = true, 94 | RedirectStandardError = true, 95 | CreateNoWindow = true, 96 | UseShellExecute = false 97 | }; 98 | var findIdentifierProcess = Process.Start(process); 99 | findIdentifierProcess.WaitForExit(); 100 | 101 | string appIdentifier = findIdentifierProcess.StandardOutput.ReadLine(); 102 | //string mainActivity = "com.unity3d.player.UnityPlayerActivity"; 103 | //string adbCommand = "shell am start -a android.intent.action.MAIN -n " + appIdentifier + "/" + mainActivity; 104 | string adbCommand = "shell monkey -p \"" + appIdentifier + "\" 1"; 105 | 106 | process = new ProcessStartInfo(adbPath, adbCommand) 107 | { 108 | RedirectStandardOutput = true, 109 | RedirectStandardError = true, 110 | CreateNoWindow = true, 111 | UseShellExecute = false 112 | }; 113 | var runProcess = Process.Start(process); 114 | string result = "Running app (monkey) " + appIdentifier + ": " + runProcess.StandardOutput.ReadLine() + ": " + runProcess.StandardOutput.ReadToEnd(); 115 | result += runProcess.StandardError.ReadToEnd(); 116 | 117 | if (runProcess.ExitCode != 0) 118 | UnityEngine.Debug.LogError(result); 119 | else 120 | UnityEngine.Debug.Log(result); 121 | } 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /Scripts/Editor/AssetUtilities/FindUnusedAssets.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Text.RegularExpressions; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using static EditorHelper; 7 | using static MyGUI; 8 | 9 | public class FindUnusedAssets : MyEditorWindow 10 | { 11 | private static readonly string[] excludedExtensions = 12 | { 13 | "unity", "preset", "spriteatlas", 14 | "dll", "m", "java", "aar", "jar", "mm", "h", "plist", 15 | "xml", "json", "txt", "md", "pdf", 16 | "asmdef", "asmref", 17 | }; 18 | private static TreeViewComparer treeViewComparer = new(); 19 | 20 | private List unusedAssets = new(); 21 | private string subfolder = ""; 22 | private bool canceled = false; 23 | private Vector2 scroll = Vector2.zero; 24 | private int lastSelectedIndex = -1; 25 | 26 | [MenuItem("Tools/Find Unused Assets")] 27 | public static void CreateWindow() 28 | { 29 | FindUnusedAssets window = GetWindow(); 30 | window.Show(); 31 | } 32 | 33 | private void OnEnable() 34 | { 35 | wantsMouseEnterLeaveWindow = true; 36 | wantsMouseMove = true; 37 | } 38 | 39 | private void OnGUI() 40 | { 41 | var ev = Event.current; 42 | if (ev.type == EventType.MouseMove) Repaint(); 43 | if (ev.type == EventType.KeyDown) KeyboardNavigation( 44 | ev, ref lastSelectedIndex, unusedAssets, escapeKey: OnEscapeKey); 45 | 46 | GUILayout.BeginHorizontal(); 47 | GUIContent labelContent = new GUIContent("Subfolder:", "\"Assets/\" + subFolder"); 48 | GUILayout.Label(labelContent, GUILayout.MaxWidth(65)); 49 | subfolder = GUILayout.TextField(subfolder); 50 | GUIContent searchContent = EditorGUIUtility.IconContent("Search Icon"); 51 | if (GUILayout.Button(searchContent, GUILayout.MaxWidth(40), GUILayout.MaxHeight(20))) 52 | { 53 | FindUnused(); 54 | } 55 | GUILayout.EndHorizontal(); 56 | 57 | GUILayout.Label($"Unused Assets: ({unusedAssets.Count})", EditorStyles.boldLabel); 58 | EditorGUILayout.Space(); 59 | 60 | bool isAnyHover = false; 61 | scroll = EditorGUILayout.BeginScrollView(scroll); 62 | for (int i = 1; i < unusedAssets.Count; i++) 63 | { 64 | var obj = unusedAssets[i]; 65 | if (obj == null) continue; 66 | 67 | var guiStyle = new GUIStyle(); guiStyle.margin = new RectOffset(); 68 | Rect rect = EditorGUILayout.GetControlRect(false, objectRowHeight, guiStyle); 69 | var buttonResult = ObjectRow(rect, i, obj, unusedAssets, ref lastSelectedIndex); 70 | if (buttonResult.isHovered) { isAnyHover = true; hoverObject = obj; } 71 | } 72 | if (!isAnyHover) hoverObject = null; 73 | EditorGUILayout.EndScrollView(); 74 | } 75 | 76 | private async void FindUnused() 77 | { 78 | canceled = false; 79 | unusedAssets.Clear(); // Empty old results 80 | 81 | var assetsSubfolder = "Assets/" + subfolder; 82 | var assetPaths = AssetDatabase.GetAllAssetPaths().Where(x => x.StartsWith("Assets/" + subfolder) 83 | && !AssetDatabase.IsValidFolder(x)).ToList(); 84 | assetPaths = assetPaths.Where(x => !x.Contains("/Resources/") && 85 | !x.Contains("/Editor/") && !x.Contains("/Plugins/") && !x.Contains("/StreamingAssets/") && 86 | !x.Contains("/Addressables/") && !x.Contains("/External/") && !x.Contains("/ExternalAssets/") 87 | && !x.Contains("/IgnoreSCM/") && !x.Contains("/AddressableAssetsData/") && !x.Contains("/FacebookSDK/") 88 | && !x.Contains("/GoogleMobileAds/") && !x.Contains("/GooglePlayGames/") 89 | && !x.Contains("/Settings/") && !x.Contains("/TextMesh Pro/")).ToList(); 90 | assetPaths = assetPaths.Where(x => !Regex.IsMatch(x, $"\\.({string.Join("|", excludedExtensions)})$")).ToList(); 91 | 92 | // If we deliberately select subfolder that is one of the above, add it again 93 | if (assetPaths.Count == 0) 94 | assetPaths = AssetDatabase.GetAllAssetPaths().Where(x => x.StartsWith("Assets/" + subfolder) 95 | && !AssetDatabase.IsValidFolder(x)).ToList(); 96 | 97 | // Do not check scripts that do not contain class derived from UnityEngine.Object 98 | var assetPathsCopy = new List(assetPaths); 99 | var scripts = assetPathsCopy.Where(x => x.EndsWith(".cs")).ToList(); 100 | var nonMonos = new List(); 101 | foreach (var s in scripts) 102 | { 103 | var loadedScript = AssetDatabase.LoadAssetAtPath(s); 104 | if (loadedScript != null && 105 | (loadedScript.GetClass() == null || !loadedScript.GetClass().IsSubclassOf(typeof(Object)))) 106 | { 107 | assetPathsCopy.Remove(s); 108 | } 109 | } 110 | assetPaths = assetPathsCopy; 111 | 112 | int total = assetPaths.Count; 113 | int current = 0; 114 | float progress = 0; 115 | 116 | if (total > 0) 117 | { 118 | // This will properly initialize SearchIndex, without it, Sync method could return incorrect (empty) results 119 | var testObj = AssetDatabase.LoadMainAssetAtPath(assetPaths.ToList()[0]); 120 | await FindAssetUsages.FindObjectUsageAsync(testObj); 121 | } 122 | 123 | var unusedAssetPaths = new List(); 124 | foreach (var assetPath in assetPaths) 125 | { 126 | current++; 127 | progress = current / (float)total; 128 | 129 | if (canceled || 130 | EditorUtility.DisplayCancelableProgressBar($"Searching... {current}/{total}", $"Canceled", progress)) 131 | { 132 | EditorUtility.ClearProgressBar(); 133 | return; 134 | } 135 | 136 | var obj = AssetDatabase.LoadMainAssetAtPath(assetPath); 137 | 138 | if (!FindAnyAssetUsage(obj)) 139 | { 140 | unusedAssetPaths.Add(assetPath); 141 | } 142 | 143 | } 144 | unusedAssetPaths = unusedAssetPaths.OrderBy(x => x, treeViewComparer).ToList(); 145 | unusedAssets = unusedAssetPaths.Select(x => AssetDatabase.LoadMainAssetAtPath(x)).ToList(); 146 | 147 | EditorUtility.ClearProgressBar(); 148 | } 149 | 150 | /// Tries to find any asset usage. If one is found, returns true 151 | private static bool FindAnyAssetUsage(Object obj) 152 | { 153 | var usedBy = FindAssetUsages.FindObjectUsageSync(obj); 154 | return usedBy.Any(); 155 | } 156 | } 157 | -------------------------------------------------------------------------------- /Scripts/Editor/AssetUtilities/FindAssetUsages.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.Linq; 3 | using System.Threading.Tasks; 4 | using UnityEditor; 5 | using UnityEditor.Search; 6 | using UnityEngine; 7 | using static EditorHelper; 8 | using static MyGUI; 9 | using Object = UnityEngine.Object; 10 | 11 | public class FindAssetUsages : MyEditorWindow 12 | { 13 | private static TreeViewComparer treeViewComparer = new(); 14 | 15 | private Object selectedObject; 16 | private List usedByObjects = new(); 17 | private List shownItems = new(); 18 | private Vector2 scroll = Vector2.zero; 19 | private int lastSelectedIndex = -1; 20 | private bool adjustSize; 21 | 22 | [MenuItem("Assets/Find Asset Usage _#F12")] 23 | public static async void CreateWindow() 24 | { 25 | var window = CreateWindow(); 26 | window.Show(); 27 | 28 | if (Selection.objects.Length > 0) 29 | window.selectedObject = Selection.objects[0]; 30 | 31 | await window.Find(); 32 | window.Repaint(); 33 | } 34 | 35 | private void OnEnable() 36 | { 37 | wantsMouseEnterLeaveWindow = true; 38 | wantsMouseMove = true; 39 | } 40 | 41 | private void OnGUI() 42 | { 43 | var ev = Event.current; 44 | if (ev.type == EventType.MouseMove) Repaint(); 45 | if (ev.type == EventType.KeyDown) KeyboardNavigation( 46 | ev, ref lastSelectedIndex, shownItems, escapeKey: OnEscapeKey); 47 | bool isAnyHover = false; 48 | GUILayout.BeginHorizontal(); 49 | if (shownItems.Count > 0) 50 | { 51 | Rect rect = EditorGUILayout.GetControlRect(false, objectRowHeight); 52 | var buttonResult = ObjectRow(rect, 0, shownItems[0], shownItems, ref lastSelectedIndex); 53 | if (buttonResult.isHovered) { isAnyHover = true; hoverObject = shownItems[0]; } 54 | } 55 | 56 | GUIContent searchContent = EditorGUIUtility.IconContent("Search Icon"); 57 | if (GUILayout.Button(searchContent, GUILayout.MaxWidth(40), GUILayout.MaxHeight(16))) 58 | _ = Find(); 59 | 60 | GUILayout.EndHorizontal(); 61 | GUILayout.Label("Is Used By:", EditorStyles.boldLabel, GUILayout.MaxHeight(16)); 62 | 63 | scroll = EditorGUILayout.BeginScrollView(scroll); 64 | for (int i = 1; i < shownItems.Count; i++) 65 | { 66 | var obj = shownItems[i]; 67 | if (obj == null) continue; 68 | 69 | var guiStyle = new GUIStyle(); guiStyle.margin = new RectOffset(); 70 | Rect rect = EditorGUILayout.GetControlRect(false, objectRowHeight, guiStyle); 71 | var buttonResult = ObjectRow(rect, i, obj, shownItems, ref lastSelectedIndex); 72 | if (buttonResult.isHovered) { isAnyHover = true; hoverObject = obj; } 73 | } 74 | if (!isAnyHover) hoverObject = null; 75 | EditorGUILayout.EndScrollView(); 76 | if (adjustSize) 77 | { 78 | float height = shownItems.Count * objectRowHeight + 30; 79 | float windowHeight = Mathf.Min(height, 1200f); 80 | if (adjustSize) windowHeight = Mathf.Max(windowHeight, position.height); // Enlarge only 81 | position = new Rect(position.position, 82 | new Vector2(position.width, windowHeight)); 83 | adjustSize = false; 84 | } 85 | } 86 | 87 | private async Task Find() 88 | { 89 | usedByObjects = await FindObjectUsageAsync(selectedObject, true, true); 90 | shownItems.Clear(); 91 | shownItems.Add(selectedObject); 92 | shownItems.AddRange(usedByObjects); 93 | adjustSize = true; 94 | Repaint(); 95 | } 96 | 97 | public static async Task> FindObjectUsageAsync(Object obj, bool filter = false, bool sort = false) 98 | { 99 | string objectPath = ""; 100 | Object asset = null; 101 | if (IsAsset(obj)) 102 | { 103 | asset = obj; 104 | objectPath = AssetDatabase.GetAssetPath(obj); 105 | } 106 | else if (IsNonAssetGameObject(obj)) 107 | { 108 | objectPath = obj.GetInstanceID().ToString(); 109 | } 110 | 111 | bool finished = false; 112 | List resultItems = new(); 113 | // This is copied from Unity's experimental package: https://github.com/Unity-Technologies/com.unity.search.extensions 114 | // from script Dependency.cs 115 | var searchContext = SearchService.CreateContext(new[] { "dep", "scene", "asset", "adb" }, $"ref=\"{objectPath}\""); 116 | SearchService.Request(searchContext, 117 | (SearchContext context, IList items) 118 | => Found(ref finished, items, ref resultItems)); 119 | await WaitUntil(() => finished); 120 | var results = resultItems.Select(x => x.ToObject()).Where(x => x != null).ToList(); 121 | 122 | if (filter) results = FilterResults(results, asset); 123 | if (sort) results = SortResults(results); 124 | return results; 125 | } 126 | 127 | // This is faster for multiple searches e.g. in FindUnusedAssets, because Async version only does 1 search per editor Frame 128 | public static List FindObjectUsageSync(Object obj, bool filter = false, bool sort = false) 129 | { 130 | string objectPath = ""; 131 | Object asset = null; 132 | if (IsAsset(obj)) 133 | { 134 | asset = obj; 135 | objectPath = AssetDatabase.GetAssetPath(obj); 136 | } 137 | else if (IsNonAssetGameObject(obj)) 138 | { 139 | objectPath = obj.GetInstanceID().ToString(); 140 | } 141 | 142 | List resultItems = new(); 143 | var searchContext = SearchService.CreateContext(new[] { "dep", "scene", "asset", "adb" }, $"ref=\"{objectPath}\""); 144 | var results = SearchService.Request(searchContext, SearchFlags.Synchronous).Fetch() 145 | .Select(x => x.ToObject()).Where(x => x != null).ToList(); 146 | 147 | if (filter) results = FilterResults(results, asset); 148 | if (sort) results = SortResults(results); 149 | return results; 150 | } 151 | 152 | // Without IsAnyPrefabInstanceRoot, results contain every childGameobject 153 | public static List FilterResults(List results, Object asset = null) 154 | { 155 | results = results.Distinct().ToList(); 156 | if (asset != null) // Only for Assets, not for Hierarchy GameObjects 157 | results = results.Where(x => !ArePartOfSameMainAssets(x, asset)).ToList(); 158 | var filteredResults = new List(); 159 | foreach (var obj in results) 160 | { 161 | if (IsAsset(obj)) 162 | { 163 | filteredResults.Add(obj); 164 | continue; 165 | } 166 | if (IsNonAssetGameObject(obj)) 167 | { 168 | var go = obj as GameObject; 169 | if (!PrefabUtility.IsPartOfAnyPrefab(go)) 170 | { 171 | filteredResults.Add(go); 172 | continue; 173 | } 174 | if (PrefabUtility.IsAnyPrefabInstanceRoot(go)) 175 | { 176 | filteredResults.Add(go); 177 | continue; 178 | } 179 | var root = PrefabUtility.GetNearestPrefabInstanceRoot(go); 180 | if (!results.Contains(root)) 181 | filteredResults.Add(go); 182 | } 183 | } 184 | return filteredResults; 185 | } 186 | 187 | // Sort as treeView, NonAssets last 188 | public static List SortResults(List results) 189 | { 190 | var sortedResults = results.Where(x => IsAsset(x)) 191 | .OrderBy(x => AssetDatabase.GetAssetPath(x), treeViewComparer).ToList(); 192 | 193 | // NonAssets last 194 | sortedResults.AddRange(results.Where(x => !IsAsset(x))); 195 | return sortedResults; 196 | } 197 | 198 | private static void Found(ref bool finished, IList items, ref List resultItems) 199 | { 200 | resultItems = items.ToList(); 201 | finished = true; 202 | } 203 | 204 | public static async Task WaitUntil(System.Func condition, int timeout = -1) 205 | { 206 | var waitTask = Task.Run(async () => 207 | { 208 | while (!condition()) await Task.Delay(1); 209 | }); 210 | 211 | if (waitTask != await Task.WhenAny(waitTask, Task.Delay(timeout))) throw new System.TimeoutException(); 212 | } 213 | 214 | } 215 | -------------------------------------------------------------------------------- /Scripts/Editor/ComponentUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using UnityEditor; 3 | using UnityEngine; 4 | using Object = UnityEngine.Object; 5 | 6 | public class ComponentUtilities 7 | { 8 | private static SerializedObject savedSerializedObject; 9 | private static Object savedTargetObject; 10 | 11 | // Property copying 12 | private static SerializedObject savedSerializedObjectForProperty; 13 | private static string savedPropertyPath; 14 | private static bool isSaved; 15 | private static Type savedType; 16 | 17 | [InitializeOnLoadMethod] 18 | static void Initialize() 19 | { 20 | EditorApplication.contextualPropertyMenu += CopyOnContextClickWithModifiers; 21 | } 22 | 23 | public static void CopyOnContextClickWithModifiers(GenericMenu menu, SerializedProperty property) 24 | { 25 | AddCopyPastePropertyOption(menu, property); 26 | // Ctrl == copy, shift == paste, alt == copy, ctrl + alt == cut, ctrl + shift == adaptive paste 27 | var modifiers = Event.current.modifiers; 28 | // Copy 29 | if (modifiers == EventModifiers.Control || modifiers == (EventModifiers.Control | EventModifiers.Alt)) 30 | { 31 | var originalObject = property.serializedObject.targetObject; 32 | savedSerializedObject = new SerializedObject(originalObject); 33 | // savedObject.targetObject is null if we delete component in the meantime. Hence we create a copy 34 | 35 | if (originalObject is not Component && originalObject is not GameObject) // Can be a scriptableObject 36 | { 37 | savedTargetObject = Object.Instantiate(property.serializedObject.targetObject); 38 | savedType = originalObject.GetType(); 39 | isSaved = true; 40 | Debug.Log("Component copied"); 41 | } 42 | else if (originalObject is Component) 43 | { 44 | Unsupported.CopyComponentToPasteboard(originalObject as Component); 45 | savedTargetObject = null; 46 | isSaved = true; 47 | savedType = originalObject.GetType(); 48 | Debug.Log("Component copied"); 49 | } 50 | else 51 | { 52 | Debug.LogError("Cannot copy this object"); 53 | } 54 | 55 | } 56 | // Paste 57 | if (modifiers == (EventModifiers.Shift | EventModifiers.Control)) // Adaptive Paste 58 | { 59 | if (!isSaved) 60 | { 61 | Debug.Log("Saved component is null"); 62 | return; 63 | } 64 | AdaptivePaste(property); 65 | } 66 | else if (modifiers == EventModifiers.Shift) // Normal Paste 67 | { 68 | if (savedSerializedObject == null) 69 | { 70 | Debug.Log("Saved component is null"); 71 | return; 72 | } 73 | if (!isSaved) 74 | { 75 | Debug.Log("Saved component target object is null"); 76 | return; 77 | } 78 | 79 | // Paste values if type is the same - Component 80 | if (savedType == property.serializedObject.targetObject.GetType() && savedTargetObject == null) 81 | { 82 | Unsupported.PasteComponentValuesFromPasteboard(property.serializedObject.targetObject as Component); 83 | Debug.Log("Component values pasted"); 84 | } // Paste values if type is the same - Something else (ScriptableObject) 85 | else if (savedType == property.serializedObject.targetObject.GetType() && savedTargetObject != null) 86 | { 87 | var name = property.serializedObject.targetObject.name; 88 | Undo.RecordObject(property.serializedObject.targetObject, "Paste component values"); 89 | EditorUtility.CopySerialized(savedTargetObject, property.serializedObject.targetObject); 90 | if (property.serializedObject.targetObject is ScriptableObject so) 91 | so.name = name; 92 | Debug.Log("Component values pasted"); 93 | } 94 | // Create new component 95 | else 96 | { 97 | GameObject g = ((Component)property.serializedObject.targetObject).gameObject; 98 | if (Unsupported.PasteComponentFromPasteboard(g)) 99 | Debug.Log("Component pasted as new"); 100 | } 101 | 102 | } 103 | // Delete Component 104 | // This has multi-edit support. Can be implemented in the same way to the other actions too, if needed 105 | if ((modifiers & EventModifiers.Alt) == EventModifiers.Alt && 106 | property.serializedObject.targetObject is Component) 107 | { 108 | foreach (var targetObject in property.serializedObject.targetObjects) 109 | { 110 | Undo.RegisterFullObjectHierarchyUndo(targetObject, "Delete component"); 111 | int undoID = Undo.GetCurrentGroup(); 112 | Undo.CollapseUndoOperations(undoID); 113 | EditorApplication.delayCall += () => { Object.DestroyImmediate(targetObject); }; 114 | } 115 | } 116 | } 117 | 118 | /// Tries to find properties with the same name and type and copypastes values 119 | private static void AdaptivePaste(SerializedProperty property) 120 | { 121 | Undo.RecordObject(property.serializedObject.targetObject, "Paste component values adaptively"); 122 | SerializedObject destination = new SerializedObject(property.serializedObject.targetObject); 123 | SerializedProperty sourceProperties = savedSerializedObject.GetIterator(); 124 | 125 | // This if statement will skip script type so that we don't override the destination component's type 126 | if (sourceProperties.NextVisible(true)) 127 | { 128 | while (sourceProperties.NextVisible(true)) // Iterate through all serializedProperties 129 | { 130 | // Find corresponding property in destination component by path 131 | SerializedProperty destinationProperty = destination.FindProperty(sourceProperties.propertyPath); 132 | 133 | // Validate that the properties are present in both components, and that they're the same type 134 | if (destinationProperty != null && destinationProperty.propertyType == sourceProperties.propertyType) 135 | { 136 | // Copy value from savedObject to destination component 137 | destination.CopyFromSerializedProperty(sourceProperties); 138 | } 139 | } 140 | } 141 | destination.ApplyModifiedProperties(); 142 | AssetDatabase.SaveAssets(); 143 | Debug.Log("Component values adaptively pasted"); 144 | } 145 | 146 | // Copy/Paste of individual properties (through property context menu) 147 | private static void AddCopyPastePropertyOption(GenericMenu menu, SerializedProperty property) 148 | { 149 | var propertyPath = property.propertyPath; 150 | menu.AddItem(new GUIContent("Copy Property"), false, 151 | () => CopyProperty(property.serializedObject, propertyPath)); 152 | if (savedSerializedObjectForProperty != null) 153 | { 154 | menu.AddItem(new GUIContent("Paste Property"), false, 155 | () => PasteProperty(property.serializedObject, propertyPath)); 156 | menu.AddItem(new GUIContent("Paste Property Adaptively"), false, 157 | () => PastePropertyAdaptively(property.serializedObject, propertyPath)); 158 | } 159 | else 160 | menu.AddDisabledItem(new GUIContent("Paste Property"), false); 161 | } 162 | 163 | private static void CopyProperty(SerializedObject serializedObject, string propertyPath) 164 | { 165 | savedPropertyPath = propertyPath; 166 | savedSerializedObjectForProperty = new SerializedObject(serializedObject.targetObject); 167 | } 168 | private static void PasteProperty(SerializedObject destination, string propertyPath) 169 | { 170 | var sourceProperty = savedSerializedObjectForProperty.FindProperty(savedPropertyPath); 171 | 172 | destination.CopyFromSerializedProperty(sourceProperty); 173 | destination.ApplyModifiedProperties(); 174 | AssetDatabase.SaveAssets(); 175 | } 176 | 177 | /// 178 | /// Paste only child properties of a property, useful for SerializeReference properties pasting to not change type 179 | /// 180 | private static void PastePropertyAdaptively(SerializedObject destination, string propertyPath) 181 | { 182 | Undo.RecordObject(destination.targetObject, "Paste property values adaptively"); 183 | var sourceProperty = savedSerializedObjectForProperty.FindProperty(savedPropertyPath); 184 | if (sourceProperty.propertyType != SerializedPropertyType.ManagedReference) 185 | { 186 | PasteProperty(destination, propertyPath); 187 | return; 188 | } 189 | 190 | foreach (SerializedProperty prop in sourceProperty) // Iterate through all serializedProperties 191 | { 192 | // Find corresponding property in destination component by name 193 | SerializedProperty destinationProperty = destination.FindProperty(propertyPath + "." + prop.name); 194 | 195 | // Validate that the properties are present in both components, and that they're the same type 196 | if (destinationProperty != null && destinationProperty.propertyType == prop.propertyType) 197 | { 198 | // Copy value from savedObject to destination component 199 | destination.CopyFromSerializedProperty(prop); 200 | } 201 | } 202 | destination.ApplyModifiedProperties(); 203 | AssetDatabase.SaveAssets(); 204 | } 205 | } 206 | -------------------------------------------------------------------------------- /Scripts/Editor/Common/EditorHelper.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using System.Reflection; 6 | using UnityEditor; 7 | using UnityEditor.ShortcutManagement; 8 | using UnityEditorInternal; 9 | using UnityEngine; 10 | using Object = UnityEngine.Object; 11 | 12 | public class EditorHelper 13 | { 14 | #region Reflection 15 | public static void OpenPropertyEditor(Object obj) 16 | { 17 | string windowTypeName = "UnityEditor.PropertyEditor"; 18 | var windowType = typeof(Editor).Assembly.GetType(windowTypeName); 19 | MethodInfo builderMethod = windowType.GetMethod("OpenPropertyEditor", 20 | BindingFlags.Static | BindingFlags.NonPublic, 21 | null, 22 | new Type[] { typeof(Object), typeof(bool) }, 23 | null 24 | ); 25 | builderMethod.Invoke(null, new object[] { obj, true }); 26 | } 27 | 28 | [Shortcut("PropertyEditor/MyEditorWindowOpenMouseOver", KeyCode.Menu, ShortcutModifiers.Alt)] 29 | public static void OpenPropertyEditorHoverItem() 30 | { 31 | var windows = Resources.FindObjectsOfTypeAll(); 32 | foreach (var window in windows) 33 | { 34 | if (window.hoverObject) 35 | { 36 | OpenPropertyEditor(window.hoverObject); 37 | return; 38 | } 39 | } 40 | string windowTypeName = "UnityEditor.PropertyEditor"; 41 | var windowType = typeof(Editor).Assembly.GetType(windowTypeName); 42 | MethodInfo builderMethod = windowType.GetMethod("OpenHoveredItemPropertyEditor", 43 | BindingFlags.Static | BindingFlags.NonPublic); 44 | builderMethod.Invoke(null, new object[] { null }); 45 | } 46 | 47 | public static void OpenHierarchyContextMenu(int itemID) 48 | { 49 | string windowTypeName = "UnityEditor.SceneHierarchyWindow"; 50 | var windowType = typeof(Editor).Assembly.GetType(windowTypeName); 51 | EditorWindow window = EditorWindow.GetWindow(windowType); 52 | FieldInfo sceneField = windowType.GetField("m_SceneHierarchy", BindingFlags.Instance | BindingFlags.NonPublic); 53 | var sceneHierarchy = sceneField.GetValue(window); 54 | 55 | string hierarchyTypeName = "UnityEditor.SceneHierarchy"; 56 | var hierarchyType = typeof(Editor).Assembly.GetType(hierarchyTypeName); 57 | MethodInfo builderMethod = hierarchyType.GetMethod("ItemContextClick", 58 | BindingFlags.Instance | BindingFlags.NonPublic); 59 | builderMethod.Invoke(sceneHierarchy, new object[] { itemID }); 60 | } 61 | 62 | // Component menu 63 | public static void OpenObjectContextMenu(Rect rect, Object obj) 64 | { 65 | var classType = typeof(EditorUtility); 66 | MethodInfo builderMethod = 67 | classType.GetMethod("DisplayObjectContextMenu", BindingFlags.Static | BindingFlags.NonPublic, null, 68 | new Type[] { typeof(Rect), typeof(Object), typeof(int) }, null); 69 | builderMethod.Invoke(null, new object[] { rect, obj, 0 }); 70 | } 71 | 72 | public static void ExpandFolder(int instanceID, bool expand) 73 | { 74 | int[] expandedFolders = InternalEditorUtility.expandedProjectWindowItems; 75 | bool isExpanded = expandedFolders.Contains(instanceID); 76 | if (expand == isExpanded) return; 77 | 78 | var unityEditorAssembly = Assembly.GetAssembly(typeof(Editor)); 79 | var projectBrowserType = unityEditorAssembly.GetType("UnityEditor.ProjectBrowser"); 80 | var projectBrowsers = Resources.FindObjectsOfTypeAll(projectBrowserType); 81 | 82 | foreach (var p in projectBrowsers) 83 | { 84 | var treeViewControllerType = unityEditorAssembly.GetType("UnityEditor.IMGUI.Controls.TreeViewController"); 85 | FieldInfo treeViewControllerField = 86 | projectBrowserType.GetField("m_AssetTree", BindingFlags.Instance | BindingFlags.NonPublic); 87 | // OneColumn has only AssetTree, TwoColumn has also FolderTree 88 | var treeViewController = treeViewControllerField.GetValue(p); 89 | if (treeViewController == null) continue; 90 | var changeGoldingMethod = 91 | treeViewControllerType.GetMethod("ChangeFolding", BindingFlags.Instance | BindingFlags.NonPublic); 92 | changeGoldingMethod.Invoke(treeViewController, new object[] { new int[] { instanceID }, expand }); 93 | EditorWindow pw = (EditorWindow)p; 94 | pw.Repaint(); 95 | } 96 | } 97 | 98 | public static void SelectWithoutFocus(params Object[] objects) 99 | { 100 | var unityEditorAssembly = Assembly.GetAssembly(typeof(Editor)); 101 | var projectBrowserType = unityEditorAssembly.GetType("UnityEditor.ProjectBrowser"); 102 | var projectBrowsers = Resources.FindObjectsOfTypeAll(projectBrowserType); 103 | var isLockedProperty = projectBrowserType.GetProperty("isLocked", BindingFlags.Instance | BindingFlags.NonPublic); 104 | var oldValues = new List(); 105 | foreach (var p in projectBrowsers) 106 | { 107 | oldValues.Add((bool)isLockedProperty.GetValue(p)); 108 | isLockedProperty.SetValue(p, true); 109 | EditorWindow pw = (EditorWindow)p; 110 | pw.Repaint(); 111 | } 112 | Selection.objects = objects; 113 | EditorApplication.delayCall += () => 114 | { 115 | for (int i = 0; i < projectBrowsers.Length; i++) 116 | { 117 | var p = projectBrowsers[i]; 118 | isLockedProperty.SetValue(p, oldValues[i]); 119 | 120 | EditorWindow pw = (EditorWindow)p; 121 | pw.Repaint(); 122 | } 123 | }; 124 | } 125 | 126 | public static void OpenObject(Object obj) 127 | { 128 | if (IsAsset(obj)) AssetDatabase.OpenAsset(obj); 129 | else if (IsNonAssetGameObject(obj)) SceneView.lastActiveSceneView.FrameSelected(); 130 | } 131 | #endregion 132 | 133 | #region Helpers 134 | public static int Mod(int x, int m) 135 | { 136 | return (x % m + m) % m; // Always positive modulus 137 | } 138 | 139 | public static bool IsComponent(Object obj) 140 | { 141 | return obj is Component; 142 | } 143 | 144 | public static bool IsAsset(Object obj) 145 | { 146 | return AssetDatabase.Contains(obj); 147 | } 148 | 149 | public static bool IsNonAssetGameObject(Object obj) 150 | { 151 | return !IsAsset(obj) && obj is GameObject; 152 | } 153 | 154 | public static bool IsSceneObject(Object obj, out GameObject main) 155 | { 156 | if (IsNonAssetGameObject(obj)) 157 | { 158 | main = (GameObject)obj; 159 | return true; 160 | } 161 | else if (IsComponent(obj) && IsNonAssetGameObject(((Component)obj).gameObject)) 162 | { 163 | main = ((Component)obj).gameObject; 164 | return true; 165 | } 166 | main = null; 167 | return false; 168 | } 169 | 170 | public static bool ArePartOfSameMainAssets(Object asset1, Object asset2) 171 | { 172 | return AssetDatabase.GetAssetPath(asset1) == AssetDatabase.GetAssetPath(asset2); 173 | } 174 | 175 | public static string GetGuid(Object obj) 176 | { 177 | if (!IsAsset(obj)) throw new ArgumentException(); 178 | return AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(obj)); 179 | } 180 | 181 | /// DragAndDropHandler example. Not necessary if drag has correctly set asset paths 182 | private static DragAndDropVisualMode OnDragDroppedToProjectTab(int dragInstanceId, string dropUponPath, bool perform) 183 | { 184 | if (!perform) return DragAndDropVisualMode.None; // Next Handler in order will handle this drag (Unity default) 185 | if (!AssetDatabase.IsValidFolder(dropUponPath)) return DragAndDropVisualMode.None; 186 | if (DragAndDrop.paths.Length == 0) return DragAndDropVisualMode.None; // Non-asset, e.g. making a prefab from scene object 187 | 188 | var dragData = DragAndDrop.GetGenericData(nameof(MyEditorWindow)); 189 | if (!(dragData is bool b && b)) 190 | return DragAndDropVisualMode.None; 191 | MoveAssets(dropUponPath, DragAndDrop.paths); 192 | var undoMethod = typeof(Undo).GetMethod("RegisterAssetsMoveUndo", BindingFlags.Static | BindingFlags.NonPublic); 193 | undoMethod.Invoke(null, new object[] { DragAndDrop.paths }); 194 | foreach (var oldPath in DragAndDrop.paths) 195 | { 196 | var assetName = Path.GetFileName(oldPath); 197 | var newPath = dropUponPath + "/" + assetName; 198 | AssetDatabase.MoveAsset(oldPath, newPath); 199 | } 200 | AssetDatabase.Refresh(); 201 | return DragAndDropVisualMode.Move; 202 | } 203 | 204 | public static void MoveAssets(string newPath, params string[] oldPaths) 205 | { 206 | if (!AssetDatabase.IsValidFolder(newPath)) 207 | { 208 | Debug.LogWarning($"Invalid destination folder: {newPath}"); 209 | return; 210 | } 211 | var undoMethod = typeof(Undo).GetMethod("RegisterAssetsMoveUndo", BindingFlags.Static | BindingFlags.NonPublic); 212 | undoMethod.Invoke(null, new object[] { oldPaths }); 213 | foreach (var oldPath in oldPaths) 214 | { 215 | var assetName = Path.GetFileName(oldPath); 216 | var newAssetPath = newPath + "/" + assetName; 217 | AssetDatabase.MoveAsset(oldPath, newAssetPath); 218 | } 219 | AssetDatabase.Refresh(); 220 | } 221 | 222 | /// 223 | /// Orders string paths in the same order as in Project Tab. Folders are first at the same level of depth 224 | /// 225 | public class TreeViewComparer : IComparer 226 | { 227 | public int Compare(string x, string y) 228 | { 229 | if (x == y) return 0; 230 | if (string.IsNullOrEmpty(x)) return 1; 231 | if (string.IsNullOrEmpty(y)) return -1; 232 | var xDir = Path.GetDirectoryName(x); 233 | var yDir = Path.GetDirectoryName(y); 234 | if (xDir == yDir) return x.CompareTo(y); 235 | if (yDir.StartsWith(xDir)) return 1; // yDir is subdirectory of xDir, x > y, x after y, yDir will be on top 236 | if (xDir.StartsWith(yDir)) return -1; 237 | return x.CompareTo(y); 238 | } 239 | } 240 | 241 | public class MyEditorWindow : EditorWindow 242 | { 243 | public Object hoverObject; 244 | 245 | protected void OnEscapeKey() 246 | { 247 | if (!docked) Close(); 248 | } 249 | } 250 | #endregion 251 | } 252 | -------------------------------------------------------------------------------- /Scripts/Editor/PrefabUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using System.Reflection; 5 | using System.Text.RegularExpressions; 6 | using UnityEditor; 7 | using UnityEditor.SceneManagement; 8 | using UnityEngine; 9 | using UnityEngine.SceneManagement; 10 | using Object = UnityEngine.Object; 11 | 12 | public static class PrefabUtilities 13 | { 14 | [MenuItem("GameObject/Prefab/RevertName", false, 49)] 15 | private static void RevertName() 16 | { 17 | Object[] selection = Selection.GetFiltered(typeof(GameObject), SelectionMode.Editable); 18 | foreach (var prefabInstance in selection) 19 | { 20 | var prefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(prefabInstance); 21 | if (prefabAsset != null) 22 | { 23 | Undo.RecordObject(prefabInstance, "Revert prefab instance name"); 24 | prefabInstance.name = prefabAsset.name; 25 | PrefabUtility.RecordPrefabInstancePropertyModifications(prefabInstance); 26 | } 27 | } 28 | } 29 | 30 | [MenuItem("GameObject/Prefab/RevertName", true, 49)] 31 | private static bool RevertNameValidation() 32 | { 33 | Object[] selection = Selection.GetFiltered(typeof(GameObject), SelectionMode.Editable); 34 | bool valid = true; 35 | foreach (var prefabInstance in selection) 36 | { 37 | if (!PrefabUtility.IsPartOfNonAssetPrefabInstance(prefabInstance)) valid = false; 38 | break; 39 | } 40 | return valid; 41 | } 42 | 43 | [MenuItem("Assets/Prefab/PrefaPropagate Name/Preserve numbering", false)] 44 | private static void PropagateNamePreserveNumbering() 45 | { 46 | PropagateName(true); 47 | } 48 | 49 | [MenuItem("Assets/Prefab/Propagate Name/Also remove numbering", false)] 50 | private static void PropagateNameRemoveNumbering() 51 | { 52 | PropagateName(false); 53 | } 54 | 55 | [MenuItem("Assets/Prefab/Propagate Name/Preserve numbering", true)] 56 | [MenuItem("Assets/Prefab/Propagate Name/Also remove numbering", true)] 57 | private static bool PropagateNameValidation() 58 | { 59 | if (Selection.assetGUIDs.Length == 0) return false; 60 | string guid = Selection.assetGUIDs[0]; 61 | string path = AssetDatabase.GUIDToAssetPath(guid); 62 | GameObject selectedPrefabAsset = AssetDatabase.LoadAssetAtPath(path); 63 | if (selectedPrefabAsset == null) return false; 64 | bool valid = false; 65 | valid = PrefabUtility.IsPartOfPrefabAsset(selectedPrefabAsset); 66 | if (!valid) valid = PrefabUtility.IsAnyPrefabInstanceRoot(selectedPrefabAsset); 67 | if (!valid) valid = PrefabUtility.IsPartOfModelPrefab(selectedPrefabAsset); 68 | return valid; 69 | } 70 | 71 | private static void PropagateName(bool preserveNumbering) 72 | { 73 | if (Application.isPlaying) return; 74 | if (Selection.assetGUIDs.Length > 0) 75 | { 76 | // Get first selected gameobject and its credentials 77 | string guid = Selection.assetGUIDs[0]; 78 | string path = AssetDatabase.GUIDToAssetPath(guid); 79 | var type = AssetDatabase.GetMainAssetTypeAtPath(path); 80 | GameObject selectedPrefabAsset = AssetDatabase.LoadAssetAtPath(path); 81 | if (type != typeof(GameObject)) return; 82 | 83 | Regex regex = new Regex(@"(\([0-9]+\))$"); 84 | 85 | // Check all scenes 86 | 87 | // Get all existing scenes in folder Assets/Scenes 88 | string[] guids = AssetDatabase.FindAssets("t:scene", new[] { "Assets" }); 89 | if (guids?.Length == 0) return; 90 | List scenePaths = guids.Select(s => AssetDatabase.GUIDToAssetPath(s)) 91 | .Where(s => !s.StartsWith("Assets/Plugins/")).ToList(); 92 | 93 | // Save currently open and active scene 94 | string currentScene = EditorSceneManager.GetActiveScene().path; //scenePaths = new List() { currentScene }; 95 | 96 | foreach (string scenePath in scenePaths) 97 | { 98 | Scene scene; 99 | if (scenePath != currentScene) 100 | { 101 | scene = EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Additive); 102 | } 103 | else 104 | { 105 | scene = EditorSceneManager.GetActiveScene(); 106 | } 107 | EditorUtility.DisplayProgressBar( 108 | "Renaming in scenes", scene.name, (float)scenePaths.IndexOf(scenePath) / scenePaths.Count); 109 | var sceneGameObjects = scene.GetRootGameObjects() 110 | .SelectMany(x => x.GetComponentsInChildren(true)).Select(x => x.gameObject); 111 | 112 | // Find all scene gameobjects that are instances of a prefab 113 | List scenePrefabInstances = new List(); 114 | foreach (GameObject go in sceneGameObjects) 115 | { 116 | if (PrefabUtility.IsAnyPrefabInstanceRoot(go)) 117 | { 118 | scenePrefabInstances.Add(go); 119 | } 120 | } 121 | 122 | bool sceneChanged = false; 123 | // Get prefab assets of prefab instances and change the name 124 | foreach (GameObject prefabInstance in scenePrefabInstances) 125 | { 126 | GameObject prefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(prefabInstance); 127 | string oldName = prefabInstance.name; 128 | if (prefabAsset && prefabAsset == selectedPrefabAsset) 129 | { 130 | if (preserveNumbering) 131 | { 132 | Match match = regex.Match(prefabInstance.name); // Match is the ordinal number 133 | prefabInstance.name = prefabAsset.name + (match.Success ? (" " + match.Value) : ""); 134 | } 135 | else prefabInstance.name = prefabAsset.name; 136 | } 137 | if (oldName != prefabInstance.name) sceneChanged = true; 138 | } 139 | 140 | if (sceneChanged) 141 | { 142 | EditorSceneManager.MarkSceneDirty(scene); 143 | if (scenePath != currentScene) 144 | { 145 | EditorSceneManager.SaveScene(scene); 146 | } 147 | } 148 | EditorSceneManager.UnloadSceneAsync(scene); 149 | } 150 | 151 | // Check all prefab assets 152 | List prefabGuids = AssetDatabase.FindAssets("t:prefab").ToList(); 153 | foreach (string prefabGuid in prefabGuids) 154 | { 155 | string prefabPath = AssetDatabase.GUIDToAssetPath(prefabGuid); 156 | if (prefabPath == AssetDatabase.GetAssetPath(selectedPrefabAsset)) continue; 157 | 158 | GameObject prefabAsset = AssetDatabase.LoadAssetAtPath(prefabPath); 159 | 160 | EditorUtility.DisplayProgressBar( 161 | "Renaming in prefabs", 162 | prefabAsset.name, (float)prefabGuids.IndexOf(prefabGuid) / prefabGuids.Count); 163 | 164 | var prefabAssetGameObjects = prefabAsset.GetComponentsInChildren(true).Select(x => x.gameObject); 165 | 166 | // Find all scene gameobjects that are instances of a prefab 167 | List nestedPrefabInstances = new List(); 168 | foreach (GameObject go in prefabAssetGameObjects) 169 | { 170 | if (PrefabUtility.IsAnyPrefabInstanceRoot(go)) 171 | { 172 | nestedPrefabInstances.Add(go); 173 | } 174 | } 175 | 176 | bool prefabChanged = false; 177 | // Get prefab assets of prefab instances and change the name 178 | foreach (GameObject nestedPrefabInstance in nestedPrefabInstances) 179 | { 180 | GameObject nestedPrefabAsset = PrefabUtility.GetCorrespondingObjectFromSource(nestedPrefabInstance); 181 | string oldName = nestedPrefabInstance.name; 182 | if (nestedPrefabAsset && nestedPrefabAsset == selectedPrefabAsset) 183 | { 184 | if (preserveNumbering) 185 | { 186 | Match match = regex.Match(nestedPrefabInstance.name); // Match is the ordinal number 187 | nestedPrefabInstance.name = nestedPrefabAsset.name + (match.Success ? (" " + match.Value) : ""); 188 | } 189 | else nestedPrefabInstance.name = nestedPrefabAsset.name; 190 | } 191 | if (oldName != nestedPrefabInstance.name) prefabChanged = true; 192 | } 193 | 194 | if (prefabChanged) 195 | { 196 | EditorUtility.SetDirty(prefabAsset); 197 | AssetDatabase.ForceReserializeAssets(new List { prefabPath }); 198 | AssetDatabase.SaveAssetIfDirty(prefabAsset); 199 | } 200 | } 201 | 202 | EditorUtility.ClearProgressBar(); 203 | } 204 | } 205 | 206 | /// Removes unused overrides from prefab assets and their nested prefabs (not scene instances) 207 | [MenuItem("Assets/Prefab/Remove Unused Prefab Overrides", false)] 208 | private static void RemoveUnusedPrefabOverrides() 209 | { 210 | var classType = typeof(PrefabUtility); 211 | var infoType = typeof(PrefabUtility).Assembly.GetType("UnityEditor.PrefabUtility+InstanceOverridesInfo"); 212 | 213 | var getInfoMethod = 214 | classType.GetMethod("GetPrefabInstanceOverridesInfo", BindingFlags.Static | BindingFlags.NonPublic, null, 215 | new Type[] { typeof(GameObject) }, null); 216 | var removeOverridesMethod = 217 | classType.GetMethod("RemovePrefabInstanceUnusedOverrides", BindingFlags.Static | BindingFlags.NonPublic, null, 218 | new Type[] { infoType }, null); 219 | 220 | var undoGroup = Undo.GetCurrentGroup(); 221 | string[] guids = AssetDatabase.FindAssets("t:Prefab", new string[] { "Assets" }); 222 | foreach (var guid in guids) 223 | { 224 | var go = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(guid)); 225 | foreach (var t in go.GetComponentsInChildren(true)) 226 | { 227 | var g = t.gameObject; 228 | if (PrefabUtility.IsAnyPrefabInstanceRoot(g)) 229 | { 230 | var infos = getInfoMethod.Invoke(null, new[] { g }); 231 | if (infos != null) removeOverridesMethod.Invoke(null, new[] { infos }); 232 | } 233 | } 234 | } 235 | AssetDatabase.SaveAssets(); 236 | Undo.CollapseUndoOperations(undoGroup); 237 | } 238 | } 239 | -------------------------------------------------------------------------------- /Scripts/Editor/AssetUtilities/AssetDependencies.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using static EditorHelper; 8 | using static MyGUI; 9 | using Object = UnityEngine.Object; 10 | 11 | public class AssetDependencies : MyEditorWindow, IHasCustomMenu 12 | { 13 | private const int rowHeight = objectRowHeight; 14 | private static class Styles 15 | { 16 | public static GUIStyle foldoutStyle = new GUIStyle(); 17 | static Styles() 18 | { 19 | foldoutStyle = new GUIStyle(EditorStyles.miniPullDown); 20 | foldoutStyle.alignment = TextAnchor.MiddleLeft; 21 | foldoutStyle.padding = new RectOffset(19, 0, 0, 0); 22 | } 23 | } 24 | 25 | private static TreeViewComparer treeViewComparer; 26 | 27 | private bool initialized; 28 | private bool adjustSize; 29 | private Vector2 scroll = Vector2.zero; 30 | private float scrollViewRectHeight = 100; 31 | 32 | private bool showSelected = true; 33 | private bool showSameName = true; // name without file extension 34 | private bool isContainsName = false; // name without file extension 35 | private bool showUses = true; 36 | private bool isRecursive = false; 37 | private bool showUsedBy = true; 38 | private bool searchInScene = true; 39 | private bool showPackages = false; 40 | private bool isPackageRecursive = false; 41 | private bool searchAgain = true; 42 | 43 | private List selectedPaths = new(); 44 | 45 | private List selected = new(); 46 | private List sameName = new(); 47 | private List uses = new(); 48 | private List usedBy = new(); 49 | private List packagesUses = new(); 50 | private List shownItems = new List(); 51 | private int lastSelectedIndex = -1; 52 | 53 | [MenuItem("Window/Asset Dependencies _#F11")] 54 | private static void CreateWindow() 55 | { 56 | AssetDependencies window; 57 | window = CreateWindow("Asset Dependencies"); 58 | window.minSize = new Vector2(100, rowHeight + 1); 59 | 60 | window.Select(); 61 | window.SetShownItems(); 62 | window.Show(); 63 | } 64 | 65 | public virtual void AddItemsToMenu(GenericMenu menu) 66 | { 67 | menu.AddItem(EditorGUIUtility.TrTextContent("Test"), false, Test); 68 | } 69 | 70 | private void Select() 71 | { 72 | selected.Clear(); 73 | var newSelectedPaths = Selection.assetGUIDs.Select(x => AssetDatabase.GUIDToAssetPath(x)); 74 | newSelectedPaths = newSelectedPaths.OrderBy(x => x, treeViewComparer); 75 | 76 | // Prefab instances in Hierarchy. ExludePrefab does not exclude instances of prefabs, only assets. 77 | var selectedHierarchy = Selection.GetTransforms(SelectionMode.Unfiltered | SelectionMode.ExcludePrefab) 78 | .Select(x => x.gameObject); 79 | selectedHierarchy = selectedHierarchy.Where(x => PrefabUtility.IsAnyPrefabInstanceRoot(x)); 80 | newSelectedPaths = newSelectedPaths.Concat( 81 | selectedHierarchy.Select(x => PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(x))); 82 | 83 | selectedPaths = newSelectedPaths.ToList(); 84 | selected = newSelectedPaths.Select(x => AssetDatabase.LoadMainAssetAtPath(x)).ToList(); 85 | lastSelectedIndex = selected.Count - 1; 86 | searchAgain = true; 87 | } 88 | 89 | private async void SetShownItems() 90 | { 91 | sameName.Clear(); 92 | uses.Clear(); 93 | // usedBy are async and cached 94 | packagesUses.Clear(); 95 | shownItems.Clear(); 96 | shownItems.AddRange(selected); 97 | 98 | if (showSameName) 99 | { 100 | var names = selectedPaths.Select(x => Path.GetFileNameWithoutExtension(x)); 101 | var sameNameGuids = names.SelectMany(x => AssetDatabase.FindAssets(x)).Distinct(); 102 | var sameNamePaths = sameNameGuids 103 | .Select(x => AssetDatabase.GUIDToAssetPath(x)); // This does Contains 104 | sameNamePaths = sameNamePaths.Where(x => !x.StartsWith("Packages") && !selectedPaths.Contains(x)); 105 | 106 | if (!isContainsName) 107 | sameNamePaths = sameNamePaths 108 | .Where(x => names.Contains(Path.GetFileNameWithoutExtension(x))); 109 | sameNamePaths = sameNamePaths.OrderBy(x => x, treeViewComparer).ToList(); 110 | sameName = sameNamePaths.Select(x => AssetDatabase.LoadMainAssetAtPath(x)).ToList(); 111 | shownItems.AddRange(sameName); 112 | } 113 | 114 | if (showUses) 115 | { 116 | var usesPaths = AssetDatabase.GetDependencies(selectedPaths.ToArray(), isRecursive); 117 | usesPaths = usesPaths.Where(x => !selectedPaths.Contains(x)).ToArray(); 118 | usesPaths = usesPaths.Where(x => !x.StartsWith("Packages")) 119 | .OrderBy(x => x, treeViewComparer).ToArray(); 120 | uses = usesPaths.Select(x => AssetDatabase.LoadMainAssetAtPath(x)).ToList(); 121 | shownItems.AddRange(uses); 122 | } 123 | 124 | if (showUsedBy) 125 | { 126 | if (searchAgain) 127 | { 128 | usedBy.Clear(); 129 | var usedByAll = new List(); 130 | foreach (var sel in selected) 131 | usedByAll.AddRange(await FindAssetUsages.FindObjectUsageAsync(sel, true)); 132 | searchAgain = false; 133 | usedBy = usedByAll.Where(x => IsAsset(x)) 134 | .OrderBy(x => AssetDatabase.GetAssetPath(x), treeViewComparer).ToList(); 135 | 136 | if (searchInScene) 137 | usedBy.AddRange(usedByAll.Where(x => !IsAsset(x))); 138 | 139 | usedBy = usedBy.Distinct().ToList(); 140 | adjustSize = true; 141 | Repaint(); 142 | } 143 | shownItems.AddRange(usedBy); 144 | } 145 | 146 | if (showPackages) 147 | { 148 | var packagesUsesPaths = AssetDatabase.GetDependencies(selectedPaths.ToArray(), isPackageRecursive); 149 | packagesUsesPaths = packagesUsesPaths.Where(x => !selectedPaths.Contains(x)).ToArray(); 150 | packagesUsesPaths = packagesUsesPaths.Where(x => x.StartsWith("Packages")) 151 | .OrderBy(x => x, treeViewComparer).ToArray(); 152 | packagesUses = packagesUsesPaths.Select(x => AssetDatabase.LoadMainAssetAtPath(x)).ToList(); 153 | shownItems.AddRange(packagesUses); 154 | } 155 | } 156 | 157 | private void Test() 158 | { 159 | 160 | } 161 | 162 | private void OnEnable() 163 | { 164 | wantsMouseEnterLeaveWindow = true; 165 | wantsMouseMove = true; 166 | } 167 | 168 | private void OnGUI() 169 | { 170 | var ev = Event.current; //Debug.Log(ev.type); 171 | var height = position.height; 172 | var columnWidth = position.width; 173 | float xPos = 0, yPos = 0; 174 | bool isAnyHover = false; 175 | 176 | if (ev.type == EventType.MouseMove) Repaint(); 177 | if (ev.type == EventType.KeyDown) KeyboardNavigation(ev, ref lastSelectedIndex, shownItems); 178 | 179 | var scrollRectHeight = height; 180 | var scrollRectWidth = columnWidth; 181 | if (scrollViewRectHeight > scrollRectHeight) columnWidth -= 13; // Vertical ScrollBar is visible 182 | scroll = GUI.BeginScrollView(new Rect(xPos, yPos, scrollRectWidth, scrollRectHeight), scroll, new Rect(0, 0, columnWidth, scrollViewRectHeight)); 183 | yPos = 0; 184 | float headerWidth = 100; float headerHeight = 16; 185 | 186 | ToggleHeader(new Rect(xPos, yPos, headerWidth, headerHeight), ref showSelected, "Selected"); 187 | GUIContent reselectContent = EditorGUIUtility.IconContent("Grid.Default@2x"); 188 | reselectContent.tooltip = "Reselect"; // Tooltip parameter in IconContent not working 189 | if (GUI.Button(new Rect(xPos + headerWidth, yPos, 20, headerHeight + 2), reselectContent)) 190 | { 191 | Select(); 192 | SetShownItems(); 193 | } 194 | yPos = 20; 195 | int i = 0; 196 | if (showSelected) 197 | { 198 | for (int j = 0; j < selected.Count; j++) 199 | { 200 | var obj = shownItems[i]; 201 | if (obj == null) continue; 202 | 203 | Rect rect = new Rect(xPos, yPos, columnWidth, rowHeight); 204 | var (isHover, _) = ObjectRow(rect, i, obj, shownItems, ref lastSelectedIndex); 205 | if (isHover) { isAnyHover = true; hoverObject = obj; } 206 | yPos += rowHeight; 207 | i++; 208 | } 209 | } 210 | else i = selected.Count; 211 | 212 | ToggleHeader(new Rect(xPos, yPos, headerWidth, headerHeight), ref showSameName, "SameName"); 213 | AdditionalToggle(new Rect(xPos + headerWidth, yPos, 16, headerHeight + 2), ref isContainsName, "Contains"); 214 | 215 | yPos += 20; 216 | if (showSameName) 217 | { 218 | for (int j = 0; j < sameName.Count; j++) 219 | { 220 | var obj = shownItems[i]; 221 | if (obj == null) continue; 222 | 223 | string pingButtonContent = uses.Contains(shownItems[i]) ? "U" : ""; 224 | pingButtonContent = usedBy.Contains(shownItems[i]) ? "I" : "" + pingButtonContent; 225 | Rect rect = new Rect(xPos, yPos, columnWidth, rowHeight); 226 | var (isHover, _) = ObjectRow(rect, i, obj, shownItems, ref lastSelectedIndex, pingButtonContent); 227 | if (isHover) { isAnyHover = true; hoverObject = obj; } 228 | yPos += rowHeight; 229 | i++; 230 | } 231 | } 232 | 233 | ToggleHeader(new Rect(xPos, yPos, headerWidth, headerHeight), ref showUses, "Uses"); 234 | AdditionalToggle(new Rect(xPos + headerWidth, yPos, 16, headerHeight + 2), ref isRecursive, "Recursive"); 235 | yPos += 20; 236 | if (showUses) 237 | { 238 | for (int j = 0; j < uses.Count; j++) 239 | { 240 | var obj = shownItems[i]; 241 | if (obj == null) continue; 242 | 243 | Rect rect = new Rect(xPos, yPos, columnWidth, rowHeight); 244 | var (isHover, _) = ObjectRow(rect, i, obj, shownItems, ref lastSelectedIndex); 245 | if (isHover) { isAnyHover = true; hoverObject = obj; } 246 | yPos += rowHeight; 247 | i++; 248 | } 249 | } 250 | 251 | ToggleHeader(new Rect(xPos, yPos, headerWidth, headerHeight), ref showUsedBy, "Is Used By"); 252 | AdditionalToggle( 253 | new Rect(xPos + headerWidth, yPos, 16, headerHeight + 2), ref searchInScene, "Search in Scene", true); 254 | GUIContent searchContent = EditorGUIUtility.IconContent("Search Icon"); 255 | if (GUI.Button(new Rect(xPos + headerWidth + 16, yPos, 20, headerHeight + 2), searchContent)) 256 | { 257 | searchAgain = true; 258 | SetShownItems(); 259 | } 260 | yPos += 20; 261 | if (showUsedBy) 262 | { 263 | for (int j = 0; j < usedBy.Count; j++) 264 | { 265 | var obj = shownItems[i]; 266 | if (obj == null) continue; 267 | 268 | Rect rect = new Rect(xPos, yPos, columnWidth, rowHeight); 269 | var (isHover, _) = ObjectRow(rect, i, obj, shownItems, ref lastSelectedIndex); 270 | if (isHover) { isAnyHover = true; hoverObject = obj; } 271 | yPos += rowHeight; 272 | i++; 273 | } 274 | } 275 | 276 | ToggleHeader(new Rect(xPos, yPos, headerWidth, headerHeight), ref showPackages, "Packages"); 277 | AdditionalToggle(new Rect(xPos + headerWidth, yPos, 16, headerHeight + 2), ref isPackageRecursive, "Recursive"); 278 | yPos += 20; 279 | if (showPackages) 280 | { 281 | for (int j = 0; j < packagesUses.Count; j++) 282 | { 283 | var obj = shownItems[i]; 284 | if (obj == null) continue; 285 | 286 | Rect rect = new Rect(xPos, yPos, columnWidth, rowHeight); 287 | var (isHover, _) = ObjectRow(rect, i, obj, shownItems, ref lastSelectedIndex); 288 | if (isHover) { isAnyHover = true; hoverObject = obj; } 289 | yPos += rowHeight; 290 | i++; 291 | } 292 | } 293 | 294 | GUI.EndScrollView(); 295 | if (!isAnyHover) hoverObject = null; 296 | 297 | scrollViewRectHeight = yPos; 298 | if (!docked && (!initialized || adjustSize)) 299 | { 300 | float windowHeight = Mathf.Min(yPos, 600f); 301 | if (adjustSize) windowHeight = Mathf.Max(windowHeight, position.height); // Enlarge only 302 | position = new Rect(position.position, 303 | new Vector2(position.width, windowHeight)); 304 | initialized = true; adjustSize = false; 305 | } 306 | } 307 | 308 | #region Drawing 309 | public void ToggleHeader(Rect rect, ref bool selected, string text) 310 | { 311 | var oldBackgroundColor = GUI.backgroundColor; 312 | if (selected) GUI.backgroundColor = Color.white * 0.3f; 313 | EditorGUI.BeginChangeCheck(); 314 | selected = GUI.Toggle(rect, selected, text, Styles.foldoutStyle); 315 | if (EditorGUI.EndChangeCheck()) 316 | { 317 | SetShownItems(); 318 | adjustSize = true; 319 | } 320 | GUI.backgroundColor = oldBackgroundColor; 321 | } 322 | 323 | private void AdditionalToggle(Rect rect, ref bool value, string tooltip, bool searchAgain = false) 324 | { 325 | EditorGUI.BeginChangeCheck(); 326 | value = GUI.Toggle(rect, value, new GUIContent("", tooltip)); 327 | if (EditorGUI.EndChangeCheck()) 328 | { 329 | if (searchAgain) this.searchAgain = true; 330 | SetShownItems(); 331 | adjustSize = true; 332 | } 333 | } 334 | #endregion 335 | } -------------------------------------------------------------------------------- /Scripts/Editor/Common/MyGUI.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEngine; 6 | using static EditorHelper; 7 | using Object = UnityEngine.Object; 8 | 9 | public static class MyGUI 10 | { 11 | public const int objectRowHeight = 16; 12 | public static class Styles 13 | { 14 | public static GUIStyle insertion = new("TV Insertion"); 15 | public static GUIStyle lineStyle = new("TV Line"); 16 | public static GUIStyle selectionStyle = new("TV Selection"); 17 | public static GUIStyle pingButtonStyle = new GUIStyle(GUI.skin.button); 18 | 19 | static Styles() 20 | { 21 | lineStyle.alignment = TextAnchor.MiddleLeft; 22 | lineStyle.padding.right += objectRowHeight; 23 | pingButtonStyle.padding = new RectOffset(2, 0, 0, 1); 24 | pingButtonStyle.alignment = TextAnchor.MiddleCenter; 25 | } 26 | } 27 | 28 | private static bool wasDoubleClick; 29 | 30 | public static (bool isHovered, bool isShortRectHovered, bool pingButtonClicked) 31 | DrawObjectRow(Rect rect, Object obj, bool isSelected, bool isPinned, string pingButtonContent = null) 32 | { 33 | var ev = Event.current; 34 | 35 | Rect shortRect = new Rect(rect.x, rect.y, rect.width - rect.height, rect.height); 36 | Rect pingButtonRect = new Rect(shortRect.xMax, shortRect.yMax - shortRect.height, shortRect.height, shortRect.height); 37 | bool isHovered = rect.Contains(ev.mousePosition); 38 | bool isShortRectHovered = shortRect.Contains(ev.mousePosition); 39 | 40 | if (ev.type == EventType.Repaint) 41 | { 42 | int height = (int)rect.height; 43 | Color oldBackGroundColor = GUI.backgroundColor; 44 | Color oldColor = GUI.contentColor; 45 | Vector2 oldIconSize = EditorGUIUtility.GetIconSize(); 46 | EditorGUIUtility.SetIconSize(new Vector2(height, height)); 47 | bool isDragged = DragAndDrop.objectReferences.Length == 1 && DragAndDrop.objectReferences.Contains(obj); 48 | 49 | if (isHovered && isSelected) GUI.backgroundColor = new Color(0.9f, 0.9f, 0.9f); 50 | if (isSelected) Styles.selectionStyle.Draw(rect, false, false, true, true); 51 | if ((isHovered || isDragged) && !isSelected) Styles.selectionStyle.Draw(rect, false, false, false, false); 52 | 53 | var style = Styles.lineStyle; 54 | var oldPadding = style.padding.right; 55 | 56 | GUIContent content = EditorGUIUtility.ObjectContent(obj, obj.GetType()); 57 | bool isAddedGameObject = false; 58 | if (IsNonAssetGameObject(obj)) 59 | { 60 | var go = (GameObject)obj; 61 | if (!go.activeInHierarchy) GUI.contentColor = Color.white * 0.694f; 62 | if (!PrefabUtility.IsAnyPrefabInstanceRoot(go)) 63 | content.image = EditorGUIUtility.IconContent("GameObject Icon").image; 64 | if (PrefabUtility.IsAddedGameObjectOverride(go)) isAddedGameObject = true; 65 | } 66 | if (isPinned) style.padding.right += height; 67 | style.Draw(rect, content, false, false, isSelected, true); 68 | GUI.contentColor = oldColor; 69 | if (isPinned) 70 | { 71 | var pinnedIconContent = EditorGUIUtility.IconContent("Favorite On Icon"); 72 | Rect pinnedIconRect = new Rect(rect.xMax - 2 * height, rect.yMax - height, height, height); 73 | EditorStyles.label.Draw(pinnedIconRect, pinnedIconContent, false, false, true, true); 74 | } 75 | if (isAddedGameObject) 76 | { 77 | var iconContent = EditorGUIUtility.IconContent("PrefabOverlayAdded Icon"); 78 | Rect iconRect = new Rect(rect.xMin, rect.yMin, height + 5, height); 79 | EditorStyles.label.Draw(iconRect, iconContent, false, false, true, true); 80 | } 81 | 82 | style.padding.right = oldPadding; 83 | EditorGUIUtility.SetIconSize(oldIconSize); 84 | GUI.backgroundColor = oldBackGroundColor; 85 | } 86 | bool pingButtonClicked = DrawPingButton(pingButtonRect, obj, pingButtonContent); 87 | return (isHovered, isShortRectHovered, pingButtonClicked); 88 | } 89 | 90 | public static bool DrawPingButton(Rect rect, Object obj, string content = null) 91 | { 92 | int height = (int)rect.height; 93 | Color oldBackgroundColor = GUI.backgroundColor; 94 | Vector2 oldIconSize = EditorGUIUtility.GetIconSize(); 95 | EditorGUIUtility.SetIconSize(new Vector2(height / 2 + 3, height / 2 + 3)); 96 | 97 | var pingButtonContent = EditorGUIUtility.IconContent("HoloLensInputModule Icon"); 98 | if (!string.IsNullOrEmpty(content)) 99 | pingButtonContent = new GUIContent(content); 100 | pingButtonContent.tooltip = AssetDatabase.GetAssetPath(obj); 101 | 102 | if (IsComponent(obj)) GUI.backgroundColor = new Color(1f, 1.5f, 1f); 103 | if (!IsAsset(obj)) pingButtonContent = EditorGUIUtility.IconContent("GameObject Icon"); 104 | 105 | bool clicked = GUI.Button(rect, pingButtonContent, Styles.pingButtonStyle); 106 | 107 | EditorGUIUtility.SetIconSize(oldIconSize); 108 | GUI.backgroundColor = oldBackgroundColor; 109 | return clicked; 110 | } 111 | 112 | private static void DrawDragInsertionLine(Rect fullRect) 113 | { 114 | Rect lineRect = new Rect(fullRect.x, fullRect.y - 4, fullRect.width, 3); 115 | GUI.Label(lineRect, GUIContent.none, Styles.insertion); 116 | } 117 | 118 | public static void KeyboardNavigation(Event ev, ref int lastSelectedIndex, List shownItems, 119 | Action deleteKey = null, Action enterKey = null, Action escapeKey = null) 120 | { 121 | if (ev.keyCode == KeyCode.DownArrow) 122 | { 123 | lastSelectedIndex = Mod(lastSelectedIndex + 1, shownItems.Count); 124 | Selection.objects = new Object[] { shownItems[lastSelectedIndex] }; 125 | ev.Use(); 126 | } 127 | else if (ev.keyCode == KeyCode.UpArrow) 128 | { 129 | lastSelectedIndex = Mod(lastSelectedIndex - 1, shownItems.Count); 130 | Selection.objects = new Object[] { shownItems[lastSelectedIndex] }; 131 | ev.Use(); 132 | } 133 | else if (ev.keyCode == KeyCode.Return || ev.keyCode == KeyCode.KeypadEnter) 134 | { 135 | var objs = shownItems.Where(x => Selection.objects.Contains(x)); 136 | foreach (var obj in objs) 137 | OpenObject(obj); 138 | enterKey?.Invoke(); 139 | ev.Use(); 140 | } 141 | else if (ev.keyCode == KeyCode.Delete) 142 | { 143 | deleteKey?.Invoke(); 144 | ev.Use(); 145 | } 146 | else if (ev.keyCode == KeyCode.Escape) 147 | { 148 | escapeKey?.Invoke(); 149 | ev.Use(); 150 | } 151 | } 152 | 153 | public static (bool isHovered, bool isShortRectHovered) 154 | ObjectRow(Rect rect, int i, Object obj, List shownItems, ref int lastSelectedIndex, 155 | string pingButtonContent = null, bool isPinned = false, Action doubleClick = null, Action middleClick = null, 156 | Action pingButtonMiddleClick = null, Action dragStarted = null, Action dragPerformed = null) 157 | { 158 | var ev = Event.current; 159 | bool isSelected = Selection.objects.Contains(obj); 160 | 161 | var buttonResult = DrawObjectRow(rect, obj, isSelected, isPinned, pingButtonContent); 162 | if (buttonResult.pingButtonClicked) 163 | { 164 | if (Event.current.button == 0) 165 | PingButtonLeftClick(obj); 166 | else if (Event.current.button == 1) 167 | PingButtonRightClick(obj); 168 | else if (Event.current.button == 2) 169 | PingButtonMiddleClick(obj, pingButtonMiddleClick); 170 | } 171 | 172 | if (buttonResult.isShortRectHovered) 173 | { 174 | if (ev.type == EventType.MouseUp && ev.button == 0 && ev.clickCount == 1) // Select on MouseUp 175 | { 176 | if (!wasDoubleClick) 177 | LeftMouseUp(obj, isSelected, i, ref lastSelectedIndex); 178 | wasDoubleClick = false; 179 | } 180 | else if (ev.type == EventType.MouseDown && ev.button == 0 && ev.clickCount == 2) 181 | { 182 | DoubleClick(obj); 183 | wasDoubleClick = true; 184 | } 185 | else if (ev.type == EventType.MouseDown && ev.button == 1) 186 | { 187 | RightClick(obj, i, ref lastSelectedIndex); 188 | 189 | } 190 | else if (ev.type == EventType.ContextClick) 191 | { 192 | ContextClick(new Rect(ev.mousePosition.x, ev.mousePosition.y, 0, 0), obj); 193 | } 194 | else if (ev.type == EventType.MouseDown && ev.button == 2) 195 | { 196 | middleClick?.Invoke(); 197 | } 198 | // Drag 199 | else if (ev.type == EventType.MouseDrag && ev.button == 0 && // Start dragging this asset 200 | DragAndDrop.visualMode == DragAndDropVisualMode.None) 201 | { 202 | DragAndDrop.PrepareStartDrag(); 203 | DragAndDrop.SetGenericData(nameof(MyEditorWindow), true); 204 | DragAndDrop.visualMode = DragAndDropVisualMode.Move; 205 | if (isSelected) 206 | DragAndDrop.objectReferences = shownItems.Where(x => Selection.objects.Contains(x)) 207 | .ToArray(); 208 | else DragAndDrop.objectReferences = new Object[] { obj }; 209 | DragAndDrop.StartDrag("MyEditorWindow Drag"); 210 | ev.Use(); 211 | dragStarted?.Invoke(); 212 | } 213 | else if (ev.type == EventType.DragUpdated && ev.button == 0) // Update drag 214 | { 215 | DragAndDrop.visualMode = DragAndDropVisualMode.Generic; 216 | GUI.Label(rect, GUIContent.none, Styles.insertion); 217 | ev.Use(); 218 | } 219 | else if (ev.type == EventType.DragPerform && ev.button == 0) // Receive drag and drop 220 | { 221 | dragPerformed?.Invoke(); 222 | } 223 | // Draw insertion line 224 | if (isPinned && DragAndDrop.visualMode != DragAndDropVisualMode.None) 225 | { 226 | if (ev.modifiers != EventModifiers.Control) // Otherwise we are trying to move asset to folder 227 | DrawDragInsertionLine(rect); 228 | } 229 | } 230 | return (buttonResult.isHovered, buttonResult.isShortRectHovered); 231 | 232 | void LeftMouseUp(Object obj, bool isSelected, int i, ref int lastSelectedIndex) 233 | { 234 | lastSelectedIndex = i; 235 | var ev = Event.current; 236 | HandleSelection(true); 237 | ev.Use(); 238 | } 239 | 240 | void DoubleClick(Object obj) 241 | { 242 | OpenObject(obj); 243 | doubleClick?.Invoke(); 244 | ev.Use(); 245 | } 246 | 247 | // This is different event then context click, bot are executed, context after right click 248 | void RightClick(Object obj, int i, ref int lastSelectedIndex) 249 | { 250 | lastSelectedIndex = i; 251 | HandleSelection(false); 252 | ev.Use(); 253 | } 254 | 255 | void ContextClick(Rect rect, Object obj) 256 | { 257 | if (IsComponent(obj)) OpenObjectContextMenu(rect, obj); 258 | else if (IsAsset(obj)) EditorUtility.DisplayPopupMenu(rect, "Assets/", null); 259 | else if (IsNonAssetGameObject(obj)) 260 | { 261 | if (Selection.transforms.Length > 0) // Just to be sure it's really a HierarchyGameobject 262 | OpenHierarchyContextMenu(Selection.transforms[0].gameObject.GetInstanceID()); 263 | } 264 | } 265 | 266 | void PingButtonLeftClick(Object obj) 267 | { 268 | if (Event.current.modifiers == EventModifiers.Alt) 269 | { 270 | string path = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(obj); 271 | obj = AssetDatabase.LoadMainAssetAtPath(path); 272 | EditorGUIUtility.PingObject(obj); 273 | } 274 | else EditorGUIUtility.PingObject(obj); 275 | } 276 | 277 | void PingButtonRightClick(Object obj) 278 | { 279 | OpenPropertyEditor(obj); 280 | } 281 | 282 | void PingButtonMiddleClick(Object obj, Action pingButtonMiddleClick = null) 283 | { 284 | if (Event.current.modifiers == EventModifiers.Alt) 285 | Debug.Log($"{GlobalObjectId.GetGlobalObjectIdSlow(obj)} InstanceID: {obj.GetInstanceID()}"); 286 | else pingButtonMiddleClick?.Invoke(); 287 | } 288 | 289 | void HandleSelection(bool leftClick) 290 | { 291 | if (ev.modifiers == EventModifiers.Control) // Ctrl select 292 | { 293 | if (!isSelected) Selection.objects = Selection.objects.Append(obj).ToArray(); 294 | else if (leftClick) Selection.objects = Selection.objects.Where(x => x != obj).ToArray(); 295 | } 296 | else if (ev.modifiers == EventModifiers.Shift) // Shift select 297 | { 298 | int firstSelected = shownItems.FindIndex(x => Selection.objects.Contains(x)); 299 | if (firstSelected != -1) 300 | { 301 | int startIndex = Mathf.Min(firstSelected + 1, i); 302 | int count = Mathf.Abs(firstSelected - i); 303 | Selection.objects = Selection.objects. 304 | Concat(shownItems.GetRange(startIndex, count)).Distinct().ToArray(); 305 | } 306 | else Selection.objects = Selection.objects.Append(obj).ToArray(); 307 | } 308 | else if (leftClick || !isSelected) 309 | { 310 | Selection.activeObject = obj; // Ordinary select 311 | Selection.objects = new Object[] { obj }; 312 | } 313 | } 314 | } 315 | } 316 | -------------------------------------------------------------------------------- /Scripts/Editor/FileUtilities.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Diagnostics; 4 | using System.IO; 5 | using System.Linq; 6 | using System.Reflection; 7 | using System.Runtime.Serialization; 8 | using System.Text.RegularExpressions; 9 | using UnityEditor; 10 | using UnityEditor.Callbacks; 11 | using UnityEditorInternal; 12 | using UnityEngine; 13 | using Debug = UnityEngine.Debug; 14 | using Object = UnityEngine.Object; 15 | 16 | // Menu item shortcuts: % == ctrl, # == shift, & == alt, _ == no modifier, LEFT, RIGHT, UP, DOWN, F1..F12, HOME, END, PGUP, PGDN 17 | public class FileUtilities 18 | { 19 | [MenuItem("Assets/File/Copy GUID %#c")] 20 | public static void CopyGuid() 21 | { 22 | if (Selection.assetGUIDs.Length > 0) 23 | { 24 | string guid = Selection.assetGUIDs[0]; 25 | GUIUtility.systemCopyBuffer = guid; 26 | Debug.Log($"{AssetDatabase.GUIDToAssetPath(guid)} GUID copied to clipboard: {guid}"); 27 | } 28 | } 29 | 30 | //private static string VisualStudio2019Path = "C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/Common7/IDE/devenv.exe"; 31 | private static string VisualStudioPath = "C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/devenv.exe"; 32 | 33 | [MenuItem("Assets/File/Open as Textfile")] 34 | public static void OpenAsTextfile() 35 | { 36 | foreach (string guid in Selection.assetGUIDs) 37 | { 38 | OpenAsTextfile(AssetDatabase.GUIDToAssetPath(guid)); 39 | } 40 | } 41 | 42 | public static void OpenAsTextfile(string path) 43 | { 44 | ProcessStartInfo process = new ProcessStartInfo(VisualStudioPath, "/edit \"" + path + "\"") 45 | { 46 | RedirectStandardOutput = true, 47 | RedirectStandardError = true, 48 | CreateNoWindow = true, 49 | UseShellExecute = false 50 | }; 51 | Process.Start(process); 52 | } 53 | 54 | [MenuItem("Assets/File/Open Metafile")] 55 | public static void OpenMetafile() 56 | { 57 | foreach (string guid in Selection.assetGUIDs) 58 | { 59 | OpenMetafile(AssetDatabase.GUIDToAssetPath(guid)); 60 | } 61 | } 62 | 63 | public static void OpenMetafile(string path) 64 | { 65 | ProcessStartInfo process = new ProcessStartInfo(VisualStudioPath, "/edit \"" + path + ".meta\"") 66 | { 67 | RedirectStandardOutput = true, 68 | RedirectStandardError = true, 69 | CreateNoWindow = true, 70 | UseShellExecute = false 71 | }; 72 | Process.Start(process); 73 | } 74 | 75 | [MenuItem("Assets/File/Serialize Class")] 76 | public static void SerializeClass() 77 | { 78 | if (Selection.assetGUIDs.Length == 0) return; 79 | var path = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]); 80 | var loadedScript = AssetDatabase.LoadAssetAtPath(path); 81 | if (loadedScript == null) return; 82 | var type = loadedScript.GetClass(); 83 | var instance = FormatterServices.GetUninitializedObject(type); 84 | Debug.Log(JsonConvert.SerializeObject(instance)); 85 | } 86 | 87 | private static string GExtensionsPath = "C:/Program Files (x86)/GitExtensions/GitExtensions.exe"; 88 | 89 | [MenuItem("Assets/File/File History GE %&h")] 90 | public static void FileHistoryGitExtensions() 91 | { 92 | foreach (string guid in Selection.assetGUIDs) 93 | { 94 | string path = AssetDatabase.GUIDToAssetPath(guid); 95 | // Remove "Assets" at the end of Application.dataPath, because asset contains Assets or Packages at the beginning 96 | path = Application.dataPath.Substring(0, Application.dataPath.Length - 6) + path; 97 | Debug.Log(path); 98 | FileHistoryGitExtensions(path); 99 | } 100 | } 101 | 102 | [MenuItem("Assets/File/Meta File History GE #&h")] 103 | public static void MetaFileHistoryGitExtensions() 104 | { 105 | foreach (string guid in Selection.assetGUIDs) 106 | { 107 | string path = AssetDatabase.GUIDToAssetPath(guid); 108 | // Remove "Assets" at the end of Application.dataPath, because asset contains Assets or Packages at the beginning 109 | path = Application.dataPath.Substring(0, Application.dataPath.Length - 6) + path + ".meta"; 110 | Debug.Log(path); 111 | FileHistoryGitExtensions(path); 112 | } 113 | } 114 | 115 | public static void FileHistoryGitExtensions(string path) 116 | { 117 | ProcessStartInfo process = new ProcessStartInfo(GExtensionsPath, " filehistory \"" + path + "\"") 118 | { 119 | RedirectStandardOutput = true, 120 | RedirectStandardError = true, 121 | CreateNoWindow = true, 122 | UseShellExecute = false 123 | }; 124 | Process.Start(process); 125 | } 126 | 127 | private static string GIMPBinFolderPath = "C:/Program Files/GIMP 2/bin/"; 128 | private static string GIMPPath = ""; 129 | 130 | [MenuItem("Assets/File/Open In GIMP")] 131 | public static void OpenInGimp() 132 | { 133 | foreach (string guid in Selection.assetGUIDs) 134 | { 135 | OpenInGimp(AssetDatabase.GUIDToAssetPath(guid)); 136 | } 137 | } 138 | 139 | public static void OpenInGimp(string path) 140 | { 141 | if (string.IsNullOrEmpty(GIMPPath)) 142 | { 143 | GIMPPath = Directory.GetFiles(GIMPBinFolderPath, "*.exe").FirstOrDefault(x => Regex.IsMatch(x, @"gimp-[0-9]+")); 144 | if (string.IsNullOrEmpty(GIMPPath)) return; 145 | } 146 | ProcessStartInfo process = new ProcessStartInfo(GIMPPath, "\"" + path + "\"") 147 | { 148 | RedirectStandardOutput = true, 149 | RedirectStandardError = true, 150 | CreateNoWindow = true, 151 | UseShellExecute = false 152 | }; 153 | Process.Start(process); 154 | } 155 | 156 | private static string BlenderFolderPath = "C:/Program Files/Blender Foundation/"; 157 | private static string BlenderPath = ""; 158 | 159 | [MenuItem("Assets/File/Open FBX in Blender")] 160 | public static void OpenFBXInBlender() 161 | { 162 | foreach (string guid in Selection.assetGUIDs) 163 | { 164 | OpenFBXInBlender(AssetDatabase.GUIDToAssetPath(guid)); 165 | } 166 | } 167 | 168 | // Inspired by https://blog.kikicode.com/2018/12/double-click-fbx-files-to-import-to.html 169 | public static void OpenFBXInBlender(string path) 170 | { 171 | if (string.IsNullOrEmpty(BlenderPath)) 172 | { 173 | BlenderPath = Directory.GetDirectories(BlenderFolderPath).LastOrDefault() + "/blender.exe"; 174 | if (string.IsNullOrEmpty(BlenderPath)) return; 175 | } 176 | 177 | // r'pathstring' - the parameter r means literal string 178 | ProcessStartInfo process = new ProcessStartInfo(BlenderPath, " --python-expr \"import bpy; bpy.context.preferences.view.show_splash = False; bpy.ops.import_scene.fbx(filepath = r'" + path + "'); \"") 179 | { 180 | RedirectStandardOutput = true, 181 | RedirectStandardError = true, 182 | CreateNoWindow = true, 183 | UseShellExecute = false 184 | }; 185 | Process.Start(process); 186 | } 187 | 188 | private static string AudacityPath = "C:/Program Files/Audacity/Audacity.exe"; 189 | 190 | [MenuItem("Assets/File/Open AudioFile in Audacity")] 191 | public static void OpenInAudacity() 192 | { 193 | foreach (string guid in Selection.assetGUIDs) 194 | { 195 | OpenInAudacity(AssetDatabase.GUIDToAssetPath(guid)); 196 | } 197 | } 198 | 199 | public static void OpenInAudacity(string path) 200 | { 201 | ProcessStartInfo process = new ProcessStartInfo(AudacityPath, "\"" + path + "\"") 202 | { 203 | RedirectStandardOutput = true, 204 | RedirectStandardError = true, 205 | CreateNoWindow = true, 206 | UseShellExecute = false 207 | }; 208 | Process.Start(process); 209 | } 210 | 211 | private static string CygwinPath = "C:/cygwin64/bin/mintty.exe"; 212 | [MenuItem("Assets/File/Open Cygwin here")] 213 | public static void OpenCygwinHere() 214 | { 215 | foreach (string guid in Selection.assetGUIDs) 216 | { 217 | string path = AssetDatabase.GUIDToAssetPath(guid); 218 | if (!AssetDatabase.IsValidFolder(path)) 219 | { 220 | path = path.Substring(0, path.LastIndexOf('/') + 1); 221 | } 222 | path = "\"" + Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length) + path + "\""; 223 | UnityEngine.Debug.Log($"Opening Cygwin in: {path}"); 224 | ProcessStartInfo process = new ProcessStartInfo(CygwinPath, "/bin/sh -lc 'cd " + path + "; exec bash'") 225 | { 226 | RedirectStandardOutput = true, 227 | RedirectStandardError = true, 228 | CreateNoWindow = true, 229 | UseShellExecute = false 230 | }; 231 | Process.Start(process); 232 | } 233 | } 234 | 235 | [OnOpenAsset(0)] 236 | public static bool OnOpenWithModifiers(int instanceID, int line) 237 | { 238 | if (Event.current == null) return false; 239 | if (Event.current.modifiers == EventModifiers.None) return false; 240 | if (Event.current.modifiers == EventModifiers.Alt) 241 | { 242 | OpenAsTextfile(AssetDatabase.GetAssetPath(EditorUtility.InstanceIDToObject(instanceID))); 243 | return true; 244 | } 245 | else if (Event.current.modifiers == EventModifiers.Shift) 246 | { 247 | OpenMetafile(AssetDatabase.GetAssetPath(EditorUtility.InstanceIDToObject(instanceID))); 248 | return true; 249 | } 250 | else if (Event.current.modifiers == (EventModifiers.Alt | EventModifiers.Command)) // Command == Windows key 251 | { 252 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 253 | string assetPath = AssetDatabase.GetAssetPath(asset); 254 | EditorUtility.RevealInFinder(assetPath); 255 | return true; 256 | } 257 | else return false; 258 | } 259 | 260 | [OnOpenAsset(1)] 261 | public static bool OnOpenFolder(int instanceID, int line) 262 | { 263 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 264 | string assetPath = AssetDatabase.GetAssetPath(asset); 265 | if (AssetDatabase.IsValidFolder(assetPath)) 266 | { 267 | assetPath = "\"" + Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length) + assetPath + "\""; 268 | assetPath = assetPath.Replace('/', '\\'); 269 | ProcessStartInfo process = new ProcessStartInfo("explorer.exe", assetPath) 270 | { 271 | RedirectStandardOutput = true, 272 | RedirectStandardError = true, 273 | CreateNoWindow = true, 274 | UseShellExecute = false 275 | }; 276 | Process.Start(process); 277 | return true; 278 | } 279 | else return false; 280 | } 281 | 282 | [OnOpenAsset(2)] 283 | public static bool OnOpenFBX(int instanceID, int line) 284 | { 285 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 286 | string assetPath = AssetDatabase.GetAssetPath(asset); 287 | if (assetPath.EndsWith(".fbx", StringComparison.OrdinalIgnoreCase)) 288 | { 289 | OpenFBXInBlender(assetPath); 290 | return true; 291 | } 292 | else return false; 293 | } 294 | 295 | [OnOpenAsset(3)] 296 | public static bool OnOpenImage(int instanceID, int line) 297 | { 298 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 299 | string assetPath = AssetDatabase.GetAssetPath(asset); 300 | if (Regex.IsMatch(assetPath, @".*\.png$|.*\.jpg$|.*\.jpeg$|.*\.bmp$|.*\.tif$|.*\.psd$|.*\.psb$", RegexOptions.IgnoreCase)) 301 | { 302 | OpenInGimp(assetPath); 303 | return true; 304 | } 305 | else return false; 306 | } 307 | 308 | [OnOpenAsset(4)] 309 | public static bool OnOpenAudio(int instanceID, int line) 310 | { 311 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 312 | string assetPath = AssetDatabase.GetAssetPath(asset); 313 | if (Regex.IsMatch(assetPath, @".*\.wav$|.*\.flac$|.*\.ogg$|.*\.mp3$|.*\.mp4$|.*\.aiff$", RegexOptions.IgnoreCase)) 314 | { 315 | OpenInAudacity(assetPath); 316 | return true; 317 | } 318 | else return false; 319 | } 320 | 321 | [OnOpenAsset(5)] 322 | public static bool OnOpenText(int instanceID, int line) 323 | { 324 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 325 | string assetPath = AssetDatabase.GetAssetPath(asset); 326 | // Last expression of the regex is for files without '.' in the name == no file extension 327 | if (Regex.IsMatch(assetPath, @".*\.txt$|.*\.json$|.*\.md$|.*\.java$|.*\.mm$|^([^.]+)$", RegexOptions.IgnoreCase)) 328 | { 329 | OpenAsTextfile(assetPath); 330 | return true; 331 | } 332 | else return false; 333 | } 334 | 335 | // This should fold out/in folders on double click 336 | // Does not work well in two columns layout 337 | //[OnOpenAsset(1)] 338 | public static bool OnOpenFolder2(int instanceID, int line) 339 | { 340 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 341 | string assetPath = AssetDatabase.GetAssetPath(asset); 342 | if (AssetDatabase.IsValidFolder(assetPath)) 343 | { 344 | int[] expandedFolders = InternalEditorUtility.expandedProjectWindowItems; 345 | bool isExpanded = expandedFolders.Contains(instanceID); 346 | ExpandFolder(instanceID, !isExpanded); 347 | return true; 348 | } 349 | else return false; 350 | } 351 | 352 | public static void ExpandFolder(int instanceID, bool expand) 353 | { 354 | int[] expandedFolders = InternalEditorUtility.expandedProjectWindowItems; 355 | bool isExpanded = expandedFolders.Contains(instanceID); 356 | if (expand == isExpanded) return; 357 | 358 | var unityEditorAssembly = Assembly.GetAssembly(typeof(Editor)); 359 | var projectBrowserType = unityEditorAssembly.GetType("UnityEditor.ProjectBrowser"); 360 | var projectBrowsers = Resources.FindObjectsOfTypeAll(projectBrowserType); 361 | 362 | foreach (var p in projectBrowsers) 363 | { 364 | var treeViewControllerType = unityEditorAssembly.GetType("UnityEditor.IMGUI.Controls.TreeViewController"); 365 | FieldInfo treeViewControllerField = 366 | projectBrowserType.GetField("m_AssetTree", BindingFlags.Instance | BindingFlags.NonPublic); 367 | var treeViewController = treeViewControllerField.GetValue(p); 368 | // For two columns layout 369 | //if (treeViewController == null) 370 | //{ 371 | // treeViewControllerField = 372 | // projectBrowserType.GetField("m_FolderTree", BindingFlags.Instance | BindingFlags.NonPublic); 373 | // treeViewController = treeViewControllerField.GetValue(p); 374 | //} 375 | if (treeViewController == null) continue; 376 | var changeGoldingMethod = 377 | treeViewControllerType.GetMethod("ChangeFolding", BindingFlags.Instance | BindingFlags.NonPublic); 378 | changeGoldingMethod.Invoke(treeViewController, new object[] { new int[] { instanceID }, expand }); 379 | EditorWindow pw = (EditorWindow)p as EditorWindow; 380 | pw.Repaint(); 381 | } 382 | } 383 | } 384 | -------------------------------------------------------------------------------- /Scripts/Editor/AssetUtilities/AssetsHistory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.IO; 4 | using System.Linq; 5 | using UnityEditor; 6 | using UnityEditor.SceneManagement; 7 | using UnityEngine; 8 | using UnityEngine.SceneManagement; 9 | using static EditorHelper; 10 | using static MyGUI; 11 | using Object = UnityEngine.Object; 12 | 13 | public class AssetsHistory : MyEditorWindow, IHasCustomMenu 14 | { 15 | protected const int rowHeight = objectRowHeight; 16 | protected const int minColumnWidth = 150; 17 | protected virtual string prefId => PlayerSettings.companyName + "." + 18 | PlayerSettings.productName + ".EpsilonDelta.AssetsHistory."; 19 | 20 | protected List groupedHistory = new List(); 21 | protected List history = new List(); 22 | protected List pinned = new List(); 23 | private int limit = 10; 24 | private int lastSelectedIndex = -1; 25 | 26 | [MenuItem("Window/Assets History")] 27 | private static void CreateWindow() 28 | { 29 | AssetsHistory window; 30 | if (Resources.FindObjectsOfTypeAll().Any(x => x.GetType() == typeof(AssetsHistory))) 31 | window = GetWindow(typeof(AssetsHistory), false, "Assets History") as AssetsHistory; 32 | else window = CreateWindow("Assets History"); 33 | window.minSize = new Vector2(100, rowHeight + 1); 34 | window.Show(); 35 | } 36 | 37 | public virtual void AddItemsToMenu(GenericMenu menu) 38 | { 39 | menu.AddItem(EditorGUIUtility.TrTextContent("Test"), false, Test); 40 | menu.AddItem(EditorGUIUtility.TrTextContent("Clear All"), false, ClearAll); 41 | menu.AddItem(EditorGUIUtility.TrTextContent("Clear History"), false, ClearHistory); 42 | menu.AddItem(EditorGUIUtility.TrTextContent("Clear Pinned"), false, ClearPinned); 43 | } 44 | 45 | protected virtual void Test() 46 | { 47 | 48 | } 49 | 50 | protected virtual void Awake() 51 | { 52 | LoadHistoryFromEditorPrefs(); 53 | } 54 | 55 | protected virtual void OnEnable() 56 | { 57 | // This is received even if invisible 58 | Selection.selectionChanged -= SelectionChanged; 59 | Selection.selectionChanged += SelectionChanged; 60 | DragAndDrop.RemoveDropHandler(OnDragDroppedSceneHandler); 61 | DragAndDrop.AddDropHandler(OnDragDroppedSceneHandler); 62 | DragAndDrop.RemoveDropHandler(OnDragDroppedHierarchyHandler); 63 | DragAndDrop.AddDropHandler(OnDragDroppedHierarchyHandler); 64 | DragAndDrop.RemoveDropHandler(OnDragDroppedInspectorHandler); 65 | DragAndDrop.AddDropHandler(OnDragDroppedInspectorHandler); 66 | AssetImportHistory.assetImported -= AssetImported; 67 | AssetImportHistory.assetImported += AssetImported; 68 | EditorSceneManager.sceneOpened -= SceneOpened; 69 | EditorSceneManager.sceneOpened += SceneOpened; 70 | EditorApplication.quitting -= SaveHistoryToEditorPrefs; 71 | EditorApplication.quitting += SaveHistoryToEditorPrefs; 72 | 73 | wantsMouseEnterLeaveWindow = true; 74 | wantsMouseMove = true; 75 | 76 | LimitAndOrderHistory(); 77 | } 78 | 79 | protected virtual void OnDisable() 80 | { 81 | } 82 | 83 | protected virtual void OnDestroy() 84 | { 85 | SaveHistoryToEditorPrefs(); 86 | } 87 | 88 | // This is received only when window is visible 89 | private void OnSelectionChange() 90 | { 91 | Repaint(); 92 | } 93 | 94 | private void OnGUI() 95 | { 96 | var ev = Event.current; //Debug.Log(ev.type); 97 | var height = position.height; 98 | var width = position.width; 99 | int lines = Mathf.FloorToInt(height / rowHeight); 100 | int columns = Mathf.FloorToInt(width / minColumnWidth); 101 | if (columns <= 1) columns = 2; 102 | float columnWidth = width / columns; 103 | float xPos = 0, yPos = 0; 104 | bool shouldLimitAndOrderHistory = false; 105 | bool isAnyShortRectHover = false; 106 | bool isAnyHover = false; 107 | 108 | if (limit != lines * columns) 109 | { 110 | limit = lines * columns; 111 | LimitAndOrderHistory(); 112 | } 113 | if (ev.type == EventType.MouseMove) Repaint(); 114 | if (ev.type == EventType.KeyDown) KeyboardNavigation(ev, ref lastSelectedIndex, groupedHistory, OnDeleteKey); 115 | for (int i = 0; i < groupedHistory.Count; i++) 116 | { 117 | var obj = groupedHistory[i]; 118 | if (obj == null) 119 | { 120 | RemoveHistory(obj); 121 | RemovePinned(obj); 122 | shouldLimitAndOrderHistory = true; // Don't modify groupedHistory in this loop 123 | continue; 124 | } 125 | 126 | Rect rect = new Rect(xPos, yPos, columnWidth, rowHeight); 127 | bool isSelected = Selection.objects.Contains(obj); 128 | bool isPinned = pinned.Contains(obj); 129 | 130 | var b = ObjectRow(rect, i, obj, groupedHistory, ref lastSelectedIndex, null, isPinned, 131 | null, 132 | () => MiddleClick(obj, isSelected, isPinned, ref shouldLimitAndOrderHistory), 133 | () => PingButtonMiddleClick(obj, isPinned, ref shouldLimitAndOrderHistory), 134 | () => DragStarted(obj, isSelected), 135 | () => DragPerformed(obj, i, isPinned, ref shouldLimitAndOrderHistory)); 136 | 137 | if (b.isHovered) 138 | { 139 | isAnyHover = true; 140 | hoverObject = obj; 141 | } 142 | 143 | // Draw insertion line at the end of pinned if dragging and mouse position is not above any pinned asset 144 | if (mouseOverWindow && !isAnyShortRectHover && // mouseOverWindow not working correctly with DragAndDrop 145 | i == pinned.Count && DragAndDrop.visualMode != DragAndDropVisualMode.None) 146 | { 147 | if (ev.modifiers != EventModifiers.Control) // Otherwise we are trying to move asset to folder 148 | DrawDragInsertionLine(rect); 149 | } 150 | 151 | if (b.isShortRectHovered) isAnyShortRectHover = true; 152 | yPos += rowHeight; 153 | if ((i + 1) % lines == 0) { xPos += columnWidth; yPos = 0; } // Draw next column 154 | } 155 | // DragAndDrop to window empty space (with no asset rows) 156 | if (ev.type == EventType.DragUpdated) 157 | { 158 | DragAndDrop.visualMode = DragAndDropVisualMode.Generic; 159 | ev.Use(); 160 | } 161 | if (ev.type == EventType.DragPerform) 162 | { 163 | DragAndDrop.AcceptDrag(); 164 | DropObjectToWindow(); 165 | shouldLimitAndOrderHistory = true; 166 | ev.Use(); 167 | } 168 | if (shouldLimitAndOrderHistory) LimitAndOrderHistory(); 169 | if (!isAnyHover) hoverObject = null; 170 | } 171 | 172 | private void MiddleClick(Object obj, bool isSelected, bool isPinned, ref bool shouldLimitAndOrderHistory) 173 | { 174 | var ev = Event.current; 175 | if (ev.modifiers == EventModifiers.Control) 176 | if (isPinned) ClearPinned(); 177 | else ClearHistory(); 178 | else if (isSelected) 179 | { 180 | RemoveAllHistory(x => Selection.objects.Contains(x)); 181 | RemoveAllPinned(x => Selection.objects.Contains(x)); 182 | } 183 | else 184 | { 185 | RemoveHistory(obj); 186 | RemovePinned(obj); 187 | } 188 | shouldLimitAndOrderHistory = true; 189 | ev.Use(); 190 | Repaint(); 191 | } 192 | 193 | private void PingButtonMiddleClick(Object obj, bool isPinned, ref bool shouldLimitAndOrderHistory) 194 | { 195 | if (!isPinned) AddPinned(obj); 196 | else RemovePinned(obj); 197 | shouldLimitAndOrderHistory = true; // Only return dirtied if we change something 198 | } 199 | 200 | private void DropObjectToWindow() 201 | { 202 | foreach (var obj in DragAndDrop.objectReferences) 203 | { 204 | AddPinned(obj); 205 | } 206 | } 207 | 208 | // Drag started from ObjectRow 209 | private void DragStarted(Object obj, bool isSelected) 210 | { 211 | DragAndDrop.SetGenericData(GetInstanceID().ToString(), true); 212 | // If path is empty (non-asset), paths array will not contain this path - probably internal property thing 213 | DragAndDrop.paths = DragAndDrop.objectReferences.Select(x => AssetDatabase.GetAssetPath(x)).ToArray(); 214 | } 215 | 216 | // Drag performed on pinned or not pinned ObjectRow 217 | private void DragPerformed(Object obj, int i, bool isPinned, ref bool shouldLimitAndOrderHistory) 218 | { 219 | var ev = Event.current; 220 | 221 | if (ev.modifiers == EventModifiers.Control) 222 | { 223 | ev.Use(); 224 | DragAndDrop.AcceptDrag(); 225 | 226 | var destinationPath = AssetDatabase.GetAssetPath(obj); 227 | if (!AssetDatabase.IsValidFolder(destinationPath)) return; 228 | MoveAssets(destinationPath, DragAndDrop.paths); 229 | } 230 | else if (isPinned) 231 | { 232 | DragAndDrop.AcceptDrag(); 233 | int k = 0; // Insert would revert order if we do not compensate 234 | foreach (var droppedObj in DragAndDrop.objectReferences) 235 | { 236 | if (!pinned.Contains(droppedObj)) AddPinned(droppedObj, i); 237 | else if (pinned.IndexOf(droppedObj) != i) 238 | { 239 | int insertIndex = pinned.IndexOf(droppedObj) > i ? i + k : i - 1; 240 | RemovePinned(droppedObj); 241 | AddPinned(droppedObj, insertIndex); 242 | } 243 | k++; 244 | } 245 | shouldLimitAndOrderHistory = true; 246 | ev.Use(); 247 | } 248 | else 249 | { 250 | var dragData = DragAndDrop.GetGenericData(GetInstanceID().ToString()); 251 | bool preventDrop = dragData is bool b && b && DragAndDrop.objectReferences.Length == 1 && 252 | DragAndDrop.objectReferences[0] == obj; // Same object row 253 | if (preventDrop) // Prevent accidental drags on itself 254 | { 255 | DragAndDrop.AcceptDrag(); 256 | ev.Use(); 257 | } 258 | else 259 | { 260 | DragAndDrop.AcceptDrag(); 261 | DropObjectToWindow(); 262 | shouldLimitAndOrderHistory = true; 263 | ev.Use(); 264 | } 265 | } 266 | } 267 | 268 | private DragAndDropVisualMode OnDragDroppedSceneHandler(Object dropUpon, Vector3 worldPosition, Vector2 viewportPosition, Transform parentForDraggedObjects, bool perform) 269 | => AddHistoryOnDragPerformed(perform); 270 | 271 | private DragAndDropVisualMode OnDragDroppedHierarchyHandler(int dropTargetInstanceID, HierarchyDropFlags dropMode, Transform parentForDraggedObjects, bool perform) 272 | => AddHistoryOnDragPerformed(perform); 273 | 274 | private DragAndDropVisualMode OnDragDroppedInspectorHandler(UnityEngine.Object[] targets, bool perform) 275 | => AddHistoryOnDragPerformed(perform); 276 | 277 | private DragAndDropVisualMode AddHistoryOnDragPerformed(bool perform) 278 | { 279 | if (!perform) return DragAndDropVisualMode.None; // Next Handler in order will handle this drag (Unity default) 280 | if (DragAndDrop.paths.Length == 0) return DragAndDropVisualMode.None; // Non-asset, e.g. making a prefab from scene object 281 | 282 | foreach (var path in DragAndDrop.paths) 283 | { 284 | var asset = AssetDatabase.LoadMainAssetAtPath(path); 285 | AddHistory(asset); 286 | LimitAndOrderHistory(); 287 | } 288 | Repaint(); 289 | return DragAndDropVisualMode.None; 290 | } 291 | 292 | private void OnDeleteKey() 293 | { 294 | RemoveAllHistory(x => Selection.objects.Contains(x)); 295 | RemoveAllPinned(x => Selection.objects.Contains(x)); 296 | LimitAndOrderHistory(); 297 | Repaint(); 298 | } 299 | 300 | protected virtual void SelectionChanged() 301 | { 302 | foreach (var guid in Selection.assetGUIDs) 303 | { 304 | var path = AssetDatabase.GUIDToAssetPath(guid); 305 | var asset = AssetDatabase.LoadMainAssetAtPath(path); 306 | AddHistory(asset); 307 | LimitAndOrderHistory(); 308 | } 309 | } 310 | 311 | [UnityEditor.ShortcutManagement.Shortcut("Assets History/Add to AssetsHistory")] 312 | public static void AddPinnedGlobal() 313 | { 314 | var windows = Resources.FindObjectsOfTypeAll(); 315 | foreach (var window in windows) 316 | { 317 | foreach (var obj in Selection.objects) 318 | { 319 | if (!window.pinned.Contains(obj)) 320 | { 321 | window.AddFilteredPinned(obj); 322 | } 323 | } 324 | window.LimitAndOrderHistory(); 325 | window.Repaint(); 326 | } 327 | } 328 | 329 | private void AssetImported(Object obj) 330 | { 331 | AddHistory(obj); 332 | LimitAndOrderHistory(); 333 | } 334 | 335 | protected virtual void SceneOpened(Scene scene, OpenSceneMode mode) 336 | { 337 | AddHistory(AssetDatabase.LoadAssetAtPath(scene.path)); 338 | LimitAndOrderHistory(); 339 | } 340 | 341 | protected virtual void AddHistory(Object obj) 342 | { 343 | AddToFront(obj, history); 344 | } 345 | 346 | protected virtual void AddPinned(Object obj, int i = -1) 347 | { 348 | if (i == -1) AddToEnd(obj, pinned); 349 | else 350 | { 351 | RemovePinned(obj); 352 | pinned.Insert(i, obj); 353 | } 354 | } 355 | 356 | protected virtual void AddFilteredPinned(Object obj) 357 | { 358 | if (IsAsset(obj)) AddPinned(obj); 359 | } 360 | 361 | protected virtual void RemoveHistory(Object obj) 362 | { 363 | history.Remove(obj); 364 | } 365 | 366 | protected virtual void RemovePinned(Object obj) 367 | { 368 | pinned.Remove(obj); 369 | } 370 | 371 | protected virtual void RemoveAllPinned(Predicate predicate) 372 | { 373 | foreach (var obj in pinned.Where(x => predicate(x)).ToList()) 374 | { 375 | RemovePinned(obj); 376 | } 377 | } 378 | 379 | protected virtual void RemoveAllHistory(Predicate predicate) 380 | { 381 | foreach (var obj in history.Where(x => predicate(x)).ToList()) 382 | { 383 | RemoveHistory(obj); 384 | } 385 | } 386 | 387 | protected void AddToFront(T obj, List list, int limit = -1) 388 | { 389 | list.Remove(obj); 390 | list.Insert(0, obj); 391 | if (limit > -1 && list.Count > limit) 392 | { 393 | for (int i = list.Count - 1; i >= limit; i--) list.RemoveAt(i); 394 | } 395 | } 396 | 397 | protected void AddToEnd(T obj, List list) 398 | { 399 | list.Remove(obj); 400 | list.Add(obj); 401 | } 402 | 403 | protected virtual void ClearAll() 404 | { 405 | history.Clear(); 406 | pinned.Clear(); 407 | LimitAndOrderHistory(); 408 | } 409 | 410 | protected virtual void ClearHistory() 411 | { 412 | history.Clear(); 413 | LimitAndOrderHistory(); 414 | } 415 | 416 | protected virtual void ClearPinned() 417 | { 418 | pinned.Clear(); 419 | LimitAndOrderHistory(); 420 | } 421 | 422 | protected virtual void LimitAndOrderHistory() 423 | { 424 | RemoveAllHistory(x => x == null); 425 | RemoveAllPinned(x => x == null); 426 | int onlyPinned = pinned.Where(x => !history.Contains(x)).Count(); 427 | int historyLimit = limit - onlyPinned; 428 | if (history.Count > historyLimit) 429 | RemoveAllHistory(x => history.IndexOf(x) >= historyLimit); 430 | //history = history.Take(historyLimit).ToList(); 431 | groupedHistory = history.Where(x => !pinned.Contains(x)) 432 | .OrderBy(x => Path.GetExtension(AssetDatabase.GetAssetPath(x))) 433 | .ThenBy(x => x.GetType().Name).ThenBy(x => x.name). 434 | ThenBy(x => x.GetInstanceID()).ToList(); 435 | groupedHistory.InsertRange(0, pinned); 436 | } 437 | 438 | protected virtual void SaveHistoryToEditorPrefs() 439 | { 440 | string pinnedPaths = string.Join("|", pinned.Select(x => AssetDatabase.GetAssetPath(x))); 441 | EditorPrefs.SetString(prefId + nameof(pinned), pinnedPaths); 442 | string historyPaths = string.Join("|", history.Select(x => AssetDatabase.GetAssetPath(x))); 443 | EditorPrefs.SetString(prefId + nameof(history), historyPaths); 444 | } 445 | 446 | protected virtual void LoadHistoryFromEditorPrefs() 447 | { 448 | string[] pinnedPaths = EditorPrefs.GetString(prefId + nameof(pinned)).Split('|'); 449 | foreach (var path in pinnedPaths) 450 | AddPinned(AssetDatabase.LoadMainAssetAtPath(path)); 451 | string[] historyPaths = EditorPrefs.GetString(prefId + nameof(history)).Split('|'); 452 | foreach (var path in historyPaths) 453 | history.Add(AssetDatabase.LoadMainAssetAtPath(path)); // Preserve order 454 | } 455 | 456 | #region Drawing 457 | private void DrawDragInsertionLine(Rect fullRect) 458 | { 459 | Rect lineRect = new Rect(fullRect.x, fullRect.y - 4, fullRect.width, 3); 460 | GUI.Label(lineRect, GUIContent.none, Styles.insertion); 461 | } 462 | #endregion 463 | } 464 | 465 | public class AssetImportHistory : AssetPostprocessor 466 | { 467 | public static Action assetImported; 468 | 469 | // Number of parameters changes in newer Unity versions 470 | static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) 471 | { 472 | foreach (var path in importedAssets) 473 | { 474 | var asset = AssetDatabase.LoadMainAssetAtPath(path); 475 | if (asset != null) assetImported?.Invoke(asset); 476 | } 477 | } 478 | } 479 | -------------------------------------------------------------------------------- /Scripts/Editor/AssetUtilities/HierarchyHistory.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections.Generic; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEditor.SceneManagement; 6 | using UnityEngine; 7 | using UnityEngine.SceneManagement; 8 | using static EditorHelper; 9 | using Object = UnityEngine.Object; 10 | #if !UNITY_2021_2_OR_NEWER 11 | using UnityEditor.Experimental.SceneManagement; // Out of experimental in 2021.2 12 | #endif 13 | // TODO: optimize, don't save to editorprefs?, inactive gameobjects - change color of text 14 | // added gameobjects to prefabs - change icon at least? 15 | // OnHierarchyChange, check everything and add to scene/prefab history newly instantiated prefab history? - too much overhead? 16 | public class HierarchyHistory : AssetsHistory 17 | { 18 | protected override string prefId => PlayerSettings.companyName + "." + 19 | PlayerSettings.productName + ".EpsilonDelta.HierarchyHistory."; 20 | 21 | protected Dictionary> perObjectHistory = 22 | new Dictionary>(); 23 | protected Dictionary> perObjectPinned = 24 | new Dictionary>(); 25 | 26 | private GlobalObjectId nullGid = default; 27 | 28 | [SerializeField] 29 | private List perObjectHistoryKeys = new List(); 30 | [SerializeField] 31 | private List perObjectHistoryValues = new List(); 32 | 33 | [SerializeField] 34 | private List perObjectPinnedKeys = new List(); 35 | [SerializeField] 36 | private List perObjectPinnedValues = new List(); 37 | 38 | [Serializable] 39 | public class StringList 40 | { 41 | public List list; 42 | public StringList(List newList) 43 | { 44 | list = newList; 45 | } 46 | 47 | public static implicit operator StringList(List l) => new StringList(l); 48 | public static implicit operator List(StringList l) => l.list; 49 | } 50 | 51 | [MenuItem("Window/Hierarchy History")] 52 | private static void CreateHierarchyHistory() 53 | { 54 | var window = GetWindow(typeof(HierarchyHistory), false, "Hierarchy History") as HierarchyHistory; 55 | window.minSize = new Vector2(100, rowHeight + 1); 56 | window.Show(); 57 | } 58 | 59 | public override void AddItemsToMenu(GenericMenu menu) 60 | { 61 | base.AddItemsToMenu(menu); 62 | menu.AddItem(EditorGUIUtility.TrTextContent("Clear All for All Objects"), false, ClearAllForAllObjects); 63 | menu.AddItem(EditorGUIUtility.TrTextContent("List all objects"), false, ListAllObjects); 64 | } 65 | 66 | protected override void Test() 67 | { 68 | var gos = FindObjectsOfType(true); 69 | var objs = GlobalObjectId.GlobalObjectIdentifierToObjectSlow( 70 | ConvertToUnpackedGid(Parse( 71 | "GlobalObjectId_V1-2-2fd19a05ecb802843bd51e0f33d4a32b-4983886930841126834-1308519948"))); 72 | Debug.Log(ConvertToUnpackedGid(Parse( 73 | "GlobalObjectId_V1-2-2fd19a05ecb802843bd51e0f33d4a32b-4983886930841126834-1308519948"))); 74 | Debug.Log(objs, objs); 75 | } 76 | 77 | protected void ListAllObjects() 78 | { 79 | foreach (var obj in perObjectHistory) 80 | { 81 | Debug.Log("History Key: " + obj.Key + "\nValues:\n " + string.Join("\n", obj.Value)); 82 | } 83 | foreach (var obj in perObjectPinned) 84 | { 85 | Debug.Log("Pinned Key: " + obj.Key + "\nValues:\n " + string.Join("\n", obj.Value)); 86 | } 87 | } 88 | 89 | protected override void OnEnable() 90 | { 91 | //Debug.Log("custom enable"); 92 | OnAfterDeserialize(); 93 | // This is received even if invisible 94 | Selection.selectionChanged -= SelectionChanged; 95 | Selection.selectionChanged += SelectionChanged; 96 | EditorApplication.hierarchyChanged -= HierarchyChanged; 97 | EditorApplication.hierarchyChanged += HierarchyChanged; 98 | PrefabStage.prefabStageOpened -= PrefabStageOpened; 99 | PrefabStage.prefabStageOpened += PrefabStageOpened; 100 | PrefabStage.prefabStageClosing -= PrefabStageClosing; 101 | PrefabStage.prefabStageClosing += PrefabStageClosing; 102 | EditorSceneManager.sceneOpened -= SceneOpened; 103 | EditorSceneManager.sceneOpened += SceneOpened; 104 | EditorApplication.playModeStateChanged -= PlayModeExitted; 105 | EditorApplication.playModeStateChanged += PlayModeExitted; 106 | EditorApplication.quitting -= OnBeforeSerialize; 107 | EditorApplication.quitting += OnBeforeSerialize; 108 | EditorApplication.quitting -= SaveHistoryToEditorPrefs; 109 | EditorApplication.quitting += SaveHistoryToEditorPrefs; 110 | wantsMouseEnterLeaveWindow = true; 111 | wantsMouseMove = true; 112 | 113 | var prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); 114 | if (prefabStage) LoadPrefabHistory(prefabStage); 115 | else LoadOpenScenesHistory(); 116 | 117 | LimitAndOrderHistory(); 118 | } 119 | protected override void OnDisable() 120 | { 121 | OnBeforeSerialize(); 122 | } 123 | 124 | protected override void SelectionChanged() 125 | { 126 | foreach (var t in Selection.transforms) 127 | { 128 | AddHistory(t.gameObject); 129 | } 130 | LimitAndOrderHistory(); 131 | } 132 | 133 | private void HierarchyChanged() 134 | { 135 | Repaint(); 136 | } 137 | 138 | private void PlayModeExitted(PlayModeStateChange stateChange) 139 | { 140 | if (stateChange == PlayModeStateChange.EnteredEditMode) 141 | { 142 | LoadOpenScenesHistory(); 143 | LimitAndOrderHistory(); 144 | Repaint(); 145 | } 146 | } 147 | 148 | protected override void SceneOpened(Scene scene, OpenSceneMode mode) 149 | { 150 | LoadSceneHistory(scene); 151 | LimitAndOrderHistory(); 152 | } 153 | 154 | private void PrefabStageOpened(PrefabStage prefabStage) 155 | { 156 | history.Clear(); 157 | pinned.Clear(); 158 | LoadPrefabHistory(prefabStage); 159 | LimitAndOrderHistory(); 160 | Repaint(); 161 | } 162 | 163 | private void PrefabStageClosing(PrefabStage prefabStage) 164 | { 165 | // If we switch to other prefab, Closing old stage is called after opening new stage 166 | if (PrefabStageUtility.GetCurrentPrefabStage() != null) return; 167 | history.Clear(); 168 | pinned.Clear(); 169 | LoadOpenScenesHistory(); 170 | LimitAndOrderHistory(); 171 | Repaint(); 172 | } 173 | 174 | private void LoadPrefabHistory(PrefabStage prefabStage) 175 | { 176 | var prefabAsset = AssetDatabase.LoadMainAssetAtPath(prefabStage.assetPath); 177 | var prefabRoot = prefabStage.prefabContentsRoot; 178 | var prefabGid = GlobalObjectId.GetGlobalObjectIdSlow(prefabAsset); 179 | var prefabChildren = prefabRoot.GetComponentsInChildren(true); 180 | if (perObjectPinned.ContainsKey(prefabGid)) 181 | { 182 | var toRemove = new List(); 183 | foreach (var gid in perObjectPinned[prefabGid]) 184 | { 185 | var obj = GlobalObjectIdentifierToPrefabObject(prefabChildren, gid); 186 | if (!obj) obj = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(gid); 187 | if (obj) AddToEnd(obj, pinned); 188 | else toRemove.Add(gid); 189 | } 190 | foreach (var gid in toRemove) 191 | { 192 | perObjectPinned[prefabGid].Remove(gid); 193 | EditorPrefs.DeleteKey(prefId + nameof(perObjectPinnedValues) + gid); 194 | } 195 | } 196 | if (perObjectHistory.ContainsKey(prefabGid)) 197 | { 198 | var toRemove = new List(); 199 | foreach (var gid in perObjectHistory[prefabGid]) 200 | { 201 | var obj = GlobalObjectIdentifierToPrefabObject(prefabChildren, gid); 202 | if (obj) AddToEnd(obj, history); 203 | else toRemove.Add(gid); 204 | } 205 | foreach (var gid in toRemove) 206 | { 207 | perObjectHistory[prefabGid].Remove(gid); 208 | EditorPrefs.DeleteKey(prefId + nameof(perObjectHistoryValues) + gid); 209 | } 210 | } 211 | } 212 | 213 | private void LoadOpenScenesHistory() 214 | { 215 | int countLoaded = EditorSceneManager.sceneCount; 216 | Scene[] loadedScenes = new Scene[countLoaded]; 217 | for (int i = 0; i < countLoaded; i++) 218 | { 219 | loadedScenes[i] = EditorSceneManager.GetSceneAt(i); 220 | } 221 | foreach (var scene in loadedScenes) 222 | { 223 | LoadSceneHistory(scene); 224 | } 225 | } 226 | 227 | private void LoadSceneHistory(Scene scene) 228 | { 229 | var sceneGid = GlobalObjectId.GetGlobalObjectIdSlow( 230 | AssetDatabase.LoadAssetAtPath(scene.path)); 231 | if (perObjectPinned.ContainsKey(sceneGid)) 232 | { 233 | var toRemove = new List(); 234 | foreach (var gid in perObjectPinned[sceneGid]) 235 | { 236 | var obj = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(gid); 237 | if (obj) AddToEnd(obj, pinned); 238 | else toRemove.Add(gid); 239 | } 240 | foreach (var gid in toRemove) 241 | { 242 | perObjectPinned[sceneGid].Remove(gid); 243 | EditorPrefs.DeleteKey(prefId + nameof(perObjectPinnedValues) + gid); 244 | } 245 | } 246 | if (perObjectHistory.ContainsKey(sceneGid)) 247 | { 248 | var toRemove = new List(); 249 | foreach (var gid in perObjectHistory[sceneGid]) 250 | { 251 | var obj = GlobalObjectId.GlobalObjectIdentifierToObjectSlow(gid); 252 | if (obj) AddToEnd(obj, history); 253 | else toRemove.Add(gid); 254 | } 255 | foreach (var gid in toRemove) 256 | { 257 | perObjectHistory[sceneGid].Remove(gid); 258 | EditorPrefs.DeleteKey(prefId + nameof(perObjectHistoryValues) + gid); 259 | } 260 | } 261 | } 262 | 263 | protected override void AddHistory(Object obj) 264 | { 265 | var objGid = GlobalObjectId.GetGlobalObjectIdSlow(obj); 266 | if (objGid.ToString() == nullGid.ToString()) 267 | { 268 | AddToFront(obj, history); 269 | return; 270 | } 271 | 272 | PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); 273 | GlobalObjectId prefabGid = new GlobalObjectId(); 274 | if (prefabStage != null) 275 | prefabGid = GlobalObjectId.GetGlobalObjectIdSlow(AssetDatabase.LoadMainAssetAtPath(prefabStage.assetPath)); 276 | if (prefabStage != null) 277 | { 278 | if (!perObjectHistory.ContainsKey(prefabGid)) perObjectHistory[prefabGid] = new List(); 279 | var subPrefabGid = ConstructGid(objGid.identifierType, prefabGid.assetGUID.ToString(), objGid.targetObjectId, 280 | objGid.targetPrefabId); 281 | if (!ComparePrefabObjectInstance(prefabGid, objGid)) 282 | { 283 | AddToFront(obj, history); 284 | AddToFront(subPrefabGid, perObjectHistory[prefabGid]); 285 | } 286 | } 287 | else if (IsSceneObject(obj, out GameObject go) && !string.IsNullOrEmpty(go.scene.path)) 288 | { 289 | var sceneGid = GlobalObjectId.GetGlobalObjectIdSlow( 290 | AssetDatabase.LoadAssetAtPath(go.scene.path)); 291 | if (!perObjectHistory.ContainsKey(sceneGid)) perObjectHistory[sceneGid] = new List(); 292 | 293 | AddToFront(obj, history); 294 | AddToFront(objGid, perObjectHistory[sceneGid]); 295 | } 296 | } 297 | 298 | protected override void AddPinned(Object obj, int i = -1) 299 | { 300 | PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); 301 | GlobalObjectId prefabGid = new GlobalObjectId(); 302 | if (prefabStage != null) 303 | prefabGid = GlobalObjectId.GetGlobalObjectIdSlow(AssetDatabase.LoadMainAssetAtPath(prefabStage.assetPath)); 304 | var objGid = GlobalObjectId.GetGlobalObjectIdSlow(obj); 305 | bool isNullGid = objGid.ToString() == nullGid.ToString(); 306 | 307 | if (prefabStage != null) 308 | { 309 | if (!perObjectPinned.ContainsKey(prefabGid)) perObjectPinned[prefabGid] = new List(); 310 | 311 | if (i == -1) 312 | { 313 | AddToEnd(objGid, perObjectPinned[prefabGid]); 314 | AddToEnd(obj, pinned); 315 | } 316 | else 317 | { 318 | RemovePinned(obj); 319 | pinned.Insert(i, obj); 320 | if (!isNullGid) perObjectPinned[prefabGid].Insert(i, objGid); 321 | } 322 | } 323 | else if (IsSceneObject(obj, out GameObject go) && !string.IsNullOrEmpty(go.scene.path)) 324 | { 325 | var sceneGid = GlobalObjectId.GetGlobalObjectIdSlow( 326 | AssetDatabase.LoadAssetAtPath(go.scene.path)); 327 | if (!perObjectPinned.ContainsKey(sceneGid)) perObjectPinned[sceneGid] = new List(); 328 | 329 | if (i == -1) 330 | { 331 | if (!isNullGid) AddToEnd(objGid, perObjectPinned[sceneGid]); 332 | AddToEnd(obj, pinned); 333 | } 334 | else 335 | { 336 | RemovePinned(obj); 337 | pinned.Insert(i, obj); 338 | perObjectPinned[sceneGid].Insert(i, objGid); 339 | } 340 | } 341 | else 342 | { 343 | Scene scene = EditorSceneManager.GetActiveScene(); 344 | var sceneGid = GlobalObjectId.GetGlobalObjectIdSlow( 345 | AssetDatabase.LoadAssetAtPath(scene.path)); 346 | if (!perObjectPinned.ContainsKey(sceneGid)) perObjectPinned[sceneGid] = new List(); 347 | 348 | if (i == -1) 349 | { 350 | if (!isNullGid) AddToEnd(objGid, perObjectPinned[sceneGid]); 351 | AddToEnd(obj, pinned); 352 | } 353 | else 354 | { 355 | RemovePinned(obj); 356 | pinned.Insert(i, obj); 357 | if (!isNullGid) perObjectPinned[sceneGid].Insert(i, objGid); 358 | } 359 | } 360 | } 361 | 362 | protected override void AddFilteredPinned(Object obj) 363 | { 364 | if (IsNonAssetGameObject(obj)) AddPinned(obj); 365 | } 366 | 367 | protected override void RemoveHistory(Object obj) 368 | { 369 | base.RemoveHistory(obj); 370 | if (obj == null) return; 371 | 372 | PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); 373 | GlobalObjectId gid = GlobalObjectId.GetGlobalObjectIdSlow(obj); 374 | if (prefabStage != null) 375 | { 376 | GlobalObjectId prefabGid = GlobalObjectId.GetGlobalObjectIdSlow( 377 | AssetDatabase.LoadMainAssetAtPath(prefabStage.assetPath)); 378 | 379 | if (perObjectHistory.ContainsKey(prefabGid)) 380 | { 381 | perObjectHistory[prefabGid].RemoveAll(x => ComparePrefabObjectInstance(x, gid)); 382 | if (perObjectHistory[prefabGid].Count == 0) 383 | { 384 | perObjectHistory.Remove(prefabGid); 385 | EditorPrefs.DeleteKey(prefId + nameof(perObjectHistoryValues) + prefabGid); 386 | } 387 | } 388 | } 389 | else if (IsSceneObject(obj, out GameObject go) && !string.IsNullOrEmpty(go.scene.path)) 390 | { 391 | var sceneGid = GlobalObjectId.GetGlobalObjectIdSlow( 392 | AssetDatabase.LoadAssetAtPath(go.scene.path)); 393 | if (perObjectHistory.ContainsKey(sceneGid)) 394 | { 395 | perObjectHistory[sceneGid].Remove(gid); 396 | if (perObjectHistory[sceneGid].Count == 0) 397 | { 398 | perObjectHistory.Remove(sceneGid); 399 | EditorPrefs.DeleteKey(prefId + nameof(perObjectHistoryValues) + sceneGid); 400 | } 401 | } 402 | 403 | } 404 | } 405 | 406 | protected override void RemoveAllHistory(Predicate predicate) 407 | { 408 | foreach (var obj in history.Where(x => predicate(x)).ToList()) 409 | { 410 | RemoveHistory(obj); 411 | } 412 | } 413 | 414 | protected override void RemovePinned(Object obj) 415 | { 416 | base.RemovePinned(obj); 417 | if (obj == null) return; 418 | 419 | GlobalObjectId gid = GlobalObjectId.GetGlobalObjectIdSlow(obj); 420 | PrefabStage prefabStage = PrefabStageUtility.GetCurrentPrefabStage(); 421 | 422 | if (prefabStage != null) 423 | { 424 | GlobalObjectId prefabGid = GlobalObjectId.GetGlobalObjectIdSlow( 425 | AssetDatabase.LoadMainAssetAtPath(prefabStage.assetPath)); 426 | 427 | if (perObjectPinned.ContainsKey(prefabGid)) 428 | { 429 | perObjectPinned[prefabGid].RemoveAll(x => ComparePrefabObjectInstance(x, gid)); 430 | if (perObjectPinned[prefabGid].Count == 0) 431 | { 432 | perObjectPinned.Remove(prefabGid); 433 | EditorPrefs.DeleteKey(prefId + nameof(perObjectPinnedValues) + prefabGid); 434 | } 435 | } 436 | } 437 | else if (IsSceneObject(obj, out GameObject go) && !string.IsNullOrEmpty(go.scene.path)) 438 | { 439 | var sceneGid = GlobalObjectId.GetGlobalObjectIdSlow( 440 | AssetDatabase.LoadAssetAtPath(go.scene.path)); 441 | if (perObjectPinned.ContainsKey(sceneGid)) 442 | { 443 | perObjectPinned[sceneGid].Remove(gid); 444 | if (perObjectPinned[sceneGid].Count == 0) 445 | { 446 | perObjectPinned.Remove(sceneGid); 447 | EditorPrefs.DeleteKey(prefId + nameof(perObjectPinnedValues) + sceneGid); 448 | } 449 | } 450 | } 451 | else 452 | { 453 | int countLoaded = EditorSceneManager.sceneCount; 454 | Scene[] loadedScenes = new Scene[countLoaded]; 455 | for (int i = 0; i < countLoaded; i++) 456 | { 457 | loadedScenes[i] = EditorSceneManager.GetSceneAt(i); 458 | } 459 | foreach (var scene in loadedScenes) 460 | { 461 | var sceneGid = GlobalObjectId.GetGlobalObjectIdSlow( 462 | AssetDatabase.LoadAssetAtPath(scene.path)); 463 | if (perObjectPinned.ContainsKey(sceneGid) && perObjectPinned[sceneGid].Contains(gid)) 464 | { 465 | perObjectPinned[sceneGid].Remove(gid); 466 | if (perObjectPinned[sceneGid].Count == 0) 467 | { 468 | perObjectPinned.Remove(sceneGid); 469 | EditorPrefs.DeleteKey(prefId + nameof(perObjectPinnedValues) + sceneGid); 470 | } 471 | break; 472 | } 473 | } 474 | } 475 | 476 | } 477 | 478 | protected override void RemoveAllPinned(Predicate predicate) 479 | { 480 | foreach (var obj in pinned.Where(x => predicate(x)).ToList()) 481 | { 482 | RemovePinned(obj); 483 | } 484 | } 485 | 486 | private void ClearAllForAllObjects() 487 | { 488 | ClearAll(); 489 | foreach (var key in perObjectHistory.Keys) 490 | { 491 | EditorPrefs.DeleteKey(prefId + nameof(perObjectHistoryValues) + key); 492 | } 493 | perObjectHistory.Clear(); 494 | perObjectHistoryKeys.Clear(); 495 | perObjectHistoryValues.Clear(); 496 | 497 | foreach (var key in perObjectPinned.Keys) 498 | { 499 | EditorPrefs.DeleteKey(prefId + nameof(perObjectPinnedValues) + key); 500 | } 501 | perObjectPinned.Clear(); 502 | perObjectPinnedKeys.Clear(); 503 | perObjectPinnedValues.Clear(); 504 | SaveHistoryToEditorPrefs(); 505 | } 506 | 507 | protected override void ClearAll() 508 | { 509 | RemoveAllHistory(_ => true); 510 | RemoveAllPinned(_ => true); 511 | LimitAndOrderHistory(); 512 | } 513 | 514 | protected override void ClearHistory() 515 | { 516 | RemoveAllHistory(_ => true); 517 | LimitAndOrderHistory(); 518 | } 519 | 520 | protected override void ClearPinned() 521 | { 522 | RemoveAllPinned(_ => true); 523 | LimitAndOrderHistory(); 524 | } 525 | 526 | protected override void SaveHistoryToEditorPrefs() 527 | { 528 | //Debug.Log("custom save"); 529 | string pinnedGidKeys = string.Join("|", perObjectPinnedKeys); 530 | EditorPrefs.SetString(prefId + nameof(perObjectPinnedKeys), pinnedGidKeys); 531 | 532 | for (int i = 0; i < perObjectPinnedKeys.Count; i++) 533 | { 534 | if (string.IsNullOrEmpty(perObjectPinnedKeys[i])) continue; 535 | string pinnedGidValue = string.Join("|", perObjectPinnedValues[i].list); 536 | EditorPrefs.SetString(prefId + nameof(perObjectPinnedValues) + perObjectPinnedKeys[i], pinnedGidValue); 537 | } 538 | 539 | string historyGidKeys = string.Join("|", perObjectHistoryKeys); 540 | EditorPrefs.SetString(prefId + nameof(perObjectHistoryKeys), historyGidKeys); 541 | 542 | for (int i = 0; i < perObjectHistoryKeys.Count; i++) 543 | { 544 | if (string.IsNullOrEmpty(perObjectHistoryKeys[i])) continue; 545 | string historyGidValue = string.Join("|", perObjectHistoryValues[i].list); 546 | EditorPrefs.SetString(prefId + nameof(perObjectHistoryValues) + perObjectHistoryKeys[i], historyGidValue); 547 | } 548 | } 549 | 550 | protected override void LoadHistoryFromEditorPrefs() 551 | { 552 | //Debug.Log("custom load"); 553 | perObjectPinnedKeys = EditorPrefs.GetString(prefId + nameof(perObjectPinnedKeys)).Split('|').ToList(); 554 | perObjectPinnedKeys.RemoveAll(x => string.IsNullOrEmpty(x)); 555 | for (int i = 0; i < perObjectPinnedKeys.Count; i++) 556 | { 557 | perObjectPinnedValues.Add( 558 | EditorPrefs.GetString(prefId + nameof(perObjectPinnedValues) + perObjectPinnedKeys[i]) 559 | .Split('|').ToList()); 560 | } 561 | 562 | perObjectHistoryKeys = EditorPrefs.GetString(prefId + nameof(perObjectHistoryKeys)).Split('|').ToList(); 563 | perObjectHistoryKeys.RemoveAll(x => string.IsNullOrEmpty(x)); 564 | for (int i = 0; i < perObjectHistoryKeys.Count; i++) 565 | { 566 | perObjectHistoryValues.Add( 567 | EditorPrefs.GetString(prefId + nameof(perObjectHistoryValues) + perObjectHistoryKeys[i]) 568 | .Split('|').ToList()); 569 | } 570 | //Debug.Log(perObjectHistoryKeys.Serialize()); 571 | //Debug.Log(perObjectHistoryValues.Select(x => x.list).Serialize()); 572 | } 573 | 574 | // ISerializationCallbackReceiver is not called when window is closed, so we handle it manually in OnEnable/Disable 575 | // Recompilation - awake/destroy not called, only OnEnable/Disable 576 | // Closing and opening window - Awake, Destroy, OnEnable, OnDisable are all called 577 | public void OnBeforeSerialize() 578 | { 579 | //Debug.Log("custom before serialize"); 580 | perObjectPinnedKeys.Clear(); 581 | perObjectPinnedValues.Clear(); 582 | foreach (KeyValuePair> pair in perObjectPinned) 583 | { 584 | perObjectPinnedKeys.Add(pair.Key.ToString()); 585 | perObjectPinnedValues.Add(pair.Value.Select(x => x.ToString()).ToList()); 586 | } 587 | 588 | perObjectHistoryKeys.Clear(); 589 | perObjectHistoryValues.Clear(); 590 | foreach (KeyValuePair> pair in perObjectHistory) 591 | { 592 | perObjectHistoryKeys.Add(pair.Key.ToString()); 593 | perObjectHistoryValues.Add(pair.Value.Select(x => x.ToString()).ToList()); 594 | } 595 | //Debug.Log(perObjectHistoryKeys.Serialize()); 596 | //Debug.Log(perObjectHistoryValues.Select(x => x.list).Serialize()); 597 | } 598 | 599 | public void OnAfterDeserialize() 600 | { 601 | //Debug.Log("custom after deserialize"); 602 | perObjectPinned.Clear(); 603 | for (int i = 0; i < perObjectPinnedKeys.Count; i++) 604 | { 605 | perObjectPinned.Add(Parse(perObjectPinnedKeys[i]), perObjectPinnedValues[i].list.Select(x => Parse(x)).ToList()); 606 | } 607 | 608 | perObjectHistory.Clear(); 609 | for (int i = 0; i < perObjectHistoryKeys.Count; i++) 610 | { 611 | perObjectHistory.Add(Parse(perObjectHistoryKeys[i]), perObjectHistoryValues[i].list.Select(x => Parse(x)).ToList()); 612 | } 613 | } 614 | 615 | #region Helpers 616 | 617 | // The default method of GlobalObjectId does not work for some reason for objects in prefab stage 618 | private Object GlobalObjectIdentifierToPrefabObject(Transform[] prefabChildren, GlobalObjectId gid) 619 | { 620 | foreach (var c in prefabChildren) 621 | { 622 | var childrenGid = GlobalObjectId.GetGlobalObjectIdSlow(c.gameObject); 623 | if (ComparePrefabObjectInstance(childrenGid, gid)) 624 | { 625 | return c.gameObject; 626 | } 627 | } 628 | return null; 629 | } 630 | 631 | // Usable for instances in the scene to select all copies 632 | private List GlobalObjectIdentifierToPrefabObjects(Transform[] prefabChildren, GlobalObjectId gid) 633 | { 634 | var objs = new List(); 635 | foreach (var c in prefabChildren) 636 | { 637 | var childrenGid = GlobalObjectId.GetGlobalObjectIdSlow(c.gameObject); 638 | if (ComparePrefabObjectInstance(childrenGid, gid)) 639 | { 640 | objs.Add(c.gameObject); 641 | } 642 | } 643 | return objs; 644 | } 645 | 646 | // Omits asset GUID and targetPrefabId. This is to sync prefab asset and prefab instances hierarchy history. 647 | // targetPrefabId is only assigned children of instances of prefabs in the scene, targetObjectId is the same everywhere. 648 | // On playmode entered, targetObjectId and targetPrefaId are added together (it's more complicated) 649 | private bool ComparePrefabObjectInstance(GlobalObjectId gid1, GlobalObjectId gid2) 650 | { 651 | return gid1.targetObjectId == gid2.targetObjectId; 652 | } 653 | 654 | private GlobalObjectId Parse(string gidString) 655 | { 656 | GlobalObjectId.TryParse(gidString, out GlobalObjectId gid); 657 | return gid; 658 | } 659 | 660 | // Parse method cannot parse GIDs with null GUIDs (a bug probably) 661 | private GlobalObjectId ConstructGid( 662 | int identifierType, string assetGUID, ulong targetObjecId, ulong targetPrefabId) 663 | { 664 | string newGidString = $"GlobalObjectId_V1-{identifierType}-{assetGUID}-{targetObjecId}-{targetPrefabId}"; 665 | return Parse(newGidString); 666 | } 667 | 668 | private GlobalObjectId ConvertToUnpackedGid(GlobalObjectId gid) 669 | { 670 | ulong unpackedTargetObjectId = (gid.targetObjectId ^ gid.targetPrefabId) & 0x7fffffffffffffff; 671 | return ConstructGid(gid.identifierType, gid.assetGUID.ToString(), unpackedTargetObjectId, 0); 672 | } 673 | #endregion 674 | } 675 | --------------------------------------------------------------------------------