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

7
src/Avalonia.Controls/Calendar/CalendarDatePicker.cs

@ -889,17 +889,12 @@ namespace Avalonia.Controls
_ignoreButtonClick = false; _ignoreButtonClick = false;
} }
} }
private void PopUp_Closed(object sender, PopupClosedEventArgs e) private void PopUp_Closed(object sender, EventArgs e)
{ {
IsDropDownOpen = false; IsDropDownOpen = false;
if(!_isPopupClosing) if(!_isPopupClosing)
{ {
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
_isPopupClosing = true; _isPopupClosing = true;
Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false); 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 = e.NameScope.Get<Popup>("PART_Popup");
_popup.Opened += PopupOpened; _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) 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?.Dispose();
_subscriptionsOnOpen = null; _subscriptionsOnOpen = null;
PopupClosedOverride(e);
if (CanFocus(this)) if (CanFocus(this))
{ {
Focus(); Focus();

1
src/Avalonia.Controls/ContextMenu.cs

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

59
src/Avalonia.Controls/Primitives/Popup.cs

@ -1,12 +1,10 @@
using System; using System;
using System.Diagnostics;
using System.Linq; using System.Linq;
using System.Reactive.Disposables; using System.Reactive.Disposables;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Input.Raw; using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.LogicalTree; using Avalonia.LogicalTree;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.Platform; using Avalonia.Platform;
@ -86,6 +84,9 @@ namespace Avalonia.Controls.Primitives
AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true); AvaloniaProperty.Register<Popup, bool>(nameof(ObeyScreenEdges), true);
#pragma warning restore 618 #pragma warning restore 618
public static readonly StyledProperty<bool> OverlayDismissEventPassThroughProperty =
AvaloniaProperty.Register<Popup, bool>(nameof(OverlayDismissEventPassThrough));
/// <summary> /// <summary>
/// Defines the <see cref="HorizontalOffset"/> property. /// Defines the <see cref="HorizontalOffset"/> property.
/// </summary> /// </summary>
@ -138,7 +139,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Raised when the popup closes. /// Raised when the popup closes.
/// </summary> /// </summary>
public event EventHandler<PopupClosedEventArgs>? Closed; public event EventHandler<EventArgs>? Closed;
/// <summary> /// <summary>
/// Raised when the popup opens. /// Raised when the popup opens.
@ -179,6 +180,9 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Gets or sets a value that determines how the <see cref="Popup"/> can be dismissed. /// Gets or sets a value that determines how the <see cref="Popup"/> can be dismissed.
/// </summary> /// </summary>
/// <remarks>
/// Light dismiss is when the user taps on any area other than the popup.
/// </remarks>
public bool IsLightDismissEnabled public bool IsLightDismissEnabled
{ {
get => GetValue(IsLightDismissEnabledProperty); get => GetValue(IsLightDismissEnabledProperty);
@ -266,6 +270,22 @@ namespace Avalonia.Controls.Primitives
set => SetValue(ObeyScreenEdgesProperty, value); 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> /// <summary>
/// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>. /// Gets or sets the Horizontal offset of the popup in relation to the <see cref="PlacementTarget"/>.
/// </summary> /// </summary>
@ -384,7 +404,7 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup) 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,
(x, handler) => x.Closed -= handler)); (x, handler) => x.Closed -= handler));
} }
@ -436,7 +456,7 @@ namespace Avalonia.Controls.Primitives
/// <summary> /// <summary>
/// Closes the popup. /// Closes the popup.
/// </summary> /// </summary>
public void Close() => CloseCore(null); public void Close() => CloseCore();
/// <summary> /// <summary>
/// Measures the control. /// Measures the control.
@ -506,7 +526,7 @@ namespace Avalonia.Controls.Primitives
} }
} }
private void CloseCore(EventArgs? closeEvent) private void CloseCore()
{ {
if (_openState is null) if (_openState is null)
{ {
@ -526,7 +546,7 @@ namespace Avalonia.Controls.Primitives
IsOpen = false; IsOpen = false;
} }
Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent)); Closed?.Invoke(this, EventArgs.Empty);
} }
private void ListenForNonClientClick(RawInputEventArgs e) private void ListenForNonClientClick(RawInputEventArgs e)
@ -535,7 +555,7 @@ namespace Avalonia.Controls.Primitives
if (IsLightDismissEnabled && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) 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)) 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 System.Linq;
using Avalonia.VisualTree; using Avalonia.VisualTree;
#nullable enable
namespace Avalonia.Input namespace Avalonia.Input
{ {
/// <summary> /// <summary>
@ -22,7 +24,7 @@ namespace Avalonia.Input
/// </returns> /// </returns>
public static IEnumerable<IInputElement> GetInputElementsAt(this IInputElement element, Point p) 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>(); 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="element">The element to test.</param>
/// <param name="p">The point on <paramref name="element"/>.</param> /// <param name="p">The point on <paramref name="element"/>.</param>
/// <returns>The topmost <see cref="IInputElement"/> at the specified position.</returns> /// <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; 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) private static bool IsHitTestVisible(IVisual visual)
{ {
var element = visual as IInputElement; var element = visual as IInputElement;

49
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

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

4
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

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

Loading…
Cancel
Save