From a04af7b780867d730bdbdcf9cbaf0c54117ce1bb Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 11:23:23 +0100 Subject: [PATCH 1/4] Update ncrunch config. --- .ncrunch/Avalonia.UnitTests.v3.ncrunchproject | 5 +++++ .ncrunch/GpuInterop.v3.ncrunchproject | 5 +++++ Avalonia.Desktop.slnf | 1 + 3 files changed, 11 insertions(+) create mode 100644 .ncrunch/Avalonia.UnitTests.v3.ncrunchproject create mode 100644 .ncrunch/GpuInterop.v3.ncrunchproject diff --git a/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject new file mode 100644 index 0000000000..cff5044edf --- /dev/null +++ b/.ncrunch/Avalonia.UnitTests.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + False + + \ No newline at end of file diff --git a/.ncrunch/GpuInterop.v3.ncrunchproject b/.ncrunch/GpuInterop.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/GpuInterop.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/Avalonia.Desktop.slnf b/Avalonia.Desktop.slnf index 3acd4bf9f2..741570061b 100644 --- a/Avalonia.Desktop.slnf +++ b/Avalonia.Desktop.slnf @@ -45,6 +45,7 @@ "tests\\Avalonia.Base.UnitTests\\Avalonia.Base.UnitTests.csproj", "tests\\Avalonia.Benchmarks\\Avalonia.Benchmarks.csproj", "tests\\Avalonia.Controls.DataGrid.UnitTests\\Avalonia.Controls.DataGrid.UnitTests.csproj", + "tests\\Avalonia.Controls.ItemsRepeater.UnitTests\\Avalonia.Controls.ItemsRepeater.UnitTests.csproj", "tests\\Avalonia.Controls.UnitTests\\Avalonia.Controls.UnitTests.csproj", "tests\\Avalonia.DesignerSupport.TestApp\\Avalonia.DesignerSupport.TestApp.csproj", "tests\\Avalonia.DesignerSupport.Tests\\Avalonia.DesignerSupport.Tests.csproj", From 035f4f0e55ba6115d6d064c5cbcb6eed91983ada Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 12:02:18 +0100 Subject: [PATCH 2/4] Added failing integration tests for #10232. --- samples/IntegrationTestApp/MainWindow.axaml | 1 + .../ComboBoxTests.cs | 61 ++++++++++++++++++- 2 files changed, 60 insertions(+), 2 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index b116e4c789..4e5a8463df 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -70,6 +70,7 @@ Item 0 Item 1 + Wrap Selection diff --git a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs index abdb4e2dd8..8df7873582 100644 --- a/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/ComboBoxTests.cs @@ -47,7 +47,64 @@ namespace Avalonia.IntegrationTests.Appium } [PlatformFact(TestPlatforms.Windows)] - public void Can_Change_Selection_With_Keyboard() + public void Can_Change_Selection_With_Keyboard_When_Closed() + { + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); + var wrap = _session.FindElementByAccessibilityId("ComboBoxWrapSelection"); + + if (wrap.GetIsChecked() != false) + wrap.Click(); + + _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowUp); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowUp); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + } + + [PlatformFact(TestPlatforms.Windows)] + public void Can_Change_Wrapping_Selection_With_Keyboard_When_Closed() + { + var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); + var wrap = _session.FindElementByAccessibilityId("ComboBoxWrapSelection"); + + if (wrap.GetIsChecked() != true) + wrap.Click(); + + _session.FindElementByAccessibilityId("ComboBoxSelectionClear").Click(); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowDown); + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowUp); + Assert.Equal("Item 0", comboBox.GetComboBoxValue()); + + comboBox.SendKeys(Keys.ArrowUp); + Assert.Equal("Item 1", comboBox.GetComboBoxValue()); + } + + [PlatformFact(TestPlatforms.Windows)] + public void Can_Change_Selection_When_Open_With_Keyboard() { var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); @@ -64,7 +121,7 @@ namespace Avalonia.IntegrationTests.Appium } [PlatformFact(TestPlatforms.Windows)] - public void Can_Change_Selection_With_Keyboard_From_Unselected() + public void Can_Change_Selection_When_Open_With_Keyboard_From_Unselected() { var comboBox = _session.FindElementByAccessibilityId("BasicComboBox"); From fdc65d1c4e41effe5913e73e6e90d4edfa5f1551 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 12:04:10 +0100 Subject: [PATCH 3/4] Different selection logic when ComboBox closed. When the `ComboBox` is closed, then the existing `MoveSelection` method doesn't work because the popup containing the `ItemsPresenter` is not visible, and so has no realized items. Instead revert to simple index-based selection logic when the dropdown is closed. Fixes #10232 --- src/Avalonia.Controls/ComboBox.cs | 46 ++++++++++++++++++++++++++----- 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index b7a298bb16..2d810236c3 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,10 +1,7 @@ using System; using System.Linq; using Avalonia.Automation.Peers; -using Avalonia.Reactive; -using Avalonia.Controls.Generators; -using Avalonia.Controls.Mixins; -using Avalonia.Controls.Presenters; +using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; @@ -12,8 +9,8 @@ using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Media; +using Avalonia.Reactive; using Avalonia.VisualTree; -using Avalonia.Controls.Metadata; namespace Avalonia.Controls { @@ -482,7 +479,22 @@ namespace Avalonia.Controls { if (ItemCount >= 1) { - MoveSelection(NavigationDirection.Next, WrapSelection); + if (IsDropDownOpen) + { + MoveSelection(NavigationDirection.Next, WrapSelection); + } + else + { + var index = SelectedIndex + 1; + var count = ItemCount; + + if (WrapSelection) + index %= count; + else + index = Math.Min(index, count - 1); + + SelectedIndex = index; + } } } @@ -490,7 +502,27 @@ namespace Avalonia.Controls { if (ItemCount >= 1) { - MoveSelection(NavigationDirection.Previous, WrapSelection); + if (IsDropDownOpen) + { + MoveSelection(NavigationDirection.Previous, WrapSelection); + } + else + { + var index = SelectedIndex - 1; + var count = ItemCount; + + if (WrapSelection) + { + if (index < 0) + index += count; + } + else + { + index = Math.Max(index, 0); + } + + SelectedIndex = index; + } } } } From a331e1f86b843e37b54e9a3a7377170040124d04 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Feb 2023 18:40:03 +0100 Subject: [PATCH 4/4] Account for enabled state of combo box items. --- src/Avalonia.Controls/ComboBox.cs | 65 +++++++++++++------------------ 1 file changed, 27 insertions(+), 38 deletions(-) diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 2d810236c3..17a6ad7a09 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -1,10 +1,13 @@ using System; +using System.Diagnostics; using System.Linq; using Avalonia.Automation.Peers; using Avalonia.Controls.Metadata; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Selection; using Avalonia.Controls.Shapes; using Avalonia.Controls.Templates; +using Avalonia.Controls.Utils; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Layout; @@ -216,7 +219,7 @@ namespace Avalonia.Controls } else if (e.Key == Key.Up) { - SelectPrev(); + SelectPrevious(); e.Handled = true; } } @@ -247,7 +250,7 @@ namespace Avalonia.Controls if (e.Delta.Y < 0) SelectNext(); else - SelectPrev(); + SelectPrevious(); e.Handled = true; } @@ -475,53 +478,39 @@ namespace Avalonia.Controls } } - private void SelectNext() + private void SelectNext() => MoveSelection(SelectedIndex, 1, WrapSelection); + private void SelectPrevious() => MoveSelection(SelectedIndex, -1, WrapSelection); + + private void MoveSelection(int startIndex, int step, bool wrap) { - if (ItemCount >= 1) - { - if (IsDropDownOpen) - { - MoveSelection(NavigationDirection.Next, WrapSelection); - } - else - { - var index = SelectedIndex + 1; - var count = ItemCount; + static bool IsSelectable(object? o) => (o as AvaloniaObject)?.GetValue(IsEnabledProperty) ?? true; - if (WrapSelection) - index %= count; - else - index = Math.Min(index, count - 1); + var count = ItemCount; - SelectedIndex = index; - } - } - } - - private void SelectPrev() - { - if (ItemCount >= 1) + for (int i = startIndex + step; i != startIndex; i += step) { - if (IsDropDownOpen) - { - MoveSelection(NavigationDirection.Previous, WrapSelection); - } - else + if (i < 0 || i >= count) { - var index = SelectedIndex - 1; - var count = ItemCount; - - if (WrapSelection) + if (wrap) { - if (index < 0) - index += count; + if (i < 0) + i += count; + else if (i >= count) + i %= count; } else { - index = Math.Max(index, 0); + return; } + } - SelectedIndex = index; + var item = ItemsView[i]; + var container = ContainerFromIndex(i); + + if (IsSelectable(item) && IsSelectable(container)) + { + SelectedIndex = i; + break; } } }