Skip to content

Instantly share code, notes, and snippets.

@nukadelic
Last active September 25, 2019 17:10
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save nukadelic/9d07cc0b2818b5cbd699313a0eb116df to your computer and use it in GitHub Desktop.
Save nukadelic/9d07cc0b2818b5cbd699313a0eb116df to your computer and use it in GitHub Desktop.
Unity ML Agents File Config Editor
// 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