// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System; using System.IO; using System.Linq; using FlaxEditor.CustomEditors; using FlaxEditor.GUI.ContextMenu; using FlaxEditor.GUI.Timeline.Undo; using FlaxEditor.SceneGraph; using FlaxEditor.Utilities; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.GUI.Timeline.Tracks { /// /// The timeline track for animating objects. /// /// public class ActorTrack : ObjectTrack { /// /// Gets the archetype. /// /// The archetype. public static TrackArchetype GetArchetype() { return new TrackArchetype { TypeId = 7, Name = "Actor", Create = options => new ActorTrack(ref options), Load = LoadTrack, Save = SaveTrack, }; } private static void LoadTrack(int version, Track track, BinaryReader stream) { var e = (ActorTrack)track; e.ActorID = stream.ReadGuid(); } private static void SaveTrack(Track track, BinaryWriter stream) { var e = (ActorTrack)track; stream.WriteGuid(ref e.ActorID); } /// /// The select actor icon. /// protected Image _selectActor; /// /// The object ID. /// public Guid ActorID; /// /// Gets the object instance (it might be missing). /// public Actor Actor { get { if (Flags.HasFlag(TrackFlags.PrefabObject)) { // TODO: reuse cached actor to improve perf foreach (var window in Editor.Instance.Windows.Windows) { if (window is Windows.Assets.PrefabWindow prefabWindow && prefabWindow.Graph.MainActor) { var actor = FindActorWithPrefabObjectID(prefabWindow.Graph.MainActor, ref ActorID); if (actor != null) return actor; } } return null; } return FlaxEngine.Object.TryFind(ref ActorID); } set { if (value != null) { if (value.HasPrefabLink && value.Scene == null) { // Track with prefab object reference assigned in Editor ActorID = value.PrefabObjectID; Flags |= TrackFlags.PrefabObject; } else { ActorID = value.ID; } } else { ActorID = Guid.Empty; } } } private static Actor FindActorWithPrefabObjectID(Actor a, ref Guid id) { if (a.PrefabObjectID == id) return a; for (int i = 0; i < a.ChildrenCount; i++) { var e = FindActorWithPrefabObjectID(a.GetChild(i), ref id); if (e != null) return e; } return null; } /// /// Initializes a new instance of the class. /// /// The track initial options. /// True if show sub-tracks keyframes as a proxy on this track, otherwise false. public ActorTrack(ref TrackCreateOptions options, bool useProxyKeyframes = true) : base(ref options, useProxyKeyframes) { // Select Actor button const float buttonSize = 18; var icons = Editor.Instance.Icons; _selectActor = new Image { TooltipText = "Selects the actor animated by this track", AutoFocus = true, AnchorPreset = AnchorPresets.MiddleRight, IsScrollable = false, Color = Style.Current.ForegroundGrey, Margin = new Margin(1), Brush = new SpriteBrush(icons.Search32), Offsets = new Margin(-buttonSize - 2 + _addButton.Offsets.Left, buttonSize, buttonSize * -0.5f, buttonSize), Parent = this, }; _selectActor.Clicked += OnClickedSelectActor; } /// public override object Object => Actor; /// protected override void OnShowAddContextMenu(ContextMenu.ContextMenu menu) { base.OnShowAddContextMenu(menu); OnSelectActorContextMenu(menu); var actor = Actor; if (actor == null) return; var type = actor.GetType(); menu.AddSeparator(); // Properties and events if (AddProperties(this, menu, type) != 0) menu.AddSeparator(); if (AddEvents(this, menu, type) != 0) menu.AddSeparator(); // Child scripts var scripts = actor.Scripts; for (int i = 0; i < scripts.Length; i++) { var script = scripts[i]; // Skip invalid or hidden scripts if (script == null || script.GetType().GetCustomAttributes(true).Any(x => x is HideInEditorAttribute)) continue; // Prevent from adding the same track twice if (SubTracks.Any(x => x is IObjectTrack y && y.Object as SceneObject == script)) continue; var name = Utilities.Utils.GetPropertyNameUI(script.GetType().Name); menu.AddButton(name, OnAddScriptTrack).Tag = script; } } /// protected override void OnContextMenu(ContextMenu.ContextMenu menu) { base.OnContextMenu(menu); menu.AddSeparator(); OnSelectActorContextMenu(menu); } private void OnSelectActorContextMenu(ContextMenu.ContextMenu menu) { var actor = Actor; var selection = Editor.Instance.SceneEditing.Selection; foreach (var node in selection) { if (node is ActorNode actorNode && IsActorValid(actorNode.Actor) && actorNode.Actor != actor) { var b = menu.AddButton("Select " + actorNode.Actor, OnClickedSelectActor); b.Tag = actorNode.Actor; b.TooltipText = Utilities.Utils.GetTooltip(actorNode.Actor); } } menu.AddButton("Select...", OnClickedSelect).TooltipText = "Opens actor picker dialog to select the target actor for this track"; } /// /// Adds the script track to this actor track. /// /// The script object. public void AddScriptTrack(Script script) { var track = (ScriptTrack)Timeline.NewTrack(ScriptTrack.GetArchetype()); track.ParentTrack = this; track.TrackIndex = TrackIndex + 1; track.Script = script; track.Rename(script.GetType().Name); Timeline.AddTrack(track); Expand(); } /// /// Determines whether actor is valid for this track. /// /// The actor. /// True if it's valid, otherwise false. protected virtual bool IsActorValid(Actor actor) { return actor; } /// /// Called when actor gets changed. /// protected virtual void OnActorChanged() { } private void OnAddScriptTrack(ContextMenuButton button) { var script = (Script)button.Tag; AddScriptTrack(script); } private void OnClickedSelectActor(ContextMenuButton b) { SetActor((Actor)b.Tag); } private void SetActor(Actor actor) { if (Actor == actor || !IsActorValid(actor)) return; var oldName = Name; Rename(actor.Name); using (new TrackUndoBlock(this, new RenameTrackAction(Timeline, this, oldName, Name))) Actor = actor; OnActorChanged(); } private void OnClickedSelect() { ActorSearchPopup.Show(this, PointFromScreen(FlaxEngine.Input.MouseScreenPosition), IsActorValid, SetActor); } private void OnClickedSelectActor(Image image, MouseButton button) { if (button == MouseButton.Left) { var actor = Actor; if (actor) { Editor.Instance.SceneEditing.Select(actor); } } } /// public override void OnDestroy() { _selectActor = null; base.OnDestroy(); } } }