mirror of
https://github.com/AnnulusGames/Alchemy.git
synced 2025-01-22 00:08:53 -05:00
Initial commit
This commit is contained in:
commit
a720750e46
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# DS_Store
|
||||
*.DS_Store
|
403
Alchemy.SourceGenerator/.gitignore
vendored
Normal file
403
Alchemy.SourceGenerator/.gitignore
vendored
Normal 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
|
14
Alchemy.SourceGenerator/Alchemy.SourceGenerator.csproj
Normal file
14
Alchemy.SourceGenerator/Alchemy.SourceGenerator.csproj
Normal 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>
|
25
Alchemy.SourceGenerator/Alchemy.SourceGenerator.sln
Normal file
25
Alchemy.SourceGenerator/Alchemy.SourceGenerator.sln
Normal 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
|
190
Alchemy.SourceGenerator/AlchemySerializeGenerator.cs
Normal file
190
Alchemy.SourceGenerator/AlchemySerializeGenerator.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
4
Alchemy.SourceGenerator/DiagnosticDescriptors.cs
Normal file
4
Alchemy.SourceGenerator/DiagnosticDescriptors.cs
Normal file
@ -0,0 +1,4 @@
|
||||
namespace Alchemy.SourceGenerator
|
||||
{
|
||||
|
||||
}
|
4
Alchemy/.editorconfig
Normal file
4
Alchemy/.editorconfig
Normal file
@ -0,0 +1,4 @@
|
||||
[*.cs]
|
||||
|
||||
# IDE0027: アクセサーに式本体を使用する
|
||||
csharp_style_expression_bodied_accessors = false
|
78
Alchemy/.gitignore
vendored
Normal file
78
Alchemy/.gitignore
vendored
Normal 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
|
8
Alchemy/Assets/Alchemy.meta
Normal file
8
Alchemy/Assets/Alchemy.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4def02a2f2f60434e82ef3fd321ee6be
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Alchemy/Assets/Alchemy/Editor.meta
Normal file
8
Alchemy/Assets/Alchemy/Editor.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 285cbd8c7df70446c901672504e736c2
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
25
Alchemy/Assets/Alchemy/Editor/Alchemy.Editor.asmdef
Normal file
25
Alchemy/Assets/Alchemy/Editor/Alchemy.Editor.asmdef
Normal 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
|
||||
}
|
7
Alchemy/Assets/Alchemy/Editor/Alchemy.Editor.asmdef.meta
Normal file
7
Alchemy/Assets/Alchemy/Editor/Alchemy.Editor.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5fd453cd0d182422093c4a764fd5eadb
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
71
Alchemy/Assets/Alchemy/Editor/AlchemyEditor.cs
Normal file
71
Alchemy/Assets/Alchemy/Editor/AlchemyEditor.cs
Normal 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 { }
|
||||
}
|
11
Alchemy/Assets/Alchemy/Editor/AlchemyEditor.cs.meta
Normal file
11
Alchemy/Assets/Alchemy/Editor/AlchemyEditor.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 09b0ba97e97f24db4a0dce5a1e942810
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
222
Alchemy/Assets/Alchemy/Editor/BuiltinPropertyGroupDrawers.cs
Normal file
222
Alchemy/Assets/Alchemy/Editor/BuiltinPropertyGroupDrawers.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fa0ce00655a224efd9ed552633ccd715
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
306
Alchemy/Assets/Alchemy/Editor/BuiltinPropertyProcessors.cs
Normal file
306
Alchemy/Assets/Alchemy/Editor/BuiltinPropertyProcessors.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 82c33c1d3a21f4e9a9e57fa46d4900aa
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f0c6d37c5a83b4eef8f5359c67329e56
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e5369cccdbb3a47c0ac7e5fcff993276
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Alchemy/Assets/Alchemy/Editor/Elements.meta
Normal file
8
Alchemy/Assets/Alchemy/Editor/Elements.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4dc283a9b881942e6bcea33ae81ca853
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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;
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 90a3b3c6a5e434218b95ca18fd032a79
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
68
Alchemy/Assets/Alchemy/Editor/Elements/ClassField.cs
Normal file
68
Alchemy/Assets/Alchemy/Editor/Elements/ClassField.cs
Normal 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;
|
||||
}
|
||||
}
|
11
Alchemy/Assets/Alchemy/Editor/Elements/ClassField.cs.meta
Normal file
11
Alchemy/Assets/Alchemy/Editor/Elements/ClassField.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6b645f1746ed04e3dbb48f5e2568f401
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
165
Alchemy/Assets/Alchemy/Editor/Elements/DictionaryField.cs
Normal file
165
Alchemy/Assets/Alchemy/Editor/Elements/DictionaryField.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ef13233afbf8a44f68f8641b03e2303f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
230
Alchemy/Assets/Alchemy/Editor/Elements/GenericField.cs
Normal file
230
Alchemy/Assets/Alchemy/Editor/Elements/GenericField.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
11
Alchemy/Assets/Alchemy/Editor/Elements/GenericField.cs.meta
Normal file
11
Alchemy/Assets/Alchemy/Editor/Elements/GenericField.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 5d48be02c25cf4bc0be72b50cae0a0c7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
171
Alchemy/Assets/Alchemy/Editor/Elements/HashMapFieldBase.cs
Normal file
171
Alchemy/Assets/Alchemy/Editor/Elements/HashMapFieldBase.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4458165df128342d3a2a50b74dcc8c47
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
93
Alchemy/Assets/Alchemy/Editor/Elements/HashSetField.cs
Normal file
93
Alchemy/Assets/Alchemy/Editor/Elements/HashSetField.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
11
Alchemy/Assets/Alchemy/Editor/Elements/HashSetField.cs.meta
Normal file
11
Alchemy/Assets/Alchemy/Editor/Elements/HashSetField.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: ba0b1c41824514728bc50f6ff3d34315
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c644baa37186c4bbe9c006dee446d9a7
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
74
Alchemy/Assets/Alchemy/Editor/Elements/ListField.cs
Normal file
74
Alchemy/Assets/Alchemy/Editor/Elements/ListField.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
11
Alchemy/Assets/Alchemy/Editor/Elements/ListField.cs.meta
Normal file
11
Alchemy/Assets/Alchemy/Editor/Elements/ListField.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d1b638bf9747241989ead75deeb13093
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
71
Alchemy/Assets/Alchemy/Editor/Elements/MethodButton.cs
Normal file
71
Alchemy/Assets/Alchemy/Editor/Elements/MethodButton.cs
Normal 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;
|
||||
}
|
||||
}
|
11
Alchemy/Assets/Alchemy/Editor/Elements/MethodButton.cs.meta
Normal file
11
Alchemy/Assets/Alchemy/Editor/Elements/MethodButton.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 098870ca8e0fc4d5b80c50ffa39f16f3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
39
Alchemy/Assets/Alchemy/Editor/Elements/PropertyListView.cs
Normal file
39
Alchemy/Assets/Alchemy/Editor/Elements/PropertyListView.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f003dc4ef9602443c8a41555882e68b1
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
85
Alchemy/Assets/Alchemy/Editor/Elements/ReflectionField.cs
Normal file
85
Alchemy/Assets/Alchemy/Editor/Elements/ReflectionField.cs
Normal 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;
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e96e973f88c414f90b77487d1a59b89d
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03f2015425fd34b8cb49aea8e59f9745
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Alchemy/Assets/Alchemy/Editor/Internal.meta
Normal file
8
Alchemy/Assets/Alchemy/Editor/Internal.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9243210c5444145978ade99e7a7af0c1
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
10
Alchemy/Assets/Alchemy/Editor/Internal/EditorIcons.cs
Normal file
10
Alchemy/Assets/Alchemy/Editor/Internal/EditorIcons.cs
Normal 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");
|
||||
}
|
||||
}
|
11
Alchemy/Assets/Alchemy/Editor/Internal/EditorIcons.cs.meta
Normal file
11
Alchemy/Assets/Alchemy/Editor/Internal/EditorIcons.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b5d3e43f293fb4a7ebcba1e3b2b91f2b
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
103
Alchemy/Assets/Alchemy/Editor/Internal/GUIHelper.cs
Normal file
103
Alchemy/Assets/Alchemy/Editor/Internal/GUIHelper.cs
Normal 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);
|
||||
});
|
||||
|
||||
}
|
||||
}
|
||||
}
|
11
Alchemy/Assets/Alchemy/Editor/Internal/GUIHelper.cs.meta
Normal file
11
Alchemy/Assets/Alchemy/Editor/Internal/GUIHelper.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b23551d43228a43c9a9207cf8f150834
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
295
Alchemy/Assets/Alchemy/Editor/Internal/InspectorHelper.cs
Normal file
295
Alchemy/Assets/Alchemy/Editor/Internal/InspectorHelper.cs
Normal 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;
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 664a7f04961bd4d6fb7fa0bcaf290687
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
53
Alchemy/Assets/Alchemy/Editor/Internal/InternalAPIHelper.cs
Normal file
53
Alchemy/Assets/Alchemy/Editor/Internal/InternalAPIHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: bd2e9148da43349219f92b909651fbdc
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 4786119bc20d840f7b77860f7b16e18c
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
298
Alchemy/Assets/Alchemy/Editor/Internal/ReflectionHelper.cs
Normal file
298
Alchemy/Assets/Alchemy/Editor/Internal/ReflectionHelper.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 7ddc52eddda264fba8fea9844f3c0f61
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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 };
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: fc340cedf3cee4da19aae647ce6299ce
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,11 @@
|
||||
using System;
|
||||
using System.Reflection;
|
||||
using UnityEditor;
|
||||
|
||||
namespace Alchemy.Editor.Internal
|
||||
{
|
||||
public static class SerializedObjectExtensions
|
||||
{
|
||||
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: e70b6aca000bb4a698521391868e2f98
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 03e4ebe06bfe34cb69ed7fae43969cec
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
54
Alchemy/Assets/Alchemy/Editor/Internal/TypeHelper.cs
Normal file
54
Alchemy/Assets/Alchemy/Editor/Internal/TypeHelper.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
11
Alchemy/Assets/Alchemy/Editor/Internal/TypeHelper.cs.meta
Normal file
11
Alchemy/Assets/Alchemy/Editor/Internal/TypeHelper.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 154d82e962f674436ad4e4d39b9438a3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
14
Alchemy/Assets/Alchemy/Editor/PropertyGroupDrawer.cs
Normal file
14
Alchemy/Assets/Alchemy/Editor/PropertyGroupDrawer.cs
Normal 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;
|
||||
}
|
||||
}
|
11
Alchemy/Assets/Alchemy/Editor/PropertyGroupDrawer.cs.meta
Normal file
11
Alchemy/Assets/Alchemy/Editor/PropertyGroupDrawer.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6c870ede4caad4eb2accdd389ae0a436
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
48
Alchemy/Assets/Alchemy/Editor/PropertyProcessor.cs
Normal file
48
Alchemy/Assets/Alchemy/Editor/PropertyProcessor.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
11
Alchemy/Assets/Alchemy/Editor/PropertyProcessor.cs.meta
Normal file
11
Alchemy/Assets/Alchemy/Editor/PropertyProcessor.cs.meta
Normal file
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c6e4cd4adfd574c4d8a27f6c20e60102
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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();
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: c20e0e8f7324b4550ac2e5ba8589c972
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Alchemy/Assets/Alchemy/Generator.meta
Normal file
8
Alchemy/Assets/Alchemy/Generator.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 3b82931c0193440458736fd4826c52de
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
BIN
Alchemy/Assets/Alchemy/Generator/Alchemy.SourceGenerator.dll
Normal file
BIN
Alchemy/Assets/Alchemy/Generator/Alchemy.SourceGenerator.dll
Normal file
Binary file not shown.
@ -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:
|
8
Alchemy/Assets/Alchemy/Runtime.meta
Normal file
8
Alchemy/Assets/Alchemy/Runtime.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 2b496f89677ec4ca8882579908a7718e
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
23
Alchemy/Assets/Alchemy/Runtime/Alchemy.asmdef
Normal file
23
Alchemy/Assets/Alchemy/Runtime/Alchemy.asmdef
Normal 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
|
||||
}
|
7
Alchemy/Assets/Alchemy/Runtime/Alchemy.asmdef.meta
Normal file
7
Alchemy/Assets/Alchemy/Runtime/Alchemy.asmdef.meta
Normal file
@ -0,0 +1,7 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 88be65f96b86746888c927a5c8ff3534
|
||||
AssemblyDefinitionImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Alchemy/Assets/Alchemy/Runtime/Inspector.meta
Normal file
8
Alchemy/Assets/Alchemy/Runtime/Inspector.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: b55a0053f22e5436bbd5290c70f8b038
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
33
Alchemy/Assets/Alchemy/Runtime/Inspector/GroupAttributes.cs
Normal file
33
Alchemy/Assets/Alchemy/Runtime/Inspector/GroupAttributes.cs
Normal 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) { }
|
||||
}
|
||||
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a66a786b5de34430bae43bb49a6b952f
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
185
Alchemy/Assets/Alchemy/Runtime/Inspector/InspectorAttributes.cs
Normal file
185
Alchemy/Assets/Alchemy/Runtime/Inspector/InspectorAttributes.cs
Normal 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; }
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 57ea17d9b1da64854af7ce0372ad77db
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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; }
|
||||
}
|
||||
}
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 921947c074aa248eead17b72da8587ab
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Alchemy/Assets/Alchemy/Runtime/Serialization.meta
Normal file
8
Alchemy/Assets/Alchemy/Runtime/Serialization.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a6213c979732d44e9aa1b5339c5b85c8
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -0,0 +1,10 @@
|
||||
#if ALCHEMY_SUPPORT_SERIALIZATION
|
||||
namespace Alchemy.Serialization
|
||||
{
|
||||
public interface IAlchemySerializationCallbackReceiver
|
||||
{
|
||||
void OnBeforeSerialize();
|
||||
void OnAfterDeserialize();
|
||||
}
|
||||
}
|
||||
#endif
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 6d700e58a5313415ab4ff85f38be75d2
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: face8c3c5e0a54daf99288ded0d16cc0
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: a4c4286ac515f4ee9abc6244a1acda54
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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
|
@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: df462af0c52084630ae0e13dbc9596d3
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Alchemy/Assets/Alchemy/Samples~.meta
Normal file
8
Alchemy/Assets/Alchemy/Samples~.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 55766883e34d9443daef03bbdf57ea2f
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
8
Alchemy/Assets/Alchemy/Samples~/Samples.meta
Normal file
8
Alchemy/Assets/Alchemy/Samples~/Samples.meta
Normal file
@ -0,0 +1,8 @@
|
||||
fileFormatVersion: 2
|
||||
guid: f5fa6db7b90db4598bada99fed111736
|
||||
folderAsset: yes
|
||||
DefaultImporter:
|
||||
externalObjects: {}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
@ -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
Loading…
Reference in New Issue
Block a user