// -----------------------------------------------------------------------
//
// 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();
}
}
}
}