From 5f2d4a3fd444e4e2e62a627b2a6531ad0b57c230 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 25 Aug 2022 14:32:13 +0100 Subject: [PATCH 1/3] Change KeyBindings to allow chaining commands --- Terminal.Gui/Core/View.cs | 41 ++++++++++++++++++++++++++------------ UnitTests/ListViewTests.cs | 33 ++++++++++++++++++++++++++++++ 2 files changed, 61 insertions(+), 13 deletions(-) diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 2870be8584..43d9019b52 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] (); - return CommandImplementations [command] (); + // if we haven't got anything yet, the current command result should be used + if (toReturn == null) { + toReturn = thisReturn; + } + + // if ever see a true then that's what we will return + if (thisReturn ?? false) { + toReturn = thisReturn.Value; + } + } } - return null; + return toReturn; } @@ -1739,7 +1754,7 @@ public override bool ProcessKey (KeyEvent keyEvent) /// /// /// - public void AddKeyBinding (Key key, Command command) + public void AddKeyBinding (Key key, params Command [] command) { if (KeyBindings.ContainsKey (key)) { KeyBindings [key] = command; @@ -1756,7 +1771,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 +1810,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 +1852,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..5f277de025 100644 --- a/UnitTests/ListViewTests.cs +++ b/UnitTests/ListViewTests.cs @@ -30,6 +30,39 @@ 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)); + + } + private class NewListDataSource : IListDataSource { public int Count => throw new NotImplementedException (); From 630d0d5f4adb1912426920884f177d91d65de6e0 Mon Sep 17 00:00:00 2001 From: tznind Date: Thu, 25 Aug 2022 14:36:43 +0100 Subject: [PATCH 2/3] Added more asserts for repeating the keybinding till at bottom of list --- UnitTests/ListViewTests.cs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/UnitTests/ListViewTests.cs b/UnitTests/ListViewTests.cs index 5f277de025..0e69d9f2ef 100644 --- a/UnitTests/ListViewTests.cs +++ b/UnitTests/ListViewTests.cs @@ -61,6 +61,26 @@ public void ListViewSelectThenDown () 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 } private class NewListDataSource : IListDataSource { From b3867cb506ccc85a5b485102a5fc1fb264e89ebd Mon Sep 17 00:00:00 2001 From: tznind Date: Sat, 27 Aug 2022 08:23:02 +0100 Subject: [PATCH 3/3] Added tests for 'no command' and multiple commands return type --- Terminal.Gui/Core/View.cs | 10 +++++++++- UnitTests/ListViewTests.cs | 38 ++++++++++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 1 deletion(-) diff --git a/Terminal.Gui/Core/View.cs b/Terminal.Gui/Core/View.cs index 43d9019b52..170289c7f8 100644 --- a/Terminal.Gui/Core/View.cs +++ b/Terminal.Gui/Core/View.cs @@ -1751,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) /// /// - /// + /// 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 { diff --git a/UnitTests/ListViewTests.cs b/UnitTests/ListViewTests.cs index 0e69d9f2ef..2785261516 100644 --- a/UnitTests/ListViewTests.cs +++ b/UnitTests/ListViewTests.cs @@ -82,6 +82,44 @@ public void ListViewSelectThenDown () 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 ();