ListView.cs 29 KB

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