using System; using System.IO; using System.Windows; using System.Windows.Controls; using System.Windows.Documents; using System.Windows.Media; using System.Windows.Threading; using MPF.Core.Data; namespace MPF.UI.Core.UserControls { public partial class LogOutput : UserControl { /// /// Document representing the text /// internal FlowDocument Document { get; private set; } /// /// Queue of items that need to be logged /// internal ProcessingQueue LogQueue { get; private set; } /// /// Paragraph backing the log /// private readonly Paragraph _paragraph; /// /// Cached value of the last line written /// #if NET48 private Run lastLine = null; #else private Run? lastLine = null; #endif public LogOutput() { InitializeComponent(); // Update the internal state Document = new FlowDocument() { Background = new SolidColorBrush(Color.FromArgb(0xFF, 0x20, 0x20, 0x20)) }; _paragraph = new Paragraph(); Document.Blocks.Add(_paragraph); // Setup the processing queue LogQueue = new ProcessingQueue(ProcessLogLine); // Add handlers OutputViewer.SizeChanged += OutputViewerSizeChanged; Output.TextChanged += OnTextChanged; ClearButton.Click += OnClearButton; SaveButton.Click += OnSaveButton; // Update the internal state Output.Document = Document; } #region Logging /// /// Enqueue text to the log with formatting /// /// LogLevel for the log /// Text to write to the log public void EnqueueLog(LogLevel logLevel, string text) { // Null text gets ignored if (text == null) return; // Enqueue the text LogQueue.Enqueue(new LogLine(text, logLevel)); } /// /// Log line wrapper /// internal readonly struct LogLine { public readonly string Text; public readonly LogLevel LogLevel; public LogLine(string text, LogLevel logLevel) { this.Text = text; this.LogLevel = logLevel; } /// /// Get the foreground Brush for the current LogLevel /// /// Brush representing the color public Brush GetForegroundColor() { switch (this.LogLevel) { case LogLevel.SECRET: return Brushes.Blue; case LogLevel.ERROR: return Brushes.Red; case LogLevel.VERBOSE: return Brushes.Yellow; case LogLevel.USER: default: return Brushes.White; } } /// /// Generate a Run object from the current LogLine /// /// Run object based on internal values public Run GenerateRun() { return new Run { Text = this.Text, Foreground = GetForegroundColor() }; } } /// /// Process the log lines in the queue /// /// LogLine item to process internal void ProcessLogLine(LogLine nextLogLine) { // Null text gets ignored string nextText = nextLogLine.Text; if (nextText == null) return; try { if (nextText.StartsWith("\r")) ReplaceLastLine(nextLogLine); else AppendToTextBox(nextLogLine); } catch (Exception ex) { // In the event that something fails horribly, we want to log AppendToTextBox(new LogLine(ex.ToString(), LogLevel.ERROR)); } } /// /// Append log line to the log text box /// /// LogLine value to append private void AppendToTextBox(LogLine logLine) { Dispatcher.Invoke(() => { var run = logLine.GenerateRun(); _paragraph.Inlines.Add(run); lastLine = run; }); } /// /// Replace the last line written to the log text box /// /// LogLine value to append private void ReplaceLastLine(LogLine logLine) { Dispatcher.Invoke(() => { #if NET48 if (lastLine == null) lastLine = new Run(); #else lastLine ??= new Run(); #endif lastLine.Text = logLine.Text; lastLine.Foreground = logLine.GetForegroundColor(); }); } #endregion #region Helpers /// /// Clear all inlines of the paragraph /// private void ClearInlines() => _paragraph.Inlines.Clear(); /// /// Save all inlines to console.log /// private void SaveInlines() { using (var sw = new StreamWriter(File.OpenWrite("console.log"))) { foreach (var inline in _paragraph.Inlines) { if (inline is Run run) sw.Write(run.Text); } } } /// /// Scroll the current view to the bottom /// public void ScrollToBottom() => OutputViewer.ScrollToBottom(); #endregion #region EventHandlers private void OnClearButton(object sender, EventArgs e) => ClearInlines(); private void OnSaveButton(object sender, EventArgs e) => SaveInlines(); private void OnTextChanged(object sender, TextChangedEventArgs e) => ScrollToBottom(); private void OutputViewerSizeChanged(object sender, SizeChangedEventArgs e) => ScrollToBottom(); #endregion } }