AccessKeyHandler.cs 8.1 KB

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