|
|
|
@ -2,12 +2,10 @@ |
|
|
|
// Licensed under the MIT license. See licence.md file in the project root for full license information.
|
|
|
|
|
|
|
|
using System; |
|
|
|
using System.Collections.Generic; |
|
|
|
using System.Diagnostics; |
|
|
|
using System.Linq; |
|
|
|
using System.Reactive.Disposables; |
|
|
|
using Avalonia.Controls.Presenters; |
|
|
|
using Avalonia.Data; |
|
|
|
using Avalonia.Input; |
|
|
|
using Avalonia.Input.Raw; |
|
|
|
using Avalonia.Interactivity; |
|
|
|
@ -15,6 +13,8 @@ using Avalonia.LogicalTree; |
|
|
|
using Avalonia.Metadata; |
|
|
|
using Avalonia.VisualTree; |
|
|
|
|
|
|
|
#nullable enable |
|
|
|
|
|
|
|
namespace Avalonia.Controls.Primitives |
|
|
|
{ |
|
|
|
/// <summary>
|
|
|
|
@ -25,8 +25,8 @@ namespace Avalonia.Controls.Primitives |
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="Child"/> property.
|
|
|
|
/// </summary>
|
|
|
|
public static readonly StyledProperty<Control> ChildProperty = |
|
|
|
AvaloniaProperty.Register<Popup, Control>(nameof(Child)); |
|
|
|
public static readonly StyledProperty<Control?> ChildProperty = |
|
|
|
AvaloniaProperty.Register<Popup, Control?>(nameof(Child)); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="IsOpen"/> property.
|
|
|
|
@ -43,11 +43,13 @@ namespace Avalonia.Controls.Primitives |
|
|
|
public static readonly StyledProperty<PlacementMode> PlacementModeProperty = |
|
|
|
AvaloniaProperty.Register<Popup, PlacementMode>(nameof(PlacementMode), defaultValue: PlacementMode.Bottom); |
|
|
|
|
|
|
|
#pragma warning disable 618
|
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="ObeyScreenEdges"/> property.
|
|
|
|
/// </summary>
|
|
|
|
public static readonly StyledProperty<bool> ObeyScreenEdgesProperty = |
|
|
|
AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true); |
|
|
|
#pragma warning restore 618
|
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="HorizontalOffset"/> property.
|
|
|
|
@ -64,8 +66,8 @@ namespace Avalonia.Controls.Primitives |
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="PlacementTarget"/> property.
|
|
|
|
/// </summary>
|
|
|
|
public static readonly StyledProperty<Control> PlacementTargetProperty = |
|
|
|
AvaloniaProperty.Register<Popup, Control>(nameof(PlacementTarget)); |
|
|
|
public static readonly StyledProperty<Control?> PlacementTargetProperty = |
|
|
|
AvaloniaProperty.Register<Popup, Control?>(nameof(PlacementTarget)); |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Defines the <see cref="StaysOpen"/> property.
|
|
|
|
@ -80,12 +82,8 @@ namespace Avalonia.Controls.Primitives |
|
|
|
AvaloniaProperty.Register<Popup, bool>(nameof(Topmost)); |
|
|
|
|
|
|
|
private bool _isOpen; |
|
|
|
private IPopupHost _popupHost; |
|
|
|
private TopLevel _topLevel; |
|
|
|
private IDisposable _nonClientListener; |
|
|
|
private IDisposable _presenterSubscription; |
|
|
|
bool _ignoreIsOpenChanged = false; |
|
|
|
private List<IDisposable> _bindings = new List<IDisposable>(); |
|
|
|
private bool _ignoreIsOpenChanged; |
|
|
|
private PopupOpenState? _openState; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Initializes static members of the <see cref="Popup"/> class.
|
|
|
|
@ -94,31 +92,26 @@ namespace Avalonia.Controls.Primitives |
|
|
|
{ |
|
|
|
IsHitTestVisibleProperty.OverrideDefaultValue<Popup>(false); |
|
|
|
ChildProperty.Changed.AddClassHandler<Popup>((x, e) => x.ChildChanged(e)); |
|
|
|
IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged(e)); |
|
|
|
} |
|
|
|
|
|
|
|
public Popup() |
|
|
|
{ |
|
|
|
|
|
|
|
IsOpenProperty.Changed.AddClassHandler<Popup>((x, e) => x.IsOpenChanged((AvaloniaPropertyChangedEventArgs<bool>)e)); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Raised when the popup closes.
|
|
|
|
/// </summary>
|
|
|
|
public event EventHandler Closed; |
|
|
|
public event EventHandler? Closed; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Raised when the popup opens.
|
|
|
|
/// </summary>
|
|
|
|
public event EventHandler Opened; |
|
|
|
public event EventHandler? Opened; |
|
|
|
|
|
|
|
public IPopupHost Host => _popupHost; |
|
|
|
public IPopupHost? Host => _openState?.PopupHost; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the control to display in the popup.
|
|
|
|
/// </summary>
|
|
|
|
[Content] |
|
|
|
public Control Child |
|
|
|
public Control? Child |
|
|
|
{ |
|
|
|
get { return GetValue(ChildProperty); } |
|
|
|
set { SetValue(ChildProperty, value); } |
|
|
|
@ -131,7 +124,7 @@ namespace Avalonia.Controls.Primitives |
|
|
|
/// This property allows a client to customize the behaviour of the popup by injecting
|
|
|
|
/// a specialized dependency resolver into the <see cref="PopupRoot"/>'s constructor.
|
|
|
|
/// </remarks>
|
|
|
|
public IAvaloniaDependencyResolver DependencyResolver |
|
|
|
public IAvaloniaDependencyResolver? DependencyResolver |
|
|
|
{ |
|
|
|
get; |
|
|
|
set; |
|
|
|
@ -183,7 +176,7 @@ namespace Avalonia.Controls.Primitives |
|
|
|
/// <summary>
|
|
|
|
/// Gets or sets the control that is used to determine the popup's position.
|
|
|
|
/// </summary>
|
|
|
|
public Control PlacementTarget |
|
|
|
public Control? PlacementTarget |
|
|
|
{ |
|
|
|
get { return GetValue(PlacementTargetProperty); } |
|
|
|
set { SetValue(PlacementTargetProperty, value); } |
|
|
|
@ -211,7 +204,7 @@ namespace Avalonia.Controls.Primitives |
|
|
|
/// <summary>
|
|
|
|
/// Gets the root of the popup window.
|
|
|
|
/// </summary>
|
|
|
|
IVisual IVisualTreeHost.Root => _popupHost?.HostedVisualTreeRoot; |
|
|
|
IVisual? IVisualTreeHost.Root => _openState?.PopupHost.HostedVisualTreeRoot; |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Opens the popup.
|
|
|
|
@ -219,50 +212,91 @@ namespace Avalonia.Controls.Primitives |
|
|
|
public void Open() |
|
|
|
{ |
|
|
|
// Popup is currently open
|
|
|
|
if (_topLevel != null) |
|
|
|
if (_openState != 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; |
|
|
|
var topLevel = placementTarget.VisualRoot as TopLevel; |
|
|
|
|
|
|
|
if (_topLevel == null) |
|
|
|
if (topLevel == null) |
|
|
|
{ |
|
|
|
throw new InvalidOperationException( |
|
|
|
"Attempted to open a popup not attached to a TopLevel"); |
|
|
|
} |
|
|
|
|
|
|
|
_popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); |
|
|
|
var popupHost = OverlayPopupHost.CreatePopupHost(placementTarget, DependencyResolver); |
|
|
|
|
|
|
|
var handlerCleanup = new CompositeDisposable(5); |
|
|
|
|
|
|
|
_bindings.Add(_popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, |
|
|
|
void DeferCleanup(IDisposable? disposable) |
|
|
|
{ |
|
|
|
if (disposable is null) |
|
|
|
{ |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
handlerCleanup.Add(disposable); |
|
|
|
} |
|
|
|
|
|
|
|
DeferCleanup(popupHost.BindConstraints(this, WidthProperty, MinWidthProperty, MaxWidthProperty, |
|
|
|
HeightProperty, MinHeightProperty, MaxHeightProperty, TopmostProperty)); |
|
|
|
|
|
|
|
_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) |
|
|
|
popupHost.SetChild(Child); |
|
|
|
((ISetLogicalParent)popupHost).SetParent(this); |
|
|
|
|
|
|
|
popupHost.ConfigurePosition( |
|
|
|
placementTarget, |
|
|
|
PlacementMode, |
|
|
|
new Point(HorizontalOffset, VerticalOffset)); |
|
|
|
|
|
|
|
DeferCleanup(SubscribeToEventHandler<IPopupHost, EventHandler<TemplateAppliedEventArgs>>(popupHost, RootTemplateApplied, |
|
|
|
(x, handler) => x.TemplateApplied += handler, |
|
|
|
(x, handler) => x.TemplateApplied -= handler)); |
|
|
|
|
|
|
|
if (topLevel is Window window) |
|
|
|
{ |
|
|
|
window.Deactivated += WindowDeactivated; |
|
|
|
DeferCleanup(SubscribeToEventHandler<Window, EventHandler>(window, WindowDeactivated, |
|
|
|
(x, handler) => x.Deactivated += handler, |
|
|
|
(x, handler) => x.Deactivated -= handler)); |
|
|
|
} |
|
|
|
else |
|
|
|
{ |
|
|
|
var parentPopuproot = _topLevel as PopupRoot; |
|
|
|
if (parentPopuproot?.Parent is Popup popup) |
|
|
|
var parentPopupRoot = topLevel as PopupRoot; |
|
|
|
|
|
|
|
if (parentPopupRoot?.Parent is Popup popup) |
|
|
|
{ |
|
|
|
popup.Closed += ParentClosed; |
|
|
|
DeferCleanup(SubscribeToEventHandler<Popup, EventHandler>(popup, ParentClosed, |
|
|
|
(x, handler) => x.Closed += handler, |
|
|
|
(x, handler) => x.Closed -= handler)); |
|
|
|
} |
|
|
|
} |
|
|
|
_topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel); |
|
|
|
_nonClientListener = InputManager.Instance?.Process.Subscribe(ListenForNonClientClick); |
|
|
|
|
|
|
|
|
|
|
|
_popupHost.Show(); |
|
|
|
DeferCleanup(topLevel.AddHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel)); |
|
|
|
|
|
|
|
DeferCleanup(InputManager.Instance?.Process.Subscribe(ListenForNonClientClick)); |
|
|
|
|
|
|
|
var cleanupPopup = Disposable.Create((popupHost, handlerCleanup), state => |
|
|
|
{ |
|
|
|
state.handlerCleanup.Dispose(); |
|
|
|
|
|
|
|
state.popupHost.SetChild(null); |
|
|
|
state.popupHost.Hide(); |
|
|
|
|
|
|
|
((ISetLogicalParent)state.popupHost).SetParent(null); |
|
|
|
state.popupHost.Dispose(); |
|
|
|
}); |
|
|
|
|
|
|
|
_openState = new PopupOpenState(topLevel, popupHost, cleanupPopup); |
|
|
|
|
|
|
|
popupHost.Show(); |
|
|
|
|
|
|
|
using (BeginIgnoringIsOpen()) |
|
|
|
{ |
|
|
|
@ -277,14 +311,19 @@ namespace Avalonia.Controls.Primitives |
|
|
|
/// </summary>
|
|
|
|
public void Close() |
|
|
|
{ |
|
|
|
if (_popupHost != null) |
|
|
|
if (_openState is null) |
|
|
|
{ |
|
|
|
_popupHost.TemplateApplied -= RootTemplateApplied; |
|
|
|
using (BeginIgnoringIsOpen()) |
|
|
|
{ |
|
|
|
IsOpen = false; |
|
|
|
} |
|
|
|
|
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
_presenterSubscription?.Dispose(); |
|
|
|
_openState.Dispose(); |
|
|
|
_openState = null; |
|
|
|
|
|
|
|
CloseCurrent(); |
|
|
|
using (BeginIgnoringIsOpen()) |
|
|
|
{ |
|
|
|
IsOpen = false; |
|
|
|
@ -293,41 +332,6 @@ namespace Avalonia.Controls.Primitives |
|
|
|
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>
|
|
|
|
/// Measures the control.
|
|
|
|
/// </summary>
|
|
|
|
@ -345,16 +349,22 @@ namespace Avalonia.Controls.Primitives |
|
|
|
Close(); |
|
|
|
} |
|
|
|
|
|
|
|
private static IDisposable SubscribeToEventHandler<T, TEventHandler>(T target, TEventHandler handler, Action<T, TEventHandler> subscribe, Action<T, TEventHandler> unsubscribe) |
|
|
|
{ |
|
|
|
subscribe(target, handler); |
|
|
|
|
|
|
|
return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler)); |
|
|
|
} |
|
|
|
|
|
|
|
/// <summary>
|
|
|
|
/// Called when the <see cref="IsOpen"/> property changes.
|
|
|
|
/// </summary>
|
|
|
|
/// <param name="e">The event args.</param>
|
|
|
|
private void IsOpenChanged(AvaloniaPropertyChangedEventArgs e) |
|
|
|
private void IsOpenChanged(AvaloniaPropertyChangedEventArgs<bool> e) |
|
|
|
{ |
|
|
|
if (!_ignoreIsOpenChanged) |
|
|
|
{ |
|
|
|
if ((bool)e.NewValue) |
|
|
|
if (e.NewValue.Value) |
|
|
|
{ |
|
|
|
Open(); |
|
|
|
} |
|
|
|
@ -373,7 +383,7 @@ namespace Avalonia.Controls.Primitives |
|
|
|
{ |
|
|
|
LogicalChildren.Clear(); |
|
|
|
|
|
|
|
((ISetLogicalParent)e.OldValue)?.SetParent(null); |
|
|
|
((ISetLogicalParent?)e.OldValue)?.SetParent(null); |
|
|
|
|
|
|
|
if (e.NewValue != null) |
|
|
|
{ |
|
|
|
@ -394,34 +404,37 @@ namespace Avalonia.Controls.Primitives |
|
|
|
|
|
|
|
private void PointerPressedOutside(object sender, PointerPressedEventArgs e) |
|
|
|
{ |
|
|
|
if (!StaysOpen) |
|
|
|
if (!StaysOpen && !IsChildOrThis((IVisual)e.Source)) |
|
|
|
{ |
|
|
|
if (!IsChildOrThis((IVisual)e.Source)) |
|
|
|
{ |
|
|
|
Close(); |
|
|
|
e.Handled = true; |
|
|
|
} |
|
|
|
Close(); |
|
|
|
e.Handled = true; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private void RootTemplateApplied(object sender, TemplateAppliedEventArgs e) |
|
|
|
{ |
|
|
|
_popupHost.TemplateApplied -= RootTemplateApplied; |
|
|
|
|
|
|
|
if (_presenterSubscription != null) |
|
|
|
if (_openState is null) |
|
|
|
{ |
|
|
|
_presenterSubscription.Dispose(); |
|
|
|
_presenterSubscription = null; |
|
|
|
return; |
|
|
|
} |
|
|
|
|
|
|
|
var popupHost = _openState.PopupHost; |
|
|
|
|
|
|
|
popupHost.TemplateApplied -= RootTemplateApplied; |
|
|
|
|
|
|
|
_openState.SetPresenterSubscription(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) |
|
|
|
if (TemplatedParent != null && popupHost.Presenter != null) |
|
|
|
{ |
|
|
|
_popupHost.Presenter?.ApplyTemplate(); |
|
|
|
_popupHost.Presenter?.GetObservable(ContentPresenter.ChildProperty) |
|
|
|
popupHost.Presenter.ApplyTemplate(); |
|
|
|
|
|
|
|
var presenterSubscription = popupHost.Presenter.GetObservable(ContentPresenter.ChildProperty) |
|
|
|
.Subscribe(SetTemplatedParentAndApplyChildTemplates); |
|
|
|
|
|
|
|
_openState.SetPresenterSubscription(presenterSubscription); |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
@ -440,7 +453,7 @@ namespace Avalonia.Controls.Primitives |
|
|
|
|
|
|
|
if (!(control is IPresenter) && control.TemplatedParent == templatedParent) |
|
|
|
{ |
|
|
|
foreach (IControl child in control.GetVisualChildren()) |
|
|
|
foreach (IControl child in control.VisualChildren) |
|
|
|
{ |
|
|
|
SetTemplatedParentAndApplyChildTemplates(child); |
|
|
|
} |
|
|
|
@ -450,22 +463,41 @@ namespace Avalonia.Controls.Primitives |
|
|
|
|
|
|
|
private bool IsChildOrThis(IVisual child) |
|
|
|
{ |
|
|
|
IVisual root = child.GetVisualRoot(); |
|
|
|
while (root is IHostedVisualTreeRoot hostedRoot ) |
|
|
|
if (_openState is null) |
|
|
|
{ |
|
|
|
if (root == this._popupHost) |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
var popupHost = _openState.PopupHost; |
|
|
|
|
|
|
|
IVisual? root = child.VisualRoot; |
|
|
|
|
|
|
|
while (root is IHostedVisualTreeRoot hostedRoot) |
|
|
|
{ |
|
|
|
if (root == popupHost) |
|
|
|
{ |
|
|
|
return true; |
|
|
|
root = hostedRoot.Host?.GetVisualRoot(); |
|
|
|
} |
|
|
|
|
|
|
|
root = hostedRoot.Host?.VisualRoot; |
|
|
|
} |
|
|
|
|
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
public bool IsInsidePopup(IVisual visual) |
|
|
|
{ |
|
|
|
return _popupHost != null && ((IVisual)_popupHost)?.IsVisualAncestorOf(visual) == true; |
|
|
|
if (_openState is null) |
|
|
|
{ |
|
|
|
return false; |
|
|
|
} |
|
|
|
|
|
|
|
var popupHost = _openState.PopupHost; |
|
|
|
|
|
|
|
return popupHost != null && ((IVisual)popupHost).IsVisualAncestorOf(visual); |
|
|
|
} |
|
|
|
|
|
|
|
public bool IsPointerOverPopup => ((IInputElement)_popupHost).IsPointerOver; |
|
|
|
public bool IsPointerOverPopup => ((IInputElement?)_openState?.PopupHost)?.IsPointerOver ?? false; |
|
|
|
|
|
|
|
private void WindowDeactivated(object sender, EventArgs e) |
|
|
|
{ |
|
|
|
@ -503,5 +535,36 @@ namespace Avalonia.Controls.Primitives |
|
|
|
_owner._ignoreIsOpenChanged = false; |
|
|
|
} |
|
|
|
} |
|
|
|
|
|
|
|
private class PopupOpenState : IDisposable |
|
|
|
{ |
|
|
|
private readonly IDisposable _cleanup; |
|
|
|
private IDisposable? _presenterCleanup; |
|
|
|
|
|
|
|
public PopupOpenState(TopLevel topLevel, IPopupHost popupHost, IDisposable cleanup) |
|
|
|
{ |
|
|
|
TopLevel = topLevel; |
|
|
|
PopupHost = popupHost; |
|
|
|
_cleanup = cleanup; |
|
|
|
} |
|
|
|
|
|
|
|
public TopLevel TopLevel { get; } |
|
|
|
|
|
|
|
public IPopupHost PopupHost { get; } |
|
|
|
|
|
|
|
public void SetPresenterSubscription(IDisposable? presenterCleanup) |
|
|
|
{ |
|
|
|
_presenterCleanup?.Dispose(); |
|
|
|
|
|
|
|
_presenterCleanup = presenterCleanup; |
|
|
|
} |
|
|
|
|
|
|
|
public void Dispose() |
|
|
|
{ |
|
|
|
_presenterCleanup?.Dispose(); |
|
|
|
|
|
|
|
_cleanup.Dispose(); |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
} |
|
|
|
|