// (c) Copyright Microsoft Corporation. // This source is subject to the Microsoft Public License (Ms-PL). // Please see http://go.microsoft.com/fwlink/?LinkID=131993] for details. // All other rights reserved. using System; using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.Windows.Controls.Primitives; using System.Windows.Input; using System.Windows.Interop; using System.Windows.Media; namespace System.Windows.Controls { /// /// PopupHelper is a simple wrapper type that helps abstract platform /// differences out of the Popup. /// internal class PopupHelper { #if SILVERLIGHT /// /// A value indicating whether Silverlight has loaded at least once, /// so that the wrapping canvas is not recreated. /// private bool _hasControlLoaded; #endif /// /// Gets a value indicating whether a visual popup state is being used /// in the current template for the Closed state. Setting this value to /// true will delay the actual setting of Popup.IsOpen to false until /// after the visual state's transition for Closed is complete. /// public bool UsesClosingVisualState { get; private set; } /// /// Gets or sets the parent control. /// private Control Parent { get; set; } #if SILVERLIGHT /// /// Gets or sets the expansive area outside of the popup. /// private Canvas OutsidePopupCanvas { get; set; } /// /// Gets or sets the canvas for the popup child. /// private Canvas PopupChildCanvas { get; set; } #endif /// /// Gets or sets the maximum drop down height value. /// public double MaxDropDownHeight { get; set; } /// /// Gets the Popup control instance. /// public Popup Popup { get; private set; } /// /// Gets or sets a value indicating whether the actual Popup is open. /// [SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode", Justification = "Provided for completeness.")] public bool IsOpen { get { return Popup.IsOpen; } set { Popup.IsOpen = value; } } /// /// Gets or sets the popup child framework element. Can be used if an /// assumption is made on the child type. /// private FrameworkElement PopupChild { get; set; } /// /// The Closed event is fired after the Popup closes. /// public event EventHandler Closed; /// /// Fired when the popup children have a focus event change, allows the /// parent control to update visual states or react to the focus state. /// public event EventHandler FocusChanged; /// /// Fired when the popup children intercept an event that may indicate /// the need for a visual state update by the parent control. /// public event EventHandler UpdateVisualStates; /// /// Initializes a new instance of the PopupHelper class. /// /// The parent control. public PopupHelper(Control parent) { Debug.Assert(parent != null, "Parent should not be null."); Parent = parent; } /// /// Initializes a new instance of the PopupHelper class. /// /// The parent control. /// The Popup template part. public PopupHelper(Control parent, Popup popup) : this(parent) { Popup = popup; } /// /// Arrange the popup. /// [SuppressMessage("Microsoft.Design", "CA1031:DoNotCatchGeneralExceptionTypes", Justification = "This try-catch pattern is used by other popup controls to keep the runtime up.")] public void Arrange() { if (Popup == null || PopupChild == null #if SILVERLIGHT || OutsidePopupCanvas == null #endif || Application.Current == null #if SILVERLIGHT || Application.Current.Host == null || Application.Current.Host.Content == null #endif || false) { return; } #if SILVERLIGHT Content hostContent = Application.Current.Host.Content; double rootWidth = hostContent.ActualWidth; double rootHeight = hostContent.ActualHeight; #else UIElement u = Parent; if (Application.Current.Windows.Count > 0) { // TODO: USE THE CURRENT WINDOW INSTEAD! WALK THE TREE! u = Application.Current.Windows[0]; } while ((u as Window) == null && u != null) { u = VisualTreeHelper.GetParent(u) as UIElement; } Window w = u as Window; if (w == null) { return; } double rootWidth = w.ActualWidth; double rootHeight = w.ActualHeight; #endif double popupContentWidth = PopupChild.ActualWidth; double popupContentHeight = PopupChild.ActualHeight; if (rootHeight == 0 || rootWidth == 0 || popupContentWidth == 0 || popupContentHeight == 0) { return; } double rootOffsetX = 0; double rootOffsetY = 0; #if SILVERLIGHT // Getting the transform will throw if the popup is no longer in // the visual tree. This can happen if you first open the popup // and then click on something else on the page that removes it // from the live tree. MatrixTransform mt = null; try { mt = Parent.TransformToVisual(null) as MatrixTransform; } catch { OnClosed(EventArgs.Empty); // IsDropDownOpen = false; } if (mt == null) { return; } rootOffsetX = mt.Matrix.OffsetX; rootOffsetY = mt.Matrix.OffsetY; #endif double myControlHeight = Parent.ActualHeight; double myControlWidth = Parent.ActualWidth; // Use or come up with a maximum popup height. double popupMaxHeight = MaxDropDownHeight; if (double.IsInfinity(popupMaxHeight) || double.IsNaN(popupMaxHeight)) { popupMaxHeight = (rootHeight - myControlHeight) * 3 / 5; } popupContentWidth = Math.Min(popupContentWidth, rootWidth); popupContentHeight = Math.Min(popupContentHeight, popupMaxHeight); popupContentWidth = Math.Max(myControlWidth, popupContentWidth); // We prefer to align the popup box with the left edge of the // control, if it will fit. double popupX = rootOffsetX; if (rootWidth < popupX + popupContentWidth) { // Since it doesn't fit when strictly left aligned, we shift it // to the left until it does fit. popupX = rootWidth - popupContentWidth; popupX = Math.Max(0, popupX); } // We prefer to put the popup below the combobox if it will fit. bool below = true; double popupY = rootOffsetY + myControlHeight; if (rootHeight < popupY + popupContentHeight) { below = false; // It doesn't fit below the combobox, lets try putting it above // the combobox. popupY = rootOffsetY - popupContentHeight; if (popupY < 0) { // doesn't really fit below either. Now we just pick top // or bottom based on wich area is bigger. if (rootOffsetY < (rootHeight - myControlHeight) / 2) { below = true; popupY = rootOffsetY + myControlHeight; } else { below = false; popupY = rootOffsetY - popupContentHeight; } } } // Now that we have positioned the popup we may need to truncate // its size. popupMaxHeight = below ? Math.Min(rootHeight - popupY, popupMaxHeight) : Math.Min(rootOffsetY, popupMaxHeight); Popup.HorizontalOffset = 0; Popup.VerticalOffset = 0; #if SILVERLIGHT OutsidePopupCanvas.Width = rootWidth; OutsidePopupCanvas.Height = rootHeight; // Transform the transparent canvas to the plugin's coordinate // space origin. Matrix transformToRootMatrix = mt.Matrix; Matrix newMatrix; transformToRootMatrix.Invert(out newMatrix); mt.Matrix = newMatrix; OutsidePopupCanvas.RenderTransform = mt; #endif PopupChild.MinWidth = myControlWidth; PopupChild.MaxWidth = rootWidth; PopupChild.MinHeight = 0; PopupChild.MaxHeight = Math.Max(0, popupMaxHeight); PopupChild.Width = popupContentWidth; // PopupChild.Height = popupContentHeight; PopupChild.HorizontalAlignment = HorizontalAlignment.Left; PopupChild.VerticalAlignment = VerticalAlignment.Top; // Set the top left corner for the actual drop down. Canvas.SetLeft(PopupChild, popupX - rootOffsetX); Canvas.SetTop(PopupChild, popupY - rootOffsetY); } /// /// Fires the Closed event. /// /// The event data. private void OnClosed(EventArgs e) { EventHandler handler = Closed; if (handler != null) { handler(this, e); } } /// /// Actually closes the popup after the VSM state animation completes. /// /// Event source. /// Event arguments. private void OnPopupClosedStateChanged(object sender, VisualStateChangedEventArgs e) { // Delayed closing of the popup until now if (e != null && e.NewState != null && e.NewState.Name == VisualStates.StatePopupClosed) { if (Popup != null) { Popup.IsOpen = false; } OnClosed(EventArgs.Empty); } } /// /// Should be called by the parent control before the base /// OnApplyTemplate method is called. /// public void BeforeOnApplyTemplate() { if (UsesClosingVisualState) { // Unhook the event handler for the popup closed visual state group. // This code is used to enable visual state transitions before // actually setting the underlying Popup.IsOpen property to false. VisualStateGroup groupPopupClosed = VisualStates.TryGetVisualStateGroup(Parent, VisualStates.GroupPopup); if (null != groupPopupClosed) { groupPopupClosed.CurrentStateChanged -= OnPopupClosedStateChanged; UsesClosingVisualState = false; } } if (Popup != null) { Popup.Closed -= Popup_Closed; } } /// /// Should be called by the parent control after the base /// OnApplyTemplate method is called. /// public void AfterOnApplyTemplate() { if (Popup != null) { Popup.Closed += Popup_Closed; } VisualStateGroup groupPopupClosed = VisualStates.TryGetVisualStateGroup(Parent, VisualStates.GroupPopup); if (null != groupPopupClosed) { groupPopupClosed.CurrentStateChanged += OnPopupClosedStateChanged; UsesClosingVisualState = true; } // TODO: Consider moving to the DropDownPopup setter // TODO: Although in line with other implementations, what happens // when the template is swapped out? if (Popup != null) { PopupChild = Popup.Child as FrameworkElement; if (PopupChild != null) { #if SILVERLIGHT // For Silverlight only, we just create the popup child with // canvas a single time. if (!_hasControlLoaded) { _hasControlLoaded = true; // Replace the poup child with a canvas PopupChildCanvas = new Canvas(); Popup.Child = PopupChildCanvas; OutsidePopupCanvas = new Canvas(); OutsidePopupCanvas.Background = new SolidColorBrush(Colors.Transparent); OutsidePopupCanvas.MouseLeftButtonDown += OutsidePopup_MouseLeftButtonDown; PopupChildCanvas.Children.Add(OutsidePopupCanvas); PopupChildCanvas.Children.Add(PopupChild); } #endif PopupChild.GotFocus += PopupChild_GotFocus; PopupChild.LostFocus += PopupChild_LostFocus; PopupChild.MouseEnter += PopupChild_MouseEnter; PopupChild.MouseLeave += PopupChild_MouseLeave; PopupChild.SizeChanged += PopupChild_SizeChanged; } } } /// /// The size of the popup child has changed. /// /// The source object. /// The event data. private void PopupChild_SizeChanged(object sender, SizeChangedEventArgs e) { Arrange(); } /// /// The mouse has clicked outside of the popup. /// /// The source object. /// The event data. private void OutsidePopup_MouseLeftButtonDown(object sender, MouseButtonEventArgs e) { if (Popup != null) { Popup.IsOpen = false; } } /// /// Connected to the Popup Closed event and fires the Closed event. /// /// The source object. /// The event data. private void Popup_Closed(object sender, EventArgs e) { OnClosed(EventArgs.Empty); } /// /// Connected to several events that indicate that the FocusChanged /// event should bubble up to the parent control. /// /// The event data. private void OnFocusChanged(EventArgs e) { EventHandler handler = FocusChanged; if (handler != null) { handler(this, e); } } /// /// Fires the UpdateVisualStates event. /// /// The event data. private void OnUpdateVisualStates(EventArgs e) { EventHandler handler = UpdateVisualStates; if (handler != null) { handler(this, e); } } /// /// The popup child has received focus. /// /// The source object. /// The event data. private void PopupChild_GotFocus(object sender, RoutedEventArgs e) { OnFocusChanged(EventArgs.Empty); } /// /// The popup child has lost focus. /// /// The source object. /// The event data. private void PopupChild_LostFocus(object sender, RoutedEventArgs e) { OnFocusChanged(EventArgs.Empty); } /// /// The popup child has had the mouse enter its bounds. /// /// The source object. /// The event data. private void PopupChild_MouseEnter(object sender, MouseEventArgs e) { OnUpdateVisualStates(EventArgs.Empty); } /// /// The mouse has left the popup child's bounds. /// /// The source object. /// The event data. private void PopupChild_MouseLeave(object sender, MouseEventArgs e) { OnUpdateVisualStates(EventArgs.Empty); } } }