// -----------------------------------------------------------------------
//
// 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;
}
}
}