using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Windows.Input;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Mixins;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Controls
{
///
/// A menu item control.
///
[PseudoClasses(":separator", ":icon", ":open", ":pressed", ":selected")]
public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable, ICommandSource
{
///
/// Defines the property.
///
public static readonly DirectProperty CommandProperty =
Button.CommandProperty.AddOwner(
menuItem => menuItem.Command,
(menuItem, command) => menuItem.Command = command,
enableDataValidation: true);
///
/// Defines the property.
///
public static readonly StyledProperty HotKeyProperty =
HotKeyManager.HotKeyProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty CommandParameterProperty =
Button.CommandParameterProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty IconProperty =
AvaloniaProperty.Register(nameof(Icon));
///
/// Defines the property.
///
public static readonly StyledProperty InputGestureProperty =
AvaloniaProperty.Register(nameof(InputGesture));
///
/// Defines the property.
///
public static readonly StyledProperty IsSelectedProperty =
ListBoxItem.IsSelectedProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty IsSubMenuOpenProperty =
AvaloniaProperty.Register(nameof(IsSubMenuOpen));
///
/// Defines the event.
///
public static readonly RoutedEvent ClickEvent =
RoutedEvent.Register(nameof(Click), RoutingStrategies.Bubble);
///
/// Defines the event.
///
public static readonly RoutedEvent PointerEnterItemEvent =
RoutedEvent.Register(nameof(PointerEnterItem), RoutingStrategies.Bubble);
///
/// Defines the event.
///
public static readonly RoutedEvent PointerLeaveItemEvent =
RoutedEvent.Register(nameof(PointerLeaveItem), RoutingStrategies.Bubble);
///
/// Defines the event.
///
public static readonly RoutedEvent SubmenuOpenedEvent =
RoutedEvent.Register(nameof(SubmenuOpened), RoutingStrategies.Bubble);
///
/// The default value for the property.
///
private static readonly ITemplate DefaultPanel =
new FuncTemplate(() => new StackPanel());
private ICommand? _command;
private bool _commandCanExecute = true;
private Popup? _popup;
private KeyGesture _hotkey;
private bool _isEmbeddedInMenu;
///
/// Initializes static members of the class.
///
static MenuItem()
{
SelectableMixin.Attach(IsSelectedProperty);
PressedMixin.Attach();
CommandProperty.Changed.Subscribe(CommandChanged);
CommandParameterProperty.Changed.Subscribe(CommandParameterChanged);
FocusableProperty.OverrideDefaultValue(true);
HeaderProperty.Changed.AddClassHandler((x, e) => x.HeaderChanged(e));
IconProperty.Changed.AddClassHandler((x, e) => x.IconChanged(e));
IsSelectedProperty.Changed.AddClassHandler((x, e) => x.IsSelectedChanged(e));
ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
ClickEvent.AddClassHandler((x, e) => x.OnClick(e));
SubmenuOpenedEvent.AddClassHandler((x, e) => x.OnSubmenuOpened(e));
IsSubMenuOpenProperty.Changed.AddClassHandler((x, e) => x.SubMenuOpenChanged(e));
}
public MenuItem()
{
// HACK: This nasty but it's all WPF's fault. Grid uses an inherited attached
// property to store SharedSizeGroup state, except property inheritance is done
// down the logical tree. In this case, the control which is setting
// Grid.IsSharedSizeScope="True" is not in the logical tree. Instead of fixing
// the way Grid stores shared size state, the developers of WPF just created a
// binding of the internal state of the visual parent to the menu item. We don't
// have much choice but to do the same for now unless we want to refactor Grid,
// which I honestly am not brave enough to do right now. Here's the same hack in
// the WPF codebase:
//
// https://github.com/dotnet/wpf/blob/89537909bdf36bc918e88b37751add46a8980bb0/src/Microsoft.DotNet.Wpf/src/PresentationFramework/System/Windows/Controls/MenuItem.cs#L2126-L2141
//
// In addition to the hack from WPF, we also make sure to return null when we have
// no parent. If we don't do this, inheritance falls back to the logical tree,
// causing the shared size scope in the parent MenuItem to be used, breaking
// menu layout.
var parentSharedSizeScope = this.GetObservable(VisualParentProperty)
.SelectMany(x =>
{
var parent = x as Control;
return parent?.GetObservable(DefinitionBase.PrivateSharedSizeScopeProperty) ??
Observable.Return(null);
});
this.Bind(DefinitionBase.PrivateSharedSizeScopeProperty, parentSharedSizeScope);
}
///
/// Occurs when a without a submenu is clicked.
///
public event EventHandler Click
{
add { AddHandler(ClickEvent, value); }
remove { RemoveHandler(ClickEvent, value); }
}
///
/// Occurs when the pointer enters a menu item.
///
///
/// A bubbling version of the event for menu items.
///
public event EventHandler PointerEnterItem
{
add { AddHandler(PointerEnterItemEvent, value); }
remove { RemoveHandler(PointerEnterItemEvent, value); }
}
///
/// Raised when the pointer leaves a menu item.
///
///
/// A bubbling version of the event for menu items.
///
public event EventHandler PointerLeaveItem
{
add { AddHandler(PointerLeaveItemEvent, value); }
remove { RemoveHandler(PointerLeaveItemEvent, value); }
}
///
/// Occurs when a 's submenu is opened.
///
public event EventHandler SubmenuOpened
{
add { AddHandler(SubmenuOpenedEvent, value); }
remove { RemoveHandler(SubmenuOpenedEvent, value); }
}
///
/// Gets or sets the command associated with the menu item.
///
public ICommand? Command
{
get { return _command; }
set { SetAndRaise(CommandProperty, ref _command, value); }
}
///
/// Gets or sets an associated with this control
///
public KeyGesture HotKey
{
get { return GetValue(HotKeyProperty); }
set { SetValue(HotKeyProperty, value); }
}
///
/// Gets or sets the parameter to pass to the property of a
/// .
///
public object CommandParameter
{
get { return GetValue(CommandParameterProperty); }
set { SetValue(CommandParameterProperty, value); }
}
///
/// Gets or sets the icon that appears in a .
///
public object Icon
{
get { return GetValue(IconProperty); }
set { SetValue(IconProperty, value); }
}
///
/// Gets or sets the input gesture that will be displayed in the menu item.
///
///
/// Setting this property does not cause the input gesture to be handled by the menu item,
/// it simply displays the gesture text in the menu.
///
public KeyGesture InputGesture
{
get { return GetValue(InputGestureProperty); }
set { SetValue(InputGestureProperty, value); }
}
///
/// Gets or sets a value indicating whether the is currently selected.
///
public bool IsSelected
{
get { return GetValue(IsSelectedProperty); }
set { SetValue(IsSelectedProperty, value); }
}
///
/// Gets or sets a value that indicates whether the submenu of the is
/// open.
///
public bool IsSubMenuOpen
{
get { return GetValue(IsSubMenuOpenProperty); }
set { SetValue(IsSubMenuOpenProperty, value); }
}
///
/// Gets or sets a value that indicates whether the has a submenu.
///
public bool HasSubMenu => !Classes.Contains(":empty");
///
/// Gets a value that indicates whether the is a top-level main menu item.
///
public bool IsTopLevel => Parent is Menu;
///
bool IMenuItem.IsPointerOverSubMenu => _popup?.IsPointerOverPopup ?? false;
///
IMenuElement? IMenuItem.Parent => Parent as IMenuElement;
protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute;
///
bool IMenuElement.MoveSelection(NavigationDirection direction, bool wrap) => MoveSelection(direction, wrap);
///
IMenuItem? IMenuElement.SelectedItem
{
get
{
var index = SelectedIndex;
return (index != -1) ?
(IMenuItem)ItemContainerGenerator.ContainerFromIndex(index) :
null;
}
set
{
SelectedIndex = ItemContainerGenerator.IndexFromContainer(value);
}
}
///
IEnumerable IMenuElement.SubItems
{
get
{
return ItemContainerGenerator.Containers
.Select(x => x.ContainerControl)
.OfType();
}
}
///
/// Opens the submenu.
///
///
/// This has the same effect as setting to true.
///
public void Open() => IsSubMenuOpen = true;
///
/// Closes the submenu.
///
///
/// This has the same effect as setting to false.
///
public void Close() => IsSubMenuOpen = false;
///
void IMenuItem.RaiseClick() => RaiseEvent(new RoutedEventArgs(ClickEvent));
///
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
base.OnPointerReleased(e);
if (!_isEmbeddedInMenu)
{
//Normally the Menu's IMenuInteractionHandler is sending the click events for us
//However when the item is not embedded into a menu we need to send them ourselves.
RaiseEvent(new RoutedEventArgs(ClickEvent));
}
}
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
if (_hotkey != null) // Control attached again, set Hotkey to create a hotkey manager for this control
{
HotKey = _hotkey;
}
base.OnAttachedToLogicalTree(e);
if (Command != null)
{
Command.CanExecuteChanged += CanExecuteChanged;
}
var parent = Parent;
while (parent is MenuItem)
{
parent = parent.Parent;
}
_isEmbeddedInMenu = parent is IMenu;
}
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
// This will cause the hotkey manager to dispose the observer and the reference to this control
if (HotKey != null)
{
_hotkey = HotKey;
HotKey = null;
}
base.OnDetachedFromLogicalTree(e);
if (Command != null)
{
Command.CanExecuteChanged -= CanExecuteChanged;
}
}
///
/// Called when the is clicked.
///
/// The click event args.
protected virtual void OnClick(RoutedEventArgs e)
{
if (!e.Handled && Command?.CanExecute(CommandParameter) == true)
{
Command.Execute(CommandParameter);
e.Handled = true;
}
}
///
protected override void OnGotFocus(GotFocusEventArgs e)
{
base.OnGotFocus(e);
e.Handled = UpdateSelectionFromEventSource(e.Source, true);
}
///
protected override void OnKeyDown(KeyEventArgs e)
{
// Don't handle here: let event bubble up to menu.
}
///
protected override void OnPointerEnter(PointerEventArgs e)
{
base.OnPointerEnter(e);
var point = e.GetCurrentPoint(null);
RaiseEvent(new PointerEventArgs(PointerEnterItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
e.Timestamp, point.Properties, e.KeyModifiers));
}
///
protected override void OnPointerLeave(PointerEventArgs e)
{
base.OnPointerLeave(e);
var point = e.GetCurrentPoint(null);
RaiseEvent(new PointerEventArgs(PointerLeaveItemEvent, this, e.Pointer, this.VisualRoot, point.Position,
e.Timestamp, point.Properties, e.KeyModifiers));
}
///
/// 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 ((IMenuItem)this).SubItems)
{
if (child != menuItem && child.IsSubMenuOpen)
{
child.IsSubMenuOpen = false;
}
}
}
}
///
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_popup != null)
{
_popup.Opened -= PopupOpened;
_popup.Closed -= PopupClosed;
_popup.DependencyResolver = null;
}
_popup = e.NameScope.Find("PART_Popup");
if (_popup != null)
{
_popup.DependencyResolver = DependencyResolver.Instance;
_popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
}
}
protected override void UpdateDataValidation(AvaloniaProperty property, BindingValue value)
{
base.UpdateDataValidation(property, value);
if (property == CommandProperty)
{
if (value.Type == BindingValueType.BindingError)
{
if (_commandCanExecute)
{
_commandCanExecute = false;
UpdateIsEffectivelyEnabled();
}
}
}
}
///
/// Closes all submenus of the menu item.
///
private void CloseSubmenus()
{
foreach (var child in ((IMenuItem)this).SubItems)
{
child.IsSubMenuOpen = false;
}
}
///
/// Called when the property changes.
///
/// The event args.
private static void CommandChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is MenuItem menuItem)
{
if (((ILogical)menuItem).IsAttachedToLogicalTree)
{
if (e.OldValue is ICommand oldCommand)
{
oldCommand.CanExecuteChanged -= menuItem.CanExecuteChanged;
}
if (e.NewValue is ICommand newCommand)
{
newCommand.CanExecuteChanged += menuItem.CanExecuteChanged;
}
}
menuItem.CanExecuteChanged(menuItem, EventArgs.Empty);
}
}
///
/// Called when the property changes.
///
/// The event args.
private static void CommandParameterChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.Sender is MenuItem menuItem)
{
menuItem.CanExecuteChanged(menuItem, EventArgs.Empty);
}
}
///
/// Called when the event fires.
///
/// The event sender.
/// The event args.
private void CanExecuteChanged(object sender, EventArgs e)
{
var canExecute = Command == null || Command.CanExecute(CommandParameter);
if (canExecute != _commandCanExecute)
{
_commandCanExecute = canExecute;
UpdateIsEffectivelyEnabled();
}
}
///
/// Called when the property changes.
///
/// The property change event.
private void HeaderChanged(AvaloniaPropertyChangedEventArgs e)
{
if (e.NewValue is string newValue && newValue == "-")
{
PseudoClasses.Add(":separator");
Focusable = false;
}
else if (e.OldValue is string oldValue && oldValue == "-")
{
PseudoClasses.Remove(":separator");
Focusable = true;
}
}
///
/// Called when the property changes.
///
/// The property change event.
private void IconChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldValue = e.OldValue as ILogical;
var newValue = e.NewValue as ILogical;
if (oldValue != null)
{
LogicalChildren.Remove(oldValue);
PseudoClasses.Remove(":icon");
}
if (newValue != null)
{
LogicalChildren.Add(newValue);
PseudoClasses.Add(":icon");
}
}
///
/// Called when the property changes.
///
/// The property change event.
private void IsSelectedChanged(AvaloniaPropertyChangedEventArgs e)
{
if ((bool)e.NewValue!)
{
Focus();
}
}
///
/// Called when the property changes.
///
/// The property change event.
private void SubMenuOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
var value = (bool)e.NewValue!;
if (value)
{
RaiseEvent(new RoutedEventArgs(SubmenuOpenedEvent));
IsSelected = true;
PseudoClasses.Add(":open");
}
else
{
CloseSubmenus();
SelectedIndex = -1;
PseudoClasses.Remove(":open");
}
}
///
/// Called when the submenu's is opened.
///
/// The event sender.
/// The event args.
private void PopupOpened(object sender, EventArgs e)
{
var selected = SelectedIndex;
if (selected != -1)
{
var container = ItemContainerGenerator.ContainerFromIndex(selected);
container?.Focus();
}
}
///
/// Called when the submenu's is closed.
///
/// The event sender.
/// The event args.
private void PopupClosed(object sender, EventArgs e)
{
SelectedItem = null;
}
void ICommandSource.CanExecuteChanged(object sender, EventArgs e) => this.CanExecuteChanged(sender, e);
///
/// A dependency resolver which returns a .
///
private class DependencyResolver : IAvaloniaDependencyResolver
{
///
/// Gets the default instance of .
///
public static readonly DependencyResolver Instance = new DependencyResolver();
///
/// Gets a service of the specified type.
///
/// The service type.
/// A service of the requested type.
public object GetService(Type serviceType)
{
if (serviceType == typeof(IAccessKeyHandler))
{
return new MenuItemAccessKeyHandler();
}
else
{
return AvaloniaLocator.Current.GetService(serviceType);
}
}
}
}
}