//********************************** Banshee Engine (www.banshee3d.com) **************************************************// //**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************// using System; using System.Collections; using System.Collections.Generic; using bs; namespace bs.Editor { /** @addtogroup GUI-Editor * @{ */ /// /// Base class for objects that display GUI for a modifyable list of elements. Elements can be added, removed and moved. /// public abstract class GUIListFieldBase { private const int IndentAmount = 5; protected List rows = new List(); protected GUILayoutY guiLayout; protected GUIIntField guiSizeField; protected GUILayoutX guiChildLayout; protected GUILayoutX guiTitleLayout; protected GUILayoutX guiInternalTitleLayout; protected GUILayoutY guiContentLayout; protected GUIToggle guiFoldout; protected bool isExpanded; protected int depth; protected LocString title; private State state; private bool isModified; /// /// Expands or collapses the entries of the dictionary. /// public bool IsExpanded { get { return isExpanded; } set { if (isExpanded != value) ToggleFoldout(value, true); } } /// /// Event that triggers when the list foldout is expanded or collapsed (rows are shown or hidden). /// public Action OnExpand; /// /// Constructs a new GUI list. /// /// Label to display on the list GUI title. /// Layout to which to append the array GUI elements to. /// Determines at which depth to render the background. Useful when you have multiple /// nested containers whose backgrounds are overlaping. Also determines background style, /// depths divisible by two will use an alternate style. protected GUIListFieldBase(LocString title, GUILayout layout, int depth) { this.title = title; this.depth = depth; guiLayout = layout.AddLayoutY(); guiTitleLayout = guiLayout.AddLayoutX(); } /// /// (Re)builds the list GUI elements. Must be called at least once in order for the contents to be populated. /// public void BuildGUI(bool force = false) { UpdateHeaderGUI(); if (!IsNull()) { // Hidden dependency: Initialize must be called after all elements are // in the dictionary so we do it in two steps int numRows = GetNumRows(); int oldNumRows = rows.Count; for (int i = oldNumRows; i < numRows; i++) { GUIListFieldRow newRow = CreateRow(); rows.Add(newRow); } for (int i = oldNumRows - 1; i >= numRows; i--) { rows[i].Destroy(); rows.RemoveAt(i); } for (int i = oldNumRows; i < numRows; i++) rows[i].Initialize(this, guiContentLayout, i, depth + 1); for (int i = 0; i < rows.Count; i++) rows[i].SetIndex(i); if(force) guiSizeField.Value = numRows; } else { foreach (var row in rows) row.Destroy(); rows.Clear(); } } /// /// Rebuilds the GUI list header if needed. /// protected void UpdateHeaderGUI() { Action BuildEmptyGUI = () => { guiInternalTitleLayout = guiTitleLayout.InsertLayoutX(0); guiInternalTitleLayout.AddElement(new GUILabel(title)); guiInternalTitleLayout.AddElement(new GUILabel("Empty", GUIOption.FixedWidth(100))); GUIContent createIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Create), new LocEdString("Create")); GUIButton createBtn = new GUIButton(createIcon, GUIOption.FixedWidth(30)); createBtn.OnClick += OnCreateButtonClicked; guiInternalTitleLayout.AddElement(createBtn); }; Action BuildFilledGUI = () => { guiInternalTitleLayout = guiTitleLayout.InsertLayoutX(0); guiFoldout = new GUIToggle(title, EditorStyles.Foldout); guiFoldout.Value = isExpanded; guiFoldout.AcceptsKeyFocus = false; guiFoldout.OnToggled += x => ToggleFoldout(x, false); guiSizeField = new GUIIntField("", GUIOption.FixedWidth(50)); guiSizeField.SetRange(0, int.MaxValue); GUIContent resizeIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Resize), new LocEdString("Resize")); GUIButton guiResizeBtn = new GUIButton(resizeIcon, GUIOption.FixedWidth(30)); guiResizeBtn.OnClick += OnResizeButtonClicked; GUIContent clearIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Clear), new LocEdString("Clear")); GUIButton guiClearBtn = new GUIButton(clearIcon, GUIOption.FixedWidth(30)); guiClearBtn.OnClick += OnClearButtonClicked; guiInternalTitleLayout.AddElement(guiFoldout); guiInternalTitleLayout.AddElement(guiSizeField); guiInternalTitleLayout.AddElement(guiResizeBtn); guiInternalTitleLayout.AddElement(guiClearBtn); guiSizeField.Value = GetNumRows(); guiChildLayout = guiLayout.AddLayoutX(); guiChildLayout.AddSpace(IndentAmount); guiChildLayout.Active = isExpanded; GUIPanel guiContentPanel = guiChildLayout.AddPanel(); GUILayoutX guiIndentLayoutX = guiContentPanel.AddLayoutX(); guiIndentLayoutX.AddSpace(IndentAmount); GUILayoutY guiIndentLayoutY = guiIndentLayoutX.AddLayoutY(); guiIndentLayoutY.AddSpace(IndentAmount); guiContentLayout = guiIndentLayoutY.AddLayoutY(); guiIndentLayoutY.AddSpace(IndentAmount); guiIndentLayoutX.AddSpace(IndentAmount); guiChildLayout.AddSpace(IndentAmount); short backgroundDepth = (short)(Inspector.START_BACKGROUND_DEPTH - depth - 1); string bgPanelStyle = depth % 2 == 0 ? EditorStylesInternal.InspectorContentBgAlternate : EditorStylesInternal.InspectorContentBg; GUIPanel backgroundPanel = guiContentPanel.AddPanel(backgroundDepth); GUITexture inspectorContentBg = new GUITexture(null, bgPanelStyle); backgroundPanel.AddElement(inspectorContentBg); }; if (state == State.None) { if (!IsNull()) { BuildFilledGUI(); state = State.Filled; } else { BuildEmptyGUI(); state = State.Empty; } } else if (state == State.Empty) { if (!IsNull()) { guiInternalTitleLayout.Destroy(); BuildFilledGUI(); state = State.Filled; } } else if (state == State.Filled) { if (IsNull()) { guiInternalTitleLayout.Destroy(); guiChildLayout.Destroy(); BuildEmptyGUI(); state = State.Empty; } } } /// /// Returns the layout that is used for positioning the elements in the title bar. /// /// Horizontal layout for positioning the title bar elements. public GUILayoutX GetTitleLayout() { return guiTitleLayout; } /// /// Refreshes contents of all list rows and checks if anything was modified. /// /// Forces the GUI fields to display the latest values assigned on the object. /// State representing was anything modified between two last calls to . public virtual InspectableState Refresh(bool force) { InspectableState state = InspectableState.NotModified; for (int i = 0; i < rows.Count; i++) state |= rows[i].Refresh(force); if (isModified) { state |= InspectableState.Modified; isModified = false; } return state; } /// /// Destroys the GUI elements. /// public void Destroy() { if (guiTitleLayout != null) { guiTitleLayout.Destroy(); guiTitleLayout = null; } if (guiChildLayout != null) { guiChildLayout.Destroy(); guiChildLayout = null; } for (int i = 0; i < rows.Count; i++) rows[i].Destroy(); rows.Clear(); } /// /// Creates a new list row GUI. /// /// Object containing the list row GUI. protected abstract GUIListFieldRow CreateRow(); /// /// Checks is the list instance not assigned. /// /// True if there is not a list instance. protected abstract bool IsNull(); /// /// Returns the number of rows in the list. /// /// Number of rows in the list. protected abstract int GetNumRows(); /// /// Gets a value of an element at the specified index in the list. /// /// Sequential index of the element whose value to retrieve. /// Value of the list element at the specified index. protected internal abstract object GetValue(int seqIndex); /// /// Sets a value of an element at the specified index in the list. /// /// Sequential index of the element whose value to set. /// Value to assign to the element. Caller must ensure it is of valid type. protected internal abstract void SetValue(int seqIndex, object value); /// /// Triggered when the user clicks on the expand/collapse toggle in the title bar. /// /// Determines whether the contents were expanded or collapsed. /// True if the foldout was expanded/collapsed from outside code. private void ToggleFoldout(bool expanded, bool external) { isExpanded = expanded; if (guiChildLayout != null) guiChildLayout.Active = isExpanded; if (external) { if (guiFoldout != null) guiFoldout.Value = isExpanded; } if (OnExpand != null) OnExpand(expanded); } /// /// Triggered when the user clicks on the create button on the title bar. Creates a brand new list with zero /// elements in the place of the current list. /// protected void OnCreateButtonClicked() { CreateList(); BuildGUI(); isModified = true; } /// /// Triggered when the user clicks on the resize button on the title bar. Changes the size of the list while /// preserving existing contents. /// protected void OnResizeButtonClicked() { ResizeList(); BuildGUI(); isModified = true; } /// /// Triggered when the user clicks on the clear button on the title bar. Deletes the current list object. /// protected void OnClearButtonClicked() { ClearList(); BuildGUI(); isModified = true; } /// /// Triggered when the user clicks on the delete button next to a list entry. Deletes an element in the list. /// /// Sequential index of the element in the list to remove. protected internal void OnDeleteButtonClicked(int index) { DeleteElement(index); guiSizeField.Value = GetNumRows(); BuildGUI(); isModified = true; } /// /// Triggered when the user clicks on the clone button next to a list entry. Clones the element and adds the clone /// to the back of the list. /// /// Sequential index of the element in the list to clone. protected internal void OnCloneButtonClicked(int index) { CloneElement(index); guiSizeField.Value = GetNumRows(); BuildGUI(); isModified = true; } /// /// Triggered when the user clicks on the move up button next to a list entry. Moves an element from the current /// list index to the one right before it, if not at zero. /// /// Sequential index of the element in the list to move. protected internal void OnMoveUpButtonClicked(int index) { MoveUpElement(index); BuildGUI(); isModified = true; } /// /// Triggered when the user clicks on the move down button next to a list entry. Moves an element from the current /// list index to the one right after it, if the element isn't already the last element. /// /// Sequential index of the element in the list to move. protected internal void OnMoveDownButtonClicked(int index) { MoveDownElement(index); BuildGUI(); isModified = true; } /// /// Creates a brand new list with zero elements in the place of the current list. /// protected abstract void CreateList(); /// /// Changes the size of the list while preserving existing contents. /// protected abstract void ResizeList(); /// /// Deletes the current list object. /// protected abstract void ClearList(); /// /// Deletes an element in the list. /// /// Sequential index of the element in the list to remove. protected internal abstract void DeleteElement(int index); /// /// Clones the element and adds the clone to the back of the list. /// /// Sequential index of the element in the list to clone. protected internal abstract void CloneElement(int index); /// /// Moves an element from the current list index to the one right before it, if not at zero. /// /// Sequential index of the element in the list to move. protected internal abstract void MoveUpElement(int index); /// /// Moves an element from the current list index to the one right after it, if the element isn't already the last /// element. /// /// Sequential index of the element in the list to move. protected internal abstract void MoveDownElement(int index); /// /// Possible states list GUI can be in. /// private enum State { None, Empty, Filled } } /// /// Creates GUI elements that allow viewing and manipulation of a . When constructing the /// object user can provide a custom type that manages GUI for individual array elements. /// /// Type of elements stored in the array. /// Type of rows that are used to handle GUI for individual array elements. public class GUIArrayField : GUIListFieldBase where RowType : GUIListFieldRow, new() { /// /// Triggered when the reference array has been changed. This does not include changes that only happen to its /// internal elements. /// public Action OnChanged; /// /// Triggered when an element in the array has been changed. /// public Action OnValueChanged; /// /// Array object whose contents are displayed. /// public ElementType[] Array { get { return array; } } protected ElementType[] array; /// /// Constructs a new GUI array field. /// /// Label to display on the array GUI title. /// Object containing the array data. Can be null. /// Layout to which to append the array GUI elements to. /// Determines at which depth to render the background. Useful when you have multiple /// nested containers whose backgrounds are overlaping. Also determines background style, /// depths divisible by two will use an alternate style. protected GUIArrayField(LocString title, ElementType[] array, GUILayout layout, int depth = 0) :base(title, layout, depth) { this.array = array; } /// /// Creates a array GUI field containing GUI elements for displaying an array. /// /// Label to display on the array GUI title. /// Object containing the array data. Can be null. /// Layout to which to append the array GUI elements to. /// Determines at which depth to render the background. Useful when you have multiple /// nested containers whose backgrounds are overlaping. Also determines background style, /// depths divisible by two will use an alternate style. /// New instance of an array GUI field. public static GUIArrayField Create(LocString title, ElementType[] array, GUILayout layout, int depth = 0) { GUIArrayField guiArray = new GUIArrayField(title, array, layout, depth); guiArray.BuildGUI(); return guiArray; } /// protected override GUIListFieldRow CreateRow() { return new RowType(); } /// protected override bool IsNull() { return array == null; } /// protected override int GetNumRows() { if (array != null) return array.GetLength(0); return 0; } /// protected internal override object GetValue(int seqIndex) { return array.GetValue(seqIndex); } /// protected internal override void SetValue(int seqIndex, object value) { array.SetValue(value, seqIndex); if (OnValueChanged != null) OnValueChanged(); } /// protected override void CreateList() { array = new ElementType[0]; if (OnChanged != null) OnChanged(array); } /// protected override void ResizeList() { int size = guiSizeField.Value; ElementType[] newArray = new ElementType[size]; int oldSize = array.GetLength(0); int maxSize = MathEx.Min(size, oldSize); for (int i = 0; i < maxSize; i++) newArray.SetValue(array.GetValue(i), i); array = newArray; if (OnChanged != null) OnChanged(array); } /// protected override void ClearList() { array = null; if (OnChanged != null) OnChanged(array); } /// protected internal override void DeleteElement(int index) { int size = MathEx.Max(0, array.GetLength(0) - 1); ElementType[] newArray = new ElementType[size]; int destIdx = 0; for (int i = 0; i < array.GetLength(0); i++) { if (i == index) continue; newArray.SetValue(array.GetValue(i), destIdx); destIdx++; } array = newArray; if (OnChanged != null) OnChanged(array); } /// protected internal override void CloneElement(int index) { int size = array.GetLength(0) + 1; ElementType[] newArray = new ElementType[size]; object clonedEntry = null; for (int i = 0; i < array.GetLength(0); i++) { object value = array.GetValue(i); newArray.SetValue(value, i); if (i == index) { if (value == null) clonedEntry = null; else clonedEntry = SerializableUtility.Clone(value); } } newArray.SetValue(clonedEntry, size - 1); array = newArray; if (OnChanged != null) OnChanged(array); } /// protected internal override void MoveUpElement(int index) { if ((index - 1) >= 0) { object previousEntry = array.GetValue(index - 1); array.SetValue(array.GetValue(index), index - 1); array.SetValue(previousEntry, index); if (OnValueChanged != null) OnValueChanged(); } } /// protected internal override void MoveDownElement(int index) { if ((index + 1) < array.GetLength(0)) { object nextEntry = array.GetValue(index + 1); array.SetValue(array.GetValue(index), index + 1); array.SetValue(nextEntry, index); if (OnValueChanged != null) OnValueChanged(); } } } /// /// Creates GUI elements that allow viewing and manipulation of a . When constructing the /// object user can provide a custom type that manages GUI for individual list elements. /// /// Type of elements stored in the list. /// Type of rows that are used to handle GUI for individual list elements. public class GUIListField : GUIListFieldBase where RowType : GUIListFieldRow, new() { /// /// Triggered when the reference list has been changed. This does not include changes that only happen to its /// internal elements. /// public Action> OnChanged; /// /// Triggered when an element in the list has been changed. /// public Action OnValueChanged; /// /// List object whose contents are displayed. /// public List List { get { return list; } } protected List list; /// /// Constructs a new GUI list field. /// /// Label to display on the list GUI title. /// Object containing the list data. Can be null. /// Layout to which to append the list GUI elements to. /// Determines at which depth to render the background. Useful when you have multiple /// nested containers whose backgrounds are overlaping. Also determines background style, /// depths divisible by two will use an alternate style. protected GUIListField(LocString title, List list, GUILayout layout, int depth = 0) : base(title, layout, depth) { this.list = list; } /// /// Creates a list GUI field containing GUI elements for displaying a list. /// /// Label to display on the list GUI title. /// Object containing the list data. Can be null. /// Layout to which to append the list GUI elements to. /// Determines at which depth to render the background. Useful when you have multiple /// nested containers whose backgrounds are overlaping. Also determines background style, /// depths divisible by two will use an alternate style. /// New instance of a list GUI field. public static GUIListField Create(LocString title, List list, GUILayout layout, int depth = 0) { GUIListField guiList = new GUIListField(title, list, layout, depth); guiList.BuildGUI(); return guiList; } /// protected override GUIListFieldRow CreateRow() { return new RowType(); } /// protected override bool IsNull() { return list == null; } /// protected override int GetNumRows() { if (list != null) return list.Count; return 0; } /// protected internal override object GetValue(int seqIndex) { return list[seqIndex]; } /// protected internal override void SetValue(int seqIndex, object value) { list[seqIndex] = (ElementType)value; if (OnValueChanged != null) OnValueChanged(); } /// protected override void CreateList() { list = new List(); if (OnChanged != null) OnChanged(list); } /// protected override void ResizeList() { int size = guiSizeField.Value; if(size == list.Count) return; if (size < list.Count) list.RemoveRange(size, list.Count - size); else { ElementType[] extraElements = new ElementType[size - list.Count]; list.AddRange(extraElements); } if (OnValueChanged != null) OnValueChanged(); } /// protected override void ClearList() { list = null; if (OnChanged != null) OnChanged(list); } /// protected internal override void DeleteElement(int index) { list.RemoveAt(index); if (OnValueChanged != null) OnValueChanged(); } /// protected internal override void CloneElement(int index) { object clonedEntry = null; if (list[index] != null) clonedEntry = SerializableUtility.Clone(list[index]); list.Add((ElementType)clonedEntry); if (OnValueChanged != null) OnValueChanged(); } /// protected internal override void MoveUpElement(int index) { if ((index - 1) >= 0) { ElementType previousEntry = list[index - 1]; list[index - 1] = list[index]; list[index] = previousEntry; if (OnValueChanged != null) OnValueChanged(); } } /// protected internal override void MoveDownElement(int index) { if ((index + 1) < list.Count) { ElementType nextEntry = list[index + 1]; list[index + 1] = list[index]; list[index] = nextEntry; if (OnValueChanged != null) OnValueChanged(); } } } /// /// Contains GUI elements for a single entry in a list. /// public abstract class GUIListFieldRow { private GUILayoutX rowLayout; private GUILayoutY contentLayout; private GUILayoutX titleLayout; private bool localTitleLayout; private int seqIndex; private int depth; private InspectableState modifiedState; protected GUIListFieldBase parent; /// /// Returns the sequential index of the list entry that this row displays. /// protected int SeqIndex { get { return seqIndex; } } /// /// Returns the depth at which the row is rendered. /// protected int Depth { get { return depth; } } /// /// Constructs a new list row object. /// protected GUIListFieldRow() { } /// /// Initializes the row and creates row GUI elements. /// /// Parent array GUI object that the entry is contained in. /// Parent layout that row GUI elements will be added to. /// Sequential index of the list entry. /// Determines the depth at which the element is rendered. internal void Initialize(GUIListFieldBase parent, GUILayout parentLayout, int seqIndex, int depth) { this.parent = parent; this.seqIndex = seqIndex; this.depth = depth; rowLayout = parentLayout.AddLayoutX(); contentLayout = rowLayout.AddLayoutY(); BuildGUI(); } /// /// Changes the index of the list element this row represents. /// /// Sequential index of the list entry. internal void SetIndex(int seqIndex) { this.seqIndex = seqIndex; } /// /// (Re)creates all row GUI elements. /// internal protected void BuildGUI() { contentLayout.Clear(); GUILayoutX externalTitleLayout = CreateGUI(contentLayout); if (localTitleLayout || (titleLayout != null && titleLayout == externalTitleLayout)) return; if (externalTitleLayout != null) { localTitleLayout = false; titleLayout = externalTitleLayout; } else { GUILayoutY buttonCenter = rowLayout.AddLayoutY(); buttonCenter.AddFlexibleSpace(); titleLayout = buttonCenter.AddLayoutX(); buttonCenter.AddFlexibleSpace(); localTitleLayout = true; } GUIContent cloneIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Clone), new LocEdString("Clone")); GUIContent deleteIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Delete), new LocEdString("Delete")); GUIContent moveUp = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.MoveUp), new LocEdString("Move up")); GUIContent moveDown = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.MoveDown), new LocEdString("Move down")); GUIButton cloneBtn = new GUIButton(cloneIcon, GUIOption.FixedWidth(30)); GUIButton deleteBtn = new GUIButton(deleteIcon, GUIOption.FixedWidth(30)); GUIButton moveUpBtn = new GUIButton(moveUp, GUIOption.FixedWidth(30)); GUIButton moveDownBtn = new GUIButton(moveDown, GUIOption.FixedWidth(30)); cloneBtn.OnClick += () => parent.OnCloneButtonClicked(seqIndex); deleteBtn.OnClick += () => parent.OnDeleteButtonClicked(seqIndex); moveUpBtn.OnClick += () => parent.OnMoveUpButtonClicked(seqIndex); moveDownBtn.OnClick += () => parent.OnMoveDownButtonClicked(seqIndex); titleLayout.AddElement(cloneBtn); titleLayout.AddElement(deleteBtn); titleLayout.AddElement(moveUpBtn); titleLayout.AddElement(moveDownBtn); } /// /// Creates GUI elements specific to type in the array row. /// /// Layout to insert the row GUI elements to. /// An optional title bar layout that the standard array buttons will be appended to. protected abstract GUILayoutX CreateGUI(GUILayoutY layout); /// /// Refreshes the GUI for the list row and checks if anything was modified. /// /// 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) { InspectableState oldState = modifiedState; if (modifiedState.HasFlag(InspectableState.Modified)) modifiedState = InspectableState.NotModified; return oldState; } /// /// Marks the contents of the row as modified. /// protected void MarkAsModified() { modifiedState |= InspectableState.ModifyInProgress; } /// /// Confirms any queued modifications, signaling parent elements. /// protected void ConfirmModify() { if (modifiedState.HasFlag(InspectableState.ModifyInProgress)) modifiedState |= InspectableState.Modified; } /// /// Gets the value contained in this list row. /// /// Type of the value. Must match the list's element type. /// Value in this list row. protected T GetValue() { return (T)parent.GetValue(seqIndex); } /// /// Sets the value contained in this list row. /// /// Type of the value. Must match the list's element type. /// Value to set. protected void SetValue(T value) { parent.SetValue(seqIndex, value); } /// /// Destroys all row GUI elements. /// public void Destroy() { if (rowLayout != null) { rowLayout.Destroy(); rowLayout = null; } contentLayout = null; titleLayout = null; localTitleLayout = false; } } /** @} */ }