Files
FlaxEngine/Source/Editor/CustomEditors/Dedicated/ActorEditor.cs
2025-12-16 00:15:29 +02:00

599 lines
25 KiB
C#

// Copyright (c) Wojciech Figat. All rights reserved.
using FlaxEditor.Actions;
using FlaxEditor.CustomEditors.Editors;
using FlaxEditor.CustomEditors.Elements;
using FlaxEditor.GUI;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Tree;
using FlaxEditor.Scripting;
using FlaxEditor.Windows.Assets;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Json;
using FlaxEngine.Utilities;
using System;
using System.Collections.Generic;
using System.Linq;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Dedicated custom editor for <see cref="Actor"/> objects.
/// </summary>
/// <seealso cref="FlaxEditor.CustomEditors.Editors.GenericEditor" />
[CustomEditor(typeof(Actor)), DefaultEditor]
public class ActorEditor : ScriptingObjectEditor
{
private Guid _linkedPrefabId;
/// <inheritdoc />
protected override List<ItemInfo> GetItemsForType(ScriptType type)
{
var items = base.GetItemsForType(type);
// Inject scripts editor
var scriptsMember = type.GetProperty("Scripts");
if (scriptsMember != ScriptMemberInfo.Null)
{
var item = new ItemInfo(scriptsMember)
{
CustomEditor = new CustomEditorAttribute(typeof(ScriptsEditor))
};
items.Add(item);
}
return items;
}
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
// Check for prefab link
var actor = Values.Count == 1 ? Values[0] as Actor : null;
if (actor != null && actor.HasPrefabLink)
{
// TODO: consider editing more than one instance of the same prefab asset at once
var prefab = FlaxEngine.Content.LoadAsync<Prefab>(actor.PrefabID);
// TODO: don't stall here?
if (prefab && !prefab.WaitForLoaded())
{
var prefabObjectId = actor.PrefabObjectID;
var prefabInstance = prefab.GetDefaultInstance(ref prefabObjectId);
if (prefabInstance != null)
{
// Use default prefab instance as a reference for the editor
Values.SetReferenceValue(prefabInstance);
// Display prefab UI (when displaying object inside Prefab Window then display only nested prefabs)
prefab.GetNestedObject(ref prefabObjectId, out var nestedPrefabId, out var nestedPrefabObjectId);
var nestedPrefab = FlaxEngine.Content.Load<Prefab>(nestedPrefabId);
var panel = layout.UniformGrid();
panel.CustomControl.Height = 20.0f;
panel.CustomControl.SlotsVertically = 1;
if (Presenter == Editor.Instance.Windows.PropertiesWin.Presenter || nestedPrefab)
{
var targetPrefab = nestedPrefab ?? prefab;
panel.CustomControl.SlotsHorizontally = 3;
// Selecting actor prefab asset
var selectPrefab = panel.Button("Select Prefab");
selectPrefab.Button.Clicked += () =>
{
Editor.Instance.Windows.ContentWin.ClearItemsSearch();
Editor.Instance.Windows.ContentWin.Select(targetPrefab);
};
// Edit selected prefab asset
var editPrefab = panel.Button("Edit Prefab");
editPrefab.Button.Clicked += () => Editor.Instance.Windows.ContentWin.Open(Editor.Instance.ContentDatabase.FindAsset(targetPrefab.ID));
}
else
{
panel.CustomControl.SlotsHorizontally = 1;
}
// Viewing changes applied to this actor
var viewChanges = panel.Button("View Changes");
viewChanges.Button.Clicked += () => ViewChanges(viewChanges.Button, new Float2(0.0f, 20.0f));
// Link event to update editor on prefab apply
_linkedPrefabId = prefab.ID;
Editor.Instance.Prefabs.PrefabApplying += OnPrefabApplying;
Editor.Instance.Prefabs.PrefabApplied += OnPrefabApplied;
}
}
}
base.Initialize(layout);
// Add custom settings button to General group
for (int i = 0; i < layout.Children.Count; i++)
{
if (layout.Children[i] is GroupElement group && group.Panel.HeaderText == "General")
{
if (actor != null)
group.Panel.TooltipText = Surface.SurfaceUtils.GetVisualScriptTypeDescription(TypeUtils.GetObjectType(actor));
var settingsButton = group.AddSettingsButton();
settingsButton.Clicked += OnSettingsButtonClicked;
break;
}
}
}
private void OnSettingsButtonClicked(Image image, MouseButton mouseButton)
{
if (mouseButton != MouseButton.Left)
return;
var cm = new ContextMenu();
var actor = (Actor)Values[0];
var scriptType = TypeUtils.GetType(actor.TypeName);
var item = scriptType.ContentItem;
if (Presenter.Owner != null)
{
var lockButton = cm.AddButton(Presenter.Owner.LockSelection ? "Unlock" : "Lock");
lockButton.ButtonClicked += button =>
{
var owner = Presenter?.Owner;
if (owner == null)
return;
owner.LockSelection = !owner.LockSelection;
// Reselect current selection
if (!owner.LockSelection && owner.Selection.Count > 0)
{
var cachedSelection = owner.Selection.ToList();
owner.Select(null);
owner.Select(cachedSelection);
}
};
}
cm.AddButton("Copy ID", OnClickCopyId);
cm.AddButton("Edit actor type", OnClickEditActorType).Enabled = item != null;
var showButton = cm.AddButton("Show in content window", OnClickShowActorType);
showButton.Enabled = item != null;
showButton.Icon = Editor.Instance.Icons.Search12;
cm.Show(image, image.Size);
}
private void OnClickCopyId()
{
var actor = (Actor)Values[0];
Clipboard.Text = JsonSerializer.GetStringID(actor.ID);
}
private void OnClickEditActorType()
{
var actor = (Actor)Values[0];
var scriptType = TypeUtils.GetType(actor.TypeName);
var item = scriptType.ContentItem;
if (item != null)
Editor.Instance.ContentEditing.Open(item);
}
private void OnClickShowActorType()
{
var actor = (Actor)Values[0];
var scriptType = TypeUtils.GetType(actor.TypeName);
var item = scriptType.ContentItem;
if (item != null)
Editor.Instance.Windows.ContentWin.Select(item);
}
/// <inheritdoc />
protected override void Deinitialize()
{
base.Deinitialize();
if (_linkedPrefabId != Guid.Empty)
{
_linkedPrefabId = Guid.Empty;
Editor.Instance.Prefabs.PrefabApplying -= OnPrefabApplying;
Editor.Instance.Prefabs.PrefabApplied -= OnPrefabApplied;
}
}
private void OnPrefabApplied(Prefab prefab, Actor instance)
{
if (prefab.ID == _linkedPrefabId)
{
// This works fine but in PrefabWindow when using live update it crashes on using color picker/float slider because UI is being rebuild
//Presenter.BuildLayoutOnUpdate();
// Better way is to just update the reference value using the new default instance of the prefab, created after changes apply
if (Values != null && prefab && !prefab.WaitForLoaded())
{
var actor = (Actor)Values[0];
var prefabObjectId = actor.PrefabObjectID;
var prefabInstance = prefab.GetDefaultInstance(ref prefabObjectId);
if (prefabInstance != null)
{
Values.SetReferenceValue(prefabInstance);
RefreshReferenceValue();
}
}
}
}
private void OnPrefabApplying(Prefab prefab, Actor instance)
{
if (prefab.ID == _linkedPrefabId)
{
// Unlink reference value (it gets deleted by the prefabs system during apply)
ClearReferenceValueAll();
}
}
private TreeNode CreateDiffNode(CustomEditor editor)
{
var node = new TreeNode(false)
{
Tag = editor
};
// Removed Script
if (editor is RemovedScriptDummy removed)
{
node.TextColor = Color.OrangeRed;
node.Text = Utilities.Utils.GetPropertyNameUI(removed.PrefabObject.GetType().Name);
}
// Removed Actor
else if (editor is RemovedActorDummy removedActor)
{
node.TextColor = Color.OrangeRed;
node.Text = $"{removedActor.PrefabObject.Name} ({Utilities.Utils.GetPropertyNameUI(removedActor.PrefabObject.GetType().Name)})";
}
// Actor or Script
else if (editor.Values[0] is SceneObject sceneObject)
{
node.TextColor = sceneObject.HasPrefabLink ? FlaxEngine.GUI.Style.Current.ProgressNormal : FlaxEngine.GUI.Style.Current.BackgroundSelected;
if (editor.Values.Info != ScriptMemberInfo.Null)
{
if (editor.Values.GetAttributes().FirstOrDefault(x => x is EditorDisplayAttribute) is EditorDisplayAttribute editorDisplayAttribute && !string.IsNullOrEmpty(editorDisplayAttribute.Name))
node.Text = $"{Utilities.Utils.GetPropertyNameUI(editorDisplayAttribute.Name)} ({Utilities.Utils.GetPropertyNameUI(editor.Values.Info.Name)})";
else
node.Text = Utilities.Utils.GetPropertyNameUI(editor.Values.Info.Name);
}
else if (sceneObject is Actor actor)
node.Text = $"{actor.Name} ({Utilities.Utils.GetPropertyNameUI(sceneObject.GetType().Name)})";
else
node.Text = Utilities.Utils.GetPropertyNameUI(sceneObject.GetType().Name);
}
// Array Item
else if (editor.ParentEditor is CollectionEditor)
{
node.Text = "Element " + editor.ParentEditor.ChildrenEditors.IndexOf(editor);
}
// Common type
else if (editor.Values.Info != ScriptMemberInfo.Null)
{
if (editor.Values.GetAttributes().FirstOrDefault(x => x is EditorDisplayAttribute) is EditorDisplayAttribute editorDisplayAttribute
&& !string.IsNullOrEmpty(editorDisplayAttribute.Name)
&& !editorDisplayAttribute.Name.Contains("_inline"))
node.Text = $"{Utilities.Utils.GetPropertyNameUI(editorDisplayAttribute.Name)} ({Utilities.Utils.GetPropertyNameUI(editor.Values.Info.Name)})";
else
node.Text = Utilities.Utils.GetPropertyNameUI(editor.Values.Info.Name);
}
// Custom type
else if (editor.Values[0] != null)
{
node.Text = editor.Values[0].ToString();
}
node.Expand(true);
return node;
}
private class RemovedScriptDummy : CustomEditor
{
/// <summary>
/// The removed prefab object (from the prefab default instance).
/// </summary>
public Script PrefabObject;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
// Not used
}
}
private class RemovedActorDummy : CustomEditor
{
/// <summary>
/// The removed prefab object (from the prefab default instance).
/// </summary>
public Actor PrefabObject;
/// <summary>
/// The prefab instance's parent.
/// </summary>
public Actor ParentActor;
/// <summary>
/// The order of the removed actor in the parent.
/// </summary>
public int OrderInParent;
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
// Not used
}
}
private TreeNode ProcessDiff(CustomEditor editor, bool skipIfNotModified = true)
{
// Special case for new Script or child actor added to actor
if ((editor.Values[0] is Script script && !script.HasPrefabLink) || (editor.Values[0] is Actor a && !a.HasPrefabLink))
return CreateDiffNode(editor);
// Skip if no change detected
var isRefEdited = editor.Values.IsReferenceValueModified;
if (!isRefEdited && skipIfNotModified && editor is not ScriptsEditor)
return null;
TreeNode result = null;
if (editor.ChildrenEditors.Count == 0 || (isRefEdited && editor is CollectionEditor))
result = CreateDiffNode(editor);
bool isScriptEditorWithRefValue = editor is ScriptsEditor && editor.Values.HasReferenceValue;
bool isActorEditorInLevel = editor is ActorEditor && editor.Values[0] is Actor actor && actor.IsPrefabRoot && actor.HasScene;
for (int i = 0; i < editor.ChildrenEditors.Count; i++)
{
var childEditor = editor.ChildrenEditors[i];
// Special case for root actor transformation (can be applied only in Prefab editor, not in Level)
if (isActorEditorInLevel && childEditor.Values.Info.Name is "LocalPosition" or "LocalOrientation" or "LocalScale" or "Name")
continue;
var child = ProcessDiff(childEditor, !isScriptEditorWithRefValue);
if (child != null)
{
if (result == null)
result = CreateDiffNode(editor);
result.AddChild(child);
}
}
// Show scripts removed from prefab instance (user may want to restore them)
if (editor is ScriptsEditor && editor.Values.HasReferenceValue && editor.Values.ReferenceValue is Script[] prefabObjectScripts)
{
for (int j = 0; j < prefabObjectScripts.Length; j++)
{
var prefabObjectScript = prefabObjectScripts[j];
bool isRemoved = true;
for (int i = 0; i < editor.ChildrenEditors.Count; i++)
{
if (editor.ChildrenEditors[i].Values is ScriptsEditor.ScriptsContainer container && container.PrefabObjectId == prefabObjectScript.PrefabObjectID)
{
// Found
isRemoved = false;
break;
}
}
if (isRemoved)
{
var dummy = new RemovedScriptDummy
{
PrefabObject = prefabObjectScript
};
var child = CreateDiffNode(dummy);
if (result == null)
result = CreateDiffNode(editor);
result.AddChild(child);
}
}
}
// Compare child actors for removed actors.
if (editor is ActorEditor && editor.Values.HasReferenceValue && editor.Values.ReferenceValue is Actor prefabObjectActor)
{
var thisActor = editor.Values[0] as Actor;
for (int i = 0; i < prefabObjectActor.ChildrenCount; i++)
{
var prefabActorChild = prefabObjectActor.Children[i];
if (thisActor == null)
continue;
bool isRemoved = true;
for (int j = 0; j < thisActor.ChildrenCount; j++)
{
var actorChild = thisActor.Children[j];
if (actorChild.PrefabObjectID == prefabActorChild.PrefabObjectID)
{
isRemoved = false;
break;
}
}
if (isRemoved)
{
var dummy = new RemovedActorDummy
{
PrefabObject = prefabActorChild,
ParentActor = thisActor,
OrderInParent = prefabActorChild.OrderInParent,
};
var child = CreateDiffNode(dummy);
if (result == null)
result = CreateDiffNode(editor);
result.AddChild(child);
}
}
}
if (editor is ScriptsEditor && result != null && result.ChildrenCount == 0)
return null;
return result;
}
private TreeNode CreateDiffTree(Actor actor, CustomEditorPresenter presenter, LayoutElementsContainer layout)
{
var actorNode = Editor.Instance.Scene.GetActorNode(actor);
ValueContainer vc = new ValueContainer(ScriptMemberInfo.Null);
vc.SetType(new ScriptType(actorNode.EditableObject.GetType()));
vc.Add(actorNode.EditableObject);
var editor = CustomEditorsUtil.CreateEditor(vc, null, false);
editor.Initialize(presenter, layout, vc);
var node = ProcessDiff(editor, false);
layout.ClearLayout();
foreach (var child in actor.Children)
{
var childNode = CreateDiffTree(child, presenter, layout);
if (childNode == null)
continue;
if (node == null)
node = CreateDiffNode(editor);
node.AddChild(childNode);
}
return node;
}
private void ViewChanges(Control target, Float2 targetLocation)
{
// Build a tree out of modified properties
var thisActor = (Actor)Values[0];
var rootActor = thisActor.IsPrefabRoot ? thisActor : thisActor.GetPrefabRoot();
var presenter = new CustomEditorPresenter(null);
var layout = new CustomElementsContainer<ContainerControl>();
var rootNode = CreateDiffTree(rootActor, presenter, layout);
// Skip if no changes detected
if (rootNode == null)
{
var cm1 = new ContextMenu();
cm1.AddButton("No changes detected");
cm1.Show(target, targetLocation);
return;
}
// Create context menu
var cm = new PrefabDiffContextMenu();
cm.Tree.AddChild(rootNode);
cm.Tree.RightClick += OnDiffNodeRightClick;
cm.Tree.Tag = cm;
cm.RevertAll += OnDiffRevertAll;
cm.ApplyAll += OnDiffApplyAll;
cm.Show(target, targetLocation);
}
private void OnDiffNodeRightClick(TreeNode node, Float2 location)
{
var diffMenu = (PrefabDiffContextMenu)node.ParentTree.Tag;
var menu = new ContextMenu();
menu.AddButton("Revert", () => OnDiffRevert((CustomEditor)node.Tag));
menu.AddSeparator();
menu.AddButton("Revert All", OnDiffRevertAll);
menu.AddButton("Apply All", OnDiffApplyAll);
diffMenu.ShowChild(menu, node.PointToParent(diffMenu, new Float2(location.X, node.HeaderHeight)));
}
private void OnDiffRevertAll()
{
RevertToReferenceValue();
}
private void OnDiffApplyAll()
{
Editor.Instance.Prefabs.ApplyAll((Actor)Values[0]);
// Ensure to refresh the layout
Presenter.BuildLayoutOnUpdate();
}
private static void GetAllPrefabObjects(List<object> objects, Actor actor)
{
objects.Add(actor);
objects.AddRange(actor.Scripts);
var children = actor.Children;
foreach (var child in children)
GetAllPrefabObjects(objects, child);
}
private void OnDiffRevert(CustomEditor editor)
{
// Special case for removed Script from actor
if (editor is RemovedScriptDummy removed)
{
Editor.Log("Reverting removed script changes to prefab (adding it)");
var actor = (Actor)Values[0];
var restored = actor.AddScript(removed.PrefabObject.GetType());
var prefabId = actor.PrefabID;
var prefabObjectId = restored.PrefabObjectID;
Script.Internal_LinkPrefab(FlaxEngine.Object.GetUnmanagedPtr(restored), prefabId, prefabObjectId);
string data = JsonSerializer.Serialize(removed.PrefabObject);
JsonSerializer.Deserialize(restored, data);
var action = AddRemoveScript.Added(restored);
Presenter.Undo?.AddAction(action);
return;
}
// Special case for reverting removed Actors
if (editor is RemovedActorDummy removedActor)
{
Editor.Log("Reverting removed actor changes to prefab (adding it)");
var parentActor = removedActor.ParentActor;
var restored = parentActor.AddChild(removedActor.PrefabObject.GetType());
var prefabId = parentActor.PrefabID;
var prefabObjectId = removedActor.PrefabObject.PrefabObjectID;
string data = JsonSerializer.Serialize(removedActor.PrefabObject);
JsonSerializer.Deserialize(restored, data);
Presenter.Owner.SceneContext.Spawn(restored, parentActor, removedActor.OrderInParent);
Actor.Internal_LinkPrefab(FlaxEngine.Object.GetUnmanagedPtr(restored), prefabId, prefabObjectId);
return;
}
// Special case for new Script added to actor
if (editor.Values[0] is Script script && !script.HasPrefabLink)
{
Editor.Log("Reverting added script changes to prefab (removing it)");
var action = AddRemoveScript.Remove(script);
action.Do();
Presenter.Undo?.AddAction(action);
return;
}
// Special case for new Actor added to actor
if (editor.Values[0] is Actor a && !a.HasPrefabLink)
{
Editor.Log("Reverting added actor changes to prefab (removing it)");
// TODO: Keep previous selection.
var context = Presenter.Owner.SceneContext;
context.Select(SceneGraph.SceneGraphFactory.FindNode(a.ID));
context.DeleteSelection();
return;
}
if (Presenter.Undo != null && Presenter.Undo.Enabled)
{
var thisActor = (Actor)Values[0];
var rootActor = thisActor.IsPrefabRoot ? thisActor : thisActor.GetPrefabRoot();
var prefabObjects = new List<object>();
GetAllPrefabObjects(prefabObjects, rootActor);
using (new UndoMultiBlock(Presenter.Undo, prefabObjects, "Revert to Prefab"))
{
editor.RevertToReferenceValue();
editor.RefreshInternal();
}
}
else
{
editor.RevertToReferenceValue();
editor.RefreshInternal();
}
}
}
}