diff --git a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs index 63e512492e2..58d0e797fed 100644 --- a/src/Avalonia.Controls/Presenters/ItemsPresenter.cs +++ b/src/Avalonia.Controls/Presenters/ItemsPresenter.cs @@ -3,8 +3,6 @@ using System.Diagnostics; using Avalonia.Controls.Primitives; using Avalonia.Input; -using Avalonia.Interactivity; -using Avalonia.Layout; namespace Avalonia.Controls.Presenters { @@ -198,6 +196,44 @@ private void CreateSimplePanelGenerator() return v.GetRealizedContainers(); return Panel?.Children; } + + internal static bool ControlMatchesTextSearch(Control control, string textSearchTerm) + { + if (control is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty)) + { + var searchText = ao.GetValue(TextSearch.TextProperty); + + if (searchText?.StartsWith(textSearchTerm, StringComparison.OrdinalIgnoreCase) == true) + { + return true; + } + } + return control is IContentControl cc && + cc.Content?.ToString()?.StartsWith(textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + } + + internal int GetIndexFromTextSearch(string textSearch) + { + if (Panel is VirtualizingPanel v) + return v.GetIndexFromTextSearch(textSearch); + return GetIndexFromTextSearch(ItemsControl?.Items, textSearch); + } + + internal static int GetIndexFromTextSearch(IReadOnlyList? items, string textSearchTerm) + { + if (items is null) + return -1; + + for (var i = 0; i < items.Count; i++) + { + if (items[i] is Control c && ControlMatchesTextSearch(c, textSearchTerm) + || items[i]?.ToString()?.StartsWith(textSearchTerm, StringComparison.OrdinalIgnoreCase) == true) + { + return i; + } + } + return -1; + } internal int IndexFromContainer(Control container) { diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 636b4bd576c..f8301319164 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -6,7 +6,6 @@ using System.Linq; using Avalonia.Controls.Selection; using Avalonia.Data; -using Avalonia.Data.Core; using Avalonia.Input; using Avalonia.Interactivity; using Avalonia.Metadata; @@ -115,7 +114,7 @@ public class SelectingItemsControl : ItemsControl /// public static readonly StyledProperty IsTextSearchEnabledProperty = AvaloniaProperty.Register(nameof(IsTextSearchEnabled), false); - + /// /// Event that should be raised by containers when their selection state changes to notify /// the parent that their selection state has changed. @@ -610,29 +609,12 @@ protected override void OnTextInput(TextInputEventArgs e) _textSearchTerm += e.Text; - bool Match(Control container) + var newIndex = Presenter?.GetIndexFromTextSearch(_textSearchTerm); + if (newIndex >= 0) { - if (container is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty)) - { - var searchText = ao.GetValue(TextSearch.TextProperty); - - if (searchText?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true) - { - return true; - } - } - - return container is IContentControl control && - control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true; + SelectedIndex = (int)newIndex; } - - var container = GetRealizedContainers().FirstOrDefault(Match); - - if (container != null) - { - SelectedIndex = IndexFromContainer(container); - } - + StartTextSearchTimer(); e.Handled = true; diff --git a/src/Avalonia.Controls/VirtualizingPanel.cs b/src/Avalonia.Controls/VirtualizingPanel.cs index 1cd676ed4f4..c2ae8445df3 100644 --- a/src/Avalonia.Controls/VirtualizingPanel.cs +++ b/src/Avalonia.Controls/VirtualizingPanel.cs @@ -3,6 +3,7 @@ using System.Collections.Specialized; using System.Diagnostics.CodeAnalysis; using Avalonia.Controls.Generators; +using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Utils; using Avalonia.Input; @@ -192,6 +193,11 @@ protected void RemoveInternalChildRange(int index, int count) Children.RemoveRange(index, count); } + internal int GetIndexFromTextSearch(string textSearchTerm) + { + return ItemsPresenter.GetIndexFromTextSearch(Items, textSearchTerm); + } + private protected override void InvalidateMeasureOnChildrenChanged() { // Don't invalidate measure when children are added or removed: the panel is responsible diff --git a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs index 413ce22e715..00a31fafe74 100644 --- a/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs @@ -198,9 +198,14 @@ private static FuncControlTemplate GetTemplate() new Popup { Name = "PART_Popup", - Child = new ItemsPresenter + Child = new ScrollViewer { - Name = "PART_ItemsPresenter", + Name = "PART_ScrollViewer", + Content = new ItemsPresenter + { + Name = "PART_ItemsPresenter", + ItemsPanel = new FuncTemplate(() => new VirtualizingStackPanel()), + }.RegisterInNameScope(scope) }.RegisterInNameScope(scope) }.RegisterInNameScope(scope) } @@ -243,23 +248,30 @@ public void Detaching_Closed_ComboBox_Keeps_Current_Focus() [InlineData(-1, 2, "c", "A item", "B item", "C item")] [InlineData(0, 1, "b", "A item", "B item", "C item")] [InlineData(2, 2, "x", "A item", "B item", "C item")] + [InlineData(0, 34, "y", "0 item", "1 item", "2 item", "3 item", "4 item", "5 item", "6 item", "7 item", "8 item", "9 item", "A item", "B item", "C item", "D item", "E item", "F item", "G item", "H item", "I item", "J item", "K item", "L item", "M item", "N item", "O item", "P item", "Q item", "R item", "S item", "T item", "U item", "V item", "W item", "X item", "Y item", "Z item")] public void TextSearch_Should_Have_Expected_SelectedIndex( int initialSelectedIndex, int expectedSelectedIndex, string searchTerm, params string[] items) { - using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) + using (UnitTestApplication.Start(TestServices.StyledWindow)) { var target = new ComboBox { Template = GetTemplate(), - ItemsSource = items.Select(x => new ComboBoxItem { Content = x }) + ItemsSource = items.Select(x => new ComboBoxItem { Content = x }), }; + TestRoot root = new(target) + { + ClientSize = new(500,500), + }; + target.ApplyTemplate(); target.Presenter.ApplyTemplate(); target.SelectedIndex = initialSelectedIndex; + root.LayoutManager.ExecuteInitialLayoutPass(); var args = new TextInputEventArgs { @@ -293,7 +305,6 @@ public void SelectedItem_Validation() Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); - } }