//********************************** 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; } } /** @} */ }