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 Dictionary rows = new Dictionary();
protected GUIDictionaryFieldRow editRow;
protected GUILayoutY guiLayout;
protected GUILayoutX guiChildLayout;
protected GUILayoutX guiTitleLayout;
protected GUILayoutY guiContentLayout;
protected bool isExpanded;
protected int depth;
protected LocString title;
private int editRowIdx = -1;
private object editKey;
private object editValue;
private object editOriginalKey;
private State state;
///
/// Constructs a new GUI dictionary.
///
/// Label to display on the dictionary GUI title.
/// 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 GUIDictionaryFieldBase(LocString title, GUILayout layout, int depth = 0)
{
this.title = title;
this.guiLayout = layout.AddLayoutY();
this.depth = depth;
}
///
/// Completely rebuilds the dictionary GUI elements.
///
protected void BuildGUI()
{
editKey = CreateKey();
editValue = CreateValue();
UpdateHeaderGUI(true);
if (!IsNull())
{
// Hidden dependency: BuildGUI must be called after all elements are
// in the dictionary so we do it in two steps
int numRows = GetNumRows();
for (int i = 0; i < numRows; i++)
{
GUIDictionaryFieldRow newRow = CreateRow();
rows.Add(i, newRow);
}
editRow = CreateRow();
editRow.BuildGUI(this, guiContentLayout, numRows, depth + 1);
editRow.Enabled = false;
for (int i = 0; i < numRows; i++)
rows[i].BuildGUI(this, guiContentLayout, i, depth + 1);
}
}
///
/// Rebuilds the GUI dictionary header if needed.
///
/// Forces the header to be rebuilt.
protected void UpdateHeaderGUI(bool forceRebuild)
{
Action BuildEmptyGUI = () =>
{
guiTitleLayout = guiLayout.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);
};
Action BuildFilledGUI = () =>
{
GUIToggle guiFoldout = new GUIToggle(title, EditorStyles.Foldout);
guiFoldout.Value = isExpanded;
guiFoldout.OnToggled += ToggleFoldout;
GUIContent clearIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Clear));
GUIButton guiClearBtn = new GUIButton(clearIcon, GUIOption.FixedWidth(30));
guiClearBtn.OnClick += OnClearButtonClicked;
GUIContent addIcon = new GUIContent(EditorBuiltin.GetInspectorWindowIcon(InspectorWindowIcon.Add));
GUIButton guiAddBtn = new GUIButton(addIcon, GUIOption.FixedWidth(30));
guiAddBtn.OnClick += OnAddButtonClicked;
guiTitleLayout = guiLayout.AddLayoutX();
guiTitleLayout.AddElement(guiFoldout);
guiTitleLayout.AddElement(guiAddBtn);
guiTitleLayout.AddElement(guiClearBtn);
guiChildLayout = guiLayout.AddLayoutX();
guiChildLayout.AddSpace(IndentAmount);
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);
ToggleFoldout(isExpanded);
};
if (forceRebuild)
{
if (state != State.None)
guiTitleLayout.Destroy();
if (state == State.Filled)
guiChildLayout.Destroy();
state = State.None;
}
if (state == State.None)
{
if (!IsNull())
{
BuildFilledGUI();
state = State.Filled;
}
else
{
BuildEmptyGUI();
state = State.Empty;
}
}
else if (state == State.Empty)
{
if (!IsNull())
{
guiTitleLayout.Destroy();
BuildFilledGUI();
state = State.Filled;
}
}
else if (state == State.Filled)
{
if (IsNull())
{
guiTitleLayout.Destroy();
guiChildLayout.Destroy();
BuildEmptyGUI();
state = State.Empty;
}
}
}
///
/// Rebuilds GUI for all existing dictionary rows.
///
private void BuildRows()
{
foreach (var KVP in rows)
KVP.Value.Destroy();
editRow.Destroy();
if (!IsNull())
{
editRow.BuildGUI(this, guiContentLayout, rows.Count, depth + 1);
editRow.Enabled = editRowIdx == rows.Count;
foreach (var KVP in rows)
KVP.Value.BuildGUI(this, guiContentLayout, KVP.Key, depth + 1);
}
else
{
rows.Clear();
}
}
///
/// 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.
///
public void Refresh()
{
bool anythingModified = false;
for (int i = 0; i < rows.Count; i++)
anythingModified |= rows[i].Refresh();
if (editRow != null && editRow.Enabled)
anythingModified |= editRow.Refresh();
// Note: I could just rebuild the individual rows but I'd have to remember their layout positions
if (anythingModified)
BuildRows();
}
///
/// Destroys the GUI elements.
///
public void Destroy()
{
if (guiLayout != null)
{
guiLayout.Destroy();
guiLayout = null;
}
guiLayout = null;
guiTitleLayout = null;
guiChildLayout = null;
for (int i = 0; i < rows.Count; i++)
rows[i].Destroy();
rows.Clear();
if (editRow != null)
editRow.Destroy();
editRow = null;
}
///
/// Checks is the specified row index the temporary edit row.
///
/// Sequential index of the row to check.
/// True if the index is of an edit row.
private bool IsTemporaryRow(int rowIdx)
{
return rowIdx == rows.Count;
}
///
/// Checks is any row being currently edited.
///
/// True if a row is being edited, false otherwise.
private bool IsEditInProgress()
{
return editRowIdx != -1;
}
///
/// Returns the number of rows in the dictionary.
///
/// Number of rows in the dictionary.
protected abstract int GetNumRows();
///
/// Checks is the dictionary instance not assigned.
///
/// True if there is not a dictionary instance.
protected abstract bool IsNull();
///
/// Gets a value of an element at the specified index in the list. Also handles temporary edit fields.
///
/// Sequential index of the row to set the value for.
/// Value of the list element at the specified key.
protected internal virtual object GetValueInternal(int rowIdx)
{
if (rowIdx == editRowIdx || IsTemporaryRow(rowIdx))
return editValue;
else
return GetValue(GetKey(rowIdx));
}
///
/// Sets a value of an element at the specified index in the list. Also handles temporary edit fields.
///
/// Sequential index of the row to set the value for.
/// Value to assign to the element. Caller must ensure it is of valid type.
protected internal virtual void SetValueInternal(int rowIdx, object value)
{
if (rowIdx == editRowIdx || IsTemporaryRow(rowIdx))
editValue = value;
else
SetValue(GetKey(rowIdx), value);
}
///
/// Changes the value of the key of the specified row.
///
/// Sequential index of the row to set the key for.
/// Key to assign to the specified row.
protected internal void SetKey(int rowIdx, object key)
{
if (editRowIdx != rowIdx)
{
Debug.LogError("Cannot change a dictionary row that is not in edit state.");
return;
}
editKey = key;
}
///
/// Gets a key for a row at the specified index. Handles the special case for the currently edited row.
///
/// Sequential index of the row for which to retrieve the key.
/// Key for a row at the specified index.
protected internal object GetKeyInternal(int rowIdx)
{
if (editRowIdx == rowIdx || IsTemporaryRow(rowIdx))
return editKey;
return GetKey(rowIdx);
}
///
/// Creates a new dictionary row GUI.
///
/// Object containing the dictionary row GUI.
protected abstract GUIDictionaryFieldRow CreateRow();
///
/// Gets a key for a row at the specified index.
///
/// Sequential index of the row for which to retrieve the key.
/// Key for a row at the specified index.
protected internal abstract object GetKey(int rowIdx);
///
/// Gets a value of an element at the specified index in the list.
///
/// Key of the element whose value to retrieve.
/// Value of the dictionary entry for 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);
///
/// Updates both key and value of an existing entry.
///
/// Original key of the entry.
/// New key of the entry.
/// New value of the entry.
protected internal abstract void EditEntry(object oldKey, object newKey, object value);
///
/// Adds a new entry to the dictionary.
///
/// Key of the entry to add.
/// Value of the entry to add.
protected internal abstract void AddEntry(object key, object value);
///
/// Removes the specified entry from the dictionary.
///
/// Key of the entry to remove.
protected internal abstract void RemoveEntry(object key);
///
/// Creates a new empty key object of a valid type that can be used in the dictionary.
///
/// New empty key object.
protected internal abstract object CreateKey();
///
/// Creates a new empty value object of a valid type that can be used in the dictionary.
///
/// New empty value object.
protected internal abstract object CreateValue();
///
/// 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);
///
/// Creates a brand new dictionary with zero elements in the place of the current dictionary.
///
protected abstract void CreateDictionary();
///
/// Deletes the current dictionary object.
///
protected abstract void DeleteDictionary();
///
/// Hides or shows the dictionary rows.
///
/// True if the rows should be displayed, false otherwise.
private void ToggleFoldout(bool expanded)
{
isExpanded = expanded;
if (guiChildLayout != null)
guiChildLayout.Active = isExpanded && (rows.Count > 0 || IsEditInProgress());
}
///
/// 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 void OnCreateButtonClicked()
{
CreateDictionary();
UpdateHeaderGUI(false);
BuildRows();
}
///
/// Triggered when the user clicks on the add button on the title bar. Adds a new empty element to the dictionary.
///
protected virtual void OnAddButtonClicked()
{
if (IsEditInProgress())
{
DialogBox.Open(
new LocEdString("Edit in progress."),
new LocEdString("You are editing the entry with key \"" + editKey + "\". You cannot add a row " +
"before applying or discarding those changes. Do you wish to apply those changes first?"),
DialogBox.Type.YesNoCancel,
x =>
{
switch (x)
{
case DialogBox.ResultType.Yes:
if (ApplyChanges())
StartAdd();
break;
case DialogBox.ResultType.No:
DiscardChanges();
StartAdd();
break;
}
});
}
else
{
if (!isExpanded)
ToggleFoldout(true);
StartAdd();
}
}
///
/// Triggered when the user clicks on the clear button on the title bar. Deletes the current dictionary object.
///
protected void OnClearButtonClicked()
{
DeleteDictionary();
UpdateHeaderGUI(false);
BuildRows();
}
///
/// Triggered when the user clicks on the delete button next to a dictionary entry. Deletes an element in the
/// dictionary.
///
/// Sequential row index of the entry that was clicked.
protected internal virtual void OnDeleteButtonClicked(int rowIdx)
{
if (IsEditInProgress())
DiscardChanges();
else
{
// Remove the entry, but ensure the rows keep referencing the original keys (dictionaries have undefined
// order so we need to compare old vs. new elements to determine if any changed).
int oldNumRows = GetNumRows();
Dictionary