feat: Implement LogViewer control and integrate into EditPage

- Added LogViewer control to display log messages with filtering options.
- Integrated LogViewer into EditPage for better log management.
- Updated EngineEditorWindow to navigate to EditPage.
- Enhanced Logger implementation for improved performance and stack trace capturing.
- Introduced PathUtility for path normalization.
- Refactored AssetManager to correct shader asset type naming.
- Removed obsolete AssetHandlerRegistryTests and cleaned up related tests.
- Updated ImportCoordinatorTests for streamlined asset import process.
This commit is contained in:
2026-04-22 20:25:14 +09:00
parent 884611181a
commit 3533d3367f
34 changed files with 1063 additions and 640 deletions

View File

@@ -0,0 +1,175 @@
using CommunityToolkit.Mvvm.ComponentModel;
using Ghost.Editor.Core;
using Ghost.Editor.Core.Contracts;
using Ghost.Editor.Core.Utilities;
using Ghost.Editor.Models;
using Ghost.Editor.Core.AssetHandler;
using System.Collections.ObjectModel;
using Microsoft.UI.Dispatching;
using Ghost.Engine;
using Ghost.Core.Utilities;
namespace Ghost.Editor.ViewModels.Controls;
internal partial class ContentBrowserViewModel : ObservableObject
{
private readonly IInspectorService _inspectorService;
private readonly IAssetRegistry _assetRegistry;
private readonly DispatcherQueue _dispatcherQueue;
private readonly Dictionary<string, ExplorerItem> _pathToDirectoryItemMap = new();
private ExplorerItem? _selectedItem;
public ObservableCollection<ExplorerItem> Directories
{
get;
} = new();
public ObservableCollection<ExplorerItem> Files
{
get;
} = new();
public ExplorerItem? SelectedItem
{
get => _selectedItem;
set
{
// TODO: Resolve inspector by reading metadata from selected asset
_selectedItem = value;
}
}
public string CurrentDirectoryPath
{
get;
set => field = PathUtility.Normalize(value);
} = string.Empty;
public ContentBrowserViewModel(IInspectorService inspectorService, IAssetRegistry assetRegistry)
{
_inspectorService = inspectorService;
_assetRegistry = assetRegistry;
_dispatcherQueue = DispatcherQueue.GetForCurrentThread();
var assetsRootItem = new ExplorerItem(EditorApplication.ASSETS_FOLDER_NAME, EditorApplication.AssetsFolderPath, true);
LoadSubFolderRecursive(assetsRootItem);
Directories.Add(assetsRootItem);
_assetRegistry.OnAssetChanged += OnAssetChanged;
}
private void OnAssetChanged(object? sender, AssetChangedEventArgs e)
{
if (Path.GetExtension(e.AssetPath) == FileExtensions.META_FILE_EXTENSION)
{
return;
}
var fullPath = PathUtility.Normalize(Path.Combine(EditorApplication.AssetsFolderPath, e.AssetPath));
var dirPath = PathUtility.Normalize(Path.GetDirectoryName(fullPath));
if (string.Equals(dirPath, CurrentDirectoryPath, StringComparison.OrdinalIgnoreCase))
{
_dispatcherQueue.TryEnqueue(() =>
{
if (e.ChangeType == AssetChangeType.Created || e.ChangeType == AssetChangeType.Renamed)
{
if (e.ChangeType == AssetChangeType.Renamed && e.OldAssetPath != null)
{
var oldFullPath = PathUtility.Normalize(Path.Combine(EditorApplication.AssetsFolderPath, e.OldAssetPath));
var oldItem = Files.FirstOrDefault(f => string.Equals(f.Path, oldFullPath, StringComparison.OrdinalIgnoreCase));
if (oldItem != null) Files.Remove(oldItem);
}
if (!Files.Any(f => string.Equals(f.Path, fullPath, StringComparison.OrdinalIgnoreCase)))
{
var isDir = Directory.Exists(fullPath);
AssetType assetType = AssetType.Unknown;
if (!isDir)
{
var ext = Path.GetExtension(fullPath);
assetType = AssetHandlerRegistry.GetAssetTypeByExtension(ext);
}
Files.Add(new ExplorerItem(Path.GetFileName(fullPath), fullPath, isDir, assetType));
}
}
else if (e.ChangeType == AssetChangeType.Deleted)
{
var item = Files.FirstOrDefault(f => string.Equals(f.Path, fullPath, StringComparison.OrdinalIgnoreCase));
if (item != null)
{
Files.Remove(item);
}
}
});
}
}
private void LoadSubFolderRecursive(ExplorerItem parentItem)
{
foreach (var directory in Directory.EnumerateDirectories(parentItem.Path))
{
var item = new ExplorerItem(Path.GetFileName(directory), directory, true);
LoadSubFolderRecursive(item);
_pathToDirectoryItemMap[directory] = item;
parentItem.Children ??= new();
parentItem.Children.Add(item);
}
}
internal void NavigateToDirectory(string? path)
{
Files.Clear();
if (!Directory.Exists(path))
{
return;
}
foreach (var directory in Directory.EnumerateDirectories(path))
{
var directoryItem = new ExplorerItem(Path.GetFileName(directory), directory, true);
Files.Add(directoryItem);
}
foreach (var file in Directory.EnumerateFiles(path))
{
if (Path.GetExtension(file) == FileExtensions.META_FILE_EXTENSION)
{
continue;
}
var ext = Path.GetExtension(file);
var assetType = AssetHandlerRegistry.GetAssetTypeByExtension(ext);
var fileItem = new ExplorerItem(Path.GetFileName(file), file, false, assetType);
Files.Add(fileItem);
}
CurrentDirectoryPath = Path.GetFullPath(path);
}
internal (ExplorerItem?, int) OpenSelected()
{
if (SelectedItem == null)
{
return (null, 0);
}
if (SelectedItem.IsDirectory)
{
NavigateToDirectory(SelectedItem.Path);
SelectedItem = _pathToDirectoryItemMap[SelectedItem.Path];
return (SelectedItem, 0);
}
else
{
// _assetRegistry.OpenAsset(SelectedItem.FullName);
return (null, 1);
}
}
}