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