16 changed files with 1183 additions and 19 deletions
@ -0,0 +1,97 @@ |
|||
<UserControl xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" |
|||
xmlns:d="http://schemas.microsoft.com/expression/blend/2008" |
|||
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" |
|||
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" |
|||
x:Class="ControlCatalog.Pages.SplitViewPage"> |
|||
|
|||
<Border> |
|||
|
|||
<Grid ColumnDefinitions="*,225"> |
|||
|
|||
<StackPanel Grid.Column="1" Orientation="Vertical" Spacing="4" Margin="5"> |
|||
<ToggleButton Name="PaneOpenButton" |
|||
Content="IsPaneOpen" |
|||
IsChecked="{Binding IsPaneOpen, ElementName=SplitView}" /> |
|||
|
|||
<ToggleButton Name="UseLightDismissOverlayModeButton" |
|||
Content="UseLightDismissOverlayMode" |
|||
IsChecked="{Binding UseLightDismissOverlayMode, ElementName=SplitView}" /> |
|||
|
|||
<ToggleSwitch OffContent="Left" OnContent="Right" Content="Placement" IsChecked="{Binding !IsLeft}" /> |
|||
|
|||
<TextBlock Text="DisplayMode" /> |
|||
<ComboBox Name="DisplayModeSelector" Width="170" Margin="10" SelectedIndex="{Binding DisplayMode}"> |
|||
<ComboBoxItem>Inline</ComboBoxItem> |
|||
<ComboBoxItem>CompactInline</ComboBoxItem> |
|||
<ComboBoxItem>Overlay</ComboBoxItem> |
|||
<ComboBoxItem>CompactOverlay</ComboBoxItem> |
|||
</ComboBox> |
|||
|
|||
<TextBlock Text="PaneBackground" /> |
|||
<ComboBox Name="PaneBackgroundSelector" SelectedIndex="0" Width="170" Margin="10"> |
|||
<ComboBoxItem Tag="{DynamicResource SystemControlBackgroundChromeMediumLowBrush}">SystemControlBackgroundChromeMediumLowBrush</ComboBoxItem> |
|||
<ComboBoxItem Tag="Red">Red</ComboBoxItem> |
|||
<ComboBoxItem Tag="Blue">Blue</ComboBoxItem> |
|||
<ComboBoxItem Tag="Green">Green</ComboBoxItem> |
|||
</ComboBox> |
|||
|
|||
<TextBlock Text="{Binding Value, ElementName=OpenPaneLengthSlider, StringFormat='{}OpenPaneLength: {0}'}" /> |
|||
<Slider Name="OpenPaneLengthSlider" Value="256" Minimum="128" Maximum="500" |
|||
Width="150" /> |
|||
|
|||
<TextBlock Text="{Binding Value, ElementName=CompactPaneLengthSlider, StringFormat='{}CompactPaneLength: {0}'}" /> |
|||
<Slider Name="CompactPaneLengthSlider" Value="48" Minimum="24" Maximum="128" |
|||
Width="150" /> |
|||
|
|||
</StackPanel> |
|||
|
|||
<Border BorderBrush="{DynamicResource SystemControlHighlightBaseLowBrush}" |
|||
BorderThickness="1"> |
|||
<!--{Binding SelectedItem.Tag, ElementName=PaneBackgroundSelector}--> |
|||
<SplitView Name="SplitView" |
|||
PanePlacement="{Binding PanePlacement}" |
|||
PaneBackground="{Binding SelectedItem.Tag, ElementName=PaneBackgroundSelector}" |
|||
OpenPaneLength="{Binding Value, ElementName=OpenPaneLengthSlider}" |
|||
CompactPaneLength="{Binding Value, ElementName=CompactPaneLengthSlider}" |
|||
DisplayMode="{Binding CurrentDisplayMode}"> |
|||
<SplitView.Pane> |
|||
<Grid> |
|||
<Grid.RowDefinitions> |
|||
<RowDefinition Height="Auto" /> |
|||
<RowDefinition Height="*" /> |
|||
<RowDefinition Height="Auto" /> |
|||
</Grid.RowDefinitions> |
|||
<TextBlock Text="PANE CONTENT" FontWeight="Bold" Name="PaneHeader" Margin="5,12,0,0" /> |
|||
<ListBoxItem Grid.Row="1" VerticalAlignment="Top" Margin="0 10"> |
|||
<StackPanel Orientation="Horizontal"> |
|||
<!--Path glyph from materialdesignicons.com--> |
|||
<Border Width="48"> |
|||
<Viewbox Width="24" Height="24" HorizontalAlignment="Left"> |
|||
<Canvas Width="24" Height="24"> |
|||
<Path Fill="{DynamicResource SystemControlForegroundBaseHighBrush}" Data="M16 17V19H2V17S2 13 9 13 16 17 16 17M12.5 7.5A3.5 3.5 0 1 0 9 11A3.5 3.5 0 0 0 12.5 7.5M15.94 13A5.32 5.32 0 0 1 18 17V19H22V17S22 13.37 15.94 13M15 4A3.39 3.39 0 0 0 13.07 4.59A5 5 0 0 1 13.07 10.41A3.39 3.39 0 0 0 15 11A3.5 3.5 0 0 0 15 4Z" /> |
|||
</Canvas> |
|||
</Viewbox> |
|||
</Border> |
|||
<TextBlock Text="People" VerticalAlignment="Center" /> |
|||
</StackPanel> |
|||
</ListBoxItem> |
|||
<TextBlock Grid.Row="2" Text="Item at bottom" Margin="60,12" /> |
|||
</Grid> |
|||
</SplitView.Pane> |
|||
|
|||
<Grid> |
|||
<TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" HorizontalAlignment="Center" VerticalAlignment="Center" Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" /> |
|||
<TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" TextAlignment="Left" Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" /> |
|||
<TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" HorizontalAlignment="Right" TextAlignment="Left" Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" /> |
|||
<TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" VerticalAlignment="Bottom" TextAlignment="Left" Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" /> |
|||
<TextBlock FontSize="14" FontWeight="700" Text="SplitViewContent" VerticalAlignment="Bottom" HorizontalAlignment="Right" TextAlignment="Left" Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" /> |
|||
</Grid> |
|||
|
|||
</SplitView> |
|||
</Border> |
|||
|
|||
</Grid> |
|||
</Border> |
|||
|
|||
</UserControl> |
|||
@ -0,0 +1,21 @@ |
|||
using Avalonia; |
|||
using Avalonia.Controls; |
|||
using Avalonia.Markup.Xaml; |
|||
using ControlCatalog.ViewModels; |
|||
|
|||
namespace ControlCatalog.Pages |
|||
{ |
|||
public class SplitViewPage : UserControl |
|||
{ |
|||
public SplitViewPage() |
|||
{ |
|||
this.InitializeComponent(); |
|||
DataContext = new SplitViewPageViewModel(); |
|||
} |
|||
|
|||
private void InitializeComponent() |
|||
{ |
|||
AvaloniaXamlLoader.Load(this); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
using System; |
|||
using Avalonia.Controls; |
|||
using ReactiveUI; |
|||
|
|||
namespace ControlCatalog.ViewModels |
|||
{ |
|||
public class SplitViewPageViewModel : ReactiveObject |
|||
{ |
|||
private bool _isLeft = true; |
|||
private int _displayMode = 3; //CompactOverlay
|
|||
|
|||
public bool IsLeft |
|||
{ |
|||
get => _isLeft; |
|||
set |
|||
{ |
|||
this.RaiseAndSetIfChanged(ref _isLeft, value); |
|||
this.RaisePropertyChanged(nameof(PanePlacement)); |
|||
} |
|||
} |
|||
|
|||
public int DisplayMode |
|||
{ |
|||
get => _displayMode; |
|||
set |
|||
{ |
|||
this.RaiseAndSetIfChanged(ref _displayMode, value); |
|||
this.RaisePropertyChanged(nameof(CurrentDisplayMode)); |
|||
} |
|||
} |
|||
|
|||
public SplitViewPanePlacement PanePlacement => _isLeft ? SplitViewPanePlacement.Left : SplitViewPanePlacement.Right; |
|||
|
|||
public SplitViewDisplayMode CurrentDisplayMode |
|||
{ |
|||
get |
|||
{ |
|||
if (Enum.IsDefined(typeof(SplitViewDisplayMode), _displayMode)) |
|||
{ |
|||
return (SplitViewDisplayMode)_displayMode; |
|||
} |
|||
return SplitViewDisplayMode.CompactOverlay; |
|||
} |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,85 @@ |
|||
namespace Avalonia.Animation.Easings |
|||
{ |
|||
/// <summary>
|
|||
/// Eases a <see cref="double"/> value
|
|||
/// using a user-defined cubic bezier curve.
|
|||
/// Good for custom easing functions that doesn't quite
|
|||
/// fit with the built-in ones.
|
|||
/// </summary>
|
|||
public class SplineEasing : Easing |
|||
{ |
|||
/// <summary>
|
|||
/// X coordinate of the first control point
|
|||
/// </summary>
|
|||
public double X1 |
|||
{ |
|||
get => _internalKeySpline.ControlPointX1; |
|||
set |
|||
{ |
|||
_internalKeySpline.ControlPointX1 = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Y coordinate of the first control point
|
|||
/// </summary>
|
|||
public double Y1 |
|||
{ |
|||
get => _internalKeySpline.ControlPointY1; |
|||
set |
|||
{ |
|||
_internalKeySpline.ControlPointY1 = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// X coordinate of the second control point
|
|||
/// </summary>
|
|||
public double X2 |
|||
{ |
|||
get => _internalKeySpline.ControlPointX2; |
|||
set |
|||
{ |
|||
_internalKeySpline.ControlPointX2 = value; |
|||
} |
|||
} |
|||
|
|||
/// <summary>
|
|||
/// Y coordinate of the second control point
|
|||
/// </summary>
|
|||
public double Y2 |
|||
{ |
|||
get => _internalKeySpline.ControlPointY2; |
|||
set |
|||
{ |
|||
_internalKeySpline.ControlPointY2 = value; |
|||
} |
|||
} |
|||
|
|||
private readonly KeySpline _internalKeySpline; |
|||
|
|||
public SplineEasing(double x1 = 0d, double y1 = 0d, double x2 = 1d, double y2 = 1d) |
|||
{ |
|||
_internalKeySpline = new KeySpline(); |
|||
|
|||
this.X1 = x1; |
|||
this.Y1 = y1; |
|||
this.X2 = x2; |
|||
this.Y1 = y2; |
|||
} |
|||
|
|||
public SplineEasing(KeySpline keySpline) |
|||
{ |
|||
_internalKeySpline = keySpline; |
|||
} |
|||
|
|||
public SplineEasing() |
|||
{ |
|||
_internalKeySpline = new KeySpline(); |
|||
} |
|||
|
|||
/// <inheritdoc/>
|
|||
public override double Ease(double progress) => |
|||
_internalKeySpline.GetSplineProgress(progress); |
|||
} |
|||
} |
|||
@ -0,0 +1,25 @@ |
|||
using System; |
|||
using System.ComponentModel; |
|||
using System.Globalization; |
|||
|
|||
// Ported from WPF open-source code.
|
|||
// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs
|
|||
|
|||
namespace Avalonia.Animation |
|||
{ |
|||
/// <summary>
|
|||
/// Converts string values to <see cref="KeySpline"/> values
|
|||
/// </summary>
|
|||
public class KeySplineTypeConverter : TypeConverter |
|||
{ |
|||
public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType) |
|||
{ |
|||
return sourceType == typeof(string); |
|||
} |
|||
|
|||
public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value) |
|||
{ |
|||
return KeySpline.Parse((string)value, culture); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,487 @@ |
|||
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); |
|||
_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); |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,14 @@ |
|||
using System; |
|||
|
|||
namespace Avalonia.Controls |
|||
{ |
|||
public class SplitViewPaneClosingEventArgs : EventArgs |
|||
{ |
|||
public bool Cancel { get; set; } |
|||
|
|||
public SplitViewPaneClosingEventArgs(bool cancel) |
|||
{ |
|||
Cancel = cancel; |
|||
} |
|||
} |
|||
} |
|||
@ -0,0 +1,219 @@ |
|||
<Styles xmlns="https://github.com/avaloniaui" |
|||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"> |
|||
|
|||
<Styles.Resources> |
|||
<x:Double x:Key="SplitViewOpenPaneThemeLength">320</x:Double> |
|||
<x:Double x:Key="SplitViewCompactPaneThemeLength">48</x:Double> |
|||
|
|||
<!-- Not used here (directly) since they're strings, but preserving for reference |
|||
<x:String x:Key="SplitViewPaneAnimationOpenDuration">00:00:00.2</x:String> |
|||
<x:String x:Key="SplitViewPaneAnimationOpenPreDuration">00:00:00.19999</x:String> |
|||
<x:String x:Key="SplitViewPaneAnimationCloseDuration">00:00:00.1</x:String>--> |
|||
</Styles.Resources> |
|||
|
|||
<Style Selector="SplitView"> |
|||
<Setter Property="OpenPaneLength" Value="{DynamicResource SplitViewOpenPaneThemeLength}" /> |
|||
<Setter Property="CompactPaneLength" Value="{DynamicResource SplitViewCompactPaneThemeLength}" /> |
|||
<Setter Property="PaneBackground" Value="{DynamicResource SystemControlPageBackgroundChromeLowBrush}" /> |
|||
</Style> |
|||
|
|||
<!-- Left --> |
|||
<Style Selector="SplitView:left"> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid Name="Container" Background="{TemplateBinding Background}"> |
|||
<Grid.ColumnDefinitions> |
|||
<!-- why is this throwing a binding error? --> |
|||
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.PaneColumnGridLength}"/> |
|||
<ColumnDefinition Width="*"/> |
|||
</Grid.ColumnDefinitions> |
|||
|
|||
<Panel Name="PART_PaneRoot" Background="{TemplateBinding PaneBackground}" |
|||
ClipToBounds="True" |
|||
HorizontalAlignment="Left" |
|||
ZIndex="100"> |
|||
<Border Child="{TemplateBinding Pane}"/> |
|||
<Rectangle Name="HCPaneBorder" Fill="{DynamicResource SystemControlForegroundTransparentBrush}" Width="1" HorizontalAlignment="Right" /> |
|||
</Panel> |
|||
|
|||
<Panel Name="ContentRoot"> |
|||
<Border Child="{TemplateBinding Content}" /> |
|||
<Rectangle Name="LightDismissLayer"/> |
|||
</Panel> |
|||
|
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<!-- Overlay --> |
|||
<Style Selector="SplitView:overlay:left /template/ Panel#PART_PaneRoot"> |
|||
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" /> |
|||
<!-- ColumnSpan should be 2 --> |
|||
<Setter Property="Grid.ColumnSpan" Value="1"/> |
|||
<Setter Property="Grid.Column" Value="0"/> |
|||
</Style> |
|||
<Style Selector="SplitView:overlay:left /template/ Panel#ContentRoot"> |
|||
<Setter Property="Grid.Column" Value="1"/> |
|||
<Setter Property="Grid.ColumnSpan" Value="2"/> |
|||
</Style> |
|||
|
|||
<!-- CompactInline --> |
|||
<Style Selector="SplitView:compactinline:left /template/ Panel#PART_PaneRoot"> |
|||
<Setter Property="Grid.ColumnSpan" Value="1"/> |
|||
<Setter Property="Grid.Column" Value="0"/> |
|||
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" /> |
|||
</Style> |
|||
<Style Selector="SplitView:compactinline:left /template/ Panel#ContentRoot"> |
|||
<Setter Property="Grid.Column" Value="1"/> |
|||
<Setter Property="Grid.ColumnSpan" Value="1"/> |
|||
</Style> |
|||
|
|||
<!-- CompactOverlay --> |
|||
<Style Selector="SplitView:compactoverlay:left /template/ Panel#PART_PaneRoot"> |
|||
<!-- ColumnSpan should be 2 --> |
|||
<Setter Property="Grid.ColumnSpan" Value="1"/> |
|||
<Setter Property="Grid.Column" Value="0"/> |
|||
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" /> |
|||
</Style> |
|||
<Style Selector="SplitView:compactoverlay:left /template/ Panel#ContentRoot"> |
|||
<Setter Property="Grid.Column" Value="1"/> |
|||
<Setter Property="Grid.ColumnSpan" Value="1"/> |
|||
</Style> |
|||
|
|||
<!-- Inline --> |
|||
<Style Selector="SplitView:inline:left /template/ Panel#PART_PaneRoot"> |
|||
<Setter Property="Grid.ColumnSpan" Value="1"/> |
|||
<Setter Property="Grid.Column" Value="0"/> |
|||
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" /> |
|||
</Style> |
|||
<Style Selector="SplitView:inline:left /template/ Panel#ContentRoot"> |
|||
<Setter Property="Grid.Column" Value="1"/> |
|||
<Setter Property="Grid.ColumnSpan" Value="1"/> |
|||
</Style> |
|||
|
|||
<!-- Right --> |
|||
<Style Selector="SplitView:right"> |
|||
<Setter Property="Template"> |
|||
<ControlTemplate> |
|||
<Grid Name="Container" Background="{TemplateBinding Background}"> |
|||
<Grid.ColumnDefinitions> |
|||
<ColumnDefinition Width="*"/> |
|||
<ColumnDefinition Width="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.PaneColumnGridLength}"/> |
|||
</Grid.ColumnDefinitions> |
|||
|
|||
<Panel Name="PART_PaneRoot" Background="{TemplateBinding PaneBackground}" |
|||
ClipToBounds="True" |
|||
HorizontalAlignment="Right" |
|||
ZIndex="100"> |
|||
<Border Child="{TemplateBinding Pane}"/> |
|||
<Rectangle Name="HCPaneBorder" |
|||
Fill="{DynamicResource SystemControlForegroundTransparentBrush}" |
|||
Width="1" HorizontalAlignment="Left" /> |
|||
</Panel> |
|||
|
|||
<Panel Name="ContentRoot"> |
|||
<Border Child="{TemplateBinding Content}" /> |
|||
<Rectangle Name="LightDismissLayer"/> |
|||
</Panel> |
|||
|
|||
</Grid> |
|||
</ControlTemplate> |
|||
</Setter> |
|||
</Style> |
|||
|
|||
<!-- Overlay --> |
|||
<Style Selector="SplitView:overlay:right /template/ Panel#PART_PaneRoot"> |
|||
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" /> |
|||
<Setter Property="Grid.ColumnSpan" Value="2"/> |
|||
<Setter Property="Grid.Column" Value="1"/> |
|||
</Style> |
|||
<Style Selector="SplitView:overlay:right /template/ Panel#ContentRoot"> |
|||
<Setter Property="Grid.Column" Value="0"/> |
|||
<Setter Property="Grid.ColumnSpan" Value="2"/> |
|||
</Style> |
|||
|
|||
<!-- CompactInline --> |
|||
<Style Selector="SplitView:compactinline:right /template/ Panel#PART_PaneRoot"> |
|||
<Setter Property="Grid.ColumnSpan" Value="1"/> |
|||
<Setter Property="Grid.Column" Value="1"/> |
|||
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" /> |
|||
</Style> |
|||
<Style Selector="SplitView:compactinline:right /template/ Panel#ContentRoot"> |
|||
<Setter Property="Grid.Column" Value="0"/> |
|||
<Setter Property="Grid.ColumnSpan" Value="1"/> |
|||
</Style> |
|||
|
|||
<!-- CompactOverlay --> |
|||
<Style Selector="SplitView:compactoverlay:right /template/ Panel#PART_PaneRoot"> |
|||
<Setter Property="Grid.ColumnSpan" Value="2"/> |
|||
<Setter Property="Grid.Column" Value="1"/> |
|||
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" /> |
|||
</Style> |
|||
<Style Selector="SplitView:compactoverlay:right /template/ Panel#ContentRoot"> |
|||
<Setter Property="Grid.Column" Value="0"/> |
|||
<Setter Property="Grid.ColumnSpan" Value="1"/> |
|||
</Style> |
|||
|
|||
<!-- Inline --> |
|||
<Style Selector="SplitView:inline:right /template/ Panel#PART_PaneRoot"> |
|||
<Setter Property="Grid.ColumnSpan" Value="1"/> |
|||
<Setter Property="Grid.Column" Value="1"/> |
|||
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" /> |
|||
</Style> |
|||
<Style Selector="SplitView:inline:right /template/ Panel#ContentRoot"> |
|||
<Setter Property="Grid.Column" Value="0"/> |
|||
<Setter Property="Grid.ColumnSpan" Value="1"/> |
|||
</Style> |
|||
|
|||
<!-- Open/Close Pane animation --> |
|||
<Style Selector="SplitView:open /template/ Panel#PART_PaneRoot"> |
|||
<Setter Property="Transitions"> |
|||
<Transitions> |
|||
<DoubleTransition Property="Width" Duration="00:00:00.2" Easing="0.1,0.9,0.2,1.0" /> |
|||
</Transitions> |
|||
</Setter> |
|||
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=OpenPaneLength}" /> |
|||
</Style> |
|||
<Style Selector="SplitView:open /template/ Rectangle#LightDismissLayer"> |
|||
<Setter Property="Transitions"> |
|||
<Transitions> |
|||
<DoubleTransition Property="Opacity" Duration="00:00:00.2" Easing="0.1,0.9,0.2,1.0" /> |
|||
</Transitions> |
|||
</Setter> |
|||
<Setter Property="Opacity" Value="1.0"/> |
|||
</Style> |
|||
|
|||
<Style Selector="SplitView:closed /template/ Panel#PART_PaneRoot"> |
|||
<Setter Property="Transitions"> |
|||
<Transitions> |
|||
<DoubleTransition Property="Width" Duration="00:00:00.1" Easing="0.1,0.9,0.2,1.0" /> |
|||
</Transitions> |
|||
</Setter> |
|||
<Setter Property="Width" Value="{Binding RelativeSource={RelativeSource TemplatedParent}, Path=TemplateSettings.ClosedPaneWidth}" /> |
|||
</Style> |
|||
<Style Selector="SplitView:closed /template/ Rectangle#LightDismissLayer"> |
|||
<Setter Property="Transitions"> |
|||
<Transitions> |
|||
<DoubleTransition Property="Opacity" Duration="00:00:00.2" Easing="0.1,0.9,0.2,1.0" /> |
|||
</Transitions> |
|||
</Setter> |
|||
<Setter Property="Opacity" Value="0.0"/> |
|||
</Style> |
|||
|
|||
<Style Selector="SplitView /template/ Rectangle#LightDismissLayer"> |
|||
<Setter Property="IsVisible" Value="False"/> |
|||
<Setter Property="Fill" Value="Transparent" /> |
|||
</Style> |
|||
<Style Selector="SplitView:lightdismiss /template/ Rectangle#LightDismissLayer"> |
|||
<Setter Property="Fill" Value="{DynamicResource SplitViewLightDismissOverlayBackground}" /> |
|||
</Style> |
|||
|
|||
<Style Selector="SplitView:overlay:open /template/ Rectangle#LightDismissLayer"> |
|||
<Setter Property="IsVisible" Value="True"/> |
|||
</Style> |
|||
<Style Selector="SplitView:compactoverlay:open /template/ Rectangle#LightDismissLayer"> |
|||
<Setter Property="IsVisible" Value="True"/> |
|||
</Style> |
|||
|
|||
</Styles> |
|||
@ -0,0 +1,66 @@ |
|||
using System; |
|||
using System.Collections.Generic; |
|||
using System.Text; |
|||
using Xunit; |
|||
|
|||
namespace Avalonia.Controls.UnitTests |
|||
{ |
|||
|
|||
public class SplitViewTests |
|||
{ |
|||
[Fact] |
|||
public void SplitView_PaneOpening_Should_Fire_Before_PaneOpened() |
|||
{ |
|||
var splitView = new SplitView(); |
|||
|
|||
bool handledOpening = false; |
|||
splitView.PaneOpening += (x, e) => |
|||
{ |
|||
handledOpening = true; |
|||
}; |
|||
|
|||
splitView.PaneOpened += (x, e) => |
|||
{ |
|||
Assert.True(handledOpening); |
|||
}; |
|||
|
|||
splitView.IsPaneOpen = true; |
|||
} |
|||
|
|||
[Fact] |
|||
public void SplitView_PaneClosing_Should_Fire_Before_PaneClosed() |
|||
{ |
|||
var splitView = new SplitView(); |
|||
splitView.IsPaneOpen = true; |
|||
|
|||
bool handledClosing = false; |
|||
splitView.PaneClosing += (x, e) => |
|||
{ |
|||
handledClosing = true; |
|||
}; |
|||
|
|||
splitView.PaneClosed += (x, e) => |
|||
{ |
|||
Assert.True(handledClosing); |
|||
}; |
|||
|
|||
splitView.IsPaneOpen = false; |
|||
} |
|||
|
|||
[Fact] |
|||
public void SplitView_Cancel_Close_Should_Prevent_Pane_From_Closing() |
|||
{ |
|||
var splitView = new SplitView(); |
|||
splitView.IsPaneOpen = true; |
|||
|
|||
splitView.PaneClosing += (x, e) => |
|||
{ |
|||
e.Cancel = true; |
|||
}; |
|||
|
|||
splitView.IsPaneOpen = false; |
|||
|
|||
Assert.True(splitView.IsPaneOpen); |
|||
} |
|||
} |
|||
} |
|||
Loading…
Reference in new issue