#nullable enable using System.Diagnostics; using System.Diagnostics.CodeAnalysis; using System.IO; using ImGuiNET; using Newtonsoft.Json; using Newtonsoft.Json.Linq; using T3.Core.DataTypes; using T3.Core.Model; using T3.Core.Operator; using T3.Core.Resource; using T3.Core.Resource.Assets; using T3.Editor.Gui.Interaction; using T3.Editor.Gui.OutputUi; using T3.Editor.Gui.Styling; using T3.Editor.Gui.UiHelpers; using T3.Editor.Gui.Windows.Output; using T3.Editor.SystemUi; using T3.Editor.UiModel.Commands; using T3.Editor.UiModel.Commands.Sections; using T3.Editor.UiModel.Commands.Graph; using T3.Editor.UiModel.InputsAndTypes; using T3.Editor.UiModel.ProjectHandling; using T3.Editor.UiModel.Selection; using T3.Serialization; namespace T3.Editor.UiModel.Modification; /// /// Various actions performed on selected nodes triggered by hotkeys for context menus /// internal static class NodeActions { internal static void ToggleBypassedForSelectedElements(NodeSelection nodeSelection) { var selectedChildUis = nodeSelection.GetSelectedChildUis().ToList(); var allSelectedAreBypassed = selectedChildUis.TrueForAll(selectedChildUi => selectedChildUi.SymbolChild.IsBypassed); var shouldBypass = !allSelectedAreBypassed; var commands = new List(); foreach (var selectedChildUi in selectedChildUis) { commands.Add(new ChangeInstanceBypassedCommand(selectedChildUi.SymbolChild, shouldBypass)); } UndoRedoStack.AddAndExecute(new MacroCommand("Changed Bypassed", commands)); } public static void ToggleDisabledForSelectedElements(NodeSelection nodeSelection) { var selectedChildren = nodeSelection.GetSelectedChildUis().ToList(); var allSelectedDisabled = selectedChildren.TrueForAll(selectedChildUi => selectedChildUi.SymbolChild.IsDisabled); var shouldDisable = !allSelectedDisabled; var commands = new List(); foreach (var selectedChildUi in selectedChildren) { commands.Add(new ChangeInstanceIsDisabledCommand(selectedChildUi, shouldDisable)); } UndoRedoStack.AddAndExecute(new MacroCommand("Disable/Enable", commands)); } public static void DeleteSelectedElements(NodeSelection nodeSelection, SymbolUi compositionSymbolUi, List? selectedChildUis = null, List? selectedInputUis = null, List? selectedOutputUis = null) { var commands = new List(); selectedChildUis ??= nodeSelection.GetSelectedChildUis().ToList(); // Deleting a collapsed section deletes its hidden contents with it - once the // header is gone they would have no visible representation left on the canvas var sectionsToDelete = nodeSelection.GetSelectedNodes
().ToList(); var hiddenOps = new List(); foreach (var selectedSection in sectionsToDelete.ToArray()) { if (selectedSection.Collapsed) { SectionTree.CollectHiddenContents(compositionSymbolUi, selectedSection, hiddenOps, sectionsToDelete); } } var childUisToDelete = new List(selectedChildUis); var childIdsToDelete = new HashSet(); foreach (var childUi in selectedChildUis) { childIdsToDelete.Add(childUi.Id); } foreach (var hiddenOp in hiddenOps) { if (childIdsToDelete.Add(hiddenOp.Id)) { childUisToDelete.Add(hiddenOp); } } if (childUisToDelete.Count != 0) { var cmd = new DeleteSymbolChildrenCommand(compositionSymbolUi, childUisToDelete); commands.Add(cmd); } var sectionIdsToDelete = new HashSet(); foreach (var sectionToDelete in sectionsToDelete) { if (!sectionIdsToDelete.Add(sectionToDelete.Id)) continue; var cmd = new DeleteSectionCommand(compositionSymbolUi, sectionToDelete); commands.Add(cmd); } if (!compositionSymbolUi.Symbol.SymbolPackage.IsReadOnly) { selectedInputUis ??= nodeSelection.GetSelectedNodes().ToList(); selectedOutputUis ??= nodeSelection.GetSelectedNodes().ToList(); if (selectedInputUis.Count > 0 || selectedOutputUis.Count > 0) { // Added after the child/section deletes so its undo restores the slots before // those commands restore child-side connections (see RemoveInputsOrOutputsCommand). commands.Add(new RemoveInputsOrOutputsCommand(compositionSymbolUi.Symbol.Id, selectedInputUis.Select(entry => entry.Id).ToArray(), selectedOutputUis.Select(entry => entry.Id).ToArray())); } } var deleteCommand = new MacroCommand("Delete elements", commands); UndoRedoStack.AddAndExecute(deleteCommand); nodeSelection.Clear(); } /// Screen position to place the section at when nothing is selected. public static Section AddSection(NodeSelection nodeSelection, ScalableCanvas canvas, Instance compositionOp, Vector2? placementScreenPos = null) { var size = new Vector2(100, 140); var posOnCanvas = canvas.InverseTransformPositionFloat(placementScreenPos ?? ImGui.GetMousePos()); var area = new ImRect(posOnCanvas, posOnCanvas + size); if (nodeSelection.IsAnythingSelected()) { for (var index = 0; index < nodeSelection.Selection.Count; index++) { var node = nodeSelection.Selection[index]; var nodeArea = new ImRect(node.PosOnCanvas, node.PosOnCanvas + node.Size); if (index == 0) { area = nodeArea; } else { area.Add(nodeArea); } } area.Expand(new Vector2(60,120)); } var section = new Section { Id = Guid.NewGuid(), Title = "", Label = "", Color = UiColors.Gray, PosOnCanvas = area.Min, Size = area.GetSize() }; // Ownership of enclosed ops and the new frame's own nesting derive from // geometry on the next layout refresh var command = new AddSectionCommand(compositionOp.GetSymbolUi(), section); UndoRedoStack.AddAndExecute(command); return section; } public static void PinSelectedToOutputWindow(ProjectView components, NodeSelection nodeSelection, Instance compositionOp, bool unpinIfAlreadySelected =false) { var outputWindow = OutputWindow.OutputWindowInstances.FirstOrDefault(ow => ow.Config.Visible) as OutputWindow; if (outputWindow == null) { //Log.Warning("Can't pin selection without visible output window"); return; } var selection = nodeSelection.GetSelectedChildUis().ToList(); if (selection.Count > 1) { Log.Info("Please select only one operator to pin to output window"); return; } if (selection.Count == 0) { outputWindow.Pinning.PinInstance(compositionOp, components, unpinIfAlreadySelected); return; } if (selection.Count == 1) { if (compositionOp.Children.TryGetChildInstance(selection[0].Id, out var child)) { outputWindow.Pinning.PinInstance(child, components, unpinIfAlreadySelected); } } } #region Copy and paste public static void CopySelectedNodesToClipboard(NodeSelection nodeSelection, Instance composition) { var selectedChildren = nodeSelection.GetSelectedNodes().ToList(); var selectedSections = nodeSelection.GetSelectedNodes
().ToList(); if (selectedChildren.Count + selectedSections.Count == 0) return; if (!GraphOperations.TryCopyNodesAsJson(composition, selectedChildren, selectedSections, out var resultJsonString)) return; EditorUi.Instance.SetClipboardText(resultJsonString); } // todo - better encapsulate this in SymbolJson public static void PasteClipboard(NodeSelection nodeSelection, ScalableCanvas canvas, Instance compositionOp) { try { var text = EditorUi.Instance.GetClipboardText(); if (string.IsNullOrEmpty(text)) { Log.Debug($"Can't paste empty clipboard."); return; } using var reader = new StringReader(text); using var jsonReader = new JsonTextReader(reader); if (JToken.ReadFrom(jsonReader, SymbolJson.LoadSettings) is not JArray jArray) return; var symbolJson = jArray[0]; if (!TryGetPastedSymbol(symbolJson, compositionOp.Symbol.SymbolPackage, out var containerSymbol)) { Log.Error($"Failed to paste symbol due to invalid symbol json"); return; } var symbolUiJson = jArray[1]; var hasContainerSymbolUi = SymbolUiJson.TryReadSymbolUiExternal(symbolUiJson, containerSymbol, out var containerSymbolUi); if (!hasContainerSymbolUi || containerSymbolUi == null) { Log.Error($"Failed to paste symbol due to invalid symbol ui json"); return; } var compositionSymbolUi = compositionOp.GetSymbolUi(); var targetPosition = canvas.InverseTransformPositionFloat(ImGui.GetMousePos()); var cmd = new CopySymbolChildrenCommand(containerSymbolUi, null, containerSymbolUi.Sections.Values.ToList(), compositionSymbolUi, targetPosition, copyMode: CopySymbolChildrenCommand.CopyMode.ClipboardSource, sourceSymbol: containerSymbol); var macro = new MacroCommand("Paste"); macro.AddAndExecCommand(cmd); // Pasting into a section near its bottom/right border grows the frame to fit SectionTree.GrowSectionToFitPlacedItems(compositionSymbolUi, targetPosition, cmd.NewSymbolChildIds, macro, nodeSelection); UndoRedoStack.Add(macro); // Select new operators nodeSelection.Clear(); foreach (var id in cmd.NewSymbolChildIds) { var newChildUi = compositionSymbolUi.ChildUis[id]; var instance = compositionOp.Children[id]; nodeSelection.AddSelection(newChildUi, instance); } foreach (var id in cmd.NewSymbolSectionIds) { var section = compositionSymbolUi.Sections[id]; nodeSelection.AddSelection(section); } } catch (Exception e) { Log.Warning("Could not paste selection from clipboard."); Log.Debug("Paste exception: " + e); } } /// /// Pasting values and other properties onto selected nodes works under the following situation: /// - For a single op Symbol (e.g. Remap) /// - its values can be pasted onto all selected operators of that type (but not itself) /// - If there were more than instance of that Symbol, the assignment would be undefined. So nothing should be done. /// - Ideally, special cases like Animation, IsBypassed should also be transferred. /// - If there is only a single Child in the clipboard and the Symbol does NOT match any of the selected children, /// we could still try to paste values onto parameters with identical names. This would be extremely valuable for /// copying values between different versions of ops /// /// public static void PasteValues(NodeSelection nodeSelection, ScalableCanvas canvas, Instance compositionOp) { try { var selectedChildUis = nodeSelection.GetSelectedChildUis().ToList(); if (selectedChildUis.Count ==0) { Log.Debug("Please select ops to paste values into."); return; } var text = EditorUi.Instance.GetClipboardText(); using var reader = new StringReader(text); var jsonReader = new JsonTextReader(reader); if (JToken.ReadFrom(jsonReader, SymbolJson.LoadSettings) is not JArray jArray) return; var symbolJson = jArray[0]; if (!TryGetPastedSymbol(symbolJson, compositionOp.Symbol.SymbolPackage, out var containerSymbol)) { Log.Error("Failed to paste values. Incorrect format."); return; } //var cmd = new MacroCommand("Paste values"); var cmds = new List(); // First pass collect symbol types Dictionary> templateSymbols = []; foreach (var child in containerSymbol.Children.Values) { if (!templateSymbols.TryGetValue(child.Symbol, out var list)) { list = []; templateSymbols.Add(child.Symbol, list); } list.Add(child); } // Filter arbitrary symbol var sourceChildUis = new List(); foreach (var (symbol, symbolChildren) in templateSymbols) { if (symbolChildren.Count > 1) { Log.Debug($"Can't paste values from {symbolChildren.Count} {symbol.Name}. Skipping."); } else { sourceChildUis.Add(symbolChildren[0]); } } var singleSource = sourceChildUis.Count == 1 ? sourceChildUis[0] : null; foreach (var target in selectedChildUis) { var source = sourceChildUis.FirstOrDefault(s => s.Symbol == target.SymbolChild.Symbol); if(source !=null) { Log.Debug(" Found match op for" + target); foreach (var sourceInput in source.Inputs.Values) { if (target.SymbolChild.Inputs.TryGetValue(sourceInput.Id, out var targetInput)) { ApplyChangesToInput(source, sourceInput, target.SymbolChild, targetInput, cmds); } } } else if (singleSource != null) { foreach (var targetInput in target.SymbolChild.Inputs.Values) { var targetInputName = targetInput.InputDefinition.Name; var targetInputType = targetInput.InputDefinition.ValueType; foreach (var sourceInput in singleSource.Inputs.Values) { var singleSourceName = sourceInput.InputDefinition.Name; var singleSourceType = sourceInput.InputDefinition.ValueType; if (singleSourceName == targetInputName && singleSourceType == targetInputType) { ApplyChangesToInput(singleSource, sourceInput, target.SymbolChild, targetInput, cmds); } } } } } if (cmds.Count > 0) { UndoRedoStack.AddAndExecute(new MacroCommand("Paste values", cmds)); } } catch (Exception e) { Log.Warning("Could not paste selection from clipboard."); Log.Debug("Paste exception: " + e); } return; void ApplyChangesToInput(Symbol.Child source, Symbol.Child.Input sourceInput, Symbol.Child target, Symbol.Child.Input targetInput, List commands) { Debug.Assert(source.Parent != null); Debug.Assert(target.Parent != null); // Copy default state if (sourceInput.IsDefault) { if (targetInput.IsDefault) return; commands.Add(new ResetInputToDefault(target.Parent, target.Id, targetInput)); return; } var isSourceAnimated = source.Parent.Animator.IsInputAnimated(source, sourceInput); var isTargetAnimated = target.Parent.Animator.IsInputAnimated(target, targetInput); if (isTargetAnimated || isSourceAnimated) { Log.Debug($" skipping animated parameter {source} {sourceInput}..."); //commands.Add(new RemoveAnimationsCommand(target.Parent.Animator, )); } if (source.Parent.Animator.IsInputAnimated(source, sourceInput)) { commands.Add(new ChangeInputValueCommand(target.Parent, target.Id, targetInput, sourceInput.Value)); //var x = new AddAnimationCommand(target.Parent!.Animator, inputSlot), } else { // Copy values commands.Add(new ChangeInputValueCommand(target.Parent, target.Id, targetInput, sourceInput.Value)); } } } private static bool TryGetPastedSymbol(JToken jToken, SymbolPackage package, [NotNullWhen(true)]out Symbol? symbol) { if (!JsonUtils.TryGetGuid(jToken[SymbolJson.JsonKeys.Id], out var guid)) { Log.Error($"Failed to parse guid in symbol json"); symbol = null; return false; } //var guidString = jToken[SymbolJson.JsonKeys.Id].Value(); // var hasId = Guid.TryParse(guidString, out var guid); // // if (!hasId) // { // Log.Error($"Failed to parse guid in symbol json: `{guidString}`"); // symbol = null; // return false; // } var jsonResult = SymbolJson.ReadSymbolRoot(guid, jToken, typeof(object), package); if (jsonResult.Symbol is null) { symbol = null; return false; } if (SymbolJson.TryReadAndApplySymbolChildren(jsonResult)) { symbol = jsonResult.Symbol; return true; } Log.Error($"Failed to get children of pasted token:\n{jToken}"); symbol = null; return false; } #endregion Copy and paste // /// /// Todo: There must be a better way... /// internal static bool TryGetShaderPath(Instance instance, [NotNullWhen(true)] out string? filePath, [NotNullWhen(true)]out IResourcePackage? owner) { bool found = false; if (instance is IShaderOperator pixelShader) { found = TryGetSourceFile(pixelShader, out filePath, out owner); } else if (instance is IShaderOperator computeShader) { found = TryGetSourceFile(computeShader, out filePath, out owner); } else if (instance is IShaderOperator geometryShader) { found = TryGetSourceFile(geometryShader, out filePath, out owner); } else if (instance is IShaderOperator vertexShader) { found = TryGetSourceFile(vertexShader, out filePath, out owner); } else { filePath = null; owner = null; } return found; static bool TryGetSourceFile(IShaderOperator op, out string filePath, [NotNullWhen(true)] out IResourcePackage? package) where T : AbstractShader { var relative = op.Path.GetCurrentValue(); var instance = op.Instance; return AssetRegistry.TryResolveAddress(relative, instance, out filePath, out package); } } public static void DisconnectDraggedNodes(Instance compositionOp, List draggedNodes) { var removeCommands = new List(); var inputConnections = new List<(Symbol.Connection connection, Type connectionType, bool isMultiIndex, int multiInputIndex)>(); var outputConnections = new List<(Symbol.Connection connection, Type connectionType, bool isMultiIndex, int multiInputIndex)>(); foreach (var node in draggedNodes) { if (node is not SymbolUi.Child childUi) continue; if (!compositionOp.Children.TryGetChildInstance(childUi.Id, out var instance) || instance.Parent == null) { Log.Error("Can't disconnect missing instance"); continue; } // Get all input connections and // relative index if they have multi-index inputs var connectionsToInput = instance.Parent.Symbol.Connections.FindAll(c => c.TargetParentOrChildId == instance.SymbolChildId && draggedNodes.All(c2 => c2.Id != c.SourceParentOrChildId)); var inConnectionInputIndex = 0; foreach (var connectionToInput in connectionsToInput) { bool isMultiInput = instance.Parent.Symbol.IsTargetMultiInput(connectionToInput); if (isMultiInput) { inConnectionInputIndex = instance.Parent.Symbol.GetMultiInputIndexFor(connectionToInput); } Type connectionType = instance.Inputs.Single(c => c.Id == connectionToInput.TargetSlotId).ValueType; inputConnections.Add((connectionToInput, connectionType, isMultiInput, isMultiInput ? inConnectionInputIndex : 0)); } // Get all output connections and // relative index if they have multi-index inputs var connectionsToOutput = instance.Parent.Symbol.Connections.FindAll(c => c.SourceParentOrChildId == instance.SymbolChildId && draggedNodes.All(c2 => c2.Id != c.TargetParentOrChildId)); var outConnectionInputIndex = 0; foreach (var connectionToOutput in connectionsToOutput) { bool isMultiInput = instance.Parent.Symbol.IsTargetMultiInput(connectionToOutput); if (isMultiInput) { outConnectionInputIndex = instance.Parent.Symbol.GetMultiInputIndexFor(connectionToOutput); } Type connectionType = instance.Outputs.Single(c => c.Id == connectionToOutput.SourceSlotId).ValueType; outputConnections.Add((connectionToOutput, connectionType, isMultiInput, isMultiInput ? outConnectionInputIndex : 0)); } } // Remove the input connections in index descending order to // prevent to get the wrong index in case of multi-input properties inputConnections.Sort((x, y) => y.multiInputIndex.CompareTo(x.multiInputIndex)); foreach (var inputConnection in inputConnections) { removeCommands.Add(new DeleteConnectionCommand(compositionOp.Symbol, inputConnection.connection, inputConnection.multiInputIndex)); } // Remove the output connections in index descending order to // prevent to get the wrong index in case of multi-input properties outputConnections.Sort((x, y) => y.multiInputIndex.CompareTo(x.multiInputIndex)); foreach(var outputConnection in outputConnections) { removeCommands.Add(new DeleteConnectionCommand(compositionOp.Symbol, outputConnection.connection, outputConnection.multiInputIndex)); } // Reconnect inputs of first nodes to outputs of last nodes if they are of the same type, // in ascending order. Inputs are paired one-to-one first; once exhausted, a matching // input is reused so a single source fans out to all targets the dragged nodes fed. outputConnections.Sort((x, y) => x.multiInputIndex.CompareTo(y.multiInputIndex)); inputConnections.Sort((x, y) => x.multiInputIndex.CompareTo(y.multiInputIndex)); var unusedInputConnections = new List<(Symbol.Connection connection, Type connectionType, bool isMultiIndex, int multiInputIndex)>(inputConnections); foreach (var itemOutputConnection in outputConnections) { (Symbol.Connection connection, Type connectionType, bool isMultiIndex, int multiInputIndex)? matchingInput = null; foreach (var candidate in unusedInputConnections) { if (candidate.connectionType != itemOutputConnection.connectionType) continue; matchingInput = candidate; unusedInputConnections.Remove(candidate); break; } if (matchingInput == null) { foreach (var candidate in inputConnections) { if (candidate.connectionType != itemOutputConnection.connectionType) continue; matchingInput = candidate; break; } } if (matchingInput == null) continue; var newConnection = new Symbol.Connection(sourceParentOrChildId: matchingInput.Value.connection.SourceParentOrChildId, sourceSlotId: matchingInput.Value.connection.SourceSlotId, targetParentOrChildId: itemOutputConnection.connection.TargetParentOrChildId, targetSlotId: itemOutputConnection.connection.TargetSlotId); removeCommands.Add(new AddConnectionCommand(compositionOp.Symbol, newConnection, itemOutputConnection.multiInputIndex)); } if (removeCommands.Count > 0) { var macro = new MacroCommand("Shake off connections", removeCommands); UndoRedoStack.AddAndExecute(macro); } } }