diff --git a/src/DynamoCore/Configuration/PreferenceSettings.cs b/src/DynamoCore/Configuration/PreferenceSettings.cs index 59bb9be117d..0045e373c7e 100644 --- a/src/DynamoCore/Configuration/PreferenceSettings.cs +++ b/src/DynamoCore/Configuration/PreferenceSettings.cs @@ -333,6 +333,11 @@ public string PythonTemplateFilePath /// public bool ShowTabsAndSpacesInScriptEditor { get; set; } + /// + /// This defines if user wants to see the enabled node Auto Complete feature for port interaction. + /// + public bool EnableNodeAutoComplete { get; set; } + /// /// Engine used by default for new Python script and string nodes. If not empty, this takes precedence over any system settings. /// @@ -408,6 +413,7 @@ public PreferenceSettings() PythonTemplateFilePath = ""; IsIronPythonDialogDisabled = false; ShowTabsAndSpacesInScriptEditor = false; + EnableNodeAutoComplete = false; DefaultPythonEngine = string.Empty; } diff --git a/src/DynamoCore/Graph/Nodes/PortModel.cs b/src/DynamoCore/Graph/Nodes/PortModel.cs index 9bd3b3baaf2..8bbaa513bee 100644 --- a/src/DynamoCore/Graph/Nodes/PortModel.cs +++ b/src/DynamoCore/Graph/Nodes/PortModel.cs @@ -2,9 +2,12 @@ using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; +using System.Reflection; using System.Xml; using Dynamo.Configuration; +using Dynamo.Engine; using Dynamo.Graph.Connectors; +using Dynamo.Graph.Nodes.ZeroTouch; using Dynamo.Graph.Workspaces; using Dynamo.Utilities; using Newtonsoft.Json; @@ -386,6 +389,55 @@ public override int GetHashCode() return GUID.GetHashCode(); } + /// + /// Returns the string representation of the fully qualified typename + /// where possible for the port if it's an input 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[]..[]. + /// + /// input port type + internal string GetInputPortType() + { + if (PortType == PortType.Output) return null; + + var ztNode = Owner as DSFunction; + if (ztNode != null) + { + var fd = ztNode.Controller.Definition; + string type; + // In the case of a node for an instance method, the first port + // type is the declaring class type of the method itself. + if (fd.Type == FunctionType.InstanceMethod) + { + if (Index > 0) + { + var param = fd.Parameters.ElementAt(Index - 1); + type = param.Type.ToString(); + } + else + { + type = fd.ClassName; + } + } + else + { + var param = fd.Parameters.ElementAt(Index); + type = param.Type.ToString(); + } + return type; + } + + var nmNode = Owner as NodeModel; + if (nmNode != null) + { + var classType = nmNode.GetType(); + var inPortAttribute = classType.GetCustomAttributes().OfType().FirstOrDefault(); + + return inPortAttribute?.PortTypes.ElementAt(Index); + } + return null; + } } /// diff --git a/src/DynamoCore/Search/SearchElements/NodeSearchElement.cs b/src/DynamoCore/Search/SearchElements/NodeSearchElement.cs index a76902d9201..2cb2da77bc6 100644 --- a/src/DynamoCore/Search/SearchElements/NodeSearchElement.cs +++ b/src/DynamoCore/Search/SearchElements/NodeSearchElement.cs @@ -171,7 +171,7 @@ public SearchElementGroup Group } /// - /// Group to which Node belongs to + /// Assembly to which Node belongs to /// public string Assembly { diff --git a/src/DynamoCoreWpf/Commands/PortCommands.cs b/src/DynamoCoreWpf/Commands/PortCommands.cs index b3400e9112e..35383aa57ff 100644 --- a/src/DynamoCoreWpf/Commands/PortCommands.cs +++ b/src/DynamoCoreWpf/Commands/PortCommands.cs @@ -4,7 +4,8 @@ namespace Dynamo.ViewModels { public partial class PortViewModel { - private DelegateCommand _connectCommand; + private DelegateCommand connectCommand; + private DelegateCommand autoCompleteCommand; private DelegateCommand portMouseEnterCommand; private DelegateCommand portMouseLeaveCommand; private DelegateCommand portMouseLeftButtonCommand; @@ -15,10 +16,24 @@ public DelegateCommand ConnectCommand { get { - if(_connectCommand == null) - _connectCommand = new DelegateCommand(Connect, CanConnect); + if(connectCommand == null) + connectCommand = new DelegateCommand(Connect, CanConnect); - return _connectCommand; + return connectCommand; + } + } + + /// + /// Command to trigger Node Auto Complete from node port interaction + /// + public DelegateCommand NodeAutoCompleteCommand + { + get + { + if (autoCompleteCommand == null) + autoCompleteCommand = new DelegateCommand(AutoComplete, CanAutoComplete); + + return autoCompleteCommand; } } diff --git a/src/DynamoCoreWpf/Commands/WorkspaceCommands.cs b/src/DynamoCoreWpf/Commands/WorkspaceCommands.cs index a18414c5c65..364ba07953a 100644 --- a/src/DynamoCoreWpf/Commands/WorkspaceCommands.cs +++ b/src/DynamoCoreWpf/Commands/WorkspaceCommands.cs @@ -12,23 +12,20 @@ public partial class WorkspaceViewModel { #region Private Delegate Command Data Members - private DelegateCommand _hideCommand; - private DelegateCommand _setCurrentOffsetCommand; - private DelegateCommand _nodeFromSelectionCommand; - private DelegateCommand _setZoomCommand; - private DelegateCommand _resetFitViewToggleCommand; - private DelegateCommand _findByIdCommand; - private DelegateCommand _alignSelectedCommand; - private DelegateCommand _setArgumentLacingCommand; - private DelegateCommand _findNodesFromSelectionCommand; - private DelegateCommand _selectAllCommand; - private DelegateCommand _graphAutoLayoutCommand; - private DelegateCommand _pauseVisualizationManagerUpdateCommand; - private DelegateCommand _unpauseVisualizationManagerUpdateCommand; - private DelegateCommand _showHideAllGeometryPreviewCommand; - private DelegateCommand _showInCanvasSearchCommand; - private DelegateCommand _pasteCommand; - private DelegateCommand _computeRunStateCommand; + private DelegateCommand hideCommand; + private DelegateCommand setCurrentOffsetCommand; + private DelegateCommand nodeFromSelectionCommand; + private DelegateCommand setZoomCommand; + private DelegateCommand resetFitViewToggleCommand; + private DelegateCommand findByIdCommand; + private DelegateCommand alignSelectedCommand; + private DelegateCommand setArgumentLacingCommand; + private DelegateCommand findNodesFromSelectionCommand; + private DelegateCommand selectAllCommand; + private DelegateCommand graphAutoLayoutCommand; + private DelegateCommand showHideAllGeometryPreviewCommand; + private DelegateCommand showInCanvasSearchCommand; + private DelegateCommand pasteCommand; #endregion @@ -43,7 +40,7 @@ public DelegateCommand CopyCommand [JsonIgnore] public DelegateCommand PasteCommand { - get { return _pasteCommand ?? (_pasteCommand = new DelegateCommand(Paste, DynamoViewModel.CanPaste)); } + get { return pasteCommand ?? (pasteCommand = new DelegateCommand(Paste, DynamoViewModel.CanPaste)); } } [JsonIgnore] @@ -51,9 +48,9 @@ public DelegateCommand SelectAllCommand { get { - if(_selectAllCommand == null) - _selectAllCommand = new DelegateCommand(SelectAll, CanSelectAll); - return _selectAllCommand; + if(selectAllCommand == null) + selectAllCommand = new DelegateCommand(SelectAll, CanSelectAll); + return selectAllCommand; } } @@ -61,8 +58,8 @@ public DelegateCommand SelectAllCommand public DelegateCommand GraphAutoLayoutCommand { get { - return _graphAutoLayoutCommand - ?? (_graphAutoLayoutCommand = + return graphAutoLayoutCommand + ?? (graphAutoLayoutCommand = new DelegateCommand(DoGraphAutoLayout, CanDoGraphAutoLayout)); } } @@ -86,10 +83,10 @@ public DelegateCommand HideCommand { get { - if(_hideCommand == null) - _hideCommand = new DelegateCommand(Hide, CanHide); + if(hideCommand == null) + hideCommand = new DelegateCommand(Hide, CanHide); - return _hideCommand; + return hideCommand; } } @@ -98,10 +95,10 @@ public DelegateCommand SetCurrentOffsetCommand { get { - if(_setCurrentOffsetCommand == null) - _setCurrentOffsetCommand = new DelegateCommand(SetCurrentOffset, CanSetCurrentOffset); + if(setCurrentOffsetCommand == null) + setCurrentOffsetCommand = new DelegateCommand(SetCurrentOffset, CanSetCurrentOffset); - return _setCurrentOffsetCommand; + return setCurrentOffsetCommand; } } @@ -110,10 +107,10 @@ public DelegateCommand NodeFromSelectionCommand { get { - if(_nodeFromSelectionCommand == null) - _nodeFromSelectionCommand = new DelegateCommand(CreateNodeFromSelection, CanCreateNodeFromSelection); + if(nodeFromSelectionCommand == null) + nodeFromSelectionCommand = new DelegateCommand(CreateNodeFromSelection, CanCreateNodeFromSelection); - return _nodeFromSelectionCommand; + return nodeFromSelectionCommand; } } @@ -122,9 +119,9 @@ public DelegateCommand SetZoomCommand { get { - if(_setZoomCommand == null) - _setZoomCommand = new DelegateCommand(SetZoom, CanSetZoom); - return _setZoomCommand; + if(setZoomCommand == null) + setZoomCommand = new DelegateCommand(SetZoom, CanSetZoom); + return setZoomCommand; } } @@ -133,9 +130,9 @@ public DelegateCommand ResetFitViewToggleCommand { get { - if (_resetFitViewToggleCommand == null) - _resetFitViewToggleCommand = new DelegateCommand(ResetFitViewToggle, CanResetFitViewToggle); - return _resetFitViewToggleCommand; + if (resetFitViewToggleCommand == null) + resetFitViewToggleCommand = new DelegateCommand(ResetFitViewToggle, CanResetFitViewToggle); + return resetFitViewToggleCommand; } } @@ -144,10 +141,10 @@ public DelegateCommand FindByIdCommand { get { - if(_findByIdCommand == null) - _findByIdCommand = new DelegateCommand(FindById, CanFindById); + if(findByIdCommand == null) + findByIdCommand = new DelegateCommand(FindById, CanFindById); - return _findByIdCommand; + return findByIdCommand; } } @@ -156,10 +153,10 @@ public DelegateCommand AlignSelectedCommand { get { - if(_alignSelectedCommand == null) - _alignSelectedCommand = new DelegateCommand(AlignSelected, CanAlignSelected); + if(alignSelectedCommand == null) + alignSelectedCommand = new DelegateCommand(AlignSelected, CanAlignSelected); - return _alignSelectedCommand; + return alignSelectedCommand; } } @@ -168,13 +165,13 @@ public DelegateCommand SetArgumentLacingCommand { get { - if (_setArgumentLacingCommand == null) + if (setArgumentLacingCommand == null) { - _setArgumentLacingCommand = new DelegateCommand( + setArgumentLacingCommand = new DelegateCommand( SetArgumentLacing, p => HasSelection); } - return _setArgumentLacingCommand; + return setArgumentLacingCommand; } } @@ -183,10 +180,10 @@ public DelegateCommand FindNodesFromSelectionCommand { get { - if(_findNodesFromSelectionCommand == null) - _findNodesFromSelectionCommand = new DelegateCommand(FindNodesFromSelection, CanFindNodesFromSelection); + if(findNodesFromSelectionCommand == null) + findNodesFromSelectionCommand = new DelegateCommand(FindNodesFromSelection, CanFindNodesFromSelection); - return _findNodesFromSelectionCommand; + return findNodesFromSelectionCommand; } } @@ -195,13 +192,13 @@ public DelegateCommand ShowHideAllGeometryPreviewCommand { get { - if (_showHideAllGeometryPreviewCommand == null) + if (showHideAllGeometryPreviewCommand == null) { - _showHideAllGeometryPreviewCommand = new DelegateCommand( + showHideAllGeometryPreviewCommand = new DelegateCommand( ShowHideAllGeometryPreview); } - return _showHideAllGeometryPreviewCommand; + return showHideAllGeometryPreviewCommand; } } @@ -211,13 +208,12 @@ public DelegateCommand ShowInCanvasSearchCommand { get { - if (_showInCanvasSearchCommand == null) - _showInCanvasSearchCommand = new DelegateCommand(OnRequestShowInCanvasSearch); + if (showInCanvasSearchCommand == null) + showInCanvasSearchCommand = new DelegateCommand(OnRequestShowInCanvasSearch); - return _showInCanvasSearchCommand; + return showInCanvasSearchCommand; } } - #endregion #region Properties for Command Data Binding diff --git a/src/DynamoCoreWpf/Controls/NodeAutoCompleteSearchControl.xaml b/src/DynamoCoreWpf/Controls/NodeAutoCompleteSearchControl.xaml new file mode 100644 index 00000000000..7efa7fc889b --- /dev/null +++ b/src/DynamoCoreWpf/Controls/NodeAutoCompleteSearchControl.xaml @@ -0,0 +1,152 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/DynamoCoreWpf/Controls/NodeAutoCompleteSearchControl.xaml.cs b/src/DynamoCoreWpf/Controls/NodeAutoCompleteSearchControl.xaml.cs new file mode 100644 index 00000000000..f01ec130bd1 --- /dev/null +++ b/src/DynamoCoreWpf/Controls/NodeAutoCompleteSearchControl.xaml.cs @@ -0,0 +1,258 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Controls.Primitives; +using System.Windows.Data; +using System.Windows.Input; +using System.Windows.Threading; +using Dynamo.Utilities; +using Dynamo.ViewModels; +using Dynamo.Wpf.ViewModels; + +namespace Dynamo.UI.Controls +{ + /// + /// Interaction logic for AutoCompleteSearchControl.xaml + /// Notice this control shares a lot of logic with InCanvasSearchControl for now + /// But they will diverge eventually because of UI improvements to auto complete. + /// + public partial class NodeAutoCompleteSearchControl + { + ListBoxItem HighlightedItem; + + internal event Action RequestShowNodeAutoCompleteSearch; + + public NodeAutoCompleteSearchViewModel ViewModel + { + get { return DataContext as NodeAutoCompleteSearchViewModel; } + } + + public NodeAutoCompleteSearchControl() + { + InitializeComponent(); + if (Application.Current != null) + { + Application.Current.Deactivated += currentApplicationDeactivated; + } + Unloaded += NodeAutoCompleteSearchControl_Unloaded; + + } + + private void NodeAutoCompleteSearchControl_Unloaded(object sender, RoutedEventArgs e) + { + if (Application.Current != null) + { + Application.Current.Deactivated -= currentApplicationDeactivated; + } + } + + private void currentApplicationDeactivated(object sender, EventArgs e) + { + OnRequestShowNodeAutoCompleteSearch(ShowHideFlags.Hide); + } + + private void OnRequestShowNodeAutoCompleteSearch(ShowHideFlags flags) + { + if (RequestShowNodeAutoCompleteSearch != null) + { + RequestShowNodeAutoCompleteSearch(flags); + } + } + + private void OnSearchTextBoxTextChanged(object sender, TextChangedEventArgs e) + { + BindingExpression binding = ((TextBox)sender).GetBindingExpression(TextBox.TextProperty); + if (binding != null) + binding.UpdateSource(); + + if (ViewModel != null) + ViewModel.SearchCommand.Execute(null); + } + + private void OnMouseLeftButtonUp(object sender, MouseButtonEventArgs e) + { + var listBoxItem = sender as ListBoxItem; + if (listBoxItem == null || e.OriginalSource is Thumb) return; + + ExecuteSearchElement(listBoxItem); + OnRequestShowNodeAutoCompleteSearch(ShowHideFlags.Hide); + e.Handled = true; + } + + private void ExecuteSearchElement(ListBoxItem listBoxItem) + { + var searchElement = listBoxItem.DataContext as NodeSearchElementViewModel; + if (searchElement != null) + { + searchElement.Position = ViewModel.InCanvasSearchPosition; + PortViewModel port = ViewModel.PortViewModel; + searchElement.CreateAndConnectCommand.Execute(port.PortModel); + } + } + + private void OnMouseEnter(object sender, MouseEventArgs e) + { + FrameworkElement fromSender = sender as FrameworkElement; + if (fromSender == null) return; + + toolTipPopup.DataContext = fromSender.DataContext; + toolTipPopup.IsOpen = true; + } + + private void OnMouseLeave(object sender, MouseEventArgs e) + { + toolTipPopup.DataContext = null; + toolTipPopup.IsOpen = false; + } + + private void OnNodeAutoCompleteSearchControlVisibilityChanged(object sender, DependencyPropertyChangedEventArgs e) + { + // If visibility is false, then stop processing it. + if (!(bool)e.NewValue) + return; + + // When launching this control, always start with clear search term. + SearchTextBox.Clear(); + + // Visibility of textbox changed, but text box has not been initialized(rendered) yet. + // Call asynchronously focus, when textbox will be ready. + Dispatcher.BeginInvoke(new Action(() => + { + SearchTextBox.Focus(); + //ViewModel.InitializeDefaultAutoCompleteCandidates(); + ViewModel.PopulateAutoCompleteCandidates(); + }), DispatcherPriority.Loaded); + } + + private void OnMembersListBoxUpdated(object sender, DataTransferEventArgs e) + { + var membersListBox = sender as ListBox; + // As soon as listbox renders, select first member. + membersListBox.ItemContainerGenerator.StatusChanged += OnMembersListBoxIcgStatusChanged; + } + + private void OnMembersListBoxIcgStatusChanged(object sender, EventArgs e) + { + if (MembersListBox.ItemContainerGenerator.Status == GeneratorStatus.ContainersGenerated) + { + MembersListBox.ItemContainerGenerator.StatusChanged -= OnMembersListBoxIcgStatusChanged; + Dispatcher.BeginInvoke(new Action(() => + { + var scrollViewer = MembersListBox.ChildOfType(); + scrollViewer.ScrollToTop(); + + UpdateHighlightedItem(GetListItemByIndex(MembersListBox, 0)); + }), + DispatcherPriority.Loaded); + } + } + + private void UpdateHighlightedItem(ListBoxItem newItem) + { + if (HighlightedItem == newItem) + return; + + // Unselect old value. + if (HighlightedItem != null) + HighlightedItem.IsSelected = false; + + HighlightedItem = newItem; + + // Select new value. + if (HighlightedItem != null) + { + HighlightedItem.IsSelected = true; + HighlightedItem.BringIntoView(); + } + } + + private ListBoxItem GetListItemByIndex(ListBox parent, int index) + { + if (parent.Equals(null)) return null; + + var generator = parent.ItemContainerGenerator; + if ((index >= 0) && (index < parent.Items.Count)) + return generator.ContainerFromIndex(index) as ListBoxItem; + + return null; + } + + private void OnInCanvasSearchKeyDown(object sender, KeyEventArgs e) + { + var key = e.Key; + + int index; + var members = MembersListBox.Items.Cast(); + NodeSearchElementViewModel highlightedMember = null; + if (HighlightedItem != null) + highlightedMember = HighlightedItem.DataContext as NodeSearchElementViewModel; + + switch (key) + { + case Key.Escape: + OnRequestShowNodeAutoCompleteSearch(ShowHideFlags.Hide); + break; + case Key.Enter: + if (HighlightedItem != null && ViewModel.CurrentMode != SearchViewModel.ViewMode.LibraryView) + { + ExecuteSearchElement(HighlightedItem); + OnRequestShowNodeAutoCompleteSearch(ShowHideFlags.Hide); + } + break; + case Key.Up: + index = MoveToNextMember(false, members, highlightedMember); + UpdateHighlightedItem(GetListItemByIndex(MembersListBox, index)); + break; + case Key.Down: + index = MoveToNextMember(true, members, highlightedMember); + UpdateHighlightedItem(GetListItemByIndex(MembersListBox, index)); + break; + } + } + + internal int MoveToNextMember(bool moveForward, + IEnumerable members, NodeSearchElementViewModel selectedMember) + { + int selectedMemberIndex = -1; + for (int i = 0; i < members.Count(); i++) + { + var member = members.ElementAt(i); + if (member.Equals(selectedMember)) + { + selectedMemberIndex = i; + break; + } + } + + int nextselectedMemberIndex = selectedMemberIndex; + if (moveForward) + nextselectedMemberIndex++; + else + nextselectedMemberIndex--; + + if (nextselectedMemberIndex < 0 || (nextselectedMemberIndex >= members.Count())) + return selectedMemberIndex; + + return nextselectedMemberIndex; + } + + private void OnMembersListBoxMouseWheel(object sender, MouseWheelEventArgs e) + { + var listBox = sender as FrameworkElement; + if (listBox == null) + return; + + var scrollViewer = listBox.ChildOfType(); + if (scrollViewer == null) + return; + + // Make delta less to achieve smooth scrolling and not jump over other elements. + var delta = e.Delta / 100; + scrollViewer.ScrollToVerticalOffset(scrollViewer.VerticalOffset - delta); + // do not propagate to child items with scrollable content + e.Handled = true; + } + } +} diff --git a/src/DynamoCoreWpf/DynamoCoreWpf.csproj b/src/DynamoCoreWpf/DynamoCoreWpf.csproj index ff706ec0a90..57f42d23995 100644 --- a/src/DynamoCoreWpf/DynamoCoreWpf.csproj +++ b/src/DynamoCoreWpf/DynamoCoreWpf.csproj @@ -121,6 +121,9 @@ + + NodeAutoCompleteSearchControl.xaml + InCanvasSearchControl.xaml @@ -294,6 +297,7 @@ + @@ -420,6 +424,10 @@ + + Designer + MSBuild:Compile + MSBuild:Compile Designer diff --git a/src/DynamoCoreWpf/Properties/Resources.Designer.cs b/src/DynamoCoreWpf/Properties/Resources.Designer.cs index 330ed701366..22e288d9e5e 100644 --- a/src/DynamoCoreWpf/Properties/Resources.Designer.cs +++ b/src/DynamoCoreWpf/Properties/Resources.Designer.cs @@ -1538,6 +1538,15 @@ public static string DynamoViewSamplesMenuShowInFolder { } } + /// + /// Looks up a localized string similar to Enable Node Auto Complete. + /// + public static string DynamoViewSettingEnableNodeAutoComplete { + get { + return ResourceManager.GetString("DynamoViewSettingEnableNodeAutoComplete", resourceCulture); + } + } + /// /// Looks up a localized string similar to Enable T-Spline nodes (requires relaunch of Dynamo). /// diff --git a/src/DynamoCoreWpf/Properties/Resources.en-US.resx b/src/DynamoCoreWpf/Properties/Resources.en-US.resx index 0530b2ce219..2406547ce48 100644 --- a/src/DynamoCoreWpf/Properties/Resources.en-US.resx +++ b/src/DynamoCoreWpf/Properties/Resources.en-US.resx @@ -2273,4 +2273,8 @@ Uninstall the following packages: {0}? Use System Default + + Enable Node Auto Complete + Setting menu | Experimental | Enable Node Auto Complete + \ No newline at end of file diff --git a/src/DynamoCoreWpf/Properties/Resources.resx b/src/DynamoCoreWpf/Properties/Resources.resx index 94847517740..fd24f063dbe 100644 --- a/src/DynamoCoreWpf/Properties/Resources.resx +++ b/src/DynamoCoreWpf/Properties/Resources.resx @@ -2275,4 +2275,8 @@ Uninstall the following packages: {0}? Use System Default Settings Menu | Default Python Engine (first option, rest are purposely not translated) + + Enable Node Auto Complete + Setting menu | Experimental | Enable Node Auto Complete + \ No newline at end of file diff --git a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs index ec42e3b1677..248e23a310e 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/DynamoViewModel.cs @@ -310,6 +310,21 @@ public bool EnableTSpline } } + /// + /// Indicates whether to enabled node Auto Complete feature for port interaction. + /// + public bool EnableNodeAutoComplete + { + get + { + return PreferenceSettings.EnableNodeAutoComplete; + } + set + { + PreferenceSettings.EnableNodeAutoComplete = value; + } + } + public int LibraryWidth { get diff --git a/src/DynamoCoreWpf/ViewModels/Core/PortViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/PortViewModel.cs index 5a4aa72cfb3..64e8a90380d 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/PortViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/PortViewModel.cs @@ -364,6 +364,22 @@ private bool CanConnect(object parameter) return true; } + // Handler to invoke node Auto Complete + private void AutoComplete(object parameter) + { + DynamoViewModel dynamoViewModel = _node.DynamoViewModel; + var svm = dynamoViewModel.CurrentSpaceViewModel.NodeAutoCompleteSearchViewModel as NodeAutoCompleteSearchViewModel; + svm.PortViewModel = parameter as PortViewModel; + dynamoViewModel.CurrentSpaceViewModel.OnRequestNodeAutoCompleteSearch(ShowHideFlags.Show); + } + + 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; + } + /// /// Handles the Mouse enter event on the port /// @@ -371,7 +387,9 @@ private bool CanConnect(object parameter) private void OnRectangleMouseEnter(object parameter) { if (MouseEnter != null) + { MouseEnter(parameter, null); + } } /// @@ -411,7 +429,5 @@ private void OnMouseLeftUseLevel(object parameter) { ShowUseLevelMenu = false; } - - } } diff --git a/src/DynamoCoreWpf/ViewModels/Core/StateMachine.cs b/src/DynamoCoreWpf/ViewModels/Core/StateMachine.cs index 62250df0492..48c4ad10b7d 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/StateMachine.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/StateMachine.cs @@ -117,6 +117,11 @@ internal void CancelActiveState() stateMachine.CancelActiveState(); } + internal DateTime GetLastStateTimestamp() + { + return stateMachine.Timestamp; + } + internal void BeginDragSelection(Point2D mouseCursor) { // This represents the first mouse-move event after the mouse-down @@ -439,6 +444,11 @@ private enum State #endregion #region Public Class Properties + /// + /// Optionally record the last time a particular state is updated. + /// Currently only used for Node AutoComplete feature. + /// + internal DateTime Timestamp { get; set; } internal bool IsInIdleState { @@ -802,6 +812,17 @@ internal bool HandlePortClicked(PortViewModel portViewModel) if (this.currentState != State.Connection) // Not in a connection attempt... { + if (Keyboard.Modifiers == ModifierKeys.Alt && + portViewModel.NodeAutoCompleteCommand.CanExecute(portViewModel)) + { + portViewModel.NodeAutoCompleteCommand.Execute(portViewModel); + this.currentState = State.Connection; + owningWorkspace.CurrentCursor = CursorLibrary.GetCursor(CursorSet.ArcSelect); + owningWorkspace.IsCursorForced = false; + Timestamp = DateTime.Now; + return true; + } + Guid nodeId = portModel.Owner.GUID; int portIndex = portModel.Index; diff --git a/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs index c9210190190..7632ad9301f 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/WorkspaceViewModel.cs @@ -158,9 +158,15 @@ public virtual void OnWorkspacePropertyEditRequested() private void OnRequestShowInCanvasSearch(object param) { var flag = (ShowHideFlags)param; + RequestShowInCanvasSearch?.Invoke(flag); + } + + internal event Action RequestNodeAutoCompleteSearch; - if (RequestShowInCanvasSearch != null) - RequestShowInCanvasSearch(flag); + internal void OnRequestNodeAutoCompleteSearch(object param) + { + var flag = (ShowHideFlags)param; + RequestNodeAutoCompleteSearch?.Invoke(flag); } #endregion @@ -221,7 +227,7 @@ public DynamoPreferencesData DynamoPreferences /// of Graph.Json. /// [JsonProperty("Camera")] - public CameraData Camera => DynamoViewModel.BackgroundPreviewViewModel.GetCameraInformation() ?? new CameraData(); + public CameraData Camera => DynamoViewModel.BackgroundPreviewViewModel?.GetCameraInformation() ?? new CameraData(); /// /// ViewModel that is used in InCanvasSearch in context menu and called by Shift+DoubleClick. @@ -229,6 +235,12 @@ public DynamoPreferencesData DynamoPreferences [JsonIgnore] public SearchViewModel InCanvasSearchViewModel { get; private set; } + /// + /// ViewModel that is used in NodeAutoComplete feature in context menu and called by Shift+DoubleClick. + /// + [JsonIgnore] + public SearchViewModel NodeAutoCompleteSearchViewModel { get; private set; } + /// /// Cursor Property Binding for WorkspaceView /// @@ -456,8 +468,14 @@ public WorkspaceViewModel(WorkspaceModel model, DynamoViewModel dynamoViewModel) foreach (AnnotationModel annotation in Model.Annotations) Model_AnnotationAdded(annotation); foreach (ConnectorModel connector in Model.Connectors) Connectors_ConnectorAdded(connector); - InCanvasSearchViewModel = new SearchViewModel(DynamoViewModel); - InCanvasSearchViewModel.Visible = true; + InCanvasSearchViewModel = new SearchViewModel(DynamoViewModel) + { + Visible = true + }; + NodeAutoCompleteSearchViewModel = new NodeAutoCompleteSearchViewModel(DynamoViewModel) + { + Visible = true + }; } /// /// This event is triggered from Workspace Model. Used in instrumentation @@ -505,6 +523,7 @@ public override void Dispose() Connectors.Clear(); Errors.Clear(); InCanvasSearchViewModel.Dispose(); + NodeAutoCompleteSearchViewModel.Dispose(); } internal void ZoomInInternal() diff --git a/src/DynamoCoreWpf/ViewModels/Search/NodeAutoCompleteSearchViewModel.cs b/src/DynamoCoreWpf/ViewModels/Search/NodeAutoCompleteSearchViewModel.cs new file mode 100644 index 00000000000..ea135825c1a --- /dev/null +++ b/src/DynamoCoreWpf/ViewModels/Search/NodeAutoCompleteSearchViewModel.cs @@ -0,0 +1,94 @@ +using System.Collections.Generic; +using System.Linq; +using Dynamo.Controls; +using Dynamo.Search.SearchElements; +using Dynamo.Wpf.ViewModels; + +namespace Dynamo.ViewModels +{ + /// + /// Search View Model for Node AutoComplate Search Bar + /// + public class NodeAutoCompleteSearchViewModel : SearchViewModel + { + internal PortViewModel PortViewModel { get; set; } + + internal NodeAutoCompleteSearchViewModel(DynamoViewModel dynamoViewModel) : base(dynamoViewModel) + { + // Do nothing for now, but we may off load some time consuming operation here later + } + + internal void InitializeDefaultAutoCompleteCandidates() + { + var candidates = new List(); + // TODO: These are hard copied all time top 7 nodes placed by customers + // This should be only served as a temporary default case. + var queries = new List(){ "Code Block", "Watch", "List Flatten", "List Create", "String", "Double", "Python" }; + foreach (var query in queries) + { + var foundNode = Search(query).ToList().FirstOrDefault(); + if(foundNode != null) + { + candidates.Add(foundNode); + } + } + FilteredResults = candidates; + } + + internal void PopulateAutoCompleteCandidates() + { + if(PortViewModel == null) return; + + var searchElements = GetMatchingNodes(); + FilteredResults = searchElements.Select(e => + { + var vm = new NodeSearchElementViewModel(e, this); + vm.RequestBitmapSource += SearchViewModelRequestBitmapSource; + return vm; + }); + } + + /// + /// Returns a collection of node search elements for nodes + /// that output a type compatible with the port type if it's an input port. + /// These search elements can belong to either zero touch, NodeModel or Builtin nodes. + /// This method returns an empty collection if the input port type cannot be inferred or + /// there are no matching nodes found for the type. Currently the match is an exact match + /// done including the rank information in the type, e.g. Point[] or var[]..[]. + /// The search elements can be made to appear in the node autocomplete search dialog. + /// + /// collection of node search elements + internal IEnumerable GetMatchingNodes() + { + var elements = new List(); + + var inputPortType = PortViewModel.PortModel.GetInputPortType(); + if (inputPortType == null) return elements; + + var libraryServices = dynamoViewModel.Model.LibraryServices; + + // Builtin functions and zero-touch functions + // Multi-return ports for these nodes do not contain type information + // and are therefore skipped. + var functionGroups = libraryServices.GetAllFunctionGroups(); + var functionDescriptors = functionGroups.SelectMany(fg => fg.Functions).Where(fd => fd.IsVisibleInLibrary); + + foreach (var descriptor in functionDescriptors) + { + if (descriptor.ReturnType.ToString() == inputPortType) + { + elements.Add(new ZeroTouchSearchElement(descriptor)); + } + } + + // NodeModel nodes + foreach (var element in Model.SearchEntries.OfType()) + { + if (element.OutputParameters.Any(op => op == inputPortType)) + elements.Add(element); + } + + return elements; + } + } +} diff --git a/src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs b/src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs index f7b96f50e3d..d89987bc175 100644 --- a/src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Search/NodeSearchElementViewModel.cs @@ -42,8 +42,12 @@ public NodeSearchElementViewModel(NodeSearchElement element, SearchViewModel svm Model.VisibilityChanged += ModelOnVisibilityChanged; if (searchViewModel != null) + { Clicked += searchViewModel.OnSearchElementClicked; + CreateAndConnectToPort += searchViewModel.OnRequestConnectToPort; + } ClickedCommand = new DelegateCommand(OnClicked); + CreateAndConnectCommand = new DelegateCommand(OnRequestCreateAndConnectToPort); LoadFonts(); } @@ -226,6 +230,20 @@ public ImageSource LargeIcon internal Point Position { get; set; } + /// + /// Create the search element as node and connect to target port + /// + public ICommand CreateAndConnectCommand { get; private set; } + + public event Action CreateAndConnectToPort; + protected virtual void OnRequestCreateAndConnectToPort(PortModel portModel) + { + if (CreateAndConnectToPort != null) + { + CreateAndConnectToPort(Model.CreationName, portModel); + } + } + public ICommand ClickedCommand { get; private set; } public event Action Clicked; diff --git a/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs b/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs index d829a410619..2c0c8db2c8f 100644 --- a/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Search/SearchViewModel.cs @@ -18,7 +18,6 @@ using Dynamo.Utilities; using Dynamo.Wpf.Services; using Dynamo.Wpf.ViewModels; -using Microsoft.Practices.Prism.ViewModel; namespace Dynamo.ViewModels { @@ -300,7 +299,7 @@ public NodeSearchElementViewModel FindViewModelForNode(string nodeName) } public NodeSearchModel Model { get; private set; } - private readonly DynamoViewModel dynamoViewModel; + internal readonly DynamoViewModel dynamoViewModel; /// /// Class name, that has been clicked in library search view. @@ -772,7 +771,7 @@ private void AddEntryToExistingCategory(NodeCategoryViewModel category, } } - private void SearchViewModelRequestBitmapSource(IconRequestEventArgs e) + protected void SearchViewModelRequestBitmapSource(IconRequestEventArgs e) { var warehouse = iconServices.GetForAssembly(e.IconAssembly, e.UseAdditionalResolutionPaths); ImageSource icon = null; @@ -1060,6 +1059,18 @@ 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 diff --git a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml index 4a4362af4d8..e21a96fb5b8 100644 --- a/src/DynamoCoreWpf/Views/Core/DynamoView.xaml +++ b/src/DynamoCoreWpf/Views/Core/DynamoView.xaml @@ -751,6 +751,10 @@ IsCheckable="True" IsChecked="{Binding EnableTSpline}" Header="{x:Static p:Resources.DynamoViewSettingEnableTSplineNodes}" /> + diff --git a/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml b/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml index e328d16a319..e68ef3fef0a 100644 --- a/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml +++ b/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml @@ -345,6 +345,16 @@ + + + + diff --git a/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml.cs b/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml.cs index be334320fe5..8f5537bbcae 100644 --- a/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml.cs +++ b/src/DynamoCoreWpf/Views/Core/WorkspaceView.xaml.cs @@ -108,6 +108,7 @@ void OnWorkspaceViewLoaded(object sender, RoutedEventArgs e) DynamoSelection.Instance.Selection.CollectionChanged += OnSelectionCollectionChanged; ViewModel.RequestShowInCanvasSearch += ShowHideInCanvasControl; + ViewModel.RequestNodeAutoCompleteSearch += ShowHideNodeAutoCompleteControl; ViewModel.DynamoViewModel.PropertyChanged += ViewModel_PropertyChanged; infiniteGridView.AttachToZoomBorder(zoomBorder); @@ -160,6 +161,11 @@ void OnWorkspaceViewUnloaded(object sender, RoutedEventArgs e) } + private void ShowHideNodeAutoCompleteControl(ShowHideFlags flag) + { + ShowHidePopup(flag, NodeAutoCompleteSearchBar); + } + private void ShowHideInCanvasControl(ShowHideFlags flag) { ShowHidePopup(flag, InCanvasSearchBar); @@ -196,6 +202,16 @@ public void HidePopUp() ShowHideContextMenu(ShowHideFlags.Hide); ShowHideInCanvasControl(ShowHideFlags.Hide); } + if (NodeAutoCompleteSearchBar.IsOpen) + { + // Suppress the mouse action from last 0.2 second otherwise mouse button release + // in Dynamo window will forcefully shutdown all the open SearchBars + if ((new TimeSpan(DateTime.Now.Ticks - ViewModel.GetLastStateTimestamp().Ticks)).TotalSeconds > 0.2) + { + ShowHideNodeAutoCompleteControl(ShowHideFlags.Hide); + ViewModel.CancelActiveState(); + } + } } internal Point GetCenterPoint() diff --git a/src/Libraries/CoreNodeModels/ColorRange.cs b/src/Libraries/CoreNodeModels/ColorRange.cs index 223dd6f90e8..99c9ab8d08f 100644 --- a/src/Libraries/CoreNodeModels/ColorRange.cs +++ b/src/Libraries/CoreNodeModels/ColorRange.cs @@ -20,7 +20,7 @@ namespace CoreNodeModels [NodeSearchTags("ColorRangeSearchTags", typeof(Resources))] [InPortNames("colors", "indices", "value")] - [InPortTypes("Color[]", "double[]", "double")] + [InPortTypes("DSCore.Color[]", "double[]", "double")] [InPortDescriptions(typeof(Resources), "ColorRangePortDataColorsToolTip", "ColorRangePortDataIndicesToolTip", diff --git a/src/Libraries/CoreNodeModels/WatchImageCore.cs b/src/Libraries/CoreNodeModels/WatchImageCore.cs index d00e0331085..a04d785e58b 100644 --- a/src/Libraries/CoreNodeModels/WatchImageCore.cs +++ b/src/Libraries/CoreNodeModels/WatchImageCore.cs @@ -14,6 +14,7 @@ namespace CoreNodeModels [NodeCategory(BuiltinNodeCategories.CORE_VIEW)] [NodeSearchTags("WatchImageSearchTags", typeof(Resources))] [IsDesignScriptCompatible] + [InPortTypes("System.Drawing.Bitmap")] [OutPortTypes("var")] [AlsoKnownAs("Dynamo.Nodes.WatchImageCore", "DSCoreNodesUI.WatchImageCore")] public class WatchImageCore : NodeModel diff --git a/src/Libraries/CoreNodes/List.cs b/src/Libraries/CoreNodes/List.cs index 3654a884499..e4d27e5cdb7 100644 --- a/src/Libraries/CoreNodes/List.cs +++ b/src/Libraries/CoreNodes/List.cs @@ -304,7 +304,6 @@ public static IList Reverse(IList list) /// Creates a new list containing the given items. /// /// Items to be stored in the new list. - [IsVisibleInDynamoLibrary(true)] public static IList __Create(IList items) { return items; diff --git a/src/Libraries/DesignScriptBuiltin/IndexingExceptions.cs b/src/Libraries/DesignScriptBuiltin/IndexingExceptions.cs index 687d4c20d37..b5f72ddfac8 100644 --- a/src/Libraries/DesignScriptBuiltin/IndexingExceptions.cs +++ b/src/Libraries/DesignScriptBuiltin/IndexingExceptions.cs @@ -1,7 +1,9 @@ using System; +using Autodesk.DesignScript.Runtime; namespace DesignScript.Builtin { + [SupressImportIntoVM] public class KeyNotFoundException : Exception { public KeyNotFoundException(string message) @@ -10,6 +12,7 @@ public KeyNotFoundException(string message) } } + [SupressImportIntoVM] public class IndexOutOfRangeException : Exception { public IndexOutOfRangeException(string message) @@ -18,6 +21,7 @@ public IndexOutOfRangeException(string message) } } + [SupressImportIntoVM] public class StringOverIndexingException : Exception { public StringOverIndexingException(string message) @@ -30,6 +34,7 @@ public StringOverIndexingException(string message) /// Null reference exception thrown with null DS builtin types: /// lists, dictionaries and strings. /// + [SupressImportIntoVM] public class BuiltinNullReferenceException : NullReferenceException { public BuiltinNullReferenceException(string message) diff --git a/test/DynamoCoreTests/Configuration/PreferenceSettingsTests.cs b/test/DynamoCoreTests/Configuration/PreferenceSettingsTests.cs index e16a7f287f3..77219edcd1b 100644 --- a/test/DynamoCoreTests/Configuration/PreferenceSettingsTests.cs +++ b/test/DynamoCoreTests/Configuration/PreferenceSettingsTests.cs @@ -56,6 +56,7 @@ public void TestSettingsSerialization() Assert.AreEqual(settings.ShowCodeBlockLineNumber, true); Assert.AreEqual(settings.IsIronPythonDialogDisabled, false); Assert.AreEqual(settings.ShowTabsAndSpacesInScriptEditor, false); + Assert.AreEqual(settings.EnableNodeAutoComplete, false); Assert.AreEqual(settings.DefaultPythonEngine, string.Empty); Assert.AreEqual(settings.MaxNumRecentFiles, PreferenceSettings.DefaultMaxNumRecentFiles); @@ -68,6 +69,7 @@ public void TestSettingsSerialization() Assert.AreEqual(settings.ShowCodeBlockLineNumber, true); Assert.AreEqual(settings.IsIronPythonDialogDisabled, false); Assert.AreEqual(settings.ShowTabsAndSpacesInScriptEditor, false); + Assert.AreEqual(settings.EnableNodeAutoComplete, false); Assert.AreEqual(settings.DefaultPythonEngine, string.Empty); Assert.AreEqual(settings.MaxNumRecentFiles, PreferenceSettings.DefaultMaxNumRecentFiles); @@ -78,6 +80,7 @@ public void TestSettingsSerialization() settings.ShowTabsAndSpacesInScriptEditor = true; settings.DefaultPythonEngine = "CP3"; settings.MaxNumRecentFiles = 24; + settings.EnableNodeAutoComplete = true; // Save settings.Save(tempPath); @@ -90,6 +93,7 @@ public void TestSettingsSerialization() Assert.AreEqual(settings.ShowTabsAndSpacesInScriptEditor, true); Assert.AreEqual(settings.DefaultPythonEngine, "CP3"); Assert.AreEqual(settings.MaxNumRecentFiles, 24); + Assert.AreEqual(settings.EnableNodeAutoComplete, true); } } } diff --git a/test/DynamoCoreWpfTests/CoreUITests.cs b/test/DynamoCoreWpfTests/CoreUITests.cs index 2dc20cb61a5..1865951c907 100644 --- a/test/DynamoCoreWpfTests/CoreUITests.cs +++ b/test/DynamoCoreWpfTests/CoreUITests.cs @@ -886,6 +886,26 @@ public void WorkspaceContextMenu_IfSubmenuOpenOnMouseHover() Assert.IsTrue(currentWs.WorkspaceLacingMenu.IsSubmenuOpen); } + [Test] + [Category("UnitTests")] + public void ShowHideNodeAutoCompleteSearchControl() + { + var currentWs = View.ChildOfType(); + + // Show Node AutoCompleteSearchBar + ViewModel.CurrentSpaceViewModel.OnRequestNodeAutoCompleteSearch(ShowHideFlags.Show); + Assert.IsTrue(currentWs.NodeAutoCompleteSearchBar.IsOpen); + + RightClick(currentWs.zoomBorder); + // Notice AutoCompleteSearchBar can co-exist with right click search for now + Assert.IsTrue(currentWs.ContextMenuPopup.IsOpen); + Assert.IsTrue(currentWs.NodeAutoCompleteSearchBar.IsOpen); + + // Hide Node AutoCompleteSearchBar + ViewModel.CurrentSpaceViewModel.OnRequestNodeAutoCompleteSearch(ShowHideFlags.Hide); + Assert.IsFalse(currentWs.NodeAutoCompleteSearchBar.IsOpen); + } + private void RightClick(IInputElement element) { element.RaiseEvent(new MouseButtonEventArgs(Mouse.PrimaryDevice, 0, MouseButton.Right) diff --git a/test/DynamoCoreWpfTests/DynamoCoreWpfTests.csproj b/test/DynamoCoreWpfTests/DynamoCoreWpfTests.csproj index d91cc0f0b0f..93cb3094055 100644 --- a/test/DynamoCoreWpfTests/DynamoCoreWpfTests.csproj +++ b/test/DynamoCoreWpfTests/DynamoCoreWpfTests.csproj @@ -126,6 +126,7 @@ + diff --git a/test/DynamoCoreWpfTests/NodeAutoCompleteSearchTests.cs b/test/DynamoCoreWpfTests/NodeAutoCompleteSearchTests.cs new file mode 100644 index 00000000000..3d808390c97 --- /dev/null +++ b/test/DynamoCoreWpfTests/NodeAutoCompleteSearchTests.cs @@ -0,0 +1,122 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Threading.Tasks; +using Dynamo.Controls; +using Dynamo.ViewModels; +using DynamoCoreWpfTests.Utility; +using NUnit.Framework; + +namespace DynamoCoreWpfTests +{ + class NodeAutoCompleteSearchTests : DynamoTestUIBase + { + protected override void GetLibrariesToPreload(List libraries) + { + libraries.Add("FunctionObject.ds"); + libraries.Add("BuiltIn.ds"); + libraries.Add("FFITarget.dll"); + } + + public override void Open(string path) + { + base.Open(path); + + DispatcherUtil.DoEvents(); + } + + public override void Run() + { + base.Run(); + + DispatcherUtil.DoEvents(); + } + + + [Test] + public void NodeSuggestions_InputPortZeroTouchNode_AreCorrect() + { + Open(@"UI\ffitarget_inputport_suggestion.dyn"); + + // Get the node view for a specific node in the graph + NodeView nodeView = NodeViewWithGuid(Guid.Parse("9aeba33453a34c73823976222b44375b").ToString()); + + var inPorts = nodeView.ViewModel.InPorts; + Assert.AreEqual(2, inPorts.Count()); + + var port = inPorts[0].PortModel; + var type = port.GetInputPortType(); + Assert.AreEqual("FFITarget.DummyPoint", type); + + port = inPorts[1].PortModel; + type = port.GetInputPortType(); + Assert.AreEqual("FFITarget.DummyVector", type); + + var searchViewModel = (ViewModel.CurrentSpaceViewModel.NodeAutoCompleteSearchViewModel as NodeAutoCompleteSearchViewModel); + searchViewModel.PortViewModel = inPorts[1]; + var suggestions = searchViewModel.GetMatchingNodes(); + Assert.AreEqual(5, suggestions.Count()); + + var suggestedNodes = suggestions.Select(s => s.FullName).OrderBy(s => s); + var nodes = new[] { "FFITarget.FFITarget.DummyVector.DummyVector", "FFITarget.FFITarget.DummyVector.ByCoordinates", + "FFITarget.FFITarget.DummyVector.ByVector", "FFITarget.FFITarget.DummyVector.Scale", "FFITarget.FFITarget.DummyPoint.DirectionTo" }; + var expectedNodes = nodes.OrderBy(s => s); + for (int i = 0; i < 5; i++) + { + Assert.AreEqual(expectedNodes.ElementAt(i), suggestedNodes.ElementAt(i)); + } + } + + [Test] + public void NodeSuggestions_InputPortBuiltInNode_AreCorrect() + { + Open(@"UI\builtin_inputport_suggestion.dyn"); + + // Get the node view for a specific node in the graph + NodeView nodeView = NodeViewWithGuid(Guid.Parse("b6cb6ceb21df4c7fb6b186e6ff399afc").ToString()); + + var inPorts = nodeView.ViewModel.InPorts; + Assert.AreEqual(2, inPorts.Count()); + + var port = inPorts[0].PortModel; + var type = port.GetInputPortType(); + Assert.AreEqual("var[]..[]", type); + + port = inPorts[1].PortModel; + type = port.GetInputPortType(); + Assert.AreEqual("string", type); + + var searchViewModel = (ViewModel.CurrentSpaceViewModel.NodeAutoCompleteSearchViewModel as NodeAutoCompleteSearchViewModel); + searchViewModel.PortViewModel = inPorts[1]; + var suggestions = searchViewModel.GetMatchingNodes(); + Assert.AreEqual(16, suggestions.Count()); + + var suggestedNodes = suggestions.Select(s => s.FullName).OrderBy(s => s); + var nodes = new[] + { + "Core.Input.File Path", + "Core.String.String from Array", + "Core.String.String from Object", + "FFITarget.DupTargetTest.Bar", + "FFITarget.FFITarget.AtLevelTestClass.sumAndConcat", + "FFITarget.FFITarget.AtLevelTestClass.SumAndConcat", + "FFITarget.FFITarget.FirstNamespace.AnotherClassWithNameConflict.PropertyA", + "FFITarget.FFITarget.FirstNamespace.AnotherClassWithNameConflict.PropertyB", + "FFITarget.FFITarget.FirstNamespace.AnotherClassWithNameConflict.PropertyC", + "FFITarget.FFITarget.FirstNamespace.ClassWithNameConflict.PropertyA", + "FFITarget.FFITarget.FirstNamespace.ClassWithNameConflict.PropertyB", + "FFITarget.FFITarget.FirstNamespace.ClassWithNameConflict.PropertyC", + "FFITarget.FFITarget.SecondNamespace.ClassWithNameConflict.PropertyD", + "FFITarget.FFITarget.SecondNamespace.ClassWithNameConflict.PropertyE", + "FFITarget.FFITarget.SecondNamespace.ClassWithNameConflict.PropertyF", + "FFITarget.FFITarget.TestData.GetStringValue" + }; + var expectedNodes = nodes.OrderBy(s => s); + for (int i = 0; i < 5; i++) + { + Assert.AreEqual(expectedNodes.ElementAt(i), suggestedNodes.ElementAt(i)); + } + } + } +} diff --git a/test/DynamoCoreWpfTests/NodeViewTests.cs b/test/DynamoCoreWpfTests/NodeViewTests.cs index e6f9f2c30a6..873bb8de87f 100644 --- a/test/DynamoCoreWpfTests/NodeViewTests.cs +++ b/test/DynamoCoreWpfTests/NodeViewTests.cs @@ -1,4 +1,5 @@ using System; +using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; @@ -17,6 +18,13 @@ public class NodeViewTests : DynamoTestUIBase { // adapted from: http://stackoverflow.com/questions/9336165/correct-method-for-using-the-wpf-dispatcher-in-unit-tests + protected override void GetLibrariesToPreload(List libraries) + { + libraries.Add("FunctionObject.ds"); + libraries.Add("BuiltIn.ds"); + libraries.Add("FFITarget.dll"); + } + public override void Open(string path) { base.Open(path); @@ -327,6 +335,22 @@ public void SettingOriginalNodeNameOnCustomNode() Assert.AreEqual(nodeViewModel.OriginalName, expectedOriginalName); } + [Test] + public void InputPortType_NodeModelNode_AreCorrect() + { + Open(@"UI\CoreUINodes.dyn"); + + // Get the node view for a specific node in the graph + NodeView nodeView = NodeViewWithGuid(Guid.Parse("9dedd5c5c8b14fbebaea28194fd38c9a").ToString()); + + var inPorts = nodeView.ViewModel.InPorts; + Assert.AreEqual(1, inPorts.Count()); + + var port = inPorts[0].PortModel; + var type = port.GetInputPortType(); + Assert.AreEqual("System.Drawing.Bitmap", type); + } + [Test] [Category("RegressionTests")] public void GettingNodeNameDoesNotTriggerPropertyChangeCycle() diff --git a/test/UI/builtin_inputport_suggestion.dyn b/test/UI/builtin_inputport_suggestion.dyn new file mode 100644 index 00000000000..0568990ceea --- /dev/null +++ b/test/UI/builtin_inputport_suggestion.dyn @@ -0,0 +1,94 @@ +{ + "Uuid": "87442094-a012-4db1-9432-789dd1c20ba8", + "IsCustomNode": false, + "Description": null, + "Name": "builtin_inputport_suggestion", + "ElementResolver": { + "ResolutionMap": {} + }, + "Inputs": [], + "Outputs": [], + "Nodes": [ + { + "ConcreteType": "Dynamo.Graph.Nodes.ZeroTouch.DSFunction, DynamoCore", + "NodeType": "FunctionNode", + "FunctionSignature": "List.RemoveIfNot@var[]..[],string", + "Id": "b6cb6ceb21df4c7fb6b186e6ff399afc", + "Inputs": [ + { + "Id": "b090a79fa5914f049ad2fbc48d7fbf74", + "Name": "list", + "Description": "list of values\n\nvar[]..[]", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + }, + { + "Id": "9c013fa7a7f84505abbba72579e32545", + "Name": "type", + "Description": "type of element\n\nstring", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Outputs": [ + { + "Id": "1211bcfe948c46a39eacb248569c4cce", + "Name": "var[]..[]", + "Description": "var[]..[]", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Replication": "Auto", + "Description": "Removes the members of the list which are not members of the specified type.\n\nList.RemoveIfNot (list: var[]..[], type: string): var[]..[]" + } + ], + "Connectors": [], + "Dependencies": [], + "NodeLibraryDependencies": [], + "Bindings": [], + "View": { + "Dynamo": { + "ScaleFactor": 1.0, + "HasRunWithoutCrash": true, + "IsVisibleInDynamoLibrary": true, + "Version": "2.9.0.2607", + "RunType": "Automatic", + "RunPeriod": "1000" + }, + "Camera": { + "Name": "Background Preview", + "EyeX": -17.0, + "EyeY": 24.0, + "EyeZ": 50.0, + "LookX": 12.0, + "LookY": -13.0, + "LookZ": -58.0, + "UpX": 0.0, + "UpY": 1.0, + "UpZ": 0.0 + }, + "NodeViews": [ + { + "ShowGeometry": true, + "Name": "List.RemoveIfNot", + "Id": "b6cb6ceb21df4c7fb6b186e6ff399afc", + "IsSetAsInput": false, + "IsSetAsOutput": false, + "Excluded": false, + "X": 505.0, + "Y": 274.0 + } + ], + "Annotations": [], + "X": 0.0, + "Y": 0.0, + "Zoom": 1.0 + } +} \ No newline at end of file diff --git a/test/UI/ffitarget_inputport_suggestion.dyn b/test/UI/ffitarget_inputport_suggestion.dyn new file mode 100644 index 00000000000..b8ba88766be --- /dev/null +++ b/test/UI/ffitarget_inputport_suggestion.dyn @@ -0,0 +1,94 @@ +{ + "Uuid": "79ea0296-2698-4563-9ebd-ca50128bd4e7", + "IsCustomNode": false, + "Description": null, + "Name": "ffitarget_inputport_suggestion", + "ElementResolver": { + "ResolutionMap": {} + }, + "Inputs": [], + "Outputs": [], + "Nodes": [ + { + "ConcreteType": "Dynamo.Graph.Nodes.ZeroTouch.DSFunction, DynamoCore", + "NodeType": "FunctionNode", + "FunctionSignature": "FFITarget.DummyPoint.Translate@FFITarget.DummyVector", + "Id": "9aeba33453a34c73823976222b44375b", + "Inputs": [ + { + "Id": "6386befce6fb4baba5131314fc55348b", + "Name": "dummyPoint", + "Description": "FFITarget.DummyPoint", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + }, + { + "Id": "4352cc5bbe58469e80d7db2760dd0871", + "Name": "direction", + "Description": "DummyVector", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Outputs": [ + { + "Id": "fc8a45bf13a449b4814ec0a843fc5be0", + "Name": "DummyPoint", + "Description": "DummyPoint", + "UsingDefaultValue": false, + "Level": 2, + "UseLevels": false, + "KeepListStructure": false + } + ], + "Replication": "Auto", + "Description": "DummyPoint.Translate (direction: DummyVector): DummyPoint" + } + ], + "Connectors": [], + "Dependencies": [], + "NodeLibraryDependencies": [], + "Bindings": [], + "View": { + "Dynamo": { + "ScaleFactor": 1.0, + "HasRunWithoutCrash": true, + "IsVisibleInDynamoLibrary": true, + "Version": "2.9.0.2607", + "RunType": "Automatic", + "RunPeriod": "1000" + }, + "Camera": { + "Name": "Background Preview", + "EyeX": -17.0, + "EyeY": 24.0, + "EyeZ": 50.0, + "LookX": 12.0, + "LookY": -13.0, + "LookZ": -58.0, + "UpX": 0.0, + "UpY": 1.0, + "UpZ": 0.0 + }, + "NodeViews": [ + { + "ShowGeometry": true, + "Name": "DummyPoint.Translate", + "Id": "9aeba33453a34c73823976222b44375b", + "IsSetAsInput": false, + "IsSetAsOutput": false, + "Excluded": false, + "X": 618.0, + "Y": 357.0 + } + ], + "Annotations": [], + "X": 0.0, + "Y": 0.0, + "Zoom": 1.0 + } +} \ No newline at end of file