Custom logger
This commit is contained in:
512
Assets/Editor/CustomLogWindow.cs
Normal file
512
Assets/Editor/CustomLogWindow.cs
Normal file
@@ -0,0 +1,512 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
using Core;
|
||||
|
||||
namespace Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Custom dockable log window with advanced filtering capabilities.
|
||||
/// Supports filtering by class, method, log level, time range, and text search.
|
||||
/// Multiple instances can be opened with independent filter states.
|
||||
///
|
||||
/// PERFORMANCE NOTE: For large log counts (10,000+), consider implementing virtualized scrolling.
|
||||
/// Only render visible entries within the scroll view to avoid GUI overhead.
|
||||
/// See DrawLogEntries() method for potential optimization location.
|
||||
/// </summary>
|
||||
public class CustomLogWindow : EditorWindow
|
||||
{
|
||||
#region Window State
|
||||
|
||||
private Vector2 scrollPosition;
|
||||
private readonly List<LogEntry> allLogs = new List<LogEntry>();
|
||||
|
||||
#endregion
|
||||
|
||||
#region Filter State
|
||||
|
||||
private HashSet<string> activeClassTags = new HashSet<string>();
|
||||
private HashSet<string> activeMethodTags = new HashSet<string>();
|
||||
private HashSet<string> selectedClassFilters = new HashSet<string>();
|
||||
private HashSet<string> selectedMethodFilters = new HashSet<string>();
|
||||
private HashSet<LogLevel> selectedLevelFilters = new HashSet<LogLevel>
|
||||
{
|
||||
LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error
|
||||
};
|
||||
private string searchText = "";
|
||||
private bool autoScroll = true;
|
||||
|
||||
// Time range filtering
|
||||
private bool enableTimeFilter = false;
|
||||
private float minTimestamp = 0;
|
||||
private float maxTimestamp = 0;
|
||||
private float currentMaxTimestamp = 0;
|
||||
|
||||
#endregion
|
||||
|
||||
#region UI State
|
||||
|
||||
private bool showClassFilter = true;
|
||||
private bool showMethodFilter = false;
|
||||
private bool showLevelFilter = true;
|
||||
private bool showTimeFilter = false;
|
||||
|
||||
// Colors for log levels
|
||||
private readonly Color debugColor = Color.white;
|
||||
private readonly Color infoColor = Color.white;
|
||||
private readonly Color warningColor = Color.yellow;
|
||||
private readonly Color errorColor = new Color(1f, 0.3f, 0.3f);
|
||||
|
||||
// Alternating row background colors
|
||||
private readonly Color evenRowColor = new Color(0.22f, 0.22f, 0.22f);
|
||||
private readonly Color oddRowColor = new Color(0.26f, 0.26f, 0.26f);
|
||||
|
||||
#endregion
|
||||
|
||||
#region Menu Items
|
||||
|
||||
[MenuItem("AppleHills/Custom Log Console")]
|
||||
public static void ShowWindow()
|
||||
{
|
||||
var window = GetWindow<CustomLogWindow>("Custom Log");
|
||||
window.Show();
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Unity Lifecycle
|
||||
|
||||
private void OnEnable()
|
||||
{
|
||||
// Subscribe to new log events
|
||||
Logging.OnLogEntryAdded += OnLogAdded;
|
||||
|
||||
// Load existing logs
|
||||
allLogs.AddRange(Logging.GetRecentLogs());
|
||||
|
||||
// Build initial tag lists
|
||||
RebuildTagLists();
|
||||
|
||||
// Initialize time range
|
||||
UpdateTimeRange();
|
||||
}
|
||||
|
||||
private void OnDisable()
|
||||
{
|
||||
Logging.OnLogEntryAdded -= OnLogAdded;
|
||||
}
|
||||
|
||||
private void OnLogAdded(LogEntry entry)
|
||||
{
|
||||
allLogs.Add(entry);
|
||||
|
||||
// Update active tags
|
||||
activeClassTags.Add(entry.ClassName);
|
||||
activeMethodTags.Add(entry.MethodName);
|
||||
|
||||
// Update time range
|
||||
UpdateTimeRange();
|
||||
|
||||
if (autoScroll)
|
||||
scrollPosition.y = float.MaxValue;
|
||||
|
||||
Repaint(); // Redraw window
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region GUI Drawing
|
||||
|
||||
private void OnGUI()
|
||||
{
|
||||
DrawToolbar();
|
||||
DrawFilters();
|
||||
DrawLogEntries();
|
||||
}
|
||||
|
||||
private void DrawToolbar()
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal(EditorStyles.toolbar);
|
||||
|
||||
// Clear button
|
||||
if (GUILayout.Button("Clear", EditorStyles.toolbarButton, GUILayout.Width(50)))
|
||||
{
|
||||
allLogs.Clear();
|
||||
activeClassTags.Clear();
|
||||
activeMethodTags.Clear();
|
||||
Logging.ClearLogs();
|
||||
Repaint();
|
||||
}
|
||||
|
||||
// Auto-scroll toggle
|
||||
autoScroll = GUILayout.Toggle(autoScroll, "Auto-scroll", EditorStyles.toolbarButton, GUILayout.Width(80));
|
||||
|
||||
GUILayout.FlexibleSpace();
|
||||
|
||||
// Log count
|
||||
var filteredCount = GetFilteredLogs().Count();
|
||||
GUILayout.Label($"{filteredCount} / {allLogs.Count} logs", GUILayout.Width(100));
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
// Export button
|
||||
if (GUILayout.Button("Export", EditorStyles.toolbarButton, GUILayout.Width(60)))
|
||||
{
|
||||
ExportLogs();
|
||||
}
|
||||
|
||||
GUILayout.Space(10);
|
||||
|
||||
// Search box
|
||||
GUILayout.Label("Search:", GUILayout.Width(50));
|
||||
searchText = GUILayout.TextField(searchText, EditorStyles.toolbarSearchField, GUILayout.Width(200));
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
}
|
||||
|
||||
private void DrawFilters()
|
||||
{
|
||||
EditorGUILayout.BeginVertical(EditorStyles.helpBox);
|
||||
|
||||
// Class Filters
|
||||
showClassFilter = EditorGUILayout.Foldout(showClassFilter, $"Class Filters ({selectedClassFilters.Count} selected)", true);
|
||||
if (showClassFilter)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("All", GUILayout.Width(50)))
|
||||
{
|
||||
selectedClassFilters = new HashSet<string>(activeClassTags);
|
||||
}
|
||||
if (GUILayout.Button("None", GUILayout.Width(50)))
|
||||
{
|
||||
selectedClassFilters.Clear();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.BeginVertical();
|
||||
foreach (var tag in activeClassTags.OrderBy(t => t))
|
||||
{
|
||||
bool isSelected = selectedClassFilters.Contains(tag);
|
||||
bool newSelection = EditorGUILayout.ToggleLeft(tag, isSelected);
|
||||
|
||||
if (newSelection && !isSelected)
|
||||
selectedClassFilters.Add(tag);
|
||||
else if (!newSelection && isSelected)
|
||||
selectedClassFilters.Remove(tag);
|
||||
}
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
// Method Filters
|
||||
showMethodFilter = EditorGUILayout.Foldout(showMethodFilter, $"Method Filters ({selectedMethodFilters.Count} selected)", true);
|
||||
if (showMethodFilter)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
if (GUILayout.Button("All", GUILayout.Width(50)))
|
||||
{
|
||||
selectedMethodFilters = new HashSet<string>(activeMethodTags);
|
||||
}
|
||||
if (GUILayout.Button("None", GUILayout.Width(50)))
|
||||
{
|
||||
selectedMethodFilters.Clear();
|
||||
}
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.BeginVertical();
|
||||
foreach (var tag in activeMethodTags.OrderBy(t => t))
|
||||
{
|
||||
bool isSelected = selectedMethodFilters.Contains(tag);
|
||||
bool newSelection = EditorGUILayout.ToggleLeft(tag, isSelected);
|
||||
|
||||
if (newSelection && !isSelected)
|
||||
selectedMethodFilters.Add(tag);
|
||||
else if (!newSelection && isSelected)
|
||||
selectedMethodFilters.Remove(tag);
|
||||
}
|
||||
EditorGUILayout.EndVertical();
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
// Log Level Filters
|
||||
showLevelFilter = EditorGUILayout.Foldout(showLevelFilter, "Log Level Filters", true);
|
||||
if (showLevelFilter)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
foreach (LogLevel level in Enum.GetValues(typeof(LogLevel)))
|
||||
{
|
||||
bool isSelected = selectedLevelFilters.Contains(level);
|
||||
bool newSelection = EditorGUILayout.ToggleLeft(level.ToString(), isSelected);
|
||||
|
||||
if (newSelection && !isSelected)
|
||||
selectedLevelFilters.Add(level);
|
||||
else if (!newSelection && isSelected)
|
||||
selectedLevelFilters.Remove(level);
|
||||
}
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUILayout.Space(5);
|
||||
|
||||
// Time Range Filter
|
||||
showTimeFilter = EditorGUILayout.Foldout(showTimeFilter, "Time Range Filter", true);
|
||||
if (showTimeFilter)
|
||||
{
|
||||
EditorGUI.indentLevel++;
|
||||
|
||||
enableTimeFilter = EditorGUILayout.Toggle("Enable Time Filter", enableTimeFilter);
|
||||
|
||||
if (enableTimeFilter && currentMaxTimestamp > 0)
|
||||
{
|
||||
EditorGUILayout.BeginHorizontal();
|
||||
EditorGUILayout.LabelField($"Min: {minTimestamp:F2}s", GUILayout.Width(100));
|
||||
EditorGUILayout.LabelField($"Max: {maxTimestamp:F2}s", GUILayout.Width(100));
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
EditorGUILayout.MinMaxSlider(
|
||||
ref minTimestamp,
|
||||
ref maxTimestamp,
|
||||
0,
|
||||
currentMaxTimestamp);
|
||||
}
|
||||
|
||||
EditorGUI.indentLevel--;
|
||||
}
|
||||
|
||||
EditorGUILayout.EndVertical();
|
||||
}
|
||||
|
||||
private void DrawLogEntries()
|
||||
{
|
||||
// PERFORMANCE NOTE: For 10,000+ logs, consider virtualized scrolling here.
|
||||
// Only render entries visible within the scroll view rect.
|
||||
// Current implementation renders all filtered logs which may cause GUI slowdown.
|
||||
|
||||
scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition);
|
||||
|
||||
var filteredLogs = GetFilteredLogs().ToList();
|
||||
|
||||
for (int i = 0; i < filteredLogs.Count; i++)
|
||||
{
|
||||
DrawLogEntry(filteredLogs[i], i);
|
||||
}
|
||||
|
||||
EditorGUILayout.EndScrollView();
|
||||
}
|
||||
|
||||
private void DrawLogEntry(LogEntry entry, int index)
|
||||
{
|
||||
// Draw alternating row background
|
||||
Color bgColor = (index % 2 == 0) ? evenRowColor : oddRowColor;
|
||||
Rect rowRect = EditorGUILayout.BeginHorizontal();
|
||||
EditorGUI.DrawRect(rowRect, bgColor);
|
||||
|
||||
// Color-code by log level
|
||||
Color color = GetColorForLevel(entry.Level);
|
||||
var originalColor = GUI.contentColor;
|
||||
GUI.contentColor = color;
|
||||
|
||||
// Timestamp
|
||||
GUILayout.Label($"[{entry.Timestamp:F2}s]", GUILayout.Width(80));
|
||||
|
||||
// Level
|
||||
GUILayout.Label($"[{entry.Level}]", GUILayout.Width(70));
|
||||
|
||||
// Class
|
||||
GUILayout.Label($"[{entry.ClassName}]", GUILayout.Width(150));
|
||||
|
||||
// Method
|
||||
GUILayout.Label($"[{entry.MethodName}]", GUILayout.Width(150));
|
||||
|
||||
// Message
|
||||
GUILayout.Label(entry.Message, GUILayout.ExpandWidth(true));
|
||||
|
||||
EditorGUILayout.EndHorizontal();
|
||||
|
||||
GUI.contentColor = originalColor;
|
||||
|
||||
// Right-click context menu
|
||||
if (Event.current.type == EventType.ContextClick && rowRect.Contains(Event.current.mousePosition))
|
||||
{
|
||||
GenericMenu menu = new GenericMenu();
|
||||
|
||||
menu.AddItem(new GUIContent("Copy Full Message"), false, () =>
|
||||
{
|
||||
EditorGUIUtility.systemCopyBuffer = entry.FullFormattedMessage;
|
||||
});
|
||||
|
||||
menu.AddItem(new GUIContent("Copy Message Only"), false, () =>
|
||||
{
|
||||
EditorGUIUtility.systemCopyBuffer = entry.Message;
|
||||
});
|
||||
|
||||
menu.AddSeparator("");
|
||||
|
||||
menu.AddItem(new GUIContent("Filter to this Class"), false, () =>
|
||||
{
|
||||
selectedClassFilters.Clear();
|
||||
selectedClassFilters.Add(entry.ClassName);
|
||||
Repaint();
|
||||
});
|
||||
|
||||
menu.AddItem(new GUIContent("Filter to this Method"), false, () =>
|
||||
{
|
||||
selectedMethodFilters.Clear();
|
||||
selectedMethodFilters.Add(entry.MethodName);
|
||||
Repaint();
|
||||
});
|
||||
|
||||
menu.AddItem(new GUIContent("Filter to this Class + Method"), false, () =>
|
||||
{
|
||||
selectedClassFilters.Clear();
|
||||
selectedClassFilters.Add(entry.ClassName);
|
||||
selectedMethodFilters.Clear();
|
||||
selectedMethodFilters.Add(entry.MethodName);
|
||||
Repaint();
|
||||
});
|
||||
|
||||
menu.AddSeparator("");
|
||||
|
||||
menu.AddItem(new GUIContent("Filter to this Log Level"), false, () =>
|
||||
{
|
||||
selectedLevelFilters.Clear();
|
||||
selectedLevelFilters.Add(entry.Level);
|
||||
Repaint();
|
||||
});
|
||||
|
||||
menu.ShowAsContext();
|
||||
Event.current.Use();
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Filtering Logic
|
||||
|
||||
private IEnumerable<LogEntry> GetFilteredLogs()
|
||||
{
|
||||
return allLogs.Where(entry =>
|
||||
{
|
||||
// Filter by class (if any selected)
|
||||
if (selectedClassFilters.Count > 0 && !selectedClassFilters.Contains(entry.ClassName))
|
||||
return false;
|
||||
|
||||
// Filter by method (if any selected)
|
||||
if (selectedMethodFilters.Count > 0 && !selectedMethodFilters.Contains(entry.MethodName))
|
||||
return false;
|
||||
|
||||
// Filter by log level
|
||||
if (!selectedLevelFilters.Contains(entry.Level))
|
||||
return false;
|
||||
|
||||
// Filter by time range
|
||||
if (enableTimeFilter)
|
||||
{
|
||||
if (entry.Timestamp < minTimestamp || entry.Timestamp > maxTimestamp)
|
||||
return false;
|
||||
}
|
||||
|
||||
// Filter by search text
|
||||
if (!string.IsNullOrEmpty(searchText))
|
||||
{
|
||||
bool matchesSearch =
|
||||
entry.ClassName.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
|
||||
entry.MethodName.Contains(searchText, StringComparison.OrdinalIgnoreCase) ||
|
||||
entry.Message.Contains(searchText, StringComparison.OrdinalIgnoreCase);
|
||||
|
||||
if (!matchesSearch)
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Helper Methods
|
||||
|
||||
private Color GetColorForLevel(LogLevel level)
|
||||
{
|
||||
return level switch
|
||||
{
|
||||
LogLevel.Debug => debugColor,
|
||||
LogLevel.Info => infoColor,
|
||||
LogLevel.Warning => warningColor,
|
||||
LogLevel.Error => errorColor,
|
||||
_ => infoColor
|
||||
};
|
||||
}
|
||||
|
||||
private void RebuildTagLists()
|
||||
{
|
||||
activeClassTags.Clear();
|
||||
activeMethodTags.Clear();
|
||||
|
||||
foreach (var log in allLogs)
|
||||
{
|
||||
activeClassTags.Add(log.ClassName);
|
||||
activeMethodTags.Add(log.MethodName);
|
||||
}
|
||||
}
|
||||
|
||||
private void UpdateTimeRange()
|
||||
{
|
||||
if (allLogs.Count > 0)
|
||||
{
|
||||
currentMaxTimestamp = allLogs.Max(l => l.Timestamp);
|
||||
|
||||
// Initialize time range on first log
|
||||
if (maxTimestamp == 0)
|
||||
{
|
||||
maxTimestamp = currentMaxTimestamp;
|
||||
}
|
||||
else
|
||||
{
|
||||
// Auto-expand max range as new logs come in
|
||||
maxTimestamp = currentMaxTimestamp;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ExportLogs()
|
||||
{
|
||||
string defaultFileName = $"logs_{DateTime.Now:yyyy-MM-dd_HH-mm-ss}.txt";
|
||||
string path = EditorUtility.SaveFilePanel("Export Logs", "", defaultFileName, "txt");
|
||||
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
var filteredLogs = GetFilteredLogs().ToList();
|
||||
var lines = filteredLogs.Select(e => e.FullFormattedMessage);
|
||||
File.WriteAllLines(path, lines);
|
||||
|
||||
EditorUtility.DisplayDialog("Export Successful",
|
||||
$"Exported {filteredLogs.Count} log entries to:\n{path}", "OK");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
EditorUtility.DisplayDialog("Export Failed",
|
||||
$"Failed to export logs:\n{ex.Message}", "OK");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#endregion
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user