A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

476 lines
21 KiB

using System;
using System.ComponentModel;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Reactive;
using Avalonia.Styling;
namespace Avalonia.Controls
{
/// <summary>
/// A control which pops up a hint when a control is hovered.
/// </summary>
/// <remarks>
/// You will probably not want to create a <see cref="ToolTip"/> control directly: if added to
/// the tree it will act as a simple <see cref="ContentControl"/> styled to look like a tooltip.
/// To add a tooltip to a control, use the <see cref="TipProperty"/> attached property,
/// assigning the content that you want displayed.
/// </remarks>
[PseudoClasses(":open")]
public class ToolTip : ContentControl, IPopupHostProvider
{
/// <summary>
/// Defines the ToolTip.Tip attached property.
/// </summary>
public static readonly AttachedProperty<object?> TipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, object?>("Tip");
/// <summary>
/// Defines the ToolTip.IsOpen attached property.
/// </summary>
public static readonly AttachedProperty<bool> IsOpenProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, bool>("IsOpen");
/// <summary>
/// Defines the ToolTip.Placement property.
/// </summary>
public static readonly AttachedProperty<PlacementMode> PlacementProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, PlacementMode>("Placement", defaultValue: PlacementMode.Pointer);
/// <summary>
/// Defines the ToolTip.HorizontalOffset property.
/// </summary>
public static readonly AttachedProperty<double> HorizontalOffsetProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, double>("HorizontalOffset");
/// <summary>
/// Defines the ToolTip.VerticalOffset property.
/// </summary>
public static readonly AttachedProperty<double> VerticalOffsetProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, double>("VerticalOffset", 20);
/// <inheritdoc cref="Popup.CustomPopupPlacementCallbackProperty"/>
public static readonly AttachedProperty<CustomPopupPlacementCallback?> CustomPopupPlacementCallbackProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, CustomPopupPlacementCallback?>("CustomPopupPlacementCallback");
/// <summary>
/// Defines the ToolTip.ShowDelay property.
/// </summary>
public static readonly AttachedProperty<int> ShowDelayProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, int>("ShowDelay", 400);
/// <summary>
/// Defines the ToolTip.BetweenShowDelay property.
/// </summary>
public static readonly AttachedProperty<int> BetweenShowDelayProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, int>("BetweenShowDelay", 100);
/// <summary>
/// Defines the ToolTip.ShowOnDisabled property.
/// </summary>
public static readonly AttachedProperty<bool> ShowOnDisabledProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, bool>("ShowOnDisabled", defaultValue: false, inherits: true);
/// <summary>
/// Defines the ToolTip.ServiceEnabled property.
/// </summary>
public static readonly AttachedProperty<bool> ServiceEnabledProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, bool>("ServiceEnabled", defaultValue: true, inherits: true);
/// <summary>
/// Stores the current <see cref="ToolTip"/> instance in the control.
/// </summary>
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;
/// <summary>
/// Initializes static members of the <see cref="ToolTip"/> class.
/// </summary>
static ToolTip()
{
IsOpenProperty.Changed.Subscribe(IsOpenChanged);
}
/// <summary>
/// Gets the value of the ToolTip.Tip attached property.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <returns>
/// The content to be displayed in the control's tooltip.
/// </returns>
public static object? GetTip(Control element)
{
return element.GetValue(TipProperty);
}
/// <summary>
/// Sets the value of the ToolTip.Tip attached property.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <param name="value">The content to be displayed in the control's tooltip.</param>
public static void SetTip(Control element, object? value)
{
element.SetValue(TipProperty, value);
}
/// <summary>
/// Gets the value of the ToolTip.IsOpen attached property.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <returns>
/// A value indicating whether the tool tip is visible.
/// </returns>
public static bool GetIsOpen(Control element)
{
return element.GetValue(IsOpenProperty);
}
/// <summary>
/// Sets the value of the ToolTip.IsOpen attached property.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <param name="value">A value indicating whether the tool tip is visible.</param>
public static void SetIsOpen(Control element, bool value)
{
element.SetValue(IsOpenProperty, value);
}
/// <summary>
/// Gets the value of the ToolTip.Placement attached property.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <returns>
/// A value indicating how the tool tip is positioned.
/// </returns>
public static PlacementMode GetPlacement(Control element)
{
return element.GetValue(PlacementProperty);
}
/// <summary>
/// Sets the value of the ToolTip.Placement attached property.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <param name="value">A value indicating how the tool tip is positioned.</param>
public static void SetPlacement(Control element, PlacementMode value)
{
element.SetValue(PlacementProperty, value);
}
/// <summary>
/// Gets the value of the ToolTip.HorizontalOffset attached property.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <returns>
/// A value indicating how the tool tip is positioned.
/// </returns>
public static double GetHorizontalOffset(Control element)
{
return element.GetValue(HorizontalOffsetProperty);
}
/// <summary>
/// Sets the value of the ToolTip.HorizontalOffset attached property.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <param name="value">A value indicating how the tool tip is positioned.</param>
public static void SetHorizontalOffset(Control element, double value)
{
element.SetValue(HorizontalOffsetProperty, value);
}
/// <summary>
/// Gets the value of the ToolTip.VerticalOffset attached property.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <returns>
/// A value indicating how the tool tip is positioned.
/// </returns>
public static double GetVerticalOffset(Control element)
{
return element.GetValue(VerticalOffsetProperty);
}
/// <summary>
/// Sets the value of the ToolTip.VerticalOffset attached property.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <param name="value">A value indicating how the tool tip is positioned.</param>
public static void SetVerticalOffset(Control element, double value)
{
element.SetValue(VerticalOffsetProperty, value);
}
/// <summary>
/// Gets the value of the ToolTip.ShowDelay attached property.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <returns>
/// A value indicating the time, in milliseconds, before a tool tip opens.
/// </returns>
public static int GetShowDelay(Control element)
{
return element.GetValue(ShowDelayProperty);
}
/// <summary>
/// Sets the value of the ToolTip.ShowDelay attached property.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <param name="value">A value indicating the time, in milliseconds, before a tool tip opens.</param>
public static void SetShowDelay(Control element, int value)
{
element.SetValue(ShowDelayProperty, value);
}
/// <summary>
/// Gets the number of milliseconds since the last tooltip closed during which the tooltip of <paramref name="element"/> will open immediately,
/// or a negative value indicating that the tooltip will always wait for <see cref="ShowDelayProperty"/> before opening.
/// </summary>
/// <param name="element">The control to get the property from.</param>
public static int GetBetweenShowDelay(Control element) => element.GetValue(BetweenShowDelayProperty);
/// <summary>
/// Sets the number of milliseconds since the last tooltip closed during which the tooltip of <paramref name="element"/> will open immediately.
/// </summary>
/// <remarks>
/// Setting a negative value disables the immediate opening behaviour. The tooltip of <paramref name="element"/> will then always wait until
/// <see cref="ShowDelayProperty"/> elapses before showing.
/// </remarks>
/// <param name="element">The control to get the property from.</param>
/// <param name="value">The number of milliseconds to set, or a negative value to disable the behaviour.</param>
public static void SetBetweenShowDelay(Control element, int value) => element.SetValue(BetweenShowDelayProperty, value);
/// <summary>
/// Gets whether a control will display a tooltip even if it disabled.
/// </summary>
/// <param name="element">The control to get the property from.</param>
public static bool GetShowOnDisabled(Control element) =>
element.GetValue(ShowOnDisabledProperty);
/// <summary>
/// Sets whether a control will display a tooltip even if it disabled.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <param name="value">Whether the control is to display a tooltip even if it disabled.</param>
public static void SetShowOnDisabled(Control element, bool value) =>
element.SetValue(ShowOnDisabledProperty, value);
/// <summary>
/// Gets whether showing and hiding of a control's tooltip will be automatically controlled by Avalonia.
/// </summary>
/// <param name="element">The control to get the property from.</param>
public static bool GetServiceEnabled(Control element) =>
element.GetValue(ServiceEnabledProperty);
/// <summary>
/// Sets whether showing and hiding of a control's tooltip will be automatically controlled by Avalonia.
/// </summary>
/// <param name="element">The control to get the property from.</param>
/// <param name="value">Whether the control is to display a tooltip even if it disabled.</param>
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);
/// <summary>
/// Gets the value of the ToolTip.CustomPopupPlacementCallback attached property.
/// </summary>
public static CustomPopupPlacementCallback? GetCustomPopupPlacementCallback(Control element)
{
return element.GetValue(CustomPopupPlacementCallbackProperty);
}
/// <summary>
/// Sets the value of the ToolTip.CustomPopupPlacementCallback attached property.
/// </summary>
public static void SetCustomPopupPlacementCallback(Control element, CustomPopupPlacementCallback? value)
{
element.SetValue(CustomPopupPlacementCallbackProperty, value);
}
private static void IsOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
var newValue = (bool)e.NewValue!;
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)
{
control.SetCurrentValue(IsOpenProperty, false);
return;
}
var toolTip = control.GetValue(ToolTipProperty);
if (toolTip == null || (tip != toolTip && tip != toolTip.Content))
{
toolTip?.Close();
toolTip = tip as ToolTip ?? new ToolTip { Content = tip };
control.SetValue(ToolTipProperty, toolTip);
}
toolTip.AdornedControl = control;
toolTip.Open(control);
}
else if (control.GetValue(ToolTipProperty) is { } toolTip)
{
toolTip.AdornedControl = null;
toolTip.Close();
}
}
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;
remove => _popupHostChangedHandler -= value;
}
private void Open(Control control)
{
Close();
if (_popup is null)
{
_popup = new Popup();
_popup.Child = this;
_popup.TakesFocusFromNativeControl = false;
_popup.WindowManagerAddShadowHint = false;
_popup.Opened += OnPopupOpened;
_popup.Closed += OnPopupClosed;
}
_subscriptions = new CompositeDisposable(new[]
{
_popup.Bind(Popup.HorizontalOffsetProperty, control.GetBindingObservable(HorizontalOffsetProperty)),
_popup.Bind(Popup.VerticalOffsetProperty, control.GetBindingObservable(VerticalOffsetProperty)),
_popup.Bind(Popup.PlacementProperty, control.GetBindingObservable(PlacementProperty)),
_popup.Bind(Popup.CustomPopupPlacementCallbackProperty, control.GetBindingObservable(CustomPopupPlacementCallbackProperty))
});
_popup.PlacementTarget = control;
_popup.SetPopupParent(control);
_popup.IsOpen = true;
}
private void Close()
{
if (AdornedControl is { } adornedControl
&& GetIsOpen(adornedControl))
{
var args = new RoutedEventArgs(ToolTipClosingEvent);
adornedControl.RaiseEvent(args);
}
if (_popup is not null)
{
_popup.IsOpen = false;
_popup.SetPopupParent(null);
_popup.PlacementTarget = null;
}
_subscriptions?.Dispose();
}
private void OnPopupClosed(object? sender, EventArgs e)
{
// This condition is true, when Popup was closed by any other reason outside of ToolTipService/ToolTip, keeping IsOpen=true.
if (AdornedControl is { } adornedControl
&& GetIsOpen(adornedControl))
{
adornedControl.SetCurrentValue(IsOpenProperty, false);
}
_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)
{
PseudoClasses.Set(":open", newValue);
}
}
}