KeyBindings.cs 18 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447
  1. #nullable enable
  2. using static System.Formats.Asn1.AsnWriter;
  3. namespace Terminal.Gui;
  4. /// <summary>
  5. /// Provides a collection of <see cref="KeyBinding"/> objects bound to a <see cref="Key"/>.
  6. /// </summary>
  7. /// <seealso cref="Application.KeyBindings"/>
  8. /// <seealso cref="View.KeyBindings"/>
  9. /// <seealso cref="Command"/>
  10. public class KeyBindings
  11. {
  12. /// <summary>
  13. /// Initializes a new instance. This constructor is used when the <see cref="KeyBindings"/> are not bound to a
  14. /// <see cref="View"/>. This is used for Application.KeyBindings and unit tests.
  15. /// </summary>
  16. public KeyBindings () { }
  17. /// <summary>Initializes a new instance bound to <paramref name="boundView"/>.</summary>
  18. public KeyBindings (View? boundView) { BoundView = boundView; }
  19. /// <summary>Adds a <see cref="KeyBinding"/> to the collection.</summary>
  20. /// <param name="key"></param>
  21. /// <param name="binding"></param>
  22. /// <param name="boundViewForAppScope">Optional View for <see cref="KeyBindingScope.Application"/> bindings.</param>
  23. public void Add (Key key, KeyBinding binding, View? boundViewForAppScope = null)
  24. {
  25. if (BoundView is { } && binding.Scope.FastHasFlags (KeyBindingScope.Application))
  26. {
  27. throw new InvalidOperationException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add");
  28. }
  29. if (BoundView is { } && boundViewForAppScope is null)
  30. {
  31. boundViewForAppScope = BoundView;
  32. }
  33. if (TryGet (key, out KeyBinding _))
  34. {
  35. throw new InvalidOperationException (@$"A key binding for {key} exists ({binding}).");
  36. //Bindings [key] = binding;
  37. }
  38. if (BoundView is { })
  39. {
  40. binding.BoundView = BoundView;
  41. }
  42. else
  43. {
  44. binding.BoundView = boundViewForAppScope;
  45. }
  46. // IMPORTANT: Add a COPY of the key. This is needed because ConfigurationManager.Apply uses DeepMemberWiseCopy
  47. // IMPORTANT: update the memory referenced by the key, and Dictionary uses caching for performance, and thus
  48. // IMPORTANT: Apply will update the Dictionary with the new key, but the old key will still be in the dictionary.
  49. // IMPORTANT: See the ConfigurationManager.Illustrate_DeepMemberWiseCopy_Breaks_Dictionary test for details.
  50. Bindings.Add (new (key), binding);
  51. }
  52. /// <summary>
  53. /// <para>Adds a new key combination that will trigger the commands in <paramref name="commands"/>.</para>
  54. /// <para>
  55. /// If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
  56. /// <paramref name="commands"/>.
  57. /// </para>
  58. /// </summary>
  59. /// <remarks>
  60. /// Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
  61. /// focus to another view and perform multiple commands there).
  62. /// </remarks>
  63. /// <param name="key">The key to check.</param>
  64. /// <param name="scope">The scope for the command.</param>
  65. /// <param name="boundViewForAppScope">Optional View for <see cref="KeyBindingScope.Application"/> bindings.</param>
  66. /// <param name="commands">
  67. /// The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
  68. /// multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
  69. /// consumed if any took effect.
  70. /// </param>
  71. public void Add (Key key, KeyBindingScope scope, View? boundViewForAppScope = null, params Command [] commands)
  72. {
  73. if (BoundView is { } && scope.FastHasFlags (KeyBindingScope.Application))
  74. {
  75. throw new ArgumentException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add");
  76. }
  77. else
  78. {
  79. // boundViewForAppScope = BoundView;
  80. }
  81. if (key is null || !key.IsValid)
  82. {
  83. //throw new ArgumentException ("Invalid Key", nameof (commands));
  84. return;
  85. }
  86. if (commands.Length == 0)
  87. {
  88. throw new ArgumentException (@"At least one command must be specified", nameof (commands));
  89. }
  90. if (TryGet (key, out KeyBinding binding))
  91. {
  92. throw new InvalidOperationException (@$"A key binding for {key} exists ({binding}).");
  93. }
  94. Add (key, new KeyBinding (commands, scope, boundViewForAppScope), boundViewForAppScope);
  95. }
  96. /// <summary>
  97. /// <para>Adds a new key combination that will trigger the commands in <paramref name="commands"/>.</para>
  98. /// <para>
  99. /// If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
  100. /// <paramref name="commands"/>.
  101. /// </para>
  102. /// </summary>
  103. /// <remarks>
  104. /// Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
  105. /// focus to another view and perform multiple commands there).
  106. /// </remarks>
  107. /// <param name="key">The key to check.</param>
  108. /// <param name="scope">The scope for the command.</param>
  109. /// <param name="commands">
  110. /// The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
  111. /// multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
  112. /// consumed if any took effect.
  113. /// </param>
  114. public void Add (Key key, KeyBindingScope scope, params Command [] commands)
  115. {
  116. if (BoundView is null && !scope.FastHasFlags (KeyBindingScope.Application))
  117. {
  118. throw new InvalidOperationException ("BoundView cannot be null.");
  119. }
  120. if (BoundView is { } && scope.FastHasFlags (KeyBindingScope.Application))
  121. {
  122. throw new InvalidOperationException ("Application scoped KeyBindings must be added via Application.KeyBindings.Add");
  123. }
  124. if (key == Key.Empty || !key.IsValid)
  125. {
  126. throw new ArgumentException (@"Invalid Key", nameof (commands));
  127. }
  128. if (commands.Length == 0)
  129. {
  130. throw new ArgumentException (@"At least one command must be specified", nameof (commands));
  131. }
  132. if (TryGet (key, out KeyBinding binding))
  133. {
  134. throw new InvalidOperationException (@$"A key binding for {key} exists ({binding}).");
  135. }
  136. Add (key, new KeyBinding (commands, scope, BoundView), BoundView);
  137. }
  138. /// <summary>
  139. /// <para>
  140. /// Adds a new key combination that will trigger the commands in <paramref name="commands"/> (if supported by the
  141. /// View - see <see cref="View.GetSupportedCommands"/>).
  142. /// </para>
  143. /// <para>
  144. /// This is a helper function for <see cref="Add(Key,KeyBinding,View?)"/>. If used for a View (
  145. /// <see cref="BoundView"/> is set), the scope will be set to <see cref="KeyBindingScope.Focused"/>.
  146. /// Otherwise, it will be set to <see cref="KeyBindingScope.Application"/>.
  147. /// </para>
  148. /// <para>
  149. /// If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
  150. /// <paramref name="commands"/>.
  151. /// </para>
  152. /// </summary>
  153. /// <remarks>
  154. /// Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
  155. /// focus to another view and perform multiple commands there).
  156. /// </remarks>
  157. /// <param name="key">The key to check.</param>
  158. /// <param name="boundViewForAppScope">Optional View for <see cref="KeyBindingScope.Application"/> bindings.</param>
  159. /// <param name="commands">
  160. /// The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
  161. /// multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
  162. /// consumed if any took effect.
  163. /// </param>
  164. public void Add (Key key, View? boundViewForAppScope = null, params Command [] commands)
  165. {
  166. if (BoundView is null && boundViewForAppScope is null)
  167. {
  168. throw new ArgumentException (@"Application scoped KeyBindings must provide a bound view to Add.", nameof (boundViewForAppScope));
  169. }
  170. Add (key, BoundView is { } ? KeyBindingScope.Focused : KeyBindingScope.Application, boundViewForAppScope, commands);
  171. }
  172. /// <summary>
  173. /// <para>
  174. /// Adds a new key combination that will trigger the commands in <paramref name="commands"/> (if supported by the
  175. /// View - see <see cref="View.GetSupportedCommands"/>).
  176. /// </para>
  177. /// <para>
  178. /// This is a helper function for <see cref="Add(Key,KeyBinding,View?)"/>. If used for a View (
  179. /// <see cref="BoundView"/> is set), the scope will be set to <see cref="KeyBindingScope.Focused"/>.
  180. /// Otherwise, it will be set to <see cref="KeyBindingScope.Application"/>.
  181. /// </para>
  182. /// <para>
  183. /// If the key is already bound to a different array of <see cref="Command"/>s it will be rebound
  184. /// <paramref name="commands"/>.
  185. /// </para>
  186. /// </summary>
  187. /// <remarks>
  188. /// Commands are only ever applied to the current <see cref="View"/> (i.e. this feature cannot be used to switch
  189. /// focus to another view and perform multiple commands there).
  190. /// </remarks>
  191. /// <param name="key">The key to check.</param>
  192. /// <param name="commands">
  193. /// The command to invoked on the <see cref="View"/> when <paramref name="key"/> is pressed. When
  194. /// multiple commands are provided,they will be applied in sequence. The bound <paramref name="key"/> strike will be
  195. /// consumed if any took effect.
  196. /// </param>
  197. public void Add (Key key, params Command [] commands)
  198. {
  199. if (BoundView is null)
  200. {
  201. throw new ArgumentException (@"Application scoped KeyBindings must provide a boundViewForAppScope to Add.");
  202. }
  203. Add (key, BoundView is { } ? KeyBindingScope.Focused : KeyBindingScope.Application, null, commands);
  204. }
  205. // TODO: Add a dictionary comparer that ignores Scope
  206. // TODO: This should not be public!
  207. /// <summary>The collection of <see cref="KeyBinding"/> objects.</summary>
  208. public Dictionary<Key, KeyBinding> Bindings { get; } = new (new KeyEqualityComparer ());
  209. /// <summary>
  210. /// Gets the keys that are bound.
  211. /// </summary>
  212. /// <returns></returns>
  213. public IEnumerable<Key> GetBoundKeys ()
  214. {
  215. return Bindings.Keys;
  216. }
  217. /// <summary>
  218. /// The view that the <see cref="KeyBindings"/> are bound to.
  219. /// </summary>
  220. /// <remarks>
  221. /// If <see langword="null"/> the KeyBindings object is being used for Application.KeyBindings.
  222. /// </remarks>
  223. internal View? BoundView { get; }
  224. /// <summary>Removes all <see cref="KeyBinding"/> objects from the collection.</summary>
  225. public void Clear () { Bindings.Clear (); }
  226. /// <summary>
  227. /// Removes all key bindings that trigger the given command set. Views can have multiple different keys bound to
  228. /// the same command sets and this method will clear all of them.
  229. /// </summary>
  230. /// <param name="command"></param>
  231. public void Clear (params Command [] command)
  232. {
  233. KeyValuePair<Key, KeyBinding> [] kvps = Bindings
  234. .Where (kvp => kvp.Value.Commands.SequenceEqual (command))
  235. .ToArray ();
  236. foreach (KeyValuePair<Key, KeyBinding> kvp in kvps)
  237. {
  238. Remove (kvp.Key);
  239. }
  240. }
  241. /// <summary>Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.</summary>
  242. /// <param name="key"></param>
  243. /// <returns></returns>
  244. public KeyBinding Get (Key key)
  245. {
  246. if (TryGet (key, out KeyBinding binding))
  247. {
  248. return binding;
  249. }
  250. throw new InvalidOperationException ($"Key {key} is not bound.");
  251. }
  252. /// <summary>Gets the <see cref="KeyBinding"/> for the specified <see cref="Key"/>.</summary>
  253. /// <param name="key"></param>
  254. /// <param name="scope"></param>
  255. /// <returns></returns>
  256. public KeyBinding Get (Key key, KeyBindingScope scope)
  257. {
  258. if (TryGet (key, scope, out KeyBinding binding))
  259. {
  260. return binding;
  261. }
  262. throw new InvalidOperationException ($"Key {key}/{scope} is not bound.");
  263. }
  264. /// <summary>Gets the array of <see cref="Command"/>s bound to <paramref name="key"/> if it exists.</summary>
  265. /// <param name="key">The key to check.</param>
  266. /// <returns>
  267. /// The array of <see cref="Command"/>s if <paramref name="key"/> is bound. An empty <see cref="Command"/> array
  268. /// if not.
  269. /// </returns>
  270. public Command [] GetCommands (Key key)
  271. {
  272. if (TryGet (key, out KeyBinding bindings))
  273. {
  274. return bindings.Commands;
  275. }
  276. return Array.Empty<Command> ();
  277. }
  278. /// <summary>Gets the first Key bound to the set of commands specified by <paramref name="commands"/>.</summary>
  279. /// <param name="commands">The set of commands to search.</param>
  280. /// <returns>The first <see cref="Key"/> bound to the set of commands specified by <paramref name="commands"/>. <see langword="null"/> if the set of caommands was not found.</returns>
  281. public Key? GetKeyFromCommands (params Command [] commands)
  282. {
  283. return Bindings.FirstOrDefault (a => a.Value.Commands.SequenceEqual (commands)).Key;
  284. }
  285. /// <summary>Gets Keys bound to the set of commands specified by <paramref name="commands"/>.</summary>
  286. /// <param name="commands">The set of commands to search.</param>
  287. /// <returns>The <see cref="Key"/>s bound to the set of commands specified by <paramref name="commands"/>. An empty list if the set of caommands was not found.</returns>
  288. public IEnumerable<Key> GetKeysFromCommands (params Command [] commands)
  289. {
  290. return Bindings.Where (a => a.Value.Commands.SequenceEqual (commands)).Select (a => a.Key);
  291. }
  292. /// <summary>Removes a <see cref="KeyBinding"/> from the collection.</summary>
  293. /// <param name="key"></param>
  294. /// <param name="boundViewForAppScope">Optional View for <see cref="KeyBindingScope.Application"/> bindings.</param>
  295. public void Remove (Key key, View? boundViewForAppScope = null)
  296. {
  297. if (!TryGet (key, out KeyBinding _))
  298. {
  299. return;
  300. }
  301. Bindings.Remove (key);
  302. }
  303. /// <summary>Replaces the commands already bound to a key.</summary>
  304. /// <remarks>
  305. /// <para>
  306. /// If the key is not already bound, it will be added.
  307. /// </para>
  308. /// </remarks>
  309. /// <param name="key">The key bound to the command to be replaced.</param>
  310. /// <param name="commands">The set of commands to replace the old ones with.</param>
  311. public void ReplaceCommands (Key key, params Command [] commands)
  312. {
  313. if (TryGet (key, out KeyBinding binding))
  314. {
  315. binding.Commands = commands;
  316. }
  317. else
  318. {
  319. Add (key, commands);
  320. }
  321. }
  322. /// <summary>Replaces a key combination already bound to a set of <see cref="Command"/>s.</summary>
  323. /// <remarks></remarks>
  324. /// <param name="oldKey">The key to be replaced.</param>
  325. /// <param name="newKey">The new key to be used. If <see cref="Key.Empty"/> no action will be taken.</param>
  326. public void ReplaceKey (Key oldKey, Key newKey)
  327. {
  328. if (!TryGet (oldKey, out KeyBinding _))
  329. {
  330. throw new InvalidOperationException ($"Key {oldKey} is not bound.");
  331. }
  332. if (!newKey.IsValid)
  333. {
  334. throw new InvalidOperationException ($"Key {newKey} is is not valid.");
  335. }
  336. KeyBinding value = Bindings [oldKey];
  337. Remove (oldKey);
  338. Add (newKey, value);
  339. }
  340. /// <summary>Gets the commands bound with the specified Key.</summary>
  341. /// <remarks></remarks>
  342. /// <param name="key">The key to check.</param>
  343. /// <param name="binding">
  344. /// When this method returns, contains the commands bound with the specified Key, if the Key is
  345. /// found; otherwise, null. This parameter is passed uninitialized.
  346. /// </param>
  347. /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
  348. public bool TryGet (Key key, out KeyBinding binding)
  349. {
  350. //if (BoundView is null)
  351. //{
  352. // throw new InvalidOperationException ("KeyBindings must be bound to a View to use this method.");
  353. //}
  354. binding = new (Array.Empty<Command> (), KeyBindingScope.Disabled, null);
  355. if (key.IsValid)
  356. {
  357. return Bindings.TryGetValue (key, out binding);
  358. }
  359. return false;
  360. }
  361. /// <summary>Gets the commands bound with the specified Key that are scoped to a particular scope.</summary>
  362. /// <remarks></remarks>
  363. /// <param name="key">The key to check.</param>
  364. /// <param name="scope">the scope to filter on</param>
  365. /// <param name="binding">
  366. /// When this method returns, contains the commands bound with the specified Key, if the Key is
  367. /// found; otherwise, null. This parameter is passed uninitialized.
  368. /// </param>
  369. /// <returns><see langword="true"/> if the Key is bound; otherwise <see langword="false"/>.</returns>
  370. public bool TryGet (Key key, KeyBindingScope scope, out KeyBinding binding)
  371. {
  372. if (!key.IsValid)
  373. {
  374. //if (BoundView is null)
  375. //{
  376. // throw new InvalidOperationException ("KeyBindings must be bound to a View to use this method.");
  377. //}
  378. binding = new (Array.Empty<Command> (), KeyBindingScope.Disabled, null);
  379. return false;
  380. }
  381. if (Bindings.TryGetValue (key, out binding))
  382. {
  383. if (scope.HasFlag (binding.Scope))
  384. {
  385. return true;
  386. }
  387. }
  388. else
  389. {
  390. binding = new (Array.Empty<Command> (), KeyBindingScope.Disabled, null);
  391. }
  392. return false;
  393. }
  394. }