Jelajahi Sumber

WIP console window

BearishSun 10 tahun lalu
induk
melakukan
9cc9e61eb7

+ 203 - 0
MBansheeEditor/ConsoleWindow.cs

@@ -0,0 +1,203 @@
+using BansheeEngine;
+using System;
+using System.Collections.Generic;
+using System.Text.RegularExpressions;
+
+namespace BansheeEditor
+{
+    /// <summary>
+    /// Displays a list of log messages.
+    /// </summary>
+    public class ConsoleWindow : EditorWindow
+    {
+        /// <summary>
+        /// Filter type that determines what kind of messages are shown in the console.
+        /// </summary>
+        [Flags]
+        private enum EntryFilter
+        {
+            Info = 0x01, Warning = 0x02, Error = 0x04, All = Info | Warning | Error
+        }
+
+        private const int ENTRY_HEIGHT = 60;
+        private GUIListView<ConsoleGUIEntry, ConsoleEntryData> listView;
+        private List<ConsoleEntryData> entries = new List<ConsoleEntryData>();
+        private EntryFilter filter = EntryFilter.All;
+
+        /// <summary>
+        /// Opens the console window.
+        /// </summary>
+        [MenuItem("Windows/Console", ButtonModifier.CtrlAlt, ButtonCode.C, 6000)]
+        private static void OpenConsoleWindow()
+        {
+            OpenWindow<ConsoleWindow>();
+        }
+
+        /// <inheritdoc/>
+        protected override LocString GetDisplayName()
+        {
+            return new LocEdString("Console");
+        }
+
+        private void OnInitialize()
+        {
+            GUILayoutY layout = GUI.AddLayoutY();
+
+            listView = new GUIListView<ConsoleGUIEntry, ConsoleEntryData>(Width, Height, ENTRY_HEIGHT, layout);
+
+            Debug.OnAdded += OnEntryAdded;
+
+            // TODO - Add buttons to filter info/warning/error
+            // TODO - Add button to clear console
+            // TODO - Add button that splits the window vertically and displays details about an entry + callstack
+            // TODO - On entry double-click open VS at that line
+            // TODO - On callstack entry double-click open VS at that line
+        }
+
+        private void OnEditorUpdate()
+        {
+            listView.Update();
+        }
+
+        private void OnDestroy()
+        {
+
+        }
+
+        /// <inheritdoc/>
+        protected override void WindowResized(int width, int height)
+        {
+            listView.SetSize(width, height);
+
+            base.WindowResized(width, height);
+        }
+
+        /// <summary>
+        /// Triggered when a new entry is added in the debug log.
+        /// </summary>
+        /// <param name="type">Type of the message.</param>
+        /// <param name="message">Message string.</param>
+        private void OnEntryAdded(DebugMessageType type, string message)
+        {
+            ConsoleEntryData newEntry = new ConsoleEntryData();
+
+            newEntry.type = type;
+
+            int firstMatchIdx = -1;
+            Regex regex = new Regex(@"\tat .* in (.*), line (\d*), column .*, namespace .*");
+            var matches = regex.Matches(message);
+
+            newEntry.callstack = new ConsoleEntryData.CallStackEntry[matches.Count];
+            for(int i = 0; i < matches.Count; i++)
+            {
+                ConsoleEntryData.CallStackEntry callstackEntry = new ConsoleEntryData.CallStackEntry();
+                callstackEntry.file = matches[i].Groups[0].Value;
+                int.TryParse(matches[i].Groups[1].Value, out callstackEntry.line);
+
+                newEntry.callstack[i] = callstackEntry;
+
+                if (firstMatchIdx == -1)
+                    firstMatchIdx = matches[i].Index;
+            }
+
+            if (firstMatchIdx != -1)
+                newEntry.message = message.Substring(0, firstMatchIdx);
+            else
+                newEntry.message = message;
+
+            entries.Add(newEntry);
+
+            if (DoesFilterMatch(type))
+                listView.AddEntry(newEntry);
+        }
+
+        /// <summary>
+        /// Changes the filter that controls what type of messages are displayed in the console.
+        /// </summary>
+        /// <param name="filter">Flags that control which type of messages should be displayed.</param>
+        private void SetFilter(EntryFilter filter)
+        {
+            if (this.filter == filter)
+                return;
+
+            listView.Clear();
+            foreach (var entry in entries)
+            {
+                if (DoesFilterMatch(entry.type))
+                    listView.AddEntry(entry);
+            }
+
+            this.filter = filter;
+        }
+
+        /// <summary>
+        /// Checks if the currently active entry filter matches the provided type (i.e. the entry with the type should be
+        /// displayed).
+        /// </summary>
+        /// <param name="type">Type of the entry to check.</param>
+        /// <returns>True if the entry with the specified type should be displayed in the console.</returns>
+        private bool DoesFilterMatch(DebugMessageType type)
+        {
+            switch (type)
+            {
+                case DebugMessageType.Info:
+                    return filter.HasFlag(EntryFilter.Info);
+                case DebugMessageType.Warning:
+                    return filter.HasFlag(EntryFilter.Warning);
+                case DebugMessageType.Error:
+                    return filter.HasFlag(EntryFilter.Error);
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Removes all entries from the console.
+        /// </summary>
+        private void Clear()
+        {
+            listView.Clear();
+            entries.Clear();
+        }
+
+        /// <summary>
+        /// Contains data for a single entry in the console.
+        /// </summary>
+        private class ConsoleEntryData : GUIListViewData
+        {
+            /// <summary>
+            /// Contains data for a single entry in a call stack associated with a console entry.
+            /// </summary>
+            public class CallStackEntry
+            {
+                public string file;
+                public int line;
+            }
+
+            public DebugMessageType type;
+            public string message;
+            public CallStackEntry[] callstack;
+        }
+
+        /// <summary>
+        /// Contains GUI elements used for displaying a single entry in the console.
+        /// </summary>
+        private class ConsoleGUIEntry : GUIListViewEntry<ConsoleEntryData>
+        {
+            private GUILabel messageLabel;
+
+            /// <inheritdoc/>
+            public override void BuildGUI()
+            {
+                messageLabel = new GUILabel(new LocEdString(""), EditorStyles.MultiLineLabel, GUIOption.FixedHeight(ENTRY_HEIGHT));
+                Layout.AddElement(messageLabel);
+            }
+
+            /// <inheritdoc/>
+            public override void UpdateContents(ConsoleEntryData data)
+            {
+                messageLabel.SetContent(new LocEdString(data.message));
+            }
+        }
+    }
+}

+ 1 - 6
MBansheeEditor/GameWindow.cs

@@ -1,9 +1,4 @@
-using System;
-using System.Collections.Generic;
-using System.Linq;
-using System.Text;
-using System.Threading.Tasks;
-using BansheeEngine;
+using BansheeEngine;
 
 namespace BansheeEditor
 {

+ 1 - 0
MBansheeEditor/MBansheeEditor.csproj

@@ -44,6 +44,7 @@
     <Compile Include="BuildManager.cs" />
     <Compile Include="CodeEditor.cs" />
     <Compile Include="ColorPicker.cs" />
+    <Compile Include="ConsoleWindow.cs" />
     <Compile Include="DialogBox.cs" />
     <Compile Include="DragDrop.cs" />
     <Compile Include="DropDownWindow.cs" />

+ 27 - 3
MBansheeEngine/Debug.cs

@@ -7,11 +7,24 @@ using System.Text;
 
 namespace BansheeEngine
 {
+    /// <summary>
+    /// Possible types of debug messages.
+    /// </summary>
+    public enum DebugMessageType // Note: Must match C++ enum DebugChannel
+    {
+        Info, Warning, Error
+    }
+
     /// <summary>
     /// Utility class providing various debug functionality.
     /// </summary>
     public sealed class Debug
     {
+        /// <summary>
+        /// Triggered when a new message is added to the debug log.
+        /// </summary>
+        public static Action<DebugMessageType, string> OnAdded;
+
         /// <summary>
         /// Logs a new informative message to the global debug log.
         /// </summary>
@@ -108,13 +121,24 @@ namespace BansheeEngine
             return sb.ToString();
         }
 
+        /// <summary>
+        /// Triggered by the runtime when a new message is added to the debug log.
+        /// </summary>
+        /// <param name="type">Type of the message that was added.</param>
+        /// <param name="message">Text of the message.</param>
+        private static void Internal_OnAdded(DebugMessageType type, string message)
+        {
+            if (OnAdded != null)
+                OnAdded(type, message);
+        }
+
         [MethodImpl(MethodImplOptions.InternalCall)]
-        internal static extern Component Internal_Log(string message);
+        internal static extern void Internal_Log(string message);
 
         [MethodImpl(MethodImplOptions.InternalCall)]
-        internal static extern Component Internal_LogWarning(string message);
+        internal static extern void Internal_LogWarning(string message);
 
         [MethodImpl(MethodImplOptions.InternalCall)]
-        internal static extern Component Internal_LogError(string message);
+        internal static extern void Internal_LogError(string message);
     }
 }

+ 186 - 0
MBansheeEngine/GUI/GUIListView.cs

@@ -0,0 +1,186 @@
+using System.Collections.Generic;
+
+namespace BansheeEngine
+{
+    // TODO - Doc
+    public class GUIListView<TEntry, TData> 
+        where TEntry : GUIListViewEntry<TData>, new()
+        where TData : GUIListViewData
+    {
+        // TODO - Only fixed size is supported. It should be nice if this object could just be placed in layout like any
+        // other GUI element. Would likely need some kind of a way to get notified when parent layout changes.
+        // (Possibly add a callback to GUIPanel when updateLayout is called?)
+
+        private List<TEntry> visibleEntries = new List<TEntry>();
+        private List<TData> entries = new List<TData>();
+        private GUIScrollArea scrollArea;
+        private GUIPanel topPadding;
+        private GUIPanel bottomPadding;
+        private int width;
+        private int height;
+        private int entryHeight;
+        private float scrollPct = 0.0f;
+        private bool contentsDirty = true;
+
+        public int NumEntries
+        {
+            get { return entries.Count; }
+        }
+
+        public int EntryHeight
+        {
+            get { return entryHeight; }
+            set { entryHeight = value; }
+        }
+
+        public GUIListView(int width, int height, int entryHeight, GUILayout layout)
+        {
+            scrollArea = new GUIScrollArea(GUIOption.FixedWidth(width), GUIOption.FixedHeight(height));
+            layout.AddElement(scrollArea);
+
+            topPadding = scrollArea.Layout.AddPanel();
+            bottomPadding = scrollArea.Layout.AddPanel();
+
+            this.width = width;
+            this.height = height;
+            this.entryHeight = entryHeight;
+        }
+
+        public void AddEntry(TData data)
+        {
+            entries.Add(data);
+            contentsDirty = true;
+        }
+
+        public void RemoveEntry(int index)
+        {
+            if (index >= 0 && index < entries.Count)
+            {
+                entries.RemoveAt(index);
+                contentsDirty = true;
+            }
+        }
+
+        public void Clear()
+        {
+            entries.Clear();
+            contentsDirty = true;
+        }
+
+        public int FindEntry(TData data)
+        {
+            return entries.FindIndex(x => x == data);
+        }
+
+        public void InsertEntry(int index, TData data)
+        {
+            if (index >= 0 && index <= entries.Count)
+                entries.Insert(index, data);
+            else
+                entries.Add(data);
+
+            contentsDirty = true;
+        }
+
+        public void SetSize(int width, int height)
+        {
+            if (width != this.width || height != this.height)
+            {
+                this.width = width;
+                this.height = height;
+
+                Rect2I bounds = scrollArea.Bounds;
+                bounds.width = width;
+                bounds.height = height;
+                scrollArea.Bounds = bounds;
+            }
+        }
+
+        public void Update()
+        {
+            int numVisibleEntries = MathEx.CeilToInt(height / (float)entryHeight) + 1;
+            numVisibleEntries = MathEx.Max(numVisibleEntries, entries.Count);
+
+            while (visibleEntries.Count < numVisibleEntries)
+            {
+                TEntry newEntry = new TEntry();
+                newEntry.Initialize(scrollArea);
+                newEntry.panel.SetHeight(entryHeight);
+
+                visibleEntries.Add(newEntry);
+                contentsDirty = true;
+            }
+
+            while (numVisibleEntries < visibleEntries.Count)
+            {
+                int lastIdx = visibleEntries.Count - 1;
+
+                visibleEntries[lastIdx].Destroy();
+                visibleEntries.RemoveAt(lastIdx);
+
+                contentsDirty = true;
+            }
+
+            if (scrollPct != scrollArea.VerticalScroll)
+            {
+                scrollPct = scrollArea.VerticalScroll;
+                contentsDirty = true;
+            }
+
+            if (contentsDirty)
+            {
+                int totalElementHeight = entries.Count*entryHeight;
+                int scrollableHeight = MathEx.Max(0, totalElementHeight - height);
+
+                int startPos = MathEx.FloorToInt(scrollPct*scrollableHeight);
+                int startIndex = startPos/entryHeight;
+
+                topPadding.SetPosition(0, 0);
+                topPadding.SetHeight(startIndex*entryHeight);
+
+                for (int i = 0; i < visibleEntries.Count; i++)
+                {
+                    visibleEntries[i].UpdateContents(entries[startIndex + i]);
+                    visibleEntries[i].panel.SetPosition(0, i * entryHeight);
+                }
+
+                int bottomPosition = (startIndex + visibleEntries.Count)*entryHeight;
+                bottomPadding.SetPosition(0, bottomPosition);
+                bottomPadding.SetHeight(totalElementHeight - bottomPosition);
+
+                contentsDirty = false;
+            }
+        }
+    }
+
+    public class GUIListViewData
+    {
+        
+    }
+
+    public abstract class GUIListViewEntry<TData>
+        where TData : GUIListViewData
+    {
+        internal GUIPanel panel;
+        internal GUILayoutY layout;
+
+        protected GUILayout Layout { get { return layout; } }
+
+        internal void Initialize(GUIScrollArea parent)
+        {
+            int numElements = parent.Layout.ChildCount;
+
+            // Last panel is always the padding panel, so keep it there
+            panel = parent.Layout.InsertPanel(numElements - 1);
+            layout = panel.AddLayoutY();
+        }
+
+        internal void Destroy()
+        {
+            panel.Destroy();
+        }
+
+        public abstract void BuildGUI();
+        public abstract void UpdateContents(TData data);
+    }
+}

+ 1 - 0
MBansheeEngine/MBansheeEngine.csproj

@@ -46,6 +46,7 @@
     <Compile Include="Bounds.cs" />
     <Compile Include="Builtin.cs" />
     <Compile Include="Camera.cs" />
+    <Compile Include="GUI\GUIListView.cs" />
     <Compile Include="Layers.cs" />
     <Compile Include="NativeCamera.cs" />
     <Compile Include="ContextMenu.cs" />

+ 20 - 0
SBansheeEngine/Include/BsScriptDebug.h

@@ -13,14 +13,34 @@ namespace BansheeEngine
 	public:
 		SCRIPT_OBJ(ENGINE_ASSEMBLY, "BansheeEngine", "Debug")
 
+		/**
+		 * @brief	Registers internal callbacks. Must be called on scripting system load.
+		 */
+		static void startUp();
+
+		/**
+		 * @brief	Unregisters internal callbacks. Must be called on scripting system shutdown.
+		 */
+		static void shutDown();
 	private:
 		ScriptDebug(MonoObject* instance);
 
+		/**
+		 * @brief	Triggered when a new entry is added to the debug log.
+		 */
+		static void onLogEntryAdded(const LogEntry& entry);
+
+		static HEvent mOnLogEntryAddedConn;
+
 		/************************************************************************/
 		/* 								CLR HOOKS						   		*/
 		/************************************************************************/
 		static void internal_log(MonoString* message);
 		static void internal_logWarning(MonoString* message);
 		static void internal_logError(MonoString* message);
+
+		typedef void(__stdcall *OnAddedThunkDef) (UINT32, MonoString*, MonoException**);
+
+		static OnAddedThunkDef onAddedThunk;
 	};
 }

+ 6 - 4
SBansheeEngine/Include/BsScriptGUIElement.h

@@ -126,10 +126,6 @@ namespace BansheeEngine
 	public:
 		SCRIPT_OBJ(ENGINE_ASSEMBLY, "BansheeEngine", "GUIElement")
 
-		typedef void(__stdcall *OnFocusChangedThunkDef) (MonoObject*, MonoException**);
-
-		static OnFocusChangedThunkDef onFocusGainedThunk;
-		static OnFocusChangedThunkDef onFocusLostThunk;
 	private:
 		ScriptGUIElement(MonoObject* instance);
 
@@ -152,5 +148,11 @@ namespace BansheeEngine
 		static void internal_SetFlexibleHeight(ScriptGUIElementBaseTBase* nativeInstance, UINT32 minHeight, UINT32 maxHeight);
 		static void internal_SetContextMenu(ScriptGUIElementBaseTBase* nativeInstance, ScriptContextMenu* contextMenu);
 		static void internal_ResetDimensions(ScriptGUIElementBaseTBase* nativeInstance);
+
+		typedef void(__stdcall *OnFocusChangedThunkDef) (MonoObject*, MonoException**);
+
+	public:
+		static OnFocusChangedThunkDef onFocusGainedThunk;
+		static OnFocusChangedThunkDef onFocusLostThunk;
 	};
 }

+ 3 - 0
SBansheeEngine/Source/BsEngineScriptLibrary.cpp

@@ -12,6 +12,7 @@
 #include "BsGameResourceManager.h"
 #include "BsApplication.h"
 #include "BsFileSystem.h"
+#include "BsScriptDebug.h"
 
 namespace BansheeEngine
 {
@@ -27,6 +28,7 @@ namespace BansheeEngine
 		MonoManager::startUp();
 		MonoAssembly& bansheeEngineAssembly = MonoManager::instance().loadAssembly(engineAssemblyPath.toString(), ENGINE_ASSEMBLY);
 
+		ScriptDebug::startUp();
 		GameResourceManager::startUp();
 		ScriptObjectManager::startUp();
 		ManagedResourceManager::startUp();
@@ -95,5 +97,6 @@ namespace BansheeEngine
 		ScriptAssemblyManager::shutDown();
 		ScriptObjectManager::shutDown();
 		GameResourceManager::shutDown();
+		ScriptDebug::shutDown();
 	}
 }

+ 23 - 0
SBansheeEngine/Source/BsScriptDebug.cpp

@@ -1,11 +1,15 @@
 #include "BsScriptDebug.h"
 #include "BsMonoManager.h"
 #include "BsMonoClass.h"
+#include "BsMonoMethod.h"
 #include "BsMonoUtil.h"
 #include "BsDebug.h"
 
 namespace BansheeEngine
 {
+	HEvent ScriptDebug::mOnLogEntryAddedConn;
+	ScriptDebug::OnAddedThunkDef ScriptDebug::onAddedThunk = nullptr;
+
 	ScriptDebug::ScriptDebug(MonoObject* instance)
 		:ScriptObject(instance)
 	{ }
@@ -15,6 +19,25 @@ namespace BansheeEngine
 		metaData.scriptClass->addInternalCall("Internal_Log", &ScriptDebug::internal_log);
 		metaData.scriptClass->addInternalCall("Internal_LogWarning", &ScriptDebug::internal_logWarning);
 		metaData.scriptClass->addInternalCall("Internal_LogError", &ScriptDebug::internal_logError);
+
+		onAddedThunk = (OnAddedThunkDef)metaData.scriptClass->getMethod("Internal_OnAdded", 2)->getThunk();
+	}
+
+	void ScriptDebug::startUp()
+	{
+		mOnLogEntryAddedConn = gDebug().onLogEntryAdded.connect(&ScriptDebug::onLogEntryAdded);
+	}
+
+	void ScriptDebug::shutDown()
+	{
+		mOnLogEntryAddedConn.disconnect();
+	}
+
+	void ScriptDebug::onLogEntryAdded(const LogEntry& entry)
+	{
+		MonoString* message = MonoUtil::stringToMono(MonoManager::instance().getDomain(), entry.getMessage());
+
+		MonoUtil::invokeThunk(onAddedThunk, entry.getChannel(), message);
 	}
 
 	void ScriptDebug::internal_log(MonoString* message)