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 { /// /// The viewmodel for node networks. /// public class NetworkViewModel : ReactiveObject { static NetworkViewModel() { NNViewRegistrar.AddRegistration(() => new NetworkView(), typeof(IViewFor)); } #region Logger private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); #endregion #region Nodes /// /// The list of nodes in this network. /// public ISourceList Nodes { get; } = new SourceList(); #endregion #region SelectedNodes /// /// 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. /// public IObservableList SelectedNodes { get; } #endregion #region Connections /// /// The list of connections in this network. /// public ISourceList Connections { get; } = new SourceList(); #endregion #region PendingConnection /// /// 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. /// public PendingConnectionViewModel PendingConnection { get => _pendingConnection; set => this.RaiseAndSetIfChanged(ref _pendingConnection, value); } private PendingConnectionViewModel _pendingConnection; public Action OnPendingConnectionDropped { get; set; } #endregion #region PendingNode /// /// 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. /// public NodeViewModel PendingNode { get => _pendingNode; set => this.RaiseAndSetIfChanged(ref _pendingNode, value); } private NodeViewModel _pendingNode; #endregion #region ConnectionFactory /// /// 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. /// public Func ConnectionFactory { get => _connectionFactory; set => this.RaiseAndSetIfChanged(ref _connectionFactory, value); } private Func _connectionFactory; #endregion #region Validator /// /// Function that is used to check if the network is valid or not. /// To run the validation, use the UpdateValidation command. /// public Func Validator { get => _validator; set => this.RaiseAndSetIfChanged(ref _validator, value); } private Func _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. /// /// The validation of the current state of the network. /// This property is automatically updated when UpdateValidation runs. /// public NetworkValidationResult LatestValidation { get => _latestValidation; private set => this.RaiseAndSetIfChanged(ref _latestValidation, value); } private NetworkValidationResult _latestValidation; #endregion #region Validation /// /// Observable that produces the latest NetworkValidationResult every time the network is validated. /// public IObservable Validation { get; } #endregion #region IsReadOnly /// /// If true, the network and its contents (nodes, connections, input/output editors, ...) cannot be modified by the user. /// public bool IsReadOnly { get => _isReadOnly; set => this.RaiseAndSetIfChanged(ref _isReadOnly, value); } private bool _isReadOnly; #endregion #region CutLine /// /// The viewmodel of the cutline used in this network view. /// public CutLineViewModel CutLine { get; } = new CutLineViewModel(); #endregion #region ZoomFactor /// /// Scale of the view. Larger means more zoomed in. Default value is 1. /// public double ZoomFactor { get => _zoomFactor; set => this.RaiseAndSetIfChanged(ref _zoomFactor, value); } private double _zoomFactor = 1; /// /// The maximum zoom level used in this network view. Default value is 2.5. /// public double MaxZoomLevel { get => _maxZoomLevel; set => this.RaiseAndSetIfChanged(ref _maxZoomLevel, value); } private double _maxZoomLevel = 2.5; /// /// The minimum zoom level used in this network view. Default value is 0.15. /// public double MinZoomLevel { get => _minZoomLevel; set => this.RaiseAndSetIfChanged(ref _minZoomLevel, value); } private double _minZoomLevel = 0.15; /// /// The drag offset of the initial view position used in this network view. Default value is (0, 0). /// public Point DragOffset { get => _dragOffset; set => this.RaiseAndSetIfChanged(ref _dragOffset, value); } private Point _dragOffset = new Point(0, 0); #endregion #region SelectionRectangle /// /// The viewmodel for the selection rectangle used in this network view. /// public SelectionRectangleViewModel SelectionRectangle { get; } = new SelectionRectangleViewModel(); #endregion #region NetworkChanged /// /// This observable pushes a notification when a connection was added to/removed from the network, /// and the relevant endpoints have been updated. /// /// /// 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. /// public IObservable ConnectionsUpdated { get; } /// /// 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. /// public IObservable NetworkChanged { get; } #endregion #region Commands /// /// Deletes the nodes in SelectedNodes that are user-removable. /// public ReactiveCommand DeleteSelectedNodes { get; } /// /// Runs the Validator function and stores the result in LatestValidation. /// public ReactiveCommand 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 nodeSet = new HashSet(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 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 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) ); } /// /// Clears SelectedNodes, setting the IsSelected property of all the nodes to false. /// public void ClearSelection() { foreach (NodeViewModel node in SelectedNodes.Items) { node.IsSelected = false; } } /// /// Starts a cut in the CutLine viewmodel. /// public void StartCut() { CutLine.IsVisible = true; } /// /// Stops the current cut in the CutLine viewmodel and applies the changes. /// public virtual void FinishCut() { Connections.RemoveMany(CutLine.IntersectingConnections.Items); CutLine.IsVisible = false; CutLine.IntersectingConnections.Clear(); } /// /// Sets PendingConnection to null. /// public void RemovePendingConnection() { PendingConnection = null; } /// /// Starts a selection in RectangleSelection /// public void StartRectangleSelection() { ClearSelection(); SelectionRectangle.IsVisible = true; SelectionRectangle.IntersectingNodes.Clear(); } /// /// Stops the current selection in RectangleSelection and applies the changes. /// public void FinishRectangleSelection() { SelectionRectangle.IsVisible = false; } } }