#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;
}
}