port from perforce
This commit is contained in:
228
intromat/NodeNetwork/Views/ConnectionView.cs
Normal file
228
intromat/NodeNetwork/Views/ConnectionView.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
66
intromat/NodeNetwork/Views/Controls/ArrowToggleButton.xaml
Normal file
66
intromat/NodeNetwork/Views/Controls/ArrowToggleButton.xaml
Normal 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>
|
||||
@@ -0,0 +1,12 @@
|
||||
using System.Windows.Controls.Primitives;
|
||||
|
||||
namespace NodeNetwork.Views.Controls
|
||||
{
|
||||
public partial class ArrowToggleButton : ToggleButton
|
||||
{
|
||||
public ArrowToggleButton()
|
||||
{
|
||||
InitializeComponent();
|
||||
}
|
||||
}
|
||||
}
|
||||
437
intromat/NodeNetwork/Views/Controls/DragCanvas.cs
Normal file
437
intromat/NodeNetwork/Views/Controls/DragCanvas.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
43
intromat/NodeNetwork/Views/Controls/FillPanel.cs
Normal file
43
intromat/NodeNetwork/Views/Controls/FillPanel.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
64
intromat/NodeNetwork/Views/EndpointGroupView.cs
Normal file
64
intromat/NodeNetwork/Views/EndpointGroupView.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
56
intromat/NodeNetwork/Views/ErrorMessageView.cs
Normal file
56
intromat/NodeNetwork/Views/ErrorMessageView.cs
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
87
intromat/NodeNetwork/Views/NetworkView.xaml
Normal file
87
intromat/NodeNetwork/Views/NetworkView.xaml
Normal 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>
|
||||
581
intromat/NodeNetwork/Views/NetworkView.xaml.cs
Normal file
581
intromat/NodeNetwork/Views/NetworkView.xaml.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
37
intromat/NodeNetwork/Views/NodeEndpointEditorView.cs
Normal file
37
intromat/NodeNetwork/Views/NodeEndpointEditorView.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
89
intromat/NodeNetwork/Views/NodeInputView.cs
Normal file
89
intromat/NodeNetwork/Views/NodeInputView.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
88
intromat/NodeNetwork/Views/NodeOutputView.cs
Normal file
88
intromat/NodeNetwork/Views/NodeOutputView.cs
Normal 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
235
intromat/NodeNetwork/Views/NodeView.cs
Normal file
235
intromat/NodeNetwork/Views/NodeView.cs
Normal 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);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
13
intromat/NodeNetwork/Views/NodeViewBase.cs
Normal file
13
intromat/NodeNetwork/Views/NodeViewBase.cs
Normal 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
|
||||
{
|
||||
}
|
||||
}
|
||||
125
intromat/NodeNetwork/Views/PendingConnectionView.cs
Normal file
125
intromat/NodeNetwork/Views/PendingConnectionView.cs
Normal 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);
|
||||
})
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
231
intromat/NodeNetwork/Views/PortView.cs
Normal file
231
intromat/NodeNetwork/Views/PortView.cs
Normal 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();
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user