Move parent and keep children in place


Sometime it is usefull to move the parent without affecting its children. I made a simple tool to allow the user to move, rotate and scale a gameObject without affecting its children. Easy to use, it also manage the Undo/Redo.

Demonstration

Here what you see is what you get. When Pressing L, or pressing the button [Lock], you can move, rotate and scale the parent of any gameObject while keeping its children in place.




How to use:

This tool require a lot of scripts to work. I organise them well enought for simplicity and usability. Simply place the 6 following scripts in any Editor/ folder inside your Asset folder.

LockChildren.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using static UnityEditor.EditorGUILayout;
#if UNITY_EDITOR
using UnityEditor;
#endif

namespace ExtUnityComponents
{
    /// <summary>
    /// Move, rotate & scale a parent, whild keeping children still
    /// </summary>
    public class LockChildren
    {
        /// <summary>
        /// Save state of a child
        /// </summary>
        public struct SaveChildPosition
        {
            public Transform Child;
            public Transform ChildTmp;
            public Vector3 PreviousPosition;
            public Quaternion PreviousRotation;
            public Vector3 PreviousScale;
        }

        private struct SaveStateParent
        {
            public Vector3 InitialPosition;
            public Quaternion InitialRotation;
            public Vector3 InitialScale;

            public SaveStateParent(Vector3 position, Quaternion rotation, Vector3 scale)
            {
                InitialPosition = position;
                InitialRotation = rotation;
                InitialScale = scale;
            }
        }

        //all child saved
        private List<SaveChildPosition> _allChilds = new List<SaveChildPosition>();
        private Vector3 _parentPosition;
        private Quaternion _parentRotation;
        private Vector3 _parentScale;
        private SaveStateParent _initialStateParent;

        private GameObject _parentTmpCreated = null;
        private List<GameObject> _allChildsTmps = new List<GameObject>();

        private bool _lockChildrenPosition = false;  //keep this value outside of the inspected gameObject
        private PivotMode _cachedPivotMode;
        private bool _hasBeenInternallyInit = false;
        private Transform _currentTarget = null;

        private TinyEditorWindowSceneView _tinyLockChildrenWindow;
        private readonly string KEY_EDITOR_PREF_LOCK_CHILDREN_WINDOW = "KEY_EDITOR_PREF_LOCK_CHILDREN_WINDOW";
        private Editor _currentEditor;

        public void Init(Transform parent, Editor current)
        {
            _currentTarget = parent;
            _currentEditor = current;
            _lockChildrenPosition = false;
            _hasBeenInternallyInit = false;
        }

        public void InitOnFirstOnSceneGUI()
        {
            InitTinyEditorWindow();
        }

        private void InitTinyEditorWindow()
        {
            _tinyLockChildrenWindow = new TinyEditorWindowSceneView();
            _tinyLockChildrenWindow.TinyInit(KEY_EDITOR_PREF_LOCK_CHILDREN_WINDOW, "Move Parent Tool", TinyEditorWindowSceneView.DEFAULT_POSITION.MIDDLE_RIGHT);
        }

        /// <summary>
        /// display Lock children button
        /// </summary>
        public void CustomOnInspectorGUI()
        {
            using (VerticalScope verticalScope = new VerticalScope(EditorStyles.helpBox))
            {
                using (HorizontalScope horizontalScope = new HorizontalScope())
                {
                    GUILayout.Label("Lock Children positions:");

                    if (DisplayToggle(_currentTarget))
                    {
                        LockStateChanged();
                    }
                }
                if (_lockChildrenPosition && IsParentAPrefabs(_currentTarget))
                {
                    GUI.color = Color.yellow;
                    GUILayout.Label("/!\\ Can't lock scaling on a prefab");
                }
            }
        }

        private bool DisplayToggle(Transform target)
        {
            bool disable = false;
            string messageButton = (_lockChildrenPosition) ? "Unlock [L]" : "Lock [L]";
            bool previousState = _lockChildrenPosition;

            if (!IsTargetHaveChild(target))
            {
                disable = true;
                messageButton = "no child to lock";
                _lockChildrenPosition = previousState = false;
            }

            EditorGUI.BeginDisabledGroup(disable);
            {
                _lockChildrenPosition = GUILayout.Toggle(_lockChildrenPosition, messageButton, EditorStyles.miniButton);
            }
            EditorGUI.EndDisabledGroup();

            return (previousState != _lockChildrenPosition);
        }

        private bool IsTargetHaveChild(Transform target)
        {
            return (target.childCount != 0);
        }

        public bool IsParentAPrefabs(Transform parent)
        {
            return (PrefabUtility.IsPartOfAnyPrefab(parent.gameObject));
        }

        /// <summary>
        /// called when the lock state changed
        /// </summary>
        private void LockStateChanged()
        {
            if (_lockChildrenPosition)
            {
                _tinyLockChildrenWindow.IsClosed = false;
                if (!_hasBeenInternallyInit)
                {
                    InitLock();
                }
            }
            else
            {
                CustomDisable();
            }
        }

        /// <summary>
        /// called for initializing the parent state and all his children state
        /// </summary>
        /// <param name="parent"></param>
        private void InitLock()
        {

            _cachedPivotMode = Tools.pivotMode;
            Tools.pivotMode = PivotMode.Pivot;
            InitTmpGameObjects(_currentTarget);
            SaveChildsPosition(_currentTarget);
            _parentPosition = _currentTarget.position;
            _initialStateParent = new SaveStateParent(_currentTarget.position, _currentTarget.rotation, _currentTarget.localScale);
            //ExtReflexion.SetSearch(_currentTarget.name);
        }

        private void InitTmpGameObjects(Transform parent)
        {
            if (_parentTmpCreated != null)
            {
                return;
            }
            _parentTmpCreated = ExtGameObject.CreateLocalGameObject("tmp Parent", parent.parent, parent.localPosition, parent.localRotation, parent.localScale, null);
            _parentTmpCreated.hideFlags = HideFlags.HideInHierarchy;
            InitTmpChilds(parent);
        }


        private void InitTmpChilds(Transform parent)
        {
            _allChildsTmps.Clear();
            for (int i = 0; i < parent.childCount; i++)
            {
                GameObject child = ExtGameObject.CreateLocalGameObject("tmp child " + i, _parentTmpCreated.transform, parent.GetChild(i).localPosition, parent.GetChild(i).localRotation, parent.GetChild(i).localScale, null);
                child.hideFlags = HideFlags.HideInHierarchy;
                _allChildsTmps.Add(child);
            }
        }

        /// <summary>
        /// save all childrens in an array, and store there position
        /// </summary>
        /// <param name="parent">parent of all childrens</param>
        public void SaveChildsPosition(Transform parent)
        {
            _allChilds.Clear();
            for (int i = 0; i < parent.childCount; i++)
            {
                SaveChildPosition newChild = new SaveChildPosition()
                {
                    Child = parent.GetChild(i),
                    ChildTmp = _allChildsTmps[i].transform,
                    PreviousPosition = parent.GetChild(i).position,
                    PreviousRotation = parent.GetChild(i).rotation,
                    PreviousScale = parent.GetChild(i).localScale//GetGlobalToLocalScaleFactor(parent.GetChild(i))
                };
                _allChilds.Add(newChild);
            }
        }

        /// <summary>
        /// clear childs and remap original pivot tool
        /// </summary>
        public void CustomDisable()
        {
            if (_parentTmpCreated != null)
            {
                GameObject.DestroyImmediate(_parentTmpCreated);
            }
            if (_allChildsTmps.Count > 0)
            {
                for (int i = _allChildsTmps.Count - 1; i >= 0; i--)
                {
                    if (_allChildsTmps[i] != null)
                    {
                        GameObject.DestroyImmediate(_allChildsTmps[i]);
                    }
                }
            }
            Tools.pivotMode = _cachedPivotMode;
            _hasBeenInternallyInit = false;
            //ExtReflexion.SetSearch("");
        }

        public void CustomOnSceneGUI()
        {
            if (_parentTmpCreated != null)
            {
                for (int i = 0; i < _allChildsTmps.Count; i++)
                {
                    ExtDrawGuizmos.DebugWireSphere(_allChildsTmps[i].transform.position, Color.red, 0.1f);
                    ExtDrawGuizmos.DebugCross(_allChildsTmps[i].transform);
                }
            }

            if (IsPressingL(Event.current))
            {
                _lockChildrenPosition = !_lockChildrenPosition;
                LockStateChanged();
                _currentEditor.Repaint();
            }

            if (_lockChildrenPosition)
            {
                ExtDrawGuizmos.DebugCross(_currentTarget.position, _currentTarget.rotation, 1);
                ShowTinyEditor();
            }
        }

        /// <summary>
        /// return true if we press on J
        /// </summary>
        /// <param name="current"></param>
        /// <returns></returns>
        private bool IsPressingL(Event current)
        {
            if ((current.keyCode == KeyCode.L && current.type == EventType.KeyUp))
            {
                return (true);
            }
            return (false);
        }


        private void ShowTinyEditor()
        {
            _tinyLockChildrenWindow.ShowEditorWindow(DisplayLockChildrenWindow, SceneView.currentDrawingSceneView, Event.current);
            if (_tinyLockChildrenWindow.IsClosed)
            {
                _lockChildrenPosition = false;
                EditorUtility.SetDirty(_currentTarget);
                LockStateChanged();
            }
        }

        private void DisplayLockChildrenWindow()
        {
            if (GUILayout.Button("Mean position"))
            {
                ExtUndo.Record(_currentTarget, "Tool: lock children move");
                Vector3 mean = ExtVector3.GetMeanOfXPoints(_currentTarget.GetAllChild().ToArray(), true);
                _currentTarget.position = mean;
            }
            if (GUILayout.Button("Mean Rotation"))
            {
                Quaternion mean = ExtQuaternion.AverageQuaternion(ExtQuaternion.GetAllRotation(_currentTarget.GetAllChild().ToArray()));
                _currentTarget.rotation = mean;
            }
            if (GUILayout.Button("Reset"))
            {
                _currentTarget.position = _initialStateParent.InitialPosition;
                _currentTarget.rotation = _initialStateParent.InitialRotation;
                _currentTarget.localScale = _initialStateParent.InitialScale;
            }
        }


        /// <summary>
        /// called every update of the editor
        /// </summary>
        /// <param name="target">manange only one target</param>
        public void OnEditorUpdate()
        {
            if (_currentTarget == null || !_lockChildrenPosition)
            {
                return;
            }

            if (IsParentChanged(_currentTarget))
            {
                MoveEveryChildToTherePreviousPosition(_currentTarget);
            }

            SaveCurrentParentPosition(_currentTarget);
            SaveChildsPosition(_currentTarget);
        }

        /// <summary>
        /// Say if the parent is moved, in position, rotation or scale
        /// </summary>
        /// <param name="parent">parent to check</param>
        /// <returns>true if the parent moved, rotate or scaled</returns>
        public bool IsParentChanged(Transform parent)
        {
            return (_parentPosition != parent.position
                || _parentRotation != parent.rotation
                || _parentScale != parent.localScale);
        }

        /// <summary>
        /// here move every children to there original state
        /// </summary>
        /// <param name="parent"></param>
        public void MoveEveryChildToTherePreviousPosition(Transform parent)
        {
            _parentTmpCreated.transform.position = parent.transform.position;
            _parentTmpCreated.transform.rotation = parent.transform.rotation;

            for (int i = 0; i < _allChilds.Count; i++)
            {
#if UNITY_EDITOR
                if (Application.isPlaying)
                {
                    MoveChildNotInsidePrefabs(i, parent);
                }
                else
                {
                    if (IsParentAPrefabs(parent))
                    {
                        MoveChildInsidePrefabs(i, parent);
                    }
                    else
                    {
                        MoveChildNotInsidePrefabs(i, parent);
                    }
                }
#else
            MoveChildNotInsidePrefabs(i, parent);
#endif
            }
        }

        private void MoveChildNotInsidePrefabs(int i, Transform parent)
        {
            ExtUndo.Record(_allChilds[i].Child, "Lock tool child lock");

            _allChilds[i].ChildTmp.position = _allChilds[i].PreviousPosition;
            _allChilds[i].ChildTmp.rotation = _allChilds[i].PreviousRotation;
            _allChilds[i].Child.SetParent(_parentTmpCreated.transform);
            _allChilds[i].Child.position = _allChilds[i].PreviousPosition;
            _allChilds[i].Child.rotation = _allChilds[i].PreviousRotation;
            _allChilds[i].Child.localScale = _allChilds[i].ChildTmp.localScale;
            _allChilds[i].Child.SetParent(parent);
        }


        /// <summary>
        /// move object inside the prefabs, we can't manage the scale here
        /// </summary>
        /// <param name="i"></param>
        /// <param name="parent"></param>
        private void MoveChildInsidePrefabs(int i, Transform parent)
        {
            _allChilds[i].ChildTmp.position = _allChilds[i].PreviousPosition;
            _allChilds[i].ChildTmp.rotation = _allChilds[i].PreviousRotation;
            _allChilds[i].Child.position = _allChilds[i].PreviousPosition;
            _allChilds[i].Child.rotation = _allChilds[i].PreviousRotation;
            _allChilds[i].Child.localScale = _allChilds[i].PreviousScale;
        }

        /// <summary>
        /// save parent state for later
        /// </summary>
        /// <param name="parent"></param>
        public void SaveCurrentParentPosition(Transform parent)
        {
            _parentPosition = parent.position;
            _parentRotation = parent.rotation;
            _parentScale = parent.localScale;
        }
    }
}
TransformEditor.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

using UnityEditor;
using UnityEngine;


/// <summary>
/// Custom Transform editor.
/// </summary>
namespace ExtUnityComponents
{
    [CustomEditor(typeof(Transform))]
    [CanEditMultipleObjects]
    public class TransformEditor : DecoratorComponentsEditor
    {
        private LockChildren _lockChildren = new LockChildren();    //ref to script

        private TransformHiddedTools _transformHiddedTools;

        /// <summary>
        /// here call the constructor of the CustomWrapperEditor class,
        /// by telling it who we are (a Transform Inspector)
        /// </summary>
        public TransformEditor()
            : base(BUILT_IN_EDITOR_COMPONENTS.TransformInspector)
        {

        }

        /// <summary>
        /// need to clean when quitting
        /// </summary>
        protected override void OnCustomDisable()
        {
            _lockChildren.CustomDisable();
        }

        /// <summary>
        /// this function is called on the first OnSceneGUI
        /// </summary>
        /// <param name="sceneview"></param>
        protected override void InitOnFirstOnSceneGUI(SceneView sceneview)
        {
            _lockChildren.InitOnFirstOnSceneGUI();
        }

        protected override void CustomOnSceneGUI(SceneView sceneview)
        {
            _lockChildren.CustomOnSceneGUI();
        }

        /// <summary>
        /// init the targets inspected and store them.
        /// This is called at the first OnInspectorGUI()
        /// Not in the constructor ! we don't have targets information in the constructor
        /// </summary>
        protected override void InitOnFirstInspectorGUI()
        {
            _transformHiddedTools = ConcretTarget.GetOrAddComponent<TransformHiddedTools>();
            _lockChildren.Init((Transform)ConcretTarget, this);
        }

        /// <summary>
        /// This function is called at each OnInspectorGUI
        /// </summary>
        protected override void OnCustomInspectorGUI()
        {
            if (targets.Length < 2)
            {
                _lockChildren.CustomOnInspectorGUI();
            }
        }

        /// <summary>
        /// this function is called at each EditorUpdate() when we need it
        /// </summary>
        protected override void OnEditorUpdate()
        {
            _lockChildren.OnEditorUpdate();
        }
    }
}


DecoratorComponentsEditor.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

using hedCommon.editor.editorWindow;
using hedCommon.extension.runtime;
using Sirenix.OdinInspector.Editor;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine;

namespace ExtUnityComponents
{
    public abstract class DecoratorComponentsEditor : Editor
    {
        /// <summary>
        /// all the virtual function at your disposal
        /// </summary>
        #region virtual functions

        /// <summary>
        /// called when the inspector is enabled
        /// </summary>
        public virtual void OnCustomEnable() { }

        /// <summary>
        /// called when the inspector is disabled
        /// </summary>
        public virtual void OnCustomDisable() { }


        /// <summary>
        /// called every OnInspectorGUI inside the herited class
        /// </summary>
        protected virtual void OnCustomInspectorGUI() { }

        /// <summary>
        /// called at the first OnInspectorGUI inside the herited class
        /// </summary>
        protected virtual void InitOnFirstInspectorGUI() { }

        /// <summary>
        /// called at the first OnSceneGUI inside the herited class
        /// </summary>
        /// <param name="sceneview">current drawing scene view</param>
        protected virtual void CustomOnSceneGUI(SceneView sceneview) { }

        /// <summary>
        /// called at the first OnSceneGUI inside the herited class
        /// </summary>
        /// <param name="sceneview">current drawing scene view</param>
        protected virtual void InitOnFirstOnSceneGUI(SceneView sceneview) { }

        /// <summary>
        /// called every editor Update inside the herited class
        /// </summary>
        protected virtual void OnEditorUpdate() { }

        /// <summary>
        /// called at the first Editor Update inside the herited class
        /// </summary>
        protected virtual void InitOnFirstEditorUpdate() { }

        public virtual void ShowTinyEditorContent() { }

        protected virtual void OnHierarchyChanged() { }


        #endregion

        /// <summary>
        /// some private variable
        /// </summary>
        #region private variables

        /// EditorPref key for save the open/close extension
        private const string KEY_EXPAND_EDITOR_EXTENSION = "KEY_EXPAND_EDITOR_EXTENSION";
        private bool _openExtensions = false;    //is this extension currently open or close ? save this for every gameObjects's components
        protected bool IsExtensionOpened() => _openExtensions;
        private readonly object[] EMPTY_ARRAY = new object[0];   //empty array for invoking methods using reflection
        private System.Type _decoratedEditorType = null;       //Type object for the internally used (decorated) editor.
        private System.Type _editedObjectType;          //Type object for the object that is edited by this editor.
        private Editor _editorInstance;                 //urrent editorInstance
        private bool _isCustomEditor = false;
        private string _nameCustomEditor;
        private bool _hasInitSceneGUI = false;
        private bool _hasInitEditorUpdate = false;
        private bool _hasInitInspector = false;
        private bool _showExtension = true;
        private bool _showTinyEditor = false;
        public TinyEditorWindowSceneView TinyEditorWindow;
        private readonly string KEY_EDITOR_PREF_EDITOR_WINDOW = "KEY_EDITOR_PREF_EDITOR_WINDOW";
        private string _tinyEditorName = "tiny Editor";
        public void ChangeNameEditor(string newName)
        {
            _tinyEditorName = newName;
            if (TinyEditorWindow != null)
            { 
                TinyEditorWindow.NameEditorWindow = newName;
            }
        }

        protected Component ConcretTarget;
        protected Component[] ConcretTargets;

        /// reflexion cache
        private Dictionary<string, MethodInfo> _decoratedMethods = new Dictionary<string, MethodInfo>();
        private Assembly _editorAssembly = Assembly.GetAssembly(typeof(Editor));

        /// <summary>
        /// get editor instance
        /// </summary>
        /// <returns>editor instance</returns>
        protected Editor GetEditorInstance()
        {
            return _editorInstance;
        }

        #endregion

        /// <summary>
        /// all the required setup and initialisation
        /// </summary>
        #region initialisation editor

        public DecoratorComponentsEditor(bool showExtension = false, string tinyEditorName = "")
        {
            SetupDecoratorSettings(showExtension, tinyEditorName, true, "");
        }

        /// <summary>
        /// constructor of the class: here we initialize reflexion variables
        /// </summary>
        /// <param name="editorTypeName">name of the component we inspect</param>
        public DecoratorComponentsEditor(BUILT_IN_EDITOR_COMPONENTS editorTypeName, bool showExtension = true, string tinyEditorName = "")
        {
            SetupDecoratorSettings(showExtension, tinyEditorName, isCustomEditor: false, editorTypeName.ToString());
            SetupCustomEditor(editorTypeName);
        }

        private void SetupDecoratorSettings(bool showExtension, string tinyEditorName, bool isCustomEditor, string nameEditor)
        {
            _showExtension = showExtension;
            _showTinyEditor = !string.IsNullOrEmpty(tinyEditorName);
            _isCustomEditor = isCustomEditor;
            _nameCustomEditor = nameEditor;
            _tinyEditorName = tinyEditorName;
        }

        private void InitTinyEditorWindow(string tinyEditorName)
        {
            TinyEditorWindow = new TinyEditorWindowSceneView();
            string key = KEY_EDITOR_PREF_EDITOR_WINDOW + this.GetType().ToString();
            TinyEditorWindow.TinyInit(key, tinyEditorName, TinyEditorWindowSceneView.DEFAULT_POSITION.UP_LEFT);
            TinyEditorWindow.IsClosable = false;
            TinyEditorWindow.IsMinimisable = true;
        }

        private void SetupCustomEditor(BUILT_IN_EDITOR_COMPONENTS editorTypeName)
        {
            _decoratedEditorType = _editorAssembly.GetTypes().Where(t => t.Name == editorTypeName.ToString()).FirstOrDefault();
            Init();

            // Check CustomEditor types.
            System.Type originalEditedType = GetCustomEditorType(_decoratedEditorType);

            if (originalEditedType != _editedObjectType)
            {
                throw new System.ArgumentException(
                    string.Format("Type {0} does not match the editor {1} type {2}",
                              _editedObjectType, editorTypeName, originalEditedType));
            }
        }

        /// <summary>
        /// need to be called here to initialize the default editor
        /// </summary>
        private void OnEnable()
        {
            SetupEditor();
            InitTargets();
            OnCustomEnable();
        }

        /// <summary>
        /// need to destroy the customEditor when quitting !
        /// </summary>
        private new void OnDisable()
        {
            if (_editorInstance != null)
            {
                DestroyImmediate(_editorInstance);
            }
            _hasInitSceneGUI = false;
            _hasInitEditorUpdate = false;
            _hasInitInspector = false;
            SceneView.duringSceneGui -= OwnOnSceneGUI;
            EditorApplication.update -= CustomEditorApplicationUpdate;
            EditorApplication.hierarchyChanged -= OnHierarchyChanged;
            OnCustomDisable();
        }

        public T GetTarget<T>() where T : Component
        {
            if (ConcretTarget == null)
            {
                return (default(T));
            }
            return ((T)ConcretTarget);
        }

        public GameObject GetGameObject()
        {
            return (ConcretTarget.gameObject);
        }
        public GameObject[] GetGameObjects()
        {
            if (ConcretTargets == null)
            {
                return (null);
            }
            GameObject[] gameObjects = new GameObject[ConcretTargets.Length];
            for (int i = 0; i < ConcretTargets.Length; i++)
            {
                gameObjects[i] = ConcretTargets[i].gameObject;
            }
            return (gameObjects);
        }

        public T[] GetTargets<T>() where T : Component
        {
            if (ConcretTarget == null)
            {
                return (default(T[]));
            }
            T[] array = new T[ConcretTargets.Length];
            for (int i = 0; i < ConcretTargets.Length; i++)
            {
                array[i] = (T)ConcretTargets[i];
            }
            return (array);
        }

        public T GetTargetIndex<T>(int index) where T : Component
        {
            if (ConcretTargets == null
                || ConcretTargets.Length == 0
                || index < 0
                || index >= ConcretTargets.Length)
            {
                return (default(T));
            }
            return ((T)ConcretTargets[index]);
        }

        /// <summary>
        /// return true if we have still the reference of the editor, but no more target...
        /// </summary>
        /// <returns></returns>
        public bool HaveWeLostTargets()
        {
            bool targetLost = ConcretTarget != null && target == null;
            bool concretTargetLost = ConcretTarget == null;
            bool contextLost = ConcretTarget != null && !Selection.gameObjects.Contains(ConcretTarget.gameObject);

            return (/*_isCustomEditor && */(targetLost || concretTargetLost || contextLost));
        }

        /// <summary>
        /// Need to destroy the instance even on Destroy !
        /// I don't know why. Unity generate leaks after each compilation
        /// because of the CreateEditor
        /// </summary>
        private void OnDestroy()
        {
            if (_editorInstance != null)
            {
                DestroyImmediate(_editorInstance);
            }
            //SceneView.duringSceneGui -= OwnOnSceneGUI;
            //EditorApplication.update -= OnEditorApplicationUpdate;
        }

        /// <summary>
        /// here create the editor instance, and open / close the extension panel if needed
        /// </summary>
        private void SetupEditor()
        {
            if (_editorInstance != null)
            {
                return;
            }
            _hasInitSceneGUI = false;
            _hasInitEditorUpdate = false;
            _hasInitInspector = false;
            CreateDefaultInstance();
            OpenOrCloseExtension();
        }


        /// <summary>
        /// here create the editor instance
        /// </summary>
        private void CreateDefaultInstance()
        {
            if (_decoratedEditorType == null || targets == null)
            {
                return;
            }
            _editorInstance = Editor.CreateEditor(targets, _decoratedEditorType);
        }

        /// <summary>
        /// manage if the extensions are opened or closed, and save this setting in EditorPrefs
        /// </summary>
        private void OpenOrCloseExtension()
        {
            if (EditorPrefs.HasKey(KEY_EXPAND_EDITOR_EXTENSION + _nameCustomEditor))
            {
                _openExtensions = EditorPrefs.GetBool(KEY_EXPAND_EDITOR_EXTENSION + _nameCustomEditor);
            }
            else
            {
                _openExtensions = false;
                EditorPrefs.SetBool(KEY_EXPAND_EDITOR_EXTENSION + _nameCustomEditor, _openExtensions);
            }
        }

       

        /// <summary>
        /// iniitalisation of reflexion variables
        /// </summary>
        private void Init()
        {
            var flags = BindingFlags.NonPublic | BindingFlags.Instance;

            var attributes = this.GetType().GetCustomAttributes(typeof(CustomEditor), true) as CustomEditor[];
            var field = attributes.Select(editor => editor.GetType().GetField("m_InspectedType", flags)).First();

            _editedObjectType = field.GetValue(attributes[0]) as System.Type;
        }

        /// <summary>
        /// from a given type of editor, get his CustomEditor as type
        /// </summary>
        /// <param name="type"></param>
        /// <returns></returns>
        private System.Type GetCustomEditorType(System.Type type)
        {
            var flags = BindingFlags.NonPublic | BindingFlags.Instance;

            var attributes = type.GetCustomAttributes(typeof(CustomEditor), true) as CustomEditor[];
            var field = attributes.Select(editor => editor.GetType().GetField("m_InspectedType", flags)).First();

            return field.GetValue(attributes[0]) as System.Type;
        }

        /// <summary>
        /// From a given name of class, invoke it using reflexion
        /// </summary>
        protected void CallMethodUsingReflexion(string methodName)
        {
            MethodInfo method = null;

            // Add MethodInfo to cache
            if (!_decoratedMethods.ContainsKey(methodName))
            {
                var flags = BindingFlags.Instance | BindingFlags.Static | BindingFlags.NonPublic | BindingFlags.Public;

                method = _decoratedEditorType.GetMethod(methodName, flags);

                if (method != null)
                {
                    _decoratedMethods[methodName] = method;
                }
                else
                {
                    //Debug.LogError(string.Format("Could not find method {0}", method));
                }
            }
            else
            {
                method = _decoratedMethods[methodName];
            }

            if (method != null)
            {
                method.Invoke(GetEditorInstance(), EMPTY_ARRAY);
            }
        }

        #endregion

        /// <summary>
        /// The core loop function for display
        /// everything
        /// </summary>
        #region Display Extension

        /// <summary>
        /// here the main InspectorGUI.
        /// It's here that we call the base OnInspecctorGUI.
        /// Then we show the GUI --------- Extension [+]
        /// then if it's open, we call the abstract class CustomOnInspectorGUI()
        /// </summary>
        public override void OnInspectorGUI()
        {
            if (!_isCustomEditor)
            {
                ManageUnityInspector();
            }
            else
            {
                ManageCustomNewInspector();
            }
        }
        private void ManageUnityInspector()
        {
            if (GetEditorInstance() == null)
            {
                return;
            }
            if (HaveWeLostTargets())
            {
                DestroyImmediate(GetEditorInstance());
                return;
            }

            GetEditorInstance().OnInspectorGUI();
            CommonExtenstionInspectorGUI();
        }
        private void ManageCustomNewInspector()
        {
            base.OnInspectorGUI();
            CommonExtenstionInspectorGUI();
        }

        /// <summary>
        /// called whatever this editor is inherited from a custom unity component,
        /// or just a standar sutom editor
        /// </summary>
        private void CommonExtenstionInspectorGUI()
        {
            TryToInitOnFirstInspectorGUI();



            if (_showExtension
                && ConcretTarget != null
                && !IsInPrefabsStage()
                && CanShowExpandFromInterfaceSettings()
                && ExpendBehavior())
            {
                OnCustomInspectorGUI();
            }
        }

        /// <summary>
        /// this function first check if the target have a IHaveDecoratorOnIt interface on it
        /// if we don't have one, return automaticly true
        /// (not every components have this decorator, like internal components or out own without the interface)
        /// 
        /// then return the settings we want
        /// </summary>
        /// <returns></returns>
        private bool CanShowExpandFromInterfaceSettings()
        {
            if (!HaveInterfaceSettings(out IHaveDecoratorOnIt haveDecorator))
            {
                return (true);
            }
            return (haveDecorator.ShowExtension);
        }

        /// <summary>
        /// this function first check if the target have a IHaveDecoratorOnIt interface on it
        /// if we don't have one, return automaticly true
        /// (not every components have this decorator, like internal components or out own without the interface)
        /// 
        /// then return the settings we want
        /// </summary>
        /// <returns></returns>
        private bool CanShowTinyEditorFromInterfaceSettings()
        {
            if (!HaveInterfaceSettings(out IHaveDecoratorOnIt haveDecorator))
            {
                return (true);
            }
            return (haveDecorator.ShowTinyEditorWindow);
        }

        /// <summary>
        /// this function check if the target have a IHaveDecoratorOnIt interface on it
        /// if we don't have one, return automaticly true
        /// (not every components have this decorator, like internal components or out own without the interface)
        /// 
        /// then return the settings we want
        /// </summary>
        /// <returns></returns>
        private bool HaveInterfaceSettings(out IHaveDecoratorOnIt haveDecorator)
        {
            haveDecorator = null;
            if (!_isCustomEditor)
            {
                return (false);
            }

            if (ConcretTarget == null)
            {
                return (false);
            }

            IHaveDecoratorOnIt[] AllInterfaceDecorator = ConcretTarget.GetComponents<IHaveDecoratorOnIt>();
            for (int i = 0; i < AllInterfaceDecorator.Length; i++)
            {
                if (AllInterfaceDecorator[i] == null)
                {
                    continue;
                }

                if (AllInterfaceDecorator[i].GetReferenceDecoratorsComponent() == ConcretTarget)
                {
                    haveDecorator = AllInterfaceDecorator[i];

                    bool targetHaveInterface = haveDecorator != null;
                    return (targetHaveInterface);
                }
            }

            return (false);
        }

        private void InitTargets()
        {
            if (ConcretTargets == null)
            {
                ConcretTarget = (Component)target;
                ConcretTargets = new Component[targets.Length];
                for (int i = 0; i < targets.Length; i++)
                {
                    ConcretTargets[i] = (Component)targets[i];
                }

                //_meshFilterHiddedTools = _concretTarget.GetOrAddComponent<MeshFilterHiddedTools>();

                SceneView.duringSceneGui -= OwnOnSceneGUI;
                SceneView.duringSceneGui += OwnOnSceneGUI;
                EditorApplication.update -= CustomEditorApplicationUpdate;
                EditorApplication.update += CustomEditorApplicationUpdate;

                EditorApplication.hierarchyChanged -= OnHierarchyChanged;
                EditorApplication.hierarchyChanged += OnHierarchyChanged;
            }
        }

        /// <summary>
        /// init the targets inspected and store them.
        /// This is called at the first OnInspectorGUI()
        /// Not in the constructor ! we don't have targets information in the constructor
        /// </summary>
        private void TryToInitOnFirstInspectorGUI()
        {
            if (!_hasInitInspector)
            {
                InitOnFirstInspectorGUI();
                _hasInitInspector = true;
            }
        }

        private void TryToInitOnFirstOnEditorUpdate()
        {
            if (!_hasInitEditorUpdate)
            {
                InitOnFirstEditorUpdate();
                _hasInitEditorUpdate = true;
            }
        }

        /// <summary>
        /// editor update loop
        /// </summary>
        private void CustomEditorApplicationUpdate()
        {
            if (!_isCustomEditor)
            {
                if (GetEditorInstance() == null)
                {
                    return;
                }
            }
            if (HaveWeLostTargets())
            {
                DestroyImmediate(GetEditorInstance());
                return;
            }

            if (!IsExtensionOpened())
            {
                return;
            }
            TryToInitOnFirstOnEditorUpdate();
            OnEditorUpdate();
        }

        /// <summary>
        /// This function is called at each OnSceneGUI to try to initialize it
        /// </summary>
        private void TryToInitOnFirstOnSceneGUI(SceneView sceneview)
        {
            if (!_hasInitSceneGUI)
            {
                if (_showTinyEditor)
                {
                    InitTinyEditorWindow(_tinyEditorName);
                }

                InitOnFirstOnSceneGUI(sceneview);
                _hasInitSceneGUI = true;
            }
        }

        /// <summary>
        /// return true if we are in a prefabs editing mode
        /// </summary>
        /// <returns></returns>
        private bool IsInPrefabsStage()
        {
            return (ExtPrefabs.IsEditingInPrefabMode(ConcretTarget.gameObject));
        }

        public void OwnOnSceneGUI(SceneView sceneview)
        {
            if (!_isCustomEditor)
            {
                if (GetEditorInstance() == null)
                {
                    return;
                }
            }
            if (HaveWeLostTargets())
            {
                DestroyImmediate(GetEditorInstance());
                return;
            }

            TryToInitOnFirstOnSceneGUI(sceneview);

            if (_showTinyEditor
                && !IsInPrefabsStage()
                && CanShowTinyEditorFromInterfaceSettings())
            {
                TinyEditorWindow.ShowEditorWindow(ShowTinyEditorContent, SceneView.currentDrawingSceneView, Event.current);
            }

            
            if (!IsExtensionOpened())
            {
                return;
            }
            

            CustomOnSceneGUI(sceneview);
        }

        /// <summary>
        /// display the Expand behavior of the component editor
        /// </summary>
        private bool ExpendBehavior()
        {
            GUIStyle lineStyle = new GUIStyle();
            lineStyle.normal.background = EditorGUIUtility.whiteTexture;
            lineStyle.stretchWidth = true;
            lineStyle.margin = new RectOffset(0, 0, 12, 18);

            var c = GUI.color;
            var p = GUILayoutUtility.GetRect(GUIContent.none, lineStyle, GUILayout.Height(1));
            p.width -= 120;
            if (Event.current.type == EventType.Repaint)
            {
                GUI.color = EditorGUIUtility.isProSkin ?
                    new Color(0.157f, 0.157f, 0.157f) : new Color(0.5f, 0.5f, 0.5f);
                lineStyle.Draw(p, false, false, false, false);
            }

            EditorGUI.LabelField(new Rect(p.xMax + 30, p.y - 7, 70, 25), "Extensions");

            GUI.color = Color.white;
            GUI.backgroundColor = Color.white;
            if (GUI.Button(new Rect(p.xMax + 100, p.y - 7, 18, 16), (_openExtensions ? " -" : " +")))
            {
                _openExtensions = !_openExtensions;
                EditorPrefs.SetBool(KEY_EXPAND_EDITOR_EXTENSION + _nameCustomEditor, _openExtensions);
            }
            GUI.backgroundColor = Color.white;


            GUI.color = c;

            return (_openExtensions);
        }

        
        #endregion

        /// <summary>
        /// other basic unity fonction we have to manage
        /// </summary>
        #region Unity Fonctions
        public void OnSceneGUI()
        {
            if (GetEditorInstance() == null)
            {
                return;
            }
            CallMethodUsingReflexion("OnSceneGUI");
        }

        protected override void OnHeaderGUI()
        {
            if (GetEditorInstance() == null)
            {
                return;
            }
            CallMethodUsingReflexion("OnHeaderGUI");
        }

        public override void DrawPreview(Rect previewArea)
        {
            if (GetEditorInstance() == null)
            {
                return;
            }
            GetEditorInstance().DrawPreview(previewArea);
        }

        public override string GetInfoString()
        {
            if (GetEditorInstance() == null)
            {
                return ("");
            }
            return GetEditorInstance().GetInfoString();
        }

        public override GUIContent GetPreviewTitle()
        {
            if (GetEditorInstance() == null)
            {
                return (null);
            }
            return GetEditorInstance().GetPreviewTitle();
        }

        public override bool HasPreviewGUI()
        {
            if (GetEditorInstance() == null)
            {
                return (false);
            }
            return GetEditorInstance().HasPreviewGUI();
        }

        public override void OnInteractivePreviewGUI(Rect r, GUIStyle background)
        {
            if (GetEditorInstance() == null)
            {
                return;
            }
            GetEditorInstance().OnInteractivePreviewGUI(r, background);
        }

        public override void OnPreviewGUI(Rect r, GUIStyle background)
        {
            if (GetEditorInstance() == null)
            {
                return;
            }
            GetEditorInstance().OnPreviewGUI(r, background);
        }

        public override void OnPreviewSettings()
        {
            if (GetEditorInstance() == null)
            {
                return;
            }
            GetEditorInstance().OnPreviewSettings();
        }

        public override void ReloadPreviewInstances()
        {
            if (GetEditorInstance() == null)
            {
                return;
            }
            GetEditorInstance().ReloadPreviewInstances();
        }

        public override Texture2D RenderStaticPreview(string assetPath, Object[] subAssets, int width, int height)
        {
            if (GetEditorInstance() == null)
            {
                return (null);
            }
            return GetEditorInstance().RenderStaticPreview(assetPath, subAssets, width, height);
        }

        public override bool RequiresConstantRepaint()
        {
            if (GetEditorInstance() == null)
            {
                return (false);
            }
            return GetEditorInstance().RequiresConstantRepaint();
        }

        public override bool UseDefaultMargins()
        {
            if (GetEditorInstance() == null)
            {
                return (true);
            }
            return GetEditorInstance().UseDefaultMargins();
        }

        #endregion
    }
}

BuiltInEditorComponentsEnum.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

/// <summary>
/// this script containt the list of all 
/// Unity's Built In Inspector Types
/// 
/// Thanks to https://pastebin.com/Kq3T8aMw
/// for the datas
/// </summary>

namespace ExtUnityComponents
{
    public enum BUILT_IN_EDITOR_COMPONENTS
    {
        AnchoredJoint2DEditor,
        AnimationClipEditor,
        AnimationEditor,
        AnimatorInspector,
        AnimatorOverrideControllerInspector,
        AssetStoreAssetInspector,
        AudioChorusFilterEditor,
        AudioClipInspector,
        AudioDistortionFilterEditor,
        AudioEchoFilterEditor,
        AudioHighPassFilterEditor,
        AudioImporterInspector,
        AudioLowPassFilterInspector,
        AudioMixerControllerInspector,
        AudioMixerGroupEditor,
        AudioMixerSnapshotControllerInspector,
        AudioReverbFilterEditor,
        AudioReverbZoneEditor,
        AudioSourceInspector,
        AvatarEditor,
        AvatarMaskInspector,
        BillboardAssetInspector,
        BillboardRendererInspector,
        BlendTreeInspector,
        BoxCollider2DEditor,
        BoxColliderEditor,
        CameraEditor,
        CanvasEditor,
        CapsuleColliderEditor,
        CharacterControllerEditor,
        CircleCollider2DEditor,
        ClothInspector,
        Collider2DEditorBase,
        Collider3DEditorBase,
        ColliderEditorBase,
        ColorPresetLibraryEditor,
        CubemapInspector,
        CurvePresetLibraryEditor,
        DefaultAssetInspector,
        DistanceJoint2DEditor,
        DoubleCurvePresetLibraryEditor,
        EdgeCollider2DEditor,
        EditorSettingsInspector,
        Effector2DEditor,
        FogEditor,
        FontInspector,
        GameObjectInspector,
        GenericInspector,
        GradientPresetLibraryEditor,
        GraphicsSettingsInspector,
        HingeJoint2DEditor,
        HingeJointEditor,
        Joint2DEditorBase,
        LightEditor,
        LightingEditor,
        LightmapParametersEditor,
        LightProbeGroupInspector,
        LightProbesInspector,
        LineRendererInspector,
        LODGroupEditor,
        MaterialEditor,
        MeshColliderEditor,
        MeshRendererEditor,
        ModelImporterClipEditor,
        ModelImporterEditor,
        ModelImporterModelEditor,
        ModelImporterRigEditor,
        ModelInspector,
        MonoScriptImporterInspector,
        MonoScriptInspector,
        MovieImporterInspector,
        MovieTextureInspector,
        NavMeshAgentInspector,
        NavMeshObstacleInspector,
        OcclusionAreaEditor,
        OcclusionPortalInspector,
        OffMeshLinkInspector,
        OtherRenderingEditor,
        ParticleRendererEditor,
        ParticleSystemInspector,
        Physics2DSettingsInspector,
        PhysicsManagerInspector,
        PlayerSettingsEditor,
        PluginImporterInspector,
        PolygonCollider2DEditor,
        ProceduralMaterialInspector,
        ProceduralTextureInspector,
        QualitySettingsEditor,
        RectTransformEditor,
        ReflectionProbeEditor,
        RendererEditorBase,
        RenderSettingsInspector,
        RenderTextureInspector,
        RigidbodyEditor,
        ScriptExecutionOrderInspector,
        ShaderImporterInspector,
        ShaderInspector,
        ShaderVariantCollectionInspector,
        SkinnedMeshRendererEditor,
        SliderJoint2DEditor,
        SpeedTreeImporterInspector,
        SpeedTreeMaterialInspector,
        SphereColliderEditor,
        SpringJoint2DEditor,
        SpriteInspector,
        SpriteRendererEditor,
        SubstanceImporterInspector,
        TagManagerInspector,
        TerrainInspector,
        TextAssetInspector,
        TextMeshInspector,
        Texture3DInspector,
        TextureImporterInspector,
        TextureInspector,
        TrailRendererInspector,
        TransformInspector,
        TreeEditor,
        TrueTypeFontImporterInspector,
        WebCamTextureInspector,
        WheelColliderEditor,
        WheelJoint2DEditor,
        WindInspector
    }

    public enum CUSTOM_EDITOR_COMPONENTS
    {
        MeshFilterEditor
    }
}
TinyEditorWindowSceneView.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

namespace ExtUnityComponents
{
    public class TinyEditorWindowSceneView
    {
        public struct SaveEditorPref
        {
            public Rect Position;
            public bool Minimised;
        }

        private SaveEditorPref _saveEditorPreference = new SaveEditorPref();

        public string GetJsonOfSave()
        {
            _saveEditorPreference.Position = _windowMenu;
            _saveEditorPreference.Minimised = _isMinimised;
            string json = JsonUtility.ToJson(_saveEditorPreference);
            return (json);
        }

        public SaveEditorPref GetFromJsonDatasEditorPref(string json)
        {
            _saveEditorPreference = JsonUtility.FromJson<SaveEditorPref>(json);
            return (_saveEditorPreference);
        }

        private Rect _windowMenu;

        public delegate void ContentToDrawDelegate();
        private ContentToDrawDelegate _methodToCall;
        private Event _currentEvent;
        private float _saveHeight = 0;
        private bool _isMinimised = false;

        private string _keyEditorPref = "";
        private string _nameEditorWindow = "";
        private int _id;

        private bool _draggable = false;
        public bool IsMinimisable = true;
        private Vector2 _lastPositionRight;
        private bool _snapRight;
        private bool _snapUp;
        public bool IsClosable = false;
        public EditorWindow _currentEditorWindow;
        private bool _lockToVisibleSizeX;    //if true, lock to the visible screen position, else, lock to the max size of the window
        private bool _lockToVisibleSizeY;    //if true, lock to the visible screen position, else, lock to the max size of the window

        private Rect _oldRect;
        private Vector2 _maxRect;  //determine the max value of size
        public bool IsClosed = false;

        public enum DEFAULT_POSITION
        {
            NONE = 0,

            UP_LEFT = 10,
            UP = 11,
            UP_RIGHT = 12,

            MIDDLE_LEFT = 20,
            MIDDLE = 21,
            MIDDLE_RIGHT = 22,

            DOWN_LEFT = 30,
            DOWN = 31,
            DOWN_RIGHT = 32,
        }

        /// <summary>
        /// 
        /// type:       UP_LEFT         UP          UP_RIGHT
        /// anchor:     (up left)       (up left)   (up right)
        /// 
        /// type:       MIDDLE_LEFT     MIDDLE      MIDDLE_RIGHT
        /// anchor:     (up left)       (up left)   (up right)
        /// 
        /// 
        /// type:       DOWN_LEFT       DOWN        DOWN_RIGHT
        /// anchor:     (down left)     (down left) (down right)
        /// </summary>
        /// <param name="position"></param>
        /// <param name="snapUp"></param>
        /// <param name="snapRight"></param>
        /// <returns></returns>
        public Rect GetNiceRectFromEnum(DEFAULT_POSITION position, float defaultWidth, out bool snapUp, out bool snapRight)
        {
            Rect rectNav = new Rect(0, 0, 0, 0);
            snapUp = true;
            snapRight = false;

            float height = 50;

            switch (position)
            {
                case DEFAULT_POSITION.UP_LEFT:
                    {
                        snapUp = true;
                        snapRight = false;

                        float x = 10;
                        float y = 10;
                        rectNav = new Rect(x, y, defaultWidth, height);
                        return (rectNav);
                    }
                case DEFAULT_POSITION.UP:
                    {
                        snapUp = true;
                        snapRight = false;

                        float x = (EditorGUIUtility.currentViewWidth / 2) - (defaultWidth / 2);
                        float y = 10;
                        rectNav = new Rect(x, y, defaultWidth, height);
                        return (rectNav);
                    }

                case DEFAULT_POSITION.UP_RIGHT:
                    {
                        snapUp = true;
                        snapRight = true;

                        float marginFromRight = 100f;
                        float x = EditorGUIUtility.currentViewWidth - defaultWidth - marginFromRight;
                        float y = 10;
                        rectNav = new Rect(x, y, defaultWidth, height);
                        return (rectNav);
                    }

                case DEFAULT_POSITION.MIDDLE_LEFT:
                    {
                        snapUp = true;
                        snapRight = false;

                        float x = 10f;
                        float y = (_currentEditorWindow.position.height / 2) - (height / 2);
                        rectNav = new Rect(x, y, defaultWidth, height);
                        return (rectNav);
                    }

                case DEFAULT_POSITION.MIDDLE_RIGHT:
                    {
                        snapUp = true;
                        snapRight = true;

                        float marginFromRight = 10f;
                        float x = EditorGUIUtility.currentViewWidth - defaultWidth - marginFromRight;
                        float y = (_currentEditorWindow.position.height / 2) - (height / 2);
                        rectNav = new Rect(x, y, defaultWidth, height);
                        return (rectNav);
                    }


                case DEFAULT_POSITION.DOWN_LEFT:
                    {
                        snapUp = false;
                        snapRight = false;

                        float marginFromDown = 10f;
                        float x = 10;
                        float y = _currentEditorWindow.position.height - height - marginFromDown;
                        rectNav = new Rect(x, y, defaultWidth, height);
                        return (rectNav);
                    }
                case DEFAULT_POSITION.DOWN:
                    {
                        float yFromDown = 10;  //from bottom, go upper
                        float x = (EditorGUIUtility.currentViewWidth / 2) - (defaultWidth / 2);
                        float y = SceneView.currentDrawingSceneView.camera.pixelRect.size.y - height - yFromDown;
                        rectNav = new Rect(x, y, defaultWidth, height);
                        return (rectNav);
                    }
                case DEFAULT_POSITION.DOWN_RIGHT:
                    {
                        float yFromDown = 10;  //from bottom, go upper
                        float xFromRight = 10;
                        float x = EditorGUIUtility.currentViewWidth - defaultWidth - xFromRight;
                        float y = SceneView.currentDrawingSceneView.camera.pixelRect.size.y - height - yFromDown;
                        rectNav = new Rect(x, y, defaultWidth, height);
                        return (rectNav);
                    }


                default:
                case DEFAULT_POSITION.MIDDLE:
                    {
                        snapUp = true;
                        snapRight = false;

                        float x = (EditorGUIUtility.currentViewWidth / 2) - (defaultWidth / 2);
                        float y = (_currentEditorWindow.position.height / 2) - (height / 2);
                        rectNav = new Rect(x, y, defaultWidth, height);
                        return (rectNav);
                    }
            }
        }

        public void TinyInit(string keyEditorPref, string nameEditorWindow, DEFAULT_POSITION position, float defaultWidth = 150)
        {
            _currentEditorWindow = SceneView.currentDrawingSceneView;

            Rect rect = GetNiceRectFromEnum(position, defaultWidth, out bool snapUp, out bool snapRight);

            Init(
                    keyEditorPref: keyEditorPref,
                    nameEditorWindow: nameEditorWindow,
                    id: keyEditorPref.GetHashCode(),
                    windowMenu: rect,
                    currentEditorWindow: _currentEditorWindow,
                    currentEvent: Event.current,
                    draggable: true,
                    isMinimisable: false,
                    snapRight: snapRight,
                    snapUp: snapUp,
                    maxRect: new Vector2(0, 0),
                    applySetup: true,
                    closable: true,
                    lockToVisibleSizeX: true,
                    lockToVisibleSizeY: true);
        }

        /// <summary>
        /// need to be called in OnGUI
        /// </summary>
        public void Init(string keyEditorPref,
                        string nameEditorWindow,
                        int id,

                        Rect windowMenu,
                        EditorWindow currentEditorWindow,
                        Event currentEvent,

                        bool draggable,
                        bool isMinimisable,
                        bool snapRight,
                        bool snapUp,

                        Vector2 maxRect,
                        bool applySetup = true,
                        bool closable = false,
                        bool lockToVisibleSizeX = true,
                        bool lockToVisibleSizeY = true)
        {
            _maxRect = maxRect;
            _lockToVisibleSizeX = lockToVisibleSizeX;
            _lockToVisibleSizeY = lockToVisibleSizeY;
            _keyEditorPref = keyEditorPref;
            _nameEditorWindow = nameEditorWindow;
            _id = id;
            _windowMenu = windowMenu;
            _currentEditorWindow = currentEditorWindow;
            _currentEvent = currentEvent;

            _draggable = draggable;
            IsMinimisable = isMinimisable;
            _snapRight = snapRight;
            _snapUp = snapUp;

            _isMinimised = false;
            _saveHeight = _windowMenu.height;
            IsClosable = closable;
            IsClosed = false;

            if (applySetup)
            {
                ApplySetup();
            }
        }

        public void SetNewPosition(Vector2 newPosition, Event currentEvent)
        {
            _currentEvent = currentEvent;
            _windowMenu.x = newPosition.x;
            _windowMenu.y = newPosition.y;
            EditorPrefs.SetString(_keyEditorPref, GetJsonOfSave());
        }

        public Rect GetPosition()
        {
            return (_windowMenu);
        }

        private void ApplySetup()
        {
            if (!string.IsNullOrEmpty(_keyEditorPref) && EditorPrefs.HasKey(_keyEditorPref))
            {
                ApplySaveSettings();
            }
            SavePositionFromRight(_snapRight, _snapUp);
        }

        /// <summary>
        /// apply the settings of the saved EditorPref
        /// </summary>
        private void ApplySaveSettings()
        {
            string keyRect = EditorPrefs.GetString(_keyEditorPref);
            _saveEditorPreference = GetFromJsonDatasEditorPref(keyRect);
            _windowMenu = new Rect(_saveEditorPreference.Position.x, _saveEditorPreference.Position.y, _windowMenu.width, _windowMenu.height);
            _isMinimised = _saveEditorPreference.Minimised;
            if (!IsMinimisable)
            {
                IsMinimisable = false;
            }
        }

        private bool HasChanged()
        {
            if (_saveEditorPreference.Position != _windowMenu
                || _saveEditorPreference.Minimised != _isMinimised)
            {
                return (true);
            }
            return (false);
        }

        private void SavePositionFromRight(bool snapRight, bool snapUp)
        {
            if (_currentEditorWindow == null)
            {
                _lastPositionRight = new Vector2(_windowMenu.x, _windowMenu.y);
                return;
            }

            float SnapWidth = (_lockToVisibleSizeX) ? EditorGUIUtility.currentViewWidth : _currentEditorWindow.position.height;
            float Snapheight = (_lockToVisibleSizeY) ? _currentEditorWindow.position.height : _currentEditorWindow.maxSize.y;

            _lastPositionRight.x = (snapRight) ? SnapWidth - _windowMenu.x : _windowMenu.x;
            _lastPositionRight.y = (snapUp) ? _windowMenu.y : Snapheight - _windowMenu.y;
        }

        private Vector2 GetNewRightPositiionFromOld(bool snapXRight, bool snapYUp)
        {
            if (_currentEditorWindow == null)
            {
                return (_lastPositionRight);
            }

            float SnapWidth = (_lockToVisibleSizeX) ? EditorGUIUtility.currentViewWidth : _currentEditorWindow.position.height;
            float Snapheight = (_lockToVisibleSizeY) ? _currentEditorWindow.position.height : _currentEditorWindow.maxSize.y;


            Vector2 pos;
            pos.x = (snapXRight) ? SnapWidth - _lastPositionRight.x : _lastPositionRight.x;
            pos.y = (snapYUp) ? _lastPositionRight.y : Snapheight - _lastPositionRight.y;
            return (pos);
        }

        /// <summary>
        /// called every frame, 
        /// </summary>
        /// <param name="action"></param>
        public Rect ShowEditorWindow(ContentToDrawDelegate action,
                                    EditorWindow currentEditorWindow,
                                    Event current)
        {

            _methodToCall = action;
            _currentEvent = current;
            _currentEditorWindow = currentEditorWindow;


            _windowMenu = ReworkPosition(_windowMenu, _snapRight, _snapUp);



            if (!IsMinimisable)
            {
                _isMinimised = false;
            }

            if (_isMinimised)
            {
                _windowMenu.height = 15f;
                _windowMenu = GUI.Window(_id, _windowMenu, NavigatorWindow, _nameEditorWindow);
                SavePositionFromRight(_snapRight, _snapUp);
            }
            else
            {
                //here resize auto
                _windowMenu = GUILayout.Window(_id, _windowMenu, NavigatorWindow, _nameEditorWindow, GUILayout.ExpandHeight(true));



                SavePositionFromRight(_snapRight, _snapUp);
            }

            if (!string.IsNullOrEmpty(_keyEditorPref) && HasChanged())
            {
                EditorPrefs.SetString(_keyEditorPref, GetJsonOfSave());
            }


            return (_windowMenu);
        }

        /// <summary>
        /// 
        /// </summary>
        /// <param name="minimise"></param>
        /// <param name="hard"></param>
        public void SetMinimise(bool minimise, bool hard)
        {
            if (!IsMinimisable)
            {
                if (!hard)
                {
                    return;
                }
                _isMinimised = minimise;
            }
            _isMinimised = minimise;
        }

        private void DisplayClosable()
        {
            float x = _windowMenu.width - 20;
            float y = 0;
            float width = 20f;
            float height = 15f;

            if (_maxRect.x > 0)
            {
                x = Mathf.Min(_maxRect.x, x);
            }
            if (_maxRect.y > 0)
            {
                y = Mathf.Min(_maxRect.y, y);
            }

            GUILayout.BeginArea(new Rect(x, y, width, height));
            if (GUILayout.Button(" x "))
            {
                IsClosed = true;
            }
            GUILayout.EndArea();
        }

        /// <summary>
        /// called to display the minimsie button
        /// </summary>
        private void DisplayMinimise()
        {
            if (_isMinimised)
            {
                GUILayout.BeginArea(new Rect(0, 0, _windowMenu.width, 15));
                GUILayout.Label("   " + _nameEditorWindow);
                GUILayout.EndArea();
            }


            float x = _windowMenu.width - 20;
            float y = 0;
            float width = 20f;
            float height = 15f;

            GUILayout.BeginArea(new Rect(x, y, width, height));
            bool minimise = GUILayout.Button(" - ");
            GUILayout.EndArea();

            if (minimise)
            {
                _isMinimised = (_isMinimised) ? false : true;
            }
        }

        private void DisplayDraggable()
        {
            GUILayout.BeginArea(new Rect(0, 0, 15, 15));
            GUILayout.Label("#");
            GUILayout.EndArea();
        }

        /// <summary>
        /// give restriction to position of the panel
        /// </summary>
        /// <param name="currentPosition"></param>
        /// <returns></returns>
        private Rect ReworkPosition(Rect currentPosition, bool snapRight, bool snapUp)
        {
            if (_currentEditorWindow == null)
            {
                return (currentPosition);
            }


            if (_maxRect.x > 0)
            {
                currentPosition.width = Mathf.Min(_maxRect.x, currentPosition.width);
            }
            if (_maxRect.y > 0)
            {
                currentPosition.height = Mathf.Min(_maxRect.y, currentPosition.height);
            }


            Vector2 snapToRight = GetNewRightPositiionFromOld(snapRight, snapUp);
            currentPosition.x = snapToRight.x;
            currentPosition.y = snapToRight.y;


            float minX = 10;
            float minY = 20;
            float maxX = EditorGUIUtility.currentViewWidth - currentPosition.width - minX;
            float maxY = _currentEditorWindow.position.height - currentPosition.height - 5;

            if (!_lockToVisibleSizeX)
            {
                maxX = _currentEditorWindow.maxSize.x - currentPosition.width - minX;
            }
            if (!_lockToVisibleSizeY)
            {
                maxY = _currentEditorWindow.maxSize.y - currentPosition.height - 5;
            }

            if (currentPosition.x < minX)
            {
                currentPosition.x = minX;
                _snapRight = false;
            }
            if (currentPosition.x > maxX)
            {
                currentPosition.x = maxX;
                _snapRight = true;
            }

            if (currentPosition.y < minY)
            {
                currentPosition.y = minY;
                _snapUp = true;
            }
            //here the _sceneView.camera.pixelRect.size.y is incoherent when applying this event
            if (_currentEvent.type == EventType.Repaint)
            {
                //Debug.Log(_currentEvent.type);
                if (currentPosition.y > maxY)
                {
                    currentPosition.y = maxY;
                    _snapUp = false;
                }
            }

            return (currentPosition);
        }


        /// <summary>
        /// draw the editor window with everything in it
        /// </summary>
        /// <param name="id"></param>
        private void NavigatorWindow(int id)
        {
            if (IsClosable)
            {
                DisplayClosable();
            }
            else if (IsMinimisable)
            {
                DisplayMinimise();
            }

            if (_draggable)
            {
                DisplayDraggable();
                GUI.DragWindow(new Rect(0, 0, _windowMenu.width - 20, 20));  //allow to drag de window only at the top
            }

            //don't draw anything if it's minimised
            if (_isMinimised)
            {
                return;
            }
            _methodToCall();
            PreventClicGoThought(Event.current);
        }

        public void PreventClicGoThought(Event current)
        {
            if ((current.type == EventType.MouseDrag || current.type == EventType.MouseDown)
                && current.button == 0)
            {
                Event.current.Use();
            }
        }
    }
}
ExtGUI.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

using System;
using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using static UnityEditor.EditorGUILayout;

/// <summary>
/// all of this function must be called in OnSceneGUI
/// </summary>
public static class ExtGUI
{
    /// <summary>
    /// This function have to be called at the end of your GUI.Window() function.
    /// It eat the clic event, so it doesn't propagate thought things below.
    /// </summary>
    /// <param name="current"></param>
    public static void PreventClicGoThought(Event current)
    {
        if ((current.type == EventType.MouseDrag || current.type == EventType.MouseDown)
            && current.button == 0)
        {
            Event.current.Use();
        }
    }

}

Then you have to put the 5 following extensions in a Script/ directory

ExtDrawGuizmos.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

using UnityEngine;
using System.Collections;
using System.Reflection;

/// <summary>
/// Debug Extension
/// 	- Static class that extends Unity's debugging functionallity.
/// 	- Attempts to mimic Unity's existing debugging behaviour for ease-of-use.
/// 	- Includes gizmo drawing methods for less memory-intensive debug visualization.
/// </summary>

public static class ExtDrawGuizmos
{
	/// <summary>
	/// 	- Debugs a wire sphere.
	/// </summary>
	/// <param name='position'>
	/// 	- The position of the center of the sphere.
	/// </param>
	/// <param name='color'>
	/// 	- The color of the sphere.
	/// </param>
	/// <param name='radius'>
	/// 	- The radius of the sphere.
	/// </param>
	/// <param name='duration'>
	/// 	- How long to draw the sphere.
	/// </param>
	/// <param name='depthTest'>
	/// 	- Whether or not the sphere should be faded when behind other objects.
	/// </param>
	public static void DebugWireSphere(Vector3 position, Color color, float radius = 1.0f, float duration = 0, bool depthTest = true)
	{
		float angle = 10.0f;
		
		Vector3 x = new Vector3(position.x, position.y + radius * Mathf.Sin(0), position.z + radius * Mathf.Cos(0));
		Vector3 y = new Vector3(position.x + radius * Mathf.Cos(0), position.y, position.z + radius * Mathf.Sin(0));
		Vector3 z = new Vector3(position.x + radius * Mathf.Cos(0), position.y + radius * Mathf.Sin(0), position.z);
		
		Vector3 new_x;
		Vector3 new_y;
		Vector3 new_z;
		
		for(int i = 1; i < 37; i++){
			
			new_x = new Vector3(position.x, position.y + radius * Mathf.Sin(angle*i*Mathf.Deg2Rad), position.z + radius * Mathf.Cos(angle*i*Mathf.Deg2Rad));
			new_y = new Vector3(position.x + radius * Mathf.Cos(angle*i*Mathf.Deg2Rad), position.y, position.z + radius * Mathf.Sin(angle*i*Mathf.Deg2Rad));
			new_z = new Vector3(position.x + radius * Mathf.Cos(angle*i*Mathf.Deg2Rad), position.y + radius * Mathf.Sin(angle*i*Mathf.Deg2Rad), position.z);
			
			Debug.DrawLine(x, new_x, color, duration, depthTest);
			Debug.DrawLine(y, new_y, color, duration, depthTest);
			Debug.DrawLine(z, new_z, color, duration, depthTest);
		
			x = new_x;
			y = new_y;
			z = new_z;
		}
	}

    public static void DebugCross(Transform transform, float lenght = 1, float duration = 1)
    {
        Debug.DrawRay(transform.position, transform.up * lenght, Color.green);
        Debug.DrawRay(transform.position, transform.forward * lenght, Color.blue);
        Debug.DrawRay(transform.transform.position, transform.right * lenght, Color.red);
    }

    public static void DebugCross(Vector3 position, Quaternion rotation, float lenght = 1, float duration = 1)
    {
        Debug.DrawRay(position, rotation * Vector3.up * lenght, Color.green);
        Debug.DrawRay(position, rotation * Vector3.forward * lenght, Color.blue);
        Debug.DrawRay(position, rotation * Vector3.right * lenght, Color.red);
    }
}
ExtGameObject.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using System.Linq;
using System;
using UnityEngine.UI;
using TMPro;
public static class ExtGameObject
{
    /// <summary>
    /// create a gameObject, with a set of components
    /// ExtGameObject.CreateGameObject("game object name", Transform parent, Vector3.zero, Quaternion.identity, Vector3 Vector.One, Component [] components)
    /// set null at components if no component to add
    /// return the created gameObject
    /// </summary>
    public static GameObject CreateLocalGameObject(string name,
        Transform parent,
        Vector3 localPosition,
        Quaternion localRotation,
        Vector3 localScale)
    {
        GameObject newObject = new GameObject(name);
        //newObject.SetActive(true);
        newObject.transform.SetParent(parent);
        newObject.transform.SetAsLastSibling();
        newObject.transform.localPosition = localPosition;
        newObject.transform.localRotation = localRotation;
        newObject.transform.localScale = localScale;

        return (newObject);
    }

}
ExtQuaternion.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

using System.Collections;
using System.Collections.Generic;
using UnityEngine;

public static class ExtQuaternion
{
    /// <summary>
    /// get an array of rotation from a list of transform
    /// </summary>
    public static Quaternion[] GetAllRotation(Transform[] rotations)
    {
        Quaternion[] array = new Quaternion[rotations.Length];
        for (int i = 0; i < rotations.Length; i++)
        {
            array[i] = rotations[i].rotation;
        }
        return (array);
    }

    // assuming qArray.Length > 1
    public static Quaternion AverageQuaternion(Quaternion[] qArray)
    {
        Quaternion qAvg = qArray[0];
        float weight;
        for (int i = 1; i < qArray.Length; i++)
        {
            weight = 1.0f / (float)(i + 1);
            qAvg = Quaternion.Slerp(qAvg, qArray[i], weight);
        }
        return qAvg;
    }
}
ExtTransform.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

using System;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;

public static class ExtTransform
{
    /// <summary>
    /// Remove every childs in a transform
    /// use: someTransform.ClearChild();
    /// someTransform.ClearChild(true); //can destroy immediatly in editor
    /// </summary>
    public static Transform ClearChild(this Transform transform, bool immediate = false)
	{
        if (Application.isPlaying)
        {
            foreach (Transform child in transform)
            {
                GameObject.Destroy(child.gameObject);
            }
        }
#if UNITY_EDITOR
        else if (immediate)
        {
            if (UnityEditor.PrefabUtility.IsPartOfPrefabInstance(transform))
            {
                Debug.Log("here we are in a prefabs !!!!!");
                GameObject rootPrefabs = UnityEditor.PrefabUtility.GetOutermostPrefabInstanceRoot(transform);


                if (rootPrefabs == null)
                {
                    return (transform);
                }
                string pathRootPrefabs = UnityEditor.PrefabUtility.GetPrefabAssetPathOfNearestInstanceRoot(rootPrefabs);
                Debug.Log(pathRootPrefabs);

                UnityEditor.PrefabUtility.UnpackPrefabInstanceAndReturnNewOutermostRoots(rootPrefabs, UnityEditor.PrefabUnpackMode.Completely);

                var tempList = transform.Cast<Transform>().ToList();
                foreach (var child in tempList)
                {
                    GameObject.DestroyImmediate(child.gameObject);
                }
                UnityEditor.PrefabUtility.SaveAsPrefabAssetAndConnect(rootPrefabs, pathRootPrefabs, UnityEditor.InteractionMode.UserAction);
            }
            else
            {
                var tempList = transform.Cast<Transform>().ToList();
                foreach (var child in tempList)
                {
                    GameObject.DestroyImmediate(child.gameObject);
                }
            }
        }
#endif
        return (transform);
	}

    /// <summary>
    /// return a list of all child in a transform (not recursive)
    /// use: List<Transform>() newList = someTransform.GetAllChild();
    /// </summary>
    /// <param name="transform"></param>
    /// <returns></returns>
    public static List<Transform> GetAllChild(this Transform transform)
    {
        List<Transform> allChild = new List<Transform>();
        foreach (Transform child in transform)
        {
            allChild.Add(child);
        }
        return (allChild);
    }
}
ExtVector3.cs
Download
Copy
/// <summary>
/// MIT License - Copyright(c) 2019 Ugo Belfiore
/// </summary>

using System;
using UnityEngine;
using System.Linq;
using System.Collections.Generic;

public static class ExtVector3
{
    /// <summary>
    /// return the middle of X Transform
    /// </summary>
    public static Vector3 GetMeanOfXPoints(Transform[] arrayTransform, bool middleBoundingBox = true)
    {
        Vector3[] arrayVect = new Vector3[arrayTransform.Length];
        for (int i = 0; i < arrayTransform.Length; i++)
        {
            arrayVect[i] = arrayTransform[i].position;
        }
        return (GetMiddleOfXPoints(arrayVect, middleBoundingBox));
    }

    /// <summary>
    /// return the middle of X points (POINTS, NOT vector)
    /// </summary>
    public static Vector3 GetMiddleOfXPoints(Vector3[] arrayVect, bool middleBoundingBox = true)
    {
        if (arrayVect.Length == 0)
            return (Vector3.zero);

        if (!middleBoundingBox)
        {
            Vector3 sum = Vector3.zero;
            for (int i = 0; i < arrayVect.Length; i++)
            {
                sum += arrayVect[i];
            }
            return (sum / arrayVect.Length);
        }
        else
        {
            if (arrayVect.Length == 1)
                return (arrayVect[0]);

            float xMin = arrayVect[0].x;
            float yMin = arrayVect[0].y;
            float zMin = arrayVect[0].z;
            float xMax = arrayVect[0].x;
            float yMax = arrayVect[0].y;
            float zMax = arrayVect[0].z;

            for (int i = 1; i < arrayVect.Length; i++)
            {
                if (arrayVect[i].x < xMin)
                    xMin = arrayVect[i].x;
                if (arrayVect[i].x > xMax)
                    xMax = arrayVect[i].x;

                if (arrayVect[i].y < yMin)
                    yMin = arrayVect[i].y;
                if (arrayVect[i].y > yMax)
                    yMax = arrayVect[i].y;

                if (arrayVect[i].z < zMin)
                    zMin = arrayVect[i].z;
                if (arrayVect[i].z > zMax)
                    zMax = arrayVect[i].z;
            }
            Vector3 lastMiddle = new Vector3((xMin + xMax) / 2, (yMin + yMax) / 2, (zMin + zMax) / 2);
            return (lastMiddle);
        }
    }
}

How it work:

the most interesting part of the calculations are inside the script

LockChildren.cs
. All the other scripts are tools and extensions made to make it work.

if you want to have more information on how to create an extension for a unity component, see the article bellow to have a simple exemple explained.

The Moving and Rotating part is pretty staight forward:

  • First you save the current position and rotation of all children of the selected gameObject
  • Then when you move the parent, you assign to the children their previous position & rotation. It work because these 2 properties are in world coordinates.

It became more complicated with the scaling part thought, because every child have its scale relative to their parent, and the global scale of an gameObject is hard to calculate, because of the rotation problem.

The "global scale" given by unity is called "lossy scale", and it is not something to trust if we want to manage rotation.

What I did, is create ghost: when I am in the lock state, I create an invisible gameObject, thanks to that flag:

gameObject.hideFlags = HideFlags.HideInHierarchy;

This new gameObject will be the ghost parent, at the same position, rotation and scale has the selected gameObject. Then I create ghost children for each children inside the parent, again, with the same position, rotation and scale. To understand better, here is an exemple with the ghost visible:

When I am scaling the parent, here is what I do to children:

  • Move the child inside the ghost parent
  • Keep the localScale of the child at the same value as the ghost child, the one at the same sibling index compared to their respective parents
  • Move the child again to its original parent.
Of course, I took great care not to leave behind invisible objects in the scene.
Inside a prefabs, the scaling part doen't work, because we can't move gameObjects outside a prefabs.









See also:

Enhance any built-in components of unity

Add behavior of any unity components using customs inspectors and reflection


Switch Camera Projection

Swap the Projection camera from Perspective to Orthographic using a linear interpolation function coupled with an easing function


EditorOnly Components and Actions when building

This article show how to remove components you don't want in build while keeping them in gameObjects, and how to execute actions when you are building your application