KeyboardImpl.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385
  1. #nullable disable
  2. #nullable enable
  3. using System.Diagnostics;
  4. namespace Terminal.Gui.App;
  5. /// <summary>
  6. /// INTERNAL: Implements <see cref="IKeyboard"/> to manage keyboard input and key bindings at the Application level.
  7. /// <para>
  8. /// This implementation decouples keyboard handling state from the static <see cref="Application"/> class,
  9. /// enabling parallelizable unit tests and better testability.
  10. /// </para>
  11. /// <para>
  12. /// See <see cref="IKeyboard"/> for usage details.
  13. /// </para>
  14. /// </summary>
  15. internal class KeyboardImpl : IKeyboard
  16. {
  17. private Key _quitKey = Key.Esc; // Resources/config.json overrides
  18. private Key _arrangeKey = Key.F5.WithCtrl; // Resources/config.json overrides
  19. private Key _nextTabGroupKey = Key.F6; // Resources/config.json overrides
  20. private Key _nextTabKey = Key.Tab; // Resources/config.json overrides
  21. private Key _prevTabGroupKey = Key.F6.WithShift; // Resources/config.json overrides
  22. private Key _prevTabKey = Key.Tab.WithShift; // Resources/config.json overrides
  23. /// <summary>
  24. /// Commands for Application.
  25. /// </summary>
  26. private readonly Dictionary<Command, View.CommandImplementation> _commandImplementations = new ();
  27. /// <inheritdoc/>
  28. public IApplication? Application { get; set; }
  29. /// <inheritdoc/>
  30. public KeyBindings KeyBindings { get; internal set; } = new (null);
  31. /// <inheritdoc/>
  32. public Key QuitKey
  33. {
  34. get => _quitKey;
  35. set
  36. {
  37. KeyBindings.Replace (_quitKey, value);
  38. _quitKey = value;
  39. }
  40. }
  41. /// <inheritdoc/>
  42. public Key ArrangeKey
  43. {
  44. get => _arrangeKey;
  45. set
  46. {
  47. KeyBindings.Replace (_arrangeKey, value);
  48. _arrangeKey = value;
  49. }
  50. }
  51. /// <inheritdoc/>
  52. public Key NextTabGroupKey
  53. {
  54. get => _nextTabGroupKey;
  55. set
  56. {
  57. KeyBindings.Replace (_nextTabGroupKey, value);
  58. _nextTabGroupKey = value;
  59. }
  60. }
  61. /// <inheritdoc/>
  62. public Key NextTabKey
  63. {
  64. get => _nextTabKey;
  65. set
  66. {
  67. KeyBindings.Replace (_nextTabKey, value);
  68. _nextTabKey = value;
  69. }
  70. }
  71. /// <inheritdoc/>
  72. public Key PrevTabGroupKey
  73. {
  74. get => _prevTabGroupKey;
  75. set
  76. {
  77. KeyBindings.Replace (_prevTabGroupKey, value);
  78. _prevTabGroupKey = value;
  79. }
  80. }
  81. /// <inheritdoc/>
  82. public Key PrevTabKey
  83. {
  84. get => _prevTabKey;
  85. set
  86. {
  87. KeyBindings.Replace (_prevTabKey, value);
  88. _prevTabKey = value;
  89. }
  90. }
  91. /// <inheritdoc/>
  92. public event EventHandler<Key>? KeyDown;
  93. /// <inheritdoc/>
  94. public event EventHandler<Key>? KeyUp;
  95. /// <summary>
  96. /// Initializes keyboard bindings.
  97. /// </summary>
  98. public KeyboardImpl ()
  99. {
  100. AddKeyBindings ();
  101. }
  102. /// <inheritdoc/>
  103. public bool RaiseKeyDownEvent (Key key)
  104. {
  105. //ebug.Assert (App.Application.MainThreadId == Thread.CurrentThread.ManagedThreadId);
  106. //Logging.Debug ($"{key}");
  107. // TODO: Add a way to ignore certain keys, esp for debugging.
  108. //#if DEBUG
  109. // if (key == Key.Empty.WithAlt || key == Key.Empty.WithCtrl)
  110. // {
  111. // Logging.Debug ($"Ignoring {key}");
  112. // return false;
  113. // }
  114. //#endif
  115. // TODO: This should match standard event patterns
  116. KeyDown?.Invoke (null, key);
  117. if (key.Handled)
  118. {
  119. return true;
  120. }
  121. if (Application?.Popover?.DispatchKeyDown (key) is true)
  122. {
  123. return true;
  124. }
  125. if (Application?.Current is null)
  126. {
  127. if (Application?.SessionStack is { })
  128. {
  129. foreach (Toplevel topLevel in Application.SessionStack.ToList ())
  130. {
  131. if (topLevel.NewKeyDownEvent (key))
  132. {
  133. return true;
  134. }
  135. if (topLevel.Modal)
  136. {
  137. break;
  138. }
  139. }
  140. }
  141. }
  142. else
  143. {
  144. if (Application.Current.NewKeyDownEvent (key))
  145. {
  146. return true;
  147. }
  148. }
  149. bool? commandHandled = InvokeCommandsBoundToKey (key);
  150. if(commandHandled is true)
  151. {
  152. return true;
  153. }
  154. return false;
  155. }
  156. /// <inheritdoc/>
  157. public bool RaiseKeyUpEvent (Key key)
  158. {
  159. if (Application?.Initialized != true)
  160. {
  161. return true;
  162. }
  163. KeyUp?.Invoke (null, key);
  164. if (key.Handled)
  165. {
  166. return true;
  167. }
  168. // TODO: Add Popover support
  169. if (Application?.SessionStack is { })
  170. {
  171. foreach (Toplevel topLevel in Application.SessionStack.ToList ())
  172. {
  173. if (topLevel.NewKeyUpEvent (key))
  174. {
  175. return true;
  176. }
  177. if (topLevel.Modal)
  178. {
  179. break;
  180. }
  181. }
  182. }
  183. return false;
  184. }
  185. /// <inheritdoc/>
  186. public bool? InvokeCommandsBoundToKey (Key key)
  187. {
  188. bool? handled = null;
  189. // Invoke any Application-scoped KeyBindings.
  190. // The first view that handles the key will stop the loop.
  191. // foreach (KeyValuePair<Key, KeyBinding> binding in KeyBindings.GetBindings (key))
  192. if (KeyBindings.TryGet (key, out KeyBinding binding))
  193. {
  194. if (binding.Target is { })
  195. {
  196. if (!binding.Target.Enabled)
  197. {
  198. return null;
  199. }
  200. handled = binding.Target?.InvokeCommands (binding.Commands, binding);
  201. }
  202. else
  203. {
  204. bool? toReturn = null;
  205. foreach (Command command in binding.Commands)
  206. {
  207. toReturn = InvokeCommand (command, key, binding);
  208. }
  209. handled = toReturn ?? true;
  210. }
  211. }
  212. return handled;
  213. }
  214. /// <inheritdoc/>
  215. public bool? InvokeCommand (Command command, Key key, KeyBinding binding)
  216. {
  217. if (!_commandImplementations.ContainsKey (command))
  218. {
  219. throw new NotSupportedException (
  220. @$"A KeyBinding was set up for the command {command} ({key}) but that command is not supported by Application."
  221. );
  222. }
  223. if (_commandImplementations.TryGetValue (command, out View.CommandImplementation? implementation))
  224. {
  225. CommandContext<KeyBinding> context = new (command, null, binding); // Create the context here
  226. return implementation (context);
  227. }
  228. return null;
  229. }
  230. /// <summary>
  231. /// <para>
  232. /// Sets the function that will be invoked for a <see cref="Command"/>.
  233. /// </para>
  234. /// <para>
  235. /// If AddCommand has already been called for <paramref name="command"/> <paramref name="f"/> will
  236. /// replace the old one.
  237. /// </para>
  238. /// </summary>
  239. /// <remarks>
  240. /// <para>
  241. /// This version of AddCommand is for commands that do not require a <see cref="ICommandContext"/>.
  242. /// </para>
  243. /// </remarks>
  244. /// <param name="command">The command.</param>
  245. /// <param name="f">The function.</param>
  246. private void AddCommand (Command command, Func<bool?> f) { _commandImplementations [command] = ctx => f (); }
  247. internal void AddKeyBindings ()
  248. {
  249. _commandImplementations.Clear ();
  250. // Things Application knows how to do
  251. AddCommand (
  252. Command.Quit,
  253. () =>
  254. {
  255. Application?.RequestStop ();
  256. return true;
  257. }
  258. );
  259. AddCommand (
  260. Command.Suspend,
  261. () =>
  262. {
  263. Application?.Driver?.Suspend ();
  264. return true;
  265. }
  266. );
  267. AddCommand (
  268. Command.NextTabStop,
  269. () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabStop));
  270. AddCommand (
  271. Command.PreviousTabStop,
  272. () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabStop));
  273. AddCommand (
  274. Command.NextTabGroup,
  275. () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Forward, TabBehavior.TabGroup));
  276. AddCommand (
  277. Command.PreviousTabGroup,
  278. () => Application?.Navigation?.AdvanceFocus (NavigationDirection.Backward, TabBehavior.TabGroup));
  279. AddCommand (
  280. Command.Refresh,
  281. () =>
  282. {
  283. Application?.LayoutAndDraw (true);
  284. return true;
  285. }
  286. );
  287. AddCommand (
  288. Command.Arrange,
  289. () =>
  290. {
  291. View? viewToArrange = Application?.Navigation?.GetFocused ();
  292. // Go up the superview hierarchy and find the first that is not ViewArrangement.Fixed
  293. while (viewToArrange is { SuperView: { }, Arrangement: ViewArrangement.Fixed })
  294. {
  295. viewToArrange = viewToArrange.SuperView;
  296. }
  297. if (viewToArrange is { })
  298. {
  299. return viewToArrange.Border?.EnterArrangeMode (ViewArrangement.Fixed);
  300. }
  301. return false;
  302. });
  303. //SetKeysToHardCodedDefaults ();
  304. // Need to clear after setting the above to ensure actually clear
  305. // because set_QuitKey etc.. may call Add
  306. KeyBindings.Clear ();
  307. KeyBindings.Add (QuitKey, Command.Quit);
  308. KeyBindings.Add (NextTabKey, Command.NextTabStop);
  309. KeyBindings.Add (PrevTabKey, Command.PreviousTabStop);
  310. KeyBindings.Add (NextTabGroupKey, Command.NextTabGroup);
  311. KeyBindings.Add (PrevTabGroupKey, Command.PreviousTabGroup);
  312. KeyBindings.Add (ArrangeKey, Command.Arrange);
  313. KeyBindings.Add (Key.CursorRight, Command.NextTabStop);
  314. KeyBindings.Add (Key.CursorDown, Command.NextTabStop);
  315. KeyBindings.Add (Key.CursorLeft, Command.PreviousTabStop);
  316. KeyBindings.Add (Key.CursorUp, Command.PreviousTabStop);
  317. // TODO: Refresh Key should be configurable
  318. KeyBindings.Add (Key.F5, Command.Refresh);
  319. // TODO: Suspend Key should be configurable
  320. if (Environment.OSVersion.Platform == PlatformID.Unix)
  321. {
  322. KeyBindings.Add (Key.Z.WithCtrl, Command.Suspend);
  323. }
  324. }
  325. }