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