@ -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 = > _ o penState ? . P opupHost;
/// <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 . Get VisualRoot( ) 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 . Get VisualChildren( ) )
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 ? ) _ o penState ? . P opupHost) ? . 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 ( ) ;
}
}
}
}