namespace Terminal.Gui;
/// Displays a group of labels each with a selected indicator. Only one of those can be selected at a given time.
public class RadioGroup : View
{
private int _cursor;
private List<(int pos, int length)> _horizontal;
private int _horizontalSpace = 2;
private Orientation _orientation = Orientation.Vertical;
private List _radioLabels = [];
private int _selected;
///
/// Initializes a new instance of the class using
/// layout.
///
public RadioGroup ()
{
CanFocus = true;
// Things this view knows how to do
AddCommand (
Command.LineUp,
() =>
{
MoveUp ();
return true;
}
);
AddCommand (
Command.LineDown,
() =>
{
MoveDown ();
return true;
}
);
AddCommand (
Command.TopHome,
() =>
{
MoveHome ();
return true;
}
);
AddCommand (
Command.BottomEnd,
() =>
{
MoveEnd ();
return true;
}
);
AddCommand (
Command.Accept,
() =>
{
SelectItem ();
return !OnAccept ();
}
);
// Default keybindings for this view
KeyBindings.Add (Key.CursorUp, Command.LineUp);
KeyBindings.Add (Key.CursorDown, Command.LineDown);
KeyBindings.Add (Key.Home, Command.TopHome);
KeyBindings.Add (Key.End, Command.BottomEnd);
KeyBindings.Add (Key.Space, Command.Accept);
LayoutStarted += RadioGroup_LayoutStarted;
HighlightStyle = Gui.HighlightStyle.PressedOutside | Gui.HighlightStyle.Pressed;
MouseClick += RadioGroup_MouseClick;
}
// TODO: Fix InvertColorsOnPress - only highlight the selected item
private void RadioGroup_MouseClick (object sender, MouseEventEventArgs e)
{
SetFocus ();
int boundsX = e.MouseEvent.X;
int boundsY = e.MouseEvent.Y;
int pos = _orientation == Orientation.Horizontal ? boundsX : boundsY;
int rCount = _orientation == Orientation.Horizontal
? _horizontal.Last ().pos + _horizontal.Last ().length
: _radioLabels.Count;
if (pos < rCount)
{
int c = _orientation == Orientation.Horizontal
? _horizontal.FindIndex (x => x.pos <= boundsX && x.pos + x.length - 2 >= boundsX)
: boundsY;
if (c > -1)
{
_cursor = SelectedItem = c;
SetNeedsDisplay ();
}
}
e.Handled = true;
}
///
/// Gets or sets the horizontal space for this if the is
///
///
public int HorizontalSpace
{
get => _horizontalSpace;
set
{
if (_horizontalSpace != value && _orientation == Orientation.Horizontal)
{
_horizontalSpace = value;
SetWidthHeight (_radioLabels);
UpdateTextFormatterText ();
SetNeedsDisplay ();
}
}
}
///
/// Gets or sets the for this . The default is
/// .
///
public Orientation Orientation
{
get => _orientation;
set => OnOrientationChanged (value);
}
///
/// The radio labels to display. A key binding will be added for each radio radio enabling the user to select
/// and/or focus the radio label using the keyboard. See for details on how HotKeys work.
///
/// The radio labels.
public string [] RadioLabels
{
get => _radioLabels.ToArray ();
set
{
// Remove old hot key bindings
foreach (string label in _radioLabels)
{
if (TextFormatter.FindHotKey (label, HotKeySpecifier, out _, out Key hotKey))
{
AddKeyBindingsForHotKey (hotKey, Key.Empty);
}
}
int prevCount = _radioLabels.Count;
_radioLabels = value.ToList ();
foreach (string label in _radioLabels)
{
if (TextFormatter.FindHotKey (label, HotKeySpecifier, out _, out Key hotKey))
{
AddKeyBindingsForHotKey (Key.Empty, hotKey);
}
}
if (IsInitialized && prevCount != _radioLabels.Count)
{
SetWidthHeight (_radioLabels);
}
SelectedItem = 0;
SetNeedsDisplay ();
}
}
/// The currently selected item from the list of radio labels
/// The selected.
public int SelectedItem
{
get => _selected;
set
{
OnSelectedItemChanged (value, SelectedItem);
_cursor = Math.Max (_selected, 0);
SetNeedsDisplay ();
}
}
///
public override void OnDrawContent (Rectangle viewport)
{
base.OnDrawContent (viewport);
Driver.SetAttribute (GetNormalColor ());
for (var i = 0; i < _radioLabels.Count; i++)
{
switch (Orientation)
{
case Orientation.Vertical:
Move (0, i);
break;
case Orientation.Horizontal:
Move (_horizontal [i].pos, 0);
break;
}
string rl = _radioLabels [i];
Driver.SetAttribute (GetNormalColor ());
Driver.AddStr ($"{(i == _selected ? Glyphs.Selected : Glyphs.UnSelected)} ");
TextFormatter.FindHotKey (rl, HotKeySpecifier, out int hotPos, out Key hotKey);
if (hotPos != -1 && hotKey != Key.Empty)
{
Rune [] rlRunes = rl.ToRunes ();
for (var j = 0; j < rlRunes.Length; j++)
{
Rune rune = rlRunes [j];
if (j == hotPos && i == _cursor)
{
Application.Driver.SetAttribute (
HasFocus
? ColorScheme.HotFocus
: GetHotNormalColor ()
);
}
else if (j == hotPos && i != _cursor)
{
Application.Driver.SetAttribute (GetHotNormalColor ());
}
else if (HasFocus && i == _cursor)
{
Application.Driver.SetAttribute (ColorScheme.Focus);
}
if (rune == HotKeySpecifier && j + 1 < rlRunes.Length)
{
j++;
rune = rlRunes [j];
if (i == _cursor)
{
Application.Driver.SetAttribute (
HasFocus
? ColorScheme.HotFocus
: GetHotNormalColor ()
);
}
else if (i != _cursor)
{
Application.Driver.SetAttribute (GetHotNormalColor ());
}
}
Application.Driver.AddRune (rune);
Driver.SetAttribute (GetNormalColor ());
}
}
else
{
DrawHotString (rl, HasFocus && i == _cursor, ColorScheme);
}
}
}
///
public override bool? OnInvokingKeyBindings (Key keyEvent)
{
// This is a bit of a hack. We want to handle the key bindings for the radio group but
// InvokeKeyBindings doesn't pass any context so we can't tell if the key binding is for
// the radio group or for one of the radio buttons. So before we call the base class
// we set SelectedItem appropriately.
Key key = keyEvent;
if (KeyBindings.TryGet (key, out _))
{
// Search RadioLabels
for (var i = 0; i < _radioLabels.Count; i++)
{
if (TextFormatter.FindHotKey (
_radioLabels [i],
HotKeySpecifier,
out _,
out Key hotKey,
true
)
&& key.NoAlt.NoCtrl.NoShift == hotKey)
{
SelectedItem = i;
break;
}
}
}
return base.OnInvokingKeyBindings (keyEvent);
}
/// Called when the view orientation has changed. Invokes the event.
///
/// True of the event was cancelled.
public virtual bool OnOrientationChanged (Orientation newOrientation)
{
var args = new OrientationEventArgs (newOrientation);
OrientationChanged?.Invoke (this, args);
if (!args.Cancel)
{
_orientation = newOrientation;
SetNeedsLayout ();
}
return args.Cancel;
}
// TODO: This should be cancelable
/// Called whenever the current selected item changes. Invokes the event.
///
///
public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem)
{
_selected = selectedItem;
SelectedItemChanged?.Invoke (this, new SelectedItemChangedArgs (selectedItem, previousSelectedItem));
}
///
/// Fired when the view orientation has changed. Can be cancelled by setting
/// to true.
///
public event EventHandler OrientationChanged;
///
public override Point? PositionCursor ()
{
int x = 0;
int y = 0;
switch (Orientation)
{
case Orientation.Vertical:
y = _cursor;
break;
case Orientation.Horizontal:
x = _horizontal [_cursor].pos;
break;
default:
return null;
}
Move (x, y);
return null; // Don't show the cursor
}
/// Allow to invoke the after their creation.
public void Refresh () { OnSelectedItemChanged (_selected, -1); }
// TODO: This should use StateEventArgs and should be cancelable.
/// Invoked when the selected radio label has changed.
public event EventHandler SelectedItemChanged;
private void CalculateHorizontalPositions ()
{
if (_orientation == Orientation.Horizontal)
{
_horizontal = new List<(int pos, int length)> ();
var start = 0;
var length = 0;
for (var i = 0; i < _radioLabels.Count; i++)
{
start += length;
length = _radioLabels [i].GetColumns () + 2 + (i < _radioLabels.Count - 1 ? _horizontalSpace : 0);
_horizontal.Add ((start, length));
}
}
}
private static Rectangle MakeRect (int x, int y, List radioLabels)
{
if (radioLabels is null)
{
return new (x, y, 0, 0);
}
var width = 0;
foreach (string s in radioLabels)
{
width = Math.Max (s.GetColumns () + 2, width);
}
return new (x, y, width, radioLabels.Count);
}
private void MoveDown ()
{
if (_cursor + 1 < _radioLabels.Count)
{
_cursor++;
SetNeedsDisplay ();
}
else if (_cursor > 0)
{
_cursor = 0;
SetNeedsDisplay ();
}
}
private void MoveEnd () { _cursor = Math.Max (_radioLabels.Count - 1, 0); }
private void MoveHome () { _cursor = 0; }
private void MoveUp ()
{
if (_cursor > 0)
{
_cursor--;
SetNeedsDisplay ();
}
else if (_radioLabels.Count - 1 > 0)
{
_cursor = _radioLabels.Count - 1;
SetNeedsDisplay ();
}
}
private void RadioGroup_LayoutStarted (object sender, EventArgs e) { SetWidthHeight (_radioLabels); }
private void SelectItem () { SelectedItem = _cursor; }
private void SetWidthHeight (List radioLabels)
{
switch (_orientation)
{
case Orientation.Vertical:
Rectangle r = MakeRect (0, 0, radioLabels);
if (IsInitialized)
{
Width = r.Width + GetAdornmentsThickness ().Horizontal;
Height = radioLabels.Count + GetAdornmentsThickness ().Vertical;
}
break;
case Orientation.Horizontal:
CalculateHorizontalPositions ();
var length = 0;
foreach ((int pos, int length) item in _horizontal)
{
length += item.length;
}
if (IsInitialized)
{
Width = length + GetAdornmentsThickness ().Vertical;
Height = 1 + GetAdornmentsThickness ().Horizontal;
}
break;
}
}
}