#nullable enable using System.Collections; using System.Collections.ObjectModel; using System.Collections.Specialized; namespace Terminal.Gui.Views; /// /// Provides a default implementation of that renders items /// using . /// public class ListWrapper : IListDataSource, IDisposable { /// /// Creates a new instance of that wraps the specified /// . /// /// public ListWrapper (ObservableCollection? source) { if (source is { }) { _count = source.Count; _marks = new (_count); _source = source; _source.CollectionChanged += Source_CollectionChanged; Length = GetMaxLengthItem (); } } private readonly ObservableCollection? _source; private int _count; private BitArray? _marks; private bool _suspendCollectionChangedEvent; /// public event NotifyCollectionChangedEventHandler? CollectionChanged; /// public int Count => _source?.Count ?? 0; /// public int Length { get; private set; } /// public bool SuspendCollectionChangedEvent { get => _suspendCollectionChangedEvent; set { _suspendCollectionChangedEvent = value; if (!_suspendCollectionChangedEvent) { CheckAndResizeMarksIfRequired (); } } } /// public void Render ( ListView container, bool marked, int item, int col, int line, int width, int viewportX = 0 ) { container.Move (Math.Max (col - viewportX, 0), line); if (_source is null) { return; } object? t = _source [item]; if (t is null) { RenderString (container, "", col, line, width); } else { if (t is string s) { RenderString (container, s, col, line, width, viewportX); } else { RenderString (container, t.ToString ()!, col, line, width, viewportX); } } } /// public bool IsMarked (int item) { if (item >= 0 && item < _count) { return _marks! [item]; } return false; } /// public void SetMark (int item, bool value) { if (item >= 0 && item < _count) { _marks! [item] = value; } } /// public IList ToList () { return _source ?? []; } /// public void Dispose () { if (_source is { }) { _source.CollectionChanged -= Source_CollectionChanged; } } /// /// INTERNAL: Searches the underlying collection for the first string element that starts with the specified search value, /// using a case-insensitive comparison. /// /// /// The comparison is performed in a case-insensitive manner using invariant culture rules. Only /// elements of type string are considered; other types in the collection are ignored. /// /// /// The string value to compare against the start of each string element in the collection. Cannot be /// null. /// /// /// The zero-based index of the first matching string element if found; otherwise, -1 if no match is found or the /// collection is empty. /// internal int StartsWith (string search) { if (_source is null || _source?.Count == 0) { return -1; } for (var i = 0; i < _source!.Count; i++) { object? t = _source [i]; if (t is string u) { if (u.ToUpper ().StartsWith (search.ToUpperInvariant ())) { return i; } } else if (t is string s && s.StartsWith (search, StringComparison.InvariantCultureIgnoreCase)) { return i; } } return -1; } private void CheckAndResizeMarksIfRequired () { if (_source != null && _count != _source.Count && _marks is { }) { _count = _source.Count; var newMarks = new BitArray (_count); for (var i = 0; i < Math.Min (_marks.Length, newMarks.Length); i++) { newMarks [i] = _marks [i]; } _marks = newMarks; Length = GetMaxLengthItem (); } } private int GetMaxLengthItem () { if (_source is null || _source?.Count == 0) { return 0; } var maxLength = 0; for (var i = 0; i < _source!.Count; i++) { object? t = _source [i]; if (t is null) { continue; } int l; l = t is string u ? u.GetColumns () : t.ToString ()!.Length; if (l > maxLength) { maxLength = l; } } return maxLength; } private static void RenderString (View driver, string str, int col, int line, int width, int viewportX = 0) { if (string.IsNullOrEmpty (str) || viewportX >= str.GetColumns ()) { // Empty string or viewport beyond string - just fill with spaces for (var i = 0; i < width; i++) { driver.AddRune ((Rune)' '); } return; } int runeLength = str.ToRunes ().Length; int startIndex = Math.Min (viewportX, Math.Max (0, runeLength - 1)); string substring = str.Substring (startIndex); string u = TextFormatter.ClipAndJustify (substring, width, Alignment.Start); driver.AddStr (u); width -= u.GetColumns (); while (width-- > 0) { driver.AddRune ((Rune)' '); } } private void Source_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e) { if (!SuspendCollectionChangedEvent) { CheckAndResizeMarksIfRequired (); CollectionChanged?.Invoke (sender, e); } } }