#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;
}
}