From a10a45e71c7c37828b16ef4e8fe8244b489f5ded Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 24 Jan 2022 09:32:01 +0000 Subject: [PATCH] Merge pull request #7250 from AvaloniaUI/fixex/pointer-popup-positioning Fix pointer popup positioning for overlay popups --- src/Avalonia.Controls/Primitives/Popup.cs | 22 +- .../Primitives/PopupTests.cs | 237 +++++++++++++++++- 2 files changed, 237 insertions(+), 22 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 30e90267db..d66260e966 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -427,16 +427,20 @@ namespace Avalonia.Controls.Primitives (x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler).DisposeWith(handlerCleanup); - SubscribeToEventHandler>(window.PlatformImpl, WindowPositionChanged, - (x, handler) => x.PositionChanged += handler, - (x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup); - - if (placementTarget is Layoutable layoutTarget) + // Recalculate popup position on parent moved/resized, but not if placement was on pointer + if (PlacementMode != PlacementMode.Pointer) { - // If the placement target is moved, update the popup position - SubscribeToEventHandler(layoutTarget, PlacementTargetLayoutUpdated, - (x, handler) => x.LayoutUpdated += handler, - (x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup); + SubscribeToEventHandler>(window.PlatformImpl, WindowPositionChanged, + (x, handler) => x.PositionChanged += handler, + (x, handler) => x.PositionChanged -= handler).DisposeWith(handlerCleanup); + + if (placementTarget is Layoutable layoutTarget) + { + // If the placement target is moved, update the popup position + SubscribeToEventHandler(layoutTarget, PlacementTargetLayoutUpdated, + (x, handler) => x.LayoutUpdated += handler, + (x, handler) => x.LayoutUpdated -= handler).DisposeWith(handlerCleanup); + } } } else if (topLevel is PopupRoot parentPopupRoot) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 24e4631aff..9c1822ff5c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -16,6 +16,8 @@ using Avalonia.VisualTree; using Xunit; using Avalonia.Input; using Avalonia.Rendering; +using System.Threading.Tasks; +using Avalonia.Threading; namespace Avalonia.Controls.UnitTests.Primitives { @@ -597,27 +599,44 @@ namespace Avalonia.Controls.UnitTests.Primitives } [Fact] - public void Popup_Should_Follow_Placement_Target_On_Window_Move() + public void Popup_Should_Not_Follow_Placement_Target_On_Window_Move_If_Pointer() { using (CreateServices()) { - var popup = new Popup { Width = 400, Height = 200 }; + var popup = new Popup + { + Width = 400, + Height = 200, + PlacementMode = PlacementMode.Pointer + }; var window = PreparedWindow(popup); + window.Show(); popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + var raised = false; if (popup.Host is PopupRoot popupRoot) { - // Moving the window must move the popup (screen coordinates have changed) - var raised = false; popupRoot.PositionChanged += (_, args) => { - Assert.Equal(new PixelPoint(10, 10), args.Point); raised = true; }; - window.Position = new PixelPoint(10, 10); - Assert.True(raised); } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if (args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + { + raised = true; + } + }; + } + window.Position = new PixelPoint(10, 10); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.False(raised); } } @@ -634,30 +653,222 @@ namespace Avalonia.Controls.UnitTests.Primitives HorizontalAlignment = HorizontalAlignment.Center, VerticalAlignment = VerticalAlignment.Center }; - var popup = new Popup() { PlacementTarget = placementTarget, Width = 10, Height = 10 }; + var popup = new Popup() + { + PlacementTarget = placementTarget, + PlacementMode = PlacementMode.Bottom, + Width = 10, + Height = 10 + }; ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); var window = PreparedWindow(placementTarget); window.Show(); popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + var raised = false; + // Resizing the window to 700x500 must move the popup to (345,255) as this is the new + // location of the placement target if (popup.Host is PopupRoot popupRoot) { - // Resizing the window to 700x500 must move the popup to (345,255) as this is the new - // location of the placement target - var raised = false; popupRoot.PositionChanged += (_, args) => { Assert.Equal(new PixelPoint(345, 255), args.Point); raised = true; }; - window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); - Assert.True(raised); } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if ((args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + && Canvas.GetLeft(overlayPopupHost) == 345 + && Canvas.GetTop(overlayPopupHost) == 255) + { + raised = true; + } + }; + } + window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.True(raised); + } + } + + [Fact] + public void Popup_Should_Not_Follow_Placement_Target_On_Window_Resize_If_Pointer_If_Pointer() + { + using (CreateServices()) + { + + var placementTarget = new Panel() + { + Width = 10, + Height = 10, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + var popup = new Popup() + { + PlacementTarget = placementTarget, + PlacementMode = PlacementMode.Pointer, + Width = 10, + Height = 10 + }; + ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); + + var window = PreparedWindow(placementTarget); + window.Show(); + popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + + // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window + Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + + var raised = false; + if (popup.Host is PopupRoot popupRoot) + { + popupRoot.PositionChanged += (_, args) => + { + raised = true; + }; + + } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if (args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + { + raised = true; + } + }; + } + window.PlatformImpl?.Resize(new Size(700D, 500D), PlatformResizeReason.Unspecified); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.False(raised); + } + } + + [Fact] + public void Popup_Should_Follow_Placement_Target_On_Target_Moved() + { + using (CreateServices()) + { + var placementTarget = new Panel() + { + Width = 10, + Height = 10, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + var popup = new Popup() + { + PlacementTarget = placementTarget, + PlacementMode = PlacementMode.Bottom, + Width = 10, + Height = 10 + }; + ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); + + var window = PreparedWindow(placementTarget); + window.Show(); + popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + + // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window + Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + + var raised = false; + // Margin will move placement target + if (popup.Host is PopupRoot popupRoot) + { + popupRoot.PositionChanged += (_, args) => + { + Assert.Equal(new PixelPoint(400, 305), args.Point); + raised = true; + }; + + } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if ((args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + && Canvas.GetLeft(overlayPopupHost) == 400 + && Canvas.GetTop(overlayPopupHost) == 305) + { + raised = true; + } + }; + } + placementTarget.Margin = new Thickness(10, 0, 0, 0); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.True(raised); + } + } + + [Fact] + public void Popup_Should_Not_Follow_Placement_Target_On_Target_Moved_If_Pointer() + { + using (CreateServices()) + { + + var placementTarget = new Panel() + { + Width = 10, + Height = 10, + HorizontalAlignment = HorizontalAlignment.Center, + VerticalAlignment = VerticalAlignment.Center + }; + var popup = new Popup() + { + PlacementTarget = placementTarget, + PlacementMode = PlacementMode.Pointer, + Width = 10, + Height = 10 + }; + ((ISetLogicalParent)popup).SetParent(popup.PlacementTarget); + + var window = PreparedWindow(placementTarget); + window.Show(); + popup.Open(); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + + // The target's initial placement is (395,295) which is a 10x10 panel centered in a 800x600 window + Assert.Equal(placementTarget.Bounds, new Rect(395D, 295D, 10, 10)); + + var raised = false; + if (popup.Host is PopupRoot popupRoot) + { + popupRoot.PositionChanged += (_, args) => + { + raised = true; + }; + + } + else if (popup.Host is OverlayPopupHost overlayPopupHost) + { + overlayPopupHost.PropertyChanged += (_, args) => + { + if (args.Property == Canvas.TopProperty + || args.Property == Canvas.LeftProperty) + { + raised = true; + } + }; + } + placementTarget.Margin = new Thickness(10, 0, 0, 0); + Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout); + Assert.False(raised); } }