ListView.cs 37 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048104910501051105210531054105510561057105810591060106110621063106410651066106710681069107010711072107310741075107610771078107910801081108210831084108510861087108810891090109110921093109410951096109710981099110011011102110311041105110611071108110911101111111211131114111511161117111811191120112111221123112411251126112711281129113011311132113311341135113611371138113911401141114211431144114511461147114811491150115111521153115411551156115711581159116011611162116311641165116611671168116911701171117211731174117511761177117811791180118111821183118411851186
  1. #nullable disable
  2. using System.Collections;
  3. using System.Collections.ObjectModel;
  4. using System.Collections.Specialized;
  5. namespace Terminal.Gui.Views;
  6. /// <summary>
  7. /// Provides a scrollable list of data where each item can be activated to perform an
  8. /// action.
  9. /// </summary>
  10. /// <remarks>
  11. /// <para>
  12. /// The <see cref="ListView"/> displays lists of data and allows the user to scroll through the data. Items in
  13. /// the can be activated firing an event (with the ENTER key or a mouse double-click). If the
  14. /// <see cref="AllowsMarking"/> property is true, elements of the list can be marked by the user.
  15. /// </para>
  16. /// <para>
  17. /// By default <see cref="ListView"/> uses <see cref="object.ToString"/> to render the items of any
  18. /// <see cref="ObservableCollection{T}"/> object (e.g. arrays, <see cref="List{T}"/>, and other collections). Alternatively, an
  19. /// object that implements <see cref="IListDataSource"/> can be provided giving full control of what is rendered.
  20. /// </para>
  21. /// <para>
  22. /// <see cref="ListView"/> can display any object that implements the <see cref="IList"/> interface.
  23. /// <see cref="string"/> values are converted into <see cref="string"/> values before rendering, and other values
  24. /// are converted into <see cref="string"/> by calling <see cref="object.ToString"/> and then converting to
  25. /// <see cref="string"/> .
  26. /// </para>
  27. /// <para>
  28. /// To change the contents of the ListView, set the <see cref="Source"/> property (when providing custom
  29. /// rendering via <see cref="IListDataSource"/>) or call <see cref="SetSource{T}"/> an <see cref="IList"/> is being
  30. /// used.
  31. /// </para>
  32. /// <para>
  33. /// When <see cref="AllowsMarking"/> is set to true the rendering will prefix the rendered items with [x] or [ ]
  34. /// and bind the SPACE key to toggle the selection. To implement a different marking style set
  35. /// <see cref="AllowsMarking"/> to false and implement custom rendering.
  36. /// </para>
  37. /// <para>
  38. /// Searching the ListView with the keyboard is supported. Users type the first characters of an item, and the
  39. /// first item that starts with what the user types will be selected.
  40. /// </para>
  41. /// </remarks>
  42. public class ListView : View, IDesignable
  43. {
  44. private bool _allowsMarking;
  45. private bool _allowsMultipleSelection = false;
  46. private int _lastSelectedItem = -1;
  47. private int _selected = -1;
  48. private IListDataSource _source;
  49. // TODO: ListView has been upgraded to use Viewport and ContentSize instead of the
  50. // TODO: bespoke _top and _left. It was a quick & dirty port. There is now duplicate logic
  51. // TODO: that could be removed.
  52. //private int _top, _left;
  53. /// <summary>
  54. /// Initializes a new instance of <see cref="ListView"/>. Set the <see cref="Source"/> property to display
  55. /// something.
  56. /// </summary>
  57. public ListView ()
  58. {
  59. CanFocus = true;
  60. // Things this view knows how to do
  61. //
  62. AddCommand (Command.Up, (ctx) =>
  63. {
  64. if (RaiseSelecting (ctx) == true)
  65. {
  66. return true;
  67. }
  68. return MoveUp ();
  69. });
  70. AddCommand (Command.Down, (ctx) =>
  71. {
  72. if (RaiseSelecting (ctx) == true)
  73. {
  74. return true;
  75. }
  76. return MoveDown ();
  77. });
  78. // TODO: add RaiseSelecting to all of these
  79. AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
  80. AddCommand (Command.ScrollDown, () => ScrollVertical (1));
  81. AddCommand (Command.PageUp, () => MovePageUp ());
  82. AddCommand (Command.PageDown, () => MovePageDown ());
  83. AddCommand (Command.Start, () => MoveHome ());
  84. AddCommand (Command.End, () => MoveEnd ());
  85. AddCommand (Command.ScrollLeft, () => ScrollHorizontal (-1));
  86. AddCommand (Command.ScrollRight, () => ScrollHorizontal (1));
  87. // Accept (Enter key) - Raise Accept event - DO NOT advance state
  88. AddCommand (Command.Accept, (ctx) =>
  89. {
  90. if (RaiseAccepting (ctx) == true)
  91. {
  92. return true;
  93. }
  94. if (OnOpenSelectedItem ())
  95. {
  96. return true;
  97. }
  98. return false;
  99. });
  100. // Select (Space key and single-click) - If markable, change mark and raise Select event
  101. AddCommand (Command.Select, (ctx) =>
  102. {
  103. if (_allowsMarking)
  104. {
  105. if (RaiseSelecting (ctx) == true)
  106. {
  107. return true;
  108. }
  109. if (MarkUnmarkSelectedItem ())
  110. {
  111. return true;
  112. }
  113. }
  114. return false;
  115. });
  116. // Hotkey - If none set, select and raise Select event. SetFocus. - DO NOT raise Accept
  117. AddCommand (Command.HotKey, (ctx) =>
  118. {
  119. if (SelectedItem == -1)
  120. {
  121. SelectedItem = 0;
  122. if (RaiseSelecting (ctx) == true)
  123. {
  124. return true;
  125. }
  126. }
  127. return !SetFocus ();
  128. });
  129. AddCommand (Command.SelectAll, (ctx) =>
  130. {
  131. if (ctx is not CommandContext<KeyBinding> keyCommandContext)
  132. {
  133. return false;
  134. }
  135. return keyCommandContext.Binding.Data is { } && MarkAll ((bool)keyCommandContext.Binding.Data);
  136. });
  137. // Default keybindings for all ListViews
  138. KeyBindings.Add (Key.CursorUp, Command.Up);
  139. KeyBindings.Add (Key.P.WithCtrl, Command.Up);
  140. KeyBindings.Add (Key.CursorDown, Command.Down);
  141. KeyBindings.Add (Key.N.WithCtrl, Command.Down);
  142. KeyBindings.Add (Key.PageUp, Command.PageUp);
  143. KeyBindings.Add (Key.PageDown, Command.PageDown);
  144. KeyBindings.Add (Key.V.WithCtrl, Command.PageDown);
  145. KeyBindings.Add (Key.Home, Command.Start);
  146. KeyBindings.Add (Key.End, Command.End);
  147. // Key.Space is already bound to Command.Select; this gives us select then move down
  148. KeyBindings.Add (Key.Space.WithShift, [Command.Select, Command.Down]);
  149. // Use the form of Add that lets us pass context to the handler
  150. KeyBindings.Add (Key.A.WithCtrl, new KeyBinding ([Command.SelectAll], true));
  151. KeyBindings.Add (Key.U.WithCtrl, new KeyBinding ([Command.SelectAll], false));
  152. }
  153. /// <inheritdoc />
  154. protected override void OnViewportChanged (DrawEventArgs e)
  155. {
  156. SetContentSize (new Size (MaxLength, _source?.Count ?? Viewport.Height));
  157. }
  158. /// <inheritdoc />
  159. protected override void OnFrameChanged (in Rectangle frame)
  160. {
  161. EnsureSelectedItemVisible ();
  162. }
  163. /// <summary>Gets or sets whether this <see cref="ListView"/> allows items to be marked.</summary>
  164. /// <value>Set to <see langword="true"/> to allow marking elements of the list.</value>
  165. /// <remarks>
  166. /// If set to <see langword="true"/>, <see cref="ListView"/> will render items marked items with "[x]", and
  167. /// unmarked items with "[ ]". SPACE key will toggle marking. The default is <see langword="false"/>.
  168. /// </remarks>
  169. public bool AllowsMarking
  170. {
  171. get => _allowsMarking;
  172. set
  173. {
  174. _allowsMarking = value;
  175. SetNeedsDraw ();
  176. }
  177. }
  178. /// <summary>
  179. /// If set to <see langword="true"/> more than one item can be selected. If <see langword="false"/> selecting an
  180. /// item will cause all others to be un-selected. The default is <see langword="false"/>.
  181. /// </summary>
  182. public bool AllowsMultipleSelection
  183. {
  184. get => _allowsMultipleSelection;
  185. set
  186. {
  187. _allowsMultipleSelection = value;
  188. if (Source is { } && !_allowsMultipleSelection)
  189. {
  190. // Clear all selections except selected
  191. for (var i = 0; i < Source.Count; i++)
  192. {
  193. if (Source.IsMarked (i) && i != _selected)
  194. {
  195. Source.SetMark (i, false);
  196. }
  197. }
  198. }
  199. SetNeedsDraw ();
  200. }
  201. }
  202. /// <summary>
  203. /// Gets the <see cref="CollectionNavigator"/> that searches the <see cref="ListView.Source"/> collection as the
  204. /// user types.
  205. /// </summary>
  206. public IListCollectionNavigator KeystrokeNavigator { get; } = new CollectionNavigator();
  207. /// <summary>Gets or sets the leftmost column that is currently visible (when scrolling horizontally).</summary>
  208. /// <value>The left position.</value>
  209. public int LeftItem
  210. {
  211. get => Viewport.X;
  212. set
  213. {
  214. if (_source is null)
  215. {
  216. return;
  217. }
  218. if (value < 0 || (MaxLength > 0 && value >= MaxLength))
  219. {
  220. throw new ArgumentException ("value");
  221. }
  222. Viewport = Viewport with { X = value };
  223. SetNeedsDraw ();
  224. }
  225. }
  226. /// <summary>Gets the widest item in the list.</summary>
  227. public int MaxLength => _source?.Length ?? 0;
  228. /// <summary>Gets or sets the index of the currently selected item.</summary>
  229. /// <value>The selected item.</value>
  230. public int SelectedItem
  231. {
  232. get => _selected;
  233. set
  234. {
  235. if (_source is null || _source.Count == 0)
  236. {
  237. return;
  238. }
  239. if (value < -1 || value >= _source.Count)
  240. {
  241. throw new ArgumentException ("value");
  242. }
  243. _selected = value;
  244. OnSelectedChanged ();
  245. }
  246. }
  247. /// <summary>Gets or sets the <see cref="IListDataSource"/> backing this <see cref="ListView"/>, enabling custom rendering.</summary>
  248. /// <value>The source.</value>
  249. /// <remarks>Use <see cref="SetSource{T}"/> to set a new <see cref="IList"/> source.</remarks>
  250. public IListDataSource Source
  251. {
  252. get => _source;
  253. set
  254. {
  255. if (_source == value)
  256. {
  257. return;
  258. }
  259. _source?.Dispose ();
  260. _source = value;
  261. if (_source is { })
  262. {
  263. _source.CollectionChanged += Source_CollectionChanged;
  264. }
  265. SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width));
  266. if (IsInitialized)
  267. {
  268. // Viewport = Viewport with { Y = 0 };
  269. }
  270. KeystrokeNavigator.Collection = _source?.ToList ();
  271. _selected = -1;
  272. _lastSelectedItem = -1;
  273. SetNeedsDraw ();
  274. }
  275. }
  276. private void Source_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e)
  277. {
  278. SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width));
  279. if (Source is { Count: > 0 } && _selected > Source.Count - 1)
  280. {
  281. SelectedItem = Source.Count - 1;
  282. }
  283. SetNeedsDraw ();
  284. OnCollectionChanged (e);
  285. }
  286. /// <summary>Gets or sets the index of the item that will appear at the top of the <see cref="View.Viewport"/>.</summary>
  287. /// <remarks>
  288. /// This a helper property for accessing <c>listView.Viewport.Y</c>.
  289. /// </remarks>
  290. /// <value>The top item.</value>
  291. public int TopItem
  292. {
  293. get => Viewport.Y;
  294. set
  295. {
  296. if (_source is null)
  297. {
  298. return;
  299. }
  300. Viewport = Viewport with { Y = value };
  301. }
  302. }
  303. /// <summary>
  304. /// If <see cref="AllowsMarking"/> and <see cref="AllowsMultipleSelection"/> are both <see langword="true"/>,
  305. /// marks all items.
  306. /// </summary>
  307. /// <param name="mark"><see langword="true"/> marks all items; otherwise unmarks all items.</param>
  308. /// <returns><see langword="true"/> if marking was successful.</returns>
  309. public bool MarkAll (bool mark)
  310. {
  311. if (!_allowsMarking)
  312. {
  313. return false;
  314. }
  315. if (AllowsMultipleSelection)
  316. {
  317. for (var i = 0; i < Source.Count; i++)
  318. {
  319. Source.SetMark (i, mark);
  320. }
  321. return true;
  322. }
  323. return false;
  324. }
  325. /// <summary>
  326. /// If <see cref="AllowsMarking"/> and <see cref="AllowsMultipleSelection"/> are both <see langword="true"/>,
  327. /// unmarks all marked items other than <see cref="SelectedItem"/>.
  328. /// </summary>
  329. /// <returns><see langword="true"/> if unmarking was successful.</returns>
  330. public bool UnmarkAllButSelected ()
  331. {
  332. if (!_allowsMarking)
  333. {
  334. return false;
  335. }
  336. if (!AllowsMultipleSelection)
  337. {
  338. for (var i = 0; i < Source.Count; i++)
  339. {
  340. if (Source.IsMarked (i) && i != _selected)
  341. {
  342. Source.SetMark (i, false);
  343. return true;
  344. }
  345. }
  346. }
  347. return true;
  348. }
  349. /// <summary>Ensures the selected item is always visible on the screen.</summary>
  350. public void EnsureSelectedItemVisible ()
  351. {
  352. if (_selected == -1)
  353. {
  354. return;
  355. }
  356. if (_selected < Viewport.Y)
  357. {
  358. Viewport = Viewport with { Y = _selected };
  359. }
  360. else if (Viewport.Height > 0 && _selected >= Viewport.Y + Viewport.Height)
  361. {
  362. Viewport = Viewport with { Y = _selected - Viewport.Height + 1 };
  363. }
  364. }
  365. /// <summary>Marks the <see cref="SelectedItem"/> if it is not already marked.</summary>
  366. /// <returns><see langword="true"/> if the <see cref="SelectedItem"/> was marked.</returns>
  367. public bool MarkUnmarkSelectedItem ()
  368. {
  369. if (UnmarkAllButSelected ())
  370. {
  371. Source.SetMark (SelectedItem, !Source.IsMarked (SelectedItem));
  372. SetNeedsDraw ();
  373. return Source.IsMarked (SelectedItem);
  374. }
  375. // BUGBUG: Shouldn't this return Source.IsMarked (SelectedItem)
  376. return false;
  377. }
  378. /// <inheritdoc/>
  379. protected override bool OnMouseEvent (MouseEventArgs me)
  380. {
  381. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)
  382. && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
  383. && me.Flags != MouseFlags.WheeledDown
  384. && me.Flags != MouseFlags.WheeledUp
  385. && me.Flags != MouseFlags.WheeledRight
  386. && me.Flags != MouseFlags.WheeledLeft)
  387. {
  388. return false;
  389. }
  390. if (!HasFocus && CanFocus)
  391. {
  392. SetFocus ();
  393. }
  394. if (_source is null)
  395. {
  396. return false;
  397. }
  398. if (me.Flags == MouseFlags.WheeledDown)
  399. {
  400. if (Viewport.Y + Viewport.Height < GetContentSize ().Height)
  401. {
  402. ScrollVertical (1);
  403. }
  404. return true;
  405. }
  406. if (me.Flags == MouseFlags.WheeledUp)
  407. {
  408. ScrollVertical (-1);
  409. return true;
  410. }
  411. if (me.Flags == MouseFlags.WheeledRight)
  412. {
  413. if (Viewport.X + Viewport.Width < GetContentSize ().Width)
  414. {
  415. ScrollHorizontal (1);
  416. }
  417. return true;
  418. }
  419. if (me.Flags == MouseFlags.WheeledLeft)
  420. {
  421. ScrollHorizontal (-1);
  422. return true;
  423. }
  424. if (me.Position.Y + Viewport.Y >= _source.Count
  425. || me.Position.Y + Viewport.Y < 0
  426. || me.Position.Y + Viewport.Y > Viewport.Y + Viewport.Height)
  427. {
  428. return true;
  429. }
  430. _selected = Viewport.Y + me.Position.Y;
  431. if (MarkUnmarkSelectedItem ())
  432. {
  433. // return true;
  434. }
  435. OnSelectedChanged ();
  436. SetNeedsDraw ();
  437. if (me.Flags == MouseFlags.Button1DoubleClicked)
  438. {
  439. return InvokeCommand (Command.Accept) is true;
  440. }
  441. return true;
  442. }
  443. /// <summary>Changes the <see cref="SelectedItem"/> to the next item in the list, scrolling the list if needed.</summary>
  444. /// <returns></returns>
  445. public virtual bool MoveDown ()
  446. {
  447. if (_source is null || _source.Count == 0)
  448. {
  449. // Do we set lastSelectedItem to -1 here?
  450. return false; //Nothing for us to move to
  451. }
  452. if (_selected >= _source.Count)
  453. {
  454. // If for some reason we are currently outside of the
  455. // valid values range, we should select the bottommost valid value.
  456. // This can occur if the backing data source changes.
  457. _selected = _source.Count - 1;
  458. OnSelectedChanged ();
  459. SetNeedsDraw ();
  460. }
  461. else if (_selected + 1 < _source.Count)
  462. {
  463. //can move by down by one.
  464. _selected++;
  465. if (_selected >= Viewport.Y + Viewport.Height)
  466. {
  467. Viewport = Viewport with { Y = Viewport.Y + 1 };
  468. }
  469. else if (_selected < Viewport.Y)
  470. {
  471. Viewport = Viewport with { Y = _selected };
  472. }
  473. OnSelectedChanged ();
  474. SetNeedsDraw ();
  475. }
  476. else if (_selected == 0)
  477. {
  478. OnSelectedChanged ();
  479. SetNeedsDraw ();
  480. }
  481. else if (_selected >= Viewport.Y + Viewport.Height)
  482. {
  483. Viewport = Viewport with { Y = _source.Count - Viewport.Height };
  484. SetNeedsDraw ();
  485. }
  486. return true;
  487. }
  488. /// <summary>Changes the <see cref="SelectedItem"/> to last item in the list, scrolling the list if needed.</summary>
  489. /// <returns></returns>
  490. public virtual bool MoveEnd ()
  491. {
  492. if (_source is { Count: > 0 } && _selected != _source.Count - 1)
  493. {
  494. _selected = _source.Count - 1;
  495. if (Viewport.Y + _selected > Viewport.Height - 1)
  496. {
  497. Viewport = Viewport with
  498. {
  499. Y = _selected < Viewport.Height - 1
  500. ? Math.Max (Viewport.Height - _selected + 1, 0)
  501. : Math.Max (_selected - Viewport.Height + 1, 0)
  502. };
  503. }
  504. OnSelectedChanged ();
  505. SetNeedsDraw ();
  506. }
  507. return true;
  508. }
  509. /// <summary>Changes the <see cref="SelectedItem"/> to the first item in the list, scrolling the list if needed.</summary>
  510. /// <returns></returns>
  511. public virtual bool MoveHome ()
  512. {
  513. if (_selected != 0)
  514. {
  515. _selected = 0;
  516. Viewport = Viewport with { Y = _selected };
  517. OnSelectedChanged ();
  518. SetNeedsDraw ();
  519. }
  520. return true;
  521. }
  522. /// <summary>
  523. /// Changes the <see cref="SelectedItem"/> to the item just below the bottom of the visible list, scrolling if
  524. /// needed.
  525. /// </summary>
  526. /// <returns></returns>
  527. public virtual bool MovePageDown ()
  528. {
  529. if (_source is null)
  530. {
  531. return true;
  532. }
  533. int n = _selected + Viewport.Height;
  534. if (n >= _source.Count)
  535. {
  536. n = _source.Count - 1;
  537. }
  538. if (n != _selected)
  539. {
  540. _selected = n;
  541. if (_source.Count >= Viewport.Height)
  542. {
  543. Viewport = Viewport with { Y = _selected };
  544. }
  545. else
  546. {
  547. Viewport = Viewport with { Y = 0 };
  548. }
  549. OnSelectedChanged ();
  550. SetNeedsDraw ();
  551. }
  552. return true;
  553. }
  554. /// <summary>Changes the <see cref="SelectedItem"/> to the item at the top of the visible list.</summary>
  555. /// <returns></returns>
  556. public virtual bool MovePageUp ()
  557. {
  558. int n = _selected - Viewport.Height;
  559. if (n < 0)
  560. {
  561. n = 0;
  562. }
  563. if (n != _selected)
  564. {
  565. _selected = n;
  566. Viewport = Viewport with { Y = _selected };
  567. OnSelectedChanged ();
  568. SetNeedsDraw ();
  569. }
  570. return true;
  571. }
  572. /// <summary>Changes the <see cref="SelectedItem"/> to the previous item in the list, scrolling the list if needed.</summary>
  573. /// <returns></returns>
  574. public virtual bool MoveUp ()
  575. {
  576. if (_source is null || _source.Count == 0)
  577. {
  578. // Do we set lastSelectedItem to -1 here?
  579. return false; //Nothing for us to move to
  580. }
  581. if (_selected >= _source.Count)
  582. {
  583. // If for some reason we are currently outside of the
  584. // valid values range, we should select the bottommost valid value.
  585. // This can occur if the backing data source changes.
  586. _selected = _source.Count - 1;
  587. OnSelectedChanged ();
  588. SetNeedsDraw ();
  589. }
  590. else if (_selected > 0)
  591. {
  592. _selected--;
  593. if (_selected > Source.Count)
  594. {
  595. _selected = Source.Count - 1;
  596. }
  597. if (_selected < Viewport.Y)
  598. {
  599. Viewport = Viewport with { Y = _selected };
  600. }
  601. else if (_selected > Viewport.Y + Viewport.Height)
  602. {
  603. Viewport = Viewport with { Y = _selected - Viewport.Height + 1 };
  604. }
  605. OnSelectedChanged ();
  606. SetNeedsDraw ();
  607. }
  608. else if (_selected < Viewport.Y)
  609. {
  610. Viewport = Viewport with { Y = _selected };
  611. SetNeedsDraw ();
  612. }
  613. return true;
  614. }
  615. /// <inheritdoc/>
  616. protected override bool OnDrawingContent ()
  617. {
  618. Attribute current = Attribute.Default;
  619. Move (0, 0);
  620. Rectangle f = Viewport;
  621. int item = Viewport.Y;
  622. bool focused = HasFocus;
  623. int col = _allowsMarking ? 2 : 0;
  624. int start = Viewport.X;
  625. for (var row = 0; row < f.Height; row++, item++)
  626. {
  627. bool isSelected = item == _selected;
  628. Attribute newAttribute = focused ? isSelected ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal) :
  629. isSelected ? GetAttributeForRole (VisualRole.Active) : GetAttributeForRole (VisualRole.Normal);
  630. if (newAttribute != current)
  631. {
  632. SetAttribute (newAttribute);
  633. current = newAttribute;
  634. }
  635. Move (0, row);
  636. if (_source is null || item >= _source.Count)
  637. {
  638. for (var c = 0; c < f.Width; c++)
  639. {
  640. AddRune ((Rune)' ');
  641. }
  642. }
  643. else
  644. {
  645. var rowEventArgs = new ListViewRowEventArgs (item);
  646. OnRowRender (rowEventArgs);
  647. if (rowEventArgs.RowAttribute is { } && current != rowEventArgs.RowAttribute)
  648. {
  649. current = (Attribute)rowEventArgs.RowAttribute;
  650. SetAttribute (current);
  651. }
  652. if (_allowsMarking)
  653. {
  654. AddRune (
  655. _source.IsMarked (item) ? AllowsMultipleSelection ? Glyphs.CheckStateChecked : Glyphs.Selected :
  656. AllowsMultipleSelection ? Glyphs.CheckStateUnChecked : Glyphs.UnSelected
  657. );
  658. AddRune ((Rune)' ');
  659. }
  660. Source.Render (this, isSelected, item, col, row, f.Width - col, start);
  661. }
  662. }
  663. return true;
  664. }
  665. /// <inheritdoc/>
  666. protected override void OnHasFocusChanged (bool newHasFocus, [CanBeNull] View currentFocused, [CanBeNull] View newFocused)
  667. {
  668. if (newHasFocus && _lastSelectedItem != _selected)
  669. {
  670. EnsureSelectedItemVisible ();
  671. }
  672. }
  673. /// <summary>Invokes the <see cref="OpenSelectedItem"/> event if it is defined.</summary>
  674. /// <returns><see langword="true"/> if the <see cref="OpenSelectedItem"/> event was fired.</returns>
  675. public bool OnOpenSelectedItem ()
  676. {
  677. if (_source is null || _source.Count <= _selected || _selected < 0 || OpenSelectedItem is null)
  678. {
  679. return false;
  680. }
  681. object value = _source.ToList () [_selected];
  682. OpenSelectedItem?.Invoke (this, new ListViewItemEventArgs (_selected, value));
  683. // BUGBUG: this should not blindly return true.
  684. return true;
  685. }
  686. /// <inheritdoc/>
  687. protected override bool OnKeyDown (Key key)
  688. {
  689. // If the key was bound to key command, let normal KeyDown processing happen. This enables overriding the default handling.
  690. // See: https://github.com/gui-cs/Terminal.Gui/issues/3950#issuecomment-2807350939
  691. if (KeyBindings.TryGet (key, out _))
  692. {
  693. return false;
  694. }
  695. // Enable user to find & select an item by typing text
  696. if (KeystrokeNavigator.Matcher.IsCompatibleKey (key))
  697. {
  698. int? newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem, (char)key);
  699. if (newItem is { } && newItem != -1)
  700. {
  701. SelectedItem = (int)newItem;
  702. EnsureSelectedItemVisible ();
  703. SetNeedsDraw ();
  704. return true;
  705. }
  706. }
  707. return false;
  708. }
  709. /// <summary>Virtual method that will invoke the <see cref="RowRender"/>.</summary>
  710. /// <param name="rowEventArgs"></param>
  711. public virtual void OnRowRender (ListViewRowEventArgs rowEventArgs) { RowRender?.Invoke (this, rowEventArgs); }
  712. // TODO: Use standard event model
  713. /// <summary>Invokes the <see cref="SelectedItemChanged"/> event if it is defined.</summary>
  714. /// <returns></returns>
  715. public virtual bool OnSelectedChanged ()
  716. {
  717. if (_selected != _lastSelectedItem)
  718. {
  719. object value = _source?.Count > 0 ? _source.ToList () [_selected] : null;
  720. SelectedItemChanged?.Invoke (this, new ListViewItemEventArgs (_selected, value));
  721. _lastSelectedItem = _selected;
  722. EnsureSelectedItemVisible ();
  723. return true;
  724. }
  725. return false;
  726. }
  727. /// <summary>This event is raised when the user Double-Clicks on an item or presses ENTER to open the selected item.</summary>
  728. public event EventHandler<ListViewItemEventArgs> OpenSelectedItem;
  729. /// <summary>This event is invoked when this <see cref="ListView"/> is being drawn before rendering.</summary>
  730. public event EventHandler<ListViewRowEventArgs> RowRender;
  731. /// <summary>This event is raised when the selected item in the <see cref="ListView"/> has changed.</summary>
  732. public event EventHandler<ListViewItemEventArgs> SelectedItemChanged;
  733. /// <summary>
  734. /// Event to raise when an item is added, removed, or moved, or the entire list is refreshed.
  735. /// </summary>
  736. public event NotifyCollectionChangedEventHandler CollectionChanged;
  737. /// <summary>Sets the source of the <see cref="ListView"/> to an <see cref="IList"/>.</summary>
  738. /// <value>An object implementing the IList interface.</value>
  739. /// <remarks>
  740. /// Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custom
  741. /// rendering.
  742. /// </remarks>
  743. public void SetSource<T> (ObservableCollection<T> source)
  744. {
  745. if (source is null && Source is not ListWrapper<T>)
  746. {
  747. Source = null;
  748. }
  749. else
  750. {
  751. Source = new ListWrapper<T> (source);
  752. }
  753. }
  754. /// <summary>Sets the source to an <see cref="IList"/> value asynchronously.</summary>
  755. /// <value>An item implementing the IList interface.</value>
  756. /// <remarks>
  757. /// Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custom
  758. /// rendering.
  759. /// </remarks>
  760. public Task SetSourceAsync<T> (ObservableCollection<T> source)
  761. {
  762. return Task.Factory.StartNew (
  763. () =>
  764. {
  765. if (source is null && (Source is null || !(Source is ListWrapper<T>)))
  766. {
  767. Source = null;
  768. }
  769. else
  770. {
  771. Source = new ListWrapper<T> (source);
  772. }
  773. return source;
  774. },
  775. CancellationToken.None,
  776. TaskCreationOptions.DenyChildAttach,
  777. TaskScheduler.Default
  778. );
  779. }
  780. private void ListView_LayoutStarted (object sender, LayoutEventArgs e) { EnsureSelectedItemVisible (); }
  781. /// <summary>
  782. /// Call the event to raises the <see cref="CollectionChanged"/>.
  783. /// </summary>
  784. /// <param name="e"></param>
  785. protected virtual void OnCollectionChanged (NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke (this, e); }
  786. /// <inheritdoc />
  787. protected override void Dispose (bool disposing)
  788. {
  789. _source?.Dispose ();
  790. base.Dispose (disposing);
  791. }
  792. /// <summary>
  793. /// Allow suspending the <see cref="CollectionChanged"/> event from being invoked,
  794. /// </summary>
  795. public void SuspendCollectionChangedEvent ()
  796. {
  797. if (Source is { })
  798. {
  799. Source.SuspendCollectionChangedEvent = true;
  800. }
  801. }
  802. /// <summary>
  803. /// Allow resume the <see cref="CollectionChanged"/> event from being invoked,
  804. /// </summary>
  805. public void ResumeSuspendCollectionChangedEvent ()
  806. {
  807. if (Source is { })
  808. {
  809. Source.SuspendCollectionChangedEvent = false;
  810. }
  811. }
  812. /// <inheritdoc />
  813. public bool EnableForDesign ()
  814. {
  815. var source = new ListWrapper<string> (["List Item 1", "List Item two", "List Item Quattro", "Last List Item"]);
  816. Source = source;
  817. return true;
  818. }
  819. }
  820. /// <summary>
  821. /// Provides a default implementation of <see cref="IListDataSource"/> that renders <see cref="ListView"/> items
  822. /// using <see cref="object.ToString()"/>.
  823. /// </summary>
  824. public class ListWrapper<T> : IListDataSource, IDisposable
  825. {
  826. private int _count;
  827. private BitArray _marks;
  828. private readonly ObservableCollection<T> _source;
  829. /// <inheritdoc/>
  830. public ListWrapper (ObservableCollection<T> source)
  831. {
  832. if (source is { })
  833. {
  834. _count = source.Count;
  835. _marks = new BitArray (_count);
  836. _source = source;
  837. _source.CollectionChanged += Source_CollectionChanged;
  838. Length = GetMaxLengthItem ();
  839. }
  840. }
  841. private void Source_CollectionChanged (object sender, NotifyCollectionChangedEventArgs e)
  842. {
  843. if (!SuspendCollectionChangedEvent)
  844. {
  845. CheckAndResizeMarksIfRequired ();
  846. CollectionChanged?.Invoke (sender, e);
  847. }
  848. }
  849. /// <inheritdoc />
  850. public event NotifyCollectionChangedEventHandler CollectionChanged;
  851. /// <inheritdoc/>
  852. public int Count => _source?.Count ?? 0;
  853. /// <inheritdoc/>
  854. public int Length { get; private set; }
  855. private bool _suspendCollectionChangedEvent;
  856. /// <inheritdoc />
  857. public bool SuspendCollectionChangedEvent
  858. {
  859. get => _suspendCollectionChangedEvent;
  860. set
  861. {
  862. _suspendCollectionChangedEvent = value;
  863. if (!_suspendCollectionChangedEvent)
  864. {
  865. CheckAndResizeMarksIfRequired ();
  866. }
  867. }
  868. }
  869. private void CheckAndResizeMarksIfRequired ()
  870. {
  871. if (_source != null && _count != _source.Count)
  872. {
  873. _count = _source.Count;
  874. BitArray newMarks = new BitArray (_count);
  875. for (var i = 0; i < Math.Min (_marks.Length, newMarks.Length); i++)
  876. {
  877. newMarks [i] = _marks [i];
  878. }
  879. _marks = newMarks;
  880. Length = GetMaxLengthItem ();
  881. }
  882. }
  883. /// <inheritdoc/>
  884. public void Render (
  885. ListView container,
  886. bool marked,
  887. int item,
  888. int col,
  889. int line,
  890. int width,
  891. int start = 0
  892. )
  893. {
  894. container.Move (Math.Max (col - start, 0), line);
  895. if (_source is { })
  896. {
  897. object t = _source [item];
  898. if (t is null)
  899. {
  900. RenderUstr (container, "", col, line, width);
  901. }
  902. else
  903. {
  904. if (t is string s)
  905. {
  906. RenderUstr (container, s, col, line, width, start);
  907. }
  908. else
  909. {
  910. RenderUstr (container, t.ToString (), col, line, width, start);
  911. }
  912. }
  913. }
  914. }
  915. /// <inheritdoc/>
  916. public bool IsMarked (int item)
  917. {
  918. if (item >= 0 && item < _count)
  919. {
  920. return _marks [item];
  921. }
  922. return false;
  923. }
  924. /// <inheritdoc/>
  925. public void SetMark (int item, bool value)
  926. {
  927. if (item >= 0 && item < _count)
  928. {
  929. _marks [item] = value;
  930. }
  931. }
  932. /// <inheritdoc/>
  933. public IList ToList () { return _source; }
  934. /// <inheritdoc/>
  935. public int StartsWith (string search)
  936. {
  937. if (_source is null || _source?.Count == 0)
  938. {
  939. return -1;
  940. }
  941. for (var i = 0; i < _source.Count; i++)
  942. {
  943. object t = _source [i];
  944. if (t is string u)
  945. {
  946. if (u.ToUpper ().StartsWith (search.ToUpperInvariant ()))
  947. {
  948. return i;
  949. }
  950. }
  951. else if (t is string s)
  952. {
  953. if (s.StartsWith (search, StringComparison.InvariantCultureIgnoreCase))
  954. {
  955. return i;
  956. }
  957. }
  958. }
  959. return -1;
  960. }
  961. private int GetMaxLengthItem ()
  962. {
  963. if (_source is null || _source?.Count == 0)
  964. {
  965. return 0;
  966. }
  967. var maxLength = 0;
  968. for (var i = 0; i < _source!.Count; i++)
  969. {
  970. object t = _source [i];
  971. int l;
  972. if (t is string u)
  973. {
  974. l = u.GetColumns ();
  975. }
  976. else if (t is string s)
  977. {
  978. l = s.Length;
  979. }
  980. else
  981. {
  982. l = t.ToString ().Length;
  983. }
  984. if (l > maxLength)
  985. {
  986. maxLength = l;
  987. }
  988. }
  989. return maxLength;
  990. }
  991. private void RenderUstr (View driver, string ustr, int col, int line, int width, int start = 0)
  992. {
  993. string str = start > ustr.GetColumns () ? string.Empty : ustr.Substring (Math.Min (start, ustr.ToRunes ().Length - 1));
  994. string u = TextFormatter.ClipAndJustify (str, width, Alignment.Start);
  995. driver.AddStr (u);
  996. width -= u.GetColumns ();
  997. while (width-- > 0)
  998. {
  999. driver.AddRune ((Rune)' ');
  1000. }
  1001. }
  1002. /// <inheritdoc />
  1003. public void Dispose ()
  1004. {
  1005. if (_source is { })
  1006. {
  1007. _source.CollectionChanged -= Source_CollectionChanged;
  1008. }
  1009. }
  1010. }