Search code examples
unity-game-engineunity-editorunity3d-editor

Saving string variables in a custom EditorWindow while formatted like a TextArea


Basically, I want to figure out how I can:

  1. Save string (or any) variables in a custom editor window (inheriting from EditorWindow) when they are changed in that window.
  2. Display strings in a format like a TextArea while still allowing saving changes as mentioned above.
  3. Display strings from a string array by index, rather than individually defined strings (I've had trouble with this before)
  4. If you know how to do the above in a custom inspector too (inheriting from Editor, not EditorWindow), that'd be great too.

I've run into this issue a few times with different classes inheriting from Editor, and previously solved by using a PropertyField rather than a TextArea/TextField, but that gets rid of the TextArea-style formatting that I want. Also, classes inheriting from EditorWindow don't seem to allow it in the same way (t = (script type)target; doesn't work, and PropertyField needs it)..?

I'm pretty new to custom inspectors and this stuff, so code examples would be super helpful if possible.

Thanks!


Solution

  • Before starting a general note because you mentioned it in your question:

    Whenever possible I strongly recommend to avoid using target at all! In particular do not set any fields directly. this makes things like marking your scene direty and thus saving changes and also Undo/Redo functionality pretty complicated as you will have to implement it by yourself!

    Rather always go through SerializedProperty combined with SerializedObject.Update and SerializedObject.ApplyModifiedProperties (examples will be below). This handles all this stuff like marking dirty and thus saving the scene changes and Undo/Redo automatically for you!


    Then to the TextArea.

    Lets say you have a class like

    public class Example : MonoBehaviour
    {
        [SerializeField] private string _exampleString;
        public string AnotherExampleString;
    }
    

    Basically there are three main options. I will do the Editor (custom Inspector) script first since there you are a bit more flexible.

    The EditorWindow will be below.


    Editor Attribute [TextArea]

    Actually you wouldn't even need an Editor script at all! Simply tag the according field(s) as [TextArea] like this:

    public class Example : MonoBehaviour
    {
        [SerializeField] [TextArea] private string _exampleString;
        // You can also directly configure the min and max line count here as well
        // By default it is 3 lines
        [TextAre(3,7)] public string AnotherExampleString;
    }
    

    This already looks like this

    enter image description here


    EditorGUILayout.PropertyField

    Then if you still need the Editor script the good thing about a EditorGUILayout.PropertyField is it automatically uses the correct drawer for the according type ... and it also applies all editor attributes! Isn't this great?

    So simply having and Editor like

    [CustomEditor(typeof(Example))]
    public class ExampleEditor : Editor
    {
        private SerializedProperty _exampleString;
        private SerializedProperty AnotherExampleString;
    
        private void OnEnable()
        {
            // Link in the serialized properties to their according fields
            _exampleString = serializedObject.FindProperty("_exampleString");
            AnotherExampleString = serializedObject.FindProperty("AnotherExampleString");
        }
    
        public override void OnInspectorGUI()
        {
            DrawScriptField();
    
            // load the real target values into the serialized properties
            serializedObject.Update();
    
            EditorGUILayout.PropertyField(_exampleString);
            EditorGUILayout.PropertyField(AnotherExampleString);
    
            // write back the changed properties into the real target
            serializedObject.ApplyModifiedProperties();
        }
    
        // Little bonus from my side so you have the script field on top
        private void DrawScriptField()
        {
            EditorGUI.BeginDisabledGroup(true);
            EditorGUILayout.ObjectField("Script", MonoScript.FromMonoBehaviour((Example)target), typeof(Example), false);
            EditorGUILayout.Space();
            EditorGUI.EndDisabledGroup();
        }
    }
    

    The result looks basically exactly the same:

    enter image description here

    EditorGUILayout.TextField

    Using a EditorGUILayout.TextArea you can display any string as a multi-line text area. This also applies to an EditorWindow.

    Lets say again we didn't tag our string fields

    public class Example : MonoBehaviour
    {
        [SerializeField] private string _exampleString;
        public string AnotherExampleString;
    }
    

    But we can make them appear just as before using this Editor script:

    [CustomEditor(typeof(Example))]
    public class ExampleEditor : Editor
    {
        private SerializedProperty _exampleString;
        private SerializedProperty AnotherExampleString;
    
        private Vector2 scroll1;
        private Vector2 scroll2;
    
        private void OnEnable()
        {
            // Link in the serialized properties to their according fields
            _exampleString = serializedObject.FindProperty("_exampleString");
            AnotherExampleString = serializedObject.FindProperty("AnotherExampleString");
        }
    
        public override void OnInspectorGUI()
        {
            DrawScriptField();
    
            // load the real target values into the serialized properties
            serializedObject.Update();
    
            EditorGUILayout.PrefixLabel(_exampleString.displayName);
            scroll1 = EditorGUILayout.BeginScrollView(scroll1,GUILayout.MaxHeight(3 * EditorGUIUtility.singleLineHeight));
            _exampleString.stringValue = EditorGUILayout.TextArea(_exampleString.stringValue, EditorStyles.textArea);
            EditorGUILayout.EndScrollView();
    
            EditorGUILayout.PrefixLabel(AnotherExampleString.displayName);
            scroll2 = EditorGUILayout.BeginScrollView(scroll2, GUILayout.MaxHeight(7 * EditorGUIUtility.singleLineHeight));
            AnotherExampleString.stringValue = EditorGUILayout.TextArea(AnotherExampleString.stringValue);
            EditorGUILayout.EndScrollView();
    
            // write back the changed properties into the real target
            serializedObject.ApplyModifiedProperties();
        }
    
        // Little bonus from my side so you have the script field on top
        private void DrawScriptField()
        {
            EditorGUI.BeginDisabledGroup(true);
            EditorGUILayout.ObjectField("Script", MonoScript.FromMonoBehaviour((Example)target), typeof(Example), false);
            EditorGUILayout.Space();
            EditorGUI.EndDisabledGroup();
        }
    }
    

    Though you can see we already had to fake it a bit using the additional EditorGUILayout.BeginScrollView


    This same thing you can also do in an EditorWindow. Most of the times it makes not much sense to go through SerializedProperty for EditorWindow

    public class ExampleWindow : EditorWindow
    {
        private string exampleString;
        private Vector2 scroll;
    
        [MenuItem("Example/Show ExampleWindow")]
        private static void Initialize()
        {
            var window = GetWindow<ExampleWindow>();
            window.Show();
        }
    
        private void OnGUI()
        {
            EditorGUILayout.PrefixLabel("Example String");
            scroll = EditorGUILayout.BeginScrollView(scroll,GUILayout.MaxHeight(3 * EditorGUIUtility.singleLineHeight));
            exampleString = EditorGUILayout.TextArea(exampleString, EditorStyles.textArea);
            EditorGUILayout.EndScrollView();
        }
    }
    

    which results in

    enter image description here