#nullable enable using System.ComponentModel; namespace Terminal.Gui; public partial class View // Command APIs { #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, RaiseAccepted); // HotKey - SetFocus and raise HotKeyHandled AddCommand (Command.HotKey, () => { if (RaiseHotKeyHandled () is true) { return true; } SetFocus (); return true; }); // Space or single-click - Raise Selected AddCommand (Command.Select, (ctx) => { if (RaiseSelected (ctx) is true) { return true; } if (CanFocus) { SetFocus (); return true; } return false; }); } /// /// Called when the View's state has been accepted by the user. Calls which can be cancelled; if not cancelled raises . /// event. The default handler calls this method. /// /// /// The event should raised after the state of the View has changed (after is raised). /// /// /// If the event was canceled. If the event was raised but not canceled. /// If no event was raised. /// protected bool? RaiseAccepted () { 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 = OnAccepted (args) || args.Handled; if (!args.Handled) { // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. Accepted?.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.Handled) { // If there's an IsDefault peer view in Subviews, try it var isDefaultView = SuperView?.Subviews.FirstOrDefault (v => v is Button { IsDefault: true }); if (isDefaultView != this && isDefaultView is Button { IsDefault: true } button) { bool? handled = isDefaultView.InvokeCommand (Command.Accept); if (handled == true) { return true; } } return SuperView?.InvokeCommand (Command.Accept) == true; } return Accepted is null ? null : args.Handled; } // TODO: Change this to CancelEventArgs /// /// Called when the View's state has been accepted by the user. Set to /// to stop processing. /// /// /// to stop processing. protected virtual bool OnAccepted (HandledEventArgs args) { return false; } /// /// Cancelable event raised when the View's state has been accepted by the user. Set /// to cancel the event. /// public event EventHandler? Accepted; /// /// Called when the user has selected the View or otherwise changed the state of the View. Calls which can be cancelled; if not cancelled raises . /// event. The default handler calls this method. /// /// /// The event should raised after the state of the View has been changed and before see . /// /// /// If the event was canceled. If the event was raised but not canceled. /// If no event was raised. /// protected bool? RaiseSelected (CommandContext 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 (OnSelected (args) || args.Cancel) { return true; } // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. Selected?.Invoke (this, args); return Selected is null ? null : args.Cancel; } /// /// Called when the user has selected the View or otherwise changed the state of the View. Set to /// to stop processing. /// /// /// to stop processing. protected virtual bool OnSelected (CommandEventArgs args) { return false; } /// /// Cancelable event raised when the user has selected the View or otherwise changed the state of the View. Set /// /// to cancel the event. /// public event EventHandler? Selected; // TODO: What does this event really do? "Called when the user has pressed the View's hot key or otherwise invoked the View's hot key command.???" /// /// Called when the View has handled the user pressing the View's . Calls which can be cancelled; if not cancelled raises . /// event. The default handler calls this method. /// /// /// If the event was handled. If the event was raised but not handled. /// If no event was raised. /// protected bool? RaiseHotKeyHandled () { 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 (OnHotKeyHandled (args) || args.Handled) { return true; } // If the event is not canceled by the virtual method, raise the event to notify any external subscribers. HotKeyHandled?.Invoke (this, args); return HotKeyHandled is null ? null : args.Handled; } /// /// Called when the View has handled the user pressing the View's . Set to /// to stop processing. /// /// /// to stop processing. protected virtual bool OnHotKeyHandled (HandledEventArgs args) { return false; } /// /// Cancelable event raised when the command is invoked. Set /// /// to cancel the event. /// public event EventHandler? HotKeyHandled; #endregion Default Implementation /// /// /// 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 . Use /// /// in cases where the command does not require a . /// /// /// The command. /// The function. protected void AddCommand (Command command, Func f) { CommandImplementations [command] = f; } /// /// /// 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 a . /// If the command requires context, use /// /// /// /// The command. /// The function. protected void AddCommand (Command command, Func f) { CommandImplementations [command] = ctx => f (); } /// Returns all commands that are supported by this . /// public IEnumerable GetSupportedCommands () { return CommandImplementations.Keys; } /// /// Invokes the specified commands. /// /// /// The key that caused the commands to be invoked, if any. /// /// /// if no command was found. /// if the command was invoked the command was handled (or cancelled) /// if the command was invoked and the command was not handled. /// 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; } /// Invokes the specified command. /// The command to invoke. /// The key that caused the command to be invoked, if any. /// /// /// if no command was found. if the command was invoked, and it /// handled (or cancelled) the command. if the command was invoked, and it did not handle (or cancel) the command. /// public bool? InvokeCommand (Command command, Key? key = null, KeyBinding? keyBinding = null) { if (CommandImplementations.TryGetValue (command, out Func? implementation)) { var context = new CommandContext (command, key, keyBinding); // Create the context here return implementation (context); } return null; } public bool? InvokeCommand (Command command, CommandContext ctx) { if (CommandImplementations.TryGetValue (command, out Func? implementation)) { return implementation (ctx); } return null; } }