diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs
index 2870be8584..170289c7f8 100644
--- a/Terminal.Gui/Core/View.cs
+++ b/Terminal.Gui/Core/View.cs
@@ -271,7 +271,7 @@ internal Direction FocusDirection {
///
/// Configurable keybindings supported by the control
///
- private Dictionary KeyBindings { get; set; } = new Dictionary ();
+ private Dictionary KeyBindings { get; set; } = new Dictionary ();
private Dictionary> CommandImplementations { get; set; } = new Dictionary> ();
///
@@ -1716,17 +1716,32 @@ public override bool ProcessKey (KeyEvent keyEvent)
/// The key event passed.
protected bool? InvokeKeybindings (KeyEvent keyEvent)
{
+ bool? toReturn = null;
+
if (KeyBindings.ContainsKey (keyEvent.Key)) {
- var command = KeyBindings [keyEvent.Key];
- if (!CommandImplementations.ContainsKey (command)) {
- throw new NotSupportedException ($"A KeyBinding was set up for the command {command} ({keyEvent.Key}) but that command is not supported by this View ({GetType ().Name})");
- }
+ foreach (var command in KeyBindings [keyEvent.Key]) {
+
+ if (!CommandImplementations.ContainsKey (command)) {
+ throw new NotSupportedException ($"A KeyBinding was set up for the command {command} ({keyEvent.Key}) but that command is not supported by this View ({GetType ().Name})");
+ }
+
+ // each command has its own return value
+ var thisReturn = CommandImplementations [command] ();
+
+ // if we haven't got anything yet, the current command result should be used
+ if (toReturn == null) {
+ toReturn = thisReturn;
+ }
- return CommandImplementations [command] ();
+ // if ever see a true then that's what we will return
+ if (thisReturn ?? false) {
+ toReturn = thisReturn.Value;
+ }
+ }
}
- return null;
+ return toReturn;
}
@@ -1736,11 +1751,19 @@ public override bool ProcessKey (KeyEvent keyEvent)
///
/// If the key is already bound to a different it will be
/// rebound to this one
+ /// Commands are only ever applied to the current (i.e. this feature
+ /// cannot be used to switch focus to another view and perform multiple commands there)
///
///
- ///
- public void AddKeyBinding (Key key, Command command)
+ /// The command(s) to run on the when is pressed.
+ /// When specifying multiple, all commands will be applied in sequence. The bound strike
+ /// will be consumed if any took effect.
+ public void AddKeyBinding (Key key, params Command [] command)
{
+ if (command.Length == 0) {
+ throw new ArgumentException ("At least one command must be specified", nameof (command));
+ }
+
if (KeyBindings.ContainsKey (key)) {
KeyBindings [key] = command;
} else {
@@ -1756,7 +1779,7 @@ public void AddKeyBinding (Key key, Command command)
protected void ReplaceKeyBinding (Key fromKey, Key toKey)
{
if (KeyBindings.ContainsKey (fromKey)) {
- Command value = KeyBindings [fromKey];
+ var value = KeyBindings [fromKey];
KeyBindings.Remove (fromKey);
KeyBindings [toKey] = value;
}
@@ -1795,9 +1818,9 @@ public void ClearKeybinding (Key key)
/// keys bound to the same command and this method will clear all of them.
///
///
- public void ClearKeybinding (Command command)
+ public void ClearKeybinding (params Command [] command)
{
- foreach (var kvp in KeyBindings.Where (kvp => kvp.Value == command).ToArray ()) {
+ foreach (var kvp in KeyBindings.Where (kvp => kvp.Value.SequenceEqual (command))) {
KeyBindings.Remove (kvp.Key);
}
}
@@ -1837,9 +1860,9 @@ public IEnumerable GetSupportedCommands ()
///
/// The command to search.
/// The used by a
- public Key GetKeyFromCommand (Command command)
+ public Key GetKeyFromCommand (params Command [] command)
{
- return KeyBindings.First (x => x.Value == command).Key;
+ return KeyBindings.First (x => x.Value.SequenceEqual (command)).Key;
}
///
diff --git a/UnitTests/ListViewTests.cs b/UnitTests/ListViewTests.cs
index 99abd469d8..2785261516 100644
--- a/UnitTests/ListViewTests.cs
+++ b/UnitTests/ListViewTests.cs
@@ -30,6 +30,97 @@ public void Constructors_Defaults ()
Assert.Equal (new Rect (0, 1, 10, 20), lv.Frame);
}
+ [Fact]
+ public void ListViewSelectThenDown ()
+ {
+ var lv = new ListView (new List () { "One", "Two", "Three" });
+ lv.AllowsMarking = true;
+
+ Assert.NotNull (lv.Source);
+
+ // first item should be selected by default
+ Assert.Equal (0, lv.SelectedItem);
+
+ // nothing is ticked
+ Assert.False (lv.Source.IsMarked (0));
+ Assert.False (lv.Source.IsMarked (1));
+ Assert.False (lv.Source.IsMarked (2));
+
+ lv.AddKeyBinding (Key.Space | Key.ShiftMask, Command.ToggleChecked, Command.LineDown);
+
+ var ev = new KeyEvent (Key.Space | Key.ShiftMask, new KeyModifiers () { Shift = true });
+
+ // view should indicate that it has accepted and consumed the event
+ Assert.True (lv.ProcessKey (ev));
+
+ // second item should now be selected
+ Assert.Equal (1, lv.SelectedItem);
+
+ // first item only should be ticked
+ Assert.True (lv.Source.IsMarked (0));
+ Assert.False (lv.Source.IsMarked (1));
+ Assert.False (lv.Source.IsMarked (2));
+
+ // Press key combo again
+ Assert.True (lv.ProcessKey (ev));
+ Assert.Equal (2, lv.SelectedItem);
+ Assert.True (lv.Source.IsMarked (0));
+ Assert.True (lv.Source.IsMarked (1));
+ Assert.False (lv.Source.IsMarked (2));
+
+ // Press key combo again
+ Assert.True (lv.ProcessKey (ev));
+ Assert.Equal (2, lv.SelectedItem); // cannot move down any further
+ Assert.True (lv.Source.IsMarked (0));
+ Assert.True (lv.Source.IsMarked (1));
+ Assert.True (lv.Source.IsMarked (2)); // but can toggle marked
+
+ // Press key combo again
+ Assert.True (lv.ProcessKey (ev));
+ Assert.Equal (2, lv.SelectedItem); // cannot move down any further
+ Assert.True (lv.Source.IsMarked (0));
+ Assert.True (lv.Source.IsMarked (1));
+ Assert.False (lv.Source.IsMarked (2)); // untoggle toggle marked
+ }
+ [Fact]
+ public void SettingEmptyKeybindingThrows ()
+ {
+ var lv = new ListView (new List () { "One", "Two", "Three" });
+ Assert.Throws (() => lv.AddKeyBinding (Key.Space));
+ }
+
+
+ ///
+ /// Tests that when none of the Commands in a chained keybinding are possible
+ /// the returns the appropriate result
+ ///
+ [Fact]
+ public void ListViewProcessKeyReturnValue_WithMultipleCommands ()
+ {
+ var lv = new ListView (new List () { "One", "Two", "Three", "Four" });
+
+ Assert.NotNull (lv.Source);
+
+ // first item should be selected by default
+ Assert.Equal (0, lv.SelectedItem);
+
+ // bind shift down to move down twice in control
+ lv.AddKeyBinding (Key.CursorDown | Key.ShiftMask, Command.LineDown, Command.LineDown);
+
+ var ev = new KeyEvent (Key.CursorDown | Key.ShiftMask, new KeyModifiers () { Shift = true });
+
+ Assert.True (lv.ProcessKey (ev), "The first time we move down 2 it should be possible");
+
+ // After moving down twice from One we should be at 'Three'
+ Assert.Equal (2, lv.SelectedItem);
+
+ // clear the items
+ lv.SetSource (null);
+
+ // Press key combo again - return should be false this time as none of the Commands are allowable
+ Assert.False (lv.ProcessKey (ev), "We cannot move down so will not respond to this");
+ }
+
private class NewListDataSource : IListDataSource {
public int Count => throw new NotImplementedException ();