#nullable enable namespace Terminal.Gui.Views; /// /// 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. /// public class FlagSelector : View, IOrientation, IDesignable { /// /// Initializes a new instance of the class. /// 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; /// /// Gets or sets the value of the selected flags. /// 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 (Value.Value)); } } /// /// Called when has changed. /// protected virtual void OnValueChanged () { } /// /// Raised when has changed. /// public event EventHandler>? ValueChanged; private FlagSelectorStyles _styles; /// /// Gets or sets the styles for the flag selector. /// public FlagSelectorStyles Styles { get => _styles; set { if (_styles == value) { return; } _styles = value; CreateCheckBoxes (); } } /// /// Set the flags and flag names. /// /// public virtual void SetFlags (IReadOnlyDictionary flags) { Flags = flags; CreateCheckBoxes (); UpdateChecked (); } /// /// Set the flags and flag names from an enum type. /// /// The enum type to extract flags from /// /// 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. /// public void SetFlags () where TEnum : struct, Enum { // Convert enum names and values to a dictionary Dictionary flagsDictionary = Enum.GetValues () .ToDictionary ( f => Convert.ToUInt32 (f), f => f.ToString () ); SetFlags (flagsDictionary); } /// /// Set the flags and flag names from an enum type with custom display names. /// /// The enum type to extract flags from /// A function that converts enum values to display names /// /// 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. /// /// /// /// // 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() /// }); /// /// public void SetFlags (Func nameSelector) where TEnum : struct, Enum { // Convert enum values and custom names to a dictionary Dictionary flagsDictionary = Enum.GetValues () .ToDictionary ( f => Convert.ToUInt32 (f), nameSelector ); SetFlags (flagsDictionary); } private IReadOnlyDictionary? _flags; /// /// Gets the flag values and names. /// public IReadOnlyDictionary? 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; /// /// If the CheckBoxes will each be automatically assigned a hotkey. /// will be used to ensure unique keys are assigned. Set /// before setting with any hotkeys that may conflict with other Views. /// public bool AssignHotKeysToCheckBoxes { get => _assignHotKeysToCheckBoxes; set { if (_assignHotKeysToCheckBoxes == value) { return; } _assignHotKeysToCheckBoxes = value; CreateCheckBoxes (); UpdateChecked (); } } /// /// Gets the list of hotkeys already used by the CheckBoxes or that should not be used if /// /// is enabled. /// public List UsedHotKeys { get; } = []; private void CreateCheckBoxes () { if (Flags is null) { return; } foreach (CheckBox cb in RemoveAll ()) { 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; } /// /// /// /// /// /// 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 ().Where (sv => (uint)(sv.Data ?? default!) != default!)) { cb.CheckedState = CheckState.UnChecked; } } private void UncheckNone () { foreach (CheckBox cb in SubViews.OfType ().Where (sv => sv.Title != "None")) { cb.CheckedState = CheckState.UnChecked; } } private void UpdateChecked () { foreach (CheckBox cb in SubViews.OfType ()) { 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 /// /// Gets or sets the for this . The default is /// . /// public Orientation Orientation { get => _orientationHelper.Orientation; set => _orientationHelper.Orientation = value; } private readonly OrientationHelper _orientationHelper; #pragma warning disable CS0067 // The event is never used /// public event EventHandler>? OrientationChanging; /// public event EventHandler>? OrientationChanged; #pragma warning restore CS0067 // The event is never used /// Called when has changed. /// public void OnOrientationChanged (Orientation newOrientation) { SetLayout (); } #endregion IOrientation /// public bool EnableForDesign () { Styles = FlagSelectorStyles.All; SetFlags ( 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; } }