AccessKeyHandler.cs 9.1 KB

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