diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml
index 013c176bca..fa40bc1a0f 100644
--- a/samples/ControlCatalog/Pages/ToolTipPage.xaml
+++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml
@@ -5,10 +5,14 @@
Spacing="4">
A control which pops up a hint when a control is hovered
-
+
ToolTip bottom placement
Moving offset
+
+
+
+
+ Nested ToolTips
+
diff --git a/src/Avalonia.Base/Input/InputExtensions.cs b/src/Avalonia.Base/Input/InputExtensions.cs
index 73dcef9e95..a5107588c9 100644
--- a/src/Avalonia.Base/Input/InputExtensions.cs
+++ b/src/Avalonia.Base/Input/InputExtensions.cs
@@ -13,36 +13,45 @@ namespace Avalonia.Input
public static class InputExtensions
{
private static readonly Func s_hitTestDelegate = IsHitTestVisible;
+ private static readonly Func s_hitTestEnabledOnlyDelegate = IsHitTestVisible_EnabledOnly;
///
/// Returns the active input elements at a point on an .
///
/// The element to test.
/// The point on .
+ /// Whether to only return elements for which is true.
///
/// The active input elements found at the point, ordered topmost first.
///
- public static IEnumerable GetInputElementsAt(this IInputElement element, Point p)
+ public static IEnumerable GetInputElementsAt(this IInputElement element, Point p, bool enabledElementsOnly = true)
{
element = element ?? throw new ArgumentNullException(nameof(element));
- return (element as Visual)?.GetVisualsAt(p, s_hitTestDelegate).Cast() ??
+ return (element as Visual)?.GetVisualsAt(p, enabledElementsOnly ? s_hitTestEnabledOnlyDelegate : s_hitTestDelegate).Cast() ??
Enumerable.Empty();
}
+
+ ///
+ public static IEnumerable GetInputElementsAt(this IInputElement element, Point p) => GetInputElementsAt(element, p, true);
///
/// Returns the topmost active input element at a point on an .
///
/// The element to test.
/// The point on .
+ /// Whether to only return elements for which is true.
/// The topmost at the specified position.
- public static IInputElement? InputHitTest(this IInputElement element, Point p)
+ public static IInputElement? InputHitTest(this IInputElement element, Point p, bool enabledElementsOnly = true)
{
element = element ?? throw new ArgumentNullException(nameof(element));
- return (element as Visual)?.GetVisualAt(p, s_hitTestDelegate) as IInputElement;
+ return (element as Visual)?.GetVisualAt(p, enabledElementsOnly ? s_hitTestEnabledOnlyDelegate : s_hitTestDelegate) as IInputElement;
}
+ ///
+ public static IInputElement? InputHitTest(this IInputElement element, Point p) => InputHitTest(element, p, true);
+
///
/// Returns the topmost active input element at a point on an .
///
@@ -52,26 +61,26 @@ namespace Avalonia.Input
/// A filter predicate. If the predicate returns false then the visual and all its
/// children will be excluded from the results.
///
+ /// Whether to only return elements for which is true.
/// The topmost at the specified position.
public static IInputElement? InputHitTest(
this IInputElement element,
Point p,
- Func filter)
+ Func filter,
+ bool enabledElementsOnly = true)
{
element = element ?? throw new ArgumentNullException(nameof(element));
filter = filter ?? throw new ArgumentNullException(nameof(filter));
+ var hitTestDelegate = enabledElementsOnly ? s_hitTestEnabledOnlyDelegate : s_hitTestDelegate;
- return (element as Visual)?.GetVisualAt(p, x => s_hitTestDelegate(x) && filter(x)) as IInputElement;
+ return (element as Visual)?.GetVisualAt(p, x => hitTestDelegate(x) && filter(x)) as IInputElement;
}
- private static bool IsHitTestVisible(Visual visual)
- {
- var element = visual as IInputElement;
- return element != null &&
- visual.IsVisible &&
- element.IsHitTestVisible &&
- element.IsEffectivelyEnabled &&
- visual.IsAttachedToVisualTree;
- }
+ ///
+ public static IInputElement? InputHitTest(this IInputElement element, Point p, Func filter) => InputHitTest(element, p, filter, true);
+
+ private static bool IsHitTestVisible(Visual visual) => visual is { IsVisible: true, IsAttachedToVisualTree: true } and IInputElement { IsHitTestVisible: true };
+
+ private static bool IsHitTestVisible_EnabledOnly(Visual visual) => IsHitTestVisible(visual) && visual is IInputElement { IsEffectivelyEnabled: true };
}
}
diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs
index 0cabf02566..b97036c7dd 100644
--- a/src/Avalonia.Base/Input/MouseDevice.cs
+++ b/src/Avalonia.Base/Input/MouseDevice.cs
@@ -75,9 +75,9 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Down:
case RawPointerEventType.XButton2Down:
if (ButtonCount(props) > 1)
- e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult);
+ e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult.firstEnabledAncestor);
else
- e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
+ e.Handled = MouseDown(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult.firstEnabledAncestor);
break;
case RawPointerEventType.LeftButtonUp:
case RawPointerEventType.RightButtonUp:
@@ -85,24 +85,24 @@ namespace Avalonia.Input
case RawPointerEventType.XButton1Up:
case RawPointerEventType.XButton2Up:
if (ButtonCount(props) != 0)
- e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult);
+ e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult.firstEnabledAncestor);
else
- e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
+ e.Handled = MouseUp(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult.firstEnabledAncestor);
break;
case RawPointerEventType.Move:
- e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult);
+ e.Handled = MouseMove(mouse, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.IntermediatePoints, e.InputHitTestResult.firstEnabledAncestor);
break;
case RawPointerEventType.Wheel:
- e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
+ e.Handled = MouseWheel(mouse, e.Timestamp, e.Root, e.Position, props, ((RawMouseWheelEventArgs)e).Delta, keyModifiers, e.InputHitTestResult.firstEnabledAncestor);
break;
case RawPointerEventType.Magnify:
- e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
+ e.Handled = GestureMagnify(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult.firstEnabledAncestor);
break;
case RawPointerEventType.Rotate:
- e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
+ e.Handled = GestureRotate(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult.firstEnabledAncestor);
break;
case RawPointerEventType.Swipe:
- e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult);
+ e.Handled = GestureSwipe(mouse, e.Timestamp, e.Root, e.Position, props, ((RawPointerGestureEventArgs)e).Delta, keyModifiers, e.InputHitTestResult.firstEnabledAncestor);
break;
}
}
diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs
index 7637de05ac..128a49e13b 100644
--- a/src/Avalonia.Base/Input/PenDevice.cs
+++ b/src/Avalonia.Base/Input/PenDevice.cs
@@ -64,17 +64,17 @@ namespace Avalonia.Input
shouldReleasePointer = true;
break;
case RawPointerEventType.LeftButtonDown:
- e.Handled = PenDown(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
+ e.Handled = PenDown(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult.firstEnabledAncestor);
break;
case RawPointerEventType.LeftButtonUp:
if (_releasePointerOnPenUp)
{
shouldReleasePointer = true;
}
- e.Handled = PenUp(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult);
+ e.Handled = PenUp(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult.firstEnabledAncestor);
break;
case RawPointerEventType.Move:
- e.Handled = PenMove(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult, e.IntermediatePoints);
+ e.Handled = PenMove(pointer, e.Timestamp, e.Root, e.Position, props, keyModifiers, e.InputHitTestResult.firstEnabledAncestor, e.IntermediatePoints);
break;
}
}
diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs
index 347ba35a41..d5a716ba60 100644
--- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs
+++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs
@@ -67,7 +67,7 @@ namespace Avalonia.Input
else if (pointerDevice.TryGetPointer(args) is { } pointer &&
pointer.Type != PointerType.Touch)
{
- var element = pointer.Captured ?? args.InputHitTestResult;
+ var element = pointer.Captured ?? args.InputHitTestResult.firstEnabledAncestor;
SetPointerOver(pointer, args.Root, element, args.Timestamp, args.Position,
new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
diff --git a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
index 595f4c0c66..1a13d37112 100644
--- a/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
+++ b/src/Avalonia.Base/Input/Raw/RawPointerEventArgs.cs
@@ -123,7 +123,7 @@ namespace Avalonia.Input.Raw
///
public Lazy?>? IntermediatePoints { get; set; }
- internal IInputElement? InputHitTestResult { get; set; }
+ internal (IInputElement? element, IInputElement? firstEnabledAncestor) InputHitTestResult { get; set; }
}
[PrivateApi]
diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs
index 04c444f441..026af11b05 100644
--- a/src/Avalonia.Base/Input/TouchDevice.cs
+++ b/src/Avalonia.Base/Input/TouchDevice.cs
@@ -44,14 +44,14 @@ namespace Avalonia.Input
{
if (args.Type == RawPointerEventType.TouchEnd)
return;
- var hit = args.InputHitTestResult;
+ var hit = args.InputHitTestResult.firstEnabledAncestor;
_pointers[args.RawPointerId] = pointer = new Pointer(Pointer.GetNextFreeId(),
PointerType.Touch, _pointers.Count == 0);
pointer.Capture(hit);
}
- var target = pointer.Captured ?? args.InputHitTestResult ?? args.Root;
+ var target = pointer.Captured ?? args.InputHitTestResult.firstEnabledAncestor ?? args.Root;
var gestureTarget = pointer.CapturedGestureRecognizer?.Target;
var updateKind = args.Type.ToUpdateKind();
var keyModifier = args.InputModifiers.ToKeyModifiers();
diff --git a/src/Avalonia.Base/Rendering/IRenderer.cs b/src/Avalonia.Base/Rendering/IRenderer.cs
index 227ff53897..bd5aff8e6b 100644
--- a/src/Avalonia.Base/Rendering/IRenderer.cs
+++ b/src/Avalonia.Base/Rendering/IRenderer.cs
@@ -87,7 +87,7 @@ namespace Avalonia.Rendering
/// children will be excluded from the results.
///
/// The visuals at the specified point, topmost first.
- IEnumerable HitTest(Point p, Visual root, Func filter);
+ IEnumerable HitTest(Point p, Visual root, Func? filter);
///
/// Hit tests a location to find first visual at the specified point.
@@ -99,6 +99,6 @@ namespace Avalonia.Rendering
/// children will be excluded from the results.
///
/// The visual at the specified point, topmost first.
- Visual? HitTestFirst(Point p, Visual root, Func filter);
+ Visual? HitTestFirst(Point p, Visual root, Func? filter);
}
}
diff --git a/src/Avalonia.Controls/Application.cs b/src/Avalonia.Controls/Application.cs
index 6b8ddc15ea..e48711efdb 100644
--- a/src/Avalonia.Controls/Application.cs
+++ b/src/Avalonia.Controls/Application.cs
@@ -268,6 +268,7 @@ namespace Avalonia
.Bind().ToConstant(this)
.Bind().ToConstant(focusManager)
.Bind().ToConstant(InputManager)
+ .Bind< IToolTipService>().ToConstant(new ToolTipService(InputManager))
.Bind().ToTransient()
.Bind().ToConstant(DragDropDevice.Instance);
diff --git a/src/Avalonia.Controls/IToolTipService.cs b/src/Avalonia.Controls/IToolTipService.cs
new file mode 100644
index 0000000000..3188434850
--- /dev/null
+++ b/src/Avalonia.Controls/IToolTipService.cs
@@ -0,0 +1,9 @@
+using Avalonia.Metadata;
+
+namespace Avalonia.Controls;
+
+[Unstable, PrivateApi]
+internal interface IToolTipService
+{
+ void Update(Visual? candidateToolTipHost);
+}
diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs
index 7f9a155ea5..c4bf961afd 100644
--- a/src/Avalonia.Controls/ToolTip.cs
+++ b/src/Avalonia.Controls/ToolTip.cs
@@ -55,6 +55,18 @@ namespace Avalonia.Controls
public static readonly AttachedProperty ShowDelayProperty =
AvaloniaProperty.RegisterAttached("ShowDelay", 400);
+ ///
+ /// Defines the ToolTip.ShowOnDisabled property.
+ ///
+ public static readonly AttachedProperty ShowOnDisabledProperty =
+ AvaloniaProperty.RegisterAttached("ShowOnDisabled", defaultValue: false, inherits: true);
+
+ ///
+ /// Defines the ToolTip.ServiceEnabled property.
+ ///
+ public static readonly AttachedProperty ServiceEnabledProperty =
+ AvaloniaProperty.RegisterAttached("ServiceEnabled", defaultValue: true, inherits: true);
+
///
/// Stores the current instance in the control.
///
@@ -69,8 +81,6 @@ namespace Avalonia.Controls
///
static ToolTip()
{
- TipProperty.Changed.Subscribe(ToolTipService.Instance.TipChanged);
- IsOpenProperty.Changed.Subscribe(ToolTipService.Instance.TipOpenChanged);
IsOpenProperty.Changed.Subscribe(IsOpenChanged);
HorizontalOffsetProperty.Changed.Subscribe(RecalculatePositionOnPropertyChanged);
@@ -213,6 +223,36 @@ namespace Avalonia.Controls
element.SetValue(ShowDelayProperty, value);
}
+ ///
+ /// Gets whether a control will display a tooltip even if it disabled.
+ ///
+ /// The control to get the property from.
+ public static bool GetShowOnDisabled(Control element) =>
+ element.GetValue(ShowOnDisabledProperty);
+
+ ///
+ /// Sets whether a control will display a tooltip even if it disabled.
+ ///
+ /// The control to get the property from.
+ /// Whether the control is to display a tooltip even if it disabled.
+ public static void SetShowOnDisabled(Control element, bool value) =>
+ element.SetValue(ShowOnDisabledProperty, value);
+
+ ///
+ /// Gets whether showing and hiding of a control's tooltip will be automatically controlled by Avalonia.
+ ///
+ /// The control to get the property from.
+ public static bool GetServiceEnabled(Control element) =>
+ element.GetValue(ServiceEnabledProperty);
+
+ ///
+ /// Sets whether showing and hiding of a control's tooltip will be automatically controlled by Avalonia.
+ ///
+ /// The control to get the property from.
+ /// Whether the control is to display a tooltip even if it disabled.
+ public static void SetServiceEnabled(Control element, bool value) =>
+ element.SetValue(ServiceEnabledProperty, value);
+
private static void IsOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs
index 9e75026d79..77e314e52d 100644
--- a/src/Avalonia.Controls/ToolTipService.cs
+++ b/src/Avalonia.Controls/ToolTipService.cs
@@ -1,6 +1,7 @@
using System;
using Avalonia.Input;
-using Avalonia.Interactivity;
+using Avalonia.Input.Raw;
+using Avalonia.Reactive;
using Avalonia.Threading;
namespace Avalonia.Controls
@@ -8,44 +9,95 @@ namespace Avalonia.Controls
///
/// Handles interaction with controls.
///
- internal sealed class ToolTipService
+ internal sealed class ToolTipService : IToolTipService, IDisposable
{
- public static ToolTipService Instance { get; } = new ToolTipService();
+ private readonly IDisposable _subscriptions;
+ private Control? _tipControl;
private DispatcherTimer? _timer;
- private ToolTipService() { }
+ public ToolTipService(IInputManager inputManager)
+ {
+ _subscriptions = new CompositeDisposable(
+ inputManager.Process.Subscribe(InputManager_OnProcess),
+ ToolTip.ServiceEnabledProperty.Changed.Subscribe(ServiceEnabledChanged),
+ ToolTip.TipProperty.Changed.Subscribe(TipChanged),
+ ToolTip.IsOpenProperty.Changed.Subscribe(TipOpenChanged));
+ }
- ///
- /// called when the property changes on a control.
- ///
- /// The event args.
- internal void TipChanged(AvaloniaPropertyChangedEventArgs e)
+ public void Dispose() => _subscriptions.Dispose();
+
+ private void InputManager_OnProcess(RawInputEventArgs e)
{
- var control = (Control)e.Sender;
+ if (e is RawPointerEventArgs pointerEvent)
+ {
+ switch (pointerEvent.Type)
+ {
+ case RawPointerEventType.Move:
+ Update(pointerEvent.InputHitTestResult.element as Visual);
+ break;
+ case RawPointerEventType.LeftButtonDown:
+ case RawPointerEventType.RightButtonDown:
+ case RawPointerEventType.MiddleButtonDown:
+ case RawPointerEventType.XButton1Down:
+ case RawPointerEventType.XButton2Down:
+ StopTimer();
+ _tipControl?.ClearValue(ToolTip.IsOpenProperty);
+ break;
+ }
+ }
+ }
- if (e.OldValue != null)
+ public void Update(Visual? candidateToolTipHost)
+ {
+ while (candidateToolTipHost != null)
{
- control.PointerEntered -= ControlPointerEntered;
- control.PointerExited -= ControlPointerExited;
- control.RemoveHandler(InputElement.PointerPressedEvent, ControlPointerPressed);
+ if (candidateToolTipHost is Control control)
+ {
+ if (!ToolTip.GetServiceEnabled(control))
+ return;
+
+ if (ToolTip.GetTip(control) != null && (control.IsEffectivelyEnabled || ToolTip.GetShowOnDisabled(control)))
+ break;
+ }
+
+ candidateToolTipHost = candidateToolTipHost?.VisualParent;
}
- if (e.NewValue != null)
+ var newControl = candidateToolTipHost as Control;
+
+ if (newControl == _tipControl)
{
- control.PointerEntered += ControlPointerEntered;
- control.PointerExited += ControlPointerExited;
- control.AddHandler(InputElement.PointerPressedEvent, ControlPointerPressed,
- RoutingStrategies.Bubble | RoutingStrategies.Tunnel | RoutingStrategies.Direct, true);
+ return;
}
+ OnTipControlChanged(_tipControl, newControl);
+ _tipControl = newControl;
+ }
+
+ private void ServiceEnabledChanged(AvaloniaPropertyChangedEventArgs args)
+ {
+ if (args.Sender == _tipControl && !ToolTip.GetServiceEnabled(_tipControl))
+ {
+ StopTimer();
+ }
+ }
+
+ ///
+ /// called when the property changes on a control.
+ ///
+ /// The event args.
+ private void TipChanged(AvaloniaPropertyChangedEventArgs e)
+ {
+ var control = (Control)e.Sender;
+
if (ToolTip.GetIsOpen(control) && e.NewValue != e.OldValue && !(e.NewValue is ToolTip))
{
if (e.NewValue is null)
{
Close(control);
}
- else
+ else
{
if (control.GetValue(ToolTip.ToolTipProperty) is { } tip)
{
@@ -55,7 +107,7 @@ namespace Avalonia.Controls
}
}
- internal void TipOpenChanged(AvaloniaPropertyChangedEventArgs e)
+ private void TipOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
@@ -64,13 +116,13 @@ namespace Avalonia.Controls
control.DetachedFromVisualTree += ControlDetaching;
control.EffectiveViewportChanged += ControlEffectiveViewportChanged;
}
- else if(e.OldValue is true && e.NewValue is false)
+ else if (e.OldValue is true && e.NewValue is false)
{
control.DetachedFromVisualTree -= ControlDetaching;
control.EffectiveViewportChanged -= ControlEffectiveViewportChanged;
}
}
-
+
private void ControlDetaching(object? sender, VisualTreeAttachmentEventArgs e)
{
var control = (Control)sender!;
@@ -79,49 +131,31 @@ namespace Avalonia.Controls
Close(control);
}
- ///
- /// Called when the pointer enters a control with an attached tooltip.
- ///
- /// The event sender.
- /// The event args.
- private void ControlPointerEntered(object? sender, PointerEventArgs e)
+ private void OnTipControlChanged(Control? oldValue, Control? newValue)
{
StopTimer();
- var control = (Control)sender!;
- var showDelay = ToolTip.GetShowDelay(control);
- if (showDelay == 0)
+ if (oldValue != null)
{
- Open(control);
+ // If the control is showing a tooltip and the pointer is over the tooltip, don't close it.
+ if (oldValue.GetValue(ToolTip.ToolTipProperty) is not { IsPointerOver: true })
+ Close(oldValue);
}
- else
+
+ if (newValue != null)
{
- StartShowTimer(showDelay, control);
+ var showDelay = ToolTip.GetShowDelay(newValue);
+ if (showDelay == 0)
+ {
+ Open(newValue);
+ }
+ else
+ {
+ StartShowTimer(showDelay, newValue);
+ }
}
}
- ///
- /// Called when the pointer leaves a control with an attached tooltip.
- ///
- /// The event sender.
- /// The event args.
- private void ControlPointerExited(object? sender, PointerEventArgs e)
- {
- var control = (Control)sender!;
-
- // If the control is showing a tooltip and the pointer is over the tooltip, don't close it.
- if (control.GetValue(ToolTip.ToolTipProperty) is { } tooltip && tooltip.IsPointerOver)
- return;
-
- Close(control);
- }
-
- private void ControlPointerPressed(object? sender, PointerPressedEventArgs e)
- {
- StopTimer();
- (sender as AvaloniaObject)?.ClearValue(ToolTip.IsOpenProperty);
- }
-
private void ControlEffectiveViewportChanged(object? sender, Layout.EffectiveViewportChangedEventArgs e)
{
var control = (Control)sender!;
@@ -140,11 +174,9 @@ namespace Avalonia.Controls
private void ToolTipPointerExited(object? sender, PointerEventArgs e)
{
- // The pointer has exited the tooltip. Close the tooltip unless the pointer is over the
+ // The pointer has exited the tooltip. Close the tooltip unless the current tooltip source is still the
// adorned control.
- if (sender is ToolTip toolTip &&
- toolTip.AdornedControl is { } control &&
- !control.IsPointerOver)
+ if (sender is ToolTip { AdornedControl: { } control } && control != _tipControl)
{
Close(control);
}
@@ -152,7 +184,7 @@ namespace Avalonia.Controls
private void StartShowTimer(int showDelay, Control control)
{
- _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(showDelay) };
+ _timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(showDelay), Tag = (this, control) };
_timer.Tick += (o, e) => Open(control);
_timer.Start();
}
@@ -175,8 +207,6 @@ namespace Avalonia.Controls
private void Close(Control control)
{
- StopTimer();
-
ToolTip.SetIsOpen(control, false);
}
diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs
index ee8bd951d0..404964d374 100644
--- a/src/Avalonia.Controls/TopLevel.cs
+++ b/src/Avalonia.Controls/TopLevel.cs
@@ -122,6 +122,7 @@ namespace Avalonia.Controls
);
private readonly IInputManager? _inputManager;
+ private readonly IToolTipService? _tooltipService;
private readonly IAccessKeyHandler? _accessKeyHandler;
private readonly IKeyboardNavigationHandler? _keyboardNavigationHandler;
private readonly IGlobalStyles? _globalStyles;
@@ -182,6 +183,8 @@ namespace Avalonia.Controls
newInputElement.PropertyChanged += topLevel.PointerOverElementOnPropertyChanged;
}
});
+
+ ToolTip.ServiceEnabledProperty.Changed.Subscribe(OnToolTipServiceEnabledChanged);
}
///
@@ -211,6 +214,7 @@ namespace Avalonia.Controls
_accessKeyHandler = TryGetService(dependencyResolver);
_inputManager = TryGetService(dependencyResolver);
+ _tooltipService = TryGetService(dependencyResolver);
_keyboardNavigationHandler = TryGetService(dependencyResolver);
_globalStyles = TryGetService(dependencyResolver);
_applicationThemeHost = TryGetService(dependencyResolver);
@@ -833,7 +837,9 @@ namespace Avalonia.Controls
var (topLevel, e) = (ValueTuple)state!;
if (e is RawPointerEventArgs pointerArgs)
{
- pointerArgs.InputHitTestResult = topLevel.InputHitTest(pointerArgs.Position);
+ var hitTestElement = topLevel.InputHitTest(pointerArgs.Position, enabledElementsOnly: false);
+
+ pointerArgs.InputHitTestResult = (hitTestElement, FirstEnabledAncestor(hitTestElement));
}
topLevel._inputManager?.ProcessInput(e);
@@ -847,6 +853,17 @@ namespace Avalonia.Controls
}
}
+ private static IInputElement? FirstEnabledAncestor(IInputElement? hitTestElement)
+ {
+ var candidate = hitTestElement;
+ while (candidate?.IsEffectivelyEnabled == false)
+ {
+ candidate = (candidate as Visual)?.Parent as IInputElement;
+ }
+
+ return candidate;
+ }
+
private void PointerOverElementOnPropertyChanged(object? sender, AvaloniaPropertyChangedEventArgs e)
{
if (e.Property == CursorProperty && sender is InputElement inputElement)
@@ -863,6 +880,30 @@ namespace Avalonia.Controls
private void SceneInvalidated(object? sender, SceneInvalidatedEventArgs e)
{
_pointerOverPreProcessor?.SceneInvalidated(e.DirtyRect);
+ UpdateToolTip(e.DirtyRect);
+ }
+
+ private static void OnToolTipServiceEnabledChanged(AvaloniaPropertyChangedEventArgs args)
+ {
+ if (args.GetNewValue()
+ && args.Priority != BindingPriority.Inherited
+ && args.Sender is Visual visual
+ && GetTopLevel(visual) is { } topLevel)
+ {
+ topLevel.UpdateToolTip(visual.Bounds.Translate((Vector)visual.TranslatePoint(default, topLevel)!));
+ }
+ }
+
+ private void UpdateToolTip(Rect dirtyRect)
+ {
+ if (_tooltipService != null && _pointerOverPreProcessor?.LastPosition is { } lastPos)
+ {
+ var clientPoint = this.PointToClient(lastPos);
+ if (dirtyRect.Contains(clientPoint))
+ {
+ _tooltipService.Update(HitTester.HitTestFirst(clientPoint, this, null));
+ }
+ }
}
void PlatformImpl_LostFocus()
diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
index 98a131752a..ddfc2b7b72 100644
--- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
@@ -1,45 +1,26 @@
using System;
-using System.Reactive.Disposables;
-using Avalonia.Markup.Xaml;
-using Avalonia.Platform;
+using System.Collections.Generic;
+using System.Runtime.CompilerServices;
+using Avalonia.Data;
+using Avalonia.Input;
+using Avalonia.Input.Raw;
+using Avalonia.Rendering;
using Avalonia.Threading;
using Avalonia.UnitTests;
-using Avalonia.Utilities;
-using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
- public class TolTipTests
+ public class ToolTipTests
{
- private MouseTestHelper _mouseHelper = new MouseTestHelper();
+ private static readonly MouseDevice s_mouseDevice = new(new Pointer(0, PointerType.Mouse, true));
- [Fact]
- public void Should_Not_Open_On_Detached_Control()
- {
- //issue #3188
- var control = new Decorator()
- {
- [ToolTip.TipProperty] = "Tip",
- [ToolTip.ShowDelayProperty] = 0
- };
-
- Assert.False(control.IsAttachedToVisualTree);
-
- //here in issue #3188 exception is raised
- _mouseHelper.Enter(control);
-
- Assert.False(ToolTip.GetIsOpen(control));
- }
-
[Fact]
public void Should_Close_When_Control_Detaches()
{
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ using (UnitTestApplication.Start(TestServices.FocusableWindow))
{
- var window = new Window();
-
var panel = new Panel();
var target = new Decorator()
@@ -50,15 +31,7 @@ namespace Avalonia.Controls.UnitTests
panel.Children.Add(target);
- window.Content = panel;
-
- window.ApplyStyling();
- window.ApplyTemplate();
- window.Presenter.ApplyTemplate();
-
- Assert.True(target.IsAttachedToVisualTree);
-
- _mouseHelper.Enter(target);
+ SetupWindowAndActivateToolTip(panel, target);
Assert.True(ToolTip.GetIsOpen(target));
@@ -71,27 +44,22 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Should_Close_When_Tip_Is_Opened_And_Detached_From_Visual_Tree()
{
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ using (UnitTestApplication.Start(TestServices.FocusableWindow))
{
- var xaml = @"
-
-
-
-
-";
- var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
-
- window.DataContext = new ToolTipViewModel();
- window.ApplyTemplate();
- window.Presenter.ApplyTemplate();
+ var target = new Decorator
+ {
+ [!ToolTip.TipProperty] = new Binding("Tip"),
+ [ToolTip.ShowDelayProperty] = 0,
+ };
- var target = window.Find("PART_target");
- var panel = window.Find("PART_panel");
-
- Assert.True(target.IsAttachedToVisualTree);
+ var panel = new Panel();
+ panel.Children.Add(target);
+
+ var mouseEnter = SetupWindowAndGetMouseEnterAction(panel);
- _mouseHelper.Enter(target);
+ panel.DataContext = new ToolTipViewModel();
+
+ mouseEnter(target);
Assert.True(ToolTip.GetIsOpen(target));
@@ -104,25 +72,15 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Should_Open_On_Pointer_Enter()
{
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ using (UnitTestApplication.Start(TestServices.FocusableWindow))
{
- var window = new Window();
-
var target = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
[ToolTip.ShowDelayProperty] = 0
};
- window.Content = target;
-
- window.ApplyStyling();
- window.ApplyTemplate();
- window.Presenter.ApplyTemplate();
-
- Assert.True(target.IsAttachedToVisualTree);
-
- _mouseHelper.Enter(target);
+ SetupWindowAndActivateToolTip(target);
Assert.True(ToolTip.GetIsOpen(target));
}
@@ -131,28 +89,19 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Content_Should_Update_When_Tip_Property_Changes_And_Already_Open()
{
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ using (UnitTestApplication.Start(TestServices.FocusableWindow))
{
- var window = new Window();
-
var target = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
[ToolTip.ShowDelayProperty] = 0
};
- window.Content = target;
-
- window.ApplyStyling();
- window.ApplyTemplate();
- window.Presenter.ApplyTemplate();
-
- _mouseHelper.Enter(target);
+ SetupWindowAndActivateToolTip(target);
Assert.True(ToolTip.GetIsOpen(target));
Assert.Equal("Tip", target.GetValue(ToolTip.ToolTipProperty).Content);
-
ToolTip.SetTip(target, "Tip1");
Assert.Equal("Tip1", target.GetValue(ToolTip.ToolTipProperty).Content);
}
@@ -161,25 +110,15 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Should_Open_On_Pointer_Enter_With_Delay()
{
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ using (UnitTestApplication.Start(TestServices.FocusableWindow))
{
- var window = new Window();
-
var target = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
[ToolTip.ShowDelayProperty] = 1
};
- window.Content = target;
-
- window.ApplyStyling();
- window.ApplyTemplate();
- window.Presenter.ApplyTemplate();
-
- Assert.True(target.IsAttachedToVisualTree);
-
- _mouseHelper.Enter(target);
+ SetupWindowAndActivateToolTip(target);
var timer = Assert.Single(Dispatcher.SnapshotTimersForUnitTests());
Assert.Equal(TimeSpan.FromMilliseconds(1), timer.Interval);
@@ -268,23 +207,15 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Should_Close_On_Null_Tip()
{
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ using (UnitTestApplication.Start(TestServices.FocusableWindow))
{
- var window = new Window();
-
var target = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
[ToolTip.ShowDelayProperty] = 0
};
- window.Content = target;
-
- window.ApplyStyling();
- window.ApplyTemplate();
- window.Presenter.ApplyTemplate();
-
- _mouseHelper.Enter(target);
+ SetupWindowAndActivateToolTip(target);
Assert.True(ToolTip.GetIsOpen(target));
@@ -297,28 +228,23 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Should_Not_Close_When_Pointer_Is_Moved_Over_ToolTip()
{
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ using (UnitTestApplication.Start(TestServices.FocusableWindow))
{
- var window = new Window();
-
var target = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
[ToolTip.ShowDelayProperty] = 0
};
- window.Content = target;
+ var mouseEnter = SetupWindowAndGetMouseEnterAction(target);
- window.ApplyStyling();
- window.ApplyTemplate();
- window.Presenter.ApplyTemplate();
+ mouseEnter(target);
- _mouseHelper.Enter(target);
Assert.True(ToolTip.GetIsOpen(target));
var tooltip = Assert.IsType(target.GetValue(ToolTip.ToolTipProperty));
- _mouseHelper.Enter(tooltip);
- _mouseHelper.Leave(target);
+
+ mouseEnter(tooltip);
Assert.True(ToolTip.GetIsOpen(target));
}
@@ -327,33 +253,25 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Should_Not_Close_When_Pointer_Is_Moved_From_ToolTip_To_Original_Control()
{
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ using (UnitTestApplication.Start(TestServices.FocusableWindow))
{
- var window = new Window();
-
var target = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
[ToolTip.ShowDelayProperty] = 0
};
- window.Content = target;
-
- window.ApplyStyling();
- window.ApplyTemplate();
- window.Presenter.ApplyTemplate();
+ var mouseEnter = SetupWindowAndGetMouseEnterAction(target);
- _mouseHelper.Enter(target);
+ mouseEnter(target);
Assert.True(ToolTip.GetIsOpen(target));
var tooltip = Assert.IsType(target.GetValue(ToolTip.ToolTipProperty));
- _mouseHelper.Enter(tooltip);
- _mouseHelper.Leave(target);
+ mouseEnter(tooltip);
Assert.True(ToolTip.GetIsOpen(target));
- _mouseHelper.Enter(target);
- _mouseHelper.Leave(tooltip);
+ mouseEnter(target);
Assert.True(ToolTip.GetIsOpen(target));
}
@@ -362,10 +280,8 @@ namespace Avalonia.Controls.UnitTests
[Fact]
public void Should_Close_When_Pointer_Is_Moved_From_ToolTip_To_Another_Control()
{
- using (UnitTestApplication.Start(TestServices.StyledWindow))
+ using (UnitTestApplication.Start(TestServices.FocusableWindow))
{
- var window = new Window();
-
var target = new Decorator()
{
[ToolTip.TipProperty] = "Tip",
@@ -379,27 +295,74 @@ namespace Avalonia.Controls.UnitTests
Children = { target, other }
};
- window.Content = panel;
-
- window.ApplyStyling();
- window.ApplyTemplate();
- window.Presenter.ApplyTemplate();
+ var mouseEnter = SetupWindowAndGetMouseEnterAction(panel);
- _mouseHelper.Enter(target);
+ mouseEnter(target);
Assert.True(ToolTip.GetIsOpen(target));
var tooltip = Assert.IsType(target.GetValue(ToolTip.ToolTipProperty));
- _mouseHelper.Enter(tooltip);
- _mouseHelper.Leave(target);
+ mouseEnter(tooltip);
Assert.True(ToolTip.GetIsOpen(target));
- _mouseHelper.Enter(other);
- _mouseHelper.Leave(tooltip);
+ mouseEnter(other);
Assert.False(ToolTip.GetIsOpen(target));
}
}
+
+ private Action SetupWindowAndGetMouseEnterAction(Control windowContent, [CallerMemberName] string testName = null)
+ {
+ var windowImpl = MockWindowingPlatform.CreateWindowMock();
+ var hitTesterMock = new Mock();
+
+ var window = new Window(windowImpl.Object)
+ {
+ HitTesterOverride = hitTesterMock.Object,
+ Content = windowContent,
+ Title = testName,
+ };
+
+ window.ApplyStyling();
+ window.ApplyTemplate();
+ window.Presenter.ApplyTemplate();
+ window.Show();
+
+ Assert.True(windowContent.IsAttachedToVisualTree);
+ Assert.True(windowContent.IsMeasureValid);
+ Assert.True(windowContent.IsVisible);
+
+ var controlIds = new Dictionary();
+
+ return control =>
+ {
+ Point point;
+
+ if (control == null)
+ {
+ point = default;
+ }
+ else
+ {
+ if (!controlIds.TryGetValue(control, out int id))
+ {
+ id = controlIds[control] = controlIds.Count;
+ }
+ point = new Point(id, int.MaxValue);
+ }
+
+ hitTesterMock.Setup(m => m.HitTestFirst(point, window, It.IsAny>()))
+ .Returns(control);
+
+ windowImpl.Object.Input(new RawPointerEventArgs(s_mouseDevice, (ulong)DateTime.Now.Ticks, window,
+ RawPointerEventType.Move, point, RawInputModifiers.None));
+
+ Assert.True(control == null || control.IsPointerOver);
+ };
+ }
+
+ private void SetupWindowAndActivateToolTip(Control windowContent, Control targetOverride = null, [CallerMemberName] string testName = null) =>
+ SetupWindowAndGetMouseEnterAction(windowContent, testName)(targetOverride ?? windowContent);
}
internal class ToolTipViewModel
diff --git a/tests/Avalonia.UnitTests/UnitTestApplication.cs b/tests/Avalonia.UnitTests/UnitTestApplication.cs
index 18ccff75f0..18b1bb6461 100644
--- a/tests/Avalonia.UnitTests/UnitTestApplication.cs
+++ b/tests/Avalonia.UnitTests/UnitTestApplication.cs
@@ -53,6 +53,8 @@ namespace Avalonia.UnitTests
Dispatcher.UIThread.RunJobs();
}
+ ((ToolTipService)AvaloniaLocator.Current.GetService())?.Dispose();
+
scope.Dispose();
Dispatcher.ResetForUnitTests();
SynchronizationContext.SetSynchronizationContext(oldContext);
@@ -67,6 +69,7 @@ namespace Avalonia.UnitTests
.Bind().ToConstant(Services.GlobalClock)
.BindToSelf(this)
.Bind().ToConstant(Services.InputManager)
+ .Bind().ToConstant(Services.InputManager == null ? null : new ToolTipService(Services.InputManager))
.Bind().ToConstant(Services.KeyboardDevice?.Invoke())
.Bind().ToConstant(Services.MouseDevice?.Invoke())
.Bind().ToFunc(Services.KeyboardNavigation ?? (() => null))