Files
2026-07-13 13:13:17 +08:00

769 lines
41 KiB
C#

using ImGuiNET;
using System.Diagnostics;
using T3.Editor.Gui.MagGraph.Interaction;
using T3.Editor.Gui.Interaction;
using T3.Editor.Gui.Interaction.Keyboard;
using T3.Editor.Gui.MagGraph.Model;
using T3.Editor.Gui.UiHelpers;
using T3.Editor.UiModel.Commands.Graph;
using T3.Editor.UiModel.ProjectHandling;
using MagItemMovement = T3.Editor.Gui.MagGraph.Interaction.MagItemMovement;
using SkillTraining = T3.Editor.Skills.Training.SkillTraining;
// ReSharper disable MemberCanBePrivate.Global
namespace T3.Editor.Gui.MagGraph.States
{
internal static class GraphStates
{
internal static State<GraphUiContext> Default
= new(
Enter: context =>
{
// Todo: this should be a reset method in context
context.TempConnections.Clear();
context.ActiveSourceItem = null;
context.ActiveTargetItem = null;
context.ActiveTargetInputId = Guid.Empty;
context.DraggedPrimaryOutputType = null;
// ReSharper disable once ConstantConditionalAccessQualifier
// This might not be initialized on startup
context.Placeholder.Reset(context);
context.DisconnectedInputHashes.Clear();
},
Update: context =>
{
if (context.ItemWithActiveCustomUi != null)
return;
if (context.ProjectView.GraphImageBackground.HasInteractionFocus)
{
context.StateMachine.SetState(BackgroundContentIsInteractive, context);
return;
}
// Check keyboard commands if focused...
if (context.View.IsFocused
&& context.View.IsHovered
&& !ImGui.IsAnyItemActive())
{
// Tab create placeholder
if (ImGui.IsKeyReleased(ImGuiKey.Tab) && !ImGui.GetIO().KeyShift)
{
var focusedObject =
context.Selector.Selection.Count == 1 &&
context.View.IsItemVisible(context.Selector.Selection[0])
? context.Selector.Selection[0]
: null;
if (focusedObject != null
&& context.Layout.Items.TryGetValue(focusedObject.Id, out var focusedItem))
{
if (focusedItem.OutputLines.Length > 0)
{
context.Placeholder.OpenForItemOutput(context, focusedItem, focusedItem.OutputLines[0]);
}
}
else
{
var posOnCanvas = context.View.InverseTransformPositionFloat(ImGui.GetMousePos());
context.Placeholder.OpenOnCanvas(context, posOnCanvas);
}
context.StateMachine.SetState(Placeholder, context);
}
// else if (ImGui.IsKeyReleased(ImGuiKey.Delete) || ImGui.IsKeyReleased(ImGuiKey.Backspace))
// {
// Modifications.DeleteSelectedOps(context);
// }
}
if (!context.View.IsHovered)
return;
// Open children or parent component with keyboard
if (UserActions.CloseOperator.Triggered() && ProjectView.Focused != null)
{
ProjectView.Focused.TrySetCompositionOpToParent();
}
if (UserActions.OpenOperator.Triggered() && ProjectView.Focused != null)
{
var isWindowActive = ImGui.IsWindowFocused() || ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByPopup);
// Check if we have either an active (selected) item or a hovered item
var itemToOpen = context.ActiveItem ?? context.HoveredItem;
if (isWindowActive && itemToOpen != null && itemToOpen.Variant == MagGraphItem.Variants.Operator)
{
Debug.Assert(itemToOpen.Instance != null);
if (itemToOpen.Instance.Children.Count > 0)
{
ProjectView.Focused.TrySetCompositionOpToChild(itemToOpen.Instance.SymbolChildId);
}
}
}
// Mouse click
var clickedDown = ImGui.IsMouseClicked(ImGuiMouseButton.Left);
if (!clickedDown)
return;
// Open children or parent component
if (ImGui.IsMouseDoubleClicked(ImGuiMouseButton.Left)
&& ProjectView.Focused != null
&& !SkillTraining.IsInPlayMode
)
{
var clickedBackground = context.ActiveItem == null;
if (clickedBackground)
{
// Skip double-clicks that started before the view was opened,
// e.g. the click that opened the project from the hub
if (!ProjectView.Focused.OpenedWithinDoubleClickTime)
{
// Already at the root composition -> return to the project hub
if (!ProjectView.Focused.TrySetCompositionOpToParent())
{
ProjectView.Focused.Close();
}
}
}
else
{
var isWindowActive = ImGui.IsWindowFocused() || ImGui.IsWindowHovered(ImGuiHoveredFlags.AllowWhenBlockedByPopup);
if (isWindowActive && context.ActiveItem.Variant == MagGraphItem.Variants.Operator)
{
Debug.Assert(context.ActiveItem.Instance != null);
// TODO: implement lib edit warning popup
// var blocked = false;
// if (UserSettings.Config.WarnBeforeLibEdit && context.ActiveItem.Instance.Symbol.Namespace.StartsWith("Lib."))
// {
// if (UserSettings.Config.WarnBeforeLibEdit)
// {
// var count = Structure.CollectDependingSymbols(instance.Symbol).Count();
// LibWarningDialog.DependencyCount = count;
// LibWarningDialog.HandledInstance = instance;
// _canvas.LibWarningDialog.ShowNextFrame();
// blocked = true;
// }
// }
// if (!blocked)
// {
// Until we align the context switching between graphs, this hack applies the current
// MagGraph scope to the legacy graph, so it's correctly saved for the Symbol in the user settings...
//ProjectView.Focused?.GraphCanvas?.SetTargetScope(context.Canvas.GetTargetScope());
if (context.ActiveItem.Instance.Children.Count > 0)
{
ProjectView.Focused.TrySetCompositionOpToChild(context.ActiveItem.Instance.SymbolChildId);
}
//ImGui.CloseCurrentPopup(); // ??
//}
}
}
}
// Outputs and inputs are handled first, because they might
// overlap with other items that got activated on click.
if (context.ActiveSourceItem != null)
{
context.StateMachine.SetState(HoldOutput, context);
context.ActiveItem = context.ActiveSourceItem;
}
else if (context.ActiveTargetItem != null)
{
context.StateMachine.SetState(HoldInput, context);
}
else if (context.ActiveItem != null)
{
context.StateMachine.SetState(HoldItem, context);
}
// Mouse is pressed but nothing is active -> background
else
{
context.StateMachine.SetState(HoldBackground, context);
}
},
Exit:
_ => { }
);
/** A special mode to prevent interaction with graph elements */
internal static State<GraphUiContext> BackgroundContentIsInteractive
= new(
Enter: _ => { },
Update: context =>
{
// Switch back
if (!context.ProjectView.GraphImageBackground.HasInteractionFocus)
{
context.StateMachine.SetState(Default, context);
return;
}
},
Exit: _ => { }
);
/// <summary>
/// Active while long tapping on background for insertion
/// </summary>
internal static State<GraphUiContext> HoldBackground
= new(
Enter: _ => { },
Update: context =>
{
if (!ImGui.IsMouseDown(ImGuiMouseButton.Left)
|| !context.View.IsFocused
|| ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
context.StateMachine.SetState(Default, context);
return;
}
const float longTapDuration = 0.3f;
var longTapProgress = context.StateMachine.StateTime / longTapDuration;
MagItemMovement.UpdateLongPressIndicator(longTapProgress);
if (!(longTapProgress > 1))
return;
// TODO: setting both, state and placeholder, feels awkward.
context.StateMachine.SetState(Placeholder, context);
var posOnCanvas = context.View.InverseTransformPositionFloat(ImGui.GetMousePos());
context.Placeholder.OpenOnCanvas(context, posOnCanvas);
},
Exit: _ => { }
);
internal static State<GraphUiContext> Placeholder
= new(
Enter: _ => { },
Update: context =>
{
if (context.Placeholder.PlaceholderItem != null)
return;
context.Placeholder.Cancel(context);
context.StateMachine.SetState(Default, context);
},
Exit: _ => { }
);
internal static State<GraphUiContext> HoldItem
= new(
Enter: context =>
{
var item = context.ActiveItem;
Debug.Assert(item != null);
var selector = context.Selector;
var isPartOfSelection = selector.IsSelected(item);
if (isPartOfSelection)
{
context.ItemMovement.SetDraggedItems(selector.Selection);
}
else
{
context.ItemMovement.SetDraggedItemIdsToSnappedForItem(item);
}
},
Update: context =>
{
Debug.Assert(context.ActiveItem != null);
if (!ImGui.IsMouseDown(ImGuiMouseButton.Left))
{
MagItemMovement.SelectActiveItem(context);
context.ItemMovement.Reset();
context.StateMachine.SetState(Default, context);
return;
}
if (ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
context.StateMachine.SetState(DragItems, context);
return;
}
const float longTapDuration = 0.3f;
var longTapProgress = context.StateMachine.StateTime / longTapDuration;
MagItemMovement.UpdateLongPressIndicator(longTapProgress);
if (!(longTapProgress > 1))
return;
MagItemMovement.SelectActiveItem(context);
context.ItemMovement.SetDraggedItemIds([context.ActiveItem.Id]);
context.StateMachine.SetState(HoldItemAfterLongTap, context);
},
Exit: _ => { }
);
internal static State<GraphUiContext> HoldItemAfterLongTap
= new(
Enter: _ => { },
Update: context =>
{
Debug.Assert(context.ActiveItem != null);
if (!ImGui.IsMouseDown(ImGuiMouseButton.Left))
{
MagItemMovement.SelectActiveItem(context);
context.StateMachine.SetState(Default, context);
return;
}
if (ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
context.StateMachine.SetState(DragItems, context);
}
},
Exit: _ => { }
);
internal static State<GraphUiContext> DragItems
= new(
Enter: context =>
{
context.ItemMovement.PrepareDragInteraction();
context.ItemMovement.StartDragOperation(context);
},
Update: context =>
{
if (!ImGui.IsMouseDown(ImGuiMouseButton.Left))
{
context.ItemMovement.CompleteDragOperation(context);
context.StateMachine.SetState(Default, context);
return;
}
context.ItemMovement.UpdateDragging(context);
},
Exit: context => { context.ItemMovement.StopDragOperation(); }
);
/// <summary>
/// Active while long tapping on background for insertion
/// </summary>
internal static State<GraphUiContext> HoldOutput
= new(
Enter: _ => { },
Update: context =>
{
var sourceItem = context.ActiveItem;
Debug.Assert(sourceItem != null);
Debug.Assert(sourceItem.OutputLines.Length > 0);
Debug.Assert(context.ActiveSourceOutputId != Guid.Empty);
// Click
if (!ImGui.IsMouseDown(ImGuiMouseButton.Left))
{
if (context.TryGetActiveOutputLine(out var outputLine))
{
context.Placeholder.OpenForItemOutput(context, sourceItem, outputLine, context.ActiveOutputDirection);
context.StateMachine.SetState(Placeholder, context);
}
else
{
context.StateMachine.SetState(Default, context);
}
return;
}
// Start dragging...
if (ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
if (!context.TryGetActiveOutputLine(out var outputLine))
{
Log.Warning("Output not found?");
context.StateMachine.SetState(Default, context);
return;
}
AddTempConnectionsForOutputDrag(context, sourceItem, outputLine);
context.ActiveSourceItem = sourceItem;
context.DraggedPrimaryOutputType = outputLine.Output.ValueType;
context.StateMachine.SetState(DragConnectionEnd, context);
}
},
Exit: _ => { }
);
internal static State<GraphUiContext> HoldInput
= new(
Enter: _ => { },
Update: context =>
{
var sourceItem = context.ActiveItem;
Debug.Assert(sourceItem != null);
Debug.Assert(sourceItem.OutputLines.Length > 0 ||
sourceItem.Variant == MagGraphItem.Variants.Output);
Debug.Assert(context.ActiveTargetInputId != Guid.Empty);
// Click
var released = !ImGui.IsMouseDown(ImGuiMouseButton.Left);
if (released)
{
if (context.TryGetActiveInputLine(out var inputLine))
{
context.Placeholder.OpenForItemInput(context, context.ActiveTargetItem, inputLine.Id, context.ActiveInputDirection);
context.StateMachine.SetState(Placeholder, context);
}
else
{
context.StateMachine.SetState(Default, context);
}
return;
}
// Start dragging...
if (ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
if (context.TryGetActiveInputLine(out var draggedInputLine)
&& draggedInputLine.InputUi != null)
{
context.View.StartDraggingFromInput(context.ActiveTargetItem, draggedInputLine.InputUi.InputDefinition);
}
else
{
context.StateMachine.SetState(Default, context);
}
}
},
Exit: _ => { }
);
internal static State<GraphUiContext> HoldingConnectionEnd
= new(
Enter: _ => { },
Update: context =>
{
if (!ImGui.IsMouseDown(ImGuiMouseButton.Left))
{
context.Placeholder.OpenToSplitHoveredConnections(context); // Will change state implicitly
return;
}
if (ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
if (context.ConnectionHovering.ConnectionHoversWhenClicked.Count == 0)
return;
var connection = context.ConnectionHovering.ConnectionHoversWhenClicked[0].Connection;
context.ActiveSourceOutputId = connection.SourceOutput.Id;
// Remove existing connection
context.StartMacroCommand("Reconnect from input")
.AddAndExecCommand(new DeleteConnectionCommand(context.CompositionInstance.Symbol,
connection.AsSymbolConnection(),
connection.MultiInputIndex));
if (MagItemMovement.DisconnectedInputWouldCollapseLine(connection))
{
var collectSnappedItems = MagItemMovement.CollectSnappedItems(connection.TargetItem);
collectSnappedItems.Remove(connection.TargetItem);
MagItemMovement.MoveSnappedItemsVertically(context,
collectSnappedItems,
connection.TargetItem.PosOnCanvas.Y +
MagGraphItem.GridSize.Y * (connection.InputLineIndex + 0.5f),
-MagGraphItem.GridSize.Y);
}
// var deletedLineIndex = 0;
// var lines = connection.TargetItem.InputLines;
// while ( deletedLineIndex < lines.Length && lines[deletedLineIndex].Id != connection.TargetInput.Id)
// {
// deletedLineIndex++;
// }
var tempConnection = new MagGraphConnection
{
Style = MagGraphConnection.ConnectionStyles.Unknown,
SourcePos = connection.SourcePos,
TargetPos = default,
SourceItem = connection.SourceItem,
TargetItem = null,
SourceOutput = connection.SourceOutput,
OutputLineIndex = 0,
VisibleOutputIndex = 0,
ConnectionHash = 0,
IsTemporary = true,
WasDisconnected = true,
};
context.TempConnections.Add(tempConnection);
context.ActiveSourceItem = connection.SourceItem;
context.DraggedPrimaryOutputType = connection.Type;
context.ActiveItem = connection.SourceItem;
context.StateMachine.SetState(DragConnectionEnd, context);
context.Layout.FlagStructureAsChanged();
}
},
Exit: _ => { }
);
internal static State<GraphUiContext> DragConnectionEnd
= new(
Enter: _ => { },
Update: context =>
{
if (ImGui.IsKeyDown(ImGuiKey.Escape))
{
context.StateMachine.SetState(Default, context);
return;
}
var posOnCanvas = context.View.InverseTransformPositionFloat(ImGui.GetMousePos());
context.PeekAnchorInCanvas = posOnCanvas;
var mouseReleased = context.StateMachine.StateTime > 0 && ImGui.IsMouseReleased(ImGuiMouseButton.Left);
if (!mouseReleased)
return;
if (InputSnapper.TryToReconnect(context))
{
context.Layout.FlagStructureAsChanged();
context.CompleteMacroCommand();
context.StateMachine.SetState(Default, context);
return;
}
var hasDisconnections = context.TempConnections.Any(c => c.WasDisconnected);
var droppedOnItem = InputPicking.TryInitializeAtPosition(context, posOnCanvas);
if (droppedOnItem)
{
context.StateMachine.SetState(PickInput, context);
}
else if (hasDisconnections)
{
// Ripped off input -> Avoid open placeholder
//UndoRedoStack.Add(context.MacroCommand);
context.CompleteMacroCommand();
context.StateMachine.SetState(Default, context);
}
else
{
// Was dropped on operator or background...
context.Placeholder.OpenOnCanvas(context, posOnCanvas, context.DraggedPrimaryOutputType,
onlyMultiInputs: context.TempConnections.Count > 1);
context.StateMachine.SetState(Placeholder, context);
}
},
Exit: _ => { }
);
internal static State<GraphUiContext> PickInput
= new(
Enter: InputPicking.Init,
Update: InputPicking.DrawHiddenInputSelector,
Exit: InputPicking.Reset
);
internal static State<GraphUiContext> PickOutput
= new(
Enter: OutputPicking.Init,
Update => { }, //OutputPicking.DrawHiddenOutputSelector,
Exit: OutputPicking.Reset
);
internal static State<GraphUiContext> HoldingConnectionBeginning
= new(
Enter: _ => { },
Update: context =>
{
var wasClick = !ImGui.IsMouseDown(ImGuiMouseButton.Left);
if (wasClick)
{
context.Placeholder.OpenToSplitHoveredConnections(context); // Will change state implicitly
return;
}
if (ImGui.IsMouseDragging(ImGuiMouseButton.Left))
{
if (context.ConnectionHovering.ConnectionHoversWhenClicked.Count == 0)
return;
context.StartMacroCommand("Reconnect from output");
foreach (var h in context.ConnectionHovering.ConnectionHoversWhenClicked
.OrderByDescending(h => h.Connection.MultiInputIndex))
{
var connection = h.Connection;
context.DisconnectedInputHashes.Add(connection.GetItemInputHash()); // keep input visible until state is complete
// Remove existing connections
context.MacroCommand!
.AddAndExecCommand(new DeleteConnectionCommand(context.CompositionInstance.Symbol,
connection.AsSymbolConnection(),
h.Connection.MultiInputIndex));
//if (connection.MultiInputIndex > 0)
// continue;
var tempConnection = new MagGraphConnection
{
Style = MagGraphConnection.ConnectionStyles.Unknown,
TargetPos = connection.TargetPos,
TargetItem = connection.TargetItem,
InputLineIndex = connection.InputLineIndex,
MultiInputIndex = connection.MultiInputIndex,
SourceItem = null,
SourceOutput = null,
IsTemporary = true,
WasDisconnected = true,
};
// Sadly keeping disconnected multi input slots visible is tricky,
// so, this is only a preparation for a potential later implementation
//Log.Debug("Keep input hash " + connection.GetItemInputHash());
//context.DisconnectedInputsHashes.Add(connection.GetItemInputHash());
context.TempConnections.Add(tempConnection);
context.DraggedPrimaryOutputType = connection.Type;
}
context.Layout.FlagStructureAsChanged();
context.StateMachine.SetState(DragConnectionBeginning, context);
}
},
Exit:
_ => { }
);
internal static State<GraphUiContext> DragConnectionBeginning
= new(
Enter: _ => { },
Update: context =>
{
if (!ImGui.IsMouseDown(ImGuiMouseButton.Left))
{
if (OutputSnapper.TryToReconnect(context))
{
context.Layout.FlagStructureAsChanged();
context.CompleteMacroCommand();
context.StateMachine.SetState(Default, context);
return;
}
var hasDisconnections = context.TempConnections.Any(c => c.WasDisconnected);
if (hasDisconnections)
{
// Ripped off connection beginnings -> just keep them disconnected
context.CompleteMacroCommand();
context.StateMachine.SetState(Default, context);
}
else
{
// Dropped on background -> pick a new operator feeding the dragged input
var posOnCanvas = context.View.InverseTransformPositionFloat(ImGui.GetMousePos());
context.Placeholder.OpenOnCanvas(context, posOnCanvas, outputTypeFilter: context.DraggedPrimaryOutputType);
context.StateMachine.SetState(Placeholder, context);
}
return;
}
if (ImGui.IsKeyDown(ImGuiKey.Escape))
{
context.CancelMacroCommand();
context.Layout.FlagStructureAsChanged();
context.StateMachine.SetState(Default, context);
}
},
Exit: _ => { });
internal static State<GraphUiContext> RenameChild
= new(
Enter: _ => { },
Update: _ => { },
Exit: _ => { }
);
internal static State<GraphUiContext> RenameSection
= new(
Enter: _ => { },
Update: _ => { },
Exit: _ => { }
);
internal static State<GraphUiContext> DragSection
= new(
Enter: _ => { },
Update: _ => { },
Exit: _ => { }
);
internal static State<GraphUiContext> ResizeSection
= new(
Enter: _ => { },
Update: SectionResizing.Draw,
Exit: _ => { }
);
/// <summary>
/// Starts dragging temp connections from the picked output. If the dragged item is part of a multi-selection,
/// the primary outputs of all other selected operators with a matching type are picked up too, ordered by
/// canvas position, so they can be dropped onto a multi-input or the symbol browser together.
/// </summary>
private static void AddTempConnectionsForOutputDrag(GraphUiContext context, MagGraphItem sourceItem, MagGraphItem.OutputLine outputLine)
{
var draggedItems = new List<MagGraphItem> { sourceItem };
var outputType = outputLine.Output.ValueType;
if (context.Selector.IsSelected(sourceItem))
{
foreach (var item in context.Layout.Items.Values)
{
if (item == sourceItem
|| item.Variant != MagGraphItem.Variants.Operator
|| !context.Selector.IsSelected(item))
continue;
if (item.OutputLines.Length == 0 || item.OutputLines[0].Output.ValueType != outputType)
continue;
draggedItems.Add(item);
}
}
draggedItems.Sort(static (a, b) => a.PosOnCanvas.Y != b.PosOnCanvas.Y
? a.PosOnCanvas.Y.CompareTo(b.PosOnCanvas.Y)
: a.PosOnCanvas.X.CompareTo(b.PosOnCanvas.X));
for (var index = 0; index < draggedItems.Count; index++)
{
var item = draggedItems[index];
var itemOutputLine = item == sourceItem ? outputLine : item.OutputLines[0];
var posOnCanvas = item.PosOnCanvas + new Vector2(MagGraphItem.GridSize.X,
MagGraphItem.GridSize.Y * (1.5f + itemOutputLine.VisibleIndex));
context.TempConnections.Add(new MagGraphConnection
{
Style = MagGraphConnection.ConnectionStyles.Unknown,
SourcePos = posOnCanvas,
TargetPos = default,
SourceItem = item,
TargetItem = null,
SourceOutput = itemOutputLine.Output,
OutputLineIndex = itemOutputLine.VisibleIndex,
VisibleOutputIndex = 0,
ConnectionHash = 0,
IsTemporary = true,
MultiInputIndex = index,
});
}
}
}
}