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
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;
|
||
|
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;
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
}
|