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 } /// /// Viewmodel class for the nodes in the network /// public class NodeViewModel : ReactiveObject { static NodeViewModel() { NNViewRegistrar.AddRegistration(() => new NodeView(), typeof(IViewFor)); Locator.CurrentMutable.RegisterPlatformBitmapLoader(); } #region Logger private static readonly log4net.ILog log = log4net.LogManager.GetLogger(System.Reflection.MethodBase.GetCurrentMethod().DeclaringType); #endregion #region Parent /// /// The network that contains this node /// public NetworkViewModel Parent { get => _parent; internal set => this.RaiseAndSetIfChanged(ref _parent, value); } private NetworkViewModel _parent; #endregion #region Name /// /// The name of the node. /// In the default view, this string is displayed at the top of the node. /// public string Name { get => _name; set => this.RaiseAndSetIfChanged(ref _name, value); } private string _name; #endregion #region HeaderIcon /// /// 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. /// public IBitmap HeaderIcon { get => _headerIcon; set => this.RaiseAndSetIfChanged(ref _headerIcon, value); } private IBitmap _headerIcon; #endregion #region Inputs /// /// The list of inputs on this node. /// public ISourceList Inputs { get; } = new SourceList(); #endregion #region Outputs /// /// The list of outputs on this node. /// public ISourceList Outputs { get; } = new SourceList(); #endregion #region VisibleInputs /// /// The list of inputs that is currently visible on this node. /// Some inputs may be hidden if the node is collapsed. /// public IObservableList VisibleInputs { get; } #endregion #region VisibleOutputs /// /// The list of outputs that is currently visible on this node. /// Some outputs may be hidden if the node is collapsed. /// public IObservableList VisibleOutputs { get; } #endregion #region VisibleEndpointGroups /// /// The list of endpoint groups that is currently visible on this node. /// Some groups may be hidden if the node is collapsed. /// public ReadOnlyObservableCollection VisibleEndpointGroups { get; } #endregion #region EndpointGroupViewModelFactory /// /// The function that is used to create endpoint group view models. /// By default, this function creates a EndpointGroupViewModel. /// public EndpointGroupViewModelFactory EndpointGroupViewModelFactory { get => _endpointGroupViewModelFactory; set => this.RaiseAndSetIfChanged(ref _endpointGroupViewModelFactory, value); } private EndpointGroupViewModelFactory _endpointGroupViewModelFactory; #endregion #region IsSelected /// /// If true, this node is currently selected in the UI. /// public bool IsSelected { get => _isSelected; set => this.RaiseAndSetIfChanged(ref _isSelected, value); } private bool _isSelected; #endregion #region IsCollapsed /// /// 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. /// public bool IsCollapsed { get => _isCollapsed; set => this.RaiseAndSetIfChanged(ref _isCollapsed, value); } private bool _isCollapsed; #endregion #region CanBeRemovedByUser /// /// If true, the user can delete this node from the network in the UI. /// True by default. /// public bool CanBeRemovedByUser { get => _canBeRemovedByUser; set => this.RaiseAndSetIfChanged(ref _canBeRemovedByUser, value); } private bool _canBeRemovedByUser; #endregion #region Position /// /// The position of this node in the network. /// public Point Position { get => _position; set => this.RaiseAndSetIfChanged(ref _position, value); } private Point _position; #endregion #region Size /// /// The rendered size of this node. /// public Size Size { get => _size; internal set => this.RaiseAndSetIfChanged(ref _size, value); } private Size _size; #endregion #region Resizable /// /// On which axes can the user resize the node? /// 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.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.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 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; } } }