using System; using System.Collections; using System.Collections.Generic; using BansheeEngine; namespace BansheeEditor { /// /// Base class for objects that display GUI for a modifyable dictionary of elements. Elements can be added, modified or /// removed. /// public abstract class GUIDictionaryFieldBase { private const int IndentAmount = 5; protected List rows = new List(); protected GUILayoutX guiChildLayout; protected GUILayoutX guiTitleLayout; protected GUILayoutY guiContentLayout; protected bool isExpanded; protected int depth; /// /// Constructs a new GUI dictionary. /// protected GUIDictionaryFieldBase() { } /// /// Updates the GUI dictionary contents. Must be called at least once in order for the contents to be populated. /// /// Type of rows that are used to handle GUI for individual dictionary elements. /// Label to display on the dictionary GUI title. /// Should the created field represent a null object. /// Number of rows to create GUI for. Only matters for a non-null dictionary. /// 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 void Update(LocString title, bool empty, int numRows, GUILayout layout, int depth = 0) where T : GUIDictionaryFieldRow, new() { Destroy(); this.depth = depth; if (empty) { guiChildLayout = null; guiContentLayout = null; guiTitleLayout = layout.AddLayoutX(); guiTitleLayout.AddElement(new GUILabel(title)); guiTitleLayout.AddElement(new GUILabel("Empty", GUIOption.FixedWidth(100))); GUIContent createIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Create)); GUIButton createBtn = new GUIButton(createIcon, GUIOption.FixedWidth(30)); createBtn.OnClick += OnCreateButtonClicked; guiTitleLayout.AddElement(createBtn); } else { GUIToggle guiFoldout = new GUIToggle(title, EditorStyles.Foldout); guiFoldout.Value = isExpanded; guiFoldout.OnToggled += OnFoldoutToggled; GUIContent clearIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Clear)); GUIButton guiClearBtn = new GUIButton(clearIcon, GUIOption.FixedWidth(30)); guiClearBtn.OnClick += OnClearButtonClicked; guiTitleLayout = layout.AddLayoutX(); guiTitleLayout.AddElement(guiFoldout); guiTitleLayout.AddElement(guiClearBtn); if (numRows > 0) { guiChildLayout = layout.AddLayoutX(); guiChildLayout.AddSpace(IndentAmount); guiChildLayout.Enabled = 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 ? EditorStyles.InspectorContentBgAlternate : EditorStyles.InspectorContentBg; GUIPanel backgroundPanel = guiContentPanel.AddPanel(backgroundDepth); GUITexture inspectorContentBg = new GUITexture(null, bgPanelStyle); backgroundPanel.AddElement(inspectorContentBg); for (int i = 0; i < numRows; i++) { GUIDictionaryFieldRow newRow = new T(); newRow.BuildGUI(this, guiContentLayout, i, depth); rows.Add(newRow); } } } } /// /// 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 dictionary rows and checks if anything was modified. /// /// True if any entry in the list was modified, false otherwise. public bool Refresh() { bool anythingModified = false; for (int i = 0; i < rows.Count; i++) { bool updateGUI; anythingModified |= rows[i].Refresh(out updateGUI); if (updateGUI) rows[i].BuildGUI(this, guiContentLayout, i, depth); } return anythingModified; } /// /// 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(); } /// /// Gets a value of an element at the specified index in the list. /// /// Key of the element whose value to retrieve. /// Value of the list element at the specified key. protected internal abstract object GetValue(object key); /// /// Sets a value of an element at the specified index in the list. /// /// Key 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(object key, object value); /// /// Checks does the element with the specified key exist in the dictionary. /// /// Key of the element to check for existence. /// True if the key exists in the dictionary, false otherwise. protected internal abstract bool Contains(object key); /// /// Triggered when the user clicks on the expand/collapse toggle in the title bar. /// /// Determines whether the contents were expanded or collapsed. private void OnFoldoutToggled(bool expanded) { isExpanded = expanded; if (guiChildLayout != null) guiChildLayout.Enabled = isExpanded; } /// /// Triggered when the user clicks on the create button on the title bar. Creates a brand new dictionary with zero /// elements in the place of the current dictionary. /// protected abstract void OnCreateButtonClicked(); /// /// Triggered when the user clicks on the clear button on the title bar. Deletes the current dictionary object. /// protected abstract void OnClearButtonClicked(); /// /// Triggered when the user clicks on the delete button next to a dictionary entry. Deletes an element in the /// dictionary. /// /// Key of the element to remove. protected internal abstract void OnDeleteButtonClicked(object key); /// /// Triggered when the user clicks on the clone button next to a dictionary entry. Clones an element and /// adds the clone to the dictionary. /// /// Key of the element to clone. protected internal abstract void OnCloneButtonClicked(object key); } /// /// 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 dictionary elements. /// /// Type of key used by the dictionary. /// Type of value stored in the dictionary. public class GUIDictionaryField : GUIDictionaryFieldBase { /// /// 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 list has been changed. /// public Action OnValueChanged; /// /// Array object whose contents are displayed. /// public IDictionary Dictionary { get { return dictionary; } } protected IDictionary dictionary; protected Type keyType; protected Type valueType; /// /// Constructs a new empty dictionary GUI. /// public GUIDictionaryField() { } /// /// Updates the GUI dictionary contents. Must be called at least once in order for the contents to be populated. /// /// Type of rows that are used to handle GUI for individual dictionary elements. /// Label to display on the list GUI title. /// Object containing the 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. public void Update(LocString title, Dictionary dictionary, GUILayout layout, int depth = 0) where RowType : GUIDictionaryFieldRow, new() { this.keyType = typeof(Key); this.valueType = typeof(Value); this.dictionary = dictionary; if (dictionary != null) base.Update(title, false, dictionary.Count, layout, depth); else base.Update(title, true, 0, layout, depth); } /// protected internal override object GetValue(object key) { return dictionary[key]; } /// protected internal override void SetValue(object key, object value) { dictionary[key] = value; if (OnValueChanged != null) OnValueChanged(); } /// protected internal override bool Contains(object key) { return dictionary.Contains(key);; } /// protected override void OnCreateButtonClicked() { dictionary = new Dictionary(); if (OnChanged != null) OnChanged(dictionary); } /// protected override void OnClearButtonClicked() { dictionary = null; if (OnChanged != null) OnChanged(dictionary); } /// protected internal override void OnDeleteButtonClicked(object key) { dictionary.Remove(key); if (OnValueChanged != null) OnValueChanged(); } /// /// Triggered when the user clicks on the clone button next to a dictionary entry. Clones the element and adds the /// clone to the dictionary. Non-value types must implement the interface in order to be /// cloned. If it doesn't the clone will point to a null reference. /// /// Key of the element to clone. protected internal override void OnCloneButtonClicked(object key) { // TODO - Not supported //int size = array.GetLength(0) + 1; //Array newArray = Array.CreateInstance(arrayType.GetElementType(), 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 // { // ValueType valueType = value as ValueType; // if (valueType != null) // clonedEntry = valueType; // else // { // ICloneable cloneable = value as ICloneable; // if (cloneable != null) // clonedEntry = cloneable.Clone(); // else // clonedEntry = null; // } // } // } //} //newArray.SetValue(clonedEntry, size - 1); //array = newArray; //if (OnChanged != null) // OnChanged(array); } } /// /// Contains GUI elements for a single entry in a dictionary. /// public abstract class GUIDictionaryFieldRow { private GUILayoutX rowLayout; private GUILayoutY keyLayout; private GUILayoutY valueLayout; private GUILayoutX titleLayout; private bool localTitleLayout; private GUIDictionaryFieldBase parent; protected object key; protected int depth; /// /// Constructs a new dictionary row object. /// protected GUIDictionaryFieldRow() { } /// /// (Re)creates all row GUI elements. /// /// Parent array GUI object that the entry is contained in. /// Parent layout that row GUI elements will be added to. /// Key of the element to create GUI for. /// Determines the depth at which the element is rendered. public void BuildGUI(GUIDictionaryFieldBase parent, GUILayout parentLayout, object key, int depth) { this.parent = parent; this.key = key; this.depth = depth; if (rowLayout == null) rowLayout = parentLayout.AddLayoutX(); if (keyLayout == null) keyLayout = rowLayout.AddLayoutY(); if (valueLayout == null) valueLayout = rowLayout.AddLayoutY(); CreateKeyGUI(keyLayout); GUILayoutX externalTitleLayout = CreateValueGUI(valueLayout); 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)); GUIContent deleteIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Delete)); GUIButton cloneBtn = new GUIButton(cloneIcon, GUIOption.FixedWidth(30)); GUIButton deleteBtn = new GUIButton(deleteIcon, GUIOption.FixedWidth(30)); cloneBtn.OnClick += () => parent.OnCloneButtonClicked(key); deleteBtn.OnClick += () => parent.OnDeleteButtonClicked(key); titleLayout.AddElement(cloneBtn); titleLayout.AddElement(deleteBtn); } /// /// Creates GUI elements specific to type in the key portion of a dictionary entry. /// /// Layout to insert the row GUI elements to. protected abstract void CreateKeyGUI(GUILayoutY layout); /// /// Creates GUI elements specific to type in the key portion of a dictionary entry. /// /// Layout to insert the row GUI elements to. /// An optional title bar layout that the standard dictionary buttons will be appended to. protected abstract GUILayoutX CreateValueGUI(GUILayoutY layout); /// /// Refreshes the GUI for the dictionary row and checks if anything was modified. /// /// Determines should the field's GUI elements be updated due to modifications. /// True if any modifications were made, false otherwise. internal protected virtual bool Refresh(out bool rebuildGUI) { rebuildGUI = false; return false; } /// /// Gets the value contained in this dictionary's row. /// /// Type of the value. Must match the dictionary's element type. /// Value in this dictionary's row. protected T GetValue() { return (T)parent.GetValue(key); } /// /// Sets the value contained in this dictionary's row. /// /// Type of the value. Must match the dictionary's element type. /// Value to set. protected void SetValue(T value) { parent.SetValue(key, value); } /// /// Destroys all row GUI elements. /// public void Destroy() { rowLayout.Destroy(); rowLayout = null; } } }