You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
246 lines
6.9 KiB
246 lines
6.9 KiB
6 months ago
|
using System;
|
||
|
using System.Collections;
|
||
|
using System.Collections.Generic;
|
||
|
using System.Linq;
|
||
|
using System.ComponentModel;
|
||
|
using System.Threading;
|
||
|
using System.Windows;
|
||
|
using System.Windows.Controls;
|
||
|
using System.Windows.Controls.Primitives;
|
||
|
using System.Windows.Input;
|
||
|
|
||
|
namespace Kingo.Plugin.NYYP
|
||
|
{
|
||
|
/// <summary>
|
||
|
/// AutoCompleteComboBox.xaml
|
||
|
/// </summary>
|
||
|
public partial class AutoCompleteComboBox : ComboBox
|
||
|
{
|
||
|
readonly SerialDisposable disposable = new SerialDisposable();
|
||
|
|
||
|
TextBox editableTextBoxCache;
|
||
|
|
||
|
Predicate<object> defaultItemsFilter;
|
||
|
|
||
|
public TextBox EditableTextBox
|
||
|
{
|
||
|
get
|
||
|
{
|
||
|
if (editableTextBoxCache == null)
|
||
|
{
|
||
|
const string name = "PART_EditableTextBox";
|
||
|
editableTextBoxCache = (TextBox)VisualTreeModule.FindChild(this, name);
|
||
|
}
|
||
|
return editableTextBoxCache;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// <summary>
|
||
|
/// Gets text to match with the query from an item.
|
||
|
/// Never null.
|
||
|
/// </summary>
|
||
|
/// <param name="item"/>
|
||
|
string TextFromItem(object item)
|
||
|
{
|
||
|
if (item == null) return string.Empty;
|
||
|
|
||
|
var d = new DependencyVariable<string>();
|
||
|
d.SetBinding(item, TextSearch.GetTextPath(this));
|
||
|
return d.Value ?? string.Empty;
|
||
|
}
|
||
|
|
||
|
protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
|
||
|
{
|
||
|
base.OnItemsSourceChanged(oldValue, newValue);
|
||
|
|
||
|
defaultItemsFilter = newValue is ICollectionView cv ? cv.Filter : null;
|
||
|
}
|
||
|
|
||
|
#region Setting
|
||
|
static readonly DependencyProperty settingProperty =
|
||
|
DependencyProperty.Register(
|
||
|
"Setting",
|
||
|
typeof(AutoCompleteComboBoxSetting),
|
||
|
typeof(AutoCompleteComboBox)
|
||
|
);
|
||
|
|
||
|
public static DependencyProperty SettingProperty
|
||
|
{
|
||
|
get { return settingProperty; }
|
||
|
}
|
||
|
|
||
|
public AutoCompleteComboBoxSetting Setting
|
||
|
{
|
||
|
get { return (AutoCompleteComboBoxSetting)GetValue(SettingProperty); }
|
||
|
set { SetValue(SettingProperty, value); }
|
||
|
}
|
||
|
|
||
|
AutoCompleteComboBoxSetting SettingOrDefault
|
||
|
{
|
||
|
get { return Setting ?? AutoCompleteComboBoxSetting.Default; }
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
#region OnTextChanged
|
||
|
long revisionId;
|
||
|
string previousText;
|
||
|
|
||
|
struct TextBoxStatePreserver
|
||
|
: IDisposable
|
||
|
{
|
||
|
readonly TextBox textBox;
|
||
|
readonly int selectionStart;
|
||
|
readonly int selectionLength;
|
||
|
readonly string text;
|
||
|
|
||
|
public void Dispose()
|
||
|
{
|
||
|
textBox.Text = text;
|
||
|
textBox.Select(selectionStart, selectionLength);
|
||
|
}
|
||
|
|
||
|
public TextBoxStatePreserver(TextBox textBox)
|
||
|
{
|
||
|
this.textBox = textBox;
|
||
|
selectionStart = textBox.SelectionStart;
|
||
|
selectionLength = textBox.SelectionLength;
|
||
|
text = textBox.Text;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
static int CountWithMax<T>(IEnumerable<T> xs, Predicate<T> predicate, int maxCount)
|
||
|
{
|
||
|
var count = 0;
|
||
|
foreach (var x in xs)
|
||
|
{
|
||
|
if (predicate(x))
|
||
|
{
|
||
|
count++;
|
||
|
if (count > maxCount) return count;
|
||
|
}
|
||
|
}
|
||
|
return count;
|
||
|
}
|
||
|
|
||
|
void Unselect()
|
||
|
{
|
||
|
var textBox = EditableTextBox;
|
||
|
textBox.Select(textBox.SelectionStart + textBox.SelectionLength, 0);
|
||
|
}
|
||
|
|
||
|
void UpdateFilter(Predicate<object> filter)
|
||
|
{
|
||
|
using (new TextBoxStatePreserver(EditableTextBox))
|
||
|
using (Items.DeferRefresh())
|
||
|
{
|
||
|
// Can empty the text box. I don't why.
|
||
|
Items.Filter = filter;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void OpenDropDown(Predicate<object> filter)
|
||
|
{
|
||
|
UpdateFilter(filter);
|
||
|
IsDropDownOpen = true;
|
||
|
Unselect();
|
||
|
}
|
||
|
|
||
|
void OpenDropDown()
|
||
|
{
|
||
|
var filter = GetFilter();
|
||
|
OpenDropDown(filter);
|
||
|
}
|
||
|
|
||
|
void UpdateSuggestionList()
|
||
|
{
|
||
|
var text = Text;
|
||
|
|
||
|
if (text == previousText) return;
|
||
|
previousText = text;
|
||
|
|
||
|
if (string.IsNullOrEmpty(text))
|
||
|
{
|
||
|
IsDropDownOpen = false;
|
||
|
SelectedItem = null;
|
||
|
|
||
|
using (Items.DeferRefresh())
|
||
|
{
|
||
|
Items.Filter = defaultItemsFilter;
|
||
|
}
|
||
|
}
|
||
|
else if (SelectedItem != null && TextFromItem(SelectedItem) == text)
|
||
|
{
|
||
|
// It seems the user selected an item.
|
||
|
// Do nothing.
|
||
|
}
|
||
|
else
|
||
|
{
|
||
|
using (new TextBoxStatePreserver(EditableTextBox))
|
||
|
{
|
||
|
SelectedItem = null;
|
||
|
}
|
||
|
|
||
|
var filter = GetFilter();
|
||
|
var maxCount = SettingOrDefault.MaxSuggestionCount;
|
||
|
var count = CountWithMax(ItemsSource?.Cast<object>() ?? Enumerable.Empty<object>(), filter, maxCount);
|
||
|
|
||
|
if (count > maxCount) return;
|
||
|
|
||
|
OpenDropDown(filter);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
void OnTextChanged(object sender, TextChangedEventArgs e)
|
||
|
{
|
||
|
var id = unchecked(++revisionId);
|
||
|
var setting = SettingOrDefault;
|
||
|
|
||
|
if (setting.Delay <= TimeSpan.Zero)
|
||
|
{
|
||
|
UpdateSuggestionList();
|
||
|
return;
|
||
|
}
|
||
|
|
||
|
disposable.Content =
|
||
|
new Timer(
|
||
|
state =>
|
||
|
{
|
||
|
Dispatcher.InvokeAsync(() =>
|
||
|
{
|
||
|
if (revisionId != id) return;
|
||
|
UpdateSuggestionList();
|
||
|
});
|
||
|
},
|
||
|
null,
|
||
|
setting.Delay,
|
||
|
Timeout.InfiniteTimeSpan
|
||
|
);
|
||
|
}
|
||
|
#endregion
|
||
|
|
||
|
void ComboBox_PreviewKeyDown(object sender, KeyEventArgs e)
|
||
|
{
|
||
|
if (Keyboard.Modifiers.HasFlag(ModifierKeys.Control) && e.Key == Key.Space)
|
||
|
{
|
||
|
OpenDropDown();
|
||
|
e.Handled = true;
|
||
|
}
|
||
|
}
|
||
|
|
||
|
Predicate<object> GetFilter()
|
||
|
{
|
||
|
var filter = SettingOrDefault.GetFilter(Text, TextFromItem);
|
||
|
|
||
|
return defaultItemsFilter != null
|
||
|
? i => defaultItemsFilter(i) && filter(i)
|
||
|
: filter;
|
||
|
}
|
||
|
|
||
|
public AutoCompleteComboBox()
|
||
|
{
|
||
|
InitializeComponent();
|
||
|
|
||
|
AddHandler(TextBoxBase.TextChangedEvent, new TextChangedEventHandler(OnTextChanged));
|
||
|
}
|
||
|
}
|
||
|
}
|