Add project files.

This commit is contained in:
Ankitkumar Satapara
2026-04-17 22:31:58 +05:30
commit 21aaef6776
473 changed files with 50152 additions and 0 deletions

View File

@@ -0,0 +1,588 @@
using Nodify.Events;
using Nodify.Interactivity;
using System.Diagnostics;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Input;
namespace Nodify
{
/// <summary>
/// Represents a connector control that can start and complete a <see cref="PendingConnection"/>.
/// Has a <see cref="ElementConnector"/> that the <see cref="Anchor"/> is calculated from for the <see cref="PendingConnection"/>. Center of this control is used if missing.
/// </summary>
[TemplatePart(Name = ElementConnector, Type = typeof(FrameworkElement))]
public class Connector : Control
{
protected const string ElementConnector = "PART_Connector";
#region Routed Events
public static readonly RoutedEvent PendingConnectionStartedEvent = EventManager.RegisterRoutedEvent(nameof(PendingConnectionStarted), RoutingStrategy.Bubble, typeof(PendingConnectionEventHandler), typeof(Connector));
public static readonly RoutedEvent PendingConnectionCompletedEvent = EventManager.RegisterRoutedEvent(nameof(PendingConnectionCompleted), RoutingStrategy.Bubble, typeof(PendingConnectionEventHandler), typeof(Connector));
public static readonly RoutedEvent PendingConnectionDragEvent = EventManager.RegisterRoutedEvent(nameof(PendingConnectionDrag), RoutingStrategy.Bubble, typeof(PendingConnectionEventHandler), typeof(Connector));
public static readonly RoutedEvent DisconnectEvent = EventManager.RegisterRoutedEvent(nameof(Disconnect), RoutingStrategy.Bubble, typeof(ConnectorEventHandler), typeof(Connector));
/// <summary>Triggered by the <see cref="EditorGestures.ConnectorGestures.Connect"/> gesture.</summary>
public event PendingConnectionEventHandler PendingConnectionStarted
{
add => AddHandler(PendingConnectionStartedEvent, value);
remove => RemoveHandler(PendingConnectionStartedEvent, value);
}
/// <summary>Triggered by the <see cref="EditorGestures.ConnectorGestures.Connect"/> gesture.</summary>
public event PendingConnectionEventHandler PendingConnectionCompleted
{
add => AddHandler(PendingConnectionCompletedEvent, value);
remove => RemoveHandler(PendingConnectionCompletedEvent, value);
}
/// <summary>
/// Occurs when the mouse is changing position and the <see cref="Connector"/> has mouse capture.
/// </summary>
public event PendingConnectionEventHandler PendingConnectionDrag
{
add => AddHandler(PendingConnectionDragEvent, value);
remove => RemoveHandler(PendingConnectionDragEvent, value);
}
/// <summary>Triggered by the <see cref="EditorGestures.ConnectorGestures.Disconnect"/> gesture.</summary>
public event ConnectorEventHandler Disconnect
{
add => AddHandler(DisconnectEvent, value);
remove => RemoveHandler(DisconnectEvent, value);
}
#endregion
#region Dependency Properties
public static readonly DependencyProperty AnchorProperty = DependencyProperty.Register(nameof(Anchor), typeof(Point), typeof(Connector), new FrameworkPropertyMetadata(BoxValue.Point));
public static readonly DependencyProperty IsConnectedProperty = DependencyProperty.Register(nameof(IsConnected), typeof(bool), typeof(Connector), new FrameworkPropertyMetadata(BoxValue.False, OnIsConnectedChanged));
public static readonly DependencyProperty DisconnectCommandProperty = DependencyProperty.Register(nameof(DisconnectCommand), typeof(ICommand), typeof(Connector));
private static readonly DependencyPropertyKey IsPendingConnectionPropertyKey = DependencyProperty.RegisterReadOnly(nameof(IsPendingConnection), typeof(bool), typeof(Connector), new FrameworkPropertyMetadata(BoxValue.False));
public static readonly DependencyProperty IsPendingConnectionProperty = IsPendingConnectionPropertyKey.DependencyProperty;
public static readonly DependencyProperty HasCustomContextMenuProperty = NodifyEditor.HasCustomContextMenuProperty.AddOwner(typeof(Connector));
/// <summary>
/// Gets the location in graph space coordinates where <see cref="Connection"/>s can be attached to.
/// Bind with <see cref="System.Windows.Data.BindingMode.OneWayToSource"/>
/// </summary>
public Point Anchor
{
get => (Point)GetValue(AnchorProperty);
set => SetValue(AnchorProperty, value);
}
/// <summary>
/// If this is set to false, the <see cref="Disconnect"/> event will not be invoked and the connector will stop updating its <see cref="Anchor"/> when moved, resized etc.
/// </summary>
public bool IsConnected
{
get => (bool)GetValue(IsConnectedProperty);
set => SetValue(IsConnectedProperty, value);
}
/// <summary>
/// Gets a value that indicates whether a <see cref="PendingConnection"/> is in progress for this <see cref="Connector"/>.
/// </summary>
public bool IsPendingConnection
{
get => (bool)GetValue(IsPendingConnectionProperty);
protected set => SetValue(IsPendingConnectionPropertyKey, value);
}
/// <summary>
/// Invoked if the <see cref="Disconnect"/> event is not handled.
/// Parameter is the <see cref="FrameworkElement.DataContext"/> of this control.
/// </summary>
public ICommand? DisconnectCommand
{
get => (ICommand?)GetValue(DisconnectCommandProperty);
set => SetValue(DisconnectCommandProperty, value);
}
/// <summary>
/// Gets or sets a value indicating whether the connector uses a custom context menu.
/// </summary>
/// <remarks>When set to true, the connector handles the right-click event for specific interactions.</remarks>
public bool HasCustomContextMenu
{
get => (bool)GetValue(HasCustomContextMenuProperty);
set => SetValue(HasCustomContextMenuProperty, value);
}
/// <summary>
/// Gets a value indicating whether the connector has a context menu.
/// </summary>
public bool HasContextMenu => ContextMenu != null || HasCustomContextMenu;
#endregion
#region Fields
private FrameworkElement? _thumb;
/// <summary>
/// Gets the <see cref="FrameworkElement"/> used to calculate the <see cref="Anchor"/>.
/// </summary>
protected internal FrameworkElement Thumb => _thumb ??= Template.FindName(ElementConnector, this) as FrameworkElement ?? this;
/// <summary>
/// Gets the <see cref="ItemContainer"/> that contains this <see cref="Connector"/>.
/// </summary>
public ItemContainer? Container { get; private set; }
/// <summary>
/// Gets the <see cref="NodifyEditor"/> that owns this <see cref="Container"/>.
/// </summary>
public NodifyEditor? Editor { get; private set; }
/// <summary>
/// Gets or sets the safe zone outside the editor's viewport that will not trigger optimizations.
/// </summary>
public static double OptimizeSafeZone = 1000d;
/// <summary>
/// Gets or sets the minimum selected items needed to trigger optimizations when outside of the <see cref="OptimizeSafeZone"/>.
/// </summary>
public static uint OptimizeMinimumSelectedItems = 100;
/// <summary>
/// Gets or sets if <see cref="Connector"/>s should enable optimizations based on <see cref="OptimizeSafeZone"/> and <see cref="OptimizeMinimumSelectedItems"/>.
/// </summary>
public static bool EnableOptimizations = false;
/// <summary>
/// Gets or sets whether cancelling a pending connection is allowed.
/// </summary>
public static bool AllowPendingConnectionCancellation { get; set; } = true;
private Point _lastUpdatedContainerPosition;
private Point _pendingConnectionEndPosition;
private bool _isHooked;
#endregion
static Connector()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(Connector), new FrameworkPropertyMetadata(typeof(Connector)));
FocusableProperty.OverrideMetadata(typeof(Connector), new FrameworkPropertyMetadata(BoxValue.True));
}
public Connector()
{
InputProcessor.AddSharedHandlers(this);
Loaded += OnConnectorLoaded;
Unloaded += OnConnectorUnloaded;
}
/// <inheritdoc />
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
Container = this.GetParentOfType<ItemContainer>();
Editor = Container?.Editor ?? this.GetParentOfType<NodifyEditor>();
}
#region Update Anchor
// Toggle events that could be used to update the Anchor
private void TrySetAnchorUpdateEvents(bool value)
{
if (Container != null && Editor != null)
{
// If events are not already hooked and we are asked to subscribe
if (value && !_isHooked)
{
Container.PreviewLocationChanged += UpdateAnchorOptimized;
Container.LocationChanged += OnLocationChanged;
Container.SizeChanged += OnContainerSizeChanged;
Editor.ViewportUpdated += OnViewportUpdated;
_isHooked = true;
}
// If events are already hooked and we are asked to unsubscribe
else if (_isHooked && !value)
{
Container.PreviewLocationChanged -= UpdateAnchorOptimized;
Container.LocationChanged -= OnLocationChanged;
Container.SizeChanged -= OnContainerSizeChanged;
Editor.ViewportUpdated -= OnViewportUpdated;
_isHooked = false;
}
}
}
private void OnContainerSizeChanged(object sender, SizeChangedEventArgs e)
=> UpdateAnchorOptimized(Container!.Location);
private void OnConnectorLoaded(object sender, RoutedEventArgs? e)
=> TrySetAnchorUpdateEvents(true);
private void OnConnectorUnloaded(object sender, RoutedEventArgs e)
=> TrySetAnchorUpdateEvents(false);
private static void OnIsConnectedChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var con = (Connector)d;
if ((bool)e.NewValue)
{
con.UpdateAnchor();
}
}
/// <inheritdoc />
protected override void OnRenderSizeChanged(SizeChangedInfo sizeInfo)
{
// Subscribe to events if not already subscribed
// Useful for advanced connectors that start collapsed because the loaded event is not called
Size newSize = sizeInfo.NewSize;
if (newSize.Width > 0d || newSize.Height > 0d)
{
TrySetAnchorUpdateEvents(true);
if (Container != null)
{
UpdateAnchorOptimized(Container!.Location);
}
}
}
private void OnLocationChanged(object sender, RoutedEventArgs e)
=> UpdateAnchorOptimized(Container!.Location);
private void OnViewportUpdated(object sender, RoutedEventArgs args)
{
if (Container != null && !Container.IsPreviewingLocation && _lastUpdatedContainerPosition != Container.Location)
{
UpdateAnchorOptimized(Container.Location);
}
}
/// <summary>
/// Updates the <see cref="Anchor"/> and applies optimizations if needed based on <see cref="EnableOptimizations"/> flag
/// </summary>
/// <param name="location"></param>
protected void UpdateAnchorOptimized(Point location)
{
// Update only connectors that are connected
if (Editor != null && IsConnected)
{
bool shouldOptimize = EnableOptimizations && Editor.SelectedContainersCount >= OptimizeMinimumSelectedItems;
if (shouldOptimize)
{
UpdateAnchorBasedOnLocation(Editor, location);
}
else
{
UpdateAnchor(location);
}
}
}
private void UpdateAnchorBasedOnLocation(NodifyEditor editor, Point location)
{
var viewport = new Rect(editor.ViewportLocation, editor.ViewportSize);
double offset = OptimizeSafeZone / editor.ViewportZoom;
Rect area = Rect.Inflate(viewport, offset, offset);
// Update only the connectors that are in the viewport or will be in the viewport
if (area.Contains(location))
{
UpdateAnchor(location);
}
}
/// <summary>
/// Updates the <see cref="Anchor"/> relative to a location. (usually <see cref="Container"/>'s location)
/// </summary>
/// <param name="location">The relative location</param>
protected void UpdateAnchor(Point location)
{
_lastUpdatedContainerPosition = location;
if (Thumb != null && Container != null)
{
var thumbSize = (Vector)Thumb.RenderSize;
Vector containerMargin = (Vector)Container.RenderSize - (Vector)Container.DesiredSize;
Point relativeLocation = Thumb.TranslatePoint((Point)(thumbSize / 2 - containerMargin / 2), Container);
Anchor = new Point(location.X + relativeLocation.X, location.Y + relativeLocation.Y);
}
}
/// <summary>
/// Updates the <see cref="Anchor"/> based on <see cref="Container"/>'s location.
/// </summary>
public void UpdateAnchor()
{
if (Container != null)
{
UpdateAnchor(Container.Location);
}
}
#endregion
#region Gesture Handling
protected InputProcessor InputProcessor { get; } = new InputProcessor();
/// <inheritdoc />
protected override void OnMouseDown(MouseButtonEventArgs e)
=> InputProcessor.ProcessEvent(e);
/// <inheritdoc />
protected override void OnMouseUp(MouseButtonEventArgs e)
{
InputProcessor.ProcessEvent(e);
// Release the mouse capture if all the mouse buttons are released and there's no interaction in progress
if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && e.RightButton == MouseButtonState.Released && e.LeftButton == MouseButtonState.Released && e.MiddleButton == MouseButtonState.Released)
{
ReleaseMouseCapture();
}
}
/// <inheritdoc />
protected override void OnMouseMove(MouseEventArgs e)
=> InputProcessor.ProcessEvent(e);
/// <inheritdoc />
protected override void OnMouseWheel(MouseWheelEventArgs e)
=> InputProcessor.ProcessEvent(e);
/// <inheritdoc />
protected override void OnLostMouseCapture(MouseEventArgs e)
=> InputProcessor.ProcessEvent(e);
/// <inheritdoc />
protected override void OnKeyUp(KeyEventArgs e)
{
InputProcessor.ProcessEvent(e);
// Release the mouse capture if all the mouse buttons are released and there's no interaction in progress
if (!InputProcessor.RequiresInputCapture && IsMouseCaptured && Mouse.RightButton == MouseButtonState.Released && Mouse.LeftButton == MouseButtonState.Released && Mouse.MiddleButton == MouseButtonState.Released)
{
ReleaseMouseCapture();
}
}
/// <inheritdoc />
protected override void OnKeyDown(KeyEventArgs e)
=> InputProcessor.ProcessEvent(e);
#endregion
#region Methods
/// <summary>
/// Initiates a new pending connection from this connector (see <see cref="IsPendingConnection"/>).
/// </summary>
/// <remarks>This method has no effect if a pending connection is already in progress.</remarks>
public void BeginConnecting()
=> BeginConnecting(new Vector(0, 0));
/// <summary>
/// Initiates a new pending connection from this connector with the specified offset (see <see cref="IsPendingConnection"/>).
/// </summary>
/// <remarks>This method has no effect if a pending connection is already in progress.</remarks>
public void BeginConnecting(Vector offset)
{
if (IsPendingConnection)
{
return;
}
UpdateAnchor();
_pendingConnectionEndPosition = Anchor + offset;
var args = new PendingConnectionEventArgs(DataContext)
{
RoutedEvent = PendingConnectionStartedEvent,
Anchor = Anchor,
Source = this
};
RaiseEvent(args);
IsPendingConnection = !args.Canceled;
}
/// <summary>
/// Updates the endpoint of the pending connection by adjusting its position with the specified offset.
/// </summary>
/// <param name="offset">The amount to adjust the pending connection's endpoint.</param>
public void UpdatePendingConnection(Vector offset)
=> UpdatePendingConnection(_pendingConnectionEndPosition + offset);
/// <summary>
/// Updates the endpoint of the pending connection to the specified position.
/// </summary>
/// <param name="position">The new position for the connection's endpoint.</param>
public void UpdatePendingConnection(Point position)
{
//Debug.Assert(IsPendingConnection);
_pendingConnectionEndPosition = position;
var args = new PendingConnectionEventArgs(DataContext)
{
RoutedEvent = PendingConnectionDragEvent,
OffsetX = _pendingConnectionEndPosition.X - Anchor.X,
OffsetY = _pendingConnectionEndPosition.Y - Anchor.Y,
Anchor = Anchor,
Source = this
};
RaiseEvent(args);
}
/// <summary>
/// Cancels the current pending connection without completing it if <see cref="AllowPendingConnectionCancellation"/> is true.
/// Otherwise, it completes the pending connection by calling <see cref="EndConnecting()"/>.
/// </summary>
/// <remarks>This method has no effect if there's no pending connection.</remarks>
public void CancelConnecting()
{
if (!AllowPendingConnectionCancellation)
{
EndConnecting();
return;
}
if (IsPendingConnection)
{
var args = new PendingConnectionEventArgs(DataContext)
{
RoutedEvent = PendingConnectionCompletedEvent,
Anchor = Anchor,
Source = this,
Canceled = true
};
RaiseEvent(args);
IsPendingConnection = false;
}
}
/// <summary>
/// Completes the current pending connection.
/// </summary>
/// <remarks>
/// Attempts to identify a target connector near the connection's endpoint and completes the pending connection.
/// If no target connector is found, the connection may be completed without a valid target.
/// This method has no effect if there's no pending connection.
/// </remarks>
public void EndConnecting()
{
if (!IsPendingConnection)
{
return;
}
FrameworkElement? elem = FindConnectionTarget(_pendingConnectionEndPosition);
EndConnecting(elem?.DataContext);
}
/// <summary>
/// Completes the current pending connection using the specified connector as the target.
/// </summary>
/// <param name="connector">The connector to use as the connection target.</param>
/// <remarks>This method has no effect if there's no pending connection.</remarks>
public void EndConnecting(Connector connector)
=> EndConnecting(connector.DataContext);
private void EndConnecting(object? targetDataContext)
{
if (!IsPendingConnection)
{
return;
}
var args = new PendingConnectionEventArgs(DataContext)
{
TargetConnector = targetDataContext,
RoutedEvent = PendingConnectionCompletedEvent,
Anchor = Anchor,
Source = this
};
RaiseEvent(args);
IsPendingConnection = false;
_pendingConnectionEndPosition = Anchor;
}
/// <summary>
/// Removes all connections associated with this connector.
/// </summary>
/// <remarks>This method has no effect if a pending connection is already in progress or the connector is not connected (see <see cref="IsConnected"/>).</remarks>
public void RemoveConnections()
{
if (!IsConnected || IsPendingConnection)
{
return;
}
object? connector = DataContext;
var args = new ConnectorEventArgs(connector)
{
RoutedEvent = DisconnectEvent,
Anchor = Anchor,
Source = this
};
RaiseEvent(args);
// Raise DisconnectCommand if event is not handled
if (!args.Handled && (DisconnectCommand?.CanExecute(connector) ?? false))
{
DisconnectCommand.Execute(connector);
}
}
/// <summary>
/// Translates the event location to graph space coordinates (relative to the <see cref="NodifyEditor.ItemsHost" />).
/// </summary>
/// <param name="e">The mouse event.</param>
/// <remarks>
/// Call <see cref="UpdateAnchor()"/> before calling this method if the <see cref="Anchor"/> is not up-to-date.
/// </remarks>
internal Point GetLocationInsideEditor(MouseEventArgs e)
{
Vector thumbOffset = e.GetPosition(Thumb) - new Point(Thumb.ActualWidth / 2, Thumb.ActualHeight / 2);
return Anchor + thumbOffset;
}
/// <summary>
/// Searches for a <see cref="Connector"/> at the specified position.
/// </summary>
/// <param name="position">The position in the editor to check for a connector.</param>
public Connector? FindTargetConnector(Point position)
{
if (Editor != null)
{
return (Connector?)PendingConnection.GetPotentialConnector(Editor, position, true);
}
return null;
}
/// <summary>
/// Searches for a potential <see cref="Connector"/> or <see cref="ItemContainer"/> at the specified position within the editor.
/// </summary>
/// <param name="position">The position in the editor to check for a potential connection target.</param>
public FrameworkElement? FindConnectionTarget(Point position)
{
if (Editor != null)
{
return PendingConnection.GetPotentialConnector(Editor, position);
}
return null;
}
#endregion
}
}

View File

@@ -0,0 +1,38 @@
using System;
using System.Windows;
namespace Nodify.Events
{
/// <summary>
/// Represents the method that will handle <see cref="Connector"/> related routed events.
/// </summary>
/// <param name="sender">The object where the event handler is attached.</param>
/// <param name="e">The event data.</param>
public delegate void ConnectorEventHandler(object sender, ConnectorEventArgs e);
/// <summary>
/// Provides data for <see cref="Nodify.Connector"/> related routed events.
/// </summary>
public class ConnectorEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="ConnectorEventArgs"/> class using the specified <see cref="Connector"/>.
/// </summary>
/// <param name="connector">The <see cref="FrameworkElement.DataContext"/> of a related <see cref="Nodify.Connector"/>.</param>
public ConnectorEventArgs(object connector)
=> Connector = connector;
/// <summary>
/// Gets or sets the <see cref="Nodify.Connector.Anchor"/> of the <see cref="Nodify.Connector"/> associated with this event.
/// </summary>
public Point Anchor { get; set; }
/// <summary>
/// Gets the <see cref="FrameworkElement.DataContext"/> of the <see cref="Nodify.Connector"/> associated with this event.
/// </summary>
public object Connector { get; }
protected override void InvokeEventHandler(Delegate genericHandler, object genericTarget)
=> ((ConnectorEventHandler)genericHandler)(genericTarget, this);
}
}

View File

@@ -0,0 +1,58 @@
using System;
using System.Windows;
namespace Nodify.Events
{
/// <summary>
/// Represents the method that will handle <see cref="PendingConnection"/> related routed events.
/// </summary>
/// <param name="sender">The object where the event handler is attached.</param>
/// <param name="e">The event data.</param>
public delegate void PendingConnectionEventHandler(object sender, PendingConnectionEventArgs e);
/// <summary>
/// Provides data for <see cref="PendingConnection"/> related routed events.
/// </summary>
public class PendingConnectionEventArgs : RoutedEventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="PendingConnectionEventArgs"/> class using the specified <see cref="SourceConnector"/>.
/// </summary>
/// <param name="sourceConnector">The <see cref="FrameworkElement.DataContext"/> of a related <see cref="Connector"/>.</param>
public PendingConnectionEventArgs(object sourceConnector)
=> SourceConnector = sourceConnector;
/// <summary>
/// Gets or sets the <see cref="Connector.Anchor"/> of the <see cref="Connector"/> that raised this event.
/// </summary>
public Point Anchor { get; set; }
/// <summary>
/// Gets the <see cref="FrameworkElement.DataContext"/> of the <see cref="Connector"/> that started this <see cref="PendingConnection"/>.
/// </summary>
public object SourceConnector { get; }
/// <summary>
/// Gets or sets the <see cref="FrameworkElement.DataContext"/> of the target <see cref="Connector"/> when the <see cref="PendingConnection"/> is completed.
/// </summary>
public object? TargetConnector { get; set; }
/// <summary>
/// Gets or sets the distance from the <see cref="SourceConnector"/> in the X axis.
/// </summary>
public double OffsetX { get; set; }
/// <summary>
/// Gets or sets the distance from the <see cref="SourceConnector"/> in the Y axis.
/// </summary>
public double OffsetY { get; set; }
/// <summary>
/// Gets or sets a value that indicates whether this <see cref="PendingConnection"/> was cancelled.
/// </summary>
public bool Canceled { get; set; }
protected override void InvokeEventHandler(Delegate genericHandler, object genericTarget)
=> ((PendingConnectionEventHandler)genericHandler)(genericTarget, this);
}
}

View File

@@ -0,0 +1,21 @@
using System.Windows.Controls;
using System.Windows;
namespace Nodify
{
public class HotKeyControl : Control
{
public static readonly DependencyProperty NumberProperty = DependencyProperty.Register(nameof(Number), typeof(int), typeof(HotKeyControl), new PropertyMetadata(BoxValue.Int0));
public int Number
{
get => (int)GetValue(NumberProperty);
set => SetValue(NumberProperty, value);
}
static HotKeyControl()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(HotKeyControl), new FrameworkPropertyMetadata(typeof(HotKeyControl)));
}
}
}

View File

@@ -0,0 +1,621 @@
using Nodify.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Windows;
using System.Windows.Controls;
using System.Windows.Documents;
using System.Windows.Input;
using System.Windows.Media;
using System.Windows.Shapes;
namespace Nodify
{
/// <summary>
/// Specifies how hotkeys are displayed for a pending connection.
/// </summary>
public enum HotKeysDisplayMode
{
/// <summary>
/// No hotkeys will be displayed for the pending connection.
/// </summary>
None,
/// <summary>
/// Display hotkeys for keyboard only.
/// </summary>
Keyboard,
/// <summary>
/// Display hotkeys for both mouse and keyboard.
/// </summary>
All
}
/// <summary>
/// Represents a pending connection usually started by a <see cref="Connector"/> which invokes the <see cref="CompletedCommand"/> when completed.
/// </summary>
public class PendingConnection : ContentControl
{
#region Dependency Properties
public static readonly DependencyProperty SourceAnchorProperty = DependencyProperty.Register(nameof(SourceAnchor), typeof(Point), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.Point, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty TargetAnchorProperty = DependencyProperty.Register(nameof(TargetAnchor), typeof(Point), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.Point, FrameworkPropertyMetadataOptions.AffectsRender));
public static readonly DependencyProperty SourceProperty = DependencyProperty.Register(nameof(Source), typeof(object), typeof(PendingConnection));
public static readonly DependencyProperty TargetProperty = DependencyProperty.Register(nameof(Target), typeof(object), typeof(PendingConnection));
public static readonly DependencyProperty PreviewTargetProperty = DependencyProperty.Register(nameof(PreviewTarget), typeof(object), typeof(PendingConnection));
public static readonly DependencyProperty EnablePreviewProperty = DependencyProperty.Register(nameof(EnablePreview), typeof(bool), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.False));
public static readonly DependencyProperty StrokeThicknessProperty = Shape.StrokeThicknessProperty.AddOwner(typeof(PendingConnection));
public static readonly DependencyProperty StrokeDashArrayProperty = Shape.StrokeDashArrayProperty.AddOwner(typeof(PendingConnection));
public static readonly DependencyProperty StrokeProperty = Shape.StrokeProperty.AddOwner(typeof(PendingConnection));
public static readonly DependencyProperty AllowOnlyConnectorsProperty = DependencyProperty.Register(nameof(AllowOnlyConnectors), typeof(bool), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.True, OnAllowOnlyConnectorsChanged));
public static readonly DependencyProperty EnableSnappingProperty = DependencyProperty.Register(nameof(EnableSnapping), typeof(bool), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.False));
public static readonly DependencyProperty DirectionProperty = BaseConnection.DirectionProperty.AddOwner(typeof(PendingConnection));
public new static readonly DependencyProperty IsVisibleProperty = DependencyProperty.Register(nameof(IsVisible), typeof(bool), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.False, FrameworkPropertyMetadataOptions.BindsTwoWayByDefault, OnVisibilityChanged));
private static void OnVisibilityChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
var connection = (PendingConnection)d;
connection.Visibility = ((bool)e.NewValue) ? Visibility.Visible : Visibility.Collapsed;
}
/// <summary>
/// Gets or sets the starting point for the connection.
/// </summary>
public Point SourceAnchor
{
get => (Point)GetValue(SourceAnchorProperty);
set => SetValue(SourceAnchorProperty, value);
}
/// <summary>
/// Gets or sets the end point for the connection.
/// </summary>
public Point TargetAnchor
{
get => (Point)GetValue(TargetAnchorProperty);
set => SetValue(TargetAnchorProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="Connector"/>'s <see cref="FrameworkElement.DataContext"/> that started this pending connection.
/// </summary>
public object? Source
{
get => GetValue(SourceProperty);
set => SetValue(SourceProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="Connector"/>'s <see cref="FrameworkElement.DataContext"/> (or potentially an <see cref="ItemContainer"/>'s <see cref="FrameworkElement.DataContext"/> if <see cref="AllowOnlyConnectors"/> is false) that the <see cref="Source"/> can connect to.
/// Only set when the connection is completed (see <see cref="CompletedCommand"/>).
/// </summary>
public object? Target
{
get => GetValue(TargetProperty);
set => SetValue(TargetProperty, value);
}
/// <summary>
/// <see cref="PreviewTarget"/> will be updated with a potential <see cref="Connector"/>'s <see cref="FrameworkElement.DataContext"/> if this is true.
/// </summary>
/// <remarks>Requires <see cref="EnableHitTesting"/> to be true.</remarks>
public bool EnablePreview
{
get => (bool)GetValue(EnablePreviewProperty);
set => SetValue(EnablePreviewProperty, value);
}
/// <summary>
/// Gets or sets the <see cref="Connector"/> or the <see cref="ItemContainer"/> (if <see cref="AllowOnlyConnectors"/> is false) that we're previewing. See <see cref="EnablePreview"/>.
/// </summary>
public object? PreviewTarget
{
get => GetValue(PreviewTargetProperty);
set => SetValue(PreviewTargetProperty, value);
}
/// <summary>
/// Enables snapping the <see cref="TargetAnchor"/> to a possible <see cref="Target"/> connector.
/// </summary>
/// <remarks>Requires <see cref="EnableHitTesting"/> to be true.</remarks>
public bool EnableSnapping
{
get => (bool)GetValue(EnableSnappingProperty);
set => SetValue(EnableSnappingProperty, value);
}
/// <summary>
/// If true will preview and connect only to <see cref="Connector"/>s, otherwise will also enable <see cref="ItemContainer"/>s.
/// </summary>
public bool AllowOnlyConnectors
{
get => (bool)GetValue(AllowOnlyConnectorsProperty);
set => SetValue(AllowOnlyConnectorsProperty, value);
}
/// <summary>
/// Gets or set the connection thickness.
/// </summary>
public double StrokeThickness
{
get => (double)GetValue(StrokeThicknessProperty);
set => SetValue(StrokeThicknessProperty, value);
}
/// <summary>
/// Gets or sets the pattern of dashes and gaps that is used to outline the connection.
/// </summary>
public DoubleCollection StrokeDashArray
{
get => (DoubleCollection)GetValue(StrokeDashArrayProperty);
set => SetValue(StrokeDashArrayProperty, value);
}
/// <summary>
/// Gets or sets the stroke color of the connection.
/// </summary>
public Brush Stroke
{
get => (Brush)GetValue(StrokeProperty);
set => SetValue(StrokeProperty, value);
}
/// <summary>
/// Gets or sets the visibility of the connection.
/// </summary>
public new bool IsVisible
{
get => base.IsVisible;
set => SetValue(IsVisibleProperty, value);
}
/// <summary>
/// Gets or sets the direction of this connection.
/// </summary>
public ConnectionDirection Direction
{
get => (ConnectionDirection)GetValue(DirectionProperty);
set => SetValue(DirectionProperty, value);
}
#endregion
#region Attached Properties
private static readonly DependencyProperty AllowOnlyConnectorsAttachedProperty = DependencyProperty.RegisterAttached("AllowOnlyConnectorsAttached", typeof(bool), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.True));
/// <summary>
/// Will be set for <see cref="Connector"/>s and <see cref="ItemContainer"/>s when the pending connection is over the element if <see cref="EnablePreview"/> or <see cref="EnableSnapping"/> is true.
/// </summary>
public static readonly DependencyProperty IsOverElementProperty = DependencyProperty.RegisterAttached("IsOverElement", typeof(bool), typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.False));
internal static bool GetAllowOnlyConnectorsAttached(UIElement elem)
=> (bool)elem.GetValue(AllowOnlyConnectorsAttachedProperty);
internal static void SetAllowOnlyConnectorsAttached(UIElement elem, bool value)
=> elem.SetValue(AllowOnlyConnectorsAttachedProperty, value);
public static bool GetIsOverElement(UIElement elem)
=> (bool)elem.GetValue(IsOverElementProperty);
public static void SetIsOverElement(UIElement elem, bool value)
=> elem.SetValue(IsOverElementProperty, value);
private static void OnAllowOnlyConnectorsChanged(DependencyObject d, DependencyPropertyChangedEventArgs e)
{
NodifyEditor? editor = ((PendingConnection)d).Editor;
if (editor != null)
{
SetAllowOnlyConnectorsAttached(editor, (bool)e.NewValue);
}
}
#endregion
#region Commands
public static readonly DependencyProperty StartedCommandProperty = DependencyProperty.Register(nameof(StartedCommand), typeof(ICommand), typeof(PendingConnection));
public static readonly DependencyProperty CompletedCommandProperty = DependencyProperty.Register(nameof(CompletedCommand), typeof(ICommand), typeof(PendingConnection));
/// <summary>
/// Gets or sets the command to invoke when the pending connection is started.
/// Will not be invoked if <see cref="NodifyEditor.ConnectionStartedCommand"/> is used.
/// <see cref="Source"/> will be set to the <see cref="Connector"/>'s <see cref="FrameworkElement.DataContext"/> that started this connection and will also be the command's parameter.
/// </summary>
public ICommand? StartedCommand
{
get => (ICommand?)GetValue(StartedCommandProperty);
set => SetValue(StartedCommandProperty, value);
}
/// <summary>
/// Gets or sets the command to invoke when the pending connection is completed.
/// Will not be invoked if <see cref="NodifyEditor.ConnectionCompletedCommand"/> is used.
/// <see cref="Target"/> will be set to the desired <see cref="Connector"/>'s <see cref="FrameworkElement.DataContext"/> and will also be the command's parameter.
/// </summary>
public ICommand? CompletedCommand
{
get => (ICommand?)GetValue(CompletedCommandProperty);
set => SetValue(CompletedCommandProperty, value);
}
#endregion
#region Fields
/// <summary>
/// Gets or sets whether hit testing is enabled for pending connections.
/// </summary>
/// <remarks>
/// - When enabled, the <see cref="IsOverElementProperty"/> is updated on connectors during the drag operation. <br />
/// - When disabled, the <see cref="EnablePreview"/> and <see cref="EnableSnapping"/> properties will have no effect. <br />
/// - Disable hit testing to improve performance.
/// </remarks>
public static bool EnableHitTesting { get; set; } = true;
/// <summary>
/// Gets or sets the maximum number of hotkeys that can be displayed for a pending connection.
/// </summary>
/// <remarks>The maximum value can be 9.</remarks>
public static uint MaxHotKeys { get; set; } = 9;
/// <summary>
/// Gets or sets whether hotkeys are enabled for pending connections.
/// </summary>
public static HotKeysDisplayMode HotKeysDisplayMode { get; set; } = HotKeysDisplayMode.Keyboard;
/// <summary>
/// Gets the <see cref="NodifyEditor"/> that owns this <see cref="PendingConnection"/>.
/// </summary>
protected NodifyEditor? Editor { get; private set; }
private FrameworkElement? _connectionTarget;
private Connector? _hotKeysSource;
private readonly List<HotKeyAdorner> _hotKeysAdorners = new List<HotKeyAdorner>();
private AdornerLayer? _adornerLayer;
private AdornerLayer AdornerLayer => _adornerLayer ??= AdornerLayer.GetAdornerLayer(this);
#endregion
static PendingConnection()
{
DefaultStyleKeyProperty.OverrideMetadata(typeof(PendingConnection), new FrameworkPropertyMetadata(typeof(PendingConnection)));
IsHitTestVisibleProperty.OverrideMetadata(typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.False));
IsEnabledProperty.OverrideMetadata(typeof(PendingConnection), new FrameworkPropertyMetadata(BoxValue.False));
}
/// <inheritdoc />
public override void OnApplyTemplate()
{
base.OnApplyTemplate();
if (Editor != null)
{
Editor.RemoveHandler(Connector.PendingConnectionStartedEvent, new PendingConnectionEventHandler(OnPendingConnectionStarted));
Editor.RemoveHandler(Connector.PendingConnectionDragEvent, new PendingConnectionEventHandler(OnPendingConnectionDrag));
Editor.RemoveHandler(Connector.PendingConnectionCompletedEvent, new PendingConnectionEventHandler(OnPendingConnectionCompleted));
Editor.RemoveHandler(PreviewKeyUpEvent, new KeyEventHandler(OnKeyUp));
}
Editor = this.GetParentOfType<NodifyEditor>();
if (Editor != null)
{
Editor.AddHandler(Connector.PendingConnectionStartedEvent, new PendingConnectionEventHandler(OnPendingConnectionStarted));
Editor.AddHandler(Connector.PendingConnectionDragEvent, new PendingConnectionEventHandler(OnPendingConnectionDrag));
Editor.AddHandler(Connector.PendingConnectionCompletedEvent, new PendingConnectionEventHandler(OnPendingConnectionCompleted));
Editor.AddHandler(PreviewKeyUpEvent, new KeyEventHandler(OnKeyUp), true);
SetAllowOnlyConnectorsAttached(Editor, AllowOnlyConnectors);
}
}
#region Event Handlers
protected virtual void OnPendingConnectionStarted(object sender, PendingConnectionEventArgs e)
{
if (!e.Handled && !e.Canceled)
{
e.Handled = true;
e.Canceled = !StartedCommand?.CanExecute(e.SourceConnector) ?? false;
Target = null;
IsVisible = !e.Canceled;
SourceAnchor = e.Anchor;
TargetAnchor = new Point(e.Anchor.X + e.OffsetX, e.Anchor.Y + e.OffsetY);
Source = e.SourceConnector;
if (!e.Canceled)
{
StartedCommand?.Execute(Source);
if (e.OriginalSource is Connector connector)
{
ShowHotKeys(connector);
}
}
if (EnablePreview)
{
PreviewTarget = e.SourceConnector;
}
}
}
protected virtual void OnPendingConnectionDrag(object sender, PendingConnectionEventArgs e)
{
if (!e.Handled && IsVisible)
{
e.Handled = true;
TargetAnchor = new Point(e.Anchor.X + e.OffsetX, e.Anchor.Y + e.OffsetY);
if (!EnableHitTesting)
{
return;
}
// Look for a potential connector
FrameworkElement? target = FindConnectionTarget(TargetAnchor);
// Update the connector's anchor and snap to it, if snapping is enabled
if (EnableSnapping && target is Connector connector)
{
connector.UpdateAnchor();
TargetAnchor = connector.Anchor;
}
SetConnectionTarget(target);
// Update the preview target if enabled
if (EnablePreview)
{
PreviewTarget = target?.DataContext;
}
}
}
protected virtual void OnPendingConnectionCompleted(object sender, PendingConnectionEventArgs e)
{
if (!e.Handled && IsVisible)
{
e.Handled = true;
IsVisible = false;
SetConnectionTarget(null);
HideHotKeys();
if (!e.Canceled)
{
Target = e.TargetConnector;
// Invoke the CompletedCommand if event is not handled
if (CompletedCommand?.CanExecute(Target) ?? false)
{
CompletedCommand?.Execute(Target);
}
}
if (EnablePreview)
{
PreviewTarget = null;
}
}
}
/// <summary>
/// Sets the connection target and updates the visual state of the target element.
/// </summary>
private void SetConnectionTarget(FrameworkElement? target)
{
if (target == _connectionTarget)
{
return;
}
if (_connectionTarget != null)
{
SetIsOverElement(_connectionTarget, false);
}
if (target != null)
{
SetIsOverElement(target, true);
}
_connectionTarget = target;
}
/// <summary>
/// Searches for a potential <see cref="Connector"/> or <see cref="ItemContainer"/> at the specified position within the editor.
/// </summary>
public FrameworkElement? FindConnectionTarget(Point position)
{
if (Editor != null)
{
return GetPotentialConnector(Editor, position, AllowOnlyConnectors);
}
return null;
}
#endregion
#region Hot Keys
private void OnKeyUp(object sender, KeyEventArgs e)
{
if (_hotKeysSource is { IsPendingConnection: true })
{
int hotKey = GetHotKey(e.Key);
if (hotKey <= _hotKeysAdorners.Count)
{
var adorner = _hotKeysAdorners.Find(x => x.Number == hotKey);
if (adorner != null)
{
var tempConnector = _hotKeysSource;
_hotKeysSource.EndConnecting(adorner.Connector);
tempConnector.ReleaseMouseCapture();
e.Handled = true;
}
}
}
}
private static int GetHotKey(Key key)
{
if (key >= Key.D1 && key <= Key.D9)
{
return key - Key.D0;
}
if (key >= Key.NumPad1 && key <= Key.NumPad9)
{
return key - Key.NumPad0;
}
return -1;
}
private void ShowHotKeys(Connector sourceConnector)
{
if (Editor == null
|| AdornerLayer == null
|| HotKeysDisplayMode == HotKeysDisplayMode.None
|| HotKeysDisplayMode == HotKeysDisplayMode.Keyboard && !(InputManager.Current.MostRecentInputDevice is KeyboardDevice))
{
return;
}
_hotKeysSource = sourceConnector;
var connectCommand = Editor.ConnectionCompletedCommand ?? CompletedCommand;
if (connectCommand != null)
{
bool isEditorConnect = connectCommand == Editor.ConnectionCompletedCommand;
var connectorsInViewport = Editor.GetConnectorsInViewport();
var possibleTargets = connectorsInViewport
.Where(x => isEditorConnect ? connectCommand.CanExecute((sourceConnector.DataContext, x.DataContext)) : connectCommand.CanExecute(x.DataContext))
.OrderBy(x => (sourceConnector.Anchor - x.Anchor).LengthSquared)
.Take((int)Math.Min(MaxHotKeys, 9));
var adorners = possibleTargets.Select((x, i) => new HotKeyAdorner(x, i + 1));
foreach (var adorner in adorners)
{
_hotKeysAdorners.Add(adorner);
AdornerLayer.Add(adorner);
}
}
}
private void HideHotKeys()
{
_hotKeysSource = null;
if (AdornerLayer != null)
{
foreach (var hotKeyAdorner in _hotKeysAdorners)
{
AdornerLayer.Remove(hotKeyAdorner);
}
_hotKeysAdorners.Clear();
}
}
#endregion
#region Helpers
/// <summary>
/// Searches for a potential <see cref="Connector"/> or <see cref="ItemContainer"/> at the specified position within the editor.
/// </summary>
/// <param name="editor">The <see cref="NodifyEditor"/> to scan for connectors or item containers.</param>
/// <param name="position">The position in the editor to check for intersections.</param>
/// <param name="allowOnlyConnectors">
/// If true, only <see cref="Connector"/>s are considered; otherwise, the method will also check for <see cref="ItemContainer"/>s.
/// </param>
/// <returns>
/// Returns one of the following, depending on what is found at the specified position:
/// <br /> - A <see cref="Connector"/> if one is present.
/// <br /> - An <see cref="ItemContainer"/> if <paramref name="allowOnlyConnectors"/> is false and a <see cref="Connector"/> is not found.
/// <br /> - The provided <see cref="NodifyEditor"/> itself if neither a <see cref="Connector"/> nor an <see cref="ItemContainer" /> is found, and <paramref name="allowOnlyConnectors"/> is true.
/// <br /> - Null if no valid element is identified at the specified position.
/// </returns>
internal static FrameworkElement? GetPotentialConnector(NodifyEditor editor, Point position, bool allowOnlyConnectors)
{
Connector? connector = editor.ItemsHost.GetElementAtPosition<Connector>(position);
if (connector != null && connector.Editor == editor)
return connector;
if (allowOnlyConnectors)
return null;
var itemContainer = editor.ItemsHost.GetElementAtPosition<ItemContainer>(position);
if (itemContainer != null && itemContainer.Editor == editor)
return itemContainer;
return editor;
}
/// <summary>
/// Searches for a potential <see cref="Connector"/> or <see cref="ItemContainer"/> at the specified position,
/// automatically determining whether to prioritize connectors based on editor settings.
/// </summary>
/// <param name="editor">The <see cref="NodifyEditor"/> to scan.</param>
/// <param name="position">The position in the editor to check for intersections.</param>
/// <returns>
/// Returns a <see cref="Connector"/>, an <see cref="ItemContainer"/>, the <see cref="NodifyEditor"/>, or null.
/// </returns>
internal static FrameworkElement? GetPotentialConnector(NodifyEditor editor, Point position)
=> GetPotentialConnector(editor, position, GetAllowOnlyConnectorsAttached(editor));
#endregion
private class HotKeyAdorner : Adorner
{
private readonly HotKeyControl _hotKeyControl;
public Connector Connector { get; }
public int Number { get; }
private Point _offset;
public HotKeyAdorner(Connector connector, int number) : base(connector)
{
IsHitTestVisible = false;
Connector = connector;
Number = number;
_hotKeyControl = new HotKeyControl
{
Number = number,
DataContext = connector.DataContext
};
AddVisualChild(_hotKeyControl);
AddLogicalChild(_hotKeyControl);
_offset = connector.Thumb.TranslatePoint(new Point(0, 0), connector);
}
protected override int VisualChildrenCount => 1;
protected override Visual GetVisualChild(int index) => _hotKeyControl;
protected override Size MeasureOverride(Size constraint)
{
_hotKeyControl.Measure(constraint);
return _hotKeyControl.DesiredSize;
}
protected override Size ArrangeOverride(Size finalSize)
{
_hotKeyControl.Arrange(new Rect(_offset, finalSize));
return finalSize;
}
}
}
}

View File

@@ -0,0 +1,44 @@
using System.Windows;
using System.Windows.Input;
namespace Nodify.Interactivity
{
public static partial class ConnectorState
{
/// <summary>
/// Represents the state for handling a connector's connecting operation in the editor,
/// enabling drag-based creation of connections between connectors.
/// </summary>
public class Connecting : DragState<Connector>
{
protected override bool HasContextMenu => Element.HasContextMenu;
protected override bool CanCancel => Connector.AllowPendingConnectionCancellation;
protected override bool IsToggle => EnableToggledConnectingMode;
/// <summary>
/// Initializes a new instance of the <see cref="Connecting"/> class.
/// </summary>
/// <param name="connector">The connector associated with this state.</param>
public Connecting(Connector connector)
: base(connector, EditorGestures.Mappings.Connector.Connect, EditorGestures.Mappings.Connector.CancelAction)
{
PositionElement = Element.Editor ?? (IInputElement)Element;
}
protected override void OnBegin(InputEventArgs e)
=> Element.BeginConnecting();
protected override void OnEnd(InputEventArgs e)
=> Element.EndConnecting();
protected override void OnCancel(InputEventArgs e)
=> Element.CancelConnecting();
protected override void OnMouseMove(MouseEventArgs e)
{
Point editorPosition = Element.GetLocationInsideEditor(e); // could also use Element.Editor.MouseLocation
Element.UpdatePendingConnection(editorPosition);
}
}
}
}

View File

@@ -0,0 +1,17 @@
namespace Nodify.Interactivity
{
public static partial class ConnectorState
{
/// <summary>
/// Determines whether toggled connecting mode is enabled, allowing the user to start and end the interaction in two steps with the same input gesture.
/// </summary>
public static bool EnableToggledConnectingMode { get; set; }
internal static void RegisterDefaultHandlers()
{
InputProcessor.Shared<Connector>.RegisterHandlerFactory(elem => new Disconnect(elem));
InputProcessor.Shared<Connector>.RegisterHandlerFactory(elem => new Connecting(elem));
InputProcessor.Shared<Connector>.RegisterHandlerFactory(elem => new Default(elem));
}
}
}

View File

@@ -0,0 +1,31 @@
using System.Windows.Input;
namespace Nodify.Interactivity
{
public static partial class ConnectorState
{
/// <summary>
/// Represents the default input state for a <see cref="Connector"/>.
/// </summary>
public class Default : InputElementState<Connector>
{
/// <summary>
/// Initializes a new instance of the <see cref="Default"/> class.
/// </summary>
/// <param name="connector">The <see cref="Connector"/> element associated with this state.</param>
public Default(Connector connector) : base(connector)
{
}
protected override void OnMouseDown(MouseButtonEventArgs e)
{
// Allow context menu to appear
if (e.ChangedButton == MouseButton.Right && Element.HasContextMenu)
{
Element.Focus();
e.Handled = true; // prevents the editor capturing the mouse
}
}
}
}
}

View File

@@ -0,0 +1,51 @@
using System.Windows.Input;
namespace Nodify.Interactivity
{
public static partial class ConnectorState
{
/// <summary>
/// Represents a state in which a connector can be disconnected from its connections based on specific gestures.
/// </summary>
public class Disconnect : InputElementState<Connector>
{
/// <summary>
/// Initializes a new instance of the <see cref="Disconnect"/> class.
/// </summary>
/// <param name="connector">The <see cref="Connector"/> element associated with this state.</param>
public Disconnect(Connector connector) : base(connector)
{
}
protected override void OnMouseDown(MouseButtonEventArgs e)
{
EditorGestures.ConnectorGestures gestures = EditorGestures.Mappings.Connector;
if (gestures.Disconnect.Matches(e.Source, e))
{
Element.Focus();
e.Handled = true; // prevent interacting with the container
}
}
protected override void OnMouseUp(MouseButtonEventArgs e)
{
EditorGestures.ConnectorGestures gestures = EditorGestures.Mappings.Connector;
if (gestures.Disconnect.Matches(e.Source, e))
{
Element.RemoveConnections();
e.Handled = true; // prevent opening context menu
}
}
protected override void OnKeyDown(KeyEventArgs e)
{
EditorGestures.ConnectorGestures gestures = EditorGestures.Mappings.Connector;
if (gestures.Disconnect.Matches(e.Source, e))
{
Element.RemoveConnections();
e.Handled = true;
}
}
}
}
}