using System; using System.Collections.Generic; using System.Linq; using System.Reactive; using System.Text; using System.Windows; using DynamicData; using NodeNetwork.ViewModels; using ReactiveUI; namespace NodeNetwork.Toolkit.ContextMenu { /// /// A viewmodel for a context menu that allows users to add nodes to a network. /// public class AddNodeContextMenuViewModel : SearchableContextMenuViewModel { static AddNodeContextMenuViewModel() { NNViewRegistrar.AddRegistration(() => new SearchableContextMenuView(), typeof(IViewFor)); } #region Network /// /// The network to which the nodes are to be added. /// public NetworkViewModel Network { get => _network; set => this.RaiseAndSetIfChanged(ref _network, value); } private NetworkViewModel _network; #endregion /// /// The format that is used to create labels for the menu entries based on the node name. /// E.g. "Add {0}" /// public string LabelFormat { get; } /// /// When adding a node to the network, /// this function is used to determine the position at which it is placed. /// public Func NodePositionFunc { get; set; } = (node) => new Point(); /// /// A callback that is called after a node is added to the network through this menu. /// public Action OnNodeAdded { get; set; } = node => { }; /// /// An interaction that is used to open contextmenu views given a SearchableContextMenuViewModel. /// Used in ShowAddNodeForPendingConnectionMenu to display this menu, and a menu for choosing an endpoint. /// public Interaction OpenContextMenu { get; } = new Interaction(); private ReactiveCommand CreateNode { get; } public AddNodeContextMenuViewModel(string labelFormat = "{0}") { LabelFormat = labelFormat; CreateNode = ReactiveCommand.Create((template) => { var nodeInstance = template.Factory(); Network.Nodes.Add(nodeInstance); nodeInstance.Position = NodePositionFunc(nodeInstance); OnNodeAdded(nodeInstance); return Unit.Default; }); } /// /// Adds a new node type to the list. /// Every time a node is added to a network from this list, the factory function in the template /// will be called to create a new instance of the viewmodel type. /// /// The template with the node type to add. public void AddNodeType(NodeTemplate template) { Commands.Add(new LabeledCommand { Label = string.Format(LabelFormat, template.Instance.Name), Command = CreateNode, CommandParameter = template }); } public void AddNodeTypes(IEnumerable templates) { foreach (var nodeTemplate in templates) { AddNodeType(nodeTemplate); } } private void ShowOnlyConnectableNodes(PendingConnectionViewModel testCon) { foreach (var cmd in Commands.Items) { var curNodeTemplate = (NodeTemplate)cmd.CommandParameter; bool hasValidEndpoint = testCon.InputIsLocked ? GetConnectableOutputs(curNodeTemplate.Instance, testCon).Any() : GetConnectableInputs(curNodeTemplate.Instance, testCon).Any(); cmd.Visible = hasValidEndpoint; } } public void ShowAddNodeForPendingConnectionMenu(PendingConnectionViewModel pendingCon) { var testCon = new PendingConnectionViewModel(pendingCon.Parent) // Copy used to test which inputs/outputs will work with the pending connection { Input = pendingCon.Input, InputIsLocked = pendingCon.InputIsLocked, Output = pendingCon.Output, OutputIsLocked = pendingCon.OutputIsLocked }; ShowOnlyConnectableNodes(testCon); // After a node type is chosen, pick an endpoint OnNodeAdded = node => { if (testCon.InputIsLocked) { var outputs = GetConnectableOutputs(node, testCon).ToList(); if (outputs.Count == 1) { // If only 1 output matches, select this one Network.Connections.Add(Network.ConnectionFactory(pendingCon.Input, outputs[0])); Network.RemovePendingConnection(); } else { // Open a menu to let the user choose the desired output to connect to var chooseEndpointVM = new SearchableContextMenuViewModel(); var cmd = ReactiveCommand.Create((o) => { Network.Connections.Add(Network.ConnectionFactory(pendingCon.Input, o)); Network.RemovePendingConnection(); return Unit.Default; }); foreach (var output in outputs) { chooseEndpointVM.Commands.Add(new LabeledCommand { Command = cmd, CommandParameter = output, Label = output.Name }); } OpenContextMenu.Handle(chooseEndpointVM).Subscribe(); } } else { var inputs = GetConnectableInputs(node, testCon).ToList(); if (inputs.Count == 1) { Network.Connections.Add(Network.ConnectionFactory(inputs[0], pendingCon.Output)); Network.RemovePendingConnection(); } else { var chooseEndpointVM = new SearchableContextMenuViewModel(); var cmd = ReactiveCommand.Create((i) => { Network.Connections.Add(Network.ConnectionFactory(i, pendingCon.Output)); Network.RemovePendingConnection(); return Unit.Default; }); foreach (var input in inputs) { chooseEndpointVM.Commands.Add(new LabeledCommand { Command = cmd, CommandParameter = input, Label = input.Name }); } OpenContextMenu.Handle(chooseEndpointVM).Subscribe(); } } }; OpenContextMenu.Handle(this).Subscribe(); } /// /// Given a set of node templates, return those which have an endpoint /// that could be connected to the specified pending connection. /// public static IEnumerable GetConnectableNodes(IEnumerable candidateNodeTemplates, PendingConnectionViewModel testCon) { foreach (var curNode in candidateNodeTemplates) { bool hasValidEndpoint = testCon.InputIsLocked ? GetConnectableOutputs(curNode.Instance, testCon).Any() : GetConnectableInputs(curNode.Instance, testCon).Any(); if (hasValidEndpoint) { yield return curNode; } } } /// /// Given a node viewmodel, return the outputs which could be connected to the pending connection. /// Assumes testCon.Input is set. /// public static IEnumerable GetConnectableOutputs(NodeViewModel node, PendingConnectionViewModel testCon) { var validator = testCon.Input.ConnectionValidator; foreach (var curOutput in node.Outputs.Items) { testCon.Output = curOutput; if (curOutput.MaxConnections > 0 && validator(testCon).IsValid) { yield return curOutput; } } } /// /// Given a node viewmodel, return the inputs which could be connected to the pending connection. /// Assumes testCon.Output is set. /// public static IEnumerable GetConnectableInputs(NodeViewModel node, PendingConnectionViewModel testCon) { foreach (var curInput in node.Inputs.Items) { var validator = curInput.ConnectionValidator; testCon.Input = curInput; if (curInput.MaxConnections > 0 && validator(testCon).IsValid) { yield return curInput; } } } } }