//********************************** 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 AnimationEditor * @{ */ /// /// Renders GUI elements that display a Scene Object, its transform, components and child objects, as well as all of /// their fields. User can then select one of the fields and the class will output a path to the selected field, as /// well as its parent scene object and component. /// public class GUIFieldSelector { private const int INDENT_AMOUNT = 5; private const int PADDING = 5; private GUIScrollArea scrollArea; private int foldoutWidth; private SceneObject rootSO; private Element rootElement; /// /// Stores a single entry in the field hierarchy. /// private struct Element { public Element(SceneObject so, Component comp, string path) { this.so = so; this.comp = comp; this.path = path; toggle = null; childLayout = null; indentLayout = null; children = null; } public SceneObject so; public Component comp; public string path; public GUIToggle toggle; public GUILayout childLayout; public GUILayout indentLayout; public Element[] children; } /// /// Triggers when the user selects a field. The subscriber will be receive a scene object the field is part of, /// component the field is part of, and a path to the field, each entry separated by "/". Component can be null /// in which case it is assumed the field is part of the SceneObject itself. Scene object names are always prefixed /// with "!", components are always prefixed with ":", while field entries have no prefix. /// /// For example: !My Scene Object/:Camera/path/to/field /// public Action OnElementSelected; /// /// Creates a new GUIFieldSelector and registers its GUI elements in the provided layout. /// /// Layout into which to add the selector GUI hierarchy. /// Scene object to inspect the fields for. /// Width of the selector area, in pixels. /// Height of the selector area, in pixels. public GUIFieldSelector(GUILayout layout, SceneObject so, int width, int height) { rootSO = so; scrollArea = new GUIScrollArea(); scrollArea.SetWidth(width); scrollArea.SetHeight(height); layout.AddElement(scrollArea); GUISkin skin = EditorBuiltin.GUISkin; GUIElementStyle style = skin.GetStyle(EditorStyles.Expand); foldoutWidth = style.Width; Rebuild(); } /// /// Rebuilds all of the selection GUI. /// private void Rebuild() { scrollArea.Layout.Clear(); rootElement = new Element(); if (rootSO == null) return; rootElement.so = rootSO; rootElement.childLayout = scrollArea.Layout; rootElement.indentLayout = null; GUILabel header = new GUILabel(new LocEdString("Select a property"), EditorStyles.Header); scrollArea.Layout.AddElement(header); scrollArea.Layout.AddSpace(5); AddSceneObjectRows(rootElement); scrollArea.Layout.AddSpace(5); scrollArea.Layout.AddFlexibleSpace(); } /// /// Registers a set of rows for all components in a , as well as its transform and /// child objects. /// /// Row element under which to create the new rows. private void AddSceneObjectRows(Element parent) { string soName = "!" + parent.so.Name; Component[] components = parent.so.GetComponents(); parent.children = new Element[components.Length + 2]; SpriteTexture soIcon = EditorBuiltin.GetEditorIcon(EditorIcon.SceneObject); SpriteTexture compIcon = EditorBuiltin.GetEditorIcon(EditorIcon.Component); // Add transform parent.children[0] = AddFoldoutRow(parent.childLayout, soIcon, "Transform", parent.so, null, parent.path + "/" + soName, ToggleTransformFoldout); // Add components for (int i = 0; i < components.Length; i++) { Component childComponent = components[i]; Action toggleCallback = (toggleParent, expand) => { SerializableObject componentObject = new SerializableObject(childComponent.GetType(), childComponent); ToggleObjectFoldout(toggleParent, componentObject, expand); }; string name = childComponent.GetType().Name; string path = parent.path + "/" + soName + "/:" + name; parent.children[i + 1] = AddFoldoutRow(parent.childLayout, compIcon, name, parent.so, childComponent, path, toggleCallback); } // Add children if (parent.so.GetNumChildren() > 0) { parent.children[parent.children.Length - 1] = AddFoldoutRow(parent.childLayout, soIcon, "Children", parent.so, null, parent.path + "/" + soName, ToggleChildFoldout); } } /// /// Registers a new row in the layout for the provided property. The type of row is determined by the property type. /// /// Parent foldout row to which to append the new elements. /// Name of the field the property belongs to. /// Slash separated path to the field from its parent object. /// Property to create the row for. /// Element containing data for the newly created row. Only valid if method returns true. /// /// Returns true if the row was successfully added, false otherwise. private bool AddPropertyRow(Element parent, string name, string path, SerializableProperty property, out Element element) { switch (property.Type) { case SerializableProperty.FieldType.Bool: case SerializableProperty.FieldType.Float: case SerializableProperty.FieldType.Int: case SerializableProperty.FieldType.Color: case SerializableProperty.FieldType.Vector2: case SerializableProperty.FieldType.Vector3: case SerializableProperty.FieldType.Vector4: element = AddFieldRow(parent.childLayout, name, parent.so, parent.comp, path, property.Type); return true; case SerializableProperty.FieldType.Object: { Action toggleCallback = (toggleParent, expand) => { SerializableObject childObject = new SerializableObject(property.InternalType, null); ToggleObjectFoldout(toggleParent, childObject, expand); }; element = AddFoldoutRow(parent.childLayout, null, name, parent.so, parent.comp, path, toggleCallback); } return true; case SerializableProperty.FieldType.Array: { Action toggleCallback = (toggleParent, expand) => { SerializableArray childObject = property.GetArray(); if (childObject != null) ToggleArrayFoldout(toggleParent, childObject, expand); }; element = AddFoldoutRow(parent.childLayout, null, name, parent.so, parent.comp, path, toggleCallback); } return true; case SerializableProperty.FieldType.List: { Action toggleCallback = (toggleParent, expand) => { SerializableList childObject = property.GetList(); if (childObject != null) ToggleListFoldout(toggleParent, childObject, expand); }; element = AddFoldoutRow(parent.childLayout, null, name, parent.so, parent.comp, path, toggleCallback); } return true; } element = new Element(); return false; } /// /// Registers a set of rows for all child fields of the provided object. /// /// Parent foldout row to which to append the new elements. /// Type of the object whose fields to display. private void AddObjectRows(Element parent, SerializableObject serializableObject) { List elements = new List(); foreach (var field in serializableObject.Fields) { if (!field.Flags.HasFlag(SerializableFieldAttributes.Animable)) continue; string propertyPath = parent.path + "/" + field.Name; Element element; if(AddPropertyRow(parent, field.Name, propertyPath, field.GetProperty(), out element)) elements.Add(element); } // Handle special fields if (serializableObject.Type == typeof(Animation)) { Animation anim = serializableObject.Object as Animation; MorphShapes morphShapes = anim?.SceneObject.GetComponent()?.Mesh.Value?.MorphShapes; if (morphShapes != null) { string propertyPath = parent.path + "/MorphShapes"; Action toggleCallback = (toggleParent, expand) => { toggleParent.childLayout.Clear(); toggleParent.children = null; toggleParent.indentLayout.Active = expand; if (expand) { List childElements = new List(); MorphChannel[] channels = morphShapes.Channels; for (int i = 0; i < channels.Length; i++) { string channelName = channels[i].Name; string framePropertyPath = parent.path + "/MorphShapes/Frames/" + channelName; string weightPropertyPath = parent.path + "/MorphShapes/Weight/" + channelName; childElements.Add(AddFieldRow(toggleParent.childLayout, channelName + " (Frames)", toggleParent.so, toggleParent.comp, framePropertyPath, SerializableProperty.FieldType.Float)); childElements.Add(AddFieldRow(toggleParent.childLayout, channelName + " (Weight)", toggleParent.so, toggleParent.comp, weightPropertyPath, SerializableProperty.FieldType.Float)); } toggleParent.children = childElements.ToArray(); } }; elements.Add(AddFoldoutRow(parent.childLayout, null, "MorphShapes", parent.so, parent.comp, propertyPath, toggleCallback)); } } parent.children = elements.ToArray(); } /// /// Registers a set of rows for all entries of the provided array. /// /// Parent foldout row to which to append the new elements. /// Array whose fields to display. private void AddArrayRows(Element parent, SerializableArray serializableArray) { List elements = new List(); int length = serializableArray.GetLength(); for (int i = 0; i < length; i++) { string name = "[" + i + "]"; string propertyPath = parent.path + name; Element element; if (AddPropertyRow(parent, name, propertyPath, serializableArray.GetProperty(i), out element)) elements.Add(element); } parent.children = elements.ToArray(); } /// /// Registers a set of rows for all entries of the provided list. /// /// Parent foldout row to which to append the new elements. /// List whose fields to display. private void AddListRows(Element parent, SerializableList serializableList) { List elements = new List(); int length = serializableList.GetLength(); for (int i = 0; i < length; i++) { string name = "[" + i + "]"; string propertyPath = parent.path + name; Element element; if (AddPropertyRow(parent, name, propertyPath, serializableList.GetProperty(i), out element)) elements.Add(element); } parent.children = elements.ToArray(); } /// /// Registers a new row in the layout. The row cannot have any further children and can be selected as the output /// field. /// /// Layout to append the row GUI elements to. /// Name of the field. /// Parent scene object of the field. /// Parent component of the field. Can be null if field belongs to . /// /// Slash separated path to the field from its parent object. /// Data type stored in the field. /// Element object storing all information about the added field. private Element AddFieldRow(GUILayout layout, string name, SceneObject so, Component component, string path, SerializableProperty.FieldType type) { Element element = new Element(so, component, path); GUILayoutX elementLayout = layout.AddLayoutX(); elementLayout.AddSpace(PADDING); elementLayout.AddSpace(foldoutWidth); GUILabel label = new GUILabel(new LocEdString(name)); elementLayout.AddElement(label); GUIButton selectBtn = new GUIButton(new LocEdString("Select")); selectBtn.OnClick += () => { DoOnElementSelected(element, type); }; elementLayout.AddFlexibleSpace(); elementLayout.AddElement(selectBtn); elementLayout.AddSpace(5); element.path = path; return element; } /// /// Registers a new row in the layout. The row cannot be selected as the output field, but rather can be expanded /// so it displays child elements. /// /// Layout to append the row GUI elements to. /// Optional icon to display next to the name. Can be null. /// Name of the field. /// Parent scene object of the field. /// Parent component of the field. Can be null if field belongs to . /// /// Slash separated path to the field from its parent object. /// Callback to trigger when the user expands or collapses the foldout. /// Element object storing all information about the added field. private Element AddFoldoutRow(GUILayout layout, SpriteTexture icon, string name, SceneObject so, Component component, string path, Action toggleCallback) { Element element = new Element(so, component, path); GUILayoutY elementLayout = layout.AddLayoutY(); GUILayoutX foldoutLayout = elementLayout.AddLayoutX(); element.toggle = new GUIToggle("", EditorStyles.Expand); element.toggle.OnToggled += x => toggleCallback(element, x); foldoutLayout.AddSpace(PADDING); foldoutLayout.AddElement(element.toggle); if (icon != null) { GUITexture guiIcon = new GUITexture(icon, GUIOption.FixedWidth(16), GUIOption.FixedWidth(16)); foldoutLayout.AddElement(guiIcon); } GUILabel label = new GUILabel(new LocEdString(name)); foldoutLayout.AddElement(label); foldoutLayout.AddFlexibleSpace(); element.indentLayout = elementLayout.AddLayoutX(); element.indentLayout.AddSpace(INDENT_AMOUNT); element.childLayout = element.indentLayout.AddLayoutY(); element.indentLayout.Active = false; return element; } /// /// Expands or collapses the set of rows displaying a 's transform (position, rotation, /// scale). /// /// Parent row element whose children to expand/collapse. /// True to expand, false to collapse. private void ToggleTransformFoldout(Element parent, bool expand) { parent.childLayout.Clear(); parent.children = null; parent.indentLayout.Active = expand; if (expand) { parent.children = new Element[3]; parent.children[0] = AddFieldRow(parent.childLayout, "Position", parent.so, null, parent.path + "/Position", SerializableProperty.FieldType.Vector3); parent.children[1] = AddFieldRow(parent.childLayout, "Rotation", parent.so, null, parent.path + "/Rotation", SerializableProperty.FieldType.Vector3); parent.children[2] = AddFieldRow(parent.childLayout, "Scale", parent.so, null, parent.path + "/Scale", SerializableProperty.FieldType.Vector3); } } /// /// Expands or collapses the set of rows displaying all children of a /// . /// /// Parent row element whose children to expand/collapse. /// True to expand, false to collapse. private void ToggleChildFoldout(Element parent, bool expand) { parent.childLayout.Clear(); parent.children = null; parent.indentLayout.Active = expand; if (expand) { int numChildren = parent.so.GetNumChildren(); parent.children = new Element[numChildren]; for (int i = 0; i < numChildren; i++) { SceneObject child = parent.so.GetChild(i); SpriteTexture soIcon = EditorBuiltin.GetEditorIcon(EditorIcon.SceneObject); parent.children[i] = AddFoldoutRow(parent.childLayout, soIcon, child.Name, child, null, parent.path, ToggleSceneObjectFoldout); } } } /// /// Expands or collapses the set of rows displaying all children of a . This includes /// it's components, transform and child scene objects. /// /// Parent row element whose children to expand/collapse. /// True to expand, false to collapse. private void ToggleSceneObjectFoldout(Element parent, bool expand) { parent.childLayout.Clear(); parent.children = null; parent.indentLayout.Active = expand; if (expand) AddSceneObjectRows(parent); } /// /// Expands or collapses the set of rows displaying all fields of a serializable object. /// /// Parent row element whose children to expand/collapse. /// Object describing the type of the object whose fields to display. /// True to expand, false to collapse. private void ToggleObjectFoldout(Element parent, SerializableObject obj, bool expand) { parent.childLayout.Clear(); parent.children = null; parent.indentLayout.Active = expand; if (expand) AddObjectRows(parent, obj); } /// /// Expands or collapses the set of rows displaying all entries of a serializable array. /// /// Parent row element whose children to expand/collapse. /// Object describing the array whose entries to display. /// True to expand, false to collapse. private void ToggleArrayFoldout(Element parent, SerializableArray obj, bool expand) { parent.childLayout.Clear(); parent.children = null; parent.indentLayout.Active = expand; if (expand) AddArrayRows(parent, obj); } /// /// Expands or collapses the set of rows displaying all entries of a serializable list. /// /// Parent row element whose children to expand/collapse. /// Object describing the list whose entries to display. /// True to expand, false to collapse. private void ToggleListFoldout(Element parent, SerializableList obj, bool expand) { parent.childLayout.Clear(); parent.children = null; parent.indentLayout.Active = expand; if (expand) AddListRows(parent, obj); } /// /// Triggered when the user selects a field. /// /// Row element for the field that was selected. /// Data type stored in the field. private void DoOnElementSelected(Element element, SerializableProperty.FieldType type) { if (OnElementSelected != null) OnElementSelected(element.so, element.comp, element.path, type); } } /** @} */ }