ListView.cs 29 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006
  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. SetNeedsDisplay ();
  196. }
  197. }
  198. /// <summary>
  199. /// Gets or sets the item that is displayed at the top of the <see cref="ListView"/>.
  200. /// </summary>
  201. /// <value>The top item.</value>
  202. public int TopItem {
  203. get => top;
  204. set {
  205. if (source == null)
  206. return;
  207. if (value < 0 || (source.Count > 0 && value >= source.Count))
  208. throw new ArgumentException ("value");
  209. top = value;
  210. SetNeedsDisplay ();
  211. }
  212. }
  213. /// <summary>
  214. /// Gets or sets the leftmost column that is currently visible (when scrolling horizontally).
  215. /// </summary>
  216. /// <value>The left position.</value>
  217. public int LeftItem {
  218. get => left;
  219. set {
  220. if (source == null)
  221. return;
  222. if (value < 0 || (Maxlength > 0 && value >= Maxlength))
  223. throw new ArgumentException ("value");
  224. left = value;
  225. SetNeedsDisplay ();
  226. }
  227. }
  228. /// <summary>
  229. /// Gets the widest item in the list.
  230. /// </summary>
  231. public int Maxlength => (source?.Length) ?? 0;
  232. /// <summary>
  233. /// Gets or sets the index of the currently selected item.
  234. /// </summary>
  235. /// <value>The selected item.</value>
  236. public int SelectedItem {
  237. get => selected;
  238. set {
  239. if (source == null || source.Count == 0) {
  240. return;
  241. }
  242. if (value < 0 || value >= source.Count) {
  243. throw new ArgumentException ("value");
  244. }
  245. selected = value;
  246. OnSelectedChanged ();
  247. }
  248. }
  249. static IListDataSource MakeWrapper (IList source)
  250. {
  251. return new ListWrapper (source);
  252. }
  253. /// <summary>
  254. /// Initializes a new instance of <see cref="ListView"/> that will display the
  255. /// contents of the object implementing the <see cref="IList"/> interface,
  256. /// with relative positioning.
  257. /// </summary>
  258. /// <param name="source">An <see cref="IList"/> data source, if the elements are strings or ustrings,
  259. /// the string is rendered, otherwise the ToString() method is invoked on the result.</param>
  260. public ListView (IList source) : this (MakeWrapper (source))
  261. {
  262. }
  263. /// <summary>
  264. /// Initializes a new instance of <see cref="ListView"/> that will display the provided data source, using relative positioning.
  265. /// </summary>
  266. /// <param name="source"><see cref="IListDataSource"/> object that provides a mechanism to render the data.
  267. /// The number of elements on the collection should not change, if you must change, set
  268. /// the "Source" property to reset the internal settings of the ListView.</param>
  269. public ListView (IListDataSource source) : base ()
  270. {
  271. this.source = source;
  272. Initialize ();
  273. }
  274. /// <summary>
  275. /// Initializes a new instance of <see cref="ListView"/>. Set the <see cref="Source"/> property to display something.
  276. /// </summary>
  277. public ListView () : base ()
  278. {
  279. Initialize ();
  280. }
  281. /// <summary>
  282. /// 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.
  283. /// </summary>
  284. /// <param name="rect">Frame for the listview.</param>
  285. /// <param name="source">An IList data source, if the elements of the IList are strings or ustrings,
  286. /// the string is rendered, otherwise the ToString() method is invoked on the result.</param>
  287. public ListView (Rect rect, IList source) : this (rect, MakeWrapper (source))
  288. {
  289. Initialize ();
  290. }
  291. /// <summary>
  292. /// Initializes a new instance of <see cref="ListView"/> with the provided data source and an absolute position
  293. /// </summary>
  294. /// <param name="rect">Frame for the listview.</param>
  295. /// <param name="source">IListDataSource object that provides a mechanism to render the data.
  296. /// The number of elements on the collection should not change, if you must change,
  297. /// set the "Source" property to reset the internal settings of the ListView.</param>
  298. public ListView (Rect rect, IListDataSource source) : base (rect)
  299. {
  300. this.source = source;
  301. Initialize ();
  302. }
  303. void Initialize ()
  304. {
  305. Source = source;
  306. CanFocus = true;
  307. // Things this view knows how to do
  308. AddCommand (Command.LineUp, () => MoveUp ());
  309. AddCommand (Command.LineDown, () => MoveDown ());
  310. AddCommand (Command.ScrollUp, () => ScrollUp (1));
  311. AddCommand (Command.ScrollDown, () => ScrollDown (1));
  312. AddCommand (Command.PageUp, () => MovePageUp ());
  313. AddCommand (Command.PageDown, () => MovePageDown ());
  314. AddCommand (Command.TopHome, () => MoveHome ());
  315. AddCommand (Command.BottomEnd, () => MoveEnd ());
  316. AddCommand (Command.OpenSelectedItem, () => OnOpenSelectedItem ());
  317. AddCommand (Command.ToggleChecked, () => MarkUnmarkRow ());
  318. // Default keybindings for all ListViews
  319. AddKeyBinding (Key.CursorUp, Command.LineUp);
  320. AddKeyBinding (Key.P | Key.CtrlMask, Command.LineUp);
  321. AddKeyBinding (Key.CursorDown, Command.LineDown);
  322. AddKeyBinding (Key.N | Key.CtrlMask, Command.LineDown);
  323. AddKeyBinding (Key.PageUp, Command.PageUp);
  324. AddKeyBinding (Key.PageDown, Command.PageDown);
  325. AddKeyBinding (Key.V | Key.CtrlMask, Command.PageDown);
  326. AddKeyBinding (Key.Home, Command.TopHome);
  327. AddKeyBinding (Key.End, Command.BottomEnd);
  328. AddKeyBinding (Key.Enter, Command.OpenSelectedItem);
  329. }
  330. ///<inheritdoc/>
  331. public override void Redraw (Rect bounds)
  332. {
  333. var current = ColorScheme.Focus;
  334. Driver.SetAttribute (current);
  335. Move (0, 0);
  336. var f = Frame;
  337. var item = top;
  338. bool focused = HasFocus;
  339. int col = allowsMarking ? 2 : 0;
  340. int start = left;
  341. for (int row = 0; row < f.Height; row++, item++) {
  342. bool isSelected = item == selected;
  343. var newcolor = focused ? (isSelected ? ColorScheme.Focus : GetNormalColor ())
  344. : (isSelected ? ColorScheme.HotNormal : GetNormalColor ());
  345. if (newcolor != current) {
  346. Driver.SetAttribute (newcolor);
  347. current = newcolor;
  348. }
  349. Move (0, row);
  350. if (source == null || item >= source.Count) {
  351. for (int c = 0; c < f.Width; c++)
  352. Driver.AddRune (' ');
  353. } else {
  354. var rowEventArgs = new ListViewRowEventArgs (item);
  355. OnRowRender (rowEventArgs);
  356. if (rowEventArgs.RowAttribute != null && current != rowEventArgs.RowAttribute) {
  357. current = (Attribute)rowEventArgs.RowAttribute;
  358. Driver.SetAttribute (current);
  359. }
  360. if (allowsMarking) {
  361. Driver.AddRune (source.IsMarked (item) ? (AllowsMultipleSelection ? Driver.Checked : Driver.Selected) :
  362. (AllowsMultipleSelection ? Driver.UnChecked : Driver.UnSelected));
  363. Driver.AddRune (' ');
  364. }
  365. Source.Render (this, Driver, isSelected, item, col, row, f.Width - col, start);
  366. }
  367. }
  368. }
  369. /// <summary>
  370. /// This event is raised when the selected item in the <see cref="ListView"/> has changed.
  371. /// </summary>
  372. public event Action<ListViewItemEventArgs> SelectedItemChanged;
  373. /// <summary>
  374. /// This event is raised when the user Double Clicks on an item or presses ENTER to open the selected item.
  375. /// </summary>
  376. public event Action<ListViewItemEventArgs> OpenSelectedItem;
  377. /// <summary>
  378. /// This event is invoked when this <see cref="ListView"/> is being drawn before rendering.
  379. /// </summary>
  380. public event Action<ListViewRowEventArgs> RowRender;
  381. private SearchCollectionNavigator navigator;
  382. ///<inheritdoc/>
  383. public override bool ProcessKey (KeyEvent kb)
  384. {
  385. if (source == null) {
  386. return base.ProcessKey (kb);
  387. }
  388. var result = InvokeKeybindings (kb);
  389. if (result != null) {
  390. return (bool)result;
  391. }
  392. // Enable user to find & select an item by typing text
  393. if (!kb.IsAlt && !kb.IsCapslock && !kb.IsCtrl && !kb.IsScrolllock && !kb.IsNumlock) {
  394. if (navigator == null) {
  395. // BUGBUG: If items change this needs to be recreated.
  396. navigator = new SearchCollectionNavigator (source.ToList ().Cast<object> ());
  397. }
  398. SelectedItem = navigator.CalculateNewIndex (SelectedItem, (char)kb.KeyValue);
  399. EnsuresVisibilitySelectedItem ();
  400. SetNeedsDisplay ();
  401. return true;
  402. }
  403. return false;
  404. }
  405. /// <summary>
  406. /// If <see cref="AllowsMarking"/> and <see cref="AllowsMultipleSelection"/> are both <see langword="true"/>,
  407. /// unmarks all marked items other than the currently selected.
  408. /// </summary>
  409. /// <returns><see langword="true"/> if unmarking was successful.</returns>
  410. public virtual bool AllowsAll ()
  411. {
  412. if (!allowsMarking)
  413. return false;
  414. if (!AllowsMultipleSelection) {
  415. for (int i = 0; i < Source.Count; i++) {
  416. if (Source.IsMarked (i) && i != selected) {
  417. Source.SetMark (i, false);
  418. return true;
  419. }
  420. }
  421. }
  422. return true;
  423. }
  424. /// <summary>
  425. /// Marks the <see cref="SelectedItem"/> if it is not already marked.
  426. /// </summary>
  427. /// <returns><see langword="true"/> if the <see cref="SelectedItem"/> was marked.</returns>
  428. public virtual bool MarkUnmarkRow ()
  429. {
  430. if (AllowsAll ()) {
  431. Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem));
  432. SetNeedsDisplay ();
  433. return true;
  434. }
  435. return false;
  436. }
  437. /// <summary>
  438. /// Changes the <see cref="SelectedItem"/> to the item at the top of the visible list.
  439. /// </summary>
  440. /// <returns></returns>
  441. public virtual bool MovePageUp ()
  442. {
  443. int n = (selected - Frame.Height);
  444. if (n < 0)
  445. n = 0;
  446. if (n != selected) {
  447. selected = n;
  448. top = selected;
  449. OnSelectedChanged ();
  450. SetNeedsDisplay ();
  451. }
  452. return true;
  453. }
  454. /// <summary>
  455. /// Changes the <see cref="SelectedItem"/> to the item just below the bottom
  456. /// of the visible list, scrolling if needed.
  457. /// </summary>
  458. /// <returns></returns>
  459. public virtual bool MovePageDown ()
  460. {
  461. var n = (selected + Frame.Height);
  462. if (n >= source.Count)
  463. n = source.Count - 1;
  464. if (n != selected) {
  465. selected = n;
  466. if (source.Count >= Frame.Height)
  467. top = selected;
  468. else
  469. top = 0;
  470. OnSelectedChanged ();
  471. SetNeedsDisplay ();
  472. }
  473. return true;
  474. }
  475. /// <summary>
  476. /// Changes the <see cref="SelectedItem"/> to the next item in the list,
  477. /// scrolling the list if needed.
  478. /// </summary>
  479. /// <returns></returns>
  480. public virtual bool MoveDown ()
  481. {
  482. if (source.Count == 0) {
  483. // Do we set lastSelectedItem to -1 here?
  484. return false; //Nothing for us to move to
  485. }
  486. if (selected >= source.Count) {
  487. // If for some reason we are currently outside of the
  488. // valid values range, we should select the bottommost valid value.
  489. // This can occur if the backing data source changes.
  490. selected = source.Count - 1;
  491. OnSelectedChanged ();
  492. SetNeedsDisplay ();
  493. } else if (selected + 1 < source.Count) { //can move by down by one.
  494. selected++;
  495. if (selected >= top + Frame.Height) {
  496. top++;
  497. } else if (selected < top) {
  498. top = selected;
  499. } else if (selected < top) {
  500. top = selected;
  501. }
  502. OnSelectedChanged ();
  503. SetNeedsDisplay ();
  504. } else if (selected == 0) {
  505. OnSelectedChanged ();
  506. SetNeedsDisplay ();
  507. } else if (selected >= top + Frame.Height) {
  508. top = source.Count - Frame.Height;
  509. SetNeedsDisplay ();
  510. }
  511. return true;
  512. }
  513. /// <summary>
  514. /// Changes the <see cref="SelectedItem"/> to the previous item in the list,
  515. /// scrolling the list if needed.
  516. /// </summary>
  517. /// <returns></returns>
  518. public virtual bool MoveUp ()
  519. {
  520. if (source.Count == 0) {
  521. // Do we set lastSelectedItem to -1 here?
  522. return false; //Nothing for us to move to
  523. }
  524. if (selected >= source.Count) {
  525. // If for some reason we are currently outside of the
  526. // valid values range, we should select the bottommost valid value.
  527. // This can occur if the backing data source changes.
  528. selected = source.Count - 1;
  529. OnSelectedChanged ();
  530. SetNeedsDisplay ();
  531. } else if (selected > 0) {
  532. selected--;
  533. if (selected > Source.Count) {
  534. selected = Source.Count - 1;
  535. }
  536. if (selected < top) {
  537. top = selected;
  538. } else if (selected > top + Frame.Height) {
  539. top = Math.Max (selected - Frame.Height + 1, 0);
  540. }
  541. OnSelectedChanged ();
  542. SetNeedsDisplay ();
  543. } else if (selected < top) {
  544. top = selected;
  545. SetNeedsDisplay ();
  546. }
  547. return true;
  548. }
  549. /// <summary>
  550. /// Changes the <see cref="SelectedItem"/> to last item in the list,
  551. /// scrolling the list if needed.
  552. /// </summary>
  553. /// <returns></returns>
  554. public virtual bool MoveEnd ()
  555. {
  556. if (source.Count > 0 && selected != source.Count - 1) {
  557. selected = source.Count - 1;
  558. if (top + selected > Frame.Height - 1) {
  559. top = selected;
  560. }
  561. OnSelectedChanged ();
  562. SetNeedsDisplay ();
  563. }
  564. return true;
  565. }
  566. /// <summary>
  567. /// Changes the <see cref="SelectedItem"/> to the first item in the list,
  568. /// scrolling the list if needed.
  569. /// </summary>
  570. /// <returns></returns>
  571. public virtual bool MoveHome ()
  572. {
  573. if (selected != 0) {
  574. selected = 0;
  575. top = selected;
  576. OnSelectedChanged ();
  577. SetNeedsDisplay ();
  578. }
  579. return true;
  580. }
  581. /// <summary>
  582. /// Scrolls the view down by <paramref name="items"/> items.
  583. /// </summary>
  584. /// <param name="items">Number of items to scroll down.</param>
  585. public virtual bool ScrollDown (int items)
  586. {
  587. top = Math.Max (Math.Min (top + items, source.Count - 1), 0);
  588. SetNeedsDisplay ();
  589. return true;
  590. }
  591. /// <summary>
  592. /// Scrolls the view up by <paramref name="items"/> items.
  593. /// </summary>
  594. /// <param name="items">Number of items to scroll up.</param>
  595. public virtual bool ScrollUp (int items)
  596. {
  597. top = Math.Max (top - items, 0);
  598. SetNeedsDisplay ();
  599. return true;
  600. }
  601. /// <summary>
  602. /// Scrolls the view right.
  603. /// </summary>
  604. /// <param name="cols">Number of columns to scroll right.</param>
  605. public virtual bool ScrollRight (int cols)
  606. {
  607. left = Math.Max (Math.Min (left + cols, Maxlength - 1), 0);
  608. SetNeedsDisplay ();
  609. return true;
  610. }
  611. /// <summary>
  612. /// Scrolls the view left.
  613. /// </summary>
  614. /// <param name="cols">Number of columns to scroll left.</param>
  615. public virtual bool ScrollLeft (int cols)
  616. {
  617. left = Math.Max (left - cols, 0);
  618. SetNeedsDisplay ();
  619. return true;
  620. }
  621. int lastSelectedItem = -1;
  622. private bool allowsMultipleSelection = true;
  623. private System.Timers.Timer searchTimer;
  624. /// <summary>
  625. /// Invokes the <see cref="SelectedItemChanged"/> event if it is defined.
  626. /// </summary>
  627. /// <returns></returns>
  628. public virtual bool OnSelectedChanged ()
  629. {
  630. if (selected != lastSelectedItem) {
  631. var value = source?.Count > 0 ? source.ToList () [selected] : null;
  632. SelectedItemChanged?.Invoke (new ListViewItemEventArgs (selected, value));
  633. if (HasFocus) {
  634. lastSelectedItem = selected;
  635. }
  636. return true;
  637. }
  638. return false;
  639. }
  640. /// <summary>
  641. /// Invokes the <see cref="OpenSelectedItem"/> event if it is defined.
  642. /// </summary>
  643. /// <returns></returns>
  644. public virtual bool OnOpenSelectedItem ()
  645. {
  646. if (source.Count <= selected || selected < 0 || OpenSelectedItem == null) {
  647. return false;
  648. }
  649. var value = source.ToList () [selected];
  650. OpenSelectedItem?.Invoke (new ListViewItemEventArgs (selected, value));
  651. return true;
  652. }
  653. /// <summary>
  654. /// Virtual method that will invoke the <see cref="RowRender"/>.
  655. /// </summary>
  656. /// <param name="rowEventArgs"></param>
  657. public virtual void OnRowRender (ListViewRowEventArgs rowEventArgs)
  658. {
  659. RowRender?.Invoke (rowEventArgs);
  660. }
  661. ///<inheritdoc/>
  662. public override bool OnEnter (View view)
  663. {
  664. Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
  665. if (lastSelectedItem == -1) {
  666. EnsuresVisibilitySelectedItem ();
  667. OnSelectedChanged ();
  668. }
  669. return base.OnEnter (view);
  670. }
  671. ///<inheritdoc/>
  672. public override bool OnLeave (View view)
  673. {
  674. if (lastSelectedItem > -1) {
  675. lastSelectedItem = -1;
  676. }
  677. return base.OnLeave (view);
  678. }
  679. void EnsuresVisibilitySelectedItem ()
  680. {
  681. SuperView?.LayoutSubviews ();
  682. if (selected < top) {
  683. top = selected;
  684. } else if (Frame.Height > 0 && selected >= top + Frame.Height) {
  685. top = Math.Max (selected - Frame.Height + 1, 0);
  686. }
  687. }
  688. ///<inheritdoc/>
  689. public override void PositionCursor ()
  690. {
  691. if (allowsMarking)
  692. Move (0, selected - top);
  693. else
  694. Move (Bounds.Width - 1, selected - top);
  695. }
  696. ///<inheritdoc/>
  697. public override bool MouseEvent (MouseEvent me)
  698. {
  699. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked) && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked) &&
  700. me.Flags != MouseFlags.WheeledDown && me.Flags != MouseFlags.WheeledUp &&
  701. me.Flags != MouseFlags.WheeledRight && me.Flags != MouseFlags.WheeledLeft)
  702. return false;
  703. if (!HasFocus && CanFocus) {
  704. SetFocus ();
  705. }
  706. if (source == null) {
  707. return false;
  708. }
  709. if (me.Flags == MouseFlags.WheeledDown) {
  710. ScrollDown (1);
  711. return true;
  712. } else if (me.Flags == MouseFlags.WheeledUp) {
  713. ScrollUp (1);
  714. return true;
  715. } else if (me.Flags == MouseFlags.WheeledRight) {
  716. ScrollRight (1);
  717. return true;
  718. } else if (me.Flags == MouseFlags.WheeledLeft) {
  719. ScrollLeft (1);
  720. return true;
  721. }
  722. if (me.Y + top >= source.Count) {
  723. return true;
  724. }
  725. selected = top + me.Y;
  726. if (AllowsAll ()) {
  727. Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem));
  728. SetNeedsDisplay ();
  729. return true;
  730. }
  731. OnSelectedChanged ();
  732. SetNeedsDisplay ();
  733. if (me.Flags == MouseFlags.Button1DoubleClicked) {
  734. OnOpenSelectedItem ();
  735. }
  736. return true;
  737. }
  738. }
  739. /// <inheritdoc/>
  740. public class ListWrapper : IListDataSourceSearchable {
  741. IList src;
  742. BitArray marks;
  743. int count, len;
  744. /// <inheritdoc/>
  745. public ListWrapper (IList source)
  746. {
  747. if (source != null) {
  748. count = source.Count;
  749. marks = new BitArray (count);
  750. src = source;
  751. len = GetMaxLengthItem ();
  752. }
  753. }
  754. /// <inheritdoc/>
  755. public int Count => src != null ? src.Count : 0;
  756. /// <inheritdoc/>
  757. public int Length => len;
  758. int GetMaxLengthItem ()
  759. {
  760. if (src == null || src?.Count == 0) {
  761. return 0;
  762. }
  763. int maxLength = 0;
  764. for (int i = 0; i < src.Count; i++) {
  765. var t = src [i];
  766. int l;
  767. if (t is ustring u) {
  768. l = u.RuneCount;
  769. } else if (t is string s) {
  770. l = s.Length;
  771. } else {
  772. l = t.ToString ().Length;
  773. }
  774. if (l > maxLength) {
  775. maxLength = l;
  776. }
  777. }
  778. return maxLength;
  779. }
  780. void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width, int start = 0)
  781. {
  782. int byteLen = ustr.Length;
  783. int used = 0;
  784. for (int i = start; i < byteLen;) {
  785. (var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen);
  786. var count = Rune.ColumnWidth (rune);
  787. if (used + count > width)
  788. break;
  789. driver.AddRune (rune);
  790. used += count;
  791. i += size;
  792. }
  793. for (; used < width; used++) {
  794. driver.AddRune (' ');
  795. }
  796. }
  797. /// <inheritdoc/>
  798. public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width, int start = 0)
  799. {
  800. container.Move (col, line);
  801. var t = src? [item];
  802. if (t == null) {
  803. RenderUstr (driver, ustring.Make (""), col, line, width);
  804. } else {
  805. if (t is ustring u) {
  806. RenderUstr (driver, u, col, line, width, start);
  807. } else if (t is string s) {
  808. RenderUstr (driver, s, col, line, width, start);
  809. } else {
  810. RenderUstr (driver, t.ToString (), col, line, width, start);
  811. }
  812. }
  813. }
  814. /// <inheritdoc/>
  815. public bool IsMarked (int item)
  816. {
  817. if (item >= 0 && item < count)
  818. return marks [item];
  819. return false;
  820. }
  821. /// <inheritdoc/>
  822. public void SetMark (int item, bool value)
  823. {
  824. if (item >= 0 && item < count)
  825. marks [item] = value;
  826. }
  827. /// <inheritdoc/>
  828. public IList ToList ()
  829. {
  830. return src;
  831. }
  832. /// <inheritdoc/>
  833. public int StartsWith (string search)
  834. {
  835. if (src == null || src?.Count == 0) {
  836. return -1;
  837. }
  838. for (int i = 0; i < src.Count; i++) {
  839. var t = src [i];
  840. if (t is ustring u) {
  841. if (u.ToUpper ().StartsWith (search.ToUpperInvariant ())) {
  842. return i;
  843. }
  844. } else if (t is string s) {
  845. if (s.ToUpperInvariant().StartsWith (search.ToUpperInvariant())) {
  846. return i;
  847. }
  848. }
  849. }
  850. return -1;
  851. }
  852. }
  853. /// <summary>
  854. /// <see cref="EventArgs"/> for <see cref="ListView"/> events.
  855. /// </summary>
  856. public class ListViewItemEventArgs : EventArgs {
  857. /// <summary>
  858. /// The index of the <see cref="ListView"/> item.
  859. /// </summary>
  860. public int Item { get; }
  861. /// <summary>
  862. /// The <see cref="ListView"/> item.
  863. /// </summary>
  864. public object Value { get; }
  865. /// <summary>
  866. /// Initializes a new instance of <see cref="ListViewItemEventArgs"/>
  867. /// </summary>
  868. /// <param name="item">The index of the <see cref="ListView"/> item.</param>
  869. /// <param name="value">The <see cref="ListView"/> item</param>
  870. public ListViewItemEventArgs (int item, object value)
  871. {
  872. Item = item;
  873. Value = value;
  874. }
  875. }
  876. /// <summary>
  877. /// <see cref="EventArgs"/> used by the <see cref="ListView.RowRender"/> event.
  878. /// </summary>
  879. public class ListViewRowEventArgs : EventArgs {
  880. /// <summary>
  881. /// The current row being rendered.
  882. /// </summary>
  883. public int Row { get; }
  884. /// <summary>
  885. /// The <see cref="Attribute"/> used by current row or
  886. /// null to maintain the current attribute.
  887. /// </summary>
  888. public Attribute? RowAttribute { get; set; }
  889. /// <summary>
  890. /// Initializes with the current row.
  891. /// </summary>
  892. /// <param name="row"></param>
  893. public ListViewRowEventArgs (int row)
  894. {
  895. Row = row;
  896. }
  897. }
  898. }