using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Unicode; namespace UnicodeCharacterInspector { internal sealed class CharacterInspectorViewModel : BindableObject { public sealed class CharacterViewModel { public int CodePoint { get; } public string Character { get; } public string DisplayText { get; } public CharacterViewModel(int codePoint) { CodePoint = codePoint; Character = char.ConvertFromUtf32(codePoint); DisplayText = UnicodeInfo.GetDisplayText(codePoint); } } private class CharacterCollection : INotifyCollectionChanged, IList { public event NotifyCollectionChangedEventHandler CollectionChanged { add { owner.CollectionChanged += value; } remove { owner.CollectionChanged -= value; } } private CharacterInspectorViewModel owner; internal CharacterCollection(CharacterInspectorViewModel owner) { this.owner = owner; } public CharacterViewModel this[int index] { get { if (index < 0 || index >= owner.characterCount) throw new IndexOutOfRangeException(); return new CharacterViewModel(owner.codePoints[index]); } } CharacterViewModel IList.this[int index] { get { return this[index]; } set { throw new NotSupportedException(); } } public int Count { get { return owner.characterCount; } } bool ICollection.IsReadOnly { get { return true; } } void ICollection.Add(CharacterViewModel item) { throw new NotSupportedException(); } void IList.Insert(int index, CharacterViewModel item) { throw new NotSupportedException(); } bool ICollection.Remove(CharacterViewModel item) { throw new NotSupportedException(); } void IList.RemoveAt(int index) { throw new NotSupportedException(); } void ICollection.Clear() { throw new NotSupportedException(); } public int IndexOf(CharacterViewModel item) { return Array.IndexOf(owner.codePoints, item); } public bool Contains(CharacterViewModel item) { return Array.IndexOf(owner.codePoints, item) >= 0; } public void CopyTo(CharacterViewModel[] array, int arrayIndex) { if (array == null) throw new ArgumentNullException(nameof(array)); if (arrayIndex < 0) throw new ArgumentOutOfRangeException(nameof(arrayIndex)); if (array.Length < arrayIndex + owner.codePoints.Length) throw new ArgumentException(); for (int i = 0, j = arrayIndex; i < owner.codePoints.Length; ++i, ++j) { array[j] = new CharacterViewModel(owner.codePoints[i]); } } public IEnumerator GetEnumerator() { int length = owner.characterCount; var array = owner.codePoints; for (int i = 0; i < length; ++i) yield return new CharacterViewModel(array[i]); } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } } private event NotifyCollectionChangedEventHandler CollectionChanged; private string text; private int selectedCharacterIndex = -1; private int[] codePoints = new int[1024]; private int characterCount; private CharacterInfoViewModel selectedCharacterInfo = new CharacterInfoViewModel(); private readonly CharacterCollection characterCollection; public CharacterInspectorViewModel() { characterCollection = new CharacterCollection(this); } private void NotifyCollectionChanged(NotifyCollectionChangedAction action) { var handler = CollectionChanged; if (handler != null) handler(this, new NotifyCollectionChangedEventArgs(action)); } public string Text { get { return text; } set { // Analyze the string to detect if there are any similarities between the new and old values. bool keepSelectedIndex = value != null && text != null && ( value.Length <= text.Length ? text.StartsWith(value, StringComparison.Ordinal) : value.StartsWith(text, StringComparison.Ordinal) ); // Here, we can deduce the information on string equality and simply return if needed. if (keepSelectedIndex && value.Length == text.Length) return; // We don't need the old value anymore. text = value; // We always need to reparse the text, as it is not possible to directly map between UTF-16 elements and their respective code point indexes. int index = 0; if (!string.IsNullOrEmpty(value)) { foreach (int codePoint in value.AsPermissiveCodePointEnumerable()) { if (index >= codePoints.Length) Array.Resize(ref codePoints, codePoints.Length * 2); codePoints[index++] = codePoint; } } characterCount = index; // Now that we parsed the text, we really know whether the previous selected index should be left untouched. if (!(keepSelectedIndex = keepSelectedIndex && selectedCharacterIndex < characterCount || selectedCharacterIndex == -1)) selectedCharacterIndex = -1; // After all this, raise the change notifications as needed. 😊 NotifyPropertyChanged(); NotifyCollectionChanged(NotifyCollectionChangedAction.Reset); if (!keepSelectedIndex) { NotifyPropertyChanged(nameof(SelectedCharacterIndex)); NotifyPropertyChanged(nameof(SelectedCharacter)); selectedCharacterInfo.Character = null; } } } public ICollection Characters { get { return characterCollection; } } public int SelectedCharacterIndex { get { return selectedCharacterIndex; } set { if (selectedCharacterIndex < -1 || selectedCharacterIndex >= codePoints.Length) throw new ArgumentOutOfRangeException(nameof(value)); if (value != selectedCharacterIndex) { string oldSelectedCharacter = SelectedCharacter; selectedCharacterIndex = value; string newSelectedCharacter = SelectedCharacter; NotifyPropertyChanged(); if (newSelectedCharacter != oldSelectedCharacter) { selectedCharacterInfo.Character = newSelectedCharacter; NotifyPropertyChanged(nameof(SelectedCharacter)); } } } } public string SelectedCharacter { get { return selectedCharacterIndex >= 0 ? char.ConvertFromUtf32(codePoints[selectedCharacterIndex]) : null; } } public CharacterInfoViewModel SelectedCharacterInfo { get { return selectedCharacterInfo; } } } }