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; _popup.KeyUp += PopupKeyUp;
} }
if (_popup.Parent != control) _popup.SetPopupParent(control);
{
((ISetLogicalParent)_popup).SetParent(null);
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.Placement = placement; _popup.Placement = placement;
@ -383,7 +379,7 @@ namespace Avalonia.Controls
if (_attachedControls is null || _attachedControls.Count == 0) if (_attachedControls is null || _attachedControls.Count == 0)
{ {
((ISetLogicalParent)_popup!).SetParent(null); _popup!.SetPopupParent(null);
} }
RaiseEvent(new RoutedEventArgs RaiseEvent(new RoutedEventArgs

16
src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs

@ -189,7 +189,8 @@ namespace Avalonia.Controls.Primitives
IsOpen = false; IsOpen = false;
Popup.IsOpen = false; Popup.IsOpen = false;
((ISetLogicalParent)Popup).SetParent(null); Popup.PlacementTarget = null;
Popup.SetPopupParent(null);
// Ensure this isn't active // Ensure this isn't active
_transientDisposable?.Dispose(); _transientDisposable?.Dispose();
@ -230,17 +231,8 @@ namespace Avalonia.Controls.Primitives
} }
} }
if (Popup.Parent != null && Popup.Parent != placementTarget) Popup.PlacementTarget = Target = placementTarget;
{ Popup.SetPopupParent(placementTarget);
((ISetLogicalParent)Popup).SetParent(null);
}
if (Popup.Parent == null || Popup.PlacementTarget != placementTarget)
{
Popup.PlacementTarget = Target = placementTarget;
((ISetLogicalParent)Popup).SetParent(placementTarget);
Popup.TemplatedParent = placementTarget.TemplatedParent;
}
if (Popup.Child == null) 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.Controls.Primitives.PopupPositioning;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.VisualTree; using Avalonia.VisualTree;
@ -135,7 +136,9 @@ namespace Avalonia.Controls.Primitives
} }
double IManagedPopupPositionerPopup.Scaling => 1; 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) public static IPopupHost CreatePopupHost(Visual target, IAvaloniaDependencyResolver? dependencyResolver)
{ {
if (TopLevel.GetTopLevel(target) is { } topLevel && topLevel.PlatformImpl?.CreatePopup() is { } popupImpl) 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,
(x, handler) => x.TemplateApplied -= handler).DisposeWith(handlerCleanup); (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) if (topLevel is Window window && window.PlatformImpl != null)
{ {
SubscribeToEventHandler<Window, EventHandler>(window, WindowDeactivated, 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) private void UpdateHostPosition(IPopupHost popupHost, Control placementTarget)
{ {
popupHost.ConfigurePosition( popupHost.ConfigurePosition(
@ -754,6 +775,11 @@ namespace Avalonia.Controls.Primitives
} }
} }
private void TargetDetached(object? sender, VisualTreeAttachmentEventArgs e)
{
Close();
}
private static void PassThroughEvent(PointerPressedEventArgs e) private static void PassThroughEvent(PointerPressedEventArgs e)
{ {
if (e.Source is LightDismissOverlayLayer layer && if (e.Source is LightDismissOverlayLayer layer &&

84
src/Avalonia.Controls/ToolTip.cs

@ -1,4 +1,5 @@
using System; using System;
using System.ComponentModel;
using Avalonia.Controls.Diagnostics; using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Metadata; using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
@ -79,8 +80,9 @@ namespace Avalonia.Controls
internal static readonly AttachedProperty<ToolTip?> ToolTipProperty = internal static readonly AttachedProperty<ToolTip?> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip?>("ToolTip"); AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip?>("ToolTip");
private IPopupHost? _popupHost; private Popup? _popup;
private Action<IPopupHost?>? _popupHostChangedHandler; private Action<IPopupHost?>? _popupHostChangedHandler;
private CompositeDisposable? _subscriptions;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ToolTip"/> class. /// Initializes static members of the <see cref="ToolTip"/> class.
@ -88,10 +90,6 @@ namespace Avalonia.Controls
static ToolTip() static ToolTip()
{ {
IsOpenProperty.Changed.Subscribe(IsOpenChanged); IsOpenProperty.Changed.Subscribe(IsOpenChanged);
HorizontalOffsetProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged);
VerticalOffsetProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged);
PlacementProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged);
} }
internal Control? AdornedControl { get; private set; } internal Control? AdornedControl { get; private set; }
@ -309,21 +307,9 @@ namespace Avalonia.Controls
} }
} }
private static void RecalculatePositionOnPropertyChanged(AvaloniaPropertyChangedEventArgs args) IPopupHost? IPopupHostProvider.PopupHost => _popup?.Host;
{
var control = (Control)args.Sender;
var tooltip = control.GetValue(ToolTipProperty);
if (tooltip == null)
{
return;
}
tooltip.RecalculatePosition(control); internal IPopupHost? PopupHost => _popup?.Host;
}
IPopupHost? IPopupHostProvider.PopupHost => _popupHost;
internal IPopupHost? PopupHost => _popupHost;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{ {
@ -331,47 +317,61 @@ namespace Avalonia.Controls
remove => _popupHostChangedHandler -= value; remove => _popupHostChangedHandler -= value;
} }
internal void RecalculatePosition(Control control)
{
_popupHost?.ConfigurePosition(control, GetPlacement(control), new Point(GetHorizontalOffset(control), GetVerticalOffset(control)));
}
private void Open(Control control) private void Open(Control control)
{ {
Close(); Close();
if (_popup is null)
{
_popup = new Popup();
_popup.Child = this;
_popup.WindowManagerAddShadowHint = false;
_popupHost = OverlayPopupHost.CreatePopupHost(control, null); _popup.Opened += OnPopupOpened;
_popupHost.SetChild(this); _popup.Closed += OnPopupClosed;
((ISetLogicalParent)_popupHost).SetParent(control); }
ApplyTemplatedParent(this, control.TemplatedParent);
_popupHost.ConfigurePosition(control, GetPlacement(control), _subscriptions = new CompositeDisposable(new[]
new Point(GetHorizontalOffset(control), GetVerticalOffset(control))); {
_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(); _popup.IsOpen = true;
_popupHostChangedHandler?.Invoke(_popupHost);
} }
private void Close() private void Close()
{ {
if (_popupHost != null) _subscriptions?.Dispose();
if (_popup is not null)
{ {
_popupHost.SetChild(null); _popup.IsOpen = false;
_popupHost.Dispose(); _popup.SetPopupParent(null);
_popupHost = null; _popup.PlacementTarget = null;
_popupHostChangedHandler?.Invoke(null);
Closed?.Invoke(this, EventArgs.Empty);
} }
} }
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) private void UpdatePseudoClasses(bool newValue)

34
src/Avalonia.Controls/ToolTipService.cs

@ -22,8 +22,7 @@ namespace Avalonia.Controls
_subscriptions = new CompositeDisposable( _subscriptions = new CompositeDisposable(
inputManager.Process.Subscribe(InputManager_OnProcess), inputManager.Process.Subscribe(InputManager_OnProcess),
ToolTip.ServiceEnabledProperty.Changed.Subscribe(ServiceEnabledChanged), ToolTip.ServiceEnabledProperty.Changed.Subscribe(ServiceEnabledChanged),
ToolTip.TipProperty.Changed.Subscribe(TipChanged), ToolTip.TipProperty.Changed.Subscribe(TipChanged));
ToolTip.IsOpenProperty.Changed.Subscribe(TipOpenChanged));
} }
public void Dispose() 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) private void OnTipControlChanged(Control? oldValue, Control? newValue)
{ {
StopTimer(); 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) private void ToolTipClosed(object? sender, EventArgs e)
{ {
_lastTipCloseTime = DateTime.UtcNow.Ticks; _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] [Fact]
public void Popup_Open_Should_Raise_Single_Opened_Event() public void Popup_Open_Should_Raise_Single_Opened_Event()
{ {

Loading…
Cancel
Save