Last active
September 25, 2019 17:10
-
-
Save nukadelic/9d07cc0b2818b5cbd699313a0eb116df to your computer and use it in GitHub Desktop.
Unity ML Agents File Config Editor
This file contains hidden or 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
// Download the DLL here: | |
// https://www.nuget.org/packages/YamlDotNet/ | |
// at "Download package ( ... KB )" | |
// browse for the lib > .Net35 folder copy paste the dll into your unity assets folder | |
// Note this script file must be located inside a folder named "Editor" | |
using System; | |
using System.Text; | |
using System.IO; | |
using System.Linq; | |
using System.Collections.Generic; | |
using YamlDotNet.Serialization; | |
using YamlDotNet.Serialization.NamingConventions; | |
using UnityEditor; | |
using UnityEngine; | |
namespace MLAgents.Editors | |
{ | |
class MLAgentsFileConfigEditor : EditorWindow | |
{ | |
static public string Title = "ML Agents Config"; | |
[MenuItem("ML Agents/Config")] | |
public static void OpenWindow() | |
{ | |
var w = GetWindow<MLAgentsFileConfigEditor>( false, Title, true ); | |
w.minSize = new Vector2( 300, 220 ); | |
} | |
EditorData data = new EditorData(); | |
bool file_loaded = false; | |
YamlSerializer file = new YamlSerializer(); | |
bool gui_isWide = false; | |
Vector2 gui_scroll; | |
bool[] gui_toggles; | |
bool gui_valueChanged = false; | |
void OnEnable() | |
{ | |
// load previous editor state so it won't get lost across different Unity sessions | |
data = data.Load(); | |
file_loaded = data.file_path != ""; | |
if( file_loaded ) file.Open( data ); | |
} | |
void OnDisable() | |
{ | |
data.Save(); | |
} | |
void OnGUI() | |
{ | |
Skin.Initialize(); | |
gui_isWide = Skin.ScreenWidth() > 360; | |
using ( var s = new EditorGUILayout.ScrollViewScope( gui_scroll, Skin.container ) ) | |
{ | |
gui_scroll = s.scrollPosition; | |
GUILayout.Space( 5 ); | |
DrawFilePicker(); | |
GUILayout.Space( 5 ); | |
DrawConfigDropDown(); | |
GUILayout.Space( 5 ); | |
EditorGUILayout.LabelField("", new GUIStyle( GUI.skin.horizontalSlider ), GUILayout.MinWidth( 50 ) ); | |
GUILayout.Space( 5 ); | |
DrawConfig(); | |
HandleMouse(); | |
} | |
DrawStickyFooter(); | |
} | |
void DrawStickyFooter() | |
{ | |
using( new EditorGUILayout.VerticalScope( Skin.footerBackground ) ) | |
{ | |
GUILayout.Space( 2 ); | |
using( new EditorGUILayout.HorizontalScope() ) | |
{ | |
using( new EditorGUILayout.VerticalScope( GUILayout.Width( 55 ) ) ) | |
{ | |
GUILayout.Space( 2 ); | |
using( new EditorGUILayout.HorizontalScope( GUILayout.Width( 55 ) ) ) | |
{ | |
GUILayout.Label( "Edit", GUILayout.Width( 30 ) ); | |
using( new EditorGUILayout.VerticalScope( GUILayout.Width( 20 ) ) ) | |
{ | |
data.editMode = GUILayout.Toggle( data.editMode, "" ); | |
GUILayout.Space( 3 ); | |
} | |
} | |
} | |
using( new EditorGUILayout.VerticalScope() ) | |
{ | |
GUILayout.Space( 2 ); | |
using( new EditorGUILayout.HorizontalScope() ) | |
{ | |
if( GUILayout.Button( "New Item" ) ) | |
Prompt.Open( Title, "Enter key name", "new_item", delegate( string input ) { | |
if( string.IsNullOrEmpty( input ) ) return false; | |
var o = new YamlObject( input, "0" ); | |
o.parent = GetRootNode(); | |
o.parent.list.Add( o ); | |
return true; | |
} ); | |
using ( var scope = new EditorGUI.DisabledGroupScope( ! gui_valueChanged ) ) | |
{ | |
if( GUILayout.Button( "Save" ) ) | |
{ | |
file.Save( data ); | |
gui_valueChanged = false; | |
} | |
if( GUILayout.Button( Skin.cancelIcon, GUILayout.Width( 20 ), GUILayout.Height( 18 ) ) ) | |
{ | |
file.Open( data ); | |
SelectConfig( data.selected_config_index, true ); | |
} | |
} | |
} | |
} | |
} | |
GUILayout.Space( 1 ); | |
} | |
} | |
void DrawFilePicker() | |
{ | |
bool click = false; | |
if( gui_isWide ) GUILayout.BeginHorizontal(); | |
GUILayout.Label("Config file path:", GUILayout.Width( 140 ) ); | |
if( ! gui_isWide ) GUILayout.BeginHorizontal(); | |
click = GUILayout.Button( | |
data.file_path, | |
GUI.skin.textField, | |
GUILayout.MaxWidth( Skin.ScreenWidth( gui_isWide ? -160 : - 45 ) ) | |
); | |
click = click || GUILayout.Button( Skin.folderIcon, GUI.skin.label, GUILayout.MaxWidth( 20 ) ); | |
GUILayout.EndHorizontal(); | |
if( click ) | |
{ | |
string filePath = EditorUtility.OpenFilePanel( "Browse Config File", "", "yaml" ); | |
if( filePath != "" ) // check if form wans't closed / cancel was pressed | |
{ | |
data.file_path = filePath; | |
data.selected_config_index = 0; | |
file_loaded = true; | |
file.Open( data ); | |
gui_valueChanged = false; | |
} | |
} | |
} | |
void DrawConfigDropDown() | |
{ | |
if( ! file_loaded ) return; | |
Rect rect; | |
bool clicked; | |
using( new EditorGUI.DisabledGroupScope( gui_valueChanged ) ) | |
{ | |
if( gui_isWide ) GUILayout.BeginHorizontal(); | |
GUILayout.Label("Selected config: ", GUILayout.Width( 140 ) ); | |
rect = GUILayoutUtility.GetLastRect(); | |
clicked = GUILayout.Button( file.configNames[ data.selected_config_index ], Skin.dropDownButton, GUILayout.MaxWidth( Skin.ScreenWidth( gui_isWide ? - 135 : - 15 ) ) ); | |
if( gui_isWide ) GUILayout.EndHorizontal(); | |
} | |
if ( file.selectedConfigWasChanged || ! clicked ) return; | |
GenericMenu configMenu = new GenericMenu(); | |
for( var i = 0; i < file.configNames.Length; ++i ) | |
{ | |
string label = file.configNames[ i ]; | |
if( i == data.selected_config_index ) | |
{ | |
configMenu.AddDisabledItem( new GUIContent( label ) ); | |
} | |
else configMenu.AddItem( new GUIContent( label ), false, | |
() => SelectConfig( file.configNames.ToList().IndexOf( label ) ) | |
); | |
} | |
rect.x += gui_isWide ? 145 : 0; | |
rect.y = rect.yMax + ( gui_isWide ? 4 : 20 ); | |
rect.width = rect.height = 0; | |
configMenu.DropDown( rect ); | |
} | |
void SelectConfig( int index, bool reaload = false ) | |
{ | |
if( index > file.items.Count - 1 ) index = 0; | |
else if( index < 0 ) index = 0; | |
bool changed = false; | |
if( data.selected_config_index != index || reaload ) | |
{ | |
data.selected_config_index = index; | |
changed = true; | |
gui_valueChanged = false; | |
} | |
if( changed || gui_toggles == null ) | |
{ | |
int c = file.items[ index ].list.Count; | |
if( file.hasDefault ) c = file.defaultConfig.Count; | |
if( c > 0 ) gui_toggles = new bool[ c ]; | |
} | |
} | |
YamlObject GetRootNode() | |
{ | |
return file.items[ data.selected_config_index ]; | |
} | |
void DrawConfig() | |
{ | |
if( ! file_loaded ) return; | |
if( Event.current.type == EventType.Repaint ) | |
labelRectList.Clear(); | |
DrawValue( GetRootNode().list ); | |
} | |
void DrawValue( List<YamlObject> items, int space = 0 ) | |
{ | |
int label_width = ( gui_isWide ? 160 : 122 ) - space; | |
foreach( var item in items ) | |
{ | |
var label = new GUIContent( item.name, item.name ); | |
using( new EditorGUILayout.HorizontalScope() ) | |
{ | |
GUILayout.Space( space ); | |
if( data.editMode ) | |
{ | |
GUILayout.Label( label, GUILayout.MaxWidth( label_width ) ); | |
labelRectCapture(); | |
GUILayout.FlexibleSpace(); | |
if( DrawItemControls( item ) ) | |
{ | |
items.Remove( item ); | |
GUIUtility.ExitGUI(); | |
gui_valueChanged = true; | |
return; | |
} | |
} | |
else | |
{ | |
if( item.isList ) GUILayout.Label( label ); | |
else GUILayout.Label( label, GUILayout.MaxWidth( label_width ) ); | |
labelRectCapture(); | |
if( ! item.isList ) | |
{ | |
using( var scope = new EditorGUI.ChangeCheckScope() ) | |
{ | |
var input_layout = GUILayout.MaxWidth( Skin.ScreenWidth( - label_width - space + 10 ) ); | |
string value = GUILayout.TextField( item.value, input_layout ); | |
if( scope.changed ) | |
{ | |
item.value = value; | |
gui_valueChanged = true; | |
} | |
} | |
} | |
} | |
} | |
if( item.isList ) | |
{ | |
DrawValue( item.list, space + 10 ); | |
} | |
} | |
} | |
bool DrawItemControls( YamlObject item ) | |
{ | |
GUILayout.Label( "Edit" ); | |
GUILayout.Space( 5 ); | |
if( GUILayout.Button( Skin.editIcon, GUILayout.Width( 20 ), GUILayout.Height( 14 ) ) ) | |
{ | |
Prompt.Open( Title, "Edit key name", item.name, delegate( string input ) | |
{ | |
input = input.Trim().ToLower(); | |
if( string.IsNullOrEmpty( input ) ) return false; | |
item.name = input; | |
gui_valueChanged = true; | |
return true; | |
}); | |
} | |
GUILayout.Space( 5 ); | |
GUILayout.Label( "Delete" ); | |
GUILayout.Space( 5 ); | |
GUI.backgroundColor = Skin.colorAlpha70; | |
if( GUILayout.Button( Skin.deleteIcon, GUILayout.Width( 20 ), GUILayout.Height( 14 ) ) ) | |
{ | |
if( EditorUtility.DisplayDialog( Title, "Delete key '" + item.name + "' ?", "Yes", "Cancel" ) ) | |
{ | |
return true; | |
} | |
} | |
GUI.backgroundColor = Color.white; | |
return false; | |
} | |
bool mouseIsDown = false; | |
Vector2 labelLastMouse; | |
bool labelValidDropIndex = false; | |
List<Rect> labelRectList = new List<Rect>(); | |
int labelDragIndex = -1; | |
int labelDropIndex = -1; | |
bool labelDropInside = false; | |
void labelRectCapture() | |
{ | |
if( Event.current.type == EventType.Repaint ) | |
{ | |
Rect labelRect = GUILayoutUtility.GetLastRect(); | |
labelRect.x = 0; | |
labelRect.width = 150; | |
labelRect.height += 4; | |
labelRect.y -= 2; | |
labelRectList.Add( labelRect ); | |
} | |
} | |
void Update() | |
{ | |
// Smooth scroll | |
if( mouseIsDown ) Repaint(); | |
} | |
int GetMouseDropIndex() | |
{ | |
for( var i = 0; i < labelRectList.Count; ++i ) | |
if( labelRectList[ i ].Contains( Event.current.mousePosition ) ) | |
return i; | |
return -1; | |
} | |
void HandleMouse() | |
{ | |
Event e = Event.current; | |
labelDropIndex = GetMouseDropIndex(); | |
// Stop mouse drag | |
if( e.type == EventType.MouseUp ) | |
{ | |
mouseIsDown = false; | |
// Apply new index | |
if( labelValidDropIndex ) | |
{ | |
YamlObject.Shift( GetRootNode().list, labelDragIndex, labelDropIndex, labelDropInside ); | |
gui_valueChanged = true; | |
} | |
labelDragIndex = -1; | |
Repaint(); | |
return; | |
} | |
// Begin mouse drag | |
if( e.type == EventType.MouseDown ) | |
{ | |
mouseIsDown = true; | |
labelLastMouse = e.mousePosition; | |
labelDragIndex = GetMouseDropIndex(); | |
labelDropIndex = -1; | |
if( labelDragIndex > -1 ) | |
{ | |
GUI.FocusControl( null ); | |
Repaint(); | |
} | |
return; | |
} | |
if( ! mouseIsDown ) return; | |
float screenH = Screen.height / EditorGUIUtility.pixelsPerPoint; | |
float minDisnceNearEdge = Mathf.Max( screenH / 4f, 70 ); | |
bool draggingDown = labelLastMouse.y - e.mousePosition.y > 0; | |
// Scroll | |
if( e.type != EventType.MouseUp && labelDragIndex > -1 ) | |
{ | |
float mouseY = e.mousePosition.y - gui_scroll.y; | |
if( mouseY < minDisnceNearEdge && draggingDown ) | |
{ | |
gui_scroll.y -= 1; | |
if( gui_scroll.y < 0 ) gui_scroll.y = 0; | |
Repaint(); | |
} | |
else if( mouseY > screenH - minDisnceNearEdge && ! draggingDown ) | |
{ | |
gui_scroll.y += 1; | |
Repaint(); | |
} | |
} | |
labelValidDropIndex = false; | |
float lineWidth = gui_isWide ? 150f : 120f; | |
float dropInsideDistance = lineWidth / 2; | |
// Draw lines | |
if( labelDragIndex > -1 && labelDropIndex > -1 ) | |
{ | |
labelDropInside = e.mousePosition.x > dropInsideDistance; | |
bool isPrevious = labelDragIndex - labelDropIndex == 1; | |
labelValidDropIndex = ! ( labelDropIndex == labelDragIndex || ( isPrevious && ! labelDropInside ) ); | |
// Show start of drag | |
// if( ! labelValidDropIndex ) | |
if( true ) { | |
Rect r = labelRectList[ labelDragIndex ]; | |
float above = r.y + r.height - gui_scroll.y + 2; | |
float under = above + 16; | |
Skin.DrawLineH( 5, under, lineWidth, Skin.grayAlpha50, 2 ); | |
Skin.DrawLineH( 5, above, lineWidth, Skin.grayAlpha50, 2 ); | |
Skin.DrawAxisY( dropInsideDistance, Skin.grayAlpha50, 2 ); | |
} | |
// Show drop target | |
if( labelValidDropIndex ) | |
{ | |
Rect r = labelRectList[ labelDropIndex ]; | |
float above = r.y + r.height - gui_scroll.y + 2; | |
float under = above + 16; | |
if( labelDropInside ) | |
{ | |
Skin.DrawLineH( 5, above, lineWidth, Color.blue, 2 ); | |
Skin.DrawLineH( 5, under, lineWidth, Color.blue, 2 ); | |
} | |
else | |
{ | |
Skin.DrawLineH( 5, under, lineWidth, Color.blue, 2 ); | |
} | |
} | |
} | |
// Update last position | |
if( e.type == EventType.MouseDrag ) | |
{ | |
labelLastMouse = e.mousePosition; | |
} | |
} | |
// _______ ______ ________ ______ | |
// | \ / \| \ / \ | |
// | $$$$$$$\| $$$$$$\\$$$$$$$$| $$$$$$\ | |
// | $$ | $$| $$__| $$ | $$ | $$__| $$ | |
// | $$ | $$| $$ $$ | $$ | $$ $$ | |
// | $$ | $$| $$$$$$$$ | $$ | $$$$$$$$ | |
// | $$__/ $$| $$ | $$ | $$ | $$ | $$ | |
// | $$ $$| $$ | $$ | $$ | $$ | $$ | |
// \$$$$$$$ \$$ \$$ \$$ \$$ \$$ | |
[Serializable] | |
class EditorData | |
{ | |
public bool hide_header = false; | |
public bool editMode = false; | |
public string file_path = ""; | |
public int selected_config_index = 0; | |
string GetDataKey() { return typeof( MLAgentsFileConfigEditor ).ToString() + "_pref_datakey_1"; } | |
public EditorData Load() | |
{ | |
var json = EditorPrefs.GetString( GetDataKey() , ""); | |
if ( json != "") return JsonUtility.FromJson<EditorData>( json ); | |
else Save(); | |
return this; | |
} | |
public void Save() | |
{ | |
var json = JsonUtility.ToJson( this ); | |
EditorPrefs.SetString( GetDataKey(), json ); | |
} | |
} | |
// __ __ ______ __ __ __ | |
// | \ / \ / \ | \ / \| \ | |
// \$$\ / $$| $$$$$$\| $$\ / $$| $$ | |
// \$$\/ $$ | $$__| $$| $$$\ / $$$| $$ | |
// \$$ $$ | $$ $$| $$$$\ $$$$| $$ | |
// \$$$$ | $$$$$$$$| $$\$$ $$ $$| $$ | |
// | $$ | $$ | $$| $$ \$$$| $$| $$_____ | |
// | $$ | $$ | $$| $$ \$ | $$| $$ \ | |
// \$$ \$$ \$$ \$$ \$$ \$$$$$$$$ | |
class YamlObject | |
{ | |
public string name; | |
public string value; | |
public List<YamlObject> list; | |
public YamlObject parent = null; | |
public bool isList { get { return list != null; } } | |
public static void Shift( List<YamlObject> list, int fromIndex, int toIndex, bool inside ) | |
{ | |
if( fromIndex == toIndex || fromIndex < 0 || toIndex < 0 ) return; | |
int insertIndex = 0; | |
// excluding the root node | |
int index = -1; | |
YamlObject fromItem = null, toItem = null; | |
Action<List<YamlObject>> scan = null; | |
scan = delegate( List<YamlObject> arr ) | |
{ | |
foreach( var item in arr ) | |
{ | |
index ++ ; | |
if( index == fromIndex ) fromItem = item; | |
if( toIndex == index && inside ) | |
{ | |
toItem = item; | |
insertIndex = 0; | |
} | |
if( toIndex == index && ! inside ) | |
{ | |
toItem = item.parent; | |
insertIndex = arr.IndexOf( item ); | |
} | |
if( item.isList ) | |
{ | |
scan( item.list ); | |
} | |
} | |
}; | |
// recursive search | |
scan( list ); | |
if( fromItem == null || toItem == null ) return; | |
fromItem.parent.list.Remove( fromItem ); | |
if( fromItem.parent.list.Count == 0 ) fromItem.parent.list = null; | |
if( toItem.list == null ) toItem.list = new List<YamlObject>(); | |
toItem.list.Insert( insertIndex , fromItem ); | |
fromItem.parent = toItem; | |
} | |
public static Dictionary<object,object> toDictionary( List<YamlObject> list ) | |
{ | |
var dict = new Dictionary<object,object>(); | |
foreach( var l in list ) | |
l.toDictionary( ref dict ); | |
return dict; | |
} | |
public void toDictionary( ref Dictionary<object,object> dict ) | |
{ | |
// if( ! isList ) throw new Exception("Only lists can be converted"); | |
if( dict == null ) dict = new Dictionary<object,object>(); | |
if( isList ) | |
{ | |
Dictionary<object,object> dictList = new Dictionary<object,object>(); | |
list.ForEach( yo => yo.toDictionary( ref dictList ) ); | |
dict.Add( this.name, dictList ); | |
} | |
else | |
{ | |
dict.Add( this.name, this.value ); | |
} | |
} | |
public YamlObject( string name, string value ) | |
{ | |
this.name = name; | |
this.value = value; | |
} | |
public YamlObject( KeyValuePair<object, object> keyVal ) | |
{ | |
this.name = keyVal.Key.ToString(); | |
if( keyVal.Value.GetType() == typeof( string ) ) | |
this.value = keyVal.Value.ToString(); | |
else | |
{ | |
this.list = new List<YamlObject>(); | |
var data = ( Dictionary<object, object> ) keyVal.Value; | |
foreach( var d in data ) | |
{ | |
var o = new YamlObject( d ); | |
o.parent = this; | |
this.list.Add( o ); | |
} | |
} | |
} | |
} | |
class YamlSerializer | |
{ | |
public string[] configNames = new string[] { " - " }; | |
public bool selectedConfigWasChanged = false; | |
public bool hasDefault = false; | |
public Dictionary<object, object> defaultConfig; | |
// public List<Dictionary<object, object>> configs; | |
public List<YamlObject> items; | |
public void Open( EditorData editorData ) | |
{ | |
selectedConfigWasChanged = false; | |
var fs = new FileStream( editorData.file_path, FileMode.Open, FileAccess.Read); | |
using ( var reader = new StreamReader( fs, Encoding.UTF8)) | |
{ | |
var deserializer = new DeserializerBuilder() | |
.WithNamingConvention( new CamelCaseNamingConvention() ) | |
.Build(); | |
var raw = deserializer.Deserialize< Dictionary< object, object > >( reader ); | |
items = new List<YamlObject>(); | |
foreach( var r in raw ) | |
{ | |
items.Add( new YamlObject( r ) ); | |
} | |
configNames = raw.Keys.Select( k => k.ToString() ).ToArray(); | |
hasDefault = configNames.Contains( "default" ); | |
var data = raw.Values.Select( config => ( Dictionary<object, object> ) config ).ToArray(); | |
// configs = new List<Dictionary<object, object>>(); | |
int index = 0; | |
foreach( var d in data ) | |
{ | |
Dictionary<object, object> dict = new Dictionary<object, object>(); | |
foreach( var k in d.Keys ) dict.Add( k.ToString(), d[ k ] ); | |
bool isDefault = configNames[ index ++ ].ToLower() == "default"; | |
if( isDefault ) defaultConfig = dict; | |
// configs.Add( dict ); | |
} | |
// configNames = configNames.SkipWhile( label => label.ToLower() == "default" ).ToArray(); | |
} | |
} | |
public void Save( EditorData editorData ) | |
{ | |
var serializer = new SerializerBuilder().Build(); | |
Dictionary<object,object> data = YamlObject.toDictionary( items ); | |
var yaml = serializer.Serialize( data ); | |
StreamWriter writer = new StreamWriter( editorData.file_path, false ); | |
writer.Write( yaml ); | |
writer.Close(); | |
} | |
} | |
// ______ ________ __ __ __ ________ | |
// / \| \| \ / \| \ | \ | |
// | $$$$$$\\$$$$$$$$ \$$\ / $$| $$ | $$$$$$$$ | |
// | $$___\$$ | $$ \$$\/ $$ | $$ | $$__ | |
// \$$ \ | $$ \$$ $$ | $$ | $$ \ | |
// _\$$$$$$\ | $$ \$$$$ | $$ | $$$$$ | |
// | \__| $$ | $$ | $$ | $$_____ | $$_____ | |
// \$$ $$ | $$ | $$ | $$ \| $$ \ | |
// \$$$$$$ \$$ \$$ \$$$$$$$$ \$$$$$$$$ | |
class Skin | |
{ | |
public static Material lineMaterial; | |
public static GUIStyle dropDownButton; | |
public static GUIStyle container; | |
public static GUIStyle footerBackground; | |
public static GUIContent folderIcon; | |
public static GUIContent deleteIcon; | |
public static GUIContent cancelIcon; | |
public static GUIContent editIcon; | |
public static Color colorAlpha70 = new Color( 1,1,1, 0.7f ); | |
public static Color grayAlpha50 = new Color( 0.5f,0.5f,0.5f, 0.5f ); | |
public static Color grayAlpha20 = new Color( 0.5f,0.5f,0.5f, 0.2f ); | |
public static bool ready = false; | |
public static void Initialize() | |
{ | |
if( ready ) return; ready = true; | |
if ( ! lineMaterial ) | |
{ | |
// Unity has a built-in shader that is useful for drawing simple colored things. | |
Shader shader = Shader.Find("Hidden/Internal-Colored"); | |
lineMaterial = new Material(shader); | |
lineMaterial.hideFlags = HideFlags.HideAndDontSave; | |
// Turn on alpha blending | |
lineMaterial.SetInt("_SrcBlend", (int)UnityEngine.Rendering.BlendMode.SrcAlpha); | |
lineMaterial.SetInt("_DstBlend", (int)UnityEngine.Rendering.BlendMode.OneMinusSrcAlpha); | |
// Turn backface culling off | |
lineMaterial.SetInt("_Cull", (int)UnityEngine.Rendering.CullMode.Off); | |
// Turn off depth writes | |
lineMaterial.SetInt("_ZWrite", 0); | |
} | |
footerBackground = new GUIStyle(); | |
StyleBackground( footerBackground, grayAlpha20 ); | |
dropDownButton = GUI.skin.FindStyle("DropDownButton"); | |
container = new GUIStyle(); | |
container.padding = new RectOffset( 5,5,5,5 ); | |
folderIcon = Icon( "LookDevObjRotation" ); | |
cancelIcon = Icon( "LookDevClose" ); | |
deleteIcon = Icon( "LookDevClose" ); | |
deleteIcon.tooltip = "Delete item"; | |
editIcon = Icon( "editicon.sml" ); | |
editIcon.tooltip = "Edit item"; | |
} | |
/// <summery> Set background color to the given style </summery> | |
public static void StyleBackground( GUIStyle target, Color background ) | |
{ | |
Texture2D backgroundTex = new Texture2D(1,1); | |
backgroundTex.SetPixel(0,0, background ); | |
backgroundTex.Apply(); | |
target.normal.background = backgroundTex; | |
target.onNormal.background = backgroundTex; | |
target.active.background = backgroundTex; | |
target.onActive.background = backgroundTex; | |
target.hover.background = backgroundTex; | |
target.onHover.background = backgroundTex; | |
target.focused.background = backgroundTex; | |
target.onFocused.background = backgroundTex; | |
} | |
public static void DrawLine( float startX, float startY, float endX, float endY, Color color ) | |
{ | |
// Convert pixel values to UV coordinates, flip the Y axis and scale to screen DPI | |
Vector2 positionStart = new Vector2( | |
EditorGUIUtility.pixelsPerPoint * startX / Screen.width, | |
EditorGUIUtility.pixelsPerPoint * startY / Screen.height | |
); | |
Vector2 positionEnd = new Vector2( | |
EditorGUIUtility.pixelsPerPoint * endX / Screen.width, | |
EditorGUIUtility.pixelsPerPoint * endY / Screen.height | |
); | |
positionStart.y = 1 - positionStart.y; | |
positionEnd.y = 1 - positionEnd.y; | |
// Draw line | |
GL.PushMatrix(); | |
lineMaterial.SetPass(0); | |
GL.LoadOrtho(); | |
GL.Begin(GL.LINES); | |
GL.Color(color); | |
GL.Vertex(positionStart); | |
GL.Vertex(positionEnd); | |
GL.End(); | |
GL.PopMatrix(); | |
} | |
public static void DrawAxisY( float x, Color color, int thinkness = 2 ) | |
{ | |
// 1 = EditorGUIUtility.pixelsPerPoint * Y / Screen.height | |
// Screen.height = EditorGUIUtility.pixelsPerPoint * Y | |
// Y = Screen.height / EditorGUIUtility.pixelsPerPoint | |
DrawLine( x, 0, x, Screen.height / EditorGUIUtility.pixelsPerPoint, color ); | |
if( thinkness > 1 ) | |
DrawAxisY( x + 1 / EditorGUIUtility.pixelsPerPoint, color, thinkness - 1 ); | |
} | |
public static void DrawLineH( float x, float y, float width, Color color, int thinkness = 2 ) | |
{ | |
DrawLine( x, y - 1, x + width, y - 1, color ); | |
if( thinkness > 1 ) | |
DrawLineH( x, y + 1 / EditorGUIUtility.pixelsPerPoint , width , color, thinkness - 1 ); | |
} | |
/// https://unitylist.com/p/5c3/Unity-editor-icons | |
public static GUIContent Icon( string name, bool darkSkinSupported = true ) | |
{ | |
string prefix = darkSkinSupported ? ( EditorGUIUtility.isProSkin ? "d_" : "" ) : ""; | |
return new GUIContent( EditorGUIUtility.IconContent( prefix + name ) ); | |
} | |
/// <summery> Get true width ( useful when using absolute coordinates and screen scaling on Windows OS ) </summery> | |
static public float ScreenWidth( float delta = 0 ) | |
{ | |
float ppp = EditorGUIUtility.pixelsPerPoint; | |
return Screen.width / ppp + delta * ppp; | |
} | |
} | |
class Prompt : EditorWindow | |
{ | |
/// body text is rich text and can accept basic html elements like <b> and <i> | |
/// onEnter accepts the text input and must return a boolean ( if true window will close, else show "bad input" helpbox ) | |
static public void Open( string title, string text, Func<string, bool> onEnter ) | |
{ | |
var w = Popup( title, onEnter ); | |
w.text = text; | |
} | |
static public void Open( string title, string text, string input, Func<string, bool> onEnter ) | |
{ | |
var w = Popup( title, onEnter ); | |
w.input = input; | |
w.text = text; | |
} | |
static public void Password( string title, string text, Func<string, bool> onEnter ) | |
{ | |
var w = Popup( title, onEnter ); | |
w.is_password = true; | |
w.text = text; | |
} | |
static Prompt Popup( string title, Func<string, bool> onEnter ) | |
{ | |
var win = GetWindow<Prompt>( true, title, true ); | |
var size = new Vector2( 300, 100 ); | |
win.minSize = size; | |
win.maxSize = size; | |
win.onEnter = onEnter; | |
return win; | |
} | |
Func<string, bool> onEnter = null; | |
string input = ""; | |
string text = ""; | |
bool valid_input = true; | |
bool is_password = false; | |
GUIStyle style_input; | |
GUIStyle style_button; | |
GUIStyle style_text; | |
void OnLostFocus() | |
{ | |
Close(); | |
} | |
void OnGUI() | |
{ | |
if( style_input == null ) | |
{ | |
style_text = new GUIStyle( GUI.skin.label ); | |
style_text.wordWrap = true; | |
style_text.richText = true; | |
style_text.margin = new RectOffset( 5,5,4,4 ); | |
style_input = new GUIStyle( GUI.skin.textField ); | |
style_input.padding = new RectOffset( 5,5,8,3 ); | |
style_input.fixedHeight = 25; | |
style_button = new GUIStyle( GUI.skin.button ); | |
style_button.padding = new RectOffset( 14,10,4,6 ); | |
style_button.margin = new RectOffset( 5,5,5,5 ); | |
style_button.fontSize = 12; | |
} | |
GUILayout.BeginVertical(); | |
{ | |
GUILayout.Space( 15 ); | |
GUILayout.Label( text , style_text ); // unknown height | |
GUILayout.Space( 5 ); | |
EditorGUI.BeginChangeCheck(); // ~20px | |
{ | |
GUILayout.BeginHorizontal(); | |
{ | |
GUILayout.Space( 8 ); | |
GUI.SetNextControlName("PromptInputTextField"); | |
if( is_password ) | |
{ | |
input = GUILayout.PasswordField( input, '*', style_input ); | |
} | |
else input = GUILayout.TextField( input, style_input ); | |
GUILayout.Space( 8 ); | |
} | |
GUILayout.EndHorizontal(); | |
if( is_password ) GUILayout.Space( 10 ); | |
GUI.FocusControl("PromptInputTextField"); | |
} | |
if( EditorGUI.EndChangeCheck() ) | |
{ | |
// Hide error if any | |
valid_input = true; | |
} | |
if( ! valid_input ) // 40px | |
{ | |
EditorGUILayout.HelpBox( "Invalid input", MessageType.Error ); | |
} | |
} | |
GUILayout.EndVertical(); | |
if( Event.current.type == EventType.Repaint ) | |
{ | |
var height = GUILayoutUtility.GetLastRect().height; | |
var size = new Vector2( minSize.x, height + 50 ); | |
minSize = size; | |
maxSize = size; | |
} | |
GUILayout.Space( 10 ); | |
GUILayout.BeginHorizontal(); // 40px | |
{ | |
GUILayout.FlexibleSpace(); | |
// GUI.backgroundColor = new Color( 0.2f, 0.8f, 0.2f, 1f ); | |
KeyCode code = Event.current.keyCode; | |
bool isEnter = ( Event.current.type == EventType.KeyDown || Event.current.isKey ) | |
&& ( code == KeyCode.KeypadEnter || code == KeyCode.Return ); | |
if( isEnter ) Event.current.Use(); | |
if( GUILayout.Button("Enter", style_button) || isEnter ) | |
{ | |
if( onEnter( input ) ) | |
{ | |
// Input is valid -> close window | |
Close(); | |
} | |
else | |
{ | |
// Show error if input is invalid | |
valid_input = false; | |
} | |
} | |
GUI.backgroundColor = Color.white; | |
} | |
GUILayout.EndHorizontal(); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment