Skip to content

Commit

Permalink
Fix tooltips not closing when the pointer leaves the window (#15312)
Browse files Browse the repository at this point in the history
  • Loading branch information
TomEdwardsEnscape authored May 2, 2024
1 parent d1d6e4f commit ff021aa
Show file tree
Hide file tree
Showing 2 changed files with 55 additions and 8 deletions.
25 changes: 18 additions & 7 deletions src/Avalonia.Controls/ToolTipService.cs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ internal sealed class ToolTipService : IToolTipService, IDisposable
private Control? _tipControl;
private long _lastTipCloseTime;
private DispatcherTimer? _timer;
private ulong _lastTipEventTime;

public ToolTipService(IInputManager inputManager)
{
Expand All @@ -35,25 +36,35 @@ private void InputManager_OnProcess(RawInputEventArgs e)
{
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);
}
}
}

Expand Down
38 changes: 37 additions & 1 deletion tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,30 @@ public void New_ToolTip_Replaces_Other_ToolTip_Immediately()
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<Control> SetupWindowAndGetMouseEnterAction(Control windowContent, [CallerMemberName] string testName = null)
{
var windowImpl = MockWindowingPlatform.CreateWindowMock();
Expand All @@ -390,6 +414,7 @@ private Action<Control> SetupWindowAndGetMouseEnterAction(Control windowContent,
Assert.True(windowContent.IsVisible);

var controlIds = new Dictionary<Control, int>();
IInputRoot lastRoot = null;

return control =>
{
Expand All @@ -411,9 +436,20 @@ private Action<Control> SetupWindowAndGetMouseEnterAction(Control windowContent,
hitTesterMock.Setup(m => m.HitTestFirst(point, window, It.IsAny<Func<Visual, bool>>()))
.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);
};
}
Expand Down

0 comments on commit ff021aa

Please sign in to comment.