| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472 |
- 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
- {
- /// <summary>
- /// A control context menu.
- /// </summary>
- public class ContextMenu : MenuBase, ISetterValue, IPopupHostProvider
- {
- /// <summary>
- /// Defines the <see cref="HorizontalOffset"/> property.
- /// </summary>
- public static readonly StyledProperty<double> HorizontalOffsetProperty =
- Popup.HorizontalOffsetProperty.AddOwner<ContextMenu>();
- /// <summary>
- /// Defines the <see cref="VerticalOffset"/> property.
- /// </summary>
- public static readonly StyledProperty<double> VerticalOffsetProperty =
- Popup.VerticalOffsetProperty.AddOwner<ContextMenu>();
- /// <summary>
- /// Defines the <see cref="PlacementAnchor"/> property.
- /// </summary>
- public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
- Popup.PlacementAnchorProperty.AddOwner<ContextMenu>();
- /// <summary>
- /// Defines the <see cref="PlacementConstraintAdjustment"/> property.
- /// </summary>
- public static readonly StyledProperty<PopupPositionerConstraintAdjustment> PlacementConstraintAdjustmentProperty =
- Popup.PlacementConstraintAdjustmentProperty.AddOwner<ContextMenu>();
- /// <summary>
- /// Defines the <see cref="PlacementGravity"/> property.
- /// </summary>
- public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
- Popup.PlacementGravityProperty.AddOwner<ContextMenu>();
- /// <summary>
- /// Defines the <see cref="PlacementMode"/> property.
- /// </summary>
- public static readonly StyledProperty<PlacementMode> PlacementModeProperty =
- Popup.PlacementProperty.AddOwner<ContextMenu>();
- /// <summary>
- /// Defines the <see cref="PlacementRect"/> property.
- /// </summary>
- public static readonly StyledProperty<Rect?> PlacementRectProperty =
- Popup.PlacementRectProperty.AddOwner<ContextMenu>();
- /// <summary>
- /// Defines the <see cref="WindowManagerAddShadowHint"/> property.
- /// </summary>
- public static readonly StyledProperty<bool> WindowManagerAddShadowHintProperty =
- Popup.WindowManagerAddShadowHintProperty.AddOwner<ContextMenu>();
- /// <summary>
- /// Defines the <see cref="PlacementTarget"/> property.
- /// </summary>
- public static readonly StyledProperty<Control?> PlacementTargetProperty =
- Popup.PlacementTargetProperty.AddOwner<ContextMenu>();
- private static readonly ITemplate<Panel> DefaultPanel =
- new FuncTemplate<Panel>(() => new StackPanel { Orientation = Orientation.Vertical });
- private Popup? _popup;
- private List<Control>? _attachedControls;
- private IInputElement? _previousFocus;
- private Action<IPopupHost?>? _popupHostChangedHandler;
- /// <summary>
- /// Initializes a new instance of the <see cref="ContextMenu"/> class.
- /// </summary>
- public ContextMenu()
- : this(new DefaultMenuInteractionHandler(true))
- {
- }
- /// <summary>
- /// Initializes a new instance of the <see cref="ContextMenu"/> class.
- /// </summary>
- /// <param name="interactionHandler">The menu interaction handler.</param>
- public ContextMenu(IMenuInteractionHandler interactionHandler)
- : base(interactionHandler)
- {
- }
- /// <summary>
- /// Initializes static members of the <see cref="ContextMenu"/> class.
- /// </summary>
- static ContextMenu()
- {
- ItemsPanelProperty.OverrideDefaultValue<ContextMenu>(DefaultPanel);
- PlacementModeProperty.OverrideDefaultValue<ContextMenu>(PlacementMode.Pointer);
- ContextMenuProperty.Changed.Subscribe(ContextMenuChanged);
- AutomationProperties.AccessibilityViewProperty.OverrideDefaultValue<ContextMenu>(AccessibilityView.Control);
- AutomationProperties.ControlTypeOverrideProperty.OverrideDefaultValue<ContextMenu>(AutomationControlType.Menu);
- }
- /// <summary>
- /// Gets or sets the Horizontal offset of the context menu in relation to the <see cref="PlacementTarget"/>.
- /// </summary>
- public double HorizontalOffset
- {
- get { return GetValue(HorizontalOffsetProperty); }
- set { SetValue(HorizontalOffsetProperty, value); }
- }
- /// <summary>
- /// Gets or sets the Vertical offset of the context menu in relation to the <see cref="PlacementTarget"/>.
- /// </summary>
- public double VerticalOffset
- {
- get { return GetValue(VerticalOffsetProperty); }
- set { SetValue(VerticalOffsetProperty, value); }
- }
- /// <summary>
- /// Gets or sets the anchor point on the <see cref="PlacementRect"/> when <see cref="PlacementMode"/>
- /// is <see cref="PlacementMode.AnchorAndGravity"/>.
- /// </summary>
- public PopupAnchor PlacementAnchor
- {
- get { return GetValue(PlacementAnchorProperty); }
- set { SetValue(PlacementAnchorProperty, value); }
- }
- /// <summary>
- /// 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.
- /// </summary>
- public PopupPositionerConstraintAdjustment PlacementConstraintAdjustment
- {
- get { return GetValue(PlacementConstraintAdjustmentProperty); }
- set { SetValue(PlacementConstraintAdjustmentProperty, value); }
- }
- /// <summary>
- /// Gets or sets a value which defines in what direction the context menu should open
- /// when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
- /// </summary>
- public PopupGravity PlacementGravity
- {
- get { return GetValue(PlacementGravityProperty); }
- set { SetValue(PlacementGravityProperty, value); }
- }
- /// <summary>
- /// Gets or sets the placement mode of the context menu in relation to the<see cref="PlacementTarget"/>.
- /// </summary>
- public PlacementMode PlacementMode
- {
- get { return GetValue(PlacementModeProperty); }
- set { SetValue(PlacementModeProperty, value); }
- }
- public bool WindowManagerAddShadowHint
- {
- get { return GetValue(WindowManagerAddShadowHintProperty); }
- set { SetValue(WindowManagerAddShadowHintProperty, value); }
- }
- /// <summary>
- /// Gets or sets the the anchor rectangle within the parent that the context menu will be placed
- /// relative to when <see cref="PlacementMode"/> is <see cref="PlacementMode.AnchorAndGravity"/>.
- /// </summary>
- /// <remarks>
- /// The placement rect defines a rectangle relative to <see cref="PlacementTarget"/> around
- /// which the popup will be opened, with <see cref="PlacementAnchor"/> determining which edge
- /// of the placement target is used.
- ///
- /// If unset, the anchor rectangle will be the bounds of the <see cref="PlacementTarget"/>.
- /// </remarks>
- public Rect? PlacementRect
- {
- get { return GetValue(PlacementRectProperty); }
- set { SetValue(PlacementRectProperty, value); }
- }
- /// <summary>
- /// Gets or sets the control that is used to determine the popup's position.
- /// </summary>
- public Control? PlacementTarget
- {
- get { return GetValue(PlacementTargetProperty); }
- set { SetValue(PlacementTargetProperty, value); }
- }
- /// <summary>
- /// Occurs when the value of the
- /// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
- /// property is changing from false to true.
- /// </summary>
- public event CancelEventHandler? ContextMenuOpening;
- /// <summary>
- /// Occurs when the value of the
- /// <see cref="P:Avalonia.Controls.ContextMenu.IsOpen" />
- /// property is changing from true to false.
- /// </summary>
- public event CancelEventHandler? ContextMenuClosing;
- /// <summary>
- /// Called when the <see cref="Control.ContextMenu"/> property changes on a control.
- /// </summary>
- /// <param name="e">The event args.</param>
- 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<Control>();
- 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<bool>();
- }
- }
- /// <summary>
- /// Opens the menu.
- /// </summary>
- public override void Open() => Open(null);
- /// <summary>
- /// Opens a context menu on the specified control.
- /// </summary>
- /// <param name="control">The control.</param>
- 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);
- }
- /// <summary>
- /// Closes the menu.
- /// </summary>
- 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 <template>.
- if (!(setter is Setter s && s.Property == ContextMenuProperty))
- {
- throw new InvalidOperationException(
- "Cannot use a control as a Setter value. Wrap the control in a <Template>.");
- }
- }
- IPopupHost? IPopupHostProvider.PopupHost => _popup?.Host;
- event Action<IPopupHost?>? 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<PlatformHotkeyConfiguration>();
- 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;
- }
- }
- }
|