Add project files.
This commit is contained in:
43
Nodify/Nodes/Events/ResizeEventArgs.cs
Normal file
43
Nodify/Nodes/Events/ResizeEventArgs.cs
Normal file
@@ -0,0 +1,43 @@
|
||||
using System;
|
||||
using System.Windows;
|
||||
|
||||
namespace Nodify.Events
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the method that will handle resize related routed events.
|
||||
/// </summary>
|
||||
/// <param name="sender">The sender of this event.</param>
|
||||
/// <param name="e">The event data.</param>
|
||||
public delegate void ResizeEventHandler(object sender, ResizeEventArgs e);
|
||||
|
||||
/// <summary>
|
||||
/// Provides data for resize related routed events.
|
||||
/// </summary>
|
||||
public class ResizeEventArgs : RoutedEventArgs
|
||||
{
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="ResizeEventArgs"/> class with the previous and the new <see cref="Size"/>.
|
||||
/// </summary>
|
||||
/// <param name="previousSize">The previous size associated with this event.</param>
|
||||
/// <param name="newSize">The new size associated with this event.</param>
|
||||
public ResizeEventArgs(Size previousSize, Size newSize)
|
||||
{
|
||||
PreviousSize = previousSize;
|
||||
NewSize = newSize;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the previous size of the object.
|
||||
/// </summary>
|
||||
public Size PreviousSize { get; }
|
||||
|
||||
|
||||
/// <summary>
|
||||
/// Gets the new size of the object.
|
||||
/// </summary>
|
||||
public Size NewSize { get; }
|
||||
|
||||
protected override void InvokeEventHandler(Delegate genericHandler, object genericTarget)
|
||||
=> ((ResizeEventHandler)genericHandler)(genericTarget, this);
|
||||
}
|
||||
}
|
||||
402
Nodify/Nodes/GroupingNode.cs
Normal file
402
Nodify/Nodes/GroupingNode.cs
Normal file
@@ -0,0 +1,402 @@
|
||||
using Nodify.Events;
|
||||
using Nodify.Interactivity;
|
||||
using System;
|
||||
using System.Linq;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Controls.Primitives;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// Specifies the possible movement modes of a <see cref="GroupingNode"/>.
|
||||
/// </summary>
|
||||
public enum GroupingMovementMode
|
||||
{
|
||||
/// <summary>
|
||||
/// The <see cref="GroupingNode"/> will move its content when moved.
|
||||
/// </summary>
|
||||
Group,
|
||||
|
||||
/// <summary>
|
||||
/// The <see cref="GroupingNode"/> will not move its content when moved.
|
||||
/// </summary>
|
||||
Self
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Defines a panel with a header that groups <see cref="ItemContainer"/>s inside it and can be resized.
|
||||
/// </summary>
|
||||
[TemplatePart(Name = ElementResizeThumb, Type = typeof(FrameworkElement))]
|
||||
[TemplatePart(Name = ElementHeader, Type = typeof(FrameworkElement))]
|
||||
[TemplatePart(Name = ElementContent, Type = typeof(FrameworkElement))]
|
||||
public class GroupingNode : HeaderedContentControl
|
||||
{
|
||||
protected static readonly object GroupMovementBoxed = GroupingMovementMode.Group;
|
||||
|
||||
protected const string ElementResizeThumb = "PART_ResizeThumb";
|
||||
protected const string ElementHeader = "PART_Header";
|
||||
protected const string ElementContent = "PART_Content";
|
||||
|
||||
#region Routed Events
|
||||
|
||||
public static readonly RoutedEvent ResizeStartedEvent = EventManager.RegisterRoutedEvent(nameof(ResizeStarted), RoutingStrategy.Bubble, typeof(ResizeEventHandler), typeof(GroupingNode));
|
||||
public static readonly RoutedEvent ResizeCompletedEvent = EventManager.RegisterRoutedEvent(nameof(ResizeCompleted), RoutingStrategy.Bubble, typeof(ResizeEventHandler), typeof(GroupingNode));
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the node finished resizing.
|
||||
/// </summary>
|
||||
public event ResizeEventHandler ResizeCompleted
|
||||
{
|
||||
add => AddHandler(ResizeCompletedEvent, value);
|
||||
remove => RemoveHandler(ResizeCompletedEvent, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Occurs when the node started resizing.
|
||||
/// </summary>
|
||||
public event ResizeEventHandler ResizeStarted
|
||||
{
|
||||
add => AddHandler(ResizeStartedEvent, value);
|
||||
remove => RemoveHandler(ResizeStartedEvent, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty HeaderBrushProperty = Node.HeaderBrushProperty.AddOwner(typeof(GroupingNode));
|
||||
public static readonly DependencyProperty CanResizeProperty = DependencyProperty.Register(nameof(CanResize), typeof(bool), typeof(GroupingNode), new FrameworkPropertyMetadata(BoxValue.True));
|
||||
public static readonly DependencyProperty ActualSizeProperty = DependencyProperty.Register(nameof(ActualSize), typeof(Size), typeof(GroupingNode), new FrameworkPropertyMetadata(BoxValue.Size, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnActualSizeChanged));
|
||||
public static readonly DependencyProperty MovementModeProperty = DependencyProperty.Register(nameof(MovementMode), typeof(GroupingMovementMode), typeof(GroupingNode), new FrameworkPropertyMetadata(GroupMovementBoxed));
|
||||
public static readonly DependencyProperty ResizeCompletedCommandProperty = DependencyProperty.Register(nameof(ResizeCompletedCommand), typeof(ICommand), typeof(GroupingNode));
|
||||
public static readonly DependencyProperty ResizeStartedCommandProperty = DependencyProperty.Register(nameof(ResizeStartedCommand), typeof(ICommand), typeof(GroupingNode));
|
||||
|
||||
private static void OnActualSizeChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var node = (GroupingNode)d;
|
||||
var newSize = (Size)e.NewValue;
|
||||
node.Width = newSize.Width;
|
||||
node.Height = newSize.Height;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the brush used for the background of the <see cref="HeaderedContentControl.Header"/> of this <see cref="GroupingNode"/>.
|
||||
/// </summary>
|
||||
public Brush HeaderBrush
|
||||
{
|
||||
get => (Brush)GetValue(HeaderBrushProperty);
|
||||
set => SetValue(HeaderBrushProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that indicates whether this <see cref="GroupingNode"/> can be resized.
|
||||
/// </summary>
|
||||
public bool CanResize
|
||||
{
|
||||
get => (bool)GetValue(CanResizeProperty);
|
||||
set => SetValue(CanResizeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the actual size of this <see cref="GroupingNode"/>.
|
||||
/// </summary>
|
||||
public Size ActualSize
|
||||
{
|
||||
get => (Size)GetValue(ActualSizeProperty);
|
||||
set => SetValue(ActualSizeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the default movement mode which can be temporarily changed by holding the <see cref="SwitchMovementModeModifierKey"/> while dragging by the header.
|
||||
/// </summary>
|
||||
public GroupingMovementMode MovementMode
|
||||
{
|
||||
get => (GroupingMovementMode)GetValue(MovementModeProperty);
|
||||
set => SetValue(MovementModeProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the <see cref="ResizeCompleted"/> event is not handled.
|
||||
/// Parameter is the <see cref="ItemContainer.ActualSize"/> of the container.
|
||||
/// </summary>
|
||||
public ICommand? ResizeCompletedCommand
|
||||
{
|
||||
get => (ICommand?)GetValue(ResizeCompletedCommandProperty);
|
||||
set => SetValue(ResizeCompletedCommandProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Invoked when the <see cref="ResizeStarted"/> event is not handled.
|
||||
/// Parameter is the <see cref="ItemContainer.ActualSize"/> of the container.
|
||||
/// </summary>
|
||||
public ICommand? ResizeStartedCommand
|
||||
{
|
||||
get => (ICommand?)GetValue(ResizeStartedCommandProperty);
|
||||
set => SetValue(ResizeStartedCommandProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
#region Fields
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="NodifyEditor"/> that owns this <see cref="GroupingNode"/>.
|
||||
/// </summary>
|
||||
protected NodifyEditor? Editor { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="NodifyEditor"/> that owns this <see cref="Container"/>.
|
||||
/// </summary>
|
||||
protected ItemContainer? Container { get; private set; }
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="FrameworkElement"/> used to resize this <see cref="GroupingNode"/>.
|
||||
/// </summary>
|
||||
protected FrameworkElement? ResizeThumb;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="HeaderedContentControl.Header"/> control of this <see cref="GroupingNode"/>.
|
||||
/// </summary>
|
||||
protected FrameworkElement? HeaderControl;
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="System.Windows.Controls.ContentControl"/> control of this <see cref="GroupingNode"/>.
|
||||
/// </summary>
|
||||
protected FrameworkElement? ContentControl;
|
||||
|
||||
private double _minHeight = 30;
|
||||
private double _minWidth = 30;
|
||||
|
||||
#endregion
|
||||
|
||||
static GroupingNode()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(GroupingNode), new FrameworkPropertyMetadata(typeof(GroupingNode)));
|
||||
FocusableProperty.OverrideMetadata(typeof(GroupingNode), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
Panel.ZIndexProperty.OverrideMetadata(typeof(GroupingNode), new FrameworkPropertyMetadata(-1, OnZIndexPropertyChanged));
|
||||
}
|
||||
|
||||
private static void OnZIndexPropertyChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
var node = (GroupingNode)d;
|
||||
if (node.Container != null)
|
||||
{
|
||||
Panel.SetZIndex(node.Container, (int)e.NewValue);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Initializes a new instance of the <see cref="GroupingNode"/> class.
|
||||
/// </summary>
|
||||
public GroupingNode()
|
||||
{
|
||||
AddHandler(Thumb.DragDeltaEvent, new DragDeltaEventHandler(OnResize));
|
||||
AddHandler(Thumb.DragCompletedEvent, new DragCompletedEventHandler(OnResizeCompleted));
|
||||
AddHandler(Thumb.DragStartedEvent, new DragStartedEventHandler(OnResizeStarted));
|
||||
|
||||
Loaded += OnNodeLoaded;
|
||||
Unloaded += OnNodeUnloaded;
|
||||
}
|
||||
|
||||
private void OnNodeLoaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (HeaderControl != null)
|
||||
{
|
||||
HeaderControl.MouseDown += OnHeaderMouseDown;
|
||||
HeaderControl.SizeChanged += OnHeaderSizeChanged;
|
||||
CalculateDesiredHeaderSize();
|
||||
}
|
||||
}
|
||||
|
||||
private void OnNodeUnloaded(object sender, RoutedEventArgs e)
|
||||
{
|
||||
if (HeaderControl != null)
|
||||
{
|
||||
HeaderControl.MouseDown -= OnHeaderMouseDown;
|
||||
HeaderControl.SizeChanged -= OnHeaderSizeChanged;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHeaderMouseDown(object sender, MouseButtonEventArgs e)
|
||||
{
|
||||
EditorGestures.ItemContainerGestures gestures = EditorGestures.Mappings.ItemContainer;
|
||||
if (Container != null && Editor != null && gestures.Drag.Matches(e.Source, e))
|
||||
{
|
||||
// Switch the default movement mode if necessary
|
||||
var prevMovementMode = MovementMode;
|
||||
if (Keyboard.Modifiers == EditorGestures.Mappings.GroupingNode.SwitchMovementMode)
|
||||
{
|
||||
MovementMode = MovementMode == GroupingMovementMode.Group ? GroupingMovementMode.Self : GroupingMovementMode.Group;
|
||||
}
|
||||
|
||||
var groupBounds = new Rect(Container.Location, RenderSize);
|
||||
|
||||
// Select the content and move with it
|
||||
if (gestures.Selection.Append.Matches(e.Source, e))
|
||||
{
|
||||
Editor.SelectArea(groupBounds, append: true, fit: true);
|
||||
}
|
||||
else if (gestures.Selection.Remove.Matches(e.Source, e))
|
||||
{
|
||||
Editor.UnselectArea(groupBounds, fit: true);
|
||||
}
|
||||
else if (gestures.Selection.Invert.Matches(e.Source, e))
|
||||
{
|
||||
if (Container.IsSelected)
|
||||
{
|
||||
Editor.UnselectArea(groupBounds, fit: true);
|
||||
Container.IsSelected = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Editor.SelectArea(groupBounds, append: true, fit: true);
|
||||
}
|
||||
}
|
||||
else if (gestures.Selection.Replace.Matches(e.Source, e) || EditorGestures.Mappings.ItemContainer.Drag.Matches(e.Source, e))
|
||||
{
|
||||
Editor.SelectArea(groupBounds, append: Container.IsSelected, fit: true);
|
||||
}
|
||||
|
||||
// Deselect content
|
||||
if (MovementMode == GroupingMovementMode.Self)
|
||||
{
|
||||
Editor.UnselectArea(groupBounds, fit: true);
|
||||
Container.IsSelected = true;
|
||||
}
|
||||
|
||||
// Switch the default movement mode back
|
||||
MovementMode = prevMovementMode;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Toggles the selection of nodes inside this group.
|
||||
/// If any contained nodes are selected, all will be unselected.
|
||||
/// If none are selected, all will be selected.
|
||||
/// </summary>
|
||||
public void ToggleContentSelection()
|
||||
{
|
||||
if (Editor != null && Container != null)
|
||||
{
|
||||
var groupBounds = new Rect(Container.Location, RenderSize);
|
||||
bool hasSelection = Editor.SelectedContainers.Any(x => x != Container && groupBounds.Contains(x.Bounds));
|
||||
if (hasSelection)
|
||||
{
|
||||
Editor.UnselectArea(groupBounds, fit: true);
|
||||
Container.IsSelected = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
Editor.SelectArea(groupBounds, append: true, fit: true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
ResizeThumb = Template.FindName(ElementResizeThumb, this) as FrameworkElement;
|
||||
HeaderControl = Template.FindName(ElementHeader, this) as FrameworkElement;
|
||||
ContentControl = Template.FindName(ElementContent, this) as FrameworkElement;
|
||||
|
||||
Container = this.GetParentOfType<ItemContainer>();
|
||||
Editor = Container?.Editor ?? this.GetParentOfType<NodifyEditor>();
|
||||
|
||||
if (Container != null)
|
||||
{
|
||||
Panel.SetZIndex(Container, Panel.GetZIndex(this));
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResize(object sender, DragDeltaEventArgs e)
|
||||
{
|
||||
if (CanResize && ReferenceEquals(e.OriginalSource, ResizeThumb))
|
||||
{
|
||||
double resultWidth = ActualWidth + e.HorizontalChange;
|
||||
double resultHeight = ActualHeight + e.VerticalChange;
|
||||
|
||||
// Snap to grid
|
||||
if (Editor != null)
|
||||
{
|
||||
uint cellSize = Editor.GridCellSize;
|
||||
resultWidth = (int)resultWidth / cellSize * cellSize;
|
||||
resultHeight = (int)resultHeight / cellSize * cellSize;
|
||||
}
|
||||
|
||||
Width = Math.Max(_minWidth, resultWidth);
|
||||
Height = Math.Max(_minHeight, resultHeight);
|
||||
|
||||
e.Handled = true;
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResizeStarted(object sender, DragStartedEventArgs e)
|
||||
{
|
||||
ActualSize = new Size(ActualWidth, ActualHeight);
|
||||
var args = new ResizeEventArgs(ActualSize, ActualSize)
|
||||
{
|
||||
RoutedEvent = ResizeStartedEvent,
|
||||
Source = this
|
||||
};
|
||||
|
||||
RaiseEvent(args);
|
||||
|
||||
// Raise ResizeStartedCommand if ResizeStartedEvent event is not handled
|
||||
if (!args.Handled && (ResizeStartedCommand?.CanExecute(ActualSize) ?? false))
|
||||
{
|
||||
ResizeStartedCommand.Execute(ActualSize);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnResizeCompleted(object sender, DragCompletedEventArgs e)
|
||||
{
|
||||
Size previousSize = ActualSize;
|
||||
var newSize = new Size(ActualWidth, ActualHeight);
|
||||
ActualSize = newSize;
|
||||
|
||||
var args = new ResizeEventArgs(previousSize, newSize)
|
||||
{
|
||||
RoutedEvent = ResizeCompletedEvent,
|
||||
Source = this
|
||||
};
|
||||
|
||||
RaiseEvent(args);
|
||||
|
||||
// Raise ResizeCompletedCommand if ResizeCompletedEvent event is not handled
|
||||
if (!args.Handled && (ResizeCompletedCommand?.CanExecute(newSize) ?? false))
|
||||
{
|
||||
ResizeCompletedCommand.Execute(newSize);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnHeaderSizeChanged(object sender, SizeChangedEventArgs e)
|
||||
=> CalculateDesiredHeaderSize();
|
||||
|
||||
private void CalculateDesiredHeaderSize()
|
||||
{
|
||||
if (HeaderControl != null && ResizeThumb != null)
|
||||
{
|
||||
_minHeight = Math.Max(HeaderControl.ActualHeight + ResizeThumb.ActualHeight, MinHeight);
|
||||
_minWidth = Math.Max(ResizeThumb.ActualWidth, MinWidth);
|
||||
|
||||
// If there's content don't resize it
|
||||
if (ContentControl != null)
|
||||
{
|
||||
_minWidth = Math.Max(_minWidth, ContentControl.DesiredSize.Width);
|
||||
_minHeight = Math.Max(_minHeight, _minHeight + ContentControl.DesiredSize.Height);
|
||||
}
|
||||
}
|
||||
|
||||
// Allow selecting only by the header
|
||||
if (Container != null)
|
||||
{
|
||||
Container.DesiredSizeForSelection = new Size(ActualWidth, Math.Max(HeaderControl?.ActualHeight ?? _minHeight, MinHeight));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
17
Nodify/Nodes/KnotNode.cs
Normal file
17
Nodify/Nodes/KnotNode.cs
Normal file
@@ -0,0 +1,17 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a control that owns a <see cref="Connector"/>.
|
||||
/// </summary>
|
||||
public class KnotNode : ContentControl
|
||||
{
|
||||
static KnotNode()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(KnotNode), new FrameworkPropertyMetadata(typeof(KnotNode)));
|
||||
FocusableProperty.OverrideMetadata(typeof(KnotNode), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
}
|
||||
}
|
||||
}
|
||||
257
Nodify/Nodes/Node.cs
Normal file
257
Nodify/Nodes/Node.cs
Normal file
@@ -0,0 +1,257 @@
|
||||
using System.Collections;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Collections.Specialized;
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a control that has a list of <see cref="Input"/> <see cref="Connector"/>s and a list of <see cref="Output"/> <see cref="Connector"/>s.
|
||||
/// </summary>
|
||||
[TemplatePart(Name = ElementInputItemsControl, Type = typeof(ItemsControl))]
|
||||
[TemplatePart(Name = ElementOutputItemsControl, Type = typeof(ItemsControl))]
|
||||
[StyleTypedProperty(Property = nameof(ContentContainerStyle), StyleTargetType = typeof(Border))]
|
||||
[StyleTypedProperty(Property = nameof(HeaderContainerStyle), StyleTargetType = typeof(Border))]
|
||||
[StyleTypedProperty(Property = nameof(FooterContainerStyle), StyleTargetType = typeof(Border))]
|
||||
public class Node : HeaderedContentControl
|
||||
{
|
||||
protected const string ElementInputItemsControl = "PART_Input";
|
||||
protected const string ElementOutputItemsControl = "PART_Output";
|
||||
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty ContentBrushProperty = DependencyProperty.Register(nameof(ContentBrush), typeof(Brush), typeof(Node));
|
||||
public static readonly DependencyProperty HeaderBrushProperty = DependencyProperty.Register(nameof(HeaderBrush), typeof(Brush), typeof(Node));
|
||||
public static readonly DependencyProperty FooterBrushProperty = DependencyProperty.Register(nameof(FooterBrush), typeof(Brush), typeof(Node));
|
||||
public static readonly DependencyProperty FooterProperty = DependencyProperty.Register(nameof(Footer), typeof(object), typeof(Node), new FrameworkPropertyMetadata(OnFooterChanged));
|
||||
public static readonly DependencyProperty FooterTemplateProperty = DependencyProperty.Register(nameof(FooterTemplate), typeof(DataTemplate), typeof(Node));
|
||||
public static readonly DependencyProperty InputConnectorTemplateProperty = DependencyProperty.Register(nameof(InputConnectorTemplate), typeof(DataTemplate), typeof(Node));
|
||||
protected static readonly DependencyPropertyKey HasFooterPropertyKey = DependencyProperty.RegisterReadOnly(nameof(HasFooter), typeof(bool), typeof(Node), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
public static readonly DependencyProperty HasFooterProperty = HasFooterPropertyKey.DependencyProperty;
|
||||
public static readonly DependencyProperty OutputConnectorTemplateProperty = DependencyProperty.Register(nameof(OutputConnectorTemplate), typeof(DataTemplate), typeof(Node));
|
||||
public static readonly DependencyProperty InputProperty = DependencyProperty.Register(nameof(Input), typeof(IEnumerable), typeof(Node));
|
||||
public static readonly DependencyProperty OutputProperty = DependencyProperty.Register(nameof(Output), typeof(IEnumerable), typeof(Node));
|
||||
public static readonly DependencyProperty ContentContainerStyleProperty = DependencyProperty.Register(nameof(ContentContainerStyle), typeof(Style), typeof(Node));
|
||||
public static readonly DependencyProperty HeaderContainerStyleProperty = DependencyProperty.Register(nameof(HeaderContainerStyle), typeof(Style), typeof(Node));
|
||||
public static readonly DependencyProperty FooterContainerStyleProperty = DependencyProperty.Register(nameof(FooterContainerStyle), typeof(Style), typeof(Node));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the brush used for the background of the <see cref="ContentControl.Content"/> of this <see cref="Node"/>.
|
||||
/// </summary>
|
||||
public Brush ContentBrush
|
||||
{
|
||||
get => (Brush)GetValue(ContentBrushProperty);
|
||||
set => SetValue(ContentBrushProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the brush used for the background of the <see cref="HeaderedContentControl.Header"/> of this <see cref="Node"/>.
|
||||
/// </summary>
|
||||
public Brush HeaderBrush
|
||||
{
|
||||
get => (Brush)GetValue(HeaderBrushProperty);
|
||||
set => SetValue(HeaderBrushProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the brush used for the background of the <see cref="Node.Footer"/> of this <see cref="Node"/>.
|
||||
/// </summary>
|
||||
public Brush FooterBrush
|
||||
{
|
||||
get => (Brush)GetValue(FooterBrushProperty);
|
||||
set => SetValue(FooterBrushProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the data for the footer of this control.
|
||||
/// </summary>
|
||||
public object Footer
|
||||
{
|
||||
get => GetValue(FooterProperty);
|
||||
set => SetValue(FooterProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the template used to display the content of the control's footer.
|
||||
/// </summary>
|
||||
public DataTemplate FooterTemplate
|
||||
{
|
||||
get => (DataTemplate)GetValue(FooterTemplateProperty);
|
||||
set => SetValue(FooterTemplateProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the template used to display the content of the control's <see cref="Input"/> connectors.
|
||||
/// </summary>
|
||||
public DataTemplate InputConnectorTemplate
|
||||
{
|
||||
get => (DataTemplate)GetValue(InputConnectorTemplateProperty);
|
||||
set => SetValue(InputConnectorTemplateProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the template used to display the content of the control's <see cref="Output"/> connectors.
|
||||
/// </summary>
|
||||
public DataTemplate OutputConnectorTemplate
|
||||
{
|
||||
get => (DataTemplate)GetValue(OutputConnectorTemplateProperty);
|
||||
set => SetValue(OutputConnectorTemplateProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the data for the input <see cref="Connector"/>s of this control.
|
||||
/// </summary>
|
||||
public IEnumerable Input
|
||||
{
|
||||
get => (IEnumerable)GetValue(InputProperty);
|
||||
set => SetValue(InputProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the data for the output <see cref="Connector"/>s of this control.
|
||||
/// </summary>
|
||||
public IEnumerable Output
|
||||
{
|
||||
get => (IEnumerable)GetValue(OutputProperty);
|
||||
set => SetValue(OutputProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the style for the content container.
|
||||
/// </summary>
|
||||
public Style ContentContainerStyle
|
||||
{
|
||||
get => (Style)GetValue(ContentContainerStyleProperty);
|
||||
set => SetValue(ContentContainerStyleProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the style for the header container.
|
||||
/// </summary>
|
||||
public Style HeaderContainerStyle
|
||||
{
|
||||
get => (Style)GetValue(HeaderContainerStyleProperty);
|
||||
set => SetValue(HeaderContainerStyleProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the style for the footer container.
|
||||
/// </summary>
|
||||
public Style FooterContainerStyle
|
||||
{
|
||||
get => (Style)GetValue(FooterContainerStyleProperty);
|
||||
set => SetValue(FooterContainerStyleProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a value that indicates whether the <see cref="Footer"/> is <see langword="null" />.
|
||||
/// </summary>
|
||||
public bool HasFooter => (bool)GetValue(HasFooterProperty);
|
||||
|
||||
private static void OnFooterChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
|
||||
{
|
||||
Node node = (Node)d;
|
||||
node.SetValue(HasFooterPropertyKey, e.NewValue != null ? BoxValue.True : BoxValue.False);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <inheritdoc cref="ItemsControl.GroupStyle"/>
|
||||
public ObservableCollection<GroupStyle> InputGroupStyle { get; } = new ObservableCollection<GroupStyle>();
|
||||
/// <inheritdoc cref="ItemsControl.GroupStyle"/>
|
||||
public ObservableCollection<GroupStyle> OutputGroupStyle { get; } = new ObservableCollection<GroupStyle>();
|
||||
|
||||
protected ItemsControl? InputItemsControl { get; private set; }
|
||||
protected ItemsControl? OutputItemsControl { get; private set; }
|
||||
|
||||
static Node()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(Node), new FrameworkPropertyMetadata(typeof(Node)));
|
||||
FocusableProperty.OverrideMetadata(typeof(Node), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
}
|
||||
|
||||
public Node()
|
||||
{
|
||||
InputGroupStyle.CollectionChanged += OnInputGroupStyleCollectionChanged;
|
||||
OutputGroupStyle.CollectionChanged += OnOutputGroupStyleCollectionChanged;
|
||||
}
|
||||
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
InputItemsControl = GetTemplateChild(ElementInputItemsControl) as ItemsControl;
|
||||
OutputItemsControl = GetTemplateChild(ElementOutputItemsControl) as ItemsControl;
|
||||
|
||||
if (InputItemsControl != null)
|
||||
{
|
||||
foreach (var style in InputGroupStyle)
|
||||
{
|
||||
InputItemsControl.GroupStyle.Add(style);
|
||||
}
|
||||
}
|
||||
|
||||
if (OutputItemsControl != null)
|
||||
{
|
||||
foreach (var style in OutputGroupStyle)
|
||||
{
|
||||
OutputItemsControl.GroupStyle.Add(style);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void OnInputGroupStyleCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (InputItemsControl != null)
|
||||
{
|
||||
SynchronizeCollection(InputItemsControl.GroupStyle, e);
|
||||
}
|
||||
}
|
||||
|
||||
private void OnOutputGroupStyleCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
if (OutputItemsControl != null)
|
||||
{
|
||||
SynchronizeCollection(OutputItemsControl.GroupStyle, e);
|
||||
}
|
||||
}
|
||||
|
||||
private static void SynchronizeCollection(ObservableCollection<GroupStyle> collection, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
switch (e.Action)
|
||||
{
|
||||
case NotifyCollectionChangedAction.Add:
|
||||
if (e.NewItems != null)
|
||||
{
|
||||
for (int i = 0; i < e.NewItems.Count; i++)
|
||||
{
|
||||
var item = (GroupStyle)e.NewItems[i]!;
|
||||
collection.Add(item);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Remove:
|
||||
if (e.OldItems != null)
|
||||
{
|
||||
for (int i = 0; i < e.OldItems.Count; i++)
|
||||
{
|
||||
var item = (GroupStyle)e.OldItems[i]!;
|
||||
collection.Remove(item);
|
||||
}
|
||||
}
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Replace:
|
||||
collection[e.NewStartingIndex] = (GroupStyle)e.NewItems![0]!;
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Move:
|
||||
collection.Move(e.OldStartingIndex, e.NewStartingIndex);
|
||||
break;
|
||||
case NotifyCollectionChangedAction.Reset:
|
||||
collection.Clear();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
59
Nodify/Nodes/NodeInput.cs
Normal file
59
Nodify/Nodes/NodeInput.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the default control for the <see cref="Node.InputConnectorTemplate"/>.
|
||||
/// </summary>
|
||||
public class NodeInput : Connector
|
||||
{
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty HeaderProperty = HeaderedContentControl.HeaderProperty.AddOwner(typeof(NodeInput));
|
||||
public static readonly DependencyProperty HeaderTemplateProperty = HeaderedContentControl.HeaderTemplateProperty.AddOwner(typeof(NodeInput));
|
||||
public static readonly DependencyProperty ConnectorTemplateProperty = DependencyProperty.Register(nameof(ConnectorTemplate), typeof(ControlTemplate), typeof(NodeInput));
|
||||
public static readonly DependencyProperty OrientationProperty = StackPanel.OrientationProperty.AddOwner(typeof(NodeInput), new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure));
|
||||
|
||||
/// <summary>
|
||||
/// Gets of sets the data used for the control's header.
|
||||
/// </summary>
|
||||
public object Header
|
||||
{
|
||||
get => GetValue(HeaderProperty);
|
||||
set => SetValue(HeaderProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the template used to display the content of the control's header.
|
||||
/// </summary>
|
||||
public DataTemplate HeaderTemplate
|
||||
{
|
||||
get => (DataTemplate)GetValue(HeaderTemplateProperty);
|
||||
set => SetValue(HeaderTemplateProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the template used to display the connecting point of this <see cref="Connector"/>.
|
||||
/// </summary>
|
||||
public ControlTemplate ConnectorTemplate
|
||||
{
|
||||
get => (ControlTemplate)GetValue(ConnectorTemplateProperty);
|
||||
set => SetValue(ConnectorTemplateProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="StackPanel.Orientation" />
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => (Orientation)GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
static NodeInput()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(NodeInput), new FrameworkPropertyMetadata(typeof(NodeInput)));
|
||||
}
|
||||
}
|
||||
}
|
||||
59
Nodify/Nodes/NodeOutput.cs
Normal file
59
Nodify/Nodes/NodeOutput.cs
Normal file
@@ -0,0 +1,59 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents the default control for the <see cref="Node.OutputConnectorTemplate"/>.
|
||||
/// </summary>
|
||||
public class NodeOutput : Connector
|
||||
{
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty HeaderProperty = HeaderedContentControl.HeaderProperty.AddOwner(typeof(NodeOutput));
|
||||
public static readonly DependencyProperty HeaderTemplateProperty = HeaderedContentControl.HeaderTemplateProperty.AddOwner(typeof(NodeOutput));
|
||||
public static readonly DependencyProperty ConnectorTemplateProperty = NodeInput.ConnectorTemplateProperty.AddOwner(typeof(NodeOutput));
|
||||
public static readonly DependencyProperty OrientationProperty = NodeInput.OrientationProperty.AddOwner(typeof(NodeOutput), new FrameworkPropertyMetadata(Orientation.Horizontal, FrameworkPropertyMetadataOptions.AffectsMeasure));
|
||||
|
||||
/// <summary>
|
||||
/// Gets of sets the data used for the control's header.
|
||||
/// </summary>
|
||||
public object Header
|
||||
{
|
||||
get => GetValue(HeaderProperty);
|
||||
set => SetValue(HeaderProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the template used to display the content of the control's header.
|
||||
/// </summary>
|
||||
public DataTemplate HeaderTemplate
|
||||
{
|
||||
get => (DataTemplate)GetValue(HeaderTemplateProperty);
|
||||
set => SetValue(HeaderTemplateProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the template used to display the connecting point of this <see cref="Connector"/>.
|
||||
/// </summary>
|
||||
public ControlTemplate ConnectorTemplate
|
||||
{
|
||||
get => (ControlTemplate)GetValue(ConnectorTemplateProperty);
|
||||
set => SetValue(ConnectorTemplateProperty, value);
|
||||
}
|
||||
|
||||
/// <inheritdoc cref="StackPanel.Orientation" />
|
||||
public Orientation Orientation
|
||||
{
|
||||
get => (Orientation)GetValue(OrientationProperty);
|
||||
set => SetValue(OrientationProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
static NodeOutput()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(NodeOutput), new FrameworkPropertyMetadata(typeof(NodeOutput)));
|
||||
}
|
||||
}
|
||||
}
|
||||
100
Nodify/Nodes/StateNode.cs
Normal file
100
Nodify/Nodes/StateNode.cs
Normal file
@@ -0,0 +1,100 @@
|
||||
using System.Windows;
|
||||
using System.Windows.Controls;
|
||||
using System.Windows.Input;
|
||||
using System.Windows.Media;
|
||||
|
||||
namespace Nodify
|
||||
{
|
||||
/// <summary>
|
||||
/// Represents a control that acts as a <see cref="Connector"/>.
|
||||
/// </summary>
|
||||
[TemplatePart(Name = ElementContent, Type = typeof(UIElement))]
|
||||
public class StateNode : Connector
|
||||
{
|
||||
protected const string ElementContent = "PART_Content";
|
||||
|
||||
#region Dependency Properties
|
||||
|
||||
public static readonly DependencyProperty HighlightBrushProperty = ItemContainer.HighlightBrushProperty.AddOwner(typeof(StateNode));
|
||||
public static readonly DependencyProperty ContentProperty = ContentPresenter.ContentProperty.AddOwner(typeof(StateNode));
|
||||
public static readonly DependencyProperty ContentTemplateProperty = ContentPresenter.ContentTemplateProperty.AddOwner(typeof(StateNode));
|
||||
public static readonly DependencyProperty CornerRadiusProperty = Border.CornerRadiusProperty.AddOwner(typeof(StateNode));
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the brush used when the <see cref="PendingConnection.IsOverElementProperty"/> attached property is true for this <see cref="StateNode"/>.
|
||||
/// </summary>
|
||||
public Brush HighlightBrush
|
||||
{
|
||||
get => (Brush)GetValue(HighlightBrushProperty);
|
||||
set => SetValue(HighlightBrushProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the data for the control's content.
|
||||
/// </summary>
|
||||
public object Content
|
||||
{
|
||||
get => GetValue(ContentProperty);
|
||||
set => SetValue(ContentProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets the template used to display the content of the control's header.
|
||||
/// </summary>
|
||||
public DataTemplate ContentTemplate
|
||||
{
|
||||
get => (DataTemplate)GetValue(ContentTemplateProperty);
|
||||
set => SetValue(ContentTemplateProperty, value);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets or sets a value that represents the degree to which the corners of the <see cref="StateNode"/> are rounded.
|
||||
/// </summary>
|
||||
public CornerRadius CornerRadius
|
||||
{
|
||||
get => (CornerRadius)GetValue(CornerRadiusProperty);
|
||||
set => SetValue(CornerRadiusProperty, value);
|
||||
}
|
||||
|
||||
#endregion
|
||||
|
||||
/// <summary>
|
||||
/// Gets the <see cref="ContentControl"/> control of this <see cref="StateNode"/>.
|
||||
/// </summary>
|
||||
protected UIElement? ContentControl { get; private set; }
|
||||
|
||||
static StateNode()
|
||||
{
|
||||
DefaultStyleKeyProperty.OverrideMetadata(typeof(StateNode), new FrameworkPropertyMetadata(typeof(StateNode)));
|
||||
FocusableProperty.OverrideMetadata(typeof(StateNode), new FrameworkPropertyMetadata(BoxValue.False));
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
public override void OnApplyTemplate()
|
||||
{
|
||||
base.OnApplyTemplate();
|
||||
|
||||
ContentControl = Template.FindName(ElementContent, this) as UIElement;
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnMouseDown(MouseButtonEventArgs e)
|
||||
{
|
||||
// Do not raise PendingConnection events if clicked on content
|
||||
if (e.OriginalSource is Visual visual && (!ContentControl?.IsAncestorOf(visual) ?? true))
|
||||
{
|
||||
base.OnMouseDown(e);
|
||||
}
|
||||
}
|
||||
|
||||
/// <inheritdoc />
|
||||
protected override void OnMouseUp(MouseButtonEventArgs e)
|
||||
{
|
||||
// Do not raise PendingConnection events if clicked on content
|
||||
if (e.OriginalSource is Visual visual && (!ContentControl?.IsAncestorOf(visual) ?? true))
|
||||
{
|
||||
base.OnMouseUp(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user