// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Runtime.InteropServices; using System.Text; using System.Text.RegularExpressions; using System.Xml; using FlaxEditor.GUI; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Input; using FlaxEditor.Options; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Windows { /// /// Editor window used to show engine output logs. /// /// public sealed class OutputLogWindow : EditorWindow { /// /// The single log message entry. /// private struct Entry { /// /// The log level. /// public LogType Level; /// /// The log time (in UTC local format). /// public DateTime Time; /// /// The message contents. /// public string Message; }; private struct TextBlockTag { internal enum Types { CodeLocation }; public Types Type; public string Url; public int Line; } /// /// The output log textbox. /// /// private sealed class OutputTextBox : RichTextBoxBase { /// /// The parent window. /// public OutputLogWindow Window; /// /// The default text style. /// public TextBlockStyle DefaultStyle; /// /// The warning text style. /// public TextBlockStyle WarningStyle; /// /// The error text style. /// public TextBlockStyle ErrorStyle; public OutputTextBox() { _consumeAllKeyDownEvents = false; } /// protected override void OnParseTextBlocks() { if (ParseTextBlocks != null) { ParseTextBlocks(_text, _textBlocks); return; } // Use cached text blocks _textBlocks.Clear(); _textBlocks.AddRange(Window._textBlocks); } /// public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { // Click on text block int textLength = TextLength; if (textLength != 0) { var hitPos = CharIndexAtPoint(ref location); if (hitPos != -1 && GetTextBlock(hitPos, out var textBlock) && textBlock.Tag is TextBlockTag tag) { switch (tag.Type) { case TextBlockTag.Types.CodeLocation: Window.Editor.CodeEditing.OpenFile(tag.Url, tag.Line); return true; } } } return base.OnMouseDoubleClick(location, button); } } /// /// Command line input textbox control which can execute debug commands. /// private class CommandLineBox : TextBox { private sealed class Item : ItemsListContextMenu.Item { public CommandLineBox Owner; public Item() { } protected override void GetTextRect(out Rectangle rect) { rect = new Rectangle(2, 0, Width - 4, Height); } public override bool OnCharInput(char c) { if (Owner != null && (!Owner._searchPopup?.Visible ?? true)) { // Redirect input into search textbox while typing and using command history Owner.Set(Owner.Text + c); return true; } return false; } public override bool OnKeyDown(KeyboardKeys key) { switch (key) { case KeyboardKeys.Delete: case KeyboardKeys.Backspace: if (Owner != null && (!Owner._searchPopup?.Visible ?? true)) { // Redirect input into search textbox while typing and using command history Owner.OnKeyDown(key); return true; } break; } return base.OnKeyDown(key); } public override void OnDestroy() { Owner = null; base.OnDestroy(); } } private OutputLogWindow _window; private ItemsListContextMenu _searchPopup; private bool _isSettingText; public CommandLineBox(float x, float y, float width, OutputLogWindow window) : base(false, x, y, width) { WatermarkText = ">"; _window = window; } private void Set(string command) { _isSettingText = true; SetText(command); SetSelection(command.Length); _isSettingText = false; } private void ShowPopup(ref ItemsListContextMenu cm, IEnumerable commands, string searchText = null) { if (cm == null) cm = new ItemsListContextMenu(180, 220, false); else cm.ClearItems(); // Add items ItemsListContextMenu.Item lastItem = null; foreach (var command in commands) { cm.AddItem(lastItem = new Item { Name = command, Owner = this, }); var flags = DebugCommands.GetCommandFlags(command); if (flags.HasFlag(DebugCommands.CommandFlags.Exec)) lastItem.TintColor = new Color(0.85f, 0.85f, 1.0f, 1.0f); else if (flags.HasFlag(DebugCommands.CommandFlags.Read) && !flags.HasFlag(DebugCommands.CommandFlags.Write)) lastItem.TintColor = new Color(0.85f, 0.85f, 0.85f, 1.0f); lastItem.Focused += item => { // Set command Set(item.Name); }; } cm.ItemClicked += item => { // Execute command OnKeyDown(KeyboardKeys.Return); }; // Setup popup var count = commands.Count(); var totalHeight = count * lastItem.Height + cm.ItemsPanel.Margin.Height + cm.ItemsPanel.Spacing * (count - 1); cm.Height = 220; if (cm.Height > totalHeight) cm.Height = totalHeight; // Limit popup height if list is small if (searchText != null) { cm.SortItems(); cm.Search(searchText); cm.UseVisibilityControl = false; cm.UseInput = false; } // Show popup cm.Show(this, Float2.Zero, ContextMenuDirection.RightUp); cm.ScrollViewTo(lastItem); if (searchText != null) { RootWindow.Window.LostFocus += OnRootWindowLostFocus; } else { lastItem.Focus(); } } private void OnRootWindowLostFocus() { // Prevent popup from staying active when editor window looses focus _searchPopup?.Hide(); if (RootWindow?.Window != null) RootWindow.Window.LostFocus -= OnRootWindowLostFocus; } /// public override void OnGotFocus() { // Precache debug commands to reduce time-to-interactive DebugCommands.InitAsync(); base.OnGotFocus(); } /// protected override void OnTextChanged() { base.OnTextChanged(); // Skip when editing text from code if (_isSettingText) return; // Show commands search popup based on current text input var text = Text.Trim(); if (text.Length != 0) { DebugCommands.Search(text, out var matches); if (matches.Length != 0) { ShowPopup(ref _searchPopup, matches, text); return; } } _searchPopup?.Hide(); } /// public override bool OnKeyDown(KeyboardKeys key) { switch (key) { case KeyboardKeys.Return: { // Run command _searchPopup?.Hide(); var command = Text.Trim(); if (command.Length == 0) return true; DebugCommands.Execute(command); SetText(string.Empty); // Update history buffer if (_window._commandHistory == null) _window._commandHistory = new List(); else if (_window._commandHistory.Count != 0 && _window._commandHistory.Last() == command) _window._commandHistory.RemoveAt(_window._commandHistory.Count - 1); _window._commandHistory.Add(command); if (_window._commandHistory.Count > CommandHistoryLimit) _window._commandHistory.RemoveAt(0); _window.SaveHistory(); return true; } case KeyboardKeys.Tab: { // Auto-complete DebugCommands.Search(Text, out var matches, true); if (matches.Length == 0) { // Nothing found } else if (matches.Length == 1) { // Exact match Set(matches[0]); } else { // Find the most common part Array.Sort(matches); int minLength = Text.Length; int maxLength = matches[0].Length; int sharedLength = minLength + 1; bool allMatch = true; for (; allMatch && sharedLength < maxLength; sharedLength++) { var shared = matches[0].Substring(0, sharedLength); for (int i = 1; i < matches.Length; i++) { if (!matches[i].StartsWith(shared, StringComparison.OrdinalIgnoreCase)) { sharedLength -= 2; allMatch = false; break; } } } if (sharedLength > minLength) { // Use the largest shared part of all matches Set(matches[0].Substring(0, sharedLength)); } } return true; } case KeyboardKeys.ArrowUp: { if (_searchPopup != null && _searchPopup.Visible) { // Route navigation to active popup var focusedItem = _searchPopup.RootWindow.FocusedControl as Item; if (focusedItem == null) _searchPopup.SelectItem((Item)_searchPopup.ItemsPanel.Children.Last()); else _searchPopup.OnKeyDown(key); } else if (TextLength == 0) { if (_window._commandHistory != null && _window._commandHistory.Count != 0) { // Show command history popup _searchPopup?.Hide(); ItemsListContextMenu cm = null; ShowPopup(ref cm, _window._commandHistory); } } return true; } case KeyboardKeys.ArrowDown: { if (_searchPopup != null && _searchPopup.Visible) { // Route navigation to active popup var focusedItem = _searchPopup.RootWindow.FocusedControl as Item; if (focusedItem == null) _searchPopup.SelectItem((Item)_searchPopup.ItemsPanel.Children.First()); else _searchPopup.OnKeyDown(key); } return true; } } return base.OnKeyDown(key); } /// public override void OnDestroy() { _searchPopup?.Dispose(); _searchPopup = null; base.OnDestroy(); } } private InterfaceOptions.TimestampsFormats _timestampsFormats; private bool _showLogType; private List _entries = new List(1024); private bool _isDirty; private int _logTypeShowMask = (int)LogType.Info | (int)LogType.Warning | (int)LogType.Error | (int)LogType.Fatal; private float _scrollSize = 18.0f; private const int OutCapacity = 64; private string[] _outMessages = new string[OutCapacity]; private byte[] _outLogTypes = new byte[OutCapacity]; private long[] _outLogTimes = new long[OutCapacity]; private int _textBufferCount; private StringBuilder _textBuffer = new StringBuilder(); private List _textBlocks = new List(); private DateTime _startupTime; private Regex _compileRegex = new Regex("(?^(?:[a-zA-Z]\\:|\\\\\\\\[ \\-\\.\\w\\.]+\\\\[ \\-\\.\\w.$]+)\\\\(?:[ \\-\\.\\w]+\\\\)*\\w([ \\w.])+)\\((?\\d{1,}),\\d{1,},\\d{1,},\\d{1,}\\): (?error|warning) (?.*)", RegexOptions.Compiled); private List _commandHistory; private const string CommandHistoryKey = "CommandHistory"; private const int CommandHistoryLimit = 30; private Button _viewDropdown; private TextBox _searchBox; private HScrollBar _hScroll; private VScrollBar _vScroll; private OutputTextBox _output; private CommandLineBox _commandLineBox; private ContextMenu _contextMenu; /// /// Initializes a new instance of the class. /// /// The editor. public OutputLogWindow(Editor editor) : base(editor, true, ScrollBars.None) { Title = "Output Log"; ClipChildren = false; FlaxEditor.Utilities.Utils.SetupCommonInputActions(this); // Setup UI _viewDropdown = new Button(2, 2, 40.0f, TextBoxBase.DefaultHeight) { TooltipText = "Change output log view options", Text = "View", Parent = this, }; _viewDropdown.Clicked += OnViewButtonClicked; _searchBox = new SearchBox(false, _viewDropdown.Right + 2, 2, Width - _viewDropdown.Right - 4) { Parent = this, }; _searchBox.TextChanged += Refresh; _hScroll = new HScrollBar(this, Height - _scrollSize - TextBox.DefaultHeight - 2, Width - _scrollSize, _scrollSize) { ThumbThickness = 10, Maximum = 0, }; _hScroll.ValueChanged += OnHScrollValueChanged; _vScroll = new VScrollBar(this, Width - _scrollSize, Height - _viewDropdown.Height - 4 - TextBox.DefaultHeight, _scrollSize) { ThumbThickness = 10, Maximum = 0, }; _vScroll.Y += _viewDropdown.Height + 2; _vScroll.ValueChanged += OnVScrollValueChanged; _output = new OutputTextBox { Window = this, IsReadOnly = true, IsMultiline = true, BackgroundSelectedFlashSpeed = 0.0f, Location = new Float2(2, _viewDropdown.Bottom + 2), Parent = this, }; _output.TargetViewOffsetChanged += OnOutputTargetViewOffsetChanged; _output.TextChanged += OnOutputTextChanged; _commandLineBox = new CommandLineBox(2, Height - 2 - TextBox.DefaultHeight, Width - 4, this) { Parent = this, }; // Setup context menu _contextMenu = new ContextMenu(); _contextMenu.AddButton("Clear log", Clear); _contextMenu.AddButton("Copy selection", _output.Copy); _contextMenu.AddButton("Select All", _output.SelectAll); _contextMenu.AddButton(Utilities.Constants.ShowInExplorer, () => FileSystem.ShowFileExplorer(Path.Combine(Globals.ProjectFolder, "Logs"))); _contextMenu.AddButton("Scroll to bottom", () => { _vScroll.TargetValue = _vScroll.Maximum; }).Icon = Editor.Icons.ArrowDown12; // Setup editor options Editor.Options.OptionsChanged += OnEditorOptionsChanged; OnEditorOptionsChanged(Editor.Options.Options); InputActions.Add(options => options.Search, _searchBox.Focus); GameCooker.Event += OnGameCookerEvent; ScriptsBuilder.CompilationFailed += OnScriptsCompilationFailed; } private void OnViewButtonClicked() { var menu = new ContextMenu(); var infoLogButton = menu.AddButton("Info"); infoLogButton.AutoCheck = true; infoLogButton.Checked = (_logTypeShowMask & (int)LogType.Info) != 0; infoLogButton.Clicked += () => ToggleLogTypeShow(LogType.Info); var warningLogButton = menu.AddButton("Warning"); warningLogButton.AutoCheck = true; warningLogButton.Checked = (_logTypeShowMask & (int)LogType.Warning) != 0; warningLogButton.Clicked += () => ToggleLogTypeShow(LogType.Warning); var errorLogButton = menu.AddButton("Error"); errorLogButton.AutoCheck = true; errorLogButton.Checked = (_logTypeShowMask & (int)LogType.Error) != 0; errorLogButton.Clicked += () => ToggleLogTypeShow(LogType.Error); menu.AddSeparator(); menu.AddButton("Load log file...", LoadLogFile); menu.Show(_viewDropdown.Parent, _viewDropdown.BottomLeft); } private void ToggleLogTypeShow(LogType type) { _logTypeShowMask ^= (int)type; Refresh(); } private void OnHScrollValueChanged() { var viewOffset = _output.ViewOffset; viewOffset.X = _hScroll.Value; _output.TargetViewOffset = viewOffset; } private void OnVScrollValueChanged() { var viewOffset = _output.ViewOffset; viewOffset.Y = _vScroll.Value; _output.TargetViewOffset = viewOffset; } private void OnOutputTargetViewOffsetChanged() { if (!_hScroll.IsThumbClicked) _hScroll.TargetValue = _output.TargetViewOffset.X; if (!_vScroll.IsThumbClicked) _vScroll.TargetValue = _output.TargetViewOffset.Y; } private void OnOutputTextChanged() { if (IsLayoutLocked) return; _hScroll.Maximum = Mathf.Max(_output.TextSize.X, _hScroll.Minimum); _vScroll.Maximum = Mathf.Max(_output.TextSize.Y - _output.Height, _vScroll.Minimum); } private void OnEditorOptionsChanged(EditorOptions options) { if (options.Interface.OutputLogTimestampsFormat == _timestampsFormats && options.Interface.OutputLogShowLogType == _showLogType && _output.DefaultStyle.Font == options.Interface.OutputLogTextFont && _output.DefaultStyle.Color == options.Interface.OutputLogTextColor && _output.DefaultStyle.ShadowColor == options.Interface.OutputLogTextShadowColor && _output.DefaultStyle.ShadowOffset == options.Interface.OutputLogTextShadowOffset) return; _output.DefaultStyle = new TextBlockStyle { Font = options.Interface.OutputLogTextFont, Color = options.Interface.OutputLogTextColor, ShadowColor = options.Interface.OutputLogTextShadowColor, ShadowOffset = options.Interface.OutputLogTextShadowOffset, BackgroundSelectedBrush = new SolidColorBrush(Style.Current.BackgroundSelected), }; _output.WarningStyle = _output.DefaultStyle; _output.WarningStyle.Color = Color.Yellow; _output.ErrorStyle = _output.DefaultStyle; _output.ErrorStyle.Color = Color.Red; _timestampsFormats = options.Interface.OutputLogTimestampsFormat; _showLogType = options.Interface.OutputLogShowLogType; Refresh(); } private void OnGameCookerEvent(GameCooker.EventType eventType) { if (eventType == GameCooker.EventType.BuildFailed && !Editor.IsHeadlessMode && Editor.Options.Options.Interface.FocusOutputLogOnGameBuildError) FocusOrShow(); } private void OnScriptsCompilationFailed() { if (!Editor.IsHeadlessMode && Editor.Options.Options.Interface.FocusOutputLogOnCompilationError) FocusOrShow(); } private void SaveHistory() { if (_commandHistory == null || _commandHistory.Count == 0) Editor.ProjectCache.RemoveCustomData(CommandHistoryKey); else Editor.ProjectCache.SetCustomData(CommandHistoryKey, FlaxEngine.Json.JsonSerializer.Serialize(_commandHistory)); } /// /// Refreshes the log output. /// private void Refresh() { _textBufferCount = 0; _textBuffer.Clear(); _textBlocks.Clear(); _isDirty = true; } /// /// Clears the log. /// public void Clear() { _entries?.Clear(); Refresh(); } /// /// Loads the log from the file selected by the user with the file pickup dialog. /// public void LoadLogFile() { if (FileSystem.ShowOpenFileDialog(null, Path.Combine(Globals.ProjectFolder, "Logs"), null, false, "Pick a log file to load", out var files)) return; if (files != null && files.Length > 0) { LoadLogFile(files[0]); } } /// /// Loads the log file. /// /// The path. public void LoadLogFile(string path) { using (var file = File.OpenRead(path)) using (var stream = new StreamReader(file)) { _entries.Clear(); var regex = new Regex(@"\[ (\d\d:\d\d:\d\d.\d\d\d) \]\: \[(\w*)\]"); while (!stream.EndOfStream) { // Read next line var line = stream.ReadLine(); if (string.IsNullOrEmpty(line)) continue; // Parse with regex var match = regex.Match(line); if (!match.Success || match.Groups.Count != 3) { // Try to add the line for multi-line logs if (_entries.Count != 0 && !line.StartsWith("======")) { ref var last = ref CollectionsMarshal.AsSpan(_entries)[_entries.Count - 1]; last.Message += '\n'; last.Message += line; } continue; } // Parse log time and type var time = match.Groups[1].Value; var level = match.Groups[2].Value; if (time.Length != 12) continue; int hours = int.Parse(time.Substring(0, 2)); int minutes = int.Parse(time.Substring(3, 2)); int seconds = int.Parse(time.Substring(6, 2)); int milliseconds = int.Parse(time.Substring(9, 3)); var timeSinceStartup = new TimeSpan(0, hours, minutes, seconds, milliseconds); var logType = (LogType)Enum.Parse(typeof(LogType), level); // Add new entry var e = new Entry { Time = _startupTime + timeSinceStartup, Level = logType, Message = line.Substring(match.Index + match.Length) }; _entries.Add(e); } Refresh(); } } /// protected override void PerformLayoutBeforeChildren() { base.PerformLayoutBeforeChildren(); if (_output != null) { _searchBox.Width = Width - _viewDropdown.Right - 4; _output.Size = new Float2(_vScroll.X - 2, _hScroll.Y - 4 - _viewDropdown.Bottom); _commandLineBox.Width = Width - 4; _commandLineBox.Y = Height - 2 - _commandLineBox.Height; } } /// public override bool OnKeyDown(KeyboardKeys key) { var input = Editor.Options.Options.Input; if (input.Search.Process(this, key)) { if (!_searchBox.ContainsFocus) { _searchBox.Focus(); _searchBox.SelectAll(); } return true; } return base.OnKeyDown(key); } /// public override bool OnMouseUp(Float2 location, MouseButton button) { if (base.OnMouseUp(location, button)) return true; if (button == MouseButton.Right) { _contextMenu.Show(this, location); return true; } return false; } /// protected override void OnSizeChanged() { base.OnSizeChanged(); // Update scroll range OnOutputTextChanged(); } /// public override void Update(float deltaTime) { FlaxEngine.Profiler.BeginEvent("OutputLogWindow.Update"); // Read the incoming log messages int logCount; do { logCount = Editor.Internal_ReadOutputLogs(ref _outMessages, ref _outLogTypes, ref _outLogTimes, OutCapacity); for (int i = 0; i < logCount; i++) { var entry = new Entry { Level = (LogType)_outLogTypes[i], Time = new DateTime(_outLogTimes[i], DateTimeKind.Utc), Message = _outMessages[i], }; _entries.Add(entry); _outMessages[i] = null; _isDirty = true; } } while (logCount != 0); if (_isDirty) { _isDirty = false; var wasEmpty = _output.TextLength == 0; // Cache fonts _output.DefaultStyle.Font.GetFont(); _output.WarningStyle.Font.GetFont(); _output.ErrorStyle.Font.GetFont(); // Generate the output log Span entries = CollectionsMarshal.AsSpan(_entries); var searchQuery = _searchBox.Text; for (int i = _textBufferCount; i < _entries.Count; i++) { ref var entry = ref entries[i]; if (((int)entry.Level & _logTypeShowMask) == 0) continue; if (searchQuery.Length != 0 && entry.Message.IndexOf(searchQuery, StringComparison.OrdinalIgnoreCase) == -1) continue; var startIndex = _textBuffer.Length; switch (_timestampsFormats) { case InterfaceOptions.TimestampsFormats.Utc: _textBuffer.AppendFormat("[ {0} ]: ", entry.Time.ToUniversalTime()); break; case InterfaceOptions.TimestampsFormats.LocalTime: _textBuffer.AppendFormat("[ {0} ]: ", entry.Time); break; case InterfaceOptions.TimestampsFormats.TimeSinceStartup: var diff = entry.Time - _startupTime; _textBuffer.AppendFormat("[ {0:00}:{1:00}:{2:00}.{3:000} ]: ", diff.Hours, diff.Minutes, diff.Seconds, diff.Milliseconds); break; } if (_showLogType) { _textBuffer.AppendFormat("[{0}] ", entry.Level); } var prefixLength = _textBuffer.Length - startIndex; if (entry.Message.IndexOf('\r') != -1) entry.Message = entry.Message.Replace("\r", ""); _textBuffer.Append(entry.Message); var endIndex = _textBuffer.Length; _textBuffer.Append('\n'); var textBlock = new TextBlock { Range = new TextRange { StartIndex = startIndex, EndIndex = endIndex - 1, }, }; switch (entry.Level) { case LogType.Info: textBlock.Style = _output.DefaultStyle; break; case LogType.Warning: textBlock.Style = _output.WarningStyle; break; case LogType.Error: case LogType.Fatal: textBlock.Style = _output.ErrorStyle; break; default: throw new ArgumentOutOfRangeException(); } var prevBlockBottom = _textBlocks.Count == 0 ? 0.0f : _textBlocks[_textBlocks.Count - 1].Bounds.Bottom; var entryText = _textBuffer.ToString(startIndex, endIndex - startIndex); var font = textBlock.Style.Font.GetFont(); if (!font) continue; var style = textBlock.Style; var lines = font.ProcessText(entryText); for (int j = 0; j < lines.Length; j++) { ref var line = ref lines[j]; textBlock.Range.StartIndex = startIndex + line.FirstCharIndex; textBlock.Range.EndIndex = startIndex + line.LastCharIndex + 1; textBlock.Bounds = new Rectangle(new Float2(0.0f, prevBlockBottom), line.Size); if (textBlock.Range.Length > 0) { // Parse compilation error/warning var regexStart = line.FirstCharIndex; if (j == 0) regexStart += prefixLength; var regexLength = line.LastCharIndex + 1 - regexStart; if (regexLength > 0) { var match = _compileRegex.Match(entryText, regexStart, regexLength); if (match.Success) { switch (match.Groups["level"].Value) { case "error": textBlock.Style = _output.ErrorStyle; break; case "warning": textBlock.Style = _output.WarningStyle; break; } textBlock.Tag = new TextBlockTag { Type = TextBlockTag.Types.CodeLocation, Url = match.Groups["path"].Value, Line = int.Parse(match.Groups["line"].Value), }; } // TODO: parsing hyperlinks with link // TODO: parsing file paths with link } } prevBlockBottom += line.Size.Y; _textBlocks.Add(textBlock); textBlock.Style = style; } } // Update the output var cachedScrollValue = _vScroll.Value; var cachedSelection = _output.SelectionRange; var cachedOutputTargetViewOffset = _output.TargetViewOffset; var isBottomScroll = _vScroll.Value >= _vScroll.Maximum - (_scrollSize*2) || wasEmpty; _output.Text = _textBuffer.ToString(); _output.TargetViewOffset = cachedOutputTargetViewOffset; _textBufferCount = _entries.Count; if (!_vScroll.IsThumbClicked) _vScroll.TargetValue = isBottomScroll ? _vScroll.Maximum : cachedScrollValue; _output.SelectionRange = cachedSelection; } base.Update(deltaTime); FlaxEngine.Profiler.EndEvent(); } /// public override void OnInit() { _startupTime = Time.StartupTime; // Load debug commands history if (Editor.ProjectCache.TryGetCustomData(CommandHistoryKey, out string history)) { try { _commandHistory = (List)FlaxEngine.Json.JsonSerializer.Deserialize(history, typeof(List)); for (int i = _commandHistory.Count - 1; i >= 0; i--) { if (string.IsNullOrEmpty(_commandHistory[i])) _commandHistory.RemoveAt(i); } } catch { // Ignore errors _commandHistory = null; } } } /// public override bool UseLayoutData => true; /// public override void OnLayoutSerialize(XmlWriter writer) { writer.WriteAttributeString("LogTypeShowMask", _logTypeShowMask.ToString()); } /// public override void OnLayoutDeserialize(XmlElement node) { if (int.TryParse(node.GetAttribute("LogTypeShowMask"), out int value1)) _logTypeShowMask = value1; } /// public override void OnDestroy() { if (IsDisposing) return; // Unbind events Editor.Options.OptionsChanged -= OnEditorOptionsChanged; GameCooker.Event -= OnGameCookerEvent; ScriptsBuilder.CompilationFailed -= OnScriptsCompilationFailed; // Cleanup _textBuffer.Clear(); _textBuffer = null; _textBlocks.Clear(); _textBlocks = null; _entries.Clear(); _entries = null; _outMessages = null; _outLogTypes = null; _outLogTimes = null; _compileRegex = null; _commandHistory = null; // Unlink controls _viewDropdown = null; _searchBox = null; _hScroll = null; _vScroll = null; _output = null; _commandLineBox = null; _contextMenu = null; base.OnDestroy(); } } }