port from perforce

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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