KeyBinding.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256
  1. // These classes use a key binding system based on the design implemented in Scintilla.Net which is an
  2. // MIT licensed open source project https://github.com/jacobslusser/ScintillaNET/blob/master/src/ScintillaNET/Command.cs
  3. namespace Terminal.Gui;
  4. /// <summary>
  5. /// Defines the scope of a <see cref="Command"/> that has been bound to a key with
  6. /// <see cref="KeyBindings.Add(Key, Terminal.Gui.Command[])"/>.
  7. /// </summary>
  8. /// <remarks>
  9. /// <para>Key bindings are scoped to the most-focused view (<see cref="Focused"/>) by default.</para>
  10. /// </remarks>
  11. public enum KeyBindingScope
  12. {
  13. /// <summary>The key binding is scoped to just the view that has focus.</summary>
  14. Focused = 0,
  15. /// <summary>
  16. /// The key binding is scoped to the View's SuperView and will be triggered even when the View does not have focus, as
  17. /// long as the SuperView does have focus. This is typically used for <see cref="View.HotKey"/>s.
  18. /// <remarks>
  19. /// <para>
  20. /// Use for Views such as MenuBar and StatusBar which provide commands (shortcuts etc...) that trigger even
  21. /// when not focused.
  22. /// </para>
  23. /// <para>
  24. /// HotKey-scoped key bindings are only invoked if the key down event was not handled by the focused view or
  25. /// any of its subviews.
  26. /// </para>
  27. /// </remarks>
  28. /// </summary>
  29. HotKey,
  30. /// <summary>
  31. /// The key binding will be triggered regardless of which view has focus. This is typically used for global
  32. /// commands.
  33. /// </summary>
  34. /// <remarks>
  35. /// Application-scoped key bindings are only invoked if the key down event was not handled by the focused view or
  36. /// any of its subviews, and if the key down event was not bound to a <see cref="View.HotKey"/>.
  37. /// </remarks>
  38. Application
  39. }
  40. /// <summary>Provides a collection of <see cref="Command"/> objects that are scoped to <see cref="KeyBindingScope"/>.</summary>
  41. public class KeyBinding
  42. {
  43. /// <summary>Initializes a new instance.</summary>
  44. /// <param name="commands"></param>
  45. /// <param name="scope"></param>
  46. public KeyBinding (Command [] commands, KeyBindingScope scope)
  47. {
  48. Commands = commands;
  49. Scope = scope;
  50. }
  51. /// <summary>The actions which can be performed by the application or bound to keys in a <see cref="View"/> control.</summary>
  52. public Command [] Commands { get; set; }
  53. /// <summary>The scope of the <see cref="Commands"/> bound to a key.</summary>
  54. public KeyBindingScope Scope { get; set; }
  55. }
  56. /// <summary>A class that provides a collection of <see cref="KeyBinding"/> objects bound to a <see cref="Key"/>.</summary>
  57. public class KeyBindings
  58. {
  59. /// <summary>The collection of <see cref="KeyBinding"/> objects.</summary>
  60. public Dictionary<Key, KeyBinding> Bindings { get; } = new ();
  61. /// <summary>Adds a <see cref="KeyBinding"/> to the collection.</summary>
  62. /// <param name="key"></param>
  63. /// <param name="binding"></param>
  64. public void Add (Key key, KeyBinding binding) { Bindings.Add (key, binding); }
  65. /// <summary>
  66. /// <para>Adds a new key combination that will trigger the commands in <paramref name="commands"/>.</para>
  67. /// <para>
  68. /// If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
  69. /// <paramref name="commands"/>.
  70. /// </para>
  71. /// </summary>
  72. /// <remarks>
  73. /// Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
  74. /// focus to another view and perform multiple commands there).
  75. /// </remarks>
  76. /// <param name="key">The key to check.</param>
  77. /// <param name="scope">The scope for the command.</param>
  78. /// <param name="commands">
  79. /// The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
  80. /// multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
  81. /// consumed if any took effect.
  82. /// </param>
  83. public void Add (Key key, KeyBindingScope scope, params Command [] commands)
  84. {
  85. if (commands.Length == 0)
  86. {
  87. throw new ArgumentException (@"At least one command must be specified", nameof (commands));
  88. }
  89. if (key is null || !key.IsValid)
  90. {
  91. //throw new ArgumentException ("Invalid Key", nameof (commands));
  92. return;
  93. }
  94. if (TryGet (key, out KeyBinding _))
  95. {
  96. Bindings [key] = new KeyBinding (commands, scope);
  97. }
  98. else
  99. {
  100. Bindings.Add (key, new KeyBinding (commands, scope));
  101. }
  102. }
  103. /// <summary>
  104. /// <para>
  105. /// Adds a new key combination that will trigger the commands in <paramref name="commands"/> (if supported by the
  106. /// View - see <see cref="View.GetSupportedCommands"/>).
  107. /// </para>
  108. /// <para>
  109. /// This is a helper function for <see cref="Add(Key,KeyBindingScope,Terminal.Gui.Command[])"/> for
  110. /// <see cref="KeyBindingScope.Focused"/> scoped commands.
  111. /// </para>
  112. /// <para>
  113. /// If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
  114. /// <paramref name="commands"/>.
  115. /// </para>
  116. /// </summary>
  117. /// <remarks>
  118. /// Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
  119. /// focus to another view and perform multiple commands there).
  120. /// </remarks>
  121. /// <param name="key">The key to check.</param>
  122. /// <param name="commands">
  123. /// The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
  124. /// multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
  125. /// consumed if any took effect.
  126. /// </param>
  127. public void Add (Key key, params Command [] commands) { Add (key, KeyBindingScope.Focused, commands); }
  128. /// <summary>Removes all <see cref="KeyBinding"/> objects from the collection.</summary>
  129. public void Clear () { Bindings.Clear (); }
  130. /// <summary>
  131. /// Removes all key bindings that trigger the given command set. Views can have multiple different keys bound to
  132. /// the same command sets and this method will clear all of them.
  133. /// </summary>
  134. /// <param name="command"></param>
  135. public void Clear (params Command [] command)
  136. {
  137. foreach (KeyValuePair<Key, KeyBinding> kvp in Bindings.Where (kvp => kvp.Value.Commands.SequenceEqual (command))
  138. .ToArray ())
  139. {
  140. Bindings.Remove (kvp.Key);
  141. }
  142. }
  143. /// <summary>Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.</summary>
  144. /// <param name="key"></param>
  145. /// <returns></returns>
  146. public KeyBinding Get (Key key) { return TryGet (key, out KeyBinding binding) ? binding : null; }
  147. /// <summary>Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.</summary>
  148. /// <param name="key"></param>
  149. /// <param name="scope"></param>
  150. /// <returns></returns>
  151. public KeyBinding Get (Key key, KeyBindingScope scope) { return TryGet (key, scope, out KeyBinding binding) ? binding : null; }
  152. /// <summary>Gets the array of <see cref="Command"/>s bound to <paramref name="key"/> if it exists.</summary>
  153. /// <param name="key">The key to check.</param>
  154. /// <returns>
  155. /// The array of <see cref="Command"/>s if <paramref name="key"/> is bound. An empty <see cref="Command"/> array
  156. /// if not.
  157. /// </returns>
  158. public Command [] GetCommands (Key key)
  159. {
  160. if (TryGet (key, out KeyBinding bindings))
  161. {
  162. return bindings.Commands;
  163. }
  164. return Array.Empty<Command> ();
  165. }
  166. /// <summary>Gets the Key used by a set of commands.</summary>
  167. /// <remarks></remarks>
  168. /// <param name="commands">The set of commands to search.</param>
  169. /// <returns>The <see cref="Key"/> used by a <see cref="Command"/></returns>
  170. /// <exception cref="InvalidOperationException">If no matching set of commands was found.</exception>
  171. public Key GetKeyFromCommands (params Command [] commands) { return Bindings.First (a => a.Value.Commands.SequenceEqual (commands)).Key; }
  172. /// <summary>Removes a <see cref="KeyBinding"/> from the collection.</summary>
  173. /// <param name="key"></param>
  174. public void Remove (Key key) { Bindings.Remove (key); }
  175. /// <summary>Replaces a key combination already bound to a set of <see cref="Command"/>s.</summary>
  176. /// <remarks></remarks>
  177. /// <param name="fromKey">The key to be replaced.</param>
  178. /// <param name="toKey">The new key to be used.</param>
  179. public void Replace (Key fromKey, Key toKey)
  180. {
  181. if (!TryGet (fromKey, out KeyBinding _))
  182. {
  183. return;
  184. }
  185. KeyBinding value = Bindings [fromKey];
  186. Bindings.Remove (fromKey);
  187. Bindings [toKey] = value;
  188. }
  189. /// <summary>Gets the commands bound with the specified Key.</summary>
  190. /// <remarks></remarks>
  191. /// <param name="key">The key to check.</param>
  192. /// <param name="binding">
  193. /// When this method returns, contains the commands bound with the specified Key, if the Key is
  194. /// found; otherwise, null. This parameter is passed uninitialized.
  195. /// </param>
  196. /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
  197. public bool TryGet (Key key, out KeyBinding binding)
  198. {
  199. if (key.IsValid)
  200. {
  201. return Bindings.TryGetValue (key, out binding);
  202. }
  203. binding = new KeyBinding (Array.Empty<Command> (), KeyBindingScope.Focused);
  204. return false;
  205. }
  206. /// <summary>Gets the commands bound with the specified Key that are scoped to a particular scope.</summary>
  207. /// <remarks></remarks>
  208. /// <param name="key">The key to check.</param>
  209. /// <param name="scope">the scope to filter on</param>
  210. /// <param name="binding">
  211. /// When this method returns, contains the commands bound with the specified Key, if the Key is
  212. /// found; otherwise, null. This parameter is passed uninitialized.
  213. /// </param>
  214. /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
  215. public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding)
  216. {
  217. if (key.IsValid && Bindings.TryGetValue (key, out binding))
  218. {
  219. if (binding.Scope == scope)
  220. {
  221. return true;
  222. }
  223. }
  224. binding = new KeyBinding (Array.Empty<Command> (), KeyBindingScope.Focused);
  225. return false;
  226. }
  227. }