5 changed files with 509 additions and 0 deletions
@ -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<object> ContentProperty = |
||||
|
AvaloniaProperty.Register<Flyout, object>(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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -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<FlyoutBase, bool> IsOpenProperty = |
||||
|
AvaloniaProperty.RegisterDirect<FlyoutBase, bool>(nameof(IsOpen), |
||||
|
x => x.IsOpen); |
||||
|
|
||||
|
public static readonly DirectProperty<FlyoutBase, Control?> TargetProperty = |
||||
|
AvaloniaProperty.RegisterDirect<FlyoutBase, Control?>(nameof(Target), x => x.Target); |
||||
|
|
||||
|
public static readonly DirectProperty<FlyoutBase, FlyoutPlacementMode> PlacementProperty = |
||||
|
AvaloniaProperty.RegisterDirect<FlyoutBase, FlyoutPlacementMode>(nameof(Placement), |
||||
|
x => x.Placement, (x, v) => x.Placement = v); |
||||
|
|
||||
|
public static readonly AttachedProperty<FlyoutBase?> AttachedFlyoutProperty = |
||||
|
AvaloniaProperty.RegisterAttached<FlyoutBase, Control, FlyoutBase?>("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<CancelEventArgs>? 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; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,80 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Avalonia.Controls |
||||
|
{ |
||||
|
public enum FlyoutPlacementMode |
||||
|
{ |
||||
|
/// <summary>
|
||||
|
/// Preferred location is above the target element
|
||||
|
/// </summary>
|
||||
|
Top = 0, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is below the target element
|
||||
|
/// </summary>
|
||||
|
Bottom = 1, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is to the left of the target element
|
||||
|
/// </summary>
|
||||
|
Left = 2, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is to the right of the target element
|
||||
|
/// </summary>
|
||||
|
Right = 3, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is centered on the screen
|
||||
|
/// </summary>
|
||||
|
Full = 4, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is above the target element, with the left edge of the flyout
|
||||
|
/// aligned with the left edge of the target element
|
||||
|
/// </summary>
|
||||
|
TopEdgeAlignedLeft = 5, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is above the target element, with the right edge of flyout aligned with right edge of the target element.
|
||||
|
/// </summary>
|
||||
|
TopEdgeAlignedRight = 6, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is below the target element, with the left edge of flyout aligned with left edge of the target element.
|
||||
|
/// </summary>
|
||||
|
BottomEdgeAlignedLeft = 7, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is below the target element, with the right edge of flyout aligned with right edge of the target element.
|
||||
|
/// </summary>
|
||||
|
BottomEdgeAlignedRight = 8, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is to the left of the target element, with the top edge of flyout aligned with top edge of the target element.
|
||||
|
/// </summary>
|
||||
|
LeftEdgeAlignedTop = 9, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is to the left of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
|
||||
|
/// </summary>
|
||||
|
LeftEdgeAlignedBottom = 10, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is to the right of the target element, with the top edge of flyout aligned with top edge of the target element.
|
||||
|
/// </summary>
|
||||
|
RightEdgeAlignedTop = 11, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is to the right of the target element, with the bottom edge of flyout aligned with bottom edge of the target element.
|
||||
|
/// </summary>
|
||||
|
RightEdgeAlignedBottom = 12, |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Preferred location is determined automatically.
|
||||
|
/// </summary>
|
||||
|
Auto = 13 |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,10 @@ |
|||||
|
using System; |
||||
|
using System.Collections.Generic; |
||||
|
using System.Text; |
||||
|
|
||||
|
namespace Avalonia.Controls |
||||
|
{ |
||||
|
public class FlyoutPresenter : ContentControl |
||||
|
{ |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,39 @@ |
|||||
|
<Styles xmlns="https://github.com/avaloniaui" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
||||
|
<Style Selector="FlyoutPresenter"> |
||||
|
<Setter Property="HorizontalContentAlignment" Value="Stretch" /> |
||||
|
<Setter Property="VerticalContentAlignment" Value="Stretch" /> |
||||
|
<Setter Property="Background" Value="{DynamicResource FlyoutPresenterBackground}" /> |
||||
|
<Setter Property="BorderBrush" Value="{DynamicResource FlyoutBorderThemeBrush}" /> |
||||
|
<Setter Property="BorderThickness" Value="{DynamicResource FlyoutBorderThemeThickness}" /> |
||||
|
<Setter Property="Padding" Value="{DynamicResource FlyoutContentThemePadding}" /> |
||||
|
<Setter Property="MinWidth" Value="{DynamicResource FlyoutThemeMinWidth}" /> |
||||
|
<Setter Property="MaxWidth" Value="{DynamicResource FlyoutThemeMaxWidth}" /> |
||||
|
<Setter Property="MinHeight" Value="{DynamicResource FlyoutThemeMinHeight}" /> |
||||
|
<Setter Property="MaxHeight" Value="{DynamicResource FlyoutThemeMaxHeight}" /> |
||||
|
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" /> |
||||
|
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" /> |
||||
|
<Setter Property="Template"> |
||||
|
<ControlTemplate> |
||||
|
<Border Name="LayoutRoot" |
||||
|
Background="{TemplateBinding Background}" |
||||
|
BorderBrush="{TemplateBinding BorderBrush}" |
||||
|
BorderThickness="{TemplateBinding BorderThickness}" |
||||
|
Padding="{DynamicResource FlyoutBorderThemePadding}"> |
||||
|
<ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}" |
||||
|
VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}"> |
||||
|
<ContentPresenter Content="{TemplateBinding Content}" |
||||
|
ContentTemplate="{TemplateBinding ContentTemplate}" |
||||
|
Margin="{TemplateBinding Padding}" |
||||
|
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}" |
||||
|
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" |
||||
|
HorizontalContentAlignment="Stretch" |
||||
|
VerticalContentAlignment="Stretch" /> |
||||
|
</ScrollViewer> |
||||
|
</Border> |
||||
|
</ControlTemplate> |
||||
|
</Setter> |
||||
|
</Style> |
||||
|
<Style Selector="FlyoutPresenter /template/ Border#LayoutRoot"> |
||||
|
<Setter Property="CornerRadius" Value="{DynamicResource OverlayCornerRadius}" /> |
||||
|
</Style> |
||||
|
</Styles> |
||||
Loading…
Reference in new issue