| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635 |
- //********************************** Banshee Engine (www.banshee3d.com) **************************************************//
- //**************** Copyright (c) 2016 Marko Pintera ([email protected]). All rights reserved. **********************//
- using System;
- using System.Collections.Generic;
- using System.Text;
- using bs;
-
- namespace bs.Editor
- {
- /** @addtogroup Inspector
- * @{
- */
- /// <summary>
- /// Inspectable field displays GUI elements for a single <see cref="SerializableProperty"/>. This is a base class that
- /// should be specialized for all supported types contained by <see cref="SerializableProperty"/>. 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.
- /// </summary>
- 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;
- /// <summary>
- /// Property this field is displaying contents of.
- /// </summary>
- 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;
- }
- }
- /// <summary>
- /// Returns the path to the field.
- /// </summary>
- public string Path => path;
- /// <summary>
- /// Creates a new inspectable field GUI for the specified property.
- /// </summary>
- /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
- /// <param name="title">Name of the property, or some other value to set as the title.</param>
- /// <param name="path">Full path to this property (includes name of this property and all parent properties).</param>
- /// <param name="type">Type of property this field will be used for displaying.</param>
- /// <param name="depth">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.</param>
- /// <param name="layout">Parent layout that all the field elements will be added to.</param>
- /// <param name="property">Serializable property referencing the array whose contents to display.</param>
- 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;
- }
- /// <summary>
- /// Checks if contents of the field have been modified, and updates them if needed.
- /// </summary>
- /// <param name="layoutIndex">Index in the parent's layout at which to insert the GUI elements for this field.
- /// </param>
- /// <returns>State representing was anything modified between two last calls to <see cref="Refresh"/>.</returns>
- public virtual InspectableState Refresh(int layoutIndex)
- {
- return InspectableState.NotModified;
- }
- /// <summary>
- /// Returns the total number of GUI elements in the field's layout.
- /// </summary>
- /// <returns>Number of GUI elements in the field's layout.</returns>
- public int GetNumLayoutElements()
- {
- return layout.NumElements;
- }
- /// <summary>
- /// 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.
- /// </summary>
- /// <returns>Title layout if the field has one, null otherwise.</returns>
- public virtual GUILayoutX GetTitleLayout()
- {
- return null;
- }
- /// <summary>
- /// Initializes the GUI elements for the field.
- /// </summary>
- /// <param name="layoutIndex">Index at which to insert the GUI elements.</param>
- protected internal abstract void Initialize(int layoutIndex);
- /// <summary>
- /// Destroys all GUI elements in the inspectable field.
- /// </summary>
- public virtual void Destroy()
- {
- layout.DestroyElements();
- }
- /// <summary>
- /// Moves keyboard focus to this field.
- /// </summary>
- /// <param name="subFieldName">
- /// Name of the sub-field to focus on. Only relevant if the inspectable field represents multiple GUI
- /// input elements.
- /// </param>
- public virtual void SetHasFocus(string subFieldName = null) { }
- /// <summary>
- /// Searches for a child field with the specified path.
- /// </summary>
- /// <param name="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
- /// </param>
- /// <returns>Matching field if one is found, null otherwise.</returns>
- public virtual InspectableField FindPath(string path)
- {
- return null;
- }
- /// <summary>
- /// Searches for a field with the specified path.
- /// </summary>
- /// <param name="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
- /// </param>
- /// <param name="depth">Path depth at which the provided set of fields is at.</param>
- /// <param name="fields">List of fields to search. Children will be searched recursively.</param>
- /// <returns>Matching field if one is found, null otherwise.</returns>
- public static InspectableField FindPath(string path, int depth, IEnumerable<InspectableField> 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;
- }
- /// <summary>
- /// Returns the top-most part of the provided field path.
- /// See <see cref="FindPath(string, int, IEnumerable{InspectableField})"/> for more information about paths.
- /// </summary>
- /// <param name="path">Path to return the sub-path of.</param>
- /// <param name="count">Number of path elements to retrieve.</param>
- /// <returns>First <paramref name="count"/> elements of the path.</returns>
- 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();
- }
- /// <summary>
- /// Zero parameter wrapper for <see cref="StartUndo(string)"/>
- /// </summary>
- protected void StartUndo()
- {
- StartUndo(null);
- }
- /// <summary>
- /// 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 <see cref="EndUndo"/> after field is done being changed.
- /// </summary>
- /// <param name="subPath">Additional path to append to the end of the current field path.</param>
- 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);
- }
- }
- /// <summary>
- /// Finishes recording an undo command started via <see cref="StartUndo(string)"/>. If any changes are detected on
- /// the field an undo command is recorded onto the undo-redo stack, otherwise nothing is done.
- /// </summary>
- protected void EndUndo()
- {
- GameObjectUndo.ResolveDiffs();
- }
- /// <summary>
- /// 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.
- /// </summary>
- /// <param name="field">Field to generate inspector GUI for.</param>
- /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
- /// <param name="path">Full path to the provided field (includes name of this field and all parent fields).</param>
- /// <param name="layout">Parent layout that all the field elements will be added to.</param>
- /// <param name="layoutIndex">Index into the parent layout at which to insert the GUI elements for the field .</param>
- /// <param name="depth">
- /// 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.
- /// </param>
- /// <returns>
- /// 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.
- /// </returns>
- public delegate InspectableField FieldOverrideCallback(SerializableField field, InspectableContext context, string path,
- InspectableFieldLayout layout, int layoutIndex, int depth);
- /// <summary>
- /// Creates inspectable fields all the fields/properties of the specified object.
- /// </summary>
- /// <param name="obj">Object whose fields the GUI will be drawn for.</param>
- /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
- /// <param name="path">Full path to the field this provided object was retrieved from.</param>
- /// <param name="depth">
- /// 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.
- /// </param>
- /// <param name="layout">Parent layout that all the field GUI elements will be added to.</param>
- /// <param name="overrideCallback">
- /// 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.
- /// </param>
- public static List<InspectableField> CreateFields(SerializableObject obj, InspectableContext context, string path,
- int depth, GUILayoutY layout, FieldOverrideCallback overrideCallback = null)
- {
- // Retrieve fields and sort by order
- List<SerializableField> fields = new List<SerializableField>();
- 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<InspectableField> inspectableFields = new List<InspectableField>();
- 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;
- }
- /// <summary>
- /// 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
- /// <see cref="CustomInspector"/> attribute.
- /// </summary>
- /// <param name="context">Context shared by all inspectable fields created by the same parent.</param>
- /// <param name="title">Name of the property, or some other value to set as the title.</param>
- /// <param name="path">Full path to this property (includes name of this property and all parent properties).</param>
- /// <param name="layoutIndex">Index into the parent layout at which to insert the GUI elements for the field .</param>
- /// <param name="depth">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.</param>
- /// <param name="layout">Parent layout that all the field elements will be added to.</param>
- /// <param name="property">Serializable property referencing the array whose contents to display.</param>
- /// <param name="style">Information that can be used for customizing field rendering and behaviour.</param>
- /// <returns>Inspectable field implementation that can be used for displaying the GUI for a serializable property
- /// of the provided type.</returns>
- 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;
- }
- /// <summary>
- /// Converts a name of an identifier (such as a field or a property) into a human readable name.
- /// </summary>
- /// <param name="input">Identifier to convert.</param>
- /// <returns>Human readable name with spaces.</returns>
- 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();
- }
- }
- /// <summary>
- /// Contains information shared between multiple inspector fields.
- /// </summary>
- public class InspectableContext
- {
- /// <summary>
- /// Creates a new context.
- /// </summary>
- /// <param name="component">
- /// Component object that inspector fields are editing. Can be null if the object being edited is not a component.
- /// </param>
- public InspectableContext(Component component = null)
- {
- Persistent = new SerializableProperties();
- Component = component;
- }
- /// <summary>
- /// Creates a new context with user-provided persistent property storage.
- /// </summary>
- /// <param name="persistent">Existing object into which to inspectable fields can store persistent data.</param>
- /// <param name="component">
- /// Component object that inspector fields are editing. Can be null if the object being edited is not a component.
- /// </param>
- public InspectableContext(SerializableProperties persistent, Component component = null)
- {
- Persistent = persistent;
- Component = component;
- }
- /// <summary>
- /// 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.
- /// </summary>
- public SerializableProperties Persistent { get; }
- /// <summary>
- /// Component object that inspector fields are editing. Can be null if the object being edited is not a component.
- /// </summary>
- public Component Component { get; }
- }
- /** @} */
- }
|