2022-09-06 13:47:45 +08:00

852 lines
27 KiB
C#

// =====================================================================
// 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);
/// <summary>
/// Base Node class
/// </summary>
public class DTInspectorNode : IComparable
{
public static bool IsInsideInspector { get; set; }
public event DTInspectorNodeEvent OnNodeRender;
public enum RenderAsEnum { Invisible, Section, TabBar, Tab, Field };
/// <summary>
/// How to render this node
/// </summary>
public RenderAsEnum RenderAs = RenderAsEnum.Field;
/// <summary>
/// Gets a list of actions of this node
/// </summary>
public List<ActionAttribute> Actions = new List<ActionAttribute>();
/// <summary>
/// Parent node
/// </summary>
public DTInspectorNode Parent;
/// <summary>
/// Child nodes
/// </summary>
public List<DTInspectorNode> Items = new List<DTInspectorNode>();
/// <summary>
/// Gets the number of child nodes
/// </summary>
public int Count { get { return Items.Count; } }
/// <summary>
/// Gets all attributes for this node
/// </summary>
public List<Attribute> Attributes;
/// <summary>
/// Node name
/// </summary>
public string Name { get; set; }
/// <summary>
/// Whether the node is expanded/visible or not
/// </summary>
public virtual bool Expanded { get; set; }
/// <summary>
/// Whether this node needs a repaint
/// </summary>
public virtual bool NeedRepaint { get; set; }
/// <summary>
/// Whether child nodes are visible or not
/// </summary>
public virtual bool ContentVisible
{
get { return mContentVisible; }
set { mContentVisible = value; }
}
/// <summary>
/// Whether this node is visible or not
/// </summary>
public virtual bool Visible
{
get { return mVisible; }
set { mVisible = value; }
}
/// <summary>
/// Whether this node is disabled or not
/// </summary>
public virtual bool Disabled { get; set; }
/// <summary>
/// The serializedObject currently processed
/// </summary>
public SerializedObject serializedObject { get; set; }
/// <summary>
/// Label/Icon/Tooltip of this node
/// </summary>
public GUIContent GUIContent { get; set; }
/// <summary>
/// The absolute Path of this node
/// </summary>
public string Path
{
get
{
return (Parent) ? Parent.Path + "/" + Name : Name;
}
}
/// <summary>
/// The nesting Level of this node
/// </summary>
public int Level { get; private set; }
/// <summary>
/// Sort order
/// </summary>
public int SortOrder { get; set; }
/// <summary>
/// Gets a child node by it's index
/// </summary>
/// <param name="index">index of the child</param>
/// <returns>a node or null</returns>
public DTInspectorNode this[int index]
{
get
{
return Items[index];
}
}
/// <summary>
/// Gets a child node by it's name
/// </summary>
/// <param name="name">name of the child</param>
/// <returns>a node or null</returns>
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);
}
/// <summary>
/// Clear everything the node contains
/// </summary>
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;
}
/// <summary>
/// Get a certain attribute
/// </summary>
/// <typeparam name="T">Type of the attribute</typeparam>
/// <returns>the attribute or null</returns>
public T GetAttribute<T>() where T : Attribute
{
return Attributes.Find(x => x.GetType().IsAssignableFrom(typeof(T))) as T;
}
/// <summary>
/// Gets all attributes
/// </summary>
/// <typeparam name="T">Type of the attribute</typeparam>
/// <returns>all attributes</returns>
public T[] GetAllAttributes<T>() where T : Attribute
{
List<Attribute> 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);
}
/// <summary>
/// Search for a node at a specific path
/// </summary>
/// <typeparam name="T">Type of the node in question</typeparam>
/// <param name="name">Path and Name of the node in question</param>
/// <param name="node">The search result or null</param>
/// <returns>true if found</returns>
public bool FindNodeAt<T>(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;
}
/// <summary>
/// Search all child nodes for a node of the given type with a certain name
/// </summary>
/// <typeparam name="T">Type of the node in question</typeparam>
/// <param name="name">Name of the node in question</param>
/// <param name="node">The search result or null</param>
/// <returns>true if found</returns>
public bool FindNode<T>(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;
}
/// <summary>
/// Sort child nodes
/// </summary>
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);
}
}
/// <summary>
/// Field Node class
/// </summary>
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);
}
}
}
}
}
/// <summary>
/// Group node class
/// </summary>
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<Attribute>();
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<string, bool> _states = new Dictionary<string, bool>();
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
}