Files
bluflame/intromat/NodeNetworkToolkit/ContextMenu/AddNodeContextMenuViewModel.cs
2026-04-18 22:31:51 +02:00

250 lines
9.8 KiB
C#

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
{
/// <summary>
/// A viewmodel for a context menu that allows users to add nodes to a network.
/// </summary>
public class AddNodeContextMenuViewModel : SearchableContextMenuViewModel
{
static AddNodeContextMenuViewModel()
{
NNViewRegistrar.AddRegistration(() => new SearchableContextMenuView(), typeof(IViewFor<AddNodeContextMenuViewModel>));
}
#region Network
/// <summary>
/// The network to which the nodes are to be added.
/// </summary>
public NetworkViewModel Network
{
get => _network;
set => this.RaiseAndSetIfChanged(ref _network, value);
}
private NetworkViewModel _network;
#endregion
/// <summary>
/// The format that is used to create labels for the menu entries based on the node name.
/// E.g. "Add {0}"
/// </summary>
public string LabelFormat { get; }
/// <summary>
/// When adding a node to the network,
/// this function is used to determine the position at which it is placed.
/// </summary>
public Func<NodeViewModel, Point> NodePositionFunc { get; set; } = (node) => new Point();
/// <summary>
/// A callback that is called after a node is added to the network through this menu.
/// </summary>
public Action<NodeViewModel> OnNodeAdded { get; set; } = node => { };
/// <summary>
/// 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.
/// </summary>
public Interaction<SearchableContextMenuViewModel, Unit> OpenContextMenu { get; } = new Interaction<SearchableContextMenuViewModel, Unit>();
private ReactiveCommand<NodeTemplate, Unit> CreateNode { get; }
public AddNodeContextMenuViewModel(string labelFormat = "{0}")
{
LabelFormat = labelFormat;
CreateNode = ReactiveCommand.Create<NodeTemplate, Unit>((template) =>
{
var nodeInstance = template.Factory();
Network.Nodes.Add(nodeInstance);
nodeInstance.Position = NodePositionFunc(nodeInstance);
OnNodeAdded(nodeInstance);
return Unit.Default;
});
}
/// <summary>
/// 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.
/// </summary>
/// <param name="template">The template with the node type to add.</param>
public void AddNodeType(NodeTemplate template)
{
Commands.Add(new LabeledCommand
{
Label = string.Format(LabelFormat, template.Instance.Name),
Command = CreateNode,
CommandParameter = template
});
}
public void AddNodeTypes(IEnumerable<NodeTemplate> 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<NodeOutputViewModel, Unit>((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<NodeInputViewModel, Unit>((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();
}
/// <summary>
/// Given a set of node templates, return those which have an endpoint
/// that could be connected to the specified pending connection.
/// </summary>
public static IEnumerable<NodeTemplate> GetConnectableNodes(IEnumerable<NodeTemplate> 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;
}
}
}
/// <summary>
/// Given a node viewmodel, return the outputs which could be connected to the pending connection.
/// Assumes testCon.Input is set.
/// </summary>
public static IEnumerable<NodeOutputViewModel> 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;
}
}
}
/// <summary>
/// Given a node viewmodel, return the inputs which could be connected to the pending connection.
/// Assumes testCon.Output is set.
/// </summary>
public static IEnumerable<NodeInputViewModel> 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;
}
}
}
}
}