Browse Source

New ToolTipClosing, ToolTipOpening attached events and ToolTip.Opened, ToolTip.Closed (#15493)

* Add ToolTip.Opened and ToolTip.Closed

* Add ToolTipOpeningEvent and ToolTipClosingEvent attache events

* Add tests and small sample

* Docs

* Remove new Opened/Closed events

* Update tests

* Restore internal Closed

* Update tests

* Don't use coerce logic, use CancelRoutedEventArgs in Opening API

* Update samples API

* Fix incorrect routed event definition

---------

Co-authored-by: Jumar Macato <16554748+jmacato@users.noreply.github.com>
pull/16143/head
Max Katz 2 years ago
committed by GitHub
parent
commit
24914cc1ed
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 11
      samples/ControlCatalog/Pages/ToolTipPage.xaml
  2. 6
      samples/ControlCatalog/Pages/ToolTipPage.xaml.cs
  3. 89
      src/Avalonia.Controls/ToolTip.cs
  4. 3
      src/Avalonia.Controls/ToolTipService.cs
  5. 81
      tests/Avalonia.Controls.UnitTests/ToolTipTests.cs

11
samples/ControlCatalog/Pages/ToolTipPage.xaml

@ -77,12 +77,23 @@
</Button>
<Border Grid.Row="3"
Grid.Column="0"
Background="{DynamicResource SystemAccentColor}"
Margin="5"
Padding="50"
ToolTip.Tip="Outer tooltip">
<TextBlock Background="{StaticResource SystemAccentColorDark1}" Padding="10" ToolTip.Tip="Inner tooltip" VerticalAlignment="Center">Nested ToolTips</TextBlock>
</Border>
<Border Grid.Row="3"
Grid.Column="1"
Background="{DynamicResource SystemAccentColor}"
Margin="5"
Padding="50"
ToolTip.ToolTipOpening="ToolTipOpening"
ToolTip.Tip="Should never be visible">
<TextBlock VerticalAlignment="Center">ToolTip replaced on the fly</TextBlock>
</Border>
</Grid>
</StackPanel>
</UserControl>

6
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.");
}
}
}

89
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<ToolTip?> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip?>("ToolTip");
/// <summary>
/// The event raised when a ToolTip is going to be shown on an element.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static readonly RoutedEvent<CancelRoutedEventArgs> ToolTipOpeningEvent =
RoutedEvent.Register<ToolTip, CancelRoutedEventArgs>("ToolTipOpening", RoutingStrategies.Direct);
/// <summary>
/// The event raised when a ToolTip on an element that was shown should now be hidden.
/// </summary>
/// <remarks>
/// 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.
/// </remarks>
public static readonly RoutedEvent ToolTipClosingEvent =
RoutedEvent.Register<ToolTip, RoutedEventArgs>("ToolTipClosing", RoutingStrategies.Direct);
private Popup? _popup;
private Action<IPopupHost?>? _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;
/// <summary>
/// Gets the value of the ToolTip.Tip attached property.
/// </summary>
@ -275,6 +295,38 @@ namespace Avalonia.Controls
public static void SetServiceEnabled(Control element, bool value) =>
element.SetValue(ServiceEnabledProperty, value);
/// <summary>
/// Adds a handler for the <see cref="ToolTipOpeningEvent"/> attached event.
/// </summary>
/// <param name="element"><see cref="Control"/> that listens to this event.</param>
/// <param name="handler">Event Handler to be added.</param>
public static void AddToolTipOpeningHandler(Control element, EventHandler<CancelRoutedEventArgs> handler) =>
element.AddHandler(ToolTipOpeningEvent, handler);
/// <summary>
/// Removes a handler for the <see cref="ToolTipOpeningEvent"/> attached event.
/// </summary>
/// <param name="element"><see cref="Control"/> that listens to this event.</param>
/// <param name="handler">Event Handler to be removed.</param>
public static void RemoveToolTipOpeningHandler(Control element, EventHandler<CancelRoutedEventArgs> handler) =>
element.RemoveHandler(ToolTipOpeningEvent, handler);
/// <summary>
/// Adds a handler for the <see cref="ToolTipClosingEvent"/> attached event.
/// </summary>
/// <param name="element"><see cref="Control"/> that listens to this event.</param>
/// <param name="handler">Event Handler to be removed.</param>
public static void AddToolTipClosingHandler(Control element, EventHandler<RoutedEventArgs> handler) =>
element.AddHandler(ToolTipClosingEvent, handler);
/// <summary>
/// Removes a handler for the <see cref="ToolTipClosingEvent"/> attached event.
/// </summary>
/// <param name="element"><see cref="Control"/> that listens to this event.</param>
/// <param name="handler">Event Handler to be removed.</param>
public static void RemoveToolTipClosingHandler(Control element, EventHandler<RoutedEventArgs> 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<IPopupHost?>? 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)

3
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;

81
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()
{

Loading…
Cancel
Save