// Copyright (c) 2012-2023 Wojciech Figat. All rights reserved. using System.Collections.Generic; using System.Linq; using FlaxEditor.Options; using FlaxEditor.Surface.Elements; using FlaxEditor.Surface.Undo; using FlaxEngine; using FlaxEngine.GUI; namespace FlaxEditor.Surface { public partial class VisjectSurface { /// /// The input actions collection to processed during user input. /// public readonly InputActionsContainer InputActions; private string _currentInputText = string.Empty; private Float2 _movingNodesDelta; private HashSet _movingNodes; private readonly Stack _inputBrackets = new Stack(); private class InputBracket { private readonly float DefaultWidth = 120f; private readonly Margin _padding = new Margin(10f); public Box Box { get; } public Float2 EndBracketPosition { get; } public List Nodes { get; } = new List(); public Rectangle Area { get; private set; } public InputBracket(Box box, Float2 nodePosition) { Box = box; EndBracketPosition = nodePosition + new Float2(DefaultWidth, 0); Update(); } public void Update() { Rectangle area; if (Nodes.Count > 0) { area = VisjectSurface.GetNodesBounds(Nodes); } else { area = new Rectangle(EndBracketPosition, new Float2(DefaultWidth, 80f)); } _padding.ExpandRectangle(ref area); var offset = EndBracketPosition - area.UpperRight; area.Location += offset; Area = area; if (!offset.IsZero) { foreach (var node in Nodes) { node.Location += offset; } } } } private string InputText { get => _currentInputText; set { _currentInputText = value; CurrentInputTextChanged(_currentInputText); } } /// /// Occurs when handling custom mouse button down event. /// public event Window.MouseButtonDelegate CustomMouseDown; /// /// Occurs when handling custom mouse button up event. /// public event Window.MouseButtonDelegate CustomMouseUp; /// /// Occurs when handling custom mouse double click event. /// public event Window.MouseButtonDelegate CustomMouseDoubleClick; /// /// Occurs when handling custom mouse move event. /// public event Window.MouseMoveDelegate CustomMouseMove; /// /// Occurs when handling custom mouse wheel event. /// public event Window.MouseWheelDelegate CustomMouseWheel; /// /// Gets the control under the mouse location. /// /// The control or null if no intersection. public SurfaceControl GetControlUnderMouse() { var pos = _rootControl.PointFromParent(ref _mousePos); return _rootControl.GetChildAt(pos) as SurfaceControl; } private void UpdateSelectionRectangle() { var p1 = _rootControl.PointFromParent(ref _leftMouseDownPos); var p2 = _rootControl.PointFromParent(ref _mousePos); var selectionRect = Rectangle.FromPoints(p1, p2); // Find controls to select for (int i = 0; i < _rootControl.Children.Count; i++) { if (_rootControl.Children[i] is SurfaceControl control) { control.IsSelected = control.IsSelectionIntersecting(ref selectionRect); } } } private void OnSurfaceControlSpawned(SurfaceControl control) { if (_inputBrackets.Count > 0 && control is SurfaceNode node) { _inputBrackets.Peek().Nodes.Add(node); _inputBrackets.Peek().Update(); } } private void OnSurfaceControlDeleted(SurfaceControl control) { if (_inputBrackets.Count > 0 && control is SurfaceNode node) { _inputBrackets.Peek().Nodes.Remove(node); _inputBrackets.Peek().Update(); } } private void OnGetNodesToMove() { if (_movingNodes == null) _movingNodes = new HashSet(); else _movingNodes.Clear(); if (CanEdit) { _isMovingSelection = true; for (int i = 0; i < _rootControl.Children.Count; i++) { if (_rootControl.Children[i] is SurfaceNode node && node.IsSelected && (node.Archetype.Flags & NodeFlags.NoMove) != NodeFlags.NoMove) { _movingNodes.Add(node); // Move nodes inside the comment if (node is SurfaceComment comment) { var commentBounds = comment.Bounds; for (int j = 0; j < _rootControl.Children.Count; j++) { if (_rootControl.Children[j] is SurfaceNode childNode && commentBounds.Contains(childNode.Bounds)) { _movingNodes.Add(childNode); } } } } } } } /// public override void OnMouseEnter(Float2 location) { _lastInstigatorUnderMouse = null; // Cache mouse location _mousePos = location; base.OnMouseEnter(location); } /// public override void OnMouseMove(Float2 location) { _lastInstigatorUnderMouse = null; // Cache mouse location _mousePos = location; // Moving around surface with mouse if (_rightMouseDown) { // Calculate delta var delta = location - _rightMouseDownPos; if (delta.LengthSquared > 0.01f) { // Move view _mouseMoveAmount += delta.Length; _rootControl.Location += delta; _rightMouseDownPos = location; Cursor = CursorType.SizeAll; } // Handled return; } if (_middleMouseDown) { // Calculate delta var delta = location - _middleMouseDownPos; if (delta.LengthSquared > 0.01f) { // Move view _mouseMoveAmount += delta.Length; _rootControl.Location += delta; _middleMouseDownPos = location; Cursor = CursorType.SizeAll; } // Handled return; } // Check if user is selecting or moving node(s) if (_leftMouseDown) { // Connecting if (_connectionInstigator != null) { } // Moving else if (_isMovingSelection) { // Calculate delta (apply view offset) var viewDelta = _rootControl.Location - _movingSelectionViewPos; _movingSelectionViewPos = _rootControl.Location; var delta = location - _leftMouseDownPos - viewDelta; if (delta.LengthSquared > 0.01f) { // Move selected nodes delta /= _targetScale; foreach (var node in _movingNodes) node.Location += delta; _leftMouseDownPos = location; _movingNodesDelta += delta; Cursor = CursorType.SizeAll; MarkAsEdited(false); } // Handled return; } // Selecting else { UpdateSelectionRectangle(); // Handled return; } } base.OnMouseMove(location); CustomMouseMove?.Invoke(ref location); } /// public override void OnLostFocus() { // Clear flags and state if (_leftMouseDown) { _leftMouseDown = false; } if (_rightMouseDown) { _rightMouseDown = false; Cursor = CursorType.Default; } if (_middleMouseDown) { _middleMouseDown = false; Cursor = CursorType.Default; } _isMovingSelection = false; ConnectingEnd(null); base.OnLostFocus(); } /// public override bool OnMouseWheel(Float2 location, float delta) { // Base bool handled = base.OnMouseWheel(location, delta); if (!handled) CustomMouseWheel?.Invoke(ref location, delta, ref handled); if (handled) { return true; } // Change scale (disable scaling during selecting nodes) if (IsMouseOver && !_leftMouseDown && !IsPrimaryMenuOpened) { var nextViewScale = ViewScale + delta * 0.1f; if (delta > 0 && !_rightMouseDown) { // Scale towards mouse when zooming in var nextCenterPosition = ViewPosition + location / ViewScale; ViewScale = nextViewScale; ViewPosition = nextCenterPosition - (location / ViewScale); } else { // Scale while keeping center position when zooming out or when dragging view var viewCenter = ViewCenterPosition; ViewScale = nextViewScale; ViewCenterPosition = viewCenter; } return true; } return false; } /// public override bool OnMouseDoubleClick(Float2 location, MouseButton button) { // Base bool handled = base.OnMouseDoubleClick(location, button); if (!handled) CustomMouseDoubleClick?.Invoke(ref location, button, ref handled); if (!handled && CanEdit) { var mousePos = _rootControl.PointFromParent(ref _mousePos); if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox) && GetControlUnderMouse() == null) { // Insert reroute node if (Undo != null) { bool undoEnabled = Undo.Enabled; Undo.Enabled = false; var rerouteNode = Context.SpawnNode(7, 29, mousePos); Undo.Enabled = undoEnabled; var spawnNodeAction = new AddRemoveNodeAction(rerouteNode, true); var disconnectBoxesAction = new ConnectBoxesAction(inputBox, outputBox, false); inputBox.BreakConnection(outputBox); disconnectBoxesAction.End(); var addConnectionsAction = new EditNodeConnections(Context, rerouteNode); outputBox.CreateConnection(rerouteNode.GetBoxes().First(b => !b.IsOutput)); rerouteNode.GetBoxes().First(b => b.IsOutput).CreateConnection(inputBox); addConnectionsAction.End(); Undo.AddAction(new MultiUndoAction(spawnNodeAction, disconnectBoxesAction, addConnectionsAction)); } else { var rerouteNode = Context.SpawnNode(7, 29, mousePos); inputBox.BreakConnection(outputBox); outputBox.CreateConnection(rerouteNode.GetBoxes().First(b => !b.IsOutput)); rerouteNode.GetBoxes().First(b => b.IsOutput).CreateConnection(inputBox); } MarkAsEdited(); handled = true; } } return handled; } /// public override bool OnMouseDown(Float2 location, MouseButton button) { // Check if user is connecting boxes if (_connectionInstigator != null) return true; // Base bool handled = base.OnMouseDown(location, button); if (!handled) CustomMouseDown?.Invoke(ref location, button, ref handled); if (handled) { // Clear flags _isMovingSelection = false; _rightMouseDown = false; _leftMouseDown = false; _middleMouseDown = false; return true; } // Just reset the input text whenever the user presses anywhere ResetInput(); // Cache data _isMovingSelection = false; _mousePos = location; if (button == MouseButton.Left) { _leftMouseDown = true; _leftMouseDownPos = location; } if (button == MouseButton.Right) { _rightMouseDown = true; _rightMouseDownPos = location; } if (button == MouseButton.Middle) { _middleMouseDown = true; _middleMouseDownPos = location; } // Check if any node is under the mouse SurfaceControl controlUnderMouse = GetControlUnderMouse(); var cLocation = _rootControl.PointFromParent(ref location); if (controlUnderMouse != null) { // Check if mouse is over header and user is pressing mouse left button if (_leftMouseDown && controlUnderMouse.CanSelect(ref cLocation)) { // Check if user is pressing control if (Root.GetKey(KeyboardKeys.Control)) { // Add to selection if (!controlUnderMouse.IsSelected) { AddToSelection(controlUnderMouse); } } // Check if node isn't selected else if (!controlUnderMouse.IsSelected) { // Select node Select(controlUnderMouse); } // Start moving selected nodes StartMouseCapture(); _movingSelectionViewPos = _rootControl.Location; _movingNodesDelta = Float2.Zero; OnGetNodesToMove(); Focus(); return true; } } else { // Cache flags and state if (_leftMouseDown) { // Start selecting or commenting StartMouseCapture(); ClearSelection(); Focus(); return true; } if (_rightMouseDown || _middleMouseDown) { // Start navigating StartMouseCapture(); Focus(); return true; } } Focus(); return true; } /// public override bool OnMouseUp(Float2 location, MouseButton button) { // Cache mouse location _mousePos = location; // Check if any control is under the mouse SurfaceControl controlUnderMouse = GetControlUnderMouse(); // Cache flags and state if (_leftMouseDown && button == MouseButton.Left) { _leftMouseDown = false; EndMouseCapture(); Cursor = CursorType.Default; // Moving nodes if (_isMovingSelection) { if (_movingNodes != null && _movingNodes.Count > 0) { if (Undo != null && !_movingNodesDelta.IsZero && CanEdit) Undo.AddAction(new MoveNodesAction(Context, _movingNodes.Select(x => x.ID).ToArray(), _movingNodesDelta)); _movingNodes.Clear(); } _movingNodesDelta = Float2.Zero; } // Connecting else if (_connectionInstigator != null) { } // Selecting else { UpdateSelectionRectangle(); } } if (_rightMouseDown && button == MouseButton.Right) { _rightMouseDown = false; EndMouseCapture(); Cursor = CursorType.Default; // Check if no move has been made at all if (_mouseMoveAmount < 3.0f) { // Check if any control is under the mouse _cmStartPos = location; if (controlUnderMouse == null) { // Show primary context menu ShowPrimaryMenu(_cmStartPos); } } _mouseMoveAmount = 0; } if (_middleMouseDown && button == MouseButton.Middle) { _middleMouseDown = false; EndMouseCapture(); Cursor = CursorType.Default; _mouseMoveAmount = 0; } // Base bool handled = base.OnMouseUp(location, button); if (!handled) CustomMouseUp?.Invoke(ref location, button, ref handled); if (handled) { // Clear flags _rightMouseDown = false; _leftMouseDown = false; _middleMouseDown = false; return true; } // Letting go of a connection or right clicking while creating a connection if (!_isMovingSelection && _connectionInstigator != null && !IsPrimaryMenuOpened) { _cmStartPos = location; Cursor = CursorType.Default; EndMouseCapture(); ShowPrimaryMenu(_cmStartPos); } return true; } /// public override bool OnCharInput(char c) { if (base.OnCharInput(c)) return true; InputText += c; return true; } private void MoveSelectedNodes(Float2 delta) { // TODO: undo delta /= _targetScale; OnGetNodesToMove(); foreach (var node in _movingNodes) node.Location += delta; _isMovingSelection = false; MarkAsEdited(false); } /// public override bool OnKeyDown(KeyboardKeys key) { if (base.OnKeyDown(key)) return true; if (InputActions.Process(Editor.Instance, this, key)) return true; if (HasNodesSelection) { var keyMoveRange = 50; switch (key) { case KeyboardKeys.Backspace: if (InputText.Length > 0) InputText = InputText.Substring(0, InputText.Length - 1); return true; case KeyboardKeys.Escape: ClearSelection(); return true; case KeyboardKeys.Return: { Box selectedBox = GetSelectedBox(SelectedNodes, false); Box toSelect = selectedBox?.ParentNode.GetNextBox(selectedBox); if (toSelect != null) { Select(toSelect.ParentNode); toSelect.ParentNode.SelectBox(toSelect); } return true; } case KeyboardKeys.ArrowUp: case KeyboardKeys.ArrowDown: { // Selected box navigation Box selectedBox = GetSelectedBox(SelectedNodes); if (selectedBox != null) { Box toSelect = (key == KeyboardKeys.ArrowUp) ? selectedBox?.ParentNode.GetPreviousBox(selectedBox) : selectedBox?.ParentNode.GetNextBox(selectedBox); if (toSelect != null && toSelect.IsOutput == selectedBox.IsOutput) { Select(toSelect.ParentNode); toSelect.ParentNode.SelectBox(toSelect); } } else if (!IsMovingSelection && CanEdit) { // Move selected nodes var delta = new Float2(0, key == KeyboardKeys.ArrowUp ? -keyMoveRange : keyMoveRange); MoveSelectedNodes(delta); } return true; } case KeyboardKeys.ArrowRight: case KeyboardKeys.ArrowLeft: { // Selected box navigation Box selectedBox = GetSelectedBox(SelectedNodes); if (selectedBox != null) { Box toSelect = null; if ((key == KeyboardKeys.ArrowRight && selectedBox.IsOutput) || (key == KeyboardKeys.ArrowLeft && !selectedBox.IsOutput)) { if (_selectedConnectionIndex < 0 || _selectedConnectionIndex >= selectedBox.Connections.Count) { _selectedConnectionIndex = 0; } toSelect = selectedBox.Connections[_selectedConnectionIndex]; } else { // Use the node with the closest Y-level // Since there are cases like 3 nodes on one side and only 1 node on the other side var elements = selectedBox.ParentNode.Elements; float minDistance = float.PositiveInfinity; for (int i = 0; i < elements.Count; i++) { if (elements[i] is Box box && box.IsOutput != selectedBox.IsOutput && Mathf.Abs(box.Y - selectedBox.Y) < minDistance) { toSelect = box; minDistance = Mathf.Abs(box.Y - selectedBox.Y); } } } if (toSelect != null) { Select(toSelect.ParentNode); toSelect.ParentNode.SelectBox(toSelect); } } else if (!IsMovingSelection && CanEdit) { // Move selected nodes var delta = new Float2(key == KeyboardKeys.ArrowLeft ? -keyMoveRange : keyMoveRange, 0); MoveSelectedNodes(delta); } return true; } case KeyboardKeys.Tab: { Box selectedBox = GetSelectedBox(SelectedNodes, false); if (selectedBox == null) return true; int connectionCount = selectedBox.Connections.Count; if (connectionCount == 0) return true; if (Root.GetKey(KeyboardKeys.Shift)) { _selectedConnectionIndex = ((_selectedConnectionIndex - 1) % connectionCount + connectionCount) % connectionCount; } else { _selectedConnectionIndex = (_selectedConnectionIndex + 1) % connectionCount; } return true; } } } return false; } private void ResetInput() { InputText = ""; _inputBrackets.Clear(); } private void CurrentInputTextChanged(string currentInputText) { if (string.IsNullOrEmpty(currentInputText)) return; if (IsPrimaryMenuOpened || !CanEdit) { InputText = ""; return; } var selection = SelectedNodes; if (selection.Count == 0) { if (_inputBrackets.Count == 0) { if (currentInputText.StartsWith(' ')) currentInputText = ""; ResetInput(); ShowPrimaryMenu(_mousePos, false, currentInputText); } else { InputText = ""; ShowPrimaryMenu(_rootControl.PointToParent(_inputBrackets.Peek().Area.Location), true, currentInputText); } return; } // Multi-Node Editing const string Comment = "//"; if (currentInputText.StartsWith(Comment)) { InputText = ""; var comment = CommentSelection(currentInputText.Substring(Comment.Length)); comment.StartRenaming(); return; } // TODO: What should happen when multiple nodes or multiple boxes are selected? if (selection.Count != 1) return; // Single Box Editing Box selectedBox = GetSelectedBox(selection, false); if (selectedBox == null) return; // TODO: Editing a primitive /* # => color * 1,43 => Vector2 * = => set node name * etc. */ if (currentInputText.StartsWith("(")) { InputText = InputText.Substring(1); // Opening bracket if (!selectedBox.IsOutput) { var bracket = new InputBracket(selectedBox, FindEmptySpace(selectedBox)); _inputBrackets.Push(bracket); Deselect(selectedBox.ParentNode); } } else if (currentInputText.StartsWith(")")) { InputText = InputText.Substring(1); // Closing bracket if (_inputBrackets.Count > 0) { var bracket = _inputBrackets.Pop(); bracket.Update(); if (selectedBox.IsOutput) { TryConnect(selectedBox, bracket.Box); } } } else { InputText = ""; // Add a new node ConnectingStart(selectedBox); Cursor = CursorType.Default; // Do I need this? EndMouseCapture(); ShowPrimaryMenu(_rootControl.PointToParent(FindEmptySpace(selectedBox)), true, currentInputText); } } private Box GetSelectedBox(List selection, bool onlyIfSelected = true) { if (selection.Count != 1) return null; // TODO: Handle multiple selected nodes // Get selected box SurfaceNode selectedNode = selection[0]; Box selectedBox = null; for (int i = 0; i < selectedNode.Elements.Count; i++) { if (selectedNode.Elements[i] is Box box && box.IsSelected) { if (selectedBox == null) { selectedBox = box; } else { // TODO: Multiple boxes are selected. How should this be handled? return null; } } } // Or get the first output box when a node with only output boxes is selected if (selectedBox == null && !onlyIfSelected) { for (int i = 0; i < selectedNode.Elements.Count; i++) { if (selectedNode.Elements[i] is Box box) { if (box.IsOutput) { selectedBox = box; break; } } } for (int i = 0; i < selectedNode.Elements.Count; i++) { if (selectedNode.Elements[i] is Box box) { if (!box.IsOutput) { selectedBox = null; break; } } } } return selectedBox; } private Float2 FindEmptySpace(Box box) { var distanceBetweenNodes = new Float2(30, 30); var node = box.ParentNode; // Same height as node float yLocation = node.Top; for (int i = 0; i < node.Elements.Count; i++) { if (node.Elements[i] is Box nodeBox && nodeBox.IsOutput == box.IsOutput && nodeBox.Y < box.Y) { // Below connected node yLocation = Mathf.Max(yLocation, nodeBox.ParentNode.Bottom + distanceBetweenNodes.Y); } } // TODO: Dodge the other nodes float xLocation = node.Location.X; if (box.IsOutput) { xLocation += node.Width + distanceBetweenNodes.X; } else { xLocation += -120 - distanceBetweenNodes.X; } return new Float2(xLocation, yLocation); } private bool IntersectsConnection(Float2 mousePosition, out InputBox inputBox, out OutputBox outputBox) { for (int i = 0; i < Nodes.Count; i++) { for (int j = 0; j < Nodes[i].Elements.Count; j++) { if (Nodes[i].Elements[j] is OutputBox ob) { for (int k = 0; k < ob.Connections.Count; k++) { if (ob.IntersectsConnection(ob.Connections[k], ref mousePosition)) { outputBox = ob; inputBox = ob.Connections[k] as InputBox; return true; } } } } } outputBox = null; inputBox = null; return false; } } }