From a71910f92866ea1577b4ff0d36dac77fc4f18d5e Mon Sep 17 00:00:00 2001 From: amwx Date: Wed, 17 Mar 2021 21:43:45 -0500 Subject: [PATCH 01/43] Initial Flyout Impl --- src/Avalonia.Controls/Flyouts/Flyout.cs | 65 ++++ src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 315 ++++++++++++++++++ .../Flyouts/FlyoutPlacementMode.cs | 80 +++++ .../Flyouts/FlyoutPresenter.cs | 10 + .../Controls/FlyoutPresenter.xaml | 39 +++ 5 files changed, 509 insertions(+) create mode 100644 src/Avalonia.Controls/Flyouts/Flyout.cs create mode 100644 src/Avalonia.Controls/Flyouts/FlyoutBase.cs create mode 100644 src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs create mode 100644 src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs create mode 100644 src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml diff --git a/src/Avalonia.Controls/Flyouts/Flyout.cs b/src/Avalonia.Controls/Flyouts/Flyout.cs new file mode 100644 index 0000000000..723a5e84f8 --- /dev/null +++ b/src/Avalonia.Controls/Flyouts/Flyout.cs @@ -0,0 +1,65 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Text; +using Avalonia.Controls.Primitives; +using Avalonia.Styling; + +#nullable enable + +namespace Avalonia.Controls +{ + public class Flyout : FlyoutBase + { + public static readonly StyledProperty ContentProperty = + AvaloniaProperty.Register(nameof(Content)); + + public Styles? FlyoutPresenterStyle + { + get + { + if (_styles == null) + { + _styles = new Styles(); + _styles.CollectionChanged += OnFlyoutPresenterStylesChanged; + } + + return _styles; + } + } + + private Styles? _styles; + private bool _stylesDirty; + + public object Content + { + get => GetValue(ContentProperty); + set => SetValue(ContentProperty, value); + } + + protected override Control CreatePresenter() + { + return new FlyoutPresenter + { + [!ContentControl.ContentProperty] = this[!ContentProperty] + }; + } + + protected override void OnOpened() + { + if (_styles != null && _stylesDirty) + { + // Presenter for flyout generally shouldn't be public, so + // we should be ok to just reset the styles + _popup.Child.Styles.Clear(); + _popup.Child.Styles.Add(_styles); + } + base.OnOpened(); + } + + private void OnFlyoutPresenterStylesChanged(object sender, NotifyCollectionChangedEventArgs e) + { + _stylesDirty = true; + } + } +} diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs new file mode 100644 index 0000000000..64ab07a658 --- /dev/null +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -0,0 +1,315 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Text; +using Avalonia.Layout; + +#nullable enable + +namespace Avalonia.Controls.Primitives +{ + public abstract class FlyoutBase : AvaloniaObject + { + private static readonly DirectProperty IsOpenProperty = + AvaloniaProperty.RegisterDirect(nameof(IsOpen), + x => x.IsOpen); + + public static readonly DirectProperty TargetProperty = + AvaloniaProperty.RegisterDirect(nameof(Target), x => x.Target); + + public static readonly DirectProperty PlacementProperty = + AvaloniaProperty.RegisterDirect(nameof(Placement), + x => x.Placement, (x, v) => x.Placement = v); + + public static readonly AttachedProperty AttachedFlyoutProperty = + AvaloniaProperty.RegisterAttached("AttachedFlyout", null); + + private bool _isOpen; + private Control? _target; + protected Popup? _popup; + + public bool IsOpen + { + get => _isOpen; + private set => SetAndRaise(IsOpenProperty, ref _isOpen, value); + } + + public FlyoutPlacementMode Placement + { + get => GetValue(PlacementProperty); + set => SetValue(PlacementProperty, value); + } + + public Control? Target + { + get => _target; + private set => SetAndRaise(TargetProperty, ref _target, 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); + } + + public static void SetAttachedFlyout(Control element, FlyoutBase? value) + { + element.SetValue(AttachedFlyoutProperty, value); + } + + public static void ShowAttachedFlyout(Control flyoutOwner) + { + var flyout = GetAttachedFlyout(flyoutOwner); + flyout?.ShowAt(flyoutOwner); + } + + public void ShowAt(Control placementTarget) + { + ShowAtCore(placementTarget); + } + + public void ShowAt(Control placementTarget, bool showAtPointer) + { + ShowAtCore(placementTarget, showAtPointer); + } + + public void Hide(bool canCancel = true) + { + if (!IsOpen) + { + return; + } + + if (canCancel) + { + bool cancel = false; + + var closing = new CancelEventArgs(); + Closing?.Invoke(this, closing); + if (cancel || closing.Cancel) + { + return; + } + } + + IsOpen = _popup.IsOpen = false; + + OnClosed(); + } + + protected virtual void ShowAtCore(Control placementTarget, bool showAtPointer = false) + { + if (placementTarget == null) + throw new ArgumentNullException("placementTarget cannot be null"); + + if (_popup == null) + { + InitPopup(); + } + + if (IsOpen) + { + if (placementTarget == Target) + { + return; + } + else // Close before opening a new one + { + Hide(false); + } + } + + if (_popup.Parent != null && _popup.Parent != placementTarget) + { + ((ISetLogicalParent)_popup).SetParent(null); + } + + _popup.PlacementTarget = Target = placementTarget; + + ((ISetLogicalParent)_popup).SetParent(placementTarget); + + if (_popup.Child == null) + { + _popup.Child = CreatePresenter(); + } + + OnOpening(); + IsOpen = _popup.IsOpen = true; + PositionPopup(showAtPointer); + OnOpened(); + } + + protected virtual void OnOpening() + { + Opening?.Invoke(this, null); + } + + protected virtual void OnOpened() + { + Opened?.Invoke(this, null); + } + + protected virtual void OnClosing(CancelEventArgs args) + { + Closing?.Invoke(this, args); + } + + protected virtual void OnClosed() + { + Closed?.Invoke(this, null); + } + + protected abstract Control CreatePresenter(); + + private void InitPopup() + { + _popup = new Popup(); + _popup.WindowManagerAddShadowHint = false; + _popup.IsLightDismissEnabled = true; + + _popup.Opened += OnPopupOpened; + _popup.Closed += OnPopupClosed; + } + + private void OnPopupOpened(object sender, EventArgs e) + { + IsOpen = true; + OnOpened(); + } + + private void OnPopupClosed(object sender, EventArgs e) + { + Hide(); + } + + private void PositionPopup(bool showAtPointer) + { + Size sz; + if(_popup.DesiredSize == Size.Empty) + { + sz = LayoutHelper.MeasureChild(_popup, Size.Infinity, new Thickness()); + } + else + { + sz = _popup.DesiredSize; + } + + if (showAtPointer) + { + _popup.PlacementMode = PlacementMode.Pointer; + } + else + { + _popup.PlacementMode = PlacementMode.AnchorAndGravity; + _popup.PlacementConstraintAdjustment = + PopupPositioning.PopupPositionerConstraintAdjustment.SlideX | + PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; + } + + + var trgtBnds = Target?.Bounds ?? Rect.Empty; + + switch (Placement) + { + case FlyoutPlacementMode.Top: //Above & centered + _popup.PlacementRect = new Rect(-sz.Width / 2, 0, sz.Width, 1); + _popup.PlacementGravity = PopupPositioning.PopupGravity.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; + + case FlyoutPlacementMode.Full: + //Not sure how the get this to work + //Popup should display at max size in the middle of the VisualRoot/Window of the Target + throw new NotSupportedException("FlyoutPlacementMode.Full is not supported at this time"); + //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; + } + } + } +} diff --git a/src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs b/src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs new file mode 100644 index 0000000000..15987daa50 --- /dev/null +++ b/src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs @@ -0,0 +1,80 @@ +using System; +using System.Collections.Generic; +using System.Text; + +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, + + /// + /// 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/Flyouts/FlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs new file mode 100644 index 0000000000..80477d6969 --- /dev/null +++ b/src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs @@ -0,0 +1,10 @@ +using System; +using System.Collections.Generic; +using System.Text; + +namespace Avalonia.Controls +{ + public class FlyoutPresenter : ContentControl + { + } +} diff --git a/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml b/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml new file mode 100644 index 0000000000..2be49d007b --- /dev/null +++ b/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml @@ -0,0 +1,39 @@ + + + + From 5b270a31421ca57f269facf7114343edcc7759e5 Mon Sep 17 00:00:00 2001 From: amwx Date: Wed, 17 Mar 2021 21:49:22 -0500 Subject: [PATCH 02/43] Add Flyout property to Button --- src/Avalonia.Controls/Button.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/Avalonia.Controls/Button.cs b/src/Avalonia.Controls/Button.cs index c779e4b0cb..121087d262 100644 --- a/src/Avalonia.Controls/Button.cs +++ b/src/Avalonia.Controls/Button.cs @@ -2,6 +2,7 @@ using System; using System.Linq; using System.Windows.Input; using Avalonia.Controls.Metadata; +using Avalonia.Controls.Primitives; using Avalonia.Data; using Avalonia.Input; using Avalonia.Interactivity; @@ -78,9 +79,14 @@ namespace Avalonia.Controls public static readonly StyledProperty IsPressedProperty = AvaloniaProperty.Register(nameof(IsPressed)); + public static readonly DirectProperty FlyoutProperty = + AvaloniaProperty.RegisterDirect(nameof(Flyout), + x => x.Flyout, (x, v) => x.Flyout = v); + private ICommand _command; private bool _commandCanExecute = true; private KeyGesture _hotkey; + private FlyoutBase _flyout; /// /// Initializes static members of the class. @@ -169,6 +175,12 @@ namespace Avalonia.Controls private set { SetValue(IsPressedProperty, value); } } + public FlyoutBase Flyout + { + get => _flyout; + set => SetAndRaise(FlyoutProperty, ref _flyout, value); + } + protected override bool IsEnabledCore => base.IsEnabledCore && _commandCanExecute; /// @@ -278,6 +290,8 @@ namespace Avalonia.Controls /// protected virtual void OnClick() { + OpenFlyout(); + var e = new RoutedEventArgs(ClickEvent); RaiseEvent(e); @@ -288,6 +302,11 @@ namespace Avalonia.Controls } } + protected virtual void OpenFlyout() + { + _flyout?.ShowAt(this); + } + /// protected override void OnPointerPressed(PointerPressedEventArgs e) { From a1583ef3e87b38cf9a169d0daa15cc8eb9178477 Mon Sep 17 00:00:00 2001 From: amwx Date: Thu, 18 Mar 2021 00:13:07 -0500 Subject: [PATCH 03/43] Couple fixes --- src/Avalonia.Controls/Flyouts/Flyout.cs | 2 + src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 43 ++++++++------------- 2 files changed, 18 insertions(+), 27 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/Flyout.cs b/src/Avalonia.Controls/Flyouts/Flyout.cs index 723a5e84f8..ed16628605 100644 --- a/src/Avalonia.Controls/Flyouts/Flyout.cs +++ b/src/Avalonia.Controls/Flyouts/Flyout.cs @@ -3,6 +3,7 @@ using System.Collections.Generic; using System.Collections.Specialized; using System.Text; using Avalonia.Controls.Primitives; +using Avalonia.Metadata; using Avalonia.Styling; #nullable enable @@ -31,6 +32,7 @@ namespace Avalonia.Controls private Styles? _styles; private bool _stylesDirty; + [Content] public object Content { get => GetValue(ContentProperty); diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 64ab07a658..ca267dc5e9 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -17,9 +17,8 @@ namespace Avalonia.Controls.Primitives public static readonly DirectProperty TargetProperty = AvaloniaProperty.RegisterDirect(nameof(Target), x => x.Target); - public static readonly DirectProperty PlacementProperty = - AvaloniaProperty.RegisterDirect(nameof(Placement), - x => x.Placement, (x, v) => x.Placement = v); + public static readonly StyledProperty PlacementProperty = + AvaloniaProperty.Register(nameof(Placement)); public static readonly AttachedProperty AttachedFlyoutProperty = AvaloniaProperty.RegisterAttached("AttachedFlyout", null); @@ -128,9 +127,11 @@ namespace Avalonia.Controls.Primitives ((ISetLogicalParent)_popup).SetParent(null); } - _popup.PlacementTarget = Target = placementTarget; - - ((ISetLogicalParent)_popup).SetParent(placementTarget); + if (_popup.PlacementTarget != placementTarget) + { + _popup.PlacementTarget = Target = placementTarget; + ((ISetLogicalParent)_popup).SetParent(placementTarget); + } if (_popup.Child == null) { @@ -138,8 +139,8 @@ namespace Avalonia.Controls.Primitives } OnOpening(); - IsOpen = _popup.IsOpen = true; PositionPopup(showAtPointer); + IsOpen = _popup.IsOpen = true; OnOpened(); } @@ -189,13 +190,14 @@ namespace Avalonia.Controls.Primitives private void PositionPopup(bool showAtPointer) { Size sz; - if(_popup.DesiredSize == Size.Empty) + if(_popup.Child.DesiredSize == Size.Empty) { - sz = LayoutHelper.MeasureChild(_popup, Size.Infinity, new Thickness()); + // Popup may not have been shown yet. Measure content + sz = LayoutHelper.MeasureChild(_popup.Child, Size.Infinity, new Thickness()); } else { - sz = _popup.DesiredSize; + sz = _popup.Child.DesiredSize; } if (showAtPointer) @@ -210,90 +212,78 @@ namespace Avalonia.Controls.Primitives PopupPositioning.PopupPositionerConstraintAdjustment.SlideY; } - var trgtBnds = Target?.Bounds ?? Rect.Empty; switch (Placement) { case FlyoutPlacementMode.Top: //Above & centered - _popup.PlacementRect = new Rect(-sz.Width / 2, 0, sz.Width, 1); + _popup.PlacementRect = new Rect(0, 0, trgtBnds.Width-1, 1); _popup.PlacementGravity = PopupPositioning.PopupGravity.Top; - + _popup.PlacementAnchor = PopupPositioning.PopupAnchor.Top; break; case FlyoutPlacementMode.TopEdgeAlignedLeft: _popup.PlacementRect = new Rect(0, 0, 0, 0); - _popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight; - + _popup.PlacementGravity = PopupPositioning.PopupGravity.TopRight; break; case FlyoutPlacementMode.TopEdgeAlignedRight: _popup.PlacementRect = new Rect(trgtBnds.Width - 1, 0, 10, 1); - _popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft; - + _popup.PlacementGravity = PopupPositioning.PopupGravity.TopLeft; break; case FlyoutPlacementMode.RightEdgeAlignedTop: _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; case FlyoutPlacementMode.Full: @@ -307,7 +297,6 @@ namespace Avalonia.Controls.Primitives //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; } } From be0cdd157b343b42ca9746f47e9337dd7bc3a20c Mon Sep 17 00:00:00 2001 From: amwx Date: Thu, 18 Mar 2021 21:27:22 -0500 Subject: [PATCH 04/43] Add ShowMode to FlyoutBase --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 89 ++++++++++++++++++- .../Flyouts/FlyoutShowMode.cs | 24 +++++ 2 files changed, 110 insertions(+), 3 deletions(-) create mode 100644 src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index ca267dc5e9..85e460a25f 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -1,7 +1,7 @@ using System; -using System.Collections.Generic; using System.ComponentModel; -using System.Text; +using Avalonia.Input; +using Avalonia.Input.Raw; using Avalonia.Layout; #nullable enable @@ -20,12 +20,19 @@ namespace Avalonia.Controls.Primitives public static readonly StyledProperty PlacementProperty = AvaloniaProperty.Register(nameof(Placement)); + public static readonly DirectProperty ShowModeProperty = + AvaloniaProperty.RegisterDirect(nameof(ShowMode), + x => x.ShowMode, (x, v) => x.ShowMode = v); + public static readonly AttachedProperty AttachedFlyoutProperty = AvaloniaProperty.RegisterAttached("AttachedFlyout", null); private bool _isOpen; private Control? _target; protected Popup? _popup; + private FlyoutShowMode _showMode = FlyoutShowMode.Standard; + Rect? enlargedPopupRect; + IDisposable? transientDisposable; public bool IsOpen { @@ -39,6 +46,12 @@ namespace Avalonia.Controls.Primitives set => SetValue(PlacementProperty, value); } + public FlyoutShowMode ShowMode + { + get => _showMode; + set => SetAndRaise(ShowModeProperty, ref _showMode, value); + } + public Control? Target { get => _target; @@ -96,7 +109,11 @@ namespace Avalonia.Controls.Primitives } IsOpen = _popup.IsOpen = false; - + + // Ensure this isn't active + transientDisposable?.Dispose(); + transientDisposable = null; + OnClosed(); } @@ -142,6 +159,72 @@ namespace Avalonia.Controls.Primitives PositionPopup(showAtPointer); IsOpen = _popup.IsOpen = true; OnOpened(); + + 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); + } + } + + private void HandleTransientDismiss(RawInputEventArgs args) + { + if (args is RawPointerEventArgs pArgs && pArgs.Type == RawPointerEventType.Move) + { + if (enlargedPopupRect == null) + { + if (_popup?.Host is PopupRoot root) + { + var tmp = root.Bounds.Inflate(100); + var scPt = root.PointToScreen(tmp.TopLeft); + enlargedPopupRect = new Rect(scPt.X, scPt.Y, tmp.Width, tmp.Height); + } + else if (_popup?.Host is OverlayPopupHost host) + { + // Overlay popups are in Window client coordinates, just use that + enlargedPopupRect = host.Bounds.Inflate(100); + } + + return; + } + + if (_popup?.Host is PopupRoot) + { + var pt = pArgs.Root.PointToScreen(pArgs.Position); + if (!enlargedPopupRect?.Contains(new Point(pt.X, pt.Y)) ?? false) + { + Hide(false); + enlargedPopupRect = null; + transientDisposable?.Dispose(); + transientDisposable = null; + } + } + else if (_popup?.Host is OverlayPopupHost) + { + if (!enlargedPopupRect?.Contains(pArgs.Position) ?? false) + { + Hide(false); + enlargedPopupRect = null; + transientDisposable?.Dispose(); + transientDisposable = null; + } + } + } } protected virtual void OnOpening() diff --git a/src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs b/src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs new file mode 100644 index 0000000000..cb5b5f00c0 --- /dev/null +++ b/src/Avalonia.Controls/Flyouts/FlyoutShowMode.cs @@ -0,0 +1,24 @@ +namespace Avalonia.Controls +{ + // Note: FlyoutShowMode.Auto was removed. MS Docs just say: + // The show mode is determined automatically based on the method used to show the flyout. + // and AFAICT Flyouts generally open with "Standard" behavior + + public enum FlyoutShowMode + { + /// + /// Behavior is typical of a flyout shown reactively, like a context menu. The open flyout takes focus. For a CommandBarFlyout, it opens in it's expanded state. + /// + Standard, + + /// + /// Behavior is typical of a flyout shown proactively. The open flyout does not take focus. For a CommandBarFlyout, it opens in it's collapsed state. + /// + Transient, + + /// + /// The flyout exhibits Transient behavior while the cursor is close to it, but is dismissed when the cursor moves away. + /// + TransientWithDismissOnPointerMoveAway + } +} From 4bd187d362ada2fe60b8010fb1525c47e5173bae Mon Sep 17 00:00:00 2001 From: amwx Date: Thu, 18 Mar 2021 21:28:40 -0500 Subject: [PATCH 05/43] Add MenuFlyout --- src/Avalonia.Controls/Flyouts/MenuFlyout.cs | 87 +++++++++++++++++++ .../Flyouts/MenuFlyoutPresenter.cs | 36 ++++++++ .../Accents/FluentControlResourcesDark.xaml | 5 ++ .../Accents/FluentControlResourcesLight.xaml | 5 ++ .../Controls/FluentControls.xaml | 2 + .../Controls/FlyoutPresenter.xaml | 5 ++ .../Controls/MenuFlyoutPresenter.xaml | 36 ++++++++ 7 files changed, 176 insertions(+) create mode 100644 src/Avalonia.Controls/Flyouts/MenuFlyout.cs create mode 100644 src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs create mode 100644 src/Avalonia.Themes.Fluent/Controls/MenuFlyoutPresenter.xaml diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs new file mode 100644 index 0000000000..8eea5f211d --- /dev/null +++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs @@ -0,0 +1,87 @@ +using System.Collections; +using System.Collections.Specialized; +using Avalonia.Collections; +using Avalonia.Controls.Primitives; +using Avalonia.Controls.Templates; +using Avalonia.Input; +using Avalonia.Metadata; +using Avalonia.Styling; + +#nullable enable + +namespace Avalonia.Controls +{ + public class MenuFlyout : FlyoutBase + { + public MenuFlyout() + { + _items = new AvaloniaList(); + } + + public static readonly DirectProperty ItemsProperty = + ItemsControl.ItemsProperty.AddOwner(x => x.Items, + (x, v) => x.Items = v); + + public static readonly DirectProperty ItemTemplateProperty = + AvaloniaProperty.RegisterDirect(nameof(ItemTemplate), + x => x.ItemTemplate, (x, v) => x.ItemTemplate = v); + + public Styles? FlyoutPresenterStyle + { + get + { + if (_styles == null) + { + _styles = new Styles(); + _styles.CollectionChanged += OnMenuFlyoutPresenterStyleChanged; + } + + return _styles; + } + } + + [Content] + public IEnumerable Items + { + get => _items; + set => SetAndRaise(ItemsProperty, ref _items, value); + } + + public IDataTemplate? ItemTemplate + { + get => _itemTemplate; + set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value); + } + + private Styles? _styles; + private bool _stylesDirty = true; + private IEnumerable _items; + private IDataTemplate? _itemTemplate; + + protected override Control CreatePresenter() + { + return new MenuFlyoutPresenter + { + [!ItemsControl.ItemsProperty] = this[!ItemsProperty], + [!ItemsControl.ItemTemplateProperty] = this[!ItemTemplateProperty] + }; + } + + protected override void OnOpened() + { + if (_styles != null && _stylesDirty) + { + // Presenter for flyout generally shouldn't be public, so + // we should be ok to just reset the styles + _popup.Child.Styles.Clear(); + _popup.Child.Styles.Add(_styles); + } + base.OnOpened(); + } + + private void OnMenuFlyoutPresenterStyleChanged(object sender, NotifyCollectionChangedEventArgs e) + { + _stylesDirty = true; + } + } +} diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs new file mode 100644 index 0000000000..7e59d824fa --- /dev/null +++ b/src/Avalonia.Controls/Flyouts/MenuFlyoutPresenter.cs @@ -0,0 +1,36 @@ +using Avalonia.Controls.Generators; +using Avalonia.Controls.Platform; +using Avalonia.Controls.Primitives; +using Avalonia.LogicalTree; + +namespace Avalonia.Controls +{ + public class MenuFlyoutPresenter : MenuBase + { + public MenuFlyoutPresenter() + :base(new DefaultMenuInteractionHandler(true)) + { + + } + + public override void Close() + { + // DefaultMenuInteractionHandler calls this + var host = this.FindLogicalAncestorOfType(); + if (host != null) + { + host.IsOpen = false; + } + } + + public override void Open() + { + //Ignore + } + + protected override IItemContainerGenerator CreateItemContainerGenerator() + { + return new MenuItemContainerGenerator(this); + } + } +} diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index abc02a6371..ae1fe42031 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -809,5 +809,10 @@ 1 + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 6a2a04f732..5af24f57a8 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -807,5 +807,10 @@ 1 + + + + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml index 0acb8f4f6e..16e05ffdfd 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FluentControls.xaml @@ -59,4 +59,6 @@ + + diff --git a/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml b/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml index 2be49d007b..3a8df060fa 100644 --- a/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml @@ -1,4 +1,9 @@ + + 1 + 0 + + + + From b1b9f6f7d2ca93a995a04c583f33e72e53e0a321 Mon Sep 17 00:00:00 2001 From: amwx Date: Thu, 18 Mar 2021 22:39:10 -0500 Subject: [PATCH 06/43] ContextFlyouts --- src/Avalonia.Controls/Control.cs | 17 ++++++++++ src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 36 +++++++++++++++++++++ 2 files changed, 53 insertions(+) diff --git a/src/Avalonia.Controls/Control.cs b/src/Avalonia.Controls/Control.cs index 4aab92c428..ebfe98a175 100644 --- a/src/Avalonia.Controls/Control.cs +++ b/src/Avalonia.Controls/Control.cs @@ -37,9 +37,16 @@ namespace Avalonia.Controls /// /// Defines the property. /// + [Obsolete("Prefer ContextFlyout")] public static readonly StyledProperty ContextMenuProperty = AvaloniaProperty.Register(nameof(ContextMenu)); + /// + /// Defines the property + /// + public static readonly StyledProperty ContextFlyoutProperty = + AvaloniaProperty.Register(nameof(ContextFlyout)); + /// /// Event raised when an element wishes to be scrolled into view. /// @@ -70,12 +77,22 @@ namespace Avalonia.Controls /// /// Gets or sets a context menu to the control. /// + [Obsolete("Prefer ContextFlyout")] public ContextMenu? ContextMenu { get => GetValue(ContextMenuProperty); set => SetValue(ContextMenuProperty, value); } + /// + /// Gets or sets a context flyout to the control + /// + public FlyoutBase? ContextFlyout + { + get => GetValue(ContextFlyoutProperty); + set => SetValue(ContextFlyoutProperty, value); + } + /// /// Gets or sets a user-defined object attached to the control. /// diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index 85e460a25f..f3f4febbb0 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -10,6 +10,11 @@ namespace Avalonia.Controls.Primitives { public abstract class FlyoutBase : AvaloniaObject { + static FlyoutBase() + { + Control.ContextFlyoutProperty.Changed.Subscribe(OnContextFlyoutPropertyChanged); + } + private static readonly DirectProperty IsOpenProperty = AvaloniaProperty.RegisterDirect(nameof(IsOpen), x => x.IsOpen); @@ -383,5 +388,36 @@ namespace Avalonia.Controls.Primitives break; } } + + private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args) + { + if (args.Sender is Control c) + { + if (args.OldValue.GetValueOrDefault() is FlyoutBase) + { + c.PointerReleased -= OnControlWithContextFlyoutPointerReleased; + } + if (args.NewValue.GetValueOrDefault() is FlyoutBase) + { + c.PointerReleased += OnControlWithContextFlyoutPointerReleased; + } + } + } + + private static void OnControlWithContextFlyoutPointerReleased(object sender, PointerReleasedEventArgs e) + { + if (sender is Control c) + { + if (e.InitialPressMouseButton == MouseButton.Right && + e.GetCurrentPoint(c).Properties.PointerUpdateKind == PointerUpdateKind.RightButtonReleased) + { + if (c.ContextFlyout != null) + { + c.ContextFlyout.ShowAt(c, true); + } + } + } + + } } } From 780dd1bc74ca87113cce57295a7ef343c25eed78 Mon Sep 17 00:00:00 2001 From: amwx Date: Fri, 19 Mar 2021 17:23:35 -0500 Subject: [PATCH 07/43] Show ContextFlyout on right click --- src/Avalonia.Controls/Flyouts/FlyoutBase.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs index f3f4febbb0..5f00c1eafe 100644 --- a/src/Avalonia.Controls/Flyouts/FlyoutBase.cs +++ b/src/Avalonia.Controls/Flyouts/FlyoutBase.cs @@ -389,15 +389,15 @@ namespace Avalonia.Controls.Primitives } } - private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args) + private static void OnContextFlyoutPropertyChanged(AvaloniaPropertyChangedEventArgs args) { if (args.Sender is Control c) { - if (args.OldValue.GetValueOrDefault() is FlyoutBase) + if (args.OldValue is FlyoutBase) { c.PointerReleased -= OnControlWithContextFlyoutPointerReleased; } - if (args.NewValue.GetValueOrDefault() is FlyoutBase) + if (args.NewValue is FlyoutBase) { c.PointerReleased += OnControlWithContextFlyoutPointerReleased; } From a7472af1305a5be4b62957db6a8e2ef629512960 Mon Sep 17 00:00:00 2001 From: amwx Date: Fri, 19 Mar 2021 17:24:16 -0500 Subject: [PATCH 08/43] Use Classes instead of Styles --- src/Avalonia.Controls/Flyouts/Flyout.cs | 39 ++++++++------------- src/Avalonia.Controls/Flyouts/MenuFlyout.cs | 39 ++++++++------------- 2 files changed, 28 insertions(+), 50 deletions(-) diff --git a/src/Avalonia.Controls/Flyouts/Flyout.cs b/src/Avalonia.Controls/Flyouts/Flyout.cs index ed16628605..ff6f78a6f4 100644 --- a/src/Avalonia.Controls/Flyouts/Flyout.cs +++ b/src/Avalonia.Controls/Flyouts/Flyout.cs @@ -15,22 +15,9 @@ namespace Avalonia.Controls public static readonly StyledProperty ContentProperty = AvaloniaProperty.Register(nameof(Content)); - public Styles? FlyoutPresenterStyle - { - get - { - if (_styles == null) - { - _styles = new Styles(); - _styles.CollectionChanged += OnFlyoutPresenterStylesChanged; - } - - return _styles; - } - } + public Classes? FlyoutPresenterClasses => _classes ??= new Classes(); - private Styles? _styles; - private bool _stylesDirty; + private Classes? _classes; [Content] public object Content @@ -49,19 +36,21 @@ namespace Avalonia.Controls protected override void OnOpened() { - if (_styles != null && _stylesDirty) + if (FlyoutPresenterClasses != null) { - // Presenter for flyout generally shouldn't be public, so - // we should be ok to just reset the styles - _popup.Child.Styles.Clear(); - _popup.Child.Styles.Add(_styles); + //Remove any classes no longer in use + for (int i = _popup.Child.Classes.Count - 1; i >= 0; i--) + { + if (!FlyoutPresenterClasses.Contains(_popup.Child.Classes[i])) + { + _popup.Child.Classes.RemoveAt(i); + } + } + + //Add new classes + _popup.Child.Classes.AddRange(FlyoutPresenterClasses); } base.OnOpened(); } - - private void OnFlyoutPresenterStylesChanged(object sender, NotifyCollectionChangedEventArgs e) - { - _stylesDirty = true; - } } } diff --git a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs index 8eea5f211d..4cea5199f1 100644 --- a/src/Avalonia.Controls/Flyouts/MenuFlyout.cs +++ b/src/Avalonia.Controls/Flyouts/MenuFlyout.cs @@ -26,19 +26,7 @@ namespace Avalonia.Controls AvaloniaProperty.RegisterDirect(nameof(ItemTemplate), x => x.ItemTemplate, (x, v) => x.ItemTemplate = v); - public Styles? FlyoutPresenterStyle - { - get - { - if (_styles == null) - { - _styles = new Styles(); - _styles.CollectionChanged += OnMenuFlyoutPresenterStyleChanged; - } - - return _styles; - } - } + public Classes? FlyoutPresenterClasses => _classes ??= new Classes(); [Content] public IEnumerable Items @@ -53,8 +41,7 @@ namespace Avalonia.Controls set => SetAndRaise(ItemTemplateProperty, ref _itemTemplate, value); } - private Styles? _styles; - private bool _stylesDirty = true; + private Classes? _classes; private IEnumerable _items; private IDataTemplate? _itemTemplate; @@ -69,19 +56,21 @@ namespace Avalonia.Controls protected override void OnOpened() { - if (_styles != null && _stylesDirty) + if (FlyoutPresenterClasses != null) { - // Presenter for flyout generally shouldn't be public, so - // we should be ok to just reset the styles - _popup.Child.Styles.Clear(); - _popup.Child.Styles.Add(_styles); + //Remove any classes no longer in use + for (int i = _popup.Child.Classes.Count - 1; i >= 0; i--) + { + if (!FlyoutPresenterClasses.Contains(_popup.Child.Classes[i])) + { + _popup.Child.Classes.RemoveAt(i); + } + } + + //Add new classes + _popup.Child.Classes.AddRange(FlyoutPresenterClasses); } base.OnOpened(); } - - private void OnMenuFlyoutPresenterStyleChanged(object sender, NotifyCollectionChangedEventArgs e) - { - _stylesDirty = true; - } } } From 88fb92d8c0dc6d5b623cb5c2d13090db65aa9f73 Mon Sep 17 00:00:00 2001 From: amwx Date: Fri, 19 Mar 2021 17:42:00 -0500 Subject: [PATCH 09/43] DefaultTheme styles --- src/Avalonia.Themes.Default/DefaultTheme.xaml | 2 ++ .../FlyoutPresenter.xaml | 30 +++++++++++++++++++ .../MenuFlyoutPresenter.xaml | 28 +++++++++++++++++ 3 files changed, 60 insertions(+) create mode 100644 src/Avalonia.Themes.Default/FlyoutPresenter.xaml create mode 100644 src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml diff --git a/src/Avalonia.Themes.Default/DefaultTheme.xaml b/src/Avalonia.Themes.Default/DefaultTheme.xaml index 107fc07875..4ae9ea4812 100644 --- a/src/Avalonia.Themes.Default/DefaultTheme.xaml +++ b/src/Avalonia.Themes.Default/DefaultTheme.xaml @@ -60,4 +60,6 @@ + + diff --git a/src/Avalonia.Themes.Default/FlyoutPresenter.xaml b/src/Avalonia.Themes.Default/FlyoutPresenter.xaml new file mode 100644 index 0000000000..39ee8e5a6e --- /dev/null +++ b/src/Avalonia.Themes.Default/FlyoutPresenter.xaml @@ -0,0 +1,30 @@ + + + diff --git a/src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml b/src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml new file mode 100644 index 0000000000..612203d3d7 --- /dev/null +++ b/src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml @@ -0,0 +1,28 @@ + + + From b6f5571495e52ce75c7d920a10be1d4836d7eed0 Mon Sep 17 00:00:00 2001 From: amwx Date: Fri, 19 Mar 2021 17:44:28 -0500 Subject: [PATCH 10/43] TextBox uses ContextFlyout --- src/Avalonia.Themes.Default/TextBox.xaml | 10 +++++++++- src/Avalonia.Themes.Fluent/Controls/TextBox.xaml | 12 ++++++++++-- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Themes.Default/TextBox.xaml b/src/Avalonia.Themes.Default/TextBox.xaml index 9085cfc2c6..6dce77e96d 100644 --- a/src/Avalonia.Themes.Default/TextBox.xaml +++ b/src/Avalonia.Themes.Default/TextBox.xaml @@ -3,6 +3,14 @@ M 11.416016,10 20,1.4160156 18.583984,0 10,8.5839846 1.4160156,0 0,1.4160156 8.5839844,10 0,18.583985 1.4160156,20 10,11.416015 18.583984,20 20,18.583985 Z m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z + + + + + + + + @@ -17,7 +25,7 @@ - + M 11.416016,10 20,1.4160156 18.583984,0 10,8.5839846 1.4160156,0 0,1.4160156 8.5839844,10 0,18.583985 1.4160156,20 10,11.416015 18.583984,20 20,18.583985 Z m10.051 7.0032c2.215 0 4.0105 1.7901 4.0105 3.9984s-1.7956 3.9984-4.0105 3.9984c-2.215 0-4.0105-1.7901-4.0105-3.9984s1.7956-3.9984 4.0105-3.9984zm0 1.4994c-1.3844 0-2.5066 1.1188-2.5066 2.499s1.1222 2.499 2.5066 2.499 2.5066-1.1188 2.5066-2.499-1.1222-2.499-2.5066-2.499zm0-5.0026c4.6257 0 8.6188 3.1487 9.7267 7.5613 0.10085 0.40165-0.14399 0.80877-0.54686 0.90931-0.40288 0.10054-0.81122-0.14355-0.91208-0.54521-0.94136-3.7492-4.3361-6.4261-8.2678-6.4261-3.9334 0-7.3292 2.6792-8.2689 6.4306-0.10063 0.40171-0.50884 0.64603-0.91177 0.54571s-0.648-0.5073-0.54737-0.90901c1.106-4.4152 5.1003-7.5667 9.728-7.5667z m0.21967 0.21965c-0.26627 0.26627-0.29047 0.68293-0.07262 0.97654l0.07262 0.08412 4.0346 4.0346c-1.922 1.3495-3.3585 3.365-3.9554 5.7495-0.10058 0.4018 0.14362 0.8091 0.54543 0.9097 0.40182 0.1005 0.80909-0.1436 0.90968-0.5455 0.52947-2.1151 1.8371-3.8891 3.5802-5.0341l1.8096 1.8098c-0.70751 0.7215-1.1438 1.71-1.1438 2.8003 0 2.2092 1.7909 4 4 4 1.0904 0 2.0788-0.4363 2.8004-1.1438l5.9193 5.9195c0.2929 0.2929 0.7677 0.2929 1.0606 0 0.2663-0.2662 0.2905-0.6829 0.0726-0.9765l-0.0726-0.0841-6.1135-6.1142 0.0012-0.0015-1.2001-1.1979-2.8699-2.8693 2e-3 -8e-4 -2.8812-2.8782 0.0012-0.0018-1.1333-1.1305-4.3064-4.3058c-0.29289-0.29289-0.76777-0.29289-1.0607 0zm7.9844 9.0458 3.5351 3.5351c-0.45 0.4358-1.0633 0.704-1.7392 0.704-1.3807 0-2.5-1.1193-2.5-2.5 0-0.6759 0.26824-1.2892 0.7041-1.7391zm1.7959-5.7655c-1.0003 0-1.9709 0.14807-2.8889 0.425l1.237 1.2362c0.5358-0.10587 1.0883-0.16119 1.6519-0.16119 3.9231 0 7.3099 2.6803 8.2471 6.4332 0.1004 0.4018 0.5075 0.6462 0.9094 0.5459 0.4019-0.1004 0.6463-0.5075 0.5459-0.9094-1.103-4.417-5.0869-7.5697-9.7024-7.5697zm0.1947 3.5093 3.8013 3.8007c-0.1018-2.0569-1.7488-3.7024-3.8013-3.8007z + + + + + + + + - + + + + + Context Flyout + A right click Flyout that can be applied to any control. + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs new file mode 100644 index 0000000000..e64d4a2cdd --- /dev/null +++ b/samples/ControlCatalog/Pages/ContextFlyoutPage.axaml.cs @@ -0,0 +1,45 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; +using ControlCatalog.ViewModels; +using Avalonia.Interactivity; +namespace ControlCatalog.Pages +{ + public class ContextFlyoutPage : UserControl + { + private TextBox _textBox; + + public ContextFlyoutPage() + { + InitializeComponent(); + + var vm = new ContextFlyoutPageViewModel(); + vm.View = this; + DataContext = vm; + + _textBox = this.FindControl("TextBox"); + + var cutButton = this.FindControl + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs b/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs new file mode 100644 index 0000000000..0803d178b9 --- /dev/null +++ b/samples/ControlCatalog/Pages/FlyoutsPage.axaml.cs @@ -0,0 +1,81 @@ +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Markup.Xaml; +using Avalonia.Interactivity; + +namespace ControlCatalog.Pages +{ + public class FlyoutsPage : UserControl + { + public FlyoutsPage() + { + InitializeComponent(); + + var afp = this.FindControl("AttachedFlyoutPanel"); + if (afp != null) + { + afp.DoubleTapped += Afp_DoubleTapped; + } + + SetXamlTexts(); + } + + private void Afp_DoubleTapped(object sender, RoutedEventArgs e) + { + if (sender is Panel p) + { + FlyoutBase.ShowAttachedFlyout(p); + } + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + + private void SetXamlTexts() + { + var bfxt = this.FindControl("ButtonFlyoutXamlText"); + bfxt.Text = ""; + + var mfxt = this.FindControl("MenuFlyoutXamlText"); + mfxt.Text = ""; + + var afxt = this.FindControl("AttachedFlyoutXamlText"); + afxt.Text = "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n" + + "\n\n In DoubleTapped handler:\n" + + "FlyoutBase.ShowAttachedFlyout(AttachedFlyoutPanel);"; + + var sfxt = this.FindControl("SharedFlyoutXamlText"); + sfxt.Text = "Declare a flyout in Resources:\n" + + "\n" + + " \n" + + " \n" + + " \n" + + " \n" + + " \n\n\n" + + "Then attach the flyout where you want it:\n" + + "