| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513 |
- #nullable enable
- namespace Terminal.Gui.Views;
- /// <summary>
- /// Provides a user interface for displaying and selecting non-mutually-exclusive flags.
- /// Flags can be set from a dictionary or directly from an enum type.
- /// </summary>
- public class FlagSelector : View, IOrientation, IDesignable
- {
- /// <summary>
- /// Initializes a new instance of the <see cref="FlagSelector"/> class.
- /// </summary>
- public FlagSelector ()
- {
- CanFocus = true;
- Width = Dim.Auto (DimAutoStyle.Content);
- Height = Dim.Auto (DimAutoStyle.Content);
- // ReSharper disable once UseObjectOrCollectionInitializer
- _orientationHelper = new (this);
- _orientationHelper.Orientation = Orientation.Vertical;
- // Accept (Enter key or DoubleClick) - Raise Accept event - DO NOT advance state
- AddCommand (Command.Accept, HandleAcceptCommand);
- CreateCheckBoxes ();
- }
- private bool? HandleAcceptCommand (ICommandContext? ctx) { return RaiseAccepting (ctx); }
- private uint? _value;
- /// <summary>
- /// Gets or sets the value of the selected flags.
- /// </summary>
- public uint? Value
- {
- get => _value;
- set
- {
- if (_value == value)
- {
- return;
- }
- _value = value;
- if (_value is null)
- {
- UncheckNone ();
- UncheckAll ();
- }
- else
- {
- UpdateChecked ();
- }
- if (ValueEdit is { })
- {
- ValueEdit.Text = _value.ToString ();
- }
- RaiseValueChanged ();
- }
- }
- private void RaiseValueChanged ()
- {
- OnValueChanged ();
- if (Value.HasValue)
- {
- ValueChanged?.Invoke (this, new EventArgs<uint> (Value.Value));
- }
- }
- /// <summary>
- /// Called when <see cref="Value"/> has changed.
- /// </summary>
- protected virtual void OnValueChanged () { }
- /// <summary>
- /// Raised when <see cref="Value"/> has changed.
- /// </summary>
- public event EventHandler<EventArgs<uint>>? ValueChanged;
- private FlagSelectorStyles _styles;
- /// <summary>
- /// Gets or sets the styles for the flag selector.
- /// </summary>
- public FlagSelectorStyles Styles
- {
- get => _styles;
- set
- {
- if (_styles == value)
- {
- return;
- }
- _styles = value;
- CreateCheckBoxes ();
- }
- }
- /// <summary>
- /// Set the flags and flag names.
- /// </summary>
- /// <param name="flags"></param>
- public virtual void SetFlags (IReadOnlyDictionary<uint, string> flags)
- {
- Flags = flags;
- CreateCheckBoxes ();
- UpdateChecked ();
- }
- /// <summary>
- /// Set the flags and flag names from an enum type.
- /// </summary>
- /// <typeparam name="TEnum">The enum type to extract flags from</typeparam>
- /// <remarks>
- /// This is a convenience method that converts an enum to a dictionary of flag values and names.
- /// The enum values are converted to uint values and the enum names become the display text.
- /// </remarks>
- public void SetFlags<TEnum> () where TEnum : struct, Enum
- {
- // Convert enum names and values to a dictionary
- Dictionary<uint, string> flagsDictionary = Enum.GetValues<TEnum> ()
- .ToDictionary (
- f => Convert.ToUInt32 (f),
- f => f.ToString ()
- );
- SetFlags (flagsDictionary);
- }
- /// <summary>
- /// Set the flags and flag names from an enum type with custom display names.
- /// </summary>
- /// <typeparam name="TEnum">The enum type to extract flags from</typeparam>
- /// <param name="nameSelector">A function that converts enum values to display names</param>
- /// <remarks>
- /// This is a convenience method that converts an enum to a dictionary of flag values and custom names.
- /// The enum values are converted to uint values and the display names are determined by the nameSelector function.
- /// </remarks>
- /// <example>
- /// <code>
- /// // Use enum values with custom display names
- /// var flagSelector = new FlagSelector ();
- /// flagSelector.SetFlags<FlagSelectorStyles>
- /// (f => f switch {
- /// FlagSelectorStyles.ShowNone => "Show None Value",
- /// FlagSelectorStyles.ShowValueEdit => "Show Value Editor",
- /// FlagSelectorStyles.All => "Everything",
- /// _ => f.ToString()
- /// });
- /// </code>
- /// </example>
- public void SetFlags<TEnum> (Func<TEnum, string> nameSelector) where TEnum : struct, Enum
- {
- // Convert enum values and custom names to a dictionary
- Dictionary<uint, string> flagsDictionary = Enum.GetValues<TEnum> ()
- .ToDictionary (
- f => Convert.ToUInt32 (f),
- nameSelector
- );
- SetFlags (flagsDictionary);
- }
- private IReadOnlyDictionary<uint, string>? _flags;
- /// <summary>
- /// Gets the flag values and names.
- /// </summary>
- public IReadOnlyDictionary<uint, string>? Flags
- {
- get => _flags;
- internal set
- {
- _flags = value;
- if (_value is null)
- {
- Value = Convert.ToUInt16 (_flags?.Keys.ElementAt (0));
- }
- }
- }
- private TextField? ValueEdit { get; set; }
- private bool _assignHotKeysToCheckBoxes;
- /// <summary>
- /// If <see langword="true"/> the CheckBoxes will each be automatically assigned a hotkey.
- /// <see cref="UsedHotKeys"/> will be used to ensure unique keys are assigned. Set <see cref="UsedHotKeys"/>
- /// before setting <see cref="Flags"/> with any hotkeys that may conflict with other Views.
- /// </summary>
- public bool AssignHotKeysToCheckBoxes
- {
- get => _assignHotKeysToCheckBoxes;
- set
- {
- if (_assignHotKeysToCheckBoxes == value)
- {
- return;
- }
- _assignHotKeysToCheckBoxes = value;
- CreateCheckBoxes ();
- UpdateChecked ();
- }
- }
- /// <summary>
- /// Gets the list of hotkeys already used by the CheckBoxes or that should not be used if
- /// <see cref="AssignHotKeysToCheckBoxes"/>
- /// is enabled.
- /// </summary>
- public List<Key> UsedHotKeys { get; } = [];
- private void CreateCheckBoxes ()
- {
- if (Flags is null)
- {
- return;
- }
- foreach (CheckBox cb in RemoveAll<CheckBox> ())
- {
- cb.Dispose ();
- }
- if (Styles.HasFlag (FlagSelectorStyles.ShowNone) && !Flags.ContainsKey (0))
- {
- Add (CreateCheckBox ("None", 0));
- }
- for (var index = 0; index < Flags.Count; index++)
- {
- if (!Styles.HasFlag (FlagSelectorStyles.ShowNone) && Flags.ElementAt (index).Key == 0)
- {
- continue;
- }
- Add (CreateCheckBox (Flags.ElementAt (index).Value, Flags.ElementAt (index).Key));
- }
- if (Styles.HasFlag (FlagSelectorStyles.ShowValueEdit))
- {
- ValueEdit = new ()
- {
- Id = "valueEdit",
- CanFocus = false,
- Text = Value.ToString (),
- Width = 5,
- ReadOnly = true,
- };
- Add (ValueEdit);
- }
- SetLayout ();
- return;
- }
- /// <summary>
- ///
- /// </summary>
- /// <param name="name"></param>
- /// <param name="flag"></param>
- /// <returns></returns>
- protected virtual CheckBox CreateCheckBox (string name, uint flag)
- {
- string nameWithHotKey = name;
- if (AssignHotKeysToCheckBoxes)
- {
- // Find the first char in label that is [a-z], [A-Z], or [0-9]
- for (var i = 0; i < name.Length; i++)
- {
- char c = char.ToLowerInvariant (name [i]);
- if (UsedHotKeys.Contains (new (c)) || !char.IsAsciiLetterOrDigit (c))
- {
- continue;
- }
- if (char.IsAsciiLetterOrDigit (c))
- {
- char? hotChar = c;
- nameWithHotKey = name.Insert (i, HotKeySpecifier.ToString ());
- UsedHotKeys.Add (new (hotChar));
- break;
- }
- }
- }
- var checkbox = new CheckBox
- {
- CanFocus = true,
- Title = nameWithHotKey,
- Id = name,
- Data = flag,
- HighlightStates = ViewBase.MouseState.In
- };
- checkbox.GettingAttributeForRole += (_, e) =>
- {
- if (SuperView is { HasFocus: false })
- {
- return;
- }
- switch (e.Role)
- {
- case VisualRole.Normal:
- e.Handled = true;
- if (!HasFocus)
- {
- e.Result = GetAttributeForRole (VisualRole.Focus);
- }
- else
- {
- // If _scheme was set, it's because of Hover
- if (checkbox.HasScheme)
- {
- e.Result = checkbox.GetAttributeForRole (VisualRole.Normal);
- }
- else
- {
- e.Result = GetAttributeForRole (VisualRole.Normal);
- }
- }
- break;
- case VisualRole.HotNormal:
- e.Handled = true;
- if (!HasFocus)
- {
- e.Result = GetAttributeForRole (VisualRole.HotFocus);
- }
- else
- {
- e.Result = GetAttributeForRole (VisualRole.HotNormal);
- }
- break;
- }
- };
- //checkbox.GettingFocusColor += (_, e) =>
- // {
- // if (SuperView is { HasFocus: true })
- // {
- // e.Cancel = true;
- // if (!HasFocus)
- // {
- // e.NewValue = GetAttributeForRole (VisualRole.Normal);
- // }
- // else
- // {
- // e.NewValue = GetAttributeForRole (VisualRole.Focus);
- // }
- // }
- // };
- checkbox.Selecting += (sender, args) =>
- {
- if (RaiseSelecting (args.Context) is true)
- {
- args.Handled = true;
- return;
- }
- ;
- if (RaiseAccepting (args.Context) is true)
- {
- args.Handled = true;
- }
- };
- checkbox.CheckedStateChanged += (sender, args) =>
- {
- uint? newValue = Value;
- if (checkbox.CheckedState == CheckState.Checked)
- {
- if (flag == default!)
- {
- newValue = 0;
- }
- else
- {
- newValue = newValue | flag;
- }
- }
- else
- {
- newValue = newValue & ~flag;
- }
- Value = newValue;
- };
- return checkbox;
- }
- private void SetLayout ()
- {
- foreach (View sv in SubViews)
- {
- if (Orientation == Orientation.Vertical)
- {
- sv.X = 0;
- sv.Y = Pos.Align (Alignment.Start);
- }
- else
- {
- sv.X = Pos.Align (Alignment.Start);
- sv.Y = 0;
- sv.Margin!.Thickness = new (0, 0, 1, 0);
- }
- }
- }
- private void UncheckAll ()
- {
- foreach (CheckBox cb in SubViews.OfType<CheckBox> ().Where (sv => (uint)(sv.Data ?? default!) != default!))
- {
- cb.CheckedState = CheckState.UnChecked;
- }
- }
- private void UncheckNone ()
- {
- foreach (CheckBox cb in SubViews.OfType<CheckBox> ().Where (sv => sv.Title != "None"))
- {
- cb.CheckedState = CheckState.UnChecked;
- }
- }
- private void UpdateChecked ()
- {
- foreach (CheckBox cb in SubViews.OfType<CheckBox> ())
- {
- var flag = (uint)(cb.Data ?? throw new InvalidOperationException ("ComboBox.Data must be set"));
- // If this flag is set in Value, check the checkbox. Otherwise, uncheck it.
- if (flag == 0 && Value != 0)
- {
- cb.CheckedState = CheckState.UnChecked;
- }
- else
- {
- cb.CheckedState = (Value & flag) == flag ? CheckState.Checked : CheckState.UnChecked;
- }
- }
- }
- #region IOrientation
- /// <summary>
- /// Gets or sets the <see cref="Orientation"/> for this <see cref="RadioGroup"/>. The default is
- /// <see cref="Orientation.Vertical"/>.
- /// </summary>
- public Orientation Orientation
- {
- get => _orientationHelper.Orientation;
- set => _orientationHelper.Orientation = value;
- }
- private readonly OrientationHelper _orientationHelper;
- #pragma warning disable CS0067 // The event is never used
- /// <inheritdoc/>
- public event EventHandler<CancelEventArgs<Orientation>>? OrientationChanging;
- /// <inheritdoc/>
- public event EventHandler<EventArgs<Orientation>>? OrientationChanged;
- #pragma warning restore CS0067 // The event is never used
- /// <summary>Called when <see cref="Orientation"/> has changed.</summary>
- /// <param name="newOrientation"></param>
- public void OnOrientationChanged (Orientation newOrientation) { SetLayout (); }
- #endregion IOrientation
- /// <inheritdoc/>
- public bool EnableForDesign ()
- {
- Styles = FlagSelectorStyles.All;
- SetFlags<FlagSelectorStyles> (
- f => f switch
- {
- FlagSelectorStyles.None => "_No Style",
- FlagSelectorStyles.ShowNone => "_Show None Value Style",
- FlagSelectorStyles.ShowValueEdit => "Show _Value Editor Style",
- FlagSelectorStyles.All => "_All Styles",
- _ => f.ToString ()
- });
- return true;
- }
- }
|