diff --git a/Assets/Editor/CustomLogWindow.cs b/Assets/Editor/CustomLogWindow.cs index 5e936742..c12d23c7 100644 --- a/Assets/Editor/CustomLogWindow.cs +++ b/Assets/Editor/CustomLogWindow.cs @@ -49,11 +49,6 @@ namespace Editor #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; @@ -123,7 +118,6 @@ namespace Editor private void OnGUI() { DrawToolbar(); - DrawFilters(); DrawLogEntries(); } @@ -144,11 +138,46 @@ namespace Editor // 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(); - // Log count - var filteredCount = GetFilteredLogs().Count(); - GUILayout.Label($"{filteredCount} / {allLogs.Count} logs", GUILayout.Width(100)); + // Search box + GUILayout.Label("Search:", GUILayout.Width(50)); + searchText = GUILayout.TextField(searchText, EditorStyles.toolbarSearchField, GUILayout.Width(150)); GUILayout.Space(10); @@ -158,135 +187,55 @@ namespace Editor ExportLogs(); } - GUILayout.Space(10); + GUILayout.Space(5); - // Search box - GUILayout.Label("Search:", GUILayout.Width(50)); - searchText = GUILayout.TextField(searchText, EditorStyles.toolbarSearchField, GUILayout.Width(200)); + // Log count + var filteredCount = GetFilteredLogs().Count(); + GUILayout.Label($"{filteredCount}/{allLogs.Count}", GUILayout.Width(80)); EditorGUILayout.EndHorizontal(); } - private void DrawFilters() + private void ShowClassFilterMenu() { - 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))) + ClassFilterWindow.ShowWindow(this, activeClassTags, selectedClassFilters, + (newSelection) => { - selectedClassFilters = new HashSet(activeClassTags); - } - if (GUILayout.Button("None", GUILayout.Width(50))) + selectedClassFilters = newSelection; + Repaint(); + }); + } + + private void ShowMethodFilterMenu() + { + MethodFilterWindow.ShowWindow(this, activeMethodTags, selectedMethodFilters, + (newSelection) => { - selectedClassFilters.Clear(); - } - EditorGUILayout.EndHorizontal(); - - EditorGUILayout.BeginVertical(); - foreach (var tag in activeClassTags.OrderBy(t => t)) + selectedMethodFilters = newSelection; + Repaint(); + }); + } + + private void ShowLevelFilterMenu() + { + LevelFilterWindow.ShowWindow(this, selectedLevelFilters, + (newSelection) => { - 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))) + selectedLevelFilters = newSelection; + Repaint(); + }); + } + + private void ShowTimeRangeWindow() + { + TimeRangeFilterWindow.ShowWindow(this, enableTimeFilter, minTimestamp, maxTimestamp, currentMaxTimestamp, + (enabled, min, max) => { - 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(); + enableTimeFilter = enabled; + minTimestamp = min; + maxTimestamp = max; + Repaint(); + }); } private void DrawLogEntries() @@ -508,5 +457,357 @@ namespace Editor #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))) + { + 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); + + 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); + } + } + + 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))) + { + 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); + + 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); + } + } + + 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); + } + } } - diff --git a/docs/custom_log_console.md b/docs/custom_log_console.md new file mode 100644 index 00000000..98015d7f --- /dev/null +++ b/docs/custom_log_console.md @@ -0,0 +1,120 @@ +# Custom Log Console + +## Overview + +A centralized logging system with an advanced filtering console that automatically tags log entries with class and method names. Provides powerful filtering capabilities beyond Unity's default console, including multi-select filters, text search, time-range filtering, and log export. + +## Using the Logging System in Code + +All logging automatically captures the calling class and method name using `CallerMemberName` and `CallerFilePath` attributes. Simply call the static logging methods: + +```csharp +using Core; + +public class MyClass : ManagedBehaviour +{ + internal override void OnManagedStart() + { + Logging.Debug("Initialization complete"); + Logging.Info("Player spawned at position"); + Logging.Warning("Missing configuration, using defaults"); + Logging.Error("Failed to load required asset"); + } +} +``` + +**Output format:** +``` +[ClassName][MethodName] Your message +``` + +**Available methods:** +- `Logging.Debug(string message)` - Detailed diagnostic information +- `Logging.Info(string message)` - General informational messages +- `Logging.Warning(string message)` - Non-critical issues +- `Logging.Error(string message)` - Critical errors + +**Note:** All logs are broadcast via the `Logging.OnLogEntryAdded` event and stored in a central buffer accessible via `Logging.GetRecentLogs()`. + +--- + +## Opening the Console Window + +**Menu:** `AppleHills > Custom Log Console` + +You can open multiple independent console instances with different filter configurations to monitor separate systems simultaneously + +--- + +## Console Interface + +![Custom Log Console Interface](media/custom_console.png) + +### Toolbar Controls + +#### 🔵 Basic Controls (Blue outline) +- **Clear** - Clears all log entries and resets tag lists +- **Auto-scroll** - Automatically scrolls to newest entries when enabled + +#### Filter Buttons (Persistent Popups) + +All filter buttons open persistent popup windows that remain open during multi-selection. Changes apply when you click "Apply" or dismiss with "Close". + +- **🔴 Classes Filter (Red outline)** + - Multi-select which classes to display + - Includes search box for quick filtering + - All/None quick actions + +- **🟢 Methods Filter (Green outline)** + - Multi-select which methods to display + - Includes search box for quick filtering + - All/None quick actions + +- **🟡 Levels Filter (Yellow outline)** + - Toggle Debug, Info, Warning, Error levels + - All/None quick actions + +- **⏱ Time Filter** + - Opens utility window with MinMaxSlider + - Filter logs by timestamp range + - Enable/disable toggle with reset option + +#### Search & Export +- **Search** - Full-text search across class names, method names, and message content +- **Export** - Save filtered logs to .txt file with timestamp +- **Count** - Shows `filtered/total` log count + +--- + +## Visual Indicators + +**Color Coding:** +- White: Debug/Info (normal operation) +- Yellow: Warning (non-critical issues) +- Red: Error (critical failures) + +**Alternating Rows:** Light/dark grey backgrounds improve readability for dense log output. + +--- + +## Technical Details + +**Event Broadcasting:** +```csharp +Logging.OnLogEntryAdded += (LogEntry entry) => { /* handle */ }; +``` + +**Manual Log Retrieval:** +```csharp +List recentLogs = Logging.GetRecentLogs(); +``` + +**LogEntry Structure:** +- `ClassName` - Captured from calling file path +- `MethodName` - Captured from `CallerMemberName` +- `Message` - User-provided message text +- `Level` - Debug/Info/Warning/Error enum +- `Timestamp` - Time.realtimeSinceStartup +- `FullFormattedMessage` - Complete formatted string + +--- \ No newline at end of file diff --git a/docs/media/custom_console.png b/docs/media/custom_console.png new file mode 100644 index 00000000..bea85472 Binary files /dev/null and b/docs/media/custom_console.png differ