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 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 BackgroundContentIsInteractive = new( Enter: _ => { }, Update: context => { // Switch back if (!context.ProjectView.GraphImageBackground.HasInteractionFocus) { context.StateMachine.SetState(Default, context); return; } }, Exit: _ => { } ); /// /// Active while long tapping on background for insertion /// internal static State 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 Placeholder = new( Enter: _ => { }, Update: context => { if (context.Placeholder.PlaceholderItem != null) return; context.Placeholder.Cancel(context); context.StateMachine.SetState(Default, context); }, Exit: _ => { } ); internal static State 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 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 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(); } ); /// /// Active while long tapping on background for insertion /// internal static State 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 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 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 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 PickInput = new( Enter: InputPicking.Init, Update: InputPicking.DrawHiddenInputSelector, Exit: InputPicking.Reset ); internal static State PickOutput = new( Enter: OutputPicking.Init, Update => { }, //OutputPicking.DrawHiddenOutputSelector, Exit: OutputPicking.Reset ); internal static State 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 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 RenameChild = new( Enter: _ => { }, Update: _ => { }, Exit: _ => { } ); internal static State RenameSection = new( Enter: _ => { }, Update: _ => { }, Exit: _ => { } ); internal static State DragSection = new( Enter: _ => { }, Update: _ => { }, Exit: _ => { } ); internal static State ResizeSection = new( Enter: _ => { }, Update: SectionResizing.Draw, Exit: _ => { } ); /// /// 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. /// private static void AddTempConnectionsForOutputDrag(GraphUiContext context, MagGraphItem sourceItem, MagGraphItem.OutputLine outputLine) { var draggedItems = new List { 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, }); } } } }