Initial commit

This commit is contained in:
AnnulusGames 2023-12-02 10:54:41 +09:00
commit a720750e46
220 changed files with 10888 additions and 0 deletions

2
.gitattributes vendored Normal file
View File

@ -0,0 +1,2 @@
# Auto detect text files and perform LF normalization
* text=auto

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
# DS_Store
*.DS_Store

403
Alchemy.SourceGenerator/.gitignore vendored Normal file
View File

@ -0,0 +1,403 @@
## Ignore Visual Studio temporary files, build results, and
## files generated by popular Visual Studio add-ons.
##
## Get latest from https://github.com/github/gitignore/blob/main/VisualStudio.gitignore
# User-specific files
*.rsuser
*.suo
*.user
*.userosscache
*.sln.docstates
# User-specific files (MonoDevelop/Xamarin Studio)
*.userprefs
# Mono auto generated files
mono_crash.*
# Build results
[Dd]ebug/
[Dd]ebugPublic/
[Rr]elease/
[Rr]eleases/
x64/
x86/
[Ww][Ii][Nn]32/
[Aa][Rr][Mm]/
[Aa][Rr][Mm]64/
bld/
[Bb]in/
[Oo]bj/
[Ll]og/
[Ll]ogs/
# Visual Studio 2015/2017 cache/options directory
.vs/
# Uncomment if you have tasks that create the project's static files in wwwroot
#wwwroot/
# Visual Studio 2017 auto generated files
Generated\ Files/
# MSTest test Results
[Tt]est[Rr]esult*/
[Bb]uild[Ll]og.*
# NUnit
*.VisualState.xml
TestResult.xml
nunit-*.xml
# Build Results of an ATL Project
[Dd]ebugPS/
[Rr]eleasePS/
dlldata.c
# Benchmark Results
BenchmarkDotNet.Artifacts/
# .NET Core
project.lock.json
project.fragment.lock.json
artifacts/
# ASP.NET Scaffolding
ScaffoldingReadMe.txt
# StyleCop
StyleCopReport.xml
# Files built by Visual Studio
*_i.c
*_p.c
*_h.h
*.ilk
*.meta
*.obj
*.iobj
*.pch
*.pdb
*.ipdb
*.pgc
*.pgd
*.rsp
*.sbr
*.tlb
*.tli
*.tlh
*.tmp
*.tmp_proj
*_wpftmp.csproj
*.log
*.tlog
*.vspscc
*.vssscc
.builds
*.pidb
*.svclog
*.scc
# Chutzpah Test files
_Chutzpah*
# Visual C++ cache files
ipch/
*.aps
*.ncb
*.opendb
*.opensdf
*.sdf
*.cachefile
*.VC.db
*.VC.VC.opendb
# Visual Studio profiler
*.psess
*.vsp
*.vspx
*.sap
# Visual Studio Trace Files
*.e2e
# TFS 2012 Local Workspace
$tf/
# Guidance Automation Toolkit
*.gpState
# ReSharper is a .NET coding add-in
_ReSharper*/
*.[Rr]e[Ss]harper
*.DotSettings.user
# TeamCity is a build add-in
_TeamCity*
# DotCover is a Code Coverage Tool
*.dotCover
# AxoCover is a Code Coverage Tool
.axoCover/*
!.axoCover/settings.json
# Coverlet is a free, cross platform Code Coverage Tool
coverage*.json
coverage*.xml
coverage*.info
# Visual Studio code coverage results
*.coverage
*.coveragexml
# NCrunch
_NCrunch_*
.*crunch*.local.xml
nCrunchTemp_*
# MightyMoose
*.mm.*
AutoTest.Net/
# Web workbench (sass)
.sass-cache/
# Installshield output folder
[Ee]xpress/
# DocProject is a documentation generator add-in
DocProject/buildhelp/
DocProject/Help/*.HxT
DocProject/Help/*.HxC
DocProject/Help/*.hhc
DocProject/Help/*.hhk
DocProject/Help/*.hhp
DocProject/Help/Html2
DocProject/Help/html
# Click-Once directory
publish/
# Publish Web Output
*.[Pp]ublish.xml
*.azurePubxml
# Note: Comment the next line if you want to checkin your web deploy settings,
# but database connection strings (with potential passwords) will be unencrypted
*.pubxml
*.publishproj
# Microsoft Azure Web App publish settings. Comment the next line if you want to
# checkin your Azure Web App publish settings, but sensitive information contained
# in these scripts will be unencrypted
PublishScripts/
# NuGet Packages
*.nupkg
# NuGet Symbol Packages
*.snupkg
# The packages folder can be ignored because of Package Restore
**/[Pp]ackages/*
# except build/, which is used as an MSBuild target.
!**/[Pp]ackages/build/
# Uncomment if necessary however generally it will be regenerated when needed
#!**/[Pp]ackages/repositories.config
# NuGet v3's project.json files produces more ignorable files
*.nuget.props
*.nuget.targets
# Microsoft Azure Build Output
csx/
*.build.csdef
# Microsoft Azure Emulator
ecf/
rcf/
# Windows Store app package directories and files
AppPackages/
BundleArtifacts/
Package.StoreAssociation.xml
_pkginfo.txt
*.appx
*.appxbundle
*.appxupload
# Visual Studio cache files
# files ending in .cache can be ignored
*.[Cc]ache
# but keep track of directories ending in .cache
!?*.[Cc]ache/
# Others
ClientBin/
~$*
*~
*.dbmdl
*.dbproj.schemaview
*.jfm
*.pfx
*.publishsettings
orleans.codegen.cs
# Including strong name files can present a security risk
# (https://github.com/github/gitignore/pull/2483#issue-259490424)
#*.snk
# Since there are multiple workflows, uncomment next line to ignore bower_components
# (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
#bower_components/
# RIA/Silverlight projects
Generated_Code/
# Backup & report files from converting an old project file
# to a newer Visual Studio version. Backup files are not needed,
# because we have git ;-)
_UpgradeReport_Files/
Backup*/
UpgradeLog*.XML
UpgradeLog*.htm
ServiceFabricBackup/
*.rptproj.bak
# SQL Server files
*.mdf
*.ldf
*.ndf
# Business Intelligence projects
*.rdl.data
*.bim.layout
*.bim_*.settings
*.rptproj.rsuser
*- [Bb]ackup.rdl
*- [Bb]ackup ([0-9]).rdl
*- [Bb]ackup ([0-9][0-9]).rdl
# Microsoft Fakes
FakesAssemblies/
# GhostDoc plugin setting file
*.GhostDoc.xml
# Node.js Tools for Visual Studio
.ntvs_analysis.dat
node_modules/
# Visual Studio 6 build log
*.plg
# Visual Studio 6 workspace options file
*.opt
# Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
*.vbw
# Visual Studio 6 auto-generated project file (contains which files were open etc.)
*.vbp
# Visual Studio 6 workspace and project file (working project files containing files to include in project)
*.dsw
*.dsp
# Visual Studio 6 technical files
*.ncb
*.aps
# Visual Studio LightSwitch build output
**/*.HTMLClient/GeneratedArtifacts
**/*.DesktopClient/GeneratedArtifacts
**/*.DesktopClient/ModelManifest.xml
**/*.Server/GeneratedArtifacts
**/*.Server/ModelManifest.xml
_Pvt_Extensions
# Paket dependency manager
.paket/paket.exe
paket-files/
# FAKE - F# Make
.fake/
# CodeRush personal settings
.cr/personal
# Python Tools for Visual Studio (PTVS)
__pycache__/
*.pyc
# Cake - Uncomment if you are using it
# tools/**
# !tools/packages.config
# Tabs Studio
*.tss
# Telerik's JustMock configuration file
*.jmconfig
# BizTalk build output
*.btp.cs
*.btm.cs
*.odx.cs
*.xsd.cs
# OpenCover UI analysis results
OpenCover/
# Azure Stream Analytics local run output
ASALocalRun/
# MSBuild Binary and Structured Log
*.binlog
# NVidia Nsight GPU debugger configuration file
*.nvuser
# MFractors (Xamarin productivity tool) working folder
.mfractor/
# Local History for Visual Studio
.localhistory/
# Visual Studio History (VSHistory) files
.vshistory/
# BeatPulse healthcheck temp database
healthchecksdb
# Backup folder for Package Reference Convert tool in Visual Studio 2017
MigrationBackup/
# Ionide (cross platform F# VS Code tools) working folder
.ionide/
# Fody - auto-generated XML schema
FodyWeavers.xsd
# VS Code files for those working on multiple tools
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
*.code-workspace
# Local History for Visual Studio Code
.history/
# Windows Installer files from build outputs
*.cab
*.msi
*.msix
*.msm
*.msp
# JetBrains Rider
*.sln.iml
.idea
## DS_Store
*.DS_Store

View File

@ -0,0 +1,14 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>9.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis" Version="3.9.0" />
<PackageReference Include="Microsoft.CodeAnalysis.Common" Version="3.9.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.3" />
</ItemGroup>
</Project>

View File

@ -0,0 +1,25 @@

Microsoft Visual Studio Solution File, Format Version 12.00
# Visual Studio Version 16
VisualStudioVersion = 25.0.1706.1
MinimumVisualStudioVersion = 10.0.40219.1
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Alchemy.SourceGenerator", "Alchemy.SourceGenerator.csproj", "{073160C2-08C9-4905-A4D0-7897AADB4E6C}"
EndProject
Global
GlobalSection(SolutionConfigurationPlatforms) = preSolution
Debug|Any CPU = Debug|Any CPU
Release|Any CPU = Release|Any CPU
EndGlobalSection
GlobalSection(ProjectConfigurationPlatforms) = postSolution
{073160C2-08C9-4905-A4D0-7897AADB4E6C}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{073160C2-08C9-4905-A4D0-7897AADB4E6C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{073160C2-08C9-4905-A4D0-7897AADB4E6C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{073160C2-08C9-4905-A4D0-7897AADB4E6C}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution
SolutionGuid = {D02A2C17-448E-4F43-BF08-7E77D0E2208B}
EndGlobalSection
EndGlobal

View File

@ -0,0 +1,190 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
namespace Alchemy.SourceGenerator
{
[Generator]
public sealed class AlchemySerializeGenerator : ISourceGenerator
{
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(SyntaxReceiver.Create);
}
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is not SyntaxReceiver receiver || receiver.TargetTypes.Count == 0) return;
var compilation = context.Compilation;
try
{
foreach (var typeSyntax in receiver.TargetTypes)
{
var fieldSymbols = new List<IFieldSymbol>();
var fields = typeSyntax.Members
.Where(x => x is FieldDeclarationSyntax)
.Select(x => (FieldDeclarationSyntax)x);
foreach (var field in fields)
{
var model = context.Compilation.GetSemanticModel(field.SyntaxTree);
foreach (var variable in field.Declaration.Variables)
{
var fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
var attribute = fieldSymbol.GetAttributes()
.FirstOrDefault(x =>
x.AttributeClass.Name is "AlchemySerializeField"
or "AlchemySerializeFieldAttribute"
or "Alchemy.Serialization.AlchemySerializeField"
or "Alchemy.Serialization.AlchemySerializeFieldAttribute");
if (attribute != null)
{
fieldSymbols.Add(fieldSymbol);
}
}
}
var typeSymbol = context.Compilation.GetSemanticModel(typeSyntax.SyntaxTree)
.GetDeclaredSymbol(typeSyntax);
var sourceText = ProcessClass((INamedTypeSymbol)typeSymbol, fieldSymbols);
var fullType = typeSymbol.ToDisplayString(SymbolDisplayFormat.FullyQualifiedFormat)
.Replace("global::", "")
.Replace("<", "_")
.Replace(">", "_");
context.AddSource(fullType + ".AlchemySerializeGenerator.g.cs", sourceText);
}
}
catch (Exception ex)
{
var diagnosticDescriptor = new DiagnosticDescriptor("AlchemySerializeGeneratorError", "AlchemySerializeGeneratorError", $"Generation failed with:\n {ex}", "AlchemySerializeGeneratorError", DiagnosticSeverity.Error, true);
context.ReportDiagnostic(Diagnostic.Create(diagnosticDescriptor, Location.None, DiagnosticSeverity.Error));
}
}
static string ProcessClass(INamedTypeSymbol typeSymbol, List<IFieldSymbol> fieldSymbols)
{
var onAfterDeserializeCodeBuilder = new StringBuilder();
var onBeforeSerializeCodeBuilder = new StringBuilder();
var serializationDataCodeBuilder = new StringBuilder();
var hasShowSerializationData = typeSymbol.GetAttributes().Any(x => x.AttributeClass.Name
is "ShowAlchemySerializationData"
or "ShowAlchemySerializationDataAttribute"
or "Alchemy.Serialization.ShowAlchemySerializationData"
or "Alchemy.Serialization.ShowAlchemySerializationDataAttribute");
var serializationDataAttibutesCode = hasShowSerializationData ? "[global::Alchemy.Inspector.ReadOnly, global::UnityEngine.TextArea(3, 999), global::UnityEngine.SerializeField]" : "[global::UnityEngine.HideInInspector, global::UnityEngine.SerializeField]";
// target class namespace
var ns = typeSymbol.ContainingNamespace.IsGlobalNamespace ? string.Empty : $"namespace {typeSymbol.ContainingNamespace} {{";
foreach (var field in fieldSymbols)
{
var serializeCode =
@$"try
{{
alchemySerializationData.{field.Name}.data = global::Alchemy.Serialization.Internal.SerializationHelper.ToJson(this.{field.Name} , alchemySerializationData.UnityObjectReferences);
alchemySerializationData.{field.Name}.isCreated = true;
}}
catch (global::System.Exception ex)
{{
global::UnityEngine.Debug.LogException(ex);
}}";
var deserializeCode =
@$"try
{{
if (alchemySerializationData.{field.Name}.isCreated)
{{
this.{field.Name} = global::Alchemy.Serialization.Internal.SerializationHelper.FromJson<{field.Type.ToDisplayString()}>(alchemySerializationData.{field.Name}.data, alchemySerializationData.UnityObjectReferences);
}}
}}
catch (global::System.Exception ex)
{{
global::UnityEngine.Debug.LogException(ex);
}}";
onBeforeSerializeCodeBuilder.AppendLine(serializeCode);
onAfterDeserializeCodeBuilder.AppendLine(deserializeCode);
serializationDataCodeBuilder.Append("public Item ").Append(field.Name).Append(" = new();");
}
return
@$"
// <auto-generated/>
{ns}
partial class {typeSymbol.Name} : global::UnityEngine.ISerializationCallbackReceiver
{{
void global::UnityEngine.ISerializationCallbackReceiver.OnAfterDeserialize()
{{
{onAfterDeserializeCodeBuilder}
if (this is global::Alchemy.Serialization.IAlchemySerializationCallbackReceiver receiver) receiver.OnAfterDeserialize();
}}
void global::UnityEngine.ISerializationCallbackReceiver.OnBeforeSerialize()
{{
if (this is global::Alchemy.Serialization.IAlchemySerializationCallbackReceiver receiver) receiver.OnBeforeSerialize();
alchemySerializationData.UnityObjectReferences.Clear();
{onBeforeSerializeCodeBuilder}
}}
[global::System.Serializable]
sealed class AlchemySerializationData
{{
{serializationDataCodeBuilder}
[global::UnityEngine.SerializeField] private global::System.Collections.Generic.List<UnityEngine.Object> unityObjectReferences = new();
public global::System.Collections.Generic.IList<UnityEngine.Object> UnityObjectReferences => unityObjectReferences;
[global::System.Serializable]
public sealed class Item
{{
[global::UnityEngine.HideInInspector] public bool isCreated = false;
[global::UnityEngine.TextArea(1, 999)] public string data;
}}
}}
{serializationDataAttibutesCode} private AlchemySerializationData alchemySerializationData = new();
}}
{(string.IsNullOrEmpty(ns) ? "" : "}")}
";
}
sealed class SyntaxReceiver : ISyntaxReceiver
{
internal static ISyntaxReceiver Create()
{
return new SyntaxReceiver();
}
public List<TypeDeclarationSyntax> TargetTypes { get; } = new();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is TypeDeclarationSyntax typeDeclarationSyntax)
{
var hasAttribute = typeDeclarationSyntax.AttributeLists
.SelectMany(x => x.Attributes)
.Any(x => x.Name.ToString()
is "AlchemySerialize"
or "AlchemySerializeAttribute"
or "Alchemy.Serialization.AlchemySerialize"
or "Alchemy.Serialization.AlchemySerializeAttribute");
if (hasAttribute)
{
TargetTypes.Add(typeDeclarationSyntax);
}
}
}
}
}
}

View File

@ -0,0 +1,4 @@
namespace Alchemy.SourceGenerator
{
}

4
Alchemy/.editorconfig Normal file
View File

@ -0,0 +1,4 @@
[*.cs]
# IDE0027: アクセサーに式本体を使用する
csharp_style_expression_bodied_accessors = false

78
Alchemy/.gitignore vendored Normal file
View File

@ -0,0 +1,78 @@
# This .gitignore file should be placed at the root of your Unity project directory
#
# Get latest from https://github.com/github/gitignore/blob/main/Unity.gitignore
#
/[Ll]ibrary/
/[Tt]emp/
/[Oo]bj/
/[Bb]uild/
/[Bb]uilds/
/[Ll]ogs/
/[Uu]ser[Ss]ettings/
# MemoryCaptures can get excessive in size.
# They also could contain extremely sensitive data
/[Mm]emoryCaptures/
# Recordings can get excessive in size
/[Rr]ecordings/
# Uncomment this line if you wish to ignore the asset store tools plugin
# /[Aa]ssets/AssetStoreTools*
# Autogenerated Jetbrains Rider plugin
/[Aa]ssets/Plugins/Editor/JetBrains*
# Visual Studio cache directory
.vs/
# Gradle cache directory
.gradle/
# Autogenerated VS/MD/Consulo solution and project files
ExportedObj/
.consulo/
*.csproj
*.unityproj
*.sln
*.suo
*.tmp
*.user
*.userprefs
*.pidb
*.booproj
*.svd
*.pdb
*.mdb
*.opendb
*.VC.db
# Unity3D generated meta files
*.pidb.meta
*.pdb.meta
*.mdb.meta
# Unity3D generated file on crash reports
sysinfo.txt
# Builds
*.apk
*.aab
*.unitypackage
*.app
# Crashlytics generated file
crashlytics-build.properties
# Packed Addressables
/[Aa]ssets/[Aa]ddressable[Aa]ssets[Dd]ata/*/*.bin*
# Temporary auto-generated Android Assets
/[Aa]ssets/[Ss]treamingAssets/aa.meta
/[Aa]ssets/[Ss]treamingAssets/aa/*
# Visual Studio Code
.vscode
## DS_Store
*.DS_Store

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4def02a2f2f60434e82ef3fd321ee6be
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 285cbd8c7df70446c901672504e736c2
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,25 @@
{
"name": "Alchemy.Editor",
"rootNamespace": "Alchemy.Editor",
"references": [
"GUID:88be65f96b86746888c927a5c8ff3534",
"GUID:2765e68924a08a94ea0ea66b31c0168f"
],
"includePlatforms": [
"Editor"
],
"excludePlatforms": [],
"allowUnsafeCode": false,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [
{
"name": "com.unity.serialization",
"expression": "",
"define": "ALCHEMY_SUPPORT_SERIALIZATION"
}
],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 5fd453cd0d182422093c4a764fd5eadb
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,71 @@
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Alchemy.Inspector;
using Alchemy.Editor.Internal;
using UnityEditor.UIElements;
#if ALCHEMY_SUPPORT_SERIALIZATION
using Alchemy.Serialization;
#endif
namespace Alchemy.Editor
{
using Editor = UnityEditor.Editor;
/// <summary>
/// Editor base class for Inspector drawing in Alchemy
/// </summary>
public abstract class AlchemyEditor : Editor
{
const string ScriptFieldName = "m_Script";
#if ALCHEMY_SUPPORT_SERIALIZATION
const string AlchemySerializationWarning = "In the current version, fields with the [AlchemySerializedField] attribute do not support editing multiple objects.";
#endif
public override VisualElement CreateInspectorGUI()
{
var root = new VisualElement();
var targetType = target.GetType();
if (targetType.HasCustomAttribute<DisableAlchemyEditorAttribute>())
{
// Create default inspector
InspectorElement.FillDefaultInspector(root, serializedObject, this);
return root;
}
#if ALCHEMY_SUPPORT_SERIALIZATION
if (targetType.HasCustomAttribute<AlchemySerializeAttribute>() && targets.Length > 1)
{
root.Add(new HelpBox(AlchemySerializationWarning, HelpBoxMessageType.Error));
}
#endif
// Add script field
if (targetType.GetCustomAttribute<HideScriptFieldAttribute>() == null)
{
var scriptField = new PropertyField(serializedObject.FindProperty(ScriptFieldName));
scriptField.SetEnabled(false);
root.Add(scriptField);
root.Add(new VisualElement()
{
style = { height = EditorGUIUtility.standardVerticalSpacing * 0.5f }
});
}
// Add elements
InspectorHelper.BuildElements(serializedObject, root, target, name => serializedObject.FindProperty(name), 0);
return root;
}
}
[CustomEditor(typeof(MonoBehaviour), editorForChildClasses: true, isFallback = true)]
[CanEditMultipleObjects]
internal sealed class MonoBehaviourEditor : AlchemyEditor { }
[CustomEditor(typeof(ScriptableObject), editorForChildClasses: true, isFallback = true)]
[CanEditMultipleObjects]
internal sealed class ScriptableObjectEditor : AlchemyEditor { }
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 09b0ba97e97f24db4a0dce5a1e942810
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,222 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEditor;
using UnityEditor.UIElements;
using Alchemy.Inspector;
using Alchemy.Editor.Internal;
using Alchemy.Editor.Elements;
namespace Alchemy.Editor.GroupDrawers
{
[CustomPropertyGroupDrawer(typeof(GroupAttribute))]
public sealed class GroupDrawer : PropertyGroupDrawer
{
public override VisualElement CreateRootElement(string label)
{
return new Box()
{
style = {
width = Length.Percent(100f),
marginTop = 3f,
paddingBottom = 2f,
paddingRight = 1f,
paddingLeft = 1f,
}
};
}
}
[CustomPropertyGroupDrawer(typeof(BoxGroupAttribute))]
public sealed class BoxGroupDrawer : PropertyGroupDrawer
{
public override VisualElement CreateRootElement(string label)
{
var helpBox = new HelpBox()
{
text = label,
style = {
flexDirection = FlexDirection.Column,
width = Length.Percent(100f),
marginTop = 3f,
paddingBottom = 3f,
paddingRight = 3f,
paddingLeft = 3f,
}
};
var labelElement = helpBox.Q<Label>();
labelElement.style.top = 2f;
labelElement.style.left = 2f;
labelElement.style.fontSize = 12f;
labelElement.style.minHeight = EditorGUIUtility.singleLineHeight;
labelElement.style.unityFontStyleAndWeight = FontStyle.Bold;
labelElement.style.alignSelf = Align.Stretch;
return helpBox;
}
}
[CustomPropertyGroupDrawer(typeof(TabGroupAttribute))]
public sealed class TabGroupDrawer : PropertyGroupDrawer
{
VisualElement rootElement;
readonly Dictionary<string, VisualElement> tabElements = new();
string[] keyArrayCache = new string[0];
int tabIndex;
int prevTabIndex;
sealed class TabItem
{
public string name;
public VisualElement element;
}
public override VisualElement CreateRootElement(string label)
{
var configKey = UniqueId + "_TabGroup";
int.TryParse(EditorUserSettings.GetConfigValue(configKey), out tabIndex);
prevTabIndex = tabIndex;
rootElement = new HelpBox()
{
style = {
flexDirection = FlexDirection.Column,
width = Length.Percent(100f),
marginTop = 3f,
paddingBottom = 3f,
paddingRight = 3f,
paddingLeft = 3f,
}
};
rootElement.Remove(rootElement.Q<Label>());
var tabGUIElement = new IMGUIContainer(() =>
{
var rect = EditorGUILayout.GetControlRect();
rect.xMin -= 3.7f;
rect.xMax += 3.7f;
rect.yMin -= 3.7f;
rect.yMax -= 1f;
tabIndex = GUI.Toolbar(rect, tabIndex, keyArrayCache);
if (tabIndex != prevTabIndex)
{
EditorUserSettings.SetConfigValue(configKey, tabIndex.ToString());
prevTabIndex = tabIndex;
}
foreach (var kv in tabElements)
{
kv.Value.style.display = keyArrayCache[tabIndex] == kv.Key ? DisplayStyle.Flex : DisplayStyle.None;
}
})
{
style = {
width = Length.Percent(100f),
marginLeft = 0f,
marginRight = 0f,
marginTop = 0f
}
};
rootElement.Add(tabGUIElement);
return rootElement;
}
public override VisualElement GetGroupElement(Attribute attribute)
{
var tabGroupAttribute = (TabGroupAttribute)attribute;
var tabName = tabGroupAttribute.TabName;
if (!tabElements.TryGetValue(tabName, out var element))
{
element = new VisualElement()
{
style = {
width = Length.Percent(100f)
}
};
rootElement.Add(element);
tabElements.Add(tabName, element);
keyArrayCache = tabElements.Keys.ToArray();
}
return element;
}
}
[CustomPropertyGroupDrawer(typeof(FoldoutGroupAttribute))]
public sealed class FoldoutGroupDrawer : PropertyGroupDrawer
{
public override VisualElement CreateRootElement(string label)
{
var configKey = UniqueId + "_FoldoutGroup";
bool.TryParse(EditorUserSettings.GetConfigValue(configKey), out var result);
var foldout = new Foldout()
{
style = {
width = Length.Percent(100f)
},
text = label,
value = result
};
foldout.RegisterValueChangedCallback(x =>
{
EditorUserSettings.SetConfigValue(configKey, x.newValue.ToString());
});
return foldout;
}
}
[CustomPropertyGroupDrawer(typeof(HorizontalGroupAttribute))]
public sealed class HorizontalGroupDrawer : PropertyGroupDrawer
{
public override VisualElement CreateRootElement(string label)
{
var root = new VisualElement()
{
style = {
width = Length.Percent(100f),
flexDirection = FlexDirection.Row
}
};
static void AdjustLabel(PropertyField element, VisualElement inspector, int childCount)
{
if (element.childCount == 0) return;
if (element.Q<Foldout>() != null) return;
var field = element[0];
field.RemoveFromClassList("unity-base-field__aligned");
var labelElement = field.Q<Label>();
labelElement.style.minWidth = 0f;
labelElement.style.width = GUIHelper.CalculateLabelWidth(element, inspector) * 0.8f / childCount;
}
root.schedule.Execute(() =>
{
if (root.childCount <= 1) return;
var inspector = root.GetFirstAncestorOfType<InspectorElement>();
foreach (var field in root.Query<PropertyField>().Build())
{
AdjustLabel(field, inspector, root.childCount);
}
foreach (var field in root.Query<GenericField>().Children<PropertyField>().Build())
{
AdjustLabel(field, inspector, root.childCount);
}
});
return root;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fa0ce00655a224efd9ed552633ccd715
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,306 @@
using UnityEngine;
using UnityEngine.UIElements;
using Alchemy.Inspector;
using Alchemy.Editor.Internal;
using Alchemy.Editor.Elements;
using UnityEditor;
namespace Alchemy.Editor.Processors
{
[CustomPropertyProcessor(typeof(ReadOnlyAttribute))]
public sealed class ReadOnlyProcessor : PropertyProcessor
{
public override void Execute()
{
Element.SetEnabled(false);
}
}
[CustomPropertyProcessor(typeof(IndentAttribute))]
public sealed class IndentProcessor : PropertyProcessor
{
const float IndentPadding = 15f;
public override void Execute()
{
Element.RegisterCallback<GeometryChangedEvent>(x => AddPadding());
}
void AddPadding()
{
var label = Element.Q<Label>();
if (label == null) return;
label.style.paddingLeft = ((IndentAttribute)Attribute).indent * IndentPadding;
}
}
[CustomPropertyProcessor(typeof(HideInPlayModeAttribute))]
public sealed class HideInPlayModeProcessor : PropertyProcessor
{
public override void Execute()
{
Element.style.display = Application.isPlaying ? DisplayStyle.None : DisplayStyle.Flex;
}
}
[CustomPropertyProcessor(typeof(HideInEditModeAttribute))]
public sealed class HideInEditModeProcessor : PropertyProcessor
{
public override void Execute()
{
Element.style.display = !Application.isPlaying ? DisplayStyle.None : DisplayStyle.Flex;
}
}
[CustomPropertyProcessor(typeof(DisableInPlayModeAttribute))]
public sealed class DisableInPlayModeProcessor : PropertyProcessor
{
public override void Execute()
{
if (Application.isPlaying) Element.SetEnabled(false);
}
}
[CustomPropertyProcessor(typeof(DisableInEditModeAttribute))]
public sealed class DisableInEditModeProcessor : PropertyProcessor
{
public override void Execute()
{
if (!Application.isPlaying) Element.SetEnabled(false);
}
}
[CustomPropertyProcessor(typeof(HideLabelAttribute))]
public sealed class HideLabelProcessor : PropertyProcessor
{
public override void Execute()
{
if (Element is AlchemyPropertyField field)
{
field.Label = string.Empty;
return;
}
var labelElement = Element.Q<Label>();
if (labelElement == null) return;
labelElement.text = string.Empty;
}
}
[CustomPropertyProcessor(typeof(LabelTextAttribute))]
public sealed class LabelTextProcessor : PropertyProcessor
{
public override void Execute()
{
var labelTextAttribute = (LabelTextAttribute)Attribute;
switch (Element)
{
case AlchemyPropertyField alchemyPropertyField:
alchemyPropertyField.Label = labelTextAttribute.Text;
break;
case Button button:
button.text = labelTextAttribute.Text;
break;
default:
var labelElement = Element.Q<Label>();
if (labelElement == null) return;
labelElement.text = labelElement.text;
break;
}
}
}
[CustomPropertyProcessor(typeof(HideIfAttribute))]
public sealed class HideIfProcessor : TrackSerializedObjectPropertyProcessor
{
protected override void OnInspectorChanged()
{
var condition = ReflectionHelper.GetValueBool(Target, ((HideIfAttribute)Attribute).Condition);
Element.style.display = condition ? DisplayStyle.None : DisplayStyle.Flex;
}
}
[CustomPropertyProcessor(typeof(ShowIfAttribute))]
public sealed class ShowIfProcessor : TrackSerializedObjectPropertyProcessor
{
protected override void OnInspectorChanged()
{
var condition = ReflectionHelper.GetValueBool(Target, ((ShowIfAttribute)Attribute).Condition);
Element.style.display = !condition ? DisplayStyle.None : DisplayStyle.Flex;
}
}
[CustomPropertyProcessor(typeof(DisableIfAttribute))]
public sealed class DisableIfProcessor : TrackSerializedObjectPropertyProcessor
{
protected override void OnInspectorChanged()
{
var condition = ReflectionHelper.GetValueBool(Target, ((DisableIfAttribute)Attribute).Condition);
Element.SetEnabled(!condition);
}
}
[CustomPropertyProcessor(typeof(EnableIfAttribute))]
public sealed class EnableIfProcessor : TrackSerializedObjectPropertyProcessor
{
protected override void OnInspectorChanged()
{
var condition = ReflectionHelper.GetValueBool(Target, ((EnableIfAttribute)Attribute).Condition);
Element.SetEnabled(condition);
}
}
[CustomPropertyProcessor(typeof(RequiredAttribute))]
public sealed class RequiredProcessor : TrackSerializedObjectPropertyProcessor
{
HelpBox helpBox;
public override void Execute()
{
if (SerializedProperty.propertyType != SerializedPropertyType.ObjectReference) return;
var message = ((RequiredAttribute)Attribute).Message ?? ObjectNames.NicifyVariableName(SerializedProperty.displayName) + " is required.";
helpBox = new HelpBox(message, HelpBoxMessageType.Error);
var parent = Element.parent;
parent.Insert(parent.IndexOf(Element), helpBox);
base.Execute();
}
protected override void OnInspectorChanged()
{
helpBox.style.display = SerializedProperty.objectReferenceValue != null ? DisplayStyle.None : DisplayStyle.Flex;
}
}
[CustomPropertyProcessor(typeof(ValidateInputAttribute))]
public sealed class ValidateInputProcessor : TrackSerializedObjectPropertyProcessor
{
HelpBox helpBox;
public override void Execute()
{
var message = ((ValidateInputAttribute)Attribute).Message ?? ObjectNames.NicifyVariableName(SerializedProperty.displayName) + " is not valid.";
helpBox = new HelpBox(message, HelpBoxMessageType.Error);
var parent = Element.parent;
parent.Insert(parent.IndexOf(Element), helpBox);
base.Execute();
}
protected override void OnInspectorChanged()
{
var result = ReflectionHelper.Invoke(Target, ((ValidateInputAttribute)Attribute).Condition, SerializedProperty.GetValue<object>());
helpBox.style.display = result is bool flag && flag ? DisplayStyle.None : DisplayStyle.Flex;
}
}
[CustomPropertyProcessor(typeof(HelpBoxAttribute))]
public sealed class HelpBoxProcessor : PropertyProcessor
{
HelpBox helpBox;
public override void Execute()
{
var att = (HelpBoxAttribute)Attribute;
helpBox = new HelpBox(att.Message, att.MessageType);
var parent = Element.parent;
parent.Insert(parent.IndexOf(Element), helpBox);
}
}
[CustomPropertyProcessor(typeof(HorizontalLineAttribute))]
public sealed class HorizontalLineProcessor : PropertyProcessor
{
public override void Execute()
{
var att = (HorizontalLineAttribute)Attribute;
var parent = Element.parent;
var line = GUIHelper.CreateLine(att.Color, EditorGUIUtility.standardVerticalSpacing * 4f);
parent.Insert(parent.IndexOf(Element), line);
}
}
[CustomPropertyProcessor(typeof(TitleAttribute))]
public sealed class TitleProcessor : PropertyProcessor
{
public override void Execute()
{
var att = (TitleAttribute)Attribute;
var parent = Element.parent;
var title = new Label(att.TitleText)
{
style = {
unityFontStyleAndWeight = FontStyle.Bold,
paddingLeft = 3f,
marginTop = 4f,
marginBottom = -2f
}
};
parent.Insert(parent.IndexOf(Element), title);
if (att.SubitleText != null)
{
var subtitle = new Label(att.SubitleText)
{
style = {
fontSize = 10f,
paddingLeft = 4.5f,
marginTop = 1.5f,
color = GUIHelper.SubtitleColor,
unityTextAlign = TextAnchor.MiddleLeft
}
};
parent.Insert(parent.IndexOf(Element), subtitle);
}
var line = GUIHelper.CreateLine(GUIHelper.LineColor, EditorGUIUtility.standardVerticalSpacing * 3f);
parent.Insert(parent.IndexOf(Element), line);
}
}
[CustomPropertyProcessor(typeof(BlockquoteAttribute))]
public sealed class BlockquoteProcessor : PropertyProcessor
{
public BlockquoteProcessor()
{
textStyle = EditorStyles.label;
textStyle.wordWrap = true;
}
readonly GUIStyle textStyle;
public override void Execute()
{
var att = (BlockquoteAttribute)Attribute;
var blockquote = new IMGUIContainer(() =>
{
var width = EditorGUIUtility.currentViewWidth;
var labelContent = new GUIContent(att.Text);
var labelHeight = textStyle.CalcHeight(labelContent, width - 3f);
var position = EditorGUILayout.GetControlRect(false, labelHeight + EditorGUIUtility.standardVerticalSpacing * 2f);
var blockRect = position;
var backgroundColor = GUIHelper.TextColor;
backgroundColor.a = 0.06f;
EditorGUI.DrawRect(blockRect, backgroundColor);
blockRect.x = position.xMin;
blockRect.width = 3;
EditorGUI.DrawRect(blockRect, GUIHelper.TextColor);
var labelPosition = position;
labelPosition.xMin += 7f;
EditorGUI.LabelField(labelPosition, labelContent, textStyle);
});
var parent = Element.parent;
parent.Insert(parent.IndexOf(Element), blockquote);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 82c33c1d3a21f4e9a9e57fa46d4900aa
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,13 @@
using System;
namespace Alchemy.Editor
{
public sealed class CustomPropertyGroupDrawerAttribute : Attribute
{
public CustomPropertyGroupDrawerAttribute(Type targetAttributeType)
{
this.targetAttributeType = targetAttributeType;
}
public readonly Type targetAttributeType;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f0c6d37c5a83b4eef8f5359c67329e56
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,16 @@
using System;
namespace Alchemy.Editor
{
public sealed class CustomPropertyProcessorAttribute : Attribute
{
public CustomPropertyProcessorAttribute(Type targetAttributeType, int order = 0)
{
this.targetAttributeType = targetAttributeType;
this.order = order;
}
public readonly Type targetAttributeType;
public readonly int order;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e5369cccdbb3a47c0ac7e5fcff993276
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 4dc283a9b881942e6bcea33ae81ca853
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,94 @@
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine.UIElements;
using Alchemy.Editor.Internal;
using Alchemy.Inspector;
using UnityEngine;
namespace Alchemy.Editor.Elements
{
/// <summary>
/// Visual Element that draws properties based on Alchemy attributes
/// </summary>
public sealed class AlchemyPropertyField : BindableElement
{
public AlchemyPropertyField(SerializedProperty property, int depth)
{
if (depth > 20) return;
var labelText = ObjectNames.NicifyVariableName(property.displayName);
switch (property.propertyType)
{
default:
element = new PropertyField(property);
break;
case SerializedPropertyType.ObjectReference:
if (property.GetAttribute<InlineEditorAttribute>() != null)
{
element = new InlineEditorObjectField(property, depth);
}
else
{
element = GUIHelper.CreateObjectField(property);
}
break;
case SerializedPropertyType.Generic:
if (property.isArray)
{
element = new PropertyListView(property, depth);
}
else
{
var targetType = property.GetPropertyType();
var foldout = new Foldout() { text = labelText };
var clickable = InternalAPIHelper.GetClickable(foldout.Q<Toggle>());
InternalAPIHelper.SetAcceptClicksIfDisabled(clickable, true);
InspectorHelper.BuildElements(property.serializedObject, foldout, property.GetValue<object>(), name => property.FindPropertyRelative(name), depth + 1);
foldout.BindProperty(property);
element = foldout;
}
break;
case SerializedPropertyType.ManagedReference:
element = new SerializeReferenceField(property, depth);
break;
}
Add(element);
}
readonly VisualElement element;
public string Label
{
get
{
return element switch
{
Foldout foldout => foldout.text,
PropertyField propertyField => propertyField.label,
SerializeReferenceField serializeReferenceField => serializeReferenceField.foldout.text,
InlineEditorObjectField inlineEditorObjectField => inlineEditorObjectField.Label,
_ => null,
};
}
set
{
switch (element)
{
case Foldout foldout:
foldout.text = value;
break;
case PropertyField propertyField:
propertyField.label = value;
break;
case SerializeReferenceField serializeReferenceField:
serializeReferenceField.foldout.text = value;
break;
case InlineEditorObjectField inlineEditorObjectField:
inlineEditorObjectField.Label = value;
break;
};
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 90a3b3c6a5e434218b95ca18fd032a79
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,68 @@
using System;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine.UIElements;
using Alchemy.Editor.Internal;
using Alchemy.Inspector;
namespace Alchemy.Editor.Elements
{
public sealed class ClassField : VisualElement
{
public ClassField(Type type, string label, int depth) : this(TypeHelper.CreateDefaultInstance(type), type, label, depth) { }
public ClassField(object obj, Type type, string label, int depth)
{
if (depth > InspectorHelper.MaxDepth) return;
var foldout = new Foldout
{
text = label,
value = false
};
var toggle = foldout.Q<Toggle>();
var clickable = InternalAPIHelper.GetClickable(toggle);
InternalAPIHelper.SetAcceptClicksIfDisabled(clickable, true);
// Build node
var rootNode = InspectorHelper.BuildInspectorNode(type);
// Add elements
foreach (var node in rootNode.DescendantsAndSelf())
{
// Get or create group element
if (node.Parent == null)
{
node.VisualElement = foldout;
}
else if (node.Drawer == null)
{
node.VisualElement = node.Parent.VisualElement;
}
else
{
node.VisualElement = node.Drawer.CreateRootElement(node.Name);
node.Parent.VisualElement.Add(node.VisualElement);
}
// Add member elements
foreach (var member in node.Members.OrderByAttributeThenByMemberType())
{
var element = new ReflectionField(obj, member, depth + 1);
element.style.width = Length.Percent(100f);
element.OnValueChanged += x => OnValueChanged?.Invoke(obj);
var e = node.Drawer?.GetGroupElement(member.GetCustomAttribute<PropertyGroupAttribute>());
if (e == null) node.VisualElement.Add(element);
else e.Add(element);
PropertyProcessor.ExecuteProcessors(null, null, obj, member, element);
}
}
Add(foldout);
}
public event Action<object> OnValueChanged;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6b645f1746ed04e3dbb48f5e2568f401
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,165 @@
using System;
using System.Collections.Generic;
using Alchemy.Editor.Internal;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Alchemy.Editor.Elements
{
public sealed class DictionaryField : HashMapFieldBase
{
public DictionaryField(object collection, string label, int depth) : base(collection, label, depth)
{
if (collection != null)
{
keyType = collection.GetType().GenericTypeArguments[0];
valueType = collection.GetType().GenericTypeArguments[1];
kvType = typeof(KeyValuePair<,>).MakeGenericType(keyType, valueType);
}
}
public override string CollectionTypeName => TypeName;
const string TypeName = "Dictionary";
const string KeyName = "Key";
const string ValueName = "Value";
readonly Type keyType;
readonly Type valueType;
readonly Type kvType;
public override bool CheckElement(object element)
{
var keyObj = ReflectionHelper.GetPropertyValue(element, element.GetType(), KeyName);
if (keyObj is string str && string.IsNullOrEmpty(str)) return true;
if (keyObj == null) return true;
return (bool)ReflectionHelper.Invoke(Collection, "ContainsKey", keyObj);
}
public override object CreateElement()
{
var keyObj = TypeHelper.CreateDefaultInstance(keyType);
var valueObj = TypeHelper.CreateDefaultInstance(valueType);
return Activator.CreateInstance(kvType, keyObj, valueObj);
}
public override void AddElement(object element)
{
var keyObj = ReflectionHelper.GetPropertyValue(element, kvType, KeyName);
var valueObj = ReflectionHelper.GetPropertyValue(element, kvType, ValueName);
ReflectionHelper.Invoke(Collection, "Add", keyObj, valueObj);
}
public override bool RemoveElement(object element)
{
return (bool)ReflectionHelper.Invoke(Collection, "Remove", ReflectionHelper.GetPropertyValue(element, kvType, KeyName));
}
public override void ClearElements()
{
ReflectionHelper.Invoke(Collection, "Clear");
}
public override HashMapItemBase CreateItem(object collection, object elementObj, string label, int depth)
{
return new Item(collection, elementObj, depth + 1);
}
public sealed class Item : HashMapItemBase
{
public Item(object collection, object keyValuePair, int depth)
{
var box = new Box()
{
style = {
marginBottom = 3.5f,
marginRight = -2f,
flexDirection = FlexDirection.Row
}
};
kvType = keyValuePair.GetType();
var keyType = kvType.GenericTypeArguments[0];
var valueType = kvType.GenericTypeArguments[1];
key = ReflectionHelper.GetPropertyValue(keyValuePair, kvType, KeyName);
value = ReflectionHelper.GetPropertyValue(keyValuePair, kvType, ValueName);
this.collection = collection;
this.keyValuePair = keyValuePair;
var keyValueElement = new VisualElement()
{
style = {
flexDirection = FlexDirection.Column,
flexGrow = 1f
}
};
box.Add(keyValueElement);
keyField = new GenericField(key, keyType, KeyName, depth)
{
style = { flexGrow = 1f }
};
keyField.OnValueChanged += SetKey;
keyValueElement.Add(keyField);
valueField = new GenericField(value, valueType, ValueName, depth)
{
style = { flexGrow = 1f }
};
valueField.OnValueChanged += SetValue;
keyValueElement.Add(valueField);
var closeButton = new Button(() => OnClose?.Invoke())
{
style = {
width = EditorGUIUtility.singleLineHeight,
height = EditorGUIUtility.singleLineHeight,
unityFontStyleAndWeight = FontStyle.Bold,
fontSize = 10f
},
text = "X",
};
box.Add(closeButton);
Add(box);
}
readonly GenericField keyField;
readonly GenericField valueField;
readonly object collection;
public override object Value => keyValuePair;
object key;
object value;
object keyValuePair;
readonly Type kvType;
public override void Lock()
{
keyField.SetEnabled(false);
valueField.OnValueChanged -= SetValue;
valueField.OnValueChanged += x =>
{
ReflectionHelper.GetProperty(collection.GetType(), "Item").SetValue(collection, x, new object[] { key });
};
}
void SetKey(object obj)
{
key = obj;
keyValuePair = Activator.CreateInstance(kvType, key, value);
OnValueChanged?.Invoke(keyValuePair);
}
void SetValue(object obj)
{
value = obj;
keyValuePair = Activator.CreateInstance(kvType, key, value);
OnValueChanged?.Invoke(keyValuePair);
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ef13233afbf8a44f68f8641b03e2303f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,230 @@
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using Alchemy.Editor.Internal;
namespace Alchemy.Editor.Elements
{
/// <summary>
/// Visual Element that creates a suitable input Field from object type
/// </summary>
public sealed class GenericField : VisualElement
{
public GenericField(object obj, Type type, string label, int depth, bool isDelayed = false)
{
Build(obj, type, label, depth, isDelayed);
GUIHelper.ScheduleAdjustLabelWidth(this);
}
void Build(object obj, Type type, string label, int depth, bool isDelayed)
{
if (depth > InspectorHelper.MaxDepth) return;
Clear();
if (obj == null && !typeof(UnityEngine.Object).IsAssignableFrom(type))
{
var nullLabelElement = new VisualElement()
{
style = {
width = Length.Percent(100f),
height = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing,
paddingLeft = 3f,
flexDirection = FlexDirection.Row
}
};
nullLabelElement.Add(new Label(label + " (Null)")
{
style = {
flexGrow = 1f,
unityTextAlign = TextAnchor.MiddleLeft
}
});
// TODO: support polymorphism
if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Nullable<>))
{
nullLabelElement.Add(new Button(() =>
{
var instance = Activator.CreateInstance(type, Activator.CreateInstance(type.GenericTypeArguments[0]));
Build(instance, type, label, depth, isDelayed);
OnValueChanged?.Invoke(instance);
})
{
text = "Create..."
});
}
else if (TypeHelper.HasDefaultConstructor(type))
{
nullLabelElement.Add(new Button(() =>
{
var instance = TypeHelper.CreateDefaultInstance(type);
Build(instance, type, label, depth, isDelayed);
OnValueChanged?.Invoke(instance);
})
{
text = "Create..."
});
}
Add(nullLabelElement);
return;
}
this.isDelayed = isDelayed;
if (type == typeof(bool))
{
AddField(new Toggle(label), (bool)obj);
}
else if (type == typeof(int))
{
AddField(new IntegerField(label), (int)obj);
}
else if (type == typeof(uint))
{
AddField(new UnsignedIntegerField(label), (uint)obj);
}
else if (type == typeof(long))
{
AddField(new LongField(label), (long)obj);
}
else if (type == typeof(ulong))
{
AddField(new UnsignedLongField(label), (ulong)obj);
}
else if (type == typeof(float))
{
AddField(new FloatField(label), (float)obj);
}
else if (type == typeof(double) || type == typeof(decimal))
{
AddField(new DoubleField(label), (double)obj);
}
else if (type == typeof(string))
{
AddField(new TextField(label), (string)obj);
}
else if (type == typeof(char))
{
var charField = new TextField(label, 1, false, false, default) { value = obj.ToString() };
charField.RegisterValueChangedCallback(x => OnValueChanged?.Invoke(x.newValue[0]));
Add(charField);
}
else if (type.IsEnum)
{
var value = (Enum)obj;
if (value.GetType().HasCustomAttribute<FlagsAttribute>()) AddField(new EnumFlagsField(label, value), value);
else AddField(new EnumField(label, value), value);
}
else if (type == typeof(Vector2))
{
AddField(new Vector2Field(label), (Vector2)obj);
}
else if (type == typeof(Vector2Int))
{
AddField(new Vector2IntField(label), (Vector2Int)obj);
}
else if (type == typeof(Vector3))
{
AddField(new Vector3Field(label), (Vector3)obj);
}
else if (type == typeof(Vector3Int))
{
AddField(new Vector3IntField(label), (Vector3Int)obj);
}
else if (type == typeof(Vector4))
{
AddField(new Vector4Field(label), (Vector4)obj);
}
else if (type == typeof(Rect))
{
AddField(new RectField(label), (Rect)obj);
}
else if (type == typeof(RectInt))
{
AddField(new RectIntField(label), (RectInt)obj);
}
else if (type == typeof(Bounds))
{
AddField(new BoundsField(label), (Bounds)obj);
}
else if (type == typeof(BoundsInt))
{
AddField(new BoundsIntField(label), (BoundsInt)obj);
}
else if (type == typeof(Color))
{
AddField(new ColorField(label), (Color)obj);
}
else if (type == typeof(Gradient))
{
AddField(new GradientField(label), (Gradient)obj);
}
else if (type == typeof(Hash128))
{
AddField(new Hash128Field(label), (Hash128)obj);
}
else if (typeof(UnityEngine.Object).IsAssignableFrom(type))
{
AddField(new ObjectField(label) { objectType = type }, (UnityEngine.Object)obj);
}
else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(HashSet<>))
{
var field = new HashSetField(obj, label, depth + 1);
field.OnValueChanged += x => OnValueChanged?.Invoke(x);
Add(field);
}
else if (type.IsGenericType && type.GetGenericTypeDefinition() == typeof(Dictionary<,>))
{
var field = new DictionaryField(obj, label, depth + 1);
field.OnValueChanged += x => OnValueChanged?.Invoke(x);
Add(field);
}
else if (typeof(IList).IsAssignableFrom(type))
{
var field = new ListField((IList)obj, label, depth + 1);
field.OnValueChanged += x => OnValueChanged?.Invoke(x);
Add(field);
}
else
{
var field = new ClassField(obj, type, label, depth + 1);
field.OnValueChanged += x => OnValueChanged?.Invoke(x);
Add(field);
}
}
public event Action<object> OnValueChanged;
bool isDelayed;
bool changed;
void AddField<T>(BaseField<T> control, T value)
{
control.value = value;
if (isDelayed && control is not ObjectField) // ignore ObjectField
{
control.RegisterValueChangedCallback(x => changed = true);
control.RegisterCallback<FocusOutEvent>(x =>
{
if (changed)
{
OnValueChanged?.Invoke(control.value);
changed = false;
}
});
}
else
{
control.RegisterValueChangedCallback(x => OnValueChanged?.Invoke(x.newValue));
}
Add(control);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5d48be02c25cf4bc0be72b50cae0a0c7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,171 @@
using System;
using System.Collections;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Alchemy.Editor.Elements
{
public abstract class HashMapFieldBase : VisualElement
{
public HashMapFieldBase(object collection, string label, int depth)
{
this.depth = depth;
this.collection = collection;
var foldout = new Foldout()
{
text = label
};
Add(foldout);
foldout.Q<Label>().style.unityFontStyleAndWeight = FontStyle.Bold;
contents = new();
foldout.Add(contents);
inputForm = new();
foldout.Add(inputForm);
addButton = new Button(() =>
{
if (isInputting) EndInput();
else StartInput();
})
{
style = {
alignSelf = Align.FlexEnd,
minWidth = 60f
},
text = "+ Add"
};
foldout.Add(addButton);
foldout.Add(new VisualElement() { style = { minHeight = 4f } });
Rebuild();
}
readonly VisualElement inputForm;
readonly VisualElement contents;
readonly Button addButton;
public object Collection => collection;
readonly object collection;
readonly int depth;
bool isInputting;
public event Action<object> OnValueChanged;
public void RegisterOnValueChangedCallback(Action<object> callback)
{
OnValueChanged += callback;
}
void StartInput()
{
void ValidateValue(object x)
{
var contains = CheckElement(x);
addButton.SetEnabled(!contains);
addButton.text = contains ? "(Invalid key)" : "Done";
}
if (isInputting) return;
isInputting = true;
var initValue = CreateElement();
var form = CreateItem(collection, initValue, "New Value", depth);
inputForm.Clear();
inputForm.Add(form);
form.OnValueChanged += ValidateValue;
form.OnClose += () =>
{
CancelInput();
};
ValidateValue(initValue);
}
void EndInput()
{
if (!isInputting) return;
isInputting = false;
addButton.text = "+ Add";
addButton.SetEnabled(true);
AddElement(inputForm.Q<HashMapItemBase>().Value);
OnValueChanged?.Invoke(collection);
Rebuild();
}
void CancelInput()
{
isInputting = false;
addButton.text = "+ Add";
addButton.SetEnabled(true);
Rebuild();
}
// Rebuild GUI contents
public void Rebuild()
{
contents.Clear();
inputForm.Clear();
if (collection == null) return;
var i = 0;
foreach (var item in (IEnumerable)collection)
{
var element = CreateItem(collection, item, "Element " + i, depth);
element.OnClose += () =>
{
if (isInputting) return;
var remove = RemoveElement(item);
if (remove)
{
OnValueChanged?.Invoke(collection);
Rebuild();
}
};
element.Lock();
contents.Add(element);
i++;
}
if (i == 0)
{
var box = new Box();
box.style.minHeight = EditorGUIUtility.singleLineHeight * 1.2f;
box.style.paddingLeft = 6f;
var label = new Label(CollectionTypeName + " is empty.");
label.style.height = Length.Percent(100f);
label.style.unityTextAlign = TextAnchor.MiddleLeft;
box.Add(label);
inputForm.Add(box);
}
}
public abstract HashMapItemBase CreateItem(object collection, object elementObj, string label, int depth);
public abstract bool CheckElement(object element);
public abstract object CreateElement();
public abstract void AddElement(object element);
public abstract bool RemoveElement(object element);
public abstract void ClearElements();
public abstract string CollectionTypeName { get; }
public abstract class HashMapItemBase : VisualElement
{
public Action OnClose;
public Action<object> OnValueChanged;
public abstract object Value { get; }
public abstract void Lock();
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4458165df128342d3a2a50b74dcc8c47
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,93 @@
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Alchemy.Editor.Internal;
namespace Alchemy.Editor.Elements
{
public sealed class HashSetField : HashMapFieldBase
{
public HashSetField(object collection, string label, int depth) : base(collection, label, depth) { }
public override string CollectionTypeName => "HashSet";
public override bool CheckElement(object key)
{
return (bool)ReflectionHelper.Invoke(Collection, "Contains", key);
}
public override object CreateElement()
{
return TypeHelper.CreateDefaultInstance(Collection.GetType().GenericTypeArguments[0]);
}
public override void AddElement(object element)
{
ReflectionHelper.Invoke(Collection, "Add", element);
}
public override bool RemoveElement(object element)
{
return (bool)ReflectionHelper.Invoke(Collection, "Remove", element);
}
public override void ClearElements()
{
ReflectionHelper.Invoke(Collection, "Clear");
}
public override HashMapItemBase CreateItem(object collection, object elementObj, string label, int depth)
{
return new Item(collection, elementObj, label, depth);
}
public sealed class Item : HashMapItemBase
{
public Item(object collection, object elementObj, string label, int depth)
{
var box = new Box()
{
style = {
marginBottom = 3.5f,
marginRight = -2f,
flexDirection = FlexDirection.Row
}
};
var valueType = elementObj == null ? collection.GetType().GenericTypeArguments[0] : elementObj.GetType();
inputField = new GenericField(elementObj, valueType, label, depth);
inputField.style.flexGrow = 1f;
inputField.OnValueChanged += x =>
{
value = x;
OnValueChanged?.Invoke(x);
};
box.Add(inputField);
var closeButton = new Button(() => OnClose?.Invoke())
{
style = {
width = EditorGUIUtility.singleLineHeight,
height = EditorGUIUtility.singleLineHeight,
unityFontStyleAndWeight = FontStyle.Bold,
fontSize = 10f
},
text = "X",
};
box.Add(closeButton);
Add(box);
}
readonly GenericField inputField;
public override void Lock()
{
inputField.SetEnabled(false);
}
object value;
public override object Value => value;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ba0b1c41824514728bc50f6ff3d34315
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,95 @@
using UnityEngine.Assertions;
using UnityEngine.UIElements;
using UnityEditor;
using UnityEditor.UIElements;
using Alchemy.Editor.Internal;
namespace Alchemy.Editor.Elements
{
/// <summary>
/// Visual Element that draws the ObjectField of the InlineEditor attribute
/// </summary>
public sealed class InlineEditorObjectField : BindableElement
{
public InlineEditorObjectField(SerializedProperty property, int depth)
{
Assert.IsTrue(property.propertyType == SerializedPropertyType.ObjectReference);
style.minHeight = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
foldout = new Foldout()
{
text = ObjectNames.NicifyVariableName(property.displayName)
};
var toggle = foldout.Q<Toggle>();
var clickable = InternalAPIHelper.GetClickable(toggle);
InternalAPIHelper.SetAcceptClicksIfDisabled(clickable, true);
foldout.BindProperty(property);
field = GUIHelper.CreateObjectField(property);
field.style.position = Position.Absolute;
field.style.width = Length.Percent(100f);
field.RegisterValueChangeCallback(x =>
{
isNull = x.changedProperty.objectReferenceValue == null;
if (!isNull) field.Q<Label>().text = string.Empty;
else field.Q<Label>().text = ObjectNames.NicifyVariableName(property.displayName);
field.pickingMode = PickingMode.Ignore;
var objectField = field.Q<ObjectField>();
objectField.pickingMode = PickingMode.Ignore;
var label = objectField.Q<Label>();
label.pickingMode = PickingMode.Ignore;
Build(x.changedProperty, depth);
});
Add(foldout);
Add(field);
Build(property, depth);
}
readonly Foldout foldout;
readonly PropertyField field;
bool isNull;
public bool IsObjectNull => isNull;
public string Label
{
get
{
if (isNull) return field.Q<Label>().text;
else return foldout.text;
}
set
{
if (isNull) field.Q<Label>().text = value;
else foldout.text = value;
}
}
void Build(SerializedProperty property, int depth)
{
foldout.Clear();
var toggle = foldout.Q<Toggle>();
isNull = property.objectReferenceValue == null;
toggle.style.display = isNull ? DisplayStyle.None : DisplayStyle.Flex;
if (!isNull)
{
foldout.Add(new VisualElement() { style = { height = EditorGUIUtility.standardVerticalSpacing } });
var so = new SerializedObject(property.objectReferenceValue);
InspectorHelper.BuildElements(so, foldout, so.targetObject, name => so.FindProperty(name), depth);
this.Bind(so);
}
else
{
this.Unbind();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c644baa37186c4bbe9c006dee446d9a7
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,74 @@
using System;
using System.Collections;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.Assertions;
using Alchemy.Editor.Internal;
namespace Alchemy.Editor.Elements
{
/// <summary>
/// Visual Element that draws an IList
/// </summary>
public sealed class ListField : VisualElement
{
sealed class Item : VisualElement
{
public int index;
}
const string ItemClassName = "unity-list-view__item";
public ListField(IList target, string label, int depth)
{
Assert.IsNotNull(target);
list = target;
listView = GUIHelper.CreateDefaultListView(label);
listView.makeItem = () => new Item();
listView.bindItem = (element, index) =>
{
((Item)element).index = index;
var value = list[index];
var listType = list.GetType();
var valueType = value != null ? value.GetType() : listType.IsGenericType ? listType.GenericTypeArguments[0] : typeof(object);
var fieldElement = new GenericField(value, valueType, label, depth);
element.Add(fieldElement);
var labelElement = fieldElement.Q<Label>();
if (labelElement != null) labelElement.text = "Element " + index;
fieldElement.OnValueChanged += x =>
{
list[((Item)element).index] = x;
NotifyOnValueChanged();
};
};
listView.unbindItem = (element, index) =>
{
element.Clear();
};
listView.itemsSource = list;
listView.itemIndexChanged += (prevIndex, index) =>
{
var item = listView.Query<VisualElement>(className: ItemClassName).AtIndex(index).Q<Item>();
item.index = index;
NotifyOnValueChanged();
};
listView.itemsAdded += indexes => NotifyOnValueChanged();
listView.Q<Label>().style.unityFontStyleAndWeight = FontStyle.Bold;
Add(listView);
}
readonly IList list;
readonly ListView listView;
public event Action<object> OnValueChanged;
void NotifyOnValueChanged()
{
OnValueChanged?.Invoke(list);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d1b638bf9747241989ead75deeb13093
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,71 @@
using System;
using System.Reflection;
using UnityEditor;
using UnityEngine.UIElements;
namespace Alchemy.Editor.Elements
{
public sealed class MethodButton : VisualElement
{
const string ButtonLabelText = "Invoke";
public MethodButton(object target, MethodInfo methodInfo)
{
var parameters = methodInfo.GetParameters();
// Create parameterless button
if (parameters.Length == 0)
{
button = new Button((Action)methodInfo.CreateDelegate(typeof(Action), target))
{
text = methodInfo.Name
};
Add(button);
return;
}
var parameterObjects = new object[parameters.Length];
var box = new HelpBox();
Add(box);
foldout = new Foldout()
{
text = methodInfo.Name,
value = false,
style = {
flexGrow = 1f
}
};
button = new Button(() => methodInfo.Invoke(target, parameterObjects))
{
text = ButtonLabelText,
style = {
position = Position.Absolute,
right = 1f,
top = 1.5f,
width = 100f
}
};
box.Add(new VisualElement() { style = { width = 12f } });
box.Add(foldout);
box.Add(button);
for (int i = 0; i < parameters.Length; i++)
{
var index = i;
var parameter = parameters[index];
parameterObjects[index] = Activator.CreateInstance(parameter.ParameterType);
var element = new GenericField(parameterObjects[index], parameter.ParameterType, ObjectNames.NicifyVariableName(parameter.Name), 0);
element.OnValueChanged += x => parameterObjects[index] = x;
element.style.paddingRight = 4f;
foldout.Add(element);
}
}
readonly Foldout foldout;
readonly Button button;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 098870ca8e0fc4d5b80c50ffa39f16f3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,39 @@
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
using Alchemy.Editor.Internal;
namespace Alchemy.Editor.Elements
{
/// <summary>
/// Visual Element that draws SerializedProperty of Array or List
/// </summary>
public sealed class PropertyListView : BindableElement
{
public PropertyListView(SerializedProperty property, int depth)
{
Assert.IsTrue(property.isArray);
var listView = GUIHelper.CreateDefaultListView(ObjectNames.NicifyVariableName(property.displayName));
listView.bindItem = (element, index) =>
{
var arrayElement = property.GetArrayElementAtIndex(index);
var e = new AlchemyPropertyField(arrayElement, depth + 1);
element.Add(e);
element.Bind(arrayElement.serializedObject);
};
listView.unbindItem = (element, index) =>
{
element.Clear();
element.Unbind();
};
listView.Q<Label>().style.unityFontStyleAndWeight = FontStyle.Bold;
listView.BindProperty(property);
Add(listView);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: f003dc4ef9602443c8a41555882e68b1
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,85 @@
using System;
using System.Reflection;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
using Alchemy.Editor.Internal;
using Alchemy.Inspector;
namespace Alchemy.Editor.Elements
{
public sealed class ReflectionField : VisualElement
{
public ReflectionField(object target, MemberInfo memberInfo, int depth)
{
Rebuild(target, memberInfo, depth);
}
public void Rebuild(object target, MemberInfo memberInfo, int depth)
{
Clear();
if (memberInfo is MethodInfo methodInfo)
{
if (methodInfo.HasCustomAttribute<ButtonAttribute>())
{
var button = new MethodButton(target, methodInfo);
Add(button);
}
return;
}
object value;
GenericField element;
switch (memberInfo)
{
default: return;
case FieldInfo fieldInfo:
value = fieldInfo.IsStatic ? fieldInfo.GetValue(null) : target == null ? TypeHelper.GetDefaultValue(fieldInfo.FieldType) : fieldInfo.GetValue(target);
var fieldType = target == null ? fieldInfo.FieldType : fieldInfo.GetValue(target)?.GetType() ?? fieldInfo.FieldType;
element = new GenericField(value, fieldType, ObjectNames.NicifyVariableName(memberInfo.Name), depth, true);
element.OnValueChanged += x =>
{
OnBeforeValueChange?.Invoke(target);
fieldInfo.SetValue(target, x);
// Force serialization
if (target is ISerializationCallbackReceiver receiver)
{
receiver.OnBeforeSerialize();
}
OnValueChanged?.Invoke(target);
};
break;
case PropertyInfo propertyInfo:
if (!propertyInfo.HasCustomAttribute<ShowInInspectorAttribute>()) return;
if (!propertyInfo.CanRead) return;
value = propertyInfo.GetMethod.IsStatic ? propertyInfo.GetValue(null) : target == null ? TypeHelper.GetDefaultValue(propertyInfo.PropertyType) : propertyInfo.GetValue(target);
var propertyType = target == null ? propertyInfo.PropertyType : propertyInfo.GetValue(target)?.GetType() ?? propertyInfo.PropertyType;
element = new GenericField(value, propertyType, ObjectNames.NicifyVariableName(memberInfo.Name), depth, true);
element.OnValueChanged += x =>
{
OnBeforeValueChange?.Invoke(target);
if (propertyInfo.CanWrite) propertyInfo.SetValue(target, x);
// Force serialization
if (target is ISerializationCallbackReceiver receiver)
{
receiver.OnBeforeSerialize();
}
OnValueChanged?.Invoke(target);
};
element.SetEnabled(propertyInfo.CanWrite);
break;
}
Add(element);
}
public event Action<object> OnBeforeValueChange;
public event Action<object> OnValueChanged;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e96e973f88c414f90b77487d1a59b89d
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,127 @@
using System;
using System.Linq;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEditor.IMGUI.Controls;
using UnityEngine;
using UnityEngine.Assertions;
using UnityEngine.UIElements;
using Alchemy.Editor.Internal;
namespace Alchemy.Editor.Elements
{
/// <summary>
/// Draw properties marked with SerializeReference attribute
/// </summary>
public sealed class SerializeReferenceField : VisualElement
{
public SerializeReferenceField(SerializedProperty property, int depth)
{
Assert.IsTrue(property.propertyType == SerializedPropertyType.ManagedReference);
style.flexDirection = FlexDirection.Row;
style.minHeight = EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing;
foldout = new Foldout()
{
text = ObjectNames.NicifyVariableName(property.displayName)
};
foldout.style.flexGrow = 1f;
foldout.BindProperty(property);
Add(foldout);
buttonContainer = new IMGUIContainer(() =>
{
var position = EditorGUILayout.GetControlRect();
var dropdownRect = position;
dropdownRect.height = EditorGUIUtility.singleLineHeight;
var buttonLabel = EditorIcons.CsScriptIcon;
try
{
buttonLabel.text = (property.managedReferenceValue == null ? "Null" : property.managedReferenceValue.GetType().Name) +
$" ({property.GetManagedReferenceFieldTypeName()})";
}
catch (InvalidOperationException)
{
// Ignoring exceptions when disposed (bad solution)
return;
}
if (GUI.Button(dropdownRect, buttonLabel, EditorStyles.objectField))
{
const int MaxTypePopupLineCount = 13;
var baseType = property.GetManagedReferenceFieldType();
SerializeReferenceDropdown dropdown = new(
TypeCache.GetTypesDerivedFrom(baseType).Append(baseType).Where(t =>
(t.IsPublic || t.IsNestedPublic) &&
!t.IsAbstract &&
!t.IsGenericType &&
!typeof(UnityEngine.Object).IsAssignableFrom(t) &&
t.IsSerializable
),
MaxTypePopupLineCount,
new AdvancedDropdownState()
);
dropdown.onItemSelected += item =>
{
property.SetManagedReferenceType(item.type);
property.isExpanded = true;
property.serializedObject.ApplyModifiedProperties();
property.serializedObject.Update();
Rebuild(property, depth);
};
dropdown.Show(position);
}
});
schedule.Execute(() =>
{
VisualElement visualElement = GetFirstAncestorOfType<InspectorElement>();
visualElement.RegisterCallback<GeometryChangedEvent>(x =>
{
buttonContainer.style.width = GUIHelper.CalculateFieldWidth(buttonContainer, visualElement) -
(buttonContainer.GetFirstAncestorOfType<Foldout>() != null ? 18f : 0f);
});
buttonContainer.style.width = GUIHelper.CalculateFieldWidth(buttonContainer, visualElement) -
(buttonContainer.GetFirstAncestorOfType<Foldout>() != null ? 18f : 0f);
});
buttonContainer.style.position = Position.Absolute;
buttonContainer.style.top = EditorGUIUtility.standardVerticalSpacing * 0.5f;
buttonContainer.style.right = 0f;
Add(buttonContainer);
Rebuild(property, depth);
}
public readonly Foldout foldout;
public readonly IMGUIContainer buttonContainer;
/// <summary>
/// Rebuild child elements
/// </summary>
void Rebuild(SerializedProperty property, int depth)
{
foldout.Clear();
if (property.managedReferenceValue == null)
{
var helpbox = new HelpBox("No type assigned.", HelpBoxMessageType.Info);
foldout.Add(helpbox);
}
else
{
InspectorHelper.BuildElements(property.serializedObject, foldout, property.managedReferenceValue, x => property.FindPropertyRelative(x), depth + 1);
}
this.Bind(property.serializedObject);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 03f2015425fd34b8cb49aea8e59f9745
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 9243210c5444145978ade99e7a7af0c1
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,10 @@
using UnityEditor;
using UnityEngine;
namespace Alchemy.Editor.Internal
{
internal static class EditorIcons
{
public static readonly GUIContent CsScriptIcon = EditorGUIUtility.IconContent("cs Script Icon");
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b5d3e43f293fb4a7ebcba1e3b2b91f2b
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,103 @@
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.Assertions;
using Alchemy.Inspector;
namespace Alchemy.Editor.Internal
{
internal static class GUIHelper
{
public static Color LineColor => EditorGUIUtility.isProSkin ? new(0.4f, 0.4f, 0.4f) : new(0.6f, 0.6f, 0.6f);
public static Color SubtitleColor => new(0.5f, 0.5f, 0.5f);
public static Color TextColor => EditorGUIUtility.isProSkin ? new(0.725f, 0.725f, 0.725f) : new(0.141f, 0.141f, 0.141f);
public static float CalculateFieldWidth(VisualElement element, VisualElement root)
{
var labelWidth = CalculateLabelWidth(element, root);
return root.resolvedStyle.width - labelWidth - 27f;
}
public static float CalculateLabelWidth(VisualElement element, VisualElement root)
{
// This code is a partial modification of the Label width calculation method actually used inside PropertyField.
var num = root.resolvedStyle.paddingLeft;
var num2 = 37f;
var num3 = 123f;
var num4 = element.GetFirstAncestorOfType<Foldout>() == null ? 0f : 15f;
var width = root.resolvedStyle.width;
var a = width * 0.45f - num2 - num - num4;
var b = Mathf.Max(num3 - num - num4, 0f);
return Mathf.Max(a, b) + 12f;
}
public static ListView CreateDefaultListView(string label)
{
return new ListView()
{
reorderable = true,
reorderMode = ListViewReorderMode.Animated,
showBorder = true,
showFoldoutHeader = true,
headerTitle = label,
showAddRemoveFooter = true,
fixedItemHeight = 20f,
virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight,
showAlternatingRowBackgrounds = AlternatingRowBackground.None,
};
}
public static PropertyField CreateObjectField(SerializedProperty property)
{
Assert.IsTrue(property.propertyType == SerializedPropertyType.ObjectReference);
var fieldInfo = property.GetFieldInfo();
var isAssetsOnly = fieldInfo.HasCustomAttribute<AssetsOnlyAttribute>();
var propertyField = new PropertyField(property);
propertyField.RegisterValueChangeCallback(x =>
{
var objectField = propertyField.Q<ObjectField>();
objectField.objectType = fieldInfo.FieldType;
objectField.allowSceneObjects = !isAssetsOnly;
});
return propertyField;
}
public static void ScheduleAdjustLabelWidth(VisualElement element)
{
void Adjust(VisualElement visualElement)
{
var label = element.Q<Label>();
if (label == null) return;
label.style.minWidth = 0f;
label.style.width = CalculateLabelWidth(element, visualElement);
}
// Adjust label width
element.schedule.Execute(() =>
{
VisualElement visualElement = element.GetFirstAncestorOfType<InspectorElement>();
visualElement.RegisterCallback<GeometryChangedEvent>(x => Adjust(visualElement));
Adjust(visualElement);
});
}
public static IMGUIContainer CreateLine(Color color, float height)
{
return new IMGUIContainer(() =>
{
var rect = EditorGUILayout.GetControlRect(false, height);
rect.xMin += 3f;
rect.y += rect.height * 0.5f;
rect.height = 1f;
EditorGUI.DrawRect(rect, color);
});
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: b23551d43228a43c9a9207cf8f150834
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,295 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEditor.UIElements;
using UnityEngine;
using UnityEngine.UIElements;
using Alchemy.Inspector;
using Alchemy.Editor.Elements;
#if ALCHEMY_SUPPORT_SERIALIZATION
using Alchemy.Serialization;
#endif
namespace Alchemy.Editor.Internal
{
public static class InspectorHelper
{
public const int MaxDepth = 15;
public sealed class GroupNode
{
public GroupNode(string name, PropertyGroupDrawer drawer)
{
this.name = name;
this.drawer = drawer;
}
readonly string name;
readonly PropertyGroupDrawer drawer;
readonly List<MemberInfo> members = new();
readonly List<GroupNode> children = new();
public string Name => name;
public IEnumerable<MemberInfo> Members => members;
public PropertyGroupDrawer Drawer => drawer;
public VisualElement VisualElement { get; set; }
public GroupNode Parent { get; private set; }
public GroupNode Find(Func<GroupNode, bool> predicate)
{
return children.FirstOrDefault(predicate);
}
public void Add(GroupNode node)
{
children.Add(node);
node.Parent = this;
}
public void AddMember(MemberInfo memberInfo)
{
members.Add(memberInfo);
}
public IEnumerable<GroupNode> DescendantsAndSelf()
{
yield return this;
foreach (var item in Descendants(children)) yield return item;
}
static IEnumerable<GroupNode> Descendants(IEnumerable<GroupNode> source)
{
foreach (var item in source)
{
yield return item;
var e = Descendants(item.children).GetEnumerator();
while (e.MoveNext())
{
yield return e.Current;
}
}
}
}
public static void BuildElements(SerializedObject serializedObject, VisualElement rootElement, object target, Func<string, SerializedProperty> findPropertyFunc, int depth)
{
if (depth >= MaxDepth) return;
if (target == null) return;
// Build node
var rootNode = BuildInspectorNode(target.GetType());
// Add elements
foreach (var node in rootNode.DescendantsAndSelf())
{
// Get or create group element
if (node.Parent == null)
{
node.VisualElement = rootElement;
}
else if (node.Drawer == null)
{
node.VisualElement = node.Parent.VisualElement;
}
else
{
node.VisualElement = node.Drawer.CreateRootElement(node.Name);
node.Parent.VisualElement.Add(node.VisualElement);
}
// Add member elements
foreach (var member in node.Members.OrderByAttributeThenByMemberType())
{
// Exclude if member has HideInInspector attribute
if (member.HasCustomAttribute<HideInInspector>()) continue;
// Add default PropertyField if member has DisableAlchemyEditorAttribute
if (member.GetCustomAttribute<DisableAlchemyEditorAttribute>() != null)
{
var p = findPropertyFunc(member.Name);
if (p != null)
{
var propertyField = new PropertyField(p);
propertyField.style.width = Length.Percent(100f);
node.VisualElement.Add(propertyField);
}
continue;
}
VisualElement element = null;
var property = findPropertyFunc(member.Name);
// Add default PropertyField if the property has a custom PropertyDrawer
if ((member is FieldInfo fieldInfo && InternalAPIHelper.GetDrawerTypeForType(fieldInfo.FieldType) != null) ||
(member is PropertyInfo propertyInfo && InternalAPIHelper.GetDrawerTypeForType(propertyInfo.PropertyType) != null))
{
if (property != null)
{
element = new PropertyField(property);
}
}
else
{
element = CreateMemberElement(serializedObject, target, member, findPropertyFunc, depth + 1);
}
if (element == null) continue;
element.style.width = Length.Percent(100f);
var e = node.Drawer?.GetGroupElement(
member.GetCustomAttributes<PropertyGroupAttribute>()
.OrderByDescending(x => x.GroupPath.Split('/').Length)
.FirstOrDefault()
);
if (e == null) node.VisualElement.Add(element);
else e.Add(element);
PropertyProcessor.ExecuteProcessors(serializedObject, property, target, member, element);
}
}
}
internal static GroupNode BuildInspectorNode(Type targetType)
{
var rootNode = new GroupNode("Inspector-Group-Root", null);
// Get all members
var members = targetType.GetMembers(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic);
// Build member nodes
foreach (var member in members)
{
var groupAttributes = member.GetCustomAttributes<PropertyGroupAttribute>(true);
if (groupAttributes.Count() == 0)
{
rootNode.AddMember(member);
continue;
}
var parentNode = rootNode;
foreach (var (groupAttribute, hierarchy) in groupAttributes
.Select(x => (x, x.GroupPath.Split('/')))
.OrderBy(x => x.Item2.Length))
{
parentNode = rootNode;
foreach (var groupName in hierarchy)
{
var next = parentNode.Find(x => x.Name == groupName);
if (next == null)
{
// Find drawer type
var drawerType = TypeCache.GetTypesWithAttribute<CustomPropertyGroupDrawerAttribute>()
.FirstOrDefault(x => x.GetCustomAttribute<CustomPropertyGroupDrawerAttribute>().targetAttributeType == groupAttribute.GetType());
var drawer = (PropertyGroupDrawer)Activator.CreateInstance(drawerType);
drawer._uniqueId = "AlchemyGroupId_" + targetType.FullName + "_" + groupAttribute.GroupPath;
next = new GroupNode(groupName, drawer);
parentNode.Add(next);
}
parentNode = next;
}
}
parentNode.AddMember(member);
}
return rootNode;
}
public static VisualElement CreateMemberElement(SerializedObject serializedObject, object target, MemberInfo memberInfo, Func<string, SerializedProperty> findPropertyFunc, int depth)
{
if (depth > MaxDepth) return null;
switch (memberInfo)
{
case MethodInfo methodInfo:
if (methodInfo.HasCustomAttribute<ButtonAttribute>())
{
return new MethodButton(target, methodInfo);
}
break;
case FieldInfo:
case PropertyInfo:
var property = findPropertyFunc?.Invoke(memberInfo.Name);
// Create property field
if (property != null)
{
return new AlchemyPropertyField(property, depth);
}
#if ALCHEMY_SUPPORT_SERIALIZATION
if (serializedObject.targetObject.GetType().HasCustomAttribute<AlchemySerializeAttribute>() &&
memberInfo.HasCustomAttribute<AlchemySerializeFieldAttribute>())
{
var element = default(VisualElement);
if (memberInfo is FieldInfo fieldInfo)
{
SerializedProperty GetProperty() => findPropertyFunc?.Invoke("alchemySerializationData").FindPropertyRelative(memberInfo.Name);
var p = GetProperty();
if (p != null)
{
var field = new ReflectionField(target, fieldInfo, depth);
var foldout = field.Q<Foldout>();
foldout?.BindProperty(p);
field.TrackPropertyValue(p, p =>
{
field.Rebuild(target, memberInfo, depth);
var foldout = field.Q<Foldout>();
foldout?.BindProperty(p);
});
var undoName = "Modified:" + p.displayName;
field.OnBeforeValueChange += x =>
{
Undo.RegisterCompleteObjectUndo(GetProperty().serializedObject.targetObject, undoName);
};
element = field;
}
}
// TODO: Supports editing of multiple objects
if (element != null && serializedObject.targetObjects.Length > 1)
{
element.SetEnabled(false);
}
return element;
}
#endif
// Create element if member has ShowInInspector attribute
if (memberInfo.HasCustomAttribute<ShowInInspectorAttribute>())
{
return new ReflectionField(target, memberInfo, depth);
}
break;
}
return null;
}
internal static IOrderedEnumerable<MemberInfo> OrderByAttributeThenByMemberType(this IEnumerable<MemberInfo> members)
{
return members
.OrderBy(x =>
{
var orderAttribute = x.GetCustomAttribute<OrderAttribute>();
if (orderAttribute == null) return 0;
return orderAttribute.Order;
})
.ThenBy(x =>
{
if (x is MethodInfo) return 1;
return 0;
});
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 664a7f04961bd4d6fb7fa0bcaf290687
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,53 @@
using System;
using System.Reflection;
using UnityEngine.UIElements;
namespace Alchemy.Editor.Internal
{
using Editor = UnityEditor.Editor;
/// <summary>
/// Call the Unity internal API using reflection. This may not work depending on your Unity version.
/// </summary>
public static class InternalAPIHelper
{
static readonly Assembly EditorAssembly = Assembly.GetAssembly(typeof(Editor));
// ScriptAttributeUtility
// https://github.com/Unity-Technologies/UnityCsReference/blob/724ff727438a68d1bc05b342c693c1d481063fd3/Editor/Mono/Inspector/Core/ScriptAttributeGUI/ScriptAttributeUtility.cs
const string Name_ScriptAttributeUtility = "UnityEditor.ScriptAttributeUtility";
public static Type GetDrawerTypeForType(Type classType)
{
var instance = EditorAssembly.CreateInstance(Name_ScriptAttributeUtility);
var utilityType = instance.GetType();
var bindingFlags = BindingFlags.NonPublic | BindingFlags.Static;
var methodInfo = utilityType.GetMethod(nameof(GetDrawerTypeForType), bindingFlags);
return (Type)methodInfo.Invoke(instance, new object[] { classType });
}
const string Name_M_Clickable = "m_Clickable";
// BaseBoolField
// https://github.com/Unity-Technologies/UnityCsReference/blob/master/Modules/UIElements/Core/Controls/BaseBoolField.cs#L12
public static Clickable GetClickable(BaseBoolField boolField)
{
var clickable = ReflectionHelper.GetField(typeof(Toggle), Name_M_Clickable).GetValue(boolField);
return (Clickable)clickable;
}
// Clickable
// https://github.com/Unity-Technologies/UnityCsReference/blob/master/Modules/UIElements/Core/Clickable.cs#L12
const string Name_AcceptClicksIfDisabled = "acceptClicksIfDisabled";
public static void SetAcceptClicksIfDisabled(Clickable clickable, bool value)
{
ReflectionHelper.GetProperty(typeof(Clickable), Name_AcceptClicksIfDisabled).SetValue(clickable, value);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: bd2e9148da43349219f92b909651fbdc
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,13 @@
using System;
using System.Reflection;
namespace Alchemy.Editor.Internal
{
public static class MemberInfoExtensions
{
public static bool HasCustomAttribute<T>(this MemberInfo memberInfo) where T : Attribute
{
return memberInfo.GetCustomAttribute<T>() != null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4786119bc20d840f7b77860f7b16e18c
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,298 @@
using System;
using System.Reflection;
using System.Collections;
using System.Collections.Generic;
using System.Linq;
using System.Linq.Expressions;
namespace Alchemy.Editor.Internal
{
public static class ReflectionHelper
{
static readonly Dictionary<(Type, string, BindingFlags, bool), FieldInfo> cacheFieldInfo = new();
static readonly Dictionary<(Type, string, BindingFlags, bool), MethodInfo> cacheMethodInfo = new();
static readonly Dictionary<(Type, string, BindingFlags, bool), PropertyInfo> cachePropertyInfo = new();
static readonly Dictionary<(Type, BindingFlags, bool), MemberInfo[]> cacheAllMembers = new();
static readonly Dictionary<(Type, string), Func<object, object>> cacheGetFieldValue = new();
static readonly Dictionary<(Type, string), Func<object, object>> cacheGetPropertyValue = new();
static readonly Dictionary<(Type, string), Func<object, object>> cacheGetMethodValue = new();
public static Func<object, object> CreateGetter(FieldInfo fieldInfo)
{
if (fieldInfo == null) return null;
if (fieldInfo.IsStatic)
{
Expression body = Expression.Convert(Expression.MakeMemberAccess(null, fieldInfo), typeof(object));
var lambda = Expression.Lambda<Func<object>>(body).Compile();
return _ => lambda();
}
if (fieldInfo.DeclaringType != null)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var tParam = Expression.Convert(objParam, fieldInfo.DeclaringType);
Expression body = Expression.Convert(Expression.MakeMemberAccess(tParam, fieldInfo), typeof(object));
return Expression.Lambda<Func<object, object>>(body, objParam).Compile();
}
return null;
}
public static Func<object, object> CreateGetter(PropertyInfo propertyInfo)
{
if (propertyInfo == null) return null;
if (propertyInfo.GetGetMethod(true).IsStatic)
{
Expression body = Expression.Convert(Expression.MakeMemberAccess(null, propertyInfo), typeof(object));
var lambda = Expression.Lambda<Func<object>>(body).Compile();
return _ => lambda();
}
if (propertyInfo.DeclaringType != null)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var tParam = Expression.Convert(objParam, propertyInfo.DeclaringType);
Expression body = Expression.Convert(Expression.MakeMemberAccess(tParam, propertyInfo), typeof(object));
return Expression.Lambda<Func<object, object>>(body, objParam).Compile();
}
return null;
}
public static Func<object, object> CreateGetter(MethodInfo methodInfo)
{
if (methodInfo == null) return null;
if (methodInfo.IsStatic)
{
Expression body = Expression.Convert(Expression.Call(null, methodInfo), typeof(object));
var lambda = Expression.Lambda<Func<object>>(body).Compile();
return _ => lambda();
}
if (methodInfo.DeclaringType != null)
{
var objParam = Expression.Parameter(typeof(object), "obj");
var tParam = Expression.Convert(objParam, methodInfo.DeclaringType);
Expression body = Expression.Convert(Expression.Call(tParam, methodInfo), typeof(object));
return Expression.Lambda<Func<object, object>>(body, objParam).Compile();
}
return null;
}
public static object GetFieldValue(object target, Type type, string name, BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
{
if (!cacheGetFieldValue.TryGetValue((type, name), out var value))
{
FieldInfo info = type.GetField(name, bindingAttr);
value = CreateGetter(info);
cacheGetFieldValue.Add((type, name), value);
}
return value?.Invoke(target);
}
public static object GetPropertyValue(object target, Type type, string name, BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
{
if (!cacheGetPropertyValue.TryGetValue((type, name), out var value))
{
PropertyInfo info = type.GetProperty(name, bindingAttr);
value = CreateGetter(info);
cacheGetPropertyValue.Add((type, name), value);
}
return value?.Invoke(target);
}
public static object GetMethodValue(object target, Type type, string name, BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
{
if (!cacheGetMethodValue.TryGetValue((type, name), out var value))
{
MethodInfo info = type.GetMethod(name, bindingAttr);
value = CreateGetter(info);
cacheGetMethodValue.Add((type, name), value);
}
return value?.Invoke(target);
}
public static FieldInfo GetField(Type type, string name, BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static, bool inherit = false)
{
FieldInfo info;
if (cacheFieldInfo.ContainsKey((type, name, bindingAttr, inherit)))
{
info = cacheFieldInfo[(type, name, bindingAttr, inherit)];
}
else
{
if (inherit)
{
info = GetAllFieldsIncludingInherited(type, bindingAttr).FirstOrDefault(x => x.Name == name);
}
else
{
info = type.GetField(name, bindingAttr);
}
cacheFieldInfo.Add((type, name, bindingAttr, inherit), info);
}
return info;
}
static IEnumerable<FieldInfo> GetAllFieldsIncludingInherited(Type type, BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
{
if (type == null) return Enumerable.Empty<FieldInfo>();
return type.GetFields(bindingAttr).Concat(GetAllFieldsIncludingInherited(type.BaseType));
}
public static PropertyInfo GetProperty(Type type, string name, BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static, bool inherit = false)
{
PropertyInfo info;
if (cachePropertyInfo.ContainsKey((type, name, bindingAttr, inherit)))
{
info = cachePropertyInfo[(type, name, bindingAttr, inherit)];
}
else
{
if (inherit)
{
info = GetAllPropertiesIncludingInherited(type, bindingAttr).FirstOrDefault(x => x.Name == name);
}
else
{
info = type.GetProperty(name, bindingAttr);
}
cachePropertyInfo.Add((type, name, bindingAttr, inherit), info);
}
return info;
}
static IEnumerable<PropertyInfo> GetAllPropertiesIncludingInherited(Type type, BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
{
if (type == null) return Enumerable.Empty<PropertyInfo>();
return type.GetProperties(bindingAttr).Concat(GetAllPropertiesIncludingInherited(type.BaseType));
}
public static MethodInfo GetMethod(Type type, string name, BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static, bool inherit = false)
{
MethodInfo info;
if (cacheMethodInfo.ContainsKey((type, name, bindingAttr, inherit)))
{
info = cacheMethodInfo[(type, name, bindingAttr, inherit)];
}
else
{
if (inherit)
{
info = GetAllMethodsIncludingInherited(type, bindingAttr).FirstOrDefault(x => x.Name == name);
}
else
{
info = type.GetMethods(bindingAttr).Where(x => x.Name == name).FirstOrDefault();
}
cacheMethodInfo.Add((type, name, bindingAttr, inherit), info);
}
return info;
}
static IEnumerable<MethodInfo> GetAllMethodsIncludingInherited(Type type, BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
{
if (type == null) return Enumerable.Empty<MethodInfo>();
return type.GetMethods(bindingAttr).Concat(GetAllMethodsIncludingInherited(type.BaseType));
}
public static object GetValue(object target, string name, BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static, bool allowProperty = true, bool allowMethod = true)
{
if (target == null) return null;
Type type = target.GetType();
object result;
while (type != null)
{
result = GetFieldValue(target, type, name, bindingAttr);
if (result != null) return result;
if (allowProperty)
{
result = GetPropertyValue(target, type, name, bindingAttr);
if (result != null) return result;
}
if (allowMethod)
{
result = GetMethodValue(target, type, name, bindingAttr);
if (result != null) return result;
}
type = type.BaseType;
}
return null;
}
public static object GetValue(object target, string name, int index)
{
if (GetValue(target, name, allowMethod: false) is not IEnumerable enumerable) return null;
IEnumerator enumerator = enumerable.GetEnumerator();
for (int i = 0; i <= index; i++)
{
if (!enumerator.MoveNext()) return null;
}
return enumerator.Current;
}
public static bool GetValueBool(object target, string name)
{
if (GetValue(target, name) is bool cond)
{
return cond;
}
return false;
}
public static MemberInfo[] GetMembers(Type type, BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static, bool inherit = false)
{
if (cacheAllMembers.ContainsKey((type, bindingAttr, inherit)))
{
return cacheAllMembers[(type, bindingAttr, inherit)];
}
else
{
MemberInfo[] memberInfos;
if (inherit)
{
memberInfos = GetMembersIncludingInherited(type, bindingAttr).ToArray();
}
else
{
memberInfos = type.GetMembers(bindingAttr);
}
cacheAllMembers.Add((type, bindingAttr, inherit), memberInfos);
return memberInfos;
}
}
static IEnumerable<MemberInfo> GetMembersIncludingInherited(Type type, BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static)
{
if (type == null) return Enumerable.Empty<MemberInfo>();
return type.GetMembers(bindingAttr).Concat(GetMembersIncludingInherited(type.BaseType));
}
public static object Invoke(object target, string name, params object[] parameters)
{
if (target == null) return false;
Type type = target.GetType();
BindingFlags bindingAttr = BindingFlags.NonPublic | BindingFlags.Public | BindingFlags.Instance | BindingFlags.Static;
while (type != null)
{
var m = GetMethod(type, name, bindingAttr);
if (m != null) return m.Invoke(m.IsStatic ? null : target, parameters);
type = type.BaseType;
}
return false;
}
public static object GetCollectionValue(object target, int index)
{
var type = target.GetType();
if (type.IsArray) return ((Array)target).GetValue(index);
else if (target is IList list) return list[index];
return null;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 7ddc52eddda264fba8fea9844f3c0f61
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,153 @@
using System;
using System.Collections.Generic;
using System.Linq;
using UnityEngine;
using UnityEditor;
using UnityEditor.IMGUI.Controls;
namespace Alchemy.Editor.Internal
{
public sealed class SerializeReferenceDropdownItem : AdvancedDropdownItem
{
public readonly Type type;
public SerializeReferenceDropdownItem(Type type, string name) : base(name)
{
this.type = type;
if (type != null) icon = (Texture2D)EditorIcons.CsScriptIcon.image;
}
}
public sealed class SerializeReferenceDropdown : AdvancedDropdown
{
private static readonly float headerHeight = EditorGUIUtility.singleLineHeight * 2f;
private static readonly int maxNamespaceNestCount = 16;
private static readonly string nullDisplayName = "(Null)";
private Type[] types;
public event Action<SerializeReferenceDropdownItem> onItemSelected;
public static void AddTo(AdvancedDropdownItem root, IEnumerable<Type> types)
{
var itemCount = 0;
var nullItem = new SerializeReferenceDropdownItem(null, nullDisplayName)
{
id = itemCount++
};
root.AddChild(nullItem);
var typeArray = types.OrderBy(x => x.FullName);
var isSingleNamespace = true;
var namespaces = new string[maxNamespaceNestCount];
foreach (Type type in typeArray)
{
var splittedTypePath = GetSplittedTypePath(type);
if (splittedTypePath.Length <= 1) continue;
for (int i = 0; (splittedTypePath.Length - 1) > i; i++)
{
var ns = namespaces[i];
if (ns == null)
{
namespaces[i] = splittedTypePath[i];
}
else if (ns != splittedTypePath[i])
{
isSingleNamespace = false;
break;
}
}
if (!isSingleNamespace)
{
break;
}
}
foreach (var type in typeArray)
{
var splittedTypePath = GetSplittedTypePath(type);
if (splittedTypePath.Length == 0) continue;
var parent = root;
if (!isSingleNamespace)
{
for (int i = 0; (splittedTypePath.Length - 1) > i; i++)
{
var foundItem = GetItem(parent, splittedTypePath[i]);
if (foundItem != null)
{
parent = foundItem;
}
else
{
var newItem = new AdvancedDropdownItem(splittedTypePath[i])
{
id = itemCount++,
};
parent.AddChild(newItem);
parent = newItem;
}
}
}
var item = new SerializeReferenceDropdownItem(type, ObjectNames.NicifyVariableName(splittedTypePath[^1]))
{
id = itemCount++
};
parent.AddChild(item);
}
}
static AdvancedDropdownItem GetItem(AdvancedDropdownItem parent, string name)
{
foreach (AdvancedDropdownItem item in parent.children)
{
if (item.name == name) return item;
}
return null;
}
public SerializeReferenceDropdown(IEnumerable<Type> types, int maxLineCount, AdvancedDropdownState state) : base(state)
{
SetTypes(types);
minimumSize = new(minimumSize.x, EditorGUIUtility.singleLineHeight * maxLineCount + headerHeight);
}
public void SetTypes(IEnumerable<Type> types)
{
this.types = types.ToArray();
}
protected override AdvancedDropdownItem BuildRoot()
{
var root = new AdvancedDropdownItem("Select Type");
AddTo(root, types);
return root;
}
protected override void ItemSelected(AdvancedDropdownItem item)
{
base.ItemSelected(item);
if (item is SerializeReferenceDropdownItem dropdownItem)
{
onItemSelected?.Invoke(dropdownItem);
}
}
static string[] GetSplittedTypePath(Type type)
{
int splitIndex = type.FullName.LastIndexOf('.');
if (splitIndex >= 0)
{
return new string[] { type.FullName[..splitIndex], type.FullName[(splitIndex + 1)..] };
}
else
{
return new string[] { type.Name };
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: fc340cedf3cee4da19aae647ce6299ce
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,11 @@
using System;
using System.Reflection;
using UnityEditor;
namespace Alchemy.Editor.Internal
{
public static class SerializedObjectExtensions
{
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: e70b6aca000bb4a698521391868e2f98
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,293 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Reflection;
using System.Linq;
using System.Text.RegularExpressions;
using UnityEngine;
using UnityEditor;
namespace Alchemy.Editor.Internal
{
public static class SerializedPropertyExtensions
{
public static bool TryGetAttribute<TAttribute>(this SerializedProperty property, out TAttribute result) where TAttribute : Attribute
{
return TryGetAttribute(property, false, out result);
}
public static bool TryGetAttribute<TAttribute>(this SerializedProperty property, bool inherit, out TAttribute result) where TAttribute : Attribute
{
TAttribute attribute = GetAttribute<TAttribute>(property, inherit);
result = attribute;
return attribute != null;
}
public static TAttribute GetAttribute<TAttribute>(this SerializedProperty property, bool inherit = false) where TAttribute : Attribute
{
if (property == null) throw new ArgumentNullException(nameof(property));
return property.GetFieldInfo().GetCustomAttribute<TAttribute>(inherit);
}
public static IEnumerable<TAttribute> GetAttributes<TAttribute>(this SerializedProperty property, bool inherit) where TAttribute : Attribute
{
if (property == null) throw new ArgumentNullException(nameof(property));
return property.GetFieldInfo().GetCustomAttributes<TAttribute>(inherit);
}
public static float GetHeight(this SerializedProperty property)
{
return EditorGUI.GetPropertyHeight(property, true);
}
public static float GetHeight(this SerializedProperty property, bool includeChildren)
{
return EditorGUI.GetPropertyHeight(property, includeChildren);
}
public static float GetHeight(this SerializedProperty property, GUIContent label, bool includeChildren)
{
return EditorGUI.GetPropertyHeight(property, label, includeChildren);
}
public static T GetValue<T>(this SerializedProperty property)
{
return GetNestedObject<T>(property.propertyPath, GetSerializedPropertyRootObject(property));
}
public static bool SetValue<T>(this SerializedProperty property, T value)
{
object obj = GetSerializedPropertyRootObject(property);
var fieldStructure = property.propertyPath.Split('.');
for (int i = 0; i < fieldStructure.Length - 1; i++)
{
obj = GetFieldOrPropertyValue<object>(fieldStructure[i], obj);
}
var fieldName = fieldStructure.Last();
return SetFieldOrPropertyValue(fieldName, obj, value);
}
static readonly Regex IndexerRegex = new(@"[^0-9]+");
public static FieldInfo GetFieldInfo(this SerializedProperty property)
{
object target = property.serializedObject.targetObject;
var splits = property.propertyPath.Split('.');
var fieldInfo = ReflectionHelper.GetField(target.GetType(), splits[0]);
target = fieldInfo.GetValue(target);
for (var i = 1; i < splits.Length; i++)
{
if (target == null) return null;
if (splits[i] == "Array")
{
i++;
if (i >= splits.Length) continue;
var index = int.Parse(IndexerRegex.Replace(splits[i], string.Empty));
var targetType = target.GetType();
if (targetType.IsArray)target = (target as Array).GetValue(index);
else target = (target as IList)[index];
i++;
if (i >= splits.Length) continue;
targetType = target.GetType();
fieldInfo = ReflectionHelper.GetField(targetType, splits[i]);
}
else
{
var targetType = target.GetType();
fieldInfo = ReflectionHelper.GetField(targetType, splits[i]);
}
target = fieldInfo?.GetValue(target);
}
return fieldInfo;
}
public static Type GetPropertyType(this SerializedProperty property, bool isCollectionType = false)
{
var fieldInfo = property.GetFieldInfo();
if (isCollectionType && property.isArray && property.propertyType != SerializedPropertyType.String)
return fieldInfo.FieldType.IsArray ?
fieldInfo.FieldType.GetElementType() :
fieldInfo.FieldType.GetGenericArguments()[0];
return fieldInfo.FieldType;
}
public static object SetManagedReferenceType(this SerializedProperty property, Type type)
{
var obj = (type != null) ? Activator.CreateInstance(type) : null;
property.managedReferenceValue = obj;
return obj;
}
public static string GetManagedReferenceFieldTypeName(this SerializedProperty property)
{
var typeName = property.managedReferenceFieldTypename;
var splitIndex = typeName.IndexOf(' ');
return typeName[(splitIndex + 1)..];
}
public static Type GetManagedReferenceFieldType(this SerializedProperty property)
{
var typeName = property.managedReferenceFieldTypename;
var splitIndex = typeName.IndexOf(' ');
var assembly = Assembly.Load(typeName[..splitIndex]);
return assembly.GetType(typeName[(splitIndex + 1)..]);
}
static UnityEngine.Object GetSerializedPropertyRootObject(SerializedProperty property)
{
return property.serializedObject.targetObject;
}
static T GetNestedObject<T>(string path, object obj, bool includeAllBases = false)
{
var parts = path.Split('.');
for (int i = 0; i < parts.Length; i++)
{
string part = parts[i];
if (part == "Array")
{
var regex = new Regex(@"[^0-9]");
var countText = regex.Replace(parts[i + 1], "");
if (!int.TryParse(countText, out var index))
{
index = -1;
}
obj = GetElementAtOrDefault(obj, index);
i++;
}
else
{
obj = GetFieldOrPropertyValue<object>(part, obj, includeAllBases);
}
}
return (T)obj;
}
static object GetElementAtOrDefault(object arrayOrListObj, int index)
{
if (arrayOrListObj is IEnumerable<object> referenceEnumerable)
{
return referenceEnumerable.ElementAtOrDefault(index);
}
if (arrayOrListObj is IList valueList)
{
object result;
if (index < 0 || index >= valueList.Count)
{
Type listType = valueList.GetType();
Type elementType = listType.IsArray ? listType.GetElementType() : listType.GetGenericArguments()[0];
result = Activator.CreateInstance(elementType);
}
else
{
result = valueList[index];
}
return result;
}
throw new ArgumentException($"Can't parse {arrayOrListObj.GetType()} as Array or List");
}
public static object GetParentObject(this SerializedProperty property)
{
if (property == null) return null;
var path = property.propertyPath.Replace(".Array.data[", "[");
object obj = property.serializedObject.targetObject;
var elements = path.Split('.');
foreach (var element in elements)
{
if (element.Contains("["))
{
var elementName = element[..element.IndexOf("[")];
var index = Convert.ToInt32(element[element.IndexOf("[")..].Replace("[", "").Replace("]", ""));
obj = ReflectionHelper.GetValue(obj, elementName, index);
}
else
{
obj = ReflectionHelper.GetValue(obj, element);
}
}
return obj;
}
static T GetFieldOrPropertyValue<T>(string fieldName, object obj, bool includeAllBases = false, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
{
var field = obj.GetType().GetField(fieldName, bindings);
if (field != null) return (T)field.GetValue(obj);
var property = obj.GetType().GetProperty(fieldName, bindings);
if (property != null) return (T)property.GetValue(obj, null);
if (includeAllBases)
{
foreach (var type in TypeHelper.GetBaseClassesAndInterfaces(obj.GetType()))
{
field = type.GetField(fieldName, bindings);
if (field != null) return (T)field.GetValue(obj);
property = type.GetProperty(fieldName, bindings);
if (property != null) return (T)property.GetValue(obj, null);
}
}
return default;
}
static bool SetFieldOrPropertyValue(string fieldName, object obj, object value, bool includeAllBases = false, BindingFlags bindings = BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic)
{
var field = obj.GetType().GetField(fieldName, bindings);
if (field != null)
{
field.SetValue(obj, value);
return true;
}
var property = obj.GetType().GetProperty(fieldName, bindings);
if (property != null)
{
property.SetValue(obj, value, null);
return true;
}
if (includeAllBases)
{
foreach (var type in TypeHelper.GetBaseClassesAndInterfaces(obj.GetType()))
{
field = type.GetField(fieldName, bindings);
if (field != null)
{
field.SetValue(obj, value);
return true;
}
property = type.GetProperty(fieldName, bindings);
if (property != null)
{
property.SetValue(obj, value, null);
return true;
}
}
}
return false;
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 03e4ebe06bfe34cb69ed7fae43969cec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,54 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace Alchemy.Editor.Internal
{
internal static class TypeHelper
{
public static object GetDefaultValue(Type type)
{
if (!type.IsValueType)
{
return null;
}
return Activator.CreateInstance(type);
}
public static object CreateDefaultInstance(Type type)
{
if (type == typeof(string)) return "";
if (type.IsSubclassOf(typeof(UnityEngine.Object))) return null;
return Activator.CreateInstance(type);
}
public static IEnumerable<Type> GetBaseClassesAndInterfaces(Type type, bool includeSelf = false)
{
List<Type> allTypes = new();
if (includeSelf) allTypes.Add(type);
if (type.BaseType == typeof(object))
{
allTypes.AddRange(type.GetInterfaces());
}
else
{
allTypes.AddRange(
Enumerable.Repeat(type.BaseType, 1)
.Concat(type.GetInterfaces())
.Concat(GetBaseClassesAndInterfaces(type.BaseType))
.Distinct()
);
}
return allTypes;
}
public static bool HasDefaultConstructor(Type type)
{
return type.GetConstructors().Any(t => t.GetParameters().Count() == 0);
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 154d82e962f674436ad4e4d39b9438a3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,14 @@
using System;
using UnityEngine.UIElements;
namespace Alchemy.Editor
{
public abstract class PropertyGroupDrawer
{
public abstract VisualElement CreateRootElement(string label);
public virtual VisualElement GetGroupElement(Attribute attribute) => null;
public string UniqueId => _uniqueId;
internal string _uniqueId;
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6c870ede4caad4eb2accdd389ae0a436
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,48 @@
using System;
using System.Linq;
using System.Reflection;
using UnityEditor;
using UnityEngine.UIElements;
namespace Alchemy.Editor
{
public abstract class PropertyProcessor
{
internal SerializedObject _serializedObject;
internal SerializedProperty _serializedProperty;
internal object _target;
internal MemberInfo _memberInfo;
internal Attribute _attribute;
internal VisualElement _element;
public SerializedObject SerializedObject => _serializedObject;
public SerializedProperty SerializedProperty => _serializedProperty;
public object Target => _target;
public MemberInfo MemberInfo => _memberInfo;
public Attribute Attribute => _attribute;
public VisualElement Element => _element;
public abstract void Execute();
internal static void ExecuteProcessors(SerializedObject serializedObject, SerializedProperty property, object target, MemberInfo memberInfo, VisualElement memberElement)
{
var attributes = memberInfo.GetCustomAttributes();
var processorTypes = TypeCache.GetTypesWithAttribute(typeof(CustomPropertyProcessorAttribute));
foreach (var attribute in attributes)
{
var processorType = processorTypes.FirstOrDefault(x => x.IsSubclassOf(typeof(PropertyProcessor)) && x.GetCustomAttribute<CustomPropertyProcessorAttribute>().targetAttributeType == attribute.GetType());
if (processorType == null) continue;
var processor = (PropertyProcessor)Activator.CreateInstance(processorType);
processor._serializedObject = serializedObject;
processor._serializedProperty = property;
processor._target = target;
processor._memberInfo = memberInfo;
processor._attribute = attribute;
processor._element = memberElement;
processor.Execute();
}
}
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c6e4cd4adfd574c4d8a27f6c20e60102
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,20 @@
using UnityEditor.UIElements;
namespace Alchemy.Editor.Processors
{
public abstract class TrackSerializedObjectPropertyProcessor : PropertyProcessor
{
public override void Execute()
{
Element.TrackSerializedObjectValue(SerializedObject, x =>
{
OnInspectorChanged();
});
OnInspectorChanged();
Element.schedule.Execute(() => OnInspectorChanged());
}
protected abstract void OnInspectorChanged();
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: c20e0e8f7324b4550ac2e5ba8589c972
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 3b82931c0193440458736fd4826c52de
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,90 @@
fileFormatVersion: 2
guid: 28c3206aebe20494dbfa4cf9983b1cee
labels:
- RoslynAnalyzer
PluginImporter:
externalObjects: {}
serializedVersion: 2
iconMap: {}
executionOrder: {}
defineConstraints: []
isPreloaded: 0
isOverridable: 0
isExplicitlyReferenced: 0
validateReferences: 1
platformData:
- first:
: Any
second:
enabled: 0
settings:
Exclude Android: 1
Exclude Editor: 1
Exclude Linux64: 1
Exclude OSXUniversal: 1
Exclude WebGL: 1
Exclude Win: 1
Exclude Win64: 1
Exclude iOS: 1
- first:
Android: Android
second:
enabled: 0
settings:
AndroidSharedLibraryType: Executable
CPU: ARMv7
- first:
Any:
second:
enabled: 0
settings: {}
- first:
Editor: Editor
second:
enabled: 0
settings:
CPU: AnyCPU
DefaultValueInitialized: true
OS: AnyOS
- first:
Standalone: Linux64
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: OSXUniversal
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win
second:
enabled: 0
settings:
CPU: None
- first:
Standalone: Win64
second:
enabled: 0
settings:
CPU: None
- first:
Windows Store Apps: WindowsStoreApps
second:
enabled: 0
settings:
CPU: AnyCPU
- first:
iPhone: iOS
second:
enabled: 0
settings:
AddToEmbeddedBinaries: false
CPU: AnyCPU
CompileFlags:
FrameworkDependencies:
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2b496f89677ec4ca8882579908a7718e
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,23 @@
{
"name": "Alchemy",
"rootNamespace": "",
"references": [
"GUID:2765e68924a08a94ea0ea66b31c0168f",
"GUID:d8b63aba1907145bea998dd612889d6b"
],
"includePlatforms": [],
"excludePlatforms": [],
"allowUnsafeCode": true,
"overrideReferences": false,
"precompiledReferences": [],
"autoReferenced": true,
"defineConstraints": [],
"versionDefines": [
{
"name": "com.unity.serialization",
"expression": "",
"define": "ALCHEMY_SUPPORT_SERIALIZATION"
}
],
"noEngineReferences": false
}

View File

@ -0,0 +1,7 @@
fileFormatVersion: 2
guid: 88be65f96b86746888c927a5c8ff3534
AssemblyDefinitionImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: b55a0053f22e5436bbd5290c70f8b038
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,33 @@
namespace Alchemy.Inspector
{
public sealed class GroupAttribute : PropertyGroupAttribute
{
public GroupAttribute(string groupPath) : base(groupPath) { }
}
public sealed class BoxGroupAttribute : PropertyGroupAttribute
{
public BoxGroupAttribute(string groupPath) : base(groupPath) { }
}
public sealed class TabGroupAttribute : PropertyGroupAttribute
{
public TabGroupAttribute(string groupPath, string tabName) : base(groupPath)
{
TabName = tabName;
}
public string TabName { get; }
}
public sealed class FoldoutGroupAttribute : PropertyGroupAttribute
{
public FoldoutGroupAttribute(string groupPath) : base(groupPath) { }
}
public sealed class HorizontalGroupAttribute : PropertyGroupAttribute
{
public HorizontalGroupAttribute(string groupPath) : base(groupPath) { }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a66a786b5de34430bae43bb49a6b952f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,185 @@
using System;
using UnityEditor;
using UnityEngine;
using UnityEngine.UIElements;
namespace Alchemy.Inspector
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Field | AttributeTargets.Property)]
public sealed class DisableAlchemyEditorAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Class)]
public sealed class HideScriptFieldAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class OrderAttribute : Attribute
{
public OrderAttribute(int order) => Order = order;
public int Order { get; }
}
[AttributeUsage(AttributeTargets.Method)]
public sealed class ButtonAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class ShowInInspectorAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class AssetsOnlyAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class InlineEditorAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class IndentAttribute : Attribute
{
public IndentAttribute(int indent = 1) => this.indent = indent;
public readonly int indent;
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class ReadOnlyAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class HideInPlayModeAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class HideInEditModeAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class DisableInPlayModeAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class DisableInEditModeAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class HideLabelAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class LabelTextAttribute : Attribute
{
public LabelTextAttribute(string text) => Text = text;
public string Text { get; }
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class HideIfAttribute : Attribute
{
public HideIfAttribute(string condition) => Condition = condition;
public string Condition { get; }
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class ShowIfAttribute : Attribute
{
public ShowIfAttribute(string condition) => Condition = condition;
public string Condition { get; }
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class DisableIfAttribute : Attribute
{
public DisableIfAttribute(string condition) => Condition = condition;
public string Condition { get; }
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class EnableIfAttribute : Attribute
{
public EnableIfAttribute(string condition) => Condition = condition;
public string Condition { get; }
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class RequiredAttribute : Attribute
{
public RequiredAttribute() => Message = null;
public RequiredAttribute(string message) => Message = message;
public string Message { get; }
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property)]
public sealed class ValidateInputAttribute : Attribute
{
public ValidateInputAttribute(string condition)
{
Condition = condition;
Message = null;
}
public ValidateInputAttribute(string condition, string message)
{
Condition = condition;
Message = message;
}
public string Condition { get; }
public string Message { get; }
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class HelpBoxAttribute : Attribute
{
public HelpBoxAttribute(string message, HelpBoxMessageType messageType = HelpBoxMessageType.Info)
{
Message = message;
MessageType = messageType;
}
public string Message { get; }
public HelpBoxMessageType MessageType { get; }
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class HorizontalLineAttribute : Attribute
{
public HorizontalLineAttribute()
{
Color = EditorGUIUtility.isProSkin ? new Color(0.4f, 0.4f, 0.4f) : new Color(0.6f, 0.6f, 0.6f);
}
public HorizontalLineAttribute(float r, float g, float b)
{
Color = new Color(r, g, b);
}
public HorizontalLineAttribute(float r, float g, float b, float a)
{
Color = new Color(r, g, b, a);
}
public Color Color { get; }
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class TitleAttribute : Attribute
{
public TitleAttribute(string titleText)
{
TitleText = titleText;
SubitleText = null;
}
public TitleAttribute(string titleText, string subtitle)
{
TitleText = titleText;
SubitleText = subtitle;
}
public string TitleText { get; }
public string SubitleText { get; }
}
[AttributeUsage(AttributeTargets.Field | AttributeTargets.Property | AttributeTargets.Method)]
public sealed class BlockquoteAttribute : Attribute
{
public BlockquoteAttribute(string text)
{
Text = text;
}
public string Text { get; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 57ea17d9b1da64854af7ce0372ad77db
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,17 @@
using System;
namespace Alchemy.Inspector
{
/// <summary>
/// Base class of attributes for creating Group on Inspector
/// </summary>
public abstract class PropertyGroupAttribute : Attribute
{
public PropertyGroupAttribute(string groupPath)
{
GroupPath = groupPath;
}
public string GroupPath { get; }
}
}

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 921947c074aa248eead17b72da8587ab
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: a6213c979732d44e9aa1b5339c5b85c8
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,10 @@
#if ALCHEMY_SUPPORT_SERIALIZATION
namespace Alchemy.Serialization
{
public interface IAlchemySerializationCallbackReceiver
{
void OnBeforeSerialize();
void OnAfterDeserialize();
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 6d700e58a5313415ab4ff85f38be75d2
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,15 @@
#if ALCHEMY_SUPPORT_SERIALIZATION
using System;
namespace Alchemy.Serialization
{
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public sealed class AlchemySerializeAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct, AllowMultiple = false, Inherited = false)]
public sealed class ShowAlchemySerializationDataAttribute : Attribute { }
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public sealed class AlchemySerializeFieldAttribute : Attribute { }
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: face8c3c5e0a54daf99288ded0d16cc0
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,109 @@
#if ALCHEMY_SUPPORT_SERIALIZATION
using System.Collections.Generic;
using Unity.Serialization.Json;
using Unity.Mathematics;
namespace Alchemy.Serialization.Internal
{
public static class SerializationHelper
{
public static string ToJson<T>(T target, IList<UnityEngine.Object> unityObjectReferences)
{
if (target == null)
{
return string.Empty;
}
if (IsUnityObject<T>())
{
var index = unityObjectReferences.IndexOf(target as UnityEngine.Object);
if (index == -1)
{
unityObjectReferences.Add(target as UnityEngine.Object);
index = unityObjectReferences.Count - 1;
}
return index.ToString();
}
return JsonSerialization.ToJson(target, new JsonSerializationParameters()
{
UserDefinedAdapters = new()
{
new UnityObjectAdapter(unityObjectReferences)
},
SerializedType = target.GetType()
});
}
public static T FromJson<T>(string json, IList<UnityEngine.Object> unityObjectReferences)
{
if (string.IsNullOrWhiteSpace(json)) return default;
return ModifiedFromJson<T>(json, new JsonSerializationParameters()
{
UserDefinedAdapters = new()
{
new UnityObjectAdapter(unityObjectReferences)
},
SerializedType = typeof(T)
});
}
public static void FromJsonOverride<T>(string json, ref T container, IList<UnityEngine.Object> unityObjectReferences)
{
if (string.IsNullOrWhiteSpace(json)) return;
ModifiedFromJsonOverride(json, ref container, new JsonSerializationParameters()
{
UserDefinedAdapters = new()
{
new UnityObjectAdapter(unityObjectReferences)
}
});
}
/// <summary>
/// Fixed a bug in the Unity.Serialization package (crash when executed at certain times)
/// Reference: https://forum.unity.com/threads/about-the-com-unity-serialization-package.1512431/
/// </summary>
static unsafe T ModifiedFromJson<T>(string json, JsonSerializationParameters parameters = default)
{
fixed (char* buffer = json)
{
using var reader = new SerializedObjectReader(buffer, json.Length, GetDefaultConfigurationForString(json, parameters));
reader.Read(out var view);
return JsonSerialization.FromJson<T>(view, parameters);
}
}
static unsafe void ModifiedFromJsonOverride<T>(string json, ref T container, JsonSerializationParameters parameters = default)
{
fixed (char* buffer = json)
{
using var reader = new SerializedObjectReader(buffer, json.Length, GetDefaultConfigurationForString(json, parameters));
reader.Read(out var view);
JsonSerialization.FromJsonOverride(view, ref container, parameters);
}
}
/// copied from internal method in JsonSerialization
static SerializedObjectReaderConfiguration GetDefaultConfigurationForString(string json, JsonSerializationParameters parameters = default)
{
var configuration = SerializedObjectReaderConfiguration.Default;
configuration.UseReadAsync = false;
configuration.ValidationType = parameters.DisableValidation ? JsonValidationType.None : parameters.Simplified ? JsonValidationType.Simple : JsonValidationType.Standard;
configuration.BlockBufferSize = math.max(json.Length * sizeof(char), 16);
configuration.TokenBufferSize = math.max(json.Length / 2, 16);
configuration.OutputBufferSize = math.max(json.Length * sizeof(char), 16);
configuration.StripStringEscapeCharacters = parameters.StringEscapeHandling;
return configuration;
}
static bool IsUnityObject<T>()
{
var type = typeof(T);
return type == typeof(UnityEngine.Object) || type.IsSubclassOf(typeof(UnityEngine.Object));
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: a4c4286ac515f4ee9abc6244a1acda54
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,66 @@
#if ALCHEMY_SUPPORT_SERIALIZATION
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Unity.Serialization.Json;
namespace Alchemy.Serialization.Internal
{
public sealed class UnityObjectAdapter : IContravariantJsonAdapter<UnityEngine.Object>, IJsonAdapter<UnityEngine.Object>
{
public UnityObjectAdapter(IList<UnityEngine.Object> objectReferenceList)
{
this.ObjectReferenceList = objectReferenceList;
}
IList<UnityEngine.Object> ObjectReferenceList { get; }
public object Deserialize(IJsonDeserializationContext context)
{
return DeserializeInternal(context.SerializedValue);
}
public UnityEngine.Object Deserialize(in JsonDeserializationContext<UnityEngine.Object> context)
{
return DeserializeInternal(context.SerializedValue);
}
public void Serialize(IJsonSerializationContext context, UnityEngine.Object value)
{
SerializeInternal(context.Writer, value);
}
public void Serialize(in JsonSerializationContext<UnityEngine.Object> context, UnityEngine.Object value)
{
SerializeInternal(context.Writer, value);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
void SerializeInternal(JsonWriter writer, UnityEngine.Object value)
{
if (value == null)
{
writer.WriteNull();
return;
}
var index = ObjectReferenceList.IndexOf(value);
if (index == -1)
{
ObjectReferenceList.Add(value);
index = ObjectReferenceList.Count - 1;
}
writer.WriteValue(index);
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
UnityEngine.Object DeserializeInternal(SerializedValueView view)
{
if (view.IsNull()) return null;
var index = view.AsInt32();
if (index < 0 || index >= ObjectReferenceList.Count) return null;
return ObjectReferenceList[index];
}
}
}
#endif

View File

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: df462af0c52084630ae0e13dbc9596d3
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 55766883e34d9443daef03bbdf57ea2f
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: f5fa6db7b90db4598bada99fed111736
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@ -0,0 +1,17 @@
%YAML 1.1
%TAG !u! tag:unity3d.com,2011:
--- !u!114 &11400000
MonoBehaviour:
m_ObjectHideFlags: 0
m_CorrespondingSourceObject: {fileID: 0}
m_PrefabInstance: {fileID: 0}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: 6655ad5b46ca640999921612370243df, type: 3}
m_Name: SampleScriptableObject
m_EditorClassIdentifier:
foo: 0
bar: {x: 0, y: 0}
baz: {fileID: 0}

Some files were not shown because too many files have changed in this diff Show More