FlagSelector.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513
  1. #nullable enable
  2. namespace Terminal.Gui.Views;
  3. /// <summary>
  4. /// Provides a user interface for displaying and selecting non-mutually-exclusive flags.
  5. /// Flags can be set from a dictionary or directly from an enum type.
  6. /// </summary>
  7. public class FlagSelector : View, IOrientation, IDesignable
  8. {
  9. /// <summary>
  10. /// Initializes a new instance of the <see cref="FlagSelector"/> class.
  11. /// </summary>
  12. public FlagSelector ()
  13. {
  14. CanFocus = true;
  15. Width = Dim.Auto (DimAutoStyle.Content);
  16. Height = Dim.Auto (DimAutoStyle.Content);
  17. // ReSharper disable once UseObjectOrCollectionInitializer
  18. _orientationHelper = new (this);
  19. _orientationHelper.Orientation = Orientation.Vertical;
  20. // Accept (Enter key or DoubleClick) - Raise Accept event - DO NOT advance state
  21. AddCommand (Command.Accept, HandleAcceptCommand);
  22. CreateCheckBoxes ();
  23. }
  24. private bool? HandleAcceptCommand (ICommandContext? ctx) { return RaiseAccepting (ctx); }
  25. private uint? _value;
  26. /// <summary>
  27. /// Gets or sets the value of the selected flags.
  28. /// </summary>
  29. public uint? Value
  30. {
  31. get => _value;
  32. set
  33. {
  34. if (_value == value)
  35. {
  36. return;
  37. }
  38. _value = value;
  39. if (_value is null)
  40. {
  41. UncheckNone ();
  42. UncheckAll ();
  43. }
  44. else
  45. {
  46. UpdateChecked ();
  47. }
  48. if (ValueEdit is { })
  49. {
  50. ValueEdit.Text = _value.ToString ();
  51. }
  52. RaiseValueChanged ();
  53. }
  54. }
  55. private void RaiseValueChanged ()
  56. {
  57. OnValueChanged ();
  58. if (Value.HasValue)
  59. {
  60. ValueChanged?.Invoke (this, new EventArgs<uint> (Value.Value));
  61. }
  62. }
  63. /// <summary>
  64. /// Called when <see cref="Value"/> has changed.
  65. /// </summary>
  66. protected virtual void OnValueChanged () { }
  67. /// <summary>
  68. /// Raised when <see cref="Value"/> has changed.
  69. /// </summary>
  70. public event EventHandler<EventArgs<uint>>? ValueChanged;
  71. private FlagSelectorStyles _styles;
  72. /// <summary>
  73. /// Gets or sets the styles for the flag selector.
  74. /// </summary>
  75. public FlagSelectorStyles Styles
  76. {
  77. get => _styles;
  78. set
  79. {
  80. if (_styles == value)
  81. {
  82. return;
  83. }
  84. _styles = value;
  85. CreateCheckBoxes ();
  86. }
  87. }
  88. /// <summary>
  89. /// Set the flags and flag names.
  90. /// </summary>
  91. /// <param name="flags"></param>
  92. public virtual void SetFlags (IReadOnlyDictionary<uint, string> flags)
  93. {
  94. Flags = flags;
  95. CreateCheckBoxes ();
  96. UpdateChecked ();
  97. }
  98. /// <summary>
  99. /// Set the flags and flag names from an enum type.
  100. /// </summary>
  101. /// <typeparam name="TEnum">The enum type to extract flags from</typeparam>
  102. /// <remarks>
  103. /// This is a convenience method that converts an enum to a dictionary of flag values and names.
  104. /// The enum values are converted to uint values and the enum names become the display text.
  105. /// </remarks>
  106. public void SetFlags<TEnum> () where TEnum : struct, Enum
  107. {
  108. // Convert enum names and values to a dictionary
  109. Dictionary<uint, string> flagsDictionary = Enum.GetValues<TEnum> ()
  110. .ToDictionary (
  111. f => Convert.ToUInt32 (f),
  112. f => f.ToString ()
  113. );
  114. SetFlags (flagsDictionary);
  115. }
  116. /// <summary>
  117. /// Set the flags and flag names from an enum type with custom display names.
  118. /// </summary>
  119. /// <typeparam name="TEnum">The enum type to extract flags from</typeparam>
  120. /// <param name="nameSelector">A function that converts enum values to display names</param>
  121. /// <remarks>
  122. /// This is a convenience method that converts an enum to a dictionary of flag values and custom names.
  123. /// The enum values are converted to uint values and the display names are determined by the nameSelector function.
  124. /// </remarks>
  125. /// <example>
  126. /// <code>
  127. /// // Use enum values with custom display names
  128. /// var flagSelector = new FlagSelector ();
  129. /// flagSelector.SetFlags&lt;FlagSelectorStyles&gt;
  130. /// (f => f switch {
  131. /// FlagSelectorStyles.ShowNone => "Show None Value",
  132. /// FlagSelectorStyles.ShowValueEdit => "Show Value Editor",
  133. /// FlagSelectorStyles.All => "Everything",
  134. /// _ => f.ToString()
  135. /// });
  136. /// </code>
  137. /// </example>
  138. public void SetFlags<TEnum> (Func<TEnum, string> nameSelector) where TEnum : struct, Enum
  139. {
  140. // Convert enum values and custom names to a dictionary
  141. Dictionary<uint, string> flagsDictionary = Enum.GetValues<TEnum> ()
  142. .ToDictionary (
  143. f => Convert.ToUInt32 (f),
  144. nameSelector
  145. );
  146. SetFlags (flagsDictionary);
  147. }
  148. private IReadOnlyDictionary<uint, string>? _flags;
  149. /// <summary>
  150. /// Gets the flag values and names.
  151. /// </summary>
  152. public IReadOnlyDictionary<uint, string>? Flags
  153. {
  154. get => _flags;
  155. internal set
  156. {
  157. _flags = value;
  158. if (_value is null)
  159. {
  160. Value = Convert.ToUInt16 (_flags?.Keys.ElementAt (0));
  161. }
  162. }
  163. }
  164. private TextField? ValueEdit { get; set; }
  165. private bool _assignHotKeysToCheckBoxes;
  166. /// <summary>
  167. /// If <see langword="true"/> the CheckBoxes will each be automatically assigned a hotkey.
  168. /// <see cref="UsedHotKeys"/> will be used to ensure unique keys are assigned. Set <see cref="UsedHotKeys"/>
  169. /// before setting <see cref="Flags"/> with any hotkeys that may conflict with other Views.
  170. /// </summary>
  171. public bool AssignHotKeysToCheckBoxes
  172. {
  173. get => _assignHotKeysToCheckBoxes;
  174. set
  175. {
  176. if (_assignHotKeysToCheckBoxes == value)
  177. {
  178. return;
  179. }
  180. _assignHotKeysToCheckBoxes = value;
  181. CreateCheckBoxes ();
  182. UpdateChecked ();
  183. }
  184. }
  185. /// <summary>
  186. /// Gets the list of hotkeys already used by the CheckBoxes or that should not be used if
  187. /// <see cref="AssignHotKeysToCheckBoxes"/>
  188. /// is enabled.
  189. /// </summary>
  190. public List<Key> UsedHotKeys { get; } = [];
  191. private void CreateCheckBoxes ()
  192. {
  193. if (Flags is null)
  194. {
  195. return;
  196. }
  197. foreach (CheckBox cb in RemoveAll<CheckBox> ())
  198. {
  199. cb.Dispose ();
  200. }
  201. if (Styles.HasFlag (FlagSelectorStyles.ShowNone) && !Flags.ContainsKey (0))
  202. {
  203. Add (CreateCheckBox ("None", 0));
  204. }
  205. for (var index = 0; index < Flags.Count; index++)
  206. {
  207. if (!Styles.HasFlag (FlagSelectorStyles.ShowNone) && Flags.ElementAt (index).Key == 0)
  208. {
  209. continue;
  210. }
  211. Add (CreateCheckBox (Flags.ElementAt (index).Value, Flags.ElementAt (index).Key));
  212. }
  213. if (Styles.HasFlag (FlagSelectorStyles.ShowValueEdit))
  214. {
  215. ValueEdit = new ()
  216. {
  217. Id = "valueEdit",
  218. CanFocus = false,
  219. Text = Value.ToString (),
  220. Width = 5,
  221. ReadOnly = true,
  222. };
  223. Add (ValueEdit);
  224. }
  225. SetLayout ();
  226. return;
  227. }
  228. /// <summary>
  229. ///
  230. /// </summary>
  231. /// <param name="name"></param>
  232. /// <param name="flag"></param>
  233. /// <returns></returns>
  234. protected virtual CheckBox CreateCheckBox (string name, uint flag)
  235. {
  236. string nameWithHotKey = name;
  237. if (AssignHotKeysToCheckBoxes)
  238. {
  239. // Find the first char in label that is [a-z], [A-Z], or [0-9]
  240. for (var i = 0; i < name.Length; i++)
  241. {
  242. char c = char.ToLowerInvariant (name [i]);
  243. if (UsedHotKeys.Contains (new (c)) || !char.IsAsciiLetterOrDigit (c))
  244. {
  245. continue;
  246. }
  247. if (char.IsAsciiLetterOrDigit (c))
  248. {
  249. char? hotChar = c;
  250. nameWithHotKey = name.Insert (i, HotKeySpecifier.ToString ());
  251. UsedHotKeys.Add (new (hotChar));
  252. break;
  253. }
  254. }
  255. }
  256. var checkbox = new CheckBox
  257. {
  258. CanFocus = true,
  259. Title = nameWithHotKey,
  260. Id = name,
  261. Data = flag,
  262. HighlightStates = ViewBase.MouseState.In
  263. };
  264. checkbox.GettingAttributeForRole += (_, e) =>
  265. {
  266. if (SuperView is { HasFocus: false })
  267. {
  268. return;
  269. }
  270. switch (e.Role)
  271. {
  272. case VisualRole.Normal:
  273. e.Handled = true;
  274. if (!HasFocus)
  275. {
  276. e.Result = GetAttributeForRole (VisualRole.Focus);
  277. }
  278. else
  279. {
  280. // If _scheme was set, it's because of Hover
  281. if (checkbox.HasScheme)
  282. {
  283. e.Result = checkbox.GetAttributeForRole (VisualRole.Normal);
  284. }
  285. else
  286. {
  287. e.Result = GetAttributeForRole (VisualRole.Normal);
  288. }
  289. }
  290. break;
  291. case VisualRole.HotNormal:
  292. e.Handled = true;
  293. if (!HasFocus)
  294. {
  295. e.Result = GetAttributeForRole (VisualRole.HotFocus);
  296. }
  297. else
  298. {
  299. e.Result = GetAttributeForRole (VisualRole.HotNormal);
  300. }
  301. break;
  302. }
  303. };
  304. //checkbox.GettingFocusColor += (_, e) =>
  305. // {
  306. // if (SuperView is { HasFocus: true })
  307. // {
  308. // e.Cancel = true;
  309. // if (!HasFocus)
  310. // {
  311. // e.NewValue = GetAttributeForRole (VisualRole.Normal);
  312. // }
  313. // else
  314. // {
  315. // e.NewValue = GetAttributeForRole (VisualRole.Focus);
  316. // }
  317. // }
  318. // };
  319. checkbox.Selecting += (sender, args) =>
  320. {
  321. if (RaiseSelecting (args.Context) is true)
  322. {
  323. args.Handled = true;
  324. return;
  325. }
  326. ;
  327. if (RaiseAccepting (args.Context) is true)
  328. {
  329. args.Handled = true;
  330. }
  331. };
  332. checkbox.CheckedStateChanged += (sender, args) =>
  333. {
  334. uint? newValue = Value;
  335. if (checkbox.CheckedState == CheckState.Checked)
  336. {
  337. if (flag == default!)
  338. {
  339. newValue = 0;
  340. }
  341. else
  342. {
  343. newValue = newValue | flag;
  344. }
  345. }
  346. else
  347. {
  348. newValue = newValue & ~flag;
  349. }
  350. Value = newValue;
  351. };
  352. return checkbox;
  353. }
  354. private void SetLayout ()
  355. {
  356. foreach (View sv in SubViews)
  357. {
  358. if (Orientation == Orientation.Vertical)
  359. {
  360. sv.X = 0;
  361. sv.Y = Pos.Align (Alignment.Start);
  362. }
  363. else
  364. {
  365. sv.X = Pos.Align (Alignment.Start);
  366. sv.Y = 0;
  367. sv.Margin!.Thickness = new (0, 0, 1, 0);
  368. }
  369. }
  370. }
  371. private void UncheckAll ()
  372. {
  373. foreach (CheckBox cb in SubViews.OfType<CheckBox> ().Where (sv => (uint)(sv.Data ?? default!) != default!))
  374. {
  375. cb.CheckedState = CheckState.UnChecked;
  376. }
  377. }
  378. private void UncheckNone ()
  379. {
  380. foreach (CheckBox cb in SubViews.OfType<CheckBox> ().Where (sv => sv.Title != "None"))
  381. {
  382. cb.CheckedState = CheckState.UnChecked;
  383. }
  384. }
  385. private void UpdateChecked ()
  386. {
  387. foreach (CheckBox cb in SubViews.OfType<CheckBox> ())
  388. {
  389. var flag = (uint)(cb.Data ?? throw new InvalidOperationException ("ComboBox.Data must be set"));
  390. // If this flag is set in Value, check the checkbox. Otherwise, uncheck it.
  391. if (flag == 0 && Value != 0)
  392. {
  393. cb.CheckedState = CheckState.UnChecked;
  394. }
  395. else
  396. {
  397. cb.CheckedState = (Value & flag) == flag ? CheckState.Checked : CheckState.UnChecked;
  398. }
  399. }
  400. }
  401. #region IOrientation
  402. /// <summary>
  403. /// Gets or sets the <see cref="Orientation"/> for this <see cref="RadioGroup"/>. The default is
  404. /// <see cref="Orientation.Vertical"/>.
  405. /// </summary>
  406. public Orientation Orientation
  407. {
  408. get => _orientationHelper.Orientation;
  409. set => _orientationHelper.Orientation = value;
  410. }
  411. private readonly OrientationHelper _orientationHelper;
  412. #pragma warning disable CS0067 // The event is never used
  413. /// <inheritdoc/>
  414. public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
  415. /// <inheritdoc/>
  416. public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
  417. #pragma warning restore CS0067 // The event is never used
  418. /// <summary>Called when <see cref="Orientation"/> has changed.</summary>
  419. /// <param name="newOrientation"></param>
  420. public void OnOrientationChanged (Orientation newOrientation) { SetLayout (); }
  421. #endregion IOrientation
  422. /// <inheritdoc/>
  423. public bool EnableForDesign ()
  424. {
  425. Styles = FlagSelectorStyles.All;
  426. SetFlags<FlagSelectorStyles> (
  427. f => f switch
  428. {
  429. FlagSelectorStyles.None => "_No Style",
  430. FlagSelectorStyles.ShowNone => "_Show None Value Style",
  431. FlagSelectorStyles.ShowValueEdit => "Show _Value Editor Style",
  432. FlagSelectorStyles.All => "_All Styles",
  433. _ => f.ToString ()
  434. });
  435. return true;
  436. }
  437. }