diff --git a/Perspex.Controls/MenuItem.cs b/Perspex.Controls/MenuItem.cs index 12572ae751..d9b9ef686f 100644 --- a/Perspex.Controls/MenuItem.cs +++ b/Perspex.Controls/MenuItem.cs @@ -89,6 +89,7 @@ namespace Perspex.Controls ClickEvent.AddClassHandler(x => x.OnClick); SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); IsSubMenuOpenProperty.Changed.AddClassHandler(x => x.SubMenuOpenChanged); + AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler(x => x.AccessKeyPressed); } /// @@ -419,6 +420,23 @@ namespace Perspex.Controls this.popup.Closed += this.PopupClosed; } + /// + /// Called when the menu item's access key is pressed. + /// + /// The event args. + private void AccessKeyPressed(RoutedEventArgs e) + { + if (this.HasSubMenu) + { + this.SelectedIndex = 0; + this.IsSubMenuOpen = true; + } + else + { + this.RaiseEvent(new RoutedEventArgs(ClickEvent)); + } + } + /// /// Closes all submenus of the menu item. /// diff --git a/Perspex.Controls/Primitives/AccessText.cs b/Perspex.Controls/Primitives/AccessText.cs index 4bc06853a4..6bcf6f6714 100644 --- a/Perspex.Controls/Primitives/AccessText.cs +++ b/Perspex.Controls/Primitives/AccessText.cs @@ -8,6 +8,10 @@ namespace Perspex.Controls.Primitives { using System; using Perspex.Media; + using Perspex.Input; + using Perspex.Rendering; + + /// /// A text block that displays a character prefixed with an underscore as an access key. @@ -20,6 +24,11 @@ namespace Perspex.Controls.Primitives public static readonly PerspexProperty ShowAccessKeyProperty = PerspexProperty.RegisterAttached("ShowAccessKey", inherits: true); + /// + /// The access key handler for the current window. + /// + private IAccessKeyHandler accessKeys; + /// /// Initializes static members of the class. /// @@ -104,6 +113,36 @@ namespace Perspex.Controls.Primitives return result.WithHeight(result.Height + 1); } + /// + /// Called when the control is attached to a visual tree. + /// + /// The root of the visual tree. + protected override void OnAttachedToVisualTree(IRenderRoot root) + { + base.OnAttachedToVisualTree(root); + this.accessKeys = (root as IInputRoot)?.AccessKeyHandler; + + if (this.accessKeys != null && this.AccessKey != 0) + { + this.accessKeys.Register(this.AccessKey, this); + } + } + + /// + /// Called when the control is detached from a visual tree. + /// + /// The root of the visual tree. + protected override void OnDetachedFromVisualTree(IRenderRoot root) + { + base.OnDetachedFromVisualTree(root); + + if (this.accessKeys != null && this.AccessKey != 0) + { + this.accessKeys.Unregister(this); + this.accessKeys = null; + } + } + /// /// Returns a string with the first underscore stripped. /// @@ -129,18 +168,24 @@ namespace Perspex.Controls.Primitives /// The new text. private void TextChanged(string text) { + var key = (char)0; + if (text != null) { int underscore = text.IndexOf('_'); if (underscore != -1 && underscore < text.Length - 1) { - this.AccessKey = text[underscore + 1]; - return; + key = text[underscore + 1]; } } - this.AccessKey = (char)0; + this.AccessKey = key; + + if (this.accessKeys != null && this.AccessKey != 0) + { + this.accessKeys.Register(this.AccessKey, this); + } } } } diff --git a/Perspex.Input/AccessKeyHandler.cs b/Perspex.Input/AccessKeyHandler.cs index 6697f3a101..55b47e1729 100644 --- a/Perspex.Input/AccessKeyHandler.cs +++ b/Perspex.Input/AccessKeyHandler.cs @@ -7,6 +7,8 @@ namespace Perspex.Input { using System; + using System.Collections.Generic; + using System.Linq; using Perspex.Interactivity; /// @@ -14,6 +16,20 @@ namespace Perspex.Input /// 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 List> registered = new List>(); + /// /// The window to which the handler belongs. /// @@ -53,17 +69,58 @@ namespace Perspex.Input } /// - /// Handles the Alt/F10 keys being pressed in the window. + /// 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 = this.registered.FirstOrDefault(x => x.Item2 == element); + + if (existing != null) + { + this.registered.Remove(existing); + } + + this.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 this.registered.Where(x => x.Item2 == element).ToList()) + { + this.registered.Remove(i); + } + } + + /// + /// Handles the Alt key being pressed in the window. /// /// The event sender. /// The event args. protected virtual void OnPreviewKeyDown(object sender, KeyEventArgs e) { - if (e.Key == Key.LeftAlt || e.Key == Key.F10) + if (e.Key == Key.LeftAlt) { this.owner.ShowAccessKeys = this.showingAccessKeys = true; e.Handled = true; } + else if ((KeyboardDevice.Instance.Modifiers & ModifierKeys.Alt) != 0) + { + var text = e.Text.ToUpper(); + var focus = this.registered + .Where(x => x.Item1 == text && x.Item2.IsEffectivelyVisible) + .FirstOrDefault()?.Item2; + + if (focus != null) + { + focus.RaiseEvent(new RoutedEventArgs(AccessKeyPressedEvent)); + } + } } /// @@ -73,13 +130,22 @@ namespace Perspex.Input /// The event args. protected virtual void OnPreviewKeyUp(object sender, KeyEventArgs e) { - if (e.Key == Key.LeftAlt || e.Key == Key.F10) + switch (e.Key) { - if (this.showingAccessKeys && this.MainMenu != null) - { + case Key.LeftAlt: + if (this.showingAccessKeys && this.MainMenu != null) + { + this.MainMenu.OpenMenu(); + e.Handled = true; + } + + break; + + case Key.F10: + this.owner.ShowAccessKeys = this.showingAccessKeys = true; this.MainMenu.OpenMenu(); e.Handled = true; - } + break; } } diff --git a/Perspex.Input/IAccessKeyHandler.cs b/Perspex.Input/IAccessKeyHandler.cs index 8f42b10c09..1d6bcc3c26 100644 --- a/Perspex.Input/IAccessKeyHandler.cs +++ b/Perspex.Input/IAccessKeyHandler.cs @@ -24,5 +24,18 @@ namespace Perspex.Input /// This method can only be called once, typically by the owner itself on creation. /// void SetOwner(IInputRoot owner); + + /// + /// Registers an input element to be associated with an access key. + /// + /// The access key. + /// The input element. + void Register(char accessKey, IInputElement element); + + /// + /// Unregisters the access keys associated with the input element. + /// + /// The input element. + void Unregister(IInputElement element); } } diff --git a/Perspex.Interactivity/RoutedEvent.cs b/Perspex.Interactivity/RoutedEvent.cs index 6441713280..a5e893c8b9 100644 --- a/Perspex.Interactivity/RoutedEvent.cs +++ b/Perspex.Interactivity/RoutedEvent.cs @@ -33,7 +33,6 @@ namespace Perspex.Interactivity Contract.Requires(eventArgsType != null); Contract.Requires(ownerType != null); Contract.Requires(typeof(RoutedEventArgs).GetTypeInfo().IsAssignableFrom(eventArgsType.GetTypeInfo())); - Contract.Requires(typeof(IInteractive).GetTypeInfo().IsAssignableFrom(ownerType.GetTypeInfo())); this.EventArgsType = eventArgsType; this.Name = name; @@ -68,7 +67,6 @@ namespace Perspex.Interactivity public static RoutedEvent Register( string name, RoutingStrategies routingStrategy) - where TOwner : IInteractive where TEventArgs : RoutedEventArgs { Contract.Requires(name != null); @@ -124,7 +122,6 @@ namespace Perspex.Interactivity { Contract.Requires(name != null); Contract.Requires(ownerType != null); - Contract.Requires(typeof(IInteractive).GetTypeInfo().IsAssignableFrom(ownerType.GetTypeInfo())); } public void AddClassHandler( diff --git a/Perspex.SceneGraph/IVisual.cs b/Perspex.SceneGraph/IVisual.cs index c7bbf551c9..e4594e5e00 100644 --- a/Perspex.SceneGraph/IVisual.cs +++ b/Perspex.SceneGraph/IVisual.cs @@ -32,6 +32,11 @@ namespace Perspex /// bool ClipToBounds { get; } + /// + /// Gets a value indicating whether this scene graph node and all its parents are visible. + /// + bool IsEffectivelyVisible { get; } + /// /// Gets a value indicating whether this scene graph node is visible. /// diff --git a/Perspex.SceneGraph/Perspex.SceneGraph.csproj b/Perspex.SceneGraph/Perspex.SceneGraph.csproj index 1e5c759905..064cbc5a20 100644 --- a/Perspex.SceneGraph/Perspex.SceneGraph.csproj +++ b/Perspex.SceneGraph/Perspex.SceneGraph.csproj @@ -50,6 +50,7 @@ + diff --git a/Perspex.SceneGraph/Visual.cs b/Perspex.SceneGraph/Visual.cs index fb8a307765..a3cfb0024c 100644 --- a/Perspex.SceneGraph/Visual.cs +++ b/Perspex.SceneGraph/Visual.cs @@ -132,6 +132,14 @@ namespace Perspex set { this.SetValue(ClipToBoundsProperty, value); } } + /// + /// Gets a value indicating whether this scene graph node and all its parents are visible. + /// + public bool IsEffectivelyVisible + { + get { return this.GetSelfAndVisualAncestors().All(x => x.IsVisible); } + } + /// /// Gets a value indicating whether this scene graph node is visible. ///