ViewKeyboard.cs 32 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879
  1. using System.ComponentModel;
  2. namespace Terminal.Gui;
  3. public partial class View
  4. {
  5. /// <summary>
  6. /// Helper to configure all things keyboard related for a View. Called from the View constructor.
  7. /// </summary>
  8. private void SetupKeyboard ()
  9. {
  10. KeyBindings = new (this);
  11. HotKeySpecifier = (Rune)'_';
  12. TitleTextFormatter.HotKeyChanged += TitleTextFormatter_HotKeyChanged;
  13. // By default, the HotKey command sets the focus
  14. AddCommand (Command.HotKey, OnHotKey);
  15. // By default, the Accept command raises the Accept event
  16. AddCommand (Command.Accept, OnAccept);
  17. }
  18. /// <summary>
  19. /// Helper to dispose all things keyboard related for a View. Called from the View Dispose method.
  20. /// </summary>
  21. private void DisposeKeyboard ()
  22. {
  23. TitleTextFormatter.HotKeyChanged -= TitleTextFormatter_HotKeyChanged;
  24. KeyBindings.Clear ();
  25. }
  26. #region HotKey Support
  27. /// <summary>
  28. /// Called when the HotKey command (<see cref="Command.HotKey"/>) is invoked. Causes this view to be focused.
  29. /// </summary>
  30. /// <returns>If <see langword="true"/> the command was canceled.</returns>
  31. private bool? OnHotKey ()
  32. {
  33. if (CanFocus)
  34. {
  35. SetFocus ();
  36. return true;
  37. }
  38. return false;
  39. }
  40. /// <summary>Invoked when the <see cref="HotKey"/> is changed.</summary>
  41. public event EventHandler<KeyChangedEventArgs> HotKeyChanged;
  42. private Key _hotKey = new ();
  43. private void TitleTextFormatter_HotKeyChanged (object sender, KeyChangedEventArgs e) { HotKeyChanged?.Invoke (this, e); }
  44. /// <summary>
  45. /// Gets or sets the hot key defined for this view. Pressing the hot key on the keyboard while this view has focus will
  46. /// invoke the <see cref="Command.HotKey"/> and <see cref="Command.Accept"/> commands. <see cref="Command.HotKey"/>
  47. /// causes the view to be focused and <see cref="Command.Accept"/> does nothing. By default, the HotKey is
  48. /// automatically set to the first character of <see cref="Text"/> that is prefixed with with
  49. /// <see cref="HotKeySpecifier"/>.
  50. /// <para>
  51. /// A HotKey is a keypress that selects a visible UI item. For selecting items across <see cref="View"/>`s (e.g.a
  52. /// <see cref="Button"/> in a <see cref="Dialog"/>) the keypress must include the <see cref="Key.WithAlt"/>
  53. /// modifier. For selecting items within a View that are not Views themselves, the keypress can be key without the
  54. /// Alt modifier. For example, in a Dialog, a Button with the text of "_Text" can be selected with Alt-T. Or, in a
  55. /// <see cref="Menu"/> with "_File _Edit", Alt-F will select (show) the "_File" menu. If the "_File" menu has a
  56. /// sub-menu of "_New" `Alt-N` or `N` will ONLY select the "_New" sub-menu if the "_File" menu is already opened.
  57. /// </para>
  58. /// </summary>
  59. /// <remarks>
  60. /// <para>See <see href="../docs/keyboard.md"/> for an overview of Terminal.Gui keyboard APIs.</para>
  61. /// <para>
  62. /// This is a helper API for configuring a key binding for the hot key. By default, this property is set whenever
  63. /// <see cref="Text"/> changes.
  64. /// </para>
  65. /// <para>
  66. /// By default, when the Hot Key is set, key bindings are added for both the base key (e.g.
  67. /// <see cref="Key.D3"/>) and the Alt-shifted key (e.g. <see cref="Key.D3"/>.
  68. /// <see cref="Key.WithAlt"/>). This behavior can be overriden by overriding
  69. /// <see cref="AddKeyBindingsForHotKey"/>.
  70. /// </para>
  71. /// <para>
  72. /// By default, when the HotKey is set to <see cref="Key.A"/> through <see cref="Key.Z"/> key bindings will
  73. /// be added for both the un-shifted and shifted versions. This means if the HotKey is <see cref="Key.A"/>, key
  74. /// bindings for <c>Key.A</c> and <c>Key.A.WithShift</c> will be added. This behavior can be overriden by
  75. /// overriding <see cref="AddKeyBindingsForHotKey"/>.
  76. /// </para>
  77. /// <para>If the hot key is changed, the <see cref="HotKeyChanged"/> event is fired.</para>
  78. /// <para>Set to <see cref="Key.Empty"/> to disable the hot key.</para>
  79. /// </remarks>
  80. public virtual Key HotKey
  81. {
  82. get => _hotKey;
  83. set
  84. {
  85. if (value is null)
  86. {
  87. throw new ArgumentException (
  88. @"HotKey must not be null. Use Key.Empty to clear the HotKey.",
  89. nameof (value)
  90. );
  91. }
  92. if (AddKeyBindingsForHotKey (_hotKey, value))
  93. {
  94. // This will cause TextFormatter_HotKeyChanged to be called, firing HotKeyChanged
  95. // BUGBUG: _hotkey should be set BEFORE setting TextFormatter.HotKey
  96. _hotKey = value;
  97. TitleTextFormatter.HotKey = value;
  98. }
  99. }
  100. }
  101. /// <summary>
  102. /// Adds key bindings for the specified HotKey. Useful for views that contain multiple items that each have their
  103. /// own HotKey such as <see cref="RadioGroup"/>.
  104. /// </summary>
  105. /// <remarks>
  106. /// <para>
  107. /// By default, key bindings are added for both the base key (e.g. <see cref="Key.D3"/>) and the Alt-shifted key
  108. /// (e.g. <c>Key.D3.WithAlt</c>) This behavior can be overriden by overriding <see cref="AddKeyBindingsForHotKey"/>.
  109. /// </para>
  110. /// <para>
  111. /// By default, when <paramref name="hotKey"/> is <see cref="Key.A"/> through <see cref="Key.Z"/> key bindings
  112. /// will be added for both the un-shifted and shifted versions. This means if the HotKey is <see cref="Key.A"/>,
  113. /// key bindings for <c>Key.A</c> and <c>Key.A.WithShift</c> will be added. This behavior can be overriden by
  114. /// overriding <see cref="AddKeyBindingsForHotKey"/>.
  115. /// </para>
  116. /// </remarks>
  117. /// <param name="prevHotKey">The HotKey <paramref name="hotKey"/> is replacing. Key bindings for this key will be removed.</param>
  118. /// <param name="hotKey">The new HotKey. If <see cref="Key.Empty"/> <paramref name="prevHotKey"/> bindings will be removed.</param>
  119. /// <returns><see langword="true"/> if the HotKey bindings were added.</returns>
  120. /// <exception cref="ArgumentException"></exception>
  121. public virtual bool AddKeyBindingsForHotKey (Key prevHotKey, Key hotKey)
  122. {
  123. if (_hotKey == hotKey)
  124. {
  125. return false;
  126. }
  127. Key newKey = hotKey;
  128. Key baseKey = newKey.NoAlt.NoShift.NoCtrl;
  129. if (newKey != Key.Empty && (baseKey == Key.Space || Rune.IsControl (baseKey.AsRune)))
  130. {
  131. throw new ArgumentException (@$"HotKey must be a printable (and non-space) key ({hotKey}).");
  132. }
  133. if (newKey != baseKey)
  134. {
  135. if (newKey.IsCtrl)
  136. {
  137. throw new ArgumentException (@$"HotKey does not support CtrlMask ({hotKey}).");
  138. }
  139. // Strip off the shift mask if it's A...Z
  140. if (baseKey.IsKeyCodeAtoZ)
  141. {
  142. newKey = newKey.NoShift;
  143. }
  144. // Strip off the Alt mask
  145. newKey = newKey.NoAlt;
  146. }
  147. // Remove base version
  148. if (KeyBindings.TryGet (prevHotKey, out _))
  149. {
  150. KeyBindings.Remove (prevHotKey);
  151. }
  152. // Remove the Alt version
  153. if (KeyBindings.TryGet (prevHotKey.WithAlt, out _))
  154. {
  155. KeyBindings.Remove (prevHotKey.WithAlt);
  156. }
  157. if (_hotKey.IsKeyCodeAtoZ)
  158. {
  159. // Remove the shift version
  160. if (KeyBindings.TryGet (prevHotKey.WithShift, out _))
  161. {
  162. KeyBindings.Remove (prevHotKey.WithShift);
  163. }
  164. // Remove alt | shift version
  165. if (KeyBindings.TryGet (prevHotKey.WithShift.WithAlt, out _))
  166. {
  167. KeyBindings.Remove (prevHotKey.WithShift.WithAlt);
  168. }
  169. }
  170. // Add the new
  171. if (newKey != Key.Empty)
  172. {
  173. // Add the base and Alt key
  174. KeyBindings.Add (newKey, KeyBindingScope.HotKey, Command.HotKey);
  175. KeyBindings.Add (newKey.WithAlt, KeyBindingScope.HotKey, Command.HotKey);
  176. // If the Key is A..Z, add ShiftMask and AltMask | ShiftMask
  177. if (newKey.IsKeyCodeAtoZ)
  178. {
  179. KeyBindings.Add (newKey.WithShift, KeyBindingScope.HotKey, Command.HotKey);
  180. KeyBindings.Add (newKey.WithShift.WithAlt, KeyBindingScope.HotKey, Command.HotKey);
  181. }
  182. }
  183. return true;
  184. }
  185. /// <summary>
  186. /// Gets or sets the specifier character for the hot key (e.g. '_'). Set to '\xffff' to disable automatic hot key
  187. /// setting support for this View instance. The default is '\xffff'.
  188. /// </summary>
  189. public virtual Rune HotKeySpecifier
  190. {
  191. get
  192. {
  193. return TitleTextFormatter.HotKeySpecifier;
  194. }
  195. set
  196. {
  197. TitleTextFormatter.HotKeySpecifier = TextFormatter.HotKeySpecifier = value;
  198. SetHotKeyFromTitle ();
  199. }
  200. }
  201. private void SetHotKeyFromTitle ()
  202. {
  203. if (TitleTextFormatter == null || HotKeySpecifier == new Rune ('\xFFFF'))
  204. {
  205. return; // throw new InvalidOperationException ("Can't set HotKey unless a TextFormatter has been created");
  206. }
  207. if (TextFormatter.FindHotKey (_title, HotKeySpecifier, out _, out Key hk))
  208. {
  209. if (_hotKey != hk)
  210. {
  211. HotKey = hk;
  212. }
  213. }
  214. else
  215. {
  216. HotKey = Key.Empty;
  217. }
  218. }
  219. #endregion HotKey Support
  220. #region Tab/Focus Handling
  221. // This is null, and allocated on demand.
  222. private List<View> _tabIndexes;
  223. /// <summary>Gets a list of the subviews that are <see cref="TabStop"/>s.</summary>
  224. /// <value>The tabIndexes.</value>
  225. public IList<View> TabIndexes => _tabIndexes?.AsReadOnly () ?? _empty;
  226. private int _tabIndex = -1;
  227. private int _oldTabIndex;
  228. /// <summary>
  229. /// Indicates the index of the current <see cref="View"/> from the <see cref="TabIndexes"/> list. See also:
  230. /// <seealso cref="TabStop"/>.
  231. /// </summary>
  232. public int TabIndex
  233. {
  234. get => _tabIndex;
  235. set
  236. {
  237. if (!CanFocus)
  238. {
  239. _tabIndex = -1;
  240. return;
  241. }
  242. if (SuperView?._tabIndexes is null || SuperView?._tabIndexes.Count == 1)
  243. {
  244. _tabIndex = 0;
  245. return;
  246. }
  247. if (_tabIndex == value && TabIndexes.IndexOf (this) == value)
  248. {
  249. return;
  250. }
  251. _tabIndex = value > SuperView._tabIndexes.Count - 1 ? SuperView._tabIndexes.Count - 1 :
  252. value < 0 ? 0 : value;
  253. _tabIndex = GetTabIndex (_tabIndex);
  254. if (SuperView._tabIndexes.IndexOf (this) != _tabIndex)
  255. {
  256. SuperView._tabIndexes.Remove (this);
  257. SuperView._tabIndexes.Insert (_tabIndex, this);
  258. SetTabIndex ();
  259. }
  260. }
  261. }
  262. private int GetTabIndex (int idx)
  263. {
  264. var i = 0;
  265. foreach (View v in SuperView._tabIndexes)
  266. {
  267. if (v._tabIndex == -1 || v == this)
  268. {
  269. continue;
  270. }
  271. i++;
  272. }
  273. return Math.Min (i, idx);
  274. }
  275. private void SetTabIndex ()
  276. {
  277. var i = 0;
  278. foreach (View v in SuperView._tabIndexes)
  279. {
  280. if (v._tabIndex == -1)
  281. {
  282. continue;
  283. }
  284. v._tabIndex = i;
  285. i++;
  286. }
  287. }
  288. private bool _tabStop = true;
  289. /// <summary>
  290. /// Gets or sets whether the view is a stop-point for keyboard navigation of focus. Will be <see langword="true"/>
  291. /// only if the <see cref="CanFocus"/> is also <see langword="true"/>. Set to <see langword="false"/> to prevent the
  292. /// view from being a stop-point for keyboard navigation.
  293. /// </summary>
  294. /// <remarks>
  295. /// The default keyboard navigation keys are <c>Key.Tab</c> and <c>Key>Tab.WithShift</c>. These can be changed by
  296. /// modifying the key bindings (see <see cref="KeyBindings.Add(Key, Command[])"/>) of the SuperView.
  297. /// </remarks>
  298. public bool TabStop
  299. {
  300. get => _tabStop;
  301. set
  302. {
  303. if (_tabStop == value)
  304. {
  305. return;
  306. }
  307. _tabStop = CanFocus && value;
  308. }
  309. }
  310. #endregion Tab/Focus Handling
  311. #region Low-level Key handling
  312. #region Key Down Event
  313. /// <summary>
  314. /// If the view is enabled, processes a new key down event and returns <see langword="true"/> if the event was
  315. /// handled.
  316. /// </summary>
  317. /// <remarks>
  318. /// <para>
  319. /// If the view has a sub view that is focused, <see cref="NewKeyDownEvent"/> will be called on the focused view
  320. /// first.
  321. /// </para>
  322. /// <para>
  323. /// If the focused sub view does not handle the key press, this method calls <see cref="OnKeyDown"/> to allow the
  324. /// view to pre-process the key press. If <see cref="OnKeyDown"/> returns <see langword="false"/>, this method then
  325. /// calls <see cref="OnInvokingKeyBindings"/> to invoke any key bindings. Then, only if no key bindings are
  326. /// handled, <see cref="OnProcessKeyDown"/> will be called allowing the view to process the key press.
  327. /// </para>
  328. /// <para>See <see href="../docs/keyboard.md">for an overview of Terminal.Gui keyboard APIs.</see></para>
  329. /// </remarks>
  330. /// <param name="keyEvent"></param>
  331. /// <returns><see langword="true"/> if the event was handled.</returns>
  332. public bool NewKeyDownEvent (Key keyEvent)
  333. {
  334. if (!Enabled)
  335. {
  336. return false;
  337. }
  338. // By default the KeyBindingScope is View
  339. if (Focused?.NewKeyDownEvent (keyEvent) == true)
  340. {
  341. return true;
  342. }
  343. // Before (fire the cancellable event)
  344. if (OnKeyDown (keyEvent))
  345. {
  346. return true;
  347. }
  348. // During (this is what can be cancelled)
  349. InvokingKeyBindings?.Invoke (this, keyEvent);
  350. if (keyEvent.Handled)
  351. {
  352. return true;
  353. }
  354. bool? handled = OnInvokingKeyBindings (keyEvent);
  355. if (handled is { } && (bool)handled)
  356. {
  357. return true;
  358. }
  359. // TODO: The below is not right. OnXXX handlers are supposed to fire the events.
  360. // TODO: But I've moved it outside of the v-function to test something.
  361. // After (fire the cancellable event)
  362. // fire event
  363. ProcessKeyDown?.Invoke (this, keyEvent);
  364. if (!keyEvent.Handled && OnProcessKeyDown (keyEvent))
  365. {
  366. return true;
  367. }
  368. return keyEvent.Handled;
  369. }
  370. /// <summary>
  371. /// Low-level API called when the user presses a key, allowing a view to pre-process the key down event. This is
  372. /// called from <see cref="NewKeyDownEvent"/> before <see cref="OnInvokingKeyBindings"/>.
  373. /// </summary>
  374. /// <param name="keyEvent">Contains the details about the key that produced the event.</param>
  375. /// <returns>
  376. /// <see langword="false"/> if the key press was not handled. <see langword="true"/> if the keypress was handled
  377. /// and no other view should see it.
  378. /// </returns>
  379. /// <remarks>
  380. /// <para>
  381. /// For processing <see cref="HotKey"/>s and commands, use <see cref="Command"/> and
  382. /// <see cref="KeyBindings.Add(Key, Command[])"/>instead.
  383. /// </para>
  384. /// <para>Fires the <see cref="KeyDown"/> event.</para>
  385. /// </remarks>
  386. public virtual bool OnKeyDown (Key keyEvent)
  387. {
  388. // fire event
  389. KeyDown?.Invoke (this, keyEvent);
  390. return keyEvent.Handled;
  391. }
  392. /// <summary>
  393. /// Invoked when the user presses a key, allowing subscribers to pre-process the key down event. This is fired
  394. /// from <see cref="OnKeyDown"/> before <see cref="OnInvokingKeyBindings"/>. Set <see cref="Key.Handled"/> to true to
  395. /// stop the key from being processed by other views.
  396. /// </summary>
  397. /// <remarks>
  398. /// <para>
  399. /// Not all terminals support key distinct up notifications, Applications should avoid depending on distinct
  400. /// KeyUp events.
  401. /// </para>
  402. /// <para>See <see href="../docs/keyboard.md">for an overview of Terminal.Gui keyboard APIs.</see></para>
  403. /// </remarks>
  404. public event EventHandler<Key> KeyDown;
  405. /// <summary>
  406. /// Low-level API called when the user presses a key, allowing views do things during key down events. This is
  407. /// called from <see cref="NewKeyDownEvent"/> after <see cref="OnInvokingKeyBindings"/>.
  408. /// </summary>
  409. /// <param name="keyEvent">Contains the details about the key that produced the event.</param>
  410. /// <returns>
  411. /// <see langword="false"/> if the key press was not handled. <see langword="true"/> if the keypress was handled
  412. /// and no other view should see it.
  413. /// </returns>
  414. /// <remarks>
  415. /// <para>
  416. /// Override <see cref="OnProcessKeyDown"/> to override the behavior of how the base class processes key down
  417. /// events.
  418. /// </para>
  419. /// <para>
  420. /// For processing <see cref="HotKey"/>s and commands, use <see cref="Command"/> and
  421. /// <see cref="KeyBindings.Add(Key, Command[])"/>instead.
  422. /// </para>
  423. /// <para>Fires the <see cref="ProcessKeyDown"/> event.</para>
  424. /// <para>
  425. /// Not all terminals support distinct key up notifications; applications should avoid depending on distinct
  426. /// KeyUp events.
  427. /// </para>
  428. /// </remarks>
  429. public virtual bool OnProcessKeyDown (Key keyEvent)
  430. {
  431. //ProcessKeyDown?.Invoke (this, keyEvent);
  432. return keyEvent.Handled;
  433. }
  434. /// <summary>
  435. /// Invoked when the user presses a key, allowing subscribers to do things during key down events. Set
  436. /// <see cref="Key.Handled"/> to true to stop the key from being processed by other views. Invoked after
  437. /// <see cref="KeyDown"/> and before <see cref="InvokingKeyBindings"/>.
  438. /// </summary>
  439. /// <remarks>
  440. /// <para>
  441. /// SubViews can use the <see cref="ProcessKeyDown"/> of their super view override the default behavior of when
  442. /// key bindings are invoked.
  443. /// </para>
  444. /// <para>
  445. /// Not all terminals support distinct key up notifications; applications should avoid depending on distinct
  446. /// KeyUp events.
  447. /// </para>
  448. /// <para>See <see href="../docs/keyboard.md">for an overview of Terminal.Gui keyboard APIs.</see></para>
  449. /// </remarks>
  450. public event EventHandler<Key> ProcessKeyDown;
  451. #endregion KeyDown Event
  452. #region KeyUp Event
  453. /// <summary>
  454. /// If the view is enabled, processes a new key up event and returns <see langword="true"/> if the event was
  455. /// handled. Called before <see cref="NewKeyDownEvent"/>.
  456. /// </summary>
  457. /// <remarks>
  458. /// <para>
  459. /// Not all terminals support key distinct down/up notifications, Applications should avoid depending on distinct
  460. /// KeyUp events.
  461. /// </para>
  462. /// <para>
  463. /// If the view has a sub view that is focused, <see cref="NewKeyUpEvent"/> will be called on the focused view
  464. /// first.
  465. /// </para>
  466. /// <para>
  467. /// If the focused sub view does not handle the key press, this method calls <see cref="OnKeyUp"/>, which is
  468. /// cancellable.
  469. /// </para>
  470. /// <para>See <see href="../docs/keyboard.md">for an overview of Terminal.Gui keyboard APIs.</see></para>
  471. /// </remarks>
  472. /// <param name="keyEvent"></param>
  473. /// <returns><see langword="true"/> if the event was handled.</returns>
  474. public bool NewKeyUpEvent (Key keyEvent)
  475. {
  476. if (!Enabled)
  477. {
  478. return false;
  479. }
  480. if (Focused?.NewKeyUpEvent (keyEvent) == true)
  481. {
  482. return true;
  483. }
  484. // Before (fire the cancellable event)
  485. if (OnKeyUp (keyEvent))
  486. {
  487. return true;
  488. }
  489. // During (this is what can be cancelled)
  490. // TODO: Until there's a clear use-case, we will not define 'during' event (e.g. OnDuringKeyUp).
  491. // After (fire the cancellable event InvokingKeyBindings)
  492. // TODO: Until there's a clear use-case, we will not define an 'after' event (e.g. OnAfterKeyUp).
  493. return false;
  494. }
  495. /// <summary>Method invoked when a key is released. This method is called from <see cref="NewKeyUpEvent"/>.</summary>
  496. /// <param name="keyEvent">Contains the details about the key that produced the event.</param>
  497. /// <returns>
  498. /// <see langword="false"/> if the key stroke was not handled. <see langword="true"/> if no other view should see
  499. /// it.
  500. /// </returns>
  501. /// <remarks>
  502. /// Not all terminals support key distinct down/up notifications, Applications should avoid depending on distinct KeyUp
  503. /// events.
  504. /// <para>
  505. /// Overrides must call into the base and return <see langword="true"/> if the base returns
  506. /// <see langword="true"/>.
  507. /// </para>
  508. /// <para>See <see href="../docs/keyboard.md">for an overview of Terminal.Gui keyboard APIs.</see></para>
  509. /// </remarks>
  510. public virtual bool OnKeyUp (Key keyEvent)
  511. {
  512. // fire event
  513. KeyUp?.Invoke (this, keyEvent);
  514. if (keyEvent.Handled)
  515. {
  516. return true;
  517. }
  518. return false;
  519. }
  520. /// <summary>
  521. /// Invoked when a key is released. Set <see cref="Key.Handled"/> to true to stop the key up event from being processed
  522. /// by other views.
  523. /// <remarks>
  524. /// Not all terminals support key distinct down/up notifications, Applications should avoid depending on
  525. /// distinct KeyDown and KeyUp events and instead should use <see cref="KeyDown"/>.
  526. /// <para>See <see href="../docs/keyboard.md">for an overview of Terminal.Gui keyboard APIs.</see></para>
  527. /// </remarks>
  528. /// </summary>
  529. public event EventHandler<Key> KeyUp;
  530. #endregion KeyUp Event
  531. #endregion Low-level Key handling
  532. #region Key Bindings
  533. /// <summary>Gets the key bindings for this view.</summary>
  534. public KeyBindings KeyBindings { get; internal set; }
  535. private Dictionary<Command, Func<bool?>> CommandImplementations { get; } = new ();
  536. /// <summary>
  537. /// Low-level API called when a user presses a key; invokes any key bindings set on the view. This is called
  538. /// during <see cref="NewKeyDownEvent"/> after <see cref="OnKeyDown"/> has returned.
  539. /// </summary>
  540. /// <remarks>
  541. /// <para>Fires the <see cref="InvokingKeyBindings"/> event.</para>
  542. /// <para>See <see href="../docs/keyboard.md">for an overview of Terminal.Gui keyboard APIs.</see></para>
  543. /// </remarks>
  544. /// <param name="keyEvent">Contains the details about the key that produced the event.</param>
  545. /// <returns>
  546. /// <see langword="false"/> if the key press was not handled. <see langword="true"/> if the keypress was handled
  547. /// and no other view should see it.
  548. /// </returns>
  549. public virtual bool? OnInvokingKeyBindings (Key keyEvent)
  550. {
  551. // fire event only if there's an app or hotkey binding for the key
  552. if (KeyBindings.TryGet (keyEvent, KeyBindingScope.Application | KeyBindingScope.HotKey, out KeyBinding _))
  553. {
  554. InvokingKeyBindings?.Invoke (this, keyEvent);
  555. if (keyEvent.Handled)
  556. {
  557. return true;
  558. }
  559. }
  560. // * If no key binding was found, `InvokeKeyBindings` returns `null`.
  561. // Continue passing the event (return `false` from `OnInvokeKeyBindings`).
  562. // * If key bindings were found, but none handled the key (all `Command`s returned `false`),
  563. // `InvokeKeyBindings` returns `false`. Continue passing the event (return `false` from `OnInvokeKeyBindings`)..
  564. // * If key bindings were found, and any handled the key (at least one `Command` returned `true`),
  565. // `InvokeKeyBindings` returns `true`. Continue passing the event (return `false` from `OnInvokeKeyBindings`).
  566. bool? handled = InvokeKeyBindings (keyEvent);
  567. if (handled is { } && (bool)handled)
  568. {
  569. // Stop processing if any key binding handled the key.
  570. // DO NOT stop processing if there are no matching key bindings or none of the key bindings handled the key
  571. return true;
  572. }
  573. if (Margin is {} && ProcessAdornmentKeyBindings (Margin, keyEvent, ref handled))
  574. {
  575. return true;
  576. }
  577. if (Padding is {} && ProcessAdornmentKeyBindings (Padding, keyEvent, ref handled))
  578. {
  579. return true;
  580. }
  581. if (Border is {} && ProcessAdornmentKeyBindings (Border, keyEvent, ref handled))
  582. {
  583. return true;
  584. }
  585. if (ProcessSubViewKeyBindings (keyEvent, ref handled))
  586. {
  587. return true;
  588. }
  589. return handled;
  590. }
  591. private bool ProcessAdornmentKeyBindings (Adornment adornment, Key keyEvent, ref bool? handled)
  592. {
  593. foreach (View subview in adornment?.Subviews)
  594. {
  595. handled = subview.OnInvokingKeyBindings (keyEvent);
  596. if (handled is { } && (bool)handled)
  597. {
  598. return true;
  599. }
  600. }
  601. return false;
  602. }
  603. private bool ProcessSubViewKeyBindings (Key keyEvent, ref bool? handled)
  604. {
  605. // Now, process any key bindings in the subviews that are tagged to KeyBindingScope.HotKey.
  606. foreach (View subview in Subviews)
  607. {
  608. if (subview.KeyBindings.TryGet (keyEvent, KeyBindingScope.HotKey, out KeyBinding binding))
  609. {
  610. //keyEvent.Scope = KeyBindingScope.HotKey;
  611. handled = subview.OnInvokingKeyBindings (keyEvent);
  612. if (handled is { } && (bool)handled)
  613. {
  614. return true;
  615. }
  616. }
  617. }
  618. return false;
  619. }
  620. // Function to search the subview hierarchy for the first view that has a KeyBindingScope.Application binding for the key.
  621. // Called from Application.OnKeyDown
  622. // TODO: Unwind recursion
  623. // QUESTION: Should this return a list of views? As is, it will only return the first view that has the binding.
  624. internal static View FindViewWithApplicationKeyBinding (View start, Key keyEvent)
  625. {
  626. if (start.KeyBindings.TryGet (keyEvent, KeyBindingScope.Application, out KeyBinding binding))
  627. {
  628. return start;
  629. }
  630. foreach (View subview in start.Subviews)
  631. {
  632. View found = FindViewWithApplicationKeyBinding (subview, keyEvent);
  633. if (found is { })
  634. {
  635. return found;
  636. }
  637. }
  638. return null;
  639. }
  640. /// <summary>
  641. /// Invoked when a key is pressed that may be mapped to a key binding. Set <see cref="Key.Handled"/> to true to
  642. /// stop the key from being processed by other views.
  643. /// </summary>
  644. public event EventHandler<Key> InvokingKeyBindings;
  645. /// <summary>
  646. /// Invokes any binding that is registered on this <see cref="View"/> and matches the <paramref name="key"/>
  647. /// <para>See <see href="../docs/keyboard.md">for an overview of Terminal.Gui keyboard APIs.</see></para>
  648. /// </summary>
  649. /// <param name="key">The key event passed.</param>
  650. /// <returns>
  651. /// <see langword="null"/> if no command was bound the <paramref name="key"/>. <see langword="true"/> if
  652. /// commands were invoked and at least one handled the command. <see langword="false"/> if commands were invoked and at
  653. /// none handled the command.
  654. /// </returns>
  655. protected bool? InvokeKeyBindings (Key key)
  656. {
  657. bool? toReturn = null;
  658. if (!KeyBindings.TryGet (key, out KeyBinding binding))
  659. {
  660. return null;
  661. }
  662. foreach (Command command in binding.Commands)
  663. {
  664. if (!CommandImplementations.ContainsKey (command))
  665. {
  666. throw new NotSupportedException (
  667. @$"A KeyBinding was set up for the command {command} ({key}) but that command is not supported by this View ({GetType ().Name})"
  668. );
  669. }
  670. // each command has its own return value
  671. bool? thisReturn = InvokeCommand (command);
  672. // if we haven't got anything yet, the current command result should be used
  673. toReturn ??= thisReturn;
  674. // if ever see a true then that's what we will return
  675. if (thisReturn ?? false)
  676. {
  677. toReturn = true;
  678. }
  679. }
  680. return toReturn;
  681. }
  682. /// <summary>
  683. /// Invokes the specified commands.
  684. /// </summary>
  685. /// <param name="commands"></param>
  686. /// <returns>
  687. /// <see langword="null"/> if no command was found.
  688. /// <see langword="true"/> if the command was invoked and it handled the command.
  689. /// <see langword="false"/> if the command was invoked and it did not handle the command.
  690. /// </returns>
  691. public bool? InvokeCommands (Command [] commands)
  692. {
  693. bool? toReturn = null;
  694. foreach (Command command in commands)
  695. {
  696. if (!CommandImplementations.ContainsKey (command))
  697. {
  698. throw new NotSupportedException (@$"{command} is not supported by ({GetType ().Name}).");
  699. }
  700. // each command has its own return value
  701. bool? thisReturn = InvokeCommand (command);
  702. // if we haven't got anything yet, the current command result should be used
  703. toReturn ??= thisReturn;
  704. // if ever see a true then that's what we will return
  705. if (thisReturn ?? false)
  706. {
  707. toReturn = true;
  708. }
  709. }
  710. return toReturn;
  711. }
  712. /// <summary>Invokes the specified command.</summary>
  713. /// <param name="command"></param>
  714. /// <returns>
  715. /// <see langword="null"/> if no command was found. <see langword="true"/> if the command was invoked and it
  716. /// handled the command. <see langword="false"/> if the command was invoked and it did not handle the command.
  717. /// </returns>
  718. public bool? InvokeCommand (Command command)
  719. {
  720. if (!CommandImplementations.ContainsKey (command))
  721. {
  722. return null;
  723. }
  724. return CommandImplementations [command] ();
  725. }
  726. /// <summary>
  727. /// <para>
  728. /// Sets the function that will be invoked for a <see cref="Command"/>. Views should call
  729. /// <see cref="AddCommand"/> for each command they support.
  730. /// </para>
  731. /// <para>
  732. /// If <see cref="AddCommand"/> has already been called for <paramref name="command"/> <paramref name="f"/> will
  733. /// replace the old one.
  734. /// </para>
  735. /// </summary>
  736. /// <param name="command">The command.</param>
  737. /// <param name="f">The function.</param>
  738. protected void AddCommand (Command command, Func<bool?> f)
  739. {
  740. // if there is already an implementation of this command
  741. // replace that implementation
  742. // else record how to perform the action (this should be the normal case)
  743. if (CommandImplementations is { })
  744. {
  745. CommandImplementations [command] = f;
  746. }
  747. }
  748. /// <summary>Returns all commands that are supported by this <see cref="View"/>.</summary>
  749. /// <returns></returns>
  750. public IEnumerable<Command> GetSupportedCommands () { return CommandImplementations.Keys; }
  751. // TODO: Add GetKeysBoundToCommand() - given a Command, return all Keys that would invoke it
  752. #endregion Key Bindings
  753. }