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;
}
}
}
}
}