OptionSelector.cs 7.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. #nullable enable
  2. using System.Collections.Immutable;
  3. using System.Diagnostics;
  4. namespace Terminal.Gui.Views;
  5. // DoubleClick - Focus, Select, and Accept the item under the mouse.
  6. // Click - Focus, Select, and do NOT Accept the item under the mouse.
  7. // CanFocus - Not Focused:
  8. // HotKey - Restore Focus. Advance Active. Do NOT Accept.
  9. // Item HotKey - Focus item. If item is not active, make Active. Do NOT Accept.
  10. // !CanFocus - Not Focused:
  11. // HotKey - Do NOT Restore Focus. Advance Active. Do NOT Accept.
  12. // Item HotKey - Do NOT Focus item. If item is not active, make Active. Do NOT Accept.
  13. // Focused:
  14. // Space key - If focused item is Active, move focus to and Acivate next. Else, Select current. Do NOT Accept.
  15. // Enter key - Select and Accept the focused item.
  16. // HotKey - Restore Focus. Advance Active. Do NOT Accept.
  17. // Item HotKey - If item is not active, make Active. Do NOT Accept.
  18. /// <summary>
  19. /// Provides a user interface for displaying and selecting a single item from a list of options.
  20. /// Each option is represented by a checkbox, but only one can be selected at a time.
  21. /// <see cref="OptionSelector{TEnum}"/> provides a type-safe version where a <see langword="enum"/> can be
  22. /// provided.
  23. /// </summary>
  24. public class OptionSelector : SelectorBase, IDesignable
  25. {
  26. /// <inheritdoc />
  27. public OptionSelector ()
  28. {
  29. // By default, for OptionSelector, Value is set to 0. It can be set to null if a developer
  30. // really wants that.
  31. base.Value = 0;
  32. }
  33. /// <inheritdoc />
  34. protected override bool OnHandlingHotKey (CommandEventArgs args)
  35. {
  36. if (base.OnHandlingHotKey (args) is true)
  37. {
  38. return true;
  39. }
  40. if (!CanFocus)
  41. {
  42. if (RaiseSelecting (args.Context) is true)
  43. {
  44. return true;
  45. }
  46. }
  47. else if (!HasFocus && Value is null)
  48. {
  49. if (RaiseSelecting (args.Context) is true)
  50. {
  51. return true;
  52. }
  53. SetFocus ();
  54. Value = Values? [0];
  55. return true;
  56. }
  57. return false;
  58. }
  59. /// <inheritdoc />
  60. protected override bool OnSelecting (CommandEventArgs args)
  61. {
  62. if (base.OnSelecting (args) is true)
  63. {
  64. return true;
  65. }
  66. if (!CanFocus || args.Context?.Source is not CheckBox checkBox)
  67. {
  68. Cycle ();
  69. return false;
  70. }
  71. if (args.Context is CommandContext<KeyBinding> { } && (int)checkBox.Data! == Value)
  72. {
  73. // Caused by keypress. If the checkbox is already checked, we cycle to the next one.
  74. Cycle ();
  75. }
  76. else
  77. {
  78. if (Value == (int)checkBox.Data!)
  79. {
  80. return true;
  81. }
  82. Value = (int)checkBox.Data!;
  83. // if (HasFocus)
  84. {
  85. UpdateChecked ();
  86. }
  87. }
  88. return false;
  89. }
  90. /// <inheritdoc />
  91. protected override void OnSubViewAdded (View view)
  92. {
  93. base.OnSubViewAdded (view);
  94. if (view is not CheckBox checkbox)
  95. {
  96. return;
  97. }
  98. checkbox.RadioStyle = true;
  99. checkbox.Selecting += OnCheckboxOnSelecting;
  100. checkbox.Accepting += OnCheckboxOnAccepting;
  101. }
  102. private void OnCheckboxOnSelecting (object? sender, CommandEventArgs args)
  103. {
  104. if (sender is not CheckBox checkbox)
  105. {
  106. return;
  107. }
  108. // Verify at most one is checked
  109. Debug.Assert (SubViews.OfType<CheckBox> ().Count (cb => cb.CheckedState == CheckState.Checked) <= 1);
  110. if (args.Context is CommandContext<MouseBinding> { } && checkbox.CheckedState == CheckState.Checked)
  111. {
  112. // If user clicks with mouse and item is already checked, do nothing
  113. args.Handled = true;
  114. return;
  115. }
  116. if (args.Context is CommandContext<KeyBinding> binding && binding.Command == Command.HotKey && checkbox.CheckedState == CheckState.Checked)
  117. {
  118. // If user uses an item hotkey and the item is already checked, do nothing
  119. args.Handled = true;
  120. return;
  121. }
  122. if (checkbox.CanFocus)
  123. {
  124. // For Select, if the view is focusable and SetFocus succeeds, by defition,
  125. // the event is handled. So return what SetFocus returns.
  126. checkbox.SetFocus ();
  127. }
  128. // Selecting doesn't normally propogate, so we do it here
  129. if (InvokeCommand (Command.Select, args.Context) is true)
  130. {
  131. // Do not return here; we want to toggle the checkbox state
  132. args.Handled = true;
  133. return;
  134. }
  135. args.Handled = true;
  136. }
  137. private void OnCheckboxOnAccepting (object? sender, CommandEventArgs args)
  138. {
  139. if (sender is not CheckBox checkbox)
  140. {
  141. return;
  142. }
  143. Value = (int)checkbox.Data!;
  144. args.Handled = false; // Do not set to false; let Accepting propagate
  145. }
  146. private void Cycle ()
  147. {
  148. int valueIndex = Values.IndexOf (v => v == Value);
  149. Value = valueIndex == Values?.Count () - 1
  150. ? Values! [0]
  151. : Values! [valueIndex + 1];
  152. if (HasFocus)
  153. {
  154. valueIndex = Values.IndexOf (v => v == Value);
  155. SubViews.OfType<CheckBox> ().ToArray () [valueIndex].SetFocus ();
  156. }
  157. // Verify at most one is checked
  158. Debug.Assert (SubViews.OfType<CheckBox> ().Count (cb => cb.CheckedState == CheckState.Checked) <= 1);
  159. }
  160. /// <summary>
  161. /// Updates the checked state of all checkbox subviews so that only the checkbox corresponding
  162. /// to the current <see cref="SelectorBase.Value"/> is checked. Throws <see cref="InvalidOperationException"/>
  163. /// if a checkbox's Data property is not set.
  164. /// </summary>
  165. /// <exception cref="InvalidOperationException"></exception>
  166. public override void UpdateChecked ()
  167. {
  168. foreach (CheckBox cb in SubViews.OfType<CheckBox> ())
  169. {
  170. int value = (int)(cb.Data ?? throw new InvalidOperationException ("CheckBox.Data must be set"));
  171. cb.CheckedState = value == Value ? CheckState.Checked : CheckState.UnChecked;
  172. }
  173. // Verify at most one is checked
  174. Debug.Assert (SubViews.OfType<CheckBox> ().Count (cb => cb.CheckedState == CheckState.Checked) <= 1);
  175. }
  176. /// <summary>
  177. /// Gets or sets the <see cref="SelectorBase.Labels"/> index for the cursor. The cursor may or may not be the selected
  178. /// RadioItem.
  179. /// </summary>
  180. /// <remarks>
  181. /// <para>
  182. /// Maps to either the X or Y position within <see cref="View.Viewport"/> depending on <see cref="Orientation"/>.
  183. /// </para>
  184. /// </remarks>
  185. public int Cursor
  186. {
  187. get => !CanFocus ? 0 : SubViews.OfType<CheckBox> ().ToArray ().IndexOf (Focused);
  188. set
  189. {
  190. if (!CanFocus)
  191. {
  192. return;
  193. }
  194. CheckBox [] checkBoxes = SubViews.OfType<CheckBox> ().ToArray ();
  195. if (value < 0 || value >= checkBoxes.Length)
  196. {
  197. throw new ArgumentOutOfRangeException (nameof (value), @"Cursor index is out of range");
  198. }
  199. checkBoxes [value].SetFocus ();
  200. }
  201. }
  202. /// <inheritdoc/>
  203. public bool EnableForDesign ()
  204. {
  205. AssignHotKeys = true;
  206. Labels = ["Option 1", "Option 2", "Third Option", "Option Quattro"];
  207. return true;
  208. }
  209. }