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,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;
}
}
}