Last active
July 6, 2023 15:02
-
-
Save SolarianZ/26a6bb7a8de62e5d5adf41cc9e34de69 to your computer and use it in GitHub Desktop.
{"category": "Unity Engine/Editor/Extensions", "keywords": "Unity, Editor, Toolbar, Custom, UI"} Add custom UI to the Unity Editor's main toolbar. This example adds a FloatField to the main toolbar to control the `Time.timeScale` . You can refer to this example to add other UI elements. It has been tested in Unity 2021.3.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System; | |
using System.Reflection; | |
using UnityEditor; | |
using UnityEngine; | |
using UnityEngine.UIElements; | |
[InitializeOnLoad] | |
public static class CustomEditorToolbar | |
{ | |
#region TimeScale Slider | |
private static FloatField _timeScaleField; | |
private static double _timeScaleLabelClickTime; | |
private static void InitializeTimeScaleSlider() | |
{ | |
// Memo Unity Bug: https://issuetracker.unity3d.com/product/unity/issues/guid/UUM-40353 | |
// The text input field may not display correctly. | |
_timeScaleField = new FloatField("Time Scale") | |
{ | |
value = Time.timeScale, | |
isDelayed = true, | |
style = | |
{ | |
width = 110, | |
}, | |
}; | |
_timeScaleField.labelElement.style.minWidth = 0; | |
_timeScaleField.labelElement.style.flexShrink = 1; | |
_timeScaleField.labelElement.tooltip = "Double-click to reset time scale to 1.\n" + | |
"Right-click to display shortcut options."; | |
_timeScaleField.labelElement.RegisterCallback<ClickEvent>(OnClickTimeScaleLabel); | |
_timeScaleField.labelElement.RegisterCallback<ContextClickEvent>(OnContextClickTimeScaleLabel); | |
// Workaround for Unity Bug UUM-40353 | |
_timeScaleField.Q("unity-text-input").style.overflow = Overflow.Visible; | |
_timeScaleField.RegisterValueChangedCallback(evt => | |
{ | |
var timeScale = evt.newValue; | |
if (timeScale < 0) timeScale = 0; | |
var timeMgrAsset = AssetDatabase.LoadAllAssetsAtPath("ProjectSettings/TimeManager.asset")[0]; | |
Undo.RecordObject(timeMgrAsset, "Change Time Scale"); | |
Time.timeScale = timeScale; | |
}); | |
_customToolbarRight.Add(_timeScaleField); | |
} | |
private static void UpdateTimeScaleField() | |
{ | |
if (!Mathf.Approximately(Time.timeScale, _timeScaleField.value)) | |
{ | |
_timeScaleField.SetValueWithoutNotify(Time.timeScale); | |
} | |
} | |
private static void OnClickTimeScaleLabel(ClickEvent evt) | |
{ | |
evt.StopPropagation(); | |
// Reset TimeScale to 1 on double click | |
var time = EditorApplication.timeSinceStartup; | |
if (time - _timeScaleLabelClickTime < 0.3f) | |
{ | |
_timeScaleLabelClickTime = 0; | |
_timeScaleField.value = 1; | |
} | |
else | |
{ | |
_timeScaleLabelClickTime = time; | |
} | |
} | |
private static void OnContextClickTimeScaleLabel(ContextClickEvent evt) | |
{ | |
evt.StopPropagation(); | |
float[] timeScaleOptions = { 0.1f, 0.2f, 0.3f, 0.4f, 0.5f, 1.5f, 2f, }; | |
TimeScaleSelectWindow.Popup(GUIUtility.GUIToScreenPoint(evt.mousePosition), | |
timeScaleOptions, newTimeScale => _timeScaleField.value = newTimeScale); | |
} | |
class TimeScaleSelectWindow : EditorWindow | |
{ | |
public static void Popup(Vector2 screenPosition, float[] options, Action<float> onSubmit) | |
{ | |
var window = CreateInstance<TimeScaleSelectWindow>(); | |
window.position = new Rect(screenPosition.x, screenPosition.y, 80, GetWindowHeight(options.Length)); | |
window._onSubmit = onSubmit; | |
window.ShowPopup(); | |
window.SetOptions(options); | |
} | |
static float GetWindowHeight(int optionCount) | |
{ | |
return optionCount * (_BUTTON_HEIGHT + _BUTTON_MARGIN_V * 2) + _WINDOW_PADDING_V * 2; | |
} | |
private const float _WINDOW_PADDING_H = 1; | |
private const float _WINDOW_PADDING_V = 2; | |
private const float _BUTTON_MARGIN_V = 1; | |
private const float _BUTTON_HEIGHT = 20; | |
private float[] _options; | |
private Action<float> _onSubmit; | |
private void SetOptions(float[] options) | |
{ | |
_options = options; | |
for (int i = 0; i < _options.Length; i++) | |
{ | |
int index = i; | |
var button = new Button(() => Submit(index)) | |
{ | |
text = $"{_options[i]:F2}x", | |
style = | |
{ | |
marginTop = _BUTTON_MARGIN_V, | |
marginBottom = _BUTTON_MARGIN_V, | |
height = _BUTTON_HEIGHT, | |
} | |
}; | |
rootVisualElement.Add(button); | |
} | |
} | |
private void Submit(int index) | |
{ | |
_onSubmit(_options[index]); | |
Close(); | |
} | |
private void OnEnable() | |
{ | |
rootVisualElement.style.paddingLeft = _WINDOW_PADDING_H; | |
rootVisualElement.style.paddingRight = _WINDOW_PADDING_H; | |
rootVisualElement.style.paddingTop = _WINDOW_PADDING_V; | |
rootVisualElement.style.paddingBottom = _WINDOW_PADDING_V; | |
} | |
private void OnLostFocus() => Close(); | |
} | |
#endregion | |
#region Custom Left UI | |
private static void InitializeCustomLeftUI() { } | |
private static void UpdateCustomLeftUI() { } | |
#endregion | |
#region Custom Right UI | |
private static void InitializeCustomRightUI() | |
{ | |
// Time Scale | |
InitializeTimeScaleSlider(); | |
} | |
private static void UpdateCustomRightUI() | |
{ | |
// Time Scale | |
UpdateTimeScaleField(); | |
} | |
#endregion | |
#region Lifecycle | |
private static VisualElement _toolbarRoot; | |
private static VisualElement _toolbarLeft; | |
private static VisualElement _toolbarRight; | |
private static VisualElement _customToolbarLeft; | |
private static VisualElement _customToolbarRight; | |
static CustomEditorToolbar() | |
{ | |
// EditorApplication.delayCall += TryInitialize; | |
EditorApplication.update -= OnUpdate; | |
EditorApplication.update += OnUpdate; | |
} | |
private static void TryInitialize() | |
{ | |
if (_toolbarRoot != null) | |
{ | |
return; | |
} | |
var toolbarType = typeof(UnityEditor.Editor).Assembly.GetType("UnityEditor.Toolbar"); | |
var toolbarObj = toolbarType.GetField("get").GetValue(null); | |
_toolbarRoot = (VisualElement)toolbarType.GetField("m_Root", | |
BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(toolbarObj); | |
_toolbarLeft = _toolbarRoot.Q("ToolbarZoneLeftAlign"); | |
_toolbarRight = _toolbarRoot.Q("ToolbarZoneRightAlign"); | |
_customToolbarLeft = new VisualElement | |
{ | |
name = "custom-toolbar-left", | |
style = | |
{ | |
flexGrow = 1, | |
flexDirection = FlexDirection.RowReverse, | |
overflow = Overflow.Hidden, | |
}, | |
}; | |
_toolbarLeft.Add(_customToolbarLeft); | |
_customToolbarRight = new VisualElement | |
{ | |
name = "custom-toolbar-right", | |
style = | |
{ | |
flexGrow = 1, | |
flexDirection = FlexDirection.Row, | |
overflow = Overflow.Hidden, | |
}, | |
}; | |
_toolbarRight.Add(_customToolbarRight); | |
InitializeCustomLeftUI(); | |
InitializeCustomRightUI(); | |
} | |
private static void OnUpdate() | |
{ | |
TryInitialize(); | |
UpdateCustomLeftUI(); | |
UpdateCustomRightUI(); | |
} | |
#endregion | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment