namespace Terminal.Gui.ViewBase; public partial class View // Command APIs { private readonly Dictionary _commandImplementations = new (); #region Default Implementation /// /// Helper to configure all things Command related for a View. Called from the View constructor. /// private void SetupCommands () { // NotBound - Invoked if no handler is bound AddCommand (Command.NotBound, RaiseCommandNotBound); // Enter - Raise Accepted AddCommand (Command.Accept, RaiseAccepting); // HotKey - SetFocus and raise HandlingHotKey AddCommand ( Command.HotKey, (ctx) => { if (RaiseHandlingHotKey (ctx) is true) { return true; } SetFocus (); // Always return true on hotkey, even if SetFocus fails because // hotkeys are always handled by the View (unless RaiseHandlingHotKey cancels). return true; }); // Space or single-click - Raise Selecting AddCommand ( Command.Select, ctx => { if (RaiseSelecting (ctx) is true) { return true; } if (CanFocus) { // For Select, if the view is focusable and SetFocus succeeds, by defition, // the event is handled. So return what SetFocus returns. return SetFocus (); } return false; }); } /// /// Called when a command that has not been bound is invoked. /// /// /// if no event was raised; input processing should continue. /// if the event was raised and was not handled (or cancelled); input processing should /// continue. /// if the event was raised and handled (or cancelled); input processing should stop. /// protected bool? RaiseCommandNotBound (ICommandContext? ctx) { CommandEventArgs args = new () { Context = ctx }; // For robustness' sake, even if the virtual method returns true, if the args // indicate the event should be cancelled, we honor that. if (OnCommandNotBound (args) || args.Handled) { return true; } // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. CommandNotBound?.Invoke (this, args); return CommandNotBound is null ? null : args.Handled; } /// /// Called when a command that has not been bound is invoked. /// Set CommandEventArgs.Handled to and return to indicate the event was /// handled and processing should stop. /// /// The event arguments. /// to stop processing. protected virtual bool OnCommandNotBound (CommandEventArgs args) { return false; } /// /// Cancelable event raised when a command that has not been bound is invoked. /// Set CommandEventArgs.Handled to to indicate the event was handled and processing should /// stop. /// public event EventHandler? CommandNotBound; /// /// Called when the user is accepting the state of the View and the has been invoked. /// Calls which can be cancelled; if not cancelled raises . /// event. The default handler calls this method. /// /// /// /// The event should be raised after the state of the View has changed (after /// is raised). /// /// /// If the Accepting event is not handled, will be invoked on the SuperView, enabling /// default Accept behavior. /// /// /// If a peer-View raises the Accepting event and the event is not cancelled, the will /// be invoked on the /// first Button in the SuperView that has set to . /// /// /// /// if no event was raised; input processing should continue. /// if the event was raised and was not handled (or cancelled); input processing should /// continue. /// if the event was raised and handled (or cancelled); input processing should stop. /// protected bool? RaiseAccepting (ICommandContext? ctx) { Logging.Debug ($"{Title} ({ctx?.Source?.Title})"); CommandEventArgs args = new () { Context = ctx }; // Best practice is to invoke the virtual method first. // This allows derived classes to handle the event and potentially cancel it. Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Calling OnAccepting..."); args.Handled = OnAccepting (args) || args.Handled; if (!args.Handled && Accepting is { }) { // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Raising Accepting..."); Accepting?.Invoke (this, args); } // If Accepting was handled, raise Accepted (non-cancelable event) if (args.Handled) { Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Calling RaiseAccepted"); RaiseAccepted (ctx); } // Accept is a special case where if the event is not canceled, the event is // - Invoked on any peer-View with IsDefault == true // - bubbled up the SuperView hierarchy. if (!args.Handled) { // If there's an IsDefault peer view in SubViews, try it View? isDefaultView = SuperView?.InternalSubViews.FirstOrDefault (v => v is Button { IsDefault: true }); if (isDefaultView != this && isDefaultView is Button { IsDefault: true } button) { // TODO: It's a bit of a hack that this uses KeyBinding. There should be an InvokeCommmand that // TODO: is generic? Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - InvokeCommand on Default View ({isDefaultView.Title})"); bool? handled = isDefaultView.InvokeCommand (Command.Accept, ctx); if (handled == true) { return true; } } if (SuperView is { }) { Logging.Debug ($"{Title} ({ctx?.Source?.Title}) - Invoking Accept on SuperView ({SuperView.Title}/{SuperView.Id})..."); return SuperView?.InvokeCommand (Command.Accept, ctx); } } return args.Handled; } /// /// Called when the user is accepting the state of the View and the has been invoked. /// Set CommandEventArgs.Handled to and return to indicate the event was /// handled and processing should stop. /// /// /// /// See for more information. /// /// /// /// to stop processing. protected virtual bool OnAccepting (CommandEventArgs args) { return false; } /// /// Cancelable event raised when the user is accepting the state of the View and the has /// been invoked. /// Set CommandEventArgs.Handled to to indicate the event was handled and processing should /// stop. /// /// /// /// See for more information. /// /// public event EventHandler? Accepting; /// /// Raises the / event indicating the View has been accepted. /// This is called after has been raised and not cancelled. /// /// /// /// Unlike , this event cannot be cancelled. It is raised after the View has been accepted. /// /// /// The command context. protected void RaiseAccepted (ICommandContext? ctx) { CommandEventArgs args = new () { Context = ctx }; OnAccepted (args); Accepted?.Invoke (this, args); } /// /// Called when the View has been accepted. This is called after has been raised and not cancelled. /// /// /// /// Unlike , this method is called after the View has been accepted and cannot cancel the operation. /// /// /// The event arguments. protected virtual void OnAccepted (CommandEventArgs args) { } /// /// Event raised when the View has been accepted. This is raised after has been raised and not cancelled. /// /// /// /// Unlike , this event cannot be cancelled. It is raised after the View has been accepted. /// /// /// See for more information. /// /// public event EventHandler? Accepted; /// /// Called when the user has performed an action (e.g. ) causing the View to change state. /// Calls which can be cancelled; if not cancelled raises . /// event. The default handler calls this method. /// /// /// The event should be raised after the state of the View has been changed and before see /// . /// /// /// if no event was raised; input processing should continue. /// if the event was raised and was not handled (or cancelled); input processing should /// continue. /// if the event was raised and handled (or cancelled); input processing should stop. /// protected bool? RaiseSelecting (ICommandContext? ctx) { //Logging.Debug ($"{Title} ({ctx?.Source?.Title})"); CommandEventArgs args = new () { Context = ctx }; // Best practice is to invoke the virtual method first. // This allows derived classes to handle the event and potentially cancel it. if (OnSelecting (args) || args.Handled) { return true; } // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. Selecting?.Invoke (this, args); return Selecting is null ? null : args.Handled; } /// /// Called when the user has performed an action (e.g. ) causing the View to change state. /// Set CommandEventArgs.Handled to and return to indicate the event was /// handled and processing should stop. /// /// The event arguments. /// to stop processing. protected virtual bool OnSelecting (CommandEventArgs args) { return false; } /// /// Cancelable event raised when the user has performed an action (e.g. ) causing the View /// to change state. /// Set CommandEventArgs.Handled to to indicate the event was handled and processing should /// stop. /// public event EventHandler? Selecting; /// /// Called when the View is handling the user pressing the View's s. Calls /// which can be cancelled; if not cancelled raises . /// event. The default handler calls this method. /// /// The context to pass with the command. /// /// if no event was raised; input processing should continue. /// if the event was raised and was not handled (or cancelled); input processing should /// continue. /// if the event was raised and handled (or cancelled); input processing should stop. /// protected bool? RaiseHandlingHotKey (ICommandContext? ctx) { CommandEventArgs args = new () { Context = ctx }; //Logging.Debug ($"{Title} ({args.Context?.Source?.Title})"); // Best practice is to invoke the virtual method first. // This allows derived classes to handle the event and potentially cancel it. if (OnHandlingHotKey (args) || args.Handled) { return true; } // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. HandlingHotKey?.Invoke (this, args); return HandlingHotKey is null ? null : args.Handled; } /// /// Called when the View is handling the user pressing the View's . /// Set CommandEventArgs.Handled to to indicate the event was handled and processing should /// stop. /// /// /// to stop processing. protected virtual bool OnHandlingHotKey (CommandEventArgs args) { return false; } /// /// Cancelable event raised when the View is handling the user pressing the View's . Set /// CommandEventArgs.Handled to to indicate the event was handled and processing should stop. /// public event EventHandler? HandlingHotKey; #endregion Default Implementation /// /// Function signature commands. /// /// Provides context about the circumstances of invoking the command. /// /// if no event was raised; input processing should continue. /// if the event was raised and was not handled (or cancelled); input processing should /// continue. /// if the event was raised and handled (or cancelled); input processing should stop. /// public delegate bool? CommandImplementation (ICommandContext? ctx); /// /// /// Sets the function that will be invoked for a . Views should call /// AddCommand for each command they support. /// /// /// If AddCommand has already been called for will /// replace the old one. /// /// /// /// /// This version of AddCommand is for commands that require . /// /// /// See the Commands Deep Dive for more information: . /// /// /// The command. /// The delegate. protected void AddCommand (Command command, CommandImplementation impl) { _commandImplementations [command] = impl; } /// /// /// Sets the function that will be invoked for a . Views should call /// AddCommand for each command they support. /// /// /// If AddCommand has already been called for will /// replace the old one. /// /// /// /// /// This version of AddCommand is for commands that do not require context. /// If the command requires context, use /// /// /// /// See the Commands Deep Dive for more information: . /// /// /// The command. /// The delegate. protected void AddCommand (Command command, Func impl) { _commandImplementations [command] = ctx => impl (); } /// Returns all commands that are supported by this . /// public IEnumerable GetSupportedCommands () { return _commandImplementations.Keys; } /// /// Invokes the specified commands. /// /// The set of commands to invoke. /// The binding that caused the invocation, if any. This will be passed as context with the command. /// /// if no command was found; input processing should continue. /// if the command was invoked and was not handled (or cancelled); input processing should /// continue. /// if the command was invoked the command was handled (or cancelled); input processing should /// stop. /// public bool? InvokeCommands (Command [] commands, TBindingType binding) { bool? toReturn = null; foreach (Command command in commands) { if (!_commandImplementations.ContainsKey (command)) { Logging.Warning (@$"{command} is not supported by this View ({GetType ().Name}). Binding: {binding}."); } // each command has its own return value bool? thisReturn = InvokeCommand (command, binding); // 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; } /// /// Invokes the specified command. /// /// The command to invoke. /// The binding that caused the invocation, if any. This will be passed as context with the command. /// /// if no command was found; input processing should continue. /// if the command was invoked and was not handled (or cancelled); input processing should /// continue. /// if the command was invoked the command was handled (or cancelled); input processing should /// stop. /// public bool? InvokeCommand (Command command, TBindingType binding) { if (!_commandImplementations.TryGetValue (command, out CommandImplementation? implementation)) { _commandImplementations.TryGetValue (Command.NotBound, out implementation); } return implementation! ( new CommandContext { Command = command, Source = this, Binding = binding }); } /// /// Invokes the specified command. /// /// The command to invoke. /// The context to pass with the command. /// /// if no command was found; input processing should continue. /// if the command was invoked and was not handled (or cancelled); input processing should /// continue. /// if the command was invoked the command was handled (or cancelled); input processing should /// stop. /// public bool? InvokeCommand (Command command, ICommandContext? ctx) { if (!_commandImplementations.TryGetValue (command, out CommandImplementation? implementation)) { _commandImplementations.TryGetValue (Command.NotBound, out implementation); } return implementation! (ctx); } /// /// Invokes the specified command without context. /// /// The command to invoke. /// /// if no command was found; input processing should continue. /// if the command was invoked and was not handled (or cancelled); input processing should /// continue. /// if the command was invoked the command was handled (or cancelled); input processing should /// stop. /// public bool? InvokeCommand (Command command) { if (!_commandImplementations.TryGetValue (command, out CommandImplementation? implementation)) { _commandImplementations.TryGetValue (Command.NotBound, out implementation); } return implementation! ( new CommandContext { Command = command, Source = this, Binding = null }); } }