using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
namespace SanAndreasUnity.UI
{
public class Console : PauseMenuWindow
{
#region Inspector Settings
///
/// Whether to only keep a certain number of logs, useful if memory usage is a concern.
///
public bool restrictLogCount = true;
///
/// Number of logs to keep before removing old ones.
///
public int maxLogCount = 1000;
#endregion
static readonly GUIContent clearLabel = new GUIContent("Clear", "Clear the contents of the console.");
static readonly GUIContent collapseLabel = new GUIContent("Collapse", "Hide repeated messages.");
private static readonly LogType[] s_allLogTypes = new LogType[] {
LogType.Log, LogType.Warning, LogType.Error, LogType.Exception, LogType.Assert
};
static readonly Dictionary logTypeColors = new Dictionary
{
{ LogType.Assert, Color.red },
{ LogType.Error, Color.red },
{ LogType.Exception, Color.red },
{ LogType.Log, Color.white },
{ LogType.Warning, Color.yellow },
};
bool isCollapsed;
public bool IsCollapsed { get { return this.isCollapsed; } set { this.isCollapsed = value; } }
readonly List logs = new List();
readonly Utilities.ConcurrentQueue queuedLogs = new Utilities.ConcurrentQueue();
readonly Dictionary logTypeFilters = new Dictionary
{
{ LogType.Assert, true },
{ LogType.Error, true },
{ LogType.Exception, true },
{ LogType.Log, true },
{ LogType.Warning, true },
};
private Dictionary m_numMessagesPerType = new Dictionary ();
Console ()
{
this.windowName = "Console";
this.useScrollView = false;
}
#region MonoBehaviour Messages
void OnDisable()
{
Application.logMessageReceivedThreaded -= HandleLogThreaded;
}
void OnEnable()
{
Application.logMessageReceivedThreaded += HandleLogThreaded;
}
void Start ()
{
this.RegisterButtonInPauseMenu ();
// adjust rect
this.windowRect = Utilities.GUIUtils.GetCenteredRect( new Vector2(550, 400) );
}
void Update()
{
UpdateQueuedLogs();
}
#endregion
static void RestoreContentColor ()
{
GUI.contentColor = Color.white;
}
void DrawCollapsedLog(Log log)
{
GUILayout.BeginHorizontal();
GUILayout.Label(log.GetTruncatedMessage());
GUILayout.FlexibleSpace();
GUILayout.Label(log.count.ToString(), GUI.skin.box);
GUILayout.EndHorizontal();
}
void DrawExpandedLog(Log log)
{
for (var i = 0; i < log.count; i += 1)
{
GUILayout.Label(log.GetTruncatedMessage());
}
}
void DrawLog(Log log)
{
GUI.contentColor = logTypeColors[log.type];
if (isCollapsed)
{
DrawCollapsedLog(log);
}
else
{
DrawExpandedLog(log);
}
}
void DrawLogList()
{
scrollPos = GUILayout.BeginScrollView(scrollPos);
// Used to determine height of accumulated log labels.
GUILayout.BeginVertical();
foreach (LogType logType in s_allLogTypes)
m_numMessagesPerType [logType] = 0;
foreach (Log log in logs)
{
if (IsLogVisible (log))
DrawLog (log);
m_numMessagesPerType [log.type] += (isCollapsed ? 1 : log.count);
}
GUILayout.EndVertical();
var innerScrollRect = GUILayoutUtility.GetLastRect();
GUILayout.EndScrollView();
var outerScrollRect = GUILayoutUtility.GetLastRect();
// If we're scrolled to bottom now, guarantee that it continues to be in next cycle.
if (Event.current.type == EventType.Repaint && IsScrolledToBottom(innerScrollRect, outerScrollRect))
{
ScrollToBottom();
}
// Ensure GUI colour is reset before drawing other components.
GUI.contentColor = Color.white;
}
void DrawToolbar()
{
GUILayout.BeginHorizontal();
foreach (LogType logType in s_allLogTypes)
{
bool currentState = logTypeFilters[logType];
int count = m_numMessagesPerType [logType];
string label = logType.ToString() + ( (count > 0) ? (" [" + count + "]") : "" );
GUI.contentColor = logTypeColors [logType];
logTypeFilters[logType] = GUILayout.Toggle (currentState, label, GUILayout.ExpandWidth(false));
GUILayout.Space(15);
}
GUILayout.EndHorizontal();
RestoreContentColor ();
GUILayout.BeginHorizontal ();
if (GUILayout.Button (clearLabel, GUILayout.ExpandWidth(false)))
{
logs.Clear();
}
GUILayout.Space (10);
isCollapsed = GUILayout.Toggle (isCollapsed, collapseLabel, GUILayout.ExpandWidth(false));
GUILayout.EndHorizontal ();
}
void DrawWindow()
{
DrawLogList();
GUILayout.Space (5);
DrawToolbar();
}
protected override void OnWindowGUI ()
{
DrawWindow ();
}
Log? GetLastLog()
{
if (logs.Count == 0)
{
return null;
}
return logs.Last();
}
void UpdateQueuedLogs()
{
Log log;
while (queuedLogs.TryDequeue(out log))
{
ProcessLogItem(log);
}
}
void HandleLogThreaded(string message, string stackTrace, LogType type)
{
var log = new Log
{
count = 1,
message = message,
stackTrace = stackTrace,
type = type,
};
// Queue the log into a ConcurrentQueue to be processed later in the Unity main thread,
// so that we don't get GUI-related errors for logs coming from other threads
queuedLogs.Enqueue(log);
}
void ProcessLogItem(Log log)
{
var lastLog = GetLastLog();
var isDuplicateOfLastLog = lastLog.HasValue && log.Equals(lastLog.Value);
if (isDuplicateOfLastLog)
{
// Replace previous log with incremented count instead of adding a new one.
log.count = lastLog.Value.count + 1;
logs[logs.Count - 1] = log;
}
else
{
logs.Add(log);
TrimExcessLogs();
}
}
bool IsLogVisible(Log log)
{
return logTypeFilters[log.type];
}
bool IsScrolledToBottom(Rect innerScrollRect, Rect outerScrollRect)
{
var innerScrollHeight = innerScrollRect.height;
// Take into account extra padding added to the scroll container.
var outerScrollHeight = outerScrollRect.height - GUI.skin.box.padding.vertical;
// If contents of scroll view haven't exceeded outer container, treat it as scrolled to bottom.
if (outerScrollHeight > innerScrollHeight)
{
return true;
}
// Scrolled to bottom (with error margin for float math)
return Mathf.Approximately(innerScrollHeight, scrollPos.y + outerScrollHeight);
}
void ScrollToBottom()
{
scrollPos = new Vector2(0, Int32.MaxValue);
}
void TrimExcessLogs()
{
if (!restrictLogCount)
{
return;
}
var amountToRemove = logs.Count - maxLogCount;
if (amountToRemove <= 0)
{
return;
}
logs.RemoveRange(0, amountToRemove);
}
}
///
/// A basic container for log details.
///
struct Log
{
public int count;
public string message;
public string stackTrace;
public LogType type;
///
/// The max string length supported by UnityEngine.GUILayout.Label without triggering this error:
/// "String too long for TextMeshGenerator. Cutting off characters."
///
private const int MaxMessageLength = 16382;
public bool Equals(Log log)
{
return message == log.message && stackTrace == log.stackTrace && type == log.type;
}
///
/// Return a truncated message if it exceeds the max message length
///
public string GetTruncatedMessage()
{
if (string.IsNullOrEmpty(message)) return message;
return message.Length <= MaxMessageLength ? message : message.Substring(0, MaxMessageLength);
}
}
}