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, Activate current. Do NOT Accept.
// Enter key - Activate 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 (RaiseActivating (args.Context) is true)
{
return true;
}
}
else if (!HasFocus && Value is null)
{
if (RaiseActivating (args.Context) is true)
{
return true;
}
SetFocus ();
Value = Values? [0];
return true;
}
return false;
}
///
protected override bool OnActivating (CommandEventArgs args)
{
if (base.OnActivating (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.Activating += OnCheckboxOnActivating;
checkbox.Accepting += OnCheckboxOnAccepting;
}
private void OnCheckboxOnActivating (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 propagate, so we do it here
if (InvokeCommand (Command.Activate, 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;
}
}