OptionSelector.cs 7.5 KB

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