#if UNITY_EDITOR using System.Collections; using System.Collections.Generic; using System.IO; using System.Linq; using System.Reflection; using UnityEngine; using UnityEditor; using UnityEditor.ShortcutManagement; using UnityEngine.UIElements; using UnityEngine.SceneManagement; using UnityEditor.SceneManagement; using UnityEditor.IMGUI.Controls; using Type = System.Type; using static VHierarchy.Libs.VUtils; using static VHierarchy.Libs.VGUI; // using static VTools.VDebug; using static VHierarchy.VHierarchyData; using static VHierarchy.VHierarchyCache; #if UNITY_6000_2_OR_NEWER using TreeViewItem = UnityEditor.IMGUI.Controls.TreeViewItem; using TreeViewState = UnityEditor.IMGUI.Controls.TreeViewState; #endif namespace VHierarchy { public static class VHierarchy { static void WrappedGUI(EditorWindow window) { var navbarHeight = 26; void navbarGui() { if (!navbars_byWindow.ContainsKey(window)) navbars_byWindow[window] = new VHierarchyNavbar(window); var navbarRect = window.position.SetPos(0, 0).SetHeight(navbarHeight); navbars_byWindow[window].OnGUI(navbarRect); } void defaultGuiWithOffset() { var defaultTopBarHeight = 20; var topOffset = navbarHeight - defaultTopBarHeight; var m_Pos_original = window.GetFieldValue("m_Pos"); GUI.BeginGroup(m_Pos_original.SetPos(0, 0).AddHeightFromBottom(-topOffset)); window.SetFieldValue("m_Pos", m_Pos_original.AddHeightFromBottom(-topOffset)); try { if (curEvent.isMouseDown && m_Pos_original.IsHovered()) t_SceneHierarchyWindow.SetMemberValue("s_LastInteractedHierarchy", window); window.InvokeMethod("DoSceneHierarchy"); window.InvokeMethod("ExecuteCommands"); // same as SceneHierarchyWindow.OnGUI() but without DoToolbarLayout(): } catch (System.Exception exception) { if (exception.InnerException is ExitGUIException) throw exception.InnerException; else throw exception; // GUIUtility.ExitGUI() works by throwing ExitGUIException, which just exits imgui loop and doesn't appear in console // but if ExitGUI is called from a reflected method (DoSceneHierarchy in this case), the exception becomes TargetInvokationException // which gets logged to console (only if debugger is attached, for some reason) // so here in such cases we rethrow the original ExitGUIException } window.SetFieldValue("m_Pos", m_Pos_original); GUI.EndGroup(); } void shadow() { if (!curEvent.isRepaint) return; var shadowLength = 30; var shadowPos = 21; var shadowGreyscale = isDarkTheme ? .1f : .28f; var shadowAlpha = isDarkTheme ? .35f : .15f; var minScrollPos = 10; var maxScrollPos = 20; if (StageUtility.GetCurrentStage() is PrefabStage) shadowPos += 30; else if (EditorSceneManager.loadedRootSceneCount > 1) shadowPos += 16; var scrollPos = window.GetMemberValue("m_SceneHierarchy").GetMemberValue("m_TreeViewState").scrollPos.y; if (scrollPos <= minScrollPos) return; var opacity = ((scrollPos - minScrollPos) / (maxScrollPos - minScrollPos)).Clamp01(); var rectWidth = window.position.width;// - 12; var rect = window.position.SetPos(0, 0).MoveY(shadowPos).SetHeight(shadowLength).SetWidth(rectWidth); var clipAtY = navbarHeight + 1; if (EditorSceneManager.loadedRootSceneCount > 1) clipAtY += 16; GUI.BeginClip(window.position.SetPos(0, clipAtY)); rect.MoveY(-clipAtY).DrawCurtainDown(Greyscale(shadowGreyscale, shadowAlpha * opacity)); GUI.EndClip(); } var doNavbarFirst = navbars_byWindow.ContainsKey(window) && navbars_byWindow[window].isSearchActive; if (doNavbarFirst) navbarGui(); defaultGuiWithOffset(); shadow(); if (!doNavbarFirst) navbarGui(); } static Dictionary navbars_byWindow = new(); static void UpdateGUIWrapping(EditorWindow window) { if (!window.hasFocus) return; var curOnGUIMethod = window.GetMemberValue("m_Parent").GetMemberValue("m_OnGUI").Method; var isWrapped = curOnGUIMethod == mi_WrappedGUI; var shouldBeWrapped = VHierarchyMenu.navigationBarEnabled; void wrap() { var hostView = window.GetMemberValue("m_Parent"); var newDelegate = typeof(VHierarchy).GetMethod(nameof(WrappedGUI), maxBindingFlags).CreateDelegate(t_EditorWindowDelegate, window); hostView.SetMemberValue("m_OnGUI", newDelegate); window.Repaint(); } void unwrap() { var hostView = window.GetMemberValue("m_Parent"); var originalDelegate = hostView.InvokeMethod("CreateDelegate", "OnGUI"); hostView.SetMemberValue("m_OnGUI", originalDelegate); window.Repaint(); } if (shouldBeWrapped && !isWrapped) wrap(); if (!shouldBeWrapped && isWrapped) unwrap(); } static void UpdateGUIWrappingForAllHierarchies() => allHierarchies.ForEach(r => UpdateGUIWrapping(r)); static void OnDomainReloaded() => toCallInGUI += UpdateGUIWrappingForAllHierarchies; static void OnWindowUnmaximized() => UpdateGUIWrappingForAllHierarchies(); static void OnHierarchyFocused() => UpdateGUIWrapping(EditorWindow.focusedWindow); static void OnDelayCall() => UpdateGUIWrappingForAllHierarchies(); static void CheckIfFocusedWindowChanged() { if (prevFocusedWindow != EditorWindow.focusedWindow) if (EditorWindow.focusedWindow?.GetType() == t_SceneHierarchyWindow) OnHierarchyFocused(); prevFocusedWindow = EditorWindow.focusedWindow; } static EditorWindow prevFocusedWindow; static void CheckIfWindowWasUnmaximized() { var isMaximized = EditorWindow.focusedWindow?.maximized == true; if (!isMaximized && wasMaximized) OnWindowUnmaximized(); wasMaximized = isMaximized; } static bool wasMaximized; static void OnSomeGUI() { toCallInGUI?.Invoke(); toCallInGUI = null; CheckIfFocusedWindowChanged(); } static void ProjectWindowItemOnGUI(string _, Rect __) => OnSomeGUI(); static void HierarchyWindowItemOnGUI(int _, Rect __) => OnSomeGUI(); static System.Action toCallInGUI; static void DelayCallLoop() { OnDelayCall(); EditorApplication.delayCall -= DelayCallLoop; EditorApplication.delayCall += DelayCallLoop; } static void RowGUI(int instanceId, Rect rowRect) { EditorWindow window; void findWindow() { if (allHierarchies.Count() == 1) { window = allHierarchies.First(); return; } var pointInsideWindow = EditorGUIUtility.GUIToScreenPoint(rowRect.center); window = allHierarchies.FirstOrDefault(r => r.position.AddHeight(30).Contains(pointInsideWindow) && r.hasFocus); } void updateWindow() { if (!window) return; // happens on half-visible rows during expand animation if (curEvent.isLayout && !lastEventWasLayout) UpdateWindow(window); lastEventWasLayout = curEvent.isLayout; } void catchScrollInputForController() { if (!window) return; if (!controllers_byWindow.ContainsKey(window)) return; if (curEvent.isScroll) controllers_byWindow[window].animatingScroll = false; } void callGUI() { if (!window) return; if (!guis_byWindow.ContainsKey(window)) return; var gui = guis_byWindow[window]; if (EditorUtility.InstanceIDToObject(instanceId) is GameObject go) gui.RowGUI_GameObject(rowRect, go); else for (int i = 0; i < EditorSceneManager.sceneCount; i++) if (EditorSceneManager.GetSceneAt(i).GetHashCode() == instanceId) gui.RowGUI_Scene(rowRect, EditorSceneManager.GetSceneAt(i)); } findWindow(); updateWindow(); catchScrollInputForController(); callGUI(); } static bool lastEventWasLayout; static void UpdateWindow(EditorWindow window) { if (!guis_byWindow.TryGetValue(window, out var gui)) gui = guis_byWindow[window] = new(window); if (!controllers_byWindow.TryGetValue(window, out var controller)) controller = controllers_byWindow[window] = new(window); gui.UpdateState(); controller.UpdateState(); controller.UpdateExpandQueue(); controller.UpdateScrollAnimation(); controller.UpdateHighlightAnimation(); } public static Dictionary guis_byWindow = new(); public static Dictionary controllers_byWindow = new(); public static Texture GetComponentIcon(Component component) { if (!component) return null; if (!componentIcons_byType.ContainsKey(component.GetType())) componentIcons_byType[component.GetType()] = EditorGUIUtility.ObjectContent(component, component.GetType()).image; return componentIcons_byType[component.GetType()]; } static Dictionary componentIcons_byType = new(); static Texture2D GetIcon_forVTabs(GameObject gameObject) { var goData = GetGameObjectData(gameObject, false); if (goData == null) return null; var iconNameOrPath = goData.iconNameOrGuid.Length == 32 ? goData.iconNameOrGuid.ToPath() : goData.iconNameOrGuid; if (!iconNameOrPath.IsNullOrEmpty()) return EditorIcons.GetIcon(iconNameOrPath); return null; } static string GetIconName_forVFavorites(GameObject gameObject) { var goData = GetGameObjectData(gameObject, false); if (goData == null) return ""; var iconNameOrPath = goData.iconNameOrGuid.Length == 32 ? goData.iconNameOrGuid.ToPath() : goData.iconNameOrGuid; return iconNameOrPath; } static string GetIconName_forVInspector(GameObject gameObject) { return GetIconName_forVFavorites(gameObject); } public static void SetIcon(GameObject gameObject, string iconName, bool recursive = false) { goDataCache.Clear(); var goData = GetGameObjectData(gameObject, createDataIfDoesntExist: true); goData.iconNameOrGuid = iconName ?? ""; goData.isIconRecursive = recursive; goInfoCache.Clear(); EditorApplication.RepaintHierarchyWindow(); } public static void SetColor(GameObject gameObject, int colorIndex, bool recursive = false) { goDataCache.Clear(); var goData = GetGameObjectData(gameObject, createDataIfDoesntExist: true); goData.colorIndex = colorIndex; goData.isColorRecursive = recursive; goInfoCache.Clear(); EditorApplication.RepaintHierarchyWindow(); } static void Shortcuts() { if (!curEvent.isKeyDown) return; if (curEvent.keyCode == KeyCode.None) return; if (EditorWindow.mouseOverWindow is not EditorWindow hoveredWindow) return; if (hoveredWindow?.GetType() != t_SceneHierarchyWindow) return; void toggleExpanded() { if (!curEvent.isKeyDown) return; if (curEvent.keyCode != KeyCode.E) return; if (curEvent.holdingAnyModifierKey) return; if (!VHierarchyMenu.toggleExpandedEnabled) return; if (Tools.viewTool == ViewTool.FPS) return; if (hoveredGo == null && hoveredScene == default) return; curEvent.Use(); if (transformToolNeedsReset = Application.unityVersion.Contains("2022")) previousTransformTool = Tools.current; if (hoveredScene == default) if (hoveredGo.transform.childCount == 0) return; if (hoveredScene != default) controllers_byWindow[hoveredWindow].ToggleExpanded(hoveredScene.handle); else controllers_byWindow[hoveredWindow].ToggleExpanded(hoveredGo.GetInstanceID()); } void collapseAll() { if (curEvent.modifiers != (EventModifiers.Shift | EventModifiers.Command) && curEvent.modifiers != (EventModifiers.Shift | EventModifiers.Control)) return; if (!curEvent.isKeyDown || curEvent.keyCode != KeyCode.E) return; if (!VHierarchyMenu.collapseEverythingEnabled) return; curEvent.Use(); controllers_byWindow[hoveredWindow].CollapseAll(); } void isolate() { if (!curEvent.isKeyDown) return; if (curEvent.keyCode != KeyCode.E) return; if (curEvent.modifiers != EventModifiers.Shift) return; if (!VHierarchyMenu.isolateEnabled) return; if (hoveredGo == null && hoveredScene == default) return; curEvent.Use(); if (hoveredGo && hoveredGo.transform.childCount == 0) return; if (!hoveredGo && hoveredScene.rootCount == 0) return; if (hoveredScene != default) controllers_byWindow[hoveredWindow].Isolate(hoveredScene.handle); else controllers_byWindow[hoveredWindow].Isolate(hoveredGo.GetInstanceID()); } void toggleActive() { if (!hoveredGo) return; if (curEvent.holdingAnyModifierKey) return; if (!curEvent.isKeyDown || curEvent.keyCode != KeyCode.A) return; if (Tools.viewTool == ViewTool.FPS) return; if (!VHierarchyMenu.toggleActiveEnabled) return; curEvent.Use(); var gos = Selection.gameObjects.Contains(hoveredGo) ? Selection.gameObjects : new[] { hoveredGo }; var active = !gos.Any(r => r.activeSelf); foreach (var r in gos) { r.RecordUndo(); r.SetActive(active); } } void delete() { if (!hoveredGo) return; if (curEvent.holdingAnyModifierKey) return; if (!curEvent.isKeyDown || curEvent.keyCode != KeyCode.X) return; if (!VHierarchyMenu.deleteEnabled) return; var gos = Selection.gameObjects.Contains(hoveredGo) ? Selection.gameObjects : new[] { hoveredGo }; foreach (var r in gos) Undo.DestroyObjectImmediate(r); curEvent.Use(); } void focus() { if (!curEvent.isKeyDown) return; if (curEvent.modifiers != EventModifiers.None) return; if (curEvent.keyCode != KeyCode.F) return; if (SceneView.sceneViews.Count == 0) return; if (!hoveredGo) return; if (!VHierarchyMenu.focusEnabled) return; var sv = SceneView.lastActiveSceneView; if (!sv || !sv.hasFocus) sv = SceneView.sceneViews.ToArray().FirstOrDefault(r => (r as SceneView).hasFocus) as SceneView; if (!sv) (sv = SceneView.lastActiveSceneView ?? SceneView.sceneViews[0] as SceneView).Focus(); sv.Frame(hoveredGo.GetBounds(), false); } void setDefaultParent() { if (!curEvent.isKeyDown) return; if (curEvent.modifiers != EventModifiers.None) return; if (curEvent.keyCode != KeyCode.D) return; if (!hoveredGo) return; if (!VHierarchyMenu.setDefaultParentEnabled) return; var isDefaultParentHovered = hoveredGo == typeof(SceneView).InvokeMethod("GetDefaultParentObjectIfSet")?.gameObject; if (isDefaultParentHovered) EditorUtility.ClearDefaultParentObject(); else EditorUtility.SetDefaultParentObject(hoveredGo); hoveredWindow.Repaint(); curEvent.Use(); } void resetDefaultParent() { if (!curEvent.isKeyDown) return; if (curEvent.modifiers != EventModifiers.Shift) return; if (curEvent.keyCode != KeyCode.D) return; if (!VHierarchyMenu.setDefaultParentEnabled) return; EditorUtility.ClearDefaultParentObject(); hoveredWindow.Repaint(); curEvent.Use(); } toggleExpanded(); toggleActive(); delete(); collapseAll(); isolate(); focus(); setDefaultParent(); resetDefaultParent(); } public static GameObject hoveredGo; public static Scene hoveredScene; public static GameObjectInfo GetGameObjectInfo(GameObject go) { if (goInfoCache.TryGetValue(go, out var cachedGoInfo)) return cachedGoInfo; var goInfo = new GameObjectInfo(); var goData = goInfo.goData = GetGameObjectData(go, createDataIfDoesntExist: false); var recursiveIconNameOrGuid = ""; var recursiveColorIndex = 0; var ruledIconNameOrGuid = ""; var ruledColorIndex = 0; void checkRules() { if (rules == null) rules = TypeCache.GetMethodsWithAttribute() .Where(r => r.IsStatic && r.GetParameters().Count() == 1 && r.GetParameters().First().ParameterType == typeof(ObjectInfo)).ToList(); if (!rules.Any()) return; var objectInfo = new ObjectInfo(go); foreach (var rule in rules) rule.Invoke(null, new[] { objectInfo }); ruledIconNameOrGuid = objectInfo.icon; ruledColorIndex = objectInfo.color; } void checkRecursion(Transform transform, int depth) { if (!transform.parent) return; var parentGoData = GetGameObjectData(transform.parent.gameObject, createDataIfDoesntExist: false); if (parentGoData != null) { if (parentGoData.isIconRecursive && parentGoData.iconNameOrGuid != "") if (recursiveIconNameOrGuid == "") recursiveIconNameOrGuid = parentGoData.iconNameOrGuid; if (parentGoData.isColorRecursive && parentGoData.colorIndex != 0) if (recursiveColorIndex == 0) recursiveColorIndex = parentGoData.colorIndex; if (parentGoData.isColorRecursive && parentGoData.colorIndex != 0) goInfo.maxColorRecursionDepth = depth + 1; } checkRecursion(transform.parent, depth + 1); } void setIcon() { var iconNameOrGuid = ""; if (goData != null && goData.iconNameOrGuid != "") iconNameOrGuid = goData.iconNameOrGuid; else if (recursiveIconNameOrGuid != "") iconNameOrGuid = recursiveIconNameOrGuid; else if (ruledIconNameOrGuid != "") iconNameOrGuid = ruledIconNameOrGuid; if (iconNameOrGuid == "") { goInfo.hasIcon = false; return; } goInfo.hasIcon = true; goInfo.hasIconByRecursion = recursiveIconNameOrGuid != ""; goInfo.iconNameOrPath = iconNameOrGuid.Length == 32 ? iconNameOrGuid.ToPath() : iconNameOrGuid; } void setColor() { var colorIndex = 0; if (goData != null && goData.colorIndex > 0) colorIndex = goData.colorIndex; else if (recursiveColorIndex != 0) colorIndex = recursiveColorIndex; else if (ruledColorIndex != 0) colorIndex = ruledColorIndex; if (colorIndex == 0) { goInfo.hasColor = false; return; } goInfo.hasColor = true; goInfo.hasColorByRecursion = recursiveColorIndex != 0; var brightness = palette?.colorBrightness ?? 1; var saturation = palette?.colorSaturation ?? 1; if (colorIndex <= VHierarchyPalette.greyColorsCount) saturation = brightness = 1; var rawColor = palette ? palette.colors[colorIndex - 1] : VHierarchyPalette.GetDefaultColor(colorIndex - 1); var brightenedColor = MathUtil.Lerp(Greyscale(.2f), rawColor, brightness); Color.RGBToHSV(brightenedColor, out float h, out float s, out float v); var saturatedColor = Color.HSVToRGB(h, s * saturation, v); goInfo.color = saturatedColor; goInfo.isGreyColor = colorIndex <= VHierarchyPalette.greyColorsCount; } checkRules(); checkRecursion(go.transform, 0); setIcon(); setColor(); return goInfoCache[go] = goInfo; } public static Dictionary goInfoCache = new(); public static List rules = null; public class GameObjectInfo { public string iconNameOrPath = ""; public bool hasIcon; public bool hasIconByRecursion; public Color color; public bool hasColor; public bool hasColorByRecursion; public int maxColorRecursionDepth; public bool isGreyColor; public GameObjectData goData; } public static GameObjectData GetGameObjectData(GameObject go, bool createDataIfDoesntExist) { if (!data) return null; if (goDataCache.TryGetValue(go, out var cachedResult)) return cachedResult; GameObjectData goData = null; SceneData sceneData = null; void sceneObject() { if (StageUtility.GetCurrentStage() is PrefabStage) return; SceneIdMap sceneIdMap = null; var currentSceneGuid = go.scene.path.ToGuid(); var originalSceneGuid = cache.originalSceneGuids_byInstanceId.GetValueOrDefault(go.GetInstanceID()) ?? currentSceneGuid; void getSceneDataFromComponents() { if (!VHierarchyData.teamModeEnabled) return; if (!dataComponents_byScene.ContainsKey(go.scene)) dataComponents_byScene[go.scene] = Resources.FindObjectsOfTypeAll().FirstOrDefault(r => r.gameObject?.scene == go.scene); if (dataComponents_byScene[go.scene]) sceneData = dataComponents_byScene[go.scene].sceneData; } void getSceneDataFromScriptableObject() { if (sceneData != null) return; data.sceneDatas_byGuid.TryGetValue(originalSceneGuid, out sceneData); } void createSceneData() { if (sceneData != null) return; if (!createDataIfDoesntExist) return; sceneData = new SceneData(); data.sceneDatas_byGuid[originalSceneGuid] = sceneData; } void getSceneIdMap() { if (sceneData == null) return; cache.sceneIdMaps_bySceneGuid.TryGetValue(originalSceneGuid, out sceneIdMap); } void createSceneIdMap() { if (sceneIdMap != null) return; if (sceneData == null) return; if (currentSceneGuid != originalSceneGuid) return; sceneIdMap = new SceneIdMap(); cache.sceneIdMaps_bySceneGuid[currentSceneGuid] = sceneIdMap; } void updateSceneIdMapAndOriginalSceneGuids() { if (sceneIdMap == null) return; if (currentSceneGuid != originalSceneGuid) return; if (!go.scene.isLoaded) return; // can happen when setting icons via api var curInstanceIdsHash = go.scene.GetRootGameObjects().FirstOrDefault()?.GetInstanceID() ?? 0; var curGlobalIdsHash = sceneData.goDatas_byGlobalId.Keys.Aggregate(0, (hash, r) => hash ^= r.GetHashCode()); if (sceneIdMap.instanceIdsHash == curInstanceIdsHash && sceneIdMap.globalIdsHash == curGlobalIdsHash) return; var globalIds = sceneData.goDatas_byGlobalId.Keys.ToList(); var instanceIds = globalIds.Select(r => Application.isPlaying ? r.UnpackForPrefab() : r) .GetObjectInstanceIds(); void clearIdMap() { if (Application.isPlaying) return; // not clearing in playmode fixes data loss on first root object when it's moved to DontDestroyOnLoad (sice it causes map update) sceneIdMap.globalIds_byInstanceId = new SerializableDictionary(); } void clearSceneGuids() { if (Application.isPlaying) return; // not clearing in playmode fixes data loss on first root object when it's moved to DontDestroyOnLoad (sice it causes map update) foreach (var instanceId in sceneIdMap.globalIds_byInstanceId.Keys) cache.originalSceneGuids_byInstanceId.Remove(instanceId); } void fillIdMap() { for (int i = 0; i < instanceIds.Length; i++) if (instanceIds[i] != 0) sceneIdMap.globalIds_byInstanceId[instanceIds[i]] = globalIds[i]; } void fillSceneGuids() { for (int i = 0; i < instanceIds.Length; i++) cache.originalSceneGuids_byInstanceId[instanceIds[i]] = currentSceneGuid; } clearIdMap(); clearSceneGuids(); fillIdMap(); fillSceneGuids(); sceneIdMap.instanceIdsHash = curInstanceIdsHash; sceneIdMap.globalIdsHash = curGlobalIdsHash; } void getGoData() { if (sceneData == null) return; if (sceneIdMap == null) return; if (!sceneIdMap.globalIds_byInstanceId.TryGetValue(go.GetInstanceID(), out var globalId)) return; sceneData.goDatas_byGlobalId.TryGetValue(globalId, out goData); } void moveGoDataToCurrentSceneGuid() { if (goData == null) return; if (currentSceneGuid == originalSceneGuid) return; if (Application.isPlaying) return; var originalSceneData = sceneData; var currentSceneData = dataComponents_byScene.GetValueOrDefault(go.scene)?.sceneData ?? data.sceneDatas_byGuid.GetValueOrDefault(currentSceneGuid); if (originalSceneData == null) return; if (currentSceneData == null) return; var globalId = go.GetGlobalID(); originalSceneData.goDatas_byGlobalId.Remove(originalSceneData.goDatas_byGlobalId.First(r => r.Value == goData).Key); currentSceneData.goDatas_byGlobalId[go.GetGlobalID()] = goData; } void createGoData() { if (goData != null) return; if (!createDataIfDoesntExist) return; goData = new GameObjectData(); sceneData.goDatas_byGlobalId[go.GetGlobalID()] = goData; } getSceneDataFromComponents(); getSceneDataFromScriptableObject(); createSceneData(); getSceneIdMap(); createSceneIdMap(); updateSceneIdMapAndOriginalSceneGuids(); getGoData(); moveGoDataToCurrentSceneGuid(); createGoData(); } void prefabObject() { if (StageUtility.GetCurrentStage() is not PrefabStage prefabStage) return; var prefabGuid = prefabStage.assetPath.ToGuid(); GlobalID sourceGlobalId; void calcGlobalId() { var rawGlobalId = go.GetGlobalID(); #if UNITY_2023_2_OR_NEWER var so = new SerializedObject(go); so.SetPropertyValue("inspectorMode", UnityEditor.InspectorMode.Debug); var rawFileId = so.FindProperty("m_LocalIdentfierInFile").longValue; if (rawFileId == 0) // happens for prefab variants in unity 6 rawFileId = (long)t_Unsupported.InvokeMethod("GetOrGenerateFileIDHint", go); #else var rawFileId = rawGlobalId.fileId; #endif // fixes fileId for prefab variants // also works for getting prefab's unpacked fileId var fileId = ((long)rawFileId ^ (long)rawGlobalId.globalObjectId.targetPrefabId) & 0x7fffffffffffffff; sourceGlobalId = new GlobalID($"GlobalObjectId_V1-1-{prefabGuid}-{fileId}-0"); } void getSceneDataFromScriptableObject() { data.sceneDatas_byGuid.TryGetValue(prefabGuid, out sceneData); } void createSceneData() { if (sceneData != null) return; if (!createDataIfDoesntExist) return; sceneData = new SceneData(); data.sceneDatas_byGuid[prefabGuid] = sceneData; } void getGoData() { if (sceneData == null) return; sceneData.goDatas_byGlobalId.TryGetValue(sourceGlobalId, out goData); } void createGoData() { if (goData != null) return; if (!createDataIfDoesntExist) return; goData = new GameObjectData(); sceneData.goDatas_byGlobalId[sourceGlobalId] = goData; } calcGlobalId(); getSceneDataFromScriptableObject(); createSceneData(); getGoData(); createGoData(); } void prefabInstance_editMode() { if (!PrefabUtility.IsPartOfPrefabInstance(go)) return; if (goData != null) return; void tryGetForSourceGo(GameObject sourceGo) { var sourceGoGlobalId = sourceGo.GetGlobalID(); var sourcePrefabGuid = sourceGoGlobalId.guid; cache.prefabInstanceGlobalIds_byInstanceIds[go.GetInstanceID()] = sourceGoGlobalId; data.sceneDatas_byGuid.TryGetValue(sourcePrefabGuid, out sceneData); sceneData?.goDatas_byGlobalId.TryGetValue(sourceGoGlobalId, out goData); if (goData == null) try { if (PrefabUtility.GetCorrespondingObjectFromSource(sourceGo) is GameObject previousSourceGo) tryGetForSourceGo(previousSourceGo); // wrapped in try-catch because GetCorrespondingObjectFromSource throws exceptions on broken prefabs } catch { } } tryGetForSourceGo(PrefabUtility.GetCorrespondingObjectFromSource(go)); } void prefabInstance_playmode() { if (!Application.isPlaying) return; if (goData != null) return; if (!cache.prefabInstanceGlobalIds_byInstanceIds.TryGetValue(go.GetInstanceID(), out var globalId)) return; var prefabGuid = globalId.guid; data.sceneDatas_byGuid.TryGetValue(prefabGuid, out sceneData); sceneData?.goDatas_byGlobalId.TryGetValue(globalId, out goData); } sceneObject(); prefabObject(); prefabInstance_editMode(); prefabInstance_playmode(); if (goData != null) goData.sceneData = sceneData; return goDataCache[go] = goData; } public static Dictionary goDataCache = new(); public static Dictionary dataComponents_byScene = new(); static VHierarchyCache cache => VHierarchyCache.instance; public static void OnHierarchyChanged() { goInfoCache.Clear(); } public static void OnDataSerialization() { goInfoCache.Clear(); goDataCache.Clear(); } static void LoadSceneBookmarkObjects() // update { if (!data) return; var scenesToLoadFor = unloadedSceneBookmarks_sceneGuids.Select(r => EditorSceneManager.GetSceneByPath(r.ToPath())) .Where(r => r.isLoaded); if (!scenesToLoadFor.Any()) return; foreach (var scene in scenesToLoadFor) { var bookmarksFromThisScene = data.bookmarks.Where(r => r.globalId.guid == scene.path.ToGuid()).ToList(); var objectsForTheseBookmarks = bookmarksFromThisScene.Select(r => !Application.isPlaying ? r.globalId : r.globalId.UnpackForPrefab()).GetObjects(); for (int i = 0; i < bookmarksFromThisScene.Count; i++) if (objectsForTheseBookmarks[i]) bookmarksFromThisScene[i]._go = objectsForTheseBookmarks[i] as GameObject; else bookmarksFromThisScene[i].failedToLoadSceneObject = true; } unloadedSceneBookmarks_sceneGuids.Clear(); foreach (var window in allHierarchies) window.Repaint(); } public static HashSet unloadedSceneBookmarks_sceneGuids = new(); static void StashBookmarkObjects() // on playmode enter before awake { stashedBookmarkObjects_byBookmark.Clear(); foreach (var bookmark in data.bookmarks) stashedBookmarkObjects_byBookmark[bookmark] = bookmark._go; } static void UnstashBookmarkObjects() // on playmode exit { foreach (var bookmark in data.bookmarks) if (stashedBookmarkObjects_byBookmark.TryGetValue(bookmark, out var stashedObject)) if (stashedObject != null) bookmark._go = stashedObject; } static Dictionary stashedBookmarkObjects_byBookmark = new(); [RuntimeInitializeOnLoadMethod(RuntimeInitializeLoadType.BeforeSceneLoad)] static void OnPlaymodeEnter_beforeAwake() { if (!data) return; StashBookmarkObjects(); } static void OnPlaymodeExit(PlayModeStateChange state) { if (state != PlayModeStateChange.EnteredEditMode) return; if (!data) return; UnstashBookmarkObjects(); // scene objects can get recreated in playmode if the scene was reloaded // in this case their respective bookmarks will be updated in OnSceneLoaded_inPlaymode to reference the recreated versions // so we ensure that after playmode bookmarks reference the same objects as they did before playmode foreach (var bookmark in data.bookmarks) if (bookmark.globalId.guid == "00000000000000000000000000000000") if (bookmark._go is GameObject gameObject) { bookmark.globalId = new GlobalID(bookmark.globalId.ToString().Replace("00000000000000000000000000000000", gameObject.scene.path.ToGuid())); data.Dirty(); } // objects from DontDestroyOnLoad that were bookmarked in playmode have globalIds with blank scene guids // we fix this after playmode, when scene guids become available } static void RepaintOnAlt() // Update { var lastEvent = typeof(Event).GetFieldValue("s_Current"); if (lastEvent.alt != wasAlt) if (EditorWindow.mouseOverWindow is EditorWindow hoveredWindow) if (hoveredWindow.GetType() == t_SceneHierarchyWindow || hoveredWindow is VHierarchySceneSelectorWindow) hoveredWindow.Repaint(); wasAlt = lastEvent.alt; } static bool wasAlt; static void SetPreviousTransformTool() { if (!transformToolNeedsReset) return; Tools.current = previousTransformTool; transformToolNeedsReset = false; // E shortcut changes transform tool in 2022 // here we undo this } static bool transformToolNeedsReset; static Tool previousTransformTool; static void DuplicateSceneData(string originalSceneGuid, string duplicatedSceneGuid) { var originalSceneData = data.sceneDatas_byGuid[originalSceneGuid]; var duplicatedSceneData = data.sceneDatas_byGuid[duplicatedSceneGuid] = new SceneData(); foreach (var kvp in originalSceneData.goDatas_byGlobalId) { var duplicatedGlobalId = new GlobalID(kvp.Key.ToString().Replace(originalSceneGuid, duplicatedSceneGuid)); var duplicatedGoData = new GameObjectData() { colorIndex = kvp.Value.colorIndex, iconNameOrGuid = kvp.Value.iconNameOrGuid }; duplicatedSceneData.goDatas_byGlobalId[duplicatedGlobalId] = duplicatedGoData; } data.Dirty(); } static void OnSceneImported(string importedScenePath) { if (curEvent.commandName != "Duplicate" && curEvent.commandName != "Paste") return; var copiedAssets_paths = new List(); var assetClipboard = typeof(Editor).Assembly.GetType("UnityEditor.AssetClipboardUtility").GetMemberValue("assetClipboard").InvokeMethod("GetEnumerator"); while (assetClipboard.MoveNext()) copiedAssets_paths.Add(assetClipboard.Current.GetMemberValue("guid").ToString().ToPath()); var originalScenePath = copiedAssets_paths.FirstOrDefault(r => File.Exists(r) && new FileInfo(r).Length == new FileInfo(importedScenePath).Length); var originalSceneGuid = originalScenePath.ToGuid(); var duplicatedSceneGuid = importedScenePath.ToGuid(); if (!data.sceneDatas_byGuid.ContainsKey(originalSceneGuid)) return; if (data.sceneDatas_byGuid.ContainsKey(duplicatedSceneGuid)) return; DuplicateSceneData(originalSceneGuid, duplicatedSceneGuid); } class SceneImportDetector : AssetPostprocessor { // scene data duplication won't work on earlier versions anyway #if UNITY_2021_2_OR_NEWER static void OnPostprocessAllAssets(string[] importedAssets, string[] deletedAssets, string[] movedAssets, string[] movedFromAssetPaths, bool didDomainReload) { if (!data) return; foreach (var r in importedAssets) if (r.EndsWith(".unity")) OnSceneImported(r); } #endif } [UnityEditor.Callbacks.PostProcessBuild] public static void ClearCacheAfterBuild(BuildTarget _, string __) => VHierarchyCache.Clear(); static void ClearCacheOnProjectLoaded() => VHierarchyCache.Clear(); [InitializeOnLoadMethod] static void Init() { if (VHierarchyMenu.pluginDisabled) return; void subscribe() { // gui EditorApplication.hierarchyWindowItemOnGUI -= RowGUI; EditorApplication.hierarchyWindowItemOnGUI = RowGUI + EditorApplication.hierarchyWindowItemOnGUI; // wrapping updaters EditorApplication.projectWindowItemOnGUI -= ProjectWindowItemOnGUI; EditorApplication.projectWindowItemOnGUI += ProjectWindowItemOnGUI; EditorApplication.hierarchyWindowItemOnGUI -= HierarchyWindowItemOnGUI; EditorApplication.hierarchyWindowItemOnGUI += HierarchyWindowItemOnGUI; EditorApplication.delayCall -= DelayCallLoop; EditorApplication.delayCall += DelayCallLoop; EditorApplication.update -= CheckIfFocusedWindowChanged; EditorApplication.update += CheckIfFocusedWindowChanged; // shortcuts var globalEventHandler = typeof(EditorApplication).GetFieldValue("globalEventHandler"); typeof(EditorApplication).SetFieldValue("globalEventHandler", Shortcuts + (globalEventHandler - Shortcuts)); // loading bookmarked objects EditorApplication.update -= LoadSceneBookmarkObjects; EditorApplication.update += LoadSceneBookmarkObjects; // other EditorApplication.update -= RepaintOnAlt; EditorApplication.update += RepaintOnAlt; EditorApplication.update -= SetPreviousTransformTool; EditorApplication.update += SetPreviousTransformTool; EditorApplication.hierarchyChanged -= OnHierarchyChanged; EditorApplication.hierarchyChanged += OnHierarchyChanged; var projectWasLoaded = typeof(EditorApplication).GetFieldValue("projectWasLoaded"); typeof(EditorApplication).SetFieldValue("projectWasLoaded", (projectWasLoaded - ClearCacheOnProjectLoaded) + ClearCacheOnProjectLoaded); } void loadData() { data = AssetDatabase.LoadAssetAtPath(ProjectPrefs.GetString("vHierarchy-lastKnownDataPath")); if (data) return; data = AssetDatabase.FindAssets("t:VHierarchyData").Select(guid => AssetDatabase.LoadAssetAtPath(guid.ToPath())).FirstOrDefault(); if (!data) return; ProjectPrefs.SetString("vHierarchy-lastKnownDataPath", data.GetPath()); } void loadPalette() { palette = AssetDatabase.LoadAssetAtPath(ProjectPrefs.GetString("vHierarchy-lastKnownPalettePath")); if (palette) return; palette = AssetDatabase.FindAssets("t:VHierarchyPalette").Select(guid => AssetDatabase.LoadAssetAtPath(guid.ToPath())).FirstOrDefault(); if (!palette) return; ProjectPrefs.SetString("vHierarchy-lastKnownPalettePath", palette.GetPath()); } void loadDataAndPaletteDelayed() { if (!data) EditorApplication.delayCall += () => EditorApplication.delayCall += loadData; if (!palette) EditorApplication.delayCall += () => EditorApplication.delayCall += loadPalette; // AssetDatabase isn't up to date at this point (it gets updated after InitializeOnLoadMethod) // and if current AssetDatabase state doesn't contain the data - it won't be loaded during Init() // so here we schedule an additional, delayed attempt to load the data // this addresses reports of data loss when trying to load it on a new machine } void migrateDataFromV1() { if (!data) return; if (ProjectPrefs.GetBool("vHierarchy-dataMigrationFromV1Attempted", false)) return; ProjectPrefs.SetBool("vHierarchy-dataMigrationFromV1Attempted", true); var lines = System.IO.File.ReadAllLines(data.GetPath()); if (lines.Length < 15 || !lines[14].Contains("sceneDatasByGuid")) return; var sceneGuids = new List(); var globalIdLists = new List>(); var goDatasByInstanceIdCounts = new List(); var sceneDatas = new List(); void parseSceneGuids() { for (int i = 16; i < lines.Length; i++) { if (lines[i].Contains("values:")) break; var startIndex = lines[i].IndexOf("- ") + 2; if (startIndex < lines[i].Length) sceneGuids.Add(lines[i].Substring(startIndex)); else sceneGuids.Add(""); } } void parseGlobalIdLists_andCountGoDatasByInstanceId() { var parsingGlobalIdList = false; var parsingGlobalIdListAtIndex = -1; for (int i = 0; i < lines.Length; i++) { var line = lines[i]; void startParsing() { if (!line.Contains("goDatasByGlobalId")) return; parsingGlobalIdList = true; parsingGlobalIdListAtIndex++; globalIdLists.Add(new List()); } void parse() { if (!parsingGlobalIdList) return; if (!line.Contains("- GlobalObjectId")) return; var startIndex = line.IndexOf("- ") + 2; if (startIndex < line.Length) globalIdLists[parsingGlobalIdListAtIndex].Add(line.Substring(startIndex)); else globalIdLists[parsingGlobalIdListAtIndex].Add(""); } void stopParsing_andCountDatasByInstanceId() { if (!line.Contains("goDatasByInstanceId")) return; parsingGlobalIdList = false; var goDatasByInstanceId_keysLine = lines[i + 1]; var goDatasByInstanceId_count = (goDatasByInstanceId_keysLine.Length - 14) / 8; goDatasByInstanceIdCounts.Add(goDatasByInstanceId_count); } startParsing(); parse(); stopParsing_andCountDatasByInstanceId(); } } void parseSceneDatas() { var firstLineIndexOfFirstSceneData = 17 + sceneGuids.Count; void parseSceneData(int sceneDataIndex) { var sceneData = new SceneData(); var globalIds = globalIdLists[sceneDataIndex]; var firstLineIndex = getFirstLineIndex(sceneDataIndex); void parseGoData(int iGoData) { var goData = new GameObjectData(); var colorLine = lines[getColorLineIndex(iGoData)]; if (colorLine.Length > 18) goData.colorIndex = int.Parse(colorLine.Substring(18)); var iconLine = lines[getIconLineIndex(iGoData)]; if (iconLine.Length > 16) goData.iconNameOrGuid = iconLine.Substring(16); var globalIdString = globalIdLists[sceneDataIndex][iGoData]; var globalId = new GlobalID(globalIdString); sceneData.goDatas_byGlobalId[globalId] = goData; // sceneData.goDatas_byGlobalId.Add(globalId, goData); } int getColorLineIndex(int goDataIndex) { var index = firstLineIndex; // - goDatasByGlobalId: index += 1; // keys: index += globalIds.Count; index += 1; // values: index += 1; // zeroth godata index += goDataIndex * 2; return index; } int getIconLineIndex(int goDataIndex) => getColorLineIndex(goDataIndex) + 1; for (int i = 0; i < globalIds.Count; i++) parseGoData(i); sceneDatas.Add(sceneData); } int getSceneDataLength(int sceneDataIndex) { int length = 0; length += 1; // - goDatasByGlobalId: length += 1; // - keys: length += globalIdLists[sceneDataIndex].Count; length += 1; // - values: length += globalIdLists[sceneDataIndex].Count * 2; length += 1; // - goDatasByInstanceId: length += 1; // - keys: 123123123 length += 1; // - values: length += goDatasByInstanceIdCounts[sceneDataIndex] * 2; return length; } int getFirstLineIndex(int sceneDataIndex) { var index = firstLineIndexOfFirstSceneData; for (int i = 0; i < sceneDataIndex; i++) index += getSceneDataLength(i); return index; } for (int i = 0; i < sceneGuids.Count; i++) parseSceneData(i); } void remapColorIndexes() { foreach (var sceneData in sceneDatas) foreach (var goData in sceneData.goDatas_byGlobalId.Values) if (goData.colorIndex == 7) goData.colorIndex = 1; else if (goData.colorIndex == 8) goData.colorIndex = 2; else if (goData.colorIndex >= 2) goData.colorIndex += 2; } void setSceneDatasToData() { for (int i = 0; i < sceneDatas.Count; i++) data.sceneDatas_byGuid[sceneGuids[i]] = sceneDatas[i]; data.Dirty(); data.Save(); } try { parseSceneGuids(); parseGlobalIdLists_andCountGoDatasByInstanceId(); parseSceneDatas(); remapColorIndexes(); setSceneDatasToData(); } catch { } } // void removeDeletedBookmarks() // { // if (!data) return; // var toRemove = data.bookmarks.Where(r => r.isDeleted); // if (!toRemove.Any()) return; // foreach (var r in toRemove.ToList()) // data.bookmarks.Remove(r); // data.Dirty(); // // delayed to give bookmarks a chance to load in update // } subscribe(); loadData(); loadPalette(); loadDataAndPaletteDelayed(); migrateDataFromV1(); // EditorApplication.delayCall += () => removeDeletedBookmarks(); OnDomainReloaded(); } public static VHierarchyData data; public static VHierarchyPalette palette; static IEnumerable allHierarchies => _allHierarchies ??= t_SceneHierarchyWindow.GetFieldValue("s_SceneHierarchyWindows").Cast(); static IEnumerable _allHierarchies; static Type t_SceneHierarchyWindow = typeof(Editor).Assembly.GetType("UnityEditor.SceneHierarchyWindow"); static Type t_HostView = typeof(Editor).Assembly.GetType("UnityEditor.HostView"); static Type t_EditorWindowDelegate = t_HostView.GetNestedType("EditorWindowDelegate", maxBindingFlags); static Type t_Unsupported = typeof(Editor).Assembly.GetType("UnityEditor.Unsupported"); static Type t_VTabs = Type.GetType("VTabs.VTabs") ?? Type.GetType("VTabs.VTabs, VTabs, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); static Type t_VFavorites = Type.GetType("VFavorites.VFavorites") ?? Type.GetType("VFavorites.VFavorites, VFavorites, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); static MethodInfo mi_WrappedGUI = typeof(VHierarchy).GetMethod(nameof(WrappedGUI), maxBindingFlags); public const string version = "2.1.3"; } #region Rules public class RuleAttribute : System.Attribute { } public class ObjectInfo { public int color = 0; public string icon = ""; public ObjectInfo(GameObject gameObject) => this.gameObject = gameObject; public GameObject gameObject; } #endregion } #endif