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;
namespace Avalonia.Controls
{
///
/// Defines constants for how the SplitView Pane should display
///
public enum SplitViewDisplayMode
{
///
/// Pane is displayed next to content, and does not auto collapse
/// when tapped outside
///
Inline,
///
/// Pane is displayed next to content. When collapsed, pane is still
/// visible according to CompactPaneLength. Pane does not auto collapse
/// when tapped outside
///
CompactInline,
///
/// Pane is displayed above content. Pane collapses when tapped outside
///
Overlay,
///
/// Pane is displayed above content. When collapsed, pane is still
/// visible according to CompactPaneLength. Pane collapses when tapped outside
///
CompactOverlay
}
///
/// Defines constants for where the Pane should appear
///
public enum SplitViewPanePlacement
{
Left,
Right
}
public class SplitViewTemplateSettings : AvaloniaObject
{
internal SplitViewTemplateSettings() { }
public static readonly StyledProperty ClosedPaneWidthProperty =
AvaloniaProperty.Register(nameof(ClosedPaneWidth), 0d);
public static readonly StyledProperty PaneColumnGridLengthProperty =
AvaloniaProperty.Register(nameof(PaneColumnGridLength));
public double ClosedPaneWidth
{
get => GetValue(ClosedPaneWidthProperty);
internal set => SetValue(ClosedPaneWidthProperty, value);
}
public GridLength PaneColumnGridLength
{
get => GetValue(PaneColumnGridLengthProperty);
internal set => SetValue(PaneColumnGridLengthProperty, value);
}
}
///
/// A control with two views: A collapsible pane and an area for content
///
public class SplitView : TemplatedControl
{
/*
Pseudo classes & combos
:open / :closed
:compactoverlay :compactinline :overlay :inline
:left :right
*/
///
/// Defines the property
///
public static readonly StyledProperty ContentProperty =
AvaloniaProperty.Register(nameof(Content));
///
/// Defines the property
///
public static readonly StyledProperty CompactPaneLengthProperty =
AvaloniaProperty.Register(nameof(CompactPaneLength), defaultValue: 48);
///
/// Defines the property
///
public static readonly StyledProperty DisplayModeProperty =
AvaloniaProperty.Register(nameof(DisplayMode), defaultValue: SplitViewDisplayMode.Overlay);
///
/// Defines the property
///
public static readonly DirectProperty IsPaneOpenProperty =
AvaloniaProperty.RegisterDirect(nameof(IsPaneOpen),
x => x.IsPaneOpen, (x, v) => x.IsPaneOpen = v);
///
/// Defines the property
///
public static readonly StyledProperty OpenPaneLengthProperty =
AvaloniaProperty.Register(nameof(OpenPaneLength), defaultValue: 320);
///
/// Defines the property
///
public static readonly StyledProperty PaneBackgroundProperty =
AvaloniaProperty.Register(nameof(PaneBackground));
///
/// Defines the property
///
public static readonly StyledProperty PanePlacementProperty =
AvaloniaProperty.Register(nameof(PanePlacement));
///
/// Defines the property
///
public static readonly StyledProperty PaneProperty =
AvaloniaProperty.Register(nameof(Pane));
///
/// Defines the property
///
public static readonly StyledProperty UseLightDismissOverlayModeProperty =
AvaloniaProperty.Register(nameof(UseLightDismissOverlayMode));
///
/// Defines the property
///
public static readonly StyledProperty TemplateSettingsProperty =
AvaloniaProperty.Register(nameof(TemplateSettings));
private bool _isPaneOpen;
private Panel _pane;
private CompositeDisposable _pointerDisposables;
public SplitView()
{
PseudoClasses.Add(":overlay");
PseudoClasses.Add(":left");
TemplateSettings = new SplitViewTemplateSettings();
}
static SplitView()
{
UseLightDismissOverlayModeProperty.Changed.AddClassHandler((x, v) => x.OnUseLightDismissChanged(v));
CompactPaneLengthProperty.Changed.AddClassHandler((x, v) => x.OnCompactPaneLengthChanged(v));
PanePlacementProperty.Changed.AddClassHandler((x, v) => x.OnPanePlacementChanged(v));
DisplayModeProperty.Changed.AddClassHandler((x, v) => x.OnDisplayModeChanged(v));
}
///
/// Gets or sets the content of the SplitView
///
[Content]
public IControl Content
{
get => GetValue(ContentProperty);
set => SetValue(ContentProperty, value);
}
///
/// Gets or sets the length of the pane when in
/// or mode
///
public double CompactPaneLength
{
get => GetValue(CompactPaneLengthProperty);
set => SetValue(CompactPaneLengthProperty, value);
}
///
/// Gets or sets the for the SplitView
///
public SplitViewDisplayMode DisplayMode
{
get => GetValue(DisplayModeProperty);
set => SetValue(DisplayModeProperty, value);
}
///
/// Gets or sets whether the pane is open or closed
///
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);
}
}
}
}
///
/// Gets or sets the length of the pane when open
///
public double OpenPaneLength
{
get => GetValue(OpenPaneLengthProperty);
set => SetValue(OpenPaneLengthProperty, value);
}
///
/// Gets or sets the background of the pane
///
public IBrush PaneBackground
{
get => GetValue(PaneBackgroundProperty);
set => SetValue(PaneBackgroundProperty, value);
}
///
/// Gets or sets the for the SplitView
///
public SplitViewPanePlacement PanePlacement
{
get => GetValue(PanePlacementProperty);
set => SetValue(PanePlacementProperty, value);
}
///
/// Gets or sets the Pane for the SplitView
///
public IControl Pane
{
get => GetValue(PaneProperty);
set => SetValue(PaneProperty, value);
}
///
/// Gets or sets whether WinUI equivalent LightDismissOverlayMode is enabled
/// 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
///
public bool UseLightDismissOverlayMode
{
get => GetValue(UseLightDismissOverlayModeProperty);
set => SetValue(UseLightDismissOverlayModeProperty, value);
}
///
/// Gets or sets the TemplateSettings for the SplitView
///
public SplitViewTemplateSettings TemplateSettings
{
get => GetValue(TemplateSettingsProperty);
set => SetValue(TemplateSettingsProperty, value);
}
///
/// Fired when the pane is closed
///
public event EventHandler PaneClosed;
///
/// Fired when the pane is closing
///
public event EventHandler PaneClosing;
///
/// Fired when the pane is opened
///
public event EventHandler PaneOpened;
///
/// Fired when the pane is opening
///
public event EventHandler PaneOpening;
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_pane = e.NameScope.Find("PART_PaneRoot");
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
var topLevel = this.VisualRoot;
if (topLevel is Window window)
{
//Logic adapted from Popup
//Basically if we're using an overlay DisplayMode, close the pane if we don't click on the pane
IDisposable subscribeToEventHandler(T target, TEventHandler handler,
Action subscribe, Action unsubscribe)
{
subscribe(target, handler);
return Disposable.Create((unsubscribe, target, handler), state => state.unsubscribe(state.target, state.handler));
}
_pointerDisposables = new CompositeDisposable(
window.AddDisposableHandler(PointerPressedEvent, PointerPressedOutside, RoutingStrategies.Tunnel),
InputManager.Instance?.Process.Subscribe(OnNonClientClick),
subscribeToEventHandler(window, Window_Deactivated,
(x, handler) => x.Deactivated += handler, (x, handler) => x.Deactivated -= handler),
subscribeToEventHandler(window.PlatformImpl, OnWindowLostFocus,
(x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler));
}
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnDetachedFromVisualTree(e);
_pointerDisposables?.Dispose();
}
private void OnWindowLostFocus()
{
if (IsPaneOpen && ShouldClosePane())
{
IsPaneOpen = false;
}
}
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)
{
if (src == _pane)
{
closePane = false;
break;
}
src = src.VisualParent;
}
if (closePane)
{
IsPaneOpen = false;
e.Handled = true;
}
}
private void OnNonClientClick(RawInputEventArgs obj)
{
if (!IsPaneOpen)
{
return;
}
var mouse = obj as RawPointerEventArgs;
if (mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
if (ShouldClosePane())
IsPaneOpen = false;
}
}
private void Window_Deactivated(object sender, EventArgs e)
{
if (IsPaneOpen && ShouldClosePane())
{
IsPaneOpen = false;
}
}
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);
}
}
}