Browse Source

Added Popup.OverlayDismissEventPassThrough.

To control whether dismiss events are passed through from the overlay layer to the underlying window content.
Steven Kirk 5 years ago
parent
commit
c45436dff1

+ 1 - 6
src/Avalonia.Controls/AutoCompleteBox.cs

@@ -1647,7 +1647,7 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="sender">The source object.</param>
         /// <param name="e">The event data.</param>
-        private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e)
+        private void DropDownPopup_Closed(object sender, EventArgs e)
         {
             // Force the drop down dependency property to be false.
             if (IsDropDownOpen)
@@ -1655,11 +1655,6 @@ namespace Avalonia.Controls
                 IsDropDownOpen = false;
             }
 
-            if (e.CloseEvent is PointerEventArgs pointerEvent)
-            {
-                pointerEvent.Handled = true;
-            }
-
             // Fire the DropDownClosed event
             if (_popupHasOpened)
             {

+ 1 - 6
src/Avalonia.Controls/Calendar/CalendarDatePicker.cs

@@ -889,17 +889,12 @@ namespace Avalonia.Controls
                 _ignoreButtonClick = false;
             }
         }
-        private void PopUp_Closed(object sender, PopupClosedEventArgs e)
+        private void PopUp_Closed(object sender, EventArgs e)
         {
             IsDropDownOpen = false;
 
             if(!_isPopupClosing)
             {
-                if (e.CloseEvent is PointerEventArgs pointerEvent)
-                {
-                    pointerEvent.Handled = true;
-                }
-
                 _isPopupClosing = true;
                 Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false);
             }

+ 1 - 21
src/Avalonia.Controls/ComboBox.cs

@@ -290,24 +290,6 @@ namespace Avalonia.Controls
 
             _popup = e.NameScope.Get<Popup>("PART_Popup");
             _popup.Opened += PopupOpened;
-            _popup.Closed += PopupClosed;
-        }
-
-        /// <summary>
-        /// Called when the ComboBox popup is closed, with the <see cref="PopupClosedEventArgs"/>
-        /// that caused the popup to close.
-        /// </summary>
-        /// <param name="e">The event args.</param>
-        /// <remarks>
-        /// This method can be overridden to control whether the event that caused the popup to close
-        /// is swallowed or passed through.
-        /// </remarks>
-        protected virtual void PopupClosedOverride(PopupClosedEventArgs e)
-        {
-            if (e.CloseEvent is PointerEventArgs pointerEvent)
-            {
-                pointerEvent.Handled = true;
-            }
         }
 
         internal void ItemFocused(ComboBoxItem dropDownItem)
@@ -318,13 +300,11 @@ namespace Avalonia.Controls
             }
         }
 
-        private void PopupClosed(object sender, PopupClosedEventArgs e)
+        private void PopupClosed(object sender, EventArgs e)
         {
             _subscriptionsOnOpen?.Dispose();
             _subscriptionsOnOpen = null;
 
-            PopupClosedOverride(e);
-
             if (CanFocus(this))
             {
                 Focus();

+ 1 - 0
src/Avalonia.Controls/ContextMenu.cs

@@ -266,6 +266,7 @@ namespace Avalonia.Controls
                     PlacementRect = PlacementRect,
                     PlacementTarget = PlacementTarget ?? control,
                     IsLightDismissEnabled = true,
+                    OverlayDismissEventPassThrough = true,
                 };
 
                 _popup.Opened += PopupOpened;

+ 50 - 9
src/Avalonia.Controls/Primitives/Popup.cs

@@ -1,12 +1,10 @@
 using System;
-using System.Diagnostics;
 using System.Linq;
 using System.Reactive.Disposables;
 using Avalonia.Controls.Presenters;
 using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
-using Avalonia.Interactivity;
 using Avalonia.LogicalTree;
 using Avalonia.Metadata;
 using Avalonia.Platform;
@@ -86,6 +84,9 @@ namespace Avalonia.Controls.Primitives
             AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true);
 #pragma warning restore 618
 
+        public static readonly StyledProperty<bool> OverlayDismissEventPassThroughProperty =
+            AvaloniaProperty.Register<Popup, bool>(nameof(OverlayDismissEventPassThrough));
+
         /// <summary>
         /// Defines the <see cref="HorizontalOffset"/> property.
         /// </summary>
@@ -138,7 +139,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Raised when the popup closes.
         /// </summary>
-        public event EventHandler<PopupClosedEventArgs>? Closed;
+        public event EventHandler<EventArgs>? Closed;
 
         /// <summary>
         /// Raised when the popup opens.
@@ -179,6 +180,9 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Gets or sets a value that determines how the <see cref="Popup"/> can be dismissed.
         /// </summary>
+        /// <remarks>
+        /// Light dismiss is when the user taps on any area other than the popup.
+        /// </remarks>
         public bool IsLightDismissEnabled
         {
             get => GetValue(IsLightDismissEnabledProperty);
@@ -266,6 +270,22 @@ namespace Avalonia.Controls.Primitives
             set => SetValue(ObeyScreenEdgesProperty, value);
         }
 
+        /// <summary>
+        /// Gets or sets a value indicating whether the event that closes the popup is passed
+        /// through to the parent window.
+        /// </summary>
+        /// <remarks>
+        /// When <see cref="IsLightDismissEnabled"/> is set to true, clicks outside the the popup
+        /// cause the popup to close. When <see cref="OverlayDismissEventPassThrough"/> is set to
+        /// false, these clicks will be handled by the popup and not be registered by the parent
+        /// window. When set to true, the events will be passed through to the parent window.
+        /// </remarks>
+        public bool OverlayDismissEventPassThrough
+        {
+            get => GetValue(OverlayDismissEventPassThroughProperty);
+            set => SetValue(OverlayDismissEventPassThroughProperty, value);
+        }
+
         /// <summary>
         /// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>.
         /// </summary>
@@ -384,7 +404,7 @@ namespace Avalonia.Controls.Primitives
 
                 if (parentPopupRoot?.Parent is Popup popup)
                 {
-                    DeferCleanup(SubscribeToEventHandler<Popup, EventHandler<PopupClosedEventArgs>>(popup, ParentClosed,
+                    DeferCleanup(SubscribeToEventHandler<Popup, EventHandler<EventArgs>>(popup, ParentClosed,
                         (x, handler) => x.Closed += handler,
                         (x, handler) => x.Closed -= handler));
                 }
@@ -436,7 +456,7 @@ namespace Avalonia.Controls.Primitives
         /// <summary>
         /// Closes the popup.
         /// </summary>
-        public void Close() => CloseCore(null);
+        public void Close() => CloseCore();
 
         /// <summary>
         /// Measures the control.
@@ -506,7 +526,7 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
-        private void CloseCore(EventArgs? closeEvent)
+        private void CloseCore()
         {
             if (_openState is null)
             {
@@ -526,7 +546,7 @@ namespace Avalonia.Controls.Primitives
                 IsOpen = false;
             }
 
-            Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent));
+            Closed?.Invoke(this, EventArgs.Empty);
         }
 
         private void ListenForNonClientClick(RawInputEventArgs e)
@@ -535,7 +555,7 @@ namespace Avalonia.Controls.Primitives
 
             if (IsLightDismissEnabled && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
             {
-                CloseCore(e);
+                CloseCore();
             }
         }
 
@@ -543,7 +563,28 @@ namespace Avalonia.Controls.Primitives
         {
             if (IsLightDismissEnabled && e.Source is IVisual v && !IsChildOrThis(v))
             {
-                CloseCore(e);
+                CloseCore();
+
+                if (OverlayDismissEventPassThrough)
+                {
+                    PassThroughEvent(e);
+                }
+            }
+        }
+
+        private void PassThroughEvent(PointerPressedEventArgs e)
+        {
+            if (e.Source is LightDismissOverlayLayer layer &&
+                layer.GetVisualRoot() is IInputElement root)
+            {
+                var p = e.GetCurrentPoint(root);
+                var hit = root.InputHitTest(p.Position, x => x != layer);
+
+                if (hit != null)
+                {
+                    hit.RaiseEvent(e);
+                    e.Handled = true;
+                }
             }
         }
 

+ 0 - 33
src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs

@@ -1,33 +0,0 @@
-using System;
-using Avalonia.Interactivity;
-
-#nullable enable
-
-namespace Avalonia.Controls.Primitives
-{
-    /// <summary>
-    /// Holds data for the <see cref="Popup.Closed"/> event.
-    /// </summary>
-    public class PopupClosedEventArgs : EventArgs
-    {
-        /// <summary>
-        /// Initializes a new instance of the <see cref="PopupClosedEventArgs"/> class.
-        /// </summary>
-        /// <param name="closeEvent"></param>
-        public PopupClosedEventArgs(EventArgs? closeEvent)
-        {
-            CloseEvent = closeEvent;
-        }
-
-        /// <summary>
-        /// Gets the event that closed the popup, if any.
-        /// </summary>
-        /// <remarks>
-        /// If <see cref="Popup.IsLightDismissEnabled"/> is true, then this property will hold details of the
-        /// interaction that caused the popup to close if the close was caused by e.g. a pointer press
-        /// outside the popup. It can be used to mark the event as handled if the event should not
-        /// be propagated.
-        /// </remarks>
-        public EventArgs? CloseEvent { get; }
-    }
-}

+ 26 - 3
src/Avalonia.Input/InputExtensions.cs

@@ -3,6 +3,8 @@ using System.Collections.Generic;
 using System.Linq;
 using Avalonia.VisualTree;
 
+#nullable enable
+
 namespace Avalonia.Input
 {
     /// <summary>
@@ -22,7 +24,7 @@ namespace Avalonia.Input
         /// </returns>
         public static IEnumerable<IInputElement> GetInputElementsAt(this IInputElement element, Point p)
         {
-            Contract.Requires<ArgumentNullException>(element != null);
+            element = element ?? throw new ArgumentNullException(nameof(element));
 
             return element.GetVisualsAt(p, s_hitTestDelegate).Cast<IInputElement>();
         }
@@ -33,13 +35,34 @@ namespace Avalonia.Input
         /// <param name="element">The element to test.</param>
         /// <param name="p">The point on <paramref name="element"/>.</param>
         /// <returns>The topmost <see cref="IInputElement"/> at the specified position.</returns>
-        public static IInputElement InputHitTest(this IInputElement element, Point p)
+        public static IInputElement? InputHitTest(this IInputElement element, Point p)
         {
-            Contract.Requires<ArgumentNullException>(element != null);
+            element = element ?? throw new ArgumentNullException(nameof(element));
 
             return element.GetVisualAt(p, s_hitTestDelegate) as IInputElement;
         }
 
+        /// <summary>
+        /// Returns the topmost active input element at a point on an <see cref="IInputElement"/>.
+        /// </summary>
+        /// <param name="element">The element to test.</param>
+        /// <param name="p">The point on <paramref name="element"/>.</param>
+        /// <param name="filter">
+        /// A filter predicate. If the predicate returns false then the visual and all its
+        /// children will be excluded from the results.
+        /// </param>
+        /// <returns>The topmost <see cref="IInputElement"/> at the specified position.</returns>
+        public static IInputElement? InputHitTest(
+            this IInputElement element,
+            Point p,
+            Func<IVisual, bool> filter)
+        {
+            element = element ?? throw new ArgumentNullException(nameof(element));
+            filter = filter ?? throw new ArgumentNullException(nameof(filter));
+
+            return element.GetVisualAt(p, x => s_hitTestDelegate(x) && filter(x)) as IInputElement;
+        }
+
         private static bool IsHitTestVisible(IVisual visual)
         {
             var element = visual as IInputElement;

+ 19 - 30
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@@ -349,53 +349,42 @@ namespace Avalonia.Controls.UnitTests.Primitives
         }
 
         [Fact]
-        public void LightDismiss_Should_Not_Handle_Closing_Click()
+        public void OverlayDismissEventPassThrough_Should_Pass_Event_To_Window_Contents()
         {
             using (CreateServices())
             {
                 var window = PreparedWindow();
+                var rendererMock = Mock.Get(window.Renderer);
                 var target = new Popup() 
                 { 
                     PlacementTarget = window ,
                     IsLightDismissEnabled = true,
+                    OverlayDismissEventPassThrough = true,
                 };
 
-                target.Open();
-
-                var e = CreatePointerPressedEventArgs(window);
-                window.RaiseEvent(e);
+                var raised = 0;
+                var border = new Border();
+                window.Content = border;
 
-                Assert.False(e.Handled);
-            }
-        }
+                rendererMock.Setup(x =>
+                    x.HitTestFirst(new Point(10, 15), window, It.IsAny<Func<IVisual, bool>>()))
+                    .Returns(border);
 
-        [Fact]
-        public void Should_Pass_Closing_Click_To_Closed_Event()
-        {
-            using (CreateServices())
-            {
-                var window = PreparedWindow();
-                var target = new Popup()
+                border.PointerPressed += (s, e) =>
                 {
-                    PlacementTarget = window,
-                    IsLightDismissEnabled = true,
+                    Assert.Same(border, e.Source);
+                    ++raised;
                 };
 
                 target.Open();
+                Assert.True(target.IsOpen);
 
-                var press = CreatePointerPressedEventArgs(window);
-                var raised = 0;
-
-                target.Closed += (s, e) =>
-                {
-                    Assert.Same(press, e.CloseEvent);
-                    ++raised;
-                };
-
-                var lightDismissLayer = window.FindDescendantOfType<VisualLayerManager>().LightDismissOverlayLayer;
-                lightDismissLayer.RaiseEvent(press);
+                var e = CreatePointerPressedEventArgs(window, new Point(10, 15));
+                var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
+                overlay.RaiseEvent(e);
 
                 Assert.Equal(1, raised);
+                Assert.False(target.IsOpen);
             }
         }
 
@@ -411,14 +400,14 @@ namespace Avalonia.Controls.UnitTests.Primitives
                     })));
         }
 
-        private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source)
+        private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source, Point p)
         {
             var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
             return new PointerPressedEventArgs(
                 source,
                 pointer,
                 source,
-                default,
+                p,
                 0,
                 new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
                 KeyModifiers.None);

+ 4 - 0
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@@ -3,6 +3,7 @@ using Avalonia.Controls.Primitives.PopupPositioning;
 using Avalonia.Input;
 using Moq;
 using Avalonia.Platform;
+using Avalonia.Rendering;
 
 namespace Avalonia.UnitTests
 {
@@ -39,6 +40,9 @@ namespace Avalonia.UnitTests
                 return CreatePopupMock(windowImpl.Object).Object;
             });
 
+            windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>()))
+                .Returns(Mock.Of<IRenderer>());
+
             windowImpl.Setup(x => x.Dispose()).Callback(() =>
             {
                 windowImpl.Object.Closed?.Invoke();