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
pull/18331/head
Jonko 12 months ago
committed by GitHub
parent
commit
418e15d294
No known key found for this signature in database GPG Key ID: B5690EEEBB952194
  1. 40
      src/Avalonia.Controls/Presenters/ItemsPresenter.cs
  2. 28
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  3. 6
      src/Avalonia.Controls/VirtualizingPanel.cs
  4. 21
      tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

40
src/Avalonia.Controls/Presenters/ItemsPresenter.cs

@ -3,8 +3,6 @@ using System.Collections.Generic;
using System.Diagnostics; using System.Diagnostics;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
namespace Avalonia.Controls.Presenters namespace Avalonia.Controls.Presenters
{ {
@ -198,6 +196,44 @@ namespace Avalonia.Controls.Presenters
return v.GetRealizedContainers(); return v.GetRealizedContainers();
return Panel?.Children; 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) internal int IndexFromContainer(Control container)
{ {

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

@ -6,7 +6,6 @@ using System.Diagnostics.CodeAnalysis;
using System.Linq; using System.Linq;
using Avalonia.Controls.Selection; using Avalonia.Controls.Selection;
using Avalonia.Data; using Avalonia.Data;
using Avalonia.Data.Core;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Interactivity; using Avalonia.Interactivity;
using Avalonia.Metadata; using Avalonia.Metadata;
@ -115,7 +114,7 @@ namespace Avalonia.Controls.Primitives
/// </summary> /// </summary>
public static readonly StyledProperty<bool> IsTextSearchEnabledProperty = public static readonly StyledProperty<bool> IsTextSearchEnabledProperty =
AvaloniaProperty.Register<SelectingItemsControl, bool>(nameof(IsTextSearchEnabled), false); AvaloniaProperty.Register<SelectingItemsControl, bool>(nameof(IsTextSearchEnabled), false);
/// <summary> /// <summary>
/// Event that should be raised by containers when their selection state changes to notify /// 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. /// the parent <see cref="SelectingItemsControl"/> that their selection state has changed.
@ -610,29 +609,12 @@ namespace Avalonia.Controls.Primitives
_textSearchTerm += e.Text; _textSearchTerm += e.Text;
bool Match(Control container) var newIndex = Presenter?.GetIndexFromTextSearch(_textSearchTerm);
if (newIndex >= 0)
{ {
if (container is AvaloniaObject ao && ao.IsSet(TextSearch.TextProperty)) SelectedIndex = (int)newIndex;
{
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;
} }
var container = GetRealizedContainers().FirstOrDefault(Match);
if (container != null)
{
SelectedIndex = IndexFromContainer(container);
}
StartTextSearchTimer(); StartTextSearchTimer();
e.Handled = true; e.Handled = true;

6
src/Avalonia.Controls/VirtualizingPanel.cs

@ -3,6 +3,7 @@ using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using Avalonia.Controls.Generators; using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives; using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils; using Avalonia.Controls.Utils;
using Avalonia.Input; using Avalonia.Input;
@ -192,6 +193,11 @@ namespace Avalonia.Controls
Children.RemoveRange(index, count); Children.RemoveRange(index, count);
} }
internal int GetIndexFromTextSearch(string textSearchTerm)
{
return ItemsPresenter.GetIndexFromTextSearch(Items, textSearchTerm);
}
private protected override void InvalidateMeasureOnChildrenChanged() private protected override void InvalidateMeasureOnChildrenChanged()
{ {
// Don't invalidate measure when children are added or removed: the panel is responsible // Don't invalidate measure when children are added or removed: the panel is responsible

21
tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs

@ -198,9 +198,14 @@ namespace Avalonia.Controls.UnitTests
new Popup new Popup
{ {
Name = "PART_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)
}.RegisterInNameScope(scope) }.RegisterInNameScope(scope)
} }
@ -243,23 +248,30 @@ namespace Avalonia.Controls.UnitTests
[InlineData(-1, 2, "c", "A item", "B item", "C item")] [InlineData(-1, 2, "c", "A item", "B item", "C item")]
[InlineData(0, 1, "b", "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(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( public void TextSearch_Should_Have_Expected_SelectedIndex(
int initialSelectedIndex, int initialSelectedIndex,
int expectedSelectedIndex, int expectedSelectedIndex,
string searchTerm, string searchTerm,
params string[] items) params string[] items)
{ {
using (UnitTestApplication.Start(TestServices.MockThreadingInterface)) using (UnitTestApplication.Start(TestServices.StyledWindow))
{ {
var target = new ComboBox var target = new ComboBox
{ {
Template = GetTemplate(), 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.ApplyTemplate();
target.Presenter.ApplyTemplate(); target.Presenter.ApplyTemplate();
target.SelectedIndex = initialSelectedIndex; target.SelectedIndex = initialSelectedIndex;
root.LayoutManager.ExecuteInitialLayoutPass();
var args = new TextInputEventArgs var args = new TextInputEventArgs
{ {
@ -293,7 +305,6 @@ namespace Avalonia.Controls.UnitTests
Assert.True(DataValidationErrors.GetHasErrors(target)); Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception })); Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));
} }
} }

Loading…
Cancel
Save