Browse Source

Initial Flyout Impl

pull/5682/head
amwx 5 years ago
parent
commit
a71910f928
  1. 65
      src/Avalonia.Controls/Flyouts/Flyout.cs
  2. 315
      src/Avalonia.Controls/Flyouts/FlyoutBase.cs
  3. 80
      src/Avalonia.Controls/Flyouts/FlyoutPlacementMode.cs
  4. 10
      src/Avalonia.Controls/Flyouts/FlyoutPresenter.cs
  5. 39
      src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml

65
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<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;
}
}
}

315
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<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;
}
}
}
}

80
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
{
/// <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
}
}

10
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
{
}
}

39
src/Avalonia.Themes.Fluent/Controls/FlyoutPresenter.xaml

@ -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…
Cancel
Save