Custom Editor GUI Elements

The closest thing I can find is AngryAnts untested post for a MyFocusControl. Another resource is using Go to Declaration in monodevelop on some of the built in gui elements which just uses reflection. If anyone has some examples on GitHub, Pastebin, etc; please drop a link.

I know there is Extending UnityGUI page in the Unity manual but it only goes over combining two elements.

I am trying to create a selectable foldout for use in a EditorWindow. One issue I am having is that the gui is not updating properly with the state of the toggle. I tried setting GUI.changed because the reflection foldout source did it. I know there are ways to set data as dirty in order for Unity to save the new data but I can’t find anything for the GUI.

Mockup:

SelectableFoldout mockup

You might suggest that I just combine a foldout with empty label and a selectable label but then I couldn’t have the label make the foldout toggle.
Also I am not entirely sure on what how to get the proper control id. I have a feeling it is just a unique int so almost any of the commented out lines(below) should probably work.

Here is the decompiled reflection source for the EditorGUI.SelectableLabel() and EditorGUI.Foldout() for reference.

I realize the code below doesn’t have the “selectable” part with the MouseMove event in yet.

public static bool SelectableFoldout(Rect position, bool isExpanded, GUIContent content,  [DefaultValue ("EditorStyles.foldout")] GUIStyle style)
{
	int controlID = 0;//GUIUtility.GetControlID ();//GUIUtility.GetControlID(content, FocusType.Keyboard, position);
	controlID = GUIUtility.GetControlID(content, FocusType.Keyboard, position);
	//controlID = GUIUtility.GetControlID(content, FocusType.Passive, position);
	//controlID = GUIUtility.GetControlID(content, FocusType.Native, position);
	//controlID = EditorGUIUtility.GetControlID(FocusType.Native);
	//controlID = "SelectableFoldout".GetHashCode();

	//Debug.Log("Control id: " + controlID);

	EventType eventType = Event.current.type;
	switch (eventType)
	{
		case EventType.MouseDown:
		{
			GUIUtility.hotControl = controlID;
			break;
		}

		case EventType.MouseUp:
		{
			Debug.Log("mouse up: " + Event.current.mousePosition);
			Debug.Log(position + " " + position.Contains(Event.current.mousePosition));

			GUIUtility.hotControl = 0;
			// Toggle the foldout
			if(position.Contains(Event.current.mousePosition))
			{
				isExpanded = !isExpanded;
			}

			Debug.Log(isExpanded);
			//GUI.changed = true;
			
			break;

		}
		case EventType.Repaint:
		{
			style.Draw(EditorGUI.IndentedRect(position), content, controlID, isExpanded);

			break;
		}
	}

	return isExpanded;
}

// Alternative construcotr if you don't want to deal with GUIContent
public static bool SelectableFoldout(Rect position, bool isExpanded, string text, [DefaultValue ("EditorStyles.foldout")] GUIStyle style)
{
	return SelectableFoldout(position, isExpanded, new GUIContent(text), style);
}

In the beginning, I was trying reimplement all of the SelectableLabel functionality but I ended up using Reflection to get access to EditorGUI.DoTextField(..). EditorGUI.DoTextField(..) handles all of the nice text field features for many gui elements like TextField, TextArea, PasswordField, and SelectableLabel.

Usage:

bool foldoutOpen = false;
foldoutOpen = MLMEditorGUI.SelectableFoldout(EditorGUILayout.GetControlRect(), foldoutOpen, "Selectable Foldout", EditorStyles.label, EditorStyles.foldout);

MLMEditorGUI.cs:

using UnityEngine;
using UnityEngine.Internal;
using UnityEditor;
using System;
using System.Reflection;

public static class MLMEditorGUI
{

	public struct GUIStructure
	{
		public Rect rect;
		public GUIContent content;
	}

	static Type recycledTextEditorType;
	static object recycledTextEditor;
	static MethodInfo EditorGUI_DoTextField_MethodInfo;
	static MethodInfo RecycledTextEditor_MouseDragSelectsWholeWords_MethodInfo;
	static MethodInfo RecycledTextEditor_SelectAll_MethodInfo;

	static DateTime lastClickTime = new DateTime();
	static int lastClickCount = 0;

	// Static Contstructor
	static MLMEditorGUI()
	{
		lastClickTime = new DateTime();
		lastClickCount = 0;

		// Instantiate a EditorGUI.RecycledTextEditor which is internal...
		BindingFlags bindingFlags = BindingFlags.Instance | BindingFlags.NonPublic | BindingFlags.Static;
		Type editorGUIType = typeof(EditorGUI).Assembly.GetType("UnityEditor.EditorGUI");

		if(editorGUIType != null)
		{
			recycledTextEditorType = editorGUIType.GetNestedType("RecycledTextEditor", bindingFlags);

			if(recycledTextEditorType != null)
			{
				recycledTextEditor = Activator.CreateInstance(recycledTextEditorType);

				if(recycledTextEditor != null)
				{
					RecycledTextEditor_MouseDragSelectsWholeWords_MethodInfo = recycledTextEditorType.GetMethod("MouseDragSelectsWholeWords");
					RecycledTextEditor_SelectAll_MethodInfo = recycledTextEditorType.GetMethod("SelectAll");

					EditorGUI_DoTextField_MethodInfo = typeof(EditorGUI).GetMethod("DoTextField", bindingFlags);
				}
			}
		}
	}




	public static bool SelectableFoldout(Rect position, bool isExpanded, GUIContent content, [DefaultValue ("EditorStyles.label")] GUIStyle labelStyle, [DefaultValue ("EditorStyles.foldout")] GUIStyle foldoutStyle)
	{
		
		Rect controlPosition = EditorGUI.IndentedRect(position);

		int controlID = 0;//GUIUtility.GetControlID ();//GUIUtility.GetControlID(content, FocusType.Keyboard, position);
		controlID = GUIUtility.GetControlID(content, FocusType.Keyboard, controlPosition);

		
		GUIStyle foldoutPaintStyle = new GUIStyle(foldoutStyle);
		foldoutStyle.padding.right = 0;
		foldoutStyle.margin.right = 0;

		GUIStyle normalLabelStyle = new GUIStyle(labelStyle);
		normalLabelStyle.margin.left = 0;
		normalLabelStyle.padding.left = 0;
		
		Rect startRect = new Rect(controlPosition);
		startRect.width = 0;
		
		GUIContent foldoutContent = new GUIContent("");
		Rect foldoutRectPos = GetHorizPosFromLastPos(new GUIStructure{rect=startRect, content=foldoutContent}, foldoutPaintStyle);


		Rect wholeRectPos = GetHorizPosFromLastPos(new GUIStructure{rect=foldoutRectPos, content=content}, normalLabelStyle);

		EventType eventType = Event.current.type;
		switch (eventType)
		{
		case EventType.MouseDown:
		{
			if (wholeRectPos.Contains(Event.current.mousePosition) && Event.current.button == 0)
			{
				// If we are triple clicking
				// We have to do this because Event.current.clickCount never returns 3
				if(lastClickCount == 2 && (DateTime.Now - lastClickTime).TotalSeconds < .3f)
				{
					//Debug.Log("Selecting All");
					if(RecycledTextEditor_SelectAll_MethodInfo != null)
					{
						RecycledTextEditor_SelectAll_MethodInfo.Invoke(recycledTextEditor, new object[] {});
					}

					Event.current.Use();
				}

				lastClickTime = DateTime.Now;
				lastClickCount = Event.current.clickCount;
			}
			// If we arn't clicking the label reset the select word feature in the text editor
			else
			{
				// Make the texteditor by default not select whole words because that can get annoying
				if(RecycledTextEditor_MouseDragSelectsWholeWords_MethodInfo != null)
				{
					// editor.MouseDragSelectsWholeWords(true);
					RecycledTextEditor_MouseDragSelectsWholeWords_MethodInfo.Invoke(recycledTextEditor, new object[] { false });
				}
			}

			break;
		}
		case EventType.MouseUp:
		{
			//Debug.Log("mouse up: " + Event.current.mousePosition);
			//Debug.Log(position + " " + position.Contains(Event.current.mousePosition));
			
			GUIUtility.hotControl = 0;
			// Toggle the foldout
			if(controlPosition.Contains(Event.current.mousePosition))
			{
				isExpanded = !isExpanded;
				//Debug.Log(isExpanded);
				
				Event.current.Use();
			}
			
			break;
			
		}
		case EventType.keyDown:
		{
			// Use up the key presses so they can't mess with the label
			if (Event.current.keyCode != KeyCode.Tab)
			{
				Event.current.Use();
			}

			break;
		}
		case EventType.ExecuteCommand:
		{
			// Use the paste and cut so they can't mess with the label
			if (Event.current.commandName == "Paste" || Event.current.commandName == "Cut")
			{
				Event.current.Use();
			}
			
			break;
		}
		case EventType.Repaint:
		{
			foldoutPaintStyle.Draw(foldoutRectPos, foldoutContent, controlID, isExpanded);

			break;
		}
		}


		// Call the EditorGUI.DoTextField
		// Which handles all of the fancy selecting and stuff
		if(recycledTextEditor != null)
		{
			if(EditorGUI_DoTextField_MethodInfo != null)
			{
				Rect labelRectPos = GetHorizPosFromLastPos(new GUIStructure{rect=foldoutRectPos, content=content}, normalLabelStyle);

				object[] args = new object[] { recycledTextEditor, controlID, labelRectPos, content.text, normalLabelStyle, null, false, false, true, false };
				// DoTextField(EditorGUI.RecycledTextEditor editor, int id, Rect position, string text, GUIStyle style, string allowedletters, out bool changed, bool reset, bool multiline, bool passwordField)
				EditorGUI_DoTextField_MethodInfo.Invoke(null, args);
			}
		}


		return isExpanded;
	}

	// Alternative construcotr if you don't want to deal with GUIContent
	public static bool SelectableFoldout(Rect position, bool isExpanded, string text, [DefaultValue ("EditorStyles.label")] GUIStyle labelStyle, [DefaultValue ("EditorStyles.foldout")] GUIStyle foldoutStyle)
	{
		return SelectableFoldout(position, isExpanded, new GUIContent(text), labelStyle, foldoutStyle);
	}

	static Rect GetHorizPosFromLastPos(GUIStructure structure, GUIStyle style)
	{
		Vector2 newContentSize = style.CalcSize(structure.content);
		
		return new Rect(structure.rect.x + structure.rect.width, structure.rect.y, newContentSize.x, newContentSize.y);
	}

	static string SubStringStartToEnd(string msg, int startIndex, int endIndex)
	{
		return msg.Substring(startIndex, endIndex-startIndex);
	}
	static GUIContent SubStringStartToEnd(GUIContent content, int startIndex, int endIndex)
	{
		return new GUIContent(SubStringStartToEnd(content.text, startIndex, endIndex));
	}


}

Code without reflection:

Also you can see the code I had before I switched over to using reflection to gain access to DoTextField in this GitHub gist. It is not complete but I think someone may find it very useful for their own GUI elements.