This commit is contained in:
Tig
2024-10-14 16:35:10 -06:00
parent bc51f8868b
commit 3f3ceae9c7
9 changed files with 215 additions and 377 deletions

View File

@@ -255,9 +255,7 @@ public partial class View // Keyboard APIs
/// <para>
/// If a more focused subview does not handle the key press, this method raises <see cref="OnKeyDown"/>/
/// <see cref="KeyDown"/> to allow the
/// view to pre-process the key press. If <see cref="OnKeyDown"/>/<see cref="KeyDown"/> is not handled
/// <see cref="InvokingKeyBindings"/>/<see cref="OnInvokingKeyBindings"/> will be raised to invoke any key
/// bindings.
/// view to pre-process the key press. If <see cref="OnKeyDown"/>/<see cref="KeyDown"/> is not handled any commands bound to the key will be invoked.
/// Then, only if no key bindings are
/// handled, <see cref="OnKeyDownNotHandled"/>/<see cref="KeyDownNotHandled"/> will be raised allowing the view to
/// process the key press.
@@ -292,8 +290,8 @@ public partial class View // Keyboard APIs
// During (this is what can be cancelled)
// TODO: NewKeyDownEvent returns bool. It should be bool? so state of RaiseInvokingKeyBindingsAndInvokeCommands can be reflected up stack
if (RaiseInvokingKeyBindingsAndInvokeCommands (key) is true || key.Handled)
// TODO: NewKeyDownEvent returns bool. It should be bool? so state of InvokeCommands can be reflected up stack
if (InvokeCommandsBoundToKey (key) is true || key.Handled)
{
return true;
}
@@ -336,7 +334,7 @@ public partial class View // Keyboard APIs
/// <summary>
/// Called when the user presses a key, allowing subscribers to pre-process the key down event. Called
/// before <see cref="InvokingKeyBindings"/> and <see cref="KeyDownNotHandled"/> are raised. Set
/// before key bindings are invoked and <see cref="KeyDownNotHandled"/> is raised. Set
/// <see cref="Key.Handled"/>
/// to true to
/// stop the key from being processed further.
@@ -357,7 +355,7 @@ public partial class View // Keyboard APIs
/// <summary>
/// Raised when the user presses a key, allowing subscribers to pre-process the key down event. Called
/// before <see cref="InvokingKeyBindings"/> and <see cref="KeyDownNotHandled"/> are raised. Set
/// before key bindings are invoked and <see cref="KeyDownNotHandled"/> is raised. Set
/// <see cref="Key.Handled"/>
/// to true to
/// stop the key from being processed further.
@@ -508,8 +506,7 @@ public partial class View // Keyboard APIs
private Dictionary<Command, CommandImplementation> CommandImplementations { get; } = new ();
/// <summary>
/// INTERNAL API: Raises the <see cref="InvokingKeyBindings"/> event and invokes the commands bound to
/// <paramref name="key"/>.
/// INTERNAL API: Invokes any commands bound to <paramref name="key"/> on this view, adornments, and subviews.
/// </summary>
/// <param name="key"></param>
/// <returns>
@@ -517,28 +514,13 @@ public partial class View // Keyboard APIs
/// continue.
/// <see langword="false"/> if a command was invoked and was not handled (or cancelled); input processing should
/// continue.
/// <see langword="true"/> if <see cref="InvokingKeyBindings"/> was handled or a command was invoked and handled (or
/// <see langword="true"/> if at least one command was invoked and handled (or
/// cancelled); input processing should stop.
/// </returns>
internal bool? RaiseInvokingKeyBindingsAndInvokeCommands (Key key)
internal bool? InvokeCommandsBoundToKey (Key key)
{
KeyBindingScope scope = KeyBindingScope.Focused | KeyBindingScope.HotKey;
// During
if (OnInvokingKeyBindings (key, scope))
{
return true;
}
InvokingKeyBindings?.Invoke (this, key);
if (key.Handled)
{
return true;
}
// After
// * If no key binding was found, `InvokeKeyBindings` returns `null`.
// Continue passing the event (return `false` from `OnInvokeKeyBindings`).
// * If key bindings were found, but none handled the key (all `Command`s returned `false`),
@@ -547,29 +529,29 @@ public partial class View // Keyboard APIs
// `InvokeKeyBindings` returns `true`. Continue passing the event (return `false` from `OnInvokeKeyBindings`).
bool? handled = InvokeCommands (key, scope);
if (handled is { } && (bool)handled)
if (handled is true)
{
// Stop processing if any key binding handled the key.
// DO NOT stop processing if there are no matching key bindings or none of the key bindings handled the key
return handled;
}
if (Margin is { } && ProcessAdornmentKeyBindings (Margin, key, scope, ref handled))
if (Margin is { } && InvokeCommandsBoundToKeyOnAdornment (Margin, key, scope, ref handled))
{
return true;
}
if (Padding is { } && ProcessAdornmentKeyBindings (Padding, key, scope, ref handled))
if (Padding is { } && InvokeCommandsBoundToKeyOnAdornment (Padding, key, scope, ref handled))
{
return true;
}
if (Border is { } && ProcessAdornmentKeyBindings (Border, key, scope, ref handled))
if (Border is { } && InvokeCommandsBoundToKeyOnAdornment (Border, key, scope, ref handled))
{
return true;
}
if (ProcessSubViewKeyBindings (key, scope, ref handled))
if (InvokeCommandsBoundToKeyOnSubviews (key, scope, ref handled))
{
return true;
}
@@ -577,32 +559,9 @@ public partial class View // Keyboard APIs
return handled;
}
/// <summary>
/// Called when a key is pressed that may be mapped to a key binding. Set <see cref="Key.Handled"/> to true to
/// stop the key from being processed by other views.
/// </summary>
/// <remarks>
/// <para>See <see href="../docs/keyboard.md">for an overview of Terminal.Gui keyboard APIs.</see></para>
/// </remarks>
/// <param name="key">Contains the details about the key that produced the event.</param>
/// <param name="scope">The scope.</param>
/// <returns>
/// <see langword="false"/> if the event was raised and was not handled (or cancelled); input processing should
/// continue.
/// <see langword="true"/> if the event was raised and handled (or cancelled); input processing should stop.
/// </returns>
protected virtual bool OnInvokingKeyBindings (Key key, KeyBindingScope scope) { return false; }
// TODO: This does not carry KeyBindingScope, but OnInvokingKeyBindings does
/// <summary>
/// Raised when a key is pressed that may be mapped to a key binding. Set <see cref="Key.Handled"/> to true to
/// stop the key from being processed by other views.
/// </summary>
public event EventHandler<Key>? InvokingKeyBindings;
private bool ProcessAdornmentKeyBindings (Adornment adornment, Key key, KeyBindingScope scope, ref bool? handled)
private static bool InvokeCommandsBoundToKeyOnAdornment (Adornment adornment, Key key, KeyBindingScope scope, ref bool? handled)
{
bool? adornmentHandled = adornment.RaiseInvokingKeyBindingsAndInvokeCommands (key);
bool? adornmentHandled = adornment.InvokeCommandsBoundToKey (key);
if (adornmentHandled is true)
{
@@ -616,7 +575,7 @@ public partial class View // Keyboard APIs
foreach (View subview in adornment.Subviews)
{
bool? subViewHandled = subview.RaiseInvokingKeyBindingsAndInvokeCommands (key);
bool? subViewHandled = subview.InvokeCommandsBoundToKey (key);
if (subViewHandled is { })
{
@@ -632,7 +591,7 @@ public partial class View // Keyboard APIs
return false;
}
private bool ProcessSubViewKeyBindings (Key key, KeyBindingScope scope, ref bool? handled, bool invoke = true)
private bool InvokeCommandsBoundToKeyOnSubviews (Key key, KeyBindingScope scope, ref bool? handled, bool invoke = true)
{
// Now, process any key bindings in the subviews that are tagged to KeyBindingScope.HotKey.
foreach (View subview in Subviews)
@@ -654,7 +613,7 @@ public partial class View // Keyboard APIs
return true;
}
bool? subViewHandled = subview.RaiseInvokingKeyBindingsAndInvokeCommands (key);
bool? subViewHandled = subview.InvokeCommandsBoundToKey (key);
if (subViewHandled is { })
{
@@ -667,7 +626,7 @@ public partial class View // Keyboard APIs
}
}
bool recurse = subview.ProcessSubViewKeyBindings (key, scope, ref handled, invoke);
bool recurse = subview.InvokeCommandsBoundToKeyOnSubviews (key, scope, ref handled, invoke);
if (recurse || (handled is { } && (bool)handled))
{

View File

@@ -309,7 +309,7 @@ internal sealed class Menu : View
protected override bool OnKeyDownNotHandled (Key keyEvent)
{
// We didn't handle the key, pass it on to host
return _host.RaiseInvokingKeyBindingsAndInvokeCommands (keyEvent) == true;
return _host.InvokeCommandsBoundToKey (keyEvent) == true;
}
private void Current_TerminalResized (object? sender, SizeChangedEventArgs e)

View File

@@ -10,109 +10,136 @@ public class Keys : Scenario
public override void Main ()
{
Application.Init ();
ObservableCollection<string> keyPressedList = [];
ObservableCollection<string> invokingKeyBindingsList = new ();
ObservableCollection<string> keyDownList = [];
ObservableCollection<string> keyDownNotHandledList = new ();
var win = new Window { Title = GetQuitKeyAndName () };
var editLabel = new Label { X = 0, Y = 0, Text = "Type text here:" };
win.Add (editLabel);
var edit = new TextField { X = Pos.Right (editLabel) + 1, Y = Pos.Top (editLabel), Width = Dim.Fill (2) };
var label = new Label
{
X = 0,
Y = 0,
Text = "_Type text here:"
};
win.Add (label);
var edit = new TextField
{
X = Pos.Right (label) + 1,
Y = Pos.Top (label),
Width = Dim.Fill (2),
Height = 1,
};
win.Add (edit);
edit.KeyDown += (s, a) => { keyPressedList.Add (a.ToString ()); };
edit.InvokingKeyBindings += (s, a) =>
{
if (edit.KeyBindings.TryGet (a, out KeyBinding binding))
{
invokingKeyBindingsList.Add ($"{a}: {string.Join (",", binding.Commands)}");
}
};
// Last KeyPress: ______
var keyPressedLabel = new Label
label = new Label
{
X = Pos.Left (editLabel), Y = Pos.Top (editLabel) + 1, Text = "Last TextView.KeyPressed:"
X = 0,
Y = Pos.Bottom (label),
Text = "Last _Application.KeyDown:"
};
win.Add (keyPressedLabel);
var labelTextViewKeypress = new Label { X = Pos.Right (keyPressedLabel) + 1, Y = Pos.Top (keyPressedLabel) };
win.Add (labelTextViewKeypress);
edit.KeyDown += (s, e) => labelTextViewKeypress.Text = e.ToString ();
keyPressedLabel = new Label
win.Add (label);
var labelAppKeypress = new Label
{
X = Pos.Left (keyPressedLabel), Y = Pos.Bottom (keyPressedLabel), Text = "Last Application.KeyDown:"
X = Pos.Right (label) + 1,
Y = Pos.Top (label)
};
win.Add (keyPressedLabel);
var labelAppKeypress = new Label { X = Pos.Right (keyPressedLabel) + 1, Y = Pos.Top (keyPressedLabel) };
win.Add (labelAppKeypress);
Application.KeyDown += (s, e) => labelAppKeypress.Text = e.ToString ();
// Key stroke log:
var keyLogLabel = new Label
{
X = Pos.Left (editLabel), Y = Pos.Top (editLabel) + 4, Text = "Application Key Events:"
};
win.Add (keyLogLabel);
int maxKeyString = Key.CursorRight.WithAlt.WithCtrl.WithShift.ToString ().Length;
var yOffset = 1;
ObservableCollection<string> keyEventlist = new ();
var keyEventListView = new ListView
label = new ()
{
X = 0,
Y = Pos.Top (keyLogLabel) + yOffset,
Width = "Key Down:".Length + maxKeyString,
Y = Pos.Bottom (label),
Text = "_Last TextField.KeyDown:"
};
win.Add (label);
var lastTextFieldKeyDownLabel = new Label
{
X = Pos.Right (label) + 1,
Y = Pos.Top (label),
Height = 1,
};
win.Add (lastTextFieldKeyDownLabel);
edit.KeyDown += (s, e) => lastTextFieldKeyDownLabel.Text = e.ToString ();
// Application key event log:
label = new Label
{
X = 0,
Y = Pos.Bottom (label) + 1,
Text = "Application Key Events:"
};
win.Add (label);
int maxKeyString = Key.CursorRight.WithAlt.WithCtrl.WithShift.ToString ().Length;
ObservableCollection<string> keyEventList = new ();
var appKeyEventListView = new ListView
{
X = 0,
Y = Pos.Bottom (label),
Width = "KeyDown:".Length + maxKeyString,
Height = Dim.Fill (),
Source = new ListWrapper<string> (keyEventlist)
Source = new ListWrapper<string> (keyEventList)
};
keyEventListView.ColorScheme = Colors.ColorSchemes ["TopLevel"];
win.Add (keyEventListView);
appKeyEventListView.ColorScheme = Colors.ColorSchemes ["TopLevel"];
win.Add (appKeyEventListView);
// OnKeyPressed
var onKeyPressedLabel = new Label
// View key events...
edit.KeyDown += (s, a) => { keyDownList.Add (a.ToString ()); };
edit.KeyDownNotHandled += (s, a) =>
{
if (edit.KeyBindings.TryGet (a, out KeyBinding binding))
{
keyDownNotHandledList.Add ($"{a}: {string.Join (",", binding.Commands)}");
}
};
// KeyDown
label = new Label
{
X = Pos.Right (keyEventListView) + 1, Y = Pos.Top (editLabel) + 4, Text = "TextView KeyDown:"
X = Pos.Right (appKeyEventListView) + 1,
Y = Pos.Top (label),
Text = "TextView Key Down:"
};
win.Add (onKeyPressedLabel);
win.Add (label);
yOffset = 1;
var onKeyPressedListView = new ListView
var onKeyDownListView = new ListView
{
X = Pos.Left (onKeyPressedLabel),
Y = Pos.Top (onKeyPressedLabel) + yOffset,
X = Pos.Left (label),
Y = Pos.Bottom (label),
Width = maxKeyString,
Height = Dim.Fill (),
Source = new ListWrapper<string> (keyPressedList)
Source = new ListWrapper<string> (keyDownList)
};
onKeyPressedListView.ColorScheme = Colors.ColorSchemes ["TopLevel"];
win.Add (onKeyPressedListView);
onKeyDownListView.ColorScheme = Colors.ColorSchemes ["TopLevel"];
win.Add (onKeyDownListView);
// OnInvokeKeyBindings
var onInvokingKeyBindingsLabel = new Label
// KeyDownNotHandled
label = new Label
{
X = Pos.Right (onKeyPressedListView) + 1,
Y = Pos.Top (editLabel) + 4,
Text = "TextView InvokingKeyBindings:"
X = Pos.Right (onKeyDownListView) + 1,
Y = Pos.Top (label),
Text = "TextView KeyDownNotHandled:"
};
win.Add (onInvokingKeyBindingsLabel);
win.Add (label);
var onInvokingKeyBindingsListView = new ListView
var onKeyDownNotHandledListView = new ListView
{
X = Pos.Left (onInvokingKeyBindingsLabel),
Y = Pos.Top (onInvokingKeyBindingsLabel) + yOffset,
Width = Dim.Fill (1),
X = Pos.Left (label),
Y = Pos.Bottom (label),
Width = maxKeyString,
Height = Dim.Fill (),
Source = new ListWrapper<string> (invokingKeyBindingsList)
Source = new ListWrapper<string> (keyDownNotHandledList)
};
onInvokingKeyBindingsListView.ColorScheme = Colors.ColorSchemes ["TopLevel"];
win.Add (onInvokingKeyBindingsListView);
onKeyDownNotHandledListView.ColorScheme = Colors.ColorSchemes ["TopLevel"];
win.Add (onKeyDownNotHandledListView);
//Application.KeyDown += (s, a) => KeyDownPressUp (a, "Down");
Application.KeyDown += (s, a) => KeyDownPressUp (a, "Down");
Application.KeyUp += (s, a) => KeyDownPressUp (a, "Up");
@@ -120,10 +147,9 @@ public class Keys : Scenario
{
// BUGBUG: KeyEvent.ToString is badly broken
var msg = $"Key{updown,-7}: {args}";
keyEventlist.Add (msg);
keyEventListView.MoveDown ();
onKeyPressedListView.MoveDown ();
onInvokingKeyBindingsListView.MoveDown ();
keyEventList.Add (msg);
appKeyEventListView.MoveDown ();
onKeyDownNotHandledListView.MoveDown ();
}
Application.Run (win);

View File

@@ -148,7 +148,7 @@ public class VkeyPacketSimulator : Scenario
}
};
tvInput.InvokingKeyBindings += (s, e) =>
tvInput.KeyDownNotHandled += (s, e) =>
{
Key ev = e;

View File

@@ -163,39 +163,39 @@ public class KeyboardTests
public void KeyBinding_OnKeyDown ()
{
var view = new ScopedKeyBindingView ();
var invoked = false;
view.InvokingKeyBindings += (s, e) => invoked = true;
var keyWasHandled = false;
view.KeyDownNotHandled += (s, e) => keyWasHandled = true;
var top = new Toplevel ();
top.Add (view);
Application.Begin (top);
Application.RaiseKeyDownEvent (Key.A);
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.True (view.ApplicationCommand);
invoked = false;
keyWasHandled = false;
view.ApplicationCommand = false;
Application.KeyBindings.Remove (KeyCode.A);
Application.RaiseKeyDownEvent (Key.A); // old
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.False (view.ApplicationCommand);
Application.KeyBindings.Add (Key.A.WithCtrl, view, Command.Save);
Application.RaiseKeyDownEvent (Key.A); // old
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.False (view.ApplicationCommand);
Application.RaiseKeyDownEvent (Key.A.WithCtrl); // new
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.True (view.ApplicationCommand);
invoked = false;
keyWasHandled = false;
Application.RaiseKeyDownEvent (Key.H);
Assert.True (invoked);
Assert.False (keyWasHandled);
invoked = false;
keyWasHandled = false;
Assert.False (view.HasFocus);
Application.RaiseKeyDownEvent (Key.F);
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.True (view.ApplicationCommand);
Assert.True (view.HotKeyCommand);
@@ -208,23 +208,23 @@ public class KeyboardTests
public void KeyBinding_OnKeyDown_Negative ()
{
var view = new ScopedKeyBindingView ();
var invoked = false;
view.InvokingKeyBindings += (s, e) => invoked = true;
var keyWasHandled = false;
view.KeyDownNotHandled += (s, e) => keyWasHandled = true;
var top = new Toplevel ();
top.Add (view);
Application.Begin (top);
Application.RaiseKeyDownEvent (Key.A.WithCtrl);
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.False (view.ApplicationCommand);
Assert.False (view.HotKeyCommand);
Assert.False (view.FocusedCommand);
invoked = false;
keyWasHandled = false;
Assert.False (view.HasFocus);
Application.RaiseKeyDownEvent (Key.Z);
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.False (view.ApplicationCommand);
Assert.False (view.HotKeyCommand);
Assert.False (view.FocusedCommand);

View File

@@ -95,7 +95,7 @@ public class HotKeyTests
{
var view = new View ();
view.KeyBindings.Add (Key.A, Command.HotKey); // implies KeyBindingScope.Focused - so this should not be invoked
view.InvokingKeyBindings += (s, e) => { Assert.Fail (); };
view.KeyDownNotHandled += (s, e) => { Assert.Fail (); };
var superView = new View ();
superView.Add (view);
@@ -109,8 +109,11 @@ public class HotKeyTests
{
var view = new View ();
view.KeyBindings.Add (Key.A, KeyBindingScope.HotKey, Command.HotKey);
bool invoked = false;
view.InvokingKeyBindings += (s, e) => { invoked = true; };
bool hotKeyInvoked = false;
view.HandlingHotKey += (s, e) => { hotKeyInvoked = true; };
bool notHandled = false;
view.KeyDownNotHandled += (s, e) => { notHandled = true; };
var superView = new View ();
superView.Add (view);
@@ -118,7 +121,8 @@ public class HotKeyTests
var ke = Key.A;
superView.NewKeyDownEvent (ke);
Assert.True (invoked);
Assert.False (notHandled);
Assert.True (hotKeyInvoked);
}

View File

@@ -7,8 +7,8 @@ namespace Terminal.Gui.ViewTests;
public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
{
/// <summary>
/// This tests that when a new key down event is sent to the view will fire the 3 key-down related
/// events: KeyDown, InvokingKeyBindings, and ProcessKeyDown. Note that KeyUp is independent.
/// This tests that when a new key down event is sent to the view will fire the key-down related
/// events: KeyDown and KeyDownNotHandled. Note that KeyUp is independent.
/// </summary>
[Theory]
[MemberData (nameof (AllViewTypes))]
@@ -33,27 +33,18 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
keyDown = true;
};
var invokingKeyBindings = false;
view.InvokingKeyBindings += (s, a) =>
{
a.Handled = false; // don't handle it so the other events are called
invokingKeyBindings = true;
};
var keyDownProcessed = false;
var keyDownNotHandled = false;
view.KeyDownNotHandled += (s, a) =>
{
a.Handled = true;
keyDownProcessed = true;
keyDownNotHandled = true;
};
// Key.Empty is invalid, but it's used here to test that the event is fired
Assert.True (view.NewKeyDownEvent (Key.Empty)); // this will be true because the ProcessKeyDown event handled it
Assert.True (keyDown);
Assert.True (invokingKeyBindings);
Assert.True (keyDownProcessed);
Assert.True (keyDownNotHandled);
view.Dispose ();
}
@@ -96,7 +87,7 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
public void NewKeyDownUpEvents_Events_Are_Raised_With_Only_Key_Modifiers (bool shift, bool alt, bool control)
{
var keyDown = false;
var keyPressed = false;
var keyDownNotHandled = false;
var keyUp = false;
var view = new OnNewKeyTestView ();
@@ -112,7 +103,7 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
Assert.True (view.OnKeyDownCalled);
keyDown = true;
};
view.KeyDownNotHandled += (s, e) => { keyPressed = true; };
view.KeyDownNotHandled += (s, e) => { keyDownNotHandled = true; };
view.KeyUp += (s, e) =>
{
@@ -125,11 +116,6 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
keyUp = true;
};
//view.ProcessKeyDownEvent (new (Key.Null | (shift ? Key.ShiftMask : 0) | (alt ? Key.AltMask : 0) | (control ? Key.CtrlMask : 0)));
//Assert.True (keyDown);
//Assert.True (view.OnKeyDownWasCalled);
//Assert.True (view.OnProcessKeyDownWasCalled);
view.NewKeyDownEvent (
new (
KeyCode.Null
@@ -138,7 +124,7 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
| (control ? KeyCode.CtrlMask : 0)
)
);
Assert.True (keyPressed);
Assert.True (keyDownNotHandled);
Assert.True (view.OnKeyDownCalled);
Assert.True (view.OnProcessKeyDownCalled);
@@ -154,107 +140,11 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
Assert.True (view.OnKeyUpCalled);
}
[Fact]
public void NewKeyDownEvent_InvokingKeyBindings_Handled_Cancels ()
{
var view = new View ();
var keyPressInvoked = false;
var invokingKeyBindingsInvoked = false;
var processKeyPressInvoked = false;
var setHandledTo = false;
view.KeyDown += (s, e) =>
{
keyPressInvoked = true;
Assert.False (e.Handled);
Assert.Equal (KeyCode.N, e.KeyCode);
};
view.InvokingKeyBindings += (s, e) =>
{
invokingKeyBindingsInvoked = true;
e.Handled = setHandledTo;
Assert.Equal (setHandledTo, e.Handled);
Assert.Equal (KeyCode.N, e.KeyCode);
};
view.KeyDownNotHandled += (s, e) =>
{
processKeyPressInvoked = true;
processKeyPressInvoked = true;
Assert.False (e.Handled);
Assert.Equal (KeyCode.N, e.KeyCode);
};
view.NewKeyDownEvent (Key.N);
Assert.True (keyPressInvoked);
Assert.True (invokingKeyBindingsInvoked);
Assert.True (processKeyPressInvoked);
keyPressInvoked = false;
invokingKeyBindingsInvoked = false;
processKeyPressInvoked = false;
setHandledTo = true;
view.NewKeyDownEvent (Key.N);
Assert.True (keyPressInvoked);
Assert.True (invokingKeyBindingsInvoked);
Assert.False (processKeyPressInvoked);
}
[Fact]
public void NewKeyDownEvent_InvokingKeyBindings_Handled_True_Stops_Processing ()
{
var keyDown = false;
var invokingKeyBindings = false;
var keyPressed = false;
var view = new OnNewKeyTestView ();
Assert.True (view.CanFocus);
view.CancelVirtualMethods = false;
view.KeyDown += (s, e) =>
{
Assert.Equal (KeyCode.A, e.KeyCode);
Assert.False (keyDown);
Assert.True (view.OnKeyDownCalled);
e.Handled = false;
keyDown = true;
};
view.InvokingKeyBindings += (s, e) =>
{
Assert.Equal (KeyCode.A, e.KeyCode);
Assert.False (keyPressed);
Assert.True (view.OnInvokingKeyBindingsCalled);
e.Handled = true;
invokingKeyBindings = true;
};
view.KeyDownNotHandled += (s, e) =>
{
Assert.Equal (KeyCode.A, e.KeyCode);
Assert.False (keyPressed);
Assert.False (view.OnProcessKeyDownCalled);
e.Handled = true;
keyPressed = true;
};
view.NewKeyDownEvent (Key.A);
Assert.True (keyDown);
Assert.True (invokingKeyBindings);
Assert.False (keyPressed);
Assert.True (view.OnKeyDownCalled);
Assert.True (view.OnInvokingKeyBindingsCalled);
Assert.False (view.OnProcessKeyDownCalled);
}
[Fact]
public void NewKeyDownEvent_Handled_True_Stops_Processing ()
{
var keyDown = false;
var invokingKeyBindings = false;
var keyPressed = false;
var keyDownNotHandled = false;
var view = new OnNewKeyTestView ();
Assert.True (view.CanFocus);
@@ -269,31 +159,21 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
keyDown = true;
};
view.InvokingKeyBindings += (s, e) =>
{
Assert.Equal (KeyCode.A, e.KeyCode);
Assert.False (keyPressed);
Assert.False (view.OnInvokingKeyBindingsCalled);
e.Handled = true;
invokingKeyBindings = true;
};
view.KeyDownNotHandled += (s, e) =>
{
Assert.Equal (KeyCode.A, e.KeyCode);
Assert.False (keyPressed);
Assert.False (keyDownNotHandled);
Assert.False (view.OnProcessKeyDownCalled);
e.Handled = true;
keyPressed = true;
keyDownNotHandled = true;
};
view.NewKeyDownEvent (Key.A);
Assert.True (keyDown);
Assert.False (invokingKeyBindings);
Assert.False (keyPressed);
Assert.False (keyDownNotHandled);
Assert.True (view.OnKeyDownCalled);
Assert.False (view.OnInvokingKeyBindingsCalled);
Assert.False (view.OnProcessKeyDownCalled);
}
@@ -301,8 +181,7 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
public void NewKeyDownEvent_KeyDown_Handled_Stops_Processing ()
{
var view = new View ();
var invokingKeyBindingsInvoked = false;
var processKeyPressInvoked = false;
var keyDownNotHandled = false;
var setHandledTo = false;
view.KeyDown += (s, e) =>
@@ -312,38 +191,27 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
Assert.Equal (KeyCode.N, e.KeyCode);
};
view.InvokingKeyBindings += (s, e) =>
{
invokingKeyBindingsInvoked = true;
Assert.False (e.Handled);
Assert.Equal (KeyCode.N, e.KeyCode);
};
view.KeyDownNotHandled += (s, e) =>
{
processKeyPressInvoked = true;
keyDownNotHandled = true;
Assert.False (e.Handled);
Assert.Equal (KeyCode.N, e.KeyCode);
};
view.NewKeyDownEvent (Key.N);
Assert.True (invokingKeyBindingsInvoked);
Assert.True (processKeyPressInvoked);
Assert.True (keyDownNotHandled);
invokingKeyBindingsInvoked = false;
processKeyPressInvoked = false;
keyDownNotHandled = false;
setHandledTo = true;
view.NewKeyDownEvent (Key.N);
Assert.False (invokingKeyBindingsInvoked);
Assert.False (processKeyPressInvoked);
Assert.False (keyDownNotHandled);
}
[Fact]
public void NewKeyDownEvent_ProcessKeyDown_Handled_Stops_Processing ()
{
var keyDown = false;
var invokingKeyBindings = false;
var processKeyDown = false;
var keyDownNotHandled = false;
var view = new OnNewKeyTestView ();
Assert.True (view.CanFocus);
@@ -358,31 +226,20 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
keyDown = true;
};
view.InvokingKeyBindings += (s, e) =>
{
Assert.Equal (KeyCode.A, e.KeyCode);
Assert.False (processKeyDown);
Assert.True (view.OnInvokingKeyBindingsCalled);
e.Handled = false;
invokingKeyBindings = true;
};
view.KeyDownNotHandled += (s, e) =>
{
Assert.Equal (KeyCode.A, e.KeyCode);
Assert.False (processKeyDown);
Assert.False (keyDownNotHandled);
Assert.True (view.OnProcessKeyDownCalled);
e.Handled = true;
processKeyDown = true;
keyDownNotHandled = true;
};
view.NewKeyDownEvent (Key.A);
Assert.True (keyDown);
Assert.True (invokingKeyBindings);
Assert.True (processKeyDown);
Assert.True (keyDownNotHandled);
Assert.True (view.OnKeyDownCalled);
Assert.True (view.OnInvokingKeyBindingsCalled);
Assert.True (view.OnProcessKeyDownCalled);
}
@@ -409,7 +266,6 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
Assert.True (view.OnKeyUpCalled);
Assert.False (view.OnKeyDownCalled);
Assert.False (view.OnInvokingKeyBindingsCalled);
Assert.False (view.OnProcessKeyDownCalled);
}
@@ -417,12 +273,12 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
[InlineData (null, null)]
[InlineData (true, true)]
[InlineData (false, false)]
public void RaiseInvokingKeyBindingsAndInvokeCommands_Returns_Nullable_Properly (bool? toReturn, bool? expected)
public void InvokeCommandsBoundToKey_Returns_Nullable_Properly (bool? toReturn, bool? expected)
{
var view = new KeyBindingsTestView ();
view.CommandReturns = toReturn;
bool? result = view.RaiseInvokingKeyBindingsAndInvokeCommands (Key.A);
bool? result = view.InvokeCommandsBoundToKey (Key.A);
Assert.Equal (expected, result);
}
@@ -444,20 +300,11 @@ public class KeyboardEventTests (ITestOutputHelper output) : TestsAllViews
{
public OnNewKeyTestView () { CanFocus = true; }
public bool CancelVirtualMethods { set; private get; }
public bool OnInvokingKeyBindingsCalled { get; set; }
public bool OnKeyDownCalled { get; set; }
public bool OnProcessKeyDownCalled { get; set; }
public bool OnKeyUpCalled { get; set; }
public override string Text { get; set; }
protected override bool OnInvokingKeyBindings (Key keyEvent, KeyBindingScope scope)
{
OnInvokingKeyBindingsCalled = true;
return CancelVirtualMethods;
}
protected override bool OnKeyDown (Key keyEvent)
{
OnKeyDownCalled = true;

View File

@@ -11,37 +11,38 @@ public class ViewKeyBindingTests (ITestOutputHelper output)
public void Focus_KeyBinding ()
{
var view = new ScopedKeyBindingView ();
var invoked = false;
view.InvokingKeyBindings += (s, e) => invoked = true;
var keyWasHandled = false;
view.KeyDownNotHandled += (s, e) => keyWasHandled = true;
var top = new Toplevel ();
top.Add (view);
Application.Begin (top);
Application.RaiseKeyDownEvent (Key.A);
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.True (view.ApplicationCommand);
invoked = false;
keyWasHandled = false;
Application.RaiseKeyDownEvent (Key.H);
Assert.True (invoked);
Assert.True (view.HotKeyCommand);
Assert.False (keyWasHandled);
invoked = false;
keyWasHandled = false;
Assert.False (view.HasFocus);
Application.RaiseKeyDownEvent (Key.F);
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.False (view.FocusedCommand);
invoked = false;
keyWasHandled = false;
view.CanFocus = true;
view.SetFocus ();
Assert.True (view.HasFocus);
Application.RaiseKeyDownEvent (Key.F);
Assert.True (invoked);
Assert.True (view.FocusedCommand);
Assert.False (keyWasHandled); // Command was invoked, but wasn't handled
Assert.True (view.ApplicationCommand);
Assert.True (view.HotKeyCommand);
Assert.True (view.FocusedCommand);
top.Dispose ();
}
@@ -50,23 +51,23 @@ public class ViewKeyBindingTests (ITestOutputHelper output)
public void Focus_KeyBinding_Negative ()
{
var view = new ScopedKeyBindingView ();
var invoked = false;
view.InvokingKeyBindings += (s, e) => invoked = true;
var keyWasHandled = false;
view.KeyDownNotHandled += (s, e) => keyWasHandled = true;
var top = new Toplevel ();
top.Add (view);
Application.Begin (top);
Application.RaiseKeyDownEvent (Key.Z);
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.False (view.ApplicationCommand);
Assert.False (view.HotKeyCommand);
Assert.False (view.FocusedCommand);
invoked = false;
keyWasHandled = false;
Assert.False (view.HasFocus);
Application.RaiseKeyDownEvent (Key.F);
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.False (view.ApplicationCommand);
Assert.False (view.HotKeyCommand);
Assert.False (view.FocusedCommand);
@@ -78,28 +79,29 @@ public class ViewKeyBindingTests (ITestOutputHelper output)
public void HotKey_KeyBinding ()
{
var view = new ScopedKeyBindingView ();
var invoked = false;
view.InvokingKeyBindings += (s, e) => invoked = true;
var keyWasHandled = false;
view.KeyDownNotHandled += (s, e) => keyWasHandled = true;
var top = new Toplevel ();
top.Add (view);
Application.Begin (top);
invoked = false;
keyWasHandled = false;
Application.RaiseKeyDownEvent (Key.H);
Assert.True (invoked);
Assert.True (view.HotKeyCommand);
Assert.False (keyWasHandled);
view.HotKey = KeyCode.Z;
invoked = false;
keyWasHandled = false;
view.HotKeyCommand = false;
Application.RaiseKeyDownEvent (Key.H); // old hot key
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.False (view.HotKeyCommand);
Application.RaiseKeyDownEvent (Key.Z); // new hot key
Assert.True (invoked);
Assert.True (view.HotKeyCommand);
Assert.False (keyWasHandled);
top.Dispose ();
}
@@ -108,18 +110,18 @@ public class ViewKeyBindingTests (ITestOutputHelper output)
public void HotKey_KeyBinding_Negative ()
{
var view = new ScopedKeyBindingView ();
var invoked = false;
view.InvokingKeyBindings += (s, e) => invoked = true;
var keyWasHandled = false;
view.KeyDownNotHandled += (s, e) => keyWasHandled = true;
var top = new Toplevel ();
top.Add (view);
Application.Begin (top);
Application.RaiseKeyDownEvent (Key.Z);
Assert.False (invoked);
Assert.False (keyWasHandled);
Assert.False (view.HotKeyCommand);
invoked = false;
keyWasHandled = false;
Application.RaiseKeyDownEvent (Key.F);
Assert.False (view.HotKeyCommand);
top.Dispose ();

View File

@@ -59,22 +59,25 @@ The Command can be invoked even if the `View` that defines them is not focused o
### **Handling Keyboard Events**
Keyboard events are retrieved from [Console Drivers](drivers.md) and passed on
to the [Application](~/api/Terminal.Gui.Application.yml) class by the [Main Loop](mainloop.md).
Keyboard events are retrieved from [Console Drivers](drivers.md) each iteration of the [Application](~/api/Terminal.Gui.Application.yml) [Main Loop](mainloop.md). The console driver raises the @Terminal.Gui.ConsoleDriver.KeyDown and @Terminal.Gui.ConsoleDriver.KeyUp events which invoke @Terminal.Gui.Application.RaiseKeyDown(Terminal.Gui.Key) and @Terminal.Gui.Application.RaiseKeyUp(Terminal.Gui.Key) respectively.
[Application](~/api/Terminal.Gui.Application.yml) then determines the current [Toplevel](~/api/Terminal.Gui.Toplevel.yml) view
(either the default created by calling @Terminal.Gui.Application.Init(Terminal.Gui.ConsoleDriver,System.String), or the one set by calling `Application.Run`). The mouse event, using [Viewport-relative coordinates](xref:Terminal.Gui.View.Viewport) is then passed to the @Terminal.Gui.View.NewKeyDownEvent(Terminal.Gui.Key) method of the current [Toplevel](~/api/Terminal.Gui.Toplevel.yml) view.
NOTE: Not all drivers/platforms support sensing distinct KeyUp events. These drivers will simulate KeyUp events by raising @Terminal.Gui.ConsoleDriver.KeyUp after @Terminal.Gui.ConsoleDriver.KeyDown.
If the view is enabled, the @Terminal.Gui.View.NewKeyDownEvent(Terminal.Gui.Key) method will do the following:
@Terminal.Gui.Application.RaiseKeyDown(Terminal.Gui.Key) raises @Terminal.Gui.Application.KeyDown and then calls @Terminal.Gui.View.NewKeyDownEvent(Terminal.Gui.Key) on all toplevel Views. If no View handles the key event, any Application-scoped key bindings will be invoked.
1) If the view has a subview that has focus, 'ProcessKeyDown' on the focused view will be called. If the focused view handles the key press, processing stops.
2) If there is no focused sub-view, or the focused sub-view does not handle the key press, @Terminal.Gui.View.OnKeyDown(Terminal.Gui.Key) will be called. If the view handles the key press, processing stops.
3) If the view does not handle the key press, @Terminal.Gui.TextField.OnInvokingKeyBindings(Terminal.Gui.Key,Terminal.Gui.KeyBindingScope) will be called. This method calls @Terminal.Gui.View.InvokeKeyBindings(Terminal.Gui.Key,Terminal.Gui.KeyBindingScope) to invoke any keys bound to commands. If the key is bound and any of it's command handlers return true, processing stops.
4) If the key is not bound, or the bound command handlers do not return true, @Terminal.Gui.View.OnProcessKeyDown(Terminal.Gui.Key) is called. If the view handles the key press, processing stops.
@Terminal.Gui.Application.RaiseKeyDown(Terminal.Gui.Key) raises @Terminal.Gui.Application.KeyDown and then calls @Terminal.Gui.View.NewKeyUpEvent(Terminal.Gui.Key) on all toplevel Views.
If a view is enabled, the @Terminal.Gui.View.NewKeyDownEvent(Terminal.Gui.Key) method will do the following:
1) If the view has a subview that has focus, 'NewKeyDown' on the focused view will be called. This is recursive. If the most-focused view handles the key press, processing stops.
2) If there is no most-focused sub-view, or a most-focused sub-view does not handle the key press, @Terminal.Gui.View.OnKeyDown(Terminal.Gui.Key) will be called. If the view handles the key press, processing stops.
3) If @Terminal.Gui.View.OnKeyDown(Terminal.Gui.Key) does not handle the event. @Terminal.Gui.View.KeyDown will be raised.
4) If the view does not handle the key down event, any bindings for the key will be invoked (see @Terminal.Gui.View.KeyBindings). If the key is bound and any of it's command handlers return true, processing stops.
5) If the key is not bound, or the bound command handlers do not return true, @Terminal.Gui.View.OnKeyDownNotHandled(Terminal.Gui.Key) is called.
## **Application Key Handling**
To define application key handling logic for an entire application in cases where the methods listed above are not suitable, use the `Application.OnKeyDown` event.
To define application key handling logic for an entire application in cases where the methods listed above are not suitable, use the @Terminal.Gui.Application.KeyDown event.
## **Key Down/Up Events**
@@ -90,17 +93,14 @@ To define application key handling logic for an entire application in cases wher
- `NewKeyDownEvent` is called on the most-focused SubView (if any) that has focus. If that call returns true, the method returns.
- Calls `OnKeyDown`.
- **During**
- Assuming `OnKeyDown` call returns false (indicating the key wasn't handled)
- `OnInvokingKeyBindings` is called to invoke any bound commands.
- `OnInvokingKeyBindings` fires the `InvokingKeyBindings` event
- Assuming `OnKeyDown` call returns false (indicating the key wasn't handled) any commands bound to the key will be invoked.
- **After**
- Assuming `OnInvokingKeyBindings` returns false (indicating the key wasn't handled)
- `OnProcessKeyDown` is called to process the key.
- `OnProcessKeyDown` fires the `ProcessKeyDown` event
- Assuming no keybinding was found or all invoked commands were not handled:
- `OnKeyDownNotHandled` is called to process the key.
- `KeyDownNotHandled` is raised.
- Subclasses of `View` can (rarely) override `OnKeyDown` to see keys before they are processed by `OnInvokingKeyBindings` and `OnProcessKeyDown
- Subclasses of `View` can (rarely) override `OnInvokingKeyBindings` to see keys before they are processed by `OnProcessKeyDown`
- Subclasses of `View` can (often) override `OnProcessKeyDown` to do normal key processing.
- Subclasses of `View` can (rarely) override `OnKeyDown` (or subscribe to `KeyDown`) to see keys before they are processed
- Subclasses of `View` can (often) override `OnKeyDownNotHandled` to do key processing for keys that were not previously handled. `TextField` and `TextView` are examples.
## ConsoleDriver