Browse Source

Search all SelectingItemsControl items with TextSearch on key input, not just realized ones (#17506)

* Modify SelectingItemsControl to not just use unrealized items

* Fixup null deref

* Get unrealized items searched in comboboxes

* Fixup one small comparison bug

* Reset file that shouldn't have been changed

* Try again

* Revert frfr

* Revert frfrfr

* Fixup per PR feedback

* Remove documentation from internal method

* Remove unused usings
Jonko 8 months ago
parent
commit
418e15d294

+ 38 - 2
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@@ -3,8 +3,6 @@ using System.Collections.Generic;
 using System.Diagnostics;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
-using Avalonia.Interactivity;
-using Avalonia.Layout;
 
 namespace Avalonia.Controls.Presenters
 {
@@ -198,6 +196,44 @@ namespace Avalonia.Controls.Presenters
                 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<object?>? 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)
         {

+ 5 - 23
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis;
 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 @@ namespace Avalonia.Controls.Primitives
         /// </summary>
         public static readonly StyledProperty<bool> IsTextSearchEnabledProperty =
             AvaloniaProperty.Register<SelectingItemsControl, bool>(nameof(IsTextSearchEnabled), false);
-
+        
         /// <summary>
         /// Event that should be raised by containers when their selection state changes to notify
         /// the parent <see cref="SelectingItemsControl"/> that their selection state has changed.
@@ -610,29 +609,12 @@ namespace Avalonia.Controls.Primitives
 
                 _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;

+ 6 - 0
src/Avalonia.Controls/VirtualizingPanel.cs

@@ -3,6 +3,7 @@ using System.Collections.Generic;
 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 @@ namespace Avalonia.Controls
             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

+ 16 - 5
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@@ -198,9 +198,14 @@ namespace Avalonia.Controls.UnitTests
                         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<Panel>(() => new VirtualizingStackPanel()),
+                                }.RegisterInNameScope(scope)
                             }.RegisterInNameScope(scope)
                         }.RegisterInNameScope(scope)
                     }
@@ -243,23 +248,30 @@ namespace Avalonia.Controls.UnitTests
         [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 @@ namespace Avalonia.Controls.UnitTests
 
                 Assert.True(DataValidationErrors.GetHasErrors(target));
                 Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
-                
             }
             
         }