|
@@ -2,7 +2,12 @@
|
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
|
|
|
|
using System;
|
|
using System;
|
|
|
|
+using System.Collections.Generic;
|
|
|
|
+using System.Diagnostics;
|
|
using System.Linq;
|
|
using System.Linq;
|
|
|
|
+using System.Reactive.Disposables;
|
|
|
|
+using Avalonia.Controls.Presenters;
|
|
|
|
+using Avalonia.Data;
|
|
using Avalonia.Input;
|
|
using Avalonia.Input;
|
|
using Avalonia.Input.Raw;
|
|
using Avalonia.Input.Raw;
|
|
using Avalonia.Interactivity;
|
|
using Avalonia.Interactivity;
|
|
@@ -42,7 +47,7 @@ namespace Avalonia.Controls.Primitives
|
|
/// Defines the <see cref="ObeyScreenEdges"/> property.
|
|
/// Defines the <see cref="ObeyScreenEdges"/> property.
|
|
/// </summary>
|
|
/// </summary>
|
|
public static readonly StyledProperty<bool> ObeyScreenEdgesProperty =
|
|
public static readonly StyledProperty<bool> ObeyScreenEdgesProperty =
|
|
- AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges));
|
|
|
|
|
|
+ AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true);
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
/// Defines the <see cref="HorizontalOffset"/> property.
|
|
/// Defines the <see cref="HorizontalOffset"/> property.
|
|
@@ -75,10 +80,12 @@ namespace Avalonia.Controls.Primitives
|
|
AvaloniaProperty.Register<Popup, bool>(nameof(Topmost));
|
|
AvaloniaProperty.Register<Popup, bool>(nameof(Topmost));
|
|
|
|
|
|
private bool _isOpen;
|
|
private bool _isOpen;
|
|
- private PopupRoot _popupRoot;
|
|
|
|
|
|
+ private IPopupHost _popupHost;
|
|
private TopLevel _topLevel;
|
|
private TopLevel _topLevel;
|
|
private IDisposable _nonClientListener;
|
|
private IDisposable _nonClientListener;
|
|
|
|
+ private IDisposable _presenterSubscription;
|
|
bool _ignoreIsOpenChanged = false;
|
|
bool _ignoreIsOpenChanged = false;
|
|
|
|
+ private List<IDisposable> _bindings = new List<IDisposable>();
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
/// Initializes static members of the <see cref="Popup"/> class.
|
|
/// Initializes static members of the <see cref="Popup"/> class.
|
|
@@ -88,7 +95,11 @@ namespace Avalonia.Controls.Primitives
|
|
IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false);
|
|
IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false);
|
|
ChildProperty.Changed.AddClassHandler<Popup>(x => x.ChildChanged);
|
|
ChildProperty.Changed.AddClassHandler<Popup>(x => x.ChildChanged);
|
|
IsOpenProperty.Changed.AddClassHandler<Popup>(x => x.IsOpenChanged);
|
|
IsOpenProperty.Changed.AddClassHandler<Popup>(x => x.IsOpenChanged);
|
|
- TopmostProperty.Changed.AddClassHandler<Popup>((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue);
|
|
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public Popup()
|
|
|
|
+ {
|
|
|
|
+
|
|
}
|
|
}
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
@@ -101,10 +112,7 @@ namespace Avalonia.Controls.Primitives
|
|
/// </summary>
|
|
/// </summary>
|
|
public event EventHandler Opened;
|
|
public event EventHandler Opened;
|
|
|
|
|
|
- /// <summary>
|
|
|
|
- /// Raised when the popup root has been created, but before it has been shown.
|
|
|
|
- /// </summary>
|
|
|
|
- public event EventHandler PopupRootCreated;
|
|
|
|
|
|
+ public IPopupHost Host => _popupHost;
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
/// Gets or sets the control to display in the popup.
|
|
/// Gets or sets the control to display in the popup.
|
|
@@ -147,10 +155,7 @@ namespace Avalonia.Controls.Primitives
|
|
set { SetValue(PlacementModeProperty, value); }
|
|
set { SetValue(PlacementModeProperty, value); }
|
|
}
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
|
- /// Gets or sets a value indicating whether the popup positions itself within the nearest screen boundary
|
|
|
|
- /// when its opened at a position where it would otherwise overlap the screen edge.
|
|
|
|
- /// </summary>
|
|
|
|
|
|
+ [Obsolete("This property has no effect")]
|
|
public bool ObeyScreenEdges
|
|
public bool ObeyScreenEdges
|
|
{
|
|
{
|
|
get => GetValue(ObeyScreenEdgesProperty);
|
|
get => GetValue(ObeyScreenEdgesProperty);
|
|
@@ -184,11 +189,6 @@ namespace Avalonia.Controls.Primitives
|
|
set { SetValue(PlacementTargetProperty, value); }
|
|
set { SetValue(PlacementTargetProperty, value); }
|
|
}
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
|
- /// Gets the root of the popup window.
|
|
|
|
- /// </summary>
|
|
|
|
- public PopupRoot PopupRoot => _popupRoot;
|
|
|
|
-
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
/// Gets or sets a value indicating whether the popup should stay open when the popup is
|
|
/// Gets or sets a value indicating whether the popup should stay open when the popup is
|
|
/// pressed or loses focus.
|
|
/// pressed or loses focus.
|
|
@@ -211,63 +211,58 @@ namespace Avalonia.Controls.Primitives
|
|
/// <summary>
|
|
/// <summary>
|
|
/// Gets the root of the popup window.
|
|
/// Gets the root of the popup window.
|
|
/// </summary>
|
|
/// </summary>
|
|
- IVisual IVisualTreeHost.Root => _popupRoot;
|
|
|
|
|
|
+ IVisual IVisualTreeHost.Root => _popupHost?.HostedVisualTreeRoot;
|
|
|
|
|
|
/// <summary>
|
|
/// <summary>
|
|
/// Opens the popup.
|
|
/// Opens the popup.
|
|
/// </summary>
|
|
/// </summary>
|
|
public void Open()
|
|
public void Open()
|
|
{
|
|
{
|
|
- if (_popupRoot == null)
|
|
|
|
|
|
+ // Popup is currently open
|
|
|
|
+ if (_topLevel != null)
|
|
|
|
+ return;
|
|
|
|
+ CloseCurrent();
|
|
|
|
+ var placementTarget = PlacementTarget ?? this.GetLogicalAncestors().OfType<IVisual>().FirstOrDefault();
|
|
|
|
+ if (placementTarget == null)
|
|
|
|
+ throw new InvalidOperationException("Popup has no logical parent and PlacementTarget is null");
|
|
|
|
+
|
|
|
|
+ _topLevel = placementTarget.GetVisualRoot() as TopLevel;
|
|
|
|
+
|
|
|
|
+ if (_topLevel == 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);
|
|
|
|
|
|
+ throw new InvalidOperationException(
|
|
|
|
+ "Attempted to open a popup not attached to a TopLevel");
|
|
}
|
|
}
|
|
|
|
|
|
- _popupRoot.Position = GetPosition();
|
|
|
|
|
|
+ _popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver);
|
|
|
|
+
|
|
|
|
+ _bindings.Add(_popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty,
|
|
|
|
+ HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty));
|
|
|
|
|
|
- if (_topLevel == null && PlacementTarget != null)
|
|
|
|
|
|
+ _popupHost.SetChild(Child);
|
|
|
|
+ ((ISetLogicalParent)_popupHost).SetParent(this);
|
|
|
|
+ _popupHost.ConfigurePosition(placementTarget,
|
|
|
|
+ PlacementMode, new Point(HorizontalOffset, VerticalOffset));
|
|
|
|
+ _popupHost.TemplateApplied += RootTemplateApplied;
|
|
|
|
+
|
|
|
|
+ var window = _topLevel as Window;
|
|
|
|
+ if (window != null)
|
|
{
|
|
{
|
|
- _topLevel = PlacementTarget.GetSelfAndLogicalAncestors().First(x => x is TopLevel) as TopLevel;
|
|
|
|
|
|
+ window.Deactivated += WindowDeactivated;
|
|
}
|
|
}
|
|
-
|
|
|
|
- if (_topLevel != null)
|
|
|
|
|
|
+ else
|
|
{
|
|
{
|
|
- var window = _topLevel as Window;
|
|
|
|
- if (window != null)
|
|
|
|
|
|
+ var parentPopuproot = _topLevel as PopupRoot;
|
|
|
|
+ if (parentPopuproot?.Parent is Popup popup)
|
|
{
|
|
{
|
|
- window.Deactivated += WindowDeactivated;
|
|
|
|
|
|
+ popup.Closed += ParentClosed;
|
|
}
|
|
}
|
|
- else
|
|
|
|
- {
|
|
|
|
- var parentPopuproot = _topLevel as PopupRoot;
|
|
|
|
- if (parentPopuproot?.Parent is Popup popup)
|
|
|
|
- {
|
|
|
|
- popup.Closed += ParentClosed;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
|
|
|
|
- _nonClientListener = InputManager.Instance.Process.Subscribe(ListenForNonClientClick);
|
|
|
|
}
|
|
}
|
|
|
|
+ _topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
|
|
|
|
+ _nonClientListener = InputManager.Instance?.Process.Subscribe(ListenForNonClientClick);
|
|
|
|
+
|
|
|
|
|
|
- PopupRootCreated?.Invoke(this, EventArgs.Empty);
|
|
|
|
-
|
|
|
|
- _popupRoot.Show();
|
|
|
|
-
|
|
|
|
- if (ObeyScreenEdges)
|
|
|
|
- {
|
|
|
|
- _popupRoot.SnapInsideScreenEdges();
|
|
|
|
- }
|
|
|
|
|
|
+ _popupHost.Show();
|
|
|
|
|
|
using (BeginIgnoringIsOpen())
|
|
using (BeginIgnoringIsOpen())
|
|
{
|
|
{
|
|
@@ -282,29 +277,14 @@ namespace Avalonia.Controls.Primitives
|
|
/// </summary>
|
|
/// </summary>
|
|
public void Close()
|
|
public void Close()
|
|
{
|
|
{
|
|
- if (_popupRoot != null)
|
|
|
|
|
|
+ if (_popupHost != null)
|
|
{
|
|
{
|
|
- if (_topLevel != null)
|
|
|
|
- {
|
|
|
|
- _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
|
|
|
|
- var window = _topLevel as Window;
|
|
|
|
- if (window != null)
|
|
|
|
- window.Deactivated -= WindowDeactivated;
|
|
|
|
- else
|
|
|
|
- {
|
|
|
|
- var parentPopuproot = _topLevel as PopupRoot;
|
|
|
|
- if (parentPopuproot?.Parent is Popup popup)
|
|
|
|
- {
|
|
|
|
- popup.Closed -= ParentClosed;
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
- _nonClientListener?.Dispose();
|
|
|
|
- _nonClientListener = null;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- _popupRoot.Hide();
|
|
|
|
|
|
+ _popupHost.TemplateApplied -= RootTemplateApplied;
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ _presenterSubscription?.Dispose();
|
|
|
|
+
|
|
|
|
+ CloseCurrent();
|
|
using (BeginIgnoringIsOpen())
|
|
using (BeginIgnoringIsOpen())
|
|
{
|
|
{
|
|
IsOpen = false;
|
|
IsOpen = false;
|
|
@@ -313,6 +293,41 @@ namespace Avalonia.Controls.Primitives
|
|
Closed?.Invoke(this, EventArgs.Empty);
|
|
Closed?.Invoke(this, EventArgs.Empty);
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ void CloseCurrent()
|
|
|
|
+ {
|
|
|
|
+ if (_topLevel != null)
|
|
|
|
+ {
|
|
|
|
+ _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
|
|
|
|
+ var window = _topLevel as Window;
|
|
|
|
+ if (window != null)
|
|
|
|
+ window.Deactivated -= WindowDeactivated;
|
|
|
|
+ else
|
|
|
|
+ {
|
|
|
|
+ var parentPopuproot = _topLevel as PopupRoot;
|
|
|
|
+ if (parentPopuproot?.Parent is Popup popup)
|
|
|
|
+ {
|
|
|
|
+ popup.Closed -= ParentClosed;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ _nonClientListener?.Dispose();
|
|
|
|
+ _nonClientListener = null;
|
|
|
|
+
|
|
|
|
+ _topLevel = null;
|
|
|
|
+ }
|
|
|
|
+ if (_popupHost != null)
|
|
|
|
+ {
|
|
|
|
+ foreach(var b in _bindings)
|
|
|
|
+ b.Dispose();
|
|
|
|
+ _bindings.Clear();
|
|
|
|
+ _popupHost.SetChild(null);
|
|
|
|
+ _popupHost.Hide();
|
|
|
|
+ ((ISetLogicalParent)_popupHost).SetParent(null);
|
|
|
|
+ _popupHost.Dispose();
|
|
|
|
+ _popupHost = null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ }
|
|
|
|
+
|
|
/// <summary>
|
|
/// <summary>
|
|
/// Measures the control.
|
|
/// Measures the control.
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -323,27 +338,14 @@ namespace Avalonia.Controls.Primitives
|
|
return new Size();
|
|
return new Size();
|
|
}
|
|
}
|
|
|
|
|
|
- /// <inheritdoc/>
|
|
|
|
- protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
|
|
|
|
- {
|
|
|
|
- base.OnAttachedToLogicalTree(e);
|
|
|
|
- _topLevel = e.Root as TopLevel;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
/// <inheritdoc/>
|
|
/// <inheritdoc/>
|
|
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
|
|
protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
|
|
{
|
|
{
|
|
base.OnDetachedFromLogicalTree(e);
|
|
base.OnDetachedFromLogicalTree(e);
|
|
- _topLevel = null;
|
|
|
|
-
|
|
|
|
- if (_popupRoot != null)
|
|
|
|
- {
|
|
|
|
- ((ISetLogicalParent)_popupRoot).SetParent(null);
|
|
|
|
- _popupRoot.Dispose();
|
|
|
|
- _popupRoot = null;
|
|
|
|
- }
|
|
|
|
|
|
+ Close();
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+
|
|
/// <summary>
|
|
/// <summary>
|
|
/// Called when the <see cref="IsOpen"/> property changes.
|
|
/// Called when the <see cref="IsOpen"/> property changes.
|
|
/// </summary>
|
|
/// </summary>
|
|
@@ -380,49 +382,6 @@ namespace Avalonia.Controls.Primitives
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- /// <summary>
|
|
|
|
- /// Gets the position for the popup based on the placement properties.
|
|
|
|
- /// </summary>
|
|
|
|
- /// <returns>The popup's position in screen coordinates.</returns>
|
|
|
|
- protected virtual PixelPoint GetPosition()
|
|
|
|
- {
|
|
|
|
- var result = GetPosition(PlacementTarget ?? this.GetVisualParent<Control>(), PlacementMode, PopupRoot,
|
|
|
|
- HorizontalOffset, VerticalOffset);
|
|
|
|
-
|
|
|
|
- return result;
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- internal static PixelPoint GetPosition(Control target, PlacementMode placement, PopupRoot popupRoot, double horizontalOffset, double verticalOffset)
|
|
|
|
- {
|
|
|
|
- var root = target?.GetVisualRoot();
|
|
|
|
- var mode = root != null ? placement : PlacementMode.Pointer;
|
|
|
|
- var scaling = root?.RenderScaling ?? 1;
|
|
|
|
-
|
|
|
|
- switch (mode)
|
|
|
|
- {
|
|
|
|
- case PlacementMode.Pointer:
|
|
|
|
- if (popupRoot != null)
|
|
|
|
- {
|
|
|
|
- var screenOffset = PixelPoint.FromPoint(new Point(horizontalOffset, verticalOffset), scaling);
|
|
|
|
- var mouseOffset = ((IInputRoot)popupRoot)?.MouseDevice?.Position ?? default;
|
|
|
|
- return new PixelPoint(
|
|
|
|
- screenOffset.X + mouseOffset.X,
|
|
|
|
- screenOffset.Y + mouseOffset.Y);
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
- return default;
|
|
|
|
-
|
|
|
|
- case PlacementMode.Bottom:
|
|
|
|
- return target?.PointToScreen(new Point(0 + horizontalOffset, target.Bounds.Height + verticalOffset)) ?? default;
|
|
|
|
-
|
|
|
|
- case PlacementMode.Right:
|
|
|
|
- return target?.PointToScreen(new Point(target.Bounds.Width + horizontalOffset, 0 + verticalOffset)) ?? default;
|
|
|
|
-
|
|
|
|
- default:
|
|
|
|
- throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
|
|
|
|
- }
|
|
|
|
- }
|
|
|
|
-
|
|
|
|
private void ListenForNonClientClick(RawInputEventArgs e)
|
|
private void ListenForNonClientClick(RawInputEventArgs e)
|
|
{
|
|
{
|
|
var mouse = e as RawPointerEventArgs;
|
|
var mouse = e as RawPointerEventArgs;
|
|
@@ -445,17 +404,62 @@ namespace Avalonia.Controls.Primitives
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
- private bool IsChildOrThis(IVisual child)
|
|
|
|
|
|
+ private void RootTemplateApplied(object sender, TemplateAppliedEventArgs e)
|
|
{
|
|
{
|
|
- IVisual root = child.GetVisualRoot();
|
|
|
|
- while (root is PopupRoot)
|
|
|
|
|
|
+ _popupHost.TemplateApplied -= RootTemplateApplied;
|
|
|
|
+
|
|
|
|
+ if (_presenterSubscription != null)
|
|
{
|
|
{
|
|
- if (root == PopupRoot) return true;
|
|
|
|
- root = ((PopupRoot)root).Parent.GetVisualRoot();
|
|
|
|
|
|
+ _presenterSubscription.Dispose();
|
|
|
|
+ _presenterSubscription = null;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ // If the Popup appears in a control template, then the child controls
|
|
|
|
+ // that appear in the popup host need to have their TemplatedParent
|
|
|
|
+ // properties set.
|
|
|
|
+ if (TemplatedParent != null)
|
|
|
|
+ {
|
|
|
|
+ _popupHost.Presenter?.ApplyTemplate();
|
|
|
|
+ _popupHost.Presenter?.GetObservable(ContentPresenter.ChildProperty)
|
|
|
|
+ .Subscribe(SetTemplatedParentAndApplyChildTemplates);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ private void SetTemplatedParentAndApplyChildTemplates(IControl control)
|
|
|
|
+ {
|
|
|
|
+ if (control != null)
|
|
|
|
+ {
|
|
|
|
+ var templatedParent = TemplatedParent;
|
|
|
|
+
|
|
|
|
+ if (control.TemplatedParent == null)
|
|
|
|
+ {
|
|
|
|
+ control.SetValue(TemplatedParentProperty, templatedParent);
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ control.ApplyTemplate();
|
|
|
|
+
|
|
|
|
+ if (!(control is IPresenter) && control.TemplatedParent == templatedParent)
|
|
|
|
+ {
|
|
|
|
+ foreach (IControl child in control.GetVisualChildren())
|
|
|
|
+ {
|
|
|
|
+ SetTemplatedParentAndApplyChildTemplates(child);
|
|
|
|
+ }
|
|
|
|
+ }
|
|
}
|
|
}
|
|
- return false;
|
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
+ private bool IsChildOrThis(IVisual child)
|
|
|
|
+ {
|
|
|
|
+ return _popupHost != null && ((IVisual)_popupHost).FindCommonVisualAncestor(child) == _popupHost;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public bool IsInsidePopup(IVisual visual)
|
|
|
|
+ {
|
|
|
|
+ return _popupHost != null && ((IVisual)_popupHost)?.IsVisualAncestorOf(visual) == true;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ public bool IsPointerOverPopup => ((IInputElement)_popupHost).IsPointerOver;
|
|
|
|
+
|
|
private void WindowDeactivated(object sender, EventArgs e)
|
|
private void WindowDeactivated(object sender, EventArgs e)
|
|
{
|
|
{
|
|
if (!StaysOpen)
|
|
if (!StaysOpen)
|