Display Localized String Editor in a custom Editor Window

Good day

I am busy making a dialogue node tree system, using Localized Strings as part. I have created a DialogueNode class to represent the nodes in the tree.

namespace Dialogue
{
    [Serializable]
    public class DialogueNode
    {
        public TableEntryReference stringId;
        public LocalizedString dialogueString;
        [SerializeReference]
        public List<LocalizedString> childStrings = new List<LocalizedString>();
    }
}

After which, I created a scriptable object to create the different node trees.

namespace Dialogue
{
    [CreateAssetMenu(fileName = "Dialogue", menuName = "New Revalo Dialogue", order = 0)]
    public class Dialogue : ScriptableObject
    {
        [SerializeField]
        private List<DialogueNode> _Nodes = new List<DialogueNode>();


#if UNITY_EDITOR
        private void Awake()
        {
            if(_Nodes.Count == 0)
            {
                _Nodes.Add(new DialogueNode());
            }
        }
#endif

        public IEnumerable<DialogueNode> GetAllNodes()
        {
            return _Nodes;
        }

        void OnValidate()
        {
            foreach(DialogueNode node in _Nodes)
            {

                if (node.dialogueString != null)
                {
                    node.stringId = node.dialogueString.TableEntryReference;
                }
            }
        }
    }
}

I am then displaying in a custom editor window to edit and change the nodes. I have created the ability to change the content of the main localized string in the node through the editor; however, for the child strings/nodes, that will be the following options in the dialogue. I would just like to display the localized string editor as seen in the inspector. I have attempted to do so via a property field, but I am not successful, and all the topics that I have found are for older versions, where I am unsure if the solutions provided are still the same, as I have not had success in implementing them.

namespace Dialogue.Editor
{
    public class DialogueEditor : EditorWindow
    {
        Dialogue selectedDialogue = null;
        StringTable tableRef;

        LocalizedString childString;

        [MenuItem("Window/Dialogue Editor")]
        public static void ShowEditorWindow()
        {
            GetWindow(typeof(DialogueEditor), false, "Dialogue Editor");
        }

        [OnOpenAssetAttribute(1)]
        public static bool OnOpenAsset(int instanceID, int line)
        {
            Dialogue dialogue = EditorUtility.InstanceIDToObject(instanceID) as Dialogue;
            if (dialogue != null)
            {
                ShowEditorWindow();
                return true;
            }
            return false;
        }

        private void OnEnable()
        {
            Selection.selectionChanged += OnSelectionChange;
        }

        private void OnDisable()
        {
            Selection.selectionChanged -= OnSelectionChange;
        }

        private void OnSelectionChange()
        {
            Dialogue dialogue = Selection.activeObject as Dialogue;
            if(dialogue != null)
            {
                selectedDialogue = dialogue;
                tableRef = LocalizationSettings.StringDatabase.GetTable("LocalStrings", LocalizationSettings.SelectedLocale);
                //EditorUtility.SetDirty(tableRef);
                //EditorUtility.SetDirty(tableRef.SharedData);
                Repaint();
            }
        }

        private void OnGUI()
        {
            if(selectedDialogue == null)
            {
                EditorGUILayout.LabelField("No Dialogue Selected");
            }
            else
            {
                foreach(DialogueNode node in selectedDialogue.GetAllNodes())
                {
                    EditorGUI.BeginChangeCheck();

                    EditorGUILayout.LabelField("Node:");
                    string NewDialogue = EditorGUILayout.TextField(node.dialogueString.GetLocalizedString());

                    for(int i = 0; i <= node.childStrings.Count -1; i++)
                    {
                        childString = node.childStrings[i];

                        EditorGUILayout.PropertyField(new SerializedObject(this).FindProperty("childString"));
                    }

                    if(NewDialogue != null && EditorGUI.EndChangeCheck())
                    {
                        Undo.RecordObject(tableRef, "Table String Update");
                        tableRef.AddEntry(node.stringId.KeyId, NewDialogue);
                        //AssetDatabase.SaveAssetIfDirty(tableRef);
                        //AssetDatabase.SaveAssetIfDirty(tableRef.SharedData);
                    }
                }
            }
        }
    }
}

I would expect those to have their own custom PropertyDrawer. It could also be a custom Editor class though. Since localization is a package, you could skim through their editor source code to try and find how they draw the localized strings.

Since you are using IMGUI you should consider switching over to UI Toolkit. For one, Unity no longer recommends using IMGUI for editor tooling and perhaps it is not displaying correctly because their custom drawer may be UI Toolkit (VisualElement) based. In any case UITK is far easier to build such UI with since you can do it entirely visually and interactively.

Thank you for the response.

I was not aware that it is now fully UI toolkit as all the forum responses and posts that I found when searching for solutions all stated that it is using IMGUI. That was why I was wondering if it was still relevant.

I will have to look into the toolkit options then, as last post I saw stated that it was not yet in the UI Toolkit at the time.

I will look at the source code when I am back at my set-up, that might help out as well, thank you.

I have moved away from drawing the Localized string Editor in my editor, and decided to use text fields to edit the key values to link the nodes and child nodes with the same Localised strings. And I am now working on a system where it will get the Key value from the String table if there is a string, and if I change the Node Key, it will go through a dictionary and find the relevant Localised string to add to the Node.

I commented out the code that is currently causing the error, where it is not pulling the key value from a node that has a string selected, but nothing in the Node Key Field.

namespace Dialogue
{
    [Serializable]
    [CreateAssetMenu(fileName = "Dialogue", menuName = "New Revalo Dialogue", order = 0)]
    public class Dialogue : ScriptableObject
    {
        [SerializeField]
        private List<DialogueNode> _Nodes = new List<DialogueNode>();

        private StringTable _TableRef;

        Dictionary<string, DialogueNode> _NodeLookup = new Dictionary<string, DialogueNode>();
        Dictionary<string, LocalizedString> _TableLookup = new Dictionary<string, LocalizedString>();

#if UNITY_EDITOR
        private void Awake()
        {
            OnValidate();

            if(_Nodes.Count == 0)
            {
                _Nodes.Add(new DialogueNode());
            }

            _TableRef = LocalizationSettings.StringDatabase.GetTable("LocalStrings", LocalizationSettings.SelectedLocale);
        }
#endif

        public IEnumerable<DialogueNode> GetAllNodes()
        {
            return _Nodes;
        }

        public DialogueNode GetRootNode()
        {
            return _Nodes[0];
        }

        void OnValidate()
        {
            _TableRef = LocalizationSettings.StringDatabase.GetTable("LocalStrings", LocalizationSettings.SelectedLocale);

            _NodeLookup.Clear();
            foreach(DialogueNode node in GetAllNodes())
            {
                _NodeLookup[node.nodeID] = node;

                if (node.dialogueString != null)
                {
                    node.stringId = node.dialogueString.TableEntryReference;
                    //node.nodeKey = _TableLookup[node.stringId].TableEntryReference.Key;
                    Debug.Log(node.stringId.ToString() + "Table Key Ref");
                }
            }

            _TableLookup.Clear();
            foreach (var v in _TableRef.Values)
            {
                _TableLookup[v.Key] = new LocalizedString(_TableRef.TableCollectionName, v.Key);
                Debug.Log(_TableLookup[v.Key]);
            }
        }

        public void UpdateNodeKey(DialogueNode node)
        {
            if(_TableLookup.ContainsKey(node.nodeKey))
            {
                node.stringId = _TableLookup[node.nodeKey].TableEntryReference;
                node.dialogueString = _TableLookup[node.nodeKey];
            }
        }

        public void UpdateChildString(DialogueNode parentNode)
        {
            foreach (ChildNode childNode in parentNode.childNodes)
            {
                if (childNode.nodeID != null)
                {
                    if(_NodeLookup.ContainsKey(childNode.nodeID))
                    {
                        childNode.childStringID = _NodeLookup[childNode.nodeID].stringId;
                        childNode.childString = _NodeLookup[childNode.nodeID].dialogueString;
                    }
                    else if (!_NodeLookup.ContainsKey(childNode.nodeID))
                    {
                        childNode.childStringID = null;
                        childNode.childString = null;
                    }
                }
            }
        }
    }
}

Below is the Scriptable Dialogue node with the updated variables.

namespace Dialogue
{
    [Serializable]
    public class DialogueNode
    {
        public string nodeID;
        public string nodeKey;
        public TableEntryReference stringId;
        public LocalizedString dialogueString;
        public List<ChildNode> childNodes = new List<ChildNode>();
        public Rect rect = new Rect(0, 0, 400, 250);
    }

    [Serializable]
    public class ChildNode
    {
        public string nodeID;
        public string childKeyName;
        public TableEntryReference childStringID;
        public LocalizedString childString;
    }
}

As well as the Dialogue Editor Script, it currently draws all that is needed to and I have not noticed a problem, however, I am not sure if there will be a problem once the NodeKey field pulls through.

namespace Dialogue.Editor
{
    [CanEditMultipleObjects]
    public class DialogueEditor : EditorWindow
    {
        private Dialogue _SelectedDialogue = null;
        private StringTable _TableRef;
        private GUIStyle _NodeStyle;

        private DialogueNode _DraggingNode = null;
        private Vector2 _DraggingOffset;

        [MenuItem("Window/Dialogue Editor")]
        public static void ShowEditorWindow()
        {
            GetWindow(typeof(DialogueEditor), false, "Dialogue Editor");
        }

        [OnOpenAssetAttribute(1)]
        public static bool OnOpenAsset(int instanceID, int line)
        {
            Dialogue dialogue = EditorUtility.InstanceIDToObject(instanceID) as Dialogue;
            if (dialogue != null)
            {
                ShowEditorWindow();
                return true;
            }
            return false;
        }

        private void OnEnable()
        {
            Selection.selectionChanged += OnSelectionChange;

            _NodeStyle = new GUIStyle();
            _NodeStyle.normal.background = EditorGUIUtility.Load("Node0") as Texture2D;
            _NodeStyle.padding = new RectOffset(10, 10, 10, 10);
            _NodeStyle.border = new RectOffset(12, 12, 12, 12);
        }

        private void OnDisable()
        {
            Selection.selectionChanged -= OnSelectionChange;
        }

        private void OnSelectionChange()
        {
            Dialogue dialogue = Selection.activeObject as Dialogue;
            if(dialogue != null)
            {
                _SelectedDialogue = dialogue;
                _TableRef = LocalizationSettings.StringDatabase.GetTable("LocalStrings", LocalizationSettings.SelectedLocale);

                Repaint();
            }
        }

        private void OnGUI()
        {
            if(_SelectedDialogue == null)
            {
                EditorGUILayout.LabelField("No Dialogue Selected");
            }
            else
            {
                ProcessEvents();
                foreach(DialogueNode node in _SelectedDialogue.GetAllNodes())
                {
                    OnGuiNode(node);
                }
            }
        }

        private void ProcessEvents()
        {
            if(Event.current.type == EventType.MouseDown && _DraggingNode == null)
            {
                _DraggingNode = GetNodeAtPoint(Event.current.mousePosition);
                if(_DraggingNode != null)
                {
                    _DraggingOffset = _DraggingNode.rect.position - Event.current.mousePosition;
                }
            }
            else if(Event.current.type == EventType.MouseDrag && _DraggingNode != null)
            {
                Undo.RecordObject(_SelectedDialogue, "Move Dialogue Node");
                _DraggingNode.rect.position = Event.current.mousePosition + _DraggingOffset;
                GUI.changed = true;
            }
            else if(Event.current.type == EventType.MouseUp && _DraggingNode != null)
            {
                _DraggingNode = null;
            }
        }

        private DialogueNode GetNodeAtPoint(Vector2 mousePosition)
        {
            DialogueNode foundNode = null;
            foreach(DialogueNode node in _SelectedDialogue.GetAllNodes())
            {
                if (node.rect.Contains(mousePosition))
                {
                    foundNode = node;
                }
            }
            return foundNode;
        }

        private void OnGuiNode(DialogueNode node)
        {
            GUILayout.BeginArea(node.rect, _NodeStyle);

            EditorGUI.BeginChangeCheck();

            EditorGUILayout.LabelField("Node:", EditorStyles.whiteLabel);
            EditorGUI.indentLevel++;
            EditorGUILayout.LabelField("NodeID:", EditorStyles.whiteLabel);

            string NewDialogueID = EditorGUILayout.TextField(node.nodeID);

            if (NewDialogueID != null && EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(_SelectedDialogue, "Node ID String Update");
                node.nodeID = NewDialogueID;
            }

            EditorGUILayout.LabelField("Node Key:", EditorStyles.whiteLabel);

            EditorGUI.BeginChangeCheck();

            string NewNodeKey = EditorGUILayout.TextField(node.nodeKey);

            if (NewNodeKey != null && EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(_TableRef, "Node Key Update");
                _SelectedDialogue.UpdateNodeKey(node);
            }

            EditorGUILayout.LabelField("Node String:", EditorStyles.whiteLabel);

            EditorGUI.BeginChangeCheck();

            string NewDialogue = EditorGUILayout.TextField(node.dialogueString.GetLocalizedString());

            if (NewDialogue != null && EditorGUI.EndChangeCheck())
            {
                Undo.RecordObject(_TableRef, "Table String Update");
                _TableRef.AddEntry(node.stringId.KeyId, NewDialogue);
            }

            EditorGUI.indentLevel++;
            EditorGUILayout.LabelField("Child Nodes:", EditorStyles.whiteLabel);

            for (int i = 0; i <= node.childNodes.Count - 1; i++)
            {
                EditorGUILayout.LabelField("Childe Node ID:", EditorStyles.whiteLabel);
                EditorGUI.indentLevel++;

                string ChildDialogueID = EditorGUILayout.TextField(node.childNodes[i].nodeID);

                if(ChildDialogueID != null && EditorGUI.EndChangeCheck())
                {
                    Undo.RecordObject(_SelectedDialogue, "Child ID String Update");
                    node.childNodes[i].nodeID = ChildDialogueID;

                    _SelectedDialogue.UpdateChildString(node);
                }

                node.childNodes[i].childKeyName = _TableRef.SharedData.GetKey(node.childNodes[i].childStringID.KeyId);

                EditorGUILayout.LabelField("Childe Node String Key:", EditorStyles.whiteLabel);
                if(node.childNodes[i].childKeyName != null) EditorGUILayout.LabelField(node.childNodes[i].childKeyName);

                EditorGUILayout.LabelField("Childe Node String:", EditorStyles.whiteLabel);
                if(node.childNodes[i].childString != null) EditorGUILayout.LabelField(node.childNodes[i].childString.GetLocalizedString());

                EditorGUI.indentLevel--;
            }

            EditorGUI.indentLevel = 0;

            GUILayout.EndArea();
        }
    }
}

I have gotten the Node Key field to display the Key Value from the Localization table for the selected string if I edit the scriptable object via the inspector, however, I would like to be able to edit an empty node by typing/selecting the value for Node Key and have it select a Localized string directly and add it to the Node Dialog String value?

However, when there is no Localized string selected, I constantly get an error on how the values cannot be null, because my Begin/End calls do not match and that table reference must contain GUID or Table Collection Name, even when it should not be running anything due to the fields and values being empty/null.