ListView.cs 30 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925
  1. #nullable enable
  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).
  19. /// Alternatively, an
  20. /// object that implements <see cref="IListDataSource"/> can be provided giving full control of what is rendered.
  21. /// </para>
  22. /// <para>
  23. /// <see cref="ListView"/> can display any object that implements the <see cref="IList"/> interface.
  24. /// <see cref="string"/> values are converted into <see cref="string"/> values before rendering, and other values
  25. /// are converted into <see cref="string"/> by calling <see cref="object.ToString"/> and then converting to
  26. /// <see cref="string"/> .
  27. /// </para>
  28. /// <para>
  29. /// To change the contents of the ListView, set the <see cref="Source"/> property (when providing custom
  30. /// rendering via <see cref="IListDataSource"/>) or call <see cref="SetSource{T}"/> an <see cref="IList"/> is being
  31. /// used.
  32. /// </para>
  33. /// <para>
  34. /// When <see cref="AllowsMarking"/> is set to true the rendering will prefix the rendered items with [x] or [ ]
  35. /// and bind the SPACE key to toggle the selection. To implement a different marking style set
  36. /// <see cref="AllowsMarking"/> to false and implement custom rendering.
  37. /// </para>
  38. /// <para>
  39. /// Searching the ListView with the keyboard is supported. Users type the first characters of an item, and the
  40. /// first item that starts with what the user types will be selected.
  41. /// </para>
  42. /// </remarks>
  43. public class ListView : View, IDesignable
  44. {
  45. // TODO: ListView has been upgraded to use Viewport and ContentSize instead of the
  46. // TODO: bespoke _top and _left. It was a quick & dirty port. There is now duplicate logic
  47. // TODO: that could be removed.
  48. //private int _top, _left;
  49. /// <summary>
  50. /// Initializes a new instance of <see cref="ListView"/>. Set the <see cref="Source"/> property to display
  51. /// something.
  52. /// </summary>
  53. public ListView ()
  54. {
  55. CanFocus = true;
  56. // Things this view knows how to do
  57. //
  58. AddCommand (Command.Up, ctx => RaiseSelecting (ctx) == true || MoveUp ());
  59. AddCommand (Command.Down, ctx => RaiseSelecting (ctx) == true || MoveDown ());
  60. // TODO: add RaiseSelecting to all of these
  61. AddCommand (Command.ScrollUp, () => ScrollVertical (-1));
  62. AddCommand (Command.ScrollDown, () => ScrollVertical (1));
  63. AddCommand (Command.PageUp, () => MovePageUp ());
  64. AddCommand (Command.PageDown, () => MovePageDown ());
  65. AddCommand (Command.Start, () => MoveHome ());
  66. AddCommand (Command.End, () => MoveEnd ());
  67. AddCommand (Command.ScrollLeft, () => ScrollHorizontal (-1));
  68. AddCommand (Command.ScrollRight, () => ScrollHorizontal (1));
  69. // Accept (Enter key) - Raise Accept event - DO NOT advance state
  70. AddCommand (
  71. Command.Accept,
  72. ctx =>
  73. {
  74. if (RaiseAccepting (ctx) == true)
  75. {
  76. return true;
  77. }
  78. return OnOpenSelectedItem ();
  79. });
  80. // Select (Space key and single-click) - If markable, change mark and raise Select event
  81. AddCommand (
  82. Command.Select,
  83. ctx =>
  84. {
  85. if (!_allowsMarking)
  86. {
  87. return false;
  88. }
  89. if (RaiseSelecting (ctx) == true)
  90. {
  91. return true;
  92. }
  93. return MarkUnmarkSelectedItem ();
  94. });
  95. // Hotkey - If none set, select and raise Select event. SetFocus. - DO NOT raise Accept
  96. AddCommand (
  97. Command.HotKey,
  98. ctx =>
  99. {
  100. if (SelectedItem is { })
  101. {
  102. return !SetFocus ();
  103. }
  104. SelectedItem = 0;
  105. if (RaiseSelecting (ctx) == true)
  106. {
  107. return true;
  108. }
  109. return !SetFocus ();
  110. });
  111. AddCommand (
  112. Command.SelectAll,
  113. ctx =>
  114. {
  115. if (ctx is not CommandContext<KeyBinding> keyCommandContext)
  116. {
  117. return false;
  118. }
  119. return keyCommandContext.Binding.Data is { } && MarkAll ((bool)keyCommandContext.Binding.Data);
  120. });
  121. // Default keybindings for all ListViews
  122. KeyBindings.Add (Key.CursorUp, Command.Up);
  123. KeyBindings.Add (Key.P.WithCtrl, Command.Up);
  124. KeyBindings.Add (Key.CursorDown, Command.Down);
  125. KeyBindings.Add (Key.N.WithCtrl, Command.Down);
  126. KeyBindings.Add (Key.PageUp, Command.PageUp);
  127. KeyBindings.Add (Key.PageDown, Command.PageDown);
  128. KeyBindings.Add (Key.V.WithCtrl, Command.PageDown);
  129. KeyBindings.Add (Key.Home, Command.Start);
  130. KeyBindings.Add (Key.End, Command.End);
  131. // Key.Space is already bound to Command.Select; this gives us select then move down
  132. KeyBindings.Add (Key.Space.WithShift, Command.Select, Command.Down);
  133. // Use the form of Add that lets us pass context to the handler
  134. KeyBindings.Add (Key.A.WithCtrl, new KeyBinding ([Command.SelectAll], true));
  135. KeyBindings.Add (Key.U.WithCtrl, new KeyBinding ([Command.SelectAll], false));
  136. }
  137. private bool _allowsMarking;
  138. private bool _allowsMultipleSelection;
  139. private IListDataSource? _source;
  140. /// <inheritdoc/>
  141. public bool EnableForDesign ()
  142. {
  143. ListWrapper<string> source = new (["List Item 1", "List Item two", "List Item Quattro", "Last List Item"]);
  144. Source = source;
  145. return true;
  146. }
  147. /// <summary>Gets or sets whether this <see cref="ListView"/> allows items to be marked.</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
  151. /// unmarked items with "[ ]". SPACE key will toggle marking. The default is <see langword="false"/>.
  152. /// </remarks>
  153. public bool AllowsMarking
  154. {
  155. get => _allowsMarking;
  156. set
  157. {
  158. _allowsMarking = value;
  159. SetNeedsDraw ();
  160. }
  161. }
  162. /// <summary>
  163. /// If set to <see langword="true"/> more than one item can be selected. If <see langword="false"/> selecting an
  164. /// item will cause all others to be un-selected. The default is <see langword="false"/>.
  165. /// </summary>
  166. public bool AllowsMultipleSelection
  167. {
  168. get => _allowsMultipleSelection;
  169. set
  170. {
  171. _allowsMultipleSelection = value;
  172. if (Source is { } && !_allowsMultipleSelection)
  173. {
  174. // Clear all selections except selected
  175. for (var i = 0; i < Source.Count; i++)
  176. {
  177. if (Source.IsMarked (i) && SelectedItem.HasValue && i != SelectedItem.Value)
  178. {
  179. Source.SetMark (i, false);
  180. }
  181. }
  182. }
  183. SetNeedsDraw ();
  184. }
  185. }
  186. /// <summary>
  187. /// Event to raise when an item is added, removed, or moved, or the entire list is refreshed.
  188. /// </summary>
  189. public event NotifyCollectionChangedEventHandler? CollectionChanged;
  190. /// <summary>Ensures the selected item is always visible on the screen.</summary>
  191. public void EnsureSelectedItemVisible ()
  192. {
  193. if (SelectedItem is null)
  194. {
  195. return;
  196. }
  197. if (SelectedItem < Viewport.Y)
  198. {
  199. Viewport = Viewport with { Y = SelectedItem.Value };
  200. }
  201. else if (Viewport.Height > 0 && SelectedItem >= Viewport.Y + Viewport.Height)
  202. {
  203. Viewport = Viewport with { Y = SelectedItem.Value - Viewport.Height + 1 };
  204. }
  205. }
  206. /// <summary>
  207. /// Gets the <see cref="CollectionNavigator"/> that searches the <see cref="ListView.Source"/> collection as the
  208. /// user types.
  209. /// </summary>
  210. public IListCollectionNavigator KeystrokeNavigator { get; } = new CollectionNavigator ();
  211. /// <summary>Gets or sets the leftmost column that is currently visible (when scrolling horizontally).</summary>
  212. /// <value>The left position.</value>
  213. public int LeftItem
  214. {
  215. get => Viewport.X;
  216. set
  217. {
  218. if (Source is null)
  219. {
  220. return;
  221. }
  222. if (value < 0 || (MaxLength > 0 && value >= MaxLength))
  223. {
  224. throw new ArgumentException ("value");
  225. }
  226. Viewport = Viewport with { X = value };
  227. SetNeedsDraw ();
  228. }
  229. }
  230. /// <summary>
  231. /// If <see cref="AllowsMarking"/> and <see cref="AllowsMultipleSelection"/> are both <see langword="true"/>,
  232. /// marks all items.
  233. /// </summary>
  234. /// <param name="mark"><see langword="true"/> marks all items; otherwise unmarks all items.</param>
  235. /// <returns><see langword="true"/> if marking was successful.</returns>
  236. public bool MarkAll (bool mark)
  237. {
  238. if (!_allowsMarking)
  239. {
  240. return false;
  241. }
  242. if (AllowsMultipleSelection)
  243. {
  244. for (var i = 0; i < Source?.Count; i++)
  245. {
  246. Source.SetMark (i, mark);
  247. }
  248. return true;
  249. }
  250. return false;
  251. }
  252. /// <summary>Marks the <see cref="SelectedItem"/> if it is not already marked.</summary>
  253. /// <returns><see langword="true"/> if the <see cref="SelectedItem"/> was marked.</returns>
  254. public bool MarkUnmarkSelectedItem ()
  255. {
  256. if (Source is null || SelectedItem is null || !UnmarkAllButSelected ())
  257. {
  258. return false;
  259. }
  260. Source.SetMark (SelectedItem.Value, !Source.IsMarked (SelectedItem.Value));
  261. SetNeedsDraw ();
  262. return Source.IsMarked (SelectedItem.Value);
  263. }
  264. /// <summary>Gets the widest item in the list.</summary>
  265. public int MaxLength => Source?.Length ?? 0;
  266. /// <summary>Changes the <see cref="SelectedItem"/> to the next item in the list, scrolling the list if needed.</summary>
  267. /// <returns></returns>
  268. public virtual bool MoveDown ()
  269. {
  270. if (Source is null || Source.Count == 0)
  271. {
  272. return false; //Nothing for us to move to
  273. }
  274. if (SelectedItem is null || SelectedItem >= Source.Count)
  275. {
  276. // If SelectedItem is null or for some reason we are currently outside the
  277. // valid values range, we should select the first or bottommost valid value.
  278. // This can occur if the backing data source changes.
  279. SelectedItem = SelectedItem is null ? 0 : Source.Count - 1;
  280. }
  281. else if (SelectedItem + 1 < Source.Count)
  282. {
  283. //can move by down by one.
  284. SelectedItem++;
  285. if (SelectedItem >= Viewport.Y + Viewport.Height)
  286. {
  287. Viewport = Viewport with { Y = Viewport.Y + 1 };
  288. }
  289. else if (SelectedItem < Viewport.Y)
  290. {
  291. Viewport = Viewport with { Y = SelectedItem.Value };
  292. }
  293. }
  294. else if (SelectedItem >= Viewport.Y + Viewport.Height)
  295. {
  296. Viewport = Viewport with { Y = Source.Count - Viewport.Height };
  297. }
  298. return true;
  299. }
  300. /// <summary>Changes the <see cref="SelectedItem"/> to last item in the list, scrolling the list if needed.</summary>
  301. /// <returns></returns>
  302. public virtual bool MoveEnd ()
  303. {
  304. if (Source is { Count: > 0 } && SelectedItem != Source.Count - 1)
  305. {
  306. SelectedItem = Source.Count - 1;
  307. if (Viewport.Y + SelectedItem > Viewport.Height - 1)
  308. {
  309. Viewport = Viewport with
  310. {
  311. Y = SelectedItem < Viewport.Height - 1
  312. ? Math.Max (Viewport.Height - SelectedItem.Value + 1, 0)
  313. : Math.Max (SelectedItem.Value - Viewport.Height + 1, 0)
  314. };
  315. }
  316. }
  317. return true;
  318. }
  319. /// <summary>Changes the <see cref="SelectedItem"/> to the first item in the list, scrolling the list if needed.</summary>
  320. /// <returns></returns>
  321. public virtual bool MoveHome ()
  322. {
  323. if (SelectedItem != 0)
  324. {
  325. SelectedItem = 0;
  326. Viewport = Viewport with { Y = SelectedItem.Value };
  327. }
  328. return true;
  329. }
  330. /// <summary>
  331. /// Changes the <see cref="SelectedItem"/> to the item just below the bottom of the visible list, scrolling if
  332. /// needed.
  333. /// </summary>
  334. /// <returns></returns>
  335. public virtual bool MovePageDown ()
  336. {
  337. if (Source is null || Source.Count == 0)
  338. {
  339. return false;
  340. }
  341. int n = (SelectedItem ?? 0) + Viewport.Height;
  342. if (n >= Source.Count)
  343. {
  344. n = Source.Count - 1;
  345. }
  346. if (n != SelectedItem)
  347. {
  348. SelectedItem = n;
  349. if (Source.Count >= Viewport.Height)
  350. {
  351. Viewport = Viewport with { Y = SelectedItem.Value };
  352. }
  353. else
  354. {
  355. Viewport = Viewport with { Y = 0 };
  356. }
  357. }
  358. return true;
  359. }
  360. /// <summary>Changes the <see cref="SelectedItem"/> to the item at the top of the visible list.</summary>
  361. /// <returns></returns>
  362. public virtual bool MovePageUp ()
  363. {
  364. if (Source is null || Source.Count == 0)
  365. {
  366. return false;
  367. }
  368. int n = (SelectedItem ?? 0) - Viewport.Height;
  369. if (n < 0)
  370. {
  371. n = 0;
  372. }
  373. if (n != SelectedItem && n < Source?.Count)
  374. {
  375. SelectedItem = n;
  376. Viewport = Viewport with { Y = SelectedItem.Value };
  377. }
  378. return true;
  379. }
  380. /// <summary>Changes the <see cref="SelectedItem"/> to the previous item in the list, scrolling the list if needed.</summary>
  381. /// <returns></returns>
  382. public virtual bool MoveUp ()
  383. {
  384. if (Source is null || Source.Count == 0)
  385. {
  386. return false; //Nothing for us to move to
  387. }
  388. if (SelectedItem is null || SelectedItem >= Source.Count)
  389. {
  390. // If SelectedItem is null or for some reason we are currently outside the
  391. // valid values range, we should select the bottommost valid value.
  392. // This can occur if the backing data source changes.
  393. SelectedItem = Source.Count - 1;
  394. }
  395. else if (SelectedItem > 0)
  396. {
  397. SelectedItem--;
  398. if (SelectedItem > Source.Count)
  399. {
  400. SelectedItem = Source.Count - 1;
  401. }
  402. if (SelectedItem < Viewport.Y)
  403. {
  404. Viewport = Viewport with { Y = SelectedItem.Value };
  405. }
  406. else if (SelectedItem > Viewport.Y + Viewport.Height)
  407. {
  408. Viewport = Viewport with { Y = SelectedItem.Value - Viewport.Height + 1 };
  409. }
  410. }
  411. else if (SelectedItem < Viewport.Y)
  412. {
  413. Viewport = Viewport with { Y = SelectedItem.Value };
  414. }
  415. return true;
  416. }
  417. /// <summary>Invokes the <see cref="OpenSelectedItem"/> event if it is defined.</summary>
  418. /// <returns><see langword="true"/> if the <see cref="OpenSelectedItem"/> event was fired.</returns>
  419. public bool OnOpenSelectedItem ()
  420. {
  421. if (Source is null || SelectedItem is null || Source.Count <= SelectedItem || SelectedItem < 0 || OpenSelectedItem is null)
  422. {
  423. return false;
  424. }
  425. object? value = Source.ToList () [SelectedItem.Value];
  426. OpenSelectedItem?.Invoke (this, new (SelectedItem.Value, value!));
  427. // BUGBUG: this should not blindly return true.
  428. return true;
  429. }
  430. /// <summary>Virtual method that will invoke the <see cref="RowRender"/>.</summary>
  431. /// <param name="rowEventArgs"></param>
  432. public virtual void OnRowRender (ListViewRowEventArgs rowEventArgs) { RowRender?.Invoke (this, rowEventArgs); }
  433. /// <summary>This event is raised when the user Double-Clicks on an item or presses ENTER to open the selected item.</summary>
  434. public event EventHandler<ListViewItemEventArgs>? OpenSelectedItem;
  435. /// <summary>
  436. /// Allow resume the <see cref="CollectionChanged"/> event from being invoked,
  437. /// </summary>
  438. public void ResumeSuspendCollectionChangedEvent ()
  439. {
  440. if (Source is { })
  441. {
  442. Source.SuspendCollectionChangedEvent = false;
  443. }
  444. }
  445. /// <summary>This event is invoked when this <see cref="ListView"/> is being drawn before rendering.</summary>
  446. public event EventHandler<ListViewRowEventArgs>? RowRender;
  447. private int? _selectedItem = null;
  448. private int? _lastSelectedItem = null;
  449. /// <summary>Gets or sets the index of the currently selected item.</summary>
  450. /// <value>The selected item or null if no item is selected.</value>
  451. public int? SelectedItem
  452. {
  453. get => _selectedItem;
  454. set
  455. {
  456. if (Source is null)
  457. {
  458. return;
  459. }
  460. if (value.HasValue && (value < 0 || value >= Source.Count))
  461. {
  462. throw new ArgumentException (@"SelectedItem must be greater than 0 or less than the number of items.");
  463. }
  464. _selectedItem = value;
  465. OnSelectedChanged ();
  466. SetNeedsDraw ();
  467. }
  468. }
  469. // TODO: Use standard event model
  470. /// <summary>Invokes the <see cref="SelectedItemChanged"/> event if it is defined.</summary>
  471. /// <returns></returns>
  472. public virtual bool OnSelectedChanged ()
  473. {
  474. if (SelectedItem != _lastSelectedItem)
  475. {
  476. object? value = SelectedItem.HasValue && Source?.Count > 0 ? Source.ToList () [SelectedItem.Value] : null;
  477. SelectedItemChanged?.Invoke (this, new (SelectedItem, value));
  478. _lastSelectedItem = SelectedItem;
  479. EnsureSelectedItemVisible ();
  480. return true;
  481. }
  482. return false;
  483. }
  484. /// <summary>This event is raised when the selected item in the <see cref="ListView"/> has changed.</summary>
  485. public event EventHandler<ListViewItemEventArgs>? SelectedItemChanged;
  486. /// <summary>Sets the source of the <see cref="ListView"/> to an <see cref="IList"/>.</summary>
  487. /// <value>An object implementing the IList interface.</value>
  488. /// <remarks>
  489. /// Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custom
  490. /// rendering.
  491. /// </remarks>
  492. public void SetSource<T> (ObservableCollection<T>? source)
  493. {
  494. if (source is null && Source is not ListWrapper<T>)
  495. {
  496. Source = null;
  497. }
  498. else
  499. {
  500. Source = new ListWrapper<T> (source);
  501. }
  502. }
  503. /// <summary>Sets the source to an <see cref="IList"/> value asynchronously.</summary>
  504. /// <value>An item implementing the IList interface.</value>
  505. /// <remarks>
  506. /// Use the <see cref="Source"/> property to set a new <see cref="IListDataSource"/> source and use custom
  507. /// rendering.
  508. /// </remarks>
  509. public Task SetSourceAsync<T> (ObservableCollection<T>? source)
  510. {
  511. return Task.Factory.StartNew (
  512. () =>
  513. {
  514. if (source is null && Source is not ListWrapper<T>)
  515. {
  516. Source = null;
  517. }
  518. else
  519. {
  520. Source = new ListWrapper<T> (source);
  521. }
  522. return source;
  523. },
  524. CancellationToken.None,
  525. TaskCreationOptions.DenyChildAttach,
  526. TaskScheduler.Default
  527. );
  528. }
  529. /// <summary>Gets or sets the <see cref="IListDataSource"/> backing this <see cref="ListView"/>, enabling custom rendering.</summary>
  530. /// <value>The source.</value>
  531. /// <remarks>Use <see cref="SetSource{T}"/> to set a new <see cref="IList"/> source.</remarks>
  532. public IListDataSource? Source
  533. {
  534. get => _source;
  535. set
  536. {
  537. if (_source == value)
  538. {
  539. return;
  540. }
  541. _source?.Dispose ();
  542. _source = value;
  543. if (_source is { })
  544. {
  545. _source.CollectionChanged += Source_CollectionChanged;
  546. SetContentSize (new Size (_source?.Length ?? Viewport.Width, _source?.Count ?? Viewport.Width));
  547. KeystrokeNavigator.Collection = _source?.ToList ();
  548. }
  549. SelectedItem = null;
  550. _lastSelectedItem = null;
  551. SetNeedsDraw ();
  552. }
  553. }
  554. /// <summary>
  555. /// Allow suspending the <see cref="CollectionChanged"/> event from being invoked,
  556. /// </summary>
  557. public void SuspendCollectionChangedEvent ()
  558. {
  559. if (Source is { })
  560. {
  561. Source.SuspendCollectionChangedEvent = true;
  562. }
  563. }
  564. /// <summary>Gets or sets the index of the item that will appear at the top of the <see cref="View.Viewport"/>.</summary>
  565. /// <remarks>
  566. /// This a helper property for accessing <c>listView.Viewport.Y</c>.
  567. /// </remarks>
  568. /// <value>The top item.</value>
  569. public int TopItem
  570. {
  571. get => Viewport.Y;
  572. set
  573. {
  574. if (Source is null)
  575. {
  576. return;
  577. }
  578. Viewport = Viewport with { Y = value };
  579. }
  580. }
  581. /// <summary>
  582. /// If <see cref="AllowsMarking"/> and <see cref="AllowsMultipleSelection"/> are both <see langword="true"/>,
  583. /// unmarks all marked items other than <see cref="SelectedItem"/>.
  584. /// </summary>
  585. /// <returns><see langword="true"/> if unmarking was successful.</returns>
  586. public bool UnmarkAllButSelected ()
  587. {
  588. if (!_allowsMarking)
  589. {
  590. return false;
  591. }
  592. if (!AllowsMultipleSelection)
  593. {
  594. for (var i = 0; i < Source?.Count; i++)
  595. {
  596. if (Source.IsMarked (i) && i != SelectedItem)
  597. {
  598. Source.SetMark (i, false);
  599. return true;
  600. }
  601. }
  602. }
  603. return true;
  604. }
  605. /// <inheritdoc/>
  606. protected override void Dispose (bool disposing)
  607. {
  608. Source?.Dispose ();
  609. base.Dispose (disposing);
  610. }
  611. /// <summary>
  612. /// Call the event to raises the <see cref="CollectionChanged"/>.
  613. /// </summary>
  614. /// <param name="e"></param>
  615. protected virtual void OnCollectionChanged (NotifyCollectionChangedEventArgs e) { CollectionChanged?.Invoke (this, e); }
  616. /// <inheritdoc/>
  617. protected override bool OnDrawingContent (DrawContext? context)
  618. {
  619. if (Source is null)
  620. {
  621. return base.OnDrawingContent (context);
  622. }
  623. var current = Attribute.Default;
  624. Move (0, 0);
  625. Rectangle f = Viewport;
  626. int item = Viewport.Y;
  627. bool focused = HasFocus;
  628. int col = _allowsMarking ? 2 : 0;
  629. int start = Viewport.X;
  630. for (var row = 0; row < f.Height; row++, item++)
  631. {
  632. bool isSelected = item == SelectedItem;
  633. Attribute newAttribute = focused ? isSelected ? GetAttributeForRole (VisualRole.Focus) : GetAttributeForRole (VisualRole.Normal) :
  634. isSelected ? GetAttributeForRole (VisualRole.Active) : GetAttributeForRole (VisualRole.Normal);
  635. if (newAttribute != current)
  636. {
  637. SetAttribute (newAttribute);
  638. current = newAttribute;
  639. }
  640. Move (0, row);
  641. if (Source is null || item >= Source.Count)
  642. {
  643. for (var c = 0; c < f.Width; c++)
  644. {
  645. AddRune ((Rune)' ');
  646. }
  647. }
  648. else
  649. {
  650. var rowEventArgs = new ListViewRowEventArgs (item);
  651. OnRowRender (rowEventArgs);
  652. if (rowEventArgs.RowAttribute is { } && current != rowEventArgs.RowAttribute)
  653. {
  654. current = (Attribute)rowEventArgs.RowAttribute;
  655. SetAttribute (current);
  656. }
  657. if (_allowsMarking)
  658. {
  659. AddRune (
  660. Source.IsMarked (item) ? AllowsMultipleSelection ? Glyphs.CheckStateChecked : Glyphs.Selected :
  661. AllowsMultipleSelection ? Glyphs.CheckStateUnChecked : Glyphs.UnSelected
  662. );
  663. AddRune ((Rune)' ');
  664. }
  665. Source.Render (this, isSelected, item, col, row, f.Width - col, start);
  666. }
  667. }
  668. return true;
  669. }
  670. /// <inheritdoc/>
  671. protected override void OnFrameChanged (in Rectangle frame) { EnsureSelectedItemVisible (); }
  672. /// <inheritdoc/>
  673. protected override void OnHasFocusChanged (bool newHasFocus, View? currentFocused, View? newFocused)
  674. {
  675. if (newHasFocus && _lastSelectedItem != SelectedItem)
  676. {
  677. EnsureSelectedItemVisible ();
  678. }
  679. }
  680. /// <inheritdoc/>
  681. protected override bool OnKeyDown (Key key)
  682. {
  683. // If the key was bound to key command, let normal KeyDown processing happen. This enables overriding the default handling.
  684. // See: https://github.com/gui-cs/Terminal.Gui/issues/3950#issuecomment-2807350939
  685. if (KeyBindings.TryGet (key, out _))
  686. {
  687. return false;
  688. }
  689. // Enable user to find & select an item by typing text
  690. if (KeystrokeNavigator.Matcher.IsCompatibleKey (key))
  691. {
  692. int? newItem = KeystrokeNavigator?.GetNextMatchingItem (SelectedItem ?? null, (char)key);
  693. if (newItem is { } && newItem != -1)
  694. {
  695. SelectedItem = (int)newItem;
  696. EnsureSelectedItemVisible ();
  697. SetNeedsDraw ();
  698. return true;
  699. }
  700. }
  701. return false;
  702. }
  703. /// <inheritdoc/>
  704. protected override bool OnMouseEvent (MouseEventArgs me)
  705. {
  706. if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)
  707. && !me.Flags.HasFlag (MouseFlags.Button1DoubleClicked)
  708. && me.Flags != MouseFlags.WheeledDown
  709. && me.Flags != MouseFlags.WheeledUp
  710. && me.Flags != MouseFlags.WheeledRight
  711. && me.Flags != MouseFlags.WheeledLeft)
  712. {
  713. return false;
  714. }
  715. if (!HasFocus && CanFocus)
  716. {
  717. SetFocus ();
  718. }
  719. if (Source is null)
  720. {
  721. return false;
  722. }
  723. if (me.Flags == MouseFlags.WheeledDown)
  724. {
  725. if (Viewport.Y + Viewport.Height < GetContentSize ().Height)
  726. {
  727. ScrollVertical (1);
  728. }
  729. return true;
  730. }
  731. if (me.Flags == MouseFlags.WheeledUp)
  732. {
  733. ScrollVertical (-1);
  734. return true;
  735. }
  736. if (me.Flags == MouseFlags.WheeledRight)
  737. {
  738. if (Viewport.X + Viewport.Width < GetContentSize ().Width)
  739. {
  740. ScrollHorizontal (1);
  741. }
  742. return true;
  743. }
  744. if (me.Flags == MouseFlags.WheeledLeft)
  745. {
  746. ScrollHorizontal (-1);
  747. return true;
  748. }
  749. if (me.Position.Y + Viewport.Y >= Source.Count
  750. || me.Position.Y + Viewport.Y < 0
  751. || me.Position.Y + Viewport.Y > Viewport.Y + Viewport.Height)
  752. {
  753. return true;
  754. }
  755. SelectedItem = Viewport.Y + me.Position.Y;
  756. if (MarkUnmarkSelectedItem ())
  757. {
  758. // return true;
  759. }
  760. SetNeedsDraw ();
  761. if (me.Flags == MouseFlags.Button1DoubleClicked)
  762. {
  763. return InvokeCommand (Command.Accept) is true;
  764. }
  765. return true;
  766. }
  767. /// <inheritdoc/>
  768. protected override void OnViewportChanged (DrawEventArgs e) { SetContentSize (new Size (MaxLength, Source?.Count ?? Viewport.Height)); }
  769. private void Source_CollectionChanged (object? sender, NotifyCollectionChangedEventArgs e)
  770. {
  771. SetContentSize (new Size (Source?.Length ?? Viewport.Width, Source?.Count ?? Viewport.Width));
  772. if (Source is { Count: > 0 } && SelectedItem.HasValue && SelectedItem > Source.Count - 1)
  773. {
  774. SelectedItem = Source.Count - 1;
  775. }
  776. SetNeedsDraw ();
  777. OnCollectionChanged (e);
  778. }
  779. }