Browse Source

ToolTip: IsOpen, Placement, Offset

pull/1133/head
Eli Arbel 9 years ago
parent
commit
426cd8c9dd
  1. 48
      samples/ControlCatalog/Pages/ToolTipPage.xaml
  2. 26
      src/Avalonia.Controls/Primitives/Popup.cs
  3. 236
      src/Avalonia.Controls/ToolTip.cs
  4. 98
      src/Avalonia.Controls/ToolTipService.cs

48
samples/ControlCatalog/Pages/ToolTipPage.xaml

@ -1,22 +1,34 @@
<UserControl xmlns="https://github.com/avaloniaui"> <UserControl xmlns="https://github.com/avaloniaui">
<StackPanel Orientation="Vertical" Gap="4"> <StackPanel Orientation="Vertical"
<TextBlock Classes="h1">ToolTip</TextBlock> Gap="4">
<TextBlock Classes="h2">A control which pops up a hint when a control is hovered</TextBlock> <TextBlock Classes="h1">ToolTip</TextBlock>
<TextBlock Classes="h2">A control which pops up a hint when a control is hovered</TextBlock>
<StackPanel Orientation="Horizontal" <StackPanel Orientation="Horizontal"
Margin="0,16,0,0" Margin="0,16,0,0"
HorizontalAlignment="Center" HorizontalAlignment="Center"
Gap="16"> Gap="16">
<Border Background="{StyleResource ThemeAccentBrush}" <CheckBox IsChecked="{Binding ElementName=Border, Path=(ToolTip.IsOpen)}"
Padding="48,48,48,48"> Content="Show ToolTip" />
<ToolTip.Tip> <Border Name="Border"
<StackPanel> Background="{StyleResource ThemeAccentBrush}"
<TextBlock Classes="h1">ToolTip</TextBlock> Margin="5"
<TextBlock Classes="h2">A control which pops up a hint when a control is hovered</TextBlock> Padding="50"
</StackPanel> ToolTip.Placement="Bottom">
</ToolTip.Tip> <ToolTip.Tip>
<TextBlock>Hover Here</TextBlock> <StackPanel>
</Border> <TextBlock Classes="h1">ToolTip</TextBlock>
<TextBlock Classes="h2">A control which pops up a hint when a control is hovered</TextBlock>
</StackPanel>
</ToolTip.Tip>
<TextBlock>Hover Here</TextBlock>
</Border>
<Border Background="{StyleResource ThemeAccentBrush}"
Margin="5"
Padding="50"
ToolTip.Tip="Another tip">
<TextBlock>And Here</TextBlock>
</Border>
</StackPanel>
</StackPanel> </StackPanel>
</StackPanel>
</UserControl> </UserControl>

26
src/Avalonia.Controls/Primitives/Popup.cs

@ -277,7 +277,7 @@ namespace Avalonia.Controls.Primitives
{ {
base.OnDetachedFromLogicalTree(e); base.OnDetachedFromLogicalTree(e);
_topLevel = null; _topLevel = null;
if (_popupRoot != null) if (_popupRoot != null)
{ {
((ISetLogicalParent)_popupRoot).SetParent(null); ((ISetLogicalParent)_popupRoot).SetParent(null);
@ -327,34 +327,40 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
/// <returns>The popup's position in screen coordinates.</returns> /// <returns>The popup's position in screen coordinates.</returns>
protected virtual Point GetPosition() protected virtual Point GetPosition()
{
return GetPosition(PlacementTarget ?? this.GetVisualParent<Control>(), PlacementMode, PopupRoot,
HorizontalOffset, VerticalOffset);
}
internal static Point GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset)
{ {
var zero = default(Point); var zero = default(Point);
var mode = PlacementMode; var mode = placement;
var target = PlacementTarget ?? this.GetVisualParent<Control>();
if (target?.GetVisualRoot() == null) if (target?.GetVisualRoot() == null)
{ {
mode = PlacementMode.Pointer; mode = PlacementMode.Pointer;
} }
switch (mode) switch (mode)
{ {
case PlacementMode.Pointer: case PlacementMode.Pointer:
if(PopupRoot != null) if (popupRoot != null)
{ {
// Scales the Horizontal and Vertical offset to screen co-ordinates. // Scales the Horizontal and Vertical offset to screen co-ordinates.
var screenOffset = new Point(HorizontalOffset * (PopupRoot as ILayoutRoot).LayoutScaling, VerticalOffset * (PopupRoot as ILayoutRoot).LayoutScaling); var screenOffset = new Point(horizontalOffset * (popupRoot as ILayoutRoot).LayoutScaling,
return (((IInputRoot)PopupRoot)?.MouseDevice?.Position ?? default(Point)) + screenOffset; verticalOffset * (popupRoot as ILayoutRoot).LayoutScaling);
return (((IInputRoot)popupRoot)?.MouseDevice?.Position ?? default(Point)) + screenOffset;
} }
return default(Point); return default(Point);
case PlacementMode.Bottom: case PlacementMode.Bottom:
return target?.PointToScreen(new Point(0 + horizontalOffset, target.Bounds.Height + verticalOffset)) ??
return target?.PointToScreen(new Point(0 + HorizontalOffset, target.Bounds.Height + VerticalOffset)) ?? zero; zero;
case PlacementMode.Right: case PlacementMode.Right:
return target?.PointToScreen(new Point(target.Bounds.Width + HorizontalOffset, 0 + VerticalOffset)) ?? zero; return target?.PointToScreen(new Point(target.Bounds.Width + horizontalOffset, 0 + verticalOffset)) ?? zero;
default: default:
throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); throw new InvalidOperationException("Invalid value for Popup.PlacementMode");

236
src/Avalonia.Controls/ToolTip.cs

@ -3,11 +3,7 @@
using System; using System;
using System.Reactive.Linq; using System.Reactive.Linq;
using System.Reactive.Subjects;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Threading;
using Avalonia.VisualTree;
namespace Avalonia.Controls namespace Avalonia.Controls
{ {
@ -29,29 +25,50 @@ namespace Avalonia.Controls
AvaloniaProperty.RegisterAttached<ToolTip, Control, object>("Tip"); AvaloniaProperty.RegisterAttached<ToolTip, Control, object>("Tip");
/// <summary> /// <summary>
/// The popup window used to display the active tooltip. /// Defines the ToolTip.IsOpen attached property.
/// </summary> /// </summary>
private static PopupRoot s_popup; public static readonly AttachedProperty<bool> IsOpenProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, bool>("IsOpen");
/// <summary> /// <summary>
/// The control that the currently visible tooltip is attached to. /// Defines the ToolTip.Placement property.
/// </summary> /// </summary>
private static Control s_current; public static readonly AttachedProperty<PlacementMode> PlacementProperty =
AvaloniaProperty.RegisterAttached<Popup, Control, PlacementMode>("Placement", defaultValue: PlacementMode.Pointer);
/// <summary> /// <summary>
/// Observable fired when a tooltip should be displayed for a control. The output from this /// Defines the ToolTip.HorizontalOffset property.
/// observable is throttled and calls <see cref="ShowToolTip(Control)"/> when the time
/// period expires.
/// </summary> /// </summary>
private static readonly Subject<Control> s_show = new Subject<Control>(); public static readonly AttachedProperty<double> HorizontalOffsetProperty =
AvaloniaProperty.RegisterAttached<Popup, Control, double>("HorizontalOffset");
/// <summary>
/// Defines the ToolTip.VerticalOffset property.
/// </summary>
public static readonly AttachedProperty<double> VerticalOffsetProperty =
AvaloniaProperty.RegisterAttached<Popup, Control, double>("VerticalOffset", 20);
/// <summary>
/// Defines the ToolTip.ShowDelay property.
/// </summary>
public static readonly AttachedProperty<int> ShowDelayProperty =
AvaloniaProperty.RegisterAttached<Popup, Control, int>("ShowDelay", 400);
/// <summary>
/// Stores the curernt <see cref="ToolTip"/> instance in the control.
/// </summary>
private static readonly AttachedProperty<ToolTip> ToolTipProperty =
AvaloniaProperty.RegisterAttached<ToolTip, Control, ToolTip>("ToolTip");
private PopupRoot _popup;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="ToolTip"/> class. /// Initializes static members of the <see cref="ToolTip"/> class.
/// </summary> /// </summary>
static ToolTip() static ToolTip()
{ {
TipProperty.Changed.Subscribe(TipChanged); TipProperty.Changed.Subscribe(ToolTipService.Instance.TipChanged);
s_show.Throttle(TimeSpan.FromSeconds(0.5), AvaloniaScheduler.Instance).Subscribe(ShowToolTip); IsOpenProperty.Changed.Subscribe(IsOpenChanged);
} }
/// <summary> /// <summary>
@ -77,101 +94,160 @@ namespace Avalonia.Controls
} }
/// <summary> /// <summary>
/// called when the <see cref="TipProperty"/> property changes on a control. /// Gets the value of the ToolTip.IsOpen attached property.
/// </summary> /// </summary>
/// <param name="e">The event args.</param> /// <param name="element">The control to get the property from.</param>
private static void TipChanged(AvaloniaPropertyChangedEventArgs e) /// <returns>
/// A value indicating whether the tool tip is visible.
/// </returns>
public static bool GetIsOpen(Control element)
{ {
var control = (Control)e.Sender; return element.GetValue(IsOpenProperty);
}
if (e.OldValue != null) /// <summary>
{ /// Sets the value of the ToolTip.IsOpen attached property.
control.PointerEnter -= ControlPointerEnter; /// </summary>
control.PointerLeave -= ControlPointerLeave; /// <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);
}
if (e.NewValue != null) /// <summary>
{ /// Gets the value of the ToolTip.Placement attached property.
control.PointerEnter += ControlPointerEnter; /// </summary>
control.PointerLeave += ControlPointerLeave; /// <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> /// <summary>
/// Shows a tooltip for the specified control. /// Sets the value of the ToolTip.Placement attached property.
/// </summary> /// </summary>
/// <param name="control">The control.</param> /// <param name="element">The control to get the property from.</param>
private static void ShowToolTip(Control control) /// <param name="value">A value indicating how the tool tip is positioned.</param>
public static void SetPlacement(Control element, PlacementMode value)
{ {
if (control != null && control.IsVisible && control.GetVisualRoot() != null) element.SetValue(PlacementProperty, value);
{ }
var cp = (control.GetVisualRoot() as IInputRoot)?.MouseDevice?.GetPosition(control);
if (cp.HasValue && control.IsVisible && new Rect(control.Bounds.Size).Contains(cp.Value)) /// <summary>
{ /// Gets the value of the ToolTip.HorizontalOffset attached property.
var position = control.PointToScreen(cp.Value) + new Vector(0, 22); /// </summary>
/// <param name="element">The control to get the property from.</param>
if (s_popup == null) /// <returns>
{ /// A value indicating how the tool tip is positioned.
s_popup = new PopupRoot(); /// </returns>
s_popup.Content = new ToolTip(); public static double GetHorizontalOffset(Control element)
} {
else return element.GetValue(HorizontalOffsetProperty);
{ }
((ISetLogicalParent)s_popup).SetParent(null);
} /// <summary>
/// Sets the value of the ToolTip.HorizontalOffset attached property.
((ISetLogicalParent)s_popup).SetParent(control); /// </summary>
((ToolTip)s_popup.Content).Content = GetTip(control); /// <param name="element">The control to get the property from.</param>
s_popup.Position = position; /// <param name="value">A value indicating how the tool tip is positioned.</param>
s_popup.Show(); public static void SetHorizontalOffset(Control element, double value)
{
s_current = control; 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> /// <summary>
/// Called when the pointer enters a control with an attached tooltip. /// Gets the value of the ToolTip.ShowDelay attached property.
/// </summary> /// </summary>
/// <param name="sender">The event sender.</param> /// <param name="element">The control to get the property from.</param>
/// <param name="e">The event args.</param> /// <returns>
private static void ControlPointerEnter(object sender, PointerEventArgs e) /// A value indicating the time, in milliseconds, before a tool tip opens.
/// </returns>
public static int GetShowDelay(Control element)
{ {
s_current = (Control)sender; return element.GetValue(ShowDelayProperty);
s_show.OnNext(s_current);
} }
/// <summary> /// <summary>
/// Called when the pointer leaves a control with an attached tooltip. /// Sets the value of the ToolTip.ShowDelay attached property.
/// </summary> /// </summary>
/// <param name="sender">The event sender.</param> /// <param name="element">The control to get the property from.</param>
/// <param name="e">The event args.</param> /// <param name="value">A value indicating the time, in milliseconds, before a tool tip opens.</param>
private static void ControlPointerLeave(object sender, PointerEventArgs e) public static void SetShowDelay(Control element, int value)
{ {
var control = (Control)sender; element.SetValue(ShowDelayProperty, value);
}
private static void IsOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
if (control == s_current) if ((bool)e.NewValue)
{ {
if (s_popup != null) var tip = GetTip(control);
if (tip == null) return;
var toolTip = control.GetValue(ToolTipProperty);
if (toolTip == null || (tip != toolTip && tip != toolTip.Content))
{ {
DisposeTooltip(); toolTip?.Close();
s_show.OnNext(null);
toolTip = tip as ToolTip ?? new ToolTip { Content = tip };
control.SetValue(ToolTipProperty, toolTip);
} }
toolTip.Open(control);
}
else
{
var toolTip = control.GetValue(ToolTipProperty);
toolTip?.Close();
} }
} }
private static void DisposeTooltip() private void Open(Control control)
{ {
if (s_popup != null) Close();
{
// Clear the ToolTip's Content in case it has control content: this will _popup = new PopupRoot { Content = this };
// reset its visual parent allowing it to be used again. ((ISetLogicalParent)_popup).SetParent(control);
((ToolTip)s_popup.Content).Content = null; _popup.Position = Popup.GetPosition(control, GetPlacement(control), _popup,
GetHorizontalOffset(control), GetVerticalOffset(control));
_popup.Show();
}
// Dispose of the popup. private void Close()
s_popup.Dispose(); {
s_popup = null; if (_popup != null)
{
_popup.Content = null;
_popup.Hide();
_popup = null;
} }
} }
} }

98
src/Avalonia.Controls/ToolTipService.cs

@ -0,0 +1,98 @@
using System;
using Avalonia.Input;
using Avalonia.Threading;
namespace Avalonia.Controls
{
/// <summary>
/// Handeles <see cref="ToolTip"/> interaction with controls.
/// </summary>
internal sealed class ToolTipService
{
public static ToolTipService Instance { get; } = new ToolTipService();
private DispatcherTimer _timer;
private ToolTipService() { }
/// <summary>
/// called when the <see cref="ToolTip.TipProperty"/> property changes on a control.
/// </summary>
/// <param name="e">The event args.</param>
internal void TipChanged(AvaloniaPropertyChangedEventArgs e)
{
var control = (Control)e.Sender;
if (e.OldValue != null)
{
control.PointerEnter -= ControlPointerEnter;
control.PointerLeave -= ControlPointerLeave;
}
if (e.NewValue != null)
{
control.PointerEnter += ControlPointerEnter;
control.PointerLeave += ControlPointerLeave;
}
}
/// <summary>
/// Called when the pointer enters a control with an attached tooltip.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void ControlPointerEnter(object sender, PointerEventArgs e)
{
StopTimer();
var control = (Control)sender;
var showDelay = ToolTip.GetShowDelay(control);
if (showDelay == 0)
{
Open(control);
}
else
{
StartShowTimer(showDelay, control);
}
}
/// <summary>
/// Called when the pointer leaves a control with an attached tooltip.
/// </summary>
/// <param name="sender">The event sender.</param>
/// <param name="e">The event args.</param>
private void ControlPointerLeave(object sender, PointerEventArgs e)
{
var control = (Control)sender;
Close(control);
}
private void StartShowTimer(int showDelay, Control control)
{
_timer = new DispatcherTimer { Interval = TimeSpan.FromMilliseconds(showDelay) };
_timer.Tick += (o, e) => Open(control);
_timer.Start();
}
private void Open(Control control)
{
StopTimer();
ToolTip.SetIsOpen(control, true);
}
private void Close(Control control)
{
StopTimer();
ToolTip.SetIsOpen(control, false);
}
private void StopTimer()
{
_timer?.Stop();
_timer = null;
}
}
}
Loading…
Cancel
Save