//********************************** 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 AnimationEditor * @{ */ /// /// Renders a hierarchical list of animation curve fields and displays information about them. Also allows field /// selection. /// internal class GUIAnimFieldDisplay { private SceneObject root; private int width; private int height; private GUIScrollArea scrollArea; private List fieldInfos = new List(); private GUIAnimFieldEntry[] fields; private GUIAnimFieldLayouts layouts; /// /// Triggered when the user clicks on an new entry in the field display. Curve field path of the selected entry /// is provided. /// public Action OnEntrySelected; /// /// Creates a new animation field display GUI element and adds it to the provided layout. /// /// Layout to add the GUI element to. /// Width of the GUI element, in pixels. /// Height of the GUI element, in pixels. /// Scene object that the root curve field paths reference. public GUIAnimFieldDisplay(GUILayout layout, int width, int height, SceneObject root) { this.root = root; scrollArea = new GUIScrollArea(ScrollBarType.ShowIfDoesntFit, ScrollBarType.NeverShow); layout.AddElement(scrollArea); SetSize(width, height); } /// /// Changes the size of the GUI element. /// /// Width of the GUI element, in pixels. /// Height of the GUI element, in pixels. public void SetSize(int width, int height) { this.width = width; this.height = height; scrollArea.SetWidth(width); scrollArea.SetHeight(height); Rebuild(); } /// /// Sets which fields to display. /// /// A list of fields to display. public void SetFields(AnimFieldInfo[] fields) { this.fieldInfos.Clear(); this.fieldInfos.AddRange(fields); Rebuild(); } /// /// Adds a new field to the existing field list, and displays it. /// /// Field to append to the field list, and display. public void AddField(AnimFieldInfo field) { bool exists = fieldInfos.Exists(x => { return x.path == field.path; }); if (!exists) { fieldInfos.Add(field); Rebuild(); } } /// /// Changes the displayed values for each field. /// /// Values to assign to the fields. Must match the number of displayed fields. public void SetDisplayValues(GUIAnimFieldPathValue[] values) { for (int i = 0; i < fields.Length; i++) { string path = fields[i].Path; for (int j = 0; j < values.Length; j++) { if(path == values[j].path) fields[i].SetValue(values[j].value); } } } /// /// Sets which fields should be displayed as selected. /// /// Curve field paths of fields to display as selected. public void SetSelection(string[] paths) { Action updateSelection = field => { bool foundSelected = false; for (int j = 0; j < paths.Length; j++) { if (field.Path == paths[j]) { field.SetSelection(true); foundSelected = true; break; } } if (!foundSelected) field.SetSelection(false); }; for (int i = 0; i < fields.Length; i++) { updateSelection(fields[i]); // Check children (only one level allowed) GUIAnimFieldEntry[] children = fields[i].GetChildren(); if (children == null) continue; for (int j = 0; j < children.Length; j++) updateSelection(children[j]); } } /// /// Rebuilds the entire GUI based on the current field list and their properties. /// private void Rebuild() { scrollArea.Layout.Clear(); fields = null; if (fieldInfos == null || root == null) return; GUILabel header = new GUILabel(new LocEdString("Properties"), EditorStyles.Header); scrollArea.Layout.AddElement(header); layouts = new GUIAnimFieldLayouts(); GUIPanel rootPanel = scrollArea.Layout.AddPanel(); GUIPanel mainPanel = rootPanel.AddPanel(); GUIPanel underlayPanel = rootPanel.AddPanel(1); GUIPanel overlayPanel = rootPanel.AddPanel(-1); GUIPanel backgroundPanel = rootPanel.AddPanel(2); layouts.main = mainPanel.AddLayoutY(); layouts.underlay = underlayPanel.AddLayoutY(); layouts.overlay = overlayPanel.AddLayoutY(); layouts.background = backgroundPanel.AddLayoutY(); GUIButton catchAll = new GUIButton("", EditorStyles.Blank); catchAll.Bounds = new Rect2I(0, 0, width, height - header.Bounds.height); catchAll.OnClick += () => OnEntrySelected(null); underlayPanel.AddElement(catchAll); layouts.main.AddSpace(5); layouts.underlay.AddSpace(5); layouts.overlay.AddSpace(5); layouts.background.AddSpace(3); // Minor hack: Background starts heigher to get it to center better fields = new GUIAnimFieldEntry[fieldInfos.Count]; for (int i = 0; i < fieldInfos.Count; i++) { if (string.IsNullOrEmpty(fieldInfos[i].path)) continue; bool entryIsMissing; if (fieldInfos[i].curveGroup.isPropertyCurve) { string pathSuffix; SerializableProperty property = Animation.FindProperty(root, fieldInfos[i].path, out pathSuffix); entryIsMissing = property == null; } else entryIsMissing = false; if (!entryIsMissing) { Color[] colors = new Color[fieldInfos[i].curveGroup.curveInfos.Length]; for (int j = 0; j < fieldInfos[i].curveGroup.curveInfos.Length; j++) colors[j] = fieldInfos[i].curveGroup.curveInfos[j].color; switch (fieldInfos[i].curveGroup.type) { case SerializableProperty.FieldType.Vector2: fields[i] = new GUIAnimVec2Entry(layouts, fieldInfos[i].path, colors); break; case SerializableProperty.FieldType.Vector3: fields[i] = new GUIAnimVec3Entry(layouts, fieldInfos[i].path, colors); break; case SerializableProperty.FieldType.Vector4: fields[i] = new GUIAnimVec4Entry(layouts, fieldInfos[i].path, colors); break; case SerializableProperty.FieldType.Color: fields[i] = new GUIAnimColorEntry(layouts, fieldInfos[i].path, colors); break; case SerializableProperty.FieldType.Bool: case SerializableProperty.FieldType.Int: case SerializableProperty.FieldType.Float: { Color color; if (colors.Length > 0) color = colors[0]; else color = Color.White; fields[i] = new GUIAnimSimpleEntry(layouts, fieldInfos[i].path, color); break; } } } else { fields[i] = new GUIAnimMissingEntry(layouts, fieldInfos[i].path); } if (fields[i] != null) fields[i].OnEntrySelected += OnEntrySelected; } if (fieldInfos.Count == 0) { GUILabel warningLbl = new GUILabel(new LocEdString("No properties. Add a new property to begin animating.")); GUILayoutY vertLayout = layouts.main.AddLayoutY(); vertLayout.AddFlexibleSpace(); GUILayoutX horzLayout = vertLayout.AddLayoutX(); vertLayout.AddFlexibleSpace(); horzLayout.AddFlexibleSpace(); horzLayout.AddElement(warningLbl); horzLayout.AddFlexibleSpace(); } layouts.main.AddSpace(5); layouts.underlay.AddSpace(5); layouts.overlay.AddSpace(5); layouts.background.AddSpace(5); layouts.main.AddFlexibleSpace(); layouts.underlay.AddFlexibleSpace(); layouts.overlay.AddFlexibleSpace(); layouts.background.AddFlexibleSpace(); } } /// /// All layouts used for placing field display GUI elements. /// internal class GUIAnimFieldLayouts { public GUILayout main; public GUILayout underlay; public GUILayout overlay; public GUILayout background; } /// /// Path/value combination used for representing the value of the currently selected frame for a specific curve path. /// internal struct GUIAnimFieldPathValue { public string path; public object value; } /// /// Base class for individual entries in a GUI animation curve field display. /// internal abstract class GUIAnimFieldEntry { private const int MAX_PATH_LENGTH = 30; protected const int INDENT_AMOUNT = 10; protected string path; private GUIButton selectionBtn; private GUITexture backgroundTexture; private int entryHeight; /// /// Triggered when the user selects this entry. Curve field path of the selected entry is provided. /// public Action OnEntrySelected; /// /// Path of the curve field path this entry represents. /// public string Path { get { return path; } } /// /// Constructs a new animation field entry and appends the necessary GUI elements to the provided layouts. /// /// Layouts to append the GUI elements to. /// Path of the curve field. /// Determines if the element is a root path, or a child (sub) element. /// Determines how much to horizontally indent the element, in pixels. public GUIAnimFieldEntry(GUIAnimFieldLayouts layouts, string path, bool child, int indentAmount) { this.path = path; GUILayoutX toggleLayout = layouts.main.AddLayoutX(); toggleLayout.AddSpace(indentAmount); selectionBtn = new GUIButton(GetDisplayName(path, child), EditorStyles.Label, GUIOption.FlexibleWidth()); selectionBtn.OnClick += () => { OnEntrySelected?.Invoke(path); }; toggleLayout.AddElement(selectionBtn); entryHeight = EditorBuiltin.GUISkin.GetStyle(EditorStyles.Label).Height; backgroundTexture = new GUITexture(Builtin.WhiteTexture, GUITextureScaleMode.StretchToFit, GUIOption.FlexibleWidth()); backgroundTexture.SetTint(Color.Transparent); backgroundTexture.SetHeight(entryHeight); layouts.background.AddElement(backgroundTexture); } /// /// Hides or shows the element. /// /// True to show the element, false otherwise. public virtual void Toggle(bool on) { selectionBtn.Active = on; backgroundTexture.Active = on; } /// /// Toggles whether the entry is displayed as selected, or not selected. /// /// True to display as selected, false otherwise. public void SetSelection(bool selected) { if(selected) backgroundTexture.SetTint(Color.DarkCyan); else backgroundTexture.SetTint(Color.Transparent); } /// /// Displays a floating point value in the value display. /// /// Value to display public virtual void SetValue(float value) { } /// /// Changes the displayed value next to the element's name. /// /// public virtual void SetValue(object value) { } /// /// Returns all child elements, if this element is complex and has children (e.g. vector). /// /// List of child elements, or null if none. public virtual GUIAnimFieldEntry[] GetChildren() { return null; } /// /// Returns the height of this element. /// /// Height of this element, in pixels. protected int GetEntryHeight() { return entryHeight; } /// /// Generates a name to display on the element's GUI based on its path. /// /// Path of the curve field. /// True to generate a short path without scene object or component info. /// Text to display on the element's GUI. protected static string GetDisplayName(string path, bool shortName) { if (string.IsNullOrEmpty(path)) return ""; string soName; string compName; string propertyPath; string trimmedPath = path.Trim('/'); GetNames(trimmedPath, shortName, out soName, out compName, out propertyPath); if (propertyPath == null) return ""; if (shortName) return propertyPath; else { string truncatedPropPath; if (propertyPath.Length > MAX_PATH_LENGTH) truncatedPropPath = "..." + propertyPath.Substring(propertyPath.Length - MAX_PATH_LENGTH); else truncatedPropPath = propertyPath; if (soName != null) { if (compName != null) return soName + "(" + compName + ") - " + truncatedPropPath; else return soName + " - " + truncatedPropPath; } else { if (compName != null) return "(" + compName + ") - " + truncatedPropPath; else return truncatedPropPath; } } } /// /// Parses a curve field path and breaks it down relevant components. /// /// Curve field path to parse. /// If true, only the last entry of the property portion of the path will be output in /// . Otherwise all properties will be output. /// Name of the scene object the path field belongs to. /// Name of the component the path field belongs to, if any. /// A list of properties relative to parent component or scene object, that determine /// which field does the path reference. If is true, only /// the last property will be output (if there are multiple). protected static void GetNames(string path, bool shortName, out string soName, out string compName, out string propertyPath) { string[] entries = path.Split('/'); // Find name of the last scene object in the path int pathIdx = 0; soName = null; for (; pathIdx < entries.Length; pathIdx++) { string entry = entries[pathIdx]; if (string.IsNullOrEmpty(entry)) continue; // Not a scene object, break if (entry[0] != '!') { if (pathIdx > 0) { string prevEntry = entries[pathIdx - 1]; soName = prevEntry.Substring(1, prevEntry.Length - 1); } break; } } if (pathIdx >= entries.Length) { compName = null; propertyPath = null; return; } // If path is referencing a component, find it { string entry = entries[pathIdx]; if (entry[0] == ':') compName = entry.Substring(1, entry.Length - 1); else compName = null; } // Look for a field name if (compName != null) { pathIdx++; if (pathIdx >= entries.Length) { propertyPath = null; return; } } if (shortName) { if (pathIdx < entries.Length) propertyPath = entries[entries.Length - 1]; else propertyPath = null; } else { StringBuilder pathBuilder = new StringBuilder(); for (; pathIdx < entries.Length - 1; pathIdx++) pathBuilder.Append(entries[pathIdx] + "/"); if (pathIdx < entries.Length) pathBuilder.Append(entries[pathIdx]); propertyPath = pathBuilder.ToString(); } } } /// /// Creates GUI for an element in animation field display, that contains no child elements. /// internal class GUIAnimSimpleEntry : GUIAnimFieldEntry { private GUILabel valueDisplay; private GUILayoutX underlayLayout; private GUILabel overlaySpacing; /// /// Constructs a new animation field entry and appends the necessary GUI elements to the provided layouts. /// /// Layouts to append the GUI elements to. /// Path of the curve field. /// Color of the path field curve, to display next to the element name. /// Determines if the element is a root path, or a child (sub) element. public GUIAnimSimpleEntry(GUIAnimFieldLayouts layouts, string path, Color color, bool child = false) : base(layouts, path, child, child ? 45 : 30) { valueDisplay = new GUILabel("", GUIOption.FixedHeight(GetEntryHeight())); underlayLayout = layouts.underlay.AddLayoutX(); underlayLayout.AddSpace(child ? 30 : 15); GUITexture colorSquare = new GUITexture(Builtin.WhiteTexture, GUIOption.FixedWidth(10), GUIOption.FixedHeight(10)); colorSquare.SetTint(color); underlayLayout.AddElement(colorSquare); underlayLayout.AddFlexibleSpace(); underlayLayout.AddElement(valueDisplay); underlayLayout.AddSpace(10); overlaySpacing = new GUILabel("", GUIOption.FixedHeight(GetEntryHeight())); layouts.overlay.AddElement(overlaySpacing); } /// public override void Toggle(bool on) { underlayLayout.Active = on; overlaySpacing.Active = on; base.Toggle(on); } /// public override void SetValue(float value) { string strValue = value.ToString("n2"); valueDisplay.SetContent(strValue); } } /// /// Base class for elements in animation field display, that contain other child elements. /// internal class GUIAnimComplexEntry : GUIAnimFieldEntry { private GUILayout foldoutLayout; private GUIToggle foldout; private GUILabel underlaySpacing; protected GUIAnimSimpleEntry[] children; /// /// Constructs a new animation field entry and appends the necessary GUI elements to the provided layouts. /// /// Layouts to append the GUI elements to. /// Path of the curve field. /// Sub-path names of the child entries to display. /// Colors of the curves to display, for each child entry. public GUIAnimComplexEntry(GUIAnimFieldLayouts layouts, string path, string[] childEntries, Color[] colors) : base(layouts, path, false, 20) { foldout = new GUIToggle("", EditorStyles.Expand); foldout.OnToggled += Toggle; GUILabel spacer = new GUILabel("", GUIOption.FixedHeight(GetEntryHeight())); foldoutLayout = layouts.overlay.AddLayoutX(); foldoutLayout.AddSpace(5); foldoutLayout.AddElement(foldout); foldoutLayout.AddElement(spacer); foldoutLayout.AddFlexibleSpace(); underlaySpacing = new GUILabel("", GUIOption.FixedHeight(GetEntryHeight())); layouts.underlay.AddElement(underlaySpacing); children = new GUIAnimSimpleEntry[childEntries.Length]; for (int i = 0; i < childEntries.Length; i++) { Color color; if (i < colors.Length) color = colors[i]; else color = Color.White; children[i] = new GUIAnimSimpleEntry(layouts, path + childEntries[i], color, true); children[i].OnEntrySelected += x => { OnEntrySelected?.Invoke(x); }; } Toggle(false); } /// public override void Toggle(bool on) { foreach(var child in children) child.Toggle(on); } /// public override GUIAnimFieldEntry[] GetChildren() { return children; } } /// /// Creates a GUI for displaying a Vector2 curve field in the animation field display GUI element. /// internal class GUIAnimVec2Entry : GUIAnimComplexEntry { /// /// Constructs a new animation field entry and appends the necessary GUI elements to the provided layouts. /// /// Layouts to append the GUI elements to. /// Path of the curve field. /// Colors of the curves to display, for each child entry. public GUIAnimVec2Entry(GUIAnimFieldLayouts layouts, string path, Color[] colors) : base(layouts, path, new[] { ".x", ".y" }, colors) { } /// public override void SetValue(object value) { if (value == null) return; Vector2 vector = (Vector2)value; children[0].SetValue(vector.x); children[1].SetValue(vector.y); } } /// /// Creates a GUI for displaying a Vector3 curve field in the animation field display GUI element. /// internal class GUIAnimVec3Entry : GUIAnimComplexEntry { /// /// Constructs a new animation field entry and appends the necessary GUI elements to the provided layouts. /// /// Layouts to append the GUI elements to. /// Path of the curve field. /// Colors of the curves to display, for each child entry. public GUIAnimVec3Entry(GUIAnimFieldLayouts layouts, string path, Color[] colors) : base(layouts, path, new[] { ".x", ".y", ".z" }, colors) { } /// public override void SetValue(object value) { if (value == null) return; Vector3 vector = (Vector3)value; children[0].SetValue(vector.x); children[1].SetValue(vector.y); children[2].SetValue(vector.z); } } /// /// Creates a GUI for displaying a Vector4 curve field in the animation field display GUI element. /// internal class GUIAnimVec4Entry : GUIAnimComplexEntry { /// /// Constructs a new animation field entry and appends the necessary GUI elements to the provided layouts. /// /// Layouts to append the GUI elements to. /// Path of the curve field. /// Colors of the curves to display, for each child entry. public GUIAnimVec4Entry(GUIAnimFieldLayouts layouts, string path, Color[] colors) : base(layouts, path, new[] { ".x", ".y", ".z", ".w" }, colors) { } /// public override void SetValue(object value) { if (value == null) return; Vector4 vector = (Vector4)value; children[0].SetValue(vector.x); children[1].SetValue(vector.y); children[2].SetValue(vector.z); children[3].SetValue(vector.w); } } /// /// Creates a GUI for displaying a Color curve field in the animation field display GUI element. /// internal class GUIAnimColorEntry : GUIAnimComplexEntry { /// /// Constructs a new animation field entry and appends the necessary GUI elements to the provided layouts. /// /// Layouts to append the GUI elements to. /// Path of the curve field. /// Colors of the curves to display, for each child entry. public GUIAnimColorEntry(GUIAnimFieldLayouts layouts, string path, Color[] colors) : base(layouts, path, new[] { ".r", ".g", ".b", ".a" }, colors) { } /// public override void SetValue(object value) { if (value == null) return; Color color = (Color)value; children[0].SetValue(color.r); children[1].SetValue(color.g); children[2].SetValue(color.b); children[3].SetValue(color.a); } } /// /// Creates a GUI for displaying an entry in the animation field display GUI element that notifies the user that /// a referenced curve field path cannot be found. /// internal class GUIAnimMissingEntry : GUIAnimFieldEntry { private GUILabel missingLabel; private GUILayoutX underlayLayout; private GUILabel overlaySpacing; public GUIAnimMissingEntry(GUIAnimFieldLayouts layouts, string path) : base(layouts, path, false, 15) { missingLabel = new GUILabel("Missing!", GUIOption.FixedHeight(GetEntryHeight())); underlayLayout = layouts.underlay.AddLayoutX(); underlayLayout.AddFlexibleSpace(); underlayLayout.AddElement(missingLabel); underlayLayout.AddSpace(15); overlaySpacing = new GUILabel("", GUIOption.FixedHeight(GetEntryHeight())); layouts.overlay.AddElement(overlaySpacing); } /// public override void Toggle(bool on) { underlayLayout.Active = on; overlaySpacing.Active = on; base.Toggle(on); } } /// /// Contains information required to display a single curve field entry in the animation field display GUI. /// internal struct AnimFieldInfo { public AnimFieldInfo(string path, FieldAnimCurves curveGroup) { this.path = path; this.curveGroup = curveGroup; } public string path; public FieldAnimCurves curveGroup; } /** @} */ }