Browse Source

Merge pull request #10492 from AvaloniaUI/flyout-api

Update Flyout and Popup APIs to make gap between them smaller
pull/10579/head
Max Katz 3 years ago
committed by GitHub
parent
commit
f247e89516
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs
  2. 16
      samples/ControlCatalog/Pages/FlyoutsPage.axaml
  3. 1
      src/Avalonia.Base/Input/MouseDevice.cs
  4. 3
      src/Avalonia.Base/Input/PenDevice.cs
  5. 2
      src/Avalonia.Base/Input/PointerEventArgs.cs
  6. 33
      src/Avalonia.Base/Input/PointerOverPreProcessor.cs
  7. 1
      src/Avalonia.Base/Input/TouchDevice.cs
  8. 38
      src/Avalonia.Controls/Flyouts/Flyout.cs
  9. 548
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  10. 77
      src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs
  11. 39
      src/Avalonia.Controls/Flyouts/MenuFlyout.cs
  12. 512
      src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs
  13. 58
      src/Avalonia.Controls/PlacementMode.cs
  14. 48
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  15. 2
      src/Avalonia.Controls/SplitButton/SplitButton.cs
  16. 12
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

5
samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs

@ -36,8 +36,9 @@ namespace ControlCatalog.Pages
customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel);
var cancellableContextBorder = this.Get<Border>("CancellableContextBorder");
cancellableContextBorder.ContextFlyout!.Closing += ContextFlyoutPage_Closing;
cancellableContextBorder.ContextFlyout!.Opening += ContextFlyoutPage_Opening;
var flyout = (Flyout)cancellableContextBorder.ContextFlyout!;
flyout.Closing += ContextFlyoutPage_Closing;
flyout.Opening += ContextFlyoutPage_Opening;
}
private ContextPageViewModel? _model;

16
samples/ControlCatalog/Pages/FlyoutsPage.axaml

@ -16,8 +16,13 @@
<MenuItem Header="Item 3" />
</MenuFlyout>
<Flyout Placement="Bottom" x:Key="BasicFlyout">
<Flyout.FlyoutPresenterTheme>
<ControlTheme TargetType="FlyoutPresenter" BasedOn="{StaticResource {x:Type FlyoutPresenter}}">
<Setter Property="CornerRadius" Value="20" />
</ControlTheme>
</Flyout.FlyoutPresenterTheme>
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
<TextBlock Text="Flyout Content with a custom presenter theme!" TextWrapping="Wrap" />
</Panel>
</Flyout>
</UserControl.Resources>
@ -136,6 +141,15 @@
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=Center">
<Button.Flyout>
<Flyout Placement="Center">
<Panel Width="100" Height="100">
<TextBlock Text="Flyout Content!" />
</Panel>
</Flyout>
</Button.Flyout>
</Button>
<Button Content="Placement=TopEdgeAlignedLeft">
<Button.Flyout>
<Flyout Placement="TopEdgeAlignedLeft">

1
src/Avalonia.Base/Input/MouseDevice.cs

@ -4,6 +4,7 @@ using Avalonia.Reactive;
using Avalonia.Input.Raw;
using Avalonia.Platform;
using Avalonia.Utilities;
#pragma warning disable CS0618
namespace Avalonia.Input
{

3
src/Avalonia.Base/Input/PenDevice.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection;
using Avalonia.Input.Raw;
using Avalonia.Platform;
#pragma warning disable CS0618
namespace Avalonia.Input
{
@ -129,7 +130,7 @@ namespace Avalonia.Input
var e = new PointerReleasedEventArgs(source, pointer, (Visual)root, p, timestamp, properties, inputModifiers,
_lastMouseDownButton);
source?.RaiseEvent(e);
source.RaiseEvent(e);
pointer.Capture(null);
_lastMouseDownButton = default;
return e.Handled;

2
src/Avalonia.Base/Input/PointerEventArgs.cs

@ -42,7 +42,9 @@ namespace Avalonia.Input
PointerPointProperties properties,
KeyModifiers modifiers,
Lazy<IReadOnlyList<RawPointerPoint>?>? previousPoints)
#pragma warning disable CS0618
: this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers)
#pragma warning restore CS0618
{
_previousPoints = previousPoints;
}

33
src/Avalonia.Base/Input/PointerOverPreProcessor.cs

@ -6,7 +6,8 @@ namespace Avalonia.Input
internal class PointerOverPreProcessor : IObserver<RawInputEventArgs>
{
private IPointerDevice? _lastActivePointerDevice;
private (IPointer pointer, PixelPoint position)? _lastPointer;
private (IPointer pointer, PixelPoint position)? _currentPointer;
private PixelPoint? _lastKnownPosition;
private readonly IInputRoot _inputRoot;
@ -15,7 +16,7 @@ namespace Avalonia.Input
_inputRoot = inputRoot ?? throw new ArgumentNullException(nameof(inputRoot));
}
public PixelPoint? LastPosition => _lastPointer?.position;
public PixelPoint? LastPosition => _lastKnownPosition;
public void OnCompleted()
{
@ -41,14 +42,14 @@ namespace Avalonia.Input
}
if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown
&& _lastPointer is (var lastPointer, var lastPosition))
&& _currentPointer is var (lastPointer, lastPosition))
{
_lastPointer = null;
_currentPointer = null;
ClearPointerOver(lastPointer, args.Root, 0, PointToClient(args.Root, lastPosition),
new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()),
args.InputModifiers.ToKeyModifiers());
}
else if (pointerDevice.TryGetPointer(args) is IPointer pointer
else if (pointerDevice.TryGetPointer(args) is { } pointer
&& pointer.Type != PointerType.Touch)
{
var element = pointer.Captured ?? args.InputHitTestResult;
@ -62,7 +63,7 @@ namespace Avalonia.Input
public void SceneInvalidated(Rect dirtyRect)
{
if (_lastPointer is (var pointer, var position))
if (_currentPointer is (var pointer, var position))
{
var clientPoint = PointToClient(_inputRoot, position);
@ -80,12 +81,12 @@ namespace Avalonia.Input
private void ClearPointerOver()
{
if (_lastPointer is (var pointer, var position))
if (_currentPointer is (var pointer, var position))
{
var clientPoint = PointToClient(_inputRoot, position);
ClearPointerOver(pointer, _inputRoot, 0, clientPoint, PointerPointProperties.None, KeyModifiers.None);
}
_lastPointer = null;
_currentPointer = null;
_lastActivePointerDevice = null;
}
@ -100,9 +101,11 @@ namespace Avalonia.Input
// Do not pass rootVisual, when we have unknown position,
// so GetPosition won't return invalid values.
#pragma warning disable CS0618
var e = new PointerEventArgs(InputElement.PointerExitedEvent, element, pointer,
position.HasValue ? root as Visual : null, position.HasValue ? position.Value : default,
timestamp, properties, inputModifiers);
#pragma warning restore CS0618
if (element is Visual v && !v.IsAttachedToVisualTree)
{
@ -122,18 +125,18 @@ namespace Avalonia.Input
root.PointerOverElement = null;
_lastActivePointerDevice = null;
_lastPointer = null;
_currentPointer = null;
}
private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot)
{
if (element is Visual v)
{
foreach (IInputElement el in v.VisualChildren)
foreach (var el in v.VisualChildren)
{
if (el.IsPointerOver)
if (el is IInputElement { IsPointerOver: true } child)
{
ClearChildrenPointerOver(e, el, true);
ClearChildrenPointerOver(e, child, true);
break;
}
}
@ -151,6 +154,8 @@ namespace Avalonia.Input
ulong timestamp, Point position, PointerPointProperties properties, KeyModifiers inputModifiers)
{
var pointerOverElement = root.PointerOverElement;
var screenPosition = ((Visual)root).PointToScreen(position);
_lastKnownPosition = screenPosition;
if (element != pointerOverElement)
{
@ -164,7 +169,7 @@ namespace Avalonia.Input
}
}
_lastPointer = (pointer, ((Visual)root).PointToScreen(position));
_currentPointer = (pointer, screenPosition);
}
private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element,
@ -186,8 +191,10 @@ namespace Avalonia.Input
el = root.PointerOverElement;
#pragma warning disable CS0618
var e = new PointerEventArgs(InputElement.PointerExitedEvent, el, pointer, (Visual)root, position,
timestamp, properties, inputModifiers);
#pragma warning restore CS0618
if (el is Visual v && branch != null && !v.IsAttachedToVisualTree)
{
ClearChildrenPointerOver(e, branch, false);

1
src/Avalonia.Base/Input/TouchDevice.cs

@ -4,6 +4,7 @@ using System.Linq;
using System.Reflection;
using Avalonia.Input.Raw;
using Avalonia.Platform;
#pragma warning disable CS0618
namespace Avalonia.Input
{

38
src/Avalonia.Controls/Flyouts/Flyout.cs

@ -1,9 +1,11 @@
using Avalonia.Controls.Primitives;
using System.ComponentModel;
using Avalonia.Controls.Primitives;
using Avalonia.Metadata;
using Avalonia.Styling;
namespace Avalonia.Controls
{
public class Flyout : FlyoutBase
public class Flyout : PopupFlyoutBase
{
/// <summary>
/// Defines the <see cref="Content"/> property
@ -18,6 +20,21 @@ namespace Avalonia.Controls
private Classes? _classes;
/// <summary>
/// Defines the <see cref="FlyoutPresenterTheme"/> property.
/// </summary>
public static readonly StyledProperty<ControlTheme?> FlyoutPresenterThemeProperty =
AvaloniaProperty.Register<Flyout, ControlTheme?>(nameof(FlyoutPresenterTheme));
/// <summary>
/// Gets or sets the <see cref="ControlTheme"/> that is applied to the container element generated for the flyout presenter.
/// </summary>
public ControlTheme? FlyoutPresenterTheme
{
get => GetValue(FlyoutPresenterThemeProperty);
set => SetValue(FlyoutPresenterThemeProperty, value);
}
/// <summary>
/// Gets or sets the content to display in this flyout
/// </summary>
@ -36,13 +53,22 @@ namespace Avalonia.Controls
};
}
protected override void OnOpened()
protected override void OnOpening(CancelEventArgs args)
{
if (_classes != null)
if (Popup.Child is { } presenter)
{
SetPresenterClasses(Popup.Child, FlyoutPresenterClasses);
if (_classes != null)
{
SetPresenterClasses(presenter, FlyoutPresenterClasses);
}
if (FlyoutPresenterTheme is { } theme)
{
presenter.SetValue(Control.ThemeProperty, theme);
}
}
base.OnOpened();
base.OnOpening(args);
}
}
}

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

@ -1,17 +1,8 @@
using System;
using System.ComponentModel;
using Avalonia.Controls.Diagnostics;
using System.Linq;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.Reactive;
namespace Avalonia.Controls.Primitives
{
public abstract class FlyoutBase : AvaloniaObject, IPopupHostProvider
public abstract class FlyoutBase : AvaloniaObject
{
/// <summary>
/// Defines the <see cref="IsOpen"/> property
@ -26,75 +17,25 @@ namespace Avalonia.Controls.Primitives
public static readonly DirectProperty<FlyoutBase, Control?> TargetProperty =
AvaloniaProperty.RegisterDirect<FlyoutBase, Control?>(nameof(Target), x => x.Target);
/// <summary>
/// Defines the <see cref="Placement"/> property
/// </summary>
public static readonly StyledProperty<FlyoutPlacementMode> PlacementProperty =
AvaloniaProperty.Register<FlyoutBase, FlyoutPlacementMode>(nameof(Placement));
/// <summary>
/// Defines the <see cref="ShowMode"/> property
/// </summary>
public static readonly StyledProperty<FlyoutShowMode> ShowModeProperty =
AvaloniaProperty.Register<FlyoutBase, FlyoutShowMode>(nameof(ShowMode));
/// <summary>
/// Defines the <see cref="OverlayInputPassThroughElement"/> property
/// </summary>
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
Popup.OverlayInputPassThroughElementProperty.AddOwner<FlyoutBase>();
/// <summary>
/// Defines the AttachedFlyout property
/// </summary>
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 Rect? _enlargedPopupRect;
private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable;
private Action<IPopupHost?>? _popupHostChangedHandler;
static FlyoutBase()
{
Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
}
public FlyoutBase()
{
_popupLazy = new Lazy<Popup>(() => CreatePopup());
}
protected Popup Popup => _popupLazy.Value;
public event EventHandler? Opened;
public event EventHandler? Closed;
/// <summary>
/// Gets whether this Flyout is currently Open
/// </summary>
public bool IsOpen
{
get => _isOpen;
private set => SetAndRaise(IsOpenProperty, ref _isOpen, value);
}
/// <summary>
/// Gets or sets the desired placement
/// </summary>
public FlyoutPlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <summary>
/// Gets or sets the desired ShowMode
/// </summary>
public FlyoutShowMode ShowMode
{
get => GetValue(ShowModeProperty);
set => SetValue(ShowModeProperty, value);
protected set => SetAndRaise(IsOpenProperty, ref _isOpen, value);
}
/// <summary>
@ -103,32 +44,9 @@ namespace Avalonia.Controls.Primitives
public Control? Target
{
get => _target;
private set => SetAndRaise(TargetProperty, ref _target, value);
protected set => SetAndRaise(TargetProperty, ref _target, value);
}
/// <summary>
/// Gets or sets an element that should receive pointer input events even when underneath
/// the flyout's overlay.
/// </summary>
public IInputElement? OverlayInputPassThroughElement
{
get => GetValue(OverlayInputPassThroughElementProperty);
set => SetValue(OverlayInputPassThroughElementProperty, value);
}
IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
public event EventHandler? Closed;
public event EventHandler<CancelEventArgs>? Closing;
public event EventHandler? Opened;
public event EventHandler? Opening;
public static FlyoutBase? GetAttachedFlyout(Control element)
{
return element.GetValue(AttachedFlyoutProperty);
@ -145,462 +63,18 @@ namespace Avalonia.Controls.Primitives
flyout?.ShowAt(flyoutOwner);
}
/// <summary>
/// Shows the Flyout at the given Control
/// </summary>
/// <param name="placementTarget">The control to show the Flyout at</param>
public void ShowAt(Control placementTarget)
{
ShowAtCore(placementTarget);
}
/// <summary>
/// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout
/// </summary>
/// <param name="placementTarget">The target control</param>
/// <param name="showAtPointer">True to show at pointer</param>
public void ShowAt(Control placementTarget, bool showAtPointer)
{
ShowAtCore(placementTarget, showAtPointer);
}
/// <summary>
/// Hides the Flyout
/// </summary>
public void Hide()
{
HideCore();
}
/// <returns>True, if action was handled</returns>
protected virtual bool HideCore(bool canCancel = true)
{
if (!IsOpen)
{
return false;
}
if (canCancel)
{
if (CancelClosing())
{
return false;
}
}
IsOpen = false;
Popup.IsOpen = false;
((ISetLogicalParent)Popup).SetParent(null);
// Ensure this isn't active
_transientDisposable?.Dispose();
_transientDisposable = null;
_enlargedPopupRect = null;
_enlargePopupRectScreenPixelRect = null;
if (Target != null)
{
Target.DetachedFromVisualTree -= PlacementTarget_DetachedFromVisualTree;
Target.KeyUp -= OnPlacementTargetOrPopupKeyUp;
}
OnClosed();
return true;
}
/// <returns>True, if action was handled</returns>
protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = false)
{
if (placementTarget == null)
{
throw new ArgumentNullException(nameof(placementTarget));
}
if (IsOpen)
{
if (placementTarget == Target)
{
return false;
}
else // Close before opening a new one
{
_ = HideCore(false);
}
}
if (Popup.Parent != null && Popup.Parent != placementTarget)
{
((ISetLogicalParent)Popup).SetParent(null);
}
if (Popup.Parent == null || Popup.PlacementTarget != placementTarget)
{
Popup.PlacementTarget = Target = placementTarget;
((ISetLogicalParent)Popup).SetParent(placementTarget);
Popup.TemplatedParent = placementTarget.TemplatedParent;
}
if (Popup.Child == null)
{
Popup.Child = CreatePresenter();
}
Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement;
if (CancelOpening())
{
return false;
}
PositionPopup(showAtPointer);
IsOpen = Popup.IsOpen = true;
OnOpened();
placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree;
placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp;
if (ShowMode == FlyoutShowMode.Standard)
{
// Try and focus content inside Flyout
if (Popup.Child.Focusable)
{
FocusManager.Instance?.Focus(Popup.Child);
}
else
{
var nextFocus = KeyboardNavigationHandler.GetNext(Popup.Child, NavigationDirection.Next);
if (nextFocus != null)
{
FocusManager.Instance?.Focus(nextFocus);
}
}
}
else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway)
{
_transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss);
}
return true;
}
private void PlacementTarget_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_ = HideCore(false);
}
private void HandleTransientDismiss(RawInputEventArgs args)
{
if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move)
{
// In ShowMode = TransientWithDismissOnPointerMoveAway, the Flyout is kept
// shown as long as the pointer is within a certain px distance from the
// flyout itself. I'm not sure what WinUI uses, but I'm defaulting to
// 100px, which seems about right
// enlargedPopupRect is the Flyout bounds enlarged 100px
// For windowed popups, enlargedPopupRect is in screen coordinates,
// for overlay popups, its in OverlayLayer coordinates
if (_enlargedPopupRect == null && _enlargePopupRectScreenPixelRect == null)
{
// 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);
_enlargePopupRectScreenPixelRect = new PixelRect(root.PointToScreen(tmp.TopLeft), root.PointToScreen(tmp.BottomRight));
}
else if (Popup?.Host is OverlayPopupHost host)
{
// Overlay popups are in OverlayLayer coordinates, just use that
_enlargedPopupRect = host.Bounds.Inflate(100);
}
return;
}
if (Popup?.Host is PopupRoot && pArgs.Root is Visual eventRoot)
{
// As long as the pointer stays within the enlargedPopupRect
// the flyout stays open. If it leaves, close it
// Despite working in screen coordinates, leaving the TopLevel
// window will not close this (as pointer events stop), which
// does match UWP
var pt = eventRoot.PointToScreen(pArgs.Position);
if (!_enlargePopupRectScreenPixelRect?.Contains(pt) ?? false)
{
HideCore(false);
}
}
else if (Popup?.Host is OverlayPopupHost)
{
// Same as above here, but just different coordinate space
// so we don't need to translate
if (!_enlargedPopupRect?.Contains(pArgs.Position) ?? false)
{
HideCore(false);
}
}
}
}
protected virtual void OnOpening(CancelEventArgs args)
{
Opening?.Invoke(this, args);
}
public abstract void ShowAt(Control placementTarget);
public abstract void Hide();
protected virtual void OnOpened()
{
Opened?.Invoke(this, EventArgs.Empty);
}
protected virtual void OnClosing(CancelEventArgs args)
{
Closing?.Invoke(this, args);
}
protected virtual void OnClosed()
{
Closed?.Invoke(this, EventArgs.Empty);
}
/// <summary>
/// Used to create the content the Flyout displays
/// </summary>
/// <returns></returns>
protected abstract Control CreatePresenter();
private Popup CreatePopup()
{
var popup = new Popup
{
WindowManagerAddShadowHint = false,
IsLightDismissEnabled = true,
//Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss.
OverlayDismissEventPassThrough = false
};
popup.Opened += OnPopupOpened;
popup.Closed += OnPopupClosed;
popup.Closing += OnPopupClosing;
popup.KeyUp += OnPlacementTargetOrPopupKeyUp;
return popup;
}
private void OnPopupOpened(object? sender, EventArgs e)
{
IsOpen = true;
_popupHostChangedHandler?.Invoke(Popup.Host);
}
private void OnPopupClosing(object? sender, CancelEventArgs e)
{
if (IsOpen)
{
e.Cancel = CancelClosing();
}
}
private void OnPopupClosed(object? sender, EventArgs e)
{
HideCore(false);
_popupHostChangedHandler?.Invoke(null);
}
// 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)) == true)
{
e.Handled = HideCore();
}
}
}
private void PositionPopup(bool showAtPointer)
{
Size sz;
// Popup.Child can't be null here, it was set in ShowAtCore.
if (Popup.Child!.DesiredSize.IsDefault)
{
// Popup may not have been shown yet. Measure content
sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
}
else
{
sz = Popup.Child.DesiredSize;
}
if (showAtPointer)
{
Popup.PlacementMode = PlacementMode.Pointer;
}
else
{
Popup.PlacementMode = PlacementMode.AnchorAndGravity;
Popup.PlacementConstraintAdjustment =
PopupPositioning.PopupPositionerConstraintAdjustment.SlideX |
PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;
}
var trgtBnds = Target?.Bounds ?? default;
switch (Placement)
{
case FlyoutPlacementMode.Top: //Above & centered
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;
break;
case FlyoutPlacementMode.TopEdgeAlignedRight:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 10, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
break;
case FlyoutPlacementMode.RightEdgeAlignedTop:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
break;
case FlyoutPlacementMode.Right: //Right & centered
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 1, trgtBnds.Height);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Right;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
break;
case FlyoutPlacementMode.RightEdgeAlignedBottom:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Right;
break;
case FlyoutPlacementMode.Bottom: //Below & centered
Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, trgtBnds.Width, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Bottom;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
break;
case FlyoutPlacementMode.BottomEdgeAlignedLeft:
Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomRight;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
break;
case FlyoutPlacementMode.BottomEdgeAlignedRight:
Popup.PlacementRect = new Rect(trgtBnds.Width - 1, trgtBnds.Height - 1, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Bottom;
break;
case FlyoutPlacementMode.LeftEdgeAlignedTop:
Popup.PlacementRect = new Rect(0, 0, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.BottomLeft;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left;
break;
case FlyoutPlacementMode.Left: //Left & centered
Popup.PlacementRect = new Rect(0, 0, 1, trgtBnds.Height);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Left;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.Left;
break;
case FlyoutPlacementMode.LeftEdgeAlignedBottom:
Popup.PlacementRect = new Rect(0, trgtBnds.Height - 1, 1, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft;
Popup.PlacementAnchor = PopupPositioning.PopupAnchor.BottomLeft;
break;
//includes Auto (not sure what determines that)...
default:
//This is just FlyoutPlacementMode.Top behavior (above & centered)
Popup.PlacementRect = new Rect(-sz.Width / 2, 0, sz.Width, 1);
Popup.PlacementGravity = PopupPositioning.PopupGravity.Top;
break;
}
}
private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
if (args.Sender is Control c)
{
if (args.OldValue is FlyoutBase)
{
c.ContextRequested -= OnControlContextRequested;
}
if (args.NewValue is FlyoutBase)
{
c.ContextRequested += OnControlContextRequested;
}
}
}
private static void OnControlContextRequested(object? sender, ContextRequestedEventArgs e)
{
if (!e.Handled
&& sender is Control control
&& control.ContextFlyout is FlyoutBase flyout)
{
if (control.ContextMenu != null)
{
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();
OnClosing(eventArgs);
return eventArgs.Cancel;
}
private bool CancelOpening()
{
var eventArgs = new CancelEventArgs();
OnOpening(eventArgs);
return eventArgs.Cancel;
}
internal static void SetPresenterClasses(Control? presenter, Classes classes)
{
if(presenter is null)
{
return;
}
//Remove any classes no longer in use, ignoring pseudo classes
for (int i = presenter.Classes.Count - 1; i >= 0; i--)
{
if (!classes.Contains(presenter.Classes[i]) &&
!presenter.Classes[i].Contains(':'))
{
presenter.Classes.RemoveAt(i);
}
}
//Add new classes
presenter.Classes.AddRange(classes);
}
}
}

77
src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs

@ -1,77 +0,0 @@
namespace Avalonia.Controls
{
public enum FlyoutPlacementMode
{
/// <summary>
/// Preferred location is above the target element
/// </summary>
Top = 0,
/// <summary>
/// Preferred location is below the target element
/// </summary>
Bottom = 1,
/// <summary>
/// Preferred location is to the left of the target element
/// </summary>
Left = 2,
/// <summary>
/// Preferred location is to the right of the target element
/// </summary>
Right = 3,
//TODO
// <summary>
// Preferred location is centered on the screen
// </summary>
//Full = 4,
/// <summary>
/// Preferred location is above the target element, with the left edge of the flyout
/// aligned with the left edge of the target element
/// </summary>
TopEdgeAlignedLeft = 5,
/// <summary>
/// Preferred location is above the target element, with the right edge of flyout aligned with right edge of the target element.
/// </summary>
TopEdgeAlignedRight = 6,
/// <summary>
/// Preferred location is below the target element, with the left edge of flyout aligned with left edge of the target element.
/// </summary>
BottomEdgeAlignedLeft = 7,
/// <summary>
/// Preferred location is below the target element, with the right edge of flyout aligned with right edge of the target element.
/// </summary>
BottomEdgeAlignedRight = 8,
/// <summary>
/// Preferred location is to the left of the target element, with the top edge of flyout aligned with top edge of the target element.
/// </summary>
LeftEdgeAlignedTop = 9,
/// <summary>
/// Preferred location is to the left of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
/// </summary>
LeftEdgeAlignedBottom = 10,
/// <summary>
/// Preferred location is to the right of the target element, with the top edge of flyout aligned with top edge of the target element.
/// </summary>
RightEdgeAlignedTop = 11,
/// <summary>
/// Preferred location is to the right of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
/// </summary>
RightEdgeAlignedBottom = 12,
/// <summary>
/// Preferred location is determined automatically.
/// </summary>
Auto = 13
}
}

39
src/Avalonia.Controls/Flyouts/MenuFlyout.cs

@ -1,4 +1,5 @@
using System.Collections;
using System.ComponentModel;
using Avalonia.Collections;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
@ -7,7 +8,7 @@ using Avalonia.Styling;
namespace Avalonia.Controls
{
public class MenuFlyout : FlyoutBase
public class MenuFlyout : PopupFlyoutBase
{
public MenuFlyout()
{
@ -34,6 +35,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<ControlTheme?> ItemContainerThemeProperty =
ItemsControl.ItemContainerThemeProperty.AddOwner<MenuFlyout>();
/// <summary>
/// Defines the <see cref="FlyoutPresenterTheme"/> property.
/// </summary>
public static readonly StyledProperty<ControlTheme?> FlyoutPresenterThemeProperty =
Flyout.FlyoutPresenterThemeProperty.AddOwner<MenuFlyout>();
public Classes FlyoutPresenterClasses => _classes ??= new Classes();
/// <summary>
@ -60,10 +67,19 @@ namespace Avalonia.Controls
/// </summary>
public ControlTheme? ItemContainerTheme
{
get { return GetValue(ItemContainerThemeProperty); }
set { SetValue(ItemContainerThemeProperty, value); }
get => GetValue(ItemContainerThemeProperty);
set => SetValue(ItemContainerThemeProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="ControlTheme"/> that is applied to the container element generated for the flyout presenter.
/// </summary>
public ControlTheme? FlyoutPresenterTheme
{
get => GetValue(FlyoutPresenterThemeProperty);
set => SetValue(FlyoutPresenterThemeProperty, value);
}
private Classes? _classes;
private IEnumerable? _items;
private IDataTemplate? _itemTemplate;
@ -78,13 +94,22 @@ namespace Avalonia.Controls
};
}
protected override void OnOpened()
protected override void OnOpening(CancelEventArgs args)
{
if (_classes != null)
if (Popup.Child is { } presenter)
{
SetPresenterClasses(Popup.Child, FlyoutPresenterClasses);
if (_classes != null)
{
SetPresenterClasses(presenter, FlyoutPresenterClasses);
}
if (FlyoutPresenterTheme is { } theme)
{
presenter.SetValue(Control.ThemeProperty, theme);
}
}
base.OnOpened();
base.OnOpening(args);
}
}
}

512
src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs

@ -0,0 +1,512 @@
using System;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Diagnostics;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Input.Raw;
using Avalonia.Layout;
using Avalonia.Logging;
using Avalonia.Reactive;
namespace Avalonia.Controls.Primitives
{
public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider
{
/// <inheritdoc cref="Popup.PlacementModeProperty"/>
public static readonly StyledProperty<PlacementMode> PlacementProperty =
Popup.PlacementModeProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.HorizontalOffsetProperty"/>
public static readonly StyledProperty<double> HorizontalOffsetProperty =
Popup.HorizontalOffsetProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.VerticalOffsetProperty"/>
public static readonly StyledProperty<double> VerticalOffsetProperty =
Popup.VerticalOffsetProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
public static readonly StyledProperty<PopupAnchor> PlacementAnchorProperty =
Popup.PlacementAnchorProperty.AddOwner<PopupFlyoutBase>();
/// <inheritdoc cref="Popup.PlacementAnchorProperty"/>
public static readonly StyledProperty<PopupGravity> PlacementGravityProperty =
Popup.PlacementGravityProperty.AddOwner<PopupFlyoutBase>();
/// <summary>
/// Defines the <see cref="ShowMode"/> property
/// </summary>
public static readonly StyledProperty<FlyoutShowMode> ShowModeProperty =
AvaloniaProperty.Register<PopupFlyoutBase, FlyoutShowMode>(nameof(ShowMode));
/// <summary>
/// Defines the <see cref="OverlayInputPassThroughElement"/> property
/// </summary>
public static readonly StyledProperty<IInputElement?> OverlayInputPassThroughElementProperty =
Popup.OverlayInputPassThroughElementProperty.AddOwner<FlyoutBase>();
private readonly Lazy<Popup> _popupLazy;
private Rect? _enlargedPopupRect;
private PixelRect? _enlargePopupRectScreenPixelRect;
private IDisposable? _transientDisposable;
private Action<IPopupHost?>? _popupHostChangedHandler;
static PopupFlyoutBase()
{
Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged);
}
public PopupFlyoutBase()
{
_popupLazy = new Lazy<Popup>(() => CreatePopup());
}
protected Popup Popup => _popupLazy.Value;
/// <summary>
/// Gets or sets the desired placement.
/// </summary>
public PlacementMode Placement
{
get => GetValue(PlacementProperty);
set => SetValue(PlacementProperty, value);
}
/// <inheritdoc cref="Popup.PlacementGravity"/>
public PopupGravity PlacementGravity
{
get => GetValue(PlacementGravityProperty);
set => SetValue(PlacementGravityProperty, value);
}
/// <inheritdoc cref="Popup.PlacementAnchor"/>
public PopupAnchor PlacementAnchor
{
get => GetValue(PlacementAnchorProperty);
set => SetValue(PlacementAnchorProperty, value);
}
/// <inheritdoc cref="Popup.HorizontalOffset"/>
public double HorizontalOffset
{
get => GetValue(HorizontalOffsetProperty);
set => SetValue(HorizontalOffsetProperty, value);
}
/// <inheritdoc cref="Popup.VerticalOffset"/>
public double VerticalOffset
{
get => GetValue(VerticalOffsetProperty);
set => SetValue(VerticalOffsetProperty, value);
}
/// <summary>
/// Gets or sets the desired ShowMode
/// </summary>
public FlyoutShowMode ShowMode
{
get => GetValue(ShowModeProperty);
set => SetValue(ShowModeProperty, value);
}
/// <summary>
/// Gets or sets an element that should receive pointer input events even when underneath
/// the flyout's overlay.
/// </summary>
public IInputElement? OverlayInputPassThroughElement
{
get => GetValue(OverlayInputPassThroughElementProperty);
set => SetValue(OverlayInputPassThroughElementProperty, value);
}
IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host;
event Action<IPopupHost?>? IPopupHostProvider.PopupHostChanged
{
add => _popupHostChangedHandler += value;
remove => _popupHostChangedHandler -= value;
}
public event EventHandler<CancelEventArgs>? Closing;
public event EventHandler? Opening;
/// <summary>
/// Shows the Flyout at the given Control
/// </summary>
/// <param name="placementTarget">The control to show the Flyout at</param>
public sealed override void ShowAt(Control placementTarget)
{
ShowAtCore(placementTarget);
}
/// <summary>
/// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout
/// </summary>
/// <param name="placementTarget">The target control</param>
/// <param name="showAtPointer">True to show at pointer</param>
public void ShowAt(Control placementTarget, bool showAtPointer)
{
ShowAtCore(placementTarget, showAtPointer);
}
/// <summary>
/// Hides the Flyout
/// </summary>
public sealed override void Hide()
{
HideCore();
}
/// <returns>True, if action was handled</returns>
protected virtual bool HideCore(bool canCancel = true)
{
if (!IsOpen)
{
return false;
}
if (canCancel)
{
if (CancelClosing())
{
return false;
}
}
IsOpen = false;
Popup.IsOpen = false;
((ISetLogicalParent)Popup).SetParent(null);
// Ensure this isn't active
_transientDisposable?.Dispose();
_transientDisposable = null;
_enlargedPopupRect = null;
_enlargePopupRectScreenPixelRect = null;
if (Target != null)
{
Target.DetachedFromVisualTree -= PlacementTarget_DetachedFromVisualTree;
Target.KeyUp -= OnPlacementTargetOrPopupKeyUp;
}
OnClosed();
return true;
}
/// <returns>True, if action was handled</returns>
protected virtual bool ShowAtCore(Control placementTarget, bool showAtPointer = false)
{
if (placementTarget == null)
{
throw new ArgumentNullException(nameof(placementTarget));
}
if (IsOpen)
{
if (placementTarget == Target)
{
return false;
}
else // Close before opening a new one
{
_ = HideCore(false);
}
}
if (Popup.Parent != null && Popup.Parent != placementTarget)
{
((ISetLogicalParent)Popup).SetParent(null);
}
if (Popup.Parent == null || Popup.PlacementTarget != placementTarget)
{
Popup.PlacementTarget = Target = placementTarget;
((ISetLogicalParent)Popup).SetParent(placementTarget);
Popup.TemplatedParent = placementTarget.TemplatedParent;
}
if (Popup.Child == null)
{
Popup.Child = CreatePresenter();
}
Popup.OverlayInputPassThroughElement = OverlayInputPassThroughElement;
if (CancelOpening())
{
return false;
}
PositionPopup(showAtPointer);
IsOpen = Popup.IsOpen = true;
OnOpened();
placementTarget.DetachedFromVisualTree += PlacementTarget_DetachedFromVisualTree;
placementTarget.KeyUp += OnPlacementTargetOrPopupKeyUp;
if (ShowMode == FlyoutShowMode.Standard)
{
// Try and focus content inside Flyout
if (Popup.Child.Focusable)
{
FocusManager.Instance?.Focus(Popup.Child);
}
else
{
var nextFocus = KeyboardNavigationHandler.GetNext(Popup.Child, NavigationDirection.Next);
if (nextFocus != null)
{
FocusManager.Instance?.Focus(nextFocus);
}
}
}
else if (ShowMode == FlyoutShowMode.TransientWithDismissOnPointerMoveAway)
{
_transientDisposable = InputManager.Instance?.Process.Subscribe(HandleTransientDismiss);
}
return true;
}
private void PlacementTarget_DetachedFromVisualTree(object? sender, VisualTreeAttachmentEventArgs e)
{
_ = HideCore(false);
}
private void HandleTransientDismiss(RawInputEventArgs args)
{
if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move)
{
// In ShowMode = TransientWithDismissOnPointerMoveAway, the Flyout is kept
// shown as long as the pointer is within a certain px distance from the
// flyout itself. I'm not sure what WinUI uses, but I'm defaulting to
// 100px, which seems about right
// enlargedPopupRect is the Flyout bounds enlarged 100px
// For windowed popups, enlargedPopupRect is in screen coordinates,
// for overlay popups, its in OverlayLayer coordinates
if (_enlargedPopupRect == null && _enlargePopupRectScreenPixelRect == null)
{
// 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);
_enlargePopupRectScreenPixelRect = new PixelRect(root.PointToScreen(tmp.TopLeft), root.PointToScreen(tmp.BottomRight));
}
else if (Popup?.Host is OverlayPopupHost host)
{
// Overlay popups are in OverlayLayer coordinates, just use that
_enlargedPopupRect = host.Bounds.Inflate(100);
}
return;
}
if (Popup?.Host is PopupRoot && pArgs.Root is Visual eventRoot)
{
// As long as the pointer stays within the enlargedPopupRect
// the flyout stays open. If it leaves, close it
// Despite working in screen coordinates, leaving the TopLevel
// window will not close this (as pointer events stop), which
// does match UWP
var pt = eventRoot.PointToScreen(pArgs.Position);
if (!_enlargePopupRectScreenPixelRect?.Contains(pt) ?? false)
{
HideCore(false);
}
}
else if (Popup?.Host is OverlayPopupHost)
{
// Same as above here, but just different coordinate space
// so we don't need to translate
if (!_enlargedPopupRect?.Contains(pArgs.Position) ?? false)
{
HideCore(false);
}
}
}
}
protected virtual void OnOpening(CancelEventArgs args)
{
Opening?.Invoke(this, args);
}
protected virtual void OnClosing(CancelEventArgs args)
{
Closing?.Invoke(this, args);
}
/// <summary>
/// Used to create the content the Flyout displays
/// </summary>
/// <returns></returns>
protected abstract Control CreatePresenter();
private Popup CreatePopup()
{
var popup = new Popup
{
WindowManagerAddShadowHint = false,
IsLightDismissEnabled = true,
//Note: This is required to prevent Button.Flyout from opening the flyout again after dismiss.
OverlayDismissEventPassThrough = false
};
popup.Opened += OnPopupOpened;
popup.Closed += OnPopupClosed;
popup.Closing += OnPopupClosing;
popup.KeyUp += OnPlacementTargetOrPopupKeyUp;
return popup;
}
private void OnPopupOpened(object? sender, EventArgs e)
{
IsOpen = true;
_popupHostChangedHandler?.Invoke(Popup.Host);
}
private void OnPopupClosing(object? sender, CancelEventArgs e)
{
if (IsOpen)
{
e.Cancel = CancelClosing();
}
}
private void OnPopupClosed(object? sender, EventArgs e)
{
HideCore(false);
_popupHostChangedHandler?.Invoke(null);
}
// 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)) == true)
{
e.Handled = HideCore();
}
}
}
private void PositionPopup(bool showAtPointer)
{
Size sz;
// Popup.Child can't be null here, it was set in ShowAtCore.
if (Popup.Child!.DesiredSize.IsDefault)
{
// Popup may not have been shown yet. Measure content
sz = LayoutHelper.MeasureChild(Popup.Child, Size.Infinity, new Thickness());
}
else
{
sz = Popup.Child.DesiredSize;
}
Popup.VerticalOffset = VerticalOffset;
Popup.HorizontalOffset = HorizontalOffset;
Popup.PlacementAnchor = PlacementAnchor;
Popup.PlacementGravity = PlacementGravity;
if (showAtPointer)
{
Popup.PlacementMode = PlacementMode.Pointer;
}
else
{
Popup.PlacementMode = Placement;
Popup.PlacementConstraintAdjustment =
PopupPositioning.PopupPositionerConstraintAdjustment.SlideX |
PopupPositioning.PopupPositionerConstraintAdjustment.SlideY;
}
}
private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args)
{
if (args.Sender is Control c)
{
if (args.OldValue is FlyoutBase)
{
c.ContextRequested -= OnControlContextRequested;
}
if (args.NewValue is FlyoutBase)
{
c.ContextRequested += OnControlContextRequested;
}
}
}
private static void OnControlContextRequested(object? sender, ContextRequestedEventArgs e)
{
if (!e.Handled
&& sender is Control control
&& control.ContextFlyout is { } flyout)
{
if (control.ContextMenu != null)
{
Logger.TryGet(LogEventLevel.Verbose, "FlyoutBase")?.Log(control, "ContextMenu and ContextFlyout are both set, defaulting to ContextMenu");
return;
}
if (flyout is PopupFlyoutBase popupFlyout)
{
// We do not support absolute popup positioning yet, so we ignore "point" at this moment.
var triggeredByPointerInput = e.TryGetPosition(null, out _);
e.Handled = popupFlyout.ShowAtCore(control, triggeredByPointerInput);
}
else
{
flyout.ShowAt(control);
e.Handled = true;
}
}
}
private bool CancelClosing()
{
var eventArgs = new CancelEventArgs();
OnClosing(eventArgs);
return eventArgs.Cancel;
}
private bool CancelOpening()
{
var eventArgs = new CancelEventArgs();
OnOpening(eventArgs);
return eventArgs.Cancel;
}
internal static void SetPresenterClasses(Control? presenter, Classes classes)
{
if(presenter is null)
{
return;
}
//Remove any classes no longer in use, ignoring pseudo classes
for (int i = presenter.Classes.Count - 1; i >= 0; i--)
{
if (!classes.Contains(presenter.Classes[i]) &&
!presenter.Classes[i].Contains(':'))
{
presenter.Classes.RemoveAt(i);
}
}
//Add new classes
presenter.Classes.AddRange(classes);
}
}
}

58
src/Avalonia.Controls/PlacementMode.cs

@ -13,28 +13,74 @@ namespace Avalonia.Controls
Pointer,
/// <summary>
/// The popup is placed at the bottom left of its target.
/// Preferred location is below the target element.
/// </summary>
Bottom,
/// <summary>
/// The popup is placed at the top right of its target.
/// Preferred location is to the right of the target element.
/// </summary>
Right,
/// <summary>
/// The popup is placed at the top left of its target.
/// Preferred location is to the left of the target element.
/// </summary>
Left,
/// <summary>
/// The popup is placed at the top left of its target.
/// Preferred location is above the target element.
/// </summary>
Top,
/// <summary>
/// The popup is placed according to anchor and gravity rules
/// Preferred location is centered over the target element.
/// </summary>
AnchorAndGravity
Center,
/// <summary>
/// The popup is placed according to <see cref="Popup.PlacementAnchor"/> and <see cref="Popup.PlacementGravity"/> rules.
/// </summary>
AnchorAndGravity,
/// <summary>
/// Preferred location is above the target element, with the left edge of the popup
/// aligned with the left edge of the target element.
/// </summary>
TopEdgeAlignedLeft,
/// <summary>
/// Preferred location is above the target element, with the right edge of popup aligned with right edge of the target element.
/// </summary>
TopEdgeAlignedRight,
/// <summary>
/// Preferred location is below the target element, with the left edge of popup aligned with left edge of the target element.
/// </summary>
BottomEdgeAlignedLeft,
/// <summary>
/// Preferred location is below the target element, with the right edge of popup aligned with right edge of the target element.
/// </summary>
BottomEdgeAlignedRight,
/// <summary>
/// Preferred location is to the left of the target element, with the top edge of popup aligned with top edge of the target element.
/// </summary>
LeftEdgeAlignedTop,
/// <summary>
/// Preferred location is to the left of the target element, with the bottom edge of popup aligned with bottom edge of the target element.
/// </summary>
LeftEdgeAlignedBottom,
/// <summary>
/// Preferred location is to the right of the target element, with the top edge of popup aligned with top edge of the target element.
/// </summary>
RightEdgeAlignedTop,
/// <summary>
/// Preferred location is to the right of the target element, with the bottom edge of popup aligned with bottom edge of the target element.
/// </summary>
RightEdgeAlignedBottom
}
}

48
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@ -478,35 +478,29 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
var bounds = new Rect(default, target.Bounds.Size);
var anchorRect = rect ?? bounds;
positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value);
if (placement == PlacementMode.Right)
var parameters = placement switch
{
positionerParameters.Anchor = PopupAnchor.TopRight;
positionerParameters.Gravity = PopupGravity.BottomRight;
}
else if (placement == PlacementMode.Bottom)
{
positionerParameters.Anchor = PopupAnchor.BottomLeft;
positionerParameters.Gravity = PopupGravity.BottomRight;
}
else if (placement == PlacementMode.Left)
{
positionerParameters.Anchor = PopupAnchor.TopLeft;
positionerParameters.Gravity = PopupGravity.BottomLeft;
}
else if (placement == PlacementMode.Top)
{
positionerParameters.Anchor = PopupAnchor.TopLeft;
positionerParameters.Gravity = PopupGravity.TopRight;
}
else if (placement == PlacementMode.AnchorAndGravity)
{
positionerParameters.Anchor = anchor;
positionerParameters.Gravity = gravity;
}
else
throw new InvalidOperationException("Invalid value for Popup.PlacementMode");
PlacementMode.Bottom => (PopupAnchor.Bottom, PopupGravity.Bottom),
PlacementMode.Right => (PopupAnchor.Right, PopupGravity.Right),
PlacementMode.Left => (PopupAnchor.Left, PopupGravity.Left),
PlacementMode.Top => (PopupAnchor.Top, PopupGravity.Top),
PlacementMode.Center => (PopupAnchor.None, PopupGravity.None),
PlacementMode.AnchorAndGravity => (anchor, gravity),
PlacementMode.TopEdgeAlignedRight => (PopupAnchor.TopRight, PopupGravity.TopLeft),
PlacementMode.TopEdgeAlignedLeft => (PopupAnchor.TopLeft, PopupGravity.TopRight),
PlacementMode.BottomEdgeAlignedLeft => (PopupAnchor.BottomLeft, PopupGravity.BottomRight),
PlacementMode.BottomEdgeAlignedRight => (PopupAnchor.BottomRight, PopupGravity.BottomLeft),
PlacementMode.LeftEdgeAlignedTop => (PopupAnchor.TopLeft, PopupGravity.BottomLeft),
PlacementMode.LeftEdgeAlignedBottom => (PopupAnchor.BottomLeft, PopupGravity.TopLeft),
PlacementMode.RightEdgeAlignedTop => (PopupAnchor.TopRight, PopupGravity.BottomRight),
PlacementMode.RightEdgeAlignedBottom => (PopupAnchor.BottomRight, PopupGravity.TopRight),
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement,
"Invalid value for Popup.PlacementMode")
};
positionerParameters.Anchor = parameters.Item1;
positionerParameters.Gravity = parameters.Item2;
}
// Invert coordinate system if FlowDirection is RTL

2
src/Avalonia.Controls/SplitButton/SplitButton.cs

@ -172,7 +172,7 @@ namespace Avalonia.Controls
flyout.Opened += Flyout_Opened;
flyout.Closed += Flyout_Closed;
_flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(FlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged);
_flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(Popup.PlacementModeProperty).Subscribe(Flyout_PlacementPropertyChanged);
}
}

12
src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs

@ -82,7 +82,7 @@ namespace Avalonia.Diagnostics.ViewModels
{
Popup p => GetPopupHostObservable(p),
Control c => Observable.CombineLatest(
new IObservable<IPopupHostProvider?>[]
new IObservable<object?>[]
{
c.GetObservable(Control.ContextFlyoutProperty),
c.GetObservable(Control.ContextMenuProperty),
@ -93,11 +93,11 @@ namespace Avalonia.Diagnostics.ViewModels
.Select(
items =>
{
var contextFlyout = items[0];
var contextMenu = (ContextMenu?)items[1];
var attachedFlyout = items[2];
var toolTip = items[3];
var buttonFlyout = items[4];
var contextFlyout = items[0] as IPopupHostProvider;
var contextMenu = items[1] as ContextMenu;
var attachedFlyout = items[2] as IPopupHostProvider;
var toolTip = items[3] as IPopupHostProvider;
var buttonFlyout = items[4] as IPopupHostProvider;
if (contextMenu != null)
//Note: ContextMenus are special since all the items are added as visual children.

Loading…
Cancel
Save