diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 58b4324a3e..ca2ed2590f 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -90,6 +90,8 @@ namespace Avalonia.Controls /// The control. public void Open(Control control) { + if (control == null) + throw new ArgumentNullException(nameof(control)); if (IsOpen) { return; diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index f9ec9796fb..2163e035dc 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -215,13 +215,21 @@ namespace Avalonia.Controls.Primitives /// public void Open() { - if (PlacementTarget == null) - throw new InvalidOperationException("It's not valid to show a popup without a PlacementTarget"); - + if (PlacementTarget == null && PlacementMode != PlacementMode.Pointer) + throw new InvalidOperationException("It's not valid to show a popup without a PlacementTarget with PlacementMode != Pointer"); if (_topLevel == null && PlacementTarget != null) - _topLevel = PlacementTarget.GetSelfAndLogicalAncestors().First(x => x is TopLevel) as TopLevel; - + _topLevel = PlacementTarget.GetSelfAndLogicalAncestors().FirstOrDefault(x => x is TopLevel) as TopLevel; + + if (_topLevel == null) + { + if (PlacementTarget == null) + throw new InvalidOperationException( + "Attempted to open a popup not attached to a TopLevel and PlacementTarget is null"); + throw new InvalidOperationException( + "Attempted to open a popup not attached to a TopLevel and PlacementTarget is also not attached to a TopLevel"); + } + if (_popupRoot == null) { _popupRoot = new PopupRoot(_topLevel, DependencyResolver) @@ -255,7 +263,7 @@ namespace Avalonia.Controls.Primitives } } _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); - _nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick); + _nonClientListener = InputManager.Instance?.Process.Subscribe(ListenForNonClientClick); PopupRootCreated?.Invoke(this, EventArgs.Empty); diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index efe4d09b3d..d3dba6c908 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -146,7 +146,11 @@ namespace Avalonia.Controls.Primitives throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); var matrix = target.TransformToVisual(_parent); if (matrix == null) + { + if (target.GetVisualRoot() == null) + throw new InvalidCastException("Target control is not attached to the visual tree"); throw new InvalidCastException("Target control is not in the same tree as the popup parent"); + } _positionerParameters.AnchorRectangle = new Rect(default, target.Bounds.Size) .TransformToAABB(matrix.Value); diff --git a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs index 015a122677..ef7dc33f76 100644 --- a/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/AutoCompleteBoxTests.cs @@ -982,6 +982,8 @@ namespace Avalonia.Controls.UnitTests AutoCompleteBox control = CreateControl(); control.Items = CreateSimpleStringArray(); TextBox textBox = GetTextBox(control); + var window = new Window {Content = control}; + window.ApplyTemplate(); Dispatcher.UIThread.RunJobs(); test.Invoke(control, textBox); } @@ -1027,7 +1029,8 @@ namespace Avalonia.Controls.UnitTests var popup = new Popup { - Name = "PART_Popup" + Name = "PART_Popup", + PlacementTarget = control }.RegisterInNameScope(scope); var panel = new Panel(); diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 58d205deaa..93db620702 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -27,7 +27,7 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }; + new Window { Content = target }.ApplyTemplate(); int openedCount = 0; @@ -36,7 +36,7 @@ namespace Avalonia.Controls.UnitTests openedCount++; }; - sut.Open(null); + sut.Open(target); Assert.Equal(1, openedCount); } @@ -53,9 +53,9 @@ namespace Avalonia.Controls.UnitTests ContextMenu = sut }; - new Window { Content = target }; + new Window { Content = target }.ApplyTemplate(); - sut.Open(null); + sut.Open(target); int closedCount = 0; @@ -190,12 +190,12 @@ namespace Avalonia.Controls.UnitTests 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 = MockWindowingPlatform.CreatePopupMock(); popupImpl.SetupGet(x => x.Scaling).Returns(1); + var windowImpl = MockWindowingPlatform.CreateWindowMock(() => popupImpl.Object); + windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); + var services = TestServices.StyledWindow.With( inputManager: new InputManager(), windowImpl: windowImpl.Object, diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 44bb7cb69b..944bf1e642 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -43,13 +43,14 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { + var window = new Window(); var target = new TemplatedControlWithPopup { PopupContent = new Canvas(), }; + window.Content = target; - var root = new TestRoot { Child = target }; - + window.ApplyTemplate(); target.ApplyTemplate(); target.Popup.Open(); @@ -117,13 +118,14 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (UnitTestApplication.Start(TestServices.StyledWindow)) { + var window = new Window(); var target = new TemplatedControlWithPopup { PopupContent = new Canvas(), }; + window.Content = target; - var root = new TestRoot { Child = target }; - + window.ApplyTemplate(); target.ApplyTemplate(); target.Popup.Open(); target.PopupContent = null; @@ -158,6 +160,7 @@ namespace Avalonia.Controls.UnitTests.Primitives new Popup { [!Popup.ChildProperty] = parent[!TemplatedControlWithPopup.PopupContentProperty], + PlacementTarget = parent }); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 2e22725125..82610c91df 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -146,7 +146,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var target = new Popup(); + var target = new Popup() {PlacementTarget = new Window()}; target.Open(); @@ -159,7 +159,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var target = new Popup(); + var target = new Popup() {PlacementTarget = new Window()}; target.Open(); @@ -173,15 +173,15 @@ namespace Avalonia.Controls.UnitTests.Primitives { using (CreateServices()) { - var target = new Popup(); - var root = new TestRoot { Child = target }; + var target = new Popup() {PlacementMode = PlacementMode.Pointer}; + var root = new Window() { Content = target }; target.Open(); var popupRoot = (ILogical)target.PopupRoot; Assert.True(popupRoot.IsAttachedToLogicalTree); - root.Child = null; + root.Content = null; Assert.False(((ILogical)target).IsAttachedToLogicalTree); } } @@ -192,7 +192,7 @@ namespace Avalonia.Controls.UnitTests.Primitives using (CreateServices()) { var window = new Window(); - var target = new Popup(); + var target = new Popup() {PlacementMode = PlacementMode.Pointer}; window.Content = target; @@ -215,9 +215,10 @@ namespace Avalonia.Controls.UnitTests.Primitives using (CreateServices()) { var window = new Window(); - var target = new Popup(); + var target = new Popup() {PlacementMode = PlacementMode.Pointer}; window.Content = target; + window.ApplyTemplate(); target.Open(); int closedCount = 0; @@ -239,10 +240,11 @@ namespace Avalonia.Controls.UnitTests.Primitives using (CreateServices()) { var window = new Window(); - var target = new Popup(); + var target = new Popup {PlacementMode = PlacementMode.Pointer}; var child = new Control(); window.Content = target; + window.ApplyTemplate(); target.Open(); Assert.Single(target.PopupRoot.GetVisualChildren()); @@ -259,15 +261,16 @@ namespace Avalonia.Controls.UnitTests.Primitives using (CreateServices()) { PopupContentControl target; - var root = new TestRoot + var root = new Window() { - Child = target = new PopupContentControl + Content = target = new PopupContentControl { Content = new Border(), Template = new FuncControlTemplate(PopupContentControlTemplate), }, - StylingParent = AvaloniaLocator.Current.GetService() + //StylingParent = AvaloniaLocator.Current.GetService() }; + root.ApplyTemplate(); target.ApplyTemplate(); var popup = (Popup)target.GetTemplateChildren().First(x => x.Name == "popup"); @@ -311,6 +314,7 @@ namespace Avalonia.Controls.UnitTests.Primitives { Child = child = new TestControl(), DataContext = "foo", + PlacementTarget = new Window() }; var beginCalled = false; @@ -332,36 +336,7 @@ namespace Avalonia.Controls.UnitTests.Primitives } - private static IDisposable CreateServices() - { - var result = AvaloniaLocator.EnterScope(); - - var styles = new Styles - { - new Style(x => x.OfType()) - { - Setters = new[] - { - new Setter(TemplatedControl.TemplateProperty, new FuncControlTemplate(PopupRootTemplate)), - } - }, - }; - - var globalStyles = new Mock(); - globalStyles.Setup(x => x.IsStylesInitialized).Returns(true); - globalStyles.Setup(x => x.Styles).Returns(styles); - - var renderInterface = new Mock(); - - AvaloniaLocator.CurrentMutable - .Bind().ToFunc(() => globalStyles.Object) - .Bind().ToConstant(new WindowingPlatformMock()) - .Bind().ToTransient() - .Bind().ToFunc(() => renderInterface.Object) - .Bind().ToConstant(new InputManager()); - - return result; - } + private static IDisposable CreateServices() => UnitTestApplication.Start(TestServices.StyledWindow); private static IControl PopupRootTemplate(PopupRoot control, INameScope scope) { @@ -377,6 +352,7 @@ namespace Avalonia.Controls.UnitTests.Primitives return new Popup { Name = "popup", + PlacementTarget = control, Child = new ContentPresenter { [~ContentPresenter.ContentProperty] = control[~ContentControl.ContentProperty], diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index cbcf08049e..75239f014f 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -17,31 +17,6 @@ namespace Avalonia.Controls.UnitTests { public class WindowTests { - [Fact] - public void Impl_ClientSize_Should_Be_Set_After_Layout_Pass() - { - using (UnitTestApplication.Start(TestServices.StyledWindow)) - { - var impl = Mock.Of(x => x.Scaling == 1); - - Mock.Get(impl).Setup(x => x.Resize(It.IsAny())).Callback(() => { }); - - var target = new Window(impl) - { - Content = new TextBlock - { - Width = 321, - Height = 432, - }, - IsVisible = true, - }; - - target.LayoutManager.ExecuteInitialLayoutPass(target); - - Mock.Get(impl).Verify(x => x.Resize(new Size(321, 432))); - } - } - [Fact] public void Setting_Title_Should_Set_Impl_Title() { @@ -302,8 +277,7 @@ namespace Avalonia.Controls.UnitTests var screens = new Mock(); screens.Setup(x => x.AllScreens).Returns(new Screen[] { screen1.Object, screen2.Object }); - var windowImpl = new Mock(); - windowImpl.SetupProperty(x => x.Position); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); windowImpl.Setup(x => x.Scaling).Returns(1); windowImpl.Setup(x => x.Screen).Returns(screens.Object); @@ -327,14 +301,12 @@ namespace Avalonia.Controls.UnitTests [Fact] public void Window_Should_Be_Centered_Relative_To_Owner_When_WindowStartupLocation_Is_CenterOwner() { - var parentWindowImpl = new Mock(); - parentWindowImpl.SetupProperty(x => x.Position); + var parentWindowImpl = MockWindowingPlatform.CreateWindowMock(); parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); parentWindowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); parentWindowImpl.Setup(x => x.Scaling).Returns(1); - var windowImpl = new Mock(); - windowImpl.SetupProperty(x => x.Position); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200)); windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1920, 1080)); windowImpl.Setup(x => x.Scaling).Returns(1); diff --git a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj index f065fcb63d..a1b3ab9736 100644 --- a/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj +++ b/tests/Avalonia.UnitTests/Avalonia.UnitTests.csproj @@ -4,6 +4,7 @@ false Library false + latest diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 36297bf58b..1b47318fe1 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -1,4 +1,6 @@ using System; +using Avalonia.Controls.Primitives.PopupPositioning; +using Avalonia.Input; using Moq; using Avalonia.Platform; @@ -15,16 +17,46 @@ namespace Avalonia.UnitTests _popupImpl = popupImpl; } + public static Mock CreateWindowMock(Func popupImpl = null) + { + var win = Mock.Of(x => x.Scaling == 1); + var mock = Mock.Get(win); + mock.Setup(x => x.CreatePopup()).Returns(() => + { + return popupImpl?.Invoke() ?? CreatePopupMock().Object; + + }); + PixelPoint pos = default; + mock.SetupGet(x => x.Position).Returns(() => pos); + mock.Setup(x => x.Move(It.IsAny())).Callback(new Action(np => pos = np)); + SetupToplevel(mock); + return mock; + } + + static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl + { + mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice()); + } + + public static Mock CreatePopupMock() + { + var positioner = Mock.Of(); + var popup = Mock.Of(x => x.Scaling == 1); + var mock = Mock.Get(popup); + mock.SetupGet(x => x.PopupPositioner).Returns(positioner); + SetupToplevel(mock); + + return mock; + } + public IWindowImpl CreateWindow() { - return _windowImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); + return _windowImpl?.Invoke() ?? CreateWindowMock(_popupImpl).Object; } public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } - - public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); } -} \ No newline at end of file +}