Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Merged
merged 6 commits into from
Apr 23, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 2 additions & 6 deletions src/Avalonia.Controls/ContextMenu.cs
Original file line number Diff line number Diff line change
Expand Up @@ -324,11 +324,7 @@ private void Open(Control control, Control placementTarget, PlacementMode placem
_popup.KeyUp += PopupKeyUp;
}

if (_popup.Parent != control)
{
((ISetLogicalParent)_popup).SetParent(null);
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.SetPopupParent(control);

_popup.Placement = placement;

Expand Down Expand Up @@ -383,7 +379,7 @@ private void PopupClosed(object? sender, EventArgs e)

if (_attachedControls is null || _attachedControls.Count == 0)
{
((ISetLogicalParent)_popup!).SetParent(null);
_popup!.SetPopupParent(null);
}

RaiseEvent(new RoutedEventArgs
Expand Down
16 changes: 4 additions & 12 deletions src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,8 @@ protected virtual bool HideCore(bool canCancel = true)
IsOpen = false;
Popup.IsOpen = false;

((ISetLogicalParent)Popup).SetParent(null);
Popup.PlacementTarget = null;
Popup.SetPopupParent(null);

// Ensure this isn't active
_transientDisposable?.Dispose();
Expand Down Expand Up @@ -228,17 +229,8 @@ protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer =
}
}

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)
{
Expand Down
5 changes: 4 additions & 1 deletion src/Avalonia.Controls/Primitives/OverlayPopupHost.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Threading;
using Avalonia.VisualTree;

Expand Down Expand Up @@ -135,7 +136,9 @@ void IManagedPopupPositionerPopup.MoveAndResize(Point devicePoint, Size virtualS
}

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)
Expand Down
26 changes: 26 additions & 0 deletions src/Avalonia.Controls/Primitives/Popup.cs
Original file line number Diff line number Diff line change
Expand Up @@ -410,6 +410,10 @@ public void Open()
(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,
Expand Down Expand Up @@ -580,6 +584,23 @@ protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs chang
}
}

/// <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(
Expand Down Expand Up @@ -754,6 +775,11 @@ private void PointerPressedDismissOverlay(object? sender, PointerPressedEventArg
}
}

private void TargetDetached(object? sender, VisualTreeAttachmentEventArgs e)
{
Close();
}

private static void PassThroughEvent(PointerPressedEventArgs e)
{
if (e.Source is LightDismissOverlayLayer layer &&
Expand Down
84 changes: 42 additions & 42 deletions src/Avalonia.Controls/ToolTip.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System;
using System.ComponentModel;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
Expand Down Expand Up @@ -79,19 +80,16 @@ public class ToolTip : ContentControl, IPopupHostProvider
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.
/// </summary>
static ToolTip()
{
IsOpenProperty.Changed.Subscribe(IsOpenChanged);

HorizontalOffsetProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged);
VerticalOffsetProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged);
PlacementProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged);
}

internal Control? AdornedControl { get; private set; }
Expand Down Expand Up @@ -309,69 +307,71 @@ private static void IsOpenChanged(AvaloniaPropertyChangedEventArgs e)
}
}

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
{
add => _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)
{
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)
Expand Down
34 changes: 1 addition & 33 deletions src/Avalonia.Controls/ToolTipService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,7 @@ public ToolTipService(IInputManager inputManager)
_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()
Expand Down Expand Up @@ -122,30 +121,6 @@ private void TipChanged(AvaloniaPropertyChangedEventArgs e)
}
}

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);
maxkatz6 marked this conversation as resolved.
Show resolved Hide resolved
}

private void OnTipControlChanged(Control? oldValue, Control? newValue)
{
StopTimer();
Expand Down Expand Up @@ -184,13 +159,6 @@ private void OnTipControlChanged(Control? oldValue, Control? newValue)
}
}

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;
Expand Down
17 changes: 17 additions & 0 deletions tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,23 @@ public void PopupRoot_Should_Be_Detached_From_Logical_Tree_When_Popup_Is_Detache
}
}

[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()
{
Expand Down
Loading