Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enable Node Autocompletion for output ports of Dynamo nodes. #11638

Merged
merged 10 commits into from
Apr 30, 2021
55 changes: 50 additions & 5 deletions src/DynamoCore/Graph/Nodes/PortModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
using Dynamo.Graph.Connectors;
using Dynamo.Graph.Nodes.ZeroTouch;
using Dynamo.Graph.Workspaces;
using Dynamo.Logging;
using Dynamo.Utilities;
using Newtonsoft.Json;
using ProtoCore.AST.AssociativeAST;
Expand Down Expand Up @@ -403,8 +404,7 @@ internal string GetInputPortType()
{
if (PortType == PortType.Output) return null;

var ztNode = Owner as DSFunction;
if (ztNode != null)
if (Owner is DSFunction ztNode)
{
var fd = ztNode.Controller.Definition;
string type;
Expand All @@ -430,13 +430,58 @@ internal string GetInputPortType()
return type;
}

var nmNode = Owner as NodeModel;
if (nmNode != null)
if (Owner is NodeModel nmNode)
{
var classType = nmNode.GetType();
var inPortAttribute = classType.GetCustomAttributes().OfType<InPortTypesAttribute>().FirstOrDefault();

return inPortAttribute?.PortTypes.ElementAt(Index);
try
{
return inPortAttribute?.PortTypes.ElementAt(Index);
}
catch (Exception e)
{
Log(e.Message);
}
}
return null;
}

/// <summary>
/// Returns the string representation of the fully qualified typename
/// where possible for the port if it's an output port. This method currently
/// returns a valid type for only Zero Touch, Builtin and NodeModel nodes,
/// and returns null otherwise. The string representation of the type also
/// contains the rank information of the type, e.g. Point[], or var[]..[].
/// </summary>
/// <returns>output port type</returns>
internal string GetOutPortType()
{
if (PortType == PortType.Input) return null;

if (Owner is DSFunction ztNode)
{
var fd = ztNode.Controller.Definition;

string type = fd.ReturnType.ToString();

return type;
}

if (Owner is NodeModel nmNode)
{
var classType = nmNode.GetType();

var outPortAttribute = classType.GetCustomAttributes().OfType<OutPortTypesAttribute>().FirstOrDefault();

try
{
return outPortAttribute?.PortTypes.ElementAt(Index);
}
catch(Exception e)
{
Log(e.Message);
}
}
return null;
}
Expand Down
10 changes: 10 additions & 0 deletions src/DynamoCore/Search/SearchElements/NodeSearchElement.cs
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public abstract class NodeSearchElement : ISearchEntry, ISource<NodeModel>
private string assembly;
private bool isVisibleInSearch = true;

internal AutoCompletionNodeElementInfo AutoCompletionNodeElementInfo { get; set; } = new AutoCompletionNodeElementInfo();

/// <summary>
/// Event is fired when a node visibility in library search was changed.
/// </summary>
Expand Down Expand Up @@ -283,6 +285,14 @@ protected virtual IEnumerable<Tuple<string, string>> GenerateInputParameters()
}
}

/// <summary>
/// This class will contain the information related to the node elements of Auto-completion feature.
/// </summary>
internal class AutoCompletionNodeElementInfo
{
internal int PortToConnect { get; set; }
}

/// <summary>
/// This class returns <see cref="NodeSearchElement"/> which is used
/// for creating a node, when the element is drag and dropped.
Expand Down
2 changes: 1 addition & 1 deletion src/DynamoCoreWpf/ViewModels/Core/PortViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -418,7 +418,7 @@ private bool CanAutoComplete(object parameter)
{
DynamoViewModel dynamoViewModel = _node.DynamoViewModel;
// If the feature is enabled from Dynamo experiment setting and if user interaction is on input port.
return dynamoViewModel.EnableNodeAutoComplete && this.PortType == PortType.Input;
return dynamoViewModel.EnableNodeAutoComplete;
reddyashish marked this conversation as resolved.
Show resolved Hide resolved
}

/// <summary>
Expand Down
123 changes: 86 additions & 37 deletions src/DynamoCoreWpf/ViewModels/Search/NodeAutoCompleteSearchViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using Dynamo.Engine;
using Dynamo.Graph.Nodes;
using Dynamo.Properties;
using Dynamo.Search.SearchElements;
using Dynamo.Wpf.ViewModels;
Expand Down Expand Up @@ -39,7 +41,7 @@ private void InitializeDefaultAutoCompleteCandidates()
var candidates = new List<NodeSearchElementViewModel>();
// TODO: These are basic input types in Dynamo
// This should be only served as a temporary default case.
var queries = new List<string>(){"String", "Number Slider", "Integer Slider", "Number", "Boolean" };
var queries = new List<string>(){"String", "Number Slider", "Integer Slider", "Number", "Boolean", "Watch", "Watch 3D", "Python Script"};
foreach (var query in queries)
{
var foundNode = Search(query).FirstOrDefault();
Expand All @@ -57,27 +59,34 @@ internal void PopulateAutoCompleteCandidates()

searchElementsCache = GetMatchingSearchElements().ToList();

// If node match searchElements found, use default suggestions
// If node match searchElements found, use default suggestions.
// These default suggestions will be populated based on the port type.
if (!searchElementsCache.Any())
{
searchElementsCache = DefaultResults.Select(e => e.Model).ToList();
switch (PortViewModel.PortModel.GetInputPortType())
if (PortViewModel.PortModel.PortType == PortType.Input)
{
case "int":
FilteredResults = DefaultResults.Where(e => e.Name == "Number Slider" || e.Name == "Integer Slider").ToList();
break;
case "double":
FilteredResults = DefaultResults.Where(e => e.Name == "Number Slider" || e.Name == "Integer Slider").ToList();
break;
case "string":
FilteredResults = DefaultResults.Where(e => e.Name == "String").ToList();
break;
case "bool":
FilteredResults = DefaultResults.Where(e => e.Name == "Boolean").ToList();
break;
default:
FilteredResults = DefaultResults;
break;
switch (PortViewModel.PortModel.GetInputPortType())
{
case "int":
FilteredResults = DefaultResults.Where(e => e.Name == "Number Slider" || e.Name == "Integer Slider").ToList();
break;
case "double":
FilteredResults = DefaultResults.Where(e => e.Name == "Number Slider" || e.Name == "Integer Slider").ToList();
break;
case "string":
FilteredResults = DefaultResults.Where(e => e.Name == "String").ToList();
break;
case "bool":
FilteredResults = DefaultResults.Where(e => e.Name == "Boolean").ToList();
break;
default:
FilteredResults = DefaultResults.Where(e => e.Name == "String" || e.Name == "Number Slider" || e.Name == "Integer Slider" || e.Name == "Number" || e.Name == "Boolean");
break;
}
}
else
{
FilteredResults = DefaultResults.Where(e => e.Name == "Watch" || e.Name == "Watch 3D" || e.Name == "Python Script").ToList();
}
}
else
Expand Down Expand Up @@ -136,55 +145,95 @@ private bool QuerySearchElements(NodeSearchElement e, string input)
internal IEnumerable<NodeSearchElement> GetMatchingSearchElements()
{
var elements = new List<NodeSearchElement>();
var inputPortType = PortViewModel.PortModel.GetInputPortType();

var portType = String.Empty;

if (PortViewModel.PortModel.PortType == PortType.Input)
{
portType = PortViewModel.PortModel.GetInputPortType();
}
else if (PortViewModel.PortModel.PortType == PortType.Output)
{
portType = PortViewModel.PortModel.GetOutPortType();
}

//List of input types that are skipped temporarily, and will display list of default suggestions instead.
var skippedInputTypes = new List<string>() { "var", "object", "string", "bool", "int", "double" };

if (inputPortType == null)
if (portType == null)
{
return elements;
}

var core = dynamoViewModel.Model.LibraryServices.LibraryManagementCore;

//if inputPortType is an array, use just the typename
var parseResult = ParserUtils.ParseWithCore($"dummyName:{ inputPortType};", core);
var parseResult = ParserUtils.ParseWithCore($"dummyName:{ portType};", core);
var ast = parseResult.CodeBlockNode.Children().FirstOrDefault() as IdentifierNode;
//if parsing the type failed, revert to original string.
inputPortType = ast != null ? ast.datatype.Name : inputPortType;
portType = ast != null ? ast.datatype.Name : portType;

//check if the input port return type is in the skipped input types list
if (skippedInputTypes.Any(s => s == inputPortType))
if (skippedInputTypes.Any(s => s == portType))
{
return elements;
}

//gather all ztsearchelements that are visible in search and filter using inputPortType and zt return type name.
var ztSearchElements = Model.SearchEntries.OfType<ZeroTouchSearchElement>().Where(x => x.IsVisibleInSearch);
foreach (var ztSearchElement in ztSearchElements)

if (PortViewModel.PortModel.PortType == PortType.Input)
{
//for now, remove rank from descriptors
var returnTypeName = ztSearchElement.Descriptor.ReturnType.Name;
foreach (var ztSearchElement in ztSearchElements)
{
//for now, remove rank from descriptors
var returnTypeName = ztSearchElement.Descriptor.ReturnType.Name;

var descriptor = ztSearchElement.Descriptor;
if ((returnTypeName == inputPortType)
|| DerivesFrom(inputPortType, returnTypeName, core))
var descriptor = ztSearchElement.Descriptor;
if ((returnTypeName == portType) || DerivesFrom(portType, returnTypeName, core))
{
elements.Add(ztSearchElement);
}
}

// NodeModel nodes, match any output return type to inputport type name
foreach (var element in Model.SearchEntries.OfType<NodeModelSearchElement>())
{
elements.Add(ztSearchElement);
if (element.OutputParameters.Any(op => op == portType))
{
elements.Add(element);
}
}
}

// NodeModel nodes, match any output return type to inputport type name
foreach (var element in Model.SearchEntries.OfType<NodeModelSearchElement>())
else if (PortViewModel.PortModel.PortType == PortType.Output)
{
if (element.OutputParameters.Any(op => op == inputPortType))
foreach (var ztSearchElement in ztSearchElements)
{
elements.Add(element);
foreach (var inputParameter in ztSearchElement.Descriptor.Parameters.Select((value, index) => new { value, index }))
{
if (inputParameter.value.Type.ToString() == portType || DerivesFrom(inputParameter.value.Type.ToString(), portType, core))
{
ztSearchElement.AutoCompletionNodeElementInfo.PortToConnect = ztSearchElement.Descriptor.Type == FunctionType.InstanceMethod ? inputParameter.index + 1 : inputParameter.index;
elements.Add(ztSearchElement);
break;
}
}
}

// NodeModel nodes, match any output return type to inputport type name
foreach (var element in Model.SearchEntries.OfType<NodeModelSearchElement>())
{
foreach (var inputParameter in element.InputParameters)
{
if (inputParameter.Item2 == portType)
{
elements.Add(element);
}
}
}
}

var comparer = new NodeSearchElementComparer(inputPortType, core);
var comparer = new NodeSearchElementComparer(portType, core);

//first sort by type distance to input port type
elements.Sort(comparer);
Expand Down
26 changes: 19 additions & 7 deletions src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs
Original file line number Diff line number Diff line change
Expand Up @@ -275,16 +275,20 @@ protected virtual void CreateAndConnectToPort(object parameter)
{
// Placing the new node to the right 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, Model.AutoCompletionNodeElementInfo.PortToConnect, adjustedX, 0, createAsDownStreamNode, false, true));
}
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));
// 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));
}

var inputNodes = initialNode.InputNodes.Values.Where(x => x != null).Select(y => y.Item2);

Expand All @@ -306,10 +310,18 @@ private void AutoLayoutNodes(object sender, EventArgs e)
{
var nodeView = (NodeView) sender;
var dynamoViewModel = nodeView.ViewModel.DynamoViewModel;
var originalNodeId = nodeView.ViewModel.NodeModel.OutputNodes.Values.SelectMany(s => s.Select(t => t.Item2)).Distinct().FirstOrDefault().GUID;

dynamoViewModel.CurrentSpace.DoGraphAutoLayout(true, true, originalNodeId);

if (nodeView.ViewModel.NodeModel.OutputNodes.Count() > 0)
{
var originalNodeId = nodeView.ViewModel.NodeModel.OutputNodes.Values.SelectMany(s => s.Select(t => t.Item2)).Distinct().FirstOrDefault().GUID;
dynamoViewModel.CurrentSpace.DoGraphAutoLayout(true, true, originalNodeId);
}
else if (nodeView.ViewModel.NodeModel.InputNodes.Count() > 0)
{
var originalNodeId = nodeView.ViewModel.NodeModel.InputNodes.Values.Select(s => s.Item2).Distinct().FirstOrDefault().GUID;
dynamoViewModel.CurrentSpace.DoGraphAutoLayout(true, true, originalNodeId);
}

DynamoSelection.Instance.ClearSelection();

// Close the undo action group once the node is created, connected and placed.
Expand Down
23 changes: 23 additions & 0 deletions test/DynamoCoreWpfTests/NodeAutoCompleteSearchTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -210,6 +210,29 @@ public void NodeSuggestions_InputPortBuiltInNode_AreCorrect()
Assert.AreEqual(0, suggestions.Count());
}

[Test]
public void NodeSuggestions_OutputPortBuiltInNode_AreCorrect()
{
Open(@"UI\builtin_outputport_suggestion.dyn");

// Get the node view for a specific node in the graph
NodeView nodeView = NodeViewWithGuid(Guid.Parse("a3412b9b1de54205a1fe6904697ffd5f").ToString());

// Get the output port type for the node.
var outPorts = nodeView.ViewModel.OutPorts;
Assert.AreEqual(1, outPorts.Count());

var port = outPorts[0].PortModel;
var type = port.GetOutPortType();
Assert.IsTrue(type.Contains("Line"));

// Trigger node autocomplete on the output port and verify the results.
var searchViewModel = ViewModel.CurrentSpaceViewModel.NodeAutoCompleteSearchViewModel;
searchViewModel.PortViewModel = outPorts[0];
var suggestions = searchViewModel.GetMatchingSearchElements();
Assert.AreEqual(29, suggestions.Count());
}

[Test]
public void NodeSearchElementComparerSortsBasedOnTypeDistance()
{
Expand Down
Loading