CheckBoxTableSourceWrapper.cs 6.2 KB

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