ListView.cs 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567
  1. //
  2. // ListView.cs: ListView control
  3. //
  4. // Authors:
  5. // Miguel de Icaza ([email protected])
  6. //
  7. //
  8. // TODO:
  9. // - Should we support multiple columns, if so, how should that be done?
  10. // - Show mark for items that have been marked.
  11. // - Mouse support
  12. // - Scrollbars?
  13. //
  14. // Column considerations:
  15. // - Would need a way to specify widths
  16. // - Should it automatically extract data out of structs/classes based on public fields/properties?
  17. // - It seems that this would be useful just for the "simple" API, not the IListDAtaSource, as that one has full support for it.
  18. // - Should a function be specified that retrieves the individual elements?
  19. //
  20. using System;
  21. using System.Collections;
  22. using System.Collections.Generic;
  23. using System.Threading;
  24. using System.Threading.Tasks;
  25. using NStack;
  26. namespace Terminal.Gui {
  27. /// <summary>
  28. /// Implement this interface to provide your own custom rendering for a list.
  29. /// </summary>
  30. public interface IListDataSource {
  31. /// <summary>
  32. /// Returns the number of elements to display
  33. /// </summary>
  34. int Count { get; }
  35. /// <summary>
  36. /// This method is invoked to render a specified item, the method should cover the entire provided width.
  37. /// </summary>
  38. /// <returns>The render.</returns>
  39. /// <param name="container">The list view to render.</param>
  40. /// <param name="driver">The console driver to render.</param>
  41. /// <param name="selected">Describes whether the item being rendered is currently selected by the user.</param>
  42. /// <param name="item">The index of the item to render, zero for the first item and so on.</param>
  43. /// <param name="col">The column where the rendering will start</param>
  44. /// <param name="line">The line where the rendering will be done.</param>
  45. /// <param name="width">The width that must be filled out.</param>
  46. /// <remarks>
  47. /// The default color will be set before this method is invoked, and will be based on whether the item is selected or not.
  48. /// </remarks>
  49. void Render (ListView container, ConsoleDriver driver, bool selected, int item, int col, int line, int width);
  50. /// <summary>
  51. /// Should return whether the specified item is currently marked.
  52. /// </summary>
  53. /// <returns><c>true</c>, if marked, <c>false</c> otherwise.</returns>
  54. /// <param name="item">Item index.</param>
  55. bool IsMarked (int item);
  56. /// <summary>
  57. /// Flags the item as marked.
  58. /// </summary>
  59. /// <param name="item">Item index.</param>
  60. /// <param name="value">If set to <c>true</c> value.</param>
  61. void SetMark (int item, bool value);
  62. }
  63. /// <summary>
  64. /// ListView widget renders a list of data.
  65. /// </summary>
  66. /// <remarks>
  67. /// <para>
  68. /// The ListView displays lists of data and allows the user to scroll through the data
  69. /// and optionally mark elements of the list (controlled by the AllowsMark property).
  70. /// </para>
  71. /// <para>
  72. /// The ListView can either render an arbitrary IList object (for example, arrays, List&lt;T&gt;
  73. /// and other collections) which are drawn by drawing the string/ustring contents or the
  74. /// result of calling ToString(). Alternatively, you can provide you own IListDataSource
  75. /// object that gives you full control of what is rendered.
  76. /// </para>
  77. /// <para>
  78. /// The ListView can display any object that implements the System.Collection.IList interface,
  79. /// string values are converted into ustring values before rendering, and other values are
  80. /// converted into ustrings by calling ToString() and then converting to ustring.
  81. /// </para>
  82. /// <para>
  83. /// If you must change the contents of the ListView, set the Source property (when you are
  84. /// providing your own rendering via the IListDataSource implementation) or call SetSource
  85. /// when you are providing an IList.
  86. /// </para>
  87. /// <para>
  88. /// When AllowsMark is set to true, then the rendering will prefix the list rendering with
  89. /// [x] or [ ] and bind the space character to toggle the selection. If you desire a different
  90. /// marking style do not set the property and provide your own custom rendering.
  91. /// </para>
  92. /// </remarks>
  93. public class ListView : View {
  94. int top;
  95. int selected;
  96. IListDataSource source;
  97. /// <summary>
  98. /// Gets or sets the IListDataSource backing this view, use SetSource() if you want to set a new IList source.
  99. /// </summary>
  100. /// <value>The source.</value>
  101. public IListDataSource Source {
  102. get => source;
  103. set {
  104. source = value;
  105. top = 0;
  106. selected = 0;
  107. SetNeedsDisplay ();
  108. }
  109. }
  110. /// <summary>
  111. /// Sets the source to an IList value, if you want to set a full IListDataSource, use the Source property.
  112. /// </summary>
  113. /// <value>An item implementing the IList interface.</value>
  114. public void SetSource (IList source)
  115. {
  116. if (source == null)
  117. Source = null;
  118. else {
  119. Source = MakeWrapper (source);
  120. }
  121. }
  122. /// <summary>
  123. /// Sets the source to an IList value asynchronously, if you want to set a full IListDataSource, use the Source property.
  124. /// </summary>
  125. /// <value>An item implementing the IList interface.</value>
  126. public Task SetSourceAsync (IList source)
  127. {
  128. return Task.Factory.StartNew (() => {
  129. if (source == null)
  130. Source = null;
  131. else
  132. Source = MakeWrapper (source);
  133. return source;
  134. }, CancellationToken.None, TaskCreationOptions.DenyChildAttach, TaskScheduler.Default);
  135. }
  136. bool allowsMarking;
  137. /// <summary>
  138. /// Gets or sets a value indicating whether this <see cref="T:Terminal.Gui.ListView"/> allows items to be marked.
  139. /// </summary>
  140. /// <value><c>true</c> if allows marking elements of the list; otherwise, <c>false</c>.
  141. /// </value>
  142. /// <remarks>
  143. /// If set to true, this will default to rendering the marked with "[x]", and unmarked valued with "[ ]"
  144. /// spaces. If you desire a different rendering, you need to implement your own renderer. This will
  145. /// also by default process the space character as a toggle for the selection.
  146. /// </remarks>
  147. public bool AllowsMarking {
  148. get => allowsMarking;
  149. set {
  150. allowsMarking = value;
  151. SetNeedsDisplay ();
  152. }
  153. }
  154. /// <summary>
  155. /// If set to true allows more than one item to be selected. If false only allow one item selected.
  156. /// </summary>
  157. public bool AllowsMultipleSelection { get; set; } = true;
  158. /// <summary>
  159. /// Gets or sets the item that is displayed at the top of the listview
  160. /// </summary>
  161. /// <value>The top item.</value>
  162. public int TopItem {
  163. get => top;
  164. set {
  165. if (source == null)
  166. return;
  167. if (top < 0 || top >= source.Count)
  168. throw new ArgumentException ("value");
  169. top = value;
  170. SetNeedsDisplay ();
  171. }
  172. }
  173. /// <summary>
  174. /// Gets or sets the currently selected item.
  175. /// </summary>
  176. /// <value>The selected item.</value>
  177. public int SelectedItem {
  178. get => selected;
  179. set {
  180. if (source == null)
  181. return;
  182. if (selected < 0 || selected >= source.Count)
  183. throw new ArgumentException ("value");
  184. selected = value;
  185. if (selected < top)
  186. top = selected;
  187. else if (selected >= top + Frame.Height)
  188. top = selected;
  189. }
  190. }
  191. static IListDataSource MakeWrapper (IList source)
  192. {
  193. return new ListWrapper (source);
  194. }
  195. /// <summary>
  196. /// Initializes a new ListView that will display the contents of the object implementing the IList interface, with relative positioning
  197. /// </summary>
  198. /// <param name="source">An IList data source, if the elements of the IList are strings or ustrings, the string is rendered, otherwise the ToString() method is invoked on the result.</param>
  199. public ListView (IList source) : this (MakeWrapper (source))
  200. {
  201. }
  202. /// <summary>
  203. /// Initializes a new ListView that will display the provided data source, uses relative positioning.
  204. /// </summary>
  205. /// <param name="source">IListDataSource object that provides a mechanism to render the data. The number of elements on the collection should not change, if you must change, set the "Source" property to reset the internal settings of the ListView.</param>
  206. public ListView (IListDataSource source) : base ()
  207. {
  208. Source = source;
  209. CanFocus = true;
  210. }
  211. /// <summary>
  212. /// Initializes a new instance of the <see cref="T:Terminal.Gui.ListView"/> class. You must set the Source property for this to show something.
  213. /// </summary>
  214. public ListView () : base ()
  215. {
  216. }
  217. /// <summary>
  218. /// Initializes a new ListView that will display the contents of the object implementing the IList interface with an absolute position.
  219. /// </summary>
  220. /// <param name="rect">Frame for the listview.</param>
  221. /// <param name="source">An IList data source, if the elements of the IList are strings or ustrings, the string is rendered, otherwise the ToString() method is invoked on the result.</param>
  222. public ListView (Rect rect, IList source) : this (rect, MakeWrapper (source))
  223. {
  224. }
  225. /// <summary>
  226. /// Initializes a new ListView that will display the provided data source with an absolute position
  227. /// </summary>
  228. /// <param name="rect">Frame for the listview.</param>
  229. /// <param name="source">IListDataSource object that provides a mechanism to render the data. The number of elements on the collection should not change, if you must change, set the "Source" property to reset the internal settings of the ListView.</param>
  230. public ListView (Rect rect, IListDataSource source) : base (rect)
  231. {
  232. Source = source;
  233. CanFocus = true;
  234. }
  235. /// <summary>
  236. /// Redraws the ListView
  237. /// </summary>
  238. /// <param name="region">Region.</param>
  239. public override void Redraw(Rect region)
  240. {
  241. var current = ColorScheme.Focus;
  242. Driver.SetAttribute (current);
  243. Move (0, 0);
  244. var f = Frame;
  245. var item = top;
  246. bool focused = HasFocus;
  247. int col = allowsMarking ? 4 : 0;
  248. for (int row = 0; row < f.Height; row++, item++) {
  249. bool isSelected = item == selected;
  250. var newcolor = focused ? (isSelected ? ColorScheme.Focus : ColorScheme.Normal) : ColorScheme.Normal;
  251. if (newcolor != current) {
  252. Driver.SetAttribute (newcolor);
  253. current = newcolor;
  254. }
  255. Move (0, row);
  256. if (source == null || item >= source.Count) {
  257. for (int c = 0; c < f.Width; c++)
  258. Driver.AddRune(' ');
  259. } else {
  260. if (allowsMarking) {
  261. Driver.AddStr (source.IsMarked (item) ? (AllowsMultipleSelection ? "[x] " : "(o)") : (AllowsMultipleSelection ? "[ ] " : "( )"));
  262. }
  263. Source.Render(this, Driver, isSelected, item, col, row, f.Width-col);
  264. }
  265. }
  266. }
  267. /// <summary>
  268. /// This event is raised when the cursor selection has changed.
  269. /// </summary>
  270. public event Action SelectedChanged;
  271. /// <summary>
  272. /// Handles cursor movement for this view, passes all other events.
  273. /// </summary>
  274. /// <returns><c>true</c>, if key was processed, <c>false</c> otherwise.</returns>
  275. /// <param name="kb">Keyboard event.</param>
  276. public override bool ProcessKey (KeyEvent kb)
  277. {
  278. if (source == null)
  279. return base.ProcessKey (kb);
  280. switch (kb.Key) {
  281. case Key.CursorUp:
  282. case Key.ControlP:
  283. return MoveUp();
  284. case Key.CursorDown:
  285. case Key.ControlN:
  286. return MoveDown();
  287. case Key.ControlV:
  288. case Key.PageDown:
  289. return MovePageDown();
  290. case Key.PageUp:
  291. return MovePageUp();
  292. case Key.Space:
  293. if (MarkUnmarkRow())
  294. return true;
  295. else
  296. break;
  297. }
  298. return base.ProcessKey (kb);
  299. }
  300. /// <summary>
  301. ///
  302. /// </summary>
  303. /// <returns></returns>
  304. public virtual bool AllowsAll ()
  305. {
  306. if (!allowsMarking)
  307. return false;
  308. if (!AllowsMultipleSelection) {
  309. for (int i = 0; i < Source.Count; i++) {
  310. if (Source.IsMarked (i) && i != selected) {
  311. Source.SetMark (i, false);
  312. return true;
  313. }
  314. }
  315. }
  316. return true;
  317. }
  318. /// <summary>
  319. ///
  320. /// </summary>
  321. /// <returns></returns>
  322. public virtual bool MarkUnmarkRow(){
  323. if (AllowsAll ()) {
  324. Source.SetMark(SelectedItem, !Source.IsMarked(SelectedItem));
  325. SetNeedsDisplay();
  326. return true;
  327. }
  328. return false;
  329. }
  330. /// <summary>
  331. ///
  332. /// </summary>
  333. /// <returns></returns>
  334. public virtual bool MovePageUp(){
  335. int n = (selected - Frame.Height);
  336. if (n < 0)
  337. n = 0;
  338. if (n != selected){
  339. selected = n;
  340. top = selected;
  341. if (SelectedChanged != null)
  342. SelectedChanged();
  343. SetNeedsDisplay();
  344. }
  345. return true;
  346. }
  347. /// <summary>
  348. ///
  349. /// </summary>
  350. /// <returns></returns>
  351. public virtual bool MovePageDown(){
  352. var n = (selected + Frame.Height);
  353. if (n > source.Count)
  354. n = source.Count - 1;
  355. if (n != selected){
  356. selected = n;
  357. if (source.Count >= Frame.Height)
  358. top = selected;
  359. else
  360. top = 0;
  361. if (SelectedChanged != null)
  362. SelectedChanged();
  363. SetNeedsDisplay();
  364. }
  365. return true;
  366. }
  367. /// <summary>
  368. ///
  369. /// </summary>
  370. /// <returns></returns>
  371. public virtual bool MoveDown(){
  372. if (selected + 1 < source.Count){
  373. selected++;
  374. if (selected >= top + Frame.Height)
  375. top++;
  376. if (SelectedChanged != null)
  377. SelectedChanged();
  378. SetNeedsDisplay();
  379. }
  380. return true;
  381. }
  382. /// <summary>
  383. ///
  384. /// </summary>
  385. /// <returns></returns>
  386. public virtual bool MoveUp(){
  387. if (selected > 0){
  388. selected--;
  389. if (selected < top)
  390. top = selected;
  391. if (SelectedChanged != null)
  392. SelectedChanged();
  393. SetNeedsDisplay();
  394. }
  395. return true;
  396. }
  397. /// <summary>
  398. /// Positions the cursor in this view
  399. /// </summary>
  400. public override void PositionCursor()
  401. {
  402. if (allowsMarking)
  403. Move (1, selected - top);
  404. else
  405. Move (0, selected - top);
  406. }
  407. ///<inheritdoc cref="MouseEvent(Gui.MouseEvent)"/>
  408. public override bool MouseEvent(MouseEvent me)
  409. {
  410. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked))
  411. return false;
  412. if (!HasFocus)
  413. SuperView.SetFocus (this);
  414. if (source == null)
  415. return false;
  416. if (me.Y + top >= source.Count)
  417. return true;
  418. selected = top + me.Y;
  419. if (AllowsAll ()) {
  420. Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem));
  421. SetNeedsDisplay ();
  422. return true;
  423. }
  424. if (SelectedChanged != null)
  425. SelectedChanged();
  426. SetNeedsDisplay ();
  427. return true;
  428. }
  429. }
  430. /// <summary>
  431. /// This class is the built-in IListDataSource that renders arbitrary
  432. /// IList instances
  433. /// </summary>
  434. public class ListWrapper : IListDataSource {
  435. IList src;
  436. BitArray marks;
  437. int count;
  438. /// <summary>
  439. /// constructor
  440. /// </summary>
  441. /// <param name="source"></param>
  442. public ListWrapper (IList source)
  443. {
  444. count = source.Count;
  445. marks = new BitArray (count);
  446. this.src = source;
  447. }
  448. /// <summary>
  449. /// Count of items.
  450. /// </summary>
  451. public int Count => src.Count;
  452. void RenderUstr (ConsoleDriver driver, ustring ustr, int col, int line, int width)
  453. {
  454. int byteLen = ustr.Length;
  455. int used = 0;
  456. for (int i = 0; i < byteLen;) {
  457. (var rune, var size) = Utf8.DecodeRune (ustr, i, i - byteLen);
  458. var count = Rune.ColumnWidth (rune);
  459. if (used+count >= width)
  460. break;
  461. driver.AddRune (rune);
  462. used += count;
  463. i += size;
  464. }
  465. for (; used < width; used++) {
  466. driver.AddRune (' ');
  467. }
  468. }
  469. /// <summary>
  470. /// Renders an item in the the list.
  471. /// </summary>
  472. /// <param name="container"></param>
  473. /// <param name="driver"></param>
  474. /// <param name="marked"></param>
  475. /// <param name="item"></param>
  476. /// <param name="col"></param>
  477. /// <param name="line"></param>
  478. /// <param name="width"></param>
  479. public void Render (ListView container, ConsoleDriver driver, bool marked, int item, int col, int line, int width)
  480. {
  481. container.Move (col, line);
  482. var t = src [item];
  483. if (t is ustring) {
  484. RenderUstr (driver, (ustring)t, col, line, width);
  485. } else if (t is string) {
  486. RenderUstr (driver, (string)t, col, line, width);
  487. } else
  488. RenderUstr (driver, t.ToString (), col, line, width);
  489. }
  490. /// <summary>
  491. /// Returns true of the item is marked. false if not.
  492. /// </summary>
  493. /// <param name="item"></param>
  494. /// <returns></returns>
  495. public bool IsMarked (int item)
  496. {
  497. if (item >= 0 && item < count)
  498. return marks [item];
  499. return false;
  500. }
  501. /// <summary>
  502. /// Sets the marked state of an item.
  503. /// </summary>
  504. /// <param name="item"></param>
  505. /// <param name="value"></param>
  506. public void SetMark (int item, bool value)
  507. {
  508. if (item >= 0 && item < count)
  509. marks [item] = value;
  510. }
  511. }
  512. }