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