Files
FlaxEngine/Source/Editor/CustomEditors/Dedicated/ScriptsEditor.cs
Ari Vuollet 58445f04c4 Fix potential incorrect null checks in FlaxEngine.Objects
The null-conditional operator checks for reference equality of the
Object, but doesn't check the validity of the unmanaged pointer. This
check is corrected in cases where the object was not immediately
returned from the bindings layer and may have been destroyed earlier.
2023-09-28 22:05:58 +03:00

816 lines
27 KiB
C#

// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using FlaxEditor.Actions;
using FlaxEditor.Content;
using FlaxEditor.GUI;
using FlaxEditor.GUI.ContextMenu;
using FlaxEditor.GUI.Drag;
using FlaxEditor.Scripting;
using FlaxEngine;
using FlaxEngine.GUI;
using FlaxEngine.Utilities;
using Object = FlaxEngine.Object;
namespace FlaxEditor.CustomEditors.Dedicated
{
/// <summary>
/// Drag and drop scripts area control.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.Control" />
public class DragAreaControl : ContainerControl
{
private DragHandlers _dragHandlers;
private DragScriptItems _dragScripts;
private DragAssets _dragAssets;
/// <summary>
/// The parent scripts editor.
/// </summary>
public ScriptsEditor ScriptsEditor;
/// <summary>
/// Initializes a new instance of the <see cref="DragAreaControl"/> class.
/// </summary>
public DragAreaControl()
: base(0, 0, 120, 40)
{
AutoFocus = false;
// Add script button
float addScriptButtonWidth = 60.0f;
var addScriptButton = new Button
{
TooltipText = "Add new scripts to the actor",
AnchorPreset = AnchorPresets.MiddleCenter,
Text = "Add script",
Parent = this,
Bounds = new Rectangle((Width - addScriptButtonWidth) / 2, 1, addScriptButtonWidth, 18),
};
addScriptButton.ButtonClicked += OnAddScriptButtonClicked;
}
private void OnAddScriptButtonClicked(Button button)
{
var scripts = Editor.Instance.CodeEditing.Scripts.Get();
if (scripts.Count == 0)
{
// No scripts
var cm1 = new ContextMenu();
cm1.AddButton("No scripts in project");
cm1.Show(this, button.BottomLeft);
return;
}
// Show context menu with list of scripts to add
var cm = new ItemsListContextMenu(180);
for (int i = 0; i < scripts.Count; i++)
{
cm.AddItem(new TypeSearchPopup.TypeItemView(scripts[i]));
}
cm.ItemClicked += item => AddScript((ScriptType)item.Tag);
cm.SortItems();
cm.Show(this, button.BottomLeft - new Float2((cm.Width - button.Width) / 2, 0));
}
/// <inheritdoc />
public override void Draw()
{
var style = Style.Current;
var size = Size;
// Info
Render2D.DrawText(style.FontSmall, "Drag scripts here", new Rectangle(2, 22, size.X - 4, size.Y - 4 - 20), style.ForegroundDisabled, TextAlignment.Center, TextAlignment.Center);
// Check if drag is over
if (IsDragOver && _dragHandlers != null && _dragHandlers.HasValidDrag)
{
var area = new Rectangle(Float2.Zero, size);
Render2D.FillRectangle(area, Color.Orange * 0.5f);
Render2D.DrawRectangle(area, Color.Black);
}
base.Draw();
}
private bool ValidateScript(ScriptItem scriptItem)
{
var scriptName = scriptItem.ScriptName;
var scriptType = ScriptsBuilder.FindScript(scriptName);
return scriptType != null;
}
private bool ValidateAsset(AssetItem assetItem)
{
if (assetItem is VisualScriptItem scriptItem)
return scriptItem.ScriptType != ScriptType.Null;
return false;
}
/// <inheritdoc />
public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
{
var result = base.OnDragEnter(ref location, data);
if (result != DragDropEffect.None)
return result;
if (_dragHandlers == null)
{
_dragScripts = new DragScriptItems(ValidateScript);
_dragAssets = new DragAssets(ValidateAsset);
_dragHandlers = new DragHandlers
{
_dragScripts,
_dragAssets,
};
}
return _dragHandlers.OnDragEnter(data);
}
/// <inheritdoc />
public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
{
var result = base.OnDragMove(ref location, data);
if (result != DragDropEffect.None)
return result;
return _dragHandlers.Effect;
}
/// <inheritdoc />
public override void OnDragLeave()
{
_dragHandlers.OnDragLeave();
base.OnDragLeave();
}
/// <inheritdoc />
public override DragDropEffect OnDragDrop(ref Float2 location, DragData data)
{
var result = base.OnDragDrop(ref location, data);
if (result != DragDropEffect.None)
return result;
if (_dragHandlers.HasValidDrag)
{
if (_dragScripts.HasValidDrag)
{
result = _dragScripts.Effect;
AddScripts(_dragScripts.Objects);
}
else if (_dragAssets.HasValidDrag)
{
result = _dragAssets.Effect;
AddScripts(_dragAssets.Objects);
}
_dragHandlers.OnDragDrop(null);
}
return result;
}
private void AddScript(ScriptType item)
{
var list = new List<ScriptType>(1) { item };
AddScripts(list);
}
private void AddScripts(List<AssetItem> items)
{
var list = new List<ScriptType>(items.Count);
for (int i = 0; i < items.Count; i++)
{
var item = (VisualScriptItem)items[i];
var scriptType = item.ScriptType;
if (scriptType == ScriptType.Null)
{
Editor.LogWarning("Invalid script type " + item.ShortName);
}
else
{
list.Add(scriptType);
}
}
AddScripts(list);
}
private void AddScripts(List<ScriptItem> items)
{
var list = new List<ScriptType>(items.Count);
for (int i = 0; i < items.Count; i++)
{
var item = items[i];
var scriptName = item.ScriptName;
var scriptType = ScriptsBuilder.FindScript(scriptName);
if (scriptType == null)
{
Editor.LogWarning("Invalid script type " + scriptName);
}
else
{
list.Add(new ScriptType(scriptType));
}
}
AddScripts(list);
}
private void AddScripts(List<ScriptType> items)
{
var actions = new List<IUndoAction>(4);
for (int i = 0; i < items.Count; i++)
{
var scriptType = items[i];
var actors = ScriptsEditor.ParentEditor.Values;
for (int j = 0; j < actors.Count; j++)
{
var actor = (Actor)actors[j];
actions.Add(AddRemoveScript.Add(actor, scriptType));
}
}
if (actions.Count == 0)
{
Editor.LogWarning("Failed to spawn scripts");
return;
}
var multiAction = new MultiUndoAction(actions);
multiAction.Do();
var presenter = ScriptsEditor.Presenter;
if (presenter != null)
{
presenter.Undo.AddAction(multiAction);
presenter.Control.Focus();
}
}
}
/// <summary>
/// Small image control added per script group that allows to drag and drop a reference to it. Also used to reorder the scripts.
/// </summary>
/// <seealso cref="FlaxEngine.GUI.Image" />
internal class ScriptDragIcon : Image
{
private ScriptsEditor _editor;
private bool _isMouseDown;
private Float2 _mouseDownPos;
/// <summary>
/// Gets the target script.
/// </summary>
public Script Script => (Script)Tag;
/// <summary>
/// Initializes a new instance of the <see cref="ScriptDragIcon"/> class.
/// </summary>
/// <param name="editor">The script editor.</param>
/// <param name="script">The target script.</param>
public ScriptDragIcon(ScriptsEditor editor, Script script)
{
Tag = script;
_editor = editor;
}
/// <inheritdoc />
public override void OnMouseEnter(Float2 location)
{
_mouseDownPos = Float2.Minimum;
base.OnMouseEnter(location);
}
/// <inheritdoc />
public override void OnMouseLeave()
{
// Check if start drag drop
if (_isMouseDown)
{
DoDrag();
_isMouseDown = false;
}
base.OnMouseLeave();
}
/// <inheritdoc />
public override void OnMouseMove(Float2 location)
{
// Check if start drag drop
if (_isMouseDown && Float2.Distance(location, _mouseDownPos) > 10.0f)
{
DoDrag();
_isMouseDown = false;
}
base.OnMouseMove(location);
}
/// <inheritdoc />
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
// Clear flag
_isMouseDown = false;
}
return base.OnMouseUp(location, button);
}
/// <inheritdoc />
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
// Set flag
_isMouseDown = true;
_mouseDownPos = location;
}
return base.OnMouseDown(location, button);
}
private void DoDrag()
{
var script = Script;
_editor.OnScriptDragChange(true, script);
DoDragDrop(DragScripts.GetDragData(script));
_editor.OnScriptDragChange(false, script);
}
}
internal class ScriptArrangeBar : Control
{
private ScriptsEditor _editor;
private int _index;
private Script _script;
private DragDropEffect _dragEffect;
public ScriptArrangeBar()
: base(0, 0, 120, 6)
{
AutoFocus = false;
Visible = false;
}
public void Init(int index, ScriptsEditor editor)
{
_editor = editor;
_index = index;
_editor.ScriptDragChange += OnScriptDragChange;
}
private void OnScriptDragChange(bool start, Script script)
{
_script = start ? script : null;
Visible = start;
OnDragLeave();
}
/// <inheritdoc />
public override void Draw()
{
base.Draw();
var color = FlaxEngine.GUI.Style.Current.BackgroundSelected * (IsDragOver ? 0.9f : 0.1f);
Render2D.FillRectangle(new Rectangle(Float2.Zero, Size), color);
}
/// <inheritdoc />
public override DragDropEffect OnDragEnter(ref Float2 location, DragData data)
{
_dragEffect = DragDropEffect.None;
var result = base.OnDragEnter(ref location, data);
if (result != DragDropEffect.None)
return result;
if (data is DragDataText textData && DragScripts.IsValidData(textData))
return _dragEffect = DragDropEffect.Move;
return result;
}
/// <inheritdoc />
public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
{
return _dragEffect;
}
/// <inheritdoc />
public override void OnDragLeave()
{
_dragEffect = DragDropEffect.None;
base.OnDragLeave();
}
/// <inheritdoc />
public override DragDropEffect OnDragDrop(ref Float2 location, DragData data)
{
var result = base.OnDragDrop(ref location, data);
if (result != DragDropEffect.None)
return result;
if (_dragEffect != DragDropEffect.None)
{
result = _dragEffect;
_dragEffect = DragDropEffect.None;
_editor.ReorderScript(_script, _index);
}
return result;
}
}
/// <summary>
/// Custom editor for actor scripts collection.
/// </summary>
/// <seealso cref="CustomEditor" />
public sealed class ScriptsEditor : SyncPointEditor
{
private CheckBox[] _scriptToggles;
/// <summary>
/// Delegate for script drag start and event events.
/// </summary>
/// <param name="start">Set to true if drag started, otherwise false.</param>
/// <param name="script">The target script to reorder.</param>
public delegate void ScriptDragDelegate(bool start, Script script);
/// <summary>
/// Occurs when script drag changes (starts or ends).
/// </summary>
public event ScriptDragDelegate ScriptDragChange;
/// <summary>
/// The scripts collection. Undo operations are recorder for scripts.
/// </summary>
private readonly List<Script> _scripts = new List<Script>();
/// <inheritdoc />
public override IEnumerable<object> UndoObjects => _scripts;
private void AddMissingScript(int index, LayoutElementsContainer layout)
{
var group = layout.Group("Missing script");
// Add settings button to the group
var settingsButton = group.AddSettingsButton();
settingsButton.Tag = index;
settingsButton.Clicked += MissingSettingsButtonOnClicked;
}
private void MissingSettingsButtonOnClicked(Image image, MouseButton mouseButton)
{
if (mouseButton != MouseButton.Left)
return;
var index = (int)image.Tag;
var cm = new ContextMenu
{
Tag = index
};
cm.AddButton("Remove", OnClickMissingRemove);
cm.Show(image, image.Size);
}
private void OnClickMissingRemove(ContextMenuButton button)
{
var index = (int)button.ParentContextMenu.Tag;
// TODO: support undo
var actors = ParentEditor.Values;
for (int i = 0; i < actors.Count; i++)
{
var actor = (Actor)actors[i];
var script = actor.GetScript(index);
if (script)
{
script.Parent = null;
Object.Destroy(script);
}
Editor.Instance.Scene.MarkSceneEdited(actor.Scene);
}
}
/// <summary>
/// Values container for the collection of the scripts. Helps with prefab linkage and reference value usage (uses Prefab Instance ID rather than index in array).
/// </summary>
public sealed class ScriptsContainer : ListValueContainer
{
private readonly Guid _prefabObjectId;
/// <summary>
/// Gets the prefab object identifier used by the container scripts. Empty if there is no valid linkage to the prefab object.
/// </summary>
public Guid PrefabObjectId => _prefabObjectId;
/// <summary>
/// Initializes a new instance of the <see cref="ScriptsContainer"/> class.
/// </summary>
/// <param name="elementType">Type of the collection elements (script type).</param>
/// <param name="index">The script index in the actor scripts collection.</param>
/// <param name="values">The collection values (scripts array).</param>
public ScriptsContainer(ScriptType elementType, int index, ValueContainer values)
: base(elementType, index)
{
Capacity = values.Count;
for (int i = 0; i < values.Count; i++)
{
var v = (IList)values[i];
Add(v[index]);
}
if (values.HasReferenceValue && Count > 0 && this[0] is Script script && script.HasPrefabLink)
{
_prefabObjectId = script.PrefabObjectID;
RefreshReferenceValue(values.ReferenceValue);
}
}
/// <inheritdoc />
public override void RefreshReferenceValue(object instanceValue)
{
// Clear
_referenceValue = null;
_hasReferenceValue = false;
if (instanceValue is IList v)
{
// Get the reference value if script with the given link id exists in the reference values collection
for (int i = 0; i < v.Count; i++)
{
if (v[i] is Script script && script.PrefabObjectID == _prefabObjectId)
{
_referenceValue = script;
_hasReferenceValue = true;
break;
}
}
}
}
}
/// <inheritdoc />
public override void Initialize(LayoutElementsContainer layout)
{
// Area for drag&drop scripts
var dragArea = layout.CustomContainer<DragAreaControl>();
dragArea.CustomControl.ScriptsEditor = this;
// No support for showing scripts from multiple actors that have different set of scripts
var scripts = (Script[])Values[0];
_scripts.Clear();
_scripts.AddRange(scripts);
for (int i = 1; i < Values.Count; i++)
{
var e = (Script[])Values[i];
if (scripts.Length != e.Length)
return;
for (int j = 0; j < e.Length; j++)
{
var t1 = scripts[j] != null ? scripts[j].TypeName : null;
var t2 = e[j] != null ? e[j].TypeName : null;
if (t1 != t2)
return;
}
}
// Scripts arrange bar
var dragBar = layout.Custom<ScriptArrangeBar>();
dragBar.CustomControl.Init(0, this);
// Scripts
var elementType = new ScriptType(typeof(Script));
_scriptToggles = new CheckBox[scripts.Length];
for (int i = 0; i < scripts.Length; i++)
{
var script = scripts[i];
if (script == null)
{
AddMissingScript(i, layout);
continue;
}
var values = new ScriptsContainer(elementType, i, Values);
var scriptType = TypeUtils.GetObjectType(script);
var editor = CustomEditorsUtil.CreateEditor(scriptType, false);
// Create group
var title = Utilities.Utils.GetPropertyNameUI(scriptType.Name);
var group = layout.Group(title, editor);
if ((Presenter.Features & FeatureFlags.CacheExpandedGroups) != 0)
{
if (Editor.Instance.ProjectCache.IsCollapsedGroup(title))
group.Panel.Close(false);
else
group.Panel.Open(false);
group.Panel.IsClosedChanged += panel => Editor.Instance.ProjectCache.SetCollapsedGroup(panel.HeaderText, panel.IsClosed);
}
else
group.Panel.Open(false);
// Customize
group.Panel.TooltipText = Editor.Instance.CodeDocs.GetTooltip(scriptType);
if (script.HasPrefabLink)
group.Panel.HeaderTextColor = FlaxEngine.GUI.Style.Current.ProgressNormal;
// Add toggle button to the group
var headerHeight = group.Panel.HeaderHeight;
var scriptToggle = new CheckBox
{
TooltipText = "If checked, script will be enabled.",
IsScrollable = false,
Checked = script.Enabled,
Parent = group.Panel,
Size = new Float2(headerHeight),
Bounds = new Rectangle(headerHeight, 0, headerHeight, headerHeight),
BoxSize = headerHeight - 4.0f,
Tag = script,
};
scriptToggle.StateChanged += OnScriptToggleCheckChanged;
_scriptToggles[i] = scriptToggle;
// Add drag button to the group
var scriptDrag = new ScriptDragIcon(this, script)
{
TooltipText = "Script reference",
AutoFocus = true,
IsScrollable = false,
Color = FlaxEngine.GUI.Style.Current.ForegroundGrey,
Parent = group.Panel,
Bounds = new Rectangle(scriptToggle.Right, 0.5f, headerHeight, headerHeight),
Margin = new Margin(1),
Brush = new SpriteBrush(Editor.Instance.Icons.DragBar12),
Tag = script,
};
// Add settings button to the group
var settingsButton = group.AddSettingsButton();
settingsButton.Tag = script;
settingsButton.Clicked += OnSettingsButtonClicked;
group.Panel.HeaderTextMargin = new Margin(scriptDrag.Right - 12, 15, 2, 2);
group.Object(values, editor);
// Scripts arrange bar
dragBar = layout.Custom<ScriptArrangeBar>();
dragBar.CustomControl.Init(i + 1, this);
}
base.Initialize(layout);
}
/// <summary>
/// Called when script drag changes.
/// </summary>
/// <param name="start">if set to <c>true</c> drag just started, otherwise ended.</param>
/// <param name="script">The target script.</param>
public void OnScriptDragChange(bool start, Script script)
{
ScriptDragChange.Invoke(start, script);
}
/// <summary>
/// Changes the script order (with undo).
/// </summary>
/// <param name="script">The script to reorder.</param>
/// <param name="targetIndex">The target index to move script.</param>
public void ReorderScript(Script script, int targetIndex)
{
// Skip if no change
if (script.OrderInParent == targetIndex)
return;
var action = ChangeScriptAction.ChangeOrder(script, targetIndex);
action.Do();
Presenter?.Undo.AddAction(action);
}
private void OnScriptToggleCheckChanged(CheckBox box)
{
var script = (Script)box.Tag;
if (script.Enabled == box.Checked)
return;
var action = ChangeScriptAction.ChangeEnabled(script, box.Checked);
action.Do();
Presenter?.Undo.AddAction(action);
}
private void OnSettingsButtonClicked(Image image, MouseButton mouseButton)
{
if (mouseButton != MouseButton.Left)
return;
var script = (Script)image.Tag;
var scriptType = TypeUtils.GetType(script.TypeName);
var item = scriptType.ContentItem;
var cm = new ContextMenu
{
Tag = script
};
cm.AddButton("Remove", OnClickRemove).Icon = Editor.Instance.Icons.Cross12;
cm.AddButton("Move up", OnClickMoveUp).Enabled = script.OrderInParent > 0;
cm.AddButton("Move down", OnClickMoveDown).Enabled = script.OrderInParent < script.Actor.Scripts.Length - 1;
// TODO: copy script
// TODO: paste script values
// TODO: paste script as new
// TODO: copy script reference
cm.AddSeparator();
cm.AddButton("Copy type name", OnClickCopyTypeName);
cm.AddButton("Edit script", OnClickEditScript).Enabled = item != null;
var showButton = cm.AddButton("Show in content window", OnClickShowScript);
showButton.Enabled = item != null;
showButton.Icon = Editor.Instance.Icons.Search12;
cm.Show(image, image.Size);
}
private void OnClickRemove(ContextMenuButton button)
{
var script = (Script)button.ParentContextMenu.Tag;
var action = AddRemoveScript.Remove(script);
action.Do();
Presenter.Undo?.AddAction(action);
}
private void OnClickMoveUp(ContextMenuButton button)
{
var script = (Script)button.ParentContextMenu.Tag;
var action = ChangeScriptAction.ChangeOrder(script, script.OrderInParent - 1);
action.Do();
Presenter.Undo?.AddAction(action);
}
private void OnClickMoveDown(ContextMenuButton button)
{
var script = (Script)button.ParentContextMenu.Tag;
var action = ChangeScriptAction.ChangeOrder(script, script.OrderInParent + 1);
action.Do();
Presenter.Undo?.AddAction(action);
}
private void OnClickCopyTypeName(ContextMenuButton button)
{
var script = (Script)button.ParentContextMenu.Tag;
Clipboard.Text = script.TypeName;
}
private void OnClickEditScript(ContextMenuButton button)
{
var script = (Script)button.ParentContextMenu.Tag;
var scriptType = TypeUtils.GetType(script.TypeName);
var item = scriptType.ContentItem;
if (item != null)
Editor.Instance.ContentEditing.Open(item);
}
private void OnClickShowScript(ContextMenuButton button)
{
var script = (Script)button.ParentContextMenu.Tag;
var scriptType = TypeUtils.GetType(script.TypeName);
var item = scriptType.ContentItem;
if (item != null)
Editor.Instance.Windows.ContentWin.Select(item);
}
/// <inheritdoc />
public override void Refresh()
{
if (Values.Count == 1)
{
var scripts = ((Actor)ParentEditor.Values[0]).Scripts;
if (!Utils.ArraysEqual(scripts, _scripts))
{
ParentEditor.RebuildLayout();
return;
}
for (int i = 0; i < _scriptToggles.Length; i++)
{
if (_scriptToggles[i] != null)
_scriptToggles[i].Checked = scripts[i].Enabled;
}
}
base.Refresh();
}
/// <inheritdoc />
protected override void Deinitialize()
{
_scriptToggles = null;
base.Deinitialize();
}
}
}