Browse Source

Never update ToolTipService while the mouse is over a tooltip (fixes flicker when redrawing a window beneath the pointer) (#15596)

Avoid race condition where a dispatcher timer callback exectues right after we stopped the timer
Fix not closing a tooltip when its pointer exit event is the last input sent to Avalonia
pull/15640/head
Tom Edwards 2 years ago
committed by GitHub
parent
commit
76f4e7a245
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 5
      src/Avalonia.Controls/IToolTipService.cs
  2. 32
      src/Avalonia.Controls/ToolTipService.cs
  3. 4
      src/Avalonia.Controls/TopLevel.cs

5
src/Avalonia.Controls/IToolTipService.cs

@ -1,9 +1,10 @@
using Avalonia.Metadata;
using Avalonia.Input;
using Avalonia.Metadata;
namespace Avalonia.Controls;
[Unstable, PrivateApi]
internal interface IToolTipService
{
void Update(Visual? candidateToolTipHost);
void Update(IInputRoot root, Visual? candidateToolTipHost);
}

32
src/Avalonia.Controls/ToolTipService.cs

@ -17,6 +17,7 @@ namespace Avalonia.Controls
private long _lastTipCloseTime;
private DispatcherTimer? _timer;
private ulong _lastTipEventTime;
private ulong _lastWindowEventTime;
public ToolTipService(IInputManager inputManager)
{
@ -36,18 +37,23 @@ namespace Avalonia.Controls
{
if (e is RawPointerEventArgs pointerEvent)
{
bool isTooltipEvent = false;
if (_tipControl?.GetValue(ToolTip.ToolTipProperty) is { } currentTip && e.Root == currentTip.PopupHost)
{
isTooltipEvent = true;
_lastTipEventTime = pointerEvent.Timestamp;
var simultaneousTipEvent = _lastTipEventTime == pointerEvent.Timestamp;
}
else if (e.Root == _tipControl?.VisualRoot)
{
_lastWindowEventTime = pointerEvent.Timestamp;
}
switch (pointerEvent.Type)
{
// 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);
case RawPointerEventType.Move:
Update(pointerEvent.Root, pointerEvent.InputHitTestResult.element as Visual);
break;
case RawPointerEventType.LeaveWindow when e.Root == _tipControl?.VisualRoot && !simultaneousTipEvent:
case RawPointerEventType.LeaveWindow when (e.Root == _tipControl?.VisualRoot && _lastTipEventTime != e.Timestamp) || (isTooltipEvent && _lastWindowEventTime != e.Timestamp):
ClearTip();
_tipControl = null;
break;
@ -68,10 +74,16 @@ namespace Avalonia.Controls
}
}
public void Update(Visual? candidateToolTipHost)
public void Update(IInputRoot root, Visual? candidateToolTipHost)
{
var currentToolTip = _tipControl?.GetValue(ToolTip.ToolTipProperty);
if (root == currentToolTip?.VisualRoot)
{
// Don't update while the pointer is over a tooltip
return;
}
while (candidateToolTipHost != null)
{
if (candidateToolTipHost == currentToolTip) // when OverlayPopupHost is in use, the tooltip is in the same window as the host control
@ -193,7 +205,11 @@ namespace Avalonia.Controls
private void StartShowTimer(int showDelay, Control control)
{
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(showDelay), Tag = (this, control) };
_timer.Tick += (o, e) => Open(control);
_timer.Tick += (o, e) =>
{
if (_timer != null)
Open(control);
};
_timer.Start();
}

4
src/Avalonia.Controls/TopLevel.cs

@ -896,12 +896,12 @@ namespace Avalonia.Controls
private void UpdateToolTip(Rect dirtyRect)
{
if (_tooltipService != null && _pointerOverPreProcessor?.LastPosition is { } lastPos)
if (_tooltipService != null && IsPointerOver && _pointerOverPreProcessor?.LastPosition is { } lastPos)
{
var clientPoint = this.PointToClient(lastPos);
if (dirtyRect.Contains(clientPoint))
{
_tooltipService.Update(HitTester.HitTestFirst(clientPoint, this, null));
_tooltipService.Update(this, HitTester.HitTestFirst(clientPoint, this, null));
}
}
}

Loading…
Cancel
Save