2
0

Application.Keyboard.cs 11 KB

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