OptionSelector.cs 8.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318
  1. #nullable enable
  2. namespace Terminal.Gui;
  3. /// <summary>
  4. /// Provides a user interface for displaying and selecting a single item from a list of options.
  5. /// Each option is represented by a checkbox, but only one can be selected at a time.
  6. /// </summary>
  7. public class OptionSelector : View, IOrientation, IDesignable
  8. {
  9. /// <summary>
  10. /// Initializes a new instance of the <see cref="OptionSelector"/> class.
  11. /// </summary>
  12. public OptionSelector ()
  13. {
  14. CanFocus = true;
  15. Width = Dim.Auto (DimAutoStyle.Content);
  16. Height = Dim.Auto (DimAutoStyle.Content);
  17. _orientationHelper = new (this);
  18. _orientationHelper.Orientation = Orientation.Vertical;
  19. // Accept (Enter key or DoubleClick) - Raise Accept event - DO NOT advance state
  20. AddCommand (Command.Accept, HandleAcceptCommand);
  21. CreateCheckBoxes ();
  22. }
  23. private bool? HandleAcceptCommand (ICommandContext? ctx) { return RaiseAccepting (ctx); }
  24. private int? _selectedItem;
  25. /// <summary>
  26. /// Gets or sets the index of the selected item.
  27. /// </summary>
  28. public int? SelectedItem
  29. {
  30. get => _selectedItem;
  31. set
  32. {
  33. if (_selectedItem == value)
  34. {
  35. return;
  36. }
  37. int? previousSelectedItem = _selectedItem;
  38. _selectedItem = value;
  39. UpdateChecked ();
  40. RaiseSelectedItemChanged (previousSelectedItem);
  41. }
  42. }
  43. private void RaiseSelectedItemChanged (int? previousSelectedItem)
  44. {
  45. OnSelectedItemChanged (SelectedItem, previousSelectedItem);
  46. if (SelectedItem.HasValue)
  47. {
  48. SelectedItemChanged?.Invoke (this, new (SelectedItem, previousSelectedItem));
  49. }
  50. }
  51. /// <summary>
  52. /// Called when <see cref="SelectedItem"/> has changed.
  53. /// </summary>
  54. protected virtual void OnSelectedItemChanged (int? selectedItem, int? previousSelectedItem) { }
  55. /// <summary>
  56. /// Raised when <see cref="SelectedItem"/> has changed.
  57. /// </summary>
  58. public event EventHandler<SelectedItemChangedArgs>? SelectedItemChanged;
  59. private IReadOnlyList<string>? _options;
  60. /// <summary>
  61. /// Gets or sets the list of options.
  62. /// </summary>
  63. public IReadOnlyList<string>? Options
  64. {
  65. get => _options;
  66. set
  67. {
  68. _options = value;
  69. CreateCheckBoxes ();
  70. }
  71. }
  72. private bool _assignHotKeysToCheckBoxes;
  73. /// <summary>
  74. /// If <see langword="true"/> the CheckBoxes will each be automatically assigned a hotkey.
  75. /// <see cref="UsedHotKeys"/> will be used to ensure unique keys are assigned. Set <see cref="UsedHotKeys"/>
  76. /// before setting <see cref="Options"/> with any hotkeys that may conflict with other Views.
  77. /// </summary>
  78. public bool AssignHotKeysToCheckBoxes
  79. {
  80. get => _assignHotKeysToCheckBoxes;
  81. set
  82. {
  83. if (_assignHotKeysToCheckBoxes == value)
  84. {
  85. return;
  86. }
  87. _assignHotKeysToCheckBoxes = value;
  88. CreateCheckBoxes ();
  89. UpdateChecked ();
  90. }
  91. }
  92. /// <summary>
  93. /// Gets the list of hotkeys already used by the CheckBoxes or that should not be used if
  94. /// <see cref="AssignHotKeysToCheckBoxes"/>
  95. /// is enabled.
  96. /// </summary>
  97. public List<Key> UsedHotKeys { get; } = new ();
  98. private void CreateCheckBoxes ()
  99. {
  100. if (Options is null)
  101. {
  102. return;
  103. }
  104. foreach (CheckBox cb in RemoveAll<CheckBox> ())
  105. {
  106. cb.Dispose ();
  107. }
  108. for (var index = 0; index < Options.Count; index++)
  109. {
  110. Add (CreateCheckBox (Options [index], index));
  111. }
  112. SetLayout ();
  113. }
  114. /// <summary>
  115. ///
  116. /// </summary>
  117. /// <param name="name"></param>
  118. /// <param name="index"></param>
  119. /// <returns></returns>
  120. protected virtual CheckBox CreateCheckBox (string name, int index)
  121. {
  122. string nameWithHotKey = name;
  123. if (AssignHotKeysToCheckBoxes)
  124. {
  125. // Find the first char in label that is [a-z], [A-Z], or [0-9]
  126. for (var i = 0; i < name.Length; i++)
  127. {
  128. char c = char.ToLowerInvariant (name [i]);
  129. if (UsedHotKeys.Contains (new (c)) || !char.IsAsciiLetterOrDigit (c))
  130. {
  131. continue;
  132. }
  133. if (char.IsAsciiLetterOrDigit (c))
  134. {
  135. char? hotChar = c;
  136. nameWithHotKey = name.Insert (i, HotKeySpecifier.ToString ());
  137. UsedHotKeys.Add (new (hotChar));
  138. break;
  139. }
  140. }
  141. }
  142. var checkbox = new CheckBox
  143. {
  144. CanFocus = true,
  145. Title = nameWithHotKey,
  146. Id = name,
  147. Data = index,
  148. HighlightStyle = HighlightStyle.Hover,
  149. RadioStyle = true
  150. };
  151. checkbox.GettingNormalColor += (_, e) =>
  152. {
  153. if (SuperView is { HasFocus: true })
  154. {
  155. e.Cancel = true;
  156. if (!HasFocus)
  157. {
  158. e.NewValue = GetFocusColor ();
  159. }
  160. else
  161. {
  162. // If _colorScheme was set, it's because of Hover
  163. if (checkbox._colorScheme is { })
  164. {
  165. e.NewValue = checkbox._colorScheme.Normal;
  166. }
  167. else
  168. {
  169. e.NewValue = GetNormalColor ();
  170. }
  171. }
  172. }
  173. };
  174. checkbox.GettingHotNormalColor += (_, e) =>
  175. {
  176. if (SuperView is { HasFocus: true })
  177. {
  178. e.Cancel = true;
  179. if (!HasFocus)
  180. {
  181. e.NewValue = GetHotFocusColor ();
  182. }
  183. else
  184. {
  185. // If _colorScheme was set, it's because of Hover
  186. if (checkbox._colorScheme is { })
  187. {
  188. e.NewValue = checkbox._colorScheme.Normal;
  189. }
  190. else
  191. {
  192. e.NewValue = GetNormalColor ();
  193. }
  194. }
  195. }
  196. };
  197. checkbox.Selecting += (sender, args) =>
  198. {
  199. if (RaiseSelecting (args.Context) is true)
  200. {
  201. args.Handled = true;
  202. return;
  203. }
  204. ;
  205. if (RaiseAccepting (args.Context) is true)
  206. {
  207. args.Handled = true;
  208. }
  209. };
  210. checkbox.CheckedStateChanged += (sender, args) =>
  211. {
  212. if (checkbox.CheckedState == CheckState.Checked)
  213. {
  214. SelectedItem = index;
  215. }
  216. };
  217. return checkbox;
  218. }
  219. private void SetLayout ()
  220. {
  221. foreach (View sv in SubViews)
  222. {
  223. if (Orientation == Orientation.Vertical)
  224. {
  225. sv.X = 0;
  226. sv.Y = Pos.Align (Alignment.Start);
  227. }
  228. else
  229. {
  230. sv.X = Pos.Align (Alignment.Start);
  231. sv.Y = 0;
  232. sv.Margin!.Thickness = new (0, 0, 1, 0);
  233. }
  234. }
  235. }
  236. private void UpdateChecked ()
  237. {
  238. foreach (CheckBox cb in SubViews.OfType<CheckBox> ())
  239. {
  240. var index = (int)(cb.Data ?? throw new InvalidOperationException ("CheckBox.Data must be set"));
  241. cb.CheckedState = index == SelectedItem ? CheckState.Checked : CheckState.UnChecked;
  242. }
  243. }
  244. #region IOrientation
  245. /// <summary>
  246. /// Gets or sets the <see cref="Orientation"/> for this <see cref="OptionSelector"/>. The default is
  247. /// <see cref="Orientation.Vertical"/>.
  248. /// </summary>
  249. public Orientation Orientation
  250. {
  251. get => _orientationHelper.Orientation;
  252. set => _orientationHelper.Orientation = value;
  253. }
  254. private readonly OrientationHelper _orientationHelper;
  255. #pragma warning disable CS0067 // The event is never used
  256. /// <inheritdoc/>
  257. public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
  258. /// <inheritdoc/>
  259. public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
  260. #pragma warning restore CS0067 // The event is never used
  261. /// <summary>Called when <see cref="Orientation"/> has changed.</summary>
  262. /// <param name="newOrientation"></param>
  263. public void OnOrientationChanged (Orientation newOrientation) { SetLayout (); }
  264. #endregion IOrientation
  265. /// <inheritdoc/>
  266. public bool EnableForDesign ()
  267. {
  268. AssignHotKeysToCheckBoxes = true;
  269. Options = new [] { "Option 1", "Option 2", "Option 3" };
  270. return true;
  271. }
  272. }