ListView.cs 29 KB

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