diff --git a/Editor/UnityDictionaryDrawer.cs b/Editor/UnityDictionaryDrawer.cs new file mode 100644 index 0000000..81fc876 --- /dev/null +++ b/Editor/UnityDictionaryDrawer.cs @@ -0,0 +1,42 @@ +using UnityEditor; +using UnityEngine; + +public class UnityDictionaryDrawer : PropertyDrawer +{ + override public void OnGUI(Rect position, SerializedProperty property, GUIContent label) + { + var propTitle = EditorGUI.BeginProperty(position, label, property); + + // PropertyDrawers don't support GUILayout so we have to construct our own rectangles + var foldoutRect = new Rect(position.left, position.top, position.width, base.GetPropertyHeight(property, label)); + bool wasExpanded = property.isExpanded; + property.isExpanded = EditorGUI.Foldout(foldoutRect, property.isExpanded, propTitle); + + // Don't use the latest isExpanded value because if it's changed then we're the wrong height + if(wasExpanded) + { + + } + + EditorGUI.EndProperty(); + } + + public override float GetPropertyHeight(SerializedProperty property, GUIContent label) + { + var total = base.GetPropertyHeight(property, label); + if(property.isExpanded) + { + total += base.GetPropertyHeight(property, label) * property.FindPropertyRelative("_keys").arraySize; + } + + return total; + } +} + +//TODO: figure out how to make it so you dont need to have one per dummy subclass. + +[CustomPropertyDrawer(typeof(UnityDictionaryIntString))] +public class UnityDictionaryDrawerIntString : UnityDictionaryDrawer +{ + +} \ No newline at end of file diff --git a/ReadOnlyListWrapper.cs b/ReadOnlyListWrapper.cs new file mode 100644 index 0000000..2a39e37 --- /dev/null +++ b/ReadOnlyListWrapper.cs @@ -0,0 +1,81 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +internal class ReadOnlyListWrapper : ICollection, ICollection +{ + private readonly List _wrappedList; + + public ReadOnlyListWrapper(List listToWrap) + { + _wrappedList = listToWrap; + } + + #region Implementation of IEnumerable + + public IEnumerator GetEnumerator() + { + return _wrappedList.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + #endregion + + #region Implementation of ICollection + + public void Add(T item) + { + throw new NotSupportedException("The list is read-only."); + } + + public void Clear() + { + throw new NotSupportedException("The list is read-only."); + } + + public bool Contains(T item) + { + return _wrappedList.Contains(item); + } + + public void CopyTo(T[] array, int arrayIndex) + { + _wrappedList.CopyTo(array, arrayIndex); + } + + public bool Remove(T item) + { + throw new NotSupportedException("The list is read-only."); + } + + public void CopyTo(Array array, int index) + { + ((ICollection)_wrappedList).CopyTo(array, index); + } + + public int Count + { + get { return _wrappedList.Count; } + } + + public object SyncRoot + { + get { return ((ICollection) _wrappedList).SyncRoot; } + } + + public bool IsSynchronized + { + get { return ((ICollection) _wrappedList).IsSynchronized; } + } + + public bool IsReadOnly + { + get { return true; } + } + + #endregion +} \ No newline at end of file diff --git a/UnityDictionary.cs b/UnityDictionary.cs index e845f18..7348ab0 100644 --- a/UnityDictionary.cs +++ b/UnityDictionary.cs @@ -1,71 +1,465 @@ -#if UNITY_EDITOR -using UnityEditor; -#endif +using System.Collections; +using System.Runtime.Serialization; using UnityEngine; using System.Collections.Generic; using System; [Serializable] -public class UnityDictionary +public class UnityDictionary : IDictionary, IDictionary { - [SerializeField] - private List _keys = new List(); + [SerializeField] + private List _keys = new List(); - [SerializeField] - private List _values = new List(); + [SerializeField] + private List _values = new List(); - private Dictionary _cache; + private int _version = 0; - public void Add(TKey key, TValue value) - { - if (_cache == null) - BuildCache(); + public UnityDictionary() + { + } - _cache.Add(key,value); - _keys.Add(key); - _values.Add(value); - } + public UnityDictionary(int capacity) + { + _keys.Capacity = capacity; + _values.Capacity = capacity; + } - public TValue this[TKey key] - { - get { - if (_cache == null) - BuildCache(); - - return _cache[key]; - } - } + // _cache maps keys to list indices; this allows us to do stuff + // like Remove() as O(1), instead of O(n) + private Dictionary _cache; + + public bool ContainsKey(TKey key) + { + BuildCacheIfNeeded(); + return _cache.ContainsKey(key); + } + + public void Add(TKey key, TValue value) + { + BuildCacheIfNeeded(); + + // Add to the cache before adding to the key/value lists + // That way, duplicate or null keys get caught before we + // modify anything permanently. + _cache.Add(key, _keys.Count); + _keys.Add(key); + _values.Add(value); + ++_version; + } + + public bool Remove(TKey key) + { + BuildCacheIfNeeded(); + + int index; + if (!_cache.TryGetValue(key, out index)) + return false; + + RemoveAt(index); + + return true; + } + + // Private method for removing the key/value pair at a particular index + // This should never be public; dictionaries aren't supposed to have any + // ordering on their elements, so the idea of an element at a particular + // index isn't valid in the outside world. That we're using indexable + // lists for storing keys/values is an implementation detail. + private void RemoveAt(int index) + { + if (_cache != null) _cache.Remove(_keys[index]); + + if(_keys.Count > 1) + { + // Copy the final key/value into this index and update the cache if it exists + _keys[index] = _keys[_keys.Count - 1]; + _values[index] = _values[_values.Count - 1]; + if(_cache != null) _cache[_keys[index]] = index; + } + + // Truncate the lists + _keys.RemoveAt(_keys.Count - 1); + _values.RemoveAt(_values.Count - 1); + + ++_version; + } + + public bool TryGetValue(TKey key, out TValue value) + { + BuildCacheIfNeeded(); + + int index; + if(!_cache.TryGetValue(key, out index)) + { + value = default(TValue); + return false; + } + + value = _values[index]; + return true; + } + + TValue IDictionary.this[TKey key] + { + get { return this[key]; } + set { this[key] = value; } + } + + public TValue this[TKey key] + { + get + { + BuildCacheIfNeeded(); + return _values[_cache[key]]; + } + set + { + BuildCacheIfNeeded(); + int index; + if (!_cache.TryGetValue(key, out index)) + { + // This key isn't presently in the dictionary, so add it + Add(key, value); + } + else + { + // The key is already in the dictionary, just update it + _values[index] = value; + ++_version; + } + } + } + + public ICollection Keys + { + get { return new ReadOnlyListWrapper(_keys); } + } + + public ICollection Values + { + get { return new ReadOnlyListWrapper(_values); } + } + + ICollection IDictionary.Keys + { + get { return new ReadOnlyListWrapper(_keys); } + } + + ICollection IDictionary.Values + { + get { return new ReadOnlyListWrapper(_values); } + } + + void BuildCacheIfNeeded() + { + if(_cache == null) BuildCache(); + } void BuildCache() { - _cache = new Dictionary(); + _cache = new Dictionary(); for (int i=0; i!=_keys.Count; i++) { - _cache.Add(_keys[i],_values[i]); + _cache.Add(_keys[i], i); } } -} -//Unfortunately, Unity currently doesn't serialize UnityDictionary, but it will serialize a dummy subclass of that. -[Serializable] -public class UnityDictionaryIntString : UnityDictionary {} + #region Implementation of IEnumerable + private class Enumerator : IEnumerator>, IDictionaryEnumerator + { + public void Dispose() { } -/* -//TODO: implement the propertydrawer, and figure out how to make it so you dont need to have one per dummy subclass. -#if UNITY_EDITOR + private int _index = -1; + private readonly UnityDictionary _dict; + private readonly int _initialVersion; -[CustomPropertyDrawer(typeof(UnityDictionaryIntString))] -public class UnityDictionaryDrawer : PropertyDrawer { - - override public void OnGUI(Rect position, SerializedProperty property, GUIContent label) - { - EditorGUI.BeginProperty (position, label, property); - - //todo: put nice drawing code here - - EditorGUI.EndProperty (); - } + public Enumerator(UnityDictionary dict) + { + _dict = dict; + _initialVersion = dict._version; + Reset(); + } + + #region Implementation of IEnumerator + + public bool MoveNext() + { + if(_dict._version != _initialVersion) + throw new InvalidOperationException("The dictionary was modified while enumerating."); + ++_index; + return _index < _dict.Count; + } + + public void Reset() + { + _index = -1; + } + + public KeyValuePair Current + { + get { + if (_dict._version != _initialVersion) + throw new InvalidOperationException("The dictionary was modified while enumerating."); + + if(_index < 0 || _index >= _dict.Count) throw new InvalidOperationException(); + return new KeyValuePair(_dict._keys[_index], _dict._values[_index]); + } + } + + object IEnumerator.Current + { + get { return Current; } + } + + #endregion + + #region Implementation of IDictionaryEnumerator + + public object Key + { + get { return Current.Key; } + } + + public object Value + { + get { return Current.Value; } + } + + public DictionaryEntry Entry + { + get { return new DictionaryEntry(Current.Key, Current.Value); } + } + + #endregion + } + + [Serializable] + private class NonGenericEnumerator : IDictionaryEnumerator + { + private readonly Enumerator _e; + public NonGenericEnumerator(Enumerator e) { _e = e; } + + // Map .Current to .Entry for the sake of old nongeneric clients that don't + // are expecting DictionaryEntry instead of KeyValuePair + public object Current { get { return Entry; } } + + public bool MoveNext() { return _e.MoveNext(); } + public void Reset() { _e.Reset(); } + + public DictionaryEntry Entry { get { return _e.Entry; } } + public object Key { get { return _e.Current.Key; } } + public object Value { get { return _e.Current.Value; } } + } + + public IEnumerator> GetEnumerator() + { + return new Enumerator(this); + } + + public void Remove(object key) + { + if(key == null) throw new ArgumentNullException("key"); + + BuildCacheIfNeeded(); + if (!((IDictionary)_cache).Contains(key)) + return; + + int index = (int) ((IDictionary) _cache)[key]; + RemoveAt(index); + } + + object IDictionary.this[object key] + { + get { + if (key == null) throw new ArgumentNullException("key"); + BuildCacheIfNeeded(); + if (!((IDictionary)_cache).Contains(key)) return null; + int index = (int) ((IDictionary) _cache)[key]; + return _values[index]; + } + set { + if (key == null) throw new ArgumentNullException("key"); + BuildCacheIfNeeded(); + if (!((IDictionary)_cache).Contains(key)) + { + Add(key, value); + } + else + { + TValue tV = ConvertObjectValHelper(value); + int index = (int)((IDictionary)_cache)[key]; + _values[index] = tV; + ++_version; + } + } + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + IDictionaryEnumerator IDictionary.GetEnumerator() + { + return new NonGenericEnumerator(new Enumerator(this)); + } + + #endregion + + #region Implementation of ICollection> + + public void Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + public bool Contains(object key) + { + if(key == null) + throw new ArgumentNullException("key"); + + BuildCacheIfNeeded(); + return ((IDictionary) _cache).Contains(key); + } + + private static T ConvertObjectValHelper(object obj) + { + T result; + try + { + if (obj != null) + result = (T)obj; + else if (!typeof(T).IsValueType) + result = default(T); + else + throw new ArgumentException(); + } + catch (InvalidCastException) + { + throw new ArgumentException(string.Format("The value \"{0}\" is not of type \"{1}\" and cannot be used in this generic collection.", obj, typeof(T).FullName)); + } + return result; + } + + public void Add(object key, object value) + { + if(key == null) throw new ArgumentNullException("key"); + + TKey tK = ConvertObjectValHelper(key); + TValue tV = ConvertObjectValHelper(value); + + Add(tK, tV); + } + + public void Clear() + { + _keys.Clear(); + _values.Clear(); + + if (_cache == null) + _cache = new Dictionary(); + else + _cache.Clear(); + + ++_version; + } + + public bool Contains(KeyValuePair item) + { + BuildCacheIfNeeded(); + int index; + if (!_cache.TryGetValue(item.Key, out index)) + return false; + + return EqualityComparer.Default.Equals(_values[index], item.Value); + } + + public void CopyTo(KeyValuePair[] array, int arrayIndex) + { + if(array == null) + throw new ArgumentNullException("array"); + if(arrayIndex < 0) + throw new ArgumentOutOfRangeException("arrayIndex"); + if(array.Length - arrayIndex < Count) + throw new ArgumentException("The provided array is too small."); + + for(int i = 0; i < _keys.Count; ++i, ++arrayIndex) + { + array[arrayIndex] = new KeyValuePair(_keys[i], _values[i]); + } + } + + public bool Remove(KeyValuePair item) + { + BuildCacheIfNeeded(); + int index; + if (!_cache.TryGetValue(item.Key, out index)) + return false; + + if (!EqualityComparer.Default.Equals(_values[index], item.Value)) + return false; + + RemoveAt(index); + return true; + } + + public void CopyTo(Array array, int index) + { + if (array == null) + throw new ArgumentNullException("array"); + if (index < 0) + throw new ArgumentOutOfRangeException("index"); + if (array.Length - index < Count) + throw new ArgumentException("The provided array is too small."); + if(!(array is KeyValuePair[]) && !(array is DictionaryEntry[]) && !(array is object[])) + throw new ArgumentException("The array is not of the appropriate type."); + + if(array is DictionaryEntry[]) + { + for (int i = 0; i < _keys.Count; ++i, ++index) + { + array.SetValue(new DictionaryEntry(_keys[i], _values[i]), index); + } + } + else + { + for (int i = 0; i < _keys.Count; ++i, ++index) + { + array.SetValue(new KeyValuePair(_keys[i], _values[i]), index); + } + } + } + + public int Count + { + get { return _keys.Count; } + } + + public object SyncRoot + { + get { return this; } + } + + public bool IsSynchronized + { + get { return false; } + } + + public bool IsReadOnly + { + get { return false; } + } + + public bool IsFixedSize + { + get { return false; } + } + + #endregion } -#endif -*/ + +//Unfortunately, Unity currently doesn't serialize UnityDictionary, but it will serialize a dummy subclass of that. +[Serializable] +public class UnityDictionaryIntString : UnityDictionary {}