// This code is part of the Fungus library (http://fungusgames.com) maintained by Chris Gregan (http://twitter.com/gofungus). // It is released for free under the MIT open source license (https://github.com/snozbot/fungus/blob/master/LICENSE) using UnityEditor; using UnityEditorInternal; using UnityEngine; using System; using System.Collections; using System.Collections.Generic; using System.Linq; using System.Text.RegularExpressions; using System.IO; using System.Reflection; namespace Fungus.EditorUtils { [CustomEditor(typeof(Block))] public class BlockEditor : Editor { protected class SetEventHandlerOperation { public Block block; public Type eventHandlerType; } protected class AddCommandOperation { public Type commandType; } private static readonly char[] SPLIT_INPUT_ON = new char[] { ' ', '/', '\\' }; private static readonly int MAX_PREVIEW_GRID = 7; private static readonly string ELIPSIS = "..."; public static List actionList = new List(); protected Texture2D upIcon; protected Texture2D downIcon; protected Texture2D addIcon; protected Texture2D duplicateIcon; protected Texture2D deleteIcon; protected string commandTextFieldContents = string.Empty; protected int filteredCommandPreviewSelectedItem = 0; protected Type commandSelectedByTextInput; static List commandTypes; static List eventHandlerTypes; private CommandListAdaptor commandListAdaptor; private SerializedProperty commandListProperty; static void CacheEventHandlerTypes() { eventHandlerTypes = EditorExtensions.FindDerivedTypes(typeof(EventHandler)).ToList(); commandTypes = EditorExtensions.FindDerivedTypes(typeof(Command)).ToList(); } [UnityEditor.Callbacks.DidReloadScripts] private static void OnScriptsReloaded() { CacheEventHandlerTypes(); } protected virtual void OnEnable() { //this appears to happen when leaving playmode try { if (serializedObject == null) return; } catch (Exception) { return; } upIcon = FungusEditorResources.Up; downIcon = FungusEditorResources.Down; addIcon = FungusEditorResources.Add; duplicateIcon = FungusEditorResources.Duplicate; deleteIcon = FungusEditorResources.Delete; commandListProperty = serializedObject.FindProperty("commandList"); commandListAdaptor = new CommandListAdaptor(target as Block, commandListProperty); CacheEventHandlerTypes(); } public virtual void DrawBlockName(Flowchart flowchart) { serializedObject.Update(); SerializedProperty blockNameProperty = serializedObject.FindProperty("blockName"); Rect blockLabelRect = new Rect(45, 5, 120, 16); EditorGUI.LabelField(blockLabelRect, new GUIContent("Block Name")); Rect blockNameRect = new Rect(45, 21, 180, 16); EditorGUI.PropertyField(blockNameRect, blockNameProperty, new GUIContent("")); // Ensure block name is unique for this Flowchart var block = target as Block; string uniqueName = flowchart.GetUniqueBlockKey(blockNameProperty.stringValue, block); if (uniqueName != block.BlockName) { blockNameProperty.stringValue = uniqueName; } serializedObject.ApplyModifiedProperties(); } public virtual void DrawBlockGUI(Flowchart flowchart) { serializedObject.Update(); var block = target as Block; // Execute any queued cut, copy, paste, etc. operations from the prevous GUI update // We need to defer applying these operations until the following update because // the ReorderableList control emits GUI errors if you clear the list in the same frame // as drawing the control (e.g. select all and then delete) if (Event.current.type == EventType.Layout) { foreach (Action action in actionList) { if (action != null) { action(); } } actionList.Clear(); } if (block == flowchart.SelectedBlock) { // Custom tinting SerializedProperty useCustomTintProp = serializedObject.FindProperty("useCustomTint"); SerializedProperty tintProp = serializedObject.FindProperty("tint"); EditorGUILayout.BeginHorizontal(); useCustomTintProp.boolValue = GUILayout.Toggle(useCustomTintProp.boolValue, " Custom Tint"); if (useCustomTintProp.boolValue) { EditorGUILayout.PropertyField(tintProp, GUIContent.none); } EditorGUILayout.EndHorizontal(); SerializedProperty descriptionProp = serializedObject.FindProperty("description"); EditorGUILayout.PropertyField(descriptionProp); DrawEventHandlerGUI(flowchart); block.UpdateIndentLevels(); // Make sure each command has a reference to its parent block foreach (var command in block.CommandList) { if (command == null) // Will be deleted from the list later on { continue; } command.ParentBlock = block; } commandListAdaptor.DrawCommandList(); // EventType.contextClick doesn't register since we moved the Block Editor to be inside // a GUI Area, no idea why. As a workaround we just check for right click instead. if (Event.current.type == EventType.MouseUp && Event.current.button == 1) { ShowContextMenu(); Event.current.Use(); } if (GUIUtility.keyboardControl == 0) //Only call keyboard shortcuts when not typing in a text field { Event e = Event.current; // Copy keyboard shortcut if (e.type == EventType.ValidateCommand && e.commandName == "Copy") { if (flowchart.SelectedCommands.Count > 0) { e.Use(); } } if (e.type == EventType.ExecuteCommand && e.commandName == "Copy") { actionList.Add(Copy); e.Use(); } // Cut keyboard shortcut if (e.type == EventType.ValidateCommand && e.commandName == "Cut") { if (flowchart.SelectedCommands.Count > 0) { e.Use(); } } if (e.type == EventType.ExecuteCommand && e.commandName == "Cut") { actionList.Add(Cut); e.Use(); } // Paste keyboard shortcut if (e.type == EventType.ValidateCommand && e.commandName == "Paste") { CommandCopyBuffer commandCopyBuffer = CommandCopyBuffer.GetInstance(); if (commandCopyBuffer.HasCommands()) { e.Use(); } } if (e.type == EventType.ExecuteCommand && e.commandName == "Paste") { actionList.Add(Paste); e.Use(); } // Duplicate keyboard shortcut if (e.type == EventType.ValidateCommand && e.commandName == "Duplicate") { if (flowchart.SelectedCommands.Count > 0) { e.Use(); } } if (e.type == EventType.ExecuteCommand && e.commandName == "Duplicate") { actionList.Add(Copy); actionList.Add(Paste); e.Use(); } // Delete keyboard shortcut if (e.type == EventType.ValidateCommand && e.commandName == "Delete") { if (flowchart.SelectedCommands.Count > 0) { e.Use(); } } if (e.type == EventType.ExecuteCommand && e.commandName == "Delete") { actionList.Add(Delete); e.Use(); } // SelectAll keyboard shortcut if (e.type == EventType.ValidateCommand && e.commandName == "SelectAll") { e.Use(); } if (e.type == EventType.ExecuteCommand && e.commandName == "SelectAll") { actionList.Add(SelectAll); e.Use(); } } } // Remove any null entries in the command list. // This can happen when a command class is deleted or renamed. for (int i = commandListProperty.arraySize - 1; i >= 0; --i) { SerializedProperty commandProperty = commandListProperty.GetArrayElementAtIndex(i); if (commandProperty.objectReferenceValue == null) { commandListProperty.DeleteArrayElementAtIndex(i); } } serializedObject.ApplyModifiedProperties(); } public virtual void DrawButtonToolbar() { GUILayout.BeginHorizontal(); //handle movement along our selection grid before something else eats our inputs if (Event.current.type == EventType.KeyDown) { //up down to change selection / esc to clear field if (Event.current.keyCode == KeyCode.UpArrow) { filteredCommandPreviewSelectedItem--; } else if (Event.current.keyCode == KeyCode.DownArrow) { filteredCommandPreviewSelectedItem++; } if (commandSelectedByTextInput != null && Event.current.keyCode == KeyCode.Return || Event.current.keyCode == KeyCode.KeypadEnter) { AddCommandCallback(commandSelectedByTextInput); commandSelectedByTextInput = null; commandTextFieldContents = String.Empty; //GUI.FocusControl("dummycontrol"); Event.current.Use(); filteredCommandPreviewSelectedItem = 0; } } // Previous Command if ((Event.current.type == EventType.KeyDown) && (Event.current.keyCode == KeyCode.PageUp)) { SelectPrevious(); GUI.FocusControl("dummycontrol"); Event.current.Use(); } // Next Command if ((Event.current.type == EventType.KeyDown) && (Event.current.keyCode == KeyCode.PageDown)) { SelectNext(); GUI.FocusControl("dummycontrol"); Event.current.Use(); } if (GUILayout.Button(upIcon)) { SelectPrevious(); } // Down Button if (GUILayout.Button(downIcon)) { SelectNext(); } GUILayout.FlexibleSpace(); //should track if text actually changes and pass that to the ShowPartialMatches so it can cache commandTextFieldContents = GUILayout.TextField(commandTextFieldContents, GUILayout.MinWidth(20), GUILayout.MaxWidth(200)); // Add Button if (GUILayout.Button(addIcon)) { ShowCommandMenu(); } // Duplicate Button if (GUILayout.Button(duplicateIcon)) { Copy(); Paste(); } // Delete Button if (GUILayout.Button(deleteIcon)) { Delete(); } GUILayout.EndHorizontal(); if (!string.IsNullOrEmpty(commandTextFieldContents)) ShowPartialMatches(); } //Handles showing partial matches against the text input next to the AddCommand button // Splits and matches and can use up down arrows and return/enter/numenter to confirm // TODO add sorting of results so we get best match at the not just just a match // e.g. "if" should show Flow/If at the top not Flow/Else If private void ShowPartialMatches() { var block = target as Block; var flowchart = (Flowchart)block.GetFlowchart(); //TODO this could be cached if input hasn't changed to avoid thrashing var filteredAttributes = GetFilteredSupportedCommands(flowchart); var upperCommandText = commandTextFieldContents.ToUpper().Trim(); if (upperCommandText.Length == 0) return; var tokens = upperCommandText.Split(SPLIT_INPUT_ON); //we want commands that have all the elements you have typed filteredAttributes = filteredAttributes.Where((x) => { bool catAny = tokens.Any(x.Value.Category.ToUpper().Contains); bool comAny = tokens.Any(x.Value.CommandName.ToUpper().Contains); bool catAll = tokens.All(x.Value.Category.ToUpper().Contains); bool comAll = tokens.All(x.Value.CommandName.ToUpper().Contains); //so if both category and command found something, then there are multiple tokens and they line up with category and command if (catAny && comAny) return true; //or its a single token or a complex token that matches entirely in cat or com else if (catAll || comAll) return true; //this setup avoids multiple bad suggestions due to a complex category name that gives many false matches on complex partials return false; }).ToList(); if (filteredAttributes == null || filteredAttributes.Count == 0) return; //show results GUILayout.Space(5); GUILayout.BeginHorizontal(); filteredCommandPreviewSelectedItem = Mathf.Clamp(filteredCommandPreviewSelectedItem, 0, filteredAttributes.Count - 1); var toShow = filteredAttributes.Select(x => x.Value.Category + "/" + x.Value.CommandName).ToArray(); //show the first x max that match our filters if(toShow.Length > MAX_PREVIEW_GRID) { toShow = toShow.Take(MAX_PREVIEW_GRID).ToArray(); toShow[MAX_PREVIEW_GRID - 1] = ELIPSIS; } filteredCommandPreviewSelectedItem = GUILayout.SelectionGrid(filteredCommandPreviewSelectedItem, toShow, 1); if (toShow[filteredCommandPreviewSelectedItem] != ELIPSIS) { commandSelectedByTextInput = filteredAttributes[filteredCommandPreviewSelectedItem].Key; } else { commandSelectedByTextInput = null; } GUILayout.EndHorizontal(); GUILayout.Space(5); } protected virtual void DrawEventHandlerGUI(Flowchart flowchart) { // Show available Event Handlers in a drop down list with type of current // event handler selected. Block block = target as Block; System.Type currentType = null; if (block._EventHandler != null) { currentType = block._EventHandler.GetType(); } string currentHandlerName = ""; if (currentType != null) { EventHandlerInfoAttribute info = EventHandlerEditor.GetEventHandlerInfo(currentType); if (info != null) { currentHandlerName = info.EventHandlerName; } } EditorGUILayout.BeginHorizontal(); EditorGUILayout.PrefixLabel(new GUIContent("Execute On Event")); if (GUILayout.Button(new GUIContent(currentHandlerName), EditorStyles.popup)) { SetEventHandlerOperation noneOperation = new SetEventHandlerOperation(); noneOperation.block = block; noneOperation.eventHandlerType = null; GenericMenu eventHandlerMenu = new GenericMenu(); eventHandlerMenu.AddItem(new GUIContent("None"), false, OnSelectEventHandler, noneOperation); // Add event handlers with no category first foreach (System.Type type in eventHandlerTypes) { EventHandlerInfoAttribute info = EventHandlerEditor.GetEventHandlerInfo(type); if (info != null && info.Category.Length == 0) { SetEventHandlerOperation operation = new SetEventHandlerOperation(); operation.block = block; operation.eventHandlerType = type; eventHandlerMenu.AddItem(new GUIContent(info.EventHandlerName), false, OnSelectEventHandler, operation); } } // Add event handlers with a category afterwards foreach (System.Type type in eventHandlerTypes) { EventHandlerInfoAttribute info = EventHandlerEditor.GetEventHandlerInfo(type); if (info != null && info.Category.Length > 0) { SetEventHandlerOperation operation = new SetEventHandlerOperation(); operation.block = block; operation.eventHandlerType = type; string typeName = info.Category + "/" + info.EventHandlerName; eventHandlerMenu.AddItem(new GUIContent(typeName), false, OnSelectEventHandler, operation); } } eventHandlerMenu.ShowAsContext(); } EditorGUILayout.EndHorizontal(); if (block._EventHandler != null) { EventHandlerEditor eventHandlerEditor = Editor.CreateEditor(block._EventHandler) as EventHandlerEditor; if (eventHandlerEditor != null) { eventHandlerEditor.DrawInspectorGUI(); DestroyImmediate(eventHandlerEditor); } } } protected void OnSelectEventHandler(object obj) { SetEventHandlerOperation operation = obj as SetEventHandlerOperation; Block block = operation.block; System.Type selectedType = operation.eventHandlerType; if (block == null) { return; } Undo.RecordObject(block, "Set Event Handler"); if (block._EventHandler != null) { Undo.DestroyObjectImmediate(block._EventHandler); } if (selectedType != null) { EventHandler newHandler = Undo.AddComponent(block.gameObject, selectedType) as EventHandler; newHandler.ParentBlock = block; block._EventHandler = newHandler; } // Because this is an async call, we need to force prefab instances to record changes PrefabUtility.RecordPrefabInstancePropertyModifications(block); } public static void BlockField(SerializedProperty property, GUIContent label, GUIContent nullLabel, Flowchart flowchart) { if (flowchart == null) { return; } var block = property.objectReferenceValue as Block; // Build dictionary of child blocks List blockNames = new List(); int selectedIndex = 0; blockNames.Add(nullLabel); var blocks = flowchart.GetComponents(); for (int i = 0; i < blocks.Length; ++i) { blockNames.Add(new GUIContent(blocks[i].BlockName)); if (block == blocks[i]) { selectedIndex = i + 1; } } selectedIndex = EditorGUILayout.Popup(label, selectedIndex, blockNames.ToArray()); if (selectedIndex == 0) { block = null; // Option 'None' } else { block = blocks[selectedIndex - 1]; } property.objectReferenceValue = block; } public static Block BlockField(Rect position, GUIContent nullLabel, Flowchart flowchart, Block block) { if (flowchart == null) { return null; } Block result = block; // Build dictionary of child blocks List blockNames = new List(); int selectedIndex = 0; blockNames.Add(nullLabel); Block[] blocks = flowchart.GetComponents(); for (int i = 0; i < blocks.Length; ++i) { blockNames.Add(new GUIContent(blocks[i].BlockName)); if (block == blocks[i]) { selectedIndex = i + 1; } } selectedIndex = EditorGUI.Popup(position, selectedIndex, blockNames.ToArray()); if (selectedIndex == 0) { result = null; // Option 'None' } else { result = blocks[selectedIndex - 1]; } return result; } // Compare delegate for sorting the list of command attributes protected static int CompareCommandAttributes(KeyValuePair x, KeyValuePair y) { int compare = (x.Value.Category.CompareTo(y.Value.Category)); if (compare == 0) { compare = (x.Value.CommandName.CompareTo(y.Value.CommandName)); } return compare; } [MenuItem("Tools/Fungus/Utilities/Export Reference Docs")] protected static void ExportReferenceDocs() { const string path = "./Docs"; ExportCommandInfo(path); ExportEventHandlerInfo(path); FlowchartWindow.ShowNotification("Exported Reference Documentation"); } protected static void ExportCommandInfo(string path) { // Dump command info List menuTypes = EditorExtensions.FindDerivedTypes(typeof(Command)).ToList(); List> filteredAttributes = GetFilteredCommandInfoAttribute(menuTypes); filteredAttributes.Sort(CompareCommandAttributes); // Build list of command categories List commandCategories = new List(); foreach (var keyPair in filteredAttributes) { CommandInfoAttribute info = keyPair.Value; if (info.Category != "" && !commandCategories.Contains(info.Category)) { commandCategories.Add(info.Category); } } commandCategories.Sort(); // Output the commands in each category foreach (string category in commandCategories) { string markdown = "# " + category + " commands # {#" + category.ToLower() + "_commands}\n\n"; markdown += "[TOC]\n"; foreach (var keyPair in filteredAttributes) { CommandInfoAttribute info = keyPair.Value; if (info.Category == category || info.Category == "" && category == "Scripting") { markdown += "# " + info.CommandName + " # {#" + info.CommandName.Replace(" ", "") + "}\n"; markdown += info.HelpText + "\n\n"; markdown += "Defined in " + keyPair.Key.FullName + "\n"; markdown += GetPropertyInfo(keyPair.Key); } } string filePath = path + "/command_ref/" + category.ToLower() + "_commands.md"; Directory.CreateDirectory(Path.GetDirectoryName(filePath)); File.WriteAllText(filePath, markdown); } } protected static void ExportEventHandlerInfo(string path) { List eventHandlerTypes = EditorExtensions.FindDerivedTypes(typeof(EventHandler)).ToList(); List eventHandlerCategories = new List(); eventHandlerCategories.Add("Core"); foreach (System.Type type in eventHandlerTypes) { EventHandlerInfoAttribute info = EventHandlerEditor.GetEventHandlerInfo(type); if (info != null && info.Category != "" && !eventHandlerCategories.Contains(info.Category)) { eventHandlerCategories.Add(info.Category); } } eventHandlerCategories.Sort(); // Output the commands in each category foreach (string category in eventHandlerCategories) { string markdown = "# " + category + " event handlers # {#" + category.ToLower() + "_events}\n\n"; markdown += "[TOC]\n"; foreach (System.Type type in eventHandlerTypes) { EventHandlerInfoAttribute info = EventHandlerEditor.GetEventHandlerInfo(type); if (info != null && (info.Category == category || (info.Category == "" && category == "Core"))) { markdown += "# " + info.EventHandlerName + " # {#" + info.EventHandlerName.Replace(" ", "") + "}\n"; markdown += info.HelpText + "\n\n"; markdown += "Defined in " + type.FullName + "\n"; markdown += GetPropertyInfo(type); } } string filePath = path + "/command_ref/" + category.ToLower() + "_events.md"; Directory.CreateDirectory(Path.GetDirectoryName(filePath)); File.WriteAllText(filePath, markdown); } } protected static string GetPropertyInfo(System.Type type) { string markdown = ""; foreach (FieldInfo field in type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)) { TooltipAttribute attribute = (TooltipAttribute)Attribute.GetCustomAttribute(field, typeof(TooltipAttribute)); if (attribute == null) { continue; } // Change field name to how it's displayed in the inspector string propertyName = Regex.Replace(field.Name, "(\\B[A-Z])", " $1"); if (propertyName.Length > 1) { propertyName = propertyName.Substring(0, 1).ToUpper() + propertyName.Substring(1); } else { propertyName = propertyName.ToUpper(); } markdown += propertyName + " | " + field.FieldType + " | " + attribute.tooltip + "\n"; } if (markdown.Length > 0) { markdown = "\nProperty | Type | Description\n --- | --- | ---\n" + markdown + "\n"; } return markdown; } protected virtual void ShowCommandMenu() { var block = target as Block; var flowchart = (Flowchart)block.GetFlowchart(); GenericMenu commandMenu = new GenericMenu(); // Build menu list var filteredAttributes = GetFilteredSupportedCommands(flowchart); foreach (var keyPair in filteredAttributes) { AddCommandOperation commandOperation = new AddCommandOperation(); commandOperation.commandType = keyPair.Key; GUIContent menuItem; if (keyPair.Value.Category == "") { menuItem = new GUIContent(keyPair.Value.CommandName); } else { menuItem = new GUIContent(keyPair.Value.Category + "/" + keyPair.Value.CommandName); } commandMenu.AddItem(menuItem, false, AddCommandCallback, commandOperation); } commandMenu.ShowAsContext(); } protected static List> GetFilteredSupportedCommands(Flowchart flowchart) { List> filteredAttributes = GetFilteredCommandInfoAttribute(commandTypes); filteredAttributes.Sort(CompareCommandAttributes); filteredAttributes = filteredAttributes.Where(x => flowchart.IsCommandSupported(x.Value)).ToList(); return filteredAttributes; } protected static List> GetFilteredCommandInfoAttribute(List menuTypes) { Dictionary> filteredAttributes = new Dictionary>(); foreach (System.Type type in menuTypes) { object[] attributes = type.GetCustomAttributes(false); foreach (object obj in attributes) { CommandInfoAttribute infoAttr = obj as CommandInfoAttribute; if (infoAttr != null) { string dictionaryName = string.Format("{0}/{1}", infoAttr.Category, infoAttr.CommandName); int existingItemPriority = -1; if (filteredAttributes.ContainsKey(dictionaryName)) { existingItemPriority = filteredAttributes[dictionaryName].Value.Priority; } if (infoAttr.Priority > existingItemPriority) { KeyValuePair keyValuePair = new KeyValuePair(type, infoAttr); filteredAttributes[dictionaryName] = keyValuePair; } } } } return filteredAttributes.Values.ToList>(); } //Used by GenericMenu Delegate protected void AddCommandCallback(object obj) { AddCommandOperation commandOperation = obj as AddCommandOperation; if (commandOperation != null) { AddCommandCallback(commandOperation.commandType); } } protected void AddCommandCallback(Type commandType) { var block = target as Block; if (block == null) { return; } var flowchart = (Flowchart)block.GetFlowchart(); // Use index of last selected command in list, or end of list if nothing selected. int index = -1; foreach (var command in flowchart.SelectedCommands) { if (command.CommandIndex + 1 > index) { index = command.CommandIndex + 1; } } if (index == -1) { index = block.CommandList.Count; } var newCommand = Undo.AddComponent(block.gameObject, commandType) as Command; block.GetFlowchart().AddSelectedCommand(newCommand); newCommand.ParentBlock = block; newCommand.ItemId = flowchart.NextItemId(); // Let command know it has just been added to the block newCommand.OnCommandAdded(block); Undo.RecordObject(block, "Set command type"); if (index < block.CommandList.Count - 1) { block.CommandList.Insert(index, newCommand); } else { block.CommandList.Add(newCommand); } // Because this is an async call, we need to force prefab instances to record changes PrefabUtility.RecordPrefabInstancePropertyModifications(block); flowchart.ClearSelectedCommands(); commandTextFieldContents = string.Empty; } public virtual void ShowContextMenu() { var block = target as Block; var flowchart = (Flowchart)block.GetFlowchart(); if (flowchart == null) { return; } bool showCut = false; bool showCopy = false; bool showDelete = false; bool showPaste = false; bool showPlay = false; if (flowchart.SelectedCommands.Count > 0) { showCut = true; showCopy = true; showDelete = true; if (flowchart.SelectedCommands.Count == 1 && Application.isPlaying) { showPlay = true; } } CommandCopyBuffer commandCopyBuffer = CommandCopyBuffer.GetInstance(); if (commandCopyBuffer.HasCommands()) { showPaste = true; } GenericMenu commandMenu = new GenericMenu(); if (showCut) { commandMenu.AddItem(new GUIContent("Cut"), false, Cut); } else { commandMenu.AddDisabledItem(new GUIContent("Cut")); } if (showCopy) { commandMenu.AddItem(new GUIContent("Copy"), false, Copy); } else { commandMenu.AddDisabledItem(new GUIContent("Copy")); } if (showPaste) { commandMenu.AddItem(new GUIContent("Paste"), false, Paste); } else { commandMenu.AddDisabledItem(new GUIContent("Paste")); } if (showDelete) { commandMenu.AddItem(new GUIContent("Delete"), false, Delete); } else { commandMenu.AddDisabledItem(new GUIContent("Delete")); } if (showPlay) { commandMenu.AddItem(new GUIContent("Play from selected"), false, PlayCommand); commandMenu.AddItem(new GUIContent("Stop all and play"), false, StopAllPlayCommand); } commandMenu.AddSeparator(""); commandMenu.AddItem(new GUIContent("Select All"), false, SelectAll); commandMenu.AddItem(new GUIContent("Select None"), false, SelectNone); commandMenu.ShowAsContext(); } protected void SelectAll() { var block = target as Block; var flowchart = (Flowchart)block.GetFlowchart(); if (flowchart == null || flowchart.SelectedBlock == null) { return; } flowchart.ClearSelectedCommands(); Undo.RecordObject(flowchart, "Select All"); foreach (Command command in flowchart.SelectedBlock.CommandList) { flowchart.AddSelectedCommand(command); } Repaint(); } protected void SelectNone() { var block = target as Block; var flowchart = (Flowchart)block.GetFlowchart(); if (flowchart == null || flowchart.SelectedBlock == null) { return; } Undo.RecordObject(flowchart, "Select None"); flowchart.ClearSelectedCommands(); Repaint(); } protected void Cut() { Copy(); Delete(); } protected void Copy() { var block = target as Block; var flowchart = (Flowchart)block.GetFlowchart(); if (flowchart == null || flowchart.SelectedBlock == null) { return; } CommandCopyBuffer commandCopyBuffer = CommandCopyBuffer.GetInstance(); commandCopyBuffer.Clear(); // Scan through all commands in execution order to see if each needs to be copied foreach (Command command in flowchart.SelectedBlock.CommandList) { if (flowchart.SelectedCommands.Contains(command)) { var type = command.GetType(); Command newCommand = Undo.AddComponent(commandCopyBuffer.gameObject, type) as Command; var fields = type.GetFields(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.FlattenHierarchy); foreach (var field in fields) { // Copy all public fields bool copy = field.IsPublic; // Copy non-public fields that have the SerializeField attribute var attributes = field.GetCustomAttributes(typeof(SerializeField), true); if (attributes.Length > 0) { copy = true; } if (copy) { field.SetValue(newCommand, field.GetValue(command)); } } } } } protected void Paste() { var block = target as Block; var flowchart = (Flowchart)block.GetFlowchart(); if (flowchart == null || flowchart.SelectedBlock == null) { return; } CommandCopyBuffer commandCopyBuffer = CommandCopyBuffer.GetInstance(); // Find where to paste commands in block (either at end or after last selected command) int pasteIndex = flowchart.SelectedBlock.CommandList.Count; if (flowchart.SelectedCommands.Count > 0) { for (int i = 0; i < flowchart.SelectedBlock.CommandList.Count; ++i) { Command command = flowchart.SelectedBlock.CommandList[i]; foreach (Command selectedCommand in flowchart.SelectedCommands) { if (command == selectedCommand) { pasteIndex = i + 1; } } } } foreach (Command command in commandCopyBuffer.GetCommands()) { // Using the Editor copy / paste functionality instead instead of reflection // because this does a deep copy of the command properties. if (ComponentUtility.CopyComponent(command)) { if (ComponentUtility.PasteComponentAsNew(flowchart.gameObject)) { Command[] commands = flowchart.GetComponents(); Command pastedCommand = commands.Last(); if (pastedCommand != null) { pastedCommand.ItemId = flowchart.NextItemId(); flowchart.SelectedBlock.CommandList.Insert(pasteIndex++, pastedCommand); } } // This stops the user pasting the command manually into another game object. ComponentUtility.CopyComponent(flowchart.transform); } } // Because this is an async call, we need to force prefab instances to record changes PrefabUtility.RecordPrefabInstancePropertyModifications(block); Repaint(); } protected void Delete() { var block = target as Block; var flowchart = (Flowchart)block.GetFlowchart(); if (flowchart == null || flowchart.SelectedBlock == null) { return; } int lastSelectedIndex = 0; for (int i = flowchart.SelectedBlock.CommandList.Count - 1; i >= 0; --i) { Command command = flowchart.SelectedBlock.CommandList[i]; foreach (Command selectedCommand in flowchart.SelectedCommands) { if (command == selectedCommand) { command.OnCommandRemoved(block); // Order of destruction is important here for undo to work Undo.DestroyObjectImmediate(command); Undo.RecordObject((Block)flowchart.SelectedBlock, "Delete"); flowchart.SelectedBlock.CommandList.RemoveAt(i); lastSelectedIndex = i; break; } } } Undo.RecordObject(flowchart, "Delete"); flowchart.ClearSelectedCommands(); if (lastSelectedIndex < flowchart.SelectedBlock.CommandList.Count) { var nextCommand = flowchart.SelectedBlock.CommandList[lastSelectedIndex]; block.GetFlowchart().AddSelectedCommand(nextCommand); } Repaint(); } protected void PlayCommand() { var targetBlock = target as Block; var flowchart = (Flowchart)targetBlock.GetFlowchart(); Command command = flowchart.SelectedCommands[0]; if (targetBlock.IsExecuting()) { // The Block is already executing. // Tell the Block to stop, wait a little while so the executing command has a // chance to stop, and then start execution again from the new command. targetBlock.Stop(); flowchart.StartCoroutine(RunBlock(flowchart, targetBlock, command.CommandIndex, 0.2f)); } else { // Block isn't executing yet so can start it now. flowchart.ExecuteBlock(targetBlock, command.CommandIndex); } } protected void StopAllPlayCommand() { var targetBlock = target as Block; var flowchart = (Flowchart)targetBlock.GetFlowchart(); Command command = flowchart.SelectedCommands[0]; // Stop all active blocks then run the selected block. flowchart.StopAllBlocks(); flowchart.StartCoroutine(RunBlock(flowchart, targetBlock, command.CommandIndex, 0.2f)); } protected IEnumerator RunBlock(Flowchart flowchart, Block targetBlock, int commandIndex, float delay) { yield return new WaitForSeconds(delay); flowchart.ExecuteBlock(targetBlock, commandIndex); } protected void SelectPrevious() { var block = target as Block; var flowchart = (Flowchart)block.GetFlowchart(); int firstSelectedIndex = flowchart.SelectedBlock.CommandList.Count; bool firstSelectedCommandFound = false; if (flowchart.SelectedCommands.Count > 0) { for (int i = 0; i < flowchart.SelectedBlock.CommandList.Count; i++) { Command commandInBlock = flowchart.SelectedBlock.CommandList[i]; foreach (Command selectedCommand in flowchart.SelectedCommands) { if (commandInBlock == selectedCommand) { if (!firstSelectedCommandFound) { firstSelectedIndex = i; firstSelectedCommandFound = true; break; } } } if (firstSelectedCommandFound) { break; } } } if (firstSelectedIndex > 0) { flowchart.ClearSelectedCommands(); flowchart.AddSelectedCommand(flowchart.SelectedBlock.CommandList[firstSelectedIndex - 1]); } Repaint(); } protected void SelectNext() { var block = target as Block; var flowchart = (Flowchart)block.GetFlowchart(); int lastSelectedIndex = -1; if (flowchart.SelectedCommands.Count > 0) { for (int i = 0; i < flowchart.SelectedBlock.CommandList.Count; i++) { Command commandInBlock = flowchart.SelectedBlock.CommandList[i]; foreach (Command selectedCommand in flowchart.SelectedCommands) { if (commandInBlock == selectedCommand) { lastSelectedIndex = i; } } } } if (lastSelectedIndex < flowchart.SelectedBlock.CommandList.Count - 1) { flowchart.ClearSelectedCommands(); flowchart.AddSelectedCommand(flowchart.SelectedBlock.CommandList[lastSelectedIndex + 1]); } Repaint(); } } }