You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 

425 lines
17 KiB

using System;
using System.Collections.Generic;
using System.IO;
using Unity.UI.Builder;
using UnityEditor.Experimental;
using UnityEditor.VFX.UI;
using UnityEngine;
using UnityEngine.UIElements;
using UnityEngine.VFX;
namespace UnityEditor.VFX
{
internal interface IVFXTemplateDescriptor
{
string header { get; }
}
internal class VFXTemplateWindow : EditorWindow
{
internal interface ISaveFileDialogHelper
{
string OpenSaveFileDialog(string title, string defaultName, string extension, string message);
}
private class SaveFileDialogHelper : ISaveFileDialogHelper
{
public string OpenSaveFileDialog(string title, string defaultName, string extension, string message) => EditorUtility.SaveFilePanelInProject(title, defaultName, extension, message);
}
private class VFXTemplateSection : IVFXTemplateDescriptor
{
public VFXTemplateSection(string text)
{
header = text;
}
public string header { get; }
}
private const string VFXTemplateWindowDocUrl = "https://docs.unity3d.com/Packages/com.unity.visualeffectgraph@{0}/manual/Templates-window.html";
private const string BuiltInCategory = "Default VFX Graph Templates";
private const string EmptyTemplateName = "Empty VFX";
private const string EmptyTemplateDescription = "Create a completely empty VFX asset";
private const string LastSelectedGuidKey = "VFXTemplateWindow.LastSelectedGuid";
private const string CreateNewVFXAssetTitle = "Create new VFX Asset";
private const string InsertTemplateTitle = "Insert a template into current VFX Asset";
private static readonly Dictionary<CreateMode, string> s_ModeToTitle = new ()
{
{ CreateMode.CreateNew, CreateNewVFXAssetTitle },
{ CreateMode.Insert, InsertTemplateTitle },
{ CreateMode.None, CreateNewVFXAssetTitle },
};
private readonly List<TreeViewItemData<IVFXTemplateDescriptor>> m_TemplatesTree = new ();
private readonly ISaveFileDialogHelper m_SaveFileDialogHelper = new SaveFileDialogHelper();
private TreeView m_ListOfTemplates;
private Texture2D m_CustomTemplateIcon;
private Image m_DetailsScreenshot;
private Label m_DetailsTitle;
private Label m_DetailsDescription;
private VisualTreeAsset m_ItemTemplate;
private Action<string> m_VFXAssetCreationCallback;
private string m_LastSelectedTemplatePath;
private int m_LastSelectedIndex;
private CreateMode m_CurrentMode;
private Action<string> m_UserCallback;
private string m_LastSelectedTemplateGuid;
private VFXView m_VfxView;
private VFXTemplateDescriptor m_SelectedTemplate;
private enum CreateMode
{
CreateNew,
Insert,
None,
}
public static void ShowCreateFromTemplate(VFXView vfxView, Action<string> callback) => ShowInternal(vfxView, CreateMode.CreateNew, callback);
public static void ShowInsertTemplate(VFXView vfxView) => ShowInternal(vfxView, CreateMode.Insert);
public static void PickTemplate(Action<string> callback) => ShowInternal(null, CreateMode.None, callback);
private static void ShowInternal(VFXView vfxView, CreateMode mode, Action<string> callback = null)
{
var templateWindow = EditorWindow.GetWindow<VFXTemplateWindow>(true, s_ModeToTitle[mode], false);
templateWindow.Setup(vfxView, mode, callback);
}
private void Setup(VFXView vfxView, CreateMode mode, Action<string> callback)
{
minSize = new Vector2(800, 300);
m_VfxView = vfxView;
m_UserCallback = callback;
m_CurrentMode = mode;
SetCallBack();
LoadTemplates();
}
private void CreateGUI()
{
m_ItemTemplate = VFXView.LoadUXML("VFXTemplateItem");
var tpl = VFXView.LoadUXML("VFXTemplateWindow");
tpl.CloneTree(rootVisualElement);
rootVisualElement.AddStyleSheetPath("VFXTemplateWindow");
rootVisualElement.name = "VFXTemplateWindowRoot";
rootVisualElement.Q<Button>("CreateButton").clicked += OnCreate;
rootVisualElement.Q<Button>("CancelButton").clicked += OnCancel;
m_CustomTemplateIcon = EditorGUIUtility.LoadIcon(Path.Combine(VisualEffectGraphPackageInfo.assetPackagePath, "Editor/Templates/UI/CustomVFXGraph@256.png"));
m_DetailsScreenshot = rootVisualElement.Q<Image>("Screenshot");
m_DetailsScreenshot.scaleMode = ScaleMode.ScaleAndCrop;
m_DetailsTitle = rootVisualElement.Q<Label>("Title");
m_DetailsDescription = rootVisualElement.Q<Label>("Description");
var helpButton = rootVisualElement.Q<Button>("HelpButton");
helpButton.clicked += OnOpenHelp;
var helpImage = helpButton.Q<Image>("HelpImage");
helpImage.image = EditorGUIUtility.LoadIcon(EditorResources.iconsPath + "_Help.png");
m_ListOfTemplates = rootVisualElement.Q<TreeView>("ListOfTemplates");
m_ListOfTemplates.virtualizationMethod = CollectionVirtualizationMethod.DynamicHeight;
m_ListOfTemplates.makeItem = CreateTemplateItem;
m_ListOfTemplates.bindItem = BindTemplateItem;
m_ListOfTemplates.unbindItem = UnbindTemplateItem;
m_ListOfTemplates.selectionChanged += OnSelectionChanged;
}
private void SetCallBack()
{
switch (m_CurrentMode)
{
case CreateMode.CreateNew:
m_VFXAssetCreationCallback = templatePath => CreateNewVisualEffect(templatePath, m_UserCallback);
break;
case CreateMode.Insert:
m_VFXAssetCreationCallback = InsertTemplateInVisualEffect;
break;
case CreateMode.None:
m_VFXAssetCreationCallback = m_UserCallback;
break;
default:
throw new ArgumentOutOfRangeException(nameof(m_CurrentMode), m_CurrentMode, null);
}
}
private void OnOpenHelp() => Help.BrowseURL(string.Format(VFXTemplateWindowDocUrl, VFXHelpURLAttribute.version));
private void LoadTemplates()
{
m_LastSelectedTemplateGuid = EditorPrefs.GetString(LastSelectedGuidKey);
CollectTemplates();
m_ListOfTemplates.ExpandAll();
}
private void OnEnable()
{
AssemblyReloadEvents.beforeAssemblyReload += OnBeforeAssemblyReload;
}
private void OnDisable()
{
AssemblyReloadEvents.beforeAssemblyReload -= OnBeforeAssemblyReload;
}
private void OnBeforeAssemblyReload()
{
this.Close();
}
private void OnDestroy()
{
EditorPrefs.SetString(LastSelectedGuidKey, m_LastSelectedTemplateGuid);
}
private void OnCancel()
{
m_LastSelectedTemplatePath = null;
m_VFXAssetCreationCallback?.Invoke(m_LastSelectedTemplatePath);
Close();
}
private void OnCreate()
{
var template = m_ListOfTemplates.selectedIndex != -1 ? (VFXTemplateDescriptor)m_ListOfTemplates.selectedItem : m_SelectedTemplate;
m_LastSelectedTemplatePath = AssetDatabase.GUIDToAssetPath(template.assetGuid);
m_VFXAssetCreationCallback?.Invoke(m_LastSelectedTemplatePath);
Close();
VFXAnalytics.GetInstance().OnSystemTemplateCreated(template.name);
m_VfxView = null;
m_VFXAssetCreationCallback = null;
}
private void CreateNewVisualEffect(string templatePath, Action<string> userCallback)
{
if (templatePath == null)
{
return;
}
var assetPath = m_SaveFileDialogHelper.OpenSaveFileDialog("", "New VFX", "vfx", "Create new VisualEffect Graph");
if (!string.IsNullOrEmpty(assetPath))
{
VisualEffectAssetEditorUtility.CreateTemplateAsset(assetPath, templatePath);
//Only null check on view is expected, it avoids GetViewWindow call
//The resource can be invalidated due to previous write on same asset from CreateTemplateAsset
if (m_VfxView != null)
{
var vfxAsset = AssetDatabase.LoadAssetAtPath<VisualEffectAsset>(assetPath);
//If m_VfxView displayed resource is null but asset isn't
//GetWindow lambda already fallback to "No Asset" or already opened window
var window = VFXViewWindow.GetWindow(vfxAsset, true);
window.LoadAsset(vfxAsset, null);
}
userCallback?.Invoke(assetPath);
}
}
private void InsertTemplateInVisualEffect(string templatePath)
{
if (!string.IsNullOrEmpty(templatePath))
{
if (GetViewWindow() is {} window)
{
window.graphView.CreateTemplateSystem(templatePath, Vector2.zero, null);
}
else
{
Close();
}
}
}
private VFXViewWindow GetViewWindow() => m_VfxView != null ? VFXViewWindow.GetWindow(m_VfxView) : null;
private void OnSelectionChanged(IEnumerable<object> obj)
{
if (obj == null)
throw new ArgumentNullException(nameof(obj));
var list = new List<object>(obj);
if (list.Count == 1)
{
if (list[0] is VFXTemplateDescriptor template)
{
m_SelectedTemplate = template;
m_DetailsTitle.text = template.name;
m_DetailsDescription.text = template.description;
m_LastSelectedTemplateGuid = template.assetGuid;
m_LastSelectedIndex = m_ListOfTemplates.selectedIndex;
m_DetailsScreenshot.image = template.thumbnail;
// Maybe set a placeholder screenshot when null
}
else
{
m_ListOfTemplates.selectedIndex = m_LastSelectedIndex;
}
}
else
{
throw new NotSupportedException("Cannot select multiple templates");
}
}
private void BindTemplateItem(VisualElement item, int index)
{
var data = m_ListOfTemplates.GetItemDataForIndex<IVFXTemplateDescriptor>(index);
var label = item.Q<Label>("TemplateName");
label.text = data.header;
string ussClass;
if (data is VFXTemplateDescriptor template)
{
item.Q<Image>("TemplateIcon").image = template.icon != null ? template.icon : m_CustomTemplateIcon;
if (template.assetGuid == m_LastSelectedTemplateGuid)
m_ListOfTemplates.SetSelection(index);
ussClass = "vfxtemplate-item";
item.RegisterCallback<ClickEvent>(OnClickItem);
}
else
{
// This is a hack to put the expand/collapse button above the item so that we can interact with it
var toggle = item.parent.parent.Q<Toggle>();
toggle.BringToFront();
ussClass = "vfxtemplate-section";
}
if (item.GetFirstAncestorWithClass("unity-tree-view__item") is { } parent)
{
parent.AddToClassList(ussClass);
}
}
private void UnbindTemplateItem(VisualElement item, int index)
{
if (item.GetFirstAncestorWithClass("unity-tree-view__item") is { } parent)
{
parent.RemoveFromClassList("vfxtemplate-item");
parent.RemoveFromClassList("vfxtemplate-section");
}
item.UnregisterCallback<ClickEvent>(OnClickItem);
}
private void OnClickItem(ClickEvent evt)
{
if (evt.clickCount == 2 && m_ListOfTemplates.selectedItem != null)
{
OnCreate();
}
}
private VisualElement CreateTemplateItem() => m_ItemTemplate.Instantiate();
private void CollectTemplates()
{
m_TemplatesTree.Clear();
var vfxAssetsGuid = AssetDatabase.FindAssets("t:VisualEffectAsset");
var allTemplates = new List<VFXTemplateDescriptor>(vfxAssetsGuid.Length);
foreach (var guid in vfxAssetsGuid)
{
var assetPath = AssetDatabase.GUIDToAssetPath(guid);
if (VFXTemplateHelper.TryGetTemplate(assetPath, out var template))
{
var isBuiltIn = assetPath.StartsWith(VisualEffectAssetEditorUtility.templatePath);
template.category = isBuiltIn ? BuiltInCategory : template.category;
template.order = isBuiltIn ? 0 : 1;
template.assetGuid = guid;
if (isBuiltIn)
{
template.icon = GetSkinIcon(template.icon);
}
allTemplates.Add(template);
}
}
if (m_CurrentMode != CreateMode.Insert)
{
allTemplates.Add(MakeEmptyTemplate());
}
var templatesGroupedByCategory = new Dictionary<string, List<VFXTemplateDescriptor>>();
foreach (var template in allTemplates)
{
if (templatesGroupedByCategory.TryGetValue(template.category, out var list))
{
list.Add(template);
}
else
{
list = new List<VFXTemplateDescriptor> { template };
templatesGroupedByCategory[template.category] = list;
}
}
// This is to prevent collapse/expand if there's only one category
if (templatesGroupedByCategory.Count == 1)
{
m_ListOfTemplates.AddToClassList("remove-toggle");
}
else
{
m_ListOfTemplates.RemoveFromClassList("remove-toggle");
}
var templates = new List<List<VFXTemplateDescriptor>>(templatesGroupedByCategory.Values);
templates.Sort((listA, listB) => listA[0].order.CompareTo(listB[0].order));
var id = 0;
var lastSelectedTemplateFound = false;
var fallBackTemplateAssetGuid = string.Empty;
foreach (var group in templates)
{
var groupId = id++;
var children = new List<TreeViewItemData<IVFXTemplateDescriptor>>(group.Count);
foreach (var child in group)
{
if (id == 2)
fallBackTemplateAssetGuid = child.assetGuid;
if (child.assetGuid == m_LastSelectedTemplateGuid)
lastSelectedTemplateFound = true;
children.Add(new TreeViewItemData<IVFXTemplateDescriptor>(id++, child));
}
var section = new TreeViewItemData<IVFXTemplateDescriptor>(groupId, new VFXTemplateSection(group[0].category), children);
m_TemplatesTree.Add(section);
}
m_ListOfTemplates.SetRootItems(m_TemplatesTree);
if (!lastSelectedTemplateFound)
{
m_LastSelectedTemplateGuid = fallBackTemplateAssetGuid;
}
}
private Texture2D GetSkinIcon(Texture2D templateIcon)
{
if (EditorGUIUtility.skinIndex == 0)
{
return templateIcon;
}
var path = AssetDatabase.GetAssetPath(templateIcon);
return EditorGUIUtility.LoadIcon(path);
}
private VFXTemplateDescriptor MakeEmptyTemplate()
{
return new VFXTemplateDescriptor
{
name = EmptyTemplateName,
icon = EditorGUIUtility.LoadIcon(Path.Combine(VisualEffectGraphPackageInfo.assetPackagePath, "Editor/Templates/UI/EmptyTemplate@256.png")),
thumbnail = EditorGUIUtility.LoadIcon(Path.Combine(VisualEffectGraphPackageInfo.assetPackagePath, "Editor/Templates/UI/3d_Empty.png")),
category = BuiltInCategory,
description = EmptyTemplateDescription,
assetGuid = "empty",
};
}
}
}