#nullable enable using System.ComponentModel; namespace Terminal.Gui; 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 () { // Enter - Raise Accepted AddCommand (Command.Accept, RaiseAccepting); // HotKey - SetFocus and raise HandlingHotKey AddCommand (Command.HotKey, () => { if (RaiseHandlingHotKey () is true) { return true; } SetFocus (); return true; }); // Space or single-click - Raise Selecting AddCommand (Command.Select, ctx => { if (RaiseSelecting (ctx) is true) { return true; } if (CanFocus) { SetFocus (); return true; } return false; }); } /// /// 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) { 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. args.Cancel = OnAccepting (args) || args.Cancel; if (!args.Cancel) { // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. Accepting?.Invoke (this, args); } // 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.Cancel) { // If there's an IsDefault peer view in SubViews, try it var isDefaultView = SuperView?.InternalSubViews.FirstOrDefault (v => v is Button { IsDefault: true }); if (isDefaultView != this && isDefaultView is Button { IsDefault: true } button) { bool? handled = isDefaultView.InvokeCommand (Command.Accept, new ([Command.Accept], null, this)); if (handled == true) { return true; } } if (SuperView is { }) { return SuperView?.InvokeCommand (Command.Accept, new ([Command.Accept], null, this)) is true; } } return Accepting is null ? null : args.Cancel; } /// /// Called when the user is accepting the state of the View and the has been invoked. Set CommandEventArgs.Cancel to /// and return to stop processing. /// /// /// /// 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.Cancel to cancel the event. /// /// /// /// See for more information. /// /// public event EventHandler? Accepting; /// /// 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) { 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.Cancel) { 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.Cancel; } /// /// Called when the user has performed an action (e.g. ) causing the View to change state. /// Set CommandEventArgs.Cancel to /// and return to cancel the state change. The default implementation does nothing. /// /// 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. /// CommandEventArgs.Cancel to to cancel the state change. /// 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. /// /// /// 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 () { CommandEventArgs args = new () { Context = new CommandContext () { Command = Command.HotKey } }; // 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.Cancel) { 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.Cancel; } /// /// Called when the View is handling the user pressing the View's . Set CommandEventArgs.Cancel to /// to stop processing. /// /// /// 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.Cancel to cancel the event. /// 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 . /// /// /// 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 /// /// /// /// 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)) { throw new NotSupportedException ( @$"A Binding was set up for the command {command} ({binding}) but that command is not supported by this View ({GetType ().Name})" ); } // 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)) { return implementation (new CommandContext () { Command = command, Binding = binding, }); } return null; } /// /// 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)) { return implementation (null); } return null; } }