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));