## ManagedBehaviour System Refactor - **Sealed `Awake()`** to prevent override mistakes that break singleton registration - **Added `OnManagedAwake()`** for early initialization (fires during registration) - **Renamed lifecycle hook:** `OnManagedAwake()` → `OnManagedStart()` (fires after boot, mirrors Unity's Awake→Start) - **40 files migrated** to new pattern (2 core, 38 components) - Eliminated all fragile `private new void Awake()` patterns - Zero breaking changes - backward compatible ## Centralized Logging System - **Automatic tagging** via `CallerMemberName` and `CallerFilePath` - logs auto-tagged as `[ClassName][MethodName] message` - **Unified API:** Single `Logging.Debug/Info/Warning/Error()` replaces custom `LogDebugMessage()` implementations - **~90 logging call sites** migrated across 10 files - **10 redundant helper methods** removed - All logs broadcast via `Logging.OnLogEntryAdded` event for real-time monitoring ## Custom Log Console (Editor Window) - **Persistent filter popups** for multi-selection (classes, methods, log levels) - windows stay open during selection - **Search** across class names, methods, and message content - **Time range filter** with MinMaxSlider - **Export** filtered logs to timestamped `.txt` files - **Right-click context menu** for quick filtering and copy actions - **Visual improvements:** White text, alternating row backgrounds, color-coded log levels - **Multiple instances** supported for simultaneous system monitoring - Open via `AppleHills > Custom Log Console` Co-authored-by: Michal Pikulski <michal@foolhardyhorizons.com> Co-authored-by: Michal Pikulski <michal.a.pikulski@gmail.com> Reviewed-on: #56
830 lines
29 KiB
C#
830 lines
29 KiB
C#
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
|
|
|
|
// 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();
|
|
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<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
|
|
}
|
|
|
|
/// <summary>
|
|
/// Persistent popup window for class filtering with multi-selection support
|
|
/// </summary>
|
|
public class ClassFilterWindow : EditorWindow
|
|
{
|
|
private HashSet<string> availableTags;
|
|
private HashSet<string> selectedTags;
|
|
private System.Action<HashSet<string>> onApply;
|
|
private Vector2 scrollPosition;
|
|
private string searchFilter = "";
|
|
|
|
public static void ShowWindow(CustomLogWindow parent, HashSet<string> available, HashSet<string> selected,
|
|
System.Action<HashSet<string>> applyCallback)
|
|
{
|
|
var window = CreateInstance<ClassFilterWindow>();
|
|
window.titleContent = new GUIContent("Class Filter");
|
|
window.availableTags = available;
|
|
window.selectedTags = new HashSet<string>(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<string>(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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Persistent popup window for method filtering with multi-selection support
|
|
/// </summary>
|
|
public class MethodFilterWindow : EditorWindow
|
|
{
|
|
private HashSet<string> availableTags;
|
|
private HashSet<string> selectedTags;
|
|
private System.Action<HashSet<string>> onApply;
|
|
private Vector2 scrollPosition;
|
|
private string searchFilter = "";
|
|
|
|
public static void ShowWindow(CustomLogWindow parent, HashSet<string> available, HashSet<string> selected,
|
|
System.Action<HashSet<string>> applyCallback)
|
|
{
|
|
var window = CreateInstance<MethodFilterWindow>();
|
|
window.titleContent = new GUIContent("Method Filter");
|
|
window.availableTags = available;
|
|
window.selectedTags = new HashSet<string>(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<string>(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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Persistent popup window for log level filtering with multi-selection support
|
|
/// </summary>
|
|
public class LevelFilterWindow : EditorWindow
|
|
{
|
|
private HashSet<LogLevel> selectedLevels;
|
|
private System.Action<HashSet<LogLevel>> onApply;
|
|
|
|
public static void ShowWindow(CustomLogWindow parent, HashSet<LogLevel> selected,
|
|
System.Action<HashSet<LogLevel>> applyCallback)
|
|
{
|
|
var window = CreateInstance<LevelFilterWindow>();
|
|
window.titleContent = new GUIContent("Log Level Filter");
|
|
window.selectedLevels = new HashSet<LogLevel>(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>
|
|
{
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Popup window for configuring time range filters
|
|
/// </summary>
|
|
public class TimeRangeFilterWindow : EditorWindow
|
|
{
|
|
private bool enableTimeFilter;
|
|
private float minTimestamp;
|
|
private float maxTimestamp;
|
|
private float currentMaxTimestamp;
|
|
private System.Action<bool, float, float> onApply;
|
|
|
|
public static void ShowWindow(CustomLogWindow parent, bool enabled, float min, float max, float currentMax,
|
|
System.Action<bool, float, float> applyCallback)
|
|
{
|
|
var window = CreateInstance<TimeRangeFilterWindow>();
|
|
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);
|
|
}
|
|
}
|
|
}
|