513 lines
20 KiB
C#
513 lines
20 KiB
C#
// =====================================================================
|
|
// Copyright 2013-2017 Fluffy Underware
|
|
// All rights reserved
|
|
//
|
|
// http://www.fluffyunderware.com
|
|
// =====================================================================
|
|
|
|
using UnityEngine;
|
|
using System.Collections.Generic;
|
|
using UnityEditor;
|
|
using FluffyUnderware.DevToolsEditor.Extensions;
|
|
using FluffyUnderware.DevTools;
|
|
using System;
|
|
using UnityEditorInternal;
|
|
using UnityEditor.AnimatedValues;
|
|
using UnityEngine.Events;
|
|
using Object = UnityEngine.Object;
|
|
|
|
namespace FluffyUnderware.DevToolsEditor
|
|
{
|
|
public class DTEditor<T> : Editor where T : UnityEngine.Object
|
|
{
|
|
#region ### Public Properties ###
|
|
|
|
/// <summary>
|
|
/// Target Script
|
|
/// </summary>
|
|
public virtual T Target
|
|
{
|
|
get
|
|
{
|
|
return (target != null) ? target as T : null;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the target is currently selected or not
|
|
/// </summary>
|
|
public bool TargetIsActive
|
|
{
|
|
get
|
|
{
|
|
if (target is MonoBehaviour)
|
|
return (target != null && ((MonoBehaviour)target).transform == Selection.activeTransform) ? true : false;
|
|
else
|
|
return true;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Whether the target is part of a prefab
|
|
/// </summary>
|
|
/// <remarks>Has issues with the new prefab system. See the comments in the code that assigns IsPrefab's value to know more about this</remarks>
|
|
// TODO Has issues with the new prefab system. See the comments in the code that assigns IsPrefab's value to know more about this
|
|
[Obsolete("Will get removed in the next major version. Use Unity's PrefabUtility to know if this.Target is part of a prefab")]
|
|
public bool IsPrefab { get; private set; }
|
|
|
|
public bool IsInsideInspector { get; private set; }
|
|
|
|
|
|
/// <summary>
|
|
/// The Root node of all inspector fields
|
|
/// </summary>
|
|
public DTGroupNode Node
|
|
{
|
|
get
|
|
{
|
|
return mRootNode;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The renderer used to render inspector fields
|
|
/// </summary>
|
|
public IDTInspectorNodeRenderer NodeRenderer
|
|
{
|
|
get
|
|
{
|
|
return mNodeRenderer;
|
|
}
|
|
set
|
|
{
|
|
mNodeRenderer = value;
|
|
}
|
|
}
|
|
/// <summary>
|
|
/// Whether the inspector needs a repaint
|
|
/// </summary>
|
|
public bool NeedRepaint { get; set; }
|
|
|
|
#endregion
|
|
|
|
|
|
DTGroupNode mRootNode;
|
|
IDTInspectorNodeRenderer mNodeRenderer = new DTInspectorNodeDefaultRenderer();
|
|
bool mEnterChildren;
|
|
|
|
#region ### Public Methods ###
|
|
|
|
|
|
|
|
|
|
#endregion
|
|
|
|
|
|
#region ### Protected Methods (override to change inspector appearance) ###
|
|
|
|
protected virtual void OnEnable()
|
|
{
|
|
if (mRootNode == null)
|
|
mRootNode = new DTGroupNode("Root");
|
|
Undo.undoRedoPerformed -= OnUndoRedo;
|
|
Undo.undoRedoPerformed += OnUndoRedo;
|
|
#pragma warning disable 618
|
|
if (target != null)
|
|
{
|
|
PrefabAssetType prefabAssetType = PrefabUtility.GetPrefabAssetType(target);
|
|
//BUG in the new prefab system, an instantiated prefab will have IsPrefab == true, while the documentation of the PrefabUtility.GetPrefabAssetType says otherwise. But is fixing this worth it knowing that IsPrefab is not used in Curvy when using the new prefab system?
|
|
IsPrefab = prefabAssetType == PrefabAssetType.Regular || prefabAssetType == PrefabAssetType.Variant;
|
|
}
|
|
else
|
|
IsPrefab = false;
|
|
#pragma warning restore 618
|
|
}
|
|
|
|
protected virtual void OnDisable()
|
|
{
|
|
if (mRootNode != null)
|
|
mRootNode.Clear();
|
|
Undo.undoRedoPerformed -= OnUndoRedo;
|
|
}
|
|
|
|
protected virtual void OnReadNodes()
|
|
{
|
|
}
|
|
|
|
protected virtual void OnSceneGUI()
|
|
{
|
|
}
|
|
|
|
public override void OnInspectorGUI()
|
|
{
|
|
RenderGUI(false);
|
|
}
|
|
|
|
public void RenderGUI(bool embedded = false)
|
|
{
|
|
if (Target == null)
|
|
return;
|
|
#if UNITY_5_6_OR_NEWER
|
|
serializedObject.UpdateIfRequiredOrScript();
|
|
#else
|
|
serializedObject.UpdateIfDirtyOrScript();
|
|
#endif
|
|
|
|
if (Node.Count == 0)
|
|
ReadNodes();
|
|
|
|
NeedRepaint = false;
|
|
|
|
if (mRootNode)
|
|
{
|
|
DTInspectorNode.IsInsideInspector = !embedded;
|
|
IsInsideInspector = !embedded;
|
|
OnCustomInspectorGUIBefore();
|
|
renderNode(mRootNode);
|
|
OnCustomInspectorGUI();
|
|
}
|
|
if (serializedObject.ApplyModifiedProperties())
|
|
OnModified();
|
|
|
|
GUI.SetNextControlName("");
|
|
|
|
if (NeedRepaint)
|
|
Repaint();
|
|
}
|
|
|
|
protected virtual void OnModified()
|
|
{
|
|
}
|
|
|
|
// <summary>
|
|
/// Add custom GUI code here, rendered before the default inspector
|
|
/// </summary>
|
|
protected virtual void OnCustomInspectorGUIBefore()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Add custom GUI code here, rendered after the default inspector
|
|
/// </summary>
|
|
protected virtual void OnCustomInspectorGUI()
|
|
{
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called to initialize a ReorderableList. Override to add custom behaviour
|
|
/// </summary>
|
|
/// <param name="node">field node</param>
|
|
/// <param name="attribute">ArrayEx attribute of the field</param>
|
|
protected virtual void SetupArrayEx(DTFieldNode node, ArrayExAttribute attribute)
|
|
{
|
|
// Defaults
|
|
if (attribute.ShowHeader)
|
|
{
|
|
node.ArrayEx.drawHeaderCallback = (Rect r) =>
|
|
{
|
|
EditorGUI.LabelField(r, node.GUIContent);
|
|
if (attribute.DropTarget)
|
|
{
|
|
Event ev = Event.current;
|
|
switch (ev.type)
|
|
{
|
|
case EventType.DragUpdated:
|
|
if (r.Contains(ev.mousePosition))
|
|
{
|
|
Type fieldType = node.serializedProperty.GetFieldType();
|
|
//bug? this code is called when dragging a game object over the list of Input Spots, but not the list of Input Game Objects. Why? Both have the ArrayExAttribute with ShowHeader being true
|
|
bool allowed = DragAndDrop.objectReferences.Length > 0 && DTEditorUtility.DragDropTypeMatch(fieldType);
|
|
DragAndDrop.visualMode = allowed ? DragAndDropVisualMode.Copy : DragAndDropVisualMode.Rejected;
|
|
}
|
|
break;
|
|
case EventType.DragPerform:
|
|
if (r.Contains(ev.mousePosition))
|
|
{
|
|
Object[] objs = DTEditorUtility.DragDropGetObjectsOfType(node.serializedProperty.GetFieldType());
|
|
|
|
foreach (Object o in objs)
|
|
{
|
|
int idx = node.serializedProperty.arraySize;
|
|
node.serializedProperty.InsertArrayElementAtIndex(idx);
|
|
node.serializedProperty.GetArrayElementAtIndex(idx).objectReferenceValue = o;
|
|
}
|
|
node.serializedObject.ApplyModifiedProperties();
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
};
|
|
}
|
|
|
|
node.ArrayEx.drawElementCallback = (Rect r, int index, bool isActive, bool isFocused) =>
|
|
{
|
|
SerializedProperty e = node.ArrayEx.serializedProperty.GetArrayElementAtIndex(index);
|
|
if (e != null)
|
|
EditorGUI.PropertyField(r, e);
|
|
};
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when UndoRedo occured
|
|
/// </summary>
|
|
public virtual void OnUndoRedo()
|
|
{
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
/// <summary>
|
|
/// builds node tree and process parsing attributes
|
|
/// </summary>
|
|
public void ReadNodes()
|
|
{
|
|
DTGroupNode._serializedObject = serializedObject;
|
|
SerializedProperty iterator = serializedObject.GetIterator();
|
|
mRootNode.Clear();
|
|
mEnterChildren = true;
|
|
|
|
DTGroupNode baseNode = mRootNode;
|
|
DTGroupNode parentNode = baseNode;
|
|
Stack<string> propertyPathStack = new Stack<string>();
|
|
Stack<DTGroupNode> baseNodeStack = new Stack<DTGroupNode>();
|
|
bool resetParent = false;
|
|
|
|
while (iterator.NextVisible(mEnterChildren))
|
|
{
|
|
mEnterChildren = false;
|
|
if (iterator.name != "m_Script" && iterator.name != "InspectorFoldout")
|
|
{
|
|
// handle baseNode resetting (AsGroup etc...)
|
|
while (propertyPathStack.Count > 0 && !iterator.propertyPath.StartsWith(propertyPathStack.Peek()))
|
|
{
|
|
propertyPathStack.Pop();
|
|
baseNode = baseNodeStack.Pop();
|
|
parentNode = baseNode;
|
|
}
|
|
|
|
|
|
DTFieldNode fieldNode = new DTFieldNode(iterator);
|
|
|
|
// get group parsing attributes
|
|
List<Attribute> groupParseAttribs = iterator.GetAttributes(typeof(IDTGroupParsingAttribute));
|
|
groupParseAttribs.Sort();
|
|
// get field parsing attributes
|
|
List<Attribute> parsingAttributes = iterator.GetAttributes(typeof(IDTFieldParsingAttribute));
|
|
|
|
foreach (IDTGroupParsingAttribute ga in groupParseAttribs)
|
|
{
|
|
|
|
if (ga is TabAttribute)
|
|
{
|
|
TabAttribute tabA = (TabAttribute)ga;
|
|
parentNode = baseNode.EnsurePath(tabA.Path, false);
|
|
|
|
if (!string.IsNullOrEmpty(tabA.TabBarName) && !string.IsNullOrEmpty(tabA.TabName))
|
|
{
|
|
if (!parentNode[tabA.TabBarName])
|
|
parentNode = (DTGroupNode)parentNode.Add(new DTGroupNode(tabA.TabBarName, null, DTInspectorNode.RenderAsEnum.TabBar));
|
|
else
|
|
parentNode = (DTGroupNode)parentNode[tabA.TabBarName];
|
|
if (!parentNode[tabA.TabName])
|
|
parentNode = (DTGroupNode)parentNode.Add(new DTGroupNode(tabA.TabName, iterator, DTInspectorNode.RenderAsEnum.Tab));
|
|
else
|
|
parentNode = (DTGroupNode)parentNode[tabA.TabName];
|
|
if (tabA.Sort != 100)
|
|
parentNode.SortOrder = tabA.Sort;
|
|
}
|
|
else
|
|
DTLog.LogWarningFormat("[DevTools] Skipping [Tab] on '{0}' because Path is missing TabBar or Tab!", iterator.propertyPath);
|
|
|
|
}
|
|
else if (ga is SectionAttribute)
|
|
{
|
|
SectionAttribute sectionA = (SectionAttribute)ga;
|
|
parentNode = createGroup(baseNode, sectionA.Path, iterator);
|
|
if (sectionA.Sort != 100)
|
|
parentNode.SortOrder = sectionA.Sort;
|
|
}
|
|
else if (ga is AsGroupAttribute)
|
|
{
|
|
AsGroupAttribute asGroupA = (AsGroupAttribute)ga;
|
|
propertyPathStack.Push(fieldNode.SerializedPropertyPath);
|
|
baseNodeStack.Push(baseNode);
|
|
parentNode = createGroup((asGroupA.PathIsAbsolute) ? baseNode : parentNode, (asGroupA.Path == null) ? fieldNode.Name : asGroupA.Path + "/" + fieldNode.Name, iterator);
|
|
baseNode = parentNode;
|
|
|
|
}
|
|
else if (ga is GroupAttribute)
|
|
{
|
|
GroupAttribute groupA = (GroupAttribute)ga;
|
|
parentNode = createGroup(baseNode, groupA.Path, iterator);
|
|
if (groupA.Sort != 100)
|
|
parentNode.SortOrder = groupA.Sort;
|
|
resetParent = true;
|
|
}
|
|
}
|
|
|
|
foreach (IDTFieldParsingAttribute pa in parsingAttributes)
|
|
{
|
|
if (pa is Hide)
|
|
{
|
|
fieldNode.Visible = false;
|
|
fieldNode.ContentVisible = false;
|
|
mEnterChildren = false;
|
|
}
|
|
else if (pa is AsGroupAttribute || pa is Inline)
|
|
{
|
|
fieldNode.Visible = false;
|
|
fieldNode.ContentVisible = false;
|
|
mEnterChildren = true;
|
|
|
|
}
|
|
else if (pa is ArrayExAttribute)
|
|
{
|
|
ArrayExAttribute arrayA = (ArrayExAttribute)pa;
|
|
fieldNode.ArrayEx = new ReorderableList(serializedObject, iterator, arrayA.Draggable, arrayA.ShowHeader, arrayA.ShowAdd, arrayA.ShowDelete);
|
|
SetupArrayEx(fieldNode, arrayA);
|
|
}
|
|
else if (pa is SortOrderAttribute)
|
|
{
|
|
fieldNode.SortOrder = ((SortOrderAttribute)pa).Sort;
|
|
}
|
|
|
|
}
|
|
|
|
parentNode.Add(fieldNode);
|
|
if (resetParent)
|
|
{
|
|
parentNode = parentNode.Parent as DTGroupNode;
|
|
resetParent = false;
|
|
}
|
|
}
|
|
}
|
|
OnReadNodes();
|
|
Node.Sort();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Renders the node tree and process rendering attributes
|
|
/// </summary>
|
|
/// <param name="node"></param>
|
|
void renderNode(DTInspectorNode node)
|
|
{
|
|
if (serializedObject == null)
|
|
return;
|
|
bool guistate = GUI.enabled;
|
|
DTInspectorNode item;
|
|
for (int i = 0; i < node.Items.Count; i++)
|
|
{
|
|
item = node[i];
|
|
item.serializedObject = serializedObject;
|
|
|
|
if (item.Disabled)
|
|
GUI.enabled = false;
|
|
if (item is DTFieldNode)
|
|
{
|
|
|
|
DTFieldNode field = (DTFieldNode)item;
|
|
|
|
field.serializedProperty = serializedObject.FindProperty(field.SerializedPropertyPath);
|
|
if (field.serializedProperty == null)
|
|
return;
|
|
field.Calculate();
|
|
|
|
if (field.Visible)
|
|
{
|
|
foreach (ActionAttribute act in item.Actions)
|
|
if (act.Position == ActionAttribute.ActionPositionEnum.Above)
|
|
NodeRenderer.RenderAction(item, act, this, Target);
|
|
|
|
if (field.ArrayEx != null)
|
|
{
|
|
field.ArrayEx.serializedProperty = field.serializedProperty;
|
|
field.ArrayEx.DoLayoutList();
|
|
}
|
|
else
|
|
{
|
|
NodeRenderer.RenderField(field);
|
|
field.raiseOnRender();
|
|
}
|
|
|
|
foreach (ActionAttribute act in item.Actions)
|
|
if (act.Position == ActionAttribute.ActionPositionEnum.Below)
|
|
NodeRenderer.RenderAction(item, act, this, Target);
|
|
}
|
|
|
|
}
|
|
else if (item is DTGroupNode)
|
|
{
|
|
DTGroupNode group = (DTGroupNode)item;
|
|
group.Calculate();
|
|
|
|
if (group.Visible)
|
|
{
|
|
foreach (ActionAttribute act in item.Actions)
|
|
if (act.Position == ActionAttribute.ActionPositionEnum.Above)
|
|
NodeRenderer.RenderAction(item, act, this, Target);
|
|
|
|
if (group.Disabled)
|
|
GUI.enabled = false;
|
|
|
|
switch (item.RenderAs)
|
|
{
|
|
case DTInspectorNode.RenderAsEnum.Section:
|
|
NodeRenderer.RenderSectionHeader(group);
|
|
if (group.ContentVisible)
|
|
{
|
|
renderNode(group);
|
|
group.raiseOnRender();
|
|
}
|
|
NodeRenderer.RenderSectionFooter(group);
|
|
break;
|
|
case DTInspectorNode.RenderAsEnum.TabBar:
|
|
NodeRenderer.RenderTabBarHeader(group, (group.MaxItemsPerRow == -1) ? group.Items.Count : group.MaxItemsPerRow);
|
|
if (group.SelectedIndex > -1)
|
|
{
|
|
renderNode(group[group.SelectedIndex]);
|
|
group[group.SelectedIndex].raiseOnRender();
|
|
}
|
|
NodeRenderer.RenderTabBarFooter(group);
|
|
break;
|
|
default:
|
|
if (group.ContentVisible)
|
|
renderNode(group);
|
|
break;
|
|
}
|
|
|
|
foreach (ActionAttribute act in item.Actions)
|
|
if (act.Position == ActionAttribute.ActionPositionEnum.Below)
|
|
NodeRenderer.RenderAction(item, act, this, Target);
|
|
|
|
}
|
|
|
|
}
|
|
GUI.enabled = guistate;
|
|
if (item.NeedRepaint)
|
|
{
|
|
item.NeedRepaint = false;
|
|
NeedRepaint = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Creates a parent group for a field, creating the path if neccessary
|
|
/// </summary>
|
|
/// <param name="baseNode">"root" the path is applied to</param>
|
|
/// <param name="path">node path</param>
|
|
/// <param name="forProperty">field property the parent node is for</param>
|
|
/// <returns></returns>
|
|
DTGroupNode createGroup(DTGroupNode baseNode, string path, SerializedProperty forProperty)
|
|
{
|
|
DTGroupNode node = baseNode.EnsurePath(path, false, forProperty);
|
|
return node;
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
}
|