From 80064e87127ef188504f6c9a7f4f4b1a58562b53 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 20 Jun 2015 20:17:53 +0200 Subject: [PATCH] More work on Menu. --- Perspex.Controls/Menu.cs | 22 ++--- Perspex.Controls/MenuItem.cs | 58 ------------- Perspex.Controls/MenuItemAccessKeyHandler.cs | 19 +++-- Perspex.Input/AccessKeyHandler.cs | 82 ++++++++++++++++--- Perspex.Input/IMainMenu.cs | 14 +++- .../VisualTree/VisualExtensions.cs | 5 ++ 6 files changed, 110 insertions(+), 90 deletions(-) diff --git a/Perspex.Controls/Menu.cs b/Perspex.Controls/Menu.cs index e4a8c6bf10..924e8b4cec 100644 --- a/Perspex.Controls/Menu.cs +++ b/Perspex.Controls/Menu.cs @@ -72,7 +72,7 @@ namespace Perspex.Controls /// /// Closes the menu. /// - public void CloseMenu() + public void Close() { foreach (MenuItem i in this.GetLogicalChildren()) { @@ -86,10 +86,11 @@ namespace Perspex.Controls /// /// Opens the menu in response to the Alt/F10 key. /// - public void OpenMenu() + public void Open() { this.SelectedIndex = 0; - ((IInputElement)this.SelectedItem)?.Focus(); + this.SelectedMenuItem.Focus(); + this.IsOpen = true; } /// @@ -138,7 +139,7 @@ namespace Perspex.Controls protected override void OnLostFocus(RoutedEventArgs e) { base.OnLostFocus(e); - //this.CloseMenu(); + //this.Close(); } /// @@ -151,12 +152,7 @@ namespace Perspex.Controls base.OnKeyDown(e); - if (this.IsOpen && e.Key == Key.Escape) - { - this.CloseMenu(); - e.Handled = true; - } - else if (menuWasOpen) + if (menuWasOpen) { // If a menu item was open and we navigate to a new one with the arrow keys, open // that menu and select the first item. @@ -199,7 +195,7 @@ namespace Perspex.Controls /// The event args. private void Deactivated(object sender, EventArgs e) { - this.CloseMenu(); + this.Close(); } /// @@ -208,7 +204,7 @@ namespace Perspex.Controls /// The event args. private void OnMenuClick(RoutedEventArgs e) { - this.CloseMenu(); + this.Close(); } /// @@ -224,7 +220,7 @@ namespace Perspex.Controls if (!this.IsLogicalParentOf(control)) { - this.CloseMenu(); + this.Close(); } } } diff --git a/Perspex.Controls/MenuItem.cs b/Perspex.Controls/MenuItem.cs index b242197c02..f4869b698b 100644 --- a/Perspex.Controls/MenuItem.cs +++ b/Perspex.Controls/MenuItem.cs @@ -277,10 +277,6 @@ namespace Perspex.Controls } break; - - default: - e.Handled = this.HandleAccessKey(e.Text); - break; } if (!passStraightToParent) @@ -289,60 +285,6 @@ namespace Perspex.Controls } } - private bool HandleAccessKey(string text) - { - text = text.ToUpper(); - - if (!this.IsSubMenuOpen) - { - var match = this.GetVisualDescendents() - .OfType() - .FirstOrDefault(x => x.AccessKey.ToString().ToUpper() == text); - - if (match != null) - { - if (this.HasSubMenu) - { - this.IsSubMenuOpen = true; - } - else - { - this.RaiseEvent(new RoutedEventArgs(ClickEvent)); - } - - return true; - } - } - else - { - var match = this.popup.PopupRoot.GetVisualDescendents() - .OfType() - .FirstOrDefault(x => x.AccessKey.ToString().ToUpper() == text) - ?.GetVisualAncestors() - .OfType() - .FirstOrDefault(); - - if (match != null) - { - var item = (MenuItem)this.ItemContainerGenerator.GetItemForContainer(match); - - if (item.HasSubMenu) - { - item.SelectedIndex = 0; - item.IsSubMenuOpen = true; - } - else - { - item.RaiseEvent(new RoutedEventArgs(ClickEvent)); - } - - return true; - } - } - - return false; - } - /// /// Called when the pointer enters the . /// diff --git a/Perspex.Controls/MenuItemAccessKeyHandler.cs b/Perspex.Controls/MenuItemAccessKeyHandler.cs index b55d074390..f4bdec0164 100644 --- a/Perspex.Controls/MenuItemAccessKeyHandler.cs +++ b/Perspex.Controls/MenuItemAccessKeyHandler.cs @@ -92,14 +92,19 @@ namespace Perspex.Controls /// The event args. protected virtual void OnKeyDown(object sender, KeyEventArgs e) { - var text = e.Text.ToUpper(); - var focus = this.registered - .Where(x => x.Item1 == text && x.Item2.IsEffectivelyVisible) - .FirstOrDefault()?.Item2; - - if (focus != null) + if (!string.IsNullOrWhiteSpace(e.Text)) { - focus.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent)); + 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(AccessKeyHandler.AccessKeyPressedEvent)); + } + + e.Handled = true; } } } diff --git a/Perspex.Input/AccessKeyHandler.cs b/Perspex.Input/AccessKeyHandler.cs index 55b47e1729..cf2251ef45 100644 --- a/Perspex.Input/AccessKeyHandler.cs +++ b/Perspex.Input/AccessKeyHandler.cs @@ -10,6 +10,7 @@ namespace Perspex.Input using System.Collections.Generic; using System.Linq; using Perspex.Interactivity; + using Perspex.VisualTree; /// /// Handles access keys for a window. @@ -40,6 +41,11 @@ namespace Perspex.Input /// private bool showingAccessKeys; + /// + /// Whether to ignore the Alt KeyUp event. + /// + private bool ignoreAltUp; + /// /// Gets or sets the window's main menu. /// @@ -64,6 +70,7 @@ namespace Perspex.Input this.owner = owner; this.owner.AddHandler(InputElement.KeyDownEvent, this.OnPreviewKeyDown, RoutingStrategies.Tunnel); + this.owner.AddHandler(InputElement.KeyDownEvent, this.OnKeyDown, RoutingStrategies.Bubble); this.owner.AddHandler(InputElement.KeyUpEvent, this.OnPreviewKeyUp, RoutingStrategies.Tunnel); this.owner.AddHandler(InputElement.PointerPressedEvent, this.OnPreviewPointerPressed, RoutingStrategies.Tunnel); } @@ -98,7 +105,7 @@ namespace Perspex.Input } /// - /// Handles the Alt key being pressed in the window. + /// Called when a key is pressed in the owner window. /// /// The event sender. /// The event args. @@ -106,19 +113,61 @@ namespace Perspex.Input { if (e.Key == Key.LeftAlt) { - this.owner.ShowAccessKeys = this.showingAccessKeys = true; + if (this.MainMenu == null || !this.MainMenu.IsOpen) + { + // When Alt is pressed without a main menu, or with a closed main menu, show + // access key markers in the window (i.e. "_File"). + this.owner.ShowAccessKeys = this.showingAccessKeys = true; + } + else + { + // If the Alt key is pressed and the main menu is open, close the main menu. + this.CloseMenu(); + this.ignoreAltUp = true; + } + + // We always handle the Alt key. + e.Handled = 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 = this.MainMenu?.IsOpen == true; + + if (e.Key == Key.Escape && menuIsOpen) + { + // When the Escape key is pressed with the main menu open, close it. + this.CloseMenu(); e.Handled = true; } - else if ((KeyboardDevice.Instance.Modifiers & ModifierKeys.Alt) != 0) + else if ((KeyboardDevice.Instance.Modifiers & ModifierKeys.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.Text.ToUpper(); - var focus = this.registered + var matches = this.registered .Where(x => x.Item1 == text && x.Item2.IsEffectivelyVisible) - .FirstOrDefault()?.Item2; + .Select(x => x.Item2); + + // If the menu is open, only match controls in the menu's visual tree. + if (menuIsOpen) + { + matches = matches.Where(x => this.MainMenu.IsVisualParentOf(x)); + } + + var match = matches.FirstOrDefault(); - if (focus != null) + // If there was a match, raise the AccessKeyPressed event on it. + if (match != null) { - focus.RaiseEvent(new RoutedEventArgs(AccessKeyPressedEvent)); + match.RaiseEvent(new RoutedEventArgs(AccessKeyPressedEvent)); + e.Handled = true; } } } @@ -133,9 +182,13 @@ namespace Perspex.Input switch (e.Key) { case Key.LeftAlt: - if (this.showingAccessKeys && this.MainMenu != null) + if (this.ignoreAltUp) + { + this.ignoreAltUp = false; + } + else if (this.showingAccessKeys && this.MainMenu != null) { - this.MainMenu.OpenMenu(); + this.MainMenu.Open(); e.Handled = true; } @@ -143,7 +196,7 @@ namespace Perspex.Input case Key.F10: this.owner.ShowAccessKeys = this.showingAccessKeys = true; - this.MainMenu.OpenMenu(); + this.MainMenu.Open(); e.Handled = true; break; } @@ -161,5 +214,14 @@ namespace Perspex.Input this.owner.ShowAccessKeys = false; } } + + /// + /// Closes the and performs other bookeeping. + /// + private void CloseMenu() + { + this.MainMenu.Close(); + this.owner.ShowAccessKeys = this.showingAccessKeys = false; + } } } diff --git a/Perspex.Input/IMainMenu.cs b/Perspex.Input/IMainMenu.cs index 2ebea79223..08aa640719 100644 --- a/Perspex.Input/IMainMenu.cs +++ b/Perspex.Input/IMainMenu.cs @@ -9,11 +9,21 @@ namespace Perspex.Input /// /// Defines the interface for a window's main menu. /// - public interface IMainMenu + public interface IMainMenu : IVisual { + /// + /// Gets a value indicating whether the menu is open. + /// + bool IsOpen { get; } + + /// + /// Closes the menu. + /// + void Close(); + /// /// Opens the menu in response to the Alt/F10 key. /// - void OpenMenu(); + void Open(); } } diff --git a/Perspex.SceneGraph/VisualTree/VisualExtensions.cs b/Perspex.SceneGraph/VisualTree/VisualExtensions.cs index 754734a4fd..3385c2c20f 100644 --- a/Perspex.SceneGraph/VisualTree/VisualExtensions.cs +++ b/Perspex.SceneGraph/VisualTree/VisualExtensions.cs @@ -117,5 +117,10 @@ namespace Perspex.VisualTree return visual; } + + public static bool IsVisualParentOf(this IVisual visual, IVisual target) + { + return target.GetVisualAncestors().Any(x => x == visual); + } } }