Browse Source

Don't hide tooltip when pointer is over it. (#13565)

* Add failing test for #8638.

* Don't hide tooltip when pointer is over tooltip.

Fixes #8638

* Close the tooltip when pointer exits.

If the pointer has been moved from the control to the tooltip, then out of the tooltip to another control, ensure that the tooltip is closed.

* AdornedControl can be a standard CLR property.
release/11.0.6
Steven Kirk 2 years ago
committed by Max Katz
parent
commit
9e0d8a8dad
  1. 18
      src/Avalonia.Controls/ToolTip.cs
  2. 33
      src/Avalonia.Controls/ToolTipService.cs
  3. 107
      tests/Avalonia.Controls.UnitTests/ToolTipTests.cs

18
src/Avalonia.Controls/ToolTip.cs

@ -78,6 +78,9 @@ namespace Avalonia.Controls
PlacementProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged);
}
internal Control? AdornedControl { get; private set; }
internal event EventHandler? Closed;
/// <summary>
/// Gets the value of the ToolTip.Tip attached property.
/// </summary>
@ -214,14 +217,13 @@ namespace Avalonia.Controls
{
var control = (Control)e.Sender;
var newValue = (bool)e.NewValue!;
ToolTip? toolTip;
if (newValue)
{
var tip = GetTip(control);
if (tip == null) return;
toolTip = control.GetValue(ToolTipProperty);
var toolTip = control.GetValue(ToolTipProperty);
if (toolTip == null || (tip != toolTip && tip != toolTip.Content))
{
toolTip?.Close();
@ -231,15 +233,16 @@ namespace Avalonia.Controls
toolTip.SetValue(ThemeVariant.RequestedThemeVariantProperty, control.ActualThemeVariant);
}
toolTip.AdornedControl = control;
toolTip.Open(control);
toolTip?.UpdatePseudoClasses(newValue);
}
else
else if (control.GetValue(ToolTipProperty) is { } toolTip)
{
toolTip = control.GetValue(ToolTipProperty);
toolTip?.Close();
toolTip.AdornedControl = null;
toolTip.Close();
toolTip?.UpdatePseudoClasses(newValue);
}
toolTip?.UpdatePseudoClasses(newValue);
}
private static void RecalculatePositionOnPropertyChanged(AvaloniaPropertyChangedEventArgs args)
@ -293,6 +296,7 @@ namespace Avalonia.Controls
_popupHost.Dispose();
_popupHost = null;
_popupHostChangedHandler?.Invoke(null);
Closed?.Invoke(this, EventArgs.Empty);
}
}

33
src/Avalonia.Controls/ToolTipService.cs

@ -1,7 +1,6 @@
using System;
using Avalonia.Input;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -105,6 +104,11 @@ namespace Avalonia.Controls
private void ControlPointerExited(object? sender, PointerEventArgs e)
{
var control = (Control)sender!;
// If the control is showing a tooltip and the pointer is over the tooltip, don't close it.
if (control.GetValue(ToolTip.ToolTipProperty) is { } tooltip && tooltip.IsPointerOver)
return;
Close(control);
}
@ -115,6 +119,27 @@ namespace Avalonia.Controls
toolTip?.RecalculatePosition(control);
}
private void ToolTipClosed(object? sender, EventArgs e)
{
if (sender is ToolTip toolTip)
{
toolTip.Closed -= ToolTipClosed;
toolTip.PointerExited -= ToolTipPointerExited;
}
}
private void ToolTipPointerExited(object? sender, PointerEventArgs e)
{
// The pointer has exited the tooltip. Close the tooltip unless the pointer is over the
// adorned control.
if (sender is ToolTip toolTip &&
toolTip.AdornedControl is { } control &&
!control.IsPointerOver)
{
Close(control);
}
}
private void StartShowTimer(int showDelay, Control control)
{
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(showDelay) };
@ -129,6 +154,12 @@ namespace Avalonia.Controls
if (control.IsAttachedToVisualTree)
{
ToolTip.SetIsOpen(control, true);
if (control.GetValue(ToolTip.ToolTipProperty) is { } tooltip)
{
tooltip.Closed += ToolTipClosed;
tooltip.PointerExited += ToolTipPointerExited;
}
}
}

107
tests/Avalonia.Controls.UnitTests/ToolTipTests.cs

@ -293,6 +293,113 @@ namespace Avalonia.Controls.UnitTests
Assert.False(ToolTip.GetIsOpen(target));
}
}
[Fact]
public void Should_Not_Close_When_Pointer_Is_Moved_Over_ToolTip()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var target = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
[ToolTip.ShowDelayProperty] = 0
};
window.Content = target;
window.ApplyStyling();
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
_mouseHelper.Enter(target);
Assert.True(ToolTip.GetIsOpen(target));
var tooltip = Assert.IsType<ToolTip>(target.GetValue(ToolTip.ToolTipProperty));
_mouseHelper.Enter(tooltip);
_mouseHelper.Leave(target);
Assert.True(ToolTip.GetIsOpen(target));
}
}
[Fact]
public void Should_Not_Close_When_Pointer_Is_Moved_From_ToolTip_To_Original_Control()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var target = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
[ToolTip.ShowDelayProperty] = 0
};
window.Content = target;
window.ApplyStyling();
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
_mouseHelper.Enter(target);
Assert.True(ToolTip.GetIsOpen(target));
var tooltip = Assert.IsType<ToolTip>(target.GetValue(ToolTip.ToolTipProperty));
_mouseHelper.Enter(tooltip);
_mouseHelper.Leave(target);
Assert.True(ToolTip.GetIsOpen(target));
_mouseHelper.Enter(target);
_mouseHelper.Leave(tooltip);
Assert.True(ToolTip.GetIsOpen(target));
}
}
[Fact]
public void Should_Close_When_Pointer_Is_Moved_From_ToolTip_To_Another_Control()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var target = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
[ToolTip.ShowDelayProperty] = 0
};
var other = new Decorator();
var panel = new StackPanel
{
Children = { target, other }
};
window.Content = panel;
window.ApplyStyling();
window.ApplyTemplate();
window.Presenter.ApplyTemplate();
_mouseHelper.Enter(target);
Assert.True(ToolTip.GetIsOpen(target));
var tooltip = Assert.IsType<ToolTip>(target.GetValue(ToolTip.ToolTipProperty));
_mouseHelper.Enter(tooltip);
_mouseHelper.Leave(target);
Assert.True(ToolTip.GetIsOpen(target));
_mouseHelper.Enter(other);
_mouseHelper.Leave(tooltip);
Assert.False(ToolTip.GetIsOpen(target));
}
}
}
internal class ToolTipViewModel

Loading…
Cancel
Save