Browse Source

Fix tooltips not closing when the pointer leaves the window (#15312)

release/11.1.0-rc1
Tom Edwards 2 years ago
committed by Max Katz
parent
commit
7990b4bc0e
  1. 25
      src/Avalonia.Controls/ToolTipService.cs
  2. 38
      tests/Avalonia.Controls.UnitTests/ToolTipTests.cs

25
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)
{
@ -36,25 +37,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);
}
}
}

38
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<Control> 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<Control, int>();
IInputRoot lastRoot = null;
return control =>
{
@ -411,9 +436,20 @@ namespace Avalonia.Controls.UnitTests
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);
};
}

Loading…
Cancel
Save