using System;
using System.Collections.Generic;
using System.ComponentModel;
using Avalonia.Automation.Peers;
using System.Linq;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Platform;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Styling;
using Avalonia.Automation;
using Avalonia.Reactive;
namespace Avalonia.Controls
{
///
/// A control context menu.
///
public class ContextMenu : MenuBase, ISetterValue, IPopupHostProvider
{
///
/// Defines the property.
///
public static readonly StyledProperty HorizontalOffsetProperty =
Popup.HorizontalOffsetProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty VerticalOffsetProperty =
Popup.VerticalOffsetProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty PlacementAnchorProperty =
Popup.PlacementAnchorProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty PlacementConstraintAdjustmentProperty =
Popup.PlacementConstraintAdjustmentProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty PlacementGravityProperty =
Popup.PlacementGravityProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty PlacementModeProperty =
Popup.PlacementProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty PlacementRectProperty =
Popup.PlacementRectProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty WindowManagerAddShadowHintProperty =
Popup.WindowManagerAddShadowHintProperty.AddOwner();
///
/// Defines the property.
///
public static readonly StyledProperty PlacementTargetProperty =
Popup.PlacementTargetProperty.AddOwner();
private static readonly ITemplate DefaultPanel =
new FuncTemplate(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup? _popup;
private List? _attachedControls;
private IInputElement? _previousFocus;
private Action? _popupHostChangedHandler;
///
/// Initializes a new instance of the class.
///
public ContextMenu()
: this(new DefaultMenuInteractionHandler(true))
{
}
///
/// Initializes a new instance of the class.
///
/// The menu interaction handler.
public ContextMenu(IMenuInteractionHandler interactionHandler)
: base(interactionHandler)
{
}
///
/// Initializes static members of the class.
///
static ContextMenu()
{
ItemsPanelProperty.OverrideDefaultValue(DefaultPanel);
PlacementModeProperty.OverrideDefaultValue(PlacementMode.Pointer);
ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue(AccessibilityView.Control);
AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue(AutomationControlType.Menu);
}
///
/// Gets or sets the Horizontal offset of the context menu in relation to the .
///
public double HorizontalOffset
{
get { return GetValue(HorizontalOffsetProperty); }
set { SetValue(HorizontalOffsetProperty, value); }
}
///
/// Gets or sets the Vertical offset of the context menu in relation to the .
///
public double VerticalOffset
{
get { return GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
///
/// Gets or sets the anchor point on the when
/// is .
///
public PopupAnchor PlacementAnchor
{
get { return GetValue(PlacementAnchorProperty); }
set { SetValue(PlacementAnchorProperty, value); }
}
///
/// Gets or sets a value describing how the context menu position will be adjusted if the
/// unadjusted position would result in the context menu being partly constrained.
///
public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
{
get { return GetValue(PlacementConstraintAdjustmentProperty); }
set { SetValue(PlacementConstraintAdjustmentProperty, value); }
}
///
/// Gets or sets a value which defines in what direction the context menu should open
/// when is .
///
public PopupGravity PlacementGravity
{
get { return GetValue(PlacementGravityProperty); }
set { SetValue(PlacementGravityProperty, value); }
}
///
/// Gets or sets the placement mode of the context menu in relation to the.
///
public PlacementMode PlacementMode
{
get { return GetValue(PlacementModeProperty); }
set { SetValue(PlacementModeProperty, value); }
}
public bool WindowManagerAddShadowHint
{
get { return GetValue(WindowManagerAddShadowHintProperty); }
set { SetValue(WindowManagerAddShadowHintProperty, value); }
}
///
/// Gets or sets the the anchor rectangle within the parent that the context menu will be placed
/// relative to when is .
///
///
/// The placement rect defines a rectangle relative to around
/// which the popup will be opened, with determining which edge
/// of the placement target is used.
///
/// If unset, the anchor rectangle will be the bounds of the .
///
public Rect? PlacementRect
{
get { return GetValue(PlacementRectProperty); }
set { SetValue(PlacementRectProperty, value); }
}
///
/// Gets or sets the control that is used to determine the popup's position.
///
public Control? PlacementTarget
{
get { return GetValue(PlacementTargetProperty); }
set { SetValue(PlacementTargetProperty, value); }
}
///
/// Occurs when the value of the
///
/// property is changing from false to true.
///
public event CancelEventHandler? ContextMenuOpening;
///
/// Occurs when the value of the
///
/// property is changing from true to false.
///
public event CancelEventHandler? ContextMenuClosing;
///
/// Called when the property changes on a control.
///
/// The event args.
private static void ContextMenuChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
if (e.OldValue is ContextMenu oldMenu)
{
control.ContextRequested -= ControlContextRequested;
control.DetachedFromVisualTree -= ControlDetachedFromVisualTree;
oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent?)oldMenu._popup)?.SetParent(null);
}
if (e.NewValue is ContextMenu newMenu)
{
newMenu._attachedControls ??= new List();
newMenu._attachedControls.Add(control);
control.ContextRequested += ControlContextRequested;
control.DetachedFromVisualTree += ControlDetachedFromVisualTree;
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == WindowManagerAddShadowHintProperty && _popup != null)
{
_popup.WindowManagerAddShadowHint = change.GetNewValue();
}
}
///
/// Opens the menu.
///
public override void Open() => Open(null);
///
/// Opens a context menu on the specified control.
///
/// The control.
public void Open(Control? control)
{
if (control is null && (_attachedControls is null || _attachedControls.Count == 0))
{
throw new ArgumentNullException(nameof(control));
}
if (control is object &&
_attachedControls is object &&
!_attachedControls.Contains(control))
{
throw new ArgumentException(
"Cannot show ContentMenu on a different control to the one it is attached to.",
nameof(control));
}
control ??= _attachedControls![0];
Open(control, PlacementTarget ?? control, false);
}
///
/// Closes the menu.
///
public override void Close()
{
if (!IsOpen)
{
return;
}
if (_popup != null && _popup.IsVisible)
{
_popup.IsOpen = false;
}
}
void ISetterValue.Initialize(ISetter setter)
{
// ContextMenu can be assigned to the ContextMenu property in a setter. This overrides
// the behavior defined in Control which requires controls to be wrapped in a .
if (!(setter is Setter s && s.Property == ContextMenuProperty))
{
throw new InvalidOperationException(
"Cannot use a control as a Setter value. Wrap the control in a .");
}
}
IPopupHost? IPopupHostProvider.PopupHost => _popup?.Host;
event Action? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
private void Open(Control control, Control placementTarget, bool requestedByPointer)
{
if (IsOpen)
{
return;
}
if (_popup == null)
{
_popup = new Popup
{
IsLightDismissEnabled = true,
OverlayDismissEventPassThrough = true,
};
_popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
_popup.Closing += PopupClosing;
_popup.KeyUp += PopupKeyUp;
}
if (_popup.Parent != control)
{
((ISetLogicalParent)_popup).SetParent(null);
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.Placement = !requestedByPointer && PlacementMode == PlacementMode.Pointer
? PlacementMode.Bottom
: PlacementMode;
//Position of the line below is really important.
//All styles are being applied only when control has logical parent.
//Line below will add ContextMenu as child to the Popup and this will trigger styles and they would be applied.
//If you will move line below somewhere else it may cause that ContextMenu will behave differently from what you are expecting.
_popup.Child = this;
_popup.PlacementTarget = placementTarget;
_popup.HorizontalOffset = HorizontalOffset;
_popup.VerticalOffset = VerticalOffset;
_popup.PlacementAnchor = PlacementAnchor;
_popup.PlacementConstraintAdjustment = PlacementConstraintAdjustment;
_popup.PlacementGravity = PlacementGravity;
_popup.PlacementRect = PlacementRect;
_popup.WindowManagerAddShadowHint = WindowManagerAddShadowHint;
IsOpen = true;
_popup.IsOpen = true;
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuOpenedEvent,
Source = this,
});
}
private void PopupOpened(object? sender, EventArgs e)
{
_previousFocus = FocusManager.Instance?.Current;
Focus();
_popupHostChangedHandler?.Invoke(_popup!.Host);
}
private void PopupClosing(object? sender, CancelEventArgs e)
{
e.Cancel = CancelClosing();
}
private void PopupClosed(object? sender, EventArgs e)
{
foreach (var i in LogicalChildren)
{
if (i is MenuItem menuItem)
{
menuItem.IsSubMenuOpen = false;
}
}
SelectedIndex = -1;
IsOpen = false;
if (_attachedControls is null || _attachedControls.Count == 0)
{
((ISetLogicalParent)_popup!).SetParent(null);
}
// HACK: Reset the focus when the popup is closed. We need to fix this so it's automatic.
FocusManager.Instance?.Focus(_previousFocus);
RaiseEvent(new RoutedEventArgs
{
RoutedEvent = MenuClosedEvent,
Source = this,
});
_popupHostChangedHandler?.Invoke(null);
}
private void PopupKeyUp(object? sender, KeyEventArgs e)
{
if (IsOpen)
{
var keymap = AvaloniaLocator.Current.GetService();
if (keymap?.OpenContextMenu.Any(k => k.Matches(e)) == true
&& !CancelClosing())
{
Close();
e.Handled = true;
}
}
}
private static void ControlContextRequested(object? sender, ContextRequestedEventArgs e)
{
if (sender is Control control
&& control.ContextMenu is ContextMenu contextMenu
&& !e.Handled
&& !contextMenu.CancelOpening())
{
var requestedByPointer = e.TryGetPosition(null, out _);
contextMenu.Open(control, e.Source as Control ?? control, requestedByPointer);
e.Handled = true;
}
}
private static void ControlDetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
if (sender is Control control
&& control.ContextMenu is ContextMenu contextMenu)
{
if (contextMenu._popup?.Parent == control)
{
((ISetLogicalParent)contextMenu._popup).SetParent(null);
}
contextMenu.Close();
}
}
private bool CancelClosing()
{
var eventArgs = new CancelEventArgs();
ContextMenuClosing?.Invoke(this, eventArgs);
return eventArgs.Cancel;
}
private bool CancelOpening()
{
var eventArgs = new CancelEventArgs();
ContextMenuOpening?.Invoke(this, eventArgs);
return eventArgs.Cancel;
}
}
}