// ----------------------------------------------------------------------- // // Copyright 2015 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- namespace Perspex.Controls { using System; using System.Linq; using System.Windows.Input; using Perspex.Controls.Primitives; using Perspex.Input; using Perspex.Interactivity; using Perspex.Rendering; using Perspex.Controls.Templates; using Perspex.Controls.Presenters; using Perspex.VisualTree; /// /// A menu item control. /// public class MenuItem : SelectingItemsControl, ISelectable { /// /// Defines the property. /// public static readonly PerspexProperty CommandProperty = Button.CommandProperty.AddOwner(); /// /// Defines the property. /// public static readonly PerspexProperty CommandParameterProperty = Button.CommandParameterProperty.AddOwner(); /// /// Defines the property. /// public static readonly PerspexProperty HeaderProperty = HeaderedItemsControl.HeaderProperty.AddOwner(); /// /// Defines the property. /// public static readonly PerspexProperty IconProperty = PerspexProperty.Register(nameof(Icon)); /// /// Defines the property. /// public static readonly PerspexProperty IsSelectedProperty = ListBoxItem.IsSelectedProperty.AddOwner(); /// /// Defines the property. /// public static readonly PerspexProperty IsSubMenuOpenProperty = PerspexProperty.Register(nameof(IsSubMenuOpen)); /// /// Defines the event. /// public static readonly RoutedEvent ClickEvent = RoutedEvent.Register(nameof(Click), RoutingStrategies.Bubble); /// /// Defines the event. /// public static readonly RoutedEvent SubmenuOpenedEvent = RoutedEvent.Register(nameof(SubmenuOpened), RoutingStrategies.Bubble); /// /// The timer used to display submenus. /// private IDisposable submenuTimer; /// /// Initializes static members of the class. /// static MenuItem() { FocusableProperty.OverrideDefaultValue(true); ClickEvent.AddClassHandler(x => x.OnClick); SubmenuOpenedEvent.AddClassHandler(x => x.OnSubmenuOpened); IsSubMenuOpenProperty.Changed.AddClassHandler(x => x.SubMenuOpenChanged); } /// /// Occurs when a without a submenu is clicked. /// public event EventHandler Click { add { this.AddHandler(ClickEvent, value); } remove { this.RemoveHandler(ClickEvent, value); } } /// /// Occurs when a 's submenu is opened. /// public event EventHandler SubmenuOpened { add { this.AddHandler(SubmenuOpenedEvent, value); } remove { this.RemoveHandler(SubmenuOpenedEvent, value); } } /// /// Gets or sets the command associated with the menu item. /// public ICommand Command { get { return this.GetValue(CommandProperty); } set { this.SetValue(CommandProperty, value); } } /// /// Gets or sets the parameter to pass to the property of a /// . /// public object CommandParameter { get { return this.GetValue(CommandParameterProperty); } set { this.SetValue(CommandParameterProperty, value); } } /// /// Gets or sets the 's header. /// public object Header { get { return this.GetValue(HeaderProperty); } set { this.SetValue(HeaderProperty, value); } } /// /// Gets or sets the icon that appears in a . /// public object Icon { get { return this.GetValue(IconProperty); } set { this.SetValue(IconProperty, value); } } /// /// Gets or sets a value indicating whether the is currently selected. /// public bool IsSelected { get { return this.GetValue(IsSelectedProperty); } set { this.SetValue(IsSelectedProperty, value); } } /// /// Gets or sets a value that indicates whether the submenu of the is /// open. /// public bool IsSubMenuOpen { get { return this.GetValue(IsSubMenuOpenProperty); } set { this.SetValue(IsSubMenuOpenProperty, value); } } /// /// Gets or sets a value that indicates whether the has a submenu. /// public bool HasSubMenu { get { return !this.Classes.Contains(":empty"); } } /// /// Gets a value that indicates whether the is a top-level menu item. /// public bool IsTopLevel { get { return this.Parent is Menu; } } /// /// Called when the is clicked. /// /// The click event args. protected virtual void OnClick(RoutedEventArgs e) { if (this.Command != null) { this.Command.Execute(this.CommandParameter); } } /// /// Called when the recieves focus. /// /// The event args. protected override void OnGotFocus(GotFocusEventArgs e) { base.OnGotFocus(e); this.IsSelected = true; } /// /// Called when a key is pressed in the . /// /// The event args. protected override void OnKeyDown(KeyEventArgs e) { // Some keypresses we want to pass straight to the parent MenuItem/Menu without giving // this MenuItem the chance to handle them. This is usually e.g. when the submenu is // closed so passing them to the base would try to move the selection in a hidden // submenu. var passStraightToParent = true; switch (e.Key) { case Key.Up: passStraightToParent = !this.IsSubMenuOpen; break; case Key.Down: if (this.IsTopLevel && this.HasSubMenu && !this.IsSubMenuOpen) { this.SelectedIndex = 0; this.IsSubMenuOpen = true; e.Handled = true; } passStraightToParent = !this.IsSubMenuOpen; break; case Key.Left: if (!this.IsTopLevel && this.IsSubMenuOpen) { this.IsSubMenuOpen = false; e.Handled = true; } passStraightToParent = this.IsTopLevel || !this.IsSubMenuOpen; break; case Key.Right: if (!this.IsTopLevel && this.HasSubMenu && !this.IsSubMenuOpen) { this.SelectedIndex = 0; this.IsSubMenuOpen = true; e.Handled = true; } passStraightToParent = this.IsTopLevel || !this.IsSubMenuOpen; break; case Key.Enter: if (this.HasSubMenu) { goto case Key.Right; } else { this.RaiseEvent(new RoutedEventArgs(ClickEvent)); } break; case Key.Escape: if (this.IsSubMenuOpen) { this.IsSubMenuOpen = false; e.Handled = true; } break; } if (!passStraightToParent) { base.OnKeyDown(e); } } /// /// Called when the pointer enters the . /// /// The event args. protected override void OnPointerEnter(PointerEventArgs e) { base.OnPointerEnter(e); var menu = this.Parent as Menu; if (menu != null && menu.IsOpen) { this.IsSubMenuOpen = true; } } /// /// Called when the pointer leaves the . /// /// The event args. protected override void OnPointerLeave(PointerEventArgs e) { base.OnPointerLeave(e); if (this.submenuTimer != null) { this.submenuTimer.Dispose(); this.submenuTimer = null; } } /// /// Called when the pointer is pressed over the . /// /// The event args. protected override void OnPointerPressed(PointerPressEventArgs e) { base.OnPointerPressed(e); if (!this.HasSubMenu) { this.RaiseEvent(new RoutedEventArgs(ClickEvent)); } else if (this.IsTopLevel) { this.IsSubMenuOpen = !this.IsSubMenuOpen; } else { this.IsSubMenuOpen = true; } e.Handled = true; } /// /// Called when a submenu is opened on this MenuItem or a child MenuItem. /// /// The event args. protected virtual void OnSubmenuOpened(RoutedEventArgs e) { var menuItem = e.Source as MenuItem; if (menuItem != null && menuItem.Parent == this) { foreach (var child in this.Items.OfType()) { if (child != menuItem && child.IsSubMenuOpen) { child.IsSubMenuOpen = false; } } } } /// /// Called when the MenuItem's template has been applied. /// protected override void OnTemplateApplied() { base.OnTemplateApplied(); var popup = this.FindTemplateChild("popup"); if (popup != null) { popup.PopupRootCreated += this.PopupRootCreated; popup.Opened += this.PopupOpened; popup.Closed += this.PopupClosed; } } /// /// Closes all submenus of the menu item. /// private void CloseSubmenus() { foreach (var child in this.Items.OfType()) { child.IsSubMenuOpen = false; } } /// /// Called when the property changes. /// /// The property change event. private void SubMenuOpenChanged(PerspexPropertyChangedEventArgs e) { var value = (bool)e.NewValue; if (value) { this.RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent)); this.IsSelected = true; } else { this.CloseSubmenus(); this.SelectedIndex = -1; } } /// /// Called when the submenu's is created. /// /// The event sender. /// The event args. private void PopupRootCreated(object sender, EventArgs e) { var popup = (Popup)sender; ItemsPresenter presenter = null; // Our ItemsPresenter is in a Popup which means that it's only created when the // Popup is opened, therefore it wasn't found by ItemsControl.OnTemplateApplied. // Now the Popup has been opened for the first time it should exist, so make sure // the PopupRoot's template is applied and look for the ItemsPresenter. foreach (var c in popup.PopupRoot.GetSelfAndVisualDescendents().OfType()) { if (c.Name == "itemsPresenter" && c is ItemsPresenter) { presenter = c as ItemsPresenter; break; } c.ApplyTemplate(); } if (presenter != null) { // The presenter was found. Set its TemplatedParent so it thinks that it had a // normal birth; may it never know its own perveristy. presenter.TemplatedParent = this; this.Presenter = presenter; } } /// /// Called when the submenu's is opened. /// /// The event sender. /// The event args. private void PopupOpened(object sender, EventArgs e) { var selected = this.SelectedItem; if (selected != null) { var container = this.ItemContainerGenerator.GetContainerForItem(selected); if (container != null) { container.Focus(); } } } /// /// Called when the submenu's is closed. /// /// The event sender. /// The event args. private void PopupClosed(object sender, EventArgs e) { this.SelectedItem = null; } } }