in class/Microsoft.SilverlightControls/:
[moon.git] / class / Microsoft.SilverlightControls / Controls / Src / ToolTip / ToolTip.cs
blob4dd8aa0861df8e49932bf4c5746e204820882167
1 // Copyright © Microsoft Corporation.
2 // This source is subject to the Microsoft Source License for Silverlight Controls (March 2008 Release).
3 // Please see http://go.microsoft.com/fwlink/?LinkID=111693 for details.
4 // All other rights reserved.
6 using System.Diagnostics;
7 using System.Windows.Markup;
8 using System.Windows.Media;
9 using System.Windows.Media.Animation;
10 using System.Windows.Controls.Primitives;
11 using System.Windows.Controls;
13 namespace System.Windows.Controls
15 /// <summary>
16 /// A control to display information when the user hovers over an owner control
17 /// </summary>
18 [TemplatePart(Name = System.Windows.Controls.ToolTip.NormalStateName, Type = typeof(Storyboard))]
19 [TemplatePart(Name = System.Windows.Controls.ToolTip.RootElementName, Type = typeof(FrameworkElement))]
20 [TemplatePart(Name = System.Windows.Controls.ToolTip.VisibleStateName, Type = typeof(Storyboard))]
21 public partial class ToolTip : ContentControl
23 #region Constants
25 private const double TOOLTIP_tolerance = 2.0;
27 #endregion Constants
29 #region HorizontalOffset Property
31 /// <summary>
32 /// Determines a horizontal offset in pixels from the left side of
33 /// the mouse bounding rectangle to the left side of the ToolTip.
34 /// </summary>
35 public double HorizontalOffset
37 get { return (double)GetValue(HorizontalOffsetProperty);}
38 set { SetValue(HorizontalOffsetProperty, value);}
41 /// <summary>
42 /// Identifies the HorizontalOffset dependency property.
43 /// </summary>
44 public static readonly DependencyProperty HorizontalOffsetProperty =
45 DependencyProperty.RegisterCore(
46 "HorizontalOffset",
47 typeof(double),
48 typeof(ToolTip),
49 new PropertyMetadata(new PropertyChangedCallback(OnHorizontalOffsetPropertyChanged)));
51 private static void OnHorizontalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
53 // HorizontalOffset dependency property should be defined on a ToolTip
54 ToolTip toolTip = (ToolTip)d;
56 double newOffset = (double)e.NewValue;
57 // Working around temporary limitations in Silverlight:
58 // perform inequality test
59 //
61 if (newOffset != (double)e.OldValue)
63 toolTip.OnOffsetChanged(newOffset, 0);
67 #endregion HorizontalOffset Property
69 #region PlacementTarget Property
70 /// <summary>
71 /// Determines a horizontal offset in pixels from the left side of
72 /// the mouse bounding rectangle to the left side of the ToolTip.
73 /// </summary>
74 public UIElement PlacementTarget
76 get { return (UIElement)GetValue(PlacementTargetProperty);}
77 set { SetValue(PlacementTargetProperty, value);}
80 /// <summary>
81 /// Identifies the HorizontalOffset dependency property.
82 /// </summary>
83 public static readonly DependencyProperty PlacementTargetProperty =
84 DependencyProperty.RegisterCore(
85 "PlacementTarget",
86 typeof(UIElement),
87 typeof(ToolTip),
88 new PropertyMetadata(new PropertyChangedCallback(OnPlacementTargetPropertyChanged)));
90 private static void OnPlacementTargetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
93 #endregion PlacementTarget Property
95 #region Placement Property
96 /// <summary>
97 /// Determines a horizontal offset in pixels from the left side of
98 /// the mouse bounding rectangle to the left side of the ToolTip.
99 /// </summary>
100 public PlacementMode Placement
102 get { return (PlacementMode)GetValue(PlacementProperty);}
103 set { SetValue(PlacementProperty, value);}
106 /// <summary>
107 /// Identifies the HorizontalOffset dependency property.
108 /// </summary>
109 public static readonly DependencyProperty PlacementProperty =
110 DependencyProperty.RegisterCore(
111 "Placement",
112 typeof(PlacementMode),
113 typeof(ToolTip),
114 new PropertyMetadata(new PropertyChangedCallback(OnPlacementPropertyChanged)));
116 private static void OnPlacementPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
119 #endregion PlacementTarget Property
121 #region IsOpen Property
123 /// <summary>
124 /// Gets a value that determines whether tooltip is displayed or not.
125 /// </summary>
126 public bool IsOpen
128 get { return (bool)GetValue(IsOpenProperty);}
129 set { SetValue(IsOpenProperty, value);}
132 /// <summary>
133 /// Identifies the IsOpen dependency property.
134 /// </summary>
135 public static readonly DependencyProperty IsOpenProperty =
136 DependencyProperty.RegisterCore(
137 "IsOpen",
138 typeof(bool),
139 typeof(ToolTip),
140 new PropertyMetadata(new PropertyChangedCallback(OnIsOpenPropertyChanged)));
142 private static void OnIsOpenPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
144 // IsOpen dependency property should be defined on a ToolTip
145 ToolTip toolTip = (ToolTip)d;
147 if (((bool)e.NewValue != (bool)e.OldValue))
149 toolTip.OnIsOpenChanged((bool)e.NewValue);
153 #endregion IsOpen Property
155 #region VerticalOffset Property
157 /// <summary>
158 /// Determines a vertical offset in pixels from the bottom of the
159 /// mouse bounding rectangle to the top of the ToolTip.
160 /// </summary>
161 public double VerticalOffset
163 get { return (double)GetValue(VerticalOffsetProperty);}
164 set { SetValue(VerticalOffsetProperty, value);}
167 /// <summary>
168 /// Identifies the VerticalOffset dependency property.
169 /// </summary>
170 public static readonly DependencyProperty VerticalOffsetProperty =
171 DependencyProperty.RegisterCore(
172 "VerticalOffset",
173 typeof(double),
174 typeof(ToolTip),
175 new PropertyMetadata(new PropertyChangedCallback(OnVerticalOffsetPropertyChanged)));
177 private static void OnVerticalOffsetPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
179 // VerticalOffset dependency property should be defined on a ToolTip
180 ToolTip toolTip = (ToolTip)d;
182 double newOffset = (double)e.NewValue;
183 if (newOffset != (double)e.OldValue)
185 toolTip.OnOffsetChanged(0, newOffset);
189 #endregion VerticalOffset Property
191 #region Template Parts
193 /// <summary>
194 /// This storyboard runs when the ToolTip closes.
195 /// </summary>
196 private Storyboard NormalState;
197 private const string NormalStateName = "Normal State";
199 /// <summary>
200 /// Part for the ToolTip.
201 /// </summary>
202 internal FrameworkElement RootElement;
203 internal const string RootElementName = "RootElement";
205 /// <summary>
206 /// This storyboard runs when the ToolTip opens.
207 /// </summary>
208 private Storyboard VisibleState;
209 private const string VisibleStateName = "Visible State";
211 #endregion Template Parts
213 #region Events
215 /// <summary>
216 /// Occurs when a ToolTip is closed and is no longer visible.
217 /// </summary>
218 public event RoutedEventHandler Closed;
220 /// <summary>
221 /// Occurs when a ToolTip becomes visible.
222 /// </summary>
223 public event RoutedEventHandler Opened;
225 #endregion Events
227 #region Data
229 private bool _beginClosing;
230 private bool _closingCompleted = true;
231 private Size _lastSize;
232 private bool _openingCompleted = true;
233 private bool _openPopup;
234 private Popup _parentPopup;
236 private delegate void PerformOnNextTick();
238 internal Popup ParentPopup
240 get { return this._parentPopup; }
241 private set { this._parentPopup = value; }
244 #endregion Data
246 /// <summary>
247 /// Creates a default ToolTip element
248 /// </summary>
249 public ToolTip() : base()
251 DefaultStyleKey = typeof (ToolTip);
252 this.SizeChanged += new SizeChangedEventHandler(OnToolTipSizeChanged);
255 #region Protected Methods
257 /// <summary>
258 /// Apply a template to the ToolTip, invoked from ApplyTemplate
259 /// </summary>
260 public override void OnApplyTemplate()
262 base.OnApplyTemplate();
264 // If the part is not present in the template,
265 // don't display content, but don't throw either
267 // get the element
268 RootElement = GetTemplateChild(System.Windows.Controls.ToolTip.RootElementName) as FrameworkElement;
270 if (RootElement != null)
272 // get the states
273 this.VisibleState = RootElement.Resources[VisibleStateName] as Storyboard;
274 this.NormalState = RootElement.Resources[NormalStateName] as Storyboard;
276 if (this.VisibleState != null)
278 this.VisibleState.Completed += new EventHandler(OnOpeningCompleted);
280 // first time through when the opened event is fired, the storyboard is not
281 // loaded from the template yet, because ApplyTemplate wasn't called yet,
282 // so I start the storyboard manually
285 OnPopupOpened(null, EventArgs.Empty);
288 if (this.NormalState != null)
290 this.NormalState.Completed += new EventHandler(OnClosingCompleted);
295 #endregion Protected Methods
297 #region Private Methods
299 private void BeginClosing()
301 this._beginClosing = false;
303 // close the popup after the animation is completed
304 if (this.NormalState != null)
306 this._closingCompleted = false;
307 this.NormalState.Begin();
311 private void HookupParentPopup()
313 Debug.Assert(this._parentPopup == null, "this._parentPopup should be null, we want to set visual tree once");
315 this._parentPopup = new Popup();
317 this.IsTabStop = false;
319 this._parentPopup.Child = this;
321 // Working around temporary limitations in Silverlight:
322 // set IsHitTestVisible on both the popup and the child
324 this._parentPopup.IsHitTestVisible = false;
325 this.IsHitTestVisible = false;
336 private void OnClosed(RoutedEventArgs e)
338 RoutedEventHandler snapshot = this.Closed;
339 if (snapshot != null)
341 snapshot(this, e);
345 // called when the closing state transition is completed
346 private void OnClosingCompleted(object sender, EventArgs e)
348 this._closingCompleted = true;
349 this._parentPopup.IsOpen = false;
351 // Working around temporary limitations in Silverlight:
352 // send the event manually
355 this.Dispatcher.BeginInvoke(delegate() { OnPopupClosed (null, EventArgs.Empty); });
357 // if the tooltip was forced to stop the closing animation, because it has to reopen,
358 // proceed with open
359 if (this._openPopup)
361 this.Dispatcher.BeginInvoke(delegate() { OpenPopup(); });
365 private void OnIsOpenChanged(bool isOpen)
367 if (isOpen)
369 if (this._parentPopup == null)
371 HookupParentPopup();
372 OpenPopup();
373 return;
376 if (!this._closingCompleted)
378 Debug.Assert(this.NormalState != null);
380 // Completed event for the closing storyboard will open the parent popup
381 // because _openPopup is set
382 this._openPopup = true;
384 this.NormalState.SkipToFill();
385 return;
388 PerformPlacement(this.HorizontalOffset, this.VerticalOffset);
389 OpenPopup();
391 else
393 Debug.Assert(this._parentPopup != null);
395 if (!this._openingCompleted)
397 if (this.NormalState != null)
399 this._beginClosing = true;
401 this.VisibleState.SkipToFill();
402 // delay start of the closing storyboard until the opening one is completed
403 return;
406 if ((this.NormalState == null) || (this.NormalState.Children.Count != 0))
408 // close immediatelly if no storyboard provided
409 this._parentPopup.IsOpen = false;
410 this.Dispatcher.BeginInvoke(delegate () { OnPopupClosed (null, EventArgs.Empty); });
412 else
414 // close the popup after the animation is completed
415 this._closingCompleted = false;
416 this.NormalState.Begin();
421 private void OpenPopup()
423 this._parentPopup.IsOpen = true;
425 // Working around temporary limitations in Silverlight:
426 // send the Opened event manually
428 this.Dispatcher.BeginInvoke(delegate () { OnPopupOpened (null, EventArgs.Empty); });
430 this._openPopup = false;
433 private void OnOffsetChanged(double horizontalOffset, double verticalOffset)
435 if (this._parentPopup == null)
437 return;
440 if (IsOpen)
442 // update the current ToolTip position if needed
443 PerformPlacement(horizontalOffset, verticalOffset);
447 private void OnOpened(RoutedEventArgs e)
449 RoutedEventHandler snapshot = this.Opened;
450 if (snapshot != null)
452 snapshot(this, e);
456 // called when the Visible state transition is completed
457 private void OnOpeningCompleted(object sender, EventArgs e)
459 this._openingCompleted = true;
461 if (this._beginClosing)
463 this.Dispatcher.BeginInvoke(delegate () { BeginClosing(); });
467 private void OnPopupClosed(object source, EventArgs e)
469 OnClosed(new RoutedEventArgs { OriginalSource = this });
472 private void OnPopupOpened(object source, EventArgs e)
475 if (this.VisibleState != null)
477 this._openingCompleted = false;
478 this.VisibleState.Begin();
480 OnOpened(new RoutedEventArgs { OriginalSource = this });
483 internal void OnRootVisualSizeChanged()
485 if (this._parentPopup != null)
487 PerformPlacement(this.HorizontalOffset, this.VerticalOffset);
491 private void OnToolTipSizeChanged(object sender, SizeChangedEventArgs e)
493 this._lastSize = e.NewSize;
494 if (this._parentPopup != null)
496 PerformPlacement(this.HorizontalOffset, this.VerticalOffset);
500 private void PerformClipping(Size size)
502 Point mouse = ToolTipService.MousePosition;
503 RectangleGeometry rectangle = new RectangleGeometry();
504 rectangle.Rect = new Rect(mouse.X, mouse.Y, size.Width, size.Height);
505 ((UIElement)Content).Clip = rectangle;
508 private void PerformPlacement(double horizontalOffset, double verticalOffset)
510 Point mouse = ToolTipService.MousePosition;
512 // align ToolTip with the bottom left corner of mouse bounding rectangle
514 double top = mouse.Y + new TextBlock().FontSize;
515 double left = mouse.X;
517 top += verticalOffset;
518 left += horizontalOffset;
520 double maxY = ToolTipService.RootVisual.ActualHeight;
521 double maxX = ToolTipService.RootVisual.ActualWidth;
523 Rect toolTipRect = new Rect(left, top, this._lastSize.Width, this._lastSize.Height);
524 Rect intersectionRect = new Rect(0, 0, maxX, maxY);
526 intersectionRect.Intersect(toolTipRect);
527 if ((Math.Abs(intersectionRect.Width - toolTipRect.Width) < TOOLTIP_tolerance) &&
528 (Math.Abs(intersectionRect.Height - toolTipRect.Height) < TOOLTIP_tolerance))
530 // ToolTip is almost completely inside the plug-in
531 this._parentPopup.VerticalOffset = top;
532 this._parentPopup.HorizontalOffset = left;
533 return;
535 else if (intersectionRect.Equals(new Rect(0, 0, maxX, maxY)))
537 //ToolTip is bigger than the plug-in
538 PerformClipping(new Size(maxX, maxY));
539 this._parentPopup.VerticalOffset = 0;
540 this._parentPopup.HorizontalOffset = 0;
542 PerformClipping(new Size(maxX, maxY));
543 return;
546 double right = left + toolTipRect.Width;
547 double bottom = top + toolTipRect.Height;
549 if (bottom > maxY)
551 // If the lower edge of the plug-in obscures the ToolTip,
552 // it repositions itself to align with the upper edge of the bounding box of the mouse.
553 bottom = top;
554 top -= toolTipRect.Height;
557 if (top < 0)
559 // If the upper edge of Plug-in obscures the ToolTip,
560 // the control repositions itself to align with the upper edge.
561 // align with the top of the plug-in
562 top = 0;
564 else if (bottom > maxY)
566 // align with the bottom edge
567 top = Math.Max(0, maxY - toolTipRect.Height);
570 if (right > maxX)
572 // If the right edge obscures the ToolTip,
573 // it opens in the opposite direction from the obscuring edge.
574 right = left;
575 left -= toolTipRect.Width;
578 if (left < 0)
580 // If the left edge obscures the ToolTip,
581 // it then aligns with the obscuring screen edge
582 left = 0;
584 else if (right > maxX)
586 // align with the right edge
587 left = Math.Max(0, maxX - toolTipRect.Width);
590 // position the parent Popup
591 this._parentPopup.VerticalOffset = top;
592 this._parentPopup.HorizontalOffset = left;
594 bottom = top + toolTipRect.Height;
595 right = left + toolTipRect.Width;
597 // if right/bottom doesn't fit into the plug-in bounds, clip the ToolTip
598 double dX = right - maxX;
599 double dY = bottom - maxY;
600 if ((dX >= TOOLTIP_tolerance) || (dY >= TOOLTIP_tolerance))
602 PerformClipping(new Size(toolTipRect.Width - dX, toolTipRect.Height - dY));
606 #endregion Private Methods