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