From bca9ee7cd5eb8edfce10535ba3a88c8befdfc0b3 Mon Sep 17 00:00:00 2001 From: hypercross Date: Tue, 19 Aug 2025 10:13:59 +0800 Subject: [PATCH] add vHierarchy --- vfolders2/vHierarchy.meta | 8 + vfolders2/vHierarchy/Read me.pdf | Bin 0 -> 116397 bytes vfolders2/vHierarchy/Read me.pdf.meta | 7 + vfolders2/vHierarchy/VHierarchy.asmdef | 16 + vfolders2/vHierarchy/VHierarchy.asmdef.meta | 7 + vfolders2/vHierarchy/VHierarchy.cs | 1815 ++++++++++++ vfolders2/vHierarchy/VHierarchy.cs.meta | 11 + vfolders2/vHierarchy/VHierarchyCache.cs | 61 + vfolders2/vHierarchy/VHierarchyCache.cs.meta | 11 + .../vHierarchy/VHierarchyComponentEditor.cs | 6 + .../VHierarchyComponentEditor.cs.meta | 11 + .../vHierarchy/VHierarchyComponentWindow.cs | 599 ++++ .../VHierarchyComponentWindow.cs.meta | 11 + vfolders2/vHierarchy/VHierarchyController.cs | 517 ++++ .../vHierarchy/VHierarchyController.cs.meta | 11 + vfolders2/vHierarchy/VHierarchyData.cs | 227 ++ vfolders2/vHierarchy/VHierarchyData.cs.meta | 11 + .../vHierarchy/VHierarchyDataComponent.cs | 142 + .../VHierarchyDataComponent.cs.meta | 11 + vfolders2/vHierarchy/VHierarchyGUI.cs | 1024 +++++++ vfolders2/vHierarchy/VHierarchyGUI.cs.meta | 11 + vfolders2/vHierarchy/VHierarchyIconEditor.cs | 6 + .../vHierarchy/VHierarchyIconEditor.cs.meta | 11 + vfolders2/vHierarchy/VHierarchyLibs.cs | 2563 +++++++++++++++++ vfolders2/vHierarchy/VHierarchyLibs.cs.meta | 11 + .../vHierarchy/VHierarchyLightingWindow.cs | 6 + .../VHierarchyLightingWindow.cs.meta | 11 + vfolders2/vHierarchy/VHierarchyMenu.cs | 159 + vfolders2/vHierarchy/VHierarchyMenu.cs.meta | 11 + vfolders2/vHierarchy/VHierarchyMenuItems.cs | 6 + .../vHierarchy/VHierarchyMenuItems.cs.meta | 11 + vfolders2/vHierarchy/VHierarchyNavbar.cs | 1081 +++++++ vfolders2/vHierarchy/VHierarchyNavbar.cs.meta | 11 + vfolders2/vHierarchy/VHierarchyPalette.cs | 221 ++ .../vHierarchy/VHierarchyPalette.cs.meta | 11 + .../vHierarchy/VHierarchyPaletteEditor.cs | 1374 +++++++++ .../VHierarchyPaletteEditor.cs.meta | 11 + .../vHierarchy/VHierarchyPaletteWindow.cs | 711 +++++ .../VHierarchyPaletteWindow.cs.meta | 11 + .../VHierarchySceneSelectorWindow.cs | 1189 ++++++++ .../VHierarchySceneSelectorWindow.cs.meta | 11 + vfolders2/vHierarchy/vHierarchy Data.asset | 19 + .../vHierarchy/vHierarchy Data.asset.meta | 8 + 43 files changed, 11970 insertions(+) create mode 100644 vfolders2/vHierarchy.meta create mode 100644 vfolders2/vHierarchy/Read me.pdf create mode 100644 vfolders2/vHierarchy/Read me.pdf.meta create mode 100644 vfolders2/vHierarchy/VHierarchy.asmdef create mode 100644 vfolders2/vHierarchy/VHierarchy.asmdef.meta create mode 100644 vfolders2/vHierarchy/VHierarchy.cs create mode 100644 vfolders2/vHierarchy/VHierarchy.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyCache.cs create mode 100644 vfolders2/vHierarchy/VHierarchyCache.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyComponentEditor.cs create mode 100644 vfolders2/vHierarchy/VHierarchyComponentEditor.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyComponentWindow.cs create mode 100644 vfolders2/vHierarchy/VHierarchyComponentWindow.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyController.cs create mode 100644 vfolders2/vHierarchy/VHierarchyController.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyData.cs create mode 100644 vfolders2/vHierarchy/VHierarchyData.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyDataComponent.cs create mode 100644 vfolders2/vHierarchy/VHierarchyDataComponent.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyGUI.cs create mode 100644 vfolders2/vHierarchy/VHierarchyGUI.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyIconEditor.cs create mode 100644 vfolders2/vHierarchy/VHierarchyIconEditor.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyLibs.cs create mode 100644 vfolders2/vHierarchy/VHierarchyLibs.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyLightingWindow.cs create mode 100644 vfolders2/vHierarchy/VHierarchyLightingWindow.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyMenu.cs create mode 100644 vfolders2/vHierarchy/VHierarchyMenu.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyMenuItems.cs create mode 100644 vfolders2/vHierarchy/VHierarchyMenuItems.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyNavbar.cs create mode 100644 vfolders2/vHierarchy/VHierarchyNavbar.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyPalette.cs create mode 100644 vfolders2/vHierarchy/VHierarchyPalette.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyPaletteEditor.cs create mode 100644 vfolders2/vHierarchy/VHierarchyPaletteEditor.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchyPaletteWindow.cs create mode 100644 vfolders2/vHierarchy/VHierarchyPaletteWindow.cs.meta create mode 100644 vfolders2/vHierarchy/VHierarchySceneSelectorWindow.cs create mode 100644 vfolders2/vHierarchy/VHierarchySceneSelectorWindow.cs.meta create mode 100644 vfolders2/vHierarchy/vHierarchy Data.asset create mode 100644 vfolders2/vHierarchy/vHierarchy Data.asset.meta diff --git a/vfolders2/vHierarchy.meta b/vfolders2/vHierarchy.meta new file mode 100644 index 0000000..0d51a57 --- /dev/null +++ b/vfolders2/vHierarchy.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: dd2b9e231b0ec4a9f97e258072ae2b33 +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/Read me.pdf b/vfolders2/vHierarchy/Read me.pdf new file mode 100644 index 0000000000000000000000000000000000000000..754aee7351aaa2e082a723fcf6e5dd73dab62b01 GIT binary patch literal 116397 zcmaI7Wmr`0`vppOcc(BklETnkGBgMxjdVKF-AI=pFmxjg3X;+xNOy+}NOyC#@AW&^ z|9m>%fZ5ERd7k~Od);fTn?+Mzk(-Z45Q}B%@7@L$AHz!q7YlnVadBQZ7H#I|G}JgPl1q@Qw^l zd;$UtN-i!oz+1?>Sbkv8wsy9%cDHtCmz2Z;e*AY2f&aUgr8xs17O$*2124qI-O1eX z|1J{!zl$IY0$99?c8*ZsPP~ea=1^;SYfBd^Yb@T^*3LFiTLyknVM$4b4^Ve&b0;jX zZ2!b|)gCjF2zmz@l0KP8JUmJow|H^y(n^-BfSW9D-cOW>&RKkNIfJgY%) zYc>qL&2^TjyapGl7pUdCCx3js&Xaj}rC&sFP(?!KK8zHHXjRct=p4T;i?lZKk321& zzE(^0H+clTsxG1SSev|M^seTa15t1G>35Y%&LMZ4-1dGDKW+MbWYWUcL0r?Q*eu!o zdF$@>)v11|L~Zx;``M!-1E_v6i|WNR_u~9losA6U*IE%wSqF(A71>k<1;|mqruU}L zdlel`@)^VS-^%RY(#CG@ajFkiq_419dF5&XQ1N#Od~w^YM}-5?t`qXfkLLN_eXvz1 zR&Kz|{r=J*G#r#6`ta@`!!S;&f$Ctg&q6(QS`c$mDtuI}{oiiXVXjs@=TSBpTbv{}^|ND!IlerDB?gn;Nz_JU73S#jpTie;#LK%bvg|T?$TpV58 zbzIFYtr_0(s>sR7nt!miV)(a{+V6ljQ8)kKz##JPH(<-{pdU1?-Q`@ITwR>4ouLdO z|NmPoBp@s#^8elB0~op=$=uA9`dJ4%ơqDL&YIw>PcfilJm#%W)dsP{g{!u=P{ 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 VJG;&Xa?lQpz`}BJ8Z=Ge{{VZ+>H+`& literal 0 HcmV?d00001 diff --git a/vfolders2/vHierarchy/Read me.pdf.meta b/vfolders2/vHierarchy/Read me.pdf.meta new file mode 100644 index 0000000..0227813 --- /dev/null +++ b/vfolders2/vHierarchy/Read me.pdf.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 520b2a02f70c04615b08281be020f305 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchy.asmdef b/vfolders2/vHierarchy/VHierarchy.asmdef new file mode 100644 index 0000000..22702c6 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchy.asmdef @@ -0,0 +1,16 @@ +{ + "name": "VHierarchy", + "rootNamespace": "", + "references": [], + "includePlatforms": [ + "Editor" + ], + "excludePlatforms": [], + "allowUnsafeCode": false, + "overrideReferences": false, + "precompiledReferences": [], + "autoReferenced": true, + "defineConstraints": [], + "versionDefines": [], + "noEngineReferences": false +} \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchy.asmdef.meta b/vfolders2/vHierarchy/VHierarchy.asmdef.meta new file mode 100644 index 0000000..d859f2d --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchy.asmdef.meta @@ -0,0 +1,7 @@ +fileFormatVersion: 2 +guid: 2c3f48364a5004fd3a152fbdf5fea703 +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchy.cs b/vfolders2/vHierarchy/VHierarchy.cs new file mode 100644 index 0000000..ef9a12e --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchy.cs @@ -0,0 +1,1815 @@ +#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 diff --git a/vfolders2/vHierarchy/VHierarchy.cs.meta b/vfolders2/vHierarchy/VHierarchy.cs.meta new file mode 100644 index 0000000..1831a9a --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchy.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 67e949be20d3641adbc9494ed5bd764e +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyCache.cs b/vfolders2/vHierarchy/VHierarchyCache.cs new file mode 100644 index 0000000..2e27db0 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyCache.cs @@ -0,0 +1,61 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditor.ShortcutManagement; +using System.Reflection; +using System.Linq; +using UnityEngine.UIElements; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using Type = System.Type; +using static VHierarchy.VHierarchyData; +using static VHierarchy.Libs.VUtils; +using static VHierarchy.Libs.VGUI; +// using static VTools.VDebug; + + +namespace VHierarchy +{ + [FilePath("Library/vHierarchy Cache.asset", FilePathAttribute.Location.ProjectFolder)] + public class VHierarchyCache : ScriptableSingleton + { + // used for finding SceneData and SceneIdMap for objects that were moved out of their original scene + public SerializableDictionary originalSceneGuids_byInstanceId = new(); + + // used as cache for converting GlobalID to InstanceID and as a way to find GameObjectData for prefabs in playmode (when prefabs produce invalid GlobalIDs) + public SerializableDictionary sceneIdMaps_bySceneGuid = new(); + + // used for fetching icons set inside prefab instances in playmode (when prefabs produce invalid GlobalIDs) + public SerializableDictionary prefabInstanceGlobalIds_byInstanceIds = new SerializableDictionary(); + + + + [System.Serializable] + public class SceneIdMap + { + public SerializableDictionary globalIds_byInstanceId = new(); + + public int instanceIdsHash; + public int globalIdsHash; + + } + + + + + + + + public static void Clear() + { + instance.originalSceneGuids_byInstanceId.Clear(); + instance.sceneIdMaps_bySceneGuid.Clear(); + + } + + + } +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchyCache.cs.meta b/vfolders2/vHierarchy/VHierarchyCache.cs.meta new file mode 100644 index 0000000..b6d9512 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyCache.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3fd3b966dd497472d86df0d7c9271088 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyComponentEditor.cs b/vfolders2/vHierarchy/VHierarchyComponentEditor.cs new file mode 100644 index 0000000..8fb6b18 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyComponentEditor.cs @@ -0,0 +1,6 @@ + + +// this file was present in a previus version and is supposed to be deleted now +// but asset store update delivery system doesn't allow deleting files +// so instead this file is now emptied +// feel free to delete it if you want diff --git a/vfolders2/vHierarchy/VHierarchyComponentEditor.cs.meta b/vfolders2/vHierarchy/VHierarchyComponentEditor.cs.meta new file mode 100644 index 0000000..18dd4d0 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyComponentEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d51d8117d96b64eaa9a83667bf4297d6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyComponentWindow.cs b/vfolders2/vHierarchy/VHierarchyComponentWindow.cs new file mode 100644 index 0000000..b84d74a --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyComponentWindow.cs @@ -0,0 +1,599 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditor.ShortcutManagement; +using System.Reflection; +using System.Linq; +using UnityEngine.UIElements; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using Type = System.Type; +using static VHierarchy.VHierarchyData; +using static VHierarchy.Libs.VUtils; +using static VHierarchy.Libs.VGUI; +// using static VTools.VDebug; + + +namespace VHierarchy +{ + public class VHierarchyComponentWindow : EditorWindow + { + + void OnGUI() + { + if (!component) component = EditorUtility.InstanceIDToObject(componentIid) as Component; + if (!component) { Close(); return; } + + if (!editor) Init(component); + + + void background() + { + position.SetPos(0, 0).Draw(GUIColors.windowBackground); + } + void header() + { + var headerRect = ExpandWidthLabelRect(18).Resize(-1).AddWidthFromMid(6); + var pinButtonRect = headerRect.SetWidthFromRight(17).SetHeightFromMid(17).Move(-21, .5f); + var closeButtonRect = headerRect.SetWidthFromRight(16).SetHeightFromMid(16).Move(-3, .5f); + + var backgroundColor = isDarkTheme ? Greyscale(.25f) : GUIColors.windowBackground; + + void startDragging() + { + if (isResizingVertically) return; + if (isResizingHorizontally) return; + if (isDragged) return; + if (!curEvent.isMouseDrag) return; + if (!headerRect.IsHovered()) return; + + + isDragged = true; + + dragStartMousePos = EditorGUIUtility.GUIToScreenPoint(curEvent.mousePosition); + dragStartWindowPos = position.position; + + + isPinned = true; + + if (floatingInstance == this) + floatingInstance = null; + + EditorApplication.RepaintHierarchyWindow(); + + + } + void updateDragging() + { + if (!isDragged) return; + + + var draggedPosition = dragStartWindowPos + EditorGUIUtility.GUIToScreenPoint(curEvent.mousePosition) - dragStartMousePos; + + if (!curEvent.isRepaint) + position = position.SetPos(draggedPosition); + + + EditorGUIUtility.hotControl = EditorGUIUtility.GetControlID(FocusType.Passive); + + } + void stopDragging() + { + if (!isDragged) return; + if (!curEvent.isMouseUp) return; + + + isDragged = false; + + EditorGUIUtility.hotControl = 0; + + } + + void background() + { + headerRect.Draw(backgroundColor); + + headerRect.SetHeightFromBottom(1).Draw(isDarkTheme ? Greyscale(.2f) : Greyscale(.7f)); + + } + void icon() + { + var iconRect = headerRect.SetWidth(20).MoveX(14).MoveY(-1); + + GUI.Label(iconRect, VHierarchy.GetComponentIcon(component)); + + } + void toggle() + { + var toggleRect = headerRect.MoveX(36).SetSize(20, 20); + + + var pi_enabled = component.GetType().GetProperty("enabled") ?? + component.GetType().BaseType?.GetProperty("enabled") ?? + component.GetType().BaseType?.BaseType?.GetProperty("enabled") ?? + component.GetType().BaseType?.BaseType?.BaseType?.GetProperty("enabled"); + + + if (pi_enabled == null) return; + + var enabled = (bool)pi_enabled.GetValue(component); + + + if (GUI.Toggle(toggleRect, enabled, "") == enabled) return; + + component.RecordUndo(); + pi_enabled.SetValue(component, !enabled); + + } + void name() + { + var nameRect = headerRect.MoveX(54).MoveY(-1); + + var s = new GUIContent(EditorGUIUtility.ObjectContent(component, component.GetType())).text; + s = s.Substring(s.LastIndexOf('(') + 1); + s = s.Substring(0, s.Length - 1); + + if (isPinned) + s += " of " + component.gameObject.name; + + + SetLabelBold(); + + GUI.Label(nameRect, s); + + ResetLabelStyle(); + + } + void nameCurtain() + { + var flatColorRect = headerRect.SetX(pinButtonRect.x + 3).SetXMax(headerRect.xMax); + var gradientRect = headerRect.SetXMax(flatColorRect.x).SetWidthFromRight(30); + + flatColorRect.Draw(backgroundColor); + gradientRect.DrawCurtainLeft(backgroundColor); + + } + void pinButton() + { + if (!isPinned && closeButtonRect.IsHovered()) return; + + + var normalColor = isDarkTheme ? Greyscale(.65f) : Greyscale(.8f); + var hoveredColor = isDarkTheme ? Greyscale(.9f) : normalColor; + var activeColor = Color.white; + + + + SetGUIColor(isPinned ? activeColor : pinButtonRect.IsHovered() ? hoveredColor : normalColor); + + GUI.Label(pinButtonRect, EditorGUIUtility.IconContent("pinned")); + + ResetGUIColor(); + + + SetGUIColor(Color.clear); + + var clicked = GUI.Button(pinButtonRect, ""); + + ResetGUIColor(); + + + if (!clicked) return; + + isPinned = !isPinned; + + if (isPinned && floatingInstance == this) + floatingInstance = null; + + if (!isPinned && !floatingInstance) + floatingInstance = this; + + EditorApplication.RepaintHierarchyWindow(); + + + } + void closeButton() + { + + SetGUIColor(Color.clear); + + if (GUI.Button(closeButtonRect, "")) + Close(); + + ResetGUIColor(); + + + var normalColor = isDarkTheme ? Greyscale(.65f) : Greyscale(.35f); + var hoveredColor = isDarkTheme ? Greyscale(.9f) : normalColor; + + + SetGUIColor(closeButtonRect.IsHovered() ? hoveredColor : normalColor); + + GUI.Label(closeButtonRect, EditorGUIUtility.IconContent("CrossIcon")); + + ResetGUIColor(); + + + if (isPinned) return; + + var escRect = closeButtonRect.Move(-22, -1).SetWidth(70); + + SetGUIEnabled(false); + + if (closeButtonRect.IsHovered()) + GUI.Label(escRect, "Esc"); + + ResetGUIEnabled(); + + } + void rightClick() + { + if (!curEvent.isMouseDown) return; + if (curEvent.mouseButton != 1) return; + if (!headerRect.IsHovered()) return; + + typeof(EditorUtility).InvokeMethod("DisplayObjectContextMenu", Rect.zero.SetPos(curEvent.mousePosition), component, 0); + + } + + startDragging(); + updateDragging(); + stopDragging(); + + background(); + icon(); + toggle(); + name(); + nameCurtain(); + pinButton(); + closeButton(); + rightClick(); + + } + void body() + { + EditorGUIUtility.labelWidth = (this.position.width * .4f).Max(120); + + + scrollPosition = EditorGUILayout.BeginScrollView(Vector2.up * scrollPosition).y; + BeginIndent(17); + + + editor?.OnInspectorGUI(); + + updateHeight(); + + + EndIndent(1); + EditorGUILayout.EndScrollView(); + + + EditorGUIUtility.labelWidth = 0; + + } + void outline() + { + if (Application.platform == RuntimePlatform.OSXEditor) return; + + position.SetPos(0, 0).DrawOutline(Greyscale(.1f)); + + } + + void updateHeight() + { + + ExpandWidthLabelRect(height: -5); + + if (!curEvent.isRepaint) return; + if (isResizingVertically) return; + + + targetHeight = lastRect.y + 30; + + position = position.SetHeight(targetHeight.Min(maxHeight)); + + + prevHeight = position.height; + + } + void updatePosition() + { + if (!curEvent.isLayout) return; + + void calcDeltaTime() + { + deltaTime = (float)(EditorApplication.timeSinceStartup - lastLayoutTime); + + if (deltaTime > .05f) + deltaTime = .0166f; + + lastLayoutTime = EditorApplication.timeSinceStartup; + + } + void resetCurPos() + { + if (currentPosition != default && !isPinned) return; + + currentPosition = position.position; // position.position is always int, which can't be used for lerping + + } + void lerpCurPos() + { + if (isPinned) return; + + var speed = 9; + + MathUtil.SmoothDamp(ref currentPosition, targetPosition, speed, ref positionDeriv, deltaTime); + // MathfUtils.Lerp(ref currentPosition, targetPosition, speed, deltaTime); + + } + void setCurPos() + { + if (isPinned) return; + + position = position.SetPos(currentPosition); + + } + + calcDeltaTime(); + resetCurPos(); + lerpCurPos(); + setCurPos(); + + } + void closeOnEscape() + { + if (isPinned) return; + if (!curEvent.isKeyDown) return; + if (curEvent.keyCode != KeyCode.Escape) return; + + Close(); + } + + void horizontalResize() + { + var showingScrollbar = targetHeight > maxHeight; + + var resizeArea = this.position.SetPos(0, 0).SetWidthFromRight(showingScrollbar ? 3 : 5).AddHeightFromBottom(-20); + + void startResize() + { + if (isDragged) return; + if (isResizingHorizontally) return; + if (!curEvent.isMouseDown && !curEvent.isMouseDrag) return; + if (!resizeArea.IsHovered()) return; + + isResizingHorizontally = true; + + resizeStartMousePos = curEvent.mousePosition_screenSpace; + resizeStartWindowSize = this.position.size; + + } + void updateResize() + { + if (!isResizingHorizontally) return; + + + var resizedWidth = resizeStartWindowSize.x + curEvent.mousePosition_screenSpace.x - resizeStartMousePos.x; + + var width = resizedWidth.Max(300); + + if (!curEvent.isRepaint) + position = position.SetWidth(width); + + + EditorGUIUtility.hotControl = EditorGUIUtility.GetControlID(FocusType.Passive); + // GUI.focused + + } + void stopResize() + { + if (!isResizingHorizontally) return; + if (!curEvent.isMouseUp) return; + + isResizingHorizontally = false; + + EditorGUIUtility.hotControl = 0; + + } + + + EditorGUIUtility.AddCursorRect(resizeArea, MouseCursor.ResizeHorizontal); + + startResize(); + updateResize(); + stopResize(); + + } + void verticalResize() + { + var resizeArea = this.position.SetPos(0, 0).SetHeightFromBottom(5); + + void startResize() + { + if (isDragged) return; + if (isResizingVertically) return; + if (!curEvent.isMouseDown && !curEvent.isMouseDrag) return; + if (!resizeArea.IsHovered()) return; + + isResizingVertically = true; + + resizeStartMousePos = curEvent.mousePosition_screenSpace; + resizeStartWindowSize = this.position.size; + + } + void updateResize() + { + if (!isResizingVertically) return; + + + var resizedHeight = resizeStartWindowSize.y + curEvent.mousePosition_screenSpace.y - resizeStartMousePos.y; + + var height = resizedHeight.Min(targetHeight).Max(50); + + if (!curEvent.isRepaint) + position = position.SetHeight(height); + + maxHeight = height; + + + EditorGUIUtility.hotControl = EditorGUIUtility.GetControlID(FocusType.Passive); + // GUI.focused + + } + void stopResize() + { + if (!isResizingVertically) return; + if (!curEvent.isMouseUp) return; + + isResizingVertically = false; + + EditorGUIUtility.hotControl = 0; + + } + + + EditorGUIUtility.AddCursorRect(resizeArea, MouseCursor.ResizeVertical); + + startResize(); + updateResize(); + stopResize(); + + } + + + background(); + + horizontalResize(); + verticalResize(); + + + header(); + + Space(3); + body(); + outline(); + + Space(7); + + + updatePosition(); + closeOnEscape(); + + + if (!isPinned) + Repaint(); + + EditorApplication.delayCall -= Repaint; + EditorApplication.delayCall += Repaint; + + } + + public Vector2 targetPosition; + public Vector2 currentPosition; + Vector2 positionDeriv; + float deltaTime; + double lastLayoutTime; + + bool isDragged; + Vector2 dragStartMousePos; + Vector2 dragStartWindowPos; + + public bool isResizingHorizontally; + public bool isResizingVertically; + public Vector2 resizeStartMousePos; + public Vector2 resizeStartWindowSize; + + public float scrollPosition; + + public float targetHeight; + public float maxHeight; + public float prevHeight; + + + + + + void OnLostFocus() + { + if (isPinned) return; + + if (curEvent.holdingAlt && EditorWindow.focusedWindow.GetType().Name == "SceneHierarchyWindow") + CloseNextFrameIfNotRefocused(); + else + Close(); + + } + + void CloseNextFrameIfNotRefocused() + { + EditorApplication.delayCall += () => { if (EditorWindow.focusedWindow != this) Close(); }; + } + + public bool isPinned; + + + + + + public void Init(Component component) + { + if (editor) + editor.DestroyImmediate(); + + this.component = component; + this.componentIid = component.GetInstanceID(); + this.editor = Editor.CreateEditor(component); + + } + + void OnDestroy() + { + editor?.DestroyImmediate(); + + editor = null; + component = null; + + EditorPrefs.SetFloat("vHierarchy-componentWindowWidth", position.width); + + } + + public Component component; + public Editor editor; + + public int componentIid; + + + + + + public static void CreateFloatingInstance(Vector2 position) + { + floatingInstance = ScriptableObject.CreateInstance(); + + floatingInstance.ShowPopup(); + + + floatingInstance.maxHeight = EditorGUIUtility.GetMainWindowPosition().height * .7f; + + + var savedWidth = EditorPrefs.GetFloat("vHierarchy-componentWindowWidth", minWidth); + + var width = savedWidth.Max(minWidth); + + floatingInstance.position = Rect.zero.SetPos(position).SetWidth(width).SetHeight(200); + floatingInstance.prevHeight = floatingInstance.position.height; + + floatingInstance.targetPosition = position; + + } + + public static VHierarchyComponentWindow floatingInstance; + + public static float minWidth => 300; + + } +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchyComponentWindow.cs.meta b/vfolders2/vHierarchy/VHierarchyComponentWindow.cs.meta new file mode 100644 index 0000000..741ab1d --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyComponentWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4b48d49a631ab443990f28938cbdedb8 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyController.cs b/vfolders2/vHierarchy/VHierarchyController.cs new file mode 100644 index 0000000..0604bfd --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyController.cs @@ -0,0 +1,517 @@ +#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.VHierarchy; +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 class VHierarchyController + { + + public void UpdateExpandQueue() + { + if (treeViewAnimatesExpansion) return; + + if (!expandQueue_toAnimate.Any()) + { + if (!expandQueue_toCollapseAfterAnimation.Any()) return; + + foreach (var r in expandQueue_toCollapseAfterAnimation) + SetExpanded_withoutAnimation(r, false); + + expandQueue_toCollapseAfterAnimation.Clear(); + + return; + + } + + + var id = expandQueue_toAnimate.First().id; + var expand = expandQueue_toAnimate.First().expand; + + + + var itemIndex = treeViewControllerData.InvokeMethod("GetRow", id); + var items = treeViewControllerData.GetMemberValue>("m_Rows"); + + var stuckCollapsing = itemIndex != -1 && items[itemIndex].id != id; // happens when collapsing long hierarchies due to a bug in TreeViewController + + if (stuckCollapsing) { window.SendEvent(new Event() { type = EventType.KeyDown, keyCode = KeyCode.None }); return; } + + + + + if (expandedIds.Contains(id) != expand) + SetExpanded_withAnimation(id, expand); + + expandQueue_toAnimate.RemoveAt(0); + + + window.Repaint(); + + + // must be called from gui because reflected methods rely on Event.current + + } + + public List expandQueue_toAnimate = new(); + public List expandQueue_toCollapseAfterAnimation = new(); + + public struct ExpandQueueEntry { public int id; public bool expand; } + + public bool animatingExpansion => expandQueue_toAnimate.Any() || expandQueue_toCollapseAfterAnimation.Any(); + + + + + + + public void UpdateScrollAnimation() + { + if (!animatingScroll) return; + + + var lerpSpeed = 10; + + var lerpedScrollPos = MathUtil.SmoothDamp(currentScrollPos, targetScrollPos, lerpSpeed, ref scrollPosDerivative, editorDeltaTime); + + SetScrollPos(lerpedScrollPos); + + window.Repaint(); + + + + if (lerpedScrollPos.DistanceTo(targetScrollPos) > .4f) return; + + SetScrollPos(targetScrollPos); + + animatingScroll = false; + + + } + + public float targetScrollPos; + public float scrollPosDerivative; + + public bool animatingScroll; + + + + + + + + public void UpdateHighlightAnimation() + { + if (!animatingHighlight) return; + + + var lerpSpeed = 1.3f; + + MathUtil.SmoothDamp(ref highlightAmount, 0, lerpSpeed, ref highlightDerivative, editorDeltaTime); + + window.Repaint(); + + + + if (highlightAmount > .05f) return; + + highlightAmount = 0; + + animatingHighlight = false; + + + } + + public float highlightAmount; + public float highlightDerivative; + + public bool animatingHighlight; + + public GameObject objectToHighlight; + + + + + + + + public void UpdateState() + { + var sceneHierarchy = window?.GetFieldValue("m_SceneHierarchy"); + + treeViewController = sceneHierarchy.GetFieldValue("m_TreeView"); + treeViewControllerData = treeViewController.GetMemberValue("data"); + + + + var treeViewControllerState = treeViewController?.GetPropertyValue("state"); + + currentScrollPos = treeViewControllerState?.scrollPos.y ?? 0; + + expandedIds = treeViewControllerState?.expandedIDs ?? new List(); + + + + var treeViewAnimator = treeViewController?.GetMemberValue("m_ExpansionAnimator"); + var treeViewAnimatorSetup = treeViewAnimator?.GetMemberValue("m_Setup"); + + treeViewAnimatesScroll = treeViewController?.GetMemberValue("m_FramingAnimFloat").isAnimating ?? false; + + treeViewAnimatesExpansion = treeViewAnimator?.GetMemberValue("isAnimating") ?? false; + + } + + object treeViewController; + object treeViewControllerData; + + public float currentScrollPos; + + public List expandedIds = new(); + + public bool treeViewAnimatesScroll; + public bool treeViewAnimatesExpansion; + + public int GetRowIndex(int instanceId) + { + return treeViewControllerData.InvokeMethod("GetRow", instanceId); + } + + + + + + + + + + + + + + + + + public void ToggleExpanded(int id) + { + SetExpanded_withAnimation(id, !expandedIds.Contains(id)); + + window.Repaint(); + + } + + public void CollapseAll() + { + var expandedRoots = new List(); + var expandedChildren = new List(); + + foreach (var iid in expandedIds) + if (EditorUtility.InstanceIDToObject(iid) is GameObject expandedGo) + if (expandedGo.transform.parent) + expandedChildren.Add(expandedGo); + else + expandedRoots.Add(expandedGo); + + + + expandQueue_toCollapseAfterAnimation = expandedChildren.Select(r => r.GetInstanceID()).ToList(); + + expandQueue_toAnimate = expandedRoots.Select(r => new ExpandQueueEntry { id = r.GetInstanceID(), expand = false }) + .OrderBy(r => GetRowIndex(r.id)).ToList(); + + StartScrollAnimation(targetScrollPos: 0); + + + window.Repaint(); + + } + + public void Isolate(int targetId) + { + + List getParents(int id) + { + var parentIds = new List(); + + if (EditorUtility.InstanceIDToObject(id) is not GameObject go) return parentIds; + + + while (go.transform.parent) + parentIds.Add((go = go.transform.parent.gameObject).GetInstanceID()); + + parentIds.Add(go.scene.handle); + + + return parentIds; + + } + + var targetItemParents = getParents(targetId); + + + + var itemsToCollapse = expandedIds.ToList(); + + itemsToCollapse.Remove(targetId); + itemsToCollapse.RemoveAll(r => targetItemParents.Contains(r)); + itemsToCollapse.RemoveAll(r => itemsToCollapse.Intersect(getParents(r)).Any()); + + if (EditorUtility.InstanceIDToObject(targetId) is GameObject) + itemsToCollapse.RemoveAll(r => EditorUtility.InstanceIDToObject(r) is not GameObject); // won't collapse scenes + + + + + expandQueue_toAnimate = itemsToCollapse.Select(id => new ExpandQueueEntry { id = id, expand = false }) + .Append(new ExpandQueueEntry { id = targetId, expand = true }) + .OrderBy(r => GetRowIndex(r.id)).ToList(); + + + window.Repaint(); + + } + + + + + + public void StartExpandAnimation(List targetExpandedIds) + { + + var toExpand = targetExpandedIds.Except(expandedIds).ToHashSet(); + var toCollapse = expandedIds.Except(targetExpandedIds).ToHashSet(); + + + + + // hanlde destroyed objects + + var sceneIds = Enumerable.Range(0, EditorSceneManager.sceneCount).Select(i => EditorSceneManager.GetSceneAt(i).handle).ToHashSet(); + + var toExpand_destroyed = toExpand.Where(id => !sceneIds.Contains(id) && Resources.InstanceIDToObject(id) as GameObject == null).ToHashSet(); + var toCollapse_destroyed = toCollapse.Where(id => !sceneIds.Contains(id) && Resources.InstanceIDToObject(id) as GameObject == null).ToHashSet(); + + + foreach (var id in toExpand_destroyed) + expandedIds.Add(id); + + foreach (var id in toCollapse_destroyed) + expandedIds.Remove(id); + + + toExpand.ExceptWith(toExpand_destroyed); + toCollapse.ExceptWith(toCollapse_destroyed); + + + + + + + // hanlde non-animated expansions/collapses + + bool hasParentToCollapse(int id) + { + var go = Resources.InstanceIDToObject(id) as GameObject; + + if (!go) return false; + if (!go.transform.parent) return false; + + + var parentId = go.transform.parent.gameObject.GetInstanceID(); + + return toCollapse.Contains(parentId) + || hasParentToCollapse(parentId); + + } + bool areAllParentsExpanded(int id) + { + var go = Resources.InstanceIDToObject(id) as GameObject; + + if (!go) return true; + if (!go.transform.parent) return true; + + + var parentId = go.transform.parent.gameObject.GetInstanceID(); + + return expandedIds.Contains(parentId) + && areAllParentsExpanded(parentId); + + } + + var toExpand_beforeAnimation = toExpand.Where(id => !areAllParentsExpanded(id)).ToHashSet(); + var toCollapse_afterAnimation = toCollapse.Where(id => hasParentToCollapse(id)).ToHashSet(); + + + foreach (var id in toExpand_beforeAnimation) + SetExpanded_withoutAnimation(id, true); + + foreach (var id in toCollapse_afterAnimation) + expandQueue_toCollapseAfterAnimation.Add(id); + + + toExpand.ExceptWith(toExpand_beforeAnimation); + toCollapse.ExceptWith(toCollapse_afterAnimation); + + + + + + + // setup animation + + expandQueue_toAnimate = toCollapse.Select(id => new ExpandQueueEntry { id = id, expand = false }) + .Concat(toExpand.Select(id => new ExpandQueueEntry { id = id, expand = true })) + .OrderBy(r => GetRowIndex(r.id)).ToList(); + + } + + public void SetExpandedIds(List targetExpandedIds) + { + treeViewControllerData.InvokeMethod("SetExpandedIDs", targetExpandedIds.ToArray()); + } + public void SetExpanded_withAnimation(int instanceId, bool expanded) + { + treeViewController.InvokeMethod("ChangeFoldingForSingleItem", instanceId, expanded); + } + public void SetExpanded_withoutAnimation(int instanceId, bool expanded) + { + treeViewControllerData.InvokeMethod("SetExpanded", instanceId, expanded); + } + + + + public void StartScrollAnimation(float targetScrollPos) + { + if (targetScrollPos.DistanceTo(currentScrollPos) < .05f) return; + + this.targetScrollPos = targetScrollPos; + + animatingScroll = true; + + } + + public void SetScrollPos(float targetScrollPos) + { + window.GetMemberValue("m_SceneHierarchy").GetMemberValue("m_TreeViewState").scrollPos = Vector2.up * targetScrollPos; + } + + + + public void RevealObject(GameObject go, bool expand, bool highlight, bool snapToTopMargin) + { + + var idsToExpand = new List(); + + if (expand && go.transform.childCount > 0) + idsToExpand.Add(go.GetInstanceID()); + + var cur = go.transform; + while (cur = cur.parent) + idsToExpand.Add(cur.gameObject.GetInstanceID()); + + idsToExpand.Add(go.scene.handle); + + idsToExpand.RemoveAll(r => expandedIds.Contains(r)); + + + + + foreach (var id in idsToExpand.SkipLast(1)) + SetExpanded_withoutAnimation(id, true); + + if (idsToExpand.Any()) + SetExpanded_withAnimation(idsToExpand.Last(), true); + + + + + var rowCount = treeViewControllerData.GetMemberValue("m_Rows").Count; + var maxScrollPos = rowCount * 16 - window.position.height + 26.9f; + + var rowIndex = treeViewControllerData.InvokeMethod("GetRow", go.GetInstanceID()); + var rowPos = rowIndex * 16f + 8; + + var scrollAreaHeight = window.GetMemberValue("treeViewRect").height; + + + + + var margin = 48; + + var targetScrollPos = 0f; + + if (expand) + targetScrollPos = (rowPos - margin).Min(maxScrollPos) + .Max(0); + else + targetScrollPos = currentScrollPos.Min(rowPos - margin) + .Max(rowPos - scrollAreaHeight + margin) + .Min(maxScrollPos) + .Max(0); + if (targetScrollPos < 25) + targetScrollPos = 0; + + + + StartScrollAnimation(targetScrollPos); + + + + + if (!highlight) return; + + highlightAmount = 2.2f; + + animatingHighlight = true; + + objectToHighlight = go; + + + } + + + + + + + + + + + + + + + public VHierarchyController(EditorWindow window) => this.window = window; + + public EditorWindow window; + + public VHierarchyGUI gui => VHierarchy.guis_byWindow[window]; + + } +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchyController.cs.meta b/vfolders2/vHierarchy/VHierarchyController.cs.meta new file mode 100644 index 0000000..3983a7d --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyController.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3513d7b44a8b94c8d800e841067bf035 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyData.cs b/vfolders2/vHierarchy/VHierarchyData.cs new file mode 100644 index 0000000..75be7b6 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyData.cs @@ -0,0 +1,227 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditor.ShortcutManagement; +using System.Reflection; +using System.Linq; +using UnityEngine.UIElements; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using Type = System.Type; +using static VHierarchy.VHierarchyCache; +using static VHierarchy.Libs.VUtils; +using static VHierarchy.Libs.VGUI; +// using static VTools.VDebug; + + +namespace VHierarchy +{ + public class VHierarchyData : ScriptableObject, ISerializationCallbackReceiver + { + + public SerializableDictionary sceneDatas_byGuid = new(); + + + [System.Serializable] + public class SceneData + { + public SerializableDictionary goDatas_byGlobalId = new(); + } + + + [System.Serializable] + public class GameObjectData + { + public int colorIndex; + public string iconNameOrGuid = ""; + + public bool isIconRecursive; + public bool isColorRecursive; + + + [System.NonSerialized] + public SceneData sceneData; // set in GetGameObjectData + + } + + public void OnBeforeSerialize() => VHierarchy.OnDataSerialization(); + public void OnAfterDeserialize() { } + + + + + + public List bookmarks = new(); + + [System.Serializable] + public class Bookmark + { + + public GameObject go + { + get + { + if (_go == null) + if (!failedToLoadSceneObject) // to prevent continuous GlobalID.GetObjects() calls if object is deleted + VHierarchy.unloadedSceneBookmarks_sceneGuids.Add(globalId.guid); + + return _go; + + } + } + public GameObject _go; + + [System.NonSerialized] + public bool failedToLoadSceneObject; + + + + public bool isLoadable => go != null; + + public bool isDeleted + { + get + { + if (isLoadable) + return false; + + if (!AssetDatabase.LoadAssetAtPath(globalId.guid.ToPath())) + return true; + + for (int i = 0; i < EditorSceneManager.sceneCount; i++) + if (EditorSceneManager.GetSceneAt(i).path == globalId.guid.ToPath()) + return true; + + return false; + + } + } + + public string assetPath => globalId.guid.ToPath(); + + public string name => go ? go.name : "Can't load object"; + + + + public Bookmark(GameObject o) + { + globalId = o.GetGlobalID(); + + _go = o; + + } + + public GlobalID globalId; + + } + + + + + public List bookmarkedScenePaths = new(); + + + + + + + + [CustomEditor(typeof(VHierarchyData))] + class Editor : UnityEditor.Editor + { + public override void OnInspectorGUI() + { + var style = new GUIStyle(EditorStyles.label) { wordWrap = true }; + + void normal() + { + if (teamModeEnabled) return; + + SetGUIEnabled(false); + BeginIndent(0); + + Space(10); + EditorGUILayout.LabelField("This file stores data about which icons and colors are assigned to objects, along with bookmarks from navigation bar and scene selector.", style); + + Space(6); + GUILayout.Label("If there are multiple people working on the project, you might want to store icon/color data in scenes to avoid merge conflicts. To do that, click the â‹® button at the top right corner and enable Team Mode.", style); + + EndIndent(10); + ResetGUIEnabled(); + } + void teamMode() + { + if (!teamModeEnabled) return; + + SetGUIEnabled(false); + BeginIndent(0); + + Space(10); + EditorGUILayout.LabelField("Now that Team Mode is enabled, create an empty script that inherits from VHierarchy.VHierarchyDataComponent and add it to any object in a scene.", style); + + Space(6); + GUILayout.Label("If such a script is present in a scene - all icon/color data for that scene will be serialized in that script, otherwise icon/color data for the scene will end up in this file.", style); + + EndIndent(10); + ResetGUIEnabled(); + } + + normal(); + teamMode(); + + } + } + + public static bool teamModeEnabled { get => EditorPrefsCached.GetBool("vHierarchy-teamModeEnabled", false); set => EditorPrefsCached.SetBool("vHierarchy-teamModeEnabled", value); } + + + + [ContextMenu("Enable Team Mode", isValidateFunction: false, priority: 1)] + public void EnableTeamMode() + { + var option = EditorUtility.DisplayDialogComplex("Licensing notice", + "To use vHierarchy 2 within a team, licenses must be purchased for each individual user as per the Asset Store EULA.\n\n Sharing one license across the team is illegal and considered piracy.", + "Acknowledge", + "Cancel", + "Purchase more seats"); + if (option == 2) + Application.OpenURL("https://prf.hn/click/camref:1100lGLBn/pubref:teammode/destination:https://assetstore.unity.com/packages/tools/utilities/vhierarchy-2-253397"); + // Application.OpenURL("https://assetstore.unity.com/packages/slug/253397"); + + + + if (option != 0) return; + + teamModeEnabled = true; + + VHierarchy.goInfoCache.Clear(); + VHierarchy.goDataCache.Clear(); + + EditorApplication.RepaintHierarchyWindow(); + + } + + [ContextMenu("Disable Team Mode", isValidateFunction: false, priority: 2)] + public void DisableTeamMode() + { + teamModeEnabled = false; + + VHierarchy.goInfoCache.Clear(); + VHierarchy.goDataCache.Clear(); + + EditorApplication.RepaintHierarchyWindow(); + + } + + [ContextMenu("Enable Team Mode", isValidateFunction: true, priority: 1)] bool asd() => !teamModeEnabled; + [ContextMenu("Disable Team Mode", isValidateFunction: true, priority: 2)] bool ads() => teamModeEnabled; + + + + + + } +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchyData.cs.meta b/vfolders2/vHierarchy/VHierarchyData.cs.meta new file mode 100644 index 0000000..cd5eee5 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyData.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 3a9752b0c8e144801967e6897679604b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyDataComponent.cs b/vfolders2/vHierarchy/VHierarchyDataComponent.cs new file mode 100644 index 0000000..744c302 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyDataComponent.cs @@ -0,0 +1,142 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditor.ShortcutManagement; +using System.Reflection; +using System.Linq; +using UnityEngine.UIElements; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using UnityEditor.Experimental.SceneManagement; +using Type = System.Type; +using static VHierarchy.VHierarchyData; +using static VHierarchy.Libs.VUtils; +using static VHierarchy.Libs.VGUI; +// using static VTools.VDebug; + + +namespace VHierarchy +{ + [ExecuteInEditMode] + public abstract class VHierarchyDataComponent : MonoBehaviour, ISerializationCallbackReceiver + { + public void Awake() + { + void register() + { + VHierarchy.dataComponents_byScene[gameObject.scene] = this; + } + void handleSceneDuplication() + { + if (sceneData == null) return; + if (!sceneData.goDatas_byGlobalId.Any()) return; + + + var curSceneGuid = gameObject.scene.path.ToGuid(); + var dataSceneGuid = sceneData.goDatas_byGlobalId.Keys.First().guid; + + if (curSceneGuid == dataSceneGuid) return; + + + var newDic = new SerializableDictionary(); + + foreach (var kvp in sceneData.goDatas_byGlobalId) + newDic[new GlobalID(kvp.Key.ToString().Replace(dataSceneGuid, curSceneGuid))] = kvp.Value; + + + sceneData.goDatas_byGlobalId = newDic; + + + EditorSceneManager.MarkSceneDirty(gameObject.scene); + EditorSceneManager.SaveScene(gameObject.scene); + + } + + register(); + handleSceneDuplication(); + + } + + + public SceneData sceneData; + + + public void OnBeforeSerialize() => VHierarchy.goDataCache.Clear(); + public void OnAfterDeserialize() => VHierarchy.goDataCache.Clear(); + + + + [CustomEditor(typeof(VHierarchyDataComponent), true)] + class Editor : UnityEditor.Editor + { + public override void OnInspectorGUI() + { + var style = new GUIStyle(EditorStyles.label) { wordWrap = true }; + + + void teamModeOn() + { + if (!VHierarchyData.teamModeEnabled) return; + + SetGUIEnabled(false); + BeginIndent(0); + + Space(4); + EditorGUILayout.LabelField("This component stores vHierarchy's data about which icons and colors are assigned to objects in this scene", style); + + // Space(6); + // EditorGUILayout.LabelField("You can disable Team Mode to store icon/color data in vHierarchy Data scriptable object, as it is done by default", style); + + + Space(2); + + EndIndent(10); + ResetGUIEnabled(); + + + + + // Space(10); + + // if (!GUILayout.Button("Disable Team Mode", GUILayout.Height(27))) return; + + // VHierarchy.data?.DisableTeamMode(); + + } + void teamModeOff() + { + if (VHierarchyData.teamModeEnabled) return; + + SetGUIEnabled(false); + BeginIndent(0); + + Space(4); + EditorGUILayout.LabelField("Enable Team Mode to store icon/color data for this scene in this component", style); + + Space(2); + + EndIndent(10); + ResetGUIEnabled(); + + + + Space(4); + + if (!GUILayout.Button("Enable Team Mode", GUILayout.Height(27))) return; + + VHierarchy.data?.EnableTeamMode(); + + } + + teamModeOn(); + teamModeOff(); + + + } + } + + } +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchyDataComponent.cs.meta b/vfolders2/vHierarchy/VHierarchyDataComponent.cs.meta new file mode 100644 index 0000000..be05b15 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyDataComponent.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9e20c7ea1a24b4a899ba82e98ad2b375 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyGUI.cs b/vfolders2/vHierarchy/VHierarchyGUI.cs new file mode 100644 index 0000000..76c7114 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyGUI.cs @@ -0,0 +1,1024 @@ +#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.VHierarchy; +using static VHierarchy.VHierarchyData; +using static VHierarchy.VHierarchyCache; + + + +namespace VHierarchy +{ + public class VHierarchyGUI + { + + public void RowGUI_GameObject(Rect rowRect, GameObject go) + { + var fullRowRect = rowRect.SetX(32).SetXMax(rowRect.xMax + 16); + + var isRowHovered = fullRowRect.AddWidthFromRight(32).IsHovered(); + + + var isRowSelected = false; + var isRowBeingRenamed = false; + var isDefaultParent = go == defaultParent; + + void setState() + { + void set_isRowSelected() + { + if (!curEvent.isRepaint) return; + + + var dragging = dragSelectionList != null + && dragSelectionList.Any(); + + isRowSelected = dragging ? (dragSelectionList.Contains(go.GetInstanceID())) : Selection.Contains(go); + + } + void set_lastVisibleSelectedRowRect() + { + if (!Selection.gameObjects.Contains(go)) return; + + lastVisibleSelectedRowRect = rowRect; + + } + void set_mousePressed() + { + if (curEvent.isMouseDown && isRowHovered) + mousePressed = true; + + if (curEvent.isMouseUp || curEvent.isMouseLeaveWindow || curEvent.isDragPerform) + mousePressed = false; + + } + void set_hoveredGo() + { + if (curEvent.isLayout) + VHierarchy.hoveredGo = null; + + if (curEvent.isRepaint && isRowHovered) + VHierarchy.hoveredGo = go; + + } + + set_isRowSelected(); + set_lastVisibleSelectedRowRect(); + set_mousePressed(); + set_hoveredGo(); + + isRowBeingRenamed = renamingRow && isRowSelected; + + } + + + void drawing() + { + if (!curEvent.isRepaint) { hierarchyLines_isFirstRowDrawn = false; return; } + + var goInfo = GetGameObjectInfo(go); + + var drawBackgroundColor = goInfo.hasColor; + var drawCustomIcon = goInfo.hasIcon; + var drawDefaultIcon = !drawCustomIcon && (isRowBeingRenamed || (!VHierarchyMenu.minimalModeEnabled || (PrefabUtility.IsAddedGameObjectOverride(go) && PrefabUtility.IsPartOfPrefabInstance(go)))); + + var makeTriangleBrighter = drawBackgroundColor && !goInfo.isGreyColor && isDarkTheme; + var makeNameBrighter = drawBackgroundColor && !goInfo.isGreyColor && isDarkTheme && !isDefaultParent; + var makeNameBold = isDefaultParent && isDarkTheme; + + Color defaultBackground; + + + void set_defaultBackground() + { + var selectedFocused = GUIColors.selectedBackground; + var selectedUnfocused = isDarkTheme ? Greyscale(.3f) : Greyscale(.68f); + var hovered = isDarkTheme ? Greyscale(.265f) : Greyscale(.7f); + var normal = GUIColors.windowBackground; + + if (isRowSelected && !isRowBeingRenamed) + defaultBackground = isTreeFocused ? selectedFocused : selectedUnfocused; + + else if (isRowHovered) + defaultBackground = hovered; + + else + defaultBackground = normal; + + } + void hideDefaultIcon() + { + if (drawDefaultIcon) return; + + rowRect.SetWidth(16).Draw(defaultBackground); + + } + void hideName() + { + if (!drawBackgroundColor && (drawCustomIcon || drawDefaultIcon) && !makeNameBold) return; + + var nameRect = rowRect.MoveX(16).SetWidth(go.name.GetLabelWidth(isBold: isDefaultParent)); +#if UNITY_2023_2_OR_NEWER + if (!go.activeInHierarchy && PrefabUtility.IsPartOfPrefabInstance(go)) + nameRect.width *= 1.1f; +#endif + + nameRect.Draw(defaultBackground); + + } + + void backgroundColor() + { + if (!drawBackgroundColor) return; + + + + var color = goInfo.color; + + if (isRowHovered) + color *= isDarkTheme ? 1.1f : .92f; + + if (isRowSelected) + color *= isDarkTheme ? 1.2f : .8f; + + if (palette?.colorGradientsEnabled == false) + color = MathUtil.Lerp(color, Greyscale(.2f), isDarkTheme ? .25f : .03f); + + if (goInfo.hasColorByRecursion) + color = MathUtil.Lerp(color, Greyscale(isDarkTheme ? .25f : .8f), .5f); + + + + + + var colorRect = rowRect.AddWidthFromRight(28).AddWidth(16); + + if (goInfo.hasColorByRecursion) + colorRect = colorRect.AddWidthFromRight(goInfo.maxColorRecursionDepth * 14); + + if (!isRowSelected && !goInfo.hasColorByRecursion) + colorRect = colorRect.AddHeightFromMid(EditorGUIUtility.pixelsPerPoint >= 2 ? -.5f : -1); + + if (goInfo.hasColorByRecursion) + colorRect = colorRect.MoveY(EditorGUIUtility.pixelsPerPoint >= 2 ? -.25f : -.5f); + + if (palette?.colorGradientsEnabled == false || goInfo.isGreyColor) { colorRect.Draw(color); return; } + + + + var hasLeftGradient = colorRect.x > 32; + + if (hasLeftGradient) + colorRect = colorRect.AddWidthFromRight(3); + + if (PrefabUtility.HasPrefabInstanceAnyOverrides(go, false) && PrefabUtility.IsOutermostPrefabInstanceRoot(go) && !hasLeftGradient) + colorRect = colorRect.AddWidthFromRight(EditorGUIUtility.pixelsPerPoint >= 2 ? -2.5f : -3); + + + + var leftGradientWith = hasLeftGradient ? 22 : 0; + var rightGradientWidth = (fullRowRect.width * .77f).Min(colorRect.width - leftGradientWith); + + var leftGradientRect = colorRect.SetWidth(leftGradientWith); + var rightGradientRect = colorRect.SetWidthFromRight(rightGradientWidth); + + var flatColorRect = colorRect.SetX(leftGradientRect.xMax).SetXMax(rightGradientRect.x); + + + + + + + leftGradientRect.AddWidth(1).DrawCurtainLeft(color); + + flatColorRect.AddWidth(1).Draw(color); + + rightGradientRect.Draw(color.MultiplyAlpha(.1f)); + rightGradientRect.DrawCurtainRight(color); + + + } + void triangle() + { + if (!drawBackgroundColor) return; + if (go.transform.childCount == 0) return; + + var triangleRect = rowRect.MoveX(-15.5f).SetWidth(16).Resize(1.5f); + + GUI.DrawTexture(triangleRect, EditorIcons.GetIcon(controller.expandedIds.Contains(go.GetInstanceID()) ? "IN_foldout_on" : "IN_foldout")); + + + if (!makeTriangleBrighter) return; + + GUI.DrawTexture(triangleRect, EditorIcons.GetIcon(controller.expandedIds.Contains(go.GetInstanceID()) ? "IN_foldout_on" : "IN_foldout")); + + } + void name() + { + if (!drawBackgroundColor && (drawCustomIcon || drawDefaultIcon) && !makeNameBold) return; + if (isRowBeingRenamed) return; + + + var nameRect = rowRect.MoveX(18).AddHeight(1); + + if (VHierarchyMenu.minimalModeEnabled && !drawCustomIcon && !drawDefaultIcon) + nameRect = nameRect.MoveX(-17); + + if (drawBackgroundColor && !goInfo.isGreyColor) + nameRect = nameRect.MoveY(.5f); + + if (!go.activeInHierarchy) // correcting unity's style padding inconsistencies + if (PrefabUtility.IsPartOfAnyPrefab(go)) + nameRect = nameRect.MoveY(-1); + else + nameRect = nameRect.Move(-1, -1.5f); + + if (makeNameBrighter && go.activeInHierarchy && !makeNameBold) + nameRect = nameRect.MoveX(-2).MoveY(-.5f); + + + + var styleName = PrefabUtility.IsPartOfAnyPrefab(go) ? + (go.activeInHierarchy ? "PR PrefabLabel" : "PR DisabledPrefabLabel") : + (go.activeInHierarchy ? "TV Line" : "PR DisabledLabel"); + + if (makeNameBrighter && go.activeInHierarchy) + styleName = "WhiteLabel"; + + if (makeNameBold) + styleName = "TV LineBold"; + + + + if (makeNameBrighter) + SetGUIColor(Greyscale(!go.activeInHierarchy ? 1.4f : isRowSelected ? 1 : goInfo.hasColorByRecursion ? .9f : .95f)); + + GUI.skin.GetStyle(styleName).Draw(nameRect, go.name, false, false, isRowSelected || makeNameBold, isTreeFocused || makeNameBold); + + if (makeNameBrighter) + ResetGUIColor(); + + } + void defaultIcon() + { + if (!drawBackgroundColor) return; + if (!drawDefaultIcon) return; + + var iconRect = rowRect.SetWidth(16); + var icon = PrefabUtility.GetIconForGameObject(go); + + if (!isDarkTheme && isRowSelected && isTreeFocused && icon.name == "GameObject Icon") + icon = EditorIcons.GetIcon("GameObject On Icon"); + + + SetGUIColor(go.activeInHierarchy ? Color.white : Greyscale(1, .4f)); + + GUI.DrawTexture(iconRect, icon); + + if (PrefabUtility.IsAddedGameObjectOverride(go)) + GUI.DrawTexture(iconRect, EditorIcons.GetIcon("PrefabOverlayAdded Icon")); + + ResetGUIColor(); + + } + void customIcon() + { + if (!drawCustomIcon) return; + + var icon = EditorIcons.GetIcon(goInfo.iconNameOrPath) ?? Texture2D.blackTexture; + + var iconRect = rowRect.SetWidth(16); + + if (icon.width < icon.height) iconRect = iconRect.SetWidthFromMid(iconRect.height * icon.width / icon.height); + if (icon.height < icon.width) iconRect = iconRect.SetHeightFromMid(iconRect.width * icon.height / icon.width); + + + SetGUIColor(go.activeInHierarchy ? Color.white : Greyscale(1, .4f)); + + GUI.DrawTexture(iconRect, icon); + + ResetGUIColor(); + + } + void hierarchyLines() + { + if (!VHierarchyMenu.hierarchyLinesEnabled) return; + + + var lineThickness = 1f; + var lineColor = isDarkTheme ? Greyscale(1, .165f) : Greyscale(0, .23f); + + var depth = ((rowRect.x - 60) / 14).RoundToInt().Max(0); + + bool isLastChild(Transform transform) => transform.parent?.GetChild(transform.parent.childCount - 1) == transform; + bool hasChilren(Transform transform) => transform.childCount > 0; + + void calcVerticalGaps_beforeFirstRowDrawn() + { + if (hierarchyLines_isFirstRowDrawn) return; + + hierarchyLines_verticalGaps.Clear(); + + var curTransform = go.transform.parent; + var curDepth = depth - 1; + + while (curTransform != null && curTransform.parent != null) + { + if (isLastChild(curTransform)) + hierarchyLines_verticalGaps.Add(curDepth - 1); + + curTransform = curTransform.parent; + curDepth--; + } + + } + void updateVerticalGaps_beforeNextRowDrawn() + { + if (isLastChild(go.transform)) + hierarchyLines_verticalGaps.Add(depth - 1); + + if (depth < hierarchyLines_prevRowDepth) + hierarchyLines_verticalGaps.RemoveAll(r => r >= depth); + + } + + void drawVerticals() + { + for (int i = 0; i < depth; i++) + if (!hierarchyLines_verticalGaps.Contains(i)) + rowRect.SetX(53 + i * 14 - lineThickness / 2) + .SetWidth(lineThickness) + .SetHeight(isLastChild(go.transform) && i == depth - 1 ? 8 + lineThickness / 2 : 16) + .Draw(lineColor); + + } + void drawHorizontals() + { + if (depth == 0) return; + + rowRect.MoveX(-21) + .SetHeightFromMid(lineThickness) + .SetWidth(hasChilren(go.transform) ? 7 : 17) + .AddWidthFromRight(-lineThickness / 2f) + .Draw(lineColor); + + } + + + + calcVerticalGaps_beforeFirstRowDrawn(); + + drawVerticals(); + drawHorizontals(); + + updateVerticalGaps_beforeNextRowDrawn(); + + hierarchyLines_prevRowDepth = depth; + hierarchyLines_isFirstRowDrawn = true; + + } + void zebraStriping() + { + if (!VHierarchyMenu.zebraStripingEnabled) return; + if (isRowSelected) return; + if (goInfo.goData?.colorIndex == 1) return; + + + var contrast = isDarkTheme ? .033f : .05f; + + + var t = rowRect.y.PingPong(16f) / 16f; + + if (isRowHovered || isRowSelected) + t = 1; + + if (t.Approx(0)) return; + + + + fullRowRect.Draw(Greyscale(isDarkTheme ? 1 : 0, contrast * t)); + + + } + void highlight() + { + if (!controller.animatingHighlight) return; + if (go != controller.objectToHighlight) return; + + + var highlightBrightness = .16f; + + + var highlightAmount = controller.highlightAmount.Clamp01(); + + highlightAmount = highlightAmount * highlightAmount * (3 - 2 * highlightAmount); + + + fullRowRect.AddWidthFromRight(123).Draw(Greyscale(1, highlightBrightness * highlightAmount)); + + } + + + set_defaultBackground(); + hideDefaultIcon(); + hideName(); + + backgroundColor(); + hierarchyLines(); + + triangle(); + name(); + defaultIcon(); + customIcon(); + zebraStriping(); + highlight(); + + } + + void componentMinimap() + { + if (!VHierarchyMenu.componentMinimapEnabled) return; + + void componentButton(Rect buttonRect, Component component) + { + void componentIcon() + { + if (!curEvent.isRepaint) return; + + + var normalOpacity = isDarkTheme ? .47f : .7f; + var activeOpacity = 1; + var pressedOpacity = isDarkTheme ? .65f : .9f; + + var isActive = (buttonRect.IsHovered() && curEvent.holdingAlt) || VHierarchyComponentWindow.floatingInstance?.component == component; + var isPressed = buttonRect.IsHovered() && mousePressed; + + var icon = GetComponentIcon(component); + + + if (!icon) return; + + SetGUIColor(Greyscale(1, isActive ? (isPressed ? pressedOpacity : activeOpacity) : normalOpacity)); + + GUI.DrawTexture(buttonRect.SetSizeFromMid(12, 12), icon); + + ResetGUIColor(); + + } + + void mouseDown() + { + if (!curEvent.holdingAlt) return; + if (!curEvent.isMouseDown) return; + if (!buttonRect.IsHovered()) return; + + curEvent.Use(); + + mouseDownPos = curEvent.mousePosition; + + } + void mouseUp() + { + if (!curEvent.holdingAlt) return; + if (!curEvent.isMouseUp) return; + if (!buttonRect.IsHovered()) return; + + curEvent.Use(); + + if (VHierarchyComponentWindow.floatingInstance?.component == component) { VHierarchyComponentWindow.floatingInstance.Close(); return; } + + + var position = EditorGUIUtility.GUIToScreenPoint(new Vector2(rowRect.xMax + 25, rowRect.y)); + + if (!VHierarchyComponentWindow.floatingInstance) + VHierarchyComponentWindow.CreateFloatingInstance(position); + + VHierarchyComponentWindow.floatingInstance.Init(component); + VHierarchyComponentWindow.floatingInstance.Focus(); + + VHierarchyComponentWindow.floatingInstance.targetPosition = position; + + } + + + if (curEvent.holdingAlt) + buttonRect.MarkInteractive(); + + componentIcon(); + + mouseDown(); + mouseUp(); + + } + + void transformComponent() + { + if (!isRowHovered) return; + if (!curEvent.holdingAlt) return; + if (!go.GetComponent()) return; + + componentButton(fullRowRect.SetWidth(13).MoveX(1.5f), go.GetComponent()); + + } + void otherComponetns() + { + var buttonWidth = 13; + var minButtonX = rowRect.x + go.name.GetLabelWidth() + buttonWidth + 2; + var buttonRect = fullRowRect.SetWidthFromRight(buttonWidth).MoveX(-1.5f); + + if (PrefabUtility.IsAnyPrefabInstanceRoot(go) && !PrefabUtility.IsPartOfModelPrefab(go)) + buttonRect = buttonRect.MoveX(-13); + + foreach (var component in go.GetComponents()) + { + if (component is Transform) continue; + if (buttonRect.x < minButtonX) continue; + + componentButton(buttonRect, component); + + buttonRect = buttonRect.MoveX(-buttonWidth); + + } + + + } + + transformComponent(); + otherComponetns(); + + } + void activationToggle() + { + if (!VHierarchyMenu.activationToggleEnabled) return; + if (!isRowHovered) return; + + var toggleRect = fullRowRect.SetWidth(16).MoveX(1); + + + SetGUIColor(Greyscale(1, .9f)); + + var newActiveSelf = EditorGUI.Toggle(toggleRect, go.activeSelf); + + ResetGUIColor(); + + + if (newActiveSelf == go.activeSelf) return; + + var gos = Selection.gameObjects.Contains(go) ? Selection.gameObjects : new[] { go }; + var newActive = gos != null && !gos.Any(r => r && r.activeSelf); + + foreach (var r in gos) + r.RecordUndo(); + + foreach (var r in gos) + r.SetActive(newActiveSelf); + + GUI.FocusControl(null); + + } + void defaultParentIndicator() + { + if (!isDefaultParent) return; + + + + var drawCustomIcon = GetGameObjectInfo(go).hasIcon; + var drawDefaultIcon = !drawCustomIcon && (isRowBeingRenamed || (!VHierarchyMenu.minimalModeEnabled || (PrefabUtility.IsAddedGameObjectOverride(go) && PrefabUtility.IsPartOfPrefabInstance(go)))); + + var indicatorRect = rowRect.MoveX(go.name.GetLabelWidth(isBold: true) + 16.5f); + + if (!drawCustomIcon && !drawDefaultIcon) + indicatorRect = indicatorRect.MoveX(-16); + + + + SetGUIColor(Greyscale(1, .6f)); + SetLabelFontSize(10); + + GUI.Label(indicatorRect, "Default parent"); + + ResetLabelStyle(); + ResetGUIColor(); + + + + + + if (!fullRowRect.IsHovered()) return; + + + var buttonRect = indicatorRect.MoveX(68.5f).SetWidth(16).MoveY(.49f); + + var iconName = "CrossIcon"; + var iconSize = 10; + var colorNormal = Greyscale(isDarkTheme ? .75f : .2f, .55f); + var colorHovered = Greyscale(isDarkTheme ? 123f : .2f, 123f); + var colorPressed = Greyscale(isDarkTheme ? .75f : .5f); + var colorDisabled = Greyscale(isDarkTheme ? .53f : .55f); + + + if (!IconButton(buttonRect, iconName, iconSize, colorNormal, colorHovered, colorPressed)) return; + + EditorUtility.ClearDefaultParentObject(); + + + } + + void altDrag() + { + if (!curEvent.holdingAlt) return; + + void mouseDown() + { + if (!curEvent.isMouseDown) return; + if (!rowRect.IsHovered()) return; + + mouseDownPos = curEvent.mousePosition; + + } + void mouseDrag() + { + if (!curEvent.isMouseDrag) return; + if ((curEvent.mousePosition - mouseDownPos).magnitude < 5) return; + if (!rowRect.Contains(mouseDownPos)) return; + if (!rowRect.Contains(curEvent.mousePosition - curEvent.mouseDelta)) return; + if (DragAndDrop.objectReferences.Any()) return; + + DragAndDrop.PrepareStartDrag(); + DragAndDrop.objectReferences = new[] { go }; + DragAndDrop.StartDrag(go.name); + + } + + mouseDown(); + mouseDrag(); + + // altdrag has to be set up manually before altClick because altClick will use() mouseDown event to prevent selection change + } + void altClick() + { + if (!fullRowRect.IsHovered()) return; + if (!curEvent.holdingAlt) return; + if (Application.isPlaying) return; + + void mouseDown() + { + if (!curEvent.isMouseDown) return; + + curEvent.Use(); + + } + void mouseUp() + { + if (!curEvent.isMouseUp) return; + + var editMultiSelection = Selection.gameObjects.Length > 1 && Selection.gameObjects.Contains(go); + + var gosToEdit = (editMultiSelection ? Selection.gameObjects : new[] { go }).ToList(); + + + if (VHierarchyPaletteWindow.instance && VHierarchyPaletteWindow.instance.gameObjects.SequenceEqual(gosToEdit)) { VHierarchyPaletteWindow.instance.Close(); return; } + + var openNearRect = editMultiSelection ? lastVisibleSelectedRowRect : rowRect; + var position = EditorGUIUtility.GUIToScreenPoint(new Vector2(curEvent.mousePosition.x + 20, openNearRect.y - 13)); + // var position = EditorGUIUtility.GUIToScreenPoint(new Vector2(openNearRect.x - 14, openNearRect.y + 18)); + + if (!VHierarchyPaletteWindow.instance) + VHierarchyPaletteWindow.CreateInstance(position); + + VHierarchyPaletteWindow.instance.Init(gosToEdit); + VHierarchyPaletteWindow.instance.Focus(); + + VHierarchyPaletteWindow.instance.targetPosition = position; + + if (editMultiSelection) + Selection.objects = null; + + } + + mouseDown(); + mouseUp(); + + } + + + + setState(); + + drawing(); + + componentMinimap(); + activationToggle(); + defaultParentIndicator(); + + altDrag(); + altClick(); + + } + + List hierarchyLines_verticalGaps = new(); + bool hierarchyLines_isFirstRowDrawn; + int hierarchyLines_prevRowDepth; + + bool mousePressed; + Vector2 mouseDownPos; + + Rect lastVisibleSelectedRowRect; + + + + + public void RowGUI_Scene(Rect rowRect, Scene scene) + { + var fullRowRect = rowRect.SetX(32).SetXMax(rowRect.xMax + 16); + + var isRowHovered = fullRowRect.AddWidthFromRight(32).IsHovered(); + var isActiveScene = EditorSceneManager.GetActiveScene() == scene; + var isStickyHeader = rowRect.y != 0 && EditorGUIUtility.GUIToScreenPoint(rowRect.position).y - window.position.y == 45; + + + void set_hoveredScene() + { + if (curEvent.isLayout) + VHierarchy.hoveredScene = default; + + if (curEvent.isRepaint && isRowHovered) + VHierarchy.hoveredScene = scene; + + } + void sceneSelector() + { + if (!VHierarchyMenu.sceneSelectorEnabled) return; + if (!scene.isLoaded) return; + + + var nameWidth = (scene.name == "" ? "Untitled" : scene.name).GetLabelWidth(isBold: isActiveScene) + (scene.isDirty ? 5 : 0) + (isActiveScene ? -.5f : -1f); + var selectorRect = rowRect.MoveX(18).SetWidth(nameWidth + 16); + + var id = EditorGUIUtility.GUIToScreenRect(selectorRect).GetHashCode(); + var isPressed = id == pressedSceneSelectorId; + + var highlightName = selectorRect.IsHovered() || (VHierarchySceneSelectorWindow.instance && VHierarchySceneSelectorWindow.instance.sceneToReplace == scene); + + + + void dummyRow() + { + if (!highlightName) return; + if (!isDarkTheme) return; + + void background() + { + var backgroundColor = Application.unityVersion.Contains("2021") ? Greyscale(isDarkTheme ? .32f : .9f) + : Greyscale(isDarkTheme ? .16f : .9f); + + rowRect.AddWidthFromMid(123).AddHeight(-1).Draw(backgroundColor); + + } + void tripleDotButton() + { + GUI.DrawTexture(rowRect.SetWidthFromRight(16).MoveX(12), EditorIcons.GetIcon("More")); + } + void sceneIcon() + { + GUI.DrawTexture(rowRect.SetWidth(16), EditorIcons.GetIcon("SceneAsset Icon")); + } + void foldoutIcon() + { + if (scene.rootCount == 0) return; + + var isSceneExpanded = controller.expandedIds.Contains(scene.handle); + + GUI.DrawTexture(rowRect.SetWidth(16).MoveX(-15.5f).SetSizeFromMid(13), EditorIcons.GetIcon(isSceneExpanded ? "IN_foldout_on" : "IN_foldout")); + } + + background(); + tripleDotButton(); + sceneIcon(); + foldoutIcon(); + + } + + void dropdownIcon() + { + var iconRect = rowRect.MoveY(-.5f).MoveX(nameWidth + 14).SetWidth(16); + + + var iconBrightness = highlightName ? 1 : .78f; + + if (!isActiveScene) + iconBrightness *= .82f; + + if (isPressed) + iconBrightness *= .83f; + + if (!isDarkTheme) + iconBrightness = .35f; + + + + SetGUIColor(Greyscale(iconBrightness)); + + GUI.DrawTexture(iconRect, EditorIcons.GetIcon("Dropdown")); + + ResetGUIColor(); + + } + void highlightedName() + { + if (!curEvent.isRepaint) return; + if (!highlightName) return; + + var nameRect = rowRect.MoveX(18).SetWidth(nameWidth + 32); + + + + + var nameStyle = isActiveScene ? "TV LineBold" : "TV Line"; + + var nameText = scene.name == "" ? "Untitled" : scene.name; + + if (scene.isDirty) + nameText += "*"; + + + var nameBrightness = 1f; + + if (isPressed) + nameBrightness *= .83f; + + if (!isDarkTheme) + nameBrightness = .0f; + + + + SetGUIColor(Greyscale(nameBrightness)); + + GUI.skin.GetStyle(nameStyle).Draw(nameRect, nameText, false, false, true, true); + + ResetGUIColor(); + + } + void buttonLogic() + { + void mouseDown() + { + var couldBeMouseDown = isStickyHeader && isRowHovered && curEvent.isUsed && !isPressed; // gets used on sticky headers by default row gui + + if (!curEvent.isMouseDown && !couldBeMouseDown) return; + if (!selectorRect.IsHovered()) return; + + + pressedSceneSelectorId = id; + + mouseDownOnSelectorPos = curEvent.mousePosition; + + curEvent.Use(); + + } + void mouseUp() + { + var couldBeMouseUp = isStickyHeader && isRowHovered && curEvent.isUsed && isPressed; // gets used on sticky headers by default row gui + + if (!curEvent.isMouseUp && !couldBeMouseUp) return; + if (!isPressed) return; + + + pressedSceneSelectorId = 0; + + curEvent.Use(); + + + if (!selectorRect.IsHovered()) return; + + if (VHierarchySceneSelectorWindow.instance) + VHierarchySceneSelectorWindow.instance.Close(); + else + VHierarchySceneSelectorWindow.Open(EditorGUIUtility.GUIToScreenPoint(rowRect.position), scene); + + } + void mouseDrag() + { + if (!curEvent.isMouseDrag) return; + if (!isPressed) return; + + if (curEvent.mousePosition.DistanceTo(mouseDownOnSelectorPos) < 3) { curEvent.Use(); return; } + + pressedSceneSelectorId = 0; + + + var sceneHierarchy = window?.GetFieldValue("m_SceneHierarchy"); + + var treeViewController = sceneHierarchy.GetFieldValue("m_TreeView"); + var treeViewControllerData = treeViewController.GetMemberValue("data"); + + var item = treeViewControllerData.InvokeMethod("FindItem", scene.handle); + + treeViewController.GetMemberValue("dragging").InvokeMethod("StartDrag", item, new List()); + + } + + + selectorRect.MarkInteractive(); + + mouseDown(); + mouseUp(); + mouseDrag(); + + } + + + + dummyRow(); + + dropdownIcon(); + highlightedName(); + buttonLogic(); + + } + + set_hoveredScene(); + sceneSelector(); + + } + + int pressedSceneSelectorId; + + Vector2 mouseDownOnSelectorPos; + + + + + + + + + + public void UpdateState() + { + + var sceneHierarchy = window?.GetFieldValue("m_SceneHierarchy"); + + var treeViewController = sceneHierarchy.GetFieldValue("m_TreeView"); + var treeViewControllerData = treeViewController.GetMemberValue("data"); + + + isTreeFocused = EditorWindow.focusedWindow == window + && GUIUtility.keyboardControl == sceneHierarchy?.GetMemberValue("m_TreeViewKeyboardControlID"); + + renamingRow = EditorGUIUtility.editingTextField + && treeViewController?.GetMemberValue("state")?.GetMemberValue("renameOverlay")?.InvokeMethod("IsRenaming") == true; + + +#if UNITY_2021_1_OR_NEWER + dragSelectionList = treeViewController?.GetFieldValue("m_DragSelection")?.GetFieldValue>("m_List"); +#else + dragSelectionList = treeViewController?.GetFieldValue>("m_DragSelection"); +#endif + + + defaultParent = typeof(SceneView).InvokeMethod("GetDefaultParentObjectIfSet")?.gameObject; + + + } + + public bool isTreeFocused; + public bool renamingRow; + + public List dragSelectionList = new(); + + GameObject defaultParent; + + + + + + + + + + + + public VHierarchyGUI(EditorWindow window) => this.window = window; + + public EditorWindow window; + + public VHierarchyController controller => VHierarchy.controllers_byWindow[window]; + + } +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchyGUI.cs.meta b/vfolders2/vHierarchy/VHierarchyGUI.cs.meta new file mode 100644 index 0000000..ceb389a --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyGUI.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 879081410e5ee4ddcb0145a4a9937193 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyIconEditor.cs b/vfolders2/vHierarchy/VHierarchyIconEditor.cs new file mode 100644 index 0000000..8fb6b18 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyIconEditor.cs @@ -0,0 +1,6 @@ + + +// this file was present in a previus version and is supposed to be deleted now +// but asset store update delivery system doesn't allow deleting files +// so instead this file is now emptied +// feel free to delete it if you want diff --git a/vfolders2/vHierarchy/VHierarchyIconEditor.cs.meta b/vfolders2/vHierarchy/VHierarchyIconEditor.cs.meta new file mode 100644 index 0000000..90282a9 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyIconEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8710ada63f66c4909ab73d206f31e954 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyLibs.cs b/vfolders2/vHierarchy/VHierarchyLibs.cs new file mode 100644 index 0000000..a9484bd --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyLibs.cs @@ -0,0 +1,2563 @@ +#if UNITY_EDITOR +using Type = System.Type; +using System.Collections; +using System.Collections.Generic; +using System.IO; +using UnityEngine; +using UnityEditor; +using System.Linq; +using System.Text.RegularExpressions; +using System.Diagnostics; +using System.Reflection; +using UnityEngine.Experimental.Rendering; +using static VHierarchy.Libs.VUtils; +using static VHierarchy.Libs.VGUI; + + +namespace VHierarchy.Libs +{ + public static class VUtils + { + + #region Reflection + + + public static object GetFieldValue(this object o, string fieldName) + { + var type = o as Type ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetFieldInfo(fieldName) is FieldInfo fieldInfo) + return fieldInfo.GetValue(target); + + + throw new System.Exception($"Field '{fieldName}' not found in type '{type.Name}' and its parent types"); + + } + public static object GetPropertyValue(this object o, string propertyName) + { + var type = o as Type ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetPropertyInfo(propertyName) is PropertyInfo propertyInfo) + return propertyInfo.GetValue(target); + + + throw new System.Exception($"Property '{propertyName}' not found in type '{type.Name}' and its parent types"); + + } + public static object GetMemberValue(this object o, string memberName) + { + var type = o as Type ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetFieldInfo(memberName) is FieldInfo fieldInfo) + return fieldInfo.GetValue(target); + + if (type.GetPropertyInfo(memberName) is PropertyInfo propertyInfo) + return propertyInfo.GetValue(target); + + + throw new System.Exception($"Member '{memberName}' not found in type '{type.Name}' and its parent types"); + + } + + public static void SetFieldValue(this object o, string fieldName, object value) + { + var type = o as Type ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetFieldInfo(fieldName) is FieldInfo fieldInfo) + fieldInfo.SetValue(target, value); + + + else throw new System.Exception($"Field '{fieldName}' not found in type '{type.Name}' and its parent types"); + + } + public static void SetPropertyValue(this object o, string propertyName, object value) + { + var type = o as Type ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetPropertyInfo(propertyName) is PropertyInfo propertyInfo) + propertyInfo.SetValue(target, value); + + + else throw new System.Exception($"Property '{propertyName}' not found in type '{type.Name}' and its parent types"); + + } + public static void SetMemberValue(this object o, string memberName, object value) + { + var type = o as Type ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetFieldInfo(memberName) is FieldInfo fieldInfo) + fieldInfo.SetValue(target, value); + + else if (type.GetPropertyInfo(memberName) is PropertyInfo propertyInfo) + propertyInfo.SetValue(target, value); + + + else throw new System.Exception($"Member '{memberName}' not found in type '{type.Name}' and its parent types"); + + } + + public static object InvokeMethod(this object o, string methodName, params object[] parameters) // todo handle null params (can't get their type) + { + var type = o as Type ?? o.GetType(); + var target = o is Type ? null : o; + + + if (type.GetMethodInfo(methodName, parameters.Select(r => r.GetType()).ToArray()) is MethodInfo methodInfo) + return methodInfo.Invoke(target, parameters); + + + throw new System.Exception($"Method '{methodName}' not found in type '{type.Name}', its parent types and interfaces"); + + } + + + public static T GetFieldValue(this object o, string fieldName) => (T)o.GetFieldValue(fieldName); + public static T GetPropertyValue(this object o, string propertyName) => (T)o.GetPropertyValue(propertyName); + public static T GetMemberValue(this object o, string memberName) => (T)o.GetMemberValue(memberName); + public static T InvokeMethod(this object o, string methodName, params object[] parameters) => (T)o.InvokeMethod(methodName, parameters); + + + + + public static FieldInfo GetFieldInfo(this Type type, string fieldName) + { + if (fieldInfoCache.TryGetValue(type, out var fieldInfosByNames)) + if (fieldInfosByNames.TryGetValue(fieldName, out var fieldInfo)) + return fieldInfo; + + + if (!fieldInfoCache.ContainsKey(type)) + fieldInfoCache[type] = new Dictionary(); + + for (var curType = type; curType != null; curType = curType.BaseType) + if (curType.GetField(fieldName, maxBindingFlags) is FieldInfo fieldInfo) + return fieldInfoCache[type][fieldName] = fieldInfo; + + + return fieldInfoCache[type][fieldName] = null; + + } + public static PropertyInfo GetPropertyInfo(this Type type, string propertyName) + { + if (propertyInfoCache.TryGetValue(type, out var propertyInfosByNames)) + if (propertyInfosByNames.TryGetValue(propertyName, out var propertyInfo)) + return propertyInfo; + + + if (!propertyInfoCache.ContainsKey(type)) + propertyInfoCache[type] = new Dictionary(); + + for (var curType = type; curType != null; curType = curType.BaseType) + if (curType.GetProperty(propertyName, maxBindingFlags) is PropertyInfo propertyInfo) + return propertyInfoCache[type][propertyName] = propertyInfo; + + + return propertyInfoCache[type][propertyName] = null; + + } + public static MethodInfo GetMethodInfo(this Type type, string methodName, params Type[] argumentTypes) + { + var methodHash = methodName.GetHashCode() ^ argumentTypes.Aggregate(0, (hash, r) => hash ^= r.GetHashCode()); + + + if (methodInfoCache.TryGetValue(type, out var methodInfosByHashes)) + if (methodInfosByHashes.TryGetValue(methodHash, out var methodInfo)) + return methodInfo; + + + + if (!methodInfoCache.ContainsKey(type)) + methodInfoCache[type] = new Dictionary(); + + for (var curType = type; curType != null; curType = curType.BaseType) + if (curType.GetMethod(methodName, maxBindingFlags, null, argumentTypes, null) is MethodInfo methodInfo) + return methodInfoCache[type][methodHash] = methodInfo; + + foreach (var interfaceType in type.GetInterfaces()) + if (interfaceType.GetMethod(methodName, maxBindingFlags, null, argumentTypes, null) is MethodInfo methodInfo) + return methodInfoCache[type][methodHash] = methodInfo; + + + + return methodInfoCache[type][methodHash] = null; + + } + + static Dictionary> fieldInfoCache = new(); + static Dictionary> propertyInfoCache = new(); + static Dictionary> methodInfoCache = new(); + + + + + + + public static T GetCustomAttributeCached(this MemberInfo memberInfo) where T : System.Attribute + { + if (!attributesCache.TryGetValue(memberInfo, out var attributes_byType)) + attributes_byType = attributesCache[memberInfo] = new(); + + if (!attributes_byType.TryGetValue(typeof(T), out var attribute)) + attribute = attributes_byType[typeof(T)] = memberInfo.GetCustomAttribute(); + + return attribute as T; + + } + + static Dictionary> attributesCache = new(); + + + + + + + public static List GetSubclasses(this Type t) => t.Assembly.GetTypes().Where(type => type.IsSubclassOf(t)).ToList(); + + public static object GetDefaultValue(this FieldInfo f, params object[] constructorVars) => f.GetValue(System.Activator.CreateInstance(((MemberInfo)f).ReflectedType, constructorVars)); + public static object GetDefaultValue(this FieldInfo f) => f.GetValue(System.Activator.CreateInstance(((MemberInfo)f).ReflectedType)); + + public static IEnumerable GetFieldsWithoutBase(this Type t) => t.GetFields().Where(r => !t.BaseType.GetFields().Any(rr => rr.Name == r.Name)); + public static IEnumerable GetPropertiesWithoutBase(this Type t) => t.GetProperties().Where(r => !t.BaseType.GetProperties().Any(rr => rr.Name == r.Name)); + + + public const BindingFlags maxBindingFlags = (BindingFlags)62; + + + + + + + + + #endregion + + #region Collections + + + public static class CollectionUtils + { + public static Dictionary MergeDictionaries(IEnumerable> dicts) + { + if (dicts.Count() == 0) return null; + if (dicts.Count() == 1) return dicts.First(); + + var mergedDict = new Dictionary(dicts.First()); + + foreach (var dict in dicts.Skip(1)) + foreach (var r in dict) + if (!mergedDict.ContainsKey(r.Key)) + mergedDict.Add(r.Key, r.Value); + + return mergedDict; + } + + } + + + public static T NextTo(this IEnumerable e, T to) => e.SkipWhile(r => !r.Equals(to)).Skip(1).FirstOrDefault(); + public static T PreviousTo(this IEnumerable e, T to) => e.Reverse().SkipWhile(r => !r.Equals(to)).Skip(1).FirstOrDefault(); + public static T NextToOrFirst(this IEnumerable e, T to) => e.NextTo(to) ?? e.First(); + public static T PreviousToOrLast(this IEnumerable e, T to) => e.PreviousTo(to) ?? e.Last(); + + public static IEnumerable InsertFirst(this IEnumerable ie, T t) => new[] { t }.Concat(ie); + + public static int IndexOfFirst(this List list, System.Func f) => list.FirstOrDefault(f) is T t ? list.IndexOf(t) : -1; + public static int IndexOfLast(this List list, System.Func f) => list.LastOrDefault(f) is T t ? list.IndexOf(t) : -1; + + public static void SortBy(this List list, System.Func keySelector) where T2 : System.IComparable => list.Sort((q, w) => keySelector(q).CompareTo(keySelector(w))); + + public static void RemoveValue(this IDictionary dictionary, TValue value) + { + if (dictionary.FirstOrDefault(r => r.Value.Equals(value)) is var kvp) + dictionary.Remove(kvp); + } + + public static void ForEach(this IEnumerable sequence, System.Action action) { foreach (T item in sequence) action(item); } + + + + public static T AddAt(this List l, T r, int i) + { + if (i < 0) i = 0; + if (i >= l.Count) + l.Add(r); + else + l.Insert(i, r); + return r; + } + public static T RemoveLast(this List l) + { + if (!l.Any()) return default; + + var r = l.Last(); + + l.RemoveAt(l.Count - 1); + + return r; + } + + public static void Add(this List list, params T[] items) + { + foreach (var r in items) + list.Add(r); + } + + + + + + + #endregion + + #region Math + + + public static class MathUtil // MathUtils name is taken by UnityEditor.MathUtils + { + + public static float TriangleArea(Vector2 A, Vector2 B, Vector2 C) => Vector3.Cross(A - B, A - C).z.Abs() / 2; + + public static Vector2 LineIntersection(Vector2 A, Vector2 B, Vector2 C, Vector2 D) + { + var a1 = B.y - A.y; + var b1 = A.x - B.x; + var c1 = a1 * A.x + b1 * A.y; + + var a2 = D.y - C.y; + var b2 = C.x - D.x; + var c2 = a2 * C.x + b2 * C.y; + + var d = a1 * b2 - a2 * b1; + + var x = (b2 * c1 - b1 * c2) / d; + var y = (a1 * c2 - a2 * c1) / d; + + return new Vector2(x, y); + + } + + + + + public static float Lerp(float f1, float f2, float t) => Mathf.LerpUnclamped(f1, f2, t); + public static float Lerp(ref float f1, float f2, float t) + { + return f1 = Lerp(f1, f2, t); + } + + public static Vector2 Lerp(Vector2 f1, Vector2 f2, float t) => Vector2.LerpUnclamped(f1, f2, t); + public static Vector2 Lerp(ref Vector2 f1, Vector2 f2, float t) + { + return f1 = Lerp(f1, f2, t); + } + + public static Vector3 Lerp(Vector3 f1, Vector3 f2, float t) => Vector3.LerpUnclamped(f1, f2, t); + public static Vector3 Lerp(ref Vector3 f1, Vector3 f2, float t) + { + return f1 = Lerp(f1, f2, t); + } + + public static Color Lerp(Color f1, Color f2, float t) => Color.LerpUnclamped(f1, f2, t); + public static Color Lerp(ref Color f1, Color f2, float t) + { + return f1 = Lerp(f1, f2, t); + } + + + public static float Lerp(float current, float target, float speed, float deltaTime) => Mathf.Lerp(current, target, GetLerpT(speed, deltaTime)); + public static float Lerp(ref float current, float target, float speed, float deltaTime) + { + return current = Lerp(current, target, speed, deltaTime); + } + + public static Vector2 Lerp(Vector2 current, Vector2 target, float speed, float deltaTime) => Vector2.Lerp(current, target, GetLerpT(speed, deltaTime)); + public static Vector2 Lerp(ref Vector2 current, Vector2 target, float speed, float deltaTime) + { + return current = Lerp(current, target, speed, deltaTime); + } + + public static Vector3 Lerp(Vector3 current, Vector3 target, float speed, float deltaTime) => Vector3.Lerp(current, target, GetLerpT(speed, deltaTime)); + public static Vector3 Lerp(ref Vector3 current, Vector3 target, float speed, float deltaTime) + { + return current = Lerp(current, target, speed, deltaTime); + } + + public static float SmoothDamp(float current, float target, float speed, ref float derivative, float deltaTime, float maxSpeed) => Mathf.SmoothDamp(current, target, ref derivative, .5f / speed, maxSpeed, deltaTime); + public static float SmoothDamp(float current, float target, float speed, ref float derivative, float deltaTime) + { + return Mathf.SmoothDamp(current, target, ref derivative, .5f / speed, Mathf.Infinity, deltaTime); + } + public static float SmoothDamp(float current, float target, float speed, ref float derivative) + { + return SmoothDamp(current, target, speed, ref derivative, Time.deltaTime); + } + public static float SmoothDamp(ref float current, float target, float speed, ref float derivative, float deltaTime, float maxSpeed) + { + return current = SmoothDamp(current, target, speed, ref derivative, deltaTime, maxSpeed); + } + public static float SmoothDamp(ref float current, float target, float speed, ref float derivative, float deltaTime) + { + return current = SmoothDamp(current, target, speed, ref derivative, deltaTime); + } + public static float SmoothDamp(ref float current, float target, float speed, ref float derivative) + { + return current = SmoothDamp(current, target, speed, ref derivative, Time.deltaTime); + } + + public static Vector2 SmoothDamp(Vector2 current, Vector2 target, float speed, ref Vector2 derivative, float deltaTime) => Vector2.SmoothDamp(current, target, ref derivative, .5f / speed, Mathf.Infinity, deltaTime); + public static Vector2 SmoothDamp(Vector2 current, Vector2 target, float speed, ref Vector2 derivative) + { + return SmoothDamp(current, target, speed, ref derivative, Time.deltaTime); + } + public static Vector2 SmoothDamp(ref Vector2 current, Vector2 target, float speed, ref Vector2 derivative, float deltaTime) + { + return current = SmoothDamp(current, target, speed, ref derivative, deltaTime); + } + public static Vector2 SmoothDamp(ref Vector2 current, Vector2 target, float speed, ref Vector2 derivative) + { + return current = SmoothDamp(current, target, speed, ref derivative, Time.deltaTime); + } + + public static Vector3 SmoothDamp(Vector3 current, Vector3 target, float speed, ref Vector3 derivative, float deltaTime) => Vector3.SmoothDamp(current, target, ref derivative, .5f / speed, Mathf.Infinity, deltaTime); + public static Vector3 SmoothDamp(Vector3 current, Vector3 target, float speed, ref Vector3 derivative) + { + return SmoothDamp(current, target, speed, ref derivative, Time.deltaTime); + } + public static Vector3 SmoothDamp(ref Vector3 current, Vector3 target, float speed, ref Vector3 derivative, float deltaTime) + { + return current = SmoothDamp(current, target, speed, ref derivative, deltaTime); + } + public static Vector3 SmoothDamp(ref Vector3 current, Vector3 target, float speed, ref Vector3 derivative) + { + return current = SmoothDamp(current, target, speed, ref derivative, Time.deltaTime); + } + + + public static float GetLerpT(float lerpSpeed, float deltaTime) => 1 - Mathf.Exp(-lerpSpeed * 2f * deltaTime); + public static float GetLerpT(float lerpSpeed) + { + return GetLerpT(lerpSpeed, Time.deltaTime); + } + + + + } + + + public static float DistanceTo(this float f1, float f2) => Mathf.Abs(f1 - f2); + public static float DistanceTo(this Vector2 f1, Vector2 f2) => (f1 - f2).magnitude; + public static float DistanceTo(this Vector3 f1, Vector3 f2) => (f1 - f2).magnitude; + + public static float Sign(this float f) => f == 0 ? 0 : Mathf.Sign(f); + + public static int Abs(this int f) => Mathf.Abs(f); + public static float Abs(this float f) => Mathf.Abs(f); + + public static int Clamp(this int f, int f0, int f1) => Mathf.Clamp(f, f0, f1); + public static float Clamp(this float f, float f0, float f1) => Mathf.Clamp(f, f0, f1); + + + public static float Clamp01(this float f) => Mathf.Clamp(f, 0, 1); + public static Vector2 Clamp01(this Vector2 f) => new(f.x.Clamp01(), f.y.Clamp01()); + public static Vector3 Clamp01(this Vector3 f) => new(f.x.Clamp01(), f.y.Clamp01(), f.z.Clamp01()); + + + public static int Pow(this int f, int pow) => (int)Mathf.Pow(f, pow); + public static float Pow(this float f, float pow) => Mathf.Pow(f, pow); + + public static float Round(this float f) => Mathf.Round(f); + public static float Ceil(this float f) => Mathf.Ceil(f); + public static float Floor(this float f) => Mathf.Floor(f); + + public static int RoundToInt(this float f) => Mathf.RoundToInt(f); + public static int CeilToInt(this float f) => Mathf.CeilToInt(f); + public static int FloorToInt(this float f) => Mathf.FloorToInt(f); + + public static int ToInt(this float f) => (int)f; + public static float ToFloat(this int f) => (float)f; + public static float ToFloat(this double f) => (float)f; + + + + public static float Sqrt(this float f) => Mathf.Sqrt(f); + + public static int Max(this int f, int ff) => Mathf.Max(f, ff); + public static int Min(this int f, int ff) => Mathf.Min(f, ff); + public static float Max(this float f, float ff) => Mathf.Max(f, ff); + public static float Min(this float f, float ff) => Mathf.Min(f, ff); + + public static float ClampMin(this float f, float limitMin) => Mathf.Max(f, limitMin); + public static float ClampMax(this float f, float limitMax) => Mathf.Min(f, limitMax); + + public static Vector3 ClampMaxMagnitude(this Vector3 v, float maxMagnitude) + { + if (v.sqrMagnitude <= maxMagnitude * maxMagnitude) + return v; + else + return v.normalized * maxMagnitude; + } + + + public static float Loop(this float f, float boundMin, float boundMax) + { + while (f < boundMin) f += boundMax - boundMin; + while (f > boundMax) f -= boundMax - boundMin; + return f; + } + public static float Loop(this float f, float boundMax) => f.Loop(0, boundMax); + + public static float PingPong(this float f, float boundMin, float boundMax) => boundMin + Mathf.PingPong(f - boundMin, boundMax - boundMin); + public static float PingPong(this float f, float boundMax) => f.PingPong(0, boundMax); + + + public static float Dot(this Vector3 v1, Vector3 v2) => Vector3.Dot(v1, v2); + + public static Vector3 Cross(this Vector3 v1, Vector3 v2) => Vector3.Cross(v1, v2); + + + public static Vector2 ProjectOn(this Vector2 v, Vector2 on) => Vector3.Project(v, on); + public static Vector3 ProjectOn(this Vector3 v, Vector3 on) => Vector3.Project(v, on); + + public static Vector3 ProjectOnPlane(this Vector3 v, Vector3 normal) => Vector3.ProjectOnPlane(v, normal); + + + public static float AngleTo(this Vector2 v, Vector2 to) => Vector2.Angle(v, to); + + public static Vector2 Rotate(this Vector2 v, float deg) => Quaternion.AngleAxis(deg, Vector3.forward) * v; + + public static float Smoothstep(this float f) { f = f.Clamp01(); return f * f * (3 - 2 * f); } + + public static float InverseLerp(this Vector2 v, Vector2 a, Vector2 b) + { + var ab = b - a; + var av = v - a; + return Vector2.Dot(av, ab) / Vector2.Dot(ab, ab); + } + + + public static bool IsOdd(this int i) => i % 2 == 1; + public static bool IsEven(this int i) => i % 2 == 0; + + public static bool IsInRange(this int i, int a, int b) => i >= a && i <= b; + public static bool IsInRange(this float i, float a, float b) => i >= a && i <= b; + + public static bool IsInRangeOf(this int i, IList list) => i.IsInRange(0, list.Count - 1); + public static bool IsInRangeOf(this int i, T[] array) => i.IsInRange(0, array.Length - 1); + + public static bool Approx(this float f1, float f2) => Mathf.Approximately(f1, f2); + + + + [System.Serializable] + public class GaussianKernel + { + public static float[,] GenerateArray(int size, float sharpness = .5f) + { + float[,] kr = new float[size, size]; + + if (size == 1) { kr[0, 0] = 1; return kr; } + + + var sigma = 1f - Mathf.Pow(sharpness, .1f) * .99999f; + var radius = (size / 2f).FloorToInt(); + + + var a = -2f * radius * radius / Mathf.Log(sigma); + var sum = 0f; + + for (int y = 0; y < size; y++) + for (int x = 0; x < size; x++) + { + var rX = size % 2 == 1 ? (x - radius) : (x - radius) + .5f; + var rY = size % 2 == 1 ? (y - radius) : (y - radius) + .5f; + var dist = Mathf.Sqrt(rX * rX + rY * rY); + kr[x, y] = Mathf.Exp(-dist * dist / a); + sum += kr[x, y]; + } + + for (int y = 0; y < size; y++) + for (int x = 0; x < size; x++) + kr[x, y] /= sum; + + return kr; + } + + + + public GaussianKernel(bool isEvenSize = false, int radius = 7, float sharpness = .5f) + { + this.isEvenSize = isEvenSize; + this.radius = radius; + this.sharpness = sharpness; + } + + public bool isEvenSize = false; + public int radius = 7; + public float sharpness = .5f; + + public int size => radius * 2 + (isEvenSize ? 0 : 1); + public float sigma => 1 - Mathf.Pow(sharpness, .1f) * .99999f; + + public float[,] Array2d() // todo test and use GenerateArray + { + float[,] kr = new float[size, size]; + + if (size == 1) { kr[0, 0] = 1; return kr; } + + var a = -2f * radius * radius / Mathf.Log(sigma); + var sum = 0f; + + for (int y = 0; y < size; y++) + for (int x = 0; x < size; x++) + { + var rX = size % 2 == 1 ? (x - radius) : (x - radius) + .5f; + var rY = size % 2 == 1 ? (y - radius) : (y - radius) + .5f; + var dist = Mathf.Sqrt(rX * rX + rY * rY); + kr[x, y] = Mathf.Exp(-dist * dist / a); + sum += kr[x, y]; + } + + for (int y = 0; y < size; y++) + for (int x = 0; x < size; x++) + kr[x, y] /= sum; + + return kr; + } + public float[] ArrayFlat() + { + var gk = Array2d(); + float[] flat = new float[size * size]; + + for (int i = 0; i < size; i++) + for (int j = 0; j < size; j++) + flat[(i * size + j)] = gk[i, j]; + + return flat; + } + + } + + + + + + + + #endregion + + #region Rects + + + public static Rect Resize(this Rect rect, float px) { rect.x += px; rect.y += px; rect.width -= px * 2; rect.height -= px * 2; return rect; } + + public static Rect SetPos(this Rect rect, Vector2 v) => rect.SetPos(v.x, v.y); + public static Rect SetPos(this Rect rect, float x, float y) { rect.x = x; rect.y = y; return rect; } + + public static Rect SetX(this Rect rect, float x) => rect.SetPos(x, rect.y); + public static Rect SetY(this Rect rect, float y) => rect.SetPos(rect.x, y); + public static Rect SetXMax(this Rect rect, float xMax) { rect.xMax = xMax; return rect; } + public static Rect SetYMax(this Rect rect, float yMax) { rect.yMax = yMax; return rect; } + + public static Rect SetMidPos(this Rect r, Vector2 v) => r.SetPos(v).MoveX(-r.width / 2).MoveY(-r.height / 2); + public static Rect SetMidPos(this Rect r, float x, float y) => r.SetMidPos(new Vector2(x, y)); + + public static Rect Move(this Rect rect, Vector2 v) { rect.position += v; return rect; } + public static Rect Move(this Rect rect, float x, float y) { rect.x += x; rect.y += y; return rect; } + public static Rect MoveX(this Rect rect, float px) { rect.x += px; return rect; } + public static Rect MoveY(this Rect rect, float px) { rect.y += px; return rect; } + + public static Rect SetWidth(this Rect rect, float f) { rect.width = f; return rect; } + public static Rect SetWidthFromMid(this Rect rect, float px) { rect.x += rect.width / 2; rect.width = px; rect.x -= rect.width / 2; return rect; } + public static Rect SetWidthFromRight(this Rect rect, float px) { rect.x += rect.width; rect.width = px; rect.x -= rect.width; return rect; } + + public static Rect SetHeight(this Rect rect, float f) { rect.height = f; return rect; } + public static Rect SetHeightFromMid(this Rect rect, float px) { rect.y += rect.height / 2; rect.height = px; rect.y -= rect.height / 2; return rect; } + public static Rect SetHeightFromBottom(this Rect rect, float px) { rect.y += rect.height; rect.height = px; rect.y -= rect.height; return rect; } + + public static Rect AddWidth(this Rect rect, float f) => rect.SetWidth(rect.width + f); + public static Rect AddWidthFromMid(this Rect rect, float f) => rect.SetWidthFromMid(rect.width + f); + public static Rect AddWidthFromRight(this Rect rect, float f) => rect.SetWidthFromRight(rect.width + f); + + public static Rect AddHeight(this Rect rect, float f) => rect.SetHeight(rect.height + f); + public static Rect AddHeightFromMid(this Rect rect, float f) => rect.SetHeightFromMid(rect.height + f); + public static Rect AddHeightFromBottom(this Rect rect, float f) => rect.SetHeightFromBottom(rect.height + f); + + public static Rect SetSize(this Rect rect, Vector2 v) => rect.SetWidth(v.x).SetHeight(v.y); + public static Rect SetSize(this Rect rect, float w, float h) => rect.SetWidth(w).SetHeight(h); + public static Rect SetSize(this Rect rect, float f) { rect.height = rect.width = f; return rect; } + + public static Rect SetSizeFromMid(this Rect r, Vector2 v) => r.Move(r.size / 2).SetSize(v).Move(-v / 2); + public static Rect SetSizeFromMid(this Rect r, float x, float y) => r.SetSizeFromMid(new Vector2(x, y)); + public static Rect SetSizeFromMid(this Rect r, float f) => r.SetSizeFromMid(new Vector2(f, f)); + + public static Rect AlignToPixelGrid(this Rect r) => GUIUtility.AlignRectToDevice(r); + + + + + + #endregion + + #region Vectors + + + public static Vector2 AddX(this Vector2 v, float f) => new(v.x + f, v.y + 0); + public static Vector2 AddY(this Vector2 v, float f) => new(v.x + 0, v.y + f); + + public static Vector3 AddX(this Vector3 v, float f) => new(v.x + f, v.y + 0, v.z + 0); + public static Vector3 AddY(this Vector3 v, float f) => new(v.x + 0, v.y + f, v.z + 0); + public static Vector3 AddZ(this Vector3 v, float f) => new(v.x + 0, v.y + 0, v.z + f); + + public static Vector3 SetX(this Vector3 v, float f) => new(f, v.y, v.z); + public static Vector3 SetY(this Vector3 v, float f) => new(v.x, f, v.z); + public static Vector3 SetZ(this Vector3 v, float f) => new(v.x, v.y, f); + + public static Vector2 xx(this Vector3 v) { return new Vector2(v.x, v.x); } + public static Vector2 xy(this Vector3 v) { return new Vector2(v.x, v.y); } + public static Vector2 xz(this Vector3 v) { return new Vector2(v.x, v.z); } + public static Vector2 yx(this Vector3 v) { return new Vector2(v.y, v.x); } + public static Vector2 yy(this Vector3 v) { return new Vector2(v.y, v.y); } + public static Vector2 yz(this Vector3 v) { return new Vector2(v.y, v.z); } + public static Vector2 zx(this Vector3 v) { return new Vector2(v.z, v.x); } + public static Vector2 zy(this Vector3 v) { return new Vector2(v.z, v.y); } + public static Vector2 zz(this Vector3 v) { return new Vector2(v.z, v.z); } + + + + + + #endregion + + #region Colors + + + public class ColorUtils + { + public static Color HSLToRGB(float h, float s, float l) + { + float hue2Rgb(float v1, float v2, float vH) + { + if (vH < 0f) + vH += 1f; + + if (vH > 1f) + vH -= 1f; + + if (6f * vH < 1f) + return v1 + (v2 - v1) * 6f * vH; + + if (2f * vH < 1f) + return v2; + + if (3f * vH < 2f) + return v1 + (v2 - v1) * (2f / 3f - vH) * 6f; + + return v1; + } + + if (s.Approx(0)) return new Color(l, l, l); + + float k1; + + if (l < .5f) + k1 = l * (1f + s); + else + k1 = l + s - s * l; + + + var k2 = 2f * l - k1; + + float r, g, b; + r = hue2Rgb(k2, k1, h + 1f / 3); + g = hue2Rgb(k2, k1, h); + b = hue2Rgb(k2, k1, h - 1f / 3); + + return new Color(r, g, b); + } + public static Color LCHtoRGB(float l, float c, float h) + { + l *= 100; + c *= 100; + h *= 360; + + double xw = 0.948110; + double yw = 1.00000; + double zw = 1.07304; + + float a = c * Mathf.Cos(Mathf.Deg2Rad * h); + float b = c * Mathf.Sin(Mathf.Deg2Rad * h); + + float fy = (l + 16) / 116; + float fx = fy + (a / 500); + float fz = fy - (b / 200); + + float x = (float)System.Math.Round(xw * ((System.Math.Pow(fx, 3) > 0.008856) ? System.Math.Pow(fx, 3) : ((fx - 16 / 116) / 7.787)), 5); + float y = (float)System.Math.Round(yw * ((System.Math.Pow(fy, 3) > 0.008856) ? System.Math.Pow(fy, 3) : ((fy - 16 / 116) / 7.787)), 5); + float z = (float)System.Math.Round(zw * ((System.Math.Pow(fz, 3) > 0.008856) ? System.Math.Pow(fz, 3) : ((fz - 16 / 116) / 7.787)), 5); + + float r = x * 3.2406f - y * 1.5372f - z * 0.4986f; + float g = -x * 0.9689f + y * 1.8758f + z * 0.0415f; + float bValue = x * 0.0557f - y * 0.2040f + z * 1.0570f; + + r = r > 0.0031308f ? 1.055f * (float)System.Math.Pow(r, 1 / 2.4) - 0.055f : r * 12.92f; + g = g > 0.0031308f ? 1.055f * (float)System.Math.Pow(g, 1 / 2.4) - 0.055f : g * 12.92f; + bValue = bValue > 0.0031308f ? 1.055f * (float)System.Math.Pow(bValue, 1 / 2.4) - 0.055f : bValue * 12.92f; + + // r = (float)System.Math.Round(System.Math.Max(0, System.Math.Min(1, r))); + // g = (float)System.Math.Round(System.Math.Max(0, System.Math.Min(1, g))); + // bValue = (float)System.Math.Round(System.Math.Max(0, System.Math.Min(1, bValue))); + + return new Color(r, g, bValue); + + } + + } + + + public static Color Greyscale(float brightness, float alpha = 1) => new(brightness, brightness, brightness, alpha); + + public static Color SetAlpha(this Color color, float alpha) { color.a = alpha; return color; } + public static Color MultiplyAlpha(this Color color, float k) { color.a *= k; return color; } + + + + + + #endregion + + #region Objects + + + public static Object[] FindObjects(Type type) + { +#if UNITY_2023_1_OR_NEWER + return Object.FindObjectsByType(type, FindObjectsSortMode.None); +#else + return Object.FindObjectsOfType(type); +#endif + } + public static T[] FindObjects() where T : Object + { +#if UNITY_2023_1_OR_NEWER + return Object.FindObjectsByType(FindObjectsSortMode.None); +#else + return Object.FindObjectsOfType(); +#endif + } + + public static void Destroy(this Object r) + { + if (Application.isPlaying) + Object.Destroy(r); + else + Object.DestroyImmediate(r); + + } + + public static void DestroyImmediate(this Object o) => Object.DestroyImmediate(o); + + + + + + #endregion + + #region GameObjects + + + public static bool IsPrefab(this GameObject go) => go.scene.name == null || go.scene.name == go.name; + + public static Bounds GetBounds(this GameObject go, bool local = false) + { + Bounds bounds = default; + + foreach (var r in go.GetComponentsInChildren()) + { + var b = local ? r.gameObject.GetComponent().sharedMesh.bounds : r.bounds; + + if (bounds == default) + bounds = b; + else + bounds.Encapsulate(b); + } + + foreach (var r in go.GetComponentsInChildren()) + { + var b = local ? new Bounds(r.terrainData.size / 2, r.terrainData.size) : new Bounds(r.transform.position + r.terrainData.size / 2, r.terrainData.size); + + if (bounds == default) + bounds = b; + else + bounds.Encapsulate(new Bounds(r.transform.position + r.terrainData.size / 2, r.terrainData.size)); + + } + + foreach (var r in go.GetComponentsInChildren()) + { + var localBounds = RectTransformUtility.CalculateRelativeRectTransformBounds(r); + var worldBounds = new Bounds(r.TransformPoint(localBounds.center), r.TransformVector(localBounds.size)); + + if (bounds == default) + bounds = worldBounds; + else + bounds.Encapsulate(worldBounds); + + } + + + if (bounds == default) + bounds.center = go.transform.position; + + return bounds; + } + + + + + + #endregion + + #region Text + + + public static bool IsEmpty(this string s) => s == ""; + public static bool IsNullOrEmpty(this string s) => string.IsNullOrEmpty(s); + + public static bool IsLower(this char c) => System.Char.IsLower(c); + public static bool IsUpper(this char c) => System.Char.IsUpper(c); + public static bool IsDigit(this char c) => System.Char.IsDigit(c); + public static bool IsLetter(this char c) => System.Char.IsLetter(c); + public static bool IsWhitespace(this char c) => System.Char.IsWhiteSpace(c); + + public static char ToLower(this char c) => System.Char.ToLower(c); + public static char ToUpper(this char c) => System.Char.ToUpper(c); + + + + public static string Decamelcase(this string s) + { + return Regex.Replace(Regex.Replace(s, @"(\P{Ll})(\P{Ll}\p{Ll})", "$1 $2"), @"(\p{Ll})(\P{Ll})", "$1 $2"); + } + public static string FormatVariableName(this string s, bool lowercaseFollowingWords = true) + { + return string.Join(" ", s.Decamelcase() + .Split(' ') + .Select(r => new[] { "", "and", "or", "with", "without", "by", "from" }.Contains(r.ToLower()) || (lowercaseFollowingWords && !s.Trim().StartsWith(r)) ? r.ToLower() + : r.Substring(0, 1).ToUpper() + r.Substring(1))).Trim(' '); + } + + public static string Remove(this string s, string toRemove) + { + if (toRemove == "") return s; + return s.Replace(toRemove, ""); + } + + + + + + + #endregion + + #region Paths + + + public static bool HasParentPath(this string path) => path.LastIndexOf('/') > 0; + public static string GetParentPath(this string path) => path.HasParentPath() ? path.Substring(0, path.LastIndexOf('/')) : ""; + + public static string GetFilename(this string path, bool withExtension = false) => withExtension ? Path.GetFileName(path) : Path.GetFileNameWithoutExtension(path); + public static string GetExtension(this string path) => Path.GetExtension(path); + + + public static string ToGlobalPath(this string localPath) => Application.dataPath + "/" + localPath.Substring(0, localPath.Length - 1); + public static string ToLocalPath(this string globalPath) => "Assets" + globalPath.Replace(Application.dataPath, ""); + + + + public static string CombinePath(this string p1, string p2, bool useBackslashOnWindows = false) + { + if (useBackslashOnWindows) // false by default because all paths in unity use forward slashes, even on Windows + return Path.Combine(p1, p2); + else + return Path.Combine(p1, p2).Replace('\\', '/'); + + } + + public static bool IsSubpathOf(this string path, string of) => path.StartsWith(of + "/") || of == ""; + + public static string GetDirectory(this string pathOrDirectory) + { + var directory = pathOrDirectory.Contains('.') ? pathOrDirectory.Substring(0, pathOrDirectory.LastIndexOf('/')) : pathOrDirectory; + + if (directory.Contains('.')) + directory = directory.Substring(0, directory.LastIndexOf('/')); + + return directory; + + } + + public static bool DirectoryExists(this string pathOrDirectory) => Directory.Exists(pathOrDirectory.GetDirectory()); + + public static string EnsureDirExists(this string pathOrDirectory) // todo to EnsureDirectoryExists + { + var directory = pathOrDirectory.GetDirectory(); + + if (directory.HasParentPath() && !Directory.Exists(directory.GetParentPath())) + EnsureDirExists(directory.GetParentPath()); + + if (!Directory.Exists(directory)) + Directory.CreateDirectory(directory); + + return pathOrDirectory; + + } + + + + public static string ClearDir(this string dir) + { + if (!Directory.Exists(dir)) return dir; + + var diri = new DirectoryInfo(dir); + foreach (var r in diri.EnumerateFiles()) r.Delete(); + foreach (var r in diri.EnumerateDirectories()) r.Delete(true); + + return dir; + } + + + + + + +#if UNITY_EDITOR + + public static string EnsurePathIsUnique(this string path) + { + if (!path.DirectoryExists()) return path; + + var s = AssetDatabase.GenerateUniqueAssetPath(path); // returns empty if parent dir doesnt exist + + return s == "" ? path : s; + + } + + public static void EnsureDirExistsAndRevealInFinder(string dir) + { + EnsureDirExists(dir); + UnityEditor.EditorUtility.OpenWithDefaultApp(dir); + } + +#endif + + + + #endregion + + #region AssetDatabase + +#if UNITY_EDITOR + + public static AssetImporter GetImporter(this Object t) => AssetImporter.GetAtPath(AssetDatabase.GetAssetPath(t)); + + public static string ToPath(this string guid) => AssetDatabase.GUIDToAssetPath(guid); // returns empty string if not found + public static List ToPaths(this IEnumerable guids) => guids.Select(r => r.ToPath()).ToList(); + + + public static string ToGuid(this string pathInProject) => AssetDatabase.AssetPathToGUID(pathInProject); + public static List ToGuids(this IEnumerable pathsInProject) => pathsInProject.Select(r => r.ToGuid()).ToList(); + + public static string GetPath(this Object o) => AssetDatabase.GetAssetPath(o); + public static string GetGuid(this Object o) => AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(o)); + + public static string GetScriptPath(string scriptName) => AssetDatabase.FindAssets("t: script " + scriptName, null).FirstOrDefault()?.ToPath() ?? "scirpt not found"; // todonow to editorutils + + + +#endif + + + + + + #endregion + + #region Serialization + + + [System.Serializable] + public class SerializableDictionary : Dictionary, ISerializationCallbackReceiver + { + [SerializeField] List keys = new(); + [SerializeField] List values = new(); + + public void OnBeforeSerialize() + { + keys.Clear(); + values.Clear(); + + foreach (KeyValuePair kvp in this) + { + keys.Add(kvp.Key); + values.Add(kvp.Value); + } + + } + public void OnAfterDeserialize() + { + this.Clear(); + + for (int i = 0; i < keys.Count; i++) + this[keys[i]] = values[i]; + + } + + } + + +#if UNITY_EDITOR + + public static object GetBoxedValue(this SerializedProperty p) + { + +#if UNITY_2022_1_OR_NEWER + switch (p.propertyType) + { + case SerializedPropertyType.Integer: + switch (p.numericType) + { + case SerializedPropertyNumericType.Int8: return (sbyte)p.intValue; + case SerializedPropertyNumericType.UInt8: return (byte)p.uintValue; + case SerializedPropertyNumericType.Int16: return (short)p.intValue; + case SerializedPropertyNumericType.UInt16: return (ushort)p.uintValue; + case SerializedPropertyNumericType.Int32: return p.intValue; + case SerializedPropertyNumericType.UInt32: return p.uintValue; + case SerializedPropertyNumericType.Int64: return p.longValue; + case SerializedPropertyNumericType.UInt64: return p.ulongValue; + default: return p.intValue; + + } + + case SerializedPropertyType.Float: + if (p.numericType == SerializedPropertyNumericType.Double) + return p.doubleValue; + else + return p.floatValue; + + case SerializedPropertyType.Hash128: return p.hash128Value; + case SerializedPropertyType.Character: return (ushort)p.uintValue; + case SerializedPropertyType.Gradient: return p.gradientValue; + case SerializedPropertyType.ManagedReference: return p.managedReferenceValue; + + + } +#endif + + switch (p.propertyType) + { + case SerializedPropertyType.Integer: return p.intValue; + case SerializedPropertyType.Float: return p.floatValue; + case SerializedPropertyType.Vector2: return p.vector2Value; + case SerializedPropertyType.Vector3: return p.vector3Value; + case SerializedPropertyType.Vector4: return p.vector4Value; + case SerializedPropertyType.Vector2Int: return p.vector2IntValue; + case SerializedPropertyType.Vector3Int: return p.vector3IntValue; + case SerializedPropertyType.Quaternion: return p.quaternionValue; + case SerializedPropertyType.Rect: return p.rectValue; + case SerializedPropertyType.RectInt: return p.rectIntValue; + case SerializedPropertyType.Bounds: return p.boundsValue; + case SerializedPropertyType.BoundsInt: return p.boundsIntValue; + case SerializedPropertyType.Enum: return p.enumValueIndex; + case SerializedPropertyType.Boolean: return p.boolValue; + case SerializedPropertyType.String: return p.stringValue; + case SerializedPropertyType.Color: return p.colorValue; + case SerializedPropertyType.ArraySize: return p.intValue; + case SerializedPropertyType.Character: return (ushort)p.intValue; + case SerializedPropertyType.AnimationCurve: return p.animationCurveValue; + case SerializedPropertyType.ObjectReference: return p.objectReferenceValue; + case SerializedPropertyType.ExposedReference: return p.exposedReferenceValue; + case SerializedPropertyType.FixedBufferSize: return p.intValue; + case SerializedPropertyType.LayerMask: return (LayerMask)p.intValue; + + } + + + return _noValue; + + } + public static void SetBoxedValue(this SerializedProperty p, object value) + { + if (value == _noValue) return; + + try + { + +#if UNITY_2022_1_OR_NEWER + switch (p.propertyType) + { + case SerializedPropertyType.ArraySize: + case SerializedPropertyType.Integer: + if (p.numericType == SerializedPropertyNumericType.UInt64) + p.ulongValue = System.Convert.ToUInt64(value); + else + p.longValue = System.Convert.ToInt64(value); + return; + + case SerializedPropertyType.Float: + if (p.numericType == SerializedPropertyNumericType.Double) + p.doubleValue = System.Convert.ToDouble(value); + else + p.floatValue = System.Convert.ToSingle(value); + return; + + case SerializedPropertyType.Character: p.uintValue = System.Convert.ToUInt16(value); return; + case SerializedPropertyType.Gradient: p.gradientValue = (Gradient)value; return; + case SerializedPropertyType.Hash128: p.hash128Value = (Hash128)value; return; + + } +#endif + + switch (p.propertyType) + { + case SerializedPropertyType.ArraySize: + case SerializedPropertyType.Integer: p.intValue = System.Convert.ToInt32(value); return; + case SerializedPropertyType.Float: p.floatValue = System.Convert.ToSingle(value); return; + case SerializedPropertyType.Vector2: p.vector2Value = (Vector2)value; return; + case SerializedPropertyType.Vector3: p.vector3Value = (Vector3)value; return; + case SerializedPropertyType.Vector4: p.vector4Value = (Vector4)value; return; + case SerializedPropertyType.Vector2Int: p.vector2IntValue = (Vector2Int)value; return; + case SerializedPropertyType.Vector3Int: p.vector3IntValue = (Vector3Int)value; return; + case SerializedPropertyType.Quaternion: p.quaternionValue = (Quaternion)value; return; + case SerializedPropertyType.Rect: p.rectValue = (Rect)value; return; + case SerializedPropertyType.RectInt: p.rectIntValue = (RectInt)value; return; + case SerializedPropertyType.Bounds: p.boundsValue = (Bounds)value; return; + case SerializedPropertyType.BoundsInt: p.boundsIntValue = (BoundsInt)value; return; + case SerializedPropertyType.String: p.stringValue = (string)value; return; + case SerializedPropertyType.Boolean: p.boolValue = (bool)value; return; + case SerializedPropertyType.Enum: p.enumValueIndex = (int)value; return; + case SerializedPropertyType.Color: p.colorValue = (Color)value; return; + case SerializedPropertyType.AnimationCurve: p.animationCurveValue = (AnimationCurve)value; return; + case SerializedPropertyType.ObjectReference: p.objectReferenceValue = (UnityEngine.Object)value; return; + case SerializedPropertyType.ExposedReference: p.exposedReferenceValue = (UnityEngine.Object)value; return; + case SerializedPropertyType.ManagedReference: p.managedReferenceValue = value; return; + + case SerializedPropertyType.LayerMask: + try + { + p.intValue = ((LayerMask)value).value; return; + } + catch (System.InvalidCastException) + { + p.intValue = System.Convert.ToInt32(value); return; + } + + } + + } + catch { } + + } + + static object _noValue = new(); + +#endif + + + + + #endregion + + #region GlobalID + +#if UNITY_EDITOR + + [System.Serializable] + public struct GlobalID : System.IEquatable + { + public Object GetObject() => GlobalObjectId.GlobalObjectIdentifierToObjectSlow(globalObjectId); + public int GetObjectInstanceId() => GlobalObjectId.GlobalObjectIdentifierToInstanceIDSlow(globalObjectId); + + + public int idType => globalObjectId.identifierType; + public string guid => globalObjectId.assetGUID.ToString(); + public ulong fileId => globalObjectId.targetObjectId; + public ulong prefabId => globalObjectId.targetPrefabId; + + public bool isNull => globalObjectId.identifierType == 0; + public bool isAsset => globalObjectId.identifierType == 1; + public bool isSceneObject => globalObjectId.identifierType == 2; + + public GlobalObjectId globalObjectId => _globalObjectId.Equals(default) && globalObjectIdString != null && GlobalObjectId.TryParse(globalObjectIdString, out var r) ? _globalObjectId = r : _globalObjectId; + public GlobalObjectId _globalObjectId; + + public GlobalID(Object o) => globalObjectIdString = (_globalObjectId = GlobalObjectId.GetGlobalObjectIdSlow(o)).ToString(); + public GlobalID(string s) => globalObjectIdString = GlobalObjectId.TryParse(s, out _globalObjectId) ? s : s; + + public string globalObjectIdString; + + + + public bool Equals(GlobalID other) => this.globalObjectIdString.Equals(other.globalObjectIdString); + + public static bool operator ==(GlobalID a, GlobalID b) => a.Equals(b); + public static bool operator !=(GlobalID a, GlobalID b) => !a.Equals(b); + + public override bool Equals(object other) => other is GlobalID otherglobalID && this.Equals(otherglobalID); + public override int GetHashCode() => globalObjectIdString == null ? 0 : globalObjectIdString.GetHashCode(); + + + public override string ToString() => globalObjectIdString; + + + + + public GlobalID UnpackForPrefab() + { + var unpackedFileId = (this.fileId ^ this.prefabId) & 0x7fffffffffffffff; + + var unpackedGId = new GlobalID($"GlobalObjectId_V1-{this.idType}-{this.guid}-{unpackedFileId}-0"); + + return unpackedGId; + + } + + } + + public static GlobalID GetGlobalID(this Object o) => new(o); + public static GlobalID[] GetGlobalIDs(this IEnumerable instanceIds) + { + var unityGlobalIds = new GlobalObjectId[instanceIds.Count()]; + + GlobalObjectId.GetGlobalObjectIdsSlow(instanceIds.ToArray(), unityGlobalIds); + + var globalIds = unityGlobalIds.Select(r => new GlobalID(r.ToString())); + + return globalIds.ToArray(); + + } + + public static Object[] GetObjects(this IEnumerable globalIDs) + { + var goids = globalIDs.Select(r => r.globalObjectId).ToArray(); + + var objects = new Object[goids.Length]; + + GlobalObjectId.GlobalObjectIdentifiersToObjectsSlow(goids, objects); + + return objects; + + } + public static int[] GetObjectInstanceIds(this IEnumerable globalIDs) + { + var goids = globalIDs.Select(r => r.globalObjectId).ToArray(); + + var iids = new int[goids.Length]; + + GlobalObjectId.GlobalObjectIdentifiersToInstanceIDsSlow(goids, iids); + + return iids; + + } + + +#endif + + + + + #endregion + + #region Editor + +#if UNITY_EDITOR + + + public static class EditorUtils + { + + public static void OpenFolder(string path) + { + var folder = AssetDatabase.LoadAssetAtPath(path, typeof(Object)); + + var t = typeof(Editor).Assembly.GetType("UnityEditor.ProjectBrowser"); + var w = (EditorWindow)t.GetField("s_LastInteractedProjectBrowser").GetValue(null); + + var m_ListAreaState = t.GetField("m_ListAreaState", maxBindingFlags).GetValue(w); + + m_ListAreaState.GetType().GetField("m_SelectedInstanceIDs").SetValue(m_ListAreaState, new List { folder.GetInstanceID() }); + + t.GetMethod("OpenSelectedFolders", maxBindingFlags).Invoke(null, null); + + } + + public static void PingObject(Object o, bool select = false, bool focusProjectWindow = true) + { + if (select) + { + Selection.activeObject = null; + Selection.activeObject = o; + } + if (focusProjectWindow) EditorUtility.FocusProjectWindow(); + EditorGUIUtility.PingObject(o); + + } + public static void PingObject(string guid, bool select = false, bool focusProjectWindow = true) => PingObject(AssetDatabase.LoadAssetAtPath(guid.ToPath())); + + public static EditorWindow OpenObjectPicker(Object obj = null, bool allowSceneObjects = false, string searchFilter = "", int controlID = 0) where T : Object + { + EditorGUIUtility.ShowObjectPicker(obj, allowSceneObjects, searchFilter, controlID); + + return Resources.FindObjectsOfTypeAll(typeof(Editor).Assembly.GetType("UnityEditor.ObjectSelector")).FirstOrDefault() as EditorWindow; + + } + public static EditorWindow OpenColorPicker(System.Action colorChangedCallback, Color color, bool showAlpha = true, bool hdr = false) + { + typeof(Editor).Assembly.GetType("UnityEditor.ColorPicker").InvokeMethod("Show", colorChangedCallback, color, showAlpha, hdr); + + return typeof(Editor).Assembly.GetType("UnityEditor.ColorPicker").GetPropertyValue("instance"); + + } + + + + public static bool CheckUnityVersion(string versionQuery) + { + if (versionQueryCache.TryGetValue(versionQuery, out var cachedResult)) return cachedResult; + + if (versionQuery.Any(r => r.IsLetter() && !versionQuery.EndsWith(" or older") && !versionQuery.EndsWith(" or newer"))) throw new System.ArgumentException("Invalid unity version query"); + + + + + var curVersion = new string(Application.unityVersion.TakeWhile(r => !r.IsLetter()).ToArray()); + + var curMajor = int.Parse(curVersion.Split('.')[0]); + var curMinor = int.Parse(curVersion.Split('.')[1]); + var curPatch = int.Parse(curVersion.Split('.')[2]); + + + + + + var givenVersion = new string(versionQuery.TakeWhile(r => !r.IsWhitespace()).ToArray()); + + var isMinorGiven = givenVersion.Count(r => r == '.') >= 1; + var isPatchGiven = givenVersion.Count(r => r == '.') >= 2; + + var givenMajor = int.Parse(givenVersion.Split('.')[0]); + var givenMinor = isMinorGiven ? int.Parse(givenVersion.Split('.')[1]) : 0; + var givenPatch = isPatchGiven ? int.Parse(givenVersion.Split('.')[2]) : 0; + + + + + + + var curVersionCanBeNewer = versionQuery.Contains("or newer"); + var curVersionCanBeOlder = versionQuery.Contains("or older"); + + + if (curMajor > givenMajor) return versionQueryCache[versionQuery] = curVersionCanBeNewer; + if (curMajor < givenMajor) return versionQueryCache[versionQuery] = curVersionCanBeOlder; + + if (!isMinorGiven) return versionQueryCache[versionQuery] = true; + + + if (curMinor > givenMinor) return versionQueryCache[versionQuery] = curVersionCanBeNewer; + if (curMinor < givenMinor) return versionQueryCache[versionQuery] = curVersionCanBeOlder; + + if (!isPatchGiven) return versionQueryCache[versionQuery] = true; + + + if (curPatch > givenPatch) return versionQueryCache[versionQuery] = curVersionCanBeNewer; + if (curPatch < givenPatch) return versionQueryCache[versionQuery] = curVersionCanBeOlder; + + return versionQueryCache[versionQuery] = true; + + + + + // query examples: + // + // "2022.3.5 or newer" + // "2022.3.5 or older" + // "2022.3 or older" + // "2022.3" + // "2022" + + } + + static Dictionary versionQueryCache = new(); + + + + public static void SetSymbolDefinedInAsmdef(string asmdefName, string symbol, bool defined) + { + var isDefined = IsSymbolDefinedInAsmdef(asmdefName, symbol); + var shouldBeDefined = defined; + + if (shouldBeDefined && !isDefined) + DefineSymbolInAsmdef(asmdefName, symbol); + + if (!shouldBeDefined && isDefined) + UndefineSymbolInAsmdef(asmdefName, symbol); + + } + public static bool IsSymbolDefinedInAsmdef(string asmdefName, string symbol) + { + var path = AssetDatabase.FindAssets("t: asmdef " + asmdefName, null).First().ToPath(); + var importer = AssetImporter.GetAtPath(path); + + var editorType = typeof(Editor).Assembly.GetType("UnityEditor.AssemblyDefinitionImporterInspector"); + var editor = Editor.CreateEditor(importer, editorType); + + var state = editor.GetFieldValue("m_ExtraDataTargets").First(); + + + var definesList = state.GetFieldValue("versionDefines"); + var isSymbolDefined = Enumerable.Range(0, definesList.Count).Any(i => definesList[i].GetFieldValue("define") == symbol); + + + Object.DestroyImmediate(editor); + + return isSymbolDefined; + + } + + static void DefineSymbolInAsmdef(string asmdefName, string symbol) + { + var path = AssetDatabase.FindAssets("t: asmdef " + asmdefName, null).First().ToPath(); + var importer = AssetImporter.GetAtPath(path); + + var editorType = typeof(Editor).Assembly.GetType("UnityEditor.AssemblyDefinitionImporterInspector"); + var editor = Editor.CreateEditor(importer, editorType); + + var state = editor.GetFieldValue("m_ExtraDataTargets").First(); + + + var definesList = state.GetFieldValue("versionDefines"); + + var defineType = definesList.GetType().GenericTypeArguments[0]; + var newDefine = System.Activator.CreateInstance(defineType); + + newDefine.SetFieldValue("name", "Unity"); + newDefine.SetFieldValue("define", symbol); + + definesList.Add(newDefine); + + + editor.InvokeMethod("Apply"); + + Object.DestroyImmediate(editor); + + } + static void UndefineSymbolInAsmdef(string asmdefName, string symbol) + { + var path = AssetDatabase.FindAssets("t: asmdef " + asmdefName, null).First().ToPath(); + var importer = AssetImporter.GetAtPath(path); + + var editorType = typeof(Editor).Assembly.GetType("UnityEditor.AssemblyDefinitionImporterInspector"); + var editor = Editor.CreateEditor(importer, editorType); + + var state = editor.GetFieldValue("m_ExtraDataTargets").First(); + + + var definesList = state.GetFieldValue("versionDefines"); + + var defineIndex = Enumerable.Range(0, definesList.Count).First(i => definesList[i].GetFieldValue("define") == symbol); + + definesList.RemoveAt(defineIndex); + + + editor.InvokeMethod("Apply"); + + Object.DestroyImmediate(editor); + + } + + + + + public static int GetCurrendUndoGroupIndex() + { + var args = new object[] { _dummyList, 0 }; + + typeof(Undo).GetMethodInfo("GetRecords", typeof(List), typeof(int).MakeByRefType()) + .Invoke(null, args); + + + return (int)args[1]; + + } + + static List _dummyList = new(); + + + + + + public static void Hide(string path) + { + if (IsHidden(path)) return; + + if (File.Exists(path)) + File.Move(path, path + "~"); + + + path += ".meta"; + if (File.Exists(path)) + File.Move(path, path + "~"); + } + public static void Unhide(string path) + { + if (!IsHidden(path)) return; + if (path.EndsWith("~")) path = path.Substring(0, path.Length - 1); + + if (File.Exists(path + "~")) + File.Move(path + "~", path); + + path += ".meta"; + if (File.Exists(path + "~")) + File.Move(path + "~", path); + } + public static bool IsHidden(string path) => path.EndsWith("~") || File.Exists(path + "~"); + + + public static void CopyDirectoryDeep(string sourcePath, string destinationPath) + { + CopyDirectoryRecursively(sourcePath, destinationPath); + + var metas = GetFilesRecursively(destinationPath, (f) => f.EndsWith(".meta")); + var guidTable = new List<(string originalGuid, string newGuid)>(); + + foreach (string meta in metas) + { + StreamReader file = new(meta); + file.ReadLine(); + string guidLine = file.ReadLine(); + file.Close(); + string originalGuid = guidLine.Substring(6, guidLine.Length - 6); + string newGuid = GUID.Generate().ToString().Replace("-", ""); + guidTable.Add((originalGuid, newGuid)); + } + + var allFiles = GetFilesRecursively(destinationPath); + + foreach (string fileToModify in allFiles) + { + string content = File.ReadAllText(fileToModify); + + foreach (var guidPair in guidTable) + { + content = content.Replace(guidPair.originalGuid, guidPair.newGuid); + } + + File.WriteAllText(fileToModify, content); + } + + AssetDatabase.Refresh(); + } + + private static void CopyDirectoryRecursively(string sourceDirName, string destDirName) + { + DirectoryInfo dir = new(sourceDirName); + + DirectoryInfo[] dirs = dir.GetDirectories(); + + if (!Directory.Exists(destDirName)) + { + Directory.CreateDirectory(destDirName); + } + + FileInfo[] files = dir.GetFiles(); + foreach (FileInfo file in files) + { + string temppath = Path.Combine(destDirName, file.Name); + file.CopyTo(temppath, false); + } + + foreach (DirectoryInfo subdir in dirs) + { + string temppath = Path.Combine(destDirName, subdir.Name); + CopyDirectoryRecursively(subdir.FullName, temppath); + } + } + + private static List GetFilesRecursively(string path, System.Func criteria = null, List files = null) + { + if (files == null) + { + files = new List(); + } + + files.AddRange(Directory.GetFiles(path).Where(f => criteria == null || criteria(f))); + + foreach (string directory in Directory.GetDirectories(path)) + { + GetFilesRecursively(directory, criteria, files); + } + + return files; + } + + + + + + // for non-extension methods + + } + + + public static class EditorPrefsCached + { + public static int GetInt(string key, int defaultValue = 0) + { + if (ints_byKey.ContainsKey(key)) + return ints_byKey[key]; + else + return ints_byKey[key] = EditorPrefs.GetInt(key, defaultValue); + + } + public static bool GetBool(string key, bool defaultValue = false) + { + if (bools_byKey.ContainsKey(key)) + return bools_byKey[key]; + else + return bools_byKey[key] = EditorPrefs.GetBool(key, defaultValue); + + } + public static float GetFloat(string key, float defaultValue = 0) + { + if (floats_byKey.ContainsKey(key)) + return floats_byKey[key]; + else + return floats_byKey[key] = EditorPrefs.GetFloat(key, defaultValue); + + } + public static string GetString(string key, string defaultValue = "") + { + if (strings_byKey.ContainsKey(key)) + return strings_byKey[key]; + else + return strings_byKey[key] = EditorPrefs.GetString(key, defaultValue); + + } + + public static void SetInt(string key, int value) + { + ints_byKey[key] = value; + + EditorPrefs.SetInt(key, value); + + } + public static void SetBool(string key, bool value) + { + bools_byKey[key] = value; + + EditorPrefs.SetBool(key, value); + + } + public static void SetFloat(string key, float value) + { + floats_byKey[key] = value; + + EditorPrefs.SetFloat(key, value); + + } + public static void SetString(string key, string value) + { + strings_byKey[key] = value; + + EditorPrefs.SetString(key, value); + + } + + + static Dictionary ints_byKey = new(); + static Dictionary bools_byKey = new(); + static Dictionary floats_byKey = new(); + static Dictionary strings_byKey = new(); + + } + + public static class ProjectPrefs + { + public static int GetInt(string key, int defaultValue = 0) => EditorPrefsCached.GetInt(key + projectId, defaultValue); + public static bool GetBool(string key, bool defaultValue = false) => EditorPrefsCached.GetBool(key + projectId, defaultValue); + public static float GetFloat(string key, float defaultValue = 0) => EditorPrefsCached.GetFloat(key + projectId, defaultValue); + public static string GetString(string key, string defaultValue = "") => EditorPrefsCached.GetString(key + projectId, defaultValue); + + public static void SetInt(string key, int value) => EditorPrefsCached.SetInt(key + projectId, value); + public static void SetBool(string key, bool value) => EditorPrefsCached.SetBool(key + projectId, value); + public static void SetFloat(string key, float value) => EditorPrefsCached.SetFloat(key + projectId, value); + public static void SetString(string key, string value) => EditorPrefsCached.SetString(key + projectId, value); + + + + public static bool HasKey(string key) => EditorPrefs.HasKey(key + projectId); + public static void DeleteKey(string key) => EditorPrefs.DeleteKey(key + projectId); + + + + public static int projectId => PlayerSettings.productGUID.GetHashCode(); + + } + + + + public static void RecordUndo(this Object o, string operationName = "") => Undo.RecordObject(o, operationName); + public static void Dirty(this Object o) => UnityEditor.EditorUtility.SetDirty(o); + public static void Save(this Object o) => AssetDatabase.SaveAssetIfDirty(o); + + + + public static void SelectInInspector(this Object[] objects, bool frameInHierarchy = false, bool frameInProject = false) + { + void setHierarchyLocked(bool isLocked) => allHierarchies.ForEach(r => r?.GetMemberValue("m_SceneHierarchy")?.SetMemberValue("m_RectSelectInProgress", isLocked)); + void setProjectLocked(bool isLocked) => allProjectBrowsers.ForEach(r => r?.SetMemberValue("m_InternalSelectionChange", isLocked)); + + + if (!frameInHierarchy) setHierarchyLocked(true); + if (!frameInProject) setProjectLocked(true); + + Selection.objects = objects?.ToArray(); + + if (!frameInHierarchy) EditorApplication.delayCall += () => setHierarchyLocked(false); + if (!frameInProject) EditorApplication.delayCall += () => setProjectLocked(false); + + } + public static void SelectInInspector(this Object obj, bool frameInHierarchy = false, bool frameInProject = false) => new[] { obj }.SelectInInspector(frameInHierarchy, frameInProject); + + static IEnumerable allHierarchies => _allHierarchies ??= typeof(Editor).Assembly.GetType("UnityEditor.SceneHierarchyWindow").GetFieldValue("s_SceneHierarchyWindows").Cast(); + static IEnumerable _allHierarchies; + + static IEnumerable allProjectBrowsers => _allProjectBrowsers ??= typeof(Editor).Assembly.GetType("UnityEditor.ProjectBrowser").GetFieldValue("s_ProjectBrowsers").Cast(); + static IEnumerable _allProjectBrowsers; + + + + public static void MoveTo(this EditorWindow window, Vector2 position, bool ensureFitsOnScreen = true) + { + if (!ensureFitsOnScreen) { window.position = window.position.SetPos(position); return; } + + var windowRect = window.position; + var unityWindowRect = EditorGUIUtility.GetMainWindowPosition(); + + position.x = position.x.Max(unityWindowRect.position.x); + position.y = position.y.Max(unityWindowRect.position.y); + + position.x = position.x.Min(unityWindowRect.xMax - windowRect.width); + position.y = position.y.Min(unityWindowRect.yMax - windowRect.height); + + window.position = windowRect.SetPos(position); + + } + + + +#endif + + #endregion + + } + + public static class VGUI + { + + #region Drawing + + + public static Rect Draw(this Rect rect, Color color) + { + EditorGUI.DrawRect(rect, color); + + return rect; + + } + public static Rect Draw(this Rect rect) => rect.Draw(Color.black); + + public static Rect DrawOutline(this Rect rect, Color color, float thickness = 1) + { + + rect.SetWidth(thickness).Draw(color); + rect.SetWidthFromRight(thickness).Draw(color); + + rect.SetHeight(thickness).Draw(color); + rect.SetHeightFromBottom(thickness).Draw(color); + + + return rect; + + } + public static Rect DrawOutline(this Rect rect, float thickness = 1) => rect.DrawOutline(Color.black, thickness); + + + + + public static Rect DrawRounded(this Rect rect, Color color, int cornerRadius) + { + if (!curEvent.isRepaint) return rect; + + cornerRadius = cornerRadius.Min((rect.height / 2).FloorToInt()).Min((rect.width / 2).FloorToInt()); + + if (cornerRadius < 0) return rect; + + GUIStyle style; + + void getStyle() + { + if (_roundedStylesByCornerRadius.TryGetValue(cornerRadius, out style)) return; + + var pixelsPerPoint = 2; + + var res = cornerRadius * 2 * pixelsPerPoint; + var pixels = new Color[res * res]; + + var white = Greyscale(1, 1); + var clear = Greyscale(1, 0); + var halfRes = res / 2; + + for (int x = 0; x < res; x++) + for (int y = 0; y < res; y++) + { + var sqrMagnitude = (new Vector2(x - halfRes + .5f, y - halfRes + .5f)).sqrMagnitude; + pixels[x + y * res] = sqrMagnitude <= halfRes * halfRes ? white : clear; + } + + var texture = new Texture2D(res, res); + texture.SetPropertyValue("pixelsPerPoint", pixelsPerPoint); + texture.hideFlags = HideFlags.DontSave; + texture.SetPixels(pixels); + texture.Apply(); + + + + style = new GUIStyle(); + style.normal.background = texture; + style.alignment = TextAnchor.MiddleCenter; + style.border = new RectOffset(cornerRadius, cornerRadius, cornerRadius, cornerRadius); + + + _roundedStylesByCornerRadius[cornerRadius] = style; + + } + void draw() + { + SetGUIColor(color); + + style.Draw(rect, false, false, false, false); + + ResetGUIColor(); + + } + + getStyle(); + draw(); + + return rect; + + } + public static Rect DrawRounded(this Rect rect, Color color, float cornerRadius) => rect.DrawRounded(color, cornerRadius.RoundToInt()); + + static Dictionary _roundedStylesByCornerRadius = new(); + + + + + public static Rect DrawBlurred(this Rect rect, Color color, int blurRadius) + { + if (!curEvent.isRepaint) return rect; + + var pixelsPerPoint = .5f; + // var pixelsPerPoint = 1f; + + var blurRadiusScaled = (blurRadius * pixelsPerPoint).RoundToInt().Max(1).Min(123); + + var croppedRectWidth = (rect.width * pixelsPerPoint).RoundToInt().Min(blurRadiusScaled * 2); + var croppedRectHeight = (rect.height * pixelsPerPoint).RoundToInt().Min(blurRadiusScaled * 2); + + var textureWidth = croppedRectWidth + blurRadiusScaled * 2; + var textureHeight = croppedRectHeight + blurRadiusScaled * 2; + + if (textureWidth <= 0 || textureWidth > 1232) return rect; + if (textureHeight <= 0 || textureHeight > 1232) return rect; + + + GUIStyle style; + + void getStyle() + { + if (_blurredStylesByTextureSize.TryGetValue((textureWidth, textureHeight), out style)) return; + + // VDebug.LogStart(blurRadius + ""); + + var pixels = new Color[textureWidth * textureHeight]; + var kernel = GaussianKernel.GenerateArray(blurRadiusScaled * 2 + 1); + + for (int x = 0; x < textureWidth; x++) + for (int y = 0; y < textureHeight; y++) + { + var sum = 0f; + + for (int xSample = (x - blurRadiusScaled).Max(blurRadiusScaled); xSample <= (x + blurRadiusScaled).Min(textureWidth - 1 - blurRadiusScaled); xSample++) + for (int ySample = (y - blurRadiusScaled).Max(blurRadiusScaled); ySample <= (y + blurRadiusScaled).Min(textureHeight - 1 - blurRadiusScaled); ySample++) + sum += kernel[blurRadiusScaled + xSample - x, blurRadiusScaled + ySample - y]; + + pixels[x + y * textureWidth] = Greyscale(1, sum); + + } + + var texture = new Texture2D(textureWidth, textureHeight); + texture.SetPropertyValue("pixelsPerPoint", pixelsPerPoint); + texture.hideFlags = HideFlags.DontSave; + texture.SetPixels(pixels); + texture.Apply(); + + + style = new GUIStyle(); + style.normal.background = texture; + style.alignment = TextAnchor.MiddleCenter; + + var borderX = ((textureWidth / 2f - 1) / pixelsPerPoint).FloorToInt(); + var borderY = ((textureHeight / 2f - 1) / pixelsPerPoint).FloorToInt(); + style.border = new RectOffset(borderX, borderX, borderY, borderY); + + _blurredStylesByTextureSize[(textureWidth, textureHeight)] = style; + + // VDebug.LogFinish(); + + } + void draw() + { + SetGUIColor(color); + + style.Draw(rect.SetSizeFromMid(rect.width + blurRadius * 2, rect.height + blurRadius * 2), false, false, false, false); + + ResetGUIColor(); + + } + + getStyle(); + draw(); + + return rect; + + } + public static Rect DrawBlurred(this Rect rect, Color color, float blurRadius) => rect.DrawBlurred(color, blurRadius.RoundToInt()); + + static Dictionary<(int, int), GUIStyle> _blurredStylesByTextureSize = new(); + + + + + static void DrawCurtain(this Rect rect, Color color, int dir) + { + void genTextures() + { + if (_gradientTextures != null) return; + + _gradientTextures = new Texture2D[4]; + + // var pixels = Enumerable.Range(0, 256).Select(r => Greyscale(1, r / 255f)); + var pixels = Enumerable.Range(0, 256).Select(r => Greyscale(1, (r / 255f).Smoothstep())); + + var up = new Texture2D(1, 256); + up.SetPixels(pixels.Reverse().ToArray()); + up.Apply(); + up.hideFlags = HideFlags.DontSave; + up.wrapMode = TextureWrapMode.Clamp; + _gradientTextures[0] = up; + + var down = new Texture2D(1, 256); + down.SetPixels(pixels.ToArray()); + down.Apply(); + down.hideFlags = HideFlags.DontSave; + down.wrapMode = TextureWrapMode.Clamp; + _gradientTextures[1] = down; + + var left = new Texture2D(256, 1); + left.SetPixels(pixels.ToArray()); + left.Apply(); + left.hideFlags = HideFlags.DontSave; + left.wrapMode = TextureWrapMode.Clamp; + _gradientTextures[2] = left; + + var right = new Texture2D(256, 1); + right.SetPixels(pixels.Reverse().ToArray()); + right.Apply(); + right.hideFlags = HideFlags.DontSave; + right.wrapMode = TextureWrapMode.Clamp; + _gradientTextures[3] = right; + + } + void draw() + { + SetGUIColor(color); + + GUI.DrawTexture(rect, _gradientTextures[dir]); + + ResetGUIColor(); + + } + + genTextures(); + draw(); + + } + + static Texture2D[] _gradientTextures; + + public static void DrawCurtainUp(this Rect rect, Color color) => rect.DrawCurtain(color, 0); + public static void DrawCurtainDown(this Rect rect, Color color) => rect.DrawCurtain(color, 1); + public static void DrawCurtainLeft(this Rect rect, Color color) => rect.DrawCurtain(color, 2); + public static void DrawCurtainRight(this Rect rect, Color color) => rect.DrawCurtain(color, 3); + + + + + + + #endregion + + #region Events + + + public class WrappedEvent + { + public Event e; + + public bool isRepaint => e.type == EventType.Repaint; + public bool isLayout => e.type == EventType.Layout; + public bool isUsed => e.type == EventType.Used; + public bool isMouseLeaveWindow => e.type == EventType.MouseLeaveWindow; + public bool isMouseEnterWindow => e.type == EventType.MouseEnterWindow; + public bool isContextClick => e.type == EventType.ContextClick; + public bool isIgnore => e.type == EventType.Ignore; + + public bool isKeyDown => e.type == EventType.KeyDown; + public bool isKeyUp => e.type == EventType.KeyUp; + public KeyCode keyCode => e.keyCode; + public char characted => e.character; + + public bool isExecuteCommand => e.type == EventType.ExecuteCommand; + public string commandName => e.commandName; + + public bool isMouse => e.isMouse; + public bool isMouseDown => e.type == EventType.MouseDown; + public bool isMouseUp => e.type == EventType.MouseUp; + public bool isMouseDrag => e.type == EventType.MouseDrag; + public bool isMouseMove => e.type == EventType.MouseMove; + public bool isScroll => e.type == EventType.ScrollWheel; + public int mouseButton => e.button; + public int clickCount => e.clickCount; + public Vector2 mousePosition => e.mousePosition; + public Vector2 mousePosition_screenSpace => GUIUtility.GUIToScreenPoint(e.mousePosition); + public Vector2 mouseDelta => e.delta; + + public bool isDragUpdate => e.type == EventType.DragUpdated; + public bool isDragPerform => e.type == EventType.DragPerform; + public bool isDragExit => e.type == EventType.DragExited; + + public EventModifiers modifiers => e.modifiers; + public bool holdingAnyModifierKey => modifiers != EventModifiers.None; + + public bool holdingAlt => e.alt; + public bool holdingShift => e.shift; + public bool holdingCtrl => e.control; + public bool holdingCmd => e.command; + public bool holdingCmdOrCtrl => e.command || e.control; + + public bool holdingAltOnly => e.modifiers == EventModifiers.Alt; // in some sessions FunctionKey is always pressed? + public bool holdingShiftOnly => e.modifiers == EventModifiers.Shift; // in some sessions FunctionKey is always pressed? + public bool holdingCtrlOnly => e.modifiers == EventModifiers.Control; + public bool holdingCmdOnly => e.modifiers == EventModifiers.Command; + public bool holdingCmdOrCtrlOnly => (e.modifiers == EventModifiers.Command || e.modifiers == EventModifiers.Control); + + public EventType type => e.type; + + public void Use() => e?.Use(); + + + public WrappedEvent(Event e) => this.e = e; + + public override string ToString() => e.ToString(); + + } + + public static WrappedEvent Wrap(this Event e) => new(e); + + public static WrappedEvent curEvent => _curEvent ??= typeof(Event).GetFieldValue("s_Current").Wrap(); + static WrappedEvent _curEvent; + + + + + + #endregion + + #region Shortcuts + + + public static Rect lastRect => GUILayoutUtility.GetLastRect(); + + public static bool isDarkTheme => EditorGUIUtility.isProSkin; + + public static bool IsHovered(this Rect r) => r.Contains(curEvent.mousePosition); + + public static float GetLabelWidth(this string s) => GUI.skin.label.CalcSize(new GUIContent(s)).x; + public static float GetLabelWidth(this string s, int fontSize) + { + SetLabelFontSize(fontSize); + + var r = s.GetLabelWidth(); + + ResetLabelStyle(); + + return r; + + } + public static float GetLabelWidth(this string s, bool isBold) + { + if (isBold) + SetLabelBold(); + + var r = s.GetLabelWidth(); + + if (isBold) + ResetLabelStyle(); + + return r; + + } + public static float GetLabelWidth(this string s, int fontSize, bool isBold) + { + if (isBold) + SetLabelBold(); + + SetLabelFontSize(fontSize); + + var r = s.GetLabelWidth(); + + ResetLabelStyle(); + + return r; + + } + + public static void SetGUIEnabled(bool enabled) { _prevGuiEnabled = GUI.enabled; GUI.enabled = enabled; } + public static void ResetGUIEnabled() => GUI.enabled = _prevGuiEnabled; + static bool _prevGuiEnabled = true; + + public static void SetLabelFontSize(int size) => GUI.skin.label.fontSize = size; + public static void SetLabelBold() => GUI.skin.label.fontStyle = FontStyle.Bold; + public static void SetLabelAlignmentCenter() => GUI.skin.label.alignment = TextAnchor.MiddleCenter; + public static void ResetLabelStyle() + { + GUI.skin.label.fontSize = 0; + GUI.skin.label.fontStyle = FontStyle.Normal; + GUI.skin.label.alignment = TextAnchor.MiddleLeft; + GUI.skin.label.wordWrap = false; + } + + + public static void SetGUIColor(Color c) + { + _guiColorStack.Push(GUI.color); + + GUI.color *= c; + + } + public static void ResetGUIColor() + { + GUI.color = _guiColorStack.Pop(); + } + + static Stack _guiColorStack = new(); + + + + public static float editorDeltaTime = .0166f; + + static void EditorDeltaTime_Update() + { + editorDeltaTime = (float)(EditorApplication.timeSinceStartup - _lastUpdateTime); + + _lastUpdateTime = EditorApplication.timeSinceStartup; + + } + static double _lastUpdateTime; + + [InitializeOnLoadMethod] + static void EditorDeltaTime_Subscribe() + { + EditorApplication.update -= EditorDeltaTime_Update; + EditorApplication.update += EditorDeltaTime_Update; + } + + + + + #endregion + + #region Controls + + + public static bool IconButton(Rect rect, string iconName, float iconSize = default, Color color = default, Color colorHovered = default, Color colorPressed = default) + { + var id = EditorGUIUtility.GUIToScreenRect(rect).GetHashCode();// GUIUtility.GetControlID(FocusType.Passive, rect); + var isPressed = id == _pressedIconButtonId; + + var wasActivated = false; + + void icon() + { + if (!curEvent.isRepaint) return; + + + if (color == default) + color = Color.white; + + if (colorHovered == default) + colorHovered = Color.white; + + if (colorPressed == default) + colorPressed = Color.white.SetAlpha(.6f); + + + if (rect.IsHovered()) + color = colorHovered; + + if (isPressed) + color = colorPressed; + + + if (iconSize == default) + iconSize = rect.width.Min(rect.height); + + var iconRect = rect.SetSizeFromMid(iconSize); + + + + SetGUIColor(color); + + GUI.DrawTexture(iconRect, EditorIcons.GetIcon(iconName)); + + ResetGUIColor(); + + + } + void mouseDown() + { + if (!curEvent.isMouseDown) return; + if (!rect.IsHovered()) return; + + _pressedIconButtonId = id; + + curEvent.Use(); + + } + void mouseUp() + { + if (!curEvent.isMouseUp) return; + if (!isPressed) return; + + _pressedIconButtonId = 0; + + if (rect.IsHovered()) + wasActivated = true; + + curEvent.Use(); + + } + void mouseDrag() + { + if (!curEvent.isMouseDrag) return; + if (!isPressed) return; + + curEvent.Use(); + + } + + rect.MarkInteractive(); + + icon(); + mouseDown(); + mouseUp(); + mouseDrag(); + + return wasActivated; + + } + + static int _pressedIconButtonId; + + + + + + #endregion + + #region Layout + + + public static void Space(float px = 6) => GUILayout.Space(px); + + public static Rect ExpandWidthLabelRect() { GUILayout.Label(""/* , GUILayout.Height(0) */, GUILayout.ExpandWidth(true)); return lastRect; } + public static Rect ExpandWidthLabelRect(float height) { GUILayout.Label("", GUILayout.Height(height), GUILayout.ExpandWidth(true)); return lastRect; } + + + + + public static void BeginIndent(float f) + { + GUILayout.BeginHorizontal(); + GUILayout.Space(f); + GUILayout.BeginVertical(); + + _indentLabelWidthStack.Push(EditorGUIUtility.labelWidth); + + EditorGUIUtility.labelWidth -= f; + } + + public static void EndIndent(float f = 0) + { + GUILayout.EndVertical(); + GUILayout.Space(f); + GUILayout.EndHorizontal(); + + EditorGUIUtility.labelWidth = _indentLabelWidthStack.Pop(); + } + static Stack _indentLabelWidthStack = new(); + + + + + + #endregion + + #region GUIColors + + + public static class GUIColors + { + public static Color windowBackground => isDarkTheme ? Greyscale(.22f) : Greyscale(.78f); // prev backgroundCol + public static Color pressedButtonBackground => isDarkTheme ? new Color(.48f, .76f, 1f, 1f) * 1.4f : new Color(.48f, .7f, 1f, 1f) * 1.2f; // prev pressedButtonCol + public static Color greyedOutTint => Greyscale(.7f); + public static Color selectedBackground => isDarkTheme ? new Color(.17f, .365f, .535f) : new Color(.2f, .375f, .555f) * 1.2f; + } + + + + + #endregion + + #region EditorIcons + + + public static partial class EditorIcons + { + public static Texture2D GetIcon(string iconNameOrPath, bool returnNullIfNotFound = false) + { + iconNameOrPath ??= ""; + + if (icons_byName.TryGetValue(iconNameOrPath, out var cachedResult) && cachedResult) return cachedResult; + + + Texture2D icon = null; + + void getCustom() + { + if (icon) return; + if (!customIcons.ContainsKey(iconNameOrPath)) return; + + var pngBytesString = customIcons[iconNameOrPath]; + var pngBytes = pngBytesString.Split("-").Select(r => System.Convert.ToByte(r, 16)).ToArray(); + + icon = new Texture2D(1, 1); + + icon.LoadImage(pngBytes); + + } + void getBuiltin() + { + if (icon) return; + + icon = typeof(EditorGUIUtility).InvokeMethod("LoadIcon", iconNameOrPath) as Texture2D; + + } + + getCustom(); + getBuiltin(); + + icons_byName[iconNameOrPath] = icon; + + if (icon == null && !returnNullIfNotFound) return Texture2D.grayTexture; + else return icon; + + } + + static Dictionary icons_byName = new(); + + static Dictionary customIcons = new() + { + ["Search_"] = "89-50-4E-47-0D-0A-1A-0A-00-00-00-0D-49-48-44-52-00-00-00-20-00-00-00-20-08-06-00-00-00-73-7A-7A-F4-00-00-00-09-70-48-59-73-00-00-0B-13-00-00-0B-13-01-00-9A-9C-18-00-00-00-01-73-52-47-42-00-AE-CE-1C-E9-00-00-00-04-67-41-4D-41-00-00-B1-8F-0B-FC-61-05-00-00-02-00-49-44-41-54-78-01-ED-56-3D-4F-02-41-10-1D-3E-22-C6-C4-CA-D0-9A-58-6B-87-8D-12-6B-0B-1B-7F-07-85-89-95-F4-34-D6-F2-07-2C-FD-23-D0-41-A2-8D-FF-00-8A-0B-0D-7A-70-7C-1C-EB-7B-B8-47-16-C4-70-7B-77-1C-0D-2F-B9-DC-DE-DD-CE-CE-DB-99-D9-37-27-B2-C7-8E-91-B1-99-EC-38-CE-71-A1-50-38-E0-78-34-1A-8D-8B-C5-E2-97-6C-1B-4A-A9-43-38-AB-8E-C7-E3-CF-D9-6C-E6-29-0D-DF-F7-5D-BE-C3-B7-1A-E7-C8-36-E0-BA-EE-1D-1C-39-6A-03-40-CC-E5-5C-49-12-9E-E7-55-B0-B0-AF-1D-F8-93-C9-A4-81-77-0F-B8-97-79-E9-71-DB-20-E1-83-44-45-92-00-77-13-38-47-04-BA-83-C1-E0-FA-BF-B9-20-71-85-39-3D-83-44-BC-48-30-9F-0C-A9-5E-B0-D7-E9-74-8E-36-D9-70-0E-89-6A-C2-4E-AC-9A-D0-45-35-07-43-1D-D6-0E-51-2A-07-51-63-D1-4A-54-B0-B2-B5-F3-B6-58-02-36-4D-DA-72-8D-B0-36-59-F3-81-E7-3C-9B-CD-9E-72-8C-75-5E-C5-12-B0-79-E3-3D-9F-CF-9F-71-AD-30-36-4B-04-28-32-B9-5C-6E-9E-F3-4C-26-D3-12-4B-4C-A7-D3-96-B6-2D-04-82-65-45-60-17-58-22-40-79-65-0D-71-8C-7B-49-2C-81-D0-97-B4-AD-CB-B5-24-0A-8C-22-6C-8A-25-70-04-DB-B6-45-F8-07-3C-42-86-FA-59-1D-43-E3-F8-D6-24-2A-28-22-81-FE-53-5C-C2-0A-11-45-2B-E8-0B-2A-6E-73-32-A5-98-0B-87-90-E2-6E-62-52-6C-90-58-34-A3-40-98-90-DB-D5-66-D4-80-F3-45-C3-62-03-A3-ED-70-38-BC-C1-AB-0B-89-0B-1D-09-57-6D-00-53-16-EC-1C-24-6E-F1-3C-82-DD-37-08-5F-4A-5C-A8-DF-1F-92-1A-2B-1B-42-E3-1A-ED-D7-D3-3F-24-D5-20-E7-DC-39-9D-1B-73-26-F8-7E-2F-49-81-F2-DA-EF-F7-4F-78-AD-93-5A-38-3B-E7-CE-D5-F2-CF-CA-14-A9-8A-DE-9C-6C-C1-B0-73-E7-AB-69-42-6A-5E-24-2D-30-EC-DC-F9-1A-12-8F-92-16-50-0B-4F-6B-8A-F5-5D-D2-04-76-5C-37-09-20-3D-75-49-1B-28-C0-67-5C-1F-74-0E-0E-79-D9-63-8F-15-FC-00-17-02-EB-AB-1A-4B-B3-E7-00-00-00-00-49-45-4E-44-AE-42-60-82", + ["Cross"] = "89-50-4E-47-0D-0A-1A-0A-00-00-00-0D-49-48-44-52-00-00-00-20-00-00-00-20-08-06-00-00-00-73-7A-7A-F4-00-00-00-09-70-48-59-73-00-00-0B-13-00-00-0B-13-01-00-9A-9C-18-00-00-00-01-73-52-47-42-00-AE-CE-1C-E9-00-00-00-04-67-41-4D-41-00-00-B1-8F-0B-FC-61-05-00-00-00-C5-49-44-41-54-78-01-ED-96-D1-0D-83-30-0C-44-9D-4E-D0-51-BA-02-13-B5-23-A4-1B-A4-13-31-42-3B-4A-37-70-8D-6A-04-42-E0-D8-88-E0-1F-3F-29-8A-50-1C-DF-05-48-62-80-20-08-9C-49-D2-20-22-5E-A9-BB-53-1B-FA-67-4A-E9-0B-0A-66-F3-06-5E-DA-79-6B-89-32-4E-BC-39-71-55-9C-63-47-B2-14-7F-01-3D-37-6A-BD-64-82-C7-7A-8E-1D-A9-9A-06-29-E1-62-35-9B-6F-C2-12-7B-B8-89-66-E2-1A-81-E6-E2-0A-13-ED-C5-2B-26-CE-11-57-98-D8-25-6E-D9-86-FE-B8-7E-02-D7-9F-10-3D-B7-21-7A-1E-44-96-C4-4D-4C-D0-E4-62-49-B8-61-22-4B-1A-B5-6D-38-BF-C7-3F-D4-3A-E9-6E-E7-B1-8E-63-55-68-0A-92-07-3F-16-63-41-92-E1-BF-80-B2-BB-20-09-82-E0-0C-7E-54-36-6A-69-F6-3F-13-EF-00-00-00-00-49-45-4E-44-AE-42-60-82", + ["Plus"] = "89-50-4E-47-0D-0A-1A-0A-00-00-00-0D-49-48-44-52-00-00-00-20-00-00-00-20-08-06-00-00-00-73-7A-7A-F4-00-00-00-09-70-48-59-73-00-00-0B-13-00-00-0B-13-01-00-9A-9C-18-00-00-00-01-73-52-47-42-00-AE-CE-1C-E9-00-00-00-04-67-41-4D-41-00-00-B1-8F-0B-FC-61-05-00-00-00-A7-49-44-41-54-78-01-ED-D5-01-09-84-30-14-06-E0-7F-C7-05-B8-06-77-0D-CE-08-46-31-C2-1A-68-04-4D-A0-51-8C-A0-0D-B4-81-0D-E6-13-14-C6-10-C5-4D-11-E1-FF-60-F8-78-0C-F7-B3-E1-04-88-02-18-63-B4-8C-04-77-98-17-5F-64-F0-F4-82-BF-C8-AA-BF-F0-14-12-E0-14-0C-C0-00-0C-A0-D6-9A-72-B1-A4-F2-F8-61-5B-6C-CD-E9-64-D4-3B-F3-1B-A5-54-81-3D-D3-D5-6A-AE-A3-DD-F5-D6-8E-E0-83-EB-0C-6E-E3-ED-36-64-9B-72-49-3A-95-7F-6C-8B-71-EC-08-7A-79-77-85-B3-48-C8-CA-DA-DA-12-9E-F8-19-32-00-03-3C-3A-40-67-D5-2D-EE-30-FF-37-34-88-02-8C-19-03-9B-84-46-97-A8-ED-00-00-00-00-49-45-4E-44-AE-42-60-82", + ["Star"] = "89-50-4E-47-0D-0A-1A-0A-00-00-00-0D-49-48-44-52-00-00-00-20-00-00-00-20-08-06-00-00-00-73-7A-7A-F4-00-00-00-09-70-48-59-73-00-00-0B-13-00-00-0B-13-01-00-9A-9C-18-00-00-00-01-73-52-47-42-00-AE-CE-1C-E9-00-00-00-04-67-41-4D-41-00-00-B1-8F-0B-FC-61-05-00-00-01-16-49-44-41-54-78-01-ED-94-6D-0D-C2-30-10-86-DF-11-04-20-61-12-70-C0-1C-50-07-AB-03-90-80-04-50-00-28-19-0E-90-00-0E-C0-C1-71-CD-BA-B0-B1-86-B5-BD-0E-FE-EC-49-2E-6D-2E-D7-CB-F5-BE-80-89-09-01-44-94-1B-81-80-19-64-28-16-0D-01-73-C8-28-59-9E-F8-07-36-FD-0D-39-22-91-94-40-B5-EE-1A-91-48-02-58-B7-EE-2B-FC-92-8F-F4-37-2C-10-41-6C-06-0A-87-4E-23-82-DE-14-F0-4F-0A-3E-F2-81-77-1B-87-AE-E4-B7-43-13-71-CF-B2-EC-F2-D5-C2-A4-92-E5-44-E9-39-05-95-89-8D-B7-2C-0F-92-63-7C-6C-11-03-D5-CD-76-A3-78-AE-24-5C-D5-4D-20-3B-0A-67-8F-94-B0-43-E5-99-0D-63-53-60-0C-D8-71-E5-11-40-85-31-A0-7A-3A-7C-30-4D-E7-DD-ED-21-8B-48-79-DA-2D-02-6C-83-02-58-3B-74-07-96-B3-43-5F-22-25-8E-F4-77-66-9B-EF-9A-BA-3B-23-A8-0C-3E-01-E8-96-73-E7-6C-53-7F-67-68-A4-82-9D-1D-AD-D3-C1-D9-A6-F7-CE-38-22-15-F6-D7-45-80-BD-B2-6F-E4-65-60-27-4B-8A-58-A7-B6-24-E9-FA-60-62-62-2C-5E-30-1D-6B-34-83-5B-F0-2B-00-00-00-00-49-45-4E-44-AE-42-60-82", + ["Star Hollow"] = "89-50-4E-47-0D-0A-1A-0A-00-00-00-0D-49-48-44-52-00-00-00-20-00-00-00-20-08-06-00-00-00-73-7A-7A-F4-00-00-00-09-70-48-59-73-00-00-0B-13-00-00-0B-13-01-00-9A-9C-18-00-00-00-01-73-52-47-42-00-AE-CE-1C-E9-00-00-00-04-67-41-4D-41-00-00-B1-8F-0B-FC-61-05-00-00-01-5A-49-44-41-54-78-01-ED-96-FD-6D-C2-30-10-C5-2F-15-03-B0-41-33-42-47-48-37-C8-06-64-03-BA-41-D9-80-6E-00-1B-B4-9D-20-DD-20-EA-04-C9-06-65-83-EB-3B-F1-2C-8C-04-F9-B0-AD-F0-4F-7E-D2-29-16-3A-5F-EC-77-1F-41-64-61-21-02-55-CD-CD-24-82-27-89-A3-84-55-12-C1-4A-E2-D8-C0-4E-F2-08-28-BF-23-97-40-62-52-60-F2-77-72-56-A0-92-40-32-09-04-B7-AE-79-00-8B-F1-9C-65-D9-AB-CC-85-27-7F-09-2B-B8-5E-CB-5C-E0-65-15-EC-8F-EB-B5-AD-61-6F-12-C0-EA-46-F0-02-8F-7C-60-DF-16-F6-65-0B-48-7F-C2-9E-6F-2C-37-78-0E-75-44-07-FF-9F-5E-0F-DE-E8-A8-C3-94-FE-A1-47-F8-1F-27-A5-C9-24-A5-B4-6D-48-9B-B1-4E-9A-98-F4-B8-20-ED-D4-20-F0-DD-72-4F-A3-91-A3-DA-05-DC-51-C6-43-5F-40-A6-EF-40-DF-0F-49-09-5B-CE-D4-68-7B-7C-1A-FA-14-32-92-D1-93-10-D5-6B-55-DF-C1-7E-7B-DC-AC-0B-86-2B-3D-04-CA-6B-54-DE-6F-57-9F-63-AF-70-D3-0F-25-3D-0F-1F-75-2F-64-EB-B9-2E-A9-EE-1D-32-E5-01-3E-61-35-5F-B2-77-85-A6-97-99-F1-4E-3F-F3-A9-25-25-DE-CD-76-B7-7A-9B-EA-38-35-F6-C9-D3-E0-C9-AF-F7-7A-DB-9B-19-9A-3C-0D-53-7A-5B-BD-99-21-A9-E0-AD-8B-09-FE-25-F7-C4-A7-01-41-5E-34-FC-5B-30-DF-7F-84-85-85-50-FE-01-12-E7-01-A3-5F-51-F9-4C-00-00-00-00-49-45-4E-44-AE-42-60-82", + ["Collapse"] = "89-50-4E-47-0D-0A-1A-0A-00-00-00-0D-49-48-44-52-00-00-00-20-00-00-00-20-08-06-00-00-00-73-7A-7A-F4-00-00-00-09-70-48-59-73-00-00-0B-13-00-00-0B-13-01-00-9A-9C-18-00-00-00-01-73-52-47-42-00-AE-CE-1C-E9-00-00-00-04-67-41-4D-41-00-00-B1-8F-0B-FC-61-05-00-00-00-CE-49-44-41-54-78-01-ED-95-01-0D-C2-30-10-45-7F-A7-60-12-2A-01-09-93-80-04-1C-80-03-EA-60-16-70-30-1C-20-01-09-93-C0-1C-1C-BF-61-24-CD-18-0C-9A-DE-1A-92-BE-A4-49-D7-25-F7-7F-72-FF-5A-A0-90-19-33-3D-10-91-1D-F4-18-8C-31-E7-B7-7F-29-7E-10-7D-5C-A8-59-4D-3C-D4-D0-67-08-3F-E6-5A-B0-D5-34-C2-16-9C-50-48-05-DB-B5-F7-C1-45-0E-28-BC-19-53-7D-F3-7B-AC-09-05-2D-57-1F-8C-96-DF-5B-AC-05-C5-AE-33-F3-ED-CF-F4-C7-98-22-ED-87-4B-A6-85-26-14-38-CA-32-3A-A1-64-E1-46-BE-A7-41-4A-E4-35-74-4B-F8-C9-B0-48-01-0B-D5-3F-8A-3F-E9-25-45-28-59-A4-93-78-3A-68-C1-E2-2E-10-72-88-A4-42-66-8A-81-62-A0-18-C8-6E-20-1A-79-BC-0F-97-71-59-14-FE-95-3B-1B-9A-F9-A3-61-43-3F-A9-00-00-00-00-49-45-4E-44-AE-42-60-82", + ["Plus Thicker"] = "89-50-4E-47-0D-0A-1A-0A-00-00-00-0D-49-48-44-52-00-00-00-20-00-00-00-20-08-06-00-00-00-73-7A-7A-F4-00-00-00-09-70-48-59-73-00-00-0B-13-00-00-0B-13-01-00-9A-9C-18-00-00-00-01-73-52-47-42-00-AE-CE-1C-E9-00-00-00-04-67-41-4D-41-00-00-B1-8F-0B-FC-61-05-00-00-00-A1-49-44-41-54-78-01-ED-96-D1-09-84-30-10-44-27-C7-15-70-D7-81-25-D9-C1-9D-95-A8-1D-D9-89-25-C4-0E-4C-07-71-85-80-12-22-91-B8-E8-CF-3C-98-8F-2C-21-79-10-D8-0D-40-48-21-DE-FB-5A-32-4B-AC-E4-87-BB-91-4B-47-BF-61-51-C8-0B-E5-7C-A0-C0-15-01-15-28-40-01-0A-98-B8-10-BA-5A-87-3C-55-B4-9E-32-FB-9D-A4-37-C6-0C-39-01-9B-38-5C-0B-27-02-DF-7D-21-F5-04-2A-1D-EE-48-20-2E-BC-13-9B-1A-49-7B-42-A4-8A-D6-13-F2-F4-D0-22-4C-C1-47-87-91-0A-14-A0-00-05-B4-04-1C-EE-46-9A-CF-3F-34-A3-F5-6B-5E-83-90-42-16-B4-42-4C-CD-3F-8F-0E-C4-00-00-00-00-49-45-4E-44-AE-42-60-82", + ["Dropdown"] = "89-50-4E-47-0D-0A-1A-0A-00-00-00-0D-49-48-44-52-00-00-00-20-00-00-00-20-08-06-00-00-00-73-7A-7A-F4-00-00-00-09-70-48-59-73-00-00-0B-13-00-00-0B-13-01-00-9A-9C-18-00-00-00-01-73-52-47-42-00-AE-CE-1C-E9-00-00-00-04-67-41-4D-41-00-00-B1-8F-0B-FC-61-05-00-00-00-78-49-44-41-54-78-01-ED-D0-B1-0D-80-20-14-84-E1-D3-D2-15-98-82-41-18-C6-51-5C-85-41-98-C2-15-AC-F1-E8-2C-50-91-04-8C-F1-BE-E4-35-0A-FC-04-40-44-44-FE-6E-28-59-14-09-15-06-BA-5B-33-A2-CC-82-E7-6A-F6-9C-E3-23-F8-58-CE-A3-05-1E-1C-0A-E2-01-AD-F0-F0-89-B3-5E-C4-D3-BF-09-2D-31-60-38-5B-26-9E-BE-19-F4-C0-90-CD-5C-C0-A2-27-06-DD-21-EE-F0-06-86-E7-34-10-11-91-2F-DB-01-40-06-CB-42-92-53-E4-4B-00-00-00-00-49-45-4E-44-AE-42-60-82", + }; + } + + + + #endregion + + #region Other + + + public static void MarkInteractive(this Rect rect) + { + if (!curEvent.isRepaint) return; + + var unclippedRect = (Rect)_mi_GUIClip_UnclipToWindow.Invoke(null, new object[] { rect }); + + var curGuiView = _pi_GUIView_current.GetValue(null); + + _mi_GUIView_MarkHotRegion.Invoke(curGuiView, new object[] { unclippedRect }); + + } + + static PropertyInfo _pi_GUIView_current = typeof(Editor).Assembly.GetType("UnityEditor.GUIView").GetProperty("current", maxBindingFlags); + static MethodInfo _mi_GUIView_MarkHotRegion = typeof(Editor).Assembly.GetType("UnityEditor.GUIView").GetMethod("MarkHotRegion", maxBindingFlags); + static MethodInfo _mi_GUIClip_UnclipToWindow = typeof(GUI).Assembly.GetType("UnityEngine.GUIClip").GetMethod("UnclipToWindow", maxBindingFlags, null, new[] { typeof(Rect) }, null); + + + + + + + + + + #endregion + + } + +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchyLibs.cs.meta b/vfolders2/vHierarchy/VHierarchyLibs.cs.meta new file mode 100644 index 0000000..68cfb99 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyLibs.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: cd278e4ae9a8649f59f016c7d739ce1a +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyLightingWindow.cs b/vfolders2/vHierarchy/VHierarchyLightingWindow.cs new file mode 100644 index 0000000..8fb6b18 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyLightingWindow.cs @@ -0,0 +1,6 @@ + + +// this file was present in a previus version and is supposed to be deleted now +// but asset store update delivery system doesn't allow deleting files +// so instead this file is now emptied +// feel free to delete it if you want diff --git a/vfolders2/vHierarchy/VHierarchyLightingWindow.cs.meta b/vfolders2/vHierarchy/VHierarchyLightingWindow.cs.meta new file mode 100644 index 0000000..6a1a7b3 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyLightingWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 25324ddc1ebdc4ee5b1d9902a8efafa7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyMenu.cs b/vfolders2/vHierarchy/VHierarchyMenu.cs new file mode 100644 index 0000000..b04b012 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyMenu.cs @@ -0,0 +1,159 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using static VHierarchy.Libs.VUtils; +using static VHierarchy.Libs.VGUI; +// using static VTools.VDebug; + + +namespace VHierarchy +{ + class VHierarchyMenu + { + + public static bool navigationBarEnabled { get => EditorPrefsCached.GetBool("vHierarchy-navigationBarEnabled", false); set => EditorPrefsCached.SetBool("vHierarchy-navigationBarEnabled", value); } + public static bool sceneSelectorEnabled { get => EditorPrefsCached.GetBool("vHierarchy-sceneSelectorEnabled", false); set => EditorPrefsCached.SetBool("vHierarchy-sceneSelectorEnabled", value); } + public static bool componentMinimapEnabled { get => EditorPrefsCached.GetBool("vHierarchy-componentMinimapEnabled", false); set => EditorPrefsCached.SetBool("vHierarchy-componentMinimapEnabled", value); } + public static bool activationToggleEnabled { get => EditorPrefsCached.GetBool("vHierarchy-acctivationToggleEnabled", false); set => EditorPrefsCached.SetBool("vHierarchy-acctivationToggleEnabled", value); } + public static bool hierarchyLinesEnabled { get => EditorPrefsCached.GetBool("vHierarchy-hierarchyLinesEnabled", false); set => EditorPrefsCached.SetBool("vHierarchy-hierarchyLinesEnabled", value); } + public static bool minimalModeEnabled { get => EditorPrefsCached.GetBool("vHierarchy-minimalModeEnabled", false); set => EditorPrefsCached.SetBool("vHierarchy-minimalModeEnabled", value); } + public static bool zebraStripingEnabled { get => EditorPrefsCached.GetBool("vHierarchy-zebraStripingEnabled", false); set => EditorPrefsCached.SetBool("vHierarchy-zebraStripingEnabled", value); } + + public static bool toggleActiveEnabled { get => EditorPrefsCached.GetBool("vHierarchy-toggleActiveEnabled", true); set => EditorPrefsCached.SetBool("vHierarchy-toggleActiveEnabled", value); } + public static bool focusEnabled { get => EditorPrefsCached.GetBool("vHierarchy-focusEnabled", true); set => EditorPrefsCached.SetBool("vHierarchy-focusEnabled", value); } + public static bool deleteEnabled { get => EditorPrefsCached.GetBool("vHierarchy-deleteEnabled", true); set => EditorPrefsCached.SetBool("vHierarchy-deleteEnabled", value); } + public static bool toggleExpandedEnabled { get => EditorPrefsCached.GetBool("vHierarchy-toggleExpandedEnabled", true); set => EditorPrefsCached.SetBool("vHierarchy-toggleExpandedEnabled", value); } + public static bool isolateEnabled { get => EditorPrefsCached.GetBool("vHierarchy-collapseEverythingElseEnabled", true); set => EditorPrefsCached.SetBool("vHierarchy-collapseEverythingElseEnabled", value); } + public static bool collapseEverythingEnabled { get => EditorPrefsCached.GetBool("vHierarchy-collapseEverythingEnabled", true); set => EditorPrefsCached.SetBool("vHierarchy-collapseEverythingEnabled", value); } + public static bool setDefaultParentEnabled { get => EditorPrefsCached.GetBool("vHierarchy-setDefaultParentEnabled", true); set => EditorPrefsCached.SetBool("vHierarchy-setDefaultParentEnabled", value); } + + public static bool pluginDisabled { get => EditorPrefsCached.GetBool("vHierarchy-pluginDisabled", false); set => EditorPrefsCached.SetBool("vHierarchy-pluginDisabled", value); } + + + + + const string dir = "Tools/vHierarchy/"; + + const string navigationBar = dir + "Navigation bar"; + const string sceneSelector = dir + "Scene selector"; + const string componentMinimap = dir + "Component minimap"; + const string activationToggle = dir + "Activation toggle"; + const string hierarchyLines = dir + "Hierarchy lines"; + const string zebraStriping = dir + "Zebra striping"; + const string minimalMode = dir + "Minimal mode"; + + const string toggleActive = dir + "A to toggle active"; + const string focus = dir + "F to focus"; + const string delete = dir + "X to delete"; + const string toggleExpanded = dir + "E to expand or collapse"; + const string isolate = dir + "Shift-E to isolate"; + const string collapseEverything = dir + "Ctrl-Shift-E to collapse all"; + const string setDefaultParent = dir + "D to set default parent"; + + const string disablePlugin = dir + "Disable vHierarchy"; + + + + + + + [MenuItem(dir + "Features", false, 1)] static void daasddsas() { } + [MenuItem(dir + "Features", true, 1)] static bool dadsdasas123() => false; + + [MenuItem(navigationBar, false, 2)] static void dadsaadsdsadasdsadadsas() { navigationBarEnabled = !navigationBarEnabled; EditorApplication.RepaintHierarchyWindow(); } + [MenuItem(navigationBar, true, 2)] static bool dadsaddasdsasadadsdasadsas() { Menu.SetChecked(navigationBar, navigationBarEnabled); return !pluginDisabled; } + + [MenuItem(sceneSelector, false, 3)] static void dadsaadsdsadassddsadadsas() { sceneSelectorEnabled = !sceneSelectorEnabled; EditorApplication.RepaintHierarchyWindow(); } + [MenuItem(sceneSelector, true, 3)] static bool dadsaddasdsasadsdadsdasadsas() { Menu.SetChecked(sceneSelector, sceneSelectorEnabled); return !pluginDisabled; } + + [MenuItem(componentMinimap, false, 4)] static void daadsdsadasdadsas() { componentMinimapEnabled = !componentMinimapEnabled; EditorApplication.RepaintHierarchyWindow(); } + [MenuItem(componentMinimap, true, 4)] static bool dadsadasddasadsas() { Menu.SetChecked(componentMinimap, componentMinimapEnabled); return !pluginDisabled; } + + [MenuItem(activationToggle, false, 5)] static void daadsdsadadsasdadsas() { activationToggleEnabled = !activationToggleEnabled; EditorApplication.RepaintHierarchyWindow(); } + [MenuItem(activationToggle, true, 5)] static bool dadsadasdsaddasadsas() { Menu.SetChecked(activationToggle, activationToggleEnabled); return !pluginDisabled; } + + [MenuItem(hierarchyLines, false, 6)] static void dadsadadsadadasss() { hierarchyLinesEnabled = !hierarchyLinesEnabled; EditorApplication.RepaintHierarchyWindow(); } + [MenuItem(hierarchyLines, true, 6)] static bool dadsaddasdasaasddsas() { Menu.SetChecked(hierarchyLines, hierarchyLinesEnabled); return !pluginDisabled; } + + [MenuItem(zebraStriping, false, 7)] static void dadsadadadssadsadass() { zebraStripingEnabled = !zebraStripingEnabled; EditorApplication.RepaintHierarchyWindow(); } + [MenuItem(zebraStriping, true, 7)] static bool dadsaddadaadsssadsaasddsas() { Menu.SetChecked(zebraStriping, zebraStripingEnabled); return !pluginDisabled; } + + [MenuItem(minimalMode, false, 8)] static void dadsadadasdsdasadadasss() { minimalModeEnabled = !minimalModeEnabled; EditorApplication.RepaintHierarchyWindow(); } + [MenuItem(minimalMode, true, 8)] static bool dadsaddadsasdadsasaasddsas() { Menu.SetChecked(minimalMode, minimalModeEnabled); return !pluginDisabled; } + + + + + + + [MenuItem(dir + "Shortcuts", false, 101)] static void dadsas() { } + [MenuItem(dir + "Shortcuts", true, 101)] static bool dadsas123() => false; + + + + [MenuItem(setDefaultParent, false, 102)] static void dadsadasdsdasadasdsadadsas() => setDefaultParentEnabled = !setDefaultParentEnabled; + [MenuItem(setDefaultParent, true, 102)] static bool dadsadsdadssdaasadadsdasadsas() { Menu.SetChecked(setDefaultParent, setDefaultParentEnabled); return !pluginDisabled; } + + + [MenuItem(toggleActive, false, 103)] static void dadsadadsas() => toggleActiveEnabled = !toggleActiveEnabled; + [MenuItem(toggleActive, true, 103)] static bool dadsaddasadsas() { Menu.SetChecked(toggleActive, toggleActiveEnabled); return !pluginDisabled; } + + [MenuItem(focus, false, 104)] static void dadsadasdadsas() => focusEnabled = !focusEnabled; + [MenuItem(focus, true, 104)] static bool dadsadsaddasadsas() { Menu.SetChecked(focus, focusEnabled); return !pluginDisabled; } + + [MenuItem(delete, false, 105)] static void dadsadsadasdadsas() => deleteEnabled = !deleteEnabled; + [MenuItem(delete, true, 105)] static bool dadsaddsasaddasadsas() { Menu.SetChecked(delete, deleteEnabled); return !pluginDisabled; } + + [MenuItem(toggleExpanded, false, 106)] static void dadsadsadasdsadadsas() => toggleExpandedEnabled = !toggleExpandedEnabled; + [MenuItem(toggleExpanded, true, 106)] static bool dadsaddsasadadsdasadsas() { Menu.SetChecked(toggleExpanded, toggleExpandedEnabled); return !pluginDisabled; } + + [MenuItem(isolate, false, 107)] static void dadsadsasdadasdsadadsas() => isolateEnabled = !isolateEnabled; + [MenuItem(isolate, true, 107)] static bool dadsaddsdasasadadsdasadsas() { Menu.SetChecked(isolate, isolateEnabled); return !pluginDisabled; } + + [MenuItem(collapseEverything, false, 108)] static void dadsadsdasadasdsadadsas() => collapseEverythingEnabled = !collapseEverythingEnabled; + [MenuItem(collapseEverything, true, 108)] static bool dadsaddssdaasadadsdasadsas() { Menu.SetChecked(collapseEverything, collapseEverythingEnabled); return !pluginDisabled; } + + + + + [MenuItem(dir + "More", false, 1001)] static void daasadsddsas() { } + [MenuItem(dir + "More", true, 1001)] static bool dadsadsdasas123() => false; + + + [MenuItem(dir + "Open manual", false, 1002)] + static void dadadssadsas() => Application.OpenURL("https://kubacho-lab.gitbook.io/vhierarchy-2"); + + [MenuItem(dir + "Join our Discord", false, 1003)] + static void dadasdsas() => Application.OpenURL("https://discord.gg/pUektnZeJT"); + + + + + [MenuItem(dir + "Deals ending soon/Get vFolders 2 at 50% off", false, 1004)] + static void dadadssadasdsas() => Application.OpenURL("https://assetstore.unity.com/packages/slug/255470?aid=1100lGLBn&pubref=deal50menu"); + + [MenuItem(dir + "Deals ending soon/Get vInspector 2 at 50% off", false, 1005)] + static void dadadssasddsas() => Application.OpenURL("https://assetstore.unity.com/packages/slug/252297?aid=1100lGLBn&pubref=deal50menu"); + + [MenuItem(dir + "Deals ending soon/Get vTabs 2 at 50% off", false, 1006)] + static void dadadadsssadsas() => Application.OpenURL("https://assetstore.unity.com/packages/slug/253396?aid=1100lGLBn&pubref=deal50menu"); + + [MenuItem(dir + "Deals ending soon/Get vFavorites 2 at 50% off", false, 1007)] + static void dadadadsssadsadsas() => Application.OpenURL("https://assetstore.unity.com/packages/slug/263643?aid=1100lGLBn&pubref=deal50menu"); + + + + + + [MenuItem(disablePlugin, false, 10001)] static void dadsadsdasadasdasdsadadsas() { pluginDisabled = !pluginDisabled; UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); } + [MenuItem(disablePlugin, true, 10001)] static bool dadsaddssdaasadsadadsdasadsas() { Menu.SetChecked(disablePlugin, pluginDisabled); return true; } + + + + + + } +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchyMenu.cs.meta b/vfolders2/vHierarchy/VHierarchyMenu.cs.meta new file mode 100644 index 0000000..764244b --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyMenu.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 4d7f0448bdeda4aad9cfdd9e7c7ee27f +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyMenuItems.cs b/vfolders2/vHierarchy/VHierarchyMenuItems.cs new file mode 100644 index 0000000..8fb6b18 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyMenuItems.cs @@ -0,0 +1,6 @@ + + +// this file was present in a previus version and is supposed to be deleted now +// but asset store update delivery system doesn't allow deleting files +// so instead this file is now emptied +// feel free to delete it if you want diff --git a/vfolders2/vHierarchy/VHierarchyMenuItems.cs.meta b/vfolders2/vHierarchy/VHierarchyMenuItems.cs.meta new file mode 100644 index 0000000..70b2abb --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyMenuItems.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 9ddf1decb62f94768bcaec3173017c87 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyNavbar.cs b/vfolders2/vHierarchy/VHierarchyNavbar.cs new file mode 100644 index 0000000..af5370a --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyNavbar.cs @@ -0,0 +1,1081 @@ +#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.VHierarchy; + + + +namespace VHierarchy +{ + public class VHierarchyNavbar + { + + public void OnGUI(Rect navbarRect) + { + void updateState() + { + if (!curEvent.isLayout) return; + + + + var isTreeFocused = window.GetFieldValue("m_SceneHierarchy").GetMemberValue("m_TreeViewKeyboardControlID") == GUIUtility.keyboardControl; + + var isWindowFocused = window == EditorWindow.focusedWindow; + + + + if (!isTreeFocused && isSearchActive) + EditorGUI.FocusTextInControl("SearchFilter"); + + + if (isTreeFocused || !isWindowFocused) + if (window.GetMemberValue("m_SearchFilter").ToString().IsNullOrEmpty()) + isSearchActive = false; + + + // in vFolders the following is used to check if search is active: + // GUI.GetNameOfFocusedControl() == "SearchFilter"; + // but in hierarchy focused control changes erratically when multiple scene headers are visible + // so a bool state is used instead + + + + + this.defaultParent = typeof(SceneView).InvokeMethod("GetDefaultParentObjectIfSet")?.gameObject; + + } + + void background() + { + var backgroundColor = Greyscale(isDarkTheme ? .235f : .8f); + var lineColor = Greyscale(isDarkTheme ? .13f : .58f); + + navbarRect.Draw(backgroundColor); + + navbarRect.SetHeightFromBottom(1).MoveY(1).Draw(lineColor); + + } + void hiddenMenu() + { + if (!curEvent.holdingAlt) return; + if (!curEvent.isMouseUp) return; + if (curEvent.mouseButton != 1) return; + if (!navbarRect.IsHovered()) return; + + + void selectData() + { + Selection.activeObject = data; + } + void selectPalette() + { + Selection.activeObject = palette; + } + void clearCache() + { + VHierarchyCache.Clear(); + } + + + + GenericMenu menu = new(); + + menu.AddDisabledItem(new GUIContent("vHierarchy hidden menu")); + + menu.AddSeparator(""); + menu.AddItem(new GUIContent("Select data"), false, selectData); + menu.AddItem(new GUIContent("Select palette"), false, selectPalette); + + menu.AddSeparator(""); + menu.AddItem(new GUIContent("Clear cache"), false, clearCache); + + menu.ShowAsContext(); + + } + + + void plusButton() + { + + var buttonRect = navbarRect.SetWidth(28).MoveX(4.5f); + + if (Application.unityVersion.StartsWith("6000")) + buttonRect = buttonRect.MoveY(-.49f); + + + var iconName = "Plus Thicker"; + var iconSize = 16; + var colorNormal = Greyscale(isDarkTheme ? .7f : .44f); + var colorHovered = Greyscale(isDarkTheme ? 1f : .42f); + var colorPressed = Greyscale(isDarkTheme ? .75f : .6f); + + if (!IconButton(buttonRect, iconName, iconSize, colorNormal, colorHovered, colorPressed)) return; + + + GUIUtility.hotControl = 0; + + var sceneHierarchy = window.GetMemberValue("m_SceneHierarchy"); + var m_CustomParentForNewGameObjects = window.GetMemberValue("m_SceneHierarchy").GetMemberValue("m_CustomParentForNewGameObjects"); + var targetSceneHandle = m_CustomParentForNewGameObjects != null ? m_CustomParentForNewGameObjects.gameObject.scene.handle : 0; + + + var menu = new GenericMenu(); + + sceneHierarchy.GetType().GetMethod("AddCreateGameObjectItemsToMenu", maxBindingFlags).Invoke(sceneHierarchy, new object[] { menu, null, true, true, false, targetSceneHandle, 3 }); + + typeof(UnityEditor.SceneManagement.SceneHierarchyHooks).InvokeMethod("AddCustomItemsToCreateMenu", menu); + + menu.DropDown(buttonRect); + + + } + + void searchButton() + { + if (searchAnimationT == 1) return; + + + var buttonRect = navbarRect.SetWidthFromRight(28).MoveX(-5); + + var iconName = "Search_"; + var iconSize = 16; + var colorNormal = Greyscale(isDarkTheme ? .75f : .2f); + var colorHovered = Greyscale(isDarkTheme ? 1f : .2f); + var colorPressed = Greyscale(isDarkTheme ? .75f : .5f); + + + if (!IconButton(buttonRect, iconName, iconSize, colorNormal, colorHovered, colorPressed)) return; + + EditorGUI.FocusTextInControl("SearchFilter"); + + EditorApplication.delayCall += () => EditorGUI.FocusTextInControl("SearchFilter"); + + isSearchActive = true; + + } + void searchOnCtrlF() + { + if (searchAnimationT == 1) return; + + if (!curEvent.isKeyDown) return; + if (!curEvent.holdingCmd && !curEvent.holdingCtrl) return; + if (curEvent.keyCode != KeyCode.F) return; + + + EditorGUI.FocusTextInControl("SearchFilter"); + + EditorApplication.delayCall += () => EditorGUI.FocusTextInControl("SearchFilter"); + + isSearchActive = true; + + + curEvent.Use(); + + } + void collapseAllButton() + { + if (searchAnimationT == 1) return; + + + var buttonRect = navbarRect.SetWidthFromRight(28).MoveX(-33); + + var iconName = "Collapse"; + var iconSize = 16; + var colorNormal = Greyscale(isDarkTheme ? .71f : .44f); + var colorHovered = Greyscale(isDarkTheme ? 1f : .42f); + var colorPressed = Greyscale(isDarkTheme ? .75f : .6f); + + + if (!IconButton(buttonRect, iconName, iconSize, colorNormal, colorHovered, colorPressed)) return; + + controller.CollapseAll(); + + } + void bookmarks() + { + if (searchAnimationT == 1) return; + if (isSearchActive && !curEvent.isRepaint) return; + + void createData() + { + if (data) return; + if (!navbarRect.IsHovered()) return; + if (!DragAndDrop.objectReferences.Any()) return; + + data = ScriptableObject.CreateInstance(); + + AssetDatabase.CreateAsset(data, GetScriptPath("VHierarchy").GetParentPath().CombinePath("vHierarchy Data.asset")); + + } + void divider() + { + if (!data) return; + if (!data.bookmarks.Any(r => r.go)) return; + + + var dividerRect = navbarRect.SetWidthFromRight(1).SetHeightFromMid(16).MoveX(-65).MoveX(1.5f); + + var dividerColor = Greyscale(isDarkTheme ? .33f : .64f); + + + dividerRect.Draw(dividerColor); + + } + void gui() + { + if (!data) return; + + this.navbarRect = navbarRect; + this.bookmarksRect = navbarRect.AddWidth(-69).AddWidthFromRight(-60).MoveX(2).MoveX(-3); + + BookmarksGUI(); + + } + + createData(); + divider(); + gui(); + + } + + void searchField() + { + if (searchAnimationT == 0) return; + + var searchFieldRect = navbarRect.SetHeightFromMid(20).AddWidth(-33).SetWidthFromRight(200f.Min(window.position.width - 120)).Move(-1, 2); + + + GUILayout.BeginArea(searchFieldRect); + GUILayout.BeginHorizontal(); + + Space(2); + window.InvokeMethod("SearchFieldGUI"); + + GUILayout.EndHorizontal(); + GUILayout.EndArea(); + + } + void closeSearchButton() + { + if (searchAnimationT == 0) return; + + + var buttonRect = navbarRect.SetWidthFromRight(30).MoveX(-4); + + var iconName = "CrossIcon"; + var iconSize = 15; + var colorNormal = Greyscale(isDarkTheme ? .9f : .2f); + var colorHovered = Greyscale(isDarkTheme ? 1f : .2f); + var colorPressed = Greyscale(isDarkTheme ? .75f : .5f); + + + if (!IconButton(buttonRect, iconName, iconSize, colorNormal, colorHovered, colorPressed)) return; + + window.InvokeMethod("ClearSearchFilter"); + + GUIUtility.keyboardControl = 0; + + isSearchActive = false; + + } + void closeSearchOnEsc() + { + if (!isSearchActive) return; + if (curEvent.keyCode != KeyCode.Escape) return; + + window.InvokeMethod("ClearSearchFilter"); + + GUIUtility.keyboardControl = 0; + + isSearchActive = false; + + } + + void searchAnimation() + { + if (!curEvent.isLayout) return; + + + var lerpSpeed = 8f; + + if (isSearchActive) + MathUtil.SmoothDamp(ref searchAnimationT, 1, lerpSpeed, ref searchAnimationDerivative, editorDeltaTime); + else + MathUtil.SmoothDamp(ref searchAnimationT, 0, lerpSpeed, ref searchAnimationDerivative, editorDeltaTime); + + + if (isSearchActive && searchAnimationT > .99f) + searchAnimationT = 1; + + if (!isSearchActive && searchAnimationT < .01f) + searchAnimationT = 0; + + + animatingSearch = searchAnimationT != 0 && searchAnimationT != 1; + + } + + void buttonsAndBookmarks() + { + SetGUIColor(Greyscale(1, (1 - searchAnimationT).Pow(2))); + GUI.BeginGroup(window.position.SetPos(0, 0).MoveX(-searchAnimationDistance * searchAnimationT)); + + searchButton(); + searchOnCtrlF(); + collapseAllButton(); + bookmarks(); + + GUI.EndGroup(); + ResetGUIColor(); + + } + void search() + { + SetGUIColor(Greyscale(1, searchAnimationT.Pow(2))); + GUI.BeginGroup(window.position.SetPos(0, 0).MoveX(searchAnimationDistance * (1 - searchAnimationT))); + + searchField(); + closeSearchButton(); + closeSearchOnEsc(); + + GUI.EndGroup(); + ResetGUIColor(); + + } + + + + updateState(); + + background(); + hiddenMenu(); + + plusButton(); + + searchAnimation(); + buttonsAndBookmarks(); + search(); + + + + if (draggingBookmark || animatingDroppedBookmark || animatingGaps || animatingTooltip || animatingSearch) + window.Repaint(); + + } + + bool animatingSearch; + + float searchAnimationDistance = 90; + float searchAnimationT; + float searchAnimationDerivative; + + string openedFolderPath; + + public bool isSearchActive; + + bool isDefaultParentTextPressed; + + GameObject defaultParent; + + GUIStyle defaultParentTextGUIStyle; + + Rect navbarRect; + Rect bookmarksRect; + + + + + + + + + + + + + void BookmarksGUI() + { + void bookmark(Vector2 centerPosition, Bookmark bookmark) + { + if (bookmark == null) return; + if (curEvent.isLayout) return; + + + var bookmarkRect = Rect.zero.SetSize(bookmarkWidth, bookmarksRect.height).SetMidPos(centerPosition); + + + void shadow() + { + if (!draggingBookmark) return; + if (draggedBookmark != bookmark) return; + + bookmarkRect.SetSizeFromMid(bookmarkWidth - 4, bookmarkWidth - 4).DrawBlurred(Greyscale(0, .3f), 15); + + } + void background() + { + if (!bookmarkRect.IsHovered()) return; + if (draggingBookmark && draggedBookmark != bookmark) return; + + var backgroundColor = Greyscale(isDarkTheme ? .35f : .7f); + + var backgroundRect = bookmarkRect.SetSizeFromMid(bookmarkRect.width - 2, bookmarkWidth - 2); + + backgroundRect.DrawRounded(backgroundColor, 4); + + + } + void icon() + { + var opacity = 1f; + var iconTexture = default(Texture); + + void set_opacity() + { + var opacityNormal = .9f; + var opacityHovered = 1f; + var opacityPressed = .75f; + var opacityDragged = .75f; + var opacityDisabled = .4f; + + var isDisabled = !bookmark.isLoadable; + + + opacity = opacityNormal; + + if (draggingBookmark) + opacity = bookmark == draggedBookmark ? opacityDragged : opacityNormal; + + else if (bookmark == pressedBookmark) + opacity = opacityPressed; + + else if (bookmarkRect.IsHovered()) + opacity = opacityHovered; + + if (isDisabled) + opacity = opacityDisabled; + + } + void getTexture() + { + var iconName = ""; + + if (bookmark.go) + if (VHierarchy.GetGameObjectData(bookmark.go, createDataIfDoesntExist: false) is GameObjectData goData && !goData.iconNameOrGuid.IsNullOrEmpty()) + iconName = goData.iconNameOrGuid.Length == 32 ? goData.iconNameOrGuid.ToPath() : goData.iconNameOrGuid; + else + iconName = AssetPreview.GetMiniThumbnail(bookmark.go).name; + + if (iconName.IsNullOrEmpty()) + iconName = "GameObject icon"; + + + iconTexture = EditorIcons.GetIcon(iconName); + + } + void drawTexture() + { + if (!iconTexture) return; + + + SetGUIColor(Greyscale(1, opacity)); + + GUI.DrawTexture(bookmarkRect.SetSizeFromMid(iconSize), iconTexture); + + ResetGUIColor(); + + } + + + set_opacity(); + getTexture(); + drawTexture(); + + } + void tooltip() + { + if (bookmark != (draggingBookmark ? (draggedBookmark) : (lastHoveredBookmark))) return; + if (tooltipOpacity == 0) return; + + var fontSize = 11; + var tooltipText = bookmark.name; + + Rect tooltipRect; + + void set_tooltipRect() + { + var width = tooltipText.GetLabelWidth(fontSize) + 6; + var height = 16 + (fontSize - 12) * 2; + + var yOffset = 28; + var rightMargin = -1; + + + tooltipRect = Rect.zero.SetMidPos(centerPosition.x, centerPosition.y + yOffset).SetSizeFromMid(width, height); + + + var maxXMax = navbarRect.xMax - rightMargin; + + if (tooltipRect.xMax > maxXMax) + tooltipRect = tooltipRect.MoveX(maxXMax - tooltipRect.xMax); + + } + void shadow() + { + var shadowAmount = .33f; + var shadowRadius = 10; + + tooltipRect.DrawBlurred(Greyscale(0, shadowAmount).MultiplyAlpha(tooltipOpacity), shadowRadius); + + } + void background() + { + var cornerRadius = 5; + + var backgroundColor = Greyscale(isDarkTheme ? .13f : .9f); + var outerEdgeColor = Greyscale(isDarkTheme ? .25f : .6f); + var innerEdgeColor = Greyscale(isDarkTheme ? .0f : .95f); + + tooltipRect.Resize(-1).DrawRounded(outerEdgeColor.SetAlpha(tooltipOpacity.Pow(2)), cornerRadius + 1); + tooltipRect.Resize(0).DrawRounded(innerEdgeColor.SetAlpha(tooltipOpacity.Pow(2)), cornerRadius + 0); + tooltipRect.Resize(1).DrawRounded(backgroundColor.SetAlpha(tooltipOpacity), cornerRadius - 1); + + } + void text() + { + var textRect = tooltipRect.MoveY(-.5f); + + var textColor = Greyscale(1f); + + SetLabelAlignmentCenter(); + SetLabelFontSize(fontSize); + SetGUIColor(textColor.SetAlpha(tooltipOpacity)); + + GUI.Label(textRect, tooltipText); + + ResetLabelStyle(); + ResetGUIColor(); + + } + + set_tooltipRect(); + shadow(); + background(); + text(); + + } + void click() + { + if (!bookmarkRect.IsHovered()) return; + if (!curEvent.isMouseUp) return; + + curEvent.Use(); + + + if (draggingBookmark) return; + if ((curEvent.mousePosition - mouseDownPosiion).magnitude > 2) return; + if (!bookmark.isLoadable) return; + + controller.RevealObject(bookmark.go, expand: true, highlight: true, snapToTopMargin: true); + + lastClickedBookmark = bookmark; + + hideTooltip = true; + + + + if (curEvent.mouseButton == 2 && VHierarchyMenu.setDefaultParentEnabled) + EditorUtility.SetDefaultParentObject(bookmark.go); + + } + + + bookmarkRect.MarkInteractive(); + + shadow(); + background(); + icon(); + tooltip(); + click(); + + } + + void normalBookmark(int i, float centerX) + { + if (data.bookmarks[i] == droppedBookmark && animatingDroppedBookmark) return; + + var centerY = bookmarksRect.height / 2; + + + var minX = centerX - bookmarkWidth / 2; + + if (minX < bookmarksRect.x) return; + + lastBookmarkX = minX; + + + bookmark(new Vector2(centerX, centerY), data.bookmarks[i]); + + } + void normalBookmarks() + { + var curCenterX = bookmarksRect.xMax - bookmarkWidth / 2; + + for (int i = 0; i < data.bookmarks.Count; i++) + { + curCenterX -= gaps[i]; + + if (!data.bookmarks[i].go) continue; + + + normalBookmark(i, curCenterX); + + + curCenterX -= bookmarkWidth; + + } + + } + void draggedBookmark_() + { + if (!draggingBookmark) return; + + var centerX = curEvent.mousePosition.x + draggedBookmarkHoldOffset.x; + var centerY = bookmarksRect.IsHovered() ? bookmarksRect.height / 2 : curEvent.mousePosition.y; + + bookmark(new Vector2(centerX, centerY), draggedBookmark); + + } + void droppedBookmark_() + { + if (!animatingDroppedBookmark) return; + + var centerX = droppedBookmarkX; + var centerY = bookmarksRect.height / 2; + + bookmark(new Vector2(centerX, centerY), droppedBookmark); + + } + + + BookmarksMouseState(); + BookmarksDragging(); + BookmarksAnimations(); + + normalBookmarks(); + draggedBookmark_(); + droppedBookmark_(); + + } + + float bookmarkWidth => 24; + float iconSize => 16; + + float lastBookmarkX; + + + + int GetBookmarkIndex(float mouseX) + { + var curBookmarkWidthSum = 0f; + + for (int i = 0; i < data.bookmarks.Count; i++) + { + if (!data.bookmarks[i].go) continue; + + curBookmarkWidthSum += bookmarkWidth; + + if (bookmarksRect.xMax - curBookmarkWidthSum < mouseX + .5f) + return i; + } + + return data.bookmarks.IndexOfLast(r => r.go) + 1; + + } + + float GetBookmarkCenterX(int i, bool includeGaps = true) + { + return bookmarksRect.xMax + - bookmarkWidth / 2 + - data.bookmarks.Take(i).Sum(r => r.go ? bookmarkWidth : 0) + - (includeGaps ? gaps.Take(i + 1).Sum() : 0); + } + + + + + + + + void BookmarksMouseState() + { + void down() + { + if (!curEvent.isMouseDown) return; + if (!bookmarksRect.IsHovered()) return; + + mousePressed = true; + + mouseDownPosiion = curEvent.mousePosition; + + var pressedBookmarkIndex = GetBookmarkIndex(mouseDownPosiion.x); + + if (pressedBookmarkIndex.IsInRangeOf(data.bookmarks)) + pressedBookmark = data.bookmarks[pressedBookmarkIndex]; + + doubleclickUnhandled = curEvent.clickCount == 2; + + curEvent.Use(); + + } + void up() + { + if (!curEvent.isMouseUp) return; + + mousePressed = false; + pressedBookmark = null; + + } + void hover() + { + var hoveredBookmarkIndex = GetBookmarkIndex(curEvent.mousePosition.x); + + mouseHoversBookmark = bookmarksRect.IsHovered() && hoveredBookmarkIndex.IsInRangeOf(data.bookmarks); + + if (mouseHoversBookmark) + lastHoveredBookmark = data.bookmarks[hoveredBookmarkIndex]; + + + } + + down(); + up(); + hover(); + + } + + bool mouseHoversBookmark; + bool mousePressed; + bool doubleclickUnhandled; + + Vector2 mouseDownPosiion; + + Bookmark pressedBookmark; + Bookmark lastHoveredBookmark; + + + + + + + void BookmarksDragging() + { + void initFromOutside() + { + if (draggingBookmark) return; + if (!bookmarksRect.IsHovered()) return; + if (!curEvent.isDragUpdate) return; + if (DragAndDrop.objectReferences.FirstOrDefault() is not GameObject draggedGo) return; + + animatingDroppedBookmark = false; + + draggingBookmark = true; + draggingBookmarkFromInside = false; + + draggedBookmark = new Bookmark(draggedGo); + draggedBookmarkHoldOffset = Vector2.zero; + + } + void initFromInside() + { + if (draggingBookmark) return; + if (!mousePressed) return; + if ((curEvent.mousePosition - mouseDownPosiion).magnitude <= 2) return; + if (pressedBookmark == null) return; + + var i = GetBookmarkIndex(mouseDownPosiion.x); + + if (i >= data.bookmarks.Count) return; + if (i < 0) return; + + + animatingDroppedBookmark = false; + + draggingBookmark = true; + draggingBookmarkFromInside = true; + + draggedBookmark = data.bookmarks[i]; + draggedBookmarkHoldOffset = new Vector2(GetBookmarkCenterX(i) - mouseDownPosiion.x, bookmarksRect.center.y - mouseDownPosiion.y); + + gaps[i] = bookmarkWidth; + + + data.RecordUndo(); + + data.bookmarks.Remove(draggedBookmark); + + } + + void acceptFromOutside() + { + if (!draggingBookmark) return; + if (!curEvent.isDragPerform) return; + if (!bookmarksRect.IsHovered()) return; + + DragAndDrop.AcceptDrag(); + curEvent.Use(); + + data.RecordUndo(); + + accept(); + + data.Dirty(); + + } + void acceptFromInside() + { + if (!draggingBookmark) return; + if (!curEvent.isMouseUp) return; + if (!bookmarksRect.IsHovered()) return; + + curEvent.Use(); + EditorGUIUtility.hotControl = 0; + + DragAndDrop.PrepareStartDrag(); // fixes phantom dragged component indicator after reordering bookmarks + + data.RecordUndo(); + data.Dirty(); + + accept(); + + } + void accept() + { + draggingBookmark = false; + draggingBookmarkFromInside = false; + mousePressed = false; + + data.bookmarks.AddAt(draggedBookmark, insertDraggedBookmarkAtIndex); + + gaps[insertDraggedBookmarkAtIndex] -= bookmarkWidth; + gaps.AddAt(0, insertDraggedBookmarkAtIndex); + + droppedBookmark = draggedBookmark; + + droppedBookmarkX = curEvent.mousePosition.x + draggedBookmarkHoldOffset.x; + droppedBookmarkXDerivative = 0; + animatingDroppedBookmark = true; + + draggedBookmark = null; + + EditorGUIUtility.hotControl = 0; + + } + + void cancelFromOutside() + { + if (!draggingBookmark) return; + if (draggingBookmarkFromInside) return; + if (bookmarksRect.IsHovered()) return; + + draggingBookmark = false; + mousePressed = false; + + } + void cancelFromInsideAndDelete() + { + if (!draggingBookmark) return; + if (!curEvent.isMouseUp) return; + if (bookmarksRect.IsHovered()) return; + + draggingBookmark = false; + + DragAndDrop.PrepareStartDrag(); // fixes phantom dragged component indicator after reordering bookmarks + + data.Dirty(); + + } + + void update() + { + if (!draggingBookmark) return; + + DragAndDrop.visualMode = DragAndDropVisualMode.Generic; + + if (draggingBookmarkFromInside) // otherwise it breaks vTabs dragndrop + EditorGUIUtility.hotControl = EditorGUIUtility.GetControlID(FocusType.Passive); + + + + insertDraggedBookmarkAtIndex = GetBookmarkIndex(curEvent.mousePosition.x + draggedBookmarkHoldOffset.x); + + } + + + initFromOutside(); + initFromInside(); + + acceptFromOutside(); + acceptFromInside(); + + cancelFromOutside(); + cancelFromInsideAndDelete(); + + update(); + + + } + + bool draggingBookmark; + bool draggingBookmarkFromInside; + + int insertDraggedBookmarkAtIndex; + + Vector2 draggedBookmarkHoldOffset; + + Bookmark draggedBookmark; + Bookmark droppedBookmark; + + + + + + + void BookmarksAnimations() + { + if (!curEvent.isLayout) return; + + void gaps_() + { + var makeSpaceForDraggedBookmark = draggingBookmark && bookmarksRect.IsHovered(); + + var lerpSpeed = 12; + + for (int i = 0; i < gaps.Count; i++) + if (makeSpaceForDraggedBookmark && i == insertDraggedBookmarkAtIndex) + gaps[i] = MathUtil.Lerp(gaps[i], bookmarkWidth, lerpSpeed, editorDeltaTime); + else + gaps[i] = MathUtil.Lerp(gaps[i], 0, lerpSpeed, editorDeltaTime); + + + + for (int i = 0; i < gaps.Count; i++) + if (gaps[i].Approx(0)) + gaps[i] = 0; + + + + animatingGaps = gaps.Any(r => r > .1f); + + + } + void droppedBookmark_() + { + if (!animatingDroppedBookmark) return; + + var lerpSpeed = 8; + + var targX = GetBookmarkCenterX(data.bookmarks.IndexOf(droppedBookmark), includeGaps: false); + + MathUtil.SmoothDamp(ref droppedBookmarkX, targX, lerpSpeed, ref droppedBookmarkXDerivative, editorDeltaTime); + + if ((droppedBookmarkX - targX).Abs() < .5f) + animatingDroppedBookmark = false; + + } + void tooltip() + { + if (!mouseHoversBookmark || lastHoveredBookmark != lastClickedBookmark) + hideTooltip = false; + + + var lerpSpeed = UnityEditorInternal.InternalEditorUtility.isApplicationActive ? 15 : 12321; + + if (mouseHoversBookmark && !draggingBookmark && !hideTooltip) + MathUtil.SmoothDamp(ref tooltipOpacity, 1, lerpSpeed, ref tooltipOpacityDerivative, editorDeltaTime); + else + MathUtil.SmoothDamp(ref tooltipOpacity, 0, lerpSpeed, ref tooltipOpacityDerivative, editorDeltaTime); + + + if (tooltipOpacity > .99f) + tooltipOpacity = 1; + + if (tooltipOpacity < .01f) + tooltipOpacity = 0; + + + animatingTooltip = tooltipOpacity != 0 && tooltipOpacity != 1; + + } + + gaps_(); + droppedBookmark_(); + tooltip(); + + } + + float droppedBookmarkX; + float droppedBookmarkXDerivative; + + float tooltipOpacity; + float tooltipOpacityDerivative; + + bool animatingDroppedBookmark; + bool animatingGaps; + bool animatingTooltip; + bool animatingBookmarks => animatingDroppedBookmark || animatingGaps; + + bool hideTooltip; + + List gaps + { + get + { + while (_gaps.Count < data.bookmarks.Count + 1) _gaps.Add(0); + while (_gaps.Count > data.bookmarks.Count + 1) _gaps.RemoveLast(); + + return _gaps; + + } + } + List _gaps = new(); + + Bookmark lastClickedBookmark; + + + + + + + + + + + + + + public VHierarchyNavbar(EditorWindow window) => this.window = window; + + public EditorWindow window; + + public VHierarchyController controller => VHierarchy.controllers_byWindow[window]; + + + } +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchyNavbar.cs.meta b/vfolders2/vHierarchy/VHierarchyNavbar.cs.meta new file mode 100644 index 0000000..611fae8 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyNavbar.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: b0951d821533c4f9486bb1430540ef3b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyPalette.cs b/vfolders2/vHierarchy/VHierarchyPalette.cs new file mode 100644 index 0000000..9c2395e --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyPalette.cs @@ -0,0 +1,221 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditor.ShortcutManagement; +using System.Reflection; +using System.Linq; +using UnityEngine.UIElements; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using UnityEditorInternal; +using static VHierarchy.Libs.VUtils; +using static VHierarchy.Libs.VGUI; +// using static VTools.VDebug; + + +namespace VHierarchy +{ + public class VHierarchyPalette : ScriptableObject + { + public List colors = new(); + + public bool colorsEnabled; + + public float colorSaturation = 1; + public float colorBrightness = 1; + public bool colorGradientsEnabled = true; + + public void ResetColors() + { + colors.Clear(); + + for (int i = 0; i < colorsCount; i++) + colors.Add(GetDefaultColor(i)); + + colorsEnabled = true; + + this.Dirty(); + + } + + public static Color GetDefaultColor(int colorIndex) + { + Color color = default; + + void grey() + { + if (colorIndex >= greyColorsCount) return; + +#if UNITY_2022_1_OR_NEWER + color = Greyscale(isDarkTheme ? .16f : .9f); +#else + color = Greyscale(isDarkTheme ? .315f : .9f); +#endif + + } + void rainbowDarkTheme() + { + if (colorIndex < greyColorsCount) return; + if (!isDarkTheme) return; + + color = ColorUtils.HSLToRGB((colorIndex - greyColorsCount.ToFloat()) / rainbowColorsCount, .45f, .35f); + + if (colorIndex == 1) + color *= 1.2f; + + if (colorIndex == 2) + color *= 1.1f; + + if (colorIndex == 6) + color *= 1.35f; + + if (colorIndex == 7) + color *= 1.3f; + + if (colorIndex == 8) + color *= 1.05f; + + + color.a = .1f; + + } + void rainbowLightTheme() + { + if (colorIndex < greyColorsCount) return; + if (isDarkTheme) return; + + color = ColorUtils.HSLToRGB((colorIndex - greyColorsCount.ToFloat()) / rainbowColorsCount, .62f, .8f); + + color.a = .1f; + + } + + grey(); + rainbowDarkTheme(); + rainbowLightTheme(); + + return color; + + } + + public static int greyColorsCount = 1; + public static int rainbowColorsCount = 8; + public static int colorsCount => greyColorsCount + rainbowColorsCount; + + + + + public List iconRows = new(); + + [System.Serializable] + public class IconRow + { + public List builtinIcons = new(); // names + public List customIcons = new(); // names or guids + + public bool enabled = true; + + public bool isCustom => !builtinIcons.Any() || customIcons.Any(); + public bool isEmpty => !builtinIcons.Any() && !customIcons.Any(); + public int iconCount => builtinIcons.Count + customIcons.Count; + + public IconRow(string[] builtinIcons) => this.builtinIcons = builtinIcons.ToList(); + public IconRow() { } + + } + + public void ResetIcons() + { + iconRows.Clear(); + + iconRows.Add(new IconRow(new[] + { + "Folder Icon", + "Canvas Icon", + "AvatarMask On Icon", + "cs Script Icon", + "StandaloneInputModule Icon", + "EventSystem Icon", + "Terrain Icon", + "ScriptableObject Icon", + + })); + iconRows.Add(new IconRow(new[] + { + "Camera Icon", + "ParticleSystem Icon", + "TrailRenderer Icon", + "Material Icon", + "ReflectionProbe Icon", + + })); + iconRows.Add(new IconRow(new[] + { + "Light Icon", + "DirectionalLight Icon", + "LightmapParameters Icon", + "LightProbes Icon", + + })); + iconRows.Add(new IconRow(new[] + { + "Rigidbody Icon", + "BoxCollider Icon", + "SphereCollider Icon", + "CapsuleCollider Icon", + "WheelCollider Icon", + "MeshCollider Icon", + + })); + iconRows.Add(new IconRow(new[] + { + "AudioSource Icon", + "AudioClip Icon", + "AudioListener Icon", + "AudioEchoFilter Icon", + "AudioReverbZone Icon", + + })); + iconRows.Add(new IconRow(new[] + { + "PreMatCube", + "PreMatSphere", + "PreMatCylinder", + "PreMatQuad", + "Favorite", + #if UNITY_2021_3_OR_NEWER + "Settings Icon", + #endif + + })); + + this.Dirty(); + + } + + + + + [ContextMenu("Export palette")] + public void Export() + { + var packagePath = EditorUtility.SaveFilePanel("Export vHierarchy Palette", "", this.GetPath().GetFilename(withExtension: false), "unitypackage"); + + var iconPaths = iconRows.SelectMany(r => r.customIcons).Select(r => r.ToPath()).Where(r => !r.IsNullOrEmpty()); + + AssetDatabase.ExportPackage(iconPaths.Append(this.GetPath()).ToArray(), packagePath); + + EditorUtility.RevealInFinder(packagePath); + + } + + + + + void Reset() { ResetColors(); ResetIcons(); } + + } +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchyPalette.cs.meta b/vfolders2/vHierarchy/VHierarchyPalette.cs.meta new file mode 100644 index 0000000..bec7d1c --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyPalette.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 61290d425f1c94a8cbfb56f754ca6757 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyPaletteEditor.cs b/vfolders2/vHierarchy/VHierarchyPaletteEditor.cs new file mode 100644 index 0000000..4add5a8 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyPaletteEditor.cs @@ -0,0 +1,1374 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditor.ShortcutManagement; +using System.Reflection; +using System.Linq; +using UnityEditorInternal; +using UnityEngine.UIElements; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using UnityEditor.IMGUI.Controls; +using static VHierarchy.Libs.VUtils; +using static VHierarchy.Libs.VGUI; +// using static VTools.VDebug; +using static VHierarchy.VHierarchyPalette; + + +namespace VHierarchy +{ + [CustomEditor(typeof(VHierarchyPalette))] + class VHierarchyPaletteEditor : Editor + { + + public override void OnInspectorGUI() + { + void colors() + { + var rowRect = ExpandWidthLabelRect(cellSize).SetX(rowsOffsetX).SetWidth(rowWidth + 16); + + void backgroundHovered() + { + if (!rowRect.IsHovered()) return; + if (pickingColor) return; + if (draggingRow) return; + + rowRect.Draw(hoveredRowBackground); + + } + void toggle() + { + var toggleRect = rowRect.SetWidth(16).MoveX(5); + + var prevEnabled = palette.colorsEnabled; + var newEnabled = EditorGUI.Toggle(toggleRect, palette.colorsEnabled); + + if (prevEnabled != newEnabled) + palette.RecordUndo(); + + palette.colorsEnabled = newEnabled; + + if (prevEnabled != newEnabled) + palette.Dirty(); + + } + void crossIcon() + { + var crossIconRect = rowRect.SetX(rowsOffsetX + iconsOffsetX + iconSpacing / 2).SetWidth(iconSize).SetHeightFromMid(iconSize); + + SetGUIColor(palette.colorsEnabled ? Color.white : disabledRowTint); + + GUI.DrawTexture(crossIconRect, EditorIcons.GetIcon("CrossIcon")); + + ResetGUIColor(); + + } + void color(int i) + { + var cellRect = rowRect.MoveX(iconsOffsetX + (i + 1) * cellSize).SetWidth(cellSize).SetHeightFromMid(cellSize); + + void backgroundPicking() + { + if (!pickingColor) return; + if (i != pickingColorAtIndex) return; + + cellRect.DrawRounded(pickingBackground, 2); + + } + void backgroundHovered_andStartPickingColor() + { + if (pickingColor) return; + if (!cellRect.IsHovered()) return; + + + SetGUIColor(Color.clear); + + var clicked = GUI.Button(cellRect.Resize(1), ""); + + ResetGUIColor(); + + + + + var isPressed = GUIUtility.hotControl == typeof(EditorGUIUtility).GetFieldValue("s_LastControlID"); + + cellRect.DrawRounded(Greyscale(isPressed ? .39f : .43f), 2); + + + + + if (!clicked) return; + + colorPickerWindow = EditorUtils.OpenColorPicker((c) => { palette.RecordUndo(); palette.Dirty(); palette.colors[i] = c; }, palette.colors[i], true, false); + + colorPickerWindow.MoveTo(EditorGUIUtility.GUIToScreenPoint(cellRect.Move(-3, 50).position)); + + pickingColor = true; + pickingColorAtIndex = i; + + } + void colorOutline() + { + var outlineColor = i < VHierarchyPalette.greyColorsCount ? Greyscale(.0f, .4f) : Greyscale(.15f, .2f); + + if (!palette.colorsEnabled) + outlineColor *= disabledRowTint; + + + cellRect.Resize(3).DrawRounded(outlineColor, 4); + + } + void color() + { + var brightness = palette.colorBrightness; + var saturation = palette.colorSaturation; + var drawGradients = palette.colorGradientsEnabled; + + if (!palette.colorGradientsEnabled) + brightness *= isDarkTheme ? .75f : .97f; + + if (i < VHierarchyPalette.greyColorsCount) + { + saturation = brightness = 1; + drawGradients = false; + } + + + var colorRaw = palette.colors[i]; + + var color = MathUtil.Lerp(Greyscale(.2f), colorRaw, brightness); + + Color.RGBToHSV(color, out float h, out float s, out float v); + color = Color.HSVToRGB(h, s * saturation, v); + + color = MathUtil.Lerp(color, colorRaw, .5f).SetAlpha(1); + + if (!palette.colorsEnabled) + color *= disabledRowTint; + + if (i >= VHierarchyPalette.greyColorsCount && isDarkTheme) + color *= 1.41f; + + + + + cellRect.Resize(4).DrawRounded(color, 3); + + if (drawGradients) + cellRect.Resize(4).AddWidthFromRight(-2).DrawCurtainLeft(GUIColors.windowBackground.SetAlpha(.45f)); + + } + void updatePickingColor() + { + if (!pickingColor) return; + + EditorApplication.RepaintHierarchyWindow(); + + } + void stopPickingColor() + { + if (!pickingColor) return; + if (colorPickerWindow) return; + + pickingColor = false; + + } + + + cellRect.MarkInteractive(); + + + backgroundPicking(); + backgroundHovered_andStartPickingColor(); + + colorOutline(); + color(); + + updatePickingColor(); + stopPickingColor(); + + } + void adjustColorsButton() + { + var cellRect = rowRect.MoveX(iconsOffsetX + (palette.colors.Count + 1) * cellSize).SetWidth(cellSize).SetHeightFromMid(cellSize).MoveX(-1f); + + + var iconSize = 16; + var iconName = "Preset.Context"; + var iconColor = Greyscale(.75f, palette.colorsEnabled ? (isDarkTheme ? 1 : .8f) : .5f); + + if (!IconButton(cellRect, iconName, iconSize, iconColor)) return; + + + if (adjustColorsWindow) { adjustColorsWindow.Close(); return; } + + var windowX = 107f.Min(this.GetMemberValue("propertyViewer").position.width - 310); + var windowY = cellRect.y + 25; + var windowWidth = 270; + var windowHeight = 92; + + adjustColorsWindow = ScriptableObject.CreateInstance(); + adjustColorsWindow.palette = palette; + adjustColorsWindow.paletteEditor = this; + + adjustColorsWindow.ShowPopup(); + adjustColorsWindow.Focus(); + + adjustColorsWindow.position = EditorGUIUtility.GUIToScreenRect(new Rect(windowX, windowY, windowWidth, windowHeight)); + + } + + + backgroundHovered(); + toggle(); + crossIcon(); + + for (int i = 0; i < palette.colors.Count; i++) + color(i); + + adjustColorsButton(); + + Space(rowSpacing - 2); + + } + void icons() + { + void row(Rect rowRect, IconRow row) + { + var isLastRow = row == palette.iconRows.Last(); + var isDraggedRow = row == draggedRow; + var spaceForCrossIcon = 0f; + + + void updatePickingIcon() + { + if (!pickingIcon) return; + if (pickingIconAtRow != row) return; + if (pickingIconAtIndex >= row.customIcons.Count) return; // somehow happens if RecordUndo is used + + palette.RecordUndo(); + palette.Dirty(); + + row.customIcons[pickingIconAtIndex] = addIconWindow.hoveredIconName; + + } + void stopPickingIcon() + { + if (!pickingIcon) return; + if (pickingIconAtRow != row) return; + if (addIconWindow) return; + + if (pickingIconAtIndex < row.customIcons.Count) + if (row.customIcons[pickingIconAtIndex] == null) + row.customIcons.RemoveAt(pickingIconAtIndex); + + pickingIcon = false; + + } + void dragndrop() + { + if (!rowRect.IsHovered()) return; + if (!row.isCustom) return; + + if (curEvent.isDragUpdate && DragAndDrop.objectReferences.First() is Texture2D) + DragAndDrop.visualMode = DragAndDropVisualMode.Copy; + + if (!curEvent.isDragPerform) return; + if (!(DragAndDrop.objectReferences.Any(r => r is Texture2D))) return; + + DragAndDrop.AcceptDrag(); + + palette.RecordUndo(); + palette.Dirty(); + + foreach (var icon in DragAndDrop.objectReferences.Where(r => r is Texture2D)) + row.customIcons.Add(icon.GetPath().ToGuid()); + + } + + void calcSpaceForCrossIcon() + { + if (row == curFirstEnabledRow) + spaceForCrossIcon = crossIconAnimationT * cellSize; + + if (row == crossIconAnimationSourceRow) + spaceForCrossIcon = (1 - crossIconAnimationT) * cellSize; + + } + + void backgroundHovered() + { + if (!rowRect.IsHovered()) return; + if (pickingColor) return; + if (pickingIcon) return; + if (draggingRow) return; + if (DragAndDrop.objectReferences.Any() && !row.isCustom) return; + + + rowRect.Draw(hoveredRowBackground); + + } + void backgroundDragged() + { + if (!isDraggedRow) return; + + rowRect.DrawBlurred(Greyscale(0, .3f), 12); + rowRect.Draw(draggedRowBackground); + + } + void toggle() + { + var prevEnabled = row.enabled; + var newEnabled = EditorGUI.Toggle(rowRect.SetWidth(16).MoveX(5), row.enabled); + + if (prevEnabled != newEnabled) + palette.RecordUndo(); + + row.enabled = newEnabled; + + if (prevEnabled != newEnabled) + palette.Dirty(); + + } + void addIconButton() + { + if (!row.isCustom) return; + if (pickingIcon && pickingIconAtRow == row) return; + + + var cellRect = rowRect.MoveX(iconsOffsetX + row.customIcons.Count * cellSize + spaceForCrossIcon).SetWidth(cellSize).SetHeightFromMid(cellSize); + + var iconSize = 16; + var iconName = "Toolbar Plus"; + var iconColor = Greyscale(.73f, row.enabled ? (isDarkTheme ? 1 : .65f) : .5f); + + if (!IconButton(cellRect, iconName, iconSize, iconColor)) return; + + + + palette.RecordUndo(); + + row.customIcons.Add(null); + + + + var windowX = 15; + var windowY = cellRect.y + 23; + var windowWidth = (this.GetMemberValue("propertyViewer").position.width - 26).Min(679); + var windowHeight = windowWidth * 1.2f; + + windowWidth = (windowWidth / AddIconWindow.cellSize).FloorToInt() * AddIconWindow.cellSize - 3; + + + addIconWindow = ScriptableObject.CreateInstance(); + addIconWindow.palette = palette; + addIconWindow.paletteEditor = this; + + addIconWindow.ShowPopup(); + addIconWindow.Focus(); + + addIconWindow.position = EditorGUIUtility.GUIToScreenRect(new Rect(windowX, windowY, windowWidth, windowHeight)); + + addIconWindow.Init(); + + + pickingIcon = true; + pickingIconAtIndex = row.customIcons.Count - 1; + pickingIconAtRow = row; + + + } + void icon(int i) + { + var cellRect = rowRect.MoveX(iconsOffsetX + spaceForCrossIcon + i * cellSize).SetWidth(cellSize).SetHeightFromMid(cellSize); + + + void backgroundPicking() + { + if (!pickingIcon) return; + if (pickingIconAtRow != row) return; + if (pickingIconAtIndex != i) return; + + cellRect.DrawRounded(pickingBackground, 2); + + } + void backgroundHovered_andEditIconButton() + { + if (!row.isCustom) return; + if (pickingIcon) return; + if (!cellRect.IsHovered()) return; + if (DragAndDrop.objectReferences.Any()) return; + + + SetGUIColor(Color.clear); + + var clicked = GUI.Button(cellRect.Resize(1), ""); + + ResetGUIColor(); + + + + + var isPressed = GUIUtility.hotControl == typeof(EditorGUIUtility).GetFieldValue("s_LastControlID"); + + cellRect.DrawRounded(Greyscale(isPressed ? .39f : .45f), 2); + + + + + if (!clicked) return; + + GenericMenu menu = new(); + + menu.AddItem(new GUIContent("Move left"), false, i == 0 ? null : () => + { + palette.RecordUndo(); + palette.Dirty(); + + var icon = row.customIcons[i]; + + row.customIcons.RemoveAt(i); + row.customIcons.AddAt(icon, i - 1); + + }); + menu.AddItem(new GUIContent("Move right"), false, i == row.customIcons.Count - 1 ? null : () => + { + palette.RecordUndo(); + palette.Dirty(); + + var icon = row.customIcons[i]; + + row.customIcons.RemoveAt(i); + row.customIcons.AddAt(icon, i + 1); + + }); + + menu.AddSeparator(""); + + menu.AddItem(new GUIContent("Remove icon"), false, () => { palette.RecordUndo(); row.customIcons.RemoveAt(i); palette.Dirty(); }); + + + menu.ShowAsContext(); + + } + + void drawIcon() + { + var iconNameOrGuid = row.isCustom ? row.customIcons[i] : row.builtinIcons[i]; + + if (iconNameOrGuid == null) return; + + var iconNameOrPath = iconNameOrGuid.Length == 32 ? iconNameOrGuid.ToPath() : iconNameOrGuid; + var icon = EditorIcons.GetIcon(iconNameOrPath) ?? Texture2D.blackTexture; + + + var cellRect = rowRect.MoveX(iconsOffsetX + spaceForCrossIcon + i * cellSize).SetWidth(cellSize).SetHeightFromMid(cellSize); + var iconRect = cellRect.SetSizeFromMid(iconSize); + + if (icon.width < icon.height) iconRect = iconRect.SetWidthFromMid(iconRect.height * icon.width / icon.height); + if (icon.height < icon.width) iconRect = iconRect.SetHeightFromMid(iconRect.width * icon.height / icon.width); + + + + SetGUIColor(row.enabled ? Color.white : disabledRowTint); + + GUI.DrawTexture(iconRect, icon); + + ResetGUIColor(); + + } + + + cellRect.MarkInteractive(); + + backgroundPicking(); + backgroundHovered_andEditIconButton(); + + drawIcon(); + + } + + + rowRect.MarkInteractive(); + + updatePickingIcon(); + stopPickingIcon(); + dragndrop(); + + calcSpaceForCrossIcon(); + backgroundHovered(); + backgroundDragged(); + toggle(); + addIconButton(); + + for (int i = 0; i < row.iconCount; i++) + icon(i); + + } + + void updateRowsCount() + { + palette.iconRows.RemoveAll(r => r.isEmpty && r != palette.iconRows.Last()); + + if (!palette.iconRows.Last().isEmpty) + palette.iconRows.Add(new IconRow()); + + } + void updateRowGapsCount() + { + while (rowGaps.Count < palette.iconRows.Count) + rowGaps.Add(0); + + while (rowGaps.Count > palette.iconRows.Count) + rowGaps.RemoveLast(); + + } + + void normalRow(int i) + { + Space(rowGaps[i] * (cellSize + rowSpacing)); + + if (i == 0 && lastRect.y != 0) + firstRowY = lastRect.y; + + Space(cellSize + rowSpacing); + + var rowRect = Rect.zero.SetPos(rowsOffsetX, lastRect.y).SetSize(rowWidth, cellSize); + + if (curEvent.isRepaint) + if (rowRect.IsHovered()) + hoveredRow = palette.iconRows[i]; + + + row(rowRect, palette.iconRows[i]); + + } + void draggedRow_() + { + if (!draggingRow) return; + + draggedRowY = (curEvent.mousePosition.y + draggedRowHoldOffset).Clamp(firstRowY, firstRowY + (palette.iconRows.Count - 1) * (cellSize + rowSpacing)); + + var rowRect = Rect.zero.SetPos(rowsOffsetX, draggedRowY).SetSize(rowWidth, cellSize); + + row(rowRect, draggedRow); + + } + void crossIcon() + { + if (!palette.iconRows.Any(r => r.enabled)) return; + + var rect = Rect.zero.SetPos(rowsOffsetX + iconsOffsetX, crossIconY).SetSize(cellSize, cellSize).Resize(iconSpacing / 2); + + GUI.DrawTexture(rect, EditorIcons.GetIcon("CrossIcon")); + + } + + + updateRowsCount(); + updateRowGapsCount(); + + + if (curEvent.isRepaint) + hoveredRow = null; + + for (int i = 0; i < palette.iconRows.Count; i++) + normalRow(i); + + crossIcon(); + + draggedRow_(); + + + } + void tutor() + { + SetGUIEnabled(false); + + + Space(4); + GUILayout.Label("Add icons with drag-and-drop or by clicking '+'"); + + Space(4); + GUILayout.Label("Click added icon to move or remove it"); + + Space(4); + GUILayout.Label("Drag rows to reorder them"); + + + ResetGUIEnabled(); + + } + + + Space(15); + colors(); + + Space(15); + icons(); + + Space(22); + tutor(); + + UpdateAnimations(); + + UpdateDragging(); + + palette.Dirty(); + + if (draggingRow || animatingCrossIcon) + Repaint(); + + } + + float iconSize => 14; + float iconSpacing => 6; + float cellSize => iconSize + iconSpacing; + float rowSpacing = 1; + float rowsOffsetX => 14; + float iconsOffsetX => 27; + + Color hoveredRowBackground => Greyscale(isDarkTheme ? 1 : 0, .05f); + Color draggedRowBackground => Greyscale(isDarkTheme ? .3f : .9f); + Color pickingBackground => Greyscale(1, .17f); + Color disabledRowTint => Greyscale(1, .45f); + + float rowWidth => cellSize * Mathf.Max(palette.colors.Count, palette.iconRows.Max(r => r.iconCount + 1)) + 55; + + bool pickingColor; + int pickingColorAtIndex; + EditorWindow colorPickerWindow; + + bool pickingIcon; + int pickingIconAtIndex; + IconRow pickingIconAtRow; + AddIconWindow addIconWindow; + + IconRow hoveredRow; + + float firstRowY = 51; + + static AdjustColorsWindow adjustColorsWindow; + + + + + + void UpdateAnimations() + { + void lerpRowGaps() + { + if (!curEvent.isLayout) return; + + var lerpSpeed = draggingRow ? 12 : 12321; + + for (int i = 0; i < rowGaps.Count; i++) + rowGaps[i] = MathUtil.Lerp(rowGaps[i], draggingRow && i == insertDraggedRowAtIndex ? 1 : 0, lerpSpeed, editorDeltaTime); + + for (int i = 0; i < rowGaps.Count; i++) + if (rowGaps[i].Approx(0)) + rowGaps[i] = 0; + else if (rowGaps[i].Approx(1)) + rowGaps[i] = 1; + + + } + + void lerpCrossIconAnimationT() + { + if (!curEvent.isLayout) return; + + var lerpSpeed = 12; + + MathUtil.Lerp(ref crossIconAnimationT, 1, lerpSpeed, editorDeltaTime); + + } + void startCrossIconAnimation() + { + if (prevFirstEnabledRow == null) { prevFirstEnabledRow = curFirstEnabledRow; return; } + if (prevFirstEnabledRow == curFirstEnabledRow) return; + + crossIconAnimationT = 0; + crossIconAnimationSourceRow = prevFirstEnabledRow; + + prevFirstEnabledRow = curFirstEnabledRow; + + } + void stopCrossIconAnimation() + { + if (!crossIconAnimationT.Approx(1)) return; + + crossIconAnimationT = 1; + crossIconAnimationSourceRow = null; + + } + void calcCrossIconY() + { + var indexOfFirstEnabled = palette.iconRows.IndexOfFirst(r => r.enabled); + var yOfFirstEnabled = firstRowY + indexOfFirstEnabled * (cellSize + rowSpacing); + for (int i = 0; i < indexOfFirstEnabled + 1; i++) + yOfFirstEnabled += rowGaps[i] * (cellSize + rowSpacing); + + + var indexOfSourceRow = palette.iconRows.IndexOf(crossIconAnimationSourceRow); + var yOfSourceRow = firstRowY + indexOfSourceRow * (cellSize + rowSpacing); + for (int i = 0; i < indexOfSourceRow + 1; i++) + yOfSourceRow += rowGaps[i] * (cellSize + rowSpacing); + + if (crossIconAnimationSourceRow == draggedRow) + yOfSourceRow = draggedRowY; + + + crossIconY = MathUtil.Lerp(yOfSourceRow, yOfFirstEnabled, crossIconAnimationT); + + if (indexOfFirstEnabled == indexOfSourceRow) + crossIconAnimationT = 1; + + } + + + lerpRowGaps(); + + lerpCrossIconAnimationT(); + startCrossIconAnimation(); + stopCrossIconAnimation(); + calcCrossIconY(); + + } + + List rowGaps = new(); + + float crossIconY = 51; + float crossIconAnimationT = 1; + IconRow crossIconAnimationSourceRow; + bool animatingCrossIcon => crossIconAnimationT != 1; + + [System.NonSerialized] IconRow prevFirstEnabledRow; + IconRow curFirstEnabledRow => palette.iconRows.FirstOrDefault(r => r.enabled); + + + + + + + void UpdateDragging() + { + void startDragging() + { + if (draggingRow) return; + if (!curEvent.isMouseDrag) return; + if (hoveredRow == null) return; + if (hoveredRow == palette.iconRows.Last()) return; + + palette.RecordUndo(); + + draggingRow = true; + draggedRow = hoveredRow; + draggingRowFromIndex = palette.iconRows.IndexOf(hoveredRow); + draggedRowHoldOffset = firstRowY + draggingRowFromIndex * (cellSize + rowSpacing) - curEvent.mousePosition.y; + + palette.iconRows.Remove(hoveredRow); + rowGaps[draggingRowFromIndex] = 1; + + } + void updateDragging() + { + if (!draggingRow) return; + + insertDraggedRowAtIndex = ((curEvent.mousePosition.y - firstRowY) / (cellSize + rowSpacing)).FloorToInt().Clamp(0, palette.iconRows.Count - 1); + + EditorGUIUtility.hotControl = EditorGUIUtility.GetControlID(FocusType.Passive); + + } + void stopDragging() + { + if (!draggingRow) return; + if (!curEvent.isMouseUp) return; + + palette.RecordUndo(); + palette.Dirty(); + + palette.iconRows.AddAt(draggedRow, insertDraggedRowAtIndex); + + rowGaps[insertDraggedRowAtIndex] = 0; + + draggingRow = false; + draggedRow = null; + + EditorGUIUtility.hotControl = 0; + + } + + + startDragging(); + updateDragging(); + stopDragging(); + + } + + IconRow draggedRow; + bool draggingRow; + int draggingRowFromIndex; + float draggedRowHoldOffset; + float draggedRowY; + int insertDraggedRowAtIndex; + + + + + + + VHierarchyPalette palette => target as VHierarchyPalette; + + } + + + class AddIconWindow : EditorWindow + { + + void OnGUI() + { + void header() + { + var headerRect = Rect.zero.SetHeight(20).SetWidth(position.width); + var closeButtonRect = headerRect.SetWidthFromRight(16).SetHeightFromMid(16).Move(-3, -.5f); + + void background() + { + headerRect.Draw(EditorGUIUtility.isProSkin ? Greyscale(.18f) : Greyscale(.7f)); + } + void title() + { + SetGUIColor(Greyscale(.8f)); + SetLabelAlignmentCenter(); + + GUI.Label(headerRect.MoveY(-1), "Add icon"); + + ResetLabelStyle(); + ResetGUIColor(); + + } + void closeButton() + { + var colorNormal = isDarkTheme ? Greyscale(.55f) : Greyscale(.35f); + var colorHovered = isDarkTheme ? Greyscale(.9f) : colorNormal; + + var iconSize = 14; + + if (IconButton(closeButtonRect, "CrossIcon", iconSize, colorNormal, colorHovered)) + Close(); + + } + void escHint() + { + if (!closeButtonRect.IsHovered()) return; + + var textRect = headerRect.SetWidthFromRight(42).MoveY(-.5f).MoveX(1); + var fontSize = 11; + var color = Greyscale(.65f); + + + SetLabelFontSize(fontSize); + SetGUIColor(color); + + GUI.Label(textRect, "Esc"); + + ResetGUIColor(); + ResetLabelStyle(); + + } + + background(); + title(); + closeButton(); + escHint(); + + Space(headerRect.height); + + } + void search() + { + var backgroundRect = ExpandWidthLabelRect(height: 21).SetWidthFromMid(position.width); + var backgroundColor = isDarkTheme ? Greyscale(.25f) : Greyscale(.8f); + + backgroundRect.Draw(backgroundColor); + + + var lineRect = backgroundRect.SetHeightFromBottom(1).MoveY(.5f); + var lineColor = isDarkTheme ? Greyscale(.15f) : Greyscale(.7f); + + lineRect.Draw(lineColor); + + + var searchRect = backgroundRect.Resize(2); + + EditorGUI.BeginChangeCheck(); + + searchString = searchField.OnGUI(searchRect, searchString); + + if (EditorGUI.EndChangeCheck()) + { + GenerateRows(); + FilterIconsBySearch(); + GenerateRows(); + } + + } + void icons() + { + void row(int i) + { + var rowRect = position.SetPos(0, 0).SetHeight(cellSize).MoveY(i * rowHeight); + var iconNames = rows[i]; + + void icon(int i) + { + var iconName = iconNames[i]; + var icon = EditorIcons.GetIcon(iconName); + + var cellRect = rowRect.SetWidth(cellSize).MoveX(i * cellSize + paddingLeft); + var iconRect = cellRect.SetSizeFromMid(iconSize); + + if (icon.width < icon.height) iconRect = iconRect.SetWidthFromMid(iconRect.height * icon.width / icon.height); + if (icon.height < icon.width) iconRect = iconRect.SetHeightFromMid(iconRect.width * icon.height / icon.width); + + + + var hoverRect = cellRect.AddHeightFromMid(rowSpacing); + + hoverRect.MarkInteractive(); + + if (hoverRect.IsHovered()) + cellRect.Draw(Greyscale(isDarkTheme ? .42f : .69f)); + + + + GUI.DrawTexture(iconRect, EditorIcons.GetIcon(iconName)); + + + + if (hoverRect.IsHovered()) + hoveredIconName = iconName; + + if (hoverRect.IsHovered() && curEvent.isMouseDown) + Close(); + + } + + for (int ii = 0; ii < iconNames.Count; ii++) + icon(ii); + } + + + if (curEvent.isRepaint) + hoveredIconName = null; + + + scrollPos = GUILayout.BeginScrollView(new Vector2(0, scrollPos)).y; + + GUILayout.Space(rows.Count * rowHeight + 23); + + + var i0 = (scrollPos / rowHeight).FloorToInt(); + var i1 = (i0 + ((position.height - 30) / rowHeight).CeilToInt()).Min(rows.Count); + + for (int ii = i0; ii < i1; ii++) + row(ii); + + + GUILayout.EndScrollView(); + + } + void hoveredIconLabel() + { + if (hoveredIconName == null) return; + + + var nameRect = position.SetPos(0, 0).SetHeightFromBottom(18).SetWidth(hoveredIconName.GetLabelWidth() + 6).Move(1, -1); + + var shadowRect = nameRect.AddWidthFromRight(10).AddHeight(10); + + + shadowRect.DrawBlurred(GUIColors.windowBackground, 12); + shadowRect.DrawBlurred(GUIColors.windowBackground.SetAlpha(.4f), 8); + + + SetLabelAlignmentCenter(); + + GUI.Label(nameRect, hoveredIconName); + + ResetLabelStyle(); + + } + void closeOnEsc() + { + if (!curEvent.isKeyDown) return; + if (curEvent.keyCode != KeyCode.Escape) return; + + hoveredIconName = null; + + Close(); + + } + void outline() + { + if (Application.platform == RuntimePlatform.OSXEditor) return; + + position.SetPos(0, 0).DrawOutline(Greyscale(.1f)); + + } + + header(); + search(); + icons(); + hoveredIconLabel(); + closeOnEsc(); + outline(); + + paletteEditor.Repaint(); + + if (EditorWindow.focusedWindow != this) + Close(); + + } + + public static float iconSize => 16; + public static float iconSpacing => 6; + public static float cellSize => iconSize + iconSpacing; + public static float rowSpacing = 1; + + public static float rowHeight => cellSize + rowSpacing; + + public static float paddingLeft => 3; + public static float paddingRight => 3; + + public string hoveredIconName; + + static string searchString = ""; + static float scrollPos; + + + + + + void LoadAllIcons() + { + if (allIconNames != null) return; + + + (float h, float s, float v, float a) GetAverageColor(Texture2D texture) + { + + var readableTexture = new Texture2D(texture.width, texture.height, texture.format, texture.mipmapCount > 1); + + Graphics.CopyTexture(texture, readableTexture); + + var pixels = readableTexture.GetPixels32(); + + readableTexture.DestroyImmediate(); + + + float hSum = 0; + float sSum = 0; + float vSum = 0; + + int nonTransparentPxCount = pixels.Length; + int coloredPxCount = pixels.Length; + + for (var i = 0; i < pixels.Length; i++) + { + if (pixels[i].a <= .1f) { nonTransparentPxCount--; coloredPxCount--; continue; } + + Color.RGBToHSV(pixels[i], out float h, out float s, out float v); + + if (s > .1f) + hSum += h; + else + coloredPxCount--; + + sSum += s; + + vSum += v; + + } + + var hAvg = hSum / coloredPxCount; + var sAvg = sSum / nonTransparentPxCount; + var vAvg = vSum / nonTransparentPxCount; + var aAvg = nonTransparentPxCount / pixels.Length.ToFloat(); + + if (coloredPxCount == 0) + hAvg = -1; + + + return (hAvg, sAvg, vAvg, aAvg); + + } + + + + var editorAssetBundle = typeof(EditorGUIUtility).InvokeMethod("GetEditorAssetBundle"); + + allIconNames = ( + + from path in editorAssetBundle.GetAllAssetNames() + + + let icon = editorAssetBundle.LoadAsset(path) + + where icon + + + where path.StartsWith("icons/") + where !path.Contains("avatarinspector") + + where !icon.name.ToLower().StartsWith("d_") + + where !icon.name.ToLower().EndsWith(".small") + where !icon.name.ToLower().EndsWith("_sml") + + where !icon.name.Contains("@") + where !icon.name.Contains("TreeEditor") + where !icon.name.Contains("scene-template") + where !icon.name.Contains("StateMachineEditor.Background") + where !icon.name.Contains("SpeedTree") + where !icon.name.Contains("TextMesh") + where !icon.name.Contains("Profiler.Instrumentation") + where !icon.name.Contains("Profiler.Record") + where !icon.name.Contains("SocialNetworks") + where !icon.name.Contains("Groove") + + + + let avgColor = GetAverageColor(icon) + + where avgColor.a > .1f + + + + orderby avgColor.h * -3f + + avgColor.s * .09f + + avgColor.v * 0f, icon.name + + + + select icon.name + + ).ToHashSet() + .ToList(); + + } + + static List allIconNames; + + + + void FilterIconsBySearch() + { + filteredIcons = ( + + from iconName in allIconNames + + where iconName.ToLower().Contains(searchString.ToLower()) + + orderby iconName.ToLower().IndexOf(searchString.ToLower(), System.StringComparison.Ordinal) + + select iconName + + ).ToList(); + } + + static List filteredIcons; + + + + void GenerateRows() + { + + var iconsPerRow = ((position.width - paddingLeft - paddingRight) / cellSize).FloorToInt(); + + rows = new(); + + var curRow = new List(); + + foreach (var icon in filteredIcons) + { + curRow.Add(icon); + + if (curRow.Count == iconsPerRow) + { + rows.Add(curRow); + curRow = new(); + } + } + + if (curRow.Any()) + rows.Add(curRow); + + } + + static List> rows; + + + + + + + public void Init() + { + LoadAllIcons(); + FilterIconsBySearch(); + GenerateRows(); + + searchField = new(); + + } + + SearchField searchField; + + + + + + public VHierarchyPalette palette; + public VHierarchyPaletteEditor paletteEditor; + + } + + class AdjustColorsWindow : EditorWindow + { + void OnGUI() + { + void header() + { + var headerRect = Rect.zero.SetHeight(20).SetWidth(position.width); + var closeButtonRect = headerRect.SetWidthFromRight(16).SetHeightFromMid(16).Move(-3, -.5f); + + void background() + { + headerRect.Draw(EditorGUIUtility.isProSkin ? Greyscale(.18f) : Greyscale(.7f)); + } + void title() + { + SetGUIColor(Greyscale(.8f)); + SetLabelAlignmentCenter(); + + GUI.Label(headerRect.MoveY(-1), "Adjust colors"); + + ResetLabelStyle(); + ResetGUIColor(); + + } + void closeButton() + { + var colorNormal = isDarkTheme ? Greyscale(.55f) : Greyscale(.35f); + var colorHovered = isDarkTheme ? Greyscale(.9f) : colorNormal; + + var iconSize = 14; + + if (IconButton(closeButtonRect, "CrossIcon", iconSize, colorNormal, colorHovered)) + Close(); + + } + void escHint() + { + if (!closeButtonRect.IsHovered()) return; + + var textRect = headerRect.SetWidthFromRight(42).MoveY(-.5f).MoveX(1); + var fontSize = 11; + var color = Greyscale(.65f); + + + SetLabelFontSize(fontSize); + SetGUIColor(color); + + GUI.Label(textRect, "Esc"); + + ResetGUIColor(); + ResetLabelStyle(); + + } + void outline() + { + if (Application.platform == RuntimePlatform.OSXEditor) return; + + position.SetPos(0, 0).DrawOutline(Greyscale(.1f)); + + } + + background(); + title(); + closeButton(); + escHint(); + outline(); + + Space(headerRect.height); + + } + void body() + { + EditorGUIUtility.labelWidth = 85; + EditorGUIUtility.keyboardControl = 0; + + palette.RecordUndo(); + + + EditorGUI.BeginChangeCheck(); + + palette.colorBrightness = (EditorGUILayout.Slider("Brightness", palette.colorBrightness, 0, 2) / .1f).RoundToInt() * .1f; + palette.colorSaturation = (EditorGUILayout.Slider("Saturation", palette.colorSaturation, 0, 2) / .1f).RoundToInt() * .1f; + + palette.colorGradientsEnabled = EditorGUILayout.Toggle("Gradients", palette.colorGradientsEnabled); + + if (EditorGUI.EndChangeCheck()) + { + VHierarchy.goInfoCache.Clear(); + + paletteEditor.Repaint(); + EditorApplication.RepaintHierarchyWindow(); + palette.Dirty(); + } + + + EditorGUIUtility.labelWidth = 0; + + } + void closeOnEsc() + { + if (!curEvent.isKeyDown) return; + if (curEvent.keyCode != KeyCode.Escape) return; + + Close(); + + } + + + header(); + + Space(7); + BeginIndent(10); + + body(); + + EndIndent(5); + + + + closeOnEsc(); + + if (EditorWindow.focusedWindow != this) + Close(); + + Repaint(); // for undo + + } + + public VHierarchyPalette palette; + public VHierarchyPaletteEditor paletteEditor; + + } + +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchyPaletteEditor.cs.meta b/vfolders2/vHierarchy/VHierarchyPaletteEditor.cs.meta new file mode 100644 index 0000000..a1fcf36 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyPaletteEditor.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 2b7dce55e9e9b476fb5d1d669c006123 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchyPaletteWindow.cs b/vfolders2/vHierarchy/VHierarchyPaletteWindow.cs new file mode 100644 index 0000000..1751200 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyPaletteWindow.cs @@ -0,0 +1,711 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditor.ShortcutManagement; +using System.Reflection; +using System.Linq; +using UnityEngine.UIElements; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using Type = System.Type; +using static VHierarchy.VHierarchy; +using static VHierarchy.VHierarchyData; +using static VHierarchy.VHierarchyPalette; +using static VHierarchy.Libs.VUtils; +using static VHierarchy.Libs.VGUI; +// using static VTools.VDebug; + + + +namespace VHierarchy +{ + public class VHierarchyPaletteWindow : EditorWindow + { + + void OnGUI() + { + if (!palette) { Close(); return; } + + int hoveredColorIndex = -1; + string hoveredIconNameOrGuid = null; + + void background() + { + position.SetPos(0, 0).Draw(windowBackground); + } + void outline() + { + if (Application.platform == RuntimePlatform.OSXEditor) return; + + position.SetPos(0, 0).DrawOutline(Greyscale(.1f)); + + } + void colors() + { + if (!palette.colorsEnabled) return; + + var rowRect = this.position.SetPos(paddingX, paddingY).SetHeight(cellSize); + + + void color(int i) + { + var cellRect = rowRect.MoveX(i * cellSize).SetWidth(cellSize).SetHeightFromMid(cellSize); + + void backgroundSelected() + { + if (!colorIndexes_initial.Contains(i)) return; + + cellRect.Resize(1).DrawRounded(selectedBackground, 2); + + } + void backgroundHovered() + { + if (!cellRect.IsHovered()) return; + + cellRect.Resize(1).DrawRounded(this.hoveredBackground, 2); + + } + void crossIcon() + { + if (i != 0) return; + + GUI.DrawTexture(cellRect.SetSizeFromMid(iconSize), EditorIcons.GetIcon("CrossIcon")); + + } + void colorOutline() + { + if (i == 0) return; + + var outlineColor = i <= VHierarchyPalette.greyColorsCount ? Greyscale(.0f, .4f) : Greyscale(.15f, .2f); + + cellRect.Resize(3).DrawRounded(outlineColor, 4); + + } + void color() + { + if (i == 0) return; + + var brightness = palette.colorBrightness; + var saturation = palette.colorSaturation; + var drawGradients = palette.colorGradientsEnabled; + + if (!palette.colorGradientsEnabled) + brightness *= isDarkTheme ? .75f : .97f; + + if (i <= VHierarchyPalette.greyColorsCount) + { + saturation = brightness = 1; + drawGradients = false; + } + + + var colorRaw = palette ? palette.colors[i - 1] : VHierarchyPalette.GetDefaultColor(i - 1); + + var color = MathUtil.Lerp(Greyscale(.2f), colorRaw, brightness); + + Color.RGBToHSV(color, out float h, out float s, out float v); + color = Color.HSVToRGB(h, s * saturation, v); + + color = MathUtil.Lerp(color, colorRaw, .5f).SetAlpha(1); + + if (i > VHierarchyPalette.greyColorsCount && isDarkTheme) + color *= 1.41f; + + + + + cellRect.Resize(4).DrawRounded(color, 3); + + if (drawGradients) + cellRect.Resize(4).AddWidthFromRight(-2).DrawCurtainLeft(GUIColors.windowBackground.SetAlpha(.45f)); + + } + void recursiveIndicator() + { + if (!curEvent.isRepaint) return; + + + var isRecursive = goDatas.First().colorIndex == i && goDatas.First().isColorRecursive; + + if (!isRecursive) return; + + + + var iconRect = cellRect.SetSizeFromMid(16).Move(-6, -7); + var shadowRect = iconRect.Resize(3).Move(2, 1).AddWidthFromRight(3); + var shadowRadius = 4; + + shadowRect.DrawBlurred(GUIColors.windowBackground, shadowRadius); + + + SetGUIColor(Color.white * 2); + + GUI.DrawTexture(iconRect, EditorIcons.GetIcon("UnityEditor.SceneHierarchyWindow@2x")); + + ResetGUIColor(); + + + } + + void setHovered() + { + if (!cellRect.IsHovered()) return; + + hoveredColorIndex = i; + + } + void closeOnClick() + { + if (!cellRect.IsHovered()) return; + if (!curEvent.isMouseUp) return; + + curEvent.Use(); + + Close(); + + } + + + + cellRect.MarkInteractive(); + + backgroundSelected(); + backgroundHovered(); + crossIcon(); + colorOutline(); + color(); + recursiveIndicator(); + + setHovered(); + closeOnClick(); + + } + + + for (int i = 0; i < palette.colors.Count + 1; i++) + color(i); + + } + void icons() + { + void row(int i, IconRow iconRow) + { + var rowRect = this.position.SetPos(paddingX, paddingY).SetHeight(cellSize).MoveY(palette.colorsEnabled ? cellSize + spaceAfterColors : 0).MoveY(i * (cellSize + rowSpacing)); + + var isFirstEnabledRow = palette.iconRows.First(r => r.enabled) == iconRow; + + + void icon(int i) + { + var cellRect = rowRect.MoveX(i * cellSize).SetWidth(cellSize).SetHeightFromMid(cellSize); + + var isCrossIcon = isFirstEnabledRow && i == 0; + var actualIconIndex = isFirstEnabledRow ? i - 1 : i; + var isCustomIcon = !isCrossIcon && actualIconIndex >= iconRow.builtinIcons.Count; + var iconNameOrGuid = isCrossIcon ? "" : isCustomIcon ? iconRow.customIcons[actualIconIndex - iconRow.builtinIcons.Count] : iconRow.builtinIcons[actualIconIndex]; + + + void backgroundSelected() + { + if (!iconNamesOrGuids_initial.Contains(iconNameOrGuid)) return; + + cellRect.Resize(1).DrawRounded(selectedBackground, 2); + + } + void backgroundHovered() + { + if (!cellRect.IsHovered()) return; + + cellRect.Resize(1).DrawRounded(this.hoveredBackground, 2); + + } + void crossIcon() + { + if (!isCrossIcon) return; + + GUI.DrawTexture(cellRect.SetSizeFromMid(iconSize), EditorIcons.GetIcon("CrossIcon")); + + } + void normalIcon() + { + if (isCrossIcon) return; + + var iconNameOrPath = iconNameOrGuid?.Length == 32 ? iconNameOrGuid.ToPath() : iconNameOrGuid; + var icon = EditorIcons.GetIcon(iconNameOrPath) ?? Texture2D.blackTexture; + + var iconRect = cellRect.SetSizeFromMid(iconSize); + + if (icon.width < icon.height) iconRect = iconRect.SetWidthFromMid(iconRect.height * icon.width / icon.height); + if (icon.height < icon.width) iconRect = iconRect.SetHeightFromMid(iconRect.width * icon.height / icon.width); + + + GUI.DrawTexture(iconRect, icon); + + } + void recursiveIndicator() + { + if (!curEvent.isRepaint) return; + + + var isRecursive = goDatas.First().iconNameOrGuid == iconNameOrGuid && goDatas.First().isIconRecursive; + + if (!isRecursive) return; + + + + var iconRect = cellRect.SetSizeFromMid(16).Move(-6, -7); + var shadowRect = iconRect.Resize(3).Move(2, 1).AddWidthFromRight(3); + var shadowRadius = 4; + + shadowRect.DrawBlurred(GUIColors.windowBackground, shadowRadius); + + + + SetGUIColor(Color.white * 2); + + GUI.DrawTexture(iconRect, EditorIcons.GetIcon("UnityEditor.SceneHierarchyWindow@2x")); + + ResetGUIColor(); + + + } + + void setHovered() + { + if (!cellRect.IsHovered()) return; + + hoveredIconNameOrGuid = iconNameOrGuid; + + } + void closeOnClick() + { + if (!cellRect.IsHovered()) return; + if (!curEvent.isMouseUp) return; + + curEvent.Use(); + + Close(); + + } + + + + cellRect.MarkInteractive(); + + backgroundSelected(); + backgroundHovered(); + crossIcon(); + normalIcon(); + recursiveIndicator(); + + setHovered(); + closeOnClick(); + + } + + + for (int j = 0; j < iconRow.iconCount + (isFirstEnabledRow ? 1 : 0); j++) + icon(j); + + } + + + var i = 0; + + foreach (var iconRow in palette.iconRows) + { + if (!iconRow.enabled) continue; + if (iconRow.isEmpty) continue; + + row(i, iconRow); + + i++; + } + + } + void editPaletteButton() + { + var buttonRect = position.SetPos(0, 0).SetWidthFromRight(16).SetHeightFromBottom(16).Move(-14, -14); + var buttonColor = isDarkTheme ? Greyscale(.6f) : Greyscale(1, .6f); + + if (!IconButton(buttonRect, "Toolbar Plus", 16, buttonColor)) return; + + + palette.SelectInInspector(frameInProject: false); + + EditorWindow.GetWindow(typeof(Editor).Assembly.GetType("UnityEditor.InspectorWindow"))?.Show(); + + this.Close(); + + } + + void setColorsAndIcons() + { + if (!curEvent.isLayout) return; + + + if (palette.iconRows.Any(r => r.enabled)) + if (hoveredIconNameOrGuid != null) + SetIcon(hoveredIconNameOrGuid, isRecursive: curEvent.holdingAlt); + else + SetInitialIcons(); + + + if (palette.colorsEnabled) + if (hoveredColorIndex != -1) + SetColor(hoveredColorIndex, isRecursive: curEvent.holdingAlt); + else + SetInitialColors(); + + } + void updatePosition() + { + if (!curEvent.isLayout) return; + + void calcDeltaTime() + { + deltaTime = (float)(EditorApplication.timeSinceStartup - lastLayoutTime); + + if (deltaTime > .05f) + deltaTime = .0166f; + + lastLayoutTime = EditorApplication.timeSinceStartup; + + } + void resetCurPos() + { + if (currentPosition != default) return; + + currentPosition = position.position; // position.position is always int, which can't be used for lerping + + } + void lerpCurPos() + { + var speed = 9; + + MathUtil.SmoothDamp(ref currentPosition, targetPosition, speed, ref positionDeriv, deltaTime); + // MathfUtils.Lerp(ref currentPosition, targetPosition, speed, deltaTime); + + } + void setCurPos() + { + position = position.SetPos(currentPosition); + } + + calcDeltaTime(); + resetCurPos(); + lerpCurPos(); + setCurPos(); + + if (!currentPosition.magnitude.Approx(targetPosition.magnitude)) + Repaint(); + + } + void closeOnEscape() + { + if (!curEvent.isKeyDown) return; + if (curEvent.keyCode != KeyCode.Escape) return; + + SetInitialColors(); + SetInitialIcons(); + + Close(); + + } + + + RecordUndoOnDatas(); + + background(); + outline(); + colors(); + icons(); + editPaletteButton(); + + setColorsAndIcons(); + updatePosition(); + closeOnEscape(); + + + VHierarchy.goInfoCache.Clear(); + + EditorApplication.RepaintHierarchyWindow(); + + } + + static float iconSize => 14; + static float iconSpacing => 6; + static float cellSize => iconSize + iconSpacing; + static float spaceAfterColors => 13; + public float rowSpacing = 1; + static float paddingX => 12; + static float paddingY => 12; + + Color windowBackground => isDarkTheme ? Greyscale(.23f) : Greyscale(.75f); + Color selectedBackground => isDarkTheme ? new Color(.3f, .5f, .7f, .8f) : new Color(.3f, .5f, .7f, .6f) * 1.25f; + Color hoveredBackground => isDarkTheme ? Greyscale(1, .3f) : Greyscale(0, .1f); + + public Vector2 targetPosition; + public Vector2 currentPosition; + Vector2 positionDeriv; + float deltaTime; + double lastLayoutTime; + + + + + + + void SetIcon(string iconNameOrGuid, bool isRecursive) + { + foreach (var r in goDatas) + { + r.isIconRecursive = isRecursive; // setting it firstbecause iconNameOrGuid setter relies on isIconRecursive + r.iconNameOrGuid = iconNameOrGuid; + } + } + void SetColor(int colorIndex, bool isRecursive) + { + foreach (var r in goDatas) + { + r.isColorRecursive = isRecursive; + r.colorIndex = colorIndex; + } + } + + void SetInitialIcons() + { + for (int i = 0; i < goDatas.Count; i++) + { + goDatas[i].isIconRecursive = isIconRecursives_initial[i]; + goDatas[i].iconNameOrGuid = iconNamesOrGuids_initial[i]; + } + } + void SetInitialColors() + { + for (int i = 0; i < goDatas.Count; i++) + { + goDatas[i].isColorRecursive = isColorRecursives_initial[i]; + goDatas[i].colorIndex = colorIndexes_initial[i]; + } + } + + void RemoveEmptyGoDatas() + { + var toRemove = goDatas.Where(r => r.iconNameOrGuid == "" && r.colorIndex == 0); + + foreach (var goData in toRemove) + goData.sceneData.goDatas_byGlobalId.RemoveValue(goData); + + if (toRemove.Any()) + Undo.CollapseUndoOperations(Undo.GetCurrentGroup() - 1); + + } + + void RecordUndoOnDatas() + { + if (usingDataSO) + if (data) + data.RecordUndo(); + + foreach (var r in usedDataComponents) + r.RecordUndo(); + + } + void MarkDatasDirty() + { + if (usingDataSO) + if (data) + data.Dirty(); + + foreach (var r in usedDataComponents) + r.Dirty(); + } + void SaveData() + { + // if (usingDataSO) + // data.Save(); + } + + bool usingDataSO => !gameObjects.Select(r => r.scene).All(r => VHierarchy.dataComponents_byScene.GetValueOrDefault(r) != null); + IEnumerable usedDataComponents => VHierarchy.dataComponents_byScene.Where(kvp => kvp.Value && gameObjects.Select(r => r.scene).Contains(kvp.Key)).Select(kvp => kvp.Value); + + + + + + + + void OnLostFocus() + { + if (curEvent.holdingAlt && EditorWindow.focusedWindow?.GetType().Name == "SceneHierarchyWindow") + CloseNextFrameIfNotRefocused(); + else + Close(); + + } + + void CloseNextFrameIfNotRefocused() + { + EditorApplication.delayCall += () => { if (EditorWindow.focusedWindow != this) Close(); }; + } + + + + + static void RepaintOnAlt() // Update + { + if (curEvent.holdingAlt != wasHoldingAlt) + if (EditorWindow.mouseOverWindow is VHierarchyPaletteWindow paletteWindow) + paletteWindow.Repaint(); + + wasHoldingAlt = curEvent.holdingAlt; + + } + + static bool wasHoldingAlt; + + + + + + + + + + public void Init(List gameObjects) + { + void createData() + { + if (VHierarchy.data) return; + + VHierarchy.data = ScriptableObject.CreateInstance(); + + AssetDatabase.CreateAsset(VHierarchy.data, GetScriptPath("VHierarchy").GetParentPath().CombinePath("vHierarchy Data.asset")); + + } + void createPalette() + { + if (VHierarchy.palette) return; + + VHierarchy.palette = ScriptableObject.CreateInstance(); + + AssetDatabase.CreateAsset(VHierarchy.palette, GetScriptPath("VHierarchy").GetParentPath().CombinePath("vHierarchy Palette.asset")); + + } + void setSize() + { + if (!palette.colorsEnabled && !palette.iconRows.Any(r => r.enabled && !r.isEmpty)) // somehow happened on first palette window opening in 2022.3.50 + palette.InvokeMethod("Reset"); + + + + var rowCellCounts = new List(); + + if (palette.colorsEnabled) + rowCellCounts.Add(palette.colors.Count + 1); + + foreach (var r in palette.iconRows.Where(r => r.enabled && !r.isEmpty)) + rowCellCounts.Add(r.iconCount + (r == palette.iconRows.First(r => r.enabled) ? 1 : 0)); + + var width = paddingX + + rowCellCounts.Max() * cellSize + + (rowCellCounts.Last() == rowCellCounts.Max() ? cellSize : 0) + + paddingX; + + + + var iconRowCount = palette.iconRows.Count(r => r.enabled && !r.isEmpty); + + var height = paddingY + + (palette.colorsEnabled ? cellSize : 0) + + (palette.colorsEnabled && palette.iconRows.Any(r => r.enabled && !r.isEmpty) ? spaceAfterColors : 0) + + cellSize * iconRowCount + + rowSpacing * (iconRowCount - 1) + + paddingY; + + + position = position.SetSize(width, height).SetPos(targetPosition); + + } + void getInfos() + { + goDatas.Clear(); + + foreach (var r in gameObjects) + goDatas.Add(VHierarchy.GetGameObjectData(r, createDataIfDoesntExist: true)); + + } + void getInitialState() + { + iconNamesOrGuids_initial = goDatas.Select(r => r.iconNameOrGuid).ToList(); + colorIndexes_initial = goDatas.Select(r => r.colorIndex).ToList(); + + isIconRecursives_initial = goDatas.Select(r => r.isIconRecursive).ToList(); + isColorRecursives_initial = goDatas.Select(r => r.isColorRecursive).ToList(); + + + } + + + this.gameObjects = gameObjects; + + RecordUndoOnDatas(); + + createData(); + createPalette(); + setSize(); + getInfos(); + getInitialState(); + + EditorApplication.update -= RepaintOnAlt; + EditorApplication.update += RepaintOnAlt; + + } + + void OnDestroy() + { + RemoveEmptyGoDatas(); + MarkDatasDirty(); + SaveData(); + + EditorApplication.update -= RepaintOnAlt; + + } + + public List gameObjects = new(); + public List goDatas = new(); + + public List iconNamesOrGuids_initial = new(); + public List colorIndexes_initial = new(); + + public List isIconRecursives_initial = new(); + public List isColorRecursives_initial = new(); + + static VHierarchyPalette palette => VHierarchy.palette; + static VHierarchyData data => VHierarchy.data; + + + + + + + + public static void CreateInstance(Vector2 position) + { + instance = ScriptableObject.CreateInstance(); + + instance.ShowPopup(); + + instance.position = instance.position.SetPos(position).SetSize(200, 300); + instance.targetPosition = position; + + } + + public static VHierarchyPaletteWindow instance; + + } +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchyPaletteWindow.cs.meta b/vfolders2/vHierarchy/VHierarchyPaletteWindow.cs.meta new file mode 100644 index 0000000..c978574 --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchyPaletteWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: 8ae240588f29744208e627125db9c9e4 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/VHierarchySceneSelectorWindow.cs b/vfolders2/vHierarchy/VHierarchySceneSelectorWindow.cs new file mode 100644 index 0000000..4b4e06c --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchySceneSelectorWindow.cs @@ -0,0 +1,1189 @@ +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using UnityEngine; +using UnityEditor; +using UnityEditor.ShortcutManagement; +using System.Reflection; +using System.Linq; +using UnityEngine.UIElements; +using UnityEngine.SceneManagement; +using UnityEditor.SceneManagement; +using UnityEditor.IMGUI.Controls; +using System.Diagnostics; +using Type = System.Type; +using Delegate = System.Delegate; +using Action = System.Action; +using static VHierarchy.VHierarchy; +using static VHierarchy.Libs.VUtils; +using static VHierarchy.Libs.VGUI; +// using static VTools.VDebug; + + +namespace VHierarchy +{ + public class VHierarchySceneSelectorWindow : EditorWindow + { + + void OnGUI() + { + + void background() + { + windowRect.Draw(windowBackground); + + } + void closeOnEscape() + { + if (!curEvent.isKeyDown) return; + if (curEvent.keyCode != KeyCode.Escape) return; + + Close(); + + EditorApplication.RepaintHierarchyWindow(); + + GUIUtility.ExitGUI(); + + } + void addTabOnEnter() + { + // if (!curEvent.isKeyDown) return; // searchfield steals fpcus + if (curEvent.keyCode != KeyCode.Return) return; + + if (keyboardFocusedRowIndex == -1) return; + if (keyboardFocusedEntry == null) return; + + OpenScene(keyboardFocusedEntry, curEvent.holdingAlt); + + this.Close(); + + } + void arrowNavigation() + { + if (!curEvent.isKeyDown) return; + if (curEvent.keyCode != KeyCode.UpArrow && curEvent.keyCode != KeyCode.DownArrow) return; + + curEvent.Use(); + + + if (curEvent.keyCode == KeyCode.UpArrow) + if (keyboardFocusedRowIndex == 0) + keyboardFocusedRowIndex = rowCount - 1; + else + keyboardFocusedRowIndex--; + + if (curEvent.keyCode == KeyCode.DownArrow) + if (keyboardFocusedRowIndex == rowCount - 1) + keyboardFocusedRowIndex = 0; + else + keyboardFocusedRowIndex++; + + + keyboardFocusedRowIndex = keyboardFocusedRowIndex.Clamp(0, rowCount - 1); + + } + void updateSearch() + { + if (searchString == prevSearchString) return; + + prevSearchString = searchString; + + + if (searchString == "") { keyboardFocusedRowIndex = -1; return; } + + UpdateSearch(); + + keyboardFocusedRowIndex = 0; + + } + + + void searchField_() + { + var searchRect = windowRect.SetHeight(18).MoveY(1).AddWidthFromMid(-2); + + + if (searchField == null) + { + searchField = new SearchField(); + searchField.SetFocus(); + + } + + + searchString = searchField.OnGUI(searchRect, searchString); + + } + void rows() + { + void bookmarked() + { + if (searchString != "") return; + if (!bookmarkedEntries.Any()) return; + + bookmarksRect = windowRect.SetHeight(bookmarkedEntries.Count * rowHeight + gaps.Sum()); + + BookmarksGUI(); + + } + void divider() + { + if (searchString != "") return; + if (!bookmarkedEntries.Any()) return; + + var splitterColor = Greyscale(.36f); + var splitterRect = bookmarksRect.SetHeightFromBottom(0).SetHeight(dividerHeight).SetHeightFromMid(1).AddWidthFromMid(-10); + + splitterRect.Draw(splitterColor); + + } + void notBookmarked() + { + if (searchString != "") return; + + if (bookmarkedEntries.Any()) + nextRowY = bookmarksRect.yMax + dividerHeight; + + foreach (var entry in allEntries) + { + if (bookmarkedEntries.Contains(entry)) continue; + if (entry == draggedBookmark) continue; + + RowGUI(windowRect.SetHeight(rowHeight).SetY(nextRowY), entry); + + nextRowY += rowHeight; + nextRowIndex++; + + } + + } + void searched() + { + if (searchString == "") return; + + foreach (var entry in searchedEntries) + { + RowGUI(windowRect.SetHeight(rowHeight).SetY(nextRowY), entry); + + nextRowY += rowHeight; + nextRowIndex++; + + } + + } + + + scrollPos = GUI.BeginScrollView(windowRect.AddHeightFromBottom(-firstRowOffsetTop), Vector2.up * scrollPos, windowRect.SetHeight(scrollAreaHeight), GUIStyle.none, GUIStyle.none).y; + + nextRowY = 0; + nextRowIndex = 0; + + bookmarked(); + divider(); + notBookmarked(); + searched(); + + scrollAreaHeight = nextRowY + 23; + rowCount = nextRowIndex; + + GUI.EndScrollView(); + + } + void noResults() + { + if (searchString == "") return; + if (searchedEntries.Any()) return; + + + GUI.enabled = false; + GUI.skin.label.alignment = TextAnchor.MiddleCenter; + + GUI.Label(windowRect.AddHeightFromBottom(-14), "No results"); + + GUI.skin.label.alignment = TextAnchor.MiddleLeft; + GUI.enabled = true; + + } + + void outline() + { + if (Application.platform == RuntimePlatform.OSXEditor) return; + + position.SetPos(0, 0).DrawOutline(Greyscale(.1f)); + + } + // void resizing() + // { + + // } + + + background(); + closeOnEscape(); + addTabOnEnter(); + arrowNavigation(); + updateSearch(); + + searchField_(); + rows(); + noResults(); + + outline(); + + + if (draggingBookmark || animatingDroppedBookmark || animatingGaps) + this.Repaint(); + + } + + Rect windowRect => position.SetPos(0, 0); + Rect bookmarksRect; + + SearchField searchField; + + Color windowBackground => Greyscale(isDarkTheme ? .23f : .8f); + + string searchString = ""; + string prevSearchString = ""; + + float scrollPos; + + float rowHeight => 22; + float dividerHeight => 11; + float firstRowOffsetTop => bookmarkedEntries.Any() && searchString == "" ? 21 : 20; + + int nextRowIndex; + float nextRowY; + + float scrollAreaHeight = 1232; + int rowCount = 123; + + int keyboardFocusedRowIndex = -1; + + + + + + + + + + void RowGUI(Rect rowRect, SceneEntry entry) + { + + var isHovered = rowRect.IsHovered(); + var isPressed = entry == pressedEntry; + var isDragged = draggingBookmark && draggedBookmark == entry; + var isDropped = animatingDroppedBookmark && droppedBookmark == entry; + var isFocused = entry == keyboardFocusedEntry; + var isBookmarked = bookmarkedEntries.Contains(entry) || entry == draggedBookmark; + + if (isDropped) + isHovered = rowRect.SetY(droppedBookmarkYTarget).IsHovered(); + + + var showBlueBackground = isFocused || isPressed || isDragged; + var showAdditiveIndicator = curEvent.holdingAlt && ((keyboardFocusedEntry?.scenePath).IsNullOrEmpty() ? isHovered : isFocused); + var showStarButton = (isHovered || (isBookmarked && !isFocused)) && !showAdditiveIndicator; + var showEnterHint = (isFocused && !isHovered && isDarkTheme) && !showAdditiveIndicator; + + + void draggedShadow() + { + if (!curEvent.isRepaint) return; + if (!isDragged) return; + + var shadowRect = rowRect.AddHeightFromMid(-4); + + var shadowOpacity = .3f; + var shadowRadius = 13; + + shadowRect.DrawBlurred(Greyscale(0, shadowOpacity), shadowRadius); + + } + void blueBackground() + { + if (!curEvent.isRepaint) return; + if (!showBlueBackground) return; + + + var backgroundRect = rowRect.AddHeightFromMid(-3); + + backgroundRect.Draw(GUIColors.selectedBackground); + + + } + void icon() + { + if (!curEvent.isRepaint) return; + + + Texture iconTexture = EditorIcons.GetIcon("SceneAsset Icon"); + + if (!iconTexture) return; + + + var iconRect = rowRect.SetWidth(16).SetHeightFromMid(16).MoveX(4 + 1); + + iconRect = iconRect.SetWidthFromMid(iconRect.height * iconTexture.width / iconTexture.height); + + + GUI.DrawTexture(iconRect, iconTexture); + + } + void name() + { + if (!curEvent.isRepaint) return; + + + var nameRect = rowRect.MoveX(21 + 1); + + var nameText = searchString != "" ? namesFormattedForFuzzySearch_byEntry[entry] + : entry.sceneName; + + + if (entry.scenePath == sceneToReplace.path) + { + SetLabelBold(); + SetGUIColor(Greyscale(1.15f)); + + GUI.skin.label.richText = true; + + GUI.Label(nameRect, nameText); + + GUI.skin.label.richText = false; + + ResetGUIColor(); + ResetLabelStyle(); + + } + + else + { + var color = showBlueBackground ? Greyscale(123, 123) + : isHovered && !isPressed ? Greyscale(1.1f) + : Greyscale(1); + SetGUIColor(color); + + GUI.skin.label.richText = true; + + GUI.Label(nameRect, nameText); + + GUI.skin.label.richText = false; + + ResetGUIColor(); + } + + } + void curtain() + { + if (!curEvent.isRepaint) return; + + + var curtainWidth = 20; + + var curtainColor = showBlueBackground ? GUIColors.selectedBackground : windowBackground; + + var curtainXMax = rowRect.xMax - (showStarButton ? 21 : showEnterHint ? 33 : showAdditiveIndicator ? 80 : 0); + + + rowRect.SetHeightFromMid(18).SetXMax(curtainXMax).SetWidthFromRight(curtainWidth).DrawCurtainLeft(curtainColor); + + rowRect.SetHeightFromMid(18).SetX(curtainXMax).SetXMax(rowRect.xMax).Draw(curtainColor); + + } + void starButton() + { + if (!showStarButton) return; + + + var buttonRect = rowRect.SetWidthFromRight(16).MoveX(-6 + 1).SetSizeFromMid(rowHeight); + + + var iconName = isBookmarked ^ buttonRect.IsHovered() ? "Star" : "Star Hollow"; + var iconSize = 16; + var colorNormal = Greyscale(isDarkTheme ? (isBookmarked ? .5f : .7f) : .3f); + var colorHovered = Greyscale(isDarkTheme ? (isBookmarked ? .9f : 1) : 0f); + var colorPressed = Greyscale(isDarkTheme ? .75f : .5f); + var colorDisabled = Greyscale(isDarkTheme ? .53f : .55f); + + + if (!IconButton(buttonRect, iconName, iconSize, colorNormal, colorHovered, colorPressed)) return; + + if (isBookmarked) + bookmarkedEntries.Remove(entry); + else + bookmarkedEntries.Add(entry); + + } + void enterHint() + { + if (!curEvent.isRepaint) return; + if (!showEnterHint) return; + + + var hintRect = rowRect.SetWidthFromRight(33); + + + SetLabelFontSize(10); + SetGUIColor(Greyscale(.9f)); + + GUI.Label(hintRect, "Enter"); + + ResetGUIColor(); + ResetLabelStyle(); + + + } + void additiveIndicator() + { + if (!curEvent.isRepaint) return; + if (!showAdditiveIndicator) return; + + + var indicatorRect = rowRect.SetWidthFromRight(81); + + + SetLabelFontSize(10); + SetGUIColor(Greyscale(.9f)); + + GUI.Label(indicatorRect, "Load additively"); + + ResetGUIColor(); + ResetLabelStyle(); + + } + void hoverHighlight() + { + if (!curEvent.isRepaint) return; + if (!isHovered) return; + if (isPressed || isDragged) return; + + + var backgroundRect = rowRect.AddHeightFromMid(-2); + + var backgroundColor = Greyscale(isDarkTheme ? 1 : 0, isPressed ? .085f : .12f); + + + backgroundRect.Draw(backgroundColor); + + } + + void mouse() + { + void down() + { + if (!curEvent.isMouseDown) return; + if (!rowRect.IsHovered()) return; + + isMousePressedOnEntry = true; + pressedEntry = entry; + + mouseDownPosition = curEvent.mousePosition; + + this.Repaint(); + + } + void up() + { + if (!curEvent.isMouseUp) return; + + isMousePressedOnEntry = false; + pressedEntry = null; + + this.Repaint(); + + + if (!isHovered) return; + if (draggingBookmark) return; + if ((curEvent.mousePosition - mouseDownPosition).magnitude > 2) return; + + curEvent.Use(); + + OpenScene(entry, curEvent.holdingAlt); + + this.Close(); + + } + + down(); + up(); + + } + void setFocusedEntry() + { + var rowIndex = (rowRect.y / rowHeight).FloorToInt(); + + if (rowIndex == keyboardFocusedRowIndex) + keyboardFocusedEntry = entry; + + } + + + rowRect.MarkInteractive(); + + draggedShadow(); + blueBackground(); + icon(); + name(); + curtain(); + starButton(); + enterHint(); + additiveIndicator(); + hoverHighlight(); + mouse(); + setFocusedEntry(); + + } + + SceneEntry pressedEntry; + + bool isMousePressedOnEntry; + + Vector2 mouseDownPosition; + + SceneEntry keyboardFocusedEntry; + + + + + void OpenScene(SceneEntry entry, bool openAdditive) + { + if (!Application.isPlaying) + if (!EditorSceneManager.SaveCurrentModifiedScenesIfUserWantsTo()) return; // if clicked cancel + + + void openScene(string path, bool additive) + { + if (!EditorApplication.isPlaying) + EditorSceneManager.OpenScene(path, additive ? OpenSceneMode.Additive : OpenSceneMode.Single); + else + SceneManager.LoadScene(path, additive ? LoadSceneMode.Additive : LoadSceneMode.Single); + + } + + + + if (openAdditive) + openScene(entry.scenePath, additive: true); + + else if (EditorSceneManager.sceneCount == 1) + openScene(entry.scenePath, additive: false); + + else + { + var openedScene = EditorSceneManager.OpenScene(entry.scenePath, OpenSceneMode.Additive); + + EditorSceneManager.MoveSceneAfter(openedScene, sceneToReplace); + + if (EditorSceneManager.GetActiveScene() == sceneToReplace) + EditorSceneManager.SetActiveScene(openedScene); + + EditorSceneManager.CloseScene(sceneToReplace, removeScene: true); + + } + + + } + + + + + + + + + + + + + public void BookmarksGUI() + { + void normalBookmark(int i) + { + if (bookmarkedEntries[i] == droppedBookmark && animatingDroppedBookmark) return; + + var bookmarkRect = bookmarksRect.SetHeight(rowHeight) + .SetY(GetBookmarY(i)); + + RowGUI(bookmarkRect, bookmarkedEntries[i]); + + } + void normalBookmarks() + { + for (int i = 0; i < bookmarkedEntries.Count; i++) + normalBookmark(i); + + } + void draggedBookmark_() + { + if (!draggingBookmark) return; + + + var bookmarkRect = bookmarksRect.SetHeight(rowHeight) + .SetY(draggedBookmarkY); + + RowGUI(bookmarkRect, draggedBookmark); + + } + void droppedBookmark_() + { + if (!animatingDroppedBookmark) return; + + var bookmarkRect = bookmarksRect.SetHeight(rowHeight) + .SetY(droppedBookmarkY); + + RowGUI(bookmarkRect, droppedBookmark); + + } + + + BookmarksDragging(); + BookmarksAnimations(); + + normalBookmarks(); + draggedBookmark_(); + droppedBookmark_(); + + } + + int GetBookmarkIndex(float mouseY) + { + return ((mouseY - bookmarksRect.y) / rowHeight).FloorToInt(); + } + + float GetBookmarY(int i, bool includeGaps = true) + { + var centerY = bookmarksRect.y + + i * rowHeight + + (includeGaps ? gaps.Take(i + 1).Sum() : 0); + + + return centerY; + + } + + + + + + + + + + void BookmarksDragging() + { + void init() + { + if (draggingBookmark) return; + if ((curEvent.mousePosition - mouseDownPosition).magnitude <= 2) return; + + if (!isMousePressedOnEntry) return; + if (!bookmarkedEntries.Contains(pressedEntry)) return; + + + var i = GetBookmarkIndex(mouseDownPosition.y); + + if (i >= bookmarkedEntries.Count) return; + if (i < 0) return; + + + animatingDroppedBookmark = false; + + draggingBookmark = true; + + draggedBookmark = bookmarkedEntries[i]; + draggedBookmarkHoldOffsetY = GetBookmarY(i) - mouseDownPosition.y; + + gaps[i] = rowHeight; + + + this.RecordUndo(); + + bookmarkedEntries.Remove(draggedBookmark); + + } + void update() + { + if (!draggingBookmark) return; + + + EditorGUIUtility.hotControl = EditorGUIUtility.GetControlID(FocusType.Passive); + + draggedBookmarkY = (curEvent.mousePosition.y + draggedBookmarkHoldOffsetY).Clamp(0, bookmarksRect.yMax - rowHeight); + + insertDraggedBookmarkAtIndex = GetBookmarkIndex(curEvent.mousePosition.y + draggedBookmarkHoldOffsetY + rowHeight / 2).Clamp(0, bookmarkedEntries.Count); + + } + void accept() + { + if (!draggingBookmark) return; + if (!curEvent.isMouseUp && !curEvent.isIgnore) return; + + curEvent.Use(); + EditorGUIUtility.hotControl = 0; + + // DragAndDrop.PrepareStartDrag(); // fixes phantom dragged component indicator after reordering bookmarks + + this.RecordUndo(); + + draggingBookmark = false; + isMousePressedOnEntry = false; + + bookmarkedEntries.AddAt(draggedBookmark, insertDraggedBookmarkAtIndex); + + gaps[insertDraggedBookmarkAtIndex] -= rowHeight; + gaps.AddAt(0, insertDraggedBookmarkAtIndex); + + droppedBookmark = draggedBookmark; + + droppedBookmarkY = draggedBookmarkY; + droppedBookmarkYDerivative = 0; + animatingDroppedBookmark = true; + + draggedBookmark = null; + pressedEntry = null; + + EditorGUIUtility.hotControl = 0; + + } + + init(); + accept(); + update(); + + } + + bool draggingBookmark; + + float draggedBookmarkHoldOffsetY; + + float draggedBookmarkY; + int insertDraggedBookmarkAtIndex; + + SceneEntry draggedBookmark; + SceneEntry droppedBookmark; + + + + + + + void BookmarksAnimations() + { + if (!curEvent.isLayout) return; + + void gaps_() + { + var makeSpaceForDraggedBookmark = draggingBookmark; + + // var lerpSpeed = 1; + var lerpSpeed = 11; + + for (int i = 0; i < gaps.Count; i++) + if (makeSpaceForDraggedBookmark && i == insertDraggedBookmarkAtIndex) + gaps[i] = MathUtil.Lerp(gaps[i], rowHeight, lerpSpeed, editorDeltaTime); + else + gaps[i] = MathUtil.Lerp(gaps[i], 0, lerpSpeed, editorDeltaTime); + + + + for (int i = 0; i < gaps.Count; i++) + if (gaps[i].Approx(0)) + gaps[i] = 0; + + + + animatingGaps = gaps.Any(r => r > .1f); + + + } + void droppedBookmark_() + { + if (!animatingDroppedBookmark) return; + + // var lerpSpeed = 1; + var lerpSpeed = 8; + + droppedBookmarkYTarget = GetBookmarY(bookmarkedEntries.IndexOf(droppedBookmark), includeGaps: false); + + MathUtil.SmoothDamp(ref droppedBookmarkY, droppedBookmarkYTarget, lerpSpeed, ref droppedBookmarkYDerivative, editorDeltaTime); + + if ((droppedBookmarkY - droppedBookmarkYTarget).Abs() < .5f) + animatingDroppedBookmark = false; + + } + + gaps_(); + droppedBookmark_(); + + } + + float droppedBookmarkY; + float droppedBookmarkYTarget; + float droppedBookmarkYDerivative; + + bool animatingDroppedBookmark; + bool animatingGaps; + + List gaps + { + get + { + while (_gaps.Count < bookmarkedEntries.Count + 1) _gaps.Add(0); + while (_gaps.Count > bookmarkedEntries.Count + 1) _gaps.RemoveLast(); + + return _gaps; + + } + } + List _gaps = new(); + + + + + + + + + + + + + + + + + + + + + public static void UpdateAllEntries() + { + void fillWithAllScenes() + { + + allEntries.Clear(); + + allEntries = AssetDatabase.FindAssets("t:scene") + .Select(r => new SceneEntry() { scenePath = r.ToPath() }) + .ToList(); + + allEntries.SortBy(r => r.scenePath.GetFilename()); + + } + // void removeAllOpen() + // { + // for (int i = 0; i < EditorSceneManager.sceneCount; i++) + // allEntries.RemoveAll(r => r.scenePath == EditorSceneManager.GetSceneAt(i).path); + // } + + fillWithAllScenes(); + // removeAllOpen(); + + } + + static List allEntries = new(); + + [System.Serializable] + public class SceneEntry + { + public string scenePath = ""; + + public string sceneName => scenePath.GetFilename(withExtension: false); + + } + + + + void GetBookmarkedEntries() + { + bookmarkedEntries = data.bookmarkedScenePaths.Select(r => allEntries.FirstOrDefault(rr => rr.scenePath == r)) + .Where(r => r != null) + .ToList(); + } + void SaveBookmarkedEntries() + { + data.bookmarkedScenePaths = bookmarkedEntries.Select(r => r.scenePath).ToList(); + + data.Dirty(); + + } + + List bookmarkedEntries = new(); + + + + void OnEnable() { UpdateAllEntries(); GetBookmarkedEntries(); } + + void OnDisable() { SaveBookmarkedEntries(); } + + + + + + + + + + + void UpdateSearch() + { + + bool tryMatch(string name, string query, int[] matchIndexes, ref float cost) + { + + var wordInitialsIndexes = new List { 0 }; + + for (int i = 1; i < name.Length; i++) + { + var separators = new[] { ' ', '-', '_', '.', '(', ')', '[', ']', }; + + var prevChar = name[i - 1]; + var curChar = name[i]; + var nextChar = i + 1 < name.Length ? name[i + 1] : default(char); + + var isSeparatedWordStart = separators.Contains(prevChar) && !separators.Contains(curChar); + var isCamelcaseHump = (curChar.IsUpper() && prevChar.IsLower()) || (curChar.IsUpper() && nextChar.IsLower()); + var isNumberStart = curChar.IsDigit() && (!prevChar.IsDigit() || prevChar == '0'); + var isAfterNumber = prevChar.IsDigit() && !curChar.IsDigit(); + + if (isSeparatedWordStart || isCamelcaseHump || isNumberStart || isAfterNumber) + wordInitialsIndexes.Add(i); + + } + + + + var nextWordInitialsIndexMap = new int[name.Length]; + + var nextWordIndex = 0; + + for (int i = 0; i < name.Length; i++) + { + if (i == wordInitialsIndexes[nextWordIndex]) + if (nextWordIndex + 1 < wordInitialsIndexes.Count) + nextWordIndex++; + else break; + + nextWordInitialsIndexMap[i] = wordInitialsIndexes[nextWordIndex]; + + } + + + + + + var iName = 0; + var iQuery = 0; + + var prevMatchIndex = -1; + + void registerMatch(int matchIndex) + { + matchIndexes[iQuery] = matchIndex; + iQuery++; + + iName = matchIndex + 1; + + prevMatchIndex = matchIndex; + + + } + + + cost = 0; + + while (iName < name.Length && iQuery < query.Length) + { + var curQuerySymbol = query[iQuery].ToLower(); + var curNameSymbol = name[iName].ToLower(); + + if (curNameSymbol == curQuerySymbol) + { + var gapLength = iName - prevMatchIndex - 1; + + cost += gapLength; + + + registerMatch(iName); + + continue; + + // consecutive matches cost 0 + // distance between index 0 and first match also counts as a gap + + } + + + + var nextWordInitialIndex = nextWordInitialsIndexMap[iName]; // wordInitialsIndexes.FirstOrDefault(i => i > iName); + var nextWordInitialSymbol = nextWordInitialIndex == default ? default : name[nextWordInitialIndex].ToLower(); + + if (nextWordInitialSymbol == curQuerySymbol) + { + var gapLength = nextWordInitialIndex - prevMatchIndex - 1; + + cost += (gapLength * .01f).ClampMax(.9f); + + + registerMatch(nextWordInitialIndex); + + continue; + + // word-initial match costs less than a gap (1+) + // but more than a consecutive match (0) + + } + + + + iName++; + + } + + + + + + + var allCharsMatched = iQuery >= query.Length; + + return allCharsMatched; + + + + // this search works great in practice + // but fails in more theoretical scenarios, mostly when user skips first letters of words + // eg searching "arn" won't find "barn_a" because search will jump to last a (word-initial) and fail afterwards + // so unity search is used as a fallback + + } + bool tryMatch_unitySearch(string name, string query, int[] matchIndexes, ref float cost) + { + long score = 0; + + List matchIndexesList = new(); + + + var matched = UnityEditor.Search.FuzzySearch.FuzzyMatch(searchString, name, ref score, matchIndexesList); + + + for (int i = 0; i < matchIndexesList.Count; i++) + matchIndexes[i] = matchIndexesList[i]; + + cost = 123212 - score; + + + return matched; + + + // this search is fast but isn't tuned for real use cases + // quering "vis" ranks "Invisible" higher than "VInspectorState" + // quering "lst" ranks "SmallShadowTemp" higher than "List" + // also sometimes it favors matches that are further away from zeroth index + + } + + string formatName(string name, IEnumerable matchIndexes) + { + var formattedName = ""; + + for (int i = 0; i < name.Length; i++) + if (matchIndexes.Contains(i)) + formattedName += "" + name[i] + ""; + else + formattedName += name[i]; + + + return formattedName; + + } + + + + var costs_byEntry = new Dictionary(); + + var matchIndexes = new int[searchString.Length]; + var matchCost = 0f; + + + foreach (var entry in allEntries) + if (tryMatch(entry.sceneName, searchString, matchIndexes, ref matchCost) || tryMatch_unitySearch(entry.sceneName, searchString, matchIndexes, ref matchCost)) + { + costs_byEntry[entry] = matchCost; + namesFormattedForFuzzySearch_byEntry[entry] = formatName(entry.sceneName, matchIndexes); + } + + + searchedEntries = costs_byEntry.Keys.OrderBy(r => costs_byEntry[r]) + .ThenBy(r => r.sceneName) + .ToList(); + } + + List searchedEntries = new(); + + Dictionary namesFormattedForFuzzySearch_byEntry = new(); + + + + + + + + void OnLostFocus() + { + EditorApplication.delayCall += () => + { + if (EditorWindow.focusedWindow != this) + { + EditorApplication.RepaintHierarchyWindow(); + + Close(); + } + }; + + // delay is needed to prevent reopening after clicking + button for the second time + } + + + + public static void Open(Vector2 rowPos, Scene sceneToReplace) + { + if (!VHierarchy.data) + { + VHierarchy.data = ScriptableObject.CreateInstance(); + + AssetDatabase.CreateAsset(VHierarchy.data, GetScriptPath("VHierarchy").GetParentPath().CombinePath("vHierarchy Data.asset")); + } + + + instance = ScriptableObject.CreateInstance(); + + instance.ShowPopup(); + instance.Focus(); + + + + var width = 190; + var height = 296; + + var offsetX = -14; + var offsetY = 18; + + instance.position = instance.position.SetPos(rowPos + new Vector2(offsetX, offsetY)).SetSize(width, height); + + + + instance.sceneToReplace = sceneToReplace; + + } + + public Scene sceneToReplace; + + public static VHierarchySceneSelectorWindow instance; + + + } +} +#endif \ No newline at end of file diff --git a/vfolders2/vHierarchy/VHierarchySceneSelectorWindow.cs.meta b/vfolders2/vHierarchy/VHierarchySceneSelectorWindow.cs.meta new file mode 100644 index 0000000..6751b5c --- /dev/null +++ b/vfolders2/vHierarchy/VHierarchySceneSelectorWindow.cs.meta @@ -0,0 +1,11 @@ +fileFormatVersion: 2 +guid: d50f2aa4a50724ddbaf21dca1fd4b816 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: diff --git a/vfolders2/vHierarchy/vHierarchy Data.asset b/vfolders2/vHierarchy/vHierarchy Data.asset new file mode 100644 index 0000000..c8b0bb2 --- /dev/null +++ b/vfolders2/vHierarchy/vHierarchy Data.asset @@ -0,0 +1,19 @@ +%YAML 1.1 +%TAG !u! tag:unity3d.com,2011: +--- !u!114 &11400000 +MonoBehaviour: + m_ObjectHideFlags: 0 + m_CorrespondingSourceObject: {fileID: 0} + m_PrefabInstance: {fileID: 0} + m_PrefabAsset: {fileID: 0} + m_GameObject: {fileID: 0} + m_Enabled: 1 + m_EditorHideFlags: 0 + m_Script: {fileID: 11500000, guid: 3a9752b0c8e144801967e6897679604b, type: 3} + m_Name: vHierarchy Data + m_EditorClassIdentifier: + sceneDatas_byGuid: + keys: [] + values: [] + bookmarks: [] + bookmarkedScenePaths: [] diff --git a/vfolders2/vHierarchy/vHierarchy Data.asset.meta b/vfolders2/vHierarchy/vHierarchy Data.asset.meta new file mode 100644 index 0000000..5eaf354 --- /dev/null +++ b/vfolders2/vHierarchy/vHierarchy Data.asset.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: ef754f649a1a02340809631806828462 +NativeFormatImporter: + externalObjects: {} + mainObjectFileID: 11400000 + userData: + assetBundleName: + assetBundleVariant: