CheckBoxTableSourceWrapper.cs 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. #nullable disable
  2. 
  3. namespace Terminal.Gui.Views;
  4. /// <summary>
  5. /// <see cref="ITableSource"/> for a <see cref="TableView"/> which adds a checkbox column as an additional column
  6. /// in the table.
  7. /// </summary>
  8. /// <remarks>
  9. /// This class wraps another <see cref="ITableSource"/> and dynamically serves its rows/cols plus an extra column.
  10. /// Data in the wrapped source can be dynamic (change over time).
  11. /// </remarks>
  12. public abstract class CheckBoxTableSourceWrapperBase : ITableSource
  13. {
  14. private readonly TableView tableView;
  15. /// <summary>
  16. /// Creates a new instance of the class presenting the data in <paramref name="toWrap"/> plus an additional
  17. /// checkbox column.
  18. /// </summary>
  19. /// <param name="tableView">
  20. /// The <see cref="TableView"/> this source will be used with. This is required for event
  21. /// registration.
  22. /// </param>
  23. /// <param name="toWrap">The original data source of the <see cref="TableView"/> that you want to add checkboxes to.</param>
  24. public CheckBoxTableSourceWrapperBase (TableView tableView, ITableSource toWrap)
  25. {
  26. Wrapping = toWrap;
  27. this.tableView = tableView;
  28. tableView.KeyBindings.ReplaceCommands (Key.Space, Command.Activate);
  29. tableView.Activating += TableView_Activating;
  30. tableView.CellToggled += TableView_CellToggled;
  31. }
  32. /// <summary>
  33. /// Gets or sets the character to use for checked entries. Defaults to <see cref="Glyphs.CheckStateChecked"/>
  34. /// </summary>
  35. public Rune CheckedRune { get; set; } = Glyphs.CheckStateChecked;
  36. /// <summary>
  37. /// Gets or sets the character to use for checked entry when <see cref="UseRadioButtons"/> is true. Defaults to
  38. /// <see cref="Glyphs.Selected"/>
  39. /// </summary>
  40. public Rune RadioCheckedRune { get; set; } = Glyphs.Selected;
  41. /// <summary>
  42. /// Gets or sets the character to use for unchecked entries when <see cref="UseRadioButtons"/> is true. Defaults
  43. /// to <see cref="Glyphs.UnSelected"/>
  44. /// </summary>
  45. public Rune RadioUnCheckedRune { get; set; } = Glyphs.UnSelected;
  46. /// <summary>
  47. /// Gets or sets the character to use for UnChecked entries. Defaults to <see cref="Glyphs.CheckStateUnChecked"/>
  48. /// </summary>
  49. public Rune UnCheckedRune { get; set; } = Glyphs.CheckStateUnChecked;
  50. /// <summary>Gets or sets whether to only allow a single row to be toggled at once (Radio button).</summary>
  51. public bool UseRadioButtons { get; set; }
  52. /// <summary>Gets the <see cref="ITableSource"/> that this instance is wrapping.</summary>
  53. public ITableSource Wrapping { get; }
  54. /// <inheritdoc/>
  55. public object this [int row, int col]
  56. {
  57. get
  58. {
  59. if (col == 0)
  60. {
  61. if (UseRadioButtons)
  62. {
  63. return IsChecked (row) ? RadioCheckedRune : RadioUnCheckedRune;
  64. }
  65. return IsChecked (row) ? CheckedRune : UnCheckedRune;
  66. }
  67. return Wrapping [row, col - 1];
  68. }
  69. }
  70. /// <inheritdoc/>
  71. public int Rows => Wrapping.Rows;
  72. /// <inheritdoc/>
  73. public int Columns => Wrapping.Columns + 1;
  74. /// <inheritdoc/>
  75. public string [] ColumnNames
  76. {
  77. get
  78. {
  79. List<string> toReturn = Wrapping.ColumnNames.ToList ();
  80. toReturn.Insert (0, " ");
  81. return toReturn.ToArray ();
  82. }
  83. }
  84. /// <summary>Clears the toggled state of all rows.</summary>
  85. protected abstract void ClearAllToggles ();
  86. /// <summary>Returns true if <paramref name="row"/> is checked.</summary>
  87. /// <param name="row"></param>
  88. /// <returns></returns>
  89. protected abstract bool IsChecked (int row);
  90. /// <summary>
  91. /// Called when the 'toggled all' action is performed. This should change state from 'some selected' to 'all
  92. /// selected' or clear selection if all area already selected.
  93. /// </summary>
  94. protected abstract void ToggleAllRows ();
  95. /// <summary>Flips the checked state of the given <paramref name="row"/>/</summary>
  96. /// <param name="row"></param>
  97. protected abstract void ToggleRow (int row);
  98. /// <summary>
  99. /// Flips the checked state for a collection of rows. If some (but not all) are selected they should flip to all
  100. /// selected.
  101. /// </summary>
  102. /// <param name="range"></param>
  103. protected abstract void ToggleRows (int [] range);
  104. private void TableView_CellToggled (object sender, CellToggledEventArgs e)
  105. {
  106. // Suppress default toggle behavior when using checkboxes
  107. // and instead handle ourselves
  108. int [] range = tableView.GetAllSelectedCells ().Select (c => c.Y).Distinct ().ToArray ();
  109. if (UseRadioButtons)
  110. {
  111. // multi selection makes it unclear what to toggle in this situation
  112. if (range.Length != 1)
  113. {
  114. e.Cancel = true;
  115. return;
  116. }
  117. ClearAllToggles ();
  118. ToggleRow (range.Single ());
  119. }
  120. else
  121. {
  122. ToggleRows (range);
  123. }
  124. e.Cancel = true;
  125. tableView.SetNeedsDraw ();
  126. }
  127. #nullable enable
  128. private void TableView_Activating (object? sender, CommandEventArgs e)
  129. {
  130. // Only handle mouse clicks, not keyboard selections
  131. if (e.Context is not CommandContext<MouseBinding> { Binding.MouseEventArgs: { } mouseArgs })
  132. {
  133. return;
  134. }
  135. // we only care about clicks (not movements)
  136. if (!mouseArgs.Flags.HasFlag (MouseFlags.Button1Clicked))
  137. {
  138. return;
  139. }
  140. Point? hit = tableView.ScreenToCell (mouseArgs.Position.X, mouseArgs.Position.Y, out int? headerIfAny);
  141. if (headerIfAny.HasValue && headerIfAny.Value == 0)
  142. {
  143. // clicking in header with radio buttons does nothing
  144. if (UseRadioButtons)
  145. {
  146. return;
  147. }
  148. // otherwise it ticks all rows
  149. ToggleAllRows ();
  150. e.Handled = true;
  151. tableView.SetNeedsDraw ();
  152. }
  153. else if (hit.HasValue && hit.Value.X == 0)
  154. {
  155. if (UseRadioButtons)
  156. {
  157. ClearAllToggles ();
  158. ToggleRow (hit.Value.Y);
  159. }
  160. else
  161. {
  162. ToggleRow (hit.Value.Y);
  163. }
  164. e.Handled = true;
  165. tableView.SetNeedsDraw ();
  166. }
  167. }
  168. #nullable restore
  169. }