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 // 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(); 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.Space(10); // Class Filter Button string classLabel = selectedClassFilters.Count == 0 ? "Classes: All" : selectedClassFilters.Count == activeClassTags.Count ? "Classes: All" : $"Classes: {selectedClassFilters.Count}"; if (GUILayout.Button(new GUIContent(classLabel, "Filter by class"), EditorStyles.toolbarDropDown, GUILayout.Width(100))) { ShowClassFilterMenu(); } // Method Filter Button string methodLabel = selectedMethodFilters.Count == 0 ? "Methods: All" : selectedMethodFilters.Count == activeMethodTags.Count ? "Methods: All" : $"Methods: {selectedMethodFilters.Count}"; if (GUILayout.Button(new GUIContent(methodLabel, "Filter by method"), EditorStyles.toolbarDropDown, GUILayout.Width(110))) { ShowMethodFilterMenu(); } // Log Level Filter Button string levelLabel = selectedLevelFilters.Count == 4 ? "Levels: All" : selectedLevelFilters.Count == 0 ? "Levels: None" : $"Levels: {selectedLevelFilters.Count}"; if (GUILayout.Button(new GUIContent(levelLabel, "Filter by log level"), EditorStyles.toolbarDropDown, GUILayout.Width(90))) { ShowLevelFilterMenu(); } // Time Range Filter Button if (GUILayout.Button(new GUIContent("⏱", "Time range filter"), EditorStyles.toolbarButton, GUILayout.Width(25))) { ShowTimeRangeWindow(); } GUILayout.FlexibleSpace(); // Search box GUILayout.Label("Search:", GUILayout.Width(50)); searchText = GUILayout.TextField(searchText, EditorStyles.toolbarSearchField, GUILayout.Width(150)); GUILayout.Space(10); // Export button if (GUILayout.Button("Export", EditorStyles.toolbarButton, GUILayout.Width(60))) { ExportLogs(); } GUILayout.Space(5); // Log count var filteredCount = GetFilteredLogs().Count(); GUILayout.Label($"{filteredCount}/{allLogs.Count}", GUILayout.Width(80)); EditorGUILayout.EndHorizontal(); } private void ShowClassFilterMenu() { ClassFilterWindow.ShowWindow(this, activeClassTags, selectedClassFilters, (newSelection) => { selectedClassFilters = newSelection; Repaint(); }); } private void ShowMethodFilterMenu() { MethodFilterWindow.ShowWindow(this, activeMethodTags, selectedMethodFilters, (newSelection) => { selectedMethodFilters = newSelection; Repaint(); }); } private void ShowLevelFilterMenu() { LevelFilterWindow.ShowWindow(this, selectedLevelFilters, (newSelection) => { selectedLevelFilters = newSelection; Repaint(); }); } private void ShowTimeRangeWindow() { TimeRangeFilterWindow.ShowWindow(this, enableTimeFilter, minTimestamp, maxTimestamp, currentMaxTimestamp, (enabled, min, max) => { enableTimeFilter = enabled; minTimestamp = min; maxTimestamp = max; Repaint(); }); } 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 } /// /// Persistent popup window for class filtering with multi-selection support /// public class ClassFilterWindow : EditorWindow { private HashSet availableTags; private HashSet selectedTags; private System.Action> onApply; private Vector2 scrollPosition; private string searchFilter = ""; public static void ShowWindow(CustomLogWindow parent, HashSet available, HashSet selected, System.Action> applyCallback) { var window = CreateInstance(); window.titleContent = new GUIContent("Class Filter"); window.availableTags = available; window.selectedTags = new HashSet(selected); window.onApply = applyCallback; var parentRect = parent.position; window.position = new Rect(parentRect.x + 50, parentRect.y + 50, 300, 400); window.ShowUtility(); } private void OnGUI() { EditorGUILayout.Space(5); // Control buttons EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("All", GUILayout.Width(60))) { if (availableTags != null) selectedTags = new HashSet(availableTags); } if (GUILayout.Button("None", GUILayout.Width(60))) { selectedTags.Clear(); } GUILayout.FlexibleSpace(); searchFilter = EditorGUILayout.TextField(searchFilter, EditorStyles.toolbarSearchField, GUILayout.Width(150)); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); // Scrollable list of toggles scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); if (availableTags != null && availableTags.Count > 0) { var filteredTags = string.IsNullOrEmpty(searchFilter) ? availableTags.OrderBy(t => t) : availableTags.Where(t => t.Contains(searchFilter, StringComparison.OrdinalIgnoreCase)).OrderBy(t => t); foreach (var tag in filteredTags) { bool isSelected = selectedTags.Contains(tag); bool newSelection = EditorGUILayout.ToggleLeft(tag, isSelected); if (newSelection != isSelected) { if (newSelection) selectedTags.Add(tag); else selectedTags.Remove(tag); } } } else { EditorGUILayout.HelpBox("No classes available", MessageType.Info); } EditorGUILayout.EndScrollView(); EditorGUILayout.Space(5); // Apply/Close buttons EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Apply", GUILayout.Width(80))) { onApply?.Invoke(selectedTags); Close(); } if (GUILayout.Button("Close", GUILayout.Width(80))) { Close(); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); } } /// /// Persistent popup window for method filtering with multi-selection support /// public class MethodFilterWindow : EditorWindow { private HashSet availableTags; private HashSet selectedTags; private System.Action> onApply; private Vector2 scrollPosition; private string searchFilter = ""; public static void ShowWindow(CustomLogWindow parent, HashSet available, HashSet selected, System.Action> applyCallback) { var window = CreateInstance(); window.titleContent = new GUIContent("Method Filter"); window.availableTags = available; window.selectedTags = new HashSet(selected); window.onApply = applyCallback; var parentRect = parent.position; window.position = new Rect(parentRect.x + 50, parentRect.y + 50, 300, 400); window.ShowUtility(); } private void OnGUI() { EditorGUILayout.Space(5); // Control buttons EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("All", GUILayout.Width(60))) { if (availableTags != null) selectedTags = new HashSet(availableTags); } if (GUILayout.Button("None", GUILayout.Width(60))) { selectedTags.Clear(); } GUILayout.FlexibleSpace(); searchFilter = EditorGUILayout.TextField(searchFilter, EditorStyles.toolbarSearchField, GUILayout.Width(150)); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); // Scrollable list of toggles scrollPosition = EditorGUILayout.BeginScrollView(scrollPosition); if (availableTags != null && availableTags.Count > 0) { var filteredTags = string.IsNullOrEmpty(searchFilter) ? availableTags.OrderBy(t => t) : availableTags.Where(t => t.Contains(searchFilter, StringComparison.OrdinalIgnoreCase)).OrderBy(t => t); foreach (var tag in filteredTags) { bool isSelected = selectedTags.Contains(tag); bool newSelection = EditorGUILayout.ToggleLeft(tag, isSelected); if (newSelection != isSelected) { if (newSelection) selectedTags.Add(tag); else selectedTags.Remove(tag); } } } else { EditorGUILayout.HelpBox("No methods available", MessageType.Info); } EditorGUILayout.EndScrollView(); EditorGUILayout.Space(5); // Apply/Close buttons EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Apply", GUILayout.Width(80))) { onApply?.Invoke(selectedTags); Close(); } if (GUILayout.Button("Close", GUILayout.Width(80))) { Close(); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); } } /// /// Persistent popup window for log level filtering with multi-selection support /// public class LevelFilterWindow : EditorWindow { private HashSet selectedLevels; private System.Action> onApply; public static void ShowWindow(CustomLogWindow parent, HashSet selected, System.Action> applyCallback) { var window = CreateInstance(); window.titleContent = new GUIContent("Log Level Filter"); window.selectedLevels = new HashSet(selected); window.onApply = applyCallback; var parentRect = parent.position; window.position = new Rect(parentRect.x + 50, parentRect.y + 50, 250, 180); window.ShowUtility(); } private void OnGUI() { EditorGUILayout.Space(5); // Control buttons EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("All", GUILayout.Width(60))) { selectedLevels = new HashSet { LogLevel.Debug, LogLevel.Info, LogLevel.Warning, LogLevel.Error }; } if (GUILayout.Button("None", GUILayout.Width(60))) { selectedLevels.Clear(); } EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(10); // Log level toggles foreach (LogLevel level in Enum.GetValues(typeof(LogLevel))) { bool isSelected = selectedLevels.Contains(level); bool newSelection = EditorGUILayout.ToggleLeft(level.ToString(), isSelected); if (newSelection != isSelected) { if (newSelection) selectedLevels.Add(level); else selectedLevels.Remove(level); } } EditorGUILayout.Space(10); // Apply/Close buttons EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Apply", GUILayout.Width(80))) { onApply?.Invoke(selectedLevels); Close(); } if (GUILayout.Button("Close", GUILayout.Width(80))) { Close(); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(5); } } /// /// Popup window for configuring time range filters /// public class TimeRangeFilterWindow : EditorWindow { private bool enableTimeFilter; private float minTimestamp; private float maxTimestamp; private float currentMaxTimestamp; private System.Action onApply; public static void ShowWindow(CustomLogWindow parent, bool enabled, float min, float max, float currentMax, System.Action applyCallback) { var window = CreateInstance(); window.titleContent = new GUIContent("Time Range Filter"); window.enableTimeFilter = enabled; window.minTimestamp = min; window.maxTimestamp = max; window.currentMaxTimestamp = currentMax; window.onApply = applyCallback; // Position near the parent window var parentRect = parent.position; window.position = new Rect(parentRect.x + 100, parentRect.y + 100, 350, 120); window.ShowUtility(); } private void OnGUI() { EditorGUILayout.Space(10); enableTimeFilter = EditorGUILayout.Toggle("Enable Time Filter", enableTimeFilter); EditorGUILayout.Space(5); 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); EditorGUILayout.Space(5); EditorGUILayout.BeginHorizontal(); if (GUILayout.Button("Reset Range")) { minTimestamp = 0; maxTimestamp = currentMaxTimestamp; } EditorGUILayout.EndHorizontal(); } else if (enableTimeFilter) { EditorGUILayout.HelpBox("No logs available for time filtering", MessageType.Info); } EditorGUILayout.Space(10); EditorGUILayout.BeginHorizontal(); GUILayout.FlexibleSpace(); if (GUILayout.Button("Apply", GUILayout.Width(80))) { onApply?.Invoke(enableTimeFilter, minTimestamp, maxTimestamp); Close(); } if (GUILayout.Button("Cancel", GUILayout.Width(80))) { Close(); } GUILayout.FlexibleSpace(); EditorGUILayout.EndHorizontal(); EditorGUILayout.Space(10); } } }