diff --git a/src/Avalonia.Controls/ContextMenu.cs b/src/Avalonia.Controls/ContextMenu.cs index 7bfe283d40..cb64cc27e6 100644 --- a/src/Avalonia.Controls/ContextMenu.cs +++ b/src/Avalonia.Controls/ContextMenu.cs @@ -324,11 +324,7 @@ namespace Avalonia.Controls _popup.KeyUp += PopupKeyUp; } - if (_popup.Parent != control) - { - ((ISetLogicalParent)_popup).SetParent(null); - ((ISetLogicalParent)_popup).SetParent(control); - } + _popup.SetPopupParent(control); _popup.Placement = placement; @@ -383,7 +379,7 @@ namespace Avalonia.Controls if (_attachedControls is null || _attachedControls.Count == 0) { - ((ISetLogicalParent)_popup!).SetParent(null); + _popup!.SetPopupParent(null); } RaiseEvent(new RoutedEventArgs diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs index 99c9f065ad..32d01eec7e 100644 --- a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -189,7 +189,8 @@ namespace Avalonia.Controls.Primitives IsOpen = false; Popup.IsOpen = false; - ((ISetLogicalParent)Popup).SetParent(null); + Popup.PlacementTarget = null; + Popup.SetPopupParent(null); // Ensure this isn't active _transientDisposable?.Dispose(); @@ -230,17 +231,8 @@ namespace Avalonia.Controls.Primitives } } - if (Popup.Parent != null && Popup.Parent != placementTarget) - { - ((ISetLogicalParent)Popup).SetParent(null); - } - - if (Popup.Parent == null || Popup.PlacementTarget != placementTarget) - { - Popup.PlacementTarget = Target = placementTarget; - ((ISetLogicalParent)Popup).SetParent(placementTarget); - Popup.TemplatedParent = placementTarget.TemplatedParent; - } + Popup.PlacementTarget = Target = placementTarget; + Popup.SetPopupParent(placementTarget); if (Popup.Child == null) { diff --git a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs index d63759cc42..43bb9b2947 100644 --- a/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs +++ b/src/Avalonia.Controls/Primitives/OverlayPopupHost.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Metadata; using Avalonia.Threading; using Avalonia.VisualTree; @@ -135,7 +136,9 @@ namespace Avalonia.Controls.Primitives } double IManagedPopupPositionerPopup.Scaling => 1; - + + // TODO12: mark PrivateAPI or internal. + [Unstable("PopupHost is consireded an internal API. Use Popup or any Popup-based controls (Flyout, Tooltip) instead.")] public static IPopupHost CreatePopupHost(Visual target, IAvaloniaDependencyResolver? dependencyResolver) { if (TopLevel.GetTopLevel(target) is { } topLevel && topLevel.PlatformImpl?.CreatePopup() is { } popupImpl) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index ef81d24d06..136f25e874 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -410,6 +410,10 @@ namespace Avalonia.Controls.Primitives (x, handler) => x.TemplateApplied += handler, (x, handler) => x.TemplateApplied -= handler).DisposeWith(handlerCleanup); + SubscribeToEventHandler>(placementTarget, TargetDetached, + (x, handler) => x.DetachedFromVisualTree += handler, + (x, handler) => x.DetachedFromVisualTree -= handler).DisposeWith(handlerCleanup); + if (topLevel is Window window && window.PlatformImpl != null) { SubscribeToEventHandler(window, WindowDeactivated, @@ -580,6 +584,23 @@ namespace Avalonia.Controls.Primitives } } + /// + /// Helper method to set popup's styling and templated parent. + /// + internal void SetPopupParent(Control? newParent) + { + if (Parent != null && Parent != newParent) + { + ((ISetLogicalParent)this).SetParent(null); + } + + if (Parent == null || PlacementTarget != newParent) + { + ((ISetLogicalParent)this).SetParent(newParent); + TemplatedParent = newParent?.TemplatedParent; + } + } + private void UpdateHostPosition(IPopupHost popupHost, Control placementTarget) { popupHost.ConfigurePosition( @@ -754,6 +775,11 @@ namespace Avalonia.Controls.Primitives } } + private void TargetDetached(object? sender, VisualTreeAttachmentEventArgs e) + { + Close(); + } + private static void PassThroughEvent(PointerPressedEventArgs e) { if (e.Source is LightDismissOverlayLayer layer && diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 99bebb79ef..245b4d74b0 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/src/Avalonia.Controls/ToolTip.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; @@ -79,8 +80,9 @@ namespace Avalonia.Controls internal static readonly AttachedProperty ToolTipProperty = AvaloniaProperty.RegisterAttached("ToolTip"); - private IPopupHost? _popupHost; + private Popup? _popup; private Action? _popupHostChangedHandler; + private CompositeDisposable? _subscriptions; /// /// Initializes static members of the class. @@ -88,10 +90,6 @@ namespace Avalonia.Controls static ToolTip() { IsOpenProperty.Changed.Subscribe(IsOpenChanged); - - HorizontalOffsetProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged); - VerticalOffsetProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged); - PlacementProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged); } internal Control? AdornedControl { get; private set; } @@ -309,21 +307,9 @@ namespace Avalonia.Controls } } - private static void RecalculatePositionOnPropertyChanged(AvaloniaPropertyChangedEventArgs args) - { - var control = (Control)args.Sender; - var tooltip = control.GetValue(ToolTipProperty); - if (tooltip == null) - { - return; - } + IPopupHost? IPopupHostProvider.PopupHost => _popup?.Host; - tooltip.RecalculatePosition(control); - } - - IPopupHost? IPopupHostProvider.PopupHost => _popupHost; - - internal IPopupHost? PopupHost => _popupHost; + internal IPopupHost? PopupHost => _popup?.Host; event Action? IPopupHostProvider.PopupHostChanged { @@ -331,47 +317,61 @@ namespace Avalonia.Controls remove => _popupHostChangedHandler -= value; } - internal void RecalculatePosition(Control control) - { - _popupHost?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); - } - private void Open(Control control) { Close(); + + if (_popup is null) + { + _popup = new Popup(); + _popup.Child = this; + _popup.WindowManagerAddShadowHint = false; - _popupHost = OverlayPopupHost.CreatePopupHost(control, null); - _popupHost.SetChild(this); - ((ISetLogicalParent)_popupHost).SetParent(control); - ApplyTemplatedParent(this, control.TemplatedParent); + _popup.Opened += OnPopupOpened; + _popup.Closed += OnPopupClosed; + } - _popupHost.ConfigurePosition(control, GetPlacement(control), - new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); + _subscriptions = new CompositeDisposable(new[] + { + _popup.Bind(Popup.HorizontalOffsetProperty, control.GetBindingObservable(HorizontalOffsetProperty)), + _popup.Bind(Popup.VerticalOffsetProperty, control.GetBindingObservable(VerticalOffsetProperty)), + _popup.Bind(Popup.PlacementProperty, control.GetBindingObservable(PlacementProperty)) + }); - WindowManagerAddShadowHintChanged(_popupHost, false); + _popup.PlacementTarget = control; + _popup.SetPopupParent(control); - _popupHost.Show(); - _popupHostChangedHandler?.Invoke(_popupHost); + _popup.IsOpen = true; } private void Close() { - if (_popupHost != null) + _subscriptions?.Dispose(); + + if (_popup is not null) { - _popupHost.SetChild(null); - _popupHost.Dispose(); - _popupHost = null; - _popupHostChangedHandler?.Invoke(null); - Closed?.Invoke(this, EventArgs.Empty); + _popup.IsOpen = false; + _popup.SetPopupParent(null); + _popup.PlacementTarget = null; } } - private void WindowManagerAddShadowHintChanged(IPopupHost host, bool hint) + private void OnPopupClosed(object? sender, EventArgs e) { - if (host is PopupRoot pr) + // This condition is true, when Popup was closed by any other reason outside of ToolTipService/ToolTip, keeping IsOpen=true. + if (AdornedControl is { } adornedControl + && GetIsOpen(adornedControl)) { - pr.WindowManagerAddShadowHint = hint; + adornedControl.SetCurrentValue(IsOpenProperty, false); } + + _popupHostChangedHandler?.Invoke(null); + Closed?.Invoke(this, EventArgs.Empty); + } + + private void OnPopupOpened(object? sender, EventArgs e) + { + _popupHostChangedHandler?.Invoke(((Popup)sender!).Host); } private void UpdatePseudoClasses(bool newValue) diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index c77f08a837..89e0083e4e 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -22,8 +22,7 @@ namespace Avalonia.Controls _subscriptions = new CompositeDisposable( inputManager.Process.Subscribe(InputManager_OnProcess), ToolTip.ServiceEnabledProperty.Changed.Subscribe(ServiceEnabledChanged), - ToolTip.TipProperty.Changed.Subscribe(TipChanged), - ToolTip.IsOpenProperty.Changed.Subscribe(TipOpenChanged)); + ToolTip.TipProperty.Changed.Subscribe(TipChanged)); } public void Dispose() @@ -122,30 +121,6 @@ namespace Avalonia.Controls } } - private void TipOpenChanged(AvaloniaPropertyChangedEventArgs e) - { - var control = (Control)e.Sender; - - if (e.OldValue is false && e.NewValue is true) - { - control.DetachedFromVisualTree += ControlDetaching; - control.EffectiveViewportChanged += ControlEffectiveViewportChanged; - } - else if (e.OldValue is true && e.NewValue is false) - { - control.DetachedFromVisualTree -= ControlDetaching; - control.EffectiveViewportChanged -= ControlEffectiveViewportChanged; - } - } - - private void ControlDetaching(object? sender, VisualTreeAttachmentEventArgs e) - { - var control = (Control)sender!; - control.DetachedFromVisualTree -= ControlDetaching; - control.EffectiveViewportChanged -= ControlEffectiveViewportChanged; - Close(control); - } - private void OnTipControlChanged(Control? oldValue, Control? newValue) { StopTimer(); @@ -184,13 +159,6 @@ namespace Avalonia.Controls } } - private void ControlEffectiveViewportChanged(object? sender, Layout.EffectiveViewportChangedEventArgs e) - { - var control = (Control)sender!; - var toolTip = control.GetValue(ToolTip.ToolTipProperty); - toolTip?.RecalculatePosition(control); - } - private void ToolTipClosed(object? sender, EventArgs e) { _lastTipCloseTime = DateTime.UtcNow.Ticks; diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 8398da59a5..e1ff48a3cc 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -221,6 +221,23 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Should_Close_When_Control_Detaches() + { + using (CreateServices()) + { + var button = new Button(); + var target = new Popup() {Placement = PlacementMode.Pointer, PlacementTarget = button}; + var root = PreparedWindow(button); + + target.Open(); + + Assert.True(target.IsOpen); + root.Content = null; + Assert.False(target.IsOpen); + } + } + [Fact] public void Popup_Open_Should_Raise_Single_Opened_Event() {