#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; namespace VTabs { public static class VTabs { static void Update() { var lastEvent = typeof(Event).GetFieldValue("s_Current"); void scrollInteractions() { if (isKeyPressed) { sidesscrollPosition = 0; return; } if (lastEvent.delta == Vector2.zero) return; if (lastEvent.type == EventType.MouseMove) { sidesscrollPosition = 0; return; } if (lastEvent.type == EventType.MouseDrag) { sidesscrollPosition = 0; return; } if (lastEvent.type != EventType.ScrollWheel && delayedMousePosition_screenSpace != EditorGUIUtility.GUIToScreenPoint(lastEvent.mousePosition)) return; // uncaptured mouse move/drag check if (lastEvent.type != EventType.ScrollWheel && Application.platform == RuntimePlatform.OSXEditor && lastEvent.delta.x == (int)lastEvent.delta.x) return; // osx uncaptured mouse move/drag in sceneview ang gameview workaround void switchTab(int dir) { if (!VTabsMenu.switchTabsEnabled) return; if (!(EditorWindow.mouseOverWindow is EditorWindow hoveredWindow)) return; if (!hoveredWindow.docked) return; if (hoveredWindow.maximized) return; var tabs = GetTabList(hoveredWindow); var i0 = tabs.IndexOf(hoveredWindow); var i1 = Mathf.Clamp(i0 + dir, 0, tabs.Count - 1); tabs[i1].Focus(); UpdateTitle(tabs[i1]); } void moveTab(int dir) { if (!VTabsMenu.moveTabsEnabled) return; if (!(EditorWindow.mouseOverWindow is EditorWindow hoveredWindow)) return; var tabs = GetTabList(hoveredWindow); var i0 = tabs.IndexOf(hoveredWindow); 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 (!lastEvent.shift) return; var scrollDelta = Application.platform == RuntimePlatform.OSXEditor ? lastEvent.delta.x // osx sends delta.y as delta.x when shift is pressed : lastEvent.delta.x - lastEvent.delta.y; // some software on windows (eg logitech options) may do that too if (VTabsMenu.reverseScrollDirectionEnabled) scrollDelta *= -1; if (scrollDelta != 0) if (lastEvent.control || lastEvent.command) moveTab(scrollDelta > 0 ? 1 : -1); else switchTab(scrollDelta > 0 ? 1 : -1); } void sidescroll() { if (lastEvent.shift) return; if (lastEvent.delta.x == 0) return; if (lastEvent.delta.x.Abs() <= 0.06f) return; if (lastEvent.delta.x.Abs() * 1.1f < lastEvent.delta.y.Abs()) { sidesscrollPosition = 0; return; } if (!VTabsMenu.sidescrollEnabled) 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 = lastEvent.delta.x.Abs() * dampenK; var deltaDampened = (a < 1 ? a : Mathf.Log(a) + 1) / dampenK * lastEvent.delta.x.Sign(); var sensitivityK = .22f; var sidescrollDelta = deltaDampened * VTabsMenu.sidescrollSensitivity * sensitivityK; if (VTabsMenu.reverseScrollDirectionEnabled) sidescrollDelta *= -1; if (sidesscrollPosition.RoundToInt() != (sidesscrollPosition += sidescrollDelta).RoundToInt()) if (lastEvent.control || lastEvent.command) moveTab(sidescrollDelta < 0 ? 1 : -1); else switchTab(sidescrollDelta < 0 ? 1 : -1); } shiftscroll(); sidescroll(); } void scrollAnimation() { if (!EditorWindow.focusedWindow) return; if (!EditorWindow.focusedWindow.docked) return; if (EditorWindow.focusedWindow.maximized) return; var dockArea = EditorWindow.focusedWindow.GetMemberValue("m_Parent"); if (dockArea.GetType() != t_DockArea) return; // happens on 2021.1.28 var curScroll = dockArea.GetFieldValue("m_ScrollOffset"); if (!curScroll.Approx(0)) curScroll -= nonZeroTabScrollOffset; if (curScroll == 0 && prevFocusedDockArea == dockArea) curScroll = prevScroll; var targScroll = GetOptimalTabScrollerPosition(EditorWindow.focusedWindow); var animationSpeed = 7f; var deltaTime = (EditorApplication.timeSinceStartup - prevTime).ToFloat().Min(.03f); var newScroll = SmoothDamp(curScroll, targScroll, animationSpeed, ref scrollDeriv, deltaTime); if (newScroll < .5f) newScroll = 0; prevScroll = newScroll; prevFocusedDockArea = dockArea; prevTime = EditorApplication.timeSinceStartup; if (newScroll.Approx(curScroll)) return; if (!newScroll.Approx(0)) newScroll += nonZeroTabScrollOffset; dockArea.SetFieldValue("m_ScrollOffset", newScroll); EditorWindow.focusedWindow.Repaint(); } void createWindowDelayed() { if (createWindowDelayedAction == null) return; if ((System.DateTime.UtcNow - lastDragndropTime).TotalSeconds < .05f) return; createWindowDelayedAction.Invoke(); createWindowDelayedAction = null; } void dragndrop() { var uncaughtDragPerform = lastKnownDraggedObject != null && !DragAndDrop.objectReferences.Any() && curEvent.isMouseEnterWindow && Application.platform == RuntimePlatform.LinuxEditor; if (!VTabsMenu.dragndropEnabled) return; if (lastEvent.type != EventType.DragUpdated && lastEvent.type != EventType.DragPerform && !uncaughtDragPerform) return; if (!(EditorWindow.mouseOverWindow is EditorWindow hoveredWindow)) return; if (!hoveredWindow.position.SetPos(0, 0).SetHeight(hoveredWindow.GetType() == t_SceneHierarchyWindow ? 5 : 40).Contains(lastEvent.mousePosition)) return; DragAndDrop.visualMode = DragAndDropVisualMode.Copy; if (DragAndDrop.objectReferences.Any()) // workaround for null objectReferences and mouseOverWindow on linux { lastKnownDraggedObject = DragAndDrop.objectReferences.First(); lastKnownHoveredWindow = EditorWindow.mouseOverWindow; } if (lastEvent.type != EventType.DragPerform && !uncaughtDragPerform) return; if (!lastKnownDraggedObject) return; if (!lastKnownHoveredWindow) return; if (lastDragndropPosition == curEvent.mousePosition) return; DragAndDrop.AcceptDrag(); var lockToObject = lastKnownDraggedObject; var dockArea = lastKnownHoveredWindow.GetMemberValue("m_Parent"); createWindowDelayedAction = () => new TabInfo(lockToObject).CreateWindow(dockArea, false); // not creating window right away to avoid scroll animation stutter lastDragndropPosition = curEvent.mousePosition; lastDragndropTime = System.DateTime.UtcNow; lastKnownDraggedObject = null; lastKnownHoveredWindow = null; } scrollInteractions(); scrollAnimation(); createWindowDelayed(); dragndrop(); CheckIfFocusedWindowChanged(); CheckIfWindowWasUnmaximized(); } static float sidesscrollPosition; static float scrollDeriv; static float prevScroll; static object prevFocusedDockArea; static double deltaTime; static double prevTime = 0; static Vector2 lastDragndropPosition; static System.DateTime lastDragndropTime; static Action createWindowDelayedAction; static Object lastKnownDraggedObject; static EditorWindow lastKnownHoveredWindow; static void CheckShortcuts() // globalEventHandler { void set_isKeyPressed() { if (curEvent.keyCode == KeyCode.LeftShift) return; if (curEvent.keyCode == KeyCode.LeftControl) return; if (curEvent.keyCode == KeyCode.LeftCommand) return; if (curEvent.keyCode == KeyCode.RightShift) return; if (curEvent.keyCode == KeyCode.RightControl) return; if (curEvent.keyCode == KeyCode.RightCommand) return; if (Event.current.type == EventType.KeyDown) isKeyPressed = true; if (Event.current.type == EventType.KeyUp) isKeyPressed = false; } void addTab() { if (!curEvent.isKeyDown) return; if (!curEvent.holdingCmdOnly && !curEvent.holdingCtrlOnly) return; if (curEvent.keyCode != KeyCode.T) return; if (!EditorWindow.mouseOverWindow) return; if (!VTabsMenu.addTabEnabled) return; curEvent.Use(); addTabMenu_openedOverWindow = EditorWindow.mouseOverWindow; List defaultTabList; List customTabList; var customTabListKey = "vTabs-CustomTabList-" + GetProjectId(); void loadDefaultTabList() { defaultTabList = new List(); defaultTabList.Add(new TabInfo("SceneView", "Scene")); defaultTabList.Add(new TabInfo("GameView", "Game")); defaultTabList.Add(new TabInfo("ProjectBrowser", "Project")); defaultTabList.Add(new TabInfo("InspectorWindow", "Inspector")); defaultTabList.Add(new TabInfo("ConsoleWindow", "Console")); defaultTabList.Add(new TabInfo("ProfilerWindow", "Profiler")); // defaultTabList.Add(new TabInfo("LightingWindow", "Lighting")); // defaultTabList.Add(new TabInfo("ProjectSettingsWindow", "Project Settings")); } void loadSavedTabList() { var json = EditorPrefs.GetString(customTabListKey); customTabList = JsonUtility.FromJson(json)?.list ?? new List(); } void saveSavedTabsList() { var json = JsonUtility.ToJson(new TabInfoList { list = customTabList }); EditorPrefs.SetString(customTabListKey, json); } loadDefaultTabList(); loadSavedTabList(); GenericMenu menu = new GenericMenu(); Vector2 menuPosition; void rememberClickPosition() { addTabMenu_lastClickPosition_screenSpace = curEvent.mousePosition_screenSpace; } void set_menuPosition() { if (curEvent.mousePosition_screenSpace.DistanceTo(addTabMenu_lastClickPosition_screenSpace) < 2) menuPosition = EditorGUIUtility.ScreenToGUIPoint(addTabMenu_lastOpenPosition_screenSpace); else menuPosition = curEvent.mousePosition - Vector2.up * 9; addTabMenu_lastOpenPosition_screenSpace = EditorGUIUtility.GUIToScreenPoint(menuPosition); #if !UNITY_2021_2_OR_NEWER if (EditorWindow.focusedWindow) menuPosition += EditorWindow.focusedWindow.position.position; #endif } void defaultTabs() { menu.AddDisabledItem(new GUIContent("Default tabs")); foreach (var tabInfo in defaultTabList) menu.AddItem(new GUIContent(tabInfo.menuItemName), false, () => { tabInfo.CreateWindow(GetDockArea(addTabMenu_openedOverWindow), false); EditorApplication.delayCall += rememberClickPosition; }); } void savedTabs() { if (!customTabList.Any()) return; menu.AddSeparator(""); menu.AddDisabledItem(new GUIContent("Saved tabs")); foreach (var tabInfo in customTabList) if (tabInfo.isPropertyEditor && !tabInfo.globalId.GetObject()) menu.AddDisabledItem(new GUIContent(tabInfo.menuItemName)); else menu.AddItem(new GUIContent(tabInfo.menuItemName), false, () => { tabInfo.CreateWindow(GetDockArea(addTabMenu_openedOverWindow), false); EditorApplication.delayCall += rememberClickPosition; }); } void remove() { if (!customTabList.Any()) return; foreach (var tabInfo in customTabList) menu.AddItem(new GUIContent("Remove/" + tabInfo.menuItemName), false, () => { customTabList.Remove(tabInfo); saveSavedTabsList(); EditorApplication.delayCall += rememberClickPosition; }); menu.AddSeparator("Remove/"); menu.AddItem(new GUIContent("Remove/Remove all"), false, () => { customTabList.Clear(); saveSavedTabsList(); EditorApplication.delayCall += rememberClickPosition; }); } void saveCurrentTab() { var menuItemName = addTabMenu_openedOverWindow.titleContent.text.Replace("/", " \u2215 ").Trim(' '); if (defaultTabList.Any(r => r.menuItemName == menuItemName)) return; if (customTabList.Any(r => r.menuItemName == menuItemName)) return; menu.AddSeparator(""); menu.AddItem(new GUIContent("Save current tab"), false, () => { customTabList.Add(new TabInfo(addTabMenu_openedOverWindow)); saveSavedTabsList(); EditorApplication.delayCall += rememberClickPosition; }); } set_menuPosition(); defaultTabs(); savedTabs(); remove(); saveCurrentTab(); menu.DropDown(Rect.zero.SetPos(menuPosition)); } void closeTab() { if (!curEvent.isKeyDown) return; if (!curEvent.holdingCmdOnly && !curEvent.holdingCtrlOnly) return; if (curEvent.keyCode != KeyCode.W) return; if (!VTabsMenu.closeTabEnabled) return; if (!EditorWindow.mouseOverWindow) return; if (!EditorWindow.mouseOverWindow.docked) return; if (GetTabList(EditorWindow.mouseOverWindow).Count <= 1) return; Event.current.Use(); tabInfosForReopening.Push(new TabInfo(EditorWindow.mouseOverWindow)); EditorWindow.mouseOverWindow.Close(); } void reopenTab() { if (!curEvent.isKeyDown) return; if (curEvent.modifiers != (EventModifiers.Command | EventModifiers.Shift) && curEvent.modifiers != (EventModifiers.Control | EventModifiers.Shift)) return; if (curEvent.keyCode != KeyCode.T) return; if (!EditorWindow.mouseOverWindow) return; if (!VTabsMenu.reopenTabEnabled) return; if (!tabInfosForReopening.Any()) return; Event.current.Use(); var window = tabInfosForReopening.Pop().CreateWindow(); UpdateTitle(window); } set_isKeyPressed(); addTab(); closeTab(); reopenTab(); } static Vector2 addTabMenu_lastClickPosition_screenSpace; static Vector2 addTabMenu_lastOpenPosition_screenSpace; static EditorWindow addTabMenu_openedOverWindow; static bool isKeyPressed; static Stack tabInfosForReopening = new Stack(); [System.Serializable] class TabInfo { public EditorWindow CreateWindow(object dockArea = null, bool atOriginalTabIndex = true) { if (dockArea == null) dockArea = originalDockArea; if (dockArea == null) return null; if (dockArea.GetType() != t_DockArea) return null; // happens in 2023.2, no idea why var lastInteractedBrowser = t_ProjectBrowser.GetFieldValue("s_LastInteractedProjectBrowser"); // changes on new browser creation // tomove to seutup var window = (EditorWindow)ScriptableObject.CreateInstance(typeName); void notifyVFavorites() { mi_VFavorites_BeforeWindowCreated?.Invoke(null, new object[] { dockArea }); } void addToDockArea() { if (atOriginalTabIndex) dockArea.InvokeMethod("AddTab", originalTabIndex, window, true); else dockArea.InvokeMethod("AddTab", window, true); } void setupBrowser() { if (!isBrowser) return; void setSavedGridSize() { if (!isGridSizeSaved) return; window.GetFieldValue("m_ListArea")?.SetMemberValue("gridSize", savedGridSize); } void setLastUsedGridSize() { if (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 (!isLayoutSaved) return; var layoutEnum = System.Enum.ToObject(t_ProjectBrowser.GetField("m_ViewMode", maxBindingFlags).FieldType, savedLayout); window.InvokeMethod("SetViewMode", layoutEnum); } void setLastUsedLayout() { if (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 (!isLocked) return; if (window.GetMemberValue("m_ViewMode") != 1) return; if (folderGuid.IsNullOrEmpty()) return; var iid = AssetDatabase.LoadAssetAtPath(AssetDatabase.GUIDToAssetPath(folderGuid)).GetInstanceID(); window.GetFieldValue("m_ListAreaState").SetFieldValue("m_SelectedInstanceIDs", new List { iid }); t_ProjectBrowser.InvokeMethod("OpenSelectedFolders"); window.SetPropertyValue("isLocked", true); } void lockToFolder_oneColumn() { if (!isLocked) return; if (window.GetMemberValue("m_ViewMode") != 0) return; if (folderGuid.IsNullOrEmpty()) return; if (!(window.GetMemberValue("m_AssetTree") is object m_AssetTree)) return; if (!(m_AssetTree.GetMemberValue("data") is object data)) return; var folderPath = folderGuid.ToPath(); var folderIid = AssetDatabase.LoadAssetAtPath(folderPath).GetInstanceID(); data.SetMemberValue("m_rootInstanceID", folderIid); m_AssetTree.InvokeMethod("ReloadData"); window.GetMemberValue("m_SearchFilter")?.SetMemberValue("m_Folders", new[] { folderPath }); window.SetPropertyValue("isLocked", true); } window.InvokeMethod("Init"); setSavedGridSize(); setLastUsedGridSize(); setSavedLayout(); setLastUsedLayout(); setLastUsedListWidth(); lockToFolder_twoColumns(); lockToFolder_oneColumn(); UpdateBrowserTitle(window); } void setupPropertyEditor() { if (!isPropertyEditor) return; if (globalId.isNull) return; var lockTo = globalId.GetObject(); if (lockedPrefabAssetObject) lockTo = 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 })); window.SetMemberValue("m_GlobalObjectId", globalId.ToString()); window.SetMemberValue("m_InspectedObject", lockTo); UpdatePropertyEditorTitle(window); } void setCustomEditorWindowTitle() { if (window.titleContent.text != window.GetType().FullName) return; if (originalTitle.IsNullOrEmpty()) return; window.titleContent.text = 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 TabInfo(EditorWindow window) { typeName = window.GetType().Name; originalDockArea = GetDockArea(window); originalTabIndex = GetTabList(window).IndexOf(window); 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.GetMemberValue("m_SearchFilter")?.GetMemberValue("m_Folders")?.FirstOrDefault() ?? "Assets" // 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 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(); } 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 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.GetMemberValue("m_SearchFilter")?.GetMemberValue("m_Folders")?.FirstOrDefault() ?? "Assets" : browser.InvokeMethod("GetActiveFolderPath"); var guid = path.ToGuid(); var name = path.GetFilename(); var icon = EditorGUIUtility.FindTexture("Project"); 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 UpdateBrowserTitles() => allBrowsers.ToList().ForEach(r => UpdateBrowserTitle(r)); 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 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(); } static void UpdatePropertyEditorTitles() => allPropertyEditors.ForEach(r => UpdatePropertyEditorTitle(r)); static Dictionary adjustedObjectIconsBySourceIid = new Dictionary(); static void UpdateGUIWrappingForBrowser(EditorWindow browser) { if (!browser.hasFocus) return; if (mi_VFavorites_CanBrowserBeWrapped != null && mi_VFavorites_CanBrowserBeWrapped.Invoke(null, new[] { browser }).Equals(false)) return; var isLocked = browser.GetMemberValue("isLocked"); var isWrapped = browser.GetMemberValue("m_Parent").GetMemberValue("m_OnGUI").Method == mi_WrappedBrowserOnGUI; void wrap() { if (!isLocked) return; if (isWrapped) 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 (isLocked) return; if (!isWrapped) return; var hostView = browser.GetMemberValue("m_Parent"); var originalDelegate = hostView.InvokeMethod("CreateDelegate", "OnGUI"); hostView.SetMemberValue("m_OnGUI", originalDelegate); browser.Repaint(); } wrap(); unwrap(); } static void UpdateGUIWrappingForAllBrowsers() => allBrowsers.ForEach(r => UpdateGUIWrappingForBrowser(r)); 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; void setRootForOneColumn() { if (!isOneColumn) return; if (curEvent.isRepaint) return; if (!(browser.GetMemberValue("m_AssetTree") is object m_AssetTree)) return; if (!(m_AssetTree.GetMemberValue("data") is object data)) return; var m_rootInstanceID = data.GetMemberValue("m_rootInstanceID"); void setInitial() { if (m_rootInstanceID != 0) return; var folderPath = browser.GetMemberValue("m_SearchFilter")?.GetMemberValue("m_Folders")?.FirstOrDefault() ?? "Assets"; var folderIid = AssetDatabase.LoadAssetAtPath(folderPath).GetInstanceID(); data.SetMemberValue("m_rootInstanceID", folderIid); m_AssetTree.InvokeMethod("ReloadData"); } void update() { if (m_rootInstanceID == 0) return; var folderIid = m_rootInstanceID; var folderPath = EditorUtility.InstanceIDToObject(folderIid).GetPath(); browser.GetMemberValue("m_SearchFilter")?.SetMemberValue("m_Folders", new[] { folderPath }); } void reset() { if (browser.GetMemberValue("isLocked")) return; data.SetMemberValue("m_rootInstanceID", 0); browser.GetMemberValue("m_SearchFilter")?.SetMemberValue("m_Folders", new[] { "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(); browser.InvokeMethod("SetFolderSelection", new[] { folderIid }, false); 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"); } setRootForOneColumn(); handleFolderChange(); oneColumn(); twoColumns(); } static void ReplaceTabScrollerButtonsWithGradients() { 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 createTextures() { if (leftScrollerGradient != null && rightScrollerGradient != null && clearTexture != null) return; clearTexture = new Texture2D(1, 1); clearTexture.hideFlags = HideFlags.DontSave; clearTexture.SetPixel(0, 0, Color.clear); clearTexture.Apply(); var res = 16; var greyscale = EditorGUIUtility.isProSkin ? .16f : .65f; leftScrollerGradient = new Texture2D(res, 1); leftScrollerGradient.hideFlags = HideFlags.DontSave; leftScrollerGradient.SetPixels(Enumerable.Range(0, res).Select(r => Greyscale(greyscale, r / (res - 1f))).Reverse().ToArray(), 0); leftScrollerGradient.Apply(); rightScrollerGradient = new Texture2D(res, 1); rightScrollerGradient.hideFlags = HideFlags.DontSave; rightScrollerGradient.SetPixels(Enumerable.Range(0, res).Select(r => Greyscale(greyscale, r / (res - 1f))).ToArray(), 0); rightScrollerGradient.Apply(); } void assignTextures() { if (leftScrollerStyle == null) return; if (rightScrollerStyle == null) return; // var hideLeftGradient = EditorWindow.focusedWindow && EditorWindow.focusedWindow.docked && GetTabList(EditorWindow.focusedWindow).First() == EditorWindow.focusedWindow; // var hideRightGradient = EditorWindow.focusedWindow && EditorWindow.focusedWindow.docked && GetTabList(EditorWindow.focusedWindow).Last() == EditorWindow.focusedWindow; var hideLeftGradient = false; var hideRightGradient = false; leftScrollerStyle.normal.background = hideLeftGradient ? clearTexture : leftScrollerGradient; rightScrollerStyle.normal.background = hideRightGradient ? clearTexture : rightScrollerGradient; } getStyles(); createTextures(); assignTextures(); } static GUIStyle leftScrollerStyle; static GUIStyle rightScrollerStyle; static Texture2D leftScrollerGradient; static Texture2D rightScrollerGradient; static Texture2D clearTexture; static void ClosePropertyEditorsWithNonLoadableObjects() { foreach (var propertyEditor in allPropertyEditors) if (propertyEditor.GetMemberValue("m_InspectedObject") == null) propertyEditor.Close(); } static void LoadPropertyEditorInspectedObjects() { foreach (var propertyEditor in allPropertyEditors) propertyEditor.InvokeMethod("LoadPersistedObject"); } static void EnsureTabVisibleOnScroller(EditorWindow window) { var pos = GetOptimalTabScrollerPosition(window); if (!pos.Approx(0)) pos += nonZeroTabScrollOffset; GetDockArea(window).SetFieldValue("m_ScrollOffset", pos); } static void EnsureActiveTabsVisibleOnScroller() => allEditorWindows.Where(r => r.hasFocus && !r.maximized && r.docked).ForEach(r => EnsureTabVisibleOnScroller(r)); static float GetOptimalTabScrollerPosition(EditorWindow activeTab) { var dockArea = activeTab.GetMemberValue("m_Parent"); var tabAreaWidth = dockArea.GetFieldValue("m_TabAreaRect").width; if (tabAreaWidth == 0) tabAreaWidth = activeTab.position.width - 38; if (tabStyle == null) if (guiStylesInitialized) tabStyle = new GUIStyle("dragtab"); else return 0; var activeTabXMin = 0f; var activeTabXMax = 0f; var tabWidthSum = 0f; var activeTabReached = false; foreach (var tab in GetTabList(activeTab)) { var tabWidth = dockArea.InvokeMethod("GetTabWidth", tabStyle, 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; } static GUIStyle tabStyle; static float nonZeroTabScrollOffset = 3f; [UnityEditor.Callbacks.PostProcessBuild] static void OnBuild(BuildTarget _, string __) { EditorApplication.delayCall += LoadPropertyEditorInspectedObjects; EditorApplication.delayCall += UpdatePropertyEditorTitles; } static void OnDomainReloaded() { toCallInGUI += UpdateGUIWrappingForAllBrowsers; toCallInGUI += UpdateBrowserTitles; } static void OnSceneOpened(Scene _, OpenSceneMode __) { LoadPropertyEditorInspectedObjects(); ClosePropertyEditorsWithNonLoadableObjects(); UpdatePropertyEditorTitles(); } static void OnProjectLoaded() { toCallInGUI += EnsureActiveTabsVisibleOnScroller; UpdatePropertyEditorTitles(); } static void OnFocusedWindowChanged() { if (EditorWindow.focusedWindow?.GetType() == t_ProjectBrowser) UpdateGUIWrappingForBrowser(EditorWindow.focusedWindow); } static void OnWindowUnmaximized() { UpdatePropertyEditorTitles(); UpdateBrowserTitles(); UpdateGUIWrappingForAllBrowsers(); 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() { UpdateBrowserTitles(); UpdateGUIWrappingForAllBrowsers(); UpdateDelayedMousePosition(); ReplaceTabScrollerButtonsWithGradients(); EditorApplication.delayCall -= DelayCallLoop; EditorApplication.delayCall += DelayCallLoop; } static void UpdateDelayedMousePosition() { var lastEvent = typeof(Event).GetFieldValue("s_Current"); delayedMousePosition_screenSpace = EditorGUIUtility.GUIToScreenPoint(lastEvent.mousePosition); } static Vector2 delayedMousePosition_screenSpace; static void ComponentTabHeaderGUI(Editor editor) { if (!(editor.target is 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 PreventGameViewZoomOnShiftScroll() // called from Update { if (!curEvent.holdingShift) return; if (Application.isPlaying) return; // zoom by scrolling is disabled in playmode anyway if (!(EditorWindow.mouseOverWindow is EditorWindow hoveredWindow)) return; if (hoveredWindow.GetType() != t_GameView) return; if (!(hoveredWindow.GetMemberValue("m_ZoomArea", false) is object zoomArea)) return; var isScroll = !curEvent.isMouseMove && curEvent.mouseDelta != Vector2.zero; if (isScroll) { zoomArea.SetMemberValue("m_Scale", lastGameViewScale); zoomArea.SetMemberValue("m_Translation", lastGameViewTranslation); } else { lastGameViewScale = zoomArea.GetMemberValue("m_Scale"); lastGameViewTranslation = zoomArea.GetMemberValue("m_Translation"); } } static Vector2 lastGameViewScale = Vector2.one; static Vector2 lastGameViewTranslation = Vector2.zero; static void TryInitializeGuiStyles() => EditorWindow.focusedWindow?.SendEvent(EditorGUIUtility.CommandEvent("")); static bool guiStylesInitialized => typeof(GUI).GetFieldValue("s_Skin") != null; static object GetDockArea(EditorWindow window) => window.GetFieldValue("m_Parent"); static List GetTabList(EditorWindow window) => GetDockArea(window).GetFieldValue>("m_Panes"); [InitializeOnLoadMethod] static void Init() { if (VTabsMenu.pluginDisabled) return; // dragndrop and scrolling EditorApplication.delayCall += () => EditorApplication.update -= Update; EditorApplication.delayCall += () => EditorApplication.update += Update; EditorApplication.delayCall -= UpdateDelayedMousePosition; EditorApplication.delayCall += UpdateDelayedMousePosition; EditorApplication.update -= PreventGameViewZoomOnShiftScroll; EditorApplication.update += PreventGameViewZoomOnShiftScroll; // shortcuts var globalEventHandler = typeof(EditorApplication).GetFieldValue("globalEventHandler"); typeof(EditorApplication).SetFieldValue("globalEventHandler", (globalEventHandler - CheckShortcuts) + CheckShortcuts); // 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; OnDomainReloaded(); } static IEnumerable allBrowsers => _allBrowsers ??= t_ProjectBrowser.GetFieldValue("s_ProjectBrowsers").Cast(); static IEnumerable _allBrowsers; static IEnumerable allPropertyEditors => Resources.FindObjectsOfTypeAll(t_PropertyEditor).Where(r => r.GetType().BaseType == typeof(EditorWindow)).Cast(); static List allEditorWindows => Resources.FindObjectsOfTypeAll().ToList(); static Type t_EditorWindow = typeof(EditorWindow); static Type t_DockArea = typeof(Editor).Assembly.GetType("UnityEditor.DockArea"); static Type t_PropertyEditor = typeof(Editor).Assembly.GetType("UnityEditor.PropertyEditor"); static Type t_ProjectBrowser = typeof(Editor).Assembly.GetType("UnityEditor.ProjectBrowser"); static Type t_GameView = typeof(Editor).Assembly.GetType("UnityEditor.GameView"); static Type t_SceneHierarchyWindow = typeof(Editor).Assembly.GetType("UnityEditor.SceneHierarchyWindow"); static Type t_HostView = typeof(Editor).Assembly.GetType("UnityEditor.HostView"); static Type t_EditorWindowDelegate = t_HostView.GetNestedType("EditorWindowDelegate", maxBindingFlags); static MethodInfo mi_WrappedBrowserOnGUI = typeof(VTabs).GetMethod(nameof(WrappedBrowserOnGUI), maxBindingFlags); static Type t_VHierarchy = Type.GetType("VHierarchy.VHierarchy") ?? Type.GetType("VHierarchy.VHierarchy, VHierarchy, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); static Type t_VFolders = Type.GetType("VFolders.VFolders") ?? Type.GetType("VFolders.VFolders, VFolders, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); static Type t_VFavorites = Type.GetType("VFavorites.VFavorites") ?? Type.GetType("VFavorites.VFavorites, VFavorites, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null"); static MethodInfo mi_VFolders_GetIcon = t_VFolders?.GetMethod("GetSmallFolderIcon_forVTabs", maxBindingFlags); static MethodInfo mi_VHierarchy_GetIcon = t_VHierarchy?.GetMethod("GetIcon_forVTabs", maxBindingFlags); static MethodInfo mi_VFavorites_BeforeWindowCreated = t_VFavorites?.GetMethod("BeforeWindowCreated_byVTabs", maxBindingFlags); static MethodInfo mi_VFavorites_CanBrowserBeWrapped = t_VFavorites?.GetMethod("CanBrowserBeWrapped_byVTabs", maxBindingFlags); const string version = "2.0.14"; } } #endif