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