Tig пре 10 месеци
родитељ
комит
878412e5d6

+ 30 - 6
Terminal.Gui/Input/Command.cs

@@ -3,18 +3,42 @@
 
 namespace Terminal.Gui;
 
-/// <summary>Actions which can be performed by the application or bound to keys in a <see cref="View"/> control.</summary>
+/// <summary>
+///     Actions which can be performed by a <see cref="View"/>. Commands are typically bound to keys via
+///     <see cref="View.KeyBindings"/> and mouse events.
+///     See also <see cref="View.InvokeCommand"/>.
+/// </summary>
 public enum Command
 {
     #region Base View Commands
 
-    /// <summary>Invoked when the HotKey for the View has been pressed.</summary>
-    HotKey,
-
-    /// <summary>Accepts the current state (e.g. list selection, button press, toggle, etc.).</summary>
+    /// <summary>
+    ///     Accepts the current state of the View (e.g. list selection, button press, checkbox state, etc.).
+    ///     <para>
+    ///         The default implementation in <see cref="View"/> calls <see cref="View.RaiseAcceptEvent"/> which raises the
+    ///         <see cref="View.Accept"/> event. If the event is not handled,
+    ///         the command is invoked on <see cref="View.SuperView"/>. This enables default Accept behavior.
+    ///     </para>
+    /// </summary>
     Accept,
 
-    /// <summary>Selects an item (e.g. a list item or menu item) without necessarily accepting it.</summary>
+    /// <summary>
+    ///     Performs a hotkey action (e.g. setting focus, accepting, and/or moving focus to the next View).
+    ///     <para>
+    ///         The default implementation in <see cref="View"/> calls <see cref="View.SetFocus"/> and then
+    ///         <see cref="View.RaiseHotKeyCommandEvent"/> which raises the
+    ///         <see cref="View.HotKeyCommand"/> event.
+    ///     </para>
+    /// </summary>
+    HotKey,
+
+    /// <summary>
+    ///     Selects an item (e.g. a list item or menu item) without necessarily accepting it.
+    ///     <para>
+    ///         The default implementation in <see cref="View"/> calls <see cref="View.RaiseSelectEvent"/> which raises the
+    ///         <see cref="View.Select"/> event.
+    ///     </para>
+    /// </summary>
     Select,
 
     #endregion

+ 260 - 0
Terminal.Gui/View/View.Command.cs

@@ -0,0 +1,260 @@
+#nullable enable
+using System.ComponentModel;
+
+namespace Terminal.Gui;
+
+public partial class View // Command APIs
+{
+    #region Default Implementation
+
+    /// <summary>
+    ///     Helper to configure all things Command related for a View. Called from the View constructor.
+    /// </summary>
+    private void SetupCommands ()
+    {
+        AddCommand (Command.Accept, RaiseAcceptEvent);
+
+        AddCommand (
+                    Command.HotKey,
+                    () =>
+                    {
+                        SetFocus ();
+
+                        return RaiseHotKeyCommandEvent ();
+                    });
+
+        AddCommand (Command.Select, RaiseSelectEvent);
+    }
+
+    /// <summary>
+    ///     Called when the <see cref="Command.Accept"/> command is invoked. Raises <see cref="Accept"/>
+    ///     event.
+    /// </summary>
+    /// <returns>
+    ///     If <see langword="true"/> the event was canceled. If <see langword="false"/> the event was raised but not canceled.
+    ///     If <see langword="null"/> no event was raised.
+    /// </returns>
+    protected bool? RaiseAcceptEvent ()
+    {
+        HandledEventArgs args = new ();
+
+        // Best practice is to invoke the virtual method first.
+        // This allows derived classes to handle the event and potentially cancel it.
+        args.Handled = OnAccept (args) || args.Handled;
+
+        if (!args.Handled)
+        {
+            // If the event is not canceled by the virtual method, raise the event to notify any external subscribers.
+            Accept?.Invoke (this, args);
+        }
+
+        // Accept is a special case where if the event is not canceled, the event is bubbled up the SuperView hierarchy.
+        if (!args.Handled)
+        {
+            return SuperView?.InvokeCommand (Command.Accept) == true;
+        }
+
+        return Accept is null ? null : args.Handled;
+    }
+
+    /// <summary>
+    ///     Called when the <see cref="Command.Accept"/> command is received. Set <see cref="HandledEventArgs.Handled"/> to
+    ///     <see langword="true"/> to stop processing.
+    /// </summary>
+    /// <param name="args"></param>
+    /// <returns><see langword="true"/> to stop processing.</returns>
+    protected virtual bool OnAccept (HandledEventArgs args) { return false; }
+
+    /// <summary>
+    ///     Cancelable event raised when the <see cref="Command.Accept"/> command is invoked. Set
+    ///     <see cref="HandledEventArgs.Handled"/>
+    ///     to cancel the event.
+    /// </summary>
+    public event EventHandler<HandledEventArgs>? Accept;
+
+    /// <summary>
+    ///     Called when the <see cref="Command.Select"/> command is invoked. Raises <see cref="Select"/>
+    ///     event.
+    /// </summary>
+    /// <returns>
+    ///     If <see langword="true"/> the event was canceled. If <see langword="false"/> the event was raised but not canceled.
+    ///     If <see langword="null"/> no event was raised.
+    /// </returns>
+    protected bool? RaiseSelectEvent ()
+    {
+        HandledEventArgs args = new ();
+
+        // Best practice is to invoke the virtual method first.
+        // This allows derived classes to handle the event and potentially cancel it.
+        if (OnSelect (args) || args.Handled)
+        {
+            return true;
+        }
+
+        // If the event is not canceled by the virtual method, raise the event to notify any external subscribers.
+        Select?.Invoke (this, args);
+
+        return Select is null ? null : args.Handled;
+    }
+
+    /// <summary>
+    ///     Called when the <see cref="Command.Select"/> command is received. Set <see cref="HandledEventArgs.Handled"/> to
+    ///     <see langword="true"/> to stop processing.
+    /// </summary>
+    /// <param name="args"></param>
+    /// <returns><see langword="true"/> to stop processing.</returns>
+    protected virtual bool OnSelect (HandledEventArgs args) { return false; }
+
+    /// <summary>
+    ///     Cancelable event raised when the <see cref="Command.Select"/> command is invoked. Set
+    ///     <see cref="HandledEventArgs.Handled"/>
+    ///     to cancel the event.
+    /// </summary>
+    public event EventHandler<HandledEventArgs>? Select;
+
+    /// <summary>
+    ///     Called when the <see cref="Command.HotKey"/> command is invoked. Raises <see cref="HotKey"/>
+    ///     event.
+    /// </summary>
+    /// <returns>
+    ///     If <see langword="true"/> the event was handled. If <see langword="false"/> the event was raised but not handled.
+    ///     If <see langword="null"/> no event was raised.
+    /// </returns>
+    protected bool? RaiseHotKeyCommandEvent ()
+    {
+        HandledEventArgs args = new ();
+
+        // Best practice is to invoke the virtual method first.
+        // This allows derived classes to handle the event and potentially cancel it.
+        if (OnHotKeyCommand (args) || args.Handled)
+        {
+            return true;
+        }
+
+        // If the event is not canceled by the virtual method, raise the event to notify any external subscribers.
+        HotKeyCommand?.Invoke (this, args);
+
+        return HotKeyCommand is null ? null : args.Handled;
+    }
+
+    /// <summary>
+    ///     Called when the <see cref="Command.HotKey"/> command is received. Set <see cref="HandledEventArgs.Handled"/> to
+    ///     <see langword="true"/> to stop processing.
+    /// </summary>
+    /// <param name="args"></param>
+    /// <returns><see langword="true"/> to stop processing.</returns>
+    protected virtual bool OnHotKeyCommand (HandledEventArgs args) { return false; }
+
+    /// <summary>
+    ///     Cancelable event raised when the <see cref="Command.HotKey"/> command is invoked. Set
+    ///     <see cref="HandledEventArgs.Handled"/>
+    ///     to cancel the event.
+    /// </summary>
+    public event EventHandler<HandledEventArgs>? HotKeyCommand;
+
+    #endregion Default Implementation
+
+    /// <summary>
+    ///     <para>
+    ///         Sets the function that will be invoked for a <see cref="Command"/>. Views should call
+    ///         AddCommand for each command they support.
+    ///     </para>
+    ///     <para>
+    ///         If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
+    ///         replace the old one.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         This version of AddCommand is for commands that require <see cref="CommandContext"/>. Use
+    ///         <see cref="AddCommand(Command,Func{System.Nullable{bool}})"/>
+    ///         in cases where the command does not require a <see cref="CommandContext"/>.
+    ///     </para>
+    /// </remarks>
+    /// <param name="command">The command.</param>
+    /// <param name="f">The function.</param>
+    protected void AddCommand (Command command, Func<CommandContext, bool?> f) { CommandImplementations [command] = f; }
+
+    /// <summary>
+    ///     <para>
+    ///         Sets the function that will be invoked for a <see cref="Command"/>. Views should call
+    ///         AddCommand for each command they support.
+    ///     </para>
+    ///     <para>
+    ///         If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
+    ///         replace the old one.
+    ///     </para>
+    /// </summary>
+    /// <remarks>
+    ///     <para>
+    ///         This version of AddCommand is for commands that do not require a <see cref="CommandContext"/>.
+    ///         If the command requires context, use
+    ///         <see cref="AddCommand(Command,Func{CommandContext,System.Nullable{bool}})"/>
+    ///     </para>
+    /// </remarks>
+    /// <param name="command">The command.</param>
+    /// <param name="f">The function.</param>
+    protected void AddCommand (Command command, Func<bool?> f) { CommandImplementations [command] = ctx => f (); }
+
+    /// <summary>Returns all commands that are supported by this <see cref="View"/>.</summary>
+    /// <returns></returns>
+    public IEnumerable<Command> GetSupportedCommands () { return CommandImplementations.Keys; }
+
+    /// <summary>
+    ///     Invokes the specified commands.
+    /// </summary>
+    /// <param name="commands"></param>
+    /// <param name="key">The key that caused the commands to be invoked, if any.</param>
+    /// <param name="keyBinding"></param>
+    /// <returns>
+    ///     <see langword="null"/> if no command was found.
+    ///     <see langword="true"/> if the command was invoked the command was handled.
+    ///     <see langword="false"/> if the command was invoked and the command was not handled.
+    /// </returns>
+    public bool? InvokeCommands (Command [] commands, Key? key = null, KeyBinding? keyBinding = null)
+    {
+        bool? toReturn = null;
+
+        foreach (Command command in commands)
+        {
+            if (!CommandImplementations.ContainsKey (command))
+            {
+                throw new NotSupportedException (@$"{command} is not supported by ({GetType ().Name}).");
+            }
+
+            // each command has its own return value
+            bool? thisReturn = InvokeCommand (command, key, keyBinding);
+
+            // if we haven't got anything yet, the current command result should be used
+            toReturn ??= thisReturn;
+
+            // if ever see a true then that's what we will return
+            if (thisReturn ?? false)
+            {
+                toReturn = true;
+            }
+        }
+
+        return toReturn;
+    }
+
+    /// <summary>Invokes the specified command.</summary>
+    /// <param name="command">The command to invoke.</param>
+    /// <param name="key">The key that caused the command to be invoked, if any.</param>
+    /// <param name="keyBinding"></param>
+    /// <returns>
+    ///     <see langword="null"/> if no command was found. <see langword="true"/> if the command was invoked, and it
+    ///     handled the command. <see langword="false"/> if the command was invoked, and it did not handle the command.
+    /// </returns>
+    public bool? InvokeCommand (Command command, Key? key = null, KeyBinding? keyBinding = null)
+    {
+        if (CommandImplementations.TryGetValue (command, out Func<CommandContext, bool?>? implementation))
+        {
+            var context = new CommandContext (command, key, keyBinding); // Create the context here
+
+            return implementation (context);
+        }
+
+        return null;
+    }
+}

+ 15 - 115
Terminal.Gui/View/View.Keyboard.cs

@@ -34,16 +34,22 @@ public partial class View // Keyboard APIs
 
     /// <summary>
     ///     Gets or sets the hot key defined for this view. Pressing the hot key on the keyboard while this view has focus will
-    ///     invoke the <see cref="Command.HotKey"/> and <see cref="Command.Accept"/> commands. <see cref="Command.HotKey"/>
-    ///     causes the view to be focused and <see cref="Command.Accept"/> does nothing. By default, the HotKey is
-    ///     automatically set to the first character of <see cref="Text"/> that is prefixed with <see cref="HotKeySpecifier"/>.
+    ///     invoke <see cref="Command.HotKey"/>. By default, the HotKey is set to the first character of <see cref="Text"/>
+    ///     that is prefixed with <see cref="HotKeySpecifier"/>.
     ///     <para>
-    ///         A HotKey is a keypress that selects a visible UI item. For selecting items across <see cref="View"/>`s (e.g.a
-    ///         <see cref="Button"/> in a <see cref="Dialog"/>) the keypress must include the <see cref="Key.WithAlt"/>
-    ///         modifier. For selecting items within a View that are not Views themselves, the keypress can be key without the
-    ///         Alt modifier. For example, in a Dialog, a Button with the text of "_Text" can be selected with Alt-T. Or, in a
-    ///         <see cref="Menu"/> with "_File _Edit", Alt-F will select (show) the "_File" menu. If the "_File" menu has a
-    ///         sub-menu of "_New" `Alt-N` or `N` will ONLY select the "_New" sub-menu if the "_File" menu is already opened.
+    ///         A HotKey is a keypress that causes a visible UI item to perform an action. For example, in a Dialog,
+    ///         with a Button with the text of "_Text" <c>Alt+T</c> will cause the button to gain focus and to raise its
+    ///         <see cref="Accept"/> event.
+    ///         Or, in a
+    ///         <see cref="Menu"/> with "_File _Edit", <c>Alt+F</c> will select (show) the "_File" menu. If the "_File" menu
+    ///         has a
+    ///         sub-menu of "_New" <c>Alt+N</c> or <c>N</c> will ONLY select the "_New" sub-menu if the "_File" menu is already
+    ///         opened.
+    ///     </para>
+    ///     <para>
+    ///         View subclasses can use< see cref="View.AddCommand(Command,Func{CommandContext,System.Nullable{bool}})"/> to
+    ///         define the
+    ///         behavior of the hot key.
     ///     </para>
     /// </summary>
     /// <remarks>
@@ -741,111 +747,5 @@ public partial class View // Keyboard APIs
         return toReturn;
     }
 
-    /// <summary>
-    ///     Invokes the specified commands.
-    /// </summary>
-    /// <param name="commands"></param>
-    /// <param name="key">The key that caused the commands to be invoked, if any.</param>
-    /// <param name="keyBinding"></param>
-    /// <returns>
-    ///     <see langword="null"/> if no command was found.
-    ///     <see langword="true"/> if the command was invoked the command was handled.
-    ///     <see langword="false"/> if the command was invoked and the command was not handled.
-    /// </returns>
-    public bool? InvokeCommands (Command [] commands, Key? key = null, KeyBinding? keyBinding = null)
-    {
-        bool? toReturn = null;
-
-        foreach (Command command in commands)
-        {
-            if (!CommandImplementations.ContainsKey (command))
-            {
-                throw new NotSupportedException (@$"{command} is not supported by ({GetType ().Name}).");
-            }
-
-            // each command has its own return value
-            bool? thisReturn = InvokeCommand (command, key, keyBinding);
-
-            // if we haven't got anything yet, the current command result should be used
-            toReturn ??= thisReturn;
-
-            // if ever see a true then that's what we will return
-            if (thisReturn ?? false)
-            {
-                toReturn = true;
-            }
-        }
-
-        return toReturn;
-    }
-
-    /// <summary>Invokes the specified command.</summary>
-    /// <param name="command">The command to invoke.</param>
-    /// <param name="key">The key that caused the command to be invoked, if any.</param>
-    /// <param name="keyBinding"></param>
-    /// <returns>
-    ///     <see langword="null"/> if no command was found. <see langword="true"/> if the command was invoked, and it
-    ///     handled the command. <see langword="false"/> if the command was invoked, and it did not handle the command.
-    /// </returns>
-    public bool? InvokeCommand (Command command, Key? key = null, KeyBinding? keyBinding = null)
-    {
-        if (CommandImplementations.TryGetValue (command, out Func<CommandContext, bool?>? implementation))
-        {
-            var context = new CommandContext (command, key, keyBinding); // Create the context here
-
-            return implementation (context);
-        }
-
-        return null;
-    }
-
-    /// <summary>
-    ///     <para>
-    ///         Sets the function that will be invoked for a <see cref="Command"/>. Views should call
-    ///         AddCommand for each command they support.
-    ///     </para>
-    ///     <para>
-    ///         If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
-    ///         replace the old one.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     <para>
-    ///         This version of AddCommand is for commands that require <see cref="CommandContext"/>. Use
-    ///         <see cref="AddCommand(Command,Func{System.Nullable{bool}})"/>
-    ///         in cases where the command does not require a <see cref="CommandContext"/>.
-    ///     </para>
-    /// </remarks>
-    /// <param name="command">The command.</param>
-    /// <param name="f">The function.</param>
-    protected void AddCommand (Command command, Func<CommandContext, bool?> f) { CommandImplementations [command] = f; }
-
-    /// <summary>
-    ///     <para>
-    ///         Sets the function that will be invoked for a <see cref="Command"/>. Views should call
-    ///         AddCommand for each command they support.
-    ///     </para>
-    ///     <para>
-    ///         If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
-    ///         replace the old one.
-    ///     </para>
-    /// </summary>
-    /// <remarks>
-    ///     <para>
-    ///         This version of AddCommand is for commands that do not require a <see cref="CommandContext"/>.
-    ///         If the command requires context, use
-    ///         <see cref="AddCommand(Command,Func{CommandContext,System.Nullable{bool}})"/>
-    ///     </para>
-    /// </remarks>
-    /// <param name="command">The command.</param>
-    /// <param name="f">The function.</param>
-    protected void AddCommand (Command command, Func<bool?> f) { CommandImplementations [command] = ctx => f (); }
-
-    /// <summary>Returns all commands that are supported by this <see cref="View"/>.</summary>
-    /// <returns></returns>
-    public IEnumerable<Command> GetSupportedCommands () { return CommandImplementations.Keys; }
-
-    // TODO: Add GetKeysBoundToCommand() - given a Command, return all Keys that would invoke it
-
     #endregion Key Bindings
 }

+ 1 - 151
Terminal.Gui/View/View.cs

@@ -135,33 +135,7 @@ public partial class View : Responder, ISupportInitializeNotification
     public View ()
     {
         SetupAdornments ();
-
-        AddCommand (
-                    Command.Select,
-                    () =>
-                    {
-                        SetFocus ();
-
-                        return RaiseSelectEvent ();
-                    });
-
-        AddCommand (
-                    Command.HotKey,
-                    () =>
-                    {
-                        SetFocus ();
-
-                        return RaiseHotKeyCommandEvent ();
-                    });
-
-        AddCommand (
-                    Command.Accept,
-                    () =>
-                    {
-                        SetFocus ();
-
-                        return RaiseAcceptEvent ();
-                    });
+        SetupCommands ();
 
         SetupKeyboard ();
 
@@ -273,130 +247,6 @@ public partial class View : Responder, ISupportInitializeNotification
 
     #endregion Constructors and Initialization
 
-    #region Base Command Events
-
-    /// <summary>
-    ///     Called when the <see cref="Command.Accept"/> command is invoked. Raises <see cref="Accept"/>
-    ///     event.
-    /// </summary>
-    /// <returns>
-    ///     If <see langword="true"/> the event was canceled. If <see langword="false"/> the event was raised but not canceled.
-    ///     If <see langword="null"/> no event was raised.
-    /// </returns>
-    protected bool? RaiseAcceptEvent ()
-    {
-        HandledEventArgs args = new ();
-
-        // Best practice is to invoke the virtual method first.
-        // This allows derived classes to handle the event and potentially cancel it.
-        if (OnAccept (args) || args.Handled)
-        {
-            return true;
-        }
-
-        // If the event is not canceled by the virtual method, raise the event to notify any external subscribers.
-        Accept?.Invoke (this, args);
-
-        return Accept is null ? null : args.Handled;
-    }
-
-    /// <summary>
-    ///     Called when the <see cref="Command.Accept"/> command is received. Set <see cref="HandledEventArgs.Handled"/> to
-    ///     <see langword="true"/> to stop processing.
-    /// </summary>
-    /// <param name="args"></param>
-    /// <returns><see langword="true"/> to stop processing.</returns>
-    protected virtual bool OnAccept (HandledEventArgs args) { return false; }
-
-    /// <summary>
-    ///     Cancelable event raised when the <see cref="Command.Accept"/> command is invoked. Set
-    ///     <see cref="HandledEventArgs.Handled"/>
-    ///     to cancel the event.
-    /// </summary>
-    public event EventHandler<HandledEventArgs>? Accept;
-
-    /// <summary>
-    ///     Called when the <see cref="Command.Select"/> command is invoked. Raises <see cref="Select"/>
-    ///     event.
-    /// </summary>
-    /// <returns>
-    ///     If <see langword="true"/> the event was canceled. If <see langword="false"/> the event was raised but not canceled.
-    ///     If <see langword="null"/> no event was raised.
-    /// </returns>
-    protected bool? RaiseSelectEvent ()
-    {
-        HandledEventArgs args = new ();
-
-        // Best practice is to invoke the virtual method first.
-        // This allows derived classes to handle the event and potentially cancel it.
-        if (OnSelect (args) || args.Handled)
-        {
-            return true;
-        }
-
-        // If the event is not canceled by the virtual method, raise the event to notify any external subscribers.
-        Select?.Invoke (this, args);
-
-        return Select is null ? null : args.Handled;
-    }
-
-    /// <summary>
-    ///     Called when the <see cref="Command.Select"/> command is received. Set <see cref="HandledEventArgs.Handled"/> to
-    ///     <see langword="true"/> to stop processing.
-    /// </summary>
-    /// <param name="args"></param>
-    /// <returns><see langword="true"/> to stop processing.</returns>
-    protected virtual bool OnSelect (HandledEventArgs args) { return false; }
-
-    /// <summary>
-    ///     Cancelable event raised when the <see cref="Command.Select"/> command is invoked. Set
-    ///     <see cref="HandledEventArgs.Handled"/>
-    ///     to cancel the event.
-    /// </summary>
-    public event EventHandler<HandledEventArgs>? Select;
-
-    /// <summary>
-    ///     Called when the <see cref="Command.HotKey"/> command is invoked. Raises <see cref="HotKey"/>
-    ///     event.
-    /// </summary>
-    /// <returns>
-    ///     If <see langword="true"/> the event was handled. If <see langword="false"/> the event was raised but not handled.
-    ///     If <see langword="null"/> no event was raised.
-    /// </returns>
-    protected bool? RaiseHotKeyCommandEvent ()
-    {
-        HandledEventArgs args = new ();
-
-        // Best practice is to invoke the virtual method first.
-        // This allows derived classes to handle the event and potentially cancel it.
-        if (OnHotKeyCommand (args) || args.Handled)
-        {
-            return true;
-        }
-
-        // If the event is not canceled by the virtual method, raise the event to notify any external subscribers.
-        HotKeyCommand?.Invoke (this, args);
-
-        return HotKeyCommand is null ? null : args.Handled;
-    }
-
-    /// <summary>
-    ///     Called when the <see cref="Command.HotKey"/> command is received. Set <see cref="HandledEventArgs.Handled"/> to
-    ///     <see langword="true"/> to stop processing.
-    /// </summary>
-    /// <param name="args"></param>
-    /// <returns><see langword="true"/> to stop processing.</returns>
-    protected virtual bool OnHotKeyCommand (HandledEventArgs args) { return false; }
-
-    /// <summary>
-    ///     Cancelable event raised when the <see cref="Command.HotKey"/> command is invoked. Set
-    ///     <see cref="HandledEventArgs.Handled"/>
-    ///     to cancel the event.
-    /// </summary>
-    public event EventHandler<HandledEventArgs>? HotKeyCommand;
-
-    #endregion Base Command Events
-
     #region Visibility
 
     private bool _enabled = true;

+ 10 - 2
Terminal.Gui/Views/Button.cs

@@ -150,15 +150,23 @@ public class Button : View, IDesignable
     }
 
     /// <summary>
-    ///     Gets or sets whether the <see cref="Button"/> will invoke the <see cref="Command.Accept"/>
-    ///     command on the <see cref="View.SuperView"/> if <see cref="View.Accept"/> is not handled by a subscriber.
+    ///     Gets or sets whether the <see cref="Button"/> will show an indicator indicating it is the default Button. If <see langword="true"/>
+    ///     <see cref="Command.Accept"/> will be invoked when the user presses <c>Enter</c> and no other peer-<see cref="View"/> processes the key.
+    ///     If <see cref="View.Accept"/> is not handled, the Gets or sets whether the <see cref="Button"/> will show an indicator indicating it is the default Button. If <see langword="true"/>
+    ///     <see cref="Command.Accept"/> command on the <see cref="View.SuperView"/> will be invoked.
     /// </summary>
     public bool IsDefault
     {
         get => _isDefault;
         set
         {
+            if (_isDefault == value)
+            {
+                return;
+            }
+
             _isDefault = value;
+
             UpdateTextFormatterText ();
             OnResizeNeeded ();
         }

+ 2 - 2
Terminal.Gui/Views/TextField.cs

@@ -326,8 +326,8 @@ public class TextField : View
                    );
 
         // By Default pressing ENTER should be ignored (OnAccept will return false or null). Only cancel if the
-        // event was fired and set Cancel = true.
-        AddCommand (Command.Accept, () => RaiseAcceptEvent () == false);
+        //// event was fired and set Cancel = true.
+        //AddCommand (Command.Accept, RaiseAcceptEvent);
 
         // Default keybindings for this view
         // We follow this as closely as possible: https://en.wikipedia.org/wiki/Table_of_keyboard_shortcuts

+ 1 - 1
UICatalog/Scenarios/Buttons.cs

@@ -232,7 +232,7 @@ public class Buttons : Scenario
             RadioLabels = new [] { "_Start", "_End", "_Center", "_Fill" },
             Title = "_9 RadioGroup",
             BorderStyle = LineStyle.Dotted,
-            CanFocus = false
+           // CanFocus = false
         };
         main.Add (radioGroup);
 

+ 125 - 21
UnitTests/View/ViewCommandTests.cs

@@ -6,9 +6,9 @@ namespace Terminal.Gui.ViewTests;
 
 public class ViewCommandTests (ITestOutputHelper output)
 {
-    // OnAccept/Accept tests
+    #region OnAccept/Accept tests
     [Fact]
-    public void Accept_Command_Raises ()
+    public void Accept_Command_Raises_NoFocus ()
     {
         var view = new ViewEventTester ();
         Assert.False (view.HasFocus);
@@ -19,7 +19,7 @@ public class ViewCommandTests (ITestOutputHelper output)
 
         Assert.Equal (1, view.AcceptCount);
 
-        Assert.True (view.HasFocus);
+        Assert.False (view.HasFocus);
     }
 
     [Fact]
@@ -36,9 +36,6 @@ public class ViewCommandTests (ITestOutputHelper output)
         Assert.Equal (0, view.AcceptCount);
     }
 
-
-
-
     [Fact]
     public void Accept_Handle_Event_OnAccept_Returns_True ()
     {
@@ -76,6 +73,126 @@ public class ViewCommandTests (ITestOutputHelper output)
         void ViewOnAccept (object sender, HandledEventArgs e) { accepted = true; }
     }
 
+    // Accept on subview should bubble up to parent
+    [Fact]
+    public void Accept_Command_Bubbles_Up_To_SuperView ()
+    {
+        var view = new ViewEventTester () { Id = "view" };
+        var subview = new ViewEventTester () { Id = "subview" };
+        view.Add (subview);
+
+        subview.InvokeCommand (Command.Accept);
+        Assert.Equal (1, subview.OnAcceptCount);
+        Assert.Equal (1, view.OnAcceptCount);
+
+        subview.HandleOnAccept = true;
+        subview.InvokeCommand (Command.Accept);
+        Assert.Equal (2, subview.OnAcceptCount);
+        Assert.Equal (1, view.OnAcceptCount);
+
+        subview.HandleOnAccept = false;
+        subview.HandleAccept = true;
+        subview.InvokeCommand (Command.Accept);
+        Assert.Equal (3, subview.OnAcceptCount);
+        Assert.Equal (1, view.OnAcceptCount);
+
+        // Add a super view to test deeper hierarchy
+        var superView = new ViewEventTester () { Id = "superView" };
+        superView.Add (view);
+
+        subview.InvokeCommand (Command.Accept);
+        Assert.Equal (4, subview.OnAcceptCount);
+        Assert.Equal (1, view.OnAcceptCount);
+        Assert.Equal (0, superView.OnAcceptCount);
+
+        subview.HandleAccept = false;
+        subview.InvokeCommand (Command.Accept);
+        Assert.Equal (5, subview.OnAcceptCount);
+        Assert.Equal (2, view.OnAcceptCount);
+        Assert.Equal (1, superView.OnAcceptCount);
+
+        view.HandleAccept = true;
+        subview.InvokeCommand (Command.Accept);
+        Assert.Equal (6, subview.OnAcceptCount);
+        Assert.Equal (3, view.OnAcceptCount);
+        Assert.Equal (1, superView.OnAcceptCount);
+
+    }
+
+    #endregion OnAccept/Accept tests
+
+    #region OnSelect/Select tests
+    [Fact]
+    public void Select_Command_Raises_NoFocus ()
+    {
+        var view = new ViewEventTester ();
+        Assert.False (view.HasFocus);
+
+        Assert.False (view.InvokeCommand (Command.Select)); // false means it was not handled
+
+        Assert.Equal (1, view.OnSelectCount);
+
+        Assert.Equal (1, view.SelectCount);
+
+        Assert.False (view.HasFocus);
+    }
+
+    [Fact]
+    public void Select_Command_Handle_OnSelect_NoEvent ()
+    {
+        var view = new ViewEventTester ();
+        Assert.False (view.HasFocus);
+
+        view.HandleOnSelect = true;
+        Assert.True (view.InvokeCommand (Command.Select));
+
+        Assert.Equal (1, view.OnSelectCount);
+
+        Assert.Equal (0, view.SelectCount);
+    }
+
+    [Fact]
+    public void Select_Handle_Event_OnSelect_Returns_True ()
+    {
+        var view = new View ();
+        var SelectInvoked = false;
+
+        view.Select += ViewOnSelect;
+
+        bool? ret = view.InvokeCommand (Command.Select);
+        Assert.True (ret);
+        Assert.True (SelectInvoked);
+
+        return;
+
+        void ViewOnSelect (object sender, HandledEventArgs e)
+        {
+            SelectInvoked = true;
+            e.Handled = true;
+        }
+    }
+
+    [Fact]
+    public void Select_Command_Invokes_Select_Event ()
+    {
+        var view = new View ();
+        var Selected = false;
+
+        view.Select += ViewOnSelect;
+
+        view.InvokeCommand (Command.Select);
+        Assert.True (Selected);
+
+        return;
+
+        void ViewOnSelect (object sender, HandledEventArgs e) { Selected = true; }
+    }
+
+
+    #endregion OnSelect/Select tests
+
+    #region OnHotKey/HotKey tests
+
     [Fact]
     public void HotKey_Command_SetsFocus ()
     {
@@ -87,6 +204,8 @@ public class ViewCommandTests (ITestOutputHelper output)
         Assert.True (view.HasFocus);
     }
 
+    #endregion OnHotKey/HotKey tests
+
     public class ViewEventTester : View
     {
         public ViewEventTester ()
@@ -122,11 +241,6 @@ public class ViewCommandTests (ITestOutputHelper output)
         {
             OnAcceptCount++;
 
-            if (!HandleOnAccept)
-            {
-                return base.OnAccept (args);
-            }
-
             return HandleOnAccept;
         }
 
@@ -140,11 +254,6 @@ public class ViewCommandTests (ITestOutputHelper output)
         protected override bool OnHotKeyCommand (HandledEventArgs args)
         {
             OnHotKeyCommandCount++;
-            if (!HandleOnHotKeyCommand)
-            {
-                return base.OnHotKeyCommand (args);
-            }
-
 
             return HandleOnHotKeyCommand;
         }
@@ -161,11 +270,6 @@ public class ViewCommandTests (ITestOutputHelper output)
         {
             OnSelectCount++;
 
-            if (!HandleOnSelect)
-            {
-                return base.OnSelect (args);
-            }
-
             return HandleOnSelect;
         }
 

+ 1 - 1
UnitTests/Views/RadioGroupTests.cs

@@ -746,7 +746,7 @@ public class RadioGroupTests (ITestOutputHelper output)
         Assert.Equal (2, selectedItemChanged);
         Assert.Equal (2, selectCount);
         Assert.Equal (4, acceptCount);
-        Assert.Equal (2, superViewAcceptCount);
+        Assert.Equal (3, superViewAcceptCount); // Accept bubbles up to superview
 
         radioGroup.DoubleClickAccepts = false;
         Assert.False (radioGroup.NewMouseEvent (new () { Position = new (0, 1), Flags = MouseFlags.Button1DoubleClicked }));