123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- namespace Terminal.Gui;
- /// <summary>Displays a group of labels each with a selected indicator. Only one of those can be selected at a given time.</summary>
- 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<string> _radioLabels = [];
- private int _selected;
- /// <summary>
- /// Initializes a new instance of the <see cref="RadioGroup"/> class using <see cref="LayoutStyle.Computed"/>
- /// layout.
- /// </summary>
- 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;
- }
- /// <summary>
- /// Gets or sets the horizontal space for this <see cref="RadioGroup"/> if the <see cref="Orientation"/> is
- /// <see cref="Orientation.Horizontal"/>
- /// </summary>
- public int HorizontalSpace
- {
- get => _horizontalSpace;
- set
- {
- if (_horizontalSpace != value && _orientation == Orientation.Horizontal)
- {
- _horizontalSpace = value;
- SetWidthHeight (_radioLabels);
- UpdateTextFormatterText ();
- SetNeedsDisplay ();
- }
- }
- }
- /// <summary>
- /// Gets or sets the <see cref="Orientation"/> for this <see cref="RadioGroup"/>. The default is
- /// <see cref="Orientation.Vertical"/>.
- /// </summary>
- public Orientation Orientation
- {
- get => _orientation;
- set => OnOrientationChanged (value);
- }
- /// <summary>
- /// 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 <see cref="View.HotKey"/> for details on how HotKeys work.
- /// </summary>
- /// <value>The radio labels.</value>
- 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 ();
- }
- }
- /// <summary>The currently selected item from the list of radio labels</summary>
- /// <value>The selected.</value>
- public int SelectedItem
- {
- get => _selected;
- set
- {
- OnSelectedItemChanged (value, SelectedItem);
- _cursor = Math.Max (_selected, 0);
- SetNeedsDisplay ();
- }
- }
- /// <inheritdoc/>
- protected internal override bool OnMouseEvent (MouseEvent me)
- {
- if (!me.Flags.HasFlag (MouseFlags.Button1Clicked))
- {
- return false;
- }
- if (!CanFocus)
- {
- return false;
- }
- SetFocus ();
- int boundsX = me.X;
- int boundsY = me.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 ();
- }
- }
- return true;
- }
- /// <inheritdoc/>
- 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);
- }
- }
- }
- /// <inheritdoc/>
- public override bool OnEnter (View view)
- {
- Application.Driver.SetCursorVisibility (CursorVisibility.Invisible);
- return base.OnEnter (view);
- }
- /// <inheritdoc/>
- 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);
- }
- /// <summary>Called when the view orientation has changed. Invokes the <see cref="OrientationChanged"/> event.</summary>
- /// <param name="newOrientation"></param>
- /// <returns>True of the event was cancelled.</returns>
- 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
- /// <summary>Called whenever the current selected item changes. Invokes the <see cref="SelectedItemChanged"/> event.</summary>
- /// <param name="selectedItem"></param>
- /// <param name="previousSelectedItem"></param>
- public virtual void OnSelectedItemChanged (int selectedItem, int previousSelectedItem)
- {
- _selected = selectedItem;
- SelectedItemChanged?.Invoke (this, new SelectedItemChangedArgs (selectedItem, previousSelectedItem));
- }
- /// <summary>
- /// Fired when the view orientation has changed. Can be cancelled by setting
- /// <see cref="OrientationEventArgs.Cancel"/> to true.
- /// </summary>
- public event EventHandler<OrientationEventArgs> OrientationChanged;
- /// <inheritdoc/>
- public override void PositionCursor ()
- {
- switch (Orientation)
- {
- case Orientation.Vertical:
- Move (0, _cursor);
- break;
- case Orientation.Horizontal:
- Move (_horizontal [_cursor].pos, 0);
- break;
- }
- }
- /// <summary>Allow to invoke the <see cref="SelectedItemChanged"/> after their creation.</summary>
- public void Refresh () { OnSelectedItemChanged (_selected, -1); }
- // TODO: This should use StateEventArgs<int> and should be cancelable.
- /// <summary>Invoked when the selected radio label has changed.</summary>
- public event EventHandler<SelectedItemChangedArgs> 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<string> 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<string> 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;
- }
- }
- }
|