KeyboardNavigationHandler.cs 8.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220
  1. using System;
  2. using System.Diagnostics.CodeAnalysis;
  3. using Avalonia.Input.Navigation;
  4. using Avalonia.Input;
  5. using Avalonia.Metadata;
  6. using Avalonia.VisualTree;
  7. namespace Avalonia.Input
  8. {
  9. /// <summary>
  10. /// Handles keyboard navigation for a window.
  11. /// </summary>
  12. [Unstable]
  13. public sealed class KeyboardNavigationHandler : IKeyboardNavigationHandler
  14. {
  15. /// <summary>
  16. /// The window to which the handler belongs.
  17. /// </summary>
  18. private IInputRoot? _owner;
  19. /// <summary>
  20. /// Sets the owner of the keyboard navigation handler.
  21. /// </summary>
  22. /// <param name="owner">The owner.</param>
  23. /// <remarks>
  24. /// This method can only be called once, typically by the owner itself on creation.
  25. /// </remarks>
  26. [PrivateApi]
  27. public void SetOwner(IInputRoot owner)
  28. {
  29. if (_owner != null)
  30. {
  31. throw new InvalidOperationException($"{nameof(KeyboardNavigationHandler)} owner has already been set.");
  32. }
  33. _owner = owner ?? throw new ArgumentNullException(nameof(owner));
  34. _owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown);
  35. }
  36. /// <summary>
  37. /// Gets the next control in the specified navigation direction.
  38. /// </summary>
  39. /// <param name="element">The element.</param>
  40. /// <param name="direction">The navigation direction.</param>
  41. /// <returns>
  42. /// The next element in the specified direction, or null if <paramref name="element"/>
  43. /// was the last in the requested direction.
  44. /// </returns>
  45. public static IInputElement? GetNext(
  46. IInputElement element,
  47. NavigationDirection direction)
  48. {
  49. element = element ?? throw new ArgumentNullException(nameof(element));
  50. return GetNextPrivate(element, null, direction, null);
  51. }
  52. private static IInputElement? GetNextPrivate(
  53. IInputElement? element,
  54. IInputRoot? owner,
  55. NavigationDirection direction,
  56. KeyDeviceType? keyDeviceType)
  57. {
  58. var elementOrOwner = element ?? owner ?? throw new ArgumentNullException(nameof(owner));
  59. // If there's a custom keyboard navigation handler as an ancestor, use that.
  60. var custom = (element as Visual)?.FindAncestorOfType<ICustomKeyboardNavigation>(true);
  61. if (custom is not null && HandlePreCustomNavigation(custom, elementOrOwner, direction, out var ce))
  62. return ce;
  63. IInputElement? result;
  64. if (direction is NavigationDirection.Next)
  65. {
  66. result = TabNavigation.GetNextTab(elementOrOwner, false);
  67. }
  68. else if (direction is NavigationDirection.Previous)
  69. {
  70. result = TabNavigation.GetPrevTab(elementOrOwner, null, false);
  71. }
  72. else if (direction is NavigationDirection.Up or NavigationDirection.Down
  73. or NavigationDirection.Left or NavigationDirection.Right)
  74. {
  75. // HACK: a window should always have some element focused,
  76. // it seems to be a difference between UWP and Avalonia focus manager implementations.
  77. result = element is null
  78. ? TabNavigation.GetNextTab(elementOrOwner, true)
  79. : XYFocus.TryDirectionalFocus(direction, element, owner, null, keyDeviceType);
  80. }
  81. else
  82. {
  83. throw new ArgumentOutOfRangeException(nameof(direction), direction, null);
  84. }
  85. // If there wasn't a custom navigation handler as an ancestor of the current element,
  86. // but there is one as an ancestor of the new element, use that.
  87. if (custom is null && HandlePostCustomNavigation(elementOrOwner, result, direction, out ce))
  88. return ce;
  89. return result;
  90. }
  91. /// <summary>
  92. /// Moves the focus in the specified direction.
  93. /// </summary>
  94. /// <param name="element">The current element.</param>
  95. /// <param name="direction">The direction to move.</param>
  96. /// <param name="keyModifiers">Any key modifiers active at the time of focus.</param>
  97. public void Move(
  98. IInputElement? element,
  99. NavigationDirection direction,
  100. KeyModifiers keyModifiers = KeyModifiers.None)
  101. {
  102. MovePrivate(element, direction, keyModifiers, null);
  103. }
  104. // TODO12: remove MovePrivate, and make Move return boolean. Or even remove whole KeyboardNavigationHandler.
  105. private bool MovePrivate(IInputElement? element, NavigationDirection direction, KeyModifiers keyModifiers, KeyDeviceType? deviceType)
  106. {
  107. var next = GetNextPrivate(element, _owner, direction, deviceType);
  108. if (next != null)
  109. {
  110. var method = direction == NavigationDirection.Next ||
  111. direction == NavigationDirection.Previous ?
  112. NavigationMethod.Tab : NavigationMethod.Directional;
  113. return next.Focus(method, keyModifiers);
  114. }
  115. return false;
  116. }
  117. /// <summary>
  118. /// Handles the Tab key being pressed in the window.
  119. /// </summary>
  120. /// <param name="sender">The event sender.</param>
  121. /// <param name="e">The event args.</param>
  122. void OnKeyDown(object? sender, KeyEventArgs e)
  123. {
  124. if (e.Key == Key.Tab)
  125. {
  126. var current = FocusManager.GetFocusManager(e.Source as IInputElement)?.GetFocusedElement();
  127. var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ?
  128. NavigationDirection.Next : NavigationDirection.Previous;
  129. e.Handled = MovePrivate(current, direction, e.KeyModifiers, e.KeyDeviceType);
  130. }
  131. else if (e.Key is Key.Left or Key.Right or Key.Up or Key.Down)
  132. {
  133. var current = FocusManager.GetFocusManager(e.Source as IInputElement)?.GetFocusedElement();
  134. var direction = e.Key switch
  135. {
  136. Key.Left => NavigationDirection.Left,
  137. Key.Right => NavigationDirection.Right,
  138. Key.Up => NavigationDirection.Up,
  139. Key.Down => NavigationDirection.Down,
  140. _ => throw new ArgumentOutOfRangeException()
  141. };
  142. e.Handled = MovePrivate(current, direction, e.KeyModifiers, e.KeyDeviceType);
  143. }
  144. }
  145. private static bool HandlePreCustomNavigation(
  146. ICustomKeyboardNavigation customHandler,
  147. IInputElement element,
  148. NavigationDirection direction,
  149. [NotNullWhen(true)] out IInputElement? result)
  150. {
  151. var (handled, next) = customHandler.GetNext(element, direction);
  152. if (handled)
  153. {
  154. if (next is not null)
  155. {
  156. result = next;
  157. return true;
  158. }
  159. var r = direction switch
  160. {
  161. NavigationDirection.Next => TabNavigation.GetNextTabOutside(customHandler),
  162. NavigationDirection.Previous => TabNavigation.GetPrevTabOutside(customHandler),
  163. _ => null
  164. };
  165. if (r is not null)
  166. {
  167. result = r;
  168. return true;
  169. }
  170. }
  171. result = null;
  172. return false;
  173. }
  174. private static bool HandlePostCustomNavigation(
  175. IInputElement element,
  176. IInputElement? newElement,
  177. NavigationDirection direction,
  178. [NotNullWhen(true)] out IInputElement? result)
  179. {
  180. if (newElement is Visual v)
  181. {
  182. var customHandler = v.FindAncestorOfType<ICustomKeyboardNavigation>(true);
  183. if (customHandler is object)
  184. {
  185. var (handled, next) = customHandler.GetNext(element, direction);
  186. if (handled && next is object)
  187. {
  188. result = next;
  189. return true;
  190. }
  191. }
  192. }
  193. result = null;
  194. return false;
  195. }
  196. }
  197. }