Files
Fishing2/Assets/Plugins/vTabs/VTabs.cs
2025-05-10 12:49:47 +08:00

1518 lines
42 KiB
C#

#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 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();
}
// foreach (var dockArea in allDockAreas)
// // dockArea.GetMemberValue<RectOffset>("m_BorderSize").Log(r => dockArea.name + ": " + r.left);
// dockArea.GetMemberValue<Rect>("m_TabAreaRect").Log(r => dockArea.name + ": " + r.x);
}
public static Dictionary<Object, VTabsGUI> guis_byDockArea = new();
public static void UpdateStyleSheet()
{
if (!Application.unityVersion.StartsWith("6000")) return;
void updatePluginFolderPath()
{
if (AssetDatabase.LoadAssetAtPath<Object>(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<StyleSheet>(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;
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<bool>("isLocked");
var isTitleDefault = browser.titleContent.text == "Project";
void setLockedTitle()
{
if (!isLocked) return;
var isOneColumn = browser.GetMemberValue<int>("m_ViewMode") == 0;
var path = isOneColumn ? browser.GetLockedFolderPath_oneColumn() : browser.InvokeMethod<string>("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<IDictionary>("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<IDictionary>("s_GUIContents").Clear();
}
setLockedTitle();
setDefaultTitle();
}
static void UpdatePropertyEditorTitle(EditorWindow propertyEditor)
{
var obj = propertyEditor.GetMemberValue<Object>("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<IDictionary>("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<bool>("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<int, Texture2D> adjustedObjectIconsBySourceIid = new Dictionary<int, Texture2D>();
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<int>("m_ViewMode") == 0;
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;
var m_rootInstanceID = data.GetMemberValue<int>("m_rootInstanceID");
void setInitial()
{
if (m_rootInstanceID != 0) return;
var folderPath = browser.GetLockedFolderPath_oneColumn();
var folderIid = AssetDatabase.LoadAssetAtPath<Object>(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.SetLockedFolderPath_oneColumn(folderPath);
browser.GetMemberValue("m_SearchFilter")?.SetMemberValue("m_Folders", new[] { folderPath }); // needed for breadcrumbs to display correctly
}
void reset()
{
if (browser.GetMemberValue<bool>("isLocked")) return;
data.SetMemberValue("m_rootInstanceID", 0);
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<string>("GetActiveFolderPath").ToGuid();
EditorApplication.delayCall += () =>
{
var delayedFolderGuid = browser.InvokeMethod<string>("GetActiveFolderPath").ToGuid();
if (delayedFolderGuid == curFolderGuid) return;
var folderIid = AssetDatabase.LoadAssetAtPath<Object>(AssetDatabase.GUIDToAssetPath(delayedFolderGuid)).GetInstanceID();
browser.InvokeMethod("SetFolderSelection", new[] { folderIid }, false);
UpdateBrowserTitle(browser);
};
}
onBreadcrumbsClick();
onDoubleclick();
onUndoRedo();
}
void oneColumn()
{
if (!isOneColumn) return;
if (!browser.InvokeMethod<bool>("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<bool>("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 UpdateWrappingForBrowser(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<bool>("isLocked");
var isWrapped = browser.GetMemberValue("m_Parent").GetMemberValue<Delegate>("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 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<Dictionary<string, GUIStyle>>("m_Styles")?.ContainsKey("dragtab scroller prev") != true) return;
if (typeof(GUISkin).GetFieldValue("current")?.GetFieldValue<Dictionary<string, GUIStyle>>("m_Styles")?.ContainsKey("dragtab scroller next") != true) return;
var t_Styles = typeof(Editor).Assembly.GetType("UnityEditor.DockArea+Styles");
leftScrollerStyle = t_Styles.GetFieldValue<GUIStyle>("tabScrollerPrevButton");
rightScrollerStyle = t_Styles.GetFieldValue<GUIStyle>("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 ClosePropertyEditorsWithNonLoadableObjects()
{
foreach (var propertyEditor in allPropertyEditors)
if (propertyEditor.GetMemberValue<Object>("m_InspectedObject") == null)
propertyEditor.Close();
}
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();
ClosePropertyEditorsWithNonLoadableObjects();
UpdateAllPropertyEditorTitles();
}
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<System.Array>("s_windowActions") ?? t_HostView.InvokeMethod<System.Array>("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<Object>("m_Parent");
if (parent?.GetType() == t_DockArea)
return parent;
else
return null;
}
public static List<EditorWindow> GetTabs(this Object dockArea) => dockArea?.GetFieldValue<List<EditorWindow>>("m_Panes");
public static string GetLockedFolderPath_oneColumn(this EditorWindow browser)
{
var path = browser.GetMemberValue<string[]>("m_LastFolders")?.FirstOrDefault();
if (path == null || path == "Assets")
path = browser.GetMemberValue("m_SearchFilter")?.GetMemberValue<string[]>("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<bool>("isLocked");
savedGridSize = window.GetFieldValue<int>("m_StartGridSize");
isGridSizeSaved = true;
savedLayout = window.GetMemberValue<int>("m_ViewMode");
isLayoutSaved = true;
var folderPath = savedLayout == 0 ? window.GetLockedFolderPath_oneColumn() // one column
: window.InvokeMethod<string>("GetActiveFolderPath"); // two columns
folderGuid = folderPath.ToGuid();
}
if (isPropertyEditor)
globalId = new GlobalID(window.GetMemberValue<string>("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<TabInfo> list = new List<TabInfo>(); }
[InitializeOnLoadMethod]
static void Init()
{
if (VTabsMenu.pluginDisabled) return;
EditorApplication.update -= UpdateGUIs;
EditorApplication.update += UpdateGUIs;
// shortcuts
var globalEventHandler = typeof(EditorApplication).GetFieldValue<EditorApplication.CallbackFunction>("globalEventHandler");
typeof(EditorApplication).SetFieldValue("globalEventHandler", (globalEventHandler - Shortcuts) + Shortcuts);
// component tabs
Editor.finishedDefaultHeaderGUI -= ComponentTabHeaderGUI;
Editor.finishedDefaultHeaderGUI += ComponentTabHeaderGUI;
// state change detectors
var projectWasLoaded = typeof(EditorApplication).GetFieldValue<UnityEngine.Events.UnityAction>("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;
// EditorApplication.delayCall += () => VTabsAddTabWindow.UpdateAllEntries();
EditorApplication.delayCall += () => UpdateStyleSheet();
SetupExtraScrollerSpace();
OnDomainReloaded();
}
public static IEnumerable<EditorWindow> allBrowsers => _allBrowsers ??= t_ProjectBrowser.GetFieldValue<IList>("s_ProjectBrowsers").Cast<EditorWindow>();
public static IEnumerable<EditorWindow> _allBrowsers;
public static IEnumerable<EditorWindow> allPropertyEditors => Resources.FindObjectsOfTypeAll(t_PropertyEditor).Where(r => r.GetType().BaseType == typeof(EditorWindow)).Cast<EditorWindow>();
public static List<EditorWindow> 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<EditorWindow>().ToList();
return _allEditorWindows ?? typeof(EditorWindow).GetMemberValue<List<EditorWindow>>("activeEditorWindows");
}
}
public static List<EditorWindow> _allEditorWindows;
public static IEnumerable<Object> allDockAreas => allEditorWindows.Where(r => r.hasFocus && r.docked && !r.maximized).Select(r => r.GetMemberValue<Object>("m_Parent"));
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_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.1";
}
}
#endif