port from perforce

This commit is contained in:
2026-04-18 22:31:51 +02:00
commit 8d0ab5b7cc
8409 changed files with 3972376 additions and 0 deletions

View File

@@ -0,0 +1,100 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using DynamicData;
using DynamicData.Aggregation;
using NodeNetwork.Views;
using ReactiveUI;
namespace NodeNetwork.ViewModels
{
/// <summary>
/// Represents a connection between a node input and a node output
/// </summary>
public class ConnectionViewModel : ReactiveObject
{
static ConnectionViewModel()
{
NNViewRegistrar.AddRegistration(() => new ConnectionView(), typeof(IViewFor<ConnectionViewModel>));
}
#region Logger
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endregion
/// <summary>
/// The network that contains this connection
/// </summary>
public NetworkViewModel Parent { get; }
/// <summary>
/// The viewmodel of the node input that is on one end of the connection.
/// </summary>
public NodeInputViewModel Input { get; }
/// <summary>
/// The viewmodel of the node output that is on one end of the connection.
/// </summary>
public NodeOutputViewModel Output { get; }
#region CanBeRemovedByUser
/// <summary>
/// If false, the user cannot delete this connection. True by default.
/// </summary>
public bool CanBeRemovedByUser
{
get => _canBeRemovedByUser;
set => this.RaiseAndSetIfChanged(ref _canBeRemovedByUser, value);
}
private bool _canBeRemovedByUser;
#endregion
#region IsHighlighted
/// <summary>
/// If true, the connection is highlighted.
/// </summary>
public bool IsHighlighted
{
get => _isHighlighted;
set => this.RaiseAndSetIfChanged(ref _isHighlighted, value);
}
private bool _isHighlighted;
#endregion
#region IsInErrorState
/// <summary>
/// If true, the connection is displayed as being in an erroneous state.
/// </summary>
public bool IsInErrorState
{
get => _isInErrorState;
set => this.RaiseAndSetIfChanged(ref _isInErrorState, value);
}
private bool _isInErrorState;
#endregion
#region IsMarkedForDelete
/// <summary>
/// If true, the connection is displayed as being marked for deletion.
/// </summary>
public bool IsMarkedForDelete => _isMarkedForDelete.Value;
private ObservableAsPropertyHelper<bool> _isMarkedForDelete;
#endregion
public ConnectionViewModel(NetworkViewModel parent, NodeInputViewModel input, NodeOutputViewModel output)
{
Parent = parent;
Input = input;
Output = output;
this.WhenAnyValue(v => v.Parent.CutLine.IntersectingConnections)
.Where(l => l != null)
.Select(list => list.Connect().Filter(c => c == this).Count().Select(c => c > 0))
.Switch()
.ToProperty(this, vm => vm.IsMarkedForDelete, out _isMarkedForDelete);
}
}
}

View File

@@ -0,0 +1,61 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using DynamicData;
using ReactiveUI;
namespace NodeNetwork.ViewModels
{
/// <summary>
/// Viewmodel class for the UI cutting line that is used to delete connections.
/// </summary>
public class CutLineViewModel : ReactiveObject
{
#region StartPoint
/// <summary>
/// The coordinates of the point at which the cutting line starts.
/// </summary>
public Point StartPoint
{
get => _startPoint;
set => this.RaiseAndSetIfChanged(ref _startPoint, value);
}
private Point _startPoint;
#endregion
#region EndPoint
/// <summary>
/// The coordinates of the point at which the cutting line ends.
/// </summary>
public Point EndPoint
{
get => _endPoint;
set => this.RaiseAndSetIfChanged(ref _endPoint, value);
}
private Point _endPoint;
#endregion
#region IsVisible
/// <summary>
/// If true, the cutting line is visible. If false, the cutting line is hidden.
/// </summary>
public bool IsVisible
{
get => _isVisible;
set => this.RaiseAndSetIfChanged(ref _isVisible, value);
}
private bool _isVisible;
#endregion
#region IntersectingConnections
/// <summary>
/// A list of connections that visually intersect with the cutting line.
/// This list is driven by the view.
/// </summary>
public ISourceList<ConnectionViewModel> IntersectingConnections { get; } = new SourceList<ConnectionViewModel>();
#endregion
}
}

View File

@@ -0,0 +1,255 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.IO;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using DynamicData;
using NodeNetwork.Utilities;
using ReactiveUI;
using Splat;
namespace NodeNetwork.ViewModels
{
/// <summary>
/// Enum type that indicates the position of the port in the endpoint
/// </summary>
public enum PortPosition
{
Left, Right
}
/// <summary>
/// Enum types that indicates the visibility behaviour of an endpoint
/// </summary>
public enum EndpointVisibility
{
/// <summary>
/// Automatically decide whether or not to show this endpoint based on the collapse status of the node
/// </summary>
Auto,
/// <summary>
/// Always show this endpoint, even if the node is collapsed
/// </summary>
AlwaysVisible,
/// <summary>
/// Always hide this endpoint
/// </summary>
AlwaysHidden
}
/// <summary>
/// Parent interface for the inputs/outputs of nodes between which connections can be made.
/// </summary>
public abstract class Endpoint : ReactiveObject
{
#region Parent
/// <summary>
/// The node that owns this endpoint
/// </summary>
public NodeViewModel Parent
{
get => _parent;
internal set => this.RaiseAndSetIfChanged(ref _parent, value);
}
private NodeViewModel _parent;
#endregion
#region Name
/// <summary>
/// The name of this endpoint.
/// In the default view, this string is displayed in the node next to the port.
/// </summary>
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
private string _name = "";
#endregion
#region Group
/// <summary>
/// The group the end point belongs to. Can be null.
/// </summary>
public EndpointGroup Group
{
get => _group;
set => this.RaiseAndSetIfChanged(ref _group, value);
}
private EndpointGroup _group;
#endregion
#region Icon
/// <summary>
/// The icon displayed near the endpoint label
/// If this is null, no icon is displayed.
/// </summary>
public IBitmap Icon
{
get => _icon;
set => this.RaiseAndSetIfChanged(ref _icon, value);
}
private IBitmap _icon;
#endregion
#region Editor
/// <summary>
/// The editor viewmodel associated with this endpoint.
/// It can be used to configure the behaviour of this endpoint or provide a default value when there is no connection.
/// The editor, if not null, will be displayed in the node, under the endpoint name next to the port.
/// </summary>
public NodeEndpointEditorViewModel Editor
{
get => _editor;
set => this.RaiseAndSetIfChanged(ref _editor, value);
}
private NodeEndpointEditorViewModel _editor;
#endregion
#region Port
/// <summary>
/// The viewmodel for the port of this endpoint. (the part the user can create connections from.)
/// </summary>
public PortViewModel Port
{
get => _port;
set => this.RaiseAndSetIfChanged(ref _port, value);
}
private PortViewModel _port;
#endregion
#region PortPosition
/// <summary>
/// Where should the port be positioned in the endpoint?
/// </summary>
public PortPosition PortPosition
{
get => _portPosition;
set => this.RaiseAndSetIfChanged(ref _portPosition, value);
}
private PortPosition _portPosition;
#endregion
#region Connections
/// <summary>
/// List of connections between this endpoint and other endpoints in the network.
/// To add a new connection, do not add it here but instead add it to the Connections property in the network.
/// </summary>
public IObservableList<ConnectionViewModel> Connections { get; }
#endregion
#region MaxConnections
/// <summary>
/// The maximum amount of connections this endpoint accepts.
/// When Connections.Count == MaxConnections, the user cannot add more connections to this endpoint
/// until a connection is removed.
/// </summary>
public int MaxConnections
{
get => _maxConnections;
set => this.RaiseAndSetIfChanged(ref _maxConnections, value);
}
private int _maxConnections;
#endregion
#region Visibility
/// <summary>
/// Visibility behaviour of this endpoint
/// </summary>
public EndpointVisibility Visibility
{
get => _visibility;
set => this.RaiseAndSetIfChanged(ref _visibility, value);
}
private EndpointVisibility _visibility;
#endregion
#region SortIndex
/// <summary>
/// Inputs and outputs are sorted by increasing values of SortIndex before being displayed.
/// </summary>
public int SortIndex
{
get => _sortIndex;
set => this.RaiseAndSetIfChanged(ref _sortIndex, value);
}
private int _sortIndex;
#endregion
protected Endpoint()
{
Port = new PortViewModel();
Visibility = EndpointVisibility.Auto;
// Setup parent relationship with Port.
this.WhenAnyValue(vm => vm.Port).PairWithPreviousValue().Subscribe(p =>
{
if (p.OldValue != null)
{
p.OldValue.Parent = null;
}
if (p.NewValue != null)
{
p.NewValue.Parent = this;
}
});
// Setup Parent relationship with Editor.
this.WhenAnyValue(vm => vm.Editor).PairWithPreviousValue().Subscribe(e =>
{
if (e.OldValue != null)
{
e.OldValue.Parent = null;
}
if (e.NewValue != null)
{
e.NewValue.Parent = this;
}
});
// Mirror the port if the endpoint is on the left instead of the right.
this.WhenAnyValue(vm => vm.Port, vm => vm.PortPosition).Subscribe(_ =>
{
if (Port == null)
{
return;
}
Port.IsMirrored = PortPosition == PortPosition.Left;
});
// Setup a binding between the Connections list in the network and in this endpoint,
// selecting only the connections where this endpoint is the input or output.
// We need the latest network connections list, but we want a null value when this endpoint is
// removed from the node, or the node is removed from the network.
var networkConnections = this.WhenAnyValue(
vm => vm.Parent,
vm => vm.Parent.Parent,
vm => vm.Parent.Parent.Connections,
(x, y, z) => Parent?.Parent?.Connections ?? new SourceList<ConnectionViewModel>())
.Switch();
Connections = networkConnections
.AutoRefresh(c => c.Input)
.AutoRefresh(c => c.Output)
.Filter(c => c.Input == this || c.Output == this)
.AsObservableList();
// Setup bindings between port mouse events and connection creation.
this.WhenAnyObservable(vm => vm.Port.ConnectionDragStarted).Subscribe(_ => CreatePendingConnection());
this.WhenAnyObservable(vm => vm.Port.ConnectionPreviewActive).Subscribe(SetConnectionPreview);
this.WhenAnyObservable(vm => vm.Port.ConnectionDragFinished).Subscribe(_ => FinishPendingConnection());
}
protected abstract void CreatePendingConnection();
protected abstract void SetConnectionPreview(bool previewActive);
protected abstract void FinishPendingConnection();
}
}

View File

@@ -0,0 +1,33 @@
using System;
using System.Collections.Generic;
using System.Text;
using ReactiveUI;
namespace NodeNetwork.ViewModels
{
public class EndpointGroup : ReactiveObject
{
public EndpointGroup Parent { get; }
#region Name
private string _name = "";
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
#endregion
public EndpointGroup(EndpointGroup parent = null)
{
Parent = parent;
}
public EndpointGroup(string name, EndpointGroup parent = null)
{
Parent = parent;
Name = name;
}
}
}

View File

@@ -0,0 +1,72 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Reactive.Linq;
using System.Text;
using DynamicData;
using NodeNetwork.Views;
using ReactiveUI;
using Splat;
namespace NodeNetwork.ViewModels
{
public class EndpointGroupViewModel : ReactiveObject
{
static EndpointGroupViewModel()
{
NNViewRegistrar.AddRegistration(() => new EndpointGroupView(), typeof(IViewFor<EndpointGroupViewModel>));
}
#region VisibleInputs
/// <summary>
/// The list of inputs that is currently visible on this group.
/// Some inputs may be hidden if the node is collapsed.
/// </summary>
public IObservableList<NodeInputViewModel> VisibleInputs { get; }
#endregion
#region VisibleOutputs
/// <summary>
/// The list of outputs that is currently visible on this group.
/// Some outputs may be hidden if the node is collapsed.
/// </summary>
public IObservableList<NodeOutputViewModel> VisibleOutputs { get; }
#endregion
#region Group
/// <summary>
/// The endpoint group wrapping the name and the parent group of this group.
/// </summary>
public EndpointGroup Group { get; }
#endregion
#region Children
/// <summary>
/// The list of nested endpoint groups.
/// </summary>
public ReadOnlyObservableCollection<EndpointGroupViewModel> Children => _children;
private readonly ReadOnlyObservableCollection<EndpointGroupViewModel> _children;
#endregion
public EndpointGroupViewModel(
EndpointGroup group,
IObservable<IChangeSet<NodeInputViewModel>> allInputs,
IObservable<IChangeSet<NodeOutputViewModel>> allOutputs,
IObservableCache<Node<EndpointGroup, EndpointGroup>, EndpointGroup> children,
EndpointGroupViewModelFactory endpointGroupViewModelFactory)
{
Group = group;
VisibleInputs = allInputs.Filter(e => e.Group == group).AsObservableList();
VisibleOutputs = allOutputs.Filter(e => e.Group == group).AsObservableList();
children
.Connect()
.Transform(n => endpointGroupViewModelFactory(n.Key, allInputs, allOutputs, n.Children, endpointGroupViewModelFactory))
.Bind(out _children)
.Subscribe();
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using DynamicData;
namespace NodeNetwork.ViewModels
{
/// <summary>
/// The factory method to create endpoint view models. Used in NodeViewModel.
/// </summary>
/// <param name="group">The endpoint group this view model wraps.</param>
/// <param name="allInputs">All inputs of the group.</param>
/// <param name="allOutputs">All outputs of the group.</param>
/// <param name="children">Nested endpoint groups.</param>
/// <param name="endpointGroupViewModelFactory">The factory method used to create the nested endpoint group view models.</param>
/// <returns>The view model for the endpoint group.</returns>
public delegate EndpointGroupViewModel EndpointGroupViewModelFactory(
EndpointGroup group,
IObservable<IChangeSet<NodeInputViewModel>> allInputs,
IObservable<IChangeSet<NodeOutputViewModel>> allOutputs,
IObservableCache<Node<EndpointGroup, EndpointGroup>, EndpointGroup> children,
EndpointGroupViewModelFactory endpointGroupViewModelFactory);
}

View File

@@ -0,0 +1,35 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NodeNetwork.Views;
using ReactiveUI;
namespace NodeNetwork.ViewModels
{
/// <summary>
/// A viewmodel for a simple error message.
/// </summary>
public class ErrorMessageViewModel : ReactiveObject
{
static ErrorMessageViewModel()
{
NNViewRegistrar.AddRegistration(() => new ErrorMessageView(), typeof(IViewFor<ErrorMessageViewModel>));
}
#region Logger
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endregion
/// <summary>
/// The text to be displayed that explains the error.
/// </summary>
public string Message { get; }
public ErrorMessageViewModel(string message)
{
Message = message;
}
}
}

View File

@@ -0,0 +1,430 @@
using NodeNetwork.Views;
using NodeNetwork.Utilities;
using ReactiveUI;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Reactive;
using System.Reactive.Concurrency;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Reactive.Subjects;
using System.Reactive.Threading.Tasks;
using System.Threading.Tasks;
using System.Windows;
using DynamicData;
using DynamicData.Alias;
namespace NodeNetwork.ViewModels
{
/// <summary>
/// The viewmodel for node networks.
/// </summary>
public class NetworkViewModel : ReactiveObject
{
static NetworkViewModel()
{
NNViewRegistrar.AddRegistration(() => new NetworkView(), typeof(IViewFor<NetworkViewModel>));
}
#region Logger
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endregion
#region Nodes
/// <summary>
/// The list of nodes in this network.
/// </summary>
public ISourceList<NodeViewModel> Nodes { get; } = new SourceList<NodeViewModel>();
#endregion
#region SelectedNodes
/// <summary>
/// A list of nodes that are currently selected in the UI.
/// The contents of this list is equal to the nodes in Nodes where the Selected property is true.
/// </summary>
public IObservableList<NodeViewModel> SelectedNodes { get; }
#endregion
#region Connections
/// <summary>
/// The list of connections in this network.
/// </summary>
public ISourceList<ConnectionViewModel> Connections { get; } = new SourceList<ConnectionViewModel>();
#endregion
#region PendingConnection
/// <summary>
/// The connection that is currently being build by the user.
/// This connection is visually displayed in the UI, but is not an actual functional connection.
/// This is used when the user drags from an endpoint to create a new connection.
/// </summary>
public PendingConnectionViewModel PendingConnection
{
get => _pendingConnection;
set => this.RaiseAndSetIfChanged(ref _pendingConnection, value);
}
private PendingConnectionViewModel _pendingConnection;
public Action OnPendingConnectionDropped { get; set; }
#endregion
#region PendingNode
/// <summary>
/// The viewmodel of the node that is not part of the network, but is displayed as a node that can be added.
/// This property is used to display a new node when the user drags a node viewmodel over the network view.
/// </summary>
public NodeViewModel PendingNode
{
get => _pendingNode;
set => this.RaiseAndSetIfChanged(ref _pendingNode, value);
}
private NodeViewModel _pendingNode;
#endregion
#region ConnectionFactory
/// <summary>
/// The function that is used to create connection viewmodels when the user creates connections in the network view.
/// By default, this function creates a ConnectionViewModel.
/// </summary>
public Func<NodeInputViewModel, NodeOutputViewModel, ConnectionViewModel> ConnectionFactory
{
get => _connectionFactory;
set => this.RaiseAndSetIfChanged(ref _connectionFactory, value);
}
private Func<NodeInputViewModel, NodeOutputViewModel, ConnectionViewModel> _connectionFactory;
#endregion
#region Validator
/// <summary>
/// Function that is used to check if the network is valid or not.
/// To run the validation, use the UpdateValidation command.
/// </summary>
public Func<NetworkViewModel, NetworkValidationResult> Validator
{
get => _validator;
set => this.RaiseAndSetIfChanged(ref _validator, value);
}
private Func<NetworkViewModel, NetworkValidationResult> _validator;
#endregion
#region LatestValidation
//Using ObservableAsPropertyHelper would be better, but causes problems with ReactiveCommand where
//the value of the property is updated only after the subscribers to the command are run.
/// <summary>
/// The validation of the current state of the network.
/// This property is automatically updated when UpdateValidation runs.
/// </summary>
public NetworkValidationResult LatestValidation
{
get => _latestValidation;
private set => this.RaiseAndSetIfChanged(ref _latestValidation, value);
}
private NetworkValidationResult _latestValidation;
#endregion
#region Validation
/// <summary>
/// Observable that produces the latest NetworkValidationResult every time the network is validated.
/// </summary>
public IObservable<NetworkValidationResult> Validation { get; }
#endregion
#region IsReadOnly
/// <summary>
/// If true, the network and its contents (nodes, connections, input/output editors, ...) cannot be modified by the user.
/// </summary>
public bool IsReadOnly
{
get => _isReadOnly;
set => this.RaiseAndSetIfChanged(ref _isReadOnly, value);
}
private bool _isReadOnly;
#endregion
#region CutLine
/// <summary>
/// The viewmodel of the cutline used in this network view.
/// </summary>
public CutLineViewModel CutLine { get; } = new CutLineViewModel();
#endregion
#region ZoomFactor
/// <summary>
/// Scale of the view. Larger means more zoomed in. Default value is 1.
/// </summary>
public double ZoomFactor
{
get => _zoomFactor;
set => this.RaiseAndSetIfChanged(ref _zoomFactor, value);
}
private double _zoomFactor = 1;
/// <summary>
/// The maximum zoom level used in this network view. Default value is 2.5.
/// </summary>
public double MaxZoomLevel
{
get => _maxZoomLevel;
set => this.RaiseAndSetIfChanged(ref _maxZoomLevel, value);
}
private double _maxZoomLevel = 2.5;
/// <summary>
/// The minimum zoom level used in this network view. Default value is 0.15.
/// </summary>
public double MinZoomLevel
{
get => _minZoomLevel;
set => this.RaiseAndSetIfChanged(ref _minZoomLevel, value);
}
private double _minZoomLevel = 0.15;
/// <summary>
/// The drag offset of the initial view position used in this network view. Default value is (0, 0).
/// </summary>
public Point DragOffset
{
get => _dragOffset;
set => this.RaiseAndSetIfChanged(ref _dragOffset, value);
}
private Point _dragOffset = new Point(0, 0);
#endregion
#region SelectionRectangle
/// <summary>
/// The viewmodel for the selection rectangle used in this network view.
/// </summary>
public SelectionRectangleViewModel SelectionRectangle { get; } = new SelectionRectangleViewModel();
#endregion
#region NetworkChanged
/// <summary>
/// This observable pushes a notification when a connection was added to/removed from the network,
/// and the relevant endpoints have been updated.
/// </summary>
/// <remarks>
/// Observing the Connections list directly will trigger the same notifications,
/// but before the endpoints have had a chance to update and so they may be in an invalid state.
/// </remarks>
public IObservable<Unit> ConnectionsUpdated { get; }
/// <summary>
/// This observable pushes a notification whenever any functional changes are made to the network.
/// Purely esthetical changes, such as the collapsing of nodes, do not trigger this observable.
/// </summary>
public IObservable<Unit> NetworkChanged { get; }
#endregion
#region Commands
/// <summary>
/// Deletes the nodes in SelectedNodes that are user-removable.
/// </summary>
public ReactiveCommand<Unit, Unit> DeleteSelectedNodes { get; }
/// <summary>
/// Runs the Validator function and stores the result in LatestValidation.
/// </summary>
public ReactiveCommand<Unit, NetworkValidationResult> UpdateValidation { get; }
#endregion
public NetworkViewModel()
{
// Setup parent relationship in nodes.
Nodes.Connect().ActOnEveryObject(
addedNode => addedNode.Parent = this,
removedNode => removedNode.Parent = null
);
// SelectedNodes is a derived collection of all nodes with IsSelected = true.
SelectedNodes = Nodes.Connect()
.AutoRefresh(node => node.IsSelected)
.Filter(node => node.IsSelected)
.AsObservableList();
// When DeleteSelectedNodes is invoked, remove all nodes that are user-removable and selected.
DeleteSelectedNodes = ReactiveCommand.Create(OnDeleteSelectedNodes);
// When a node is removed, delete any connections from/to that node.
Nodes.Preview().OnItemRemoved(removedNode =>
{
Connections.RemoveMany(removedNode.Inputs.Items.SelectMany(o => o.Connections.Items));
Connections.RemoveMany(removedNode.Outputs.Items.SelectMany(o => o.Connections.Items));
bool pendingConnectionInvalid = PendingConnection?.Input?.Parent == removedNode ||
PendingConnection?.Output?.Parent == removedNode;
if (pendingConnectionInvalid)
{
RemovePendingConnection();
}
}).Subscribe();
// If, while dragging a pending connection, the mouse is released over the canvas, then cancel the connection.
OnPendingConnectionDropped = RemovePendingConnection;
// When the list of nodes is reset, remove any connections whose input/output node was removed.
/*Nodes.ShouldReset.Subscribe(_ =>
{
// Create a hashset with all nodes for O(1) search
HashSet<NodeViewModel> nodeSet = new HashSet<NodeViewModel>(Nodes);
var connections = Connections.Items.ToArray();
for (var i = connections.Length - 1; i >= 0; i--)
{
if (!nodeSet.Contains(connections[i].Input.Parent) || !nodeSet.Contains(connections[i].Output.Parent))
{
Connections.RemoveAt(i);
}
}
var pendingConnInputNode = PendingConnection?.Input?.Parent;
var pendingConnOutputNode = PendingConnection?.Output?.Parent;
bool pendingConnectionInvalid = (pendingConnInputNode != null && !nodeSet.Contains(pendingConnInputNode)) ||
(pendingConnOutputNode != null && !nodeSet.Contains(pendingConnOutputNode));
if (pendingConnectionInvalid)
{
RemovePendingConnection();
}
});*/
// Setup a default ConnectionFactory that will be used to create connections.
ConnectionFactory = (input, output) => new ConnectionViewModel(this, input, output);
// Setup a default network validator that always returns valid.
Validator = _ => new NetworkValidationResult(true, true, null);
// Setup the validation command.
UpdateValidation = ReactiveCommand.Create(() => {
var result = Validator(this);
LatestValidation = result;
return result;
});
// Setup Validation observable
var onValidationPropertyUpdate = this.WhenAnyValue(vm => vm.LatestValidation).Publish().RefCount();
Validation = Observable.Defer(() => onValidationPropertyUpdate.StartWith(LatestValidation));
// When a connection or node changes, validate the network.
// Zip is used because when a connection is removed, it will trigger a change in both the input and the output and we want to combine these.
var a = Nodes.Connect()
.AutoRefreshOnObservable(node => node.Inputs.Connect())
.SelectMany(node => node.Inputs.Items)
.AutoRefreshOnObservable(input => input.Connections.Connect())
.SelectMany(input => input.Connections.Items);
var b = Nodes.Connect()
.AutoRefreshOnObservable(node => node.Outputs.Connect())
.SelectMany(node => node.Outputs.Items)
.AutoRefreshOnObservable(output => output.Connections.Connect())
.SelectMany(output => output.Connections.Items);
ConnectionsUpdated = Observable.Zip(
a,
b,
(x, y) => Unit.Default
).Publish().RefCount();
ConnectionsUpdated.InvokeCommand(UpdateValidation);
Nodes.Connect().Select((IChangeSet<NodeViewModel> n) => Unit.Default).InvokeCommand(UpdateValidation);
// Push a network change notification when a functional network change occurs.
// These include:
// - Nodes are added/removed
// - Connections are added/removed
// - Endpoint editors change
// - Network validation changes
NetworkChanged = Observable.Merge(
Observable.Select(Nodes.Connect(), _ => Unit.Default),
Observable.Select(Nodes.Connect().MergeMany(node => node.Inputs.Connect()), _ => Unit.Default),
Observable.Select(Nodes.Connect().MergeMany(node => node.Outputs.Connect()), _ => Unit.Default),
ConnectionsUpdated,
OnEditorChanged(),
Validation.Select(_ => Unit.Default)
).Publish().RefCount();
}
protected virtual void OnDeleteSelectedNodes()
{
Nodes.RemoveMany(SelectedNodes.Items.Where(n => n.CanBeRemovedByUser).ToArray());
}
private IObservable<Unit> OnEditorChanged()
{
return Observable.Merge(
Nodes.Connect().MergeMany(n =>
n.Inputs.Connect().MergeMany(i =>
// Use WhenAnyObservable because Editor can change.
i.WhenAnyObservable(vm => vm.Editor.Changed)
)
).Select(_ => Unit.Default),
Nodes.Connect().MergeMany(n =>
n.Outputs.Connect().MergeMany(o =>
o.WhenAnyObservable(vm => vm.Editor.Changed)
)
).Select(_ => Unit.Default)
);
}
/// <summary>
/// Clears SelectedNodes, setting the IsSelected property of all the nodes to false.
/// </summary>
public void ClearSelection()
{
foreach (NodeViewModel node in SelectedNodes.Items)
{
node.IsSelected = false;
}
}
/// <summary>
/// Starts a cut in the CutLine viewmodel.
/// </summary>
public void StartCut()
{
CutLine.IsVisible = true;
}
/// <summary>
/// Stops the current cut in the CutLine viewmodel and applies the changes.
/// </summary>
public virtual void FinishCut()
{
Connections.RemoveMany(CutLine.IntersectingConnections.Items);
CutLine.IsVisible = false;
CutLine.IntersectingConnections.Clear();
}
/// <summary>
/// Sets PendingConnection to null.
/// </summary>
public void RemovePendingConnection()
{
PendingConnection = null;
}
/// <summary>
/// Starts a selection in RectangleSelection
/// </summary>
public void StartRectangleSelection()
{
ClearSelection();
SelectionRectangle.IsVisible = true;
SelectionRectangle.IntersectingNodes.Clear();
}
/// <summary>
/// Stops the current selection in RectangleSelection and applies the changes.
/// </summary>
public void FinishRectangleSelection()
{
SelectionRectangle.IsVisible = false;
}
}
}

View File

@@ -0,0 +1,39 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using NodeNetwork.Views;
using ReactiveUI;
namespace NodeNetwork.ViewModels
{
/// <summary>
/// The viewmodel for the editor component that is displayed next to a node endpoint.
/// </summary>
public class NodeEndpointEditorViewModel : ReactiveObject
{
static NodeEndpointEditorViewModel()
{
NNViewRegistrar.AddRegistration(() => new NodeEndpointEditorView(), typeof(IViewFor<NodeEndpointEditorViewModel>));
}
#region Logger
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endregion
#region Parent
/// <summary>
/// The endpoint that has this object as its editor.
/// </summary>
public Endpoint Parent
{
get => _parent;
internal set => this.RaiseAndSetIfChanged(ref _parent, value);
}
private Endpoint _parent;
#endregion
}
}

View File

@@ -0,0 +1,203 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using NodeNetwork.Views;
using ReactiveUI;
using System.Reactive.Linq;
using DynamicData;
namespace NodeNetwork.ViewModels
{
/// <summary>
/// Viewmodel class for inputs on a node.
/// Inputs are endpoints that can only be connected to outputs.
/// </summary>
public class NodeInputViewModel : Endpoint
{
static NodeInputViewModel()
{
NNViewRegistrar.AddRegistration(() => new NodeInputView(), typeof(IViewFor<NodeInputViewModel>));
}
#region Logger
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endregion
#region IsEditorVisible
/// <summary>
/// If true, the editor is visible. Otherwise, the editor is hidden.
/// See HideEditorIfConnected.
/// </summary>
public bool IsEditorVisible => _isEditorVisible.Value;
private ObservableAsPropertyHelper<bool> _isEditorVisible;
#endregion
#region HideEditorIfConnected
/// <summary>
/// If true, the editor of this input will be hidden if Connection is not null.
/// This makes sense if the editor is used to provide a value when no connection is present.
/// </summary>
public bool HideEditorIfConnected
{
get => _hideEditorIfConnected;
set => this.RaiseAndSetIfChanged(ref _hideEditorIfConnected, value);
}
private bool _hideEditorIfConnected;
#endregion
#region ConnectionValidator
/// <summary>
/// This function is called when a new connection with this input is pending.
/// It decides whether or not the pending connection is valid.
/// If the validation result says the pending connection is invalid,
/// then the user will not be able to add the connection to the network.
/// </summary>
public Func<PendingConnectionViewModel, ConnectionValidationResult> ConnectionValidator
{
get => _connectionValidator;
set => this.RaiseAndSetIfChanged(ref _connectionValidator, value);
}
private Func<PendingConnectionViewModel, ConnectionValidationResult> _connectionValidator;
#endregion
public NodeInputViewModel()
{
this.HideEditorIfConnected = true;
this.Connections.CountChanged.Select(c => c == 0).StartWith(true)
.CombineLatest(this.WhenAnyValue(vm => vm.HideEditorIfConnected), (noConnections, hideEditorIfConnected) => !hideEditorIfConnected || noConnections)
.ToProperty(this, vm => vm.IsEditorVisible, out _isEditorVisible);
this.ConnectionValidator = con => new ConnectionValidationResult(true, null);
this.MaxConnections = 1;
this.PortPosition = PortPosition.Left;
}
/// <summary>
/// Sets the pending connection in the network to a new connection with this endpoint as the input.
/// If this input already is connected, and MaxConnections == 1,
/// then the connection is replaced by a pending connection without this endpoint.
/// If the connection would be invalid, no pending connection is made.
/// Called when the user clicks on this endpoint.
/// </summary>
protected override void CreatePendingConnection()
{
NetworkViewModel network = Parent?.Parent;
if (network == null)
{
return;
}
PendingConnectionViewModel pendingConnection;
if (MaxConnections == 1 && Connections.Items.Any())
{
var conn = Connections.Items.First();
pendingConnection = new PendingConnectionViewModel(network)
{
Output = conn.Output,
OutputIsLocked = true,
LooseEndPoint = Port.CenterPoint
};
network.Connections.Remove(conn);
}
else if(Connections.Count < MaxConnections)
{
pendingConnection = new PendingConnectionViewModel(network) { Input = this, InputIsLocked = true, LooseEndPoint = Port.CenterPoint };
}
else
{
return;
}
pendingConnection.LooseEndPoint = Port.CenterPoint;
network.PendingConnection = pendingConnection;
}
/// <summary>
/// Sets this endpoint as the input of the pending connection and updates its validation.
/// Called when the user drags and holds a pending connection over this endpoint.
/// </summary>
/// <param name="previewActive">
/// True to set this endpoint as the output of the pending connection.
/// To remove this endpoint from the pending connection, set this to false.
/// </param>
protected override void SetConnectionPreview(bool previewActive)
{
PendingConnectionViewModel pendingCon = Parent.Parent.PendingConnection;
if (pendingCon.Input != null && (pendingCon.Input != this || pendingCon.InputIsLocked))
{
return;
}
if (previewActive)
{
pendingCon.Input = this;
pendingCon.Validation = ConnectionValidator(pendingCon);
}
else
{
pendingCon.Input = null;
pendingCon.Validation = null;
}
}
/// <summary>
/// Tries to create a new connection in the network based on the pending connection and this endpoint as the input.
/// If the connection would be invalid, no connection is made.
/// The pending connection is deleted.
/// Called when the user drags and releases a pending connection over this endpoint.
/// </summary>
protected override void FinishPendingConnection()
{
NetworkViewModel network = Parent?.Parent;
if (network == null)
{
return;
}
if (network.PendingConnection.Input == this && !network.PendingConnection.InputIsLocked)
{
//Only allow drag from output to input, not input to input
if (network.PendingConnection.Input.Parent != network.PendingConnection.Output.Parent)
{
//Dont allow connections between an input and an output on the same node
if (network.PendingConnection.Validation.IsValid)
{
//Don't allow a new connection if max amount of connections has been reached and we
//can't automatically remove one.
if (Connections.Count < MaxConnections || MaxConnections == 1)
{
//Connection is valid
bool canCreateConnection = true;
if (MaxConnections == Connections.Count && MaxConnections == 1)
{
//Remove the connection to this input
network.Connections.Remove(Connections.Items.First());
}
else if (MaxConnections > 2)
{
// Make sure connection does not exist already.
if (network.Connections.Items.Any(con => con.Output == network.PendingConnection.Output && con.Input == this))
{
canCreateConnection = false;
}
}
if (canCreateConnection)
{
//Add new connection
network.Connections.Add(network.ConnectionFactory(this, network.PendingConnection.Output));
}
}
}
}
}
network.RemovePendingConnection();
}
}
}

View File

@@ -0,0 +1,124 @@
using System;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using DynamicData;
using NodeNetwork.Utilities;
using NodeNetwork.Views;
using ReactiveUI;
namespace NodeNetwork.ViewModels
{
/// <summary>
/// Viewmodel class for outputs on a node.
/// Outputs are endpoints that can only be connected to inputs.
/// </summary>
public class NodeOutputViewModel : Endpoint
{
static NodeOutputViewModel()
{
NNViewRegistrar.AddRegistration(() => new NodeOutputView(), typeof(IViewFor<NodeOutputViewModel>));
}
#region Logger
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endregion
public NodeOutputViewModel()
{
MaxConnections = Int32.MaxValue;
this.PortPosition = PortPosition.Right;
}
/// <summary>
/// Sets the pending connection in the network to a new connection with this endpoint as the output.
/// If the connection would be invalid, no pending connection is made.
/// Called when the user clicks on this endpoint.
/// </summary>
protected override void CreatePendingConnection()
{
NetworkViewModel network = Parent?.Parent;
if (network == null)
{
return;
}
if (Connections.Count >= MaxConnections)
{
return;
}
network.PendingConnection = new PendingConnectionViewModel(network) { Output = this, OutputIsLocked = true, LooseEndPoint = Port.CenterPoint };
}
/// <summary>
/// Sets this endpoint as the output of the pending connection and updates its validation.
/// Called when the user drags and holds a pending connection over this endpoint.
/// </summary>
/// <param name="previewActive">
/// True to set this endpoint as the output of the pending connection.
/// To remove this endpoint from the pending connection, set this to false.
/// </param>
protected override void SetConnectionPreview(bool previewActive)
{
PendingConnectionViewModel pendingCon = Parent.Parent.PendingConnection;
if (pendingCon.Output != null && (pendingCon.Output != this || pendingCon.OutputIsLocked))
{
return;
}
if (previewActive)
{
pendingCon.Output = this;
pendingCon.Validation = pendingCon.Input.ConnectionValidator(pendingCon);
}
else
{
pendingCon.Output = null;
pendingCon.Validation = null;
}
}
/// <summary>
/// Tries to create a new connection in the network based on the pending connection and this endpoint as the output.
/// If the connection would be invalid, no connection is made.
/// The pending connection is deleted.
/// Called when the user drags and releases a pending connection over this endpoint.
/// </summary>
protected override void FinishPendingConnection()
{
NetworkViewModel network = Parent?.Parent;
if (network == null)
{
return;
}
if (network.PendingConnection.Output == this && !network.PendingConnection.OutputIsLocked)
{
//Only allow drag from output to input, not input to input
if (network.PendingConnection.Input.Parent != network.PendingConnection.Output.Parent)
{
//Dont allow connections between an input and an output on the same node
if (network.PendingConnection.Validation.IsValid)
{
//Connection is valid
if (MaxConnections > Connections.Count)
{
//MaxConnections hasn't been reached yet.
if (!network.Connections.Items.Any(con => con.Output == this && con.Input == network.PendingConnection.Input))
{
//Connection does not exist already
network.Connections.Add(network.ConnectionFactory(network.PendingConnection.Input, this));
}
}
}
}
}
network.RemovePendingConnection();
}
}
}

View File

@@ -0,0 +1,352 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Reactive;
using System.Reactive.Linq;
using System.Runtime.Serialization;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using DynamicData;
using DynamicData.Binding;
using NodeNetwork.Views;
using ReactiveUI;
using Splat;
namespace NodeNetwork.ViewModels
{
public enum ResizeOrientation
{
None,
Horizontal,
Vertical,
HorizontalAndVertical
}
/// <summary>
/// Viewmodel class for the nodes in the network
/// </summary>
public class NodeViewModel : ReactiveObject
{
static NodeViewModel()
{
NNViewRegistrar.AddRegistration(() => new NodeView(), typeof(IViewFor<NodeViewModel>));
Locator.CurrentMutable.RegisterPlatformBitmapLoader();
}
#region Logger
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endregion
#region Parent
/// <summary>
/// The network that contains this node
/// </summary>
public NetworkViewModel Parent
{
get => _parent;
internal set => this.RaiseAndSetIfChanged(ref _parent, value);
}
private NetworkViewModel _parent;
#endregion
#region Name
/// <summary>
/// The name of the node.
/// In the default view, this string is displayed at the top of the node.
/// </summary>
public string Name
{
get => _name;
set => this.RaiseAndSetIfChanged(ref _name, value);
}
private string _name;
#endregion
#region HeaderIcon
/// <summary>
/// The icon displayed in the header of the node.
/// If this is null, no icon is displayed.
/// In the default view, this icon is displayed at the top of the node.
/// </summary>
public IBitmap HeaderIcon
{
get => _headerIcon;
set => this.RaiseAndSetIfChanged(ref _headerIcon, value);
}
private IBitmap _headerIcon;
#endregion
#region Inputs
/// <summary>
/// The list of inputs on this node.
/// </summary>
public ISourceList<NodeInputViewModel> Inputs { get; } = new SourceList<NodeInputViewModel>();
#endregion
#region Outputs
/// <summary>
/// The list of outputs on this node.
/// </summary>
public ISourceList<NodeOutputViewModel> Outputs { get; } = new SourceList<NodeOutputViewModel>();
#endregion
#region VisibleInputs
/// <summary>
/// The list of inputs that is currently visible on this node.
/// Some inputs may be hidden if the node is collapsed.
/// </summary>
public IObservableList<NodeInputViewModel> VisibleInputs { get; }
#endregion
#region VisibleOutputs
/// <summary>
/// The list of outputs that is currently visible on this node.
/// Some outputs may be hidden if the node is collapsed.
/// </summary>
public IObservableList<NodeOutputViewModel> VisibleOutputs { get; }
#endregion
#region VisibleEndpointGroups
/// <summary>
/// The list of endpoint groups that is currently visible on this node.
/// Some groups may be hidden if the node is collapsed.
/// </summary>
public ReadOnlyObservableCollection<EndpointGroupViewModel> VisibleEndpointGroups { get; }
#endregion
#region EndpointGroupViewModelFactory
/// <summary>
/// The function that is used to create endpoint group view models.
/// By default, this function creates a EndpointGroupViewModel.
/// </summary>
public EndpointGroupViewModelFactory EndpointGroupViewModelFactory
{
get => _endpointGroupViewModelFactory;
set => this.RaiseAndSetIfChanged(ref _endpointGroupViewModelFactory, value);
}
private EndpointGroupViewModelFactory _endpointGroupViewModelFactory;
#endregion
#region IsSelected
/// <summary>
/// If true, this node is currently selected in the UI.
/// </summary>
public bool IsSelected
{
get => _isSelected;
set => this.RaiseAndSetIfChanged(ref _isSelected, value);
}
private bool _isSelected;
#endregion
#region IsCollapsed
/// <summary>
/// If true, this node is currently collapsed.
/// If the node is collapsed, some parts of the node are hidden to provide a more compact view.
/// </summary>
public bool IsCollapsed
{
get => _isCollapsed;
set => this.RaiseAndSetIfChanged(ref _isCollapsed, value);
}
private bool _isCollapsed;
#endregion
#region CanBeRemovedByUser
/// <summary>
/// If true, the user can delete this node from the network in the UI.
/// True by default.
/// </summary>
public bool CanBeRemovedByUser
{
get => _canBeRemovedByUser;
set => this.RaiseAndSetIfChanged(ref _canBeRemovedByUser, value);
}
private bool _canBeRemovedByUser;
#endregion
#region Position
/// <summary>
/// The position of this node in the network.
/// </summary>
public Point Position
{
get => _position;
set => this.RaiseAndSetIfChanged(ref _position, value);
}
private Point _position;
#endregion
#region Size
/// <summary>
/// The rendered size of this node.
/// </summary>
public Size Size
{
get => _size;
internal set => this.RaiseAndSetIfChanged(ref _size, value);
}
private Size _size;
#endregion
#region Resizable
/// <summary>
/// On which axes can the user resize the node?
/// </summary>
public ResizeOrientation Resizable
{
get => _resizable;
set => this.RaiseAndSetIfChanged(ref _resizable, value);
}
private ResizeOrientation _resizable;
#endregion
public NodeViewModel()
{
// Setup a default EndpointGroupViewModelFactory that will be used to create endpoint groups.
EndpointGroupViewModelFactory = (group, allInputs, allOutputs, children, factory) => new EndpointGroupViewModel(group, allInputs, allOutputs, children, factory);
this.Name = "Untitled";
this.CanBeRemovedByUser = true;
this.Resizable = ResizeOrientation.Horizontal;
// Setup parent relationship with inputs.
Inputs.Connect().ActOnEveryObject(
addedInput => addedInput.Parent = this,
removedInput => removedInput.Parent = null
);
// Setup parent relationship with outputs.
Outputs.Connect().ActOnEveryObject(
addedOutput => addedOutput.Parent = this,
removedOutput => removedOutput.Parent = null
);
// When an input is removed, delete any connection to/from that input
Inputs.Preview().OnItemRemoved(removedInput =>
{
if (Parent != null)
{
Parent.Connections.RemoveMany(removedInput.Connections.Items);
bool pendingConnectionInvalid = Parent.PendingConnection?.Input == removedInput;
if (pendingConnectionInvalid)
{
Parent.RemovePendingConnection();
}
}
}).Subscribe();
// Same for outputs.
Outputs.Preview().OnItemRemoved(removedOutput =>
{
if (Parent != null)
{
Parent.Connections.RemoveMany(removedOutput.Connections.Items);
bool pendingConnectionInvalid = Parent.PendingConnection?.Output == removedOutput;
if (pendingConnectionInvalid)
{
Parent.RemovePendingConnection();
}
}
}).Subscribe();
// If collapsed, hide inputs without connections, otherwise show all.
var onCollapseChange = this.WhenAnyValue(vm => vm.IsCollapsed).Publish();
onCollapseChange.Connect();
var visibilityFilteredInputs = Inputs.Connect()
.AutoRefreshOnObservable(_ => onCollapseChange)
.AutoRefresh(vm => vm.Visibility)
.AutoRefresh(vm => vm.Group)
.Filter(i =>
{
if (IsCollapsed)
{
return i.Visibility == EndpointVisibility.AlwaysVisible || (i.Visibility == EndpointVisibility.Auto && i.Connections.Items.Any());
}
return i.Visibility != EndpointVisibility.AlwaysHidden;
});
VisibleInputs = visibilityFilteredInputs
.Filter(i => i.Group == null)
.Sort(Comparer<NodeInputViewModel>.Create((i1, i2) => i1.SortIndex.CompareTo(i2.SortIndex)),
resort: Inputs.Connect().WhenValueChanged(i => i.SortIndex).Select(_ => Unit.Default))
.AsObservableList();
// Same for outputs.
var visibilityFilteredOutputs = Outputs.Connect()
.AutoRefreshOnObservable(_ => onCollapseChange)
.AutoRefresh(vm => vm.Visibility)
.AutoRefresh(vm => vm.Group)
.Filter(o =>
{
if (IsCollapsed)
{
return o.Visibility == EndpointVisibility.AlwaysVisible || (o.Visibility == EndpointVisibility.Auto && o.Connections.Items.Any());
}
return o.Visibility != EndpointVisibility.AlwaysHidden;
});
VisibleOutputs = visibilityFilteredOutputs
.Filter(o => o.Group == null)
.Sort(Comparer<NodeOutputViewModel>.Create((o1, o2) => o1.SortIndex.CompareTo(o2.SortIndex)),
resort: Outputs.Connect().WhenValueChanged(o => o.SortIndex).Select(_ => Unit.Default))
.AsObservableList();
// Get all the groups, also the empty ones.
var allInputGroups
= visibilityFilteredInputs
.TransformMany(GetAllGroupsInHierarchy)
.AddKey(g => g);
var allOutputGroups
= visibilityFilteredOutputs
.TransformMany(GetAllGroupsInHierarchy)
.AddKey(g => g);
IEnumerable<EndpointGroup> GetAllGroupsInHierarchy(Endpoint endpoint)
{
var group = endpoint.Group;
while (group != null)
{
yield return group;
group = group.Parent;
}
}
// Merge needs AddKey first, otherwise removal of endpoints leads to confusion.
var allGroups
= allInputGroups
.Merge(allOutputGroups)
.DistinctValues(g => g);
// Used as temporary root for TransformToTree.
var root = new EndpointGroup();
// To react on change of the EndpointGroupViewModelFactory.
var onEndpointGroupViewModelFactoryChange = this.WhenAnyValue(vm => vm.EndpointGroupViewModelFactory);
allGroups
.TransformToTree(group => group.Parent ?? root)
.AutoRefreshOnObservable(_ => onEndpointGroupViewModelFactoryChange)
.Transform(n => EndpointGroupViewModelFactory(n.Key,
visibilityFilteredInputs,
visibilityFilteredOutputs,
n.Children,
EndpointGroupViewModelFactory))
.Bind(out var groups)
.Subscribe();
VisibleEndpointGroups = groups;
}
}
}

View File

@@ -0,0 +1,133 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using NodeNetwork.Views;
using ReactiveUI;
namespace NodeNetwork.ViewModels
{
/// <summary>
/// Viewmodel for a connection that is currently being build by the user.
/// </summary>
public class PendingConnectionViewModel : ReactiveObject
{
static PendingConnectionViewModel()
{
NNViewRegistrar.AddRegistration(() => new PendingConnectionView(), typeof(IViewFor<PendingConnectionViewModel>));
}
#region Logger
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endregion
#region Parent
/// <summary>
/// The network viewmodel that this connection is being build in.
/// </summary>
public NetworkViewModel Parent { get; }
#endregion
#region Input
/// <summary>
/// The node input viewmodel, if any, that is on one side of the connection.
/// Can be null.
/// </summary>
public NodeInputViewModel Input
{
get => _input;
set => this.RaiseAndSetIfChanged(ref _input, value);
}
private NodeInputViewModel _input;
#endregion
#region InputIsLocked
/// <summary>
/// If true, Input will not be changed.
/// This is used to mark Input as the starting point of the pending connection.
/// </summary>
public bool InputIsLocked
{
get => _inputIsLocked;
set => this.RaiseAndSetIfChanged(ref _inputIsLocked, value);
}
private bool _inputIsLocked;
#endregion
#region Output
/// <summary>
/// The node output viewmodel, if any, that is on one side of the connection.
/// Can be null.
/// </summary>
public NodeOutputViewModel Output
{
get => _output;
set => this.RaiseAndSetIfChanged(ref _output, value);
}
private NodeOutputViewModel _output;
#endregion
#region OutputIsLocked
/// <summary>
/// If true, Output will not be changed.
/// This is used to mark Output as the starting point of the pending connection.
/// </summary>
public bool OutputIsLocked
{
get => _outputIsLocked;
set => this.RaiseAndSetIfChanged(ref _outputIsLocked, value);
}
private bool _outputIsLocked;
#endregion
#region LooseEndPoint
/// <summary>
/// The current coordinates of the point where the pending connection ends on the loose side.
/// This value is used when the Input or Output is null.
/// </summary>
public Point LooseEndPoint
{
get => _looseEndPoint;
set => this.RaiseAndSetIfChanged(ref _looseEndPoint, value);
}
private Point _looseEndPoint;
#endregion
#region BoundingBox
/// <summary>
/// The rectangle that contains the entire connection view.
/// </summary>
public Rect BoundingBox => _boundingBox.Value;
private readonly ObservableAsPropertyHelper<Rect> _boundingBox;
#endregion
#region Validation
/// <summary>
/// The validation of the current connection state.
/// If invalid, the connection will be displayed as such and an error message will be displayed.
/// The pending connection must be valid before it can be added to the network as a real connection.
/// </summary>
public ConnectionValidationResult Validation
{
get => _validation;
set => this.RaiseAndSetIfChanged(ref _validation, value);
}
private ConnectionValidationResult _validation;
#endregion
public PendingConnectionViewModel(NetworkViewModel parent)
{
Parent = parent;
this.WhenAnyValue(vm => vm.Input, vm => vm.Output, vm => vm.LooseEndPoint)
.Select(_ =>
{
Point p1 = Output?.Port.CenterPoint ?? LooseEndPoint;
Point p2 = Input?.Port.CenterPoint ?? LooseEndPoint;
return new Rect(p1, p2);
}).ToProperty(this, vm => vm.BoundingBox, out _boundingBox);
}
}
}

View File

@@ -0,0 +1,168 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using NodeNetwork.Views;
using ReactiveUI;
namespace NodeNetwork.ViewModels
{
/// <summary>
/// Viewmodel class for the UI part of an endpoint that is used to create connections.
/// </summary>
public class PortViewModel : ReactiveObject
{
static PortViewModel()
{
NNViewRegistrar.AddRegistration(() => new PortView(), typeof(IViewFor<PortViewModel>));
}
#region Logger
private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType);
#endregion
#region Parent
/// <summary>
/// The Endpoint that owns this port.
/// </summary>
public Endpoint Parent
{
get => _parent;
set => this.RaiseAndSetIfChanged(ref _parent, value);
}
private Endpoint _parent;
#endregion
#region CenterPoint
/// <summary>
/// The coordinates, relative to the network, of the center of this port.
/// Used to draw connections.
/// </summary>
public Point CenterPoint
{
get => _centerPoint;
set => this.RaiseAndSetIfChanged(ref _centerPoint, value);
}
private Point _centerPoint;
#endregion
#region IsMirrored
/// <summary>
/// If true, the view for this viewmodel will be horizontally mirrored.
/// </summary>
public bool IsMirrored
{
get => _isMirrored;
set => this.RaiseAndSetIfChanged(ref _isMirrored, value);
}
private bool _isMirrored;
#endregion
#region IsVisible
/// <summary>
/// If true, this port is visible. If false, this port is hidden.
/// True by default.
/// </summary>
public bool IsVisible
{
get => _isVisible;
set => this.RaiseAndSetIfChanged(ref _isVisible, value);
}
private bool _isVisible;
#endregion
#region IsHighlighted
/// <summary>
/// If true, this port is highlighted.
/// This could be, for example, because the mouse is hovering over the port.
/// </summary>
public bool IsHighlighted
{
get => _isHighlighted;
set => this.RaiseAndSetIfChanged(ref _isHighlighted, value);
}
private bool _isHighlighted;
#endregion
#region IsInErrorMode
/// <summary>
/// If true, the port will visually indicate there is an error with this port.
/// In the default view this is used to indicate a pending connection validation error.
/// </summary>
public bool IsInErrorMode
{
get => _isInErrorMode;
set => this.RaiseAndSetIfChanged(ref _isInErrorMode, value);
}
private bool _isInErrorMode;
#endregion
#region ConnectionDragStarted
/// <summary>
/// Observable that fires when the user starts a new pending connection from this port.
/// </summary>
public IObservable<Unit> ConnectionDragStarted => _connectionDragStarted;
private readonly Subject<Unit> _connectionDragStarted = new Subject<Unit>();
#endregion
#region ConnectionPreview
/// <summary>
/// Fires when a pending connection is dragged over this port.
/// </summary>
public IObservable<bool> ConnectionPreviewActive => _connectionPreviewActive;
private readonly Subject<bool> _connectionPreviewActive = new Subject<bool>();
#endregion
#region ConnectionDragFinished
/// <summary>
/// Fires when the user drops the pending connection on this port.
/// </summary>
public IObservable<Unit> ConnectionDragFinished => _connectionDragFinished;
private readonly Subject<Unit> _connectionDragFinished = new Subject<Unit>();
#endregion
public PortViewModel()
{
IsVisible = true;
}
public virtual void OnDragFromPort()
{
_connectionDragStarted.OnNext(Unit.Default);
}
public void OnPortEnter()
{
IsHighlighted = true;
PendingConnectionViewModel pendingConnection = Parent.Parent?.Parent?.PendingConnection;
if (pendingConnection != null && pendingConnection.Input != Parent && pendingConnection.Output != Parent)
{
_connectionPreviewActive.OnNext(true);
}
}
public void OnPortLeave()
{
IsHighlighted = false;
PendingConnectionViewModel pendingConnection = Parent.Parent?.Parent?.PendingConnection;
if (pendingConnection != null)
{
_connectionPreviewActive.OnNext(false);
}
}
public virtual void OnDropOnPort()
{
if (Parent?.Parent?.Parent?.PendingConnection != null)
{
_connectionDragFinished.OnNext(Unit.Default);
}
}
}
}

View File

@@ -0,0 +1,79 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using DynamicData;
using ReactiveUI;
namespace NodeNetwork.ViewModels
{
/// <summary>
/// Viewmodel for the view that is used to select nodes by dragging a rectangle around them.
/// </summary>
public class SelectionRectangleViewModel : ReactiveObject
{
#region StartPoint
/// <summary>
/// The coordinates of the first corner of the rectangle (where the user clicked down).
/// </summary>
public Point StartPoint
{
get => _startPoint;
set => this.RaiseAndSetIfChanged(ref _startPoint, value);
}
private Point _startPoint;
#endregion
#region EndPoint
/// <summary>
/// The coordinates of the second corner of the rectangle.
/// </summary>
public Point EndPoint
{
get => _endPoint;
set => this.RaiseAndSetIfChanged(ref _endPoint, value);
}
private Point _endPoint;
#endregion
#region Rectangle
/// <summary>
/// The Rect object formed by StartPoint and EndPoint.
/// </summary>
public Rect Rectangle => _rectangle.Value;
private readonly ObservableAsPropertyHelper<Rect> _rectangle;
#endregion
#region IsVisible
/// <summary>
/// If true, the selection rectangle view is visible.
/// </summary>
public bool IsVisible
{
get => _isVisible;
set => this.RaiseAndSetIfChanged(ref _isVisible, value);
}
private bool _isVisible;
#endregion
#region IntersectingNodes
/// <summary>
/// List of nodes visually intersecting or contained in the rectangle.
/// This list is driven by the view.
/// </summary>
public ISourceList<NodeViewModel> IntersectingNodes { get; } = new SourceList<NodeViewModel>();
#endregion
public SelectionRectangleViewModel()
{
this.WhenAnyValue(vm => vm.StartPoint, vm => vm.EndPoint)
.Select(_ => new Rect(StartPoint, EndPoint))
.ToProperty(this, vm => vm.Rectangle, out _rectangle);
IntersectingNodes.Connect().ActOnEveryObject(node => node.IsSelected = true, node => node.IsSelected = false);
}
}
}