using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using NStack;
namespace Terminal.Gui {
public partial class View {
ShortcutHelper _shortcutHelper;
///
/// Event invoked when the is changed.
///
public event EventHandler HotKeyChanged;
Key _hotKey = Key.Null;
///
/// Gets or sets the HotKey defined for this view. A user pressing HotKey on the keyboard while this view has focus will cause the Clicked event to fire.
///
public virtual Key HotKey {
get => _hotKey;
set {
if (_hotKey != value) {
var v = value == Key.Unknown ? Key.Null : value;
if (_hotKey != Key.Null && ContainsKeyBinding (Key.Space | _hotKey)) {
if (v == Key.Null) {
ClearKeybinding (Key.Space | _hotKey);
} else {
ReplaceKeyBinding (Key.Space | _hotKey, Key.Space | v);
}
} else if (v != Key.Null) {
AddKeyBinding (Key.Space | v, Command.Accept);
}
_hotKey = TextFormatter.HotKey = v;
}
}
}
///
/// Gets or sets the specifier character for the hotkey (e.g. '_'). Set to '\xffff' to disable hotkey support for this View instance. The default is '\xffff'.
///
public virtual Rune HotKeySpecifier {
get {
if (TextFormatter != null) {
return TextFormatter.HotKeySpecifier;
} else {
return new Rune ('\xFFFF');
}
}
set {
TextFormatter.HotKeySpecifier = value;
SetHotKey ();
}
}
///
/// This is the global setting that can be used as a global shortcut to invoke an action if provided.
///
public Key Shortcut {
get => _shortcutHelper.Shortcut;
set {
if (_shortcutHelper.Shortcut != value && (ShortcutHelper.PostShortcutValidation (value) || value == Key.Null)) {
_shortcutHelper.Shortcut = value;
}
}
}
///
/// The keystroke combination used in the as string.
///
public ustring ShortcutTag => ShortcutHelper.GetShortcutTag (_shortcutHelper.Shortcut);
///
/// The action to run if the is defined.
///
public virtual Action ShortcutAction { get; set; }
// This is null, and allocated on demand.
List _tabIndexes;
///
/// Configurable keybindings supported by the control
///
private Dictionary KeyBindings { get; set; } = new Dictionary ();
private Dictionary> CommandImplementations { get; set; } = new Dictionary> ();
///
/// This returns a tab index list of the subviews contained by this view.
///
/// The tabIndexes.
public IList TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty;
int _tabIndex = -1;
///
/// Indicates the index of the current from the list.
///
public int TabIndex {
get { return _tabIndex; }
set {
if (!CanFocus) {
_tabIndex = -1;
return;
} else if (SuperView?._tabIndexes == null || SuperView?._tabIndexes.Count == 1) {
_tabIndex = 0;
return;
} else if (_tabIndex == value) {
return;
}
_tabIndex = value > SuperView._tabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 : value < 0 ? 0 : value;
_tabIndex = GetTabIndex (_tabIndex);
if (SuperView._tabIndexes.IndexOf (this) != _tabIndex) {
SuperView._tabIndexes.Remove (this);
SuperView._tabIndexes.Insert (_tabIndex, this);
SetTabIndex ();
}
}
}
int GetTabIndex (int idx)
{
var i = 0;
foreach (var v in SuperView._tabIndexes) {
if (v._tabIndex == -1 || v == this) {
continue;
}
i++;
}
return Math.Min (i, idx);
}
void SetTabIndex ()
{
var i = 0;
foreach (var v in SuperView._tabIndexes) {
if (v._tabIndex == -1) {
continue;
}
v._tabIndex = i;
i++;
}
}
bool _tabStop = true;
///
/// This only be if the is also
/// and the focus can be avoided by setting this to
///
public bool TabStop {
get => _tabStop;
set {
if (_tabStop == value) {
return;
}
_tabStop = CanFocus && value;
}
}
int _oldTabIndex;
///
/// Invoked when a character key is pressed and occurs after the key up event.
///
public event EventHandler KeyPress;
///
public override bool ProcessKey (KeyEvent keyEvent)
{
if (!Enabled) {
return false;
}
var args = new KeyEventEventArgs (keyEvent);
KeyPress?.Invoke (this, args);
if (args.Handled)
return true;
if (Focused?.Enabled == true) {
Focused?.KeyPress?.Invoke (this, args);
if (args.Handled)
return true;
}
return Focused?.Enabled == true && Focused?.ProcessKey (keyEvent) == true;
}
///
/// Invokes any binding that is registered on this
/// and matches the
///
/// The key event passed.
protected bool? InvokeKeybindings (KeyEvent keyEvent)
{
bool? toReturn = null;
if (KeyBindings.ContainsKey (keyEvent.Key)) {
foreach (var command in KeyBindings [keyEvent.Key]) {
if (!CommandImplementations.ContainsKey (command)) {
throw new NotSupportedException ($"A KeyBinding was set up for the command {command} ({keyEvent.Key}) but that command is not supported by this View ({GetType ().Name})");
}
// each command has its own return value
var thisReturn = CommandImplementations [command] ();
// if we haven't got anything yet, the current command result should be used
if (toReturn == null) {
toReturn = thisReturn;
}
// if ever see a true then that's what we will return
if (thisReturn ?? false) {
toReturn = true;
}
}
}
return toReturn;
}
///
/// Adds a new key combination that will trigger the given
/// (if supported by the View - see )
///
/// If the key is already bound to a different it will be
/// rebound to this one
/// Commands are only ever applied to the current (i.e. this feature
/// cannot be used to switch focus to another view and perform multiple commands there)
///
///
/// The command(s) to run on the when is pressed.
/// When specifying multiple commands, all commands will be applied in sequence. The bound strike
/// will be consumed if any took effect.
public void AddKeyBinding (Key key, params Command [] command)
{
if (command.Length == 0) {
throw new ArgumentException ("At least one command must be specified", nameof (command));
}
if (KeyBindings.ContainsKey (key)) {
KeyBindings [key] = command;
} else {
KeyBindings.Add (key, command);
}
}
///
/// Replaces a key combination already bound to .
///
/// The key to be replaced.
/// The new key to be used.
protected void ReplaceKeyBinding (Key fromKey, Key toKey)
{
if (KeyBindings.ContainsKey (fromKey)) {
var value = KeyBindings [fromKey];
KeyBindings.Remove (fromKey);
KeyBindings [toKey] = value;
}
}
///
/// Checks if the key binding already exists.
///
/// The key to check.
/// If the key already exist, otherwise.
public bool ContainsKeyBinding (Key key)
{
return KeyBindings.ContainsKey (key);
}
///
/// Removes all bound keys from the View and resets the default bindings.
///
public void ClearKeybindings ()
{
KeyBindings.Clear ();
}
///
/// Clears the existing keybinding (if any) for the given .
///
///
public void ClearKeybinding (Key key)
{
KeyBindings.Remove (key);
}
///
/// Removes all key bindings that trigger the given command. Views can have multiple different
/// keys bound to the same command and this method will clear all of them.
///
///
public void ClearKeybinding (params Command [] command)
{
foreach (var kvp in KeyBindings.Where (kvp => kvp.Value.SequenceEqual (command)).ToArray ()) {
KeyBindings.Remove (kvp.Key);
}
}
///
/// States that the given supports a given
/// and what to perform to make that command happen
///
/// If the already has an implementation the
/// will replace the old one
///
/// The command.
/// The function.
protected void AddCommand (Command command, Func f)
{
// if there is already an implementation of this command
if (CommandImplementations.ContainsKey (command)) {
// replace that implementation
CommandImplementations [command] = f;
} else {
// else record how to perform the action (this should be the normal case)
CommandImplementations.Add (command, f);
}
}
///
/// Returns all commands that are supported by this .
///
///
public IEnumerable GetSupportedCommands ()
{
return CommandImplementations.Keys;
}
///
/// Gets the key used by a command.
///
/// The command to search.
/// The used by a
public Key GetKeyFromCommand (params Command [] command)
{
return KeyBindings.First (kb => kb.Value.SequenceEqual (command)).Key;
}
///
public override bool ProcessHotKey (KeyEvent keyEvent)
{
if (!Enabled) {
return false;
}
var args = new KeyEventEventArgs (keyEvent);
if (MostFocused?.Enabled == true) {
MostFocused?.KeyPress?.Invoke (this, args);
if (args.Handled)
return true;
}
if (MostFocused?.Enabled == true && MostFocused?.ProcessKey (keyEvent) == true)
return true;
if (_subviews == null || _subviews.Count == 0)
return false;
foreach (var view in _subviews)
if (view.Enabled && view.ProcessHotKey (keyEvent))
return true;
return false;
}
///
public override bool ProcessColdKey (KeyEvent keyEvent)
{
if (!Enabled) {
return false;
}
var args = new KeyEventEventArgs (keyEvent);
KeyPress?.Invoke (this, args);
if (args.Handled)
return true;
if (MostFocused?.Enabled == true) {
MostFocused?.KeyPress?.Invoke (this, args);
if (args.Handled)
return true;
}
if (MostFocused?.Enabled == true && MostFocused?.ProcessKey (keyEvent) == true)
return true;
if (_subviews == null || _subviews.Count == 0)
return false;
foreach (var view in _subviews)
if (view.Enabled && view.ProcessColdKey (keyEvent))
return true;
return false;
}
///
/// Invoked when a key is pressed.
///
public event EventHandler KeyDown;
///
public override bool OnKeyDown (KeyEvent keyEvent)
{
if (!Enabled) {
return false;
}
var args = new KeyEventEventArgs (keyEvent);
KeyDown?.Invoke (this, args);
if (args.Handled) {
return true;
}
if (Focused?.Enabled == true) {
Focused.KeyDown?.Invoke (this, args);
if (args.Handled) {
return true;
}
if (Focused?.OnKeyDown (keyEvent) == true) {
return true;
}
}
return false;
}
///
/// Invoked when a key is released.
///
public event EventHandler KeyUp;
///
public override bool OnKeyUp (KeyEvent keyEvent)
{
if (!Enabled) {
return false;
}
var args = new KeyEventEventArgs (keyEvent);
KeyUp?.Invoke (this, args);
if (args.Handled) {
return true;
}
if (Focused?.Enabled == true) {
Focused.KeyUp?.Invoke (this, args);
if (args.Handled) {
return true;
}
if (Focused?.OnKeyUp (keyEvent) == true) {
return true;
}
}
return false;
}
void SetHotKey ()
{
if (TextFormatter == null) {
return; // throw new InvalidOperationException ("Can't set HotKey unless a TextFormatter has been created");
}
TextFormatter.FindHotKey (_text, HotKeySpecifier, true, out _, out var hk);
if (_hotKey != hk) {
HotKey = hk;
}
}
}
}