A cross-platform UI framework for .NET
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 

464 lines
16 KiB

using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Input.Raw;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Platform;
using Avalonia.VisualTree;
using System;
using System.Reactive.Disposables;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
namespace Avalonia.Controls
{
/// <summary>
/// Defines constants for how the SplitView Pane should display
/// </summary>
public enum SplitViewDisplayMode
{
/// <summary>
/// Pane is displayed next to content, and does not auto collapse
/// when tapped outside
/// </summary>
Inline,
/// <summary>
/// Pane is displayed next to content. When collapsed, pane is still
/// visible according to CompactPaneLength. Pane does not auto collapse
/// when tapped outside
/// </summary>
CompactInline,
/// <summary>
/// Pane is displayed above content. Pane collapses when tapped outside
/// </summary>
Overlay,
/// <summary>
/// Pane is displayed above content. When collapsed, pane is still
/// visible according to CompactPaneLength. Pane collapses when tapped outside
/// </summary>
CompactOverlay
}
/// <summary>
/// Defines constants for where the Pane should appear
/// </summary>
public enum SplitViewPanePlacement
{
Left,
Right
}
public class SplitViewTemplateSettings : AvaloniaObject
{
internal SplitViewTemplateSettings() { }
public static readonly StyledProperty<double> ClosedPaneWidthProperty =
AvaloniaProperty.Register<SplitViewTemplateSettings, double>(nameof(ClosedPaneWidth), 0d);
public static readonly StyledProperty<GridLength> PaneColumnGridLengthProperty =
AvaloniaProperty.Register<SplitViewTemplateSettings, GridLength>(nameof(PaneColumnGridLength));
public double ClosedPaneWidth
{
get => GetValue(ClosedPaneWidthProperty);
internal set => SetValue(ClosedPaneWidthProperty, value);
}
public GridLength PaneColumnGridLength
{
get => GetValue(PaneColumnGridLengthProperty);
internal set => SetValue(PaneColumnGridLengthProperty, value);
}
}
/// <summary>
/// A control with two views: A collapsible pane and an area for content
/// </summary>
[PseudoClasses(":open", ":closed")]
[PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline")]
[PseudoClasses(":left", ":right")]
[PseudoClasses(":lightdismiss")]
public class SplitView : ContentControl
{
/*
Pseudo classes & combos
:open / :closed
:compactoverlay :compactinline :overlay :inline
:left :right
*/
/// <summary>
/// Defines the <see cref="CompactPaneLength"/> property
/// </summary>
public static readonly StyledProperty<double> CompactPaneLengthProperty =
AvaloniaProperty.Register<SplitView, double>(nameof(CompactPaneLength), defaultValue: 48);
/// <summary>
/// Defines the <see cref="DisplayMode"/> property
/// </summary>
public static readonly StyledProperty<SplitViewDisplayMode> DisplayModeProperty =
AvaloniaProperty.Register<SplitView, SplitViewDisplayMode>(nameof(DisplayMode), defaultValue: SplitViewDisplayMode.Overlay);
/// <summary>
/// Defines the <see cref="IsPaneOpen"/> property
/// </summary>
public static readonly DirectProperty<SplitView, bool> IsPaneOpenProperty =
AvaloniaProperty.RegisterDirect<SplitView, bool>(nameof(IsPaneOpen),
x => x.IsPaneOpen, (x, v) => x.IsPaneOpen = v);
/// <summary>
/// Defines the <see cref="OpenPaneLength"/> property
/// </summary>
public static readonly StyledProperty<double> OpenPaneLengthProperty =
AvaloniaProperty.Register<SplitView, double>(nameof(OpenPaneLength), defaultValue: 320);
/// <summary>
/// Defines the <see cref="PaneBackground"/> property
/// </summary>
public static readonly StyledProperty<IBrush> PaneBackgroundProperty =
AvaloniaProperty.Register<SplitView, IBrush>(nameof(PaneBackground));
/// <summary>
/// Defines the <see cref="PanePlacement"/> property
/// </summary>
public static readonly StyledProperty<SplitViewPanePlacement> PanePlacementProperty =
AvaloniaProperty.Register<SplitView, SplitViewPanePlacement>(nameof(PanePlacement));
/// <summary>
/// Defines the <see cref="Pane"/> property
/// </summary>
public static readonly StyledProperty<object> PaneProperty =
AvaloniaProperty.Register<SplitView, object>(nameof(Pane));
/// <summary>
/// Defines the <see cref="PaneTemplate"/> property.
/// </summary>
public static readonly StyledProperty<IDataTemplate> PaneTemplateProperty =
AvaloniaProperty.Register<HeaderedContentControl, IDataTemplate>(nameof(PaneTemplate));
/// <summary>
/// Defines the <see cref="UseLightDismissOverlayMode"/> property
/// </summary>
public static readonly StyledProperty<bool> UseLightDismissOverlayModeProperty =
AvaloniaProperty.Register<SplitView, bool>(nameof(UseLightDismissOverlayMode));
/// <summary>
/// Defines the <see cref="TemplateSettings"/> property
/// </summary>
public static readonly StyledProperty<SplitViewTemplateSettings> TemplateSettingsProperty =
AvaloniaProperty.Register<SplitView, SplitViewTemplateSettings>(nameof(TemplateSettings));
private bool _isPaneOpen;
private Panel _pane;
private IDisposable _pointerDisposable;
public SplitView()
{
PseudoClasses.Add(":overlay");
PseudoClasses.Add(":left");
TemplateSettings = new SplitViewTemplateSettings();
}
static SplitView()
{
UseLightDismissOverlayModeProperty.Changed.AddClassHandler<SplitView>((x, v) => x.OnUseLightDismissChanged(v));
CompactPaneLengthProperty.Changed.AddClassHandler<SplitView>((x, v) => x.OnCompactPaneLengthChanged(v));
PanePlacementProperty.Changed.AddClassHandler<SplitView>((x, v) => x.OnPanePlacementChanged(v));
DisplayModeProperty.Changed.AddClassHandler<SplitView>((x, v) => x.OnDisplayModeChanged(v));
}
/// <summary>
/// Gets or sets the length of the pane when in <see cref="SplitViewDisplayMode.CompactOverlay"/>
/// or <see cref="SplitViewDisplayMode.CompactInline"/> mode
/// </summary>
public double CompactPaneLength
{
get => GetValue(CompactPaneLengthProperty);
set => SetValue(CompactPaneLengthProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="SplitViewDisplayMode"/> for the SplitView
/// </summary>
public SplitViewDisplayMode DisplayMode
{
get => GetValue(DisplayModeProperty);
set => SetValue(DisplayModeProperty, value);
}
/// <summary>
/// Gets or sets whether the pane is open or closed
/// </summary>
public bool IsPaneOpen
{
get => _isPaneOpen;
set
{
if (value == _isPaneOpen)
{
return;
}
if (value)
{
OnPaneOpening(this, null);
SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value);
PseudoClasses.Add(":open");
PseudoClasses.Remove(":closed");
OnPaneOpened(this, null);
}
else
{
SplitViewPaneClosingEventArgs args = new SplitViewPaneClosingEventArgs(false);
OnPaneClosing(this, args);
if (!args.Cancel)
{
SetAndRaise(IsPaneOpenProperty, ref _isPaneOpen, value);
PseudoClasses.Add(":closed");
PseudoClasses.Remove(":open");
OnPaneClosed(this, null);
}
}
}
}
/// <summary>
/// Gets or sets the length of the pane when open
/// </summary>
public double OpenPaneLength
{
get => GetValue(OpenPaneLengthProperty);
set => SetValue(OpenPaneLengthProperty, value);
}
/// <summary>
/// Gets or sets the background of the pane
/// </summary>
public IBrush PaneBackground
{
get => GetValue(PaneBackgroundProperty);
set => SetValue(PaneBackgroundProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="SplitViewPanePlacement"/> for the SplitView
/// </summary>
public SplitViewPanePlacement PanePlacement
{
get => GetValue(PanePlacementProperty);
set => SetValue(PanePlacementProperty, value);
}
/// <summary>
/// Gets or sets the Pane for the SplitView
/// </summary>
public object Pane
{
get => GetValue(PaneProperty);
set => SetValue(PaneProperty, value);
}
/// <summary>
/// Gets or sets the data template used to display the header content of the control.
/// </summary>
public IDataTemplate PaneTemplate
{
get => GetValue(PaneTemplateProperty);
set => SetValue(PaneTemplateProperty, value);
}
/// <summary>
/// Gets or sets whether WinUI equivalent LightDismissOverlayMode is enabled
/// <para>When enabled, and the pane is open in Overlay or CompactOverlay mode,
/// the contents of the splitview are darkened to visually separate the open pane
/// and the rest of the SplitView</para>
/// </summary>
public bool UseLightDismissOverlayMode
{
get => GetValue(UseLightDismissOverlayModeProperty);
set => SetValue(UseLightDismissOverlayModeProperty, value);
}
/// <summary>
/// Gets or sets the TemplateSettings for the SplitView
/// </summary>
public SplitViewTemplateSettings TemplateSettings
{
get => GetValue(TemplateSettingsProperty);
set => SetValue(TemplateSettingsProperty, value);
}
/// <summary>
/// Fired when the pane is closed
/// </summary>
public event EventHandler<EventArgs> PaneClosed;
/// <summary>
/// Fired when the pane is closing
/// </summary>
public event EventHandler<SplitViewPaneClosingEventArgs> PaneClosing;
/// <summary>
/// Fired when the pane is opened
/// </summary>
public event EventHandler<EventArgs> PaneOpened;
/// <summary>
/// Fired when the pane is opening
/// </summary>
public event EventHandler<EventArgs> PaneOpening;
protected override bool RegisterContentPresenter(IContentPresenter presenter)
{
var result = base.RegisterContentPresenter(presenter);
if (presenter.Name == "PART_PanePresenter")
{
return true;
}
return result;
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_pane = e.NameScope.Find<Panel>("PART_PaneRoot");
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var topLevel = this.VisualRoot;
if (topLevel is Window window)
{
_pointerDisposable = window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel);
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_pointerDisposable?.Dispose();
}
private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
{
if (!IsPaneOpen)
{
return;
}
//If we click within the Pane, don't do anything
//Otherwise, ClosePane if open & using an overlay display mode
bool closePane = ShouldClosePane();
if (!closePane)
{
return;
}
var src = e.Source as IVisual;
while (src != null)
{
// Make assumption that if Popup is in visual tree,
// owning control is within pane
// This works because if pane is triggered to close
// when clicked anywhere else in Window, the pane
// would close before the popup is opened
if (src == _pane || src is PopupRoot)
{
closePane = false;
break;
}
src = src.VisualParent;
}
if (closePane)
{
IsPaneOpen = false;
e.Handled = true;
}
}
private bool ShouldClosePane()
{
return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay);
}
protected virtual void OnPaneOpening(SplitView sender, EventArgs args)
{
PaneOpening?.Invoke(sender, args);
}
protected virtual void OnPaneOpened(SplitView sender, EventArgs args)
{
PaneOpened?.Invoke(sender, args);
}
protected virtual void OnPaneClosing(SplitView sender, SplitViewPaneClosingEventArgs args)
{
PaneClosing?.Invoke(sender, args);
}
protected virtual void OnPaneClosed(SplitView sender, EventArgs args)
{
PaneClosed?.Invoke(sender, args);
}
private void OnCompactPaneLengthChanged(AvaloniaPropertyChangedEventArgs e)
{
var newLen = (double)e.NewValue;
var displayMode = DisplayMode;
if (displayMode == SplitViewDisplayMode.CompactInline)
{
TemplateSettings.ClosedPaneWidth = newLen;
}
else if (displayMode == SplitViewDisplayMode.CompactOverlay)
{
TemplateSettings.ClosedPaneWidth = newLen;
TemplateSettings.PaneColumnGridLength = new GridLength(newLen, GridUnitType.Pixel);
}
}
private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldState = e.OldValue.ToString().ToLower();
var newState = e.NewValue.ToString().ToLower();
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
}
private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e)
{
var oldState = e.OldValue.ToString().ToLower();
var newState = e.NewValue.ToString().ToLower();
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
var (closedPaneWidth, paneColumnGridLength) = (SplitViewDisplayMode)e.NewValue switch
{
SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)),
SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)),
SplitViewDisplayMode.Inline => (0, new GridLength(0, GridUnitType.Auto)),
SplitViewDisplayMode.CompactInline => (CompactPaneLength, new GridLength(0, GridUnitType.Auto)),
_ => throw new NotImplementedException(),
};
TemplateSettings.ClosedPaneWidth = closedPaneWidth;
TemplateSettings.PaneColumnGridLength = paneColumnGridLength;
}
private void OnUseLightDismissChanged(AvaloniaPropertyChangedEventArgs e)
{
var mode = (bool)e.NewValue;
PseudoClasses.Set(":lightdismiss", mode);
}
}
}