1057 lines
37 KiB
C#
1057 lines
37 KiB
C#
// Copyright (c) Wojciech Figat. All rights reserved.
|
|
|
|
using System;
|
|
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
|
|
{
|
|
/// <summary>
|
|
/// The input actions collection to processed during user input.
|
|
/// </summary>
|
|
public readonly InputActionsContainer InputActions;
|
|
|
|
/// <summary>
|
|
/// Optional feature.
|
|
/// </summary>
|
|
public bool PanWithMiddleMouse = false;
|
|
|
|
private string _currentInputText = string.Empty;
|
|
private Float2 _movingNodesDelta;
|
|
private Float2 _gridRoundingDelta;
|
|
private HashSet<SurfaceNode> _movingNodes;
|
|
private HashSet<SurfaceNode> _temporarySelectedNodes;
|
|
private readonly Stack<InputBracket> _inputBrackets = new Stack<InputBracket>();
|
|
|
|
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<SurfaceNode> Nodes { get; } = new List<SurfaceNode>();
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Occurs when handling custom mouse button down event.
|
|
/// </summary>
|
|
public event Window.MouseButtonDelegate CustomMouseDown;
|
|
|
|
/// <summary>
|
|
/// Occurs when handling custom mouse button up event.
|
|
/// </summary>
|
|
public event Window.MouseButtonDelegate CustomMouseUp;
|
|
|
|
/// <summary>
|
|
/// Occurs when handling custom mouse double click event.
|
|
/// </summary>
|
|
public event Window.MouseButtonDelegate CustomMouseDoubleClick;
|
|
|
|
/// <summary>
|
|
/// Occurs when handling custom mouse move event.
|
|
/// </summary>
|
|
public event Window.MouseMoveDelegate CustomMouseMove;
|
|
|
|
/// <summary>
|
|
/// Occurs when handling custom mouse wheel event.
|
|
/// </summary>
|
|
public event Window.MouseWheelDelegate CustomMouseWheel;
|
|
|
|
/// <summary>
|
|
/// Gets the control under the mouse location.
|
|
/// </summary>
|
|
/// <returns>The control or null if no intersection.</returns>
|
|
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);
|
|
var selectionChanged = false;
|
|
|
|
// Find controls to select
|
|
for (int i = 0; i < _rootControl.Children.Count; i++)
|
|
{
|
|
if (_rootControl.Children[i] is SurfaceControl control)
|
|
{
|
|
var select = control.IsSelectionIntersecting(ref selectionRect);
|
|
|
|
if (Root.GetKey(KeyboardKeys.Shift))
|
|
{
|
|
if (select == control.IsSelected && _temporarySelectedNodes.Contains(control))
|
|
{
|
|
control.IsSelected = !select;
|
|
selectionChanged = true;
|
|
}
|
|
}
|
|
else if (Root.GetKey(KeyboardKeys.Control))
|
|
{
|
|
if (select != control.IsSelected && !_temporarySelectedNodes.Contains(control))
|
|
{
|
|
control.IsSelected = select;
|
|
selectionChanged = true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (select != control.IsSelected)
|
|
{
|
|
control.IsSelected = select;
|
|
selectionChanged = true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
if (selectionChanged)
|
|
SelectionChanged?.Invoke();
|
|
}
|
|
|
|
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<SurfaceNode>();
|
|
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);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Snaps a coordinate point to the grid.
|
|
/// </summary>
|
|
/// <param name="point">The point to be rounded.</param>
|
|
/// <param name="ceil">Round to ceiling instead?</param>
|
|
/// <returns>Rounded coordinate.</returns>
|
|
public Float2 SnapToGrid(Float2 point, bool ceil = false)
|
|
{
|
|
float gridSize = GridSnappingSize;
|
|
Float2 snapped = point.Absolute / gridSize;
|
|
snapped = ceil ? Float2.Ceil(snapped) : Float2.Floor(snapped);
|
|
snapped.X = (float)Math.CopySign(snapped.X * gridSize, point.X);
|
|
snapped.Y = (float)Math.CopySign(snapped.Y * gridSize, point.Y);
|
|
return snapped;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnMouseEnter(Float2 location)
|
|
{
|
|
_lastInstigatorUnderMouse = null;
|
|
|
|
// Cache mouse location
|
|
_mousePos = location;
|
|
|
|
base.OnMouseEnter(location);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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)
|
|
{
|
|
if (PanWithMiddleMouse)
|
|
{
|
|
// 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)
|
|
{
|
|
var gridSnap = GridSnappingEnabled;
|
|
if (!gridSnap)
|
|
_gridRoundingDelta = Float2.Zero; // Reset in case user toggled option between frames.
|
|
|
|
// Calculate delta (apply view offset)
|
|
var viewDelta = _rootControl.Location - _movingSelectionViewPos;
|
|
_movingSelectionViewPos = _rootControl.Location;
|
|
var delta = location - _leftMouseDownPos - viewDelta + _gridRoundingDelta;
|
|
var deltaLengthSquared = delta.LengthSquared;
|
|
|
|
delta /= _targetScale;
|
|
if ((!gridSnap || Mathf.Abs(delta.X) >= GridSnappingSize || (Mathf.Abs(delta.Y) >= GridSnappingSize))
|
|
&& deltaLengthSquared > 0.01f)
|
|
{
|
|
if (gridSnap)
|
|
{
|
|
Float2 unroundedDelta = delta;
|
|
delta = SnapToGrid(unroundedDelta);
|
|
_gridRoundingDelta = (unroundedDelta - delta) * _targetScale; // Standardize unit of the rounding delta, in case user zooms between node movements.
|
|
}
|
|
|
|
foreach (var node in _movingNodes)
|
|
{
|
|
if (gridSnap)
|
|
{
|
|
Float2 unroundedLocation = node.Location;
|
|
node.Location = SnapToGrid(unroundedLocation);
|
|
}
|
|
node.Location += delta;
|
|
}
|
|
|
|
_leftMouseDownPos = location;
|
|
_movingNodesDelta += delta; // TODO: Figure out how to handle undo for differing values of _gridRoundingDelta between selected nodes. For now it will be a small error in undo.
|
|
if (_movingNodes.Count > 0)
|
|
{
|
|
Cursor = CursorType.SizeAll;
|
|
MarkAsEdited(false);
|
|
}
|
|
}
|
|
|
|
// Handled
|
|
return;
|
|
}
|
|
// Selecting
|
|
else
|
|
{
|
|
UpdateSelectionRectangle();
|
|
|
|
// Handled
|
|
return;
|
|
}
|
|
}
|
|
|
|
base.OnMouseMove(location);
|
|
CustomMouseMove?.Invoke(ref location);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override void OnLostFocus()
|
|
{
|
|
// Clear flags and state
|
|
if (_leftMouseDown)
|
|
{
|
|
_leftMouseDown = false;
|
|
}
|
|
if (_rightMouseDown)
|
|
{
|
|
_rightMouseDown = false;
|
|
Cursor = CursorType.Default;
|
|
}
|
|
if (_middleMouseDown)
|
|
{
|
|
_middleMouseDown = false;
|
|
if (PanWithMiddleMouse)
|
|
Cursor = CursorType.Default;
|
|
}
|
|
_isMovingSelection = false;
|
|
ConnectingEnd(null);
|
|
|
|
base.OnLostFocus();
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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 && !_rightMouseDown && !IsPrimaryMenuOpened)
|
|
{
|
|
var nextViewScale = ViewScale + delta * 0.1f;
|
|
|
|
// Scale towards/ away from mouse when zooming in/ out
|
|
var nextCenterPosition = ViewPosition + location / ViewScale;
|
|
ViewScale = nextViewScale;
|
|
ViewPosition = nextCenterPosition - (location / ViewScale);
|
|
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
public override bool OnMouseDoubleClick(Float2 location, MouseButton button)
|
|
{
|
|
// Base
|
|
bool handled = base.OnMouseDoubleClick(location, button);
|
|
if (!handled)
|
|
CustomMouseDoubleClick?.Invoke(ref location, button, ref handled);
|
|
|
|
// Insert reroute node
|
|
if (!handled && CanEdit && CanUseNodeType(7, 29))
|
|
{
|
|
var mousePos = _rootControl.PointFromParent(ref _mousePos);
|
|
if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox) && GetControlUnderMouse() == null)
|
|
{
|
|
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;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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(_temporarySelectedNodes == null)
|
|
_temporarySelectedNodes = new HashSet<SurfaceNode>();
|
|
else
|
|
_temporarySelectedNodes.Clear();
|
|
|
|
for (int i = 0; i < _rootControl.Children.Count; i++)
|
|
{
|
|
if (_rootControl.Children[i] is SurfaceNode node && node.IsSelected)
|
|
{
|
|
_temporarySelectedNodes.Add(node);
|
|
}
|
|
}
|
|
|
|
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))
|
|
{
|
|
AddToSelection(controlUnderMouse);
|
|
}
|
|
else if (Root.GetKey(KeyboardKeys.Shift))
|
|
{
|
|
RemoveFromSelection(controlUnderMouse);
|
|
}
|
|
// Check if node isn't selected
|
|
else if (!controlUnderMouse.IsSelected)
|
|
{
|
|
// Select node
|
|
Select(controlUnderMouse);
|
|
}
|
|
|
|
// Start moving selected nodes
|
|
if (!Root.GetKey(KeyboardKeys.Shift))
|
|
{
|
|
StartMouseCapture();
|
|
_movingSelectionViewPos = _rootControl.Location;
|
|
_movingNodesDelta = Float2.Zero;
|
|
OnGetNodesToMove();
|
|
}
|
|
|
|
Focus();
|
|
return true;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// Cache flags and state
|
|
if (_leftMouseDown)
|
|
{
|
|
// Start selecting or commenting
|
|
StartMouseCapture();
|
|
|
|
if (!Root.GetKey(KeyboardKeys.Control) && !Root.GetKey(KeyboardKeys.Shift))
|
|
{
|
|
ClearSelection();
|
|
}
|
|
|
|
Focus();
|
|
return true;
|
|
}
|
|
if (_rightMouseDown || _middleMouseDown)
|
|
{
|
|
// Start navigating
|
|
StartMouseCapture();
|
|
Focus();
|
|
return true;
|
|
}
|
|
}
|
|
|
|
Focus();
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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)
|
|
{
|
|
WasBoxSelecting = IsBoxSelecting;
|
|
WasMovingSelection = _isMovingSelection;
|
|
|
|
_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();
|
|
}
|
|
}
|
|
bool showPrimaryMenu = false;
|
|
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)
|
|
{
|
|
showPrimaryMenu = true;
|
|
}
|
|
}
|
|
_mouseMoveAmount = 0;
|
|
}
|
|
if (_middleMouseDown && button == MouseButton.Middle)
|
|
{
|
|
_middleMouseDown = false;
|
|
if (_middleMouseDown)
|
|
{
|
|
EndMouseCapture();
|
|
Cursor = CursorType.Default;
|
|
}
|
|
if (_mouseMoveAmount > 0 && _middleMouseDown)
|
|
_mouseMoveAmount = 0;
|
|
else if (CanEdit)
|
|
{
|
|
// Surface was not moved with MMB so try to remove connection underneath
|
|
var mousePos = _rootControl.PointFromParent(ref location);
|
|
if (IntersectsConnection(mousePos, out InputBox inputBox, out OutputBox outputBox))
|
|
{
|
|
var action = new EditNodeConnections(inputBox.ParentNode.Context, inputBox.ParentNode);
|
|
inputBox.BreakConnection(outputBox);
|
|
action.End();
|
|
AddBatchedUndoAction(action);
|
|
MarkAsEdited();
|
|
}
|
|
}
|
|
}
|
|
|
|
// 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;
|
|
}
|
|
|
|
// If none of the child controls handled this show the primary context menu
|
|
if (showPrimaryMenu)
|
|
{
|
|
ShowPrimaryMenu(_cmStartPos);
|
|
}
|
|
// Letting go of a connection or right clicking while creating a connection
|
|
else if (!_isMovingSelection && _connectionInstigator != null && !IsPrimaryMenuOpened)
|
|
{
|
|
_cmStartPos = location;
|
|
Cursor = CursorType.Default;
|
|
EndMouseCapture();
|
|
ShowPrimaryMenu(_cmStartPos);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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);
|
|
}
|
|
|
|
/// <inheritdoc />
|
|
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<SurfaceNode> 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;
|
|
}
|
|
}
|
|
}
|