// ----------------------------------------------------------------------- // // Copyright 2014 MIT Licence. See licence.md for more information. // // ----------------------------------------------------------------------- namespace Perspex.Controls { using System; using Perspex.Interactivity; using Perspex.Collections; using Perspex.Rendering; using Perspex.VisualTree; using Splat; /// /// Displays a popup window. /// public class Popup : Control, ILogical, IVisualTreeHost { /// /// Defines the property. /// public static readonly PerspexProperty ChildProperty = PerspexProperty.Register(nameof(Child)); /// /// Defines the property. /// public static readonly PerspexProperty IsOpenProperty = PerspexProperty.Register(nameof(IsOpen)); /// /// Defines the property. /// public static readonly PerspexProperty PlacementModeProperty = PerspexProperty.Register(nameof(PlacementMode), defaultValue: PlacementMode.Bottom); /// /// Defines the property. /// public static readonly PerspexProperty PlacementTargetProperty = PerspexProperty.Register(nameof(PlacementTarget)); /// /// Defines the property. /// public static readonly PerspexProperty StaysOpenProperty = PerspexProperty.Register(nameof(StaysOpen), true); /// /// The root of the popup. /// private PopupRoot popupRoot; /// /// The top level control of the Popup's visual tree. /// private TopLevel topLevel; /// /// The popup's logical child. /// private PerspexSingleItemList logicalChild = new PerspexSingleItemList(); /// /// Initializes static members of the class. /// static Popup() { IsOpenProperty.Changed.AddClassHandler(x => x.IsOpenChanged); } /// /// Initializes a new instance of the class. /// public Popup() { this.GetObservableWithHistory(ChildProperty).Subscribe(this.ChildChanged); } /// /// Raised when the popup closes. /// public event EventHandler Closed; /// /// Raised when the popup opens. /// public event EventHandler Opened; /// /// Raised when the popup root has been created, but before it has been shown. /// public event EventHandler PopupRootCreated; /// /// Gets or sets the control to display in the popup. /// public Control Child { get { return this.GetValue(ChildProperty); } set { this.SetValue(ChildProperty, value); } } /// /// Gets or sets a dependency resolver for the . /// /// /// This property allows a client to customize the behaviour of the popup by injecting /// a specialized dependency resolver into the 's constructor. /// public IDependencyResolver DependencyResolver { get; set; } /// /// Gets or sets a value indicating whether the popup is currently open. /// public bool IsOpen { get { return this.GetValue(IsOpenProperty); } set { this.SetValue(IsOpenProperty, value); } } /// /// Gets or sets the placement mode of the popup in relation to the . /// public PlacementMode PlacementMode { get { return this.GetValue(PlacementModeProperty); } set { this.SetValue(PlacementModeProperty, value); } } /// /// Gets or sets the control that is used to determine the popup's position. /// public Control PlacementTarget { get { return this.GetValue(PlacementTargetProperty); } set { this.SetValue(PlacementTargetProperty, value); } } /// /// Gets the root of the popup window. /// public PopupRoot PopupRoot { get { return this.popupRoot; } } /// /// Gets or sets a value indicating whether the popup should stay open when the popup loses focus. /// public bool StaysOpen { get { return this.GetValue(StaysOpenProperty); } set { this.SetValue(StaysOpenProperty, value); } } /// /// Gets the logical children of the popup. /// IPerspexReadOnlyList ILogical.LogicalChildren { get { return this.logicalChild; } } /// /// Gets the root of the popup window. /// IVisual IVisualTreeHost.Root { get { return this.popupRoot; } } /// /// Opens the popup. /// public void Open() { if (this.popupRoot == null) { this.popupRoot = new PopupRoot(this.DependencyResolver) { [~PopupRoot.ContentProperty] = this[~ChildProperty], [~PopupRoot.WidthProperty] = this[~WidthProperty], [~PopupRoot.HeightProperty] = this[~HeightProperty], [~PopupRoot.MinWidthProperty] = this[~MinWidthProperty], [~PopupRoot.MaxWidthProperty] = this[~MaxWidthProperty], [~PopupRoot.MinHeightProperty] = this[~MinHeightProperty], [~PopupRoot.MaxHeightProperty] = this[~MaxHeightProperty], }; ((ISetLogicalParent)this.popupRoot).SetParent(this); } this.popupRoot.SetPosition(this.GetPosition()); this.topLevel.Deactivated += this.MaybeClose; this.popupRoot.AddHandler(PopupRoot.PointerPressedEvent, this.MaybeClose, RoutingStrategies.Bubble, true); this.topLevel.AddHandler(TopLevel.PointerPressedEvent, this.MaybeClose, RoutingStrategies.Tunnel); this.PopupRootCreated?.Invoke(this, EventArgs.Empty); this.popupRoot.Show(); this.IsOpen = true; this.Opened?.Invoke(this, EventArgs.Empty); } /// /// Closes the popup. /// public void Close() { if (this.popupRoot != null) { this.popupRoot.PointerPressed -= this.MaybeClose; this.topLevel.RemoveHandler(TopLevel.PointerPressedEvent, this.MaybeClose); this.topLevel.Deactivated -= this.MaybeClose; this.popupRoot.Hide(); } this.IsOpen = false; this.Closed?.Invoke(this, EventArgs.Empty); } /// /// Measures the control. /// /// The available size for the control. /// A size of 0,0 as Popup itself takes up no space. protected override Size MeasureCore(Size availableSize) { return new Size(); } /// /// Called when the control is added to the visual tree. /// /// THe root of the visual tree. protected override void OnAttachedToVisualTree(IRenderRoot root) { base.OnAttachedToVisualTree(root); this.topLevel = root as TopLevel; } /// /// Called when the control is removed to the visual tree. /// /// THe root of the visual tree. protected override void OnDetachedFromVisualTree(IRenderRoot root) { base.OnDetachedFromVisualTree(root); this.topLevel = null; } /// /// Called when the property changes. /// /// The event args. private void IsOpenChanged(PerspexPropertyChangedEventArgs e) { if ((bool)e.NewValue) { this.Open(); } else { this.Close(); } } /// /// Called when the property changes. /// /// The old and new values. private void ChildChanged(Tuple values) { if (values.Item1 != null) { ((ISetLogicalParent)values.Item1).SetParent(null); } if (values.Item2 != null) { ((ISetLogicalParent)values.Item2).SetParent(this); } this.logicalChild.SingleItem = values.Item2; } /// /// Gets the position for the popup based on the placement properties. /// /// The popup's position in screen coordinates. private Point GetPosition() { var target = this.PlacementTarget ?? this.GetVisualParent(); Point point; if (target != null) { switch (this.PlacementMode) { case PlacementMode.Bottom: point = new Point(0, target.Bounds.Height); break; case PlacementMode.Right: point = new Point(target.Bounds.Width, 0); break; default: throw new InvalidOperationException("Invalid value for Popup.PlacementMode"); } return target.PointToScreen(point); } else { return new Point(); } } /// /// Conditionally closes the popup in response to an event, based on the value of the /// property. /// /// The event sender. /// The event args. private void MaybeClose(object sender, EventArgs e) { if (!this.StaysOpen) { var routed = e as RoutedEventArgs; if (routed != null) { routed.Handled = true; } this.Close(); } } } }