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.
 
 
 

598 lines
21 KiB

using System;
using Avalonia.Controls.Metadata;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Reactive;
namespace Avalonia.Controls
{
/// <summary>
/// A control with two views: A collapsible pane and an area for content
/// </summary>
[TemplatePart("PART_PaneRoot", typeof(Panel))]
[PseudoClasses(pcOpen, pcClosed)]
[PseudoClasses(pcCompactOverlay, pcCompactInline, pcOverlay, pcInline)]
[PseudoClasses(pcLeft, pcRight)]
[PseudoClasses(pcLightDismiss)]
public class SplitView : ContentControl
{
protected const string pcOpen = ":open";
protected const string pcClosed = ":closed";
protected const string pcCompactOverlay = ":compactoverlay";
protected const string pcCompactInline = ":compactinline";
protected const string pcOverlay = ":overlay";
protected const string pcInline = ":inline";
protected const string pcLeft = ":left";
protected const string pcRight = ":right";
protected const string pcLightDismiss = ":lightDismiss";
/// <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 StyledProperty<bool> IsPaneOpenProperty =
AvaloniaProperty.Register<SplitView, bool>(
nameof(IsPaneOpen),
defaultValue: false,
coerce: CoerceIsPaneOpen);
/// <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<SplitView, 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 DirectProperty<SplitView, SplitViewTemplateSettings> TemplateSettingsProperty =
AvaloniaProperty.RegisterDirect<SplitView, SplitViewTemplateSettings>(nameof(TemplateSettings),
x => x.TemplateSettings);
/// <summary>
/// Defines the <see cref="PaneClosed"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> PaneClosedEvent =
RoutedEvent.Register<SplitView, RoutedEventArgs>(
nameof(PaneClosed),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PaneClosing"/> event.
/// </summary>
public static readonly RoutedEvent<CancelRoutedEventArgs> PaneClosingEvent =
RoutedEvent.Register<SplitView, CancelRoutedEventArgs>(
nameof(PaneClosing),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PaneOpened"/> event.
/// </summary>
public static readonly RoutedEvent<RoutedEventArgs> PaneOpenedEvent =
RoutedEvent.Register<SplitView, RoutedEventArgs>(
nameof(PaneOpened),
RoutingStrategies.Bubble);
/// <summary>
/// Defines the <see cref="PaneOpening"/> event.
/// </summary>
public static readonly RoutedEvent<CancelRoutedEventArgs> PaneOpeningEvent =
RoutedEvent.Register<SplitView, CancelRoutedEventArgs>(
nameof(PaneOpening),
RoutingStrategies.Bubble);
private Panel? _pane;
private IDisposable? _pointerDisposable;
private SplitViewTemplateSettings _templateSettings = new SplitViewTemplateSettings();
private string? _lastDisplayModePseudoclass;
private string? _lastPlacementPseudoclass;
/// <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 => GetValue(IsPaneOpenProperty);
set => SetValue(IsPaneOpenProperty, value);
}
/// <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>
[DependsOn(nameof(PaneTemplate))]
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 => _templateSettings;
private set => SetAndRaise(TemplateSettingsProperty, ref _templateSettings, value);
}
/// <summary>
/// Fired when the pane is closed.
/// </summary>
public event EventHandler<RoutedEventArgs>? PaneClosed
{
add => AddHandler(PaneClosedEvent, value);
remove => RemoveHandler(PaneClosedEvent, value);
}
/// <summary>
/// Fired when the pane is closing.
/// </summary>
/// <remarks>
/// The event args <see cref="CancelRoutedEventArgs.Cancel"/> property may be set to true to cancel the event
/// and keep the pane open.
/// </remarks>
public event EventHandler<CancelRoutedEventArgs>? PaneClosing
{
add => AddHandler(PaneClosingEvent, value);
remove => RemoveHandler(PaneClosingEvent, value);
}
/// <summary>
/// Fired when the pane is opened.
/// </summary>
public event EventHandler<RoutedEventArgs>? PaneOpened
{
add => AddHandler(PaneOpenedEvent, value);
remove => RemoveHandler(PaneOpenedEvent, value);
}
/// <summary>
/// Fired when the pane is opening.
/// </summary>
/// <remarks>
/// The event args <see cref="CancelRoutedEventArgs.Cancel"/> property may be set to true to cancel the event
/// and keep the pane closed.
/// </remarks>
public event EventHandler<CancelRoutedEventArgs>? PaneOpening
{
add => AddHandler(PaneOpeningEvent, value);
remove => RemoveHandler(PaneOpeningEvent, value);
}
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");
UpdateVisualStateForDisplayMode(DisplayMode);
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
// :left and :right style triggers contain the template so we need to do this as
// soon as we're attached so the template applies. The other visual states can
// be updated after the template applies
UpdateVisualStateForPanePlacementProperty(PanePlacement);
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_pointerDisposable?.Dispose();
}
/// <inheritdoc/>
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == CompactPaneLengthProperty)
{
UpdateVisualStateForCompactPaneLength(change.GetNewValue<double>());
}
else if (change.Property == DisplayModeProperty)
{
UpdateVisualStateForDisplayMode(change.GetNewValue<SplitViewDisplayMode>());
}
else if (change.Property == IsPaneOpenProperty)
{
bool isPaneOpen = change.GetNewValue<bool>();
if (isPaneOpen)
{
PseudoClasses.Add(pcOpen);
PseudoClasses.Remove(pcClosed);
OnPaneOpened(new RoutedEventArgs(PaneOpenedEvent, this));
}
else
{
PseudoClasses.Add(pcClosed);
PseudoClasses.Remove(pcOpen);
OnPaneClosed(new RoutedEventArgs(PaneClosedEvent, this));
}
}
else if (change.Property == PaneProperty)
{
if (change.OldValue is ILogical oldChild)
{
LogicalChildren.Remove(oldChild);
}
if (change.NewValue is ILogical newChild)
{
LogicalChildren.Add(newChild);
}
}
else if (change.Property == PanePlacementProperty)
{
UpdateVisualStateForPanePlacementProperty(change.GetNewValue<SplitViewPanePlacement>());
}
else if (change.Property == UseLightDismissOverlayModeProperty)
{
var mode = change.GetNewValue<bool>();
PseudoClasses.Set(pcLightDismiss, mode);
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
if (!e.Handled && e.Key == Key.Escape)
{
if (IsPaneOpen && IsInOverlayMode())
{
SetCurrentValue(IsPaneOpenProperty, false);
e.Handled = true;
}
}
base.OnKeyDown(e);
}
private void PointerReleasedOutside(object? sender, PointerReleasedEventArgs e)
{
if (!IsPaneOpen || _pane == null)
{
return;
}
var closePane = true;
var src = e.Source as Visual;
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)
{
SetCurrentValue(IsPaneOpenProperty, false);
e.Handled = true;
}
}
private bool IsInOverlayMode()
{
return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay);
}
protected virtual void OnPaneOpening(CancelRoutedEventArgs args)
{
RaiseEvent(args);
}
protected virtual void OnPaneOpened(RoutedEventArgs args)
{
EnableLightDismiss();
RaiseEvent(args);
}
protected virtual void OnPaneClosing(CancelRoutedEventArgs args)
{
RaiseEvent(args);
}
protected virtual void OnPaneClosed(RoutedEventArgs args)
{
_pointerDisposable?.Dispose();
_pointerDisposable = null;
RaiseEvent(args);
}
/// <summary>
/// Gets the appropriate PseudoClass for the given <see cref="SplitViewDisplayMode"/>.
/// </summary>
private static string GetPseudoClass(SplitViewDisplayMode mode)
{
return mode switch
{
SplitViewDisplayMode.Inline => pcInline,
SplitViewDisplayMode.CompactInline => pcCompactInline,
SplitViewDisplayMode.Overlay => pcOverlay,
SplitViewDisplayMode.CompactOverlay => pcCompactOverlay,
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null)
};
}
/// <summary>
/// Gets the appropriate PseudoClass for the given <see cref="SplitViewPanePlacement"/>.
/// </summary>
private static string GetPseudoClass(SplitViewPanePlacement placement)
{
return placement switch
{
SplitViewPanePlacement.Left => pcLeft,
SplitViewPanePlacement.Right => pcRight,
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null)
};
}
/// <summary>
/// Called when the <see cref="IsPaneOpen"/> property has to be coerced.
/// </summary>
/// <param name="value">The value to coerce.</param>
protected virtual bool OnCoerceIsPaneOpen(bool value)
{
CancelRoutedEventArgs eventArgs;
if (value)
{
eventArgs = new CancelRoutedEventArgs(PaneOpeningEvent, this);
OnPaneOpening(eventArgs);
}
else
{
eventArgs = new CancelRoutedEventArgs(PaneClosingEvent, this);
OnPaneClosing(eventArgs);
}
if (eventArgs.Cancel)
{
return !value;
}
return value;
}
private void UpdateVisualStateForCompactPaneLength(double newLen)
{
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 UpdateVisualStateForDisplayMode(SplitViewDisplayMode newValue)
{
if (!string.IsNullOrEmpty(_lastDisplayModePseudoclass))
{
PseudoClasses.Remove(_lastDisplayModePseudoclass);
}
_lastDisplayModePseudoclass = GetPseudoClass(newValue);
PseudoClasses.Add(_lastDisplayModePseudoclass);
var (closedPaneWidth, paneColumnGridLength) = 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 UpdateVisualStateForPanePlacementProperty(SplitViewPanePlacement newValue)
{
if (!string.IsNullOrEmpty(_lastPlacementPseudoclass))
{
PseudoClasses.Remove(_lastPlacementPseudoclass);
}
_lastPlacementPseudoclass = GetPseudoClass(newValue);
PseudoClasses.Add(_lastPlacementPseudoclass);
}
private void EnableLightDismiss()
{
if (_pane == null)
return;
// If this returns false, we're not in Overlay or CompactOverlay DisplayMode
// and don't need the light dismiss behavior
if (!IsInOverlayMode())
return;
var topLevel = TopLevel.GetTopLevel(this);
if (topLevel != null)
{
_pointerDisposable = Disposable.Create(() =>
{
topLevel.PointerReleased -= PointerReleasedOutside;
topLevel.BackRequested -= TopLevelBackRequested;
});
topLevel.PointerReleased += PointerReleasedOutside;
topLevel.BackRequested += TopLevelBackRequested;
}
}
private void TopLevelBackRequested(object? sender, RoutedEventArgs e)
{
if (!IsInOverlayMode())
return;
SetCurrentValue(IsPaneOpenProperty, false);
e.Handled = true;
}
/// <summary>
/// Coerces/validates the <see cref="IsPaneOpen"/> property value.
/// </summary>
/// <param name="instance">The <see cref="SplitView"/> instance.</param>
/// <param name="value">The value to coerce.</param>
/// <returns>The coerced/validated value.</returns>
private static bool CoerceIsPaneOpen(AvaloniaObject instance, bool value)
{
if (instance is SplitView splitView)
{
return splitView.OnCoerceIsPaneOpen(value);
}
return value;
}
}
}