Browse Source

Use ContextRequested event to show ContextFlyout + some refactoring of FlyoutBase

pull/6059/head
Max Katz 5 years ago
parent
commit
800788be20
  1. 144
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  2. 2
      src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs
  3. 10
      src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs
  4. 131
      tests/Avalonia.Controls.UnitTests/FlyoutTests.cs

144
src/Avalonia.Controls/Flyouts/FlyoutBase.cs

@ -1,6 +1,9 @@
using System;
using System.ComponentModel;
using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Logging;
@ -49,6 +52,7 @@ namespace Avalonia.Controls.Primitives
public static readonly AttachedProperty<FlyoutBase?> AttachedFlyoutProperty =
AvaloniaProperty.RegisterAttached<FlyoutBase, Control, FlyoutBase?>("AttachedFlyout", null);
private readonly Lazy<Popup> _popupLazy;
private bool _isOpen;
private Control? _target;
private FlyoutShowMode _showMode = FlyoutShowMode.Standard;
@ -56,7 +60,12 @@ namespace Avalonia.Controls.Primitives
private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable;
protected Popup? Popup { get; private set; }
public FlyoutBase()
{
_popupLazy = new Lazy<Popup>(() => CreatePopup());
}
protected Popup Popup => _popupLazy.Value;
/// <summary>
/// Gets whether this Flyout is currently Open
@ -142,18 +151,19 @@ namespace Avalonia.Controls.Primitives
HideCore();
}
protected virtual void HideCore(bool canCancel = true)
/// <returns>True, if action was handled</returns>
protected virtual bool HideCore(bool canCancel = true)
{
if (!IsOpen)
{
return;
return false;
}
if (canCancel)
{
if (CancelClosing())
{
return;
return false;
}
}
@ -166,34 +176,40 @@ namespace Avalonia.Controls.Primitives
_enlargedPopupRect = null;
_enlargePopupRectScreenPixelRect = null;
if (Target != null)
{
Target.DetachedFromVisualTree -= PlacementTarget_DetachedFromVisualTree;
Target.KeyUp -= OnPlacementTargetOrPopupKeyUp;
}
OnClosed();
return true;
}
protected virtual void ShowAtCore(Control placementTarget, bool showAtPointer = false)
/// <returns>True, if action was handled</returns>
protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = false)
{
if (placementTarget == null)
throw new ArgumentNullException("placementTarget cannot be null");
if (Popup == null)
{
InitPopup();
throw new ArgumentNullException(nameof(placementTarget));
}
if (IsOpen)
{
if (placementTarget == Target)
{
return;
return false;
}
else // Close before opening a new one
{
HideCore(false);
_ = HideCore(false);
}
}
if (CancelOpening())
{
return;
return false;
}
if (Popup.Parent != null && Popup.Parent != placementTarget)
@ -212,11 +228,13 @@ namespace Avalonia.Controls.Primitives
Popup.Child = CreatePresenter();
}
OnOpening();
PositionPopup(showAtPointer);
IsOpen = Popup.IsOpen = true;
IsOpen = Popup.IsOpen = true;
OnOpened();
placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree;
placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp;
if (ShowMode == FlyoutShowMode.Standard)
{
// Try and focus content inside Flyout
@ -237,6 +255,13 @@ namespace Avalonia.Controls.Primitives
{
_transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss);
}
return true;
}
private void PlacementTarget_DetachedFromVisualTree(object sender, VisualTreeAttachmentEventArgs e)
{
_ = HideCore(false);
}
private void HandleTransientDismiss(RawInputEventArgs args)
@ -255,7 +280,7 @@ namespace Avalonia.Controls.Primitives
{
// Only do this once when the Flyout opens & cache the result
if (Popup?.Host is PopupRoot root)
{
{
// Get the popup root bounds and convert to screen coordinates
var tmp = root.Bounds.Inflate(100);
@ -295,9 +320,9 @@ namespace Avalonia.Controls.Primitives
}
}
protected virtual void OnOpening()
protected virtual void OnOpening(CancelEventArgs args)
{
Opening?.Invoke(this, null);
Opening?.Invoke(this, args);
}
protected virtual void OnOpened()
@ -321,15 +346,18 @@ namespace Avalonia.Controls.Primitives
/// <returns></returns>
protected abstract Control CreatePresenter();
private void InitPopup()
private Popup CreatePopup()
{
Popup = new Popup();
Popup.WindowManagerAddShadowHint = false;
Popup.IsLightDismissEnabled = true;
Popup.Opened += OnPopupOpened;
Popup.Closed += OnPopupClosed;
Popup.Closing += OnPopupClosing;
var popup = new Popup();
popup.WindowManagerAddShadowHint = false;
popup.IsLightDismissEnabled = true;
popup.OverlayDismissEventPassThrough = true;
popup.Opened += OnPopupOpened;
popup.Closed += OnPopupClosed;
popup.Closing += OnPopupClosing;
popup.KeyUp += OnPlacementTargetOrPopupKeyUp;
return popup;
}
private void OnPopupOpened(object sender, EventArgs e)
@ -339,7 +367,10 @@ namespace Avalonia.Controls.Primitives
private void OnPopupClosing(object sender, CancelEventArgs e)
{
e.Cancel = CancelClosing();
if (IsOpen)
{
e.Cancel = CancelClosing();
}
}
private void OnPopupClosed(object sender, EventArgs e)
@ -347,10 +378,27 @@ namespace Avalonia.Controls.Primitives
HideCore(false);
}
// This method is handling both popup logical tree and target logical tree.
private void OnPlacementTargetOrPopupKeyUp(object sender, KeyEventArgs e)
{
if (!e.Handled
&& IsOpen
&& Target?.ContextFlyout == this)
{
var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
if (keymap.OpenContextMenu.Any(k => k.Matches(e)))
{
e.Handled = HideCore();
}
}
}
private void PositionPopup(bool showAtPointer)
{
Size sz;
if(Popup.Child.DesiredSize == Size.Empty)
// Popup.Child can't be null here, it was set in ShowAtCore.
if (Popup.Child!.DesiredSize == Size.Empty)
{
// Popup may not have been shown yet. Measure content
sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
@ -377,19 +425,19 @@ namespace Avalonia.Controls.Primitives
switch (Placement)
{
case FlyoutPlacementMode.Top: //Above & centered
Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width-1, 1);
Popup.PlacementRect = new Rect(0, 0, trgtBnds.Width - 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Top;
break;
case FlyoutPlacementMode.TopEdgeAlignedLeft:
Popup.PlacementRect = new Rect(0, 0, 0, 0);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
break;
case FlyoutPlacementMode.TopEdgeAlignedRight:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 10, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
break;
case FlyoutPlacementMode.RightEdgeAlignedTop:
@ -461,46 +509,44 @@ namespace Avalonia.Controls.Primitives
{
if (args.OldValue is FlyoutBase)
{
c.PointerReleased -= OnControlWithContextFlyoutPointerReleased;
c.ContextRequested -= OnControlContextRequested;
}
if (args.NewValue is FlyoutBase)
{
c.PointerReleased += OnControlWithContextFlyoutPointerReleased;
c.ContextRequested += OnControlContextRequested;
}
}
}
private static void OnControlWithContextFlyoutPointerReleased(object sender, PointerReleasedEventArgs e)
private static void OnControlContextRequested(object sender, ContextRequestedEventArgs e)
{
if (sender is Control c)
var control = (Control)sender;
if (!e.Handled
&& control.ContextFlyout is FlyoutBase flyout)
{
if (e.InitialPressMouseButton == MouseButton.Right &&
e.GetCurrentPoint(c).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonReleased)
if (control.ContextMenu != null)
{
if (c.ContextFlyout != null)
{
if (c.ContextMenu != null)
{
Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(c, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
return;
}
c.ContextFlyout.ShowAt(c, true);
}
Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
return;
}
}
// We do not support absolute popup positioning yet, so we ignore "point" at this moment.
var triggeredByPointerInput = e.TryGetPosition(null, out _);
e.Handled = flyout.ShowAtCore(control, triggeredByPointerInput);
}
}
private bool CancelClosing()
{
var eventArgs = new CancelEventArgs();
Closing?.Invoke(this, eventArgs);
OnClosing(eventArgs);
return eventArgs.Cancel;
}
private bool CancelOpening()
{
var eventArgs = new CancelEventArgs();
Opening?.Invoke(this, eventArgs);
OnOpening(eventArgs);
return eventArgs.Cancel;
}

2
src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs

@ -12,7 +12,7 @@
Standard,
/// <summary>
/// Behavior is typical of a flyout shown proactively. The open flyout does not take focus. For a CommandBarFlyout, it opens in it's collapsed state.
/// Behavior is typical of a flyout shown proactively. The open flyout does not take focus.
/// </summary>
Transient,

10
src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs

@ -29,16 +29,8 @@ namespace Avalonia.Controls
var host = this.FindLogicalAncestorOfType<Popup>();
if (host != null)
{
for (int i = 0; i < LogicalChildren.Count; i++)
{
if (LogicalChildren[i] is MenuItem item)
{
item.IsSubMenuOpen = false;
}
}
SelectedIndex = -1;
host.IsOpen = false;
host.IsOpen = false;
}
}

131
tests/Avalonia.Controls.UnitTests/FlyoutTests.cs

@ -1,10 +1,18 @@
using System;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Markup.Xaml;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests
@ -28,6 +36,7 @@ namespace Avalonia.Controls.UnitTests
f.ShowAt(window);
Assert.Equal(1, tracker);
Assert.True(f.IsOpen);
}
}
@ -51,6 +60,31 @@ namespace Avalonia.Controls.UnitTests
}
}
[Fact]
public void Opening_Is_Cancellable()
{
using (CreateServicesWithFocus())
{
var window = PreparedWindow();
window.Show();
int tracker = 0;
Flyout f = new Flyout();
f.Opening += (s, e) =>
{
tracker++;
if (e is CancelEventArgs cancelEventArgs)
{
cancelEventArgs.Cancel = true;
}
};
f.ShowAt(window);
Assert.Equal(1, tracker);
Assert.False(f.IsOpen);
}
}
[Fact]
public void Closing_Raises_Single_Closing_Event()
{
@ -101,16 +135,89 @@ namespace Avalonia.Controls.UnitTests
var window = PreparedWindow();
window.Show();
int tracker = 0;
Flyout f = new Flyout();
var tracker = 0;
var f = new Flyout();
f.Closing += (s, e) =>
{
tracker++;
e.Cancel = true;
};
f.ShowAt(window);
f.Hide();
Assert.True(f.IsOpen);
Assert.Equal(1, tracker);
}
}
[Fact]
public void Cancel_Light_Dismiss_Closing_Keeps_Flyout_Open()
{
using (CreateServicesWithFocus())
{
var window = PreparedWindow();
window.Width = 100;
window.Height = 100;
var button = new Button
{
Height = 10,
Width = 10,
HorizontalAlignment = Layout.HorizontalAlignment.Left,
VerticalAlignment = Layout.VerticalAlignment.Top
};
window.Content = button;
window.Show();
var tracker = 0;
var f = new Flyout();
f.Content = new Border { Width = 10, Height = 10 };
f.Closing += (s, e) =>
{
tracker++;
e.Cancel = true;
};
f.ShowAt(window);
var e = CreatePointerPressedEventArgs(window, new Point(90, 90));
var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
overlay.RaiseEvent(e);
Assert.Equal(1, tracker);
Assert.True(f.IsOpen);
}
}
[Fact]
public void Light_Dismiss_Closes_Flyout()
{
using (CreateServicesWithFocus())
{
var window = PreparedWindow();
window.Width = 100;
window.Height = 100;
var button = new Button
{
Height = 10,
Width = 10,
HorizontalAlignment = Layout.HorizontalAlignment.Left,
VerticalAlignment = Layout.VerticalAlignment.Top
};
window.Content = button;
window.Show();
var f = new Flyout();
f.Content = new Border { Width = 10, Height = 10 };
f.ShowAt(window);
var e = CreatePointerPressedEventArgs(window, new Point(90, 90));
var overlay = LightDismissOverlayLayer.GetLightDismissOverlayLayer(window);
overlay.RaiseEvent(e);
Assert.False(f.IsOpen);
}
}
@ -317,9 +424,27 @@ namespace Avalonia.Controls.UnitTests
private Window PreparedWindow(object content = null)
{
var w = new Window { Content = content };
var renderer = new Mock<IRenderer>();
var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();
var windowImpl = Mock.Get(platform.CreateWindow());
windowImpl.Setup(x => x.CreateRenderer(It.IsAny<IRenderRoot>())).Returns(renderer.Object);
var w = new Window(windowImpl.Object) { Content = content };
w.ApplyTemplate();
return w;
}
private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source, Point p)
{
var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
return new PointerPressedEventArgs(
source,
pointer,
source,
p,
0,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
KeyModifiers.None);
}
}
}

Loading…
Cancel
Save