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,269 @@
<Application x:Class="Nodify.Shapes.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>
<Pen x:Key="{x:Static nodify:BaseConnection.FocusVisualPenKey}"
Thickness="1"
Brush="DodgerBlue"
DashStyle="{x:Static DashStyles.Dash}" />
</ResourceDictionary>
<ResourceDictionary>
<Style x:Key="IconButton"
TargetType="Button"
BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="Background"
Value="Transparent" />
<Setter Property="BorderBrush"
Value="Transparent" />
<Setter Property="Padding"
Value="2" />
<Setter Property="Cursor"
Value="Hand" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type Button}">
<Border x:Name="PART_Border"
BorderThickness="{TemplateBinding BorderThickness}"
Background="{TemplateBinding Background}"
Padding="{TemplateBinding Padding}"
CornerRadius="3"
SnapsToDevicePixels="True">
<ContentPresenter x:Name="contentPresenter"
Focusable="False"
Margin="{TemplateBinding Padding}"
SnapsToDevicePixels="{TemplateBinding SnapsToDevicePixels}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Border>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsEnabled"
Value="False">
<Setter Property="Opacity"
Value="0.4" />
</Trigger>
</Style.Triggers>
</Style>
<!--ICONS-->
<DataTemplate x:Key="MinusIcon">
<Viewbox Stretch="Fill">
<Grid>
<Path StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M0 0h24v24H0z" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M5 12h14" />
</Grid>
</Viewbox>
</DataTemplate>
<DataTemplate x:Key="PlusIcon">
<Viewbox Stretch="Fill">
<Grid>
<Path StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M0 0h24v24H0z" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M12 5v14M5 12h14" />
</Grid>
</Viewbox>
</DataTemplate>
<DataTemplate x:Key="MaximizeIcon">
<Viewbox Stretch="Fill">
<Grid>
<Path StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M0 0h24v24H0z" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M4 8V6a2 2 0 0 1 2-2h2M4 16v2a2 2 0 0 0 2 2h2M16 4h2a2 2 0 0 1 2 2v2M16 20h2a2 2 0 0 0 2-2v-2" />
</Grid>
</Viewbox>
</DataTemplate>
<DataTemplate x:Key="CursorIcon">
<Viewbox Stretch="Fill"
Margin="2">
<Path Stroke="White"
StrokeThickness="1"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M.256.255a.874.874 0 0 0-.18.974l4.753 17.114a.875.875 0 0 0 1.603-.012L10 10l8.334-3.57a.875.875 0 0 0 .01-1.601L1.23.075a.874.874 0 0 0-.974.18Z" />
</Viewbox>
</DataTemplate>
<DataTemplate x:Key="CircleIcon">
<Viewbox Stretch="Fill">
<Grid>
<Path StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M0 0h24v24H0z" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M3 12a9 9 0 1 0 18 0 9 9 0 1 0-18 0" />
</Grid>
</Viewbox>
</DataTemplate>
<DataTemplate x:Key="SquareIcon">
<Viewbox Stretch="Fill">
<Grid>
<Path StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M0 0h24v24H0z" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M3 5a2 2 0 0 1 2-2h14a2 2 0 0 1 2 2v14a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z" />
</Grid>
</Viewbox>
</DataTemplate>
<DataTemplate x:Key="TriangleIcon">
<Viewbox Stretch="Fill">
<Grid>
<Path StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M0 0h24v24H0z" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M10.363 3.591 2.257 17.125a1.914 1.914 0 0 0 1.636 2.871h16.214a1.914 1.914 0 0 0 1.636-2.87L13.637 3.59a1.914 1.914 0 0 0-3.274 0z" />
</Grid>
</Viewbox>
</DataTemplate>
<DataTemplate x:Key="ArrowBackIcon">
<Viewbox Stretch="Fill">
<Grid>
<Path StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M0 0h24v24H0z" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="m9 14-4-4 4-4" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M5 10h11a4 4 0 1 1 0 8h-1" />
</Grid>
</Viewbox>
</DataTemplate>
<DataTemplate x:Key="ArrowForwardIcon">
<Viewbox Stretch="Fill">
<Grid>
<Path StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M0 0h24v24H0z" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="m15 14 4-4-4-4" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M19 10H8a4 4 0 1 0 0 8h1" />
</Grid>
</Viewbox>
</DataTemplate>
<DataTemplate x:Key="LockIcon">
<Viewbox Stretch="Fill">
<Grid>
<Path StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M0 0h24v24H0z" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M5 13a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2v-6z" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M11 16a1 1 0 1 0 2 0 1 1 0 0 0-2 0M8 11V7a4 4 0 1 1 8 0v4" />
</Grid>
</Viewbox>
</DataTemplate>
<DataTemplate x:Key="LockOpenIcon">
<Viewbox Stretch="Fill">
<Grid>
<Path StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M0 0h24v24H0z" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M5 13a2 2 0 0 1 2-2h10a2 2 0 0 1 2 2v6a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2z" />
<Path Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M11 16a1 1 0 1 0 2 0 1 1 0 1 0-2 0M8 11V6a4 4 0 0 1 8 0" />
</Grid>
</Viewbox>
</DataTemplate>
</ResourceDictionary>
</ResourceDictionary.MergedDictionaries>
</ResourceDictionary>
</Application.Resources>
</Application>

View File

@@ -0,0 +1,21 @@
using Nodify.Interactivity;
using System.Windows;
using System.Windows.Input;
namespace Nodify.Shapes
{
/// <summary>
/// Interaction logic for App.xaml
/// </summary>
public partial class App : Application
{
public App()
{
NodifyEditor.EnableDraggingContainersOptimizations = false;
NodifyEditor.EnableCuttingLinePreview = true;
EditorGestures.Mappings.Connection.Disconnect.Value = new AnyGesture(new Interactivity.MouseGesture(MouseAction.LeftClick, ModifierKeys.Alt), new Interactivity.MouseGesture(MouseAction.RightClick));
EditorGestures.Mappings.Editor.Pan.Value = new AnyGesture(EditorGestures.Mappings.Editor.Pan.Value, new Interactivity.MouseGesture(MouseAction.LeftClick, Key.Space));
}
}
}

View File

@@ -0,0 +1,9 @@
using Nodify.Shapes.Canvas;
namespace Nodify.Shapes
{
public class AppShellViewModel : ObservableObject
{
public CanvasViewModel Canvas { get; } = new CanvasViewModel();
}
}

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,94 @@
using Nodify.Interactivity;
using System;
using System.Linq;
using System.Windows;
using System.Windows.Input;
namespace Nodify.Shapes.Canvas
{
public enum CanvasTool
{
None,
Ellipse,
Rectangle,
Triangle
}
public class CanvasToolbarViewModel : ObservableObject
{
public static readonly CanvasTool[] AvailableTools = Enum.GetValues(typeof(CanvasTool)).Cast<CanvasTool>().ToArray();
internal static readonly EditorGestures EditorGestures = new EditorGestures();
private bool _locked;
public bool Locked
{
get => _locked;
set
{
if (SetProperty(ref _locked, value))
{
var newMappings = _locked ? LockedGestureMappings.Instance : EditorGestures;
EditorGestures.Mappings.Apply(newMappings);
if (_locked)
{
_selectedTool = CanvasTool.None;
OnPropertyChanged(nameof(SelectedTool));
}
}
}
}
public ICommand ToggleLockCommand { get; set; }
private CanvasTool _selectedTool = CanvasTool.None;
public CanvasTool SelectedTool
{
get => _selectedTool;
set
{
if (SetProperty(ref _selectedTool, Locked ? CanvasTool.None : value))
{
var newMappings = _selectedTool == CanvasTool.None ? EditorGestures : DrawingGesturesMappings.Instance;
EditorGestures.Mappings.Apply(newMappings);
}
}
}
public CanvasViewModel Canvas { get; }
public CanvasToolbarViewModel(CanvasViewModel canvas)
{
// copy any user modifications
EditorGestures.Apply(EditorGestures.Mappings);
ToggleLockCommand = new DelegateCommand(() => Locked = !Locked);
Canvas = canvas;
}
public ShapeViewModel CreateShapeAtLocation(Point location)
{
using (Canvas.UndoRedo.Batch("Create shape"))
{
ShapeViewModel shape = SelectedTool switch
{
CanvasTool.Ellipse => new EllipseViewModel(),
CanvasTool.Rectangle => new RectangleViewModel(),
CanvasTool.Triangle => new TriangleViewModel(),
CanvasTool.None => throw new InvalidOperationException("Cannot draw in this state"),
_ => throw new NotImplementedException(nameof(CanvasTool)),
};
shape.Location = location;
shape.Text = "Double click to edit";
Canvas.AddShape(shape);
Canvas.SelectedShapes.Clear();
Canvas.SelectedShapes.Add(shape);
return shape;
}
}
}
}

View File

@@ -0,0 +1,710 @@
<UserControl x:Class="Nodify.Shapes.Canvas.CanvasView"
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.Shapes.Canvas"
xmlns:nodify="https://miroiu.github.io/nodify"
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
xmlns:controls="clr-namespace:Nodify.Shapes.Controls"
xmlns:o="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
mc:Ignorable="d"
Background="#0a172a"
Foreground="white"
d:DataContext="{d:DesignInstance Type=local:CanvasViewModel, IsDesignTimeCreatable=True}"
d:DesignHeight="450"
d:DesignWidth="800">
<Grid>
<Grid.Resources>
<shared:BooleanToVisibilityConverter x:Key="BooleanToVisibilityConverter" />
<shared:InverseBooleanConverter x:Key="InverseBooleanConverter" />
<shared:ColorToSolidColorBrushConverter x:Key="ColorToSolidColorBrushConverter" />
<VisualBrush x:Key="GridDrawingBrush"
TileMode="Tile"
Viewport="0 0 30 30"
ViewportUnits="Absolute"
Viewbox="0 0 30 30"
ViewboxUnits="Absolute"
o:Freeze="True"
Transform="{Binding ViewportTransform, ElementName=Editor}">
<VisualBrush.Visual>
<Rectangle Width="1"
Height="1"
Fill="White" />
</VisualBrush.Visual>
</VisualBrush>
</Grid.Resources>
<nodify:NodifyEditor ItemsSource="{Binding Shapes}"
SelectedItems="{Binding SelectedShapes}"
Connections="{Binding Connections}"
Decorators="{Binding Decorators}"
Background="{StaticResource GridDrawingBrush}"
ConnectionCompletedCommand="{Binding CreateConnectionCommand}"
ItemsDragStartedCommand="{Binding MoveShapesStartedCommand}"
ItemsDragCompletedCommand="{Binding MoveShapesCompletedCommand}"
RemoveConnectionCommand="{Binding RemoveConnectionCommand}"
SelectionChanged="Editor_SelectionChanged"
ItemsMoved="Editor_ItemsMoved"
MouseDown="Editor_MouseDown"
MouseMove="Editor_MouseMove"
MouseUp="Editor_MouseUp"
GridCellSize="5"
x:Name="Editor">
<nodify:NodifyEditor.Style>
<Style TargetType="{x:Type nodify:NodifyEditor}"
BasedOn="{StaticResource {x:Type nodify:NodifyEditor}}">
<Setter Property="Cursor"
Value="Cross" />
<Style.Triggers>
<DataTrigger Binding="{Binding CanvasToolbar.SelectedTool}"
Value="{x:Static local:CanvasTool.None}">
<Setter Property="Cursor"
Value="Arrow" />
</DataTrigger>
</Style.Triggers>
</Style>
</nodify:NodifyEditor.Style>
<nodify:NodifyEditor.Resources>
<DataTemplate DataType="{x:Type local:EllipseViewModel}">
<Ellipse Stretch="Fill"
Focusable="False"
Fill="{Binding Color, Converter={StaticResource ColorToSolidColorBrushConverter}}"
Stroke="{Binding BorderColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
StrokeThickness="2"
Opacity="0.8" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:RectangleViewModel}">
<Rectangle Stretch="Fill"
Focusable="False"
Fill="{Binding Color, Converter={StaticResource ColorToSolidColorBrushConverter}}"
Stroke="{Binding BorderColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
StrokeThickness="2"
Opacity="0.8" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:TriangleViewModel}">
<Polygon Points="0,100 50,0 100,100"
Focusable="False"
Stretch="Fill"
Fill="{Binding Color, Converter={StaticResource ColorToSolidColorBrushConverter}}"
Stroke="{Binding BorderColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
StrokeThickness="2"
Opacity="0.8" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:ShapeToolbarViewModel}">
<Canvas Visibility="{Binding Shape, Converter={StaticResource BooleanToVisibilityConverter}}">
<shared:Swatches SelectedColor="{Binding Shape.Color}"
Colors="{x:Static local:ShapeViewModel.Colors}"
IsEnabled="{Binding DataContext.CanvasToolbar.Locked, ElementName=Editor, Converter={StaticResource InverseBooleanConverter}}"
Canvas.Top="-70"
Panel.ZIndex="1">
<shared:Swatches.Effect>
<DropShadowEffect ShadowDepth="1" />
</shared:Swatches.Effect>
</shared:Swatches>
</Canvas>
</DataTemplate>
<DataTemplate DataType="{x:Type local:UserCursorViewModel}">
<StackPanel IsHitTestVisible="False">
<Viewbox Width="24"
Height="24"
Margin="-10 0 0 5"
Stretch="Fill"
HorizontalAlignment="Left">
<Path Fill="{Binding Color, Converter={StaticResource ColorToSolidColorBrushConverter}}"
Stroke="White"
StrokeLineJoin="Round"
StrokeStartLineCap="Round"
StrokeEndLineCap="Round"
Data="M.256.255a.874.874 0 0 0-.18.974l4.753 17.114a.875.875 0 0 0 1.603-.012L10 10l8.334-3.57a.875.875 0 0 0 .01-1.601L1.23.075a.874.874 0 0 0-.974.18Z" />
</Viewbox>
<Border CornerRadius="3"
Background="{Binding Color, Converter={StaticResource ColorToSolidColorBrushConverter}, ConverterParameter=0.7}"
Padding="6 2">
<TextBlock Text="{Binding Name}" />
</Border>
</StackPanel>
</DataTemplate>
<Style TargetType="{x:Type nodify:PendingConnection}"
BasedOn="{StaticResource {x:Type nodify:PendingConnection}}">
<Setter Property="Stroke">
<Setter.Value>
<SolidColorBrush Color="White"
Opacity="0.7" />
</Setter.Value>
</Setter>
</Style>
</nodify:NodifyEditor.Resources>
<nodify:NodifyEditor.ConnectionTemplate>
<DataTemplate DataType="{x:Type local:ConnectionViewModel}">
<nodify:StepConnection Source="{Binding Source.Anchor}"
Target="{Binding Target.Anchor}"
SourcePosition="{Binding Source.Position}"
TargetPosition="{Binding Target.Position}"
Cursor="Hand"
IsSelectable="True"
Fill="Transparent"
FocusVisualPadding="2"
StrokeThickness="3">
<nodify:StepConnection.Stroke>
<SolidColorBrush Color="White"
Opacity="0.7" />
</nodify:StepConnection.Stroke>
<nodify:StepConnection.Style>
<Style TargetType="{x:Type nodify:StepConnection}">
<Setter Property="OutlineBrush"
Value="Transparent" />
<Style.Triggers>
<Trigger Property="IsMouseOver"
Value="True">
<Setter Property="OutlineBrush">
<Setter.Value>
<SolidColorBrush Color="White"
Opacity="0.15" />
</Setter.Value>
</Setter>
</Trigger>
<Trigger Property="IsSelected"
Value="True">
<Setter Property="OutlineBrush">
<Setter.Value>
<SolidColorBrush Color="DodgerBlue"
Opacity="0.25" />
</Setter.Value>
</Setter>
</Trigger>
</Style.Triggers>
</Style>
</nodify:StepConnection.Style>
</nodify:StepConnection>
</DataTemplate>
</nodify:NodifyEditor.ConnectionTemplate>
<nodify:NodifyEditor.DecoratorContainerStyle>
<Style TargetType="{x:Type nodify:DecoratorContainer}"
BasedOn="{StaticResource {x:Type nodify:DecoratorContainer}}">
<Setter Property="Location"
Value="{Binding Location}" />
</Style>
</nodify:NodifyEditor.DecoratorContainerStyle>
<nodify:NodifyEditor.ItemContainerStyle>
<Style TargetType="{x:Type nodify:ItemContainer}"
BasedOn="{StaticResource {x:Type nodify:ItemContainer}}">
<Style.Resources>
<Style TargetType="{x:Type nodify:Connector}"
BasedOn="{StaticResource {x:Type nodify:Connector}}">
<Setter Property="BorderBrush"
Value="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType=nodify:ItemContainer}}" />
<Setter Property="Background"
Value="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType=nodify:ItemContainer}}" />
<Setter Property="HorizontalContentAlignment"
Value="Center" />
<Setter Property="VerticalContentAlignment"
Value="Center" />
<Setter Property="IsConnected"
Value="True" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate>
<Grid Background="Transparent">
<Ellipse x:Name="PART_Connector"
Width="14"
Height="14"
Stroke="{TemplateBinding BorderBrush}"
Fill="{TemplateBinding Background}"
HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
VerticalAlignment="{TemplateBinding VerticalContentAlignment}" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
<Style TargetType="{x:Type shared:Resizer}"
BasedOn="{StaticResource {x:Type shared:Resizer}}">
<Setter Property="Background"
Value="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType=nodify:ItemContainer}}" />
<Setter Property="IsEnabled"
Value="{Binding DataContext.CanvasToolbar.Locked, ElementName=Editor, Converter={StaticResource InverseBooleanConverter}}" />
</Style>
<!--For some reason, the designer doesn't like having these bindings on the element itself but in a style-->
<Style TargetType="{x:Type controls:ResizableContainer}"
BasedOn="{StaticResource {x:Type shared:ResizablePanel}}">
<Setter Property="BorderBrush"
Value="{Binding BorderBrush, RelativeSource={RelativeSource AncestorType=nodify:ItemContainer}}" />
<Setter Property="Padding"
Value="3" />
<Setter Property="ResizeStartedCommand"
Value="{Binding DataContext.ResizeShapeStartedCommand, ElementName=Editor}" />
<Setter Property="ResizeCompletedCommand"
Value="{Binding DataContext.ResizeShapeCompletedCommand, ElementName=Editor}" />
</Style>
</Style.Resources>
<Setter Property="Location"
Value="{Binding Location}" />
<Setter Property="SelectedBrush"
Value="{Binding BorderColor, Converter={StaticResource ColorToSolidColorBrushConverter}}" />
<Setter Property="SelectedBorderThickness"
Value="1" />
<Setter Property="BorderBrush"
Value="Transparent" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type nodify:ItemContainer}">
<controls:ResizableContainer Height="{Binding Height, Mode=TwoWay}"
Width="{Binding Width, Mode=TwoWay}"
Directions="Corners"
GridCellSize="5"
Focusable="False">
<Grid>
<ContentPresenter Cursor="Hand" />
<shared:EditableTextBlock Text="{Binding Text}"
IsEnabled="{Binding DataContext.CanvasToolbar.Locked, ElementName=Editor, Converter={StaticResource InverseBooleanConverter}}"
HorizontalAlignment="Center"
VerticalAlignment="Center"
HorizontalContentAlignment="Center"
FontSize="16"
ToolTip="Double click to edit" />
<DockPanel LastChildFill="False">
<nodify:Connector DataContext="{Binding LeftConnector}"
Anchor="{Binding Anchor, Mode=OneWayToSource}"
DockPanel.Dock="Left"
HorizontalContentAlignment="Left"
Margin="-11 0 0 0"
Visibility="Hidden"
Height="Auto"
Width="25"
x:Name="LeftConnector" />
<nodify:Connector DataContext="{Binding RightConnector}"
Anchor="{Binding Anchor, Mode=OneWayToSource}"
DockPanel.Dock="Right"
HorizontalContentAlignment="Right"
Margin="0 0 -11 0"
Visibility="Hidden"
Height="Auto"
Width="25"
x:Name="RightConnector" />
<nodify:Connector DataContext="{Binding TopConnector}"
Anchor="{Binding Anchor, Mode=OneWayToSource}"
DockPanel.Dock="Top"
VerticalContentAlignment="Top"
Margin="0 -11 0 0"
Visibility="Hidden"
Width="Auto"
Height="25"
x:Name="TopConnector" />
<nodify:Connector DataContext="{Binding BottomConnector}"
Anchor="{Binding Anchor, Mode=OneWayToSource}"
DockPanel.Dock="Bottom"
VerticalContentAlignment="Bottom"
Margin="0 0 0 -11"
Visibility="Hidden"
Width="Auto"
Height="25"
x:Name="BottomConnector" />
</DockPanel>
</Grid>
</controls:ResizableContainer>
<ControlTemplate.Triggers>
<MultiTrigger>
<MultiTrigger.Conditions>
<Condition Property="IsSelected"
Value="True" />
<Condition Property="IsPreviewingSelection"
Value="{x:Null}" />
</MultiTrigger.Conditions>
<MultiTrigger.Setters>
<Setter Property="Visibility"
TargetName="LeftConnector"
Value="Visible" />
<Setter Property="Visibility"
TargetName="RightConnector"
Value="Visible" />
<Setter Property="Visibility"
TargetName="TopConnector"
Value="Visible" />
<Setter Property="Visibility"
TargetName="BottomConnector"
Value="Visible" />
</MultiTrigger.Setters>
</MultiTrigger>
<Trigger Property="IsPreviewingSelection"
Value="True">
<Setter Property="Visibility"
TargetName="LeftConnector"
Value="Visible" />
<Setter Property="Visibility"
TargetName="RightConnector"
Value="Visible" />
<Setter Property="Visibility"
TargetName="TopConnector"
Value="Visible" />
<Setter Property="Visibility"
TargetName="BottomConnector"
Value="Visible" />
</Trigger>
</ControlTemplate.Triggers>
</ControlTemplate>
</Setter.Value>
</Setter>
<Style.Triggers>
<Trigger Property="IsSelected"
Value="True">
<Setter Property="Panel.ZIndex"
Value="1" />
</Trigger>
</Style.Triggers>
</Style>
</nodify:NodifyEditor.ItemContainerStyle>
</nodify:NodifyEditor>
<!--MINIMAP-->
<shared:ResizablePanel Directions="BottomLeft"
VerticalAlignment="Top"
HorizontalAlignment="Right"
Width="300"
Height="200"
MinWidth="250"
MinHeight="150"
BorderBrush="{x:Null}"
Focusable="False"
Margin="20">
<shared:ResizablePanel.Resources>
<Style TargetType="{x:Type shared:Resizer}">
<Setter Property="Cursor"
Value="SizeNESW" />
<Setter Property="Template">
<Setter.Value>
<ControlTemplate TargetType="{x:Type shared:Resizer}">
<Grid Margin="-3 -6 -6 -3"
Background="Transparent">
<Rectangle Height="12"
Width="3"
Fill="#d2d4d7"
HorizontalAlignment="Left"
VerticalAlignment="Bottom" />
<Rectangle Height="3"
Width="12"
Fill="#d2d4d7"
VerticalAlignment="Bottom"
HorizontalAlignment="Left" />
</Grid>
</ControlTemplate>
</Setter.Value>
</Setter>
</Style>
</shared:ResizablePanel.Resources>
<Border CornerRadius="3"
BorderBrush="#1e293b"
BorderThickness="3">
<Border.Effect>
<DropShadowEffect ShadowDepth="1" />
</Border.Effect>
<nodify:Minimap ItemsSource="{Binding ItemsSource, ElementName=Editor}"
ViewportLocation="{Binding ViewportLocation, ElementName=Editor}"
ViewportSize="{Binding ViewportSize, ElementName=Editor}"
ResizeToViewport="True"
Zoom="Minimap_Zoom">
<nodify:Minimap.Resources>
<DataTemplate DataType="{x:Type local:EllipseViewModel}">
<Ellipse Stretch="Fill"
Fill="{Binding Color, Converter={StaticResource ColorToSolidColorBrushConverter}}"
Stroke="{Binding BorderColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
StrokeThickness="2"
Opacity="0.8" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:RectangleViewModel}">
<Rectangle Stretch="Fill"
Fill="{Binding Color, Converter={StaticResource ColorToSolidColorBrushConverter}}"
Stroke="{Binding BorderColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
StrokeThickness="2"
Opacity="0.8" />
</DataTemplate>
<DataTemplate DataType="{x:Type local:TriangleViewModel}">
<Polygon Points="0,100 50,0 100,100"
Stretch="Fill"
Fill="{Binding Color, Converter={StaticResource ColorToSolidColorBrushConverter}}"
Stroke="{Binding BorderColor, Converter={StaticResource ColorToSolidColorBrushConverter}}"
StrokeThickness="2"
Opacity="0.8" />
</DataTemplate>
</nodify:Minimap.Resources>
<nodify:Minimap.Background>
<SolidColorBrush Color="#111a2d"
Opacity="0.5" />
</nodify:Minimap.Background>
<nodify:Minimap.ItemContainerStyle>
<Style TargetType="{x:Type nodify:MinimapItem}">
<Setter Property="Location"
Value="{Binding Location}" />
<Setter Property="Width"
Value="{Binding Width}" />
<Setter Property="Height"
Value="{Binding Height}" />
</Style>
</nodify:Minimap.ItemContainerStyle>
<nodify:Minimap.ViewportStyle>
<Style TargetType="Rectangle">
<Setter Property="StrokeThickness"
Value="3" />
<Setter Property="Fill">
<Setter.Value>
<SolidColorBrush Color="#445e87"
Opacity="0.3" />
</Setter.Value>
</Setter>
</Style>
</nodify:Minimap.ViewportStyle>
</nodify:Minimap>
</Border>
</shared:ResizablePanel>
<!--TOOLBARS-->
<Border VerticalAlignment="Bottom"
HorizontalAlignment="Center"
Margin="0 0 0 20"
CornerRadius="3"
Background="#1e293b">
<Border.Effect>
<DropShadowEffect ShadowDepth="1" />
</Border.Effect>
<ItemsControl Focusable="True"
KeyboardNavigation.TabNavigation="Cycle">
<ItemsControl.Resources>
<Style TargetType="Button"
BasedOn="{StaticResource IconButton}">
<Setter Property="Margin"
Value="2" />
<Setter Property="Width"
Value="32" />
<Setter Property="Height"
Value="32" />
<Setter Property="Focusable"
Value="False" />
</Style>
</ItemsControl.Resources>
<ListBox BorderThickness="0"
Background="Transparent"
ItemsSource="{x:Static local:CanvasToolbarViewModel.AvailableTools}"
SelectedValue="{Binding CanvasToolbar.SelectedTool}"
KeyboardNavigation.ControlTabNavigation="None">
<ListBox.ItemContainerStyle>
<Style TargetType="ListBoxItem">
<Setter Property="Margin"
Value="2" />
<Setter Property="Padding"
Value="0" />
<Setter Property="Width"
Value="32" />
<Setter Property="Height"
Value="32" />
</Style>
</ListBox.ItemContainerStyle>
<ListBox.Resources>
<DataTemplate x:Key="Cursor">
<Border CornerRadius="3"
Padding="5">
<ContentPresenter ContentTemplate="{StaticResource CursorIcon}" />
</Border>
</DataTemplate>
<DataTemplate x:Key="Circle">
<Border CornerRadius="3"
Padding="5">
<ContentPresenter ContentTemplate="{StaticResource CircleIcon}" />
</Border>
</DataTemplate>
<DataTemplate x:Key="Square">
<Border CornerRadius="3"
Padding="5">
<ContentPresenter ContentTemplate="{StaticResource SquareIcon}" />
</Border>
</DataTemplate>
<DataTemplate x:Key="Triangle">
<Border CornerRadius="3"
Padding="5">
<ContentPresenter ContentTemplate="{StaticResource TriangleIcon}" />
</Border>
</DataTemplate>
</ListBox.Resources>
<ListBox.ItemTemplate>
<DataTemplate>
<ContentControl Content="{Binding}"
Focusable="False">
<ContentControl.Style>
<Style TargetType="ContentControl">
<Setter Property="ContentTemplate"
Value="{StaticResource Cursor}" />
<Style.Triggers>
<DataTrigger Binding="{Binding}"
Value="{x:Static local:CanvasTool.Ellipse}">
<Setter Property="ContentTemplate"
Value="{StaticResource Circle}" />
</DataTrigger>
<DataTrigger Binding="{Binding}"
Value="{x:Static local:CanvasTool.Rectangle}">
<Setter Property="ContentTemplate"
Value="{StaticResource Square}" />
</DataTrigger>
<DataTrigger Binding="{Binding}"
Value="{x:Static local:CanvasTool.Triangle}">
<Setter Property="ContentTemplate"
Value="{StaticResource Triangle}" />
</DataTrigger>
</Style.Triggers>
</Style>
</ContentControl.Style>
</ContentControl>
</DataTemplate>
</ListBox.ItemTemplate>
<ListBox.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal"
IsItemsHost="True" />
</ItemsPanelTemplate>
</ListBox.ItemsPanel>
</ListBox>
<Button Command="{Binding UndoCommand}"
CommandTarget="{Binding ElementName=Editor}"
ContentTemplate="{StaticResource ArrowBackIcon}"
ToolTip="CTRL+Z"
Padding="1" />
<Button Command="{Binding RedoCommand}"
CommandTarget="{Binding ElementName=Editor}"
ContentTemplate="{StaticResource ArrowForwardIcon}"
ToolTip="CTRL+SHIFT+z"
Padding="1" />
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
</ItemsControl>
</Border>
<Border VerticalAlignment="Bottom"
HorizontalAlignment="Left"
Margin="20 0 0 20"
CornerRadius="3"
Background="#1e293b">
<Border.Effect>
<DropShadowEffect ShadowDepth="1" />
</Border.Effect>
<ItemsControl Focusable="True"
KeyboardNavigation.TabNavigation="Cycle"
KeyboardNavigation.DirectionalNavigation="Cycle"
KeyboardNavigation.ControlTabNavigation="None">
<ItemsControl.Resources>
<Style TargetType="Button"
BasedOn="{StaticResource IconButton}">
<Setter Property="Margin"
Value="2" />
<Setter Property="Width"
Value="32" />
<Setter Property="Height"
Value="32" />
</Style>
</ItemsControl.Resources>
<Button Command="{x:Static nodify:EditorCommands.ZoomIn}"
CommandTarget="{Binding ElementName=Editor}"
ContentTemplate="{StaticResource PlusIcon}" />
<Button Command="{x:Static nodify:EditorCommands.ZoomOut}"
CommandTarget="{Binding ElementName=Editor}"
ContentTemplate="{StaticResource MinusIcon}" />
<Button Command="{x:Static nodify:EditorCommands.FitToScreen}"
CommandTarget="{Binding ElementName=Editor}"
ContentTemplate="{StaticResource MaximizeIcon}" />
<Button Command="{Binding ToggleLockCommand}"
DataContext="{Binding CanvasToolbar}">
<Button.Style>
<Style TargetType="Button"
BasedOn="{StaticResource {x:Type Button}}">
<Setter Property="ContentTemplate"
Value="{StaticResource LockOpenIcon}" />
<Style.Triggers>
<DataTrigger Binding="{Binding Locked}"
Value="True">
<Setter Property="ContentTemplate"
Value="{StaticResource LockIcon}" />
</DataTrigger>
</Style.Triggers>
</Style>
</Button.Style>
</Button>
</ItemsControl>
</Border>
<Border VerticalAlignment="Bottom"
HorizontalAlignment="Right"
Margin="0 0 20 20"
Height="38"
CornerRadius="3">
<ItemsControl ItemsSource="{Binding Cursors}"
Focusable="True"
KeyboardNavigation.TabNavigation="Cycle"
KeyboardNavigation.ControlTabNavigation="None">
<ItemsControl.ItemsPanel>
<ItemsPanelTemplate>
<StackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</ItemsControl.ItemsPanel>
<ItemsControl.ItemTemplate>
<DataTemplate DataType="{x:Type local:UserCursorViewModel}">
<Border CornerRadius="50"
Height="28"
Width="28"
Margin="-3"
Background="{Binding Color, Converter={StaticResource ColorToSolidColorBrushConverter}}"
Padding="6 2">
<Border.Effect>
<DropShadowEffect ShadowDepth="1" />
</Border.Effect>
<Button Command="{x:Static nodify:EditorCommands.BringIntoView}"
CommandParameter="{Binding Location}"
CommandTarget="{Binding ElementName=Editor}"
Style="{StaticResource IconButton}"
ToolTip="{Binding Name}"
Content="{Binding Name[0]}"
Foreground="White"
Margin="-6 -2" />
</Border>
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Border>
</Grid>
</UserControl>

View File

@@ -0,0 +1,146 @@
using Nodify.Events;
using Nodify.Shapes.Canvas.UndoRedo;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
using System.Windows.Threading;
namespace Nodify.Shapes.Canvas
{
public partial class CanvasView : UserControl
{
private readonly DispatcherTimer _moveToLocationTimer;
private readonly DispatcherTimer _generateLocationTimer;
private readonly Random _random = new Random();
private readonly Dictionary<UserCursorViewModel, Point> _moveToLocations = new Dictionary<UserCursorViewModel, Point>();
public CanvasView()
{
InitializeComponent();
_moveToLocationTimer = new DispatcherTimer(TimeSpan.FromSeconds(1d / 60d), DispatcherPriority.Background, OnMoveToLocationTick, Dispatcher);
_generateLocationTimer = new DispatcherTimer(TimeSpan.FromSeconds(3), DispatcherPriority.Background, OnGenerateNewLocation, Dispatcher);
}
public override void OnApplyTemplate()
{
_moveToLocationTimer.Start();
_generateLocationTimer.Start();
OnGenerateNewLocation(this, EventArgs.Empty);
base.OnApplyTemplate();
}
private void OnGenerateNewLocation(object? sender, EventArgs e)
{
var canvasVM = (CanvasViewModel)DataContext;
foreach (var cursor in canvasVM.Cursors)
{
_moveToLocations[cursor] = Editor.MouseLocation + new Vector(_random.Next(-1500, 1500), _random.Next(-1000, 1000));
}
}
private void OnMoveToLocationTick(object? sender, EventArgs e)
{
var canvasVM = (CanvasViewModel)DataContext;
double speed = 0.015d;
for (int i = 0; i < canvasVM.Cursors.Count; i++)
{
var user = canvasVM.Cursors[i];
var targetLocation = _moveToLocations[user];
Vector dir = targetLocation - user.Location;
var newLocation = user.Location + dir * speed;
user.Location = newLocation;
}
}
#region Drawing shapes
private ShapeViewModel? _drawingShape;
private Point _initialLocation;
private void Editor_MouseDown(object sender, MouseButtonEventArgs e)
{
var toolbarVm = ((CanvasViewModel)DataContext).CanvasToolbar;
if (toolbarVm.SelectedTool != CanvasTool.None && DrawingGesturesMappings.Instance.Draw.Matches(this, e))
{
_initialLocation = Editor.MouseLocation;
_drawingShape = toolbarVm.CreateShapeAtLocation(Editor.MouseLocation);
}
}
private void Editor_MouseMove(object sender, MouseEventArgs e)
{
if (_drawingShape != null)
{
_drawingShape.Width = Math.Abs(Editor.MouseLocation.X - _initialLocation.X);
_drawingShape.Height = Math.Abs(Editor.MouseLocation.Y - _initialLocation.Y);
if (Editor.MouseLocation.X < _initialLocation.X)
{
_drawingShape.Location = new Point(Editor.MouseLocation.X, _drawingShape.Location.Y);
}
if (Editor.MouseLocation.Y < _initialLocation.Y)
{
_drawingShape.Location = new Point(_drawingShape.Location.X, Editor.MouseLocation.Y);
}
}
}
private void Editor_MouseUp(object sender, MouseButtonEventArgs e)
{
_drawingShape = null;
}
#endregion
private void Minimap_Zoom(object sender, ZoomEventArgs e)
{
Editor.ZoomAtPosition(e.Zoom, e.Location);
}
private void Editor_SelectionChanged(object sender, SelectionChangedEventArgs e)
{
var selectedNodes = e.AddedItems.OfType<ShapeViewModel>().ToList();
var deselectedNodes = e.RemovedItems.OfType<ShapeViewModel>().ToList();
if (selectedNodes.Count == 0 && deselectedNodes.Count == 0)
{
// The selection event most likely contains the selected connections
return;
}
string batchLabel = selectedNodes.Count > 0 ? "Select shapes" : "Deselect shapes";
var canvasVM = (CanvasViewModel)DataContext;
using (canvasVM.UndoRedo.Batch(batchLabel))
{
var selectNodes = new SelectShapesAction(selectedNodes, canvasVM);
var deselectNodes = new DeselectShapesAction(deselectedNodes, canvasVM);
canvasVM.UndoRedo.Record(deselectNodes);
canvasVM.UndoRedo.Record(selectNodes);
}
}
private void Editor_ItemsMoved(object sender, ItemsMovedEventArgs e)
{
var shapes = e.Items.Cast<ShapeViewModel>().ToList();
var canvasVM = (CanvasViewModel)DataContext;
using (canvasVM.UndoRedo.Batch("Move shapes"))
{
var moveShapes = new MoveShapesAction(shapes, e.Offset);
canvasVM.UndoRedo.Record(moveShapes);
}
}
}
}

View File

@@ -0,0 +1,221 @@
using Nodify.Shapes.Canvas.UndoRedo;
using Nodify.UndoRedo;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Input;
namespace Nodify.Shapes.Canvas
{
public class CanvasViewModel : ObservableObject
{
private readonly NodifyObservableCollection<ShapeViewModel> _shapes = new NodifyObservableCollection<ShapeViewModel>();
public IReadOnlyCollection<ShapeViewModel> Shapes => _shapes;
public NodifyObservableCollection<ShapeViewModel> SelectedShapes { get; } = new NodifyObservableCollection<ShapeViewModel>();
public NodifyObservableCollection<ICanvasDecorator> Decorators { get; } = new NodifyObservableCollection<ICanvasDecorator>();
public NodifyObservableCollection<ConnectionViewModel> Connections { get; } = new NodifyObservableCollection<ConnectionViewModel>();
private ShapeToolbarViewModel ShapeToolbar { get; } = new ShapeToolbarViewModel();
public CanvasToolbarViewModel CanvasToolbar { get; }
public ICommand MoveShapesStartedCommand { get; }
public ICommand MoveShapesCompletedCommand { get; }
public ICommand ResizeShapeStartedCommand { get; }
public ICommand ResizeShapeCompletedCommand { get; }
public ICommand CreateConnectionCommand { get; }
public ICommand RemoveConnectionCommand { get; }
public ICommand DeleteSelectionCommand { get; }
public ICommand UndoCommand { get; }
public ICommand RedoCommand { get; }
public NodifyObservableCollection<UserCursorViewModel> Cursors { get; } = new NodifyObservableCollection<UserCursorViewModel>();
public IActionsHistory UndoRedo { get; } = ActionsHistory.Global;
private readonly Random _rand = new Random();
public CanvasViewModel()
{
CanvasToolbar = new CanvasToolbarViewModel(this);
UndoCommand = new RequeryCommand(UndoRedo.Undo, () => UndoRedo.CanUndo && !CanvasToolbar.Locked);
RedoCommand = new RequeryCommand(UndoRedo.Redo, () => UndoRedo.CanRedo && !CanvasToolbar.Locked);
MoveShapesStartedCommand = new DelegateCommand(MoveShapesStartedHandler);
MoveShapesCompletedCommand = new DelegateCommand(MoveShapesCompletedHandler);
ResizeShapeStartedCommand = new DelegateCommand(ResizeShapeStartedHandler);
ResizeShapeCompletedCommand = new DelegateCommand(ResizeShapeCompletedHandler);
DeleteSelectionCommand = new DelegateCommand(DeleteSelection, () => !CanvasToolbar.Locked);
CreateConnectionCommand = new DelegateCommand<(object Source, object Target)>(
((object Source, object Target) pendingConnection) => AddConnection((ConnectorViewModel)pendingConnection.Source, (ConnectorViewModel)pendingConnection.Target),
((object Source, object Target) pendingConnection) => CanConnect((ConnectorViewModel)pendingConnection.Source, (ConnectorViewModel)pendingConnection.Target));
RemoveConnectionCommand = new DelegateCommand<ConnectionViewModel>(RemoveConnection);
SelectedShapes.WhenAdded(shape =>
{
ShapeToolbar.Shape = SelectedShapes.Count == 1 ? shape : null;
});
SelectedShapes.WhenRemoved(shape =>
{
ShapeToolbar.Shape = SelectedShapes.Count == 1 ? SelectedShapes.Single() : null;
});
SelectedShapes.WhenCleared(shapes => ShapeToolbar.Shape = null);
FillCanvasWithShapes();
}
private void MoveShapesStartedHandler()
{
ShapeToolbar.Hide();
}
private void MoveShapesCompletedHandler()
{
ShapeToolbar.Show();
}
private void ResizeShapeStartedHandler()
{
UndoRedo.ExecuteAction(new ResizeShapesAction(this));
}
private void ResizeShapeCompletedHandler()
{
if (UndoRedo.Current is ResizeShapesAction resizeShapes)
{
resizeShapes.SaveSizes();
}
}
private void FillCanvasWithShapes()
{
// Disable undo redo to avoid recording object construction
UndoRedo.IsEnabled = false;
var cursorCount = _rand.Next(3, 6);
for (int i = 0; i < cursorCount; i++)
{
var color = ShapeViewModel.Colors[_rand.Next(0, ShapeViewModel.Colors.Count)];
Cursors.Add(new UserCursorViewModel
{
Name = $"User {i + 1}",
Color = color,
Location = new Point(_rand.Next(0, 1000), _rand.Next(0, 1000))
});
}
var ellipse = new EllipseViewModel
{
Location = new Point(100, 50),
Width = 150,
Height = 150
};
_shapes.Add(ellipse);
var rectangle = new RectangleViewModel
{
Location = new Point(400, 100),
Width = 150,
Height = 150
};
_shapes.Add(rectangle);
Connections.Add(new ConnectionViewModel(ellipse.RightConnector, rectangle.LeftConnector));
var ellipse2 = new EllipseViewModel
{
Location = new Point(100, 250),
Width = 150,
Height = 150
};
_shapes.Add(ellipse2);
var rectangle2 = new RectangleViewModel
{
Location = new Point(450, 400),
Width = 150,
Height = 150
};
_shapes.Add(rectangle2);
var triangle = new TriangleViewModel
{
Location = new Point(800, 200),
Width = 150,
Height = 150
};
_shapes.Add(triangle);
Connections.Add(new ConnectionViewModel(ellipse2.BottomConnector, rectangle2.TopConnector));
Connections.Add(new ConnectionViewModel(rectangle.RightConnector, rectangle2.RightConnector));
SelectedShapes.Add(triangle);
Decorators.Add(ShapeToolbar);
Decorators.AddRange(Cursors);
// Re-enable undo redo
UndoRedo.IsEnabled = true;
}
public void AddShape(ShapeViewModel shape)
{
var action = new DelegateAction(() => _shapes.Add(shape), () => _shapes.Remove(shape), "Add shape");
UndoRedo.ExecuteAction(action);
}
private void AddConnection(ConnectorViewModel source, ConnectorViewModel target)
{
var connection = new ConnectionViewModel(source, target);
var action = new DelegateAction(() => Connections.Add(connection), () => Connections.Remove(connection), "Connect");
UndoRedo.ExecuteAction(action);
}
private void RemoveConnection(ConnectionViewModel connection)
{
var action = new DelegateAction(() => Connections.Remove(connection), () => Connections.Add(connection), "Disconnect");
UndoRedo.ExecuteAction(action);
}
private bool CanConnect(ConnectorViewModel? source, ConnectorViewModel? target)
{
return source != null
&& target != null
&& source != target
&& !Connections.Contains(new ConnectionViewModel(source, target));
}
public void DeleteSelection()
{
if (SelectedShapes.Count == 0)
return;
using (UndoRedo.Batch("Delete selection"))
{
var selection = SelectedShapes.ToList();
var action = new DelegateAction(() => _shapes.RemoveRange(selection), () => _shapes.AddRange(selection), "Delete shapes");
UndoRedo.ExecuteAction(action);
foreach (var shape in selection)
{
var connectors = new[] { shape.LeftConnector, shape.RightConnector, shape.TopConnector, shape.BottomConnector };
var connectionsToRemove = Connections.Where(x => connectors.Contains(x.Source) || connectors.Contains(x.Target)).ToList();
foreach (var connection in connectionsToRemove)
{
RemoveConnection(connection);
}
}
}
}
}
}

View File

@@ -0,0 +1,19 @@
using System;
namespace Nodify.Shapes.Canvas
{
public class ConnectionViewModel : IEquatable<ConnectionViewModel>
{
public ConnectionViewModel(ConnectorViewModel source, ConnectorViewModel target)
{
Source = source;
Target = target;
}
public ConnectorViewModel Source { get; }
public ConnectorViewModel Target { get; }
public bool Equals(ConnectionViewModel? other)
=> other?.Source == Source && other.Target == Target;
}
}

View File

@@ -0,0 +1,21 @@
using System.Windows;
namespace Nodify.Shapes.Canvas
{
public class ConnectorViewModel : ObservableObject
{
public ConnectorViewModel(ConnectorPosition position)
{
Position = position;
}
private Point _anchor;
public Point Anchor
{
get => _anchor;
set => SetProperty(ref _anchor, value);
}
public ConnectorPosition Position { get; }
}
}

View File

@@ -0,0 +1,9 @@
using System.Windows;
namespace Nodify.Shapes.Canvas
{
public interface ICanvasDecorator
{
Point Location { get; set; }
}
}

View File

@@ -0,0 +1,69 @@
using System.ComponentModel;
using System.Windows;
namespace Nodify.Shapes.Canvas
{
public class ShapeToolbarViewModel : ObservableObject, ICanvasDecorator
{
private ShapeViewModel? _shape;
public ShapeViewModel? Shape
{
get => _shape;
set
{
var prevShape = _shape;
if (SetProperty(ref _shape, value))
{
_hiddenShape = null;
HookLocationEvents(prevShape, value);
}
}
}
private ShapeViewModel? _hiddenShape;
private Point _location;
public Point Location
{
get => _location;
set => SetProperty(ref _location, value);
}
private void HookLocationEvents(ShapeViewModel? prevShape, ShapeViewModel? newShape)
{
if (prevShape != null)
prevShape.PropertyChanged -= OnLocationChanged;
if (newShape != null)
{
newShape.PropertyChanged += OnLocationChanged;
Location = newShape.Location;
}
}
private void OnLocationChanged(object? sender, PropertyChangedEventArgs args)
{
if (args.PropertyName == nameof(ShapeViewModel.Location))
Location = ((ShapeViewModel)sender!).Location;
}
public void Hide()
{
_hiddenShape = _shape;
_shape = null;
OnPropertyChanged(nameof(Shape));
}
public void Show()
{
if (_hiddenShape != null)
{
_shape = _hiddenShape;
_hiddenShape = null;
OnPropertyChanged(nameof(Shape));
}
}
}
}

View File

@@ -0,0 +1,19 @@
using System.Windows;
using System.Windows.Media;
namespace Nodify.Shapes.Canvas
{
public class UserCursorViewModel : ObservableObject, ICanvasDecorator
{
private Point _location;
public Point Location
{
get => _location;
set => SetProperty(ref _location, value);
}
public string? Name { get; set; }
public Color Color { get; set; }
}
}

View File

@@ -0,0 +1,19 @@
using Nodify.Interactivity;
using System.Windows.Input;
namespace Nodify.Shapes.Canvas
{
public class DrawingGesturesMappings : EditorGestures
{
public static readonly DrawingGesturesMappings Instance = new DrawingGesturesMappings();
public InputGestureRef Draw { get; }
public DrawingGesturesMappings()
{
Apply(UnboundGestureMappings.Instance);
Draw = new Interactivity.MouseGesture(MouseAction.LeftClick);
}
}
}

View File

@@ -0,0 +1,17 @@
using Nodify.Interactivity;
using System.Windows.Input;
namespace Nodify.Shapes.Canvas
{
public class LockedGestureMappings : EditorGestures
{
public static readonly LockedGestureMappings Instance = new LockedGestureMappings();
public LockedGestureMappings()
{
Apply(UnboundGestureMappings.Instance);
Editor.Pan.Value = new AnyGesture(new Interactivity.MouseGesture(MouseAction.LeftClick), new Interactivity.MouseGesture(MouseAction.RightClick), new Interactivity.MouseGesture(MouseAction.MiddleClick));
}
}
}

View File

@@ -0,0 +1,18 @@
using Nodify.Interactivity;
namespace Nodify.Shapes.Canvas
{
public class UnboundGestureMappings : EditorGestures
{
public static readonly UnboundGestureMappings Instance = new UnboundGestureMappings();
public UnboundGestureMappings()
{
Editor.Selection.Unbind();
Editor.SelectAll.Unbind();
ItemContainer.Selection.Unbind();
Connection.Disconnect.Unbind();
Connector.Connect.Unbind();
}
}
}

View File

@@ -0,0 +1,13 @@
using System.Windows.Media;
namespace Nodify.Shapes.Canvas
{
public class EllipseViewModel : ShapeViewModel
{
public EllipseViewModel()
{
Color = Color.FromRgb(67, 141, 87);
Text = "Ellipse";
}
}
}

View File

@@ -0,0 +1,13 @@
using System.Windows.Media;
namespace Nodify.Shapes.Canvas
{
public class RectangleViewModel : ShapeViewModel
{
public RectangleViewModel()
{
Color = Color.FromRgb(63, 138, 226);
Text = "Rectangle";
}
}
}

View File

@@ -0,0 +1,72 @@
using Nodify.UndoRedo;
using System.Collections.Generic;
using System.Windows;
using System.Windows.Media;
namespace Nodify.Shapes.Canvas
{
public abstract class ShapeViewModel : Undoable
{
public ShapeViewModel(IActionsHistory history) : base(history)
{
RecordProperty<ShapeViewModel>(x => x.Color);
RecordProperty<ShapeViewModel>(x => x.Text);
}
public ShapeViewModel() : this(ActionsHistory.Global)
{
}
public static readonly IReadOnlyList<Color> Colors = new Color[]
{
Color.FromRgb(207, 76, 44),
Color.FromRgb(234, 156, 65),
Color.FromRgb(235, 195, 71),
Color.FromRgb(67, 141, 87),
Color.FromRgb(63, 138, 226),
Color.FromRgb(128, 61, 236),
};
private Point _location;
public Point Location
{
get => _location;
set => SetProperty(ref _location, value);
}
private double _width;
public double Width
{
get => _width;
set => SetProperty(ref _width, value);
}
private double _height;
public double Height
{
get => _height;
set => SetProperty(ref _height, value);
}
private Color _color;
public Color Color
{
get => _color;
set => SetProperty(ref _color, value).Then(x => OnPropertyChanged(nameof(BorderColor)));
}
public Color BorderColor => Color * 1.5f;
private string? _text;
public string? Text
{
get => _text;
set => SetProperty(ref _text, value);
}
public ConnectorViewModel LeftConnector { get; } = new ConnectorViewModel(ConnectorPosition.Left);
public ConnectorViewModel RightConnector { get; } = new ConnectorViewModel(ConnectorPosition.Right);
public ConnectorViewModel TopConnector { get; } = new ConnectorViewModel(ConnectorPosition.Top);
public ConnectorViewModel BottomConnector { get; } = new ConnectorViewModel(ConnectorPosition.Bottom);
}
}

View File

@@ -0,0 +1,13 @@
using System.Windows.Media;
namespace Nodify.Shapes.Canvas
{
public class TriangleViewModel : ShapeViewModel
{
public TriangleViewModel()
{
Color = Color.FromRgb(235, 195, 71);
Text = "Triangle";
}
}
}

View File

@@ -0,0 +1,34 @@
using Nodify.UndoRedo;
using System.Collections.Generic;
using System.Windows;
namespace Nodify.Shapes.Canvas.UndoRedo
{
public class MoveShapesAction : IAction
{
private readonly IReadOnlyCollection<ShapeViewModel> _shapes;
private readonly Vector _offset;
public string? Label => "Move shapes";
public MoveShapesAction(IReadOnlyCollection<ShapeViewModel> shapes, Vector offset)
{
_shapes = shapes;
_offset = offset;
}
public void Execute()
=> ApplyOffset(_offset);
public void Undo()
=> ApplyOffset(-_offset);
private void ApplyOffset(Vector offset)
{
foreach (var shape in _shapes)
{
shape.Location += offset;
}
}
}
}

View File

@@ -0,0 +1,53 @@
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using Nodify.UndoRedo;
namespace Nodify.Shapes.Canvas.UndoRedo
{
public class ResizeShapesAction : IAction
{
private readonly IReadOnlyCollection<ShapeViewModel> _resizedShapes;
private readonly Dictionary<ShapeViewModel, Size> _initialSizes;
private Dictionary<ShapeViewModel, Size>? _finalSizes;
private readonly Dictionary<ShapeViewModel, Point> _initialLocations;
private Dictionary<ShapeViewModel, Point>? _finalLocations;
public ResizeShapesAction(CanvasViewModel canvas)
{
_resizedShapes = canvas.SelectedShapes;
_initialSizes = _resizedShapes.ToDictionary(x => x, x => new Size(x.Width, x.Height));
_initialLocations = _resizedShapes.ToDictionary(x => x, x => x.Location);
}
public string? Label => "Resize shapes";
public void Execute()
{
_finalLocations?.ForEach(x => x.Key.Location = x.Value);
_finalSizes?.ForEach(x =>
{
x.Key.Width = x.Value.Width;
x.Key.Height = x.Value.Height;
});
}
public void Undo()
{
_initialSizes.ForEach(x =>
{
x.Key.Width = x.Value.Width;
x.Key.Height = x.Value.Height;
});
_initialLocations.ForEach(x => x.Key.Location = x.Value);
}
public void SaveSizes()
{
_finalLocations = _resizedShapes.ToDictionary(x => x, x => x.Location);
_finalSizes = _resizedShapes.ToDictionary(x => x, x => new Size(x.Width, x.Height));
}
}
}

View File

@@ -0,0 +1,59 @@
using Nodify.UndoRedo;
using System.Collections.Generic;
namespace Nodify.Shapes.Canvas.UndoRedo
{
public class SelectShapesAction : IAction
{
public string? Label => "Select shapes";
private readonly IReadOnlyCollection<ShapeViewModel> _nodes;
private readonly CanvasViewModel _canvas;
public SelectShapesAction(IReadOnlyCollection<ShapeViewModel> nodes, CanvasViewModel canvas)
{
_nodes = nodes;
_canvas = canvas;
}
public void Execute()
{
_canvas.SelectedShapes.AddRange(_nodes);
}
public void Undo()
{
_canvas.SelectedShapes.RemoveRange(_nodes);
}
public override string? ToString()
=> Label;
}
public class DeselectShapesAction : IAction
{
public string? Label => "Deselect shapes";
private readonly IReadOnlyCollection<ShapeViewModel> _nodes;
private readonly CanvasViewModel _canvas;
public DeselectShapesAction(IReadOnlyCollection<ShapeViewModel> nodes, CanvasViewModel canvas)
{
_nodes = nodes;
_canvas = canvas;
}
public void Execute()
{
_canvas.SelectedShapes.RemoveRange(_nodes);
}
public void Undo()
{
_canvas.SelectedShapes.AddRange(_nodes);
}
public override string? ToString()
=> Label;
}
}

View File

@@ -0,0 +1,31 @@
using System.Windows;
namespace Nodify.Shapes.Controls
{
internal class ResizableContainer : ResizablePanel
{
public static readonly DependencyProperty GridCellSizeProperty = NodifyEditor.GridCellSizeProperty.AddOwner(typeof(ResizableContainer));
public uint GridCellSize
{
get => (uint)GetValue(GridCellSizeProperty);
set => SetValue(GridCellSizeProperty, value);
}
protected override void OnMove(double x, double y)
{
// we can't use the default behavior because we are not inside a Canvas
if (TemplatedParent is ItemContainer item)
{
item.Location = new Point(item.Location.X + x, item.Location.Y + y);
}
}
protected override void OnProcessDelta(ref double dx, ref double dy)
{
// snap to grid
dx = (int)dx / GridCellSize * GridCellSize;
dy = (int)dy / GridCellSize * GridCellSize;
}
}
}

View File

@@ -0,0 +1,35 @@
<Window x:Class="Nodify.Shapes.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:o="http://schemas.microsoft.com/winfx/2006/xaml/presentation/options"
xmlns:local="clr-namespace:Nodify.Shapes"
xmlns:canvas="clr-namespace:Nodify.Shapes.Canvas"
mc:Ignorable="d"
xmlns:shared="clr-namespace:Nodify;assembly=Nodify.Shared"
Title="Canvas"
Height="650"
Width="1200">
<Window.InputBindings>
<KeyBinding Command="{Binding Canvas.RedoCommand}"
Key="Y"
Modifiers="Ctrl" />
<KeyBinding Command="{Binding Canvas.RedoCommand}"
Key="Z"
Modifiers="Ctrl+Shift" />
<KeyBinding Command="{Binding Canvas.UndoCommand}"
Key="Z"
Modifiers="Ctrl" />
<KeyBinding Command="{Binding Canvas.DeleteSelectionCommand}"
Key="Delete" />
</Window.InputBindings>
<Window.DataContext>
<local:AppShellViewModel />
</Window.DataContext>
<Grid>
<canvas:CanvasView DataContext="{Binding Canvas}" />
</Grid>
</Window>

View File

@@ -0,0 +1,28 @@
using System.Windows;
using System.Windows.Input;
namespace Nodify.Shapes
{
/// <summary>
/// Interaction logic for MainWindow.xaml
/// </summary>
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
PendingConnection.HotKeysDisplayMode = HotKeysDisplayMode.All;
EventManager.RegisterClassHandler(
typeof(UIElement),
Keyboard.PreviewGotKeyboardFocusEvent,
(KeyboardFocusChangedEventHandler)OnPreviewGotKeyboardFocus);
}
private void OnPreviewGotKeyboardFocus(object sender, KeyboardFocusChangedEventArgs e)
{
Title = e.NewFocus.ToString();
}
}
}

View File

@@ -0,0 +1,19 @@
<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>