// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved. using System; using System.Collections.Generic; using System.Linq; 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; using FlaxEditor.Windows.Assets; using FlaxEngine; using FlaxEngine.GUI; using FlaxEngine.Json; using FlaxEngine.Utilities; namespace FlaxEditor.CustomEditors.Dedicated { /// /// Dedicated custom editor for objects. /// /// [CustomEditor(typeof(Actor)), DefaultEditor] public class ActorEditor : ScriptingObjectEditor { private Guid _linkedPrefabId; /// protected override List 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; } /// 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(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); if (Presenter == Editor.Instance.Windows.PropertiesWin.Presenter) { // Add some UI var panel = layout.CustomContainer(); panel.CustomControl.Height = 20.0f; panel.CustomControl.SlotsVertically = 1; 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(prefab); }; // Edit selected prefab asset var editPrefab = panel.Button("Edit Prefab"); editPrefab.Button.Clicked += () => { Editor.Instance.Windows.ContentWin.ClearItemsSearch(); Editor.Instance.Windows.ContentWin.Select(prefab); Editor.Instance.Windows.ContentWin.Open(Editor.Instance.Windows.ContentWin.View.Selection[0]); }; // 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 is PropertiesWindow propertiesWindow) { var lockButton = cm.AddButton(propertiesWindow.LockObjects ? "Unlock" : "Lock"); lockButton.ButtonClicked += button => { propertiesWindow.LockObjects = !propertiesWindow.LockObjects; // Reselect current selection if (!propertiesWindow.LockObjects && Editor.Instance.SceneEditing.SelectionCount > 0) { var cachedSelection = Editor.Instance.SceneEditing.Selection.ToArray(); Editor.Instance.SceneEditing.Select(null); Editor.Instance.SceneEditing.Select(cachedSelection); } }; } else if (Presenter.Owner is PrefabWindow prefabWindow) { var lockButton = cm.AddButton(prefabWindow.LockSelectedObjects ? "Unlock" : "Lock"); lockButton.ButtonClicked += button => { prefabWindow.LockSelectedObjects = !prefabWindow.LockSelectedObjects; // Reselect current selection if (!prefabWindow.LockSelectedObjects && prefabWindow.Selection.Count > 0) { var cachedSelection = prefabWindow.Selection.ToList(); prefabWindow.Select(null); prefabWindow.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); } /// protected override void Deinitialize() { base.Deinitialize(); if (_linkedPrefabId != Guid.Empty) { _linkedPrefabId = Guid.Empty; Editor.Instance.Prefabs.PrefabApplied -= 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); } // 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; 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) { 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 { /// /// The removed prefab object (from the prefab default instance). /// public Script PrefabObject; /// public override void Initialize(LayoutElementsContainer layout) { // Not used } } private TreeNode ProcessDiff(CustomEditor editor, bool skipIfNotModified = true) { // Special case for new Script added to actor if (editor.Values[0] is Script script && !script.HasPrefabLink) return CreateDiffNode(editor); // Skip if no change detected var isRefEdited = editor.Values.IsReferenceValueModified; if (!isRefEdited && skipIfNotModified) 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") 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); } } } return result; } private void ViewChanges(Control target, Float2 targetLocation) { // Build a tree out of modified properties var rootNode = ProcessDiff(this, false); // Skip if no changes detected if (rootNode == null || rootNode.ChildrenCount == 0) { 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 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), ref prefabId, ref prefabObjectId); string data = JsonSerializer.Serialize(removed.PrefabObject); JsonSerializer.Deserialize(restored, data); var action = AddRemoveScript.Added(restored); Presenter.Undo?.AddAction(action); 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; } editor.RevertToReferenceValue(); } } }