Application.Keyboard.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323
  1. #nullable enable
  2. namespace Terminal.Gui;
  3. public static partial class Application // Keyboard handling
  4. {
  5. /// <summary>
  6. /// Called when the user presses a key (by the <see cref="ConsoleDriver"/>). Raises the cancelable
  7. /// <see cref="KeyDown"/> event, then calls <see cref="View.NewKeyDownEvent"/> on all top level views, and finally
  8. /// if the key was not handled, invokes any Application-scoped <see cref="KeyBindings"/>.
  9. /// </summary>
  10. /// <remarks>Can be used to simulate key press events.</remarks>
  11. /// <param name="key"></param>
  12. /// <returns><see langword="true"/> if the key was handled.</returns>
  13. public static bool RaiseKeyDownEvent (Key key)
  14. {
  15. KeyDown?.Invoke (null, key);
  16. if (key.Handled)
  17. {
  18. return true;
  19. }
  20. if (Top is null)
  21. {
  22. foreach (Toplevel topLevel in TopLevels.ToList ())
  23. {
  24. if (topLevel.NewKeyDownEvent (key))
  25. {
  26. return true;
  27. }
  28. if (topLevel.Modal)
  29. {
  30. break;
  31. }
  32. }
  33. }
  34. else
  35. {
  36. if (Top.NewKeyDownEvent (key))
  37. {
  38. return true;
  39. }
  40. }
  41. // Invoke any Application-scoped KeyBindings.
  42. // The first view that handles the key will stop the loop.
  43. foreach (KeyValuePair<Key, KeyBinding> binding in KeyBindings.Bindings.Where (b => b.Key == key.KeyCode))
  44. {
  45. if (binding.Value.BoundView is { })
  46. {
  47. bool? handled = binding.Value.BoundView?.InvokeCommands (binding.Value.Commands, binding.Key, binding.Value);
  48. if (handled != null && (bool)handled)
  49. {
  50. return true;
  51. }
  52. }
  53. else
  54. {
  55. if (!KeyBindings.TryGet (key, KeyBindingScope.Application, out KeyBinding appBinding))
  56. {
  57. continue;
  58. }
  59. bool? toReturn = null;
  60. foreach (Command command in appBinding.Commands)
  61. {
  62. toReturn = InvokeCommand (command, key, appBinding);
  63. }
  64. return toReturn ?? true;
  65. }
  66. }
  67. return false;
  68. static bool? InvokeCommand (Command command, Key key, KeyBinding appBinding)
  69. {
  70. if (!CommandImplementations!.ContainsKey (command))
  71. {
  72. throw new NotSupportedException (
  73. @$"A KeyBinding was set up for the command {command} ({key}) but that command is not supported by Application."
  74. );
  75. }
  76. if (CommandImplementations.TryGetValue (command, out View.CommandImplementation? implementation))
  77. {
  78. var context = new CommandContext (command, key, appBinding); // Create the context here
  79. return implementation (context);
  80. }
  81. return false;
  82. }
  83. }
  84. /// <summary>
  85. /// Raised when the user presses a key.
  86. /// <para>
  87. /// Set <see cref="Key.Handled"/> to <see langword="true"/> to indicate the key was handled and to prevent
  88. /// additional processing.
  89. /// </para>
  90. /// </summary>
  91. /// <remarks>
  92. /// All drivers support firing the <see cref="KeyDown"/> event. Some drivers (Curses) do not support firing the
  93. /// <see cref="KeyDown"/> and <see cref="KeyUp"/> events.
  94. /// <para>Fired after <see cref="KeyDown"/> and before <see cref="KeyUp"/>.</para>
  95. /// </remarks>
  96. public static event EventHandler<Key>? KeyDown;
  97. /// <summary>
  98. /// Called when the user releases a key (by the <see cref="ConsoleDriver"/>). Raises the cancelable <see cref="KeyUp"/>
  99. /// event
  100. /// then calls <see cref="View.NewKeyUpEvent"/> on all top level views. Called after <see cref="RaiseKeyDownEvent"/>.
  101. /// </summary>
  102. /// <remarks>Can be used to simulate key release events.</remarks>
  103. /// <param name="key"></param>
  104. /// <returns><see langword="true"/> if the key was handled.</returns>
  105. public static bool RaiseKeyUpEvent (Key key)
  106. {
  107. if (!Initialized)
  108. {
  109. return true;
  110. }
  111. KeyUp?.Invoke (null, key);
  112. if (key.Handled)
  113. {
  114. return true;
  115. }
  116. foreach (Toplevel topLevel in TopLevels.ToList ())
  117. {
  118. if (topLevel.NewKeyUpEvent (key))
  119. {
  120. return true;
  121. }
  122. if (topLevel.Modal)
  123. {
  124. break;
  125. }
  126. }
  127. return false;
  128. }
  129. #region Application-scoped KeyBindings
  130. static Application () { AddApplicationKeyBindings (); }
  131. /// <summary>Gets the Application-scoped key bindings.</summary>
  132. public static KeyBindings KeyBindings { get; internal set; } = new ();
  133. internal static void AddApplicationKeyBindings ()
  134. {
  135. CommandImplementations = new ();
  136. // Things this view knows how to do
  137. AddCommand (
  138. Command.Quit,
  139. static () =>
  140. {
  141. RequestStop ();
  142. return true;
  143. }
  144. );
  145. AddCommand (
  146. Command.Suspend,
  147. static () =>
  148. {
  149. Driver?.Suspend ();
  150. return true;
  151. }
  152. );
  153. AddCommand (
  154. Command.NextTabStop,
  155. static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
  156. AddCommand (
  157. Command.PreviousTabStop,
  158. static () => Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop));
  159. AddCommand (
  160. Command.NextTabGroup,
  161. static () => Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup));
  162. AddCommand (
  163. Command.PreviousTabGroup,
  164. static () => Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup));
  165. AddCommand (
  166. Command.Refresh,
  167. static () =>
  168. {
  169. LayoutAndDrawToplevels ();
  170. return true;
  171. }
  172. );
  173. AddCommand (
  174. Command.Edit,
  175. static () =>
  176. {
  177. View? viewToArrange = Navigation?.GetFocused ();
  178. // Go up the superview hierarchy and find the first that is not ViewArrangement.Fixed
  179. while (viewToArrange is { SuperView: { }, Arrangement: ViewArrangement.Fixed })
  180. {
  181. viewToArrange = viewToArrange.SuperView;
  182. }
  183. if (viewToArrange is { })
  184. {
  185. return viewToArrange.Border?.EnterArrangeMode (ViewArrangement.Fixed);
  186. }
  187. return false;
  188. });
  189. KeyBindings.Clear ();
  190. // Resources/config.json overrides
  191. NextTabKey = Key.Tab;
  192. PrevTabKey = Key.Tab.WithShift;
  193. NextTabGroupKey = Key.F6;
  194. PrevTabGroupKey = Key.F6.WithShift;
  195. QuitKey = Key.Esc;
  196. ArrangeKey = Key.F5.WithCtrl;
  197. KeyBindings.Add (QuitKey, KeyBindingScope.Application, Command.Quit);
  198. KeyBindings.Add (Key.CursorRight, KeyBindingScope.Application, Command.NextTabStop);
  199. KeyBindings.Add (Key.CursorDown, KeyBindingScope.Application, Command.NextTabStop);
  200. KeyBindings.Add (Key.CursorLeft, KeyBindingScope.Application, Command.PreviousTabStop);
  201. KeyBindings.Add (Key.CursorUp, KeyBindingScope.Application, Command.PreviousTabStop);
  202. KeyBindings.Add (NextTabKey, KeyBindingScope.Application, Command.NextTabStop);
  203. KeyBindings.Add (PrevTabKey, KeyBindingScope.Application, Command.PreviousTabStop);
  204. KeyBindings.Add (NextTabGroupKey, KeyBindingScope.Application, Command.NextTabGroup);
  205. KeyBindings.Add (PrevTabGroupKey, KeyBindingScope.Application, Command.PreviousTabGroup);
  206. KeyBindings.Add (ArrangeKey, KeyBindingScope.Application, Command.Edit);
  207. // TODO: Refresh Key should be configurable
  208. KeyBindings.Add (Key.F5, KeyBindingScope.Application, Command.Refresh);
  209. // TODO: Suspend Key should be configurable
  210. if (Environment.OSVersion.Platform == PlatformID.Unix)
  211. {
  212. KeyBindings.Add (Key.Z.WithCtrl, KeyBindingScope.Application, Command.Suspend);
  213. }
  214. }
  215. /// <summary>
  216. /// Gets the list of Views that have <see cref="KeyBindingScope.Application"/> key bindings.
  217. /// </summary>
  218. /// <remarks>
  219. /// This is an internal method used by the <see cref="View"/> class to add Application key bindings.
  220. /// </remarks>
  221. /// <returns>The list of Views that have Application-scoped key bindings.</returns>
  222. internal static List<KeyBinding> GetViewKeyBindings ()
  223. {
  224. // Get the list of views that do not have Application-scoped key bindings
  225. return KeyBindings.Bindings
  226. .Where (kv => kv.Value.Scope != KeyBindingScope.Application)
  227. .Select (kv => kv.Value)
  228. .Distinct ()
  229. .ToList ();
  230. }
  231. private static void ReplaceKey (Key oldKey, Key newKey)
  232. {
  233. if (KeyBindings.Bindings.Count == 0)
  234. {
  235. return;
  236. }
  237. if (newKey == Key.Empty)
  238. {
  239. KeyBindings.Remove (oldKey);
  240. }
  241. else
  242. {
  243. KeyBindings.ReplaceKey (oldKey, newKey);
  244. }
  245. }
  246. #endregion Application-scoped KeyBindings
  247. /// <summary>
  248. /// <para>
  249. /// Sets the function that will be invoked for a <see cref="Command"/>.
  250. /// </para>
  251. /// <para>
  252. /// If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
  253. /// replace the old one.
  254. /// </para>
  255. /// </summary>
  256. /// <remarks>
  257. /// <para>
  258. /// This version of AddCommand is for commands that do not require a <see cref="CommandContext"/>.
  259. /// </para>
  260. /// </remarks>
  261. /// <param name="command">The command.</param>
  262. /// <param name="f">The function.</param>
  263. private static void AddCommand (Command command, Func<bool?> f) { CommandImplementations! [command] = ctx => f (); }
  264. /// <summary>
  265. /// Commands for Application.
  266. /// </summary>
  267. private static Dictionary<Command, View.CommandImplementation>? CommandImplementations { get; set; }
  268. }