port from perforce
This commit is contained in:
100
intromat/NodeNetwork/ViewModels/ConnectionViewModel.cs
Normal file
100
intromat/NodeNetwork/ViewModels/ConnectionViewModel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
61
intromat/NodeNetwork/ViewModels/CutLineViewModel.cs
Normal file
61
intromat/NodeNetwork/ViewModels/CutLineViewModel.cs
Normal 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
|
||||
}
|
||||
}
|
||||
255
intromat/NodeNetwork/ViewModels/Endpoint.cs
Normal file
255
intromat/NodeNetwork/ViewModels/Endpoint.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
33
intromat/NodeNetwork/ViewModels/EndpointGroup.cs
Normal file
33
intromat/NodeNetwork/ViewModels/EndpointGroup.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
72
intromat/NodeNetwork/ViewModels/EndpointGroupViewModel.cs
Normal file
72
intromat/NodeNetwork/ViewModels/EndpointGroupViewModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
35
intromat/NodeNetwork/ViewModels/ErrorMessageViewModel.cs
Normal file
35
intromat/NodeNetwork/ViewModels/ErrorMessageViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
430
intromat/NodeNetwork/ViewModels/NetworkViewModel.cs
Normal file
430
intromat/NodeNetwork/ViewModels/NetworkViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
203
intromat/NodeNetwork/ViewModels/NodeInputViewModel.cs
Normal file
203
intromat/NodeNetwork/ViewModels/NodeInputViewModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
124
intromat/NodeNetwork/ViewModels/NodeOutputViewModel.cs
Normal file
124
intromat/NodeNetwork/ViewModels/NodeOutputViewModel.cs
Normal 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();
|
||||
}
|
||||
}
|
||||
}
|
||||
352
intromat/NodeNetwork/ViewModels/NodeViewModel.cs
Normal file
352
intromat/NodeNetwork/ViewModels/NodeViewModel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
133
intromat/NodeNetwork/ViewModels/PendingConnectionViewModel.cs
Normal file
133
intromat/NodeNetwork/ViewModels/PendingConnectionViewModel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
168
intromat/NodeNetwork/ViewModels/PortViewModel.cs
Normal file
168
intromat/NodeNetwork/ViewModels/PortViewModel.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user