//********************************** 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 string name; protected int depth; protected bool active = true; 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; /// /// Name portion of the field path. /// public string Name => name; /// /// Activates or deactivates the underlying GUI elements. /// public bool Active { get => active; set => SetActive(value); } /// /// 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; if (path != null) { int lastSlash = path.LastIndexOf('/'); if (lastSlash == -1) name = path; else name = path.Substring(lastSlash); } 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(); } /// /// Activates or deactivates the underlying GUI elements. /// protected virtual void SetActive(bool active) { if (this.active != active) { layout.SetActive(active); this.active = active; } } /// /// 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; } } /** @} */ }