//********************************** Banshee Engine (www.banshee3d.com) **************************************************// //**************** Copyright (c) 2016 Marko Pintera (marko.pintera@gmail.com). All rights reserved. **********************// using bs; using System; using System.Collections.Generic; using System.IO; namespace bs.Editor { /** @addtogroup Windows * @{ */ /// /// Displays a list of log messages. /// [DefaultSize(600, 300)] public class LogWindow : EditorWindow { #region Constants public const string CLEAR_ON_PLAY_KEY = "EditorClearLogOnPlay"; private const int TITLE_HEIGHT = 25; private const int ENTRY_HEIGHT = 39; private const int SEPARATOR_WIDTH = 3; private const float DETAILS_PANE_SIZE_PCT = 0.7f; private static readonly Color SEPARATOR_COLOR = new Color(33.0f / 255.0f, 33.0f / 255.0f, 33.0f / 255.0f); #endregion #region Fields private static int sSelectedElementIdx = -1; private GUIListView listView; private List entries = new List(); private List filteredEntries = new List(); private EntryFilter filter = EntryFilter.All; private GUITexture detailsSeparator; private GUIScrollArea detailsArea; #endregion #region Private properties /// /// Returns the height of the list area. /// private int ListHeight { get { return Height - TITLE_HEIGHT; } } #endregion #region Public methods /// /// Rebuilds the list of all entires in the console. /// public void Refresh() { ClearEntries(); LogEntry[] existingEntries = Debug.Messages; for (int i = 0; i < existingEntries.Length; i++) OnEntryAdded(existingEntries[i].type, existingEntries[i].message); } #endregion #region Private methods /// /// Opens the console window. /// [MenuItem("Windows/Log", ButtonModifier.CtrlAlt, ButtonCode.M, 6000)] private static void OpenConsoleWindow() { OpenWindow(); } /// protected override LocString GetDisplayName() { return new LocEdString("Log"); } private void OnInitialize() { GUILayoutY layout = GUI.AddLayoutY(); GUILayoutX titleLayout = layout.AddLayoutX(); GUIContentImages infoImages = new GUIContentImages( EditorBuiltin.GetLogMessageIcon(LogMessageIcon.Info, 16, false), EditorBuiltin.GetLogMessageIcon(LogMessageIcon.Info, 16, true)); GUIContentImages warningImages = new GUIContentImages( EditorBuiltin.GetLogMessageIcon(LogMessageIcon.Warning, 16, false), EditorBuiltin.GetLogMessageIcon(LogMessageIcon.Warning, 16, true)); GUIContentImages errorImages = new GUIContentImages( EditorBuiltin.GetLogMessageIcon(LogMessageIcon.Error, 16, false), EditorBuiltin.GetLogMessageIcon(LogMessageIcon.Error, 16, true)); GUIToggle infoBtn = new GUIToggle(new GUIContent(infoImages), EditorStyles.Button, GUIOption.FixedHeight(TITLE_HEIGHT)); GUIToggle warningBtn = new GUIToggle(new GUIContent(warningImages), EditorStyles.Button, GUIOption.FixedHeight(TITLE_HEIGHT)); GUIToggle errorBtn = new GUIToggle(new GUIContent(errorImages), EditorStyles.Button, GUIOption.FixedHeight(TITLE_HEIGHT)); GUIToggle detailsBtn = new GUIToggle(new LocEdString("Show details"), EditorStyles.Button, GUIOption.FixedHeight(TITLE_HEIGHT)); GUIButton clearBtn = new GUIButton(new LocEdString("Clear"), GUIOption.FixedHeight(TITLE_HEIGHT)); GUIToggle clearOnPlayBtn = new GUIToggle(new LocEdString("Clear on play"), EditorStyles.Button, GUIOption.FixedHeight(TITLE_HEIGHT)); titleLayout.AddElement(infoBtn); titleLayout.AddElement(warningBtn); titleLayout.AddElement(errorBtn); titleLayout.AddFlexibleSpace(); titleLayout.AddElement(detailsBtn); titleLayout.AddElement(clearBtn); titleLayout.AddElement(clearOnPlayBtn); infoBtn.Value = filter.HasFlag(EntryFilter.Info); warningBtn.Value = filter.HasFlag(EntryFilter.Warning); errorBtn.Value = filter.HasFlag(EntryFilter.Error); clearOnPlayBtn.Value = EditorSettings.GetBool(CLEAR_ON_PLAY_KEY, true); infoBtn.OnToggled += x => { if (x) SetFilter(filter | EntryFilter.Info); else SetFilter(filter & ~EntryFilter.Info); }; warningBtn.OnToggled += x => { if (x) SetFilter(filter | EntryFilter.Warning); else SetFilter(filter & ~EntryFilter.Warning); }; errorBtn.OnToggled += x => { if (x) SetFilter(filter | EntryFilter.Error); else SetFilter(filter & ~EntryFilter.Error); }; detailsBtn.OnToggled += ToggleDetailsPanel; clearBtn.OnClick += ClearLog; clearOnPlayBtn.OnToggled += ToggleClearOnPlay; GUILayoutX mainLayout = layout.AddLayoutX(); listView = new GUIListView(Width, ListHeight, ENTRY_HEIGHT, mainLayout); detailsSeparator = new GUITexture(Builtin.WhiteTexture, GUIOption.FixedWidth(SEPARATOR_WIDTH)); detailsArea = new GUIScrollArea(ScrollBarType.ShowIfDoesntFit, ScrollBarType.NeverShow); mainLayout.AddElement(detailsSeparator); mainLayout.AddElement(detailsArea); detailsSeparator.Active = false; detailsArea.Active = false; detailsSeparator.SetTint(SEPARATOR_COLOR); Refresh(); Debug.OnAdded += OnEntryAdded; } private void OnEditorUpdate() { listView.Update(); } private void OnDestroy() { Debug.OnAdded -= OnEntryAdded; } /// protected override void WindowResized(int width, int height) { if (detailsArea.Active) { int listWidth = width - (int)(width * DETAILS_PANE_SIZE_PCT) - SEPARATOR_WIDTH; listView.SetSize(listWidth, height - TITLE_HEIGHT); } else listView.SetSize(width, height - TITLE_HEIGHT); base.WindowResized(width, height); } /// /// Triggered when a new entry is added in the debug log. /// /// Type of the message. /// Message string. private void OnEntryAdded(DebugMessageType type, string message) { // Check if compiler message or reported exception, otherwise parse it as a normal log message ParsedLogEntry logEntry = ScriptCodeManager.ParseCompilerMessage(message); if (logEntry == null) logEntry = Debug.ParseExceptionMessage(message); if (logEntry == null) logEntry = Debug.ParseLogMessage(message); ConsoleEntryData newEntry = new ConsoleEntryData(); newEntry.type = type; newEntry.callstack = logEntry.callstack; newEntry.message = logEntry.message; entries.Add(newEntry); if (DoesFilterMatch(type)) { listView.AddEntry(newEntry); filteredEntries.Add(newEntry); } } /// /// Changes the filter that controls what type of messages are displayed in the console. /// /// Flags that control which type of messages should be displayed. private void SetFilter(EntryFilter filter) { if (this.filter == filter) return; this.filter = filter; listView.Clear(); filteredEntries.Clear(); foreach (var entry in entries) { if (DoesFilterMatch(entry.type)) { listView.AddEntry(entry); filteredEntries.Add(entry); } } sSelectedElementIdx = -1; } /// /// Checks if the currently active entry filter matches the provided type (the entry with the type that should be /// displayed). /// /// Type of the entry to check. /// True if the entry with the specified type should be displayed in the console. private bool DoesFilterMatch(DebugMessageType type) { switch (type) { case DebugMessageType.Info: return filter.HasFlag(EntryFilter.Info); case DebugMessageType.Warning: case DebugMessageType.CompilerWarning: return filter.HasFlag(EntryFilter.Warning); case DebugMessageType.Error: case DebugMessageType.CompilerError: return filter.HasFlag(EntryFilter.Error); } return false; } /// /// Removes all entries from the debug log. /// private void ClearLog() { Debug.Clear(); ClearEntries(); } /// /// Removes all entries from the console. /// private void ClearEntries() { listView.Clear(); entries.Clear(); filteredEntries.Clear(); sSelectedElementIdx = -1; RefreshDetailsPanel(); } /// /// Shows or hides the details panel. /// /// True to show, false to hide. private void ToggleDetailsPanel(bool show) { detailsArea.Active = show; detailsSeparator.Active = show; if (show) { int listWidth = Width - (int)(Width * DETAILS_PANE_SIZE_PCT) - SEPARATOR_WIDTH; listView.SetSize(listWidth, ListHeight); RefreshDetailsPanel(); } else listView.SetSize(Width, ListHeight); } /// /// Toggles should the console be cleared when the play mode is entered. /// /// True if the console should be cleared upon entering the play mode. private void ToggleClearOnPlay(bool clear) { EditorSettings.SetBool(CLEAR_ON_PLAY_KEY, clear); } /// /// Updates the contents of the details panel according to the currently selected element. /// private void RefreshDetailsPanel() { detailsArea.Layout.Clear(); if (sSelectedElementIdx != -1) { GUILayoutX paddingX = detailsArea.Layout.AddLayoutX(); paddingX.AddSpace(5); GUILayoutY paddingY = paddingX.AddLayoutY(); paddingX.AddSpace(5); paddingY.AddSpace(5); GUILayoutY mainLayout = paddingY.AddLayoutY(); paddingY.AddSpace(5); ConsoleEntryData entry = filteredEntries[sSelectedElementIdx]; LocString message = new LocEdString(entry.message); GUILabel messageLabel = new GUILabel(message, EditorStyles.MultiLineLabel, GUIOption.FlexibleHeight()); mainLayout.AddElement(messageLabel); mainLayout.AddSpace(10); if (entry.callstack != null) { foreach (var call in entry.callstack) { string fileName = Path.GetFileName(call.file); string callMessage; if (string.IsNullOrEmpty(call.method)) callMessage = "\tin " + fileName + ":" + call.line; else callMessage = "\t" + call.method + " in " + fileName + ":" + call.line; GUIButton callBtn = new GUIButton(new LocEdString(callMessage)); mainLayout.AddElement(callBtn); CallStackEntry hoistedCall = call; callBtn.OnClick += () => { CodeEditor.OpenFile(hoistedCall.file, hoistedCall.line); }; } } mainLayout.AddFlexibleSpace(); } else { GUILayoutX centerX = detailsArea.Layout.AddLayoutX(); centerX.AddFlexibleSpace(); GUILayoutY centerY = centerX.AddLayoutY(); centerX.AddFlexibleSpace(); centerY.AddFlexibleSpace(); GUILabel nothingSelectedLbl = new GUILabel(new LocEdString("(No entry selected)")); centerY.AddElement(nothingSelectedLbl); centerY.AddFlexibleSpace(); } } #endregion #region Types /// /// Filter type that determines what kind of messages are shown in the console. /// [Flags] private enum EntryFilter { Info = 0x01, Warning = 0x02, Error = 0x04, All = Info | Warning | Error } /// /// Contains data for a single entry in the console. /// private class ConsoleEntryData : GUIListViewData { public DebugMessageType type; public string message; public CallStackEntry[] callstack; } /// /// Contains GUI elements used for displaying a single entry in the console. /// private class ConsoleGUIEntry : GUIListViewEntry { private const int CALLER_LABEL_HEIGHT = 11; private const int PADDING = 3; private const int MESSAGE_HEIGHT = ENTRY_HEIGHT - CALLER_LABEL_HEIGHT - PADDING * 2; private static readonly Color BG_COLOR = Color.DarkGray; private static readonly Color SELECTION_COLOR = Color.DarkCyan; private GUIPanel overlay; private GUIPanel main; private GUIPanel underlay; private GUITexture icon; private GUILabel messageLabel; private GUILabel functionLabel; private GUITexture background; private int entryIdx; private string file; private int line; /// public override void BuildGUI() { main = Layout.AddPanel(0, 1, 1, GUIOption.FixedHeight(ENTRY_HEIGHT)); overlay = main.AddPanel(-1, 0, 0, GUIOption.FixedHeight(ENTRY_HEIGHT)); underlay = main.AddPanel(1, 0, 0, GUIOption.FixedHeight(ENTRY_HEIGHT)); GUILayoutX mainLayout = main.AddLayoutX(); GUILayoutY overlayLayout = overlay.AddLayoutY(); GUILayoutY underlayLayout = underlay.AddLayoutY(); icon = new GUITexture(null, GUIOption.FixedWidth(32), GUIOption.FixedHeight(32)); messageLabel = new GUILabel(new LocEdString(""), EditorStyles.MultiLineLabel, GUIOption.FixedHeight(MESSAGE_HEIGHT)); functionLabel = new GUILabel(new LocEdString(""), GUIOption.FixedHeight(CALLER_LABEL_HEIGHT)); mainLayout.AddSpace(PADDING); GUILayoutY iconLayout = mainLayout.AddLayoutY(); iconLayout.AddFlexibleSpace(); iconLayout.AddElement(icon); iconLayout.AddFlexibleSpace(); mainLayout.AddSpace(PADDING); GUILayoutY messageLayout = mainLayout.AddLayoutY(); messageLayout.AddSpace(PADDING); messageLayout.AddElement(messageLabel); messageLayout.AddElement(functionLabel); messageLayout.AddSpace(PADDING); mainLayout.AddFlexibleSpace(); mainLayout.AddSpace(PADDING); background = new GUITexture(Builtin.WhiteTexture, GUIOption.FixedHeight(ENTRY_HEIGHT)); underlayLayout.AddElement(background); GUIButton button = new GUIButton(new LocEdString(""), EditorStyles.Blank, GUIOption.FixedHeight(ENTRY_HEIGHT)); overlayLayout.AddElement(button); button.OnClick += OnClicked; button.OnDoubleClick += OnDoubleClicked; } /// public override void UpdateContents(int index, ConsoleEntryData data) { if (index != sSelectedElementIdx) { if (index%2 != 0) { background.Visible = true; background.SetTint(BG_COLOR); } else { background.Visible = false; } } else { background.Visible = true; background.SetTint(SELECTION_COLOR); } switch (data.type) { case DebugMessageType.Info: icon.SetTexture(EditorBuiltin.GetLogMessageIcon(LogMessageIcon.Info, 32, false)); break; case DebugMessageType.Warning: case DebugMessageType.CompilerWarning: icon.SetTexture(EditorBuiltin.GetLogMessageIcon(LogMessageIcon.Warning, 32, false)); break; case DebugMessageType.Error: case DebugMessageType.CompilerError: icon.SetTexture(EditorBuiltin.GetLogMessageIcon(LogMessageIcon.Error, 32, false)); break; } messageLabel.SetContent(new LocEdString(data.message)); string method = ""; if (data.callstack != null && data.callstack.Length > 0) { string filePath = data.callstack[0].file; bool isFilePathValid = filePath.IndexOfAny(Path.GetInvalidPathChars()) == -1; if (isFilePathValid) file = Path.GetFileName(data.callstack[0].file); else file = ""; line = data.callstack[0].line; if (string.IsNullOrEmpty(data.callstack[0].method)) method = "\tin " + file + ":" + line; else method = "\t" + data.callstack[0].method + " in " + file + ":" + line; } else { file = ""; line = 0; } functionLabel.SetContent(new LocEdString(method)); entryIdx = index; } /// /// Triggered when the entry is selected. /// private void OnClicked() { sSelectedElementIdx = entryIdx; LogWindow window = GetWindow(); window.RefreshDetailsPanel(); RefreshEntries(); } /// /// Triggered when the entry is double-clicked. /// private void OnDoubleClicked() { if(!string.IsNullOrEmpty(file)) CodeEditor.OpenFile(file, line); } } #endregion } /** @} */ }