PopupHelper.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497
  1. // (c) Copyright Microsoft Corporation.
  2. // This source is subject to the Microsoft Public License (Ms-PL).
  3. // Please see http://go.microsoft.com/fwlink/?LinkID=131993] for details.
  4. // All other rights reserved.
  5. using System;
  6. using System.Diagnostics;
  7. using System.Diagnostics.CodeAnalysis;
  8. using System.Windows.Controls.Primitives;
  9. using System.Windows.Input;
  10. using System.Windows.Interop;
  11. using System.Windows.Media;
  12. namespace System.Windows.Controls
  13. {
  14. /// <summary>
  15. /// PopupHelper is a simple wrapper type that helps abstract platform
  16. /// differences out of the Popup.
  17. /// </summary>
  18. internal class PopupHelper
  19. {
  20. #if SILVERLIGHT
  21. /// <summary>
  22. /// A value indicating whether Silverlight has loaded at least once,
  23. /// so that the wrapping canvas is not recreated.
  24. /// </summary>
  25. private bool _hasControlLoaded;
  26. #endif
  27. /// <summary>
  28. /// Gets a value indicating whether a visual popup state is being used
  29. /// in the current template for the Closed state. Setting this value to
  30. /// true will delay the actual setting of Popup.IsOpen to false until
  31. /// after the visual state's transition for Closed is complete.
  32. /// </summary>
  33. public bool UsesClosingVisualState { get; private set; }
  34. /// <summary>
  35. /// Gets or sets the parent control.
  36. /// </summary>
  37. private Control Parent { get; set; }
  38. #if SILVERLIGHT
  39. /// <summary>
  40. /// Gets or sets the expansive area outside of the popup.
  41. /// </summary>
  42. private Canvas OutsidePopupCanvas { get; set; }
  43. /// <summary>
  44. /// Gets or sets the canvas for the popup child.
  45. /// </summary>
  46. private Canvas PopupChildCanvas { get; set; }
  47. #endif
  48. /// <summary>
  49. /// Gets or sets the maximum drop down height value.
  50. /// </summary>
  51. public double MaxDropDownHeight { get; set; }
  52. /// <summary>
  53. /// Gets the Popup control instance.
  54. /// </summary>
  55. public Popup Popup { get; private set; }
  56. /// <summary>
  57. /// Gets or sets a value indicating whether the actual Popup is open.
  58. /// </summary>
  59. [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Provided for completeness.")]
  60. public bool IsOpen
  61. {
  62. get { return Popup.IsOpen; }
  63. set { Popup.IsOpen = value; }
  64. }
  65. /// <summary>
  66. /// Gets or sets the popup child framework element. Can be used if an
  67. /// assumption is made on the child type.
  68. /// </summary>
  69. private FrameworkElement PopupChild { get; set; }
  70. /// <summary>
  71. /// The Closed event is fired after the Popup closes.
  72. /// </summary>
  73. public event EventHandler Closed;
  74. /// <summary>
  75. /// Fired when the popup children have a focus event change, allows the
  76. /// parent control to update visual states or react to the focus state.
  77. /// </summary>
  78. public event EventHandler FocusChanged;
  79. /// <summary>
  80. /// Fired when the popup children intercept an event that may indicate
  81. /// the need for a visual state update by the parent control.
  82. /// </summary>
  83. public event EventHandler UpdateVisualStates;
  84. /// <summary>
  85. /// Initializes a new instance of the PopupHelper class.
  86. /// </summary>
  87. /// <param name="parent">The parent control.</param>
  88. public PopupHelper(Control parent)
  89. {
  90. Debug.Assert(parent != null, "Parent should not be null.");
  91. Parent = parent;
  92. }
  93. /// <summary>
  94. /// Initializes a new instance of the PopupHelper class.
  95. /// </summary>
  96. /// <param name="parent">The parent control.</param>
  97. /// <param name="popup">The Popup template part.</param>
  98. public PopupHelper(Control parent, Popup popup)
  99. : this(parent)
  100. {
  101. Popup = popup;
  102. }
  103. /// <summary>
  104. /// Arrange the popup.
  105. /// </summary>
  106. [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This try-catch pattern is used by other popup controls to keep the runtime up.")]
  107. public void Arrange()
  108. {
  109. if (Popup == null
  110. || PopupChild == null
  111. #if SILVERLIGHT
  112. || OutsidePopupCanvas == null
  113. #endif
  114. || Application.Current == null
  115. #if SILVERLIGHT
  116. || Application.Current.Host == null
  117. || Application.Current.Host.Content == null
  118. #endif
  119. || false)
  120. {
  121. return;
  122. }
  123. #if SILVERLIGHT
  124. Content hostContent = Application.Current.Host.Content;
  125. double rootWidth = hostContent.ActualWidth;
  126. double rootHeight = hostContent.ActualHeight;
  127. #else
  128. UIElement u = Parent;
  129. if (Application.Current.Windows.Count > 0)
  130. {
  131. // TODO: USE THE CURRENT WINDOW INSTEAD! WALK THE TREE!
  132. u = Application.Current.Windows[0];
  133. }
  134. while ((u as Window) == null && u != null)
  135. {
  136. u = VisualTreeHelper.GetParent(u) as UIElement;
  137. }
  138. Window w = u as Window;
  139. if (w == null)
  140. {
  141. return;
  142. }
  143. double rootWidth = w.ActualWidth;
  144. double rootHeight = w.ActualHeight;
  145. #endif
  146. double popupContentWidth = PopupChild.ActualWidth;
  147. double popupContentHeight = PopupChild.ActualHeight;
  148. if (rootHeight == 0 || rootWidth == 0 || popupContentWidth == 0 || popupContentHeight == 0)
  149. {
  150. return;
  151. }
  152. double rootOffsetX = 0;
  153. double rootOffsetY = 0;
  154. #if SILVERLIGHT
  155. // Getting the transform will throw if the popup is no longer in
  156. // the visual tree. This can happen if you first open the popup
  157. // and then click on something else on the page that removes it
  158. // from the live tree.
  159. MatrixTransform mt = null;
  160. try
  161. {
  162. mt = Parent.TransformToVisual(null) as MatrixTransform;
  163. }
  164. catch
  165. {
  166. OnClosed(EventArgs.Empty); // IsDropDownOpen = false;
  167. }
  168. if (mt == null)
  169. {
  170. return;
  171. }
  172. rootOffsetX = mt.Matrix.OffsetX;
  173. rootOffsetY = mt.Matrix.OffsetY;
  174. #endif
  175. double myControlHeight = Parent.ActualHeight;
  176. double myControlWidth = Parent.ActualWidth;
  177. // Use or come up with a maximum popup height.
  178. double popupMaxHeight = MaxDropDownHeight;
  179. if (double.IsInfinity(popupMaxHeight) || double.IsNaN(popupMaxHeight))
  180. {
  181. popupMaxHeight = (rootHeight - myControlHeight) * 3 / 5;
  182. }
  183. popupContentWidth = Math.Min(popupContentWidth, rootWidth);
  184. popupContentHeight = Math.Min(popupContentHeight, popupMaxHeight);
  185. popupContentWidth = Math.Max(myControlWidth, popupContentWidth);
  186. // We prefer to align the popup box with the left edge of the
  187. // control, if it will fit.
  188. double popupX = rootOffsetX;
  189. if (rootWidth < popupX + popupContentWidth)
  190. {
  191. // Since it doesn't fit when strictly left aligned, we shift it
  192. // to the left until it does fit.
  193. popupX = rootWidth - popupContentWidth;
  194. popupX = Math.Max(0, popupX);
  195. }
  196. // We prefer to put the popup below the combobox if it will fit.
  197. bool below = true;
  198. double popupY = rootOffsetY + myControlHeight;
  199. if (rootHeight < popupY + popupContentHeight)
  200. {
  201. below = false;
  202. // It doesn't fit below the combobox, lets try putting it above
  203. // the combobox.
  204. popupY = rootOffsetY - popupContentHeight;
  205. if (popupY < 0)
  206. {
  207. // doesn't really fit below either. Now we just pick top
  208. // or bottom based on wich area is bigger.
  209. if (rootOffsetY < (rootHeight - myControlHeight) / 2)
  210. {
  211. below = true;
  212. popupY = rootOffsetY + myControlHeight;
  213. }
  214. else
  215. {
  216. below = false;
  217. popupY = rootOffsetY - popupContentHeight;
  218. }
  219. }
  220. }
  221. // Now that we have positioned the popup we may need to truncate
  222. // its size.
  223. popupMaxHeight = below ? Math.Min(rootHeight - popupY, popupMaxHeight) : Math.Min(rootOffsetY, popupMaxHeight);
  224. Popup.HorizontalOffset = 0;
  225. Popup.VerticalOffset = 0;
  226. #if SILVERLIGHT
  227. OutsidePopupCanvas.Width = rootWidth;
  228. OutsidePopupCanvas.Height = rootHeight;
  229. // Transform the transparent canvas to the plugin's coordinate
  230. // space origin.
  231. Matrix transformToRootMatrix = mt.Matrix;
  232. Matrix newMatrix;
  233. transformToRootMatrix.Invert(out newMatrix);
  234. mt.Matrix = newMatrix;
  235. OutsidePopupCanvas.RenderTransform = mt;
  236. #endif
  237. PopupChild.MinWidth = myControlWidth;
  238. PopupChild.MaxWidth = rootWidth;
  239. PopupChild.MinHeight = 0;
  240. PopupChild.MaxHeight = Math.Max(0, popupMaxHeight);
  241. PopupChild.Width = popupContentWidth;
  242. // PopupChild.Height = popupContentHeight;
  243. PopupChild.HorizontalAlignment = HorizontalAlignment.Left;
  244. PopupChild.VerticalAlignment = VerticalAlignment.Top;
  245. // Set the top left corner for the actual drop down.
  246. Canvas.SetLeft(PopupChild, popupX - rootOffsetX);
  247. Canvas.SetTop(PopupChild, popupY - rootOffsetY);
  248. }
  249. /// <summary>
  250. /// Fires the Closed event.
  251. /// </summary>
  252. /// <param name="e">The event data.</param>
  253. private void OnClosed(EventArgs e)
  254. {
  255. EventHandler handler = Closed;
  256. if (handler != null)
  257. {
  258. handler(this, e);
  259. }
  260. }
  261. /// <summary>
  262. /// Actually closes the popup after the VSM state animation completes.
  263. /// </summary>
  264. /// <param name="sender">Event source.</param>
  265. /// <param name="e">Event arguments.</param>
  266. private void OnPopupClosedStateChanged(object sender, VisualStateChangedEventArgs e)
  267. {
  268. // Delayed closing of the popup until now
  269. if (e != null && e.NewState != null && e.NewState.Name == VisualStates.StatePopupClosed)
  270. {
  271. if (Popup != null)
  272. {
  273. Popup.IsOpen = false;
  274. }
  275. OnClosed(EventArgs.Empty);
  276. }
  277. }
  278. /// <summary>
  279. /// Should be called by the parent control before the base
  280. /// OnApplyTemplate method is called.
  281. /// </summary>
  282. public void BeforeOnApplyTemplate()
  283. {
  284. if (UsesClosingVisualState)
  285. {
  286. // Unhook the event handler for the popup closed visual state group.
  287. // This code is used to enable visual state transitions before
  288. // actually setting the underlying Popup.IsOpen property to false.
  289. VisualStateGroup groupPopupClosed = VisualStates.TryGetVisualStateGroup(Parent, VisualStates.GroupPopup);
  290. if (null != groupPopupClosed)
  291. {
  292. groupPopupClosed.CurrentStateChanged -= OnPopupClosedStateChanged;
  293. UsesClosingVisualState = false;
  294. }
  295. }
  296. if (Popup != null)
  297. {
  298. Popup.Closed -= Popup_Closed;
  299. }
  300. }
  301. /// <summary>
  302. /// Should be called by the parent control after the base
  303. /// OnApplyTemplate method is called.
  304. /// </summary>
  305. public void AfterOnApplyTemplate()
  306. {
  307. if (Popup != null)
  308. {
  309. Popup.Closed += Popup_Closed;
  310. }
  311. VisualStateGroup groupPopupClosed = VisualStates.TryGetVisualStateGroup(Parent, VisualStates.GroupPopup);
  312. if (null != groupPopupClosed)
  313. {
  314. groupPopupClosed.CurrentStateChanged += OnPopupClosedStateChanged;
  315. UsesClosingVisualState = true;
  316. }
  317. // TODO: Consider moving to the DropDownPopup setter
  318. // TODO: Although in line with other implementations, what happens
  319. // when the template is swapped out?
  320. if (Popup != null)
  321. {
  322. PopupChild = Popup.Child as FrameworkElement;
  323. if (PopupChild != null)
  324. {
  325. #if SILVERLIGHT
  326. // For Silverlight only, we just create the popup child with
  327. // canvas a single time.
  328. if (!_hasControlLoaded)
  329. {
  330. _hasControlLoaded = true;
  331. // Replace the poup child with a canvas
  332. PopupChildCanvas = new Canvas();
  333. Popup.Child = PopupChildCanvas;
  334. OutsidePopupCanvas = new Canvas();
  335. OutsidePopupCanvas.Background = new SolidColorBrush(Colors.Transparent);
  336. OutsidePopupCanvas.MouseLeftButtonDown += OutsidePopup_MouseLeftButtonDown;
  337. PopupChildCanvas.Children.Add(OutsidePopupCanvas);
  338. PopupChildCanvas.Children.Add(PopupChild);
  339. }
  340. #endif
  341. PopupChild.GotFocus += PopupChild_GotFocus;
  342. PopupChild.LostFocus += PopupChild_LostFocus;
  343. PopupChild.MouseEnter += PopupChild_MouseEnter;
  344. PopupChild.MouseLeave += PopupChild_MouseLeave;
  345. PopupChild.SizeChanged += PopupChild_SizeChanged;
  346. }
  347. }
  348. }
  349. /// <summary>
  350. /// The size of the popup child has changed.
  351. /// </summary>
  352. /// <param name="sender">The source object.</param>
  353. /// <param name="e">The event data.</param>
  354. private void PopupChild_SizeChanged(object sender, SizeChangedEventArgs e)
  355. {
  356. Arrange();
  357. }
  358. /// <summary>
  359. /// The mouse has clicked outside of the popup.
  360. /// </summary>
  361. /// <param name="sender">The source object.</param>
  362. /// <param name="e">The event data.</param>
  363. private void OutsidePopup_MouseLeftButtonDown(object sender, MouseButtonEventArgs e)
  364. {
  365. if (Popup != null)
  366. {
  367. Popup.IsOpen = false;
  368. }
  369. }
  370. /// <summary>
  371. /// Connected to the Popup Closed event and fires the Closed event.
  372. /// </summary>
  373. /// <param name="sender">The source object.</param>
  374. /// <param name="e">The event data.</param>
  375. private void Popup_Closed(object sender, EventArgs e)
  376. {
  377. OnClosed(EventArgs.Empty);
  378. }
  379. /// <summary>
  380. /// Connected to several events that indicate that the FocusChanged
  381. /// event should bubble up to the parent control.
  382. /// </summary>
  383. /// <param name="e">The event data.</param>
  384. private void OnFocusChanged(EventArgs e)
  385. {
  386. EventHandler handler = FocusChanged;
  387. if (handler != null)
  388. {
  389. handler(this, e);
  390. }
  391. }
  392. /// <summary>
  393. /// Fires the UpdateVisualStates event.
  394. /// </summary>
  395. /// <param name="e">The event data.</param>
  396. private void OnUpdateVisualStates(EventArgs e)
  397. {
  398. EventHandler handler = UpdateVisualStates;
  399. if (handler != null)
  400. {
  401. handler(this, e);
  402. }
  403. }
  404. /// <summary>
  405. /// The popup child has received focus.
  406. /// </summary>
  407. /// <param name="sender">The source object.</param>
  408. /// <param name="e">The event data.</param>
  409. private void PopupChild_GotFocus(object sender, RoutedEventArgs e)
  410. {
  411. OnFocusChanged(EventArgs.Empty);
  412. }
  413. /// <summary>
  414. /// The popup child has lost focus.
  415. /// </summary>
  416. /// <param name="sender">The source object.</param>
  417. /// <param name="e">The event data.</param>
  418. private void PopupChild_LostFocus(object sender, RoutedEventArgs e)
  419. {
  420. OnFocusChanged(EventArgs.Empty);
  421. }
  422. /// <summary>
  423. /// The popup child has had the mouse enter its bounds.
  424. /// </summary>
  425. /// <param name="sender">The source object.</param>
  426. /// <param name="e">The event data.</param>
  427. private void PopupChild_MouseEnter(object sender, MouseEventArgs e)
  428. {
  429. OnUpdateVisualStates(EventArgs.Empty);
  430. }
  431. /// <summary>
  432. /// The mouse has left the popup child's bounds.
  433. /// </summary>
  434. /// <param name="sender">The source object.</param>
  435. /// <param name="e">The event data.</param>
  436. private void PopupChild_MouseLeave(object sender, MouseEventArgs e)
  437. {
  438. OnUpdateVisualStates(EventArgs.Empty);
  439. }
  440. }
  441. }