Browse Source

Improve CommandBar dynamic overflow separator handling (#21039)

* Improve CommandBar dynamic overflow separator handling

* More changes

* Updated sample

---------

Co-authored-by: Julien Lebosquain <julien@lebosquain.net>
release/12.0.0-rc2
Javier Suárez 5 months ago
committed by GitHub
parent
commit
3a5b3e376e
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 28
      samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml
  2. 64
      samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs
  3. 154
      src/Avalonia.Controls/CommandBar/CommandBar.cs
  4. 311
      tests/Avalonia.Controls.UnitTests/CommandBarTests.cs

28
samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml

@ -8,6 +8,7 @@
<StreamGeometry x:Key="BoldIcon">M15.6,10.79C17.04,10.07 18,8.64 18,7C18,4.79 16.21,3 14,3H7V21H14.73C16.78,21 18.5,19.37 18.5,17.32C18.5,15.82 17.72,14.53 16.5,13.77C16.2,13.59 15.9,13.44 15.6,13.32V10.79M10,6.5H13C13.83,6.5 14.5,7.17 14.5,8C14.5,8.83 13.83,9.5 13,9.5H10V6.5M13.5,17.5H10V14H13.5C14.33,14 15,14.67 15,15.5C15,16.33 14.33,17.5 13.5,17.5Z</StreamGeometry>
<StreamGeometry x:Key="ItalicIcon">M10,4V7H12.21L8.79,15H6V18H14V15H11.79L15.21,7H18V4H10Z</StreamGeometry>
<StreamGeometry x:Key="DeleteIcon">M19,4H15.5L14.5,3H9.5L8.5,4H5V6H19M6,19A2,2 0 0,0 8,21H16A2,2 0 0,0 18,19V7H6V19Z</StreamGeometry>
<StreamGeometry x:Key="ExportIcon">M14,2H6A2,2 0 0,0 4,4V20A2,2 0 0,0 6,22H18A2,2 0 0,0 20,20V8L14,2M18,20H6V4H13V9H18V20M16,11V18.1L13.9,16L11.1,18.8L8.3,16L11.1,13.2L9,11.1L16,11Z</StreamGeometry>
</UserControl.Resources>
<DockPanel>
@ -33,6 +34,18 @@
IsChecked="True"
IsCheckedChanged="OnDynamicOverflowChanged" />
<CheckBox x:Name="SecondaryVisibleCheck"
Content="Include secondary command"
IsChecked="False"
IsCheckedChanged="OnSecondaryVisibilityChanged" />
<Separator />
<TextBlock Text="Resize to force grouped commands into overflow and verify that primary separators never become leading, trailing, or overflow items."
FontSize="12"
Opacity="0.7"
TextWrapping="Wrap" />
<Separator />
<TextBlock Text="Overflow Priority" FontWeight="SemiBold" />
@ -47,11 +60,11 @@
<Border DockPanel.Dock="Right" Width="1" Background="{DynamicResource SystemControlForegroundBaseMediumLowBrush}" />
<StackPanel Margin="12" Spacing="8">
<TextBlock x:Name="StatusText"
Text="Showing 6 of 6 commands, 0 in overflow"
FontSize="12"
Opacity="0.7" />
<StackPanel Margin="12" Spacing="8">
<TextBlock x:Name="StatusText"
Text="Visible primary: 6 commands, 2 separators&#x0a;Overflow items: 2 commands, 1 separator&#x0a;Synthetic overflow divider: present"
FontSize="12"
Opacity="0.7" />
<Border x:Name="BarContainer"
Width="400"
HorizontalAlignment="Left"
@ -63,11 +76,16 @@
<CommandBar.PrimaryCommands>
<CommandBarButton Label="New" DynamicOverflowOrder="0"><CommandBarButton.Icon><PathIcon Data="{StaticResource AddIcon}" /></CommandBarButton.Icon></CommandBarButton>
<CommandBarButton Label="Save" DynamicOverflowOrder="0"><CommandBarButton.Icon><PathIcon Data="{StaticResource SaveIcon}" /></CommandBarButton.Icon></CommandBarButton>
<CommandBarSeparator />
<CommandBarButton Label="Share" DynamicOverflowOrder="1"><CommandBarButton.Icon><PathIcon Data="{StaticResource ShareIcon}" /></CommandBarButton.Icon></CommandBarButton>
<CommandBarButton Label="Bold" DynamicOverflowOrder="2"><CommandBarButton.Icon><PathIcon Data="{StaticResource BoldIcon}" /></CommandBarButton.Icon></CommandBarButton>
<CommandBarSeparator />
<CommandBarButton Label="Italic" DynamicOverflowOrder="2"><CommandBarButton.Icon><PathIcon Data="{StaticResource ItalicIcon}" /></CommandBarButton.Icon></CommandBarButton>
<CommandBarButton Label="Delete" DynamicOverflowOrder="3"><CommandBarButton.Icon><PathIcon Data="{StaticResource DeleteIcon}" /></CommandBarButton.Icon></CommandBarButton>
</CommandBar.PrimaryCommands>
<CommandBar.SecondaryCommands>
<CommandBarButton x:Name="DemoSecondaryCommand" Label="Export"><CommandBarButton.Icon><PathIcon Data="{StaticResource ExportIcon}" /></CommandBarButton.Icon></CommandBarButton>
</CommandBar.SecondaryCommands>
</CommandBar>
</Border>
</StackPanel>

64
samples/ControlCatalog/Pages/CommandBar/CommandBarDynamicOverflowPage.xaml.cs

@ -9,7 +9,13 @@ namespace ControlCatalog.Pages
public CommandBarDynamicOverflowPage()
{
InitializeComponent();
if (SecondaryVisibleCheck.IsChecked != true)
{
DemoBar.SecondaryCommands?.Remove(DemoSecondaryCommand);
}
((INotifyCollectionChanged)DemoBar.OverflowItems).CollectionChanged += OnOverflowChanged;
((INotifyCollectionChanged)DemoBar.VisiblePrimaryCommands).CollectionChanged += OnOverflowChanged;
UpdateStatus();
}
@ -29,6 +35,26 @@ namespace ControlCatalog.Pages
DemoBar.IsDynamicOverflowEnabled = DynamicOverflowCheck.IsChecked == true;
}
private void OnSecondaryVisibilityChanged(object? sender, RoutedEventArgs e)
{
if (DemoBar?.SecondaryCommands == null || DemoSecondaryCommand == null)
return;
bool shouldInclude = SecondaryVisibleCheck.IsChecked == true;
bool isIncluded = DemoBar.SecondaryCommands.Contains(DemoSecondaryCommand);
if (shouldInclude && !isIncluded)
{
DemoBar.SecondaryCommands.Add(DemoSecondaryCommand);
}
else if (!shouldInclude && isIncluded)
{
DemoBar.SecondaryCommands.Remove(DemoSecondaryCommand);
}
UpdateStatus();
}
private void OnOverflowChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
UpdateStatus();
@ -36,10 +62,40 @@ namespace ControlCatalog.Pages
private void UpdateStatus()
{
var total = DemoBar.PrimaryCommands.Count;
var overflow = DemoBar.OverflowItems.Count;
var visible = total - overflow;
StatusText.Text = $"Showing {visible} of {total} commands, {overflow} in overflow";
int visiblePrimaryCommandCount = 0;
int visiblePrimarySeparatorCount = 0;
foreach (var item in DemoBar.VisiblePrimaryCommands)
{
if (item is CommandBarSeparator)
visiblePrimarySeparatorCount++;
else
visiblePrimaryCommandCount++;
}
int overflowCommandCount = 0;
int overflowSeparatorCount = 0;
bool hasSyntheticOverflowDivider = false;
foreach (var item in DemoBar.OverflowItems)
{
if (item is CommandBarSeparator separator)
{
overflowSeparatorCount++;
if (!DemoBar.PrimaryCommands.Contains(separator) &&
!DemoBar.SecondaryCommands.Contains(separator))
{
hasSyntheticOverflowDivider = true;
}
}
else
{
overflowCommandCount++;
}
}
StatusText.Text =
$"Visible primary: {visiblePrimaryCommandCount} commands, {visiblePrimarySeparatorCount} separators\n" +
$"Overflow items: {overflowCommandCount} commands, {overflowSeparatorCount} separators\n" +
$"Synthetic overflow divider: {(hasSyntheticOverflowDivider ? "present" : "absent")}";
}
}
}

154
src/Avalonia.Controls/CommandBar/CommandBar.cs

@ -7,6 +7,7 @@ using Avalonia.Controls.Metadata;
using Avalonia.Controls.Primitives;
using Avalonia.Interactivity;
using Avalonia.Metadata;
using Avalonia.Reactive;
using Avalonia.Threading;
namespace Avalonia.Controls
@ -136,6 +137,8 @@ namespace Avalonia.Controls
private readonly ObservableCollection<ICommandBarElement> _visiblePrimaryCommands = new();
private readonly ObservableCollection<ICommandBarElement> _overflowItems = new();
private readonly CommandBarSeparator _overflowPrimarySecondarySeparator = new();
private readonly CompositeDisposable _secondaryCommandVisibilitySubscriptions = new();
private bool _isDynamicUpdateInProgress;
private double _constraintWidth = double.PositiveInfinity;
private bool _openedViaKeyboard;
@ -151,6 +154,7 @@ namespace Avalonia.Controls
var secondaryCommands = new ObservableCollection<ICommandBarElement>();
SetCurrentValue(SecondaryCommandsProperty, (IList<ICommandBarElement>)secondaryCommands);
RebuildSecondaryCommandVisibilitySubscriptions();
SizeChanged += CommandBar_SizeChanged;
}
@ -386,6 +390,18 @@ namespace Avalonia.Controls
UpdateDynamicOverflow();
}
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
base.OnAttachedToVisualTree(e);
RebuildSecondaryCommandVisibilitySubscriptions();
}
protected override void OnDetachedFromVisualTree(VisualTreeAttachmentEventArgs e)
{
_secondaryCommandVisibilitySubscriptions.Clear();
base.OnDetachedFromVisualTree(e);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
@ -439,6 +455,7 @@ namespace Avalonia.Controls
oldSecondary.CollectionChanged -= OnSecondaryCommandsChanged;
if (change.NewValue is INotifyCollectionChanged newSecondary)
newSecondary.CollectionChanged += OnSecondaryCommandsChanged;
RebuildSecondaryCommandVisibilitySubscriptions();
UpdateDynamicOverflow();
}
}
@ -583,6 +600,7 @@ namespace Avalonia.Controls
private void OnSecondaryCommandsChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RebuildSecondaryCommandVisibilitySubscriptions();
UpdateDynamicOverflow();
}
@ -598,12 +616,9 @@ namespace Avalonia.Controls
{
_visiblePrimaryCommands.Clear();
_overflowItems.Clear();
SetOverflowMode(_overflowPrimarySecondarySeparator, false);
foreach (var item in SecondaryCommands)
{
SetOverflowMode(item, true);
_overflowItems.Add(item);
}
var overflowedPrimaryCommands = new List<ICommandBarElement>();
var availableWidth = double.IsFinite(_constraintWidth) ? _constraintWidth : Bounds.Width;
@ -669,8 +684,6 @@ namespace Avalonia.Controls
? a.Order.CompareTo(b.Order)
: a.Index.CompareTo(b.Index));
// Separators stay in the primary bar but are not counted toward maxItems.
// If no non-separator buttons fit, separators are moved to overflow too.
var visibleIndices = new HashSet<int>();
int nonSeparatorCount = 0;
for (var i = 0; i < prioritized.Count; i++)
@ -688,6 +701,8 @@ namespace Avalonia.Controls
if (nonSeparatorCount == 0)
visibleIndices.Clear();
TrimOrphanedSeparatorsFromVisibleCommands(PrimaryCommands, visibleIndices);
for (var i = 0; i < PrimaryCommands.Count; i++)
{
if (visibleIndices.Contains(i))
@ -695,15 +710,20 @@ namespace Avalonia.Controls
SetOverflowMode(PrimaryCommands[i], false);
_visiblePrimaryCommands.Add(PrimaryCommands[i]);
}
else if (PrimaryCommands[i] is CommandBarSeparator)
{
SetOverflowMode(PrimaryCommands[i], false);
}
else
{
SetOverflowMode(PrimaryCommands[i], true);
_overflowItems.Add(PrimaryCommands[i]);
overflowedPrimaryCommands.Add(PrimaryCommands[i]);
}
}
}
}
AddOverflowItems(overflowedPrimaryCommands);
HasSecondaryCommands = _overflowItems.Count > 0;
UpdateOverflowButtonVisibility();
}
@ -731,6 +751,8 @@ namespace Avalonia.Controls
if (SecondaryCommands != null)
foreach (var cmd in SecondaryCommands)
ApplyLabelPositionToElement(cmd);
ApplyLabelPositionToElement(_overflowPrimarySecondarySeparator);
}
private void ApplyLabelPositionToElement(ICommandBarElement element)
@ -758,5 +780,121 @@ namespace Avalonia.Controls
_ => HasSecondaryCommands // Auto
};
}
private void AddOverflowItems(IReadOnlyList<ICommandBarElement> overflowedPrimaryCommands)
{
for (var i = 0; i < overflowedPrimaryCommands.Count; i++)
_overflowItems.Add(overflowedPrimaryCommands[i]);
if (overflowedPrimaryCommands.Count > 0 && HasVisibleElements(SecondaryCommands))
{
SetOverflowMode(_overflowPrimarySecondarySeparator, true);
_overflowItems.Add(_overflowPrimarySecondarySeparator);
}
foreach (var item in SecondaryCommands)
{
SetOverflowMode(item, true);
_overflowItems.Add(item);
}
}
private void RebuildSecondaryCommandVisibilitySubscriptions()
{
_secondaryCommandVisibilitySubscriptions.Clear();
if (SecondaryCommands is null)
return;
for (var i = 0; i < SecondaryCommands.Count; i++)
{
if (SecondaryCommands[i] is Avalonia.Visual visual)
{
bool isInitialValue = true;
visual.GetObservable(Avalonia.Visual.IsVisibleProperty)
.Subscribe(_ =>
{
if (isInitialValue)
{
isInitialValue = false;
return;
}
UpdateDynamicOverflow();
})
.DisposeWith(_secondaryCommandVisibilitySubscriptions);
}
}
}
private static bool HasVisibleElements(IList<ICommandBarElement> commands)
{
for (var i = 0; i < commands.Count; i++)
{
if (commands[i] is Avalonia.Visual visual && visual.IsVisible)
return true;
}
return false;
}
private static void TrimOrphanedSeparatorsFromVisibleCommands(
IList<ICommandBarElement> commands, HashSet<int> visibleIndices)
{
var toRemove = new List<int>();
for (var i = 0; i < commands.Count; i++)
{
if (!visibleIndices.Contains(i) || commands[i] is not CommandBarSeparator)
continue;
bool hasNonSeparatorBefore = FindNonSeparatorInVisibleCommands(
commands, visibleIndices, forward: false, startIndex: i - 1, out _);
bool hasNonSeparatorAfter = FindNonSeparatorInVisibleCommands(
commands, visibleIndices, forward: true, startIndex: i + 1, out _);
if (!hasNonSeparatorBefore || !hasNonSeparatorAfter)
toRemove.Add(i);
}
foreach (var idx in toRemove)
visibleIndices.Remove(idx);
bool previousWasSeparator = false;
for (var i = 0; i < commands.Count; i++)
{
if (!visibleIndices.Contains(i))
continue;
if (commands[i] is CommandBarSeparator)
{
if (previousWasSeparator)
visibleIndices.Remove(i);
else
previousWasSeparator = true;
}
else
{
previousWasSeparator = false;
}
}
}
private static bool FindNonSeparatorInVisibleCommands(
IList<ICommandBarElement> commands, HashSet<int> visibleIndices,
bool forward, int startIndex, out int foundIndex)
{
foundIndex = -1;
var i = startIndex;
while (forward ? i < commands.Count : i >= 0)
{
if (visibleIndices.Contains(i) && commands[i] is not CommandBarSeparator)
{
foundIndex = i;
return true;
}
i += forward ? 1 : -1;
}
return false;
}
}
}

311
tests/Avalonia.Controls.UnitTests/CommandBarTests.cs

@ -874,13 +874,17 @@ public class CommandBarItemWidthTests : ScopedTestBase
public void ItemWidthBottom_Controls_HowManyButtonsFit()
{
var cb = CreateWithWidth(300);
cb.SecondaryCommands!.Add(new CommandBarButton()); // forces overflow button
var secondary = new CommandBarButton();
cb.SecondaryCommands!.Add(secondary); // forces overflow button
for (int i = 0; i < 4; i++)
cb.PrimaryCommands!.Add(new CommandBarButton());
cb.IsDynamicOverflowEnabled = true;
Assert.Equal(3, cb.VisiblePrimaryCommands.Count);
Assert.Equal(1, cb.OverflowItems.Count - 1); // -1 for the secondary command
Assert.Equal(3, cb.OverflowItems.Count);
Assert.IsType<CommandBarButton>(cb.OverflowItems[0]);
Assert.IsType<CommandBarSeparator>(cb.OverflowItems[1]);
Assert.Same(secondary, cb.OverflowItems[2]);
}
[Fact]
@ -1196,6 +1200,309 @@ public class CommandBarOverflowKeyboardTests : ScopedTestBase
}
}
public class CommandBarSeparatorOverflowTests : ScopedTestBase
{
private static CommandBar CreateWithWidth(double width)
{
var cb = new CommandBar();
cb.Measure(new Size(width, double.PositiveInfinity));
return cb;
}
[Fact]
public void TrailingSeparator_IsNotLastVisibleItem()
{
// [Btn, Btn, Sep, Btn] with room for 2 buttons: Sep should NOT trail.
var cb = CreateWithWidth(300);
cb.SecondaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.IsDynamicOverflowEnabled = true;
Assert.IsNotType<CommandBarSeparator>(cb.VisiblePrimaryCommands[^1]);
}
[Fact]
public void TrailingSeparator_MovedToOverflow()
{
// [Btn, Sep, Btn, Btn] with room for 1 button: Sep after the single visible button should overflow.
var cb = CreateWithWidth(300);
cb.ItemWidthBottom = 260;
cb.SecondaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.IsDynamicOverflowEnabled = true;
Assert.Equal(1, cb.VisiblePrimaryCommands.Count);
Assert.IsType<CommandBarButton>(cb.VisiblePrimaryCommands[0]);
}
[Fact]
public void MultipleSeparators_AllTrailingOnesStripped()
{
// [Btn, Sep, Sep, Btn] with room for 1: both trailing separators should be stripped.
var cb = CreateWithWidth(300);
cb.ItemWidthBottom = 260;
cb.SecondaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.IsDynamicOverflowEnabled = true;
Assert.Equal(1, cb.VisiblePrimaryCommands.Count);
Assert.IsType<CommandBarButton>(cb.VisiblePrimaryCommands[0]);
}
[Fact]
public void MidSeparator_StaysVisible_WhenButtonsOnBothSides()
{
// [Btn, Sep, Btn] with room for all: separator stays.
var cb = CreateWithWidth(300);
cb.PrimaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.IsDynamicOverflowEnabled = true;
Assert.Equal(3, cb.VisiblePrimaryCommands.Count);
Assert.IsType<CommandBarSeparator>(cb.VisiblePrimaryCommands[1]);
}
[Fact]
public void AllButtonsOverflow_SeparatorsAlsoOverflow()
{
// [Sep, Btn, Btn] with room for 0: everything overflows.
var cb = CreateWithWidth(50);
cb.SecondaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands!.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.IsDynamicOverflowEnabled = true;
Assert.Empty(cb.VisiblePrimaryCommands);
}
[Fact]
public void LeadingSeparator_IsStrippedFromVisible()
{
// [Sep, Btn, Btn, Btn] with room for 2: leading Sep should be stripped.
var cb = CreateWithWidth(300);
cb.SecondaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands!.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.IsDynamicOverflowEnabled = true;
Assert.IsNotType<CommandBarSeparator>(cb.VisiblePrimaryCommands[0]);
}
[Fact]
public void ConsecutiveSeparators_CollapsedToOne()
{
// [Btn, Sep, Sep, Btn] all fit: only one separator should remain.
var cb = CreateWithWidth(300);
cb.PrimaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.IsDynamicOverflowEnabled = true;
int sepCount = CountSeparators(cb.VisiblePrimaryCommands);
Assert.Equal(1, sepCount);
}
[Fact]
public void OrphanedMidSeparator_RemovedWhenNeighborOverflows()
{
// [Btn1, Sep, Btn2, Sep, Btn3] with room for 2: Btn3 overflows,
// second Sep becomes trailing and is removed. First Sep stays.
var cb = CreateWithWidth(300);
cb.ItemWidthBottom = 100;
cb.SecondaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.IsDynamicOverflowEnabled = true;
Assert.IsNotType<CommandBarSeparator>(cb.VisiblePrimaryCommands[^1]);
Assert.Equal(1, CountSeparators(cb.VisiblePrimaryCommands));
}
[Fact]
public void SeparatorBetweenOverflowedButtons_IsRemoved()
{
// [Btn1, Btn2, Sep, Btn3, Btn4] with room for 2: Btn3 and Btn4 overflow,
// Sep has no non-separator after it in visible set, so it is removed.
var cb = CreateWithWidth(300);
cb.ItemWidthBottom = 100;
cb.SecondaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.IsDynamicOverflowEnabled = true;
Assert.Equal(0, CountSeparators(cb.VisiblePrimaryCommands));
}
[Fact]
public void MultipleSeparatorGroups_OnlyValidOnesRemain()
{
// [Btn, Sep, Btn, Sep, Btn, Sep, Btn] with room for 3:
// last Btn overflows, last Sep becomes trailing, the rest stay.
var cb = CreateWithWidth(300);
cb.SecondaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarButton());
cb.IsDynamicOverflowEnabled = true;
Assert.IsNotType<CommandBarSeparator>(cb.VisiblePrimaryCommands[^1]);
Assert.IsNotType<CommandBarSeparator>(cb.VisiblePrimaryCommands[0]);
}
[Fact]
public void OnlySeparators_AllOverflow()
{
// [Sep, Sep, Sep] with no buttons: all should overflow.
var cb = CreateWithWidth(300);
cb.SecondaryCommands!.Add(new CommandBarButton());
cb.PrimaryCommands!.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.PrimaryCommands.Add(new CommandBarSeparator());
cb.IsDynamicOverflowEnabled = true;
Assert.Empty(cb.VisiblePrimaryCommands);
}
[Fact]
public void PrimarySeparator_IsRemovedInsteadOfBecomingFirstOverflowItem()
{
var cb = CreateWithWidth(300);
cb.ItemWidthBottom = 260;
var leadingSeparator = new CommandBarSeparator();
var firstButton = new CommandBarButton();
var overflowedButton = new CommandBarButton();
cb.PrimaryCommands!.Add(leadingSeparator);
cb.PrimaryCommands.Add(firstButton);
cb.PrimaryCommands.Add(overflowedButton);
cb.IsDynamicOverflowEnabled = true;
Assert.Single(cb.OverflowItems);
Assert.Same(overflowedButton, cb.OverflowItems[0]);
Assert.DoesNotContain(leadingSeparator, cb.OverflowItems);
}
[Fact]
public void OverflowedPrimaryCommands_PrecedeSecondaryCommands_WithSyntheticSeparator()
{
var cb = CreateWithWidth(300);
cb.ItemWidthBottom = 260;
var visiblePrimary = new CommandBarButton();
var originalPrimarySeparator = new CommandBarSeparator();
var overflowedPrimaryOne = new CommandBarButton();
var overflowedPrimaryTwo = new CommandBarButton();
var secondary = new CommandBarButton();
cb.PrimaryCommands!.Add(visiblePrimary);
cb.PrimaryCommands.Add(originalPrimarySeparator);
cb.PrimaryCommands.Add(overflowedPrimaryOne);
cb.PrimaryCommands.Add(overflowedPrimaryTwo);
cb.SecondaryCommands!.Add(secondary);
cb.IsDynamicOverflowEnabled = true;
Assert.Equal(4, cb.OverflowItems.Count);
Assert.Same(overflowedPrimaryOne, cb.OverflowItems[0]);
Assert.Same(overflowedPrimaryTwo, cb.OverflowItems[1]);
Assert.IsType<CommandBarSeparator>(cb.OverflowItems[2]);
Assert.NotSame(originalPrimarySeparator, cb.OverflowItems[2]);
Assert.Same(secondary, cb.OverflowItems[3]);
Assert.DoesNotContain(originalPrimarySeparator, cb.OverflowItems);
}
[Fact]
public void HiddenSecondaryCommands_DoNotGetSyntheticOverflowSeparator()
{
var cb = CreateWithWidth(300);
cb.ItemWidthBottom = 260;
var visiblePrimary = new CommandBarButton();
var overflowedPrimary = new CommandBarButton();
var hiddenSecondary = new CommandBarButton { IsVisible = false };
cb.PrimaryCommands!.Add(visiblePrimary);
cb.PrimaryCommands.Add(overflowedPrimary);
cb.SecondaryCommands!.Add(hiddenSecondary);
cb.IsDynamicOverflowEnabled = true;
Assert.Equal(2, cb.OverflowItems.Count);
Assert.Same(overflowedPrimary, cb.OverflowItems[0]);
Assert.Same(hiddenSecondary, cb.OverflowItems[1]);
Assert.DoesNotContain(cb.OverflowItems, x => x is CommandBarSeparator);
}
[Fact]
public void TogglingSecondaryVisibility_RebuildsSyntheticOverflowSeparator()
{
var cb = CreateWithWidth(300);
cb.ItemWidthBottom = 260;
var visiblePrimary = new CommandBarButton();
var overflowedPrimary = new CommandBarButton();
var secondary = new CommandBarButton();
cb.PrimaryCommands!.Add(visiblePrimary);
cb.PrimaryCommands.Add(overflowedPrimary);
cb.SecondaryCommands!.Add(secondary);
cb.IsDynamicOverflowEnabled = true;
Assert.Equal(3, cb.OverflowItems.Count);
Assert.Same(overflowedPrimary, cb.OverflowItems[0]);
Assert.IsType<CommandBarSeparator>(cb.OverflowItems[1]);
Assert.Same(secondary, cb.OverflowItems[2]);
secondary.IsVisible = false;
Assert.Equal(2, cb.OverflowItems.Count);
Assert.Same(overflowedPrimary, cb.OverflowItems[0]);
Assert.Same(secondary, cb.OverflowItems[1]);
Assert.DoesNotContain(cb.OverflowItems, x => x is CommandBarSeparator);
secondary.IsVisible = true;
Assert.Equal(3, cb.OverflowItems.Count);
Assert.Same(overflowedPrimary, cb.OverflowItems[0]);
Assert.IsType<CommandBarSeparator>(cb.OverflowItems[1]);
Assert.Same(secondary, cb.OverflowItems[2]);
}
private static int CountSeparators(IReadOnlyList<ICommandBarElement> items)
{
int count = 0;
for (var i = 0; i < items.Count; i++)
{
if (items[i] is CommandBarSeparator)
count++;
}
return count;
}
}
file sealed class DelegateCommand : System.Windows.Input.ICommand
{
private readonly System.Action<object?> _execute;

Loading…
Cancel
Save