Browse Source

Properly implement keyboard selection.

pull/11455/head
Steven Kirk 3 years ago
parent
commit
06bfa29b12
  1. 8
      src/Avalonia.Controls/ListBox.cs
  2. 22
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  3. 6
      src/Avalonia.Controls/VirtualizingStackPanel.cs
  4. 141
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  5. 118
      tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs

8
src/Avalonia.Controls/ListBox.cs

@ -128,8 +128,11 @@ namespace Avalonia.Controls
protected override void OnKeyDown(KeyEventArgs e)
{
var hotkeys = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
var ctrl = hotkeys is not null && e.KeyModifiers.HasAllFlags(hotkeys.CommandModifiers);
if (e.Key.ToNavigationDirection() is { } direction)
if (!ctrl &&
e.Key.ToNavigationDirection() is { } direction &&
direction.IsDirectional())
{
e.Handled |= MoveSelection(
direction,
@ -144,12 +147,11 @@ namespace Avalonia.Controls
}
else if (e.Key == Key.Space || e.Key == Key.Enter)
{
var toggle = hotkeys is not null && e.KeyModifiers.HasAllFlags(hotkeys.CommandModifiers);
e.Handled |= UpdateSelectionFromEventSource(
e.Source,
true,
e.KeyModifiers.HasFlag(KeyModifiers.Shift),
toggle);
ctrl);
}
base.OnKeyDown(e);

22
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -704,7 +704,8 @@ namespace Avalonia.Controls.Primitives
bool wrap = false,
bool rangeModifier = false)
{
var from = ContainerFromIndex(Selection.AnchorIndex);
var from = GetContainerFromEventSource(FocusManager.Instance?.Current) ??
ContainerFromIndex(Selection.AnchorIndex);
return MoveSelection(from, direction, wrap, rangeModifier);
}
@ -722,14 +723,29 @@ namespace Avalonia.Controls.Primitives
bool wrap = false,
bool rangeModifier = false)
{
if (Presenter?.Panel is INavigableContainer container &&
GetNextControl(container, direction, from, wrap) is Control next)
if (Presenter?.Panel is not INavigableContainer container)
return false;
if (from is null)
{
direction = direction switch
{
NavigationDirection.Down => NavigationDirection.First,
NavigationDirection.Up => NavigationDirection.Last,
NavigationDirection.Right => NavigationDirection.First,
NavigationDirection.Left => NavigationDirection.Last,
_ => direction,
};
}
if (GetNextControl(container, direction, from, wrap) is Control next)
{
var index = IndexFromContainer(next);
if (index != -1)
{
UpdateSelection(index, true, rangeModifier);
next.Focus();
return true;
}
}

6
src/Avalonia.Controls/VirtualizingStackPanel.cs

@ -265,12 +265,14 @@ namespace Avalonia.Controls
protected override IInputElement? GetControl(NavigationDirection direction, IInputElement? from, bool wrap)
{
var count = Items.Count;
var fromControl = from as Control;
if (count == 0 || from is not Control fromControl)
if (count == 0 ||
fromControl is null && direction is not NavigationDirection.First or NavigationDirection.Last)
return null;
var horiz = Orientation == Orientation.Horizontal;
var fromIndex = from != null ? IndexFromContainer(fromControl) : -1;
var fromIndex = fromControl != null ? IndexFromContainer(fromControl) : -1;
var toIndex = fromIndex;
switch (direction)

141
tests/Avalonia.Controls.UnitTests/ListBoxTests.cs

@ -12,6 +12,7 @@ using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
@ -752,6 +753,146 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(Enumerable.Range(0, 10).Select(x => $"Item{10 - x}"), realized);
}
[Fact]
public void Arrow_Keys_Should_Move_Selection_Vertical()
{
using var app = UnitTestApplication.Start(TestServices.RealFocus);
var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray();
var target = new ListBox
{
Template = ListBoxTemplate(),
ItemsSource = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
SelectedIndex = 0,
};
Prepare(target);
RaiseKeyEvent(target, Key.Down);
Assert.Equal(1, target.SelectedIndex);
RaiseKeyEvent(target, Key.Down);
Assert.Equal(2, target.SelectedIndex);
RaiseKeyEvent(target, Key.Up);
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void Arrow_Keys_Should_Move_Selection_Horizontal()
{
using var app = UnitTestApplication.Start(TestServices.RealFocus);
var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray();
var target = new ListBox
{
Template = ListBoxTemplate(),
ItemsSource = items,
ItemsPanel = new FuncTemplate<Panel>(() => new VirtualizingStackPanel
{
Orientation = Orientation.Horizontal
}),
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
SelectedIndex = 0,
};
Prepare(target);
RaiseKeyEvent(target, Key.Right);
Assert.Equal(1, target.SelectedIndex);
RaiseKeyEvent(target, Key.Right);
Assert.Equal(2, target.SelectedIndex);
RaiseKeyEvent(target, Key.Left);
Assert.Equal(1, target.SelectedIndex);
}
[Fact]
public void Arrow_Keys_Should_Focus_Selection()
{
using var app = UnitTestApplication.Start(TestServices.RealFocus);
var items = Enumerable.Range(0, 10).Select(x => $"Item {x}").ToArray();
var target = new ListBox
{
Template = ListBoxTemplate(),
ItemsSource = items,
ItemTemplate = new FuncDataTemplate<string>((x, _) => new TextBlock { Height = 10 }),
SelectedIndex = 0,
};
Prepare(target);
RaiseKeyEvent(target, Key.Down);
Assert.True(target.ContainerFromIndex(1).IsFocused);
RaiseKeyEvent(target, Key.Down);
Assert.True(target.ContainerFromIndex(2).IsFocused);
RaiseKeyEvent(target, Key.Up);
Assert.True(target.ContainerFromIndex(1).IsFocused);
}
[Fact]
public void Down_Key_Selecting_From_No_Selection_And_No_Focus_Selects_From_Start()
{
using var app = UnitTestApplication.Start(TestServices.RealFocus);
var target = new ListBox
{
Template = ListBoxTemplate(),
ItemsSource = new[] { "Foo", "Bar", "Baz" },
Width = 100,
Height = 100,
};
Prepare(target);
RaiseKeyEvent(target, Key.Down);
Assert.Equal(0, target.SelectedIndex);
}
[Fact]
public void Down_Key_Selecting_From_No_Selection_Selects_From_Focus()
{
using var app = UnitTestApplication.Start(TestServices.RealFocus);
var target = new ListBox
{
Template = ListBoxTemplate(),
ItemsSource = new[] { "Foo", "Bar", "Baz" },
Width = 100,
Height = 100,
};
Prepare(target);
target.ContainerFromIndex(1)!.Focus();
RaiseKeyEvent(target, Key.Down);
Assert.Equal(2, target.SelectedIndex);
}
[Fact]
public void Ctrl_Down_Key_Moves_Focus_But_Not_Selection()
{
using var app = UnitTestApplication.Start(TestServices.RealFocus);
var target = new ListBox
{
Template = ListBoxTemplate(),
ItemsSource = new[] { "Foo", "Bar", "Baz" },
Width = 100,
Height = 100,
SelectedIndex = 0,
};
Prepare(target);
target.ContainerFromIndex(0)!.Focus();
RaiseKeyEvent(target, Key.Down, KeyModifiers.Control);
Assert.Equal(0, target.SelectedIndex);
Assert.True(target.ContainerFromIndex(1).IsFocused);
}
[Fact]
public void WrapSelection_Should_Wrap()
{

118
tests/Avalonia.Controls.UnitTests/ListBoxTests_Multiple.cs

@ -479,6 +479,104 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(1, target.SelectedItems.Count);
}
}
[Fact]
public void Shift_Arrow_Key_Selects_Range()
{
using var app = UnitTestApplication.Start(TestServices.RealFocus);
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
ItemsSource = new[] { "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
Width = 100,
Height = 100,
SelectedIndex = 0,
};
var root = new TestRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
RaiseKeyEvent(target, Key.Down, KeyModifiers.Shift);
Assert.Equal(new[] { "Foo", "Bar", }, target.SelectedItems);
Assert.Equal(new[] { 0, 1 }, SelectedContainers(target));
Assert.True(target.ContainerFromIndex(1).IsFocused);
RaiseKeyEvent(target, Key.Down, KeyModifiers.Shift);
Assert.Equal(new[] { "Foo", "Bar", "Baz", }, target.SelectedItems);
Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target));
Assert.True(target.ContainerFromIndex(2).IsFocused);
RaiseKeyEvent(target, Key.Up, KeyModifiers.Shift);
Assert.Equal(new[] { "Foo", "Bar", }, target.SelectedItems);
Assert.Equal(new[] { 0, 1 }, SelectedContainers(target));
Assert.True(target.ContainerFromIndex(1).IsFocused);
}
[Fact]
public void Shift_Down_Key_Selecting_Selects_Range_End_From_Focus()
{
using var app = UnitTestApplication.Start(TestServices.RealFocus);
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
ItemsSource = new[] { "Foo", "Bar", "Baz" },
SelectionMode = SelectionMode.Multiple,
Width = 100,
Height = 100,
SelectedIndex = 0,
};
var root = new TestRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
target.ContainerFromIndex(1)!.Focus();
RaiseKeyEvent(target, Key.Down, KeyModifiers.Shift);
Assert.Equal(new[] { "Foo", "Bar", "Baz" }, target.SelectedItems);
Assert.Equal(new[] { 0, 1, 2 }, SelectedContainers(target));
Assert.True(target.ContainerFromIndex(2).IsFocused);
}
[Fact]
public void Shift_Down_Key_Selecting_Selects_Range_End_From_Focus_Moved_With_Ctrl_Key()
{
using var app = UnitTestApplication.Start(TestServices.RealFocus);
var target = new ListBox
{
Template = new FuncControlTemplate(CreateListBoxTemplate),
ItemsSource = new[] { "Foo", "Bar", "Baz", "Qux" },
SelectionMode = SelectionMode.Multiple,
Width = 100,
Height = 100,
SelectedIndex = 0,
};
var root = new TestRoot(target);
root.LayoutManager.ExecuteInitialLayoutPass();
RaiseKeyEvent(target, Key.Down, KeyModifiers.Shift);
Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
Assert.Equal(new[] { 0, 1 }, SelectedContainers(target));
Assert.True(target.ContainerFromIndex(1).IsFocused);
RaiseKeyEvent(target, Key.Down, KeyModifiers.Control);
Assert.Equal(new[] { "Foo", "Bar" }, target.SelectedItems);
Assert.Equal(new[] { 0, 1 }, SelectedContainers(target));
Assert.True(target.ContainerFromIndex(2).IsFocused);
RaiseKeyEvent(target, Key.Down, KeyModifiers.Shift);
Assert.Equal(new[] { "Foo", "Bar", "Baz", "Qux" }, target.SelectedItems);
Assert.Equal(new[] { 0, 1, 2, 3 }, SelectedContainers(target));
Assert.True(target.ContainerFromIndex(3).IsFocused);
}
private Control CreateListBoxTemplate(TemplatedControl parent, INameScope scope)
{
return new ScrollViewer
@ -501,20 +599,14 @@ namespace Avalonia.Controls.UnitTests
}.RegisterInNameScope(scope);
}
private static void ApplyTemplate(ListBox target)
private static void RaiseKeyEvent(Control target, Key key, KeyModifiers inputModifiers = 0)
{
// Apply the template to the ListBox itself.
target.ApplyTemplate();
// Then to its inner ScrollViewer.
var scrollViewer = (ScrollViewer)target.GetVisualChildren().Single();
scrollViewer.ApplyTemplate();
// Then make the ScrollViewer create its child.
((ContentPresenter)scrollViewer.Presenter).UpdateChild();
// Now the ItemsPresenter should be reigstered, so apply its template.
((Control)target.Presenter).ApplyTemplate();
target.RaiseEvent(new KeyEventArgs
{
RoutedEvent = InputElement.KeyDownEvent,
KeyModifiers = inputModifiers,
Key = key
});
}
private static IEnumerable<int> SelectedContainers(SelectingItemsControl target)

Loading…
Cancel
Save