diff --git a/src/Avalonia.Controls/IMenu.cs b/src/Avalonia.Controls/IMenu.cs
index ddf2997014..b3ec77b108 100644
--- a/src/Avalonia.Controls/IMenu.cs
+++ b/src/Avalonia.Controls/IMenu.cs
@@ -8,8 +8,7 @@ namespace Avalonia.Controls
///
/// Represents a or .
///
- [NotClientImplementable]
- public interface IMenu : IMenuElement, IInputElement
+ internal interface IMenu : IMenuElement, IInputElement
{
///
/// Gets the menu interaction handler.
diff --git a/src/Avalonia.Controls/IMenuElement.cs b/src/Avalonia.Controls/IMenuElement.cs
index 2a81d20bd9..a474f335f2 100644
--- a/src/Avalonia.Controls/IMenuElement.cs
+++ b/src/Avalonia.Controls/IMenuElement.cs
@@ -8,8 +8,7 @@ namespace Avalonia.Controls
///
/// Represents an or .
///
- [NotClientImplementable]
- public interface IMenuElement : IInputElement, ILogical
+ internal interface IMenuElement : IInputElement, ILogical
{
///
/// Gets or sets the currently selected submenu item.
diff --git a/src/Avalonia.Controls/IMenuItem.cs b/src/Avalonia.Controls/IMenuItem.cs
index 1257d33684..3a9c5373a6 100644
--- a/src/Avalonia.Controls/IMenuItem.cs
+++ b/src/Avalonia.Controls/IMenuItem.cs
@@ -5,8 +5,7 @@ namespace Avalonia.Controls
///
/// Represents a .
///
- [NotClientImplementable]
- public interface IMenuItem : IMenuElement
+ internal interface IMenuItem : IMenuElement
{
///
/// Gets or sets a value that indicates whether the item has a submenu.
diff --git a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
index d2b23a7ac3..79a3038edf 100644
--- a/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
+++ b/src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
@@ -39,91 +39,20 @@ namespace Avalonia.Controls.Platform
DelayRun = delayRun;
}
- public virtual void Attach(IMenu menu)
- {
- if (Menu != null)
- {
- throw new NotSupportedException("DefaultMenuInteractionHandler is already attached.");
- }
-
- Menu = menu;
- Menu.GotFocus += GotFocus;
- Menu.LostFocus += LostFocus;
- Menu.KeyDown += KeyDown;
- Menu.PointerPressed += PointerPressed;
- Menu.PointerReleased += PointerReleased;
- Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
- Menu.AddHandler(MenuBase.MenuOpenedEvent, MenuOpened);
- Menu.AddHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
- Menu.AddHandler(MenuItem.PointerExitedItemEvent, PointerExited);
- Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved);
-
- _root = Menu.VisualRoot;
-
- if (_root is InputElement inputRoot)
- {
- inputRoot.AddHandler(InputElement.PointerPressedEvent, RootPointerPressed, RoutingStrategies.Tunnel);
- }
-
- if (_root is WindowBase window)
- {
- window.Deactivated += WindowDeactivated;
- }
-
- if (_root is TopLevel tl && tl.PlatformImpl is ITopLevelImpl pimpl)
- pimpl.LostFocus += TopLevelLostPlatformFocus;
-
- _inputManagerSubscription = InputManager?.Process.Subscribe(RawInput);
- }
-
- public virtual void Detach(IMenu menu)
- {
- if (Menu != menu)
- {
- throw new NotSupportedException("DefaultMenuInteractionHandler is not attached to the menu.");
- }
-
- Menu.GotFocus -= GotFocus;
- Menu.LostFocus -= LostFocus;
- Menu.KeyDown -= KeyDown;
- Menu.PointerPressed -= PointerPressed;
- Menu.PointerReleased -= PointerReleased;
- Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
- Menu.RemoveHandler(MenuBase.MenuOpenedEvent, MenuOpened);
- Menu.RemoveHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
- Menu.RemoveHandler(MenuItem.PointerExitedItemEvent, PointerExited);
- Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved);
-
- if (_root is InputElement inputRoot)
- {
- inputRoot.RemoveHandler(InputElement.PointerPressedEvent, RootPointerPressed);
- }
-
- if (_root is WindowBase root)
- {
- root.Deactivated -= WindowDeactivated;
- }
-
- if (_root is TopLevel tl && tl.PlatformImpl != null)
- tl.PlatformImpl.LostFocus -= TopLevelLostPlatformFocus;
-
- _inputManagerSubscription?.Dispose();
-
- Menu = null;
- _root = null;
- }
+ public void Attach(MenuBase menu) => AttachCore(menu);
+ public void Detach(MenuBase menu) => DetachCore(menu);
protected Action DelayRun { get; }
protected IInputManager? InputManager { get; }
- protected IMenu? Menu { get; private set; }
+ internal IMenu? Menu { get; private set; }
protected static TimeSpan MenuShowDelay { get; } = TimeSpan.FromMilliseconds(400);
protected internal virtual void GotFocus(object? sender, GotFocusEventArgs e)
{
- var item = GetMenuItem(e.Source as Control);
+ var item = GetMenuItemCore(e.Source as Control);
if (item?.Parent != null)
{
@@ -133,7 +62,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void LostFocus(object? sender, RoutedEventArgs e)
{
- var item = GetMenuItem(e.Source as Control);
+ var item = GetMenuItemCore(e.Source as Control);
if (item != null)
{
@@ -143,146 +72,12 @@ namespace Avalonia.Controls.Platform
protected internal virtual void KeyDown(object? sender, KeyEventArgs e)
{
- KeyDown(GetMenuItem(e.Source as Control), e);
- }
-
- protected internal virtual void KeyDown(IMenuItem? item, KeyEventArgs e)
- {
- switch (e.Key)
- {
- case Key.Up:
- case Key.Down:
- {
- if (item?.IsTopLevel == true && item.HasSubMenu)
- {
- if (!item.IsSubMenuOpen)
- {
- Open(item, true);
- }
- else
- {
- item.MoveSelection(NavigationDirection.First, true);
- }
-
- e.Handled = true;
- }
- else
- {
- goto default;
- }
- break;
- }
-
- case Key.Left:
- {
- if (item is { IsSubMenuOpen: true, SelectedItem: null })
- {
- item.Close();
- }
- else if (item?.Parent is IMenuItem { IsTopLevel: false, IsSubMenuOpen: true } parent)
- {
- parent.Close();
- parent.Focus();
- e.Handled = true;
- }
- else
- {
- goto default;
- }
- break;
- }
-
- case Key.Right:
- {
- if (item != null && !item.IsTopLevel && item.HasSubMenu)
- {
- Open(item, true);
- e.Handled = true;
- }
- else
- {
- goto default;
- }
- break;
- }
-
- case Key.Enter:
- {
- if (item != null)
- {
- if (!item.HasSubMenu)
- {
- Click(item);
- }
- else
- {
- Open(item, true);
- }
-
- e.Handled = true;
- }
- break;
- }
-
- case Key.Escape:
- {
- if (item?.Parent is IMenuElement parent)
- {
- parent.Close();
- parent.Focus();
- }
- else
- {
- Menu!.Close();
- }
-
- e.Handled = true;
- break;
- }
-
- default:
- {
- var direction = e.Key.ToNavigationDirection();
-
- if (direction?.IsDirectional() == true)
- {
- if (item == null && _isContextMenu)
- {
- if (Menu!.MoveSelection(direction.Value, true) == true)
- {
- e.Handled = true;
- }
- }
- else if (item?.Parent?.MoveSelection(direction.Value, true) == true)
- {
- // If the the parent is an IMenu which successfully moved its selection,
- // and the current menu is open then close the current menu and open the
- // new menu.
- if (item.IsSubMenuOpen &&
- item.Parent is IMenu &&
- item.Parent.SelectedItem is object &&
- item.Parent.SelectedItem != item)
- {
- item.Close();
- Open(item.Parent.SelectedItem, true);
- }
- e.Handled = true;
- }
- }
-
- break;
- }
- }
-
- if (!e.Handled && item?.Parent is IMenuItem parentItem)
- {
- KeyDown(parentItem, e);
- }
+ KeyDown(GetMenuItemCore(e.Source as Control), e);
}
protected internal virtual void AccessKeyPressed(object? sender, RoutedEventArgs e)
{
- var item = GetMenuItem(e.Source as Control);
+ var item = GetMenuItemCore(e.Source as Control);
if (item == null)
{
@@ -303,7 +98,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerEntered(object? sender, RoutedEventArgs e)
{
- var item = GetMenuItem(e.Source as Control);
+ var item = GetMenuItemCore(e.Source as Control);
if (item?.Parent == null)
{
@@ -349,7 +144,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerMoved(object? sender, PointerEventArgs e)
{
// HACK: #8179 needs to be addressed to correctly implement it in the PointerPressed method.
- var item = GetMenuItem(e.Source as Control) as MenuItem;
+ var item = GetMenuItemCore(e.Source as Control) as MenuItem;
if (item == null)
return;
@@ -370,7 +165,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerExited(object? sender, RoutedEventArgs e)
{
- var item = GetMenuItem(e.Source as Control);
+ var item = GetMenuItemCore(e.Source as Control);
if (item?.Parent == null)
{
@@ -405,7 +200,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerPressed(object? sender, PointerPressedEventArgs e)
{
- var item = GetMenuItem(e.Source as Control);
+ var item = GetMenuItemCore(e.Source as Control);
if (sender is Visual visual &&
e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed && item?.HasSubMenu == true)
@@ -436,7 +231,7 @@ namespace Avalonia.Controls.Platform
protected internal virtual void PointerReleased(object? sender, PointerReleasedEventArgs e)
{
- var item = GetMenuItem(e.Source as Control);
+ var item = GetMenuItemCore(e.Source as Control);
if (e.InitialPressMouseButton == MouseButton.Left && item?.HasSubMenu == false)
{
@@ -478,13 +273,84 @@ namespace Avalonia.Controls.Platform
{
Menu?.Close();
}
-
- private void TopLevelLostPlatformFocus()
+
+ internal static MenuItem? GetMenuItem(StyledElement? item) => (MenuItem?)GetMenuItemCore(item);
+
+ internal void AttachCore(IMenu menu)
{
- Menu?.Close();
+ if (Menu != null)
+ {
+ throw new NotSupportedException("DefaultMenuInteractionHandler is already attached.");
+ }
+
+ Menu = menu;
+ Menu.GotFocus += GotFocus;
+ Menu.LostFocus += LostFocus;
+ Menu.KeyDown += KeyDown;
+ Menu.PointerPressed += PointerPressed;
+ Menu.PointerReleased += PointerReleased;
+ Menu.AddHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
+ Menu.AddHandler(MenuBase.MenuOpenedEvent, MenuOpened);
+ Menu.AddHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
+ Menu.AddHandler(MenuItem.PointerExitedItemEvent, PointerExited);
+ Menu.AddHandler(InputElement.PointerMovedEvent, PointerMoved);
+
+ _root = Menu.VisualRoot;
+
+ if (_root is InputElement inputRoot)
+ {
+ inputRoot.AddHandler(InputElement.PointerPressedEvent, RootPointerPressed, RoutingStrategies.Tunnel);
+ }
+
+ if (_root is WindowBase window)
+ {
+ window.Deactivated += WindowDeactivated;
+ }
+
+ if (_root is TopLevel tl && tl.PlatformImpl is ITopLevelImpl pimpl)
+ pimpl.LostFocus += TopLevelLostPlatformFocus;
+
+ _inputManagerSubscription = InputManager?.Process.Subscribe(RawInput);
}
- protected void Click(IMenuItem item)
+ internal void DetachCore(IMenu menu)
+ {
+ if (Menu != menu)
+ {
+ throw new NotSupportedException("DefaultMenuInteractionHandler is not attached to the menu.");
+ }
+
+ Menu.GotFocus -= GotFocus;
+ Menu.LostFocus -= LostFocus;
+ Menu.KeyDown -= KeyDown;
+ Menu.PointerPressed -= PointerPressed;
+ Menu.PointerReleased -= PointerReleased;
+ Menu.RemoveHandler(AccessKeyHandler.AccessKeyPressedEvent, AccessKeyPressed);
+ Menu.RemoveHandler(MenuBase.MenuOpenedEvent, MenuOpened);
+ Menu.RemoveHandler(MenuItem.PointerEnteredItemEvent, PointerEntered);
+ Menu.RemoveHandler(MenuItem.PointerExitedItemEvent, PointerExited);
+ Menu.RemoveHandler(InputElement.PointerMovedEvent, PointerMoved);
+
+ if (_root is InputElement inputRoot)
+ {
+ inputRoot.RemoveHandler(InputElement.PointerPressedEvent, RootPointerPressed);
+ }
+
+ if (_root is WindowBase root)
+ {
+ root.Deactivated -= WindowDeactivated;
+ }
+
+ if (_root is TopLevel tl && tl.PlatformImpl != null)
+ tl.PlatformImpl.LostFocus -= TopLevelLostPlatformFocus;
+
+ _inputManagerSubscription?.Dispose();
+
+ Menu = null;
+ _root = null;
+ }
+
+ internal void Click(IMenuItem item)
{
item.RaiseClick();
@@ -494,7 +360,7 @@ namespace Avalonia.Controls.Platform
}
}
- protected void CloseMenu(IMenuItem item)
+ internal void CloseMenu(IMenuItem item)
{
var current = (IMenuElement?)item;
@@ -506,7 +372,7 @@ namespace Avalonia.Controls.Platform
current?.Close();
}
- protected void CloseWithDelay(IMenuItem item)
+ internal void CloseWithDelay(IMenuItem item)
{
void Execute()
{
@@ -519,7 +385,141 @@ namespace Avalonia.Controls.Platform
DelayRun(Execute, MenuShowDelay);
}
- protected void Open(IMenuItem item, bool selectFirst)
+ internal void KeyDown(IMenuItem? item, KeyEventArgs e)
+ {
+ switch (e.Key)
+ {
+ case Key.Up:
+ case Key.Down:
+ {
+ if (item?.IsTopLevel == true && item.HasSubMenu)
+ {
+ if (!item.IsSubMenuOpen)
+ {
+ Open(item, true);
+ }
+ else
+ {
+ item.MoveSelection(NavigationDirection.First, true);
+ }
+
+ e.Handled = true;
+ }
+ else
+ {
+ goto default;
+ }
+ break;
+ }
+
+ case Key.Left:
+ {
+ if (item is { IsSubMenuOpen: true, SelectedItem: null })
+ {
+ item.Close();
+ }
+ else if (item?.Parent is IMenuItem { IsTopLevel: false, IsSubMenuOpen: true } parent)
+ {
+ parent.Close();
+ parent.Focus();
+ e.Handled = true;
+ }
+ else
+ {
+ goto default;
+ }
+ break;
+ }
+
+ case Key.Right:
+ {
+ if (item != null && !item.IsTopLevel && item.HasSubMenu)
+ {
+ Open(item, true);
+ e.Handled = true;
+ }
+ else
+ {
+ goto default;
+ }
+ break;
+ }
+
+ case Key.Enter:
+ {
+ if (item != null)
+ {
+ if (!item.HasSubMenu)
+ {
+ Click(item);
+ }
+ else
+ {
+ Open(item, true);
+ }
+
+ e.Handled = true;
+ }
+ break;
+ }
+
+ case Key.Escape:
+ {
+ if (item?.Parent is IMenuElement parent)
+ {
+ parent.Close();
+ parent.Focus();
+ }
+ else
+ {
+ Menu!.Close();
+ }
+
+ e.Handled = true;
+ break;
+ }
+
+ default:
+ {
+ var direction = e.Key.ToNavigationDirection();
+
+ if (direction?.IsDirectional() == true)
+ {
+ if (item == null && _isContextMenu)
+ {
+ if (Menu!.MoveSelection(direction.Value, true) == true)
+ {
+ e.Handled = true;
+ }
+ }
+ else if (item?.Parent?.MoveSelection(direction.Value, true) == true)
+ {
+ // If the the parent is an IMenu which successfully moved its selection,
+ // and the current menu is open then close the current menu and open the
+ // new menu.
+ if (item.IsSubMenuOpen &&
+ item.Parent is IMenu &&
+ item.Parent.SelectedItem is object &&
+ item.Parent.SelectedItem != item)
+ {
+ item.Close();
+ Open(item.Parent.SelectedItem, true);
+ }
+ e.Handled = true;
+ }
+ }
+
+ break;
+ }
+ }
+
+ if (!e.Handled && item?.Parent is IMenuItem parentItem)
+ {
+ KeyDown(parentItem, e);
+ }
+ }
+
+ internal void Open(IMenuItem item, bool selectFirst)
{
item.Open();
@@ -529,7 +529,7 @@ namespace Avalonia.Controls.Platform
}
}
- protected void OpenWithDelay(IMenuItem item)
+ internal void OpenWithDelay(IMenuItem item)
{
void Execute()
{
@@ -542,7 +542,7 @@ namespace Avalonia.Controls.Platform
DelayRun(Execute, MenuShowDelay);
}
- protected void SelectItemAndAncestors(IMenuItem item)
+ internal void SelectItemAndAncestors(IMenuItem item)
{
var current = (IMenuItem?)item;
@@ -553,7 +553,7 @@ namespace Avalonia.Controls.Platform
}
}
- protected static IMenuItem? GetMenuItem(StyledElement? item)
+ internal static IMenuItem? GetMenuItemCore(StyledElement? item)
{
while (true)
{
@@ -565,6 +565,11 @@ namespace Avalonia.Controls.Platform
}
}
+ private void TopLevelLostPlatformFocus()
+ {
+ Menu?.Close();
+ }
+
private static void DefaultDelayRun(Action action, TimeSpan timeSpan)
{
DispatcherTimer.RunOnce(action, timeSpan);
diff --git a/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs b/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs
index 47b5a048b0..d1d2267f05 100644
--- a/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs
+++ b/src/Avalonia.Controls/Platform/IMenuInteractionHandler.cs
@@ -12,11 +12,11 @@ namespace Avalonia.Controls.Platform
/// Attaches the interaction handler to a menu.
///
/// The menu.
- void Attach(IMenu menu);
+ void Attach(MenuBase menu);
///
/// Detaches the interaction handler from the attached menu.
///
- void Detach(IMenu menu);
+ void Detach(MenuBase menu);
}
}
diff --git a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
index eaf95b0c8c..506a62525c 100644
--- a/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
@@ -554,7 +554,7 @@ namespace Avalonia.Controls.UnitTests.Platform
var contextMenu = Mock.Of(x => x.MoveSelection(NavigationDirection.Down, true) == true);
var e = new KeyEventArgs { Key = Key.Down, Source = contextMenu };
- target.Attach(contextMenu);
+ target.AttachCore(contextMenu);
target.KeyDown(contextMenu, e);
Mock.Get(contextMenu).Verify(x => x.MoveSelection(NavigationDirection.Down, true));