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 0000000..754aee7 Binary files /dev/null and b/vfolders2/vHierarchy/Read me.pdf differ 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: