Browse Source

ToolTip should use Popup internally + fix Popups not closing when placement target is closed (#15358)

* Add Popup.SetPopupParent helper method

* Update Tooltip to use Popup internally instead of PopupRoot

* Mark OverlayPopupHost.CreatePopupHost as unstable method

* Close tooltip when popup is closed

* Fix popups not closing when target is detached

* Remove unrelated change
pull/15485/head
Max Katz 2 years ago
committed by GitHub
parent
commit
296b366e78
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 8
      src/Avalonia.Controls/ContextMenu.cs
  2. 16
      src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
  3. 5
      src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
  4. 26
      src/Avalonia.Controls/Primitives/Popup.cs
  5. 84
      src/Avalonia.Controls/ToolTip.cs
  6. 34
      src/Avalonia.Controls/ToolTipService.cs
  7. 17
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

8
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

16
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)
{

5
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)

26
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<Control, EventHandler<VisualTreeAttachmentEventArgs>>(placementTarget, TargetDetached,
(x, handler) => x.DetachedFromVisualTree += handler,
(x, handler) => x.DetachedFromVisualTree -= handler).DisposeWith(handlerCleanup);
if (topLevel is Window window && window.PlatformImpl != null)
{
SubscribeToEventHandler<Window, EventHandler>(window, WindowDeactivated,
@ -580,6 +584,23 @@ namespace Avalonia.Controls.Primitives
}
}
/// <summary>
/// Helper method to set popup's styling and templated parent.
/// </summary>
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 &&

84
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<ToolTip?> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip?>("ToolTip");
private IPopupHost? _popupHost;
private Popup? _popup;
private Action<IPopupHost?>? _popupHostChangedHandler;
private CompositeDisposable? _subscriptions;
/// <summary>
/// Initializes static members of the <see cref="ToolTip"/> 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<IPopupHost?>? 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)

34
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;

17
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()
{

Loading…
Cancel
Save