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
16 /// A control to display information when the user hovers over an owner control
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
25 private const double TOOLTIP_tolerance
= 2.0;
29 #region HorizontalOffset Property
32 /// Determines a horizontal offset in pixels from the left side of
33 /// the mouse bounding rectangle to the left side of the ToolTip.
35 public double HorizontalOffset
37 get { return (double)GetValue(HorizontalOffsetProperty);}
38 set { SetValue(HorizontalOffsetProperty, value);}
42 /// Identifies the HorizontalOffset dependency property.
44 public static readonly DependencyProperty HorizontalOffsetProperty
=
45 DependencyProperty
.RegisterCore(
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
61 if (newOffset
!= (double)e
.OldValue
)
63 toolTip
.OnOffsetChanged(newOffset
, 0);
67 #endregion HorizontalOffset Property
69 #region PlacementTarget Property
71 /// Determines a horizontal offset in pixels from the left side of
72 /// the mouse bounding rectangle to the left side of the ToolTip.
74 public UIElement PlacementTarget
76 get { return (UIElement)GetValue(PlacementTargetProperty);}
77 set { SetValue(PlacementTargetProperty, value);}
81 /// Identifies the HorizontalOffset dependency property.
83 public static readonly DependencyProperty PlacementTargetProperty
=
84 DependencyProperty
.RegisterCore(
88 new PropertyMetadata(new PropertyChangedCallback(OnPlacementTargetPropertyChanged
)));
90 private static void OnPlacementTargetPropertyChanged(DependencyObject d
, DependencyPropertyChangedEventArgs e
)
93 #endregion PlacementTarget Property
95 #region Placement Property
97 /// Determines a horizontal offset in pixels from the left side of
98 /// the mouse bounding rectangle to the left side of the ToolTip.
100 public PlacementMode Placement
102 get { return (PlacementMode)GetValue(PlacementProperty);}
103 set { SetValue(PlacementProperty, value);}
107 /// Identifies the HorizontalOffset dependency property.
109 public static readonly DependencyProperty PlacementProperty
=
110 DependencyProperty
.RegisterCore(
112 typeof(PlacementMode
),
114 new PropertyMetadata(new PropertyChangedCallback(OnPlacementPropertyChanged
)));
116 private static void OnPlacementPropertyChanged(DependencyObject d
, DependencyPropertyChangedEventArgs e
)
119 #endregion PlacementTarget Property
121 #region IsOpen Property
124 /// Gets a value that determines whether tooltip is displayed or not.
128 get { return (bool)GetValue(IsOpenProperty);}
129 set { SetValue(IsOpenProperty, value);}
133 /// Identifies the IsOpen dependency property.
135 public static readonly DependencyProperty IsOpenProperty
=
136 DependencyProperty
.RegisterCore(
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
158 /// Determines a vertical offset in pixels from the bottom of the
159 /// mouse bounding rectangle to the top of the ToolTip.
161 public double VerticalOffset
163 get { return (double)GetValue(VerticalOffsetProperty);}
164 set { SetValue(VerticalOffsetProperty, value);}
168 /// Identifies the VerticalOffset dependency property.
170 public static readonly DependencyProperty VerticalOffsetProperty
=
171 DependencyProperty
.RegisterCore(
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
194 /// This storyboard runs when the ToolTip closes.
196 private Storyboard NormalState
;
197 private const string NormalStateName
= "Normal State";
200 /// Part for the ToolTip.
202 internal FrameworkElement RootElement
;
203 internal const string RootElementName
= "RootElement";
206 /// This storyboard runs when the ToolTip opens.
208 private Storyboard VisibleState
;
209 private const string VisibleStateName
= "Visible State";
211 #endregion Template Parts
216 /// Occurs when a ToolTip is closed and is no longer visible.
218 public event RoutedEventHandler Closed
;
221 /// Occurs when a ToolTip becomes visible.
223 public event RoutedEventHandler Opened
;
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; }
247 /// Creates a default ToolTip element
249 public ToolTip() : base()
251 DefaultStyleKey
= typeof (ToolTip
);
252 this.SizeChanged
+= new SizeChangedEventHandler(OnToolTipSizeChanged
);
255 #region Protected Methods
258 /// Apply a template to the ToolTip, invoked from ApplyTemplate
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
268 RootElement
= GetTemplateChild(System
.Windows
.Controls
.ToolTip
.RootElementName
) as FrameworkElement
;
270 if (RootElement
!= null)
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)
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,
361 this.Dispatcher
.BeginInvoke(delegate() { OpenPopup(); }
);
365 private void OnIsOpenChanged(bool isOpen
)
369 if (this._parentPopup
== null)
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();
388 PerformPlacement(this.HorizontalOffset
, this.VerticalOffset
);
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
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); }
);
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)
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)
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
;
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
));
546 double right
= left
+ toolTipRect
.Width
;
547 double bottom
= top
+ toolTipRect
.Height
;
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.
554 top
-= toolTipRect
.Height
;
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
564 else if (bottom
> maxY
)
566 // align with the bottom edge
567 top
= Math
.Max(0, maxY
- toolTipRect
.Height
);
572 // If the right edge obscures the ToolTip,
573 // it opens in the opposite direction from the obscuring edge.
575 left
-= toolTipRect
.Width
;
580 // If the left edge obscures the ToolTip,
581 // it then aligns with the obscuring screen edge
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