diff --git a/Terminal.Gui/Application/Application.Mouse.cs b/Terminal.Gui/Application/Application.Mouse.cs
index 38db8727c..90973ab68 100644
--- a/Terminal.Gui/Application/Application.Mouse.cs
+++ b/Terminal.Gui/Application/Application.Mouse.cs
@@ -159,7 +159,7 @@ public static partial class Application // Mouse handling
return;
}
- if (GrabMouse (deepestViewUnderMouse, mouseEvent))
+ if (HandleMouseGrab (deepestViewUnderMouse, mouseEvent))
{
return;
}
@@ -245,7 +245,7 @@ public static partial class Application // Mouse handling
}
}
- internal static bool GrabMouse (View? deepestViewUnderMouse, MouseEvent mouseEvent)
+ internal static bool HandleMouseGrab (View? deepestViewUnderMouse, MouseEvent mouseEvent)
{
if (MouseGrabView is { })
{
diff --git a/Terminal.Gui/View/View.Keyboard.cs b/Terminal.Gui/View/View.Keyboard.cs
index 9561b6a5c..6d15692dc 100644
--- a/Terminal.Gui/View/View.Keyboard.cs
+++ b/Terminal.Gui/View/View.Keyboard.cs
@@ -14,6 +14,8 @@ public partial class View // Keyboard APIs
HotKeySpecifier = (Rune)'_';
TitleTextFormatter.HotKeyChanged += TitleTextFormatter_HotKeyChanged;
+ // TODO: It's incorrect to think of Commands as being Keyboard things. The code below should be moved to View.cs
+
// By default, the HotKey command sets the focus
AddCommand (Command.HotKey, OnHotKey);
@@ -614,7 +616,7 @@ public partial class View // Keyboard APIs
// Now, process any key bindings in the subviews that are tagged to KeyBindingScope.HotKey.
foreach (View subview in Subviews)
{
- if (subview == Focused)
+ if (subview.HasFocus)
{
continue;
}
diff --git a/Terminal.Gui/View/View.Mouse.cs b/Terminal.Gui/View/View.Mouse.cs
index 11b669562..22b637794 100644
--- a/Terminal.Gui/View/View.Mouse.cs
+++ b/Terminal.Gui/View/View.Mouse.cs
@@ -406,7 +406,11 @@ public partial class View // Mouse APIs
// If mouse is still in bounds, generate a click
if (!WantContinuousButtonPressed && Viewport.Contains (mouseEvent.Position))
{
- return OnMouseClick (new (mouseEvent));
+ var meea = new MouseEventEventArgs (mouseEvent);
+
+ // We can ignore the return value of OnMouseClick; if the click is handled
+ // meea.Handled and meea.MouseEvent.Handled will be true
+ OnMouseClick (meea);
}
return mouseEvent.Handled = true;
diff --git a/Terminal.Gui/View/View.Navigation.cs b/Terminal.Gui/View/View.Navigation.cs
index 6774e959c..1e8f41bad 100644
--- a/Terminal.Gui/View/View.Navigation.cs
+++ b/Terminal.Gui/View/View.Navigation.cs
@@ -292,7 +292,7 @@ public partial class View // Focus and cross-view navigation management (TabStop
///
internal bool RestoreFocus ()
{
- View [] indicies = GetFocusChain (NavigationDirection.Forward, TabStop);
+ View [] indicies = GetFocusChain (NavigationDirection.Forward, null);
if (Focused is null && _previouslyFocused is { } && indicies.Contains (_previouslyFocused))
{
diff --git a/Terminal.Gui/Views/Button.cs b/Terminal.Gui/Views/Button.cs
index 742c34151..1141a4f7b 100644
--- a/Terminal.Gui/Views/Button.cs
+++ b/Terminal.Gui/Views/Button.cs
@@ -21,7 +21,8 @@ namespace Terminal.Gui;
/// be fired.
///
///
-/// Set to to have the event
+/// Set to to have the
+/// event
/// invoked repeatedly while the button is pressed.
///
///
@@ -34,13 +35,13 @@ public class Button : View, IDesignable
private bool _isDefault;
///
- /// Gets or sets whether s are shown with a shadow effect by default.
+ /// Gets or sets whether s are shown with a shadow effect by default.
///
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
public static ShadowStyle DefaultShadow { get; set; } = ShadowStyle.None;
///
- /// Gets or sets the default Highlight Style.
+ /// Gets or sets the default Highlight Style.
///
[SerializableConfigurationProperty (Scope = typeof (ThemeScope))]
public static HighlightStyle DefaultHighlightStyle { get; set; } = HighlightStyle.Pressed | HighlightStyle.Hover;
@@ -62,11 +63,31 @@ public class Button : View, IDesignable
CanFocus = true;
// Override default behavior of View
- AddCommand (Command.HotKey, () =>
- {
- SetFocus ();
- return !OnAccept ();
- });
+ AddCommand (
+ Command.HotKey,
+ () =>
+ {
+ bool cachedIsDefault = IsDefault; // Supports "Swap Default" in Buttons scenario
+
+ bool? handled = OnAccept ();
+
+ if (handled == true)
+ {
+ return true;
+ }
+
+ SetFocus ();
+
+ // TODO: If `IsDefault` were a property on `View` *any* View could work this way. That's theoretical as
+ // TODO: no use-case has been identified for any View other than Button to act like this.
+ // If Accept was not handled...
+ if (cachedIsDefault && SuperView is { })
+ {
+ return SuperView.InvokeCommand (Command.Accept);
+ }
+
+ return false;
+ });
KeyBindings.Add (Key.Space, Command.HotKey);
KeyBindings.Add (Key.Enter, Command.HotKey);
@@ -80,7 +101,7 @@ public class Button : View, IDesignable
private bool _wantContinuousButtonPressed;
- ///
+ ///
public override bool WantContinuousButtonPressed
{
get => _wantContinuousButtonPressed;
@@ -104,10 +125,7 @@ public class Button : View, IDesignable
}
}
- private void Button_MouseClick (object sender, MouseEventEventArgs e)
- {
- e.Handled = InvokeCommand (Command.HotKey) == true;
- }
+ private void Button_MouseClick (object sender, MouseEventEventArgs e) { e.Handled = InvokeCommand (Command.HotKey) == true; }
private void Button_TitleChanged (object sender, EventArgs e)
{
@@ -115,22 +133,24 @@ public class Button : View, IDesignable
TextFormatter.HotKeySpecifier = HotKeySpecifier;
}
- ///
+ ///
public override string Text
{
- get => base.Title;
- set => base.Text = base.Title = value;
+ get => Title;
+ set => base.Text = Title = value;
}
- ///
+ ///
public override Rune HotKeySpecifier
{
get => base.HotKeySpecifier;
set => TextFormatter.HotKeySpecifier = base.HotKeySpecifier = value;
}
- /// Gets or sets whether the is the default action to activate in a dialog.
- /// true if is default; otherwise, false.
+ ///
+ /// Gets or sets whether the will invoke the
+ /// command on the if is not handled by a subscriber.
+ ///
public bool IsDefault
{
get => _isDefault;
@@ -158,6 +178,7 @@ public class Button : View, IDesignable
if (TextFormatter.Text [i] == Text [0])
{
Move (i, 0);
+
return null; // Don't show the cursor
}
}
@@ -170,6 +191,7 @@ public class Button : View, IDesignable
protected override void UpdateTextFormatterText ()
{
base.UpdateTextFormatterText ();
+
if (NoDecorations)
{
TextFormatter.Text = Text;
@@ -191,11 +213,11 @@ public class Button : View, IDesignable
}
}
- ///
+ ///
public bool EnableForDesign ()
{
Title = "_Button";
return true;
}
-}
\ No newline at end of file
+}
diff --git a/Terminal.Gui/Views/FrameView.cs b/Terminal.Gui/Views/FrameView.cs
index f10967413..00b6bb806 100644
--- a/Terminal.Gui/Views/FrameView.cs
+++ b/Terminal.Gui/Views/FrameView.cs
@@ -1,5 +1,6 @@
namespace Terminal.Gui;
+// TODO: FrameView is mis-named, really. It's far more about it being a TabGroup than a frame.
///
/// The FrameView is a container View with a border around it.
///
@@ -23,6 +24,7 @@ public class FrameView : View
private void FrameView_MouseClick (object sender, MouseEventEventArgs e)
{
+ // base sets focus on HotKey
e.Handled = InvokeCommand (Command.HotKey) == true;
}
diff --git a/Terminal.Gui/Views/Window.cs b/Terminal.Gui/Views/Window.cs
index 01cf4f752..1cae3dbc4 100644
--- a/Terminal.Gui/Views/Window.cs
+++ b/Terminal.Gui/Views/Window.cs
@@ -32,25 +32,6 @@ public class Window : Toplevel
BorderStyle = DefaultBorderStyle;
ShadowStyle = DefaultShadow;
- // This enables the default button to be activated by the Enter key.
- AddCommand (
- Command.Accept,
- () =>
- {
- // TODO: Perhaps all views should support the concept of being default?
- // ReSharper disable once InvertIf
- if (Subviews.FirstOrDefault (v => v is Button { IsDefault: true, Enabled: true }) is Button
- defaultBtn)
- {
- defaultBtn.InvokeCommand (Command.Accept);
-
- return true;
- }
-
- return OnAccept ();
- }
- );
-
KeyBindings.Add (Key.Enter, Command.Accept);
}
diff --git a/UICatalog/Scenarios/Buttons.cs b/UICatalog/Scenarios/Buttons.cs
index 5c8e0865c..f3e9158dd 100644
--- a/UICatalog/Scenarios/Buttons.cs
+++ b/UICatalog/Scenarios/Buttons.cs
@@ -32,7 +32,7 @@ public class Buttons : Scenario
// This is the default button (IsDefault = true); if user presses ENTER in the TextField
// the scenario will quit
var defaultButton = new Button { X = Pos.Center (), Y = Pos.AnchorEnd (), IsDefault = true, Text = "_Quit" };
- defaultButton.Accept += (s, e) => Application.RequestStop ();
+ main.Accept += (s, e) => Application.RequestStop ();
main.Add (defaultButton);
var swapButton = new Button
@@ -46,6 +46,7 @@ public class Buttons : Scenario
swapButton.Accept += (s, e) =>
{
+ e.Handled = !swapButton.IsDefault;
defaultButton.IsDefault = !defaultButton.IsDefault;
swapButton.IsDefault = !swapButton.IsDefault;
};
@@ -57,6 +58,7 @@ public class Buttons : Scenario
{
string btnText = button.Text;
MessageBox.Query ("Message", $"Did you click {txt}?", "Yes", "No");
+ e.Handled = true;
};
}
@@ -96,11 +98,19 @@ public class Buttons : Scenario
main.Add (
button = new () { X = 2, Y = Pos.Bottom (button) + 1, Height = 2, Text = "a Newline\nin the button" }
);
- button.Accept += (s, e) => MessageBox.Query ("Message", "Question?", "Yes", "No");
+ button.Accept += (s, e) =>
+ {
+ MessageBox.Query ("Message", "Question?", "Yes", "No");
+ e.Handled = true;
+ };
var textChanger = new Button { X = 2, Y = Pos.Bottom (button) + 1, Text = "Te_xt Changer" };
main.Add (textChanger);
- textChanger.Accept += (s, e) => textChanger.Text += "!";
+ textChanger.Accept += (s, e) =>
+ {
+ textChanger.Text += "!";
+ e.Handled = true;
+ };
main.Add (
button = new ()
diff --git a/UICatalog/Scenarios/Dialogs.cs b/UICatalog/Scenarios/Dialogs.cs
index 5872cd093..167a6816c 100644
--- a/UICatalog/Scenarios/Dialogs.cs
+++ b/UICatalog/Scenarios/Dialogs.cs
@@ -22,6 +22,7 @@ public class Dialogs : Scenario
var frame = new FrameView
{
+ TabStop = TabBehavior.TabStop, // FrameView normally sets to TabGroup
X = Pos.Center (),
Y = 1,
Width = Dim.Percent (75),
@@ -181,7 +182,7 @@ public class Dialogs : Scenario
X = Pos.Center (), Y = Pos.Bottom (frame) + 2, IsDefault = true, Text = "_Show Dialog"
};
- showDialogButton.Accept += (s, e) =>
+ app.Accept += (s, e) =>
{
Dialog dlg = CreateDemoDialog (
widthEdit,
@@ -194,6 +195,7 @@ public class Dialogs : Scenario
);
Application.Run (dlg);
dlg.Dispose ();
+ e.Handled = true;
};
app.Add (showDialogButton);
diff --git a/docfx/docs/navigation.md b/docfx/docs/navigation.md
index c68b56d61..b8f76e531 100644
--- a/docfx/docs/navigation.md
+++ b/docfx/docs/navigation.md
@@ -6,6 +6,7 @@
- What are the visual cues that help the user know what keystrokes will change the focus?
- What are the visual cues that help the user know what keystrokes will cause action in elements of the application that don't currently have focus?
- What is the order in which UI elements are traversed when using keyboard navigation?
+- What are the default actions for standard key/mouse input (e.g. Hotkey, `Space`, `Enter`, `MouseClick`)?
## Lexicon & Taxonomy
@@ -208,7 +209,18 @@ These could also be named `Gain/Lose`. They could also be combined into a single
QUESTION: Should we retain the same names as in v1 to simplify porting? Or, given the semantics of `Handled` v. `Cancel` are reversed would it be better to rename and/or combine?
-## `TabIndex` and `TabIndexes`
+## Built-In Views Interactivity
+
+| | | | | **Keyboard** | | | | **Mouse** | | | | |
+|----------------|-------------------------|------------|---------------|--------------|-----------------------|------------------------------|---------------------------|------------------------------|------------------------------|------------------------------|----------------|---------------|
+| | **Number
of States** | **Static** | **IsDefault** | **Hotkeys** | **Select
Command** | **Accept
Command** | **Hotkey
Command** | **CanFocus
Click** | **CanFocus
DblCLick** | **!CanFocus
Click** | **RightClick** | **GrabMouse** |
+| **View** | 1 | Yes | No | 1 | OnSelect | OnAccept | Focus | Focus | | | | No |
+| **Label** | 1 | Yes | No | 1 | OnSelect | OnAccept | FocusNext | Focus | | FocusNext | | No |
+| **Button** | 1 | No | Yes | 1 | OnSelect | Focus
OnAccept | Focus
OnAccept | HotKey | | Select | | No |
+| **Checkbox** | 3 | No | No | 1 | OnSelect
Advance | OnAccept | OnAccept | Select | | Select | | No |
+| **RadioGroup** | > 1 | No | No | 2+ | Advance | Set SelectedItem
OnAccept | Focus
Set SelectedItem | SetFocus
Set _cursor | | SetFocus
Set _cursor | | No |
+| **Slider** | > 1 | No | No | 1 | SetFocusedOption | SetFocusedOption
OnAccept | Focus | SetFocus
SetFocusedOption | | SetFocus
SetFocusedOption | | Yes |
+| **ListView** | > 1 | No | No | 1 | MarkUnMarkRow | OpenSelectedItem
OnAccept | OnAccept | SetMark
OnSelectedChanged | OpenSelectedItem
OnAccept | | | No |
### v1 Behavior