using System; using System.Collections.Generic; using System.IO; using System.Linq; using UnityEditor; using UnityEngine; using Core; namespace Editor { /// /// 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. /// public class CustomLogWindow : EditorWindow { #region Window State private Vector2 scrollPosition; private readonly List allLogs = new List(); #endregion #region Filter State private HashSet activeClassTags = new HashSet(); private HashSet activeMethodTags = new HashSet(); private HashSet selectedClassFilters = new HashSet(); private HashSet selectedMethodFilters = new HashSet(); private HashSet selectedLevelFilters = new HashSet { 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("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(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(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 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 } }