Browse Source

Added Popup.OverlayDismissEventPassThrough.

To control whether dismiss events are passed through from the overlay layer to the underlying window content.
pull/4099/head
Steven Kirk 6 years ago
parent
commit
c45436dff1
  1. 7
      src/Avalonia.Controls/AutoCompleteBox.cs
  2. 7
      src/Avalonia.Controls/Calendar/CalendarDatePicker.cs
  3. 22
      src/Avalonia.Controls/ComboBox.cs
  4. 1
      src/Avalonia.Controls/ContextMenu.cs
  5. 59
      src/Avalonia.Controls/Primitives/Popup.cs
  6. 33
      src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
  7. 29
      src/Avalonia.Input/InputExtensions.cs
  8. 49
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  9. 4
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs

7
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)
{

7
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);
}

22
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
src/Avalonia.Controls/ContextMenu.cs

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

59
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;
}
}
}

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; }
}
}

29
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;

49
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
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();

Loading…
Cancel
Save