You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
432 lines
20 KiB
432 lines
20 KiB
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEditorInternal;
|
|
using UnityEngine;
|
|
using UnityObject = UnityEngine.Object;
|
|
|
|
namespace FullscreenEditor {
|
|
|
|
/// <summary>Clone of the internal UnityEditor.ShowMode.</summary>
|
|
public enum ShowMode {
|
|
/// <summary>Show as a normal window with max, min & close buttons.</summary>
|
|
NormalWindow = 0,
|
|
/// <summary>Used for a popup menu. On mac this means light shadow and no titlebar.</summary>
|
|
PopupMenu = 1,
|
|
/// <summary>Utility window - floats above the app. Disappears when app loses focus.</summary>
|
|
Utility = 2,
|
|
/// <summary>Window has no shadow or decorations. Used internally for dragging stuff around.</summary>
|
|
NoShadow = 3,
|
|
/// <summary>The Unity main window. On mac, this is the same as NormalWindow, except window doesn't have a close button.</summary>
|
|
MainWindow = 4,
|
|
/// <summary>Aux windows. The ones that close the moment you move the mouse out of them.</summary>
|
|
AuxWindow = 5,
|
|
/// <summary>Like PopupMenu, but without keyboard focus.</summary>
|
|
Tooltip = 6,
|
|
// Show as fullscreen window
|
|
Fullscreen = 8
|
|
}
|
|
|
|
/// <summary>Helper class for suppressing unity logs when calling a method that may show unwanted logs.</summary>
|
|
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;
|
|
}
|
|
|
|
}
|
|
|
|
/// <summary>Miscellaneous utilities for Fullscreen Editor.</summary>
|
|
[InitializeOnLoad]
|
|
public static class FullscreenUtility {
|
|
|
|
/// <summary>Contains a Texture2D icon loaded from a base64.</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Returns wheter the extension is running with debugging enabled.</summary>
|
|
public static bool DebugModeEnabled {
|
|
get {
|
|
#if FULLSCREEN_DEBUG
|
|
return true;
|
|
#else
|
|
return false;
|
|
#endif
|
|
}
|
|
}
|
|
|
|
/// <summary>The mouse position, can be called outside an OnGUI method.</summary>
|
|
public static Vector2 MousePosition { get { return mousePosition; } }
|
|
|
|
/// <summary>The icon of this plugin.</summary>
|
|
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=="
|
|
);
|
|
|
|
/// <summary>A smaller icon of this plugin.</summary>
|
|
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=="
|
|
);
|
|
|
|
/// <summary>The icon to show on the game view toolbar when fullscreen on play is enabled.</summary>
|
|
public static readonly Icon FullscreenOnPlayIcon = new Icon(
|
|
"iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAAAZlBMVEUAAAAZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRkZGRnaqT0eAAAAIXRSTlMA9rSVTunBWTwgFNzOrvvYo4x1ZGA4MALTx6eae3dTCgkQ40nLAAAAcklEQVQY053OORaFIBBE0WpsBQSchz+qvf9NSmCHJlZ4g3cKj8ddbD3q6pWmSwqRnjH/SIxKQ1WN4yOkMpSuDKuhLX41ZOEt3IJxxe0mLA7Ww0LLb87NkGvDJYYk7fkPNYWK0H8G9yIqY2rzHx9ix3i6E/A3BM5M0CXHAAAAAElFTkSuQmCC",
|
|
"iVBORw0KGgoAAAANSUhEUgAAABEAAAARCAMAAAAMs7fIAAAAY1BMVEUAAAD///////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////+aRQ2gAAAAIHRSTlMA9rSV6cFZPCAU3M6uT/vYo4x1ZGBMODAJ08enmnt3U4mFYqQAAABvSURBVBjTnc43AoAwDATBkwHbOJBz1P9fiQvUUqByitXh98XG1xZlsYbhlYy5HTGdxEqkoqLEsjOJdLnJHRTd/pCQhtUwM/qPZwNmA22hIeUtpqZLte4VRRyWtIeqTITpmjC2zCJ9qNMe63wT8fce2Z8EsL04YKYAAAAASUVORK5CYII="
|
|
);
|
|
|
|
/// <summary>Show the notification of a newly opened <see cref="FullscreenView"/> about using the shortcut to close.</summary>
|
|
/// <param name="window">The fullscreen window.</param>
|
|
/// <param name="menuItemPath">The <see cref="MenuItem"/> path containing a shortcut.</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Show a fullscreen notification in an <see cref="EditorWindow"/>.</summary>
|
|
/// <param name="window">The host of the notification.</param>
|
|
/// <param name="message">The message to show.</param>
|
|
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();
|
|
}
|
|
|
|
/// <summary>Does the given <see cref="MenuItem"/> path contains a key binding?</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Gets a human-readable shortcut.</summary>
|
|
/// <param name="menuItemPath">The <see cref="MenuItem"/> path containing a shortcut.</param>
|
|
/// <returns></returns>
|
|
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<Texture2D>(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;
|
|
}
|
|
}
|
|
|
|
/// <summary>Find an object by it's name and type.</summary>
|
|
/// <param name="name">The name of the object to search for.</param>
|
|
/// <typeparam name="T">The type of the object to search for.</typeparam>
|
|
public static T GetRef<T>(string name) where T : UnityObject {
|
|
return Resources.FindObjectsOfTypeAll<T>().FirstOrDefault(obj => obj.name == name);
|
|
}
|
|
|
|
/// <summary>Find an object by it's name and type.</summary>
|
|
/// <param name="name">The name of the object to search for.</param>
|
|
/// <param name="type">The type of the object to search for.</param>
|
|
public static UnityObject GetRef(Type type, string name) {
|
|
return Resources.FindObjectsOfTypeAll(type).FirstOrDefault(obj => obj.name == name);
|
|
}
|
|
|
|
/// <summary>Get the main view.</summary>
|
|
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");
|
|
}
|
|
|
|
/// <summary>Get the main game view.</summary>
|
|
public static EditorWindow GetMainGameView() {
|
|
if(Types.GameView.HasMethod("GetMainGameView")) { // Removed in 2019.3 alpha
|
|
return Types.GameView.InvokeMethod<EditorWindow>("GetMainGameView");
|
|
} else if(Types.PreviewEditorWindow.HasMethod("GetMainPreviewWindow")) { // Removed in 2019.3 beta
|
|
return Types.PreviewEditorWindow.InvokeMethod<EditorWindow>("GetMainPreviewWindow");
|
|
} else { // if (Types.PlayModeView.HasMethod("GetMainPlayModeView"))
|
|
return Types.PlayModeView.InvokeMethod<EditorWindow>("GetMainPlayModeView");
|
|
}
|
|
}
|
|
|
|
/// <summary>Get all the game views. This returns even the docked game views which are not visible.</summary>
|
|
public static EditorWindow[] GetGameViews() {
|
|
return Resources
|
|
.FindObjectsOfTypeAll(Types.GameView)
|
|
.Cast<EditorWindow>()
|
|
.ToArray();
|
|
}
|
|
|
|
/// <summary>Returns the focused view if it is a dock area with more than one tab, otherwise, returns the focused window.</summary>
|
|
public static ScriptableObject GetFocusedViewOrWindow() {
|
|
var mostSpecificView = Types.GUIView.GetPropertyValue<ScriptableObject>("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<Array>("allChildren").Length == viewHierarchy.Length;
|
|
|
|
if(alone && EditorWindow.focusedWindow.InvokeMethod<int>("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;
|
|
}
|
|
|
|
/// <summary>Returns all the parents of a given view.</summary>
|
|
public static ScriptableObject[] GetViewHierarchy(ScriptableObject view) {
|
|
if(!view)
|
|
return new ScriptableObject[0];
|
|
|
|
view.EnsureOfType(Types.View);
|
|
|
|
var list = new List<ScriptableObject>() { view };
|
|
var parent = view.GetPropertyValue<ScriptableObject>("parent");
|
|
|
|
while(parent) { // Get the least specific view
|
|
view = parent;
|
|
list.Add(view);
|
|
parent = view.GetPropertyValue<ScriptableObject>("parent");
|
|
}
|
|
|
|
return list.ToArray();
|
|
}
|
|
|
|
/// <summary>Get all the children view of a given view.</summary>
|
|
public static ScriptableObject[] GetAllViewChildren(ScriptableObject view) {
|
|
if(!view)
|
|
return new ScriptableObject[0];
|
|
|
|
view.EnsureOfType(Types.View);
|
|
|
|
return view.GetPropertyValue<Array>("allChildren")
|
|
.Cast<ScriptableObject>()
|
|
.ToArray();
|
|
}
|
|
|
|
/// <summary>Returns wheter a given view is focused or not.</summary>
|
|
public static bool IsViewFocused(ScriptableObject view) {
|
|
if(!view)
|
|
return false;
|
|
|
|
view.EnsureOfType(Types.View);
|
|
|
|
var focused = Types.GUIView.GetPropertyValue<ScriptableObject>("focusedView");
|
|
var children = GetAllViewChildren(view);
|
|
|
|
return children.Contains(focused);
|
|
}
|
|
|
|
/// <summary>Focus a view.</summary>
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary> Returns the display scaling of the editor, e.g. 1.5 if 125%</summary>
|
|
public static float GetDisplayScaling() {
|
|
return EditorGUIUtility.pixelsPerPoint;
|
|
}
|
|
|
|
/// <summary> Returns a screen rect corrected to fit the editor scaling</summary>
|
|
public static Rect DpiCorrectedArea(Rect area) {
|
|
var scaling = GetDisplayScaling();
|
|
area.width /= scaling;
|
|
area.height /= scaling;
|
|
return area;
|
|
}
|
|
|
|
/// <summary>Get the default height of the editor toolbars.</summary>
|
|
public static float GetToolbarHeight() {
|
|
try {
|
|
if(typeof(EditorGUI).HasField("kWindowToolbarHeight")) {
|
|
var result = typeof(EditorGUI).GetFieldValue<object>("kWindowToolbarHeight");
|
|
if(result is int)
|
|
return (int)result;
|
|
else
|
|
return result.GetPropertyValue<float>("value");
|
|
} else
|
|
return 17f; // Default on < 2019.3 versions
|
|
} catch(Exception e) {
|
|
if(FullscreenUtility.DebugModeEnabled)
|
|
Debug.LogException(e);
|
|
return 17f;
|
|
}
|
|
}
|
|
|
|
/// <summary>Set the default height of the editor toolbars.</summary>
|
|
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<object>("kWindowToolbarHeight");
|
|
result.SetFieldValue("m_Value", value);
|
|
return true;
|
|
} else {
|
|
return false;
|
|
}
|
|
} catch(Exception e) {
|
|
if(FullscreenUtility.DebugModeEnabled)
|
|
Debug.LogException(e);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
/// <summary>Set Game View target display.</summary>
|
|
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");
|
|
}
|
|
}
|
|
|
|
}
|
|
}
|
|
|