diff --git a/src/Avalonia.Controls/IToolTipService.cs b/src/Avalonia.Controls/IToolTipService.cs index 3188434850..f3082c0e24 100644 --- a/src/Avalonia.Controls/IToolTipService.cs +++ b/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); } diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs index b080769bee..c3231fcde9 100644 --- a/src/Avalonia.Controls/ToolTipService.cs +++ b/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(); } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 404964d374..53821c7a5c 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/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)); } } }