// Copyright (c) 2012-2024 Wojciech Figat. All rights reserved.
using System;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
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
{
internal class NewScriptItem : ItemsListContextMenu.Item
{
private string _scriptName;
public string ScriptName
{
get => _scriptName;
set
{
_scriptName = value;
Name = $"Create script '{value}'";
}
}
public NewScriptItem(string scriptName)
{
ScriptName = scriptName;
TooltipText = "Create a new script";
}
}
///
/// Drag and drop scripts area control.
///
///
public class DragAreaControl : ContainerControl
{
private DragHandlers _dragHandlers;
private DragScriptItems _dragScripts;
private DragAssets _dragAssets;
private Button _addScriptsButton;
///
/// The parent scripts editor.
///
public ScriptsEditor ScriptsEditor;
///
/// Initializes a new instance of the class.
///
public DragAreaControl()
: base(0, 0, 120, 40)
{
AutoFocus = false;
// Add script button
var buttonText = "Add script";
var textSize = Style.Current.FontMedium.MeasureText(buttonText);
float addScriptButtonWidth = (textSize.X < 60.0f) ? 60.0f : textSize.X + 4;
var buttonHeight = (textSize.Y < 18) ? 18 : textSize.Y + 4;
_addScriptsButton = new Button
{
TooltipText = "Add new scripts to the actor",
AnchorPreset = AnchorPresets.MiddleCenter,
Text = buttonText,
Parent = this,
Bounds = new Rectangle((Width - addScriptButtonWidth) / 2, 1, addScriptButtonWidth, buttonHeight),
};
_addScriptsButton.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.TextChanged += text =>
{
if (!IsValidScriptName(text))
return;
if (!cm.ItemsPanel.Children.Any(x => x.Visible && x is not NewScriptItem))
{
// If there are no visible items, that means the search failed so we can find the create script button or create one if it's the first time
var newScriptItem = (NewScriptItem)cm.ItemsPanel.Children.FirstOrDefault(x => x is NewScriptItem);
if (newScriptItem != null)
{
newScriptItem.Visible = true;
newScriptItem.ScriptName = text;
}
else
{
cm.AddItem(new NewScriptItem(text));
}
}
else
{
// Make sure to hide the create script button if there
var newScriptItem = cm.ItemsPanel.Children.FirstOrDefault(x => x is NewScriptItem);
if (newScriptItem != null)
newScriptItem.Visible = false;
}
};
cm.ItemClicked += item =>
{
if (item.Tag is ScriptType script)
{
AddScript(script);
}
else if (item is NewScriptItem newScriptItem)
{
CreateScript(newScriptItem);
}
};
cm.SortItems();
cm.Show(this, button.BottomLeft - new Float2((cm.Width - button.Width) / 2, 0));
}
///
public override void Draw()
{
var style = Style.Current;
var size = Size;
// Info
Render2D.DrawText(style.FontSmall, "Drag scripts here", new Rectangle(2, _addScriptsButton.Height + 4, 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, style.Selection);
Render2D.DrawRectangle(area, style.SelectionBorder);
}
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;
}
private static bool IsValidScriptName(string text)
{
if (string.IsNullOrEmpty(text))
return false;
if (text.Contains(' '))
return false;
if (char.IsDigit(text[0]))
return false;
if (text.Any(c => !char.IsLetterOrDigit(c) && c != '_'))
return false;
return Editor.Instance.ContentDatabase.GetProxy("cs").IsFileNameValid(text);
}
///
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);
}
///
public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
{
var result = base.OnDragMove(ref location, data);
if (result != DragDropEffect.None || _dragHandlers == null)
return result;
return _dragHandlers.Effect;
}
///
public override void OnDragLeave()
{
_dragHandlers?.OnDragLeave();
base.OnDragLeave();
}
///
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 CreateScript(NewScriptItem item)
{
ScriptsEditor.NewScriptName = item.ScriptName;
var paths = Directory.GetFiles(Globals.ProjectSourceFolder, "*.Build.cs");
string moduleName = null;
foreach (var p in paths)
{
var file = File.ReadAllText(p);
if (!file.Contains("GameProjectTarget"))
continue; // Skip
if (file.Contains("Modules.Add(\"Game\")"))
{
// Assume Game represents the main game module
moduleName = "Game";
break;
}
}
// Ensure the path slashes are correct for the OS
var correctedPath = Path.GetFullPath(Globals.ProjectSourceFolder);
if (string.IsNullOrEmpty(moduleName))
{
var error = FileSystem.ShowBrowseFolderDialog(Editor.Instance.Windows.MainWindow, correctedPath, "Select a module folder to put the new script in", out moduleName);
if (error)
return;
}
var path = Path.Combine(Globals.ProjectSourceFolder, moduleName, item.ScriptName + ".cs");
Editor.Instance.ContentDatabase.GetProxy("cs").Create(path, null);
}
///
/// Attach a script to the actor.
///
/// The script.
public void AddScript(ScriptType item)
{
var list = new List(1) { item };
AddScripts(list);
}
private void AddScripts(List items)
{
var list = new List(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 items)
{
var list = new List(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 items)
{
var actions = new List();
for (int i = 0; i < items.Count; i++)
{
var scriptType = items[i];
RequireScriptAttribute scriptAttribute = null;
if (scriptType.HasAttribute(typeof(RequireScriptAttribute), false))
{
foreach (var e in scriptType.GetAttributes(false))
{
if (e is not RequireScriptAttribute requireScriptAttribute)
continue;
scriptAttribute = requireScriptAttribute;
break;
}
}
// See if script requires a specific actor type
RequireActorAttribute actorAttribute = null;
if (scriptType.HasAttribute(typeof(RequireActorAttribute), false))
{
foreach (var e in scriptType.GetAttributes(false))
{
if (e is not RequireActorAttribute requireActorAttribute)
continue;
actorAttribute = requireActorAttribute;
break;
}
}
var actors = ScriptsEditor.ParentEditor.Values;
for (int j = 0; j < actors.Count; j++)
{
var actor = (Actor)actors[j];
// If required actor exists but is not this actor type then skip adding to actor
if (actorAttribute != null)
{
if (actor.GetType() != actorAttribute.RequiredType && !actor.GetType().IsSubclassOf(actorAttribute.RequiredType))
{
Editor.LogWarning($"`{Utilities.Utils.GetPropertyNameUI(scriptType.Name)}` not added to `{actor}` due to script requiring an Actor type of `{actorAttribute.RequiredType}`.");
continue;
}
}
actions.Add(AddRemoveScript.Add(actor, scriptType));
// Check if actor has required scripts and add them if the actor does not.
if (scriptAttribute != null)
{
foreach (var type in scriptAttribute.RequiredTypes)
{
if (!type.IsSubclassOf(typeof(Script)))
{
Editor.LogWarning($"`{Utilities.Utils.GetPropertyNameUI(type.Name)}` not added to `{actor}` due to the class not being a subclass of Script.");
continue;
}
if (actor.GetScript(type) != null)
continue;
actions.Add(AddRemoveScript.Add(actor, new ScriptType(type)));
}
}
}
}
if (actions.Count == 0)
{
Editor.LogWarning("Failed to spawn scripts");
return;
}
var multiAction = new MultiUndoAction(actions);
multiAction.Do();
var presenter = ScriptsEditor.Presenter;
ScriptsEditor.ParentEditor?.RebuildLayout();
if (presenter != null)
{
presenter.Undo.AddAction(multiAction);
presenter.Control.Focus();
}
}
}
///
/// Small image control added per script group that allows to drag and drop a reference to it. Also used to reorder the scripts.
///
///
internal class DragImage : Image
{
private bool _isMouseDown;
private Float2 _mouseDownPos;
///
/// Action called when drag event should start.
///
public Action Drag;
///
public override void OnMouseEnter(Float2 location)
{
_mouseDownPos = Float2.Minimum;
base.OnMouseEnter(location);
}
///
public override void OnMouseLeave()
{
if (_isMouseDown)
{
_isMouseDown = false;
Drag(this);
}
base.OnMouseLeave();
}
///
public override void OnMouseMove(Float2 location)
{
if (_isMouseDown && Float2.Distance(location, _mouseDownPos) > 10.0f)
{
_isMouseDown = false;
Drag(this);
}
base.OnMouseMove(location);
}
///
public override bool OnMouseUp(Float2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
_isMouseDown = false;
return true;
}
return base.OnMouseUp(location, button);
}
///
public override bool OnMouseDown(Float2 location, MouseButton button)
{
if (button == MouseButton.Left)
{
_isMouseDown = true;
_mouseDownPos = location;
return true;
}
return base.OnMouseDown(location, button);
}
}
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();
}
///
public override void Draw()
{
base.Draw();
var color = Style.Current.BackgroundSelected * (IsDragOver ? 0.9f : 0.1f);
Render2D.FillRectangle(new Rectangle(Float2.Zero, Size), color);
}
///
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;
}
///
public override DragDropEffect OnDragMove(ref Float2 location, DragData data)
{
return _dragEffect;
}
///
public override void OnDragLeave()
{
_dragEffect = DragDropEffect.None;
base.OnDragLeave();
}
///
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;
}
}
///
/// Custom editor for actor scripts collection.
///
///
public sealed class ScriptsEditor : SyncPointEditor
{
private CheckBox[] _scriptToggles;
///
/// Delegate for script drag start and event events.
///
/// Set to true if drag started, otherwise false.
/// The target script to reorder.
public delegate void ScriptDragDelegate(bool start, Script script);
///
/// Occurs when script drag changes (starts or ends).
///
public event ScriptDragDelegate ScriptDragChange;
///
/// The scripts collection. Undo operations are recorder for scripts.
///
private readonly List