From 17f1104ef0b2c8b645f5bbc1a8cc5046b3de544f Mon Sep 17 00:00:00 2001 From: aparajit-pratap Date: Thu, 15 Oct 2020 09:31:20 -0400 Subject: [PATCH] Node auto layout for node autocomplete (#11177) * trial commit * update * place suggested node wrt node height and width * execute graph auto layout command after connecting input node * node auto layout for node autocomplete * revert unnecessary change * revert unnecessary change * cleanup * cleanup * rename parameter * rename variables * add code comments, null check * revert unnecessary change * more code comments * refactor into delegate command on nodesearchelementviewmodel * cleanup * remove unused usings Co-authored-by: tanga --- .../Graph/Workspaces/LayoutExtensions.cs | 22 ++++- src/DynamoCore/Graph/Workspaces/UndoRedo.cs | 15 +++ src/DynamoCore/Models/DynamoModelCommands.cs | 82 +++++++++------- src/DynamoCore/Models/RecordableCommands.cs | 38 +++++++- .../NodeAutoCompleteSearchControl.xaml.cs | 5 +- .../ViewModels/Core/DynamoViewModel.cs | 5 +- .../Search/NodeSearchElementViewModel.cs | 93 +++++++++++++++++-- .../ViewModels/Search/SearchViewModel.cs | 15 +-- 8 files changed, 211 insertions(+), 64 deletions(-) diff --git a/src/DynamoCore/Graph/Workspaces/LayoutExtensions.cs b/src/DynamoCore/Graph/Workspaces/LayoutExtensions.cs index 24de261ce03..54b86564b63 100644 --- a/src/DynamoCore/Graph/Workspaces/LayoutExtensions.cs +++ b/src/DynamoCore/Graph/Workspaces/LayoutExtensions.cs @@ -18,7 +18,9 @@ public static class LayoutExtensions /// This function wraps a few methods on the workspace model layer /// to set up and run the graph layout algorithm. /// - internal static List DoGraphAutoLayout(this WorkspaceModel workspace) + /// Workspace on which graph layout will be performed. + /// If true, skip initializing new undo action group. + internal static List DoGraphAutoLayout(this WorkspaceModel workspace, bool reuseUndoRedoGroup = false) { if (workspace.Nodes.Count() < 2) return null; @@ -32,9 +34,11 @@ public static class LayoutExtensions List layoutSubgraphs; List> subgraphClusters; - GenerateCombinedGraph(workspace, isGroupLayout,out layoutSubgraphs, out subgraphClusters); + GenerateCombinedGraph(workspace, isGroupLayout, out layoutSubgraphs, out subgraphClusters); - RecordUndoGraphLayout(workspace, isGroupLayout); + + RecordUndoGraphLayout(workspace, isGroupLayout, reuseUndoRedoGroup); + // Generate subgraphs separately for each cluster subgraphClusters.ForEach( @@ -194,7 +198,8 @@ private static void GenerateCombinedGraph(this WorkspaceModel workspace, bool is /// /// A . /// True if all the selected models are groups. - private static void RecordUndoGraphLayout(this WorkspaceModel workspace, bool isGroupLayout) + /// Skip creating new undo action group, reuse existing group if true. + private static void RecordUndoGraphLayout(this WorkspaceModel workspace, bool isGroupLayout, bool reuseUndoRedoGroup) { List undoItems = new List(); @@ -221,7 +226,14 @@ private static void RecordUndoGraphLayout(this WorkspaceModel workspace, bool is } } - WorkspaceModel.RecordModelsForModification(undoItems, workspace.UndoRecorder); + if (reuseUndoRedoGroup) + { + workspace.RecordModelsForModification(undoItems); + } + else + { + WorkspaceModel.RecordModelsForModification(undoItems, workspace.UndoRecorder); + } } /// diff --git a/src/DynamoCore/Graph/Workspaces/UndoRedo.cs b/src/DynamoCore/Graph/Workspaces/UndoRedo.cs index 2ea29eb1fce..221a2af8e55 100644 --- a/src/DynamoCore/Graph/Workspaces/UndoRedo.cs +++ b/src/DynamoCore/Graph/Workspaces/UndoRedo.cs @@ -110,6 +110,21 @@ internal static void RecordModelsForModification(List models, UndoRed } } + /// + /// This method assumes an undo-redo action group already exists + /// and records in it models that are modified. + /// + /// + internal void RecordModelsForModification(List models) + { + if (null == UndoRecorder) return; + + if (!ShouldProceedWithRecording(models)) return; + + foreach (var model in models) + UndoRecorder.RecordModificationForUndo(model); + } + internal static void RecordModelsForUndo(Dictionary models, UndoRedoRecorder recorder) { if (null == recorder) diff --git a/src/DynamoCore/Models/DynamoModelCommands.cs b/src/DynamoCore/Models/DynamoModelCommands.cs index 36097e7ac41..43d9512d44f 100644 --- a/src/DynamoCore/Models/DynamoModelCommands.cs +++ b/src/DynamoCore/Models/DynamoModelCommands.cs @@ -83,43 +83,57 @@ private void CreateAndConnectNodeImpl(CreateAndConnectNodeCommand command) { using (CurrentWorkspace.UndoRecorder.BeginActionGroup()) { - var newNode = CreateNodeFromNameOrType(command.ModelGuid, command.NewNodeName); - newNode.X = command.X; - newNode.Y = command.Y; - var existingNode = CurrentWorkspace.GetModelInternal(command.ModelGuids.ElementAt(1)) as NodeModel; - - if(newNode == null || existingNode == null) return; - - AddNodeToCurrentWorkspace(newNode, false, command.AddNewNodeToSelection); - CurrentWorkspace.UndoRecorder.RecordCreationForUndo(newNode); - - PortModel inPortModel, outPortModel; - if (command.CreateAsDownstreamNode) - { - // Connect output port of Existing Node to input port of New node - outPortModel = existingNode.OutPorts[command.OutputPortIndex]; - inPortModel = newNode.InPorts[command.InputPortIndex]; - } - else - { - // Connect output port of New Node to input port of existing node - outPortModel = newNode.OutPorts[command.OutputPortIndex]; - inPortModel = existingNode.InPorts[command.InputPortIndex]; - } + CreateAndConnectNodeImplWithUndoGroup(command); + } + } + + /// + /// This method assumes that there exists an undo-redo action group already + /// that can be used to record creation and deletion of models. + /// + /// + private void CreateAndConnectNodeImplWithUndoGroup(CreateAndConnectNodeCommand command) + { + var newNode = CreateNodeFromNameOrType(command.ModelGuid, command.NewNodeName); + + if (newNode == null) return; - var models = GetConnectorsToAddAndDelete(inPortModel, outPortModel); + newNode.X = command.X; + newNode.Y = command.Y; - foreach (var modelPair in models) + var existingNode = CurrentWorkspace.GetModelInternal(command.ModelGuids.ElementAt(1)) as NodeModel; + + if (existingNode == null) return; + + AddNodeToCurrentWorkspace(newNode, false, command.AddNewNodeToSelection); + CurrentWorkspace.UndoRecorder.RecordCreationForUndo(newNode); + + PortModel inPortModel, outPortModel; + if (command.CreateAsDownstreamNode) + { + // Connect output port of Existing Node to input port of New node + outPortModel = existingNode.OutPorts[command.OutputPortIndex]; + inPortModel = newNode.InPorts[command.InputPortIndex]; + } + else + { + // Connect output port of New Node to input port of existing node + outPortModel = newNode.OutPorts[command.OutputPortIndex]; + inPortModel = existingNode.InPorts[command.InputPortIndex]; + } + + var models = GetConnectorsToAddAndDelete(inPortModel, outPortModel); + + foreach (var modelPair in models) + { + switch (modelPair.Value) { - switch (modelPair.Value) - { - case UndoRedoRecorder.UserAction.Creation: - CurrentWorkspace.UndoRecorder.RecordCreationForUndo(modelPair.Key); - break; - case UndoRedoRecorder.UserAction.Deletion: - CurrentWorkspace.UndoRecorder.RecordDeletionForUndo(modelPair.Key); - break; - } + case UndoRedoRecorder.UserAction.Creation: + CurrentWorkspace.UndoRecorder.RecordCreationForUndo(modelPair.Key); + break; + case UndoRedoRecorder.UserAction.Deletion: + CurrentWorkspace.UndoRecorder.RecordDeletionForUndo(modelPair.Key); + break; } } } diff --git a/src/DynamoCore/Models/RecordableCommands.cs b/src/DynamoCore/Models/RecordableCommands.cs index 7be35438101..f7fb04858be 100644 --- a/src/DynamoCore/Models/RecordableCommands.cs +++ b/src/DynamoCore/Models/RecordableCommands.cs @@ -641,8 +641,6 @@ protected override void SerializeCore(XmlElement element) [DataContract] public class CreateNodeCommand : ModelBasedRecordableCommand { - #region Public Class Methods - private void SetProperties(double x, double y, bool defaultPosition, bool transformCoordinates) { X = x; @@ -651,6 +649,8 @@ private void SetProperties(double x, double y, bool defaultPosition, bool transf TransformCoordinates = transformCoordinates; } + #region Public Class Methods + /// /// /// The node. @@ -817,6 +817,31 @@ internal override void TrackAnalytics() [DataContract] public class CreateAndConnectNodeCommand : ModelBasedRecordableCommand { + private readonly bool reuseUndoRedoGroup; + + /// + /// Creates a new CreateAndConnectNodeCommand with the given inputs + /// + /// + /// + /// The name of node to create + /// + /// + /// + /// + /// + /// new node to be created as downstream or upstream node wrt the existing node + /// + /// select the new node after it is created by default + /// Skip creating new undo action group and reuse existing one if true. + internal CreateAndConnectNodeCommand(Guid newNodeGuid, Guid existingNodeGuid, string newNodeName, + int outPortIndex, int inPortIndex, + double x, double y, bool createAsDownstreamNode, bool addNewNodeToSelection, bool reuseUndoRedoGroup) + : this(newNodeGuid, existingNodeGuid, newNodeName, outPortIndex, inPortIndex, + x, y, createAsDownstreamNode, addNewNodeToSelection) + { + this.reuseUndoRedoGroup = reuseUndoRedoGroup; + } #region Public Class Methods @@ -879,7 +904,14 @@ public CreateAndConnectNodeCommand(Guid newNodeGuid, Guid existingNodeGuid, stri protected override void ExecuteCore(DynamoModel dynamoModel) { - dynamoModel.CreateAndConnectNodeImpl(this); + if (reuseUndoRedoGroup) + { + dynamoModel.CreateAndConnectNodeImplWithUndoGroup(this); + } + else + { + dynamoModel.CreateAndConnectNodeImpl(this); + } } protected override void SerializeCore(XmlElement element) diff --git a/src/DynamoCoreWpf/Controls/NodeAutoCompleteSearchControl.xaml.cs b/src/DynamoCoreWpf/Controls/NodeAutoCompleteSearchControl.xaml.cs index f01ec130bd1..a539f130bae 100644 --- a/src/DynamoCoreWpf/Controls/NodeAutoCompleteSearchControl.xaml.cs +++ b/src/DynamoCoreWpf/Controls/NodeAutoCompleteSearchControl.xaml.cs @@ -88,7 +88,10 @@ private void ExecuteSearchElement(ListBoxItem listBoxItem) { searchElement.Position = ViewModel.InCanvasSearchPosition; PortViewModel port = ViewModel.PortViewModel; - searchElement.CreateAndConnectCommand.Execute(port.PortModel); + if (searchElement.CreateAndConnectCommand.CanExecute(port.PortModel)) + { + searchElement.CreateAndConnectCommand.Execute(port.PortModel); + } } } diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs index 248e23a310e..e3fca35c9d0 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs @@ -1924,7 +1924,10 @@ internal bool CanAlignSelected(object param) public void DoGraphAutoLayout(object parameter) { - this.CurrentSpaceViewModel.GraphAutoLayoutCommand.Execute(parameter); + if (CurrentSpaceViewModel.GraphAutoLayoutCommand.CanExecute(parameter)) + { + CurrentSpaceViewModel.GraphAutoLayoutCommand.Execute(parameter); + } } internal bool CanDoGraphAutoLayout(object parameter) diff --git a/src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs b/src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs index d89987bc175..b1f7e903a05 100644 --- a/src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs @@ -5,9 +5,13 @@ using System.Windows.Input; using System.Windows.Media; using Dynamo.Configuration; +using Dynamo.Controls; using Dynamo.Graph.Nodes; +using Dynamo.Graph.Workspaces; using Dynamo.Logging; +using Dynamo.Models; using Dynamo.Search.SearchElements; +using Dynamo.Selection; using Dynamo.ViewModels; using FontAwesome.WPF; using Microsoft.Practices.Prism.Commands; @@ -20,6 +24,7 @@ public class NodeSearchElementViewModel : ViewModelBase, ISearchEntryViewModel private bool isSelected; private SearchViewModel searchViewModel; + private IDisposable undoRecorderGroup; public event RequestBitmapSourceHandler RequestBitmapSource; public void OnRequestBitmapSource(IconRequestEventArgs e) @@ -44,10 +49,9 @@ public NodeSearchElementViewModel(NodeSearchElement element, SearchViewModel svm if (searchViewModel != null) { Clicked += searchViewModel.OnSearchElementClicked; - CreateAndConnectToPort += searchViewModel.OnRequestConnectToPort; } ClickedCommand = new DelegateCommand(OnClicked); - CreateAndConnectCommand = new DelegateCommand(OnRequestCreateAndConnectToPort); + CreateAndConnectCommand = new DelegateCommand(CreateAndConnectToPort, CanCreateAndConnectToPort); LoadFonts(); } @@ -62,6 +66,10 @@ public override void Dispose() Model.VisibilityChanged -= ModelOnVisibilityChanged; if (searchViewModel != null) { + if (RequestBitmapSource != null) + { + RequestBitmapSource -= searchViewModel.SearchViewModelRequestBitmapSource; + } Clicked -= searchViewModel.OnSearchElementClicked; searchViewModel = null; } @@ -233,14 +241,85 @@ public ImageSource LargeIcon /// /// Create the search element as node and connect to target port /// - public ICommand CreateAndConnectCommand { get; private set; } + public ICommand CreateAndConnectCommand { get; } - public event Action CreateAndConnectToPort; - protected virtual void OnRequestCreateAndConnectToPort(PortModel portModel) + /// + /// Create new node for search element, connect to port and place using graph auto layout. + /// + /// Port model to connect to + protected virtual void CreateAndConnectToPort(object parameter) { - if (CreateAndConnectToPort != null) + var portModel = (PortModel) parameter; + var dynamoViewModel = searchViewModel.dynamoViewModel; + + // Initialize a new undo action group before calling + // node CreateAndConnect and AutoLayout commands. + if (undoRecorderGroup == null) { - CreateAndConnectToPort(Model.CreationName, portModel); + undoRecorderGroup = dynamoViewModel.CurrentSpace.UndoRecorder.BeginActionGroup(); + + // Node auto layout can be performed correctly only when the positions and sizes + // of nodes are known, which is possible only after the node views are ready. + dynamoViewModel.NodeViewReady += AutoLayoutNodes; + } + + var initialNode = portModel.Owner; + var initialNodeVm = dynamoViewModel.CurrentSpaceViewModel.Nodes.FirstOrDefault(x => x.Id == initialNode.GUID); + var id = Guid.NewGuid(); + + var adjustedX = initialNodeVm.X; + + var createAsDownStreamNode = portModel.PortType == PortType.Output; + // Placing the new node based on which input port it is connecting to. + if (createAsDownStreamNode) + { + // Placing the new node to the right of initial node + adjustedX += initialNode.Width + 50; + } + else + { + // Placing the new node to the left of initial node + adjustedX -= initialNode.Width + 50; + } + + // Create a new node based on node creation name and connection ports + dynamoViewModel.ExecuteCommand(new DynamoModel.CreateAndConnectNodeCommand(id, initialNode.GUID, + Model.CreationName, 0, portModel.Index, adjustedX, 0, createAsDownStreamNode, false, true)); + + // Clear current selections and select all input nodes as we need to perform Auto layout on only the input nodes. + DynamoSelection.Instance.ClearSelection(); + var inputNodes = initialNode.InputNodes.Values.Where(x => x != null).Select(y => y.Item2); + + foreach (var inputNode in inputNodes) + { + DynamoSelection.Instance.Selection.AddUnique(inputNode); + } + } + + protected virtual bool CanCreateAndConnectToPort(object parameter) + { + // Do not auto connect code block node since default code block node do not have output port + if (Model.CreationName.Contains("Code Block")) return false; + + return true; + } + + private void AutoLayoutNodes(object sender, EventArgs e) + { + var nodeView = (NodeView) sender; + var dynamoViewModel = nodeView.ViewModel.DynamoViewModel; + + dynamoViewModel.CurrentSpace.DoGraphAutoLayout(true); + + DynamoSelection.Instance.ClearSelection(); + + // Close the undo action group once the node is created, connected and placed. + if (undoRecorderGroup != null) + { + undoRecorderGroup.Dispose(); + undoRecorderGroup = null; + + dynamoViewModel.NodeViewReady -= AutoLayoutNodes; } } diff --git a/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs b/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs index 2c0c8db2c8f..90184b076ab 100644 --- a/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs @@ -345,6 +345,7 @@ public override void Dispose() } Model.EntryUpdated -= UpdateEntry; Model.EntryRemoved -= RemoveEntry; + base.Dispose(); } @@ -771,7 +772,7 @@ private void AddEntryToExistingCategory(NodeCategoryViewModel category, } } - protected void SearchViewModelRequestBitmapSource(IconRequestEventArgs e) + internal void SearchViewModelRequestBitmapSource(IconRequestEventArgs e) { var warehouse = iconServices.GetForAssembly(e.IconAssembly, e.UseAdditionalResolutionPaths); ImageSource icon = null; @@ -1059,18 +1060,6 @@ public void OnSearchElementClicked(NodeModel nodeModel, Point position) OnRequestFocusSearch(); } - internal void OnRequestConnectToPort(string nodeCreationName, PortModel portModel) - { - if (!nodeCreationName.Contains("Code Block")) - { - // Create a new node based on node creation name and connect ports - dynamoViewModel.ExecuteCommand(new DynamoModel.CreateAndConnectNodeCommand(Guid.NewGuid(), portModel.Owner.GUID, - nodeCreationName, 0, portModel.Index, portModel.CenterX - 50, portModel.CenterY + 200, false, false)); - } - - OnRequestFocusSearch(); - } - #endregion #region Commands