Reputation: 11325
This is how I declared the List :
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using UnityEditor;
using UnityEngine;
using UnityEditorInternal;
public class ConversationTrigger : MonoBehaviour
{
public List<Conversation> conversations = new List<Conversation>();
This is the editor script using this List :
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEditorInternal;
using UnityEngine;
[CustomEditor(typeof(ConversationTrigger))]
public class ConversationTriggerEditor : Editor
{
private ReorderableList ConversationsList;
private void OnEnable()
{
ConversationTrigger conversationtrigger = (ConversationTrigger)target;
ConversationsList = new ReorderableList(serializedObject, serializedObject.FindProperty("conversations"), true, true, true, true);
}
public override void OnInspectorGUI()
{
serializedObject.Update();
ConversationsList.DoLayoutList();
serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(target);
}
}
This is a screenshot of the Inspector before using the ReorderableList. I could click on a Conversation item and collapse it and then on each child to collapse it again and so on :
And this screenshot is when using the ReorderableList :
Now I can only drag the items and change places and when clicking on + it will add a duplication of the last item.
What I want to do for now I can think of at least 3 things :
When selecting existing item for example Opening and then if clicking on the + add a new empty Conversation item under the Opening. If I selected the Magic add empty new one to the end and if selected the one in the middle Locked Room add empty item after it. The idea is to add empty item after the selected item.
When removing item/s same the selected one/s
How can I make that when clicking double click on one of the items or making a arrow that will collapse the items and show all the childs like in my screenshot in my question like Opening and others ?
Upvotes: 4
Views: 6534
Reputation: 90724
It took me quite a while but I love EditorScripting :D
Assuming your data structure looks like this
public class ConversationTrigger : MonoBehaviour
{
public List<Conversation> conversations;
public void SaveConversations() { }
public void LoadConversations() { }
}
[Serializable]
public class Conversation
{
public string Name;
public bool Foldout;
public List<Dialogue> Dialogues;
}
[Serializable]
public class Dialogue
{
public string Name;
public bool Foldout;
public List<string> Sentences;
}
I came up with the following EditorScript. The idea is based on a former question of mine on How to select elements in nested reordeablelist.
It got quite compley as usual for EditorScript. I tried to comment a lot but if anything is unclear just ask me in the comments.
It is a huge pity that the ReorderableList
is not a documented features since it is so extremly powerful and useful ...
There are multiple things you have to override:
drawHeaderCallback
drawElementCallback
elementHeightCallback
onAddCallback
and in order to be able to ineract with them nested store the different ReorderableList
s in dictionaries:
[CustomEditor(typeof(ConversationTrigger))]
public class ConversationTriggerEditor : Editor
{
private ConversationTrigger _conversationTrigger;
[SerializeField] private ReorderableList conversationsList;
private SerializedProperty _conversations;
private int _currentlySelectedConversationIndex = -1;
private readonly Dictionary<string, ReorderableList> _dialoguesListDict = new Dictionary<string, ReorderableList>();
private readonly Dictionary<string, ReorderableList> _sentencesListDict = new Dictionary<string, ReorderableList>();
private void OnEnable()
{
_conversationTrigger = (ConversationTrigger)target;
_conversations = serializedObject.FindProperty("conversations");
conversationsList = new ReorderableList(serializedObject, _conversations)
{
displayAdd = true,
displayRemove = true,
draggable = true,
drawHeaderCallback = DrawConversationsHeader,
drawElementCallback = DrawConversationsElement,
onAddCallback = (list) =>
{
SerializedProperty addedElement;
// if something is selected add after that element otherwise on the end
if (_currentlySelectedConversationIndex >= 0)
{
list.serializedProperty.InsertArrayElementAtIndex(_currentlySelectedConversationIndex + 1);
addedElement = list.serializedProperty.GetArrayElementAtIndex(_currentlySelectedConversationIndex + 1);
}
else
{
list.serializedProperty.arraySize++;
addedElement = list.serializedProperty.GetArrayElementAtIndex(list.serializedProperty.arraySize - 1);
}
var name = addedElement.FindPropertyRelative("Name");
var foldout = addedElement.FindPropertyRelative("Foldout");
var dialogues = addedElement.FindPropertyRelative("Dialogues");
name.stringValue = "";
foldout.boolValue = true;
dialogues.arraySize = 0;
},
elementHeightCallback = (index) =>
{
return GetConversationHeight(_conversations.GetArrayElementAtIndex(index));
}
};
}
public override void OnInspectorGUI()
{
serializedObject.Update();
// if there are no elements reset _currentlySelectedConversationIndex
if (conversationsList.serializedProperty.arraySize - 1 < _currentlySelectedConversationIndex) _currentlySelectedConversationIndex = -1;
conversationsList.DoLayoutList();
if (GUILayout.Button("Save Conversations"))
{
_conversationTrigger.SaveConversations();
}
if (GUILayout.Button("Load Conversations"))
{
Undo.RecordObject(_conversationTrigger, "Loaded conversations from JSON");
_conversationTrigger.LoadConversations();
}
serializedObject.ApplyModifiedProperties();
}
#region Drawers
#region List Headers
private void DrawConversationsHeader(Rect rect)
{
EditorGUI.LabelField(rect, "Conversations");
}
private void DrawDialoguesHeader(Rect rect)
{
EditorGUI.LabelField(rect, "Dialogues");
}
private void DrawSentencesHeader(Rect rect)
{
EditorGUI.LabelField(rect, "Sentences");
}
#endregion List Headers
#region Elements
private void DrawConversationsElement(Rect rect, int index, bool isActive, bool isFocused)
{
if (isActive) _currentlySelectedConversationIndex = index;
var conversation = _conversations.GetArrayElementAtIndex(index);
var position = new Rect(rect);
var name = conversation.FindPropertyRelative("Name");
var foldout = conversation.FindPropertyRelative("Foldout");
var dialogues = conversation.FindPropertyRelative("Dialogues");
string dialoguesListKey = conversation.propertyPath;
EditorGUI.indentLevel++;
{
// make the label be a foldout
foldout.boolValue = EditorGUI.Foldout(new Rect(position.x, position.y, 10, EditorGUIUtility.singleLineHeight), foldout.boolValue, foldout.boolValue ? "" : name.stringValue);
if (foldout.boolValue)
{
// draw the name field
name.stringValue = EditorGUI.TextField(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight), name.stringValue);
position.y += EditorGUIUtility.singleLineHeight;
if (!_dialoguesListDict.ContainsKey(dialoguesListKey))
{
// create reorderabl list and store it in dict
var dialoguesList = new ReorderableList(conversation.serializedObject, dialogues)
{
displayAdd = true,
displayRemove = true,
draggable = true,
drawHeaderCallback = DrawDialoguesHeader,
drawElementCallback = (convRect, convIndex, convActive, convFocused) => { DrawDialoguesElement(_dialoguesListDict[dialoguesListKey], convRect, convIndex, convActive, convFocused); },
elementHeightCallback = (dialogIndex) =>
{
return GetDialogueHeight(_dialoguesListDict[dialoguesListKey].serializedProperty.GetArrayElementAtIndex(dialogIndex));
},
onAddCallback = (list) =>
{
list.serializedProperty.arraySize++;
var addedElement = list.serializedProperty.GetArrayElementAtIndex(list.serializedProperty.arraySize - 1);
var newDialoguesName = addedElement.FindPropertyRelative("Name");
var newDialoguesFoldout = addedElement.FindPropertyRelative("Foldout");
var sentences = addedElement.FindPropertyRelative("Sentences");
newDialoguesName.stringValue = "";
newDialoguesFoldout.boolValue = true;
sentences.arraySize = 0;
}
};
_dialoguesListDict[dialoguesListKey] = dialoguesList;
}
_dialoguesListDict[dialoguesListKey].DoList(new Rect(position.x, position.y, position.width, position.height - EditorGUIUtility.singleLineHeight));
}
}
EditorGUI.indentLevel--;
}
private void DrawDialoguesElement(ReorderableList list, Rect rect, int index, bool isActive, bool isFocused)
{
if (list == null) return;
var dialog = list.serializedProperty.GetArrayElementAtIndex(index);
var position = new Rect(rect);
var foldout = dialog.FindPropertyRelative("Foldout");
var name = dialog.FindPropertyRelative("Name");
{
// make the label be a foldout
foldout.boolValue = EditorGUI.Foldout(new Rect(position.x, position.y, 10, EditorGUIUtility.singleLineHeight), foldout.boolValue, foldout.boolValue ? "" : name.stringValue);
var sentencesListKey = dialog.propertyPath;
var sentences = dialog.FindPropertyRelative("Sentences");
if (foldout.boolValue)
{
// draw the name field
name.stringValue = EditorGUI.TextField(new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight), name.stringValue);
position.y += EditorGUIUtility.singleLineHeight;
if (!_sentencesListDict.ContainsKey(sentencesListKey))
{
// create reorderabl list and store it in dict
var sentencesList = new ReorderableList(sentences.serializedObject, sentences)
{
displayAdd = true,
displayRemove = true,
draggable = true,
// header for the dialog list
drawHeaderCallback = DrawSentencesHeader,
// how a sentence is displayed
drawElementCallback = (sentenceRect, sentenceIndex, sentenceIsActive, sentenceIsFocused) =>
{
var sentence = sentences.GetArrayElementAtIndex(sentenceIndex);
// draw simple textArea for sentence
sentence.stringValue = EditorGUI.TextArea(sentenceRect, sentence.stringValue);
},
// Sentences have simply a fixed height of 2 lines
elementHeight = EditorGUIUtility.singleLineHeight * 2,
// when a sentence is added
onAddCallback = (sentList) =>
{
sentList.serializedProperty.arraySize++;
var addedElement = sentList.serializedProperty.GetArrayElementAtIndex(sentList.serializedProperty.arraySize - 1);
addedElement.stringValue = "";
}
};
// store the created ReorderableList
_sentencesListDict[sentencesListKey] = sentencesList;
}
// Draw the list
_sentencesListDict[sentencesListKey].DoList(new Rect(position.x, position.y, position.width, position.height - EditorGUIUtility.singleLineHeight));
}
}
}
#endregion Elements
#endregion Drawers
#region Helpers
#region HeightGetter
/// <summary>
/// Returns the height of given Conversation property
/// </summary>
/// <param name="conversation"></param>
/// <returns>height of given Conversation property</returns>
private float GetConversationHeight(SerializedProperty conversation)
{
var foldout = conversation.FindPropertyRelative("Foldout");
// if not foldout the height is simply 1 line
var height = EditorGUIUtility.singleLineHeight;
// otherwise we sum up every controls and child heights
if (foldout.boolValue)
{
// we need some more lines:
// for the Name field,
// the list header,
// the list buttons and a bit buffer
height += EditorGUIUtility.singleLineHeight * 5;
var dialogues = conversation.FindPropertyRelative("Dialogues");
for (var d = 0; d < dialogues.arraySize; d++)
{
var dialog = dialogues.GetArrayElementAtIndex(d);
height += GetDialogueHeight(dialog);
}
}
return height;
}
/// <summary>
/// Returns the height of given Dialogue property
/// </summary>
/// <param name="dialog"></param>
/// <returns>height of given Dialogue property</returns>
private float GetDialogueHeight(SerializedProperty dialog)
{
var foldout = dialog.FindPropertyRelative("Foldout");
// same game for the dialog if not foldout it is only a single line
var height = EditorGUIUtility.singleLineHeight;
// otherwise sum up controls and child heights
if (foldout.boolValue)
{
// we need some more lines:
// for the Name field,
// the list header,
// the list buttons and a bit buffer
height += EditorGUIUtility.singleLineHeight * 4;
var sentences = dialog.FindPropertyRelative("Sentences");
// the sentences are easier since they always have the same height
// in this example 2 lines so simply do
// at least have space for 1 sentences even if there is none
height += EditorGUIUtility.singleLineHeight * Mathf.Max(1, sentences.arraySize) * 2;
}
return height;
}
#endregion
#endregion Helpers
}
Adding new Conversations, give them names, fold them. If a conversation is selected add the new conversation after it
Adding dialogues and sentences and still being able to rearrange and (un)fold everything
Upvotes: 5