// Copyright (c) The Avalonia Project. All rights reserved.
// Licensed under the MIT license. See licence.md file in the project root for full license information.
using System;
using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
using Avalonia.Rendering;
using Avalonia.VisualTree;
using Avalonia.Layout;
namespace Avalonia.Controls.Primitives
{
///
/// Displays a popup window.
///
public class Popup : Control, IVisualTreeHost
{
///
/// Defines the property.
///
public static readonly StyledProperty ChildProperty =
AvaloniaProperty.Register(nameof(Child));
///
/// Defines the property.
///
public static readonly DirectProperty IsOpenProperty =
AvaloniaProperty.RegisterDirect(
nameof(IsOpen),
o => o.IsOpen,
(o, v) => o.IsOpen = v);
///
/// Defines the property.
///
public static readonly StyledProperty PlacementModeProperty =
AvaloniaProperty.Register(nameof(PlacementMode), defaultValue: PlacementMode.Bottom);
///
/// Defines the property.
///
public static readonly StyledProperty HorizontalOffsetProperty =
AvaloniaProperty.Register(nameof(HorizontalOffset));
///
/// Defines the property.
///
public static readonly StyledProperty VerticalOffsetProperty =
AvaloniaProperty.Register(nameof(VerticalOffset));
///
/// Defines the property.
///
public static readonly StyledProperty PlacementTargetProperty =
AvaloniaProperty.Register(nameof(PlacementTarget));
///
/// Defines the property.
///
public static readonly StyledProperty StaysOpenProperty =
AvaloniaProperty.Register(nameof(StaysOpen), true);
private bool _isOpen;
private PopupRoot _popupRoot;
private TopLevel _topLevel;
private IDisposable _nonClientListener;
bool _ignoreIsOpenChanged = false;
///
/// Initializes static members of the class.
///
static Popup()
{
IsHitTestVisibleProperty.OverrideDefaultValue(false);
ChildProperty.Changed.AddClassHandler(x => x.ChildChanged);
IsOpenProperty.Changed.AddClassHandler(x => x.IsOpenChanged);
}
///
/// 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.
///
[Content]
public Control Child
{
get { return GetValue(ChildProperty); }
set { 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 IAvaloniaDependencyResolver DependencyResolver
{
get;
set;
}
///
/// Gets or sets a value indicating whether the popup is currently open.
///
public bool IsOpen
{
get { return _isOpen; }
set { SetAndRaise(IsOpenProperty, ref _isOpen, value); }
}
///
/// Gets or sets the placement mode of the popup in relation to the .
///
public PlacementMode PlacementMode
{
get { return GetValue(PlacementModeProperty); }
set { SetValue(PlacementModeProperty, value); }
}
///
/// Gets or sets the Horizontal offset of the popup in relation to the
///
public double HorizontalOffset
{
get { return GetValue(HorizontalOffsetProperty); }
set { SetValue(HorizontalOffsetProperty, value); }
}
///
/// Gets or sets the Vertical offset of the popup in relation to the
///
public double VerticalOffset
{
get { return GetValue(VerticalOffsetProperty); }
set { SetValue(VerticalOffsetProperty, value); }
}
///
/// Gets or sets the control that is used to determine the popup's position.
///
public Control PlacementTarget
{
get { return GetValue(PlacementTargetProperty); }
set { SetValue(PlacementTargetProperty, value); }
}
///
/// Gets the root of the popup window.
///
public PopupRoot PopupRoot => _popupRoot;
///
/// Gets or sets a value indicating whether the popup should stay open when the popup is
/// pressed or loses focus.
///
public bool StaysOpen
{
get { return GetValue(StaysOpenProperty); }
set { SetValue(StaysOpenProperty, value); }
}
///
/// Gets the root of the popup window.
///
IVisual IVisualTreeHost.Root => _popupRoot;
///
/// Opens the popup.
///
public void Open()
{
if (_popupRoot == null)
{
_popupRoot = new PopupRoot(DependencyResolver)
{
[~ContentControl.ContentProperty] = this[~ChildProperty],
[~WidthProperty] = this[~WidthProperty],
[~HeightProperty] = this[~HeightProperty],
[~MinWidthProperty] = this[~MinWidthProperty],
[~MaxWidthProperty] = this[~MaxWidthProperty],
[~MinHeightProperty] = this[~MinHeightProperty],
[~MaxHeightProperty] = this[~MaxHeightProperty],
};
((ISetLogicalParent)_popupRoot).SetParent(this);
}
_popupRoot.Position = GetPosition();
if (_topLevel == null && PlacementTarget != null)
{
_topLevel = PlacementTarget.GetSelfAndLogicalAncestors().First(x => x is TopLevel) as TopLevel;
}
if (_topLevel != null)
{
var window = _topLevel as Window;
if (window != null)
window.Deactivated += WindowDeactivated;
_topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
_nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick);
}
PopupRootCreated?.Invoke(this, EventArgs.Empty);
_popupRoot.Show();
_ignoreIsOpenChanged = true;
IsOpen = true;
_ignoreIsOpenChanged = false;
Opened?.Invoke(this, EventArgs.Empty);
}
///
/// Closes the popup.
///
public void Close()
{
if (_popupRoot != null)
{
if (_topLevel != null)
{
_topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
var window = _topLevel as Window;
if (window != null)
window.Deactivated -= WindowDeactivated;
_nonClientListener?.Dispose();
_nonClientListener = null;
}
_popupRoot.Hide();
}
IsOpen = false;
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();
}
///
protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnAttachedToLogicalTree(e);
_topLevel = e.Root as TopLevel;
}
///
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
{
base.OnDetachedFromLogicalTree(e);
_topLevel = null;
if (_popupRoot != null)
{
((ISetLogicalParent)_popupRoot).SetParent(null);
_popupRoot.Dispose();
_popupRoot = null;
}
}
///
/// Called when the property changes.
///
/// The event args.
private void IsOpenChanged(AvaloniaPropertyChangedEventArgs e)
{
if (!_ignoreIsOpenChanged)
{
if ((bool)e.NewValue)
{
Open();
}
else
{
Close();
}
}
}
///
/// Called when the property changes.
///
/// The event args.
private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
{
LogicalChildren.Clear();
((ISetLogicalParent)e.OldValue)?.SetParent(null);
if (e.NewValue != null)
{
((ISetLogicalParent)e.NewValue).SetParent(this);
LogicalChildren.Add((ILogical)e.NewValue);
}
}
///
/// Gets the position for the popup based on the placement properties.
///
/// The popup's position in screen coordinates.
protected virtual Point GetPosition()
{
var zero = default(Point);
var mode = PlacementMode;
var target = PlacementTarget ?? this.GetVisualParent();
if (target?.GetVisualRoot() == null)
{
mode = PlacementMode.Pointer;
}
switch (mode)
{
case PlacementMode.Pointer:
if (MouseDevice.Instance != null)
{
// Scales the Horizontal and Vertical offset to screen co-ordinates.
var screenOffset = new Point(HorizontalOffset * (PopupRoot as ILayoutRoot).LayoutScaling, VerticalOffset * (PopupRoot as ILayoutRoot).LayoutScaling);
return MouseDevice.Instance.Position + screenOffset;
}
return default(Point);
case PlacementMode.Bottom:
return target?.PointToScreen(new Point(0 + HorizontalOffset, target.Bounds.Height + VerticalOffset)) ?? zero;
case PlacementMode.Right:
return target?.PointToScreen(new Point(target.Bounds.Width + HorizontalOffset, 0 + VerticalOffset)) ?? zero;
default:
throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
}
}
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawMouseEventArgs;
if (!StaysOpen && mouse?.Type == RawMouseEventType.NonClientLeftButtonDown)
{
Close();
}
}
private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
{
if (!StaysOpen)
{
var root = ((IVisual)e.Source).GetVisualRoot();
if (root != this.PopupRoot)
{
Close();
e.Handled = true;
}
}
}
private void WindowDeactivated(object sender, EventArgs e)
{
if (!StaysOpen)
{
Close();
}
}
}
}