Skip to content

Instantly share code, notes, and snippets.

@Anthelmed
Last active May 12, 2023 19:20
Show Gist options
  • Save Anthelmed/fc5040ca4e036024098ba6228601a40c to your computer and use it in GitHub Desktop.
Save Anthelmed/fc5040ca4e036024098ba6228601a40c to your computer and use it in GitHub Desktop.
Automatically generate for you a MonoBehavior script containing reference to all the VisualElements that have the "auto-reference" uss class, this is updating every time you add or remove an "auto-reference" uss class inside your UXML file. Code is under the MIT license: https://github.com/git/git-scm.com/blob/main/MIT-LICENSE.txt
#if UNITY_EDITOR
using System.Collections.Generic;
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using UnityEditor;
using UnityEngine.UIElements;
namespace Postprocessors
{
internal class UIToolkitAutoReferencesPostprocessor : AssetPostprocessor
{
private const string FolderName = "UIToolkitAutoReferences";
private static readonly string FolderPath = $"Assets/{FolderName}";
private const string FileNameSuffix = "AutoReferences";
private const string AutoReferenceClassName = "auto-reference";
private const string UXMLFileExtension = ".uxml";
private static async void OnPostprocessAllAssets(string[] importedAssetPaths, string[] deletedAssetPaths,
string[] movedAssetPaths,
string[] movedFromAssetPaths)
{
var refreshAssetDatabase = false;
foreach (var assetPath in importedAssetPaths)
{
var fileInfo = new FileInfo(assetPath);
if (!string.Equals(fileInfo.Extension, UXMLFileExtension)) continue;
refreshAssetDatabase = await CreateOrUpdateScriptFile(assetPath);
}
foreach (var assetPath in deletedAssetPaths)
{
var fileInfo = new FileInfo(assetPath);
if (!string.Equals(fileInfo.Extension, UXMLFileExtension)) continue;
refreshAssetDatabase = DeleteScriptFile(assetPath);
}
if (refreshAssetDatabase)
AssetDatabase.Refresh();
}
private static async Task<bool> CreateOrUpdateScriptFile(string assetPath)
{
var refreshAssetDatabase = false;
if (!AssetDatabase.IsValidFolder(FolderPath))
AssetDatabase.CreateFolder("Assets", FolderName);
var fileInfo = new FileInfo(assetPath);
var shortName = ShortName(fileInfo);
var fileName = $"{ToPascalCase(shortName)}{FileNameSuffix}";
var filePath = $"{FolderPath}/{fileName}.cs";
var newContent = GenerateScriptContent(assetPath);
if (File.Exists(filePath))
{
var previousContent = await File.ReadAllTextAsync(filePath);
if (string.Equals(previousContent, newContent)) return false;
refreshAssetDatabase = DeleteScriptFile(assetPath);
}
if (string.IsNullOrEmpty(newContent)) return refreshAssetDatabase;
await File.WriteAllTextAsync(filePath, newContent);
return true;
}
private static bool DeleteScriptFile(string assetPath)
{
var fileInfo = new FileInfo(assetPath);
var shortName = ShortName(fileInfo);
var fileName = $"{ToPascalCase(shortName)}{FileNameSuffix}";
var filePath = $"{FolderPath}/{fileName}.cs";
if (!File.Exists(filePath)) return false;
File.Delete(filePath);
return true;
}
private static string GenerateScriptContent(string assetPath)
{
var fileInfo = new FileInfo(assetPath);
var shortName = ShortName(fileInfo);
var fileName = $"{ToPascalCase(shortName)}{FileNameSuffix}";
var visualTreeAsset = (VisualTreeAsset)AssetDatabase.LoadAssetAtPath(assetPath, typeof(VisualTreeAsset));
var root = visualTreeAsset.CloneTree();
var taggedVisualElements = root.Query(className: AutoReferenceClassName).ToList();
if (taggedVisualElements.Count == 0) return string.Empty;
var content = ScriptContentTemplate;
content = content.Replace(FileNameVariable, fileName);
var privateElementReferences = GeneratePrivateElementReferencesContent(taggedVisualElements);
var publicElementReferences = GeneratePublicElementReferencesContent(taggedVisualElements);
content = content.Replace(PrivateElementReferencesVariable, privateElementReferences);
content = content.Replace(PublicElementReferencesVariable, publicElementReferences);
return content;
}
private static string GeneratePrivateElementReferencesContent(List<VisualElement> visualElements)
{
var elementReferences = new List<string>();
foreach (var visualElement in visualElements)
{
var elementType = visualElement.GetType().Name;
var elementID = visualElement.name;
var elementPrivateName = ToCamelCase(elementID);
var elementReference = PrivateElementReferenceTemplate;
elementReference = elementReference.Replace(ElementTypeVariable, elementType);
elementReference = elementReference.Replace(PrivateElementNameVariable, elementPrivateName);
elementReferences.Add(elementReference);
}
return string.Join("\n", elementReferences);
}
private static string GeneratePublicElementReferencesContent(List<VisualElement> visualElements)
{
var elementReferences = new List<string>();
foreach (var visualElement in visualElements)
{
var elementType = visualElement.GetType().Name;
var elementID = visualElement.name;
var elementPrivateName = ToCamelCase(elementID);
var elementPublicName = ToPascalCase(elementID);
var elementReference = PublicElementReferenceTemplate;
elementReference = elementReference.Replace(ElementTypeVariable, elementType);
elementReference = elementReference.Replace(PrivateElementNameVariable, elementPrivateName);
elementReference = elementReference.Replace(PublicElementNameVariable, elementPublicName);
elementReference = elementReference.Replace(ElementIDVariable, elementID);
elementReferences.Add(elementReference);
}
return string.Join("\n", elementReferences);
}
#region StringExtensionsRegion
private static string ShortName(FileInfo fileInfo)
{
return fileInfo.Name.Replace(fileInfo.Extension, "");
}
private static string ToCamelCase(string value)
{
value = value.Replace(" ", "-");
value = value.Replace("_", "-");
value = value.Replace("--", "-");
value = Regex.Replace(value, "([A-Z])([A-Z]+)($|[A-Z])",
m => $"{m.Groups[1].Value}{m.Groups[2].Value.ToLower()}{m.Groups[3].Value}");
value = Regex.Replace(value, "-.", m => m.Value.ToUpper()[1..]);
value = $"{char.ToLower(value[0])}{value[1..]}";
return value;
}
private static string ToPascalCase(string value)
{
value = ToCamelCase(value);
value = $"{char.ToUpper(value[0])}{value[1..]}";
return value;
}
#endregion
#region TemplateRegion
private const string FileNameVariable = "#FILE_NAME#";
private const string PrivateElementReferencesVariable = "#PRIVATE_ELEMENT_REFERENCES#";
private const string PublicElementReferencesVariable = "#PUBLIC_ELEMENT_REFERENCES#";
private const string ElementTypeVariable = "#ELEMENT_TYPE#";
private const string PrivateElementNameVariable = "#PRIVATE_ELEMENT_NAME#";
private const string PublicElementNameVariable = "#PUBLIC_ELEMENT_NAME#";
private const string ElementIDVariable = "#ELEMENT_ID#";
private const string ScriptContentTemplate = @"
using UnityEngine;
using UnityEngine.UIElements;
/*
* This file was auto-generated by the UIToolkitAutoReferencesPostprocessor class
*/
namespace "+FolderName+@"
{
public class "+FileNameVariable+@" : MonoBehaviour
{
[SerializeField] private UIDocument uiDocument;
private VisualElement _rootVisualElement;
private VisualElement RootVisualElement => _rootVisualElement ??=
uiDocument.rootVisualElement;
"+PrivateElementReferencesVariable+@"
"+PublicElementReferencesVariable+@"
private void Start()
{
if (uiDocument == null)
Debug.LogError($""uiDocument field empty in {name} component"", this);
}
}
}";
private const string PrivateElementReferenceTemplate = @" private "+ElementTypeVariable+@" _"+PrivateElementNameVariable+@";";
private const string PublicElementReferenceTemplate =
@" public " + ElementTypeVariable + @" "+PublicElementNameVariable+@" => _"+PrivateElementNameVariable+@" ??=
RootVisualElement.Q<"+ElementTypeVariable+@">("""+ElementIDVariable+@""");";
#endregion
}
}
#endif
@ChadSpec
Copy link

@Anthelmed Can you clarify what kind of license this gist has and could you please add it to the header? Much appreciated.

@Anthelmed
Copy link
Author

@ChadSpec Hello sorry for the delay, It's using MIT license so you can do whatever you want with it.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment