diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 92293a32d6..d39cab0284 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -7,6 +7,7 @@ using Avalonia.Controls.Platform; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.LogicalTree; namespace Avalonia.Controls @@ -90,9 +91,14 @@ namespace Avalonia.Controls /// The control. public void Open(Control control) { + if (IsOpen) + { + return; + } + if (_popup == null) { - _popup = new Popup() + _popup = new Popup { PlacementMode = PlacementMode.Pointer, PlacementTarget = control, @@ -107,7 +113,14 @@ namespace Avalonia.Controls ((ISetLogicalParent)_popup).SetParent(control); _popup.Child = this; _popup.IsOpen = true; + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); } /// @@ -115,13 +128,15 @@ namespace Avalonia.Controls /// public override void Close() { + if (!IsOpen) + { + return; + } + if (_popup != null && _popup.IsVisible) { _popup.IsOpen = false; } - - SelectedIndex = -1; - IsOpen = false; } protected override IItemContainerGenerator CreateItemContainerGenerator() @@ -129,6 +144,18 @@ namespace Avalonia.Controls return new MenuItemContainerGenerator(this); } + private void CloseCore() + { + SelectedIndex = -1; + IsOpen = false; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); + } + private void PopupOpened(object sender, EventArgs e) { Focus(); @@ -145,8 +172,7 @@ namespace Avalonia.Controls i.IsSubMenuOpen = false; } - contextMenu.IsOpen = false; - contextMenu.SelectedIndex = -1; + contextMenu.CloseCore(); } } diff --git a/src/Avalonia.Controls/Menu.cs b/src/Avalonia.Controls/Menu.cs index b0fb3f2b3b..b60a97e1c8 100644 --- a/src/Avalonia.Controls/Menu.cs +++ b/src/Avalonia.Controls/Menu.cs @@ -40,37 +40,41 @@ namespace Avalonia.Controls /// public override void Close() { - if (IsOpen) + if (!IsOpen) { - foreach (var i in ((IMenu)this).SubItems) - { - i.Close(); - } - - IsOpen = false; - SelectedIndex = -1; + return; + } - RaiseEvent(new RoutedEventArgs - { - RoutedEvent = MenuClosedEvent, - Source = this, - }); + foreach (var i in ((IMenu)this).SubItems) + { + i.Close(); } + + IsOpen = false; + SelectedIndex = -1; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuClosedEvent, + Source = this, + }); } /// public override void Open() { - if (!IsOpen) + if (IsOpen) { - IsOpen = true; - - RaiseEvent(new RoutedEventArgs - { - RoutedEvent = MenuOpenedEvent, - Source = this, - }); + return; } + + IsOpen = true; + + RaiseEvent(new RoutedEventArgs + { + RoutedEvent = MenuOpenedEvent, + Source = this, + }); } /// diff --git a/src/Avalonia.Controls/MenuBase.cs b/src/Avalonia.Controls/MenuBase.cs index d6eb40360b..aeee685980 100644 --- a/src/Avalonia.Controls/MenuBase.cs +++ b/src/Avalonia.Controls/MenuBase.cs @@ -31,13 +31,13 @@ namespace Avalonia.Controls /// Defines the event. /// public static readonly RoutedEvent MenuOpenedEvent = - RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(MenuOpened), RoutingStrategies.Bubble); /// /// Defines the event. /// public static readonly RoutedEvent MenuClosedEvent = - RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); + RoutedEvent.Register(nameof(MenuClosed), RoutingStrategies.Bubble); private bool _isOpen; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index e02d46c1df..42c1a98349 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -2,6 +2,7 @@ // Licensed under the MIT license. See licence.md file in the project root for full license information. using System; +using System.Diagnostics; using System.Linq; using Avalonia.Input; using Avalonia.Input.Raw; @@ -270,9 +271,10 @@ namespace Avalonia.Controls.Primitives _popupRoot.SnapInsideScreenEdges(); } - _ignoreIsOpenChanged = true; - IsOpen = true; - _ignoreIsOpenChanged = false; + using (BeginIgnoringIsOpen()) + { + IsOpen = true; + } Opened?.Invoke(this, EventArgs.Empty); } @@ -305,7 +307,11 @@ namespace Avalonia.Controls.Primitives _popupRoot.Hide(); } - IsOpen = false; + using (BeginIgnoringIsOpen()) + { + IsOpen = false; + } + Closed?.Invoke(this, EventArgs.Empty); } @@ -467,5 +473,26 @@ namespace Avalonia.Controls.Primitives Close(); } } + + private IgnoreIsOpenScope BeginIgnoringIsOpen() + { + return new IgnoreIsOpenScope(this); + } + + private readonly struct IgnoreIsOpenScope : IDisposable + { + private readonly Popup _owner; + + public IgnoreIsOpenScope(Popup owner) + { + _owner = owner; + _owner._ignoreIsOpenChanged = true; + } + + public void Dispose() + { + _owner._ignoreIsOpenChanged = false; + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 6482fcb4da..58d205deaa 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -16,6 +16,60 @@ namespace Avalonia.Controls.UnitTests private Mock popupImpl; private MouseTestHelper _mouse = new MouseTestHelper(); + [Fact] + public void Opening_Raises_Single_Opened_Event() + { + using (Application()) + { + var sut = new ContextMenu(); + var target = new Panel + { + ContextMenu = sut + }; + + new Window { Content = target }; + + int openedCount = 0; + + sut.MenuOpened += (sender, args) => + { + openedCount++; + }; + + sut.Open(null); + + Assert.Equal(1, openedCount); + } + } + + [Fact] + public void Closing_Raises_Single_Closed_Event() + { + using (Application()) + { + var sut = new ContextMenu(); + var target = new Panel + { + ContextMenu = sut + }; + + new Window { Content = target }; + + sut.Open(null); + + int closedCount = 0; + + sut.MenuClosed += (sender, args) => + { + closedCount++; + }; + + sut.Close(); + + Assert.Equal(1, closedCount); + } + } + [Fact] public void Clicking_On_Control_Toggles_ContextMenu() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index e266150901..2e22725125 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -3,6 +3,7 @@ using System; using System.Collections.Specialized; +using System.Diagnostics; using System.Linq; using Moq; using Avalonia.Controls.Presenters; @@ -185,6 +186,53 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Popup_Open_Should_Raise_Single_Opened_Event() + { + using (CreateServices()) + { + var window = new Window(); + var target = new Popup(); + + window.Content = target; + + int openedCount = 0; + + target.Opened += (sender, args) => + { + openedCount++; + }; + + target.Open(); + + Assert.Equal(1, openedCount); + } + } + + [Fact] + public void Popup_Close_Should_Raise_Single_Closed_Event() + { + using (CreateServices()) + { + var window = new Window(); + var target = new Popup(); + + window.Content = target; + target.Open(); + + int closedCount = 0; + + target.Closed += (sender, args) => + { + closedCount++; + }; + + target.Close(); + + Assert.Equal(1, closedCount); + } + } + [Fact] public void PopupRoot_Should_Have_Template_Applied() {