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.
391 lines
11 KiB
391 lines
11 KiB
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 |
|
{ |
|
/// <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)); |
|
} |
|
} |
|
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<DependencyObject>(); |
|
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<T> |
|
: DependencyObject |
|
{ |
|
static readonly DependencyProperty valueProperty = |
|
DependencyProperty.Register( |
|
"Value", |
|
typeof(T), |
|
typeof(DependencyVariable<T>) |
|
); |
|
|
|
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 |
|
{ |
|
/// <summary> |
|
/// 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 <c>true</c> |
|
/// if its text contains the query (case insensitive). |
|
/// </summary> |
|
/// <param name="query"> |
|
/// The string input by user. |
|
/// </param> |
|
/// <param name="stringFromItem"> |
|
/// The function to get a string which identifies the specified item. |
|
/// </param> |
|
/// <returns></returns> |
|
public virtual Predicate<object> GetFilter(string query, Func<object, string> stringFromItem) |
|
{ |
|
return item => stringFromItem(item).IndexOf(query, StringComparison.InvariantCultureIgnoreCase) >= 0; |
|
} |
|
|
|
/// <summary> |
|
/// 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. |
|
/// </summary> |
|
public virtual int MaxSuggestionCount |
|
{ |
|
get { return 100; } |
|
} |
|
|
|
/// <summary> |
|
/// Gets the duration to delay updating the suggestion list. |
|
/// Returns <c>Zero</c> if no delay. |
|
/// Default: 300ms. |
|
/// </summary> |
|
public virtual TimeSpan Delay |
|
{ |
|
get { return TimeSpan.FromMilliseconds(300.0); } |
|
} |
|
|
|
static AutoCompleteComboBoxSetting @default = new AutoCompleteComboBoxSetting(); |
|
|
|
/// <summary> |
|
/// Gets the default setting. |
|
/// </summary> |
|
public static AutoCompleteComboBoxSetting Default |
|
{ |
|
get { return @default; } |
|
set |
|
{ |
|
if (value == null) throw new ArgumentNullException(nameof(value)); |
|
@default = value; |
|
} |
|
} |
|
} |
|
} |