Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
wants to merge 16 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
26 changes: 26 additions & 0 deletions src/Avalonia.Controls/Presenters/ItemsPresenter.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
Expand Down Expand Up @@ -198,6 +200,30 @@ 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);

var matchingControl = Panel?.Children.FirstOrDefault(c => ControlMatchesTextSearch(c, textSearch));
return matchingControl != null ? Panel!.Children.IndexOf(matchingControl) : -1;
}

internal int IndexFromContainer(Control container)
{
Expand Down
28 changes: 6 additions & 22 deletions src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
using System.ComponentModel;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Selection;
using Avalonia.Data;
using Avalonia.Data.Core;
Expand Down Expand Up @@ -115,7 +116,7 @@ public class SelectingItemsControl : ItemsControl
/// </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.
Expand Down Expand Up @@ -610,29 +611,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;
Expand Down
20 changes: 20 additions & 0 deletions src/Avalonia.Controls/VirtualizingPanel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,9 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Diagnostics.CodeAnalysis;
using System.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
using Avalonia.Input;
Expand Down Expand Up @@ -192,6 +194,24 @@ protected void RemoveInternalChildRange(int index, int count)
Children.RemoveRange(index, count);
}

/// <summary>
/// Gets the index of a content control item contained within this panel that matches the given text search
/// </summary>
/// <param name="textSearchTerm">The beginning of a string (case-insensitive) to search for in the panel</param>
/// <returns>The index of the first IContentControl item contained in the panel</returns>
protected internal int GetIndexFromTextSearch(string textSearchTerm)
{
var matchingControl = Items.FirstOrDefault(i => i is Control c && ItemsPresenter.ControlMatchesTextSearch(c, textSearchTerm));
if (matchingControl != null)
{
return Items.IndexOf(matchingControl);
}

var matchingObject = Items.FirstOrDefault(i =>
i?.ToString()?.StartsWith(textSearchTerm, StringComparison.OrdinalIgnoreCase) == true);
return matchingObject != null ? Items.IndexOf(matchingObject) : -1;
}

private protected override void InvalidateMeasureOnChildrenChanged()
{
// Don't invalidate measure when children are added or removed: the panel is responsible
Expand Down
21 changes: 16 additions & 5 deletions tests/Avalonia.Controls.UnitTests/ComboBoxTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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<Panel>(() => new VirtualizingStackPanel()),
}.RegisterInNameScope(scope)
}.RegisterInNameScope(scope)
}.RegisterInNameScope(scope)
}
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -293,7 +305,6 @@ public void SelectedItem_Validation()

Assert.True(DataValidationErrors.GetHasErrors(target));
Assert.True(DataValidationErrors.GetErrors(target).SequenceEqual(new[] { exception }));

}

}
Expand Down
Loading