/* * Copyright 2019 faddenSoft * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. */ using System; using System.Collections; using System.Collections.Generic; using System.Collections.Specialized; using System.Diagnostics; using System.ComponentModel; using System.Windows.Media; using System.Windows.Media.Imaging; namespace SourceGen { /// /// List of items formatted for display. /// /// /// This is intended to be useful as an ItemSource for a WPF ListView. We need to implement /// plain IList to cause ListView to perform data virtualization, and the property/collection /// changed events so the view will pick up our changes. /// /// The ItemsControl.ItemsSource property wants an IEnumerable (which IList implements). /// According to various articles, if the object implements IList, and the UI element /// is providing *UI* virtualization, you will also get *data* virtualization. This behavior /// doesn't seem to be documented anywhere, but the consensus is that it's expected to work. /// /// Implementing generic IList<> doesn't seem necessary for XAML, but may be useful /// for other consumers of the data. /// /// The list is initially filled with null references, with FormattedParts instances /// generated on demand. This is done by requesting individual items from the /// LineListGen object. /// /// NOTE: it may or may not be possible to implement this trivially with an /// ObservableCollection. At an earlier iteration it wasn't, and I'd like to keep this /// around even if it is now possible, in case the pendulum swings back the other way. /// /// Additional reading on data virtualization: /// https://www.codeproject.com/Articles/34405/WPF-Data-Virtualization?msg=5635751 /// https://web.archive.org/web/20121216034305/http://www.zagstudio.com/blog/498 /// https://web.archive.org/web/20121107200359/http://www.zagstudio.com/blog/378 /// https://github.com/lvaleriu/Virtualization /// public class DisplayList : IList, IList, INotifyCollectionChanged, INotifyPropertyChanged { /// /// List of formatted parts. DO NOT access this directly outside the event-sending /// method wrappers. /// private List mList; /// /// Data generation object. /// /// /// This property is set by the LineListGen constructor. /// public LineListGen ListGen { get; set; } /// /// Set of selected items, by list index. /// public DisplayListSelection SelectedIndices { get; private set; } /// /// Constructs an empty collection, with the default initial capacity. /// public DisplayList() { mList = new List(); SelectedIndices = new DisplayListSelection(); } #region Property / Collection Changed public event NotifyCollectionChangedEventHandler CollectionChanged; public event PropertyChangedEventHandler PropertyChanged; // See ObservableCollection class, e.g. // https://github.com/Microsoft/referencesource/blob/master/System/compmod/system/collections/objectmodel/observablecollection.cs private const string CountString = "Count"; private const string IndexerName = "Item[]"; protected virtual void OnPropertyChanged(PropertyChangedEventArgs e) { PropertyChanged?.Invoke(this, e); } private void OnPropertyChanged(string propertyName) { OnPropertyChanged(new PropertyChangedEventArgs(propertyName)); } protected virtual void OnCollectionChanged(NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke(this, e); } private void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index) { OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index)); } private void OnCollectionChanged(NotifyCollectionChangedAction action, object item, int index, int oldIndex) { OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, item, index, oldIndex)); } private void OnCollectionChanged(NotifyCollectionChangedAction action, object oldItem, object newItem, int index) { OnCollectionChanged(new NotifyCollectionChangedEventArgs(action, newItem, oldItem, index)); } private void OnCollectionReset() { OnCollectionChanged(new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset)); } #endregion Property / Collection Changed #region IList / IList public int Count => ((IList)mList).Count; public bool IsReadOnly => ((IList)mList).IsReadOnly; public bool IsFixedSize => ((IList)mList).IsFixedSize; public object SyncRoot => ((IList)mList).SyncRoot; public bool IsSynchronized => ((IList)mList).IsSynchronized; public void Add(FormattedParts item) { ((IList)mList).Add(item); OnPropertyChanged(CountString); OnPropertyChanged(IndexerName); OnCollectionReset(); } public int Add(object value) { int posn = ((IList)mList).Add(value); if (posn >= 0) { OnPropertyChanged(CountString); OnPropertyChanged(IndexerName); OnCollectionChanged(NotifyCollectionChangedAction.Add, value, posn); } return posn; } public void Clear() { ((IList)mList).Clear(); OnPropertyChanged(CountString); OnPropertyChanged(IndexerName); OnCollectionReset(); // Not strictly necessary, but does free up the memory sooner. SelectedIndices = new DisplayListSelection(); } public bool Contains(FormattedParts item) { return ((IList)mList).Contains(item); } bool IList.Contains(object value) { return Contains((FormattedParts)value); } public void CopyTo(FormattedParts[] array, int arrayIndex) { ((IList)mList).CopyTo(array, arrayIndex); } public void CopyTo(Array array, int index) { ((IList)mList).CopyTo(array, index); } public IEnumerator GetEnumerator() { // Use the indexer, rather than mList's enumerator, to get on-demand string gen. for (int i = 0; i < Count; i++) { yield return this[i]; } } IEnumerator IEnumerable.GetEnumerator() { return GetEnumerator(); } public int IndexOf(FormattedParts item) { return ((IList)mList).IndexOf(item); } int IList.IndexOf(object value) { return IndexOf((FormattedParts)value); } public void Insert(int index, FormattedParts item) { ((IList)mList).Insert(index, item); OnPropertyChanged(CountString); OnPropertyChanged(IndexerName); OnCollectionChanged(NotifyCollectionChangedAction.Add, item, index); } void IList.Insert(int index, object value) { Insert(index, (FormattedParts)value); } public void RemoveAt(int index) { FormattedParts removed = mList[index]; ((IList)mList).RemoveAt(index); OnPropertyChanged(CountString); OnPropertyChanged(IndexerName); OnCollectionChanged(NotifyCollectionChangedAction.Remove, removed, index); } public bool Remove(FormattedParts item) { // NotifyCollectionChangedAction.Remove wants an index. We can find the index // of the first matching item and then do a RemoveAt, but this call just isn't // all that interesting for us, so it's easier to ignore it. //return ((IList)mList).Remove(item); throw new NotSupportedException(); } void IList.Remove(object value) { //Remove((FormattedParts)value); throw new NotSupportedException(); } object IList.this[int index] { // forward to generic impl get { return this[index]; } set { this[index] = (FormattedParts)value; } } // For IList. public FormattedParts this[int index] { get { FormattedParts parts = mList[index]; if (parts == null) { parts = mList[index] = GetEntry(index); } return parts; } set { FormattedParts orig = mList[index]; mList[index] = value; OnPropertyChanged(IndexerName); OnCollectionChanged(NotifyCollectionChangedAction.Replace, orig, value, index); } } #endregion IList / IList /// /// Retrieves the Nth element. /// private FormattedParts GetEntry(int index) { FormattedParts parts = mList[index]; if (parts == null) { parts = mList[index] = ListGen.GetFormattedParts(index); parts.ListIndex = index; } return parts; } /// /// Resets the list, filling it with empty elements. Also resets the selected indices. /// /// New size of the list. public void ResetList(int size) { // TODO: can we recycle existing elements and just add/trim as needed? Clear(); mList.Capacity = size; for (int i = 0; i < size; i++) { // add directly to list so we don't send events mList.Add(null); } SelectedIndices = new DisplayListSelection(size); // send one big notification at the end; "reset" means "forget everything you knew" OnPropertyChanged(CountString); OnPropertyChanged(IndexerName); OnCollectionReset(); } /// /// A range of lines has been replaced with a new range of lines. The new set may be /// the same size, larger, or smaller than the previous. /// /// Start index of change area. /// Number of old lines. /// Number of new lines. May be zero. public void ClearListSegment(int startIndex, int oldCount, int newCount) { Debug.WriteLine("ClearListSegment start=" + startIndex + " old=" + oldCount + " new=" + newCount + " (mList.Count=" + mList.Count + ")"); Debug.Assert(startIndex >= 0 && startIndex < mList.Count); Debug.Assert(oldCount >= 0 && startIndex + oldCount <= mList.Count); Debug.Assert(newCount >= 0); // Remove the old elements to clear them. if (oldCount != 0) { mList.RemoveRange(startIndex, oldCount); } // Replace with the appropriate number of null entries. for (int i = 0; i < newCount; i++) { mList.Insert(startIndex, null); } // TODO(someday): can we null out existing entries, and just insert/remove when // counts differ? if (oldCount != newCount) { SelectedIndices = new DisplayListSelection(mList.Count); RecalculateListIndices(); } OnPropertyChanged(CountString); OnPropertyChanged(IndexerName); // TODO: this causes the ListView to format the entire listing, despite // being virtual. So we're regenerating the entire list after something trivial, // like renaming a label, which hampers performance. Need to figure this out. OnCollectionReset(); } /// /// Recalculates the list index fields after lines are added or removed. /// private void RecalculateListIndices() { Debug.WriteLine("Recalculating list indices"); for (int i = 0; i < mList.Count; i++) { if (mList[i] != null) { mList[i].ListIndex = i; } } } /// /// List elements. Instances are immutable except for ListIndex. /// public class FormattedParts { public string Offset { get; private set; } public string Addr { get; private set; } public string Bytes { get; private set; } public string Flags { get; private set; } public string Attr { get; private set; } public string Label { get; private set; } public string Opcode { get; private set; } public string Operand { get; private set; } public string Comment { get; private set; } public bool IsLongComment { get; private set; } public bool HasBackgroundColor { get; private set; } public Brush BackgroundBrush { get; private set; } public bool IsVisualizationSet { get; private set; } public Visualization[] VisualizationSet { get; private set; } // Set to true if we want to highlight the address and label fields. This is // examined by a data trigger in CodeListItemStyle.xaml. public bool HasAddrLabelHighlight { get; private set; } // Set to true if we want to highlight the operand field. This is // examined by a data trigger in CodeListItemStyle.xaml. public bool HasOperandHighlight { get; private set; } // Set to true if the Flags field has been modified. public bool HasModifiedFlags { get { return (mPartFlags & PartFlags.HasModifiedFlags) != 0; } } // Set to true if the address here is actually non-addressable. public bool IsNonAddressable { get { return (mPartFlags & PartFlags.IsNonAddressable) != 0; } } // List index, filled in on demand. If the list is regenerated we want to // renumber elements without having to recreate them, so this field is mutable. public int ListIndex { get; set; } = -1; [Flags] public enum PartFlags { None = 0, HasModifiedFlags = 1, // Flags field is non-default IsNonAddressable = 1 << 1, // this is a non-addressable region } private PartFlags mPartFlags; private static Color NoColor = CommonWPF.Helper.ZeroColor; // Private constructor -- create instances with factory methods. private FormattedParts() { Offset = Addr = Bytes = Flags = Attr = Label = Opcode = Operand = Comment = string.Empty; } /// /// Clones the specified object. /// private static FormattedParts Clone(FormattedParts orig) { FormattedParts newParts = FormattedParts.Create(orig.Offset, orig.Addr, orig.Bytes, orig.Flags, orig.Attr, orig.Label, orig.Opcode, orig.Operand, orig.Comment, orig.mPartFlags); newParts.IsLongComment = orig.IsLongComment; newParts.HasAddrLabelHighlight = orig.HasAddrLabelHighlight; newParts.ListIndex = orig.ListIndex; return newParts; } public static FormattedParts Create(string offset, string addr, string bytes, string flags, string attr, string label, string opcode, string operand, string comment, PartFlags pflags) { FormattedParts parts = new FormattedParts(); parts.Offset = offset; parts.Addr = addr; parts.Bytes = bytes; parts.Flags = flags; parts.Attr = attr; parts.Label = label; parts.Opcode = opcode; parts.Operand = operand; parts.Comment = comment; parts.IsLongComment = false; parts.mPartFlags = pflags; return parts; } public static FormattedParts CreateBlankLine() { FormattedParts parts = new FormattedParts(); return parts; } public static FormattedParts CreateLongComment(string comment) { FormattedParts parts = new FormattedParts(); parts.Comment = comment; parts.IsLongComment = true; return parts; } public static FormattedParts CreateNote(string comment, Color color) { FormattedParts parts = new FormattedParts(); parts.Comment = comment; parts.IsLongComment = true; if (color != NoColor) { parts.HasBackgroundColor = true; parts.BackgroundBrush = new SolidColorBrush(color); parts.BackgroundBrush.Freeze(); // export runs on non-UI thread } return parts; } public static FormattedParts CreateDirective(string opstr, string operandStr) { FormattedParts parts = new FormattedParts(); parts.Opcode = opstr; parts.Operand = operandStr; return parts; } public static FormattedParts CreatePreLabelDirective(string addrStr, string labelStr) { FormattedParts parts = new FormattedParts(); parts.Addr = addrStr; parts.Label = labelStr; return parts; } public static FormattedParts CreateFullDirective(string label, string opstr, string operandStr, string comment) { FormattedParts parts = new FormattedParts(); parts.Label = label; parts.Opcode = opstr; parts.Operand = operandStr; parts.Comment = comment; return parts; } public static FormattedParts CreateVisualizationSet(VisualizationSet visSet) { FormattedParts parts = new FormattedParts(); if (visSet.Count == 0) { // should not happen parts.Comment = "!EMPTY VSET!"; parts.IsLongComment = true; } else { string fmt; if (visSet.Count == 1) { fmt = Res.Strings.VIS_SET_SINGLE_FMT; } else { fmt = Res.Strings.VIS_SET_MULTIPLE_FMT; } parts.Comment = string.Format(fmt, "Bitmap", visSet[0].Tag, visSet.Count - 1); parts.VisualizationSet = visSet.ToArray(); parts.IsVisualizationSet = true; } return parts; } public static FormattedParts AddSelectionAddrHighlight(FormattedParts orig) { FormattedParts newParts = Clone(orig); newParts.HasAddrLabelHighlight = true; return newParts; } public static FormattedParts RemoveSelectionAddrHighlight(FormattedParts orig) { FormattedParts newParts = Clone(orig); newParts.HasAddrLabelHighlight = false; return newParts; } public static FormattedParts AddSelectionOperHighlight(FormattedParts orig) { FormattedParts newParts = Clone(orig); newParts.HasOperandHighlight = true; return newParts; } public static FormattedParts RemoveSelectionOperHighlight(FormattedParts orig) { FormattedParts newParts = Clone(orig); newParts.HasOperandHighlight = false; return newParts; } public override string ToString() { return "[Parts: index=" + ListIndex + " off=" + Offset + "]"; } } } }