Browse Source

Completely re-create PopupRoot on reopening Popup

Nikita Tsukanov 6 years ago
parent
commit
79cf3e5cea

+ 138 - 58
src/Avalonia.Controls/Primitives/Popup.cs

@@ -2,7 +2,11 @@
 // 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.Data;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Interactivity;
@@ -79,6 +83,8 @@ namespace Avalonia.Controls.Primitives
         private TopLevel _topLevel;
         private IDisposable _nonClientListener;
         bool _ignoreIsOpenChanged = false;
+        private List<IDisposable> _bindings = new List<IDisposable>();
+        private PopupContentHost _decorator = new PopupContentHost();
 
         /// <summary>
         /// Initializes static members of the <see cref="Popup"/> class.
@@ -91,6 +97,11 @@ namespace Avalonia.Controls.Primitives
             TopmostProperty.Changed.AddClassHandler<Popup>((p, e) => p.PopupRoot.Topmost = (bool)e.NewValue);
         }
 
+        public Popup()
+        {
+            _decorator[~PopupContentHost.ChildProperty] = this[~ChildProperty];
+        }
+
         /// <summary>
         /// Raised when the popup closes.
         /// </summary>
@@ -215,38 +226,48 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public void Open()
         {
-            if (PlacementTarget == null && PlacementMode != PlacementMode.Pointer)
-                throw new InvalidOperationException("It's not valid to show a popup without a PlacementTarget with PlacementMode != Pointer");
+            // 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");
             
-            if (_topLevel == null && PlacementTarget != null)
-                _topLevel = PlacementTarget.GetSelfAndLogicalAncestors().FirstOrDefault(x => x is TopLevel) as TopLevel;
+            _topLevel = placementTarget.GetVisualRoot() as TopLevel;
 
             if (_topLevel == null)
             {
-                if (PlacementTarget == null)
-                    throw new InvalidOperationException(
-                        "Attempted to open a popup not attached to a TopLevel and PlacementTarget is null");
                 throw new InvalidOperationException(
-                    "Attempted to open a popup not attached to a TopLevel and PlacementTarget is also not attached to a TopLevel");
+                    "Attempted to open a popup not attached to a TopLevel");
             }
 
-            if (_popupRoot == null)
+
+            _popupRoot = new PopupRoot(_topLevel, DependencyResolver)
             {
-                _popupRoot = new PopupRoot(_topLevel, 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);
-            }
+                [~WidthProperty] = this[~WidthProperty],
+                [~HeightProperty] = this[~HeightProperty],
+                [~MinWidthProperty] = this[~MinWidthProperty],
+                [~MaxWidthProperty] = this[~MaxWidthProperty],
+                [~MinHeightProperty] = this[~MinHeightProperty],
+                [~MaxHeightProperty] = this[~MaxHeightProperty],
+            };
+
+            void Bind(AvaloniaProperty prop) => _bindings.Add(_popupRoot.Bind(prop, this[~prop]));
+
+            Bind(WidthProperty);
+            Bind(MinWidthProperty);
+            Bind(MaxWidthProperty);
+            Bind(HeightProperty);
+            Bind(MinHeightProperty);
+            Bind(MaxHeightProperty);
+
+            _popupRoot.Content = _decorator;
+            
 
-            _popupRoot.ConfigurePosition(PlacementTarget ?? this.GetVisualParent<Control>(),
+            ((ISetLogicalParent)_popupRoot).SetParent(this);
+
+            _popupRoot.ConfigurePosition(placementTarget,
                 PlacementMode, new Point(HorizontalOffset, VerticalOffset));
             
             var window = _topLevel as Window;
@@ -282,35 +303,47 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public void Close()
         {
-            if (_popupRoot != null)
+            CloseCurrent();
+            using (BeginIgnoringIsOpen())
+            {
+                IsOpen = false;
+            }
+
+            Closed?.Invoke(this, EventArgs.Empty);
+        }
+        void CloseCurrent()
+        {
+            if (_topLevel != null)
             {
-                if (_topLevel != null)
+                _topLevel.RemoveHandler(PointerPressedEvent, PointerPressedOutside);
+                var window = _topLevel as Window;
+                if (window != null)
+                    window.Deactivated -= WindowDeactivated;
+                else
                 {
-                    _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)
                     {
-                        var parentPopuproot = _topLevel as PopupRoot;
-                        if (parentPopuproot?.Parent is Popup popup)
-                        {
-                            popup.Closed -= ParentClosed;
-                        }
+                        popup.Closed -= ParentClosed;
                     }
-                    _nonClientListener?.Dispose();
-                    _nonClientListener = null;
                 }
-
-                _popupRoot.Hide();
+                _nonClientListener?.Dispose();
+                _nonClientListener = null;
+                
+                _topLevel = null;
             }
-
-            using (BeginIgnoringIsOpen())
+            if (_popupRoot != null)
             {
-                IsOpen = false;
+                foreach(var b in _bindings)
+                    b.Dispose();
+                _bindings.Clear();
+                _popupRoot.Content = null;
+                _popupRoot.Hide();
+                ((ISetLogicalParent)_popupRoot).SetParent(null);
+                _popupRoot.Dispose();
+                _popupRoot = null;
             }
 
-            Closed?.Invoke(this, EventArgs.Empty);
         }
 
         /// <summary>
@@ -323,27 +356,14 @@ namespace Avalonia.Controls.Primitives
             return new Size();
         }
 
-        /// <inheritdoc/>
-        protected override void OnAttachedToLogicalTree(LogicalTreeAttachmentEventArgs e)
-        {
-            base.OnAttachedToLogicalTree(e);
-            _topLevel = e.Root as TopLevel;
-        }
-
         /// <inheritdoc/>
         protected override void OnDetachedFromLogicalTree(LogicalTreeAttachmentEventArgs e)
         {
             base.OnDetachedFromLogicalTree(e);
-            _topLevel = null;
-
-            if (_popupRoot != null)
-            {
-                ((ISetLogicalParent)_popupRoot).SetParent(null);
-                _popupRoot.Dispose();
-                _popupRoot = null;
-            }
+            Close();
         }
 
+
         /// <summary>
         /// Called when the <see cref="IsOpen"/> property changes.
         /// </summary>
@@ -449,5 +469,65 @@ namespace Avalonia.Controls.Primitives
                 _owner._ignoreIsOpenChanged = false;
             }
         }
+
+        // For some reason when PopupRoot.Content is bound directly to Child weird stuff happens 
+        class PopupContentHost : Control
+        {
+            /// <summary>
+            /// Defines the <see cref="Child"/> property.
+            /// </summary>
+            public static readonly StyledProperty<IControl> ChildProperty =
+                AvaloniaProperty.Register<Decorator, IControl>(nameof(Child));
+
+            static PopupContentHost()
+            {
+                ChildProperty.Changed.AddClassHandler<PopupContentHost>(x => x.ChildChanged);
+            }
+            
+            public IControl Child
+            {
+                get { return GetValue(ChildProperty); }
+                set { SetValue(ChildProperty, value); }
+            }
+            
+            /// <inheritdoc/>
+            protected override Size MeasureOverride(Size availableSize)
+            {
+                if (Child == null)
+                    return Size.Empty;
+                Child.Measure(availableSize);
+                return Child.DesiredSize;
+            }
+
+            /// <inheritdoc/>
+            protected override Size ArrangeOverride(Size finalSize)
+            {
+                Child?.Arrange(new Rect(finalSize));
+                return finalSize;
+            }
+
+            protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+            {
+                if (Child != null)
+                    VisualChildren.Add(Child);
+            }
+
+            protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
+            {
+                if (Child != null)
+                    VisualChildren.Remove(Child);
+            }
+
+            private void ChildChanged(AvaloniaPropertyChangedEventArgs e)
+            {
+                var oldChild = (Control)e.OldValue;
+                var newChild = (Control)e.NewValue;
+
+                if (oldChild != null) VisualChildren.Remove(oldChild);
+
+                if (newChild != null && VisualRoot != null)
+                    VisualChildren.Add(newChild);
+            }
+        }
     }
 }

+ 5 - 1
src/Avalonia.Controls/Primitives/PopupRoot.cs

@@ -2,9 +2,13 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Linq;
+using System.Text;
 using Avalonia.Controls.Platform;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.Data;
+using Avalonia.Diagnostics;
 using Avalonia.Interactivity;
 using Avalonia.Media;
 using Avalonia.Platform;
@@ -125,7 +129,7 @@ namespace Avalonia.Controls.Primitives
             PlatformImpl?.PopupPositioner.Update(_positionerParameters);
         }
 
-        public void ConfigurePosition(Control target, PlacementMode placement, Point offset,
+        public void ConfigurePosition(IVisual target, PlacementMode placement, Point offset,
             PopupPositioningEdge anchor = PopupPositioningEdge.None,
             PopupPositioningEdge gravity = PopupPositioningEdge.None)
         {

+ 1 - 0
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@@ -284,6 +284,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
                     new[]
                     {
                         "ContentPresenter",
+                        "PopupContentHost",
                         "ContentPresenter",
                         "Border",
                     },