Add project files.

This commit is contained in:
Ankitkumar Satapara
2026-04-17 22:31:58 +05:30
commit 21aaef6776
473 changed files with 50152 additions and 0 deletions

View File

@@ -0,0 +1,53 @@
<Application x:Class="Nodify.Playground.App"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:nodify="https://miroiu.github.io/nodify"
StartupUri="MainWindow.xaml">
<Application.Resources>
<ResourceDictionary>
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary>
<Style x:Key="{x:Static SystemParameters.FocusVisualStyleKey}">
<Setter Property="Control.Template">
<Setter.Value>
<ControlTemplate>
<Rectangle StrokeThickness="1"
StrokeDashArray="2"
Margin="-2"
RadiusX="3"
RadiusY="3"
Stroke="DodgerBlue" />
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/Nodify.xaml" />
<ResourceDictionary Source="pack://application:,,,/Nodify;component/Themes/FocusVisual.xaml" />
<ResourceDictionary Source="pack://application:,,,/Nodify.Shared;component/Themes/Icons.xaml" />
<ResourceDictionary Source="pack://application:,,,/Nodify.Shared;component/Themes/Nodify.xaml" />
<ResourceDictionary Source="pack://application:,,,/Nodify.Playground;component/Themes/Nodify.xaml" />
<ResourceDictionary>
<Color x:Key="NodifyEditor.FocusVisualColor">DodgerBlue</Color>
<Style TargetType="{x:Type nodify:HotKeyControl}"
BasedOn="{StaticResource {x:Type nodify:HotKeyControl}}">
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type nodify:HotKeyControl}">
<Border CornerRadius="3"
Background="OrangeRed">
<TextBlock Text="{Binding Number, RelativeSource={RelativeSource TemplatedParent}}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
Foreground="White" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,17 @@
using System;
using System.Collections.Generic;
using System.Configuration;
using System.Data;
using System.Linq;
using System.Threading.Tasks;
using System.Windows;
namespace Nodify.Playground
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
}
}

View File

@@ -0,0 +1,10 @@
using System.Windows;
[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)
)]

View File

@@ -0,0 +1,41 @@
using System;
namespace Nodify.Playground
{
public class BaseSettingViewModel<T> : ObservableObject, ISettingViewModel
{
public string Name { get; }
public string? Description { get; }
private object? _value;
object? ISettingViewModel.Value
{
get => _value;
set => SetProperty(ref _value, value);
}
public SettingsType Type { get;}
public T Value
{
get => (T)((ISettingViewModel)this).Value!;
set => ((ISettingViewModel)this).Value = value;
}
public BaseSettingViewModel(string name, string? description = default)
{
Name = name;
Description = description;
Type = typeof(T) switch
{
{ } t when t == typeof(string) => SettingsType.Text,
{ } t when t == typeof(bool) => SettingsType.Boolean,
{ } t when t == typeof(uint) || t == typeof(double) => SettingsType.Number,
{ } t when t == typeof(PointEditor) => SettingsType.Point,
{ IsEnum: true } => SettingsType.Option,
_ => throw new InvalidOperationException($"Type {typeof(T).Name} does not have a matching {nameof(SettingsType)}.")
};
}
}
}

View File

@@ -0,0 +1,53 @@
using System;
using System.Globalization;
using System.Windows.Controls;
using System.Windows.Data;
namespace Nodify.Playground
{
public class FlowToConnectorPositionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is ConnectionViewModel connection)
{
var connector = parameter is "Input" ? connection.Input : connection.Output;
if (connector.Node is KnotNodeViewModel)
{
var otherConnector = connection.Input == connector ? connection.Output : connection.Input;
if (otherConnector.Node is KnotNodeViewModel)
{
return ToPosition(connector == connection.Input ? ConnectorFlow.Input : ConnectorFlow.Output, connector.Node.Orientation);
}
return ToPosition(otherConnector.Flow == ConnectorFlow.Output ? ConnectorFlow.Input : ConnectorFlow.Output, connector.Node.Orientation);
}
return ToPosition(connector.Flow, connector.Node.Orientation);
}
return value;
}
private ConnectorPosition ToPosition(ConnectorFlow flow, Orientation orientation)
{
if (orientation == Orientation.Horizontal)
{
return flow == ConnectorFlow.Output
? ConnectorPosition.Right
: ConnectorPosition.Left;
}
return flow == ConnectorFlow.Output
? ConnectorPosition.Bottom
: ConnectorPosition.Top;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotImplementedException();
}
}
}

View File

@@ -0,0 +1,29 @@
using System;
using System.Globalization;
using System.Windows.Data;
namespace Nodify.Playground
{
public class FlowToDirectionConverter : IValueConverter
{
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is ConnectorFlow flow)
{
return flow == ConnectorFlow.Output ? ConnectionDirection.Forward : ConnectionDirection.Backward;
}
return value;
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
if (value is ConnectionDirection dir)
{
return dir == ConnectionDirection.Forward ? ConnectorFlow.Output : ConnectorFlow.Input;
}
return value;
}
}
}

View File

@@ -0,0 +1,27 @@
using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;
using System.Windows.Markup;
namespace Nodify.Playground
{
public class UIntToRectConverter : MarkupExtension, IValueConverter
{
public uint Multiplier { get; set; } = 1;
public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
{
uint size = System.Convert.ToUInt32(value) * Multiplier;
return new Rect(0d, 0d, size, size);
}
public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
{
throw new NotSupportedException();
}
public override object ProvideValue(IServiceProvider serviceProvider)
=> this;
}
}

View File

@@ -0,0 +1,21 @@
using System.Windows;
namespace Nodify.Playground
{
public class CommentNodeViewModel : NodeViewModel
{
private string? _title;
public string? Title
{
get => _title;
set => SetProperty(ref _title, value);
}
private Size _size;
public Size Size
{
get => _size;
set => SetProperty(ref _size, value);
}
}
}

View File

@@ -0,0 +1,51 @@
using System.Windows;
using System.Windows.Input;
namespace Nodify.Playground
{
public class ConnectionViewModel : ObservableObject
{
private NodifyEditorViewModel _graph = default!;
public NodifyEditorViewModel Graph
{
get => _graph;
internal set => SetProperty(ref _graph, value);
}
private ConnectorViewModel _input = default!;
public ConnectorViewModel Input
{
get => _input;
set => SetProperty(ref _input, value);
}
private ConnectorViewModel _output = default!;
public ConnectorViewModel Output
{
get => _output;
set => SetProperty(ref _output, value);
}
private bool _isSelected;
public bool IsSelected
{
get => _isSelected;
set => SetProperty(ref _isSelected, value);
}
public ICommand SplitCommand { get; }
public ICommand DisconnectCommand { get; }
public ConnectionViewModel()
{
SplitCommand = new DelegateCommand<Point>(Split);
DisconnectCommand = new DelegateCommand(Remove);
}
public void Split(Point point)
=> Graph.Schema.SplitConnection(this, point);
public void Remove()
=> Graph.Connections.Remove(this);
}
}

View File

@@ -0,0 +1,109 @@
using System.Linq;
using System.Windows;
namespace Nodify.Playground
{
public enum ConnectorFlow
{
Input,
Output
}
public enum ConnectorShape
{
Circle,
Triangle,
Square,
}
public class ConnectorViewModel : ObservableObject
{
private string? _title;
public string? Title
{
get => _title;
set => SetProperty(ref _title, value);
}
private bool _isConnected;
public bool IsConnected
{
get => _isConnected;
set => SetProperty(ref _isConnected, value);
}
private Point _anchor;
public Point Anchor
{
get => _anchor;
set => SetProperty(ref _anchor, value);
}
private NodeViewModel _node = default!;
public NodeViewModel Node
{
get => _node;
internal set
{
if (SetProperty(ref _node, value))
{
OnNodeChanged();
}
}
}
private ConnectorShape _shape;
public ConnectorShape Shape
{
get => _shape;
set => SetProperty(ref _shape, value);
}
public ConnectorFlow Flow { get; private set; }
public int MaxConnections { get; set; } = 2;
public NodifyObservableCollection<ConnectionViewModel> Connections { get; } = new NodifyObservableCollection<ConnectionViewModel>();
public ConnectorViewModel()
{
Connections.WhenAdded(c =>
{
c.Input.IsConnected = true;
c.Output.IsConnected = true;
}).WhenRemoved(c =>
{
if (c.Input.Connections.Count == 0)
{
c.Input.IsConnected = false;
}
if (c.Output.Connections.Count == 0)
{
c.Output.IsConnected = false;
}
});
}
protected virtual void OnNodeChanged()
{
if (Node is FlowNodeViewModel flow)
{
Flow = flow.Input.Contains(this) ? ConnectorFlow.Input : ConnectorFlow.Output;
}
else if (Node is KnotNodeViewModel knot)
{
Flow = knot.Flow;
}
}
public bool IsConnectedTo(ConnectorViewModel con)
=> Connections.Any(c => c.Input == con || c.Output == con);
public virtual bool AllowsNewConnections()
=> Connections.Count < MaxConnections;
public void Disconnect()
=> Node.Graph.Schema.DisconnectConnector(this);
}
}

View File

@@ -0,0 +1,34 @@
using System.Windows.Controls;
namespace Nodify.Playground
{
public class FlowNodeViewModel : NodeViewModel
{
private string? _title;
public string? Title
{
get => _title;
set => SetProperty(ref _title, value);
}
public NodifyObservableCollection<ConnectorViewModel> Input { get; } = new NodifyObservableCollection<ConnectorViewModel>();
public NodifyObservableCollection<ConnectorViewModel> Output { get; } = new NodifyObservableCollection<ConnectorViewModel>();
public FlowNodeViewModel()
{
Orientation = Orientation.Horizontal;
Input.WhenAdded(c => c.Node = this)
.WhenRemoved(c => c.Disconnect());
Output.WhenAdded(c => c.Node = this)
.WhenRemoved(c => c.Disconnect());
}
public void Disconnect()
{
Input.Clear();
Output.Clear();
}
}
}

View File

@@ -0,0 +1,133 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
namespace Nodify.Playground
{
public class GraphSchema
{
#region Add Connection
public bool CanAddConnection(ConnectorViewModel source, object target)
{
if (target is ConnectorViewModel con)
{
return source != con
&& source.Node != con.Node
&& source.Node.Graph == con.Node.Graph
&& source.Shape == con.Shape
&& source.AllowsNewConnections()
&& con.AllowsNewConnections()
&& (source.Flow != con.Flow || con.Node is KnotNodeViewModel)
&& !source.IsConnectedTo(con);
}
else if (source.AllowsNewConnections() && target is FlowNodeViewModel node)
{
var allConnectors = source.Flow == ConnectorFlow.Input ? node.Output : node.Input;
return allConnectors.Any(c => c.AllowsNewConnections());
}
return false;
}
public bool TryAddConnection(ConnectorViewModel source, object? target)
{
if (target != null && CanAddConnection(source, target))
{
if (target is ConnectorViewModel connector)
{
AddConnection(source, connector);
return true;
}
else if (target is FlowNodeViewModel node)
{
AddConnection(source, node);
return true;
}
}
return false;
}
private void AddConnection(ConnectorViewModel source, ConnectorViewModel target)
{
var sourceIsInput = source.Flow == ConnectorFlow.Input;
source.Node.Graph.Connections.Add(new ConnectionViewModel
{
Input = sourceIsInput ? source : target,
Output = sourceIsInput ? target : source
});
}
private void AddConnection(ConnectorViewModel source, FlowNodeViewModel target)
{
var allConnectors = source.Flow == ConnectorFlow.Input ? target.Output : target.Input;
var connector = allConnectors.First(c => c.AllowsNewConnections());
AddConnection(source, connector);
}
#endregion
public void DisconnectConnector(ConnectorViewModel connector)
{
var graph = connector.Node.Graph;
var connections = connector.Connections.ToList();
connections.ForEach(c => graph.Connections.Remove(c));
}
public void SplitConnection(ConnectionViewModel connection, Point location)
{
var knot = new KnotNodeViewModel(connection.Output.Node.Orientation)
{
Location = location,
Flow = connection.Output.Flow,
Connector = new ConnectorViewModel
{
MaxConnections = connection.Output.MaxConnections + connection.Input.MaxConnections,
Shape = connection.Input.Shape
}
};
connection.Graph.Nodes.Add(knot);
AddConnection(connection.Output, knot.Connector);
AddConnection(knot.Connector, connection.Input);
connection.Remove();
}
public void AddCommentAroundNodes(IList<NodeViewModel> nodes, string? text = default)
{
var rect = nodes.GetBoundingBox(50);
var comment = new CommentNodeViewModel
{
Location = rect.Location,
Size = rect.Size,
Title = text ?? "New comment"
};
nodes[0].Graph.Nodes.Add(comment);
}
/// <summary>
/// Rewires all connections from the source connector to the target connector if possible.
/// </summary>
/// <remarks>The source must be an input connector.</remarks>
public void Rewire(ConnectorViewModel source, ConnectorViewModel target)
{
if (source == target || source.Flow != ConnectorFlow.Input)
return;
var connectionsToRewire = source.Connections.ToList();
foreach (var connection in connectionsToRewire)
{
if (CanAddConnection(connection.Output, target))
{
source.Node.Graph.Connections.Remove(connection);
AddConnection(connection.Output, target);
}
}
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Windows.Controls;
namespace Nodify.Playground
{
public class KnotNodeViewModel : NodeViewModel
{
public KnotNodeViewModel(Orientation orientation)
{
Orientation = orientation;
}
public KnotNodeViewModel() : this(Orientation.Horizontal)
{
}
private ConnectorViewModel _connector = default!;
public ConnectorViewModel Connector
{
get => _connector;
set
{
if (SetProperty(ref _connector, value))
{
_connector.Node = this;
}
}
}
public ConnectorFlow Flow { get; set; }
}
}

View File

@@ -0,0 +1,32 @@
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Nodify.Playground
{
public abstract class NodeViewModel : ObservableObject
{
private NodifyEditorViewModel _graph = default!;
public NodifyEditorViewModel Graph
{
get => _graph;
internal set => SetProperty(ref _graph, value);
}
private Point _location;
public Point Location
{
get => _location;
set => SetProperty(ref _location, value);
}
public Orientation Orientation { get; protected set; }
public ICommand DeleteCommand { get; }
public NodeViewModel()
{
DeleteCommand = new DelegateCommand(() => Graph.Nodes.Remove(this));
}
}
}

View File

@@ -0,0 +1,735 @@
<UserControl x:Class="Nodify.Playground.NodifyEditorView"
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:Nodify.Playground"
xmlns:nodify="https://miroiu.github.io/nodify"
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
mc:Ignorable="d"
Background="{DynamicResource NodifyEditor.BackgroundBrush}"
d:DesignHeight="450"
d:DesignWidth="800">
<UserControl.DataContext>
<local:NodifyEditorViewModel />
</UserControl.DataContext>
<UserControl.Resources>
<shared:RandomBrushConverter x:Key="RandomBrushConverter" />
<local:FlowToDirectionConverter x:Key="FlowToDirectionConverter" />
<local:FlowToConnectorPositionConverter x:Key="FlowToConnectorPositionConverter" />
<GeometryDrawing x:Key="SmallGridGeometry"
Geometry="M0,0 L0,1 0.03,1 0.03,0.03 1,0.03 1,0 Z"
Brush="{DynamicResource GridLinesBrush}" />
<GeometryDrawing x:Key="LargeGridGeometry"
Geometry="M0,0 L0,1 0.015,1 0.015,0.015 1,0.015 1,0 Z"
Brush="{DynamicResource GridLinesBrush}" />
<DrawingBrush x:Key="SmallGridLinesDrawingBrush"
TileMode="Tile"
ViewportUnits="Absolute"
Viewport="{Binding GridSpacing, Source={x:Static local:EditorSettings.Instance}, Converter={local:UIntToRectConverter}}"
Transform="{Binding ViewportTransform, ElementName=Editor}"
Drawing="{StaticResource SmallGridGeometry}" />
<DrawingBrush x:Key="LargeGridLinesDrawingBrush"
TileMode="Tile"
ViewportUnits="Absolute"
Opacity="0.5"
Viewport="{Binding GridSpacing, Source={x:Static local:EditorSettings.Instance}, Converter={local:UIntToRectConverter Multiplier=10}}"
Transform="{Binding ViewportTransform, ElementName=Editor}"
Drawing="{StaticResource LargeGridGeometry}" />
<SolidColorBrush x:Key="SquareConnectorColor"
Color="MediumSlateBlue" />
<SolidColorBrush x:Key="TriangleConnectorColor"
Color="MediumVioletRed" />
<SolidColorBrush x:Key="SquareConnectorOutline"
Color="MediumSlateBlue"
Opacity="0.15" />
<SolidColorBrush x:Key="TriangleConnectorOutline"
Color="MediumVioletRed"
Opacity="0.15" />
<UIElement x:Key="ConnectionAnimationPlaceholder"
Opacity="1" />
<Storyboard x:Key="HighlightConnection">
<DoubleAnimation Storyboard.Target="{StaticResource ConnectionAnimationPlaceholder}"
Storyboard.TargetProperty="(UIElement.Opacity)"
Duration="0:0:0.3"
From="1"
To="0.3" />
</Storyboard>
<Style x:Key="ConnectionStyle"
TargetType="{x:Type nodify:BaseConnection}"
BasedOn="{StaticResource {x:Type nodify:BaseConnection}}">
<Style.Triggers>
<DataTrigger Binding="{Binding Input.Shape}"
Value="{x:Static local:ConnectorShape.Square}">
<Setter Property="Stroke"
Value="{StaticResource SquareConnectorColor}" />
<Setter Property="Fill"
Value="{StaticResource SquareConnectorColor}" />
<Setter Property="OutlineBrush"
Value="{StaticResource SquareConnectorOutline}" />
</DataTrigger>
<DataTrigger Binding="{Binding Input.Shape}"
Value="{x:Static local:ConnectorShape.Triangle}">
<Setter Property="Stroke"
Value="{StaticResource TriangleConnectorColor}" />
<Setter Property="Fill"
Value="{StaticResource TriangleConnectorColor}" />
<Setter Property="OutlineBrush"
Value="{StaticResource TriangleConnectorOutline}" />
</DataTrigger>
<Trigger Property="IsMouseDirectlyOver"
Value="True">
<Trigger.EnterActions>
<BeginStoryboard Name="HighlightConnection"
Storyboard="{StaticResource HighlightConnection}" />
</Trigger.EnterActions>
<Trigger.ExitActions>
<RemoveStoryboard BeginStoryboardName="HighlightConnection" />
</Trigger.ExitActions>
<Setter Property="Opacity"
Value="1" />
</Trigger>
<Trigger Property="IsSelectable"
Value="True">
<Setter Property="Cursor"
Value="Hand" />
</Trigger>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsMouseDirectlyOver"
Value="False" />
<Condition Property="IsSelected"
Value="False" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="OutlineBrush"
Value="Transparent" />
</MultiTrigger.Setters>
</MultiTrigger>
</Style.Triggers>
<Setter Property="Opacity"
Value="{Binding Source={StaticResource ConnectionAnimationPlaceholder}, Path=Opacity}" />
<Setter Property="Stroke"
Value="{DynamicResource Connection.StrokeBrush}" />
<Setter Property="Fill"
Value="{DynamicResource Connection.StrokeBrush}" />
<Setter Property="OutlineBrush">
<Setter.Value>
<SolidColorBrush Color="{DynamicResource Connection.StrokeColor}"
Opacity="0.15" />
</Setter.Value>
</Setter>
<Setter Property="ToolTip"
Value="Double click to split" />
<Setter Property="Source"
Value="{Binding Output.Anchor}" />
<Setter Property="Target"
Value="{Binding Input.Anchor}" />
<Setter Property="SplitCommand"
Value="{Binding SplitCommand}" />
<Setter Property="DisconnectCommand"
Value="{Binding DisconnectCommand}" />
<Setter Property="SourceOffsetMode"
Value="{Binding ConnectionSourceOffsetMode, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="TargetOffsetMode"
Value="{Binding ConnectionTargetOffsetMode, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="SourceOffset"
Value="{Binding ConnectionSourceOffset.Size, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="TargetOffset"
Value="{Binding ConnectionTargetOffset.Size, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="ArrowSize"
Value="{Binding ConnectionArrowSize.Size, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="ArrowEnds"
Value="{Binding ArrowHeadEnds, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="ArrowShape"
Value="{Binding ArrowHeadShape, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="Spacing"
Value="{Binding ConnectionSpacing, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="Direction"
Value="{Binding Output.Flow, Converter={StaticResource FlowToDirectionConverter}}" />
<Setter Property="SourceOrientation"
Value="{Binding Output.Node.Orientation}" />
<Setter Property="TargetOrientation"
Value="{Binding Input.Node.Orientation}" />
<Setter Property="DirectionalArrowsCount"
Value="{Binding DirectionalArrowsCount, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="DirectionalArrowsOffset"
Value="{Binding DirectionalArrowsOffset, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="IsAnimatingDirectionalArrows"
Value="{Binding IsAnimatingConnections, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="DirectionalArrowsAnimationDuration"
Value="{Binding DirectionalArrowsAnimationDuration, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="Text"
Value="{Binding ConnectionText, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="IsSelectable"
Value="{Binding SelectableConnections, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="IsSelected"
Value="{Binding IsSelected}" />
<Setter Property="StrokeThickness"
Value="{Binding ConnectionStrokeThickness, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="OutlineThickness"
Value="{Binding ConnectionOutlineThickness, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="FocusVisualPadding"
Value="{Binding ConnectionFocusVisualPadding, Source={x:Static local:EditorSettings.Instance}}" />
</Style>
<DataTemplate x:Key="CircuitConnectionTemplate">
<nodify:CircuitConnection Style="{StaticResource ConnectionStyle}"
Angle="{Binding CircuitConnectionAngle, Source={x:Static local:EditorSettings.Instance}}"
CornerRadius="{Binding ConnectionCornerRadius, Source={x:Static local:EditorSettings.Instance}}" />
</DataTemplate>
<DataTemplate x:Key="StepConnectionTemplate">
<nodify:StepConnection Style="{StaticResource ConnectionStyle}"
CornerRadius="{Binding ConnectionCornerRadius, Source={x:Static local:EditorSettings.Instance}}"
SourcePosition="{Binding ., Converter={StaticResource FlowToConnectorPositionConverter}, ConverterParameter=Output}"
TargetPosition="{Binding ., Converter={StaticResource FlowToConnectorPositionConverter}, ConverterParameter=Input}" />
</DataTemplate>
<DataTemplate x:Key="LineConnectionTemplate">
<nodify:LineConnection Style="{StaticResource ConnectionStyle}"
CornerRadius="{Binding ConnectionCornerRadius, Source={x:Static local:EditorSettings.Instance}}" />
</DataTemplate>
<DataTemplate x:Key="ConnectionTemplate">
<nodify:Connection Style="{StaticResource ConnectionStyle}" />
</DataTemplate>
<ControlTemplate x:Key="SquareConnector"
TargetType="Control">
<Rectangle Width="14"
Height="14"
StrokeDashCap="Round"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Stroke="{TemplateBinding BorderBrush}"
Fill="{TemplateBinding Background}"
StrokeThickness="2" />
</ControlTemplate>
<ControlTemplate x:Key="TriangleConnector"
TargetType="Control">
<Polygon Width="14"
Height="14"
Points="1,13 13,13 7,1"
StrokeDashCap="Round"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Stroke="{TemplateBinding BorderBrush}"
Fill="{TemplateBinding Background}"
StrokeThickness="2" />
</ControlTemplate>
<Storyboard x:Key="MarchingAnts">
<DoubleAnimation RepeatBehavior="Forever"
Storyboard.TargetProperty="StrokeDashOffset"
BeginTime="00:00:00"
Duration="0:3:0"
From="1000"
To="0" />
</Storyboard>
<Style x:Key="SelectionRectangleStyle"
TargetType="Rectangle"
BasedOn="{StaticResource NodifyEditor.SelectionRectangleStyle}">
<Setter Property="StrokeDashArray"
Value="4 4" />
<Setter Property="StrokeThickness"
Value="2" />
<Style.Triggers>
<EventTrigger RoutedEvent="FrameworkElement.Loaded">
<BeginStoryboard Storyboard="{StaticResource MarchingAnts}" />
</EventTrigger>
</Style.Triggers>
</Style>
<Style x:Key="CuttingLineStyle"
TargetType="{x:Type nodify:CuttingLine}"
BasedOn="{StaticResource {x:Type nodify:CuttingLine}}">
<Setter Property="StrokeDashArray"
Value="1 1" />
<Setter Property="StrokeThickness"
Value="2" />
</Style>
</UserControl.Resources>
<Grid>
<nodify:NodifyEditor x:Name="Editor"
ItemsSource="{Binding Nodes}"
SelectedItem="{Binding SelectedNode}"
SelectedItems="{Binding SelectedNodes}"
CanSelectMultipleItems="{Binding CanSelectMultipleNodes, Source={x:Static local:EditorSettings.Instance}}"
Connections="{Binding Connections}"
SelectedConnection="{Binding SelectedConnection}"
SelectedConnections="{Binding SelectedConnections}"
CanSelectMultipleConnections="{Binding CanSelectMultipleConnections, Source={x:Static local:EditorSettings.Instance}}"
PendingConnection="{Binding PendingConnection}"
DisconnectConnectorCommand="{Binding DisconnectConnectorCommand}"
ViewportLocation="{Binding Location.Value, Source={x:Static local:EditorSettings.Instance}}"
ViewportSize="{Binding ViewportSize, Mode=OneWayToSource}"
ViewportZoom="{Binding Zoom, Source={x:Static local:EditorSettings.Instance}}"
MinViewportZoom="{Binding MinZoom, Source={x:Static local:EditorSettings.Instance}}"
MaxViewportZoom="{Binding MaxZoom, Source={x:Static local:EditorSettings.Instance}}"
AutoPanSpeed="{Binding AutoPanningSpeed, Source={x:Static local:EditorSettings.Instance}}"
AutoPanEdgeDistance="{Binding AutoPanningEdgeDistance, Source={x:Static local:EditorSettings.Instance}}"
GridCellSize="{Binding GridSpacing, Source={x:Static local:EditorSettings.Instance}}"
EnableRealtimeSelection="{Binding EnableRealtimeSelection, Source={x:Static local:EditorSettings.Instance}}"
DisableAutoPanning="{Binding DisableAutoPanning, Source={x:Static local:EditorSettings.Instance}}"
DisablePanning="{Binding DisablePanning, Source={x:Static local:EditorSettings.Instance}}"
DisableZooming="{Binding DisableZooming, Source={x:Static local:EditorSettings.Instance}}"
DisplayConnectionsOnTop="{Binding DisplayConnectionsOnTop, Source={x:Static local:EditorSettings.Instance}}"
BringIntoViewSpeed="{Binding BringIntoViewSpeed, Source={x:Static local:EditorSettings.Instance}}"
BringIntoViewMaxDuration="{Binding BringIntoViewMaxDuration, Source={x:Static local:EditorSettings.Instance}}"
SelectionRectangleStyle="{StaticResource SelectionRectangleStyle}"
CuttingLineStyle="{StaticResource CuttingLineStyle}">
<nodify:NodifyEditor.Style>
<Style TargetType="{x:Type nodify:NodifyEditor}"
BasedOn="{StaticResource {x:Type nodify:NodifyEditor}}">
<Setter Property="ConnectionTemplate"
Value="{StaticResource ConnectionTemplate}" />
<Style.Triggers>
<DataTrigger Binding="{Binding ShowGridLines, Source={x:Static local:PlaygroundSettings.Instance}}"
Value="True">
<Setter Property="Background"
Value="{StaticResource SmallGridLinesDrawingBrush}" />
</DataTrigger>
<DataTrigger Binding="{Binding ConnectionStyle, Mode=TwoWay, Source={x:Static local:EditorSettings.Instance}}"
Value="Line">
<Setter Property="ConnectionTemplate"
Value="{StaticResource LineConnectionTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding ConnectionStyle, Mode=TwoWay, Source={x:Static local:EditorSettings.Instance}}"
Value="Circuit">
<Setter Property="ConnectionTemplate"
Value="{StaticResource CircuitConnectionTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding ConnectionStyle, Mode=TwoWay, Source={x:Static local:EditorSettings.Instance}}"
Value="Step">
<Setter Property="ConnectionTemplate"
Value="{StaticResource StepConnectionTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
</nodify:NodifyEditor.Style>
<nodify:NodifyEditor.InputBindings>
<KeyBinding Key="Delete"
Command="{Binding DeleteSelectionCommand}" />
<KeyBinding Key="C"
Command="{Binding CommentSelectionCommand}" />
</nodify:NodifyEditor.InputBindings>
<nodify:NodifyEditor.Resources>
<Style TargetType="{x:Type nodify:PendingConnection}"
BasedOn="{StaticResource {x:Type nodify:PendingConnection}}">
<Setter Property="CompletedCommand"
Value="{Binding Graph.CreateConnectionCommand}" />
<Setter Property="Source"
Value="{Binding Source, Mode=OneWayToSource}" />
<Setter Property="Target"
Value="{Binding PreviewTarget, Mode=OneWayToSource}" />
<Setter Property="PreviewTarget"
Value="{Binding PreviewTarget, Mode=OneWayToSource}" />
<Setter Property="Content"
Value="{Binding PreviewText}" />
<Setter Property="EnablePreview"
Value="{Binding EnablePendingConnectionPreview, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="EnableSnapping"
Value="{Binding EnablePendingConnectionSnapping, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="AllowOnlyConnectors"
Value="{Binding AllowConnectingToConnectorsOnly, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="Direction"
Value="{Binding Source.Flow, Converter={StaticResource FlowToDirectionConverter}}" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type nodify:PendingConnection}">
<Canvas>
<nodify:Connection Source="{TemplateBinding SourceAnchor}"
Target="{TemplateBinding TargetAnchor}"
Direction="{TemplateBinding Direction}"
SourceOrientation="{Binding Source.Node.Orientation}"
TargetOrientation="{Binding TargetOrientation}"
DirectionalArrowsCount="{Binding DirectionalArrowsCount, Source={x:Static local:EditorSettings.Instance}}"
StrokeThickness="{TemplateBinding StrokeThickness}"
SourceOffset="{Binding ConnectionSourceOffset.Size, Source={x:Static local:EditorSettings.Instance}}"
TargetOffset="{Binding ConnectionTargetOffset.Size, Source={x:Static local:EditorSettings.Instance}}"
SourceOffsetMode="{Binding ConnectionSourceOffsetMode, Source={x:Static local:EditorSettings.Instance}}"
TargetOffsetMode="None"
ArrowSize="{Binding ConnectionArrowSize.Size, Source={x:Static local:EditorSettings.Instance}}"
ArrowEnds="{Binding ArrowHeadEnds, Source={x:Static local:EditorSettings.Instance}}"
ArrowShape="{Binding ArrowHeadShape, Source={x:Static local:EditorSettings.Instance}}"
Spacing="{Binding ConnectionSpacing, Source={x:Static local:EditorSettings.Instance}}">
<nodify:Connection.Style>
<Style TargetType="nodify:Connection"
BasedOn="{StaticResource {x:Type nodify:Connection}}">
<Setter Property="Stroke"
Value="{DynamicResource Connection.StrokeBrush}" />
<Setter Property="Fill"
Value="{DynamicResource Connection.StrokeBrush}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Source.Shape}"
Value="{x:Static local:ConnectorShape.Square}">
<Setter Property="Stroke"
Value="{StaticResource SquareConnectorColor}" />
<Setter Property="Fill"
Value="{StaticResource SquareConnectorColor}" />
</DataTrigger>
<DataTrigger Binding="{Binding Source.Shape}"
Value="{x:Static local:ConnectorShape.Triangle}">
<Setter Property="Stroke"
Value="{StaticResource TriangleConnectorColor}" />
<Setter Property="Fill"
Value="{StaticResource TriangleConnectorColor}" />
</DataTrigger>
</Style.Triggers>
</Style>
</nodify:Connection.Style>
</nodify:Connection>
<Border Background="{TemplateBinding Background}"
Canvas.Left="{Binding TargetAnchor.X, RelativeSource={RelativeSource TemplatedParent}}"
Canvas.Top="{Binding TargetAnchor.Y, RelativeSource={RelativeSource TemplatedParent}}"
Visibility="{Binding PreviewText, Converter={shared:StringToVisibilityConverter}}"
Padding="{TemplateBinding Padding}"
BorderThickness="{TemplateBinding BorderThickness}"
BorderBrush="{TemplateBinding BorderBrush}"
CornerRadius="3"
Margin="15">
<ContentPresenter />
</Border>
</Canvas>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type nodify:Connector}"
BasedOn="{StaticResource {x:Type nodify:Connector}}">
<Setter Property="Anchor"
Value="{Binding Anchor, Mode=OneWayToSource}" />
<Setter Property="IsConnected"
Value="{Binding IsConnected}" />
</Style>
<Style TargetType="{x:Type nodify:NodeInput}"
BasedOn="{StaticResource {x:Type nodify:NodeInput}}">
<Style.Triggers>
<DataTrigger Binding="{Binding Shape}"
Value="{x:Static local:ConnectorShape.Square}">
<Setter Property="ConnectorTemplate"
Value="{StaticResource SquareConnector}" />
<Setter Property="BorderBrush"
Value="{StaticResource SquareConnectorColor}" />
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Title}"
Margin="0 0 5 0" />
<TextBox Text="{Binding MaxConnections}"
MinWidth="30" />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
<DataTrigger Binding="{Binding Shape}"
Value="{x:Static local:ConnectorShape.Triangle}">
<Setter Property="ConnectorTemplate"
Value="{StaticResource TriangleConnector}" />
<Setter Property="BorderBrush"
Value="{StaticResource TriangleConnectorColor}" />
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<StackPanel Orientation="Horizontal">
<TextBlock Text="{Binding Title}"
Margin="0 0 5 0"
VerticalAlignment="Center" />
<CheckBox />
</StackPanel>
</DataTemplate>
</Setter.Value>
</Setter>
</DataTrigger>
</Style.Triggers>
<Setter Property="HeaderTemplate">
<Setter.Value>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<TextBlock Text="{Binding Title}" />
</DataTemplate>
</Setter.Value>
</Setter>
<Setter Property="Header"
Value="{Binding}" />
<Setter Property="Anchor"
Value="{Binding Anchor, Mode=OneWayToSource}" />
<Setter Property="IsConnected"
Value="{Binding IsConnected}" />
<Setter Property="Background"
Value="Transparent" />
</Style>
<Style TargetType="{x:Type nodify:NodeOutput}"
BasedOn="{StaticResource {x:Type nodify:NodeOutput}}">
<Style.Triggers>
<DataTrigger Binding="{Binding Shape}"
Value="{x:Static local:ConnectorShape.Square}">
<Setter Property="ConnectorTemplate"
Value="{StaticResource SquareConnector}" />
<Setter Property="BorderBrush"
Value="{StaticResource SquareConnectorColor}" />
</DataTrigger>
<DataTrigger Binding="{Binding Shape}"
Value="{x:Static local:ConnectorShape.Triangle}">
<Setter Property="ConnectorTemplate"
Value="{StaticResource TriangleConnector}" />
<Setter Property="BorderBrush"
Value="{StaticResource TriangleConnectorColor}" />
</DataTrigger>
</Style.Triggers>
<Setter Property="Header"
Value="{Binding Title}" />
<Setter Property="Anchor"
Value="{Binding Anchor, Mode=OneWayToSource}" />
<Setter Property="IsConnected"
Value="{Binding IsConnected}" />
<Setter Property="Background"
Value="Transparent" />
</Style>
<DataTemplate DataType="{x:Type local:KnotNodeViewModel}">
<nodify:KnotNode Content="{Binding Connector}" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:CommentNodeViewModel}">
<nodify:GroupingNode ActualSize="{Binding Size}"
Header="{Binding Title}"
MovementMode="{Binding GroupingNodeMovement, Mode=TwoWay, Source={x:Static local:EditorSettings.Instance}}" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:FlowNodeViewModel}">
<nodify:Node Input="{Binding Input}"
Output="{Binding Output}"
Header="{Binding Title}" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:VerticalNodeViewModel}">
<nodify:Node Header="{Binding Input}"
Footer="{Binding Output}"
Content="{Binding Title}">
<nodify:Node.ContentTemplate>
<DataTemplate>
<TextBlock Text="{Binding}"
Margin="5" />
</DataTemplate>
</nodify:Node.ContentTemplate>
<nodify:Node.HeaderTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}"
Focusable="False">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<nodify:NodeInput Orientation="Vertical" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</nodify:Node.HeaderTemplate>
<nodify:Node.FooterTemplate>
<DataTemplate>
<ItemsControl ItemsSource="{Binding}"
Focusable="False">
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:ConnectorViewModel}">
<nodify:NodeOutput Orientation="Vertical" />
</DataTemplate>
</ItemsControl.ItemTemplate>
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Center" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</DataTemplate>
</nodify:Node.FooterTemplate>
</nodify:Node>
</DataTemplate>
</nodify:NodifyEditor.Resources>
<nodify:NodifyEditor.ItemContainerStyle>
<Style TargetType="{x:Type nodify:ItemContainer}"
BasedOn="{StaticResource {x:Type nodify:ItemContainer}}">
<Setter Property="BorderThickness"
Value="2" />
<Setter Property="SelectedBorderThickness"
Value="4" />
<Setter Property="IsSelectable"
Value="{Binding SelectableNodes, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="IsDraggable"
Value="{Binding DraggableNodes, Source={x:Static local:EditorSettings.Instance}}" />
<Setter Property="CacheMode">
<Setter.Value>
<BitmapCache RenderAtScale="{Binding MaxZoom, Source={x:Static local:EditorSettings.Instance}}"
EnableClearType="True" />
</Setter.Value>
</Setter>
<Setter Property="Location"
Value="{Binding Location}" />
<Style.Triggers>
<Trigger Property="IsSelected"
Value="True">
<Setter Property="Panel.ZIndex"
Value="1" />
</Trigger>
</Style.Triggers>
</Style>
</nodify:NodifyEditor.ItemContainerStyle>
</nodify:NodifyEditor>
<Grid Background="{StaticResource LargeGridLinesDrawingBrush}"
Visibility="{Binding ShowGridLines, Source={x:Static local:PlaygroundSettings.Instance}, Converter={shared:BooleanToVisibilityConverter}}"
Panel.ZIndex="-2" />
<nodify:Minimap ItemsSource="{Binding ItemsSource, ElementName=Editor}"
ViewportSize="{Binding ViewportSize, ElementName=Editor}"
ViewportLocation="{Binding ViewportLocation, ElementName=Editor}"
Visibility="{Binding ShowMinimap, Source={x:Static local:PlaygroundSettings.Instance}, Converter={shared:BooleanToVisibilityConverter}}"
IsReadOnly="{Binding DisableMinimapControls, Source={x:Static local:PlaygroundSettings.Instance}}"
ResizeToViewport="{Binding ResizeToViewport, Source={x:Static local:PlaygroundSettings.Instance}}"
MaxViewportOffset="{Binding MinimapMaxViewportOffset.Size, Source={x:Static local:PlaygroundSettings.Instance}}"
Zoom="Minimap_Zoom"
HorizontalAlignment="Right"
VerticalAlignment="Bottom"
Width="300"
Height="200"
Margin="5 40">
<nodify:Minimap.ItemTemplate>
<DataTemplate DataType="{x:Type local:NodeViewModel}">
<Grid />
</DataTemplate>
</nodify:Minimap.ItemTemplate>
<nodify:Minimap.ItemContainerStyle>
<Style TargetType="{x:Type nodify:MinimapItem}"
BasedOn="{StaticResource {x:Type nodify:MinimapItem}}">
<Setter Property="Location"
Value="{Binding Location}" />
<Setter Property="Width"
Value="150" />
<Setter Property="Height"
Value="130" />
</Style>
</nodify:Minimap.ItemContainerStyle>
</nodify:Minimap>
<StackPanel HorizontalAlignment="Right"
VerticalAlignment="Top"
Margin="5 60"
Width="250">
<Border CornerRadius="3"
Visibility="{Binding SelectedConnection, Converter={shared:BooleanToVisibilityConverter}}"
Background="{DynamicResource PanelBackgroundBrush}"
BorderThickness="1"
BorderBrush="{DynamicResource BorderBrush}"
Margin="0 0 0 10">
<StackPanel Margin="10">
<StackPanel.Resources>
<Style TargetType="{x:Type TextBlock}"
BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Margin"
Value="0 0 0 5" />
</Style>
</StackPanel.Resources>
<StackPanel Margin="0 0 0 14">
<TextBlock Text="Selected connection"
Foreground="{DynamicResource Node.ForegroundBrush}"
FontWeight="Bold" />
</StackPanel>
<TextBlock TextWrapping="Wrap"
Margin="0 0 0 14"
Foreground="{DynamicResource Node.ForegroundBrush}">
<Run>From</Run>
<Run Text="{Binding SelectedConnection.Output.Node.Title}"
Foreground="Red" />
<Run> - </Run>
<Run Text="{Binding SelectedConnection.Output.Title}"
Foreground="Red" />
<Run>to</Run>
<Run Text="{Binding SelectedConnection.Input.Node.Title}"
Foreground="Red" />
<Run> - </Run>
<Run Text="{Binding SelectedConnection.Input.Title}"
Foreground="Red" />
</TextBlock>
<Button Command="{Binding SelectedConnection.DisconnectCommand}"
HorizontalAlignment="Left"
Style="{StaticResource HollowButton}"
Content="Delete" />
</StackPanel>
</Border>
<Border CornerRadius="3"
Visibility="{Binding SelectedNode, Converter={shared:BooleanToVisibilityConverter}}"
Background="{DynamicResource PanelBackgroundBrush}"
BorderThickness="1"
BorderBrush="{DynamicResource BorderBrush}">
<StackPanel Margin="10">
<StackPanel.Resources>
<Style TargetType="{x:Type TextBlock}"
BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Margin"
Value="0 0 0 5" />
</Style>
</StackPanel.Resources>
<StackPanel Margin="0 0 0 14">
<TextBlock Text="Selected node"
Foreground="{DynamicResource Node.ForegroundBrush}"
FontWeight="Bold" />
</StackPanel>
<TextBlock TextWrapping="Wrap"
Margin="0 0 0 14"
Foreground="{DynamicResource Node.ForegroundBrush}">
<Run>Title: </Run>
<Run Text="{Binding SelectedNode.Title}"
Foreground="Red" />
</TextBlock>
<TextBlock TextWrapping="Wrap"
Margin="0 0 0 14"
Foreground="{DynamicResource Node.ForegroundBrush}">
<Run>Location: </Run>
<Run Text="{Binding SelectedNode.Location}"
Foreground="Red" />
</TextBlock>
<Button Command="{Binding SelectedNode.DeleteCommand}"
HorizontalAlignment="Left"
Style="{StaticResource HollowButton}"
Content="Delete" />
</StackPanel>
</Border>
</StackPanel>
</Grid>
</UserControl>

View File

@@ -0,0 +1,51 @@
using Nodify.Events;
using Nodify.Interactivity;
using System.Windows.Controls;
namespace Nodify.Playground
{
public partial class NodifyEditorView : UserControl
{
public NodifyEditor EditorInstance => Editor;
public NodifyEditorView()
{
InitializeComponent();
EditorInstance.ActiveNavigationLayerChanged += DisplayActiveNavigationLayer;
}
static NodifyEditorView()
{
InputProcessor.Shared<Connector>.ReplaceHandlerFactory<ConnectorState.Connecting>(elem => new CustomConnecting(elem));
InputProcessor.Shared<Connector>.RegisterHandlerFactory(elem => new RetargetConnections(elem));
}
private void Minimap_Zoom(object sender, ZoomEventArgs e)
{
EditorInstance.ZoomAtPosition(e.Zoom, e.Location);
}
private void DisplayActiveNavigationLayer(KeyboardNavigationLayerId layerId)
{
var editorVm = (NodifyEditorViewModel)EditorInstance.DataContext;
if (layerId == KeyboardNavigationLayerId.Nodes)
{
editorVm.KeyboardNavigationLayer = nameof(KeyboardNavigationLayerId.Nodes);
}
else if (layerId == KeyboardNavigationLayerId.Connections)
{
editorVm.KeyboardNavigationLayer = nameof(KeyboardNavigationLayerId.Connections);
}
else if (layerId == KeyboardNavigationLayerId.Decorators)
{
editorVm.KeyboardNavigationLayer = nameof(KeyboardNavigationLayerId.Decorators);
}
else
{
editorVm.KeyboardNavigationLayer = "Custom";
}
}
}
}

View File

@@ -0,0 +1,133 @@
using System.Linq;
using System.Windows;
using System.Windows.Input;
namespace Nodify.Playground
{
public class NodifyEditorViewModel : ObservableObject
{
public NodifyEditorViewModel()
{
Schema = new GraphSchema();
PendingConnection = new PendingConnectionViewModel
{
Graph = this
};
DeleteSelectionCommand = new DelegateCommand(DeleteSelection, () => SelectedNodes.Count > 0 || SelectedConnections.Count > 0);
CommentSelectionCommand = new RequeryCommand(() => Schema.AddCommentAroundNodes(SelectedNodes, "New comment"), () => SelectedNodes.Count > 0);
DisconnectConnectorCommand = new DelegateCommand<ConnectorViewModel>(c => c.Disconnect());
CreateConnectionCommand = new DelegateCommand<object>(target => Schema.TryAddConnection(PendingConnection.Source!, target),
target => PendingConnection.Source != null && target != null && Schema.CanAddConnection(PendingConnection.Source, target));
Connections.WhenAdded(c =>
{
c.Graph = this;
c.Input.Connections.Add(c);
c.Output.Connections.Add(c);
})
// Called when the collection is cleared
.WhenRemoved(c =>
{
c.Input.Connections.Remove(c);
c.Output.Connections.Remove(c);
});
Nodes.WhenAdded(x => x.Graph = this)
// Not called when the collection is cleared
.WhenRemoved(x =>
{
if (x is FlowNodeViewModel flow)
{
flow.Disconnect();
}
else if (x is KnotNodeViewModel knot)
{
knot.Connector.Disconnect();
}
})
.WhenCleared(x => Connections.Clear());
}
private NodifyObservableCollection<NodeViewModel> _nodes = new NodifyObservableCollection<NodeViewModel>();
public NodifyObservableCollection<NodeViewModel> Nodes
{
get => _nodes;
set => SetProperty(ref _nodes, value);
}
private NodifyObservableCollection<NodeViewModel> _selectedNodes = new NodifyObservableCollection<NodeViewModel>();
public NodifyObservableCollection<NodeViewModel> SelectedNodes
{
get => _selectedNodes;
set => SetProperty(ref _selectedNodes, value);
}
private NodifyObservableCollection<ConnectionViewModel> _selectedConnections = new NodifyObservableCollection<ConnectionViewModel>();
public NodifyObservableCollection<ConnectionViewModel> SelectedConnections
{
get => _selectedConnections;
set => SetProperty(ref _selectedConnections, value);
}
private NodifyObservableCollection<ConnectionViewModel> _connections = new NodifyObservableCollection<ConnectionViewModel>();
public NodifyObservableCollection<ConnectionViewModel> Connections
{
get => _connections;
set => SetProperty(ref _connections, value);
}
private Size _viewportSize;
public Size ViewportSize
{
get => _viewportSize;
set => SetProperty(ref _viewportSize, value);
}
public PendingConnectionViewModel PendingConnection { get; }
private ConnectionViewModel? _selectedConnection;
public ConnectionViewModel? SelectedConnection
{
get => _selectedConnection;
set => SetProperty(ref _selectedConnection, value);
}
private NodeViewModel? _selectedNode;
public NodeViewModel? SelectedNode
{
get => _selectedNode;
set => SetProperty(ref _selectedNode, value);
}
public GraphSchema Schema { get; }
private string? _keyboardNavigationLayer;
public string? KeyboardNavigationLayer
{
get => _keyboardNavigationLayer;
set => SetProperty(ref _keyboardNavigationLayer, value);
}
public ICommand DeleteSelectionCommand { get; }
public ICommand DisconnectConnectorCommand { get; }
public ICommand CreateConnectionCommand { get; }
public ICommand CommentSelectionCommand { get; }
private void DeleteSelection()
{
foreach (var connection in SelectedConnections.ToList())
{
connection.Remove();
}
var selected = SelectedNodes.ToList();
for (int i = 0; i < selected.Count; i++)
{
Nodes.Remove(selected[i]);
}
}
}
}

View File

@@ -0,0 +1,79 @@
using System.Windows.Controls;
namespace Nodify.Playground
{
public class PendingConnectionViewModel : ObservableObject
{
private NodifyEditorViewModel _graph = default!;
public NodifyEditorViewModel Graph
{
get => _graph;
internal set => SetProperty(ref _graph, value);
}
private ConnectorViewModel? _source;
public ConnectorViewModel? Source
{
get => _source;
set
{
if(SetProperty(ref _source, value))
{
SetTargetOrientation();
}
}
}
private object? _previewTarget;
public object? PreviewTarget
{
get => _previewTarget;
set
{
if (SetProperty(ref _previewTarget, value))
{
OnPreviewTargetChanged();
}
}
}
private string? _previewText;
public string? PreviewText
{
get => _previewText;
set => SetProperty(ref _previewText, value);
}
private Orientation _targetOrientation;
public Orientation TargetOrientation
{
get => _targetOrientation;
set => SetProperty(ref _targetOrientation, value);
}
protected virtual void OnPreviewTargetChanged()
{
bool canConnect = PreviewTarget != null && Graph.Schema.CanAddConnection(Source!, PreviewTarget);
PreviewText = PreviewTarget switch
{
ConnectorViewModel con when con == Source => $"Can't connect to self",
ConnectorViewModel con => $"{(canConnect ? "Connect" : "Can't connect")} to {con.Title ?? "pin"}",
FlowNodeViewModel flow => $"{(canConnect ? "Connect" : "Can't connect")} to {flow.Title ?? "node"}",
_ => null
};
SetTargetOrientation();
}
private void SetTargetOrientation()
{
TargetOrientation = PreviewTarget switch
{
ConnectorViewModel con when con.Node is FlowNodeViewModel flow => flow.Orientation,
FlowNodeViewModel flow => flow.Orientation,
NodifyEditorViewModel editor when Source?.Node is FlowNodeViewModel flow => flow.Orientation,
_ => Orientation.Horizontal,
};
}
}
}

View File

@@ -0,0 +1,132 @@
using Nodify.Interactivity;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Nodify.Playground
{
/// <summary>
/// Connecting state that prevents connecting when <see cref="RetargetConnections"/> is in progress.
/// </summary>
public class CustomConnecting : ConnectorState.Connecting
{
protected override bool CanBegin => !RetargetConnections.InProgress;
public CustomConnecting(Connector connector) : base(connector)
{
}
}
/// <summary>
/// Hold CTRL+LeftClick on a connector to start reconnecting it.
/// </summary>
public class RetargetConnections : DragState<Connector>
{
public static InputGestureRef Reconnect { get; } = new Interactivity.MouseGesture(MouseAction.LeftClick, ModifierKeys.Control)
{
IgnoreModifierKeysOnRelease = true
};
/// <summary>
/// Used to prevent connecting when <see cref="EditorSettings.EnableStickyConnectors"/> is enabled.
/// </summary>
public static bool InProgress { get; private set; }
protected override bool CanBegin => ViewModel.IsConnected && ViewModel.Flow == ConnectorFlow.Input;
protected override bool IsToggle => EditorSettings.Instance.EnableStickyConnectors;
private ConnectorViewModel ViewModel => (ConnectorViewModel)Element.DataContext;
private Vector _connectorOffset;
private Connector? _targetConnector;
public RetargetConnections(Connector element) : base(element, Reconnect, EditorGestures.Mappings.Connector.CancelAction)
{
PositionElement = Element.Editor ?? (IInputElement)Element;
}
protected override void OnBegin(InputEventArgs e)
{
_connectorOffset = ViewModel.Node.Orientation == Orientation.Horizontal
? (Vector)EditorSettings.Instance.ConnectionTargetOffset.Value
: new Vector(EditorSettings.Instance.ConnectionTargetOffset.Value.Y, EditorSettings.Instance.ConnectionTargetOffset.Value.X);
InProgress = true;
}
protected override void OnMouseMove(MouseEventArgs e)
{
var position = Element.Editor!.MouseLocation;
if (EditorSettings.Instance.EnablePendingConnectionHitTesting)
{
var connector = Element.FindTargetConnector(position);
connector?.UpdateAnchor();
SetTargetConnector(connector);
UpdateConnections(connector != null ? connector.Anchor : position + _connectorOffset);
}
else
{
UpdateConnections(position + _connectorOffset);
}
}
private void UpdateConnections(Point position)
{
foreach (var connection in ViewModel.Connections)
{
connection.Input.Anchor = position;
}
}
protected override void OnEnd(InputEventArgs e)
{
var position = Element.Editor!.MouseLocation;
var target = Element.FindTargetConnector(position);
target?.UpdateAnchor();
if (target?.DataContext is ConnectorViewModel targetVM && ViewModel != targetVM)
{
ViewModel.Node.Graph.Schema.Rewire(ViewModel, targetVM);
}
SetTargetConnector(null);
// Reset the position of connections that were not rewired
Element.UpdateAnchor();
InProgress = false;
}
protected override void OnCancel(InputEventArgs e)
{
SetTargetConnector(null);
// Reset the position of connections that were not rewired
Element.UpdateAnchor();
InProgress = false;
}
/// <summary>
/// Sets the connection target and updates the visual state of the target element.
/// </summary>
private void SetTargetConnector(Connector? target)
{
if (target == _targetConnector)
{
return;
}
if (_targetConnector != null)
{
PendingConnection.SetIsOverElement(_targetConnector, false);
}
if (target != null)
{
PendingConnection.SetIsOverElement(target, true);
}
_targetConnector = target;
}
}
}

View File

@@ -0,0 +1,12 @@
using System.Windows.Controls;
namespace Nodify.Playground
{
public class VerticalNodeViewModel : FlowNodeViewModel
{
public VerticalNodeViewModel()
{
Orientation = Orientation.Vertical;
}
}
}

View File

@@ -0,0 +1,82 @@
using Nodify.Interactivity;
using System.Windows.Input;
namespace Nodify.Playground
{
public enum EditorInputMode
{
Default,
PanOnly,
SelectOnly,
CutOnly
}
public enum EditorGesturesMappings
{
Default,
Custom
}
public static class EditorInputModeExtensions
{
public static void Apply(this EditorGestures mappings, EditorInputMode inputMode)
{
mappings.Apply(PlaygroundSettings.Instance.EditorGesturesMappings.ToGesturesMappings());
switch (inputMode)
{
case EditorInputMode.PanOnly:
mappings.Editor.Selection.Unbind();
mappings.Editor.Cutting.Unbind();
mappings.ItemContainer.Selection.Unbind();
mappings.ItemContainer.Drag.Unbind();
mappings.Connector.Connect.Unbind();
break;
case EditorInputMode.SelectOnly:
mappings.Editor.Pan.Unbind();
mappings.Editor.Cutting.Unbind();
mappings.ItemContainer.Drag.Unbind();
mappings.Connector.Connect.Unbind();
break;
case EditorInputMode.CutOnly:
mappings.Editor.Cutting.Value = new Interactivity.MouseGesture(MouseAction.LeftClick);
mappings.Editor.Selection.Unbind();
mappings.Editor.Pan.Unbind();
mappings.ItemContainer.Selection.Unbind();
mappings.ItemContainer.Drag.Unbind();
mappings.Connector.Connect.Unbind();
break;
case EditorInputMode.Default:
break;
}
}
public static void Apply(this EditorGestures value, EditorGesturesMappings mappings)
{
var newMappings = mappings.ToGesturesMappings();
value.Apply(newMappings);
}
public static EditorGestures ToGesturesMappings(this EditorGesturesMappings mappings)
{
return mappings switch
{
EditorGesturesMappings.Custom => new CustomGesturesMappings(),
_ => new EditorGestures()
};
}
}
public class CustomGesturesMappings : EditorGestures
{
public CustomGesturesMappings()
{
Editor.Pan.Value = new AnyGesture(new Interactivity.MouseGesture(MouseAction.LeftClick), new Interactivity.MouseGesture(MouseAction.MiddleClick));
Editor.ZoomModifierKey = ModifierKeys.Control;
Editor.Selection.Apply(new SelectionGestures(MouseAction.RightClick));
// comment to drag with right click - we copy the default gestures of the item container which uses left click for selection
ItemContainer.Drag.Value = new AnyGesture(ItemContainer.Selection.Replace.Value, ItemContainer.Selection.Remove.Value, ItemContainer.Selection.Append.Value, ItemContainer.Selection.Invert.Value);
ItemContainer.Selection.Apply(Editor.Selection);
}
}
}

View File

@@ -0,0 +1,951 @@
using Nodify.Interactivity;
using System.Collections.Generic;
using System.Windows;
namespace Nodify.Playground
{
public enum ConnectionStyle
{
Default,
Line,
Circuit,
Step
}
public class EditorSettings : ObservableObject
{
private readonly IReadOnlyCollection<ISettingViewModel> _settings;
public IEnumerable<ISettingViewModel> Settings => PlaygroundSettings.Instance.FilterAndSort(_settings);
private readonly IReadOnlyCollection<ISettingViewModel> _advancedSettings;
public IEnumerable<ISettingViewModel> AdvancedSettings => PlaygroundSettings.Instance.FilterAndSort(_advancedSettings);
private EditorSettings()
{
PlaygroundSettings.Instance.PropertyChanged += OnSearchTextChanged;
_settings = new List<ISettingViewModel>()
{
new ProxySettingViewModel<bool>(
() => Instance.EnableRealtimeSelection,
val => Instance.EnableRealtimeSelection = val,
"Realtime selection: ",
"Selects items when finished if disabled."),
new ProxySettingViewModel<bool>(
() => Instance.SelectableNodes,
val => Instance.SelectableNodes = val,
"Selectable nodes: ",
"Whether nodes can be selected."),
new ProxySettingViewModel<bool>(
() => Instance.DraggableNodes,
val => Instance.DraggableNodes= val,
"Draggable nodes: ",
"Whether nodes can be dragged."),
new ProxySettingViewModel<bool>(
() => Instance.CanSelectMultipleNodes,
val => Instance.CanSelectMultipleNodes = val,
"Can select multiple nodes: "),
new ProxySettingViewModel<bool>(
() => Instance.EnablePendingConnectionSnapping,
val => Instance.EnablePendingConnectionSnapping = val,
"Pending connection snapping: ",
"Whether to snap the pending connection to connectors"),
new ProxySettingViewModel<bool>(
() => Instance.EnablePendingConnectionPreview,
val => Instance.EnablePendingConnectionPreview = val,
"Pending connection preview: ",
"Show information about the pending connection."),
new ProxySettingViewModel<bool>(
() => Instance.AllowConnectingToConnectorsOnly,
val => Instance.AllowConnectingToConnectorsOnly = val,
"Disable drop connection on node: ",
"Can connect directly to nodes if enabled"),
new ProxySettingViewModel<bool>(
() => Instance.DisableAutoPanning,
val => Instance.DisableAutoPanning = val,
"Disable auto panning: "),
new ProxySettingViewModel<bool>(
() => Instance.DisablePanning,
val => Instance.DisablePanning = val,
"Disable panning: "),
new ProxySettingViewModel<bool>(
() => Instance.DisableZooming,
val => Instance.DisableZooming = val,
"Disable zooming: "),
new ProxySettingViewModel<uint>(
() => Instance.GridSpacing,
val => Instance.GridSpacing = val,
"Grid spacing: ",
"Snapping value in pixels"),
new ProxySettingViewModel<PointEditor>(
() => Instance.Location,
val => Instance.Location = val,
"Location: ",
"The viewport's location."),
new ProxySettingViewModel<double>(
() => Instance.Zoom,
val => Instance.Zoom = val,
"Zoom: ",
"The viewport's zoom. Not accurate when trying to zoom outside the MinViewportZoom and MaxViewportZoom because of dependency property coercion not updating the binding with the final result."),
new ProxySettingViewModel<double>(
() => Instance.MinZoom,
val => Instance.MinZoom = val,
"Min zoom: "),
new ProxySettingViewModel<double>(
() => Instance.MaxZoom,
val => Instance.MaxZoom = val,
"Max zoom: "),
new ProxySettingViewModel<double>(
() => Instance.AutoPanningSpeed,
val => Instance.AutoPanningSpeed = val,
"Auto panning speed: ",
"Speed value in pixels per tick"),
new ProxySettingViewModel<double>(
() => Instance.AutoPanningEdgeDistance,
val => Instance.AutoPanningEdgeDistance = val,
"Auto panning edge distance: ",
"Distance from edge to trigger auto panning"),
new ProxySettingViewModel<bool>(
() => Instance.EnableStickyConnectors,
val => Instance.EnableStickyConnectors = val,
"Enable sticky connectors: ",
"The connection can be completed in two steps (e.g. click to create pending connection, click to connect)"),
new ProxySettingViewModel<bool>(
() => Instance.SelectableConnections,
val => Instance.SelectableConnections = val,
"Selectable connections: ",
"Whether connections can be selected."),
new ProxySettingViewModel<bool>(
() => Instance.CanSelectMultipleConnections,
val => Instance.CanSelectMultipleConnections = val,
"Can select multiple connections: "),
new ProxySettingViewModel<ConnectionStyle>(
() => Instance.ConnectionStyle,
val => Instance.ConnectionStyle = val,
"Connection style: "),
new ProxySettingViewModel<string?>(
() => Instance.ConnectionText,
val => Instance.ConnectionText = val,
"Connection text: "),
new ProxySettingViewModel<double>(
() => Instance.CircuitConnectionAngle,
val => Instance.CircuitConnectionAngle = val,
"Connection angle: ",
"Applies to circuit connection style"),
new ProxySettingViewModel<double>(
() => Instance.ConnectionCornerRadius,
val => Instance.ConnectionCornerRadius = val,
"Connection corner radius: ",
"The corner radius between the line segments."),
new ProxySettingViewModel<double>(
() => Instance.ConnectionSpacing,
val => Instance.ConnectionSpacing = val,
"Connection spacing: ",
"The distance between the start point and the where the angle breaks"),
new ProxySettingViewModel<PointEditor>(
() => Instance.ConnectionArrowSize,
val => Instance.ConnectionArrowSize = val,
"Connection arrowhead size: ",
"The size of the arrowhead."),
new ProxySettingViewModel<uint>(
() => Instance.DirectionalArrowsCount,
val => Instance.DirectionalArrowsCount = val,
"Directional arrows count: ",
"The number of arrowheads to draw on the line flowing in the direction of the connection."),
new ProxySettingViewModel<double>(
() => Instance.DirectionalArrowsOffset,
val => Instance.DirectionalArrowsOffset = val,
"Directional arrows offset: ",
"Used to animate the directional arrowheads flowing in the direction of the connection (value is between 0 and 1)."),
new ProxySettingViewModel<bool>(
() => Instance.IsAnimatingConnections,
val => Instance.IsAnimatingConnections = val,
"Animate directional arrows: ",
"Used to animate the directional arrowheads by animating the DirectionalArrowsOffset value"),
new ProxySettingViewModel<double>(
() => Instance.DirectionalArrowsAnimationDuration,
val => Instance.DirectionalArrowsAnimationDuration = val,
"Arrows animation duration: ",
"The duration in seconds of a directional arrowhead flowing from start to end."),
new ProxySettingViewModel<ArrowHeadEnds>(
() => Instance.ArrowHeadEnds,
val => Instance.ArrowHeadEnds = val,
"Connection arrowhead end: ",
"The location of the arrowhead."),
new ProxySettingViewModel<ArrowHeadShape>(
() => Instance.ArrowHeadShape,
val => Instance.ArrowHeadShape = val,
"Connection arrowhead shape: ",
"The shape of the arrow head."),
new ProxySettingViewModel<ConnectionOffsetMode>(
() => Instance.ConnectionSourceOffsetMode,
val => Instance.ConnectionSourceOffsetMode = val,
"Connection source offset mode: "),
new ProxySettingViewModel<ConnectionOffsetMode>(
() => Instance.ConnectionTargetOffsetMode,
val => Instance.ConnectionTargetOffsetMode = val,
"Connection target offset mode: "),
new ProxySettingViewModel<PointEditor>(
() => Instance.ConnectionSourceOffset,
val => Instance.ConnectionSourceOffset = val,
"Connection source offset: "),
new ProxySettingViewModel<PointEditor>(
() => Instance.ConnectionTargetOffset,
val => Instance.ConnectionTargetOffset = val,
"Connection target offset: "),
new ProxySettingViewModel<double>(
() => Instance.ConnectionStrokeThickness,
val => Instance.ConnectionStrokeThickness = val,
"Connection stroke thickness: "),
new ProxySettingViewModel<double>(
() => Instance.ConnectionOutlineThickness,
val => Instance.ConnectionOutlineThickness = val,
"Connection outline thickness: "),
new ProxySettingViewModel<double>(
() => Instance.ConnectionFocusVisualPadding,
val => Instance.ConnectionFocusVisualPadding = val,
"Connection focus visual padding: "),
new ProxySettingViewModel<bool>(
() => Instance.DisplayConnectionsOnTop,
val => Instance.DisplayConnectionsOnTop = val,
"Display connections on top: "),
new ProxySettingViewModel<double>(
() => Instance.BringIntoViewSpeed,
val => Instance.BringIntoViewSpeed = val,
"Bring into view speed: ",
"Bring location into view animation speed in pixels per second"),
new ProxySettingViewModel<double>(
() => Instance.BringIntoViewMaxDuration,
val => Instance.BringIntoViewMaxDuration = val,
"Bring into view max duration: ",
"Bring location into view max animation duration"),
new ProxySettingViewModel<GroupingMovementMode>(
() => Instance.GroupingNodeMovement,
val => Instance.GroupingNodeMovement = val,
"Grouping node movement: ",
"Whether the grouping node is sticky or not"),
};
_advancedSettings = new List<ISettingViewModel>()
{
new ProxySettingViewModel<uint>(
() => Instance.MaxHotKeys,
val => Instance.MaxHotKeys = val,
"Max hot keys: ",
"The maximum number of generated hot keys"),
new ProxySettingViewModel<HotKeysDisplayMode>(
() => Instance.HotKeysDisplayMode,
val => Instance.HotKeysDisplayMode = val,
"Hot keys display mode: ",
"Specifies how hotkeys are displayed for a pending connection."),
new ProxySettingViewModel<double>(
() => Instance.MouseActionSuppressionThreshold,
val => Instance.MouseActionSuppressionThreshold = val,
"Context menu suppression threshold: ",
"Disable context menu after mouse moved this far."),
new ProxySettingViewModel<bool>(
() => Instance.PreserveSelectionOnRightClick,
val => Instance.PreserveSelectionOnRightClick = val,
"Preserve selection on right click: ",
"Whether right-click on the container should preserve the current selection."),
new ProxySettingViewModel<double>(
() => Instance.AutoPanningTickRate,
val => Instance.AutoPanningTickRate = val,
"Auto panning tick rate: ",
"How often is the new position calculated in milliseconds. Disable and enable auto panning for this to have effect."),
new ProxySettingViewModel<bool>(
() => Instance.EnableSnappingCorrection,
val => Instance.EnableSnappingCorrection = val,
"Enable snapping correction: ",
"Correct the final position when moving a selection."),
new ProxySettingViewModel<bool>(
() => Instance.EnableCuttingLinePreview,
val => Instance.EnableCuttingLinePreview = val,
"Enable cutting line preview: ",
"Applies custom connection style on intersection (hurts performance due to hit testing)."),
new ProxySettingViewModel<bool>(
() => Instance.EnablePendingConnectionHitTesting,
val => Instance.EnablePendingConnectionHitTesting = val,
"Enable pending connection hit testing: ",
"Disable to improve performance."),
new ProxySettingViewModel<bool>(
() => Instance.EnableConnectorOptimizations,
val => Instance.EnableConnectorOptimizations = val,
"Enable connector optimizations: ",
"Enables optimizations for connectors based on viewport distance and minimum selected nodes."),
new ProxySettingViewModel<double>(
() => Instance.OptimizeSafeZone,
val => Instance.OptimizeSafeZone = val,
"Optimize connectors safe zone: ",
"The minimum distance from the viewport where connectors will start optimizing"),
new ProxySettingViewModel<uint>(
() => Instance.OptimizeMinimumSelectedItems,
val => Instance.OptimizeMinimumSelectedItems = val,
"Optimize connectors minimum selection: ",
"The minimum selected items needed to start optimizing when outside the safe zone."),
new ProxySettingViewModel<bool>(
() => Instance.EnableRenderingOptimizations,
val => Instance.EnableRenderingOptimizations = val,
"Enable nodes rendering optimization: ",
"Enables rendering optimizations for nodes based on zoom out percent and nodes count. (zoom in/out to apply optimization)"),
new ProxySettingViewModel<double>(
() => Instance.OptimizeRenderingZoomOutPercent,
val => Instance.OptimizeRenderingZoomOutPercent = val,
"Rendering optimization zoom out percent: ",
"The zoom out percent that triggers the optimization. (percent of x = 1 - MinViewportZoom)"),
new ProxySettingViewModel<uint>(
() => Instance.OptimizeRenderingMinimumNodes,
val => Instance.OptimizeRenderingMinimumNodes = val,
"Rendering optimization minimum nodes: ",
"The minimum nodes needed to start optimizing when zoom out percent is met."),
new ProxySettingViewModel<bool>(
() => Instance.EnableDraggingOptimizations,
val => Instance.EnableDraggingOptimizations = val,
"Enable nodes dragging optimizations: ",
"Simulates dragging visually but only commits the changes at the end."),
new ProxySettingViewModel<double>(
() => Instance.FitToScreenExtentMargin,
val => Instance.FitToScreenExtentMargin = val,
"Fit to screen extent margin: ",
"Adds some margin to the nodes extent when fit to screen"),
new ProxySettingViewModel<bool>(
() => Instance.AllowMinimapPanningCancellation,
val => Instance.AllowMinimapPanningCancellation = val,
"Allow minimap panning cancellation: ",
"Right click or escape to cancel panning."),
new ProxySettingViewModel<bool>(
() => Instance.AllowCuttingCancellation,
val => Instance.AllowCuttingCancellation = val,
"Allow cutting cancellation: ",
"Right click to cancel cutting."),
new ProxySettingViewModel<bool>(
() => Instance.AllowPushItemsCancellation,
val => Instance.AllowPushItemsCancellation = val,
"Allow push nodes cancellation: ",
"Right click to cancel pushing nodes."),
new ProxySettingViewModel<bool>(
() => Instance.AllowPanningCancellation,
val => Instance.AllowPanningCancellation= val,
"Allow panning cancellation: ",
"Press Escape to cancel panning."),
new ProxySettingViewModel<bool>(
() => Instance.AllowSelectionCancellation,
val => Instance.AllowSelectionCancellation = val,
"Allow selection cancellation: ",
"Press Escape to cancel selecting."),
new ProxySettingViewModel<bool>(
() => Instance.AllowDraggingCancellation,
val => Instance.AllowDraggingCancellation = val,
"Allow dragging cancellation: ",
"Right click to cancel dragging."),
new ProxySettingViewModel<bool>(
() => Instance.AllowPendingConnectionCancellation,
val => Instance.AllowPendingConnectionCancellation = val,
"Allow pending connection cancellation: ",
"Right click to cancel pending connection."),
new ProxySettingViewModel<bool>(
() => Instance.EnableToggledCutting,
val => Instance.EnableToggledCutting = val,
"Enable toggled cutting mode: ",
"The interaction will be completed in two steps using the same gesture to start and end."),
new ProxySettingViewModel<bool>(
() => Instance.EnableToggledPushingItems,
val => Instance.EnableToggledPushingItems = val,
"Enable toggled pushing items mode: ",
"The interaction will be completed in two steps using the same gesture to start and end."),
new ProxySettingViewModel<bool>(
() => Instance.EnableToggledPanning,
val => Instance.EnableToggledPanning = val,
"Enable toggled panning mode: ",
"The interaction will be completed in two steps using the same gesture to start and end."),
new ProxySettingViewModel<bool>(
() => Instance.EnableToggledSelecting,
val => Instance.EnableToggledSelecting = val,
"Enable toggled selecting mode: ",
"The interaction will be completed in two steps using the same gesture to start and end."),
new ProxySettingViewModel<bool>(
() => Instance.EnableToggledDragging,
val => Instance.EnableToggledDragging = val,
"Enable toggled dragging mode: ",
"The interaction will be completed in two steps using the same gesture to start and end."),
new ProxySettingViewModel<bool>(
() => Instance.EnableMinimapToggledPanning,
val => Instance.EnableMinimapToggledPanning = val,
"Enable minimap toggled panning mode: ",
"The interaction will be completed in two steps using the same gesture to start and end."),
new ProxySettingViewModel<bool>(
() => Instance.AllowPanningWhileSelecting,
val => Instance.AllowPanningWhileSelecting = val,
"Allow panning while selecting: ",
"Whether panning is allowed while selecting items in the editor."),
new ProxySettingViewModel<bool>(
() => Instance.AllowPanningWhileCutting,
val => Instance.AllowPanningWhileCutting = val,
"Allow panning while cutting: ",
"Whether panning is allowed while cutting connections in the editor."),
new ProxySettingViewModel<bool>(
() => Instance.AllowPanningWhilePushingItems,
val => Instance.AllowPanningWhilePushingItems = val,
"Allow panning while pushing items: ",
"Whether panning is allowed while pushing items items in the editor."),
new ProxySettingViewModel<bool>(
() => Instance.AllowZoomingWhileSelecting,
val => Instance.AllowZoomingWhileSelecting = val,
"Allow zooming while selecting: ",
"Whether zooming is allowed while selecting items in the editor."),
new ProxySettingViewModel<bool>(
() => Instance.AllowZoomingWhileCutting,
val => Instance.AllowZoomingWhileCutting = val,
"Allow zooming while cutting: ",
"Whether zooming is allowed while cutting connections in the editor."),
new ProxySettingViewModel<bool>(
() => Instance.AllowZoomingWhilePushingItems,
val => Instance.AllowZoomingWhilePushingItems = val,
"Allow zooming while pushing items: ",
"Whether zooming is allowed while pushing items connections in the editor."),
new ProxySettingViewModel<bool>(
() => Instance.AllowZoomingWhilePanning,
val => Instance.AllowZoomingWhilePanning = val,
"Allow zooming while panning: ",
"Whether zooming is allowed while panning connections in the editor."),
};
EnableCuttingLinePreview = true;
}
private void OnSearchTextChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(PlaygroundSettings.SearchText))
{
OnPropertyChanged(nameof(Settings));
OnPropertyChanged(nameof(AdvancedSettings));
}
}
public static EditorSettings Instance { get; } = new EditorSettings();
#region Default settings
private bool _enablePendingConnectionSnapping = true;
public bool EnablePendingConnectionSnapping
{
get => _enablePendingConnectionSnapping;
set => SetProperty(ref _enablePendingConnectionSnapping, value);
}
private bool _enablePendingConnectionPreview = true;
public bool EnablePendingConnectionPreview
{
get => _enablePendingConnectionPreview;
set => SetProperty(ref _enablePendingConnectionPreview, value);
}
private bool _allowConnectingToConnectorsOnly;
public bool AllowConnectingToConnectorsOnly
{
get => _allowConnectingToConnectorsOnly;
set => SetProperty(ref _allowConnectingToConnectorsOnly, value);
}
private bool _realtimeSelection = true;
public bool EnableRealtimeSelection
{
get => _realtimeSelection;
set => SetProperty(ref _realtimeSelection, value);
}
private bool _disableAutoPanning = false;
public bool DisableAutoPanning
{
get => _disableAutoPanning;
set => SetProperty(ref _disableAutoPanning, value);
}
private double _autoPanningSpeed = 15d;
public double AutoPanningSpeed
{
get => _autoPanningSpeed;
set => SetProperty(ref _autoPanningSpeed, value);
}
private double _autoPanningEdgeDistance = 15d;
public double AutoPanningEdgeDistance
{
get => _autoPanningEdgeDistance;
set => SetProperty(ref _autoPanningEdgeDistance, value);
}
private bool _disablePanning = false;
public bool DisablePanning
{
get => _disablePanning;
set => SetProperty(ref _disablePanning, value);
}
private bool _disableZooming = false;
public bool DisableZooming
{
get => _disableZooming;
set => SetProperty(ref _disableZooming, value);
}
private uint _gridSpacing = 15u;
public uint GridSpacing
{
get => _gridSpacing;
set => SetProperty(ref _gridSpacing, value);
}
private double _minZoom = 0.1;
public double MinZoom
{
get => _minZoom;
set => SetProperty(ref _minZoom, value);
}
private double _maxZoom = 2;
public double MaxZoom
{
get => _maxZoom;
set => SetProperty(ref _maxZoom, value);
}
private double _zoom = 1;
public double Zoom
{
get => _zoom;
set => SetProperty(ref _zoom, value);
}
private PointEditor _location = new PointEditor();
public PointEditor Location
{
get => _location;
set => SetProperty(ref _location, value);
}
private bool _selectableConnections = true;
public bool SelectableConnections
{
get => _selectableConnections;
set => SetProperty(ref _selectableConnections, value);
}
private bool _canSelectMultipleConnections = true;
public bool CanSelectMultipleConnections
{
get => _canSelectMultipleConnections;
set => SetProperty(ref _canSelectMultipleConnections, value);
}
private bool _draggableNodes = true;
public bool DraggableNodes
{
get => _draggableNodes;
set => SetProperty(ref _draggableNodes, value);
}
private bool _selectableNodes = true;
public bool SelectableNodes
{
get => _selectableNodes;
set => SetProperty(ref _selectableNodes, value);
}
private bool _canSelectMultipleNodes = true;
public bool CanSelectMultipleNodes
{
get => _canSelectMultipleNodes;
set => SetProperty(ref _canSelectMultipleNodes, value);
}
private ConnectionStyle _connectionStyle;
public ConnectionStyle ConnectionStyle
{
get => _connectionStyle;
set => SetProperty(ref _connectionStyle, value);
}
private string? _connectionText;
public string? ConnectionText
{
get => _connectionText;
set => SetProperty(ref _connectionText, value);
}
private double _circuitConnectionAngle = 45;
public double CircuitConnectionAngle
{
get => _circuitConnectionAngle;
set => SetProperty(ref _circuitConnectionAngle, value);
}
private double _connectionCornerRadius = 10;
public double ConnectionCornerRadius
{
get => _connectionCornerRadius;
set => SetProperty(ref _connectionCornerRadius, value);
}
private double _connectionSpacing = 20;
public double ConnectionSpacing
{
get => _connectionSpacing;
set => SetProperty(ref _connectionSpacing, value);
}
private ConnectionOffsetMode _srcConnectionOffsetMode = ConnectionOffsetMode.Static;
public ConnectionOffsetMode ConnectionSourceOffsetMode
{
get => _srcConnectionOffsetMode;
set => SetProperty(ref _srcConnectionOffsetMode, value);
}
private ConnectionOffsetMode _targetConnectionOffsetMode = ConnectionOffsetMode.Static;
public ConnectionOffsetMode ConnectionTargetOffsetMode
{
get => _targetConnectionOffsetMode;
set => SetProperty(ref _targetConnectionOffsetMode, value);
}
private ArrowHeadEnds _arrowHeadEnds = ArrowHeadEnds.End;
public ArrowHeadEnds ArrowHeadEnds
{
get => _arrowHeadEnds;
set => SetProperty(ref _arrowHeadEnds, value);
}
private ArrowHeadShape _arrowHeadShape = ArrowHeadShape.Arrowhead;
public ArrowHeadShape ArrowHeadShape
{
get => _arrowHeadShape;
set => SetProperty(ref _arrowHeadShape, value);
}
private PointEditor _connectionSourceOffset = new Size(14, 0);
public PointEditor ConnectionSourceOffset
{
get => _connectionSourceOffset;
set => SetProperty(ref _connectionSourceOffset, value);
}
private PointEditor _connectionTargetOffset = new Size(14, 0);
public PointEditor ConnectionTargetOffset
{
get => _connectionTargetOffset;
set => SetProperty(ref _connectionTargetOffset, value);
}
private double _connectionStrokeThickness = 3;
public double ConnectionStrokeThickness
{
get => _connectionStrokeThickness;
set => SetProperty(ref _connectionStrokeThickness, value);
}
private double _connectionOutlineThickness = 5;
public double ConnectionOutlineThickness
{
get => _connectionOutlineThickness;
set => SetProperty(ref _connectionOutlineThickness, value);
}
private double _connectionFocusVisualPadding = 1;
public double ConnectionFocusVisualPadding
{
get => _connectionFocusVisualPadding;
set => SetProperty(ref _connectionFocusVisualPadding, value);
}
private uint _directionalArrowsCount = 3;
public uint DirectionalArrowsCount
{
get => _directionalArrowsCount;
set => SetProperty(ref _directionalArrowsCount, value);
}
private double _directionalArrowsOffset;
public double DirectionalArrowsOffset
{
get => _directionalArrowsOffset;
set => SetProperty(ref _directionalArrowsOffset, value);
}
private bool _isAnimatingConnections;
public bool IsAnimatingConnections
{
get => _isAnimatingConnections;
set => SetProperty(ref _isAnimatingConnections, value);
}
private double _directionalArrowsAnimationDuration = 2.0;
public double DirectionalArrowsAnimationDuration
{
get => _directionalArrowsAnimationDuration;
set => SetProperty(ref _directionalArrowsAnimationDuration, value);
}
private PointEditor _connectionArrowSize = new Size(8, 8);
public PointEditor ConnectionArrowSize
{
get => _connectionArrowSize;
set => SetProperty(ref _connectionArrowSize, value);
}
private bool _displayConnectionsOnTop;
public bool DisplayConnectionsOnTop
{
get => _displayConnectionsOnTop;
set => SetProperty(ref _displayConnectionsOnTop, value);
}
private double _bringIntoViewSpeed = 1000;
public double BringIntoViewSpeed
{
get => _bringIntoViewSpeed;
set => SetProperty(ref _bringIntoViewSpeed, value);
}
private double _bringIntoViewMaxDuration = 1;
public double BringIntoViewMaxDuration
{
get => _bringIntoViewMaxDuration;
set => SetProperty(ref _bringIntoViewMaxDuration, value);
}
private GroupingMovementMode _groupingNodeMovement;
public GroupingMovementMode GroupingNodeMovement
{
get => _groupingNodeMovement;
set => SetProperty(ref _groupingNodeMovement, value);
}
#endregion
#region Advanced settings
public uint MaxHotKeys
{
get => PendingConnection.MaxHotKeys;
set => PendingConnection.MaxHotKeys = value;
}
public HotKeysDisplayMode HotKeysDisplayMode
{
get => PendingConnection.HotKeysDisplayMode;
set => PendingConnection.HotKeysDisplayMode = value;
}
public bool PreserveSelectionOnRightClick
{
get => ItemContainer.PreserveSelectionOnRightClick;
set => ItemContainer.PreserveSelectionOnRightClick = value;
}
public double MouseActionSuppressionThreshold
{
get => NodifyEditor.MouseActionSuppressionThreshold;
set => NodifyEditor.MouseActionSuppressionThreshold = value;
}
public double AutoPanningTickRate
{
get => NodifyEditor.AutoPanningTickRate;
set => NodifyEditor.AutoPanningTickRate = value;
}
public bool AllowMinimapPanningCancellation
{
get => Minimap.AllowPanningCancellation;
set => Minimap.AllowPanningCancellation = value;
}
public bool AllowCuttingCancellation
{
get => NodifyEditor.AllowCuttingCancellation;
set => NodifyEditor.AllowCuttingCancellation = value;
}
public bool AllowPushItemsCancellation
{
get => NodifyEditor.AllowPushItemsCancellation;
set => NodifyEditor.AllowPushItemsCancellation = value;
}
public bool AllowPanningCancellation
{
get => NodifyEditor.AllowPanningCancellation;
set => NodifyEditor.AllowPanningCancellation = value;
}
public bool AllowSelectionCancellation
{
get => NodifyEditor.AllowSelectionCancellation;
set => NodifyEditor.AllowSelectionCancellation = value;
}
public bool AllowDraggingCancellation
{
get => NodifyEditor.AllowDraggingCancellation;
set => NodifyEditor.AllowDraggingCancellation = value;
}
public bool AllowPendingConnectionCancellation
{
get => Connector.AllowPendingConnectionCancellation;
set => Connector.AllowPendingConnectionCancellation = value;
}
public bool EnableSnappingCorrection
{
get => NodifyEditor.EnableSnappingCorrection;
set => NodifyEditor.EnableSnappingCorrection = value;
}
public bool EnableCuttingLinePreview
{
get => NodifyEditor.EnableCuttingLinePreview;
set => NodifyEditor.EnableCuttingLinePreview = value;
}
public bool EnablePendingConnectionHitTesting
{
get => PendingConnection.EnableHitTesting;
set => PendingConnection.EnableHitTesting = value;
}
public bool EnableConnectorOptimizations
{
get => Connector.EnableOptimizations;
set => Connector.EnableOptimizations = value;
}
public double OptimizeSafeZone
{
get => Connector.OptimizeSafeZone;
set => Connector.OptimizeSafeZone = value;
}
public uint OptimizeMinimumSelectedItems
{
get => Connector.OptimizeMinimumSelectedItems;
set => Connector.OptimizeMinimumSelectedItems = value;
}
public bool EnableRenderingOptimizations
{
get => NodifyEditor.EnableRenderingContainersOptimizations;
set => NodifyEditor.EnableRenderingContainersOptimizations = value;
}
public uint OptimizeRenderingMinimumNodes
{
get => NodifyEditor.OptimizeRenderingMinimumContainers;
set => NodifyEditor.OptimizeRenderingMinimumContainers = value;
}
public double OptimizeRenderingZoomOutPercent
{
get => NodifyEditor.OptimizeRenderingZoomOutPercent;
set => NodifyEditor.OptimizeRenderingZoomOutPercent = value;
}
public double FitToScreenExtentMargin
{
get => NodifyEditor.FitToScreenExtentMargin;
set => NodifyEditor.FitToScreenExtentMargin = value;
}
public bool EnableDraggingOptimizations
{
get => NodifyEditor.EnableDraggingContainersOptimizations;
set => NodifyEditor.EnableDraggingContainersOptimizations = value;
}
public bool EnableStickyConnectors
{
get => ConnectorState.EnableToggledConnectingMode;
set => ConnectorState.EnableToggledConnectingMode = value;
}
public bool EnableToggledPanning
{
get => EditorState.EnableToggledPanningMode;
set => EditorState.EnableToggledPanningMode = value;
}
public bool EnableToggledCutting
{
get => EditorState.EnableToggledCuttingMode;
set => EditorState.EnableToggledCuttingMode = value;
}
public bool EnableToggledPushingItems
{
get => EditorState.EnableToggledPushingItemsMode;
set => EditorState.EnableToggledPushingItemsMode = value;
}
public bool EnableToggledSelecting
{
get => EditorState.EnableToggledSelectingMode;
set => EditorState.EnableToggledSelectingMode = value;
}
public bool EnableToggledDragging
{
get => ContainerState.EnableToggledDraggingMode;
set => ContainerState.EnableToggledDraggingMode = value;
}
public bool EnableMinimapToggledPanning
{
get => MinimapState.EnableToggledPanningMode;
set => MinimapState.EnableToggledPanningMode = value;
}
public bool AllowPanningWhileSelecting
{
get => EditorState.AllowPanningWhileSelecting;
set => EditorState.AllowPanningWhileSelecting = value;
}
public bool AllowPanningWhileCutting
{
get => EditorState.AllowPanningWhileCutting;
set => EditorState.AllowPanningWhileCutting = value;
}
public bool AllowPanningWhilePushingItems
{
get => EditorState.AllowPanningWhilePushingItems;
set => EditorState.AllowPanningWhilePushingItems = value;
}
public bool AllowZoomingWhileSelecting
{
get => EditorState.AllowZoomingWhileSelecting;
set => EditorState.AllowZoomingWhileSelecting = value;
}
public bool AllowZoomingWhileCutting
{
get => EditorState.AllowZoomingWhileCutting;
set => EditorState.AllowZoomingWhileCutting = value;
}
public bool AllowZoomingWhilePushingItems
{
get => EditorState.AllowZoomingWhilePushingItems;
set => EditorState.AllowZoomingWhilePushingItems = value;
}
public bool AllowZoomingWhilePanning
{
get => EditorState.AllowZoomingWhilePanning;
set => EditorState.AllowZoomingWhilePanning = value;
}
#endregion
}
}

View File

@@ -0,0 +1,47 @@
<UserControl x:Class="Nodify.Playground.EditorSettingsView"
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:Nodify.Playground"
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
d:Foreground="{DynamicResource ForegroundBrush}"
d:Background="{DynamicResource PanelBackgroundBrush}"
mc:Ignorable="d">
<StackPanel>
<Border BorderThickness="1"
Padding="10"
HorizontalAlignment="Stretch">
<local:SettingsView Items="{Binding Source={x:Static local:EditorSettings.Instance}, Path=Settings}"/>
</Border>
<Expander
Header="Advanced"
Padding="0 5 0 0"
BorderThickness="0 0 0 1"
IsExpanded="True"
BorderBrush="{DynamicResource BackgroundBrush}">
<Expander.Style>
<Style TargetType="{x:Type Expander}"
BasedOn="{StaticResource {x:Type Expander}}">
<Setter Property="Tag"
Value="{StaticResource ExpandRightIcon}" />
<Style.Triggers>
<Trigger Property="IsExpanded"
Value="True">
<Setter Property="Tag"
Value="{StaticResource ExpandDownIcon}" />
</Trigger>
</Style.Triggers>
</Style>
</Expander.Style>
<Border BorderThickness="1"
Padding="10"
HorizontalAlignment="Stretch">
<local:SettingsView Items="{Binding Source={x:Static local:EditorSettings.Instance}, Path=AdvancedSettings}"/>
</Border>
</Expander>
</StackPanel>
</UserControl>

View File

@@ -0,0 +1,26 @@
using System;
using System.Collections.Generic;
using System.Text;
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;
namespace Nodify.Playground
{
/// <summary>
/// Interaction logic for EditorSettingsView.xaml
/// </summary>
public partial class EditorSettingsView : UserControl
{
public EditorSettingsView()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,76 @@
using System.Collections.Generic;
using System.Windows;
namespace Nodify.Playground
{
public static class NodeViewModelExtensions
{
public static Rect GetBoundingBox(this IList<NodeViewModel> nodes, double padding = 0, int gridCellSize = 15)
{
double minX = double.MaxValue;
double minY = double.MaxValue;
double maxX = double.MinValue;
double maxY = double.MinValue;
for (int i = 0; i < nodes.Count; i++)
{
var node = nodes[i];
var width = 200; //node.Width
var height = 200; //node.Height
if (node.Location.X < minX)
{
minX = node.Location.X;
}
if (node.Location.Y < minY)
{
minY = node.Location.Y;
}
var sizeX = node.Location.X + width;
if (sizeX > maxX)
{
maxX = sizeX;
}
var sizeY = node.Location.Y + height;
if (sizeY > maxY)
{
maxY = sizeY;
}
}
var result = new Rect(minX - padding, minY - padding, maxX - minX + padding * 2, maxY - minY + padding * 2);
result.X = (int)result.X / gridCellSize * gridCellSize;
result.Y = (int)result.Y / gridCellSize * gridCellSize;
return result;
}
public static void AddRange<T>(this ICollection<T> col, IEnumerable<T> items)
{
if (items is IList<T> itemsCol)
{
for (int i = 0; i < itemsCol.Count; i++)
{
col.Add(itemsCol[i]);
}
}
else if (items is T[] itemsArr)
{
for (int i = 0; i < itemsArr.Length; i++)
{
col.Add(itemsArr[i]);
}
}
else
{
foreach (var item in items)
{
col.Add(item);
}
}
}
}
}

View File

@@ -0,0 +1,186 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
namespace Nodify.Playground
{
public struct NodesGeneratorSettings
{
private static readonly Random _rand = new Random();
public NodesGeneratorSettings(uint count)
{
GridSnap = 15;
MinNodesCount = MaxNodesCount = count;
MinInputCount = MinOutputCount = 0;
MaxInputCount = MaxOutputCount = 7;
ConnectorNameGenerator = (s, i) => $"{new string('C', (int)i % 5)} {i}";
NodeNameGenerator = (s, i) => $"Node {i}";
NodeLocationGenerator = (s, i) =>
{
static double EaseOut(double percent, double increment, double start, double end, double total)
=> -end * (increment /= total) * (increment - 2) + start;
var xDistanceBetweenNodes = _rand.Next(150, 350);
var yDistanceBetweenNodes = _rand.Next(200, 350);
var randSignX = _rand.Next(0, 100) > 50 ? 1 : -1;
var randSignY = _rand.Next(0, 100) > 50 ? 1 : -1;
var gridOffsetX = i * xDistanceBetweenNodes;
var gridOffsetY = i * yDistanceBetweenNodes;
var x = gridOffsetX * Math.Sin(xDistanceBetweenNodes * randSignX / (i + 1));
var y = gridOffsetY * Math.Sin(yDistanceBetweenNodes * randSignY / (i + 1));
var easeX = x * EaseOut(i / count, i, 1, 0.01, count);
var easeY = y * EaseOut(i / count, i, 1, 0.01, count);
x = s.Snap((int)easeX);
y = s.Snap((int)easeY);
return new Point(x, y);
};
}
public uint GridSnap;
public uint MinNodesCount;
public uint MaxNodesCount;
public uint MinInputCount;
public uint MaxInputCount;
public uint MinOutputCount;
public uint MaxOutputCount;
public Func<NodesGeneratorSettings, uint, string?> ConnectorNameGenerator;
public Func<NodesGeneratorSettings, uint, string?> NodeNameGenerator;
public Func<NodesGeneratorSettings, uint, Point> NodeLocationGenerator;
public int Snap(int x)
=> x / (int)GridSnap * (int)GridSnap;
}
public static class RandomNodesGenerator
{
private static readonly Random _rand = new Random();
public static List<T> GenerateNodes<T>(NodesGeneratorSettings settings)
where T : FlowNodeViewModel, new()
{
var nodes = new List<T>();
var count = _rand.Next((int)settings.MinNodesCount, (int)settings.MaxNodesCount + 1);
for (uint i = 0; i < count; i++)
{
var node = new T
{
Title = settings.NodeNameGenerator(settings, i),
Location = settings.NodeLocationGenerator(settings, i)
};
nodes.Add(node);
node.Input.AddRange(GenerateConnectors(settings, _rand.Next((int)settings.MinInputCount, (int)settings.MaxInputCount + 1)));
node.Output.AddRange(GenerateConnectors(settings, _rand.Next((int)settings.MinOutputCount, (int)settings.MaxOutputCount + 1)));
}
return nodes;
}
public static List<ConnectionViewModel> GenerateConnections(IList<NodeViewModel> nodes)
{
HashSet<NodeViewModel> visited = new HashSet<NodeViewModel>(nodes.Count);
List<ConnectionViewModel> connections = new List<ConnectionViewModel>(nodes.Count);
for (uint i = 0; i < nodes.Count; i++)
{
var n1 = nodes[_rand.Next(0, nodes.Count)];
var n2 = nodes[_rand.Next(0, nodes.Count)];
if (n1 == n2 && !(visited.Add(n1) && visited.Add(n2)))
{
continue;
}
List<ConnectorViewModel> input = n1 is FlowNodeViewModel flow ? flow.Input.ToList() :
n1 is KnotNodeViewModel knot ? new List<ConnectorViewModel> { knot.Connector } : new List<ConnectorViewModel>();
List<ConnectorViewModel> output = n2 is FlowNodeViewModel flow2 ? flow2.Output.ToList() :
n2 is KnotNodeViewModel knot2 ? new List<ConnectorViewModel> { knot2.Connector } : new List<ConnectorViewModel>();
connections.AddRange(ConnectPins(input, output));
}
return connections;
}
public static List<ConnectionViewModel> ConnectPins(IList<ConnectorViewModel> source, IList<ConnectorViewModel> target)
{
Dictionary<ConnectorViewModel, List<ConnectorViewModel>> connections = new Dictionary<ConnectorViewModel, List<ConnectorViewModel>>();
List<ConnectionViewModel> result = new List<ConnectionViewModel>();
for (int di = 0; di < target.Count; di++)
{
if (source.Count > 1 && target.Count > 1 && _rand.Next() % 2 == 0)
{
continue;
}
var outP = target[di];
if (!connections.TryGetValue(outP, out var outConns))
{
var newList = new List<ConnectorViewModel>();
connections.Add(outP, newList);
outConns = newList;
}
var conNum = _rand.Next(0, source.Count + 1);
for (uint ci = 0; ci < conNum; ci++)
{
var inP = source[_rand.Next(0, conNum)];
if (!connections.TryGetValue(inP, out var inConns))
{
var newList = new List<ConnectorViewModel>();
connections.Add(inP, newList);
inConns = newList;
}
if (!connections[inP].Contains(outP) && !connections[outP].Contains(inP))
{
var isInput = inP.Flow == ConnectorFlow.Input;
var connection = new ConnectionViewModel
{
Input = isInput ? inP : outP,
Output = isInput ? outP : inP
};
result.Add(connection);
inConns.Add(outP);
outConns.Add(inP);
}
}
}
return result;
}
public static List<ConnectorViewModel> GenerateConnectors(NodesGeneratorSettings settings, int count)
{
var list = new List<ConnectorViewModel>(count);
for (uint i = 0; i < count; i++)
{
int shapeVal = _rand.Next() % 3;
var connector = new ConnectorViewModel
{
Title = settings.ConnectorNameGenerator(settings, i),
Shape = PlaygroundSettings.Instance.UseCustomConnectors ? (ConnectorShape)shapeVal : ConnectorShape.Circle
};
list.Add(connector);
}
return list;
}
}
}

View File

@@ -0,0 +1,24 @@
namespace Nodify.Playground
{
public enum SettingsType
{
Boolean,
Number,
Option,
Point,
Text
}
public interface ISettingViewModel
{
string Name { get; }
/// <summary>
/// Represents the content within the tooltip.
/// </summary>
string? Description { get; }
object? Value { get; set; }
SettingsType Type { get;}
}
}

View File

@@ -0,0 +1,275 @@
<Window x:Class="Nodify.Playground.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
xmlns:local="clr-namespace:Nodify.Playground"
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
xmlns:nodify="https://miroiu.github.io/nodify"
mc:Ignorable="d"
Title="MainWindow"
Height="700"
Width="1300">
<Window.Resources>
<shared:DebugConverter x:Key="DebugConverter" />
<shared:ToStringConverter x:Key="ToStringConverter" />
<shared:StringToVisibilityConverter x:Key="StringToVisibilityConverter" />
</Window.Resources>
<Window.DataContext>
<local:PlaygroundViewModel />
</Window.DataContext>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<local:NodifyEditorView x:Name="EditorView"
DataContext="{Binding GraphViewModel}"
Grid.RowSpan="3" />
<!--ACTIONS-->
<Border VerticalAlignment="Top"
Background="{DynamicResource PanelBackgroundBrush}"
Padding="10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<StackPanel Orientation="Horizontal">
<Button Command="{Binding GenerateRandomNodesCommand}"
Content="GENERATE NODES"
ToolTip="Generate nodes using the specified settings."
Style="{StaticResource HollowButton}" />
<Button Command="{Binding ToggleConnectionsCommand}"
Content="{Binding ConnectNodesText}"
ToolTip="Will add new connections if Connect Nodes is checked, otherwise it will disconnect nodes."
Style="{StaticResource HollowButton}" />
<Button Command="{Binding PerformanceTestCommand}"
Content="PERFORMANCE TEST"
ToolTip="You will encounter rendering performance issues. Try disabling the connections to see the difference."
Style="{StaticResource HollowButton}" />
<Button Command="{Binding ResetCommand}"
Content="RESET PLAYGROUND"
ToolTip="Reset the Location, Zoom, Nodes and Connections."
Style="{StaticResource HollowButton}" />
<Button Click="BringIntoView_Click"
Content="BRING INTO VIEW"
ToolTip="Bring a random node into view."
Style="{StaticResource HollowButton}" />
<Button Command="{x:Static nodify:EditorCommands.FitToScreen}"
Content="FIT TO SCREEN"
ToolTip="Scales the viewport to fit all nodes if that's possible."
CommandTarget="{Binding EditorInstance, ElementName=EditorView}"
Style="{StaticResource HollowButton}" />
<Button Command="{Binding GraphViewModel.CommentSelectionCommand}"
Content="COMMENT SELECTION"
ToolTip="Creates a comment node containing the selected nodes."
Style="{StaticResource HollowButton}" />
<Button Click="AnimateConnections_Click"
Content="TOGGLE CONNECTIONS ANIMATION"
ToolTip="Starts or stops animating the directional arrows on all connections (see DirectionalArrowsCount)"
Style="{StaticResource HollowButton}" />
</StackPanel>
<Button Style="{StaticResource IconButton}"
Content="{StaticResource ThemeIcon}"
Command="{Binding Source={x:Static shared:ThemeManager.SetNextThemeCommand}}"
ToolTip="Change theme"
Grid.Column="1" />
</Grid>
</Border>
<!--SETTINGS-->
<Expander Grid.Row="1"
HorizontalContentAlignment="Left"
VerticalContentAlignment="Center"
HorizontalAlignment="Left"
Background="{DynamicResource PanelBackgroundBrush}"
IsExpanded="True"
ExpandDirection="Left"
Padding="0 1 4 3">
<Expander.Style>
<Style TargetType="{x:Type Expander}"
BasedOn="{StaticResource {x:Type Expander}}">
<Setter Property="Tag"
Value="{StaticResource ExpandRightIcon}" />
<Style.Triggers>
<Trigger Property="IsExpanded"
Value="True">
<Setter Property="Tag"
Value="{StaticResource ExpandLeftIcon}" />
</Trigger>
</Style.Triggers>
</Style>
</Expander.Style>
<Grid>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" />
</Grid.RowDefinitions>
<StackPanel Margin="0 0 5 10">
<TextBlock Margin="0 0 0 3">Search:</TextBlock>
<TextBox Text="{Binding Source={x:Static local:PlaygroundSettings.Instance}, Path=SearchText, UpdateSourceTrigger=PropertyChanged, Mode=TwoWay}"
Padding="4" />
</StackPanel>
<ScrollViewer Grid.Row="1">
<Grid IsSharedSizeScope="True"
Width="330">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<Expander Header="Playground Settings"
Padding="0 5 0 0"
BorderThickness="0 0 0 1"
IsExpanded="True"
BorderBrush="{DynamicResource BackgroundBrush}">
<Expander.Style>
<Style TargetType="{x:Type Expander}"
BasedOn="{StaticResource {x:Type Expander}}">
<Setter Property="Tag"
Value="{StaticResource ExpandRightIcon}" />
<Style.Triggers>
<Trigger Property="IsExpanded"
Value="True">
<Setter Property="Tag"
Value="{StaticResource ExpandDownIcon}" />
</Trigger>
</Style.Triggers>
</Style>
</Expander.Style>
<Border BorderThickness="1"
Padding="10"
HorizontalAlignment="Stretch">
<local:SettingsView Items="{Binding Source={x:Static local:PlaygroundSettings.Instance}, Path=Settings}" />
</Border>
</Expander>
<Expander Header="Editor Settings"
Padding="0 5 0 0"
BorderThickness="0 0 0 1"
IsExpanded="True"
BorderBrush="{DynamicResource BackgroundBrush}"
Grid.Row="1">
<Expander.Style>
<Style TargetType="{x:Type Expander}"
BasedOn="{StaticResource {x:Type Expander}}">
<Setter Property="Tag"
Value="{StaticResource ExpandRightIcon}" />
<Style.Triggers>
<Trigger Property="IsExpanded"
Value="True">
<Setter Property="Tag"
Value="{StaticResource ExpandDownIcon}" />
</Trigger>
</Style.Triggers>
</Style>
</Expander.Style>
<local:EditorSettingsView />
</Expander>
</Grid>
</ScrollViewer>
</Grid>
</Expander>
<!--INFORMATION-->
<Border Grid.Row="2"
Background="{DynamicResource PanelBackgroundBrush}"
VerticalAlignment="Bottom"
Padding="10">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="*" />
<ColumnDefinition Width="Auto" />
</Grid.ColumnDefinitions>
<Grid.Resources>
<Style TargetType="{x:Type TextBlock}"
BasedOn="{StaticResource {x:Type TextBlock}}">
<Setter Property="Foreground"
Value="{DynamicResource ForegroundBrush}" />
<Setter Property="Margin"
Value="0 0 15 0" />
</Style>
</Grid.Resources>
<StackPanel Orientation="Horizontal">
<TextBlock ToolTip="The number of selected items.">
<TextBlock.Inlines>
<Run Text="Selected nodes: " />
<Run Foreground="YellowGreen"
Text="{Binding GraphViewModel.SelectedNodes.Count, Mode=OneWay}" />
<Run Text="/" />
<Run Text="{Binding GraphViewModel.Nodes.Count, Mode=OneWay}" />
</TextBlock.Inlines>
</TextBlock>
<TextBlock ToolTip="The number of selected connections.">
<TextBlock.Inlines>
<Run Text="Selected connections: " />
<Run Foreground="YellowGreen"
Text="{Binding GraphViewModel.SelectedConnections.Count, Mode=OneWay}" />
<Run Text="/" />
<Run Text="{Binding GraphViewModel.Connections.Count, Mode=OneWay}" />
</TextBlock.Inlines>
</TextBlock>
<Border Visibility="{Binding GraphViewModel.KeyboardNavigationLayer, Converter={StaticResource StringToVisibilityConverter}}"
ToolTip="Press CTRL+[ or CTRL+] in the editor to change the keyboard navigation layer."
Padding="14 0 0 0"
Height="16"
CornerRadius="3"
Background="{DynamicResource PanelBackgroundBrush}"
BorderThickness="1"
BorderBrush="{DynamicResource BorderBrush}">
<TextBlock>
<Run Text="Navigating: " />
<Run Foreground="{DynamicResource ForegroundBrush}"
Text="{Binding GraphViewModel.KeyboardNavigationLayer, Mode=OneWay}" />
</TextBlock>
</Border>
</StackPanel>
<StackPanel Orientation="Horizontal"
HorizontalAlignment="Right">
<TextBlock ToolTip="The viewport's location.">
<TextBlock.Inlines>
<Run Text="Location: " />
<Run Foreground="Orange"
Text="{Binding Location.Value, Mode=OneWay, Converter={StaticResource ToStringConverter}, Source={x:Static local:EditorSettings.Instance}}" />
</TextBlock.Inlines>
</TextBlock>
<TextBlock ToolTip="The viewport's size.">
<TextBlock.Inlines>
<Run Text="Size: " />
<Run Foreground="YellowGreen"
Text="{Binding GraphViewModel.ViewportSize, Mode=OneWay, Converter={StaticResource ToStringConverter}}" />
</TextBlock.Inlines>
</TextBlock>
<TextBlock ToolTip="The viewport's zoom. Not accurate when trying to zoom outside the MinViewportZoom and MaxViewportZoom because of dependency property coercion not updating the binding with the final result.">
<TextBlock.Inlines>
<Run Text="Zoom: " />
<Run Foreground="DodgerBlue"
Text="{Binding Zoom, Mode=OneWay, Converter={StaticResource ToStringConverter}, Source={x:Static local:EditorSettings.Instance}}" />
</TextBlock.Inlines>
</TextBlock>
<TextBlock ToolTip="The estimated frame rate. (my be buggy)">
<TextBlock.Inlines>
<Run Text="FPS: " />
<Run Foreground="LawnGreen"
Name="FPSText" />
</TextBlock.Inlines>
</TextBlock>
</StackPanel>
</Grid>
</Border>
</Grid>
</Window>

View File

@@ -0,0 +1,95 @@
using System;
using System.Windows;
using System.Windows.Input;
using System.Windows.Media;
namespace Nodify.Playground
{
public static class CompositionTargetEx
{
private static TimeSpan _last = TimeSpan.Zero;
private static event Action<double>? FrameUpdating;
public static event Action<double> Rendering
{
add
{
if (FrameUpdating == null)
{
CompositionTarget.Rendering += OnRendering;
}
FrameUpdating += value;
}
remove
{
FrameUpdating -= value;
if (FrameUpdating == null)
{
CompositionTarget.Rendering -= OnRendering;
}
}
}
private static void OnRendering(object? sender, EventArgs e)
{
RenderingEventArgs args = (RenderingEventArgs)e;
var renderingTime = args.RenderingTime;
if (renderingTime == _last)
return;
double fps = 1000 / (renderingTime - _last).TotalMilliseconds;
_last = renderingTime;
FrameUpdating?.Invoke(fps);
}
}
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
private readonly Random _rand = new Random();
public MainWindow()
{
InitializeComponent();
CompositionTargetEx.Rendering += OnRendering;
EventManager.RegisterClassHandler(
typeof(UIElement),
Keyboard.PreviewGotKeyboardFocusEvent,
(KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus);
}
private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
Title = e.NewFocus.ToString();
}
private void OnRendering(double fps)
{
FPSText.Text = fps.ToString("0");
}
private void BringIntoView_Click(object sender, RoutedEventArgs e)
{
if (DataContext is PlaygroundViewModel model)
{
NodifyObservableCollection<NodeViewModel> nodes = model.GraphViewModel.Nodes;
int index = _rand.Next(nodes.Count);
if (nodes.Count > index)
{
NodeViewModel node = nodes[index];
EditorCommands.BringIntoView.Execute(node.Location, EditorView.Editor);
}
}
}
private void AnimateConnections_Click(object sender, RoutedEventArgs e)
{
EditorSettings.Instance.IsAnimatingConnections = !EditorSettings.Instance.IsAnimatingConnections;
}
}
}

View File

@@ -0,0 +1,17 @@
<Project Sdk="Microsoft.NET.Sdk.WindowsDesktop">
<PropertyGroup>
<OutputType>WinExe</OutputType>
<TargetFrameworks>net9-windows;net8-windows;net6-windows;net5-windows;netcoreapp3.1;net48;net472</TargetFrameworks>
<UseWPF>true</UseWPF>
<Nullable>enable</Nullable>
</PropertyGroup>
<PropertyGroup Condition="'$(TargetFramework)'=='net472' OR '$(TargetFramework)'=='net48'">
<LangVersion>8.0</LangVersion>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Nodify\Nodify.csproj" />
<ProjectReference Include="..\Nodify.Shared\Nodify.Shared.csproj" />
</ItemGroup>
</Project>

View File

@@ -0,0 +1,220 @@
using Nodify.Interactivity;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
namespace Nodify.Playground
{
public class PlaygroundSettings : ObservableObject
{
private readonly IReadOnlyCollection<ISettingViewModel> _settings;
public IEnumerable<ISettingViewModel> Settings => FilterAndSort(_settings);
private string? _searchText;
public string? SearchText
{
get => _searchText;
set => SetProperty(ref _searchText, value)
.Then(() => OnPropertyChanged(nameof(Settings)));
}
private PlaygroundSettings()
{
_settings = new List<ISettingViewModel>()
{
new ProxySettingViewModel<EditorGesturesMappings>(
() => Instance.EditorGesturesMappings,
val => Instance.EditorGesturesMappings = val,
"Editor input mappings"),
new ProxySettingViewModel<EditorInputMode>(
() => Instance.EditorInputMode,
val => Instance.EditorInputMode = val,
"Editor input mode"),
new ProxySettingViewModel<bool>(
() => Instance.ShowMinimap,
val => Instance.ShowMinimap = val,
"Show minimap",
"Set Enable nodes dragging optimization to false for realtime updates"),
new ProxySettingViewModel<bool>(
() => Instance.DisableMinimapControls,
val => Instance.DisableMinimapControls = val,
"Disable minimap controls",
"Whether the minimap can move and zoom the viewport"),
new ProxySettingViewModel<bool>(
() => Instance.ResizeToViewport,
val => Instance.ResizeToViewport = val,
"Minimap resize to viewport",
"Whether the minimap should resized to also display the viewport"),
new ProxySettingViewModel<PointEditor>(
() => Instance.MinimapMaxViewportOffset,
val => Instance.MinimapMaxViewportOffset = val,
"Minimap max viewport offset",
"The max position from the items extent that the viewport can move to"),
new ProxySettingViewModel<bool>(
() => Instance.ShowGridLines,
val => Instance.ShowGridLines = val,
"Show grid lines:"),
new ProxySettingViewModel<bool>(
() => Instance.ShouldConnectNodes,
val => Instance.ShouldConnectNodes = val,
"Connect nodes:"),
new ProxySettingViewModel<bool>(
() => Instance.AsyncLoading,
val => Instance.AsyncLoading = val,
"Async loading:"),
new ProxySettingViewModel<bool>(
() => Instance.UseCustomConnectors,
val => Instance.UseCustomConnectors = val,
"Custom connectors:"),
new ProxySettingViewModel<uint>(
() => Instance.MinNodes,
val => Instance.MinNodes = val,
"Min nodes:"),
new ProxySettingViewModel<uint>(
() => Instance.MaxNodes,
val => Instance.MaxNodes = val,
"Max nodes:"),
new ProxySettingViewModel<uint>(
() => Instance.MinConnectors,
val => Instance.MinConnectors = val,
"Min connectors:"),
new ProxySettingViewModel<uint>(
() => Instance.MaxConnectors,
val => Instance.MaxConnectors = val,
"Max connectors:"),
new ProxySettingViewModel<uint>(
() => Instance.PerformanceTestNodes,
val => Instance.PerformanceTestNodes = val,
"Performance test nodes:"),
};
}
public IEnumerable<ISettingViewModel> FilterAndSort(IReadOnlyCollection<ISettingViewModel> settings)
{
if (string.IsNullOrWhiteSpace(SearchText))
{
return settings;
}
string searchText = SearchText!.ToLowerInvariant();
var matchingValues = settings.Where(s => s.Name.ToLowerInvariant().Contains(searchText) || (s.Description?.ToLowerInvariant()?.Contains(searchText) ?? false));
var sortedValues = matchingValues.OrderByDescending(s => s.Name.ToLowerInvariant().Contains(searchText));
return sortedValues;
}
public static PlaygroundSettings Instance { get; } = new PlaygroundSettings();
private EditorGesturesMappings _editorGesturesMappings;
public EditorGesturesMappings EditorGesturesMappings
{
get => _editorGesturesMappings;
set => SetProperty(ref _editorGesturesMappings, value)
.Then(() => EditorGestures.Mappings.Apply(value));
}
private EditorInputMode _editorInputMode;
public EditorInputMode EditorInputMode
{
get => _editorInputMode;
set => SetProperty(ref _editorInputMode, value)
.Then(() => EditorGestures.Mappings.Apply(value));
}
private bool _showMinimap = true;
public bool ShowMinimap
{
get => _showMinimap;
set => SetProperty(ref _showMinimap, value);
}
private bool _disableMinimapControls = false;
public bool DisableMinimapControls
{
get => _disableMinimapControls;
set => SetProperty(ref _disableMinimapControls, value);
}
private bool _resizeToViewport = false;
public bool ResizeToViewport
{
get => _resizeToViewport;
set => SetProperty(ref _resizeToViewport, value);
}
private PointEditor _minimapViewportOffset = new Size(2000, 2000);
public PointEditor MinimapMaxViewportOffset
{
get => _minimapViewportOffset;
set => SetProperty(ref _minimapViewportOffset, value);
}
private bool _shouldConnectNodes = true;
public bool ShouldConnectNodes
{
get => _shouldConnectNodes;
set => SetProperty(ref _shouldConnectNodes, value);
}
private bool _asyncLoading = true;
public bool AsyncLoading
{
get => _asyncLoading;
set => SetProperty(ref _asyncLoading, value);
}
private uint _minNodes = 10;
public uint MinNodes
{
get => _minNodes;
set => SetProperty(ref _minNodes, value)
.Then(() => MaxNodes = MaxNodes < MinNodes ? MinNodes : MaxNodes);
}
private uint _maxNodes = 100;
public uint MaxNodes
{
get => _maxNodes;
set => SetProperty(ref _maxNodes, value)
.Then(() => MaxNodes = MaxNodes < MinNodes ? MinNodes : MaxNodes);
}
private uint _minConnectors = 0;
public uint MinConnectors
{
get => _minConnectors;
set => SetProperty(ref _minConnectors, value)
.Then(() => MaxConnectors = MaxConnectors < MinConnectors ? MinConnectors : MaxConnectors);
}
private uint _maxConnectors = 4;
public uint MaxConnectors
{
get => _maxConnectors;
set => SetProperty(ref _maxConnectors, value)
.Then(() => MaxConnectors = MaxConnectors < MinConnectors ? MinConnectors : MaxConnectors);
}
private uint _performanceTestNodes = 1000;
public uint PerformanceTestNodes
{
get => _performanceTestNodes;
set => SetProperty(ref _performanceTestNodes, value);
}
private bool _showGridLines = true;
public bool ShowGridLines
{
get => _showGridLines;
set => SetProperty(ref _showGridLines, value);
}
private bool _customConnectors = true;
public bool UseCustomConnectors
{
get => _customConnectors;
set => SetProperty(ref _customConnectors, value);
}
}
}

View File

@@ -0,0 +1,168 @@
using System;
using System.Collections;
using System.Threading.Tasks;
using System.Windows.Data;
using System.Windows.Input;
namespace Nodify.Playground
{
public class PlaygroundViewModel : ObservableObject
{
public NodifyEditorViewModel GraphViewModel { get; } = new NodifyEditorViewModel();
public PlaygroundViewModel()
{
GenerateRandomNodesCommand = new DelegateCommand(GenerateRandomNodes);
PerformanceTestCommand = new DelegateCommand(PerformanceTest);
ToggleConnectionsCommand = new DelegateCommand(ToggleConnections);
ResetCommand = new DelegateCommand(ResetGraph);
BindingOperations.EnableCollectionSynchronization(GraphViewModel.Nodes, GraphViewModel.Nodes);
BindingOperations.EnableCollectionSynchronization(GraphViewModel.Connections, GraphViewModel.Connections);
Settings.PropertyChanged += OnSettingsChanged;
}
private void OnSettingsChanged(object? sender, System.ComponentModel.PropertyChangedEventArgs e)
{
if (e.PropertyName == nameof(PlaygroundSettings.ShouldConnectNodes))
OnPropertyChanged(nameof(ConnectNodesText));
}
public ICommand GenerateRandomNodesCommand { get; }
public ICommand PerformanceTestCommand { get; }
public ICommand ToggleConnectionsCommand { get; }
public ICommand ResetCommand { get; }
public PlaygroundSettings Settings => PlaygroundSettings.Instance;
public string ConnectNodesText => Settings.ShouldConnectNodes ? "CONNECT NODES" : "DISCONNECT NODES";
private void ResetGraph()
{
GraphViewModel.Nodes.Clear();
EditorSettings.Instance.Location = new System.Windows.Point(0, 0);
EditorSettings.Instance.Zoom = 1.0d;
}
private async void GenerateRandomNodes()
{
uint minNodesByType = Settings.MinNodes / 2;
uint maxNodesByType = Settings.MaxNodes / 2;
var nodes = RandomNodesGenerator.GenerateNodes<FlowNodeViewModel>(new NodesGeneratorSettings(minNodesByType)
{
MinNodesCount = minNodesByType,
MaxNodesCount = maxNodesByType,
MinInputCount = Settings.MinConnectors,
MaxInputCount = Settings.MaxConnectors,
MinOutputCount = Settings.MinConnectors,
MaxOutputCount = Settings.MaxConnectors,
GridSnap = EditorSettings.Instance.GridSpacing
});
var verticalNodes = RandomNodesGenerator.GenerateNodes<VerticalNodeViewModel>(new NodesGeneratorSettings(minNodesByType)
{
MinNodesCount = minNodesByType,
MaxNodesCount = maxNodesByType,
MinInputCount = Settings.MinConnectors,
MaxInputCount = Settings.MaxConnectors,
MinOutputCount = Settings.MinConnectors,
MaxOutputCount = Settings.MaxConnectors,
GridSnap = EditorSettings.Instance.GridSpacing
});
GraphViewModel.Nodes.Clear();
await CopyToAsync(nodes, GraphViewModel.Nodes);
await CopyToAsync(verticalNodes, GraphViewModel.Nodes);
if (Settings.ShouldConnectNodes)
{
await ConnectNodes();
}
}
private async void ToggleConnections()
{
if (Settings.ShouldConnectNodes)
{
await ConnectNodes();
}
else
{
GraphViewModel.Connections.Clear();
}
}
private async void PerformanceTest()
{
uint count = Settings.PerformanceTestNodes;
int distance = 500;
int size = (int)count / (int)Math.Sqrt(count);
var nodes = RandomNodesGenerator.GenerateNodes<FlowNodeViewModel>(new NodesGeneratorSettings(count)
{
NodeLocationGenerator = (s, i) => new System.Windows.Point(i % size * distance, i / size * distance),
MinInputCount = Settings.MinConnectors,
MaxInputCount = Settings.MaxConnectors,
MinOutputCount = Settings.MinConnectors,
MaxOutputCount = Settings.MaxConnectors,
GridSnap = EditorSettings.Instance.GridSpacing
});
GraphViewModel.Nodes.Clear();
await CopyToAsync(nodes, GraphViewModel.Nodes);
if (Settings.ShouldConnectNodes)
{
await ConnectNodes();
}
}
private async Task ConnectNodes()
{
var schema = new GraphSchema();
var connections = RandomNodesGenerator.GenerateConnections(GraphViewModel.Nodes);
if (Settings.AsyncLoading)
{
await Task.Run(() =>
{
for (int i = 0; i < connections.Count; i++)
{
var con = connections[i];
schema.TryAddConnection(con.Input, con.Output);
}
});
}
else
{
for (int i = 0; i < connections.Count; i++)
{
var con = connections[i];
schema.TryAddConnection(con.Input, con.Output);
}
}
}
private async Task CopyToAsync(IList source, IList target)
{
if (Settings.AsyncLoading)
{
await Task.Run(() =>
{
for (int i = 0; i < source.Count; i++)
{
target.Add(source[i]);
}
});
}
else
{
for (int i = 0; i < source.Count; i++)
{
target.Add(source[i]);
}
}
}
}
}

View File

@@ -0,0 +1,80 @@
using System.Windows;
namespace Nodify.Playground
{
public class PointEditor : ObservableObject
{
public double X
{
get => Value.X;
set
{
Value = new Point(value, Value.Y);
if (value >= 0)
{
Size = new Size(value, Size.Height);
}
}
}
public double Y
{
get => Value.Y;
set
{
Value = new Point(Value.X, value);
if (value >= 0)
{
Size = new Size(Size.Width, value);
}
}
}
private Point _value;
public Point Value
{
get => _value;
set => SetProperty(ref _value, value)
.Then(() =>
{
OnPropertyChanged(nameof(X));
OnPropertyChanged(nameof(Y));
});
}
private Size _size;
public Size Size
{
get => _size;
set => SetProperty(ref _size, value)
.Then(() =>
{
OnPropertyChanged(nameof(X));
OnPropertyChanged(nameof(Y));
});
}
public string XLabel { get; set; } = "x";
public string YLabel { get; set; } = "y";
public static implicit operator PointEditor(Point point)
{
return new PointEditor
{
X = point.X,
Y = point.Y
};
}
public static implicit operator PointEditor(Size size)
{
return new PointEditor
{
X = size.Width,
Y = size.Height,
XLabel = "w",
YLabel = "h"
};
}
}
}

View File

@@ -0,0 +1,43 @@
<UserControl x:Class="Nodify.Playground.PointEditorView"
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:Nodify.Playground"
d:DataContext="{d:DesignInstance Type={x:Type local:PointEditor}, IsDesignTimeCreatable=True}"
mc:Ignorable="d"
d:DesignHeight="450"
d:DesignWidth="800">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
MinWidth="20" />
<ColumnDefinition MinWidth="30" />
</Grid.ColumnDefinitions>
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="Auto" />
</Grid.RowDefinitions>
<TextBlock TextAlignment="Center"
VerticalAlignment="Center">
<Run Text="{Binding XLabel}" />
<Run Text=":" />
</TextBlock>
<TextBox Text="{Binding X, Mode=TwoWay}"
Grid.Column="1" />
<TextBlock TextAlignment="Center"
VerticalAlignment="Center"
Grid.Row="1"
Margin="0 5 0 0">
<Run Text="{Binding YLabel}" />
<Run Text=":" />
</TextBlock>
<TextBox Text="{Binding Y, Mode=TwoWay}"
Margin="0 5 0 0"
Grid.Row="1"
Grid.Column="1" />
</Grid>
</UserControl>

View File

@@ -0,0 +1,12 @@
using System.Windows.Controls;
namespace Nodify.Playground
{
public partial class PointEditorView : UserControl
{
public PointEditorView()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,23 @@
using System;
namespace Nodify.Playground
{
public class ProxySettingViewModel<T> : BaseSettingViewModel<T>
{
private readonly Func<T> _getter;
private readonly Action<T> _setter;
public ProxySettingViewModel(Func<T> getter, Action<T> setter, string name, string? description = default)
: base(name, description)
{
_getter = getter;
_setter = setter;
}
public new T Value
{
get => _getter();
set => _setter(value);
}
}
}

View File

@@ -0,0 +1,93 @@
<UserControl x:Class="Nodify.Playground.SettingsView"
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:Nodify.Playground"
xmlns:nodify="clr-namespace:Nodify;assembly=Nodify.Shared"
d:DataContext="{d:DesignInstance Type=local:PlaygroundViewModel, IsDesignTimeCreatable=True}"
d:Foreground="{DynamicResource ForegroundBrush}"
d:Background="{DynamicResource PanelBackgroundBrush}"
mc:Ignorable="d">
<ItemsControl ItemsSource="{Binding Items, RelativeSource={RelativeSource AncestorType=UserControl}}"
Focusable="False">
<ItemsControl.Resources>
<DataTemplate x:Key="TextEditorTemplate"
DataType="{x:Type local:ISettingViewModel}">
<TextBox Text="{Binding Value}"
TextWrapping="Wrap"
AcceptsReturn="True" />
</DataTemplate>
<DataTemplate x:Key="NumberEditorTemplate"
DataType="{x:Type local:ISettingViewModel}">
<TextBox Text="{Binding Value}" />
</DataTemplate>
<DataTemplate x:Key="BooleanEditorTemplate"
DataType="{x:Type local:ISettingViewModel}">
<CheckBox IsChecked="{Binding Value}" />
</DataTemplate>
<DataTemplate x:Key="PointEditorTemplate"
DataType="{x:Type local:ISettingViewModel}">
<local:PointEditorView DataContext="{Binding Value, Mode=TwoWay}" />
</DataTemplate>
<DataTemplate x:Key="OptionEditorTemplate"
DataType="{x:Type local:ISettingViewModel}">
<ComboBox DisplayMemberPath="Name"
SelectedValuePath="Value"
SelectedValue="{Binding Value, Mode=TwoWay}"
ItemsSource="{Binding Value, Converter={nodify:EnumValuesConverter}}" />
</DataTemplate>
</ItemsControl.Resources>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:ISettingViewModel}">
<Grid>
<Grid.ColumnDefinitions>
<ColumnDefinition Width="Auto"
SharedSizeGroup="PropertyName" />
<ColumnDefinition />
</Grid.ColumnDefinitions>
<TextBlock Text="{Binding Name}"
ToolTip="{Binding Description}"
Margin="0 5 5 0"
Grid.Column="0" />
<ContentControl Content="{Binding}"
Margin="5 5 5 0"
Grid.Column="1"
Focusable="False">
<ContentControl.Style>
<Style TargetType="{x:Type ContentControl}">
<Style.Triggers>
<DataTrigger Binding="{Binding Type}"
Value="Boolean">
<Setter Property="ContentTemplate"
Value="{StaticResource ResourceKey=BooleanEditorTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}"
Value="Number">
<Setter Property="ContentTemplate"
Value="{StaticResource ResourceKey=NumberEditorTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}"
Value="Point">
<Setter Property="ContentTemplate"
Value="{StaticResource ResourceKey=PointEditorTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}"
Value="Option">
<Setter Property="ContentTemplate"
Value="{StaticResource ResourceKey=OptionEditorTemplate}" />
</DataTrigger>
<DataTrigger Binding="{Binding Type}"
Value="Text">
<Setter Property="ContentTemplate"
Value="{StaticResource ResourceKey=TextEditorTemplate}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</Grid>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</UserControl>

View File

@@ -0,0 +1,23 @@
using System.Collections.Generic;
using System.Windows;
using System.Windows.Controls;
namespace Nodify.Playground
{
public partial class SettingsView : UserControl
{
public static readonly DependencyProperty ItemsProperty =
DependencyProperty.Register(nameof(Items), typeof(IEnumerable<ISettingViewModel>), typeof(SettingsView));
public IEnumerable<ISettingViewModel> Items
{
get => (IEnumerable<ISettingViewModel>)GetValue(ItemsProperty);
set => SetValue(ItemsProperty, value);
}
public SettingsView()
{
InitializeComponent();
}
}
}

View File

@@ -0,0 +1,10 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:o="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options">
<SolidColorBrush x:Key="PanelBackgroundBrush"
o:Freeze="True"
Color="{DynamicResource PanelBackgroundColor}"
Opacity="0.8" />
</ResourceDictionary>

View File

@@ -0,0 +1,10 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Brushes.xaml" />
</ResourceDictionary.MergedDictionaries>
<Color x:Key="PanelBackgroundColor">#1A1A1A</Color>
</ResourceDictionary>

View File

@@ -0,0 +1,10 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Brushes.xaml" />
</ResourceDictionary.MergedDictionaries>
<Color x:Key="PanelBackgroundColor">#E9EEFE</Color>
</ResourceDictionary>

View File

@@ -0,0 +1,10 @@
<ResourceDictionary xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
<ResourceDictionary.MergedDictionaries>
<ResourceDictionary Source="Brushes.xaml" />
</ResourceDictionary.MergedDictionaries>
<Color x:Key="PanelBackgroundColor">#2A1B47</Color>
</ResourceDictionary>