Skip to content
7 changes: 5 additions & 2 deletions src/DynamoCore/Graph/Workspaces/LayoutExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,11 @@ public static class LayoutExtensions

GenerateCombinedGraph(workspace, isGroupLayout, out layoutSubgraphs, out subgraphClusters);


RecordUndoGraphLayout(workspace, isGroupLayout, reuseUndoRedoGroup);
//only record graph layout undo when it is not node autocomplete
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Curious what does this mean?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Since we are using the layout method that exists in Dynamo already, it adds an undo for the auto layout. So if we skip it when triggered from node autocomplete it helps minimize the undos

if (!isNodeAutoComplete)
{
RecordUndoGraphLayout(workspace, isGroupLayout, reuseUndoRedoGroup);
}

// Generate subgraphs separately for each cluster
subgraphClusters.ForEach(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@
using Dynamo.Wpf.Utilities;
using Dynamo.ViewModels;
using System.Reflection;
using Dynamo.Core;
using Dynamo.Graph.Workspaces;
using Dynamo.Graph;

namespace Dynamo.NodeAutoComplete.ViewModels
{
Expand Down Expand Up @@ -780,6 +783,11 @@ internal void DeleteTransientNodes()
if (transientNodes.Any())
{
dynamoViewModel.Model.ExecuteCommand(new DynamoModel.DeleteModelCommand(transientNodes.Select(x => x.Id)));

//remove the initial layout of the transient nodes from the undo stack
wsViewModel.Model.UndoRecorder.PopFromUndoGroup();
//remove the deletion of the transient nodes from the undo stack
wsViewModel.Model.UndoRecorder.PopFromUndoGroup();
}
}

Expand All @@ -802,16 +810,26 @@ internal void AddCluster(ClusterResultItem ClusterResultItem)

List<List<NodeItem>> nodeStacks = NodeAutoCompleteUtilities.ComputeNodePlacementHeuristics(clusterConnections, clusterNodes);

//store our nodes and wires to allow for one undo
List<ModelBase> newNodesAndWires = new List<ModelBase>();

double xoffset = node.X + node.NodeModel.Width;
foreach (var nodeStack in nodeStacks)
{
xoffset += node.NodeModel.Width;
foreach(var newNode in nodeStack)
{
// Retreive assembly name and node full name from type.id.
// Retrieve assembly name and node full name from type.id.
var typeInfo = wsViewModel.NodeAutoCompleteSearchViewModel.GetInfoFromTypeId(newNode.Type.Id);
dynamoViewModel.Model.ExecuteCommand(new DynamoModel.CreateNodeCommand(Guid.NewGuid().ToString(), typeInfo.FullName, xoffset, node.NodeModel.Y, false, false));

//disallow the node creation command from the undo group, we group node creation and wires below
wsViewModel.Model.UndoRecorder.PopFromUndoGroup();
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

oh interesting, I guess with this we can keep the current implementation with commands?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes! :)


var nodeFromCluster = wsViewModel.Nodes.LastOrDefault();

newNodesAndWires.Add(nodeFromCluster.NodeModel);

nodeFromCluster.IsTransient = true;
nodeFromCluster.IsHidden = true;
clusterMapping.Add(newNode.Id, nodeFromCluster);
Expand All @@ -824,33 +842,30 @@ internal void AddCluster(ClusterResultItem ClusterResultItem)
index++;
}
}
using (var undoGroup = wsViewModel.Model.UndoRecorder.BeginActionGroup())

clusterConnections.ForEach(connection =>
{
clusterConnections.ForEach(connection =>
{
// Connect the nodes
var sourceNode = clusterMapping[connection.StartNode.NodeId].NodeModel;
var targetNode = clusterMapping[connection.EndNode.NodeId].NodeModel;
// The port index is 1- based (currently a hack and not expected from service)
var sourcePort = sourceNode.OutPorts.FirstOrDefault(p => p.Index == connection.StartNode.PortIndex - 1);
var targetPort = targetNode.InPorts.FirstOrDefault(p => p.Index == connection.EndNode.PortIndex - 1);
// Connect the nodes
var sourceNode = clusterMapping[connection.StartNode.NodeId].NodeModel;
var targetNode = clusterMapping[connection.EndNode.NodeId].NodeModel;
// The port index is 1- based (currently a hack and not expected from service)
var sourcePort = sourceNode.OutPorts.FirstOrDefault(p => p.Index == connection.StartNode.PortIndex - 1);
var targetPort = targetNode.InPorts.FirstOrDefault(p => p.Index == connection.EndNode.PortIndex - 1);

if (targetPort != null && targetPort.Connectors.Count == 0)
{
var connector = ConnectorModel.Make(sourceNode, targetNode, connection.StartNode.PortIndex - 1, connection.EndNode.PortIndex - 1);
if (connector != null && !targetPort.IsConnected)
{
wsViewModel.Model.UndoRecorder.RecordCreationForUndo(connector);
}
});

// Connect the cluster to the original node and port
var connector = ConnectorModel.Make(node.NodeModel, targetNodeFromCluster.NodeModel, 0, ClusterResultItem.EntryNodeInPort);
if (connector != null && !targetNodeFromCluster.OutPorts.FirstOrDefault().IsConnected)
{
wsViewModel.Model.UndoRecorder.RecordCreationForUndo(connector);
newNodesAndWires.Add(connector);
}
}

//Make connectors invisible ( just like the cluster nodes ) before they get a chance to be drawn.
});

// Connect the cluster to the original node and port
var connector = ConnectorModel.Make(node.NodeModel, targetNodeFromCluster.NodeModel, 0, ClusterResultItem.EntryNodeInPort);
newNodesAndWires.Add(connector);

// Make connectors invisible ( just like the cluster nodes ) before they get a chance to be drawn.
var clusterNodesModel = clusterMapping.Values.ToList();
clusterNodesModel.ForEach(nodeInCluster => nodeInCluster?.NodeModel?.AllConnectors?.ToList().ForEach(connector =>
{
Expand All @@ -872,7 +887,25 @@ internal void AddCluster(ClusterResultItem ClusterResultItem)

// AutoLayout should be called after all nodes are connected.
NodeAutoCompleteUtilities.PostAutoLayoutNodes(wsViewModel.DynamoViewModel.CurrentSpace, node.NodeModel, clusterNodesModel.Select(x => x.NodeModel), false, false, false, finalizer);

//record all node and wire creation as one undo
RecordUndoModels(wsViewModel.Model, newNodesAndWires);
}

private void RecordUndoModels(WorkspaceModel workspace, List<ModelBase> undoItems)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Saw a similar implementation somewhere else, maybe we dont have to repeat it and can put this as a utility function in the DynamoUtility class/project?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I went ahead and reused the method from the DynamoModel (where it is currently).

{
var userActionDictionary = new Dictionary<ModelBase, UndoRedoRecorder.UserAction>();
//Add models that were newly created
foreach (var undoItem in undoItems)
{
if(undoItem is null) continue;

userActionDictionary.Add(undoItem, UndoRedoRecorder.UserAction.Creation);
}

WorkspaceModel.RecordModelsForUndo(userActionDictionary, workspace.UndoRecorder);
}

/// <summary>
/// Key function to populate node autocomplete results to display
/// </summary>
Expand All @@ -890,17 +923,17 @@ internal void PopulateClusterAutoComplete()
fullResults = GetMLNodeClusterAutocompleteResults();
var comboboxResults = QualifiedResults.Select(x => new NodeAutoCompleteClusterResult { Description = x.Description });
dynamoViewModel.UIDispatcher.BeginInvoke(() =>
{
{
if (!IsOpen)
{
// view dissapeared while the background thread was waiting for the server response.
// Ignore the results are we're no longer interested.
return;
}

// this runs synchronously on the UI thread, so the UI can't dissapear during execution
ClusterResults = comboboxResults;
if(QualifiedResults.Any())
if (QualifiedResults.Any())
{
var ClusterResultItem = QualifiedResults.First();
AddCluster(ClusterResultItem);
Expand Down Expand Up @@ -1058,7 +1091,7 @@ internal void SearchAutoCompleteCandidates(string input)

//Write the Lucene documents to memory
LuceneUtility.CommitWriterChanges();

var luceneResults = SearchNodeAutocomplete(input);
var foundNodesModels = luceneResults.Select(x => x.Model);
var foundNodes = foundNodesModels.Select(MakeNodeSearchElementVM);
Expand Down Expand Up @@ -1129,9 +1162,9 @@ internal IEnumerable<NodeSearchElement> GetMatchingSearchElements()
ParseResult parseResult = null;
try
{
parseResult = ParserUtils.ParseWithCore($"dummyName:{ portType};", core);
parseResult = ParserUtils.ParseWithCore($"dummyName:{portType};", core);
}
catch(ProtoCore.BuildHaltException)
catch (ProtoCore.BuildHaltException)
{ }

var ast = parseResult?.CodeBlockNode.Children().FirstOrDefault() as IdentifierNode;
Expand Down Expand Up @@ -1205,7 +1238,7 @@ internal IEnumerable<NodeSearchElement> GetMatchingSearchElements()
elements.Sort(comparer);
//then sort by node library group (create, action, or query node)
//this results in a list of elements with 3 major groups(create,action,query), each group is sub sorted into types and then sorted by name.
return elements.OrderBy(x => x.Group).ThenBy(x => x.OutputParameters.FirstOrDefault().ToString()).ThenBy(x => x.Name);
return elements.OrderBy(x => x.Group).ThenBy(x => x.OutputParameters.FirstOrDefault().ToString()).ThenBy(x => x.Name);
}

/// <summary>
Expand Down Expand Up @@ -1292,7 +1325,7 @@ 1 x is after y.
else
{
//if inputPortType is an array, lets just use the class name to match
xTypeNames = xTypeNames.Select(type => (ParserUtils.ParseWithCore($"dummyName:{ type};", core).
xTypeNames = xTypeNames.Select(type => (ParserUtils.ParseWithCore($"dummyName:{type};", core).
CodeBlockNode.Children().ElementAt(0) as TypedIdentifierNode).datatype.Name);
}
if (y is ZeroTouchSearchElement yzt)
Expand All @@ -1302,7 +1335,7 @@ 1 x is after y.
// for non ZT nodes, we don't have concrete return types, so we need to parse the typename.
else
{
yTypeNames = yTypeNames.Select(type => (ParserUtils.ParseWithCore($"dummyName:{ type};", core).
yTypeNames = yTypeNames.Select(type => (ParserUtils.ParseWithCore($"dummyName:{type};", core).
CodeBlockNode.Children().ElementAt(0) as TypedIdentifierNode).datatype.Name);
}

Expand Down
Loading