port from perforce

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

View File

@@ -0,0 +1,9 @@
[*.cs]
dotnet_diagnostic.IDE0019.severity = none
dotnet_diagnostic.IDE0038.severity = none
dotnet_diagnostic.IDE0044.severity = none
dotnet_diagnostic.IDE0052.severity = none
dotnet_diagnostic.IDE0058.severity = none
dotnet_diagnostic.IDE0062.severity = none
dotnet_diagnostic.IDE0090.severity = none

View File

@@ -0,0 +1,8 @@
// This file is used by Code Analysis to maintain SuppressMessage
// attributes that are applied to this project.
// Project-level suppressions either have no target or are given
// a specific target and scoped to a namespace, type, member, etc.
using System.Diagnostics.CodeAnalysis;
[assembly: SuppressMessage("Interoperability", "CA1416:Validate platform compatibility", Justification = "<Pending>")]

View File

@@ -0,0 +1,68 @@
using System;
using System.Collections.Generic;
using NodeNetwork.ViewModels;
using NodeNetwork.Views;
using ReactiveUI;
using Splat;
namespace NodeNetwork
{
/// <summary>
/// A locator is used to find the correct view corresponding to a viewmodel.
/// In ReactiveUI, usually Splat is used, but others exist. This class acts as an intermediate registrar.
/// It gathers registrations and registers them to the preferred locator.
/// </summary>
public sealed class NNViewRegistrar
{
private static readonly List<Tuple<Func<object>, Type>> PendingRegistrations = new List<Tuple<Func<object>, Type>>();
private static Action<Func<object>, Type> _registerAction;
public static void AddRegistration(Func<object> factory, Type serviceType)
{
if (factory == null)
{
throw new ArgumentNullException(nameof(factory));
}
else if (serviceType == null)
{
throw new ArgumentNullException(nameof(serviceType));
}
if (_registerAction == null)
{
PendingRegistrations.Add(Tuple.Create(factory, serviceType));
}
else
{
_registerAction(factory, serviceType);
}
}
public static void RegisterToLocator(Action<Func<object>, Type> newRegisterAction)
{
if (newRegisterAction == null)
{
throw new ArgumentNullException(nameof(newRegisterAction));
}
else if (_registerAction != null)
{
throw new InvalidOperationException("A locator has already been set");
}
_registerAction = newRegisterAction;
foreach (var t in PendingRegistrations)
{
_registerAction(t.Item1, t.Item2);
}
PendingRegistrations.Clear();
}
/// <summary>
/// Register all NodeNetwork view/viewmodel pairs to Locator.CurrentMutable.
/// </summary>
public static void RegisterSplat()
{
RegisterToLocator((f, t) => Locator.CurrentMutable.Register(f, t));
}
}
}

View File

@@ -0,0 +1,39 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net7.0-windows7.0;net48</TargetFrameworks>
<OutputType>library</OutputType>
<GenerateAssemblyInfo>false</GenerateAssemblyInfo>
<UseWPF>true</UseWPF>
<GeneratePackageOnBuild>true</GeneratePackageOnBuild>
<Version>6.0.0</Version>
<Authors>Wouter De Keersmaecker</Authors>
<PackageTags>wpf reactiveui node network editor node-editor graph</PackageTags>
<PackageLicenseExpression>Apache-2.0</PackageLicenseExpression>
<RepositoryUrl>https://www.github.com/wouterdek/nodenetwork</RepositoryUrl>
<RepositoryType>Github</RepositoryType>
<IncludeSymbols>true</IncludeSymbols>
</PropertyGroup>
<PropertyGroup Condition=" '$(Configuration)|$(Platform)' == 'Release|AnyCPU' ">
<DocumentationFile>bin\Release\NodeNetwork.xml</DocumentationFile>
</PropertyGroup>
<ItemGroup>
<None Include=".editorconfig" Link=".editorconfig" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CSharp" Version="4.7.0" />
<PackageReference Include="log4net" Version="2.0.12" />
<PackageReference Include="ReactiveUI" Version="13.2.18" />
<PackageReference Include="ReactiveUI.Events.WPF" Version="13.2.18" />
<PackageReference Include="ReactiveUI.WPF" Version="13.2.18" />
<PackageReference Include="Splat.Drawing" Version="11.0.1" />
<PackageReference Include="System.Buffers" Version="4.5.1" />
<PackageReference Include="System.Collections.Immutable" Version="5.0.0" />
<PackageReference Include="System.Drawing.Primitives" Version="4.3.0" />
<PackageReference Include="System.Memory" Version="4.5.4" />
<PackageReference Include="System.Numerics.Vectors" Version="4.5.0" />
<PackageReference Include="System.Runtime.CompilerServices.Unsafe" Version="5.0.0" />
<PackageReference Include="System.Threading.Tasks.Extensions" Version="4.5.4" />
<PackageReference Include="System.ValueTuple" Version="4.5.0" />
<PackageReference Include="System.Data.DataSetExtensions" Version="4.5.0" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,55 @@
using System.Reflection;
using System.Resources;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;
using System.Windows;
// General Information about an assembly is controlled through the following
// set of attributes. Change these attribute values to modify the information
// associated with an assembly.
[assembly: AssemblyTitle("NodeNetwork")]
[assembly: AssemblyDescription("A library with a WPF node editor component based on ReactiveUI")]
[assembly: AssemblyConfiguration("")]
[assembly: AssemblyCompany("")]
[assembly: AssemblyProduct("NodeNetwork")]
[assembly: AssemblyCopyright("Copyright (c) Wouter De Keersmaecker")]
[assembly: AssemblyTrademark("")]
[assembly: AssemblyCulture("")]
// Setting ComVisible to false makes the types in this assembly not visible
// to COM components. If you need to access a type in this assembly from
// COM, set the ComVisible attribute to true on that type.
[assembly: ComVisible(false)]
//In order to begin building localizable applications, set
//<UICulture>CultureYouAreCodingWith</UICulture> in your .csproj file
//inside a <PropertyGroup>. For example, if you are using US english
//in your source files, set the <UICulture> to en-US. Then uncomment
//the NeutralResourceLanguage attribute below. Update the "en-US" in
//the line below to match the UICulture setting in the project file.
//[assembly: NeutralResourcesLanguage("en-US", UltimateResourceFallbackLocation.Satellite)]
[assembly:ThemeInfo(
ResourceDictionaryLocation.None, //where theme specific resource dictionaries are located
//(used if a resource is not found in the page,
// or application resource dictionaries)
ResourceDictionaryLocation.SourceAssembly //where the generic resource dictionary is located
//(used if a resource is not found in the page,
// app, or any theme specific resource dictionaries)
)]
// Version information for an assembly consists of the following four values:
//
// Major Version
// Minor Version
// Build Number
// Revision
//
// You can specify all the values or you can default the Build and Revision Numbers
// by using the '*' as shown below:
// [assembly: AssemblyVersion("1.0.*")]
[assembly: AssemblyVersion("6.0.0.0")]
[assembly: AssemblyFileVersion("6.0.0.0")]

View File

@@ -0,0 +1,74 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:NodeNetwork.Views"
xmlns:controls="clr-namespace:NodeNetwork.Views.Controls">
<Style TargetType="{x:Type views:ConnectionView}">
<Setter Property="RegularBrush" Value="White"/>
<Setter Property="ErrorBrush" Value="DarkRed"/>
<Setter Property="HighlightBrush" Value="Yellow"/>
<Setter Property="MarkedForDeleteBrush" Value="Red"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="views:ConnectionView">
<controls:FillPanel>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="{x:Static views:ConnectionView.HighlightVisualStatesGroup}">
<VisualState Name="{x:Static views:ConnectionView.HighlightedState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="HighlightPath" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState Name="{x:Static views:ConnectionView.NonHighlightedState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="HighlightPath" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup Name="{x:Static views:ConnectionView.ErrorVisualStatesGroup}">
<VisualState Name="{x:Static views:ConnectionView.ErrorState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ErrorPath" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState Name="{x:Static views:ConnectionView.NonErrorState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ErrorPath" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup Name="{x:Static views:ConnectionView.MarkedForDeleteVisualStatesGroup}">
<VisualState Name="{x:Static views:ConnectionView.MarkedForDeleteState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="DeleteMarkPath" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState Name="{x:Static views:ConnectionView.NotMarkedForDeleteState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="DeleteMarkPath" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Path x:Name="RegularPath" StrokeThickness="2" IsHitTestVisible="False" Stroke="{TemplateBinding RegularBrush}" Data="{TemplateBinding Geometry}"/>
<Path x:Name="ErrorPath" StrokeThickness="2" IsHitTestVisible="False" Stroke="{TemplateBinding ErrorBrush}" Data="{TemplateBinding Geometry}"/>
<Path x:Name="DeleteMarkPath" StrokeThickness="2" IsHitTestVisible="False" Stroke="{TemplateBinding MarkedForDeleteBrush}" Data="{TemplateBinding Geometry}"/>
<Path x:Name="HighlightPath" StrokeThickness="2" IsHitTestVisible="False" Stroke="{TemplateBinding HighlightBrush}" Data="{TemplateBinding Geometry}"/>
</controls:FillPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,53 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:wpf="clr-namespace:NodeNetwork.Utilities.WPF">
<wpf:NullVisibilityConverter x:Key="NullConverter"/>
<ControlTemplate x:Key="LeftAlignedEndpoint">
<Grid Margin="-10, 0, 0, 5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="30"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="5"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Column="1" Grid.Row="0" VerticalAlignment="Center">
<Image x:Name="Icon" MaxWidth="20" MaxHeight="20" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="0,0,5,0" Visibility="{Binding Icon, Mode=OneWay, Converter={StaticResource NullConverter}}"/>
<TextBlock x:Name="NameLabel" TextTrimming="CharacterEllipsis" FontSize="14" Width="auto" Foreground="White" VerticalAlignment="Center"/>
</StackPanel>
<reactiveUi:ViewModelViewHost x:Name="EndpointHost" Grid.Column="0" Grid.Row="0" Margin="0,4.9,10,4.9" Width="20" Height="20"
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" IsTabStop="False"/>
<reactiveUi:ViewModelViewHost x:Name="EditorHost" Grid.Column="1" VerticalContentAlignment="Center" IsTabStop="False"/>
</Grid>
</ControlTemplate>
<ControlTemplate x:Key="RightAlignedEndpoint">
<Grid Margin="0, 0, -10, 5">
<Grid.ColumnDefinitions>
<ColumnDefinition Width="5"/>
<ColumnDefinition Width="*"/>
<ColumnDefinition Width="Auto"/>
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto"/>
<RowDefinition Height="*"/>
</Grid.RowDefinitions>
<StackPanel Orientation="Horizontal" Grid.Column="1" Grid.Row="0" HorizontalAlignment="Right">
<TextBlock x:Name="NameLabel" TextTrimming="CharacterEllipsis" FontSize="14" Width="auto" Foreground="White" VerticalAlignment="Center"/>
<Image x:Name="Icon" MaxWidth="20" MaxHeight="20" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="5,0,0,0" Visibility="{Binding Icon, Mode=OneWay, Converter={StaticResource NullConverter}}"/>
</StackPanel>
<reactiveUi:ViewModelViewHost x:Name="EndpointHost" Grid.Column="2" Grid.Row="0" Margin="10,4.9,0,4.9" Width="20" Height="20"
HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" IsTabStop="False"/>
<reactiveUi:ViewModelViewHost x:Name="EditorHost" Grid.Column="1" VerticalContentAlignment="Center" HorizontalContentAlignment="Right" HorizontalAlignment="Right" IsTabStop="False"/>
</Grid>
</ControlTemplate>
</ResourceDictionary>

View File

@@ -0,0 +1,102 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:views="clr-namespace:NodeNetwork.Views"
xmlns:viewModels="clr-namespace:NodeNetwork.ViewModels">
<Style TargetType="{x:Type views:EndpointGroupView}">
<Setter Property="Background" Value="#20000000"/>
<Setter Property="Foreground" Value="#FFFFFF"/>
<Setter Property="TitleFontFamily" Value="Segoe UI SemiLight"/>
<Setter Property="TitleFontSize" Value="18"/>
<Setter Property="IsTabStop" Value="False" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="views:EndpointGroupView">
<Grid Margin="0,5,0,0">
<Canvas Background="{TemplateBinding Background}" IsHitTestVisible="False"/>
<StackPanel Orientation="Vertical">
<DockPanel Margin="0,5,0,0">
<TextBlock x:Name="NameLabel" TextTrimming="CharacterEllipsis" FontSize="{TemplateBinding TitleFontSize}" FontFamily="{TemplateBinding TitleFontFamily}"
Foreground="{TemplateBinding Foreground}" VerticalAlignment="Center" Margin="20,0,10,0">
<TextBlock.Style>
<Style TargetType="TextBlock">
<Style.Triggers>
<Trigger Property="Text" Value="">
<Setter Property="Visibility" Value="Collapsed" />
</Trigger>
<Trigger Property="Text" Value="{x:Null}">
<Setter Property="Visibility" Value="Collapsed" />
</Trigger>
</Style.Triggers>
</Style>
</TextBlock.Style>
</TextBlock>
</DockPanel>
<DockPanel>
<ItemsControl x:Name="InputsList" IsTabStop="False">
<ItemsControl.Style>
<Style TargetType="ItemsControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=EndpointsStackingOrientation, RelativeSource={RelativeSource AncestorType={x:Type views:NodeView}}}" Value="Vertical">
<Setter Property="DockPanel.Dock" Value="Top" />
</DataTrigger>
<DataTrigger Binding="{Binding Path=EndpointsStackingOrientation, RelativeSource={RelativeSource AncestorType={x:Type views:NodeView}}}" Value="Horizontal">
<Setter Property="DockPanel.Dock" Value="Left" />
</DataTrigger>
</Style.Triggers>
</Style>
</ItemsControl.Style>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="viewModels:NodeInputViewModel">
<reactiveUi:ViewModelViewHost ViewModel="{Binding}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
IsTabStop="False" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl x:Name="OutputsList" IsTabStop="False">
<ItemsControl.Style>
<Style TargetType="ItemsControl">
<Style.Triggers>
<DataTrigger
Binding="{Binding Path=EndpointsStackingOrientation, RelativeSource={RelativeSource AncestorType={x:Type views:NodeView}}}"
Value="Vertical">
<Setter Property="DockPanel.Dock" Value="Bottom" />
</DataTrigger>
<DataTrigger
Binding="{Binding Path=EndpointsStackingOrientation, RelativeSource={RelativeSource AncestorType={x:Type views:NodeView}}}"
Value="Horizontal">
<Setter Property="DockPanel.Dock" Value="Right" />
</DataTrigger>
</Style.Triggers>
</Style>
</ItemsControl.Style>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="viewModels:NodeOutputViewModel">
<reactiveUi:ViewModelViewHost ViewModel="{Binding}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
IsTabStop="False" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
<ItemsControl x:Name="EndpointGroupsList" IsTabStop="False">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="viewModels:EndpointGroupViewModel">
<reactiveUi:ViewModelViewHost ViewModel="{Binding}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
IsTabStop="False" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</StackPanel>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,26 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:NodeNetwork.Views">
<Style TargetType="{x:Type views:ErrorMessageView}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="views:ErrorMessageView">
<Border Background="#EEE" CornerRadius="3">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"/>
<ColumnDefinition Width="*"/>
</Grid.ColumnDefinitions>
<Grid Grid.Column="0" HorizontalAlignment="Left" VerticalAlignment="Center" Width="36" Height="36" Margin="5">
<Ellipse Fill="#ef513a"/>
<Line Stroke="White" StrokeThickness="3" X1="12" Y1="12" X2="24" Y2="24"/>
<Line Stroke="White" StrokeThickness="3" X1="24" Y1="12" X2="12" Y2="24"/>
</Grid>
<TextBlock Grid.Column="1" x:Name="TextBlock" Margin="5" TextWrapping="Wrap" FontSize="18" FontWeight="Normal" Foreground="#ef513a" VerticalAlignment="Center"/>
</Grid>
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,13 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="pack://application:,,,/NodeNetwork;component/Themes/ConnectionView.xaml"/>
<ResourceDictionary Source="pack://application:,,,/NodeNetwork;component/Themes/PendingConnectionView.xaml"/>
<ResourceDictionary Source="pack://application:,,,/NodeNetwork;component/Themes/ErrorMessageView.xaml"/>
<ResourceDictionary Source="pack://application:,,,/NodeNetwork;component/Themes/NodeEndpointEditorView.xaml"/>
<ResourceDictionary Source="pack://application:,,,/NodeNetwork;component/Themes/NodeView.xaml"/>
<ResourceDictionary Source="pack://application:,,,/NodeNetwork;component/Themes/NodeInputView.xaml"/>
<ResourceDictionary Source="pack://application:,,,/NodeNetwork;component/Themes/NodeOutputView.xaml"/>
<ResourceDictionary Source="pack://application:,,,/NodeNetwork;component/Themes/PortView.xaml"/>
<ResourceDictionary Source="pack://application:,,,/NodeNetwork;component/Themes/EndpointGroupView.xaml"/>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>

View File

@@ -0,0 +1,13 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:NodeNetwork.Views">
<Style TargetType="{x:Type views:NodeEndpointEditorView}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="views:NodeEndpointEditorView">
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,24 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:NodeNetwork.Views"
xmlns:viewModels="clr-namespace:NodeNetwork.ViewModels">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Endpoint.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="{x:Type views:NodeInputView}">
<Setter Property="IsTabStop" Value="False"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=PortPosition}" Value="{x:Static viewModels:PortPosition.Left}">
<DataTrigger.Setters>
<Setter Property="Template" Value="{StaticResource LeftAlignedEndpoint}"/>
</DataTrigger.Setters>
</DataTrigger>
<DataTrigger Binding="{Binding Path=PortPosition}" Value="{x:Static viewModels:PortPosition.Right}">
<DataTrigger.Setters>
<Setter Property="Template" Value="{StaticResource RightAlignedEndpoint}"/>
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,24 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:NodeNetwork.Views"
xmlns:viewModels="clr-namespace:NodeNetwork.ViewModels">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Endpoint.xaml" />
</ResourceDictionary.MergedDictionaries>
<Style TargetType="{x:Type views:NodeOutputView}">
<Setter Property="IsTabStop" Value="False"/>
<Style.Triggers>
<DataTrigger Binding="{Binding Path=PortPosition}" Value="{x:Static viewModels:PortPosition.Left}">
<DataTrigger.Setters>
<Setter Property="Template" Value="{StaticResource LeftAlignedEndpoint}"/>
</DataTrigger.Setters>
</DataTrigger>
<DataTrigger Binding="{Binding Path=PortPosition}" Value="{x:Static viewModels:PortPosition.Right}">
<DataTrigger.Setters>
<Setter Property="Template" Value="{StaticResource RightAlignedEndpoint}"/>
</DataTrigger.Setters>
</DataTrigger>
</Style.Triggers>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,175 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:NodeNetwork.Views"
xmlns:controls="clr-namespace:NodeNetwork.Views.Controls"
xmlns:viewModels="clr-namespace:NodeNetwork.ViewModels"
xmlns:reactiveUi="http://reactiveui.net">
<Style TargetType="{x:Type views:NodeView}">
<Setter Property="Background" Value="#5D9CEC"/>
<Setter Property="Foreground" Value="#FFFFFF"/>
<Setter Property="TitleFontFamily" Value="Segoe UI Semibold"/>
<Setter Property="TitleFontSize" Value="18"/>
<Setter Property="BorderBrush" Value="Orange"/>
<Setter Property="CornerRadius" Value="4"/>
<Setter Property="ArrowSize" Value="20"/>
<Setter Property="EndpointsStackingOrientation" Value="Vertical"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="views:NodeView">
<controls:FillPanel x:Name="Container">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="{x:Static views:NodeView.SelectedVisualStatesGroup}">
<VisualState Name="{x:Static views:NodeView.SelectedState}">
<Storyboard>
<ThicknessAnimation Duration="0" To="-3"
Storyboard.TargetName="Container" Storyboard.TargetProperty="Margin"/>
<ThicknessAnimation Duration="0" To="3" Storyboard.TargetName="Border" Storyboard.TargetProperty="BorderThickness"/>
</Storyboard>
</VisualState>
<VisualState Name="{x:Static views:NodeView.UnselectedState}">
<Storyboard>
<ThicknessAnimation Duration="0" To="0" Storyboard.TargetName="Container" Storyboard.TargetProperty="Margin"/>
<ThicknessAnimation Duration="0" To="0" Storyboard.TargetName="Border" Storyboard.TargetProperty="BorderThickness"/>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Border x:Name="Border" Margin="10,0,10,0" CornerRadius="{TemplateBinding CornerRadius}"
MinWidth="100" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}">
<Grid>
<Thumb VerticalAlignment="Bottom" Height="10" x:Name="ResizeVerticalThumb" Cursor="SizeNS">
<Thumb.Template>
<ControlTemplate>
<Canvas Background="White" Opacity="0"/>
</ControlTemplate>
</Thumb.Template>
<Thumb.Style>
<Style TargetType="Thumb">
<Style.Triggers>
<DataTrigger Binding="{Binding Resizable}" Value="Horizontal">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Resizable}" Value="None">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Thumb.Style>
</Thumb>
<Thumb HorizontalAlignment="Right" Width="10" x:Name="ResizeHorizontalThumb" Cursor="SizeWE">
<Thumb.Template>
<ControlTemplate>
<Canvas Background="White" Opacity="0"/>
</ControlTemplate>
</Thumb.Template>
<Thumb.Style>
<Style TargetType="Thumb">
<Style.Triggers>
<DataTrigger Binding="{Binding Resizable}" Value="Vertical">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Resizable}" Value="None">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Thumb.Style>
</Thumb>
<Thumb HorizontalAlignment="Right" VerticalAlignment="Bottom" Width="10" Height="10" x:Name="ResizeDiagonalThumb" Cursor="SizeNWSE">
<Thumb.Template>
<ControlTemplate>
<Canvas Background="White" Opacity="0"/>
</ControlTemplate>
</Thumb.Template>
<Thumb.Style>
<Style TargetType="Thumb">
<Style.Triggers>
<DataTrigger Binding="{Binding Resizable}" Value="Vertical">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Resizable}" Value="Horizontal">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
<DataTrigger Binding="{Binding Resizable}" Value="None">
<Setter Property="Visibility" Value="Collapsed" />
</DataTrigger>
</Style.Triggers>
</Style>
</Thumb.Style>
</Thumb>
<StackPanel>
<Canvas x:Name="HeaderTopMargin" Width="auto" Height="10"/>
<DockPanel>
<Image x:Name="HeaderIcon" MaxWidth="{TemplateBinding ArrowSize}" MaxHeight="{TemplateBinding ArrowSize}" DockPanel.Dock="Left" HorizontalAlignment="Left" VerticalAlignment="Center" Margin="10,0,0,0"/>
<TextBlock x:Name="NameLabel" TextWrapping="Wrap" Background="Transparent" Foreground="{TemplateBinding Foreground}" DockPanel.Dock="Left" HorizontalAlignment="Left" VerticalAlignment="Center" TextAlignment="Center"
FontFamily="{TemplateBinding TitleFontFamily}" FontSize="{TemplateBinding TitleFontSize}" Margin="10,0,0,0"/>
<controls:ArrowToggleButton x:Name="CollapseButton" Width="{TemplateBinding ArrowSize}" Height="{TemplateBinding ArrowSize}" DockPanel.Dock="Right" HorizontalAlignment="Right" VerticalAlignment="Center" Margin="20,0,10,0"/>
</DockPanel>
<Canvas x:Name="HeaderBottomMargin" Width="auto" Height="3"/>
<ContentPresenter x:Name="LeadingControlPresenter" Style="{TemplateBinding LeadingControlPresenterStyle}"/>
<DockPanel>
<ItemsControl x:Name="InputsList" IsTabStop="False">
<ItemsControl.Style>
<Style TargetType="ItemsControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=EndpointsStackingOrientation, RelativeSource={RelativeSource AncestorType={x:Type views:NodeView}}}" Value="Vertical">
<Setter Property="DockPanel.Dock" Value="Top"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=EndpointsStackingOrientation, RelativeSource={RelativeSource AncestorType={x:Type views:NodeView}}}" Value="Horizontal">
<Setter Property="DockPanel.Dock" Value="Left"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ItemsControl.Style>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="viewModels:NodeInputViewModel">
<reactiveUi:ViewModelViewHost ViewModel="{Binding}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" IsTabStop="False"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ItemsControl x:Name="OutputsList" IsTabStop="False">
<ItemsControl.Style>
<Style TargetType="ItemsControl">
<Style.Triggers>
<DataTrigger Binding="{Binding Path=EndpointsStackingOrientation, RelativeSource={RelativeSource AncestorType={x:Type views:NodeView}}}" Value="Vertical">
<Setter Property="DockPanel.Dock" Value="Bottom"/>
</DataTrigger>
<DataTrigger Binding="{Binding Path=EndpointsStackingOrientation, RelativeSource={RelativeSource AncestorType={x:Type views:NodeView}}}" Value="Horizontal">
<Setter Property="DockPanel.Dock" Value="Right"/>
</DataTrigger>
</Style.Triggers>
</Style>
</ItemsControl.Style>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="viewModels:NodeOutputViewModel">
<reactiveUi:ViewModelViewHost ViewModel="{Binding}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" IsTabStop="False"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</DockPanel>
<ItemsControl x:Name="EndpointGroupsList" IsTabStop="False">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="viewModels:EndpointGroupViewModel">
<reactiveUi:ViewModelViewHost ViewModel="{Binding}"
HorizontalContentAlignment="Stretch"
VerticalContentAlignment="Stretch"
IsTabStop="False" />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
<ContentPresenter x:Name="TrailingControlPresenter" Style="{TemplateBinding TrailingControlPresenterStyle}"/>
<Canvas x:Name="BottomMargin" Width="auto" Height="5"/>
</StackPanel>
</Grid>
</Border>
</controls:FillPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,44 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:NodeNetwork.Views"
xmlns:controls="clr-namespace:NodeNetwork.Views.Controls">
<Style TargetType="{x:Type views:PendingConnectionView}">
<Setter Property="RegularBrush" Value="#C8FFFFFF"/>
<Setter Property="ErrorBrush" Value="#C8FF0000"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="views:PendingConnectionView">
<controls:FillPanel>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="{x:Static views:PendingConnectionView.ErrorVisualStatesGroup}">
<VisualState Name="{x:Static views:PendingConnectionView.ErrorState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="RegularPath" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ErrorPath" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState Name="{x:Static views:PendingConnectionView.NonErrorState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="RegularPath" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ErrorPath" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Path x:Name="RegularPath" StrokeThickness="2" IsHitTestVisible="False" Stroke="{TemplateBinding RegularBrush}" Data="{TemplateBinding Geometry}"/>
<Path x:Name="ErrorPath" StrokeThickness="2" IsHitTestVisible="False" Stroke="{TemplateBinding ErrorBrush}" Data="{TemplateBinding Geometry}"/>
</controls:FillPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,78 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:views="clr-namespace:NodeNetwork.Views"
xmlns:controls="clr-namespace:NodeNetwork.Views.Controls">
<Style TargetType="{x:Type views:PortView}">
<Setter Property="RegularStroke" Value="#9E9E9E"/>
<Setter Property="RegularFill" Value="#E0E0E0"/>
<Setter Property="ConnectedStroke" Value="#AEAEAE"/>
<Setter Property="ConnectedFill" Value="#F0F0F0"/>
<Setter Property="HighlightStroke" Value="#30FFFFFF"/>
<Setter Property="HighlightFill" Value="#30FFFFFF"/>
<Setter Property="ErrorStroke" Value="#F44336"/>
<Setter Property="ErrorFill" Value="#FFCDD2"/>
<Setter Property="IsTabStop" Value="False"/>
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="views:PortView">
<controls:FillPanel>
<VisualStateManager.VisualStateGroups>
<VisualStateGroup Name="{x:Static views:PortView.ConnectedVisualStatesGroup}">
<VisualState Name="{x:Static views:PortView.ConnectedState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ConnectedEllipse" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState Name="{x:Static views:PortView.DisconnectedState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ConnectedEllipse" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup Name="{x:Static views:PortView.HighlightVisualStatesGroup}">
<VisualState Name="{x:Static views:PortView.HighlightedState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="HighlightEllipse" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState Name="{x:Static views:PortView.NonHighlightedState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="HighlightEllipse" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
<VisualStateGroup Name="{x:Static views:PortView.ErrorVisualStatesGroup}">
<VisualState Name="{x:Static views:PortView.ErrorState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ErrorEllipse" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Visible}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState Name="{x:Static views:PortView.NonErrorState}">
<Storyboard>
<ObjectAnimationUsingKeyFrames Duration="0" Storyboard.TargetName="ErrorEllipse" Storyboard.TargetProperty="Visibility">
<DiscreteObjectKeyFrame Value="{x:Static Visibility.Collapsed}" />
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<Ellipse Name="RegularEllipse" StrokeThickness="2" Stroke="{TemplateBinding RegularStroke}" Fill="{TemplateBinding RegularFill}"/>
<Ellipse Name="ConnectedEllipse" StrokeThickness="2" Stroke="{TemplateBinding ConnectedStroke}" Fill="{TemplateBinding ConnectedFill}"/>
<Ellipse Name="ErrorEllipse" StrokeThickness="2" Stroke="{TemplateBinding ErrorStroke}" Fill="{TemplateBinding ErrorFill}"/>
<Ellipse Name="HighlightEllipse" StrokeThickness="2" Stroke="{TemplateBinding HighlightStroke}" Fill="{TemplateBinding HighlightFill}"/>
</controls:FillPanel>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>

View File

@@ -0,0 +1,134 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace NodeNetwork.Utilities
{
//Class for calculating the longest common subsequence of two lists
//For example: the LCS of 'computer' and 'houseboat' is 'out'
public class LongestCommonSubsequence
{
/// <summary>
/// The type of change that occured.
/// </summary>
public enum ChangeType
{
Removed, Added
}
/// <summary>
/// Returns the changes to be made to oldList to reach the state of newList.
/// First all items that are in oldList but not in the LCS of oldList and newList are removed.
/// The list is then identical to the LCS of oldList and newList.
/// Then all items that are in newList but not in the LCS of oldList and newList are added.
/// The list is then identical to newList.
/// </summary>
/// <typeparam name="T">The type of items contained in the two lists</typeparam>
/// <param name="oldList">The first list</param>
/// <param name="newList">The second list</param>
/// <returns></returns>
public static IEnumerable<(int index, T item, ChangeType change)> GetChanges<T>(IList<T> oldList, IList<T> newList)
{
if (oldList == null)
{
throw new ArgumentNullException(nameof(oldList));
}else if (newList == null)
{
throw new ArgumentNullException(nameof(newList));
}
T[] lcs = LongestCommonSubsequence.Calculate(oldList, newList).ToArray();
//Initial data => LCS
int lcsCursor = lcs.Length - 1;
for (int initialDataCursor = oldList.Count - 1; initialDataCursor >= 0; initialDataCursor--)
{
if (lcsCursor >= 0 && oldList[initialDataCursor].Equals(lcs[lcsCursor]))
{
lcsCursor--;
}
else
{
yield return (initialDataCursor, oldList[initialDataCursor], ChangeType.Removed);
}
}
//LCS => newdata
lcsCursor = 0;
for (int newDataCursor = 0; newDataCursor < newList.Count; newDataCursor++)
{
if (lcsCursor < lcs.Length && newList[newDataCursor].Equals(lcs[lcsCursor]))
{
lcsCursor++;
}
else
{
yield return (newDataCursor, newList[newDataCursor], ChangeType.Added);
}
}
}
/// <summary>
/// Returns the longest common subsequence of two lists
/// For example: the LCS of 'computer' and 'houseboat' is 'out'
/// </summary>
/// <typeparam name="T">The type of items contained in the two lists</typeparam>
/// <param name="seq1">The first list</param>
/// <param name="seq2">The second list</param>
/// <returns>An enumerable of items that are both in seq1 and seq2 and which follows the consecutive order of both lists</returns>
public static IEnumerable<T> Calculate<T>(IList<T> seq1, IList<T> seq2)
{
int[,] matrix = CalculateLCSMatrix(seq1, seq2);
return Backtrack(seq1, seq2, matrix).Reverse();
}
private static int[,] CalculateLCSMatrix<T>(IList<T> seq1, IList<T> seq2)
{
int[,] matrix = new int[seq1.Count + 1, seq2.Count + 1];
for (int i = 1; i < matrix.GetLength(0); i++)
{
for (int j = 1; j < matrix.GetLength(1); j++)
{
if (seq1[i - 1].Equals(seq2[j - 1]))
{
matrix[i, j] = matrix[i - 1, j - 1] + 1;
}
else
{
matrix[i, j] = Math.Max(matrix[i - 1, j], matrix[i, j - 1]);
}
}
}
return matrix;
}
private static IEnumerable<T> Backtrack<T>(IList<T> seq1, IList<T> seq2, int[,] matrix)
{
int i = matrix.GetLength(0) - 1;
int j = matrix.GetLength(1) - 1;
bool done = false;
while (!done)
{
if (i > 0 && j > 0 && seq1[i - 1].Equals(seq2[j - 1]))
{
yield return seq1[i - 1];
i -= 1;
j -= 1;
}
else if (j > 0 && (i == 0 || matrix[i, j - 1] >= matrix[i - 1, j]))
{
j -= 1;
}
else if (i > 0 && (j == 0 || matrix[i, j - 1] < matrix[i - 1, j]))
{
i -= 1;
}
else
{
done = true;
}
}
}
}
}

View File

@@ -0,0 +1,100 @@
using System;
using System.Linq.Expressions;
using System.Reactive.Linq;
using DynamicData;
using ReactiveUI;
namespace NodeNetwork.Utilities
{
public static class ReactiveExtensions
{
/// <summary>
/// Pass through values if and only if the last value produced by 'throttleCondition' is false.
/// When 'throttleCondition' is false, no values are passed through.
/// When 'throttleCondition' changes from true to false, if one or more values was blocked during the period
/// in which the throttle was active, the latest value will be passed through.
/// </summary>
/// <typeparam name="T">The datatype in the observable</typeparam>
/// <param name="self">The source observable</param>
/// <param name="throttleCondition">An observable of booleans that determines the current throttle state</param>
/// <returns>The new observable</returns>
public static IObservable<T> ThrottleWhen<T>(this IObservable<T> self, IObservable<bool> throttleCondition)
{
var isPaused = throttleCondition.Prepend(false).DistinctUntilChanged();
return Observable.Defer(() =>
{
object lockObj = new object();
bool gateIsOpen = false;
return Observable.CombineLatest(
self.Synchronize(lockObj).Do(_ => gateIsOpen = true),
isPaused.Synchronize(lockObj).Do(paused => gateIsOpen = !paused && gateIsOpen),
(number, paused) => (number, paused)
)
.Where(tuple => !tuple.paused && gateIsOpen)
.Select(tuple => tuple.number);
});
}
/// <summary>
/// Create a one way list binding from the viewmodel to the view.
/// The view list property will be automatically updated to reflect
/// the viewmodel source list property.
/// </summary>
/// <typeparam name="TView">The type of the view</typeparam>
/// <typeparam name="TViewModel">The type of the viewmodel</typeparam>
/// <typeparam name="TData">The type of the data stored in the list</typeparam>
/// <typeparam name="TProperty">The type of the target property in the view</typeparam>
/// <param name="self">The view used for the binding</param>
/// <param name="vmDummy">A dummy viewmodel parameter, used to infer the viewmodel type</param>
/// <param name="vmProperty">The source property in the viewmodel that contains the list</param>
/// <param name="viewProperty">The target property in the view to bind the list to.</param>
/// <returns>An object that when disposed, disconnects the binding.</returns>
public static IDisposable BindList<TView, TViewModel, TData, TProperty>(
this TView self,
TViewModel vmDummy,
Expression<Func<TViewModel, IObservableList<TData>>> vmProperty,
Expression<Func<TView, TProperty>> viewProperty
)
where TView: class, IViewFor<TViewModel>
where TViewModel: class
{
IDisposable lastBinding = null;
return
// Get latest viewmodel
self.WhenAnyValue(v => v.ViewModel)
.Where(vm => vm != null)
// Get latest non-null list from viewmodel property
.Select(vm => vm.WhenAnyValue(vmProperty))
.Switch()
.Where(sourceList => sourceList != null)
// Clean up last list binding
.Do(p => lastBinding?.Dispose())
// Create new list binding
.Select(sourceList =>
{
lastBinding = sourceList.Connect().Bind(out var list).Subscribe();
return list;
})
// When the observable is disposed, dispose the list binding too
.Finally(() =>
{
lastBinding?.Dispose();
})
// Bind the new bindable list to the view property
.BindTo(self, viewProperty);
}
/// <summary>
/// Takes an observable of T values and returns an observable of tuples of T values containing the latest value and the previous value.
/// The first item in the source observable produces a tuple with the previous value set to default(T).
/// </summary>
/// <typeparam name="T">The type of object in the observable</typeparam>
/// <param name="obs">The source observable</param>
/// <returns>The resulting observable</returns>
public static IObservable<(T OldValue, T NewValue)> PairWithPreviousValue<T>(this IObservable<T> obs)
{
return obs.Scan((oldValue: default(T), newValue: default(T)), (pair, newVal) => (pair.newValue, newVal));
}
}
}

View File

@@ -0,0 +1,22 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Data;
namespace NodeNetwork.Utilities.WPF
{
public class BoolToZIndexConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return ((bool) value) ? 1 : 0;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
namespace NodeNetwork.Utilities.WPF
{
public class NullVisibilityConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
return value == null ? Visibility.Collapsed : Visibility.Visible;
}
public object ConvertBack(object value, Type targetType, object parameter, System.Globalization.CultureInfo culture)
{
throw new NotSupportedException();
}
}
}

View File

@@ -0,0 +1,74 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Data;
using System.Windows.Media;
namespace NodeNetwork.Views.Controls
{
public static class WPFUtils
{
public static T FindParent<T>(DependencyObject childObject) where T : DependencyObject
{
DependencyObject curObj = childObject;
do
{
curObj = VisualTreeHelper.GetParent(curObj);
if (curObj == null) return default(T);
} while (!(curObj is T));
return (T)curObj;
}
public static DependencyObject GetVisualAncestorNLevelsUp(DependencyObject childObject, int levels)
{
DependencyObject curObj = childObject;
for (int i = 0; i < levels; i++)
{
curObj = VisualTreeHelper.GetParent(curObj);
if (curObj == null) return null;
}
return curObj;
}
public static IEnumerable<Point> GetIntersectionPoints(Geometry g1, Geometry g2)
{
Geometry og1 = g1.GetWidenedPathGeometry(new Pen(Brushes.Black, 1.0));
Geometry og2 = g2.GetWidenedPathGeometry(new Pen(Brushes.Black, 1.0));
CombinedGeometry cg = new CombinedGeometry(GeometryCombineMode.Intersect, og1, og2);
PathGeometry pg = cg.GetFlattenedPathGeometry();
foreach (PathFigure figure in pg.Figures)
{
Rect fig = new PathGeometry(new[] { figure }).Bounds;
yield return new Point(fig.Left + fig.Width / 2.0, fig.Top + fig.Height / 2.0);
}
}
public static IEnumerable<T> FindDescendantsOfType<T>(DependencyObject root, bool skipChildrenOnHit) where T : DependencyObject
{
int childCount = VisualTreeHelper.GetChildrenCount(root);
for (int i = 0; i < childCount; i++)
{
var obj = VisualTreeHelper.GetChild(root, i);
if (obj is T t)
{
yield return t;
if (skipChildrenOnHit)
{
continue;
}
}
foreach (var subChild in FindDescendantsOfType<T>(obj, skipChildrenOnHit))
{
yield return subChild;
}
}
}
}
}

View File

@@ -0,0 +1,57 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
namespace NodeNetwork
{
/// <summary>
/// A class that represents a generic validation result.
/// </summary>
public abstract class ValidationResult
{
/// <summary>
/// True if the subject is valid
/// </summary>
public bool IsValid { get; }
/// <summary>
/// A viewmodel of the message that is to be displayed explaining this validation result.
/// </summary>
public object MessageViewModel { get; }
protected ValidationResult(bool isValid, object messageViewModel)
{
this.IsValid = isValid;
this.MessageViewModel = messageViewModel;
}
}
/// <summary>
/// A validation of the node network.
/// </summary>
public class NetworkValidationResult : ValidationResult
{
/// <summary>
/// If false, the network is in a state where trying to parse it (by walking from node to node) can cause problems.
/// For example, this property is false if the network contains loops since parsing it could then result in infinite loops.
/// </summary>
public bool NetworkIsTraversable { get; }
public NetworkValidationResult(bool isValid, bool isTraversable, object messageViewModel) : base(isValid, messageViewModel)
{
NetworkIsTraversable = isTraversable;
}
}
/// <summary>
/// A validation of a connection between nodes.
/// </summary>
public class ConnectionValidationResult : ValidationResult
{
public ConnectionValidationResult(bool isValid, object messageViewModel) : base(isValid, messageViewModel)
{
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -0,0 +1,228 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using NodeNetwork.ViewModels;
using ReactiveUI;
namespace NodeNetwork.Views
{
[TemplateVisualState(Name = HighlightedState, GroupName = HighlightVisualStatesGroup)]
[TemplateVisualState(Name = NonHighlightedState, GroupName = HighlightVisualStatesGroup)]
[TemplateVisualState(Name = ErrorState, GroupName = ErrorVisualStatesGroup)]
[TemplateVisualState(Name = NonErrorState, GroupName = ErrorVisualStatesGroup)]
[TemplateVisualState(Name = MarkedForDeleteState, GroupName = MarkedForDeleteVisualStatesGroup)]
[TemplateVisualState(Name = NotMarkedForDeleteState, GroupName = MarkedForDeleteVisualStatesGroup)]
public class ConnectionView : Control, IViewFor<ConnectionViewModel>
{
#region ViewModel
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel),
typeof(ConnectionViewModel), typeof(ConnectionView), new PropertyMetadata(null));
public ConnectionViewModel ViewModel
{
get => (ConnectionViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (ConnectionViewModel)value;
}
#endregion
#region States
#region HighlightStates
public const string HighlightVisualStatesGroup = "HighlightStates";
public const string HighlightedState = "Highlighted";
public const string NonHighlightedState = "NonHighlighted";
#endregion
#region ErrorStates
public const string ErrorVisualStatesGroup = "ErrorStates";
public const string ErrorState = "Error";
public const string NonErrorState = "NoError";
#endregion
#region ErrorStates
public const string MarkedForDeleteVisualStatesGroup = "MarkedForDeleteStates";
public const string MarkedForDeleteState = "Marked";
public const string NotMarkedForDeleteState = "NotMarked";
#endregion
#endregion
#region RegularBrush
public Brush RegularBrush
{
get => (Brush)this.GetValue(RegularBrushProperty);
set => this.SetValue(RegularBrushProperty, value);
}
public static readonly DependencyProperty RegularBrushProperty = DependencyProperty.Register(nameof(RegularBrush), typeof(Brush), typeof(ConnectionView), new PropertyMetadata());
#endregion
#region ErrorBrush
public Brush ErrorBrush
{
get => (Brush)this.GetValue(ErrorBrushProperty);
set => this.SetValue(ErrorBrushProperty, value);
}
public static readonly DependencyProperty ErrorBrushProperty = DependencyProperty.Register(nameof(ErrorBrush), typeof(Brush), typeof(ConnectionView), new PropertyMetadata());
#endregion
#region HighlightBrush
public Brush HighlightBrush
{
get => (Brush)this.GetValue(HighlightBrushProperty);
set => this.SetValue(HighlightBrushProperty, value);
}
public static readonly DependencyProperty HighlightBrushProperty = DependencyProperty.Register(nameof(HighlightBrush), typeof(Brush), typeof(ConnectionView), new PropertyMetadata());
#endregion
#region MarkedForDeleteBrush
public Brush MarkedForDeleteBrush
{
get => (Brush)this.GetValue(MarkedForDeleteBrushProperty);
set => this.SetValue(MarkedForDeleteBrushProperty, value);
}
public static readonly DependencyProperty MarkedForDeleteBrushProperty =
DependencyProperty.Register(nameof(MarkedForDeleteBrush), typeof(Brush), typeof(ConnectionView), new PropertyMetadata());
#endregion
#region Geometry
public Geometry Geometry
{
get => (Geometry)this.GetValue(GeometryProperty);
private set => this.SetValue(GeometryProperty, value);
}
public static readonly DependencyProperty GeometryProperty = DependencyProperty.Register(nameof(Geometry), typeof(Geometry), typeof(ConnectionView));
#endregion
public ConnectionView()
{
this.DefaultStyleKey = typeof(ConnectionView);
SetupPathData();
SetupBrushesBinding();
}
public override void OnApplyTemplate()
{
VisualStateManager.GoToState(this, NonHighlightedState, false);
VisualStateManager.GoToState(this, NonErrorState, false);
VisualStateManager.GoToState(this, NotMarkedForDeleteState, false);
}
private void SetupPathData()
{
this.WhenActivated(d => d(
this.WhenAny(
v => v.ViewModel.Input.Port.CenterPoint,
v => v.ViewModel.Input.PortPosition,
v => v.ViewModel.Output.Port.CenterPoint,
v => v.ViewModel.Output.PortPosition,
(a, b, c, e) => (a, b, c, e))
.Select(_
=> BuildSmoothBezier(
ViewModel.Input.Port.CenterPoint,
ViewModel.Input.PortPosition,
ViewModel.Output.Port.CenterPoint,
ViewModel.Output.PortPosition))
.BindTo(this, v => v.Geometry)
));
}
private void SetupBrushesBinding()
{
this.WhenActivated(d =>
{
this.WhenAnyValue(v => v.ViewModel.IsHighlighted).Subscribe(isHighlighted =>
{
VisualStateManager.GoToState(this, isHighlighted ? HighlightedState : NonHighlightedState, true);
}).DisposeWith(d);
this.WhenAnyValue(v => v.ViewModel.IsInErrorState).Subscribe(isInErrorState =>
{
VisualStateManager.GoToState(this, isInErrorState ? ErrorState : NonErrorState, true);
}).DisposeWith(d);
this.WhenAnyValue(v => v.ViewModel.IsMarkedForDelete).Subscribe(isMarkedForDelete =>
{
VisualStateManager.GoToState(this, isMarkedForDelete ? MarkedForDeleteState : NotMarkedForDeleteState, true);
}).DisposeWith(d);
});
}
public static PathGeometry BuildSmoothBezier(Point startPoint, PortPosition startPosition, Point endPoint, PortPosition endPosition)
{
Vector startGradient = ToGradient(startPosition);
Vector endGradient = ToGradient(endPosition);
return BuildSmoothBezier(startPoint, startGradient, endPoint, endGradient);
}
public static PathGeometry BuildSmoothBezier(Point startPoint, PortPosition startPosition, Point endPoint)
{
Vector startGradient = ToGradient(startPosition);
Vector endGradient = -startGradient;
return BuildSmoothBezier(startPoint, startGradient, endPoint, endGradient);
}
public static PathGeometry BuildSmoothBezier(Point startPoint, Point endPoint, PortPosition endPosition)
{
Vector endGradient = ToGradient(endPosition);
Vector startGradient = -endGradient;
return BuildSmoothBezier(startPoint, startGradient, endPoint, endGradient);
}
private static Vector ToGradient(PortPosition portPosition)
{
switch (portPosition)
{
case PortPosition.Left:
return new Vector(-1, 0);
case PortPosition.Right:
return new Vector(1, 0);
default:
throw new NotImplementedException();
}
}
private const double MinGradient = 10;
private const double WidthScaling = 5;
private static PathGeometry BuildSmoothBezier(Point startPoint, Vector startGradient, Point endPoint, Vector endGradient)
{
double width = endPoint.X - startPoint.X;
var gradientScale = Math.Sqrt(Math.Abs(width) * WidthScaling + MinGradient * MinGradient);
Point startGradientPoint = startPoint + startGradient * gradientScale;
Point endGradientPoint = endPoint + endGradient * gradientScale;
Point midPoint = new Point((startGradientPoint.X + endGradientPoint.X) / 2d, (startPoint.Y + endPoint.Y) / 2d);
PathFigure pathFigure = new PathFigure
{
StartPoint = startPoint,
IsClosed = false,
Segments =
{
new QuadraticBezierSegment(startGradientPoint, midPoint, true),
new QuadraticBezierSegment(endGradientPoint, endPoint, true)
}
};
PathGeometry geom = new PathGeometry();
geom.Figures.Add(pathFigure);
return geom;
}
}
}

View File

@@ -0,0 +1,66 @@
<ToggleButton x:Class="NodeNetwork.Views.Controls.ArrowToggleButton"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:NodeNetwork.Views"
mc:Ignorable="d"
d:DesignHeight="30" d:DesignWidth="30" Name="button">
<ToggleButton.Template>
<ControlTemplate>
<Viewbox StretchDirection="Both" Stretch="Uniform">
<Grid Width="20" Height="20">
<Path Width="20" Height="12" Stretch="Fill" Stroke="#333" Fill="Transparent" StrokeThickness="3" Data="M 0,0 L 10,9.5 L 20,0 ">
<Path.Style>
<Style TargetType="Path">
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=button}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Path.Stroke).(SolidColorBrush.Color)" To="#555" Duration="0:0:0.1"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<ColorAnimation Storyboard.TargetProperty="(Path.Stroke).(SolidColorBrush.Color)" To="#333" Duration="0:0:0.1"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Path.Style>
</Path>
<Grid.RenderTransform>
<RotateTransform CenterX="10" CenterY="10"/>
</Grid.RenderTransform>
<Grid.Style>
<Style TargetType="Grid">
<Style.Triggers>
<DataTrigger Binding="{Binding IsChecked, ElementName=button}" Value="True">
<DataTrigger.EnterActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Grid.RenderTransform).(RotateTransform.Angle)" To="-90" Duration="0:0:0.1"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.EnterActions>
<DataTrigger.ExitActions>
<BeginStoryboard>
<Storyboard>
<DoubleAnimation Storyboard.TargetProperty="(Grid.RenderTransform).(RotateTransform.Angle)" To="0" Duration="0:0:0.1"/>
</Storyboard>
</BeginStoryboard>
</DataTrigger.ExitActions>
</DataTrigger>
</Style.Triggers>
</Style>
</Grid.Style>
</Grid>
</Viewbox>
</ControlTemplate>
</ToggleButton.Template>
</ToggleButton>

View File

@@ -0,0 +1,12 @@
using System.Windows.Controls.Primitives;
namespace NodeNetwork.Views.Controls
{
public partial class ArrowToggleButton : ToggleButton
{
public ArrowToggleButton()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,437 @@
using System;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Input;
using System.Windows.Media;
namespace NodeNetwork.Views.Controls
{
public class DragCanvas : Canvas
{
#region Position
public static readonly DependencyProperty DragOffsetProperty = DependencyProperty.Register(nameof(DragOffset),
typeof(Point), typeof(DragCanvas), new PropertyMetadata(new Point(), DragOffsetChanged));
/// <summary>
/// Gets or sets the current canvas drag offset.
/// </summary>
public Point DragOffset
{
get => (Point)GetValue(DragOffsetProperty);
set => SetValue(DragOffsetProperty, value);
}
private static void DragOffsetChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var canvas = (DragCanvas)d;
if (e.NewValue is Point position)
{
canvas.ApplyDragToChildren(position.X - canvas._previousDragOffset.X, position.Y - canvas._previousDragOffset.Y);
}
}
#endregion
#region Dragging
/// <summary>
/// Triggered when the user clicks and moves the canvas, starting a drag
/// </summary>
/// <param name="sender">The dragcanvas that triggered this event</param>
/// <param name="args">The mouseevent that triggered this event</param>
public delegate void DragStartEventHandler(object sender, MouseEventArgs args);
public event DragStartEventHandler DragStart;
/// <summary>
/// Triggered when the user drags the canvas
/// </summary>
/// <param name="sender">The dragcanvas that triggered this event</param>
/// <param name="args">Contains the distance traveled since the last drag move or drag start event</param>
public delegate void DragMoveEventHandler(object sender, DragMoveEventArgs args);
public event DragMoveEventHandler DragMove;
/// <summary>
/// Triggered when the user releases the mouse and the drag stops.
/// </summary>
/// <param name="sender">The dragcanvas that triggered this event</param>
/// <param name="args">Contains the total distance traveled</param>
public delegate void DragEndEventHandler(object sender, DragMoveEventArgs args);
public event DragEndEventHandler DragStop;
public bool IsDraggingEnabled { get; set; } = true;
#region StartDragGesture
public static readonly DependencyProperty StartDragGestureProperty = DependencyProperty.Register(nameof(StartDragGesture),
typeof(MouseGesture), typeof(DragCanvas), new PropertyMetadata(new MouseGesture(MouseAction.LeftClick)));
/// <summary>
/// This mouse gesture starts a drag on the canvas. Left click by default.
/// </summary>
public MouseGesture StartDragGesture
{
get => (MouseGesture)GetValue(StartDragGestureProperty);
set => SetValue(StartDragGestureProperty, value);
}
#endregion
/// <summary>
/// Used when the mousebutton is down to check if the initial click was in this element.
/// This is useful because we dont want to assume a drag operation when the user moves the mouse but originally clicked a different element
/// </summary>
private bool _userClickedThisElement;
/// <summary>
/// Is a drag operation currently in progress?
/// </summary>
private bool _dragActive;
/// <summary>
/// The position of the mouse (screen co-ordinate) where the mouse was clicked down.
/// </summary>
private Point _originScreenCoordPosition;
/// <summary>
/// The position of the mouse (screen co-ordinate) when the previous DragDelta event was fired
/// </summary>
private Point _previousMouseScreenPos;
private Point _previousDragOffset;
/// <summary>
/// This event puts the control into a state where it is ready for a drag operation.
/// </summary>
protected override void OnMouseDown(MouseButtonEventArgs e)
{
if (IsDraggingEnabled && StartDragGesture.Matches(this, e))
{
_userClickedThisElement = true;
_previousMouseScreenPos = _originScreenCoordPosition = e.GetPosition(this);
Focus();
CaptureMouse(); //All mouse events will now be handled by the dragcanvas
}
}
/// <summary>
/// Trigger a dragging event when the user moves the mouse while the left mouse button is pressed
/// </summary>
protected override void OnMouseMove(MouseEventArgs e)
{
if (_userClickedThisElement && !_dragActive)
{
_dragActive = true;
DragStart?.Invoke(this, e);
}
if (_dragActive)
{
Point curMouseScreenPos = e.GetPosition(this);
if (!curMouseScreenPos.Equals(_previousMouseScreenPos))
{
double xDelta = curMouseScreenPos.X - _previousMouseScreenPos.X;
double yDelta = curMouseScreenPos.Y - _previousMouseScreenPos.Y;
var dragEvent = new DragMoveEventArgs(e, xDelta, yDelta);
DragMove?.Invoke(this, dragEvent);
this.DragOffset = new Point(_previousDragOffset.X + xDelta, _previousDragOffset.Y + yDelta);
_previousMouseScreenPos = curMouseScreenPos;
}
}
base.OnMouseMove(e);
}
/// <summary>
/// Stop dragging when the user releases the mouse button
/// </summary>
protected override void OnMouseUp(MouseButtonEventArgs e)
{
_userClickedThisElement = false;
ReleaseMouseCapture(); //Stop absorbing all mouse events
if (_dragActive)
{
_dragActive = false;
Point curMouseScreenPos = e.GetPosition(this);
double xDelta = curMouseScreenPos.X - _originScreenCoordPosition.X;
double yDelta = curMouseScreenPos.Y - _originScreenCoordPosition.Y;
DragStop?.Invoke(this, new DragMoveEventArgs(e, xDelta, yDelta));
}
}
private void ApplyDragToChildren(double deltaX, double deltaY)
{
foreach (UIElement cur in Children)
{
double prevLeft = Canvas.GetLeft(cur);
if (Double.IsNaN(prevLeft))
{
prevLeft = 0;
}
double prevTop = Canvas.GetTop(cur);
if (Double.IsNaN(prevTop))
{
prevTop = 0;
}
Canvas.SetLeft(cur, prevLeft + (deltaX));
Canvas.SetTop(cur, prevTop + (deltaY));
}
_previousDragOffset = new Point(_previousDragOffset.X + deltaX, _previousDragOffset.Y + deltaY);
}
#endregion
#region Zoom
public event EventHandler<ZoomEventArgs> Zoom;
private double _wheelOffset = 6;
#region ZoomFactor
public static readonly DependencyProperty ZoomFactorProperty = DependencyProperty.Register(nameof(ZoomFactor),
typeof(double), typeof(DragCanvas), new PropertyMetadata(1d, OnZoomFactorPropChanged, ZoomFactorValueCoerce));
public double ZoomFactor
{
get => (double)GetValue(ZoomFactorProperty);
set => SetValue(ZoomFactorProperty, value);
}
private bool isUpdatingZoomFactor;
private static void OnZoomFactorPropChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
DragCanvas dc = (DragCanvas)d;
if (!dc.isUpdatingZoomFactor)
{
dc.SetZoomImpl(new Point(dc.ActualWidth / 2, dc.ActualHeight / 2), (double)e.OldValue, (double)e.NewValue, null);
}
}
private static object ZoomFactorValueCoerce(DependencyObject d, object baseValue)
{
if (baseValue is double doubleValue)
{
var canvas = (DragCanvas)d;
if (doubleValue < canvas.MinZoomFactor)
{
return canvas.MinZoomFactor;
}
else if (doubleValue > canvas.MaxZoomFactor)
{
return canvas.MaxZoomFactor;
}
}
return baseValue;
}
#endregion
#region MaxZoomFactor
public static readonly DependencyProperty MaxZoomFactorProperty = DependencyProperty.Register(nameof(MaxZoomFactor),
typeof(double), typeof(DragCanvas), new FrameworkPropertyMetadata(2.5d, MaxZoomFactorChanged));
public double MaxZoomFactor
{
get { return (double)GetValue(MaxZoomFactorProperty); }
set { SetValue(MaxZoomFactorProperty, value); }
}
private static void MaxZoomFactorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var doubleValue = (double)e.NewValue;
if (double.IsNaN(doubleValue) || double.IsInfinity(doubleValue) || doubleValue <= 0)
{
throw new ArgumentException("MaxZoomFactor can not be NaN, Infinity or less than zero");
}
var canvas = (DragCanvas)d;
var binding = BindingOperations.GetBindingExpression(canvas, ZoomFactorProperty);
binding?.UpdateTarget();
}
#endregion
#region MinZoomFactor
public static readonly DependencyProperty MinZoomFactorProperty = DependencyProperty.Register(nameof(MinZoomFactor),
typeof(double), typeof(DragCanvas), new FrameworkPropertyMetadata(0.15d, MinZoomFactorChanged));
public double MinZoomFactor
{
get { return (double)GetValue(MinZoomFactorProperty); }
set { SetValue(MinZoomFactorProperty, value); }
}
private static void MinZoomFactorChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var doubleValue = (double)e.NewValue;
if (double.IsNaN(doubleValue) || double.IsInfinity(doubleValue) || doubleValue <= 0)
{
throw new ArgumentException("MinZoomFactor can not be NaN, Infinity or less than zero");
}
var canvas = (DragCanvas)d;
var binding = BindingOperations.GetBindingExpression(canvas, ZoomFactorProperty);
binding?.UpdateTarget();
}
#endregion
private Rect ZoomView(Rect curView, double curZoom, double newZoom, Point relZoomPoint) //curView in content space, relZoomPoint is relative to view space
{
double zoomModifier = curZoom / newZoom;
Size newSize = new Size(curView.Width * zoomModifier, curView.Height * zoomModifier);
Point zoomCenter = new Point(curView.X + (curView.Width * relZoomPoint.X), curView.Y + (curView.Height * relZoomPoint.Y));
double newX = zoomCenter.X - (relZoomPoint.X * newSize.Width);
double newY = zoomCenter.Y - (relZoomPoint.Y * newSize.Height);
Point newPos = new Point(newX, newY);
return new Rect(newPos, newSize);
}
protected override void OnMouseWheel(MouseWheelEventArgs e)
{
e.Handled = true;
//Calculate new scaling factor
var wheelOffset = _wheelOffset + (e.Delta / 120);
double newScale = Math.Log(1 + (wheelOffset / 10d)) * 2d;
if (newScale < MinZoomFactor)
{
newScale = MinZoomFactor;
}
else if (newScale > MaxZoomFactor)
{
newScale = MaxZoomFactor;
}
Point zoomCenter = e.GetPosition(this);
SetZoom(zoomCenter, newScale, e);
}
public void SetZoom(Point zoomCenter, double newScale, MouseEventArgs parentEvent)
{
SetZoomImpl(zoomCenter, ZoomFactor, newScale, parentEvent);
isUpdatingZoomFactor = true;
ZoomFactor = newScale;
isUpdatingZoomFactor = false;
}
private void SetZoomImpl(Point zoomCenter, double oldScale, double newScale, MouseEventArgs parentEvent)
{
//Calculate current viewing window onto the content
Point topLeftContentSpace = TranslatePoint(new Point(0, 0), Children[0]);
Point bottomRightContentSpace = TranslatePoint(new Point(ActualWidth, ActualHeight), Children[0]);
Rect curView = new Rect
{
Location = topLeftContentSpace,
Size = new Size(bottomRightContentSpace.X - topLeftContentSpace.X, bottomRightContentSpace.Y - topLeftContentSpace.Y)
};
//Mouse position as a fraction of the view size
Point relZoomPoint = new Point
{
X = zoomCenter.X / this.ActualWidth,
Y = zoomCenter.Y / this.ActualHeight
};
//Calculate new viewing window
Rect newView = ZoomView(curView, oldScale, newScale, relZoomPoint);
//Calculate new content offset based on the new view
Point newOffset = new Point(-newView.X * newScale, -newView.Y * newScale);
//Calculate new viewing window scale
ScaleTransform newScaleTransform = new ScaleTransform
{
ScaleX = newScale,
ScaleY = newScale
};
var zoomEvent = new ZoomEventArgs(parentEvent, new ScaleTransform(oldScale, oldScale), newScaleTransform, newOffset);
Zoom?.Invoke(this, zoomEvent);
ApplyZoomToChildren(zoomEvent);
DragOffset = new Point(zoomEvent.ContentOffset.X, zoomEvent.ContentOffset.Y);
_wheelOffset = 10d * Math.Pow(Math.E, newScale / 2) - 10;
}
private void ApplyZoomToChildren(ZoomEventArgs e)
{
foreach (UIElement cur in this.Children)
{
cur.RenderTransform = e.NewScale;
}
}
#endregion
#region Viewport
/// <summary>
/// Centers the canvas and sets the minimum zoom factor for the specified viewport.
/// </summary>
/// <param name="viewport">The desired viewport.</param>
public void SetViewport(Rect viewport)
{
// Get current view size
var topLeftContentSpace = TranslatePoint(new Point(0, 0), Children[0]);
var bottomRightContentSpace = TranslatePoint(new Point(ActualWidth, ActualHeight), Children[0]);
var curViewSize = new Size(bottomRightContentSpace.X - topLeftContentSpace.X, bottomRightContentSpace.Y - topLeftContentSpace.Y);
// Calc new scale
var oldZoom = ZoomFactor;
var newScaleX = oldZoom * curViewSize.Width / viewport.Width;
var newScaleY = oldZoom * curViewSize.Height / viewport.Height;
// Calc new zoom
var zoom = Math.Min(newScaleX, newScaleY);
ZoomFactor = zoom;
this.UpdateLayout();
var boundingCenter = new Point(viewport.TopLeft.X + viewport.Width / 2d, viewport.TopLeft.Y + viewport.Height / 2d);
// Update current view size
topLeftContentSpace = TranslatePoint(new Point(0, 0), Children[0]);
bottomRightContentSpace = TranslatePoint(new Point(ActualWidth, ActualHeight), Children[0]);
curViewSize = new Size(bottomRightContentSpace.X - topLeftContentSpace.X, bottomRightContentSpace.Y - topLeftContentSpace.Y);
// Calc new position offset
var viewOffset = new Point(boundingCenter.X - curViewSize.Width / 2d, boundingCenter.Y - curViewSize.Height / 2d);
this.DragOffset = new Point(-viewOffset.X * ZoomFactor, -viewOffset.Y * ZoomFactor);
}
#endregion
}
public class DragMoveEventArgs : EventArgs
{
public MouseEventArgs MouseEvent { get; }
public double DeltaX { get; }
public double DeltaY { get; }
public DragMoveEventArgs(MouseEventArgs mouseEvent, double deltaX, double deltaY)
{
this.MouseEvent = mouseEvent;
this.DeltaX = deltaX;
this.DeltaY = deltaY;
}
}
public class ZoomEventArgs : EventArgs
{
public MouseEventArgs MouseEvent { get; }
public ScaleTransform OldScaleScale { get; }
public ScaleTransform NewScale { get; }
public Point ContentOffset { get; }
public ZoomEventArgs(MouseEventArgs e, ScaleTransform oldScale, ScaleTransform newScale, Point contentOffset)
{
this.MouseEvent = e;
this.OldScaleScale = oldScale;
this.NewScale = newScale;
this.ContentOffset = contentOffset;
}
}
}

View File

@@ -0,0 +1,43 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
namespace NodeNetwork.Views.Controls
{
/// <summary>
/// Simple panel that stretches its children to fill the panel.
/// </summary>
public class FillPanel : Panel
{
protected override Size MeasureOverride(Size availableSize)
{
Size maxSize = new Size(0, 0);
foreach (UIElement e in InternalChildren)
{
e.Measure(availableSize);
maxSize = new Size(
Math.Max(maxSize.Width, e.DesiredSize.Width),
Math.Max(maxSize.Height, e.DesiredSize.Height)
);
}
return maxSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
Rect size = new Rect(new Point(), finalSize);
foreach (UIElement e in InternalChildren)
{
e.Arrange(size);
}
return finalSize;
}
}
}

View File

@@ -0,0 +1,497 @@
<reactiveUi:ViewModelViewHost x:Class="NodeNetwork.Views.Controls.ViewModelViewHostNoAnimations"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:NodeNetwork.Views.Controls"
xmlns:reactiveUi="http://reactiveui.net"
mc:Ignorable="d"
d:DesignHeight="300" d:DesignWidth="300">
<reactiveUi:ViewModelViewHost.Style>
<Style TargetType="reactiveUi:TransitioningContentControl">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="reactiveUi:TransitioningContentControl">
<Grid
x:Name="PART_Container"
VerticalAlignment="{TemplateBinding VerticalAlignment}"
HorizontalAlignment="{TemplateBinding HorizontalAlignment}">
<VisualStateManager.VisualStateGroups>
<VisualStateGroup x:Name="PresentationStates">
<VisualState x:Name="Normal">
<Storyboard>
<ObjectAnimationUsingKeyFrames
BeginTime="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_Fade">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="1" To="0"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="0" To="1"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_SlideLeft">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="-30" To="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_SlideRight">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="-30" To="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_SlideDown">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="-30" To="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_SlideUp">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="-30" To="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_MoveLeft">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="-30" To="0"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="0" To="30"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_MoveRight">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="-30" To="0"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="0" To="30"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_MoveDown">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="-30" To="0"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="0" To="30"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_MoveUp">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="-30" To="0"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="0" To="30"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_DropDown">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="-30" To="0"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="0" To="30"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="0" To="1"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="1" To="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_DropUp">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="0" To="30"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="-30" To="0"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="0" To="1"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="1" To="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_DropRight">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="-30" To="0"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="0" To="30"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="0" To="1"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="1" To="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_DropLeft">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="-30" To="0"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="0" To="30"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="0" To="1"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="1" To="0"/>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_BounceLeftIn">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="-90" To="0">
</DoubleAnimation>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="0" To="1">
</DoubleAnimation>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_BounceLeftOut">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="0" To="-90"/>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_BounceRightIn">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="-90" To="0"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="0" To="1"/>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_BounceRightOut">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.X)"
From="0" To="-90"/>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_BounceUpIn">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="-90" To="0"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="0" To="1"/>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_BounceUpOut">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="0" To="-90"/>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_BounceDownIn">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="-90" To="0"/>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Opacity)"
From="0" To="1"/>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
<VisualState x:Name="Transition_BounceDownOut">
<Storyboard>
<DoubleAnimation
BeginTime="00:00:00" Duration="00:00:00"
Storyboard.TargetName="PART_PreviousContentPresentationSite"
Storyboard.TargetProperty="(UIElement.RenderTransform).(TransformGroup.Children)[1].(TranslateTransform.Y)"
From="0" To="-90"/>
<ObjectAnimationUsingKeyFrames
Storyboard.TargetName="PART_CurrentContentPresentationSite"
Storyboard.TargetProperty="(UIElement.Visibility)">
<DiscreteObjectKeyFrame KeyTime="00:00:00">
<DiscreteObjectKeyFrame.Value>
<Visibility>Collapsed</Visibility>
</DiscreteObjectKeyFrame.Value>
</DiscreteObjectKeyFrame>
</ObjectAnimationUsingKeyFrames>
</Storyboard>
</VisualState>
</VisualStateGroup>
</VisualStateManager.VisualStateGroups>
<ContentPresenter
x:Name="PART_PreviousContentPresentationSite"
Content="{x:Null}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<ContentPresenter.RenderTransform>
<TransformGroup>
<TransformGroup.Children>
<ScaleTransform ScaleX="1" ScaleY="1" />
<TranslateTransform X="0" Y="0" />
</TransformGroup.Children>
</TransformGroup>
</ContentPresenter.RenderTransform>
</ContentPresenter>
<ContentPresenter
x:Name="PART_CurrentContentPresentationSite"
Content="{x:Null}"
ContentTemplate="{TemplateBinding ContentTemplate}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}">
<ContentPresenter.RenderTransform>
<TransformGroup>
<TransformGroup.Children>
<ScaleTransform ScaleX="1" ScaleY="1" />
<TranslateTransform X="0" Y="0" />
</TransformGroup.Children>
</TransformGroup>
</ContentPresenter.RenderTransform>
</ContentPresenter>
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</reactiveUi:ViewModelViewHost.Style>
</reactiveUi:ViewModelViewHost>

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using ReactiveUI;
namespace NodeNetwork.Views.Controls
{
public partial class ViewModelViewHostNoAnimations : ViewModelViewHost
{
public ViewModelViewHostNoAnimations()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,64 @@
using System;
using System.Collections.Generic;
using System.Reactive.Disposables;
using System.Text;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using NodeNetwork.Utilities;
using NodeNetwork.ViewModels;
using ReactiveUI;
namespace NodeNetwork.Views
{
[TemplatePart(Name = nameof(NameLabel), Type = typeof(TextBlock))]
[TemplatePart(Name = nameof(InputsList), Type = typeof(ItemsControl))]
[TemplatePart(Name = nameof(OutputsList), Type = typeof(ItemsControl))]
[TemplatePart(Name = nameof(EndpointGroupsList), Type = typeof(ItemsControl))]
public class EndpointGroupView : ReactiveUserControl<EndpointGroupViewModel>
{
private TextBlock NameLabel { get; set; }
private ItemsControl InputsList { get; set; }
private ItemsControl OutputsList { get; set; }
private ItemsControl EndpointGroupsList { get; set; }
#region Properties
public static readonly DependencyProperty TitleFontFamilyProperty = DependencyProperty.Register(nameof(TitleFontFamily), typeof(FontFamily), typeof(EndpointGroupView));
public FontFamily TitleFontFamily
{
get => (FontFamily)GetValue(TitleFontFamilyProperty);
set => SetValue(TitleFontFamilyProperty, value);
}
public static readonly DependencyProperty TitleFontSizeProperty = DependencyProperty.Register(nameof(TitleFontSize), typeof(double), typeof(EndpointGroupView));
public double TitleFontSize
{
get => (double)GetValue(TitleFontSizeProperty);
set => SetValue(TitleFontSizeProperty, value);
}
#endregion
public EndpointGroupView()
{
DefaultStyleKey = typeof(EndpointGroupView);
this.WhenActivated(d =>
{
this.OneWayBind(ViewModel, vm => vm.Group.Name, v => v.NameLabel.Text).DisposeWith(d);
this.BindList(ViewModel, vm => vm.VisibleInputs, v => v.InputsList.ItemsSource).DisposeWith(d);
this.BindList(ViewModel, vm => vm.VisibleOutputs, v => v.OutputsList.ItemsSource).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.Children, v => v.EndpointGroupsList.ItemsSource).DisposeWith(d);
});
}
public override void OnApplyTemplate()
{
NameLabel = GetTemplateChild(nameof(NameLabel)) as TextBlock;
InputsList = GetTemplateChild(nameof(InputsList)) as ItemsControl;
OutputsList = GetTemplateChild(nameof(OutputsList)) as ItemsControl;
EndpointGroupsList = GetTemplateChild(nameof(EndpointGroupsList)) as ItemsControl;
}
}
}

View File

@@ -0,0 +1,56 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using NodeNetwork.ViewModels;
using ReactiveUI;
namespace NodeNetwork.Views
{
[TemplatePart(Name = nameof(TextBlock), Type = typeof(TextBlock))]
public class ErrorMessageView : Control, IViewFor<ErrorMessageViewModel>
{
#region ViewModel
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel),
typeof(ErrorMessageViewModel), typeof(ErrorMessageView), new PropertyMetadata(null));
public ErrorMessageViewModel ViewModel
{
get => (ErrorMessageViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (ErrorMessageViewModel)value;
}
#endregion
private TextBlock TextBlock { get; set; }
public ErrorMessageView()
{
DefaultStyleKey = typeof(ErrorMessageView);
SetupBindings();
}
private void SetupBindings()
{
this.WhenActivated(d =>
{
this.OneWayBind(ViewModel, vm => vm.Message, v => v.TextBlock.Text).DisposeWith(d);
});
}
public override void OnApplyTemplate()
{
TextBlock = GetTemplateChild(nameof(TextBlock)) as TextBlock;
}
}
}

View File

@@ -0,0 +1,87 @@
<UserControl x:Class="NodeNetwork.Views.NetworkView"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:local="clr-namespace:NodeNetwork.Views"
xmlns:controls="clr-namespace:NodeNetwork.Views.Controls"
xmlns:reactiveUi="http://reactiveui.net"
xmlns:viewModels="clr-namespace:NodeNetwork.ViewModels"
xmlns:wpf="clr-namespace:NodeNetwork.Utilities.WPF"
mc:Ignorable="d"
d:DesignHeight="500" d:DesignWidth="800" Focusable="True" AllowDrop="True" x:Name="self" Background="#333">
<UserControl.InputBindings>
<KeyBinding x:Name="deleteBinding" Key="Delete"/>
</UserControl.InputBindings>
<UserControl.Resources>
<wpf:BoolToZIndexConverter x:Key="BoolToZIndexConverter"/>
</UserControl.Resources>
<Grid Focusable="True" KeyboardNavigation.IsTabStop="False">
<controls:DragCanvas Zoom="DragCanvas_OnZoom" x:Name="dragCanvas" MouseLeftButtonDown="OnClickCanvas" Background="#01000000">
<Canvas Name="contentContainer" LayoutUpdated="ContentContainer_OnLayoutUpdated" Width="{Binding ActualWidth, ElementName=dragCanvas}" Height="{Binding ActualHeight, ElementName=dragCanvas}">
<Canvas.Clip>
<RectangleGeometry x:Name="clippingGeometry"/>
</Canvas.Clip>
<!-- Bit of a hack, but this allows backgrounds that move with the nodes and connections.
Had to use a separate giant canvas because contentContainer is actually pretty small and any control higher won't have the image moving properly.
-->
<Canvas Name="backgroundCanvas" IsHitTestVisible="False" Width="1E15" Height="1E15" Canvas.Left="-1E6" Canvas.Top="-1E6" Background="{Binding NetworkBackground, ElementName=self}"/>
<ItemsControl x:Name="connectionsControl" Width="{Binding ActualWidth, ElementName=contentContainer}" Height="{Binding ActualHeight, ElementName=contentContainer}" IsTabStop="False">
<ItemsControl.ItemTemplate>
<DataTemplate>
<controls:ViewModelViewHostNoAnimations ViewModel="{Binding}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" IsTabStop="False"/>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<Canvas></Canvas>
</ItemsPanelTemplate>
<!-- Stop connections from stacking -->
</ItemsControl.ItemsPanel>
</ItemsControl>
<ItemsControl x:Name="nodesControl" Width="{Binding ActualWidth, ElementName=contentContainer}" Height="{Binding ActualHeight, ElementName=contentContainer}" IsTabStop="False">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="viewModels:NodeViewModel">
<Canvas>
<!-- When modifying, check the comment in NetworkView.OnNodeDragStart -->
<Thumb DragStarted="OnNodeDragStart" DragDelta="OnNodeDrag" DragCompleted="OnNodeDragEnd" Canvas.Left="{Binding Path=Position.X}" Canvas.Top="{Binding Path=Position.Y}">
<Thumb.Template>
<ControlTemplate>
<controls:ViewModelViewHostNoAnimations ViewModel="{Binding}" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" IsTabStop="False"/>
</ControlTemplate>
</Thumb.Template>
</Thumb>
</Canvas>
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemContainerStyle>
<Style TargetType="{x:Type ContentPresenter}">
<Style.Setters>
<Setter Property="Panel.ZIndex" Value="{Binding Path=IsSelected, Converter={StaticResource BoolToZIndexConverter}}"/>
</Style.Setters>
</Style>
</ItemsControl.ItemContainerStyle>
</ItemsControl>
<Line x:Name="cutLine" Stroke="LightGray" StrokeDashArray="2, 4" StrokeThickness="1" />
<Rectangle x:Name="selectionRectangle" Stroke="White" Fill="Transparent" StrokeDashArray="2, 4" StrokeThickness="1"/>
<controls:ViewModelViewHostNoAnimations x:Name="pendingConnectionView" HorizontalContentAlignment="Stretch" VerticalContentAlignment="Stretch" IsTabStop="False"/>
<controls:ViewModelViewHostNoAnimations x:Name="pendingNodeView" Opacity="0.5" IsTabStop="False"/>
</Canvas>
</controls:DragCanvas>
<Popup x:Name="messagePopup" Placement="Top" StaysOpen="True" HorizontalAlignment="Right">
<reactiveUi:ViewModelViewHost Name="messagePopupHost" IsTabStop="False"/>
</Popup>
<Border Name="messageHostBorder" Background="#EEE" CornerRadius="5" HorizontalAlignment="Center" VerticalAlignment="Top" Padding="10, 10, 10, 10" Margin="20">
<reactiveUi:ViewModelViewHost Name="messageHost" IsTabStop="False"/>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,581 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Diagnostics;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Data;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using System.Windows.Navigation;
using System.Windows.Shapes;
using DynamicData;
using NodeNetwork.Utilities;
using NodeNetwork.ViewModels;
using NodeNetwork.Views.Controls;
using ReactiveUI;
namespace NodeNetwork.Views
{
public partial class NetworkView : IViewFor<NetworkViewModel>
{
#region ViewModel
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel),
typeof(NetworkViewModel), typeof(NetworkView), new PropertyMetadata(null));
public NetworkViewModel ViewModel
{
get => (NetworkViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (NetworkViewModel)value;
}
#endregion
#region NetworkViewportRegion
/// <summary>
/// The rectangle to use as a clipping mask for contentContainer
/// </summary>
public Rect NetworkViewportRegion
{
get
{
double left = Canvas.GetLeft(contentContainer);
if (Double.IsNaN(left))
{
left = 0;
}
double top = Canvas.GetTop(contentContainer);
if (Double.IsNaN(top))
{
top = 0;
}
if (contentContainer.RenderTransform is ScaleTransform)
{
GeneralTransform transform = this.TransformToDescendant(contentContainer);
return transform.TransformBounds(new Rect(0, 0, this.ActualWidth, this.ActualHeight));
}
return new Rect(-left, -top, this.ActualWidth, this.ActualHeight);
}
}
private BindingExpressionBase _viewportBinding;
#endregion
#region Node move events
public class NodeMovementEventArgs : EventArgs
{
public IEnumerable<NodeViewModel> Nodes { get; }
public NodeMovementEventArgs(IEnumerable<NodeViewModel> nodes) => Nodes = nodes.ToList();
}
//Start
public class NodeMoveStartEventArgs : NodeMovementEventArgs
{
public DragStartedEventArgs DragEvent { get; }
public NodeMoveStartEventArgs(IEnumerable<NodeViewModel> nodes, DragStartedEventArgs dragEvent) :
base(nodes)
{
DragEvent = dragEvent;
}
}
public delegate void NodeMoveStartDelegate(object sender, NodeMoveStartEventArgs e);
/// <summary>Occurs when a (set of) node(s) is selected and starts moving.</summary>
public event NodeMoveStartDelegate NodeMoveStart;
//Move
public class NodeMoveEventArgs : NodeMovementEventArgs
{
public DragDeltaEventArgs DragEvent { get; }
public NodeMoveEventArgs(IEnumerable<NodeViewModel> nodes, DragDeltaEventArgs dragEvent) : base(nodes)
{
DragEvent = dragEvent;
}
}
public delegate void NodeMoveDelegate(object sender, NodeMoveEventArgs e);
/// <summary>Occurs one or more times as the mouse changes position when a (set of) node(s) is selected and has mouse capture.</summary>
public event NodeMoveDelegate NodeMove;
//End
public class NodeMoveEndEventArgs : NodeMovementEventArgs
{
public DragCompletedEventArgs DragEvent { get; }
public NodeMoveEndEventArgs(IEnumerable<NodeViewModel> nodes, DragCompletedEventArgs dragEvent) : base(nodes)
{
DragEvent = dragEvent;
}
}
public delegate void NodeMoveEndDelegate(object sender, NodeMoveEndEventArgs e);
/// <summary>Occurs when a (set of) node(s) loses mouse capture.</summary>
public event NodeMoveEndDelegate NodeMoveEnd;
#endregion
#region NetworkBackground
public static readonly DependencyProperty NetworkBackgroundProperty = DependencyProperty.Register(nameof(NetworkBackground),
typeof(Brush), typeof(NetworkView), new PropertyMetadata(null));
public Brush NetworkBackground
{
get => (Brush)GetValue(NetworkBackgroundProperty);
set => SetValue(NetworkBackgroundProperty, value);
}
#endregion
/// <summary>
/// The element that is used as an origin for the position of the elements of the network.
/// </summary>
/// <example>
/// Can be used for calculating the mouse position relative to the network.
/// <code>
/// Mouse.GetPosition(network.CanvasOriginElement)
/// </code>
/// </example>
public IInputElement CanvasOriginElement => contentContainer;
#region StartCutGesture
public static readonly DependencyProperty StartCutGestureProperty = DependencyProperty.Register(nameof(StartCutGesture),
typeof(MouseGesture), typeof(NetworkView), new PropertyMetadata(new MouseGesture(MouseAction.RightClick)));
/// <summary>
/// This mouse gesture starts a cut, making the cutline visible. Right click by default.
/// </summary>
public MouseGesture StartCutGesture
{
get => (MouseGesture)GetValue(StartCutGestureProperty);
set => SetValue(StartCutGestureProperty, value);
}
#endregion
#region StartSelectionRectangleGesture
public static readonly DependencyProperty StartSelectionRectangleGestureProperty = DependencyProperty.Register(nameof(StartSelectionRectangleGesture),
typeof(MouseGesture), typeof(NetworkView), new PropertyMetadata(new MouseGesture(MouseAction.LeftClick, ModifierKeys.Shift)));
/// <summary>
/// This mouse gesture starts a selection, making the selection rectangle visible. Left click + Shift by default.
/// </summary>
public MouseGesture StartSelectionRectangleGesture
{
get => (MouseGesture)GetValue(StartSelectionRectangleGestureProperty);
set => SetValue(StartSelectionRectangleGestureProperty, value);
}
#endregion
public NetworkView()
{
InitializeComponent();
if (DesignerProperties.GetIsInDesignMode(this)) { return; }
SetupNodes();
SetupConnections();
SetupCutLine();
SetupViewportBinding();
SetupKeyboardShortcuts();
SetupErrorMessages();
SetupDragAndDrop();
SetupSelectionRectangle();
}
#region Setup
private void SetupNodes()
{
this.WhenActivated(d => d(
this.BindList(ViewModel, vm => vm.Nodes, v => v.nodesControl.ItemsSource)
));
}
private void SetupConnections()
{
this.WhenActivated(d =>
{
this.BindList(ViewModel, vm => vm.Connections, v => v.connectionsControl.ItemsSource).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.PendingConnection, v => v.pendingConnectionView.ViewModel).DisposeWith(d);
this.Events().MouseMove
.Select(e => e.GetPosition(contentContainer))
.BindTo(this, v => v.ViewModel.PendingConnection.LooseEndPoint)
.DisposeWith(d);
this.Events().MouseLeftButtonUp
.Where(_ => ViewModel.PendingConnection != null)
.Subscribe(_ => ViewModel.OnPendingConnectionDropped())
.DisposeWith(d);
});
}
private void SetupKeyboardShortcuts()
{
this.WhenActivated(d =>
{
this.Events().MouseLeftButtonDown.Subscribe(_ => Focus()).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.DeleteSelectedNodes, v => v.deleteBinding.Command).DisposeWith(d);
});
}
private void SetupCutLine()
{
this.WhenActivated(d =>
{
this.OneWayBind(ViewModel, vm => vm.CutLine.StartPoint.X, v => v.cutLine.X1).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.CutLine.StartPoint.Y, v => v.cutLine.Y1).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.CutLine.EndPoint.X, v => v.cutLine.X2).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.CutLine.EndPoint.Y, v => v.cutLine.Y2).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.CutLine.IsVisible, v => v.cutLine.Visibility,
isVisible => isVisible ? Visibility.Visible : Visibility.Collapsed)
.DisposeWith(d);
bool cutGestured = false;
dragCanvas.Events().MouseDown.Subscribe(e =>
{
if (StartCutGesture.Matches(this, e))
{
Point pos = e.GetPosition(contentContainer);
ViewModel.CutLine.StartPoint = pos;
ViewModel.CutLine.EndPoint = pos;
cutGestured = true;
e.Handled = true;
}
}).DisposeWith(d);
dragCanvas.Events().MouseMove.Subscribe(e =>
{
if (!ViewModel.CutLine.IsVisible && cutGestured)
{
ViewModel.StartCut();
}
if (ViewModel.CutLine.IsVisible)
{
ViewModel.CutLine.EndPoint = e.GetPosition(contentContainer);
ViewModel.CutLine.IntersectingConnections.Edit(l =>
{
l.Clear();
l.AddRange(FindIntersectingConnections().Where(val => val.intersects).Select(val => val.con));
});
e.Handled = true;
}
}).DisposeWith(d);
dragCanvas.Events().MouseUp.Subscribe(e =>
{
cutGestured = false;
if (ViewModel.CutLine.IsVisible)
{
//Do cuts
ViewModel.FinishCut();
e.Handled = true;
}
}).DisposeWith(d);
});
}
private void SetupViewportBinding()
{
this.WhenActivated(d =>
{
this.Bind(ViewModel, vm => vm.ZoomFactor, v => v.dragCanvas.ZoomFactor);
this.Bind(ViewModel, vm => vm.MaxZoomLevel, v => v.dragCanvas.MaxZoomFactor);
this.Bind(ViewModel, vm => vm.MinZoomLevel, v => v.dragCanvas.MinZoomFactor);
this.Bind(ViewModel, vm => vm.DragOffset, v => v.dragCanvas.DragOffset);
});
Binding binding = new Binding
{
Source = this,
Path = new PropertyPath(nameof(NetworkViewportRegion)),
Mode = BindingMode.OneWay,
UpdateSourceTrigger = UpdateSourceTrigger.PropertyChanged
};
_viewportBinding = BindingOperations.SetBinding(clippingGeometry, RectangleGeometry.RectProperty, binding);
}
private void SetupErrorMessages()
{
messageHostBorder.Visibility = Visibility.Collapsed; //Start collapsed
messagePopup.VerticalOffset = -15;
this.WhenActivated(d =>
{
this.OneWayBind(ViewModel, vm => vm.LatestValidation.IsValid, v => v.messageHostBorder.Visibility,
isValid => isValid ? Visibility.Collapsed : Visibility.Visible)
.DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.LatestValidation.MessageViewModel, v => v.messageHost.ViewModel)
.DisposeWith(d);
this.WhenAnyValue(v => v.ViewModel.PendingConnection.Validation)
.Select(_ => ViewModel.PendingConnection?.Validation?.MessageViewModel != null)
.BindTo(this, v => v.messagePopup.IsOpen)
.DisposeWith(d);
this.WhenAnyValue(v => v.ViewModel.PendingConnection.Validation)
.Select(_ => ViewModel.PendingConnection?.Validation?.MessageViewModel)
.BindTo(this, v => v.messagePopupHost.ViewModel)
.DisposeWith(d);
this.WhenAnyValue(vm => vm.ViewModel.PendingConnection.BoundingBox)
.Select(b => new Rect(contentContainer.TranslatePoint(b.TopLeft, this), contentContainer.TranslatePoint(b.BottomRight, this)))
.BindTo(this, v => v.messagePopup.PlacementRectangle)
.DisposeWith(d);
this.WhenAnyValue(vm => vm.ViewModel.PendingConnection.BoundingBox)
.Select(b => (b.Width / 2d) - (messagePopup.Child.RenderSize.Width / 2d))
.BindTo(this, v => v.messagePopup.HorizontalOffset)
.DisposeWith(d);
});
}
private void SetupDragAndDrop()
{
this.WhenActivated(d =>
{
this.OneWayBind(ViewModel, vm => vm.PendingNode, v => v.pendingNodeView.ViewModel).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.PendingNode, v => v.pendingNodeView.Visibility,
node => node == null ? Visibility.Collapsed : Visibility.Visible)
.DisposeWith(d);
this.WhenAnyValue(v => v.ViewModel.PendingNode.Position).Subscribe(pos =>
{
Canvas.SetLeft(pendingNodeView, pos.X);
Canvas.SetTop(pendingNodeView, pos.Y);
}).DisposeWith(d);
this.Events().DragOver.Subscribe(e =>
{
object data = e.Data.GetData("nodeVM");
NodeViewModel newNodeVm = data as NodeViewModel;
ViewModel.PendingNode = newNodeVm;
if (ViewModel.PendingNode != null)
{
ViewModel.PendingNode.Position = e.GetPosition(contentContainer);
}
e.Effects = newNodeVm != null ? DragDropEffects.Copy : DragDropEffects.None;
}).DisposeWith(d);
this.Events().Drop.Subscribe(e =>
{
object data = e.Data.GetData("nodeVM");
NodeViewModel newNodeVm = data as NodeViewModel;
if (newNodeVm != null)
{
this.ViewModel.PendingNode =
new NodeViewModel(); //Fixes issue with newNodeVm sticking around in pendingNodeView, messing up position updates
this.ViewModel.PendingNode = null;
newNodeVm.Position = e.GetPosition(contentContainer);
ViewModel.Nodes.Add(newNodeVm);
}
}).DisposeWith(d);
this.Events().DragLeave.Subscribe(_ => ViewModel.PendingNode = null).DisposeWith(d);
});
}
private void SetupSelectionRectangle()
{
this.WhenActivated(d =>
{
this.WhenAnyValue(vm => vm.ViewModel.SelectionRectangle.Rectangle.Left)
.Subscribe(left => Canvas.SetLeft(selectionRectangle, left))
.DisposeWith(d);
this.WhenAnyValue(vm => vm.ViewModel.SelectionRectangle.Rectangle.Top)
.Subscribe(top => Canvas.SetTop(selectionRectangle, top))
.DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.SelectionRectangle.Rectangle.Width, v => v.selectionRectangle.Width).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.SelectionRectangle.Rectangle.Height, v => v.selectionRectangle.Height).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.SelectionRectangle.IsVisible, v => v.selectionRectangle.Visibility).DisposeWith(d);
this.Events().PreviewMouseDown.Subscribe(e =>
{
if (ViewModel != null && StartSelectionRectangleGesture.Matches(this, e))
{
CaptureMouse();
dragCanvas.IsDraggingEnabled = false;
ViewModel.StartRectangleSelection();
ViewModel.SelectionRectangle.StartPoint = e.GetPosition(contentContainer);
ViewModel.SelectionRectangle.EndPoint = ViewModel.SelectionRectangle.StartPoint;
}
}).DisposeWith(d);
this.Events().MouseMove.Subscribe(e =>
{
if (ViewModel != null && ViewModel.SelectionRectangle.IsVisible)
{
ViewModel.SelectionRectangle.EndPoint = e.GetPosition(contentContainer);
UpdateSelectionRectangleIntersections();
}
}).DisposeWith(d);
this.Events().MouseUp.Subscribe(e =>
{
if (ViewModel != null && ViewModel.SelectionRectangle.IsVisible)
{
ViewModel.FinishRectangleSelection();
dragCanvas.IsDraggingEnabled = true;
ReleaseMouseCapture();
}
}).DisposeWith(d);
});
}
// Real, accurate but expensive hittesting
/*private void UpdateSelectionRectangleIntersections()
{
RectangleGeometry geometry = new RectangleGeometry(ViewModel.SelectionRectangle.Rectangle);
ViewModel.SelectionRectangle.IntersectingNodes.Clear();
VisualTreeHelper.HitTest(nodesControl, element =>
{
if (element is NodeView)
{
//return HitTestFilterBehavior.ContinueSkipChildren;
}
return HitTestFilterBehavior.Continue;
}, result =>
{
if ((result.VisualHit as FrameworkElement)?.DataContext is NodeViewModel nodeVm &&
!ViewModel.SelectionRectangle.IntersectingNodes.Contains(nodeVm))
{
Debug.WriteLine(result.VisualHit);
ViewModel.SelectionRectangle.IntersectingNodes.Add(nodeVm);
}
return HitTestResultBehavior.Continue;
}, new GeometryHitTestParameters(geometry));
}*/
// Approximate but cheap boundingbox-based hittesting
private void UpdateSelectionRectangleIntersections()
{
var selectionRect = ViewModel.SelectionRectangle.Rectangle;
var nodesHit = WPFUtils.FindDescendantsOfType<NodeViewBase>(nodesControl, true)
.Where(nodeView =>
{
//return selectionRect.Contains(new Rect(nodeView.ViewModel.Position, nodeView.RenderSize));
var viewModel = (NodeViewModel)((dynamic)nodeView).ViewModel;
return selectionRect.IntersectsWith(new Rect(viewModel.Position, nodeView.RenderSize));
})
.Select(view => (NodeViewModel)((dynamic)view).ViewModel);
ViewModel.SelectionRectangle.IntersectingNodes.Clear();
ViewModel.SelectionRectangle.IntersectingNodes.AddRange(nodesHit);
}
#endregion
#region Viewport bound updates
private void DragCanvas_OnZoom(object source, ZoomEventArgs args)
{
_viewportBinding?.UpdateTarget();
}
private void ContentContainer_OnLayoutUpdated(object sender, EventArgs e)
{
_viewportBinding?.UpdateTarget();
}
#endregion
#region Node move events
private void OnNodeDragStart(object sender, DragStartedEventArgs e)
{
// Hacky fix for issue #78. A nested thumb being dragged would also drag the node around, which is incorrect.
// For some reason, trying to stop the MouseMove event from bubbling up does not work, so instead we check
// here what caused this drag event. Only the Thumb around the node may cause drag events.
bool isCorrectSource = WPFUtils.GetVisualAncestorNLevelsUp((DependencyObject)e.OriginalSource, 6) == nodesControl;
if (NodeMoveStart != null && isCorrectSource)
{
var args = new NodeMoveStartEventArgs(ViewModel.SelectedNodes.Items, e);
NodeMoveStart(sender, args);
}
}
private void OnNodeDrag(object sender, DragDeltaEventArgs e)
{
// See OnNodeDragStart
bool isCorrectSource = WPFUtils.GetVisualAncestorNLevelsUp((DependencyObject)e.OriginalSource, 6) == nodesControl;
if (isCorrectSource)
{
foreach (NodeViewModel node in ViewModel.SelectedNodes.Items)
{
node.Position = new Point(node.Position.X + e.HorizontalChange, node.Position.Y + e.VerticalChange);
}
if (NodeMove != null)
{
var args = new NodeMoveEventArgs(ViewModel.SelectedNodes.Items, e);
NodeMove(sender, args);
}
}
}
private void OnNodeDragEnd(object sender, DragCompletedEventArgs e)
{
// See OnNodeDragStart
bool isCorrectSource = WPFUtils.GetVisualAncestorNLevelsUp((DependencyObject)e.OriginalSource, 6) == nodesControl;
if (NodeMoveEnd != null && isCorrectSource)
{
var args = new NodeMoveEndEventArgs(ViewModel.SelectedNodes.Items, e);
NodeMoveEnd(sender, args);
}
}
#endregion
private void OnClickCanvas(object sender, MouseButtonEventArgs e)
{
ViewModel.ClearSelection();
}
private IEnumerable<(ConnectionViewModel con, bool intersects)> FindIntersectingConnections()
{
foreach (ConnectionViewModel con in ViewModel.Connections.Items)
{
PathGeometry conGeom = ConnectionView.BuildSmoothBezier(con.Input.Port.CenterPoint, con.Input.PortPosition, con.Output.Port.CenterPoint, con.Output.PortPosition);
LineGeometry cutLineGeom = new LineGeometry(ViewModel.CutLine.StartPoint, ViewModel.CutLine.EndPoint);
bool hasIntersections = WPFUtils.GetIntersectionPoints(conGeom, cutLineGeom).Any();
yield return (con, hasIntersections);
}
}
public void CenterAndZoomView()
{
if (ViewModel.Nodes.Count == 0)
{
return;
}
var bounding = ViewModel.Nodes.Items.Select(node =>
{
var currentTopLeft = node.Position;
var currentBottomRight = Point.Add(node.Position, new Vector(node.Size.Width, node.Size.Height));
var nodeBounding = new Rect(currentTopLeft, currentBottomRight);
return nodeBounding;
}).Aggregate((r1, r2) =>
{
r1.Union(r2);
return r1;
});
this.dragCanvas?.SetViewport(bounding);
}
}
}

View File

@@ -0,0 +1,37 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using NodeNetwork.ViewModels;
using ReactiveUI;
namespace NodeNetwork.Views
{
public class NodeEndpointEditorView : Control, IViewFor<NodeEndpointEditorViewModel>
{
#region ViewModel
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel),
typeof(NodeEndpointEditorViewModel), typeof(NodeEndpointEditorView), new PropertyMetadata(null));
public NodeEndpointEditorViewModel ViewModel
{
get => (NodeEndpointEditorViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (NodeEndpointEditorViewModel)value;
}
#endregion
public NodeEndpointEditorView()
{
DefaultStyleKey = typeof(NodeEndpointEditorView);
}
}
}

View File

@@ -0,0 +1,89 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using NodeNetwork.ViewModels;
using ReactiveUI;
using Splat;
namespace NodeNetwork.Views
{
[TemplatePart(Name = nameof(EndpointHost), Type = typeof(ViewModelViewHost))]
[TemplatePart(Name = nameof(EditorHost), Type = typeof(ViewModelViewHost))]
[TemplatePart(Name = nameof(NameLabel), Type = typeof(TextBlock))]
[TemplatePart(Name = nameof(Icon), Type = typeof(Image))]
public class NodeInputView : Control, IViewFor<NodeInputViewModel>
{
#region ViewModel
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel),
typeof(NodeInputViewModel), typeof(NodeInputView), new PropertyMetadata(null));
public NodeInputViewModel ViewModel
{
get => (NodeInputViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (NodeInputViewModel)value;
}
#endregion
private ViewModelViewHost EndpointHost { get; set; }
private ViewModelViewHost EditorHost { get; set; }
private TextBlock NameLabel { get; set; }
private Image Icon { get; set; }
private bool _isHeaderEmpty;
public NodeInputView()
{
DefaultStyleKey = typeof(NodeInputView);
SetupBindings();
}
private void SetupBindings()
{
this.WhenActivated(d =>
{
this.OneWayBind(ViewModel, vm => vm.Name, v => v.NameLabel.Text).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.Port, v => v.EndpointHost.ViewModel).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.Port.IsVisible, v => v.EndpointHost.Visibility).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.Editor, v => v.EditorHost.ViewModel).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.IsEditorVisible, v => v.EditorHost.Visibility).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.Icon, v => v.Icon.Source, img => img?.ToNative()).DisposeWith(d);
this.WhenAnyValue(v => v.ViewModel.Name, v => v.ViewModel.Icon,
(name, icon) => String.IsNullOrEmpty(name) && icon == null)
.Subscribe(v =>
{
_isHeaderEmpty = v;
if(EditorHost != null)
{
Grid.SetRow(EditorHost, _isHeaderEmpty ? 0 : 1);
}
})
.DisposeWith(d);
});
}
public override void OnApplyTemplate()
{
EndpointHost = GetTemplateChild(nameof(EndpointHost)) as ViewModelViewHost;
EditorHost = GetTemplateChild(nameof(EditorHost)) as ViewModelViewHost;
NameLabel = GetTemplateChild(nameof(NameLabel)) as TextBlock;
Icon = GetTemplateChild(nameof(Icon)) as Image;
if (EditorHost != null)
Grid.SetRow(EditorHost, _isHeaderEmpty ? 0 : 1);
}
}
}

View File

@@ -0,0 +1,88 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using NodeNetwork.ViewModels;
using ReactiveUI;
using Splat;
namespace NodeNetwork.Views
{
[TemplatePart(Name = nameof(EndpointHost), Type = typeof(ViewModelViewHost))]
[TemplatePart(Name = nameof(EditorHost), Type = typeof(ViewModelViewHost))]
[TemplatePart(Name = nameof(NameLabel), Type = typeof(TextBlock))]
[TemplatePart(Name = nameof(Icon), Type = typeof(Image))]
public class NodeOutputView : Control, IViewFor<NodeOutputViewModel>
{
#region ViewModel
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel),
typeof(NodeOutputViewModel), typeof(NodeOutputView), new PropertyMetadata(null));
public NodeOutputViewModel ViewModel
{
get => (NodeOutputViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (NodeOutputViewModel)value;
}
#endregion
private ViewModelViewHost EndpointHost { get; set; }
private ViewModelViewHost EditorHost { get; set; }
private TextBlock NameLabel { get; set; }
private Image Icon { get; set; }
private bool _isHeaderEmpty;
public NodeOutputView()
{
DefaultStyleKey = typeof(NodeOutputView);
SetupBindings();
}
private void SetupBindings()
{
this.WhenActivated(d =>
{
this.OneWayBind(ViewModel, vm => vm.Name, v => v.NameLabel.Text).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.Port, v => v.EndpointHost.ViewModel).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.Port.IsVisible, v => v.EndpointHost.Visibility).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.Editor, v => v.EditorHost.ViewModel).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.Icon, v => v.Icon.Source, img => img?.ToNative()).DisposeWith(d);
this.WhenAnyValue(v => v.ViewModel.Name, v => v.ViewModel.Icon,
(name, icon) => String.IsNullOrEmpty(name) && icon == null)
.Subscribe(v =>
{
_isHeaderEmpty = v;
if (EditorHost != null)
{
Grid.SetRow(EditorHost, _isHeaderEmpty ? 0 : 1);
}
})
.DisposeWith(d);
});
}
public override void OnApplyTemplate()
{
EndpointHost = GetTemplateChild(nameof(EndpointHost)) as ViewModelViewHost;
EditorHost = GetTemplateChild(nameof(EditorHost)) as ViewModelViewHost;
NameLabel = GetTemplateChild(nameof(NameLabel)) as TextBlock;
Icon = GetTemplateChild(nameof(Icon)) as Image;
if (EditorHost != null)
Grid.SetRow(EditorHost, _isHeaderEmpty ? 0 : 1);
}
}
}

View File

@@ -0,0 +1,235 @@
using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Linq.Expressions;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Controls.Primitives;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Media.Imaging;
using DynamicData;
using DynamicData.Binding;
using NodeNetwork.Utilities;
using NodeNetwork.ViewModels;
using NodeNetwork.Views.Controls;
using ReactiveUI;
using Splat;
namespace NodeNetwork.Views
{
[TemplatePart(Name = nameof(CollapseButton), Type = typeof(ArrowToggleButton))]
[TemplatePart(Name = nameof(NameLabel), Type = typeof(TextBlock))]
[TemplatePart(Name = nameof(HeaderIcon), Type = typeof(Image))]
[TemplatePart(Name = nameof(InputsList), Type = typeof(ItemsControl))]
[TemplatePart(Name = nameof(OutputsList), Type = typeof(ItemsControl))]
[TemplatePart(Name = nameof(EndpointGroupsList), Type = typeof(ItemsControl))]
[TemplatePart(Name = nameof(ResizeVerticalThumb), Type = typeof(Thumb))]
[TemplatePart(Name = nameof(ResizeHorizontalThumb), Type = typeof(Thumb))]
[TemplatePart(Name = nameof(ResizeDiagonalThumb), Type = typeof(Thumb))]
[TemplateVisualState(Name = SelectedState, GroupName = SelectedVisualStatesGroup)]
[TemplateVisualState(Name = UnselectedState, GroupName = SelectedVisualStatesGroup)]
[TemplateVisualState(Name = CollapsedState, GroupName = CollapsedVisualStatesGroup)]
[TemplateVisualState(Name = ExpandedState, GroupName = CollapsedVisualStatesGroup)]
public class NodeView : NodeViewBase, IViewFor<NodeViewModel>
{
#region SelectedStates
public const string SelectedVisualStatesGroup = "SelectedStates";
public const string SelectedState = "Selected";
public const string UnselectedState = "Unselected";
#endregion
#region CollapsedStates
public const string CollapsedVisualStatesGroup = "CollapsedStates";
public const string CollapsedState = "Collapsed";
public const string ExpandedState = "Expanded";
#endregion
#region ViewModel
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel),
typeof(NodeViewModel), typeof(NodeView), new PropertyMetadata(null));
public NodeViewModel ViewModel
{
get => (NodeViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (NodeViewModel)value;
}
#endregion
#region Properties
public static readonly DependencyProperty CornerRadiusProperty = DependencyProperty.Register(nameof(CornerRadius), typeof(CornerRadius), typeof(NodeView));
public CornerRadius CornerRadius
{
get => (CornerRadius)GetValue(CornerRadiusProperty);
set => SetValue(CornerRadiusProperty, value);
}
public static readonly DependencyProperty ArrowSizeProperty = DependencyProperty.Register(nameof(ArrowSize), typeof(double), typeof(NodeView));
public double ArrowSize
{
get => (double)GetValue(ArrowSizeProperty);
set => SetValue(ArrowSizeProperty, value);
}
public static readonly DependencyProperty TitleFontFamilyProperty = DependencyProperty.Register(nameof(TitleFontFamily), typeof(FontFamily), typeof(NodeView));
public FontFamily TitleFontFamily
{
get => (FontFamily)GetValue(TitleFontFamilyProperty);
set => SetValue(TitleFontFamilyProperty, value);
}
public static readonly DependencyProperty TitleFontSizeProperty = DependencyProperty.Register(nameof(TitleFontSize), typeof(double), typeof(NodeView));
public double TitleFontSize
{
get => (double)GetValue(TitleFontSizeProperty);
set => SetValue(TitleFontSizeProperty, value);
}
public static readonly DependencyProperty EndpointsStackingOrientationProperty = DependencyProperty.Register(nameof(EndpointsStackingOrientation), typeof(Orientation), typeof(NodeView));
public Orientation EndpointsStackingOrientation
{
get => (Orientation)GetValue(EndpointsStackingOrientationProperty);
set => SetValue(EndpointsStackingOrientationProperty, value);
}
public static readonly DependencyProperty LeadingControlPresenterStyleProperty = DependencyProperty.Register(nameof(LeadingControlPresenterStyle), typeof(Style), typeof(NodeView));
public Style LeadingControlPresenterStyle
{
get => (Style)GetValue(LeadingControlPresenterStyleProperty);
set => SetValue(LeadingControlPresenterStyleProperty, value);
}
public static readonly DependencyProperty TrailingControlPresenterStyleProperty = DependencyProperty.Register(nameof(TrailingControlPresenterStyle), typeof(Style), typeof(NodeView));
public Style TrailingControlPresenterStyle
{
get => (Style)GetValue(TrailingControlPresenterStyleProperty);
set => SetValue(TrailingControlPresenterStyleProperty, value);
}
#endregion
private ArrowToggleButton CollapseButton { get; set; }
private TextBlock NameLabel { get; set; }
private Image HeaderIcon { get; set; }
private ItemsControl InputsList { get; set; }
private ItemsControl OutputsList { get; set; }
private ItemsControl EndpointGroupsList { get; set; }
private Thumb ResizeVerticalThumb { get; set; }
private Thumb ResizeHorizontalThumb { get; set; }
private Thumb ResizeDiagonalThumb { get; set; }
public NodeView()
{
DefaultStyleKey = typeof(NodeView);
SetupBindings();
SetupEvents();
SetupVisualStateBindings();
}
public override void OnApplyTemplate()
{
CollapseButton = GetTemplateChild(nameof(CollapseButton)) as ArrowToggleButton;
NameLabel = GetTemplateChild(nameof(NameLabel)) as TextBlock;
HeaderIcon = GetTemplateChild(nameof(HeaderIcon)) as Image;
InputsList = GetTemplateChild(nameof(InputsList)) as ItemsControl;
OutputsList = GetTemplateChild(nameof(OutputsList)) as ItemsControl;
EndpointGroupsList = GetTemplateChild(nameof(EndpointGroupsList)) as ItemsControl;
ResizeVerticalThumb = GetTemplateChild(nameof(ResizeVerticalThumb)) as Thumb;
ResizeHorizontalThumb = GetTemplateChild(nameof(ResizeHorizontalThumb)) as Thumb;
ResizeDiagonalThumb = GetTemplateChild(nameof(ResizeDiagonalThumb)) as Thumb;
ResizeVerticalThumb.DragDelta += (sender, e) => ApplyResize(e, false, true);
ResizeHorizontalThumb.DragDelta += (sender, e) => ApplyResize(e, true, false);
ResizeDiagonalThumb.DragDelta += (sender, e) => ApplyResize(e, true, true);
VisualStateManager.GoToState(this, ExpandedState, false);
VisualStateManager.GoToState(this, UnselectedState, false);
}
private void ApplyResize(DragDeltaEventArgs e, bool horizontal, bool vertical)
{
if (horizontal)
{
MinWidth = Math.Max(20, MinWidth + e.HorizontalChange);
}
if (vertical)
{
MinHeight = Math.Max(20, MinHeight + e.VerticalChange);
}
}
private void SetupBindings()
{
this.WhenActivated(d =>
{
this.Bind(ViewModel, vm => vm.IsCollapsed, v => v.CollapseButton.IsChecked).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.Name, v => v.NameLabel.Text).DisposeWith(d);
this.BindList(ViewModel, vm => vm.VisibleInputs, v => v.InputsList.ItemsSource).DisposeWith(d);
this.BindList(ViewModel, vm => vm.VisibleOutputs, v => v.OutputsList.ItemsSource).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.VisibleEndpointGroups, v => v.EndpointGroupsList.ItemsSource).DisposeWith(d);
this.WhenAnyValue(v => v.ActualWidth, v => v.ActualHeight, (width, height) => new Size(width, height))
.BindTo(this, v => v.ViewModel.Size).DisposeWith(d);
this.OneWayBind(ViewModel, vm => vm.HeaderIcon, v => v.HeaderIcon.Source, img => img?.ToNative()).DisposeWith(d);
});
}
private void SetupEvents()
{
this.MouseLeftButtonDown += (sender, args) =>
{
this.Focus();
if (ViewModel == null)
{
return;
}
if (ViewModel.IsSelected)
{
return;
}
if (ViewModel.Parent != null && !Keyboard.IsKeyDown(Key.LeftCtrl) && !Keyboard.IsKeyDown(Key.RightCtrl))
{
ViewModel.Parent.ClearSelection();
}
ViewModel.IsSelected = true;
};
}
private void SetupVisualStateBindings()
{
this.WhenActivated(d =>
{
this.WhenAnyValue(v => v.ViewModel.IsCollapsed).Subscribe(isCollapsed =>
{
VisualStateManager.GoToState(this, isCollapsed ? CollapsedState : ExpandedState, true);
}).DisposeWith(d);
this.WhenAnyValue(v => v.ViewModel.IsSelected).Subscribe(isSelected =>
{
VisualStateManager.GoToState(this, isSelected ? SelectedState : UnselectedState, true);
}).DisposeWith(d);
});
}
}
}

View File

@@ -0,0 +1,13 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows.Controls;
namespace NodeNetwork.Views
{
public class NodeViewBase : Control
{
}
}

View File

@@ -0,0 +1,125 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Media;
using System.Windows.Shapes;
using NodeNetwork.ViewModels;
using ReactiveUI;
namespace NodeNetwork.Views
{
[TemplateVisualState(Name = ErrorState, GroupName = ErrorVisualStatesGroup)]
[TemplateVisualState(Name = NonErrorState, GroupName = ErrorVisualStatesGroup)]
public class PendingConnectionView : Control, IViewFor<PendingConnectionViewModel>
{
#region ViewModel
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel),
typeof(PendingConnectionViewModel), typeof(PendingConnectionView), new PropertyMetadata(null));
public PendingConnectionViewModel ViewModel
{
get => (PendingConnectionViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (PendingConnectionViewModel)value;
}
#endregion
#region States
#region ErrorStates
public const string ErrorVisualStatesGroup = "ErrorStates";
public const string ErrorState = "Error";
public const string NonErrorState = "NoError";
#endregion
#endregion
#region RegularBrush
public Brush RegularBrush
{
get => (Brush)this.GetValue(RegularBrushProperty);
set => this.SetValue(RegularBrushProperty, value);
}
public static readonly DependencyProperty RegularBrushProperty = DependencyProperty.Register(nameof(RegularBrush), typeof(Brush), typeof(PendingConnectionView));
#endregion
#region ErrorBrush
public Brush ErrorBrush
{
get => (Brush)this.GetValue(ErrorBrushProperty);
set => this.SetValue(ErrorBrushProperty, value);
}
public static readonly DependencyProperty ErrorBrushProperty = DependencyProperty.Register(nameof(ErrorBrush), typeof(Brush), typeof(PendingConnectionView));
#endregion
#region Geometry
public Geometry Geometry
{
get => (Geometry)this.GetValue(GeometryProperty);
set => this.SetValue(GeometryProperty, value);
}
public static readonly DependencyProperty GeometryProperty = DependencyProperty.Register(nameof(Geometry), typeof(Geometry), typeof(PendingConnectionView));
#endregion
public PendingConnectionView()
{
this.DefaultStyleKey = typeof(PendingConnectionView);
SetupPathData();
SetupVisualStateBindings();
}
public override void OnApplyTemplate()
{
VisualStateManager.GoToState(this, NonErrorState, false);
}
private void SetupPathData()
{
this.WhenActivated(d => d(
this.WhenAnyValue(v => v.ViewModel.LooseEndPoint)
.Select(_ =>
{
if (ViewModel.Input == null)
{
return ConnectionView.BuildSmoothBezier(ViewModel.Output.Port.CenterPoint,
ViewModel.Output.PortPosition,
ViewModel.LooseEndPoint);
}
else if (ViewModel.Output == null)
{
return ConnectionView.BuildSmoothBezier(ViewModel.LooseEndPoint,
ViewModel.Input.Port.CenterPoint,
ViewModel.Input.PortPosition);
}
else
{
return ConnectionView.BuildSmoothBezier(ViewModel.Output.Port.CenterPoint,
ViewModel.Output.PortPosition,
ViewModel.Input.Port.CenterPoint,
ViewModel.Input.PortPosition);
}
})
.BindTo(this, v => v.Geometry)
));
}
private void SetupVisualStateBindings()
{
this.WhenActivated(d => d(
this.WhenAnyValue(v => v.ViewModel.Validation.IsValid).Subscribe(isValid =>
{
VisualStateManager.GoToState(this, isValid ? NonErrorState : ErrorState, true);
})
));
}
}
}

View File

@@ -0,0 +1,231 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using System.Reactive.Linq;
using System.Text;
using System.Threading.Tasks;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
using NodeNetwork.ViewModels;
using NodeNetwork.Views.Controls;
using ReactiveUI;
namespace NodeNetwork.Views
{
[TemplateVisualState(Name = ConnectedState, GroupName = ConnectedVisualStatesGroup)]
[TemplateVisualState(Name = DisconnectedState, GroupName = ConnectedVisualStatesGroup)]
[TemplateVisualState(Name = HighlightedState, GroupName = HighlightVisualStatesGroup)]
[TemplateVisualState(Name = NonHighlightedState, GroupName = HighlightVisualStatesGroup)]
[TemplateVisualState(Name = ErrorState, GroupName = ErrorVisualStatesGroup)]
[TemplateVisualState(Name = NonErrorState, GroupName = ErrorVisualStatesGroup)]
public class PortView : Control, IViewFor<PortViewModel>
{
#region ViewModel
public static readonly DependencyProperty ViewModelProperty = DependencyProperty.Register(nameof(ViewModel),
typeof(PortViewModel), typeof(PortView), new PropertyMetadata(null));
public PortViewModel ViewModel
{
get => (PortViewModel)GetValue(ViewModelProperty);
set => SetValue(ViewModelProperty, value);
}
object IViewFor.ViewModel
{
get => ViewModel;
set => ViewModel = (PortViewModel)value;
}
#endregion
#region ConnectedStates
public const string ConnectedVisualStatesGroup = "ConnectedStates";
public const string ConnectedState = "Connected";
public const string DisconnectedState = "Disconnected";
#endregion
#region HighlightStates
public const string HighlightVisualStatesGroup = "HighlightStates";
public const string HighlightedState = "Highlighted";
public const string NonHighlightedState = "NonHighlighted";
#endregion
#region ErrorStates
public const string ErrorVisualStatesGroup = "ErrorStates";
public const string ErrorState = "Error";
public const string NonErrorState = "NoError";
#endregion
#region Brushes
#region RegularStroke
public Brush RegularStroke
{
get => (Brush)this.GetValue(RegularStrokeProperty);
set => this.SetValue(RegularStrokeProperty, value);
}
public static readonly DependencyProperty RegularStrokeProperty = DependencyProperty.Register(nameof(RegularStroke), typeof(Brush), typeof(PortView));
#endregion
#region RegularFill
public Brush RegularFill
{
get => (Brush)this.GetValue(RegularFillProperty);
set => this.SetValue(RegularFillProperty, value);
}
public static readonly DependencyProperty RegularFillProperty = DependencyProperty.Register(nameof(RegularFill), typeof(Brush), typeof(PortView));
#endregion
#region ConnectedStroke
public Brush ConnectedStroke
{
get => (Brush)this.GetValue(ConnectedStrokeProperty);
set => this.SetValue(ConnectedStrokeProperty, value);
}
public static readonly DependencyProperty ConnectedStrokeProperty = DependencyProperty.Register(nameof(ConnectedStroke), typeof(Brush), typeof(PortView));
#endregion
#region ConnectedFill
public Brush ConnectedFill
{
get => (Brush)this.GetValue(ConnectedFillProperty);
set => this.SetValue(ConnectedFillProperty, value);
}
public static readonly DependencyProperty ConnectedFillProperty = DependencyProperty.Register(nameof(ConnectedFill), typeof(Brush), typeof(PortView));
#endregion
#region HighlightStroke
public Brush HighlightStroke
{
get => (Brush)this.GetValue(HighlightStrokeProperty);
set => this.SetValue(HighlightStrokeProperty, value);
}
public static readonly DependencyProperty HighlightStrokeProperty = DependencyProperty.Register(nameof(HighlightStroke), typeof(Brush), typeof(PortView));
#endregion
#region HighlightFill
public Brush HighlightFill
{
get => (Brush)this.GetValue(HighlightFillProperty);
set => this.SetValue(HighlightFillProperty, value);
}
public static readonly DependencyProperty HighlightFillProperty = DependencyProperty.Register(nameof(HighlightFill), typeof(Brush), typeof(PortView));
#endregion
#region ErrorStroke
public Brush ErrorStroke
{
get => (Brush)this.GetValue(ErrorStrokeProperty);
set => this.SetValue(ErrorStrokeProperty, value);
}
public static readonly DependencyProperty ErrorStrokeProperty = DependencyProperty.Register(nameof(ErrorStroke), typeof(Brush), typeof(PortView));
#endregion
#region ErrorFill
public Brush ErrorFill
{
get => (Brush)this.GetValue(ErrorFillProperty);
set => this.SetValue(ErrorFillProperty, value);
}
public static readonly DependencyProperty ErrorFillProperty = DependencyProperty.Register(nameof(ErrorFill), typeof(Brush), typeof(PortView));
#endregion
#endregion
public PortView()
{
this.DefaultStyleKey = typeof(PortView);
}
public override void OnApplyTemplate()
{
VisualStateManager.GoToState(this, DisconnectedState, false);
VisualStateManager.GoToState(this, NonHighlightedState, false);
VisualStateManager.GoToState(this, NonErrorState, false);
SetupLayoutEvent();
SetupMouseEvents();
SetupVisualStateBindings();
}
private void SetupVisualStateBindings()
{
this.WhenActivated(d =>
{
this.WhenAnyObservable(v => v.ViewModel.Parent.Connections.CountChanged).Select(c => c == 0).Subscribe(isDisconnected =>
{
VisualStateManager.GoToState(this, isDisconnected ? DisconnectedState : ConnectedState, true);
}).DisposeWith(d);
this.WhenAnyValue(v => v.ViewModel.IsHighlighted).Subscribe(isHighlighted =>
{
VisualStateManager.GoToState(this, isHighlighted ? HighlightedState : NonHighlightedState, true);
}).DisposeWith(d);
this.WhenAnyValue(v => v.ViewModel.IsInErrorMode).Subscribe(isInErrorMode =>
{
VisualStateManager.GoToState(this, isInErrorMode ? ErrorState : NonErrorState, true);
}).DisposeWith(d);
});
}
protected virtual void SetupLayoutEvent()
{
this.WhenActivated(d =>
{
this.Events().LayoutUpdated.Subscribe(e =>
{
//Update endpoint center point
if (ViewModel == null)
{
return;
}
NetworkView networkView = WPFUtils.FindParent<NetworkView>(this);
if (networkView == null)
{
return;
}
Point center = new Point(this.ActualWidth / 2d, this.ActualHeight / 2d);
if (Margin.Left < 0)
{
center.X += Margin.Left;
}
else if (Margin.Right < 0)
{
center.X -= Margin.Right;
}
var transform = this.TransformToAncestor(networkView.contentContainer);
ViewModel.CenterPoint = transform.Transform(center);
}).DisposeWith(d);
});
}
private void SetupMouseEvents()
{
this.MouseLeftButtonDown += (sender, e) =>
{
e.Handled = true;
ViewModel.OnDragFromPort();
};
this.MouseEnter += (sender, e) =>
{
e.Handled = true;
ViewModel.OnPortEnter();
};
this.MouseLeave += (sender, e) =>
{
e.Handled = true;
ViewModel.OnPortLeave();
};
this.MouseLeftButtonUp += (sender, e) =>
{
e.Handled = true;
ViewModel.OnDropOnPort();
};
}
}
}

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,519 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v5.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v5.0": {
"NodeNetwork/6.0.0": {
"dependencies": {
"Microsoft.CSharp": "4.7.0",
"ReactiveUI": "13.2.18",
"ReactiveUI.Events.WPF": "13.2.18",
"ReactiveUI.WPF": "13.2.18",
"Splat.Drawing": "11.0.1",
"System.Buffers": "4.5.1",
"System.Collections.Immutable": "5.0.0",
"System.Data.DataSetExtensions": "4.5.0",
"System.Drawing.Primitives": "4.3.0",
"System.Memory": "4.5.4",
"System.Numerics.Vectors": "4.5.0",
"System.Runtime.CompilerServices.Unsafe": "5.0.0",
"System.Threading.Tasks.Extensions": "4.5.4",
"System.ValueTuple": "4.5.0",
"log4net": "2.0.12"
},
"runtime": {
"NodeNetwork.dll": {}
}
},
"DynamicData/7.1.1": {
"dependencies": {
"System.Reactive": "5.0.0"
},
"runtime": {
"lib/net5.0/DynamicData.dll": {
"assemblyVersion": "7.1.0.0",
"fileVersion": "7.1.1.11487"
}
}
},
"log4net/2.0.12": {
"dependencies": {
"System.Configuration.ConfigurationManager": "4.5.0"
},
"runtime": {
"lib/netstandard2.0/log4net.dll": {
"assemblyVersion": "2.0.12.0",
"fileVersion": "2.0.12.0"
}
}
},
"Microsoft.CSharp/4.7.0": {},
"Microsoft.NETCore.Platforms/2.0.0": {},
"Microsoft.NETCore.Targets/1.1.0": {},
"Pharmacist.Common/2.0.8": {
"dependencies": {
"System.Reactive": "5.0.0"
},
"runtime": {
"lib/net5.0/Pharmacist.Common.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.8.15338"
}
}
},
"ReactiveUI/13.2.18": {
"dependencies": {
"DynamicData": "7.1.1",
"Splat": "11.0.1",
"System.Reactive": "5.0.0",
"System.Runtime.Serialization.Primitives": "4.3.0"
},
"runtime": {
"lib/net5.0-windows7.0/ReactiveUI.dll": {
"assemblyVersion": "13.2.0.0",
"fileVersion": "13.2.18.42750"
}
}
},
"ReactiveUI.Events.WPF/13.2.18": {
"dependencies": {
"Pharmacist.Common": "2.0.8",
"System.Reactive": "5.0.0"
},
"runtime": {
"lib/net5.0-windows7.0/ReactiveUI.Events.WPF.dll": {
"assemblyVersion": "13.2.0.0",
"fileVersion": "13.2.18.42750"
}
}
},
"ReactiveUI.WPF/13.2.18": {
"dependencies": {
"ReactiveUI": "13.2.18",
"System.Reactive": "5.0.0"
},
"runtime": {
"lib/net5.0-windows7.0/ReactiveUI.Wpf.dll": {
"assemblyVersion": "13.2.0.0",
"fileVersion": "13.2.18.42750"
}
}
},
"Splat/11.0.1": {
"runtime": {
"lib/net5.0/Splat.dll": {
"assemblyVersion": "11.0.0.0",
"fileVersion": "11.0.1.61461"
}
}
},
"Splat.Drawing/11.0.1": {
"dependencies": {
"Splat": "11.0.1",
"System.Diagnostics.Contracts": "4.3.0",
"System.Drawing.Primitives": "4.3.0",
"System.Runtime.Serialization.Primitives": "4.3.0"
},
"runtime": {
"lib/net5.0-windows7.0/Splat.Drawing.dll": {
"assemblyVersion": "11.0.0.0",
"fileVersion": "11.0.1.61461"
}
}
},
"System.Buffers/4.5.1": {},
"System.Collections.Immutable/5.0.0": {},
"System.Configuration.ConfigurationManager/4.5.0": {
"dependencies": {
"System.Security.Cryptography.ProtectedData": "4.5.0",
"System.Security.Permissions": "4.5.0"
}
},
"System.Data.DataSetExtensions/4.5.0": {},
"System.Diagnostics.Contracts/4.3.0": {
"dependencies": {
"System.Runtime": "4.3.0"
}
},
"System.Drawing.Primitives/4.3.0": {
"dependencies": {
"System.Runtime": "4.3.0",
"System.Runtime.Extensions": "4.3.0"
}
},
"System.Globalization/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.IO/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"System.Text.Encoding": "4.3.0",
"System.Threading.Tasks": "4.3.0"
}
},
"System.Memory/4.5.4": {},
"System.Numerics.Vectors/4.5.0": {},
"System.Reactive/5.0.0": {
"runtime": {
"lib/net5.0/System.Reactive.dll": {
"assemblyVersion": "5.0.0.0",
"fileVersion": "5.0.0.1"
}
}
},
"System.Reflection/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.IO": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Reflection.Primitives/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Resources.ResourceManager/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Globalization": "4.3.0",
"System.Reflection": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Runtime/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0"
}
},
"System.Runtime.CompilerServices.Unsafe/5.0.0": {},
"System.Runtime.Extensions/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Runtime.Serialization.Primitives/4.3.0": {
"dependencies": {
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Security.AccessControl/4.5.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"System.Security.Principal.Windows": "4.5.0"
}
},
"System.Security.Cryptography.ProtectedData/4.5.0": {},
"System.Security.Permissions/4.5.0": {
"dependencies": {
"System.Security.AccessControl": "4.5.0"
}
},
"System.Security.Principal.Windows/4.5.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0"
}
},
"System.Text.Encoding/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Threading.Tasks/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Threading.Tasks.Extensions/4.5.4": {},
"System.ValueTuple/4.5.0": {}
}
},
"libraries": {
"NodeNetwork/6.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"DynamicData/7.1.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Pc6J5bFnSxEa64PV2V67FMcLlDdpv6m+zTBKSnRN3aLon/WtWWy8kuDpHFbJlgXHtqc6Nxloj9ItuvDlvKC/8w==",
"path": "dynamicdata/7.1.1",
"hashPath": "dynamicdata.7.1.1.nupkg.sha512"
},
"log4net/2.0.12": {
"type": "package",
"serviceable": true,
"sha512": "sha512-9P67BCftJ7KG+B7rNOM1A9KczUwyEDed6zbAddy5Cj/73xVkzi+rEAHeOgUnW5wDqy1JFlY8+oTP0m1PgJ03Tg==",
"path": "log4net/2.0.12",
"hashPath": "log4net.2.0.12.nupkg.sha512"
},
"Microsoft.CSharp/4.7.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==",
"path": "microsoft.csharp/4.7.0",
"hashPath": "microsoft.csharp.4.7.0.nupkg.sha512"
},
"Microsoft.NETCore.Platforms/2.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-VdLJOCXhZaEMY7Hm2GKiULmn7IEPFE4XC5LPSfBVCUIA8YLZVh846gtfBJalsPQF2PlzdD7ecX7DZEulJ402ZQ==",
"path": "microsoft.netcore.platforms/2.0.0",
"hashPath": "microsoft.netcore.platforms.2.0.0.nupkg.sha512"
},
"Microsoft.NETCore.Targets/1.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==",
"path": "microsoft.netcore.targets/1.1.0",
"hashPath": "microsoft.netcore.targets.1.1.0.nupkg.sha512"
},
"Pharmacist.Common/2.0.8": {
"type": "package",
"serviceable": true,
"sha512": "sha512-NNiYvv2oTd18ckJMBt8zatCBPj0KwbzbxMuxgu45m20VtqvkvR6AborbOhkhmJC70iaSv3uLvhTnFExIJGDnQA==",
"path": "pharmacist.common/2.0.8",
"hashPath": "pharmacist.common.2.0.8.nupkg.sha512"
},
"ReactiveUI/13.2.18": {
"type": "package",
"serviceable": true,
"sha512": "sha512-w457p8goRjJgbggK2whtgYO8U9SOT1JbMDyZOKceibWoZVuOGME4RfUc60/AsLVmfwlkI+9ve+xc2S696nJZuQ==",
"path": "reactiveui/13.2.18",
"hashPath": "reactiveui.13.2.18.nupkg.sha512"
},
"ReactiveUI.Events.WPF/13.2.18": {
"type": "package",
"serviceable": true,
"sha512": "sha512-m9mFgMdSWFAdlCEFOahD2bfDo+zV8BlLxOhTSVSEFbtiSC0loYHSbJDBZYu/nxTTYzjqHTN86PZ7T9NoRgfVGw==",
"path": "reactiveui.events.wpf/13.2.18",
"hashPath": "reactiveui.events.wpf.13.2.18.nupkg.sha512"
},
"ReactiveUI.WPF/13.2.18": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ALpQe9Y970v4d/RfFT/keb93POq1WEYwUq0bfvZchUiYG4j8GOcFe085x+D5bfwh2aCRz/hHNl+9fTFVnCRisw==",
"path": "reactiveui.wpf/13.2.18",
"hashPath": "reactiveui.wpf.13.2.18.nupkg.sha512"
},
"Splat/11.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-C1p+fZE+xgwKCqnf9Qsk0lrJYGiUplWpS2wp9/RR/YNHkjx7vBCRebau1Uej7ZXda71eJgZVBYzZq0/qsQOc3Q==",
"path": "splat/11.0.1",
"hashPath": "splat.11.0.1.nupkg.sha512"
},
"Splat.Drawing/11.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-hSho9yLfiv2+QhWqZImg01lhZZzz/6qPP3r2AQQRRJdxIoqgcgw1VqBWYC5bT+4QzcX/hAcjtpmQXmYEg2KBZw==",
"path": "splat.drawing/11.0.1",
"hashPath": "splat.drawing.11.0.1.nupkg.sha512"
},
"System.Buffers/4.5.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==",
"path": "system.buffers/4.5.1",
"hashPath": "system.buffers.4.5.1.nupkg.sha512"
},
"System.Collections.Immutable/5.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==",
"path": "system.collections.immutable/5.0.0",
"hashPath": "system.collections.immutable.5.0.0.nupkg.sha512"
},
"System.Configuration.ConfigurationManager/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-UIFvaFfuKhLr9u5tWMxmVoDPkFeD+Qv8gUuap4aZgVGYSYMdERck4OhLN/2gulAc0nYTEigWXSJNNWshrmxnng==",
"path": "system.configuration.configurationmanager/4.5.0",
"hashPath": "system.configuration.configurationmanager.4.5.0.nupkg.sha512"
},
"System.Data.DataSetExtensions/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-221clPs1445HkTBZPL+K9sDBdJRB8UN8rgjO3ztB0CQ26z//fmJXtlsr6whGatscsKGBrhJl5bwJuKSA8mwFOw==",
"path": "system.data.datasetextensions/4.5.0",
"hashPath": "system.data.datasetextensions.4.5.0.nupkg.sha512"
},
"System.Diagnostics.Contracts/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==",
"path": "system.diagnostics.contracts/4.3.0",
"hashPath": "system.diagnostics.contracts.4.3.0.nupkg.sha512"
},
"System.Drawing.Primitives/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-1QU/c35gwdhvj77fkScXQQbjiVAqIL3fEYn/19NE0CV/ic5TN5PyWAft8HsrbRd4SBLEoErNCkWSzMDc0MmbRw==",
"path": "system.drawing.primitives/4.3.0",
"hashPath": "system.drawing.primitives.4.3.0.nupkg.sha512"
},
"System.Globalization/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==",
"path": "system.globalization/4.3.0",
"hashPath": "system.globalization.4.3.0.nupkg.sha512"
},
"System.IO/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
"path": "system.io/4.3.0",
"hashPath": "system.io.4.3.0.nupkg.sha512"
},
"System.Memory/4.5.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==",
"path": "system.memory/4.5.4",
"hashPath": "system.memory.4.5.4.nupkg.sha512"
},
"System.Numerics.Vectors/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==",
"path": "system.numerics.vectors/4.5.0",
"hashPath": "system.numerics.vectors.4.5.0.nupkg.sha512"
},
"System.Reactive/5.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==",
"path": "system.reactive/5.0.0",
"hashPath": "system.reactive.5.0.0.nupkg.sha512"
},
"System.Reflection/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
"path": "system.reflection/4.3.0",
"hashPath": "system.reflection.4.3.0.nupkg.sha512"
},
"System.Reflection.Primitives/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
"path": "system.reflection.primitives/4.3.0",
"hashPath": "system.reflection.primitives.4.3.0.nupkg.sha512"
},
"System.Resources.ResourceManager/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==",
"path": "system.resources.resourcemanager/4.3.0",
"hashPath": "system.resources.resourcemanager.4.3.0.nupkg.sha512"
},
"System.Runtime/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
"path": "system.runtime/4.3.0",
"hashPath": "system.runtime.4.3.0.nupkg.sha512"
},
"System.Runtime.CompilerServices.Unsafe/5.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==",
"path": "system.runtime.compilerservices.unsafe/5.0.0",
"hashPath": "system.runtime.compilerservices.unsafe.5.0.0.nupkg.sha512"
},
"System.Runtime.Extensions/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==",
"path": "system.runtime.extensions/4.3.0",
"hashPath": "system.runtime.extensions.4.3.0.nupkg.sha512"
},
"System.Runtime.Serialization.Primitives/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==",
"path": "system.runtime.serialization.primitives/4.3.0",
"hashPath": "system.runtime.serialization.primitives.4.3.0.nupkg.sha512"
},
"System.Security.AccessControl/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==",
"path": "system.security.accesscontrol/4.5.0",
"hashPath": "system.security.accesscontrol.4.5.0.nupkg.sha512"
},
"System.Security.Cryptography.ProtectedData/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-wLBKzFnDCxP12VL9ANydSYhk59fC4cvOr9ypYQLPnAj48NQIhqnjdD2yhP8yEKyBJEjERWS9DisKL7rX5eU25Q==",
"path": "system.security.cryptography.protecteddata/4.5.0",
"hashPath": "system.security.cryptography.protecteddata.4.5.0.nupkg.sha512"
},
"System.Security.Permissions/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-9gdyuARhUR7H+p5CjyUB/zPk7/Xut3wUSP8NJQB6iZr8L3XUXTMdoLeVAg9N4rqF8oIpE7MpdqHdDHQ7XgJe0g==",
"path": "system.security.permissions/4.5.0",
"hashPath": "system.security.permissions.4.5.0.nupkg.sha512"
},
"System.Security.Principal.Windows/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-U77HfRXlZlOeIXd//Yoj6Jnk8AXlbeisf1oq1os+hxOGVnuG+lGSfGqTwTZBoORFF6j/0q7HXIl8cqwQ9aUGqQ==",
"path": "system.security.principal.windows/4.5.0",
"hashPath": "system.security.principal.windows.4.5.0.nupkg.sha512"
},
"System.Text.Encoding/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
"path": "system.text.encoding/4.3.0",
"hashPath": "system.text.encoding.4.3.0.nupkg.sha512"
},
"System.Threading.Tasks/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
"path": "system.threading.tasks/4.3.0",
"hashPath": "system.threading.tasks.4.3.0.nupkg.sha512"
},
"System.Threading.Tasks.Extensions/4.5.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
"path": "system.threading.tasks.extensions/4.5.4",
"hashPath": "system.threading.tasks.extensions.4.5.4.nupkg.sha512"
},
"System.ValueTuple/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==",
"path": "system.valuetuple/4.5.0",
"hashPath": "system.valuetuple.4.5.0.nupkg.sha512"
}
}
}

View File

@@ -0,0 +1,519 @@
{
"runtimeTarget": {
"name": ".NETCoreApp,Version=v7.0",
"signature": ""
},
"compilationOptions": {},
"targets": {
".NETCoreApp,Version=v7.0": {
"NodeNetwork/6.0.0": {
"dependencies": {
"Microsoft.CSharp": "4.7.0",
"ReactiveUI": "13.2.18",
"ReactiveUI.Events.WPF": "13.2.18",
"ReactiveUI.WPF": "13.2.18",
"Splat.Drawing": "11.0.1",
"System.Buffers": "4.5.1",
"System.Collections.Immutable": "5.0.0",
"System.Data.DataSetExtensions": "4.5.0",
"System.Drawing.Primitives": "4.3.0",
"System.Memory": "4.5.4",
"System.Numerics.Vectors": "4.5.0",
"System.Runtime.CompilerServices.Unsafe": "5.0.0",
"System.Threading.Tasks.Extensions": "4.5.4",
"System.ValueTuple": "4.5.0",
"log4net": "2.0.12"
},
"runtime": {
"NodeNetwork.dll": {}
}
},
"DynamicData/7.1.1": {
"dependencies": {
"System.Reactive": "5.0.0"
},
"runtime": {
"lib/net5.0/DynamicData.dll": {
"assemblyVersion": "7.1.0.0",
"fileVersion": "7.1.1.11487"
}
}
},
"log4net/2.0.12": {
"dependencies": {
"System.Configuration.ConfigurationManager": "4.5.0"
},
"runtime": {
"lib/netstandard2.0/log4net.dll": {
"assemblyVersion": "2.0.12.0",
"fileVersion": "2.0.12.0"
}
}
},
"Microsoft.CSharp/4.7.0": {},
"Microsoft.NETCore.Platforms/2.0.0": {},
"Microsoft.NETCore.Targets/1.1.0": {},
"Pharmacist.Common/2.0.8": {
"dependencies": {
"System.Reactive": "5.0.0"
},
"runtime": {
"lib/net5.0/Pharmacist.Common.dll": {
"assemblyVersion": "2.0.0.0",
"fileVersion": "2.0.8.15338"
}
}
},
"ReactiveUI/13.2.18": {
"dependencies": {
"DynamicData": "7.1.1",
"Splat": "11.0.1",
"System.Reactive": "5.0.0",
"System.Runtime.Serialization.Primitives": "4.3.0"
},
"runtime": {
"lib/net5.0-windows7.0/ReactiveUI.dll": {
"assemblyVersion": "13.2.0.0",
"fileVersion": "13.2.18.42750"
}
}
},
"ReactiveUI.Events.WPF/13.2.18": {
"dependencies": {
"Pharmacist.Common": "2.0.8",
"System.Reactive": "5.0.0"
},
"runtime": {
"lib/net5.0-windows7.0/ReactiveUI.Events.WPF.dll": {
"assemblyVersion": "13.2.0.0",
"fileVersion": "13.2.18.42750"
}
}
},
"ReactiveUI.WPF/13.2.18": {
"dependencies": {
"ReactiveUI": "13.2.18",
"System.Reactive": "5.0.0"
},
"runtime": {
"lib/net5.0-windows7.0/ReactiveUI.Wpf.dll": {
"assemblyVersion": "13.2.0.0",
"fileVersion": "13.2.18.42750"
}
}
},
"Splat/11.0.1": {
"runtime": {
"lib/net5.0/Splat.dll": {
"assemblyVersion": "11.0.0.0",
"fileVersion": "11.0.1.61461"
}
}
},
"Splat.Drawing/11.0.1": {
"dependencies": {
"Splat": "11.0.1",
"System.Diagnostics.Contracts": "4.3.0",
"System.Drawing.Primitives": "4.3.0",
"System.Runtime.Serialization.Primitives": "4.3.0"
},
"runtime": {
"lib/net5.0-windows7.0/Splat.Drawing.dll": {
"assemblyVersion": "11.0.0.0",
"fileVersion": "11.0.1.61461"
}
}
},
"System.Buffers/4.5.1": {},
"System.Collections.Immutable/5.0.0": {},
"System.Configuration.ConfigurationManager/4.5.0": {
"dependencies": {
"System.Security.Cryptography.ProtectedData": "4.5.0",
"System.Security.Permissions": "4.5.0"
}
},
"System.Data.DataSetExtensions/4.5.0": {},
"System.Diagnostics.Contracts/4.3.0": {
"dependencies": {
"System.Runtime": "4.3.0"
}
},
"System.Drawing.Primitives/4.3.0": {
"dependencies": {
"System.Runtime": "4.3.0",
"System.Runtime.Extensions": "4.3.0"
}
},
"System.Globalization/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.IO/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0",
"System.Text.Encoding": "4.3.0",
"System.Threading.Tasks": "4.3.0"
}
},
"System.Memory/4.5.4": {},
"System.Numerics.Vectors/4.5.0": {},
"System.Reactive/5.0.0": {
"runtime": {
"lib/net5.0/System.Reactive.dll": {
"assemblyVersion": "5.0.0.0",
"fileVersion": "5.0.0.1"
}
}
},
"System.Reflection/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.IO": "4.3.0",
"System.Reflection.Primitives": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Reflection.Primitives/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Resources.ResourceManager/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Globalization": "4.3.0",
"System.Reflection": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Runtime/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0"
}
},
"System.Runtime.CompilerServices.Unsafe/5.0.0": {},
"System.Runtime.Extensions/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Runtime.Serialization.Primitives/4.3.0": {
"dependencies": {
"System.Resources.ResourceManager": "4.3.0",
"System.Runtime": "4.3.0"
}
},
"System.Security.AccessControl/4.5.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"System.Security.Principal.Windows": "4.5.0"
}
},
"System.Security.Cryptography.ProtectedData/4.5.0": {},
"System.Security.Permissions/4.5.0": {
"dependencies": {
"System.Security.AccessControl": "4.5.0"
}
},
"System.Security.Principal.Windows/4.5.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0"
}
},
"System.Text.Encoding/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Threading.Tasks/4.3.0": {
"dependencies": {
"Microsoft.NETCore.Platforms": "2.0.0",
"Microsoft.NETCore.Targets": "1.1.0",
"System.Runtime": "4.3.0"
}
},
"System.Threading.Tasks.Extensions/4.5.4": {},
"System.ValueTuple/4.5.0": {}
}
},
"libraries": {
"NodeNetwork/6.0.0": {
"type": "project",
"serviceable": false,
"sha512": ""
},
"DynamicData/7.1.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Pc6J5bFnSxEa64PV2V67FMcLlDdpv6m+zTBKSnRN3aLon/WtWWy8kuDpHFbJlgXHtqc6Nxloj9ItuvDlvKC/8w==",
"path": "dynamicdata/7.1.1",
"hashPath": "dynamicdata.7.1.1.nupkg.sha512"
},
"log4net/2.0.12": {
"type": "package",
"serviceable": true,
"sha512": "sha512-9P67BCftJ7KG+B7rNOM1A9KczUwyEDed6zbAddy5Cj/73xVkzi+rEAHeOgUnW5wDqy1JFlY8+oTP0m1PgJ03Tg==",
"path": "log4net/2.0.12",
"hashPath": "log4net.2.0.12.nupkg.sha512"
},
"Microsoft.CSharp/4.7.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA==",
"path": "microsoft.csharp/4.7.0",
"hashPath": "microsoft.csharp.4.7.0.nupkg.sha512"
},
"Microsoft.NETCore.Platforms/2.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-VdLJOCXhZaEMY7Hm2GKiULmn7IEPFE4XC5LPSfBVCUIA8YLZVh846gtfBJalsPQF2PlzdD7ecX7DZEulJ402ZQ==",
"path": "microsoft.netcore.platforms/2.0.0",
"hashPath": "microsoft.netcore.platforms.2.0.0.nupkg.sha512"
},
"Microsoft.NETCore.Targets/1.1.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-aOZA3BWfz9RXjpzt0sRJJMjAscAUm3Hoa4UWAfceV9UTYxgwZ1lZt5nO2myFf+/jetYQo4uTP7zS8sJY67BBxg==",
"path": "microsoft.netcore.targets/1.1.0",
"hashPath": "microsoft.netcore.targets.1.1.0.nupkg.sha512"
},
"Pharmacist.Common/2.0.8": {
"type": "package",
"serviceable": true,
"sha512": "sha512-NNiYvv2oTd18ckJMBt8zatCBPj0KwbzbxMuxgu45m20VtqvkvR6AborbOhkhmJC70iaSv3uLvhTnFExIJGDnQA==",
"path": "pharmacist.common/2.0.8",
"hashPath": "pharmacist.common.2.0.8.nupkg.sha512"
},
"ReactiveUI/13.2.18": {
"type": "package",
"serviceable": true,
"sha512": "sha512-w457p8goRjJgbggK2whtgYO8U9SOT1JbMDyZOKceibWoZVuOGME4RfUc60/AsLVmfwlkI+9ve+xc2S696nJZuQ==",
"path": "reactiveui/13.2.18",
"hashPath": "reactiveui.13.2.18.nupkg.sha512"
},
"ReactiveUI.Events.WPF/13.2.18": {
"type": "package",
"serviceable": true,
"sha512": "sha512-m9mFgMdSWFAdlCEFOahD2bfDo+zV8BlLxOhTSVSEFbtiSC0loYHSbJDBZYu/nxTTYzjqHTN86PZ7T9NoRgfVGw==",
"path": "reactiveui.events.wpf/13.2.18",
"hashPath": "reactiveui.events.wpf.13.2.18.nupkg.sha512"
},
"ReactiveUI.WPF/13.2.18": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ALpQe9Y970v4d/RfFT/keb93POq1WEYwUq0bfvZchUiYG4j8GOcFe085x+D5bfwh2aCRz/hHNl+9fTFVnCRisw==",
"path": "reactiveui.wpf/13.2.18",
"hashPath": "reactiveui.wpf.13.2.18.nupkg.sha512"
},
"Splat/11.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-C1p+fZE+xgwKCqnf9Qsk0lrJYGiUplWpS2wp9/RR/YNHkjx7vBCRebau1Uej7ZXda71eJgZVBYzZq0/qsQOc3Q==",
"path": "splat/11.0.1",
"hashPath": "splat.11.0.1.nupkg.sha512"
},
"Splat.Drawing/11.0.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-hSho9yLfiv2+QhWqZImg01lhZZzz/6qPP3r2AQQRRJdxIoqgcgw1VqBWYC5bT+4QzcX/hAcjtpmQXmYEg2KBZw==",
"path": "splat.drawing/11.0.1",
"hashPath": "splat.drawing.11.0.1.nupkg.sha512"
},
"System.Buffers/4.5.1": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Rw7ijyl1qqRS0YQD/WycNst8hUUMgrMH4FCn1nNm27M4VxchZ1js3fVjQaANHO5f3sN4isvP4a+Met9Y4YomAg==",
"path": "system.buffers/4.5.1",
"hashPath": "system.buffers.4.5.1.nupkg.sha512"
},
"System.Collections.Immutable/5.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-FXkLXiK0sVVewcso0imKQoOxjoPAj42R8HtjjbSjVPAzwDfzoyoznWxgA3c38LDbN9SJux1xXoXYAhz98j7r2g==",
"path": "system.collections.immutable/5.0.0",
"hashPath": "system.collections.immutable.5.0.0.nupkg.sha512"
},
"System.Configuration.ConfigurationManager/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-UIFvaFfuKhLr9u5tWMxmVoDPkFeD+Qv8gUuap4aZgVGYSYMdERck4OhLN/2gulAc0nYTEigWXSJNNWshrmxnng==",
"path": "system.configuration.configurationmanager/4.5.0",
"hashPath": "system.configuration.configurationmanager.4.5.0.nupkg.sha512"
},
"System.Data.DataSetExtensions/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-221clPs1445HkTBZPL+K9sDBdJRB8UN8rgjO3ztB0CQ26z//fmJXtlsr6whGatscsKGBrhJl5bwJuKSA8mwFOw==",
"path": "system.data.datasetextensions/4.5.0",
"hashPath": "system.data.datasetextensions.4.5.0.nupkg.sha512"
},
"System.Diagnostics.Contracts/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-eelRRbnm+OloiQvp9CXS0ixjNQldjjkHO4iIkR5XH2VIP8sUB/SIpa1TdUW6/+HDcQ+MlhP3pNa1u5SbzYuWGA==",
"path": "system.diagnostics.contracts/4.3.0",
"hashPath": "system.diagnostics.contracts.4.3.0.nupkg.sha512"
},
"System.Drawing.Primitives/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-1QU/c35gwdhvj77fkScXQQbjiVAqIL3fEYn/19NE0CV/ic5TN5PyWAft8HsrbRd4SBLEoErNCkWSzMDc0MmbRw==",
"path": "system.drawing.primitives/4.3.0",
"hashPath": "system.drawing.primitives.4.3.0.nupkg.sha512"
},
"System.Globalization/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-kYdVd2f2PAdFGblzFswE4hkNANJBKRmsfa2X5LG2AcWE1c7/4t0pYae1L8vfZ5xvE2nK/R9JprtToA61OSHWIg==",
"path": "system.globalization/4.3.0",
"hashPath": "system.globalization.4.3.0.nupkg.sha512"
},
"System.IO/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-3qjaHvxQPDpSOYICjUoTsmoq5u6QJAFRUITgeT/4gqkF1bajbSmb1kwSxEA8AHlofqgcKJcM8udgieRNhaJ5Cg==",
"path": "system.io/4.3.0",
"hashPath": "system.io.4.3.0.nupkg.sha512"
},
"System.Memory/4.5.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-1MbJTHS1lZ4bS4FmsJjnuGJOu88ZzTT2rLvrhW7Ygic+pC0NWA+3hgAen0HRdsocuQXCkUTdFn9yHJJhsijDXw==",
"path": "system.memory/4.5.4",
"hashPath": "system.memory.4.5.4.nupkg.sha512"
},
"System.Numerics.Vectors/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-QQTlPTl06J/iiDbJCiepZ4H//BVraReU4O4EoRw1U02H5TLUIT7xn3GnDp9AXPSlJUDyFs4uWjWafNX6WrAojQ==",
"path": "system.numerics.vectors/4.5.0",
"hashPath": "system.numerics.vectors.4.5.0.nupkg.sha512"
},
"System.Reactive/5.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ==",
"path": "system.reactive/5.0.0",
"hashPath": "system.reactive.5.0.0.nupkg.sha512"
},
"System.Reflection/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-KMiAFoW7MfJGa9nDFNcfu+FpEdiHpWgTcS2HdMpDvt9saK3y/G4GwprPyzqjFH9NTaGPQeWNHU+iDlDILj96aQ==",
"path": "system.reflection/4.3.0",
"hashPath": "system.reflection.4.3.0.nupkg.sha512"
},
"System.Reflection.Primitives/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-5RXItQz5As4xN2/YUDxdpsEkMhvw3e6aNveFXUn4Hl/udNTCNhnKp8lT9fnc3MhvGKh1baak5CovpuQUXHAlIA==",
"path": "system.reflection.primitives/4.3.0",
"hashPath": "system.reflection.primitives.4.3.0.nupkg.sha512"
},
"System.Resources.ResourceManager/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-/zrcPkkWdZmI4F92gL/TPumP98AVDu/Wxr3CSJGQQ+XN6wbRZcyfSKVoPo17ilb3iOr0cCRqJInGwNMolqhS8A==",
"path": "system.resources.resourcemanager/4.3.0",
"hashPath": "system.resources.resourcemanager.4.3.0.nupkg.sha512"
},
"System.Runtime/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-JufQi0vPQ0xGnAczR13AUFglDyVYt4Kqnz1AZaiKZ5+GICq0/1MH/mO/eAJHt/mHW1zjKBJd7kV26SrxddAhiw==",
"path": "system.runtime/4.3.0",
"hashPath": "system.runtime.4.3.0.nupkg.sha512"
},
"System.Runtime.CompilerServices.Unsafe/5.0.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-ZD9TMpsmYJLrxbbmdvhwt9YEgG5WntEnZ/d1eH8JBX9LBp+Ju8BSBhUGbZMNVHHomWo2KVImJhTDl2hIgw/6MA==",
"path": "system.runtime.compilerservices.unsafe/5.0.0",
"hashPath": "system.runtime.compilerservices.unsafe.5.0.0.nupkg.sha512"
},
"System.Runtime.Extensions/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-guW0uK0fn5fcJJ1tJVXYd7/1h5F+pea1r7FLSOz/f8vPEqbR2ZAknuRDvTQ8PzAilDveOxNjSfr0CHfIQfFk8g==",
"path": "system.runtime.extensions/4.3.0",
"hashPath": "system.runtime.extensions.4.3.0.nupkg.sha512"
},
"System.Runtime.Serialization.Primitives/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-Wz+0KOukJGAlXjtKr+5Xpuxf8+c8739RI1C+A2BoQZT+wMCCoMDDdO8/4IRHfaVINqL78GO8dW8G2lW/e45Mcw==",
"path": "system.runtime.serialization.primitives/4.3.0",
"hashPath": "system.runtime.serialization.primitives.4.3.0.nupkg.sha512"
},
"System.Security.AccessControl/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-vW8Eoq0TMyz5vAG/6ce483x/CP83fgm4SJe5P8Tb1tZaobcvPrbMEL7rhH1DRdrYbbb6F0vq3OlzmK0Pkwks5A==",
"path": "system.security.accesscontrol/4.5.0",
"hashPath": "system.security.accesscontrol.4.5.0.nupkg.sha512"
},
"System.Security.Cryptography.ProtectedData/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-wLBKzFnDCxP12VL9ANydSYhk59fC4cvOr9ypYQLPnAj48NQIhqnjdD2yhP8yEKyBJEjERWS9DisKL7rX5eU25Q==",
"path": "system.security.cryptography.protecteddata/4.5.0",
"hashPath": "system.security.cryptography.protecteddata.4.5.0.nupkg.sha512"
},
"System.Security.Permissions/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-9gdyuARhUR7H+p5CjyUB/zPk7/Xut3wUSP8NJQB6iZr8L3XUXTMdoLeVAg9N4rqF8oIpE7MpdqHdDHQ7XgJe0g==",
"path": "system.security.permissions/4.5.0",
"hashPath": "system.security.permissions.4.5.0.nupkg.sha512"
},
"System.Security.Principal.Windows/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-U77HfRXlZlOeIXd//Yoj6Jnk8AXlbeisf1oq1os+hxOGVnuG+lGSfGqTwTZBoORFF6j/0q7HXIl8cqwQ9aUGqQ==",
"path": "system.security.principal.windows/4.5.0",
"hashPath": "system.security.principal.windows.4.5.0.nupkg.sha512"
},
"System.Text.Encoding/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-BiIg+KWaSDOITze6jGQynxg64naAPtqGHBwDrLaCtixsa5bKiR8dpPOHA7ge3C0JJQizJE+sfkz1wV+BAKAYZw==",
"path": "system.text.encoding/4.3.0",
"hashPath": "system.text.encoding.4.3.0.nupkg.sha512"
},
"System.Threading.Tasks/4.3.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-LbSxKEdOUhVe8BezB/9uOGGppt+nZf6e1VFyw6v3DN6lqitm0OSn2uXMOdtP0M3W4iMcqcivm2J6UgqiwwnXiA==",
"path": "system.threading.tasks/4.3.0",
"hashPath": "system.threading.tasks.4.3.0.nupkg.sha512"
},
"System.Threading.Tasks.Extensions/4.5.4": {
"type": "package",
"serviceable": true,
"sha512": "sha512-zteT+G8xuGu6mS+mzDzYXbzS7rd3K6Fjb9RiZlYlJPam2/hU7JCBZBVEcywNuR+oZ1ncTvc/cq0faRr3P01OVg==",
"path": "system.threading.tasks.extensions/4.5.4",
"hashPath": "system.threading.tasks.extensions.4.5.4.nupkg.sha512"
},
"System.ValueTuple/4.5.0": {
"type": "package",
"serviceable": true,
"sha512": "sha512-okurQJO6NRE/apDIP23ajJ0hpiNmJ+f0BwOlB/cSqTLQlw5upkf+5+96+iG2Jw40G1fCVCyPz/FhIABUjMR+RQ==",
"path": "system.valuetuple/4.5.0",
"hashPath": "system.valuetuple.4.5.0.nupkg.sha512"
}
}
}

View File

@@ -0,0 +1,62 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>NodeNetwork</id>
<version>6.0.0</version>
<authors>Wouter De Keersmaecker</authors>
<license type="expression">Apache-2.0</license>
<licenseUrl>https://licenses.nuget.org/Apache-2.0</licenseUrl>
<description>Package Description</description>
<tags>wpf reactiveui node network editor node-editor graph</tags>
<repository type="Github" url="https://www.github.com/wouterdek/nodenetwork" />
<dependencies>
<group targetFramework=".NETFramework4.8">
<dependency id="Microsoft.CSharp" version="4.7.0" exclude="Build,Analyzers" />
<dependency id="ReactiveUI" version="13.2.18" exclude="Build,Analyzers" />
<dependency id="ReactiveUI.Events.WPF" version="13.2.18" exclude="Build,Analyzers" />
<dependency id="ReactiveUI.WPF" version="13.2.18" exclude="Build,Analyzers" />
<dependency id="Splat.Drawing" version="11.0.1" exclude="Build,Analyzers" />
<dependency id="System.Buffers" version="4.5.1" exclude="Build,Analyzers" />
<dependency id="System.Collections.Immutable" version="5.0.0" exclude="Build,Analyzers" />
<dependency id="System.Data.DataSetExtensions" version="4.5.0" exclude="Build,Analyzers" />
<dependency id="System.Drawing.Primitives" version="4.3.0" exclude="Build,Analyzers" />
<dependency id="System.Memory" version="4.5.4" exclude="Build,Analyzers" />
<dependency id="System.Numerics.Vectors" version="4.5.0" exclude="Build,Analyzers" />
<dependency id="System.Runtime.CompilerServices.Unsafe" version="5.0.0" exclude="Build,Analyzers" />
<dependency id="System.Threading.Tasks.Extensions" version="4.5.4" exclude="Build,Analyzers" />
<dependency id="System.ValueTuple" version="4.5.0" exclude="Build,Analyzers" />
<dependency id="log4net" version="2.0.12" exclude="Build,Analyzers" />
</group>
<group targetFramework="net7.0-windows7.0">
<dependency id="Microsoft.CSharp" version="4.7.0" exclude="Build,Analyzers" />
<dependency id="ReactiveUI" version="13.2.18" exclude="Build,Analyzers" />
<dependency id="ReactiveUI.Events.WPF" version="13.2.18" exclude="Build,Analyzers" />
<dependency id="ReactiveUI.WPF" version="13.2.18" exclude="Build,Analyzers" />
<dependency id="Splat.Drawing" version="11.0.1" exclude="Build,Analyzers" />
<dependency id="System.Buffers" version="4.5.1" exclude="Build,Analyzers" />
<dependency id="System.Collections.Immutable" version="5.0.0" exclude="Build,Analyzers" />
<dependency id="System.Data.DataSetExtensions" version="4.5.0" exclude="Build,Analyzers" />
<dependency id="System.Drawing.Primitives" version="4.3.0" exclude="Build,Analyzers" />
<dependency id="System.Memory" version="4.5.4" exclude="Build,Analyzers" />
<dependency id="System.Numerics.Vectors" version="4.5.0" exclude="Build,Analyzers" />
<dependency id="System.Runtime.CompilerServices.Unsafe" version="5.0.0" exclude="Build,Analyzers" />
<dependency id="System.Threading.Tasks.Extensions" version="4.5.4" exclude="Build,Analyzers" />
<dependency id="System.ValueTuple" version="4.5.0" exclude="Build,Analyzers" />
<dependency id="log4net" version="2.0.12" exclude="Build,Analyzers" />
</group>
</dependencies>
<frameworkReferences>
<group targetFramework="net7.0-windows7.0">
<frameworkReference name="Microsoft.WindowsDesktop.App.WPF" />
</group>
<group targetFramework=".NETFramework4.8" />
</frameworkReferences>
<frameworkAssemblies>
<frameworkAssembly assemblyName="System.Drawing.Primitives" targetFramework=".NETFramework4.8" />
</frameworkAssemblies>
</metadata>
<files>
<file src="E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\NodeNetwork.dll" target="lib\net48\NodeNetwork.dll" />
<file src="E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net7.0-windows7.0\NodeNetwork.dll" target="lib\net7.0-windows7.0\NodeNetwork.dll" />
</files>
</package>

View File

@@ -0,0 +1,64 @@
<?xml version="1.0" encoding="utf-8"?>
<package xmlns="http://schemas.microsoft.com/packaging/2012/06/nuspec.xsd">
<metadata>
<id>NodeNetwork</id>
<version>6.0.0</version>
<authors>Wouter De Keersmaecker</authors>
<license type="expression">Apache-2.0</license>
<licenseUrl>https://licenses.nuget.org/Apache-2.0</licenseUrl>
<description>Package Description</description>
<tags>wpf reactiveui node network editor node-editor graph</tags>
<repository type="Github" url="https://www.github.com/wouterdek/nodenetwork" />
<dependencies>
<group targetFramework=".NETFramework4.8">
<dependency id="Microsoft.CSharp" version="4.7.0" exclude="Build,Analyzers" />
<dependency id="ReactiveUI" version="13.2.18" exclude="Build,Analyzers" />
<dependency id="ReactiveUI.Events.WPF" version="13.2.18" exclude="Build,Analyzers" />
<dependency id="ReactiveUI.WPF" version="13.2.18" exclude="Build,Analyzers" />
<dependency id="Splat.Drawing" version="11.0.1" exclude="Build,Analyzers" />
<dependency id="System.Buffers" version="4.5.1" exclude="Build,Analyzers" />
<dependency id="System.Collections.Immutable" version="5.0.0" exclude="Build,Analyzers" />
<dependency id="System.Data.DataSetExtensions" version="4.5.0" exclude="Build,Analyzers" />
<dependency id="System.Drawing.Primitives" version="4.3.0" exclude="Build,Analyzers" />
<dependency id="System.Memory" version="4.5.4" exclude="Build,Analyzers" />
<dependency id="System.Numerics.Vectors" version="4.5.0" exclude="Build,Analyzers" />
<dependency id="System.Runtime.CompilerServices.Unsafe" version="5.0.0" exclude="Build,Analyzers" />
<dependency id="System.Threading.Tasks.Extensions" version="4.5.4" exclude="Build,Analyzers" />
<dependency id="System.ValueTuple" version="4.5.0" exclude="Build,Analyzers" />
<dependency id="log4net" version="2.0.12" exclude="Build,Analyzers" />
</group>
<group targetFramework="net7.0-windows7.0">
<dependency id="Microsoft.CSharp" version="4.7.0" exclude="Build,Analyzers" />
<dependency id="ReactiveUI" version="13.2.18" exclude="Build,Analyzers" />
<dependency id="ReactiveUI.Events.WPF" version="13.2.18" exclude="Build,Analyzers" />
<dependency id="ReactiveUI.WPF" version="13.2.18" exclude="Build,Analyzers" />
<dependency id="Splat.Drawing" version="11.0.1" exclude="Build,Analyzers" />
<dependency id="System.Buffers" version="4.5.1" exclude="Build,Analyzers" />
<dependency id="System.Collections.Immutable" version="5.0.0" exclude="Build,Analyzers" />
<dependency id="System.Data.DataSetExtensions" version="4.5.0" exclude="Build,Analyzers" />
<dependency id="System.Drawing.Primitives" version="4.3.0" exclude="Build,Analyzers" />
<dependency id="System.Memory" version="4.5.4" exclude="Build,Analyzers" />
<dependency id="System.Numerics.Vectors" version="4.5.0" exclude="Build,Analyzers" />
<dependency id="System.Runtime.CompilerServices.Unsafe" version="5.0.0" exclude="Build,Analyzers" />
<dependency id="System.Threading.Tasks.Extensions" version="4.5.4" exclude="Build,Analyzers" />
<dependency id="System.ValueTuple" version="4.5.0" exclude="Build,Analyzers" />
<dependency id="log4net" version="2.0.12" exclude="Build,Analyzers" />
</group>
</dependencies>
<frameworkReferences>
<group targetFramework="net7.0-windows7.0">
<frameworkReference name="Microsoft.WindowsDesktop.App.WPF" />
</group>
<group targetFramework=".NETFramework4.8" />
</frameworkReferences>
<frameworkAssemblies>
<frameworkAssembly assemblyName="System.Drawing.Primitives" targetFramework=".NETFramework4.8" />
</frameworkAssemblies>
</metadata>
<files>
<file src="E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\NodeNetwork.pdb" target="lib\net48\NodeNetwork.pdb" />
<file src="E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net7.0-windows7.0\NodeNetwork.pdb" target="lib\net7.0-windows7.0\NodeNetwork.pdb" />
<file src="E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\NodeNetwork.dll" target="lib\net48\NodeNetwork.dll" />
<file src="E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net7.0-windows7.0\NodeNetwork.dll" target="lib\net7.0-windows7.0\NodeNetwork.dll" />
</files>
</package>

View File

@@ -0,0 +1,4 @@
// <autogenerated />
using System;
using System.Reflection;
[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETFramework,Version=v4.8", FrameworkDisplayName = ".NET Framework 4.8")]

View File

@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace XamlGeneratedNamespace {
/// <summary>
/// GeneratedInternalTypeHelper
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "7.0.3.0")]
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
public sealed class GeneratedInternalTypeHelper : System.Windows.Markup.InternalTypeHelper {
/// <summary>
/// CreateInstance
/// </summary>
protected override object CreateInstance(System.Type type, System.Globalization.CultureInfo culture) {
return System.Activator.CreateInstance(type, ((System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic)
| (System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.CreateInstance)), null, null, culture);
}
/// <summary>
/// GetPropertyValue
/// </summary>
protected override object GetPropertyValue(System.Reflection.PropertyInfo propertyInfo, object target, System.Globalization.CultureInfo culture) {
return propertyInfo.GetValue(target, System.Reflection.BindingFlags.Default, null, null, culture);
}
/// <summary>
/// SetPropertyValue
/// </summary>
protected override void SetPropertyValue(System.Reflection.PropertyInfo propertyInfo, object target, object value, System.Globalization.CultureInfo culture) {
propertyInfo.SetValue(target, value, System.Reflection.BindingFlags.Default, null, null, culture);
}
/// <summary>
/// CreateDelegate
/// </summary>
protected override System.Delegate CreateDelegate(System.Type delegateType, object target, string handler) {
return ((System.Delegate)(target.GetType().InvokeMember("_CreateDelegate", (System.Reflection.BindingFlags.InvokeMethod
| (System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)), null, target, new object[] {
delegateType,
handler}, null)));
}
/// <summary>
/// AddEventHandler
/// </summary>
protected override void AddEventHandler(System.Reflection.EventInfo eventInfo, object target, System.Delegate handler) {
eventInfo.AddEventHandler(target, handler);
}
}
}

View File

@@ -0,0 +1,62 @@
//------------------------------------------------------------------------------
// <auto-generated>
// This code was generated by a tool.
// Runtime Version:4.0.30319.42000
//
// Changes to this file may cause incorrect behavior and will be lost if
// the code is regenerated.
// </auto-generated>
//------------------------------------------------------------------------------
namespace XamlGeneratedNamespace {
/// <summary>
/// GeneratedInternalTypeHelper
/// </summary>
[System.Diagnostics.DebuggerNonUserCodeAttribute()]
[System.CodeDom.Compiler.GeneratedCodeAttribute("PresentationBuildTasks", "7.0.3.0")]
[System.ComponentModel.EditorBrowsableAttribute(System.ComponentModel.EditorBrowsableState.Never)]
public sealed class GeneratedInternalTypeHelper : System.Windows.Markup.InternalTypeHelper {
/// <summary>
/// CreateInstance
/// </summary>
protected override object CreateInstance(System.Type type, System.Globalization.CultureInfo culture) {
return System.Activator.CreateInstance(type, ((System.Reflection.BindingFlags.Public | System.Reflection.BindingFlags.NonPublic)
| (System.Reflection.BindingFlags.Instance | System.Reflection.BindingFlags.CreateInstance)), null, null, culture);
}
/// <summary>
/// GetPropertyValue
/// </summary>
protected override object GetPropertyValue(System.Reflection.PropertyInfo propertyInfo, object target, System.Globalization.CultureInfo culture) {
return propertyInfo.GetValue(target, System.Reflection.BindingFlags.Default, null, null, culture);
}
/// <summary>
/// SetPropertyValue
/// </summary>
protected override void SetPropertyValue(System.Reflection.PropertyInfo propertyInfo, object target, object value, System.Globalization.CultureInfo culture) {
propertyInfo.SetValue(target, value, System.Reflection.BindingFlags.Default, null, null, culture);
}
/// <summary>
/// CreateDelegate
/// </summary>
protected override System.Delegate CreateDelegate(System.Type delegateType, object target, string handler) {
return ((System.Delegate)(target.GetType().InvokeMember("_CreateDelegate", (System.Reflection.BindingFlags.InvokeMethod
| (System.Reflection.BindingFlags.NonPublic | System.Reflection.BindingFlags.Instance)), null, target, new object[] {
delegateType,
handler}, null)));
}
/// <summary>
/// AddEventHandler
/// </summary>
protected override void AddEventHandler(System.Reflection.EventInfo eventInfo, object target, System.Delegate handler) {
eventInfo.AddEventHandler(target, handler);
}
}
}

View File

@@ -0,0 +1,3 @@
is_global = true
build_property.RootNamespace = NodeNetwork
build_property.ProjectDir = E:\p4\bluflame\intromat\NodeNetwork\

View File

@@ -0,0 +1 @@
1b698fd39048a6f5ebb757d661607b83d59dea23

View File

@@ -0,0 +1,45 @@
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\NodeNetwork.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\NodeNetwork.pdb
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\DynamicData.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\log4net.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\Pharmacist.Common.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\ReactiveUI.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\ReactiveUI.Events.WPF.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\ReactiveUI.Wpf.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\Splat.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\Splat.Drawing.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\System.Buffers.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\System.Collections.Immutable.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\System.Memory.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\System.Numerics.Vectors.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\System.Reactive.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\System.Runtime.CompilerServices.Unsafe.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\System.Threading.Tasks.Extensions.dll
E:\p4\bluflame\intromat\NodeNetwork\bin\Debug\net48\System.ValueTuple.dll
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Themes\Generic.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Views\Controls\ArrowToggleButton.g.cs
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Views\Controls\ViewModelViewHostNoAnimations.g.cs
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Views\NetworkView.g.cs
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\GeneratedInternalTypeHelper.g.cs
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\NodeNetwork_MarkupCompile.cache
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\NodeNetwork_MarkupCompile.lref
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Themes\ConnectionView.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Themes\Endpoint.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Themes\EndpointGroupView.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Themes\ErrorMessageView.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Themes\NodeEndpointEditorView.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Themes\NodeInputView.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Themes\NodeOutputView.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Themes\NodeView.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Themes\PendingConnectionView.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Themes\PortView.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Views\Controls\ArrowToggleButton.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Views\Controls\ViewModelViewHostNoAnimations.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\Views\NetworkView.baml
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\NodeNetwork.g.resources
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\NodeNetwork.csproj.CoreCompileInputs.cache
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\NodeNetwork.csproj.CopyComplete
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\NodeNetwork.dll
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\NodeNetwork.pdb
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\NodeNetwork.csproj.AssemblyReference.cache
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\NodeNetwork.GeneratedMSBuildEditorConfig.editorconfig

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,3 @@
is_global = true
build_property.RootNamespace = NodeNetwork_2v5vgb3o_wpftmp
build_property.ProjectDir = E:\p4\bluflame\intromat\NodeNetwork\

View File

@@ -0,0 +1,20 @@
NodeNetwork
library
C#
.cs
E:\p4\bluflame\intromat\NodeNetwork\obj\Debug\net48\
NodeNetwork
none
false
TRACE;DEBUG;NETFRAMEWORK;NET48;
14-663422403
39-781031003
431661498142
Themes\ConnectionView.xaml;Themes\Endpoint.xaml;Themes\EndpointGroupView.xaml;Themes\ErrorMessageView.xaml;Themes\Generic.xaml;Themes\NodeEndpointEditorView.xaml;Themes\NodeInputView.xaml;Themes\NodeOutputView.xaml;Themes\NodeView.xaml;Themes\PendingConnectionView.xaml;Themes\PortView.xaml;Views\Controls\ArrowToggleButton.xaml;Views\Controls\ViewModelViewHostNoAnimations.xaml;Views\NetworkView.xaml;
False

Some files were not shown because too many files have changed in this diff Show More