Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
FilteringComboBox.cs
Go to the documentation of this file.
1 // Copyright (c) 2014 Silicon Studio Corp. (http://siliconstudio.co.jp)
2 // This file is distributed under GPL v3. See LICENSE.md for details.
3 using System;
4 using System.Collections;
5 using System.Linq;
6 using System.Threading.Tasks;
7 using System.Windows;
8 using System.Windows.Controls;
9 using System.Windows.Controls.Primitives;
10 using System.Windows.Data;
11 using System.Windows.Input;
12 
13 using SiliconStudio.Presentation.Core;
14 using SiliconStudio.Presentation.Extensions;
15 
16 namespace SiliconStudio.Presentation.Controls
17 {
18  [TemplatePart(Name = "PART_EditableTextBox", Type = typeof(TextBox))]
19  [TemplatePart(Name = "PART_ListBox", Type = typeof(ListBox))]
20  public class FilteringComboBox : Selector
21  {
23  {
24  private string token;
25  private string tokenLowercase;
26 
27  public string Token { get { return token; } set { token = value; tokenLowercase = (value ?? "").ToLowerInvariant(); } }
28 
29  public int Compare(object x, object y)
30  {
31  var a = x.ToString();
32  var b = y.ToString();
33 
34  if (string.IsNullOrWhiteSpace(token))
35  return string.Compare(a, b, StringComparison.InvariantCultureIgnoreCase);
36 
37  var indexA = a.IndexOf(tokenLowercase, StringComparison.InvariantCultureIgnoreCase);
38  var indexB = b.IndexOf(tokenLowercase, StringComparison.InvariantCultureIgnoreCase);
39 
40  if (indexA == 0 && indexB > 0)
41  return -1;
42  if (indexB == 0 && indexA > 0)
43  return 1;
44 
45  return string.Compare(a, b, StringComparison.InvariantCultureIgnoreCase);
46  }
47  }
48 
49  /// <summary>
50  /// The instance of <see cref="FilteringComboBoxSort"/> used for filtering and sorting items.
51  /// </summary>
52  private readonly FilteringComboBoxSort sort;
53 
54  /// <summary>
55  /// The input text box.
56  /// </summary>
57  private TextBox editableTextBox;
58 
59  /// <summary>
60  /// The filtered list box.
61  /// </summary>
62  private ListBox listBox;
63  /// <summary>
64  /// Indicates that the selection is being internally cleared and that the drop down should not be opened nor refreshed.
65  /// </summary>
66  ///
67  private bool clearing;
68  /// <summary>
69  /// Indicates that the selection is being internally updated and that the text should not be cleared.
70  /// </summary>
71  private bool updatingSelection;
72 
73  /// <summary>
74  /// Indicates that the text box is being validated and that the update of the text should not impact the selected item.
75  /// </summary>
76  private bool validating;
77 
78  public static readonly DependencyProperty IsDropDownOpenProperty = DependencyProperty.Register("IsDropDownOpen", typeof(bool), typeof(FilteringComboBox));
79 
80  public static readonly DependencyProperty ClearTextAfterValidationProperty = DependencyProperty.Register("ClearTextAfterValidation", typeof(bool), typeof(FilteringComboBox));
81 
82  /// <summary>
83  /// Identifies the <see cref="WatermarkContent"/> dependency property.
84  /// </summary>
85  public static readonly DependencyProperty WatermarkContentProperty = DependencyProperty.Register("WatermarkContent", typeof(object), typeof(FilteringComboBox), new PropertyMetadata(null));
86 
87  public static readonly DependencyProperty ItemsToExcludeProperty = DependencyProperty.Register("ItemsToExclude", typeof(IEnumerable), typeof(FilteringComboBox));
88 
89  /// <summary>
90  /// Raised just before the TextBox changes are validated. This event is cancellable
91  /// </summary>
92  public static readonly RoutedEvent ValidatingEvent = EventManager.RegisterRoutedEvent("Validating", RoutingStrategy.Bubble, typeof(CancelRoutedEventHandler), typeof(FilteringComboBox));
93 
94  /// <summary>
95  /// Raised when TextBox changes have been validated.
96  /// </summary>
97  public static readonly RoutedEvent ValidatedEvent = EventManager.RegisterRoutedEvent("Validated", RoutingStrategy.Bubble, typeof(ValidationRoutedEventHandler<string>), typeof(FilteringComboBox));
98 
100  {
101  sort = new FilteringComboBoxSort();
102  IsTextSearchEnabled = false;
103  }
104 
105  public bool IsDropDownOpen { get { return (bool)GetValue(IsDropDownOpenProperty); } set { SetValue(IsDropDownOpenProperty, value); } }
106 
107  public bool ClearTextAfterValidation { get { return (bool)GetValue(ClearTextAfterValidationProperty); } set { SetValue(ClearTextAfterValidationProperty, value); } }
108 
109  /// <summary>
110  /// Gets or sets the content to display when the TextBox is empty.
111  /// </summary>
112  public object WatermarkContent { get { return GetValue(WatermarkContentProperty); } set { SetValue(WatermarkContentProperty, value); } }
113 
114  public IEnumerable ItemsToExclude { get { return (IEnumerable)GetValue(ItemsToExcludeProperty); } set { SetValue(ItemsToExcludeProperty, value); } }
115 
116  /// <summary>
117  /// Raised just before the TextBox changes are validated. This event is cancellable
118  /// </summary>
119  public event CancelRoutedEventHandler Validating { add { AddHandler(ValidatingEvent, value); } remove { RemoveHandler(ValidatingEvent, value); } }
120 
121  /// <summary>
122  /// Raised when TextBox changes have been validated.
123  /// </summary>
124  public event ValidationRoutedEventHandler<string> Validated { add { AddHandler(ValidatedEvent, value); } remove { RemoveHandler(ValidatedEvent, value); } }
125 
126  protected override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
127  {
128  base.OnItemsSourceChanged(oldValue, newValue);
129  if (newValue != null)
130  {
131  var cvs = (CollectionView)CollectionViewSource.GetDefaultView(newValue);
132  cvs.Filter = Filter;
133  var listCollectionView = cvs as ListCollectionView;
134  if (listCollectionView != null)
135  {
136  listCollectionView.CustomSort = sort;
137  }
138  }
139  }
140 
141  public override void OnApplyTemplate()
142  {
143  base.OnApplyTemplate();
144 
145  editableTextBox = GetTemplateChild("PART_EditableTextBox") as TextBox;
146  if (editableTextBox == null)
147  throw new InvalidOperationException("A part named 'PART_EditableTextBox' must be present in the ControlTemplate, and must be of type 'SiliconStudio.Presentation.Controls.Input.TextBox'.");
148 
149  listBox = GetTemplateChild("PART_ListBox") as ListBox;
150  if (listBox == null)
151  throw new InvalidOperationException("A part named 'PART_ListBox' must be present in the ControlTemplate, and must be of type 'ListBox'.");
152 
153  editableTextBox.TextChanged += EditableTextBoxTextChanged;
154  editableTextBox.PreviewKeyDown += EditableTextBoxPreviewKeyDown;
155  editableTextBox.Validating += EditableTextBoxValidating;
156  editableTextBox.Validated += EditableTextBoxValidated;
157  editableTextBox.Cancelled += EditableTextBoxCancelled;
158  editableTextBox.LostFocus += EditableTextBoxLostFocus;
159  listBox.PreviewMouseUp += ListBoxMouseUp;
160  }
161 
162  protected override void OnSelectionChanged(SelectionChangedEventArgs e)
163  {
164  base.OnSelectionChanged(e);
165  if (SelectedItem == null && !updatingSelection)
166  {
167  clearing = true;
168  editableTextBox.Clear();
169  clearing = false;
170  }
171  }
172 
173  private void UpdateText()
174  {
175  if (listBox.SelectedItem != null)
176  {
177  editableTextBox.Text = listBox.SelectedItem.ToString();
178  IsDropDownOpen = false;
179  }
180  }
181 
182  private void EditableTextBoxValidating(object sender, CancelRoutedEventArgs e)
183  {
184  // This may happens somehow when the template is refreshed.
185  if (!ReferenceEquals(sender, editableTextBox))
186  return;
187 
188  validating = true;
189  UpdateText();
190  validating = false;
191 
192  var cancelRoutedEventArgs = new CancelRoutedEventArgs(ValidatingEvent);
193  RaiseEvent(cancelRoutedEventArgs);
194  if (cancelRoutedEventArgs.Cancel)
195  e.Cancel = true;
196  }
197 
198  private void EditableTextBoxValidated(object sender, ValidationRoutedEventArgs<string> e)
199  {
200  // This may happens somehow when the template is refreshed.
201  if (!ReferenceEquals(sender, editableTextBox))
202  return;
203 
204  var validatedArgs = new RoutedEventArgs(ValidatedEvent);
205  RaiseEvent(validatedArgs);
206 
207  if (ClearTextAfterValidation)
208  {
209  clearing = true;
210  editableTextBox.Text = string.Empty;
211  clearing = false;
212  }
213  }
214 
215  private async void EditableTextBoxCancelled(object sender, RoutedEventArgs e)
216  {
217  // This may happens somehow when the template is refreshed.
218  if (!ReferenceEquals(sender, editableTextBox))
219  return;
220 
221  clearing = true;
222  editableTextBox.Text = string.Empty;
223  // Defer closing the popup in case we lost the focus because of a click in the list box - so it can still raise the correct event
224  // This is a very hackish, we should find a better way to do it!
225  await Task.Delay(100);
226  IsDropDownOpen = false;
227  clearing = false;
228  }
229 
230  private async void EditableTextBoxLostFocus(object sender, RoutedEventArgs e)
231  {
232  // This may happens somehow when the template is refreshed.
233  if (!ReferenceEquals(sender, editableTextBox))
234  return;
235 
236  clearing = true;
237  // Defer closing the popup in case we lost the focus because of a click in the list box - so it can still raise the correct event
238  // This is a very hackish, we should find a better way to do it!
239  await Task.Delay(100);
240  IsDropDownOpen = false;
241  clearing = false;
242  }
243 
244  private void ListBoxMouseUp(object sender, MouseButtonEventArgs e)
245  {
246  if (e.ChangedButton == MouseButton.Left && listBox.SelectedIndex > -1)
247  {
248  UpdateText();
249  editableTextBox.Validate();
250  }
251  }
252 
253  private void EditableTextBoxTextChanged(object sender, TextChangedEventArgs e)
254  {
255  if (ItemsSource == null)
256  return;
257 
258  updatingSelection = true;
259  if (!IsDropDownOpen && !clearing)
260  {
261  // Setting IsDropDownOpen to true will select all the text. We don't want this behavior, so let's save and restore the caret index.
262  var index = editableTextBox.CaretIndex;
263  IsDropDownOpen = true;
264  editableTextBox.CaretIndex = index;
265  }
266  sort.Token = editableTextBox.Text;
267  var cvs = CollectionViewSource.GetDefaultView(ItemsSource);
268  cvs.Refresh();
269  if (listBox.Items.Count > 0 && !validating)
270  {
271  listBox.SelectedIndex = 0;
272  }
273  updatingSelection = false;
274  }
275 
276  private void EditableTextBoxPreviewKeyDown(object sender, KeyEventArgs e)
277  {
278  if (listBox.Items.Count > 0)
279  {
280  updatingSelection = true;
281  if (e.Key == Key.Escape)
282  {
283  IsDropDownOpen = false;
284  }
285  if (e.Key == Key.Up)
286  {
287  listBox.SelectedIndex = Math.Max(listBox.SelectedIndex - 1, 0);
288  if (listBox.SelectedItem != null)
289  listBox.ScrollIntoView(listBox.SelectedItem);
290  }
291  if (e.Key == Key.Down)
292  {
293  listBox.SelectedIndex = Math.Min(listBox.SelectedIndex + 1, listBox.Items.Count - 1);
294  if (listBox.SelectedItem != null)
295  listBox.ScrollIntoView(listBox.SelectedItem);
296  }
297  if (e.Key == Key.PageUp)
298  {
299  var stackPanel = listBox.FindVisualChildOfType<VirtualizingStackPanel>();
300  if (stackPanel != null)
301  {
302  var count = stackPanel.Children.Count;
303  listBox.SelectedIndex = Math.Max(listBox.SelectedIndex - count, 0);
304  }
305  else
306  {
307  listBox.SelectedIndex = 0;
308  }
309  if (listBox.SelectedItem != null)
310  listBox.ScrollIntoView(listBox.SelectedItem);
311  }
312  if (e.Key == Key.PageDown)
313  {
314  var stackPanel = listBox.FindVisualChildOfType<VirtualizingStackPanel>();
315  if (stackPanel != null)
316  {
317  var count = stackPanel.Children.Count;
318  listBox.SelectedIndex = Math.Min(listBox.SelectedIndex + count, listBox.Items.Count - 1);
319  }
320  else
321  {
322  listBox.SelectedIndex = listBox.Items.Count - 1;
323  }
324  if (listBox.SelectedItem != null)
325  listBox.ScrollIntoView(listBox.SelectedItem);
326  }
327  if (e.Key == Key.Home)
328  {
329  listBox.SelectedIndex = 0;
330  }
331  if (e.Key == Key.End)
332  {
333  listBox.SelectedIndex = listBox.Items.Count - 1;
334  }
335  updatingSelection = false;
336  }
337  }
338 
339  private bool Filter(object obj)
340  {
341  if (editableTextBox == null)
342  return true;
343 
344  var filter = editableTextBox.Text;
345  if (string.IsNullOrWhiteSpace(filter))
346  return true;
347 
348  if (obj == null)
349  return false;
350 
351  if (ItemsToExclude != null && ItemsToExclude.Cast<object>().Contains(obj))
352  return false;
353 
354  var text = obj.ToString();
355  return text.IndexOf(filter, StringComparison.InvariantCultureIgnoreCase) > -1 || MatchCamelCase(text);
356  }
357 
358  private bool MatchCamelCase(string text)
359  {
360  var camelCaseSplit = text.CamelCaseSplit();
361  var filter = editableTextBox.Text.ToLowerInvariant();
362  int currentFilterChar = 0;
363 
364  foreach (var word in camelCaseSplit)
365  {
366  int currentWordChar = 0;
367  while (currentFilterChar > 0)
368  {
369  if (char.ToLower(word[currentWordChar]) == filter[currentFilterChar])
370  break;
371  --currentFilterChar;
372  }
373 
374  while (char.ToLower(word[currentWordChar]) == filter[currentFilterChar])
375  {
376  ++currentWordChar;
377  ++currentFilterChar;
378  if (currentFilterChar == filter.Length)
379  return true;
380 
381  if (currentWordChar == word.Length)
382  break;
383  }
384  }
385  return currentFilterChar == filter.Length;
386  }
387  }
388 }
override void OnItemsSourceChanged(IEnumerable oldValue, IEnumerable newValue)
function b
MouseButton
Mouse buttons.
Definition: MouseButton.cs:10
An implementation of the TextBoxBase control that provides additional features such as a proper valid...
Definition: TextBox.cs:29
function a
_In_ size_t _In_ DXGI_FORMAT _In_ size_t _In_ float size_t y
Definition: DirectXTexP.h:191
_In_ size_t count
Definition: DirectXTexP.h:174
override void OnSelectionChanged(SelectionChangedEventArgs e)
delegate void CancelRoutedEventHandler(object sender, CancelRoutedEventArgs e)