633 lines
18 KiB
C#
633 lines
18 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using UnityEditor;
|
|
using UnityEngine;
|
|
|
|
namespace Unity.VisualScripting
|
|
{
|
|
public abstract class StateWidget<TState> : NodeWidget<StateCanvas, TState>, IStateWidget
|
|
where TState : class, IState
|
|
{
|
|
protected StateWidget(StateCanvas canvas, TState state) : base(canvas, state)
|
|
{
|
|
minResizeSize = new Vector2(State.DefaultWidth, 0);
|
|
}
|
|
|
|
public virtual bool canForceEnter => true;
|
|
|
|
public virtual bool canForceExit => true;
|
|
|
|
public virtual bool canToggleStart => true;
|
|
|
|
|
|
#region Model
|
|
|
|
protected TState state => element;
|
|
|
|
protected IStateDebugData stateDebugData => GetDebugData<IStateDebugData>();
|
|
|
|
protected State.Data stateData => reference.hasData ? reference.GetElementData<State.Data>(state) : null;
|
|
|
|
IState IStateWidget.state => state;
|
|
|
|
protected StateDescription description { get; private set; }
|
|
|
|
protected StateAnalysis analysis => state.Analysis<StateAnalysis>(context);
|
|
|
|
protected override void CacheDescription()
|
|
{
|
|
description = state.Description<StateDescription>();
|
|
|
|
title = description.title;
|
|
summary = description.summary;
|
|
|
|
titleContent.text = " " + title;
|
|
titleContent.image = description.icon?[IconSize.Small];
|
|
summaryContent.text = summary;
|
|
|
|
Reposition();
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Lifecycle
|
|
|
|
public override void BeforeFrame()
|
|
{
|
|
base.BeforeFrame();
|
|
|
|
if (currentContentOuterHeight != targetContentOuterHeight)
|
|
{
|
|
Reposition();
|
|
}
|
|
}
|
|
|
|
public override void HandleInput()
|
|
{
|
|
if (e.IsMouseDrag(MouseButton.Left) &&
|
|
e.ctrlOrCmd &&
|
|
!canvas.isCreatingTransition)
|
|
{
|
|
if (state.canBeSource)
|
|
{
|
|
canvas.StartTransition(state);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("Cannot create a transition from this state.\n");
|
|
}
|
|
|
|
e.Use();
|
|
}
|
|
else if (e.IsMouseDrag(MouseButton.Left) && canvas.isCreatingTransition)
|
|
{
|
|
e.Use();
|
|
}
|
|
else if (e.IsMouseUp(MouseButton.Left) && canvas.isCreatingTransition)
|
|
{
|
|
var source = canvas.transitionSource;
|
|
var destination = (canvas.hoveredWidget as IStateWidget)?.state;
|
|
|
|
if (destination == null)
|
|
{
|
|
canvas.CompleteTransitionToNewState();
|
|
}
|
|
else if (destination == source)
|
|
{
|
|
canvas.CancelTransition();
|
|
}
|
|
else if (destination.canBeDestination)
|
|
{
|
|
canvas.EndTransition(destination);
|
|
}
|
|
else
|
|
{
|
|
Debug.LogWarning("Cannot create a transition to this state.\n");
|
|
canvas.CancelTransition();
|
|
}
|
|
|
|
e.Use();
|
|
}
|
|
|
|
base.HandleInput();
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Contents
|
|
|
|
protected virtual string title { get; set; }
|
|
|
|
protected virtual string summary { get; set; }
|
|
|
|
private GUIContent titleContent { get; } = new GUIContent();
|
|
|
|
private GUIContent summaryContent { get; } = new GUIContent();
|
|
|
|
#endregion
|
|
|
|
|
|
#region Positioning
|
|
|
|
public override IEnumerable<IWidget> positionDependers => state.transitions.Select(transition => (IWidget)canvas.Widget(transition));
|
|
|
|
public Rect titlePosition { get; private set; }
|
|
|
|
public Rect summaryPosition { get; private set; }
|
|
|
|
public Rect contentOuterPosition { get; private set; }
|
|
|
|
public Rect contentBackgroundPosition { get; private set; }
|
|
|
|
public Rect contentInnerPosition { get; private set; }
|
|
|
|
private float targetContentOuterHeight;
|
|
|
|
private float currentContentOuterHeight;
|
|
|
|
private bool revealInitialized;
|
|
|
|
private Rect _position;
|
|
|
|
public override Rect position
|
|
{
|
|
get { return _position; }
|
|
set
|
|
{
|
|
state.position = value.position;
|
|
state.width = value.width;
|
|
}
|
|
}
|
|
|
|
public override void CachePosition()
|
|
{
|
|
var edgeOrigin = state.position;
|
|
var edgeX = edgeOrigin.x;
|
|
var edgeY = edgeOrigin.y;
|
|
var edgeWidth = state.width;
|
|
var innerOrigin = EdgeToInnerPosition(new Rect(edgeOrigin, Vector2.zero)).position;
|
|
var innerX = innerOrigin.x;
|
|
var innerY = innerOrigin.y;
|
|
var innerWidth = EdgeToInnerPosition(new Rect(0, 0, edgeWidth, 0)).width;
|
|
var innerHeight = 0f;
|
|
|
|
var y = innerY;
|
|
|
|
if (showTitle)
|
|
{
|
|
using (LudiqGUIUtility.iconSize.Override(IconSize.Small))
|
|
{
|
|
titlePosition = new Rect
|
|
(
|
|
innerX,
|
|
y,
|
|
innerWidth,
|
|
Styles.title.CalcHeight(titleContent, innerWidth)
|
|
);
|
|
|
|
y += titlePosition.height;
|
|
innerHeight += titlePosition.height;
|
|
}
|
|
}
|
|
|
|
if (showTitle && showSummary)
|
|
{
|
|
y += Styles.spaceBetweenTitleAndSummary;
|
|
innerHeight += Styles.spaceBetweenTitleAndSummary;
|
|
}
|
|
|
|
if (showSummary)
|
|
{
|
|
summaryPosition = new Rect
|
|
(
|
|
innerX,
|
|
y,
|
|
innerWidth,
|
|
Styles.summary.CalcHeight(summaryContent, innerWidth)
|
|
);
|
|
|
|
y += summaryPosition.height;
|
|
innerHeight += summaryPosition.height;
|
|
}
|
|
|
|
if (showContent)
|
|
{
|
|
var contentInnerWidth = edgeWidth - Styles.contentBackground.padding.left - Styles.contentBackground.padding.right;
|
|
|
|
targetContentOuterHeight = revealContent ? (Styles.spaceBeforeContent + Styles.contentBackground.padding.top + GetContentHeight(contentInnerWidth) + Styles.contentBackground.padding.bottom) : 0;
|
|
|
|
if (!revealInitialized)
|
|
{
|
|
currentContentOuterHeight = targetContentOuterHeight;
|
|
revealInitialized = true;
|
|
}
|
|
|
|
currentContentOuterHeight = Mathf.Lerp(currentContentOuterHeight, targetContentOuterHeight, canvas.repaintDeltaTime * Styles.contentRevealSpeed);
|
|
|
|
if (Mathf.Abs(targetContentOuterHeight - currentContentOuterHeight) < 1)
|
|
{
|
|
currentContentOuterHeight = targetContentOuterHeight;
|
|
}
|
|
|
|
contentOuterPosition = new Rect
|
|
(
|
|
edgeX,
|
|
y,
|
|
edgeWidth,
|
|
currentContentOuterHeight
|
|
);
|
|
|
|
contentBackgroundPosition = new Rect
|
|
(
|
|
0,
|
|
Styles.spaceBeforeContent,
|
|
edgeWidth,
|
|
currentContentOuterHeight - Styles.spaceBeforeContent
|
|
);
|
|
|
|
contentInnerPosition = new Rect
|
|
(
|
|
Styles.contentBackground.padding.left,
|
|
Styles.spaceBeforeContent + Styles.contentBackground.padding.top,
|
|
contentInnerWidth,
|
|
contentBackgroundPosition.height - Styles.contentBackground.padding.top
|
|
);
|
|
|
|
y += contentOuterPosition.height;
|
|
innerHeight += contentOuterPosition.height;
|
|
}
|
|
|
|
var edgeHeight = InnerToEdgePosition(new Rect(0, 0, 0, innerHeight)).height;
|
|
|
|
_position = new Rect
|
|
(
|
|
edgeX,
|
|
edgeY,
|
|
edgeWidth,
|
|
edgeHeight
|
|
);
|
|
}
|
|
|
|
protected virtual float GetContentHeight(float width) => 0;
|
|
|
|
#endregion
|
|
|
|
|
|
#region Drawing
|
|
|
|
protected virtual bool showTitle => true;
|
|
|
|
protected virtual bool showSummary => !StringUtility.IsNullOrWhiteSpace(summary);
|
|
|
|
protected virtual bool showContent => false;
|
|
|
|
protected virtual NodeColorMix baseColor => NodeColor.Gray;
|
|
|
|
protected override NodeColorMix color
|
|
{
|
|
get
|
|
{
|
|
if (stateDebugData.runtimeException != null)
|
|
{
|
|
return NodeColor.Red;
|
|
}
|
|
|
|
var color = baseColor;
|
|
|
|
if (state.isStart)
|
|
{
|
|
color = NodeColor.Green;
|
|
}
|
|
|
|
if (stateData?.isActive ?? false)
|
|
{
|
|
color = NodeColor.Blue;
|
|
}
|
|
else if (EditorApplication.isPaused)
|
|
{
|
|
if (EditorTimeBinding.frame == stateDebugData.lastEnterFrame)
|
|
{
|
|
color = NodeColor.Blue;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
color.blue = Mathf.Lerp(1, 0, (EditorTimeBinding.time - stateDebugData.lastExitTime) / Styles.enterFadeDuration);
|
|
}
|
|
|
|
return color;
|
|
}
|
|
}
|
|
|
|
protected override NodeShape shape => NodeShape.Square;
|
|
|
|
private bool revealContent
|
|
{
|
|
get
|
|
{
|
|
switch (BoltState.Configuration.statesReveal)
|
|
{
|
|
case StateRevealCondition.Always:
|
|
|
|
return true;
|
|
case StateRevealCondition.Never:
|
|
|
|
return false;
|
|
case StateRevealCondition.OnHover:
|
|
|
|
return isMouseOver;
|
|
case StateRevealCondition.OnHoverWithAlt:
|
|
|
|
return isMouseOver && e.alt;
|
|
case StateRevealCondition.WhenSelected:
|
|
|
|
return selection.Contains(state);
|
|
case StateRevealCondition.OnHoverOrSelected:
|
|
|
|
return isMouseOver || selection.Contains(state);
|
|
case StateRevealCondition.OnHoverWithAltOrSelected:
|
|
|
|
return isMouseOver && e.alt || selection.Contains(state);
|
|
default:
|
|
|
|
throw new UnexpectedEnumValueException<StateRevealCondition>(BoltState.Configuration.statesReveal);
|
|
}
|
|
}
|
|
}
|
|
|
|
private bool revealedContent;
|
|
|
|
private void CheckReveal()
|
|
{
|
|
var revealContent = this.revealContent;
|
|
|
|
if (revealContent != revealedContent)
|
|
{
|
|
Reposition();
|
|
}
|
|
|
|
revealedContent = revealContent;
|
|
}
|
|
|
|
protected override bool dim
|
|
{
|
|
get
|
|
{
|
|
var dim = BoltCore.Configuration.dimInactiveNodes && !analysis.isEntered;
|
|
|
|
if (isMouseOver || isSelected)
|
|
{
|
|
dim = false;
|
|
}
|
|
|
|
return dim;
|
|
}
|
|
}
|
|
|
|
public override void DrawForeground()
|
|
{
|
|
BeginDim();
|
|
|
|
base.DrawForeground();
|
|
|
|
if (showTitle)
|
|
{
|
|
DrawTitle();
|
|
}
|
|
|
|
if (showSummary)
|
|
{
|
|
DrawSummary();
|
|
}
|
|
|
|
if (showContent)
|
|
{
|
|
DrawContentWrapped();
|
|
}
|
|
|
|
EndDim();
|
|
|
|
CheckReveal();
|
|
}
|
|
|
|
private void DrawTitle()
|
|
{
|
|
using (LudiqGUIUtility.iconSize.Override(IconSize.Small))
|
|
{
|
|
GUI.Label(titlePosition, titleContent, invertForeground ? Styles.titleInverted : Styles.title);
|
|
}
|
|
}
|
|
|
|
private void DrawSummary()
|
|
{
|
|
GUI.Label(summaryPosition, summaryContent, invertForeground ? Styles.summaryInverted : Styles.summary);
|
|
}
|
|
|
|
private void DrawContentWrapped()
|
|
{
|
|
GUI.BeginClip(contentOuterPosition);
|
|
|
|
DrawContentBackground();
|
|
|
|
DrawContent();
|
|
|
|
GUI.EndClip();
|
|
}
|
|
|
|
protected virtual void DrawContentBackground()
|
|
{
|
|
if (e.IsRepaint)
|
|
{
|
|
Styles.contentBackground.Draw(contentBackgroundPosition, false, false, false, false);
|
|
}
|
|
}
|
|
|
|
protected virtual void DrawContent() { }
|
|
|
|
#endregion
|
|
|
|
|
|
#region Selecting
|
|
|
|
public override bool canSelect => true;
|
|
|
|
#endregion
|
|
|
|
|
|
#region Dragging
|
|
|
|
protected override bool snapToGrid => BoltCore.Configuration.snapToGrid;
|
|
|
|
public override bool canDrag => true;
|
|
|
|
public override void ExpandDragGroup(HashSet<IGraphElement> dragGroup)
|
|
{
|
|
if (BoltCore.Configuration.carryChildren)
|
|
{
|
|
foreach (var transition in state.outgoingTransitions)
|
|
{
|
|
if (dragGroup.Contains(transition.destination))
|
|
{
|
|
continue;
|
|
}
|
|
|
|
dragGroup.Add(transition.destination);
|
|
|
|
canvas.Widget(transition.destination).ExpandDragGroup(dragGroup);
|
|
}
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Deleting
|
|
|
|
public override bool canDelete => true;
|
|
|
|
#endregion
|
|
|
|
|
|
#region Resizing
|
|
|
|
public override bool canResizeHorizontal => true;
|
|
|
|
#endregion
|
|
|
|
|
|
#region Clipboard
|
|
|
|
public override void ExpandCopyGroup(HashSet<IGraphElement> copyGroup)
|
|
{
|
|
copyGroup.UnionWith(state.transitions.Cast<IGraphElement>());
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
#region Actions
|
|
|
|
protected override IEnumerable<DropdownOption> contextOptions
|
|
{
|
|
get
|
|
{
|
|
if (Application.isPlaying && reference.hasData)
|
|
{
|
|
if (canForceEnter)
|
|
{
|
|
yield return new DropdownOption((Action)ForceEnter, "Force Enter");
|
|
}
|
|
|
|
if (canForceExit)
|
|
{
|
|
yield return new DropdownOption((Action)ForceExit, "Force Exit");
|
|
}
|
|
}
|
|
|
|
if (canToggleStart)
|
|
{
|
|
yield return new DropdownOption((Action)ToggleStart, "Toggle Start");
|
|
}
|
|
|
|
if (state.canBeSource)
|
|
{
|
|
yield return new DropdownOption((Action)MakeTransition, "Make Transition");
|
|
}
|
|
|
|
if (state.canBeSource && state.canBeDestination)
|
|
{
|
|
yield return new DropdownOption((Action)MakeSelfTransition, "Make Self Transition");
|
|
}
|
|
|
|
foreach (var baseOption in base.contextOptions)
|
|
{
|
|
yield return baseOption;
|
|
}
|
|
}
|
|
}
|
|
|
|
private void ForceEnter()
|
|
{
|
|
using (var flow = Flow.New(reference))
|
|
{
|
|
state.OnEnter(flow, StateEnterReason.Forced);
|
|
}
|
|
}
|
|
|
|
private void ForceExit()
|
|
{
|
|
using (var flow = Flow.New(reference))
|
|
{
|
|
state.OnExit(flow, StateExitReason.Forced);
|
|
}
|
|
}
|
|
|
|
protected void MakeTransition()
|
|
{
|
|
canvas.StartTransition(state);
|
|
}
|
|
|
|
protected void MakeSelfTransition()
|
|
{
|
|
canvas.StartTransition(state);
|
|
canvas.EndTransition(state);
|
|
}
|
|
|
|
protected void ToggleStart()
|
|
{
|
|
UndoUtility.RecordEditedObject("Toggle State Start");
|
|
|
|
state.isStart = !state.isStart;
|
|
}
|
|
|
|
#endregion
|
|
|
|
|
|
public static class Styles
|
|
{
|
|
static Styles()
|
|
{
|
|
title = new GUIStyle(BoltCore.Styles.nodeLabel);
|
|
title.fontSize = 12;
|
|
title.alignment = TextAnchor.MiddleCenter;
|
|
title.wordWrap = true;
|
|
|
|
summary = new GUIStyle(BoltCore.Styles.nodeLabel);
|
|
summary.fontSize = 10;
|
|
summary.alignment = TextAnchor.MiddleCenter;
|
|
summary.wordWrap = true;
|
|
|
|
titleInverted = new GUIStyle(title);
|
|
titleInverted.normal.textColor = ColorPalette.unityBackgroundDark;
|
|
|
|
summaryInverted = new GUIStyle(summary);
|
|
summaryInverted.normal.textColor = ColorPalette.unityBackgroundDark;
|
|
|
|
contentBackground = new GUIStyle("In BigTitle");
|
|
contentBackground.padding = new RectOffset(0, 0, 4, 4);
|
|
}
|
|
|
|
public static readonly GUIStyle title;
|
|
|
|
public static readonly GUIStyle summary;
|
|
|
|
public static readonly GUIStyle titleInverted;
|
|
|
|
public static readonly GUIStyle summaryInverted;
|
|
|
|
public static readonly GUIStyle contentBackground;
|
|
|
|
public static readonly float spaceBeforeContent = 5;
|
|
|
|
public static readonly float spaceBetweenTitleAndSummary = 0;
|
|
|
|
public static readonly float enterFadeDuration = 0.5f;
|
|
|
|
public static readonly float contentRevealSpeed = 15;
|
|
}
|
|
}
|
|
}
|