|
|
|
@ -11,81 +11,154 @@ namespace Perspex.Controls |
|
|
|
using System.Windows.Input; |
|
|
|
using Perspex.Controls.Primitives; |
|
|
|
using Perspex.Input; |
|
|
|
using Perspex.LogicalTree; |
|
|
|
using Perspex.VisualTree; |
|
|
|
using Perspex.Interactivity; |
|
|
|
using Perspex.Rendering; |
|
|
|
using Perspex.Controls.Templates; |
|
|
|
using Perspex.Controls.Presenters; |
|
|
|
|
|
|
|
public class MenuItem : HeaderedItemsControl, IMenu |
|
|
|
/// <summary>
|
|
|
|
/// A menu item control.
|
|
|
|
/// </summary>
|
|
|
|
public class MenuItem : HeaderedItemsControl |
|
|
|
{ |
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="Command"/> property.
|
|
|
|
/// </summary>
|
|
|
|
public static readonly PerspexProperty<ICommand> CommandProperty = |
|
|
|
Button.CommandProperty.AddOwner<MenuItem>(); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="CommandParameter"/> property.
|
|
|
|
/// </summary>
|
|
|
|
public static readonly PerspexProperty<object> CommandParameterProperty = |
|
|
|
Button.CommandParameterProperty.AddOwner<MenuItem>(); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="Icon"/> property.
|
|
|
|
/// </summary>
|
|
|
|
public static readonly PerspexProperty<object> IconProperty = |
|
|
|
PerspexProperty.Register<MenuItem, object>("Icon"); |
|
|
|
PerspexProperty.Register<MenuItem, object>(nameof(Icon)); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="IsSubMenuOpen"/> property.
|
|
|
|
/// </summary>
|
|
|
|
public static readonly PerspexProperty<bool> IsSubMenuOpenProperty = |
|
|
|
PerspexProperty.Register<MenuItem, bool>("IsSubMenuOpen"); |
|
|
|
PerspexProperty.Register<MenuItem, bool>(nameof(IsSubMenuOpen)); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="Click"/> event.
|
|
|
|
/// </summary>
|
|
|
|
public static readonly RoutedEvent<RoutedEventArgs> ClickEvent = |
|
|
|
RoutedEvent.Register<MenuItem, RoutedEventArgs>("Click", RoutingStrategies.Bubble); |
|
|
|
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(Click), RoutingStrategies.Bubble); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="SubmenuOpened"/> event.
|
|
|
|
/// </summary>
|
|
|
|
public static readonly RoutedEvent<RoutedEventArgs> SubmenuOpenedEvent = |
|
|
|
RoutedEvent.Register<MenuItem, RoutedEventArgs>(nameof(SubmenuOpened), RoutingStrategies.Bubble); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// The timer used to display submenus.
|
|
|
|
/// </summary>
|
|
|
|
private IDisposable submenuTimer; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes static members of the <see cref="MenuItem"/> class.
|
|
|
|
/// </summary>
|
|
|
|
static MenuItem() |
|
|
|
{ |
|
|
|
ClickEvent.AddClassHandler<MenuItem>(x => x.OnClick); |
|
|
|
SubmenuOpenedEvent.AddClassHandler<MenuItem>(x => x.OnSubmenuOpened); |
|
|
|
IsSubMenuOpenProperty.Changed.Subscribe(SubMenuOpenChanged); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Occurs when a <see cref="MenuItem"/> without a submenu is clicked.
|
|
|
|
/// </summary>
|
|
|
|
public event EventHandler<RoutedEventArgs> Click |
|
|
|
{ |
|
|
|
add { this.AddHandler(ClickEvent, value); } |
|
|
|
remove { this.RemoveHandler(ClickEvent, value); } |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Occurs when a <see cref="MenuItem"/>'s submenu is opened.
|
|
|
|
/// </summary>
|
|
|
|
public event EventHandler<RoutedEventArgs> SubmenuOpened |
|
|
|
{ |
|
|
|
add { this.AddHandler(SubmenuOpenedEvent, value); } |
|
|
|
remove { this.RemoveHandler(SubmenuOpenedEvent, value); } |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the command associated with the menu item.
|
|
|
|
/// </summary>
|
|
|
|
public ICommand Command |
|
|
|
{ |
|
|
|
get { return this.GetValue(CommandProperty); } |
|
|
|
set { this.SetValue(CommandProperty, value); } |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the parameter to pass to the <see cref="Command"/> property of a
|
|
|
|
/// <see cref="MenuItem"/>.
|
|
|
|
/// </summary>
|
|
|
|
public object CommandParameter |
|
|
|
{ |
|
|
|
get { return this.GetValue(CommandParameterProperty); } |
|
|
|
set { this.SetValue(CommandParameterProperty, value); } |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the icon that appears in a <see cref="MenuItem"/>.
|
|
|
|
/// </summary>
|
|
|
|
public object Icon |
|
|
|
{ |
|
|
|
get { return this.GetValue(IconProperty); } |
|
|
|
set { this.SetValue(IconProperty, value); } |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets a value that indicates whether the submenu of the <see cref="MenuItem"/> is
|
|
|
|
/// open.
|
|
|
|
/// </summary>
|
|
|
|
public bool IsSubMenuOpen |
|
|
|
{ |
|
|
|
get { return this.GetValue(IsSubMenuOpenProperty); } |
|
|
|
set { this.SetValue(IsSubMenuOpenProperty, value); } |
|
|
|
} |
|
|
|
|
|
|
|
void IMenu.ChildPointerEnter(MenuItem item) |
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets a value that indicates whether the <see cref="MenuItem"/> has a submenu.
|
|
|
|
/// </summary>
|
|
|
|
public bool HasSubMenu |
|
|
|
{ |
|
|
|
get { return !this.Classes.Contains(":empty"); } |
|
|
|
} |
|
|
|
|
|
|
|
void IMenu.ChildSubMenuOpened(MenuItem item) |
|
|
|
/// <summary>
|
|
|
|
/// Gets a value that indicates whether the <see cref="MenuItem"/> is a top-level menu item.
|
|
|
|
/// </summary>
|
|
|
|
public bool IsTopLevel |
|
|
|
{ |
|
|
|
foreach (var i in this.Items.Cast<object>().OfType<MenuItem>()) |
|
|
|
{ |
|
|
|
i.IsSubMenuOpen = i == item; |
|
|
|
} |
|
|
|
get; |
|
|
|
private set; |
|
|
|
} |
|
|
|
|
|
|
|
void IMenu.CloseMenu() |
|
|
|
/// <summary>
|
|
|
|
/// Called when the <see cref="MenuItem"/> is attached to the visual tree.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="root">The root of the visual tree.</param>
|
|
|
|
protected override void OnAttachedToVisualTree(IRenderRoot root) |
|
|
|
{ |
|
|
|
this.IsSubMenuOpen = false; |
|
|
|
this.GetParentMenu().CloseMenu(); |
|
|
|
base.OnAttachedToVisualTree(root); |
|
|
|
this.IsTopLevel = this.Parent is Menu; |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Called when the <see cref="MenuItem"/> is clicked.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="e">The click event args.</param>
|
|
|
|
protected virtual void OnClick(RoutedEventArgs e) |
|
|
|
{ |
|
|
|
if (this.Command != null) |
|
|
|
@ -94,71 +167,157 @@ namespace Perspex.Controls |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Called when the pointer enters the <see cref="MenuItem"/>.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="e">The event args.</param>
|
|
|
|
protected override void OnPointerEnter(PointerEventArgs e) |
|
|
|
{ |
|
|
|
base.OnPointerEnter(e); |
|
|
|
this.GetLogicalParent<IMenu>()?.ChildPointerEnter(this); |
|
|
|
} |
|
|
|
|
|
|
|
protected override void OnPointerPressed(PointerPressEventArgs e) |
|
|
|
{ |
|
|
|
base.OnPointerPressed(e); |
|
|
|
var menu = this.Parent as Menu; |
|
|
|
|
|
|
|
if (this.Classes.Contains(":empty")) |
|
|
|
if (menu != null && menu.IsOpen) |
|
|
|
{ |
|
|
|
RoutedEventArgs click = new RoutedEventArgs |
|
|
|
{ |
|
|
|
RoutedEvent = ClickEvent, |
|
|
|
}; |
|
|
|
|
|
|
|
this.RaiseEvent(click); |
|
|
|
this.GetParentMenu().CloseMenu(); |
|
|
|
this.IsSubMenuOpen = true; |
|
|
|
} |
|
|
|
else |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Called when the pointer leaves the <see cref="MenuItem"/>.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="e">The event args.</param>
|
|
|
|
protected override void OnPointerLeave(PointerEventArgs e) |
|
|
|
{ |
|
|
|
base.OnPointerLeave(e); |
|
|
|
|
|
|
|
if (this.submenuTimer != null) |
|
|
|
{ |
|
|
|
this.IsSubMenuOpen = !this.IsSubMenuOpen; |
|
|
|
this.submenuTimer.Dispose(); |
|
|
|
this.submenuTimer = null; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private IMenu GetParentMenu() |
|
|
|
/// <summary>
|
|
|
|
/// Called when the pointer is pressed over the <see cref="MenuItem"/>.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="e">The event args.</param>
|
|
|
|
protected override void OnPointerPressed(PointerPressEventArgs e) |
|
|
|
{ |
|
|
|
var parent = this.GetLogicalParent<IMenu>(); |
|
|
|
base.OnPointerPressed(e); |
|
|
|
|
|
|
|
if (parent != null) |
|
|
|
if (!this.HasSubMenu) |
|
|
|
{ |
|
|
|
return parent; |
|
|
|
this.RaiseEvent(new RoutedEventArgs(ClickEvent)); |
|
|
|
} |
|
|
|
else if (this.IsTopLevel) |
|
|
|
{ |
|
|
|
this.IsSubMenuOpen = !this.IsSubMenuOpen; |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
var popupRoot = this.GetVisualAncestors().OfType<PopupRoot>().FirstOrDefault(); |
|
|
|
var parentItem = ((ILogical)popupRoot).GetLogicalParent<Popup>().TemplatedParent; |
|
|
|
return (IMenu)parentItem; |
|
|
|
this.IsSubMenuOpen = true; |
|
|
|
} |
|
|
|
|
|
|
|
e.Handled = true; |
|
|
|
} |
|
|
|
|
|
|
|
private void OnSubMenuOpenChanged(bool open) |
|
|
|
/// <summary>
|
|
|
|
/// Called when a submenu is opened on this MenuItem or a child MenuItem.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="e">The event args.</param>
|
|
|
|
protected virtual void OnSubmenuOpened(RoutedEventArgs e) |
|
|
|
{ |
|
|
|
if (!open && this.Items != null) |
|
|
|
var menuItem = e.Source as MenuItem; |
|
|
|
|
|
|
|
if (menuItem != null && menuItem.Parent == this) |
|
|
|
{ |
|
|
|
foreach (var item in this.Items.Cast<object>().OfType<MenuItem>()) |
|
|
|
foreach (var child in this.Items.OfType<MenuItem>()) |
|
|
|
{ |
|
|
|
item.IsSubMenuOpen = false; |
|
|
|
if (child != menuItem && child.IsSubMenuOpen) |
|
|
|
{ |
|
|
|
child.IsSubMenuOpen = false; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
else if (open) |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Called when the MenuItem's template has been applied.
|
|
|
|
/// </summary>
|
|
|
|
protected override void OnTemplateApplied() |
|
|
|
{ |
|
|
|
base.OnTemplateApplied(); |
|
|
|
|
|
|
|
var popup = this.FindTemplateChild<Popup>("popup"); |
|
|
|
|
|
|
|
if (popup != null) |
|
|
|
{ |
|
|
|
popup.Opened += this.PopupFirstOpened; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Closes all submenus of the menu item.
|
|
|
|
/// </summary>
|
|
|
|
private void CloseSubmenus() |
|
|
|
{ |
|
|
|
foreach (var child in this.Items.OfType<MenuItem>()) |
|
|
|
{ |
|
|
|
this.GetParentMenu().ChildSubMenuOpened(this); |
|
|
|
child.IsSubMenuOpen = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Called when the <see cref="IsSubMenuOpen"/> property changes.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="e">The property change event.</param>
|
|
|
|
private static void SubMenuOpenChanged(PerspexPropertyChangedEventArgs e) |
|
|
|
{ |
|
|
|
var sender = e.Sender as MenuItem; |
|
|
|
var value = (bool)e.NewValue; |
|
|
|
|
|
|
|
if (sender != null) |
|
|
|
{ |
|
|
|
sender.OnSubMenuOpenChanged((bool)e.NewValue); |
|
|
|
if (value) |
|
|
|
{ |
|
|
|
sender.RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent)); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
sender.CloseSubmenus(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Called the first time the MenuItem's popup is opened.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="sender">The event sender.</param>
|
|
|
|
/// <param name="e">The event args.</param>
|
|
|
|
private void PopupFirstOpened(object sender, EventArgs e) |
|
|
|
{ |
|
|
|
var popup = (Popup)sender; |
|
|
|
|
|
|
|
// 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.
|
|
|
|
popup.PopupRoot.ApplyTemplate(); |
|
|
|
var presenter = popup.PopupRoot.FindControl<ItemsPresenter>("itemsPresenter"); |
|
|
|
|
|
|
|
if (presenter != null) |
|
|
|
{ |
|
|
|
// The presenter was found. First make its Panel's ChildLogicalParent point to
|
|
|
|
// this so that the child MenuItems will be logically parented by the parent
|
|
|
|
// MenuItem and then assign it to our Presenter property.
|
|
|
|
presenter.ApplyTemplate(); |
|
|
|
((IItemsPanel)presenter.Panel).ChildLogicalParent = this; |
|
|
|
this.Presenter = presenter; |
|
|
|
} |
|
|
|
|
|
|
|
// Don't call this event handler again.
|
|
|
|
popup.Opened -= this.PopupFirstOpened; |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|