AccessKeyHandler.cs 8.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260
  1. using System;
  2. using System.Collections.Generic;
  3. using System.Linq;
  4. using Avalonia.Interactivity;
  5. using Avalonia.VisualTree;
  6. namespace Avalonia.Input
  7. {
  8. /// <summary>
  9. /// Handles access keys for a window.
  10. /// </summary>
  11. public class AccessKeyHandler : IAccessKeyHandler
  12. {
  13. /// <summary>
  14. /// Defines the AccessKeyPressed attached event.
  15. /// </summary>
  16. public static readonly RoutedEvent<RoutedEventArgs> AccessKeyPressedEvent =
  17. RoutedEvent.Register<RoutedEventArgs>(
  18. "AccessKeyPressed",
  19. RoutingStrategies.Bubble,
  20. typeof(AccessKeyHandler));
  21. /// <summary>
  22. /// The registered access keys.
  23. /// </summary>
  24. private readonly List<Tuple<string, IInputElement>> _registered = new List<Tuple<string, IInputElement>>();
  25. /// <summary>
  26. /// The window to which the handler belongs.
  27. /// </summary>
  28. private IInputRoot? _owner;
  29. /// <summary>
  30. /// Whether access keys are currently being shown;
  31. /// </summary>
  32. private bool _showingAccessKeys;
  33. /// <summary>
  34. /// Whether to ignore the Alt KeyUp event.
  35. /// </summary>
  36. private bool _ignoreAltUp;
  37. /// <summary>
  38. /// Whether the AltKey is down.
  39. /// </summary>
  40. private bool _altIsDown;
  41. /// <summary>
  42. /// Element to restore following AltKey taking focus.
  43. /// </summary>
  44. private IInputElement? _restoreFocusElement;
  45. /// <summary>
  46. /// The window's main menu.
  47. /// </summary>
  48. private IMainMenu? _mainMenu;
  49. /// <summary>
  50. /// Gets or sets the window's main menu.
  51. /// </summary>
  52. public IMainMenu? MainMenu
  53. {
  54. get => _mainMenu;
  55. set
  56. {
  57. if (_mainMenu != null)
  58. {
  59. _mainMenu.MenuClosed -= MainMenuClosed;
  60. }
  61. _mainMenu = value;
  62. if (_mainMenu != null)
  63. {
  64. _mainMenu.MenuClosed += MainMenuClosed;
  65. }
  66. }
  67. }
  68. /// <summary>
  69. /// Sets the owner of the access key handler.
  70. /// </summary>
  71. /// <param name="owner">The owner.</param>
  72. /// <remarks>
  73. /// This method can only be called once, typically by the owner itself on creation.
  74. /// </remarks>
  75. public void SetOwner(IInputRoot owner)
  76. {
  77. if (_owner != null)
  78. {
  79. throw new InvalidOperationException("AccessKeyHandler owner has already been set.");
  80. }
  81. _owner = owner ?? throw new ArgumentNullException(nameof(owner));
  82. _owner.AddHandler(InputElement.KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel);
  83. _owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown, RoutingStrategies.Bubble);
  84. _owner.AddHandler(InputElement.KeyUpEvent, OnPreviewKeyUp, RoutingStrategies.Tunnel);
  85. _owner.AddHandler(InputElement.PointerPressedEvent, OnPreviewPointerPressed, RoutingStrategies.Tunnel);
  86. }
  87. /// <summary>
  88. /// Registers an input element to be associated with an access key.
  89. /// </summary>
  90. /// <param name="accessKey">The access key.</param>
  91. /// <param name="element">The input element.</param>
  92. public void Register(char accessKey, IInputElement element)
  93. {
  94. var existing = _registered.FirstOrDefault(x => x.Item2 == element);
  95. if (existing != null)
  96. {
  97. _registered.Remove(existing);
  98. }
  99. _registered.Add(Tuple.Create(accessKey.ToString().ToUpper(), element));
  100. }
  101. /// <summary>
  102. /// Unregisters the access keys associated with the input element.
  103. /// </summary>
  104. /// <param name="element">The input element.</param>
  105. public void Unregister(IInputElement element)
  106. {
  107. foreach (var i in _registered.Where(x => x.Item2 == element).ToList())
  108. {
  109. _registered.Remove(i);
  110. }
  111. }
  112. /// <summary>
  113. /// Called when a key is pressed in the owner window.
  114. /// </summary>
  115. /// <param name="sender">The event sender.</param>
  116. /// <param name="e">The event args.</param>
  117. protected virtual void OnPreviewKeyDown(object sender, KeyEventArgs e)
  118. {
  119. if (e.Key == Key.LeftAlt || e.Key == Key.RightAlt)
  120. {
  121. _altIsDown = true;
  122. if (MainMenu == null || !MainMenu.IsOpen)
  123. {
  124. // TODO: Use FocusScopes to store the current element and restore it when context menu is closed.
  125. // Save currently focused input element.
  126. _restoreFocusElement = FocusManager.Instance.Current;
  127. // When Alt is pressed without a main menu, or with a closed main menu, show
  128. // access key markers in the window (i.e. "_File").
  129. _owner!.ShowAccessKeys = _showingAccessKeys = true;
  130. }
  131. else
  132. {
  133. // If the Alt key is pressed and the main menu is open, close the main menu.
  134. CloseMenu();
  135. _ignoreAltUp = true;
  136. _restoreFocusElement?.Focus();
  137. _restoreFocusElement = null;
  138. }
  139. // We always handle the Alt key.
  140. e.Handled = true;
  141. }
  142. else if (_altIsDown)
  143. {
  144. _ignoreAltUp = true;
  145. }
  146. }
  147. /// <summary>
  148. /// Called when a key is pressed in the owner window.
  149. /// </summary>
  150. /// <param name="sender">The event sender.</param>
  151. /// <param name="e">The event args.</param>
  152. protected virtual void OnKeyDown(object sender, KeyEventArgs e)
  153. {
  154. bool menuIsOpen = MainMenu?.IsOpen == true;
  155. if (e.KeyModifiers.HasAllFlags(KeyModifiers.Alt) || menuIsOpen)
  156. {
  157. // If any other key is pressed with the Alt key held down, or the main menu is open,
  158. // find all controls who have registered that access key.
  159. var text = e.Key.ToString().ToUpper();
  160. var matches = _registered
  161. .Where(x => x.Item1 == text && x.Item2.IsEffectivelyVisible)
  162. .Select(x => x.Item2);
  163. // If the menu is open, only match controls in the menu's visual tree.
  164. if (menuIsOpen)
  165. {
  166. matches = matches.Where(x => MainMenu.IsVisualAncestorOf(x));
  167. }
  168. var match = matches.FirstOrDefault();
  169. // If there was a match, raise the AccessKeyPressed event on it.
  170. if (match != null)
  171. {
  172. match.RaiseEvent(new RoutedEventArgs(AccessKeyPressedEvent));
  173. e.Handled = true;
  174. }
  175. }
  176. }
  177. /// <summary>
  178. /// Handles the Alt/F10 keys being released in the window.
  179. /// </summary>
  180. /// <param name="sender">The event sender.</param>
  181. /// <param name="e">The event args.</param>
  182. protected virtual void OnPreviewKeyUp(object sender, KeyEventArgs e)
  183. {
  184. switch (e.Key)
  185. {
  186. case Key.LeftAlt:
  187. case Key.RightAlt:
  188. _altIsDown = false;
  189. if (_ignoreAltUp)
  190. {
  191. _ignoreAltUp = false;
  192. }
  193. else if (_showingAccessKeys && MainMenu != null)
  194. {
  195. MainMenu.Open();
  196. e.Handled = true;
  197. }
  198. break;
  199. }
  200. }
  201. /// <summary>
  202. /// Handles pointer presses in the window.
  203. /// </summary>
  204. /// <param name="sender">The event sender.</param>
  205. /// <param name="e">The event args.</param>
  206. protected virtual void OnPreviewPointerPressed(object sender, PointerEventArgs e)
  207. {
  208. if (_showingAccessKeys)
  209. {
  210. _owner!.ShowAccessKeys = false;
  211. }
  212. }
  213. /// <summary>
  214. /// Closes the <see cref="MainMenu"/> and performs other bookeeping.
  215. /// </summary>
  216. private void CloseMenu()
  217. {
  218. MainMenu!.Close();
  219. _owner!.ShowAccessKeys = _showingAccessKeys = false;
  220. }
  221. private void MainMenuClosed(object sender, EventArgs e)
  222. {
  223. _owner!.ShowAccessKeys = false;
  224. }
  225. }
  226. }