// Copyright (c) The Avalonia Project. All rights reserved. // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; using System.Collections.Generic; using System.Linq; using Avalonia.Interactivity; using Avalonia.VisualTree; namespace Avalonia.Input { /// /// Handles access keys for a window. /// public class AccessKeyHandler : IAccessKeyHandler { /// /// Defines the AccessKeyPressed attached event. /// public static readonly RoutedEvent AccessKeyPressedEvent = RoutedEvent.Register( "AccessKeyPressed", RoutingStrategies.Bubble, typeof(AccessKeyHandler)); /// /// The registered access keys. /// private readonly List> _registered = new List>(); /// /// The window to which the handler belongs. /// private IInputRoot _owner; /// /// Whether access keys are currently being shown; /// private bool _showingAccessKeys; /// /// Whether to ignore the Alt KeyUp event. /// private bool _ignoreAltUp; /// /// Whether the AltKey is down. /// private bool _altIsDown; /// /// Element to restore folowing AltKey taking focus. /// private IInputElement _restoreFocusElement; /// /// Gets or sets the window's main menu. /// public IMainMenu MainMenu { get; set; } /// /// Sets the owner of the access key handler. /// /// The owner. /// /// This method can only be called once, typically by the owner itself on creation. /// public void SetOwner(IInputRoot owner) { Contract.Requires(owner != null); if (_owner != null) { throw new InvalidOperationException("AccessKeyHandler owner has already been set."); } _owner = owner; _owner.AddHandler(InputElement.KeyDownEvent, OnPreviewKeyDown, RoutingStrategies.Tunnel); _owner.AddHandler(InputElement.KeyDownEvent, OnKeyDown, RoutingStrategies.Bubble); _owner.AddHandler(InputElement.KeyUpEvent, OnPreviewKeyUp, RoutingStrategies.Tunnel); _owner.AddHandler(InputElement.PointerPressedEvent, OnPreviewPointerPressed, RoutingStrategies.Tunnel); } /// /// Registers an input element to be associated with an access key. /// /// The access key. /// The input element. public void Register(char accessKey, IInputElement element) { var existing = _registered.FirstOrDefault(x => x.Item2 == element); if (existing != null) { _registered.Remove(existing); } _registered.Add(Tuple.Create(accessKey.ToString().ToUpper(), element)); } /// /// Unregisters the access keys associated with the input element. /// /// The input element. public void Unregister(IInputElement element) { foreach (var i in _registered.Where(x => x.Item2 == element).ToList()) { _registered.Remove(i); } } /// /// Called when a key is pressed in the owner window. /// /// The event sender. /// The event args. protected virtual void OnPreviewKeyDown(object sender, KeyEventArgs e) { if (e.Key == Key.LeftAlt) { _altIsDown = true; if (MainMenu == null || !MainMenu.IsOpen) { // TODO: Use FocusScopes to store the current element and restore it when context menu is closed. // Save currently focused input element. _restoreFocusElement = FocusManager.Instance.Current; // When Alt is pressed without a main menu, or with a closed main menu, show // access key markers in the window (i.e. "_File"). _owner.ShowAccessKeys = _showingAccessKeys = true; } else { // If the Alt key is pressed and the main menu is open, close the main menu. CloseMenu(); _ignoreAltUp = true; _restoreFocusElement?.Focus(); _restoreFocusElement = null; } // We always handle the Alt key. e.Handled = true; } else if (_altIsDown) { _ignoreAltUp = true; } } /// /// Called when a key is pressed in the owner window. /// /// The event sender. /// The event args. protected virtual void OnKeyDown(object sender, KeyEventArgs e) { bool menuIsOpen = MainMenu?.IsOpen == true; if (e.Key == Key.Escape && menuIsOpen) { // When the Escape key is pressed with the main menu open, close it. CloseMenu(); e.Handled = true; } else if ((e.Modifiers & InputModifiers.Alt) != 0 || menuIsOpen) { // If any other key is pressed with the Alt key held down, or the main menu is open, // find all controls who have registered that access key. var text = e.Key.ToString().ToUpper(); var matches = _registered .Where(x => x.Item1 == text && x.Item2.IsEffectivelyVisible) .Select(x => x.Item2); // If the menu is open, only match controls in the menu's visual tree. if (menuIsOpen) { matches = matches.Where(x => MainMenu.IsVisualAncestorOf(x)); } var match = matches.FirstOrDefault(); // If there was a match, raise the AccessKeyPressed event on it. if (match != null) { match.RaiseEvent(new RoutedEventArgs(AccessKeyPressedEvent)); e.Handled = true; } } } /// /// Handles the Alt/F10 keys being released in the window. /// /// The event sender. /// The event args. protected virtual void OnPreviewKeyUp(object sender, KeyEventArgs e) { switch (e.Key) { case Key.LeftAlt: _altIsDown = false; if (_ignoreAltUp) { _ignoreAltUp = false; } else if (_showingAccessKeys && MainMenu != null) { MainMenu.Open(); e.Handled = true; } break; case Key.F10: _owner.ShowAccessKeys = _showingAccessKeys = true; MainMenu.Open(); e.Handled = true; break; } } /// /// Handles pointer presses in the window. /// /// The event sender. /// The event args. protected virtual void OnPreviewPointerPressed(object sender, PointerEventArgs e) { if (_showingAccessKeys) { _owner.ShowAccessKeys = false; } } /// /// Closes the and performs other bookeeping. /// private void CloseMenu() { MainMenu.Close(); _owner.ShowAccessKeys = _showingAccessKeys = false; } } }