//********************************** Banshee Engine (www.banshee3d.com) **************************************************// //**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************// using System; using System.Collections.Generic; using bs; namespace bs.Editor { /** @addtogroup Inspector * @{ */ /// /// Displays GUI elements for all the inspectable fields in an object. /// public abstract class Inspector { public const short START_BACKGROUND_DEPTH = 50; /// /// Returns the main GUI layout for the inspector. /// protected GUILayoutY Layout { get { return layout; } } /// /// Returns the main GUI panel for the inspector. is a child of this panel. /// protected GUIPanel GUI { get { return mainPanel; } } /// /// Returns the secondary GUI panel. Located at the bottom of the inspector window and unlike has /// no padding or styling applied. Only available when inspecting resources. /// protected GUIPanel PreviewGUI { get { return previewPanel; } } /// /// Returns the object the inspector is currently displaying. If the current object is a resource use /// instead; /// protected object InspectedObject { get { return inspectedObject; } } /// /// Returns the path to the resource the inspector is currently displaying. /// protected string InspectedResourcePath { get { return inspectedResourcePath; } } /// /// 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. /// protected internal SerializableProperties Persistent { get { return persistent; } } protected InspectorFieldDrawer drawer; private GUIPanel rootGUI; private GUIPanel mainPanel; private GUIPanel previewPanel; private GUILayoutY layout; private object inspectedObject; private string inspectedResourcePath; private SerializableProperties persistent; /// /// Common code called by both Initialize() overloads. /// /// Primary GUI panel to add the GUI elements to. /// Secondary GUI panel located at the bottom of the inspector window, aimed primarily for /// resource previews, but can be used for any purpose. /// 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. private void InitializeBase(GUIPanel mainGui, GUIPanel previewGui, SerializableProperties persistent) { rootGUI = mainGui; this.persistent = persistent; GUILayout contentLayoutX = mainGui.AddLayoutX(); contentLayoutX.AddSpace(5); GUILayout contentLayoutY = contentLayoutX.AddLayoutY(); contentLayoutY.AddSpace(5); GUIPanel contentPanel = contentLayoutY.AddPanel(); contentLayoutY.AddSpace(5); contentLayoutX.AddSpace(5); GUIPanel backgroundPanel = mainGui.AddPanel(START_BACKGROUND_DEPTH); GUITexture inspectorContentBg = new GUITexture(null, EditorStylesInternal.InspectorContentBg); backgroundPanel.AddElement(inspectorContentBg); mainPanel = contentPanel; previewPanel = previewGui; layout = GUI.AddLayoutY(); } /// /// Initializes the inspector using an object instance. Must be called after construction. /// /// GUI panel to add the GUI elements to. /// Instance of the object whose fields to display GUI for. /// 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. internal virtual void Initialize(GUIPanel gui, object instance, SerializableProperties persistent) { InitializeBase(gui, null, persistent); drawer = new InspectorFieldDrawer(new InspectableContext(Persistent, instance as Component), Layout); inspectedObject = instance; Initialize(); Refresh(); } /// /// Initializes the inspector using a resource path. Must be called after construction. /// /// Primary GUI panel to add the GUI elements to. /// Secondary GUI panel located at the bottom of the inspector window, aimed primarily for /// resource previews, but can be used for any purpose. /// Path to the resource for which to display GUI for. /// 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. internal virtual void Initialize(GUIPanel mainGui, GUIPanel previewGui, string path, SerializableProperties persistent) { InitializeBase(mainGui, previewGui, persistent); drawer = new InspectorFieldDrawer(new InspectableContext(Persistent), Layout); inspectedResourcePath = path; Initialize(); Refresh(); } /// /// Changes keyboard focus to the provided field. /// /// Path to the field on the object being inspected. internal virtual void FocusOnField(string path) { drawer.FocusOnField(path); } /// /// 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. /// /// Name of the field being modified. protected void StartUndo(string field) { if (inspectedObject is Component component) GameObjectUndo.RecordComponent(component, field); } /// /// Finishes recording an undo command started via . If any changes are detected on /// the object an undo command is recorded onto the undo-redo stack, otherwise nothing is done. /// protected void EndUndo() { GameObjectUndo.ResolveDiffs(); } /// /// Loads the currently inspected resource into the field. By default resources /// are not loaded and you can only retrieve their path through . /// protected void LoadResource() { if(!string.IsNullOrEmpty(inspectedResourcePath)) inspectedObject = ProjectLibrary.Load(inspectedResourcePath); } /// /// Hides or shows the inspector GUI elements. /// /// True to make the GUI elements visible. internal virtual void SetVisible(bool visible) { rootGUI.Active = visible; } /// /// Destroys all inspector GUI elements. /// internal void Destroy() { Layout.Destroy(); GUI.Destroy(); } /// /// Called when the inspector is first created. /// protected internal abstract void Initialize(); /// /// Checks if contents of the inspector have been modified, and updates them if needed. /// /// Forces the GUI fields to display the latest values assigned on the object. /// State representing was anything modified between two last calls to . protected internal virtual InspectableState Refresh(bool force = false) { return drawer.Refresh(force); } } /// /// Helper class that draws the inspector field elements for an object, allowing you to draw default set of fields, /// override certain fields with your own, or add new fields manually. /// public sealed class InspectorFieldDrawer { /// /// Contains information about a conditional that determines whether a field will be active or not. /// private struct ConditionalInfo { public Func callback; public string field; public ConditionalInfo(string field, Func callback) { this.field = field; this.callback = callback; } } /// /// 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); /// /// List of fields created and updated by the drawer. /// public List Fields { get; } = new List(); private InspectableContext context; private GUILayoutY layout; private string path; private int depth; private int rootIndex; private List conditionals; // Category private int categoryIndex; private string categoryName; private InspectableCategory category; /// /// Creates new empty inspector field drawer. /// /// Context shared by all inspectable fields created by the same parent. /// Parent layout that all the field GUI elements will be added to. /// Root path to be used for all created child fields. /// /// Determines how deep within the inspector nesting hierarchy will the generated fields be. Some fields may /// contain other fields, in which case you should increase this value by one. /// public InspectorFieldDrawer(InspectableContext context, GUILayoutY layout, string path = "", int depth = 0) { this.context = context; this.layout = layout; this.path = path; this.depth = depth; } /// /// Creates the default inspector GUI for the provided object. /// /// Object whose fields to create the GUI for. /// /// If not null, the added fields will be limited to this particular type (not including any base types the actual /// object type might be a part of). If null, then fields for the entire class hierarchy of the provided object's /// type will be created. /// /// /// 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 void AddDefault(object obj, Type subType = null, FieldOverrideCallback overrideCallback = null) { if (obj == null) return; SerializableObject serializableObject = new SerializableObject(obj.GetType(), obj); AddDefault(serializableObject, subType, overrideCallback); } /// /// Creates the default inspector GUI for the provided object. /// /// Object whose fields to create the GUI for. /// /// If not null, the added fields will be limited to this particular type (not including any base types the actual /// object type might be a part of). If null, then fields for the entire class hierarchy of the provided object's /// type will be created. /// /// /// 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 void AddDefault(SerializableObject obj, Type subType = null, FieldOverrideCallback overrideCallback = null) { if (obj == null) return; // Retrieve fields and sort by order List fields = new List(); while (obj != null) { if (subType == null || subType == obj.Type) { 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 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) BeginCategory(newCategory); else EndCategory(); } string fieldName = field.Name; string readableName = InspectableField.GetReadableIdentifierName(fieldName); Func callback = null; if (overrideCallback != null) { callback = (path, fieldLayout, layoutIndex, depth) => overrideCallback(field, context, path, fieldLayout, layoutIndex, depth); } AddFieldInternal(readableName, fieldName, field.GetProperty(), InspectableFieldStyle.Create(field), callback); } } /// /// Adds a custom inspectable field with a custom getter and setter. /// /// Type the field is inspecting. /// Name of the field. /// Method that returns the current value of the field. /// Method that sets a new value of the field. public void AddField(string name, Func getter, Action setter) { SerializableProperty property = SerializableProperty.Create(getter, setter); AddFieldInternal(name, name, property, null, null); } /// /// Creates a new field accessing the provided property. /// /// Title to display on the field. /// Name of the field. /// Property used to access the field contents. /// Optional style used to customize the look of the field. /// /// Optional callback allowing the caller to override how is the field created. /// private void AddFieldInternal(string title, string name, SerializableProperty property, InspectableFieldStyleInfo style, Func fieldCreateCallback) { 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 childPath = string.IsNullOrEmpty(path) ? name : $"{path}/{name}"; InspectableField inspectableField = null; if (fieldCreateCallback != null) inspectableField = fieldCreateCallback(path, new InspectableFieldLayout(parentLayout), currentIndex, depth); if (inspectableField == null) { inspectableField = InspectableField.CreateField(context, title, childPath, currentIndex, childDepth, new InspectableFieldLayout(parentLayout), property, style); } if (category != null) category.AddChild(inspectableField); else Fields.Add(inspectableField); currentIndex += inspectableField.GetNumLayoutElements(); if (category != null) categoryIndex = currentIndex; else rootIndex = currentIndex; } /// /// Adds a condition that determines whether a field will be shown or hidde. /// /// Name of the field the condition applies to. /// The callback that returns true if the field should be shown, false otherwise. public void AddConditional(string field, Func callback) { if(conditionals == null) conditionals = new List(); conditionals.Add(new ConditionalInfo(field, callback)); } /// /// Opens up a new category. Any new fields will be parented to this category. Category must be closed by calling /// or by calling this method with a new category. /// /// Name of the category. public void BeginCategory(string name) { if(category != null) EndCategory(); string categoryPath = string.IsNullOrEmpty(path) ? $"[{name}]" : $"{path}/[{name}]"; category = new InspectableCategory(context, name, categoryPath, depth, new InspectableFieldLayout(layout)); category.Initialize(rootIndex); category.Refresh(rootIndex); rootIndex += category.GetNumLayoutElements(); Fields.Add(category); categoryName = name; categoryIndex = 0; } /// /// Ends the category started with . /// public void EndCategory() { category = null; categoryName = null; categoryIndex = 0; } /// /// Checks if contents of the inspector fields have been modified, and updates them if needed. /// /// Forces the GUI fields to display the latest values assigned on the object. /// State representing was anything modified between two last calls to . public InspectableState Refresh(bool force = false) { InspectableState state = InspectableState.NotModified; int currentIndex = 0; foreach (var field in Fields) { state |= field.Refresh(currentIndex, force); currentIndex += field.GetNumLayoutElements(); if (conditionals != null) { foreach (var conditional in conditionals) { if (conditional.field == field.Name && conditional.callback != null) { bool active = conditional.callback(); if (active != field.Active) field.Active = active; } } } } return state; } /// /// Destroys and removes all the child inspector fields. /// public void Clear() { foreach(var field in Fields) field.Destroy(); Fields.Clear(); conditionals?.Clear(); rootIndex = 0; category = null; categoryName = null; categoryIndex = 0; } /// /// Changes keyboard focus to the provided field. /// /// Path to the field on the object being inspected. public void FocusOnField(string path) { InspectableField field = InspectableField.FindPath(path, 0, Fields); field?.SetHasFocus(); } } /** @} */ }