ListView.cs 29 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Linq;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using NStack;
  8. namespace Terminal.Gui {
  9. /// <summary>
  10. /// Implement <see cref="IListDataSource"/> to provide custom rendering for a <see cref="ListView"/>.
  11. /// </summary>
  12. public interface IListDataSource {
  13. /// <summary>
  14. /// Returns the number of elements to display
  15. /// </summary>
  16. int Count { get; }
  17. /// <summary>
  18. /// Returns the maximum length of elements to display
  19. /// </summary>
  20. int Length { get; }
  21. /// <summary>
  22. /// This method is invoked to render a specified item, the method should cover the entire provided width.
  23. /// </summary>
  24. /// <returns>The render.</returns>
  25. /// <param name="container">The list view to render.</param>
  26. /// <param name="driver">The console driver to render.</param>
  27. /// <param name="selected">Describes whether the item being rendered is currently selected by the user.</param>
  28. /// <param name="item">The index of the item to render, zero for the first item and so on.</param>
  29. /// <param name="col">The column where the rendering will start</param>
  30. /// <param name="line">The line where the rendering will be done.</param>
  31. /// <param name="width">The width that must be filled out.</param>
  32. /// <param name="start">The index of the string to be displayed.</param>
  33. /// <remarks>
  34. /// The default color will be set before this method is invoked, and will be based on whether the item is selected or not.
  35. /// </remarks>
  36. void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width, int start = 0);
  37. /// <summary>
  38. /// Should return whether the specified item is currently marked.
  39. /// </summary>
  40. /// <returns><see langword="true"/>, if marked, <see langword="false"/> otherwise.</returns>
  41. /// <param name="item">Item index.</param>
  42. bool IsMarked (int item);
  43. /// <summary>
  44. /// Flags the item as marked.
  45. /// </summary>
  46. /// <param name="item">Item index.</param>
  47. /// <param name="value">If set to <see langword="true"/> value.</param>
  48. void SetMark (int item, bool value);
  49. /// <summary>
  50. /// Return the source as IList.
  51. /// </summary>
  52. /// <returns></returns>
  53. IList ToList ();
  54. }
  55. /// <summary>
  56. /// Implement <see cref="IListDataSourceSearchable"/> to provide custom rendering for a <see cref="ListView"/> that
  57. /// supports searching for items.
  58. /// </summary>
  59. public interface IListDataSourceSearchable : IListDataSource {
  60. /// <summary>
  61. /// Finds the first item that starts with the specified search string. Used by the default implementation
  62. /// to support typing the first characters of an item to find it and move the selection to i.
  63. /// </summary>
  64. /// <param name="search">Text to search for.</param>
  65. /// <returns>The index of the first <see cref="ListView"/> item that starts with <paramref name="search"/>.
  66. /// Returns <see langword="-1"/> if <paramref name="search"/> was not found.</returns>
  67. int StartsWith (string search);
  68. }
  69. /// <summary>
  70. /// ListView <see cref="View"/> renders a scrollable list of data where each item can be activated to perform an action.
  71. /// </summary>
  72. /// <remarks>
  73. /// <para>
  74. /// The <see cref="ListView"/> displays lists of data and allows the user to scroll through the data.
  75. /// Items in the can be activated firing an event (with the ENTER key or a mouse double-click).
  76. /// If the <see cref="AllowsMarking"/> property is true, elements of the list can be marked by the user.
  77. /// </para>
  78. /// <para>
  79. /// By default <see cref="ListView"/> uses <see cref="object.ToString"/> to render the items of any
  80. /// <see cref="IList"/> object (e.g. arrays, <see cref="List{T}"/>,
  81. /// and other collections). Alternatively, an object that implements <see cref="IListDataSource"/>
  82. /// or <see cref="IListDataSourceSearchable"/> can be provided giving full control of what is rendered.
  83. /// </para>
  84. /// <para>
  85. /// <see cref="ListView"/> can display any object that implements the <see cref="IList"/> interface.
  86. /// <see cref="string"/> values are converted into <see cref="ustring"/> values before rendering, and other values are
  87. /// converted into <see cref="string"/> by calling <see cref="object.ToString"/> and then converting to <see cref="ustring"/> .
  88. /// </para>
  89. /// <para>
  90. /// To change the contents of the ListView, set the <see cref="Source"/> property (when
  91. /// providing custom rendering via <see cref="IListDataSource"/>) or call <see cref="SetSource"/>
  92. /// an <see cref="IList"/> is being used.
  93. /// </para>
  94. /// <para>
  95. /// When <see cref="AllowsMarking"/> is set to true the rendering will prefix the rendered items with
  96. /// [x] or [ ] and bind the SPACE key to toggle the selection. To implement a different
  97. /// marking style set <see cref="AllowsMarking"/> to false and implement custom rendering.
  98. /// </para>
  99. /// <para>
  100. /// By default or if <see cref="Source"/> is set to an object that implements
  101. /// <see cref="IListDataSourceSearchable"/>, searching the ListView with the keyboard is supported. Users type the
  102. /// first characters of an item, and the first item that starts with what the user types will be selected.
  103. /// </para>
  104. /// </remarks>
  105. public class ListView : View {
  106. int top, left;
  107. int selected;
  108. IListDataSource source;
  109. /// <summary>
  110. /// Gets or sets the <see cref="IListDataSource"/> backing this <see cref="ListView"/>, enabling custom rendering.
  111. /// </summary>
  112. /// <value>The source.</value>
  113. /// <remarks>
  114. /// Use <see cref="SetSource"/> to set a new <see cref="IList"/> source.
  115. /// </remarks>
  116. public IListDataSource Source {
  117. get => source;
  118. set {
  119. source = value;
  120. top = 0;
  121. selected = 0;
  122. lastSelectedItem = -1;
  123. SetNeedsDisplay ();
  124. }
  125. }
  126. /// <summary>
  127. /// Sets the source of the <see cref="ListView"/> to an <see cref="IList"/>.
  128. /// </summary>
  129. /// <value>An object implementing the IList interface.</value>
  130. /// <remarks>
  131. /// Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custome rendering.
  132. /// </remarks>
  133. public void SetSource (IList source)
  134. {
  135. if (source == null && (Source == null || !(Source is ListWrapper)))
  136. Source = null;
  137. else {
  138. Source = MakeWrapper (source);
  139. }
  140. }
  141. /// <summary>
  142. /// Sets the source to an <see cref="IList"/> value asynchronously.
  143. /// </summary>
  144. /// <value>An item implementing the IList interface.</value>
  145. /// <remarks>
  146. /// Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custom rendering.
  147. /// </remarks>
  148. public Task SetSourceAsync (IList source)
  149. {
  150. return Task.Factory.StartNew (() => {
  151. if (source == null && (Source == null || !(Source is ListWrapper)))
  152. Source = null;
  153. else
  154. Source = MakeWrapper (source);
  155. return source;
  156. }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
  157. }
  158. bool allowsMarking;
  159. /// <summary>
  160. /// Gets or sets whether this <see cref="ListView"/> allows items to be marked.
  161. /// </summary>
  162. /// <value>Set to <see langword="true"/> to allow marking elements of the list.</value>
  163. /// <remarks>
  164. /// If set to <see langword="true"/>, <see cref="ListView"/> will render items marked items with "[x]", and unmarked items with "[ ]"
  165. /// spaces. SPACE key will toggle marking. The default is <see langword="false"/>.
  166. /// </remarks>
  167. public bool AllowsMarking {
  168. get => allowsMarking;
  169. set {
  170. allowsMarking = value;
  171. if (allowsMarking) {
  172. AddKeyBinding (Key.Space, Command.ToggleChecked);
  173. } else {
  174. ClearKeybinding (Key.Space);
  175. }
  176. SetNeedsDisplay ();
  177. }
  178. }
  179. /// <summary>
  180. /// If set to <see langword="true"/> more than one item can be selected. If <see langword="false"/> selecting
  181. /// an item will cause all others to be un-selected. The default is <see langword="false"/>.
  182. /// </summary>
  183. public bool AllowsMultipleSelection {
  184. get => allowsMultipleSelection;
  185. set {
  186. allowsMultipleSelection = value;
  187. if (Source != null && !allowsMultipleSelection) {
  188. // Clear all selections except selected
  189. for (int i = 0; i < Source.Count; i++) {
  190. if (Source.IsMarked (i) && i != selected) {
  191. Source.SetMark (i, false);
  192. }
  193. }
  194. }
  195. }
  196. }
  197. /// <summary>
  198. /// Gets or sets the item that is displayed at the top of the <see cref="ListView"/>.
  199. /// </summary>
  200. /// <value>The top item.</value>
  201. public int TopItem {
  202. get => top;
  203. set {
  204. if (source == null)
  205. return;
  206. if (value < 0 || (source.Count > 0 && value >= source.Count))
  207. throw new ArgumentException ("value");
  208. top = value;
  209. SetNeedsDisplay ();
  210. }
  211. }
  212. /// <summary>
  213. /// Gets or sets the leftmost column that is currently visible (when scrolling horizontally).
  214. /// </summary>
  215. /// <value>The left position.</value>
  216. public int LeftItem {
  217. get => left;
  218. set {
  219. if (source == null)
  220. return;
  221. if (value < 0 || (Maxlength > 0 && value >= Maxlength))
  222. throw new ArgumentException ("value");
  223. left = value;
  224. SetNeedsDisplay ();
  225. }
  226. }
  227. /// <summary>
  228. /// Gets the widest item in the list.
  229. /// </summary>
  230. public int Maxlength => (source?.Length) ?? 0;
  231. /// <summary>
  232. /// Gets or sets the index of the currently selected item.
  233. /// </summary>
  234. /// <value>The selected item.</value>
  235. public int SelectedItem {
  236. get => selected;
  237. set {
  238. if (source == null || source.Count == 0) {
  239. return;
  240. }
  241. if (value < 0 || value >= source.Count) {
  242. throw new ArgumentException ("value");
  243. }
  244. selected = value;
  245. OnSelectedChanged ();
  246. }
  247. }
  248. static IListDataSource MakeWrapper (IList source)
  249. {
  250. return new ListWrapper (source);
  251. }
  252. /// <summary>
  253. /// Initializes a new instance of <see cref="ListView"/> that will display the
  254. /// contents of the object implementing the <see cref="IList"/> interface,
  255. /// with relative positioning.
  256. /// </summary>
  257. /// <param name="source">An <see cref="IList"/> data source, if the elements are strings or ustrings,
  258. /// the string is rendered, otherwise the ToString() method is invoked on the result.</param>
  259. public ListView (IList source) : this (MakeWrapper (source))
  260. {
  261. }
  262. /// <summary>
  263. /// Initializes a new instance of <see cref="ListView"/> that will display the provided data source, using relative positioning.
  264. /// </summary>
  265. /// <param name="source"><see cref="IListDataSource"/> object that provides a mechanism to render the data.
  266. /// The number of elements on the collection should not change, if you must change, set
  267. /// the "Source" property to reset the internal settings of the ListView.</param>
  268. public ListView (IListDataSource source) : base ()
  269. {
  270. this.source = source;
  271. Initialize ();
  272. }
  273. /// <summary>
  274. /// Initializes a new instance of <see cref="ListView"/>. Set the <see cref="Source"/> property to display something.
  275. /// </summary>
  276. public ListView () : base ()
  277. {
  278. Initialize ();
  279. }
  280. /// <summary>
  281. /// Initializes a new instance of <see cref="ListView"/> that will display the contents of the object implementing the <see cref="IList"/> interface with an absolute position.
  282. /// </summary>
  283. /// <param name="rect">Frame for the listview.</param>
  284. /// <param name="source">An IList data source, if the elements of the IList are strings or ustrings,
  285. /// the string is rendered, otherwise the ToString() method is invoked on the result.</param>
  286. public ListView (Rect rect, IList source) : this (rect, MakeWrapper (source))
  287. {
  288. Initialize ();
  289. }
  290. /// <summary>
  291. /// Initializes a new instance of <see cref="ListView"/> with the provided data source and an absolute position
  292. /// </summary>
  293. /// <param name="rect">Frame for the listview.</param>
  294. /// <param name="source">IListDataSource object that provides a mechanism to render the data.
  295. /// The number of elements on the collection should not change, if you must change,
  296. /// set the "Source" property to reset the internal settings of the ListView.</param>
  297. public ListView (Rect rect, IListDataSource source) : base (rect)
  298. {
  299. this.source = source;
  300. Initialize ();
  301. }
  302. void Initialize ()
  303. {
  304. Source = source;
  305. CanFocus = true;
  306. // Things this view knows how to do
  307. AddCommand (Command.LineUp, () => MoveUp ());
  308. AddCommand (Command.LineDown, () => MoveDown ());
  309. AddCommand (Command.ScrollUp, () => ScrollUp (1));
  310. AddCommand (Command.ScrollDown, () => ScrollDown (1));
  311. AddCommand (Command.PageUp, () => MovePageUp ());
  312. AddCommand (Command.PageDown, () => MovePageDown ());
  313. AddCommand (Command.TopHome, () => MoveHome ());
  314. AddCommand (Command.BottomEnd, () => MoveEnd ());
  315. AddCommand (Command.OpenSelectedItem, () => OnOpenSelectedItem ());
  316. AddCommand (Command.ToggleChecked, () => MarkUnmarkRow ());
  317. // Default keybindings for all ListViews
  318. AddKeyBinding (Key.CursorUp, Command.LineUp);
  319. AddKeyBinding (Key.P | Key.CtrlMask, Command.LineUp);
  320. AddKeyBinding (Key.CursorDown, Command.LineDown);
  321. AddKeyBinding (Key.N | Key.CtrlMask, Command.LineDown);
  322. AddKeyBinding (Key.PageUp, Command.PageUp);
  323. AddKeyBinding (Key.PageDown, Command.PageDown);
  324. AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown);
  325. AddKeyBinding (Key.Home, Command.TopHome);
  326. AddKeyBinding (Key.End, Command.BottomEnd);
  327. AddKeyBinding (Key.Enter, Command.OpenSelectedItem);
  328. }
  329. ///<inheritdoc/>
  330. public override void Redraw (Rect bounds)
  331. {
  332. var current = ColorScheme.Focus;
  333. Driver.SetAttribute (current);
  334. Move (0, 0);
  335. var f = Frame;
  336. var item = top;
  337. bool focused = HasFocus;
  338. int col = allowsMarking ? 2 : 0;
  339. int start = left;
  340. for (int row = 0; row < f.Height; row++, item++) {
  341. bool isSelected = item == selected;
  342. var newcolor = focused ? (isSelected ? ColorScheme.Focus : GetNormalColor ())
  343. : (isSelected ? ColorScheme.HotNormal : GetNormalColor ());
  344. if (newcolor != current) {
  345. Driver.SetAttribute (newcolor);
  346. current = newcolor;
  347. }
  348. Move (0, row);
  349. if (source == null || item >= source.Count) {
  350. for (int c = 0; c < f.Width; c++)
  351. Driver.AddRune (' ');
  352. } else {
  353. var rowEventArgs = new ListViewRowEventArgs (item);
  354. OnRowRender (rowEventArgs);
  355. if (rowEventArgs.RowAttribute != null && current != rowEventArgs.RowAttribute) {
  356. current = (Attribute)rowEventArgs.RowAttribute;
  357. Driver.SetAttribute (current);
  358. }
  359. if (allowsMarking) {
  360. Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) :
  361. (AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected));
  362. Driver.AddRune (' ');
  363. }
  364. Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start);
  365. }
  366. }
  367. }
  368. /// <summary>
  369. /// This event is raised when the selected item in the <see cref="ListView"/> has changed.
  370. /// </summary>
  371. public event Action<ListViewItemEventArgs> SelectedItemChanged;
  372. /// <summary>
  373. /// This event is raised when the user Double Clicks on an item or presses ENTER to open the selected item.
  374. /// </summary>
  375. public event Action<ListViewItemEventArgs> OpenSelectedItem;
  376. /// <summary>
  377. /// This event is invoked when this <see cref="ListView"/> is being drawn before rendering.
  378. /// </summary>
  379. public event Action<ListViewRowEventArgs> RowRender;
  380. private SearchCollectionNavigator navigator;
  381. ///<inheritdoc/>
  382. public override bool ProcessKey (KeyEvent kb)
  383. {
  384. if (source == null) {
  385. return base.ProcessKey (kb);
  386. }
  387. var result = InvokeKeybindings (kb);
  388. if (result != null) {
  389. return (bool)result;
  390. }
  391. // Enable user to find & select an item by typing text
  392. if (!kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock) {
  393. if (navigator == null) {
  394. // BUGBUG: If items change this needs to be recreated.
  395. navigator = new SearchCollectionNavigator (source.ToList().Cast<string>());
  396. }
  397. SelectedItem = navigator.CalculateNewIndex (SelectedItem, (char)kb.KeyValue);
  398. EnsuresVisibilitySelectedItem ();
  399. SetNeedsDisplay ();
  400. return true;
  401. }
  402. return false;
  403. }
  404. /// <summary>
  405. /// If <see cref="AllowsMarking"/> and <see cref="AllowsMultipleSelection"/> are both <see langword="true"/>,
  406. /// unmarks all marked items other than the currently selected.
  407. /// </summary>
  408. /// <returns><see langword="true"/> if unmarking was successful.</returns>
  409. public virtual bool AllowsAll ()
  410. {
  411. if (!allowsMarking)
  412. return false;
  413. if (!AllowsMultipleSelection) {
  414. for (int i = 0; i < Source.Count; i++) {
  415. if (Source.IsMarked (i) && i != selected) {
  416. Source.SetMark (i, false);
  417. return true;
  418. }
  419. }
  420. }
  421. return true;
  422. }
  423. /// <summary>
  424. /// Marks the <see cref="SelectedItem"/> if it is not already marked.
  425. /// </summary>
  426. /// <returns><see langword="true"/> if the <see cref="SelectedItem"/> was marked.</returns>
  427. public virtual bool MarkUnmarkRow ()
  428. {
  429. if (AllowsAll ()) {
  430. Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem));
  431. SetNeedsDisplay ();
  432. return true;
  433. }
  434. return false;
  435. }
  436. /// <summary>
  437. /// Changes the <see cref="SelectedItem"/> to the item at the top of the visible list.
  438. /// </summary>
  439. /// <returns></returns>
  440. public virtual bool MovePageUp ()
  441. {
  442. int n = (selected - Frame.Height);
  443. if (n < 0)
  444. n = 0;
  445. if (n != selected) {
  446. selected = n;
  447. top = selected;
  448. OnSelectedChanged ();
  449. SetNeedsDisplay ();
  450. }
  451. return true;
  452. }
  453. /// <summary>
  454. /// Changes the <see cref="SelectedItem"/> to the item just below the bottom
  455. /// of the visible list, scrolling if needed.
  456. /// </summary>
  457. /// <returns></returns>
  458. public virtual bool MovePageDown ()
  459. {
  460. var n = (selected + Frame.Height);
  461. if (n >= source.Count)
  462. n = source.Count - 1;
  463. if (n != selected) {
  464. selected = n;
  465. if (source.Count >= Frame.Height)
  466. top = selected;
  467. else
  468. top = 0;
  469. OnSelectedChanged ();
  470. SetNeedsDisplay ();
  471. }
  472. return true;
  473. }
  474. /// <summary>
  475. /// Changes the <see cref="SelectedItem"/> to the next item in the list,
  476. /// scrolling the list if needed.
  477. /// </summary>
  478. /// <returns></returns>
  479. public virtual bool MoveDown ()
  480. {
  481. if (source.Count == 0) {
  482. // Do we set lastSelectedItem to -1 here?
  483. return false; //Nothing for us to move to
  484. }
  485. if (selected >= source.Count) {
  486. // If for some reason we are currently outside of the
  487. // valid values range, we should select the bottommost valid value.
  488. // This can occur if the backing data source changes.
  489. selected = source.Count - 1;
  490. OnSelectedChanged ();
  491. SetNeedsDisplay ();
  492. } else if (selected + 1 < source.Count) { //can move by down by one.
  493. selected++;
  494. if (selected >= top + Frame.Height) {
  495. top++;
  496. } else if (selected < top) {
  497. top = selected;
  498. } else if (selected < top) {
  499. top = selected;
  500. }
  501. OnSelectedChanged ();
  502. SetNeedsDisplay ();
  503. } else if (selected == 0) {
  504. OnSelectedChanged ();
  505. SetNeedsDisplay ();
  506. } else if (selected >= top + Frame.Height) {
  507. top = source.Count - Frame.Height;
  508. SetNeedsDisplay ();
  509. }
  510. return true;
  511. }
  512. /// <summary>
  513. /// Changes the <see cref="SelectedItem"/> to the previous item in the list,
  514. /// scrolling the list if needed.
  515. /// </summary>
  516. /// <returns></returns>
  517. public virtual bool MoveUp ()
  518. {
  519. if (source.Count == 0) {
  520. // Do we set lastSelectedItem to -1 here?
  521. return false; //Nothing for us to move to
  522. }
  523. if (selected >= source.Count) {
  524. // If for some reason we are currently outside of the
  525. // valid values range, we should select the bottommost valid value.
  526. // This can occur if the backing data source changes.
  527. selected = source.Count - 1;
  528. OnSelectedChanged ();
  529. SetNeedsDisplay ();
  530. } else if (selected > 0) {
  531. selected--;
  532. if (selected > Source.Count) {
  533. selected = Source.Count - 1;
  534. }
  535. if (selected < top) {
  536. top = selected;
  537. } else if (selected > top + Frame.Height) {
  538. top = Math.Max (selected - Frame.Height + 1, 0);
  539. }
  540. OnSelectedChanged ();
  541. SetNeedsDisplay ();
  542. } else if (selected < top) {
  543. top = selected;
  544. SetNeedsDisplay ();
  545. }
  546. return true;
  547. }
  548. /// <summary>
  549. /// Changes the <see cref="SelectedItem"/> to last item in the list,
  550. /// scrolling the list if needed.
  551. /// </summary>
  552. /// <returns></returns>
  553. public virtual bool MoveEnd ()
  554. {
  555. if (source.Count > 0 && selected != source.Count - 1) {
  556. selected = source.Count - 1;
  557. if (top + selected > Frame.Height - 1) {
  558. top = selected;
  559. }
  560. OnSelectedChanged ();
  561. SetNeedsDisplay ();
  562. }
  563. return true;
  564. }
  565. /// <summary>
  566. /// Changes the <see cref="SelectedItem"/> to the first item in the list,
  567. /// scrolling the list if needed.
  568. /// </summary>
  569. /// <returns></returns>
  570. public virtual bool MoveHome ()
  571. {
  572. if (selected != 0) {
  573. selected = 0;
  574. top = selected;
  575. OnSelectedChanged ();
  576. SetNeedsDisplay ();
  577. }
  578. return true;
  579. }
  580. /// <summary>
  581. /// Scrolls the view down by <paramref name="items"/> items.
  582. /// </summary>
  583. /// <param name="items">Number of items to scroll down.</param>
  584. public virtual bool ScrollDown (int items)
  585. {
  586. top = Math.Max (Math.Min (top + items, source.Count - 1), 0);
  587. SetNeedsDisplay ();
  588. return true;
  589. }
  590. /// <summary>
  591. /// Scrolls the view up by <paramref name="items"/> items.
  592. /// </summary>
  593. /// <param name="items">Number of items to scroll up.</param>
  594. public virtual bool ScrollUp (int items)
  595. {
  596. top = Math.Max (top - items, 0);
  597. SetNeedsDisplay ();
  598. return true;
  599. }
  600. /// <summary>
  601. /// Scrolls the view right.
  602. /// </summary>
  603. /// <param name="cols">Number of columns to scroll right.</param>
  604. public virtual bool ScrollRight (int cols)
  605. {
  606. left = Math.Max (Math.Min (left + cols, Maxlength - 1), 0);
  607. SetNeedsDisplay ();
  608. return true;
  609. }
  610. /// <summary>
  611. /// Scrolls the view left.
  612. /// </summary>
  613. /// <param name="cols">Number of columns to scroll left.</param>
  614. public virtual bool ScrollLeft (int cols)
  615. {
  616. left = Math.Max (left - cols, 0);
  617. SetNeedsDisplay ();
  618. return true;
  619. }
  620. int lastSelectedItem = -1;
  621. private bool allowsMultipleSelection = true;
  622. private System.Timers.Timer searchTimer;
  623. /// <summary>
  624. /// Invokes the <see cref="SelectedItemChanged"/> event if it is defined.
  625. /// </summary>
  626. /// <returns></returns>
  627. public virtual bool OnSelectedChanged ()
  628. {
  629. if (selected != lastSelectedItem) {
  630. var value = source?.Count > 0 ? source.ToList () [selected] : null;
  631. SelectedItemChanged?.Invoke (new ListViewItemEventArgs (selected, value));
  632. if (HasFocus) {
  633. lastSelectedItem = selected;
  634. }
  635. return true;
  636. }
  637. return false;
  638. }
  639. /// <summary>
  640. /// Invokes the <see cref="OpenSelectedItem"/> event if it is defined.
  641. /// </summary>
  642. /// <returns></returns>
  643. public virtual bool OnOpenSelectedItem ()
  644. {
  645. if (source.Count <= selected || selected < 0 || OpenSelectedItem == null) {
  646. return false;
  647. }
  648. var value = source.ToList () [selected];
  649. OpenSelectedItem?.Invoke (new ListViewItemEventArgs (selected, value));
  650. return true;
  651. }
  652. /// <summary>
  653. /// Virtual method that will invoke the <see cref="RowRender"/>.
  654. /// </summary>
  655. /// <param name="rowEventArgs"></param>
  656. public virtual void OnRowRender (ListViewRowEventArgs rowEventArgs)
  657. {
  658. RowRender?.Invoke (rowEventArgs);
  659. }
  660. ///<inheritdoc/>
  661. public override bool OnEnter (View view)
  662. {
  663. Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
  664. if (lastSelectedItem == -1) {
  665. EnsuresVisibilitySelectedItem ();
  666. OnSelectedChanged ();
  667. }
  668. return base.OnEnter (view);
  669. }
  670. ///<inheritdoc/>
  671. public override bool OnLeave (View view)
  672. {
  673. if (lastSelectedItem > -1) {
  674. lastSelectedItem = -1;
  675. }
  676. return base.OnLeave (view);
  677. }
  678. void EnsuresVisibilitySelectedItem ()
  679. {
  680. SuperView?.LayoutSubviews ();
  681. if (selected < top) {
  682. top = selected;
  683. } else if (Frame.Height > 0 && selected >= top + Frame.Height) {
  684. top = Math.Max (selected - Frame.Height + 1, 0);
  685. }
  686. }
  687. ///<inheritdoc/>
  688. public override void PositionCursor ()
  689. {
  690. if (allowsMarking)
  691. Move (0, selected - top);
  692. else
  693. Move (Bounds.Width - 1, selected - top);
  694. }
  695. ///<inheritdoc/>
  696. public override bool MouseEvent (MouseEvent me)
  697. {
  698. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
  699. me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp &&
  700. me.Flags != MouseFlags.WheeledRight && me.Flags != MouseFlags.WheeledLeft)
  701. return false;
  702. if (!HasFocus && CanFocus) {
  703. SetFocus ();
  704. }
  705. if (source == null) {
  706. return false;
  707. }
  708. if (me.Flags == MouseFlags.WheeledDown) {
  709. ScrollDown (1);
  710. return true;
  711. } else if (me.Flags == MouseFlags.WheeledUp) {
  712. ScrollUp (1);
  713. return true;
  714. } else if (me.Flags == MouseFlags.WheeledRight) {
  715. ScrollRight (1);
  716. return true;
  717. } else if (me.Flags == MouseFlags.WheeledLeft) {
  718. ScrollLeft (1);
  719. return true;
  720. }
  721. if (me.Y + top >= source.Count) {
  722. return true;
  723. }
  724. selected = top + me.Y;
  725. if (AllowsAll ()) {
  726. Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem));
  727. SetNeedsDisplay ();
  728. return true;
  729. }
  730. OnSelectedChanged ();
  731. SetNeedsDisplay ();
  732. if (me.Flags == MouseFlags.Button1DoubleClicked) {
  733. OnOpenSelectedItem ();
  734. }
  735. return true;
  736. }
  737. }
  738. /// <inheritdoc/>
  739. public class ListWrapper : IListDataSourceSearchable {
  740. IList src;
  741. BitArray marks;
  742. int count, len;
  743. /// <inheritdoc/>
  744. public ListWrapper (IList source)
  745. {
  746. if (source != null) {
  747. count = source.Count;
  748. marks = new BitArray (count);
  749. src = source;
  750. len = GetMaxLengthItem ();
  751. }
  752. }
  753. /// <inheritdoc/>
  754. public int Count => src != null ? src.Count : 0;
  755. /// <inheritdoc/>
  756. public int Length => len;
  757. int GetMaxLengthItem ()
  758. {
  759. if (src == null || src?.Count == 0) {
  760. return 0;
  761. }
  762. int maxLength = 0;
  763. for (int i = 0; i < src.Count; i++) {
  764. var t = src [i];
  765. int l;
  766. if (t is ustring u) {
  767. l = u.RuneCount;
  768. } else if (t is string s) {
  769. l = s.Length;
  770. } else {
  771. l = t.ToString ().Length;
  772. }
  773. if (l > maxLength) {
  774. maxLength = l;
  775. }
  776. }
  777. return maxLength;
  778. }
  779. void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0)
  780. {
  781. int byteLen = ustr.Length;
  782. int used = 0;
  783. for (int i = start; i < byteLen;) {
  784. (var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen);
  785. var count = Rune.ColumnWidth (rune);
  786. if (used + count > width)
  787. break;
  788. driver.AddRune (rune);
  789. used += count;
  790. i += size;
  791. }
  792. for (; used < width; used++) {
  793. driver.AddRune (' ');
  794. }
  795. }
  796. /// <inheritdoc/>
  797. public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width, int start = 0)
  798. {
  799. container.Move (col, line);
  800. var t = src? [item];
  801. if (t == null) {
  802. RenderUstr (driver, ustring.Make (""), col, line, width);
  803. } else {
  804. if (t is ustring u) {
  805. RenderUstr (driver, u, col, line, width, start);
  806. } else if (t is string s) {
  807. RenderUstr (driver, s, col, line, width, start);
  808. } else {
  809. RenderUstr (driver, t.ToString (), col, line, width, start);
  810. }
  811. }
  812. }
  813. /// <inheritdoc/>
  814. public bool IsMarked (int item)
  815. {
  816. if (item >= 0 && item < count)
  817. return marks [item];
  818. return false;
  819. }
  820. /// <inheritdoc/>
  821. public void SetMark (int item, bool value)
  822. {
  823. if (item >= 0 && item < count)
  824. marks [item] = value;
  825. }
  826. /// <inheritdoc/>
  827. public IList ToList ()
  828. {
  829. return src;
  830. }
  831. /// <inheritdoc/>
  832. public int StartsWith (string search)
  833. {
  834. if (src == null || src?.Count == 0) {
  835. return -1;
  836. }
  837. for (int i = 0; i < src.Count; i++) {
  838. var t = src [i];
  839. if (t is ustring u) {
  840. if (u.ToUpper ().StartsWith (search.ToUpperInvariant ())) {
  841. return i;
  842. }
  843. } else if (t is string s) {
  844. if (s.ToUpperInvariant().StartsWith (search.ToUpperInvariant())) {
  845. return i;
  846. }
  847. }
  848. }
  849. return -1;
  850. }
  851. }
  852. /// <summary>
  853. /// <see cref="EventArgs"/> for <see cref="ListView"/> events.
  854. /// </summary>
  855. public class ListViewItemEventArgs : EventArgs {
  856. /// <summary>
  857. /// The index of the <see cref="ListView"/> item.
  858. /// </summary>
  859. public int Item { get; }
  860. /// <summary>
  861. /// The <see cref="ListView"/> item.
  862. /// </summary>
  863. public object Value { get; }
  864. /// <summary>
  865. /// Initializes a new instance of <see cref="ListViewItemEventArgs"/>
  866. /// </summary>
  867. /// <param name="item">The index of the <see cref="ListView"/> item.</param>
  868. /// <param name="value">The <see cref="ListView"/> item</param>
  869. public ListViewItemEventArgs (int item, object value)
  870. {
  871. Item = item;
  872. Value = value;
  873. }
  874. }
  875. /// <summary>
  876. /// <see cref="EventArgs"/> used by the <see cref="ListView.RowRender"/> event.
  877. /// </summary>
  878. public class ListViewRowEventArgs : EventArgs {
  879. /// <summary>
  880. /// The current row being rendered.
  881. /// </summary>
  882. public int Row { get; }
  883. /// <summary>
  884. /// The <see cref="Attribute"/> used by current row or
  885. /// null to maintain the current attribute.
  886. /// </summary>
  887. public Attribute? RowAttribute { get; set; }
  888. /// <summary>
  889. /// Initializes with the current row.
  890. /// </summary>
  891. /// <param name="row"></param>
  892. public ListViewRowEventArgs (int row)
  893. {
  894. Row = row;
  895. }
  896. }
  897. }