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;
using System.Windows.Media;
using System.Windows.Data;
namespace Kingo.Plugin.DLTB_IDG
{
///
/// AutoCompleteComboBox.xaml
///
public partial class AutoCompleteComboBox : ComboBox
{
readonly SerialDisposable disposable = new SerialDisposable();
TextBox editableTextBoxCache;
Predicate defaultItemsFilter;
public TextBox EditableTextBox
{
get
{
if (editableTextBoxCache == null)
{
const string name = "PART_EditableTextBox";
editableTextBoxCache = (TextBox)VisualTreeModule.FindChild(this, name);
}
return editableTextBoxCache;
}
}
///
/// Gets text to match with the query from an item.
/// Never null.
///
///
string TextFromItem(object item)
{
if (item == null) return string.Empty;
var d = new DependencyVariable();
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(IEnumerable xs, Predicate 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 filter)
{
using (new TextBoxStatePreserver(EditableTextBox))
using (Items.DeferRefresh())
{
// Can empty the text box. I don't why.
Items.Filter = filter;
}
}
void OpenDropDown(Predicate 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() ?? Enumerable.Empty(), 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 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));
}
}
sealed class SerialDisposable
: IDisposable
{
IDisposable content;
public IDisposable Content
{
get { return content; }
set
{
if (content != null)
{
content.Dispose();
}
content = value;
}
}
public void Dispose()
{
Content = null;
}
}
static class VisualTreeModule
{
public static FrameworkElement FindChild(DependencyObject obj, string childName)
{
if (obj == null) return null;
var queue = new Queue();
queue.Enqueue(obj);
while (queue.Count > 0)
{
obj = queue.Dequeue();
var childCount = VisualTreeHelper.GetChildrenCount(obj);
for (var i = 0; i < childCount; i++)
{
var child = VisualTreeHelper.GetChild(obj, i);
var fe = child as FrameworkElement;
if (fe != null && fe.Name == childName)
{
return fe;
}
queue.Enqueue(child);
}
}
return null;
}
}
sealed class DependencyVariable
: DependencyObject
{
static readonly DependencyProperty valueProperty =
DependencyProperty.Register(
"Value",
typeof(T),
typeof(DependencyVariable)
);
public static DependencyProperty ValueProperty
{
get { return valueProperty; }
}
public T Value
{
get { return (T)GetValue(ValueProperty); }
set { SetValue(ValueProperty, value); }
}
public void SetBinding(Binding binding)
{
BindingOperations.SetBinding(this, ValueProperty, binding);
}
public void SetBinding(object dataContext, string propertyPath)
{
SetBinding(new Binding(propertyPath) { Source = dataContext });
}
}
public class AutoCompleteComboBoxSetting
{
///
/// Gets a filter function which determines whether items should be suggested or not
/// for the specified query.
/// Default: Gets the filter which maps an item to true
/// if its text contains the query (case insensitive).
///
///
/// The string input by user.
///
///
/// The function to get a string which identifies the specified item.
///
///
public virtual Predicate GetFilter(string query, Func stringFromItem)
{
return item => stringFromItem(item).IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0;
}
///
/// Gets an integer.
/// The combobox opens the drop down
/// if the number of suggested items is less than the value.
/// Note that the value is larger, it's heavier to open the drop down.
/// Default: 100.
///
public virtual int MaxSuggestionCount
{
get { return 100; }
}
///
/// Gets the duration to delay updating the suggestion list.
/// Returns Zero if no delay.
/// Default: 300ms.
///
public virtual TimeSpan Delay
{
get { return TimeSpan.FromMilliseconds(300.0); }
}
static AutoCompleteComboBoxSetting @default = new AutoCompleteComboBoxSetting();
///
/// Gets the default setting.
///
public static AutoCompleteComboBoxSetting Default
{
get { return @default; }
set
{
if (value == null) throw new ArgumentNullException(nameof(value));
@default = value;
}
}
}
}