diff --git a/src/DynamoCoreWpf/DynamoCoreWpf.csproj b/src/DynamoCoreWpf/DynamoCoreWpf.csproj index 5de998ab136..22268a6b374 100644 --- a/src/DynamoCoreWpf/DynamoCoreWpf.csproj +++ b/src/DynamoCoreWpf/DynamoCoreWpf.csproj @@ -223,6 +223,7 @@ + diff --git a/src/DynamoCoreWpf/NodeViewCustomization/NodeViewCustomizations/FunctionNodeViewCustomization.cs b/src/DynamoCoreWpf/NodeViewCustomization/NodeViewCustomizations/FunctionNodeViewCustomization.cs index aeb01b0797e..d849381a392 100644 --- a/src/DynamoCoreWpf/NodeViewCustomization/NodeViewCustomizations/FunctionNodeViewCustomization.cs +++ b/src/DynamoCoreWpf/NodeViewCustomization/NodeViewCustomizations/FunctionNodeViewCustomization.cs @@ -19,8 +19,6 @@ public void CustomizeView(Function function, NodeView nodeView) functionNodeModel = function; dynamoViewModel = nodeView.ViewModel.DynamoViewModel; - nodeView.MainContextMenu.Items.Add(new Separator()); - // edit contents var editItem = new MenuItem { diff --git a/src/DynamoCoreWpf/Utilities/NodeContextMenuBuilder.cs b/src/DynamoCoreWpf/Utilities/NodeContextMenuBuilder.cs new file mode 100644 index 00000000000..ab079f433ff --- /dev/null +++ b/src/DynamoCoreWpf/Utilities/NodeContextMenuBuilder.cs @@ -0,0 +1,453 @@ +using System.Collections; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.Windows; +using System.Windows.Controls; +using System.Windows.Data; +using Dynamo.Controls; +using Dynamo.Graph.Nodes; +using Dynamo.ViewModels; + +namespace Dynamo.Wpf.Utilities +{ + /// + /// A static class containing methods for dynamically building a Node's Context Menu. + /// + internal static class NodeContextMenuBuilder + { + /// + /// A reference to the NodeViewModel whose ContextMenu is being built. + /// + internal static NodeViewModel NodeViewModel { get; set; } + + /// + /// A reference to the ContextMenu to which items are being added. + /// + internal static ContextMenu ContextMenu { get; set; } + + // Builds the node's Context Menu, including re-adding any injected MenuItems from + // the NodeViewCustomization process. + internal static void Build(ContextMenu contextMenu, NodeViewModel nodeViewModel, OrderedDictionary nodeViewCustomizationMenuItems) + { + ContextMenu = contextMenu; + NodeViewModel = nodeViewModel; + + AddContextMenuItem(BuildDeleteMenuItem()); + AddContextMenuItem(BuildGroupsMenuItem()); + if (nodeViewModel.ShowsVisibilityToggles) AddContextMenuItem(BuildPreviewMenuItem()); + AddContextMenuItem(BuildFreezeMenuItem()); + AddContextMenuItem(BuildShowLabelsMenuItem()); + AddContextMenuItem(BuildRenameMenuItem()); + if (nodeViewModel.ArgumentLacing != LacingStrategy.Disabled) AddContextMenuItem(BuildLacingMenuItem()); + + //AddContextMenuItem(BuildDismissedAlertsMenuItem()); + + if (nodeViewCustomizationMenuItems.Count > 0) + { + AddContextMenuSeparator(); + AddInjectedNodeViewCustomizationMenuItems(nodeViewCustomizationMenuItems); + } + + if(NodeViewModel.IsInput || NodeViewModel.IsOutput) AddContextMenuSeparator(); + if(NodeViewModel.IsInput) AddContextMenuItem(BuildIsInputMenuItem()); + if(NodeViewModel.IsOutput) AddContextMenuItem(BuildIsOutputMenuItem()); + + AddContextMenuSeparator(); + AddContextMenuItem(BuildHelpMenuItem()); + } + + /// + /// Adds items to a context menu. + /// + /// + internal static void AddContextMenuItem(MenuItem menuItem) => ContextMenu.Items.Add(menuItem); + + /// + /// Adds a new separator a context menu. + /// + internal static void AddContextMenuSeparator() => ContextMenu.Items.Add(new Separator()); + + /// + /// Creates a new MenuItem in the node's Context Menu. + /// Using optional named arguments here as it makes the Binding syntax + /// much cleaner if all handled in the method body. + /// + /// + /// + /// + /// + /// + /// + /// + /// + /// + internal static MenuItem CreateMenuItem + ( + string name = null, + string header = null, + Binding command = null, + string commandParameter = null, + bool isCheckable = false, + Binding isChecked = null, + Binding visibility = null, + Binding isEnabled = null, + Binding itemsSource = null + ) + { + MenuItem menuItem = new MenuItem { Header = header, IsCheckable = isCheckable }; + + if (!string.IsNullOrWhiteSpace(name)) menuItem.Name = name; + if (command != null) menuItem.SetBinding(MenuItem.CommandProperty, command); + if (commandParameter != null) menuItem.CommandParameter = commandParameter; + if (isChecked != null) menuItem.SetBinding(MenuItem.IsCheckedProperty, isChecked); + if (visibility != null) menuItem.SetBinding(UIElement.VisibilityProperty, visibility); + if (isEnabled != null) menuItem.SetBinding(UIElement.IsEnabledProperty, isEnabled); + if (itemsSource != null) menuItem.SetBinding(ItemsControl.ItemsSourceProperty, itemsSource); + + return menuItem; + } + + internal static MenuItem BuildDeleteMenuItem() + { + return CreateMenuItem + ( + name: "deleteElem_cm", + header: Properties.Resources.ContextMenuDelete, + command: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.DeleteCommand)) + } + ); + } + + internal static MenuItem BuildGroupsMenuItem() + { + MenuItem groupsMenuItem = CreateMenuItem(header: Properties.Resources.ContextMenuGroups); + + groupsMenuItem.Items.Add(CreateMenuItem + ( + name: "createGroup_cm", + header: Properties.Resources.ContextCreateGroupFromSelection, + command: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.CreateGroupCommand)) + } + ) + ); + groupsMenuItem.Items.Add(CreateMenuItem + ( + name: "unGroup_cm", + header: Properties.Resources.ContextUnGroupFromSelection, + command: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.UngroupCommand)) + } + ) + ); + groupsMenuItem.Items.Add(CreateMenuItem + ( + name: "addtoGroup", + header: Properties.Resources.ContextAddGroupFromSelection, + command: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.AddToGroupCommand)) + } + ) + ); + + return groupsMenuItem; + } + + internal static MenuItem BuildPreviewMenuItem() + { + MenuItem previewMenuItem = CreateMenuItem + ( + name: "isVisible_cm", + header: Properties.Resources.NodeContextMenuPreview, + command: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.ToggleIsVisibleCommand)) + }, + isCheckable: true, + isChecked: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.IsVisible)), + Mode = BindingMode.OneWay + } + ); + + return previewMenuItem; + } + + internal static MenuItem BuildFreezeMenuItem() + { + MenuItem freezeMenuItem = CreateMenuItem + ( + name: "nodeIsFrozen", + header: Properties.Resources.NodesRunStatus, + command: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.ToggleIsFrozenCommand)) + }, + isCheckable: true, + isChecked: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.IsFrozenExplicitly)), + Mode = BindingMode.OneWay + }, + isEnabled: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.CanToggleFrozen)), + Mode = BindingMode.OneWay + } + ); + + return freezeMenuItem; + } + + internal static MenuItem BuildShowLabelsMenuItem() + { + MenuItem showLabelsMenuItem = CreateMenuItem + ( + name: "isDisplayLabelsEnabled_cm", + header: Properties.Resources.NodeContextMenuShowLabels, + isCheckable: true, + isChecked: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.IsDisplayingLabels)), + Mode = BindingMode.TwoWay + }, + isEnabled: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.CanDisplayLabels)) + } + ); + + return showLabelsMenuItem; + } + + internal static MenuItem BuildRenameMenuItem() + { + MenuItem renameMenuItem = CreateMenuItem + ( + name: "rename_cm", + header: Properties.Resources.NodeContextMenuRenameNode, + command: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.RenameCommand)) + } + ); + return renameMenuItem; + } + + internal static MenuItem BuildLacingMenuItem() + { + MenuItem lacingMenuItem = CreateMenuItem(header: Properties.Resources.ContextMenuLacing); + + lacingMenuItem.Items.Add(CreateMenuItem + ( + header: Properties.Resources.ContextMenuLacingAuto, + command: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.SetLacingTypeCommand)), + }, + commandParameter: "Auto", + isCheckable: true, + isChecked: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.ArgumentLacing)), + Converter = new EnumToBooleanConverter(), + ConverterParameter = "Auto", + Mode = BindingMode.OneWay + } + )); + + lacingMenuItem.Items.Add(CreateMenuItem + ( + header: Properties.Resources.ContextMenuLacingShortest, + command: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.SetLacingTypeCommand)), + }, + commandParameter: "Shortest", + isCheckable: true, + isChecked: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.ArgumentLacing)), + Converter = new EnumToBooleanConverter(), + ConverterParameter = "Shortest", + Mode = BindingMode.OneWay + } + )); + + lacingMenuItem.Items.Add(CreateMenuItem + ( + header: Properties.Resources.ContextMenuLacingLongest, + command: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.SetLacingTypeCommand)), + }, + commandParameter: "Longest", + isCheckable: true, + isChecked: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.ArgumentLacing)), + Converter = new EnumToBooleanConverter(), + ConverterParameter = "Longest", + Mode = BindingMode.OneWay + } + )); + + lacingMenuItem.Items.Add(CreateMenuItem + ( + header: Properties.Resources.ContextMenuLacingCrossProduct, + command: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.SetLacingTypeCommand)), + }, + commandParameter: "CrossProduct", + isCheckable: true, + isChecked: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.ArgumentLacing)), + Converter = new EnumToBooleanConverter(), + ConverterParameter = "CrossProduct", + Mode = BindingMode.OneWay + } + )); + + return lacingMenuItem; + } + + // To be connected in a future PR for Node Info States. + private static MenuItem BuildDismissedAlertsMenuItem() + { + //MenuItem dismissedAlertsMenuItem = CreateMenuItem + //( + // name: "dismissedAlerts", + // header: Wpf.Properties.Resources.NodeInformationalStateDismissedAlerts, + // itemsSource: new Binding + // { + // Source = viewModel, + // Path = new PropertyPath(nameof("DismissedAlerts"), + // UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged + // } + //); + //DataTemplate itemTemplate = new DataTemplate(typeof(MenuItem)); + + //FrameworkElementFactory grid = new FrameworkElementFactory(typeof(Grid)); + //grid.Name = "mainTemplateGrid"; + //grid.SetValue(WidthProperty, 220.0); + //grid.SetValue(HeightProperty, 30.0); + //grid.SetValue(MarginProperty, new Thickness(-15,0,0,0)); + //grid.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Left); + //grid.SetValue(VerticalAlignmentProperty, VerticalAlignment.Stretch); + //grid.SetValue(BackgroundProperty, new SolidColorBrush(Colors.Transparent)); + + //FrameworkElementFactory textBlock = new FrameworkElementFactory(typeof(TextBlock)); + //textBlock.SetValue(HorizontalAlignmentProperty, HorizontalAlignment.Stretch); + //textBlock.SetValue(MarginProperty, new Thickness(15, 0, 0, 0)); + //textBlock.SetValue(IsHitTestVisibleProperty, false); + //textBlock.SetValue(VerticalAlignmentProperty, VerticalAlignment.Center); + //textBlock.SetValue(ForegroundProperty, new SolidColorBrush(Color.FromRgb(238,238,238))); + + //dismissedAlertsMenuItem.ItemTemplate = itemTemplate; + + //AddContextMenuItem(dismissedAlertsMenuItem, insertionPoint++); + + return null; + } + + internal static void AddInjectedNodeViewCustomizationMenuItems(OrderedDictionary nodeViewCustomizationMenuItems) + { + foreach (DictionaryEntry keyValuePair in nodeViewCustomizationMenuItems) + { + ContextMenu.Items.Add(keyValuePair.Value); + } + } + + internal static MenuItem BuildIsInputMenuItem() + { + return CreateMenuItem + ( + name: "isInput_cm", + header: Properties.Resources.NodeContextMenuIsInput, + isCheckable: true, + isChecked: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.IsSetAsInput)), + Mode = BindingMode.TwoWay, + } + ); + } + + internal static MenuItem BuildIsOutputMenuItem() + { + return CreateMenuItem + ( + name: "isOutput_cm", + header: Properties.Resources.NodeContextMenuIsOutput, + isCheckable: true, + isChecked: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.IsSetAsOutput)), + Mode = BindingMode.TwoWay, + } + ); + } + + internal static MenuItem BuildHelpMenuItem() + { + return CreateMenuItem + ( + name: "help_cm", + header: Properties.Resources.NodeContextMenuHelp, + command: new Binding + { + Source = NodeViewModel, + Path = new PropertyPath(nameof(NodeViewModel.ShowHelpCommand)) + } + ); + } + + /// + /// A list of the names of nodes' default menu items, which cannot be dynamically injected into its context menu. + /// + internal static readonly List NodeContextMenuDefaultItemNames = new List + { + Properties.Resources.ContextMenuDelete, + Properties.Resources.ContextMenuGroups, + Properties.Resources.NodeContextMenuPreview, + Properties.Resources.NodesRunStatus, + Properties.Resources.NodeContextMenuShowLabels, + Properties.Resources.NodeContextMenuRenameNode, + Properties.Resources.ContextMenuLacing, + //Wpf.Properties.Resources.NodeInformationalStateDismissedAlerts, + Properties.Resources.NodeContextMenuIsInput, + Properties.Resources.NodeContextMenuIsOutput, + Properties.Resources.NodeContextMenuHelp + }; + } +} diff --git a/src/DynamoCoreWpf/ViewModels/Core/NodeViewModel.cs b/src/DynamoCoreWpf/ViewModels/Core/NodeViewModel.cs index 33dbf032e71..68d54466b42 100644 --- a/src/DynamoCoreWpf/ViewModels/Core/NodeViewModel.cs +++ b/src/DynamoCoreWpf/ViewModels/Core/NodeViewModel.cs @@ -203,12 +203,7 @@ public bool IsSetAsOutput } } - /// - /// Used to determine whether the node's context menu display an Output/Input menu - /// - [JsonIgnore] - public bool IsInputOrOutput => IsInput || IsOutput; - + /// /// The Name of the nodemodel this view points to /// this is the name of the node as it is displayed in the UI. @@ -283,7 +278,7 @@ public bool IsCustomFunction { get { return nodeLogic.IsCustomFunction ? true : false; } } - + /// /// Element's left position is two-way bound to this value /// diff --git a/src/DynamoCoreWpf/Views/Core/NodeView.xaml b/src/DynamoCoreWpf/Views/Core/NodeView.xaml index d9564016027..9676379602a 100644 --- a/src/DynamoCoreWpf/Views/Core/NodeView.xaml +++ b/src/DynamoCoreWpf/Views/Core/NodeView.xaml @@ -13,7 +13,7 @@ MouseEnter="OnNodeViewMouseEnter" MouseLeave="OnNodeViewMouseLeave" MouseLeftButtonDown="topControl_MouseLeftButtonDown" - MouseRightButtonDown="topControl_MouseRightButtonDown" + MouseRightButtonDown="DisplayNodeContextMenu" PreviewMouseLeftButtonDown="OnPreviewMouseLeftButtonDown" PreviewMouseMove="OnNodeViewMouseMove"> @@ -47,7 +47,10 @@ - + @@ -190,108 +193,6 @@ - - - - - - - - - - - - - - - - - - - - - - - - - @@ -560,7 +461,7 @@