// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Collections.Generic;
using System.Linq;
using FlaxEditor.Content;
using FlaxEditor.GUI;
using FlaxEditor.GUI.Drag;
using FlaxEditor.GUI.Tree;
using FlaxEditor.Scripting;
using FlaxEditor.Utilities;
using FlaxEditor.Windows;
using FlaxEditor.Windows.Assets;
using FlaxEngine;
using FlaxEngine.GUI;
using Object = FlaxEngine.Object;
namespace FlaxEditor.SceneGraph.GUI
{
///
/// Tree node GUI control used as a proxy object for actors hierarchy.
///
///
public class ActorTreeNode : TreeNode
{
private int _orderInParent;
private DragActors _dragActors;
private DragScripts _dragScripts;
private DragAssets _dragAssets;
private DragActorType _dragActorType;
private DragHandlers _dragHandlers;
private List _highlights;
private bool _hasSearchFilter;
///
/// The actor node that owns this node.
///
protected ActorNode _actorNode;
///
/// Gets the actor.
///
public Actor Actor => _actorNode.Actor;
///
/// Gets the actor node.
///
public ActorNode ActorNode => _actorNode;
///
/// Initializes a new instance of the class.
///
public ActorTreeNode()
: base(true)
{
ChildrenIndent = 16.0f;
}
internal virtual void LinkNode(ActorNode node)
{
_actorNode = node;
var actor = node.Actor;
if (actor != null)
{
_orderInParent = actor.OrderInParent;
Visible = (actor.HideFlags & HideFlags.HideInHierarchy) == 0;
var id = actor.ID;
if (Editor.Instance.ProjectCache.IsExpandedActor(ref id))
{
Expand(true);
}
}
else
{
_orderInParent = 0;
}
UpdateText();
}
internal void OnOrderInParentChanged()
{
if (Parent is ActorTreeNode parent)
{
for (int i = 0; i < parent.ChildrenCount; i++)
{
if (parent.Children[i] is ActorTreeNode child && child.Actor)
child._orderInParent = child.Actor.OrderInParent;
}
parent.SortChildren();
}
else if (Actor)
{
_orderInParent = Actor.OrderInParent;
}
}
internal void OnNameChanged()
{
UpdateText();
}
///
/// Updates the tree node text.
///
public virtual void UpdateText()
{
Text = _actorNode.Name;
}
///
/// Updates the query search filter.
///
/// The filter text.
public void UpdateFilter(string filterText)
{
// SKip hidden actors
var actor = Actor;
if (actor != null && (actor.HideFlags & HideFlags.HideInHierarchy) != 0)
return;
bool noFilter = string.IsNullOrWhiteSpace(filterText);
_hasSearchFilter = !noFilter;
// Update itself
bool isThisVisible;
if (noFilter)
{
// Clear filter
_highlights?.Clear();
isThisVisible = true;
}
else
{
var text = Text;
if (QueryFilterHelper.Match(filterText, text, out QueryFilterHelper.Range[] ranges))
{
// Update highlights
if (_highlights == null)
_highlights = new List(ranges.Length);
else
_highlights.Clear();
var font = Style.Current.FontSmall;
var textRect = TextRect;
for (int i = 0; i < ranges.Length; i++)
{
var start = font.GetCharPosition(text, ranges[i].StartIndex);
var end = font.GetCharPosition(text, ranges[i].EndIndex);
_highlights.Add(new Rectangle(start.X + textRect.X, textRect.Y, end.X - start.X, textRect.Height));
}
isThisVisible = true;
}
else
{
// Hide
_highlights?.Clear();
isThisVisible = false;
}
}
// Update children
bool isAnyChildVisible = false;
for (int i = 0; i < _children.Count; i++)
{
if (_children[i] is ActorTreeNode child)
{
child.UpdateFilter(filterText);
isAnyChildVisible |= child.Visible;
}
}
bool isExpanded = isAnyChildVisible;
// Restore cached state on query filter clear
if (noFilter && actor != null)
{
var id = actor.ID;
isExpanded = Editor.Instance.ProjectCache.IsExpandedActor(ref id);
}
if (isExpanded)
{
Expand(true);
}
else
{
Collapse(true);
}
Visible = isThisVisible | isAnyChildVisible;
}
///
public override void Update(float deltaTime)
{
// Update hidden state
var actor = Actor;
if (actor && !_hasSearchFilter)
{
Visible = (actor.HideFlags & HideFlags.HideInHierarchy) == 0;
}
base.Update(deltaTime);
}
///
protected override bool ShowTooltip => true;
///
public override bool OnShowTooltip(out string text, out Float2 location, out Rectangle area)
{
// Evaluate tooltip text once it's actually needed
var actor = _actorNode.Actor;
if (string.IsNullOrEmpty(TooltipText) && actor)
TooltipText = Surface.SurfaceUtils.GetVisualScriptTypeDescription(TypeUtils.GetObjectType(actor));
return base.OnShowTooltip(out text, out location, out area);
}
///
protected override Color CacheTextColor()
{
// Update node text color (based on ActorNode.IsActiveInHierarchy but with optimized logic a little)
if (Parent is ActorTreeNode)
{
Color color = Style.Current.Foreground;
var actor = Actor;
if (actor)
{
if (actor.HasPrefabLink)
{
// Prefab
color = Style.Current.ProgressNormal;
}
if (!actor.IsActiveInHierarchy)
{
// Inactive
return Style.Current.ForegroundGrey;
}
if (actor.Scene != null && Editor.Instance.StateMachine.IsPlayMode && actor.IsStatic)
{
// Static
return color * 0.85f;
}
}
// Default
return color;
}
return base.CacheTextColor();
}
///
public override int Compare(Control other)
{
if (other is ActorTreeNode node)
{
return _orderInParent - node._orderInParent;
}
return base.Compare(other);
}
///
/// Starts the actor renaming action.
///
public void StartRenaming(EditorWindow window)
{
// Block renaming during scripts reload
if (Editor.Instance.ProgressReporting.CompileScripts.IsActive)
return;
Select();
// Disable scrolling of view
if (window is SceneTreeWindow)
(window as SceneTreeWindow).ScrollingOnSceneTreeView(false);
else if (window is PrefabWindow)
(window as PrefabWindow).ScrollingOnTreeView(false);
// Start renaming the actor
var dialog = RenamePopup.Show(this, TextRect, _actorNode.Name, false);
dialog.Renamed += OnRenamed;
dialog.Closed += popup =>
{
// Enable scrolling of view
if (window is SceneTreeWindow)
(window as SceneTreeWindow).ScrollingOnSceneTreeView(true);
else if (window is PrefabWindow)
(window as PrefabWindow).ScrollingOnTreeView(true);
};
}
private void OnRenamed(RenamePopup renamePopup)
{
using (new UndoBlock(ActorNode.Root.Undo, Actor, "Rename"))
Actor.Name = renamePopup.Text;
}
///
protected override void OnExpandedChanged()
{
base.OnExpandedChanged();
if (!IsLayoutLocked && Actor)
{
var id = Actor.ID;
Editor.Instance.ProjectCache.SetExpandedActor(ref id, IsExpanded);
}
}
///
public override void Draw()
{
base.Draw();
// Draw all highlights
if (_highlights != null)
{
var style = Style.Current;
var color = style.ProgressNormal * 0.6f;
for (int i = 0; i < _highlights.Count; i++)
Render2D.FillRectangle(_highlights[i], color);
}
}
///
protected override DragDropEffect OnDragEnterHeader(DragData data)
{
// Check if cannot edit scene or there is no scene loaded (handle case for actors in prefab editor)
if (_actorNode?.ParentScene != null)
{
if (!Editor.Instance.StateMachine.CurrentState.CanEditScene || !Level.IsAnySceneLoaded)
return DragDropEffect.None;
}
else
{
if (!Editor.Instance.StateMachine.CurrentState.CanEditContent)
return DragDropEffect.None;
}
if (_dragHandlers == null)
_dragHandlers = new DragHandlers();
// Check if drop actors
if (_dragActors == null)
{
_dragActors = new DragActors(ValidateDragActor);
_dragHandlers.Add(_dragActors);
}
if (_dragActors.OnDragEnter(data))
return _dragActors.Effect;
// Check if drop scripts
if (_dragScripts == null)
{
_dragScripts = new DragScripts(ValidateDragScript);
_dragHandlers.Add(_dragScripts);
}
if (_dragScripts.OnDragEnter(data))
return _dragScripts.Effect;
// Check if drag assets
if (_dragAssets == null)
{
_dragAssets = new DragAssets(ValidateDragAsset);
_dragHandlers.Add(_dragAssets);
}
if (_dragAssets.OnDragEnter(data))
return _dragAssets.Effect;
// Check if drag actor type
if (_dragActorType == null)
{
_dragActorType = new DragActorType(ValidateDragActorType);
_dragHandlers.Add(_dragActorType);
}
if (_dragActorType.OnDragEnter(data))
return _dragActorType.Effect;
return DragDropEffect.None;
}
///
protected override DragDropEffect OnDragMoveHeader(DragData data)
{
return _dragHandlers.Effect;
}
///
protected override void OnDragLeaveHeader()
{
_dragHandlers.OnDragLeave();
}
[Serializable]
private class ReparentAction : IUndoAction
{
[Serialize]
private Guid[] _ids;
[Serialize]
private int _actorsCount;
[Serialize]
private Guid[] _prefabIds;
[Serialize]
private Guid[] _prefabObjectIds;
public ReparentAction(Actor actor)
: this(new List { actor })
{
}
public ReparentAction(List actors)
{
var allActors = new List(Mathf.NextPowerOfTwo(actors.Count));
for (int i = 0; i < actors.Count; i++)
{
GetAllActors(allActors, actors[i]);
}
var allScripts = new List