Paradox Game Engine  v1.0.0 beta06
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Properties Events Macros Pages
TextLogViewer.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.Collections.Specialized;
6 using System.Linq;
7 using System.Windows;
8 using System.Windows.Controls;
9 using System.Windows.Controls.Primitives;
10 using System.Windows.Documents;
11 using System.Windows.Media;
12 
13 using SiliconStudio.Core.Diagnostics;
14 
15 namespace SiliconStudio.Presentation.Controls
16 {
17  /// <summary>
18  /// This control displays a collection of <see cref="ILogMessage"/>.
19  /// </summary>
20  [TemplatePart(Name = "PART_LogTextBox", Type = typeof(RichTextBox))]
21  [TemplatePart(Name = "PART_ClearLog", Type = typeof(ButtonBase))]
22  [TemplatePart(Name = "PART_PreviousResult", Type = typeof(ButtonBase))]
23  [TemplatePart(Name = "PART_NextResult", Type = typeof(ButtonBase))]
24  public class TextLogViewer : Control
25  {
26  private readonly List<TextRange> searchMatches = new List<TextRange>();
27  private int currentResult;
28 
29  /// <summary>
30  /// The <see cref="RichTextBox"/> in which the log messages are actually displayed.
31  /// </summary>
32  private RichTextBox logTextBox;
33 
34  /// <summary>
35  /// Identifies the <see cref="LogMessages"/> dependency property.
36  /// </summary>
37  public static readonly DependencyProperty LogMessagesProperty = DependencyProperty.Register("LogMessages", typeof(ICollection<ILogMessage>), typeof(TextLogViewer), new PropertyMetadata(null, LogMessagesPropertyChanged));
38 
39  /// <summary>
40  /// Identifies the <see cref="AutoScroll"/> dependency property.
41  /// </summary>
42  public static readonly DependencyProperty AutoScrollProperty = DependencyProperty.Register("AutoScroll", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(true));
43 
44  /// <summary>
45  /// Identifies the <see cref="IsToolBarVisible"/> dependency property.
46  /// </summary>
47  public static readonly DependencyProperty IsToolBarVisibleProperty = DependencyProperty.Register("IsToolBarVisible", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(true));
48 
49  /// <summary>
50  /// Identifies the <see cref="CanClearLog"/> dependency property.
51  /// </summary>
52  public static readonly DependencyProperty CanClearLogProperty = DependencyProperty.Register("CanClearLog", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(true));
53 
54  /// <summary>
55  /// Identifies the <see cref="CanFilterLog"/> dependency property.
56  /// </summary>
57  public static readonly DependencyProperty CanFilterLogProperty = DependencyProperty.Register("CanFilterLog", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(true));
58 
59  /// <summary>
60  /// Identifies the <see cref="CanSearchLog"/> dependency property.
61  /// </summary>
62  public static readonly DependencyProperty CanSearchLogProperty = DependencyProperty.Register("CanSearchLog", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(true));
63 
64  /// <summary>
65  /// Identifies the <see cref="SearchToken"/> dependency property.
66  /// </summary>
67  public static readonly DependencyProperty SearchTokenProperty = DependencyProperty.Register("SearchToken", typeof(string), typeof(TextLogViewer), new PropertyMetadata("", SearchTokenChanged));
68 
69  /// <summary>
70  /// Identifies the <see cref="SearchMatchCase"/> dependency property.
71  /// </summary>
72  public static readonly DependencyProperty SearchMatchCaseProperty = DependencyProperty.Register("SearchMatchCase", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(false, SearchTokenChanged));
73 
74  /// <summary>
75  /// Identifies the <see cref="SearchMatchWord"/> dependency property.
76  /// </summary>
77  public static readonly DependencyProperty SearchMatchWordProperty = DependencyProperty.Register("SearchMatchWord", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(false, SearchTokenChanged));
78 
79  /// <summary>
80  /// Identifies the <see cref="SearchMatchBrush"/> dependency property.
81  /// </summary>
82  public static readonly DependencyProperty SearchMatchBrushProperty = DependencyProperty.Register("SearchMatchBrush", typeof(Brush), typeof(TextLogViewer), new PropertyMetadata(Brushes.LightSteelBlue, TextPropertyChanged));
83 
84  /// <summary>
85  /// Identifies the <see cref="DebugBrush"/> dependency property.
86  /// </summary>
87  public static readonly DependencyProperty DebugBrushProperty = DependencyProperty.Register("DebugBrush", typeof(Brush), typeof(TextLogViewer), new PropertyMetadata(Brushes.White, TextPropertyChanged));
88 
89  /// <summary>
90  /// Identifies the <see cref="VerboseBrush"/> dependency property.
91  /// </summary>
92  public static readonly DependencyProperty VerboseBrushProperty = DependencyProperty.Register("VerboseBrush", typeof(Brush), typeof(TextLogViewer), new PropertyMetadata(Brushes.White, TextPropertyChanged));
93 
94  /// <summary>
95  /// Identifies the <see cref="InfoBrush"/> dependency property.
96  /// </summary>
97  public static readonly DependencyProperty InfoBrushProperty = DependencyProperty.Register("InfoBrush", typeof(Brush), typeof(TextLogViewer), new PropertyMetadata(Brushes.White, TextPropertyChanged));
98 
99  /// <summary>
100  /// Identifies the <see cref="WarningBrush"/> dependency property.
101  /// </summary>
102  public static readonly DependencyProperty WarningBrushProperty = DependencyProperty.Register("WarningBrush", typeof(Brush), typeof(TextLogViewer), new PropertyMetadata(Brushes.White, TextPropertyChanged));
103 
104  /// <summary>
105  /// Identifies the <see cref="ErrorBrush"/> dependency property.
106  /// </summary>
107  public static readonly DependencyProperty ErrorBrushProperty = DependencyProperty.Register("ErrorBrush", typeof(Brush), typeof(TextLogViewer), new PropertyMetadata(Brushes.White, TextPropertyChanged));
108 
109  /// <summary>
110  /// Identifies the <see cref="FatalBrush"/> dependency property.
111  /// </summary>
112  public static readonly DependencyProperty FatalBrushProperty = DependencyProperty.Register("FatalBrush", typeof(Brush), typeof(TextLogViewer), new PropertyMetadata(Brushes.White, TextPropertyChanged));
113 
114  /// <summary>
115  /// Identifies the <see cref="ShowDebugMessages"/> dependency property.
116  /// </summary>
117  public static readonly DependencyProperty ShowDebugMessagesProperty = DependencyProperty.Register("ShowDebugMessages", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(true, TextPropertyChanged));
118 
119  /// <summary>
120  /// Identifies the <see cref="ShowVerboseMessages"/> dependency property.
121  /// </summary>
122  public static readonly DependencyProperty ShowVerboseMessagesProperty = DependencyProperty.Register("ShowVerboseMessages", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(true, TextPropertyChanged));
123 
124  /// <summary>
125  /// Identifies the <see cref="ShowInfoMessages"/> dependency property.
126  /// </summary>
127  public static readonly DependencyProperty ShowInfoMessagesProperty = DependencyProperty.Register("ShowInfoMessages", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(true, TextPropertyChanged));
128 
129  /// <summary>
130  /// Identifies the <see cref="ShowWarningMessages"/> dependency property.
131  /// </summary>
132  public static readonly DependencyProperty ShowWarningMessagesProperty = DependencyProperty.Register("ShowWarningMessages", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(true, TextPropertyChanged));
133 
134  /// <summary>
135  /// Identifies the <see cref="ShowErrorMessages"/> dependency property.
136  /// </summary>
137  public static readonly DependencyProperty ShowErrorMessagesProperty = DependencyProperty.Register("ShowErrorMessages", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(true, TextPropertyChanged));
138 
139  /// <summary>
140  /// Identifies the <see cref="ShowFatalMessages"/> dependency property.
141  /// </summary>
142  public static readonly DependencyProperty ShowFatalMessagesProperty = DependencyProperty.Register("ShowFatalMessages", typeof(bool), typeof(TextLogViewer), new PropertyMetadata(true, TextPropertyChanged));
143 
144  /// <summary>
145  /// Gets or sets the collection of <see cref="ILogMessage"/> to display.
146  /// </summary>
147  public ICollection<ILogMessage> LogMessages { get { return (ICollection<ILogMessage>)GetValue(LogMessagesProperty); } set { SetValue(LogMessagesProperty, value); } }
148 
149  /// <summary>
150  /// Gets or sets whether the control should automatically scroll when new lines are added when the scrollbar is already at the bottom.
151  /// </summary>
152  public bool AutoScroll { get { return (bool)GetValue(AutoScrollProperty); } set { SetValue(AutoScrollProperty, value); } }
153 
154  /// <summary>
155  /// Gets or sets whether the tool bar should be visible.
156  /// </summary>
157  public bool IsToolBarVisible { get { return (bool)GetValue(IsToolBarVisibleProperty); } set { SetValue(IsToolBarVisibleProperty, value); } }
158 
159  /// <summary>
160  /// Gets or sets whether it is possible to clear the log text.
161  /// </summary>
162  public bool CanClearLog { get { return (bool)GetValue(CanClearLogProperty); } set { SetValue(CanClearLogProperty, value); } }
163 
164  /// <summary>
165  /// Gets or sets whether it is possible to filter the log text.
166  /// </summary>
167  public bool CanFilterLog { get { return (bool)GetValue(CanFilterLogProperty); } set { SetValue(CanFilterLogProperty, value); } }
168 
169  /// <summary>
170  /// Gets or sets whether it is possible to search the log text.
171  /// </summary>
172  public bool CanSearchLog { get { return (bool)GetValue(CanSearchLogProperty); } set { SetValue(CanSearchLogProperty, value); } }
173 
174  /// <summary>
175  /// Gets or sets the current search token.
176  /// </summary>
177  public string SearchToken { get { return (string)GetValue(SearchTokenProperty); } set { SetValue(SearchTokenProperty, value); } }
178 
179  /// <summary>
180  /// Gets or sets whether the search result should match the case.
181  /// </summary>
182  public bool SearchMatchCase { get { return (bool)GetValue(SearchMatchCaseProperty); } set { SetValue(SearchMatchCaseProperty, value); } }
183 
184  /// <summary>
185  /// Gets or sets whether the search result should match whole words only.
186  /// </summary>
187  public bool SearchMatchWord { get { return (bool)GetValue(SearchMatchWordProperty); } set { SetValue(SearchMatchWordProperty, value); } }
188 
189  /// <summary>
190  /// Gets or sets the brush used to emphasize search results.
191  /// </summary>
192  public Brush SearchMatchBrush { get { return (Brush)GetValue(SearchMatchBrushProperty); } set { SetValue(SearchMatchBrushProperty, value); } }
193 
194  /// <summary>
195  /// Gets or sets the brush used to emphasize debug messages.
196  /// </summary>
197  public Brush DebugBrush { get { return (Brush)GetValue(DebugBrushProperty); } set { SetValue(DebugBrushProperty, value); } }
198 
199  /// <summary>
200  /// Gets or sets the brush used to emphasize verbose messages.
201  /// </summary>
202  public Brush VerboseBrush { get { return (Brush)GetValue(VerboseBrushProperty); } set { SetValue(VerboseBrushProperty, value); } }
203 
204  /// <summary>
205  /// Gets or sets the brush used to emphasize info messages.
206  /// </summary>
207  public Brush InfoBrush { get { return (Brush)GetValue(InfoBrushProperty); } set { SetValue(InfoBrushProperty, value); } }
208 
209  /// <summary>
210  /// Gets or sets the brush used to emphasize warning messages.
211  /// </summary>
212  public Brush WarningBrush { get { return (Brush)GetValue(WarningBrushProperty); } set { SetValue(WarningBrushProperty, value); } }
213 
214  /// <summary>
215  /// Gets or sets the brush used to emphasize error messages.
216  /// </summary>
217  public Brush ErrorBrush { get { return (Brush)GetValue(ErrorBrushProperty); } set { SetValue(ErrorBrushProperty, value); } }
218 
219  /// <summary>
220  /// Gets or sets the brush used to emphasize fatal messages.
221  /// </summary>
222  public Brush FatalBrush { get { return (Brush)GetValue(FatalBrushProperty); } set { SetValue(FatalBrushProperty, value); } }
223 
224  /// <summary>
225  /// Gets or sets whether the log viewer should display debug messages.
226  /// </summary>
227  public bool ShowDebugMessages { get { return (bool)GetValue(ShowDebugMessagesProperty); } set { SetValue(ShowDebugMessagesProperty, value); } }
228 
229  /// <summary>
230  /// Gets or sets whether the log viewer should display verbose messages.
231  /// </summary>
232  public bool ShowVerboseMessages { get { return (bool)GetValue(ShowVerboseMessagesProperty); } set { SetValue(ShowVerboseMessagesProperty, value); } }
233 
234  /// <summary>
235  /// Gets or sets whether the log viewer should display info messages.
236  /// </summary>
237  public bool ShowInfoMessages { get { return (bool)GetValue(ShowInfoMessagesProperty); } set { SetValue(ShowInfoMessagesProperty, value); } }
238 
239  /// <summary>
240  /// Gets or sets whether the log viewer should display warning messages.
241  /// </summary>
242  public bool ShowWarningMessages { get { return (bool)GetValue(ShowWarningMessagesProperty); } set { SetValue(ShowWarningMessagesProperty, value); } }
243 
244  /// <summary>
245  /// Gets or sets whether the log viewer should display error messages.
246  /// </summary>
247  public bool ShowErrorMessages { get { return (bool)GetValue(ShowErrorMessagesProperty); } set { SetValue(ShowErrorMessagesProperty, value); } }
248 
249  /// <summary>
250  /// Gets or sets whether the log viewer should display fatal messages.
251  /// </summary>
252  public bool ShowFatalMessages { get { return (bool)GetValue(ShowFatalMessagesProperty); } set { SetValue(ShowFatalMessagesProperty, value); } }
253 
254  /// <inheritdoc/>
255  public override void OnApplyTemplate()
256  {
257  base.OnApplyTemplate();
258 
259  logTextBox = GetTemplateChild("PART_LogTextBox") as RichTextBox;
260  if (logTextBox == null)
261  throw new InvalidOperationException("A part named 'PART_LogTextBox' must be present in the ControlTemplate, and must be of type 'RichTextBox'.");
262 
263  var clearLogButton = GetTemplateChild("PART_ClearLog") as ButtonBase;
264  if (clearLogButton != null)
265  {
266  clearLogButton.Click += ClearLog;
267  }
268 
269  var previousResultButton = GetTemplateChild("PART_PreviousResult") as ButtonBase;
270  if (previousResultButton != null)
271  {
272  previousResultButton.Click += PreviousResultClicked;
273  }
274  var nextResultButton = GetTemplateChild("PART_NextResult") as ButtonBase;
275  if (nextResultButton != null)
276  {
277  nextResultButton.Click += NextResultClicked;
278  }
279 
280  ResetText();
281  }
282 
283  private void ClearLog(object sender, RoutedEventArgs e)
284  {
285  LogMessages.Clear();
286  }
287 
288  private void ResetText()
289  {
290  if (logTextBox != null)
291  {
292  ClearSearchResults();
293  var document = new FlowDocument(new Paragraph());
294  if (LogMessages != null)
295  {
296  var logMessages = LogMessages.ToList();
297  AppendText(document, logMessages);
298  }
299  logTextBox.Document = document;
300  }
301  }
302 
303  private void AppendText(FlowDocument document, IEnumerable<ILogMessage> logMessages)
304  {
305  if (document == null) throw new ArgumentNullException("document");
306  if (logTextBox != null && logMessages != null)
307  {
308  var paragraph = (Paragraph)document.Blocks.AsEnumerable().First();
309  var stringComparison = SearchMatchCase ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
310  var searchToken = SearchToken;
311  foreach (var message in logMessages.Where(x => ShouldDisplayMessage(x.Type)))
312  {
313  var lineText = message + Environment.NewLine;
314  var logColor = GetLogColor(message.Type);
315  if (string.IsNullOrEmpty(searchToken))
316  {
317  paragraph.Inlines.Add(new Run(lineText) { Foreground = logColor });
318  }
319  else
320  {
321  do
322  {
323  int tokenIndex = lineText.IndexOf(searchToken, stringComparison);
324  if (tokenIndex == -1)
325  {
326  paragraph.Inlines.Add(new Run(lineText) { Foreground = logColor });
327  break;
328  }
329  bool acceptResult = true;
330  if (SearchMatchWord && lineText.Length > 1)
331  {
332  if (tokenIndex > 0)
333  {
334  char c = lineText[tokenIndex - 1];
335  if ((c >= 'A' && c <= 'A') || (c >= 'a' && c <= 'z'))
336  acceptResult = false;
337  }
338  if (tokenIndex + searchToken.Length < lineText.Length)
339  {
340  char c = lineText[tokenIndex + searchToken.Length];
341  if ((c >= 'A' && c <= 'A') || (c >= 'a' && c <= 'z'))
342  acceptResult = false;
343  }
344  }
345 
346  if (acceptResult)
347  {
348  if (tokenIndex > 0)
349  paragraph.Inlines.Add(new Run(lineText.Substring(0, tokenIndex)) { Foreground = logColor });
350 
351  var tokenRun = new Run(lineText.Substring(tokenIndex, searchToken.Length)) { Background = SearchMatchBrush, Foreground = logColor };
352  paragraph.Inlines.Add(tokenRun);
353  var tokenRange = new TextRange(tokenRun.ContentStart, tokenRun.ContentEnd);
354  searchMatches.Add(tokenRange);
355  lineText = lineText.Substring(tokenIndex + searchToken.Length);
356  }
357  } while (lineText.Length > 0);
358  }
359  }
360  }
361  }
362 
363 
364  private void ClearSearchResults()
365  {
366  searchMatches.Clear();
367  }
368 
369  private void SelectFirstOccurence()
370  {
371  if (searchMatches.Count > 0)
372  {
373  SelectSearchResult(0);
374  }
375  }
376 
377  private void SelectPreviousOccurence()
378  {
379  if (searchMatches.Count > 0)
380  {
381  int previousResult = (searchMatches.Count + currentResult - 1) % searchMatches.Count;
382  SelectSearchResult(previousResult);
383  }
384  }
385 
386  private void SelectNextOccurence()
387  {
388  if (searchMatches.Count > 0)
389  {
390  int nextResult = (currentResult + 1) % searchMatches.Count;
391  SelectSearchResult(nextResult);
392  }
393  }
394 
395  private void SelectSearchResult(int resultIndex)
396  {
397  var result = searchMatches[resultIndex];
398  logTextBox.Selection.Select(result.Start, result.End);
399  Rect selectionRect = logTextBox.Selection.Start.GetCharacterRect(LogicalDirection.Forward);
400  double offset = selectionRect.Top + logTextBox.VerticalOffset;
401  logTextBox.ScrollToVerticalOffset(offset - logTextBox.ActualHeight / 2);
402  logTextBox.BringIntoView();
403  currentResult = resultIndex;
404  }
405 
406  private bool ShouldDisplayMessage(LogMessageType type)
407  {
408  switch (type)
409  {
410  case LogMessageType.Debug:
411  return ShowDebugMessages;
412  case LogMessageType.Verbose:
413  return ShowVerboseMessages;
414  case LogMessageType.Info:
415  return ShowInfoMessages;
416  case LogMessageType.Warning:
417  return ShowWarningMessages;
418  case LogMessageType.Error:
419  return ShowErrorMessages;
420  case LogMessageType.Fatal:
421  return ShowFatalMessages;
422  default:
423  throw new ArgumentOutOfRangeException("type");
424  }
425  }
426 
427  private Brush GetLogColor(LogMessageType type)
428  {
429  switch (type)
430  {
431  case LogMessageType.Debug:
432  return DebugBrush;
433  case LogMessageType.Verbose:
434  return VerboseBrush;
435  case LogMessageType.Info:
436  return InfoBrush;
437  case LogMessageType.Warning:
438  return WarningBrush;
439  case LogMessageType.Error:
440  return ErrorBrush;
441  case LogMessageType.Fatal:
442  return FatalBrush;
443  default:
444  throw new ArgumentOutOfRangeException("type");
445  }
446  }
447 
448  private static void TextPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
449  {
450  var logViewer = (TextLogViewer)d;
451  logViewer.ResetText();
452  if (logViewer.logTextBox != null)
453  {
454  logViewer.logTextBox.ScrollToEnd();
455  }
456  }
457 
458  /// <summary>
459  /// Raised when the <see cref="LogMessages"/> dependency property is changed.
460  /// </summary>
461  private static void LogMessagesPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
462  {
463  var logViewer = (TextLogViewer)d;
464  var oldValue = e.OldValue as ICollection<ILogMessage>;
465  var newValue = e.NewValue as ICollection<ILogMessage>;
466  if (oldValue != null)
467  {
468  // ReSharper disable SuspiciousTypeConversion.Global - go home resharper, you're drunk
469  var notifyCollectionChanged = oldValue as INotifyCollectionChanged;
470  // ReSharper restore SuspiciousTypeConversion.Global
471  if (notifyCollectionChanged != null)
472  {
473  notifyCollectionChanged.CollectionChanged -= logViewer.LogMessagesCollectionChanged;
474  }
475  }
476  if (e.NewValue != null)
477  {
478  // ReSharper disable SuspiciousTypeConversion.Global - go home resharper, you're drunk
479  var notifyCollectionChanged = newValue as INotifyCollectionChanged;
480  // ReSharper restore SuspiciousTypeConversion.Global
481  if (notifyCollectionChanged != null)
482  {
483  notifyCollectionChanged.CollectionChanged += logViewer.LogMessagesCollectionChanged;
484  }
485  }
486  logViewer.ResetText();
487  }
488 
489  /// <summary>
490  /// Raised when the <see cref="SearchToken"/> property is changed.
491  /// </summary>
492  private static void SearchTokenChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
493  {
494  var logViewer = (TextLogViewer)d;
495  logViewer.ResetText();
496  logViewer.SelectFirstOccurence();
497  }
498 
499  /// <summary>
500  /// Raised when the collection of log messages is observable and changes.
501  /// </summary>
502  private void LogMessagesCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
503  {
504  bool shouldScroll = AutoScroll && logTextBox != null && logTextBox.ExtentHeight - logTextBox.ViewportHeight - logTextBox.VerticalOffset < 1.0;
505 
506  if (e.Action == NotifyCollectionChangedAction.Add)
507  {
508  if (e.NewItems != null)
509  {
510  if (logTextBox != null)
511  {
512  if (logTextBox.Document == null)
513  {
514  logTextBox.Document = new FlowDocument(new Paragraph());
515  }
516  AppendText(logTextBox.Document, e.NewItems.Cast<ILogMessage>());
517  }
518  }
519  }
520  else
521  {
522  ResetText();
523  }
524 
525  if (shouldScroll)
526  {
527  logTextBox.ScrollToEnd();
528  }
529  }
530 
531  private void PreviousResultClicked(object sender, RoutedEventArgs e)
532  {
533  SelectPreviousOccurence();
534  }
535 
536  private void NextResultClicked(object sender, RoutedEventArgs e)
537  {
538  SelectNextOccurence();
539  }
540  }
541 }
This control displays a collection of ILogMessage.
LogMessageType
Type of a LogMessage.
The base interface for log messages used by the logging infrastructure.
Definition: ILogMessage.cs:8
The area of the image will be blanked out by its background.