Updating ProjectBrowser

This commit is contained in:
2026-02-04 19:08:18 +09:00
parent 59991f47d5
commit eadd13931f
30 changed files with 382 additions and 139 deletions

View File

@@ -1,15 +0,0 @@
namespace Ghost.Editor.Core.AssetHandle;
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
internal class AssetImporterAttribute : Attribute
{
public string[] SupportedExtensions
{
get;
}
public AssetImporterAttribute(params string[] supportedExtensions)
{
SupportedExtensions = supportedExtensions;
}
}

View File

@@ -1,15 +0,0 @@
namespace Ghost.Editor.Core.AssetHandle;
[AttributeUsage(AttributeTargets.Method)]
public class AssetOpenHandlerAttribute : Attribute
{
public string[] Extensions
{
get;
}
public AssetOpenHandlerAttribute(params string[] extensions)
{
Extensions = extensions.Select(e => e.StartsWith('.') ? e.ToLowerInvariant() : '.' + e.ToLowerInvariant()).ToArray();
}
}

View File

@@ -0,0 +1,102 @@
namespace Ghost.Editor.Core;
/// <summary>
/// The base class for all attributes that can be discovered via <see cref="Utilities.TypeCache"/>.
/// </summary>
public abstract class DiscoverableAttributeBase : Attribute;
[AttributeUsage(AttributeTargets.Method)]
public class AssetOpenHandlerAttribute : DiscoverableAttributeBase
{
public string[] Extensions
{
get;
}
public AssetOpenHandlerAttribute(params string[] extensions)
{
Extensions = extensions.Select(e => e.StartsWith('.') ? e.ToLowerInvariant() : '.' + e.ToLowerInvariant()).ToArray();
}
}
[AttributeUsage(AttributeTargets.Class, AllowMultiple = true)]
internal class AssetImporterAttribute : DiscoverableAttributeBase
{
public string[] SupportedExtensions
{
get;
}
public AssetImporterAttribute(params string[] supportedExtensions)
{
SupportedExtensions = supportedExtensions;
}
}
[AttributeUsage(AttributeTargets.Class)]
public class CustomEditorAttribute : DiscoverableAttributeBase
{
internal Type TargetType
{
get;
}
public CustomEditorAttribute(Type targetType)
{
TargetType = targetType;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public class EditorInjectionAttribute : DiscoverableAttributeBase
{
public enum ServiceLifetime
{
Singleton,
Transient,
Scoped
}
public ServiceLifetime Lifetime
{
get;
}
public Type? ImplementationType
{
get;
}
public EditorInjectionAttribute(ServiceLifetime lifetime, Type? implementationType = null)
{
Lifetime = lifetime;
ImplementationType = implementationType;
}
}
[AttributeUsage(AttributeTargets.Method)]
public sealed class ContextMenuItemAttribute : DiscoverableAttributeBase
{
public string Tag
{
get;
}
public string Name
{
get;
}
public int Group
{
get;
}
public ContextMenuItemAttribute(string tag, string name, int group = 0)
{
Tag = tag;
Name = name;
Group = group;
}
}

View File

@@ -4,7 +4,6 @@
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/BasicInput/PropertyField.xaml" />
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/BasicInput/Float3Field.xaml" />
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/Internal/ComponentDataView.xaml" />
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/Internal/NavigationTabView.xaml" />
<ResourceDictionary Source="ms-appx:///Ghost.Editor.Core/Controls/Internal/ComponentView.xaml" />
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -7,7 +7,7 @@ using Microsoft.UI.Xaml.Controls;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.Controls.Internal;
namespace Ghost.Editor.Core.Controls;
internal sealed unsafe partial class ComponentView : Control
{

View File

@@ -2,7 +2,7 @@
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Core.Controls.Internal">
xmlns:local="using:Ghost.Editor.Core.Controls">
<Style TargetType="local:ComponentView">
<Setter Property="Template">

View File

@@ -2,7 +2,7 @@ using Ghost.Editor.Core.Contracts;
using Microsoft.UI.Xaml;
using Microsoft.UI.Xaml.Controls;
namespace Ghost.Editor.Controls.Internal;
namespace Ghost.Editor.Controls;
public partial class NavigationTabPage : TabViewItem, INavigationAware
{

View File

@@ -1,5 +0,0 @@
<?xml version="1.0" encoding="utf-8" ?>
<ResourceDictionary
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="using:Ghost.Editor.Controls.Internal" />

View File

@@ -0,0 +1,95 @@
using Ghost.Editor.Core.Utilities;
using Microsoft.UI.Xaml.Controls;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.Controls;
public sealed partial class ContextFlyout : MenuFlyout
{
private bool _isPopulated;
public string Tag
{
get; set;
} = string.Empty;
public ContextFlyout()
{
Opening += ContextFlyout_Opening;
}
private void PopulateContextMenu()
{
var methods = TypeCache.GetMethodsWithAttribute<ContextMenuItemAttribute>();
if (methods == null)
{
return;
}
var list = new List<(ContextMenuItemAttribute attr, MethodInfo method)>();
foreach (var method in methods)
{
var attributes = method.GetCustomAttributes(typeof(ContextMenuItemAttribute), false);
var attribute = (ContextMenuItemAttribute)attributes[0];
if (!string.Equals(attribute.Tag, Tag, StringComparison.OrdinalIgnoreCase))
{
continue;
}
list.Add((attribute, method));
}
var span = CollectionsMarshal.AsSpan(list);
span.Sort((a, b) =>
{
var result = a.attr.Group.CompareTo(b.attr.Group);
if (result == 0)
{
result = string.CompareOrdinal(a.attr.Name, b.attr.Name);
}
return result;
});
// itemContainer may be a main thread only collection (for example, ObservableCollection), we run it on the UI thread
var i = 0;
var group = 0;
while (i < list.Count)
{
var (attr, method) = list[i];
if (attr.Group != group)
{
Items.Add(new MenuFlyoutSeparator());
group = attr.Group;
}
// TODO: Group items with / in the name into submenus
var menuItem = new MenuFlyoutItem
{
Text = attr.Name
};
menuItem.Click += (_, _) =>
{
method.Invoke(null, null);
};
Items.Add(menuItem);
i++;
}
}
private async void ContextFlyout_Opening(object? sender, object e)
{
if (_isPopulated)
{
return;
}
PopulateContextMenu();
_isPopulated = true;
}
}

View File

@@ -1,3 +1,4 @@
using Microsoft.UI.Dispatching;
using Microsoft.UI.Xaml;
namespace Ghost.Editor.Core;
@@ -11,10 +12,25 @@ public static class EditorApplication
private static string s_currentProjectPath = string.Empty;
private static string s_currentProjectName = string.Empty;
private static DispatcherQueue? s_dispatcherQueue;
internal static Application CurrentApplication => Application.Current;
internal static string CurrentProjectPath => s_currentProjectPath;
internal static string CurrentProjectName => s_currentProjectName;
public static DispatcherQueue DispatcherQueue
{
get
{
if (s_dispatcherQueue is null)
{
throw new InvalidOperationException("DispatcherQueue is not initialized.");
}
return s_dispatcherQueue;
}
}
internal static void Initialize(IServiceProvider serviceProvider, string projectPath, string projectName)
{
s_serviceProvider = serviceProvider;
@@ -22,6 +38,11 @@ public static class EditorApplication
s_currentProjectName = projectName;
}
internal static void SetDispatcherQueue(DispatcherQueue dispatcherQueue)
{
s_dispatcherQueue = dispatcherQueue;
}
public static T GetService<T>()
where T : class
{

View File

@@ -1,28 +0,0 @@
namespace Ghost.Editor.Core;
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Interface, AllowMultiple = false, Inherited = false)]
public class EditorInjectionAttribute : Attribute
{
public enum ServiceLifetime
{
Singleton,
Transient,
Scoped
}
public ServiceLifetime Lifetime
{
get;
}
public Type? ImplementationType
{
get;
}
public EditorInjectionAttribute(ServiceLifetime lifetime, Type? implementationType = null)
{
Lifetime = lifetime;
ImplementationType = implementationType;
}
}

View File

@@ -32,13 +32,7 @@
<Page Update="Controls\BasicInput\Vector3Field.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\ControlsDictionary.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\Internal\ComponentDataView.xaml">
<SubType>Designer</SubType>
</Page>
<Page Update="Controls\Internal\NavigationTabView.xaml">
<Page Update="Controls\Internal\ComponentView.xaml">
<SubType>Designer</SubType>
</Page>
</ItemGroup>

View File

@@ -1,10 +0,0 @@
namespace Ghost.Editor.Core.Inspector;
[AttributeUsage(AttributeTargets.Class)]
public class CustomEditorAttribute(Type targetType) : Attribute
{
internal Type TargetType
{
get;
} = targetType;
}

View File

@@ -1,13 +1,21 @@
using Ghost.Core.Attributes;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Ghost.Editor.Core.Utilities;
public static class TypeCache
{
private static readonly TypeInfo[] s_types;
private static TypeInfo[] s_types;
private static Dictionary<nint, List<MethodInfo>> s_attributeMethodCache;
static TypeCache()
{
s_types = LoadTypes();
s_attributeMethodCache = FindMethodWithAttribute();
}
private static TypeInfo[] LoadTypes()
{
var loadableTypes = new List<Type>(512);
var assembliesToScan = AppDomain.CurrentDomain.GetAssemblies()
@@ -26,7 +34,32 @@ public static class TypeCache
}
}
s_types = loadableTypes.Select(t => t.GetTypeInfo()).ToArray();
return loadableTypes.Select(t => t.GetTypeInfo()).ToArray();
}
private static Dictionary<nint, List<MethodInfo>> FindMethodWithAttribute()
{
var dict = new Dictionary<nint, List<MethodInfo>>();
foreach (var type in s_types)
{
foreach (var method in type.DeclaredMethods)
{
var attrs = method.GetCustomAttributes<DiscoverableAttributeBase>(false);
foreach (var attr in attrs)
{
var key = attr.GetType().TypeHandle.Value;
ref var methodList = ref CollectionsMarshal.GetValueRefOrAddDefault(dict, key, out var exist);
if (!exist)
{
methodList = new List<MethodInfo>();
}
methodList!.Add(method);
}
}
}
return dict;
}
internal static void Init()
@@ -35,8 +68,26 @@ public static class TypeCache
// This method exists to force the static constructor to run.
}
public static Type[] GetTypes()
internal static void Reload()
{
s_types = LoadTypes();
s_attributeMethodCache = FindMethodWithAttribute();
}
public static IReadOnlyCollection<TypeInfo> GetTypes()
{
return s_types;
}
public static IReadOnlyCollection<MethodInfo>? GetMethodsWithAttribute<T>()
where T : DiscoverableAttributeBase
{
var key = typeof(T).TypeHandle.Value;
if (s_attributeMethodCache.TryGetValue(key, out var methods))
{
return methods;
}
return null;
}
}