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

Add ItemsControl.IsTextSearchEnabled #6210

Merged
Merged
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
69 changes: 0 additions & 69 deletions src/Avalonia.Controls/ComboBox.cs
Original file line number Diff line number Diff line change
Expand Up @@ -77,14 +77,6 @@ public class ComboBox : SelectingItemsControl
public static readonly StyledProperty<VerticalAlignment> VerticalContentAlignmentProperty =
ContentControl.VerticalContentAlignmentProperty.AddOwner<ComboBox>();

/// <summary>
/// Defines the <see cref="IsTextSearchEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsTextSearchEnabledProperty =
AvaloniaProperty.Register<ComboBox, bool>(nameof(IsTextSearchEnabled), true);

private string _textSearchTerm = string.Empty;
private DispatcherTimer _textSearchTimer;
private bool _isDropDownOpen;
private Popup _popup;
private object _selectionBoxItem;
Expand Down Expand Up @@ -173,15 +165,6 @@ public VerticalAlignment VerticalContentAlignment
set { SetValue(VerticalContentAlignmentProperty, value); }
}

/// <summary>
/// Gets or sets a value that specifies whether a user can jump to a value by typing.
/// </summary>
public bool IsTextSearchEnabled
{
get { return GetValue(IsTextSearchEnabledProperty); }
set { SetValue(IsTextSearchEnabledProperty, value); }
}

/// <inheritdoc/>
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
Expand Down Expand Up @@ -247,32 +230,6 @@ protected override void OnKeyDown(KeyEventArgs e)
}
}

/// <inheritdoc />
protected override void OnTextInput(TextInputEventArgs e)
{
if (!IsTextSearchEnabled || e.Handled)
return;

StopTextSearchTimer();

_textSearchTerm += e.Text;

bool match(ItemContainerInfo info) =>
info.ContainerControl is IContentControl control &&
control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true;

var info = ItemContainerGenerator.Containers.FirstOrDefault(match);

if (info != null)
{
SelectedIndex = info.Index;
}

StartTextSearchTimer();

e.Handled = true;
}

/// <inheritdoc/>
protected override void OnPointerWheelChanged(PointerWheelEventArgs e)
{
Expand Down Expand Up @@ -470,31 +427,5 @@ private void SelectPrev()

SelectedIndex = prev;
}

private void StartTextSearchTimer()
{
_textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_textSearchTimer.Tick += TextSearchTimer_Tick;
_textSearchTimer.Start();
}

private void StopTextSearchTimer()
{
if (_textSearchTimer == null)
{
return;
}

_textSearchTimer.Stop();
_textSearchTimer.Tick -= TextSearchTimer_Tick;

_textSearchTimer = null;
}

private void TextSearchTimer_Tick(object sender, EventArgs e)
{
_textSearchTerm = string.Empty;
StopTextSearchTimer();
}
}
}
1 change: 1 addition & 0 deletions src/Avalonia.Controls/ItemsControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Collections;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Metadata;
Expand Down
74 changes: 74 additions & 0 deletions src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Threading;
using Avalonia.VisualTree;

#nullable enable
Expand Down Expand Up @@ -91,6 +92,12 @@ public class SelectingItemsControl : ItemsControl
AvaloniaProperty.Register<SelectingItemsControl, SelectionMode>(
nameof(SelectionMode));

/// <summary>
/// Defines the <see cref="IsTextSearchEnabled"/> property.
/// </summary>
public static readonly StyledProperty<bool> IsTextSearchEnabledProperty =
AvaloniaProperty.Register<ItemsControl, bool>(nameof(IsTextSearchEnabled), true);

/// <summary>
/// Event that should be raised by items that implement <see cref="ISelectable"/> to
/// notify the parent <see cref="SelectingItemsControl"/> that their selection state
Expand All @@ -110,6 +117,8 @@ public class SelectingItemsControl : ItemsControl
RoutingStrategies.Bubble);

private static readonly IList Empty = Array.Empty<object>();
private string _textSearchTerm = string.Empty;
private DispatcherTimer? _textSearchTimer;
private ISelectionModel? _selection;
private int _oldSelectedIndex;
private object? _oldSelectedItem;
Expand Down Expand Up @@ -305,6 +314,15 @@ protected ISelectionModel Selection
}
}

/// <summary>
/// Gets or sets a value that specifies whether a user can jump to a value by typing.
/// </summary>
public bool IsTextSearchEnabled
{
get { return GetValue(IsTextSearchEnabledProperty); }
set { SetValue(IsTextSearchEnabledProperty, value); }
}

/// <summary>
/// Gets or sets the selection mode.
/// </summary>
Expand Down Expand Up @@ -490,6 +508,36 @@ protected override void OnInitialized()
}
}

protected override void OnTextInput(TextInputEventArgs e)
{
if (!e.Handled)
{
if (!IsTextSearchEnabled)
return;

StopTextSearchTimer();
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would move the search logic to an asynchronous method that accepts as parameters the text to search and a CancellationToken, because if the itemcontrol contains many elements the OnTextInput event would not terminate until the search is completed.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wouldn't this result in OnTextInput being async void? Is that okay, or am I missing something? How much time should be given before the search gets cancelled?

Copy link
Member

@maxkatz6 maxkatz6 Jul 10, 2021

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@workgroupengineering are you suggesting do search on thread pool? I don't think it will work nicely in general. And won't work at all, if we are doing Content?.ToString() (as it is rn), since it requires UI thread to read Content property.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@maxkatz6 Not exactly what I wanted to suggest. I wanted to suggest queuing with low priority the search operation in the dispatcher, in order to permit the completing of the events tunneling. If another OnTextInput is fired , the last queued search operation is cancelled and adding newone.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I see, it might make sense. Not sure if it won't cause unwanted delay for user.


_textSearchTerm += e.Text;

bool match(ItemContainerInfo info) =>
info.ContainerControl is IContentControl control &&
control.Content?.ToString()?.StartsWith(_textSearchTerm, StringComparison.OrdinalIgnoreCase) == true;

var info = ItemContainerGenerator.Containers.FirstOrDefault(match);

if (info != null)
{
SelectedIndex = info.Index;
}

StartTextSearchTimer();

e.Handled = true;
}

base.OnTextInput(e);
}

protected override void OnKeyDown(KeyEventArgs e)
{
base.OnKeyDown(e);
Expand Down Expand Up @@ -962,6 +1010,32 @@ private void EndUpdating()
}
}

private void StartTextSearchTimer()
{
_textSearchTimer = new DispatcherTimer { Interval = TimeSpan.FromSeconds(1) };
_textSearchTimer.Tick += TextSearchTimer_Tick;
_textSearchTimer.Start();
}

private void StopTextSearchTimer()
{
if (_textSearchTimer == null)
{
return;
}

_textSearchTimer.Tick -= TextSearchTimer_Tick;
_textSearchTimer.Stop();

_textSearchTimer = null;
}

private void TextSearchTimer_Tick(object sender, EventArgs e)
{
_textSearchTerm = string.Empty;
StopTextSearchTimer();
}

// When in a BeginInit..EndInit block, or when the DataContext is updating, we need to
// defer changes to the selection model because we have no idea in which order properties
// will be set. Consider:
Expand Down