FlagSelector.cs 7.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244
  1. #nullable enable
  2. using System.Collections.Immutable;
  3. namespace Terminal.Gui.Views;
  4. // DoubleClick - Focus, Select (Toggle), and Accept the item under the mouse.
  5. // Click - Focus, Select (Toggle), and do NOT Accept the item under the mouse.
  6. // Not Focused:
  7. // HotKey - Restore Focus. Do NOT change Active.
  8. // Item HotKey - Focus item. Select (Toggle) item. Do NOT Accept.
  9. // Focused:
  10. // Space key - Select (Toggle) focused item. Do NOT Accept.
  11. // Enter key - Select (Toggle) and Accept the focused item.
  12. // HotKey - No-op.
  13. // Item HotKey - Focus item, Select (Toggle), and do NOT Accept.
  14. /// <summary>
  15. /// Provides a user interface for displaying and selecting non-mutually-exclusive flags from a provided dictionary.
  16. /// <see cref="FlagSelector{TFlagsEnum}"/> provides a type-safe version where a `[Flags]` <see langword="enum"/> can be
  17. /// provided.
  18. /// </summary>
  19. public class FlagSelector : SelectorBase, IDesignable
  20. {
  21. /// <inheritdoc />
  22. protected override void OnSubViewAdded (View view)
  23. {
  24. base.OnSubViewAdded (view);
  25. if (view is not CheckBox checkbox)
  26. {
  27. return;
  28. }
  29. checkbox.RadioStyle = false;
  30. checkbox.CheckedStateChanging += OnCheckboxOnCheckedStateChanging;
  31. checkbox.CheckedStateChanged += OnCheckboxOnCheckedStateChanged;
  32. checkbox.Selecting += OnCheckboxOnSelecting;
  33. checkbox.Accepting += OnCheckboxOnAccepting;
  34. }
  35. private void OnCheckboxOnCheckedStateChanging (object? sender, ResultEventArgs<CheckState> args)
  36. {
  37. if (sender is not CheckBox checkbox)
  38. {
  39. return;
  40. }
  41. if (checkbox.CheckedState == CheckState.Checked && (int)checkbox.Data! == 0 && Value == 0)
  42. {
  43. args.Handled = true;
  44. }
  45. }
  46. private void OnCheckboxOnCheckedStateChanged (object? sender, EventArgs<CheckState> args)
  47. {
  48. if (sender is not CheckBox checkbox)
  49. {
  50. return;
  51. }
  52. int newValue = Value ?? 0;
  53. if (checkbox.CheckedState == CheckState.Checked)
  54. {
  55. if ((int)checkbox.Data! == default!)
  56. {
  57. newValue = 0;
  58. }
  59. else
  60. {
  61. newValue |= (int)checkbox.Data!;
  62. }
  63. }
  64. else
  65. {
  66. newValue &= ~(int)checkbox.Data!;
  67. }
  68. Value = newValue;
  69. }
  70. private void OnCheckboxOnSelecting (object? sender, CommandEventArgs args)
  71. {
  72. if (sender is not CheckBox checkbox)
  73. {
  74. return;
  75. }
  76. if (checkbox.CanFocus)
  77. {
  78. // For Select, if the view is focusable and SetFocus succeeds, by defition,
  79. // the event is handled. So return what SetFocus returns.
  80. checkbox.SetFocus ();
  81. }
  82. // Selecting doesn't normally propogate, so we do it here
  83. if (InvokeCommand (Command.Select, args.Context) is true)
  84. {
  85. // Do not return here; we want to toggle the checkbox state
  86. args.Handled = true;
  87. //return;
  88. }
  89. }
  90. private void OnCheckboxOnAccepting (object? sender, CommandEventArgs args)
  91. {
  92. if (sender is not CheckBox checkbox)
  93. {
  94. return;
  95. }
  96. Value = (int)checkbox.Data!;
  97. args.Handled = false; // Do not set to false; let Accepting propagate
  98. }
  99. private int? _value;
  100. /// <summary>
  101. /// Gets or sets the value of the selected flags.
  102. /// </summary>
  103. public override int? Value
  104. {
  105. get => _value;
  106. set
  107. {
  108. if (_updatingChecked || _value == value)
  109. {
  110. return;
  111. }
  112. int? previousValue = _value;
  113. _value = value;
  114. if (_value is null)
  115. {
  116. UncheckNone ();
  117. UncheckAll ();
  118. }
  119. else
  120. {
  121. UpdateChecked ();
  122. }
  123. RaiseValueChanged (previousValue);
  124. }
  125. }
  126. private void UncheckNone ()
  127. {
  128. // Uncheck ONLY the None checkbox (Data == 0)
  129. _updatingChecked = true;
  130. foreach (CheckBox cb in SubViews.OfType<CheckBox> ().Where (sv => (int)sv.Data! == 0))
  131. {
  132. cb.CheckedState = CheckState.UnChecked;
  133. }
  134. _updatingChecked = false;
  135. }
  136. private void UncheckAll ()
  137. {
  138. // Uncheck all NON-None checkboxes (Data != 0)
  139. _updatingChecked = true;
  140. foreach (CheckBox cb in SubViews.OfType<CheckBox> ().Where (sv => (int)(sv.Data ?? default!) != default!))
  141. {
  142. cb.CheckedState = CheckState.UnChecked;
  143. }
  144. _updatingChecked = false;
  145. }
  146. private bool _updatingChecked = false;
  147. /// <inheritdoc />
  148. public override void UpdateChecked ()
  149. {
  150. if (_updatingChecked)
  151. {
  152. return;
  153. }
  154. _updatingChecked = true;
  155. foreach (CheckBox cb in SubViews.OfType<CheckBox> ())
  156. {
  157. var flag = (int)(cb.Data ?? throw new InvalidOperationException ("CheckBox.Data must be set"));
  158. // If this flag is set in Value, check the checkbox. Otherwise, uncheck it.
  159. if (flag == 0)
  160. {
  161. cb.CheckedState = (Value != 0) ? CheckState.UnChecked : CheckState.Checked;
  162. }
  163. else
  164. {
  165. cb.CheckedState = (Value & flag) == flag ? CheckState.Checked : CheckState.UnChecked;
  166. }
  167. }
  168. _updatingChecked = false;
  169. }
  170. /// <inheritdoc />
  171. protected override void OnCreatingSubViews ()
  172. {
  173. // FlagSelector supports a "None" check box; add it
  174. if (Styles.HasFlag (SelectorStyles.ShowNoneFlag) && Values is { } && !Values.Contains (0))
  175. {
  176. Add (CreateCheckBox ("None", 0));
  177. }
  178. }
  179. /// <inheritdoc />
  180. protected override void OnCreatedSubViews ()
  181. {
  182. // If the values include 0, and ShowNoneFlag is not specified, remove the "None" check box
  183. if (!Styles.HasFlag (SelectorStyles.ShowNoneFlag))
  184. {
  185. CheckBox? noneCheckBox = SubViews.OfType<CheckBox> ().FirstOrDefault (cb => (int)cb.Data! == 0);
  186. if (noneCheckBox is { })
  187. {
  188. Remove (noneCheckBox);
  189. noneCheckBox.Dispose ();
  190. }
  191. }
  192. }
  193. /// <inheritdoc/>
  194. public bool EnableForDesign ()
  195. {
  196. Styles = SelectorStyles.All;
  197. AssignHotKeys = true;
  198. SetValuesAndLabels<SelectorStyles> ();
  199. Labels = Enum.GetValues<SelectorStyles> ()
  200. .Select (
  201. l => l switch
  202. {
  203. SelectorStyles.None => "No Style",
  204. SelectorStyles.ShowNoneFlag => "Show None Value Style",
  205. SelectorStyles.ShowValue => "Show Value Editor Style",
  206. SelectorStyles.All => "All Styles",
  207. _ => l.ToString ()
  208. }).ToList ();
  209. return true;
  210. }
  211. }