ListWrapper.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. #nullable enable
  2. using System.Collections;
  3. using System.Collections.ObjectModel;
  4. using System.Collections.Specialized;
  5. namespace Terminal.Gui.Views;
  6. /// <summary>
  7. /// Provides a default implementation of <see cref="IListDataSource"/> that renders <see cref="ListView"/> items
  8. /// using <see cref="object.ToString()"/>.
  9. /// </summary>
  10. public class ListWrapper<T> : IListDataSource, IDisposable
  11. {
  12. /// <summary>
  13. /// Creates a new instance of <see cref="ListWrapper{T}"/> that wraps the specified
  14. /// <see cref="ObservableCollection{T}"/>.
  15. /// </summary>
  16. /// <param name="source"></param>
  17. public ListWrapper (ObservableCollection<T>? source)
  18. {
  19. if (source is { })
  20. {
  21. _count = source.Count;
  22. _marks = new (_count);
  23. _source = source;
  24. _source.CollectionChanged += Source_CollectionChanged;
  25. Length = GetMaxLengthItem ();
  26. }
  27. }
  28. private readonly ObservableCollection<T>? _source;
  29. private int _count;
  30. private BitArray? _marks;
  31. private bool _suspendCollectionChangedEvent;
  32. /// <inheritdoc/>
  33. public event NotifyCollectionChangedEventHandler? CollectionChanged;
  34. /// <inheritdoc/>
  35. public int Count => _source?.Count ?? 0;
  36. /// <inheritdoc/>
  37. public int Length { get; private set; }
  38. /// <inheritdoc/>
  39. public bool SuspendCollectionChangedEvent
  40. {
  41. get => _suspendCollectionChangedEvent;
  42. set
  43. {
  44. _suspendCollectionChangedEvent = value;
  45. if (!_suspendCollectionChangedEvent)
  46. {
  47. CheckAndResizeMarksIfRequired ();
  48. }
  49. }
  50. }
  51. /// <inheritdoc/>
  52. public void Render (
  53. ListView container,
  54. bool marked,
  55. int item,
  56. int col,
  57. int line,
  58. int width,
  59. int viewportX = 0
  60. )
  61. {
  62. container.Move (Math.Max (col - viewportX, 0), line);
  63. if (_source is null)
  64. {
  65. return;
  66. }
  67. object? t = _source [item];
  68. if (t is null)
  69. {
  70. RenderString (container, "", col, line, width);
  71. }
  72. else
  73. {
  74. if (t is string s)
  75. {
  76. RenderString (container, s, col, line, width, viewportX);
  77. }
  78. else
  79. {
  80. RenderString (container, t.ToString ()!, col, line, width, viewportX);
  81. }
  82. }
  83. }
  84. /// <inheritdoc/>
  85. public bool IsMarked (int item)
  86. {
  87. if (item >= 0 && item < _count)
  88. {
  89. return _marks! [item];
  90. }
  91. return false;
  92. }
  93. /// <inheritdoc/>
  94. public void SetMark (int item, bool value)
  95. {
  96. if (item >= 0 && item < _count)
  97. {
  98. _marks! [item] = value;
  99. }
  100. }
  101. /// <inheritdoc/>
  102. public IList ToList () { return _source ?? []; }
  103. /// <inheritdoc/>
  104. public void Dispose ()
  105. {
  106. if (_source is { })
  107. {
  108. _source.CollectionChanged -= Source_CollectionChanged;
  109. }
  110. }
  111. /// <summary>
  112. /// INTERNAL: Searches the underlying collection for the first string element that starts with the specified search value,
  113. /// using a case-insensitive comparison.
  114. /// </summary>
  115. /// <remarks>
  116. /// The comparison is performed in a case-insensitive manner using invariant culture rules. Only
  117. /// elements of type string are considered; other types in the collection are ignored.
  118. /// </remarks>
  119. /// <param name="search">
  120. /// The string value to compare against the start of each string element in the collection. Cannot be
  121. /// null.
  122. /// </param>
  123. /// <returns>
  124. /// The zero-based index of the first matching string element if found; otherwise, -1 if no match is found or the
  125. /// collection is empty.
  126. /// </returns>
  127. internal int StartsWith (string search)
  128. {
  129. if (_source is null || _source?.Count == 0)
  130. {
  131. return -1;
  132. }
  133. for (var i = 0; i < _source!.Count; i++)
  134. {
  135. object? t = _source [i];
  136. if (t is string u)
  137. {
  138. if (u.ToUpper ().StartsWith (search.ToUpperInvariant ()))
  139. {
  140. return i;
  141. }
  142. }
  143. else if (t is string s && s.StartsWith (search, StringComparison.InvariantCultureIgnoreCase))
  144. {
  145. return i;
  146. }
  147. }
  148. return -1;
  149. }
  150. private void CheckAndResizeMarksIfRequired ()
  151. {
  152. if (_source != null && _count != _source.Count && _marks is { })
  153. {
  154. _count = _source.Count;
  155. var newMarks = new BitArray (_count);
  156. for (var i = 0; i < Math.Min (_marks.Length, newMarks.Length); i++)
  157. {
  158. newMarks [i] = _marks [i];
  159. }
  160. _marks = newMarks;
  161. Length = GetMaxLengthItem ();
  162. }
  163. }
  164. private int GetMaxLengthItem ()
  165. {
  166. if (_source is null || _source?.Count == 0)
  167. {
  168. return 0;
  169. }
  170. var maxLength = 0;
  171. for (var i = 0; i < _source!.Count; i++)
  172. {
  173. object? t = _source [i];
  174. if (t is null)
  175. {
  176. continue;
  177. }
  178. int l;
  179. l = t is string u ? u.GetColumns () : t.ToString ()!.Length;
  180. if (l > maxLength)
  181. {
  182. maxLength = l;
  183. }
  184. }
  185. return maxLength;
  186. }
  187. private static void RenderString (View driver, string str, int col, int line, int width, int viewportX = 0)
  188. {
  189. if (string.IsNullOrEmpty (str) || viewportX >= str.GetColumns ())
  190. {
  191. // Empty string or viewport beyond string - just fill with spaces
  192. for (var i = 0; i < width; i++)
  193. {
  194. driver.AddRune ((Rune)' ');
  195. }
  196. return;
  197. }
  198. int runeLength = str.ToRunes ().Length;
  199. int startIndex = Math.Min (viewportX, Math.Max (0, runeLength - 1));
  200. string substring = str.Substring (startIndex);
  201. string u = TextFormatter.ClipAndJustify (substring, width, Alignment.Start);
  202. driver.AddStr (u);
  203. width -= u.GetColumns ();
  204. while (width-- > 0)
  205. {
  206. driver.AddRune ((Rune)' ');
  207. }
  208. }
  209. private void Source_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e)
  210. {
  211. if (!SuspendCollectionChangedEvent)
  212. {
  213. CheckAndResizeMarksIfRequired ();
  214. CollectionChanged?.Invoke (sender, e);
  215. }
  216. }
  217. }