KeyboardImpl.cs 14 KB

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