Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
TextBox.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.Generic;
5 using System.ComponentModel;
6 using System.Globalization;
7 using System.Linq;
8 using System.Text;
9 using System.Threading;
10 using System.Windows;
11 using System.Windows.Controls;
12 using System.Windows.Input;
13 using System.Windows.Media;
14 
15 namespace SiliconStudio.Presentation.Controls
16 {
17  public enum TrimmingSource
18  {
19  Begin,
20  Middle,
21  End
22  }
23 
24  /// <summary>
25  /// An implementation of the <see cref="TextBoxBase"/> control that provides additional features such as a proper
26  /// validation/cancellation workflow, and a watermark to display when the text is empty.
27  /// </summary>
28  [TemplatePart(Name = "PART_TrimmedText", Type = typeof(TextBlock))]
29  public class TextBox : TextBoxBase
30  {
31  private TextBlock trimmedTextBlock;
32  private readonly Timer validationTimer;
33 
34  /// <summary>
35  /// Identifies the <see cref="UseTimedValidation"/> dependency property.
36  /// </summary>
37  public static readonly DependencyProperty UseTimedValidationProperty = DependencyProperty.Register("UseTimedValidation", typeof(bool), typeof(TextBox), new PropertyMetadata(false, OnUseTimedValidationPropertyChanged));
38 
39  /// <summary>
40  /// Identifies the <see cref="ValidationDelay"/> dependency property.
41  /// </summary>
42  public static readonly DependencyProperty ValidationDelayProperty = DependencyProperty.Register("ValidationDelay", typeof(int), typeof(TextBox), new PropertyMetadata(500));
43 
44  /// <summary>
45  /// Identifies the <see cref="TrimmedText"/> dependency property.
46  /// </summary>
47  public static readonly DependencyPropertyKey TrimmedTextPropertyKey = DependencyProperty.RegisterReadOnly("TrimmedText", typeof(string), typeof(TextBox), new PropertyMetadata(""));
48 
49  /// <summary>
50  /// Identifies the <see cref="WatermarkContent"/> dependency property.
51  /// </summary>
52  public static readonly DependencyProperty WatermarkContentProperty = DependencyProperty.Register("WatermarkContent", typeof(object), typeof(TextBox), new PropertyMetadata(null));
53 
54  /// <summary>
55  /// Identifies the <see cref="WatermarkContentTemplate"/> dependency property.
56  /// </summary>
57  public static readonly DependencyProperty WatermarkContentTemplateProperty = DependencyProperty.Register("WatermarkContentTemplate", typeof(DataTemplate), typeof(TextBox), new PropertyMetadata(null));
58 
59  /// <summary>
60  /// Identifies the <see cref="TextTrimming"/> dependency property.
61  /// </summary>
62  public static readonly DependencyProperty TextTrimmingProperty = DependencyProperty.Register("TextTrimming", typeof(TextTrimming), typeof(TextBox), new PropertyMetadata(TextTrimming.None));
63 
64  /// <summary>
65  /// Identifies the <see cref="TrimmingSource"/> dependency property.
66  /// </summary>
67  public static readonly DependencyProperty TrimmingSourceProperty = DependencyProperty.Register("TrimmingSource", typeof(TrimmingSource), typeof(TextBox), new PropertyMetadata(TrimmingSource.End));
68 
69  /// <summary>
70  /// Identifies the <see cref="TrimmedText"/> dependency property.
71  /// </summary>
72  public static readonly DependencyProperty TrimmedTextProperty = TrimmedTextPropertyKey.DependencyProperty;
73 
74  /// <summary>
75  /// Clears the current <see cref="System.Windows.Controls.TextBox.Text"/> of a text box.
76  /// </summary>
77  public static RoutedCommand ClearTextCommand { get; private set; }
78 
79  static TextBox()
80  {
81  DefaultStyleKeyProperty.OverrideMetadata(typeof(TextBox), new FrameworkPropertyMetadata(typeof(TextBox)));
82  ClearTextCommand = new RoutedCommand("ClearTextCommand", typeof(System.Windows.Controls.TextBox));
83  CommandManager.RegisterClassCommandBinding(typeof(System.Windows.Controls.TextBox), new CommandBinding(ClearTextCommand, OnClearTextCommand));
84  }
85 
86  public TextBox()
87  {
88  WordSeparators = " \t";
89  if (DesignerProperties.GetIsInDesignMode(this) == false)
90  validationTimer = new Timer(x => Dispatcher.InvokeAsync(Validate), null, Timeout.Infinite, Timeout.Infinite);
91  }
92 
93  /// <summary>
94  /// Gets or sets whether the text should be automatically validated after a delay defined by the <see cref="ValidationDelay"/> property.
95  /// </summary>
96  public bool UseTimedValidation { get { return (bool)GetValue(UseTimedValidationProperty); } set { SetValue(UseTimedValidationProperty, value); } }
97 
98  /// <summary>
99  /// Gets or sets the amount of time before a validation of input text happens, in milliseconds.
100  /// Every change to the <see cref="TextBox.Text"/> property reset the timer to this value.
101  /// </summary>
102  /// <remarks>The default value is <c>500</c> milliseconds.</remarks>
103  public int ValidationDelay { get { return (int)GetValue(ValidationDelayProperty); } set { SetValue(ValidationDelayProperty, value); } }
104 
105  /// <summary>
106  /// Gets the trimmed text to display when the control does not have the focus, depending of the value of the <see cref="TextTrimming"/> property.
107  /// </summary>
108  public string TrimmedText { get { return (string)GetValue(TrimmedTextPropertyKey.DependencyProperty); } private set { SetValue(TrimmedTextPropertyKey, value); } }
109 
110  /// <summary>
111  /// Gets or sets the content to display when the TextBox is empty.
112  /// </summary>
113  public object WatermarkContent { get { return GetValue(WatermarkContentProperty); } set { SetValue(WatermarkContentProperty, value); } }
114 
115  /// <summary>
116  /// Gets or sets the template of the content to display when the TextBox is empty.
117  /// </summary>
118  public DataTemplate WatermarkContentTemplate { get { return (DataTemplate)GetValue(WatermarkContentTemplateProperty); } set { SetValue(WatermarkContentTemplateProperty, value); } }
119 
120  /// <summary>
121  /// Gets or sets how to trim the text when the control does not have the focus.
122  /// </summary>
123  public TextTrimming TextTrimming { get { return (TextTrimming)GetValue(TextTrimmingProperty); } set { SetValue(TextTrimmingProperty, value); } }
124 
125  /// <summary>
126  /// Gets or sets the source position of the text trimming when the control does not have the focus.
127  /// </summary>
128  public TrimmingSource TrimmingSource { get { return (TrimmingSource)GetValue(TrimmingSourceProperty); } set { SetValue(TrimmingSourceProperty, value); } }
129 
130  /// <summary>
131  /// Gets or sets the charcters used to separate word when computing trimming using <see cref="System.Windows.TextTrimming.WordEllipsis"/>.
132  /// </summary>
133  public string WordSeparators { get; set; }
134 
135  /// <inheritdoc/>
136  public override void OnApplyTemplate()
137  {
138  base.OnApplyTemplate();
139 
140  trimmedTextBlock = GetTemplateChild("PART_TrimmedText") as TextBlock;
141  if (trimmedTextBlock == null)
142  throw new InvalidOperationException("A part named 'PART_TrimmedText' must be present in the ControlTemplate, and must be of type 'TextBlock'.");
143  }
144 
145  /// <summary>
146  /// Raised when the text of the TextBox changes.
147  /// </summary>
148  /// <param name="oldValue">The old value of the <see cref="TextBox.Text"/> property.</param>
149  /// <param name="newValue">The new value of the <see cref="TextBox.Text"/> property.</param>
150  protected override void OnTextChanged(string oldValue, string newValue)
151  {
152  if (UseTimedValidation)
153  {
154  if (ValidationDelay > 0.0)
155  {
156  if (validationTimer != null)
157  validationTimer.Change(ValidationDelay, Timeout.Infinite);
158  }
159  else
160  {
161  Validate();
162  }
163  }
164 
165  var availableWidth = ActualWidth;
166  if (trimmedTextBlock != null)
167  availableWidth -= trimmedTextBlock.Margin.Left + trimmedTextBlock.Margin.Right;
168 
169  PerformEllipsis(availableWidth);
170  }
171 
172  protected override Size ArrangeOverride(Size arrangeBounds)
173  {
174  var arrangedSize = base.ArrangeOverride(arrangeBounds);
175  var availableWidth = arrangeBounds.Width;
176  if (trimmedTextBlock != null)
177  availableWidth -= trimmedTextBlock.Margin.Left + trimmedTextBlock.Margin.Right;
178 
179  PerformEllipsis(availableWidth);
180  return arrangedSize;
181  }
182 
183  private void PerformEllipsis(double availableWidth)
184  {
185  if (TextTrimming == TextTrimming.None)
186  {
187  TrimmedText = Text;
188  return;
189  }
190  // TODO: optim
191  //// Don't process the text if the current available width is the same that the current trimmed text width
192  //double epsilon = GetTextWidth(".");
193  //if (Math.Abs(availableWidth - trimmedTextWidth) < epsilon)
194  // return arrangedSize;
195 
196  double[] sizes;
197  var textWidth = GetTextWidth(Text, out sizes);
198  if (availableWidth >= textWidth)
199  {
200  TrimmedText = Text;
201  return;
202  }
203 
204  string[] text;
205 
206  switch (TextTrimming)
207  {
208  case TextTrimming.CharacterEllipsis:
209  text = Text.ToCharArray().Select(c => c.ToString(CultureInfo.InvariantCulture)).ToArray();
210  break;
211  case TextTrimming.WordEllipsis:
212  text = SplitWords(Text);
213  break;
214  default:
215  throw new ArgumentException("Invalid 'TextTrimming' argument.");
216  }
217 
218  const string Ellipsis = "...";
219  if (TrimmingSource == TrimmingSource.Begin)
220  {
221  var n = text.Length - 1;
222  var sb = new StringBuilder();
223 
224  //for (var i = 0; i < Offset; i++)
225  // sb.Append(text[i]);
226  sb.Append(Ellipsis);
227 
228  var starting = sb.ToString();
229  sb.Clear();
230  var currentWidth = GetTextWidth(starting);
231 
232  while (true)
233  {
234  var test = currentWidth + sizes[n];
235 
236  if (test > availableWidth)
237  break;
238 
239  sb.Insert(0, text[n--]);
240  currentWidth = test;
241  }
242 
243  TrimmedText = string.Format("{0}{1}", starting, sb);
244  }
245  else if (TrimmingSource == TrimmingSource.Middle)
246  {
247  var currentWidth = GetTextWidth(Ellipsis);
248  var n1 = 0;
249  var n2 = text.Length - 1;
250 
251  var begin = new StringBuilder();
252  var end = new StringBuilder();
253 
254  while (true)
255  {
256  var test = currentWidth + sizes[n1] + sizes[n2];
257 
258  if (test > availableWidth)
259  break;
260 
261  begin.Append(text[n1++]);
262  end.Insert(0, text[n2--]);
263 
264  currentWidth = test;
265  }
266 
267  TrimmedText = string.Format("{0}{2}{1}", begin, end, Ellipsis);
268  }
269  else if (TrimmingSource == TrimmingSource.End)
270  {
271  var n = 0;
272  var sb = new StringBuilder();
273  // TODO: allow to skip some characters before trimming (ie. keep the extension at the end for instance)
274  //for (var i = 0; i < Offset; i++)
275  // sb.Insert(0, text[text.Length - i - 1]);
276  sb.Insert(0, Ellipsis);
277  var ending = sb.ToString();
278  sb.Clear();
279  var currentWidth = GetTextWidth(ending);
280 
281  while (true)
282  {
283  var test = currentWidth + sizes[n];
284 
285  if (test > availableWidth)
286  break;
287 
288  sb.Append(text[n++]);
289  currentWidth = test;
290  }
291 
292  TrimmedText = string.Format("{0}{1}", sb, ending);
293  }
294  }
295 
296  private double GetTextWidth(string text)
297  {
298  double[] dummy;
299  return GetTextWidth(text, out dummy);
300  }
301 
302  private double GetTextWidth(string text, out double[] sizes)
303  {
304  var typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch);
305  var totalWidth = 0.0;
306  // We use a period to ensure space characters will have their actual size used.
307  var period = new FormattedText(".", CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, typeface, FontSize, Brushes.Black);
308  double periodWidth = period.Width;
309 
310  if (TextTrimming == TextTrimming.CharacterEllipsis)
311  {
312  sizes = new double[text.Length];
313 
314  for (var i = 0; i < text.Length; i++)
315  {
316  string token = text[i].ToString(CultureInfo.CurrentUICulture) + ".";
317  var formattedText = new FormattedText(token, CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, typeface, FontSize, Brushes.Black);
318  double width = formattedText.Width - periodWidth;
319 
320  sizes[i] = width;
321  totalWidth += width;
322  }
323  }
324  else if (TextTrimming == TextTrimming.WordEllipsis)
325  {
326  var words = SplitWords(text);
327  sizes = new double[words.Length];
328 
329  for (var i = 0; i < words.Length; i++)
330  {
331  string token = words[i] + ".";
332  var formattedText = new FormattedText(token, CultureInfo.CurrentUICulture, FlowDirection.LeftToRight, typeface, FontSize, Brushes.Black);
333  double width = formattedText.Width - periodWidth;
334  sizes[i] = width;
335  totalWidth += width;
336  }
337  }
338  else
339  throw new ArgumentException("Invalid 'TextTrimming' argument.");
340 
341  return totalWidth;
342  }
343 
344  private string[] SplitWords(string text)
345  {
346  var words = new List<string>();
347 
348  var sb = new StringBuilder();
349  foreach (char c in text)
350  {
351  if (WordSeparators.Contains(c))
352  {
353  if (sb.Length > 0)
354  words.Add(sb.ToString());
355 
356  sb.Clear();
357 
358  words.Add(c.ToString(CultureInfo.InvariantCulture));
359  continue;
360  }
361 
362  sb.Append(c);
363  }
364 
365  if (sb.Length > 0)
366  words.Add(sb.ToString());
367 
368  return words.ToArray();
369  }
370 
371  private static void OnUseTimedValidationPropertyChanged(DependencyObject sender, DependencyPropertyChangedEventArgs e)
372  {
373  var txt = (TextBox)sender;
374  if ((bool)e.NewValue)
375  {
376  txt.Validate();
377  }
378  }
379 
380  private static void OnClearTextCommand(object sender, ExecutedRoutedEventArgs e)
381  {
382  var textBox = sender as TextBox;
383  if (textBox != null)
384  {
385  textBox.Clear();
386  }
387  }
388  }
389 }
An implementation of the TextBoxBase control that provides additional features such as a proper valid...
Definition: TextBox.cs:29
override void OnTextChanged(string oldValue, string newValue)
Raised when the text of the TextBox changes.
Definition: TextBox.cs:150
An implementation of the System.Windows.Controls.TextBox control that provides additional features su...
Definition: TextBoxBase.cs:13
SiliconStudio.Paradox.Graphics.Font.FontStyle FontStyle
override Size ArrangeOverride(Size arrangeBounds)
Definition: TextBox.cs:172