// 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++) { var script = scripts[i]; var item = new TypeSearchPopup.TypeItemView(script); if (script.GetAttributes(false).FirstOrDefault(x => x is RequireActorAttribute) is RequireActorAttribute requireActor) { var actors = ScriptsEditor.ParentEditor.Values; foreach (var a in actors) { if (a.GetType() != requireActor.RequiredType) { item.Enabled = false; break; } } } cm.AddItem(item); } 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\")") || file.Contains("Modules.Add(nameof(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(); // Scroll to bottom of script control where a new script is added. if (presenter.Panel.Parent is Panel p && Editor.Instance.Options.Options.Interface.ScrollToScriptOnAdd) { var loc = ScriptsEditor.Layout.Control.BottomLeft; p.ScrollViewTo(loc); } } } } /// /// 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