diff --git a/Assets/Fungus/Scripts/Components/Flowchart.cs b/Assets/Fungus/Scripts/Components/Flowchart.cs index 34e590c8..4ebdd2b7 100644 --- a/Assets/Fungus/Scripts/Components/Flowchart.cs +++ b/Assets/Fungus/Scripts/Components/Flowchart.cs @@ -41,8 +41,7 @@ namespace Fungus [SerializeField] protected Rect scrollViewRect; [HideInInspector] - [FormerlySerializedAs("selectedSequence")] - [SerializeField] protected Block selectedBlock; + [SerializeField] protected List selectedBlocks = new List(); [HideInInspector] [SerializeField] protected List selectedCommands = new List(); @@ -325,9 +324,22 @@ namespace Fungus public virtual Rect ScrollViewRect { get { return scrollViewRect; } set { scrollViewRect = value; } } /// - /// Currently selected block in the Flowchart editor. + /// Current actively selected block in the Flowchart editor. /// - public virtual Block SelectedBlock { get { return selectedBlock; } set { selectedBlock = value; } } + public virtual Block SelectedBlock + { + get + { + return selectedBlocks.FirstOrDefault(); + } + set + { + selectedBlocks.Clear(); + selectedBlocks.Add(value); + } + } + + public virtual List SelectedBlocks { get { return selectedBlocks; } set { selectedBlocks = value; } } /// /// Currently selected command in the Flowchart editor. @@ -1040,6 +1052,25 @@ namespace Fungus } } + /// + /// Clears the list of selected blocks. + /// + public virtual void ClearSelectedBlocks() + { + selectedBlocks.Clear(); + } + + /// + /// Adds a block to the list of selected blocks. + /// + public virtual void AddSelectedBlock(Block block) + { + if (!selectedBlocks.Contains(block)) + { + selectedBlocks.Add(block); + } + } + /// /// Reset the commands and variables in the Flowchart. /// diff --git a/Assets/Fungus/Scripts/Editor/BlockInspector.cs b/Assets/Fungus/Scripts/Editor/BlockInspector.cs index 7f0534a3..cfce2dbf 100644 --- a/Assets/Fungus/Scripts/Editor/BlockInspector.cs +++ b/Assets/Fungus/Scripts/Editor/BlockInspector.cs @@ -83,6 +83,12 @@ namespace Fungus.EditorUtils var flowchart = (Flowchart)block.GetFlowchart(); + if (flowchart.SelectedBlocks.Count > 1) + { + GUILayout.Label("Multiple blocks selected"); + return; + } + if (activeBlockEditor == null || !block.Equals(activeBlockEditor.target)) { diff --git a/Assets/Fungus/Scripts/Editor/FlowchartWindow.cs b/Assets/Fungus/Scripts/Editor/FlowchartWindow.cs index 17e7069d..07b01719 100644 --- a/Assets/Fungus/Scripts/Editor/FlowchartWindow.cs +++ b/Assets/Fungus/Scripts/Editor/FlowchartWindow.cs @@ -33,6 +33,10 @@ namespace Fungus.EditorUtils protected int forceRepaintCount; protected Texture2D addTexture; + + protected Rect selectionBox; + protected Vector2 startSelectionBoxPosition; + protected List mouseDownSelectionState; [MenuItem("Tools/Fungus/Flowchart Window")] static void Init() @@ -120,7 +124,7 @@ namespace Fungus.EditorUtils // Delete any scheduled objects foreach (var deleteBlock in deleteList) { - bool isSelected = (flowchart.SelectedBlock == deleteBlock); + bool isSelected = (flowchart.SelectedBlocks.Contains(deleteBlock)); var commandList = deleteBlock.CommandList; foreach (var command in commandList) @@ -142,6 +146,9 @@ namespace Fungus.EditorUtils DrawFlowchartView(flowchart); DrawOverlay(flowchart); + // Handle selection box events after block and overlay events + HandleSelectionBox(flowchart); + if (forceRepaintCount > 0) { // Redraw on next frame to get crisp refresh rate @@ -248,18 +255,6 @@ namespace Fungus.EditorUtils GLDraw.BeginGroup(scriptViewRect); - if (Event.current.button == 0 && - Event.current.type == EventType.MouseDown && - !mouseOverVariables) - { - flowchart.SelectedBlock = null; - if (!EditorGUI.actionKey) - { - flowchart.ClearSelectedCommands(); - } - Selection.activeGameObject = flowchart.gameObject; - } - // The center of the Flowchart depends on the block positions and window dimensions, so we calculate it // here in the FlowchartWindow class and store it on the Flowchart object for use later. CalcFlowchartCenter(flowchart, blocks); @@ -280,6 +275,8 @@ namespace Fungus.EditorUtils BeginWindows(); windowBlockMap.Clear(); + bool useEvent = false; + bool endDrag = false; for (int i = 0; i < blocks.Length; ++i) { var block = blocks[i]; @@ -297,28 +294,33 @@ namespace Fungus.EditorUtils tempRect.width = Mathf.Max(Mathf.Max(nodeWidthA, nodeWidthB), 120); tempRect.height = 40; - if (Event.current.type == EventType.MouseDrag && dragWindowId == i) + if (dragWindowId > -1 && flowchart.SelectedBlocks.Contains(block)) { - tempRect.x += Event.current.delta.x; - tempRect.y += Event.current.delta.y; + if (Event.current.type == EventType.MouseDrag) + { + tempRect.x += Event.current.delta.x; + tempRect.y += Event.current.delta.y; - forceRepaintCount = 6; - } - else if (Event.current.type == EventType.MouseUp && - dragWindowId == i) - { - Vector2 newPos = new Vector2(tempRect.x, tempRect.y); - - tempRect.x = startDragPosition.x; - tempRect.y = startDragPosition.y; - - Undo.RecordObject((Block)block, "Node Position"); - - tempRect.x = newPos.x; - tempRect.y = newPos.y; + forceRepaintCount = 6; + useEvent = true; + } + else if (Event.current.rawType == EventType.MouseUp) + { + Vector2 newPos = new Vector2(tempRect.x, tempRect.y); + tempRect.x = startDragPosition.x + (newPos.x - blocks[dragWindowId]._NodeRect.position.x); + tempRect.y = startDragPosition.y + (newPos.y - blocks[dragWindowId]._NodeRect.position.y); - dragWindowId = -1; - forceRepaintCount = 6; + block._NodeRect = tempRect; + + Undo.RecordObject(block, "Node Position"); + + tempRect.x = newPos.x; + tempRect.y = newPos.y; + + forceRepaintCount = 6; + useEvent = true; + endDrag = true; + } } block._NodeRect = tempRect; @@ -335,6 +337,13 @@ namespace Fungus.EditorUtils windowBlockMap.Add(block); } + dragWindowId = endDrag ? -1 : dragWindowId; + + if (useEvent) + { + Event.current.Use(); + } + EndWindows(); // Draw Event Handler labels @@ -407,6 +416,23 @@ namespace Fungus.EditorUtils GLDraw.EndGroup(); EditorZoomArea.End(); + + // If event has yet to be used and user isn't multiselecting or panning, clear selection + bool validModifier = Event.current.alt || GetAppendModifierDown(); + if (Event.current.type == EventType.MouseDown && Event.current.button == 0 && !validModifier) + { + Undo.RecordObject(flowchart, "Deselect"); + flowchart.ClearSelectedCommands(); + flowchart.ClearSelectedBlocks(); + Selection.activeGameObject = flowchart.gameObject; + } + + // Draw selection box + if (startSelectionBoxPosition.x >= 0 && startSelectionBoxPosition.y >= 0) + { + GUI.Box(selectionBox, "", (GUIStyle) "SelectionRect"); + forceRepaintCount = 6; + } } public virtual void CalcFlowchartCenter(Flowchart flowchart, Block[] blocks) @@ -436,6 +462,77 @@ namespace Fungus.EditorUtils flowchart.CenterPosition = center; } + protected virtual void HandleSelectionBox(Flowchart flowchart) + { + if (Event.current.button == 0 && Event.current.modifiers != EventModifiers.Alt && + !(UnityEditor.Tools.current == Tool.View && UnityEditor.Tools.viewTool == ViewTool.Pan)) + { + switch (Event.current.type) + { + case EventType.MouseDown: + startSelectionBoxPosition = Event.current.mousePosition; + mouseDownSelectionState = new List(flowchart.SelectedBlocks); + Event.current.Use(); + break; + + case EventType.MouseDrag: + if (startSelectionBoxPosition.x >= 0 && startSelectionBoxPosition.y >= 0) + { + var topLeft = Vector2.Min(startSelectionBoxPosition, Event.current.mousePosition); + var bottomRight = Vector2.Max(startSelectionBoxPosition, Event.current.mousePosition); + selectionBox = Rect.MinMaxRect(topLeft.x, topLeft.y, bottomRight.x, bottomRight.y); + + Rect zoomSelectionBox = selectionBox; + zoomSelectionBox.position -= flowchart.ScrollPos * flowchart.Zoom; + zoomSelectionBox.position /= flowchart.Zoom; + zoomSelectionBox.size /= flowchart.Zoom; + + foreach (var block in flowchart.GetComponents()) + { + if (zoomSelectionBox.Overlaps(block._NodeRect)) + { + if (mouseDownSelectionState.Contains(block)) + { + flowchart.SelectedBlocks.Remove(block); + } + else + { + flowchart.AddSelectedBlock(block); + } + } + else if (mouseDownSelectionState.Contains(block)) + { + flowchart.AddSelectedBlock(block); + } + else + { + flowchart.SelectedBlocks.Remove(block); + } + } + } + Event.current.Use(); + break; + } + + if (Event.current.rawType == EventType.MouseUp) + { + selectionBox.size = Vector2.zero; + selectionBox.position = Vector2.one * -1; + startSelectionBoxPosition = selectionBox.position; + + var tempList = new List(flowchart.SelectedBlocks); + flowchart.SelectedBlocks = mouseDownSelectionState; + Undo.RecordObject(flowchart, "Select"); + flowchart.SelectedBlocks = tempList; + + if (flowchart.SelectedBlock != null) + { + SetBlockForInspector(flowchart, flowchart.SelectedBlock); + } + } + } + } + protected virtual void PanAndZoom(Flowchart flowchart) { // Right click to drag view @@ -482,7 +579,7 @@ namespace Fungus.EditorUtils zoom = true; } - if (zoom) + if (zoom && selectionBox.size == Vector2.zero) { flowchart.Zoom -= Event.current.delta.y * 0.01f; flowchart.Zoom = Mathf.Clamp(flowchart.Zoom, minZoomValue, maxZoomValue); @@ -532,22 +629,18 @@ namespace Fungus.EditorUtils protected virtual void SelectBlock(Flowchart flowchart, Block block) { // Select the block and also select currently executing command - ShowBlockInspector(flowchart); flowchart.SelectedBlock = block; - flowchart.ClearSelectedCommands(); - if (block.ActiveCommand != null) - { - flowchart.AddSelectedCommand(block.ActiveCommand); - } + SetBlockForInspector(flowchart, block); } public static Block CreateBlock(Flowchart flowchart, Vector2 position) { Block newBlock = flowchart.CreateBlock(position); Undo.RegisterCreatedObjectUndo(newBlock, "New Block"); - ShowBlockInspector(flowchart); - flowchart.SelectedBlock = newBlock; - flowchart.ClearSelectedCommands(); + + // Use AddSelected instead of Select for when multiple blocks are duplicated + flowchart.AddSelectedBlock(newBlock); + SetBlockForInspector(flowchart, newBlock); return newBlock; } @@ -583,25 +676,50 @@ namespace Fungus.EditorUtils if (Event.current.button == 0 && Event.current.alt == false) { - dragWindowId = windowId; + if (!GetAppendModifierDown()) + { + dragWindowId = windowId; + + startDragPosition.x = block._NodeRect.x; + startDragPosition.y = block._NodeRect.y; + } - startDragPosition.x = block._NodeRect.x; - startDragPosition.y = block._NodeRect.y; + Event.current.Use(); } if (windowId < windowBlockMap.Count) { Undo.RecordObject(flowchart, "Select"); - SelectBlock(flowchart, block); + if (GetAppendModifierDown()) + { + if (flowchart.SelectedBlocks.Contains(block)) + { + flowchart.SelectedBlocks.Remove(block); + } + else + { + flowchart.AddSelectedBlock(block); + } + } + else + { + if (flowchart.SelectedBlocks.Contains(block)) + { + SetBlockForInspector(flowchart, block); + } + else + { + SelectBlock(flowchart, block); + } + } GUIUtility.keyboardControl = 0; // Fix for textarea not refeshing (change focus) } } bool selected = false; - if (flowchart.SelectedBlock != null && - flowchart.SelectedBlock.Equals(block)) + if (flowchart.SelectedBlocks.Contains(block)) { selected = true; } @@ -670,7 +788,7 @@ namespace Fungus.EditorUtils nodeStyleCopy.normal.background = offTex; GUI.backgroundColor = tintColor; - GUI.Box(GUILayoutUtility.GetLastRect(), block.BlockName, nodeStyleCopy); + GUI.Box(boxRect, block.BlockName, nodeStyleCopy); GUI.backgroundColor = Color.white; @@ -683,10 +801,14 @@ namespace Fungus.EditorUtils if (Event.current.type == EventType.ContextClick) { + flowchart.AddSelectedBlock(block); + GenericMenu menu = new GenericMenu (); - menu.AddItem(new GUIContent ("Duplicate"), false, DuplicateBlock, block); - menu.AddItem(new GUIContent ("Delete"), false, DeleteBlock, block); + // Use a copy because flowchart.SelectedBlocks gets modified + var blockList = new List(flowchart.SelectedBlocks); + menu.AddItem(new GUIContent ("Duplicate"), false, DuplicateBlocks, blockList); + menu.AddItem(new GUIContent ("Delete"), false, DeleteBlocks, blockList); menu.ShowAsContext(); } @@ -804,61 +926,68 @@ namespace Fungus.EditorUtils GUI.Label(dotBRect, "", new GUIStyle("U2D.dragDotActive")); } - public static void DeleteBlock(object obj) + public static void DeleteBlocks(object obj) { - var block = obj as Block; - FlowchartWindow.deleteList.Add(block); + var blocks = obj as List; + blocks.ForEach(block => FlowchartWindow.deleteList.Add(block)); } - protected static void DuplicateBlock(object obj) + protected static void DuplicateBlocks(object obj) { var flowchart = GetFlowchart(); - Block block = obj as Block; - Vector2 newPosition = new Vector2(block._NodeRect.position.x + + Undo.RecordObject(flowchart, "Select"); + flowchart.ClearSelectedBlocks(); + + var blocks = obj as List; + + foreach (var block in blocks) + { + Vector2 newPosition = new Vector2(block._NodeRect.position.x + block._NodeRect.width + 20, block._NodeRect.y); - Block oldBlock = block; + Block oldBlock = block; - Block newBlock = FlowchartWindow.CreateBlock(flowchart, newPosition); - newBlock.BlockName = flowchart.GetUniqueBlockKey(oldBlock.BlockName + " (Copy)"); + Block newBlock = FlowchartWindow.CreateBlock(flowchart, newPosition); + newBlock.BlockName = flowchart.GetUniqueBlockKey(oldBlock.BlockName + " (Copy)"); - Undo.RecordObject(newBlock, "Duplicate Block"); + Undo.RecordObject(newBlock, "Duplicate Block"); - var commandList = oldBlock.CommandList; - foreach (var command in commandList) - { - if (ComponentUtility.CopyComponent(command)) + var commandList = oldBlock.CommandList; + foreach (var command in commandList) { - if (ComponentUtility.PasteComponentAsNew(flowchart.gameObject)) + if (ComponentUtility.CopyComponent(command)) { - Command[] commands = flowchart.GetComponents(); - Command pastedCommand = commands.Last(); - if (pastedCommand != null) + if (ComponentUtility.PasteComponentAsNew(flowchart.gameObject)) { - pastedCommand.ItemId = flowchart.NextItemId(); - newBlock.CommandList.Add(pastedCommand); + Command[] commands = flowchart.GetComponents(); + Command pastedCommand = commands.Last(); + if (pastedCommand != null) + { + pastedCommand.ItemId = flowchart.NextItemId(); + newBlock.CommandList.Add(pastedCommand); + } } + + // This stops the user pasting the command manually into another game object. + ComponentUtility.CopyComponent(flowchart.transform); } - - // This stops the user pasting the command manually into another game object. - ComponentUtility.CopyComponent(flowchart.transform); } - } - if (oldBlock._EventHandler != null) - { - if (ComponentUtility.CopyComponent(oldBlock._EventHandler)) + if (oldBlock._EventHandler != null) { - if (ComponentUtility.PasteComponentAsNew(flowchart.gameObject)) + if (ComponentUtility.CopyComponent(oldBlock._EventHandler)) { - EventHandler[] eventHandlers = flowchart.GetComponents(); - EventHandler pastedEventHandler = eventHandlers.Last(); - if (pastedEventHandler != null) + if (ComponentUtility.PasteComponentAsNew(flowchart.gameObject)) { - pastedEventHandler.ParentBlock = newBlock; - newBlock._EventHandler = pastedEventHandler; + EventHandler[] eventHandlers = flowchart.GetComponents(); + EventHandler pastedEventHandler = eventHandlers.Last(); + if (pastedEventHandler != null) + { + pastedEventHandler.ParentBlock = newBlock; + newBlock._EventHandler = pastedEventHandler; + } } } } @@ -880,6 +1009,16 @@ namespace Fungus.EditorUtils EditorUtility.SetDirty(blockInspector); } + protected static void SetBlockForInspector(Flowchart flowchart, Block block) + { + ShowBlockInspector(flowchart); + flowchart.ClearSelectedCommands(); + if (block.ActiveCommand != null) + { + flowchart.AddSelectedCommand(block.ActiveCommand); + } + } + /// /// Displays a temporary text alert in the center of the Flowchart window. /// @@ -891,5 +1030,10 @@ namespace Fungus.EditorUtils window.ShowNotification(new GUIContent(notificationText)); } } + + protected virtual bool GetAppendModifierDown() + { + return Event.current.shift || EditorGUI.actionKey; + } } } \ No newline at end of file