diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 78dc994df7..13f00bdc87 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -7,11 +7,20 @@ namespace Avalonia.Controls using System; using System.Reactive.Linq; using System.Linq; + using System.ComponentModel; + public class ContextMenu : SelectingItemsControl { private bool _isOpen; private Popup _popup; + /// + /// Defines the property. + /// + public static readonly DirectProperty IsOpenProperty = + AvaloniaProperty.RegisterDirect(nameof(IsOpen), o => o.IsOpen); + + /// /// Initializes static members of the class. /// @@ -22,6 +31,26 @@ namespace Avalonia.Controls MenuItem.ClickEvent.AddClassHandler(x => x.OnContextMenuClick, handledEventsToo: true); } + /// + /// Gets a value indicating whether the popup is open + /// + public bool IsOpen => _isOpen; + + /// + /// 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. /// @@ -59,12 +88,12 @@ namespace Avalonia.Controls { if (_popup != null && _popup.IsVisible) { - _popup.Close(); + _popup.IsOpen = false; } SelectedIndex = -1; - _isOpen = false; + SetAndRaise(IsOpenProperty, ref _isOpen, false); } /// @@ -89,11 +118,11 @@ namespace Avalonia.Controls } ((ISetLogicalParent)_popup).SetParent(control); - _popup.Child = control.ContextMenu; + _popup.Child = this; - _popup.Open(); + _popup.IsOpen = true; - control.ContextMenu._isOpen = true; + SetAndRaise(IsOpenProperty, ref _isOpen, true); } } @@ -118,21 +147,37 @@ namespace Avalonia.Controls var control = (Control)sender; var contextMenu = control.ContextMenu; - if (e.MouseButton == MouseButton.Right) + if (control.ContextMenu._isOpen) { - if (control.ContextMenu._isOpen) - { - control.ContextMenu.Hide(); - } + if (contextMenu.CancelClosing()) + return; - contextMenu.Show(control); + control.ContextMenu.Hide(); e.Handled = true; } - else if (contextMenu._isOpen) + + if (e.MouseButton == MouseButton.Right) { - control.ContextMenu.Hide(); + if (contextMenu.CancelOpening()) + return; + + contextMenu.Show(control); e.Handled = true; } } + + 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; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs new file mode 100644 index 0000000000..94d5caa720 --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -0,0 +1,179 @@ +using System; +using System.Windows.Input; +using Avalonia.Controls.Primitives; +using Avalonia.Data; +using Avalonia.Input; +using Avalonia.Markup.Data; +using Avalonia.Platform; +using Avalonia.UnitTests; +using Moq; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class ContextMenuTests + { + private Mock popupImpl; + + [Fact] + public void Clicking_On_Control_Toggles_ContextMenu() + { + using (Application()) + { + popupImpl.Setup(x => x.Show()).Verifiable(); + popupImpl.Setup(x => x.Hide()).Verifiable(); + + var sut = new ContextMenu(); + var target = new Panel + { + ContextMenu = sut + }; + + new Window { Content = target }; + + target.RaiseEvent(new PointerReleasedEventArgs + { + RoutedEvent = InputElement.PointerReleasedEvent, + MouseButton = MouseButton.Right + }); + + Assert.True(sut.IsOpen); + + target.RaiseEvent(new PointerReleasedEventArgs + { + RoutedEvent = InputElement.PointerReleasedEvent, + MouseButton = MouseButton.None + }); + + Assert.False(sut.IsOpen); + popupImpl.Verify(x => x.Show(), Times.Once); + popupImpl.Verify(x => x.Hide(), Times.Once); + } + } + + [Fact] + public void Right_Clicking_On_Control_Twice_Re_Opens_ContextMenu() + { + using (Application()) + { + popupImpl.Setup(x => x.Show()).Verifiable(); + popupImpl.Setup(x => x.Hide()).Verifiable(); + + var sut = new ContextMenu(); + var target = new Panel + { + ContextMenu = sut + }; + new Window { Content = target }; + + target.RaiseEvent(new PointerReleasedEventArgs + { + RoutedEvent = InputElement.PointerReleasedEvent, + MouseButton = MouseButton.Right + }); + + Assert.True(sut.IsOpen); + + target.RaiseEvent(new PointerReleasedEventArgs + { + RoutedEvent = InputElement.PointerReleasedEvent, + MouseButton = MouseButton.Right + }); + + Assert.True(sut.IsOpen); + popupImpl.Verify(x => x.Hide(), Times.Once); + popupImpl.Verify(x => x.Show(), Times.Exactly(2)); + } + } + + [Fact] + public void Cancelling_Opening_Does_Not_Show_ContextMenu() + { + using (Application()) + { + popupImpl.Setup(x => x.Show()).Verifiable(); + + bool eventCalled = false; + var sut = new ContextMenu(); + var target = new Panel + { + ContextMenu = sut + }; + new Window { Content = target }; + + sut.ContextMenuOpening += (c, e) => { eventCalled = true; e.Cancel = true; }; + + target.RaiseEvent(new PointerReleasedEventArgs + { + RoutedEvent = InputElement.PointerReleasedEvent, + MouseButton = MouseButton.Right + }); + + Assert.True(eventCalled); + Assert.False(sut.IsOpen); + popupImpl.Verify(x => x.Show(), Times.Never); + } + } + + [Fact] + public void Cancelling_Closing_Leaves_ContextMenuOpen() + { + using (Application()) + { + popupImpl.Setup(x => x.Show()).Verifiable(); + popupImpl.Setup(x => x.Hide()).Verifiable(); + + bool eventCalled = false; + var sut = new ContextMenu(); + var target = new Panel + { + ContextMenu = sut + }; + new Window { Content = target }; + + sut.ContextMenuClosing += (c, e) => { eventCalled = true; e.Cancel = true; }; + + target.RaiseEvent(new PointerReleasedEventArgs + { + RoutedEvent = InputElement.PointerReleasedEvent, + MouseButton = MouseButton.Right + }); + + Assert.True(sut.IsOpen); + + target.RaiseEvent(new PointerReleasedEventArgs + { + RoutedEvent = InputElement.PointerReleasedEvent, + MouseButton = MouseButton.None + }); + + Assert.True(eventCalled); + Assert.True(sut.IsOpen); + + popupImpl.Verify(x => x.Show(), Times.Once()); + popupImpl.Verify(x => x.Hide(), Times.Never); + } + } + + private IDisposable Application() + { + var screen = new Rect(new Point(), new Size(100, 100)); + var screenImpl = new Mock(); + screenImpl.Setup(x => x.ScreenCount).Returns(1); + screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(screen, screen, true) }); + + var windowImpl = new Mock(); + windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); + + popupImpl = new Mock(); + popupImpl.SetupGet(x => x.Scaling).Returns(1); + + var services = TestServices.StyledWindow.With( + inputManager: new InputManager(), + windowImpl: windowImpl.Object, + windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, () => popupImpl.Object)); + + return UnitTestApplication.Start(services); + } + } +}