From aa2ad652e34afea5a9ed9af1ae8395f3f935102f Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 26 Feb 2023 11:26:51 -0500 Subject: [PATCH 01/10] Remove FlyoutPlacementMode and improve PlacementMode instead --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 90 +------------------ .../Flyouts/FlyoutPlacementMode.cs | 77 ---------------- src/Avalonia.Controls/PlacementMode.cs | 53 +++++++++-- .../PopupPositioning/IPopupPositioner.cs | 45 ++++------ 4 files changed, 70 insertions(+), 195 deletions(-) delete mode 100644 src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 9d4abec549..373386e259 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -29,8 +29,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property /// - public static readonly StyledProperty PlacementProperty = - AvaloniaProperty.Register(nameof(Placement)); + public static readonly StyledProperty PlacementProperty = + AvaloniaProperty.Register(nameof(Placement)); /// /// Defines the property @@ -87,7 +87,7 @@ namespace Avalonia.Controls.Primitives /// /// Gets or sets the desired placement /// - public FlyoutPlacementMode Placement + public PlacementMode Placement { get => GetValue(PlacementProperty); set => SetValue(PlacementProperty, value); @@ -452,93 +452,11 @@ namespace Avalonia.Controls.Primitives } else { - Popup.PlacementMode = PlacementMode.AnchorAndGravity; + Popup.PlacementMode = Placement; 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) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs b/src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs deleted file mode 100644 index 2e77de3b3b..0000000000 --- a/src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs +++ /dev/null @@ -1,77 +0,0 @@ -namespace Avalonia.Controls -{ - public enum FlyoutPlacementMode - { - /// - /// Preferred location is above the target element - /// - Top = 0, - - /// - /// Preferred location is below the target element - /// - Bottom = 1, - - /// - /// Preferred location is to the left of the target element - /// - Left = 2, - - /// - /// Preferred location is to the right of the target element - /// - Right = 3, - - //TODO - // - // Preferred location is centered on the screen - // - //Full = 4, - - /// - /// Preferred location is above the target element, with the left edge of the flyout - /// aligned with the left edge of the target element - /// - TopEdgeAlignedLeft = 5, - - /// - /// Preferred location is above the target element, with the right edge of flyout aligned with right edge of the target element. - /// - TopEdgeAlignedRight = 6, - - /// - /// Preferred location is below the target element, with the left edge of flyout aligned with left edge of the target element. - /// - BottomEdgeAlignedLeft = 7, - - /// - /// Preferred location is below the target element, with the right edge of flyout aligned with right edge of the target element. - /// - BottomEdgeAlignedRight = 8, - - /// - /// Preferred location is to the left of the target element, with the top edge of flyout aligned with top edge of the target element. - /// - LeftEdgeAlignedTop = 9, - - /// - /// Preferred location is to the left of the target element, with the bottom edge of flyout aligned with bottom edge of the target element. - /// - LeftEdgeAlignedBottom = 10, - - /// - /// Preferred location is to the right of the target element, with the top edge of flyout aligned with top edge of the target element. - /// - RightEdgeAlignedTop = 11, - - /// - /// Preferred location is to the right of the target element, with the bottom edge of flyout aligned with bottom edge of the target element. - /// - RightEdgeAlignedBottom = 12, - - /// - /// Preferred location is determined automatically. - /// - Auto = 13 - } -} diff --git a/src/Avalonia.Controls/PlacementMode.cs b/src/Avalonia.Controls/PlacementMode.cs index 68a4b9eecb..fa4f029cf3 100644 --- a/src/Avalonia.Controls/PlacementMode.cs +++ b/src/Avalonia.Controls/PlacementMode.cs @@ -13,28 +13,69 @@ namespace Avalonia.Controls Pointer, /// - /// The popup is placed at the bottom left of its target. + /// Preferred location is below the target element. /// Bottom, /// - /// The popup is placed at the top right of its target. + /// Preferred location is to the right of the target element. /// Right, /// - /// The popup is placed at the top left of its target. + /// Preferred location is to the left of the target element. /// Left, /// - /// The popup is placed at the top left of its target. + /// Preferred location is above the target element. /// Top, + + /// + /// The popup is placed according to and rules. + /// + AnchorAndGravity, /// - /// The popup is placed according to anchor and gravity rules + /// Preferred location is above the target element, with the left edge of the popup + /// aligned with the left edge of the target element. + /// + TopEdgeAlignedLeft, + + /// + /// Preferred location is above the target element, with the right edge of popup aligned with right edge of the target element. + /// + TopEdgeAlignedRight, + + /// + /// Preferred location is below the target element, with the left edge of popup aligned with left edge of the target element. + /// + BottomEdgeAlignedLeft, + + /// + /// Preferred location is below the target element, with the right edge of popup aligned with right edge of the target element. + /// + BottomEdgeAlignedRight, + + /// + /// Preferred location is to the left of the target element, with the top edge of popup aligned with top edge of the target element. + /// + LeftEdgeAlignedTop, + + /// + /// Preferred location is to the left of the target element, with the bottom edge of popup aligned with bottom edge of the target element. + /// + LeftEdgeAlignedBottom, + + /// + /// Preferred location is to the right of the target element, with the top edge of popup aligned with top edge of the target element. + /// + RightEdgeAlignedTop, + + /// + /// Preferred location is to the right of the target element, with the bottom edge of popup aligned with bottom edge of the target element. /// - AnchorAndGravity + RightEdgeAlignedBottom } } diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 2e70947457..0bb596d778 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -480,33 +480,26 @@ namespace Avalonia.Controls.Primitives.PopupPositioning var anchorRect = rect ?? bounds; 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.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 From 733238a8afe68ea128874eec0f86b1510c9cdc5d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 26 Feb 2023 11:48:42 -0500 Subject: [PATCH 02/10] Implement PlacementMode.Center --- .../ControlCatalog/Pages/FlyoutsPage.axaml | 9 ++++ src/Avalonia.Controls/PlacementMode.cs | 5 +++ .../PopupPositioning/IPopupPositioner.cs | 45 ++++++++++++++----- 3 files changed, 47 insertions(+), 12 deletions(-) diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml index 54aa9d1b67..8dbc6d283f 100644 --- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml +++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml @@ -136,6 +136,15 @@ + Top, + + /// + /// Preferred location is centered over the target element. + /// + Center, /// /// The popup is placed according to and rules. diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 0bb596d778..3f1b8008b1 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -464,21 +464,25 @@ namespace Avalonia.Controls.Primitives.PopupPositioning positionerParameters.Anchor = PopupAnchor.TopLeft; positionerParameters.Gravity = PopupGravity.BottomRight; } - else + else if (placement == PlacementMode.Center) { - if (target == null) - throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); - var matrix = target.TransformToVisual(topLevel); - if (matrix == null) + // Start with top left position, and add offset to move it to the center + var anchorRectangle = GetAnchorRectangle(topLevel, target, rect); + positionerParameters.AnchorRectangle = anchorRectangle; + positionerParameters.Anchor = PopupAnchor.TopLeft; + positionerParameters.Gravity = PopupGravity.BottomRight; + + var targetSize = positionerParameters.Size; + if (targetSize != default) { - if (target.GetVisualRoot() == null) - throw new InvalidOperationException("Target control is not attached to the visual tree"); - throw new InvalidOperationException("Target control is not in the same tree as the popup parent"); + positionerParameters.Offset += new Point( + (anchorRectangle.Width - targetSize.Width) / 2, + (anchorRectangle.Height - targetSize.Height) / 2); } - - var bounds = new Rect(default, target.Bounds.Size); - var anchorRect = rect ?? bounds; - positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value); + } + else + { + positionerParameters.AnchorRectangle = GetAnchorRectangle(topLevel, target, rect); var parameters = placement switch { @@ -528,6 +532,23 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } } } + + private static Rect GetAnchorRectangle(TopLevel topLevel, Visual target, Rect? rect) + { + if (target == null) + throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); + var matrix = target.TransformToVisual(topLevel); + if (matrix == null) + { + if (target.GetVisualRoot() == null) + throw new InvalidOperationException("Target control is not attached to the visual tree"); + throw new InvalidOperationException("Target control is not in the same tree as the popup parent"); + } + + var bounds = new Rect(default, target.Bounds.Size); + var anchorRect = rect ?? bounds; + return anchorRect.Intersect(bounds).TransformToAABB(matrix.Value); + } } } From 8281c6e14f4e6288270ab89769433aa1789cffa0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 26 Feb 2023 12:05:07 -0500 Subject: [PATCH 03/10] Split FlyoutBase and PopupFlyoutBase --- src/Avalonia.Controls/Flyouts/Flyout.cs | 2 +- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 471 +----------------- src/Avalonia.Controls/Flyouts/MenuFlyout.cs | 2 +- .../Flyouts/PopupFlyoutBase.cs | 470 +++++++++++++++++ .../SplitButton/SplitButton.cs | 2 +- 5 files changed, 484 insertions(+), 463 deletions(-) create mode 100644 src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs diff --git a/src/Avalonia.Controls/Flyouts/Flyout.cs b/src/Avalonia.Controls/Flyouts/Flyout.cs index df3fe28a29..262edbcc14 100644 --- a/src/Avalonia.Controls/Flyouts/Flyout.cs +++ b/src/Avalonia.Controls/Flyouts/Flyout.cs @@ -3,7 +3,7 @@ using Avalonia.Metadata; namespace Avalonia.Controls { - public class Flyout : FlyoutBase + public class Flyout : PopupFlyoutBase { /// /// Defines the property diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 373386e259..b5328ccab8 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/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 { /// /// Defines the property @@ -26,80 +17,25 @@ namespace Avalonia.Controls.Primitives public static readonly DirectProperty TargetProperty = AvaloniaProperty.RegisterDirect(nameof(Target), x => x.Target); - /// - /// Defines the property - /// - public static readonly StyledProperty PlacementProperty = - AvaloniaProperty.Register(nameof(Placement)); - - /// - /// Defines the property - /// - public static readonly DirectProperty ShowModeProperty = - AvaloniaProperty.RegisterDirect(nameof(ShowMode), - x => x.ShowMode, (x, v) => x.ShowMode = v); - - /// - /// Defines the property - /// - public static readonly DirectProperty OverlayInputPassThroughElementProperty = - Popup.OverlayInputPassThroughElementProperty.AddOwner( - o => o._overlayInputPassThroughElement, - (o, v) => o._overlayInputPassThroughElement = v); - /// /// Defines the AttachedFlyout property /// public static readonly AttachedProperty AttachedFlyoutProperty = AvaloniaProperty.RegisterAttached("AttachedFlyout", null); - private readonly Lazy _popupLazy; private bool _isOpen; private Control? _target; - private FlyoutShowMode _showMode = FlyoutShowMode.Standard; - private Rect? _enlargedPopupRect; - private PixelRect? _enlargePopupRectScreenPixelRect; - private IDisposable? _transientDisposable; - private Action? _popupHostChangedHandler; - private IInputElement? _overlayInputPassThroughElement; - - static FlyoutBase() - { - Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); - } - - public FlyoutBase() - { - _popupLazy = new Lazy(() => CreatePopup()); - } - - protected Popup Popup => _popupLazy.Value; + public event EventHandler? Opened; + public event EventHandler? Closed; + /// /// Gets whether this Flyout is currently Open /// public bool IsOpen { get => _isOpen; - private set => SetAndRaise(IsOpenProperty, ref _isOpen, value); - } - - /// - /// Gets or sets the desired placement - /// - public PlacementMode Placement - { - get => GetValue(PlacementProperty); - set => SetValue(PlacementProperty, value); - } - - /// - /// Gets or sets the desired ShowMode - /// - public FlyoutShowMode ShowMode - { - get => _showMode; - set => SetAndRaise(ShowModeProperty, ref _showMode, value); + protected set => SetAndRaise(IsOpenProperty, ref _isOpen, value); } /// @@ -108,32 +44,9 @@ namespace Avalonia.Controls.Primitives public Control? Target { get => _target; - private set => SetAndRaise(TargetProperty, ref _target, value); - } - - /// - /// Gets or sets an element that should receive pointer input events even when underneath - /// the flyout's overlay. - /// - public IInputElement? OverlayInputPassThroughElement - { - get => _overlayInputPassThroughElement; - set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + protected set => SetAndRaise(TargetProperty, ref _target, value); } - - IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; - - event Action? IPopupHostProvider.PopupHostChanged - { - add => _popupHostChangedHandler += value; - remove => _popupHostChangedHandler -= value; - } - - public event EventHandler? Closed; - public event EventHandler? Closing; - public event EventHandler? Opened; - public event EventHandler? Opening; - + public static FlyoutBase? GetAttachedFlyout(Control element) { return element.GetValue(AttachedFlyoutProperty); @@ -150,380 +63,18 @@ namespace Avalonia.Controls.Primitives flyout?.ShowAt(flyoutOwner); } - /// - /// Shows the Flyout at the given Control - /// - /// The control to show the Flyout at - public void ShowAt(Control placementTarget) - { - ShowAtCore(placementTarget); - } - - /// - /// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout - /// - /// The target control - /// True to show at pointer - public void ShowAt(Control placementTarget, bool showAtPointer) - { - ShowAtCore(placementTarget, showAtPointer); - } - - /// - /// Hides the Flyout - /// - public void Hide() - { - HideCore(); - } - - /// True, if action was handled - 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; - } - - /// True, if action was handled - 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.SetValue(StyledElement.TemplatedParentProperty, 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); } - - /// - /// Used to create the content the Flyout displays - /// - /// - 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(); - - 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 = 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 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); - } } } diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index b028a8f007..79a6cdb313 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs @@ -7,7 +7,7 @@ using Avalonia.Styling; namespace Avalonia.Controls { - public class MenuFlyout : FlyoutBase + public class MenuFlyout : PopupFlyoutBase { public MenuFlyout() { diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs new file mode 100644 index 0000000000..4a19c215cf --- /dev/null +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -0,0 +1,470 @@ +using System; +using System.ComponentModel; +using System.Linq; +using Avalonia.Controls.Diagnostics; +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 + { + /// + /// Defines the property + /// + public static readonly StyledProperty PlacementProperty = + AvaloniaProperty.Register(nameof(Placement)); + + /// + /// Defines the property + /// + public static readonly DirectProperty ShowModeProperty = + AvaloniaProperty.RegisterDirect(nameof(ShowMode), + x => x.ShowMode, (x, v) => x.ShowMode = v); + + /// + /// Defines the property + /// + public static readonly DirectProperty OverlayInputPassThroughElementProperty = + Popup.OverlayInputPassThroughElementProperty.AddOwner( + o => o._overlayInputPassThroughElement, + (o, v) => o._overlayInputPassThroughElement = v); + + private readonly Lazy _popupLazy; + private FlyoutShowMode _showMode = FlyoutShowMode.Standard; + private Rect? _enlargedPopupRect; + private PixelRect? _enlargePopupRectScreenPixelRect; + private IDisposable? _transientDisposable; + private Action? _popupHostChangedHandler; + private IInputElement? _overlayInputPassThroughElement; + + static PopupFlyoutBase() + { + Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); + } + + public PopupFlyoutBase() + { + _popupLazy = new Lazy(() => CreatePopup()); + } + + protected Popup Popup => _popupLazy.Value; + + /// + /// Gets or sets the desired placement + /// + public PlacementMode Placement + { + get => GetValue(PlacementProperty); + set => SetValue(PlacementProperty, value); + } + + /// + /// Gets or sets the desired ShowMode + /// + public FlyoutShowMode ShowMode + { + get => _showMode; + set => SetAndRaise(ShowModeProperty, ref _showMode, value); + } + + /// + /// Gets or sets an element that should receive pointer input events even when underneath + /// the flyout's overlay. + /// + public IInputElement? OverlayInputPassThroughElement + { + get => _overlayInputPassThroughElement; + set => SetAndRaise(OverlayInputPassThroughElementProperty, ref _overlayInputPassThroughElement, value); + } + + IPopupHost? IPopupHostProvider.PopupHost => Popup?.Host; + + event Action? IPopupHostProvider.PopupHostChanged + { + add => _popupHostChangedHandler += value; + remove => _popupHostChangedHandler -= value; + } + + public event EventHandler? Closing; + public event EventHandler? Opening; + + /// + /// Shows the Flyout at the given Control + /// + /// The control to show the Flyout at + public sealed override void ShowAt(Control placementTarget) + { + ShowAtCore(placementTarget); + } + + /// + /// Shows the Flyout for the given control at the current pointer location, as in a ContextFlyout + /// + /// The target control + /// True to show at pointer + public void ShowAt(Control placementTarget, bool showAtPointer) + { + ShowAtCore(placementTarget, showAtPointer); + } + + /// + /// Hides the Flyout + /// + public sealed override void Hide() + { + HideCore(); + } + + /// True, if action was handled + 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; + } + + /// True, if action was handled + 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.SetValue(StyledElement.TemplatedParentProperty, 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); + } + + /// + /// Used to create the content the Flyout displays + /// + /// + 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(); + + 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 = 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); + } + } +} diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 31a06d875a..896c8eae31 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -176,7 +176,7 @@ namespace Avalonia.Controls flyout.Opened += Flyout_Opened; flyout.Closed += Flyout_Closed; - _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(FlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged); + _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(PopupFlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged); } } From 4e47a273047f4e3b76838d66bd95d8095b9e762e Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 26 Feb 2023 12:24:28 -0500 Subject: [PATCH 04/10] Add some popup properties to the PopupFlyoutBase --- .../Pages/ContextFlyoutPage.xaml.cs | 5 +- .../Flyouts/PopupFlyoutBase.cs | 58 +++++++++++++++++-- .../SplitButton/SplitButton.cs | 2 +- .../Diagnostics/ViewModels/VisualTreeNode.cs | 12 ++-- 4 files changed, 63 insertions(+), 14 deletions(-) diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs index 8bd1f4d85a..0388eee4e2 100644 --- a/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.xaml.cs @@ -36,8 +36,9 @@ namespace ControlCatalog.Pages customContextRequestedBorder.AddHandler(ContextRequestedEvent, CustomContextRequested, RoutingStrategies.Tunnel); var cancellableContextBorder = this.Get("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; diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs index 4a19c215cf..052143622c 100644 --- a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -2,6 +2,7 @@ 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; @@ -13,11 +14,25 @@ namespace Avalonia.Controls.Primitives { public abstract class PopupFlyoutBase : FlyoutBase, IPopupHostProvider { - /// - /// Defines the property - /// + /// public static readonly StyledProperty PlacementProperty = - AvaloniaProperty.Register(nameof(Placement)); + Popup.PlacementModeProperty.AddOwner(); + + /// + public static readonly StyledProperty HorizontalOffsetProperty = + Popup.HorizontalOffsetProperty.AddOwner(); + + /// + public static readonly StyledProperty VerticalOffsetProperty = + Popup.VerticalOffsetProperty.AddOwner(); + + /// + public static readonly StyledProperty PlacementAnchorProperty = + Popup.PlacementAnchorProperty.AddOwner(); + + /// + public static readonly StyledProperty PlacementGravityProperty = + Popup.PlacementGravityProperty.AddOwner(); /// /// Defines the property @@ -55,14 +70,43 @@ namespace Avalonia.Controls.Primitives protected Popup Popup => _popupLazy.Value; /// - /// Gets or sets the desired placement + /// Gets or sets the desired placement. /// public PlacementMode Placement { get => GetValue(PlacementProperty); set => SetValue(PlacementProperty, value); } + + /// + public PopupGravity PlacementGravity + { + get { return GetValue(PlacementGravityProperty); } + set { SetValue(PlacementGravityProperty, value); } + } + + /// + public PopupAnchor PlacementAnchor + { + get { return GetValue(PlacementAnchorProperty); } + set { SetValue(PlacementAnchorProperty, value); } + } + + /// + public double HorizontalOffset + { + get { return GetValue(HorizontalOffsetProperty); } + set { SetValue(HorizontalOffsetProperty, value); } + } + /// + public double VerticalOffset + { + get { return GetValue(VerticalOffsetProperty); } + set { SetValue(VerticalOffsetProperty, value); } + } + + /// /// Gets or sets the desired ShowMode /// @@ -379,6 +423,10 @@ namespace Avalonia.Controls.Primitives sz = Popup.Child.DesiredSize; } + Popup.VerticalOffset = VerticalOffset; + Popup.HorizontalOffset = HorizontalOffset; + Popup.PlacementAnchor = PlacementAnchor; + Popup.PlacementGravity = PlacementGravity; if (showAtPointer) { Popup.PlacementMode = PlacementMode.Pointer; diff --git a/src/Avalonia.Controls/SplitButton/SplitButton.cs b/src/Avalonia.Controls/SplitButton/SplitButton.cs index 896c8eae31..f5b361fa16 100644 --- a/src/Avalonia.Controls/SplitButton/SplitButton.cs +++ b/src/Avalonia.Controls/SplitButton/SplitButton.cs @@ -176,7 +176,7 @@ namespace Avalonia.Controls flyout.Opened += Flyout_Opened; flyout.Closed += Flyout_Closed; - _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(PopupFlyoutBase.PlacementProperty).Subscribe(Flyout_PlacementPropertyChanged); + _flyoutPropertyChangedDisposable = flyout.GetPropertyChangedObservable(Popup.PlacementModeProperty).Subscribe(Flyout_PlacementPropertyChanged); } } diff --git a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs b/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs index f9fb0d18ef..c3ebb1beaf 100644 --- a/src/Avalonia.Diagnostics/Diagnostics/ViewModels/VisualTreeNode.cs +++ b/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[] + new IObservable[] { 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. From d249848d7618f56468dd11522d944deb14012902 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Sun, 26 Feb 2023 12:58:18 -0500 Subject: [PATCH 05/10] Simplify PlacementMode.Center --- .../PopupPositioning/IPopupPositioner.cs | 46 ++++++------------- 1 file changed, 13 insertions(+), 33 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 3f1b8008b1..0c9bb89caa 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -464,25 +464,21 @@ namespace Avalonia.Controls.Primitives.PopupPositioning positionerParameters.Anchor = PopupAnchor.TopLeft; positionerParameters.Gravity = PopupGravity.BottomRight; } - else if (placement == PlacementMode.Center) + else { - // Start with top left position, and add offset to move it to the center - var anchorRectangle = GetAnchorRectangle(topLevel, target, rect); - positionerParameters.AnchorRectangle = anchorRectangle; - positionerParameters.Anchor = PopupAnchor.TopLeft; - positionerParameters.Gravity = PopupGravity.BottomRight; - - var targetSize = positionerParameters.Size; - if (targetSize != default) + if (target == null) + throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); + var matrix = target.TransformToVisual(topLevel); + if (matrix == null) { - positionerParameters.Offset += new Point( - (anchorRectangle.Width - targetSize.Width) / 2, - (anchorRectangle.Height - targetSize.Height) / 2); + if (target.GetVisualRoot() == null) + throw new InvalidOperationException("Target control is not attached to the visual tree"); + throw new InvalidOperationException("Target control is not in the same tree as the popup parent"); } - } - else - { - positionerParameters.AnchorRectangle = GetAnchorRectangle(topLevel, target, rect); + + var bounds = new Rect(default, target.Bounds.Size); + var anchorRect = rect ?? bounds; + positionerParameters.AnchorRectangle = anchorRect.Intersect(bounds).TransformToAABB(matrix.Value); var parameters = placement switch { @@ -490,6 +486,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning 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), @@ -532,23 +529,6 @@ namespace Avalonia.Controls.Primitives.PopupPositioning } } } - - private static Rect GetAnchorRectangle(TopLevel topLevel, Visual target, Rect? rect) - { - if (target == null) - throw new InvalidOperationException("Placement mode is not Pointer and PlacementTarget is null"); - var matrix = target.TransformToVisual(topLevel); - if (matrix == null) - { - if (target.GetVisualRoot() == null) - throw new InvalidOperationException("Target control is not attached to the visual tree"); - throw new InvalidOperationException("Target control is not in the same tree as the popup parent"); - } - - var bounds = new Rect(default, target.Bounds.Size); - var anchorRect = rect ?? bounds; - return anchorRect.Intersect(bounds).TransformToAABB(matrix.Value); - } } } From 584756043d19c6fe83cf8eff83d13e0d4d593230 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 27 Feb 2023 00:43:55 -0500 Subject: [PATCH 06/10] Try to fix last known position reset --- .../Input/PointerOverPreProcessor.cs | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index 7781255d89..7c1fdc7233 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -6,7 +6,8 @@ namespace Avalonia.Input internal class PointerOverPreProcessor : IObserver { 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,9 +42,9 @@ namespace Avalonia.Input } if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown - && _lastPointer is (var lastPointer, var lastPosition)) + && _currentPointer is (var lastPointer, var lastPosition)) { - _lastPointer = null; + _currentPointer = null; ClearPointerOver(lastPointer, args.Root, 0, PointToClient(args.Root, lastPosition), new PointerPointProperties(args.InputModifiers, args.Type.ToUpdateKind()), args.InputModifiers.ToKeyModifiers()); @@ -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; } @@ -122,7 +123,7 @@ namespace Avalonia.Input root.PointerOverElement = null; _lastActivePointerDevice = null; - _lastPointer = null; + _currentPointer = null; } private void ClearChildrenPointerOver(PointerEventArgs e, IInputElement element, bool clearRoot) @@ -164,7 +165,9 @@ namespace Avalonia.Input } } - _lastPointer = (pointer, ((Visual)root).PointToScreen(position)); + var screenPosition = ((Visual)root).PointToScreen(position); + _lastKnownPosition = screenPosition; + _currentPointer = (pointer, screenPosition); } private void SetPointerOverToElement(IPointer pointer, IInputRoot root, IInputElement element, From 6496abf14b2419948ad4bd87b5e299f2ab663d62 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Mon, 27 Feb 2023 00:44:15 -0500 Subject: [PATCH 07/10] Changes after review, added FlyoutPresenterTheme --- .../ControlCatalog/Pages/FlyoutsPage.axaml | 7 +++- src/Avalonia.Controls/Flyouts/Flyout.cs | 36 +++++++++++++++--- src/Avalonia.Controls/Flyouts/MenuFlyout.cs | 37 ++++++++++++++++--- .../Flyouts/PopupFlyoutBase.cs | 27 ++++++-------- 4 files changed, 80 insertions(+), 27 deletions(-) diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml b/samples/ControlCatalog/Pages/FlyoutsPage.axaml index 8dbc6d283f..35ece516bd 100644 --- a/samples/ControlCatalog/Pages/FlyoutsPage.axaml +++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml @@ -16,8 +16,13 @@ + + + + + - + diff --git a/src/Avalonia.Controls/Flyouts/Flyout.cs b/src/Avalonia.Controls/Flyouts/Flyout.cs index 262edbcc14..c0d3600c1e 100644 --- a/src/Avalonia.Controls/Flyouts/Flyout.cs +++ b/src/Avalonia.Controls/Flyouts/Flyout.cs @@ -1,5 +1,7 @@ -using Avalonia.Controls.Primitives; +using System.ComponentModel; +using Avalonia.Controls.Primitives; using Avalonia.Metadata; +using Avalonia.Styling; namespace Avalonia.Controls { @@ -18,6 +20,21 @@ namespace Avalonia.Controls private Classes? _classes; + /// + /// Defines the property. + /// + public static readonly StyledProperty FlyoutPresenterThemeProperty = + AvaloniaProperty.Register(nameof(FlyoutPresenterTheme)); + + /// + /// Gets or sets the that is applied to the container element generated for the flyout presenter. + /// + public ControlTheme? FlyoutPresenterTheme + { + get => GetValue(FlyoutPresenterThemeProperty); + set => SetValue(FlyoutPresenterThemeProperty, value); + } + /// /// Gets or sets the content to display in this flyout /// @@ -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); } } } diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index 79a6cdb313..df973debc7 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs +++ b/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; @@ -34,6 +35,12 @@ namespace Avalonia.Controls public static readonly StyledProperty ItemContainerThemeProperty = ItemsControl.ItemContainerThemeProperty.AddOwner(); + /// + /// Defines the property. + /// + public static readonly StyledProperty FlyoutPresenterThemeProperty = + Flyout.FlyoutPresenterThemeProperty.AddOwner(); + public Classes FlyoutPresenterClasses => _classes ??= new Classes(); /// @@ -60,10 +67,19 @@ namespace Avalonia.Controls /// public ControlTheme? ItemContainerTheme { - get { return GetValue(ItemContainerThemeProperty); } - set { SetValue(ItemContainerThemeProperty, value); } + get => GetValue(ItemContainerThemeProperty); + set => SetValue(ItemContainerThemeProperty, value); } + /// + /// Gets or sets the that is applied to the container element generated for the flyout presenter. + /// + 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); } } } diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs index 052143622c..f5eb8f4a02 100644 --- a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -37,9 +37,8 @@ namespace Avalonia.Controls.Primitives /// /// Defines the property /// - public static readonly DirectProperty ShowModeProperty = - AvaloniaProperty.RegisterDirect(nameof(ShowMode), - x => x.ShowMode, (x, v) => x.ShowMode = v); + public static readonly StyledProperty ShowModeProperty = + AvaloniaProperty.Register(nameof(ShowMode)); /// /// Defines the property @@ -50,7 +49,6 @@ namespace Avalonia.Controls.Primitives (o, v) => o._overlayInputPassThroughElement = v); private readonly Lazy _popupLazy; - private FlyoutShowMode _showMode = FlyoutShowMode.Standard; private Rect? _enlargedPopupRect; private PixelRect? _enlargePopupRectScreenPixelRect; private IDisposable? _transientDisposable; @@ -81,39 +79,38 @@ namespace Avalonia.Controls.Primitives /// public PopupGravity PlacementGravity { - get { return GetValue(PlacementGravityProperty); } - set { SetValue(PlacementGravityProperty, value); } + get => GetValue(PlacementGravityProperty); + set => SetValue(PlacementGravityProperty, value); } /// public PopupAnchor PlacementAnchor { - get { return GetValue(PlacementAnchorProperty); } - set { SetValue(PlacementAnchorProperty, value); } + get => GetValue(PlacementAnchorProperty); + set => SetValue(PlacementAnchorProperty, value); } /// public double HorizontalOffset { - get { return GetValue(HorizontalOffsetProperty); } - set { SetValue(HorizontalOffsetProperty, value); } + get => GetValue(HorizontalOffsetProperty); + set => SetValue(HorizontalOffsetProperty, value); } /// public double VerticalOffset { - get { return GetValue(VerticalOffsetProperty); } - set { SetValue(VerticalOffsetProperty, value); } + get => GetValue(VerticalOffsetProperty); + set => SetValue(VerticalOffsetProperty, value); } - /// /// Gets or sets the desired ShowMode /// public FlyoutShowMode ShowMode { - get => _showMode; - set => SetAndRaise(ShowModeProperty, ref _showMode, value); + get => GetValue(ShowModeProperty); + set => SetValue(ShowModeProperty, value); } /// From 67f52011816eb3d3929491ebd44209cbc6af8c81 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 7 Mar 2023 12:36:20 +0900 Subject: [PATCH 08/10] Fix tests --- src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs index d827251cf3..a3d05a34b7 100644 --- a/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/PopupFlyoutBase.cs @@ -225,7 +225,7 @@ namespace Avalonia.Controls.Primitives { Popup.PlacementTarget = Target = placementTarget; ((ISetLogicalParent)Popup).SetParent(placementTarget); - Popup.SetValue(StyledElement.TemplatedParentProperty, placementTarget.TemplatedParent); + Popup.TemplatedParent = placementTarget.TemplatedParent; } if (Popup.Child == null) From 7452980c39165e545798b97d8351850a8554fbcd Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 7 Mar 2023 14:32:25 +0900 Subject: [PATCH 09/10] Move _lastKnownPosition --- src/Avalonia.Base/Input/PointerOverPreProcessor.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index 7c1fdc7233..3967c5abe0 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -152,6 +152,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) { @@ -165,8 +167,6 @@ namespace Avalonia.Input } } - var screenPosition = ((Visual)root).PointToScreen(position); - _lastKnownPosition = screenPosition; _currentPointer = (pointer, screenPosition); } From 2473bb8408e2f8882beecbd0b23ab354551e19c8 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Tue, 7 Mar 2023 14:32:31 +0900 Subject: [PATCH 10/10] Fix some warnings --- src/Avalonia.Base/Input/MouseDevice.cs | 1 + src/Avalonia.Base/Input/PenDevice.cs | 3 ++- src/Avalonia.Base/Input/PointerEventArgs.cs | 2 ++ src/Avalonia.Base/Input/PointerOverPreProcessor.cs | 14 +++++++++----- src/Avalonia.Base/Input/TouchDevice.cs | 1 + 5 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Base/Input/MouseDevice.cs b/src/Avalonia.Base/Input/MouseDevice.cs index 50980f1c3d..44412cd152 100644 --- a/src/Avalonia.Base/Input/MouseDevice.cs +++ b/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 { diff --git a/src/Avalonia.Base/Input/PenDevice.cs b/src/Avalonia.Base/Input/PenDevice.cs index 285249a5f8..b3cd39212b 100644 --- a/src/Avalonia.Base/Input/PenDevice.cs +++ b/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; diff --git a/src/Avalonia.Base/Input/PointerEventArgs.cs b/src/Avalonia.Base/Input/PointerEventArgs.cs index 50d7cc5dc5..28a3c3aefb 100644 --- a/src/Avalonia.Base/Input/PointerEventArgs.cs +++ b/src/Avalonia.Base/Input/PointerEventArgs.cs @@ -42,7 +42,9 @@ namespace Avalonia.Input PointerPointProperties properties, KeyModifiers modifiers, Lazy?>? previousPoints) +#pragma warning disable CS0618 : this(routedEvent, source, pointer, rootVisual, rootVisualPosition, timestamp, properties, modifiers) +#pragma warning restore CS0618 { _previousPoints = previousPoints; } diff --git a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs index 3967c5abe0..0afdb8e080 100644 --- a/src/Avalonia.Base/Input/PointerOverPreProcessor.cs +++ b/src/Avalonia.Base/Input/PointerOverPreProcessor.cs @@ -42,14 +42,14 @@ namespace Avalonia.Input } if (args.Type is RawPointerEventType.LeaveWindow or RawPointerEventType.NonClientLeftButtonDown - && _currentPointer is (var lastPointer, var lastPosition)) + && _currentPointer is var (lastPointer, lastPosition)) { _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; @@ -101,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) { @@ -130,11 +132,11 @@ namespace Avalonia.Input { 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; } } @@ -189,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); diff --git a/src/Avalonia.Base/Input/TouchDevice.cs b/src/Avalonia.Base/Input/TouchDevice.cs index 125d5fc813..bab1b9f784 100644 --- a/src/Avalonia.Base/Input/TouchDevice.cs +++ b/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 {