using System.Text; using System; using System.Collections.Generic; using System.Linq; 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 { int _selected = -1; int _cursor; DisplayModeLayout _displayMode; int _horizontalSpace = 2; List<(int pos, int length)> _horizontal; /// /// Initializes a new instance of the class using layout. /// public RadioGroup () : this (radioLabels: new string [] { }) { } /// /// Initializes a new instance of the class using layout. /// /// The radio labels; an array of strings that can contain hotkeys using an underscore before the letter. /// The index of the item to be selected, the value is clamped to the number of items. public RadioGroup (string [] radioLabels, int selected = 0) : base () { SetInitialProperties (Rect.Empty, radioLabels, selected); } /// /// Initializes a new instance of the class using layout. /// /// Boundaries for the radio group. /// The radio labels; an array of strings that can contain hotkeys using an underscore before the letter. /// The index of item to be selected, the value is clamped to the number of items. public RadioGroup (Rect rect, string [] radioLabels, int selected = 0) : base (rect) { SetInitialProperties (rect, radioLabels, selected); } /// /// Initializes a new instance of the class using layout. /// The frame is computed from the provided radio labels. /// /// The x coordinate. /// The y coordinate. /// The radio labels; an array of strings that can contain hotkeys using an underscore before the letter. /// The item to be selected, the value is clamped to the number of items. public RadioGroup (int x, int y, string [] radioLabels, int selected = 0) : this (MakeRect (x, y, radioLabels != null ? radioLabels.ToList () : null), radioLabels, selected) { } void SetInitialProperties (Rect rect, string [] radioLabels, int selected) { HotKeySpecifier = new Rune ('_'); if (radioLabels != null) { RadioLabels = radioLabels; } _selected = selected; Frame = rect; 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 true; }); // Default keybindings for this view KeyBindings.Add (KeyCode.CursorUp, Command.LineUp); KeyBindings.Add (KeyCode.CursorDown, Command.LineDown); KeyBindings.Add (KeyCode.Home, Command.TopHome); KeyBindings.Add (KeyCode.End, Command.BottomEnd); KeyBindings.Add (KeyCode.Space, Command.Accept); LayoutStarted += RadioGroup_LayoutStarted; } void RadioGroup_LayoutStarted (object sender, EventArgs e) { SetWidthHeight (_radioLabels); } /// /// Gets or sets the for this . /// public DisplayModeLayout DisplayMode { get { return _displayMode; } set { if (_displayMode != value) { _displayMode = value; SetWidthHeight (_radioLabels); SetNeedsDisplay (); } } } /// /// Gets or sets the horizontal space for this if the is /// public int HorizontalSpace { get { return _horizontalSpace; } set { if (_horizontalSpace != value && _displayMode == DisplayModeLayout.Horizontal) { _horizontalSpace = value; SetWidthHeight (_radioLabels); UpdateTextFormatterText (); SetNeedsDisplay (); } } } void SetWidthHeight (List radioLabels) { switch (_displayMode) { case DisplayModeLayout.Vertical: var r = MakeRect (0, 0, radioLabels); Bounds = new Rect (Bounds.Location, new Size (r.Width, radioLabels.Count)); break; case DisplayModeLayout.Horizontal: CalculateHorizontalPositions (); var length = 0; foreach (var item in _horizontal) { length += item.length; } var hr = new Rect (0, 0, length, 1); if (IsAdded && LayoutStyle == LayoutStyle.Computed) { Width = hr.Width; Height = 1; } else { Bounds = new Rect (Bounds.Location, new Size (hr.Width, radioLabels.Count)); } break; } } static Rect MakeRect (int x, int y, List radioLabels) { if (radioLabels == null) { return new Rect (x, y, 0, 0); } int width = 0; foreach (var s in radioLabels) { width = Math.Max (s.GetColumns () + 2, width); } return new Rect (x, y, width, radioLabels.Count); } List _radioLabels = new List (); /// /// 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 (var label in _radioLabels) { if (TextFormatter.FindHotKey (label, HotKeySpecifier, true, out _, out var hotKey)) { AddKeyBindingsForHotKey (hotKey, KeyCode.Null); } } var prevCount = _radioLabels.Count; _radioLabels = value.ToList (); foreach (var label in _radioLabels) { if (TextFormatter.FindHotKey (label, HotKeySpecifier, true, out _, out var hotKey)) { AddKeyBindingsForHotKey (KeyCode.Null, hotKey); } } if (IsInitialized && prevCount != _radioLabels.Count) { SetWidthHeight (_radioLabels); } SelectedItem = 0; _cursor = 0; SetNeedsDisplay (); } } /// 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. var key = keyEvent; if (KeyBindings.TryGet (key, out _)) { // Search RadioLabels for (int i = 0; i < _radioLabels.Count; i++) { if (TextFormatter.FindHotKey (_radioLabels [i], HotKeySpecifier, true, out _, out var hotKey) && (key.NoAlt.NoCtrl.NoShift) == hotKey) { SelectedItem = i; keyEvent.Scope = KeyBindingScope.HotKey; break; } } } return base.OnInvokingKeyBindings (keyEvent); } void CalculateHorizontalPositions () { if (_displayMode == DisplayModeLayout.Horizontal) { _horizontal = new List<(int pos, int length)> (); int start = 0; int length = 0; for (int i = 0; i < _radioLabels.Count; i++) { start += length; length = _radioLabels [i].GetColumns () + 2 + (i < _radioLabels.Count - 1 ? _horizontalSpace : 0); _horizontal.Add ((start, length)); } } } /// public override void OnDrawContent (Rect contentArea) { base.OnDrawContent (contentArea); Driver.SetAttribute (GetNormalColor ()); for (int i = 0; i < _radioLabels.Count; i++) { switch (DisplayMode) { case DisplayModeLayout.Vertical: Move (0, i); break; case DisplayModeLayout.Horizontal: Move (_horizontal [i].pos, 0); break; } var rl = _radioLabels [i]; Driver.SetAttribute (GetNormalColor ()); Driver.AddStr ($"{(i == _selected ? CM.Glyphs.Selected : CM.Glyphs.UnSelected)} "); TextFormatter.FindHotKey (rl, HotKeySpecifier, true, out int hotPos, out var hotKey); if (hotPos != -1 && (hotKey != KeyCode.Null)) { var rlRunes = rl.ToRunes (); for (int 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 void PositionCursor () { switch (DisplayMode) { case DisplayModeLayout.Vertical: Move (0, _cursor); break; case DisplayModeLayout.Horizontal: Move (_horizontal [_cursor].pos, 0); break; } } /// /// Invoked when the selected radio label has changed. /// public event EventHandler SelectedItemChanged; /// /// The currently selected item from the list of radio labels /// /// The selected. public int SelectedItem { get => _selected; set { OnSelectedItemChanged (value, SelectedItem); _cursor = _selected; SetNeedsDisplay (); } } /// /// Allow to invoke the after their creation. /// public void Refresh () { OnSelectedItemChanged (_selected, -1); } /// /// 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)); } void SelectItem () { SelectedItem = _cursor; } void MoveEnd () { _cursor = Math.Max (_radioLabels.Count - 1, 0); } void MoveHome () { _cursor = 0; } void MoveDown () { if (_cursor + 1 < _radioLabels.Count) { _cursor++; SetNeedsDisplay (); } else if (_cursor > 0) { _cursor = 0; SetNeedsDisplay (); } } void MoveUp () { if (_cursor > 0) { _cursor--; SetNeedsDisplay (); } else if (_radioLabels.Count - 1 > 0) { _cursor = _radioLabels.Count - 1; SetNeedsDisplay (); } } /// public override bool MouseEvent (MouseEvent me) { if (!me.Flags.HasFlag (MouseFlags.Button1Clicked)) { return false; } if (!CanFocus) { return false; } SetFocus (); int boundsX = me.X; int boundsY = me.Y; var pos = _displayMode == DisplayModeLayout.Horizontal ? boundsX : boundsY; var rCount = _displayMode == DisplayModeLayout.Horizontal ? _horizontal.Last ().pos + _horizontal.Last ().length : _radioLabels.Count; if (pos < rCount) { var c = _displayMode == DisplayModeLayout.Horizontal ? _horizontal.FindIndex ((x) => x.pos <= boundsX && x.pos + x.length - 2 >= boundsX) : boundsY; if (c > -1) { _cursor = SelectedItem = c; SetNeedsDisplay (); } } return true; } /// public override bool OnEnter (View view) { Application.Driver.SetCursorVisibility (CursorVisibility.Invisible); return base.OnEnter (view); } } /// /// Used for choose the display mode of this /// public enum DisplayModeLayout { /// /// Vertical mode display. It's the default. /// Vertical, /// /// Horizontal mode display. /// Horizontal }