Search code examples
arraysunity-game-engineunity-editor

Unsubscribe from SceneView draw calls when Edited Unity PropertyDrawer Array element got deleted


I'm making an editor in PropertyDrawer using SceneView.duringSceneGui. So it involves subscribing to SceneView.duringSceneGui when a property needs to draw stuff in SceneView and unsubscribing when it's gone. However I have no idea how to know if edited array element was removed from an array. It still exists in the memory and SceneView.duringSceneGui subscribed method is still there. I need to know when to stop editing and unsubscribe from it.

I guess I need to implement some context object, to store property value, edited object, PropertyDrawer and that subscription method should be there, to be able to unsubscribe exactly that editor... Although there may be only one editor running at once.

Does anybody found that out? Couldn't find anything with PropertyDrawers and array elements being deleted or removed.

TL.DR. Does Unity has an event to tell that PropertyDrawer's array element was removed or is there a simple or neat way to figure this out?


Solution

  • So, while making an example, I've solved my problem by getting property value every SceneView draw call and on catching an exception or if that value isn't edited stopped editor. I've added [NonSerialized] to _IsInEditMode to fix a new issue that I caught at the last moment, so that one is crucial.

    Not sure if it's the best way to do that. If anybody will ever need to make a SceneView editor for some class, here's the example that works on arrays and lists also. Just separate it into 3 files and put them in respective folders, like Editor/ for MyClassDrawer.

    using InspectorSerializedUtility;
    using System;
    using System.Collections;
    using System.Collections.Generic;
    using System.Linq;
    using System.Reflection;
    using System.Text.RegularExpressions;
    using UnityEditor;
    using UnityEditor.SceneManagement;
    using UnityEngine;
    using UnityEngine.SceneManagement;
    
    public class MyScript : MonoBehaviour
    {
        public MyClass field;
        public List<MyClass> list = new List<MyClass>();
    }
    
    [Serializable]
    public class MyClass
    {
    #if UNITY_EDITOR
        [NonSerialized]
        public bool _IsInEditMode;
    #endif
        public Vector3 position;
    
        public void Reset()
        {
            position = Vector3.zero;
    #if UNITY_EDITOR
            _IsInEditMode = false;
    #endif
        }
    }
    
    [CustomPropertyDrawer(typeof(MyClass))]
    public class MyClassDrawer : PropertyDrawer
    {
        public MyClass value;
        public Transform targetTransform;
        private Tool internalTool;
        bool editorStarted {
            get => value?._IsInEditMode ?? false;
            set {
                if (this.value != null)
                    this.value._IsInEditMode = value;
            }
        }
        private SerializedProperty currentProperty;
        private SerializedProperty drawerProperty;
        private static MyClassDrawer currentlyEditedDrawer;
    
        string editorButtonText(bool isInEditMode) => isInEditMode ? "Stop Editing" : "Start Editing";
        Color editorButtonColor(bool isInEditMode) => isInEditMode ? Color.red + Color.white / 2f : Color.white;
    
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            //Debug.Log("OnGUI");
            drawerProperty = property;
            targetTransform = ((Component)property.serializedObject.targetObject).transform;
            var val = property.GetValue<MyClass>();
    
            GUI.color = editorButtonColor(val._IsInEditMode);
            var toggle = GUI.Toggle(position, val._IsInEditMode, editorButtonText(val._IsInEditMode), "Button");
            if (toggle != val._IsInEditMode)
            {
                if (toggle && currentlyEditedDrawer != null && currentlyEditedDrawer.editorStarted)
                    currentlyEditedDrawer.StopEditor();
                value = val;
                currentlyEditedDrawer = this;
                if (toggle)
                    StartEditor();
                else
                    StopEditor();
            }
    
            GUI.color = Color.white;
        }
    
        public void OnDrawScene(SceneView sv) => OnDrawScene();
        public void OnDrawScene()
        {
            //Debug.Log("OnDrawScene");
            MyClass value = null;
            try
            {
                value = currentProperty.GetValue<MyClass>();
                if (!value._IsInEditMode)
                {
                    StopEditor();
                    return;
                }
            } catch
            {
                StopEditor();
                return;
            }
            var m = Handles.matrix;
            Handles.matrix = targetTransform.localToWorldMatrix;
    
            if (Tools.current == Tool.Move)
            {
                internalTool = Tool.Move;
                Tools.current = Tool.None;
            }
    
            if (internalTool == Tool.Move)
            {
                var pos = Handles.PositionHandle(value.position, Quaternion.identity);
                if (value.position != pos)
                {
                    Undo.RecordObject(targetTransform, "position changed");
                    value.position = pos;
                }
            }
    
            Handles.matrix = m;
        }
    
        public void StartEditor()
        {
            currentProperty = drawerProperty;
            editorStarted = true;
            Debug.Log("StartEditor");
            Subscribe();
            CallAllSceneViewRepaint();
        }
    
        private void Subscribe()
        {
            Unsubscribe();
            SceneView.duringSceneGui += OnDrawScene;
            Selection.selectionChanged += StopEditor;
            EditorSceneManager.sceneClosed += StopEditor;
            AssemblyReloadEvents.beforeAssemblyReload += StopEditor;
        }
    
        public void StopEditor(Scene s) => StopEditor();
        public void StopEditor()
        {
            Tools.current = internalTool;
            editorStarted = false;
            Unsubscribe();
            currentProperty = null;
            CallAllSceneViewRepaint();
        }
    
        private void Unsubscribe()
        {
            SceneView.duringSceneGui -= OnDrawScene;
            Selection.selectionChanged -= StopEditor;
            EditorSceneManager.sceneClosed -= StopEditor;
            AssemblyReloadEvents.beforeAssemblyReload -= StopEditor;
        }
    
        private void CallAllSceneViewRepaint()
        {
            foreach (SceneView sv in SceneView.sceneViews)
                sv.Repaint();
        }
    
    }
    
    namespace InspectorSerializedUtility
    {
        /// <summary>
        /// https://gist.github.com/douduck08/6d3e323b538a741466de00c30aa4b61f
        /// </summary>
        public static class InspectorSeriallizedUtils
        {
    
            public static T GetValue<T>(this SerializedProperty property) where T : class
            {
                try
                {
                    if (property.serializedObject.targetObject == null) return null;
                }
                catch
                {
                    return null;
                }
                object obj = property.serializedObject.targetObject;
                string path = property.propertyPath.Replace(".Array.data", "");
                string[] fieldStructure = path.Split('.');
                Regex rgx = new Regex(@"\[\d+\]");
                for (int i = 0; i < fieldStructure.Length; i++)
                {
                    if (fieldStructure[i].Contains("["))
                    {
                        int index = System.Convert.ToInt32(new string(fieldStructure[i].Where(c => char.IsDigit(c)).ToArray()));
                        obj = GetFieldValueWithIndex(rgx.Replace(fieldStructure[i], ""), obj, index);
                    }
                    else
                    {
                        obj = GetFieldValue(fieldStructure[i], obj);
                    }
                }
                return (T)obj;
            }
    
            public static bool SetValue<T>(this SerializedProperty property, T value) where T : class
            {
                object obj = property.serializedObject.targetObject;
                string path = property.propertyPath.Replace(".Array.data", "");
                string[] fieldStructure = path.Split('.');
                Regex rgx = new Regex(@"\[\d+\]");
                for (int i = 0; i < fieldStructure.Length - 1; i++)
                {
                    if (fieldStructure[i].Contains("["))
                    {
                        int index = System.Convert.ToInt32(new string(fieldStructure[i].Where(c => char.IsDigit(c)).ToArray()));
                        obj = GetFieldValueWithIndex(rgx.Replace(fieldStructure[i], ""), obj, index);
                    }
                    else
                    {
                        obj = GetFieldValue(fieldStructure[i], obj);
                    }
                }
    
                string fieldName = fieldStructure.Last();
                if (fieldName.Contains("["))
                {
                    int index = System.Convert.ToInt32(new string(fieldName.Where(c => char.IsDigit(c)).ToArray()));
                    return SetFieldValueWithIndex(rgx.Replace(fieldName, ""), obj, index, value);
                }
                else
                {
                    Debug.Log(value);
                    return SetFieldValue(fieldName, obj, value);
                }
            }
    
            private static object GetFieldValue(string fieldName, object obj, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
            {
                FieldInfo field = obj.GetType().GetField(fieldName, bindings);
                if (field != null)
                {
                    return field.GetValue(obj);
                }
                return default(object);
            }
    
            private static object GetFieldValueWithIndex(string fieldName, object obj, int index, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
            {
                FieldInfo field = obj.GetType().GetField(fieldName, bindings);
                if (field != null)
                {
                    object list = field.GetValue(obj);
                    if (list.GetType().IsArray)
                    {
                        return ((object[])list)[index];
                    }
                    else if (list is IEnumerable)
                    {
                        return ((IList)list)[index];
                    }
                }
                return default(object);
            }
    
            public static bool SetFieldValue(string fieldName, object obj, object value, bool includeAllBases = false, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
            {
                FieldInfo field = obj.GetType().GetField(fieldName, bindings);
                if (field != null)
                {
                    field.SetValue(obj, value);
                    return true;
                }
                return false;
            }
    
            public static bool SetFieldValueWithIndex(string fieldName, object obj, int index, object value, bool includeAllBases = false, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
            {
                FieldInfo field = obj.GetType().GetField(fieldName, bindings);
                if (field != null)
                {
                    object list = field.GetValue(obj);
                    if (list.GetType().IsArray)
                    {
                        ((object[])list)[index] = value;
                        return true;
                    }
                    else if (value is IEnumerable)
                    {
                        ((IList)list)[index] = value;
                        return true;
                    }
                }
                return false;
            }
        }
    }