// ===================================================================== // Copyright 2013-2017 Fluffy Underware // All rights reserved // // http://www.fluffyunderware.com // ===================================================================== using UnityEngine; using UnityEditor; using System; using FluffyUnderware.DevTools; using FluffyUnderware.DevTools.Extensions; using FluffyUnderware.DevToolsEditor.Extensions; using System.Collections.Generic; using UnityEditor.AnimatedValues; using UnityEditorInternal; namespace FluffyUnderware.DevToolsEditor { public delegate void DTInspectorNodeEvent(DTInspectorNode e); /// /// Base Node class /// public class DTInspectorNode : IComparable { public static bool IsInsideInspector { get; set; } public event DTInspectorNodeEvent OnNodeRender; public enum RenderAsEnum { Invisible, Section, TabBar, Tab, Field }; /// /// How to render this node /// public RenderAsEnum RenderAs = RenderAsEnum.Field; /// /// Gets a list of actions of this node /// public List Actions = new List(); /// /// Parent node /// public DTInspectorNode Parent; /// /// Child nodes /// public List Items = new List(); /// /// Gets the number of child nodes /// public int Count { get { return Items.Count; } } /// /// Gets all attributes for this node /// public List Attributes; /// /// Node name /// public string Name { get; set; } /// /// Whether the node is expanded/visible or not /// public virtual bool Expanded { get; set; } /// /// Whether this node needs a repaint /// public virtual bool NeedRepaint { get; set; } /// /// Whether child nodes are visible or not /// public virtual bool ContentVisible { get { return mContentVisible; } set { mContentVisible = value; } } /// /// Whether this node is visible or not /// public virtual bool Visible { get { return mVisible; } set { mVisible = value; } } /// /// Whether this node is disabled or not /// public virtual bool Disabled { get; set; } /// /// The serializedObject currently processed /// public SerializedObject serializedObject { get; set; } /// /// Label/Icon/Tooltip of this node /// public GUIContent GUIContent { get; set; } /// /// The absolute Path of this node /// public string Path { get { return (Parent) ? Parent.Path + "/" + Name : Name; } } /// /// The nesting Level of this node /// public int Level { get; private set; } /// /// Sort order /// public int SortOrder { get; set; } /// /// Gets a child node by it's index /// /// index of the child /// a node or null public DTInspectorNode this[int index] { get { return Items[index]; } } /// /// Gets a child node by it's name /// /// name of the child /// a node or null public DTInspectorNode this[string name] { get { return Items.Find(x => x.Name.Equals(name, StringComparison.CurrentCultureIgnoreCase)); } } public int Index { get; protected set; } bool mContentVisible = true; bool mVisible = true; protected DTInspectorNode(string name) { SortOrder = 100; Name = name; } public virtual int CompareTo(object obj) { DTInspectorNode other = (DTInspectorNode)obj; int c = SortOrder.CompareTo(other.SortOrder); return (c != 0) ? c : Index.CompareTo(other.Index); } /// /// Clear everything the node contains /// public virtual void Clear() { for (int i = 0; i < Items.Count; i++) Items[i].Clear(); Items.Clear(); } public virtual DTInspectorNode Add(DTInspectorNode node) { if (node != null) { Items.Add(node); node.Index = Items.IndexOf(node); node.Parent = this; node.Level = node.Parent.Level + 1; } return node; } public virtual void Calculate(bool firstRun = false) { } public void Delete() { foreach (DTInspectorNode it in Items) it.Delete(); if (Parent) { Parent.Items.Remove(this); Parent.Sort(); // to reset Index } Parent = null; } /// /// Get a certain attribute /// /// Type of the attribute /// the attribute or null public T GetAttribute() where T : Attribute { return Attributes.Find(x => x.GetType().IsAssignableFrom(typeof(T))) as T; } /// /// Gets all attributes /// /// Type of the attribute /// all attributes public T[] GetAllAttributes() where T : Attribute { List res = Attributes.FindAll(x => x.GetType().IsAssignableFrom(typeof(T))); T[] r = new T[res.Count]; for (int i = 0; i < res.Count; i++) r[i] = (T)res[i]; return r; } public static implicit operator bool(DTInspectorNode a) { return !object.ReferenceEquals(a, null); } /// /// Search for a node at a specific path /// /// Type of the node in question /// Path and Name of the node in question /// The search result or null /// true if found public bool FindNodeAt(string pathAndName, out T node) where T : DTInspectorNode { string[] p = pathAndName.Split('/'); node = null; DTInspectorNode N = this; for (int i = 0; i < p.Length; i++) { N = N[p[i]]; if (N == null) return false; } node = N as T; return node != null; } /// /// Search all child nodes for a node of the given type with a certain name /// /// Type of the node in question /// Name of the node in question /// The search result or null /// true if found public bool FindNode(string name, out T node) where T : DTInspectorNode { node = this[name] as T; if (node != null) return true; else { foreach (DTInspectorNode it in Items) if (it.FindNode(name, out node)) return true; } return false; } /// /// Sort child nodes /// public void Sort() { Items.Sort(); foreach (DTInspectorNode it in Items) { it.Index = Items.IndexOf(it); if (it is DTGroupNode) it.Sort(); } } internal void raiseOnRender() { if (OnNodeRender != null) OnNodeRender(this); } } /// /// Field Node class /// public class DTFieldNode : DTInspectorNode { public string SerializedPropertyPath; public SerializedProperty serializedProperty { get; set; } public bool IncludeChildren { get; set; } public ReorderableList ArrayEx { get; set; } public string Tooltip { get; set; } public string HelpURL { get; set; } public DTFieldNode(SerializedProperty property) : base(property.name) { RenderAs = RenderAsEnum.Field; SerializedPropertyPath = property.propertyPath; Attributes = property.GetAttributes(typeof(IDTFieldRenderAttribute)); Attributes.Sort(); GUIContent = new GUIContent(ObjectNames.NicifyVariableName(Name)); Calculate(true); } public override void Calculate(bool firstRun = false) { IncludeChildren = true; Visible = true; Disabled = false; Actions.Clear(); foreach (IDTFieldRenderAttribute a in Attributes) { if (a is Hide || a is AsGroupAttribute) { Visible = false; ContentVisible = false; IncludeChildren = false; } else if (a is Inline) { Visible = false; IncludeChildren = true; } else if (serializedProperty != null) { if (a is FieldConditionAttribute) { FieldConditionAttribute condA = (FieldConditionAttribute)a; bool met = condA.ConditionMet(serializedProperty.serializedObject.targetObject); switch (condA.Action) { case ConditionalAttribute.ActionEnum.Show: Visible = met; break; case ConditionalAttribute.ActionEnum.Hide: Visible = !met; break; case ConditionalAttribute.ActionEnum.Enable: Disabled = !met; break; case ConditionalAttribute.ActionEnum.Disable: Disabled = met; break; default: if (met) Actions.Add(condA); break; } } else if (a is FieldActionAttribute) { Actions.Add((FieldActionAttribute)a); } } } } } /// /// Group node class /// public class DTGroupNode : DTInspectorNode { static internal SerializedObject _serializedObject; public override bool Expanded { get { return mState.target; } set { if (mState.target != value) { mState.target = value; if (serializedObject != null) DTPersistentState.SetBool(serializedObject.targetObject.GetInstanceID().ToString() + Path, value); NeedRepaint = true; } } } public float ExpandedFaded { get { return mState.faded; } } public override bool ContentVisible { get { return base.ContentVisible && ExpandedFaded != 0; } set { base.ContentVisible = value; } } public override bool NeedRepaint { get { return mState.isAnimating; } } public string HelpURL { get; set; } public bool Fixed { get; set; } #region --- Tab Bar Properties --- public int SelectedIndex { get { return Count > 0 ? Mathf.Clamp(mSelectedItem, 0, Count - 1) : -1; } set { mSelectedItem = Count > 0 ? Mathf.Clamp(value, 0, Count - 1) : -1; } } public DTInspectorNode SelectedItem { get { return (SelectedIndex != -1) ? Items[SelectedIndex] : null; } set { SelectedIndex = (value == null) ? -1 : value.Index; } } public int MaxItemsPerRow { get; set; } #endregion AnimBool mState = new AnimBool(true); int mSelectedItem = -1; public DTGroupNode(string name, SerializedProperty forProperty = null, RenderAsEnum renderAs = RenderAsEnum.Section) : base(name) { RenderAs = renderAs; MaxItemsPerRow = -1; if (forProperty != null) { Attributes = forProperty.GetAttributes(typeof(IDTGroupRenderAttribute)); Attributes.Sort(); Calculate(true); } else { Attributes = new List(); GUIContent = new GUIContent(ObjectNames.NicifyVariableName(Name)); } } public override void Calculate(bool firstRun = false) { Actions.Clear(); foreach (IDTGroupRenderAttribute a in Attributes) { if (a is GroupAttribute) { GroupAttribute groupA = (GroupAttribute)a; if (groupA.Invisible) RenderAs = RenderAsEnum.Invisible; if (firstRun) { mState.value = groupA.Expanded; getAdditionalGroupParams(groupA); } } else if (serializedObject != null) { if (a is GroupConditionAttribute) { GroupConditionAttribute condA = (GroupConditionAttribute)a; bool met = condA.ConditionMet(serializedObject.targetObject); switch (condA.Action) { case ConditionalAttribute.ActionEnum.Show: Visible = met; break; case ConditionalAttribute.ActionEnum.Hide: Visible = !met; break; case ConditionalAttribute.ActionEnum.Enable: Disabled = !met; break; case ConditionalAttribute.ActionEnum.Disable: Disabled = met; break; default: if (met) Actions.Add(condA); break; } } else if (a is GroupActionAttribute) { Actions.Add((GroupActionAttribute)a); } } } } void getAdditionalGroupParams(GroupAttribute a) { if (a == null) { GUIContent = new GUIContent(ObjectNames.NicifyVariableName(Name)); } else { GUIContent = new GUIContent(a.Label ?? ObjectNames.NicifyVariableName(Name), a.Tooltip); HelpURL = a.HelpURL; } } public DTGroupNode EnsurePath(string path, bool includesName = false, SerializedProperty forProperty = null) { DTGroupNode node = this; if (!string.IsNullOrEmpty(path)) { string[] p = path.Split('/'); int hi = (includesName) ? p.Length - 1 : p.Length; for (int i = 0; i < hi; i++) { DTGroupNode sub = node[p[i]] as DTGroupNode; if (sub == null) { if (forProperty != null && i == hi - 1) sub = new DTGroupNode(p[i], forProperty); else sub = new DTGroupNode(p[i]); node.Add(sub); } node = sub; } } return node; } public GUIContent[] GetItemsGUIContent() { GUIContent[] res = new GUIContent[Count]; for (int i = 0; i < Count; i++) res[i] = this[i].GUIContent; return res; } public DTGroupNode FindTabBarAt(string nameAndPath) { DTGroupNode node; if (FindNodeAt(nameAndPath, out node)) if (node.RenderAs == RenderAsEnum.TabBar) return node; return null; } public override DTInspectorNode Add(DTInspectorNode node) { node = base.Add(node); DTGroupNode grp = node as DTGroupNode; if (grp != null && _serializedObject != null && _serializedObject.targetObject != null) grp.mState.value = DTPersistentState.GetBool(_serializedObject.targetObject.GetInstanceID().ToString() + grp.Path, grp.mState.value); return node; } public DTGroupNode AddSection(string name, DTInspectorNodeEvent func) { DTInspectorNode item = this[name]; if (item) { if (item.RenderAs != RenderAsEnum.Section) return null; } else { item = new DTGroupNode(name, null, RenderAsEnum.Section); Add(item); item.OnNodeRender += func; } return item as DTGroupNode; } public DTGroupNode AddTab(string name, DTInspectorNodeEvent func) { if (RenderAs == RenderAsEnum.TabBar) { DTInspectorNode item = this[name]; if (item) { if (item.RenderAs != RenderAsEnum.Tab) return null; } else { item = new DTGroupNode(name, null, RenderAsEnum.Tab); Add(item); item.OnNodeRender += func; } return item as DTGroupNode; } return null; } } public static class DTPersistentState { static Dictionary _states = new Dictionary(); public static bool GetBool(string ident, bool defaultState = true) { bool res; if (!_states.TryGetValue(ident, out res)) { res = defaultState; _states.Add(ident, defaultState); } return res; } public static void SetBool(string ident, bool state) { if (!_states.ContainsKey(ident)) _states.Add(ident, state); else _states[ident] = state; } } #region ### Renderer ### public interface IDTInspectorNodeRenderer { void RenderTabBarHeader(DTGroupNode node, int maxItemsPerRow); void RenderTabBarFooter(DTGroupNode node); void RenderSectionHeader(DTGroupNode node); void RenderSectionFooter(DTGroupNode node); void RenderField(DTFieldNode node); void RenderAction(DTInspectorNode node, ActionAttribute action, System.Object editorObject, System.Object targetObject); } public class DTInspectorNodeDefaultRenderer : IDTInspectorNodeRenderer { public static Texture HelpIcon { get { if (mHelpIcon == null) mHelpIcon = (Texture)EditorGUIUtility.Load("icons/_Help.png"); return mHelpIcon; } } static Texture mHelpIcon; public static GUIStyle BoldFoldout { get { if (mBoldFoldout == null) { mBoldFoldout = new GUIStyle(EditorStyles.foldout); mBoldFoldout.fontStyle = FontStyle.Bold; mBoldFoldout.margin.top += 2; mBoldFoldout.margin.bottom += 4; } return mBoldFoldout; } } static GUIStyle mBoldFoldout; public static GUIStyle TabbarButton { get { if (mTabbarButton == null) { mTabbarButton = new GUIStyle(EditorStyles.toolbarButton); mTabbarButton.alignment = TextAnchor.MiddleCenter; GUIStyle skinButton = GUI.skin.button; RectOffset skinButtonPadding = skinButton.padding; RectOffset skinButtonMargin = skinButton.margin; RectOffset skinButtonBorder = skinButton.border; RectOffset skinButtonOverflow = skinButton.overflow; mTabbarButton.padding = new RectOffset(skinButtonPadding.left, skinButtonPadding.right, skinButtonPadding.top, skinButtonPadding.bottom); mTabbarButton.margin = new RectOffset(skinButtonMargin.left, skinButtonMargin.right, skinButtonMargin.top, skinButtonMargin.bottom); mTabbarButton.border = new RectOffset(skinButtonBorder.left, skinButtonBorder.right, skinButtonBorder.top, skinButtonBorder.bottom); mTabbarButton.overflow = new RectOffset(skinButtonOverflow.left, skinButtonOverflow.right, skinButtonOverflow.top, skinButtonOverflow.bottom); } return mTabbarButton; } } static GUIStyle mTabbarButton; public virtual void RenderAction(DTInspectorNode node, ActionAttribute action, System.Object editorObject, System.Object targetObject) { switch (action.Action) { case ActionAttribute.ActionEnum.ShowInfo: EditorGUILayout.HelpBox(action.ActionData as string, MessageType.Info); return; case ActionAttribute.ActionEnum.ShowWarning: EditorGUILayout.HelpBox(action.ActionData as string, MessageType.Warning); return; case ActionAttribute.ActionEnum.ShowError: EditorGUILayout.HelpBox(action.ActionData as string, MessageType.Error); return; case ActionAttribute.ActionEnum.Callback: action.Callback(editorObject); return; } } public virtual void RenderField(DTFieldNode node) { EditorGUILayout.PropertyField(node.serializedProperty, node.IncludeChildren); } public virtual void RenderSectionHeader(DTGroupNode node) { GUILayout.Space(10); string helpUrl = node.HelpURL; Rect controlRect = EditorGUILayout.GetControlRect(false, 16); int xOffset = (node.Level <= 1 && DTInspectorNode.IsInsideInspector) ? 12 : 0; controlRect.x -= xOffset; bool toggleState = node.Expanded; int indentLevel = RenderHeader(controlRect, xOffset, helpUrl, node.GUIContent, ref toggleState); node.Expanded = toggleState; EditorGUILayout.BeginFadeGroup(node.ExpandedFaded); EditorGUI.indentLevel = (node.Level <= 1) ? indentLevel : indentLevel + 1; } #pragma warning disable 162 #pragma warning disable 429 private static readonly Color SectionHeaderBackgroundColor = DTEditorUtility.UsesNewEditorUI ? new Color(1f, 1f, 1f) : new Color(0.8f, 0.8f, 0.8f); #pragma warning restore 429 #pragma warning restore 162 public static int RenderHeader(Rect controlRect, int xOffset, string helpUrl, GUIContent toggleGuiContent, ref bool toggleState) { int indentLevel = EditorGUI.indentLevel; EditorGUI.indentLevel = Mathf.Max(0, indentLevel - 1); controlRect = EditorGUI.IndentedRect(controlRect); DTGUI.PushColor(SectionHeaderBackgroundColor); const int additionalHeight = 4; Rect boxRectangle = new Rect(controlRect.x, controlRect.y - 2, controlRect.width + xOffset + 1, controlRect.height + additionalHeight); GUI.Box(boxRectangle, "", GUI.skin.box); DTHandles.DrawOutline(boxRectangle, Color.black); DTGUI.PopColor(); Rect expandClickableArea = new Rect(controlRect); bool hasHelp = !string.IsNullOrEmpty(helpUrl); if (hasHelp) expandClickableArea.width -= 12; toggleState = GUI.Toggle(expandClickableArea, toggleState, toggleGuiContent, BoldFoldout); if (hasHelp) { if (GUI.Button(new Rect(boxRectangle.xMax - 20, boxRectangle.y + 3, 16, 16) , new GUIContent(HelpIcon, "Help"), new GUIStyle())) Application.OpenURL(helpUrl); } GUILayout.Space(additionalHeight); return indentLevel; } public virtual void RenderSectionFooter(DTGroupNode node) { if (node.Level > 1) EditorGUI.indentLevel--; EditorGUILayout.EndFadeGroup(); } public virtual void RenderTabBarHeader(DTGroupNode node, int maxItemsPerRow) { GUILayout.Space(4); int s = GUILayout.SelectionGrid(node.SelectedIndex, node.GetItemsGUIContent(), Mathf.Min(node.Count, maxItemsPerRow), TabbarButton); if (s != node.SelectedIndex) { node.SelectedIndex = s; GUIUtility.keyboardControl = 0; } Color c = GUI.color; if (!EditorGUIUtility.isProSkin) GUI.color = new Color(c.r, c.g, c.b, 0.5f); GUILayout.BeginVertical(GUI.skin.box); GUI.color = c; } public virtual void RenderTabBarFooter(DTGroupNode node) { GUILayout.EndVertical(); } } #endregion }