2 changed files with 490 additions and 0 deletions
@ -0,0 +1,476 @@ |
|||||
|
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 |
||||
|
{ |
||||
|
/// <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>
|
||||
|
public class SplitView : TemplatedControl |
||||
|
{ |
||||
|
/* |
||||
|
Pseudo classes & combos |
||||
|
:open / :closed |
||||
|
:compactoverlay :compactinline :overlay :inline |
||||
|
:left :right |
||||
|
*/ |
||||
|
|
||||
|
/// <summary>
|
||||
|
/// Defines the <see cref="Content"/> property
|
||||
|
/// </summary>
|
||||
|
public static readonly StyledProperty<IControl> ContentProperty = |
||||
|
AvaloniaProperty.Register<SplitView, IControl>(nameof(Content)); |
||||
|
|
||||
|
/// <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<IControl> PaneProperty = |
||||
|
AvaloniaProperty.Register<SplitView, IControl>(nameof(Pane)); |
||||
|
|
||||
|
/// <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 CompositeDisposable _pointerDisposables; |
||||
|
|
||||
|
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 content of the SplitView
|
||||
|
/// </summary>
|
||||
|
[Content] |
||||
|
public IControl Content |
||||
|
{ |
||||
|
get => GetValue(ContentProperty); |
||||
|
set => SetValue(ContentProperty, value); |
||||
|
} |
||||
|
|
||||
|
/// <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 IControl Pane |
||||
|
{ |
||||
|
get => GetValue(PaneProperty); |
||||
|
set => SetValue(PaneProperty, 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 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) |
||||
|
{ |
||||
|
//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, TEventHandler>(T target, TEventHandler handler, |
||||
|
Action<T, TEventHandler> subscribe, Action<T, TEventHandler> 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, EventHandler>(window, Window_Deactivated, |
||||
|
(x, handler) => x.Deactivated += handler, (x, handler) => x.Deactivated -= handler), |
||||
|
subscribeToEventHandler<IWindowImpl, Action>(window.PlatformImpl, OnWindowLostFocus, |
||||
|
(x, handler) => x.LostFocus += handler, (x, handler) => x.LostFocus -= handler)); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e) |
||||
|
{ |
||||
|
base.OnDetachedFromVisualTree(e); |
||||
|
if (_pointerDisposables != null) |
||||
|
_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 state = (SplitViewDisplayMode)e.NewValue; |
||||
|
//PaneColumn Specs: Width/ColumnWidth
|
||||
|
// Overlay - 0px/0px
|
||||
|
// CompactOverlay - CompactPaneLength/CompactPaneLength
|
||||
|
// Inline - 0px/Auto
|
||||
|
// CompactInline - CompactPaneLength/Auto
|
||||
|
TemplateSettings.ClosedPaneWidth = (state == SplitViewDisplayMode.CompactInline || |
||||
|
state == SplitViewDisplayMode.CompactOverlay) ? CompactPaneLength : 0; |
||||
|
|
||||
|
TemplateSettings.PaneColumnGridLength = (state == SplitViewDisplayMode.CompactInline || |
||||
|
state == SplitViewDisplayMode.Inline) ? new GridLength(0, GridUnitType.Auto) : |
||||
|
state == SplitViewDisplayMode.Overlay ? new GridLength(0, GridUnitType.Pixel) : |
||||
|
new GridLength(CompactPaneLength, GridUnitType.Pixel); |
||||
|
} |
||||
|
|
||||
|
private void OnUseLightDismissChanged(AvaloniaPropertyChangedEventArgs e) |
||||
|
{ |
||||
|
var mode = (bool)e.NewValue; |
||||
|
PseudoClasses.Set(":lightdismiss", mode); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
@ -0,0 +1,14 @@ |
|||||
|
using System; |
||||
|
|
||||
|
namespace Avalonia.Controls |
||||
|
{ |
||||
|
public class SplitViewPaneClosingEventArgs : EventArgs |
||||
|
{ |
||||
|
public bool Cancel { get; set; } |
||||
|
|
||||
|
public SplitViewPaneClosingEventArgs(bool cancel) |
||||
|
{ |
||||
|
Cancel = cancel; |
||||
|
} |
||||
|
} |
||||
|
} |
||||
Loading…
Reference in new issue