diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs index 5c6e35b567..8e0ca62131 100644 --- a/src/Avalonia.Controls/ToolTip.cs +++ b/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; + /// /// Gets the value of the ToolTip.Tip attached property. /// @@ -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); } } diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index d983309a72..d7b0d1202c 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/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; + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index 74e506105d..98a131752a 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/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(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(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(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