using System.Collections.Immutable; using System.Diagnostics; namespace Terminal.Gui.Views; // DoubleClick - Focus, Select, and Accept the item under the mouse. // Click - Focus, Select, and do NOT Accept the item under the mouse. // CanFocus - Not Focused: // HotKey - Restore Focus. Advance Active. Do NOT Accept. // Item HotKey - Focus item. If item is not active, make Active. Do NOT Accept. // !CanFocus - Not Focused: // HotKey - Do NOT Restore Focus. Advance Active. Do NOT Accept. // Item HotKey - Do NOT Focus item. If item is not active, make Active. Do NOT Accept. // Focused: // Space key - If focused item is Active, move focus to and Acivate next. Else, Select current. Do NOT Accept. // Enter key - Select and Accept the focused item. // HotKey - Restore Focus. Advance Active. Do NOT Accept. // Item HotKey - If item is not active, make Active. Do NOT Accept. /// /// 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. /// provides a type-safe version where a can be /// provided. /// public class OptionSelector : SelectorBase, IDesignable { /// public OptionSelector () { // By default, for OptionSelector, Value is set to 0. It can be set to null if a developer // really wants that. base.Value = 0; } /// protected override bool OnHandlingHotKey (CommandEventArgs args) { if (base.OnHandlingHotKey (args) is true) { return true; } if (!CanFocus) { if (RaiseSelecting (args.Context) is true) { return true; } } else if (!HasFocus && Value is null) { if (RaiseSelecting (args.Context) is true) { return true; } SetFocus (); Value = Values? [0]; return true; } return false; } /// protected override bool OnSelecting (CommandEventArgs args) { if (base.OnSelecting (args) is true) { return true; } if (!CanFocus || args.Context?.Source is not CheckBox checkBox) { Cycle (); return false; } if (args.Context is CommandContext { } && (int)checkBox.Data! == Value) { // Caused by keypress. If the checkbox is already checked, we cycle to the next one. Cycle (); } else { if (Value == (int)checkBox.Data!) { return true; } Value = (int)checkBox.Data!; // if (HasFocus) { UpdateChecked (); } } return false; } /// protected override void OnSubViewAdded (View view) { base.OnSubViewAdded (view); if (view is not CheckBox checkbox) { return; } checkbox.RadioStyle = true; checkbox.Selecting += OnCheckboxOnSelecting; checkbox.Accepting += OnCheckboxOnAccepting; } private void OnCheckboxOnSelecting (object? sender, CommandEventArgs args) { if (sender is not CheckBox checkbox) { return; } // Verify at most one is checked Debug.Assert (SubViews.OfType ().Count (cb => cb.CheckedState == CheckState.Checked) <= 1); if (args.Context is CommandContext { } && checkbox.CheckedState == CheckState.Checked) { // If user clicks with mouse and item is already checked, do nothing args.Handled = true; return; } if (args.Context is CommandContext binding && binding.Command == Command.HotKey && checkbox.CheckedState == CheckState.Checked) { // If user uses an item hotkey and the item is already checked, do nothing args.Handled = true; return; } if (checkbox.CanFocus) { // For Select, if the view is focusable and SetFocus succeeds, by defition, // the event is handled. So return what SetFocus returns. checkbox.SetFocus (); } // Selecting doesn't normally propogate, so we do it here if (InvokeCommand (Command.Select, args.Context) is true) { // Do not return here; we want to toggle the checkbox state args.Handled = true; return; } args.Handled = true; } private void OnCheckboxOnAccepting (object? sender, CommandEventArgs args) { if (sender is not CheckBox checkbox) { return; } Value = (int)checkbox.Data!; args.Handled = false; // Do not set to false; let Accepting propagate } private void Cycle () { int valueIndex = Values.IndexOf (v => v == Value); Value = valueIndex == Values?.Count () - 1 ? Values! [0] : Values! [valueIndex + 1]; if (HasFocus) { valueIndex = Values.IndexOf (v => v == Value); SubViews.OfType ().ToArray () [valueIndex].SetFocus (); } // Verify at most one is checked Debug.Assert (SubViews.OfType ().Count (cb => cb.CheckedState == CheckState.Checked) <= 1); } /// /// Updates the checked state of all checkbox subviews so that only the checkbox corresponding /// to the current is checked. Throws /// if a checkbox's Data property is not set. /// /// public override void UpdateChecked () { foreach (CheckBox cb in SubViews.OfType ()) { int value = (int)(cb.Data ?? throw new InvalidOperationException ("CheckBox.Data must be set")); cb.CheckedState = value == Value ? CheckState.Checked : CheckState.UnChecked; } // Verify at most one is checked Debug.Assert (SubViews.OfType ().Count (cb => cb.CheckedState == CheckState.Checked) <= 1); } /// /// Gets or sets the index for the cursor. The cursor may or may not be the selected /// RadioItem. /// /// /// /// Maps to either the X or Y position within depending on . /// /// public int Cursor { get => !CanFocus ? 0 : SubViews.OfType ().ToArray ().IndexOf (Focused); set { if (!CanFocus) { return; } CheckBox [] checkBoxes = SubViews.OfType ().ToArray (); if (value < 0 || value >= checkBoxes.Length) { throw new ArgumentOutOfRangeException (nameof (value), @"Cursor index is out of range"); } checkBoxes [value].SetFocus (); } } /// public bool EnableForDesign () { AssignHotKeys = true; Labels = ["Option 1", "Option 2", "Third Option", "Option Quattro"]; return true; } }