#nullable enable using System.Diagnostics; namespace Terminal.Gui.Views; /// /// Provides a user interface for displaying and selecting a single item from a list of options. /// Each option is represented by a checkbox, but only one can be selected at a time. /// public class OptionSelector : View, IOrientation, IDesignable { /// /// Initializes a new instance of the class. /// public OptionSelector () { CanFocus = true; Width = Dim.Auto (DimAutoStyle.Content); Height = Dim.Auto (DimAutoStyle.Content); _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 int? _selectedItem; /// /// Gets or sets the index of the selected item. Will be if no item is selected. /// public int? SelectedItem { get => _selectedItem; set { if (value < 0 || value >= SubViews.OfType ().Count ()) { throw new ArgumentOutOfRangeException (nameof (value), @$"SelectedItem must be between 0 and {SubViews.OfType ().Count ()-1}"); } if (_selectedItem == value) { return; } int? previousSelectedItem = _selectedItem; _selectedItem = value; UpdateChecked (); RaiseSelectedItemChanged (previousSelectedItem); } } private void RaiseSelectedItemChanged (int? previousSelectedItem) { OnSelectedItemChanged (SelectedItem, previousSelectedItem); if (SelectedItem.HasValue) { SelectedItemChanged?.Invoke (this, new (SelectedItem, previousSelectedItem)); } } /// /// Called when has changed. /// protected virtual void OnSelectedItemChanged (int? selectedItem, int? previousSelectedItem) { } /// /// Raised when has changed. /// public event EventHandler? SelectedItemChanged; private IReadOnlyList? _options; /// /// Gets or sets the list of options. /// public IReadOnlyList? Options { get => _options; set { _options = value; CreateCheckBoxes (); } } 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; } = new (); private void CreateCheckBoxes () { if (Options is null) { return; } foreach (CheckBox cb in RemoveAll ()) { cb.Dispose (); } for (var index = 0; index < Options.Count; index++) { Add (CreateCheckBox (Options [index], index)); } SetLayout (); } /// /// /// /// /// /// protected virtual CheckBox CreateCheckBox (string name, int index) { 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 = index, //HighlightStates = HighlightStates.Hover, RadioStyle = true }; 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.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) => { if (checkbox.CheckedState == CheckState.Checked) { SelectedItem = index; } }; 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 UpdateChecked () { foreach (CheckBox cb in SubViews.OfType ()) { var index = (int)(cb.Data ?? throw new InvalidOperationException ("CheckBox.Data must be set")); cb.CheckedState = index == SelectedItem ? 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 () { AssignHotKeysToCheckBoxes = true; Options = ["Option 1", "Option 2", "Third Option", "Option Quattro"]; return true; } }