فهرست منبع

Update status bar when debug log contents are removed
Clear compiler messages from log upon recompilation
Option to clear debug log upon entering play mode

BearishSun 10 سال پیش
والد
کامیت
dd61a6d788

+ 125 - 125
BansheeEditor/Include/BsGUIStatusBar.h

@@ -1,126 +1,126 @@
-#pragma once
-
-#include "BsEditorPrerequisites.h"
-#include "BsGUIElementContainer.h"
-
-namespace BansheeEngine
-{
-	/**
-	 * @brief  Editor window status bar that displays log messages and various other information.
-	 */
-	class BS_ED_EXPORT GUIStatusBar : public GUIElementContainer
-	{
-		struct PrivatelyConstruct {};
-
-	public:
-		/**
-		 * Returns type name of the GUI element used for finding GUI element styles. 
-		 */
-		static const String& getGUITypeName();
-
-		/**
-		 * Returns type name of the internal background GUI element. 
-		 */
-		static const String& getGUIBackgroundTypeName();
-
-		/**
-		 * Returns type name of the internal message button GUI element. 
-		 */
-		static const String& getGUIMessageTypeName();
-
-		/**
-		 * @brief	Creates a new GUI status bar.
-		 *
-		 * @param	options			Options that allow you to control how is the element positioned and sized.
-		 *							This will override any similar options set by style.
-		 * @param	styleName		Optional style to use for the element. Style will be retrieved
-		 *							from GUISkin of the GUIWidget the element is used on. If not specified
-		 *							default style is used.
-		 */
-		static GUIStatusBar* create(const GUIOptions& options, const String& style = StringUtil::BLANK);
-
-		/**
-		 * @brief	Creates a new GUI status bar.
-		 *
-		 * @param	options			Options that allow you to control how is the element positioned and sized.
-		 *							This will override any similar options set by style.
-		 * @param	styleName		Optional style to use for the element. Style will be retrieved
-		 *							from GUISkin of the GUIWidget the element is used on. If not specified
-		 *							default style is used.
-		 */
-		static GUIStatusBar* create(const String& style = StringUtil::BLANK);
-
-		GUIStatusBar(const PrivatelyConstruct& dummy, const String& style, const GUIDimensions& dimensions);
-
-		/**
-		 * @brief	Updates the active project displayed on the status bar.
-		 * 	
-		 * @param	name		Name of the project.
-		 * @param	modified	Should the project be displayed as modified (i.e. needs saving).
-		 */
-		void setProject(const WString& name, bool modified);
-
-		/**
-		 * @brief	Updates the active scene displayed on the status bar.
-		 * 	
-		 * @param	name		Name of the scene.
-		 * @param	modified	Should the scene be displayed as modified (i.e. needs saving).
-		 */
-		void setScene(const WString& name, bool modified);
-
-		/**
-		 * @brief	Activates or deactivates the "compilation in progress" visuals on the status bar.
-		 */
-		void setIsCompiling(bool compiling);
-
-		/**
-		 * @copydoc	GUIElement::setTint
-		 */
-		virtual void setTint(const Color& color) override;
-
-		/**
-		 * @copydoc	GUIElement::_updateLayoutInternal
-		 */
-		void _updateLayoutInternal(const GUILayoutData& data) override;
-
-		/**
-		 * @copydoc	GUIElement::_getOptimalSize
-		 */
-		Vector2I _getOptimalSize() const override;
-
-		Event<void()> onMessageClicked; /**< Triggered when the user clicks on the console message. */
-	private:
-		virtual ~GUIStatusBar();
-
-		/**
-		 * @copydoc	GUIElement::styleUpdated
-		 */
-		void styleUpdated() override;
-
-		/**
-		 * @brief	Triggered when a new entry is added to the debug Log.
-		 */
-		void logEntryAdded(const LogEntry& entry);
-
-		/**
-		 * @brief	Triggered when the user clicks on the message display.
-		 */
-		void messageBtnClicked();
-
-	private:
-		static const Color COLOR_INFO;
-		static const Color COLOR_WARNING;
-		static const Color COLOR_ERROR;
-
-		GUIPanel* mPanel;
-		GUIPanel* mBgPanel;
-		GUIButton* mMessage;
-		GUILabel* mScene;
-		GUILabel* mProject;
-		GUILabel* mCompiling;
-		GUITexture* mBackground;
-
-		HEvent mLogEntryAddedConn;
-		HEvent mMessageBtnPressedConn;
-	};
+#pragma once
+
+#include "BsEditorPrerequisites.h"
+#include "BsGUIElementContainer.h"
+
+namespace BansheeEngine
+{
+	/**
+	 * @brief  Editor window status bar that displays log messages and various other information.
+	 */
+	class BS_ED_EXPORT GUIStatusBar : public GUIElementContainer
+	{
+		struct PrivatelyConstruct {};
+
+	public:
+		/**
+		 * Returns type name of the GUI element used for finding GUI element styles. 
+		 */
+		static const String& getGUITypeName();
+
+		/**
+		 * Returns type name of the internal background GUI element. 
+		 */
+		static const String& getGUIBackgroundTypeName();
+
+		/**
+		 * Returns type name of the internal message button GUI element. 
+		 */
+		static const String& getGUIMessageTypeName();
+
+		/**
+		 * @brief	Creates a new GUI status bar.
+		 *
+		 * @param	options			Options that allow you to control how is the element positioned and sized.
+		 *							This will override any similar options set by style.
+		 * @param	styleName		Optional style to use for the element. Style will be retrieved
+		 *							from GUISkin of the GUIWidget the element is used on. If not specified
+		 *							default style is used.
+		 */
+		static GUIStatusBar* create(const GUIOptions& options, const String& style = StringUtil::BLANK);
+
+		/**
+		 * @brief	Creates a new GUI status bar.
+		 *
+		 * @param	options			Options that allow you to control how is the element positioned and sized.
+		 *							This will override any similar options set by style.
+		 * @param	styleName		Optional style to use for the element. Style will be retrieved
+		 *							from GUISkin of the GUIWidget the element is used on. If not specified
+		 *							default style is used.
+		 */
+		static GUIStatusBar* create(const String& style = StringUtil::BLANK);
+
+		GUIStatusBar(const PrivatelyConstruct& dummy, const String& style, const GUIDimensions& dimensions);
+
+		/**
+		 * @brief	Updates the active project displayed on the status bar.
+		 * 	
+		 * @param	name		Name of the project.
+		 * @param	modified	Should the project be displayed as modified (i.e. needs saving).
+		 */
+		void setProject(const WString& name, bool modified);
+
+		/**
+		 * @brief	Updates the active scene displayed on the status bar.
+		 * 	
+		 * @param	name		Name of the scene.
+		 * @param	modified	Should the scene be displayed as modified (i.e. needs saving).
+		 */
+		void setScene(const WString& name, bool modified);
+
+		/**
+		 * @brief	Activates or deactivates the "compilation in progress" visuals on the status bar.
+		 */
+		void setIsCompiling(bool compiling);
+
+		/**
+		 * @copydoc	GUIElement::setTint
+		 */
+		virtual void setTint(const Color& color) override;
+
+		/**
+		 * @copydoc	GUIElement::_updateLayoutInternal
+		 */
+		void _updateLayoutInternal(const GUILayoutData& data) override;
+
+		/**
+		 * @copydoc	GUIElement::_getOptimalSize
+		 */
+		Vector2I _getOptimalSize() const override;
+
+		Event<void()> onMessageClicked; /**< Triggered when the user clicks on the console message. */
+	private:
+		virtual ~GUIStatusBar();
+
+		/**
+		 * @copydoc	GUIElement::styleUpdated
+		 */
+		void styleUpdated() override;
+
+		/**
+		 * @brief	Triggered when the debug Log was modified.
+		 */
+		void logModified();
+
+		/**
+		 * @brief	Triggered when the user clicks on the message display.
+		 */
+		void messageBtnClicked();
+
+	private:
+		static const Color COLOR_INFO;
+		static const Color COLOR_WARNING;
+		static const Color COLOR_ERROR;
+
+		GUIPanel* mPanel;
+		GUIPanel* mBgPanel;
+		GUIButton* mMessage;
+		GUILabel* mScene;
+		GUILabel* mProject;
+		GUILabel* mCompiling;
+		GUITexture* mBackground;
+
+		HEvent mLogEntryAddedConn;
+		HEvent mMessageBtnPressedConn;
+	};
 }

+ 229 - 218
BansheeEditor/Source/BsGUIStatusBar.cpp

@@ -1,219 +1,230 @@
-#include "BsGUIStatusBar.h"
-#include "BsGUILayoutX.h"
-#include "BsGUILayoutY.h"
-#include "BsGUILabel.h"
-#include "BsGUIButton.h"
-#include "BsGUITexture.h"
-#include "BsGUIPanel.h"
-#include "BsGUISpace.h"
-#include "BsDebug.h"
-#include "BsBuiltinEditorResources.h"
-
-using namespace std::placeholders;
-
-namespace BansheeEngine
-{
-	const Color GUIStatusBar::COLOR_INFO = Color(1.0f, 1.0f, 1.0f, 1.0f);
-	const Color GUIStatusBar::COLOR_WARNING = Color(192 / 255.0f, 176 / 255.0f, 0.0f, 1.0f);
-	const Color GUIStatusBar::COLOR_ERROR = Color(192 / 255.0f, 36 / 255.0f, 0.0f, 1.0f);
-
-	GUIStatusBar::GUIStatusBar(const PrivatelyConstruct& dummy,
-		const String& style, const GUIDimensions& dimensions)
-		:GUIElementContainer(dimensions, style)
-	{
-		mPanel = GUIPanel::create();
-		mBgPanel = GUIPanel::create(1);
-		_registerChildElement(mPanel);
-		_registerChildElement(mBgPanel);
-
-		mBackground = GUITexture::create(GUIOptions(GUIOption::flexibleWidth()), getSubStyleName(getGUIBackgroundTypeName()));
-		mMessage = GUIButton::create(HString(L""), GUIOptions(GUIOption::flexibleWidth()), getSubStyleName(getGUIMessageTypeName()));
-		mScene = GUILabel::create(HString(L"Scene: Unnamed"), GUIOptions(GUIOption::fixedWidth(150)));
-		mProject = GUILabel::create(HString(L"Project: None"), GUIOptions(GUIOption::fixedWidth(200)));
-		mCompiling = GUILabel::create(HString(L"Compiling..."), GUIOptions(GUIOption::fixedWidth(100)));
-
-		GUILayoutY* vertLayout = mPanel->addNewElement<GUILayoutY>();
-		vertLayout->addNewElement<GUIFixedSpace>(3);
-		GUILayoutX* horzLayout = vertLayout->addNewElement<GUILayoutX>();
-
-		horzLayout->addNewElement<GUIFixedSpace>(10);
-		horzLayout->addElement(mMessage);
-		horzLayout->addNewElement<GUIFlexibleSpace>();
-		horzLayout->addElement(mScene);
-		horzLayout->addNewElement<GUIFixedSpace>(10);
-		horzLayout->addElement(mProject);
-		horzLayout->addNewElement<GUIFixedSpace>(10);
-		horzLayout->addElement(mCompiling);
-		horzLayout->addNewElement<GUIFixedSpace>(10);
-
-		mBgPanel->addElement(mBackground);
-		mCompiling->setActive(false);
-
-		mLogEntryAddedConn = gDebug().onLogEntryAdded.connect(std::bind(&GUIStatusBar::logEntryAdded, this, _1));
-		mMessageBtnPressedConn = mMessage->onClick.connect(std::bind(&GUIStatusBar::messageBtnClicked, this));
-	}
-
-	GUIStatusBar::~GUIStatusBar()
-	{
-		mLogEntryAddedConn.disconnect();
-		mMessageBtnPressedConn.disconnect();
-	}
-
-	GUIStatusBar* GUIStatusBar::create(const GUIOptions& options, const String& style)
-	{
-		const String* curStyle = &style;
-		if (*curStyle == StringUtil::BLANK)
-			curStyle = &getGUITypeName();
-
-		return bs_new<GUIStatusBar>(PrivatelyConstruct(), *curStyle, GUIDimensions::create(options));
-	}
-
-	GUIStatusBar* GUIStatusBar::create(const String& style)
-	{
-		const String* curStyle = &style;
-		if (*curStyle == StringUtil::BLANK)
-			curStyle = &getGUITypeName();
-
-		return bs_new<GUIStatusBar>(PrivatelyConstruct(), *curStyle, GUIDimensions::create());
-	}
-
-	void GUIStatusBar::setProject(const WString& name, bool modified)
-	{
-		WStringStream content;
-		content << L"Project: ";
-
-		if (name.size() > 20)
-			content << name.substr(0, 20) << L"...";
-		else
-			content << name;
-
-		if (modified)
-			content << L"*";
-
-		mProject->setContent(HString(content.str()));
-	}
-
-	void GUIStatusBar::setScene(const WString& name, bool modified)
-	{
-		WStringStream content;
-		content << L"Scene: ";
-
-		if (name.size() > 15)
-			content << name.substr(0, 15) << L"...";
-		else
-			content << name;
-
-		if (modified)
-			content << L"*";
-
-		mScene->setContent(HString(content.str()));
-	}
-
-	void GUIStatusBar::setIsCompiling(bool compiling)
-	{
-		mCompiling->setActive(compiling);
-	}
-	
-	void GUIStatusBar::setTint(const Color& color)
-	{
-		mBackground->setTint(color);
-		mMessage->setTint(color);
-	}
-
-	void GUIStatusBar::_updateLayoutInternal(const GUILayoutData& data)
-	{
-		mPanel->_setLayoutData(data);
-		mPanel->_updateLayoutInternal(data);
-
-		mBgPanel->_setLayoutData(data);
-		mBgPanel->_updateLayoutInternal(data);
-	}
-
-	Vector2I GUIStatusBar::_getOptimalSize() const
-	{
-		return mBgPanel->_getOptimalSize();
-	}
-	
-	void GUIStatusBar::styleUpdated()
-	{
-		mBackground->setStyle(getSubStyleName(getGUIBackgroundTypeName()));
-		mMessage->setStyle(getSubStyleName(getGUIMessageTypeName()));
-	}
-
-	void GUIStatusBar::logEntryAdded(const LogEntry& entry)
-	{
-		HSpriteTexture iconTexture;
-		Color textColor = COLOR_INFO;
-
-		UINT32 logChannel = entry.getChannel();
-		switch (logChannel)
-		{
-		case (UINT32)DebugChannel::Debug:
-			iconTexture = BuiltinEditorResources::instance().getLogMessageIcon(LogMessageIcon::Info, 16, false);
-			break;
-		case (UINT32)DebugChannel::Warning:
-			iconTexture = BuiltinEditorResources::instance().getLogMessageIcon(LogMessageIcon::Warning, 16, false);
-			textColor = COLOR_WARNING;
-			break;
-		case (UINT32)DebugChannel::Error:
-			iconTexture = BuiltinEditorResources::instance().getLogMessageIcon(LogMessageIcon::Error, 16, false);
-			textColor = COLOR_ERROR;
-			break;
-		}
-
-		WString message = toWString(entry.getMessage());
-		WString::size_type lfPos = message.find_first_of('\n');
-		WString::size_type crPos = message.find_first_of('\r');
-		WString::size_type newlinePos;
-
-		if (lfPos >= 0)
-		{
-			if (crPos >= 0)
-				newlinePos = std::min(lfPos, crPos);
-			else
-				newlinePos = lfPos;
-		}
-		else if (crPos >= 0)
-			newlinePos = crPos;
-		else
-			newlinePos = -1;
-
-		if (newlinePos == -1)
-		{
-			GUIContent messageContent(HString(message), iconTexture);
-			mMessage->setContent(messageContent);
-			mMessage->setTint(textColor);
-		}
-		else
-		{
-			WString firstLine = message.substr(0, newlinePos);
-
-			GUIContent messageContent(HString(firstLine), iconTexture);
-			mMessage->setContent(messageContent);
-			mMessage->setTint(textColor);
-		}		
-	}
-
-	void GUIStatusBar::messageBtnClicked()
-	{
-		onMessageClicked();
-	}
-
-	const String& GUIStatusBar::getGUITypeName()
-	{
-		static String TypeName = "GUIStatusBar";
-		return TypeName;
-	}
-
-	const String& GUIStatusBar::getGUIBackgroundTypeName()
-	{
-		static String TypeName = "GUIStatusBarBg";
-		return TypeName;
-	}
-
-	const String& GUIStatusBar::getGUIMessageTypeName()
-	{
-		static String TypeName = "GUIStatusBarMessage";
-		return TypeName;
-	}
+#include "BsGUIStatusBar.h"
+#include "BsGUILayoutX.h"
+#include "BsGUILayoutY.h"
+#include "BsGUILabel.h"
+#include "BsGUIButton.h"
+#include "BsGUITexture.h"
+#include "BsGUIPanel.h"
+#include "BsGUISpace.h"
+#include "BsDebug.h"
+#include "BsBuiltinEditorResources.h"
+
+using namespace std::placeholders;
+
+namespace BansheeEngine
+{
+	const Color GUIStatusBar::COLOR_INFO = Color(0.7f, 0.7f, 0.7f);
+	const Color GUIStatusBar::COLOR_WARNING = Color(192 / 255.0f, 176 / 255.0f, 0.0f);
+	const Color GUIStatusBar::COLOR_ERROR = Color(192 / 255.0f, 36 / 255.0f, 0.0f);
+
+	GUIStatusBar::GUIStatusBar(const PrivatelyConstruct& dummy,
+		const String& style, const GUIDimensions& dimensions)
+		:GUIElementContainer(dimensions, style)
+	{
+		mPanel = GUIPanel::create();
+		mBgPanel = GUIPanel::create(1);
+		_registerChildElement(mPanel);
+		_registerChildElement(mBgPanel);
+
+		mBackground = GUITexture::create(GUIOptions(GUIOption::flexibleWidth()), getSubStyleName(getGUIBackgroundTypeName()));
+		mMessage = GUIButton::create(HString(L""), GUIOptions(GUIOption::flexibleWidth()), getSubStyleName(getGUIMessageTypeName()));
+		mScene = GUILabel::create(HString(L"Scene: Unnamed"), GUIOptions(GUIOption::fixedWidth(150)));
+		mProject = GUILabel::create(HString(L"Project: None"), GUIOptions(GUIOption::fixedWidth(200)));
+		mCompiling = GUILabel::create(HString(L"Compiling..."), GUIOptions(GUIOption::fixedWidth(100)));
+
+		GUILayoutY* vertLayout = mPanel->addNewElement<GUILayoutY>();
+		vertLayout->addNewElement<GUIFixedSpace>(3);
+		GUILayoutX* horzLayout = vertLayout->addNewElement<GUILayoutX>();
+
+		horzLayout->addNewElement<GUIFixedSpace>(10);
+		horzLayout->addElement(mMessage);
+		horzLayout->addNewElement<GUIFlexibleSpace>();
+		horzLayout->addElement(mScene);
+		horzLayout->addNewElement<GUIFixedSpace>(10);
+		horzLayout->addElement(mProject);
+		horzLayout->addNewElement<GUIFixedSpace>(10);
+		horzLayout->addElement(mCompiling);
+		horzLayout->addNewElement<GUIFixedSpace>(10);
+
+		mBgPanel->addElement(mBackground);
+		mCompiling->setActive(false);
+
+		mLogEntryAddedConn = gDebug().onLogModified.connect(std::bind(&GUIStatusBar::logModified, this));
+		mMessageBtnPressedConn = mMessage->onClick.connect(std::bind(&GUIStatusBar::messageBtnClicked, this));
+	}
+
+	GUIStatusBar::~GUIStatusBar()
+	{
+		mLogEntryAddedConn.disconnect();
+		mMessageBtnPressedConn.disconnect();
+	}
+
+	GUIStatusBar* GUIStatusBar::create(const GUIOptions& options, const String& style)
+	{
+		const String* curStyle = &style;
+		if (*curStyle == StringUtil::BLANK)
+			curStyle = &getGUITypeName();
+
+		return bs_new<GUIStatusBar>(PrivatelyConstruct(), *curStyle, GUIDimensions::create(options));
+	}
+
+	GUIStatusBar* GUIStatusBar::create(const String& style)
+	{
+		const String* curStyle = &style;
+		if (*curStyle == StringUtil::BLANK)
+			curStyle = &getGUITypeName();
+
+		return bs_new<GUIStatusBar>(PrivatelyConstruct(), *curStyle, GUIDimensions::create());
+	}
+
+	void GUIStatusBar::setProject(const WString& name, bool modified)
+	{
+		WStringStream content;
+		content << L"Project: ";
+
+		if (name.size() > 20)
+			content << name.substr(0, 20) << L"...";
+		else
+			content << name;
+
+		if (modified)
+			content << L"*";
+
+		mProject->setContent(HString(content.str()));
+	}
+
+	void GUIStatusBar::setScene(const WString& name, bool modified)
+	{
+		WStringStream content;
+		content << L"Scene: ";
+
+		if (name.size() > 15)
+			content << name.substr(0, 15) << L"...";
+		else
+			content << name;
+
+		if (modified)
+			content << L"*";
+
+		mScene->setContent(HString(content.str()));
+	}
+
+	void GUIStatusBar::setIsCompiling(bool compiling)
+	{
+		mCompiling->setActive(compiling);
+	}
+	
+	void GUIStatusBar::setTint(const Color& color)
+	{
+		mBackground->setTint(color);
+		mMessage->setTint(color);
+	}
+
+	void GUIStatusBar::_updateLayoutInternal(const GUILayoutData& data)
+	{
+		mPanel->_setLayoutData(data);
+		mPanel->_updateLayoutInternal(data);
+
+		mBgPanel->_setLayoutData(data);
+		mBgPanel->_updateLayoutInternal(data);
+	}
+
+	Vector2I GUIStatusBar::_getOptimalSize() const
+	{
+		return mBgPanel->_getOptimalSize();
+	}
+	
+	void GUIStatusBar::styleUpdated()
+	{
+		mBackground->setStyle(getSubStyleName(getGUIBackgroundTypeName()));
+		mMessage->setStyle(getSubStyleName(getGUIMessageTypeName()));
+	}
+
+	void GUIStatusBar::logModified()
+	{
+		LogEntry entry;
+		if(!gDebug().getLog().getLastEntry(entry))
+		{
+			GUIContent messageContent(HString(L""));
+			mMessage->setContent(messageContent);
+
+			return;
+		}
+
+		HSpriteTexture iconTexture;
+		Color textColor = COLOR_INFO;
+
+		UINT32 logChannel = entry.getChannel();
+		switch (logChannel)
+		{
+		case (UINT32)DebugChannel::Debug:
+			iconTexture = BuiltinEditorResources::instance().getLogMessageIcon(LogMessageIcon::Info, 16, false);
+			break;
+		case (UINT32)DebugChannel::Warning:
+		case (UINT32)DebugChannel::CompilerWarning:
+			iconTexture = BuiltinEditorResources::instance().getLogMessageIcon(LogMessageIcon::Warning, 16, false);
+			textColor = COLOR_WARNING;
+			break;
+		case (UINT32)DebugChannel::Error:
+		case (UINT32)DebugChannel::CompilerError:
+			iconTexture = BuiltinEditorResources::instance().getLogMessageIcon(LogMessageIcon::Error, 16, false);
+			textColor = COLOR_ERROR;
+			break;
+		}
+
+		WString message = toWString(entry.getMessage());
+		WString::size_type lfPos = message.find_first_of('\n');
+		WString::size_type crPos = message.find_first_of('\r');
+		WString::size_type newlinePos;
+
+		if (lfPos >= 0)
+		{
+			if (crPos >= 0)
+				newlinePos = std::min(lfPos, crPos);
+			else
+				newlinePos = lfPos;
+		}
+		else if (crPos >= 0)
+			newlinePos = crPos;
+		else
+			newlinePos = -1;
+
+		if (newlinePos == -1)
+		{
+			GUIContent messageContent(HString(message), iconTexture);
+			mMessage->setContent(messageContent);
+			mMessage->setTint(textColor);
+		}
+		else
+		{
+			WString firstLine = message.substr(0, newlinePos);
+
+			GUIContent messageContent(HString(firstLine), iconTexture);
+			mMessage->setContent(messageContent);
+			mMessage->setTint(textColor);
+		}		
+	}
+
+	void GUIStatusBar::messageBtnClicked()
+	{
+		onMessageClicked();
+	}
+
+	const String& GUIStatusBar::getGUITypeName()
+	{
+		static String TypeName = "GUIStatusBar";
+		return TypeName;
+	}
+
+	const String& GUIStatusBar::getGUIBackgroundTypeName()
+	{
+		static String TypeName = "GUIStatusBarBg";
+		return TypeName;
+	}
+
+	const String& GUIStatusBar::getGUIMessageTypeName()
+	{
+		static String TypeName = "GUIStatusBarMessage";
+		return TypeName;
+	}
 }

+ 94 - 85
BansheeUtility/Include/BsDebug.h

@@ -1,86 +1,95 @@
-#pragma once
-
-#include "BsPrerequisitesUtil.h"
-#include "BsLog.h"
-
-namespace BansheeEngine
-{
-	class Log;
-
-	/**
-	 * @brief	Available types of channels that debug messages can be logged to.
-	 */
-	enum class DebugChannel
-	{
-		Debug, Warning, Error
-	};
-
-	/**
-	 * @brief	Utility class providing various debug functionality. Thread safe.
-	 */
-	class BS_UTILITY_EXPORT Debug
-	{
-	public:
-		/**
-		 * @brief	Adds a log entry in the "Debug" channel.
-		 */
-		void logDebug(const String& msg);
-
-		/**
-		 * @brief	Adds a log entry in the "Warning" channel.
-		 */
-		void logWarning(const String& msg);
-
-		/**
-		 * @brief	Adds a log entry in the "Error" channel.
-		 */
-		void logError(const String& msg);
-
-		/**
-		 * @brief	Adds a log entry in the specified channel. You may specify custom channels as needed.
-		 */
-		void log(const String& msg, UINT32 channel);
-
-		/**
-		 * @brief	Retrieves the Log used by the Debug instance.
-		 */
-		Log& getLog() { return mLog; }
-
-		/**
-		 * @brief	Converts raw pixels into a BMP image. See "BitmapWriter" for more information.
-		 */
-		void writeAsBMP(UINT8* rawPixels, UINT32 bytesPerPixel, UINT32 width, UINT32 height, const Path& filePath, bool overwrite = true) const;
-
-		/**
-		 * @brief	Saves a log about the current state of the application to the specified location.
-		 * 
-		 * @param	path	Absolute path to the log filename.
-		 */
-		void saveLog(const Path& path);
-
-		/**
-		 * @brief	Triggers callbacks that notify external code that a log entry was added.
-		 * 			
-		 * @note	Internal method. Sim thread only.
-		 */
-		void _triggerCallbacks();
-
-		/**
-		 * @brief	Triggered when a new entry in the log is added.
-		 * 			
-		 * @note	Sim thread only.
-		 */
-		Event<void(const LogEntry&)> onLogEntryAdded;
-	private:
-		Log mLog;
-	};
-
-	BS_UTILITY_EXPORT Debug& gDebug();
-
-#define LOGDBG(x) BansheeEngine::gDebug().logDebug((x));
-#define LOGWRN(x) BansheeEngine::gDebug().logWarning((x));
-#define LOGERR(x) BansheeEngine::gDebug().logError((x));
-
-#define LOGDBG_VERBOSE(x)
-#define LOGWRN_VERBOSE(x)
+#pragma once
+
+#include "BsPrerequisitesUtil.h"
+#include "BsLog.h"
+
+namespace BansheeEngine
+{
+	class Log;
+
+	/**
+	 * @brief	Available types of channels that debug messages can be logged to.
+	 */
+	enum class DebugChannel
+	{
+		Debug, Warning, Error, CompilerWarning, CompilerError
+	};
+
+	/**
+	 * @brief	Utility class providing various debug functionality. Thread safe.
+	 */
+	class BS_UTILITY_EXPORT Debug
+	{
+	public:
+		/**
+		 * @brief	Adds a log entry in the "Debug" channel.
+		 */
+		void logDebug(const String& msg);
+
+		/**
+		 * @brief	Adds a log entry in the "Warning" channel.
+		 */
+		void logWarning(const String& msg);
+
+		/**
+		 * @brief	Adds a log entry in the "Error" channel.
+		 */
+		void logError(const String& msg);
+
+		/**
+		 * @brief	Adds a log entry in the specified channel. You may specify custom channels as needed.
+		 */
+		void log(const String& msg, UINT32 channel);
+
+		/**
+		 * @brief	Retrieves the Log used by the Debug instance.
+		 */
+		Log& getLog() { return mLog; }
+
+		/**
+		 * @brief	Converts raw pixels into a BMP image. See "BitmapWriter" for more information.
+		 */
+		void writeAsBMP(UINT8* rawPixels, UINT32 bytesPerPixel, UINT32 width, UINT32 height, const Path& filePath, bool overwrite = true) const;
+
+		/**
+		 * @brief	Saves a log about the current state of the application to the specified location.
+		 * 
+		 * @param	path	Absolute path to the log filename.
+		 */
+		void saveLog(const Path& path) const;
+
+		/**
+		 * @brief	Triggers callbacks that notify external code that a log entry was added.
+		 * 			
+		 * @note	Internal method. Sim thread only.
+		 */
+		void _triggerCallbacks();
+
+		/**
+		 * @brief	Triggered when a new entry in the log is added.
+		 * 			
+		 * @note	Sim thread only.
+		 */
+		Event<void(const LogEntry&)> onLogEntryAdded;
+
+		/**
+		 * @brief	Triggered whenever one or multiple log entries were added or removed.
+		 *			Triggers only once per frame.
+		 * 			
+		 * @note	Sim thread only.
+		 */
+		Event<void()> onLogModified;
+	private:
+		UINT64 mLogHash = 0;
+		Log mLog;
+	};
+
+	BS_UTILITY_EXPORT Debug& gDebug();
+
+#define LOGDBG(x) BansheeEngine::gDebug().logDebug((x));
+#define LOGWRN(x) BansheeEngine::gDebug().logWarning((x));
+#define LOGERR(x) BansheeEngine::gDebug().logError((x));
+
+#define LOGDBG_VERBOSE(x)
+#define LOGWRN_VERBOSE(x)
 }

+ 98 - 77
BansheeUtility/Include/BsLog.h

@@ -1,78 +1,99 @@
-#pragma once
-
-#include "BsPrerequisitesUtil.h"
-#include "BsEvent.h"
-
-namespace BansheeEngine
-{
-	/**
-	 * @brief	A single log entry, containing a message and a channel the message
-	 * 			was recorded on.
-	 */
-	class BS_UTILITY_EXPORT LogEntry
-	{
-	public:
-		LogEntry() { }
-		LogEntry(const String& msg, UINT32 channel);
-
-		UINT32 getChannel() const { return mChannel; }
-		const String& getMessage() const { return mMsg; }
-
-	private:
-		String mMsg;
-		UINT32 mChannel;
-	};
-
-	/**
-	 * @brief	Used for logging messages. Can categorize messages according to channels, save the log to a file
-	 * 			and send out callbacks when a new message is added.
-	 * 			
-	 * @note	Thread safe.
-	 */
-	class BS_UTILITY_EXPORT Log
-	{
-	public:
-		Log();
-		~Log();
-
-		/**
-		 * @brief	Logs a new message. 
-		 *
-		 * @param	message	The message describing the log entry.
-		 * @param	channel Channel in which to store the log entry.
-		 */
-		void logMsg(const String& message, UINT32 channel);
-
-		/**
-		 * @brief	Removes all log entries. 
-		 */
-		void clear();
-
-		/**
-		 * @brief	Returns all existing log entries.
-		 */
-		Vector<LogEntry> getEntries() const;
-
-		/**
-		 * @brief	Returns the latest unread entry from the log queue, and removes the entry from the unread entries
-		 * 			list.
-		 * 			
-		 * @param	entry	Entry that was read, or undefined if no entries exist.
-		 * 					
-		 * @returns	True if an unread entry was retrieved, false otherwise.
-		 */
-		bool getUnreadEntry(LogEntry& entry);
-
-	private:
-		friend class Debug;
-
-		/**
-		 * @brief	Returns all log entries, including those marked as unread.
-		 */
-		Vector<LogEntry> getAllEntries() const;
-
-		Vector<LogEntry> mEntries;
-		Queue<LogEntry> mUnreadEntries;
-		BS_RECURSIVE_MUTEX(mMutex);
-	};
+#pragma once
+
+#include "BsPrerequisitesUtil.h"
+#include "BsEvent.h"
+
+namespace BansheeEngine
+{
+	/**
+	 * @brief	A single log entry, containing a message and a channel the message
+	 * 			was recorded on.
+	 */
+	class BS_UTILITY_EXPORT LogEntry
+	{
+	public:
+		LogEntry() { }
+		LogEntry(const String& msg, UINT32 channel);
+
+		UINT32 getChannel() const { return mChannel; }
+		const String& getMessage() const { return mMsg; }
+
+	private:
+		String mMsg;
+		UINT32 mChannel;
+	};
+
+	/**
+	 * @brief	Used for logging messages. Can categorize messages according to channels, save the log to a file
+	 * 			and send out callbacks when a new message is added.
+	 * 			
+	 * @note	Thread safe.
+	 */
+	class BS_UTILITY_EXPORT Log
+	{
+	public:
+		Log();
+		~Log();
+
+		/**
+		 * @brief	Logs a new message. 
+		 *
+		 * @param	message	The message describing the log entry.
+		 * @param	channel Channel in which to store the log entry.
+		 */
+		void logMsg(const String& message, UINT32 channel);
+
+		/**
+		 * @brief	Removes all log entries. 
+		 */
+		void clear();
+
+		/**
+		 * @brief	Removes all log entries in a specific channel. 
+		 */
+		void clear(UINT32 channel);
+
+		/**
+		 * @brief	Returns all existing log entries.
+		 */
+		Vector<LogEntry> getEntries() const;
+
+		/**
+		 * @brief	Returns the latest unread entry from the log queue, and removes the entry from the unread entries
+		 * 			list.
+		 * 			
+		 * @param	entry	Entry that was retrieved, or undefined if no entries exist.
+		 * 					
+		 * @returns	True if an unread entry was retrieved, false otherwise.
+		 */
+		bool getUnreadEntry(LogEntry& entry);
+
+		/**
+		 * @brief	Returns the last available log entry.
+		 *
+		 * @param	entry	Entry that was retrieved, or undefined if no entries exist.
+		 *
+		 * @returns	True if an entry was retrieved, false otherwise.
+		 */
+		bool getLastEntry(LogEntry& entry);
+
+		/**
+		 * @brief	Returns a hash value that is modified whenever entries in the log change. This can be used for
+		 *			checking for changes by external systems.
+		 */
+		UINT64 getHash() const { return mHash; }
+
+	private:
+		friend class Debug;
+
+		/**
+		 * @brief	Returns all log entries, including those marked as unread.
+		 */
+		Vector<LogEntry> getAllEntries() const;
+
+		Vector<LogEntry> mEntries;
+		Queue<LogEntry> mUnreadEntries;
+		UINT64 mHash;
+		BS_RECURSIVE_MUTEX(mMutex);
+	};
 }

+ 264 - 257
BansheeUtility/Source/BsDebug.cpp

@@ -1,258 +1,265 @@
-#include "BsDebug.h"
-#include "BsLog.h"
-#include "BsException.h"
-#include "BsBitmapWriter.h"
-#include "BsFileSystem.h"
-#include "BsDataStream.h"
-
-#if BS_PLATFORM == BS_PLATFORM_WIN32 && BS_COMPILER == BS_COMPILER_MSVC
-#include <windows.h>
-#include <iostream>
-
-void logToIDEConsole(const BansheeEngine::String& message)
-{
-	OutputDebugString(message.c_str());
-	OutputDebugString("\n");
-
-	// Also default output in case we're running without debugger attached
-	std::cout << message << std::endl;
-}
-#else
-void logToIDEConsole(const BansheeEngine::String& message)
-{
-	// Do nothing
-}
-#endif
-
-namespace BansheeEngine
-{
-	void Debug::logDebug(const String& msg)
-	{
-		mLog.logMsg(msg, (UINT32)DebugChannel::Debug);
-		logToIDEConsole(msg);
-	}
-
-	void Debug::logWarning(const String& msg)
-	{
-		mLog.logMsg(msg, (UINT32)DebugChannel::Warning);
-		logToIDEConsole(msg);
-	}
-
-	void Debug::logError(const String& msg)
-	{
-		mLog.logMsg(msg, (UINT32)DebugChannel::Error);
-		logToIDEConsole(msg);
-	}
-
-	void Debug::log(const String& msg, UINT32 channel)
-	{
-		mLog.logMsg(msg, channel);
-		logToIDEConsole(msg);
-	}
-
-	void Debug::writeAsBMP(UINT8* rawPixels, UINT32 bytesPerPixel, UINT32 width, UINT32 height, const Path& filePath, bool overwrite) const
-	{
-		if(FileSystem::isFile(filePath))
-		{
-			if(overwrite)
-				FileSystem::remove(filePath);
-			else
-				BS_EXCEPT(FileNotFoundException, "File already exists at specified location: " + filePath.toString());
-		}
-
-		DataStreamPtr ds = FileSystem::createAndOpenFile(filePath);
-
-		UINT32 bmpDataSize = BitmapWriter::getBMPSize(width, height, bytesPerPixel);
-		UINT8* bmpBuffer = bs_newN<UINT8>(bmpDataSize);
-
-		BitmapWriter::rawPixelsToBMP(rawPixels, bmpBuffer, width, height, bytesPerPixel);
-
-		ds->write(bmpBuffer, bmpDataSize);
-		ds->close();
-
-		bs_deleteN(bmpBuffer, bmpDataSize);
-	}
-
-	void Debug::_triggerCallbacks()
-	{
-		LogEntry entry;
-		while (mLog.getUnreadEntry(entry))
-		{
-			onLogEntryAdded(entry);
-		}
-	}
-
-	void Debug::saveLog(const Path& path)
-	{
-		static const char* style =
-			R"(html {
-  font-family: sans-serif;
-} 
-            
-table
-{
-    border-collapse: collapse;
-    border-spacing: 0;
-    empty-cells: show;
-    border: 1px solid #cbcbcb;
-  	width:100%;
-  	table-layout:fixed;
-}
-
-table caption 
-{
-    color: #000;
-    font: italic 85%/1 arial, sans-serif;
-    padding: 1em 0;
-    text-align: center;
-}
-
-table td,
-table th 
-{
-    border-left: 1px solid #cbcbcb;/*  inner column border */
-    border-width: 0 0 0 1px;
-    font-size: inherit;
-    margin: 0;
-    overflow: visible; /*to make ths where the title is really long work*/
-    padding: 0.5em 1em; /* cell padding */
-}
-
-table td:first-child,
-table th:first-child 
-{
-    border-left-width: 0;
-}
-
-table thead 
-{
-    background-color: #e0e0e0;
-    color: #000;
-    text-align: left;
-    vertical-align: bottom;
-}
-
-table td 
-{
-    background-color: transparent;
-  	word-wrap:break-word;
-  	vertical-align: top;
-  	color: #7D7D7D;
-}
-
-.debug-row td {
-    background-color: #FFFFFF;
-}
-
-.debug-alt-row td {
-    background-color: #f2f2f2;
-}
-
-.warn-row td {
-    background-color: #ffc016;
-    color: #5F5F5F;
-}
-
-.warn-alt-row td {
-    background-color: #fdcb41;
-    color: #5F5F5F;
-}
-
-.error-row td {
-    background-color: #9f1621;
-    color: #9F9F9F;
-}
-
-.error-alt-row td {
-    background-color: #ae1621;
-    color: #9F9F9F;
-}
-)";
-
-		static const char* htmlPreStyleHeader =
-			R"(<!DOCTYPE html>
-<html lang="en">
-<head>
-<style type="text/css">
-)";
-
-		static const char* htmlPostStyleHeader =
-			R"(</style>
-<title>Banshee Engine Log</title>
-</head>
-<body>
-<h1>Banshee Engine Log</h1>
-<table border="1" cellpadding="1" cellspacing="1">
-	<thead>
-		<tr>
-			<th scope="col" style="width:60px">Type</th>
-			<th scope="col">Description</th>
-		</tr>
-	</thead>
-	<tbody>
-)";
-
-		static const char* htmlFooter =
-			R"(   </tbody>
-</table>
-</body>
-</html>)";
-
-		StringStream stream;
-		stream << htmlPreStyleHeader;
-		stream << style;
-		stream << htmlPostStyleHeader;
-
-		bool alternate = false;
-		Vector<LogEntry> entries = mLog.getAllEntries();
-		for (auto& entry : entries)
-		{
-			String channelName;
-			if (entry.getChannel() == (UINT32)DebugChannel::Error)
-			{
-				if (!alternate)
-					stream << R"(		<tr class="error-row">)" << std::endl;
-				else
-					stream << R"(		<tr class="error-alt-row">)" << std::endl;
-
-				stream << R"(			<td>Error</td>)" << std::endl;
-			}
-			else if (entry.getChannel() == (UINT32)DebugChannel::Warning)
-			{
-				if (!alternate)
-					stream << R"(		<tr class="warn-row">)" << std::endl;
-				else
-					stream << R"(		<tr class="warn-alt-row">)" << std::endl;
-
-				stream << R"(			<td>Warning</td>)" << std::endl;
-			}
-			else
-			{
-				if (!alternate)
-					stream << R"(		<tr class="debug-row">)" << std::endl;
-				else
-					stream << R"(		<tr class="debug-alt-row">)" << std::endl;
-
-				stream << R"(			<td>Debug</td>)" << std::endl;
-			}
-
-			String parsedMessage = StringUtil::replaceAll(entry.getMessage(), "\n", "<br>\n");
-
-			stream << R"(			<td>)" << parsedMessage << "</td>" << std::endl;
-			stream << R"(		</tr>)" << std::endl;
-
-			alternate = !alternate;
-		}
-
-		stream << htmlFooter;
-
-		DataStreamPtr fileStream = FileSystem::createAndOpenFile(path);
-		fileStream->writeString(stream.str());
-	}
-
-	BS_UTILITY_EXPORT Debug& gDebug()
-	{
-		static Debug debug;
-		return debug;
-	}
+#include "BsDebug.h"
+#include "BsLog.h"
+#include "BsException.h"
+#include "BsBitmapWriter.h"
+#include "BsFileSystem.h"
+#include "BsDataStream.h"
+
+#if BS_PLATFORM == BS_PLATFORM_WIN32 && BS_COMPILER == BS_COMPILER_MSVC
+#include <windows.h>
+#include <iostream>
+
+void logToIDEConsole(const BansheeEngine::String& message)
+{
+	OutputDebugString(message.c_str());
+	OutputDebugString("\n");
+
+	// Also default output in case we're running without debugger attached
+	std::cout << message << std::endl;
+}
+#else
+void logToIDEConsole(const BansheeEngine::String& message)
+{
+	// Do nothing
+}
+#endif
+
+namespace BansheeEngine
+{
+	void Debug::logDebug(const String& msg)
+	{
+		mLog.logMsg(msg, (UINT32)DebugChannel::Debug);
+		logToIDEConsole(msg);
+	}
+
+	void Debug::logWarning(const String& msg)
+	{
+		mLog.logMsg(msg, (UINT32)DebugChannel::Warning);
+		logToIDEConsole(msg);
+	}
+
+	void Debug::logError(const String& msg)
+	{
+		mLog.logMsg(msg, (UINT32)DebugChannel::Error);
+		logToIDEConsole(msg);
+	}
+
+	void Debug::log(const String& msg, UINT32 channel)
+	{
+		mLog.logMsg(msg, channel);
+		logToIDEConsole(msg);
+	}
+
+	void Debug::writeAsBMP(UINT8* rawPixels, UINT32 bytesPerPixel, UINT32 width, UINT32 height, const Path& filePath, bool overwrite) const
+	{
+		if(FileSystem::isFile(filePath))
+		{
+			if(overwrite)
+				FileSystem::remove(filePath);
+			else
+				BS_EXCEPT(FileNotFoundException, "File already exists at specified location: " + filePath.toString());
+		}
+
+		DataStreamPtr ds = FileSystem::createAndOpenFile(filePath);
+
+		UINT32 bmpDataSize = BitmapWriter::getBMPSize(width, height, bytesPerPixel);
+		UINT8* bmpBuffer = bs_newN<UINT8>(bmpDataSize);
+
+		BitmapWriter::rawPixelsToBMP(rawPixels, bmpBuffer, width, height, bytesPerPixel);
+
+		ds->write(bmpBuffer, bmpDataSize);
+		ds->close();
+
+		bs_deleteN(bmpBuffer, bmpDataSize);
+	}
+
+	void Debug::_triggerCallbacks()
+	{
+		LogEntry entry;
+		while (mLog.getUnreadEntry(entry))
+		{
+			onLogEntryAdded(entry);
+		}
+
+		UINT64 hash = mLog.getHash();
+		if(mLogHash != hash)
+		{
+			onLogModified();
+			mLogHash = hash;
+		}
+	}
+
+	void Debug::saveLog(const Path& path) const
+	{
+		static const char* style =
+			R"(html {
+  font-family: sans-serif;
+} 
+            
+table
+{
+    border-collapse: collapse;
+    border-spacing: 0;
+    empty-cells: show;
+    border: 1px solid #cbcbcb;
+  	width:100%;
+  	table-layout:fixed;
+}
+
+table caption 
+{
+    color: #000;
+    font: italic 85%/1 arial, sans-serif;
+    padding: 1em 0;
+    text-align: center;
+}
+
+table td,
+table th 
+{
+    border-left: 1px solid #cbcbcb;/*  inner column border */
+    border-width: 0 0 0 1px;
+    font-size: inherit;
+    margin: 0;
+    overflow: visible; /*to make ths where the title is really long work*/
+    padding: 0.5em 1em; /* cell padding */
+}
+
+table td:first-child,
+table th:first-child 
+{
+    border-left-width: 0;
+}
+
+table thead 
+{
+    background-color: #e0e0e0;
+    color: #000;
+    text-align: left;
+    vertical-align: bottom;
+}
+
+table td 
+{
+    background-color: transparent;
+  	word-wrap:break-word;
+  	vertical-align: top;
+  	color: #7D7D7D;
+}
+
+.debug-row td {
+    background-color: #FFFFFF;
+}
+
+.debug-alt-row td {
+    background-color: #f2f2f2;
+}
+
+.warn-row td {
+    background-color: #ffc016;
+    color: #5F5F5F;
+}
+
+.warn-alt-row td {
+    background-color: #fdcb41;
+    color: #5F5F5F;
+}
+
+.error-row td {
+    background-color: #9f1621;
+    color: #9F9F9F;
+}
+
+.error-alt-row td {
+    background-color: #ae1621;
+    color: #9F9F9F;
+}
+)";
+
+		static const char* htmlPreStyleHeader =
+			R"(<!DOCTYPE html>
+<html lang="en">
+<head>
+<style type="text/css">
+)";
+
+		static const char* htmlPostStyleHeader =
+			R"(</style>
+<title>Banshee Engine Log</title>
+</head>
+<body>
+<h1>Banshee Engine Log</h1>
+<table border="1" cellpadding="1" cellspacing="1">
+	<thead>
+		<tr>
+			<th scope="col" style="width:60px">Type</th>
+			<th scope="col">Description</th>
+		</tr>
+	</thead>
+	<tbody>
+)";
+
+		static const char* htmlFooter =
+			R"(   </tbody>
+</table>
+</body>
+</html>)";
+
+		StringStream stream;
+		stream << htmlPreStyleHeader;
+		stream << style;
+		stream << htmlPostStyleHeader;
+
+		bool alternate = false;
+		Vector<LogEntry> entries = mLog.getAllEntries();
+		for (auto& entry : entries)
+		{
+			String channelName;
+			if (entry.getChannel() == (UINT32)DebugChannel::Error || entry.getChannel() == (UINT32)DebugChannel::CompilerError)
+			{
+				if (!alternate)
+					stream << R"(		<tr class="error-row">)" << std::endl;
+				else
+					stream << R"(		<tr class="error-alt-row">)" << std::endl;
+
+				stream << R"(			<td>Error</td>)" << std::endl;
+			}
+			else if (entry.getChannel() == (UINT32)DebugChannel::Warning || entry.getChannel() == (UINT32)DebugChannel::CompilerWarning)
+			{
+				if (!alternate)
+					stream << R"(		<tr class="warn-row">)" << std::endl;
+				else
+					stream << R"(		<tr class="warn-alt-row">)" << std::endl;
+
+				stream << R"(			<td>Warning</td>)" << std::endl;
+			}
+			else
+			{
+				if (!alternate)
+					stream << R"(		<tr class="debug-row">)" << std::endl;
+				else
+					stream << R"(		<tr class="debug-alt-row">)" << std::endl;
+
+				stream << R"(			<td>Debug</td>)" << std::endl;
+			}
+
+			String parsedMessage = StringUtil::replaceAll(entry.getMessage(), "\n", "<br>\n");
+
+			stream << R"(			<td>)" << parsedMessage << "</td>" << std::endl;
+			stream << R"(		</tr>)" << std::endl;
+
+			alternate = !alternate;
+		}
+
+		stream << htmlFooter;
+
+		DataStreamPtr fileStream = FileSystem::createAndOpenFile(path);
+		fileStream->writeString(stream.str());
+	}
+
+	BS_UTILITY_EXPORT Debug& gDebug()
+	{
+		static Debug debug;
+		return debug;
+	}
 }

+ 118 - 74
BansheeUtility/Source/BsLog.cpp

@@ -1,75 +1,119 @@
-#include "BsLog.h"
-#include "BsException.h"
-
-namespace BansheeEngine
-{
-	LogEntry::LogEntry(const String& msg, UINT32 channel)
-		:mMsg(msg), mChannel(channel)
-	{ }
-
-	Log::Log()
-	{
-	}
-
-	Log::~Log()
-	{
-		clear();
-	}
-
-	void Log::logMsg(const String& message, UINT32 channel)
-	{
-		BS_LOCK_RECURSIVE_MUTEX(mMutex);
-
-		mUnreadEntries.push(LogEntry(message, channel));
-	}
-
-	void Log::clear()
-	{
-		BS_LOCK_RECURSIVE_MUTEX(mMutex);
-
-		mEntries.clear();
-
-		while (!mUnreadEntries.empty())
-			mUnreadEntries.pop();
-	}
-
-	bool Log::getUnreadEntry(LogEntry& entry)
-	{
-		BS_LOCK_RECURSIVE_MUTEX(mMutex);
-
-		if (mUnreadEntries.empty())
-			return false;
-
-		entry = mUnreadEntries.front();
-		mUnreadEntries.pop();
-		mEntries.push_back(entry);
-
-		return true;
-	}
-
-	Vector<LogEntry> Log::getEntries() const
-	{
-		BS_LOCK_RECURSIVE_MUTEX(mMutex);
-
-		return mEntries;
-	}
-
-	Vector<LogEntry> Log::getAllEntries() const
-	{
-		Vector<LogEntry> entries;
-		{
-			BS_LOCK_RECURSIVE_MUTEX(mMutex);
-
-			for (auto& entry : mEntries)
-				entries.push_back(entry);
-
-			Queue<LogEntry> unreadEntries = mUnreadEntries;
-			while (!unreadEntries.empty())
-			{
-				entries.push_back(unreadEntries.front());
-				unreadEntries.pop();
-			}
-		}
-		return entries;
-	}
+#include "BsLog.h"
+#include "BsException.h"
+
+namespace BansheeEngine
+{
+	LogEntry::LogEntry(const String& msg, UINT32 channel)
+		:mMsg(msg), mChannel(channel)
+	{ }
+
+	Log::Log()
+		:mHash(0)
+	{
+	}
+
+	Log::~Log()
+	{
+		clear();
+	}
+
+	void Log::logMsg(const String& message, UINT32 channel)
+	{
+		BS_LOCK_RECURSIVE_MUTEX(mMutex);
+
+		mUnreadEntries.push(LogEntry(message, channel));
+	}
+
+	void Log::clear()
+	{
+		BS_LOCK_RECURSIVE_MUTEX(mMutex);
+
+		mEntries.clear();
+
+		while (!mUnreadEntries.empty())
+			mUnreadEntries.pop();
+
+		mHash++;
+	}
+
+	void Log::clear(UINT32 channel)
+	{
+		BS_LOCK_RECURSIVE_MUTEX(mMutex);
+
+		Vector<LogEntry> newEntries;
+		for(auto& entry : mEntries)
+		{
+			if (entry.getChannel() == channel)
+				continue;
+
+			newEntries.push_back(entry);
+		}
+
+		mEntries = newEntries;
+
+		Queue<LogEntry> newUnreadEntries;
+		while (!mUnreadEntries.empty())
+		{
+			LogEntry entry = mUnreadEntries.front();
+			mUnreadEntries.pop();
+
+			if (entry.getChannel() == channel)
+				continue;
+
+			newUnreadEntries.push(entry);
+		}
+
+		mUnreadEntries = newUnreadEntries;
+		mHash++;
+	}
+
+	bool Log::getUnreadEntry(LogEntry& entry)
+	{
+		BS_LOCK_RECURSIVE_MUTEX(mMutex);
+
+		if (mUnreadEntries.empty())
+			return false;
+
+		entry = mUnreadEntries.front();
+		mUnreadEntries.pop();
+		mEntries.push_back(entry);
+		mHash++;
+
+		return true;
+	}
+
+	bool Log::getLastEntry(LogEntry& entry)
+	{
+		if (mEntries.size() == 0)
+			return false;
+
+		entry = mEntries.back();
+		return true;
+	}
+
+	Vector<LogEntry> Log::getEntries() const
+	{
+		BS_LOCK_RECURSIVE_MUTEX(mMutex);
+
+		return mEntries;
+	}
+
+	Vector<LogEntry> Log::getAllEntries() const
+	{
+		Vector<LogEntry> entries;
+		{
+			BS_LOCK_RECURSIVE_MUTEX(mMutex);
+
+			for (auto& entry : mEntries)
+				entries.push_back(entry);
+
+			Queue<LogEntry> unreadEntries = mUnreadEntries;
+			while (!unreadEntries.empty())
+			{
+				entries.push_back(unreadEntries.front());
+				unreadEntries.pop();
+			}
+		}
+		return entries;
+	}
 }

+ 541 - 490
MBansheeEditor/ConsoleWindow.cs

@@ -1,490 +1,541 @@
-using BansheeEngine;
-using System;
-using System.Collections.Generic;
-using System.IO;
-
-namespace BansheeEditor
-{
-    /// <summary>
-    /// Displays a list of log messages.
-    /// </summary>
-    [DefaultSize(600, 300)]
-    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 TITLE_HEIGHT = 21;
-        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);
-        private static int sSelectedElementIdx = -1;
-
-        private GUIListView<ConsoleGUIEntry, ConsoleEntryData> listView;
-        private List<ConsoleEntryData> entries = new List<ConsoleEntryData>();
-        private List<ConsoleEntryData> filteredEntries = new List<ConsoleEntryData>();
-        private EntryFilter filter = EntryFilter.All;
-        private GUITexture detailsSeparator;
-        private GUIScrollArea detailsArea;
-
-        /// <summary>
-        /// Returns the height of the list area.
-        /// </summary>
-        private int ListHeight
-        {
-            get { return Height - TITLE_HEIGHT; }
-        }
-
-        /// <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();
-            GUILayoutX titleLayout = layout.AddLayoutX();
-
-            GUIContentImages infoImages = new GUIContentImages(
-                EditorBuiltin.GetLogIcon(LogIcon.Info, 16, false), 
-                EditorBuiltin.GetLogIcon(LogIcon.Info, 16, true));
-
-            GUIContentImages warningImages = new GUIContentImages(
-                EditorBuiltin.GetLogIcon(LogIcon.Warning, 16, false), 
-                EditorBuiltin.GetLogIcon(LogIcon.Warning, 16, true));
-
-            GUIContentImages errorImages = new GUIContentImages(
-                EditorBuiltin.GetLogIcon(LogIcon.Error, 16, false), 
-                EditorBuiltin.GetLogIcon(LogIcon.Error, 16, true));
-
-            GUIToggle infoBtn = new GUIToggle(new GUIContent(infoImages), EditorStyles.Button, GUIOption.FixedHeight(25));
-            GUIToggle warningBtn = new GUIToggle(new GUIContent(warningImages), EditorStyles.Button, GUIOption.FixedHeight(25));
-            GUIToggle errorBtn = new GUIToggle(new GUIContent(errorImages), EditorStyles.Button, GUIOption.FixedHeight(25));
-
-            GUIToggle detailsBtn = new GUIToggle(new LocEdString("Show details"), EditorStyles.Button, GUIOption.FixedHeight(25));
-            GUIButton clearBtn = new GUIButton(new LocEdString("Clear"), GUIOption.FixedHeight(25));
-
-            titleLayout.AddElement(infoBtn);
-            titleLayout.AddElement(warningBtn);
-            titleLayout.AddElement(errorBtn);
-            titleLayout.AddFlexibleSpace();
-            titleLayout.AddElement(detailsBtn);
-            titleLayout.AddElement(clearBtn);
-
-            infoBtn.Value = filter.HasFlag(EntryFilter.Info);
-            warningBtn.Value = filter.HasFlag(EntryFilter.Warning);
-            errorBtn.Value = filter.HasFlag(EntryFilter.Error);
-
-            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 += Clear;
-
-            GUILayoutX mainLayout = layout.AddLayoutX();
-
-            listView = new GUIListView<ConsoleGUIEntry, ConsoleEntryData>(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);
-
-            LogEntry[] existingEntries = Debug.Messages;
-            for (int i = 0; i < existingEntries.Length; i++)
-                OnEntryAdded(existingEntries[i].type, existingEntries[i].message);
-
-            Debug.OnAdded += OnEntryAdded;
-        }
-
-        private void OnEditorUpdate()
-        {
-            listView.Update();
-        }
-
-        private void OnDestroy()
-        {
-            Debug.OnAdded -= OnEntryAdded;
-        }
-
-        /// <inheritdoc/>
-        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);
-        }
-
-        /// <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)
-        {
-            // Check if compiler message, otherwise parse it normally
-            ParsedLogEntry logEntry = ScriptCodeManager.ParseCompilerMessage(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);
-            }
-        }
-
-        /// <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;
-
-            this.filter = filter;
-
-            listView.Clear();
-            filteredEntries.Clear();
-            foreach (var entry in entries)
-            {
-                if (DoesFilterMatch(entry.type))
-                {
-                    listView.AddEntry(entry);
-                    filteredEntries.Add(entry);
-                }
-            }
-
-            sSelectedElementIdx = -1;
-        }
-
-        /// <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()
-        {
-            Debug.Clear();
-
-            listView.Clear();
-            entries.Clear();
-            filteredEntries.Clear();
-
-            sSelectedElementIdx = -1;
-            RefreshDetailsPanel();
-        }
-
-        /// <summary>
-        /// Shows or hides the details panel.
-        /// </summary>
-        /// <param name="show">True to show, false to hide.</param>
-        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);
-        }
-
-        /// <summary>
-        /// Updates the contents of the details panel according to the currently selected element.
-        /// </summary>
-        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);
-                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);
-                        };
-                    }
-                }
-            }
-            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();
-            }
-        }
-
-        /// <summary>
-        /// Contains data for a single entry in the console.
-        /// </summary>
-        private class ConsoleEntryData : GUIListViewData
-        {
-            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 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.LightGray;
-            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;
-
-            /// <inheritdoc/>
-            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;
-            }
-
-            /// <inheritdoc/>
-            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.GetLogIcon(LogIcon.Info, 32, false));
-                        break;
-                    case DebugMessageType.Warning:
-                        icon.SetTexture(EditorBuiltin.GetLogIcon(LogIcon.Warning, 32, false));
-                        break;
-                    case DebugMessageType.Error:
-                        icon.SetTexture(EditorBuiltin.GetLogIcon(LogIcon.Error, 32, false));
-                        break;
-                }
-
-                messageLabel.SetContent(new LocEdString(data.message));
-
-                string method = "";
-                if (data.callstack != null && data.callstack.Length > 0)
-                {
-                    file = Path.GetFileName(data.callstack[0].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;
-            }
-
-            /// <summary>
-            /// Triggered when the entry is selected.
-            /// </summary>
-            private void OnClicked()
-            {
-                sSelectedElementIdx = entryIdx;
-
-                ConsoleWindow window = GetWindow<ConsoleWindow>();
-                window.RefreshDetailsPanel();
-
-                RefreshEntries();
-            }
-
-            /// <summary>
-            /// Triggered when the entry is double-clicked.
-            /// </summary>
-            private void OnDoubleClicked()
-            {
-                if(!string.IsNullOrEmpty(file))
-                    CodeEditor.OpenFile(file, line);
-            }
-        }
-    }
-}
+using BansheeEngine;
+using System;
+using System.Collections.Generic;
+using System.IO;
+
+namespace BansheeEditor
+{
+    /// <summary>
+    /// Displays a list of log messages.
+    /// </summary>
+    [DefaultSize(600, 300)]
+    public class ConsoleWindow : EditorWindow
+    {
+        #region Constants
+        public const string CLEAR_ON_PLAY_KEY = "EditorClearConsoleOnPlay";
+
+        private const int TITLE_HEIGHT = 21;
+        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<ConsoleGUIEntry, ConsoleEntryData> listView;
+        private List<ConsoleEntryData> entries = new List<ConsoleEntryData>();
+        private List<ConsoleEntryData> filteredEntries = new List<ConsoleEntryData>();
+        private EntryFilter filter = EntryFilter.All;
+        private GUITexture detailsSeparator;
+        private GUIScrollArea detailsArea;
+        #endregion
+        #region Private properties
+
+        /// <summary>
+        /// Returns the height of the list area.
+        /// </summary>
+        private int ListHeight
+        {
+            get { return Height - TITLE_HEIGHT; }
+        }
+        #endregion
+        #region Public methods
+
+        /// <summary>
+        /// Rebuilds the list of all entires in the console.
+        /// </summary>
+        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
+
+        /// <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();
+            GUILayoutX titleLayout = layout.AddLayoutX();
+
+            GUIContentImages infoImages = new GUIContentImages(
+                EditorBuiltin.GetLogIcon(LogIcon.Info, 16, false),
+                EditorBuiltin.GetLogIcon(LogIcon.Info, 16, true));
+
+            GUIContentImages warningImages = new GUIContentImages(
+                EditorBuiltin.GetLogIcon(LogIcon.Warning, 16, false),
+                EditorBuiltin.GetLogIcon(LogIcon.Warning, 16, true));
+
+            GUIContentImages errorImages = new GUIContentImages(
+                EditorBuiltin.GetLogIcon(LogIcon.Error, 16, false),
+                EditorBuiltin.GetLogIcon(LogIcon.Error, 16, true));
+
+            GUIToggle infoBtn = new GUIToggle(new GUIContent(infoImages), EditorStyles.Button, GUIOption.FixedHeight(25));
+            GUIToggle warningBtn = new GUIToggle(new GUIContent(warningImages), EditorStyles.Button, GUIOption.FixedHeight(25));
+            GUIToggle errorBtn = new GUIToggle(new GUIContent(errorImages), EditorStyles.Button, GUIOption.FixedHeight(25));
+
+            GUIToggle detailsBtn = new GUIToggle(new LocEdString("Show details"), EditorStyles.Button, GUIOption.FixedHeight(25));
+            GUIButton clearBtn = new GUIButton(new LocEdString("Clear"), GUIOption.FixedHeight(25));
+            GUIToggle clearOnPlayBtn = new GUIToggle(new LocEdString("Clear on play"), EditorStyles.Button, GUIOption.FixedHeight(25));
+
+            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<ConsoleGUIEntry, ConsoleEntryData>(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;
+        }
+
+        /// <inheritdoc/>
+        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);
+        }
+
+        /// <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)
+        {
+            // Check if compiler message, otherwise parse it normally
+            ParsedLogEntry logEntry = ScriptCodeManager.ParseCompilerMessage(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);
+            }
+        }
+
+        /// <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;
+
+            this.filter = filter;
+
+            listView.Clear();
+            filteredEntries.Clear();
+            foreach (var entry in entries)
+            {
+                if (DoesFilterMatch(entry.type))
+                {
+                    listView.AddEntry(entry);
+                    filteredEntries.Add(entry);
+                }
+            }
+
+            sSelectedElementIdx = -1;
+        }
+
+        /// <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:
+                case DebugMessageType.CompilerWarning:
+                    return filter.HasFlag(EntryFilter.Warning);
+                case DebugMessageType.Error:
+                case DebugMessageType.CompilerError:
+                    return filter.HasFlag(EntryFilter.Error);
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Removes all entries from the debug log.
+        /// </summary>
+        private void ClearLog()
+        {
+            Debug.Clear();
+
+            ClearEntries();
+        }
+
+        /// <summary>
+        /// Removes all entries from the console.
+        /// </summary>
+        private void ClearEntries()
+        {
+            listView.Clear();
+            entries.Clear();
+            filteredEntries.Clear();
+
+            sSelectedElementIdx = -1;
+            RefreshDetailsPanel();
+        }
+
+        /// <summary>
+        /// Shows or hides the details panel.
+        /// </summary>
+        /// <param name="show">True to show, false to hide.</param>
+        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);
+        }
+
+        /// <summary>
+        /// Toggles should the console be cleared when the play mode is entered.
+        /// </summary>
+        /// <param name="clear">True if the console should be cleared upon entering the play mode.</param>
+        private void ToggleClearOnPlay(bool clear)
+        {
+            EditorSettings.SetBool(CLEAR_ON_PLAY_KEY, clear);
+        }
+
+        /// <summary>
+        /// Updates the contents of the details panel according to the currently selected element.
+        /// </summary>
+        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);
+                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);
+                        };
+                    }
+                }
+            }
+            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
+        /// <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
+        }
+
+        /// <summary>
+        /// Contains data for a single entry in the console.
+        /// </summary>
+        private class ConsoleEntryData : GUIListViewData
+        {
+            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 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.LightGray;
+            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;
+
+            /// <inheritdoc/>
+            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;
+            }
+
+            /// <inheritdoc/>
+            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.GetLogIcon(LogIcon.Info, 32, false));
+                        break;
+                    case DebugMessageType.Warning:
+                    case DebugMessageType.CompilerWarning:
+                        icon.SetTexture(EditorBuiltin.GetLogIcon(LogIcon.Warning, 32, false));
+                        break;
+                    case DebugMessageType.Error:
+                    case DebugMessageType.CompilerError:
+                        icon.SetTexture(EditorBuiltin.GetLogIcon(LogIcon.Error, 32, false));
+                        break;
+                }
+
+                messageLabel.SetContent(new LocEdString(data.message));
+
+                string method = "";
+                if (data.callstack != null && data.callstack.Length > 0)
+                {
+                    file = Path.GetFileName(data.callstack[0].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;
+            }
+
+            /// <summary>
+            /// Triggered when the entry is selected.
+            /// </summary>
+            private void OnClicked()
+            {
+                sSelectedElementIdx = entryIdx;
+
+                ConsoleWindow window = GetWindow<ConsoleWindow>();
+                window.RefreshDetailsPanel();
+
+                RefreshEntries();
+            }
+
+            /// <summary>
+            /// Triggered when the entry is double-clicked.
+            /// </summary>
+            private void OnDoubleClicked()
+            {
+                if(!string.IsNullOrEmpty(file))
+                    CodeEditor.OpenFile(file, line);
+            }
+        }
+
+        #endregion
+    }
+}

+ 23 - 0
MBansheeEditor/EditorApplication.cs

@@ -104,6 +104,17 @@ namespace BansheeEditor
 
                 if (!value)
                     Selection.SceneObject = null;
+                else
+                {
+                    if (EditorSettings.GetBool(ConsoleWindow.CLEAR_ON_PLAY_KEY, true))
+                    {
+                        Debug.Clear();
+
+                        ConsoleWindow console = EditorWindow.GetWindow<ConsoleWindow>();
+                        if (console != null)
+                            console.Refresh();
+                    }
+                }
 
                 Internal_SetIsPlaying(value);
             }
@@ -654,6 +665,18 @@ namespace BansheeEditor
         /// </summary>
         public static void FrameStep()
         {
+            if (IsStopped)
+            {
+                if (EditorSettings.GetBool(ConsoleWindow.CLEAR_ON_PLAY_KEY, true))
+                {
+                    Debug.Clear();
+
+                    ConsoleWindow console = EditorWindow.GetWindow<ConsoleWindow>();
+                    if (console != null)
+                        console.Refresh();
+                }
+            }
+
             ToggleToolbarItem("Play", false);
             ToggleToolbarItem("Pause", true);
             Internal_FrameStep();

+ 212 - 205
MBansheeEditor/ScriptCodeManager.cs

@@ -1,205 +1,212 @@
-using System.IO;
-using System.Text;
-using System.Text.RegularExpressions;
-using BansheeEngine;
-
-namespace BansheeEditor
-{
-    /// <summary>
-    /// Handles various operations related to script code in the active project, like compilation and code editor syncing.
-    /// </summary>
-    public sealed class ScriptCodeManager
-    {
-        private bool isGameAssemblyDirty;
-        private bool isEditorAssemblyDirty;
-        private CompilerInstance compilerInstance;
-
-        /// <summary>
-        /// Constructs a new script code manager.
-        /// </summary>
-        internal ScriptCodeManager()
-        {
-            ProjectLibrary.OnEntryAdded += OnEntryAdded;
-            ProjectLibrary.OnEntryRemoved += OnEntryRemoved;
-            ProjectLibrary.OnEntryImported += OnEntryImported;
-        }
-
-        /// <summary>
-        /// Triggers required compilation or code editor syncing if needed.
-        /// </summary>
-        internal void Update()
-        {
-            if (CodeEditor.IsSolutionDirty)
-                CodeEditor.SyncSolution();
-
-            if (EditorApplication.IsStopped)
-            {
-                if (compilerInstance == null)
-                {
-                    string outputDir = EditorApplication.ScriptAssemblyPath;
-
-                    if (isGameAssemblyDirty)
-                    {
-                        compilerInstance = ScriptCompiler.CompileAsync(
-                            ScriptAssemblyType.Game, BuildManager.ActivePlatform, true, outputDir);
-
-                        EditorApplication.SetStatusCompiling(true);
-                        isGameAssemblyDirty = false;
-                    }
-                    else if (isEditorAssemblyDirty)
-                    {
-                        compilerInstance = ScriptCompiler.CompileAsync(
-                            ScriptAssemblyType.Editor, BuildManager.ActivePlatform, true, outputDir);
-
-                        EditorApplication.SetStatusCompiling(true);
-                        isEditorAssemblyDirty = false;
-                    }
-                }
-                else
-                {
-                    if (compilerInstance.IsDone)
-                    {
-                        if (compilerInstance.HasErrors)
-                        {
-                            foreach (var msg in compilerInstance.WarningMessages)
-                                Debug.LogError(FormMessage(msg));
-
-                            foreach (var msg in compilerInstance.ErrorMessages)
-                                Debug.LogError(FormMessage(msg));
-                        }
-
-                        compilerInstance.Dispose();
-                        compilerInstance = null;
-
-                        EditorApplication.SetStatusCompiling(false);
-                        EditorApplication.ReloadAssemblies();
-                    }
-                }
-            }
-        }
-
-        /// <summary>
-        /// Triggered when a new resource is added to the project library.
-        /// </summary>
-        /// <param name="path">Path of the added resource, relative to the project's resource folder.</param>
-        private void OnEntryAdded(string path)
-        {
-            if (IsCodeEditorFile(path))
-                CodeEditor.MarkSolutionDirty();
-        }
-
-        /// <summary>
-        /// Triggered when a resource is removed from the project library.
-        /// </summary>
-        /// <param name="path">Path of the removed resource, relative to the project's resource folder.</param>
-        private void OnEntryRemoved(string path)
-        {
-            if (IsCodeEditorFile(path))
-                CodeEditor.MarkSolutionDirty();
-        }
-
-        /// <summary>
-        /// Triggered when a resource is (re)imported in the project library.
-        /// </summary>
-        /// <param name="path">Path of the imported resource, relative to the project's resource folder.</param>
-        private void OnEntryImported(string path)
-        {
-            LibraryEntry entry = ProjectLibrary.GetEntry(path);
-            if (entry == null || entry.Type != LibraryEntryType.File)
-                return;
-
-            FileEntry fileEntry = (FileEntry)entry;
-            if (fileEntry.ResType != ResourceType.ScriptCode)
-                return;
-
-            ScriptCode codeFile = ProjectLibrary.Load<ScriptCode>(path);
-            if(codeFile == null)
-                return;
-
-            if(codeFile.EditorScript)
-                isEditorAssemblyDirty = true;
-            else
-                isGameAssemblyDirty = true;
-        }
-
-        /// <summary>
-        /// Checks is the resource at the provided path a file relevant to the code editor.
-        /// </summary>
-        /// <param name="path">Path to the resource, absolute or relative to the project's resources folder.</param>
-        /// <returns>True if the file is relevant to the code editor, false otherwise.</returns>
-        private bool IsCodeEditorFile(string path)
-        {
-            LibraryEntry entry = ProjectLibrary.GetEntry(path);
-            if (entry != null && entry.Type == LibraryEntryType.File)
-            {
-                FileEntry fileEntry = (FileEntry)entry;
-
-                foreach (var codeType in CodeEditor.CodeTypes)
-                {
-                    if (fileEntry.ResType == codeType)
-                        return true;
-                }
-            }
-
-            return false;
-        }
-
-        /// <summary>
-        /// Converts data reported by the compiler into a readable string.
-        /// </summary>
-        /// <param name="msg">Message data as reported by the compiler.</param>
-        /// <returns>Readable message string.</returns>
-        private string FormMessage(CompilerMessage msg)
-        {
-            StringBuilder sb = new StringBuilder();
-
-            if (msg.type == CompilerMessageType.Error)
-                sb.AppendLine("Compiler error: " + msg.message);
-            else
-                sb.AppendLine("Compiler warning: " + msg.message);
-
-            sb.AppendLine("\tin " + msg.file + "[" + msg.line + ":" + msg.column + "]");
-
-            return sb.ToString();
-        }
-
-        /// <summary>
-        /// Parses a log message and outputs a data object with a separate message and callstack entries. If the message
-        /// is not a valid compiler message null is returned.
-        /// </summary>
-        /// <param name="message">Message to parse.</param>
-        /// <returns>Parsed log message or null if not a valid compiler message.</returns>
-        public static ParsedLogEntry ParseCompilerMessage(string message)
-        {
-            // Note: If modifying FormMessage method make sure to update this one as well to match the formattting
-
-            // Check for error
-            Regex regex = new Regex(@"Compiler error: (.*)\n\tin (.*)\[(.*):.*\]");
-            var match = regex.Match(message);
-
-            // Check for warning
-            if (!match.Success)
-            {
-                regex = new Regex(@"Compiler warning: (.*)\n\tin (.*)\[(.*):.*\]");
-                match = regex.Match(message);
-            }
-
-            // No match
-            if (!match.Success)
-                return null;
-
-            ParsedLogEntry entry = new ParsedLogEntry();
-            entry.callstack = new CallStackEntry[1];
-
-            entry.message = match.Groups[1].Value;
-
-            CallStackEntry callstackEntry = new CallStackEntry();
-            callstackEntry.method = "";
-            callstackEntry.file = match.Groups[2].Value;
-            int.TryParse(match.Groups[3].Value, out callstackEntry.line);
-
-            entry.callstack[0] = callstackEntry;
-            return entry;
-        }
-    }
-}
+using System.IO;
+using System.Text;
+using System.Text.RegularExpressions;
+using BansheeEngine;
+
+namespace BansheeEditor
+{
+    /// <summary>
+    /// Handles various operations related to script code in the active project, like compilation and code editor syncing.
+    /// </summary>
+    public sealed class ScriptCodeManager
+    {
+        private bool isGameAssemblyDirty;
+        private bool isEditorAssemblyDirty;
+        private CompilerInstance compilerInstance;
+
+        /// <summary>
+        /// Constructs a new script code manager.
+        /// </summary>
+        internal ScriptCodeManager()
+        {
+            ProjectLibrary.OnEntryAdded += OnEntryAdded;
+            ProjectLibrary.OnEntryRemoved += OnEntryRemoved;
+            ProjectLibrary.OnEntryImported += OnEntryImported;
+        }
+
+        /// <summary>
+        /// Triggers required compilation or code editor syncing if needed.
+        /// </summary>
+        internal void Update()
+        {
+            if (CodeEditor.IsSolutionDirty)
+                CodeEditor.SyncSolution();
+
+            if (EditorApplication.IsStopped)
+            {
+                if (compilerInstance == null)
+                {
+                    string outputDir = EditorApplication.ScriptAssemblyPath;
+
+                    if (isGameAssemblyDirty)
+                    {
+                        compilerInstance = ScriptCompiler.CompileAsync(
+                            ScriptAssemblyType.Game, BuildManager.ActivePlatform, true, outputDir);
+
+                        EditorApplication.SetStatusCompiling(true);
+                        isGameAssemblyDirty = false;
+                    }
+                    else if (isEditorAssemblyDirty)
+                    {
+                        compilerInstance = ScriptCompiler.CompileAsync(
+                            ScriptAssemblyType.Editor, BuildManager.ActivePlatform, true, outputDir);
+
+                        EditorApplication.SetStatusCompiling(true);
+                        isEditorAssemblyDirty = false;
+                    }
+                }
+                else
+                {
+                    if (compilerInstance.IsDone)
+                    {
+                        Debug.Clear(DebugMessageType.CompilerWarning);
+                        Debug.Clear(DebugMessageType.CompilerError);
+
+                        ConsoleWindow window = EditorWindow.GetWindow<ConsoleWindow>();
+                        if (window != null)
+                            window.Refresh();
+
+                        if (compilerInstance.HasErrors)
+                        {
+                            foreach (var msg in compilerInstance.WarningMessages)
+                                Debug.LogMessage(FormMessage(msg), DebugMessageType.CompilerWarning);
+
+                            foreach (var msg in compilerInstance.ErrorMessages)
+                                Debug.LogMessage(FormMessage(msg), DebugMessageType.CompilerError);
+                        }
+
+                        compilerInstance.Dispose();
+                        compilerInstance = null;
+
+                        EditorApplication.SetStatusCompiling(false);
+                        EditorApplication.ReloadAssemblies();
+                    }
+                }
+            }
+        }
+
+        /// <summary>
+        /// Triggered when a new resource is added to the project library.
+        /// </summary>
+        /// <param name="path">Path of the added resource, relative to the project's resource folder.</param>
+        private void OnEntryAdded(string path)
+        {
+            if (IsCodeEditorFile(path))
+                CodeEditor.MarkSolutionDirty();
+        }
+
+        /// <summary>
+        /// Triggered when a resource is removed from the project library.
+        /// </summary>
+        /// <param name="path">Path of the removed resource, relative to the project's resource folder.</param>
+        private void OnEntryRemoved(string path)
+        {
+            if (IsCodeEditorFile(path))
+                CodeEditor.MarkSolutionDirty();
+        }
+
+        /// <summary>
+        /// Triggered when a resource is (re)imported in the project library.
+        /// </summary>
+        /// <param name="path">Path of the imported resource, relative to the project's resource folder.</param>
+        private void OnEntryImported(string path)
+        {
+            LibraryEntry entry = ProjectLibrary.GetEntry(path);
+            if (entry == null || entry.Type != LibraryEntryType.File)
+                return;
+
+            FileEntry fileEntry = (FileEntry)entry;
+            if (fileEntry.ResType != ResourceType.ScriptCode)
+                return;
+
+            ScriptCode codeFile = ProjectLibrary.Load<ScriptCode>(path);
+            if(codeFile == null)
+                return;
+
+            if(codeFile.EditorScript)
+                isEditorAssemblyDirty = true;
+            else
+                isGameAssemblyDirty = true;
+        }
+
+        /// <summary>
+        /// Checks is the resource at the provided path a file relevant to the code editor.
+        /// </summary>
+        /// <param name="path">Path to the resource, absolute or relative to the project's resources folder.</param>
+        /// <returns>True if the file is relevant to the code editor, false otherwise.</returns>
+        private bool IsCodeEditorFile(string path)
+        {
+            LibraryEntry entry = ProjectLibrary.GetEntry(path);
+            if (entry != null && entry.Type == LibraryEntryType.File)
+            {
+                FileEntry fileEntry = (FileEntry)entry;
+
+                foreach (var codeType in CodeEditor.CodeTypes)
+                {
+                    if (fileEntry.ResType == codeType)
+                        return true;
+                }
+            }
+
+            return false;
+        }
+
+        /// <summary>
+        /// Converts data reported by the compiler into a readable string.
+        /// </summary>
+        /// <param name="msg">Message data as reported by the compiler.</param>
+        /// <returns>Readable message string.</returns>
+        private string FormMessage(CompilerMessage msg)
+        {
+            StringBuilder sb = new StringBuilder();
+
+            if (msg.type == CompilerMessageType.Error)
+                sb.AppendLine("Compiler error: " + msg.message);
+            else
+                sb.AppendLine("Compiler warning: " + msg.message);
+
+            sb.AppendLine("\tin " + msg.file + "[" + msg.line + ":" + msg.column + "]");
+
+            return sb.ToString();
+        }
+
+        /// <summary>
+        /// Parses a log message and outputs a data object with a separate message and callstack entries. If the message
+        /// is not a valid compiler message null is returned.
+        /// </summary>
+        /// <param name="message">Message to parse.</param>
+        /// <returns>Parsed log message or null if not a valid compiler message.</returns>
+        public static ParsedLogEntry ParseCompilerMessage(string message)
+        {
+            // Note: If modifying FormMessage method make sure to update this one as well to match the formattting
+
+            // Check for error
+            Regex regex = new Regex(@"Compiler error: (.*)\n\tin (.*)\[(.*):.*\]");
+            var match = regex.Match(message);
+
+            // Check for warning
+            if (!match.Success)
+            {
+                regex = new Regex(@"Compiler warning: (.*)\n\tin (.*)\[(.*):.*\]");
+                match = regex.Match(message);
+            }
+
+            // No match
+            if (!match.Success)
+                return null;
+
+            ParsedLogEntry entry = new ParsedLogEntry();
+            entry.callstack = new CallStackEntry[1];
+
+            entry.message = match.Groups[1].Value;
+
+            CallStackEntry callstackEntry = new CallStackEntry();
+            callstackEntry.method = "";
+            callstackEntry.file = match.Groups[2].Value;
+            int.TryParse(match.Groups[3].Value, out callstackEntry.line);
+
+            entry.callstack[0] = callstackEntry;
+            return entry;
+        }
+    }
+}

+ 28 - 1
MBansheeEngine/Debug.cs

@@ -14,7 +14,7 @@ namespace BansheeEngine
     /// </summary>
     public enum DebugMessageType // Note: Must match C++ enum DebugChannel
     {
-        Info, Warning, Error
+        Info, Warning, Error, CompilerWarning, CompilerError
     }
 
     /// <summary>
@@ -104,6 +104,19 @@ namespace BansheeEngine
             Internal_LogError(sb.ToString());
         }
 
+        /// <summary>
+        /// Logs a new message to the global debug log using the provided type.
+        /// </summary>
+        /// <param name="message">Message to log.</param>
+        /// <param name="type">Type of the message to log.</param>
+        internal static void LogMessage(object message, DebugMessageType type)
+        {
+            StringBuilder sb = new StringBuilder();
+            sb.AppendLine(message.ToString());
+
+            Internal_LogMessage(sb.ToString(), type);
+        }
+
         /// <summary>
         /// Clears all messages from the debug log.
         /// </summary>
@@ -112,6 +125,14 @@ namespace BansheeEngine
             Internal_Clear();
         }
 
+        /// <summary>
+        /// Clears all messages of the specified type from the debug log.
+        /// </summary>
+        internal static void Clear(DebugMessageType type)
+        {
+            Internal_ClearType(type);
+        }
+
         /// <summary>
         /// Returns the stack trace of the current point in code.
         /// </summary>
@@ -231,9 +252,15 @@ namespace BansheeEngine
         [MethodImpl(MethodImplOptions.InternalCall)]
         internal static extern void Internal_LogError(string message);
 
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        internal static extern void Internal_LogMessage(string message, DebugMessageType type);
+
         [MethodImpl(MethodImplOptions.InternalCall)]
         internal static extern void Internal_Clear();
 
+        [MethodImpl(MethodImplOptions.InternalCall)]
+        internal static extern void Internal_ClearType(DebugMessageType type);
+
         [MethodImpl(MethodImplOptions.InternalCall)]
         internal static extern LogEntry[] Internal_GetMessages();
     }

+ 49 - 47
SBansheeEngine/Include/BsScriptDebug.h

@@ -1,48 +1,50 @@
-#pragma once
-
-#include "BsScriptEnginePrerequisites.h"
-#include "BsScriptObject.h"
-
-namespace BansheeEngine
-{
-	/**
-	 * @brief	Interop class between C++ & CLR for Debug.
-	 */
-	class BS_SCR_BE_EXPORT ScriptDebug : public ScriptObject<ScriptDebug>
-	{
-	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);
-		static void internal_clear();
-		static MonoArray* internal_getMessages();
-
-		typedef void(__stdcall *OnAddedThunkDef) (UINT32, MonoString*, MonoException**);
-
-		static OnAddedThunkDef onAddedThunk;
-	};
+#pragma once
+
+#include "BsScriptEnginePrerequisites.h"
+#include "BsScriptObject.h"
+
+namespace BansheeEngine
+{
+	/**
+	 * @brief	Interop class between C++ & CLR for Debug.
+	 */
+	class BS_SCR_BE_EXPORT ScriptDebug : public ScriptObject<ScriptDebug>
+	{
+	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);
+		static void internal_logMessage(MonoString* message, UINT32 type);
+		static void internal_clear();
+		static void internal_clearType(UINT32 type);
+		static MonoArray* internal_getMessages();
+
+		typedef void(__stdcall *OnAddedThunkDef) (UINT32, MonoString*, MonoException**);
+
+		static OnAddedThunkDef onAddedThunk;
+	};
 }

+ 102 - 91
SBansheeEngine/Source/BsScriptDebug.cpp

@@ -1,92 +1,103 @@
-#include "BsScriptDebug.h"
-#include "BsMonoManager.h"
-#include "BsMonoClass.h"
-#include "BsMonoMethod.h"
-#include "BsMonoUtil.h"
-#include "BsDebug.h"
-#include "BsScriptLogEntry.h"
-
-namespace BansheeEngine
-{
-	HEvent ScriptDebug::mOnLogEntryAddedConn;
-	ScriptDebug::OnAddedThunkDef ScriptDebug::onAddedThunk = nullptr;
-
-	/**
-	 * @brief	C++ version of the managed LogEntry structure.
-	 */
-	struct ScriptLogEntryData
-	{
-		UINT32 type;
-		MonoString* message;
-	};
-
-	ScriptDebug::ScriptDebug(MonoObject* instance)
-		:ScriptObject(instance)
-	{ }
-
-	void ScriptDebug::initRuntimeData()
-	{
-		metaData.scriptClass->addInternalCall("Internal_Log", &ScriptDebug::internal_log);
-		metaData.scriptClass->addInternalCall("Internal_LogWarning", &ScriptDebug::internal_logWarning);
-		metaData.scriptClass->addInternalCall("Internal_LogError", &ScriptDebug::internal_logError);
-		metaData.scriptClass->addInternalCall("Internal_LogError", &ScriptDebug::internal_logError);
-		metaData.scriptClass->addInternalCall("Internal_Clear", &ScriptDebug::internal_clear);
-		metaData.scriptClass->addInternalCall("Internal_GetMessages", &ScriptDebug::internal_getMessages);
-
-		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(entry.getMessage());
-
-		MonoUtil::invokeThunk(onAddedThunk, entry.getChannel(), message);
-	}
-
-	void ScriptDebug::internal_log(MonoString* message)
-	{
-		gDebug().logDebug(MonoUtil::monoToString(message));
-	}
-
-	void ScriptDebug::internal_logWarning(MonoString* message)
-	{
-		gDebug().logWarning(MonoUtil::monoToString(message));
-	}
-
-	void ScriptDebug::internal_logError(MonoString* message)
-	{
-		gDebug().logError(MonoUtil::monoToString(message));
-	}
-
-	void ScriptDebug::internal_clear()
-	{
-		gDebug().getLog().clear();
-	}
-
-	MonoArray* ScriptDebug::internal_getMessages()
-	{
-		Vector<LogEntry> entries = gDebug().getLog().getEntries();
-
-		UINT32 numEntries = (UINT32)entries.size();
-		ScriptArray output = ScriptArray::create<ScriptLogEntry>(numEntries);
-		for (UINT32 i = 0; i < numEntries; i++)
-		{
-			MonoString* message = MonoUtil::stringToMono(entries[i].getMessage());
-
-			ScriptLogEntryData scriptEntry = { entries[i].getChannel(), message };
-			output.set(i, scriptEntry);
-		}
-
-		return output.getInternal();
-	}
+#include "BsScriptDebug.h"
+#include "BsMonoManager.h"
+#include "BsMonoClass.h"
+#include "BsMonoMethod.h"
+#include "BsMonoUtil.h"
+#include "BsDebug.h"
+#include "BsScriptLogEntry.h"
+
+namespace BansheeEngine
+{
+	HEvent ScriptDebug::mOnLogEntryAddedConn;
+	ScriptDebug::OnAddedThunkDef ScriptDebug::onAddedThunk = nullptr;
+
+	/**
+	 * @brief	C++ version of the managed LogEntry structure.
+	 */
+	struct ScriptLogEntryData
+	{
+		UINT32 type;
+		MonoString* message;
+	};
+
+	ScriptDebug::ScriptDebug(MonoObject* instance)
+		:ScriptObject(instance)
+	{ }
+
+	void ScriptDebug::initRuntimeData()
+	{
+		metaData.scriptClass->addInternalCall("Internal_Log", &ScriptDebug::internal_log);
+		metaData.scriptClass->addInternalCall("Internal_LogWarning", &ScriptDebug::internal_logWarning);
+		metaData.scriptClass->addInternalCall("Internal_LogError", &ScriptDebug::internal_logError);
+		metaData.scriptClass->addInternalCall("Internal_LogMessage", &ScriptDebug::internal_logMessage);
+		metaData.scriptClass->addInternalCall("Internal_Clear", &ScriptDebug::internal_clear);
+		metaData.scriptClass->addInternalCall("Internal_ClearType", &ScriptDebug::internal_clearType);
+		metaData.scriptClass->addInternalCall("Internal_GetMessages", &ScriptDebug::internal_getMessages);
+
+		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(entry.getMessage());
+
+		MonoUtil::invokeThunk(onAddedThunk, entry.getChannel(), message);
+	}
+
+	void ScriptDebug::internal_log(MonoString* message)
+	{
+		gDebug().logDebug(MonoUtil::monoToString(message));
+	}
+
+	void ScriptDebug::internal_logWarning(MonoString* message)
+	{
+		gDebug().logWarning(MonoUtil::monoToString(message));
+	}
+
+	void ScriptDebug::internal_logError(MonoString* message)
+	{
+		gDebug().logError(MonoUtil::monoToString(message));
+	}
+
+	void ScriptDebug::internal_logMessage(MonoString* message, UINT32 type)
+	{
+		gDebug().log(MonoUtil::monoToString(message), type);
+	}
+
+	void ScriptDebug::internal_clear()
+	{
+		gDebug().getLog().clear();
+	}
+
+	void ScriptDebug::internal_clearType(UINT32 type)
+	{
+		gDebug().getLog().clear(type);
+	}
+
+	MonoArray* ScriptDebug::internal_getMessages()
+	{
+		Vector<LogEntry> entries = gDebug().getLog().getEntries();
+
+		UINT32 numEntries = (UINT32)entries.size();
+		ScriptArray output = ScriptArray::create<ScriptLogEntry>(numEntries);
+		for (UINT32 i = 0; i < numEntries; i++)
+		{
+			MonoString* message = MonoUtil::stringToMono(entries[i].getMessage());
+
+			ScriptLogEntryData scriptEntry = { entries[i].getChannel(), message };
+			output.set(i, scriptEntry);
+		}
+
+		return output.getInternal();
+	}
 }

+ 3 - 2
TODO.txt

@@ -11,7 +11,6 @@ Other polish:
 
 Optional:
  - Ortographic camera views (+ gizmo in scene view corner that shows camera orientation) (Use custom handles and implement this?)
- - Add "focus on object" key (F) - animate it: rotate camera towards then speed towards while zooming in (+ menu entry)
  - Add tooltips to toolbar items and other buttons with icons
  - Undo/Redo
   - CmdRecordSO records an SO and all its children but it should only record a single SO
@@ -20,17 +19,19 @@ Optional:
   - Add commands for breaking or reverting a scene object 
   - Test & finalize undo/redo system
  - Test VS 2015 Community as code editor (possibly also move the code to VS 2015)
+  - Add drop down in Settings with external code editor
  - Drag and drop of a mesh into Hierarchy doesn't instantiate it
  - Drag and dropping a prefab onto the scene (or hierarchy) should work the same as with meshes
  - Undocking of editor window doesn't work (they just get lost)
  - Start editor in fullscreen
  - If user clears the default shader he has no way of re-assigning it - add default shader to project folder? (needs to be packaged with project)
- - Toggle to enable/disable SceneObject in Inspector
+ - Toggle to enable/disable SceneObject in Inspector (+ Fade out disabled objects in hierarchy)
  - Resource import options don't get saved
  - Cursors should be replaced with better ones, or at least hot-spots fixed
  - Either disable light tool icons before release or make them functional (With gizmos)
 
  More optional:
+ - Gizmo icons for camera & light
  - Add a way to use GUI elements in game window (Default GUI available to all, plus GUIWidget component for custom ones. Make sure skin can be changed for both.)
  - If a field gets optimized out from a material's GPU program it won't get persisted by the inspector (or serialized)
   - (i.e. the field exists in shader desc but not as a GPU variable)