diff --git a/Terminal.Gui/View/ViewKeyboard.cs b/Terminal.Gui/View/ViewKeyboard.cs
index 3e62f1b9d..7863c6799 100644
--- a/Terminal.Gui/View/ViewKeyboard.cs
+++ b/Terminal.Gui/View/ViewKeyboard.cs
@@ -129,9 +129,10 @@ public partial class View
///
/// The HotKey is replacing. Key bindings for this key will be removed.
/// The new HotKey. If bindings will be removed.
+ /// Arbitrary context that can be associated with this key binding.
/// if the HotKey bindings were added.
///
- public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey)
+ public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey, [CanBeNull] object context = null)
{
if (_hotKey == hotKey)
{
@@ -194,15 +195,16 @@ public partial class View
// Add the new
if (newKey != Key.Empty)
{
+ KeyBinding keyBinding = new ([Command.HotKey], KeyBindingScope.HotKey, context);
// Add the base and Alt key
- KeyBindings.Add (newKey, KeyBindingScope.HotKey, Command.HotKey);
- KeyBindings.Add (newKey.WithAlt, KeyBindingScope.HotKey, Command.HotKey);
+ KeyBindings.Add (newKey, keyBinding);
+ KeyBindings.Add (newKey.WithAlt, keyBinding);
// If the Key is A..Z, add ShiftMask and AltMask | ShiftMask
if (newKey.IsKeyCodeAtoZ)
{
- KeyBindings.Add (newKey.WithShift, KeyBindingScope.HotKey, Command.HotKey);
- KeyBindings.Add (newKey.WithShift.WithAlt, KeyBindingScope.HotKey, Command.HotKey);
+ KeyBindings.Add (newKey.WithShift, keyBinding);
+ KeyBindings.Add (newKey.WithShift.WithAlt, keyBinding);
}
}
diff --git a/Terminal.Gui/Views/RadioGroup.cs b/Terminal.Gui/Views/RadioGroup.cs
index 12aa1243c..efdfa5260 100644
--- a/Terminal.Gui/Views/RadioGroup.cs
+++ b/Terminal.Gui/Views/RadioGroup.cs
@@ -65,16 +65,32 @@ public class RadioGroup : View
Command.Accept,
() =>
{
- SelectItem ();
+ SelectedItem = _cursor;
+
return !OnAccept ();
}
);
+ AddCommand (
+ Command.HotKey,
+ ctx =>
+ {
+ SetFocus ();
+ if (ctx.KeyBinding?.Context is { } && (int)ctx.KeyBinding?.Context! < _radioLabels.Count)
+ {
+ SelectedItem = (int)ctx.KeyBinding?.Context!;
+
+ return !OnAccept();
+ }
+
+ return true;
+ });
+
SetupKeyBindings ();
LayoutStarted += RadioGroup_LayoutStarted;
- HighlightStyle = Gui.HighlightStyle.PressedOutside | Gui.HighlightStyle.Pressed;
+ HighlightStyle = HighlightStyle.PressedOutside | HighlightStyle.Pressed;
MouseClick += RadioGroup_MouseClick;
}
@@ -84,6 +100,7 @@ public class RadioGroup : View
private void SetupKeyBindings ()
{
KeyBindings.Clear ();
+
// Default keybindings for this view
if (Orientation == Orientation.Vertical)
{
@@ -95,6 +112,7 @@ public class RadioGroup : View
KeyBindings.Add (Key.CursorLeft, Command.LineUp);
KeyBindings.Add (Key.CursorRight, Command.LineDown);
}
+
KeyBindings.Add (Key.Home, Command.TopHome);
KeyBindings.Add (Key.End, Command.BottomEnd);
KeyBindings.Add (Key.Space, Command.Accept);
@@ -179,11 +197,13 @@ public class RadioGroup : View
int prevCount = _radioLabels.Count;
_radioLabels = value.ToList ();
- foreach (string label in _radioLabels)
+ for (var index = 0; index < _radioLabels.Count; index++)
{
+ string label = _radioLabels [index];
+
if (TextFormatter.FindHotKey (label, HotKeySpecifier, out _, out Key hotKey))
{
- AddKeyBindingsForHotKey (Key.Empty, hotKey);
+ AddKeyBindingsForHotKey (Key.Empty, hotKey, index);
}
}
@@ -192,7 +212,7 @@ public class RadioGroup : View
}
}
- ///
+ ///
public override string Text
{
get
@@ -201,6 +221,7 @@ public class RadioGroup : View
{
return string.Empty;
}
+
// Return labels as a CSV string
return string.Join (",", _radioLabels);
}
@@ -220,73 +241,51 @@ public class RadioGroup : View
/// 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)
+ get => _selected;
+ set
{
- case Orientation.Vertical:
- Move (0, i);
-
- break;
- case Orientation.Horizontal:
- Move (_horizontal [i].pos, 0);
-
- break;
+ OnSelectedItemChanged (value, SelectedItem);
+ _cursor = Math.Max (_selected, 0);
+ SetNeedsDisplay ();
}
+ }
+
+ ///
+ public override void OnDrawContent (Rectangle viewport)
+ {
+ base.OnDrawContent (viewport);
- 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)
+ for (var i = 0; i < _radioLabels.Count; i++)
{
- Rune [] rlRunes = rl.ToRunes ();
-
- for (var j = 0; j < rlRunes.Length; j++)
+ switch (Orientation)
{
- Rune rune = rlRunes [j];
+ case Orientation.Vertical:
+ Move (0, i);
- 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);
- }
+ break;
+ case Orientation.Horizontal:
+ Move (_horizontal [i].pos, 0);
- if (rune == HotKeySpecifier && j + 1 < rlRunes.Length)
- {
- j++;
- rune = rlRunes [j];
+ break;
+ }
- if (i == _cursor)
+ 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
@@ -294,185 +293,177 @@ public override void OnDrawContent (Rectangle viewport)
: GetHotNormalColor ()
);
}
- else if (i != _cursor)
+ 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);
+ }
+ }
+ }
+
+ /// 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;
+ SetupKeyBindings ();
+ SetContentSize ();
+ }
+
+ 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 (selectedItem, previousSelectedItem));
+ }
+
+ ///
+ /// Fired when the view orientation has changed. Can be cancelled by setting
+ /// to true.
+ ///
+ public event EventHandler OrientationChanged;
+
+ ///
+ public override Point? PositionCursor ()
+ {
+ var x = 0;
+ var 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 MoveDownRight ()
+ {
+ 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 MoveUpLeft ()
+ {
+ if (_cursor > 0)
+ {
+ _cursor--;
+ SetNeedsDisplay ();
+ }
+ else if (_radioLabels.Count - 1 > 0)
+ {
+ _cursor = _radioLabels.Count - 1;
+ SetNeedsDisplay ();
+ }
+ }
+
+ private void RadioGroup_LayoutStarted (object sender, EventArgs e) { SetContentSize (); }
+
+ private void SetContentSize ()
+ {
+ switch (_orientation)
+ {
+ case Orientation.Vertical:
+ var width = 0;
+
+ foreach (string s in _radioLabels)
+ {
+ width = Math.Max (s.GetColumns () + 2, width);
}
- Application.Driver.AddRune (rune);
- Driver.SetAttribute (GetNormalColor ());
- }
- }
- else
- {
- DrawHotString (rl, HasFocus && i == _cursor, ColorScheme);
- }
- }
-}
+ SetContentSize (new (width, _radioLabels.Count));
+
+ break;
+
+ case Orientation.Horizontal:
+ _horizontal = new ();
+ 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));
+ }
+
+ SetContentSize (new (_horizontal.Sum (item => item.length), 1));
-///
-public override bool? OnInvokingKeyBindings (Key keyEvent)
-{
- // TODO: Use CommandContext
- // 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;
- SetupKeyBindings ();
- SetContentSize ();
- }
-
- 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 MoveDownRight ()
-{
- 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 MoveUpLeft ()
-{
- if (_cursor > 0)
- {
- _cursor--;
- SetNeedsDisplay ();
- }
- else if (_radioLabels.Count - 1 > 0)
- {
- _cursor = _radioLabels.Count - 1;
- SetNeedsDisplay ();
- }
-}
-
-private void RadioGroup_LayoutStarted (object sender, EventArgs e) { SetContentSize (); }
-private void SelectItem () { SelectedItem = _cursor; }
-
-private void SetContentSize ()
-{
- switch (_orientation)
- {
- case Orientation.Vertical:
- var width = 0;
-
- foreach (string s in _radioLabels)
- {
- width = Math.Max (s.GetColumns () + 2, width);
- }
-
- SetContentSize (new (width, _radioLabels.Count));
- break;
-
- case 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));
- }
- SetContentSize (new (_horizontal.Sum (item => item.length), 1));
- break;
- }
-}
}
diff --git a/UnitTests/Views/RadioGroupTests.cs b/UnitTests/Views/RadioGroupTests.cs
index 0e82b3026..faf0275f2 100644
--- a/UnitTests/Views/RadioGroupTests.cs
+++ b/UnitTests/Views/RadioGroupTests.cs
@@ -14,12 +14,12 @@ public class RadioGroupTests (ITestOutputHelper output)
Assert.Equal (Rectangle.Empty, rg.Frame);
Assert.Equal (0, rg.SelectedItem);
- rg = new() { RadioLabels = new [] { "Test" } };
+ rg = new () { RadioLabels = new [] { "Test" } };
Assert.True (rg.CanFocus);
Assert.Single (rg.RadioLabels);
Assert.Equal (0, rg.SelectedItem);
- rg = new()
+ rg = new ()
{
X = 1,
Y = 2,
@@ -32,7 +32,7 @@ public class RadioGroupTests (ITestOutputHelper output)
Assert.Equal (new (1, 2, 20, 5), rg.Frame);
Assert.Equal (0, rg.SelectedItem);
- rg = new() { X = 1, Y = 2, RadioLabels = new [] { "Test" } };
+ rg = new () { X = 1, Y = 2, RadioLabels = new [] { "Test" } };
var view = new View { Width = 30, Height = 40 };
view.Add (rg);
@@ -130,6 +130,51 @@ public class RadioGroupTests (ITestOutputHelper output)
Assert.Equal (1, rg.SelectedItem);
}
+ [Fact]
+ public void HotKey_For_View_SetsFocus ()
+ {
+ var superView = new View
+ {
+ CanFocus = true
+ };
+ superView.Add (new View { CanFocus = true });
+
+ var group = new RadioGroup
+ {
+ Title = "Radio_Group",
+ RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" }
+ };
+ superView.Add (group);
+
+ Assert.False (group.HasFocus);
+ Assert.Equal (0, group.SelectedItem);
+
+ group.NewKeyDownEvent (Key.G.WithAlt);
+
+ Assert.Equal (0, group.SelectedItem);
+ Assert.True (group.HasFocus);
+ }
+
+ [Fact]
+ public void HotKey_For_Item_SetsFocus ()
+ {
+ var superView = new View
+ {
+ CanFocus = true
+ };
+ superView.Add (new View { CanFocus = true });
+ var group = new RadioGroup { RadioLabels = new [] { "_Left", "_Right", "Cen_tered", "_Justified" } };
+ superView.Add (group);
+
+ Assert.False (group.HasFocus);
+ Assert.Equal (0, group.SelectedItem);
+
+ group.NewKeyDownEvent (Key.R);
+
+ Assert.Equal (1, group.SelectedItem);
+ Assert.True (group.HasFocus);
+ }
+
[Fact]
public void HotKey_Command_Does_Not_Accept ()
{
@@ -184,12 +229,8 @@ public class RadioGroupTests (ITestOutputHelper output)
var expected = @$"
┌────────────────────────────┐
-│{
- CM.Glyphs.Selected
-} Test │
-│{
- CM.Glyphs.UnSelected
-} New Test 你 │
+│{CM.Glyphs.Selected} Test │
+│{CM.Glyphs.UnSelected} New Test 你 │
│ │
└────────────────────────────┘
";
@@ -209,11 +250,7 @@ public class RadioGroupTests (ITestOutputHelper output)
expected = @$"
┌────────────────────────────┐
-│{
- CM.Glyphs.Selected
-} Test {
- CM.Glyphs.UnSelected
-} New Test 你 │
+│{CM.Glyphs.Selected} Test {CM.Glyphs.UnSelected} New Test 你 │
│ │
│ │
└────────────────────────────┘
@@ -234,11 +271,7 @@ public class RadioGroupTests (ITestOutputHelper output)
expected = @$"
┌────────────────────────────┐
-│{
- CM.Glyphs.Selected
-} Test {
- CM.Glyphs.UnSelected
-} New Test 你 │
+│{CM.Glyphs.Selected} Test {CM.Glyphs.UnSelected} New Test 你 │
│ │
│ │
└────────────────────────────┘