/// /// MIT License - Copyright(c) 2019 Ugo Belfiore /// 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 { /// /// all the virtual function at your disposal /// #region virtual functions /// /// called when the inspector is enabled /// public virtual void OnCustomEnable() { } /// /// called when the inspector is disabled /// public virtual void OnCustomDisable() { } /// /// called every OnInspectorGUI inside the herited class /// protected virtual void OnCustomInspectorGUI() { } /// /// called at the first OnInspectorGUI inside the herited class /// protected virtual void InitOnFirstInspectorGUI() { } /// /// called at the first OnSceneGUI inside the herited class /// /// current drawing scene view protected virtual void CustomOnSceneGUI(SceneView sceneview) { } /// /// called at the first OnSceneGUI inside the herited class /// /// current drawing scene view protected virtual void InitOnFirstOnSceneGUI(SceneView sceneview) { } /// /// called every editor Update inside the herited class /// protected virtual void OnEditorUpdate() { } /// /// called at the first Editor Update inside the herited class /// protected virtual void InitOnFirstEditorUpdate() { } public virtual void ShowTinyEditorContent() { } protected virtual void OnHierarchyChanged() { } #endregion /// /// some private variable /// #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 _decoratedMethods = new Dictionary(); private Assembly _editorAssembly = Assembly.GetAssembly(typeof(Editor)); /// /// get editor instance /// /// editor instance protected Editor GetEditorInstance() { return _editorInstance; } #endregion /// /// all the required setup and initialisation /// #region initialisation editor public DecoratorComponentsEditor(bool showExtension = false, string tinyEditorName = "") { SetupDecoratorSettings(showExtension, tinyEditorName, true, ""); } /// /// constructor of the class: here we initialize reflexion variables /// /// name of the component we inspect 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)); } } /// /// need to be called here to initialize the default editor /// private void OnEnable() { SetupEditor(); InitTargets(); OnCustomEnable(); } /// /// need to destroy the customEditor when quitting ! /// 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() 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() 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(int index) where T : Component { if (ConcretTargets == null || ConcretTargets.Length == 0 || index < 0 || index >= ConcretTargets.Length) { return (default(T)); } return ((T)ConcretTargets[index]); } /// /// return true if we have still the reference of the editor, but no more target... /// /// 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)); } /// /// Need to destroy the instance even on Destroy ! /// I don't know why. Unity generate leaks after each compilation /// because of the CreateEditor /// private void OnDestroy() { if (_editorInstance != null) { DestroyImmediate(_editorInstance); } //SceneView.duringSceneGui -= OwnOnSceneGUI; //EditorApplication.update -= OnEditorApplicationUpdate; } /// /// here create the editor instance, and open / close the extension panel if needed /// private void SetupEditor() { if (_editorInstance != null) { return; } _hasInitSceneGUI = false; _hasInitEditorUpdate = false; _hasInitInspector = false; CreateDefaultInstance(); OpenOrCloseExtension(); } /// /// here create the editor instance /// private void CreateDefaultInstance() { if (_decoratedEditorType == null || targets == null) { return; } _editorInstance = Editor.CreateEditor(targets, _decoratedEditorType); } /// /// manage if the extensions are opened or closed, and save this setting in EditorPrefs /// 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); } } /// /// iniitalisation of reflexion variables /// 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; } /// /// from a given type of editor, get his CustomEditor as type /// /// /// 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; } /// /// From a given name of class, invoke it using reflexion /// 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 /// /// The core loop function for display /// everything /// #region Display Extension /// /// 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() /// 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(); } /// /// called whatever this editor is inherited from a custom unity component, /// or just a standar sutom editor /// private void CommonExtenstionInspectorGUI() { TryToInitOnFirstInspectorGUI(); if (_showExtension && ConcretTarget != null && !IsInPrefabsStage() && CanShowExpandFromInterfaceSettings() && ExpendBehavior()) { OnCustomInspectorGUI(); } } /// /// 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 /// /// private bool CanShowExpandFromInterfaceSettings() { if (!HaveInterfaceSettings(out IHaveDecoratorOnIt haveDecorator)) { return (true); } return (haveDecorator.ShowExtension); } /// /// 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 /// /// private bool CanShowTinyEditorFromInterfaceSettings() { if (!HaveInterfaceSettings(out IHaveDecoratorOnIt haveDecorator)) { return (true); } return (haveDecorator.ShowTinyEditorWindow); } /// /// 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 /// /// private bool HaveInterfaceSettings(out IHaveDecoratorOnIt haveDecorator) { haveDecorator = null; if (!_isCustomEditor) { return (false); } if (ConcretTarget == null) { return (false); } IHaveDecoratorOnIt[] AllInterfaceDecorator = ConcretTarget.GetComponents(); 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(); SceneView.duringSceneGui -= OwnOnSceneGUI; SceneView.duringSceneGui += OwnOnSceneGUI; EditorApplication.update -= CustomEditorApplicationUpdate; EditorApplication.update += CustomEditorApplicationUpdate; EditorApplication.hierarchyChanged -= OnHierarchyChanged; EditorApplication.hierarchyChanged += OnHierarchyChanged; } } /// /// 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 /// private void TryToInitOnFirstInspectorGUI() { if (!_hasInitInspector) { InitOnFirstInspectorGUI(); _hasInitInspector = true; } } private void TryToInitOnFirstOnEditorUpdate() { if (!_hasInitEditorUpdate) { InitOnFirstEditorUpdate(); _hasInitEditorUpdate = true; } } /// /// editor update loop /// private void CustomEditorApplicationUpdate() { if (!_isCustomEditor) { if (GetEditorInstance() == null) { return; } } if (HaveWeLostTargets()) { DestroyImmediate(GetEditorInstance()); return; } if (!IsExtensionOpened()) { return; } TryToInitOnFirstOnEditorUpdate(); OnEditorUpdate(); } /// /// This function is called at each OnSceneGUI to try to initialize it /// private void TryToInitOnFirstOnSceneGUI(SceneView sceneview) { if (!_hasInitSceneGUI) { if (_showTinyEditor) { InitTinyEditorWindow(_tinyEditorName); } InitOnFirstOnSceneGUI(sceneview); _hasInitSceneGUI = true; } } /// /// return true if we are in a prefabs editing mode /// /// 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); } /// /// display the Expand behavior of the component editor /// 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 /// /// other basic unity fonction we have to manage /// #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 } }