Browse Source

Merge pull request #10697 from amwx/fix/SplitViewOverlay

SplitView fixes
pull/10725/head
Dan Walmsley 3 years ago
committed by GitHub
parent
commit
b24960537c
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 230
      src/Avalonia.Controls/SplitView/SplitView.cs
  2. 224
      tests/Avalonia.Controls.UnitTests/SplitViewTests.cs

230
src/Avalonia.Controls/SplitView/SplitView.cs

@ -8,6 +8,7 @@ using Avalonia.Interactivity;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.Reactive;
namespace Avalonia.Controls
{
@ -15,18 +16,21 @@ namespace Avalonia.Controls
/// A control with two views: A collapsible pane and an area for content
/// </summary>
[TemplatePart("PART_PaneRoot", typeof(Panel))]
[PseudoClasses(":open", ":closed")]
[PseudoClasses(":compactoverlay", ":compactinline", ":overlay", ":inline")]
[PseudoClasses(":left", ":right")]
[PseudoClasses(":lightdismiss")]
[PseudoClasses(pcOpen, pcClosed)]
[PseudoClasses(pcCompactOverlay, pcCompactInline, pcOverlay, pcInline)]
[PseudoClasses(pcLeft, pcRight)]
[PseudoClasses(pcLightDismiss)]
public class SplitView : ContentControl
{
/*
Pseudo classes & combos
:open / :closed
:compactoverlay :compactinline :overlay :inline
:left :right
*/
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
@ -94,8 +98,9 @@ namespace Avalonia.Controls
/// <summary>
/// Defines the <see cref="TemplateSettings"/> property
/// </summary>
public static readonly StyledProperty<SplitViewTemplateSettings> TemplateSettingsProperty =
AvaloniaProperty.Register<SplitView, SplitViewTemplateSettings>(nameof(TemplateSettings));
public static readonly DirectProperty<SplitView, SplitViewTemplateSettings> TemplateSettingsProperty =
AvaloniaProperty.RegisterDirect<SplitView, SplitViewTemplateSettings>(nameof(TemplateSettings),
x => x.TemplateSettings);
/// <summary>
/// Defines the <see cref="PaneClosed"/> event.
@ -131,18 +136,9 @@ namespace Avalonia.Controls
private Panel? _pane;
private IDisposable? _pointerDisposable;
public SplitView()
{
PseudoClasses.Add(":overlay");
PseudoClasses.Add(":left");
TemplateSettings = new SplitViewTemplateSettings();
}
static SplitView()
{
}
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"/>
@ -208,7 +204,7 @@ namespace Avalonia.Controls
get => GetValue(PaneProperty);
set => SetValue(PaneProperty, value);
}
/// <summary>
/// Gets or sets the data template used to display the header content of the control.
/// </summary>
@ -235,8 +231,8 @@ namespace Avalonia.Controls
/// </summary>
public SplitViewTemplateSettings TemplateSettings
{
get => GetValue(TemplateSettingsProperty);
set => SetValue(TemplateSettingsProperty, value);
get => _templateSettings;
private set => SetAndRaise(TemplateSettingsProperty, ref _templateSettings, value);
}
/// <summary>
@ -291,7 +287,7 @@ namespace Avalonia.Controls
{
return true;
}
return result;
}
@ -299,17 +295,18 @@ namespace Avalonia.Controls
{
base.OnApplyTemplate(e);
_pane = e.NameScope.Find<Panel>("PART_PaneRoot");
UpdateVisualStateForDisplayMode(DisplayMode);
}
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);
}
// :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)
@ -325,36 +322,11 @@ namespace Avalonia.Controls
if (change.Property == CompactPaneLengthProperty)
{
var newLen = change.GetNewValue<double>();
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);
}
UpdateVisualStateForCompactPaneLength(change.GetNewValue<double>());
}
else if (change.Property == DisplayModeProperty)
{
var oldState = GetPseudoClass(change.GetOldValue<SplitViewDisplayMode>());
var newState = GetPseudoClass(change.GetNewValue<SplitViewDisplayMode>());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
var (closedPaneWidth, paneColumnGridLength) = change.GetNewValue<SplitViewDisplayMode>() 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;
UpdateVisualStateForDisplayMode(change.GetNewValue<SplitViewDisplayMode>());
}
else if (change.Property == IsPaneOpenProperty)
{
@ -362,15 +334,15 @@ namespace Avalonia.Controls
if (isPaneOpen)
{
PseudoClasses.Add(":open");
PseudoClasses.Remove(":closed");
PseudoClasses.Add(pcOpen);
PseudoClasses.Remove(pcClosed);
OnPaneOpened(new RoutedEventArgs(PaneOpenedEvent, this));
}
else
{
PseudoClasses.Add(":closed");
PseudoClasses.Remove(":open");
PseudoClasses.Add(pcClosed);
PseudoClasses.Remove(pcOpen);
OnPaneClosed(new RoutedEventArgs(PaneClosedEvent, this));
}
@ -389,33 +361,37 @@ namespace Avalonia.Controls
}
else if (change.Property == PanePlacementProperty)
{
var oldState = GetPseudoClass(change.GetOldValue<SplitViewPanePlacement>());
var newState = GetPseudoClass(change.GetNewValue<SplitViewPanePlacement>());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
UpdateVisualStateForPanePlacementProperty(change.GetNewValue<SplitViewPanePlacement>());
}
else if (change.Property == UseLightDismissOverlayModeProperty)
{
var mode = change.GetNewValue<bool>();
PseudoClasses.Set(":lightdismiss", mode);
PseudoClasses.Set(pcLightDismiss, mode);
}
}
private void PointerPressedOutside(object? sender, PointerPressedEventArgs e)
protected override void OnKeyDown(KeyEventArgs e)
{
if (!IsPaneOpen)
if (!e.Handled && e.Key == Key.Escape)
{
return;
if (IsPaneOpen && IsInOverlayMode())
{
SetCurrentValue(IsPaneOpenProperty, false);
e.Handled = true;
}
}
//If we click within the Pane, don't do anything
//Otherwise, ClosePane if open & using an overlay display mode
bool closePane = ShouldClosePane();
if (!closePane)
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)
{
@ -432,14 +408,15 @@ namespace Avalonia.Controls
src = src.VisualParent;
}
if (closePane)
{
SetCurrentValue(IsPaneOpenProperty, false);
e.Handled = true;
}
}
private bool ShouldClosePane()
private bool IsInOverlayMode()
{
return (DisplayMode == SplitViewDisplayMode.CompactOverlay || DisplayMode == SplitViewDisplayMode.Overlay);
}
@ -451,6 +428,7 @@ namespace Avalonia.Controls
protected virtual void OnPaneOpened(RoutedEventArgs args)
{
EnableLightDismiss();
RaiseEvent(args);
}
@ -461,6 +439,8 @@ namespace Avalonia.Controls
protected virtual void OnPaneClosed(RoutedEventArgs args)
{
_pointerDisposable?.Dispose();
_pointerDisposable = null;
RaiseEvent(args);
}
@ -471,14 +451,14 @@ namespace Avalonia.Controls
{
return mode switch
{
SplitViewDisplayMode.Inline => "inline",
SplitViewDisplayMode.CompactInline => "compactinline",
SplitViewDisplayMode.Overlay => "overlay",
SplitViewDisplayMode.CompactOverlay => "compactoverlay",
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>
@ -486,8 +466,8 @@ namespace Avalonia.Controls
{
return placement switch
{
SplitViewPanePlacement.Left => "left",
SplitViewPanePlacement.Right => "right",
SplitViewPanePlacement.Left => pcLeft,
SplitViewPanePlacement.Right => pcRight,
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null)
};
}
@ -519,6 +499,86 @@ namespace Avalonia.Controls
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>

224
tests/Avalonia.Controls.UnitTests/SplitViewTests.cs

@ -1,6 +1,5 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Input;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
@ -62,5 +61,224 @@ namespace Avalonia.Controls.UnitTests
Assert.True(splitView.IsPaneOpen);
}
[Fact]
public void SplitView_TemplateSettings_Are_Correct_For_Display_Modes()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow);
var wnd = new Window
{
Width = 1280,
Height = 720
};
var splitView = new SplitView();
wnd.Content = splitView;
wnd.Show();
var zeroGridLength = new GridLength(0);
var compactLength = splitView.CompactPaneLength;
var compactGridLength = new GridLength(compactLength);
// Overlay is default DisplayMode
Assert.Equal(0, splitView.TemplateSettings.ClosedPaneWidth);
Assert.Equal(zeroGridLength, splitView.TemplateSettings.PaneColumnGridLength);
splitView.DisplayMode = SplitViewDisplayMode.CompactOverlay;
Assert.Equal(compactLength, splitView.TemplateSettings.ClosedPaneWidth);
Assert.Equal(compactGridLength, splitView.TemplateSettings.PaneColumnGridLength);
splitView.DisplayMode = SplitViewDisplayMode.Inline;
Assert.Equal(0, splitView.TemplateSettings.ClosedPaneWidth);
Assert.Equal(GridLength.Auto, splitView.TemplateSettings.PaneColumnGridLength);
splitView.DisplayMode = SplitViewDisplayMode.CompactInline;
Assert.Equal(compactLength, splitView.TemplateSettings.ClosedPaneWidth);
Assert.Equal(GridLength.Auto, splitView.TemplateSettings.PaneColumnGridLength);
}
[Fact]
public void SplitView_TemplateSettings_Update_With_CompactPaneLength()
{
var splitView = new SplitView();
// CompactInline:
// - ClosedPaneWidth = CompactPaneLength
// - PaneColumnGridLength = Auto
splitView.DisplayMode = SplitViewDisplayMode.CompactInline;
var compactLength = splitView.CompactPaneLength;
Assert.Equal(GridLength.Auto, splitView.TemplateSettings.PaneColumnGridLength);
Assert.Equal(compactLength, splitView.TemplateSettings.ClosedPaneWidth);
splitView.CompactPaneLength = 100;
Assert.Equal(GridLength.Auto, splitView.TemplateSettings.PaneColumnGridLength);
Assert.Equal(100, splitView.TemplateSettings.ClosedPaneWidth);
// CompactOverlay:
// - ClosedPaneWidth = CompactPaneLength
// - PaneColumnGridLength = GridLength { CompactPaneLength, Pixel }
splitView.DisplayMode = SplitViewDisplayMode.CompactOverlay;
splitView.CompactPaneLength = 50;
Assert.Equal(new GridLength(50), splitView.TemplateSettings.PaneColumnGridLength);
Assert.Equal(50, splitView.TemplateSettings.ClosedPaneWidth);
// Value shouldn't change for these - changing the display mode will update
// the template settings with the right value
splitView.DisplayMode = SplitViewDisplayMode.Inline;
splitView.CompactPaneLength = 1;
Assert.Equal(GridLength.Auto, splitView.TemplateSettings.PaneColumnGridLength);
Assert.Equal(0, splitView.TemplateSettings.ClosedPaneWidth);
splitView.DisplayMode = SplitViewDisplayMode.Overlay;
splitView.CompactPaneLength = 2;
Assert.Equal(new GridLength(0), splitView.TemplateSettings.PaneColumnGridLength);
Assert.Equal(0, splitView.TemplateSettings.ClosedPaneWidth);
}
[Fact]
public void SplitView_Pointer_Closes_Pane_In_Overlay_Mode()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow
.With(globalClock: new MockGlobalClock()));
var wnd = new Window
{
Width = 1280,
Height = 720
};
var splitView = new SplitView();
wnd.Content = splitView;
wnd.Show();
splitView.IsPaneOpen = true;
splitView.RaiseEvent(new PointerReleasedEventArgs(splitView,
null, wnd, new Point(1270, 30), 0,
new PointerPointProperties(),
KeyModifiers.None,
MouseButton.Left));
Assert.False(splitView.IsPaneOpen);
// Inline shouldn't close the pane
splitView.DisplayMode = SplitViewDisplayMode.Inline;
splitView.IsPaneOpen = true;
splitView.RaiseEvent(new PointerReleasedEventArgs(splitView,
null, wnd, new Point(1270, 30), 0,
new PointerPointProperties(),
KeyModifiers.None,
MouseButton.Left));
Assert.True(splitView.IsPaneOpen);
}
[Fact]
public void SplitView_Pointer_Should_Not_Close_Pane_If_Over_Pane()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow
.With(globalClock: new MockGlobalClock()));
var wnd = new Window
{
Width = 1280,
Height = 720
};
var clickBorder = new Border
{
Width = 100,
Height = 100,
HorizontalAlignment = Layout.HorizontalAlignment.Left,
VerticalAlignment = Layout.VerticalAlignment.Top
};
var splitView = new SplitView
{
Pane = clickBorder
};
wnd.Content = splitView;
wnd.Show();
splitView.IsPaneOpen = true;
clickBorder.RaiseEvent(new PointerReleasedEventArgs(splitView,
null, wnd, new Point(5, 5), 0,
new PointerPointProperties(),
KeyModifiers.None,
MouseButton.Left));
Assert.True(splitView.IsPaneOpen);
}
[Fact]
public void SplitView_Escape_Key_Closes_Light_Dismissable_Pane()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow
.With(globalClock: new MockGlobalClock()));
var wnd = new Window
{
Width = 1280,
Height = 720
};
var button = new Button();
var splitView = new SplitView
{
Pane = button
};
wnd.Content = splitView;
wnd.Show();
splitView.IsPaneOpen = true;
button.RaiseEvent(new KeyEventArgs
{
Key = Key.Escape,
RoutedEvent = InputElement.KeyDownEvent
});
Assert.False(splitView.IsPaneOpen);
splitView.DisplayMode = SplitViewDisplayMode.Inline;
splitView.IsPaneOpen = true;
button.RaiseEvent(new KeyEventArgs
{
Key = Key.Escape,
RoutedEvent = InputElement.KeyDownEvent
});
Assert.True(splitView.IsPaneOpen);
}
[Fact]
public void Top_Level_Back_Requested_Closes_Light_Dismissable_Pane()
{
using var app = UnitTestApplication.Start(TestServices.StyledWindow
.With(globalClock: new MockGlobalClock()));
var wnd = new Window
{
Width = 1280,
Height = 720
};
var splitView = new SplitView();
wnd.Content = splitView;
wnd.Show();
splitView.IsPaneOpen = true;
wnd.RaiseEvent(new Interactivity.RoutedEventArgs(TopLevel.BackRequestedEvent));
Assert.False(splitView.IsPaneOpen);
splitView.DisplayMode = SplitViewDisplayMode.Inline;
splitView.IsPaneOpen = true;
wnd.RaiseEvent(new Interactivity.RoutedEventArgs(TopLevel.BackRequestedEvent));
Assert.True(splitView.IsPaneOpen);
}
}
}

Loading…
Cancel
Save