From 88109aa98df5abf8212e6147a8f460554a9cacb4 Mon Sep 17 00:00:00 2001 From: Konstantin Dyachenko Date: Sat, 28 Mar 2026 12:57:20 +0700 Subject: [PATCH] [Add] VTabs 2 --- Assets/vTabs.meta | 8 + Assets/vTabs/Read me.pdf | Bin 0 -> 49974 bytes Assets/vTabs/Read me.pdf.meta | 14 + Assets/vTabs/VTabs.asmdef | 16 + Assets/vTabs/VTabs.asmdef.meta | 14 + Assets/vTabs/VTabs.cs | 1571 +++++++++++++++ Assets/vTabs/VTabs.cs.meta | 18 + Assets/vTabs/VTabsAddTabWindow.cs | 1220 ++++++++++++ Assets/vTabs/VTabsAddTabWindow.cs.meta | 18 + Assets/vTabs/VTabsCache.cs | 42 + Assets/vTabs/VTabsCache.cs.meta | 18 + Assets/vTabs/VTabsGUI.cs | 1005 ++++++++++ Assets/vTabs/VTabsGUI.cs.meta | 18 + Assets/vTabs/VTabsLibs.cs | 1907 +++++++++++++++++++ Assets/vTabs/VTabsLibs.cs.meta | 18 + Assets/vTabs/VTabsMenu.cs | 197 ++ Assets/vTabs/VTabsMenu.cs.meta | 18 + Assets/vTabs/VTabsMenuItems.cs | 6 + Assets/vTabs/VTabsMenuItems.cs.meta | 18 + Assets/vTabs/VTabsPlaceholderWindow.cs | 242 +++ Assets/vTabs/VTabsPlaceholderWindow.cs.meta | 18 + 21 files changed, 6386 insertions(+) create mode 100644 Assets/vTabs.meta create mode 100644 Assets/vTabs/Read me.pdf create mode 100644 Assets/vTabs/Read me.pdf.meta create mode 100644 Assets/vTabs/VTabs.asmdef create mode 100644 Assets/vTabs/VTabs.asmdef.meta create mode 100644 Assets/vTabs/VTabs.cs create mode 100644 Assets/vTabs/VTabs.cs.meta create mode 100644 Assets/vTabs/VTabsAddTabWindow.cs create mode 100644 Assets/vTabs/VTabsAddTabWindow.cs.meta create mode 100644 Assets/vTabs/VTabsCache.cs create mode 100644 Assets/vTabs/VTabsCache.cs.meta create mode 100644 Assets/vTabs/VTabsGUI.cs create mode 100644 Assets/vTabs/VTabsGUI.cs.meta create mode 100644 Assets/vTabs/VTabsLibs.cs create mode 100644 Assets/vTabs/VTabsLibs.cs.meta create mode 100644 Assets/vTabs/VTabsMenu.cs create mode 100644 Assets/vTabs/VTabsMenu.cs.meta create mode 100644 Assets/vTabs/VTabsMenuItems.cs create mode 100644 Assets/vTabs/VTabsMenuItems.cs.meta create mode 100644 Assets/vTabs/VTabsPlaceholderWindow.cs create mode 100644 Assets/vTabs/VTabsPlaceholderWindow.cs.meta 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 0000000000000000000000000000000000000000..490eb6d969c00668475260774a018c4e49d355a1 GIT binary patch literal 49974 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%0D0DL&D}UuU`IXfIREH!!|HOrKG%=*mZ5j; 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 literal 0 HcmV?d00001 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