diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml
index fa40bc1a0f..8aaed903f3 100644
--- a/samples/ControlCatalog/Pages/ToolTipPage.xaml
+++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml
@@ -77,12 +77,23 @@
Nested ToolTips
+
+
+ ToolTip replaced on the fly
+
diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml.cs b/samples/ControlCatalog/Pages/ToolTipPage.xaml.cs
index 202f83aaf1..0e8bf3a181 100644
--- a/samples/ControlCatalog/Pages/ToolTipPage.xaml.cs
+++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml.cs
@@ -1,4 +1,5 @@
using Avalonia.Controls;
+using Avalonia.Interactivity;
using Avalonia.Markup.Xaml;
namespace ControlCatalog.Pages
@@ -14,5 +15,10 @@ namespace ControlCatalog.Pages
{
AvaloniaXamlLoader.Load(this);
}
+
+ private void ToolTipOpening(object? sender, CancelRoutedEventArgs args)
+ {
+ ((Control)args.Source!).SetValue(ToolTip.TipProperty, "New tip set from ToolTipOpening.");
+ }
}
}
diff --git a/src/Avalonia.Controls/ToolTip.cs b/src/Avalonia.Controls/ToolTip.cs
index 245b4d74b0..8bf0adedee 100644
--- a/src/Avalonia.Controls/ToolTip.cs
+++ b/src/Avalonia.Controls/ToolTip.cs
@@ -3,6 +3,7 @@ using System.ComponentModel;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
+using Avalonia.Interactivity;
using Avalonia.Reactive;
using Avalonia.Styling;
@@ -80,6 +81,28 @@ namespace Avalonia.Controls
internal static readonly AttachedProperty ToolTipProperty =
AvaloniaProperty.RegisterAttached("ToolTip");
+ ///
+ /// The event raised when a ToolTip is going to be shown on an element.
+ ///
+ ///
+ /// To prevent a tooltip from appearing in the UI, your handler for ToolTipOpening can mark the event data handled.
+ /// Otherwise, the tooltip is displayed, using the value of the ToolTip property as the tooltip content.
+ /// Another possible scenario is that you could write a handler that resets the value of the ToolTip property for the element that is the event source, just before the tooltip is displayed.
+ /// ToolTipOpening will not be raised if the value of ToolTip is null or otherwise unset. Do not deliberately set ToolTip to null while a tooltip is open or opening; this will not have the effect of closing the tooltip, and will instead create an undesirable visual artifact in the UI.
+ ///
+ public static readonly RoutedEvent ToolTipOpeningEvent =
+ RoutedEvent.Register("ToolTipOpening", RoutingStrategies.Direct);
+
+ ///
+ /// The event raised when a ToolTip on an element that was shown should now be hidden.
+ ///
+ ///
+ /// Marking the ToolTipClosing event as handled does not cancel closing the tooltip.
+ /// Once the tooltip is displayed, closing the tooltip is done only in response to user interaction with the UI.
+ ///
+ public static readonly RoutedEvent ToolTipClosingEvent =
+ RoutedEvent.Register("ToolTipClosing", RoutingStrategies.Direct);
+
private Popup? _popup;
private Action? _popupHostChangedHandler;
private CompositeDisposable? _subscriptions;
@@ -92,9 +115,6 @@ namespace Avalonia.Controls
IsOpenProperty.Changed.Subscribe(IsOpenChanged);
}
- internal Control? AdornedControl { get; private set; }
- internal event EventHandler? Closed;
-
///
/// Gets the value of the ToolTip.Tip attached property.
///
@@ -275,6 +295,38 @@ namespace Avalonia.Controls
public static void SetServiceEnabled(Control element, bool value) =>
element.SetValue(ServiceEnabledProperty, value);
+ ///
+ /// Adds a handler for the attached event.
+ ///
+ /// that listens to this event.
+ /// Event Handler to be added.
+ public static void AddToolTipOpeningHandler(Control element, EventHandler handler) =>
+ element.AddHandler(ToolTipOpeningEvent, handler);
+
+ ///
+ /// Removes a handler for the attached event.
+ ///
+ /// that listens to this event.
+ /// Event Handler to be removed.
+ public static void RemoveToolTipOpeningHandler(Control element, EventHandler handler) =>
+ element.RemoveHandler(ToolTipOpeningEvent, handler);
+
+ ///
+ /// Adds a handler for the attached event.
+ ///
+ /// that listens to this event.
+ /// Event Handler to be removed.
+ public static void AddToolTipClosingHandler(Control element, EventHandler handler) =>
+ element.AddHandler(ToolTipClosingEvent, handler);
+
+ ///
+ /// Removes a handler for the attached event.
+ ///
+ /// that listens to this event.
+ /// Event Handler to be removed.
+ public static void RemoveToolTipClosingHandler(Control element, EventHandler handler) =>
+ element.RemoveHandler(ToolTipClosingEvent, handler);
+
private static void IsOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
@@ -282,8 +334,20 @@ namespace Avalonia.Controls
if (newValue)
{
+ var args = new CancelRoutedEventArgs(ToolTipOpeningEvent);
+ control.RaiseEvent(args);
+ if (args.Cancel)
+ {
+ control.SetCurrentValue(IsOpenProperty, false);
+ return;
+ }
+
var tip = GetTip(control);
- if (tip == null) return;
+ if (tip == null)
+ {
+ control.SetCurrentValue(IsOpenProperty, false);
+ return;
+ }
var toolTip = control.GetValue(ToolTipProperty);
if (toolTip == null || (tip != toolTip && tip != toolTip.Content))
@@ -292,25 +356,23 @@ namespace Avalonia.Controls
toolTip = tip as ToolTip ?? new ToolTip { Content = tip };
control.SetValue(ToolTipProperty, toolTip);
- toolTip.SetValue(ThemeVariant.RequestedThemeVariantProperty, control.ActualThemeVariant);
}
toolTip.AdornedControl = control;
toolTip.Open(control);
- toolTip?.UpdatePseudoClasses(newValue);
}
else if (control.GetValue(ToolTipProperty) is { } toolTip)
{
toolTip.AdornedControl = null;
toolTip.Close();
- toolTip?.UpdatePseudoClasses(newValue);
}
}
- IPopupHost? IPopupHostProvider.PopupHost => _popup?.Host;
-
+ internal Control? AdornedControl { get; private set; }
+ internal event EventHandler? Closed;
internal IPopupHost? PopupHost => _popup?.Host;
+ IPopupHost? IPopupHostProvider.PopupHost => _popup?.Host;
event Action? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
@@ -346,6 +408,13 @@ namespace Avalonia.Controls
private void Close()
{
+ if (AdornedControl is { } adornedControl
+ && GetIsOpen(adornedControl))
+ {
+ var args = new RoutedEventArgs(ToolTipClosingEvent);
+ adornedControl.RaiseEvent(args);
+ }
+
_subscriptions?.Dispose();
if (_popup is not null)
@@ -366,12 +435,14 @@ namespace Avalonia.Controls
}
_popupHostChangedHandler?.Invoke(null);
+ UpdatePseudoClasses(false);
Closed?.Invoke(this, EventArgs.Empty);
}
private void OnPopupOpened(object? sender, EventArgs e)
{
_popupHostChangedHandler?.Invoke(((Popup)sender!).Host);
+ UpdatePseudoClasses(true);
}
private void UpdatePseudoClasses(bool newValue)
diff --git a/src/Avalonia.Controls/ToolTipService.cs b/src/Avalonia.Controls/ToolTipService.cs
index c3231fcde9..d01114be6f 100644
--- a/src/Avalonia.Controls/ToolTipService.cs
+++ b/src/Avalonia.Controls/ToolTipService.cs
@@ -221,7 +221,8 @@ namespace Avalonia.Controls
{
ToolTip.SetIsOpen(control, true);
- if (control.GetValue(ToolTip.ToolTipProperty) is { } tooltip)
+ // Value can be coerced back to false, need to double check.
+ if (ToolTip.GetIsOpen(control) && control.GetValue(ToolTip.ToolTipProperty) is { } tooltip)
{
tooltip.Closed += ToolTipClosed;
tooltip.PointerExited += ToolTipPointerExited;
diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
index 8dafef7e18..6199a20872 100644
--- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs
@@ -368,6 +368,87 @@ namespace Avalonia.Controls.UnitTests
Assert.False(ToolTip.GetIsOpen(other));
}
+ [Fact]
+ public void ToolTip_Events_Order_Is_Defined()
+ {
+ using var app = UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow));
+
+ var tip = new ToolTip() { Content = "Tip" };
+ var target = new Decorator()
+ {
+ [ToolTip.TipProperty] = tip,
+ [ToolTip.ShowDelayProperty] = 0
+ };
+
+ var eventsOrder = new List<(string eventName, object sender, object source)>();
+
+ ToolTip.AddToolTipOpeningHandler(target,
+ (sender, args) => eventsOrder.Add(("Opening", sender, args.Source)));
+ ToolTip.AddToolTipClosingHandler(target,
+ (sender, args) => eventsOrder.Add(("Closing", sender, args.Source)));
+
+ SetupWindowAndActivateToolTip(target);
+
+ Assert.True(ToolTip.GetIsOpen(target));
+
+ target[ToolTip.TipProperty] = null;
+
+ Assert.False(ToolTip.GetIsOpen(target));
+
+ Assert.Equal(
+ new[]
+ {
+ ("Opening", (object)target, (object)target),
+ ("Closing", target, target)
+ },
+ eventsOrder);
+ }
+
+ [Fact]
+ public void ToolTip_Is_Not_Opened_If_Opening_Event_Handled()
+ {
+ using var app = UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow));
+
+ var tip = new ToolTip() { Content = "Tip" };
+ var target = new Decorator()
+ {
+ [ToolTip.TipProperty] = tip,
+ [ToolTip.ShowDelayProperty] = 0
+ };
+
+ ToolTip.AddToolTipOpeningHandler(target,
+ (sender, args) => args.Cancel = true);
+
+ SetupWindowAndActivateToolTip(target);
+
+ Assert.False(ToolTip.GetIsOpen(target));
+ }
+
+ [Fact]
+ public void ToolTip_Can_Be_Replaced_On_The_Fly_Via_Opening_Event()
+ {
+ using var app = UnitTestApplication.Start(ConfigureServices(TestServices.FocusableWindow));
+
+ var tip1 = new ToolTip() { Content = "Hi" };
+ var tip2 = new ToolTip() { Content = "Bye" };
+ var target = new Decorator()
+ {
+ [ToolTip.TipProperty] = tip1,
+ [ToolTip.ShowDelayProperty] = 0
+ };
+
+ ToolTip.AddToolTipOpeningHandler(target,
+ (sender, args) => target[ToolTip.TipProperty] = tip2);
+
+ SetupWindowAndActivateToolTip(target);
+
+ Assert.True(ToolTip.GetIsOpen(target));
+
+ target[ToolTip.TipProperty] = null;
+
+ Assert.False(ToolTip.GetIsOpen(target));
+ }
+
[Fact]
public void Should_Close_When_Pointer_Leaves_Window()
{