using System; using System.Collections.Generic; using System.Linq; using UnityEditor; using UnityEditorInternal; using UnityEngine; using UnityObject = UnityEngine.Object; namespace FullscreenEditor { /// Clone of the internal UnityEditor.ShowMode. public enum ShowMode { /// Show as a normal window with max, min & close buttons. NormalWindow = 0, /// Used for a popup menu. On mac this means light shadow and no titlebar. PopupMenu = 1, /// Utility window - floats above the app. Disappears when app loses focus. Utility = 2, /// Window has no shadow or decorations. Used internally for dragging stuff around. NoShadow = 3, /// The Unity main window. On mac, this is the same as NormalWindow, except window doesn't have a close button. MainWindow = 4, /// Aux windows. The ones that close the moment you move the mouse out of them. AuxWindow = 5, /// Like PopupMenu, but without keyboard focus. Tooltip = 6, // Show as fullscreen window Fullscreen = 8 } /// Helper class for suppressing unity logs when calling a method that may show unwanted logs. internal class SuppressLog : IDisposable { private readonly bool lastState; internal static ILogger Logger { get { #if UNITY_2017_1_OR_NEWER return Debug.unityLogger; #else return Debug.logger; #endif } } public SuppressLog() { lastState = Logger.logEnabled; Logger.logEnabled = false; } public void Dispose() { Logger.logEnabled = lastState; } } /// Miscellaneous utilities for Fullscreen Editor. [InitializeOnLoad] public static class FullscreenUtility { /// Contains a Texture2D icon loaded from a base64. public class Icon { private string m_base64; private Texture2D m_texture; public Texture2D Texture { get { return m_texture ? m_texture : (m_texture = FindOrLoadTexture(m_base64)); } } public Icon(string base64) { m_base64 = base64; } public Icon(string base64, string proVariantBase64) { m_base64 = EditorGUIUtility.isProSkin ? proVariantBase64 : base64; } public static implicit operator Texture2D(Icon icon) { return icon.Texture; } } private static Vector2 mousePosition; public static bool IsWindows { get { return Application.platform == RuntimePlatform.WindowsEditor; } } public static bool IsMacOS { get { return Application.platform == RuntimePlatform.OSXEditor; } } public static bool IsLinux { get { return Application.platform == RuntimePlatform.LinuxEditor; } } static FullscreenUtility() { var lastUpdate = EditorApplication.timeSinceStartup; EditorApplication.update += () => { if(EditorApplication.timeSinceStartup - lastUpdate > 0.5f && FullscreenPreferences.RectSource.Value == RectSourceMode.AtMousePosition) { EditorApplication.RepaintHierarchyWindow(); EditorApplication.RepaintProjectWindow(); lastUpdate = EditorApplication.timeSinceStartup; } }; EditorApplication.hierarchyWindowItemOnGUI += (rect, id) => RecalculateMousePosition(); EditorApplication.projectWindowItemOnGUI += (rect, id) => RecalculateMousePosition(); #if UNITY_2019_1_OR_NEWER SceneView.duringSceneGui += sceneView => RecalculateMousePosition(); #else SceneView.onSceneGUIDelegate += sceneView => RecalculateMousePosition(); #endif } private static void RecalculateMousePosition() { mousePosition = GUIUtility.GUIToScreenPoint(Event.current.mousePosition); } /// Returns wheter the extension is running with debugging enabled. public static bool DebugModeEnabled { get { #if FULLSCREEN_DEBUG return true; #else return false; #endif } } /// The mouse position, can be called outside an OnGUI method. public static Vector2 MousePosition { get { return mousePosition; } } /// The icon of this plugin. public static readonly Icon FullscreenIcon = new Icon( "iVBORw0KGgoAAAANSUhEUgAAAC4AAAAuCAMAAABgZ9sFAAAApVBMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+4/eNVAAAANnRSTlMA/AfHA2sLtBTy8MxYO/nk9u3Qj06wNCceGeCppF9LPhDot5iBewHYn1JDQS8sIyCJdkdluZF8FV+iAAAB6klEQVRIx+2SWbKbMBBFJcAYC8xswAMznme/pPe/tNASLgImVH5TeefLvhxaouuS/xTpHrwi0nJ/BZE04O2IeomrcqHnTL+RN4HLLH16qOJr/53YYiYI9pcmS/MmMdk+6OoTaLHOPErsNpqlPZ2KnIJpMzerk0x3bAq00efDujw5BuFNws+OwsCY7Md169SJfTaug/nVhsoWYFRHtkqTqZ4I6LA+e1j8qScJe8NHuB4M6+brtgJkreJmZPHty/hjkWLJpk9CmU/UJaLxk8y1RgwAcELSxbfA9gnR+A3WClGnAMAqlaC+OpE+FxcSvPGWwkbBHwXkxx1BXQ7JJ8upz1s5qS+ARAdRBvw/iDSYquSfRflDbCz4k6wyhGEUGl+Y7g9t5QlT1O4LyHlBjgAF7ltzaSx9LH1DAaenOpY+qGfPsF5pretgP3rL3G0AUDcsQOrjRdVZjDpA2bFFQ35ITxMdmmD0Jbo+yVDXezdHXcZmt31KHEA8Fx996G/283c4F+2nY7q8bNOry6MRfd3ZmVaM67OHMQ+jDHe1vF1T4ycd1SlQ07F8jLycmZQOTsdFthw0jKIFIMPTD8yGhlJpRpTvxHaKfsHux225kFcOq9rMc5yVvPGeSUR2Q6VcXk7z3888n68a+eav+AWMDWJNSUXp9QAAAABJRU5ErkJggg==" ); /// A smaller icon of this plugin. public static readonly Icon FullscreenIconSmall = new Icon( "iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAAclBMVEUAAABmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmZmbn+Ue6AAAAJXRSTlMABfscDPdX3LmNgF9IKxPtoXdvQxiumWlTJPLjkYcx6M3KxrM+6Owz4AAAAKJJREFUCNdlzlkOwyAMBNDBkECALDRkb9LV979infazliz5eX4GS/Cd6wFE1/mQcWHm4qmhH4Vc5WmaGwXVBPq5pTYCmFdqxRNfPFOJwJx2sX55pK3CaCdUQwZMhtJKFtAG/5PVNzeS1wo47rWqbG+cLUHJoOLD8SY/pnijN67csl0A5bkoqBfzuki/xnTM4vHs3xlEe/aPqKd9GGoAwY1pbj58lQvfBIytyAAAAABJRU5ErkJggg==", "iVBORw0KGgoAAAANSUhEUgAAAA8AAAAPCAMAAAAMCGV4AAAAclBMVEUAAAC0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLS0tLSyGfY5AAAAJXRSTlMABfsM91ccGNy5jYBfSCsT7aF3b0OumWlTJPLjkYcx6M3KxrM+ZTSY8wAAAKJJREFUCNdljtsSgyAMRJegAgIqeNdqr/n/X2ycPjYzO5Oz+3KwBp/cACC65INBz8zFy6B6FvKVF9PSKqg20I876iKAZaNOeObeM5UIzPkQrt4eea8x2Rn1aABtoColASqN/zOySbTsjQLOR6NqO2hnS1DWqPl0vEvHFO/0wY07tiugPBcFDcK8reLX6sQsPF3+SSPayz+imY9xbAAEN+Wl/QKAZAvuNVnEigAAAABJRU5ErkJggg==" ); /// The icon to show on the game view toolbar when fullscreen on play is enabled. public static readonly Icon FullscreenOnPlayIcon = new Icon( "iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAAAZlBMVEUAAAAZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRnaqT0eAAAAIXRSTlMA9rSVTunBWTwgFNzOrvvYo4x1ZGA4MALTx6eae3dTCgkQ40nLAAAAcklEQVQY053OORaFIBBE0WpsBQSchz+qvf9NSmCHJlZ4g3cKj8ddbD3q6pWmSwqRnjH/SIxKQ1WN4yOkMpSuDKuhLX41ZOEt3IJxxe0mLA7Ww0LLb87NkGvDJYYk7fkPNYWK0H8G9yIqY2rzHx9ix3i6E/A3BM5M0CXHAAAAAElFTkSuQmCC", "iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAAAY1BMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+aRQ2gAAAAIHRSTlMA9rSV6cFZPCAU3M6uT/vYo4x1ZGBMODAJ08enmnt3U4mFYqQAAABvSURBVBjTnc43AoAwDATBkwHbOJBz1P9fiQvUUqByitXh98XG1xZlsYbhlYy5HTGdxEqkoqLEsjOJdLnJHRTd/pCQhtUwM/qPZwNmA22hIeUtpqZLte4VRRyWtIeqTITpmjC2zCJ9qNMe63wT8fce2Z8EsL04YKYAAAAASUVORK5CYII=" ); /// Show the notification of a newly opened about using the shortcut to close. /// The fullscreen window. /// The path containing a shortcut. internal static void ShowFullscreenExitNotification(EditorWindow window, string menuItemPath) { if(FullscreenPreferences.DisableNotifications) return; var notification = string.Format("Press {0} to exit fullscreen", TextifyMenuItemShortcut(menuItemPath)); ShowFullscreenNotification(window, notification); } /// Show a fullscreen notification in an . /// The host of the notification. /// The message to show. public static void ShowFullscreenNotification(EditorWindow window, string message) { if(!window) return; window.ShowNotification(new GUIContent(message, FullscreenIcon)); window.Repaint(); if(EditorWindow.mouseOverWindow) // This definitely made sense when I made it, so I won't remove EditorWindow.mouseOverWindow.Repaint(); } /// Does the given path contains a key binding? public static bool MenuItemHasShortcut(string menuItemPath) { var index = menuItemPath.LastIndexOf(" "); if(index++ == -1) return false; var shortcut = menuItemPath.Substring(index).Replace("_", ""); var evt = Event.KeyboardEvent(shortcut); shortcut = InternalEditorUtility.TextifyEvent(evt); return !shortcut.Equals("None", StringComparison.InvariantCultureIgnoreCase); } /// Gets a human-readable shortcut. /// The path containing a shortcut. /// public static string TextifyMenuItemShortcut(string menuItemPath) { var index = menuItemPath.LastIndexOf(" "); if(index++ == -1) return "None"; var shortcut = menuItemPath.Substring(index).Replace("_", ""); var evt = Event.KeyboardEvent(shortcut); shortcut = InternalEditorUtility.TextifyEvent(evt); return shortcut; } private static Texture2D FindOrLoadTexture(string base64) { var found = GetRef(base64); return found ? found : LoadTexture(base64); } private static Texture2D LoadTexture(string base64) { try { var texture = new Texture2D(1, 1, TextureFormat.ARGB32, false, true); var bytes = Convert.FromBase64String(base64); texture.name = base64; texture.hideFlags = HideFlags.HideAndDontSave; texture.LoadImage(bytes); return texture; } catch(Exception e) { Logger.Error("Failed to load texture: {0}", e); return null; } } /// Find an object by it's name and type. /// The name of the object to search for. /// The type of the object to search for. public static T GetRef(string name) where T : UnityObject { return Resources.FindObjectsOfTypeAll().FirstOrDefault(obj => obj.name == name); } /// Find an object by it's name and type. /// The name of the object to search for. /// The type of the object to search for. public static UnityObject GetRef(Type type, string name) { return Resources.FindObjectsOfTypeAll(type).FirstOrDefault(obj => obj.name == name); } /// Get the main view. public static ScriptableObject GetMainView() { var containers = Resources.FindObjectsOfTypeAll(Types.MainView); if(containers.Length > 0) return containers[0] as ScriptableObject; throw new Exception("Couldn't find main view"); } /// Get the main game view. public static EditorWindow GetMainGameView() { if(Types.GameView.HasMethod("GetMainGameView")) { // Removed in 2019.3 alpha return Types.GameView.InvokeMethod("GetMainGameView"); } else if(Types.PreviewEditorWindow.HasMethod("GetMainPreviewWindow")) { // Removed in 2019.3 beta return Types.PreviewEditorWindow.InvokeMethod("GetMainPreviewWindow"); } else { // if (Types.PlayModeView.HasMethod("GetMainPlayModeView")) return Types.PlayModeView.InvokeMethod("GetMainPlayModeView"); } } /// Get all the game views. This returns even the docked game views which are not visible. public static EditorWindow[] GetGameViews() { return Resources .FindObjectsOfTypeAll(Types.GameView) .Cast() .ToArray(); } /// Returns the focused view if it is a dock area with more than one tab, otherwise, returns the focused window. public static ScriptableObject GetFocusedViewOrWindow() { var mostSpecificView = Types.GUIView.GetPropertyValue("focusedView"); if(!mostSpecificView) return null; // The most specific obj is a window, open it instead of the view if(mostSpecificView.IsOfType(Types.HostView, false)) return EditorWindow.focusedWindow; var viewHierarchy = GetViewHierarchy(mostSpecificView); var leastSpecificView = viewHierarchy.LastOrDefault(); // The view hierarchy has the same length of all the views in this ContainerWindow // This means there are no cousins views handled by a split group on the surroundings of this one // So, we're alone on the container var alone = leastSpecificView.GetPropertyValue("allChildren").Length == viewHierarchy.Length; if(alone && EditorWindow.focusedWindow.InvokeMethod("GetNumTabs") > 1) alone = false; // But, we may not be the only tab on the host view // If the focused view is in the main view, or we are the only child on this view, then we open the window return alone || leastSpecificView.IsOfType(Types.MainView) ? EditorWindow.focusedWindow : leastSpecificView; } /// Returns all the parents of a given view. public static ScriptableObject[] GetViewHierarchy(ScriptableObject view) { if(!view) return new ScriptableObject[0]; view.EnsureOfType(Types.View); var list = new List() { view }; var parent = view.GetPropertyValue("parent"); while(parent) { // Get the least specific view view = parent; list.Add(view); parent = view.GetPropertyValue("parent"); } return list.ToArray(); } /// Get all the children view of a given view. public static ScriptableObject[] GetAllViewChildren(ScriptableObject view) { if(!view) return new ScriptableObject[0]; view.EnsureOfType(Types.View); return view.GetPropertyValue("allChildren") .Cast() .ToArray(); } /// Returns wheter a given view is focused or not. public static bool IsViewFocused(ScriptableObject view) { if(!view) return false; view.EnsureOfType(Types.View); var focused = Types.GUIView.GetPropertyValue("focusedView"); var children = GetAllViewChildren(view); return children.Contains(focused); } /// Focus a view. public static void FocusView(ScriptableObject guiView) { if(!guiView) return; // guiView.EnsureOfType(Types.GUIView); if(guiView.IsOfType(Types.GUIView)) guiView.InvokeMethod("Focus"); else { var vp = new ViewPyramid(guiView); var vc = vp.Container; var methodName = "Internal_BringLiveAfterCreation"; if(vc) { if(vc.HasMethod(methodName, new Type[] { typeof(bool), typeof(bool), typeof(bool) })) // displayImmediately, setFocus, showMaximized vc.InvokeMethod(methodName, false, true, false); else // displayImmediately, setFocus vc.InvokeMethod(methodName, false, true); } } } /// Returns the display scaling of the editor, e.g. 1.5 if 125% public static float GetDisplayScaling() { return EditorGUIUtility.pixelsPerPoint; } /// Returns a screen rect corrected to fit the editor scaling public static Rect DpiCorrectedArea(Rect area) { var scaling = GetDisplayScaling(); area.width /= scaling; area.height /= scaling; return area; } /// Get the default height of the editor toolbars. public static float GetToolbarHeight() { try { if(typeof(EditorGUI).HasField("kWindowToolbarHeight")) { var result = typeof(EditorGUI).GetFieldValue("kWindowToolbarHeight"); if(result is int) return (int)result; else return result.GetPropertyValue("value"); } else return 17f; // Default on < 2019.3 versions } catch(Exception e) { if(FullscreenUtility.DebugModeEnabled) Debug.LogException(e); return 17f; } } /// Set the default height of the editor toolbars. public static bool SetToolbarHeight(float value) { try { // On Unity blow 2019.3 this is a const field and cannot be changed if(typeof(EditorGUI).HasField("kWindowToolbarHeight")) { var result = typeof(EditorGUI).GetFieldValue("kWindowToolbarHeight"); result.SetFieldValue("m_Value", value); return true; } else { return false; } } catch(Exception e) { if(FullscreenUtility.DebugModeEnabled) Debug.LogException(e); return false; } } /// Set Game View target display. public static void SetGameViewDisplayTarget(EditorWindow gameView, int display) { gameView.EnsureOfType(Types.GameView); if(gameView.HasProperty("targetDisplay")) { gameView.SetPropertyValue("targetDisplay", display); } else if(gameView.HasField("m_TargetDisplay")) { gameView.SetFieldValue("m_TargetDisplay", display); } else { Logger.Error("Could not set Game View target display"); } } } }