From ff021aa5d91e52f14d6a676aa9717680fa600770 Mon Sep 17 00:00:00 2001 From: Tom Edwards <109803929+TomEdwardsEnscape@users.noreply.github.com> Date: Thu, 2 May 2024 10:12:03 +0300 Subject: [PATCH] Fix tooltips not closing when the pointer leaves the window (#15312) --- src/Avalonia.Controls/ToolTipService.cs | 25 ++++++++---- .../ToolTipTests.cs | 38 ++++++++++++++++++- 2 files changed, 55 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index 89e0083e4e..b080769bee 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/src/Avalonia.Controls/ToolTipService.cs @@ -16,6 +16,7 @@ namespace Avalonia.Controls private Control? _tipControl; private long _lastTipCloseTime; private DispatcherTimer? _timer; + private ulong _lastTipEventTime; public ToolTipService(IInputManager inputManager) { @@ -35,25 +36,35 @@ namespace Avalonia.Controls { if (e is RawPointerEventArgs pointerEvent) { - if (e.Root == _tipControl?.GetValue(ToolTip.ToolTipProperty)?.PopupHost) - { - return; // pointer is over the current tooltip - } + if (_tipControl?.GetValue(ToolTip.ToolTipProperty) is { } currentTip && e.Root == currentTip.PopupHost) + _lastTipEventTime = pointerEvent.Timestamp; + + var simultaneousTipEvent = _lastTipEventTime == pointerEvent.Timestamp; switch (pointerEvent.Type) { - case RawPointerEventType.Move: + // sometimes there is a null hit test as soon as the pointer enters a tooltip + case RawPointerEventType.Move when !(simultaneousTipEvent && pointerEvent.InputHitTestResult.element == null): Update(pointerEvent.InputHitTestResult.element as Visual); break; + case RawPointerEventType.LeaveWindow when e.Root == _tipControl?.VisualRoot && !simultaneousTipEvent: + ClearTip(); + _tipControl = null; + break; case RawPointerEventType.LeftButtonDown: case RawPointerEventType.RightButtonDown: case RawPointerEventType.MiddleButtonDown: case RawPointerEventType.XButton1Down: case RawPointerEventType.XButton2Down: - StopTimer(); - _tipControl?.ClearValue(ToolTip.IsOpenProperty); + ClearTip(); break; } + + void ClearTip() + { + StopTimer(); + _tipControl?.ClearValue(ToolTip.IsOpenProperty); + } } } diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index 8b76c99df3..8dafef7e18 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -368,6 +368,30 @@ namespace Avalonia.Controls.UnitTests Assert.False(ToolTip.GetIsOpen(other)); } + [Fact] + public void Should_Close_When_Pointer_Leaves_Window() + { + using (UnitTestApplication.Start(TestServices.FocusableWindow)) + { + var target = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 0 + }; + + var mouseEnter = SetupWindowAndGetMouseEnterAction(target); + + mouseEnter(target); + Assert.True(ToolTip.GetIsOpen(target)); + + var topLevel = TopLevel.GetTopLevel(target); + topLevel.PlatformImpl.Input(new RawPointerEventArgs(s_mouseDevice, (ulong)DateTime.Now.Ticks, topLevel, + RawPointerEventType.LeaveWindow, default(RawPointerPoint), RawInputModifiers.None)); + + Assert.False(ToolTip.GetIsOpen(target)); + } + } + private Action SetupWindowAndGetMouseEnterAction(Control windowContent, [CallerMemberName] string testName = null) { var windowImpl = MockWindowingPlatform.CreateWindowMock(); @@ -390,6 +414,7 @@ namespace Avalonia.Controls.UnitTests Assert.True(windowContent.IsVisible); var controlIds = new Dictionary(); + IInputRoot lastRoot = null; return control => { @@ -411,9 +436,20 @@ namespace Avalonia.Controls.UnitTests hitTesterMock.Setup(m => m.HitTestFirst(point, window, It.IsAny>())) .Returns(control); - windowImpl.Object.Input(new RawPointerEventArgs(s_mouseDevice, (ulong)DateTime.Now.Ticks, (IInputRoot)control?.VisualRoot ?? window, + var root = (IInputRoot)control?.VisualRoot ?? window; + var timestamp = (ulong)DateTime.Now.Ticks; + + windowImpl.Object.Input(new RawPointerEventArgs(s_mouseDevice, timestamp, root, RawPointerEventType.Move, point, RawInputModifiers.None)); + if (lastRoot != null && lastRoot != root) + { + ((TopLevel)lastRoot).PlatformImpl?.Input(new RawPointerEventArgs(s_mouseDevice, timestamp, + lastRoot, RawPointerEventType.LeaveWindow, new Point(-1,-1), RawInputModifiers.None)); + } + + lastRoot = root; + Assert.True(control == null || control.IsPointerOver); }; }