├── .gitattributes ├── .gitignore ├── Documentation └── APKInstaller.pdf ├── LICENSE ├── README.md ├── Scripts └── Editor │ ├── APKInstaller.cs │ ├── AppDataUtility.cs │ ├── AssetUtilities │ ├── AssetDependencies.cs │ ├── AssetUtilities.cs │ ├── AssetsHistory.cs │ ├── FindAssetUsages.cs │ ├── FindMissingReferences.cs │ ├── FindMissingScripts.cs │ ├── FindUnusedAssets.cs │ ├── HierarchyHistory.cs │ └── HierarchyHistorySimple.cs │ ├── Common │ ├── EditorHelper.cs │ └── MyGUI.cs │ ├── ComponentUtilities.cs │ ├── EditorUtilities.cs │ ├── FileUtilities.cs │ ├── HierarchyUtilities.cs │ ├── InspectorExtensions.cs │ ├── MyShortcuts.cs │ ├── OpenScenes.cs │ ├── PrefabUtilities.cs │ ├── RectToolRounding.cs │ └── UnityUtilities.Editor.asmdef └── _config.yml /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | * eol=lf 4 | -------------------------------------------------------------------------------- /.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 | -------------------------------------------------------------------------------- /Documentation/APKInstaller.pdf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/EpsilonD3lta/UnityUtilities/268fc1ef689f4f0feb92bbbdc9ccd0232bd52f82/Documentation/APKInstaller.pdf -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/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/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 Object = UnityEngine.Object; 8 | using static EditorHelper; 9 | using static MyGUI; 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/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/AssetUtilities/AssetsHistory.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 Object = UnityEngine.Object; 9 | using static EditorHelper; 10 | using static MyGUI; 11 | using System.IO; 12 | using System.Reflection; 13 | 14 | public class AssetsHistory : MyEditorWindow, IHasCustomMenu 15 | { 16 | protected const int rowHeight = objectRowHeight; 17 | protected const int minColumnWidth = 150; 18 | protected virtual string prefId => PlayerSettings.companyName + "." + 19 | PlayerSettings.productName + ".EpsilonDelta.AssetsHistory."; 20 | 21 | protected List groupedHistory = new List(); 22 | protected List history = new List(); 23 | protected List pinned = new List(); 24 | private int limit = 10; 25 | private int lastSelectedIndex = -1; 26 | 27 | [MenuItem("Window/Assets History")] 28 | private static void CreateWindow() 29 | { 30 | AssetsHistory window; 31 | if (Resources.FindObjectsOfTypeAll().Any(x => x.GetType() == typeof(AssetsHistory))) 32 | window = GetWindow(typeof(AssetsHistory), false, "Assets History") as AssetsHistory; 33 | else window = CreateWindow("Assets History"); 34 | window.minSize = new Vector2(100, rowHeight + 1); 35 | window.Show(); 36 | } 37 | 38 | public virtual void AddItemsToMenu(GenericMenu menu) 39 | { 40 | menu.AddItem(EditorGUIUtility.TrTextContent("Test"), false, Test); 41 | menu.AddItem(EditorGUIUtility.TrTextContent("Clear All"), false, ClearAll); 42 | menu.AddItem(EditorGUIUtility.TrTextContent("Clear History"), false, ClearHistory); 43 | menu.AddItem(EditorGUIUtility.TrTextContent("Clear Pinned"), false, ClearPinned); 44 | } 45 | 46 | protected virtual void Test() 47 | { 48 | 49 | } 50 | 51 | protected virtual void Awake() 52 | { 53 | LoadHistoryFromEditorPrefs(); 54 | } 55 | 56 | protected virtual void OnEnable() 57 | { 58 | // This is received even if invisible 59 | Selection.selectionChanged -= SelectionChanged; 60 | Selection.selectionChanged += SelectionChanged; 61 | DragAndDrop.RemoveDropHandler(OnDragDroppedSceneHandler); 62 | DragAndDrop.AddDropHandler(OnDragDroppedSceneHandler); 63 | DragAndDrop.RemoveDropHandler(OnDragDroppedHierarchyHandler); 64 | DragAndDrop.AddDropHandler(OnDragDroppedHierarchyHandler); 65 | DragAndDrop.RemoveDropHandler(OnDragDroppedInspectorHandler); 66 | DragAndDrop.AddDropHandler(OnDragDroppedInspectorHandler); 67 | AssetImportHistory.assetImported -= AssetImported; 68 | AssetImportHistory.assetImported += AssetImported; 69 | EditorSceneManager.sceneOpened -= SceneOpened; 70 | EditorSceneManager.sceneOpened += SceneOpened; 71 | EditorApplication.quitting -= SaveHistoryToEditorPrefs; 72 | EditorApplication.quitting += SaveHistoryToEditorPrefs; 73 | 74 | wantsMouseEnterLeaveWindow = true; 75 | wantsMouseMove = true; 76 | 77 | LimitAndOrderHistory(); 78 | } 79 | 80 | protected virtual void OnDisable() 81 | { 82 | } 83 | 84 | protected virtual void OnDestroy() 85 | { 86 | SaveHistoryToEditorPrefs(); 87 | } 88 | 89 | // This is received only when window is visible 90 | private void OnSelectionChange() 91 | { 92 | Repaint(); 93 | } 94 | 95 | private void OnGUI() 96 | { 97 | var ev = Event.current; //Debug.Log(ev.type); 98 | var height = position.height; 99 | var width = position.width; 100 | int lines = Mathf.FloorToInt(height / rowHeight); 101 | int columns = Mathf.FloorToInt(width / minColumnWidth); 102 | if (columns <= 1) columns = 2; 103 | float columnWidth = width / columns; 104 | float xPos = 0, yPos = 0; 105 | bool shouldLimitAndOrderHistory = false; 106 | bool isAnyShortRectHover = false; 107 | bool isAnyHover = false; 108 | 109 | if (limit != lines * columns) 110 | { 111 | limit = lines * columns; 112 | LimitAndOrderHistory(); 113 | } 114 | if (ev.type == EventType.MouseMove) Repaint(); 115 | if (ev.type == EventType.KeyDown) KeyboardNavigation(ev, ref lastSelectedIndex, groupedHistory, OnDeleteKey); 116 | for (int i = 0; i < groupedHistory.Count; i++) 117 | { 118 | var obj = groupedHistory[i]; 119 | if (obj == null) 120 | { 121 | RemoveHistory(obj); 122 | RemovePinned(obj); 123 | shouldLimitAndOrderHistory = true; // Don't modify groupedHistory in this loop 124 | continue; 125 | } 126 | 127 | Rect rect = new Rect(xPos, yPos, columnWidth, rowHeight); 128 | bool isSelected = Selection.objects.Contains(obj); 129 | bool isPinned = pinned.Contains(obj); 130 | 131 | var b = ObjectRow(rect, i, obj, groupedHistory, ref lastSelectedIndex, null, isPinned, 132 | null, 133 | () => MiddleClick(obj, isSelected, isPinned, ref shouldLimitAndOrderHistory), 134 | () => PingButtonMiddleClick(obj, isPinned, ref shouldLimitAndOrderHistory), 135 | () => DragStarted(obj, isSelected), 136 | () => DragPerformed(obj, i, isPinned, ref shouldLimitAndOrderHistory)); 137 | 138 | if (b.isHovered) 139 | { 140 | isAnyHover = true; 141 | hoverObject = obj; 142 | } 143 | 144 | // Draw insertion line at the end of pinned if dragging and mouse position is not above any pinned asset 145 | if (mouseOverWindow && !isAnyShortRectHover && // mouseOverWindow not working correctly with DragAndDrop 146 | i == pinned.Count && DragAndDrop.visualMode != DragAndDropVisualMode.None) 147 | { 148 | if (ev.modifiers != EventModifiers.Control) // Otherwise we are trying to move asset to folder 149 | DrawDragInsertionLine(rect); 150 | } 151 | 152 | if (b.isShortRectHovered) isAnyShortRectHover = true; 153 | yPos += rowHeight; 154 | if ((i + 1) % lines == 0) { xPos += columnWidth; yPos = 0; } // Draw next column 155 | } 156 | // DragAndDrop to window empty space (with no asset rows) 157 | if (ev.type == EventType.DragUpdated) 158 | { 159 | DragAndDrop.visualMode = DragAndDropVisualMode.Generic; 160 | ev.Use(); 161 | } 162 | if (ev.type == EventType.DragPerform) 163 | { 164 | DragAndDrop.AcceptDrag(); 165 | DropObjectToWindow(); 166 | shouldLimitAndOrderHistory = true; 167 | ev.Use(); 168 | } 169 | if (shouldLimitAndOrderHistory) LimitAndOrderHistory(); 170 | if (!isAnyHover) hoverObject = null; 171 | } 172 | 173 | private void MiddleClick(Object obj, bool isSelected, bool isPinned, ref bool shouldLimitAndOrderHistory) 174 | { 175 | var ev = Event.current; 176 | if (ev.modifiers == EventModifiers.Control) 177 | if (isPinned) ClearPinned(); 178 | else ClearHistory(); 179 | else if (isSelected) 180 | { 181 | RemoveAllHistory(x => Selection.objects.Contains(x)); 182 | RemoveAllPinned(x => Selection.objects.Contains(x)); 183 | } 184 | else 185 | { 186 | RemoveHistory(obj); 187 | RemovePinned(obj); 188 | } 189 | shouldLimitAndOrderHistory = true; 190 | ev.Use(); 191 | Repaint(); 192 | } 193 | 194 | private void PingButtonMiddleClick(Object obj, bool isPinned, ref bool shouldLimitAndOrderHistory) 195 | { 196 | if (!isPinned) AddPinned(obj); 197 | else RemovePinned(obj); 198 | shouldLimitAndOrderHistory = true; // Only return dirtied if we change something 199 | } 200 | 201 | private void DropObjectToWindow() 202 | { 203 | foreach (var obj in DragAndDrop.objectReferences) 204 | { 205 | AddPinned(obj); 206 | } 207 | } 208 | 209 | // Drag started from ObjectRow 210 | private void DragStarted(Object obj, bool isSelected) 211 | { 212 | DragAndDrop.SetGenericData(GetInstanceID().ToString(), true); 213 | // If path is empty (non-asset), paths array will not contain this path - probably internal property thing 214 | DragAndDrop.paths = DragAndDrop.objectReferences.Select(x => AssetDatabase.GetAssetPath(x)).ToArray(); 215 | } 216 | 217 | // Drag performed on pinned or not pinned ObjectRow 218 | private void DragPerformed(Object obj, int i, bool isPinned, ref bool shouldLimitAndOrderHistory) 219 | { 220 | var ev = Event.current; 221 | 222 | if (ev.modifiers == EventModifiers.Control) 223 | { 224 | ev.Use(); 225 | DragAndDrop.AcceptDrag(); 226 | 227 | var destinationPath = AssetDatabase.GetAssetPath(obj); 228 | if (!AssetDatabase.IsValidFolder(destinationPath)) return; 229 | MoveAssets(destinationPath, DragAndDrop.paths); 230 | } 231 | else if (isPinned) 232 | { 233 | DragAndDrop.AcceptDrag(); 234 | int k = 0; // Insert would revert order if we do not compensate 235 | foreach (var droppedObj in DragAndDrop.objectReferences) 236 | { 237 | if (!pinned.Contains(droppedObj)) AddPinned(droppedObj, i); 238 | else if (pinned.IndexOf(droppedObj) != i) 239 | { 240 | int insertIndex = pinned.IndexOf(droppedObj) > i ? i + k : i - 1; 241 | RemovePinned(droppedObj); 242 | AddPinned(droppedObj, insertIndex); 243 | } 244 | k++; 245 | } 246 | shouldLimitAndOrderHistory = true; 247 | ev.Use(); 248 | } 249 | else 250 | { 251 | var dragData = DragAndDrop.GetGenericData(GetInstanceID().ToString()); 252 | bool preventDrop = dragData is bool b && b && DragAndDrop.objectReferences.Length == 1 && 253 | DragAndDrop.objectReferences[0] == obj; // Same object row 254 | if (preventDrop) // Prevent accidental drags on itself 255 | { 256 | DragAndDrop.AcceptDrag(); 257 | ev.Use(); 258 | } 259 | else 260 | { 261 | DragAndDrop.AcceptDrag(); 262 | DropObjectToWindow(); 263 | shouldLimitAndOrderHistory = true; 264 | ev.Use(); 265 | } 266 | } 267 | } 268 | 269 | private DragAndDropVisualMode OnDragDroppedSceneHandler(Object dropUpon, Vector3 worldPosition, Vector2 viewportPosition, Transform parentForDraggedObjects, bool perform) 270 | => AddHistoryOnDragPerformed(perform); 271 | 272 | private DragAndDropVisualMode OnDragDroppedHierarchyHandler(int dropTargetInstanceID, HierarchyDropFlags dropMode, Transform parentForDraggedObjects, bool perform) 273 | => AddHistoryOnDragPerformed(perform); 274 | 275 | private DragAndDropVisualMode OnDragDroppedInspectorHandler(UnityEngine.Object[] targets, bool perform) 276 | => AddHistoryOnDragPerformed(perform); 277 | 278 | private DragAndDropVisualMode AddHistoryOnDragPerformed(bool perform) 279 | { 280 | if (!perform) return DragAndDropVisualMode.None; // Next Handler in order will handle this drag (Unity default) 281 | if (DragAndDrop.paths.Length == 0) return DragAndDropVisualMode.None; // Non-asset, e.g. making a prefab from scene object 282 | 283 | foreach (var path in DragAndDrop.paths) 284 | { 285 | var asset = AssetDatabase.LoadMainAssetAtPath(path); 286 | AddHistory(asset); 287 | LimitAndOrderHistory(); 288 | } 289 | Repaint(); 290 | return DragAndDropVisualMode.None; 291 | } 292 | 293 | private void OnDeleteKey() 294 | { 295 | RemoveAllHistory(x => Selection.objects.Contains(x)); 296 | RemoveAllPinned(x => Selection.objects.Contains(x)); 297 | LimitAndOrderHistory(); 298 | Repaint(); 299 | } 300 | 301 | protected virtual void SelectionChanged() 302 | { 303 | foreach (var guid in Selection.assetGUIDs) 304 | { 305 | var path = AssetDatabase.GUIDToAssetPath(guid); 306 | var asset = AssetDatabase.LoadMainAssetAtPath(path); 307 | AddHistory(asset); 308 | LimitAndOrderHistory(); 309 | } 310 | } 311 | 312 | [UnityEditor.ShortcutManagement.Shortcut("Assets History/Add to AssetsHistory")] 313 | public static void AddPinnedGlobal() 314 | { 315 | var windows = Resources.FindObjectsOfTypeAll(); 316 | foreach (var window in windows) 317 | { 318 | foreach (var obj in Selection.objects) 319 | { 320 | if (!window.pinned.Contains(obj)) 321 | { 322 | window.AddFilteredPinned(obj); 323 | } 324 | } 325 | window.LimitAndOrderHistory(); 326 | window.Repaint(); 327 | } 328 | } 329 | 330 | private void AssetImported(Object obj) 331 | { 332 | AddHistory(obj); 333 | LimitAndOrderHistory(); 334 | } 335 | 336 | protected virtual void SceneOpened(Scene scene, OpenSceneMode mode) 337 | { 338 | AddHistory(AssetDatabase.LoadAssetAtPath(scene.path)); 339 | LimitAndOrderHistory(); 340 | } 341 | 342 | protected virtual void AddHistory(Object obj) 343 | { 344 | AddToFront(obj, history); 345 | } 346 | 347 | protected virtual void AddPinned(Object obj, int i = -1) 348 | { 349 | if (i == -1) AddToEnd(obj, pinned); 350 | else 351 | { 352 | RemovePinned(obj); 353 | pinned.Insert(i, obj); 354 | } 355 | } 356 | 357 | protected virtual void AddFilteredPinned(Object obj) 358 | { 359 | if (IsAsset(obj)) AddPinned(obj); 360 | } 361 | 362 | protected virtual void RemoveHistory(Object obj) 363 | { 364 | history.Remove(obj); 365 | } 366 | 367 | protected virtual void RemovePinned(Object obj) 368 | { 369 | pinned.Remove(obj); 370 | } 371 | 372 | protected virtual void RemoveAllPinned(Predicate predicate) 373 | { 374 | foreach (var obj in pinned.Where(x => predicate(x)).ToList()) 375 | { 376 | RemovePinned(obj); 377 | } 378 | } 379 | 380 | protected virtual void RemoveAllHistory(Predicate predicate) 381 | { 382 | foreach (var obj in history.Where(x => predicate(x)).ToList()) 383 | { 384 | RemoveHistory(obj); 385 | } 386 | } 387 | 388 | protected void AddToFront(T obj, List list, int limit = -1) 389 | { 390 | list.Remove(obj); 391 | list.Insert(0, obj); 392 | if (limit > -1 && list.Count > limit) 393 | { 394 | for (int i = list.Count - 1; i >= limit; i--) list.RemoveAt(i); 395 | } 396 | } 397 | 398 | protected void AddToEnd(T obj, List list) 399 | { 400 | list.Remove(obj); 401 | list.Add(obj); 402 | } 403 | 404 | protected virtual void ClearAll() 405 | { 406 | history.Clear(); 407 | pinned.Clear(); 408 | LimitAndOrderHistory(); 409 | } 410 | 411 | protected virtual void ClearHistory() 412 | { 413 | history.Clear(); 414 | LimitAndOrderHistory(); 415 | } 416 | 417 | protected virtual void ClearPinned() 418 | { 419 | pinned.Clear(); 420 | LimitAndOrderHistory(); 421 | } 422 | 423 | protected virtual void LimitAndOrderHistory() 424 | { 425 | RemoveAllHistory(x => x == null); 426 | RemoveAllPinned(x => x == null); 427 | int onlyPinned = pinned.Where(x => !history.Contains(x)).Count(); 428 | int historyLimit = limit - onlyPinned; 429 | if (history.Count > historyLimit) 430 | RemoveAllHistory(x => history.IndexOf(x) >= historyLimit); 431 | //history = history.Take(historyLimit).ToList(); 432 | groupedHistory = history.Where(x => !pinned.Contains(x)) 433 | .OrderBy(x => Path.GetExtension(AssetDatabase.GetAssetPath(x))) 434 | .ThenBy(x => x.GetType().Name).ThenBy(x => x.name). 435 | ThenBy(x => x.GetInstanceID()).ToList(); 436 | groupedHistory.InsertRange(0, pinned); 437 | } 438 | 439 | protected virtual void SaveHistoryToEditorPrefs() 440 | { 441 | string pinnedPaths = string.Join("|", pinned.Select(x => AssetDatabase.GetAssetPath(x))); 442 | EditorPrefs.SetString(prefId + nameof(pinned), pinnedPaths); 443 | string historyPaths = string.Join("|", history.Select(x => AssetDatabase.GetAssetPath(x))); 444 | EditorPrefs.SetString(prefId + nameof(history), historyPaths); 445 | } 446 | 447 | protected virtual void LoadHistoryFromEditorPrefs() 448 | { 449 | string[] pinnedPaths = EditorPrefs.GetString(prefId + nameof(pinned)).Split('|'); 450 | foreach (var path in pinnedPaths) 451 | AddPinned(AssetDatabase.LoadMainAssetAtPath(path)); 452 | string[] historyPaths = EditorPrefs.GetString(prefId + nameof(history)).Split('|'); 453 | foreach (var path in historyPaths) 454 | history.Add(AssetDatabase.LoadMainAssetAtPath(path)); // Preserve order 455 | } 456 | 457 | #region Drawing 458 | private void DrawDragInsertionLine(Rect fullRect) 459 | { 460 | Rect lineRect = new Rect(fullRect.x, fullRect.y - 4, fullRect.width, 3); 461 | GUI.Label(lineRect, GUIContent.none, Styles.insertion); 462 | } 463 | #endregion 464 | } 465 | 466 | public class AssetImportHistory : AssetPostprocessor 467 | { 468 | public static Action assetImported; 469 | 470 | // Number of parameters changes in newer Unity versions 471 | static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths) 472 | { 473 | foreach (var path in importedAssets) 474 | { 475 | var asset = AssetDatabase.LoadMainAssetAtPath(path); 476 | if (asset != null) assetImported?.Invoke(asset); 477 | } 478 | } 479 | } 480 | -------------------------------------------------------------------------------- /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 | 128 | // This is faster for multiple searches e.g. in FindUnusedAssets, because Async version only does 1 search per editor Frame 129 | public static List FindObjectUsageSync(Object obj, bool filter = false, bool sort = false) 130 | { 131 | string objectPath = ""; 132 | Object asset = null; 133 | if (IsAsset(obj)) 134 | { 135 | asset = obj; 136 | objectPath = AssetDatabase.GetAssetPath(obj); 137 | } 138 | else if (IsNonAssetGameObject(obj)) 139 | { 140 | objectPath = obj.GetInstanceID().ToString(); 141 | } 142 | 143 | List resultItems = new(); 144 | var searchContext = SearchService.CreateContext(new[] { "dep", "scene", "asset", "adb" }, $"ref=\"{objectPath}\""); 145 | var results = SearchService.Request(searchContext, SearchFlags.Synchronous).Fetch() 146 | .Select(x => x.ToObject()).Where(x => x != null).ToList(); 147 | 148 | if (filter) results = FilterResults(results, asset); 149 | if (sort) results = SortResults(results); 150 | return results; 151 | } 152 | 153 | // Without IsAnyPrefabInstanceRoot, results contain every childGameobject 154 | public static List FilterResults(List results, Object asset = null) 155 | { 156 | results = results.Distinct().ToList(); 157 | if (asset != null) // Only for Assets, not for Hierarchy GameObjects 158 | results = results.Where(x => !ArePartOfSameMainAssets(x, asset)).ToList(); 159 | var filteredResults = new List(); 160 | foreach (var obj in results) 161 | { 162 | if (IsAsset(obj)) 163 | { 164 | filteredResults.Add(obj); 165 | continue; 166 | } 167 | if (IsNonAssetGameObject(obj)) 168 | { 169 | var go = obj as GameObject; 170 | if (!PrefabUtility.IsPartOfAnyPrefab(go)) 171 | { 172 | filteredResults.Add(go); 173 | continue; 174 | } 175 | if (PrefabUtility.IsAnyPrefabInstanceRoot(go)) 176 | { 177 | filteredResults.Add(go); 178 | continue; 179 | } 180 | var root = PrefabUtility.GetNearestPrefabInstanceRoot(go); 181 | if (!results.Contains(root)) 182 | filteredResults.Add(go); 183 | } 184 | } 185 | return filteredResults; 186 | } 187 | 188 | // Sort as treeView, NonAssets last 189 | public static List SortResults(List results) 190 | { 191 | var sortedResults = results.Where(x => IsAsset(x)) 192 | .OrderBy(x => AssetDatabase.GetAssetPath(x), treeViewComparer).ToList(); 193 | 194 | // NonAssets last 195 | sortedResults.AddRange(results.Where(x => !IsAsset(x))); 196 | return sortedResults; 197 | } 198 | 199 | private static void Found(ref bool finished, IList items, ref List resultItems) 200 | { 201 | resultItems = items.ToList(); 202 | finished = true; 203 | } 204 | 205 | public static async Task WaitUntil(System.Func condition, int timeout = -1) 206 | { 207 | var waitTask = Task.Run(async () => 208 | { 209 | while (!condition()) await Task.Delay(1); 210 | }); 211 | 212 | if (waitTask != await Task.WhenAny(waitTask, Task.Delay(timeout))) throw new System.TimeoutException(); 213 | } 214 | 215 | } 216 | 217 | -------------------------------------------------------------------------------- /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 |  2 | using System; 3 | using UnityEditor; 4 | using UnityEditor.SceneManagement; 5 | using UnityEngine; 6 | using UnityEngine.SceneManagement; 7 | 8 | // Modified from: http://wiki.unity3d.com/index.php?title=FindMissingScripts&oldid=17367 9 | // License: Content is available under Creative Commons Attribution Share Alike https://www.apache.org/licenses/LICENSE-2.0 10 | public class FindMissingScripts : EditorWindow 11 | { 12 | string folderPath = ""; 13 | [MenuItem("Tools/Find Missing Scripts")] 14 | public static void FindMissingScriptsShow() 15 | { 16 | EditorWindow.GetWindow(typeof(FindMissingScripts)); 17 | } 18 | 19 | static int missingCount = -1; 20 | void OnGUI() 21 | { 22 | EditorGUILayout.LabelField("Folder path from Assets. Start with /, eg.: /Prefabs"); 23 | folderPath = EditorGUILayout.TextField(folderPath); 24 | 25 | EditorGUILayout.BeginHorizontal(); 26 | { 27 | EditorGUILayout.LabelField("Missing Scripts:"); 28 | EditorGUILayout.LabelField("" + (missingCount == -1 ? "---" : missingCount.ToString())); 29 | } 30 | EditorGUILayout.EndHorizontal(); 31 | 32 | if (GUILayout.Button("Find missing scripts")) 33 | { 34 | missingCount = 0; 35 | EditorUtility.DisplayProgressBar("Searching Prefabs", "", 0.0f); 36 | 37 | string[] files = System.IO.Directory.GetFiles(Application.dataPath + folderPath, "*.prefab", System.IO.SearchOption.AllDirectories); 38 | EditorUtility.DisplayCancelableProgressBar("Searching Prefabs", "Found " + files.Length + " prefabs", 0.0f); 39 | 40 | Scene currentScene = EditorSceneManager.GetActiveScene(); 41 | string scenePath = currentScene.path; 42 | EditorSceneManager.NewScene(NewSceneSetup.EmptyScene); 43 | 44 | for (int i = 0; i < files.Length; i++) 45 | { 46 | string prefabPath = files[i].Replace(Application.dataPath, "Assets"); 47 | if (EditorUtility.DisplayCancelableProgressBar("Processing Prefabs " + i + "/" + files.Length, prefabPath, (float)i / (float)files.Length)) 48 | break; 49 | 50 | GameObject go = UnityEditor.AssetDatabase.LoadAssetAtPath(prefabPath, typeof(GameObject)) as GameObject; 51 | 52 | if (go != null) 53 | { 54 | FindInGO(go); 55 | go = null; 56 | EditorUtility.UnloadUnusedAssetsImmediate(true); 57 | } 58 | } 59 | 60 | EditorUtility.DisplayProgressBar("Cleanup", "Cleaning up", 1.0f); 61 | EditorSceneManager.OpenScene(scenePath, OpenSceneMode.Single); 62 | 63 | EditorUtility.UnloadUnusedAssetsImmediate(true); 64 | GC.Collect(); 65 | 66 | EditorUtility.ClearProgressBar(); 67 | } 68 | 69 | if (GUILayout.Button("Find missing scripts in selected GO")) 70 | { 71 | FindInSelected(); 72 | } 73 | } 74 | 75 | private static void FindInSelected() 76 | { 77 | GameObject[] go = Selection.gameObjects; 78 | missingCount = 0; 79 | foreach (GameObject g in go) 80 | { 81 | FindInSelectedGO(g); 82 | } 83 | Debug.Log(string.Format("Found {0} missing", missingCount)); 84 | } 85 | 86 | private static void FindInSelectedGO(GameObject g) 87 | { 88 | Component[] components = g.GetComponents(); 89 | for (int i = 0; i < components.Length; i++) 90 | { 91 | if (components[i] == null) 92 | { 93 | missingCount++; 94 | string s = g.name; 95 | Transform t = g.transform; 96 | while (t.parent != null) 97 | { 98 | s = t.parent.name + "/" + s; 99 | t = t.parent; 100 | } 101 | Debug.LogWarning(s + " has an empty script attached in position: " + i, g); 102 | } 103 | } 104 | // Now recurse through each child GO (if there are any): 105 | foreach (Transform childT in g.transform) 106 | { 107 | //Debug.Log("Searching " + childT.name + " " ); 108 | FindInSelectedGO(childT.gameObject); 109 | } 110 | } 111 | 112 | private static void FindInGO(GameObject go, string prefabName = "") 113 | { 114 | Component[] components = go.GetComponents(); 115 | for (int i = 0; i < components.Length; i++) 116 | { 117 | if (components[i] == null) 118 | { 119 | missingCount++; 120 | Transform t = go.transform; 121 | 122 | string componentPath = go.name; 123 | while (t.parent != null) 124 | { 125 | componentPath = t.parent.name + "/" + componentPath; 126 | t = t.parent; 127 | } 128 | Debug.LogWarning("Prefab " + prefabName + " has an empty script attached:\n" + componentPath, go); 129 | } 130 | } 131 | 132 | foreach (Transform child in go.transform) 133 | { 134 | FindInGO(child.gameObject, prefabName); 135 | } 136 | } 137 | } 138 | -------------------------------------------------------------------------------- /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/HierarchyHistory.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using UnityEditor; 3 | using UnityEditor.SceneManagement; 4 | using UnityEngine; 5 | using UnityEngine.SceneManagement; 6 | using Object = UnityEngine.Object; 7 | using System.Linq; 8 | using System; 9 | using static EditorHelper; 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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /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/Common/MyGUI.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor; 2 | using UnityEngine; 3 | using Object = UnityEngine.Object; 4 | using static EditorHelper; 5 | using System.Linq; 6 | using NUnit.Framework; 7 | using System.Collections.Generic; 8 | using System; 9 | using System.Runtime.CompilerServices; 10 | 11 | public static class MyGUI 12 | { 13 | public const int objectRowHeight = 16; 14 | public static class Styles 15 | { 16 | public static GUIStyle insertion = "TV Insertion"; 17 | public static GUIStyle lineStyle = "TV Line"; 18 | public static GUIStyle selectionStyle = "TV Selection"; 19 | public static GUIStyle pingButtonStyle; 20 | 21 | static Styles() 22 | { 23 | lineStyle = new GUIStyle(lineStyle); 24 | lineStyle.alignment = TextAnchor.MiddleLeft; 25 | lineStyle.padding.right += objectRowHeight; 26 | pingButtonStyle = new GUIStyle(GUI.skin.button); 27 | pingButtonStyle.padding = new RectOffset(2, 0, 0, 1); 28 | pingButtonStyle.alignment = TextAnchor.MiddleCenter; 29 | } 30 | } 31 | 32 | private static bool wasDoubleClick; 33 | 34 | public static (bool isHovered, bool isShortRectHovered, bool pingButtonClicked) 35 | DrawObjectRow(Rect rect, Object obj, bool isSelected, bool isPinned, string pingButtonContent = null) 36 | { 37 | var ev = Event.current; 38 | 39 | Rect shortRect = new Rect(rect.x, rect.y, rect.width - rect.height, rect.height); 40 | Rect pingButtonRect = new Rect(shortRect.xMax, shortRect.yMax - shortRect.height, shortRect.height, shortRect.height); 41 | bool isHovered = rect.Contains(ev.mousePosition); 42 | bool isShortRectHovered = shortRect.Contains(ev.mousePosition); 43 | 44 | if (ev.type == EventType.Repaint) 45 | { 46 | int height = (int)rect.height; 47 | Color oldBackGroundColor = GUI.backgroundColor; 48 | Color oldColor = GUI.contentColor; 49 | Vector2 oldIconSize = EditorGUIUtility.GetIconSize(); 50 | EditorGUIUtility.SetIconSize(new Vector2(height, height)); 51 | bool isDragged = DragAndDrop.objectReferences.Length == 1 && DragAndDrop.objectReferences.Contains(obj); 52 | 53 | if (isHovered && isSelected) GUI.backgroundColor = new Color(0.9f, 0.9f, 0.9f); 54 | if (isSelected) Styles.selectionStyle.Draw(rect, false, false, true, true); 55 | if ((isHovered || isDragged) && !isSelected) Styles.selectionStyle.Draw(rect, false, false, false, false); 56 | 57 | var style = Styles.lineStyle; 58 | var oldPadding = style.padding.right; 59 | 60 | GUIContent content = EditorGUIUtility.ObjectContent(obj, obj.GetType()); 61 | bool isAddedGameObject = false; 62 | if (IsNonAssetGameObject(obj)) 63 | { 64 | var go = (GameObject)obj; 65 | if (!go.activeInHierarchy) GUI.contentColor = Color.white * 0.694f; 66 | if (!PrefabUtility.IsAnyPrefabInstanceRoot(go)) 67 | content.image = EditorGUIUtility.IconContent("GameObject Icon").image; 68 | if (PrefabUtility.IsAddedGameObjectOverride(go)) isAddedGameObject = true; 69 | } 70 | if (isPinned) style.padding.right += height; 71 | style.Draw(rect, content, false, false, isSelected, true); 72 | GUI.contentColor = oldColor; 73 | if (isPinned) 74 | { 75 | var pinnedIconContent = EditorGUIUtility.IconContent("Favorite On Icon"); 76 | Rect pinnedIconRect = new Rect(rect.xMax - 2 * height, rect.yMax - height, height, height); 77 | EditorStyles.label.Draw(pinnedIconRect, pinnedIconContent, false, false, true, true); 78 | } 79 | if (isAddedGameObject) 80 | { 81 | var iconContent = EditorGUIUtility.IconContent("PrefabOverlayAdded Icon"); 82 | Rect iconRect = new Rect(rect.xMin, rect.yMin, height + 5, height); 83 | EditorStyles.label.Draw(iconRect, iconContent, false, false, true, true); 84 | } 85 | 86 | style.padding.right = oldPadding; 87 | EditorGUIUtility.SetIconSize(oldIconSize); 88 | GUI.backgroundColor = oldBackGroundColor; 89 | } 90 | bool pingButtonClicked = DrawPingButton(pingButtonRect, obj, pingButtonContent); 91 | return (isHovered, isShortRectHovered, pingButtonClicked); 92 | } 93 | 94 | public static bool DrawPingButton(Rect rect, Object obj, string content = null) 95 | { 96 | int height = (int)rect.height; 97 | Color oldBackgroundColor = GUI.backgroundColor; 98 | Vector2 oldIconSize = EditorGUIUtility.GetIconSize(); 99 | EditorGUIUtility.SetIconSize(new Vector2(height / 2 + 3, height / 2 + 3)); 100 | 101 | var pingButtonContent = EditorGUIUtility.IconContent("HoloLensInputModule Icon"); 102 | if (!string.IsNullOrEmpty(content)) 103 | pingButtonContent = new GUIContent(content); 104 | pingButtonContent.tooltip = AssetDatabase.GetAssetPath(obj); 105 | 106 | if (IsComponent(obj)) GUI.backgroundColor = new Color(1f, 1.5f, 1f); 107 | if (!IsAsset(obj)) pingButtonContent = EditorGUIUtility.IconContent("GameObject Icon"); 108 | 109 | bool clicked = GUI.Button(rect, pingButtonContent, Styles.pingButtonStyle); 110 | 111 | EditorGUIUtility.SetIconSize(oldIconSize); 112 | GUI.backgroundColor = oldBackgroundColor; 113 | return clicked; 114 | } 115 | 116 | private static void DrawDragInsertionLine(Rect fullRect) 117 | { 118 | Rect lineRect = new Rect(fullRect.x, fullRect.y - 4, fullRect.width, 3); 119 | GUI.Label(lineRect, GUIContent.none, Styles.insertion); 120 | } 121 | 122 | public static void KeyboardNavigation(Event ev, ref int lastSelectedIndex, List shownItems, 123 | Action deleteKey = null, Action enterKey = null, Action escapeKey = null) 124 | { 125 | if (ev.keyCode == KeyCode.DownArrow) 126 | { 127 | lastSelectedIndex = Mod(lastSelectedIndex + 1, shownItems.Count); 128 | Selection.objects = new Object[] { shownItems[lastSelectedIndex] }; 129 | ev.Use(); 130 | } 131 | else if (ev.keyCode == KeyCode.UpArrow) 132 | { 133 | lastSelectedIndex = Mod(lastSelectedIndex - 1, shownItems.Count); 134 | Selection.objects = new Object[] { shownItems[lastSelectedIndex] }; 135 | ev.Use(); 136 | } 137 | else if (ev.keyCode == KeyCode.Return || ev.keyCode == KeyCode.KeypadEnter) 138 | { 139 | var objs = shownItems.Where(x => Selection.objects.Contains(x)); 140 | foreach (var obj in objs) 141 | OpenObject(obj); 142 | enterKey?.Invoke(); 143 | ev.Use(); 144 | } 145 | else if (ev.keyCode == KeyCode.Delete) 146 | { 147 | deleteKey?.Invoke(); 148 | ev.Use(); 149 | } 150 | else if (ev.keyCode == KeyCode.Escape) 151 | { 152 | escapeKey?.Invoke(); 153 | ev.Use(); 154 | } 155 | } 156 | 157 | public static (bool isHovered, bool isShortRectHovered) 158 | ObjectRow(Rect rect, int i, Object obj, List shownItems, ref int lastSelectedIndex, 159 | string pingButtonContent = null, bool isPinned = false, Action doubleClick = null, Action middleClick = null, 160 | Action pingButtonMiddleClick = null, Action dragStarted = null, Action dragPerformed = null) 161 | { 162 | var ev = Event.current; 163 | bool isSelected = Selection.objects.Contains(obj); 164 | 165 | var buttonResult = DrawObjectRow(rect, obj, isSelected, isPinned, pingButtonContent); 166 | if (buttonResult.pingButtonClicked) 167 | { 168 | if (Event.current.button == 0) 169 | PingButtonLeftClick(obj); 170 | else if (Event.current.button == 1) 171 | PingButtonRightClick(obj); 172 | else if (Event.current.button == 2) 173 | PingButtonMiddleClick(obj, pingButtonMiddleClick); 174 | } 175 | 176 | if (buttonResult.isShortRectHovered) 177 | { 178 | if (ev.type == EventType.MouseUp && ev.button == 0 && ev.clickCount == 1) // Select on MouseUp 179 | { 180 | if (!wasDoubleClick) 181 | LeftMouseUp(obj, isSelected, i, ref lastSelectedIndex); 182 | wasDoubleClick = false; 183 | } 184 | else if (ev.type == EventType.MouseDown && ev.button == 0 && ev.clickCount == 2) 185 | { 186 | DoubleClick(obj); 187 | wasDoubleClick = true; 188 | } 189 | else if (ev.type == EventType.MouseDown && ev.button == 1) 190 | { 191 | RightClick(obj, i, ref lastSelectedIndex); 192 | 193 | } 194 | else if (ev.type == EventType.ContextClick) 195 | { 196 | ContextClick(new Rect(ev.mousePosition.x, ev.mousePosition.y, 0, 0), obj); 197 | } 198 | else if (ev.type == EventType.MouseDown && ev.button == 2) 199 | { 200 | middleClick?.Invoke(); 201 | } 202 | // Drag 203 | else if (ev.type == EventType.MouseDrag && ev.button == 0 && // Start dragging this asset 204 | DragAndDrop.visualMode == DragAndDropVisualMode.None) 205 | { 206 | DragAndDrop.PrepareStartDrag(); 207 | DragAndDrop.SetGenericData(nameof(MyEditorWindow), true); 208 | DragAndDrop.visualMode = DragAndDropVisualMode.Move; 209 | if (isSelected) 210 | DragAndDrop.objectReferences = shownItems.Where(x => Selection.objects.Contains(x)) 211 | .ToArray(); 212 | else DragAndDrop.objectReferences = new Object[] { obj }; 213 | DragAndDrop.StartDrag("MyEditorWindow Drag"); 214 | ev.Use(); 215 | dragStarted?.Invoke(); 216 | } 217 | else if (ev.type == EventType.DragUpdated && ev.button == 0) // Update drag 218 | { 219 | DragAndDrop.visualMode = DragAndDropVisualMode.Generic; 220 | GUI.Label(rect, GUIContent.none, Styles.insertion); 221 | ev.Use(); 222 | } 223 | else if (ev.type == EventType.DragPerform && ev.button == 0) // Receive drag and drop 224 | { 225 | dragPerformed?.Invoke(); 226 | } 227 | // Draw insertion line 228 | if (isPinned && DragAndDrop.visualMode != DragAndDropVisualMode.None) 229 | { 230 | if (ev.modifiers != EventModifiers.Control) // Otherwise we are trying to move asset to folder 231 | DrawDragInsertionLine(rect); 232 | } 233 | } 234 | return (buttonResult.isHovered, buttonResult.isShortRectHovered); 235 | 236 | void LeftMouseUp(Object obj, bool isSelected, int i, ref int lastSelectedIndex) 237 | { 238 | lastSelectedIndex = i; 239 | var ev = Event.current; 240 | HandleSelection(true); 241 | ev.Use(); 242 | } 243 | 244 | void DoubleClick(Object obj) 245 | { 246 | OpenObject(obj); 247 | doubleClick?.Invoke(); 248 | ev.Use(); 249 | } 250 | 251 | // This is different event then context click, bot are executed, context after right click 252 | void RightClick(Object obj, int i, ref int lastSelectedIndex) 253 | { 254 | lastSelectedIndex = i; 255 | HandleSelection(false); 256 | ev.Use(); 257 | } 258 | 259 | void ContextClick(Rect rect, Object obj) 260 | { 261 | if (IsComponent(obj)) OpenObjectContextMenu(rect, obj); 262 | else if (IsAsset(obj)) EditorUtility.DisplayPopupMenu(rect, "Assets/", null); 263 | else if (IsNonAssetGameObject(obj)) 264 | { 265 | if (Selection.transforms.Length > 0) // Just to be sure it's really a HierarchyGameobject 266 | OpenHierarchyContextMenu(Selection.transforms[0].gameObject.GetInstanceID()); 267 | } 268 | } 269 | 270 | void PingButtonLeftClick(Object obj) 271 | { 272 | if (Event.current.modifiers == EventModifiers.Alt) 273 | { 274 | string path = PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(obj); 275 | obj = AssetDatabase.LoadMainAssetAtPath(path); 276 | EditorGUIUtility.PingObject(obj); 277 | } 278 | else EditorGUIUtility.PingObject(obj); 279 | } 280 | 281 | void PingButtonRightClick(Object obj) 282 | { 283 | OpenPropertyEditor(obj); 284 | } 285 | 286 | void PingButtonMiddleClick(Object obj, Action pingButtonMiddleClick = null) 287 | { 288 | if (Event.current.modifiers == EventModifiers.Alt) 289 | Debug.Log($"{GlobalObjectId.GetGlobalObjectIdSlow(obj)} InstanceID: {obj.GetInstanceID()}"); 290 | else pingButtonMiddleClick?.Invoke(); 291 | } 292 | 293 | void HandleSelection(bool leftClick) 294 | { 295 | if (ev.modifiers == EventModifiers.Control) // Ctrl select 296 | { 297 | if (!isSelected) Selection.objects = Selection.objects.Append(obj).ToArray(); 298 | else if (leftClick) Selection.objects = Selection.objects.Where(x => x != obj).ToArray(); 299 | } 300 | else if (ev.modifiers == EventModifiers.Shift) // Shift select 301 | { 302 | int firstSelected = shownItems.FindIndex(x => Selection.objects.Contains(x)); 303 | if (firstSelected != -1) 304 | { 305 | int startIndex = Mathf.Min(firstSelected + 1, i); 306 | int count = Mathf.Abs(firstSelected - i); 307 | Selection.objects = Selection.objects. 308 | Concat(shownItems.GetRange(startIndex, count)).Distinct().ToArray(); 309 | } 310 | else Selection.objects = Selection.objects.Append(obj).ToArray(); 311 | } 312 | else if (leftClick || !isSelected) 313 | { 314 | Selection.activeObject = obj; // Ordinary select 315 | Selection.objects = new Object[] { obj }; 316 | } 317 | } 318 | } 319 | } 320 | -------------------------------------------------------------------------------- /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/EditorUtilities.cs: -------------------------------------------------------------------------------- 1 | using System; 2 | using System.Collections; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using UnityEditor; 6 | using UnityEngine; 7 | using Object = UnityEngine.Object; 8 | using Debug = UnityEngine.Debug; 9 | 10 | public class EditorUtilities 11 | { 12 | [MenuItem("Editor/Recompile Scripts _F5")] 13 | public static void RecompileScripts() 14 | { 15 | UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); 16 | } 17 | 18 | [MenuItem("Editor/Recompile Scripts Clean &F5")] 19 | public static void RecompileScriptsClean() 20 | { 21 | UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(UnityEditor.Compilation.RequestScriptCompilationOptions.CleanBuildCache); 22 | } 23 | 24 | [MenuItem("Editor/Open Editor Log")] 25 | public static void OpenEditorLog() 26 | { 27 | var localAppData = Environment.GetFolderPath(Environment.SpecialFolder.LocalApplicationData); 28 | string VSCodePath = localAppData + "/Programs/Microsoft VS Code/Code.exe"; 29 | Debug.Log(VSCodePath + " \"" + localAppData + "/Unity/Editor.log\""); 30 | ProcessStartInfo process = new ProcessStartInfo( 31 | VSCodePath, " \"" + localAppData + "/Unity/Editor/Editor.log\"") 32 | { 33 | RedirectStandardOutput = true, 34 | RedirectStandardError = true, 35 | CreateNoWindow = true, 36 | UseShellExecute = false 37 | }; 38 | Process.Start(process); 39 | } 40 | } 41 | 42 | public class EditorLoopUpdater 43 | { 44 | private static bool isLooping = false; 45 | private static string PrefId => PlayerSettings.companyName + "." + PlayerSettings.productName + ".EpsilonDelta.EditorLoop"; 46 | 47 | [InitializeOnLoadMethod] 48 | public static void LoadSetting() 49 | { 50 | isLooping = EditorPrefs.GetBool(PrefId, false); 51 | if (isLooping) EditorApplication.update += QueryUpdate; 52 | Application.runInBackground = isLooping; 53 | } 54 | 55 | [MenuItem("Editor/Loop _F7")] 56 | public static void Loop() 57 | { 58 | isLooping = !isLooping; 59 | if (isLooping) EditorApplication.update += QueryUpdate; 60 | else EditorApplication.update -= QueryUpdate; 61 | //Application.runInBackground = isLooping; // This is not necessary and changes ProjectSettings 62 | EditorPrefs.SetBool(PrefId, isLooping); 63 | } 64 | 65 | [MenuItem("Editor/Loop _F7", true)] 66 | private static bool LoopValidate() 67 | { 68 | Menu.SetChecked("Editor/Loop", isLooping); 69 | return true; 70 | } 71 | 72 | public static void QueryUpdate() 73 | { 74 | EditorApplication.QueuePlayerLoopUpdate(); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /Scripts/Editor/FileUtilities.cs: -------------------------------------------------------------------------------- 1 | using Newtonsoft.Json; 2 | using System; 3 | using System.Collections.Generic; 4 | using System.Diagnostics; 5 | using System.IO; 6 | using System.Linq; 7 | using System.Reflection; 8 | using System.Text.RegularExpressions; 9 | using UnityEditor; 10 | using UnityEditor.Callbacks; 11 | using UnityEditorInternal; 12 | using UnityEngine; 13 | using Object = UnityEngine.Object; 14 | using Debug = UnityEngine.Debug; 15 | using System.Runtime.Serialization; 16 | 17 | // Menu item shortcuts: % == ctrl, # == shift, & == alt, _ == no modifier, LEFT, RIGHT, UP, DOWN, F1..F12, HOME, END, PGUP, PGDN 18 | public class FileUtilities 19 | { 20 | [MenuItem("Assets/File/Copy GUID %#c")] 21 | public static void CopyGuid() 22 | { 23 | if (Selection.assetGUIDs.Length > 0) 24 | { 25 | string guid = Selection.assetGUIDs[0]; 26 | GUIUtility.systemCopyBuffer = guid; 27 | Debug.Log($"{AssetDatabase.GUIDToAssetPath(guid)} GUID copied to clipboard: {guid}"); 28 | } 29 | } 30 | 31 | //private static string VisualStudio2019Path = "C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/Common7/IDE/devenv.exe"; 32 | private static string VisualStudioPath = "C:/Program Files/Microsoft Visual Studio/2022/Community/Common7/IDE/devenv.exe"; 33 | 34 | [MenuItem("Assets/File/Open as Textfile")] 35 | public static void OpenAsTextfile() 36 | { 37 | foreach (string guid in Selection.assetGUIDs) 38 | { 39 | OpenAsTextfile(AssetDatabase.GUIDToAssetPath(guid)); 40 | } 41 | } 42 | 43 | public static void OpenAsTextfile(string path) 44 | { 45 | ProcessStartInfo process = new ProcessStartInfo(VisualStudioPath, "/edit \"" + path + "\"") 46 | { 47 | RedirectStandardOutput = true, 48 | RedirectStandardError = true, 49 | CreateNoWindow = true, 50 | UseShellExecute = false 51 | }; 52 | Process.Start(process); 53 | } 54 | 55 | [MenuItem("Assets/File/Open Metafile")] 56 | public static void OpenMetafile() 57 | { 58 | foreach (string guid in Selection.assetGUIDs) 59 | { 60 | OpenMetafile(AssetDatabase.GUIDToAssetPath(guid)); 61 | } 62 | } 63 | 64 | public static void OpenMetafile(string path) 65 | { 66 | ProcessStartInfo process = new ProcessStartInfo(VisualStudioPath, "/edit \"" + path + ".meta\"") 67 | { 68 | RedirectStandardOutput = true, 69 | RedirectStandardError = true, 70 | CreateNoWindow = true, 71 | UseShellExecute = false 72 | }; 73 | Process.Start(process); 74 | } 75 | 76 | [MenuItem("Assets/File/Serialize Class")] 77 | public static void SerializeClass() 78 | { 79 | if (Selection.assetGUIDs.Length == 0) return; 80 | var path = AssetDatabase.GUIDToAssetPath(Selection.assetGUIDs[0]); 81 | var loadedScript = AssetDatabase.LoadAssetAtPath(path); 82 | if (loadedScript == null) return; 83 | var type = loadedScript.GetClass(); 84 | var instance = FormatterServices.GetUninitializedObject(type); 85 | Debug.Log(JsonConvert.SerializeObject(instance)); 86 | } 87 | 88 | private static string GExtensionsPath = "C:/Program Files (x86)/GitExtensions/GitExtensions.exe"; 89 | 90 | [MenuItem("Assets/File/File History GE %&h")] 91 | public static void FileHistoryGitExtensions() 92 | { 93 | foreach (string guid in Selection.assetGUIDs) 94 | { 95 | string path = AssetDatabase.GUIDToAssetPath(guid); 96 | // Remove "Assets" at the end of Application.dataPath, because asset contains Assets or Packages at the beginning 97 | path = Application.dataPath.Substring(0, Application.dataPath.Length - 6) + path; 98 | Debug.Log(path); 99 | FileHistoryGitExtensions(path); 100 | } 101 | } 102 | 103 | [MenuItem("Assets/File/Meta File History GE #&h")] 104 | public static void MetaFileHistoryGitExtensions() 105 | { 106 | foreach (string guid in Selection.assetGUIDs) 107 | { 108 | string path = AssetDatabase.GUIDToAssetPath(guid); 109 | // Remove "Assets" at the end of Application.dataPath, because asset contains Assets or Packages at the beginning 110 | path = Application.dataPath.Substring(0, Application.dataPath.Length - 6) + path + ".meta"; 111 | Debug.Log(path); 112 | FileHistoryGitExtensions(path); 113 | } 114 | } 115 | 116 | public static void FileHistoryGitExtensions(string path) 117 | { 118 | ProcessStartInfo process = new ProcessStartInfo(GExtensionsPath, " filehistory \"" + path + "\"") 119 | { 120 | RedirectStandardOutput = true, 121 | RedirectStandardError = true, 122 | CreateNoWindow = true, 123 | UseShellExecute = false 124 | }; 125 | Process.Start(process); 126 | } 127 | 128 | private static string GIMPBinFolderPath = "C:/Program Files/GIMP 2/bin/"; 129 | private static string GIMPPath = ""; 130 | 131 | [MenuItem("Assets/File/Open In GIMP")] 132 | public static void OpenInGimp() 133 | { 134 | foreach (string guid in Selection.assetGUIDs) 135 | { 136 | OpenInGimp(AssetDatabase.GUIDToAssetPath(guid)); 137 | } 138 | } 139 | 140 | public static void OpenInGimp(string path) 141 | { 142 | if (string.IsNullOrEmpty(GIMPPath)) 143 | { 144 | GIMPPath = Directory.GetFiles(GIMPBinFolderPath, "*.exe").FirstOrDefault(x => Regex.IsMatch(x, @"gimp-[0-9]+")); 145 | if (string.IsNullOrEmpty(GIMPPath)) return; 146 | } 147 | ProcessStartInfo process = new ProcessStartInfo(GIMPPath, "\"" + path + "\"") 148 | { 149 | RedirectStandardOutput = true, 150 | RedirectStandardError = true, 151 | CreateNoWindow = true, 152 | UseShellExecute = false 153 | }; 154 | Process.Start(process); 155 | } 156 | 157 | private static string BlenderFolderPath = "C:/Program Files/Blender Foundation/"; 158 | private static string BlenderPath = ""; 159 | 160 | [MenuItem("Assets/File/Open FBX in Blender")] 161 | public static void OpenFBXInBlender() 162 | { 163 | foreach (string guid in Selection.assetGUIDs) 164 | { 165 | OpenFBXInBlender(AssetDatabase.GUIDToAssetPath(guid)); 166 | } 167 | } 168 | 169 | // Inspired by https://blog.kikicode.com/2018/12/double-click-fbx-files-to-import-to.html 170 | public static void OpenFBXInBlender(string path) 171 | { 172 | if (string.IsNullOrEmpty(BlenderPath)) 173 | { 174 | BlenderPath = Directory.GetDirectories(BlenderFolderPath).LastOrDefault() + "/blender.exe"; 175 | if (string.IsNullOrEmpty(BlenderPath)) return; 176 | } 177 | 178 | // r'pathstring' - the parameter r means literal string 179 | ProcessStartInfo process = new ProcessStartInfo(BlenderPath, " --python-expr \"import bpy; bpy.context.preferences.view.show_splash = False; bpy.ops.import_scene.fbx(filepath = r'" + path + "'); \"") 180 | { 181 | RedirectStandardOutput = true, 182 | RedirectStandardError = true, 183 | CreateNoWindow = true, 184 | UseShellExecute = false 185 | }; 186 | Process.Start(process); 187 | } 188 | 189 | private static string AudacityPath = "C:/Program Files/Audacity/Audacity.exe"; 190 | 191 | [MenuItem("Assets/File/Open AudioFile in Audacity")] 192 | public static void OpenInAudacity() 193 | { 194 | foreach (string guid in Selection.assetGUIDs) 195 | { 196 | OpenInAudacity(AssetDatabase.GUIDToAssetPath(guid)); 197 | } 198 | } 199 | 200 | public static void OpenInAudacity(string path) 201 | { 202 | ProcessStartInfo process = new ProcessStartInfo(AudacityPath, "\"" + path + "\"") 203 | { 204 | RedirectStandardOutput = true, 205 | RedirectStandardError = true, 206 | CreateNoWindow = true, 207 | UseShellExecute = false 208 | }; 209 | Process.Start(process); 210 | } 211 | 212 | private static string CygwinPath = "C:/cygwin64/bin/mintty.exe"; 213 | [MenuItem("Assets/File/Open Cygwin here")] 214 | public static void OpenCygwinHere() 215 | { 216 | foreach (string guid in Selection.assetGUIDs) 217 | { 218 | string path = AssetDatabase.GUIDToAssetPath(guid); 219 | if (!AssetDatabase.IsValidFolder(path)) 220 | { 221 | path = path.Substring(0, path.LastIndexOf('/') + 1); 222 | } 223 | path = "\"" + Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length) + path + "\""; 224 | UnityEngine.Debug.Log($"Opening Cygwin in: {path}"); 225 | ProcessStartInfo process = new ProcessStartInfo(CygwinPath, "/bin/sh -lc 'cd " + path + "; exec bash'") 226 | { 227 | RedirectStandardOutput = true, 228 | RedirectStandardError = true, 229 | CreateNoWindow = true, 230 | UseShellExecute = false 231 | }; 232 | Process.Start(process); 233 | } 234 | } 235 | 236 | [OnOpenAsset(0)] 237 | public static bool OnOpenWithModifiers(int instanceID, int line) 238 | { 239 | if (Event.current == null) return false; 240 | if (Event.current.modifiers == EventModifiers.None) return false; 241 | if (Event.current.modifiers == EventModifiers.Alt) 242 | { 243 | OpenAsTextfile(AssetDatabase.GetAssetPath(EditorUtility.InstanceIDToObject(instanceID))); 244 | return true; 245 | } 246 | else if (Event.current.modifiers == EventModifiers.Shift) 247 | { 248 | OpenMetafile(AssetDatabase.GetAssetPath(EditorUtility.InstanceIDToObject(instanceID))); 249 | return true; 250 | } 251 | else if (Event.current.modifiers == (EventModifiers.Alt | EventModifiers.Command)) // Command == Windows key 252 | { 253 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 254 | string assetPath = AssetDatabase.GetAssetPath(asset); 255 | EditorUtility.RevealInFinder(assetPath); 256 | return true; 257 | } 258 | else return false; 259 | } 260 | 261 | [OnOpenAsset(1)] 262 | public static bool OnOpenFolder(int instanceID, int line) 263 | { 264 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 265 | string assetPath = AssetDatabase.GetAssetPath(asset); 266 | if (AssetDatabase.IsValidFolder(assetPath)) 267 | { 268 | assetPath = "\"" + Application.dataPath.Substring(0, Application.dataPath.Length - "Assets".Length) + assetPath + "\""; 269 | assetPath = assetPath.Replace('/', '\\'); 270 | ProcessStartInfo process = new ProcessStartInfo("explorer.exe", assetPath) 271 | { 272 | RedirectStandardOutput = true, 273 | RedirectStandardError = true, 274 | CreateNoWindow = true, 275 | UseShellExecute = false 276 | }; 277 | Process.Start(process); 278 | return true; 279 | } 280 | else return false; 281 | } 282 | 283 | [OnOpenAsset(2)] 284 | public static bool OnOpenFBX(int instanceID, int line) 285 | { 286 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 287 | string assetPath = AssetDatabase.GetAssetPath(asset); 288 | if (assetPath.EndsWith(".fbx", StringComparison.OrdinalIgnoreCase)) 289 | { 290 | OpenFBXInBlender(assetPath); 291 | return true; 292 | } 293 | else return false; 294 | } 295 | 296 | [OnOpenAsset(3)] 297 | public static bool OnOpenImage(int instanceID, int line) 298 | { 299 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 300 | string assetPath = AssetDatabase.GetAssetPath(asset); 301 | if (Regex.IsMatch(assetPath, @".*\.png$|.*\.jpg$|.*\.jpeg$|.*\.bmp$|.*\.psd$|.*\.psb$", RegexOptions.IgnoreCase)) 302 | { 303 | OpenInGimp(assetPath); 304 | return true; 305 | } 306 | else return false; 307 | } 308 | 309 | [OnOpenAsset(4)] 310 | public static bool OnOpenAudio(int instanceID, int line) 311 | { 312 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 313 | string assetPath = AssetDatabase.GetAssetPath(asset); 314 | if (Regex.IsMatch(assetPath, @".*\.wav$|.*\.flac$|.*\.ogg$|.*\.mp3$|.*\.mp4$|.*\.aiff$", RegexOptions.IgnoreCase)) 315 | { 316 | OpenInAudacity(assetPath); 317 | return true; 318 | } 319 | else return false; 320 | } 321 | 322 | [OnOpenAsset(5)] 323 | public static bool OnOpenText(int instanceID, int line) 324 | { 325 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 326 | string assetPath = AssetDatabase.GetAssetPath(asset); 327 | // Last expression of the regex is for files without '.' in the name == no file extension 328 | if (Regex.IsMatch(assetPath, @".*\.txt$|.*\.json$|.*\.md$|.*\.java$|.*\.mm$|^([^.]+)$", RegexOptions.IgnoreCase)) 329 | { 330 | OpenAsTextfile(assetPath); 331 | return true; 332 | } 333 | else return false; 334 | } 335 | 336 | // This should fold out/in folders on double click 337 | // Does not work well in two columns layout 338 | //[OnOpenAsset(1)] 339 | public static bool OnOpenFolder2(int instanceID, int line) 340 | { 341 | Object asset = EditorUtility.InstanceIDToObject(instanceID); 342 | string assetPath = AssetDatabase.GetAssetPath(asset); 343 | if (AssetDatabase.IsValidFolder(assetPath)) 344 | { 345 | int[] expandedFolders = InternalEditorUtility.expandedProjectWindowItems; 346 | bool isExpanded = expandedFolders.Contains(instanceID); 347 | ExpandFolder(instanceID, !isExpanded); 348 | return true; 349 | } 350 | else return false; 351 | } 352 | 353 | public static void ExpandFolder(int instanceID, bool expand) 354 | { 355 | int[] expandedFolders = InternalEditorUtility.expandedProjectWindowItems; 356 | bool isExpanded = expandedFolders.Contains(instanceID); 357 | if (expand == isExpanded) return; 358 | 359 | var unityEditorAssembly = Assembly.GetAssembly(typeof(Editor)); 360 | var projectBrowserType = unityEditorAssembly.GetType("UnityEditor.ProjectBrowser"); 361 | var projectBrowsers = Resources.FindObjectsOfTypeAll(projectBrowserType); 362 | 363 | foreach (var p in projectBrowsers) 364 | { 365 | var treeViewControllerType = unityEditorAssembly.GetType("UnityEditor.IMGUI.Controls.TreeViewController"); 366 | FieldInfo treeViewControllerField = 367 | projectBrowserType.GetField("m_AssetTree", BindingFlags.Instance | BindingFlags.NonPublic); 368 | var treeViewController = treeViewControllerField.GetValue(p); 369 | // For two columns layout 370 | //if (treeViewController == null) 371 | //{ 372 | // treeViewControllerField = 373 | // projectBrowserType.GetField("m_FolderTree", BindingFlags.Instance | BindingFlags.NonPublic); 374 | // treeViewController = treeViewControllerField.GetValue(p); 375 | //} 376 | if (treeViewController == null) continue; 377 | var changeGoldingMethod = 378 | treeViewControllerType.GetMethod("ChangeFolding", BindingFlags.Instance | BindingFlags.NonPublic); 379 | changeGoldingMethod.Invoke(treeViewController, new object[] { new int[] { instanceID }, expand }); 380 | EditorWindow pw = (EditorWindow)p as EditorWindow; 381 | pw.Repaint(); 382 | } 383 | } 384 | } 385 | -------------------------------------------------------------------------------- /Scripts/Editor/HierarchyUtilities.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor.Presets; 2 | using UnityEditor; 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/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 | [MenuItem("CONTEXT/RectTransform/Corners to Anchors")] 39 | public static void CornersToAnchors(MenuCommand command) 40 | { 41 | if (Selection.transforms == null || Selection.transforms.Length == 0) 42 | return; 43 | 44 | Undo.IncrementCurrentGroup(); 45 | Undo.SetCurrentGroupName("CornersToAnchors"); 46 | var undoGroup = Undo.GetCurrentGroup(); 47 | 48 | foreach (Transform transform in Selection.transforms) 49 | { 50 | RectTransform t = transform as RectTransform; 51 | if (t == null) continue; 52 | 53 | Undo.RecordObject(t, "CornersToAnchors"); 54 | t.offsetMin = t.offsetMax = new Vector2(0, 0); 55 | } 56 | Undo.CollapseUndoOperations(undoGroup); 57 | } 58 | 59 | [Shortcut("Anchors To Corners", KeyCode.T, ShortcutModifiers.Alt)] 60 | public static void AnchorsToCornersGlobal() 61 | { 62 | AnchorsToCorners(null); 63 | } 64 | 65 | [Shortcut("MakeScreenshot", KeyCode.R, ShortcutModifiers.Alt, displayName = "Make ScreenShot")] 66 | public static void Screenshot() 67 | { 68 | Screenshot(null); 69 | } 70 | 71 | [MenuItem("CONTEXT/Camera/Screenshot")] 72 | public static void Screenshot(MenuCommand command) 73 | { 74 | if (!AssetDatabase.IsValidFolder("Assets/Screenshots")) 75 | { 76 | AssetDatabase.CreateFolder("Assets", "Screenshots"); 77 | } 78 | var path = $"Assets/Screenshots/Screenshot_{DateTime.Now:yyyy-MM-dd-HH_mm_ss}.png"; 79 | ScreenCapture.CaptureScreenshot(path); 80 | var timerStart = DateTime.Now; 81 | EditorApplication.update += Refresh; 82 | 83 | void Refresh() 84 | { 85 | if (timerStart.AddSeconds(0.5f) < DateTime.Now) 86 | { 87 | EditorApplication.update -= Refresh; 88 | AssetDatabase.ImportAsset(path); 89 | } 90 | } 91 | } 92 | } -------------------------------------------------------------------------------- /Scripts/Editor/MyShortcuts.cs: -------------------------------------------------------------------------------- 1 | using UnityEditor.ShortcutManagement; 2 | using UnityEditor; 3 | using UnityEngine; 4 | using System.Reflection; 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/OpenScenes.cs: -------------------------------------------------------------------------------- 1 | using System.Collections.Generic; 2 | using System.IO; 3 | using System.Linq; 4 | using UnityEditor; 5 | using UnityEditor.SceneManagement; 6 | using UnityEngine; 7 | using static EditorHelper; 8 | using static MyGUI; 9 | 10 | public class OpenScenes : MyEditorWindow 11 | { 12 | private static TreeViewComparer treeViewComparer = new(); 13 | private Vector2 scroll; 14 | private int lastSelectedIndex = -1; 15 | private List sceneAssets = new(); 16 | private bool adjustSize = true; 17 | 18 | [MenuItem("File/Open Scenes... %o", false, 160)] 19 | protected static void CreateWindow() 20 | { 21 | var window = GetWindow(false, "Open Scenes"); 22 | window.autoRepaintOnSceneChange = true; 23 | window.minSize = new Vector2(100, 40); 24 | var scenePaths = AssetDatabase.FindAssets("t:scene", new string[] { "Assets" }) 25 | .Select(x => AssetDatabase.GUIDToAssetPath(x)).OrderBy(x => x, treeViewComparer); 26 | window.sceneAssets = scenePaths.Select(x => AssetDatabase.LoadMainAssetAtPath(x)).ToList(); 27 | } 28 | 29 | private void OnEnable() 30 | { 31 | wantsMouseEnterLeaveWindow = true; 32 | wantsMouseMove = true; 33 | } 34 | 35 | protected virtual void OnGUI() 36 | { 37 | var ev = Event.current; 38 | if (ev.type == EventType.MouseMove) Repaint(); 39 | if (ev.type == EventType.KeyDown) KeyboardNavigation( 40 | ev, ref lastSelectedIndex, sceneAssets, enterKey: OnEnterKey, escapeKey: OnEscapeKey); 41 | 42 | bool isAnyHover = false; 43 | scroll = EditorGUILayout.BeginScrollView(scroll); 44 | for (int i = 0; i < sceneAssets.Count; i++) 45 | { 46 | var obj = sceneAssets[i]; 47 | if (obj == null) continue; 48 | 49 | var guiStyle = new GUIStyle(); guiStyle.margin = new RectOffset(); 50 | Rect rect = EditorGUILayout.GetControlRect(false, objectRowHeight, guiStyle); 51 | var buttonResult = ObjectRow(rect, i, obj, sceneAssets, ref lastSelectedIndex, doubleClick: OnEnterKey); 52 | if (buttonResult.isHovered) { isAnyHover = true; hoverObject = obj; } 53 | } 54 | if (!isAnyHover) hoverObject = null; 55 | EditorGUILayout.EndScrollView(); 56 | if (adjustSize) 57 | { 58 | float height = sceneAssets.Count * objectRowHeight; 59 | float windowHeight = Mathf.Min(height, 1200f); 60 | position = new Rect(position.position, 61 | new Vector2(position.width, windowHeight)); 62 | adjustSize = false; 63 | } 64 | } 65 | 66 | private void OnEnterKey() 67 | { 68 | if (!docked) Close(); 69 | } 70 | 71 | //protected virtual void OpenScene(string scenePath, EventModifiers eventModifiers) 72 | //{ 73 | // if (EditorApplication.isPlaying || string.IsNullOrEmpty(scenePath)) return; 74 | 75 | // if (EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) 76 | // { 77 | // bool additive = eventModifiers == EventModifiers.Control; 78 | // var sceneMode = additive ? OpenSceneMode.Additive : OpenSceneMode.Single; 79 | // EditorSceneManager.OpenScene(scenePath, sceneMode); 80 | // Repaint(); 81 | // UnityEditorInternal.InternalEditorUtility.RepaintAllViews(); 82 | // } 83 | 84 | // if (!docked) Close(); 85 | //} 86 | } 87 | -------------------------------------------------------------------------------- /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/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/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 | } -------------------------------------------------------------------------------- /_config.yml: -------------------------------------------------------------------------------- 1 | theme: jekyll-theme-hacker --------------------------------------------------------------------------------