diff --git a/Assets/vTabs.meta b/Assets/vTabs.meta new file mode 100644 index 0000000..69ef6c3 --- /dev/null +++ b/Assets/vTabs.meta @@ -0,0 +1,8 @@ +fileFormatVersion: 2 +guid: baa6e88cc47804cafb29dc337c1e639b +folderAsset: yes +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: diff --git a/Assets/vTabs/Read me.pdf b/Assets/vTabs/Read me.pdf new file mode 100644 index 0000000..490eb6d Binary files /dev/null and b/Assets/vTabs/Read me.pdf differ diff --git a/Assets/vTabs/Read me.pdf.meta b/Assets/vTabs/Read me.pdf.meta new file mode 100644 index 0000000..a689071 --- /dev/null +++ b/Assets/vTabs/Read me.pdf.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 01fbcccbc9a1a44c2bf42479f17dfb69 +DefaultImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 253396 + packageName: vTabs 2 + packageVersion: 2.1.6 + assetPath: Assets/vTabs/Read me.pdf + uploadId: 874244 diff --git a/Assets/vTabs/VTabs.asmdef b/Assets/vTabs/VTabs.asmdef new file mode 100644 index 0000000..5a2a8d3 --- /dev/null +++ b/Assets/vTabs/VTabs.asmdef @@ -0,0 +1,16 @@ +{ + "name": "VTabs", + "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/Assets/vTabs/VTabs.asmdef.meta b/Assets/vTabs/VTabs.asmdef.meta new file mode 100644 index 0000000..8a343a2 --- /dev/null +++ b/Assets/vTabs/VTabs.asmdef.meta @@ -0,0 +1,14 @@ +fileFormatVersion: 2 +guid: 06ab1c341392b4a318f67b84e0da6aab +AssemblyDefinitionImporter: + externalObjects: {} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 253396 + packageName: vTabs 2 + packageVersion: 2.1.6 + assetPath: Assets/vTabs/VTabs.asmdef + uploadId: 874244 diff --git a/Assets/vTabs/VTabs.cs b/Assets/vTabs/VTabs.cs new file mode 100644 index 0000000..97625df --- /dev/null +++ b/Assets/vTabs/VTabs.cs @@ -0,0 +1,1571 @@ +#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 System.Diagnostics; +using Type = System.Type; +using Delegate = System.Delegate; +using Action = System.Action; +using static VTabs.Libs.VUtils; +using static VTabs.Libs.VGUI; +// using static VTools.VDebug; + + + +namespace VTabs +{ + public static class VTabs + { + + static void UpdateGUIs() + { + foreach (var dockArea in allDockAreas) + if (!guis_byDockArea.ContainsKey(dockArea)) + guis_byDockArea[dockArea] = new VTabsGUI(dockArea); + + foreach (var dockArea in guis_byDockArea.Keys.ToList().Where(r => !r)) + guis_byDockArea.Remove(dockArea); + + + + foreach (var gui in guis_byDockArea.Values) + { + gui.UpdateScrollAnimation(); + gui.UpdateLockButtonHiding(); + } + + + } + + public static Dictionary guis_byDockArea = new(); + + + + + + + + public static void UpdateStyleSheet() + { + if (!Application.unityVersion.StartsWith("6000")) return; + + + void updatePluginFolderPath() + { + if (AssetDatabase.LoadAssetAtPath(pluginFolderPath.CombinePath("VTabs.cs")) is not null) return; + + + var mainScriptPath = GetScriptPath(nameof(VTabs)); + + ProjectPrefs.SetString("vTabs-plugin-folder-path", mainScriptPath.GetParentPath()); + + } + + void generate() + { + var s = ""; + + void addComment() + { + s += + @" +/* This file is generated by vTabs to modify tab style */ +/* Feel free to remove it from version control */ + "; + } + + void addLargeTabs() + { + if (!VTabsMenu.largeTabStyleEnabled) return; + + s += + @" +/* tab text */ +.dragtab +{ + padding-left: 10px; +} + +/* tab itself */ +.tab +{ + padding-right: 27px; +} + "; + } + void addNeatTabs() + { + if (!VTabsMenu.neatTabStyleEnabled) return; + + s += + @" +/* tab text */ +.dragtab +{ + padding-left: 10px; +} + +/* tab itself */ +.tab +{ + padding-right: -1px; +} + "; + } + + void addClassicBackground_dark() + { + if (!VTabsMenu.classicBackgroundEnabled) return; + if (!isDarkTheme) return; + + s += + @" +/* background */ +.dockarea +{ + background-color: #262626; +} + +/* tab text */ +.dragtab +{ + color: #dedede; +} + +/* top and bottom bars */ +.AppToolbar +{ + background-color: #181818; +} + "; + } + void addClassicBackground_light() + { + if (!VTabsMenu.classicBackgroundEnabled) return; + if (isDarkTheme) return; + + s += + @" +/* background */ +.dockarea +{ + background-color: #a9a9a9; +} + +/* top and bottom bars */ +.AppToolbar +{ + background-color: #888888; +} + "; + } + void addGreyBackground() + { + if (!VTabsMenu.greyBackgroundEnabled) return; + + s += + @" +/* background */ +.dockarea +{ + background-color: #222222; +} + +/* tab text */ +.dragtab +{ + color: #dedede; +} + +/* top and bottom bars */ +.AppToolbar +{ + background-color: #222222; +} + "; + } + + void save() + { + styleSheetPath.EnsureDirExists(); + + System.IO.File.WriteAllText(styleSheetPath, s); + + AssetDatabase.ImportAsset(styleSheetPath); + + } + void addMetadata() + { + var importer = AssetImporter.GetAtPath(styleSheetPath); + + importer.userData = VTabsMenu.tabStyle + " " + VTabsMenu.backgroundStyle + " " + (isDarkTheme ? 1 : 0); + + importer.Dirty(); + importer.SaveAndReimport(); + + } + + + addComment(); + + addLargeTabs(); + addNeatTabs(); + + addClassicBackground_dark(); + addClassicBackground_light(); + addGreyBackground(); + + save(); + addMetadata(); + + EditorUtility.RequestScriptReload(); + + } + void delete() + { + System.IO.Directory.Delete(styleSheetsFolderPath, recursive: true); + + System.IO.File.Delete(styleSheetsFolderPath + ".meta"); + + AssetDatabase.Refresh(); + + } + void update() + { + var importer = AssetImporter.GetAtPath(styleSheetPath); + + if (importer.userData == null || importer.userData.Length != 5) return; + + + var tabStyle = (int)char.GetNumericValue(importer.userData[0]); + var backgroundStyle = (int)char.GetNumericValue(importer.userData[2]); + var wasDarkTheme = (int)char.GetNumericValue(importer.userData[4]) == 1; + + if (tabStyle != VTabsMenu.tabStyle || backgroundStyle != VTabsMenu.backgroundStyle || isDarkTheme != wasDarkTheme) + generate(); + + } + + + updatePluginFolderPath(); + + var hasStyleSheet = AssetDatabase.LoadAssetAtPath(styleSheetPath) != null; + var shouldHaveStyleSheet = (VTabsMenu.tabStyle != 0 || VTabsMenu.backgroundStyle != 0) && !VTabsMenu.pluginDisabled; + + + if (shouldHaveStyleSheet && !hasStyleSheet) generate(); + if (!shouldHaveStyleSheet && hasStyleSheet) delete(); + + if (shouldHaveStyleSheet && hasStyleSheet) update(); + + } + + static string pluginFolderPath => ProjectPrefs.GetString("vTabs-plugin-folder-path"); + static string styleSheetsFolderPath => pluginFolderPath.CombinePath("StyleSheets"); + static string styleSheetPath => pluginFolderPath.CombinePath("StyleSheets/Extensions/common.uss"); + + + + + + + + + + + + static void Shortcuts() // globalEventHandler + { + if (!curEvent.isKeyDown) return; + if (EditorWindow.focusedWindow?.GetType() == t_GameView && t_ShortcutIntegrations.GetMemberValue("ignoreWhenPlayModeFocused") && Application.isPlaying) return; + + void addTab() + { + if (curEvent.keyCode != KeyCode.T) return; + if (curEvent.modifiers != EventModifiers.Control + && curEvent.modifiers != EventModifiers.Command) return; + + if (!VTabsMenu.addTabShortcutEnabled) return; + + if (EditorWindow.mouseOverWindow.GetDockArea() is not Object dockArea) return; + if (!guis_byDockArea.TryGetValue(dockArea, out var gui)) return; + + + VTabsAddTabWindow.Open(dockArea); + + EditorWindow.mouseOverWindow.Repaint(); // for + button to light up + + curEvent.Use(); + + } + void closeTab() + { + if (curEvent.keyCode != KeyCode.W) return; + if (curEvent.modifiers != EventModifiers.Control + && curEvent.modifiers != EventModifiers.Command) return; + + if (!VTabsMenu.closeTabShortcutEnabled) return; + + if (EditorWindow.mouseOverWindow.GetDockArea() is not Object dockArea) return; + if (!guis_byDockArea.TryGetValue(dockArea, out var gui)) return; + + if (gui.tabs.Count == 1) return; + + + gui.CloseTab(gui.activeTab); + + curEvent.Use(); + + } + void reopenTab() + { + if (curEvent.keyCode != KeyCode.T) return; + if (curEvent.modifiers != (EventModifiers.Command | EventModifiers.Shift) + && curEvent.modifiers != (EventModifiers.Control | EventModifiers.Shift)) return; + + if (!VTabsMenu.reopenTabShortcutEnabled) return; + + if (EditorWindow.mouseOverWindow.GetDockArea() is not Object dockArea) return; + if (!guis_byDockArea.TryGetValue(dockArea, out var gui)) return; + + + gui.ReopenClosedTab(); + + curEvent.Use(); + + } + + addTab(); + closeTab(); + reopenTab(); + + } + + + + + + + + + + + + + + + + + static void UpdateBrowserTitle(EditorWindow browser) + { + if (mi_VFavorites_CanBrowserBeWrapped != null && mi_VFavorites_CanBrowserBeWrapped.Invoke(null, new[] { browser }).Equals(false)) return; + + var isLocked = browser.GetMemberValue("isLocked"); + var isTitleDefault = browser.titleContent.text == "Project"; + + void setLockedTitle() + { + if (!isLocked) return; + + var isOneColumn = browser.GetMemberValue("m_ViewMode") == 0; + + var path = isOneColumn ? browser.GetLockedFolderPath_oneColumn() : browser.InvokeMethod("GetActiveFolderPath"); + var guid = path.ToGuid(); + + var name = path.GetFilename(); + var icon = EditorGUIUtility.FindTexture("Project"); + + if (name.StartsWith("com.")) + if (UnityEditor.PackageManager.PackageInfo.FindForAssetPath(path) is UnityEditor.PackageManager.PackageInfo packageInfo) + name = packageInfo.displayName; + + + void getIconFromVFolders() + { + if (mi_VFolders_GetIcon == null) return; + + if (mi_VFolders_GetIcon.Invoke(null, new[] { guid }) is Texture2D iconFromVFolders) + icon = iconFromVFolders; + + } + + getIconFromVFolders(); + + + browser.titleContent = new GUIContent(name, icon); + + t_DockArea.GetFieldValue("s_GUIContents").Clear(); + + } + void setDefaultTitle() + { + if (isLocked) return; + if (isTitleDefault) return; + + var name = "Project"; + var icon = EditorGUIUtility.FindTexture("Project@2x"); + + browser.titleContent = new GUIContent(name, icon); + + t_DockArea.GetFieldValue("s_GUIContents").Clear(); + + } + + setLockedTitle(); + setDefaultTitle(); + + } + + static void UpdatePropertyEditorTitle(EditorWindow propertyEditor) + { + var obj = propertyEditor.GetMemberValue("m_InspectedObject"); + + if (!obj) return; + + + var name = obj is Component component ? GetComponentName(component) : obj.name; + var sourceIcon = AssetPreview.GetMiniThumbnail(obj); + var adjustedIcon = sourceIcon; + + + void getSourceIconFromVHierarchy() + { + if (mi_VHierarchy_GetIcon == null) return; + if (obj is not GameObject gameObject) return; + + if (mi_VHierarchy_GetIcon.Invoke(null, new[] { gameObject }) is Texture2D iconFromVHierarchy) + sourceIcon = iconFromVHierarchy; + + } + void getAdjustedIcon() + { + if (adjustedObjectIconsBySourceIid.TryGetValue(sourceIcon.GetInstanceID(), out adjustedIcon)) return; + + + adjustedIcon = new Texture2D(sourceIcon.width, sourceIcon.height, sourceIcon.format, sourceIcon.mipmapCount, false); + adjustedIcon.hideFlags = HideFlags.DontSave; + adjustedIcon.SetPropertyValue("pixelsPerPoint", (sourceIcon.width / 16f).RoundToInt()); + + Graphics.CopyTexture(sourceIcon, adjustedIcon); + + + adjustedObjectIconsBySourceIid[sourceIcon.GetInstanceID()] = adjustedIcon; + + } + + + getSourceIconFromVHierarchy(); + getAdjustedIcon(); + + propertyEditor.titleContent = new GUIContent(name, adjustedIcon); + + propertyEditor.SetMemberValue("m_InspectedObject", null); // prevents further title updates from both internal code and vTabs + + t_DockArea.GetFieldValue("s_GUIContents").Clear(); + + } + + + public static void UpdateTitle(EditorWindow window) + { + if (window == null) return; + + var isPropertyEditor = window.GetType() == t_PropertyEditor; + var isBrowser = window.GetType() == t_ProjectBrowser; + + if (!isPropertyEditor && !isBrowser) return; + + + if (isPropertyEditor) + UpdatePropertyEditorTitle(window); + + if (isBrowser) + if (window.GetPropertyValue("isLocked")) + UpdateBrowserTitle(window); + + } + + static void UpdateAllBrowserTitles() + { + foreach (var r in allBrowsers) + UpdateBrowserTitle(r); + } + static void UpdateAllPropertyEditorTitles() + { + foreach (var r in allPropertyEditors) + UpdatePropertyEditorTitle(r); + } + + + static Dictionary adjustedObjectIconsBySourceIid = new Dictionary(); + + + + + + + + + + + + + + + static void WrappedBrowserOnGUI(EditorWindow browser) + { + var headerHeight = 26; + var footerHeight = 21; + var breadcrubsYOffset = .5f; + + var headerRect = browser.position.SetPos(0, 0).SetHeight(headerHeight); + var footerRect = browser.position.SetPos(0, 0).SetHeightFromBottom(footerHeight); + var listAreaRect = browser.position.SetPos(0, 0).AddHeight(-footerHeight).AddHeightFromBottom(-headerHeight); + + var breadcrumbsRect = headerRect.AddHeightFromBottom(-breadcrubsYOffset * 2); + var topGapRect = headerRect.SetHeight(breadcrubsYOffset * 2); + + var breadcrumbsTint = isDarkTheme ? Greyscale(0, .05f) : Greyscale(0, .02f); + var topGapColor = isDarkTheme ? Greyscale(.24f, 1) : Greyscale(.8f, 1); + + var isOneColumn = browser.GetMemberValue("m_ViewMode") == 0; + + var prevSelection = Selection.objects; + + + + void setRootForOneColumn() + { + if (!isOneColumn) return; + if (curEvent.isRepaint) return; + if (browser.GetMemberValue("m_AssetTree") is not object m_AssetTree) return; + if (m_AssetTree.GetMemberValue("data") is not object data) return; + +#if UNITY_6000_3_OR_NEWER + var m_rootInstanceID = data.GetMemberValue("m_rootInstanceID"); +#else + var m_rootInstanceID = data.GetMemberValue("m_rootInstanceID"); +#endif + + + void setInitial() + { + if (m_rootInstanceID != 0) return; + + var folderPath = browser.GetLockedFolderPath_oneColumn(); + var folderIid = AssetDatabase.LoadAssetAtPath(folderPath).GetInstanceID(); + +#if UNITY_6000_3_OR_NEWER + data.SetMemberValue("m_rootInstanceID", (EntityId)folderIid); +#else + data.SetMemberValue("m_rootInstanceID", folderIid); +#endif + + m_AssetTree.InvokeMethod("ReloadData"); + + } + void update() + { + if (m_rootInstanceID == 0) return; + + var folderIid = m_rootInstanceID; + var folderPath = _EditorUtility_InstanceIDToObject(folderIid).GetPath(); + + browser.SetLockedFolderPath_oneColumn(folderPath); + + browser.GetMemberValue("m_SearchFilter")?.SetMemberValue("m_Folders", new[] { folderPath }); // needed for breadcrumbs to display correctly + + } + void reset() + { + if (browser.GetMemberValue("isLocked")) return; + +#if UNITY_6000_3_OR_NEWER + data.SetMemberValue("m_rootInstanceID", (EntityId)0); +#else + data.SetMemberValue("m_rootInstanceID", 0); +#endif + browser.SetLockedFolderPath_oneColumn("Assets"); + + m_AssetTree.InvokeMethod("ReloadData"); + + // returns the browser to normal state on unlock + + } + + setInitial(); + update(); + reset(); + + } + void handleFolderChange() + { + if (isOneColumn) return; + + void onBreadcrumbsClick() + { + if (!curEvent.isMouseUp) return; + if (!breadcrumbsRect.IsHovered()) return; + + browser.RecordUndo(); + + toCallInGUI += () => UpdateBrowserTitle(browser); + toCallInGUI += () => browser.Repaint(); + + } + void onDoubleclick() + { + if (!curEvent.isMouseDown) return; + if (curEvent.clickCount != 2) return; + + browser.RecordUndo(); + + EditorApplication.delayCall += () => UpdateBrowserTitle(browser); + EditorApplication.delayCall += () => browser.Repaint(); + + } + void onUndoRedo() + { + if (!curEvent.isKeyDown) return; + if (!curEvent.holdingCmdOrCtrl) return; + if (curEvent.keyCode != KeyCode.Z) return; + + var curFolderGuid = browser.InvokeMethod("GetActiveFolderPath").ToGuid(); + + EditorApplication.delayCall += () => + { + var delayedFolderGuid = browser.InvokeMethod("GetActiveFolderPath").ToGuid(); + + if (delayedFolderGuid == curFolderGuid) return; + + + var folderIid = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(delayedFolderGuid)).GetInstanceID(); + +#if UNITY_6000_3_OR_NEWER + browser.InvokeMethod("SetFolderSelection", new[] { (EntityId)folderIid }, false); +#else + browser.InvokeMethod("SetFolderSelection", new[] { folderIid }, false); +#endif + + UpdateBrowserTitle(browser); + + }; + + } + + onBreadcrumbsClick(); + onDoubleclick(); + onUndoRedo(); + + } + + void oneColumn() + { + if (!isOneColumn) return; + + + + + if (!browser.InvokeMethod("Initialized")) + browser.InvokeMethod("Init"); + + + + var m_TreeViewKeyboardControlID = GUIUtility.GetControlID(FocusType.Keyboard); + + + + + + + + browser.InvokeMethod("OnEvent"); + + if (curEvent.isMouseDown && browser.position.SetPos(0, 0).IsHovered()) + t_ProjectBrowser.SetFieldValue("s_LastInteractedProjectBrowser", browser); + + + + + // header + browser.SetFieldValue("m_ListHeaderRect", breadcrumbsRect); + + if (curEvent.isRepaint) + browser.InvokeMethod("BreadCrumbBar"); + + breadcrumbsRect.Draw(breadcrumbsTint); + topGapRect.Draw(topGapColor); + + breadcrumbsRect.SetHeightFromBottom(1).Draw(Greyscale(.14f)); + + + + + // footer + browser.SetFieldValue("m_BottomBarRect", footerRect); + browser.InvokeMethod("BottomBar"); + + + + + // tree + browser.GetMemberValue("m_AssetTree")?.InvokeMethod("OnGUI", listAreaRect, m_TreeViewKeyboardControlID); + + + + + + + + + + + + + browser.InvokeMethod("HandleCommandEvents"); + + + + } + void twoColumns() + { + if (isOneColumn) return; + + + + if (!browser.InvokeMethod("Initialized")) + browser.InvokeMethod("Init"); + + + + var m_ListKeyboardControlID = GUIUtility.GetControlID(FocusType.Keyboard); + + var startGridSize = browser.GetFieldValue("m_ListArea")?.GetMemberValue("gridSize"); + + + + + + browser.InvokeMethod("OnEvent"); + + if (curEvent.isMouseDown && browser.position.SetPos(0, 0).IsHovered()) + t_ProjectBrowser.SetFieldValue("s_LastInteractedProjectBrowser", browser); + + + + + // header + browser.SetFieldValue("m_ListHeaderRect", breadcrumbsRect); + + + browser.InvokeMethod("BreadCrumbBar"); + + breadcrumbsRect.Draw(breadcrumbsTint); + topGapRect.Draw(topGapColor); + + breadcrumbsRect.SetHeightFromBottom(1).Draw(Greyscale(.14f)); + + + + + // footer + browser.SetFieldValue("m_BottomBarRect", footerRect); + browser.InvokeMethod("BottomBar"); + + + + + // list area + browser.GetFieldValue("m_ListArea").InvokeMethod("OnGUI", listAreaRect, m_ListKeyboardControlID); + + // block grid size changes when ctrl-shift-scrolling + if (curEvent.holdingCmdOrCtrl) + browser.GetFieldValue("m_ListArea").SetMemberValue("gridSize", startGridSize); + + + + + + browser.SetFieldValue("m_StartGridSize", browser.GetFieldValue("m_ListArea").GetMemberValue("gridSize")); + + browser.InvokeMethod("HandleContextClickInListArea", listAreaRect); + browser.InvokeMethod("HandleCommandEvents"); + + + + } + + void preventSelectionChangeInOtherBrowsers() + { + if (Selection.objects.SequenceEqual(prevSelection)) return; + + + allBrowsers.ForEach(r => r?.SetMemberValue("m_InternalSelectionChange", true)); + + EditorApplication.delayCall += () => + allBrowsers.ForEach(r => r?.SetMemberValue("m_InternalSelectionChange", false)); + + } + + + + setRootForOneColumn(); + handleFolderChange(); + + oneColumn(); + twoColumns(); + + preventSelectionChangeInOtherBrowsers(); + + } + + + static void UpdateWrappingForBrowser(EditorWindow browser) + { + if (!browser.hasFocus) return; + if (mi_VFavorites_CanBrowserBeWrapped != null && mi_VFavorites_CanBrowserBeWrapped.Invoke(null, new[] { browser }).Equals(false)) return; + + var isWrapped = browser.GetMemberValue("m_Parent").GetMemberValue("m_OnGUI").Method == mi_WrappedBrowserOnGUI; + + var isLocked = browser.GetMemberValue("isLocked"); + var shouldBeWrapped = isLocked && !VTabsGUI.disableWrapping; + + void wrap() + { + if (isWrapped) return; + if (!shouldBeWrapped) return; + + var hostView = browser.GetMemberValue("m_Parent"); + + var newDelegate = typeof(VTabs).GetMethod(nameof(WrappedBrowserOnGUI), maxBindingFlags).CreateDelegate(t_EditorWindowDelegate, browser); + + hostView.SetMemberValue("m_OnGUI", newDelegate); + + browser.Repaint(); + + + browser.SetMemberValue("useTreeViewSelectionInsteadOfMainSelection", false); + + } + void unwrap() + { + if (!isWrapped) return; + if (shouldBeWrapped) return; + + var hostView = browser.GetMemberValue("m_Parent"); + + var originalDelegate = hostView.InvokeMethod("CreateDelegate", "OnGUI"); + + hostView.SetMemberValue("m_OnGUI", originalDelegate); + + browser.Repaint(); + + } + + wrap(); + unwrap(); + + } + + static void UpdateWrappingForAllBrowsers() + { + foreach (var r in allBrowsers) + UpdateWrappingForBrowser(r); + } + + + + + + + + + + + + + + + static void HideTabScrollerButtons() + { + void getStyles() + { + if (leftScrollerStyle != null && rightScrollerStyle != null) return; + + if (!guiStylesInitialized) TryInitializeGuiStyles(); + if (!guiStylesInitialized) return; + + if (typeof(GUISkin).GetFieldValue("current")?.GetFieldValue>("m_Styles")?.ContainsKey("dragtab scroller prev") != true) return; + if (typeof(GUISkin).GetFieldValue("current")?.GetFieldValue>("m_Styles")?.ContainsKey("dragtab scroller next") != true) return; + + + var t_Styles = typeof(Editor).Assembly.GetType("UnityEditor.DockArea+Styles"); + + leftScrollerStyle = t_Styles.GetFieldValue("tabScrollerPrevButton"); + rightScrollerStyle = t_Styles.GetFieldValue("tabScrollerNextButton"); + + } + void createTexture() + { + if (clearTexture != null) return; + + clearTexture = new Texture2D(1, 1); + clearTexture.hideFlags = HideFlags.DontSave; + clearTexture.SetPixel(0, 0, Color.clear); + clearTexture.Apply(); + + } + void assignTexture() + { + if (leftScrollerStyle == null) return; + if (rightScrollerStyle == null) return; + + leftScrollerStyle.normal.background = clearTexture; + rightScrollerStyle.normal.background = clearTexture; + + } + + getStyles(); + createTexture(); + assignTexture(); + + } + + static GUIStyle leftScrollerStyle; + static GUIStyle rightScrollerStyle; + + static Texture2D clearTexture; + + + + + + + + + + + + + + + static void ReplaceUnloadedPropertyEditors_withPlaceholderWIndows() + { + // closes non-prefabs too because m_InspectedObject is set to null when changing title + // and doesn't work that reliably anyway + // so I just commented it + + + // foreach (var propertyEditor in allPropertyEditors) + // if (propertyEditor.GetMemberValue("m_InspectedObject") == null) + + // ScriptableObject.CreateInstance() + // .Open_andReplacePropertyEditor(propertyEditor); + + } + + static void LoadPropertyEditorInspectedObjects() + { + foreach (var propertyEditor in allPropertyEditors) + propertyEditor.InvokeMethod("LoadPersistedObject"); + + } + + static void EnsureActiveTabsVisibleOnScroller() + { + foreach (var dockArea in allDockAreas) + { + if (!guis_byDockArea.TryGetValue(dockArea, out var gui)) continue; + + + var scrollPos = gui.GetTargetScrollPosition(); + + if (!scrollPos.Approx(0)) + scrollPos += gui.nonZeroTabScrollOffset; + + + dockArea.SetFieldValue("m_ScrollOffset", scrollPos); + + } + } + + public static void RepaintAllDockAreas() + { + foreach (var dockarea in allDockAreas) + dockarea.InvokeMethod("Repaint"); + } + + + + + + + + + + + + + [UnityEditor.Callbacks.PostProcessBuild] + static void OnBuild(BuildTarget _, string __) + { + EditorApplication.delayCall += LoadPropertyEditorInspectedObjects; + EditorApplication.delayCall += UpdateAllPropertyEditorTitles; + } + + static void OnDomainReloaded() + { + toCallInGUI += UpdateWrappingForAllBrowsers; + toCallInGUI += UpdateAllBrowserTitles; + + } + + static void OnSceneOpened(Scene _, OpenSceneMode __) + { + LoadPropertyEditorInspectedObjects(); + ReplaceUnloadedPropertyEditors_withPlaceholderWIndows(); + UpdateAllPropertyEditorTitles(); + + } + + static void OnPrefabStageClosing(PrefabStage _) + { + ReplaceUnloadedPropertyEditors_withPlaceholderWIndows(); + } + + static void OnProjectLoaded() + { + toCallInGUI += EnsureActiveTabsVisibleOnScroller; + + UpdateAllPropertyEditorTitles(); + + } + + static void OnFocusedWindowChanged() + { + if (EditorWindow.focusedWindow?.GetType() == t_ProjectBrowser) + UpdateWrappingForBrowser(EditorWindow.focusedWindow); + + } + + static void OnWindowUnmaximized() + { + UpdateAllPropertyEditorTitles(); + UpdateAllBrowserTitles(); + + UpdateWrappingForAllBrowsers(); + + + EnsureActiveTabsVisibleOnScroller(); + + } + + + + + + + static void CheckIfFocusedWindowChanged() + { + if (prevFocusedWindow != EditorWindow.focusedWindow) + OnFocusedWindowChanged(); + + 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 Action toCallInGUI; + + + + static void DelayCallLoop() + { + UpdateAllBrowserTitles(); + UpdateWrappingForAllBrowsers(); + HideTabScrollerButtons(); + + EditorApplication.delayCall -= DelayCallLoop; + EditorApplication.delayCall += DelayCallLoop; + + } + + + + static void Update() + { + CheckIfFocusedWindowChanged(); + CheckIfWindowWasUnmaximized(); + } + + + + + + + + + + + + + + + static void ComponentTabHeaderGUI(Editor editor) + { + if (editor.target is not Component component) return; + + var headerRect = ExpandWidthLabelRect(height: 0).MoveY(-48).SetHeight(50).AddWidthFromMid(8); + var nameRect = headerRect.MoveX(43).MoveY(5).SetHeight(20).SetXMax(headerRect.xMax - 50); + var subtextRect = headerRect.MoveX(43).MoveY(22).SetHeight(20); + + + void hideName() + { + var maskRect = headerRect.AddWidthFromRight(-45).AddWidth(-50); + + var maskColor = Greyscale(isDarkTheme ? .24f : .8f); + + maskRect.Draw(maskColor); + + } + void name() + { + SetLabelFontSize(13); + + GUI.Label(nameRect, GetComponentName(component)); + + ResetLabelStyle(); + + } + void componentOf() + { + SetGUIEnabled(false); + + GUI.Label(subtextRect, "Component of"); + + ResetGUIEnabled(); + + } + void goName() + { + var goNameRect = subtextRect.MoveX("Component of ".GetLabelWidth() - 3).SetWidth(component.gameObject.name.GetLabelWidth(isBold: true)); + + goNameRect.MarkInteractive(); + + SetGUIEnabled(goNameRect.IsHovered() && !mousePressedOnGoName); + SetLabelBold(); + + GUI.Label(goNameRect, component.gameObject.name); + + ResetGUIEnabled(); + ResetLabelStyle(); + + + + if (curEvent.isMouseDown && goNameRect.IsHovered()) + { + mousePressedOnGoName = true; + curEvent.Use(); + } + + if (curEvent.isMouseUp) + { + if (mousePressedOnGoName) + EditorGUIUtility.PingObject(component.gameObject); + + mousePressedOnGoName = false; + curEvent.Use(); + } + + if (curEvent.isMouseLeaveWindow || (!curEvent.isLayout && !goNameRect.Resize(1).IsHovered())) + mousePressedOnGoName = false; + + } + + + + hideName(); + name(); + componentOf(); + goName(); + + Space(-4); + + } + + static bool mousePressedOnGoName; + + static string GetComponentName(Component component) + { + if (!component) return ""; + + var name = new GUIContent(EditorGUIUtility.ObjectContent(component, component.GetType())).text; + + name = name.Substring(name.LastIndexOf('(') + 1); + name = name.Substring(0, name.Length - 1); + + return name; + + } + + + + + + + + + + + + + + + static void SetupExtraScrollerSpace() + { + + var action = t_WindowAction.GetMethod("CreateWindowMenuItem", maxBindingFlags).Invoke(null, new[] { "vTabs dummy for creating extra space for + button", null, null }); + + + var extraSpaceAmount = 21f; + + action.SetMemberValue("width", extraSpaceAmount); + + + var mi_ShouldCreateExtraSpace = typeof(VTabs).GetMethod("ShouldCreateExtraSpace", maxBindingFlags); + + var funcDelegate = Delegate.CreateDelegate(t_ValidateHandler, mi_ShouldCreateExtraSpace); + + action.SetMemberValue("validateHandler", funcDelegate); + + + + + var defaultActions = t_HostView.GetMemberValue("s_windowActions") ?? t_HostView.InvokeMethod("FetchWindowActionFromAttribute"); + + var newActions = System.Array.CreateInstance(t_WindowAction, defaultActions.Length + 1); + + System.Array.Copy(defaultActions, newActions, defaultActions.Length); + + newActions.SetValue(action, defaultActions.Length); + + + t_HostView.SetMemberValue("s_windowActions", newActions); + + } + + static bool ShouldCreateExtraSpace(EditorWindow window, object _) => VTabsMenu.addTabButtonEnabled && window == window.GetDockArea().GetTabs()?.Last(); + + + + + + + + + + + + + + static void TryInitializeGuiStyles() => EditorWindow.focusedWindow?.SendEvent(EditorGUIUtility.CommandEvent("")); + + static bool guiStylesInitialized => typeof(GUI).GetFieldValue("s_Skin") != null; + + + + + static Object GetDockArea(this EditorWindow window) + { + var parent = window?.GetFieldValue("m_Parent"); + + if (parent?.GetType() == t_DockArea) + return parent; + else + return null; + + } + + public static List GetTabs(this Object dockArea) => dockArea?.GetFieldValue>("m_Panes"); + + + + + public static string GetLockedFolderPath_oneColumn(this EditorWindow browser) + { + var path = browser.GetMemberValue("m_LastFolders")?.FirstOrDefault(); + + if (path == null || path == "Assets") + path = browser.GetMemberValue("m_SearchFilter")?.GetMemberValue("m_Folders")?.FirstOrDefault() ?? "Assets"; // to migrate locked folder paths from 2.0.14 + + return path; + + // unlike in two column layout, there's no such concept as active folder path in one column + // so we have to serialize locked folder path in some unused field + // m_LastFolders appears to work fine for this purpose + // m_SearchFilter was used before v2.0.15 but could get changed when moving/creating/deleting assets + + } + + public static void SetLockedFolderPath_oneColumn(this EditorWindow browser, string folderPath) + { + browser.SetMemberValue("m_LastFolders", new[] { folderPath }); + } + + + + + + + + + + + + + + + [System.Serializable] + public class TabInfo + { + public TabInfo(EditorWindow window) + { + typeName = window.GetType().Name; + originalDockArea = window.GetDockArea(); + originalTabIndex = window.GetDockArea().GetTabs().IndexOf(window); + wasFocused = window.hasFocus; + originalTitle = window.titleContent.text; + menuItemName = window.titleContent.text.Replace("/", " \u2215 ").Trim(' '); + + if (isBrowser) + { + isLocked = window.GetPropertyValue("isLocked"); + + + savedGridSize = window.GetFieldValue("m_StartGridSize"); + + isGridSizeSaved = true; + + + savedLayout = window.GetMemberValue("m_ViewMode"); + + isLayoutSaved = true; + + + var folderPath = savedLayout == 0 ? window.GetLockedFolderPath_oneColumn() // one column + : window.InvokeMethod("GetActiveFolderPath"); // two columns + folderGuid = folderPath.ToGuid(); + + } + + if (isPropertyEditor) + globalId = new GlobalID(window.GetMemberValue("m_GlobalObjectId")); + + } + public TabInfo(Object lockTo) + { + isLocked = true; + typeName = lockTo is DefaultAsset ? t_ProjectBrowser.Name : t_PropertyEditor.Name; + + + if (isBrowser) + folderGuid = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(lockTo)); + + if (isPropertyEditor) + globalId = lockTo.GetGlobalID(); + +#if UNITY_2021_2_OR_NEWER + if (isPropertyEditor) + if (StageUtility.GetCurrentStage() is PrefabStage && globalId.ToString().Contains("00000000000000000000000000000000")) + lockedPrefabAssetObject = lockTo; +#endif + + } + public TabInfo(string typeName, string menuItemName) { this.typeName = typeName; this.menuItemName = menuItemName; } + + public string typeName; + public string menuItemName; + public object originalDockArea; + public int originalTabIndex; + public string originalTitle; + public bool wasFocused; + + public bool isBrowser => typeName == t_ProjectBrowser.Name; + public bool isLocked; + public string folderGuid = ""; + public int savedGridSize; + public int savedLayout; + public bool isGridSizeSaved = false; + public bool isLayoutSaved = false; + + public bool isPropertyEditor => typeName == t_PropertyEditor.Name; + public GlobalID globalId; + public Object lockedPrefabAssetObject; + + } + + [System.Serializable] + class TabInfoList { public List list = new List(); } + + + + + + + + + + + + [InitializeOnLoadMethod] + static void Init() + { + if (VTabsMenu.pluginDisabled) return; + + + EditorApplication.update -= UpdateGUIs; + EditorApplication.update += UpdateGUIs; + + + + // shortcuts + var globalEventHandler = typeof(EditorApplication).GetFieldValue("globalEventHandler"); + typeof(EditorApplication).SetFieldValue("globalEventHandler", (globalEventHandler - Shortcuts) + Shortcuts); + + + // component tabs + Editor.finishedDefaultHeaderGUI -= ComponentTabHeaderGUI; + Editor.finishedDefaultHeaderGUI += ComponentTabHeaderGUI; + + + + + // state change detectors + var projectWasLoaded = typeof(EditorApplication).GetFieldValue("projectWasLoaded"); + typeof(EditorApplication).SetFieldValue("projectWasLoaded", (projectWasLoaded - OnProjectLoaded) + OnProjectLoaded); + + UnityEditor.SceneManagement.EditorSceneManager.sceneOpened -= OnSceneOpened; + UnityEditor.SceneManagement.EditorSceneManager.sceneOpened += OnSceneOpened; + + EditorApplication.projectWindowItemOnGUI -= ProjectWindowItemOnGUI; + EditorApplication.projectWindowItemOnGUI += ProjectWindowItemOnGUI; + + EditorApplication.hierarchyWindowItemOnGUI -= HierarchyWindowItemOnGUI; + EditorApplication.hierarchyWindowItemOnGUI += HierarchyWindowItemOnGUI; + + EditorApplication.delayCall -= DelayCallLoop; + EditorApplication.delayCall += DelayCallLoop; + + EditorApplication.update -= Update; + EditorApplication.update += Update; + + EditorApplication.quitting -= VTabsCache.Save; + EditorApplication.quitting += VTabsCache.Save; + + + PrefabStage.prefabStageClosing += OnPrefabStageClosing; + + + + // EditorApplication.delayCall += () => VTabsAddTabWindow.UpdateAllEntries(); + + EditorApplication.delayCall += () => UpdateStyleSheet(); + + SetupExtraScrollerSpace(); + + OnDomainReloaded(); + + } + + + public static IEnumerable allBrowsers => _allBrowsers ??= t_ProjectBrowser.GetFieldValue("s_ProjectBrowsers").Cast(); + public static IEnumerable _allBrowsers; + + public static IEnumerable allPropertyEditors => Resources.FindObjectsOfTypeAll(t_PropertyEditor).Where(r => r.GetType().BaseType == typeof(EditorWindow)).Cast(); + + public static List allEditorWindows + { + get + { + if (typeof(EditorWindow).GetPropertyInfo("activeEditorWindows") == null) // this variable doesn't exist in early 2022.3 versions, even though UnityCsReference repo says it does + return Resources.FindObjectsOfTypeAll(typeof(EditorWindow)).Cast().ToList(); + + return _allEditorWindows ?? typeof(EditorWindow).GetMemberValue>("activeEditorWindows"); + } + } + public static List _allEditorWindows; + + public static IEnumerable allDockAreas => allEditorWindows.Where(r => r.hasFocus && !r.maximized).Select(r => r.GetMemberValue("m_Parent")).Where(r => r.GetType() == t_DockArea); + + + + public static Type t_DockArea = typeof(Editor).Assembly.GetType("UnityEditor.DockArea"); + public static Type t_PropertyEditor = typeof(Editor).Assembly.GetType("UnityEditor.PropertyEditor"); + public static Type t_ProjectBrowser = typeof(Editor).Assembly.GetType("UnityEditor.ProjectBrowser"); + public static Type t_SceneHierarchyWindow = typeof(Editor).Assembly.GetType("UnityEditor.SceneHierarchyWindow"); + public static Type t_InspectorWindow = typeof(Editor).Assembly.GetType("UnityEditor.InspectorWindow"); + public static Type t_WindowAction = typeof(Editor).Assembly.GetType("UnityEditor.WindowAction"); + public static Type t_HostView = typeof(Editor).Assembly.GetType("UnityEditor.HostView"); + public static Type t_EditorWindowDelegate = t_HostView.GetNestedType("EditorWindowDelegate", maxBindingFlags); + public static Type t_ValidateHandler = t_WindowAction.GetNestedType("ValidateHandler", maxBindingFlags); + public static Type t_EditorWindowShowButtonDelegate = t_HostView.GetNestedType("EditorWindowShowButtonDelegate", maxBindingFlags); + public static Type t_GameView = typeof(Editor).Assembly.GetType("UnityEditor.GameView"); + public static Type t_ShortcutIntegrations = typeof(Editor).Assembly.GetType("UnityEditor.ShortcutManagement.ShortcutIntegration"); + + public static Type t_VHierarchy = Type.GetType("VHierarchy.VHierarchy") ?? Type.GetType("VHierarchy.VHierarchy, VHierarchy, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); + public static Type t_VFolders = Type.GetType("VFolders.VFolders") ?? Type.GetType("VFolders.VFolders, VFolders, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); + public static Type t_VFavorites = Type.GetType("VFavorites.VFavorites") ?? Type.GetType("VFavorites.VFavorites, VFavorites, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); + + public static MethodInfo mi_WrappedBrowserOnGUI = typeof(VTabs).GetMethod(nameof(WrappedBrowserOnGUI), maxBindingFlags); + + public static MethodInfo mi_VFolders_GetIcon = t_VFolders?.GetMethod("GetSmallFolderIcon_forVTabs", maxBindingFlags); + public static MethodInfo mi_VHierarchy_GetIcon = t_VHierarchy?.GetMethod("GetIcon_forVTabs", maxBindingFlags); + public static MethodInfo mi_VFavorites_BeforeWindowCreated = t_VFavorites?.GetMethod("BeforeWindowCreated_byVTabs", maxBindingFlags); + public static MethodInfo mi_VFavorites_CanBrowserBeWrapped = t_VFavorites?.GetMethod("CanBrowserBeWrapped_byVTabs", maxBindingFlags); + + + + + + const string version = "2.1.6"; + + } +} +#endif diff --git a/Assets/vTabs/VTabs.cs.meta b/Assets/vTabs/VTabs.cs.meta new file mode 100644 index 0000000..9903395 --- /dev/null +++ b/Assets/vTabs/VTabs.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: fc570418b0cd44cd49a39403a3ba1cf7 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 253396 + packageName: vTabs 2 + packageVersion: 2.1.6 + assetPath: Assets/vTabs/VTabs.cs + uploadId: 874244 diff --git a/Assets/vTabs/VTabsAddTabWindow.cs b/Assets/vTabs/VTabsAddTabWindow.cs new file mode 100644 index 0000000..6f376d8 --- /dev/null +++ b/Assets/vTabs/VTabsAddTabWindow.cs @@ -0,0 +1,1220 @@ +#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 VTabs.VTabsCache; +using static VTabs.VTabs; +using static VTabs.Libs.VUtils; +using static VTabs.Libs.VGUI; +// using static VTools.VDebug; + + + +namespace VTabs +{ + public class VTabsAddTabWindow : EditorWindow + { + + void OnGUI() + { + + void background() + { + windowRect.Draw(windowBackground); + + } + void closeOnEscape() + { + if (!curEvent.isKeyDown) return; + if (curEvent.keyCode != KeyCode.Escape) return; + + Close(); + + dockArea.GetMemberValue("actualView").Repaint(); // for + button to fade + + 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; + + AddTab(keyboardFocusedEntry); + + 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)); + + } + + + + 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, TabEntry 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; + + var showBlueBackground = isFocused || isPressed || isDragged; + + if (isDropped) + isHovered = rowRect.SetY(droppedBookmarkYTarget).IsHovered(); + + + void draggedShadow() + { + 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(entry.iconName, returnNullIfNotFound: true); + + 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.name; + + + 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 starButton() + { + if (!isHovered && !isBookmarked) return; + if (isFocused && !isHovered) 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 (!isFocused) return; + if (isHovered) return; + if (!isDarkTheme) return; + + + var hintRect = rowRect.SetWidthFromRight(33); + + + SetLabelFontSize(10); + SetGUIColor(Greyscale(.9f)); + + GUI.Label(hintRect, "Enter"); + + ResetGUIColor(); + ResetLabelStyle(); + + + } + void hoverHighlight() + { + 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(); + + AddTab(entry); + + this.Close(); + + } + + down(); + up(); + + } + void setFocusedEntry() + { + var rowIndex = (rowRect.y / rowHeight).FloorToInt(); + + if (rowIndex == keyboardFocusedRowIndex) + keyboardFocusedEntry = entry; + + } + + + rowRect.MarkInteractive(); + + draggedShadow(); + blueBackground(); + icon(); + name(); + starButton(); + enterHint(); + hoverHighlight(); + + mouse(); + setFocusedEntry(); + + } + + TabEntry pressedEntry; + + bool isMousePressedOnEntry; + + Vector2 mouseDownPosition; + + TabEntry keyboardFocusedEntry; + + + + + void AddTab(TabEntry entry) + { + var windowType = Type.GetType(entry.typeString); + + var window = ScriptableObject.CreateInstance(windowType) as EditorWindow; + + + var windowName = entry.name; + var windowIcon = EditorIcons.GetIcon(entry.iconName, returnNullIfNotFound: true); + + window.titleContent = new GUIContent(windowName, windowIcon); + + + dockArea.InvokeMethod("AddTab", window, true); + + + window.Focus(); + + } + + + + + + + + + + + + + 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; + + TabEntry draggedBookmark; + TabEntry 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 fillWithDefaults() + { + + allEntries.Clear(); + + + foreach (var type in TypeCache.GetTypesWithAttribute()) + { + var titleAttribute = type.GetCustomAttribute(); + + var entry = new TabEntry(); + + entry.typeString = type.AssemblyQualifiedName; + entry.name = titleAttribute.title ?? ""; + entry.iconName = titleAttribute.useTypeNameAsIconName ? type.FullName : titleAttribute.icon ?? ""; + + + if (entry.iconName.IsNullOrEmpty()) continue; // filters out internal windows and such + + allEntries.Add(entry); + + } + + + allEntries.Add(new TabEntry() { name = "Preferences", iconName = "d_Settings@2x", typeString = "UnityEditor.PreferenceSettingsWindow, UnityEditor.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" }); + allEntries.Add(new TabEntry() { name = "Project Settings", iconName = "d_Settings@2x", typeString = "UnityEditor.ProjectSettingsWindow, UnityEditor.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" }); + + allEntries.Add(new TabEntry() { name = "Background Tasks", iconName = "", typeString = "UnityEditor.ProgressWindow, UnityEditor.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" }); + + allEntries.Add(new TabEntry() { name = "Frame Debugger", iconName = "", typeString = "UnityEditor.FrameDebuggerWindow, UnityEditor.CoreModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" }); + allEntries.Add(new TabEntry() { name = "Physics Debug", iconName = "", typeString = "UnityEditor.PhysicsDebugWindow, UnityEditor.PhysicsModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" }); + allEntries.Add(new TabEntry() { name = "UI Toolkit Debugger", iconName = "", typeString = "UnityEditor.UIElements.Debugger.UIElementsDebugger, UnityEditor.UIElementsModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" }); + allEntries.Add(new TabEntry() { name = "UI Builder", iconName = "d_UIBuilder@2x", typeString = "Unity.UI.Builder.Builder, UnityEditor.UIBuilderModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" }); + + allEntries.Add(new TabEntry() { name = "Test Runnder", iconName = "", typeString = "UnityEditor.TestTools.TestRunner.TestRunnerWindow, UnityEditor.TestRunner, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null" }); + allEntries.Add(new TabEntry() { name = "Search", iconName = "d_SearchWindow@2x", typeString = "UnityEditor.Search.SearchWindow, UnityEditor.QuickSearchModule, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" }); + + allEntries.Add(new TabEntry() { name = "Build Settings", iconName = "", typeString = "UnityEditor.BuildPlayerWindow, UnityEditor.CoreModule, Version = 0.0.0.0, Culture = neutral, PublicKeyToken = null" }); + allEntries.Add(new TabEntry() { name = "Build Profiles", iconName = "", typeString = "UnityEditor.Build.Profile.BuildProfileWindow, UnityEditor.BuildProfileModule, Version = 0.0.0.0, Culture = neutral, PublicKeyToken = null" }); + + allEntries.Add(new TabEntry() { name = "Shortcuts", iconName = "", typeString = "UnityEditor.ShortcutManagement.ShortcutManagerWindow, UnityEditor.CoreModule, Version = 0.0.0.0, Culture = neutral, PublicKeyToken = null" }); + + allEntries.Add(new TabEntry() { name = "IMGUI Debugger", iconName = "", typeString = "UnityEditor.GUIViewDebuggerWindow, UnityEditor.CoreModule, Version = 0.0.0.0, Culture = neutral, PublicKeyToken = null" }); + + + + allEntries.RemoveAll(r => allEntries.Count(rr => rr.name == r.name) > 1); + + + + var order = new string[] + { + "Scene", + "Game", + "Project", + "Console", + "Inspector", + "Hierarchy", + "Package Manager", + "Project Settings", + + "Animation", + "Animator", + + "Profiler", + + "Lighting", + "Light Explorer", + // "Viewer", + "Occlusion", + + "UI Toolkit Debugger", + "UI Builder", + + "Frame Debugger", + "Physics Debug", + + "Preferences", + "Simulator", + + "Build Settings", + "Build Profiles", + + + + }.ToList(); + + + allEntries.SortBy(r => order.IndexOf(r.name) is int i && i != -1 ? i : 1232); + + } + void rememberAllOpenTabs() + { + foreach (var window in VTabs.allEditorWindows) + RememberWindow(window); + + } + void removeBlacklisted() + { + allEntries.RemoveAll(r => r.name == "Asset Store"); + allEntries.RemoveAll(r => r.name == "UI Toolkit Samples"); + } + void removeUnresolvableTypes() + { + allEntries.RemoveAll(r => Type.GetType(r.typeString) == null); + } + + + if (allEntries.Count < 15 || allEntries.Any(r => r == null || r.typeString.IsNullOrEmpty())) + fillWithDefaults(); + + rememberAllOpenTabs(); + removeBlacklisted(); + removeUnresolvableTypes(); + + } + + public static void RememberWindow(EditorWindow window) + { + if (!window.docked) return; + if (window.GetType() == t_PropertyEditor) return; + if (window.GetType() == t_InspectorWindow) return; + if (window.GetType() == t_ProjectBrowser) return; + + + var typeString = window.GetType().AssemblyQualifiedName; + + if (allEntries.Any(r => r.typeString == typeString)) return; + + + var name = window.titleContent.text; + + var iconName = window.titleContent.image ? window.titleContent.image.name : ""; + + + allEntries.Add(new TabEntry { typeString = typeString, name = name, iconName = iconName }); + + } + + static List allEntries => VTabsCache.instance.allTabEntries; + + + + void GetBookmarkedEntries() + { + var bookmarkedTabTypeStrings = EditorPrefs.GetString("vTabs-bookmarked-tab-types").Split("---"); + + bookmarkedEntries = bookmarkedTabTypeStrings.Where(r => Type.GetType(r) != null) + .Select(bookmarkedTypeString => allEntries.FirstOrDefault(r => r.typeString == bookmarkedTypeString)) + .Where(r => r != null) + .ToList(); + + } + void SaveBookmarkedEntries() + { + var bookmarkedTabTypeStrings = bookmarkedEntries.Select(r => r.typeString); + + EditorPrefs.SetString("vTabs-bookmarked-tab-types", string.Join("---", bookmarkedTabTypeStrings)); + + } + + List bookmarkedEntries = new(); + + + + void OnEnable() { UpdateAllEntries(); GetBookmarkedEntries(); } + + void OnDisable() { SaveBookmarkedEntries(); VTabsCache.Save(); } + + + + + + + + + + + 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.name, searchString, matchIndexes, ref matchCost) || tryMatch_unitySearch(entry.name, searchString, matchIndexes, ref matchCost)) + { + costs_byEntry[entry] = matchCost; + namesFormattedForFuzzySearch_byEntry[entry] = formatName(entry.name, matchIndexes); + } + + + searchedEntries = costs_byEntry.Keys.OrderBy(r => costs_byEntry[r]) + .ThenBy(r => r.name) + .ToList(); + } + + List searchedEntries = new(); + + Dictionary namesFormattedForFuzzySearch_byEntry = new(); + + + + + + + + void OnLostFocus() + { + EditorApplication.delayCall += () => + { + if (EditorWindow.focusedWindow != this) + { + dockArea.GetMemberValue("actualView").Repaint(); // for + button to fade + + Close(); + } + }; + + // delay is needed to prevent reopening after clicking + button for the second time + } + + + + public static void Open(Object dockArea) + { + instance = ScriptableObject.CreateInstance(); + + instance.ShowPopup(); + instance.Focus(); + + + + var gui = VTabs.guis_byDockArea[dockArea]; + + var windowRect = dockArea.GetMemberValue("actualView").GetMemberValue("position"); + + var lastTabEndPosition = windowRect.position + Vector2.right * gui.tabEndPositions.Last().ClampMax(windowRect.width - 30); + + + var width = 161; + var height = 276; + + var offsetX = -26; + var offsetY = 24; + + instance.position = instance.position.SetPos(lastTabEndPosition + new Vector2(offsetX, offsetY)) + .SetSize(width, height); + + + instance.dockArea = dockArea; + + UpdateAllEntries(); + + } + + public Object dockArea; + + public static VTabsAddTabWindow instance; + + } +} +#endif \ No newline at end of file diff --git a/Assets/vTabs/VTabsAddTabWindow.cs.meta b/Assets/vTabs/VTabsAddTabWindow.cs.meta new file mode 100644 index 0000000..43ae500 --- /dev/null +++ b/Assets/vTabs/VTabsAddTabWindow.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 928f84418aa3c41649729951fe66405c +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 253396 + packageName: vTabs 2 + packageVersion: 2.1.6 + assetPath: Assets/vTabs/VTabsAddTabWindow.cs + uploadId: 874244 diff --git a/Assets/vTabs/VTabsCache.cs b/Assets/vTabs/VTabsCache.cs new file mode 100644 index 0000000..b727675 --- /dev/null +++ b/Assets/vTabs/VTabsCache.cs @@ -0,0 +1,42 @@ +#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 static VTabs.VTabsAddTabWindow; +using static VTabs.Libs.VUtils; +using static VTabs.Libs.VGUI; +// using static VTools.VDebug; + +namespace VTabs +{ + [FilePath("Library/vTabs Cache.asset", FilePathAttribute.Location.ProjectFolder)] + public class VTabsCache : ScriptableSingleton + { + + public List allTabEntries = new(); + + + [System.Serializable] + public class TabEntry + { + public string name = ""; + public string iconName = ""; + public string typeString = ""; + } + + + + public static void Save() => instance.Save(saveAsText: true); + + public static void Clear() => instance.allTabEntries.Clear(); + + } +} +#endif \ No newline at end of file diff --git a/Assets/vTabs/VTabsCache.cs.meta b/Assets/vTabs/VTabsCache.cs.meta new file mode 100644 index 0000000..e3acf12 --- /dev/null +++ b/Assets/vTabs/VTabsCache.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: e727e3495b7464d28aa9d2d5cf3a06bf +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 253396 + packageName: vTabs 2 + packageVersion: 2.1.6 + assetPath: Assets/vTabs/VTabsCache.cs + uploadId: 874244 diff --git a/Assets/vTabs/VTabsGUI.cs b/Assets/vTabs/VTabsGUI.cs new file mode 100644 index 0000000..8ba72a7 --- /dev/null +++ b/Assets/vTabs/VTabsGUI.cs @@ -0,0 +1,1005 @@ +#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 System.Diagnostics; +using Type = System.Type; +using Delegate = System.Delegate; +using Action = System.Action; +using static VTabs.VTabs; +using static VTabs.Libs.VUtils; +using static VTabs.Libs.VGUI; +// using static VTools.VDebug; + + + +namespace VTabs +{ + public class VTabsGUI + { + + public void TabStripGUI(Rect stripRect) + { + void dividers() + { + if (!curEvent.isRepaint) return; + if (!VTabsMenu.dividersEnabled) return; + + void divider(int i) + { + if (tabs[i] == activeTab) return; + if (i + 1 < tabs.Count && tabs[i + 1] == activeTab) return; + + if (tabEndPositions[i] > stripRect.xMax - 25) return; // todo use tabarearect + + + var dividerGreyscale = isDarkTheme ? .24f : .45f; + + if (Application.unityVersion.StartsWith("6000") && VTabsMenu.classicBackgroundEnabled && isDarkTheme) + dividerGreyscale += .02f; + + if (!Application.unityVersion.StartsWith("6000") && isDarkTheme) + dividerGreyscale += .04f; + + if (VTabsMenu.largeTabStyleEnabled && isDarkTheme) + dividerGreyscale += .04f; + + + + + + + var dividerHeight = Application.unityVersion.StartsWith("6000") ? 16 : 12; + var dividerWidth = 1; + + var dividerOffsetX = VTabsMenu.neatTabStyleEnabled ? -2 : 0; + + var dividerRect = stripRect.SetX(0).SetWidth(0).MoveX(tabEndPositions[i] + dividerOffsetX).SetSizeFromMid(dividerWidth, dividerHeight); + + + + dividerRect.Draw(Greyscale(dividerGreyscale)); + + } + + for (int i = 0; i < tabs.Count; i++) + divider(i); + + } + void addTabButton() + { + if (!VTabsMenu.addTabButtonEnabled) return; + + + var buttonRect = stripRect.SetX(tabEndPositions.Last()).SetWidth(0).SetWidthFromMid(24).MoveX(VTabsMenu.neatTabStyleEnabled ? 12 : 13); + + + var distToRight = stripRect.xMax - buttonRect.xMax; + + if (distToRight < 10) return; + + + interactiveRects.Add(buttonRect); + + + + var fadeStart = 10; + var fadeEnd = 25; + var fadeK = ((distToRight - fadeStart) / (fadeEnd - fadeStart)).Clamp01().Pow(2); + + var iconName = stripRect.IsHovered() && curEvent.holdingAlt && tabInfosForReopening.Any() ? "UndoHistory" : "d_Toolbar Plus"; + var iconSize = 16; + var colorNormal = Greyscale(isDarkTheme ? .5f : .47f, fadeK); + var colorHovered = Greyscale(isDarkTheme ? 1f : .1f); + var colorPressed = Greyscale(isDarkTheme ? .75f : .5f); + + if (VTabsAddTabWindow.instance && VTabsAddTabWindow.instance.dockArea == dockArea) + colorNormal = colorHovered; + + if (DragAndDrop.objectReferences.Any()) + colorHovered = colorNormal; + + + if (!IconButton(buttonRect, iconName, iconSize, colorNormal, colorHovered, colorPressed)) return; + + if (curEvent.holdingAlt) + { + if (tabInfosForReopening.Any()) + ReopenClosedTab(); + + return; + + } + + if (VTabsAddTabWindow.instance) + VTabsAddTabWindow.instance.Close(); + else + VTabsAddTabWindow.Open(dockArea); + + } + void closeTabButton() + { + if (!VTabsMenu.closeTabButtonEnabled) return; + if (tabs.Count == 1 && !curEvent.holdingAlt) return; + + isCloseButtonHovered = false; + + if (hoveredTab == null) return; + if (hoveredTab == hideCloseButtonOnTab) return; + + + + var buttonRect = stripRect.SetX(tabEndPositions[hoveredTabIndex]).SetWidth(0).SetSizeFromMid(12).MoveX(VTabsMenu.largeTabStyleEnabled ? -16 : -14); + + if (buttonRect.xMax > stripRect.xMax - 10) return; + + interactiveRects.Add(buttonRect); + + isCloseButtonHovered = buttonRect.IsHovered(); + + + + + if (!Application.unityVersion.StartsWith("6000")) + { + var backgroundColor = isDarkTheme ? Greyscale(hoveredTab == activeTab ? .23f : .19f) + : Greyscale(hoveredTab == activeTab ? .25f : .2f); + + buttonRect.Resize(-2).DrawBlurred(backgroundColor, 3); + buttonRect.Resize(-1).DrawBlurred(backgroundColor.SetAlpha(.6f), 5); + + } + + + + + var iconName = "Cross"; + var iconSize = 14; + var colorNormal = Greyscale(isDarkTheme ? .55f : .35f); + var colorHovered = Greyscale(isDarkTheme ? 1f : .0f); + var colorPressed = Greyscale(isDarkTheme ? .75f : .5f); + + + if (!IconButton(buttonRect, iconName, iconSize, colorNormal, colorHovered, colorPressed)) return; + + void closeNextUpdate() + { + CloseTab(hoveredTab); + + EditorApplication.update -= closeNextUpdate; + + } + + if (tabs.Count == 1) + EditorApplication.update += closeNextUpdate; // prevents error on dockarea destruction + else + CloseTab(hoveredTab); + + } + void curtains() + { + if (!curEvent.isRepaint) return; + + + var isUnity6Background = Application.unityVersion.StartsWith("6000") ? !VTabsMenu.classicBackgroundEnabled : false; + + var fadeDistance = 10; + var curtainWidth = 25; + var curtainGreyscale = isDarkTheme ? isUnity6Background ? .075f : .15f + : isUnity6Background ? .86f : .65f; + + + var tabAreaRect = dockArea.GetMemberValue("m_TabAreaRect"); + + var leftCurtainOpacity = (scrollPos / fadeDistance).Clamp01(); + var rightCurtainOpacity = ((tabEndPositions.Last() - tabAreaRect.width + 4) / fadeDistance).Clamp01(); + + + tabAreaRect.SetWidth(curtainWidth).DrawCurtainRight(Greyscale(curtainGreyscale, leftCurtainOpacity)); + tabAreaRect.SetWidthFromRight(curtainWidth).DrawCurtainLeft(Greyscale(curtainGreyscale, rightCurtainOpacity)); + + + // // fade plus button + // if (activeTab == tabs.Last() && VTabsMenu.addTabButtonEnabled) + // tabAreaRect.SetWidthFromRight(0).SetWidth(20).Draw(Greyscale(curtainGreyscale, rightCurtainOpacity)); + + } + + + interactiveRects.Clear(); + + if (curEvent.isLayout) + UpdateState(); + + dividers(); + addTabButton(); + closeTabButton(); + curtains(); + + tabStripElement.pickingMode = interactiveRects.Any(r => r.IsHovered()) ? PickingMode.Position + : PickingMode.Ignore; + } + + List interactiveRects = new(); + + bool isCloseButtonHovered; + + + + + public void UpdateState() + { + void scrollPos_() + { + scrollPos = dockArea.GetFieldValue("m_ScrollOffset"); + + if (scrollPos != 0) + scrollPos -= nonZeroTabScrollOffset; + + } + void tabEndPositions_() + { + tabEndPositions.Clear(); + + + var curPos = -scrollPos + + dockArea.GetMemberValue("m_TabAreaRect").x * 2 // internally this offset is erroneously applied twice + - 2; + + foreach (var tab in tabs) + { + curPos += GetTabWidth(tab); + + tabEndPositions.Add(curPos.Round()); // internally tabs are drawn using plain round(), not roundToPixelGrid() + + } + } + void hoveredTab_() + { + hoveredTab = null; + hoveredTabIndex = -1; + + if (!tabStripElement.contentRect.IsHovered()) return; + + + for (int i = tabs.Count - 1; i >= 0; i--) + if (curEvent.mousePosition.x < tabEndPositions[i]) + hoveredTabIndex = i; + + if (hoveredTabIndex.IsInRangeOf(tabs)) + hoveredTab = tabs[hoveredTabIndex]; + + } + + scrollPos_(); + tabEndPositions_(); + hoveredTab_(); + + } + + float scrollPos; + + public List tabEndPositions = new(); + + int hoveredTabIndex; + + EditorWindow hoveredTab; + + + + + + + + + void DelayCallRepaintLoop() + { + if (!activeTab) return; // happens when maximized + + + isTabStripHovered = tabStripElement.contentRect.Move(activeTab.position.position).Contains(curEvent.mousePosition_screenSpace); + + if (isTabStripHovered) + activeTab.Repaint(); + + + EditorApplication.delayCall += DelayCallRepaintLoop; + + + // needed because dockarea can fail to repaint when mouse enters/leaves interactive regions (buttons, tabs) + // seems to only happen in unity 6 when active tab is uitk based + + } + + bool isTabStripHovered; + + + + + + + + + + + + void HandleTabScrolling(EventBase e) + { + if (e is MouseMoveEvent) { sidescrollPosition = 0; return; } + if (e is not WheelEvent scrollEvent) return; + + + void switchTab(int dir) + { + var i0 = tabs.IndexOf(activeTab); + var i1 = Mathf.Clamp(i0 + dir, 0, tabs.Count - 1); + + tabs[i1].Focus(); + + VTabs.UpdateTitle(tabs[i1]); + + } + void moveTab(int dir) + { + var i0 = tabs.IndexOf(activeTab); + var i1 = Mathf.Clamp(i0 + dir, 0, tabs.Count - 1); + + var r = tabs[i0]; + tabs[i0] = tabs[i1]; + tabs[i1] = r; + + tabs[i1].Focus(); + + } + + void shiftscroll() + { + if (!VTabsMenu.switchTabShortcutEnabled) return; + + if (scrollEvent.modifiers != (EventModifiers.Shift) + && scrollEvent.modifiers != (EventModifiers.Shift | EventModifiers.Control) + && scrollEvent.modifiers != (EventModifiers.Shift | EventModifiers.Command)) return; + + + + var scrollDelta = Application.platform == RuntimePlatform.OSXEditor ? scrollEvent.delta.x // osx sends delta.y as delta.x when shift is pressed + : scrollEvent.delta.x - scrollEvent.delta.y; // some software on windows (eg logitech options) may do that too + if (VTabsMenu.reverseScrollDirectionEnabled) + scrollDelta *= -1; + + if (scrollDelta == 0) return; + + e.StopPropagation(); + + + + if (scrollEvent.ctrlKey || scrollEvent.commandKey) + moveTab(scrollDelta > 0 ? 1 : -1); + else + switchTab(scrollDelta > 0 ? 1 : -1); + + } + void sidescroll() + { + if (!VTabsMenu.sidescrollEnabled) return; + + if (scrollEvent.modifiers != EventModifiers.None + && scrollEvent.modifiers != EventModifiers.Command + && scrollEvent.modifiers != EventModifiers.Control) return; + + + + if (scrollEvent.delta.x.Abs() < scrollEvent.delta.y.Abs()) { sidescrollPosition = 0; return; } + + e.StopPropagation(); + + if (scrollEvent.delta.x.Abs() <= 0.06f) return; + + + + var dampenK = 5; // the larger this k is - the smaller big deltas are, and the less is sidescroll's dependency on scroll speed + var a = scrollEvent.delta.x.Abs() * dampenK; + var deltaDampened = (a < 1 ? a : Mathf.Log(a) + 1) / dampenK * -scrollEvent.delta.x.Sign(); + + var sensitivityK = .22f; + var scrollDelta = deltaDampened * VTabsMenu.sidescrollSensitivity * sensitivityK; + + if (VTabsMenu.reverseScrollDirectionEnabled) + scrollDelta *= -1; + + if (sidescrollPosition.RoundToInt() == (sidescrollPosition += scrollDelta).RoundToInt()) return; + + + + + if (scrollEvent.ctrlKey || scrollEvent.commandKey) + moveTab(scrollDelta > 0 ? 1 : -1); + else + switchTab(scrollDelta > 0 ? 1 : -1); + + } + + + shiftscroll(); + sidescroll(); + + } + + float sidescrollPosition; + + + + void HandleDragndrop(EventBase e) + { + if (!VTabsMenu.dragndropEnabled) return; + + + var dragndropArea = panel.visualTree.contentRect.SetHeight(activeTab.GetType() == t_SceneHierarchyWindow ? 20 : 40); + + if (!dragndropArea.Contains(e.originalMousePosition)) return; + + + + if (e is DragUpdatedEvent dragUpdatedEvent) + DragAndDrop.visualMode = DragAndDropVisualMode.Copy; + + + + if (e is not DragPerformEvent dragPerformEvent) return; + + DragAndDrop.AcceptDrag(); + + AddTab(new VTabs.TabInfo(DragAndDrop.objectReferences.First())); + + lastDragndropTime = System.DateTime.UtcNow; + + } + + static System.DateTime lastDragndropTime; + + + + void HandleHidingCloseButton(EventBase e) + { + if (e is MouseDownEvent && !isCloseButtonHovered) + if (hoveredTab != null && hoveredTab != activeTab) + hideCloseButtonOnTab = hoveredTab; + + if (e is MouseMoveEvent) + if (hoveredTab != hideCloseButtonOnTab) + hideCloseButtonOnTab = null; + } + + EditorWindow hideCloseButtonOnTab; + + + + void HandleHiddenMenu(MouseDownEvent mouseDownEvent) + { + if (mouseDownEvent.modifiers != EventModifiers.Alt) return; + if (mouseDownEvent.button != 1) return; + if (!tabStripElement.contentRect.Contains(mouseDownEvent.mousePosition)) return; + + + mouseDownEvent.StopPropagation(); + + + GenericMenu menu = new(); + + menu.AddDisabledItem(new GUIContent("vTabs hidden menu")); + + menu.AddSeparator(""); + menu.AddItem(new GUIContent("Don't hide left column"), disableWrapping, () => disableWrapping = !disableWrapping); + + menu.AddSeparator(""); + menu.AddItem(new GUIContent("Select cache"), false, () => Selection.activeObject = VTabsCache.instance); + menu.AddItem(new GUIContent("Clear cache"), false, VTabsCache.Clear); + + menu.ShowAsContext(); + + + } + + public static bool disableWrapping + { + get => EditorPrefsCached.GetBool("vTabs-disableWrapping", defaultValue: false); + set => EditorPrefsCached.SetBool("vTabs-disableWrapping", value); + } + + + + + + + + + + + + + public EditorWindow AddTab(TabInfo tabInfo, bool atOriginalTabIndex = false) + { + + var lastInteractedBrowser = t_ProjectBrowser.GetFieldValue("s_LastInteractedProjectBrowser"); // changes on new browser creation + + var window = (EditorWindow)ScriptableObject.CreateInstance(tabInfo.typeName); + + void notifyVFavorites() + { + mi_VFavorites_BeforeWindowCreated?.Invoke(null, new object[] { dockArea }); + } + void addToDockArea() + { + if (atOriginalTabIndex) + dockArea.InvokeMethod("AddTab", tabInfo.originalTabIndex, window, true); + else + dockArea.InvokeMethod("AddTab", window, true); + + } + + void setupBrowser() + { + if (!tabInfo.isBrowser) return; + + + void setSavedGridSize() + { + if (!tabInfo.isGridSizeSaved) return; + + window.GetFieldValue("m_ListArea")?.SetMemberValue("gridSize", tabInfo.savedGridSize); + + } + void setLastUsedGridSize() + { + if (tabInfo.isGridSizeSaved) return; + if (lastInteractedBrowser == null) return; + + var listAreaSource = lastInteractedBrowser.GetFieldValue("m_ListArea"); + var listAreaDest = window.GetFieldValue("m_ListArea"); + + if (listAreaSource != null && listAreaDest != null) + listAreaDest.SetPropertyValue("gridSize", listAreaSource.GetPropertyValue("gridSize")); + + } + + void setSavedLayout() + { + if (!tabInfo.isLayoutSaved) return; + + var layoutEnum = System.Enum.ToObject(t_ProjectBrowser.GetField("m_ViewMode", maxBindingFlags).FieldType, tabInfo.savedLayout); + + window.InvokeMethod("SetViewMode", layoutEnum); + + } + void setLastUsedLayout() + { + if (tabInfo.isLayoutSaved) return; + if (lastInteractedBrowser == null) return; + + window.InvokeMethod("SetViewMode", lastInteractedBrowser.GetMemberValue("m_ViewMode")); + + } + + void setLastUsedListWidth() + { + if (lastInteractedBrowser == null) return; + + window.SetFieldValue("m_DirectoriesAreaWidth", lastInteractedBrowser.GetFieldValue("m_DirectoriesAreaWidth")); + + } + + void lockToFolder_twoColumns() + { + if (!tabInfo.isLocked) return; + if (window.GetMemberValue("m_ViewMode") != 1) return; + if (tabInfo.folderGuid.IsNullOrEmpty()) return; + + + var iid = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(tabInfo.folderGuid)).GetInstanceID(); + +#if UNITY_6000_3_OR_NEWER + window.GetFieldValue("m_ListAreaState").SetFieldValue("m_SelectedInstanceIDs", new List { (EntityId)iid }); +#else + window.GetFieldValue("m_ListAreaState").SetFieldValue("m_SelectedInstanceIDs", new List { iid }); +#endif + + t_ProjectBrowser.InvokeMethod("OpenSelectedFolders"); + + + window.SetPropertyValue("isLocked", true); + + } + void lockToFolder_oneColumn() + { + if (!tabInfo.isLocked) return; + if (window.GetMemberValue("m_ViewMode") != 0) return; + if (tabInfo.folderGuid.IsNullOrEmpty()) return; + + if (window.GetMemberValue("m_AssetTree") is not object m_AssetTree) return; + if (m_AssetTree.GetMemberValue("data") is not object data) return; + + + var folderPath = tabInfo.folderGuid.ToPath(); + var folderIid = AssetDatabase.LoadAssetAtPath(folderPath).GetInstanceID(); + +#if UNITY_6000_3_OR_NEWER + data.SetMemberValue("m_rootInstanceID", (EntityId)folderIid); +#else + data.SetMemberValue("m_rootInstanceID", folderIid); +#endif + + m_AssetTree.InvokeMethod("ReloadData"); + + VTabs.SetLockedFolderPath_oneColumn(window, folderPath); + + + window.SetPropertyValue("isLocked", true); + + } + + + window.InvokeMethod("Init"); + + setSavedGridSize(); + setLastUsedGridSize(); + + setSavedLayout(); + setLastUsedLayout(); + + setLastUsedListWidth(); + + lockToFolder_twoColumns(); + lockToFolder_oneColumn(); + + VTabs.UpdateTitle(window); + + } + void setupPropertyEditor() + { + if (!tabInfo.isPropertyEditor) return; + + + var lockTo = tabInfo.globalId.GetObject(); + + if (tabInfo.lockedPrefabAssetObject) + lockTo = tabInfo.lockedPrefabAssetObject; // globalId api doesn't work for prefab asset objects, so we use direct object reference in such cases + + if (!lockTo) return; + + + window.GetMemberValue("tracker").InvokeMethod("SetObjectsLockedByThisTracker", (new List { lockTo })); + + + if (StageUtility.GetCurrentStage() is PrefabStage && tabInfo.globalId.isNull) + window.SetMemberValue("m_GlobalObjectId", GlobalID.GetForPrefabStageObject(lockTo).ToString()); + else + window.SetMemberValue("m_GlobalObjectId", tabInfo.globalId.ToString()); + + + + + window.SetMemberValue("m_InspectedObject", lockTo); + + VTabs.UpdateTitle(window); + + } + + void setCustomEditorWindowTitle() + { + if (window.titleContent.text != window.GetType().FullName) return; + if (tabInfo.originalTitle.IsNullOrEmpty()) return; + + window.titleContent.text = tabInfo.originalTitle; + + // custom EditorWindows often have their titles set in EditorWindow.GetWindow + // and when such windows are created via ScriptableObject.CreateInstance, their titles default to window type name + // so we have to set original window title in such cases + + } + + + notifyVFavorites(); + addToDockArea(); + + setupBrowser(); + setupPropertyEditor(); + + setCustomEditorWindowTitle(); + + + window.Focus(); + + + + return window; + } + + public void CloseTab(EditorWindow tab) + { + tabInfosForReopening.Push(new TabInfo(tab)); + + VTabsAddTabWindow.RememberWindow(tab); + + tab.Close(); + + } + + public void ReopenClosedTab() + { + if (!tabInfosForReopening.Any()) return; + + + var tabInfo = tabInfosForReopening.Pop(); + + + var prevActiveTab = activeTab; + + var reopenedTab = AddTab(tabInfo, atOriginalTabIndex: true); + + if (!tabInfo.wasFocused) + prevActiveTab.Focus(); + + + + VTabs.UpdateTitle(reopenedTab); + + } + + Stack tabInfosForReopening = new(); + + + + + + + + + + + + + + public void UpdateScrollAnimation() + { + if (activeTab != EditorWindow.focusedWindow) return; + if (!guiStylesInitialized) return; + if ((System.DateTime.UtcNow - lastDragndropTime).TotalSeconds < .05f) return; // to avoid stutter after dragndrop + + + + var curScrollPos = dockArea.GetFieldValue("m_ScrollOffset"); + + if (!curScrollPos.Approx(0)) + curScrollPos -= nonZeroTabScrollOffset; + + if (curScrollPos == 0) + curScrollPos = prevScrollPos; // prevents immediate jump to 0 on tab close + + + + var targScrollPos = GetTargetScrollPosition(); + + // var animationSpeed = 1f; + var animationSpeed = 7f; + + var newScrollPos = MathUtil.SmoothDamp(curScrollPos, targScrollPos, animationSpeed, ref scrollPosDeriv, editorDeltaTime); + + if (newScrollPos < .5f) + newScrollPos = 0; + + prevScrollPos = newScrollPos; + + + + + if (newScrollPos.Approx(curScrollPos)) return; + + if (!newScrollPos.Approx(0)) + newScrollPos += nonZeroTabScrollOffset; + + dockArea.SetFieldValue("m_ScrollOffset", newScrollPos); + + activeTab.Repaint(); + + } + + public float nonZeroTabScrollOffset = 3f; + + float scrollPosDeriv; + float prevScrollPos; + + + + public float GetTargetScrollPosition() + { + if (!guiStylesInitialized) return 0; + + + var tabAreaWidth = dockArea.GetFieldValue("m_TabAreaRect").width; + + if (tabAreaWidth == 0) + tabAreaWidth = activeTab.position.width - 38; + + + + + var activeTabXMin = 0f; + var activeTabXMax = 0f; + + var tabWidthSum = 0f; + + var activeTabReached = false; + + foreach (var tab in tabs) + { + var tabWidth = GetTabWidth(tab); + + tabWidthSum += tabWidth; + + + if (activeTabReached) continue; + + activeTabXMin = activeTabXMax; + activeTabXMax += tabWidth; + + if (tab == activeTab) + activeTabReached = true; + + } + + + + + var optimalScrollPos = 0f; + + var visibleAreaPadding = 65f; + + var visibleAreaXMin = activeTabXMin - visibleAreaPadding; + var visibleAreaXMax = activeTabXMax + visibleAreaPadding; + + optimalScrollPos = Mathf.Max(optimalScrollPos, visibleAreaXMax - tabAreaWidth); + optimalScrollPos = Mathf.Min(optimalScrollPos, tabWidthSum - tabAreaWidth + 4); + + optimalScrollPos = Mathf.Min(optimalScrollPos, visibleAreaXMin); + optimalScrollPos = Mathf.Max(optimalScrollPos, 0); + + + + + return optimalScrollPos; + + } + + public float GetTabWidth(EditorWindow tab) + { + if (guiStylesInitialized) + tabStyle ??= typeof(GUI).GetMemberValue("s_Skin")?.FindStyle("dragtab"); + + if (tabStyle == null) return 0; + + + return dockArea.InvokeMethod("GetTabWidth", tabStyle, tab); + + } + + static GUIStyle tabStyle; + + bool guiStylesInitialized => typeof(GUI).GetFieldValue("s_Skin") != null; + + + + + + + + + + public void UpdateLockButtonHiding() + { + bool isLocked(EditorWindow window) + { + if (window.GetType() == t_SceneHierarchyWindow) + return window.GetMemberValue("m_SceneHierarchy").GetMemberValue("isLocked"); + + if (window.GetType() == t_InspectorWindow) + return window.GetMemberValue("isLocked"); + + return false; + } + + var shouldHideLockButton = VTabsMenu.hideLockButtonEnabled && !isLocked(activeTab); + + + + if (!shouldHideLockButton && lockButtonDelegate != null) + { + dockArea.SetMemberValue("m_ShowButton", lockButtonDelegate); + lockButtonDelegate = null; + } + + if (shouldHideLockButton) + { + lockButtonDelegate ??= dockArea.GetMemberValue("m_ShowButton"); + + dockArea.SetMemberValue("m_ShowButton", null); + } + + } + + object lockButtonDelegate; + + + + + + + + + + + public VTabsGUI(Object dockArea) + { + this.dockArea = dockArea; + + + panel = dockArea.GetMemberValue("actualView").rootVisualElement.panel; + + tabs = dockArea.GetMemberValue>("m_Panes"); + + + + + panel.visualTree.RegisterCallback(HandleTabScrolling, TrickleDown.TrickleDown); + panel.visualTree.RegisterCallback(HandleTabScrolling, TrickleDown.TrickleDown); + + panel.visualTree.RegisterCallback(HandleDragndrop, TrickleDown.TrickleDown); + panel.visualTree.RegisterCallback(HandleDragndrop, TrickleDown.NoTrickleDown); // no trickledown to avoid creating tab when dropping on navbar + + panel.visualTree.RegisterCallback(HandleHidingCloseButton, TrickleDown.TrickleDown); + panel.visualTree.RegisterCallback(HandleHidingCloseButton, TrickleDown.TrickleDown); + + panel.visualTree.RegisterCallback(HandleHiddenMenu, TrickleDown.TrickleDown); + + + + + + + tabStripElement = new IMGUIContainer(); + + tabStripElement.name = "vTabs-tab-strip"; + + tabStripElement.style.width = Length.Percent(100); + tabStripElement.style.height = Application.unityVersion.StartsWith("6000") ? 24 : 19; + tabStripElement.style.position = Position.Absolute; + + tabStripElement.pickingMode = PickingMode.Ignore; + + tabStripElement.onGUIHandler = () => TabStripGUI(tabStripElement.contentRect); + + panel.visualTree.Add(tabStripElement); + + + + + EditorApplication.delayCall += DelayCallRepaintLoop; + + } + + Object dockArea; + IPanel panel; + public List tabs; + IMGUIContainer tabStripElement; + + public EditorWindow activeTab => tabs.FirstOrDefault(r => r.hasFocus); + + } +} +#endif \ No newline at end of file diff --git a/Assets/vTabs/VTabsGUI.cs.meta b/Assets/vTabs/VTabsGUI.cs.meta new file mode 100644 index 0000000..d7c9011 --- /dev/null +++ b/Assets/vTabs/VTabsGUI.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 291c4a11da18041c49b4772d9e054145 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 253396 + packageName: vTabs 2 + packageVersion: 2.1.6 + assetPath: Assets/vTabs/VTabsGUI.cs + uploadId: 874244 diff --git a/Assets/vTabs/VTabsLibs.cs b/Assets/vTabs/VTabsLibs.cs new file mode 100644 index 0000000..603b1f5 --- /dev/null +++ b/Assets/vTabs/VTabsLibs.cs @@ -0,0 +1,1907 @@ + +#if UNITY_EDITOR +using System.Collections; +using System.Collections.Generic; +using System.IO; +using System.Text; +using System.Text.RegularExpressions; +using System.Reflection; +using System.Linq; +using UnityEngine; +using UnityEngine.Rendering; +using UnityEngine.Experimental.Rendering; +using UnityEditor; +using Type = System.Type; +using static VTabs.Libs.VUtils; + + + +namespace VTabs.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 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 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 ProjectOn(this Vector2 v, Vector2 on) => Vector3.Project(v, on).magnitude; + public static float ProjectOn(this Vector3 v, Vector3 on) => Vector3.Project(v, on).magnitude; + + 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 Colors + + + 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 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 p, string p2) => Path.Combine(p, p2); + + 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 + + + public static bool IsValidGuid(this string guid) => AssetDatabase.AssetPathToGUID(AssetDatabase.GUIDToAssetPath(guid), AssetPathToGUIDOptions.OnlyExistingAssets) != ""; + + + public static T Reimport(this T t) where T : Object { AssetDatabase.ImportAsset(t.GetPath(), ImportAssetOptions.ForceUpdate); return t; } + + + + // toremove + public static Object LoadGuid(this string guid) => AssetDatabase.LoadAssetAtPath(guid.ToPath(), typeof(Object)); + public static T LoadGuid(this string guid) where T : Object => AssetDatabase.LoadAssetAtPath(guid.ToPath()); + + + // toremove + // public static List FindAllAssetsOfType_guids(Type type) => AssetDatabase.FindAssets("t:" + type.Name).ToList(); + // public static List FindAllAssetsOfType_guids(Type type, string path) => AssetDatabase.FindAssets("t:" + type.Name, new[] { path }).ToList(); + // public static List FindAllAssetsOfType() where T : Object => FindAllAssetsOfType_guids(typeof(T)).Select(r => (T)r.LoadGuid()).ToList(); + // public static List FindAllAssetsOfType(string path) where T : Object => FindAllAssetsOfType_guids(typeof(T), path).Select(r => (T)r.LoadGuid()).ToList(); + + +#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 GetForPrefabStageObject(Object o) + { + if (UnityEditor.SceneManagement.StageUtility.GetCurrentStage() is not UnityEditor.SceneManagement.PrefabStage prefabStage) + { + Debug.LogError("GetForPrefabAssetObject() got called outside of prefab stgage!"); + + return o.GetGlobalID(); + + } + + + + var rawGlobalId = o.GetGlobalID(); + + +#if UNITY_2023_2_OR_NEWER + + var so = new SerializedObject(o); + + 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)typeof(Editor).Assembly.GetType("UnityEditor.Unsupported").InvokeMethod("GetOrGenerateFileIDHint", o); + +#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; + + + var prefabGuid = prefabStage.assetPath.ToGuid(); + + + + + var sourceGlobalId = new GlobalID($"GlobalObjectId_V1-1-{prefabGuid}-{fileId}-0"); + + return sourceGlobalId; + + } + + } + + 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 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", true)); + 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 + + #region Instance/Entity ID mess + + + static int _GlobalObjectId_GlobalObjectIdentifierToInstanceIDSlow(GlobalObjectId id) + { +#if UNITY_6000_3_OR_NEWER + return GlobalObjectId.GlobalObjectIdentifierToEntityIdSlow(id); +#else + return GlobalObjectId.GlobalObjectIdentifierToInstanceIDSlow(id); +#endif + + } + + static void _GlobalObjectId_GlobalObjectIdentifiersToInstanceIDsSlow(GlobalObjectId[] identifiers, int[] outputInstanceIDs) + { +#if UNITY_6000_3_OR_NEWER + + var outputEntityIds = new EntityId[outputInstanceIDs.Length]; + + GlobalObjectId.GlobalObjectIdentifiersToEntityIdsSlow(identifiers, outputEntityIds); + + for (int i = 0; i < outputEntityIds.Length; i++) + outputInstanceIDs[i] = (int)outputEntityIds[i]; + +#else + + GlobalObjectId.GlobalObjectIdentifiersToInstanceIDsSlow(identifiers, outputInstanceIDs); + +#endif + + } + + static void _GlobalObjectId_GetGlobalObjectIdsSlow(int[] ids, GlobalObjectId[] outputIdentifiers) + { +#if UNITY_6000_3_OR_NEWER + GlobalObjectId.GetGlobalObjectIdsSlow(ids.Select(r => (EntityId)r).ToArray(), outputIdentifiers); +#else + GlobalObjectId.GetGlobalObjectIdsSlow(ids, outputIdentifiers); +#endif + + } + + + + public static Object _EditorUtility_InstanceIDToObject(int iid) + { +#if UNITY_6000_3_OR_NEWER + return EditorUtility.EntityIdToObject(iid); +#else + return EditorUtility.InstanceIDToObject(iid); +#endif + } + + public static string _AssetDatabase_GetAssetPath(int instanceID) + { +#if UNITY_6000_3_OR_NEWER + return AssetDatabase.GetAssetPath((EntityId)instanceID); +#else + return AssetDatabase.GetAssetPath(instanceID); +#endif + } + + public static int[] _Selection_instanceIDs + { + get + { +#if UNITY_6000_3_OR_NEWER + return Selection.entityIds.Select(r => (int)r).ToArray(); +#else + return Selection.instanceIDs; +#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; } + + + + + #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() + { + ["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", + ["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", + }; + + } + + + + #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/Assets/vTabs/VTabsLibs.cs.meta b/Assets/vTabs/VTabsLibs.cs.meta new file mode 100644 index 0000000..8077099 --- /dev/null +++ b/Assets/vTabs/VTabsLibs.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 91d4456e469254096af9035b29263ca5 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 253396 + packageName: vTabs 2 + packageVersion: 2.1.6 + assetPath: Assets/vTabs/VTabsLibs.cs + uploadId: 874244 diff --git a/Assets/vTabs/VTabsMenu.cs b/Assets/vTabs/VTabsMenu.cs new file mode 100644 index 0000000..05a62fe --- /dev/null +++ b/Assets/vTabs/VTabsMenu.cs @@ -0,0 +1,197 @@ +#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 static VTabs.Libs.VUtils; +// using static VTools.VDebug; + + +namespace VTabs +{ + public static class VTabsMenu + { + + public static bool dragndropEnabled { get => EditorPrefsCached.GetBool("vTabs-dragndropEnabled", true); set => EditorPrefsCached.SetBool("vTabs-dragndropEnabled", value); } + public static bool addTabButtonEnabled { get => EditorPrefsCached.GetBool("vTabs-addTabButtonEnabled", false); set => EditorPrefsCached.SetBool("vTabs-addTabButtonEnabled", value); } + public static bool closeTabButtonEnabled { get => EditorPrefsCached.GetBool("vTabs-closeTabButtonEnabled", false); set => EditorPrefsCached.SetBool("vTabs-closeTabButtonEnabled", value); } + public static bool dividersEnabled { get => EditorPrefsCached.GetBool("vTabs-dividersEnabled", false); set => EditorPrefsCached.SetBool("vTabs-dividersEnabled", value); } + public static bool hideLockButtonEnabled { get => EditorPrefsCached.GetBool("vTabs-hideLockButtonEnabled", false); set => EditorPrefsCached.SetBool("vTabs-hideLockButtonEnabled", value); } + + public static int tabStyle { get => EditorPrefsCached.GetInt("vTabs-tabStyle", 0); set => EditorPrefsCached.SetInt("vTabs-tabStyle", value); } + public static bool defaultTabStyleEnabled => tabStyle == 0 || !Application.unityVersion.StartsWith("6000"); + public static bool largeTabStyleEnabled => tabStyle == 1 && Application.unityVersion.StartsWith("6000"); + public static bool neatTabStyleEnabled => tabStyle == 2 && Application.unityVersion.StartsWith("6000"); + + public static int backgroundStyle { get => EditorPrefsCached.GetInt("vTabs-backgroundStyle", 0); set => EditorPrefsCached.SetInt("vTabs-backgroundStyle", value); } + public static bool defaultBackgroundEnabled => backgroundStyle == 0 || !Application.unityVersion.StartsWith("6000"); + public static bool classicBackgroundEnabled => backgroundStyle == 1 && Application.unityVersion.StartsWith("6000"); + public static bool greyBackgroundEnabled => backgroundStyle == 2 && Application.unityVersion.StartsWith("6000"); + + + public static bool switchTabShortcutEnabled { get => EditorPrefsCached.GetBool("vTabs-switchTabShortcutEnabled", true); set => EditorPrefsCached.SetBool("vTabs-switchTabShortcutEnabled", value); } + public static bool addTabShortcutEnabled { get => EditorPrefsCached.GetBool("vTabs-addTabShortcutEnabled", true); set => EditorPrefsCached.SetBool("vTabs-addTabShortcutEnabled", value); } + public static bool closeTabShortcutEnabled { get => EditorPrefsCached.GetBool("vTabs-closeTabShortcutEnabled", true); set => EditorPrefsCached.SetBool("vTabs-closeTabShortcutEnabled", value); } + public static bool reopenTabShortcutEnabled { get => EditorPrefsCached.GetBool("vTabs-reopenTabShortcutEnabled", true); set => EditorPrefsCached.SetBool("vTabs-reopenTabShortcutEnabled", value); } + + public static bool sidescrollEnabled { get => EditorPrefsCached.GetBool("vTabs-sidescrollEnabled", Application.platform == RuntimePlatform.OSXEditor); set => EditorPrefsCached.SetBool("vTabs-sidescrollEnabled", value); } + public static float sidescrollSensitivity { get => EditorPrefsCached.GetFloat("vTabs-sidescrollSensitivity", 1); set => EditorPrefsCached.SetFloat("vTabs-sidescrollSensitivity", value); } + public static bool reverseScrollDirectionEnabled { get => EditorPrefs.GetBool("vTabs-reverseScrollDirectionDirection", false); set => EditorPrefs.SetBool("vTabs-reverseScrollDirectionDirection", value); } + + public static bool pluginDisabled { get => EditorPrefsCached.GetBool("vTabs-pluginDisabled", false); set => EditorPrefsCached.SetBool("vTabs-pluginDisabled", value); } + + + + + const string dir = "Tools/vTabs/"; +#if UNITY_EDITOR_OSX + const string cmd = "Cmd"; +#else + const string cmd = "Ctrl"; +#endif + + const string dragndrop = dir + "Create tabs with Drag-and-Drop"; + const string reverseScrollDirection = dir + "Reverse direction"; + const string addTabButton = dir + "Add Tab button"; + const string closeTabButton = dir + "Close Tab button"; + const string dividers = dir + "Tab dividers"; + const string hideLockButton = dir + "Hide lock button"; + + const string defaultTabStyle = dir + "Tab style/Default"; + const string largeTabs = dir + "Tab style/Large"; + const string neatTabs = dir + "Tab style/Neat"; + + const string defaultBackgroundStyle = dir + "Background style/Default"; + const string classicBackground = dir + "Background style/Classic"; + const string greyBackground = dir + "Background style/Grey"; + + + const string switchTabShortcut = dir + "Shift-Scroll to switch tab"; + const string addTabShortcut = dir + cmd + "-T to add tab"; + const string closeTabShortcut = dir + cmd + "-W to close tab"; + const string reopenTabShortcut = dir + cmd + "-Shift-T to reopen closed tab"; + + + const string sidescroll = dir + "Sidescroll to switch tab"; + const string increaseSensitivity = dir + "Increase sensitivity"; + const string decreaseSensitivity = dir + "Decrease sensitivity"; + + + const string disablePlugin = dir + "Disable vTabs"; + + + + + + + + [MenuItem(dir + "Features", false, 1)] static void dadsas() { } + [MenuItem(dir + "Features", true, 1)] static bool dadsas123() => false; + + // [MenuItem(dragndrop, false, 2)] static void dadsadsadasdsadadsas() => dragndropEnabled = !dragndropEnabled; + // [MenuItem(dragndrop, true, 2)] static bool dadsaddsasadadsdasadsas() { Menu.SetChecked(dragndrop, dragndropEnabled); return !pluginDisabled; } + + [MenuItem(addTabButton, false, 3)] static void dadsadsadsadasdsadadsas() { addTabButtonEnabled = !addTabButtonEnabled; VTabs.RepaintAllDockAreas(); } + [MenuItem(addTabButton, true, 3)] static bool dadsadasddsasadadsdasadsas() { Menu.SetChecked(addTabButton, addTabButtonEnabled); return !pluginDisabled; } + + [MenuItem(closeTabButton, false, 4)] static void dadsadsaddassadasdsadadsas() { closeTabButtonEnabled = !closeTabButtonEnabled; VTabs.RepaintAllDockAreas(); } + [MenuItem(closeTabButton, true, 4)] static bool dadsadasddsadsasadadsdasadsas() { Menu.SetChecked(closeTabButton, closeTabButtonEnabled); return !pluginDisabled; } + + [MenuItem(dividers, false, 5)] static void dadsadsaddasdssadasdsadadsas() { dividersEnabled = !dividersEnabled; VTabs.RepaintAllDockAreas(); } + [MenuItem(dividers, true, 5)] static bool dadsadasddsdsadsasadadsdasadsas() { Menu.SetChecked(dividers, dividersEnabled); return !pluginDisabled; } + + [MenuItem(hideLockButton, false, 7)] static void dadsadsaddsdassadasdsadadsas() { hideLockButtonEnabled = !hideLockButtonEnabled; VTabs.RepaintAllDockAreas(); } + [MenuItem(hideLockButton, true, 7)] static bool dadsadasdsdsadsasadadsdasadsas() { Menu.SetChecked(hideLockButton, hideLockButtonEnabled); return !pluginDisabled; } + +#if UNITY_6000_0_OR_NEWER + + [MenuItem(defaultTabStyle, false, 8)] static void dadsadsaddasdssadasdssdadadsas() { tabStyle = 0; VTabs.UpdateStyleSheet(); } + [MenuItem(defaultTabStyle, true, 8)] static bool dadsadasddsdsdsadsasadadsdasadsas() { Menu.SetChecked(defaultTabStyle, tabStyle == 0); return !pluginDisabled; } + + [MenuItem(largeTabs, false, 9)] static void dadsadsaddasdssadsdasdssdadadsas() { tabStyle = 1; VTabs.UpdateStyleSheet(); } + [MenuItem(largeTabs, true, 9)] static bool dadsadasddsdsdsdsadsasadadsdasadsas() { Menu.SetChecked(largeTabs, tabStyle == 1); return !pluginDisabled; } + + [MenuItem(neatTabs, false, 10)] static void dadsadsaddasdsssadasdssdadadsas() { tabStyle = 2; VTabs.UpdateStyleSheet(); } + [MenuItem(neatTabs, true, 10)] static bool dadsadasddsdsddssadsasadadsdasadsas() { Menu.SetChecked(neatTabs, tabStyle == 2); return !pluginDisabled; } + + + + [MenuItem(defaultBackgroundStyle, false, 11)] static void dadsadsaddasdsdssadasdssdadadsas() { backgroundStyle = 0; VTabs.UpdateStyleSheet(); } + [MenuItem(defaultBackgroundStyle, true, 11)] static bool dadsadasddssddsdsadsasadadsdasadsas() { Menu.SetChecked(defaultBackgroundStyle, backgroundStyle == 0); return !pluginDisabled; } + + [MenuItem(classicBackground, false, 12)] static void dadsadsadsddasdssadsdasdssdadadsas() { backgroundStyle = 1; VTabs.UpdateStyleSheet(); } + [MenuItem(classicBackground, true, 12)] static bool dadsadasddsdsdsdsdsadsasadadsdasadsas() { Menu.SetChecked(classicBackground, backgroundStyle == 1); return !pluginDisabled; } + + // [MenuItem(greyBackground, false, 12)] static void dadsadsdsadsddasdssadsdasdssdadadsas() { backgroundStyle = 2; VTabs.UpdateStyleSheet(); } + // [MenuItem(greyBackground, true, 12)] static bool dadsadasdsddsdsdsdsdsadsasadadsdasadsas() { Menu.SetChecked(greyBackground, backgroundStyle == 2); return !pluginDisabled; } + +#endif + + + + + [MenuItem(dir + "Shortcuts", false, 101)] static void daaadsas() { } + [MenuItem(dir + "Shortcuts", true, 101)] static bool daadsdsas123() => false; + + [MenuItem(switchTabShortcut, false, 102)] static void dadsadsadsadsadasdsadadsas() => switchTabShortcutEnabled = !switchTabShortcutEnabled; + [MenuItem(switchTabShortcut, true, 102)] static bool dadsadasdasddsasadadsdasadsas() { Menu.SetChecked(switchTabShortcut, switchTabShortcutEnabled); return !pluginDisabled; } + + [MenuItem(addTabShortcut, false, 103)] static void dadsadadsas() => addTabShortcutEnabled = !addTabShortcutEnabled; + [MenuItem(addTabShortcut, true, 103)] static bool dadsaddasadsas() { Menu.SetChecked(addTabShortcut, addTabShortcutEnabled); return !pluginDisabled; } + + [MenuItem(closeTabShortcut, false, 104)] static void dadsadasdadsas() => closeTabShortcutEnabled = !closeTabShortcutEnabled; + [MenuItem(closeTabShortcut, true, 104)] static bool dadsadsaddasadsas() { Menu.SetChecked(closeTabShortcut, closeTabShortcutEnabled); return !pluginDisabled; } + + [MenuItem(reopenTabShortcut, false, 105)] static void dadsadsadasdadsas() => reopenTabShortcutEnabled = !reopenTabShortcutEnabled; + [MenuItem(reopenTabShortcut, true, 105)] static bool dadsaddsasaddasadsas() { Menu.SetChecked(reopenTabShortcut, reopenTabShortcutEnabled); return !pluginDisabled; } + + + + +#if UNITY_EDITOR_OSX + + [MenuItem(dir + "Trackpad", false, 1001)] static void daadsdsadsas() { } + [MenuItem(dir + "Trackpad", true, 1001)] static bool dadsasasdads() => false; + + [MenuItem(sidescroll, false, 1002)] static void dadsadsadsadsadasdadssadadsas() => sidescrollEnabled = !sidescrollEnabled; + [MenuItem(sidescroll, true, 1002)] static bool dadsadasdasddsadassadadsdasadsas() { Menu.SetChecked(sidescroll, sidescrollEnabled); return !pluginDisabled; } + + [MenuItem(increaseSensitivity, false, 1004)] static void qdadadsssa() { sidescrollSensitivity += .2f; Debug.Log("vTabs: scrolling sensitivity increased to " + sidescrollSensitivity * 100 + "%"); } + [MenuItem(increaseSensitivity, true, 1004)] static bool qdaddasadsssa() => !pluginDisabled; + + [MenuItem(decreaseSensitivity, false, 1005)] static void qdasadsssa() { sidescrollSensitivity -= .2f; Debug.Log("vTabs: trackpad sensitivity decreased to " + sidescrollSensitivity * 100 + "%"); } + [MenuItem(decreaseSensitivity, true, 1005)] static bool qdaddasdsaadsssa() => !pluginDisabled; + + // [MenuItem(reverseScrollDirection, false, 1006)] static void dadsadadssadsadsadasdadssadadsas() => reverseScrollDirectionEnabled = !reverseScrollDirectionEnabled; + // [MenuItem(reverseScrollDirection, true, 1006)] static bool dadsadasdadsasddsadassadadsdasadsas() { Menu.SetChecked(reverseScrollDirection, reverseScrollDirectionEnabled); return !pluginDisabled; } // don't delete the option, there are people using it + +#endif + + + + + + + [MenuItem(dir + "More", false, 10001)] static void daasadsddsas() { } + [MenuItem(dir + "More", true, 10001)] static bool dadsadsdasas123() => false; + + [MenuItem(dir + "Open manual", false, 10002)] + static void dadadssadsas() => Application.OpenURL("https://kubacho-lab.gitbook.io/vtabs-2"); + + [MenuItem(dir + "Join our Discord", false, 10003)] + static void dadasdsas() => Application.OpenURL("https://discord.gg/pUektnZeJT"); + + + + + [MenuItem(disablePlugin, false, 100001)] static void dadsadsdasadasdasdsadadsas() { pluginDisabled = !pluginDisabled; VTabs.UpdateStyleSheet(); UnityEditor.Compilation.CompilationPipeline.RequestScriptCompilation(); } + [MenuItem(disablePlugin, true, 100001)] static bool dadsaddssdaasadsadadsdasadsas() { Menu.SetChecked(disablePlugin, pluginDisabled); return true; } + + + } +} +#endif \ No newline at end of file diff --git a/Assets/vTabs/VTabsMenu.cs.meta b/Assets/vTabs/VTabsMenu.cs.meta new file mode 100644 index 0000000..a57bb9e --- /dev/null +++ b/Assets/vTabs/VTabsMenu.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 3cf48ada98a1147c4881faf7b79475e6 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 253396 + packageName: vTabs 2 + packageVersion: 2.1.6 + assetPath: Assets/vTabs/VTabsMenu.cs + uploadId: 874244 diff --git a/Assets/vTabs/VTabsMenuItems.cs b/Assets/vTabs/VTabsMenuItems.cs new file mode 100644 index 0000000..8fb6b18 --- /dev/null +++ b/Assets/vTabs/VTabsMenuItems.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/Assets/vTabs/VTabsMenuItems.cs.meta b/Assets/vTabs/VTabsMenuItems.cs.meta new file mode 100644 index 0000000..d4989bd --- /dev/null +++ b/Assets/vTabs/VTabsMenuItems.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 102836a637c684c11b7581d444fecb04 +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 253396 + packageName: vTabs 2 + packageVersion: 2.1.6 + assetPath: Assets/vTabs/VTabsMenuItems.cs + uploadId: 874244 diff --git a/Assets/vTabs/VTabsPlaceholderWindow.cs b/Assets/vTabs/VTabsPlaceholderWindow.cs new file mode 100644 index 0000000..24da072 --- /dev/null +++ b/Assets/vTabs/VTabsPlaceholderWindow.cs @@ -0,0 +1,242 @@ +#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 System.Diagnostics; +using Type = System.Type; +using Delegate = System.Delegate; +using Action = System.Action; +using static VTabs.Libs.VUtils; +using static VTabs.Libs.VGUI; +// using static VTools.VDebug; + + + +namespace VTabs +{ + public class VTabsPlaceholderWindow : EditorWindow + { + + + void OnGUI() + { + + // GUILayout.Label(objectGlobalID.ToString()); + // GUILayout.Label(objectGlobalID.guid.ToPath()); + + + // if (isSceneObject) + // GUILayout.Label("scene object"); + + // if (isPrefabObject) + // GUILayout.Label("prefab object"); + + + + var fontSize = 13; + + + var assetName = objectGlobalID.guid.ToPath().GetFilename(); + + var assetIcon = AssetDatabase.GetCachedIcon(objectGlobalID.guid.ToPath()); + + + void label() + { + + GUI.skin.label.fontSize = fontSize; + + + + GUILayout.Label("This object is from " + assetName + ", which isn't loaded"); + + + var iconRect = lastRect.MoveX("This object is from".GetLabelWidth()).SetWidth(20).SetSizeFromMid(16).MoveX(.5f); + + GUI.DrawTexture(iconRect, assetIcon); + + + + GUI.skin.label.fontSize = 0; + + } + void button() + { + GUI.skin.button.fontSize = fontSize; + + + var buttonText = "Load " + assetName; + + if (GUILayout.Button(buttonText, GUILayout.Height(30), GUILayout.Width(buttonText.GetLabelWidth(fontSize: fontSize) + 34))) + if (isPrefabObject) + PrefabStageUtility.OpenPrefab(objectGlobalID.guid.ToPath()); + else if (isSceneObject) + EditorSceneManager.OpenScene(objectGlobalID.guid.ToPath()); + { } // todonow + + + + var iconRect = lastRect.MoveX("Load".GetLabelWidth()).SetWidth(20).SetSizeFromMid(16).MoveX(23 - 3); + + GUI.DrawTexture(iconRect, assetIcon); + + + + + GUI.skin.button.fontSize = 0; + + + } + + + GUILayout.Space(15); + // BeginIndent(10); + GUILayout.BeginHorizontal(); + GUILayout.Space(10); + GUILayout.BeginVertical(); + + + label(); + + Space(10); + button(); + + GUILayout.EndVertical(); + GUILayout.EndHorizontal(); + + + + void tryLoadPrefabObject() + { + if (!isPrefabObject) return; + if (StageUtility.GetCurrentStage() is not PrefabStage prefabStage) return; + if (prefabStage.assetPath != objectGlobalID.guid.ToPath()) return; + + if (objectGlobalID.GetObject() is not Object prefabAssetObject) return; + + + + if (prefabAssetObject is Component assetComponent) + if (prefabStage.prefabContentsRoot.GetComponentsInChildren(assetComponent.GetType()) + .FirstOrDefault(r => GlobalID.GetForPrefabStageObject(r) == objectGlobalID) is Component instanceComoponent) + Close_andOpenPropertyEditor(instanceComoponent); + + + + if (prefabAssetObject is GameObject assetGo) + if (prefabStage.prefabContentsRoot.GetComponentsInChildren() + .Select(r => r.gameObject) + .FirstOrDefault(r => GlobalID.GetForPrefabStageObject(r) == objectGlobalID) is GameObject isntanceGo) + Close_andOpenPropertyEditor(isntanceGo); + + } + void tryLoadSceneObject() + { + if (!isSceneObject) return; + + var loadedScenes = Enumerable.Range(0, EditorSceneManager.sceneCount) + .Select(i => EditorSceneManager.GetSceneAt(i)) + .Where(r => r.isLoaded); + if (!loadedScenes.Any(r => r.path == objectGlobalID.guid.ToPath())) return; + + if (objectGlobalID.GetObject() is not Object loadedObject) return; + + + Close_andOpenPropertyEditor(loadedObject); + + + } + + + tryLoadPrefabObject(); + tryLoadSceneObject(); + + + + } + + + + + public void Close_andOpenPropertyEditor(Object o) + { + var dockArea = this.GetMemberValue("m_Parent"); + var tabIndex = dockArea.GetMemberValue>("m_Panes").IndexOf(this); + + + var tabInfo = new VTabs.TabInfo(o); + + tabInfo.originalTabIndex = tabIndex; + + + VTabs.guis_byDockArea[dockArea].AddTab(tabInfo, atOriginalTabIndex: true); + + + + this.Close(); + + } + + + + + + + + + + + public void Open_andReplacePropertyEditor(EditorWindow propertyEditorToReplace) + { + + objectGlobalID = new GlobalID(propertyEditorToReplace.GetMemberValue("m_GlobalObjectId")); + + + isSceneObject = AssetDatabase.GetMainAssetTypeAtPath(objectGlobalID.guid.ToPath()) == typeof(SceneAsset); + isPrefabObject = AssetDatabase.GetMainAssetTypeAtPath(objectGlobalID.guid.ToPath()) == typeof(GameObject); + + if (!isSceneObject && !isPrefabObject) { propertyEditorToReplace.Close(); Object.DestroyImmediate(this); return; } + + + + + var dockArea = propertyEditorToReplace.GetMemberValue("m_Parent"); + + var tabIndex = dockArea.GetMemberValue>("m_Panes") + .IndexOf(propertyEditorToReplace); + + dockArea.InvokeMethod("AddTab", tabIndex, this, true); + + + + + + this.titleContent = propertyEditorToReplace.titleContent; + + + if (propertyEditorToReplace.hasFocus) + this.Focus(); + + + propertyEditorToReplace.Close(); + + + } + + public GlobalID objectGlobalID; + + public bool isSceneObject; + public bool isPrefabObject; + + // todonow scene config? active/additive, if active, which otehr additive scenes were loaded? + + } +} +#endif \ No newline at end of file diff --git a/Assets/vTabs/VTabsPlaceholderWindow.cs.meta b/Assets/vTabs/VTabsPlaceholderWindow.cs.meta new file mode 100644 index 0000000..faff3ef --- /dev/null +++ b/Assets/vTabs/VTabsPlaceholderWindow.cs.meta @@ -0,0 +1,18 @@ +fileFormatVersion: 2 +guid: 176a13b0483f24d39af2f0b00467885b +MonoImporter: + externalObjects: {} + serializedVersion: 2 + defaultReferences: [] + executionOrder: 0 + icon: {instanceID: 0} + userData: + assetBundleName: + assetBundleVariant: +AssetOrigin: + serializedVersion: 1 + productId: 253396 + packageName: vTabs 2 + packageVersion: 2.1.6 + assetPath: Assets/vTabs/VTabsPlaceholderWindow.cs + uploadId: 874244