//********************************** 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 System.Runtime.CompilerServices; using System.Text; using bs; namespace bs.Editor { /** @addtogroup Inspector * @{ */ /// /// Displays GUI for a serializable property containing a dictionary. Dictionary contents are displayed as rows of /// entries that can be shown, hidden or manipulated. /// public class InspectableDictionary : InspectableField { private InspectableDictionaryGUI dictionaryGUIField; /// /// Creates a new inspectable dictionary GUI for the specified property. /// /// Context shared by all inspectable fields created by the same parent. /// Name of the property, or some other value to set as the title. /// Full path to this property (includes name of this property and all parent properties). /// Determines how deep within the inspector nesting hierarchy is this field. Some fields may /// contain other fields, in which case you should increase this value by one. /// Parent layout that all the field elements will be added to. /// Serializable property referencing the dictionary whose contents to display. public InspectableDictionary(InspectableContext context, string title, string path, int depth, InspectableFieldLayout layout, SerializableProperty property) : base(context, title, path, SerializableProperty.FieldType.Dictionary, depth, layout, property) { } /// public override GUILayoutX GetTitleLayout() { return dictionaryGUIField.GetTitleLayout(); } /// public override InspectableState Refresh(int layoutIndex) { return dictionaryGUIField.Refresh(); } /// protected internal override void Initialize(int layoutIndex) { GUILayout dictionaryLayout = layout.AddLayoutY(layoutIndex); dictionaryGUIField = InspectableDictionaryGUI.Create(context, title, path, property, dictionaryLayout, depth); dictionaryGUIField.IsExpanded = context.Persistent.GetBool(path + "_Expanded"); dictionaryGUIField.OnExpand += x => context.Persistent.SetBool(path + "_Expanded", x); } /// public override InspectableField FindPath(string path) { string subPath = GetSubPath(path, depth + 1); if (string.IsNullOrEmpty(subPath)) return null; int lastLeftIdx = subPath.LastIndexOf("Key["); int lastRightIdx = -1; bool isKey; if (lastLeftIdx != -1) { lastRightIdx = subPath.LastIndexOf(']', lastLeftIdx); isKey = true; } else { lastLeftIdx = subPath.LastIndexOf("Value["); lastRightIdx = subPath.LastIndexOf(']', lastLeftIdx); isKey = false; } if (lastLeftIdx == -1 || lastRightIdx == -1) return null; int count = lastRightIdx - 1 - lastLeftIdx; if (count <= 0) return null; string arrayIdxStr = subPath.Substring(lastLeftIdx, count); if (!int.TryParse(arrayIdxStr, out int idx)) return null;; if (idx >= dictionaryGUIField.NumRows) return null; InspectableDictionaryGUIRow row = dictionaryGUIField.GetRow(idx); InspectableField field = null; if (isKey) field = row?.FieldKey; else field = row?.FieldValue; if (field != null) { if (field.Path == path) return field; return field.FindPath(path); } return null; } /// /// Creates GUI elements that allow viewing and manipulation of a referenced /// by a serializable property. /// private class InspectableDictionaryGUI : GUIDictionaryFieldBase { private SerializableProperty property; private IDictionary dictionary; private int numElements; private InspectableContext context; private string path; private List orderedKeys = new List(); /// /// Context shared by all inspectable fields created by the same parent. /// public InspectableContext Context { get { return context; } } /// /// Returns a property path to the array field (name of the array field and all parent object fields). /// public string Path { get { return path; } } /// /// Returns the number of rows in the array. /// public int NumRows { get { return GetNumRows(); } } /// /// Constructs a new dictionary GUI. /// /// Context shared by all inspectable fields created by the same parent. /// Label to display on the list GUI title. /// Full path to this property (includes name of this property and all parent properties). /// /// Serializable property referencing a 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 InspectableDictionaryGUI(InspectableContext context, LocString title, string path, SerializableProperty property, GUILayout layout, int depth = 0) : base(title, layout, depth) { this.property = property; this.context = context; this.path = path; dictionary = property.GetValue(); if (dictionary != null) numElements = dictionary.Count; UpdateKeys(); } /// /// Builds the inspectable dictionary GUI elements. Must be called at least once in order for the contents to /// be populated. /// /// Context shared by all inspectable fields created by the same parent. /// Label to display on the list GUI title. /// Full path to this property (includes name of this property and all parent properties). /// /// Serializable property referencing a 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. public static InspectableDictionaryGUI Create(InspectableContext context, LocString title, string path, SerializableProperty property, GUILayout layout, int depth = 0) { InspectableDictionaryGUI guiDictionary = new InspectableDictionaryGUI(context, title, path, property, layout, depth); guiDictionary.BuildGUI(); return guiDictionary; } /// /// Returns an array row at the specified index. /// /// Index of the row. /// Array row representation or null if index is out of range. public InspectableDictionaryGUIRow GetRow(int idx) { if (idx < GetNumRows()) return (InspectableDictionaryGUIRow)rows[idx]; return null; } /// public override InspectableState Refresh() { // Check if any modifications to the array were made outside the inspector IDictionary newDict = property.GetValue(); if (dictionary == null && newDict != null) { dictionary = newDict; numElements = dictionary.Count; BuildGUI(); } else if (newDict == null && dictionary != null) { dictionary = null; numElements = 0; BuildGUI(); } else { if (dictionary != null) { if (numElements != dictionary.Count) { numElements = dictionary.Count; BuildGUI(); } } } return base.Refresh(); } /// /// Updates the ordered set of keys used for mapping sequential indexes to keys. Should be called whenever a /// dictionary key changes. /// private void UpdateKeys() { orderedKeys.Clear(); if (dictionary != null) { SerializableDictionary dict = property.GetDictionary(); foreach (var key in dictionary.Keys) orderedKeys.Add(dict.GetProperty(key).Key); } } /// protected override GUIDictionaryFieldRow CreateRow() { return new InspectableDictionaryGUIRow(); } /// protected override int GetNumRows() { if (dictionary != null) return dictionary.Count; return 0; } /// protected override bool IsNull() { return dictionary == null; } /// protected internal override object GetKey(int rowIdx) { return orderedKeys[rowIdx]; } /// protected internal override object GetValue(object key) { SerializableProperty keyProperty = (SerializableProperty)key; SerializableDictionary dictionary = property.GetDictionary(); return dictionary.GetProperty(keyProperty.GetValue()).Value; } /// protected internal override void SetValue(object key, object value) { // Setting the value should be done through the property throw new InvalidOperationException(); } /// protected internal override bool Contains(object key) { SerializableProperty keyProperty = (SerializableProperty)key; return dictionary.Contains(keyProperty.GetValue()); ; } /// protected internal override void EditEntry(object oldKey, object newKey, object value) { SerializableProperty oldKeyProperty = (SerializableProperty)oldKey; SerializableProperty newKeyProperty = (SerializableProperty)newKey; SerializableProperty valueProperty = (SerializableProperty)value; dictionary.Remove(oldKeyProperty.GetValue()); dictionary.Add(newKeyProperty.GetValue(), valueProperty.GetValue()); numElements = dictionary.Count; UpdateKeys(); } /// protected internal override void AddEntry(object key, object value) { StartUndo(); SerializableProperty keyProperty = (SerializableProperty)key; SerializableProperty valueProperty = (SerializableProperty)value; dictionary.Add(keyProperty.GetValue(), valueProperty.GetValue()); numElements = dictionary.Count; EndUndo(); UpdateKeys(); } /// protected internal override void RemoveEntry(object key) { StartUndo(); SerializableProperty keyProperty = (SerializableProperty)key; dictionary.Remove(keyProperty.GetValue()); numElements = dictionary.Count; EndUndo(); UpdateKeys(); } /// protected internal override object CreateKey() { SerializableDictionary dictionary = property.GetDictionary(); DictionaryDataWrapper data = new DictionaryDataWrapper(); data.value = SerializableUtility.Create(dictionary.KeyType); SerializableProperty keyProperty = new SerializableProperty(dictionary.KeyPropertyType, dictionary.KeyType, () => data.value, (x) => data.value = x); return keyProperty; } /// protected internal override object CreateValue() { SerializableDictionary dictionary = property.GetDictionary(); DictionaryDataWrapper data = new DictionaryDataWrapper(); data.value = SerializableUtility.Create(dictionary.ValueType); SerializableProperty valueProperty = new SerializableProperty(dictionary.ValuePropertyType, dictionary.ValueType, () => data.value, (x) => data.value = x); return valueProperty; } /// protected internal override KeyValuePair CloneElement(int index) { SerializableProperty keyProperty = (SerializableProperty)GetKey(index); SerializableProperty valueProperty = (SerializableProperty)GetValue(keyProperty); SerializableDictionary dictionary = property.GetDictionary(); DictionaryDataWrapper keyData = new DictionaryDataWrapper(); keyData.value = SerializableUtility.Clone(keyProperty.GetValue()); SerializableProperty clonedKeyProperty = new SerializableProperty(dictionary.KeyPropertyType, dictionary.KeyType, () => keyData.value, (x) => keyData.value = x); DictionaryDataWrapper valueData = new DictionaryDataWrapper(); valueData.value = SerializableUtility.Clone(valueProperty.GetValue()); SerializableProperty clonedValueProperty = new SerializableProperty(dictionary.ValuePropertyType, dictionary.ValueType, () => valueData.value, (x) => valueData.value = x); return new KeyValuePair(clonedKeyProperty, clonedValueProperty); } /// protected override void CreateDictionary() { StartUndo(); dictionary = property.CreateDictionaryInstance(); numElements = dictionary.Count; property.SetValue(dictionary); EndUndo(); UpdateKeys(); } /// protected override void DeleteDictionary() { StartUndo(); dictionary = null; numElements = 0; property.SetValue(null); EndUndo(); UpdateKeys(); } /// /// Notifies the system to start recording a new undo command. Any changes to the field after this is called /// will be recorded in the command. User must call after field is done being changed. /// protected void StartUndo() { if (context.Component != null) GameObjectUndo.RecordComponent(context.Component, path); } /// /// Finishes recording an undo command started via . If any changes are detected on the /// field an undo command is recorded onto the undo-redo stack, otherwise nothing is done. /// protected void EndUndo() { GameObjectUndo.ResolveDiffs(); } /// /// Wraps a dictionary key or a value. /// class DictionaryDataWrapper { public object value; } } /// /// Contains GUI elements for a single key/value pair in the dictionary. /// private class InspectableDictionaryGUIRow : GUIDictionaryFieldRow { /// /// Inspectable field displaying the key on the dictionary row. /// public InspectableField FieldKey { get; private set; } /// /// Inspectable field displaying the value on the dictionary row. /// public InspectableField FieldValue { get; private set; } private GUILayoutY keyLayout; /// protected override GUILayoutX CreateKeyGUI(GUILayoutY layout) { keyLayout = layout; InspectableDictionaryGUI dictParent = (InspectableDictionaryGUI)parent; SerializableProperty property = GetKey(); string entryPath = dictParent.Path + "Key[" + RowIdx + "]"; FieldKey = CreateField(dictParent.Context, "Key", entryPath, 0, Depth + 1, new InspectableFieldLayout(layout), property); return FieldKey.GetTitleLayout(); } /// protected override void CreateValueGUI(GUILayoutY layout) { InspectableDictionaryGUI dictParent = (InspectableDictionaryGUI)parent; SerializableProperty property = GetValue(); string entryPath = dictParent.Path + "Value[" + RowIdx + "]"; FieldValue = CreateField(dictParent.Context, "Value", entryPath, 0, Depth + 1, new InspectableFieldLayout(layout), property); } /// protected override void OnEditModeChanged(bool editMode) { keyLayout.Disabled = !editMode; } /// protected internal override InspectableState Refresh() { FieldKey.Property = GetKey(); FieldValue.Property = GetValue(); return FieldValue.Refresh(0); } } } /** @} */ }