//********************************** Banshee Engine (www.banshee3d.com) **************************************************//
//**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************//
using System;
using System.Collections.Generic;
using System.Text;
using bs;
namespace bs.Editor
{
/** @addtogroup Inspector
* @{
*/
///
/// Inspectable field displays GUI elements for a single . This is a base class that
/// should be specialized for all supported types contained by . Inspectable fields
/// can and should be created recursively - normally complex types like objects and arrays will contain fields of their
/// own, while primitive types like integer or boolean will consist of only a GUI element.
///
public abstract class InspectableField
{
protected InspectableContext context;
protected InspectableFieldLayout layout;
protected SerializableProperty property;
protected string title;
protected string path;
protected int depth;
protected SerializableProperty.FieldType type;
///
/// Property this field is displaying contents of.
///
public SerializableProperty Property
{
get { return property; }
set
{
if (value != null && value.Type != type)
{
throw new ArgumentException(
"Attempting to initialize an inspectable field with a property of invalid type.");
}
property = value;
}
}
///
/// Returns the path to the field.
///
public string Path => path;
///
/// Creates a new inspectable field GUI for the specified property.
///
/// Context shared by all inspectable fields created by the same parent.
/// Name of the property, or some other value to set as the title.
/// Full path to this property (includes name of this property and all parent properties).
/// Type of property this field will be used for displaying.
/// Determines how deep within the inspector nesting hierarchy is this field. Some fields may
/// contain other fields, in which case you should increase this value by one.
/// Parent layout that all the field elements will be added to.
/// Serializable property referencing the array whose contents to display.
public InspectableField(InspectableContext context, string title, string path, SerializableProperty.FieldType type,
int depth, InspectableFieldLayout layout, SerializableProperty property)
{
this.context = context;
this.layout = layout;
this.title = title;
this.path = path;
this.type = type;
this.depth = depth;
Property = property;
}
///
/// Checks if contents of the field have been modified, and updates them if needed.
///
/// Index in the parent's layout at which to insert the GUI elements for this field.
///
/// State representing was anything modified between two last calls to .
public virtual InspectableState Refresh(int layoutIndex)
{
return InspectableState.NotModified;
}
///
/// Returns the total number of GUI elements in the field's layout.
///
/// Number of GUI elements in the field's layout.
public int GetNumLayoutElements()
{
return layout.NumElements;
}
///
/// Returns an optional title layout. Certain fields may contain separate title and content layouts. Parent fields
/// may use the separate title layout instead of the content layout to append elements. Having a separate title
/// layout is purely cosmetical.
///
/// Title layout if the field has one, null otherwise.
public virtual GUILayoutX GetTitleLayout()
{
return null;
}
///
/// Initializes the GUI elements for the field.
///
/// Index at which to insert the GUI elements.
protected internal abstract void Initialize(int layoutIndex);
///
/// Destroys all GUI elements in the inspectable field.
///
public virtual void Destroy()
{
layout.DestroyElements();
}
///
/// Moves keyboard focus to this field.
///
///
/// Name of the sub-field to focus on. Only relevant if the inspectable field represents multiple GUI
/// input elements.
///
public virtual void SetHasFocus(string subFieldName = null) { }
///
/// Searches for a child field with the specified path.
///
///
/// Path relative to the current field. Path entries are field names separated with "/". Fields within
/// categories are placed within a special category group, surrounded by "[]". Some examples:
/// - myField
/// - myObject/myField
/// - myObject/[myCategory]/myField
///
/// Matching field if one is found, null otherwise.
public virtual InspectableField FindPath(string path)
{
return null;
}
///
/// Searches for a field with the specified path.
///
///
/// Path to search for. Path entries are readable field names separated with "/". Fields within categories are
/// placed within a special category group, surrounded by "[]". Some examples:
/// - myField
/// - myObject/myField
/// - myObject/[myCategory]/myField
///
/// Path depth at which the provided set of fields is at.
/// List of fields to search. Children will be searched recursively.
/// Matching field if one is found, null otherwise.
public static InspectableField FindPath(string path, int depth, IEnumerable fields)
{
string subPath = GetSubPath(path, depth + 1);
foreach (var field in fields)
{
InspectableField foundField = null;
if (field.path == path)
foundField = field;
else if (field.path == subPath)
foundField = field.FindPath(path);
if (foundField != null)
return foundField;
}
return null;
}
///
/// Returns the top-most part of the provided field path.
/// See for more information about paths.
///
/// Path to return the sub-path of.
/// Number of path elements to retrieve.
/// First elements of the path.
public static string GetSubPath(string path, int count)
{
if (count <= 0)
return null;
StringBuilder sb = new StringBuilder();
int foundSections = 0;
bool gotFirstChar = false;
for (int i = 0; i < path.Length; i++)
{
if (path[i] == '/')
{
if (!gotFirstChar)
{
gotFirstChar = true;
continue;
}
foundSections++;
if (foundSections == count)
break;
}
sb.Append(path[i]);
gotFirstChar = true;
}
return sb.ToString();
}
///
/// Zero parameter wrapper for
///
protected void StartUndo()
{
StartUndo(null);
}
///
/// Notifies the system to start recording a new undo command. Any changes to the field after this is called
/// will be recorded in the command. User must call after field is done being changed.
///
/// Additional path to append to the end of the current field path.
protected void StartUndo(string subPath)
{
if (context.Component != null)
{
string fullPath = path;
if (!string.IsNullOrEmpty(subPath))
fullPath = path.TrimEnd('/') + '/' + subPath.TrimStart('/');
GameObjectUndo.RecordComponent(context.Component, fullPath);
}
}
///
/// Finishes recording an undo command started via . If any changes are detected on
/// the field an undo command is recorded onto the undo-redo stack, otherwise nothing is done.
///
protected void EndUndo()
{
GameObjectUndo.ResolveDiffs();
}
///
/// Allows the user to override the default inspector GUI for a specific field in an object. If this method
/// returns null the default field will be used instead.
///
/// Field to generate inspector GUI for.
/// Context shared by all inspectable fields created by the same parent.
/// Full path to the provided field (includes name of this field and all parent fields).
/// Parent layout that all the field elements will be added to.
/// Index into the parent layout at which to insert the GUI elements for the field .
///
/// Determines how deep within the inspector nesting hierarchy is this field. Some fields may contain other fields,
/// in which case you should increase this value by one.
///
///
/// Inspectable field implementation that can be used for displaying the GUI for the provided field. Or null if
/// default field GUI should be used instead.
///
public delegate InspectableField FieldOverrideCallback(SerializableField field, InspectableContext context, string path,
InspectableFieldLayout layout, int layoutIndex, int depth);
///
/// Creates inspectable fields all the fields/properties of the specified object.
///
/// Object whose fields the GUI will be drawn for.
/// Context shared by all inspectable fields created by the same parent.
/// Full path to the field this provided object was retrieved from.
///
/// Determines how deep within the inspector nesting hierarchy is this objects. Some fields may contain other
/// fields, in which case you should increase this value by one.
///
/// Parent layout that all the field GUI elements will be added to.
///
/// Optional callback that allows you to override the look of individual fields in the object. If non-null the
/// callback will be called with information about every field in the provided object. If the callback returns
/// non-null that inspectable field will be used for drawing the GUI, otherwise the default inspector field type
/// will be used.
///
public static List CreateFields(SerializableObject obj, InspectableContext context, string path,
int depth, GUILayoutY layout, FieldOverrideCallback overrideCallback = null)
{
// Retrieve fields and sort by order
List fields = new List();
while (obj != null)
{
SerializableField[] subTypeFields = obj.Fields;
Array.Sort(subTypeFields,
(x, y) =>
{
int orderX = x.Flags.HasFlag(SerializableFieldAttributes.Order) ? x.Style.Order : 0;
int orderY = y.Flags.HasFlag(SerializableFieldAttributes.Order) ? y.Style.Order : 0;
return orderX.CompareTo(orderY);
});
fields.AddRange(subTypeFields);
obj = obj.Base;
}
// Generate per-field GUI while grouping by category
int rootIndex = 0;
int categoryIndex = 0;
string categoryName = null;
InspectableCategory category = null;
List inspectableFields = new List();
foreach (var field in fields)
{
if (!field.Flags.HasFlag(SerializableFieldAttributes.Inspectable))
continue;
if (field.Flags.HasFlag(SerializableFieldAttributes.Category))
{
string newCategory = field.Style.CategoryName;
if (!string.IsNullOrEmpty(newCategory) && categoryName != newCategory)
{
string categoryPath = string.IsNullOrEmpty(path) ? $"[{newCategory}]" : $"{path}/[{newCategory}]";
category = new InspectableCategory(context, newCategory, categoryPath, depth,
new InspectableFieldLayout(layout));
category.Initialize(rootIndex);
category.Refresh(rootIndex);
rootIndex += category.GetNumLayoutElements();
inspectableFields.Add(category);
categoryName = newCategory;
categoryIndex = 0;
}
else
{
categoryName = null;
category = null;
}
}
int currentIndex;
int childDepth;
GUILayoutY parentLayout;
if (category != null)
{
currentIndex = categoryIndex;
parentLayout = category.ChildLayout;
childDepth = depth + 1;
}
else
{
currentIndex = rootIndex;
parentLayout = layout;
childDepth = depth;
}
string fieldName = field.Name;
string readableName = GetReadableIdentifierName(fieldName);
string childPath = string.IsNullOrEmpty(path) ? fieldName : $"{path}/{fieldName}";
InspectableField inspectableField = null;
if(overrideCallback != null)
inspectableField = overrideCallback(field, context, path, new InspectableFieldLayout(parentLayout),
currentIndex, depth);
if (inspectableField == null)
{
inspectableField = CreateField(context, readableName, childPath,
currentIndex, childDepth, new InspectableFieldLayout(parentLayout), field.GetProperty(),
InspectableFieldStyle.Create(field));
}
if (category != null)
category.AddChild(inspectableField);
else
inspectableFields.Add(inspectableField);
currentIndex += inspectableField.GetNumLayoutElements();
if (category != null)
categoryIndex = currentIndex;
else
rootIndex = currentIndex;
}
return inspectableFields;
}
///
/// Creates a new inspectable field, automatically detecting the most appropriate implementation for the type
/// contained in the provided serializable property. This may be one of the built-in inspectable field implemetations
/// (like ones for primitives like int or bool), or a user defined implementation defined with a
/// attribute.
///
/// Context shared by all inspectable fields created by the same parent.
/// Name of the property, or some other value to set as the title.
/// Full path to this property (includes name of this property and all parent properties).
/// Index into the parent layout at which to insert the GUI elements for the field .
/// Determines how deep within the inspector nesting hierarchy is this field. Some fields may
/// contain other fields, in which case you should increase this value by one.
/// Parent layout that all the field elements will be added to.
/// Serializable property referencing the array whose contents to display.
/// Information that can be used for customizing field rendering and behaviour.
/// Inspectable field implementation that can be used for displaying the GUI for a serializable property
/// of the provided type.
public static InspectableField CreateField(InspectableContext context, string title, string path, int layoutIndex,
int depth, InspectableFieldLayout layout, SerializableProperty property, InspectableFieldStyleInfo style = null)
{
InspectableField field = null;
Type type = property.InternalType;
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(RRef<>))
type = type.GenericTypeArguments[0];
Type customInspectable = InspectorUtility.GetCustomInspectable(type);
if (customInspectable != null)
{
field = (InspectableField) Activator.CreateInstance(customInspectable, context, title, path, depth, layout,
property, style);
}
else
{
switch (property.Type)
{
case SerializableProperty.FieldType.Int:
if (style != null && style.StyleFlags.HasFlag(InspectableFieldStyleFlags.AsLayerMask))
field = new InspectableLayerMask(context, title, path, depth, layout, property);
else
{
if (style?.RangeStyle == null || !style.RangeStyle.Slider)
field = new InspectableInt(context, title, path, depth, layout, property, style);
else
field = new InspectableRangedInt(context, title, path, depth, layout, property, style);
}
break;
case SerializableProperty.FieldType.Float:
if (style?.RangeStyle == null || !style.RangeStyle.Slider)
field = new InspectableFloat(context, title, path, depth, layout, property, style);
else
field = new InspectableRangedFloat(context, title, path, depth, layout, property, style);
break;
case SerializableProperty.FieldType.Bool:
field = new InspectableBool(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.Color:
field = new InspectableColor(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.ColorGradient:
field = new InspectableColorGradient(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.Curve:
field = new InspectableCurve(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.FloatDistribution:
field = new InspectableFloatDistribution(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.Vector2Distribution:
field = new InspectableVector2Distribution(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.Vector3Distribution:
field = new InspectableVector3Distribution(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.ColorDistribution:
field = new InspectableColorDistribution(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.String:
field = new InspectableString(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.Vector2:
field = new InspectableVector2(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.Vector3:
field = new InspectableVector3(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.Vector4:
field = new InspectableVector4(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.Quaternion:
if (style != null && style.StyleFlags.HasFlag(InspectableFieldStyleFlags.AsQuaternion))
field = new InspectableQuaternion(context, title, path, depth, layout, property);
else
field = new InspectableEuler(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.Resource:
field = new InspectableResource(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.RRef:
field = new InspectableRRef(context, title, path, depth, layout, property, style);
break;
case SerializableProperty.FieldType.GameObjectRef:
field = new InspectableGameObjectRef(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.Object:
field = new InspectableObject(context, title, path, depth, layout, property, style);
break;
case SerializableProperty.FieldType.Array:
field = new InspectableArray(context, title, path, depth, layout, property, style);
break;
case SerializableProperty.FieldType.List:
field = new InspectableList(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.Dictionary:
field = new InspectableDictionary(context, title, path, depth, layout, property);
break;
case SerializableProperty.FieldType.Enum:
field = new InspectableEnum(context, title, path, depth, layout, property);
break;
}
}
if (field == null)
throw new Exception("No inspector exists for the provided field type.");
field.Initialize(layoutIndex);
field.Refresh(layoutIndex);
return field;
}
///
/// Converts a name of an identifier (such as a field or a property) into a human readable name.
///
/// Identifier to convert.
/// Human readable name with spaces.
public static string GetReadableIdentifierName(string input)
{
if (string.IsNullOrEmpty(input))
return "";
StringBuilder sb = new StringBuilder();
bool nextUpperIsSpace = true;
if (input[0] == '_')
{
// Skip
nextUpperIsSpace = false;
}
else if (input[0] == 'm' && input.Length > 1 && char.IsUpper(input[1]))
{
// Skip
nextUpperIsSpace = false;
}
else if (char.IsLower(input[0]))
sb.Append(char.ToUpper(input[0]));
else
{
sb.Append(input[0]);
nextUpperIsSpace = false;
}
for (int i = 1; i < input.Length; i++)
{
if (input[i] == '_')
{
sb.Append(' ');
nextUpperIsSpace = false;
}
else if (char.IsUpper(input[i]))
{
if (nextUpperIsSpace)
{
sb.Append(' ');
sb.Append(input[i]);
}
else
sb.Append(char.ToLower(input[i]));
nextUpperIsSpace = false;
}
else
{
sb.Append(input[i]);
nextUpperIsSpace = true;
}
}
return sb.ToString();
}
}
///
/// Contains information shared between multiple inspector fields.
///
public class InspectableContext
{
///
/// Creates a new context.
///
///
/// Component object that inspector fields are editing. Can be null if the object being edited is not a component.
///
public InspectableContext(Component component = null)
{
Persistent = new SerializableProperties();
Component = component;
}
///
/// Creates a new context with user-provided persistent property storage.
///
/// Existing object into which to inspectable fields can store persistent data.
///
/// Component object that inspector fields are editing. Can be null if the object being edited is not a component.
///
public InspectableContext(SerializableProperties persistent, Component component = null)
{
Persistent = persistent;
Component = component;
}
///
/// A set of properties that the inspector can read/write. They will be persisted even after the inspector is closed
/// and restored when it is re-opened.
///
public SerializableProperties Persistent { get; }
///
/// Component object that inspector fields are editing. Can be null if the object being edited is not a component.
///
public Component Component { get; }
}
/** @} */
}